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