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