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