From 01dee465614be81b76f4b7036c4915b071553c4d Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Tue, 3 Nov 2026 11:15:58 +0000 Subject: [PATCH] ods: charge a cell's text when the cell is read, not when its row ends The importer kept every cell of a row and only charged their text to the 64 MiB allowance when the row finished, so one row of long cells could hold up to 16,384 MiB before being refused. Text is now charged as each cell finishes; the row charges only its repeats. --- crates/xlsx/src/ods.rs | 11 ++++++++++- crates/xlsx/src/tests/ods_tests.rs | 12 ++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/crates/xlsx/src/ods.rs b/crates/xlsx/src/ods.rs index 8415ce395..be9eb79bd 100644 --- a/crates/xlsx/src/ods.rs +++ b/crates/xlsx/src/ods.rs @@ -470,6 +470,14 @@ impl Import { None => return Err(IoError::TooLarge("ODS row exceeds worksheet width".into())), }; if kept { + // Charge the text a row keeps before the row is done, so a row of many long cells is + // refused as it grows; finish_row charges the row's repeats beyond the first. + if let Value::Text(t) = &cell.value { + self.text = self.text.saturating_add(u64::from(draft.repeat).saturating_mul(t.len() as u64)); + if self.text > MAX_TEXT { + return Err(IoError::TooLarge("ODS expands beyond the import limit (64 MiB of text)".into())); + } + } row.cells.push(RepeatedCell { col: row.col, repeat: draft.repeat, width, height, cell }); } row.col = end; @@ -537,7 +545,8 @@ impl Import { if !entry.cell.is_blank() { self.cells = self.cells.saturating_add(count); if let Value::Text(t) = &entry.cell.value { - self.text = self.text.saturating_add(count.saturating_mul(t.len() as u64)); + let repeats = count.saturating_sub(u64::from(entry.repeat)); + self.text = self.text.saturating_add(repeats.saturating_mul(t.len() as u64)); } } let merged = entry.width > 1 || entry.height > 1; diff --git a/crates/xlsx/src/tests/ods_tests.rs b/crates/xlsx/src/tests/ods_tests.rs index 47c258657..bac55efeb 100644 --- a/crates/xlsx/src/tests/ods_tests.rs +++ b/crates/xlsx/src/tests/ods_tests.rs @@ -200,6 +200,18 @@ fn ods_expansion_bounds_stop_small_repeat_bombs() { "x".repeat(1024) ); assert!(matches!(read_ods(&ods(&text)), Err(IoError::TooLarge(_)))); + // One row of cells each just under the 1 MiB cell limit, past 64 MiB in all: refused while + // the row is read, before it holds them all. The row's end is never reached. + let long = format!(r#""#, "x".repeat(1024 * 1024 - 1)); + let cells = ods(&format!(r#"{}"#, long.repeat(65))); + assert!(matches!(read_ods(&cells), Err(IoError::TooLarge(_)))); + // A row repeated once more than its cells' text allows is refused too, and one that fits reads. + let cell = format!(r#""#, "y".repeat(1024)); + let rows = |n: u32| { + ods(&format!(r#"{cell}"#)) + }; + assert!(read_ods(&rows(65_536)).is_ok()); + assert!(matches!(read_ods(&rows(65_537)), Err(IoError::TooLarge(_)))); } #[test]