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 <echelon@gmail.com>
This commit is contained in:
Nigel Ihza Firdausy 2026-10-12 01:31:59 +07:00 committed by GitHub
parent 57c9e3a9b4
commit 0059ed657c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 41 additions and 8 deletions

View File

@ -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<usize, AnnotError> {
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 {

View File

@ -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<char> = "See https://example.org/a?b=1, or www.rust-lang.org. Not a.b or http:/x.".chars().collect();