M6: Extract, Split and Combine write no Password-field values either (#3926)

CodeRabbit on #3943: Extract Pages copies the fields of the pages it takes, and
write_new (Combine, Extract, Split, Create) wrote that copy without the writable()
boundary saves use, so the new file kept a typed Password value. write_unchecked, which
write_new and Split's size measuring both go through, now writes the sanitized copy. The
other write_full/write_incremental calls in the engine build in-memory bytes (display,
previews, the working copy, print rasters), not files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mtlcl410
2026-10-23 14:38:29 +08:00
co-authored by Claude Opus 5.5
parent 509b5d5cf3
commit 048cf14d31
2 changed files with 15 additions and 2 deletions
+4 -1
View File
@@ -4915,9 +4915,12 @@ impl Session {
}
/// [`Self::write_new`] without the check, for bytes nobody writes: measuring a part's size.
/// Like a save, a new file never carries a typed Password-field value (#3926): Extract and
/// Split copy the fields of the pages they take.
fn write_unchecked(&self, doc: &pdfcraft_cos::Document) -> Result<Arc<Vec<u8>>, EditError> {
let opts = SaveOptions { mod_date: self.now().map(pdfcraft_cos::pdf_date), ..SaveOptions::default() };
write_full(doc, &opts).map(Arc::new).map_err(|e| EditError::Write(e.to_string()))
let doc = writable(doc)?;
write_full(&doc, &opts).map(Arc::new).map_err(|e| EditError::Write(e.to_string()))
}
/// Whether a changed document still needs a recovery snapshot. This does not consume it.
+11 -1
View File
@@ -2042,7 +2042,8 @@ fn appears_in_pdf(bytes: &Arc<Vec<u8>>, needle: &str) -> bool {
}
/// #3926: a value typed into a text field with the Password flag stays in the open document but
/// is written to no file: Save, a full rewrite, Reduce File Size or a recovery snapshot. ISO
/// is written to no file: Save, a full rewrite, Reduce File Size, a recovery snapshot, or a new
/// file made from its pages (Extract, Split, Combine). ISO
/// 32000-2 §12.7.5.3, Table 231, note to the Password flag: PDF processors should never store
/// the value in the PDF file.
#[test]
@@ -2065,6 +2066,15 @@ fn password_field_values_are_kept_in_memory_but_written_to_no_file() {
let snaps = s.autosave_snapshots();
assert_eq!(snaps.len(), 1);
assert!(!appears_in_pdf(&snaps[0].bytes, "hunter2"), "the recovery snapshot");
// New files built from its pages too (#3943 review): Extract copies the fields of the pages it
// takes, and Split writes each part the same way.
let extracted = s.extract(id, &[0]).unwrap();
assert!(!appears_in_pdf(&extracted, "hunter2"), "Extract Pages");
assert!(appears_in_pdf(&extracted, "*******"), "the extracted page keeps the masked appearance");
for (_, _, part) in s.split_by_size(id, 1).unwrap() {
assert!(!appears_in_pdf(&part, "hunter2"), "Split");
}
assert!(!appears_in_pdf(&s.combine(&[("form.pdf".into(), s.get(id).unwrap().bytes.clone())]).unwrap(), "hunter2"), "Combine");
// Reopened, the field is empty, as in any reader.
let mut t = Session::new();
let reopened = t.open("saved.pdf", None, saved, None).unwrap();