diff --git a/core/src/avm2/globals/toplevel.rs b/core/src/avm2/globals/toplevel.rs index 05d084b44..9b2a8d61e 100644 --- a/core/src/avm2/globals/toplevel.rs +++ b/core/src/avm2/globals/toplevel.rs @@ -54,82 +54,6 @@ pub fn is_nan<'gc>( } } -/// Converts a `WStr` to an integer (as an `f64`). -/// -/// This function might fail for some invalid inputs, by returning `f64::NAN`. -/// -/// `radix` is only valid in the range `2..=36`, plus the special `0` value, which means the -/// radix is inferred from the string; hexadecimal if it starts with a `0x` prefix (case -/// insensitive), or decimal otherwise. -/// `strict` tells whether to fail on trailing garbage, or ignore it. -fn string_to_int(mut s: &WStr, mut radix: i32, strict: bool) -> f64 { - // Allow leading whitespace. - skip_spaces(&mut s); - - let is_negative = parse_sign(&mut s); - - if radix == 16 || radix == 0 { - if let Some(after_0x) = s - .strip_prefix(WStr::from_units(b"0x")) - .or_else(|| s.strip_prefix(WStr::from_units(b"0X"))) - { - // Consume hexadecimal prefix. - s = after_0x; - - // Explicit hexadecimal. - radix = 16; - } else if radix == 0 { - // Default to decimal. - radix = 10; - } - } - - // Fail on invalid radix or blank string. - if !(2..=36).contains(&radix) || s.is_empty() { - return f64::NAN; - } - - // Actual number parsing. - let mut result = 0.0; - let start = s; - s = s.trim_start_matches(|c| { - match u8::try_from(c) - .ok() - .and_then(|c| char::from(c).to_digit(radix as u32)) - { - Some(digit) => { - result *= f64::from(radix); - result += f64::from(digit); - true - } - None => false, - } - }); - - // Fail if we got no digits. - // TODO: Compare by reference instead? - if s.len() == start.len() { - return f64::NAN; - } - - if strict { - // Allow trailing whitespace. - skip_spaces(&mut s); - - // Fail if we got digits, but we're in strict mode and not at end of string. - if !s.is_empty() { - return f64::NAN; - } - } - - // Apply sign. - if is_negative { - result = -result; - } - - result -} - pub fn parse_int<'gc>( activation: &mut Activation<'_, 'gc, '_>, _this: Option>, @@ -145,224 +69,10 @@ pub fn parse_int<'gc>( None => 0, }; - let result = string_to_int(&string, radix, false); + let result = crate::avm2::value::string_to_int(&string, radix, false); Ok(result.into()) } -/// Strips leading whitespace. -fn skip_spaces(s: &mut &WStr) { - *s = s.trim_start_matches(|c| { - matches!( - c, - 0x20 | 0x09 | 0x0d | 0x0a | 0x0c | 0x0b | 0x2000 - ..=0x200b | 0x2028 | 0x2029 | 0x205f | 0x3000 - ) - }); -} - -/// Consumes an optional sign character. -/// Returns whether a minus sign was consumed. -fn parse_sign(s: &mut &WStr) -> bool { - if let Some(after_sign) = s.strip_prefix(b'-') { - *s = after_sign; - true - } else if let Some(after_sign) = s.strip_prefix(b'+') { - *s = after_sign; - false - } else { - false - } -} - -/// Converts a `WStr` to an `f64`. -/// -/// This function might fail for some invalid inputs, by returning `None`. -/// -/// `strict` typically tells whether to behave like `Number()` or `parseFloat()`: -/// * `strict == true` fails on trailing garbage, but interprets blank strings (which are empty or consist only of whitespace) as zero. -/// * `strict == false` ignores trailing garbage, but fails on blank strings. -fn string_to_f64(mut s: &WStr, swf_version: u8, strict: bool) -> Option { - fn is_ascii_digit(c: u16) -> bool { - u8::try_from(c).map_or(false, |c| c.is_ascii_digit()) - } - - // Allow leading whitespace. - skip_spaces(&mut s); - - // Handle blank strings as described above. - if s.is_empty() { - return if strict { Some(0.0) } else { None }; - } - - // Parse sign. - let is_negative = parse_sign(&mut s); - let after_sign = s; - - // Count digits before decimal point. - s = s.trim_start_matches(is_ascii_digit); - let mut total_digits = after_sign.len() - s.len(); - - // Count digits after decimal point. - if let Some(after_dot) = s.strip_prefix(b'.') { - s = after_dot; - s = s.trim_start_matches(is_ascii_digit); - total_digits += after_dot.len() - s.len(); - } - - // Handle exponent. - let mut exponent: i32 = 0; - if let Some(after_e) = s.strip_prefix(b"eE".as_ref()) { - s = after_e; - - // Parse exponent sign. - let exponent_is_negative = parse_sign(&mut s); - - // Fail if string ends with "e-" with no exponent value specified. - if exponent_is_negative && s.is_empty() { - return None; - } - - // Parse exponent itself. - s = s.trim_start_matches(|c| { - match u8::try_from(c) - .ok() - .and_then(|c| char::from(c).to_digit(10)) - { - Some(digit) => { - exponent = exponent.wrapping_mul(10); - exponent = exponent.wrapping_add(digit as i32); - true - } - None => false, - } - }); - - // Apply exponent sign. - if exponent_is_negative { - exponent = exponent.wrapping_neg(); - } - } - - // Allow trailing whitespace. - skip_spaces(&mut s); - - // If we got no digits, check for Infinity/-Infinity. Otherwise fail. - if total_digits == 0 { - if let Some(after_infinity) = s.strip_prefix(WStr::from_units(b"Infinity")) { - s = after_infinity; - - // Allow end of string or a whitespace. Otherwise fail. - if !s.is_empty() { - skip_spaces(&mut s); - // TODO: Compare by reference instead? - if s.len() == after_infinity.len() { - return None; - } - } - - let result = if is_negative { - f64::NEG_INFINITY - } else { - f64::INFINITY - }; - return Some(result); - } - return None; - } - - // Fail if we got digits, but we're in strict mode and not at end of string or at a null character. - if strict && !s.is_empty() && !s.starts_with(b'\0') { - return None; - } - - // Bug compatibility: https://bugzilla.mozilla.org/show_bug.cgi?id=513018 - let s = if swf_version >= 11 { - &after_sign[..after_sign.len() - s.len()] - } else { - after_sign - }; - - // Finally, calculate the result. - let mut result = if total_digits > 15 { - // With more than 15 digits, avmplus uses integer arithmetic to avoid rounding errors. - let mut result: i64 = 0; - let mut decimal_digits = -1; - for c in s { - if let Some(digit) = u8::try_from(c) - .ok() - .and_then(|c| char::from(c).to_digit(10)) - { - if decimal_digits != -1 { - decimal_digits += 1; - } - - result *= 10; - result += i64::from(digit); - } else if c == b'.' as u16 { - decimal_digits = 0; - } else { - break; - } - } - - if decimal_digits > 0 { - exponent -= decimal_digits; - } - - if exponent > 0 { - result *= i64::pow(10, exponent as u32); - } - - result as f64 - } else { - let mut result = 0.0; - let mut decimal_digits = -1; - for c in s { - if let Some(digit) = u8::try_from(c) - .ok() - .and_then(|c| char::from(c).to_digit(10)) - { - if decimal_digits != -1 { - decimal_digits += 1; - } - - result *= 10.0; - result += digit as f64; - } else if c == b'.' as u16 { - decimal_digits = 0; - } else { - break; - } - } - - if decimal_digits > 0 { - exponent -= decimal_digits; - } - - if exponent > 0 { - result *= f64::powi(10.0, exponent); - } - - result - }; - - if exponent < 0 { - if exponent < -307 { - let diff = exponent + 307; - result /= f64::powi(10.0, -diff); - exponent = -307; - } - result /= f64::powi(10.0, -exponent); - } - - // Apply sign. - if is_negative { - result = -result; - } - - Some(result) -} - pub fn parse_float<'gc>( activation: &mut Activation<'_, 'gc, '_>, _this: Option>, @@ -371,7 +81,7 @@ pub fn parse_float<'gc>( if let Some(value) = args.get(0) { let string = value.coerce_to_string(activation)?; let swf_version = activation.context.swf.version(); - if let Some(result) = string_to_f64(&string, swf_version, false) { + if let Some(result) = crate::avm2::value::string_to_f64(&string, swf_version, false) { return Ok(result.into()); } } diff --git a/core/src/avm2/value.rs b/core/src/avm2/value.rs index 8533a8899..12779c87b 100644 --- a/core/src/avm2/value.rs +++ b/core/src/avm2/value.rs @@ -138,6 +138,295 @@ impl PartialEq for Value<'_> { } } +/// Strips leading whitespace. +fn skip_spaces(s: &mut &WStr) { + *s = s.trim_start_matches(|c| { + matches!( + c, + 0x20 | 0x09 | 0x0d | 0x0a | 0x0c | 0x0b | 0x2000 + ..=0x200b | 0x2028 | 0x2029 | 0x205f | 0x3000 + ) + }); +} + +/// Consumes an optional sign character. +/// Returns whether a minus sign was consumed. +fn parse_sign(s: &mut &WStr) -> bool { + if let Some(after_sign) = s.strip_prefix(b'-') { + *s = after_sign; + true + } else if let Some(after_sign) = s.strip_prefix(b'+') { + *s = after_sign; + false + } else { + false + } +} + +/// Converts a `WStr` to an integer (as an `f64`). +/// +/// This function might fail for some invalid inputs, by returning `f64::NAN`. +/// +/// `radix` is only valid in the range `2..=36`, plus the special `0` value, which means the +/// radix is inferred from the string; hexadecimal if it starts with a `0x` prefix (case +/// insensitive), or decimal otherwise. +/// `strict` tells whether to fail on trailing garbage, or ignore it. +pub fn string_to_int(mut s: &WStr, mut radix: i32, strict: bool) -> f64 { + // Allow leading whitespace. + skip_spaces(&mut s); + + let is_negative = parse_sign(&mut s); + + if radix == 16 || radix == 0 { + if let Some(after_0x) = s + .strip_prefix(WStr::from_units(b"0x")) + .or_else(|| s.strip_prefix(WStr::from_units(b"0X"))) + { + // Consume hexadecimal prefix. + s = after_0x; + + // Explicit hexadecimal. + radix = 16; + } else if radix == 0 { + // Default to decimal. + radix = 10; + } + } + + // Fail on invalid radix or blank string. + if !(2..=36).contains(&radix) || s.is_empty() { + return f64::NAN; + } + + // Actual number parsing. + let mut result = 0.0; + let start = s; + s = s.trim_start_matches(|c| { + match u8::try_from(c) + .ok() + .and_then(|c| char::from(c).to_digit(radix as u32)) + { + Some(digit) => { + result *= f64::from(radix); + result += f64::from(digit); + true + } + None => false, + } + }); + + // Fail if we got no digits. + // TODO: Compare by reference instead? + if s.len() == start.len() { + return f64::NAN; + } + + if strict { + // Allow trailing whitespace. + skip_spaces(&mut s); + + // Fail if we got digits, but we're in strict mode and not at end of string. + if !s.is_empty() { + return f64::NAN; + } + } + + // Apply sign. + if is_negative { + result = -result; + } + + // We should only return integers and +/-Infinity. + debug_assert!(result.is_infinite() || result.fract() == 0.0); + result +} + +/// Converts a `WStr` to an `f64`. +/// +/// This function might fail for some invalid inputs, by returning `None`. +/// +/// `strict` typically tells whether to behave like `Number()` or `parseFloat()`: +/// * `strict == true` fails on trailing garbage, but interprets blank strings (which are empty or consist only of whitespace) as zero. +/// * `strict == false` ignores trailing garbage, but fails on blank strings. +pub fn string_to_f64(mut s: &WStr, swf_version: u8, strict: bool) -> Option { + fn is_ascii_digit(c: u16) -> bool { + u8::try_from(c).map_or(false, |c| c.is_ascii_digit()) + } + + fn to_decimal_digit(c: u16) -> Option { + u8::try_from(c) + .ok() + .and_then(|c| char::from(c).to_digit(10)) + } + + // Allow leading whitespace. + skip_spaces(&mut s); + + // Handle blank strings as described above. + if s.is_empty() { + return if strict { Some(0.0) } else { None }; + } + + // Parse sign. + let is_negative = parse_sign(&mut s); + let after_sign = s; + + // Count digits before decimal point. + s = s.trim_start_matches(is_ascii_digit); + let mut total_digits = after_sign.len() - s.len(); + + // Count digits after decimal point. + if let Some(after_dot) = s.strip_prefix(b'.') { + s = after_dot; + s = s.trim_start_matches(is_ascii_digit); + total_digits += after_dot.len() - s.len(); + } + + // Handle exponent. + let mut exponent: i32 = 0; + if let Some(after_e) = s.strip_prefix(b"eE".as_ref()) { + s = after_e; + + // Parse exponent sign. + let exponent_is_negative = parse_sign(&mut s); + + // Fail if string ends with "e-" with no exponent value specified. + if exponent_is_negative && s.is_empty() { + return None; + } + + // Parse exponent itself. + s = s.trim_start_matches(|c| match to_decimal_digit(c) { + Some(digit) => { + exponent = exponent.wrapping_mul(10); + exponent = exponent.wrapping_add(digit as i32); + true + } + None => false, + }); + + // Apply exponent sign. + if exponent_is_negative { + exponent = exponent.wrapping_neg(); + } + } + + // Allow trailing whitespace. + skip_spaces(&mut s); + + // If we got no digits, check for Infinity/-Infinity. Otherwise fail. + if total_digits == 0 { + if let Some(after_infinity) = s.strip_prefix(WStr::from_units(b"Infinity")) { + s = after_infinity; + + // Allow end of string or a whitespace. Otherwise fail. + if !s.is_empty() { + skip_spaces(&mut s); + // TODO: Compare by reference instead? + if s.len() == after_infinity.len() { + return None; + } + } + + let result = if is_negative { + f64::NEG_INFINITY + } else { + f64::INFINITY + }; + return Some(result); + } + return None; + } + + // Fail if we got digits, but we're in strict mode and not at end of string or at a null character. + if strict && !s.is_empty() && !s.starts_with(b'\0') { + return None; + } + + // Bug compatibility: https://bugzilla.mozilla.org/show_bug.cgi?id=513018 + let s = if swf_version >= 11 { + &after_sign[..after_sign.len() - s.len()] + } else { + after_sign + }; + + // Finally, calculate the result. + let mut result = if total_digits > 15 { + // With more than 15 digits, avmplus uses integer arithmetic to avoid rounding errors. + let mut result: i64 = 0; + let mut decimal_digits = -1; + for c in s { + if let Some(digit) = to_decimal_digit(c) { + if decimal_digits != -1 { + decimal_digits += 1; + } + + result *= 10; + result += i64::from(digit); + } else if c == b'.' as u16 { + decimal_digits = 0; + } else { + break; + } + } + + if decimal_digits > 0 { + exponent -= decimal_digits; + } + + if exponent > 0 { + result *= i64::pow(10, exponent as u32); + } + + result as f64 + } else { + let mut result = 0.0; + let mut decimal_digits = -1; + for c in s { + if let Some(digit) = to_decimal_digit(c) { + if decimal_digits != -1 { + decimal_digits += 1; + } + + result *= 10.0; + result += digit as f64; + } else if c == b'.' as u16 { + decimal_digits = 0; + } else { + break; + } + } + + if decimal_digits > 0 { + exponent -= decimal_digits; + } + + if exponent > 0 { + result *= f64::powi(10.0, exponent); + } + + result + }; + + if exponent < 0 { + if exponent < -307 { + let diff = exponent + 307; + result /= f64::powi(10.0, -diff); + exponent = -307; + } + result /= f64::powi(10.0, -exponent); + } + + // Apply sign. + if is_negative { + result = -result; + } + + // We shouldn't return `NaN` after a successful parsing. + debug_assert!(!result.is_nan()); + Some(result) +} + pub fn abc_int(translation_unit: TranslationUnit<'_>, index: Index) -> Result { if index.0 == 0 { return Ok(0); @@ -384,44 +673,8 @@ impl<'gc> Value<'gc> { Value::Unsigned(u) => *u as f64, Value::Integer(i) => *i as f64, Value::String(s) => { - let strim = s.trim(); - if strim.is_empty() { - 0.0 - } else if strim.starts_with(WStr::from_units(b"0x")) - || strim.starts_with(WStr::from_units(b"0X")) - { - let mut n: f64 = 0.0; - for c in &strim[2..] { - let digit = u8::try_from(c).ok().and_then(|c| (c as char).to_digit(16)); - if let Some(digit) = digit { - n = 16.0 * n + f64::from(digit); - } else { - return Ok(f64::NAN); - } - } - - n - } else { - let (sign, digits) = if let Some(stripped) = strim.strip_prefix(b'+') { - (1.0, stripped) - } else if let Some(stripped) = strim.strip_prefix(b'-') { - (-1.0, stripped) - } else { - (1.0, strim) - }; - - if digits == b"Infinity" { - return Ok(sign * f64::INFINITY); - } else if digits.starts_with([b'i', b'I'].as_ref()) { - // Avoid Rust f64::parse accepting "inf" and "infinity" - return Ok(f64::NAN); - } - - //TODO: This is slightly more permissive than ES3 spec, as - //Rust documentation claims it will accept "inf" as f64 - //infinity. - sign * digits.parse().unwrap_or(f64::NAN) - } + let swf_version = activation.context.swf.version(); + string_to_f64(s, swf_version, true).unwrap_or_else(|| string_to_int(s, 0, true)) } Value::Object(_) => self .coerce_to_primitive(Some(Hint::Number), activation)?