diff --git a/crates/numfmt/src/lib.rs b/crates/numfmt/src/lib.rs index 99c754521..829ce6664 100644 --- a/crates/numfmt/src/lib.rs +++ b/crates/numfmt/src/lib.rs @@ -332,6 +332,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. @@ -425,7 +433,8 @@ 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. +/// 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 { @@ -445,13 +454,7 @@ pub fn text_function(v: &Value, code: &str, sys: DateSystem) -> Result n, // 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); - } + 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 a610954c2..08d86c8ba 100644 --- a/crates/numfmt/src/render.rs +++ b/crates/numfmt/src/render.rs @@ -45,6 +45,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 8a738aab7..097571e66 100644 --- a/crates/numfmt/src/tests.rs +++ b/crates/numfmt/src/tests.rs @@ -400,6 +400,9 @@ fn text_fn() { 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()));