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 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
mtlcl410
2026-10-15 02:26:51 -04:00
committed by GitHub
co-authored by Claude Opus 5.5 Brandon Thomas
parent 0a054a425c
commit e337a49631
2 changed files with 22 additions and 6 deletions
+5 -6
View File
@@ -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<Object> = a.iter().map(|x| if x.as_ref() == Some(obj) { Object::Null } else { x.clone() }).collect();
+17
View File
@@ -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<pdfcraft_cos::ObjRef> {
Some(pdfcraft_cos::ObjRef::new(n, 0))
}