From 7c851d7906573d4df66d7b94de50d5123a27deda Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Tue, 3 Nov 2026 12:03:51 +0000 Subject: [PATCH] numfmt: TEXT returns #VALUE! when its result passes a cell's 32,767 characters A text section repeats its input once per @ placeholder, and TEXT handed back whatever that built, so nested TEXT calls could multiply a long text until memory ran out. Results past the cell text limit are now #VALUE!. --- crates/numfmt/src/lib.rs | 13 ++++++++++++- crates/numfmt/src/tests.rs | 4 ++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/crates/numfmt/src/lib.rs b/crates/numfmt/src/lib.rs index 52cada2c7..99c754521 100644 --- a/crates/numfmt/src/lib.rs +++ b/crates/numfmt/src/lib.rs @@ -425,6 +425,9 @@ impl NumberFormat { /// The `TEXT()` worksheet function: formats a value with a format code. Numeric text is /// converted to a number first; other text and booleans (as `TRUE`/`FALSE`) go through the text /// section, or are returned as is. +/// Longest text a cell holds, in characters (Excel's limit), and so the longest `TEXT` result. +const MAX_CELL_TEXT_CHARS: usize = 32_767; + pub fn text_function(v: &Value, code: &str, sys: DateSystem) -> Result { if code.is_empty() { return Ok(String::new()); @@ -440,7 +443,15 @@ pub fn text_function(v: &Value, code: &str, sys: DateSystem) -> Result return Ok(fmt.format_text(if b { "TRUE" } else { "FALSE" }).text), Value::Text(t) => match gridcraft_core::parse::parse_number_text(&t) { Some(n) => n, - None => return Ok(fmt.format_text(&t).text), + // Every `@` repeats the text: a result past a cell's limit is `#VALUE!`, so nested + // TEXT calls can't multiply it further. + None => { + let out = fmt.format_text(&t).text; + if out.len() > MAX_CELL_TEXT_CHARS && out.chars().count() > MAX_CELL_TEXT_CHARS { + return Err(CellError::Value); + } + return Ok(out); + } }, Value::Array(_) => return Err(CellError::Value), }; diff --git a/crates/numfmt/src/tests.rs b/crates/numfmt/src/tests.rs index 4b2ee089d..8a738aab7 100644 --- a/crates/numfmt/src/tests.rs +++ b/crates/numfmt/src/tests.rs @@ -396,6 +396,10 @@ fn text_fn() { assert_eq!(text_function(&Value::Number(1234.5), "$#,##0.00", S), Ok("$1,234.50".into())); assert_eq!(text_function(&Value::from("5"), "0.00", S), Ok("5.00".into())); assert_eq!(text_function(&Value::from("abc"), "0.00", S), Ok("abc".into())); + // Each `@` repeats the text: up to a cell's 32,767 characters, then #VALUE!. + let at = "@".repeat(255); + assert_eq!(text_function(&Value::from("x".repeat(128).as_str()), &at, S).map(|s| s.len()), Ok(128 * 255)); + assert_eq!(text_function(&Value::from("x".repeat(32_000).as_str()), &at, S), Err(CellError::Value)); assert_eq!(text_function(&Value::Error(CellError::NA), "0", S), Err(CellError::NA)); assert_eq!(text_function(&Value::Number(46302.0), "yyyy", S), Ok("2026".into())); assert_eq!(text_function(&Value::Number(0.5), "General", S), Ok("0.5".into()));