From e337a496312163a5b7f7741becdab4ee8424dead Mon Sep 17 00:00:00 2001 From: mtlcl410 Date: Thu, 15 Oct 2026 14:26:51 +0800 Subject: [PATCH] fix(a11y): mark_decorative no longer panics on an unpaired ParentTree key (#1809) (#1862) clear_parent_tree walked /Nums with chunks_mut(2) and indexed pair[1], so a trailing key without a value that matched the page's StructParents panicked after the page content had already been replaced. Walk whole pairs only and leave an unpaired key as it is: it maps nothing, so there is nothing to clear. Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz Co-authored-by: Claude Opus 5.5 Co-authored-by: Brandon Thomas --- crates/a11y/src/alt.rs | 11 +++++------ crates/a11y/src/tests.rs | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/crates/a11y/src/alt.rs b/crates/a11y/src/alt.rs index 21bd8448d..71bc90a03 100644 --- a/crates/a11y/src/alt.rs +++ b/crates/a11y/src/alt.rs @@ -414,12 +414,11 @@ fn clear_parent_tree(doc: &mut Document, key: i64, obj: ObjRef) -> Result<(), Al let Some(pt) = tree_root(doc).and_then(|r| r.get(b"ParentTree").and_then(Object::as_ref)) else { return Ok(()) }; let Some(nums) = doc.get(pt).as_dict().and_then(|d| d.get(b"Nums").and_then(|n| n.as_array().cloned())) else { return Ok(()) }; let mut nums = nums; - for pair in nums.chunks_mut(2) { - if pair[0].as_int() == Some(key) { - match &pair[1] { - Object::Array(a) => { - pair[1] = Object::Array(a.iter().map(|x| if x.as_ref() == Some(obj) { Object::Null } else { x.clone() }).collect()) - } + // Whole pairs only: a trailing key without a value maps nothing, so it is left as it is. + for [k, v] in nums.as_chunks_mut::<2>().0 { + if k.as_int() == Some(key) { + match &*v { + Object::Array(a) => *v = Object::Array(a.iter().map(|x| if x.as_ref() == Some(obj) { Object::Null } else { x.clone() }).collect()), Object::Ref(r) => { if let Some(a) = doc.get(*r).as_array().cloned() { let a: Vec = a.iter().map(|x| if x.as_ref() == Some(obj) { Object::Null } else { x.clone() }).collect(); diff --git a/crates/a11y/src/tests.rs b/crates/a11y/src/tests.rs index 6f9deaeb8..6e23c5c6a 100644 --- a/crates/a11y/src/tests.rs +++ b/crates/a11y/src/tests.rs @@ -607,6 +607,23 @@ fn marking_a_leaf_tag_as_an_artifact_preserves_appearance_and_structure_integrit assert_eq!(mark_tag_artifact(&mut doc, pdfcraft_cos::ObjRef::new(7, 0)), Err(ArtifactError::NotATag(7))); } +#[test] +fn marking_decorative_survives_a_parent_tree_with_an_unpaired_key() { + // A trailing key without a value, matching the page's StructParents (#1809), and the same + // with no valid pair at all. + let tree = |nums: &str| pdf(&[format!("<< /Nums {nums} >>")], "").get(pdfcraft_cos::ObjRef::new(1, 0)).as_ref().clone(); + for (nums, cleared) in [("[0 [7 0 R 8 0 R] 0]", "[0 [null 8 0 R] 0]"), ("[0]", "[0]")] { + let mut doc = figures_doc(); + let pt = pdfcraft_cos::ObjRef::new(9, 0); + doc.set(pt, tree(nums)); + let f = figures(&doc); + mark_decorative(&mut doc, f[0].obj).unwrap(); + assert_eq!(figures(&doc).len(), 1, "{nums}"); + // The pair is cleared and the unpaired key is kept as it was. + assert_eq!(*doc.get(pt), tree(cleared), "{nums}"); + } +} + fn ref_(n: u32) -> Option { Some(pdfcraft_cos::ObjRef::new(n, 0)) }