avm2: Re-use code in `coerce_to_number`

Use `string_to_f64` and `string_to_int`, as [`MathUtils::convertStringToNumber`](858d034a3b/core/MathUtils.cpp (L453-L466))
in avmplus does.
This commit is contained in:
relrelb 2022-07-03 20:16:32 +03:00 committed by relrelb
parent 265dcd2e8d
commit 9c1a05aaaf
2 changed files with 293 additions and 330 deletions

View File

@ -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<Object<'gc>>,
@ -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<f64> {
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<Object<'gc>>,
@ -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());
}
}

View File

@ -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<f64> {
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<u32> {
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<i32>) -> Result<i32, Error> {
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)?