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