Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions src/runtime/node/zlib/NativeBrotli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -415,16 +415,20 @@ mod _impl {
}

pub fn set_flush(&mut self, flush: c_int) {
// Caller passes a valid BrotliEncoderOperation discriminant (Node
// zlib constants 0..=3). Exhaustive match — `Op` is `#[repr(u32)]`
// so the prior `c_int` bit-cast was a width hazard anyway. Out-of-
// range traps.
// The shared `write`/`writeSync` validation accepts the zlib
// flush range (0..=6), so `Z_FINISH` (4) / `Z_BLOCK` (5) /
// `Z_TREES` (6) can reach here. Node stores the raw int; the
// decoder never consumes it and `get_error_info` only compares
// against `Op::finish`, while the brotli encoder treats any
// unknown op as a non-flushing, non-terminal step. Map anything
// outside the brotli operation set to `process` so both paths
// match Node instead of panicking.
self.flush = match flush {
0 => Op::process,
1 => Op::flush,
2 => Op::finish,
3 => Op::emit_metadata,
n => unreachable!("invalid BrotliEncoderOperation {n}"),
_ => Op::process,
};
}

Expand Down
89 changes: 88 additions & 1 deletion test/js/node/zlib/zlib.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { deflateSync, gunzipSync, gzipSync, inflateSync } from "bun";
import { describe, expect, it } from "bun:test";
import { tmpdirSync } from "harness";
import { bunEnv, bunExe, tmpdirSync } from "harness";
import * as buffer from "node:buffer";
import * as fs from "node:fs";
import { resolve } from "node:path";
Expand Down Expand Up @@ -323,6 +323,93 @@ describe("zlib.brotli", () => {
expect(compressed.toString()).toEqual(Buffer.from(compressedString3, "base64").toString());
}
});

// Node validates the native write()/writeSync() flush argument against the
// zlib range (0..=6), so passing a zlib flush constant like Z_FINISH (4) or
// Z_BLOCK (5) to a brotli stream reaches the native set_flush. The Rust
// port mapped only 0..=3 and panicked on the rest; these should instead
// complete the flush and let end() surface the usual Z_BUF_ERROR.
it.concurrent.each([
["Z_FINISH", zlib.constants.Z_FINISH],
["Z_BLOCK", zlib.constants.Z_BLOCK],
])("BrotliDecompress.flush(%s) completes instead of aborting", async (_, kind) => {
const script = `
const z = require("zlib");
const d = z.createBrotliDecompress();
const events = [];
d.on("error", e => {
events.push("err:" + e.code);
console.log(JSON.stringify(events));
});
d.flush(${kind}, () => events.push("flushed"));
d.end();
`;
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", script],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
void stderr;
expect(stdout.trim()).toBe(JSON.stringify(["flushed", "err:Z_BUF_ERROR"]));
expect(exitCode).toBe(0);
});

it.concurrent("BrotliDecompress.flush(Z_FINISH) still decodes valid input", async () => {
const script = `
const z = require("zlib");
const d = z.createBrotliDecompress();
const buf = z.brotliCompressSync(Buffer.from("hello world"));
const out = [];
d.on("data", b => out.push(b));
d.on("error", e => { console.log("err:" + e.code); process.exit(1); });
d.on("end", () => console.log("data:" + Buffer.concat(out).toString()));
d.write(buf);
d.flush(z.constants.Z_FINISH, () => console.log("flushed"));
d.end();
`;
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", script],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
void stderr;
expect(stdout.trim().split("\n")).toEqual(["flushed", "data:hello world"]);
expect(exitCode).toBe(0);
});

it.concurrent("BrotliCompress.flush(Z_FINISH) still produces decodable output", async () => {
// set_flush is shared between encode and decode; on the encode path the
// stored flush op is passed directly to BrotliEncoderCompressStream, so
// the out-of-range mapping must not break the finish sequence.
const script = `
const z = require("zlib");
const c = z.createBrotliCompress();
const chunks = [];
c.on("data", b => chunks.push(b));
c.on("error", e => { console.log("err:" + e.code); process.exit(1); });
c.on("end", () => {
const out = z.brotliDecompressSync(Buffer.concat(chunks));
console.log("roundtrip:" + out.toString());
});
c.write(Buffer.from("hello world"));
c.flush(z.constants.Z_FINISH, () => console.log("flushed"));
c.end();
`;
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", script],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
void stderr;
expect(stdout.trim().split("\n")).toEqual(["flushed", "roundtrip:hello world"]);
expect(exitCode).toBe(0);
});
});

it.each([
Expand Down
Loading