mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 07:16:52 +00:00
JavaScript: parseInt and parseFloat count their copies toward the heap limit
parseInt collected every digit into a vector outside the heap count and then copied the digits again for a base-10 parse, and parseFloat copied its whole decimal prefix the same way, so one call on a long string could allocate more than the heap limit. parseInt now counts the digits in place, reads bases other than 10 straight from the string, and both functions reserve heap for the base-10 copy before they make it. A concatenation that the global functions flatten is charged to the heap, as sort keys are.
This commit is contained in:
@@ -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<std::rc::Rc<[u16]>> {
|
||||
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<u32> {
|
||||
@@ -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<u32> = 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::<String>().parse::<f64>().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<f64> {
|
||||
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::<f64>().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::<f64>().unwrap_or(f64::NAN)))
|
||||
Ok(Value::Number(sign * parse_decimal(interp, rest.get(..n).unwrap_or_default())?))
|
||||
}
|
||||
|
||||
/// `isNaN` (section 15.1.2.4).
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user