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.
This commit is contained in:
Marco Arnulfo authored and GitHub committed 2026-10-12 07:54:17 -04:00
1 parent 240ea53b8b
commit 77c3f2003a
3 files changed
+34 -1

No files matched your search

+4 -1
View File
@@ -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<Value> {
+24
View File
@@ -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.
+6
View File
@@ -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<Vec<u8>> {
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<String> {
self.editor.as_ref().map(|e| e.cos.repair_log().to_vec()).unwrap_or_default()