From d06641a133dfef9dcb54f1d8a688a119e01a0926 Mon Sep 17 00:00:00 2001 From: skylar taylor-barrick Date: Sun, 18 Oct 2026 19:46:41 +0200 Subject: [PATCH] test: document the #3276 regression tests and helpers --- crates/cos/tests/encryption.rs | 5 +++++ crates/engine/src/tests/security_save.rs | 11 +++++++++++ 2 files changed, 16 insertions(+) diff --git a/crates/cos/tests/encryption.rs b/crates/cos/tests/encryption.rs index b3e951f428..93409eb603 100644 --- a/crates/cos/tests/encryption.rs +++ b/crates/cos/tests/encryption.rs @@ -378,11 +378,13 @@ fn removing_an_inline_encrypt_dictionary_leaves_an_unencrypted_file() { // Undo can restore a snapshot whose output security differs from the last saved file (#3276). // Overlaying such a snapshot onto that file must be refused before anything changes. +/// Edit `/Title` in the document information dictionary: an unsaved change to compare. fn set_title(doc: &mut Document, title: &str) { let info = doc.trailer().reference(b"Info").unwrap(); doc.update_dict(info, |d| d.set(b"Title".to_vec(), Object::String(PdfString { bytes: title.as_bytes().to_vec(), hex: false }))).unwrap(); } +/// Protect again with the same passwords and no restrictions; `seed` picks the key and `/ID`. fn rekey(doc: &mut Document, alg: Algorithm, seed: u8) { doc.set_encryption(&NewEncryption { algorithm: alg, @@ -395,6 +397,7 @@ fn rekey(doc: &mut Document, alg: Algorithm, seed: u8) { .unwrap(); } +/// Replace the first file identifier, which is part of the key, and keep the second. fn with_first_id(doc: &mut Document, id: &[u8]) { let Some(Object::Array(ids)) = doc.trailer().get(b"ID").cloned() else { panic!("no /ID") }; let id = Object::String(PdfString { bytes: id.to_vec(), hex: true }); @@ -407,6 +410,8 @@ fn state(doc: &Document) -> (Dict, Vec, Vec, bool, Vec) { (doc.trailer().clone(), doc.object_numbers(), doc.modified_objects(), doc.is_modified(), save) } +/// Every incompatible pairing is refused with `CosError::Security` and leaves the target exactly +/// as it was; the same overlay with matching security does apply. #[test] fn an_overlay_with_different_output_security_is_refused_before_any_change() { let encrypted = Arc::new(protect(Algorithm::Aes256, "user", "owner", -1)); diff --git a/crates/engine/src/tests/security_save.rs b/crates/engine/src/tests/security_save.rs index f012d65008..ca0c8c01cb 100644 --- a/crates/engine/src/tests/security_save.rs +++ b/crates/engine/src/tests/security_save.rs @@ -2,6 +2,8 @@ use super::*; use pdfcraft_cos::{Algorithm, Document as CosDocument, NewEncryption, Object}; +/// A two-page file with a title and XMP metadata, protected with `user`/`owner` and no +/// restrictions. `inline` moves `/Encrypt` into the trailer, as some writers store it. fn encrypted_fixture(algorithm: Algorithm, inline: bool) -> Arc> { let mut doc = CosDocument::open(Arc::new(fixture(2))).unwrap(); pdfcraft_organize::set_info(&mut doc, "Title", "Security regression title").unwrap(); @@ -59,6 +61,7 @@ fn assert_saved_content(s: &Session, id: DocId, bytes: Arc>, password: O Ok(()) } +/// Remove protection, save, undo, save: the file is encrypted again with its original passwords. #[test] fn undo_saved_security_removal_restores_encrypted_content() { let mut failures = Vec::new(); @@ -94,6 +97,7 @@ fn undo_saved_security_removal_restores_encrypted_content() { assert!(failures.is_empty(), "{}", failures.join("\n")); } +/// Protect, save, undo, save: the file is plain again and readable without a password. #[test] fn undo_saved_protection_restores_plain_content() { let mut failures = Vec::new(); @@ -121,6 +125,8 @@ fn undo_saved_protection_restores_plain_content() { assert!(failures.is_empty(), "{}", failures.join("\n")); } +/// Undoing a saved change of algorithm, permissions, metadata encryption or key restores the +/// original security dictionary, even though the password strings never changed. #[test] fn undo_saved_rekey_with_the_same_passwords_restores_original_security() { let mut failures = Vec::new(); @@ -155,6 +161,8 @@ fn undo_saved_rekey_with_the_same_passwords_restores_original_security() { assert!(failures.is_empty(), "{}", failures.join("\n")); } +/// A full save renumbers `/Encrypt`; undoing past it must not leave the restored dictionary +/// encrypted as an ordinary object. #[test] fn undo_encrypted_compaction_restores_the_encrypt_dictionary_exemption() { let mut cos = CosDocument::open(Arc::new(fixture(3))).unwrap(); @@ -220,6 +228,7 @@ fn undo_saved_removal_after_unlock_keeps_the_user_restrictions() { } } +/// Undo across a save that kept the same security still appends to the last saved file. #[test] fn undo_ordinary_encrypted_edits_keeps_the_last_saved_revision() { let mut s = Session::new(); @@ -237,6 +246,8 @@ fn undo_ordinary_encrypted_edits_keeps_the_last_saved_revision() { assert_saved_content(&s, id, rotated, Some("user"), true).unwrap(); } +/// Undoing a saved password change restores the original passwords; the new ones no longer open +/// the file. #[test] fn undo_saved_password_change_restores_original_passwords() { let mut s = Session::new();