From 8517c7c48dd27d69a211f83c13ebc9645c28b6b2 Mon Sep 17 00:00:00 2001 From: MarekSpodBiedry <73122968+MarekSpodBiedry@users.noreply.github.com> Date: Mon, 12 Oct 2026 08:32:47 +0200 Subject: [PATCH] xlsx: warn when hyperlinks past the 10,000-cell cap are dropped on read (#513) --- crates/xlsx/src/sheet_read.rs | 5 +++-- crates/xlsx/src/tests/malformed.rs | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/crates/xlsx/src/sheet_read.rs b/crates/xlsx/src/sheet_read.rs index 77b46d95b..3e5c83b32 100644 --- a/crates/xlsx/src/sheet_read.rs +++ b/crates/xlsx/src/sheet_read.rs @@ -104,7 +104,7 @@ pub fn read_sheet(cx: &mut Ctx<'_>, part: &str, name: &str) -> Result read_hyperlinks(e, &rels, &mut sheet), + "hyperlinks" => read_hyperlinks(cx, name, e, &rels, &mut sheet), "autoFilter" => sheet.autofilter = read_autofilter(cx, e), "printOptions" => { sheet.print.gridlines = e.flag("gridLines", false); @@ -709,7 +709,7 @@ fn read_validation(v: &El) -> Option { }) } -fn read_hyperlinks(e: &El, rels: &[Rel], sheet: &mut Sheet) { +fn read_hyperlinks(cx: &mut Ctx<'_>, name: &str, e: &El, rels: &[Rel], sheet: &mut Sheet) { let mut cells = 0u64; for h in e.kids("hyperlink") { let Some(range) = h.attr("ref").and_then(RangeRef::parse) else { continue }; @@ -727,6 +727,7 @@ fn read_hyperlinks(e: &El, rels: &[Rel], sheet: &mut Sheet) { let link = Hyperlink { target, tooltip: h.attr("tooltip").map(str::to_string) }; for c in range.iter() { if cells >= MAX_HYPERLINK_CELLS { + cx.warn(format!("sheet \"{name}\": hyperlinks past the first {MAX_HYPERLINK_CELLS} cells were dropped")); return; } cells += 1; diff --git a/crates/xlsx/src/tests/malformed.rs b/crates/xlsx/src/tests/malformed.rs index b14e53ab8..4700f8bdb 100644 --- a/crates/xlsx/src/tests/malformed.rs +++ b/crates/xlsx/src/tests/malformed.rs @@ -193,3 +193,19 @@ fn hostile_protected_ranges_stay_locked() { assert!(!matches!(h.verify("password"), Ok(true)), "{}", r.title); } } + +#[test] +fn hyperlinks_past_the_cell_cap_are_reported() { + // A styles part, so the only possible warning is the hyperlink one. + let parts = [("xl/styles.xml", r#""#)]; + let rels = r#""#; + let body = r#""#; + let (wb, rep) = read_xlsx(&minimal(body, &parts, rels, "")).unwrap(); + assert_eq!(wb.sheet(0).unwrap().hyperlinks.len(), 10_000); + assert!(rep.warnings.iter().any(|w| w.contains("hyperlinks past the first 10000 cells were dropped")), "{:?}", rep.warnings); + // A small list reads in full with no warning. + let body = r#""#; + let (wb, rep) = read_xlsx(&minimal(body, &parts, rels, "")).unwrap(); + assert_eq!(wb.sheet(0).unwrap().hyperlinks.len(), 3); + assert!(rep.warnings.is_empty(), "{:?}", rep.warnings); +}