Compare commits

...

4 Commits

Author SHA1 Message Date
Claude Bot
5f71d3ce53 refactor: Use jsc.MAX_SAFE_INTEGER and simplify object check
- Replace hardcoded 9007199254740991 with jsc.MAX_SAFE_INTEGER constant
- Simplify options object detection to just isObject() and !isCallable()
  (no need to check !isNumber() and !isBigInt() since numbers and bigints
  are not objects in this context)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-10 04:09:13 +00:00
Claude Bot
fe908531d4 refactor: Factor out common offset validation
Move buffer length calculation and offset validation outside the
if/else branches to reduce code duplication.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-10 03:58:43 +00:00
Claude Bot
f72fdfe558 refactor: Use validateInteger for better type validation
Use jsc.Node.validators.validateInteger for both offset and length
parameters to provide better error messages and consistent validation
with the rest of the codebase.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-09 14:45:03 +00:00
Claude Bot
a2d0d9dac1 fix: Support options object in fs.writeSync
This commit adds support for passing options as an object to fs.writeSync,
matching Node.js behavior. Previously, Bun only supported positional
parameters (fd, buffer, offset, length, position). Now it also accepts:

fs.writeSync(fd, buffer, { offset, length, position })

The fix properly validates the options and throws ERR_OUT_OF_RANGE when
offset or length values exceed the buffer bounds.

Fixes the test-fs-write-sync-optional-params.js Node.js test.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-09 14:31:53 +00:00
2 changed files with 163 additions and 21 deletions

View File

@@ -2467,15 +2467,69 @@ pub const Arguments = struct {
}
},
// fs.write(fd, buffer[, offset[, length[, position]]], callback)
// or fs.write(fd, buffer, options, callback) where options is { offset, length, position }
.buffer => {
if (current.isUndefinedOrNull() or current.isFunction()) break :parse;
args.offset = @intCast(try jsc.Node.validators.validateInteger(ctx, current, "offset", 0, 9007199254740991));
arguments.eat();
current = arguments.next() orelse break :parse;
if (!(current.isNumber() or current.isBigInt())) break :parse;
const length = current.to(i64);
const buf_len = args.buffer.buffer.slice().len;
// Check if this is an options object
if (current.isObject() and !current.isCallable()) {
// Named parameters object: { offset?, length?, position? }
// Handle offset
if (try current.getTruthy(ctx, "offset")) |offset_val| {
args.offset = @intCast(try jsc.Node.validators.validateInteger(ctx, offset_val, "offset", 0, jsc.MAX_SAFE_INTEGER));
}
// Handle length
if (try current.getTruthy(ctx, "length")) |length_val| {
const length = try jsc.Node.validators.validateInteger(ctx, length_val, "length", null, null);
const max_len = @min(buf_len - args.offset, std.math.maxInt(i32));
if (length > max_len or length < 0) {
return ctx.throwRangeError(
@as(f64, @floatFromInt(length)),
.{ .field_name = "length", .min = @as(i64, 0), .max = @as(i64, @intCast(max_len)) },
);
}
args.length = @intCast(length);
}
// Handle position
if (try current.getTruthy(ctx, "position")) |position_val| {
if (position_val.isNumber() or position_val.isBigInt()) {
const position = position_val.to(i52);
if (position >= 0) args.position = position;
}
}
arguments.eat();
} else {
// Positional parameters: offset, length, position
args.offset = @intCast(try jsc.Node.validators.validateInteger(ctx, current, "offset", 0, jsc.MAX_SAFE_INTEGER));
arguments.eat();
current = arguments.next() orelse break :parse;
if (!(current.isNumber() or current.isBigInt())) break :parse;
const length = current.to(i64);
const max_len = @min(buf_len - args.offset, std.math.maxInt(i32));
if (length > max_len or length < 0) {
return ctx.throwRangeError(
@as(f64, @floatFromInt(length)),
.{ .field_name = "length", .min = 0, .max = @intCast(max_len) },
);
}
args.length = @intCast(length);
arguments.eat();
current = arguments.next() orelse break :parse;
if (!(current.isNumber() or current.isBigInt())) break :parse;
const position = current.to(i52);
if (position >= 0) args.position = position;
arguments.eat();
}
// Validate offset is within buffer bounds (applies to both paths)
const max_offset = @min(buf_len, std.math.maxInt(i64));
if (args.offset > max_offset) {
return ctx.throwRangeError(
@@ -2483,22 +2537,6 @@ pub const Arguments = struct {
.{ .field_name = "offset", .max = @intCast(max_offset) },
);
}
const max_len = @min(buf_len - args.offset, std.math.maxInt(i32));
if (length > max_len or length < 0) {
return ctx.throwRangeError(
@as(f64, @floatFromInt(length)),
.{ .field_name = "length", .min = 0, .max = @intCast(max_len) },
);
}
args.length = @intCast(length);
arguments.eat();
current = arguments.next() orelse break :parse;
if (!(current.isNumber() or current.isBigInt())) break :parse;
const position = current.to(i52);
if (position >= 0) args.position = position;
arguments.eat();
},
}
}

View File

@@ -0,0 +1,104 @@
'use strict';
const common = require('../common');
// This test ensures that fs.writeSync accepts "named parameters" object
// and doesn't interpret objects as strings
const assert = require('assert');
const fs = require('fs');
const tmpdir = require('../common/tmpdir');
tmpdir.refresh();
const dest = tmpdir.resolve('tmp.txt');
const buffer = Buffer.from('zyx');
function testInvalid(dest, expectedCode, ...bufferAndOptions) {
if (bufferAndOptions.length >= 2) {
bufferAndOptions[1] = common.mustNotMutateObjectDeep(bufferAndOptions[1]);
}
let fd;
try {
fd = fs.openSync(dest, 'w+');
assert.throws(
() => fs.writeSync(fd, ...bufferAndOptions),
{ code: expectedCode });
} finally {
if (fd != null) fs.closeSync(fd);
}
}
function testValid(dest, buffer, options) {
const length = options?.length;
let fd, bytesWritten, bytesRead;
try {
fd = fs.openSync(dest, 'w');
bytesWritten = fs.writeSync(fd, buffer, options);
} finally {
if (fd != null) fs.closeSync(fd);
}
try {
fd = fs.openSync(dest, 'r');
bytesRead = fs.readSync(fd, buffer, options);
} finally {
if (fd != null) fs.closeSync(fd);
}
assert.ok(bytesWritten >= bytesRead);
if (length !== undefined && length !== null) {
assert.strictEqual(bytesWritten, length);
assert.strictEqual(bytesRead, length);
}
}
{
// Test if second argument is not wrongly interpreted as string or options
for (const badBuffer of [
undefined, null, true, 42, 42n, Symbol('42'), NaN, [], () => {},
common.mustNotCall(),
common.mustNotMutateObjectDeep({}),
{},
{ buffer: 'amNotParam' },
{ string: 'amNotParam' },
{ buffer: new Uint8Array(1) },
{ buffer: new Uint8Array(1).buffer },
Promise.resolve(new Uint8Array(1)),
new Date(),
new String('notPrimitive'),
{ toString() { return 'amObject'; } },
{ [Symbol.toPrimitive]: (hint) => 'amObject' },
]) {
testInvalid(dest, 'ERR_INVALID_ARG_TYPE', common.mustNotMutateObjectDeep(badBuffer));
}
// First argument (buffer or string) is mandatory
testInvalid(dest, 'ERR_INVALID_ARG_TYPE');
// Various invalid options
testInvalid(dest, 'ERR_OUT_OF_RANGE', buffer, { length: 5 });
testInvalid(dest, 'ERR_OUT_OF_RANGE', buffer, { offset: 5 });
testInvalid(dest, 'ERR_OUT_OF_RANGE', buffer, { length: 1, offset: 3 });
testInvalid(dest, 'ERR_OUT_OF_RANGE', buffer, { length: -1 });
testInvalid(dest, 'ERR_OUT_OF_RANGE', buffer, { offset: -1 });
testInvalid(dest, 'ERR_INVALID_ARG_TYPE', buffer, { offset: false });
testInvalid(dest, 'ERR_INVALID_ARG_TYPE', buffer, { offset: true });
// Test compatibility with fs.readSync counterpart with reused options
for (const options of [
undefined,
null,
{},
{ length: 1 },
{ position: 5 },
{ length: 1, position: 5 },
{ length: 1, position: -1, offset: 2 },
{ length: null },
{ position: null },
{ offset: 1 },
]) {
testValid(dest, buffer, common.mustNotMutateObjectDeep(options));
}
}