Skip to main content

script/dom/stream/
decompressionstream.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 http://mozilla.org/MPL/2.0/. */
4
5use std::cell::RefCell;
6use std::io::{self, Write};
7use std::ptr;
8
9use brotli::DecompressorWriter as BrotliDecoder;
10use dom_struct::dom_struct;
11use flate2::write::{DeflateDecoder, GzDecoder, ZlibDecoder};
12use js::jsapi::JSObject;
13use js::jsval::UndefinedValue;
14use js::rust::{HandleObject as SafeHandleObject, HandleValue as SafeHandleValue};
15use js::typedarray::Uint8;
16use malloc_size_of::{MallocSizeOf, MallocSizeOfOps};
17use script_bindings::reflector::{Reflector, reflect_dom_object_with_proto};
18
19use crate::dom::bindings::buffer_source::create_buffer_source;
20use crate::dom::bindings::codegen::Bindings::CompressionStreamBinding::CompressionFormat;
21use crate::dom::bindings::codegen::Bindings::DecompressionStreamBinding::DecompressionStreamMethods;
22use crate::dom::bindings::conversions::SafeToJSValConvertible;
23use crate::dom::bindings::error::{Error, Fallible};
24use crate::dom::bindings::root::{Dom, DomRoot};
25use crate::dom::stream::compressionstream::{BROTLI_BUFFER_SIZE, convert_chunk_to_vec};
26use crate::dom::stream::transformstreamdefaultcontroller::TransformerType;
27use crate::dom::types::{
28    GlobalScope, ReadableStream, TransformStream, TransformStreamDefaultController, WritableStream,
29};
30
31/// <https://compression.spec.whatwg.org/#decompressionstream>
32#[dom_struct]
33pub(crate) struct DecompressionStream {
34    reflector_: Reflector,
35
36    /// <https://streams.spec.whatwg.org/#generictransformstream>
37    transform: Dom<TransformStream>,
38
39    /// <https://compression.spec.whatwg.org/#decompressionstream-format>
40    format: CompressionFormat,
41
42    // <https://compression.spec.whatwg.org/#decompressionstream-context>
43    #[no_trace]
44    context: RefCell<DecompressionContext>,
45}
46
47impl DecompressionStream {
48    fn new_inherited(
49        transform: &TransformStream,
50        format: CompressionFormat,
51    ) -> DecompressionStream {
52        DecompressionStream {
53            reflector_: Reflector::new(),
54            transform: Dom::from_ref(transform),
55            format,
56            context: RefCell::new(DecompressionContext::new(format)),
57        }
58    }
59
60    fn new_with_proto(
61        cx: &mut js::context::JSContext,
62        global: &GlobalScope,
63        proto: Option<SafeHandleObject>,
64        transform: &TransformStream,
65        format: CompressionFormat,
66    ) -> DomRoot<DecompressionStream> {
67        reflect_dom_object_with_proto(
68            cx,
69            Box::new(DecompressionStream::new_inherited(transform, format)),
70            global,
71            proto,
72        )
73    }
74}
75
76impl DecompressionStreamMethods<crate::DomTypeHolder> for DecompressionStream {
77    /// <https://compression.spec.whatwg.org/#dom-decompressionstream-decompressionstream>
78    fn Constructor(
79        cx: &mut js::context::JSContext,
80        global: &GlobalScope,
81        proto: Option<SafeHandleObject>,
82        format: CompressionFormat,
83    ) -> Fallible<DomRoot<DecompressionStream>> {
84        // Step 1. If format is unsupported in DecompressionStream, then throw a TypeError.
85        // NOTE: All of "brotli", "deflate", "deflate-raw" and "gzip" are supported.
86
87        // Step 2. Set this’s format to format.
88        // Step 5. Set this’s transform to a new TransformStream.
89        let transform = TransformStream::new_with_proto(cx, global, None);
90        let decompression_stream =
91            DecompressionStream::new_with_proto(cx, global, proto, &transform, format);
92
93        // Step 3. Let transformAlgorithm be an algorithm which takes a chunk argument and runs the
94        // decompress and enqueue a chunk algorithm with this and chunk.
95        // Step 4. Let flushAlgorithm be an algorithm which takes no argument and runs the
96        // decompress flush and enqueue algorithm with this.
97        let transformer_type = TransformerType::Decompressor(decompression_stream.clone());
98
99        // Step 6. Set up this’s transform with transformAlgorithm set to transformAlgorithm and
100        // flushAlgorithm set to flushAlgorithm.
101        transform.set_up(cx, global, transformer_type)?;
102
103        Ok(decompression_stream)
104    }
105
106    /// <https://streams.spec.whatwg.org/#dom-generictransformstream-readable>
107    fn Readable(&self) -> DomRoot<ReadableStream> {
108        // The readable getter steps are to return this’s transform.[[readable]].
109        self.transform.get_readable()
110    }
111
112    /// <https://streams.spec.whatwg.org/#dom-generictransformstream-writable>
113    fn Writable(&self) -> DomRoot<WritableStream> {
114        // The writable getter steps are to return this’s transform.[[writable]].
115        self.transform.get_writable()
116    }
117}
118
119/// <https://compression.spec.whatwg.org/#decompress-and-enqueue-a-chunk>
120pub(crate) fn decompress_and_enqueue_a_chunk(
121    cx: &mut js::context::JSContext,
122    global: &GlobalScope,
123    ds: &DecompressionStream,
124    chunk: SafeHandleValue,
125    controller: &TransformStreamDefaultController,
126) -> Fallible<()> {
127    // Step 1. If chunk is not a BufferSource type, then throw a TypeError.
128    let chunk = convert_chunk_to_vec(cx, chunk)?;
129
130    // Step 2. Let buffer be the result of decompressing chunk with ds’s format and context. If
131    // this results in an error, then throw a TypeError.
132    // NOTE: In our implementation, the enum type of context already indicates the format.
133    let buffer = {
134        let mut decompression_context = ds.context.borrow_mut();
135        let buffer = decompression_context
136            .decompress(&chunk)
137            .map_err(|_| Error::Type(c"Failed to decompress a chunk of compressed input".into()))?;
138
139        // Step 3. If buffer is empty, return.
140        if buffer.is_empty() {
141            return Ok(());
142        }
143        buffer
144    };
145    // Step 4. Let arrays be the result of splitting buffer into one or more non-empty pieces and
146    // converting them into Uint8Arrays.
147    // Step 5. For each Uint8Array array of arrays, enqueue array in ds’s transform.
148    // NOTE: We process the result in a single Uint8Array.
149    rooted!(&in(cx) let mut js_object = ptr::null_mut::<JSObject>());
150    let array = create_buffer_source::<Uint8>(cx, &buffer, js_object.handle_mut())
151        .map_err(|_| Error::Type(c"Cannot convert byte sequence to Uint8Array".to_owned()))?;
152    rooted!(&in(cx) let mut rval = UndefinedValue());
153    array.safe_to_jsval(cx, rval.handle_mut());
154    controller.enqueue(cx, global, rval.handle())?;
155
156    // Step 6. If the end of the compressed input has been reached, and ds’s context has not fully
157    // consumed chunk, then throw a TypeError.
158    if ds.context.borrow().is_ended {
159        return Err(Error::Type(
160            c"The end of the compressed input has been reached".to_owned(),
161        ));
162    }
163
164    Ok(())
165}
166
167/// <https://compression.spec.whatwg.org/#decompress-flush-and-enqueue>
168pub(crate) fn decompress_flush_and_enqueue(
169    cx: &mut js::context::JSContext,
170    global: &GlobalScope,
171    ds: &DecompressionStream,
172    controller: &TransformStreamDefaultController,
173) -> Fallible<()> {
174    // Step 1. Let buffer be the result of decompressing an empty input with ds’s format and
175    // context, with the finish flag.
176    // NOTE: In our implementation, the enum type of context already indicates the format.
177    let buffer = {
178        let mut decompression_context = ds.context.borrow_mut();
179        decompression_context
180            .finalize()
181            .map_err(|_| Error::Type(c"Failed to finalize the decompression stream".into()))?
182    };
183    // Step 2. If buffer is empty, return.
184    if !buffer.is_empty() {
185        // Step 2.1. Let arrays be the result of splitting buffer into one or more non-empty pieces
186        // and converting them into Uint8Arrays.
187        // Step 2.2. For each Uint8Array array of arrays, enqueue array in ds’s transform.
188        // NOTE: We process the result in a single Uint8Array.
189        rooted!(&in(cx) let mut js_object = ptr::null_mut::<JSObject>());
190        let array = create_buffer_source::<Uint8>(cx, &buffer, js_object.handle_mut())
191            .map_err(|_| Error::Type(c"Cannot convert byte sequence to Uint8Array".to_owned()))?;
192        rooted!(&in(cx) let mut rval = UndefinedValue());
193        array.safe_to_jsval(cx, rval.handle_mut());
194        controller.enqueue(cx, global, rval.handle())?;
195    }
196
197    // Step 3. If the end of the compressed input has not been reached, then throw a TypeError.
198    //
199    // NOTE: If the end of the compressed input has not been reached, flate2::write::DeflateDecoder
200    // and flate2::write::GzDecoder can detect it and throw an error on `try_finish` in Step 1.
201    // However, flate2::write::ZlibDecoder does not. We need to test it by ourselves.
202    //
203    // To test it, we write one more byte to the decoder. If it accepts the extra byte, this
204    // indicates the end has not been reached. Otherwise, the end has been reached. This test has
205    // to been done before calling `try_finish`, so we execute it in Step 1, and store the result
206    // in `is_ended`.
207    if !ds.context.borrow().is_ended {
208        return Err(Error::Type(
209            c"The end of the compressed input has not been reached".to_owned(),
210        ));
211    }
212
213    Ok(())
214}
215
216/// An enum grouping decoders of differenct compression algorithms.
217enum Decoder {
218    Brotli(Box<BrotliDecoder<Vec<u8>>>),
219    Deflate(ZlibDecoder<Vec<u8>>),
220    DeflateRaw(DeflateDecoder<Vec<u8>>),
221    Gzip(GzDecoder<Vec<u8>>),
222}
223
224impl MallocSizeOf for Decoder {
225    #[expect(unsafe_code)]
226    fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
227        match self {
228            Decoder::Brotli(decoder) => unsafe { ops.malloc_size_of(&**decoder) },
229            Decoder::Deflate(decoder) => decoder.size_of(ops),
230            Decoder::DeflateRaw(decoder) => decoder.size_of(ops),
231            Decoder::Gzip(decoder) => decoder.size_of(ops),
232        }
233    }
234}
235
236/// <https://compression.spec.whatwg.org/#decompressionstream-context>
237/// Used to encapsulate the logic of decoder.
238#[derive(MallocSizeOf)]
239struct DecompressionContext {
240    decoder: Decoder,
241    is_ended: bool,
242}
243
244impl DecompressionContext {
245    fn new(format: CompressionFormat) -> DecompressionContext {
246        let decoder = match format {
247            CompressionFormat::Brotli => {
248                Decoder::Brotli(Box::new(BrotliDecoder::new(Vec::new(), BROTLI_BUFFER_SIZE)))
249            },
250            CompressionFormat::Deflate => Decoder::Deflate(ZlibDecoder::new(Vec::new())),
251            CompressionFormat::Deflate_raw => Decoder::DeflateRaw(DeflateDecoder::new(Vec::new())),
252            CompressionFormat::Gzip => Decoder::Gzip(GzDecoder::new(Vec::new())),
253        };
254        DecompressionContext {
255            decoder,
256            is_ended: false,
257        }
258    }
259
260    fn decompress(&mut self, mut chunk: &[u8]) -> Result<Vec<u8>, io::Error> {
261        let mut result = Vec::new();
262
263        match &mut self.decoder {
264            Decoder::Brotli(decoder) => {
265                while !chunk.is_empty() {
266                    let written = decoder.write(chunk)?;
267                    if written == 0 {
268                        self.is_ended = true;
269                        break;
270                    }
271                    chunk = &chunk[written..];
272                }
273                decoder.flush()?;
274                result.append(decoder.get_mut());
275            },
276            Decoder::Deflate(decoder) => {
277                while !chunk.is_empty() {
278                    let written = decoder.write(chunk)?;
279                    if written == 0 {
280                        self.is_ended = true;
281                        break;
282                    }
283                    chunk = &chunk[written..];
284                }
285                decoder.flush()?;
286                result.append(decoder.get_mut());
287            },
288            Decoder::DeflateRaw(decoder) => {
289                while !chunk.is_empty() {
290                    let written = decoder.write(chunk)?;
291                    if written == 0 {
292                        self.is_ended = true;
293                        break;
294                    }
295                    chunk = &chunk[written..];
296                }
297                decoder.flush()?;
298                result.append(decoder.get_mut());
299            },
300            Decoder::Gzip(decoder) => {
301                while !chunk.is_empty() {
302                    let written = decoder.write(chunk)?;
303                    if written == 0 {
304                        self.is_ended = true;
305                        break;
306                    }
307                    chunk = &chunk[written..];
308                }
309                decoder.flush()?;
310                result.append(decoder.get_mut());
311            },
312        }
313
314        Ok(result)
315    }
316
317    fn finalize(&mut self) -> Result<Vec<u8>, io::Error> {
318        let mut result = Vec::new();
319
320        match &mut self.decoder {
321            Decoder::Brotli(decoder) => {
322                if decoder.close().is_ok() {
323                    self.is_ended = true;
324                };
325                result.append(decoder.get_mut());
326            },
327            Decoder::Deflate(decoder) => {
328                // Compressed data in "Deflate" format does not have trailing bytes. Therefore,
329                // `ZlibEncoder::try_finish` is designed not to throw an error when the end of
330                // compressed input has not been reached, in order to decompress as much of the
331                // input as possible.
332                //
333                // To detect whether the end is reached, the workaround is to write one more byte to
334                // the encoder. Refusing to take the extra byte indicates the end has been reached.
335                //
336                // Note that we need to pull out the data in buffer first to avoid the extra byte
337                // contaminate the output.
338                decoder.flush()?;
339                result.append(decoder.get_mut());
340                if decoder.write(&[0])? == 0 {
341                    self.is_ended = true;
342                }
343                decoder.try_finish()?;
344            },
345            Decoder::DeflateRaw(decoder) => {
346                if decoder.try_finish().is_ok() {
347                    self.is_ended = true;
348                };
349                result.append(decoder.get_mut());
350            },
351            Decoder::Gzip(decoder) => {
352                if decoder.try_finish().is_ok() {
353                    self.is_ended = true;
354                };
355                result.append(decoder.get_mut());
356            },
357        }
358
359        Ok(result)
360    }
361}