Compare commits

..
Author SHA1 Message Date
Dylan M. Taylor 7db8b5b746 functions: NUMBERVALUE strips trailing percent signs in one pass
Each trailing % copied the rest of the text again, so a long run of
them made one call quadratic. The signs are now counted and cut off in
place.
2026-11-03 12:01:27 +00:00
3 changed files with 15 additions and 27 deletions
+14 -5
View File
@@ -339,11 +339,11 @@ fn numbervalue(a: &[Arg], c: &mut dyn Ctx) -> R<Value> {
if body.is_empty() {
return num_val(0.0);
}
let mut pct = 0;
while let Some(b) = body.strip_suffix('%') {
body = b.to_string();
pct += 1;
}
// Count the trailing percent signs and cut them off in place: copying the rest once per sign
// made a long run of them quadratic.
let kept = body.trim_end_matches('%').len();
let pct = body.len() - kept;
body.truncate(kept);
let mut seen_dec = false;
let mut out = String::new();
for c in body.chars() {
@@ -1084,6 +1084,15 @@ mod tests {
use crate::util::testutil::*;
/// Trailing percent signs each divide by 100, and a long run of them is cut off in one pass.
#[test]
fn numbervalue_percent_signs() {
close(ev("NUMBERVALUE", vec![t("5%%")]), 0.0005);
close(ev("NUMBERVALUE", vec![t("1,5%"), t(","), t(".")]), 0.015);
let many = format!("1{}", "%".repeat(32_000));
close(ev("NUMBERVALUE", vec![t(&many)]), 0.0);
}
#[test]
fn bahttext() {
let bt = |x: f64| ev("BAHTTEXT", vec![n(x)]);
+1 -10
View File
@@ -470,14 +470,6 @@ 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;
@@ -545,8 +537,7 @@ impl Import {
if !entry.cell.is_blank() {
self.cells = self.cells.saturating_add(count);
if let Value::Text(t) = &entry.cell.value {
let repeats = count.saturating_sub(u64::from(entry.repeat));
self.text = self.text.saturating_add(repeats.saturating_mul(t.len() as u64));
self.text = self.text.saturating_add(count.saturating_mul(t.len() as u64));
}
}
let merged = entry.width > 1 || entry.height > 1;
-12
View File
@@ -200,18 +200,6 @@ 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#"<table:table-cell office:value-type="string" office:string-value="{}"/>"#, "x".repeat(1024 * 1024 - 1));
let cells = ods(&format!(r#"<table:table table:name="Data"><table:table-row>{}"#, 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#"<table:table-cell office:value-type="string" office:string-value="{}"/>"#, "y".repeat(1024));
let rows = |n: u32| {
ods(&format!(r#"<table:table table:name="Data"><table:table-row table:number-rows-repeated="{n}">{cell}</table:table-row></table:table>"#))
};
assert!(read_ods(&rows(65_536)).is_ok());
assert!(matches!(read_ods(&rows(65_537)), Err(IoError::TooLarge(_))));
}
#[test]