From 08aa5112520d9407253b160e6ddd18db97ea5315 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 4 Nov 2026 11:04:38 -0800 Subject: [PATCH] Preferences: a recovery folder that can't be listed is refused Data Recovery lists the folders in its recovery folder before it starts, so a folder that can be written but not listed (a Unix folder with mode 0333) left it unable to start. Choose... now lists the picked folder as well as making a folder in it, and refuses it with a status message when either fails. The progress line says what is checked. --- crates/ui-egui/src/prefs_dialog.rs | 9 +++++---- crates/ui-egui/src/prefs_dialog/tests.rs | 14 ++++++++++++++ docs/target-app-parity.md | 2 +- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/crates/ui-egui/src/prefs_dialog.rs b/crates/ui-egui/src/prefs_dialog.rs index 3aa48bc10..ba00aabbf 100644 --- a/crates/ui-egui/src/prefs_dialog.rs +++ b/crates/ui-egui/src/prefs_dialog.rs @@ -427,17 +427,18 @@ fn choose_recovery_folder(app: &mut VectorcraftApp, d: &mut Dialog) { } } -/// `Ok` when `path` is a folder that a folder can be created in, as the recovery store makes its -/// own there: an empty folder is made there and removed. A folder that can't be removed again is -/// an error too. +/// `Ok` when `path` is a folder that can be listed and that a folder can be created in, as the +/// recovery store lists its folders there and makes its own: the folder is listed, and an empty +/// folder is made there and removed. A folder that can't be removed again is an error too. fn writable_folder(path: &str) -> Result<(), String> { let dir = std::path::Path::new(path); if !dir.is_dir() { return Err(format!("{path} is not a folder")); } + let refused = |e: std::io::Error| format!("Data Recovery can't keep copies in {path}: {e}"); + std::fs::read_dir(dir).map_err(refused)?; let nanos = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |t| t.as_nanos()); let check = dir.join(format!(".vectorcraft-write-check-{}-{nanos}", std::process::id())); - let refused = |e: std::io::Error| format!("Data Recovery can't keep copies in {path}: {e}"); std::fs::create_dir(&check).map_err(refused)?; std::fs::remove_dir(&check).map_err(refused) } diff --git a/crates/ui-egui/src/prefs_dialog/tests.rs b/crates/ui-egui/src/prefs_dialog/tests.rs index 6e8095fcd..ff5dcbca7 100644 --- a/crates/ui-egui/src/prefs_dialog/tests.rs +++ b/crates/ui-egui/src/prefs_dialog/tests.rs @@ -506,6 +506,20 @@ fn a_recovery_folder_that_cannot_be_written_is_refused() { assert!(a.ui.status.starts_with("Data Recovery can't keep copies in "), "{}", a.ui.status); } let _ = std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o755)); + // A folder that can be written but not listed: the recovery store lists its folders. + let unlisted = dir.join("unlisted"); + std::fs::create_dir(&unlisted).unwrap(); + std::fs::set_permissions(&unlisted, std::fs::Permissions::from_mode(0o333)).unwrap(); + // An account that may list anyway (root) has nothing to refuse. + if std::fs::read_dir(&unlisted).is_err() { + let answer = unlisted.to_string_lossy().into_owned(); + a.services.pick_folder = Some(Box::new(move || Some(answer.clone()))); + a.ui.status.clear(); + choose_recovery_folder(&mut a, &mut d); + assert_eq!(d.str("recoveryFolder"), "kept"); + assert!(a.ui.status.starts_with("Data Recovery can't keep copies in "), "{}", a.ui.status); + } + let _ = std::fs::set_permissions(&unlisted, std::fs::Permissions::from_mode(0o755)); } let _ = std::fs::remove_dir_all(&dir); } diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 8bb07e8a6..47f4f1d0b 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1134,7 +1134,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur PDF export writes a linear or radial gradient that mixes a spot color with process colors, or two spot colors, as a DeviceN shading of the spot inks and the process inks its stops use, so each spot ink prints on its own plate; an off-center midpoint is written as a stop with the inks halfway between its two stops. A gradient that mixes a spot color with RGB or Lab colors is still written without the spot ink, with a warning. A Perceptual gradient of spot inks is written as a Separation or DeviceN shading of the stops on its curve; a dithered gradient fill, and a Perceptual gradient with RGB or Lab stops other than white or more than 4,096 stops on its curve, are written as images in process colors, with a warning. A Lab spot color in such a shading is named by the same Separation space, with the Lab alternate, as its flat fills (`engine/src/tests/linked_stops.rs`, `engine/src/tests/labspots.rs`). PDF export writes a spot color named All or None (in a Separation space, the names of every plate and of no plate: [ISO 32000-1:2008](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf), 8.6.6.4) as the ink All Spot or None Spot (All Spot 2 and on when a spot swatch already has that name) in its fills, strokes, gradients, colorized images and Lab alternates; Registration still prints on every plate (`pdf/src/tests/output.rs`). - `.vectorcraft` (lossless JSON, compressed, atomic saves, save down to v1), Save As with a format chooser, Save a Copy, Revert, templates, Data Recovery after a crash (browser tabs hold their copies with Web Locks too, so a background tab's copies are never taken for a crash's, and a tab repairs copies another removed when it resumes: #367), background save and export. The desktop app writes its warnings and errors (graphics adapter, a lost graphics device, panics the guard recovered from) to standard error and to `logs/vectorcraft.log` next to the preferences, the two previous runs kept and each file capped at 16 MiB, so a launch without a terminal still leaves something to attach to a bug report (#465). Native documents with a lot of path data (traced or generated art, in layers, symbols or patterns) are saved as compact JSON, about a fifth of the size; small documents stay pretty-printed (#1492). Saving to a network drive on macOS falls back to a plain fsync when the share refuses a full flush (#1625, part of #1620; not confirmed on a real share), and a save still succeeds when a folder refuses the temporary file, the rename over the document or copying permissions: the content is written elsewhere first and copied in, and a failed copy keeps the temporary file and names it (#1649, part of #1620). A document opened by a relative path finds its relatively linked images, and `vectorcraft-cli convert` lists what reading the input left out as `importWarnings` (#1606, fixes #1604). - Preferences › File Handling › Data Recovery › Folder has a Choose… button on the desktop that picks the folder Data Recovery keeps its copies in ([Illustrator reference, Set Data Recovery preferences](https://web.archive.org/web/20210601162814/https://helpx.adobe.com/pdf/illustrator_reference.pdf)); a picked folder that a file cannot be created in is refused with a status message, and the field keeps its value. + Preferences › File Handling › Data Recovery › Folder has a Choose… button on the desktop that picks the folder Data Recovery keeps its copies in ([Illustrator reference, Set Data Recovery preferences](https://web.archive.org/web/20210601162814/https://helpx.adobe.com/pdf/illustrator_reference.pdf)); a picked folder that Data Recovery cannot create its folders in or list is refused with a status message, and the field keeps its value. A name typed in the desktop save panel without the file type's extension gets it, and when a file of that name exists, which the panel did not ask about, Save, Save As, Save a Copy, Save as Template, the exports and every other write through a save panel ask before replacing it (dialog `replaceFile`; Cancel writes nothing). A name returned by the desktop save panel with the extension of a format the save does not write has it replaced with the saved format's: Save As of a new document saves `card.ait` and `card.png` as `card.vectorcraft`, and Save as Template saves `card.pdf` as `card.vctemplate`. An extension that names no format stays part of the name (`notes.v2` is saved as `notes.v2.vectorcraft`). Export As, Save as PDF, Save for Office Documents, Save for Web, Text Export Options, print to file, the presets' Export… and the library, swatch and Document Info saves replace a typed extension of another format than the one they write in the same way: Export As PNG writes `card.svg` as `card.png`, Save as PDF writes `card.png` as `card.pdf`, and `document.exportSelection` with a `format` suggests and adds that format's extension. An extension of the written format stays as typed in any letter case (`photo.JPEG` for JPEG), Export As SVG writes `art.svgz` compressed, and `document.exportSelection` without a `format` writes the format the typed extension names (`tests/saveext.rs`).