From 196231d233d318e046577346cdd19ddccda49b4d Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 14 Nov 2026 14:37:55 -0800 Subject: [PATCH] Save: File > Save checks the files written beside the path against the file read from File > Save to the document's own file planned the save with acknowledgeLoss set, so that only its own path was asked about, and then wrote that plan. With the acknowledgment in the plan, the files written beside the path (an EPS saved with Use Artboards writes one per artboard) were not checked against the file the document was read from. A document opened from one of those artboard files, with things left out when it was read, was written over by File > Save without the question. Save As and Save a Copy already plan again without the acknowledgment before writing; File > Save now does the same. Follow-up to #6751. --- crates/ui-egui/src/io/save.rs | 4 +++- crates/ui-egui/src/tests/save.rs | 31 +++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/io/save.rs b/crates/ui-egui/src/io/save.rs index 6e38e8cc63..10fae638c4 100644 --- a/crates/ui-egui/src/io/save.rs +++ b/crates/ui-egui/src/io/save.rs @@ -263,7 +263,9 @@ pub(super) fn save_current_in(app: &mut VectorcraftApp, p: &Value, folder: Optio return Ok(r); } let retry = super::read_only_save::Retry::new(&first, folder); - write_plan(app, first, ("file.save", p.clone()), Some(retry)) + // Only `path` was asked about: the files written beside it are checked against the file the + // document was read from when the save writes them. + write_plan(app, plan(app, SaveMode::Save, p)?, ("file.save", p.clone()), Some(retry)) } /// File → Save, Save As…, Save a Copy…, Save as Template… and the options dialogs' OK. `p` is the diff --git a/crates/ui-egui/src/tests/save.rs b/crates/ui-egui/src/tests/save.rs index 33e6709913..b9b4111516 100644 --- a/crates/ui-egui/src/tests/save.rs +++ b/crates/ui-egui/src/tests/save.rs @@ -355,6 +355,37 @@ fn a_save_does_not_replace_the_file_read_from_through_a_file_written_beside_the_ let _ = std::fs::remove_dir_all(d); } +#[test] +fn file_save_does_not_replace_the_file_read_from_through_a_file_written_beside_the_path() { + let d = dir("lossy-beside-save"); + let path = d.join("Art.eps").to_string_lossy().to_string(); + let mut app = app(); + checks_files_there(&mut app); + app.run("file.new", json!({"width": 200, "height": 100, "artboards": 2})).unwrap(); + rect(&mut app); + let options = json!({"useArtboards": true, "previewFormat": "none", "thumbnails": false}); + let _ = app.run("file.saveAs", json!({"path": path, "options": options})); + crate::background::wait_all(&mut app); + let siblings = artboard_files(&d); + assert_eq!(siblings.len(), 2, "{siblings:?}"); + let sibling = siblings[0].clone(); + let saved = std::fs::read(&sibling).unwrap(); + std::fs::remove_file(&path).unwrap(); + let st = app.session.doc_mut().unwrap(); + st.imported_from = Some(sibling.clone()); + st.import_losses = vec!["hidden type".into()]; + rect(&mut app); + // File > Save to the document's own file reuses its options, so it writes the artboard files + // again; the one the document was read from is refused, as on Save As. + let _ = app.run("file.save", json!({})); + crate::background::wait_all(&mut app); + assert!(app.ui.dialog.as_ref().is_none_or(|d| d.kind != dialogs::confirm::REPLACE_FILES)); + assert_eq!(std::fs::read(&sibling).unwrap(), saved); + assert!(!std::path::Path::new(&path).exists(), "no file of the set is written when one is refused"); + assert!(app.session.active().unwrap().is_dirty()); + let _ = std::fs::remove_dir_all(d); +} + /// The app checks for files that are there, as the desktop app does. fn checks_files_there(app: &mut VectorcraftApp) { app.services.exists = Some(std::sync::Arc::new(|p: &str| std::path::Path::new(p).exists()));