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]