diff --git a/crates/js/src/builtins/global.rs b/crates/js/src/builtins/global.rs index 1732ce3f6..14f1b6805 100644 --- a/crates/js/src/builtins/global.rs +++ b/crates/js/src/builtins/global.rs @@ -33,13 +33,27 @@ fn trim_start(s: &[u16]) -> &[u16] { s.get(s.iter().position(|&u| !is_str_space(u)).unwrap_or(s.len())..).unwrap_or_default() } -/// ToString of the first argument, as code units, with each one taking a unit of fuel. +/// ToString of the first argument, as code units, with each one taking a unit of fuel. A +/// concatenation is flattened into a new buffer, which is charged to the heap. fn units_of(interp: &mut Interp, args: &[Value]) -> JsResult> { - let s = interp.to_str(&arg(args, 0))?.flat(); + let s = interp.to_str(&arg(args, 0))?; for _ in 0..s.len() { interp.use_fuel()?; } - Ok(s) + let rope = s.buffer_addr().is_none(); + let bytes = crate::heap::string_bytes(s.len()); + if rope { + let mark = interp.temp_roots.len(); + interp.temp_roots.push(Value::String(s.clone())); + let reserved = interp.reserve_heap(bytes); + interp.temp_roots.truncate(mark); + reserved?; + } + let units = s.flat(); + if rope { + interp.heap.bytes = interp.heap.bytes.saturating_add(bytes); + } + Ok(units) } fn digit_value(u: u16) -> Option { @@ -76,19 +90,31 @@ fn parse_int(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult rest = rest.get(2..).unwrap_or_default(); r = 16; } - let digits: Vec = rest.iter().map_while(|&u| digit_value(u).filter(|&d| d < r)).collect(); - if digits.is_empty() { + let n = rest.iter().map_while(|&u| digit_value(u).filter(|&d| d < r)).count(); + let prefix = rest.get(..n).unwrap_or_default(); + if prefix.is_empty() { return Ok(Value::Number(f64::NAN)); } + let digits = prefix.iter().filter_map(|&u| digit_value(u)); let value = match r { - 10 => digits.iter().filter_map(|&d| char::from_digit(d, 10)).collect::().parse::().unwrap_or(f64::NAN), - 2 | 4 | 8 | 16 | 32 => crate::chars::radix_value(digits.iter().copied(), r).unwrap_or(f64::NAN), + 10 => parse_decimal(interp, prefix)?, + 2 | 4 | 8 | 16 | 32 => crate::chars::radix_value(digits, r).unwrap_or(f64::NAN), // Other radices may be approximated (section 15.1.2.2, step 13). - _ => digits.iter().fold(0.0, |n, &d| n * f64::from(r) + f64::from(d)), + _ => digits.fold(0.0, |n, d| n * f64::from(r) + f64::from(d)), }; Ok(Value::Number(sign * value)) } +/// The number that `text`, ASCII decimal digits with an optional fraction and exponent, stands +/// for. The text is copied into a string of one byte per code unit for parsing, and that copy is +/// checked against the heap limit first. +fn parse_decimal(interp: &mut Interp, text: &[u16]) -> JsResult { + interp.reserve_heap(text.len())?; + let mut ascii = String::with_capacity(text.len()); + ascii.extend(text.iter().filter_map(|&u| char::from_u32(u32::from(u)))); + Ok(ascii.parse::().unwrap_or(f64::NAN)) +} + /// The length of the longest prefix of `s` that is a StrDecimalLiteral without its sign and /// without `Infinity`. fn decimal_prefix(s: &[u16]) -> usize { @@ -148,8 +174,7 @@ fn parse_float(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResu if n == 0 { return Ok(Value::Number(f64::NAN)); } - let text = String::from_utf16_lossy(rest.get(..n).unwrap_or_default()); - Ok(Value::Number(sign * text.parse::().unwrap_or(f64::NAN))) + Ok(Value::Number(sign * parse_decimal(interp, rest.get(..n).unwrap_or_default())?)) } /// `isNaN` (section 15.1.2.4). diff --git a/crates/js/src/builtins_tests.rs b/crates/js/src/builtins_tests.rs index c6179254c..ba5454999 100644 --- a/crates/js/src/builtins_tests.rs +++ b/crates/js/src/builtins_tests.rs @@ -329,6 +329,33 @@ fn define_properties_checks_the_heap_before_each_new_property() { assert!(realm.interp().heap.bytes <= heap_bytes, "{} > {heap_bytes}", realm.interp().heap.bytes); } +#[test] +fn number_parsing_counts_its_copy_toward_the_heap_limit() { + // `s` is 1 Mi digits, so parsing it in base 10 copies 1 MiB, which does not fit in the + // 512 KiB left. Bases 16 and 36 read the digits where they are and still fit, but + // flattening a concatenation of `s` makes a 2 MiB buffer, which does not. + let setup = "var s = new Array((1 << 20) + 1).join('1'); var pad = new Array(800000).join('2'); 0"; + let mut measure = small(Limits::default()); + assert_eq!(measure.run(setup), Ok(JsValue::Number(0.0))); + measure.interp().collect_garbage(); + let heap_bytes = measure.interp().heap.bytes + (512 << 10); + assert!(heap_bytes >= 4 << 20, "{heap_bytes}"); + let limits = Limits { heap_bytes, ..Limits::default() }; + let mut realm = small(limits.clone()); + assert_eq!(realm.run(setup), Ok(JsValue::Number(0.0))); + assert_eq!( + realm.run("parseInt(s, 16) > 0 && parseInt(s, 36) > 0 && parseInt('12') === 12 && parseFloat('1.5') === 1.5"), + Ok(JsValue::Bool(true)) + ); + assert_eq!(limit(&mut realm, "parseInt(s)"), LimitKind::Heap); + let mut realm = small(limits.clone()); + assert_eq!(realm.run(setup), Ok(JsValue::Number(0.0))); + assert_eq!(limit(&mut realm, "parseInt(s + '1', 16)"), LimitKind::Heap); + let mut realm = small(limits); + assert_eq!(realm.run(setup), Ok(JsValue::Number(0.0))); + assert_eq!(limit(&mut realm, "parseFloat(s)"), LimitKind::Heap); +} + #[test] fn values_built_ins_hold_survive_collections() { // A small heap makes the callbacks' garbage start collections while the results are built.