use std::borrow::BorrowMut;
use std::cell::RefCell;
use std::io::{self, Write};
use std::ptr;
use brotli::CompressorWriter as BrotliEncoder;
use dom_struct::dom_struct;
use flate2::Compression;
use flate2::write::{DeflateEncoder, GzEncoder, ZlibEncoder};
use js::jsapi::JSObject;
use js::jsval::UndefinedValue;
use js::rust::{HandleObject as SafeHandleObject, HandleValue as SafeHandleValue};
use js::typedarray::Uint8;
use malloc_size_of::{MallocSizeOf, MallocSizeOfOps};
use crate::dom::bindings::buffer_source::create_buffer_source;
use crate::dom::bindings::codegen::Bindings::CompressionStreamBinding::{
CompressionFormat, CompressionStreamMethods,
};
use crate::dom::bindings::codegen::UnionTypes::ArrayBufferViewOrArrayBuffer;
use crate::dom::bindings::conversions::{SafeFromJSValConvertible, SafeToJSValConvertible};
use crate::dom::bindings::error::{Error, Fallible};
use crate::dom::bindings::reflector::{Reflector, reflect_dom_object_with_proto_and_cx};
use crate::dom::bindings::root::{Dom, DomRoot};
use crate::dom::stream::transformstreamdefaultcontroller::TransformerType;
use crate::dom::types::{
GlobalScope, ReadableStream, TransformStream, TransformStreamDefaultController, WritableStream,
};
use crate::script_runtime::{CanGc, JSContext as SafeJSContext};
pub(crate) const BROTLI_BUFFER_SIZE: usize = 4096;
const BROTLI_QUALITIY_LEVEL: u32 = 5;
const BROTLI_WINDOW_SIZE: u32 = 22;
#[dom_struct]
pub(crate) struct CompressionStream {
reflector_: Reflector,
transform: Dom<TransformStream>,
format: CompressionFormat,
#[no_trace]
context: RefCell<CompressionContext>,
}
impl CompressionStream {
fn new_inherited(transform: &TransformStream, format: CompressionFormat) -> CompressionStream {
CompressionStream {
reflector_: Reflector::new(),
transform: Dom::from_ref(transform),
format,
context: RefCell::new(CompressionContext::new(format)),
}
}
fn new_with_proto(
cx: &mut js::context::JSContext,
global: &GlobalScope,
proto: Option<SafeHandleObject>,
transform: &TransformStream,
format: CompressionFormat,
) -> DomRoot<CompressionStream> {
reflect_dom_object_with_proto_and_cx(
Box::new(CompressionStream::new_inherited(transform, format)),
global,
proto,
cx,
)
}
}
impl CompressionStreamMethods<crate::DomTypeHolder> for CompressionStream {
fn Constructor(
cx: &mut js::context::JSContext,
global: &GlobalScope,
proto: Option<SafeHandleObject>,
format: CompressionFormat,
) -> Fallible<DomRoot<CompressionStream>> {
let transform = TransformStream::new_with_proto(global, None, CanGc::from_cx(cx));
let compression_stream =
CompressionStream::new_with_proto(cx, global, proto, &transform, format);
let transformer_type = TransformerType::Compressor(compression_stream.clone());
transform.set_up(cx, global, transformer_type)?;
Ok(compression_stream)
}
fn Readable(&self) -> DomRoot<ReadableStream> {
self.transform.get_readable()
}
fn Writable(&self) -> DomRoot<WritableStream> {
self.transform.get_writable()
}
}
pub(crate) fn compress_and_enqueue_a_chunk(
cx: &mut js::context::JSContext,
global: &GlobalScope,
cs: &CompressionStream,
chunk: SafeHandleValue,
controller: &TransformStreamDefaultController,
) -> Fallible<()> {
let chunk = convert_chunk_to_vec(cx.into(), chunk, CanGc::from_cx(cx))?;
let mut compression_context = cs.context.borrow_mut();
let buffer = compression_context
.compress(&chunk)
.map_err(|_| Error::Operation(Some("Failed to compress a chunk of input".into())))?;
if buffer.is_empty() {
return Ok(());
}
rooted!(&in(cx) let mut js_object = ptr::null_mut::<JSObject>());
let buffer_source = create_buffer_source::<Uint8>(
cx.into(),
&buffer,
js_object.handle_mut(),
CanGc::from_cx(cx),
)
.map_err(|_| Error::Type(c"Cannot convert byte sequence to Uint8Array".to_owned()))?;
rooted!(&in(cx) let mut rval = UndefinedValue());
buffer_source.safe_to_jsval(cx.into(), rval.handle_mut(), CanGc::from_cx(cx));
controller.enqueue(cx, global, rval.handle())?;
Ok(())
}
pub(crate) fn compress_flush_and_enqueue(
cx: &mut js::context::JSContext,
global: &GlobalScope,
cs: &CompressionStream,
controller: &TransformStreamDefaultController,
) -> Fallible<()> {
let mut compression_context = cs.context.borrow_mut();
let buffer = compression_context
.finalize()
.map_err(|_| Error::Operation(Some("Failed to finalize the compression stream".into())))?;
if buffer.is_empty() {
return Ok(());
}
rooted!(&in(cx) let mut js_object = ptr::null_mut::<JSObject>());
let buffer_source = create_buffer_source::<Uint8>(
cx.into(),
&buffer,
js_object.handle_mut(),
CanGc::from_cx(cx),
)
.map_err(|_| Error::Type(c"Cannot convert byte sequence to Uint8Array".to_owned()))?;
rooted!(&in(cx) let mut rval = UndefinedValue());
buffer_source.safe_to_jsval(cx.into(), rval.handle_mut(), CanGc::from_cx(cx));
controller.enqueue(cx, global, rval.handle())?;
Ok(())
}
enum Encoder {
Brotli(Box<BrotliEncoder<Vec<u8>>>),
Deflate(ZlibEncoder<Vec<u8>>),
DeflateRaw(DeflateEncoder<Vec<u8>>),
Gzip(GzEncoder<Vec<u8>>),
}
impl MallocSizeOf for Encoder {
#[expect(unsafe_code)]
fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
match self {
Encoder::Brotli(encoder) => unsafe { ops.malloc_size_of(&**encoder) },
Encoder::Deflate(encoder) => encoder.size_of(ops),
Encoder::DeflateRaw(encoder) => encoder.size_of(ops),
Encoder::Gzip(encoder) => encoder.size_of(ops),
}
}
}
#[derive(MallocSizeOf)]
struct CompressionContext {
encoder: Encoder,
}
impl CompressionContext {
fn new(format: CompressionFormat) -> CompressionContext {
let encoder = match format {
CompressionFormat::Brotli => Encoder::Brotli(Box::new(BrotliEncoder::new(
Vec::new(),
BROTLI_BUFFER_SIZE,
BROTLI_QUALITIY_LEVEL,
BROTLI_WINDOW_SIZE,
))),
CompressionFormat::Deflate => {
Encoder::Deflate(ZlibEncoder::new(Vec::new(), Compression::default()))
},
CompressionFormat::Deflate_raw => {
Encoder::DeflateRaw(DeflateEncoder::new(Vec::new(), Compression::default()))
},
CompressionFormat::Gzip => {
Encoder::Gzip(GzEncoder::new(Vec::new(), Compression::default()))
},
};
CompressionContext { encoder }
}
fn compress(&mut self, chunk: &[u8]) -> Result<Vec<u8>, io::Error> {
let mut result = Vec::new();
match &mut self.encoder {
Encoder::Brotli(encoder) => {
encoder.write_all(chunk)?;
encoder.flush()?;
result.append(encoder.get_mut());
},
Encoder::Deflate(encoder) => {
encoder.write_all(chunk)?;
encoder.flush()?;
result.append(encoder.get_mut());
},
Encoder::DeflateRaw(encoder) => {
encoder.write_all(chunk)?;
encoder.flush()?;
result.append(encoder.get_mut());
},
Encoder::Gzip(encoder) => {
encoder.write_all(chunk)?;
encoder.flush()?;
result.append(encoder.get_mut());
},
}
Ok(result)
}
fn finalize(&mut self) -> Result<Vec<u8>, io::Error> {
let mut result = Vec::new();
match &mut self.encoder {
Encoder::Brotli(encoder) => {
let encoder = std::mem::replace(
encoder.borrow_mut(),
BrotliEncoder::new(
Vec::new(),
BROTLI_BUFFER_SIZE,
BROTLI_QUALITIY_LEVEL,
BROTLI_WINDOW_SIZE,
),
);
result = encoder.into_inner();
},
Encoder::Deflate(encoder) => {
encoder.try_finish()?;
result.append(encoder.get_mut());
},
Encoder::DeflateRaw(encoder) => {
encoder.try_finish()?;
result.append(encoder.get_mut());
},
Encoder::Gzip(encoder) => {
encoder.try_finish()?;
result.append(encoder.get_mut());
},
}
Ok(result)
}
}
pub(crate) fn convert_chunk_to_vec(
cx: SafeJSContext,
chunk: SafeHandleValue,
can_gc: CanGc,
) -> Result<Vec<u8>, Error> {
let conversion_result = ArrayBufferViewOrArrayBuffer::safe_from_jsval(cx, chunk, (), can_gc)
.map_err(|_| {
Error::Type(c"Unable to convert chunk into ArrayBuffer or ArrayBufferView".to_owned())
})?;
let buffer_source = conversion_result.get_success_value().ok_or_else(|| {
Error::Type(c"Unable to convert chunk into ArrayBuffer or ArrayBufferView".to_owned())
})?;
match buffer_source {
ArrayBufferViewOrArrayBuffer::ArrayBufferView(view) => Ok(view.to_vec()),
ArrayBufferViewOrArrayBuffer::ArrayBuffer(buffer) => Ok(buffer.to_vec()),
}
}