diff --git a/boa_engine/src/builtins/number/mod.rs b/boa_engine/src/builtins/number/mod.rs index d9043af931a..744bb4001e1 100644 --- a/boa_engine/src/builtins/number/mod.rs +++ b/boa_engine/src/builtins/number/mod.rs @@ -850,7 +850,7 @@ impl Number { /// Checks if the float argument is an integer. #[allow(clippy::float_cmp)] pub(crate) fn is_float_integer(number: f64) -> bool { - number.is_finite() && number.abs().floor() == number.abs() + number.is_finite() && number.trunc() == number } /// The abstract operation `Number::equal` takes arguments diff --git a/boa_engine/src/builtins/string/mod.rs b/boa_engine/src/builtins/string/mod.rs index bcfe708bd81..5fa46a6f2d7 100644 --- a/boa_engine/src/builtins/string/mod.rs +++ b/boa_engine/src/builtins/string/mod.rs @@ -255,22 +255,30 @@ impl String { // b. If ! IsIntegralNumber(nextCP) is false, throw a RangeError exception. if !Number::is_float_integer(nextcp) { return Err(JsNativeError::range() - .with_message(format!("invalid code point: {nextcp}")) + .with_message(format!("codepoint `{nextcp}` is not an integer")) .into()); } // c. If ℝ(nextCP) < 0 or ℝ(nextCP) > 0x10FFFF, throw a RangeError exception. if nextcp < 0.0 || nextcp > f64::from(0x0010_FFFF) { return Err(JsNativeError::range() - .with_message(format!("invalid code point: {nextcp}")) + .with_message(format!("codepoint `{nextcp}` outside of Unicode range")) .into()); } - let nextcp = - char::from_u32(nextcp as u32).expect("Checked above the range of `nextcp`"); + // SAFETY: + // - `nextcp` is not NaN (by the call to `is_float_integer`). + // - `nextcp` is not infinite (by the call to `is_float_integer`). + // - `nextcp` is in the u32 range (by the check above). + let nextcp = unsafe { nextcp.to_int_unchecked::() }; // d. Set result to the string-concatenation of result and ! UTF16EncodeCodePoint(ℝ(nextCP)). - result.extend_from_slice(nextcp.encode_utf16(&mut buf)); + result.extend_from_slice(match u16::try_from(nextcp) { + Ok(ref cp) => std::slice::from_ref(cp), + Err(_) => char::from_u32(nextcp) + .expect("u32 is in range and cannot be a surrogate by the conversion above") + .encode_utf16(&mut buf), + }); } // 3. Assert: If codePoints is empty, then result is the empty String. diff --git a/boa_engine/src/builtins/string/tests.rs b/boa_engine/src/builtins/string/tests.rs index d2a51294e35..45bca332c49 100644 --- a/boa_engine/src/builtins/string/tests.rs +++ b/boa_engine/src/builtins/string/tests.rs @@ -856,3 +856,57 @@ fn search() { TestAction::assert_eq("'ba'.search(/a/)", 1), ]); } + +#[test] +fn from_code_point() { + // Taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/fromCodePoint + run_test_actions([ + TestAction::assert_eq("String.fromCodePoint(42)", "*"), + TestAction::assert_eq("String.fromCodePoint(65, 90)", "AZ"), + TestAction::assert_eq("String.fromCodePoint(0x404)", "Є"), + TestAction::assert_eq( + "String.fromCodePoint(0x2f804)", + js_string!(&[0xD87E, 0xDC04]), + ), + TestAction::assert_eq( + "String.fromCodePoint(0x1D306, 0x1D307)", + js_string!(&[0xD834, 0xDF06, 0xD834, 0xDF07]), + ), + // Should encode to unpaired surrogates + TestAction::assert_eq( + "String.fromCharCode(0xD800, 0xD8FF)", + js_string!(&[0xD800, 0xD8FF]), + ), + TestAction::assert_eq("String.fromCodePoint(9731, 9733, 9842, 0x4F60)", "☃★♲你"), + TestAction::assert_native_error( + "String.fromCodePoint('_')", + ErrorKind::Range, + "codepoint `NaN` is not an integer", + ), + TestAction::assert_native_error( + "String.fromCodePoint(Infinity)", + ErrorKind::Range, + "codepoint `inf` is not an integer", + ), + TestAction::assert_native_error( + "String.fromCodePoint(-1)", + ErrorKind::Range, + "codepoint `-1` outside of Unicode range", + ), + TestAction::assert_native_error( + "String.fromCodePoint(3.14)", + ErrorKind::Range, + "codepoint `3.14` is not an integer", + ), + TestAction::assert_native_error( + "String.fromCodePoint(3e-2)", + ErrorKind::Range, + "codepoint `0.03` is not an integer", + ), + TestAction::assert_native_error( + "String.fromCodePoint(NaN)", + ErrorKind::Range, + "codepoint `NaN` is not an integer", + ), + ]); +}