Skip to content

implement text-{en,de}coder-streams - #18

Open
karthik2804 wants to merge 3 commits into
bytecodealliance:mainfrom
karthik2804:encode_decoded_stream
Open

karthik2804 wants to merge 3 commits into
bytecodealliance:mainfrom
karthik2804:encode_decoded_stream

Conversation

@karthik2804

Copy link
Copy Markdown
Contributor

This commit moves the existing TextDecoder and TextEncoder implementations into a new encoding crate, and adds the corresponding stream classes.

@karthik2804
karthik2804 force-pushed the encode_decoded_stream branch from a082d75 to f08c7d0 Compare July 14, 2026 12:41
@karthik2804
karthik2804 force-pushed the encode_decoded_stream branch 4 times, most recently from 50829c2 to 207fe7f Compare August 10, 2026 08:09
@karthik2804
karthik2804 marked this pull request as ready for review August 10, 2026 12:26
@zkat zkat mentioned this pull request Aug 27, 2026
3 of 4 tasks
This commit moves the existing TextDecoder and TextEncoder implementations into a new `encoding` crate, and adds the corresponding stream classes.

Signed-off-by: Karthik Ganeshram <kganeshr@akamai.com>

@tschneidereit tschneidereit left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for this! I left a whole bunch of comments, a few of which definitely need addressing. Please push changes addressing them as new commits instead of force-pushing changes to the existing commit, so I can see what changed.

(Note: I'll probably make sense to start reading the comments in text_encoder_stream.rs first, because I started commenting there first, and some comments in text_decoder_stream.rs might make less sense otherwise.)

fatal: bool,

/// <https://encoding.spec.whatwg.org/#textdecoder-ignore-bom>
#[no_trace(name = "ignoreBOM")]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no_trace doesn't have a name parameter. This doesn't cause a compile error only because the parameter is ignored.

Suggested change
#[no_trace(name = "ignoreBOM")]
#[no_trace]

// (Omitted — encoding_rs's Decoder buffers partial sequences internally;
// see the comment on the struct definition.)

let self_payload = scope.root_value(self.as_value());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function::new_callback takes impl ToJSVal, so no need to convert to a value here. You can just pass self to new_callback

Suggested change
let self_payload = scope.root_value(self.as_value());

c"textDecoderTransform",
1,
decoder_transform_cb,
self_payload,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self_payload,
self,

c"textDecoderFlush",
0,
decoder_flush_cb,
self_payload,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self_payload,
self,

Comment on lines +134 to +136
let transformer = Object::new_plain(scope)?;
transformer.set_property(scope, c"transform", transform_cb)?;
transformer.set_property(scope, c"flush", flush_cb)?;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unfortunately unsafe: it allows code to intercept the set_property calls by installing setters on Object.prototype, and thus modify internal behavior of the builtin.

Additionally, if Object.prototype has start or cancel functions or a readableType member, those would all be used in setting up the TransformStream, and expose / enable modifying internal behavior.

This can be worked around by creating an object without a prototype. However, in this case we should do something entirely different to begin with: use the native transform and flush algorithms directly.

The way that could work is by mirroring ReadableStream::new_native, with a new function with this signature:

impl TransformStream<'_> {
    /// <https://streams.spec.whatwg.org/#transformstream-set-up>
    pub fn new_native<'r>(
        scope: &'r Scope<'_>,
        transform: AlgorithmArg<'r>,
        flush: AlgorithmArg<'r>,
        cancel: AlgorithmArg<'r>,
    ) -> Result<TransformStream<'r>, ExnThrown>;
}

The body would roughly be:

  • js::class::create_instance_with::<TransformStreamImpl>, as create_readable_stream does.
  • initialize_transform_stream with a start promise resolved with undefined, writable high-water mark 1, readable high-water mark 0, and no size algorithms.
  • TransformStreamDefaultController::new.
  • set_up_transform_stream_default_controller, passing AlgorithmArg::Native for the algorithms.

You could then avoid creating JSFunctions for transform and flush entirely.

chunk: Uint8Array<'_>,
) -> Result<(), ExnThrown> {
let chunk_val = scope.root_value(chunk.as_value());
stream.data().transform.get(scope).enqueue(scope, chunk_val)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unsafe, because it retains the data() borrow across the calls into JS that enqueue can trigger. Those could then call back into the EncoderStream and take a data_mut, causing a mutable borrow to coexist with an immutable one.

// Step 6: Set this’s [[TransformStream]] to transformStream.
self.data_mut().transform.set(transform_stream);

// Step 7: Set this’s [[pending high surrogate]] to null.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This step doesn't exist in the spec at all. Make sure to double-check that the step comments are fully accurate throughout the implementation, please. (The actual operation also happens to be redundant, since leading_surrogate is initialized to its default None value anyway.)


/// <https://encoding.spec.whatwg.org/#dom-textencoder-encoding>
#[getter]
fn encoding(&self) -> String {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should be able to return an &'static str here, but that might be wrong. Would be good to try though, to avoid the allocation of a string we'll throw away immediately.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works!

args: CallbackArgs<'_>,
payload: HandleValue<'_>,
) -> Result<Value, ExnThrown> {
let stream: TextDecoderStream<'_> = Object::from_value(scope, *payload)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can also use cast_payload

}

/// The <https://encoding.spec.whatwg.org/#decode-and-enqueue-a-chunk> algorithm.
fn decoder_transform_cb(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can reuse most of TextDecoder::decode, if the core of that is split out into a pub(crate) helper function, with the decoder passed in as an argument.

That'd give us the fast paths provided by that implementation, and avoid a few perf hazards in here. In particular, the input is copied into a Vec instead of directly using the input buffer's bytes as a slice, and the decoded result is converted from a String to a JSString, running decoding again. Both are avoided by reusing a shared implementation.

Signed-off-by: Karthik Ganeshram <kganeshr@akamai.com>
Signed-off-by: Karthik Ganeshram <kganeshr@akamai.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants