Browse Source

Add TextEncoder, TextDecoder implementations to boa_runtime (#3994)

* Add a TextEncoder implementation to boa_runtime

* Add a TextDecoder implementation as well.
pull/4003/head
Hans Larsen 2 months ago committed by GitHub
parent
commit
ddb1901d65
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      Cargo.lock
  2. 1
      core/runtime/Cargo.toml
  3. 5
      core/runtime/src/lib.rs
  4. 124
      core/runtime/src/text.rs
  5. 152
      core/runtime/src/text/tests.rs

1
Cargo.lock generated

@ -535,6 +535,7 @@ version = "0.19.0"
dependencies = [ dependencies = [
"boa_engine", "boa_engine",
"boa_gc", "boa_gc",
"boa_interop",
"indoc", "indoc",
"rustc-hash 2.0.0", "rustc-hash 2.0.0",
"textwrap", "textwrap",

1
core/runtime/Cargo.toml

@ -13,6 +13,7 @@ rust-version.workspace = true
[dependencies] [dependencies]
boa_engine.workspace = true boa_engine.workspace = true
boa_gc.workspace = true boa_gc.workspace = true
boa_interop.workspace = true
rustc-hash = { workspace = true, features = ["std"] } rustc-hash = { workspace = true, features = ["std"] }
[dev-dependencies] [dev-dependencies]

5
core/runtime/src/lib.rs

@ -56,6 +56,11 @@ mod console;
#[doc(inline)] #[doc(inline)]
pub use console::{Console, Logger}; pub use console::{Console, Logger};
mod text;
#[doc(inline)]
pub use text::{TextDecoder, TextEncoder};
#[cfg(test)] #[cfg(test)]
pub(crate) mod test { pub(crate) mod test {
use boa_engine::{builtins, Context, JsResult, JsValue, Source}; use boa_engine::{builtins, Context, JsResult, JsValue, Source};

124
core/runtime/src/text.rs

@ -0,0 +1,124 @@
//! Module implementing JavaScript classes to handle text encoding and decoding.
//!
//! See <https://developer.mozilla.org/en-US/docs/Web/API/Encoding_API> for more information.
use boa_engine::object::builtins::JsUint8Array;
use boa_engine::string::CodePoint;
use boa_engine::{
js_string, Context, Finalize, JsData, JsNativeError, JsObject, JsResult, JsString, Trace,
};
use boa_interop::js_class;
#[cfg(test)]
mod tests;
/// The `TextDecoder`[mdn] class represents an encoder for a specific method, that is
/// a specific character encoding, like `utf-8`.
///
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder
#[derive(Debug, Clone, JsData, Trace, Finalize)]
pub struct TextDecoder;
impl TextDecoder {
/// Register the `TextDecoder` class into the realm.
///
/// # Errors
/// This will error if the context or realm cannot register the class.
pub fn register(context: &mut Context) -> JsResult<()> {
context.register_global_class::<Self>()?;
Ok(())
}
/// The `decode()` method of the `TextDecoder` interface returns a `JsString` containing
/// the given `Uint8Array` decoded in the specific method. This will replace any
/// invalid characters with the Unicode replacement character.
pub fn decode(text: &JsUint8Array, context: &mut Context) -> JsString {
let buffer = text.iter(context).collect::<Vec<u8>>();
let string = String::from_utf8_lossy(&buffer);
JsString::from(string.as_ref())
}
}
js_class! {
class TextDecoder {
property encoding {
fn get() -> JsString {
js_string!("utf-8")
}
}
// Creates a new `TextEncoder` object. Encoding is optional but MUST BE
// "utf-8" if specified. Options is ignored.
constructor(encoding: Option<JsString>, _options: Option<JsObject>) {
if let Some(e) = encoding {
if e != js_string!("utf-8") {
return Err(JsNativeError::typ().with_message("Only utf-8 encoding is supported").into());
}
}
Ok(TextDecoder)
}
fn decode(array: JsUint8Array, context: &mut Context) -> JsString {
TextDecoder::decode(&array, context)
}
}
}
/// The `TextEncoder`[mdn] class represents an encoder for a specific method, that is
/// a specific character encoding, like `utf-8`.
///
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder
#[derive(Debug, Clone, JsData, Trace, Finalize)]
pub struct TextEncoder;
impl TextEncoder {
/// Register the `TextEncoder` class into the realm.
///
/// # Errors
/// This will error if the context or realm cannot register the class.
pub fn register(context: &mut Context) -> JsResult<()> {
context.register_global_class::<Self>()?;
Ok(())
}
/// The `encode()` method of the `TextEncoder` interface returns a `Uint8Array` containing
/// the given string encoded in the specific method.
///
/// # Errors
/// This will error if there is an issue creating the `Uint8Array`.
pub fn encode(text: &JsString, context: &mut Context) -> JsResult<JsUint8Array> {
// TODO: move this logic to JsString.
JsUint8Array::from_iter(
text.code_points().flat_map(|s| match s {
CodePoint::Unicode(c) => c.to_string().as_bytes().to_vec(),
CodePoint::UnpairedSurrogate(_) => "\u{FFFD}".as_bytes().to_vec(),
}),
context,
)
}
}
js_class! {
class TextEncoder {
property encoding {
fn get() -> JsString {
js_string!("utf-8")
}
}
// Creates a new `TextEncoder` object. Encoding is optional but MUST BE
// "utf-8" if specified. Options is ignored.
constructor(encoding: Option<JsString>, _options: Option<JsObject>) {
if let Some(e) = encoding {
if e != js_string!("utf-8") {
return Err(JsNativeError::typ().with_message("Only utf-8 encoding is supported").into());
}
}
Ok(TextEncoder)
}
fn encode(text: JsString, context: &mut Context) -> JsResult<JsUint8Array> {
TextEncoder::encode(&text, context)
}
}
}

152
core/runtime/src/text/tests.rs

@ -0,0 +1,152 @@
use crate::test::{run_test_actions_with, TestAction};
use crate::{TextDecoder, TextEncoder};
use boa_engine::object::builtins::JsUint8Array;
use boa_engine::property::Attribute;
use boa_engine::{js_str, js_string, Context, JsString};
use indoc::indoc;
#[test]
fn encoder_js() {
let context = &mut Context::default();
TextEncoder::register(context).unwrap();
run_test_actions_with(
[
TestAction::run(indoc! {r#"
const encoder = new TextEncoder();
encoded = encoder.encode("Hello, World!");
"#}),
TestAction::inspect_context(|context| {
let encoded = context
.global_object()
.get(js_str!("encoded"), context)
.unwrap();
let array =
JsUint8Array::from_object(encoded.as_object().unwrap().clone()).unwrap();
let buffer = array.iter(context).collect::<Vec<_>>();
assert_eq!(buffer, b"Hello, World!");
}),
],
context,
);
}
#[test]
fn encoder_js_unpaired() {
use crate::test::{run_test_actions_with, TestAction};
use indoc::indoc;
let context = &mut Context::default();
TextEncoder::register(context).unwrap();
let unpaired_surrogates: [u16; 3] = [0xDC58, 0xD83C, 0x0015];
let text = JsString::from(&unpaired_surrogates);
context
.register_global_property(js_str!("text"), text, Attribute::default())
.unwrap();
run_test_actions_with(
[
TestAction::run(indoc! {r#"
const encoder = new TextEncoder();
encoded = encoder.encode(text);
"#}),
TestAction::inspect_context(|context| {
let encoded = context
.global_object()
.get(js_str!("encoded"), context)
.unwrap();
let array =
JsUint8Array::from_object(encoded.as_object().unwrap().clone()).unwrap();
let buffer = array.iter(context).collect::<Vec<_>>();
assert_eq!(buffer, "\u{FFFD}\u{FFFD}\u{15}".as_bytes());
}),
],
context,
);
}
#[test]
fn decoder_js() {
let context = &mut Context::default();
TextDecoder::register(context).unwrap();
run_test_actions_with(
[
TestAction::run(indoc! {r#"
const d = new TextDecoder();
decoded = d.decode(
Uint8Array.from([ 72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33 ])
);
"#}),
TestAction::inspect_context(|context| {
let decoded = context
.global_object()
.get(js_str!("decoded"), context)
.unwrap();
assert_eq!(decoded.as_string(), Some(&js_string!("Hello, World!")));
}),
],
context,
);
}
#[test]
fn decoder_js_invalid() {
use crate::test::{run_test_actions_with, TestAction};
use indoc::indoc;
let context = &mut Context::default();
TextDecoder::register(context).unwrap();
run_test_actions_with(
[
TestAction::run(indoc! {r#"
const d = new TextDecoder();
decoded = d.decode(
Uint8Array.from([ 72, 101, 108, 108, 111, 160, 87, 111, 114, 108, 100, 161 ])
);
"#}),
TestAction::inspect_context(|context| {
let decoded = context
.global_object()
.get(js_str!("decoded"), context)
.unwrap();
assert_eq!(
decoded.as_string(),
Some(&js_string!("Hello\u{FFFD}World\u{FFFD}"))
);
}),
],
context,
);
}
#[test]
fn roundtrip() {
let context = &mut Context::default();
TextEncoder::register(context).unwrap();
TextDecoder::register(context).unwrap();
run_test_actions_with(
[
TestAction::run(indoc! {r#"
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const text = "Hello, World!";
const encoded = encoder.encode(text);
decoded = decoder.decode(encoded);
"#}),
TestAction::inspect_context(|context| {
let decoded = context
.global_object()
.get(js_str!("decoded"), context)
.unwrap();
assert_eq!(decoded.as_string(), Some(&js_string!("Hello, World!")));
}),
],
context,
);
}
Loading…
Cancel
Save