Skip to content
59 changes: 27 additions & 32 deletions src/jsc/bindings/JSBuffer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1313,11 +1313,9 @@
auto& vm = JSC::getVM(lexicalGlobalObject);
auto scope = DECLARE_THROW_SCOPE(vm);

if (callFrame->argumentCount() < 1) {
return JSValue::encode(castedThis);
}

auto value = callFrame->uncheckedArgument(0);
// No early return for 0 arguments: Node's fill() forwards an undefined
// value into the numeric path, which zero-fills the whole buffer.
auto value = callFrame->argument(0);
// Capture byteLength up front for two orthogonal purposes:
// 1. The upper-bound argument to validateInteger(end) so `end >
// buf.length` throws ERR_OUT_OF_RANGE with Node's wording and
Expand All @@ -1335,30 +1333,25 @@
size_t offset = 0;
size_t end = limit;
WebCore::BufferEncodingType encoding = WebCore::BufferEncodingType::utf8;
JSValue encodingValue = jsUndefined();
JSValue offsetValue = jsUndefined();
JSValue endValue = jsUndefined();

switch (callFrame->argumentCount()) {
case 4:
encodingValue = callFrame->uncheckedArgument(3);
[[fallthrough]];
case 3:
endValue = callFrame->uncheckedArgument(2);
[[fallthrough]];
case 2:
offsetValue = callFrame->uncheckedArgument(1);
[[fallthrough]];
default:
break;
}
// argument() (not uncheckedArgument) so trailing positional arguments past
// the fourth are ignored like Node's plain-JS fill(value, offset, end,
// encoding) signature, instead of degrading every slot to undefined.
JSValue offsetValue = callFrame->argument(1);
JSValue endValue = callFrame->argument(2);
JSValue encodingValue = callFrame->argument(3);

Check notice on line 1341 in src/jsc/bindings/JSBuffer.cpp

View check run for this annotation

Claude / Claude Code Review

fill(str, offset, end, null) throws where Node accepts null encoding as utf8 (pre-existing)

🟣 One more pre-existing throw-where-Node-succeeds shape in this same intake, not introduced by the PR: `Buffer.alloc(5, 0xaa).fill("a", 1, 3, null)` returns `<aa 61 61 aa aa>` in Node (its `normalizeEncoding` checks `enc == null` and falls back to utf8), but Bun throws `ERR_INVALID_ARG_TYPE` because the gate at line 1365 checks `!encodingValue.isUndefined()` — `null` passes the gate and reaches `parseEncoding` → `validateString`. Since this PR already folded in the other adjacent pre-existing di
Comment thread
robobun marked this conversation as resolved.
Resolved

if (offsetValue.isUndefined() || offsetValue.isString()) {
encodingValue = offsetValue;
offsetValue = jsUndefined();
} else if (endValue.isString()) {
encodingValue = endValue;
endValue = jsUndefined();
// Node's _fill only reinterprets a string offset/end as the encoding when the
// fill value is itself a string; otherwise validateInteger below rejects them.
// The offset-slot branch also discards end: fill("a", "hex", 3) fills everything.
if (value.isString()) {
if (offsetValue.isUndefined() || offsetValue.isString()) {
encodingValue = offsetValue;
offsetValue = jsUndefined();
endValue = jsUndefined();
} else if (endValue.isString()) {
encodingValue = endValue;
endValue = jsUndefined();
}
Comment thread
robobun marked this conversation as resolved.
Resolved
}

// ── 1. Encoding parse (FIRST validation) ────────────────────────────
Expand Down Expand Up @@ -1386,10 +1379,12 @@
if (!offsetValue.isUndefined()) {
Bun::V::validateInteger(scope, lexicalGlobalObject, offsetValue, "offset"_s, jsNumber(0), jsNumber(Bun::Buffer::kMaxLength), &offset);
RETURN_IF_EXCEPTION(scope, {});
}
if (!endValue.isUndefined()) {
Bun::V::validateInteger(scope, lexicalGlobalObject, endValue, "end"_s, jsNumber(0), jsNumber(limit), &end);
RETURN_IF_EXCEPTION(scope, {});
// Node only reads `end` once `offset` is present: fill(v, undefined, end)
// ignores end (it is never validated) and fills the whole buffer.
if (!endValue.isUndefined()) {
Bun::V::validateInteger(scope, lexicalGlobalObject, endValue, "end"_s, jsNumber(0), jsNumber(limit), &end);
RETURN_IF_EXCEPTION(scope, {});
}
}

// Node short-circuits empty/inverted ranges before coercing `value`,
Expand Down
82 changes: 82 additions & 0 deletions test/js/node/buffer-fill-args-fixture.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

123 changes: 122 additions & 1 deletion test/js/node/buffer.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Buffer, SlowBuffer, isAscii, isUtf8, kMaxLength } from "buffer";
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
import { bunEnv, bunExe, gc, isASAN, isDebug, withoutAggressiveGC } from "harness";
import { bunEnv, bunExe, gc, isASAN, isDebug, nodeExe, withoutAggressiveGC } from "harness";
import { createHash } from "node:crypto";
import { join } from "node:path";
import vm from "node:vm";

const BufferModule = await import("buffer");
Expand Down Expand Up @@ -3768,6 +3769,126 @@ describe("ERR_BUFFER_OUT_OF_BOUNDS", () => {
}
});

// Node's _fill (lib/buffer.js) only reinterprets a string `offset`/`end` as
// the encoding when the fill value is itself a string; otherwise they reach
// validateOffset and a non-number throws ERR_INVALID_ARG_TYPE. And `end` is
// only read at all once `offset` is present.
describe("Buffer.fill offset/end argument handling", () => {
it("rejects a string offset when the fill value is not a string", () => {
for (const value of [0, true, new Uint8Array([1])]) {
expect(() => Buffer.alloc(5).fill(value, "1", 3)).toThrow(
expect.objectContaining({
name: "TypeError",
code: "ERR_INVALID_ARG_TYPE",
message: `The "offset" argument must be of type number. Received type string ('1')`,
}),
);
// Two-argument form: a string in the offset slot is not an encoding either.
expect(() => Buffer.alloc(5).fill(value, "hex")).toThrow(
expect.objectContaining({
code: "ERR_INVALID_ARG_TYPE",
message: `The "offset" argument must be of type number. Received type string ('hex')`,
}),
);
}
expect(() => Buffer.alloc(5).fill(0, null, 3)).toThrow(
expect.objectContaining({
code: "ERR_INVALID_ARG_TYPE",
message: 'The "offset" argument must be of type number. Received null',
}),
);
});

it("rejects a string end when the fill value is not a string", () => {
for (const value of [0, true, new Uint8Array([1])]) {
expect(() => Buffer.alloc(5).fill(value, 1, "3")).toThrow(
expect.objectContaining({
name: "TypeError",
code: "ERR_INVALID_ARG_TYPE",
message: `The "end" argument must be of type number. Received type string ('3')`,
}),
);
}
expect(() => Buffer.alloc(5).fill(0, 1, null)).toThrow(
expect.objectContaining({
code: "ERR_INVALID_ARG_TYPE",
message: 'The "end" argument must be of type number. Received null',
}),
);
});

it("ignores end entirely when offset is undefined", () => {
// Node resets end = buf.length when offset === undefined; end is never
// validated, so even an otherwise invalid value there is accepted.
expect(Array.from(Buffer.alloc(5, 0xaa).fill(0, undefined, 3))).toEqual([0, 0, 0, 0, 0]);
expect(Array.from(Buffer.alloc(5, 0xaa).fill(0, undefined, "3"))).toEqual([0, 0, 0, 0, 0]);
expect(Array.from(Buffer.alloc(5, 0xaa).fill(0, undefined, -1))).toEqual([0, 0, 0, 0, 0]);
expect(Array.from(Buffer.alloc(5, 0xaa).fill(0, undefined, null))).toEqual([0, 0, 0, 0, 0]);
});

it("discards end when a string value's offset slot holds the encoding", () => {
// fill(value, encoding) has no end slot, so anything there is ignored.
expect(Buffer.alloc(5, 0xaa).fill("b", "utf8", 3).toString()).toBe("bbbbb");
expect(Buffer.alloc(5, 0xaa).fill("b", undefined, 3).toString()).toBe("bbbbb");
// An explicit numeric offset keeps end meaningful.
expect(Array.from(Buffer.alloc(5, 0xaa).fill("b", 1, 3))).toEqual([0xaa, 0x62, 0x62, 0xaa, 0xaa]);
});

it("still treats a string offset/end as the encoding when the value is a string", () => {
expect(Buffer.alloc(4, 0xaa).fill("ab", "utf16le").toString("hex")).toBe("61006200");
expect(Buffer.alloc(4, 0xaa).fill("ab", 0, "utf16le").toString("hex")).toBe("61006200");
expect(() => Buffer.alloc(4).fill("a", "bogus")).toThrow(expect.objectContaining({ code: "ERR_UNKNOWN_ENCODING" }));
expect(() => Buffer.alloc(4).fill("a", 0, "bogus")).toThrow(
expect.objectContaining({ code: "ERR_UNKNOWN_ENCODING" }),
);
});

it("never treats the 4-argument encoding as meaningful for a non-string value", () => {
expect(Array.from(Buffer.alloc(5, 0xaa).fill(0, 1, 3, "bogus"))).toEqual([0xaa, 0, 0, 0xaa, 0xaa]);
});

it("zero-fills the whole buffer when called with no arguments", () => {
// Node forwards the undefined value into the numeric path, which coerces to 0.
expect(Array.from(Buffer.alloc(3, 0xaa).fill())).toEqual([0, 0, 0]);
expect(Array.from(Buffer.alloc(3, 0xaa).fill(undefined))).toEqual([0, 0, 0]);
});

it("ignores positional arguments past the fourth", () => {
expect(Array.from(Buffer.alloc(5, 0xaa).fill(0, 1, 3, "utf8", "x"))).toEqual([0xaa, 0, 0, 0xaa, 0xaa]);
expect(Buffer.alloc(5, 0xaa).fill("ab", 0, 4, "utf16le", "x").toString("hex")).toBe("61006200aa");
});

it("lets an undefined offset shadow an explicit 4th-argument encoding, like Node", () => {
// Node's _fill assigns `encoding = offset` whenever offset is undefined or
// a string, so fill(str, undefined, ..., encoding) falls back to utf8 and a
// bogus 4th-argument encoding is never even validated.
expect(Buffer.alloc(4, 0xaa).fill("ab", undefined, undefined, "utf16le").toString("hex")).toBe("61626162");
expect(Buffer.alloc(4, 0xaa).fill("a", undefined, undefined, "bogus").toString("hex")).toBe("61616161");
// With a numeric offset the explicit encoding is honored.
expect(Buffer.alloc(4, 0xaa).fill("ab", 0, undefined, "utf16le").toString("hex")).toBe("61006200");
expect(() => Buffer.alloc(4).fill("a", 1, undefined, "bogus")).toThrow(
expect.objectContaining({ code: "ERR_UNKNOWN_ENCODING" }),
);
});

// Differential test: the fixture enumerates every fill() argument shape and
// prints the resulting bytes or the thrown error class + code. Running it
// under Node.js and under Bun must produce byte-identical output.
it.skipIf(!nodeExe())("every fill() argument shape produces the same output in Node.js and Bun", async () => {
const fixture = join(import.meta.dir, "buffer-fill-args-fixture.js");
async function run(exe) {
await using proc = Bun.spawn({ cmd: [exe, fixture], env: bunEnv, stdout: "pipe" });
const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]);
return { stdout, exitCode };
}
const [bunRun, nodeRun] = await Promise.all([run(bunExe()), run(nodeExe())]);
expect(nodeRun.stdout).toContain("fill(");
expect(bunRun.stdout).toBe(nodeRun.stdout);
expect(nodeRun.exitCode).toBe(0);
expect(bunRun.exitCode).toBe(0);
});
});

describe("*Write methods with NaN/invalid offset and length", () => {
// Regression test: NaN offset/length values must be handled safely.
// NaN offset should be treated as 0, and length should be clamped to buffer size.
Expand Down
Loading