diff --git a/crates/numfmt/src/lib.rs b/crates/numfmt/src/lib.rs index b41e1bb59..524ff5d47 100644 --- a/crates/numfmt/src/lib.rs +++ b/crates/numfmt/src/lib.rs @@ -393,6 +393,14 @@ impl NumberFormat { None => Formatted { text: t.to_string(), color: None, fill: None, numeric: false }, } } + + /// [`Self::format_text`]'s text, or `None` once it passes `max_units` UTF-16 code units. + fn format_text_within(&self, t: &str, max_units: usize) -> Option { + match self.inner.text_section.and_then(|i| self.inner.sections.get(i)) { + Some(sec) => render::render_text_within(sec, t, max_units).map(|out| out.text), + None => (t.encode_utf16().count() <= max_units).then(|| t.to_string()), + } + } } /// Formats a cell value for display. @@ -486,6 +494,10 @@ 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 UTF-16 code units (Excel's 32,767 characters, which XLSB also +/// stores that way), 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()); @@ -504,7 +516,9 @@ 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 => return fmt.format_text_within(&t, MAX_CELL_TEXT_CHARS).ok_or(CellError::Value), }, Value::Array(_) => return Err(CellError::Value), }; diff --git a/crates/numfmt/src/render.rs b/crates/numfmt/src/render.rs index 19864cdc5..e086ecaa2 100644 --- a/crates/numfmt/src/render.rs +++ b/crates/numfmt/src/render.rs @@ -52,6 +52,35 @@ pub(crate) fn render_text(sec: &Section, text: &str) -> Out { out } +/// [`render_text`], but `None` once the output passes `max_units` UTF-16 code units (a cell's +/// limit), checked before each piece is appended, so an oversized result is never built. +pub(crate) fn render_text_within(sec: &Section, text: &str, max_units: usize) -> Option { + let text_units = text.encode_utf16().count(); + let mut out = Out::default(); + let mut units = 0usize; + for t in &sec.toks { + match t { + Tok::At => { + units = units.saturating_add(text_units); + if units > max_units { + return None; + } + out.push(text); + } + Tok::Fill(c) => out.fill(*c), + other => { + let literal = other.literal_text(); + units = units.saturating_add(literal.encode_utf16().count()); + if units > max_units { + return None; + } + out.push(&literal); + } + } + } + Some(out) +} + /// Renders a number through a General section (literals around the General keyword). pub(crate) fn render_general(sec: &Section, v: f64, width: usize) -> Out { let mut out = Out::default(); diff --git a/crates/numfmt/src/tests.rs b/crates/numfmt/src/tests.rs index d661d7acf..7bbfaf66f 100644 --- a/crates/numfmt/src/tests.rs +++ b/crates/numfmt/src/tests.rs @@ -384,6 +384,13 @@ 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)); + // The limit counts UTF-16 units: 16,384 emoji are 32,768 of them, one too many. + assert_eq!(text_function(&Value::from("\u{1F600}".repeat(16_383).as_str()), "@", S).map(|s| s.chars().count()), Ok(16_383)); + assert_eq!(text_function(&Value::from("\u{1F600}".repeat(16_384).as_str()), "@", 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()));