diff --git a/crates/expr/src/host.rs b/crates/expr/src/host.rs index bce803c56..505eb7146 100644 --- a/crates/expr/src/host.rs +++ b/crates/expr/src/host.rs @@ -138,9 +138,10 @@ pub fn footage_data_json(f: &effectcraft_project::Footage) -> Option { Some((h, b)) => (h.clone(), b.to_vec()), None => (vec![], vec![]), }; - // Every row object repeats every header name: a long header over many rows is far - // more than the text itself. - let keys: usize = header.iter().map(|k| k.len() + 4).sum(); + // 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(); if keys.saturating_mul(body.len()) > MAX_DATA_JSON_BYTES { return None; } diff --git a/crates/expr/src/prelude.js b/crates/expr/src/prelude.js index c545482f3..d1d9bb4b9 100644 --- a/crates/expr/src/prelude.js +++ b/crates/expr/src/prelude.js @@ -1205,10 +1205,13 @@ function nearestKey(t) { return thisProperty.nearestKey(t); } (function () { var nest = __nest, stack = __stack, apply = Reflect.apply, construct = Reflect.construct, str = String; var define = Object.defineProperty, describe = Object.getOwnPropertyDescriptor, proto = Object.getPrototypeOf; + // The original `join`, kept before the guard below replaces it. Descriptors below have a null + // prototype: one that inherits `get`/`set` from a changed `Object.prototype` would be refused. + var join = Array.prototype.join; // Replace `obj[name]` keeping its attributes. function replace(obj, name, f) { var d = describe(obj, name); - define(obj, name, { value: f, writable: d.writable, enumerable: d.enumerable, configurable: true }); + define(obj, name, { __proto__: null, value: f, writable: d.writable, enumerable: d.enumerable, configurable: true }); } function guard(obj, name) { var f = obj[name]; @@ -1232,19 +1235,20 @@ function nearestKey(t) { return thisProperty.nearestKey(t); } // in helpers (`push`, `slice`, `join`, `String`) can be replaced by the expression, and // must not make the check see other text than the constructor gets. var w = function () { - var n = arguments.length, args = [], params = ''; + var n = arguments.length, args = [], heads = []; for (var i = 0; i < n; i++) { var s = str(arguments[i]); if (typeof s !== 'string') s = ''; - define(args, i, { value: s, writable: true, enumerable: true, configurable: true }); - if (i < n - 1) params = i ? params + ',' + s : s; + define(args, i, { __proto__: null, value: s, writable: true, enumerable: true, configurable: true }); + if (i < n - 1) define(heads, i, { __proto__: null, value: s, writable: true, enumerable: true, configurable: true }); } stack(); - nest(params); + // Joined in one pass: adding each piece to a growing string copies it every time. + nest(apply(join, heads, [','])); nest(n ? args[n - 1] : ''); return construct(F, args); }; - define(w, 'name', { value: F.name, configurable: true }); + define(w, 'name', { __proto__: null, value: F.name, configurable: true }); w.prototype = F.prototype; replace(F.prototype, 'constructor', w); return w; diff --git a/crates/expr/src/tests.rs b/crates/expr/src/tests.rs index 80a81f7fc..823d8ecbd 100644 --- a/crates/expr/src/tests.rs +++ b/crates/expr/src/tests.rs @@ -2114,3 +2114,22 @@ fn delimited_data_row_objects_are_bounded() { }; assert!(crate::host::footage_data_json(&f).unwrap().contains("\"a\":1")); } + +/// The row-object estimate counts serialized (escaped) header bytes and each distinct name once. +#[test] +fn delimited_data_estimate_counts_escaped_and_distinct_headers() { + let data = |text: String| effectcraft_project::Footage { + path: "/d.csv".into(), + kind: effectcraft_project::FootageKind::Data, + data: Some(text), + ..Default::default() + }; + // A control character serializes as six bytes: 200 KiB of them over 100 rows is ~120 MiB. + let mut text = format!("{}\n", "\u{1}".repeat(200 << 10)); + text.push_str(&"1\n".repeat(100)); + assert!(crate::host::footage_data_json(&data(text)).is_none()); + // A name repeated across many columns is one key per row object. + let mut text = format!("{}\n", vec!["x"; 100].join(",")); + text.push_str(&"1\n".repeat(140_000)); + assert!(crate::host::footage_data_json(&data(text)).is_some()); +} diff --git a/crates/expr/src/tests/deep_nesting.rs b/crates/expr/src/tests/deep_nesting.rs index 292d23edf..0d379e5fb 100644 --- a/crates/expr/src/tests/deep_nesting.rs +++ b/crates/expr/src/tests/deep_nesting.rs @@ -69,6 +69,12 @@ fn eval_and_function_still_work() { assert_eq!(f.num("Object.getPrototypeOf(function* () { yield 7; }).constructor('yield 7')().next().value", 0.0), 7.0); } +#[test] +fn function_constructor_ignores_changes_to_object_prototype() { + let f = fx(); + assert_eq!(f.num("Object.prototype.get = 1; Object.prototype.set = 1; Function('a', 'b', 'return a + b')(2, 3)", 0.0), 5.0); +} + #[test] fn deep_array_result_is_an_error_or_text_not_a_crash() { // The result itself: detaching it used to `toString` it past depth 4.