diff --git a/core/string/src/str.rs b/core/string/src/str.rs index 59e98d9d57d..3ec6957e823 100644 --- a/core/string/src/str.rs +++ b/core/string/src/str.rs @@ -331,17 +331,15 @@ impl<'a> JsStr<'a> { (Some(b'0'), Some(b'b' | b'B')) => Some(2), (Some(b'0'), Some(b'o' | b'O')) => Some(8), (Some(b'0'), Some(b'x' | b'X')) => Some(16), - // Make sure that no further variants of "infinity" are parsed. - (Some(b'i' | b'I'), _) => { - return f64::NAN; - } _ => None, }; // Parse numbers that begin with `0b`, `0o` and `0x`. if let Some(base) = base { let string = &string[2..]; - if string.is_empty() { + + // Rejects things like `0x+1` or `0o-1` + if string.is_empty() || string.starts_with(['+', '-']) { return f64::NAN; } @@ -362,7 +360,14 @@ impl<'a> JsStr<'a> { return value; } - fast_float2::parse(string).unwrap_or(f64::NAN) + match fast_float2::parse::(string) { + // `Infinity`, `+Infinity` and `-Infinity` already returned above, so any other + // spelling `fast_float2` reads as infinite (`inf`, `+infinity`, ...) is not a + // `StringNumericLiteral`. A decimal literal that overflows does have digits, and + // its `StringNumericValue` is infinite, so it must be kept. + Ok(f) if f.is_finite() || string.bytes().any(|b| b.is_ascii_digit()) => f, + Ok(_) | Err(_) => f64::NAN, + } } /// Gets an iterator of all the Unicode codepoints of a [`JsStr`]. diff --git a/core/string/src/tests.rs b/core/string/src/tests.rs index 0a4f80a602b..c412b20dce0 100644 --- a/core/string/src/tests.rs +++ b/core/string/src/tests.rs @@ -565,3 +565,69 @@ fn starts_with_and_ends_with_basic() { assert!(!basic.starts_with(end_needle)); assert!(basic.ends_with(end_needle)); } + +#[test] +#[allow(clippy::float_cmp)] +fn to_number() { + // `Infinity`, `+Infinity` and `-Infinity` are the only spellings of the infinite + // `StrUnsignedDecimalLiteral`. Every other casing, abbreviation or sign combination is not a + // `StringNumericLiteral` and must be `NaN`. + assert_eq!(JsString::from("Infinity").to_number(), f64::INFINITY); + assert_eq!(JsString::from("+Infinity").to_number(), f64::INFINITY); + assert_eq!(JsString::from("-Infinity").to_number(), f64::NEG_INFINITY); + for invalid in [ + "inf", + "INF", + "Inf", + "infinity", + "+inf", + "-inf", + "+Inf", + "-Inf", + "+INF", + "-INF", + "+infinity", + "-infinity", + "+INFINITY", + "-INFINITY", + "+iNfInItY", + ] { + assert!( + JsString::from(invalid).to_number().is_nan(), + "`{invalid}` is not a `StringNumericLiteral`" + ); + } + + // A `NonDecimalIntegerLiteral` is a bare sequence of digits, so a sign after the prefix is + // invalid. + assert_eq!(JsString::from("0x10").to_number(), 16.0); + assert_eq!(JsString::from("0X10").to_number(), 16.0); + assert_eq!(JsString::from("0b101").to_number(), 5.0); + assert_eq!(JsString::from("0o17").to_number(), 15.0); + // Wider than `u32`, so this takes the slow path. + assert_eq!(JsString::from("0x1FFFFFFFF").to_number(), 8_589_934_591.0); + for invalid in [ + "0x", "0b", "0o", "0x+1", "0x-1", "0x+0", "0b+1", "0b-1", "0o+7", "0o-7", + ] { + assert!( + JsString::from(invalid).to_number().is_nan(), + "`{invalid}` is not a `StringNumericLiteral`" + ); + } + + // `StrWhiteSpace` around the literal is stripped before it is parsed. + assert_eq!(JsString::from("").to_number(), 0.0); + assert_eq!(JsString::from(" \t\n").to_number(), 0.0); + assert_eq!( + JsString::from(" \t-Infinity\n ").to_number(), + f64::NEG_INFINITY + ); + assert!(JsString::from(" -inf ").to_number().is_nan()); + assert!(JsString::from(" 0x+1 ").to_number().is_nan()); + + // A decimal literal too large for `f64` is still a `StringNumericLiteral`; its + // `StringNumericValue` rounds to an infinity and must not be rejected. + assert_eq!(JsString::from("1e400").to_number(), f64::INFINITY); + assert_eq!(JsString::from("-1e400").to_number(), f64::NEG_INFINITY); + assert_eq!(JsString::from("1e999").to_number(), f64::INFINITY); +}