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