fix(table): ignore malformed formula delimiters instead of slicing reversed

Bug:
evaluate_table_formula located "(" with find (forward) and ")" with
rfind (backward), then sliced body[open+1..close]. For malformed input
like =)( the delimiters come out reversed and the slice panics ("slice
index starts at 2 but ends at 0") during tessellation - on the render
path, from a single pasted cell value.

Reproduce:
1. Insert a table and double-click a cell.
2. Type =)( (or =a)b(c ) and press Enter.
3. Before: the next repaint panics.
4. After: the formula does not evaluate; the literal text renders.

Fix:
body.get(open + 1..close)? - reversed delimiters yield None and the
caller's existing fallback shows the literal text. Well-formed formulas
are untouched: regression tests keep =SUM(A1:B2), =AVERAGE(A1:B2) and
=A1 evaluating, and reject =)(, =a)b(c and =).
This commit is contained in:
Karim Jerbi committed 2026-09-30 19:15:49 +01:00
1 parent ee188a6e3c
commit d175861fa4
1 file changed
+51 -1
+51 -1
View File
@@ -505,7 +505,10 @@ fn evaluate_table_formula(table: &Table, expression: &str) -> Option<String> {
let open = body.find('(')?;
let close = body.rfind(')')?;
let function = body[..open].trim().to_ascii_uppercase();
let range = &body[open + 1..close];
// `find('(')` scans forward while `rfind(')')` scans backward, so a
// malformed body like `)(` orders them reversed; the safe `get` yields
// None (fall back to the literal text) instead of a slice panic.
let range = body.get(open + 1..close)?;
let (first, last) = range.split_once(':').unwrap_or((range, range));
let (r1, c1) = table_cell_reference(first)?;
let (r2, c2) = table_cell_reference(last)?;
@@ -3367,3 +3370,50 @@ pub(crate) fn normalize_scripted_table(old: Option<&Table>, new: &mut Table) {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_table() -> Table {
let mut table = Table::new(codec::types::Vector3::new(0.0, 0.0, 0.0), 2, 2);
table.set_cell_text(0, 0, "10");
table.set_cell_text(0, 1, "20");
table.set_cell_text(1, 0, "30");
table.set_cell_text(1, 1, "40");
table
}
/// Delimiters located independently (`find('(')` forward, `rfind(')')`
/// backward) must not produce a reversed slice: `=)(` used to build
/// `&body[2..0]` — `slice index starts at 2 but ends at 0` — killing the
/// render pass from a single pasted cell value.
#[test]
fn malformed_formula_is_ignored_instead_of_panicking() {
let table = sample_table();
for expression in ["=)(", "=a)b(c", "=)"] {
assert_eq!(
evaluate_table_formula(&table, expression),
None,
"{expression} should not evaluate"
);
}
}
/// Well-formed formulas keep evaluating across the whole file — the
/// guard must only reject malformed delimiters.
#[test]
fn well_formed_formulas_still_evaluate() {
let table = sample_table();
assert_eq!(
evaluate_table_formula(&table, "=SUM(A1:B2)"),
Some("100".to_string())
);
assert_eq!(
evaluate_table_formula(&table, "=AVERAGE(A1:B2)"),
Some("25".to_string())
);
assert_eq!(evaluate_table_formula(&table, "=A1"), Some("10".to_string()));
assert_eq!(evaluate_table_formula(&table, "not a formula"), None);
}
}