Skip to main content

script/dom/stream/
compressionstream.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
5#[cfg(feature = "brotli-compression-stream")]
6use std::borrow::BorrowMut;
7use std::cell::RefCell;
8use std::io::{self, Write};
9use std::ptr;
10
11#[cfg(feature = "brotli-compression-stream")]
12use brotli::CompressorWriter as BrotliEncoder;
13use dom_struct::dom_struct;
14use flate2::Compression;
15use flate2::write::{DeflateEncoder, GzEncoder, ZlibEncoder};
16use js::context::JSContext;
17use js::jsapi::JSObject;
18use js::jsval::UndefinedValue;
19use js::rust::{HandleObject as SafeHandleObject, HandleValue as SafeHandleValue};
20use js::typedarray::Uint8;
21#[cfg(feature = "brotli-compression-stream")]
22use malloc_size_of::MallocShallowSizeOf;
23use malloc_size_of::{MallocSizeOf, MallocSizeOfOps};
24use script_bindings::reflector::{Reflector, reflect_dom_object_with_proto};
25
26use crate::dom::bindings::buffer_source::{create_buffer_source, get_buffer_source_copy};
27use crate::dom::bindings::codegen::Bindings::CompressionStreamBinding::{
28    CompressionFormat, CompressionStreamMethods,
29};
30use crate::dom::bindings::codegen::UnionTypes::ArrayBufferViewOrArrayBuffer;
31use crate::dom::bindings::conversions::{FromJSValConvertible, ToJSValConvertible};
32use crate::dom::bindings::error::{Error, Fallible};
33use crate::dom::bindings::root::{Dom, DomRoot};
34use crate::dom::stream::transformstreamdefaultcontroller::TransformerType;
35use crate::dom::types::{
36    GlobalScope, ReadableStream, TransformStream, TransformStreamDefaultController, WritableStream,
37};
38
39#[cfg(feature = "brotli-compression-stream")]
40pub(crate) const BROTLI_BUFFER_SIZE: usize = 4096;
41#[cfg(feature = "brotli-compression-stream")]
42const BROTLI_QUALITIY_LEVEL: u32 = 5;
43#[cfg(feature = "brotli-compression-stream")]
44const BROTLI_WINDOW_SIZE: u32 = 22;
45
46/// <https://compression.spec.whatwg.org/#compressionstream>
47#[dom_struct]
48pub(crate) struct CompressionStream {
49    reflector_: Reflector,
50
51    /// <https://streams.spec.whatwg.org/#generictransformstream>
52    transform: Dom<TransformStream>,
53
54    /// <https://compression.spec.whatwg.org/#compressionstream-format>
55    format: CompressionFormat,
56
57    // <https://compression.spec.whatwg.org/#compressionstream-context>
58    #[no_trace]
59    context: RefCell<CompressionContext>,
60}
61
62impl CompressionStream {
63    fn new_inherited(
64        transform: &TransformStream,
65        format: CompressionFormat,
66    ) -> Fallible<CompressionStream> {
67        Ok(CompressionStream {
68            reflector_: Reflector::new(),
69            transform: Dom::from_ref(transform),
70            format,
71            context: RefCell::new(CompressionContext::new(format)?),
72        })
73    }
74    #[cfg_attr(crown, expect(crown::unrooted_must_root))] // reflect will only be called on the box which is fine
75    fn new_with_proto(
76        cx: &mut JSContext,
77        global: &GlobalScope,
78        proto: Option<SafeHandleObject>,
79        transform: &TransformStream,
80        format: CompressionFormat,
81    ) -> Fallible<DomRoot<CompressionStream>> {
82        Ok(reflect_dom_object_with_proto(
83            cx,
84            Box::new(CompressionStream::new_inherited(transform, format)?),
85            global,
86            proto,
87        ))
88    }
89}
90
91impl CompressionStreamMethods<crate::DomTypeHolder> for CompressionStream {
92    /// <https://compression.spec.whatwg.org/#dom-compressionstream-compressionstream>
93    fn Constructor(
94        cx: &mut JSContext,
95        global: &GlobalScope,
96        proto: Option<SafeHandleObject>,
97        format: CompressionFormat,
98    ) -> Fallible<DomRoot<CompressionStream>> {
99        // Step 1. If format is unsupported in CompressionStream, then throw a TypeError.
100        // NOTE: All of "brotli", "deflate", "deflate-raw" and "gzip" are supported.
101
102        // Step 2. Set this’s format to format.
103        // Step 5. Set this’s transform to a new TransformStream.
104        let transform = TransformStream::new_with_proto(cx, global, None);
105        let compression_stream =
106            CompressionStream::new_with_proto(cx, global, proto, &transform, format)?;
107
108        // Step 3. Let transformAlgorithm be an algorithm which takes a chunk argument and runs the
109        // compress and enqueue a chunk algorithm with this and chunk.
110        // Step 4. Let flushAlgorithm be an algorithm which takes no argument and runs the compress
111        // flush and enqueue algorithm with this.
112
113        // Step 6. Set up this’s transform with transformAlgorithm set to transformAlgorithm and
114        // flushAlgorithm set to flushAlgorithm.
115        transform.set_up(
116            cx,
117            global,
118            TransformerType::Compressor(compression_stream.as_traced()),
119        )?;
120
121        Ok(compression_stream)
122    }
123
124    /// <https://streams.spec.whatwg.org/#dom-generictransformstream-readable>
125    fn Readable(&self) -> DomRoot<ReadableStream> {
126        // The readable getter steps are to return this’s transform.[[readable]].
127        self.transform.get_readable()
128    }
129
130    /// <https://streams.spec.whatwg.org/#dom-generictransformstream-writable>
131    fn Writable(&self) -> DomRoot<WritableStream> {
132        // The writable getter steps are to return this’s transform.[[writable]].
133        self.transform.get_writable()
134    }
135}
136
137/// <https://compression.spec.whatwg.org/#compress-and-enqueue-a-chunk>
138pub(crate) fn compress_and_enqueue_a_chunk(
139    cx: &mut JSContext,
140    global: &GlobalScope,
141    cs: &CompressionStream,
142    chunk: SafeHandleValue,
143    controller: &TransformStreamDefaultController,
144) -> Fallible<()> {
145    // Step 1. If chunk is not a BufferSource type, then throw a TypeError.
146    let chunk = convert_chunk_to_vec(cx, chunk)?;
147
148    // Step 2. Let buffer be the result of compressing chunk with cs’s format and context.
149    // NOTE: In our implementation, the enum type of context already indicates the format.
150    let buffer = {
151        let mut compression_context = cs.context.borrow_mut();
152        let buffer = compression_context
153            .compress(&chunk)
154            .map_err(|_| Error::Operation(Some("Failed to compress a chunk of input".into())))?;
155
156        // Step 3. If buffer is empty, return.
157        if buffer.is_empty() {
158            return Ok(());
159        }
160        buffer
161    };
162
163    // Step 4. Let arrays be the result of splitting buffer into one or more non-empty pieces and
164    // converting them into Uint8Arrays.
165    // Step 5. For each Uint8Array array of arrays, enqueue array in cs’s transform.
166    // NOTE: We process the result in a single Uint8Array.
167    rooted!(&in(cx) let mut js_object = ptr::null_mut::<JSObject>());
168    let buffer_source = create_buffer_source::<Uint8>(cx, &buffer, js_object.handle_mut())
169        .map_err(|_| Error::Type(c"Cannot convert byte sequence to Uint8Array".to_owned()))?;
170    rooted!(&in(cx) let mut rval = UndefinedValue());
171    buffer_source.to_jsval(cx, rval.handle_mut());
172    controller.enqueue(cx, global, rval.handle())?;
173
174    Ok(())
175}
176
177/// <https://compression.spec.whatwg.org/#compress-flush-and-enqueue>
178pub(crate) fn compress_flush_and_enqueue(
179    cx: &mut JSContext,
180    global: &GlobalScope,
181    cs: &CompressionStream,
182    controller: &TransformStreamDefaultController,
183) -> Fallible<()> {
184    // Step 1. Let buffer be the result of compressing an empty input with cs’s format and context,
185    // with the finish flag.
186    // NOTE: In our implementation, the enum type of context already indicates the format.
187    let buffer = {
188        let mut compression_context = cs.context.borrow_mut();
189        let buffer = compression_context.finalize().map_err(|_| {
190            Error::Operation(Some("Failed to finalize the compression stream".into()))
191        })?;
192
193        // Step 2. If buffer is empty, return.
194        if buffer.is_empty() {
195            return Ok(());
196        }
197        buffer
198    };
199
200    // Step 3. Let arrays be the result of splitting buffer into one or more non-empty pieces and
201    // converting them into Uint8Arrays.
202    // Step 4. For each Uint8Array array of arrays, enqueue array in cs’s transform.
203    // NOTE: We process the result in a single Uint8Array.
204    rooted!(&in(cx) let mut js_object = ptr::null_mut::<JSObject>());
205    let buffer_source = create_buffer_source::<Uint8>(cx, &buffer, js_object.handle_mut())
206        .map_err(|_| Error::Type(c"Cannot convert byte sequence to Uint8Array".to_owned()))?;
207    rooted!(&in(cx) let mut rval = UndefinedValue());
208    buffer_source.to_jsval(cx, rval.handle_mut());
209    controller.enqueue(cx, global, rval.handle())?;
210
211    Ok(())
212}
213
214/// An enum grouping encoders of differenct compression algorithms.
215enum Encoder {
216    #[cfg(feature = "brotli-compression-stream")]
217    Brotli(Box<BrotliEncoder<Vec<u8>>>),
218    Deflate(ZlibEncoder<Vec<u8>>),
219    DeflateRaw(DeflateEncoder<Vec<u8>>),
220    Gzip(GzEncoder<Vec<u8>>),
221}
222
223impl MallocSizeOf for Encoder {
224    fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
225        match self {
226            #[cfg(feature = "brotli-compression-stream")]
227            Encoder::Brotli(encoder) => encoder.shallow_size_of(ops),
228            Encoder::Deflate(encoder) => encoder.size_of(ops),
229            Encoder::DeflateRaw(encoder) => encoder.size_of(ops),
230            Encoder::Gzip(encoder) => encoder.size_of(ops),
231        }
232    }
233}
234
235/// <https://compression.spec.whatwg.org/#compressionstream-context>
236/// Used to encapsulate the logic of encoder.
237#[derive(MallocSizeOf)]
238struct CompressionContext {
239    encoder: Encoder,
240}
241
242impl CompressionContext {
243    fn new(format: CompressionFormat) -> Fallible<CompressionContext> {
244        let encoder = match format {
245            #[cfg(feature = "brotli-compression-stream")]
246            CompressionFormat::Brotli => Encoder::Brotli(Box::new(BrotliEncoder::new(
247                Vec::new(),
248                BROTLI_BUFFER_SIZE,
249                BROTLI_QUALITIY_LEVEL,
250                BROTLI_WINDOW_SIZE,
251            ))),
252            #[cfg(not(feature = "brotli-compression-stream"))]
253            CompressionFormat::Brotli => {
254                return Err(Error::NotSupported(Some("Brotli not supported".into())));
255            },
256            CompressionFormat::Deflate => {
257                Encoder::Deflate(ZlibEncoder::new(Vec::new(), Compression::default()))
258            },
259            CompressionFormat::Deflate_raw => {
260                Encoder::DeflateRaw(DeflateEncoder::new(Vec::new(), Compression::default()))
261            },
262            CompressionFormat::Gzip => {
263                Encoder::Gzip(GzEncoder::new(Vec::new(), Compression::default()))
264            },
265        };
266        Ok(CompressionContext { encoder })
267    }
268
269    fn compress(&mut self, chunk: &[u8]) -> Result<Vec<u8>, io::Error> {
270        let mut result = Vec::new();
271
272        match &mut self.encoder {
273            #[cfg(feature = "brotli-compression-stream")]
274            Encoder::Brotli(encoder) => {
275                encoder.write_all(chunk)?;
276                encoder.flush()?;
277                result.append(encoder.get_mut());
278            },
279            Encoder::Deflate(encoder) => {
280                encoder.write_all(chunk)?;
281                encoder.flush()?;
282                result.append(encoder.get_mut());
283            },
284            Encoder::DeflateRaw(encoder) => {
285                encoder.write_all(chunk)?;
286                encoder.flush()?;
287                result.append(encoder.get_mut());
288            },
289            Encoder::Gzip(encoder) => {
290                encoder.write_all(chunk)?;
291                encoder.flush()?;
292                result.append(encoder.get_mut());
293            },
294        }
295
296        Ok(result)
297    }
298
299    fn finalize(&mut self) -> Result<Vec<u8>, io::Error> {
300        let mut result = Vec::new();
301
302        match &mut self.encoder {
303            #[cfg(feature = "brotli-compression-stream")]
304            Encoder::Brotli(encoder) => {
305                let encoder = std::mem::replace(
306                    encoder.borrow_mut(),
307                    BrotliEncoder::new(
308                        Vec::new(),
309                        BROTLI_BUFFER_SIZE,
310                        BROTLI_QUALITIY_LEVEL,
311                        BROTLI_WINDOW_SIZE,
312                    ),
313                );
314                result = encoder.into_inner();
315            },
316            Encoder::Deflate(encoder) => {
317                encoder.try_finish()?;
318                result.append(encoder.get_mut());
319            },
320            Encoder::DeflateRaw(encoder) => {
321                encoder.try_finish()?;
322                result.append(encoder.get_mut());
323            },
324            Encoder::Gzip(encoder) => {
325                encoder.try_finish()?;
326                result.append(encoder.get_mut());
327            },
328        }
329
330        Ok(result)
331    }
332}
333
334pub(crate) fn convert_chunk_to_vec(
335    cx: &mut JSContext,
336    chunk: SafeHandleValue,
337) -> Result<Vec<u8>, Error> {
338    let conversion_result =
339        ArrayBufferViewOrArrayBuffer::from_jsval(cx, chunk, ()).map_err(|_| {
340            Error::Type(c"Unable to convert chunk into ArrayBuffer or ArrayBufferView".to_owned())
341        })?;
342    let buffer_source = conversion_result.get_success_value().ok_or_else(|| {
343        Error::Type(c"Unable to convert chunk into ArrayBuffer or ArrayBufferView".to_owned())
344    })?;
345    Ok(get_buffer_source_copy(buffer_source.into()))
346}