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