From e5bbb57c5a35b4ea7238b0924de0c851aa390f8e Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Wed, 11 Nov 2026 12:42:58 +0000 Subject: [PATCH] address review: header bytes counted as JSON without serializing them --- crates/expr/src/host.rs | 16 +++++++++++++++- crates/expr/src/tests.rs | 7 +++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/crates/expr/src/host.rs b/crates/expr/src/host.rs index 505eb7146..590089545 100644 --- a/crates/expr/src/host.rs +++ b/crates/expr/src/host.rs @@ -123,6 +123,20 @@ fn cell_json(c: &str) -> serde_json::Value { /// Bytes of header names the row objects of delimited data may repeat in all. const MAX_DATA_JSON_BYTES: usize = 64 << 20; +/// The bytes `s` takes as a JSON string (quotes and `serde_json`'s escapes), counted without +/// building it: a header of control characters must not be serialized in full to be refused. +pub(crate) fn json_string_len(s: &str) -> usize { + let body: usize = s + .chars() + .map(|c| match c { + '"' | '\\' | '\n' | '\r' | '\t' | '\u{8}' | '\u{c}' => 2, + c if u32::from(c) < 0x20 => 6, + c => c.len_utf8(), + }) + .fold(0, usize::saturating_add); + body.saturating_add(2) +} + /// The data of a data footage item as `{data, rows, header}` JSON (see [`Req::FootageData`]). pub fn footage_data_json(f: &effectcraft_project::Footage) -> Option { let text = f.data.as_deref()?; @@ -141,7 +155,7 @@ pub fn footage_data_json(f: &effectcraft_project::Footage) -> Option { // Every row object repeats every distinct header name (a repeated name keeps its last // column), as serialized: a long header over many rows is far more than the text itself. let distinct: std::collections::HashSet<&String> = header.iter().collect(); - let keys: usize = distinct.iter().map(|k| serde_json::to_string(k).map_or(k.len() * 6 + 2, |j| j.len()) + 2).sum(); + let keys: usize = distinct.iter().map(|k| json_string_len(k) + 2).fold(0, usize::saturating_add); if keys.saturating_mul(body.len()) > MAX_DATA_JSON_BYTES { return None; } diff --git a/crates/expr/src/tests.rs b/crates/expr/src/tests.rs index 8777426f6..8b7551a8e 100644 --- a/crates/expr/src/tests.rs +++ b/crates/expr/src/tests.rs @@ -2134,3 +2134,10 @@ fn delimited_data_estimate_counts_escaped_and_distinct_headers() { text.push_str(&"1\n".repeat(140_000)); assert!(crate::host::footage_data_json(&data(text)).is_some()); } + +#[test] +fn json_string_length_matches_the_serialized_one() { + for s in ["", "plain", "a\"b\\c", "tab\tnl\ncr\rbs\u{8}ff\u{c}", "\u{1}\u{1f}\u{7f}", "é界😀", &"\u{1}".repeat(1000)] { + assert_eq!(crate::host::json_string_len(s), serde_json::to_string(s).unwrap().len(), "{s:?}"); + } +}