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