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