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;
23use crate::dom::bindings::codegen::Bindings::CompressionStreamBinding::CompressionFormat;
24use crate::dom::bindings::codegen::Bindings::DecompressionStreamBinding::DecompressionStreamMethods;
25use crate::dom::bindings::conversions::ToJSValConvertible;
26use crate::dom::bindings::error::{Error, Fallible};
27use crate::dom::bindings::root::{Dom, DomRoot};
28#[cfg(feature = "brotli-compression-stream")]
29use crate::dom::stream::compressionstream::BROTLI_BUFFER_SIZE;
30use crate::dom::stream::compressionstream::convert_chunk_to_vec;
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 chunk = convert_chunk_to_vec(cx, chunk)?;
138
139    // Step 2. Let buffer be the result of decompressing chunk with ds’s format and context. If
140    // this results in an error, then throw a TypeError.
141    // NOTE: In our implementation, the enum type of context already indicates the format.
142    let buffer = {
143        let mut decompression_context = ds.context.borrow_mut();
144        let buffer = decompression_context
145            .decompress(&chunk)
146            .map_err(|_| Error::Type(c"Failed to decompress a chunk of compressed input".into()))?;
147
148        // Step 3. If buffer is empty, return.
149        if buffer.is_empty() {
150            return Ok(());
151        }
152        buffer
153    };
154    // Step 4. Let arrays be the result of splitting buffer into one or more non-empty pieces and
155    // converting them into Uint8Arrays.
156    // Step 5. For each Uint8Array array of arrays, enqueue array in ds’s transform.
157    // NOTE: We process the result in a single Uint8Array.
158    rooted!(&in(cx) let mut js_object = ptr::null_mut::<JSObject>());
159    let array = create_buffer_source::<Uint8>(cx, &buffer, js_object.handle_mut())
160        .map_err(|_| Error::Type(c"Cannot convert byte sequence to Uint8Array".to_owned()))?;
161    rooted!(&in(cx) let mut rval = UndefinedValue());
162    array.to_jsval(cx, rval.handle_mut());
163    controller.enqueue(cx, global, rval.handle())?;
164
165    // Step 6. If the end of the compressed input has been reached, and ds’s context has not fully
166    // consumed chunk, then throw a TypeError.
167    if ds.context.borrow().is_ended {
168        return Err(Error::Type(
169            c"The end of the compressed input has been reached".to_owned(),
170        ));
171    }
172
173    Ok(())
174}
175
176/// <https://compression.spec.whatwg.org/#decompress-flush-and-enqueue>
177pub(crate) fn decompress_flush_and_enqueue(
178    cx: &mut js::context::JSContext,
179    global: &GlobalScope,
180    ds: &DecompressionStream,
181    controller: &TransformStreamDefaultController,
182) -> Fallible<()> {
183    // Step 1. Let buffer be the result of decompressing an empty input with ds’s format and
184    // context, with the finish flag.
185    // NOTE: In our implementation, the enum type of context already indicates the format.
186    let buffer = {
187        let mut decompression_context = ds.context.borrow_mut();
188        decompression_context
189            .finalize()
190            .map_err(|_| Error::Type(c"Failed to finalize the decompression stream".into()))?
191    };
192    // Step 2. If buffer is empty, return.
193    if !buffer.is_empty() {
194        // Step 2.1. Let arrays be the result of splitting buffer into one or more non-empty pieces
195        // and converting them into Uint8Arrays.
196        // Step 2.2. For each Uint8Array array of arrays, enqueue array in ds’s transform.
197        // NOTE: We process the result in a single Uint8Array.
198        rooted!(&in(cx) let mut js_object = ptr::null_mut::<JSObject>());
199        let array = create_buffer_source::<Uint8>(cx, &buffer, js_object.handle_mut())
200            .map_err(|_| Error::Type(c"Cannot convert byte sequence to Uint8Array".to_owned()))?;
201        rooted!(&in(cx) let mut rval = UndefinedValue());
202        array.to_jsval(cx, rval.handle_mut());
203        controller.enqueue(cx, global, rval.handle())?;
204    }
205
206    // Step 3. If the end of the compressed input has not been reached, then throw a TypeError.
207    //
208    // NOTE: If the end of the compressed input has not been reached, flate2::write::DeflateDecoder
209    // and flate2::write::GzDecoder can detect it and throw an error on `try_finish` in Step 1.
210    // However, flate2::write::ZlibDecoder does not. We need to test it by ourselves.
211    //
212    // To test it, we write one more byte to the decoder. If it accepts the extra byte, this
213    // indicates the end has not been reached. Otherwise, the end has been reached. This test has
214    // to been done before calling `try_finish`, so we execute it in Step 1, and store the result
215    // in `is_ended`.
216    if !ds.context.borrow().is_ended {
217        return Err(Error::Type(
218            c"The end of the compressed input has not been reached".to_owned(),
219        ));
220    }
221
222    Ok(())
223}
224
225/// An enum grouping decoders of differenct compression algorithms.
226enum Decoder {
227    #[cfg(feature = "brotli-compression-stream")]
228    Brotli(Box<BrotliDecoder<Vec<u8>>>),
229    Deflate(ZlibDecoder<Vec<u8>>),
230    DeflateRaw(DeflateDecoder<Vec<u8>>),
231    Gzip(GzDecoder<Vec<u8>>),
232}
233
234impl MallocSizeOf for Decoder {
235    fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
236        match self {
237            #[cfg(feature = "brotli-compression-stream")]
238            Decoder::Brotli(decoder) => decoder.shallow_size_of(ops),
239            Decoder::Deflate(decoder) => decoder.size_of(ops),
240            Decoder::DeflateRaw(decoder) => decoder.size_of(ops),
241            Decoder::Gzip(decoder) => decoder.size_of(ops),
242        }
243    }
244}
245
246/// <https://compression.spec.whatwg.org/#decompressionstream-context>
247/// Used to encapsulate the logic of decoder.
248#[derive(MallocSizeOf)]
249struct DecompressionContext {
250    decoder: Decoder,
251    is_ended: bool,
252}
253
254impl DecompressionContext {
255    fn new(format: CompressionFormat) -> Fallible<DecompressionContext> {
256        let decoder = match format {
257            #[cfg(feature = "brotli-compression-stream")]
258            CompressionFormat::Brotli => {
259                Decoder::Brotli(Box::new(BrotliDecoder::new(Vec::new(), BROTLI_BUFFER_SIZE)))
260            },
261            #[cfg(not(feature = "brotli-compression-stream"))]
262            CompressionFormat::Brotli => {
263                return Err(Error::NotSupported(Some("Brotli not supported".into())));
264            },
265            CompressionFormat::Deflate => Decoder::Deflate(ZlibDecoder::new(Vec::new())),
266            CompressionFormat::Deflate_raw => Decoder::DeflateRaw(DeflateDecoder::new(Vec::new())),
267            CompressionFormat::Gzip => Decoder::Gzip(GzDecoder::new(Vec::new())),
268        };
269        Ok(DecompressionContext {
270            decoder,
271            is_ended: false,
272        })
273    }
274
275    fn decompress(&mut self, mut chunk: &[u8]) -> Result<Vec<u8>, io::Error> {
276        let mut result = Vec::new();
277
278        match &mut self.decoder {
279            #[cfg(feature = "brotli-compression-stream")]
280            Decoder::Brotli(decoder) => {
281                while !chunk.is_empty() {
282                    let written = decoder.write(chunk)?;
283                    if written == 0 {
284                        self.is_ended = true;
285                        break;
286                    }
287                    chunk = &chunk[written..];
288                }
289                decoder.flush()?;
290                result.append(decoder.get_mut());
291            },
292            Decoder::Deflate(decoder) => {
293                while !chunk.is_empty() {
294                    let written = decoder.write(chunk)?;
295                    if written == 0 {
296                        self.is_ended = true;
297                        break;
298                    }
299                    chunk = &chunk[written..];
300                }
301                decoder.flush()?;
302                result.append(decoder.get_mut());
303            },
304            Decoder::DeflateRaw(decoder) => {
305                while !chunk.is_empty() {
306                    let written = decoder.write(chunk)?;
307                    if written == 0 {
308                        self.is_ended = true;
309                        break;
310                    }
311                    chunk = &chunk[written..];
312                }
313                decoder.flush()?;
314                result.append(decoder.get_mut());
315            },
316            Decoder::Gzip(decoder) => {
317                while !chunk.is_empty() {
318                    let written = decoder.write(chunk)?;
319                    if written == 0 {
320                        self.is_ended = true;
321                        break;
322                    }
323                    chunk = &chunk[written..];
324                }
325                decoder.flush()?;
326                result.append(decoder.get_mut());
327            },
328        }
329
330        Ok(result)
331    }
332
333    fn finalize(&mut self) -> Result<Vec<u8>, io::Error> {
334        let mut result = Vec::new();
335
336        match &mut self.decoder {
337            #[cfg(feature = "brotli-compression-stream")]
338            Decoder::Brotli(decoder) => {
339                if decoder.close().is_ok() {
340                    self.is_ended = true;
341                };
342                result.append(decoder.get_mut());
343            },
344            Decoder::Deflate(decoder) => {
345                // Compressed data in "Deflate" format does not have trailing bytes. Therefore,
346                // `ZlibEncoder::try_finish` is designed not to throw an error when the end of
347                // compressed input has not been reached, in order to decompress as much of the
348                // input as possible.
349                //
350                // To detect whether the end is reached, the workaround is to write one more byte to
351                // the encoder. Refusing to take the extra byte indicates the end has been reached.
352                //
353                // Note that we need to pull out the data in buffer first to avoid the extra byte
354                // contaminate the output.
355                decoder.flush()?;
356                result.append(decoder.get_mut());
357                if decoder.write(&[0])? == 0 {
358                    self.is_ended = true;
359                }
360                decoder.try_finish()?;
361            },
362            Decoder::DeflateRaw(decoder) => {
363                if decoder.try_finish().is_ok() {
364                    self.is_ended = true;
365                };
366                result.append(decoder.get_mut());
367            },
368            Decoder::Gzip(decoder) => {
369                if decoder.try_finish().is_ok() {
370                    self.is_ended = true;
371                };
372                result.append(decoder.get_mut());
373            },
374        }
375
376        Ok(result)
377    }
378}