diff --git a/crates/engine/src/cmd/pdflayers.rs b/crates/engine/src/cmd/pdflayers.rs index e5cca382..937fe81c 100644 --- a/crates/engine/src/cmd/pdflayers.rs +++ b/crates/engine/src/cmd/pdflayers.rs @@ -263,6 +263,28 @@ pub(crate) mod tests { s.execute("file.exportPdf", &json!({})).unwrap(); } + #[test] + fn unreadable_non_null_prev_entries_refuse_layer_rewrites() { + for prev in ["7 0 R", "-1", "false", "<<>>", "[]", "/Unknown", "(unknown)"] { + let bytes = two_layer_pdf_options(0, "", &format!("/Prev {prev}")); + assert_eq!(designcraft_render::pdf_layers(&bytes).len(), 2, "reader control: {prev}"); + assert!(designcraft_render::pdf_hide_layers(&bytes, &["Red".into()]).is_none(), "must not truncate unknown history: {prev}"); + } + for trailer in ["", "/Prev null"] { + let bytes = two_layer_pdf_options(0, "", trailer); + assert!(designcraft_render::pdf_hide_layers(&bytes, &["Red".into()]).is_some(), "end-of-chain control: {trailer}"); + } + let mut revised = two_layer_pdf(); + let prev = + std::str::from_utf8(&revised).unwrap().rsplit_once("startxref").unwrap().1.split_whitespace().next().unwrap().parse::().unwrap(); + add_pdf_revision(&mut revised); + let real_prev = std::str::from_utf8(&revised).unwrap().replace(&format!("/Prev {prev}>>"), &format!("/Prev {prev}.0>>")); + assert!(real_prev.contains(&format!("/Prev {prev}.0>>"))); + for bytes in [revised, real_prev.into_bytes()] { + assert!(designcraft_render::pdf_hide_layers(&bytes, &["Red".into()]).is_some(), "readable integer/real history must still work"); + } + } + #[test] fn fake_null_trailer_cannot_bypass_hybrid_rejection() { for real_guard in ["", "/XRefStm 0"] { diff --git a/crates/render/src/pdf_layers.rs b/crates/render/src/pdf_layers.rs index 47f93c06..2573598d 100644 --- a/crates/render/src/pdf_layers.rs +++ b/crates/render/src/pdf_layers.rs @@ -176,6 +176,9 @@ pub fn pdf_hide_layers(bytes: &[u8], hidden: &[String]) -> Option> { return None; } offset = t.get::(b"Prev"); + if offset.is_none() && has_non_null_entry(&t, b"Prev") { + return None; + } } if offset.is_some() { return None;