From 0059ed657c762cd94ffadccb34a7747ab8c7377e Mon Sep 17 00:00:00 2001 From: Nigel Ihza Firdausy <106657905+Ni-zav@users.noreply.github.com> Date: Mon, 12 Oct 2026 01:31:59 +0700 Subject: [PATCH] fix(annot): validate finite link areas before add or edit (#940) * fix(annot): validate finite link areas before add or edit * Test: find the edited link by its /Annots index, not its position in the list --------- Co-authored-by: Brandon Thomas --- crates/annot/src/links.rs | 24 ++++++++++++++++-------- crates/annot/src/tests.rs | 25 +++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/crates/annot/src/links.rs b/crates/annot/src/links.rs index 068a8808..2e45b4e4 100644 --- a/crates/annot/src/links.rs +++ b/crates/annot/src/links.rs @@ -202,13 +202,24 @@ fn write_action(doc: &Document, d: &mut Dict, a: &LinkAction) -> Result<(), Anno Ok(()) } +/// Check link geometry before writing PDF objects: a user or automation call can supply +/// non-finite coordinates or a finite pair whose difference overflows to infinity. +fn checked_rect(rect: [f64; 4]) -> Result<[f64; 4], AnnotError> { + if !rect.iter().all(|v| v.is_finite()) { + return Err(AnnotError::Invalid("the link area must have finite coordinates".into())); + } + let r = [rect[0].min(rect[2]), rect[1].min(rect[3]), rect[0].max(rect[2]), rect[1].max(rect[3])]; + let (width, height) = (r[2] - r[0], r[3] - r[1]); + if !width.is_finite() || !height.is_finite() || width < 2.0 || height < 2.0 { + return Err(AnnotError::Invalid("the link area is too small or too large".into())); + } + Ok(r) +} + /// Add a link over `rect` (user space). Returns its index in the page's `/Annots`. pub fn add(doc: &mut Document, page: usize, rect: [f64; 4], action: &LinkAction, style: &LinkStyle) -> Result { let p = page_ref(doc, page)?; - let r = [rect[0].min(rect[2]), rect[1].min(rect[3]), rect[0].max(rect[2]), rect[1].max(rect[3])]; - if !r.iter().all(|v| v.is_finite()) || r[2] - r[0] < 2.0 || r[3] - r[1] < 2.0 { - return Err(AnnotError::Invalid("the link area is too small".into())); - } + let r = checked_rect(rect)?; if matches!(action, LinkAction::Other(_)) { return Err(AnnotError::Invalid("choose a page or a web address".into())); } @@ -250,10 +261,7 @@ pub fn set( let (_, r) = link_at(doc, page, index)?; let mut d = doc.get(r).as_dict().cloned().unwrap_or_default(); if let Some(rect) = rect { - let rr = [rect[0].min(rect[2]), rect[1].min(rect[3]), rect[0].max(rect[2]), rect[1].max(rect[3])]; - if rr[2] - rr[0] < 2.0 || rr[3] - rr[1] < 2.0 { - return Err(AnnotError::Invalid("the link area is too small".into())); - } + let rr = checked_rect(rect)?; d.set(b"Rect".to_vec(), Object::Array(rr.iter().map(|v| Object::Real(*v)).collect())); } if let Some(s) = style { diff --git a/crates/annot/src/tests.rs b/crates/annot/src/tests.rs index 7e924958..059c495a 100644 --- a/crates/annot/src/tests.rs +++ b/crates/annot/src/tests.rs @@ -574,6 +574,31 @@ fn links_are_added_edited_listed_and_removed() { assert!(links::add(&mut doc, 0, [0.0, 0.0, 1.0, 1.0], &LinkAction::Page(0), &LinkStyle::default()).is_err(), "too small"); } +#[test] +fn link_edit_rejects_non_finite_and_overflowed_rectangles_without_mutating_the_pdf() { + use crate::links::{self, LinkAction, LinkStyle}; + let mut doc = fixture(); + let original_rect = [10.0, 20.0, 110.0, 70.0]; + let index = links::add(&mut doc, 0, original_rect, &LinkAction::Uri("https://example.org".into()), &LinkStyle::default()).unwrap(); + let before = links::list(&doc); + let changed_action = LinkAction::Uri("https://other.example".into()); + for rect in [ + [f64::NAN, 0.0, 100.0, 30.0], + [0.0, 0.0, f64::INFINITY, 30.0], + [f64::NEG_INFINITY, 0.0, 100.0, 30.0], + [-1e308, 0.0, 1e308, 30.0], + [0.0, 0.0, 1.5, 30.0], + ] { + assert!(links::set(&mut doc, 0, index, Some(rect), Some(&changed_action), None).is_err(), "edit accepted {rect:?}"); + assert_eq!(links::list(&doc), before, "invalid edit changed an existing link"); + assert!(links::add(&mut doc, 0, rect, &changed_action, &LinkStyle::default()).is_err(), "add accepted {rect:?}"); + } + // A reversed but otherwise valid rectangle is normalized on edit. + links::set(&mut doc, 0, index, Some([110.0, 70.0, 10.0, 20.0]), None, None).unwrap(); + let edited = links::list(&doc).into_iter().find(|l| l.page == 0 && l.index == index).map(|l| l.rect); + assert_eq!(edited, Some(original_rect)); +} + #[test] fn urls_are_found_in_text() { let t: Vec = "See https://example.org/a?b=1, or www.rust-lang.org. Not a.b or http:/x.".chars().collect();