From 0e631e20dabaf022eb3e83b0a60025898cdd8bb9 Mon Sep 17 00:00:00 2001
From: prxnvt <53838599+prxnvt@users.noreply.github.com>
Date: Sat, 31 Oct 2026 13:02:48 -0400
Subject: [PATCH] XLSX: a hostile-file test for form controls
A sheet with more controls than a sheet keeps (10,000), properties
parts that are not XML, are missing or hold numbers no control can take
(1e400, NaN, -inf, 2^64), shape ids that are not numbers, anchors far
outside the grid, and oversized names and links. Reading keeps exactly
the cap, every control passes the model's rules, and a save and reopen
keeps them all.
---
crates/xlsx/src/tests/controls_tests.rs | 50 +++++++++++++++++++++++++
1 file changed, 50 insertions(+)
diff --git a/crates/xlsx/src/tests/controls_tests.rs b/crates/xlsx/src/tests/controls_tests.rs
index a4f97439a..f8aa60d08 100644
--- a/crates/xlsx/src/tests/controls_tests.rs
+++ b/crates/xlsx/src/tests/controls_tests.rs
@@ -232,3 +232,53 @@ fn the_properties_part_wins_over_the_vml() {
let (back, _) = read_xlsx(&zipped).unwrap();
assert_eq!(back.sheet(0).unwrap().controls[0].max, 500);
}
+
+/// A hostile sheet: more controls than a sheet keeps, properties parts that are not XML or hold
+/// numbers no control can take, shape ids that are not numbers, and oversized names, captions and
+/// links. Reading must not panic, must keep at most the cap, and every control it keeps must pass
+/// the model's rules.
+#[test]
+fn hostile_form_controls_are_capped_and_repaired() {
+ let n = 13_000;
+ let mut controls = String::from("");
+ let anchor = r#"1-99999999999999999999990-4020"#;
+ for i in 0..n {
+ let rid = match i % 10 {
+ 0 => "rIdB",
+ 1 => "rIdC",
+ _ => "rIdA",
+ };
+ let _ = std::fmt::Write::write_fmt(
+ &mut controls,
+ format_args!(r#"{anchor}"#, 2000 + i, "n".repeat(400)),
+ );
+ }
+ controls
+ .push_str(r#""#);
+ let rels = r#""#;
+ let link = format!("$B${}", "9".repeat(500));
+ let prop_a = format!(
+ r#""#
+ );
+ let body = format!(r#"{controls}"#)
+ .replace("", r#""#);
+ let bytes = minimal(
+ &body,
+ &[("xl/worksheets/_rels/sheet1.xml.rels", rels), ("xl/ctrlProps/a.xml", &prop_a), ("xl/ctrlProps/b.xml", "\u{0}\u{1}not xml <<<")],
+ "",
+ "",
+ );
+ let (wb, _) = read_xlsx(&bytes).unwrap();
+ let c = &wb.sheet(0).unwrap().controls;
+ assert_eq!(c.len(), 10_000, "the cap of one sheet");
+ for x in c {
+ assert_eq!(x.kind, ControlKind::Slider);
+ assert!(x.check_numbers().is_ok(), "{x:?}");
+ assert!(x.value >= x.min && x.value <= x.max, "{x:?}");
+ assert!(x.name.chars().count() <= 255 && x.link.is_none(), "{x:?}");
+ assert!(x.anchor.width >= 1.0 && x.anchor.height >= 1.0 && x.anchor.cell.row < gridcraft_core::MAX_ROWS, "{x:?}");
+ }
+ // Written back and read again, the kept controls stay valid.
+ let (back, _) = read_xlsx(&write_xlsx(&wb).unwrap()).unwrap();
+ assert_eq!(back.sheet(0).unwrap().controls.len(), c.len());
+}