1use std::io::Write as _;
21use std::sync::{Arc, Mutex};
22
23use rquickjs::function::Func;
24use rquickjs::{Class, Ctx, Function, Object, TypedArray, Value, class::Trace};
25
26enum Coder {
31 GzipEncode(flate2::write::GzEncoder<Vec<u8>>),
32 DeflateEncode(flate2::write::ZlibEncoder<Vec<u8>>),
33 DeflateRawEncode(flate2::write::DeflateEncoder<Vec<u8>>),
34 GzipDecode(flate2::write::GzDecoder<Vec<u8>>),
35 DeflateDecode(flate2::write::ZlibDecoder<Vec<u8>>),
36 DeflateRawDecode(flate2::write::DeflateDecoder<Vec<u8>>),
37}
38
39impl Coder {
40 fn new(format: &str, decompress: bool) -> Option<Self> {
41 let level = flate2::Compression::default();
42 Some(match (format, decompress) {
43 ("gzip", false) => Self::GzipEncode(flate2::write::GzEncoder::new(Vec::new(), level)),
44 ("deflate", false) => Self::DeflateEncode(flate2::write::ZlibEncoder::new(Vec::new(), level)),
45 ("deflate-raw", false) => Self::DeflateRawEncode(flate2::write::DeflateEncoder::new(Vec::new(), level)),
46 ("gzip", true) => Self::GzipDecode(flate2::write::GzDecoder::new(Vec::new())),
47 ("deflate", true) => Self::DeflateDecode(flate2::write::ZlibDecoder::new(Vec::new())),
48 ("deflate-raw", true) => Self::DeflateRawDecode(flate2::write::DeflateDecoder::new(Vec::new())),
49 _ => return None,
50 })
51 }
52
53 fn push(&mut self, data: &[u8]) -> std::io::Result<Vec<u8>> {
57 match self {
58 Self::GzipEncode(c) => {
59 c.write_all(data)?;
60 Ok(std::mem::take(c.get_mut()))
61 },
62 Self::DeflateEncode(c) => {
63 c.write_all(data)?;
64 Ok(std::mem::take(c.get_mut()))
65 },
66 Self::DeflateRawEncode(c) => {
67 c.write_all(data)?;
68 Ok(std::mem::take(c.get_mut()))
69 },
70 Self::GzipDecode(c) => {
71 c.write_all(data)?;
72 Ok(std::mem::take(c.get_mut()))
73 },
74 Self::DeflateDecode(c) => {
75 c.write_all(data)?;
76 Ok(std::mem::take(c.get_mut()))
77 },
78 Self::DeflateRawDecode(c) => {
79 c.write_all(data)?;
80 Ok(std::mem::take(c.get_mut()))
81 },
82 }
83 }
84
85 fn finish(self) -> std::io::Result<Vec<u8>> {
88 match self {
89 Self::GzipEncode(c) => c.finish(),
90 Self::DeflateEncode(c) => c.finish(),
91 Self::DeflateRawEncode(c) => c.finish(),
92 Self::GzipDecode(c) => c.finish(),
93 Self::DeflateDecode(c) => c.finish(),
94 Self::DeflateRawDecode(c) => c.finish(),
95 }
96 }
97}
98
99type SharedCoder = Arc<Mutex<Option<Coder>>>;
102
103fn lock(coder: &SharedCoder) -> std::sync::MutexGuard<'_, Option<Coder>> {
104 coder.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
105}
106
107fn buffer_source_bytes<'js>(ctx: &Ctx<'js>, chunk: &Value<'js>) -> rquickjs::Result<Vec<u8>> {
111 crate::node::bytes::buffer_source_bytes(ctx, chunk).map_err(|_| {
113 rquickjs::Exception::throw_type(
114 ctx,
115 "Failed to execute 'write': chunk could not be converted to a BufferSource",
116 )
117 })
118}
119
120fn enqueue(controller: &Object<'_>, bytes: Vec<u8>) -> rquickjs::Result<()> {
123 if bytes.is_empty() {
124 return Ok(());
125 }
126 let chunk = TypedArray::new(controller.ctx().clone(), bytes)?.into_value();
127 controller
128 .get::<_, Function<'_>>("enqueue")?
129 .call::<_, ()>((rquickjs::function::This(controller.clone()), chunk))
130}
131
132fn io_error(ctx: &Ctx<'_>, what: &str, e: &std::io::Error) -> rquickjs::Error {
133 rquickjs::Exception::throw_type(ctx, &format!("{what}: {e}"))
134}
135
136fn transform_stream<'js>(ctx: &Ctx<'js>, coder: SharedCoder) -> rquickjs::Result<Object<'js>> {
139 let transformer = Object::new(ctx.clone())?;
140 {
141 let coder = coder.clone();
142 transformer.set(
143 "transform",
144 Func::from(
145 move |ctx: Ctx<'js>, chunk: Value<'js>, controller: Object<'js>| -> rquickjs::Result<()> {
146 let bytes = buffer_source_bytes(&ctx, &chunk)?;
147 let out = {
148 let mut guard = lock(&coder);
149 let Some(coder) = guard.as_mut() else {
150 return Err(rquickjs::Exception::throw_type(&ctx, "the stream is already closed"));
151 };
152 coder
153 .push(&bytes)
154 .map_err(|e| io_error(&ctx, "compression failed", &e))?
155 };
156 enqueue(&controller, out)
157 },
158 ),
159 )?;
160 }
161 {
162 let coder = coder.clone();
163 transformer.set(
164 "flush",
165 Func::from(move |ctx: Ctx<'js>, controller: Object<'js>| -> rquickjs::Result<()> {
166 let out = match lock(&coder).take() {
167 None => return Ok(()),
168 Some(coder) => coder
169 .finish()
170 .map_err(|e| io_error(&ctx, "compression failed at end of stream", &e))?,
171 };
172 enqueue(&controller, out)
173 }),
174 )?;
175 }
176
177 ctx
178 .globals()
179 .get::<_, rquickjs::function::Constructor<'js>>("TransformStream")?
180 .construct((transformer,))
181}
182
183#[derive(Trace)]
186struct Duplex<'js> {
187 readable: Value<'js>,
188 writable: Value<'js>,
189}
190
191impl<'js> Duplex<'js> {
192 fn new(ctx: &Ctx<'js>, format: &str, decompress: bool) -> rquickjs::Result<Self> {
193 let what = if decompress {
194 "DecompressionStream"
195 } else {
196 "CompressionStream"
197 };
198 let Some(coder) = Coder::new(format, decompress) else {
199 return Err(rquickjs::Exception::throw_type(
200 ctx,
201 &format!(
202 "Failed to construct '{what}': '{format}' is not a valid enum value of type CompressionFormat \
203 (expected 'gzip', 'deflate' or 'deflate-raw')"
204 ),
205 ));
206 };
207 let stream = transform_stream(ctx, Arc::new(Mutex::new(Some(coder))))?;
208 Ok(Self {
209 readable: stream.get("readable")?,
210 writable: stream.get("writable")?,
211 })
212 }
213}
214
215#[derive(Trace)]
217#[rquickjs::class(rename = "CompressionStream")]
218pub struct CompressionStreamJs<'js> {
219 inner: Duplex<'js>,
220}
221
222#[derive(Trace)]
224#[rquickjs::class(rename = "DecompressionStream")]
225pub struct DecompressionStreamJs<'js> {
226 inner: Duplex<'js>,
227}
228
229#[allow(unsafe_code)]
230unsafe impl<'js> rquickjs::JsLifetime<'js> for CompressionStreamJs<'js> {
231 type Changed<'to> = CompressionStreamJs<'to>;
232}
233#[allow(unsafe_code)]
234unsafe impl<'js> rquickjs::JsLifetime<'js> for DecompressionStreamJs<'js> {
235 type Changed<'to> = DecompressionStreamJs<'to>;
236}
237
238#[rquickjs::methods]
239impl<'js> CompressionStreamJs<'js> {
240 #[qjs(prop, rename = rquickjs::atom::PredefinedAtom::SymbolToStringTag, configurable)]
243 pub fn to_string_tag() -> &'static str {
244 "CompressionStream"
245 }
246
247 #[qjs(constructor)]
248 pub fn new(ctx: Ctx<'js>, format: String) -> rquickjs::Result<Self> {
249 Ok(Self {
250 inner: Duplex::new(&ctx, &format, false)?,
251 })
252 }
253
254 #[qjs(get, rename = "readable")]
255 pub fn readable(&self) -> Value<'js> {
256 self.inner.readable.clone()
257 }
258
259 #[qjs(get, rename = "writable")]
260 pub fn writable(&self) -> Value<'js> {
261 self.inner.writable.clone()
262 }
263}
264
265#[rquickjs::methods]
266impl<'js> DecompressionStreamJs<'js> {
267 #[qjs(prop, rename = rquickjs::atom::PredefinedAtom::SymbolToStringTag, configurable)]
270 pub fn to_string_tag() -> &'static str {
271 "DecompressionStream"
272 }
273
274 #[qjs(constructor)]
275 pub fn new(ctx: Ctx<'js>, format: String) -> rquickjs::Result<Self> {
276 Ok(Self {
277 inner: Duplex::new(&ctx, &format, true)?,
278 })
279 }
280
281 #[qjs(get, rename = "readable")]
282 pub fn readable(&self) -> Value<'js> {
283 self.inner.readable.clone()
284 }
285
286 #[qjs(get, rename = "writable")]
287 pub fn writable(&self) -> Value<'js> {
288 self.inner.writable.clone()
289 }
290}
291
292pub fn install(ctx: &Ctx<'_>) -> rquickjs::Result<()> {
293 let globals = ctx.globals();
294 Class::<CompressionStreamJs<'_>>::define(&globals)?;
295 Class::<DecompressionStreamJs<'_>>::define(&globals)?;
296 Ok(())
297}