From 77c3f2003a4a92aabd6662df0f7539dfc9bd70ec Mon Sep 17 00:00:00 2001 From: Marco Arnulfo <103679419+marcoarnulfo@users.noreply.github.com> Date: Mon, 12 Oct 2026 13:54:17 +0200 Subject: [PATCH] fix(automation): doc_save reports a required full rewrite as not incremental (#1329) (#1405) doc_save reported "incremental": !full, from the arguments alone. When an in-place save had to be a full rewrite (after doc_protect adds or removes encryption, or after Sanitize), write_incremental fell back to write_full, but the result still said the save was incremental. The result now says whether the written file starts with the file it was saved over, read from the new Document::saved_bytes. An ordinary in-place save still reports true and an explicit full save false. Adds an_in_place_save_reports_a_required_rewrite_as_not_incremental, which fails before this change for both doc_protect and doc_remove_hidden. --- crates/automation/src/lib.rs | 5 ++++- crates/automation/tests/automation.rs | 24 ++++++++++++++++++++++++ crates/engine/src/lib.rs | 6 ++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/crates/automation/src/lib.rs b/crates/automation/src/lib.rs index f29c90478..dc140fc59 100644 --- a/crates/automation/src/lib.rs +++ b/crates/automation/src/lib.rs @@ -863,10 +863,13 @@ impl Automation { self.apply(a, Edit::FlattenFillSign)?; } let bytes = if full { self.session.save_full_bytes(id) } else { self.session.save_bytes(id) }.map_err(failed)?; + // A save that adds or removes encryption, or must not keep the old revision (Sanitize, + // redaction), is rewritten in full even when an incremental save was asked for. + let incremental = !full && bytes.starts_with(&self.doc(a)?.saved_bytes()); write_atomic(&target, &bytes)?; let path = target.to_string_lossy().into_owned(); self.session.mark_saved(id, bytes.clone(), Some(path.clone())).map_err(failed)?; - Ok(json!({ "path": path, "bytes": bytes.len(), "incremental": !full, "document": summary(self.doc(a)?) })) + Ok(json!({ "path": path, "bytes": bytes.len(), "incremental": incremental, "document": summary(self.doc(a)?) })) } fn doc_revisions(&mut self, a: &Args) -> Result { diff --git a/crates/automation/tests/automation.rs b/crates/automation/tests/automation.rs index 1c8f931bf..2c94ae9e3 100644 --- a/crates/automation/tests/automation.rs +++ b/crates/automation/tests/automation.rs @@ -400,6 +400,30 @@ fn edit_undo_redo_and_save_round_trip() { assert_eq!(after.windows(5).filter(|w| w == b"%%EOF").count(), 2); } +/// Issue #1329: an in-place save says it was incremental only when it kept the file it was +/// saving over: protecting or sanitizing rewrites the file, and the result says so. +#[test] +fn an_in_place_save_reports_a_required_rewrite_as_not_incremental() { + let dir = workdir("save-incremental-flag"); + let original = std::fs::read(dir.join("a.pdf")).unwrap(); + let cases = [ + ("page_rotate", json!({ "pages": [1], "degrees": 90 }), true), + ("doc_protect", json!({ "open_password": "openme" }), false), + ("doc_remove_hidden", json!({}), false), + ]; + for (tool, args, incremental) in cases { + std::fs::write(dir.join("a.pdf"), &original).unwrap(); + let mut a = auto(&dir); + let doc = ok(&mut a, "doc_open", json!({ "path": "a.pdf" }))["doc"].as_u64().unwrap(); + let mut args = args; + args["doc"] = json!(doc); + ok(&mut a, tool, args); + let saved = ok(&mut a, "doc_save", json!({ "doc": doc })); + let kept = std::fs::read(dir.join("a.pdf")).unwrap().starts_with(&original); + assert_eq!((saved["incremental"].as_bool(), kept), (Some(incremental), incremental), "{tool}"); + } +} + /// organize.field-name-conflicts: combining files whose forms share a field name keeps the /// fields separate by renaming, says so in the result, and can be told to merge instead; any /// other policy is an argument error. diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index d2479b4ef..700043047 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -695,6 +695,12 @@ impl Document { self.editor.as_ref().map(|e| e.cos.revision_ends()).unwrap_or_default() } + /// The file as last opened or saved, without unsaved edits: what an incremental save appends + /// to. + pub fn saved_bytes(&self) -> Arc> { + self.editor.as_ref().map_or_else(|| self.bytes.clone(), |e| e.cos.bytes().clone()) + } + /// Notes about damage repaired while opening (Document Properties ▸ Advanced, notices). pub fn repair_log(&self) -> Vec { self.editor.as_ref().map(|e| e.cos.repair_log().to_vec()).unwrap_or_default()