use std::io::Write as _;
use std::sync::{Arc, Mutex};
use rquickjs::function::Func;
use rquickjs::{Class, Ctx, Function, Object, TypedArray, Value, class::Trace};
enum Coder {
GzipEncode(flate2::write::GzEncoder<Vec<u8>>),
DeflateEncode(flate2::write::ZlibEncoder<Vec<u8>>),
DeflateRawEncode(flate2::write::DeflateEncoder<Vec<u8>>),
GzipDecode(flate2::write::GzDecoder<Vec<u8>>),
DeflateDecode(flate2::write::ZlibDecoder<Vec<u8>>),
DeflateRawDecode(flate2::write::DeflateDecoder<Vec<u8>>),
}
impl Coder {
fn new(format: &str, decompress: bool) -> Option<Self> {
let level = flate2::Compression::default();
Some(match (format, decompress) {
("gzip", false) => Self::GzipEncode(flate2::write::GzEncoder::new(Vec::new(), level)),
("deflate", false) => Self::DeflateEncode(flate2::write::ZlibEncoder::new(Vec::new(), level)),
("deflate-raw", false) => Self::DeflateRawEncode(flate2::write::DeflateEncoder::new(Vec::new(), level)),
("gzip", true) => Self::GzipDecode(flate2::write::GzDecoder::new(Vec::new())),
("deflate", true) => Self::DeflateDecode(flate2::write::ZlibDecoder::new(Vec::new())),
("deflate-raw", true) => Self::DeflateRawDecode(flate2::write::DeflateDecoder::new(Vec::new())),
_ => return None,
})
}
fn push(&mut self, data: &[u8]) -> std::io::Result<Vec<u8>> {
match self {
Self::GzipEncode(c) => {
c.write_all(data)?;
Ok(std::mem::take(c.get_mut()))
},
Self::DeflateEncode(c) => {
c.write_all(data)?;
Ok(std::mem::take(c.get_mut()))
},
Self::DeflateRawEncode(c) => {
c.write_all(data)?;
Ok(std::mem::take(c.get_mut()))
},
Self::GzipDecode(c) => {
c.write_all(data)?;
Ok(std::mem::take(c.get_mut()))
},
Self::DeflateDecode(c) => {
c.write_all(data)?;
Ok(std::mem::take(c.get_mut()))
},
Self::DeflateRawDecode(c) => {
c.write_all(data)?;
Ok(std::mem::take(c.get_mut()))
},
}
}
fn finish(self) -> std::io::Result<Vec<u8>> {
match self {
Self::GzipEncode(c) => c.finish(),
Self::DeflateEncode(c) => c.finish(),
Self::DeflateRawEncode(c) => c.finish(),
Self::GzipDecode(c) => c.finish(),
Self::DeflateDecode(c) => c.finish(),
Self::DeflateRawDecode(c) => c.finish(),
}
}
}
type SharedCoder = Arc<Mutex<Option<Coder>>>;
fn lock(coder: &SharedCoder) -> std::sync::MutexGuard<'_, Option<Coder>> {
coder.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn buffer_source_bytes<'js>(ctx: &Ctx<'js>, chunk: &Value<'js>) -> rquickjs::Result<Vec<u8>> {
crate::node::bytes::buffer_source_bytes(ctx, chunk).map_err(|_| {
rquickjs::Exception::throw_type(
ctx,
"Failed to execute 'write': chunk could not be converted to a BufferSource",
)
})
}
fn enqueue(controller: &Object<'_>, bytes: Vec<u8>) -> rquickjs::Result<()> {
if bytes.is_empty() {
return Ok(());
}
let chunk = TypedArray::new(controller.ctx().clone(), bytes)?.into_value();
controller
.get::<_, Function<'_>>("enqueue")?
.call::<_, ()>((rquickjs::function::This(controller.clone()), chunk))
}
fn io_error(ctx: &Ctx<'_>, what: &str, e: &std::io::Error) -> rquickjs::Error {
rquickjs::Exception::throw_type(ctx, &format!("{what}: {e}"))
}
fn transform_stream<'js>(ctx: &Ctx<'js>, coder: SharedCoder) -> rquickjs::Result<Object<'js>> {
let transformer = Object::new(ctx.clone())?;
{
let coder = coder.clone();
transformer.set(
"transform",
Func::from(
move |ctx: Ctx<'js>, chunk: Value<'js>, controller: Object<'js>| -> rquickjs::Result<()> {
let bytes = buffer_source_bytes(&ctx, &chunk)?;
let out = {
let mut guard = lock(&coder);
let Some(coder) = guard.as_mut() else {
return Err(rquickjs::Exception::throw_type(&ctx, "the stream is already closed"));
};
coder
.push(&bytes)
.map_err(|e| io_error(&ctx, "compression failed", &e))?
};
enqueue(&controller, out)
},
),
)?;
}
{
let coder = coder.clone();
transformer.set(
"flush",
Func::from(move |ctx: Ctx<'js>, controller: Object<'js>| -> rquickjs::Result<()> {
let out = match lock(&coder).take() {
None => return Ok(()),
Some(coder) => coder
.finish()
.map_err(|e| io_error(&ctx, "compression failed at end of stream", &e))?,
};
enqueue(&controller, out)
}),
)?;
}
ctx
.globals()
.get::<_, rquickjs::function::Constructor<'js>>("TransformStream")?
.construct((transformer,))
}
#[derive(Trace)]
struct Duplex<'js> {
readable: Value<'js>,
writable: Value<'js>,
}
impl<'js> Duplex<'js> {
fn new(ctx: &Ctx<'js>, format: &str, decompress: bool) -> rquickjs::Result<Self> {
let what = if decompress {
"DecompressionStream"
} else {
"CompressionStream"
};
let Some(coder) = Coder::new(format, decompress) else {
return Err(rquickjs::Exception::throw_type(
ctx,
&format!(
"Failed to construct '{what}': '{format}' is not a valid enum value of type CompressionFormat \
(expected 'gzip', 'deflate' or 'deflate-raw')"
),
));
};
let stream = transform_stream(ctx, Arc::new(Mutex::new(Some(coder))))?;
Ok(Self {
readable: stream.get("readable")?,
writable: stream.get("writable")?,
})
}
}
#[derive(Trace)]
#[rquickjs::class(rename = "CompressionStream")]
pub struct CompressionStreamJs<'js> {
inner: Duplex<'js>,
}
#[derive(Trace)]
#[rquickjs::class(rename = "DecompressionStream")]
pub struct DecompressionStreamJs<'js> {
inner: Duplex<'js>,
}
#[allow(unsafe_code)]
unsafe impl<'js> rquickjs::JsLifetime<'js> for CompressionStreamJs<'js> {
type Changed<'to> = CompressionStreamJs<'to>;
}
#[allow(unsafe_code)]
unsafe impl<'js> rquickjs::JsLifetime<'js> for DecompressionStreamJs<'js> {
type Changed<'to> = DecompressionStreamJs<'to>;
}
#[rquickjs::methods]
impl<'js> CompressionStreamJs<'js> {
#[qjs(prop, rename = rquickjs::atom::PredefinedAtom::SymbolToStringTag, configurable)]
pub fn to_string_tag() -> &'static str {
"CompressionStream"
}
#[qjs(constructor)]
pub fn new(ctx: Ctx<'js>, format: String) -> rquickjs::Result<Self> {
Ok(Self {
inner: Duplex::new(&ctx, &format, false)?,
})
}
#[qjs(get, rename = "readable")]
pub fn readable(&self) -> Value<'js> {
self.inner.readable.clone()
}
#[qjs(get, rename = "writable")]
pub fn writable(&self) -> Value<'js> {
self.inner.writable.clone()
}
}
#[rquickjs::methods]
impl<'js> DecompressionStreamJs<'js> {
#[qjs(prop, rename = rquickjs::atom::PredefinedAtom::SymbolToStringTag, configurable)]
pub fn to_string_tag() -> &'static str {
"DecompressionStream"
}
#[qjs(constructor)]
pub fn new(ctx: Ctx<'js>, format: String) -> rquickjs::Result<Self> {
Ok(Self {
inner: Duplex::new(&ctx, &format, true)?,
})
}
#[qjs(get, rename = "readable")]
pub fn readable(&self) -> Value<'js> {
self.inner.readable.clone()
}
#[qjs(get, rename = "writable")]
pub fn writable(&self) -> Value<'js> {
self.inner.writable.clone()
}
}
pub fn install(ctx: &Ctx<'_>) -> rquickjs::Result<()> {
let globals = ctx.globals();
Class::<CompressionStreamJs<'_>>::define(&globals)?;
Class::<DecompressionStreamJs<'_>>::define(&globals)?;
Ok(())
}