Skip to main content

nodejs/stdlib/
zlib.rs

1//! Node `zlib` module — real DEFLATE / zlib / gzip / brotli / zstd + CRC-32.
2//!
3//! One-shot compression is backed by real codecs: `flate2` for deflate/zlib/gzip
4//! (`Compression::default()` == level 6, matching node's default), the `brotli`
5//! crate (quality 11, lgwin 22 — node's `BROTLI_DEFAULT_QUALITY`/`BROTLI_DEFAULT_WINDOW`)
6//! for brotli, and `zstd` (level 3 == `ZSTD_CLEVEL_DEFAULT`) for zstd. Round-trips
7//! and cross-decoding with the real `node` binary hold for every codec, and
8//! `zlib.crc32` uses `crc32fast`.
9//!
10//! Every one-shot function ships in both flavours: the synchronous `*Sync` form
11//! returns a Buffer directly, and the asynchronous form runs the same codec and
12//! invokes `callback(err, buffer)` via a queued microtask (node's zlib async work
13//! is off-thread, but the codecs here are fast enough to run inline before the
14//! callback fires, so results are identical). `zlib.unzip`/`unzipSync` auto-detect
15//! gzip vs zlib framing by magic bytes, matching node's `Unzip`.
16//!
17//! The streaming transform classes (`Deflate`, `Gunzip`, `BrotliCompress`, …) and
18//! their `create*` factories still require a streaming Transform backend we don't
19//! have here, so those return an honest "not supported" error rather than a
20//! silently-wrong result.
21
22use crate::host::{with_host, JsObj};
23use flate2::read::{DeflateDecoder, GzDecoder, ZlibDecoder};
24use flate2::write::{DeflateEncoder, GzEncoder, ZlibEncoder};
25use flate2::Compression;
26use fusevm::Value;
27use std::io::{Read, Write};
28
29use super::buffer;
30
31/// `zlib` module functions routed through `stdlib::call`.
32pub const MODULE_METHODS: &[&str] = &[
33    // One-shot synchronous (return a Buffer).
34    "gzipSync",
35    "gunzipSync",
36    "deflateSync",
37    "inflateSync",
38    "deflateRawSync",
39    "inflateRawSync",
40    "unzipSync",
41    "brotliCompressSync",
42    "brotliDecompressSync",
43    "zstdCompressSync",
44    "zstdDecompressSync",
45    // One-shot asynchronous (invoke callback(err, buffer)).
46    "gzip",
47    "gunzip",
48    "deflate",
49    "inflate",
50    "deflateRaw",
51    "inflateRaw",
52    "unzip",
53    "brotliCompress",
54    "brotliDecompress",
55    "zstdCompress",
56    "zstdDecompress",
57    // CRC-32 checksum (node 22+): returns a number.
58    "crc32",
59    // Streaming factories — honest errors (no streaming Transform backend).
60    "createDeflate",
61    "createInflate",
62    "createGzip",
63    "createGunzip",
64    "createDeflateRaw",
65    "createInflateRaw",
66    "createUnzip",
67    "createBrotliCompress",
68    "createBrotliDecompress",
69    "createZstdCompress",
70    "createZstdDecompress",
71];
72
73pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
74    // Streaming factories are unsupported — honest error, never a fake.
75    if method.starts_with("create") {
76        return Some(Err(crate::host::type_error(&format!(
77            "zlib.{method} is not supported in node-js (no streaming backend)"
78        ))));
79    }
80
81    // crc32(data[, value]) -> number, not a Buffer.
82    if method == "crc32" {
83        let data = input_bytes(args);
84        let init = {
85            let n = super::arg_num(args, 1);
86            if n.is_nan() {
87                0
88            } else {
89                n as i64 as u32
90            }
91        };
92        return Some(Ok(Value::Float(crc32(&data, init) as f64)));
93    }
94
95    // Asynchronous variants: op(buffer[, options], callback).
96    if is_async(method) {
97        return Some(run_async(method, args));
98    }
99
100    // Synchronous variants: `<op>Sync` -> Buffer. Compute the full output BEFORE
101    // re-entering the host to allocate the result Buffer, so the two `with_host`
102    // calls (input read, output alloc) never nest into a double borrow.
103    let base = method.strip_suffix("Sync")?;
104    let out = oneshot(base, &input_bytes(args));
105    Some(out.map(|bytes| buffer::from_bytes(&bytes)))
106}
107
108/// Names that take a trailing `callback(err, buffer)`.
109fn is_async(method: &str) -> bool {
110    matches!(
111        method,
112        "gzip"
113            | "gunzip"
114            | "deflate"
115            | "inflate"
116            | "deflateRaw"
117            | "inflateRaw"
118            | "unzip"
119            | "brotliCompress"
120            | "brotliDecompress"
121            | "zstdCompress"
122            | "zstdDecompress"
123    )
124}
125
126/// Run `op` and invoke the trailing callback with `(err, buffer)`.
127fn run_async(op: &str, args: &[Value]) -> Result<Value, String> {
128    let Some(cb) = args.last().cloned() else {
129        return Ok(Value::Undef);
130    };
131    let input = input_bytes(args);
132    let (err, buf) = match oneshot(op, &input) {
133        Ok(bytes) => (with_host(|h| h.null()), buffer::from_bytes(&bytes)),
134        Err(e) => (with_host(|h| h.new_str(e)), Value::Undef),
135    };
136    with_host(|h| h.queue_micro(cb, vec![err, buf]));
137    Ok(Value::Undef)
138}
139
140/// Dispatch a codec op name (no `Sync` suffix) to its byte transform.
141fn oneshot(op: &str, input: &[u8]) -> Result<Vec<u8>, String> {
142    match op {
143        "gzip" => gzip(input),
144        "gunzip" => gunzip(input),
145        "deflate" => deflate(input),
146        "inflate" => inflate(input),
147        "deflateRaw" => deflate_raw(input),
148        "inflateRaw" => inflate_raw(input),
149        "unzip" => unzip(input),
150        "brotliCompress" => brotli_compress(input),
151        "brotliDecompress" => brotli_decompress(input),
152        "zstdCompress" => zstd_compress(input),
153        "zstdDecompress" => zstd_decompress(input),
154        _ => Err(format!("Error: unknown zlib op '{op}'")),
155    }
156}
157
158/// Input bytes of `args[0]`: a Buffer's backing `@@bytes`, else the utf-8 bytes of
159/// its string coercion (node accepts a Buffer, TypedArray, DataView, or string).
160fn input_bytes(args: &[Value]) -> Vec<u8> {
161    let v = args.first().cloned().unwrap_or(Value::Undef);
162    with_host(|h| match h.get(&v) {
163        Some(JsObj::Object(p)) => match p.get("@@bytes").and_then(|b| h.get(b)) {
164            Some(JsObj::Array(items)) => items.iter().map(|x| h.to_number(x) as u8).collect(),
165            _ => h.str_of(&v).into_bytes(),
166        },
167        _ => h.str_of(&v).into_bytes(),
168    })
169}
170
171/// Map an I/O error (a malformed compressed stream, etc.) to a node-style message.
172fn io_err(e: std::io::Error) -> String {
173    format!("Error: {e}")
174}
175
176fn gzip(input: &[u8]) -> Result<Vec<u8>, String> {
177    let mut enc = GzEncoder::new(Vec::new(), Compression::default());
178    enc.write_all(input).map_err(io_err)?;
179    enc.finish().map_err(io_err)
180}
181
182fn gunzip(input: &[u8]) -> Result<Vec<u8>, String> {
183    let mut out = Vec::new();
184    GzDecoder::new(input)
185        .read_to_end(&mut out)
186        .map_err(io_err)?;
187    Ok(out)
188}
189
190fn deflate(input: &[u8]) -> Result<Vec<u8>, String> {
191    let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
192    enc.write_all(input).map_err(io_err)?;
193    enc.finish().map_err(io_err)
194}
195
196fn inflate(input: &[u8]) -> Result<Vec<u8>, String> {
197    let mut out = Vec::new();
198    ZlibDecoder::new(input)
199        .read_to_end(&mut out)
200        .map_err(io_err)?;
201    Ok(out)
202}
203
204fn deflate_raw(input: &[u8]) -> Result<Vec<u8>, String> {
205    let mut enc = DeflateEncoder::new(Vec::new(), Compression::default());
206    enc.write_all(input).map_err(io_err)?;
207    enc.finish().map_err(io_err)
208}
209
210fn inflate_raw(input: &[u8]) -> Result<Vec<u8>, String> {
211    let mut out = Vec::new();
212    DeflateDecoder::new(input)
213        .read_to_end(&mut out)
214        .map_err(io_err)?;
215    Ok(out)
216}
217
218/// `unzip`: auto-detect gzip (magic `1f 8b`) vs zlib framing, like node's `Unzip`.
219fn unzip(input: &[u8]) -> Result<Vec<u8>, String> {
220    if input.starts_with(&[0x1f, 0x8b]) {
221        gunzip(input)
222    } else {
223        inflate(input)
224    }
225}
226
227fn brotli_compress(input: &[u8]) -> Result<Vec<u8>, String> {
228    let mut out = Vec::new();
229    {
230        // buffer_size 4096, quality 11, lgwin 22 (node defaults).
231        let mut enc = brotli::CompressorWriter::new(&mut out, 4096, 11, 22);
232        enc.write_all(input).map_err(io_err)?;
233        // Drop flushes/finalizes the stream at end of scope.
234    }
235    Ok(out)
236}
237
238fn brotli_decompress(input: &[u8]) -> Result<Vec<u8>, String> {
239    let mut out = Vec::new();
240    brotli::Decompressor::new(input, 4096)
241        .read_to_end(&mut out)
242        .map_err(io_err)?;
243    Ok(out)
244}
245
246fn zstd_compress(input: &[u8]) -> Result<Vec<u8>, String> {
247    // level 3 == ZSTD_CLEVEL_DEFAULT (node's default).
248    zstd::encode_all(input, 3).map_err(io_err)
249}
250
251fn zstd_decompress(input: &[u8]) -> Result<Vec<u8>, String> {
252    zstd::decode_all(input).map_err(io_err)
253}
254
255/// CRC-32 (IEEE) of `data`, seeded with `init` (node's `zlib.crc32(data, value)`).
256fn crc32(data: &[u8], init: u32) -> u32 {
257    let mut h = crc32fast::Hasher::new_with_initial(init);
258    h.update(data);
259    h.finalize()
260}