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;
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        // An error-first callback receives an ERROR OBJECT, not the message:
135        // `zlib.gunzip(garbage, cb)` gives an `err` whose `code` is
136        // `Z_DATA_ERROR`. A bare string made that — and `err.message` — all
137        // `undefined`.
138        // `decode_err` has already built the coded Error and parked it as the
139        // pending exception for the SYNC path; take that rather than rebuilding
140        // one from the message, which carries no `code`/`errno`.
141        Err(e) => (
142            with_host(|h| {
143                h.exc
144                    .take()
145                    .unwrap_or_else(|| crate::builtins::synth_error(h, &e))
146            }),
147            Value::Undef,
148        ),
149    };
150    with_host(|h| h.queue_micro(cb, vec![err, buf]));
151    Ok(Value::Undef)
152}
153
154/// Dispatch a codec op name (no `Sync` suffix) to its byte transform.
155fn oneshot(op: &str, input: &[u8]) -> Result<Vec<u8>, String> {
156    match op {
157        "gzip" => gzip(input),
158        "gunzip" => gunzip(input),
159        "deflate" => deflate(input),
160        "inflate" => inflate(input),
161        "deflateRaw" => deflate_raw(input),
162        "inflateRaw" => inflate_raw(input),
163        "unzip" => unzip(input),
164        "brotliCompress" => brotli_compress(input),
165        "brotliDecompress" => brotli_decompress(input),
166        "zstdCompress" => zstd_compress(input),
167        "zstdDecompress" => zstd_decompress(input),
168        _ => Err(format!("Error: unknown zlib op '{op}'")),
169    }
170}
171
172/// Input bytes of `args[0]`: a Buffer's backing `@@bytes`, else the utf-8 bytes of
173/// its string coercion (node accepts a Buffer, TypedArray, DataView, or string).
174fn input_bytes(args: &[Value]) -> Vec<u8> {
175    let v = args.first().cloned().unwrap_or(Value::Undef);
176    // Only a Buffer's own `@@bytes` was read, so a TypedArray or DataView —
177    // both of which this comment already promised to accept — was stringified
178    // and compressed as the text `[object Object]`.
179    match super::buffer::view_bytes(&v) {
180        Some(b) => b,
181        None => with_host(|h| h.str_of(&v)).into_bytes(),
182    }
183}
184
185/// Map an I/O error (a malformed compressed stream, etc.) to a node-style message.
186fn io_err(e: std::io::Error) -> String {
187    format!("Error: {e}")
188}
189
190/// A decode failure, worded and coded as zlib does. Node carries `err.code`
191/// (`Z_DATA_ERROR`) and `err.errno` (-3), which callers branch on; a plain
192/// message left both `undefined` and reported Rust's wording — `corrupt
193/// deflate stream` for a header zlib calls an `incorrect header check`.
194///
195/// The two cases are told apart by looking at the HEADER: a stream whose magic
196/// is wrong never decodes, while one that passes the header and then runs out
197/// is a truncation, which zlib reports as a BUF error instead.
198fn decode_err(truncated: bool) -> String {
199    let (code, errno, msg) = if truncated {
200        ("Z_BUF_ERROR", -5, "unexpected end of file")
201    } else {
202        ("Z_DATA_ERROR", -3, "incorrect header check")
203    };
204    let e = crate::builtins::make_error_pub("Error", msg);
205    for (k, v) in [
206        ("errno", Value::Float(errno as f64)),
207        ("code", with_host(|h| h.new_str(code.to_string()))),
208    ] {
209        let _ = crate::builtins::set_property_pub(&e, k, v);
210    }
211    with_host(|h| h.exc = Some(e));
212    format!("Error: {msg}")
213}
214
215/// Whether `input` carries a well-formed header for `kind`, so a failure after
216/// it is a truncation rather than garbage.
217fn header_ok(kind: &str, input: &[u8]) -> bool {
218    match kind {
219        "gzip" => input.len() >= 2 && input[0] == 0x1f && input[1] == 0x8b,
220        // RFC 1950: CM must be 8 and the two header bytes are a multiple of 31.
221        "zlib" => {
222            input.len() >= 2
223                && input[0] & 0x0f == 8
224                && (u16::from(input[0]) * 256 + u16::from(input[1])) % 31 == 0
225        }
226        // A raw deflate stream has no header to check.
227        _ => true,
228    }
229}
230
231fn gzip(input: &[u8]) -> Result<Vec<u8>, String> {
232    let mut enc = GzEncoder::new(Vec::new(), Compression::default());
233    enc.write_all(input).map_err(io_err)?;
234    enc.finish().map_err(io_err)
235}
236
237fn gunzip(input: &[u8]) -> Result<Vec<u8>, String> {
238    let mut out = Vec::new();
239    GzDecoder::new(input)
240        .read_to_end(&mut out)
241        .map_err(|_| decode_err(header_ok("gzip", input)))?;
242    Ok(out)
243}
244
245fn deflate(input: &[u8]) -> Result<Vec<u8>, String> {
246    let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
247    enc.write_all(input).map_err(io_err)?;
248    enc.finish().map_err(io_err)
249}
250
251fn inflate(input: &[u8]) -> Result<Vec<u8>, String> {
252    let mut out = Vec::new();
253    ZlibDecoder::new(input)
254        .read_to_end(&mut out)
255        .map_err(|_| decode_err(header_ok("zlib", input)))?;
256    Ok(out)
257}
258
259fn deflate_raw(input: &[u8]) -> Result<Vec<u8>, String> {
260    let mut enc = DeflateEncoder::new(Vec::new(), Compression::default());
261    enc.write_all(input).map_err(io_err)?;
262    enc.finish().map_err(io_err)
263}
264
265fn inflate_raw(input: &[u8]) -> Result<Vec<u8>, String> {
266    let mut out = Vec::new();
267    DeflateDecoder::new(input)
268        .read_to_end(&mut out)
269        .map_err(io_err)?;
270    Ok(out)
271}
272
273/// `unzip`: auto-detect gzip (magic `1f 8b`) vs zlib framing, like node's `Unzip`.
274fn unzip(input: &[u8]) -> Result<Vec<u8>, String> {
275    if input.starts_with(&[0x1f, 0x8b]) {
276        gunzip(input)
277    } else {
278        inflate(input)
279    }
280}
281
282fn brotli_compress(input: &[u8]) -> Result<Vec<u8>, String> {
283    let mut out = Vec::new();
284    {
285        // buffer_size 4096, quality 11, lgwin 22 (node defaults).
286        let mut enc = brotli::CompressorWriter::new(&mut out, 4096, 11, 22);
287        enc.write_all(input).map_err(io_err)?;
288        // Drop flushes/finalizes the stream at end of scope.
289    }
290    Ok(out)
291}
292
293fn brotli_decompress(input: &[u8]) -> Result<Vec<u8>, String> {
294    let mut out = Vec::new();
295    brotli::Decompressor::new(input, 4096)
296        .read_to_end(&mut out)
297        .map_err(io_err)?;
298    Ok(out)
299}
300
301fn zstd_compress(input: &[u8]) -> Result<Vec<u8>, String> {
302    // level 3 == ZSTD_CLEVEL_DEFAULT (node's default).
303    zstd::encode_all(input, 3).map_err(io_err)
304}
305
306fn zstd_decompress(input: &[u8]) -> Result<Vec<u8>, String> {
307    zstd::decode_all(input).map_err(io_err)
308}
309
310/// CRC-32 (IEEE) of `data`, seeded with `init` (node's `zlib.crc32(data, value)`).
311fn crc32(data: &[u8], init: u32) -> u32 {
312    let mut h = crc32fast::Hasher::new_with_initial(init);
313    h.update(data);
314    h.finalize()
315}