Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions core/string/src/str.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -362,7 +360,14 @@ impl<'a> JsStr<'a> {
return value;
}

fast_float2::parse(string).unwrap_or(f64::NAN)
match fast_float2::parse::<f64, &str>(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`].
Expand Down
66 changes: 66 additions & 0 deletions core/string/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}