From 82f0b6f8a0b2f07acb9e40d1bf2a080089fee4b4 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Sat, 24 Oct 2026 16:30:38 +0200 Subject: [PATCH] Reject non-finite numbers in XFDF coordinate lists on import (#4449) Found while testing crates/xfdf/src/lib.rs:274-276 (csv) used by annot_from_xml at :924-928 Co-authored-by: Claude Haiku 5.5 --- crates/xfdf/src/lib.rs | 4 ++- crates/xfdf/src/tests.rs | 59 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/crates/xfdf/src/lib.rs b/crates/xfdf/src/lib.rs index a6da88f4d..6131c9c96 100644 --- a/crates/xfdf/src/lib.rs +++ b/crates/xfdf/src/lib.rs @@ -271,8 +271,10 @@ fn text_of(doc: &Document, d: &Dict, k: &[u8]) -> Option { }) } +/// The numbers of a list; empty when any entry is non-finite (`nan`, `inf`), so the list is unusable as a whole. fn csv(list: &str) -> Vec { - list.split([',', ';', ' ']).filter(|t| !t.is_empty()).filter_map(|t| t.trim().parse().ok()).collect() + let v: Vec = list.split([',', ';', ' ']).filter(|t| !t.is_empty()).filter_map(|t| t.trim().parse().ok()).collect(); + if v.iter().all(|x| x.is_finite()) { v } else { Vec::new() } } fn arr(v: &[f64]) -> Object { diff --git a/crates/xfdf/src/tests.rs b/crates/xfdf/src/tests.rs index 80e7d8049..17c48c281 100644 --- a/crates/xfdf/src/tests.rs +++ b/crates/xfdf/src/tests.rs @@ -1874,3 +1874,62 @@ fn a_comment_locked_by_an_indirect_flag_is_not_replaced_by_an_import() { assert!(annots.contains(&Object::Ref(note)), "the locked comment was replaced"); assert_eq!(doc.get(note), before, "the locked comment was changed"); } + +#[test] +fn import_skips_annotations_with_non_finite_coordinates() { + for rect in ["nan,10,100,100", "0,inf,100,100", "0,10,infinity,100", "0,10,100,-inf"] { + let mut doc = blank(); + let xfdf = format!( + r#""# + ); + let r = import(&mut doc, xfdf.as_bytes()).unwrap(); + assert_eq!(r.comments, 1, "rect {rect}: {r:?}"); + let names = annots_of(&doc); + assert_eq!(names.len(), 1, "rect {rect}: {names:?}"); + assert_eq!(names[0].get(b"NM"), Some(&PdfString::literal(b"good".to_vec()).into()), "rect {rect}"); + } +} + +#[test] +fn import_drops_non_finite_numbers_from_every_coordinate_list() { + let xfdf = r#" + + 1,2;inf,4;5,6 + 1,2;3,-inf + + + "#; + let mut doc = blank(); + let r = import(&mut doc, xfdf.as_bytes()).unwrap(); + assert_eq!(r.comments, 5, "{r:?}"); + fn finite(doc: &Document, o: &Object) -> bool { + match &*doc.resolve(o) { + Object::Real(x) => x.is_finite(), + Object::Array(a) => a.iter().all(|e| finite(doc, e)), + _ => true, + } + } + let annots = annots_of(&doc); + assert_eq!(annots.len(), 5); + for d in &annots { + for (k, v) in d.iter() { + if k.as_slice() != b"P" { + assert!(finite(&doc, v), "{d:?}"); + } + } + assert!(d.get(b"L").is_none() && d.get(b"CL").is_none() && d.get(b"Vertices").is_none(), "{d:?}"); + } +} + +fn annots_of(doc: &Document) -> Vec { + let page = &pdfcraft_model::pages(doc)[0]; + page.dict + .get(b"Annots") + .map(|a| doc.resolve(a)) + .and_then(|a| a.as_array().cloned()) + .unwrap_or_default() + .iter() + .map(|a| doc.resolve(a).as_dict().cloned().unwrap()) + .filter(|d| d.get(b"Subtype") != Some(&Object::name("Widget"))) + .collect() +}