mirror of https://github.com/boa-dev/boa.git
Browse Source
* Add a TextEncoder implementation to boa_runtime * Add a TextDecoder implementation as well.pull/4003/head
Hans Larsen
2 months ago
committed by
GitHub
5 changed files with 283 additions and 0 deletions
@ -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) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -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…
Reference in new issue