Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
63 changes: 62 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,67 @@
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.each([
["Z_FINISH", zlib.constants.Z_FINISH],
["Z_BLOCK", zlib.constants.Z_BLOCK],
])("BrotliDecompress.flush(%s) completes instead of aborting", async (_, kind) => {

Check warning on line 335 in test/js/node/zlib/zlib.test.js

View check run for this annotation

Claude / Claude Code Review

Tests cover BrotliDecompress only, not BrotliCompress

nit: `createBrotliCompress().flush(Z_FINISH)` hit the same `unreachable!()` before this fix (set_flush is shared between encode and decode), and the new code comment explicitly justifies encoder semantics — but the tests only exercise `createBrotliDecompress()`. Per CLAUDE.md §Tests ("Cover the variant matrix, not just the repro. Every sibling entry point receiving the same fix"), consider adding a `BrotliCompress` row to this `it.each`.
Comment thread
robobun marked this conversation as resolved.
Outdated
Comment thread
claude[bot] marked this conversation as resolved.
Outdated
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]);
expect({ stdout: stdout.trim(), exitCode }).toEqual({
stdout: JSON.stringify(["flushed", "err:Z_BUF_ERROR"]),
exitCode: 0,
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
void stderr;
});

it("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]);
expect({ stdout: stdout.trim().split("\n"), exitCode }).toEqual({
stdout: ["flushed", "data:hello world"],
exitCode: 0,
});
void stderr;
});
});

it.each([
Expand Down
Loading