diff --git a/src/bun.js/node/types.zig b/src/bun.js/node/types.zig index 6ff1448c06d61..5f9034610548b 100644 --- a/src/bun.js/node/types.zig +++ b/src/bun.js/node/types.zig @@ -23,6 +23,7 @@ const Shimmer = @import("../bindings/shimmer.zig").Shimmer; const Syscall = bun.sys; const URL = @import("../../url.zig").URL; const Value = std.json.Value; +const validators = @import("./util/validators.zig"); pub const Path = @import("./path.zig"); @@ -1210,14 +1211,16 @@ pub fn timeLikeFromJS(globalObject: *JSC.JSGlobalObject, value: JSC.JSValue, _: pub fn modeFromJS(ctx: JSC.C.JSContextRef, value: JSC.JSValue, exception: JSC.C.ExceptionRef) ?Mode { const mode_int = if (value.isNumber()) brk: { - if (!value.isUInt32AsAnyInt()) { - exception.* = ctx.ERR_OUT_OF_RANGE("The value of \"mode\" is out of range. It must be an integer. Received {d}", .{value.asNumber()}).toJS().asObjectRef(); - return null; - } - break :brk @as(Mode, @truncate(value.to(Mode))); + const m = validators.validateUint32(ctx, value, "mode", .{}, false) catch return null; + break :brk @as(Mode, @as(u24, @truncate(m))); } else brk: { if (value.isUndefinedOrNull()) return null; + if (!value.isString()) { + _ = ctx.throwInvalidArgumentTypeValue("mode", "number", value); + return null; + } + // An easier method of constructing the mode is to use a sequence of // three octal digits (e.g. 765). The left-most digit (7 in the example), // specifies the permissions for the file owner. The middle digit (6 in @@ -1232,16 +1235,12 @@ pub fn modeFromJS(ctx: JSC.C.JSContextRef, value: JSC.JSValue, exception: JSC.C. } break :brk std.fmt.parseInt(Mode, slice, 8) catch { - JSC.throwInvalidArguments("Invalid mode string: must be an octal number", .{}, ctx, exception); + var formatter = bun.JSC.ConsoleObject.Formatter{ .globalThis = ctx }; + exception.* = ctx.ERR_INVALID_ARG_VALUE("The argument 'mode' must be a 32-bit unsigned integer or an octal string. Received {}", .{value.toFmt(&formatter)}).toJS().asObjectRef(); return null; }; }; - if (mode_int < 0) { - JSC.throwInvalidArguments("Invalid mode: must be greater than or equal to 0.", .{}, ctx, exception); - return null; - } - return mode_int & 0o777; } diff --git a/src/bun.js/node/util/validators.zig b/src/bun.js/node/util/validators.zig index b7f56555c0c65..554f62ddbffc8 100644 --- a/src/bun.js/node/util/validators.zig +++ b/src/bun.js/node/util/validators.zig @@ -97,15 +97,17 @@ pub fn validateUint32(globalThis: *JSGlobalObject, value: JSValue, comptime name try throwErrInvalidArgType(globalThis, name_fmt, name_args, "number", value); } if (!value.isAnyInt()) { - try throwRangeError(globalThis, "The value of \"" ++ name_fmt ++ "\" is out of range. It must be an integer. Received {s}", name_args ++ .{value}); + var formatter = JSC.ConsoleObject.Formatter{ .globalThis = globalThis }; + try throwRangeError(globalThis, "The value of \"" ++ name_fmt ++ "\" is out of range. It must be an integer. Received {}", name_args ++ .{value.toFmt(&formatter)}); } const num: i64 = value.asInt52(); const min: i64 = if (greater_than_zero) 1 else 0; const max: i64 = @intCast(std.math.maxInt(u32)); if (num < min or num > max) { - try throwRangeError(globalThis, "The value of \"" ++ name_fmt ++ "\" is out of range. It must be >= {d} and <= {d}. Received {s}", name_args ++ .{ min, max, value }); + var formatter = JSC.ConsoleObject.Formatter{ .globalThis = globalThis }; + try throwRangeError(globalThis, "The value of \"" ++ name_fmt ++ "\" is out of range. It must be >= {d} and <= {d}. Received {}", name_args ++ .{ min, max, value.toFmt(&formatter) }); } - return @truncate(num); + return @truncate(@as(u63, @intCast(num))); } pub fn validateString(globalThis: *JSGlobalObject, value: JSValue, comptime name_fmt: string, name_args: anytype) !void { diff --git a/test/js/node/test/parallel/fs-open.test.js b/test/js/node/test/parallel/fs-open.test.js new file mode 100644 index 0000000000000..c8c102d7a3605 --- /dev/null +++ b/test/js/node/test/parallel/fs-open.test.js @@ -0,0 +1,102 @@ +//#FILE: test-fs-open.js +//#SHA1: 0466ad8882a3256fdd8da5fc8da3167f6dde4fd6 +//----------------- +'use strict'; +const fs = require('fs'); +const path = require('path'); + +test('fs.openSync throws ENOENT for non-existent file', () => { + expect(() => { + fs.openSync('/8hvftyuncxrt/path/to/file/that/does/not/exist', 'r'); + }).toThrow(expect.objectContaining({ + code: 'ENOENT', + message: expect.any(String) + })); +}); + +test('fs.openSync succeeds for existing file', () => { + expect(() => fs.openSync(__filename)).not.toThrow(); +}); + +test('fs.open succeeds with various valid arguments', async () => { + await expect(fs.promises.open(__filename)).resolves.toBeDefined(); + await expect(fs.promises.open(__filename, 'r')).resolves.toBeDefined(); + await expect(fs.promises.open(__filename, 'rs')).resolves.toBeDefined(); + await expect(fs.promises.open(__filename, 'r', 0)).resolves.toBeDefined(); + await expect(fs.promises.open(__filename, 'r', null)).resolves.toBeDefined(); +}); + +test('fs.open throws for invalid mode argument', () => { + expect(() => fs.open(__filename, 'r', 'boom', () => {})).toThrow(({ + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', + message: `The argument 'mode' must be a 32-bit unsigned integer or an octal string. Received boom` + })); + expect(() => fs.open(__filename, 'r', 5.5, () => {})).toThrow(({ + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: `The value of "mode" is out of range. It must be an integer. Received 5.5` + })); + expect(() => fs.open(__filename, 'r', -7, () => {})).toThrow(({ + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: `The value of "mode" is out of range. It must be >= 0 and <= 4294967295. Received -7` + })); + expect(() => fs.open(__filename, 'r', 4304967295, () => {})).toThrow(({ + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: `The value of "mode" is out of range. It must be >= 0 and <= 4294967295. Received 4304967295` + })); +}); + +test('fs.open throws for invalid argument combinations', () => { + const invalidArgs = [[], ['r'], ['r', 0], ['r', 0, 'bad callback']]; + invalidArgs.forEach(args => { + expect(() => fs.open(__filename, ...args)).toThrow(expect.objectContaining({ + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: expect.any(String) + })); + }); +}); + +test('fs functions throw for invalid path types', () => { + const invalidPaths = [false, 1, [], {}, null, undefined]; + invalidPaths.forEach(path => { + expect(() => fs.open(path, 'r', () => {})).toThrow(expect.objectContaining({ + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: expect.any(String) + })); + expect(() => fs.openSync(path, 'r')).toThrow(expect.objectContaining({ + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: expect.any(String) + })); + expect(fs.promises.open(path, 'r')).rejects.toThrow(expect.objectContaining({ + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: expect.any(String) + })); + }); +}); + +test('fs functions throw for invalid modes', () => { + const invalidModes = [false, [], {}]; + invalidModes.forEach(mode => { + expect(() => fs.open(__filename, 'r', mode, () => {})).toThrow(expect.objectContaining({ + code: 'ERR_INVALID_ARG_TYPE', + message: expect.any(String) + })); + expect(() => fs.openSync(__filename, 'r', mode)).toThrow(expect.objectContaining({ + code: 'ERR_INVALID_ARG_TYPE', + message: expect.any(String) + })); + expect(fs.promises.open(__filename, 'r', mode)).rejects.toThrow(expect.objectContaining({ + code: 'ERR_INVALID_ARG_TYPE', + message: expect.any(String) + })); + }); +}); + +//<#END_FILE: test-fs-open.js