fix(pdf): drop an oversized hyperlink before it is copied once per line

Every linked line copied the destination string, and its length was only
checked when the annotation was written, so one multi-megabyte link on text
wrapping onto many lines was copied that many times first. The length is
checked where the link areas are collected.
This commit is contained in:
Dylan M. Taylor committed 2026-11-03 04:23:05 +00:00
1 parent 02c38a6d95
commit 5bbab0fa18
2 files changed
+20 -1

No files matched your search

+17
View File
@@ -284,3 +284,20 @@ fn depth_samples() {
std::fs::write(format!("{stem}-{name}.png"), figcraft_render::encode_png(&pix).unwrap()).unwrap();
}
}
#[test]
fn an_oversized_link_is_dropped_before_it_is_copied_per_line() {
// One link far past MAX_URL on text that wraps onto many lines: no annotation, and the
// destination is never copied once per line on the way to being refused.
let mut d = Document::new("long link");
let p = d.first_page().unwrap_or(NodeId(0));
let f = d.create(p, NodeKind::Frame, "F", 0.0, 0.0, 300.0, 4000.0).unwrap();
let words = "word ".repeat(2_000);
let mut td = TextData::new(words.trim_end());
td.style.font_size = 20.0;
td.style.link = Some("https://example.com/".to_string() + &"a".repeat(1 << 20));
let t = d.create(f, NodeKind::Text(td), "T", 10.0, 10.0, 280.0, 3980.0).unwrap();
d.get_mut(t).unwrap().fills = vec![Paint::solid(Color::BLACK)];
let pdf = export_pdf(&d, &[f], &PdfOptions::default()).unwrap();
assert_eq!(text(&pdf).matches("/Subtype/Link").count(), 0);
}
+3 -1
View File
@@ -111,7 +111,9 @@ fn record_links(ex: &mut Exporter, layout: &TextLayout, gxf: Affine) {
let glyphs = layout.glyphs.get(line.glyph_start..line.glyph_end).unwrap_or(&[]);
for g in glyphs.iter().map(Some).chain([None]) {
let link = g.and_then(|g| {
let url = layout.styles.get(g.style)?.link.as_deref().filter(|u| !u.trim().is_empty())?;
// Checked here, not when the annotation is written: every linked line copies
// the destination, so one oversized link must not be copied at all.
let url = layout.styles.get(g.style)?.link.as_deref().map(str::trim).filter(|u| !u.is_empty() && u.len() <= crate::MAX_URL)?;
let (a, b) = (g.x, g.x + g.advance);
(a.is_finite() && b.is_finite()).then(|| (url, a.min(b), a.max(b)))
});