mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-24 10:16:59 -06:00
M6: reject unreadable PDF trailer history links (#1670)
Reject a non-null Prev entry that cannot be read as an offset instead of silently treating it as the end of the guarded trailer chain. Keep absent/null end markers and readable numeric offsets unchanged. Reproduced CodeRabbit discussion_r4366245854 with an independent probe and a failing native regression before the fix. The synthetic regression covers seven unreadable value types plus absent/null and integer/real history controls. Validation: all 9 cargo xtask ci gates passed (3063 tests passed, 6 skipped; 16 WASM-safe crates). One unrelated GPU test was labelled leaky by nextest in that run and passed cleanly in an isolated rerun. Independent checks: 17 original supported PDF variants still pass qpdf, logical preservation and Poppler pixels; 6 original refusal cases remain refused. All 11 additional Prev controls pass (7 refused, 4 accepted).
This commit is contained in:
@@ -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::<usize>().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"] {
|
||||
|
||||
@@ -176,6 +176,9 @@ pub fn pdf_hide_layers(bytes: &[u8], hidden: &[String]) -> Option<Vec<u8>> {
|
||||
return None;
|
||||
}
|
||||
offset = t.get::<usize>(b"Prev");
|
||||
if offset.is_none() && has_non_null_entry(&t, b"Prev") {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
if offset.is_some() {
|
||||
return None;
|
||||
|
||||
Reference in New Issue
Block a user