Skip to content
Merged
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
48 changes: 48 additions & 0 deletions crates/wasm-rquickjs/skeleton/src/builtin/abort_signal.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use futures::future::{AbortHandle, Abortable};
use rquickjs::function::This;
use rquickjs::{Ctx, Persistent, Value};

pub(crate) async fn with_abort_signal<'js, F, T>(
ctx: &Ctx<'js>,
signal: Option<Value<'js>>,
future: F,
) -> rquickjs::Result<T>
where
F: Future<Output = rquickjs::Result<T>>,
{
let signal = match signal {
Some(signal) if !signal.is_undefined() && !signal.is_null() => signal,
_ => return future.await,
};
let signal = rquickjs::Object::from_value(signal)?;
if signal.get::<_, bool>("aborted")? {
return Err(ctx.throw(signal.get::<_, Value<'js>>("reason")?));
}

let add: rquickjs::Function<'js> = signal.get("addEventListener")?;
let remove: rquickjs::Function<'js> = signal.get("removeEventListener")?;
let (handle, registration) = AbortHandle::new_pair();
let callback = rquickjs::Function::new(ctx.clone(), move || handle.abort())?;
let options = rquickjs::Object::new(ctx.clone())?;
options.set("once", true)?;
add.call::<_, ()>((This(signal.clone()), "abort", callback.clone(), options))?;

if signal.get::<_, bool>("aborted")? {
let _ = remove.call::<_, ()>((This(signal.clone()), "abort", callback));
return Err(ctx.throw(signal.get::<_, Value<'js>>("reason")?));
}

let signal = Persistent::save(ctx, signal);
let callback = Persistent::save(ctx, callback);
let remove = Persistent::save(ctx, remove);
let result = Abortable::new(future, registration).await;
let signal = signal.restore(ctx)?;
let callback = callback.restore(ctx)?;
let remove = remove.restore(ctx)?;
let _ = remove.call::<_, ()>((This(signal.clone()), "abort", callback));

match result {
Ok(result) => result,
Err(_) => Err(ctx.throw(signal.get::<_, Value<'js>>("reason")?)),
}
}
25 changes: 16 additions & 9 deletions crates/wasm-rquickjs/skeleton/src/builtin/encoding.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
ERR_ENCODING_NOT_SUPPORTED,
ERR_INVALID_ARG_TYPE,
ERR_INVALID_THIS,
ERR_NO_ICU,
} from '__wasm_rquickjs_builtin/internal/errors';

const customInspectSymbol = Symbol.for('nodejs.util.inspect.custom');
Expand Down Expand Up @@ -132,6 +131,13 @@ function toDecodeBytes(input) {
}

function decodeNative(bytes, state, stream) {
if (state.nativeDecoder !== null) {
const [result, error] = state.nativeDecoder.decode(bytes, stream, state.fatal);
if (error !== undefined) {
throw new ERR_ENCODING_INVALID_ENCODED_DATA(state.encoding);
}
return result;
}
const [result, error] = encodingNative.decode(bytes, state.encoding, stream, state.fatal, state.ignoreBOMForNextDecode);
if (error !== undefined) {
throw new ERR_ENCODING_INVALID_ENCODED_DATA(state.encoding);
Expand All @@ -144,9 +150,6 @@ export class TextDecoder {
validateOptions(options);
const encoding = normalizeLabel(label);
const fatal = !!options?.fatal;
if (fatal) {
throw new ERR_NO_ICU('fatal');
}

textDecoderState.set(this, {
encoding,
Expand All @@ -155,6 +158,9 @@ export class TextDecoder {
ignoreBOMForNextDecode: !!options?.ignoreBOM,
pending: new Uint8Array(0),
streaming: false,
nativeDecoder: encodingNative.has_native_text_decoder()
? new encodingNative.NativeTextDecoder(encoding, !!options?.ignoreBOM)
: null,
});
}

Expand All @@ -176,12 +182,16 @@ export class TextDecoder {

let bytes = toDecodeBytes(buffer);
const stream = !!options?.stream;
if (state.pending.length !== 0) {
if (state.nativeDecoder !== null) {
state.streaming = stream;
return decodeNative(bytes, state, stream);
}
if (state.nativeDecoder === null && state.pending.length !== 0) {
bytes = concatBytes(state.pending, bytes);
state.pending = new Uint8Array(0);
}

if (stream) {
if (stream && state.nativeDecoder === null) {
const pendingLength = trailingIncompleteLength(bytes, state.encoding);
if (pendingLength !== 0) {
state.pending = bytes.slice(bytes.length - pendingLength);
Expand Down Expand Up @@ -248,9 +258,6 @@ export class TextDecoderStream extends streams.TransformStream {
validateOptions(options);
const encoding = normalizeLabel(label);
const fatal = !!options?.fatal;
if (fatal) {
throw new ERR_NO_ICU('fatal');
}

let decoder;
super({
Expand Down
101 changes: 100 additions & 1 deletion crates/wasm-rquickjs/skeleton/src/builtin/encoding.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
#[cfg(feature = "encoding")]
use encoding_rs::{Encoding, UTF_8, UTF_16BE, UTF_16LE};
use encoding_rs::{CoderResult, Decoder, DecoderResult, Encoding, UTF_8, UTF_16BE, UTF_16LE};
use rquickjs::JsLifetime;
use rquickjs::class::Trace;
#[cfg(feature = "encoding")]
use std::cell::RefCell;
use std::ptr;
use std::ptr::NonNull;

#[rquickjs::module(rename = "camelCase")]
pub mod native_module {
pub use super::NativeTextDecoder;
use rquickjs::convert::Coerced;
use rquickjs::prelude::*;
use rquickjs::{Ctx, TypedArray};

#[rquickjs::function]
pub fn has_native_text_decoder() -> bool {
cfg!(feature = "encoding")
}

#[rquickjs::function]
pub fn supports_encoding(encoding: Coerced<String>) -> bool {
let encoding = encoding.0;
Expand Down Expand Up @@ -76,6 +84,97 @@ pub mod native_module {
}
}

#[cfg(feature = "encoding")]
struct NativeDecoderState {
encoding: &'static Encoding,
ignore_bom: bool,
decoder: Decoder,
}

#[cfg(feature = "encoding")]
impl NativeDecoderState {
fn new(encoding: &'static Encoding, ignore_bom: bool) -> Self {
let decoder = if ignore_bom {
encoding.new_decoder_without_bom_handling()
} else {
encoding.new_decoder_with_bom_removal()
};
Self {
encoding,
ignore_bom,
decoder,
}
}

fn reset(&mut self) {
*self = Self::new(self.encoding, self.ignore_bom);
}
}

#[derive(Trace, JsLifetime)]
#[rquickjs::class]
pub struct NativeTextDecoder {
#[cfg(feature = "encoding")]
#[qjs(skip_trace)]
inner: RefCell<NativeDecoderState>,
}

#[cfg(feature = "encoding")]
#[rquickjs::methods]
impl NativeTextDecoder {
#[qjs(constructor)]
pub fn new(
ctx: rquickjs::Ctx<'_>,
encoding: rquickjs::convert::Coerced<String>,
ignore_bom: bool,
) -> rquickjs::Result<Self> {
let encoding = Encoding::for_label(encoding.0.as_bytes())
.ok_or_else(|| rquickjs::Exception::throw_message(&ctx, "Unsupported text encoding"))?;
Ok(Self {
inner: RefCell::new(NativeDecoderState::new(encoding, ignore_bom)),
})
}

pub fn decode(
&self,
bytes: rquickjs::TypedArray<'_, u8>,
stream: bool,
fatal: bool,
) -> rquickjs::prelude::List<(Option<String>, Option<String>)> {
let Some(bytes) = bytes.as_bytes() else {
return rquickjs::prelude::List((Some(String::new()), None));
};
let mut state = self.inner.borrow_mut();
let capacity = if fatal {
state
.decoder
.max_utf8_buffer_length_without_replacement(bytes.len())
} else {
state.decoder.max_utf8_buffer_length(bytes.len())
}
.unwrap_or(0);
let mut output = String::with_capacity(capacity);
let malformed = if fatal {
let (result, _) =
state
.decoder
.decode_to_string_without_replacement(bytes, &mut output, !stream);
matches!(result, DecoderResult::Malformed(_, _))
} else {
let (result, _, _) = state.decoder.decode_to_string(bytes, &mut output, !stream);
matches!(result, CoderResult::OutputFull)
};
if !stream || malformed {
state.reset();
}
if malformed {
rquickjs::prelude::List((None, Some("Malformed input".to_string())))
} else {
rquickjs::prelude::List((Some(output), None))
}
}
}

#[rquickjs::class]
#[derive(Trace, JsLifetime)]
pub struct EncodeIntoResult {
Expand Down
Loading
Loading