Skip to main content

script/dom/encoding/
textencoderstream.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cell::Cell;
6use std::num::{NonZero, NonZeroU16};
7use std::ptr::{self, NonNull};
8
9use dom_struct::dom_struct;
10use js::context::JSContext;
11use js::conversions::{ToJSValConvertible, latin1_to_string};
12use js::jsapi::{JS_DeprecatedStringHasLatin1Chars, JSObject, JSType};
13use js::jsval::UndefinedValue;
14use js::rust::wrappers2::{JS_GetTwoByteStringCharsAndLength, JS_IsExceptionPending, ToPrimitive};
15use js::rust::{
16    HandleObject as SafeHandleObject, HandleValue as SafeHandleValue,
17    MutableHandleValue as SafeMutableHandleValue, ToString,
18};
19use js::typedarray::Uint8;
20use script_bindings::reflector::{Reflector, reflect_dom_object_with_proto};
21
22use crate::dom::bindings::buffer_source::create_buffer_source;
23use crate::dom::bindings::codegen::Bindings::TextEncoderStreamBinding::TextEncoderStreamMethods;
24use crate::dom::bindings::error::{Error, Fallible, throw_dom_exception};
25use crate::dom::bindings::root::{Dom, DomRoot};
26use crate::dom::bindings::str::DOMString;
27use crate::dom::stream::readablestream::ReadableStream;
28use crate::dom::stream::transformstreamdefaultcontroller::TransformerType;
29use crate::dom::stream::writablestream::WritableStream;
30use crate::dom::types::{GlobalScope, TransformStream, TransformStreamDefaultController};
31
32/// String converted from an input JS Value
33enum ConvertedInput<'a> {
34    String(String),
35    CodeUnits(&'a [u16]),
36}
37
38/// Converts a JS value to primitive type so that it can be used with
39/// `ToString`.
40///
41/// Set `rval` to `chunk` if `chunk` is a primitive JS value. Otherwise, convert
42/// `chunk` into a primitive JS value and then set `rval` to the converted
43/// primitive. This follows the `ToString` procedure with the exception that it
44/// does not convert the value to string.
45///
46/// See below for the `ToString` procedure in spec:
47/// <https://tc39.es/ecma262/multipage/abstract-operations.html#sec-tostring>
48#[expect(unsafe_code)]
49fn jsval_to_primitive(
50    cx: &mut JSContext,
51    global: &GlobalScope,
52    chunk: SafeHandleValue,
53    mut rval: SafeMutableHandleValue,
54) -> Fallible<()> {
55    // Step 1. If argument is a String, return argument.
56    // Step 2. If argument is a Symbol, throw a TypeError exception.
57    // Step 3. If argument is undefined, return "undefined".
58    // Step 4. If argument is null, return "null".
59    // Step 5. If argument is true, return "true".
60    // Step 6. If argument is false, return "false".
61    // Step 7. If argument is a Number, return Number::toString(argument, 10).
62    // Step 8. If argument is a BigInt, return BigInt::toString(argument, 10).
63    if chunk.is_primitive() {
64        rval.set(chunk.get());
65
66        return Ok(());
67    }
68
69    // Step 9. Assert: argument is an Object.
70    assert!(chunk.is_object());
71
72    // Step 10. Let primValue be ? ToPrimitive(argument, string).
73    rooted!(&in(cx) let obj = chunk.to_object());
74    let is_success = unsafe { ToPrimitive(cx, obj.handle(), JSType::JSTYPE_STRING, rval) };
75    log::debug!("ToPrimitive is_success={:?}", is_success);
76    if !is_success {
77        unsafe {
78            if !JS_IsExceptionPending(cx) {
79                throw_dom_exception(
80                    cx,
81                    global,
82                    Error::Type(c"Cannot convert JSObject to primitive".to_owned()),
83                );
84            }
85        }
86        return Err(Error::JSFailed);
87    }
88
89    Ok(())
90}
91
92/// <https://encoding.spec.whatwg.org/#textencoderstream-encoder>
93#[derive(Default, JSTraceable, MallocSizeOf)]
94pub(crate) struct Encoder {
95    /// <https://encoding.spec.whatwg.org/#textencoderstream-pending-high-surrogate>
96    leading_surrogate: Cell<Option<NonZeroU16>>,
97}
98
99impl Encoder {
100    fn encode(&self, maybe_ill_formed: ConvertedInput<'_>) -> String {
101        match maybe_ill_formed {
102            ConvertedInput::String(s) => {
103                // Rust String is already UTF-8 encoded and cannot contain
104                // surrogate
105                if !s.is_empty() && self.leading_surrogate.take().is_some() {
106                    let mut output = String::with_capacity(1 + s.len());
107                    output.push('\u{FFFD}');
108                    output.push_str(&s);
109                    return output;
110                }
111
112                s
113            },
114            ConvertedInput::CodeUnits(code_units) => self.encode_from_code_units(code_units),
115        }
116    }
117
118    /// Encode an input slice of code unit into unicode scalar values
119    fn encode_from_code_units(&self, input: &[u16]) -> String {
120        // <https://encoding.spec.whatwg.org/#encode-and-enqueue-a-chunk>
121        //
122        // Step 3. Let output be the I/O queue of bytes « end-of-queue ».
123        let mut output = String::with_capacity(input.len());
124        // Step 4. While true:
125        // Step 4.1 Let item be the result of reading from input.
126        for result in char::decode_utf16(input.iter().cloned()) {
127            // Step 4.3 Let result be the result of executing the convert code unit
128            //      to scalar value algorithm with encoder, item and input.
129
130            // <https://encoding.spec.whatwg.org/#convert-code-unit-to-scalar-value>
131            match result {
132                Ok(c) => {
133                    // Step 1. If encoder’s leading surrogate is non-null:
134                    // Step 1.1 Let leadingSurrogate be encoder’s leading surrogate.
135                    // Step 1.2 Set encoder’s leading surrogate to null.
136                    if self.leading_surrogate.take().is_some() {
137                        // Step 1.5 Return U+FFFD (�).
138                        output.push('\u{FFFD}');
139                    }
140
141                    // Step 1.4 Restore item to input.
142                    // Note: pushing item to output is equivalent to restoring item to input
143                    //      and rerun the convert-code-unit-to-scalar-value algo
144                    output.push(c);
145                },
146                Err(error) => {
147                    let unpaired_surrogate = error.unpaired_surrogate();
148                    match code_point_type(unpaired_surrogate) {
149                        CodePointType::LeadingSurrogate => {
150                            // Step 1.1 If encoder’s leading surrogate is non-null:
151                            // Step 1.2 Set encoder’s leading surrogate to null.
152                            if self.leading_surrogate.take().is_some() {
153                                output.push('\u{FFFD}');
154                            }
155
156                            // Step 1.4 Restore item to input.
157                            // Note: Replacing encoder's leading_surrogate is equivalent
158                            //      to restore item back to input and rerun the convert-
159                            //      code-unit-to-scalar-value algo.
160                            // Step 2. If item is a leading surrogate, then set encoder’s
161                            //      leading surrogate to item and return continue.
162                            self.leading_surrogate
163                                .replace(NonZero::new(unpaired_surrogate));
164                        },
165                        CodePointType::TrailingSurrogate => match self.leading_surrogate.take() {
166                            // Step 1.1 If encoder’s leading surrogate is non-null:
167                            // Step 1.2 Set encoder’s leading surrogate to null.
168                            Some(leading_surrogate) => {
169                                // Step 1.3 If item is a trailing surrogate, then return a scalar
170                                //      value from surrogates given leadingSurrogate and item.
171                                let c = char::decode_utf16([
172                                    leading_surrogate.get(),
173                                    unpaired_surrogate,
174                                ])
175                                .next()
176                                .expect("A pair of surrogate is supplied")
177                                .expect("Decoding a pair of surrogate cannot fail");
178                                output.push(c);
179                            },
180                            // Step 3. If item is a trailing surrogate, then return U+FFFD (�).
181                            None => output.push('\u{FFFD}'),
182                        },
183                        CodePointType::ScalarValue => unreachable!("Scalar Value won't fail"),
184                    }
185                },
186            }
187        }
188
189        output
190    }
191}
192
193enum CodePointType {
194    ScalarValue,
195    LeadingSurrogate,
196    TrailingSurrogate,
197}
198
199fn code_point_type(value: u16) -> CodePointType {
200    match value {
201        0xD800..=0xDBFF => CodePointType::LeadingSurrogate,
202        0xDC00..=0xDFFF => CodePointType::TrailingSurrogate,
203        _ => CodePointType::ScalarValue,
204    }
205}
206
207/// <https://encoding.spec.whatwg.org/#encode-and-enqueue-a-chunk>
208#[expect(unsafe_code)]
209pub(crate) fn encode_and_enqueue_a_chunk(
210    cx: &mut JSContext,
211    global: &GlobalScope,
212    chunk: SafeHandleValue,
213    encoder: &Encoder,
214    controller: &TransformStreamDefaultController,
215) -> Fallible<()> {
216    // Step 1. Let input be the result of converting chunk to a DOMString.
217    // Step 2. Convert input to an I/O queue of code units.
218    rooted!(&in(cx) let mut rval = UndefinedValue());
219    jsval_to_primitive(cx, global, chunk, rval.handle_mut())?;
220
221    assert!(!rval.is_object());
222    rooted!(&in(cx) let jsstr = unsafe { ToString(cx, rval.handle()) });
223    if jsstr.is_null() {
224        unsafe {
225            if !JS_IsExceptionPending(cx) {
226                throw_dom_exception(
227                    cx,
228                    global,
229                    Error::Type(c"Cannot convert JS primitive to string".to_owned()),
230                );
231            }
232        }
233
234        return Err(Error::JSFailed);
235    }
236
237    let input = unsafe {
238        if JS_DeprecatedStringHasLatin1Chars(*jsstr) {
239            let s = NonNull::new(*jsstr).expect("jsstr cannot be null");
240            ConvertedInput::String(latin1_to_string(cx, s))
241        } else {
242            let mut len = 0;
243            let data = JS_GetTwoByteStringCharsAndLength(cx, *jsstr, &mut len);
244            let maybe_ill_formed_code_units = std::slice::from_raw_parts(data, len);
245            ConvertedInput::CodeUnits(maybe_ill_formed_code_units)
246        }
247    };
248
249    // Step 3. Let output be the I/O queue of bytes « end-of-queue ».
250    // Step 4. While true:
251    // Step 4.1 Let item be the result of reading from input.
252    // Step 4.3 Let result be the result of executing the convert code unit
253    //      to scalar value algorithm with encoder, item and input.
254    // Step 4.4 If result is not continue, then process an item with result,
255    //      encoder’s encoder, input, output, and "fatal".
256    let output = encoder.encode(input);
257
258    // Step 4.2 If item is end-of-queue:
259    // Step 4.2.1 Convert output into a byte sequence.
260    let output = output.as_bytes();
261    // Step 4.2.2 If output is not empty:
262    if output.is_empty() {
263        // Step 4.2.3
264        return Ok(());
265    }
266
267    // Step 4.2.2.1 Let chunk be the result of creating a Uint8Array object
268    //      given output and encoder’s relevant realm.
269    rooted!(&in(cx) let mut js_object = ptr::null_mut::<JSObject>());
270    let chunk = create_buffer_source::<Uint8>(cx, output, js_object.handle_mut())
271        .map_err(|_| Error::Type(c"Cannot convert byte sequence to Uint8Array".to_owned()))?;
272    rooted!(&in(cx) let mut rval = UndefinedValue());
273    chunk.safe_to_jsval(cx, rval.handle_mut());
274    // Step 4.2.2.2 Enqueue chunk into encoder’s transform.
275    controller.enqueue(cx, global, rval.handle())?;
276    Ok(())
277}
278
279/// <https://encoding.spec.whatwg.org/#encode-and-flush>
280pub(crate) fn encode_and_flush(
281    cx: &mut JSContext,
282    global: &GlobalScope,
283    encoder: &Encoder,
284    controller: &TransformStreamDefaultController,
285) -> Fallible<()> {
286    // Step 1. If encoder’s leading surrogate is non-null:
287    if encoder.leading_surrogate.get().is_some() {
288        // Step 1.1 Let chunk be the result of creating a Uint8Array object
289        //      given « 0xEF, 0xBF, 0xBD » and encoder’s relevant realm.
290        rooted!(&in(cx) let mut js_object = ptr::null_mut::<JSObject>());
291        let chunk =
292            create_buffer_source::<Uint8>(cx, &[0xEF_u8, 0xBF, 0xBD], js_object.handle_mut())
293                .map_err(|_| {
294                    Error::Type(c"Cannot convert byte sequence to Uint8Array".to_owned())
295                })?;
296        rooted!(&in(cx) let mut rval = UndefinedValue());
297        chunk.safe_to_jsval(cx, rval.handle_mut());
298        // Step 1.2 Enqueue chunk into encoder’s transform.
299        return controller.enqueue(cx, global, rval.handle());
300    }
301
302    Ok(())
303}
304
305/// <https://encoding.spec.whatwg.org/#textencoderstream>
306#[dom_struct]
307pub(crate) struct TextEncoderStream {
308    reflector_: Reflector,
309
310    /// <https://streams.spec.whatwg.org/#generictransformstream>
311    transform: Dom<TransformStream>,
312}
313
314impl TextEncoderStream {
315    fn new_inherited(transform: &TransformStream) -> TextEncoderStream {
316        Self {
317            reflector_: Reflector::new(),
318            transform: Dom::from_ref(transform),
319        }
320    }
321
322    /// <https://encoding.spec.whatwg.org/#dom-textencoderstream>
323    fn new_with_proto(
324        cx: &mut JSContext,
325        global: &GlobalScope,
326        proto: Option<SafeHandleObject>,
327    ) -> Fallible<DomRoot<TextEncoderStream>> {
328        // Step 1. Set this’s encoder to an instance of the UTF-8 encoder.
329        let encoder = Encoder::default();
330
331        // Step 2. Let transformAlgorithm be an algorithm which takes a chunk argument
332        //      and runs the encode and enqueue a chunk algorithm with this and chunk.
333        // Step 3. Let flushAlgorithm be an algorithm which runs the encode and flush
334        //      algorithm with this.
335
336        // Step 4. Let transformStream be a new TransformStream.
337        let transform = TransformStream::new_with_proto(cx, global, None);
338        // Step 5. Set up transformStream with transformAlgorithm set to transformAlgorithm
339        //      and flushAlgorithm set to flushAlgorithm.
340        transform.set_up(cx, global, TransformerType::Encoder(encoder))?;
341
342        // Step 6. Set this’s transform to transformStream.
343        Ok(reflect_dom_object_with_proto(
344            cx,
345            Box::new(TextEncoderStream::new_inherited(&transform)),
346            global,
347            proto,
348        ))
349    }
350}
351
352impl TextEncoderStreamMethods<crate::DomTypeHolder> for TextEncoderStream {
353    /// <https://encoding.spec.whatwg.org/#dom-textencoderstream>
354    fn Constructor(
355        cx: &mut JSContext,
356        global: &GlobalScope,
357        proto: Option<SafeHandleObject>,
358    ) -> Fallible<DomRoot<TextEncoderStream>> {
359        TextEncoderStream::new_with_proto(cx, global, proto)
360    }
361
362    /// <https://encoding.spec.whatwg.org/#dom-textencoder-encoding>
363    fn Encoding(&self) -> DOMString {
364        // Returns "utf-8".
365        DOMString::from("utf-8")
366    }
367
368    /// <https://streams.spec.whatwg.org/#dom-generictransformstream-readable>
369    fn Readable(&self) -> DomRoot<ReadableStream> {
370        self.transform.get_readable()
371    }
372
373    /// <https://streams.spec.whatwg.org/#dom-generictransformstream-writable>
374    fn Writable(&self) -> DomRoot<WritableStream> {
375        self.transform.get_writable()
376    }
377}