From 834ad11d48d64b37e9e79a45995d2a741ca3dbf5 Mon Sep 17 00:00:00 2001 From: chloe caruso Date: Tue, 14 Jan 2025 20:53:02 -0800 Subject: [PATCH] get node:fs tests passing part 1 (#16270) --- cmake/tools/SetupLLVM.cmake | 4 +- jsconfig.json | 22 - src/bake/production.zig | 3 - src/bun.js/ConsoleObject.zig | 150 ++-- src/bun.js/api/server.zig | 14 +- src/bun.js/bindings/JSPropertyIterator.cpp | 30 - src/bun.js/bindings/JSPropertyIterator.zig | 25 - src/bun.js/bindings/ProcessBindingUV.cpp | 443 ++++++++--- src/bun.js/bindings/bindings.cpp | 28 + src/bun.js/bindings/bindings.zig | 13 +- src/bun.js/event_loop.zig | 6 + src/bun.js/javascript.zig | 290 +++++--- src/bun.js/module_loader.zig | 14 - src/bun.js/node/node.classes.ts | 4 +- src/bun.js/node/node_assert.zig | 2 - src/bun.js/node/node_fs.zig | 693 +++++++++--------- src/bun.js/node/node_fs_binding.zig | 219 +++--- src/bun.js/node/node_fs_stat_watcher.zig | 4 + src/bun.js/node/node_fs_watcher.zig | 7 +- src/bun.js/node/types.zig | 337 +++++---- src/bun.js/node/util/validators.zig | 21 +- src/bun.js/webcore/blob.zig | 3 +- src/bun.zig | 123 +++- src/bundler/bundle_v2.zig | 19 +- src/cli.zig | 9 +- src/cli/exec_command.zig | 4 +- src/cli/test_command.zig | 1 - src/copy_file.zig | 2 +- src/crash_handler.zig | 7 + src/darwin_c.zig | 116 --- src/fd.zig | 9 +- src/fmt.zig | 11 +- src/install/install.zig | 57 +- src/install/lockfile.zig | 6 +- src/install/patch_install.zig | 6 +- src/js/internal/test/binding.ts | 77 -- src/js/node/fs.promises.ts | 158 ++-- src/js/node/fs.ts | 105 +-- src/js/node/readline.ts | 70 +- src/js/node/stream.consumers.ts | 43 +- src/js/node/util.ts | 50 +- src/linux_c.zig | 140 ---- src/logger.zig | 15 + src/node_fallbacks.zig | 2 +- src/output.zig | 12 +- src/patch.zig | 28 +- src/resolver/resolve_path.zig | 9 +- src/shell/interpreter.zig | 61 +- src/shell/shell.zig | 2 +- src/shell/subproc.zig | 2 +- src/string.zig | 2 +- src/string_immutable.zig | 43 +- src/sys.zig | 634 ++++++++++++++-- src/sys_uv.zig | 3 +- src/transpiler.zig | 1 + src/windows.zig | 2 +- src/windows_c.zig | 143 ---- test/cli/install/migration/migrate.test.ts | 1 + test/js/bun/http/fetch-file-upload.test.ts | 2 +- test/js/bun/io/bun-write.test.js | 2 +- test/js/bun/shell/bunshell.test.ts | 2 +- test/js/bun/spawn/spawn.test.ts | 4 +- test/js/bun/test/stack.test.ts | 4 +- test/js/bun/util/inspect.test.js | 27 +- test/js/bun/util/reportError.test.ts | 4 +- test/js/node/crypto/node-crypto.test.js | 2 +- test/js/node/fs/cp.test.ts | 2 +- test/js/node/fs/fs-oom.test.ts | 8 +- test/js/node/fs/fs.test.ts | 52 +- test/js/node/process-binding.test.ts | 2 +- test/js/node/test/common/index.js | 53 +- .../test/parallel/test-binding-constants.js | 3 +- test/js/node/test/parallel/test-fs-access.js | 237 ++++++ .../test/parallel/test-fs-append-file-sync.js | 103 +++ .../node/test/parallel/test-fs-append-file.js | 187 +++++ .../parallel/test-fs-assert-encoding-error.js | 80 ++ .../node/test/parallel/test-fs-chmod-mask.js | 89 +++ test/js/node/test/parallel/test-fs-chmod.js | 152 ++++ .../test/parallel/test-fs-close-errors.js | 35 + test/js/node/test/parallel/test-fs-close.js | 12 + .../js/node/test/parallel/test-fs-copyfile.js | 164 +++++ .../test/parallel/test-fs-existssync-false.js | 35 - test/js/node/test/parallel/test-fs-fchmod.js | 84 +++ test/js/node/test/parallel/test-fs-fchown.js | 61 ++ .../test-fs-filehandle-use-after-close.js | 25 + test/js/node/test/parallel/test-fs-fmap.js | 29 - test/js/node/test/parallel/test-fs-lchmod.js | 66 ++ test/js/node/test/parallel/test-fs-lchown.js | 64 ++ .../test/parallel/test-fs-mkdir-mode-mask.js | 40 + .../node/test/parallel/test-fs-mkdir-rmdir.js | 37 + .../node/test/parallel/test-fs-null-bytes.js | 158 ++++ .../test-fs-promises-file-handle-stream.js | 48 ++ .../test-fs-promises-file-handle-sync.js | 35 + .../test/parallel/test-fs-promises-watch.js | 136 ++++ .../parallel/test-fs-read-empty-buffer.js | 41 ++ .../test/parallel/test-fs-readdir-pipe.js | 21 + .../test/parallel/test-fs-readfile-eof.js | 24 +- .../parallel/test-fs-readfile-pipe-large.js | 6 +- .../test/parallel/test-fs-readfile-pipe.js | 5 +- .../parallel/test-fs-readfilesync-enoent.js | 33 - .../test-fs-readfilesync-pipe-large.js | 6 +- .../test/parallel/test-fs-readv-promisify.js | 18 + .../test/parallel/test-fs-realpath-native.js | 21 + .../test-fs-realpath-on-substed-drive.js | 52 -- .../js/node/test/parallel/test-fs-realpath.js | 618 ++++++++++++++++ .../parallel/test-fs-symlink-dir-junction.js | 64 -- .../node/test/parallel/test-fs-symlink-dir.js | 84 --- .../test/parallel/test-fs-symlink-longpath.js | 28 - .../test/parallel/test-fs-utimes-y2K38.js | 1 - ...st-fs-watch-file-enoent-after-deletion.js} | 38 +- ...ecursive-add-file-to-existing-subfolder.js | 3 +- ...st-fs-watch-recursive-add-file-with-url.js | 3 +- .../test-fs-watch-recursive-add-file.js | 3 +- .../test-fs-watch-recursive-add-folder.js | 3 +- ...s-watch-recursive-linux-parallel-remove.js | 35 - .../test-fs-watch-recursive-sync-write.js | 30 +- .../test-fs-watch-recursive-update-file.js | 2 +- test/js/node/test/parallel/test-fs-watch.js | 25 +- .../parallel/test-fs-write-file-buffer.js | 3 - .../test-fs-write-file-invalid-path.js | 1 - .../test/parallel/test-http-chunk-problem.js | 15 +- .../test-process-chdir-errormessage.js | 2 +- ...-base-prototype-accessors-enumerability.js | 4 +- test/js/node/watch/fs.watch.test.ts | 8 +- test/js/web/fetch/fetch.test.ts | 4 +- test/js/web/fetch/fetch.unix.test.ts | 2 +- 126 files changed, 5137 insertions(+), 2415 deletions(-) delete mode 100644 jsconfig.json delete mode 100644 src/js/internal/test/binding.ts create mode 100644 test/js/node/test/parallel/test-fs-access.js create mode 100644 test/js/node/test/parallel/test-fs-append-file-sync.js create mode 100644 test/js/node/test/parallel/test-fs-append-file.js create mode 100644 test/js/node/test/parallel/test-fs-assert-encoding-error.js create mode 100644 test/js/node/test/parallel/test-fs-chmod-mask.js create mode 100644 test/js/node/test/parallel/test-fs-chmod.js create mode 100644 test/js/node/test/parallel/test-fs-close-errors.js create mode 100644 test/js/node/test/parallel/test-fs-close.js create mode 100644 test/js/node/test/parallel/test-fs-copyfile.js delete mode 100644 test/js/node/test/parallel/test-fs-existssync-false.js create mode 100644 test/js/node/test/parallel/test-fs-fchmod.js create mode 100644 test/js/node/test/parallel/test-fs-fchown.js create mode 100644 test/js/node/test/parallel/test-fs-filehandle-use-after-close.js delete mode 100644 test/js/node/test/parallel/test-fs-fmap.js create mode 100644 test/js/node/test/parallel/test-fs-lchmod.js create mode 100644 test/js/node/test/parallel/test-fs-lchown.js create mode 100644 test/js/node/test/parallel/test-fs-mkdir-mode-mask.js create mode 100644 test/js/node/test/parallel/test-fs-mkdir-rmdir.js create mode 100644 test/js/node/test/parallel/test-fs-null-bytes.js create mode 100644 test/js/node/test/parallel/test-fs-promises-file-handle-stream.js create mode 100644 test/js/node/test/parallel/test-fs-promises-file-handle-sync.js create mode 100644 test/js/node/test/parallel/test-fs-promises-watch.js create mode 100644 test/js/node/test/parallel/test-fs-read-empty-buffer.js create mode 100644 test/js/node/test/parallel/test-fs-readdir-pipe.js delete mode 100644 test/js/node/test/parallel/test-fs-readfilesync-enoent.js create mode 100644 test/js/node/test/parallel/test-fs-readv-promisify.js create mode 100644 test/js/node/test/parallel/test-fs-realpath-native.js delete mode 100644 test/js/node/test/parallel/test-fs-realpath-on-substed-drive.js create mode 100644 test/js/node/test/parallel/test-fs-realpath.js delete mode 100644 test/js/node/test/parallel/test-fs-symlink-dir-junction.js delete mode 100644 test/js/node/test/parallel/test-fs-symlink-dir.js delete mode 100644 test/js/node/test/parallel/test-fs-symlink-longpath.js rename test/js/node/test/parallel/{test-fs-long-path.js => test-fs-watch-file-enoent-after-deletion.js} (61%) delete mode 100644 test/js/node/test/parallel/test-fs-watch-recursive-linux-parallel-remove.js diff --git a/cmake/tools/SetupLLVM.cmake b/cmake/tools/SetupLLVM.cmake index 2bcc97ceed..92423dc607 100644 --- a/cmake/tools/SetupLLVM.cmake +++ b/cmake/tools/SetupLLVM.cmake @@ -41,11 +41,11 @@ if(APPLE) endif() endif() - list(APPEND LLVM_PATHS ${HOMEBREW_PREFIX}/opt/llvm/bin) - if(USE_LLVM_VERSION) list(APPEND LLVM_PATHS ${HOMEBREW_PREFIX}/opt/llvm@${LLVM_VERSION_MAJOR}/bin) endif() + + list(APPEND LLVM_PATHS ${HOMEBREW_PREFIX}/opt/llvm/bin) endif() if(UNIX) diff --git a/jsconfig.json b/jsconfig.json deleted file mode 100644 index 0fecda676b..0000000000 --- a/jsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "extends": "./tsconfig.base.json", - "compilerOptions": { - "importsNotUsedAsValues": "preserve" - }, - "include": [".", "packages/bun-types/index.d.ts"], - "exclude": [ - "src/test", - "src/js/out", - // "src/js/builtins", - "packages", - "bench", - "examples/*/*", - "test", - "vendor", - "bun-webkit", - "vendor/WebKit", - "src/api/demo", - "node_modules" - ], - "files": ["src/js/builtins.d.ts"] -} diff --git a/src/bake/production.zig b/src/bake/production.zig index f9414927d1..2e95030f94 100644 --- a/src/bake/production.zig +++ b/src/bake/production.zig @@ -844,9 +844,6 @@ export fn BakeProdLoad(pt: *PerThread, key: bun.String) bun.String { if (pt.module_map.get(utf8.slice())) |value| { return pt.bundled_outputs[value.get()].value.toBunString(); } - for (pt.module_map.keys()) |keys| { - std.debug.print("key that does exist: {s}\n", .{keys}); - } return bun.String.dead; } diff --git a/src/bun.js/ConsoleObject.zig b/src/bun.js/ConsoleObject.zig index cadd785ed9..f79efc038a 100644 --- a/src/bun.js/ConsoleObject.zig +++ b/src/bun.js/ConsoleObject.zig @@ -3262,86 +3262,88 @@ pub const Formatter = struct { else bun.asByteSlice(@tagName(arrayBuffer.typed_array_type)), ); + if (slice.len == 0) { + writer.print("({d}) []", .{arrayBuffer.len}); + return; + } writer.print("({d}) [ ", .{arrayBuffer.len}); - if (slice.len > 0) { - switch (jsType) { - .Int8Array => this.writeTypedArray( + switch (jsType) { + .Int8Array => this.writeTypedArray( + *@TypeOf(writer), + &writer, + i8, + @alignCast(std.mem.bytesAsSlice(i8, slice)), + enable_ansi_colors, + ), + .Int16Array => this.writeTypedArray( + *@TypeOf(writer), + &writer, + i16, + @alignCast(std.mem.bytesAsSlice(i16, slice)), + enable_ansi_colors, + ), + .Uint16Array => this.writeTypedArray( + *@TypeOf(writer), + &writer, + u16, + @alignCast(std.mem.bytesAsSlice(u16, slice)), + enable_ansi_colors, + ), + .Int32Array => this.writeTypedArray( + *@TypeOf(writer), + &writer, + i32, + @alignCast(std.mem.bytesAsSlice(i32, slice)), + enable_ansi_colors, + ), + .Uint32Array => this.writeTypedArray( + *@TypeOf(writer), + &writer, + u32, + @alignCast(std.mem.bytesAsSlice(u32, slice)), + enable_ansi_colors, + ), + .Float16Array => this.writeTypedArray( + *@TypeOf(writer), + &writer, + f16, + @alignCast(std.mem.bytesAsSlice(f16, slice)), + enable_ansi_colors, + ), + .Float32Array => this.writeTypedArray( + *@TypeOf(writer), + &writer, + f32, + @alignCast(std.mem.bytesAsSlice(f32, slice)), + enable_ansi_colors, + ), + .Float64Array => this.writeTypedArray( + *@TypeOf(writer), + &writer, + f64, + @alignCast(std.mem.bytesAsSlice(f64, slice)), + enable_ansi_colors, + ), + .BigInt64Array => this.writeTypedArray( + *@TypeOf(writer), + &writer, + i64, + @alignCast(std.mem.bytesAsSlice(i64, slice)), + enable_ansi_colors, + ), + .BigUint64Array => { + this.writeTypedArray( *@TypeOf(writer), &writer, - i8, - @as([]align(std.meta.alignment([]i8)) i8, @alignCast(std.mem.bytesAsSlice(i8, slice))), + u64, + @as([]align(std.meta.alignment([]u64)) u64, @alignCast(std.mem.bytesAsSlice(u64, slice))), enable_ansi_colors, - ), - .Int16Array => this.writeTypedArray( - *@TypeOf(writer), - &writer, - i16, - @as([]align(std.meta.alignment([]i16)) i16, @alignCast(std.mem.bytesAsSlice(i16, slice))), - enable_ansi_colors, - ), - .Uint16Array => this.writeTypedArray( - *@TypeOf(writer), - &writer, - u16, - @as([]align(std.meta.alignment([]u16)) u16, @alignCast(std.mem.bytesAsSlice(u16, slice))), - enable_ansi_colors, - ), - .Int32Array => this.writeTypedArray( - *@TypeOf(writer), - &writer, - i32, - @as([]align(std.meta.alignment([]i32)) i32, @alignCast(std.mem.bytesAsSlice(i32, slice))), - enable_ansi_colors, - ), - .Uint32Array => this.writeTypedArray( - *@TypeOf(writer), - &writer, - u32, - @as([]align(std.meta.alignment([]u32)) u32, @alignCast(std.mem.bytesAsSlice(u32, slice))), - enable_ansi_colors, - ), - .Float16Array => this.writeTypedArray( - *@TypeOf(writer), - &writer, - f16, - @as([]align(std.meta.alignment([]f16)) f16, @alignCast(std.mem.bytesAsSlice(f16, slice))), - enable_ansi_colors, - ), - .Float32Array => this.writeTypedArray( - *@TypeOf(writer), - &writer, - f32, - @as([]align(std.meta.alignment([]f32)) f32, @alignCast(std.mem.bytesAsSlice(f32, slice))), - enable_ansi_colors, - ), - .Float64Array => this.writeTypedArray( - *@TypeOf(writer), - &writer, - f64, - @as([]align(std.meta.alignment([]f64)) f64, @alignCast(std.mem.bytesAsSlice(f64, slice))), - enable_ansi_colors, - ), - .BigInt64Array => this.writeTypedArray( - *@TypeOf(writer), - &writer, - i64, - @as([]align(std.meta.alignment([]i64)) i64, @alignCast(std.mem.bytesAsSlice(i64, slice))), - enable_ansi_colors, - ), - .BigUint64Array => { - this.writeTypedArray( - *@TypeOf(writer), - &writer, - u64, - @as([]align(std.meta.alignment([]u64)) u64, @alignCast(std.mem.bytesAsSlice(u64, slice))), - enable_ansi_colors, - ); - }, + ); + }, - // Uint8Array, Uint8ClampedArray, DataView, ArrayBuffer - else => this.writeTypedArray(*@TypeOf(writer), &writer, u8, slice, enable_ansi_colors), - } + // Uint8Array, Uint8ClampedArray, DataView, ArrayBuffer + else => this.writeTypedArray(*@TypeOf(writer), &writer, u8, slice, enable_ansi_colors), } writer.writeAll(" ]"); diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index 686bdaf49f..a841498d3e 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -2709,18 +2709,14 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp file.pathlike.fd else switch (bun.sys.open(file.pathlike.path.sliceZ(&file_buf), bun.O.RDONLY | bun.O.NONBLOCK | bun.O.CLOEXEC, 0)) { .result => |_fd| _fd, - .err => |err| return this.runErrorHandler(err.withPath(file.pathlike.path.slice()).toSystemError().toErrorInstance( - globalThis, - )), + .err => |err| return this.runErrorHandler(err.withPath(file.pathlike.path.slice()).toJSC(globalThis)), }; // stat only blocks if the target is a file descriptor const stat: bun.Stat = switch (bun.sys.fstat(fd)) { .result => |result| result, .err => |err| { - this.runErrorHandler(err.withPathLike(file.pathlike).toSystemError().toErrorInstance( - globalThis, - )); + this.runErrorHandler(err.withPathLike(file.pathlike).toJSC(globalThis)); if (auto_close) { _ = bun.sys.close(fd); } @@ -2757,11 +2753,9 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp .errno = @as(bun.sys.Error.Int, @intCast(@intFromEnum(std.posix.E.INVAL))), .syscall = .sendfile, }; - var sys = err.withPathLike(file.pathlike).toSystemError(); + var sys = err.withPathLike(file.pathlike).toShellSystemError(); sys.message = bun.String.static("File must be regular or FIFO"); - this.runErrorHandler(sys.toErrorInstance( - globalThis, - )); + this.runErrorHandler(sys.toErrorInstance(globalThis)); return; } } diff --git a/src/bun.js/bindings/JSPropertyIterator.cpp b/src/bun.js/bindings/JSPropertyIterator.cpp index c7e3ae8fed..4cdd5748b5 100644 --- a/src/bun.js/bindings/JSPropertyIterator.cpp +++ b/src/bun.js/bindings/JSPropertyIterator.cpp @@ -94,36 +94,6 @@ extern "C" JSPropertyIterator* Bun__JSPropertyIterator__create(JSC::JSGlobalObje return JSPropertyIterator::create(vm, array.releaseData()); } -// The only non-own property that we sometimes want to get is the code property. -extern "C" EncodedJSValue Bun__JSPropertyIterator__getCodeProperty(JSPropertyIterator* iter, JSC::JSGlobalObject* globalObject, JSC::JSObject* object) -{ - if (UNLIKELY(!iter)) { - return {}; - } - - auto& vm = iter->vm; - auto scope = DECLARE_THROW_SCOPE(vm); - RETURN_IF_EXCEPTION(scope, {}); - if (UNLIKELY(object->type() == JSC::ProxyObjectType)) { - return {}; - } - - auto& builtinNames = WebCore::builtinNames(vm); - - PropertySlot slot(object, PropertySlot::InternalMethodType::VMInquiry, vm.ptr()); - if (!object->getNonIndexPropertySlot(globalObject, builtinNames.codePublicName(), slot)) { - return {}; - } - - if (slot.isAccessor() || slot.isCustom()) { - return {}; - } - - RETURN_IF_EXCEPTION(scope, {}); - - return JSValue::encode(slot.getPureResult()); -} - extern "C" size_t Bun__JSPropertyIterator__getLongestPropertyName(JSPropertyIterator* iter, JSC::JSGlobalObject* globalObject, JSC::JSObject* object) { size_t longest = 0; diff --git a/src/bun.js/bindings/JSPropertyIterator.zig b/src/bun.js/bindings/JSPropertyIterator.zig index 353d89d83a..56f763551f 100644 --- a/src/bun.js/bindings/JSPropertyIterator.zig +++ b/src/bun.js/bindings/JSPropertyIterator.zig @@ -8,7 +8,6 @@ extern "C" fn Bun__JSPropertyIterator__getNameAndValueNonObservable(iter: ?*anyo extern "C" fn Bun__JSPropertyIterator__getName(iter: ?*anyopaque, propertyName: *bun.String, i: usize) void; extern "C" fn Bun__JSPropertyIterator__deinit(iter: ?*anyopaque) void; extern "C" fn Bun__JSPropertyIterator__getLongestPropertyName(iter: ?*anyopaque, globalObject: *JSC.JSGlobalObject, object: *anyopaque) usize; -extern "C" fn Bun__JSPropertyIterator__getCodeProperty(iter: ?*anyopaque, globalObject: *JSC.JSGlobalObject, object: *anyopaque) JSC.JSValue; pub const JSPropertyIteratorOptions = struct { skip_empty_name: bool, include_value: bool, @@ -27,7 +26,6 @@ pub fn JSPropertyIterator(comptime options: JSPropertyIteratorOptions) type { globalObject: *JSC.JSGlobalObject, object: *JSC.JSCell = undefined, value: JSC.JSValue = .zero, - tried_code_property: bool = false, pub fn getLongestPropertyName(this: *@This()) usize { if (this.impl == null) return 0; @@ -60,7 +58,6 @@ pub fn JSPropertyIterator(comptime options: JSPropertyIteratorOptions) type { pub fn reset(this: *@This()) void { this.iter_i = 0; this.i = 0; - this.tried_code_property = false; } /// The bun.String returned has not incremented it's reference count. @@ -107,27 +104,5 @@ pub fn JSPropertyIterator(comptime options: JSPropertyIteratorOptions) type { unreachable; } - - /// "code" is not always an own property, and we want to get it without risking exceptions. - pub fn getCodeProperty(this: *@This()) ?bun.String { - if (comptime !options.include_value) { - @compileError("TODO"); - } - - if (this.tried_code_property) { - return null; - } - - this.tried_code_property = true; - - const current = Bun__JSPropertyIterator__getCodeProperty(this.impl, this.globalObject, this.object); - if (current == .zero) { - return null; - } - current.ensureStillAlive(); - this.value = current; - - return bun.String.static("code"); - } }; } diff --git a/src/bun.js/bindings/ProcessBindingUV.cpp b/src/bun.js/bindings/ProcessBindingUV.cpp index b3a9aa5e0c..e29cd4d397 100644 --- a/src/bun.js/bindings/ProcessBindingUV.cpp +++ b/src/bun.js/bindings/ProcessBindingUV.cpp @@ -9,91 +9,346 @@ // clang-format off +#if !defined(E2BIG) +#define E2BIG 7 +#endif +#if !defined(EACCES) +#define EACCES 13 +#endif +#if !defined(EADDRINUSE) +#define EADDRINUSE 48 +#endif +#if !defined(EADDRNOTAVAIL) +#define EADDRNOTAVAIL 49 +#endif +#if !defined(EAFNOSUPPORT) +#define EAFNOSUPPORT 47 +#endif +#if !defined(EAGAIN) +#define EAGAIN 35 +#endif +#if !defined(EAI_ADDRFAMILY) +#define EAI_ADDRFAMILY 3000 +#endif +#if !defined(EAI_AGAIN) +#define EAI_AGAIN 3001 +#endif +#if !defined(EAI_BADFLAGS) +#define EAI_BADFLAGS 3002 +#endif +#if !defined(EAI_BADHINTS) +#define EAI_BADHINTS 3013 +#endif +#if !defined(EAI_CANCELED) +#define EAI_CANCELED 3003 +#endif +#if !defined(EAI_FAIL) +#define EAI_FAIL 3004 +#endif +#if !defined(EAI_FAMILY) +#define EAI_FAMILY 3005 +#endif +#if !defined(EAI_MEMORY) +#define EAI_MEMORY 3006 +#endif +#if !defined(EAI_NODATA) +#define EAI_NODATA 3007 +#endif +#if !defined(EAI_NONAME) +#define EAI_NONAME 3008 +#endif +#if !defined(EAI_OVERFLOW) +#define EAI_OVERFLOW 3009 +#endif +#if !defined(EAI_PROTOCOL) +#define EAI_PROTOCOL 3014 +#endif +#if !defined(EAI_SERVICE) +#define EAI_SERVICE 3010 +#endif +#if !defined(EAI_SOCKTYPE) +#define EAI_SOCKTYPE 3011 +#endif +#if !defined(EALREADY) +#define EALREADY 37 +#endif +#if !defined(EBADF) +#define EBADF 9 +#endif +#if !defined(EBUSY) +#define EBUSY 16 +#endif +#if !defined(ECANCELED) +#define ECANCELED 89 +#endif +#if !defined(ECHARSET) +#define ECHARSET 4080 +#endif +#if !defined(ECONNABORTED) +#define ECONNABORTED 53 +#endif +#if !defined(ECONNREFUSED) +#define ECONNREFUSED 61 +#endif +#if !defined(ECONNRESET) +#define ECONNRESET 54 +#endif +#if !defined(EDESTADDRREQ) +#define EDESTADDRREQ 39 +#endif +#if !defined(EEXIST) +#define EEXIST 17 +#endif +#if !defined(EFAULT) +#define EFAULT 14 +#endif +#if !defined(EFBIG) +#define EFBIG 27 +#endif +#if !defined(EHOSTUNREACH) +#define EHOSTUNREACH 65 +#endif +#if !defined(EINTR) +#define EINTR 4 +#endif +#if !defined(EINVAL) +#define EINVAL 22 +#endif +#if !defined(EIO) +#define EIO 5 +#endif +#if !defined(EISCONN) +#define EISCONN 56 +#endif +#if !defined(EISDIR) +#define EISDIR 21 +#endif +#if !defined(ELOOP) +#define ELOOP 62 +#endif +#if !defined(EMFILE) +#define EMFILE 24 +#endif +#if !defined(EMSGSIZE) +#define EMSGSIZE 40 +#endif +#if !defined(ENAMETOOLONG) +#define ENAMETOOLONG 63 +#endif +#if !defined(ENETDOWN) +#define ENETDOWN 50 +#endif +#if !defined(ENETUNREACH) +#define ENETUNREACH 51 +#endif +#if !defined(ENFILE) +#define ENFILE 23 +#endif +#if !defined(ENOBUFS) +#define ENOBUFS 55 +#endif +#if !defined(ENODEV) +#define ENODEV 19 +#endif +#if !defined(ENOENT) +#define ENOENT 2 +#endif +#if !defined(ENOMEM) +#define ENOMEM 12 +#endif +#if !defined(ENONET) +#define ENONET 4056 +#endif +#if !defined(ENOPROTOOPT) +#define ENOPROTOOPT 42 +#endif +#if !defined(ENOSPC) +#define ENOSPC 28 +#endif +#if !defined(ENOSYS) +#define ENOSYS 78 +#endif +#if !defined(ENOTCONN) +#define ENOTCONN 57 +#endif +#if !defined(ENOTDIR) +#define ENOTDIR 20 +#endif +#if !defined(ENOTEMPTY) +#define ENOTEMPTY 66 +#endif +#if !defined(ENOTSOCK) +#define ENOTSOCK 38 +#endif +#if !defined(ENOTSUP) +#define ENOTSUP 45 +#endif +#if !defined(EOVERFLOW) +#define EOVERFLOW 84 +#endif +#if !defined(EPERM) +#define EPERM 1 +#endif +#if !defined(EPIPE) +#define EPIPE 32 +#endif +#if !defined(EPROTO) +#define EPROTO 100 +#endif +#if !defined(EPROTONOSUPPORT) +#define EPROTONOSUPPORT 43 +#endif +#if !defined(EPROTOTYPE) +#define EPROTOTYPE 41 +#endif +#if !defined(ERANGE) +#define ERANGE 34 +#endif +#if !defined(EROFS) +#define EROFS 30 +#endif +#if !defined(ESHUTDOWN) +#define ESHUTDOWN 58 +#endif +#if !defined(ESPIPE) +#define ESPIPE 29 +#endif +#if !defined(ESRCH) +#define ESRCH 3 +#endif +#if !defined(ETIMEDOUT) +#define ETIMEDOUT 60 +#endif +#if !defined(ETXTBSY) +#define ETXTBSY 26 +#endif +#if !defined(EXDEV) +#define EXDEV 18 +#endif +#if !defined(UNKNOWN) +#define UNKNOWN 4094 +#endif +// this is intentionally always overridden +#if defined(EOF) +#undef EOF +#endif +#define EOF 4095 +#if !defined(ENXIO) +#define ENXIO 6 +#endif +#if !defined(EMLINK) +#define EMLINK 31 +#endif +#if !defined(EHOSTDOWN) +#define EHOSTDOWN 64 +#endif +#if !defined(EREMOTEIO) +#define EREMOTEIO 4030 +#endif +#if !defined(ENOTTY) +#define ENOTTY 25 +#endif +#if !defined(EFTYPE) +#define EFTYPE 79 +#endif +#if !defined(EILSEQ) +#define EILSEQ 92 +#endif +#if !defined(ESOCKTNOSUPPORT) +#define ESOCKTNOSUPPORT 44 +#endif +#if !defined(ENODATA) +#define ENODATA 96 +#endif +#if !defined(EUNATCH) +#define EUNATCH 4023 +#endif + #define BUN_UV_ERRNO_MAP(macro) \ - macro(E2BIG, -7, "argument list too long") \ - macro(EACCES, -13, "permission denied") \ - macro(EADDRINUSE, -48, "address already in use") \ - macro(EADDRNOTAVAIL, -49, "address not available") \ - macro(EAFNOSUPPORT, -47, "address family not supported") \ - macro(EAGAIN, -35, "resource temporarily unavailable") \ - macro(EAI_ADDRFAMILY, -3000, "address family not supported") \ - macro(EAI_AGAIN, -3001, "temporary failure") \ - macro(EAI_BADFLAGS, -3002, "bad ai_flags value") \ - macro(EAI_BADHINTS, -3013, "invalid value for hints") \ - macro(EAI_CANCELED, -3003, "request canceled") \ - macro(EAI_FAIL, -3004, "permanent failure") \ - macro(EAI_FAMILY, -3005, "ai_family not supported") \ - macro(EAI_MEMORY, -3006, "out of memory") \ - macro(EAI_NODATA, -3007, "no address") \ - macro(EAI_NONAME, -3008, "unknown node or service") \ - macro(EAI_OVERFLOW, -3009, "argument buffer overflow") \ - macro(EAI_PROTOCOL, -3014, "resolved protocol is unknown") \ - macro(EAI_SERVICE, -3010, "service not available for socket type") \ - macro(EAI_SOCKTYPE, -3011, "socket type not supported") \ - macro(EALREADY, -37, "connection already in progress") \ - macro(EBADF, -9, "bad file descriptor") \ - macro(EBUSY, -16, "resource busy or locked") \ - macro(ECANCELED, -89, "operation canceled") \ - macro(ECHARSET, -4080, "invalid Unicode character") \ - macro(ECONNABORTED, -53, "software caused connection abort") \ - macro(ECONNREFUSED, -61, "connection refused") \ - macro(ECONNRESET, -54, "connection reset by peer") \ - macro(EDESTADDRREQ, -39, "destination address required") \ - macro(EEXIST, -17, "file already exists") \ - macro(EFAULT, -14, "bad address in system call argument") \ - macro(EFBIG, -27, "file too large") \ - macro(EHOSTUNREACH, -65, "host is unreachable") \ - macro(EINTR, -4, "interrupted system call") \ - macro(EINVAL, -22, "invalid argument") \ - macro(EIO, -5, "i/o error") \ - macro(EISCONN, -56, "socket is already connected") \ - macro(EISDIR, -21, "illegal operation on a directory") \ - macro(ELOOP, -62, "too many symbolic links encountered") \ - macro(EMFILE, -24, "too many open files") \ - macro(EMSGSIZE, -40, "message too long") \ - macro(ENAMETOOLONG, -63, "name too long") \ - macro(ENETDOWN, -50, "network is down") \ - macro(ENETUNREACH, -51, "network is unreachable") \ - macro(ENFILE, -23, "file table overflow") \ - macro(ENOBUFS, -55, "no buffer space available") \ - macro(ENODEV, -19, "no such device") \ - macro(ENOENT, -2, "no such file or directory") \ - macro(ENOMEM, -12, "not enough memory") \ - macro(ENONET, -4056, "machine is not on the network") \ - macro(ENOPROTOOPT, -42, "protocol not available") \ - macro(ENOSPC, -28, "no space left on device") \ - macro(ENOSYS, -78, "function not implemented") \ - macro(ENOTCONN, -57, "socket is not connected") \ - macro(ENOTDIR, -20, "not a directory") \ - macro(ENOTEMPTY, -66, "directory not empty") \ - macro(ENOTSOCK, -38, "socket operation on non-socket") \ - macro(ENOTSUP, -45, "operation not supported on socket") \ - macro(EOVERFLOW, -84, "value too large for defined data type") \ - macro(EPERM, -1, "operation not permitted") \ - macro(EPIPE, -32, "broken pipe") \ - macro(EPROTO, -100, "protocol error") \ - macro(EPROTONOSUPPORT, -43, "protocol not supported") \ - macro(EPROTOTYPE, -41, "protocol wrong type for socket") \ - macro(ERANGE, -34, "result too large") \ - macro(EROFS, -30, "read-only file system") \ - macro(ESHUTDOWN, -58, "cannot send after transport endpoint shutdown") \ - macro(ESPIPE, -29, "invalid seek") \ - macro(ESRCH, -3, "no such process") \ - macro(ETIMEDOUT, -60, "connection timed out") \ - macro(ETXTBSY, -26, "text file is busy") \ - macro(EXDEV, -18, "cross-device link not permitted") \ - macro(UNKNOWN, -4094, "unknown error") \ - macro(EOF, -4095, "end of file") \ - macro(ENXIO, -6, "no such device or address") \ - macro(EMLINK, -31, "too many links") \ - macro(EHOSTDOWN, -64, "host is down") \ - macro(EREMOTEIO, -4030, "remote I/O error") \ - macro(ENOTTY, -25, "inappropriate ioctl for device") \ - macro(EFTYPE, -79, "inappropriate file type or format") \ - macro(EILSEQ, -92, "illegal byte sequence") \ - macro(ESOCKTNOSUPPORT, -44, "socket type not supported") \ - macro(ENODATA, -96, "no data available") \ - macro(EUNATCH, -4023, "protocol driver not attache") + macro(E2BIG, "argument list too long") \ + macro(EACCES, "permission denied") \ + macro(EADDRINUSE, "address already in use") \ + macro(EADDRNOTAVAIL, "address not available") \ + macro(EAFNOSUPPORT, "address family not supported") \ + macro(EAGAIN, "resource temporarily unavailable") \ + macro(EAI_ADDRFAMILY, "address family not supported") \ + macro(EAI_AGAIN, "temporary failure") \ + macro(EAI_BADFLAGS, "bad ai_flags value") \ + macro(EAI_BADHINTS, "invalid value for hints") \ + macro(EAI_CANCELED, "request canceled") \ + macro(EAI_FAIL, "permanent failure") \ + macro(EAI_FAMILY, "ai_family not supported") \ + macro(EAI_MEMORY, "out of memory") \ + macro(EAI_NODATA, "no address") \ + macro(EAI_NONAME, "unknown node or service") \ + macro(EAI_OVERFLOW, "argument buffer overflow") \ + macro(EAI_PROTOCOL, "resolved protocol is unknown") \ + macro(EAI_SERVICE, "service not available for socket type") \ + macro(EAI_SOCKTYPE, "socket type not supported") \ + macro(EALREADY, "connection already in progress") \ + macro(EBADF, "bad file descriptor") \ + macro(EBUSY, "resource busy or locked") \ + macro(ECANCELED, "operation canceled") \ + macro(ECHARSET, "invalid Unicode character") \ + macro(ECONNABORTED, "software caused connection abort") \ + macro(ECONNREFUSED, "connection refused") \ + macro(ECONNRESET, "connection reset by peer") \ + macro(EDESTADDRREQ, "destination address required") \ + macro(EEXIST, "file already exists") \ + macro(EFAULT, "bad address in system call argument") \ + macro(EFBIG, "file too large") \ + macro(EHOSTUNREACH, "host is unreachable") \ + macro(EINTR, "interrupted system call") \ + macro(EINVAL, "invalid argument") \ + macro(EIO, "i/o error") \ + macro(EISCONN, "socket is already connected") \ + macro(EISDIR, "illegal operation on a directory") \ + macro(ELOOP, "too many symbolic links encountered") \ + macro(EMFILE, "too many open files") \ + macro(EMSGSIZE, "message too long") \ + macro(ENAMETOOLONG, "name too long") \ + macro(ENETDOWN, "network is down") \ + macro(ENETUNREACH, "network is unreachable") \ + macro(ENFILE, "file table overflow") \ + macro(ENOBUFS, "no buffer space available") \ + macro(ENODEV, "no such device") \ + macro(ENOENT, "no such file or directory") \ + macro(ENOMEM, "not enough memory") \ + macro(ENONET, "machine is not on the network") \ + macro(ENOPROTOOPT, "protocol not available") \ + macro(ENOSPC, "no space left on device") \ + macro(ENOSYS, "function not implemented") \ + macro(ENOTCONN, "socket is not connected") \ + macro(ENOTDIR, "not a directory") \ + macro(ENOTEMPTY, "directory not empty") \ + macro(ENOTSOCK, "socket operation on non-socket") \ + macro(ENOTSUP, "operation not supported on socket") \ + macro(EOVERFLOW, "value too large for defined data type") \ + macro(EPERM, "operation not permitted") \ + macro(EPIPE, "broken pipe") \ + macro(EPROTO, "protocol error") \ + macro(EPROTONOSUPPORT, "protocol not supported") \ + macro(EPROTOTYPE, "protocol wrong type for socket") \ + macro(ERANGE, "result too large") \ + macro(EROFS, "read-only file system") \ + macro(ESHUTDOWN, "cannot send after transport endpoint shutdown") \ + macro(ESPIPE, "invalid seek") \ + macro(ESRCH, "no such process") \ + macro(ETIMEDOUT, "connection timed out") \ + macro(ETXTBSY, "text file is busy") \ + macro(EXDEV, "cross-device link not permitted") \ + macro(UNKNOWN, "unknown error") \ + macro(EOF, "end of file") \ + macro(ENXIO, "no such device or address") \ + macro(EMLINK, "too many links") \ + macro(EHOSTDOWN, "host is down") \ + macro(EREMOTEIO, "remote I/O error") \ + macro(ENOTTY, "inappropriate ioctl for device") \ + macro(EFTYPE, "inappropriate file type or format") \ + macro(EILSEQ, "illegal byte sequence") \ + macro(ESOCKTNOSUPPORT, "socket type not supported") \ + macro(ENODATA, "no data available") \ + macro(EUNATCH, "protocol driver not attached") // clang-format on namespace Bun { @@ -112,17 +367,11 @@ JSC_DEFINE_HOST_FUNCTION(jsErrname, (JSGlobalObject * globalObject, JSC::CallFra } auto err = arg0.toInt32(globalObject); - switch (err) { -#define CASE(name, value, desc) \ - case value: \ - return JSValue::encode(JSC::jsString(vm, String(#name##_s))); +#define CASE(name, desc) \ + if (err == -name) return JSValue::encode(JSC::jsString(vm, String(#name##_s))); - BUN_UV_ERRNO_MAP(CASE) + BUN_UV_ERRNO_MAP(CASE) #undef CASE - default: { - break; - } - } return JSValue::encode(jsString(vm, makeString("Unknown system error: "_s, err))); } @@ -140,7 +389,7 @@ JSC_DEFINE_HOST_FUNCTION(jsGetErrorMap, (JSGlobalObject * globalObject, JSC::Cal map->set(globalObject, JSC::jsNumber(value), arr); }; -#define PUT_PROPERTY(name, value, desc) putProperty(vm, map, globalObject, #name##_s, value, desc##_s); +#define PUT_PROPERTY(name, desc) putProperty(vm, map, globalObject, #name##_s, -name, desc##_s); BUN_UV_ERRNO_MAP(PUT_PROPERTY) #undef PUT_PROPERTY @@ -157,11 +406,11 @@ JSObject* create(VM& vm, JSGlobalObject* globalObject) // Before: 96305608 // After: 95973832 const auto putNamedProperty = [](JSC::VM& vm, JSObject* bindingObject, const ASCIILiteral name, int value) -> void { - bindingObject->putDirect(vm, JSC::Identifier::fromString(vm, makeString("UV_"_s, name)), JSC::jsNumber(value)); + bindingObject->putDirect(vm, JSC::Identifier::fromString(vm, name), JSC::jsNumber(value)); }; -#define PUT_PROPERTY(name, value, desc) \ - putNamedProperty(vm, bindingObject, #name##_s, value); +#define PUT_PROPERTY(name, desc) \ + putNamedProperty(vm, bindingObject, "UV_" #name##_s, -name); BUN_UV_ERRNO_MAP(PUT_PROPERTY) #undef PUT_PROPERTY diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index bce7203c73..d553e86f4a 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -6280,3 +6280,31 @@ CPP_DECL void Bun__CallFrame__getCallerSrcLoc(JSC::CallFrame* callFrame, JSC::JS *outLine = lineColumn.line; *outColumn = lineColumn.column; } + +extern "C" EncodedJSValue Bun__JSObject__getCodePropertyVMInquiry(JSC::JSGlobalObject* global, JSC::JSObject* object) +{ + if (UNLIKELY(!object)) { + return {}; + } + + auto& vm = global->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + if (UNLIKELY(object->type() == JSC::ProxyObjectType)) { + return {}; + } + + auto& builtinNames = WebCore::builtinNames(vm); + + PropertySlot slot(object, PropertySlot::InternalMethodType::VMInquiry, &vm); + ASSERT(!scope.exception()); + if (!object->getNonIndexPropertySlot(global, builtinNames.codePublicName(), slot)) { + ASSERT(!scope.exception()); + return {}; + } + + if (slot.isAccessor() || slot.isCustom()) { + return {}; + } + + return JSValue::encode(slot.getPureResult()); +} diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index 1d88a6d235..8f74b97657 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -156,6 +156,15 @@ pub const JSObject = extern struct { pub fn putRecord(this: *JSObject, global: *JSGlobalObject, key: *ZigString, values: []ZigString) void { return cppFn("putRecord", .{ this, global, key, values.ptr, values.len }); } + + extern fn Bun__JSObject__getCodePropertyVMInquiry(*JSGlobalObject, *JSObject) JSValue; + + /// This will not call getters or be observable from JavaScript. + pub fn getCodePropertyVMInquiry(obj: *JSObject, global: *JSGlobalObject) ?JSValue { + const v = Bun__JSObject__getCodePropertyVMInquiry(global, obj); + if (v == .zero) return null; + return v; + } }; pub const CachedBytecode = opaque { @@ -5518,12 +5527,12 @@ pub const JSValue = enum(i64) { } /// Many Node.js APIs use `validateBoolean` - /// Missing value, null, and undefined return `null` + /// Missing value and undefined return `null` pub inline fn getBooleanStrict(this: JSValue, global: *JSGlobalObject, comptime property_name: []const u8) JSError!?bool { const prop = try this.get(global, property_name) orelse return null; return switch (prop) { - .null, .undefined => null, + .undefined => null, .false, .true => prop == .true, else => { return JSC.Node.validators.throwErrInvalidArgType(global, property_name, .{}, "boolean", prop); diff --git a/src/bun.js/event_loop.zig b/src/bun.js/event_loop.zig index 232be9a227..f1d8deb324 100644 --- a/src/bun.js/event_loop.zig +++ b/src/bun.js/event_loop.zig @@ -372,6 +372,7 @@ const Link = JSC.Node.Async.link; const Symlink = JSC.Node.Async.symlink; const Readlink = JSC.Node.Async.readlink; const Realpath = JSC.Node.Async.realpath; +const RealpathNonNative = JSC.Node.Async.realpathNonNative; const Mkdir = JSC.Node.Async.mkdir; const Fsync = JSC.Node.Async.fsync; const Rename = JSC.Node.Async.rename; @@ -457,6 +458,7 @@ pub const Task = TaggedPointerUnion(.{ Symlink, Readlink, Realpath, + RealpathNonNative, Mkdir, Fsync, Fdatasync, @@ -1213,6 +1215,10 @@ pub const EventLoop = struct { var any: *Realpath = task.get(Realpath).?; any.runFromJSThread(); }, + @field(Task.Tag, typeBaseName(@typeName(RealpathNonNative))) => { + var any: *RealpathNonNative = task.get(RealpathNonNative).?; + any.runFromJSThread(); + }, @field(Task.Tag, typeBaseName(@typeName(Mkdir))) => { var any: *Mkdir = task.get(Mkdir).?; any.runFromJSThread(); diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index 41d5c1ba2f..b56003968a 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -3778,6 +3778,7 @@ pub const VirtualMachine = struct { var exception_holder = ZigException.Holder.init(); var exception = exception_holder.zigException(); defer exception_holder.deinit(this); + defer error_instance.ensureStillAlive(); // The ZigException structure stores substrings of the source code, in // which we need the lifetime of this data to outlive the inner call to @@ -3850,9 +3851,28 @@ pub const VirtualMachine = struct { } const name = exception.name; - const message = exception.message; + const is_error_instance = error_instance != .zero and error_instance.jsType() == .ErrorInstance; + const code: ?[]const u8 = if (is_error_instance) code: { + if (error_instance.uncheckedPtrCast(JSC.JSObject).getCodePropertyVMInquiry(this.global)) |code_value| { + if (code_value.isString()) { + const code_string = code_value.toBunString2(this.global) catch { + // JSC::JSString to WTF::String can only fail on out of memory. + bun.outOfMemory(); + }; + defer code_string.deref(); + + if (code_string.is8Bit()) { + // We can count on this memory being valid until the end + // of this function because + break :code code_string.latin1(); + } + } + } + break :code null; + } else null; + var did_print_name = false; if (source_lines.next()) |source| brk: { if (source.text.len == 0) break :brk; @@ -3893,7 +3913,7 @@ pub const VirtualMachine = struct { ); } - try this.printErrorNameAndMessage(name, message, Writer, writer, allow_ansi_color); + try this.printErrorNameAndMessage(name, message, code, Writer, writer, allow_ansi_color); } else if (top_frame) |top| { defer did_print_name = true; const display_line = source.line + 1; @@ -3938,12 +3958,12 @@ pub const VirtualMachine = struct { } } - try this.printErrorNameAndMessage(name, message, Writer, writer, allow_ansi_color); + try this.printErrorNameAndMessage(name, message, code, Writer, writer, allow_ansi_color); } } if (!did_print_name) { - try this.printErrorNameAndMessage(name, message, Writer, writer, allow_ansi_color); + try this.printErrorNameAndMessage(name, message, code, Writer, writer, allow_ansi_color); } // This is usually unsafe to do, but we are protecting them each time first @@ -3955,125 +3975,112 @@ pub const VirtualMachine = struct { errors_to_append.deinit(); } - var saw_cause = false; - if (error_instance != .zero) { - const error_instance_type = error_instance.jsType(); - if (error_instance_type == .ErrorInstance) { - const Iterator = JSC.JSPropertyIterator(.{ - .include_value = true, - .skip_empty_name = true, - .own_properties_only = true, - .observable = false, - .only_non_index_properties = true, - }); - var iterator = try Iterator.init(this.global, error_instance); - defer iterator.deinit(); - const longest_name = @min(iterator.getLongestPropertyName(), 10); - var is_first_property = true; - while ((try iterator.next()) orelse iterator.getCodeProperty()) |field| { - const value = iterator.value; - if (field.eqlComptime("message") or field.eqlComptime("name") or field.eqlComptime("stack")) { - continue; + if (is_error_instance) { + var saw_cause = false; + const Iterator = JSC.JSPropertyIterator(.{ + .include_value = true, + .skip_empty_name = true, + .own_properties_only = true, + .observable = false, + .only_non_index_properties = true, + }); + var iterator = try Iterator.init(this.global, error_instance); + defer iterator.deinit(); + const longest_name = @min(iterator.getLongestPropertyName(), 10); + var is_first_property = true; + while (try iterator.next()) |field| { + const value = iterator.value; + if (field.eqlComptime("message") or field.eqlComptime("name") or field.eqlComptime("stack")) { + continue; + } + + // We special-case the code property. Let's avoid printing it twice. + if (field.eqlComptime("code") and code != null) { + continue; + } + + const kind = value.jsType(); + if (kind == .ErrorInstance and + // avoid infinite recursion + !prev_had_errors) + { + if (field.eqlComptime("cause")) { + saw_cause = true; + } + value.protect(); + try errors_to_append.append(value); + } else if (kind.isObject() or kind.isArray() or value.isPrimitive() or kind.isStringLike()) { + var bun_str = bun.String.empty; + defer bun_str.deref(); + const prev_disable_inspect_custom = formatter.disable_inspect_custom; + const prev_quote_strings = formatter.quote_strings; + const prev_max_depth = formatter.max_depth; + formatter.depth += 1; + defer { + formatter.depth -= 1; + formatter.max_depth = prev_max_depth; + formatter.quote_strings = prev_quote_strings; + formatter.disable_inspect_custom = prev_disable_inspect_custom; + } + formatter.max_depth = 1; + formatter.quote_strings = true; + formatter.disable_inspect_custom = true; + + const pad_left = longest_name -| field.length(); + is_first_property = false; + try writer.writeByteNTimes(' ', pad_left); + + try writer.print(comptime Output.prettyFmt(" {}: ", allow_ansi_color), .{field}); + + // When we're printing errors for a top-level uncaught exception / rejection, suppress further errors here. + if (allow_side_effects) { + if (this.global.hasException()) { + this.global.clearException(); + } } - // We special-case the code property. Let's avoid printing it twice. - if (field.eqlComptime("code")) { - if (!iterator.tried_code_property) continue; - } - - const kind = value.jsType(); - if (kind == .ErrorInstance and - // avoid infinite recursion - !prev_had_errors) - { - if (field.eqlComptime("cause")) { - saw_cause = true; - } - value.protect(); - try errors_to_append.append(value); - } else if (kind.isObject() or kind.isArray() or value.isPrimitive() or kind.isStringLike()) { - var bun_str = bun.String.empty; - defer bun_str.deref(); - const prev_disable_inspect_custom = formatter.disable_inspect_custom; - const prev_quote_strings = formatter.quote_strings; - const prev_max_depth = formatter.max_depth; - formatter.depth += 1; - defer { - formatter.depth -= 1; - formatter.max_depth = prev_max_depth; - formatter.quote_strings = prev_quote_strings; - formatter.disable_inspect_custom = prev_disable_inspect_custom; - } - formatter.max_depth = 1; - formatter.quote_strings = true; - formatter.disable_inspect_custom = true; - - const pad_left = longest_name -| field.length(); - is_first_property = false; - try writer.writeByteNTimes(' ', pad_left); - - try writer.print(comptime Output.prettyFmt(" {}: ", allow_ansi_color), .{field}); - - // When we're printing errors for a top-level uncaught exception / rejection, suppress further errors here. - if (allow_side_effects) { - if (this.global.hasException()) { - this.global.clearException(); - } - } - - formatter.format( - JSC.Formatter.Tag.getAdvanced( - value, - this.global, - .{ .disable_inspect_custom = true, .hide_global = true }, - ), - Writer, - writer, + formatter.format( + JSC.Formatter.Tag.getAdvanced( value, this.global, - allow_ansi_color, - ) catch {}; - - if (allow_side_effects) { - // When we're printing errors for a top-level uncaught exception / rejection, suppress further errors here. - if (this.global.hasException()) { - this.global.clearException(); - } - } else if (this.global.hasException() or formatter.failed) { - return; - } - - try writer.writeAll(comptime Output.prettyFmt(",\n", allow_ansi_color)); - } - } - - if (!is_first_property) { - try writer.writeAll("\n"); - } - } else { - // If you do reportError([1,2,3]] we should still show something at least. - const tag = JSC.Formatter.Tag.getAdvanced( - error_instance, - this.global, - .{ .disable_inspect_custom = true, .hide_global = true }, - ); - if (tag.tag != .NativeCode) { - try formatter.format( - tag, + .{ .disable_inspect_custom = true, .hide_global = true }, + ), Writer, writer, - error_instance, + value, this.global, allow_ansi_color, - ); + ) catch {}; - // Always include a newline in this case - try writer.writeAll("\n"); + if (allow_side_effects) { + // When we're printing errors for a top-level uncaught exception / rejection, suppress further errors here. + if (this.global.hasException()) { + this.global.clearException(); + } + } else if (this.global.hasException() or formatter.failed) { + return; + } + + try writer.writeAll(comptime Output.prettyFmt(",\n", allow_ansi_color)); } } + if (code) |code_str| { + const pad_left = longest_name -| "code".len; + is_first_property = false; + try writer.writeByteNTimes(' ', pad_left); + + try writer.print(comptime Output.prettyFmt(" code: {}\n", allow_ansi_color), .{ + bun.fmt.quote(code_str), + }); + } + + if (!is_first_property) { + try writer.writeAll("\n"); + } + // "cause" is not enumerable, so the above loop won't see it. - if (!saw_cause and error_instance_type == .ErrorInstance) { + if (!saw_cause) { if (error_instance.getOwn(this.global, "cause")) |cause| { if (cause.jsType() == .ErrorInstance) { cause.protect(); @@ -4081,6 +4088,26 @@ pub const VirtualMachine = struct { } } } + } else if (error_instance != .zero) { + // If you do reportError([1,2,3]] we should still show something at least. + const tag = JSC.Formatter.Tag.getAdvanced( + error_instance, + this.global, + .{ .disable_inspect_custom = true, .hide_global = true }, + ); + if (tag.tag != .NativeCode) { + try formatter.format( + tag, + Writer, + writer, + error_instance, + this.global, + allow_ansi_color, + ); + + // Always include a newline in this case + try writer.writeAll("\n"); + } } try printStackTrace(@TypeOf(writer), writer, exception.stack, allow_ansi_color); @@ -4091,10 +4118,47 @@ pub const VirtualMachine = struct { } } - fn printErrorNameAndMessage(_: *VirtualMachine, name: String, message: String, comptime Writer: type, writer: Writer, comptime allow_ansi_color: bool) !void { + fn printErrorNameAndMessage( + _: *VirtualMachine, + name: String, + message: String, + optional_code: ?[]const u8, + comptime Writer: type, + writer: Writer, + comptime allow_ansi_color: bool, + ) !void { if (!name.isEmpty() and !message.isEmpty()) { - const display_name: String = if (name.eqlComptime("Error")) String.init("error") else name; - try writer.print(comptime Output.prettyFmt("{}: {s}\n", allow_ansi_color), .{ display_name, message }); + const display_name, const display_message = if (name.eqlComptime("Error")) brk: { + // If `err.code` is set, and `err.message` is of form `{code}: {text}`, + // use the code as the name since `error: ENOENT: no such ...` is + // not as nice looking since it there are two error prefixes. + if (optional_code) |code| if (bun.strings.isAllASCII(code)) { + const has_prefix = switch (message.isUTF16()) { + inline else => |is_utf16| has_prefix: { + const msg_chars = if (is_utf16) message.utf16() else message.latin1(); + // + 1 to ensure the message is a non-empty string. + break :has_prefix msg_chars.len > code.len + ": ".len + 1 and + (if (is_utf16) + // there is no existing function to perform this slice comparison + // []const u16, []const u8 + for (code, msg_chars[0..code.len]) |a, b| { + if (a != b) break false; + } else true + else + bun.strings.eqlLong(msg_chars[0..code.len], code, false)) and + msg_chars[code.len] == ':' and + msg_chars[code.len + 1] == ' '; + }, + }; + if (has_prefix) break :brk .{ + String.init(code), + message.substring(code.len + ": ".len), + }; + }; + + break :brk .{ String.static("error"), message }; + } else .{ name, message }; + try writer.print(comptime Output.prettyFmt("{}: {s}\n", allow_ansi_color), .{ display_name, display_message }); } else if (!name.isEmpty()) { if (!name.hasPrefixComptime("error")) { try writer.print(comptime Output.prettyFmt("error: {}\n", allow_ansi_color), .{name}); diff --git a/src/bun.js/module_loader.zig b/src/bun.js/module_loader.zig index cfc6030147..8831304b53 100644 --- a/src/bun.js/module_loader.zig +++ b/src/bun.js/module_loader.zig @@ -2479,15 +2479,6 @@ pub const ModuleLoader = struct { return jsSyntheticModule(.InternalForTesting, specifier); }, - .@"internal/test/binding" => { - if (!Environment.isDebug) { - if (!is_allowed_to_use_internal_testing_apis) - return null; - } - - return jsSyntheticModule(.@"internal:test/binding", specifier); - }, - // These are defined in src/js/* .@"bun:ffi" => return jsSyntheticModule(.@"bun:ffi", specifier), .@"bun:sql" => { @@ -2765,7 +2756,6 @@ pub const HardcodedModule = enum { @"node:cluster", // these are gated behind '--expose-internals' @"bun:internal-for-testing", - @"internal/test/binding", /// Already resolved modules go in here. /// This does not remap the module name, it is just a hash table. @@ -2847,8 +2837,6 @@ pub const HardcodedModule = enum { .{ "@vercel/fetch", HardcodedModule.@"@vercel/fetch" }, .{ "utf-8-validate", HardcodedModule.@"utf-8-validate" }, .{ "abort-controller", HardcodedModule.@"abort-controller" }, - - .{ "internal/test/binding", HardcodedModule.@"internal/test/binding" }, }, ); @@ -3008,8 +2996,6 @@ pub const HardcodedModule = enum { .{ "next/dist/compiled/ws", .{ .path = "ws" } }, .{ "next/dist/compiled/node-fetch", .{ .path = "node-fetch" } }, .{ "next/dist/compiled/undici", .{ .path = "undici" } }, - - .{ "internal/test/binding", .{ .path = "internal/test/binding" } }, }; const bun_extra_alias_kvs = [_]struct { string, Alias }{ diff --git a/src/bun.js/node/node.classes.ts b/src/bun.js/node/node.classes.ts index 3a8d426c6b..0ebf557bf1 100644 --- a/src/bun.js/node/node.classes.ts +++ b/src/bun.js/node/node.classes.ts @@ -571,8 +571,6 @@ export default [ mkdtempSync: { fn: "mkdtempSync", length: 2 }, open: { fn: "open", length: 4 }, openSync: { fn: "openSync", length: 3 }, - opendir: { fn: "opendir", length: 3 }, - opendirSync: { fn: "opendirSync", length: 2 }, readdir: { fn: "readdir", length: 3 }, readdirSync: { fn: "readdirSync", length: 2 }, read: { fn: "read", length: 6 }, @@ -610,6 +608,8 @@ export default [ writeSync: { fn: "writeSync", length: 5 }, writev: { fn: "writev", length: 4 }, writevSync: { fn: "writevSync", length: 3 }, + realpathNative: { fn: "realpathNative", length: 3 }, + realpathNativeSync: { fn: "realpathNativeSync", length: 3 }, // TODO: // Dir: { fn: 'Dir', length: 3 }, Dirent: { getter: "getDirent" }, diff --git a/src/bun.js/node/node_assert.zig b/src/bun.js/node/node_assert.zig index 36d944975a..b0016b55c1 100644 --- a/src/bun.js/node/node_assert.zig +++ b/src/bun.js/node/node_assert.zig @@ -10,8 +10,6 @@ const JSValue = JSC.JSValue; const StringDiffList = MyersDiff.DiffList([]const u8); -const print = std.debug.print; - /// Compare `actual` and `expected`, producing a diff that would turn `actual` /// into `expected`. /// diff --git a/src/bun.js/node/node_fs.zig b/src/bun.js/node/node_fs.zig index d0500f31cb..3a64db9dbe 100644 --- a/src/bun.js/node/node_fs.zig +++ b/src/bun.js/node/node_fs.zig @@ -10,7 +10,6 @@ const JSC = bun.JSC; const PathString = JSC.PathString; const Environment = bun.Environment; const C = bun.C; -const Flavor = JSC.Node.Flavor; const system = std.posix.system; const Maybe = JSC.Maybe; const Encoding = JSC.Node.Encoding; @@ -57,6 +56,11 @@ else // Windows does not have permissions 0; +// All async FS functions are run in a thread pool, but some implementations may +// decide to do something slightly different. For example, reading a file has +// an extra stack buffer in the async case. +pub const Flavor = enum { sync, @"async" }; + const ArrayBuffer = JSC.MarkedArrayBuffer; const Buffer = JSC.Buffer; const FileSystemFlags = JSC.Node.FileSystemFlags; @@ -89,6 +93,7 @@ pub const Async = struct { pub const readlink = NewAsyncFSTask(Return.Readlink, Arguments.Readlink, NodeFS.readlink); pub const readv = NewUVFSRequest(Return.Readv, Arguments.Readv, .readv); pub const realpath = NewAsyncFSTask(Return.Realpath, Arguments.Realpath, NodeFS.realpath); + pub const realpathNonNative = NewAsyncFSTask(Return.Realpath, Arguments.Realpath, NodeFS.realpathNonNative); pub const rename = NewAsyncFSTask(Return.Rename, Arguments.Rename, NodeFS.rename); pub const rm = NewAsyncFSTask(Return.Rm, Arguments.Rm, NodeFS.rm); pub const rmdir = NewAsyncFSTask(Return.Rmdir, Arguments.RmDir, NodeFS.rmdir); @@ -126,7 +131,6 @@ pub const Async = struct { .path = PathLike{ .string = PathString.init(this.path) }, .recursive = true, }, - .sync, ); switch (result) { .err => |err| { @@ -374,7 +378,7 @@ pub const Async = struct { var this: *Task = @alignCast(@fieldParentPtr("task", task)); var node_fs = NodeFS{}; - this.result = Function(&node_fs, this.args, .promise); + this.result = Function(&node_fs, this.args, .@"async"); if (this.result == .err) { this.result.err.path = bun.default_allocator.dupe(u8, this.result.err.path) catch ""; @@ -703,8 +707,8 @@ pub fn NewAsyncCpTask(comptime is_shell: bool) type { const args = this.args; var src_buf: bun.OSPathBuffer = undefined; var dest_buf: bun.OSPathBuffer = undefined; - const src = args.src.osPath(@ptrCast(&src_buf)); - const dest = args.dest.osPath(@ptrCast(&dest_buf)); + const src = args.src.osPath(&src_buf); + const dest = args.dest.osPath(&dest_buf); if (Environment.isWindows) { const attributes = windows.GetFileAttributesW(src); @@ -1297,7 +1301,7 @@ pub const Arguments = struct { pub const Truncate = struct { /// Passing a file descriptor is deprecated and may result in an error being thrown in the future. path: PathOrFileDescriptor, - len: JSC.WebCore.Blob.SizeType = 0, + len: JSC.WebCore.Blob.SizeType, flags: i32 = 0, pub fn deinit(this: @This()) void { @@ -1355,12 +1359,9 @@ pub const Arguments = struct { } pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!Writev { - const fd_value = arguments.nextEat() orelse { - return ctx.throwInvalidArguments("file descriptor is required", .{}); - }; - + const fd_value = arguments.nextEat() orelse JSC.JSValue.undefined; const fd = try JSC.Node.fileDescriptorFromJS(ctx, fd_value) orelse { - return ctx.throwInvalidArguments("file descriptor must be a number", .{}); + return throwInvalidFdError(ctx, fd_value); }; const buffers = try JSC.Node.VectorArrayBuffer.fromJS( @@ -1412,12 +1413,9 @@ pub const Arguments = struct { } pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!Readv { - const fd_value = arguments.nextEat() orelse { - return ctx.throwInvalidArguments("file descriptor is required", .{}); - }; - + const fd_value = arguments.nextEat() orelse JSC.JSValue.undefined; const fd = try JSC.Node.fileDescriptorFromJS(ctx, fd_value) orelse { - return ctx.throwInvalidArguments("file descriptor must be a number", .{}); + return throwInvalidFdError(ctx, fd_value); }; const buffers = try JSC.Node.VectorArrayBuffer.fromJS( @@ -1461,14 +1459,11 @@ pub const Arguments = struct { } pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!FTruncate { - const fd = try JSC.Node.fileDescriptorFromJS(ctx, arguments.next() orelse { - return ctx.throwInvalidArguments("file descriptor is required", .{}); - }) orelse { - return ctx.throwInvalidArguments("file descriptor must be a number", .{}); + const fd_value = arguments.nextEat() orelse JSC.JSValue.undefined; + const fd = try JSC.Node.fileDescriptorFromJS(ctx, fd_value) orelse { + return throwInvalidFdError(ctx, fd_value); }; - arguments.eat(); - const len: JSC.WebCore.Blob.SizeType = brk: { const len_value = arguments.next() orelse break :brk 0; if (len_value.isNumber()) { @@ -1512,22 +1507,15 @@ pub const Arguments = struct { }; arguments.eat(); - if (!uid_value.isNumber()) { - return ctx.throwInvalidArgumentTypeValue("uid", "number", uid_value); - } - break :brk @as(uid_t, @intCast(uid_value.toInt32())); + break :brk wrapTo(uid_t, try JSC.Node.validators.validateInteger(ctx, uid_value, "uid", .{}, -1, std.math.maxInt(u32))); }; const gid: gid_t = brk: { const gid_value = arguments.next() orelse break :brk { return ctx.throwInvalidArguments("gid is required", .{}); }; - arguments.eat(); - if (!gid_value.isNumber()) { - return ctx.throwInvalidArgumentTypeValue("gid", "number", gid_value); - } - break :brk @as(gid_t, @intCast(gid_value.toInt32())); + break :brk wrapTo(gid_t, try JSC.Node.validators.validateInteger(ctx, gid_value, "gid", .{}, -1, std.math.maxInt(u32))); }; return Chown{ .path = path, .uid = uid, .gid = gid }; @@ -1544,10 +1532,9 @@ pub const Arguments = struct { pub fn toThreadSafe(_: *const @This()) void {} pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!Fchown { - const fd = try JSC.Node.fileDescriptorFromJS(ctx, arguments.next() orelse { - return ctx.throwInvalidArguments("file descriptor is required", .{}); - }) orelse { - return ctx.throwInvalidArguments("file descriptor must be a number", .{}); + const fd_value = arguments.nextEat() orelse JSC.JSValue.undefined; + const fd = try JSC.Node.fileDescriptorFromJS(ctx, fd_value) orelse { + return throwInvalidFdError(ctx, fd_value); }; const uid: uid_t = brk: { @@ -1556,22 +1543,26 @@ pub const Arguments = struct { }; arguments.eat(); - break :brk @as(uid_t, @intCast(uid_value.toInt32())); + break :brk wrapTo(uid_t, try JSC.Node.validators.validateInteger(ctx, uid_value, "uid", .{}, -1, std.math.maxInt(u32))); }; const gid: gid_t = brk: { const gid_value = arguments.next() orelse break :brk { return ctx.throwInvalidArguments("gid is required", .{}); }; - arguments.eat(); - break :brk @as(gid_t, @intCast(gid_value.toInt32())); + break :brk wrapTo(gid_t, try JSC.Node.validators.validateInteger(ctx, gid_value, "gid", .{}, -1, std.math.maxInt(u32))); }; return Fchown{ .fd = fd, .uid = uid, .gid = gid }; } }; + fn wrapTo(T: type, in: i64) T { + comptime bun.assert(@typeInfo(T).Int.signedness == .unsigned); + return @intCast(@mod(in, std.math.maxInt(T))); + } + pub const LChown = Chown; pub const Lutimes = struct { @@ -1613,7 +1604,7 @@ pub const Arguments = struct { arguments.eat(); - return Lutimes{ .path = path, .atime = atime, .mtime = mtime }; + return .{ .path = path, .atime = atime, .mtime = mtime }; } }; @@ -1639,10 +1630,9 @@ pub const Arguments = struct { }; errdefer path.deinit(); - const mode: Mode = try JSC.Node.modeFromJS(ctx, arguments.next() orelse { - return ctx.throwInvalidArguments("mode is required", .{}); - }) orelse { - return ctx.throwInvalidArguments("mode must be a string or integer", .{}); + const mode_arg = arguments.next() orelse .undefined; + const mode: Mode = try JSC.Node.modeFromJS(ctx, mode_arg) orelse { + return JSC.Node.validators.throwErrInvalidArgType(ctx, "mode", .{}, "number", mode_arg); }; arguments.eat(); @@ -1660,18 +1650,14 @@ pub const Arguments = struct { pub fn toThreadSafe(_: *const @This()) void {} pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!FChmod { - const fd = try JSC.Node.fileDescriptorFromJS(ctx, arguments.next() orelse { - return ctx.throwInvalidArguments("file descriptor is required", .{}); - }) orelse { - return ctx.throwInvalidArguments("file descriptor must be a number", .{}); + const fd_value = arguments.nextEat() orelse JSC.JSValue.undefined; + const fd = try JSC.Node.fileDescriptorFromJS(ctx, fd_value) orelse { + return throwInvalidFdError(ctx, fd_value); }; - arguments.eat(); - - const mode: Mode = try JSC.Node.modeFromJS(ctx, arguments.next() orelse { - return ctx.throwInvalidArguments("mode is required", .{}); - }) orelse { - return ctx.throwInvalidArguments("mode must be a string or integer", .{}); + const mode_arg = arguments.next() orelse .undefined; + const mode: Mode = try JSC.Node.modeFromJS(ctx, mode_arg) orelse { + return JSC.Node.validators.throwErrInvalidArgType(ctx, "mode", .{}, "number", mode_arg); }; arguments.eat(); @@ -1738,10 +1724,9 @@ pub const Arguments = struct { pub fn toThreadSafe(_: *@This()) void {} pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!Fstat { - const fd = try JSC.Node.fileDescriptorFromJS(ctx, arguments.next() orelse { - return ctx.throwInvalidArguments("file descriptor is required", .{}); - }) orelse { - return ctx.throwInvalidArguments("file descriptor must be a number", .{}); + const fd_value = arguments.nextEat() orelse JSC.JSValue.undefined; + const fd = try JSC.Node.fileDescriptorFromJS(ctx, fd_value) orelse { + return throwInvalidFdError(ctx, fd_value); }; const big_int = brk: { @@ -2095,6 +2080,9 @@ pub const Arguments = struct { mode = try JSC.Node.modeFromJS(ctx, mode_) orelse mode; } } + if (val.isNumber() or val.isString()) { + mode = try JSC.Node.modeFromJS(ctx, val) orelse mode; + } } return Mkdir{ @@ -2235,10 +2223,9 @@ pub const Arguments = struct { pub fn toThreadSafe(_: Close) void {} pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!Close { - const fd = try JSC.Node.fileDescriptorFromJS(ctx, arguments.next() orelse { - return ctx.throwInvalidArguments("file descriptor is required", .{}); - }) orelse { - return ctx.throwInvalidArguments("file descriptor must be a number", .{}); + const fd_value = arguments.nextEat() orelse JSC.JSValue.undefined; + const fd = try JSC.Node.fileDescriptorFromJS(ctx, fd_value) orelse { + return throwInvalidFdError(ctx, fd_value); }; return Close{ .fd = fd }; @@ -2323,12 +2310,10 @@ pub const Arguments = struct { } pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!Futimes { - const fd = try JSC.Node.fileDescriptorFromJS(ctx, arguments.next() orelse { - return ctx.throwInvalidArguments("file descriptor is required", .{}); - }) orelse { - return ctx.throwInvalidArguments("file descriptor must be a number", .{}); + const fd_value = arguments.nextEat() orelse JSC.JSValue.undefined; + const fd = try JSC.Node.fileDescriptorFromJS(ctx, fd_value) orelse { + return throwInvalidFdError(ctx, fd_value); }; - arguments.eat(); const atime = JSC.Node.timeLikeFromJS(ctx, arguments.next() orelse { return ctx.throwInvalidArguments("atime is required", .{}); @@ -2375,7 +2360,6 @@ pub const Arguments = struct { /// The kernel ignores the position argument and always appends the data to /// the end of the file. /// @since v0.0.2 - /// pub const Write = struct { fd: FileDescriptor, buffer: StringOrBuffer, @@ -2398,12 +2382,10 @@ pub const Arguments = struct { } pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!Write { - const fd = try JSC.Node.fileDescriptorFromJS(ctx, arguments.next() orelse { - return ctx.throwInvalidArguments("file descriptor is required", .{}); - }) orelse { - return ctx.throwInvalidArguments("file descriptor must be a number", .{}); + const fd_value = arguments.nextEat() orelse JSC.JSValue.undefined; + const fd = try JSC.Node.fileDescriptorFromJS(ctx, fd_value) orelse { + return throwInvalidFdError(ctx, fd_value); }; - arguments.eat(); const buffer_value = arguments.next(); const buffer = StringOrBuffer.fromJS(ctx, bun.default_allocator, buffer_value orelse { @@ -2427,45 +2409,59 @@ pub const Arguments = struct { arguments.eat(); - // TODO: make this faster by passing argument count at comptime - if (arguments.next()) |current_| { - parse: { - var current = current_; - switch (buffer) { - // fs.write(fd, string[, position[, encoding]], callback) - else => { - if (current.isNumber()) { - args.position = current.to(i52); - arguments.eat(); - current = arguments.next() orelse break :parse; - } - - if (current.isString()) { - args.encoding = try Encoding.assert(current, ctx, args.encoding); - arguments.eat(); - } - }, - // fs.write(fd, buffer[, offset[, length[, position]]], callback) - .buffer => { - if (!current.isNumber()) { - break :parse; - } - - if (!(current.isNumber() or current.isBigInt())) break :parse; - args.offset = current.to(u52); - arguments.eat(); - current = arguments.next() orelse break :parse; - - if (!(current.isNumber() or current.isBigInt())) break :parse; - args.length = current.to(u52); - arguments.eat(); - current = arguments.next() orelse break :parse; - - if (!(current.isNumber() or current.isBigInt())) break :parse; + parse: { + var current = arguments.next() orelse break :parse; + switch (buffer) { + // fs.write(fd, string[, position[, encoding]], callback) + else => { + if (current.isNumber()) { args.position = current.to(i52); arguments.eat(); - }, - } + current = arguments.next() orelse break :parse; + } + + if (current.isString()) { + args.encoding = try Encoding.assert(current, ctx, args.encoding); + arguments.eat(); + } + }, + // fs.write(fd, buffer[, offset[, length[, position]]], callback) + .buffer => { + 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; + if (args.offset > buf_len) { + return ctx.throwRangeError( + @as(f64, @floatFromInt(args.offset)), + .{ .field_name = "offset", .max = @intCast(@min(buf_len, std.math.maxInt(i64))) }, + ); + } + if (length > buf_len - args.offset) { + return ctx.throwRangeError( + @as(f64, @floatFromInt(length)), + .{ .field_name = "length", .max = @intCast(@min(buf_len - args.offset, std.math.maxInt(i64))) }, + ); + } + if (length < 0) { + return ctx.throwRangeError( + @as(f64, @floatFromInt(length)), + .{ .field_name = "length", .min = 0 }, + ); + } + 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(); + }, } } @@ -2491,12 +2487,10 @@ pub const Arguments = struct { } pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!Read { - const fd = try JSC.Node.fileDescriptorFromJS(ctx, arguments.next() orelse { - return ctx.throwInvalidArguments("file descriptor is required", .{}); - }) orelse { - return ctx.throwInvalidArguments("file descriptor must be a number", .{}); + const fd_value = arguments.nextEat() orelse JSC.JSValue.undefined; + const fd = try JSC.Node.fileDescriptorFromJS(ctx, fd_value) orelse { + return throwInvalidFdError(ctx, fd_value); }; - arguments.eat(); const buffer_value = arguments.next(); const buffer = Buffer.fromJS(ctx, buffer_value orelse { @@ -2532,7 +2526,9 @@ pub const Arguments = struct { if (arguments.next()) |arg_position| { arguments.eat(); if (arg_position.isNumber() or arg_position.isBigInt()) { - args.position = @as(ReadPosition, @intCast(arg_position.to(i52))); + const num = arg_position.to(i52); + if (num >= 0) + args.position = @as(ReadPosition, @intCast(num)); } } } else if (current.isObject()) { @@ -2551,15 +2547,26 @@ pub const Arguments = struct { if (try current.getTruthy(ctx, "position")) |num| { if (num.isNumber() or num.isBigInt()) { - args.position = num.to(i52); + const n = num.to(i52); + if (n >= 0) + args.position = num.to(i52); } } } } - if (defined_length and args.length > 0 and buffer.slice().len == 0) { - var formatter = bun.JSC.ConsoleObject.Formatter{ .globalThis = ctx }; - return ctx.ERR_INVALID_ARG_VALUE("The argument 'buffer' is empty and cannot be written. Received {}", .{buffer_value.?.toFmt(&formatter)}).throw(); + if (defined_length and args.length > 0) { + const buf_length = buffer.slice().len; + if (buf_length == 0) { + var formatter = bun.JSC.ConsoleObject.Formatter{ .globalThis = ctx }; + return ctx.ERR_INVALID_ARG_VALUE("The argument 'buffer' is empty and cannot be written. Received {}", .{buffer_value.?.toFmt(&formatter)}).throw(); + } + if (args.length > buf_length) { + return ctx.throwRangeError( + @as(f64, @floatFromInt(args.length)), + .{ .field_name = "length", .max = @intCast(@min(buf_length, std.math.maxInt(i64))) }, + ); + } } return args; @@ -2686,15 +2693,13 @@ pub const Arguments = struct { } if (try arg.getTruthy(ctx, "mode")) |mode_| { - mode = try JSC.Node.modeFromJS(ctx, mode_) orelse { - return ctx.throwInvalidArguments("Invalid mode", .{}); - }; + mode = try JSC.Node.modeFromJS(ctx, mode_) orelse mode; } } } const data = try StringOrBuffer.fromJSWithEncodingMaybeAsync(ctx, bun.default_allocator, data_value, encoding, arguments.will_be_async) orelse { - return ctx.throwInvalidArguments("data must be a string or TypedArray", .{}); + return ctx.ERR_INVALID_ARG_TYPE("The \"data\" argument must be of type string or an instance of Buffer, TypedArray, or DataView", .{}).throw(); }; // Note: Signal is not implemented @@ -2811,11 +2816,7 @@ pub const Arguments = struct { if (arguments.next()) |arg| { arguments.eat(); - if (arg.isString()) { - mode = try FileSystemFlags.fromJS(ctx, arg) orelse { - return ctx.throwInvalidArguments("Invalid mode", .{}); - }; - } + mode = try FileSystemFlags.fromJSNumberOnly(ctx, arg, .access); } return Access{ @@ -2834,12 +2835,10 @@ pub const Arguments = struct { } pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!FdataSync { - const fd = try JSC.Node.fileDescriptorFromJS(ctx, arguments.next() orelse { - return ctx.throwInvalidArguments("file descriptor is required", .{}); - }) orelse { - return ctx.throwInvalidArguments("file descriptor must be a number", .{}); + const fd_value = arguments.nextEat() orelse JSC.JSValue.undefined; + const fd = try JSC.Node.fileDescriptorFromJS(ctx, fd_value) orelse { + return throwInvalidFdError(ctx, fd_value); }; - arguments.eat(); return FdataSync{ .fd = fd }; } @@ -2876,12 +2875,10 @@ pub const Arguments = struct { }; errdefer dest.deinit(); - var mode: i32 = 0; + var mode: Mode = 0; if (arguments.next()) |arg| { arguments.eat(); - if (arg.isNumber()) { - mode = arg.coerce(i32, ctx); - } + mode = @intFromEnum(try FileSystemFlags.fromJSNumberOnly(ctx, arg, .copy_file)); } return CopyFile{ @@ -2988,12 +2985,10 @@ pub const Arguments = struct { pub fn toThreadSafe(_: *const @This()) void {} pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!Fsync { - const fd = try JSC.Node.fileDescriptorFromJS(ctx, arguments.next() orelse { - return ctx.throwInvalidArguments("file descriptor is required", .{}); - }) orelse { - return ctx.throwInvalidArguments("file descriptor must be a number", .{}); + const fd_value = arguments.nextEat() orelse JSC.JSValue.undefined; + const fd = try JSC.Node.fileDescriptorFromJS(ctx, fd_value) orelse { + return throwInvalidFdError(ctx, fd_value); }; - arguments.eat(); return Fsync{ .fd = fd }; } @@ -3031,8 +3026,15 @@ pub const StringOrUndefined = union(enum) { } }; +/// For use in `Return`'s definitions to act as `void` while returning `null` to JavaScript +const Null = struct { + pub fn toJS(_: @This(), _: *JSC.JSGlobalObject) JSC.JSValue { + return .null; + } +}; + const Return = struct { - pub const Access = void; + pub const Access = Null; pub const AppendFile = void; pub const Close = void; pub const CopyFile = void; @@ -3181,19 +3183,21 @@ pub const NodeFS = struct { /// /// We want to avoid allocating a new path buffer for every error message so that JSC can clone + GC it. /// That means a stack-allocated buffer won't suffice. Instead, we re-use - /// the heap allocated buffer on the NodefS struct + /// the heap allocated buffer on the NodeFS struct sync_error_buf: bun.PathBuffer = undefined, vm: ?*JSC.VirtualMachine = null, pub const ReturnType = Return; - pub fn access(this: *NodeFS, args: Arguments.Access, comptime _: Flavor) Maybe(Return.Access) { + pub fn access(this: *NodeFS, args: Arguments.Access, _: Flavor) Maybe(Return.Access) { const path = args.path.sliceZ(&this.sync_error_buf); - return Syscall.access(path, @intFromEnum(args.mode)); + return switch (Syscall.access(path, @intFromEnum(args.mode))) { + .err => |err| .{ .err = err }, + .result => .{ .result = .{} }, + }; } - pub fn appendFile(this: *NodeFS, args: Arguments.AppendFile, comptime flavor: Flavor) Maybe(Return.AppendFile) { - _ = flavor; + pub fn appendFile(this: *NodeFS, args: Arguments.AppendFile, _: Flavor) Maybe(Return.AppendFile) { var data = args.data.slice(); switch (args.file) { @@ -3233,8 +3237,7 @@ pub const NodeFS = struct { } } - pub fn close(_: *NodeFS, args: Arguments.Close, comptime flavor: Flavor) Maybe(Return.Close) { - _ = flavor; + pub fn close(_: *NodeFS, args: Arguments.Close, _: Flavor) Maybe(Return.Close) { return if (Syscall.close(args.fd)) |err| .{ .err = err } else Maybe(Return.Close).success; } @@ -3343,12 +3346,23 @@ pub const NodeFS = struct { return Maybe(Return.CopyFile).success; } + pub fn copyFile(this: *NodeFS, args: Arguments.CopyFile, _: Flavor) Maybe(Return.CopyFile) { + return switch (this.copyFileInner(args)) { + .result => .{ .result = {} }, + .err => |err| .{ .err = .{ + .errno = err.errno, + .syscall = .copyfile, + .path = args.src.slice(), + .dest = args.dest.slice(), + } }, + }; + } + /// https://github.com/libuv/libuv/pull/2233 /// https://github.com/pnpm/pnpm/issues/2761 /// https://github.com/libuv/libuv/pull/2578 /// https://github.com/nodejs/node/issues/34624 - pub fn copyFile(this: *NodeFS, args: Arguments.CopyFile, comptime flavor: Flavor) Maybe(Return.CopyFile) { - _ = flavor; + fn copyFileInner(this: *NodeFS, args: Arguments.CopyFile) Maybe(Return.CopyFile) { const ret = Maybe(Return.CopyFile); // TODO: do we need to fchown? @@ -3361,7 +3375,7 @@ pub const NodeFS = struct { if (args.mode.isForceClone()) { // https://www.manpagez.com/man/2/clonefile/ - return ret.errnoSysP(C.clonefile(src, dest, 0), .clonefile, src) orelse ret.success; + return ret.errnoSysP(C.clonefile(src, dest, 0), .copyfile, src) orelse ret.success; } else { const stat_ = switch (Syscall.stat(src)) { .result => |result| result, @@ -3383,7 +3397,7 @@ pub const NodeFS = struct { _ = Syscall.unlink(dest); } - if (ret.errnoSysP(C.clonefile(src, dest, 0), .clonefile, src) == null) { + if (ret.errnoSysP(C.clonefile(src, dest, 0), .copyfile, src) == null) { _ = C.chmod(dest, stat_.mode); return ret.success; } @@ -3553,10 +3567,6 @@ pub const NodeFS = struct { } if (comptime Environment.isWindows) { - if (args.mode.isForceClone()) { - return Maybe(Return.CopyFile).todo(); - } - const src_buf = bun.OSPathBufferPool.get(); defer bun.OSPathBufferPool.put(src_buf); const dest_buf = bun.OSPathBufferPool.get(); @@ -3575,10 +3585,8 @@ pub const NodeFS = struct { return Maybe(Return.CopyFile).todo(); } - pub fn exists(this: *NodeFS, args: Arguments.Exists, comptime flavor: Flavor) Maybe(Return.Exists) { - _ = flavor; - const Ret = Maybe(Return.Exists); - const path = args.path orelse return Ret{ .result = false }; + pub fn exists(this: *NodeFS, args: Arguments.Exists, _: Flavor) Maybe(Return.Exists) { + const path = args.path orelse return .{ .result = false }; const slice = path.sliceZ(&this.sync_error_buf); // Use libuv access on windows @@ -3591,11 +3599,10 @@ pub const NodeFS = struct { // hidden from the client, which checks permissions. Similar // problems can occur to FUSE mounts. const rc = (system.access(slice, std.posix.F_OK)); - return Ret{ .result = rc == 0 }; + return .{ .result = rc == 0 }; } - pub fn chown(this: *NodeFS, args: Arguments.Chown, comptime flavor: Flavor) Maybe(Return.Chown) { - _ = flavor; + pub fn chown(this: *NodeFS, args: Arguments.Chown, _: Flavor) Maybe(Return.Chown) { if (comptime Environment.isWindows) { return Syscall.chown(args.path.sliceZ(&this.sync_error_buf), args.uid, args.gid); } @@ -3605,27 +3612,22 @@ pub const NodeFS = struct { return Syscall.chown(path, args.uid, args.gid); } - /// This should almost never be async - pub fn chmod(this: *NodeFS, args: Arguments.Chmod, comptime flavor: Flavor) Maybe(Return.Chmod) { - _ = flavor; - if (comptime Environment.isWindows) { - return Syscall.chmod(args.path.sliceZ(&this.sync_error_buf), args.mode); - } - + pub fn chmod(this: *NodeFS, args: Arguments.Chmod, _: Flavor) Maybe(Return.Chmod) { const path = args.path.sliceZ(&this.sync_error_buf); + if (comptime Environment.isWindows) { + return Syscall.chmod(path, args.mode); + } + return Maybe(Return.Chmod).errnoSysP(C.chmod(path, args.mode), .chmod, path) orelse Maybe(Return.Chmod).success; } - /// This should almost never be async - pub fn fchmod(_: *NodeFS, args: Arguments.FChmod, comptime flavor: Flavor) Maybe(Return.Fchmod) { - _ = flavor; + pub fn fchmod(_: *NodeFS, args: Arguments.FChmod, _: Flavor) Maybe(Return.Fchmod) { return Syscall.fchmod(args.fd, args.mode); } - pub fn fchown(_: *NodeFS, args: Arguments.Fchown, comptime flavor: Flavor) Maybe(Return.Fchown) { - _ = flavor; + pub fn fchown(_: *NodeFS, args: Arguments.Fchown, _: Flavor) Maybe(Return.Fchown) { if (comptime Environment.isWindows) { return Syscall.fchown(args.fd, args.uid, args.gid); } @@ -3634,21 +3636,21 @@ pub const NodeFS = struct { Maybe(Return.Fchown).success; } - pub fn fdatasync(_: *NodeFS, args: Arguments.FdataSync, comptime _: Flavor) Maybe(Return.Fdatasync) { + pub fn fdatasync(_: *NodeFS, args: Arguments.FdataSync, _: Flavor) Maybe(Return.Fdatasync) { if (Environment.isWindows) { return Syscall.fdatasync(args.fd); } return Maybe(Return.Fdatasync).errnoSysFd(system.fdatasync(args.fd.int()), .fdatasync, args.fd) orelse Maybe(Return.Fdatasync).success; } - pub fn fstat(_: *NodeFS, args: Arguments.Fstat, comptime _: Flavor) Maybe(Return.Fstat) { + pub fn fstat(_: *NodeFS, args: Arguments.Fstat, _: Flavor) Maybe(Return.Fstat) { return switch (Syscall.fstat(args.fd)) { - .result => |result| Maybe(Return.Fstat){ .result = Stats.init(result, false) }, - .err => |err| Maybe(Return.Fstat){ .err = err }, + .result => |result| .{ .result = Stats.init(result, false) }, + .err => |err| .{ .err = err }, }; } - pub fn fsync(_: *NodeFS, args: Arguments.Fsync, comptime _: Flavor) Maybe(Return.Fsync) { + pub fn fsync(_: *NodeFS, args: Arguments.Fsync, _: Flavor) Maybe(Return.Fsync) { if (Environment.isWindows) { return Syscall.fsync(args.fd); } @@ -3656,22 +3658,21 @@ pub const NodeFS = struct { Maybe(Return.Fsync).success; } - pub fn ftruncateSync(args: Arguments.FTruncate) Maybe(Return.Ftruncate) { + pub fn ftruncate(_: *NodeFS, args: Arguments.FTruncate, _: Flavor) Maybe(Return.Ftruncate) { return Syscall.ftruncate(args.fd, args.len orelse 0); } - pub fn ftruncate(_: *NodeFS, args: Arguments.FTruncate, comptime flavor: Flavor) Maybe(Return.Ftruncate) { - _ = flavor; - return ftruncateSync(args); - } - - pub fn futimes(_: *NodeFS, args: Arguments.Futimes, comptime _: Flavor) Maybe(Return.Futimes) { + pub fn futimes(_: *NodeFS, args: Arguments.Futimes, _: Flavor) Maybe(Return.Futimes) { if (comptime Environment.isWindows) { var req: uv.fs_t = uv.fs_t.uninitialized; defer req.deinit(); const rc = uv.uv_fs_futime(uv.Loop.get(), &req, bun.uvfdcast(args.fd), args.mtime, args.atime, null); return if (rc.errno()) |e| - Maybe(Return.Futimes){ .err = .{ .errno = e, .syscall = .futime } } + .{ .err = .{ + .errno = e, + .syscall = .futime, + .fd = args.fd, + } } else Maybe(Return.Futimes).success; } @@ -3681,26 +3682,23 @@ pub const NodeFS = struct { args.atime, }; - return if (Maybe(Return.Futimes).errnoSys(system.futimens(args.fd.int(), ×), .futimens)) |err| + return if (Maybe(Return.Futimes).errnoSysFd(system.futimens(args.fd.int(), ×), .futime, args.fd)) |err| err else Maybe(Return.Futimes).success; } - pub fn lchmod(this: *NodeFS, args: Arguments.LCHmod, comptime flavor: Flavor) Maybe(Return.Lchmod) { - _ = flavor; + pub fn lchmod(this: *NodeFS, args: Arguments.LCHmod, _: Flavor) Maybe(Return.Lchmod) { if (comptime Environment.isWindows) { return Maybe(Return.Lchmod).todo(); } const path = args.path.sliceZ(&this.sync_error_buf); - return Maybe(Return.Lchmod).errnoSysP(C.lchmod(path, args.mode), .lchmod, path) orelse Maybe(Return.Lchmod).success; } - pub fn lchown(this: *NodeFS, args: Arguments.LChown, comptime flavor: Flavor) Maybe(Return.Lchown) { - _ = flavor; + pub fn lchown(this: *NodeFS, args: Arguments.LChown, _: Flavor) Maybe(Return.Lchown) { if (comptime Environment.isWindows) { return Maybe(Return.Lchown).todo(); } @@ -3711,25 +3709,21 @@ pub const NodeFS = struct { Maybe(Return.Lchown).success; } - pub fn link(this: *NodeFS, args: Arguments.Link, comptime _: Flavor) Maybe(Return.Link) { - var new_path_buf: bun.PathBuffer = undefined; + pub fn link(this: *NodeFS, args: Arguments.Link, _: Flavor) Maybe(Return.Link) { + var to_buf: bun.PathBuffer = undefined; const from = args.old_path.sliceZ(&this.sync_error_buf); - const to = args.new_path.sliceZ(&new_path_buf); + const to = args.new_path.sliceZ(&to_buf); if (Environment.isWindows) { return Syscall.link(from, to); } - return Maybe(Return.Link).errnoSysP(system.link(from, to, 0), .link, from) orelse + return Maybe(Return.Link).errnoSysPD(system.link(from, to, 0), .link, args.old_path.slice(), args.new_path.slice()) orelse Maybe(Return.Link).success; } - pub fn lstat(this: *NodeFS, args: Arguments.Lstat, comptime _: Flavor) Maybe(Return.Lstat) { - return switch (Syscall.lstat( - args.path.sliceZ( - &this.sync_error_buf, - ), - )) { + pub fn lstat(this: *NodeFS, args: Arguments.Lstat, _: Flavor) Maybe(Return.Lstat) { + return switch (Syscall.lstat(args.path.sliceZ(&this.sync_error_buf))) { .result => |result| Maybe(Return.Lstat){ .result = .{ .stats = Stats.init(result, args.big_int) } }, .err => |err| brk: { if (!args.throw_if_no_entry and err.getErrno() == .NOENT) { @@ -3740,14 +3734,12 @@ pub const NodeFS = struct { }; } - pub fn mkdir(this: *NodeFS, args: Arguments.Mkdir, comptime flavor: Flavor) Maybe(Return.Mkdir) { - return if (args.recursive) mkdirRecursive(this, args, flavor) else mkdirNonRecursive(this, args, flavor); + pub fn mkdir(this: *NodeFS, args: Arguments.Mkdir, _: Flavor) Maybe(Return.Mkdir) { + return if (args.recursive) mkdirRecursive(this, args) else mkdirNonRecursive(this, args); } // Node doesn't absolute the path so we don't have to either - pub fn mkdirNonRecursive(this: *NodeFS, args: Arguments.Mkdir, comptime flavor: Flavor) Maybe(Return.Mkdir) { - _ = flavor; - + pub fn mkdirNonRecursive(this: *NodeFS, args: Arguments.Mkdir) Maybe(Return.Mkdir) { const path = args.path.sliceZ(&this.sync_error_buf); return switch (Syscall.mkdir(path, args.mode)) { .result => Maybe(Return.Mkdir){ .result = .{ .none = {} } }, @@ -3755,19 +3747,12 @@ pub const NodeFS = struct { }; } - pub const MkdirDummyVTable = struct { - pub fn onCreateDir(_: @This(), _: bun.OSPathSliceZ) void { - return; - } - }; - - pub fn mkdirRecursive(this: *NodeFS, args: Arguments.Mkdir, comptime flavor: Flavor) Maybe(Return.Mkdir) { - return mkdirRecursiveImpl(this, args, flavor, MkdirDummyVTable, .{}); + pub fn mkdirRecursive(this: *NodeFS, args: Arguments.Mkdir) Maybe(Return.Mkdir) { + return mkdirRecursiveImpl(this, args, void, {}); } // TODO: verify this works correctly with unicode codepoints - pub fn mkdirRecursiveImpl(this: *NodeFS, args: Arguments.Mkdir, comptime flavor: Flavor, comptime Ctx: type, ctx: Ctx) Maybe(Return.Mkdir) { - _ = flavor; + pub fn mkdirRecursiveImpl(this: *NodeFS, args: Arguments.Mkdir, comptime Ctx: type, ctx: Ctx) Maybe(Return.Mkdir) { const buf = bun.OSPathBufferPool.get(); defer bun.OSPathBufferPool.put(buf); const path: bun.OSPathSliceZ = if (Environment.isWindows) @@ -3789,7 +3774,7 @@ pub const NodeFS = struct { } pub fn mkdirRecursiveOSPath(this: *NodeFS, path: bun.OSPathSliceZ, mode: Mode, comptime return_path: bool) Maybe(Return.Mkdir) { - return mkdirRecursiveOSPathImpl(this, MkdirDummyVTable, .{}, path, mode, return_path); + return mkdirRecursiveOSPathImpl(this, void, {}, path, mode, return_path); } pub fn mkdirRecursiveOSPathImpl( @@ -3800,7 +3785,7 @@ pub const NodeFS = struct { mode: Mode, comptime return_path: bool, ) Maybe(Return.Mkdir) { - const VTable = struct { + const callbacks = struct { pub fn onCreateDir(c: Ctx, dirpath: bun.OSPathSliceZ) void { if (Ctx != void) { c.onCreateDir(dirpath); @@ -3820,8 +3805,21 @@ pub const NodeFS = struct { else => { return .{ .err = err.withPath(this.osPathIntoSyncErrorBuf(path[0..len])) }; }, - .EXIST => { - return .{ .result = .{ .none = {} } }; + .EXIST => return switch (bun.sys.directoryExistsAt(bun.invalid_fd, path)) { + .err => .{ .err = .{ + .errno = err.errno, + .syscall = .mkdir, + .path = this.osPathIntoSyncErrorBuf(path[0..len]), + } }, + // if is a directory, OK. otherwise failure + .result => |result| if (result) + .{ .result = .{ .none = {} } } + else + .{ .err = .{ + .errno = err.errno, + .syscall = .mkdir, + .path = this.osPathIntoSyncErrorBuf(path[0..len]), + } }, }, // continue .NOENT => { @@ -3833,7 +3831,7 @@ pub const NodeFS = struct { } }, .result => { - VTable.onCreateDir(ctx, path); + callbacks.onCreateDir(ctx, path); if (!return_path) { return .{ .result = .{ .none = {} } }; } @@ -3875,7 +3873,7 @@ pub const NodeFS = struct { } }, .result => { - VTable.onCreateDir(ctx, parent); + callbacks.onCreateDir(ctx, parent); // We found a parent that worked working_mem[i] = std.fs.path.sep; break; @@ -3906,7 +3904,7 @@ pub const NodeFS = struct { }, .result => { - VTable.onCreateDir(ctx, parent); + callbacks.onCreateDir(ctx, parent); working_mem[i] = std.fs.path.sep; }, } @@ -3932,7 +3930,7 @@ pub const NodeFS = struct { .result => {}, } - VTable.onCreateDir(ctx, working_mem[0..len :0]); + callbacks.onCreateDir(ctx, working_mem[0..len :0]); if (!return_path) { return .{ .result = .{ .none = {} } }; } @@ -3960,7 +3958,11 @@ pub const NodeFS = struct { defer req.deinit(); const rc = uv.uv_fs_mkdtemp(bun.Async.Loop.get(), &req, @ptrCast(prefix_buf.ptr), null); if (rc.errno()) |errno| { - return .{ .err = .{ .errno = errno, .syscall = .mkdtemp, .path = prefix_buf[0 .. len + 6] } }; + return .{ .err = .{ + .errno = errno, + .syscall = .mkdtemp, + .path = prefix_buf[0 .. len + 6], + } }; } return .{ .result = JSC.ZigString.dupeForJS(bun.sliceTo(req.path, 0), bun.default_allocator) catch bun.outOfMemory(), @@ -3980,6 +3982,7 @@ pub const NodeFS = struct { .err = Syscall.Error{ .errno = @as(Syscall.Error.Int, @truncate(@intFromEnum(errno))), .syscall = .mkdtemp, + .path = prefix_buf[0 .. len + 6], }, }; } @@ -4017,58 +4020,47 @@ pub const NodeFS = struct { return Maybe(Return.OpenDir).todo(); } - fn _read(_: *NodeFS, args: Arguments.Read, comptime _: Flavor) Maybe(Return.Read) { + fn readInner(_: *NodeFS, args: Arguments.Read) Maybe(Return.Read) { if (Environment.allow_assert) bun.assert(args.position == null); var buf = args.buffer.slice(); buf = buf[@min(args.offset, buf.len)..]; buf = buf[0..@min(buf.len, args.length)]; return switch (Syscall.read(args.fd, buf)) { - .err => |err| .{ - .err = err, - }, - .result => |amt| .{ - .result = .{ - .bytes_read = @as(u52, @truncate(amt)), - }, - }, + .err => |err| .{ .err = err }, + .result => |amt| .{ .result = .{ + .bytes_read = @as(u52, @truncate(amt)), + } }, }; } - fn _pread(_: *NodeFS, args: Arguments.Read, comptime flavor: Flavor) Maybe(Return.Read) { - _ = flavor; + fn preadInner(_: *NodeFS, args: Arguments.Read) Maybe(Return.Read) { var buf = args.buffer.slice(); buf = buf[@min(args.offset, buf.len)..]; buf = buf[0..@min(buf.len, args.length)]; return switch (Syscall.pread(args.fd, buf, args.position.?)) { - .err => |err| .{ - .err = err, - }, - .result => |amt| .{ - .result = .{ - .bytes_read = @as(u52, @truncate(amt)), - }, - }, + .err => |err| .{ .err = .{ + .errno = err.errno, + .fd = args.fd, + .syscall = .read, + } }, + .result => |amt| .{ .result = .{ + .bytes_read = @as(u52, @truncate(amt)), + } }, }; } - pub fn read(this: *NodeFS, args: Arguments.Read, comptime flavor: Flavor) Maybe(Return.Read) { + pub fn read(this: *NodeFS, args: Arguments.Read, _: Flavor) Maybe(Return.Read) { const len1 = args.buffer.slice().len; const len2 = args.length; if (len1 == 0 or len2 == 0) { return Maybe(Return.Read).initResult(.{ .bytes_read = 0 }); } return if (args.position != null) - this._pread( - args, - comptime flavor, - ) + this.preadInner(args) else - this._read( - args, - comptime flavor, - ); + this.readInner(args); } pub fn uv_read(this: *NodeFS, args: Arguments.Read, rc: i64) Maybe(Return.Read) { @@ -4097,16 +4089,16 @@ pub const NodeFS = struct { return Maybe(Return.Readv).initResult(.{ .bytes_read = @intCast(rc) }); } - pub fn readv(this: *NodeFS, args: Arguments.Readv, comptime flavor: Flavor) Maybe(Return.Readv) { - return if (args.position != null) _preadv(this, args, flavor) else _readv(this, args, flavor); + pub fn readv(this: *NodeFS, args: Arguments.Readv, _: Flavor) Maybe(Return.Readv) { + return if (args.position != null) preadvInner(this, args) else readvInner(this, args); } - pub fn writev(this: *NodeFS, args: Arguments.Writev, comptime flavor: Flavor) Maybe(Return.Writev) { - return if (args.position != null) _pwritev(this, args, flavor) else _writev(this, args, flavor); + pub fn writev(this: *NodeFS, args: Arguments.Writev, _: Flavor) Maybe(Return.Writev) { + return if (args.position != null) pwritevInner(this, args) else writevInner(this, args); } - pub fn write(this: *NodeFS, args: Arguments.Write, comptime flavor: Flavor) Maybe(Return.Write) { - return if (args.position != null) _pwrite(this, args, flavor) else _write(this, args, flavor); + pub fn write(this: *NodeFS, args: Arguments.Write, _: Flavor) Maybe(Return.Write) { + return if (args.position != null) pwriteInner(this, args) else writeInner(this, args); } pub fn uv_write(this: *NodeFS, args: Arguments.Write, rc: i64) Maybe(Return.Write) { @@ -4135,9 +4127,7 @@ pub const NodeFS = struct { return Maybe(Return.Writev).initResult(.{ .bytes_written = @intCast(rc) }); } - fn _write(_: *NodeFS, args: Arguments.Write, comptime flavor: Flavor) Maybe(Return.Write) { - _ = flavor; - + fn writeInner(_: *NodeFS, args: Arguments.Write) Maybe(Return.Write) { var buf = args.buffer.slice(); buf = buf[@min(args.offset, buf.len)..]; buf = buf[0..@min(buf.len, args.length)]; @@ -4154,8 +4144,7 @@ pub const NodeFS = struct { }; } - fn _pwrite(_: *NodeFS, args: Arguments.Write, comptime flavor: Flavor) Maybe(Return.Write) { - _ = flavor; + fn pwriteInner(_: *NodeFS, args: Arguments.Write) Maybe(Return.Write) { const position = args.position.?; var buf = args.buffer.slice(); @@ -4163,17 +4152,18 @@ pub const NodeFS = struct { buf = buf[0..@min(args.length, buf.len)]; return switch (Syscall.pwrite(args.fd, buf, position)) { - .err => |err| .{ - .err = err, - }, + .err => |err| .{ .err = .{ + .errno = err.errno, + .fd = args.fd, + .syscall = .write, + } }, .result => |amt| .{ .result = .{ .bytes_written = @as(u52, @truncate(amt)), } }, }; } - fn _preadv(_: *NodeFS, args: Arguments.Readv, comptime flavor: Flavor) Maybe(Return.Readv) { - _ = flavor; + fn preadvInner(_: *NodeFS, args: Arguments.Readv) Maybe(Return.Readv) { const position = args.position.?; return switch (Syscall.preadv(args.fd, args.buffers.buffers.items, position)) { @@ -4186,8 +4176,7 @@ pub const NodeFS = struct { }; } - fn _readv(_: *NodeFS, args: Arguments.Readv, comptime flavor: Flavor) Maybe(Return.Readv) { - _ = flavor; + fn readvInner(_: *NodeFS, args: Arguments.Readv) Maybe(Return.Readv) { return switch (Syscall.readv(args.fd, args.buffers.buffers.items)) { .err => |err| .{ .err = err, @@ -4198,8 +4187,7 @@ pub const NodeFS = struct { }; } - fn _pwritev(_: *NodeFS, args: Arguments.Writev, comptime flavor: Flavor) Maybe(Return.Write) { - _ = flavor; + fn pwritevInner(_: *NodeFS, args: Arguments.Writev) Maybe(Return.Write) { const position = args.position.?; return switch (Syscall.pwritev(args.fd, @ptrCast(args.buffers.buffers.items), position)) { .err => |err| .{ @@ -4211,8 +4199,7 @@ pub const NodeFS = struct { }; } - fn _writev(_: *NodeFS, args: Arguments.Writev, comptime flavor: Flavor) Maybe(Return.Write) { - _ = flavor; + fn writevInner(_: *NodeFS, args: Arguments.Writev) Maybe(Return.Write) { return switch (Syscall.writev(args.fd, @ptrCast(args.buffers.buffers.items))) { .err => |err| .{ .err = err, @@ -4230,13 +4217,21 @@ pub const NodeFS = struct { } } - return switch (args.recursive) { + const maybe = switch (args.recursive) { inline else => |recursive| switch (args.tag()) { - .buffers => _readdir(&this.sync_error_buf, args, Buffer, recursive, flavor), - .with_file_types => _readdir(&this.sync_error_buf, args, Dirent, recursive, flavor), - .files => _readdir(&this.sync_error_buf, args, bun.String, recursive, flavor), + .buffers => readdirInner(&this.sync_error_buf, args, Buffer, recursive, flavor), + .with_file_types => readdirInner(&this.sync_error_buf, args, Dirent, recursive, flavor), + .files => readdirInner(&this.sync_error_buf, args, bun.String, recursive, flavor), }, }; + return switch (maybe) { + .err => |err| .{ .err = .{ + .syscall = .scandir, + .errno = err.errno, + .path = err.path, + } }, + .result => |result| .{ .result = result }, + }; } fn readdirWithEntries( @@ -4628,7 +4623,7 @@ pub const NodeFS = struct { return null; } - fn _readdir( + fn readdirInner( buf: *bun.PathBuffer, args: Arguments.Readdir, comptime ExpectedType: type, @@ -5162,7 +5157,7 @@ pub const NodeFS = struct { if (Environment.isWindows) { _ = std.os.windows.kernel32.SetEndOfFile(fd.cast()); } else { - _ = ftruncateSync(.{ .fd = fd, .len = @as(JSC.WebCore.Blob.SizeType, @truncate(written)) }); + _ = Syscall.ftruncate(fd, @intCast(@as(u63, @truncate(written)))); } } @@ -5202,7 +5197,35 @@ pub const NodeFS = struct { }; } + pub fn realpathNonNative(this: *NodeFS, args: Arguments.Realpath, comptime _: Flavor) Maybe(Return.Realpath) { + // For `fs.realpath`, Node.js uses `lstat`, exposing the native system call under + // `fs.realpath.native`. In Bun, the system call is the default, but the error + // code must be changed to make it seem like it is using lstat (tests expect this) + return switch (this.realpathInner(args)) { + .result => |res| .{ .result = res }, + .err => |err| .{ .err = .{ + .errno = err.errno, + .syscall = .lstat, + .path = args.path.slice(), + } }, + }; + } + pub fn realpath(this: *NodeFS, args: Arguments.Realpath, comptime _: Flavor) Maybe(Return.Realpath) { + // Native realpath needs to force `realpath` as the name + return switch (this.realpathInner(args)) { + .result => |res| .{ .result = res }, + .err => |err| .{ + .err = .{ + .errno = err.errno, + .syscall = .realpath, + .path = args.path.slice(), + }, + }, + }; + } + + pub fn realpathInner(this: *NodeFS, args: Arguments.Realpath) Maybe(Return.Realpath) { if (Environment.isWindows) { var req: uv.fs_t = uv.fs_t.uninitialized; defer req.deinit(); @@ -5287,21 +5310,17 @@ pub const NodeFS = struct { } pub const realpathNative = realpath; - // pub fn realpathNative(this: *NodeFS, args: Arguments.Realpath, comptime flavor: Flavor) Maybe(Return.Realpath) { - // _ = args; - // - // - // return error.NotImplementedYet; - // } - pub fn rename(this: *NodeFS, args: Arguments.Rename, comptime flavor: Flavor) Maybe(Return.Rename) { - _ = flavor; + pub fn rename(this: *NodeFS, args: Arguments.Rename, _: Flavor) Maybe(Return.Rename) { const from_buf = &this.sync_error_buf; var to_buf: bun.PathBuffer = undefined; const from = args.old_path.sliceZ(from_buf); const to = args.new_path.sliceZ(&to_buf); - return Syscall.rename(from, to); + return switch (Syscall.rename(from, to)) { + .result => |result| .{ .result = result }, + .err => |err| .{ .err = err.withPathDest(args.old_path.slice(), args.new_path.slice()) }, + }; } pub fn rmdir(this: *NodeFS, args: Arguments.RmDir, comptime _: Flavor) Maybe(Return.Rmdir) { @@ -5495,13 +5514,16 @@ pub const NodeFS = struct { ); } - return Syscall.symlink( + return switch (Syscall.symlink( args.old_path.sliceZ(&this.sync_error_buf), args.new_path.sliceZ(&to_buf), - ); + )) { + .result => |result| .{ .result = result }, + .err => |err| .{ .err = err.withPathDest(args.old_path.slice(), args.new_path.slice()) }, + }; } - fn _truncate(this: *NodeFS, path: PathLike, len: JSC.WebCore.Blob.SizeType, flags: i32, comptime _: Flavor) Maybe(Return.Truncate) { + fn truncateInner(this: *NodeFS, path: PathLike, len: JSC.WebCore.Blob.SizeType, flags: i32) Maybe(Return.Truncate) { if (comptime Environment.isWindows) { const file = bun.sys.open( path.sliceZ(&this.sync_error_buf), @@ -5518,17 +5540,13 @@ pub const NodeFS = struct { Maybe(Return.Truncate).success; } - pub fn truncate(this: *NodeFS, args: Arguments.Truncate, comptime flavor: Flavor) Maybe(Return.Truncate) { + pub fn truncate(this: *NodeFS, args: Arguments.Truncate, _: Flavor) Maybe(Return.Truncate) { return switch (args.path) { - .fd => |fd| this.ftruncate( - Arguments.FTruncate{ .fd = fd, .len = args.len }, - flavor, - ), - .path => this._truncate( + .fd => |fd| Syscall.ftruncate(fd, args.len), + .path => this.truncateInner( args.path.path, args.len, args.flags, - flavor, ), }; } @@ -5576,7 +5594,7 @@ pub const NodeFS = struct { return if (rc.errno()) |errno| .{ .err = Syscall.Error{ .errno = errno, - .syscall = .utimes, + .syscall = .utime, } } else Maybe(Return.Utimes).success; @@ -5595,7 +5613,7 @@ pub const NodeFS = struct { }, }; - return if (Maybe(Return.Utimes).errnoSysP(std.c.utimes(args.path.sliceZ(&this.sync_error_buf), ×), .utimes, args.path.slice())) |err| + return if (Maybe(Return.Utimes).errnoSysP(std.c.utimes(args.path.sliceZ(&this.sync_error_buf), ×), .utime, args.path.slice())) |err| err else Maybe(Return.Utimes).success; @@ -5616,7 +5634,7 @@ pub const NodeFS = struct { return if (rc.errno()) |errno| .{ .err = Syscall.Error{ .errno = errno, - .syscall = .utimes, + .syscall = .utime, } } else Maybe(Return.Utimes).success; @@ -5635,34 +5653,33 @@ pub const NodeFS = struct { }, }; - return if (Maybe(Return.Lutimes).errnoSysP(C.lutimes(args.path.sliceZ(&this.sync_error_buf), ×), .lutimes, args.path.slice())) |err| + return if (Maybe(Return.Lutimes).errnoSysP(C.lutimes(args.path.sliceZ(&this.sync_error_buf), ×), .lutime, args.path.slice())) |err| err else Maybe(Return.Lutimes).success; } pub fn watch(_: *NodeFS, args: Arguments.Watch, comptime _: Flavor) Maybe(Return.Watch) { - return args.createFSWatcher(); + return switch (args.createFSWatcher()) { + .result => |result| .{ .result = result.js_this }, + .err => |err| .{ .err = .{ + .errno = err.errno, + .syscall = err.syscall, + .path = if (err.path.len > 0) args.path.slice() else "", + } }, + }; } /// This function is `cpSync`, but only if you pass `{ recursive: ..., force: ..., errorOnExist: ..., mode: ... }' /// The other options like `filter` use a JS fallback, see `src/js/internal/fs/cp.ts` - pub fn cp(this: *NodeFS, args: Arguments.Cp, comptime flavor: Flavor) Maybe(Return.Cp) { - comptime bun.assert(flavor == .sync); - - var src_buf: bun.PathBuffer = undefined; - var dest_buf: bun.PathBuffer = undefined; + pub fn cp(this: *NodeFS, args: Arguments.Cp, _: Flavor) Maybe(Return.Cp) { + var src_buf: bun.OSPathBuffer = undefined; + var dest_buf: bun.OSPathBuffer = undefined; const src = args.src.osPath(&src_buf); const dest = args.dest.osPath(&dest_buf); - return this._cpSync( - @as(*bun.OSPathBuffer, @alignCast(@ptrCast(&src_buf))), - @intCast(src.len), - @as(*bun.OSPathBuffer, @alignCast(@ptrCast(&dest_buf))), - @intCast(dest.len), - args, - ); + return this.cpSyncInner(&src_buf, @intCast(src.len), &dest_buf, @intCast(dest.len), args); } pub fn osPathIntoSyncErrorBuf(this: *NodeFS, slice: anytype) []const u8 { @@ -5683,7 +5700,7 @@ pub const NodeFS = struct { } else {} } - fn _cpSync( + fn cpSyncInner( this: *NodeFS, src_buf: *bun.OSPathBuffer, src_dir_len: PathString.PathInt, @@ -5709,7 +5726,7 @@ pub const NodeFS = struct { const r = this._copySingleFileSync( src, dest, - @enumFromInt((if (cp_flags.errorOnExist or !cp_flags.force) Constants.COPYFILE_EXCL else @as(u8, 0))), + @enumFromInt(if (cp_flags.errorOnExist or !cp_flags.force) Constants.COPYFILE_EXCL else @as(u8, 0)), attributes, args, ); @@ -5743,13 +5760,11 @@ pub const NodeFS = struct { } if (!cp_flags.recursive) { - return .{ - .err = .{ - .errno = @intFromEnum(E.ISDIR), - .syscall = .copyfile, - .path = this.osPathIntoSyncErrorBuf(src), - }, - }; + return .{ .err = .{ + .errno = @intFromEnum(E.ISDIR), + .syscall = .copyfile, + .path = this.osPathIntoSyncErrorBuf(src), + } }; } if (comptime Environment.isMac) try_with_clonefile: { @@ -5789,7 +5804,7 @@ pub const NodeFS = struct { defer _ = Syscall.close(fd); switch (this.mkdirRecursiveOSPath(dest, Arguments.Mkdir.DefaultMode, false)) { - .err => |err| return Maybe(Return.Cp){ .err = err }, + .err => |err| return .{ .err = err }, .result => {}, } @@ -5816,7 +5831,7 @@ pub const NodeFS = struct { switch (current.kind) { .directory => { - const r = this._cpSync( + const r = this.cpSyncInner( src_buf, src_dir_len + @as(PathString.PathInt, @intCast(1 + name_slice.len)), dest_buf, @@ -5971,7 +5986,7 @@ pub const NodeFS = struct { const mkdirResult = this.mkdirRecursive(.{ .path = PathLike{ .string = PathString.init(dest[0..len]) }, .recursive = true, - }, .sync); + }); if (mkdirResult == .err) { return Maybe(Return.CopyFile){ .err = mkdirResult.err }; } @@ -6066,7 +6081,7 @@ pub const NodeFS = struct { const mkdirResult = this.mkdirRecursive(.{ .path = PathLike{ .string = PathString.init(dest[0..len]) }, .recursive = true, - }, .sync); + }); if (mkdirResult == .err) { return Maybe(Return.CopyFile){ .err = mkdirResult.err }; } @@ -6228,13 +6243,19 @@ pub const NodeFS = struct { } }; +fn throwInvalidFdError(global: *JSC.JSGlobalObject, value: JSC.JSValue) bun.JSError { + if (value.isNumber()) { + return global.ERR_OUT_OF_RANGE("The value of \"fd\" is out of range. It must be an integer. Received {d}", .{bun.fmt.double(value.asNumber())}).throw(); + } + return JSC.Node.validators.throwErrInvalidArgType(global, "fd", .{}, "number", value); +} + pub export fn Bun__mkdirp(globalThis: *JSC.JSGlobalObject, path: [*:0]const u8) bool { return globalThis.bunVM().nodeFS().mkdirRecursive( Arguments.Mkdir{ .path = PathLike{ .string = PathString.init(bun.span(path)) }, .recursive = true, }, - .sync, ) != .err; } diff --git a/src/bun.js/node/node_fs_binding.zig b/src/bun.js/node/node_fs_binding.zig index cee9ac021f..89e4bea172 100644 --- a/src/bun.js/node/node_fs_binding.zig +++ b/src/bun.js/node/node_fs_binding.zig @@ -14,95 +14,69 @@ const NodeFSFunction = fn (this: *JSC.Node.NodeJSFS, globalObject: *JSC.JSGlobal const NodeFSFunctionEnum = std.meta.DeclEnum(JSC.Node.NodeFS); -fn callSync(comptime FunctionEnum: NodeFSFunctionEnum) NodeFSFunction { - const Function = @field(JSC.Node.NodeFS, @tagName(FunctionEnum)); - const FunctionType = @TypeOf(Function); +/// Returns bindings to call JSC.Node.NodeFS.. +/// Async calls use a thread pool. +fn Bindings(comptime function_name: NodeFSFunctionEnum) type { + const function = @field(JSC.Node.NodeFS, @tagName(function_name)); + const fn_info = @typeInfo(@TypeOf(function)).Fn; + if (fn_info.params.len != 3) { + @compileError("Expected fn(NodeFS, Arguments) Return for NodeFS." ++ @tagName(function_name)); + } + const Arguments = fn_info.params[1].type.?; - const function: std.builtin.Type.Fn = comptime @typeInfo(FunctionType).Fn; - comptime if (function.params.len != 3) @compileError("Expected 3 arguments"); - const Arguments = comptime function.params[1].type.?; - const FormattedName = comptime [1]u8{std.ascii.toUpper(@tagName(FunctionEnum)[0])} ++ @tagName(FunctionEnum)[1..]; - const Result = comptime JSC.Maybe(@field(JSC.Node.NodeFS.ReturnType, FormattedName)); - _ = Result; - - const NodeBindingClosure = struct { - pub fn bind(this: *JSC.Node.NodeJSFS, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { - var arguments = callframe.arguments_old(8); - - var slice = ArgumentsSlice.init(globalObject.bunVM(), arguments.slice()); + return struct { + pub fn runSync(this: *JSC.Node.NodeJSFS, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + var slice = ArgumentsSlice.init(globalObject.bunVM(), callframe.arguments()); defer slice.deinit(); - const args = if (comptime Arguments != void) - (try Arguments.fromJS(globalObject, &slice)) - else - Arguments{}; - defer { - if (comptime Arguments != void and @hasDecl(Arguments, "deinit")) args.deinit(); - } + const args = if (Arguments != void) + try Arguments.fromJS(globalObject, &slice); + + defer if (comptime Arguments != void and @hasDecl(Arguments, "deinit")) + args.deinit(); if (globalObject.hasException()) { return .zero; } - var result = Function( - &this.node_fs, - args, - comptime Flavor.sync, - ); - switch (result) { - .err => |err| { - return globalObject.throwValue(JSC.JSValue.c(err.toJS(globalObject))); - }, - .result => |*res| { - return globalObject.toJS(res, .temporary); - }, - } + + var result = function(&this.node_fs, args, .sync); + return switch (result) { + .err => |err| globalObject.throwValue(JSC.JSValue.c(err.toJS(globalObject))), + .result => |*res| globalObject.toJS(res, .temporary), + }; } - }; - return NodeBindingClosure.bind; -} - -fn call(comptime FunctionEnum: NodeFSFunctionEnum) NodeFSFunction { - const Function = @field(JSC.Node.NodeFS, @tagName(FunctionEnum)); - const FunctionType = @TypeOf(Function); - - const function: std.builtin.Type.Fn = comptime @typeInfo(FunctionType).Fn; - comptime if (function.params.len != 3) @compileError("Expected 3 arguments"); - const Arguments = comptime function.params[1].type.?; - const NodeBindingClosure = struct { - pub fn bind(this: *JSC.Node.NodeJSFS, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { - var arguments = callframe.arguments_old(8); - - var slice = ArgumentsSlice.init(globalObject.bunVM(), arguments.slice()); + pub fn runAsync(this: *JSC.Node.NodeJSFS, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + var slice = ArgumentsSlice.init(globalObject.bunVM(), callframe.arguments()); slice.will_be_async = true; - const args = if (comptime Arguments != void) - (Arguments.fromJS(globalObject, &slice) catch { + + const args = if (Arguments != void) + Arguments.fromJS(globalObject, &slice) catch |err| { slice.deinit(); - return .zero; - }) - else - Arguments{}; + return err; + }; if (globalObject.hasException()) { slice.deinit(); return .zero; } - const Task = @field(JSC.Node.Async, @tagName(FunctionEnum)); - if (comptime FunctionEnum == .cp) { - return Task.create(globalObject, this, args, globalObject.bunVM(), slice.arena); - } else { - if (comptime FunctionEnum == .readdir) { - if (args.recursive) { - return JSC.Node.Async.readdir_recursive.create(globalObject, args, globalObject.bunVM()); - } - } - - return Task.create(globalObject, this, args, globalObject.bunVM()); + const Task = @field(JSC.Node.Async, @tagName(function_name)); + switch (comptime function_name) { + .cp => return Task.create(globalObject, this, args, globalObject.bunVM(), slice.arena), + .readdir => if (args.recursive) return JSC.Node.Async.readdir_recursive.create(globalObject, args, globalObject.bunVM()), + else => {}, } + return Task.create(globalObject, this, args, globalObject.bunVM()); } }; - return NodeBindingClosure.bind; +} + +fn callAsync(comptime FunctionEnum: NodeFSFunctionEnum) NodeFSFunction { + return Bindings(FunctionEnum).runAsync; +} +fn callSync(comptime FunctionEnum: NodeFSFunctionEnum) NodeFSFunction { + return Bindings(FunctionEnum).runSync; } pub const NodeJSFS = struct { @@ -121,43 +95,52 @@ pub const NodeJSFS = struct { this.destroy(); } - pub const access = call(.access); - pub const appendFile = call(.appendFile); - pub const close = call(.close); - pub const copyFile = call(.copyFile); - pub const cp = call(.cp); - pub const exists = call(.exists); - pub const chown = call(.chown); - pub const chmod = call(.chmod); - pub const fchmod = call(.fchmod); - pub const fchown = call(.fchown); - pub const fstat = call(.fstat); - pub const fsync = call(.fsync); - pub const ftruncate = call(.ftruncate); - pub const futimes = call(.futimes); - pub const lchmod = call(.lchmod); - pub const lchown = call(.lchown); - pub const link = call(.link); - pub const lstat = call(.lstat); - pub const mkdir = call(.mkdir); - pub const mkdtemp = call(.mkdtemp); - pub const open = call(.open); - pub const read = call(.read); - pub const write = call(.write); - pub const readdir = call(.readdir); - pub const readFile = call(.readFile); - pub const writeFile = call(.writeFile); - pub const readlink = call(.readlink); - pub const rm = call(.rm); - pub const rmdir = call(.rmdir); - pub const realpath = call(.realpath); - pub const rename = call(.rename); - pub const stat = call(.stat); - pub const symlink = call(.symlink); - pub const truncate = call(.truncate); - pub const unlink = call(.unlink); - pub const utimes = call(.utimes); - pub const lutimes = call(.lutimes); + pub fn getDirent(_: *NodeJSFS, globalThis: *JSC.JSGlobalObject) JSC.JSValue { + return JSC.Node.Dirent.getConstructor(globalThis); + } + + pub fn getStats(_: *NodeJSFS, globalThis: *JSC.JSGlobalObject) JSC.JSValue { + return JSC.Node.StatsSmall.getConstructor(globalThis); + } + + pub const access = callAsync(.access); + pub const appendFile = callAsync(.appendFile); + pub const close = callAsync(.close); + pub const copyFile = callAsync(.copyFile); + pub const cp = callAsync(.cp); + pub const exists = callAsync(.exists); + pub const chown = callAsync(.chown); + pub const chmod = callAsync(.chmod); + pub const fchmod = callAsync(.fchmod); + pub const fchown = callAsync(.fchown); + pub const fstat = callAsync(.fstat); + pub const fsync = callAsync(.fsync); + pub const ftruncate = callAsync(.ftruncate); + pub const futimes = callAsync(.futimes); + pub const lchmod = callAsync(.lchmod); + pub const lchown = callAsync(.lchown); + pub const link = callAsync(.link); + pub const lstat = callAsync(.lstat); + pub const mkdir = callAsync(.mkdir); + pub const mkdtemp = callAsync(.mkdtemp); + pub const open = callAsync(.open); + pub const read = callAsync(.read); + pub const write = callAsync(.write); + pub const readdir = callAsync(.readdir); + pub const readFile = callAsync(.readFile); + pub const writeFile = callAsync(.writeFile); + pub const readlink = callAsync(.readlink); + pub const rm = callAsync(.rm); + pub const rmdir = callAsync(.rmdir); + pub const realpath = callAsync(.realpathNonNative); + pub const realpathNative = callAsync(.realpath); + pub const rename = callAsync(.rename); + pub const stat = callAsync(.stat); + pub const symlink = callAsync(.symlink); + pub const truncate = callAsync(.truncate); + pub const unlink = callAsync(.unlink); + pub const utimes = callAsync(.utimes); + pub const lutimes = callAsync(.lutimes); pub const accessSync = callSync(.access); pub const appendFileSync = callSync(.appendFile); pub const closeSync = callSync(.close); @@ -185,7 +168,8 @@ pub const NodeJSFS = struct { pub const readFileSync = callSync(.readFile); pub const writeFileSync = callSync(.writeFile); pub const readlinkSync = callSync(.readlink); - pub const realpathSync = callSync(.realpath); + pub const realpathSync = callSync(.realpathNonNative); + pub const realpathNativeSync = callSync(.realpath); pub const renameSync = callSync(.rename); pub const statSync = callSync(.stat); pub const symlinkSync = callSync(.symlink); @@ -195,30 +179,15 @@ pub const NodeJSFS = struct { pub const lutimesSync = callSync(.lutimes); pub const rmSync = callSync(.rm); pub const rmdirSync = callSync(.rmdir); - pub const writev = call(.writev); + pub const writev = callAsync(.writev); pub const writevSync = callSync(.writev); - pub const readv = call(.readv); + pub const readv = callAsync(.readv); pub const readvSync = callSync(.readv); - pub const fdatasyncSync = callSync(.fdatasync); - pub const fdatasync = call(.fdatasync); - - pub fn getDirent(_: *NodeJSFS, globalThis: *JSC.JSGlobalObject) JSC.JSValue { - return JSC.Node.Dirent.getConstructor(globalThis); - } - - pub fn getStats(_: *NodeJSFS, globalThis: *JSC.JSGlobalObject) JSC.JSValue { - return JSC.Node.StatsSmall.getConstructor(globalThis); - } - + pub const fdatasync = callAsync(.fdatasync); pub const watch = callSync(.watch); pub const watchFile = callSync(.watchFile); pub const unwatchFile = callSync(.unwatchFile); - - // Not implemented yet: - const notimpl = fdatasync; - pub const opendir = notimpl; - pub const opendirSync = notimpl; }; pub fn createBinding(globalObject: *JSC.JSGlobalObject) JSC.JSValue { diff --git a/src/bun.js/node/node_fs_stat_watcher.zig b/src/bun.js/node/node_fs_stat_watcher.zig index 8071bfbc2c..f7bfb1eece 100644 --- a/src/bun.js/node/node_fs_stat_watcher.zig +++ b/src/bun.js/node/node_fs_stat_watcher.zig @@ -409,6 +409,10 @@ pub const StatWatcher = struct { }, ) catch |err| this.globalThis.reportActiveExceptionAsUnhandled(err); + if (this.closed) { + this.used_by_scheduler_thread.store(false, .release); + return; + } vm.rareData().nodeFSStatWatcherScheduler(vm).append(this); } diff --git a/src/bun.js/node/node_fs_watcher.zig b/src/bun.js/node/node_fs_watcher.zig index 05cf2b4987..c718225e15 100644 --- a/src/bun.js/node/node_fs_watcher.zig +++ b/src/bun.js/node/node_fs_watcher.zig @@ -427,12 +427,7 @@ pub const FSWatcher = struct { }; } - pub fn createFSWatcher(this: Arguments) JSC.Maybe(JSC.JSValue) { - return switch (FSWatcher.init(this)) { - .result => |result| .{ .result = result.js_this }, - .err => |err| .{ .err = err }, - }; - } + pub const createFSWatcher = FSWatcher.init; }; pub fn initJS(this: *FSWatcher, listener: JSC.JSValue) void { diff --git a/src/bun.js/node/types.zig b/src/bun.js/node/types.zig index 302e11fd5e..c3d84429e3 100644 --- a/src/bun.js/node/types.zig +++ b/src/bun.js/node/types.zig @@ -37,24 +37,6 @@ pub const Buffer = JSC.MarkedArrayBuffer; /// On unix it is what the utimens api expects pub const TimeLike = if (Environment.isWindows) f64 else std.posix.timespec; -pub const Flavor = enum { - sync, - promise, - callback, - - pub fn Wrap(comptime this: Flavor, comptime T: type) type { - return comptime brk: { - switch (this) { - .sync => break :brk T, - // .callback => { - // const Callback = CallbackTask(Type); - // }, - else => @compileError("Not implemented yet"), - } - }; - } -}; - /// Node.js expects the error to include contextual information /// - "syscall" /// - "path" @@ -635,18 +617,22 @@ pub const StringOrBuffer = union(enum) { return fromJSMaybeAsync(global, allocator, value, is_async); } - var str = try bun.String.fromJS2(value, global); - defer str.deref(); - if (str.isEmpty()) { - return fromJSMaybeAsync(global, allocator, value, is_async); + if (value.isString()) { + var str = try bun.String.fromJS2(value, global); + defer str.deref(); + if (str.isEmpty()) { + return fromJSMaybeAsync(global, allocator, value, is_async); + } + + const out = str.encode(encoding); + defer global.vm().reportExtraMemory(out.len); + + return .{ + .encoded_slice = JSC.ZigString.Slice.init(bun.default_allocator, out), + }; } - const out = str.encode(encoding); - defer global.vm().reportExtraMemory(out.len); - - return .{ - .encoded_slice = JSC.ZigString.Slice.init(bun.default_allocator, out), - }; + return null; } pub fn fromJSWithEncodingValue(global: *JSC.JSGlobalObject, allocator: std.mem.Allocator, value: JSC.JSValue, encoding_value: JSC.JSValue) bun.JSError!?StringOrBuffer { @@ -928,11 +914,11 @@ pub const PathLike = union(enum) { return sliceZWithForceCopy(this, buf, false); } - pub inline fn sliceW(this: PathLike, buf: *bun.PathBuffer) [:0]const u16 { - return strings.toWPath(@alignCast(std.mem.bytesAsSlice(u16, buf)), this.slice()); + pub inline fn sliceW(this: PathLike, buf: *bun.WPathBuffer) [:0]const u16 { + return strings.toWPath(buf, this.slice()); } - pub inline fn osPath(this: PathLike, buf: *bun.PathBuffer) bun.OSPathSliceZ { + pub inline fn osPath(this: PathLike, buf: *bun.OSPathBuffer) bun.OSPathSliceZ { if (comptime Environment.isWindows) { return sliceW(this, buf); } @@ -966,59 +952,36 @@ pub const PathLike = union(enum) { pub fn fromJSWithAllocator(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice, allocator: std.mem.Allocator) bun.JSError!?PathLike { const arg = arguments.next() orelse return null; switch (arg.jsType()) { - JSC.JSValue.JSType.Uint8Array, - JSC.JSValue.JSType.DataView, + .Uint8Array, + .DataView, => { const buffer = Buffer.fromTypedArray(ctx, arg); try Valid.pathBuffer(buffer, ctx); + try Valid.pathNullBytes(buffer.slice(), ctx); arguments.protectEat(); - return PathLike{ .buffer = buffer }; + return .{ .buffer = buffer }; }, - JSC.JSValue.JSType.ArrayBuffer => { + .ArrayBuffer => { const buffer = Buffer.fromArrayBuffer(ctx, arg); try Valid.pathBuffer(buffer, ctx); + try Valid.pathNullBytes(buffer.slice(), ctx); arguments.protectEat(); - - return PathLike{ .buffer = buffer }; + return .{ .buffer = buffer }; }, - JSC.JSValue.JSType.String, - JSC.JSValue.JSType.StringObject, - JSC.JSValue.JSType.DerivedStringObject, + .String, + .StringObject, + .DerivedStringObject, => { var str = arg.toBunString(ctx); defer str.deref(); arguments.eat(); - try Valid.pathStringLength(str.length(), ctx); - - if (arguments.will_be_async) { - var sliced = str.toThreadSafeSlice(allocator); - sliced.reportExtraMemory(ctx.vm()); - - if (sliced.underlying.isEmpty()) { - return PathLike{ .encoded_slice = sliced.utf8 }; - } - - return PathLike{ .threadsafe_string = sliced }; - } else { - var sliced = str.toSlice(allocator); - - // Costs nothing to keep both around. - if (sliced.isWTFAllocated()) { - str.ref(); - return PathLike{ .slice_with_underlying_string = sliced }; - } - - sliced.reportExtraMemory(ctx.vm()); - - // It is expensive to keep both around. - return PathLike{ .encoded_slice = sliced.utf8 }; - } + return try fromBunString(ctx, str, arguments.will_be_async, allocator); }, else => { if (arg.as(JSC.DOMURL)) |domurl| { @@ -1029,49 +992,58 @@ pub const PathLike = union(enum) { } arguments.eat(); - try Valid.pathStringLength(str.length(), ctx); - - if (arguments.will_be_async) { - var sliced = str.toThreadSafeSlice(allocator); - sliced.reportExtraMemory(ctx.vm()); - - if (sliced.underlying.isEmpty()) { - return PathLike{ .encoded_slice = sliced.utf8 }; - } - - return PathLike{ .threadsafe_string = sliced }; - } else { - var sliced = str.toSlice(allocator); - - // Costs nothing to keep both around. - if (sliced.isWTFAllocated()) { - str.ref(); - return PathLike{ .slice_with_underlying_string = sliced }; - } - - sliced.reportExtraMemory(ctx.vm()); - - // It is expensive to keep both around. - return PathLike{ .encoded_slice = sliced.utf8 }; - } + return try fromBunString(ctx, str, arguments.will_be_async, allocator); } return null; }, } } + + pub fn fromBunString(global: *JSC.JSGlobalObject, str: bun.String, will_be_async: bool, allocator: std.mem.Allocator) !PathLike { + try Valid.pathStringLength(str.length(), global); + + if (will_be_async) { + var sliced = str.toThreadSafeSlice(allocator); + errdefer sliced.deinit(); + + try Valid.pathNullBytes(sliced.slice(), global); + + sliced.reportExtraMemory(global.vm()); + + if (sliced.underlying.isEmpty()) { + return .{ .encoded_slice = sliced.utf8 }; + } + return .{ .threadsafe_string = sliced }; + } else { + var sliced = str.toSlice(allocator); + errdefer if (!sliced.isWTFAllocated()) sliced.deinit(); + + try Valid.pathNullBytes(sliced.slice(), global); + + // Costs nothing to keep both around. + if (sliced.isWTFAllocated()) { + str.ref(); + return .{ .slice_with_underlying_string = sliced }; + } + + sliced.reportExtraMemory(global.vm()); + + // It is expensive to keep both around. + return .{ .encoded_slice = sliced.utf8 }; + } + } }; pub const Valid = struct { - pub fn fileDescriptor(fd: i64, ctx: JSC.C.JSContextRef) bun.JSError!void { - if (fd < 0) { - return ctx.throwInvalidArguments("Invalid file descriptor, must not be negative number", .{}); - } - + pub fn fileDescriptor(fd: i64, global: JSC.C.JSContextRef) bun.JSError!void { const fd_t = if (Environment.isWindows) bun.windows.libuv.uv_file else bun.FileDescriptorInt; - - if (fd > std.math.maxInt(fd_t)) { - return ctx.throwInvalidArguments("Invalid file descriptor, must not be greater than {d}", .{std.math.maxInt(fd_t)}); + if (fd < 0 or fd > std.math.maxInt(fd_t)) { + return global.throwRangeError(fd, .{ + .min = 0, + .max = std.math.maxInt(fd_t), + .field_name = "fd", + }); } } @@ -1079,26 +1051,24 @@ pub const Valid = struct { switch (zig_str.len) { 0...bun.MAX_PATH_BYTES => return, else => { - // TODO: should this be an EINVAL? var system_error = bun.sys.Error.fromCode(.NAMETOOLONG, .open).withPath(zig_str.slice()).toSystemError(); system_error.syscall = bun.String.dead; return ctx.throwValue(system_error.toErrorInstance(ctx)); }, } - unreachable; + comptime unreachable; } pub fn pathStringLength(len: usize, ctx: JSC.C.JSContextRef) bun.JSError!void { switch (len) { 0...bun.MAX_PATH_BYTES => return, else => { - // TODO: should this be an EINVAL? var system_error = bun.sys.Error.fromCode(.NAMETOOLONG, .open).toSystemError(); system_error.syscall = bun.String.dead; return ctx.throwValue(system_error.toErrorInstance(ctx)); }, } - unreachable; + comptime unreachable; } pub fn pathString(zig_str: JSC.ZigString, ctx: JSC.C.JSContextRef) bun.JSError!void { @@ -1118,7 +1088,13 @@ pub const Valid = struct { }, 1...bun.MAX_PATH_BYTES => return, } - unreachable; + comptime unreachable; + } + + pub fn pathNullBytes(slice: []const u8, global: *JSC.JSGlobalObject) bun.JSError!void { + if (bun.strings.indexOfChar(slice, 0) != null) { + return global.ERR_INVALID_ARG_VALUE("The argument 'path' must be a string, Uint8Array, or URL without null bytes. Received {}", .{bun.fmt.quote(slice)}).throw(); + } } }; @@ -1252,42 +1228,68 @@ pub fn fileDescriptorFromJS(ctx: JSC.C.JSContextRef, value: JSC.JSValue) bun.JSE null; } +// Equivalent to `toUnixTimestamp` +// // Node.js docs: // > Values can be either numbers representing Unix epoch time in seconds, Dates, or a numeric string like '123456789.0'. // > If the value can not be converted to a number, or is NaN, Infinity, or -Infinity, an Error will be thrown. pub fn timeLikeFromJS(globalObject: *JSC.JSGlobalObject, value: JSC.JSValue) ?TimeLike { - if (value.jsType() == .JSDate) { - const milliseconds = value.getUnixTimestamp(); - if (!std.math.isFinite(milliseconds)) { - return null; + // Number is most common case + if (value.isNumber()) { + const seconds = value.asNumber(); + if (std.math.isFinite(seconds)) { + if (seconds < 0) { + return timeLikeFromNow(); + } + return timeLikeFromSeconds(seconds); } - - if (comptime Environment.isWindows) { - return milliseconds / 1000.0; - } - - return TimeLike{ - .tv_sec = @intFromFloat(@divFloor(milliseconds, std.time.ms_per_s)), - .tv_nsec = @intFromFloat(@mod(milliseconds, std.time.ms_per_s) * std.time.ns_per_ms), - }; - } - - if (!value.isNumber() and !value.isString()) { return null; + } else switch (value.jsType()) { + .JSDate => { + const milliseconds = value.getUnixTimestamp(); + if (std.math.isFinite(milliseconds)) { + return timeLikeFromMilliseconds(milliseconds); + } + }, + .String => { + const seconds = value.coerceToDouble(globalObject); + if (std.math.isFinite(seconds)) { + return timeLikeFromSeconds(seconds); + } + }, + else => {}, } + return null; +} - const seconds = value.coerce(f64, globalObject); - if (!std.math.isFinite(seconds)) { - return null; - } - - if (comptime Environment.isWindows) { +fn timeLikeFromSeconds(seconds: f64) TimeLike { + if (Environment.isWindows) { return seconds; } - - return TimeLike{ + return .{ .tv_sec = @intFromFloat(seconds), - .tv_nsec = @intFromFloat(@mod(seconds, 1.0) * std.time.ns_per_s), + .tv_nsec = @intFromFloat(@mod(seconds, 1) * std.time.ns_per_s), + }; +} + +fn timeLikeFromMilliseconds(milliseconds: f64) TimeLike { + if (Environment.isWindows) { + return milliseconds / 1000.0; + } + return .{ + .tv_sec = @intFromFloat(@divFloor(milliseconds, std.time.ms_per_s)), + .tv_nsec = @intFromFloat(@mod(milliseconds, std.time.ms_per_s) * std.time.ns_per_ms), + }; +} + +fn timeLikeFromNow() TimeLike { + const nanos = std.time.nanoTimestamp(); + if (Environment.isWindows) { + return @as(TimeLike, @floatFromInt(nanos)) / std.time.ns_per_s; + } + return .{ + .tv_sec = @truncate(@divFloor(nanos, std.time.ns_per_s)), + .tv_nsec = @truncate(@mod(nanos, std.time.ns_per_s)), }; } @@ -1302,11 +1304,11 @@ pub fn modeFromJS(ctx: JSC.C.JSContextRef, value: JSC.JSValue) bun.JSError!?Mode return ctx.throwInvalidArgumentTypeValue("mode", "number", value); } - // 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 - // the example), specifies permissions for the group. The right-most - // digit (5 in the example), specifies the permissions for others. + // 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 + // the example), specifies permissions for the group. The right-most + // digit (5 in the example), specifies the permissions for others. var zig_str = JSC.ZigString.Empty; value.toZigString(&zig_str, ctx); @@ -1535,13 +1537,41 @@ pub const FileSystemFlags = enum(Mode) { return null; } + + /// Equivalent of GetValidFileMode, which is used to implement fs.access and copyFile + pub fn fromJSNumberOnly(global: *JSC.JSGlobalObject, value: JSC.JSValue, comptime kind: enum { access, copy_file }) bun.JSError!FileSystemFlags { + // Allow only int32 or null/undefined values. + if (!value.isNumber()) { + if (value.isUndefinedOrNull()) { + return @enumFromInt(switch (kind) { + .access => 0, // F_OK + .copy_file => 0, // constexpr int kDefaultCopyMode = 0; + }); + } + return global.ERR_INVALID_ARG_TYPE("mode must be int32 or null/undefined", .{}).throw(); + } + const min, const max = .{ 0, 7 }; + if (value.isInt32()) { + const int: i32 = value.asInt32(); + if (int < min or int > max) { + return global.ERR_OUT_OF_RANGE(comptime std.fmt.comptimePrint("mode is out of range: >= {d} && <= {d}", .{ min, max }), .{}).throw(); + } + return @enumFromInt(int); + } else { + const float = value.asNumber(); + if (std.math.isNan(float) or std.math.isInf(float) or float < min or float > max) { + return global.ERR_OUT_OF_RANGE(comptime std.fmt.comptimePrint("mode is out of range: >= {d} && <= {d}", .{ min, max }), .{}).throw(); + } + return @enumFromInt(@as(i32, @intFromFloat(float))); + } + } }; /// Stats and BigIntStats classes from node:fs -pub fn StatType(comptime Big: bool) type { - const Int = if (Big) i64 else i32; - const Float = if (Big) i64 else f64; - const Timestamp = if (Big) u64 else u0; +pub fn StatType(comptime big: bool) type { + const Int = if (big) i64 else i32; + const Float = if (big) i64 else f64; + const Timestamp = if (big) u64 else u0; const Date = packed struct { value: Float, @@ -1553,7 +1583,7 @@ pub fn StatType(comptime Big: bool) type { }; return extern struct { - pub usingnamespace if (Big) JSC.Codegen.JSBigIntStats else JSC.Codegen.JSStats; + pub usingnamespace if (big) JSC.Codegen.JSBigIntStats else JSC.Codegen.JSStats; pub usingnamespace bun.New(@This()); // Stats stores these as i32, but BigIntStats stores all of these as i64 @@ -1594,12 +1624,21 @@ pub fn StatType(comptime Big: bool) type { } fn toTimeMS(ts: StatTimespec) Float { - if (Big) { - const tv_sec: i64 = @intCast(ts.tv_sec); - const tv_nsec: i64 = @intCast(ts.tv_nsec); - return @as(i64, @intCast(tv_sec * std.time.ms_per_s)) + @as(i64, @intCast(@divTrunc(tv_nsec, std.time.ns_per_ms))); + // On windows, Node.js purposefully mis-interprets time values + // > On win32, time is stored in uint64_t and starts from 1601-01-01. + // > libuv calculates tv_sec and tv_nsec from it and converts to signed long, + // > which causes Y2038 overflow. On the other platforms it is safe to treat + // > negative values as pre-epoch time. + const tv_sec = if (Environment.isWindows) @as(u32, @bitCast(ts.tv_sec)) else ts.tv_sec; + const tv_nsec = if (Environment.isWindows) @as(u32, @bitCast(ts.tv_nsec)) else ts.tv_nsec; + if (big) { + const sec: i64 = @intCast(tv_sec); + const nsec: i64 = @intCast(tv_nsec); + return @as(i64, @intCast(sec * std.time.ms_per_s)) + + @as(i64, @intCast(@divTrunc(nsec, std.time.ns_per_ms))); } else { - return (@as(f64, @floatFromInt(@max(ts.tv_sec, 0))) * std.time.ms_per_s) + (@as(f64, @floatFromInt(@as(usize, @intCast(@max(ts.tv_nsec, 0))))) / std.time.ns_per_ms); + return (@as(f64, @floatFromInt(tv_sec)) * std.time.ms_per_s) + + (@as(f64, @floatFromInt(tv_nsec)) / std.time.ns_per_ms); } } @@ -1610,7 +1649,7 @@ pub fn StatType(comptime Big: bool) type { pub fn callback(this: *This, globalObject: *JSC.JSGlobalObject) JSC.JSValue { const value = @field(this, @tagName(field)); const Type = @TypeOf(value); - if (comptime Big and @typeInfo(Type) == .Int) { + if (comptime big and @typeInfo(Type) == .Int) { if (Type == u64) { return JSC.JSValue.fromUInt64NoTruncate(globalObject, value); } @@ -1752,19 +1791,19 @@ pub fn StatType(comptime Big: bool) type { .atime_ms = toTimeMS(aTime), .mtime_ms = toTimeMS(mTime), .ctime_ms = toTimeMS(cTime), - .atime_ns = if (Big) toNanoseconds(aTime) else 0, - .mtime_ns = if (Big) toNanoseconds(mTime) else 0, - .ctime_ns = if (Big) toNanoseconds(cTime) else 0, + .atime_ns = if (big) toNanoseconds(aTime) else 0, + .mtime_ns = if (big) toNanoseconds(mTime) else 0, + .ctime_ns = if (big) toNanoseconds(cTime) else 0, // Linux doesn't include this info in stat // maybe it does in statx, but do you really need birthtime? If you do please file an issue. .birthtime_ms = if (Environment.isLinux) 0 else toTimeMS(stat_.birthtime()), - .birthtime_ns = if (Big and !Environment.isLinux) toNanoseconds(stat_.birthtime()) else 0, + .birthtime_ns = if (big and !Environment.isLinux) toNanoseconds(stat_.birthtime()) else 0, }; } pub fn constructor(globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) bun.JSError!*This { - if (Big) { + if (big) { return globalObject.throwInvalidArguments("BigIntStats is not a constructor", .{}); } diff --git a/src/bun.js/node/util/validators.zig b/src/bun.js/node/util/validators.zig index cf8f3b1e04..a4f37d968d 100644 --- a/src/bun.js/node/util/validators.zig +++ b/src/bun.js/node/util/validators.zig @@ -60,12 +60,12 @@ pub fn validateInteger(globalThis: *JSGlobalObject, value: JSValue, comptime nam if (!value.isNumber()) return throwErrInvalidArgType(globalThis, name_fmt, name_args, "number", value); if (!value.isAnyInt()) { - return throwRangeError(globalThis, "The value of \"" ++ name_fmt ++ "\" is out of range. It must be an integer. Received {s}", name_args ++ .{value}); + return throwRangeError(globalThis, "The value of \"" ++ name_fmt ++ "\" is out of range. It must be an integer. Received {}", name_args ++ .{bun.fmt.double(value.asNumber())}); } const num = value.asInt52(); if (num < min or num > max) { - return throwRangeError(globalThis, "The value of \"" ++ name_fmt ++ "\" is out of range. It must be >= {d} and <= {d}. Received {s}", name_args ++ .{ min, max, value }); + return throwRangeError(globalThis, "The value of \"" ++ name_fmt ++ "\" is out of range. It must be >= {d} && <= {d}. Received {}", name_args ++ .{ min, max, num }); } return num; } @@ -77,16 +77,17 @@ pub fn validateInt32(globalThis: *JSGlobalObject, value: JSValue, comptime name_ if (!value.isNumber()) { return throwErrInvalidArgType(globalThis, name_fmt, name_args, "number", value); } - if (!value.isInt32()) { + if (!value.isAnyInt()) { var formatter = JSC.ConsoleObject.Formatter{ .globalThis = globalThis }; return throwRangeError(globalThis, "The value of \"" ++ name_fmt ++ "\" is out of range. It must be an integer. Received {}", name_args ++ .{value.toFmt(&formatter)}); } - const num = value.asInt32(); - if (num < min or num > max) { + const num = value.asNumber(); + // Use floating point comparison here to ensure values out of i32 range get caught instead of clamp/truncated. + if (num < @as(f64, @floatFromInt(min)) or num > @as(f64, @floatFromInt(max))) { var formatter = JSC.ConsoleObject.Formatter{ .globalThis = globalThis }; - return 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 throwRangeError(globalThis, "The value of \"" ++ name_fmt ++ "\" is out of range. It must be >= {d} && <= {d}. Received {}", name_args ++ .{ min, max, value.toFmt(&formatter) }); } - return num; + return @intFromFloat(num); } pub fn validateUint32(globalThis: *JSGlobalObject, value: JSValue, comptime name_fmt: string, name_args: anytype, greater_than_zero: bool) bun.JSError!u32 { @@ -102,7 +103,7 @@ pub fn validateUint32(globalThis: *JSGlobalObject, value: JSValue, comptime name const max: i64 = @intCast(std.math.maxInt(u32)); if (num < min or num > max) { var formatter = JSC.ConsoleObject.Formatter{ .globalThis = globalThis }; - return 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 throwRangeError(globalThis, "The value of \"" ++ name_fmt ++ "\" is out of range. It must be >= {d} && <= {d}. Received {}", name_args ++ .{ min, max, value.toFmt(&formatter) }); } return @truncate(@as(u63, @intCast(num))); } @@ -129,11 +130,11 @@ pub fn validateNumber(globalThis: *JSGlobalObject, value: JSValue, comptime name } if (!valid) { if (min != null and max != null) { - return throwRangeError(globalThis, "The value of \"" ++ name_fmt ++ "\" is out of range. It must be >= {d} and <= {d}. Received {s}", name_args ++ .{ min, max, value }); + return throwRangeError(globalThis, "The value of \"" ++ name_fmt ++ "\" is out of range. It must be >= {d} && <= {d}. Received {s}", name_args ++ .{ min, max, value }); } else if (min != null) { return throwRangeError(globalThis, "The value of \"" ++ name_fmt ++ "\" is out of range. It must be >= {d}. Received {s}", name_args ++ .{ max, value }); } else { - return throwRangeError(globalThis, "The value of \"" ++ name_fmt ++ "\" is out of range. It must and <= {d}. Received {s}", name_args ++ .{ max, value }); + return throwRangeError(globalThis, "The value of \"" ++ name_fmt ++ "\" is out of range. It must be <= {d}. Received {s}", name_args ++ .{ max, value }); } } return num; diff --git a/src/bun.js/webcore/blob.zig b/src/bun.js/webcore/blob.zig index 53d4c87113..cc75cc0eeb 100644 --- a/src/bun.js/webcore/blob.zig +++ b/src/bun.js/webcore/blob.zig @@ -830,7 +830,6 @@ pub const Blob = struct { .recursive = true, .always_return_none = true, }, - .sync, )) { .result => { this.mkdirp_if_not_exists = false; @@ -2757,7 +2756,7 @@ pub const Blob = struct { pub fn throw(this: *CopyFileWindows, err: bun.sys.Error) void { const globalThis = this.promise.strong.globalThis.?; const promise = this.promise.swap(); - const err_instance = err.toSystemError().toErrorInstance(globalThis); + const err_instance = err.toJSC(globalThis); var event_loop = this.event_loop; event_loop.enter(); defer event_loop.exit(); diff --git a/src/bun.zig b/src/bun.zig index 5ae74eae5c..36251997c3 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -3353,12 +3353,23 @@ pub inline fn resolveSourcePath( } const RuntimeEmbedRoot = enum { + /// Relative to `/codegen`. codegen, + /// Relative to `src` src, + /// Reallocates the slice at every call. Avoid this if possible. An example + /// using this reasonably is referencing incremental_visualizer.html, which + /// is reloaded from disk for each request, but more importantly allows + /// maintaining the DevServer state while hacking on the visualizer. src_eager, + /// Avoid this if possible. See `.src_eager`. codegen_eager, }; +/// Load a file at runtime. This is only to be used in debug builds, +/// specifically when `Environment.codegen_embed` is false. This allows quick +/// iteration on files, as this skips the Zig compiler. Once Zig gains good +/// incremental support, the non-eager cases can be deleted. pub fn runtimeEmbedFile( comptime root: RuntimeEmbedRoot, comptime sub_path: []const u8, @@ -3901,7 +3912,7 @@ pub fn WeakPtr(comptime T: type, comptime weakable_field: std.meta.FieldEnum(T)) pub const DebugThreadLock = if (Environment.allow_assert) struct { - owning_thread: ?std.Thread.Id = null, + owning_thread: ?std.Thread.Id, locked_at: crash_handler.StoredTrace, pub const unlocked: DebugThreadLock = .{ @@ -4225,3 +4236,113 @@ pub const WPathBufferPool = if (Environment.isWindows) PathBufferPoolT(bun.WPath pub const OSPathBufferPool = if (Environment.isWindows) WPathBufferPool else PathBufferPool; pub const S3 = @import("./s3/client.zig"); + +const CowString = CowSlice(u8); + +/// "Copy on write" slice. There are many instances when it is desired to re-use +/// a slice, but doing so would make it unknown if that slice should be freed. +/// This structure, in release builds, is the same size as `[]const T`, but +/// stores one bit for if deinitialziation should free the underlying memory. +/// +/// const str = CowSlice(u8).initOwned(try alloc.dupe(u8, "hello!"), alloc); +/// const borrow = str.borrow(); +/// assert(borrow.slice().ptr == str.slice().ptr) +/// borrow.deinit(alloc); // knows it is borrowed, no free +/// str.deinit(alloc); // calls free +/// +/// In a debug build, there are aggressive assertions to ensure unintentional +/// frees do not happen. But in a release build, the developer is expected to +/// keep slice owners alive beyond the lifetimes of the borrowed instances. +/// +/// CowSlice does not support slices longer than 2^(@bitSizeOf(usize)-1). +pub fn CowSlice(T: type) type { + const DebugData = if (Environment.allow_assert) struct { + mutex: std.Thread.Mutex, + allocator: Allocator, + borrows: usize, + }; + return struct { + ptr: [*]const T, + flags: packed struct(usize) { + len: @Type(.{ .Int = .{ + .bits = @bitSizeOf(usize) - 1, + .signedness = .unsigned, + } }), + is_owned: bool, + }, + debug: if (Environment.allow_assert) ?*DebugData else void, + + const cow_str_assertions = Environment.isDebug; + + /// `data` is transferred into the returned string, and must be freed with + /// `.deinit()` when the string and its borrows are done being used. + pub fn initOwned(data: []const T, allocator: Allocator) @This() { + return .{ + .ptr = data.ptr, + .flags = .{ + .is_owned = true, + .len = @intCast(data.len), + }, + .debug = if (cow_str_assertions) + bun.new(DebugData(.{ + .mutex = .{}, + .allocator = allocator, + .borrows = 0, + })), + }; + } + + /// `.deinit` will not free memory from this slice. + pub fn initNeverFree(data: []const T) @This() { + return .{ + .ptr = data.ptr, + .flags = .{ + .is_owned = false, + .len = @intCast(data.len), + }, + .debug = null, + }; + } + + pub fn slice(str: @This()) []const T { + return str.ptr[0..str.flags.len]; + } + + /// Returns a new string. The borrowed string should be deinitialized + /// so that debug assertions that perform. + pub fn borrow(str: @This()) @This() { + if (cow_str_assertions) if (str.debug) |debug| { + debug.mutex.lock(); + defer debug.mutex.unlock(); + debug.borrows += 1; + }; + return .{ + .ptr = str.ptr, + .flags = .{ + .is_owned = false, + .len = str.flags.len, + }, + .debug = str.debug, + }; + } + + pub fn deinit(str: @This(), allocator: Allocator) void { + if (cow_str_assertions) if (str.debug) |debug| { + debug.mutex.lock(); + defer debug.mutex.unlock(); + bun.assert(debug.allocator == allocator); + if (str.flags.is_owned) { + bun.assert(debug.borrows == 0); // active borrows become invalid data + } else { + debug.borrows -= 1; // double deinit of a borrowed string + } + bun.destroy(debug); + }; + if (str.flags.is_owned) { + allocator.free(str.slice()); + } + } + }; +} + +const Allocator = std.mem.Allocator; diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index 55ce5fe86e..00eef30e77 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -13346,12 +13346,9 @@ pub const LinkerContext = struct { }, )) { .err => |err| { - var message = err.toSystemError().message.toUTF8(bun.default_allocator); - defer message.deinit(); - c.log.addErrorFmt(null, Logger.Loc.Empty, bun.default_allocator, "{} writing sourcemap for chunk {}", .{ - bun.fmt.quote(message.slice()), + try c.log.addSysError(bun.default_allocator, err, "writing sourcemap for chunk {}", .{ bun.fmt.quote(chunk.final_rel_path), - }) catch unreachable; + }); return error.WriteFailed; }, .result => {}, @@ -13497,12 +13494,9 @@ pub const LinkerContext = struct { }, )) { .err => |err| { - var message = err.toSystemError().message.toUTF8(bun.default_allocator); - defer message.deinit(); - c.log.addErrorFmt(null, Logger.Loc.Empty, bun.default_allocator, "{} writing chunk {}", .{ - bun.fmt.quote(message.slice()), + try c.log.addSysError(bun.default_allocator, err, "writing chunk {}", .{ bun.fmt.quote(chunk.final_rel_path), - }) catch unreachable; + }); return error.WriteFailed; }, .result => {}, @@ -13617,10 +13611,7 @@ pub const LinkerContext = struct { }, )) { .err => |err| { - const utf8 = err.toSystemError().message.toUTF8(bun.default_allocator); - defer utf8.deinit(); - c.log.addErrorFmt(null, Logger.Loc.Empty, bun.default_allocator, "{} writing file {}", .{ - bun.fmt.quote(utf8.slice()), + c.log.addSysError(bun.default_allocator, err, "writing file {}", .{ bun.fmt.quote(src.src_path.text), }) catch unreachable; return error.WriteFailed; diff --git a/src/cli.zig b/src/cli.zig index 48fa3399e5..ef679798ba 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -236,7 +236,6 @@ pub const Arguments = struct { clap.parseParam("--conditions ... Pass custom conditions to resolve") catch unreachable, clap.parseParam("--fetch-preconnect ... Preconnect to a URL while code is loading") catch unreachable, clap.parseParam("--max-http-header-size Set the maximum size of HTTP headers in bytes. Default is 16KiB") catch unreachable, - clap.parseParam("--expose-internals Expose internals used for testing Bun itself. Usage of these APIs is completely unsupported.") catch unreachable, clap.parseParam("--dns-result-order Set the default order of DNS lookup results. Valid orders: verbatim (default), ipv4first, ipv6first") catch unreachable, clap.parseParam("--expose-gc Expose gc() on the global object. Has no effect on Bun.gc().") catch unreachable, clap.parseParam("--no-deprecation Suppress all reporting of the custom deprecation.") catch unreachable, @@ -807,9 +806,6 @@ pub const Arguments = struct { bun.JSC.RuntimeTranspilerCache.is_disabled = true; } - if (args.flag("--expose-internals")) { - bun.JSC.ModuleLoader.is_allowed_to_use_internal_testing_apis = true; - } if (args.flag("--no-deprecation")) { Bun__Node__ProcessNoDeprecation = true; } @@ -1409,7 +1405,10 @@ pub const HelpCommand = struct { pub const ReservedCommand = struct { pub fn exec(_: std.mem.Allocator) !void { @setCold(true); - const command_name = bun.argv[1]; + const command_name = for (bun.argv[1..]) |arg| { + if (arg.len > 1 and arg[0] == '-') continue; + break arg; + } else bun.argv[1]; Output.prettyError( \\Uh-oh. bun {s} is a subcommand reserved for future use by Bun. \\ diff --git a/src/cli/exec_command.zig b/src/cli/exec_command.zig index 0711d7b1e3..58a0b757eb 100644 --- a/src/cli/exec_command.zig +++ b/src/cli/exec_command.zig @@ -29,7 +29,7 @@ pub const ExecCommand = struct { const cwd = switch (bun.sys.getcwd(&buf)) { .result => |p| p, .err => |e| { - Output.prettyErrorln("error: Failed to run script {s} due to error {s}", .{ script, e.toSystemError() }); + Output.err(e, "failed to run script {s}", .{script}); Global.exit(1); }, }; @@ -40,7 +40,7 @@ pub const ExecCommand = struct { const script_path = bun.path.join(parts, .auto); const code = bun.shell.Interpreter.initAndRunFromSource(ctx, mini, script_path, script) catch |err| { - Output.prettyErrorln("error: Failed to run script {s} due to error {s}", .{ script_path, @errorName(err) }); + Output.err(err, "failed to run script {s}", .{script_path}); Global.exit(1); }; diff --git a/src/cli/test_command.zig b/src/cli/test_command.zig index 050a83880c..d76218c15e 100644 --- a/src/cli/test_command.zig +++ b/src/cli/test_command.zig @@ -820,7 +820,6 @@ pub const CommandLineReporter = struct { }, .always_return_none = true, }, - .sync, ); // Write the lcov.info file to a temporary file we atomically rename to the final name after it succeeds diff --git a/src/copy_file.zig b/src/copy_file.zig index 1d66e8ed04..220aeb7733 100644 --- a/src/copy_file.zig +++ b/src/copy_file.zig @@ -42,7 +42,7 @@ const InputType = if (Environment.isWindows) bun.OSPathSliceZ else posix.fd_t; /// This means that it cannot work with TTYs and some special devices /// But it can work with two ordinary files /// -/// on macoS and other platforms, sendfile() only works when one of the ends is a socket +/// on macOS and other platforms, sendfile() only works when one of the ends is a socket /// and in general on macOS, it doesn't seem to have much performance impact. const LinuxCopyFileState = packed struct { /// This is the most important flag for reducing the system call count diff --git a/src/crash_handler.zig b/src/crash_handler.zig index b8d4e5a4e4..846288fc3b 100644 --- a/src/crash_handler.zig +++ b/src/crash_handler.zig @@ -1652,6 +1652,13 @@ pub fn dumpStackTrace(trace: std.builtin.StackTrace) void { stderr.writeAll(proc.stderr) catch return; } +pub fn dumpCurrentStackTrace(first_address: ?usize) void { + var addrs: [32]usize = undefined; + var stack: std.builtin.StackTrace = .{ .index = 0, .instruction_addresses = &addrs }; + std.debug.captureStackTrace(first_address, &stack); + dumpStackTrace(stack); +} + /// A variant of `std.builtin.StackTrace` that stores its data within itself /// instead of being a pointer. This allows storing captured stack traces /// for later printing. diff --git a/src/darwin_c.zig b/src/darwin_c.zig index b13c73ff9d..3019c177e7 100644 --- a/src/darwin_c.zig +++ b/src/darwin_c.zig @@ -313,122 +313,6 @@ pub const SystemErrno = enum(u8) { if (code >= max) return null; return @enumFromInt(code); } - - pub fn label(this: SystemErrno) ?[:0]const u8 { - return labels.get(this) orelse null; - } - - const LabelMap = std.EnumMap(SystemErrno, [:0]const u8); - pub const labels: LabelMap = brk: { - var map: LabelMap = LabelMap.initFull(""); - map.put(.E2BIG, "Argument list too long"); - map.put(.EACCES, "Permission denied"); - map.put(.EADDRINUSE, "Address already in use"); - map.put(.EADDRNOTAVAIL, "Can't assign requested address"); - map.put(.EAFNOSUPPORT, "Address family not supported by protocol family"); - map.put(.EAGAIN, "non-blocking and interrupt i/o. Resource temporarily unavailable"); - map.put(.EALREADY, "Operation already in progress"); - map.put(.EAUTH, "Authentication error"); - map.put(.EBADARCH, "Bad CPU type in executable"); - map.put(.EBADEXEC, "Program loading errors. Bad executable"); - map.put(.EBADF, "Bad file descriptor"); - map.put(.EBADMACHO, "Malformed Macho file"); - map.put(.EBADMSG, "Bad message"); - map.put(.EBADRPC, "RPC struct is bad"); - map.put(.EBUSY, "Device / Resource busy"); - map.put(.ECANCELED, "Operation canceled"); - map.put(.ECHILD, "No child processes"); - map.put(.ECONNABORTED, "Software caused connection abort"); - map.put(.ECONNREFUSED, "Connection refused"); - map.put(.ECONNRESET, "Connection reset by peer"); - map.put(.EDEADLK, "Resource deadlock avoided"); - map.put(.EDESTADDRREQ, "Destination address required"); - map.put(.EDEVERR, "Device error, for example paper out"); - map.put(.EDOM, "math software. Numerical argument out of domain"); - map.put(.EDQUOT, "Disc quota exceeded"); - map.put(.EEXIST, "File or folder exists"); - map.put(.EFAULT, "Bad address"); - map.put(.EFBIG, "File too large"); - map.put(.EFTYPE, "Inappropriate file type or format"); - map.put(.EHOSTDOWN, "Host is down"); - map.put(.EHOSTUNREACH, "No route to host"); - map.put(.EIDRM, "Identifier removed"); - map.put(.EILSEQ, "Illegal byte sequence"); - map.put(.EINPROGRESS, "Operation now in progress"); - map.put(.EINTR, "Interrupted system call"); - map.put(.EINVAL, "Invalid argument"); - map.put(.EIO, "Input/output error"); - map.put(.EISCONN, "Socket is already connected"); - map.put(.EISDIR, "Is a directory"); - map.put(.ELOOP, "Too many levels of symbolic links"); - map.put(.EMFILE, "Too many open files"); - map.put(.EMLINK, "Too many links"); - map.put(.EMSGSIZE, "Message too long"); - map.put(.EMULTIHOP, "Reserved"); - map.put(.ENAMETOOLONG, "File name too long"); - map.put(.ENEEDAUTH, "Need authenticator"); - map.put(.ENETDOWN, "ipc/network software - operational errors Network is down"); - map.put(.ENETRESET, "Network dropped connection on reset"); - map.put(.ENETUNREACH, "Network is unreachable"); - map.put(.ENFILE, "Too many open files in system"); - map.put(.ENOATTR, "Attribute not found"); - map.put(.ENOBUFS, "No buffer space available"); - map.put(.ENODATA, "No message available on STREAM"); - map.put(.ENODEV, "Operation not supported by device"); - map.put(.ENOENT, "No such file or directory"); - map.put(.ENOEXEC, "Exec format error"); - map.put(.ENOLCK, "No locks available"); - map.put(.ENOLINK, "Reserved"); - map.put(.ENOMEM, "Out of memory"); - map.put(.ENOMSG, "No message of desired type"); - map.put(.ENOPOLICY, "No such policy registered"); - map.put(.ENOPROTOOPT, "Protocol not available"); - map.put(.ENOSPC, "No space left on device"); - map.put(.ENOSR, "No STREAM resources"); - map.put(.ENOSTR, "Not a STREAM"); - map.put(.ENOSYS, "Function not implemented"); - map.put(.ENOTBLK, "Block device required"); - map.put(.ENOTCONN, "Socket is not connected"); - map.put(.ENOTDIR, "Not a directory"); - map.put(.ENOTEMPTY, "Directory not empty"); - map.put(.ENOTRECOVERABLE, "State not recoverable"); - map.put(.ENOTSOCK, "ipc/network software - argument errors. Socket operation on non-socket"); - map.put(.ENOTSUP, "Operation not supported"); - map.put(.ENOTTY, "Inappropriate ioctl for device"); - map.put(.ENXIO, "Device not configured"); - map.put(.EOVERFLOW, "Value too large to be stored in data type"); - map.put(.EOWNERDEAD, "Previous owner died"); - map.put(.EPERM, "Operation not permitted"); - map.put(.EPFNOSUPPORT, "Protocol family not supported"); - map.put(.EPIPE, "Broken pipe"); - map.put(.EPROCLIM, "quotas & mush. Too many processes"); - map.put(.EPROCUNAVAIL, "Bad procedure for program"); - map.put(.EPROGMISMATCH, "Program version wrong"); - map.put(.EPROGUNAVAIL, "RPC prog. not avail"); - map.put(.EPROTO, "Protocol error"); - map.put(.EPROTONOSUPPORT, "Protocol not supported"); - map.put(.EPROTOTYPE, "Protocol wrong type for socket"); - map.put(.EPWROFF, "Intelligent device errors. Device power is off"); - map.put(.EQFULL, "Interface output queue is full"); - map.put(.ERANGE, "Result too large"); - map.put(.EREMOTE, "Too many levels of remote in path"); - map.put(.EROFS, "Read-only file system"); - map.put(.ERPCMISMATCH, "RPC version wrong"); - map.put(.ESHLIBVERS, "Shared library version mismatch"); - map.put(.ESHUTDOWN, "Can’t send after socket shutdown"); - map.put(.ESOCKTNOSUPPORT, "Socket type not supported"); - map.put(.ESPIPE, "Illegal seek"); - map.put(.ESRCH, "No such process"); - map.put(.ESTALE, "Network File System. Stale NFS file handle"); - map.put(.ETIME, "STREAM ioctl timeout"); - map.put(.ETIMEDOUT, "Operation timed out"); - map.put(.ETOOMANYREFS, "Too many references: can't splice"); - map.put(.ETXTBSY, "Text file busy"); - map.put(.EUSERS, "Too many users"); - // map.put(.EWOULDBLOCK, "Operation would block"); - map.put(.EXDEV, "Cross-device link"); - break :brk map; - }; }; pub const UV_E2BIG: i32 = @intFromEnum(SystemErrno.E2BIG); diff --git a/src/fd.zig b/src/fd.zig index 508cbf39ec..24c36a965f 100644 --- a/src/fd.zig +++ b/src/fd.zig @@ -321,7 +321,12 @@ pub const FDImpl = packed struct { // If a non-number is given, returns null. // If the given number is not an fd (negative), an error is thrown and error.JSException is returned. pub fn fromJSValidated(value: JSValue, global: *JSC.JSGlobalObject) bun.JSError!?FDImpl { - if (!value.isAnyInt()) return null; + if (!value.isNumber()) { + return null; + } + if (!value.isAnyInt()) { + return global.ERR_OUT_OF_RANGE("The value of \"fd\" is out of range. It must be an integer. Received {}", .{bun.fmt.double(value.asNumber())}).throw(); + } const fd64 = value.toInt64(); try JSC.Node.Valid.fileDescriptor(fd64, global); const fd: i32 = @intCast(fd64); @@ -362,7 +367,7 @@ pub const FDImpl = packed struct { // ambiguous and almost certainly a mistake. You probably meant to format fd.cast(). // // Remember this formatter will - // - on posix, print the numebr + // - on posix, print the number // - on windows, print if it is a handle or a libuv file descriptor // - in debug on all platforms, print the path of the file descriptor // diff --git a/src/fmt.zig b/src/fmt.zig index 986a90bb8d..532abb7a99 100644 --- a/src/fmt.zig +++ b/src/fmt.zig @@ -1760,6 +1760,7 @@ pub const js_bindings = struct { } }; +// Equivalent to ERR_OUT_OF_RANGE from fn NewOutOfRangeFormatter(comptime T: type) type { return struct { value: T, @@ -1770,12 +1771,12 @@ fn NewOutOfRangeFormatter(comptime T: type) type { pub fn format(self: @This(), comptime _: []const u8, _: fmt.FormatOptions, writer: anytype) !void { try writer.writeAll("The value of \""); try writer.writeAll(self.field_name); - try writer.writeAll("\" "); + try writer.writeAll("\" is out of range. It "); const min = self.min; const max = self.max; if (min != std.math.maxInt(i64) and max != std.math.maxInt(i64)) { - try std.fmt.format(writer, "must be >= {d} and <= {d}.", .{ min, max }); + try std.fmt.format(writer, "must be >= {d} && <= {d}.", .{ min, max }); } else if (min != std.math.maxInt(i64)) { try std.fmt.format(writer, "must be >= {d}.", .{min}); } else if (max != std.math.maxInt(i64)) { @@ -1787,11 +1788,11 @@ fn NewOutOfRangeFormatter(comptime T: type) type { } if (comptime T == f64 or T == f32) { - try std.fmt.format(writer, " Received: {}", .{double(self.value)}); + try std.fmt.format(writer, " Received {}", .{double(self.value)}); } else if (comptime T == []const u8) { - try std.fmt.format(writer, " Received: {s}", .{self.value}); + try std.fmt.format(writer, " Received {s}", .{self.value}); } else { - try std.fmt.format(writer, " Received: {d}", .{self.value}); + try std.fmt.format(writer, " Received {d}", .{self.value}); } } }; diff --git a/src/install/install.zig b/src/install/install.zig index 065bb1ff02..93ff07b8d0 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -11360,10 +11360,7 @@ pub const PackageManager = struct { switch (bun.sys.File.toSource(package_json_path, manager.allocator)) { .result => |s| break :src s, .err => |e| { - Output.prettyError( - "error: failed to read package.json: {}\n", - .{e.withPath(package_json_path).toSystemError()}, - ); + Output.err(e, "failed to read {s}", .{bun.fmt.quote(package_json_path)}); Global.crash(); }, } @@ -11774,10 +11771,7 @@ pub const PackageManager = struct { switch (bun.sys.File.toSource(package_json_path, manager.allocator)) { .result => |s| break :brk s, .err => |e| { - Output.prettyError( - "error: failed to read package.json: {}\n", - .{e.withPath(package_json_path).toSystemError()}, - ); + Output.err(e, "failed to read {s}", .{bun.fmt.quote(package_json_path)}); Global.crash(); }, } @@ -11882,10 +11876,7 @@ pub const PackageManager = struct { const cache_dir_path = switch (bun.sys.getFdPath(bun.toFD(cache_dir.fd), &buf2)) { .result => |s| s, .err => |e| { - Output.prettyError( - "error: failed to read from cache {}\n", - .{e.toSystemError()}, - ); + Output.err(e, "failed to read from cache", .{}); Global.crash(); }, }; @@ -11896,10 +11887,7 @@ pub const PackageManager = struct { }; const random_tempdir = bun.span(bun.fs.FileSystem.instance.tmpname("node_modules_tmp", buf2[0..], bun.fastRandom()) catch |e| { - Output.prettyError( - "error: failed to make tempdir {s}\n", - .{@errorName(e)}, - ); + Output.err(e, "failed to make tempdir", .{}); Global.crash(); }); @@ -11910,10 +11898,7 @@ pub const PackageManager = struct { // will `rename()` it out and back again. const has_nested_node_modules = has_nested_node_modules: { var new_folder_handle = std.fs.cwd().openDir(new_folder, .{}) catch |e| { - Output.prettyError( - "error: failed to open directory {s} {s}\n", - .{ new_folder, @errorName(e) }, - ); + Output.err(e, "failed to open directory {s}", .{new_folder}); Global.crash(); }; defer new_folder_handle.close(); @@ -11930,10 +11915,7 @@ pub const PackageManager = struct { }; const patch_tag_tmpname = bun.span(bun.fs.FileSystem.instance.tmpname("patch_tmp", buf3[0..], bun.fastRandom()) catch |e| { - Output.prettyError( - "error: failed to make tempdir {s}\n", - .{@errorName(e)}, - ); + Output.err(e, "failed to make tempdir", .{}); Global.crash(); }); @@ -11951,10 +11933,7 @@ pub const PackageManager = struct { break :has_bun_patch_tag null; }; var new_folder_handle = std.fs.cwd().openDir(new_folder, .{}) catch |e| { - Output.prettyError( - "error: failed to open directory {s} {s}\n", - .{ new_folder, @errorName(e) }, - ); + Output.err(e, "failed to open directory {s}", .{new_folder}); Global.crash(); }; defer new_folder_handle.close(); @@ -12105,20 +12084,14 @@ pub const PackageManager = struct { )) { .result => |fd| fd, .err => |e| { - Output.prettyError( - "error: failed to open temp file {}\n", - .{e.toSystemError()}, - ); + Output.err(e, "failed to open temp file", .{}); Global.crash(); }, }; defer _ = bun.sys.close(tmpfd); if (bun.sys.File.writeAll(.{ .handle = tmpfd }, patchfile_contents.items).asErr()) |e| { - Output.prettyError( - "error: failed to write patch to temp file {}\n", - .{e.toSystemError()}, - ); + Output.err(e, "failed to write patch to temp file", .{}); Global.crash(); } @@ -12143,11 +12116,8 @@ pub const PackageManager = struct { const args = bun.JSC.Node.Arguments.Mkdir{ .path = .{ .string = bun.PathString.init(manager.options.patch_features.commit.patches_dir) }, }; - if (nodefs.mkdirRecursive(args, .sync).asErr()) |e| { - Output.prettyError( - "error: failed to make patches dir {}\n", - .{e.toSystemError()}, - ); + if (nodefs.mkdirRecursive(args).asErr()) |e| { + Output.err(e, "failed to make patches dir {}", .{bun.fmt.quote(args.path.slice())}); Global.crash(); } @@ -12159,10 +12129,7 @@ pub const PackageManager = struct { path_in_patches_dir, .{ .move_fallback = true }, ).asErr()) |e| { - Output.prettyError( - "error: failed renaming patch file to patches dir {}\n", - .{e.toSystemError()}, - ); + Output.err(e, "failed renaming patch file to patches dir", .{}); Global.crash(); } diff --git a/src/install/lockfile.zig b/src/install/lockfile.zig index 969a31c1dd..c8d16268cc 100644 --- a/src/install/lockfile.zig +++ b/src/install/lockfile.zig @@ -2508,7 +2508,7 @@ pub fn saveToDisk(this: *Lockfile, save_format: LoadResult.LockfileFormat, verbo const file = switch (File.openat(std.fs.cwd(), tmpname, bun.O.CREAT | bun.O.WRONLY, 0o777)) { .err => |err| { - Output.err(err, "failed to create temporary file to save lockfile\n{}", .{}); + Output.err(err, "failed to create temporary file to save lockfile", .{}); Global.crash(); }, .result => |f| f, @@ -2518,7 +2518,7 @@ pub fn saveToDisk(this: *Lockfile, save_format: LoadResult.LockfileFormat, verbo .err => |e| { file.close(); _ = bun.sys.unlink(tmpname); - Output.err(e, "failed to write lockfile\n{}", .{}); + Output.err(e, "failed to write lockfile", .{}); Global.crash(); }, .result => {}, @@ -2534,7 +2534,7 @@ pub fn saveToDisk(this: *Lockfile, save_format: LoadResult.LockfileFormat, verbo .err => |err| { file.close(); _ = bun.sys.unlink(tmpname); - Output.err(err, "failed to change lockfile permissions\n{}", .{}); + Output.err(err, "failed to change lockfile permissions", .{}); Global.crash(); }, .result => {}, diff --git a/src/install/patch_install.zig b/src/install/patch_install.zig index 842ca4a01f..50e7eb49ec 100644 --- a/src/install/patch_install.zig +++ b/src/install/patch_install.zig @@ -282,10 +282,10 @@ pub const PatchTask = struct { )) { .result => |txt| txt, .err => |e| { - try log.addErrorFmtOpts( + try log.addSysError( this.manager.allocator, - "failed to read patchfile: {}", - .{e.toSystemError()}, + e, + "failed to read patchfile", .{}, ); return; diff --git a/src/js/internal/test/binding.ts b/src/js/internal/test/binding.ts deleted file mode 100644 index a6240072ed..0000000000 --- a/src/js/internal/test/binding.ts +++ /dev/null @@ -1,77 +0,0 @@ -function internalBinding(name: string) { - switch (name) { - case "async_wrap": - case "buffer": - case "cares_wrap": - case "constants": - case "contextify": - case "config": - case "fs": - case "fs_event_wrap": - case "http_parser": - case "inspector": - case "os": - case "pipe_wrap": - case "process_wrap": - case "signal_wrap": - case "tcp_wrap": - case "tty_wrap": - case "udp_wrap": - case "url": - case "util": - case "uv": - case "v8": - case "zlib": - case "js_stream": { - // Public bindings - return (process as any).binding(name); - } - - case "blob": - case "block_list": - case "builtins": - case "credentials": - case "encoding_binding": - case "errors": - case "fs_dir": - case "heap_utils": - case "http2": - case "internal_only_v8": - case "js_udp_wrap": - case "messaging": - case "modules": - case "module_wrap": - case "mksnapshot": - case "options": - case "performance": - case "permission": - case "process_methods": - case "report": - case "sea": - case "serdes": - case "spawn_sync": - case "stream_pipe": - case "stream_wrap": - case "string_decoder": - case "symbols": - case "task_queue": - case "timers": - case "trace_events": - case "types": - case "wasi": - case "wasm_web_api": - case "watchdog": - case "worker": { - // Private bindings - throw new Error( - `Bun does not implement internal binding: ${name}. This being a node.js internal, it will not be implemented outside of usage in Node.js' test suite.`, - ); - } - - default: { - throw new Error(`No such binding: ${name}`); - } - } -} - -export { internalBinding }; diff --git a/src/js/node/fs.promises.ts b/src/js/node/fs.promises.ts index 5960cc01bf..98efb00193 100644 --- a/src/js/node/fs.promises.ts +++ b/src/js/node/fs.promises.ts @@ -21,6 +21,8 @@ const kDeserialize = Symbol("kDeserialize"); const kEmptyObject = ObjectFreeze({ __proto__: null }); const kFlag = Symbol("kFlag"); +const { validateObject } = require("internal/validators"); + function watch( filename: string | Buffer | URL, options: { encoding?: BufferEncoding; persistent?: boolean; recursive?: boolean; signal?: AbortSignal } = {}, @@ -35,7 +37,7 @@ function watch( } else if (Buffer.isBuffer(filename)) { filename = filename.toString(); } else if (typeof filename !== "string") { - throw new TypeError("Expected path to be a string or Buffer"); + throw $ERR_INVALID_ARG_TYPE("filename", ["string", "Buffer", "URL"], filename); } let nextEventResolve: Function | null = null; if (typeof options === "string") { @@ -149,7 +151,7 @@ const private_symbols = { kRef, kUnref, kFd, - FileHandle: null, + FileHandle: null as any, fs, }; @@ -158,13 +160,13 @@ const _writeFile = fs.writeFile.bind(fs); const _appendFile = fs.appendFile.bind(fs); const exports = { - access: fs.access.bind(fs), - appendFile: function (fileHandleOrFdOrPath, ...args) { + access: asyncWrap(fs.access, "access"), + appendFile: async function (fileHandleOrFdOrPath, ...args) { fileHandleOrFdOrPath = fileHandleOrFdOrPath?.[kFd] ?? fileHandleOrFdOrPath; return _appendFile(fileHandleOrFdOrPath, ...args); }, - close: fs.close.bind(fs), - copyFile: fs.copyFile.bind(fs), + close: asyncWrap(fs.close, "close"), + copyFile: asyncWrap(fs.copyFile, "copyFile"), cp, exists: async function exists() { try { @@ -173,32 +175,32 @@ const exports = { return false; } }, - chown: fs.chown.bind(fs), - chmod: fs.chmod.bind(fs), - fchmod: fs.fchmod.bind(fs), - fchown: fs.fchown.bind(fs), - fstat: fs.fstat.bind(fs), - fsync: fs.fsync.bind(fs), - fdatasync: fs.fdatasync.bind(fs), - ftruncate: fs.ftruncate.bind(fs), - futimes: fs.futimes.bind(fs), - lchmod: fs.lchmod.bind(fs), - lchown: fs.lchown.bind(fs), - link: fs.link.bind(fs), - lstat: fs.lstat.bind(fs), - mkdir: fs.mkdir.bind(fs), - mkdtemp: fs.mkdtemp.bind(fs), + chown: asyncWrap(fs.chown, "chown"), + chmod: asyncWrap(fs.chmod, "chmod"), + fchmod: asyncWrap(fs.fchmod, "fchmod"), + fchown: asyncWrap(fs.fchown, "fchown"), + fstat: asyncWrap(fs.fstat, "fstat"), + fsync: asyncWrap(fs.fsync, "fsync"), + fdatasync: asyncWrap(fs.fdatasync, "fdatasync"), + ftruncate: asyncWrap(fs.ftruncate, "ftruncate"), + futimes: asyncWrap(fs.futimes, "futimes"), + lchmod: asyncWrap(fs.lchmod, "lchmod"), + lchown: asyncWrap(fs.lchown, "lchown"), + link: asyncWrap(fs.link, "link"), + lstat: asyncWrap(fs.lstat, "lstat"), + mkdir: asyncWrap(fs.mkdir, "mkdir"), + mkdtemp: asyncWrap(fs.mkdtemp, "mkdtemp"), open: async (path, flags = "r", mode = 0o666) => { return new FileHandle(await fs.open(path, flags, mode), flags); }, - read: fs.read.bind(fs), - write: fs.write.bind(fs), - readdir: fs.readdir.bind(fs), + read: asyncWrap(fs.read, "read"), + write: asyncWrap(fs.write, "write"), + readdir: asyncWrap(fs.readdir, "readdir"), readFile: function (fileHandleOrFdOrPath, ...args) { fileHandleOrFdOrPath = fileHandleOrFdOrPath?.[kFd] ?? fileHandleOrFdOrPath; return _readFile(fileHandleOrFdOrPath, ...args); }, - writeFile: function (fileHandleOrFdOrPath, ...args) { + writeFile: function (fileHandleOrFdOrPath, ...args: any[]) { fileHandleOrFdOrPath = fileHandleOrFdOrPath?.[kFd] ?? fileHandleOrFdOrPath; if ( !$isTypedArrayView(args[0]) && @@ -207,21 +209,22 @@ const exports = { ) { $debug("fs.promises.writeFile async iterator slow path!"); // Node accepts an arbitrary async iterator here + // @ts-expect-error TODO return writeFileAsyncIterator(fileHandleOrFdOrPath, ...args); } return _writeFile(fileHandleOrFdOrPath, ...args); }, - readlink: fs.readlink.bind(fs), - realpath: fs.realpath.bind(fs), - rename: fs.rename.bind(fs), - stat: fs.stat.bind(fs), - symlink: fs.symlink.bind(fs), - truncate: fs.truncate.bind(fs), - unlink: fs.unlink.bind(fs), - utimes: fs.utimes.bind(fs), - lutimes: fs.lutimes.bind(fs), - rm: fs.rm.bind(fs), - rmdir: fs.rmdir.bind(fs), + readlink: asyncWrap(fs.readlink, "readlink"), + realpath: asyncWrap(fs.realpath, "realpath"), + rename: asyncWrap(fs.rename, "rename"), + stat: asyncWrap(fs.stat, "stat"), + symlink: asyncWrap(fs.symlink, "symlink"), + truncate: asyncWrap(fs.truncate, "truncate"), + unlink: asyncWrap(fs.unlink, "unlink"), + utimes: asyncWrap(fs.utimes, "utimes"), + lutimes: asyncWrap(fs.lutimes, "lutimes"), + rm: asyncWrap(fs.rm, "rm"), + rmdir: asyncWrap(fs.rmdir, "rmdir"), writev: async (fd, buffers, position) => { var bytesWritten = await fs.writev(fd, buffers, position); return { @@ -247,6 +250,15 @@ const exports = { }; export default exports; +function asyncWrap(fn: any, name: string) { + const wrapped = async function (...args) { + return fn.$apply(fs, args); + }; + Object.defineProperty(wrapped, "name", { value: name }); + Object.defineProperty(wrapped, "length", { value: fn.length }); + return wrapped; +} + { const { writeFile, @@ -264,6 +276,7 @@ export default exports; writev, close, } = exports; + let isArrayBufferView; // Partially taken from https://github.com/nodejs/node/blob/c25878d370/lib/internal/fs/promises.js#L148 // These functions await the result so that errors propagate correctly with @@ -291,9 +304,9 @@ export default exports; [kClosePromise]; [kRefs]; - async appendFile(data, options: object | string | undefined) { + async appendFile(data, options) { const fd = this[kFd]; - throwEBADFIfNecessary(writeFile, fd); + throwEBADFIfNecessary("writeFile", fd); let encoding = "utf8"; let flush = false; @@ -315,7 +328,7 @@ export default exports; async chmod(mode) { const fd = this[kFd]; - throwEBADFIfNecessary(fchmod, fd); + throwEBADFIfNecessary("fchmod", fd); try { this[kRef](); @@ -327,7 +340,7 @@ export default exports; async chown(uid, gid) { const fd = this[kFd]; - throwEBADFIfNecessary(fchown, fd); + throwEBADFIfNecessary("fchown", fd); try { this[kRef](); @@ -339,7 +352,7 @@ export default exports; async datasync() { const fd = this[kFd]; - throwEBADFIfNecessary(fdatasync, fd); + throwEBADFIfNecessary("fdatasync", fd); try { this[kRef](); @@ -351,7 +364,7 @@ export default exports; async sync() { const fd = this[kFd]; - throwEBADFIfNecessary(fsync, fd); + throwEBADFIfNecessary("fsync", fd); try { this[kRef](); @@ -363,7 +376,21 @@ export default exports; async read(buffer, offset, length, position) { const fd = this[kFd]; - throwEBADFIfNecessary(read, fd); + throwEBADFIfNecessary("read", fd); + + isArrayBufferView ??= require("node:util/types").isArrayBufferView; + if (!isArrayBufferView(buffer)) { + // This is fh.read(params) + if (buffer != undefined) { + validateObject(buffer, "options"); + } + ({ buffer = Buffer.alloc(16384), offset = 0, length, position = null } = buffer ?? {}); + } + length = length ?? buffer?.byteLength - offset; + + if (length === 0) { + return { buffer, bytesRead: 0 }; + } try { this[kRef](); @@ -375,7 +402,7 @@ export default exports; async readv(buffers, position) { const fd = this[kFd]; - throwEBADFIfNecessary(readv, fd); + throwEBADFIfNecessary("readv", fd); try { this[kRef](); @@ -387,7 +414,7 @@ export default exports; async readFile(options) { const fd = this[kFd]; - throwEBADFIfNecessary(readFile, fd); + throwEBADFIfNecessary("readFile", fd); try { this[kRef](); @@ -403,7 +430,7 @@ export default exports; async stat(options) { const fd = this[kFd]; - throwEBADFIfNecessary(fstat, fd); + throwEBADFIfNecessary("fstat", fd); try { this[kRef](); @@ -415,7 +442,7 @@ export default exports; async truncate(len = 0) { const fd = this[kFd]; - throwEBADFIfNecessary(ftruncate, fd); + throwEBADFIfNecessary("ftruncate", fd); try { this[kRef](); @@ -427,7 +454,7 @@ export default exports; async utimes(atime, mtime) { const fd = this[kFd]; - throwEBADFIfNecessary(futimes, fd); + throwEBADFIfNecessary("futimes", fd); try { this[kRef](); @@ -439,8 +466,22 @@ export default exports; async write(buffer, offset, length, position) { const fd = this[kFd]; - throwEBADFIfNecessary(write, fd); + throwEBADFIfNecessary("write", fd); + if (buffer?.byteLength === 0) return { __proto__: null, bytesWritten: 0, buffer }; + + isArrayBufferView ??= require("node:util/types").isArrayBufferView; + if (isArrayBufferView(buffer)) { + if (typeof offset === "object") { + ({ offset = 0, length = buffer.byteLength - offset, position = null } = offset ?? kEmptyObject); + } + + if (offset == null) { + offset = 0; + } + if (typeof length !== "number") length = buffer.byteLength - offset; + if (typeof position !== "number") position = null; + } try { this[kRef](); return { buffer, bytesWritten: await write(fd, buffer, offset, length, position) }; @@ -451,7 +492,7 @@ export default exports; async writev(buffers, position) { const fd = this[kFd]; - throwEBADFIfNecessary(writev, fd); + throwEBADFIfNecessary("writev", fd); try { this[kRef](); @@ -461,9 +502,9 @@ export default exports; } } - async writeFile(data: string, options: object | string | undefined = "utf8") { + async writeFile(data: string, options: any = "utf8") { const fd = this[kFd]; - throwEBADFIfNecessary(writeFile, fd); + throwEBADFIfNecessary("writeFile", fd); let encoding: string = "utf8"; if (options == null || typeof options === "function") { @@ -520,14 +561,14 @@ export default exports; readableWebStream(options = kEmptyObject) { const fd = this[kFd]; - throwEBADFIfNecessary(fs.createReadStream, fd); + throwEBADFIfNecessary("fs".createReadStream, fd); return Bun.file(fd).stream(); } createReadStream(options = kEmptyObject) { const fd = this[kFd]; - throwEBADFIfNecessary(fs.createReadStream, fd); + throwEBADFIfNecessary("fs".createReadStream, fd); return require("node:fs").createReadStream("", { fd: this, highWaterMark: 64 * 1024, @@ -537,7 +578,7 @@ export default exports; createWriteStream(options = kEmptyObject) { const fd = this[kFd]; - throwEBADFIfNecessary(fs.createWriteStream, fd); + throwEBADFIfNecessary("fs".createWriteStream, fd); return require("node:fs").createWriteStream("", { fd: this, ...options, @@ -569,13 +610,12 @@ export default exports; }); } -function throwEBADFIfNecessary(fn, fd) { +function throwEBADFIfNecessary(fn: string, fd) { if (fd === -1) { - // eslint-disable-next-line no-restricted-syntax - const err = new Error("Bad file descriptor"); + const err: any = new Error("Bad file descriptor"); err.code = "EBADF"; err.name = "SystemError"; - err.syscall = fn.name; + err.syscall = fn; throw err; } } diff --git a/src/js/node/fs.ts b/src/js/node/fs.ts index 88b1b7aab3..e237064b55 100644 --- a/src/js/node/fs.ts +++ b/src/js/node/fs.ts @@ -9,16 +9,14 @@ const { ERR_INVALID_ARG_TYPE, ERR_OUT_OF_RANGE } = require("internal/errors"); const { validateInteger } = require("internal/validators"); const NumberIsFinite = Number.isFinite; -const DateNow = Date.now; const DatePrototypeGetTime = Date.prototype.getTime; const isDate = types.isDate; const ObjectSetPrototypeOf = Object.setPrototypeOf; // Private exports +// `fs` points to the return value of `node_fs_binding.zig`'s `createBinding` function. const { FileHandle, kRef, kUnref, kFd, fs } = promises.$data; -// reusing a different private symbol -// this points to `node_fs_binding.zig`'s `createBinding` function. const constants = $processBindingConstants.fs; var _writeStreamPathFastPathSymbol = Symbol.for("Bun.NodeWriteStreamFastPath"); @@ -112,18 +110,18 @@ class FSWatcher extends EventEmitter { } /** Implemented in `node_fs_stat_watcher.zig` */ -// interface StatWatcherHandle { -// ref(); -// unref(); -// close(); -// } +interface StatWatcherHandle { + ref(); + unref(); + close(); +} function openAsBlob(path, options) { return Promise.$resolve(Bun.file(path, options)); } class StatWatcher extends EventEmitter { - // _handle: StatWatcherHandle; + _handle: StatWatcherHandle | null; constructor(path, options) { super(); @@ -158,8 +156,7 @@ var access = function access(path, mode, callback) { } ensureCallback(callback); - - fs.access(path, mode).then(nullcallback(callback), callback); + fs.access(path, mode).then(callback, callback); }, appendFile = function appendFile(path, data, options, callback) { if (!$isCallable(callback)) { @@ -173,8 +170,8 @@ var access = function access(path, mode, callback) { }, close = function close(fd, callback) { if ($isCallable(callback)) { - fs.close(fd).then(() => callback(), callback); - } else if (callback == undefined) { + fs.close(fd).then(() => callback(null), callback); + } else if (callback === undefined) { fs.close(fd).then(() => {}); } else { callback = ensureCallback(callback); @@ -269,11 +266,11 @@ var access = function access(path, mode, callback) { fs.futimes(fd, atime, mtime).then(nullcallback(callback), callback); }, - lchmod = function lchmod(path, mode, callback) { + lchmod = constants.O_SYMLINK !== undefined ? function lchmod(path, mode, callback) { ensureCallback(callback); fs.lchmod(path, mode).then(nullcallback(callback), callback); - }, + } : undefined, // lchmod is only available on macOS lchown = function lchown(path, uid, gid, callback) { ensureCallback(callback); @@ -430,7 +427,7 @@ var access = function access(path, mode, callback) { ensureCallback(callback); - fs.realpath(p, options).then(function (resolvedPath) { + fs.realpath(p, options, false).then(function (resolvedPath) { callback(null, resolvedPath); }, callback); }, @@ -523,7 +520,7 @@ var access = function access(path, mode, callback) { fsyncSync = fs.fsyncSync.bind(fs), ftruncateSync = fs.ftruncateSync.bind(fs), futimesSync = fs.futimesSync.bind(fs), - lchmodSync = fs.lchmodSync.bind(fs), + lchmodSync = constants.O_SYMLINK !== undefined ? fs.lchmodSync.bind(fs) : undefined, // lchmod is only available on macOS lchownSync = fs.lchownSync.bind(fs), linkSync = fs.linkSync.bind(fs), lstatSync = fs.lstatSync.bind(fs), @@ -587,11 +584,8 @@ var access = function access(path, mode, callback) { }, callback); }; -// TODO: make symbols a separate export somewhere var kCustomPromisifiedSymbol = Symbol.for("nodejs.util.promisify.custom"); - exists[kCustomPromisifiedSymbol] = path => new Promise(resolve => exists(path, resolve)); - read[kCustomPromisifiedSymbol] = async function (fd, bufferOrOptions, ...rest) { const { isArrayBufferView } = require("node:util/types"); let buffer; @@ -610,11 +604,12 @@ read[kCustomPromisifiedSymbol] = async function (fd, bufferOrOptions, ...rest) { return { bytesRead, buffer }; }; - write[kCustomPromisifiedSymbol] = async function (fd, stringOrBuffer, ...rest) { const bytesWritten = await fs.write(fd, stringOrBuffer, ...rest); return { bytesWritten, buffer: stringOrBuffer }; }; +writev[kCustomPromisifiedSymbol] = promises.writev; +readv[kCustomPromisifiedSymbol] = promises.readv; // TODO: move this entire thing into native code. // the reason it's not done right now is because there isnt a great way to have multiple @@ -651,7 +646,7 @@ function unwatchFile(filename, listener) { filename = getValidatedPath(filename); var stat = statWatchers.get(filename); - if (!stat) return; + if (!stat) return throwIfNullBytesInFileName(filename); if (listener) { stat.removeListener("change", listener); if (stat.listenerCount("change") !== 0) { @@ -664,20 +659,9 @@ function unwatchFile(filename, listener) { statWatchers.delete(filename); } -function callbackify(fsFunction, args) { - const callback = args[args.length - 1]; - try { - var result = fsFunction.$apply(fs, args.slice(0, args.length - 1)); - result.then( - (...args) => callback(null, ...args), - err => callback(err), - ); - } catch (e) { - if (typeof callback === "function") { - callback(e); - } else { - throw e; - } +function throwIfNullBytesInFileName(filename: string) { + if (filename.indexOf("\u0000") !== -1) { + throw $ERR_INVALID_ARG_VALUE("path", "string without null bytes", filename); } } @@ -781,6 +765,11 @@ function ReadStream(this: typeof ReadStream, pathOrFd, options) { fd = defaultReadStreamOptions.fd, }: Partial = options; + if (encoding && !Buffer.isEncoding(encoding)) { + const reason = "is invalid encoding"; + throw $ERR_INVALID_ARG_VALUE("encoding", encoding, reason); + } + if (pathOrFd?.constructor?.name === "URL") { pathOrFd = Bun.fileURLToPath(pathOrFd); } @@ -1061,9 +1050,13 @@ var defaultWriteStreamOptions = { }, }; -var WriteStreamClass = (WriteStream = function WriteStream(path, options = defaultWriteStreamOptions) { +var WriteStreamClass = (WriteStream = function WriteStream(path, options: any = defaultWriteStreamOptions) { if (!(this instanceof WriteStream)) { - return new WriteStream(path, options); + return new (WriteStream as any)(path, options); + } + + if (typeof options === "string") { + options = { encoding: options }; } if (!options) { @@ -1088,6 +1081,11 @@ var WriteStreamClass = (WriteStream = function WriteStream(path, options = defau options.pos = start; } + if (encoding && !Buffer.isEncoding(encoding)) { + const reason = "is invalid encoding"; + throw $ERR_INVALID_ARG_VALUE("encoding", encoding, reason); + } + var tempThis = {}; var handle = null; if (fd != null) { @@ -1386,9 +1384,20 @@ Object.defineProperties(fs, { }, }); -// lol -realpath.native = realpath; -realpathSync.native = realpathSync; +// @ts-ignore +realpath.native = function realpath(p, options, callback) { + if ($isCallable(options)) { + callback = options; + options = undefined; + } + + ensureCallback(callback); + + fs.realpathNative(p, options).then(function (resolvedPath) { + callback(null, resolvedPath); + }, callback); +}; +realpathSync.native = fs.realpathNativeSync.bind(fs); // attempt to use the native code version if possible // and on MacOS, simple cases of recursive directory trees can be done in a single `clonefile()` @@ -1415,19 +1424,21 @@ function cp(src, dest, options, callback) { promises.cp(src, dest, options).then(() => callback(), callback); } -function _toUnixTimestamp(time, name = "time") { +function _toUnixTimestamp(time: any, name = "time") { + // @ts-ignore if (typeof time === "string" && +time == time) { return +time; } - if (NumberIsFinite(time)) { + // @ts-ignore + if ($isFinite(time)) { if (time < 0) { - return DateNow() / 1000; + return Date.now() / 1000; } return time; } if (isDate(time)) { // Convert to 123.456 UNIX timestamp - return DatePrototypeGetTime(time) / 1000; + return time.getTime() / 1000; } throw new TypeError(`Expected ${name} to be a number or Date`); } @@ -1583,8 +1594,8 @@ setName(ftruncate, "ftruncate"); setName(ftruncateSync, "ftruncateSync"); setName(futimes, "futimes"); setName(futimesSync, "futimesSync"); -setName(lchmod, "lchmod"); -setName(lchmodSync, "lchmodSync"); +if (lchmod) setName(lchmod, "lchmod"); +if (lchmodSync) setName(lchmodSync, "lchmodSync"); setName(lchown, "lchown"); setName(lchownSync, "lchownSync"); setName(link, "link"); diff --git a/src/js/node/readline.ts b/src/js/node/readline.ts index 8cd6e67421..7bea6f227e 100644 --- a/src/js/node/readline.ts +++ b/src/js/node/readline.ts @@ -27,6 +27,7 @@ // ---------------------------------------------------------------------------- const EventEmitter = require("node:events"); const { StringDecoder } = require("node:string_decoder"); +const { promisify } = require("internal/promisify"); const { validateFunction, @@ -40,9 +41,6 @@ const { const internalGetStringWidth = $newZigFunction("string.zig", "String.jsGetStringWidth", 1); -const ObjectGetPrototypeOf = Object.getPrototypeOf; -const ObjectGetOwnPropertyDescriptors = Object.getOwnPropertyDescriptors; -const ObjectValues = Object.values; const PromiseReject = Promise.reject; var isWritable; @@ -158,72 +156,6 @@ function stripVTControlCharacters(str) { return RegExpPrototypeSymbolReplace.$call(ansi, str, ""); } -// Promisify - -var kCustomPromisifiedSymbol = SymbolFor("nodejs.util.promisify.custom"); -var kCustomPromisifyArgsSymbol = Symbol("customPromisifyArgs"); - -function promisify(original) { - validateFunction(original, "original"); - - if (original[kCustomPromisifiedSymbol]) { - let fn = original[kCustomPromisifiedSymbol]; - - validateFunction(fn, "util.promisify.custom"); - - return ObjectDefineProperty(fn, kCustomPromisifiedSymbol, { - __proto__: null, - value: fn, - enumerable: false, - writable: false, - configurable: true, - }); - } - - // Names to create an object from in case the callback receives multiple - // arguments, e.g. ['bytesRead', 'buffer'] for fs.read. - var argumentNames = original[kCustomPromisifyArgsSymbol]; - - function fn(...args) { - return new Promise((resolve, reject) => { - ArrayPrototypePush.$call(args, (err, ...values) => { - if (err) { - return reject(err); - } - if (argumentNames !== undefined && values.length > 1) { - var obj = {}; - for (var i = 0; i < argumentNames.length; i++) obj[argumentNames[i]] = values[i]; - resolve(obj); - } else { - resolve(values[0]); - } - }); - original.$apply(this, args); - }); - } - - ObjectSetPrototypeOf(fn, ObjectGetPrototypeOf(original)); - - ObjectDefineProperty(fn, kCustomPromisifiedSymbol, { - __proto__: null, - value: fn, - enumerable: false, - writable: false, - configurable: true, - }); - - var descriptors = ObjectGetOwnPropertyDescriptors(original); - var propertiesValues = ObjectValues(descriptors); - for (var i = 0; i < propertiesValues.length; i++) { - // We want to use null-prototype objects to not rely on globally mutable - // %Object.prototype%. - ObjectSetPrototypeOf(propertiesValues[i], null); - } - return ObjectDefineProperties(fn, descriptors); -} - -promisify.custom = kCustomPromisifiedSymbol; - // Constants const kUTF16SurrogateThreshold = 0x10000; // 2 ** 16 diff --git a/src/js/node/stream.consumers.ts b/src/js/node/stream.consumers.ts index d56c456bb4..c03427bc09 100644 --- a/src/js/node/stream.consumers.ts +++ b/src/js/node/stream.consumers.ts @@ -1,18 +1,39 @@ // Hardcoded module "node:stream/consumers" / "readable-stream/consumer" -const arrayBuffer = Bun.readableStreamToArrayBuffer; -const bytes = Bun.readableStreamToBytes; -const text = Bun.readableStreamToText; -const json = stream => Bun.readableStreamToText(stream).then(JSON.parse); - -const buffer = async readableStream => { - return new Buffer(await arrayBuffer(readableStream)); -}; - -const blob = Bun.readableStreamToBlob; +export async function arrayBuffer(stream): Promise { + if ($isReadableStream(stream)) return Bun.readableStreamToArrayBuffer(stream); + const chunks: any[] = []; + for await (const chunk of stream) chunks.push(chunk); + return Buffer.concat(chunks).buffer as ArrayBuffer; +} +export async function text(stream): Promise { + if ($isReadableStream(stream)) return Bun.readableStreamToText(stream); + const dec = new TextDecoder(); + let str = ""; + for await (const chunk of stream) { + if (typeof chunk === "string") str += chunk; + else str += dec.decode(chunk, { stream: true }); + } + // Flush the streaming TextDecoder so that any pending + // incomplete multibyte characters are handled. + str += dec.decode(undefined, { stream: false }); + return str; +} +export async function json(stream): Promise { + if ($isReadableStream(stream)) return Bun.readableStreamToJSON(stream).then(JSON.parse); + return JSON.parse(await text(stream)); +} +export async function buffer(stream): Promise { + return new Buffer(await arrayBuffer(stream)); +} +async function blob(stream) { + if ($isReadableStream(stream)) return Bun.readableStreamToBlob(stream).then(JSON.parse); + const chunks: any[] = []; + for await (const chunk of stream) chunks.push(chunk); + return new Blob(chunks); +} export default { arrayBuffer, - bytes, text, json, buffer, diff --git a/src/js/node/util.ts b/src/js/node/util.ts index 8023c24647..e2498b85e1 100644 --- a/src/js/node/util.ts +++ b/src/js/node/util.ts @@ -316,17 +316,43 @@ function aborted(signal: AbortSignal, resource: object) { } cjs_exports = { - format, - formatWithOptions, - stripVTControlCharacters, - deprecate, + // This is in order of `node --print 'Object.keys(util)'` + // _errnoException, + // _exceptionWithHostPort, + _extend, + callbackify, debug: debuglog, debuglog, - _extend, + deprecate, + format, + styleText, + formatWithOptions, + // getCallSite, + // getCallSites, + // getSystemErrorMap, + getSystemErrorName, + // getSystemErrorMessage, + inherits, inspect, + isDeepStrictEqual, + promisify, + stripVTControlCharacters, + toUSVString, + // transferableAbortSignal, + // transferableAbortController, + aborted, types, + // parseEnv, + parseArgs, + TextDecoder, + TextEncoder, + // MIMEType, + // MIMEParams, + + // Deprecated in Node.js 22, removed in 23 isArray: $isArray, isBoolean, + isBuffer, isNull, isNullOrUndefined, isNumber, @@ -336,22 +362,10 @@ cjs_exports = { isRegExp, isObject, isDate, - isFunction, isError, + isFunction, isPrimitive, - isBuffer, log, - inherits, - toUSVString, - promisify, - callbackify, - isDeepStrictEqual, - TextDecoder, - TextEncoder, - parseArgs, - styleText, - getSystemErrorName, - aborted, }; export default cjs_exports; diff --git a/src/linux_c.zig b/src/linux_c.zig index d29d846da3..0709ca04f4 100644 --- a/src/linux_c.zig +++ b/src/linux_c.zig @@ -152,146 +152,6 @@ pub const SystemErrno = enum(u8) { if (code >= max) return null; return @enumFromInt(code); } - - pub fn label(this: SystemErrno) ?[:0]const u8 { - return labels.get(this) orelse null; - } - - const LabelMap = std.EnumMap(SystemErrno, [:0]const u8); - pub const labels: LabelMap = brk: { - var map: LabelMap = LabelMap.initFull(""); - - map.put(.EPERM, "Operation not permitted"); - map.put(.ENOENT, "No such file or directory"); - map.put(.ESRCH, "No such process"); - map.put(.EINTR, "Interrupted system call"); - map.put(.EIO, "I/O error"); - map.put(.ENXIO, "No such device or address"); - map.put(.E2BIG, "Argument list too long"); - map.put(.ENOEXEC, "Exec format error"); - map.put(.EBADF, "Bad file descriptor"); - map.put(.ECHILD, "No child processes"); - map.put(.EAGAIN, "Try again"); - map.put(.ENOMEM, "Out of memory"); - map.put(.EACCES, "Permission denied"); - map.put(.EFAULT, "Bad address"); - map.put(.ENOTBLK, "Block device required"); - map.put(.EBUSY, "Device or resource busy"); - map.put(.EEXIST, "File or folder exists"); - map.put(.EXDEV, "Cross-device link"); - map.put(.ENODEV, "No such device"); - map.put(.ENOTDIR, "Not a directory"); - map.put(.EISDIR, "Is a directory"); - map.put(.EINVAL, "Invalid argument"); - map.put(.ENFILE, "File table overflow"); - map.put(.EMFILE, "Too many open files"); - map.put(.ENOTTY, "Not a typewriter"); - map.put(.ETXTBSY, "Text file busy"); - map.put(.EFBIG, "File too large"); - map.put(.ENOSPC, "No space left on device"); - map.put(.ESPIPE, "Illegal seek"); - map.put(.EROFS, "Read-only file system"); - map.put(.EMLINK, "Too many links"); - map.put(.EPIPE, "Broken pipe"); - map.put(.EDOM, "Math argument out of domain of func"); - map.put(.ERANGE, "Math result not representable"); - map.put(.EDEADLK, "Resource deadlock would occur"); - map.put(.ENAMETOOLONG, "File name too long"); - map.put(.ENOLCK, "No record locks available"); - map.put(.ENOSYS, "Function not implemented"); - map.put(.ENOTEMPTY, "Directory not empty"); - map.put(.ELOOP, "Too many symbolic links encountered"); - map.put(.ENOMSG, "No message of desired type"); - map.put(.EIDRM, "Identifier removed"); - map.put(.ECHRNG, "Channel number out of range"); - map.put(.EL2NSYNC, "Level 2 not synchronized"); - map.put(.EL3HLT, "Level 3 halted"); - map.put(.EL3RST, "Level 3 reset"); - map.put(.ELNRNG, "Link number out of range"); - map.put(.EUNATCH, "Protocol driver not attached"); - map.put(.ENOCSI, "No CSI structure available"); - map.put(.EL2HLT, "Level 2 halted"); - map.put(.EBADE, "Invalid exchange"); - map.put(.EBADR, "Invalid request descriptor"); - map.put(.EXFULL, "Exchange full"); - map.put(.ENOANO, "No anode"); - map.put(.EBADRQC, "Invalid request code"); - map.put(.EBADSLT, "Invalid slot"); - map.put(.EBFONT, "Bad font file format"); - map.put(.ENOSTR, "Device not a stream"); - map.put(.ENODATA, "No data available"); - map.put(.ETIME, "Timer expired"); - map.put(.ENOSR, "Out of streams resources"); - map.put(.ENONET, "Machine is not on the network"); - map.put(.ENOPKG, "Package not installed"); - map.put(.EREMOTE, "Object is remote"); - map.put(.ENOLINK, "Link has been severed"); - map.put(.EADV, "Advertise error"); - map.put(.ESRMNT, "Srmount error"); - map.put(.ECOMM, "Communication error on send"); - map.put(.EPROTO, "Protocol error"); - map.put(.EMULTIHOP, "Multihop attempted"); - map.put(.EDOTDOT, "RFS specific error"); - map.put(.EBADMSG, "Not a data message"); - map.put(.EOVERFLOW, "Value too large for defined data type"); - map.put(.ENOTUNIQ, "Name not unique on network"); - map.put(.EBADFD, "File descriptor in bad state"); - map.put(.EREMCHG, "Remote address changed"); - map.put(.ELIBACC, "Can not access a needed shared library"); - map.put(.ELIBBAD, "Accessing a corrupted shared library"); - map.put(.ELIBSCN, "lib section in a.out corrupted"); - map.put(.ELIBMAX, "Attempting to link in too many shared libraries"); - map.put(.ELIBEXEC, "Cannot exec a shared library directly"); - map.put(.EILSEQ, "Illegal byte sequence"); - map.put(.ERESTART, "Interrupted system call should be restarted"); - map.put(.ESTRPIPE, "Streams pipe error"); - map.put(.EUSERS, "Too many users"); - map.put(.ENOTSOCK, "Socket operation on non-socket"); - map.put(.EDESTADDRREQ, "Destination address required"); - map.put(.EMSGSIZE, "Message too long"); - map.put(.EPROTOTYPE, "Protocol wrong type for socket"); - map.put(.ENOPROTOOPT, "Protocol not available"); - map.put(.EPROTONOSUPPORT, "Protocol not supported"); - map.put(.ESOCKTNOSUPPORT, "Socket type not supported"); - map.put(.ENOTSUP, "Operation not supported on transport endpoint"); - map.put(.EPFNOSUPPORT, "Protocol family not supported"); - map.put(.EAFNOSUPPORT, "Address family not supported by protocol"); - map.put(.EADDRINUSE, "Address already in use"); - map.put(.EADDRNOTAVAIL, "Cannot assign requested address"); - map.put(.ENETDOWN, "Network is down"); - map.put(.ENETUNREACH, "Network is unreachable"); - map.put(.ENETRESET, "Network dropped connection because of reset"); - map.put(.ECONNABORTED, "Software caused connection abort"); - map.put(.ECONNRESET, "Connection reset by peer"); - map.put(.ENOBUFS, "No buffer space available"); - map.put(.EISCONN, "Transport endpoint is already connected"); - map.put(.ENOTCONN, "Transport endpoint is not connected"); - map.put(.ESHUTDOWN, "Cannot send after transport endpoint shutdown"); - map.put(.ETOOMANYREFS, "Too many references: cannot splice"); - map.put(.ETIMEDOUT, "Connection timed out"); - map.put(.ECONNREFUSED, "Connection refused"); - map.put(.EHOSTDOWN, "Host is down"); - map.put(.EHOSTUNREACH, "No route to host"); - map.put(.EALREADY, "Operation already in progress"); - map.put(.EINPROGRESS, "Operation now in progress"); - map.put(.ESTALE, "Stale NFS file handle"); - map.put(.EUCLEAN, "Structure needs cleaning"); - map.put(.ENOTNAM, "Not a XENIX named type file"); - map.put(.ENAVAIL, "No XENIX semaphores available"); - map.put(.EISNAM, "Is a named type file"); - map.put(.EREMOTEIO, "Remote I/O error"); - map.put(.EDQUOT, "Quota exceeded"); - map.put(.ENOMEDIUM, "No medium found"); - map.put(.EMEDIUMTYPE, "Wrong medium type"); - map.put(.ECANCELED, "Operation Canceled"); - map.put(.ENOKEY, "Required key not available"); - map.put(.EKEYEXPIRED, "Key has expired"); - map.put(.EKEYREVOKED, "Key has been revoked"); - map.put(.EKEYREJECTED, "Key was rejected by service"); - map.put(.EOWNERDEAD, "Owner died"); - map.put(.ENOTRECOVERABLE, "State not recoverable"); - break :brk map; - }; }; pub const UV_E2BIG: i32 = @intFromEnum(SystemErrno.E2BIG); diff --git a/src/logger.zig b/src/logger.zig index d0af8cb71e..dd6459162f 100644 --- a/src/logger.zig +++ b/src/logger.zig @@ -984,6 +984,21 @@ pub const Log = struct { }); } + // Use a bun.sys.Error's message in addition to some extra context. + pub fn addSysError(log: *Log, alloc: std.mem.Allocator, e: bun.sys.Error, comptime fmt: string, args: anytype) OOM!void { + const tag_name, const sys_errno = e.getErrorCodeTagName() orelse { + try log.addErrorFmt(null, Loc.Empty, alloc, fmt, args); + return; + }; + try log.addErrorFmt( + null, + Loc.Empty, + alloc, + "{s}: " ++ fmt, + .{bun.sys.coreutils_error_map.get(sys_errno) orelse tag_name} ++ args, + ); + } + pub fn addZigErrorWithNote(log: *Log, allocator: std.mem.Allocator, err: anyerror, comptime noteFmt: string, args: anytype) OOM!void { @setCold(true); log.errors += 1; diff --git a/src/node_fallbacks.zig b/src/node_fallbacks.zig index ae9785e7d0..01370964f5 100644 --- a/src/node_fallbacks.zig +++ b/src/node_fallbacks.zig @@ -29,7 +29,7 @@ pub const FallbackModule = struct { return @embedFile(code_path); } - return bun.runtimeEmbedFile(.codegen_eager, code_path); + return bun.runtimeEmbedFile(.codegen, code_path); } }; diff --git a/src/output.zig b/src/output.zig index ce10ab3950..20abdd9cde 100644 --- a/src/output.zig +++ b/src/output.zig @@ -1063,12 +1063,20 @@ pub inline fn err(error_name: anytype, comptime fmt: []const u8, args: anytype) const info = @typeInfo(T); if (comptime T == bun.sys.Error or info == .Pointer and info.Pointer.child == bun.sys.Error) { - prettyErrorln("error:: " ++ fmt, args ++ .{error_name}); + const e: bun.sys.Error = error_name; + const tag_name, const sys_errno = e.getErrorCodeTagName() orelse { + err("unknown error", fmt, args); + return; + }; + if (bun.sys.coreutils_error_map.get(sys_errno)) |label| { + prettyErrorln("{s}: {s}: " ++ fmt ++ " ({s})", .{ tag_name, label } ++ args ++ .{@tagName(e.syscall)}); + } else { + prettyErrorln("{s}: " ++ fmt ++ " ({s})", .{tag_name} ++ args ++ .{@tagName(e.syscall)}); + } return; } const display_name, const is_comptime_name = display_name: { - // Zig string literals are of type *const [n:0]u8 // we assume that no one will pass this type from not using a string literal. if (info == .Pointer and info.Pointer.size == .One and info.Pointer.is_const) { diff --git a/src/patch.zig b/src/patch.zig index 363184f47e..eaddbc26c9 100644 --- a/src/patch.zig +++ b/src/patch.zig @@ -75,7 +75,7 @@ pub const PatchFile = struct { } }; - pub fn apply(this: *const PatchFile, allocator: Allocator, patch_dir: bun.FileDescriptor) ?JSC.SystemError { + pub fn apply(this: *const PatchFile, allocator: Allocator, patch_dir: bun.FileDescriptor) ?bun.sys.Error { var state: ApplyState = .{}; var sfb = std.heap.stackFallback(1024, allocator); var arena = bun.ArenaAllocator.init(sfb.get()); @@ -87,7 +87,7 @@ pub const PatchFile = struct { const pathz = arena.allocator().dupeZ(u8, part.file_deletion.path) catch bun.outOfMemory(); if (bun.sys.unlinkat(patch_dir, pathz).asErr()) |e| { - return e.withPath(pathz).toSystemError(); + return e.withPath(pathz); } }, .file_rename => { @@ -97,7 +97,7 @@ pub const PatchFile = struct { if (std.fs.path.dirname(to_path)) |todir| { const abs_patch_dir = switch (state.patchDirAbsPath(patch_dir)) { .result => |p| p, - .err => |e| return e.toSystemError(), + .err => |e| return e, }; const path_to_make = bun.path.joinZ(&[_][]const u8{ abs_patch_dir, @@ -108,11 +108,11 @@ pub const PatchFile = struct { .path = .{ .string = bun.PathString.init(path_to_make) }, .recursive = true, .mode = 0o755, - }, .sync).asErr()) |e| return e.toSystemError(); + }).asErr()) |e| return e; } if (bun.sys.renameat(patch_dir, from_path, patch_dir, to_path).asErr()) |e| { - return e.toSystemError(); + return e; } }, .file_creation => { @@ -126,7 +126,7 @@ pub const PatchFile = struct { .path = .{ .string = bun.PathString.init(filedir) }, .recursive = true, .mode = @intCast(@intFromEnum(mode)), - }, .sync).asErr()) |e| return e.toSystemError(); + }).asErr()) |e| return e; } const newfile_fd = switch (bun.sys.openat( @@ -136,7 +136,7 @@ pub const PatchFile = struct { mode.toBunMode(), )) { .result => |fd| fd, - .err => |e| return e.withPath(filepath.slice()).toSystemError(), + .err => |e| return e.withPath(filepath.slice()), }; defer _ = bun.sys.close(newfile_fd); @@ -180,14 +180,14 @@ pub const PatchFile = struct { while (written < file_contents.len) { switch (bun.sys.write(newfile_fd, file_contents[written..])) { .result => |bytes| written += bytes, - .err => |e| return e.withPath(filepath.slice()).toSystemError(), + .err => |e| return e.withPath(filepath.slice()), } } }, .file_patch => { // TODO: should we compute the hash of the original file and check it against the on in the patch? if (applyPatch(part.file_patch, &arena, patch_dir, &state).asErr()) |e| { - return e.toSystemError(); + return e; } }, .file_mode_change => { @@ -195,22 +195,22 @@ pub const PatchFile = struct { const filepath = arena.allocator().dupeZ(u8, part.file_mode_change.path) catch bun.outOfMemory(); if (comptime bun.Environment.isPosix) { if (bun.sys.fchmodat(patch_dir, filepath, newmode.toBunMode(), 0).asErr()) |e| { - return e.toSystemError(); + return e; } } if (comptime bun.Environment.isWindows) { const absfilepath = switch (state.patchDirAbsPath(patch_dir)) { .result => |p| p, - .err => |e| return e.toSystemError(), + .err => |e| return e, }; const fd = switch (bun.sys.open(bun.path.joinZ(&[_][]const u8{ absfilepath, filepath }, .auto), bun.O.RDWR, 0)) { - .err => |e| return e.toSystemError(), + .err => |e| return e, .result => |f| f, }; defer _ = bun.sys.close(fd); if (bun.sys.fchmod(fd, newmode.toBunMode()).asErr()) |e| { - return e.toSystemError(); + return e; } } }, @@ -1150,7 +1150,7 @@ pub const TestingAPIs = struct { defer args.deinit(); if (args.patchfile.apply(bun.default_allocator, args.dirfd)) |err| { - return globalThis.throwValue(err.toErrorInstance(globalThis)); + return globalThis.throwValue(err.toJSC(globalThis)); } return .true; diff --git a/src/resolver/resolve_path.zig b/src/resolver/resolve_path.zig index daa531a1c7..dec3d1bc49 100644 --- a/src/resolver/resolve_path.zig +++ b/src/resolver/resolve_path.zig @@ -682,13 +682,16 @@ pub fn windowsFilesystemRootT(comptime T: type, path: []const T) []const T { } } - // UNC + // UNC and device paths if (path.len >= 5 and Platform.windows.isSeparatorT(T, path[0]) and Platform.windows.isSeparatorT(T, path[1]) and - !Platform.windows.isSeparatorT(T, path[2]) and - path[2] != '.') + !Platform.windows.isSeparatorT(T, path[2])) { + // device path + if (path[2] == '.' and Platform.windows.isSeparatorT(T, path[3])) return path[0..4]; + + // UNC if (bun.strings.indexOfAnyT(T, path[3..], "/\\")) |idx| { if (bun.strings.indexOfAnyT(T, path[4 + idx ..], "/\\")) |idx_second| { return path[0 .. idx + idx_second + 4 + 1]; // +1 to skip second separator diff --git a/src/shell/interpreter.zig b/src/shell/interpreter.zig index 6fb90f25ac..dd99771700 100644 --- a/src/shell/interpreter.zig +++ b/src/shell/interpreter.zig @@ -1325,14 +1325,14 @@ pub const Interpreter = struct { const cwd: [:0]const u8 = switch (Syscall.getcwdZ(pathbuf)) { .result => |cwd| cwd, .err => |err| { - return .{ .err = .{ .sys = err.toSystemError() } }; + return .{ .err = .{ .sys = err.toShellSystemError() } }; }, }; const cwd_fd = switch (Syscall.open(cwd, bun.O.DIRECTORY | bun.O.RDONLY, 0)) { .result => |fd| fd, .err => |err| { - return .{ .err = .{ .sys = err.toSystemError() } }; + return .{ .err = .{ .sys = err.toShellSystemError() } }; }, }; @@ -1346,7 +1346,7 @@ pub const Interpreter = struct { log("Duping stdin", .{}); const stdin_fd = switch (if (bun.Output.Source.Stdio.isStdinNull()) bun.sys.openNullDevice() else ShellSyscall.dup(shell.STDIN_FD)) { .result => |fd| fd, - .err => |err| return .{ .err = .{ .sys = err.toSystemError() } }, + .err => |err| return .{ .err = .{ .sys = err.toShellSystemError() } }, }; const stdin_reader = IOReader.init(stdin_fd, event_loop); @@ -1387,7 +1387,7 @@ pub const Interpreter = struct { }; if (cwd_) |c| { - if (interpreter.root_shell.changeCwdImpl(interpreter, c, true).asErr()) |e| return .{ .err = .{ .sys = e.toSystemError() } }; + if (interpreter.root_shell.changeCwdImpl(interpreter, c, true).asErr()) |e| return .{ .err = .{ .sys = e.toShellSystemError() } }; } return .{ .result = interpreter }; @@ -1461,7 +1461,7 @@ pub const Interpreter = struct { switch (try interp.run()) { .err => |e| { interp.deinitEverything(); - bun.Output.prettyErrorln("error: Failed to run script {s} due to error {}", .{ std.fs.path.basename(path), e.toSystemError() }); + bun.Output.err(e, "Failed to run script {s}", .{std.fs.path.basename(path)}); bun.Global.exit(1); return 1; }, @@ -1528,7 +1528,7 @@ pub const Interpreter = struct { switch (try interp.run()) { .err => |e| { interp.deinitEverything(); - bun.Output.prettyErrorln("error: Failed to run script {s} due to error {}", .{ path_for_errors, e.toSystemError() }); + bun.Output.err(e, "Failed to run script {s}", .{path_for_errors}); bun.Global.exit(1); return 1; }, @@ -3403,7 +3403,7 @@ pub const Interpreter = struct { closefd(pipe[0]); closefd(pipe[1]); } - const system_err = err.toSystemError(); + const system_err = err.toShellSystemError(); this.writeFailingError("bun: {s}\n", .{system_err.message}); return .yield; } @@ -3423,7 +3423,7 @@ pub const Interpreter = struct { const subshell_state = switch (this.base.shell.dupeForSubshell(this.base.interpreter.allocator, cmd_io, .pipeline)) { .result => |s| s, .err => |err| { - const system_err = err.toSystemError(); + const system_err = err.toShellSystemError(); this.writeFailingError("bun: {s}\n", .{system_err.message}); return .yield; }, @@ -4874,7 +4874,7 @@ pub const Interpreter = struct { switch (this.exec.bltn.start()) { .result => {}, .err => |e| { - this.writeFailingError("bun: {s}: {s}", .{ @tagName(this.exec.bltn.kind), e.toSystemError().message }); + this.writeFailingError("bun: {s}: {s}", .{ @tagName(this.exec.bltn.kind), e.toShellSystemError().message }); return; }, } @@ -4968,7 +4968,7 @@ pub const Interpreter = struct { const flags = this.node.redirect.toFlags(); const redirfd = switch (ShellSyscall.openat(this.base.shell.cwd_fd, path, flags, perm)) { .err => |e| { - return this.writeFailingError("bun: {s}: {s}", .{ e.toSystemError().message, path }); + return this.writeFailingError("bun: {s}: {s}", .{ e.toShellSystemError().message, path }); }, .result => |f| f, }; @@ -5559,7 +5559,7 @@ pub const Interpreter = struct { const flags = node.redirect.toFlags(); const redirfd = switch (ShellSyscall.openat(cmd.base.shell.cwd_fd, path, flags, perm)) { .err => |e| { - cmd.writeFailingError("bun: {s}: {s}", .{ e.toSystemError().message, path }); + cmd.writeFailingError("bun: {s}: {s}", .{ e.toShellSystemError().message, path }); return .yield; }, .result => |f| f, @@ -5800,12 +5800,17 @@ pub const Interpreter = struct { /// Error messages formatted to match bash fn taskErrorToString(this: *Builtin, comptime kind: Kind, err: anytype) []const u8 { switch (@TypeOf(err)) { - Syscall.Error => return switch (err.getErrno()) { - bun.C.E.NOENT => this.fmtErrorArena(kind, "{s}: No such file or directory\n", .{err.path}), - bun.C.E.NAMETOOLONG => this.fmtErrorArena(kind, "{s}: File name too long\n", .{err.path}), - bun.C.E.ISDIR => this.fmtErrorArena(kind, "{s}: is a directory\n", .{err.path}), - bun.C.E.NOTEMPTY => this.fmtErrorArena(kind, "{s}: Directory not empty\n", .{err.path}), - else => this.fmtErrorArena(kind, "{s}\n", .{err.toSystemError().message.byteSlice()}), + Syscall.Error => { + if (err.getErrorCodeTagName()) |entry| { + _, const sys_errno = entry; + if (bun.sys.coreutils_error_map.get(sys_errno)) |message| { + if (err.path.len > 0) { + return this.fmtErrorArena(kind, "{s}: {s}\n", .{ err.path, message }); + } + return this.fmtErrorArena(kind, "{s}\n", .{message}); + } + } + return this.fmtErrorArena(kind, "unknown error {d}\n", .{err.errno}); }, JSC.SystemError => { if (err.path.length() == 0) return this.fmtErrorArena(kind, "{s}\n", .{err.message.byteSlice()}); @@ -6427,7 +6432,7 @@ pub const Interpreter = struct { .mtime = mtime, .path = .{ .string = bun.PathString.init(filepath) }, }; - if (node_fs.utimes(args, .callback).asErr()) |err| out: { + if (node_fs.utimes(args, .sync).asErr()) |err| out: { if (err.getErrno() == bun.C.E.NOENT) { const perm = 0o664; switch (Syscall.open(filepath, bun.O.CREAT | bun.O.WRONLY, perm)) { @@ -6436,12 +6441,12 @@ pub const Interpreter = struct { break :out; }, .err => |e| { - this.err = e.withPath(bun.default_allocator.dupe(u8, filepath) catch bun.outOfMemory()).toSystemError(); + this.err = e.withPath(bun.default_allocator.dupe(u8, filepath) catch bun.outOfMemory()).toShellSystemError(); break :out; }, } } - this.err = err.withPath(bun.default_allocator.dupe(u8, filepath) catch bun.outOfMemory()).toSystemError(); + this.err = err.withPath(bun.default_allocator.dupe(u8, filepath) catch bun.outOfMemory()).toShellSystemError(); } if (this.event_loop == .js) { @@ -6831,10 +6836,10 @@ pub const Interpreter = struct { var vtable = MkdirVerboseVTable{ .inner = this, .active = this.opts.verbose }; - switch (node_fs.mkdirRecursiveImpl(args, .callback, *MkdirVerboseVTable, &vtable)) { + switch (node_fs.mkdirRecursiveImpl(args, *MkdirVerboseVTable, &vtable)) { .result => {}, .err => |e| { - this.err = e.withPath(bun.default_allocator.dupe(u8, filepath) catch bun.outOfMemory()).toSystemError(); + this.err = e.withPath(bun.default_allocator.dupe(u8, filepath) catch bun.outOfMemory()).toShellSystemError(); std.mem.doNotOptimizeAway(&node_fs); }, } @@ -6844,7 +6849,7 @@ pub const Interpreter = struct { .recursive = false, .always_return_none = true, }; - switch (node_fs.mkdirNonRecursive(args, .callback)) { + switch (node_fs.mkdirNonRecursive(args)) { .result => { if (this.opts.verbose) { this.created_directories.appendSlice(filepath[0..filepath.len]) catch bun.outOfMemory(); @@ -6852,7 +6857,7 @@ pub const Interpreter = struct { } }, .err => |e| { - this.err = e.withPath(bun.default_allocator.dupe(u8, filepath) catch bun.outOfMemory()).toSystemError(); + this.err = e.withPath(bun.default_allocator.dupe(u8, filepath) catch bun.outOfMemory()).toShellSystemError(); std.mem.doNotOptimizeAway(&node_fs); }, } @@ -8489,7 +8494,7 @@ pub const Interpreter = struct { return this.writeFailingError(buf, 1); }, else => { - const sys_err = e.toSystemError(); + const sys_err = e.toShellSystemError(); const buf = this.bltn.fmtErrorArena(.mv, "{s}: {s}\n", .{ sys_err.path.byteSlice(), sys_err.message.byteSlice() }); return this.writeFailingError(buf, 1); }, @@ -8623,7 +8628,7 @@ pub const Interpreter = struct { exec.tasks_done += 1; if (exec.tasks_done >= exec.task_count) { if (exec.err) |err| { - const e = err.toSystemError(); + const e = err.toShellSystemError(); const buf = this.bltn.fmtErrorArena(.mv, "{}: {}\n", .{ e.path, e.message }); _ = this.writeFailingError(buf, err.errno); return; @@ -11324,7 +11329,7 @@ pub const Interpreter = struct { pub fn onReaderError(this: *IOReader, err: bun.sys.Error) void { this.setReading(false); - this.err = err.toSystemError(); + this.err = err.toShellSystemError(); for (this.readers.slice()) |r| { r.onReaderDone(if (this.err) |*e| brk: { e.ref(); @@ -11716,7 +11721,7 @@ pub const Interpreter = struct { pub fn onError(this: *This, err__: bun.sys.Error) void { this.setWriting(false); - const ee = err__.toSystemError(); + const ee = err__.toShellSystemError(); this.err = ee; log("IOWriter(0x{x}, fd={}) onError errno={s} errmsg={} errsyscall={}", .{ @intFromPtr(this), this.fd, @tagName(ee.getErrno()), ee.message, ee.syscall }); var seen_alloc = std.heap.stackFallback(@sizeOf(usize) * 64, bun.default_allocator); diff --git a/src/shell/shell.zig b/src/shell/shell.zig index 481c0581e9..6133dc62c1 100644 --- a/src/shell/shell.zig +++ b/src/shell/shell.zig @@ -55,7 +55,7 @@ pub const ShellErr = union(enum) { pub fn newSys(e: anytype) @This() { return .{ .sys = switch (@TypeOf(e)) { - Syscall.Error => e.toSystemError(), + Syscall.Error => e.toShellSystemError(), JSC.SystemError => e, else => @compileError("Invalid `e`: " ++ @typeName(e)), }, diff --git a/src/shell/subproc.zig b/src/shell/subproc.zig index 672abdcf22..c88aa8190a 100644 --- a/src/shell/subproc.zig +++ b/src/shell/subproc.zig @@ -848,7 +848,7 @@ pub const ShellSubprocess = struct { ) catch |err| { return .{ .err = .{ .custom = std.fmt.allocPrint(bun.default_allocator, "Failed to spawn process: {s}", .{@errorName(err)}) catch bun.outOfMemory() } }; }) { - .err => |err| return .{ .err = .{ .sys = err.toSystemError() } }, + .err => |err| return .{ .err = .{ .sys = err.toShellSystemError() } }, .result => |result| result, }; diff --git a/src/string.zig b/src/string.zig index d90f2bcf1b..843340c410 100644 --- a/src/string.zig +++ b/src/string.zig @@ -1002,7 +1002,7 @@ pub const String = extern struct { }; } - pub fn toThreadSafeSlice(this: *String, allocator: std.mem.Allocator) SliceWithUnderlyingString { + pub fn toThreadSafeSlice(this: *const String, allocator: std.mem.Allocator) SliceWithUnderlyingString { if (this.tag == .WTFStringImpl) { if (!this.value.WTFStringImpl.isThreadSafe()) { const slice = this.value.WTFStringImpl.toUTF8WithoutRef(allocator); diff --git a/src/string_immutable.zig b/src/string_immutable.zig index ab1d796490..4fed847464 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -832,7 +832,7 @@ pub fn startsWithGeneric(comptime T: type, self: []const T, str: []const T) bool return false; } - return eqlLong(bun.reinterpretSlice(u8, self[0..str.len]), str, false); + return eqlLong(bun.reinterpretSlice(u8, self[0..str.len]), bun.reinterpretSlice(u8, str[0..str.len]), false); } pub inline fn endsWith(self: string, str: string) bool { @@ -1137,6 +1137,19 @@ pub fn hasPrefixCaseInsensitive(str: []const u8, prefix: []const u8) bool { return hasPrefixCaseInsensitiveT(u8, str, prefix); } +pub fn eqlLongT(comptime T: type, a_str: []const T, b_str: []const T, comptime check_len: bool) bool { + if (comptime check_len) { + const len = b_str.len; + if (len == 0) { + return a_str.len == 0; + } + if (a_str.len != len) { + return false; + } + } + return eqlLong(bun.reinterpretSlice(u8, a_str), bun.reinterpretSlice(u8, b_str), false); +} + pub fn eqlLong(a_str: string, b_str: string, comptime check_len: bool) bool { const len = b_str.len; @@ -1886,7 +1899,7 @@ pub fn fromWPath(buf: []u8, utf16: []const u16) [:0]const u8 { return buf[0..encode_into_result.written :0]; } -pub fn toNTPath(wbuf: []u16, utf8: []const u8) [:0]const u16 { +pub fn toNTPath(wbuf: []u16, utf8: []const u8) [:0]u16 { if (!std.fs.path.isAbsoluteWindows(utf8)) { return toWPathNormalized(wbuf, utf8); } @@ -1915,19 +1928,23 @@ pub fn toNTMaxPath(buf: []u8, utf8: []const u8) [:0]const u8 { return buf[0 .. toPathNormalized(buf[prefix.len..], utf8).len + prefix.len :0]; } -pub fn addNTPathPrefix(wbuf: []u16, utf16: []const u16) [:0]const u16 { +pub fn addNTPathPrefix(wbuf: []u16, utf16: []const u16) [:0]u16 { wbuf[0..bun.windows.nt_object_prefix.len].* = bun.windows.nt_object_prefix; @memcpy(wbuf[bun.windows.nt_object_prefix.len..][0..utf16.len], utf16); wbuf[utf16.len + bun.windows.nt_object_prefix.len] = 0; return wbuf[0 .. utf16.len + bun.windows.nt_object_prefix.len :0]; } -pub fn addNTPathPrefixIfNeeded(wbuf: []u16, utf16: []const u16) [:0]const u16 { +pub fn addNTPathPrefixIfNeeded(wbuf: []u16, utf16: []const u16) [:0]u16 { if (hasPrefixComptimeType(u16, utf16, bun.windows.nt_object_prefix)) { @memcpy(wbuf[0..utf16.len], utf16); wbuf[utf16.len] = 0; return wbuf[0..utf16.len :0]; } + if (hasPrefixComptimeType(u16, utf16, bun.windows.nt_maxpath_prefix)) { + // Replace prefix + return addNTPathPrefix(wbuf, utf16[bun.windows.nt_maxpath_prefix.len..]); + } return addNTPathPrefix(wbuf, utf16); } @@ -1948,7 +1965,7 @@ pub fn toWPathNormalizeAutoExtend(wbuf: []u16, utf8: []const u8) [:0]const u16 { return toWPathNormalized(wbuf, utf8); } -pub fn toWPathNormalized(wbuf: []u16, utf8: []const u8) [:0]const u16 { +pub fn toWPathNormalized(wbuf: []u16, utf8: []const u8) [:0]u16 { const renormalized = bun.PathBufferPool.get(); defer bun.PathBufferPool.put(renormalized); @@ -2012,10 +2029,10 @@ pub fn toWDirNormalized(wbuf: []u16, utf8: []const u8) [:0]const u16 { return toWDirPath(wbuf, path_to_use); } -pub fn toWPath(wbuf: []u16, utf8: []const u8) [:0]const u16 { +pub fn toWPath(wbuf: []u16, utf8: []const u8) [:0]u16 { return toWPathMaybeDir(wbuf, utf8, false); } -pub fn toPath(buf: []u8, utf8: []const u8) [:0]const u8 { +pub fn toPath(buf: []u8, utf8: []const u8) [:0]u8 { return toPathMaybeDir(buf, utf8, false); } @@ -2048,7 +2065,7 @@ pub fn assertIsValidWindowsPath(comptime T: type, path: []const T) void { } } -pub fn toWPathMaybeDir(wbuf: []u16, utf8: []const u8, comptime add_trailing_lash: bool) [:0]const u16 { +pub fn toWPathMaybeDir(wbuf: []u16, utf8: []const u8, comptime add_trailing_lash: bool) [:0]u16 { bun.unsafeAssert(wbuf.len > 0); var result = bun.simdutf.convert.utf8.to.utf16.with_errors.le( @@ -2065,7 +2082,7 @@ pub fn toWPathMaybeDir(wbuf: []u16, utf8: []const u8, comptime add_trailing_lash return wbuf[0..result.count :0]; } -pub fn toPathMaybeDir(buf: []u8, utf8: []const u8, comptime add_trailing_lash: bool) [:0]const u8 { +pub fn toPathMaybeDir(buf: []u8, utf8: []const u8, comptime add_trailing_lash: bool) [:0]u8 { bun.unsafeAssert(buf.len > 0); var len = utf8.len; @@ -4531,6 +4548,14 @@ pub fn trimLeadingPattern2(slice_: []const u8, comptime byte1: u8, comptime byte return slice; } +/// prefix is of type []const u8 or []const u16 +pub fn trimPrefixComptime(comptime T: type, buffer: []const T, comptime prefix: anytype) []const T { + return if (hasPrefixComptimeType(T, buffer, prefix)) + buffer[prefix.len..] + else + buffer; +} + /// Get the line number and the byte offsets of `line_range_count` above the desired line number /// The final element is the end index of the desired line const LineRange = struct { diff --git a/src/sys.zig b/src/sys.zig index fa7d22c4df..c1c1da2beb 100644 --- a/src/sys.zig +++ b/src/sys.zig @@ -130,6 +130,8 @@ pub const O = switch (Environment.os) { pub const TMPFILE = 0o20040000; pub const NDELAY = NONBLOCK; + pub const SYMLINK = bun.C.translated.O_SYMLINK; + pub const toPacked = toPackedO; }, }, @@ -194,7 +196,7 @@ pub const Tag = enum(u8) { link, lseek, lstat, - lutimes, + lutime, mkdir, mkdtemp, fnctl, @@ -211,7 +213,7 @@ pub const Tag = enum(u8) { symlink, symlinkat, unlink, - utimes, + utime, write, getcwd, getenv, @@ -229,6 +231,7 @@ pub const Tag = enum(u8) { pidfd_open, poll, watch, + scandir, kevent, kqueue, @@ -250,6 +253,7 @@ pub const Tag = enum(u8) { try_write, socketpair, setsockopt, + statx, uv_spawn, uv_pipe, @@ -320,7 +324,7 @@ pub const Error = struct { } pub fn format(self: Error, comptime fmt: []const u8, opts: std.fmt.FormatOptions, writer: anytype) !void { - try self.toSystemError().format(fmt, opts, writer); + try self.toShellSystemError().format(fmt, opts, writer); } pub inline fn getErrno(this: Error) E { @@ -356,6 +360,21 @@ pub const Error = struct { }; } + pub inline fn withPathDest(this: Error, path: anytype, dest: anytype) Error { + if (std.meta.Child(@TypeOf(path)) == u16) { + @compileError("Do not pass WString path to withPathDest, it needs the path encoded as utf8 (path)"); + } + if (std.meta.Child(@TypeOf(dest)) == u16) { + @compileError("Do not pass WString path to withPathDest, it needs the path encoded as utf8 (dest)"); + } + return Error{ + .errno = this.errno, + .syscall = this.syscall, + .path = bun.span(path), + .dest = bun.span(dest), + }; + } + pub inline fn withPathLike(this: Error, pathlike: anytype) Error { return switch (pathlike) { .fd => |fd| this.withFd(fd), @@ -391,36 +410,44 @@ pub const Error = struct { return bun.errnoToZigErr(this.errno); } - pub fn toSystemError(this: Error) SystemError { + /// 1. Convert libuv errno values into libc ones. + /// 2. Get the tag name as a string for printing. + pub fn getErrorCodeTagName(err: *const Error) ?struct { [:0]const u8, C.SystemErrno } { + if (!Environment.isWindows) { + if (err.errno > 0 and err.errno < C.SystemErrno.max) { + const system_errno = @as(C.SystemErrno, @enumFromInt(err.errno)); + return .{ @tagName(system_errno), system_errno }; + } + } else { + const system_errno: C.SystemErrno = brk: { + // setRuntimeSafety(false) because we use tagName function, which will be null on invalid enum value. + @setRuntimeSafety(false); + if (err.from_libuv) { + break :brk @enumFromInt(@intFromEnum(bun.windows.libuv.translateUVErrorToE(@as(c_int, err.errno) * -1))); + } + + break :brk @enumFromInt(err.errno); + }; + if (bun.tagName(bun.C.SystemErrno, system_errno)) |errname| { + return .{ errname, system_errno }; + } + } + return null; + } + + /// Simpler formatting which does not allocate a message + pub fn toShellSystemError(this: Error) SystemError { var err = SystemError{ .errno = @as(c_int, this.errno) * -1, .syscall = bun.String.static(@tagName(this.syscall)), }; // errno label - if (!Environment.isWindows) { - if (this.errno > 0 and this.errno < C.SystemErrno.max) { - const system_errno = @as(C.SystemErrno, @enumFromInt(this.errno)); - err.code = bun.String.static(@tagName(system_errno)); - if (C.SystemErrno.labels.get(system_errno)) |label| { - err.message = bun.String.static(label); - } - } - } else { - const system_errno = brk: { - // setRuntimeSafety(false) because we use tagName function, which will be null on invalid enum value. - @setRuntimeSafety(false); - if (this.from_libuv) { - break :brk @as(C.SystemErrno, @enumFromInt(@intFromEnum(bun.windows.libuv.translateUVErrorToE(err.errno)))); - } - - break :brk @as(C.SystemErrno, @enumFromInt(this.errno)); - }; - if (bun.tagName(bun.C.SystemErrno, system_errno)) |errname| { - err.code = bun.String.static(errname); - if (C.SystemErrno.labels.get(system_errno)) |label| { - err.message = bun.String.static(label); - } + if (this.getErrorCodeTagName()) |resolved_errno| { + const code, const system_errno = resolved_errno; + err.code = bun.String.static(code); + if (coreutils_error_map.get(system_errno)) |label| { + err.message = bun.String.static(label); } } @@ -439,6 +466,68 @@ pub const Error = struct { return err; } + /// More complex formatting to precisely match the printing that Node.js emits. + /// Use this whenever the error will be sent to JavaScript instead of the shell variant above. + pub fn toSystemError(this: Error) SystemError { + var err = SystemError{ + .errno = -%@as(c_int, this.errno), + .syscall = bun.String.static(@tagName(this.syscall)), + }; + + // errno label + var code: ?[:0]const u8 = null; + var label: ?[]const u8 = null; + if (this.getErrorCodeTagName()) |resolved_errno| { + code, const system_errno = resolved_errno; + err.code = bun.String.static(code.?); + label = libuv_error_map.get(system_errno); + } + + // format taken from Node.js 'exceptions.cc' + // search keyword: `Local UVException(Isolate* isolate,` + var message_buf: [4096]u8 = undefined; + const message = message: { + var stream = std.io.fixedBufferStream(&message_buf); + const writer = stream.writer(); + brk: { + if (code) |c| { + writer.writeAll(c) catch break :brk; + writer.writeAll(": ") catch break :brk; + } + writer.writeAll(label orelse "Unknown Error") catch break :brk; + writer.writeAll(", ") catch break :brk; + writer.writeAll(@tagName(this.syscall)) catch break :brk; + if (this.path.len > 0) { + writer.writeAll(" '") catch break :brk; + writer.writeAll(this.path) catch break :brk; + writer.writeAll("'") catch break :brk; + + if (this.dest.len > 0) { + writer.writeAll(" -> '") catch break :brk; + writer.writeAll(this.dest) catch break :brk; + writer.writeAll("'") catch break :brk; + } + } + } + break :message stream.getWritten(); + }; + err.message = bun.String.createUTF8(message); + + if (this.path.len > 0) { + err.path = bun.String.createUTF8(this.path); + } + + if (this.dest.len > 0) { + err.dest = bun.String.createUTF8(this.dest); + } + + if (this.fd != bun.invalid_fd) { + err.fd = this.fd; + } + + return err; + } + pub inline fn todo() Error { if (Environment.isDebug) { @panic("bun.sys.Error.todo() was called"); @@ -609,9 +698,9 @@ pub fn lstat(path: [:0]const u8) Maybe(bun.Stat) { if (Environment.isWindows) { return sys_uv.lstat(path); } else { - var stat_ = mem.zeroes(bun.Stat); - if (Maybe(bun.Stat).errnoSysP(C.lstat(path, &stat_), .lstat, path)) |err| return err; - return Maybe(bun.Stat){ .result = stat_ }; + var stat_buf = mem.zeroes(bun.Stat); + if (Maybe(bun.Stat).errnoSysP(C.lstat(path, &stat_buf), .lstat, path)) |err| return err; + return Maybe(bun.Stat){ .result = stat_buf }; } } @@ -667,7 +756,6 @@ pub fn mkdiratW(dir_fd: bun.FileDescriptor, file_path: []const u16, _: i32) Mayb if (dir_to_make == .err) { return .{ .err = dir_to_make.err }; } - _ = close(dir_to_make.result); return .{ .result = {} }; } @@ -810,15 +898,24 @@ pub fn normalizePathWindows( var path = if (T == u16) path_ else bun.strings.convertUTF8toUTF16InBuffer(wbuf, path_); if (std.fs.path.isAbsoluteWindowsWTF16(path)) { - // handle the special "nul" device - // we technically should handle the other DOS devices too. - if (path_.len >= "\\nul".len and - (bun.strings.eqlComptimeT(T, path_[path_.len - "\\nul".len ..], "\\nul") or - bun.strings.eqlComptimeT(T, path_[path_.len - "\\NUL".len ..], "\\NUL"))) - { - @memcpy(buf[0..bun.strings.w("\\??\\NUL").len], bun.strings.w("\\??\\NUL")); - buf[bun.strings.w("\\??\\NUL").len] = 0; - return .{ .result = buf[0..bun.strings.w("\\??\\NUL").len :0] }; + if (path_.len >= 4) { + if ((bun.strings.eqlComptimeT(T, path_[path_.len - "\\nul".len ..], "\\nul") or + bun.strings.eqlComptimeT(T, path_[path_.len - "\\NUL".len ..], "\\NUL"))) + { + @memcpy(buf[0..bun.strings.w("\\??\\NUL").len], bun.strings.w("\\??\\NUL")); + buf[bun.strings.w("\\??\\NUL").len] = 0; + return .{ .result = buf[0..bun.strings.w("\\??\\NUL").len :0] }; + } + if ((path[1] == '/' or path[1] == '\\') and + (path[2] == '.' or path[2] == '?') and + (path[3] == '/' or path[3] == '\\')) + { + buf[0..4].* = .{ '\\', '\\', path[2], '\\' }; + const rest = path[4..]; + @memcpy(buf[4..][0..rest.len], rest); + buf[path.len] = 0; + return .{ .result = buf[0..path.len :0] }; + } } const norm = bun.path.normalizeStringGenericTZ(u16, path, buf, .{ .add_nt_prefix = true, .zero_terminate = true }); @@ -872,7 +969,7 @@ pub fn normalizePathWindows( fn openDirAtWindowsNtPath( dirFd: bun.FileDescriptor, - path: []const u16, + path: [:0]const u16, options: WindowsOpenDirOptions, ) Maybe(bun.FileDescriptor) { const iterable = options.iterable; @@ -886,6 +983,17 @@ fn openDirAtWindowsNtPath( const rename_flag: u32 = if (can_rename_or_delete) w.DELETE else 0; const read_only_flag: u32 = if (read_only) 0 else w.FILE_ADD_FILE | w.FILE_ADD_SUBDIRECTORY; const flags: u32 = iterable_flag | base_flags | rename_flag | read_only_flag; + const open_reparse_point: w.DWORD = if (no_follow) w.FILE_OPEN_REPARSE_POINT else 0x0; + + // NtCreateFile seems to not function on device paths. + // Since it is absolute, it can just use CreateFileW + if (bun.strings.hasPrefixComptimeUTF16(path, "\\\\.\\")) + return openWindowsDevicePath( + path, + flags, + if (options.create) w.FILE_OPEN_IF else w.FILE_OPEN, + w.FILE_DIRECTORY_FILE | w.FILE_SYNCHRONOUS_IO_NONALERT | w.FILE_OPEN_FOR_BACKUP_INTENT | open_reparse_point, + ); const path_len_bytes: u16 = @truncate(path.len * 2); var nt_name = w.UNICODE_STRING{ @@ -906,7 +1014,6 @@ fn openDirAtWindowsNtPath( .SecurityDescriptor = null, .SecurityQualityOfService = null, }; - const open_reparse_point: w.DWORD = if (no_follow) w.FILE_OPEN_REPARSE_POINT else 0x0; var fd: w.HANDLE = w.INVALID_HANDLE_VALUE; var io: w.IO_STATUS_BLOCK = undefined; @@ -965,6 +1072,33 @@ fn openDirAtWindowsNtPath( } } +fn openWindowsDevicePath( + path: [:0]const u16, + dwDesiredAccess: u32, + dwCreationDisposition: u32, + dwFlagsAndAttributes: u32, +) Maybe(bun.FileDescriptor) { + const rc = std.os.windows.kernel32.CreateFileW( + path, + dwDesiredAccess, + FILE_SHARE, + null, + dwCreationDisposition, + dwFlagsAndAttributes, + null, + ); + if (rc == w.INVALID_HANDLE_VALUE) { + return .{ .err = .{ + .errno = if (windows.Win32Error.get().toSystemErrno()) |e| + @intFromEnum(e) + else + @intFromEnum(bun.C.E.UNKNOWN), + .syscall = .open, + } }; + } + return .{ .result = bun.toFD(rc) }; +} + pub const WindowsOpenDirOptions = packed struct { iterable: bool = false, no_follow: bool = false, @@ -2033,7 +2167,7 @@ pub fn chown(path: [:0]const u8, uid: posix.uid_t, gid: posix.gid_t) Maybe(void) pub fn symlink(target: [:0]const u8, dest: [:0]const u8) Maybe(void) { while (true) { - if (Maybe(void).errnoSysPD(syscall.symlink(target, dest), .symlink, target, dest)) |err| { + if (Maybe(void).errnoSys(syscall.symlink(target, dest), .symlink)) |err| { if (err.getErrno() == .INTR) continue; return err; } @@ -2703,12 +2837,17 @@ pub fn faccessat(dir_: anytype, subpath: anytype) JSC.Maybe(bool) { return JSC.Maybe(bool){ .result = false }; } -pub fn directoryExistsAt(dir_: anytype, subpath: anytype) JSC.Maybe(bool) { - const dir_fd = bun.toFD(dir_); +pub fn directoryExistsAt(dir: anytype, subpath: anytype) JSC.Maybe(bool) { + const dir_fd = bun.toFD(dir); if (comptime Environment.isWindows) { const wbuf = bun.WPathBufferPool.get(); defer bun.WPathBufferPool.put(wbuf); - const path = bun.strings.toNTPath(wbuf, subpath); + const path = if (std.meta.Child(@TypeOf(subpath)) == u16) + bun.strings.addNTPathPrefixIfNeeded(wbuf, subpath) + else + bun.strings.toNTPath(wbuf, subpath); + bun.path.dangerouslyConvertPathToWindowsInPlace(u16, path); + const path_len_bytes: u16 = @truncate(path.len * 2); var nt_name = w.UNICODE_STRING{ .Length = path_len_bytes, @@ -2730,8 +2869,11 @@ pub fn directoryExistsAt(dir_: anytype, subpath: anytype) JSC.Maybe(bool) { }; var basic_info: w.FILE_BASIC_INFORMATION = undefined; const rc = kernel32.NtQueryAttributesFile(&attr, &basic_info); - if (JSC.Maybe(bool).errnoSysP(rc, .access, subpath)) |err| { - syslog("NtQueryAttributesFile({}, {}, O_DIRECTORY | O_RDONLY, 0) = {}", .{ dir_fd, bun.fmt.fmtOSPath(path, .{}), err }); + if (rc == .OBJECT_NAME_INVALID) { + bun.Output.warn("internal error: invalid object name: {}", .{bun.fmt.fmtOSPath(path, .{})}); + } + if (JSC.Maybe(bool).errnoSys(rc, .access)) |err| { + syslog("NtQueryAttributesFile({}, {}, O_DIRECTORY | O_RDONLY, 0) = {} {d}", .{ dir_fd, bun.fmt.fmtOSPath(path, .{}), err, rc }); return err; } @@ -2740,12 +2882,31 @@ pub fn directoryExistsAt(dir_: anytype, subpath: anytype) JSC.Maybe(bool) { basic_info.FileAttributes & kernel32.FILE_ATTRIBUTE_READONLY == 0; syslog("NtQueryAttributesFile({}, {}, O_DIRECTORY | O_RDONLY, 0) = {d}", .{ dir_fd, bun.fmt.fmtOSPath(path, .{}), @intFromBool(is_dir) }); - return .{ - .result = is_dir, - }; + return .{ .result = is_dir }; } - return faccessat(dir_fd, subpath); + const have_statx = Environment.isLinux; + if (have_statx) brk: { + var statx: std.os.linux.Statx = undefined; + if (Maybe(bool).errnoSys(bun.C.linux.statx( + dir_fd.cast(), + subpath, + std.os.linux.AT.SYMLINK_NOFOLLOW | std.os.linux.AT.STATX_SYNC_AS_STAT, + std.os.linux.STATX_TYPE, + &statx, + ), .statx)) |err| { + if (err.err.getErrno() == .NOSYS) break :brk; // Linux < 4.11 + return err; + } + return .{ .result = S.ISDIR(statx.mode) }; + } + + // TODO: on macOS, try getattrlist + + return switch (fstatat(dir_fd, subpath)) { + .err => |err| .{ .err = err }, + .result => |result| .{ .result = S.ISDIR(result.mode) }, + }; } pub fn setNonblocking(fd: bun.FileDescriptor) Maybe(void) { @@ -3615,3 +3776,372 @@ pub inline fn toLibUVOwnedFD( pub const Dir = @import("./dir.zig"); const FILE_SHARE = w.FILE_SHARE_WRITE | w.FILE_SHARE_READ | w.FILE_SHARE_DELETE; + +/// This map is derived off of uv.h's definitions, and is what Node.js uses in printing errors. +pub const libuv_error_map = brk: { + const entries: []const struct { [:0]const u8, [:0]const u8 } = &.{ + .{ "E2BIG", "argument list too long" }, + .{ "EACCES", "permission denied" }, + .{ "EADDRINUSE", "address already in use" }, + .{ "EADDRNOTAVAIL", "address not available" }, + .{ "EAFNOSUPPORT", "address family not supported" }, + .{ "EAGAIN", "resource temporarily unavailable" }, + .{ "EAI_ADDRFAMILY", "address family not supported" }, + .{ "EAI_AGAIN", "temporary failure" }, + .{ "EAI_BADFLAGS", "bad ai_flags value" }, + .{ "EAI_BADHINTS", "invalid value for hints" }, + .{ "EAI_CANCELED", "request canceled" }, + .{ "EAI_FAIL", "permanent failure" }, + .{ "EAI_FAMILY", "ai_family not supported" }, + .{ "EAI_MEMORY", "out of memory" }, + .{ "EAI_NODATA", "no address" }, + .{ "EAI_NONAME", "unknown node or service" }, + .{ "EAI_OVERFLOW", "argument buffer overflow" }, + .{ "EAI_PROTOCOL", "resolved protocol is unknown" }, + .{ "EAI_SERVICE", "service not available for socket type" }, + .{ "EAI_SOCKTYPE", "socket type not supported" }, + .{ "EALREADY", "connection already in progress" }, + .{ "EBADF", "bad file descriptor" }, + .{ "EBUSY", "resource busy or locked" }, + .{ "ECANCELED", "operation canceled" }, + .{ "ECHARSET", "invalid Unicode character" }, + .{ "ECONNABORTED", "software caused connection abort" }, + .{ "ECONNREFUSED", "connection refused" }, + .{ "ECONNRESET", "connection reset by peer" }, + .{ "EDESTADDRREQ", "destination address required" }, + .{ "EEXIST", "file already exists" }, + .{ "EFAULT", "bad address in system call argument" }, + .{ "EFBIG", "file too large" }, + .{ "EHOSTUNREACH", "host is unreachable" }, + .{ "EINTR", "interrupted system call" }, + .{ "EINVAL", "invalid argument" }, + .{ "EIO", "i/o error" }, + .{ "EISCONN", "socket is already connected" }, + .{ "EISDIR", "illegal operation on a directory" }, + .{ "ELOOP", "too many symbolic links encountered" }, + .{ "EMFILE", "too many open files" }, + .{ "EMSGSIZE", "message too long" }, + .{ "ENAMETOOLONG", "name too long" }, + .{ "ENETDOWN", "network is down" }, + .{ "ENETUNREACH", "network is unreachable" }, + .{ "ENFILE", "file table overflow" }, + .{ "ENOBUFS", "no buffer space available" }, + .{ "ENODEV", "no such device" }, + .{ "ENOENT", "no such file or directory" }, + .{ "ENOMEM", "not enough memory" }, + .{ "ENONET", "machine is not on the network" }, + .{ "ENOPROTOOPT", "protocol not available" }, + .{ "ENOSPC", "no space left on device" }, + .{ "ENOSYS", "function not implemented" }, + .{ "ENOTCONN", "socket is not connected" }, + .{ "ENOTDIR", "not a directory" }, + .{ "ENOTEMPTY", "directory not empty" }, + .{ "ENOTSOCK", "socket operation on non-socket" }, + .{ "ENOTSUP", "operation not supported on socket" }, + .{ "EOVERFLOW", "value too large for defined data type" }, + .{ "EPERM", "operation not permitted" }, + .{ "EPIPE", "broken pipe" }, + .{ "EPROTO", "protocol error" }, + .{ "EPROTONOSUPPORT", "protocol not supported" }, + .{ "EPROTOTYPE", "protocol wrong type for socket" }, + .{ "ERANGE", "result too large" }, + .{ "EROFS", "read-only file system" }, + .{ "ESHUTDOWN", "cannot send after transport endpoint shutdown" }, + .{ "ESPIPE", "invalid seek" }, + .{ "ESRCH", "no such process" }, + .{ "ETIMEDOUT", "connection timed out" }, + .{ "ETXTBSY", "text file is busy" }, + .{ "EXDEV", "cross-device link not permitted" }, + .{ "UNKNOWN", "unknown error" }, + .{ "EOF", "end of file" }, + .{ "ENXIO", "no such device or address" }, + .{ "EMLINK", "too many links" }, + .{ "EHOSTDOWN", "host is down" }, + .{ "EREMOTEIO", "remote I/O error" }, + .{ "ENOTTY", "inappropriate ioctl for device" }, + .{ "EFTYPE", "inappropriate file type or format" }, + .{ "EILSEQ", "illegal byte sequence" }, + .{ "ESOCKTNOSUPPORT", "socket type not supported" }, + .{ "ENODATA", "no data available" }, + .{ "EUNATCH", "protocol driver not attached" }, + }; + const SystemErrno = bun.C.SystemErrno; + var map = std.EnumMap(SystemErrno, [:0]const u8).initFull("unknown error"); + for (entries) |entry| { + const key, const text = entry; + if (@hasField(SystemErrno, key)) { + map.put(@field(SystemErrno, key), text); + } + } + + // sanity check + bun.assert(std.mem.eql(u8, map.get(SystemErrno.ENOENT).?, "no such file or directory")); + + break :brk map; +}; + +/// This map is derived off of what coreutils uses in printing errors. This is +/// equivalent to `strerror`, but as strings with constant lifetime. +pub const coreutils_error_map = brk: { + // macOS and Linux have slightly different error messages. + const entries: []const struct { [:0]const u8, [:0]const u8 } = switch (Environment.os) { + // Since windows is just an emulation of linux, it will derive the linux error messages. + .linux, .windows, .wasm => &.{ + .{ "EPERM", "Operation not permitted" }, + .{ "ENOENT", "No such file or directory" }, + .{ "ESRCH", "No such process" }, + .{ "EINTR", "Interrupted system call" }, + .{ "EIO", "Input/output error" }, + .{ "ENXIO", "No such device or address" }, + .{ "E2BIG", "Argument list too long" }, + .{ "ENOEXEC", "Exec format error" }, + .{ "EBADF", "Bad file descriptor" }, + .{ "ECHILD", "No child processes" }, + .{ "EAGAIN", "Resource temporarily unavailable" }, + .{ "ENOMEM", "Cannot allocate memory" }, + .{ "EACCES", "Permission denied" }, + .{ "EFAULT", "Bad address" }, + .{ "ENOTBLK", "Block device required" }, + .{ "EBUSY", "Device or resource busy" }, + .{ "EEXIST", "File exists" }, + .{ "EXDEV", "Invalid cross-device link" }, + .{ "ENODEV", "No such device" }, + .{ "ENOTDIR", "Not a directory" }, + .{ "EISDIR", "Is a directory" }, + .{ "EINVAL", "Invalid argument" }, + .{ "ENFILE", "Too many open files in system" }, + .{ "EMFILE", "Too many open files" }, + .{ "ENOTTY", "Inappropriate ioctl for device" }, + .{ "ETXTBSY", "Text file busy" }, + .{ "EFBIG", "File too large" }, + .{ "ENOSPC", "No space left on device" }, + .{ "ESPIPE", "Illegal seek" }, + .{ "EROFS", "Read-only file system" }, + .{ "EMLINK", "Too many links" }, + .{ "EPIPE", "Broken pipe" }, + .{ "EDOM", "Numerical argument out of domain" }, + .{ "ERANGE", "Numerical result out of range" }, + .{ "EDEADLK", "Resource deadlock avoided" }, + .{ "ENAMETOOLONG", "File name too long" }, + .{ "ENOLCK", "No locks available" }, + .{ "ENOSYS", "Function not implemented" }, + .{ "ENOTEMPTY", "Directory not empty" }, + .{ "ELOOP", "Too many levels of symbolic links" }, + .{ "ENOMSG", "No message of desired type" }, + .{ "EIDRM", "Identifier removed" }, + .{ "ECHRNG", "Channel number out of range" }, + .{ "EL2NSYNC", "Level 2 not synchronized" }, + .{ "EL3HLT", "Level 3 halted" }, + .{ "EL3RST", "Level 3 reset" }, + .{ "ELNRNG", "Link number out of range" }, + .{ "EUNATCH", "Protocol driver not attached" }, + .{ "ENOCSI", "No CSI structure available" }, + .{ "EL2HLT", "Level 2 halted" }, + .{ "EBADE", "Invalid exchange" }, + .{ "EBADR", "Invalid request descriptor" }, + .{ "EXFULL", "Exchange full" }, + .{ "ENOANO", "No anode" }, + .{ "EBADRQC", "Invalid request code" }, + .{ "EBADSLT", "Invalid slot" }, + .{ "EBFONT", "Bad font file format" }, + .{ "ENOSTR", "Device not a stream" }, + .{ "ENODATA", "No data available" }, + .{ "ETIME", "Timer expired" }, + .{ "ENOSR", "Out of streams resources" }, + .{ "ENONET", "Machine is not on the network" }, + .{ "ENOPKG", "Package not installed" }, + .{ "EREMOTE", "Object is remote" }, + .{ "ENOLINK", "Link has been severed" }, + .{ "EADV", "Advertise error" }, + .{ "ESRMNT", "Srmount error" }, + .{ "ECOMM", "Communication error on send" }, + .{ "EPROTO", "Protocol error" }, + .{ "EMULTIHOP", "Multihop attempted" }, + .{ "EDOTDOT", "RFS specific error" }, + .{ "EBADMSG", "Bad message" }, + .{ "EOVERFLOW", "Value too large for defined data type" }, + .{ "ENOTUNIQ", "Name not unique on network" }, + .{ "EBADFD", "File descriptor in bad state" }, + .{ "EREMCHG", "Remote address changed" }, + .{ "ELIBACC", "Can not access a needed shared library" }, + .{ "ELIBBAD", "Accessing a corrupted shared library" }, + .{ "ELIBSCN", ".lib section in a.out corrupted" }, + .{ "ELIBMAX", "Attempting to link in too many shared libraries" }, + .{ "ELIBEXEC", "Cannot exec a shared library directly" }, + .{ "EILSEQ", "Invalid or incomplete multibyte or wide character" }, + .{ "ERESTART", "Interrupted system call should be restarted" }, + .{ "ESTRPIPE", "Streams pipe error" }, + .{ "EUSERS", "Too many users" }, + .{ "ENOTSOCK", "Socket operation on non-socket" }, + .{ "EDESTADDRREQ", "Destination address required" }, + .{ "EMSGSIZE", "Message too long" }, + .{ "EPROTOTYPE", "Protocol wrong type for socket" }, + .{ "ENOPROTOOPT", "Protocol not available" }, + .{ "EPROTONOSUPPORT", "Protocol not supported" }, + .{ "ESOCKTNOSUPPORT", "Socket type not supported" }, + .{ "EOPNOTSUPP", "Operation not supported" }, + .{ "EPFNOSUPPORT", "Protocol family not supported" }, + .{ "EAFNOSUPPORT", "Address family not supported by protocol" }, + .{ "EADDRINUSE", "Address already in use" }, + .{ "EADDRNOTAVAIL", "Cannot assign requested address" }, + .{ "ENETDOWN", "Network is down" }, + .{ "ENETUNREACH", "Network is unreachable" }, + .{ "ENETRESET", "Network dropped connection on reset" }, + .{ "ECONNABORTED", "Software caused connection abort" }, + .{ "ECONNRESET", "Connection reset by peer" }, + .{ "ENOBUFS", "No buffer space available" }, + .{ "EISCONN", "Transport endpoint is already connected" }, + .{ "ENOTCONN", "Transport endpoint is not connected" }, + .{ "ESHUTDOWN", "Cannot send after transport endpoint shutdown" }, + .{ "ETOOMANYREFS", "Too many references: cannot splice" }, + .{ "ETIMEDOUT", "Connection timed out" }, + .{ "ECONNREFUSED", "Connection refused" }, + .{ "EHOSTDOWN", "Host is down" }, + .{ "EHOSTUNREACH", "No route to host" }, + .{ "EALREADY", "Operation already in progress" }, + .{ "EINPROGRESS", "Operation now in progress" }, + .{ "ESTALE", "Stale file handle" }, + .{ "EUCLEAN", "Structure needs cleaning" }, + .{ "ENOTNAM", "Not a XENIX named type file" }, + .{ "ENAVAIL", "No XENIX semaphores available" }, + .{ "EISNAM", "Is a named type file" }, + .{ "EREMOTEIO", "Remote I/O error" }, + .{ "EDQUOT", "Disk quota exceeded" }, + .{ "ENOMEDIUM", "No medium found" }, + .{ "EMEDIUMTYPE", "Wrong medium type" }, + .{ "ECANCELED", "Operation canceled" }, + .{ "ENOKEY", "Required key not available" }, + .{ "EKEYEXPIRED", "Key has expired" }, + .{ "EKEYREVOKED", "Key has been revoked" }, + .{ "EKEYREJECTED", "Key was rejected by service" }, + .{ "EOWNERDEAD", "Owner died" }, + .{ "ENOTRECOVERABLE", "State not recoverable" }, + .{ "ERFKILL", "Operation not possible due to RF-kill" }, + .{ "EHWPOISON", "Memory page has hardware error" }, + }, + // Mac has slightly different messages. To keep it consistent with bash/coreutils, + // it will use those altered messages. + .mac => &.{ + .{ "E2BIG", "Argument list too long" }, + .{ "EACCES", "Permission denied" }, + .{ "EADDRINUSE", "Address already in use" }, + .{ "EADDRNOTAVAIL", "Can't assign requested address" }, + .{ "EAFNOSUPPORT", "Address family not supported by protocol family" }, + .{ "EAGAIN", "non-blocking and interrupt i/o. Resource temporarily unavailable" }, + .{ "EALREADY", "Operation already in progress" }, + .{ "EAUTH", "Authentication error" }, + .{ "EBADARCH", "Bad CPU type in executable" }, + .{ "EBADEXEC", "Program loading errors. Bad executable" }, + .{ "EBADF", "Bad file descriptor" }, + .{ "EBADMACHO", "Malformed Macho file" }, + .{ "EBADMSG", "Bad message" }, + .{ "EBADRPC", "RPC struct is bad" }, + .{ "EBUSY", "Device / Resource busy" }, + .{ "ECANCELED", "Operation canceled" }, + .{ "ECHILD", "No child processes" }, + .{ "ECONNABORTED", "Software caused connection abort" }, + .{ "ECONNREFUSED", "Connection refused" }, + .{ "ECONNRESET", "Connection reset by peer" }, + .{ "EDEADLK", "Resource deadlock avoided" }, + .{ "EDESTADDRREQ", "Destination address required" }, + .{ "EDEVERR", "Device error, for example paper out" }, + .{ "EDOM", "math software. Numerical argument out of domain" }, + .{ "EDQUOT", "Disc quota exceeded" }, + .{ "EEXIST", "File or folder exists" }, + .{ "EFAULT", "Bad address" }, + .{ "EFBIG", "File too large" }, + .{ "EFTYPE", "Inappropriate file type or format" }, + .{ "EHOSTDOWN", "Host is down" }, + .{ "EHOSTUNREACH", "No route to host" }, + .{ "EIDRM", "Identifier removed" }, + .{ "EILSEQ", "Illegal byte sequence" }, + .{ "EINPROGRESS", "Operation now in progress" }, + .{ "EINTR", "Interrupted system call" }, + .{ "EINVAL", "Invalid argument" }, + .{ "EIO", "Input/output error" }, + .{ "EISCONN", "Socket is already connected" }, + .{ "EISDIR", "Is a directory" }, + .{ "ELOOP", "Too many levels of symbolic links" }, + .{ "EMFILE", "Too many open files" }, + .{ "EMLINK", "Too many links" }, + .{ "EMSGSIZE", "Message too long" }, + .{ "EMULTIHOP", "Reserved" }, + .{ "ENAMETOOLONG", "File name too long" }, + .{ "ENEEDAUTH", "Need authenticator" }, + .{ "ENETDOWN", "ipc/network software - operational errors Network is down" }, + .{ "ENETRESET", "Network dropped connection on reset" }, + .{ "ENETUNREACH", "Network is unreachable" }, + .{ "ENFILE", "Too many open files in system" }, + .{ "ENOATTR", "Attribute not found" }, + .{ "ENOBUFS", "No buffer space available" }, + .{ "ENODATA", "No message available on STREAM" }, + .{ "ENODEV", "Operation not supported by device" }, + .{ "ENOENT", "No such file or directory" }, + .{ "ENOEXEC", "Exec format error" }, + .{ "ENOLCK", "No locks available" }, + .{ "ENOLINK", "Reserved" }, + .{ "ENOMEM", "Out of memory" }, + .{ "ENOMSG", "No message of desired type" }, + .{ "ENOPOLICY", "No such policy registered" }, + .{ "ENOPROTOOPT", "Protocol not available" }, + .{ "ENOSPC", "No space left on device" }, + .{ "ENOSR", "No STREAM resources" }, + .{ "ENOSTR", "Not a STREAM" }, + .{ "ENOSYS", "Function not implemented" }, + .{ "ENOTBLK", "Block device required" }, + .{ "ENOTCONN", "Socket is not connected" }, + .{ "ENOTDIR", "Not a directory" }, + .{ "ENOTEMPTY", "Directory not empty" }, + .{ "ENOTRECOVERABLE", "State not recoverable" }, + .{ "ENOTSOCK", "ipc/network software - argument errors. Socket operation on non-socket" }, + .{ "ENOTSUP", "Operation not supported" }, + .{ "ENOTTY", "Inappropriate ioctl for device" }, + .{ "ENXIO", "Device not configured" }, + .{ "EOVERFLOW", "Value too large to be stored in data type" }, + .{ "EOWNERDEAD", "Previous owner died" }, + .{ "EPERM", "Operation not permitted" }, + .{ "EPFNOSUPPORT", "Protocol family not supported" }, + .{ "EPIPE", "Broken pipe" }, + .{ "EPROCLIM", "quotas & mush. Too many processes" }, + .{ "EPROCUNAVAIL", "Bad procedure for program" }, + .{ "EPROGMISMATCH", "Program version wrong" }, + .{ "EPROGUNAVAIL", "RPC prog. not avail" }, + .{ "EPROTO", "Protocol error" }, + .{ "EPROTONOSUPPORT", "Protocol not supported" }, + .{ "EPROTOTYPE", "Protocol wrong type for socket" }, + .{ "EPWROFF", "Intelligent device errors. Device power is off" }, + .{ "EQFULL", "Interface output queue is full" }, + .{ "ERANGE", "Result too large" }, + .{ "EREMOTE", "Too many levels of remote in path" }, + .{ "EROFS", "Read-only file system" }, + .{ "ERPCMISMATCH", "RPC version wrong" }, + .{ "ESHLIBVERS", "Shared library version mismatch" }, + .{ "ESHUTDOWN", "Can’t send after socket shutdown" }, + .{ "ESOCKTNOSUPPORT", "Socket type not supported" }, + .{ "ESPIPE", "Illegal seek" }, + .{ "ESRCH", "No such process" }, + .{ "ESTALE", "Network File System. Stale NFS file handle" }, + .{ "ETIME", "STREAM ioctl timeout" }, + .{ "ETIMEDOUT", "Operation timed out" }, + .{ "ETOOMANYREFS", "Too many references: can't splice" }, + .{ "ETXTBSY", "Text file busy" }, + .{ "EUSERS", "Too many users" }, + .{ "EWOULDBLOCK", "Operation would block" }, + .{ "EXDEV", "Cross-device link" }, + }, + }; + + const SystemErrno = bun.C.SystemErrno; + var map = std.EnumMap(SystemErrno, [:0]const u8).initFull("unknown error"); + for (entries) |entry| { + const key, const text = entry; + if (@hasField(SystemErrno, key)) { + map.put(@field(SystemErrno, key), text); + } + } + + // sanity check + bun.assert(std.mem.eql(u8, map.get(SystemErrno.ENOENT).?, "No such file or directory")); + + break :brk map; +}; diff --git a/src/sys_uv.zig b/src/sys_uv.zig index 75557717a1..27b87a19aa 100644 --- a/src/sys_uv.zig +++ b/src/sys_uv.zig @@ -214,8 +214,7 @@ pub fn link(from: [:0]const u8, to: [:0]const u8) Maybe(void) { log("uv link({s}, {s}) = {d}", .{ from, to, rc.int() }); return if (rc.errno()) |errno| - // which one goes in the .path field? - .{ .err = .{ .errno = errno, .syscall = .link } } + .{ .err = .{ .errno = errno, .syscall = .link, .path = from, .dest = to } } else .{ .result = {} }; } diff --git a/src/transpiler.zig b/src/transpiler.zig index 5836e0e20c..cb5eb891e5 100644 --- a/src/transpiler.zig +++ b/src/transpiler.zig @@ -260,6 +260,7 @@ pub const PluginRunner = struct { importer, target, ) orelse return null; + if (!on_resolve_plugin.isObject()) return null; const path_value = try on_resolve_plugin.get(global, "path") orelse return null; if (path_value.isEmptyOrUndefinedOrNull()) return null; if (!path_value.isString()) { diff --git a/src/windows.zig b/src/windows.zig index b7d7d6860c..3bc67c7d4b 100644 --- a/src/windows.zig +++ b/src/windows.zig @@ -3068,7 +3068,7 @@ pub fn translateNTStatusToErrno(err: win32.NTSTATUS) bun.C.E { } else .BUSY, .OBJECT_NAME_INVALID => if (comptime Environment.isDebug) brk: { bun.Output.debugWarn("Received OBJECT_NAME_INVALID, indicates a file path conversion issue.", .{}); - std.debug.dumpCurrentStackTrace(null); + bun.crash_handler.dumpCurrentStackTrace(null); break :brk .INVAL; } else .INVAL, diff --git a/src/windows_c.zig b/src/windows_c.zig index df725226c6..956fc5d81c 100644 --- a/src/windows_c.zig +++ b/src/windows_c.zig @@ -850,149 +850,6 @@ pub const SystemErrno = enum(u16) { if (code >= max) return null; return @as(SystemErrno, @enumFromInt(code)); } - - pub fn label(this: SystemErrno) ?[:0]const u8 { - return labels.get(this) orelse null; - } - - const LabelMap = std.enums.EnumMap(SystemErrno, [:0]const u8); - pub const labels: LabelMap = brk: { - var map: LabelMap = LabelMap.initFull(""); - - map.put(.EPERM, "Operation not permitted"); - map.put(.ENOENT, "No such file or directory"); - map.put(.ESRCH, "No such process"); - map.put(.EINTR, "Interrupted system call"); - map.put(.EIO, "I/O error"); - map.put(.ENXIO, "No such device or address"); - map.put(.E2BIG, "Argument list too long"); - map.put(.ENOEXEC, "Exec format error"); - map.put(.EBADF, "Bad file descriptor"); - map.put(.ECHILD, "No child processes"); - map.put(.EAGAIN, "Try again"); - map.put(.EOF, "End of file"); - map.put(.ENOMEM, "Out of memory"); - map.put(.EACCES, "Permission denied"); - map.put(.EFAULT, "Bad address"); - map.put(.ENOTBLK, "Block device required"); - map.put(.EBUSY, "Device or resource busy"); - map.put(.EEXIST, "File or folder exists"); - map.put(.EXDEV, "Cross-device link"); - map.put(.ENODEV, "No such device"); - map.put(.ENOTDIR, "Not a directory"); - map.put(.EISDIR, "Is a directory"); - map.put(.EINVAL, "Invalid argument"); - map.put(.ENFILE, "File table overflow"); - map.put(.EMFILE, "Too many open files"); - map.put(.ECHARSET, "Invalid or incomplete multibyte or wide character"); - map.put(.ENOTTY, "Not a typewriter"); - map.put(.ETXTBSY, "Text file busy"); - map.put(.EFBIG, "File too large"); - map.put(.ENOSPC, "No space left on device"); - map.put(.ESPIPE, "Illegal seek"); - map.put(.EROFS, "Read-only file system"); - map.put(.EMLINK, "Too many links"); - map.put(.EPIPE, "Broken pipe"); - map.put(.EDOM, "Math argument out of domain of func"); - map.put(.ERANGE, "Math result not representable"); - map.put(.EDEADLK, "Resource deadlock would occur"); - map.put(.ENAMETOOLONG, "File name too long"); - map.put(.ENOLCK, "No record locks available"); - map.put(.EUNKNOWN, "An unknown error occurred"); - map.put(.ENOSYS, "Function not implemented"); - map.put(.ENOTEMPTY, "Directory not empty"); - map.put(.ELOOP, "Too many symbolic links encountered"); - map.put(.ENOMSG, "No message of desired type"); - map.put(.EIDRM, "Identifier removed"); - map.put(.ECHRNG, "Channel number out of range"); - map.put(.EL2NSYNC, "Level 2 not synchronized"); - map.put(.EL3HLT, "Level 3 halted"); - map.put(.EL3RST, "Level 3 reset"); - map.put(.ELNRNG, "Link number out of range"); - map.put(.EUNATCH, "Protocol driver not attached"); - map.put(.ENOCSI, "No CSI structure available"); - map.put(.EL2HLT, "Level 2 halted"); - map.put(.EBADE, "Invalid exchange"); - map.put(.EBADR, "Invalid request descriptor"); - map.put(.EXFULL, "Exchange full"); - map.put(.ENOANO, "No anode"); - map.put(.EBADRQC, "Invalid request code"); - map.put(.EBADSLT, "Invalid slot"); - map.put(.EBFONT, "Bad font file format"); - map.put(.ENOSTR, "Device not a stream"); - map.put(.ENODATA, "No data available"); - map.put(.ETIME, "Timer expired"); - map.put(.ENOSR, "Out of streams resources"); - map.put(.ENONET, "Machine is not on the network"); - map.put(.ENOPKG, "Package not installed"); - map.put(.EREMOTE, "Object is remote"); - map.put(.ENOLINK, "Link has been severed"); - map.put(.EADV, "Advertise error"); - map.put(.ESRMNT, "Srmount error"); - map.put(.ECOMM, "Communication error on send"); - map.put(.EPROTO, "Protocol error"); - map.put(.EMULTIHOP, "Multihop attempted"); - map.put(.EDOTDOT, "RFS specific error"); - map.put(.EBADMSG, "Not a data message"); - map.put(.EOVERFLOW, "Value too large for defined data type"); - map.put(.ENOTUNIQ, "Name not unique on network"); - map.put(.EBADFD, "File descriptor in bad state"); - map.put(.EREMCHG, "Remote address changed"); - map.put(.ELIBACC, "Can not access a needed shared library"); - map.put(.ELIBBAD, "Accessing a corrupted shared library"); - map.put(.ELIBSCN, "lib section in a.out corrupted"); - map.put(.ELIBMAX, "Attempting to link in too many shared libraries"); - map.put(.ELIBEXEC, "Cannot exec a shared library directly"); - map.put(.EILSEQ, "Illegal byte sequence"); - map.put(.ERESTART, "Interrupted system call should be restarted"); - map.put(.ESTRPIPE, "Streams pipe error"); - map.put(.EUSERS, "Too many users"); - map.put(.ENOTSOCK, "Socket operation on non-socket"); - map.put(.EDESTADDRREQ, "Destination address required"); - map.put(.EMSGSIZE, "Message too long"); - map.put(.EPROTOTYPE, "Protocol wrong type for socket"); - map.put(.ENOPROTOOPT, "Protocol not available"); - map.put(.EPROTONOSUPPORT, "Protocol not supported"); - map.put(.ESOCKTNOSUPPORT, "Socket type not supported"); - map.put(.ENOTSUP, "Operation not supported on transport endpoint"); - map.put(.EPFNOSUPPORT, "Protocol family not supported"); - map.put(.EAFNOSUPPORT, "Address family not supported by protocol"); - map.put(.EADDRINUSE, "Address already in use"); - map.put(.EADDRNOTAVAIL, "Cannot assign requested address"); - map.put(.ENETDOWN, "Network is down"); - map.put(.ENETUNREACH, "Network is unreachable"); - map.put(.ENETRESET, "Network dropped connection because of reset"); - map.put(.ECONNABORTED, "Software caused connection abort"); - map.put(.ECONNRESET, "Connection reset by peer"); - map.put(.ENOBUFS, "No buffer space available"); - map.put(.EISCONN, "Transport endpoint is already connected"); - map.put(.ENOTCONN, "Transport endpoint is not connected"); - map.put(.ESHUTDOWN, "Cannot send after transport endpoint shutdown"); - map.put(.ETOOMANYREFS, "Too many references: cannot splice"); - map.put(.ETIMEDOUT, "Connection timed out"); - map.put(.ECONNREFUSED, "Connection refused"); - map.put(.EHOSTDOWN, "Host is down"); - map.put(.EHOSTUNREACH, "No route to host"); - map.put(.EALREADY, "Operation already in progress"); - map.put(.EINPROGRESS, "Operation now in progress"); - map.put(.ESTALE, "Stale NFS file handle"); - map.put(.EUCLEAN, "Structure needs cleaning"); - map.put(.ENOTNAM, "Not a XENIX named type file"); - map.put(.ENAVAIL, "No XENIX semaphores available"); - map.put(.EISNAM, "Is a named type file"); - map.put(.EREMOTEIO, "Remote I/O error"); - map.put(.EDQUOT, "Quota exceeded"); - map.put(.ENOMEDIUM, "No medium found"); - map.put(.EMEDIUMTYPE, "Wrong medium type"); - map.put(.ECANCELED, "Operation Canceled"); - map.put(.ENOKEY, "Required key not available"); - map.put(.EKEYEXPIRED, "Key has expired"); - map.put(.EKEYREVOKED, "Key has been revoked"); - map.put(.EKEYREJECTED, "Key was rejected by service"); - map.put(.EOWNERDEAD, "Owner died"); - map.put(.ENOTRECOVERABLE, "State not recoverable"); - break :brk map; - }; }; pub const UV_E2BIG = -uv.UV_E2BIG; diff --git a/test/cli/install/migration/migrate.test.ts b/test/cli/install/migration/migrate.test.ts index ea083297a3..b1527621f4 100644 --- a/test/cli/install/migration/migrate.test.ts +++ b/test/cli/install/migration/migrate.test.ts @@ -113,6 +113,7 @@ test("migrate from npm lockfile that is missing `resolved` properties", async () test("npm lockfile with relative workspaces", async () => { const testDir = tmpdirSync(); + console.log(join(import.meta.dir, "lockfile-with-workspaces"), testDir, { recursive: true }); fs.cpSync(join(import.meta.dir, "lockfile-with-workspaces"), testDir, { recursive: true }); const { exitCode, stderr } = Bun.spawnSync([bunExe(), "install"], { env: bunEnv, diff --git a/test/js/bun/http/fetch-file-upload.test.ts b/test/js/bun/http/fetch-file-upload.test.ts index ae8a26a870..b779e3b6c5 100644 --- a/test/js/bun/http/fetch-file-upload.test.ts +++ b/test/js/bun/http/fetch-file-upload.test.ts @@ -171,7 +171,7 @@ test("missing file throws the expected error", async () => { proxy: "http://localhost:3000", }); expect(Bun.peek.status(resp)).toBe("rejected"); - expect(async () => await resp).toThrow("No such file or directory"); + expect(async () => await resp).toThrow("no such file or directory"); } }); Bun.gc(true); diff --git a/test/js/bun/io/bun-write.test.js b/test/js/bun/io/bun-write.test.js index 18a744c336..926ebba987 100644 --- a/test/js/bun/io/bun-write.test.js +++ b/test/js/bun/io/bun-write.test.js @@ -479,7 +479,7 @@ describe("ENOENT", () => { const file = join(dir, "file"); try { expect(async () => await Bun.write(file, "contents", { createPath: false })).toThrow( - "No such file or directory", + "no such file or directory", ); expect(fs.existsSync(file)).toBe(false); } finally { diff --git a/test/js/bun/shell/bunshell.test.ts b/test/js/bun/shell/bunshell.test.ts index 31b59ffa41..0cbabced2e 100644 --- a/test/js/bun/shell/bunshell.test.ts +++ b/test/js/bun/shell/bunshell.test.ts @@ -781,7 +781,7 @@ booga" test("error without recursive option", async () => { const { stderr } = await $`rm -v ${temp_dir}`; - expect(stderr.toString()).toEqual(`rm: ${temp_dir}: is a directory\n`); + expect(stderr.toString()).toEqual(`rm: ${temp_dir}: Is a directory\n`); }); test("recursive", async () => { diff --git a/test/js/bun/spawn/spawn.test.ts b/test/js/bun/spawn/spawn.test.ts index 4f2adf2035..50e2c01426 100644 --- a/test/js/bun/spawn/spawn.test.ts +++ b/test/js/bun/spawn/spawn.test.ts @@ -81,7 +81,7 @@ for (let [gcTick, label] of [ cmd: ["node", "-e", "console.log('hi')"], cwd: "./this-should-not-exist", }); - }).toThrow("No such file or directory"); + }).toThrow("no such file or directory"); }); }); @@ -525,7 +525,7 @@ for (let [gcTick, label] of [ cmd: ["node", "-e", "console.log('hi')"], cwd: "./this-should-not-exist", }); - }).toThrow("No such file or directory"); + }).toThrow("no such file or directory"); }); }); }); diff --git a/test/js/bun/test/stack.test.ts b/test/js/bun/test/stack.test.ts index 2a55d5b0e1..db623a38ad 100644 --- a/test/js/bun/test/stack.test.ts +++ b/test/js/bun/test/stack.test.ts @@ -113,11 +113,11 @@ test("throwing inside an error suppresses the error and continues printing prope const { stderr, exitCode } = result; - expect(stderr.toString().trim()).toStartWith(`error: No such file or directory + expect(stderr.toString().trim()).toStartWith(`ENOENT: no such file or directory, open 'this-file-path-is-bad' path: "this-file-path-is-bad", syscall: "open", errno: -2, - code: "ENOENT", + code: "ENOENT" `); expect(exitCode).toBe(1); }); diff --git a/test/js/bun/util/inspect.test.js b/test/js/bun/util/inspect.test.js index 6ee2d89b6a..347aaddb33 100644 --- a/test/js/bun/util/inspect.test.js +++ b/test/js/bun/util/inspect.test.js @@ -225,7 +225,9 @@ it("TypedArray prints", () => { expect(input).toBe(`${TypedArray.name}(${buffer.length}) [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]`); for (let i = 1; i < buffer.length + 1; i++) { expect(Bun.inspect(buffer.subarray(i))).toBe( - `${TypedArray.name}(${buffer.length - i}) [ ` + [...buffer.subarray(i)].join(", ") + " ]", + buffer.length - i === 0 + ? `${TypedArray.name}(${buffer.length - i}) []` + : `${TypedArray.name}(${buffer.length - i}) [ ` + [...buffer.subarray(i)].join(", ") + " ]", ); } } @@ -239,9 +241,11 @@ it("BigIntArray", () => { expect(input).toBe(`${TypedArray.name}(${buffer.length}) [ 1n, 2n, 3n, 4n, 5n, 6n, 7n, 8n, 9n, 10n ]`); for (let i = 1; i < buffer.length + 1; i++) { expect(Bun.inspect(buffer.subarray(i))).toBe( - `${TypedArray.name}(${buffer.length - i}) [ ` + - [...buffer.subarray(i)].map(a => a.toString(10) + "n").join(", ") + - " ]", + buffer.length - i === 0 + ? `${TypedArray.name}(${buffer.length - i}) []` + : `${TypedArray.name}(${buffer.length - i}) [ ` + + [...buffer.subarray(i)].map(a => a.toString(10) + "n").join(", ") + + " ]", ); } } @@ -255,7 +259,9 @@ for (let TypedArray of [Float32Array, Float64Array]) { expect(input).toBe(`${TypedArray.name}(${buffer.length}) [ ${[Math.fround(42.68)].join(", ")} ]`); for (let i = 1; i < buffer.length + 1; i++) { expect(Bun.inspect(buffer.subarray(i))).toBe( - `${TypedArray.name}(${buffer.length - i}) [ ` + [...buffer.subarray(i)].join(", ") + " ]", + buffer.length - i === 0 + ? `${TypedArray.name}(${buffer.length - i}) []` + : `${TypedArray.name}(${buffer.length - i}) [ ` + [...buffer.subarray(i)].join(", ") + " ]", ); } }); @@ -269,7 +275,9 @@ for (let TypedArray of [Float32Array, Float64Array]) { ); for (let i = 1; i < buffer.length + 1; i++) { expect(Bun.inspect(buffer.subarray(i))).toBe( - `${TypedArray.name}(${buffer.length - i}) [ ` + [...buffer.subarray(i)].join(", ") + " ]", + buffer.length - i === 0 + ? `${TypedArray.name}(${buffer.length - i}) []` + : `${TypedArray.name}(${buffer.length - i}) [ ` + [...buffer.subarray(i)].join(", ") + " ]", ); } }); @@ -553,12 +561,7 @@ describe("console.logging class displays names and extends", async () => { class A {} const cases = [A, class B extends A {}, class extends A {}, class {}]; - const expected_logs = [ - "[class A]", - "[class B extends A]", - "[class (anonymous) extends A]", - "[class (anonymous)]", - ]; + const expected_logs = ["[class A]", "[class B extends A]", "[class (anonymous) extends A]", "[class (anonymous)]"]; for (let i = 0; i < cases.length; i++) { it(expected_logs[i], () => { diff --git a/test/js/bun/util/reportError.test.ts b/test/js/bun/util/reportError.test.ts index f1e6008991..d2849fe901 100644 --- a/test/js/bun/util/reportError.test.ts +++ b/test/js/bun/util/reportError.test.ts @@ -43,9 +43,9 @@ error error Uint8Array(1) [ 0 ] error -Uint8Array(0) [ ] +Uint8Array(0) [] error -ArrayBuffer(0) [ ] +ArrayBuffer(0) [] error ArrayBuffer(1) [ 0 ] error: string diff --git a/test/js/node/crypto/node-crypto.test.js b/test/js/node/crypto/node-crypto.test.js index f8cb1fb62f..af41d61407 100644 --- a/test/js/node/crypto/node-crypto.test.js +++ b/test/js/node/crypto/node-crypto.test.js @@ -439,7 +439,7 @@ describe("createHash", () => { it("repeated calls doesnt segfault", () => { function fn() { - crypto.createHash("sha1").update(Math.random(), "ascii").digest("base64"); + crypto.createHash("sha1").update(Math.random().toString(), "ascii").digest("base64"); } for (let i = 0; i < 10; i++) fn(); diff --git a/test/js/node/fs/cp.test.ts b/test/js/node/fs/cp.test.ts index 134dbd016d..4972aac905 100644 --- a/test/js/node/fs/cp.test.ts +++ b/test/js/node/fs/cp.test.ts @@ -1,6 +1,6 @@ import { describe, expect, jest, test } from "bun:test"; import fs from "fs"; -import { tempDirWithFiles } from "harness"; +import { isWindows, tempDirWithFiles } from "harness"; import { join } from "path"; const impls = [ diff --git a/test/js/node/fs/fs-oom.test.ts b/test/js/node/fs/fs-oom.test.ts index a859dc6a89..22749c15aa 100644 --- a/test/js/node/fs/fs-oom.test.ts +++ b/test/js/node/fs/fs-oom.test.ts @@ -7,14 +7,14 @@ setSyntheticAllocationLimitForTesting(128 * 1024 * 1024); // /dev/zero reports a size of 0. So we need a separate test for reDgular files that are huge. if (isPosix) { test("fs.readFileSync(/dev/zero) should throw an OOM without crashing the process.", () => { - expect(() => readFileSync("/dev/zero")).toThrow("Out of memory"); + expect(() => readFileSync("/dev/zero")).toThrow("ENOMEM: not enough memory, read '/dev/zero'"); Bun.gc(true); }); test.each(["utf8", "ucs2", "latin1", "hex", "base64", "base64url"] as const)( "fs.readFileSync(/dev/zero, '%s') should throw an OOM without crashing the process.", encoding => { - expect(() => readFileSync("/dev/zero", encoding)).toThrow("Out of memory"); + expect(() => readFileSync("/dev/zero", encoding)).toThrow("ENOMEM: not enough memory, read '/dev/zero'"); Bun.gc(true); }, ); @@ -29,7 +29,7 @@ if (isLinux) { let buf = new Uint8Array(8 * 1024 * 1024); buf.fill(42); for (let i = 0; i < 1024 * 1024 * 16 + 1; i += buf.byteLength) { - writeSync(memfd, buf, i, buf.byteLength); + writeSync(memfd, buf, 0, buf.byteLength, i); } })(memfd); Bun.gc(true); @@ -37,7 +37,7 @@ if (isLinux) { try { expect(() => (encoding === "buffer" ? readFileSync(memfd) : readFileSync(memfd, encoding))).toThrow( - "Out of memory", + "ENOMEM: not enough memory", ); } finally { Bun.gc(true); diff --git a/test/js/node/fs/fs.test.ts b/test/js/node/fs/fs.test.ts index 86fc06de30..faa7680fde 100644 --- a/test/js/node/fs/fs.test.ts +++ b/test/js/node/fs/fs.test.ts @@ -584,8 +584,8 @@ describe("mkdirSync", () => { }); it("should throw ENOENT for empty string", () => { - expect(() => mkdirSync("", { recursive: true })).toThrow("No such file or directory"); - expect(() => mkdirSync("")).toThrow("No such file or directory"); + expect(() => mkdirSync("", { recursive: true })).toThrow("no such file or directory"); + expect(() => mkdirSync("")).toThrow("no such file or directory"); }); it("throws for invalid options", () => { @@ -673,7 +673,7 @@ it("promises.readFile", async () => { expect.unreachable(); } catch (e: any) { expect(e).toBeInstanceOf(Error); - expect(e.message).toBe("No such file or directory"); + expect(e.message).toBe("ENOENT: no such file or directory, open '/i-dont-exist'"); expect(e.code).toBe("ENOENT"); expect(e.errno).toBe(-2); expect(e.path).toBe("/i-dont-exist"); @@ -1004,10 +1004,10 @@ it("statSync throwIfNoEntry", () => { it("statSync throwIfNoEntry: true", () => { const path = join(tmpdirSync(), "does", "not", "exist"); - expect(() => statSync(path, { throwIfNoEntry: true })).toThrow("No such file or directory"); - expect(() => statSync(path)).toThrow("No such file or directory"); - expect(() => lstatSync(path, { throwIfNoEntry: true })).toThrow("No such file or directory"); - expect(() => lstatSync(path)).toThrow("No such file or directory"); + expect(() => statSync(path, { throwIfNoEntry: true })).toThrow("no such file or directory"); + expect(() => statSync(path)).toThrow("no such file or directory"); + expect(() => lstatSync(path, { throwIfNoEntry: true })).toThrow("no such file or directory"); + expect(() => lstatSync(path)).toThrow("no such file or directory"); }); it("stat == statSync", async () => { @@ -2425,9 +2425,9 @@ describe("fs/promises", () => { const text = await new Response(subprocess.stdout).text(); const node = JSON.parse(text); expect(bun.length).toEqual(node.length); - expect([...new Set(node.map(v => v.path))]).toEqual([full]); - expect([...new Set(bun.map(v => v.path))]).toEqual([full]); - expect(bun.map(v => join(v.path, v.name)).sort()).toEqual(node.map(v => join(v.path, v.name)).sort()); + expect([...new Set(node.map(v => v.parentPath))]).toEqual([full]); + expect([...new Set(bun.map(v => v.parentPath))]).toEqual([full]); + expect(bun.map(v => join(v.parentPath, v.name)).sort()).toEqual(node.map(v => join(v.path, v.name)).sort()); }, 100000); it("readdir(path, {withFileTypes: true, recursive: true}) produces the same result as Node.js", async () => { @@ -2695,7 +2695,7 @@ it("fstatSync(decimal)", () => { expect(() => fstatSync(eval("-1.0"))).toThrow(); expect(() => fstatSync(eval("Infinity"))).toThrow(); expect(() => fstatSync(eval("-Infinity"))).toThrow(); - expect(() => fstatSync(2147483647 + 1)).toThrow(expect.objectContaining({ code: "ERR_INVALID_ARG_TYPE" })); // > max int32 is not valid in most C APIs still. + expect(() => fstatSync(2147483647 + 1)).toThrow(expect.objectContaining({ code: "ERR_OUT_OF_RANGE" })); // > max int32 is not valid in most C APIs still. expect(() => fstatSync(2147483647)).toThrow(expect.objectContaining({ code: "EBADF" })); // max int32 is a valid fd }); @@ -3279,26 +3279,26 @@ it("new Stats", () => { it("test syscall errno, issue#4198", () => { const path = `${tmpdir()}/non-existent-${Date.now()}.txt`; - expect(() => openSync(path, "r")).toThrow("No such file or directory"); - expect(() => readSync(2147483640, Buffer.alloc(1))).toThrow("Bad file descriptor"); - expect(() => readlinkSync(path)).toThrow("No such file or directory"); - expect(() => realpathSync(path)).toThrow("No such file or directory"); - expect(() => readFileSync(path)).toThrow("No such file or directory"); - expect(() => renameSync(path, `${path}.2`)).toThrow("No such file or directory"); - expect(() => statSync(path)).toThrow("No such file or directory"); - expect(() => unlinkSync(path)).toThrow("No such file or directory"); - expect(() => rmSync(path)).toThrow("No such file or directory"); - expect(() => rmdirSync(path)).toThrow("No such file or directory"); - expect(() => closeSync(2147483640)).toThrow("Bad file descriptor"); + expect(() => openSync(path, "r")).toThrow("no such file or directory"); + expect(() => readSync(2147483640, Buffer.alloc(1))).toThrow("bad file descriptor"); + expect(() => readlinkSync(path)).toThrow("no such file or directory"); + expect(() => realpathSync(path)).toThrow("no such file or directory"); + expect(() => readFileSync(path)).toThrow("no such file or directory"); + expect(() => renameSync(path, `${path}.2`)).toThrow("no such file or directory"); + expect(() => statSync(path)).toThrow("no such file or directory"); + expect(() => unlinkSync(path)).toThrow("no such file or directory"); + expect(() => rmSync(path)).toThrow("no such file or directory"); + expect(() => rmdirSync(path)).toThrow("no such file or directory"); + expect(() => closeSync(2147483640)).toThrow("bad file descriptor"); mkdirSync(path); - expect(() => mkdirSync(path)).toThrow("File or folder exists"); + expect(() => mkdirSync(path)).toThrow("file already exists"); expect(() => unlinkSync(path)).toThrow( ( { - "darwin": "Operation not permitted", - "linux": "Is a directory", - "win32": "Operation not permitted", + "darwin": "operation not permitted", + "linux": "illegal operation on a directory", + "win32": "operation not permitted", } as any )[process.platform], ); diff --git a/test/js/node/process-binding.test.ts b/test/js/node/process-binding.test.ts index a0702ea10b..e0eea52644 100644 --- a/test/js/node/process-binding.test.ts +++ b/test/js/node/process-binding.test.ts @@ -25,6 +25,6 @@ describe("process.binding", () => { const map = uv.getErrorMap(); expect(map).toBeDefined(); - expect(map.get(-56)).toEqual(["EISCONN", "socket is already connected"]); + expect(map.get(uv.UV_EISCONN)).toEqual(["EISCONN", "socket is already connected"]); }); }); diff --git a/test/js/node/test/common/index.js b/test/js/node/test/common/index.js index 9c283bb4d4..2e56cef098 100644 --- a/test/js/node/test/common/index.js +++ b/test/js/node/test/common/index.js @@ -30,7 +30,7 @@ const net = require('net'); // Do not require 'os' until needed so that test-os-checked-function can // monkey patch it. If 'os' is required here, that test will fail. const path = require('path'); -const { inspect } = require('util'); +const { inspect, getCallSites } = require('util'); const { isMainThread } = require('worker_threads'); const { isModuleNamespaceObject } = require('util/types'); @@ -147,6 +147,8 @@ const isOpenBSD = process.platform === 'openbsd'; const isLinux = process.platform === 'linux'; const isMacOS = process.platform === 'darwin'; const isASan = process.config.variables.asan === 1; +const isRiscv64 = process.arch === 'riscv64'; +const isDebug = process.features.debug; const isPi = (() => { try { // Normal Raspberry Pi detection is to find the `Raspberry Pi` string in @@ -176,8 +178,7 @@ if (process.env.NODE_TEST_WITH_ASYNC_HOOKS) { const destroydIdsList = {}; const destroyListList = {}; const initHandles = {}; - const { internalBinding } = require('internal/test/binding'); - const async_wrap = internalBinding('async_wrap'); + const async_wrap = process.binding('async_wrap'); process.on('exit', () => { // Iterate through handles to make sure nothing crashes @@ -284,7 +285,7 @@ function platformTimeout(ms) { const multipliers = typeof ms === 'bigint' ? { two: 2n, four: 4n, seven: 7n } : { two: 2, four: 4, seven: 7 }; - if (process.features.debug) + if (isDebug) ms = multipliers.two * ms; if (exports.isAIX || exports.isIBMi) @@ -293,6 +294,10 @@ function platformTimeout(ms) { if (isPi) return multipliers.two * ms; // Raspberry Pi devices + if (isRiscv64) { + return multipliers.four * ms; + } + return ms; } @@ -564,8 +569,7 @@ function _mustCallInner(fn, criteria = 1, field) { } function hasMultiLocalhost() { - const { internalBinding } = require('internal/test/binding'); - const { TCP, constants: TCPConstants } = internalBinding('tcp_wrap'); + const { TCP, constants: TCPConstants } = process.binding('tcp_wrap'); const t = new TCP(TCPConstants.SOCKET); const ret = t.bind('127.0.0.2', 0); t.close(); @@ -976,6 +980,32 @@ function spawnPromisified(...args) { }); } +/** + * Escape values in a string template literal. On Windows, this function + * does not escape anything (which is fine for paths, as `"` is not a valid char + * in a path on Windows), so you should use it only to escape paths – or other + * values on tests which are skipped on Windows. + * This function is meant to be used for tagged template strings. + * @returns {[string, object | undefined]} An array that can be passed as + * arguments to `exec` or `execSync`. + */ +function escapePOSIXShell(cmdParts, ...args) { + if (common.isWindows) { + // On Windows, paths cannot contain `"`, so we can return the string unchanged. + return [String.raw({ raw: cmdParts }, ...args)]; + } + // On POSIX shells, we can pass values via the env, as there's a standard way for referencing a variable. + const env = { ...process.env }; + let cmd = cmdParts[0]; + for (let i = 0; i < args.length; i++) { + const envVarName = `ESCAPED_${i}`; + env[envVarName] = args[i]; + cmd += '${' + envVarName + '}' + cmdParts[i + 1]; + } + + return [cmd, { env }]; +}; + function getPrintedStackTrace(stderr) { const lines = stderr.split('\n'); @@ -1039,6 +1069,7 @@ const common = { childShouldThrowAndAbort, createZeroFilledFile, defaultAutoSelectFamilyAttemptTimeout, + escapePOSIXShell, expectsError, expectRequiredModule, expectWarning, @@ -1056,6 +1087,7 @@ const common = { invalidArgTypeHelper, isAlive, isASan, + isDebug, isDumbTerminal, isFreeBSD, isLinux, @@ -1207,6 +1239,15 @@ const common = { get checkoutEOL() { return fs.readFileSync(__filename).includes('\r\n') ? '\r\n' : '\n'; }, + + get isInsideDirWithUnusualChars() { + return __dirname.includes('%') || + (!isWindows && __dirname.includes('\\')) || + __dirname.includes('$') || + __dirname.includes('\n') || + __dirname.includes('\r') || + __dirname.includes('\t'); + }, }; const validProperties = new Set(Object.keys(common)); diff --git a/test/js/node/test/parallel/test-binding-constants.js b/test/js/node/test/parallel/test-binding-constants.js index 4a96b7c744..bd6e533816 100644 --- a/test/js/node/test/parallel/test-binding-constants.js +++ b/test/js/node/test/parallel/test-binding-constants.js @@ -2,8 +2,7 @@ 'use strict'; require('../common'); -const { internalBinding } = require('internal/test/binding'); -const constants = internalBinding('constants'); +const constants = process.binding('constants'); const assert = require('assert'); assert.deepStrictEqual( diff --git a/test/js/node/test/parallel/test-fs-access.js b/test/js/node/test/parallel/test-fs-access.js new file mode 100644 index 0000000000..e4ce90adc1 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-access.js @@ -0,0 +1,237 @@ +'use strict'; + +// This tests that fs.access and fs.accessSync works as expected +// and the errors thrown from these APIs include the desired properties + +const common = require('../common'); +if (!common.isWindows && process.getuid() === 0) + common.skip('as this test should not be run as `root`'); + +if (common.isIBMi) + common.skip('IBMi has a different access permission mechanism'); + +const assert = require('assert'); +const fs = require('fs'); + +const { UV_ENOENT } = process.binding('uv'); + +const tmpdir = require('../common/tmpdir'); +const doesNotExist = tmpdir.resolve('__this_should_not_exist'); +const readOnlyFile = tmpdir.resolve('read_only_file'); +const readWriteFile = tmpdir.resolve('read_write_file'); + +function createFileWithPerms(file, mode) { + fs.writeFileSync(file, ''); + fs.chmodSync(file, mode); +} + +tmpdir.refresh(); +createFileWithPerms(readOnlyFile, 0o444); +createFileWithPerms(readWriteFile, 0o666); + +// On non-Windows supported platforms, fs.access(readOnlyFile, W_OK, ...) +// always succeeds if node runs as the super user, which is sometimes the +// case for tests running on our continuous testing platform agents. +// +// In this case, this test tries to change its process user id to a +// non-superuser user so that the test that checks for write access to a +// read-only file can be more meaningful. +// +// The change of user id is done after creating the fixtures files for the same +// reason: the test may be run as the superuser within a directory in which +// only the superuser can create files, and thus it may need superuser +// privileges to create them. +// +// There's not really any point in resetting the process' user id to 0 after +// changing it to 'nobody', since in the case that the test runs without +// superuser privilege, it is not possible to change its process user id to +// superuser. +// +// It can prevent the test from removing files created before the change of user +// id, but that's fine. In this case, it is the responsibility of the +// continuous integration platform to take care of that. +let hasWriteAccessForReadonlyFile = false; +if (!common.isWindows && process.getuid() === 0) { + hasWriteAccessForReadonlyFile = true; + try { + process.setuid('nobody'); + hasWriteAccessForReadonlyFile = false; + } catch { + // Continue regardless of error. + } +} + +assert.strictEqual(typeof fs.constants.F_OK, 'number'); +assert.strictEqual(typeof fs.constants.R_OK, 'number'); +assert.strictEqual(typeof fs.constants.W_OK, 'number'); +assert.strictEqual(typeof fs.constants.X_OK, 'number'); + +const throwNextTick = (e) => { process.nextTick(() => { throw e; }); }; + +fs.access(__filename, common.mustCall(function(...args) { + assert.deepStrictEqual(args, [null]); +})); +fs.promises.access(__filename) + .then(common.mustCall()) + .catch(throwNextTick); +fs.access(__filename, fs.constants.R_OK, common.mustCall(function(...args) { + assert.deepStrictEqual(args, [null]); +})); +fs.promises.access(__filename, fs.constants.R_OK) + .then(common.mustCall()) + .catch(throwNextTick); +fs.access(readOnlyFile, fs.constants.R_OK, common.mustCall(function(...args) { + assert.deepStrictEqual(args, [null]); +})); +fs.promises.access(readOnlyFile, fs.constants.R_OK) + .then(common.mustCall()) + .catch(throwNextTick); + +{ + const expectedError = (err) => { + assert.notStrictEqual(err, null); + assert.strictEqual(err.code, 'ENOENT'); + assert.strictEqual(err.path, doesNotExist); + }; + const expectedErrorPromise = (err) => { + expectedError(err); + // TODO: https://github.com/oven-sh/bun/issues/2704 + // assert.match(err.stack, /at async Object\.access/); + }; + fs.access(doesNotExist, common.mustCall(expectedError)); + fs.promises.access(doesNotExist) + .then(common.mustNotCall(), common.mustCall(expectedErrorPromise)) + .catch(throwNextTick); +} + +{ + function expectedError(err) { + assert.strictEqual(this, undefined); + if (hasWriteAccessForReadonlyFile) { + assert.ifError(err); + } else { + assert.notStrictEqual(err, null); + assert.strictEqual(err.path, readOnlyFile); + } + } + fs.access(readOnlyFile, fs.constants.W_OK, common.mustCall(expectedError)); + fs.promises.access(readOnlyFile, fs.constants.W_OK) + .then(common.mustNotCall(), common.mustCall(expectedError)) + .catch(throwNextTick); +} + +{ + const expectedError = (err) => { + assert.strictEqual(err.code, 'ERR_INVALID_ARG_TYPE'); + assert.ok(err instanceof TypeError); + return true; + }; + assert.throws( + () => { fs.access(100, fs.constants.F_OK, common.mustNotCall()); }, + expectedError + ); + + fs.promises.access(100, fs.constants.F_OK) + .then(common.mustNotCall(), common.mustCall(expectedError)) + .catch(throwNextTick); +} + +assert.throws( + () => { + fs.access(__filename, fs.constants.F_OK); + }, + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + }); + +assert.throws( + () => { + fs.access(__filename, fs.constants.F_OK, common.mustNotMutateObjectDeep({})); + }, + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + }); + +// Regular access should not throw. +fs.accessSync(__filename); +const mode = fs.constants.R_OK | fs.constants.W_OK; +fs.accessSync(readWriteFile, mode); + +// Invalid modes should throw. +[ + false, + 1n, + { [Symbol.toPrimitive]() { return fs.constants.R_OK; } }, + [1], + 'r', +].forEach((mode, i) => { + console.log(mode, i); + assert.throws( + () => fs.access(readWriteFile, mode, common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + } + ); + assert.throws( + () => fs.accessSync(readWriteFile, mode), + { + code: 'ERR_INVALID_ARG_TYPE', + } + ); +}); + +// Out of range modes should throw +[ + -1, + 8, + Infinity, + NaN, +].forEach((mode, i) => { + console.log(mode, i); + assert.throws( + () => fs.access(readWriteFile, mode, common.mustNotCall()), + { + code: 'ERR_OUT_OF_RANGE', + } + ); + assert.throws( + () => fs.accessSync(readWriteFile, mode), + { + code: 'ERR_OUT_OF_RANGE', + } + ); +}); + +assert.throws( + () => { fs.accessSync(doesNotExist); }, + (err) => { + assert.strictEqual(err.code, 'ENOENT'); + assert.strictEqual(err.path, doesNotExist); + assert.strictEqual( + err.message, + `ENOENT: no such file or directory, access '${doesNotExist}'` + ); + assert.strictEqual(err.constructor, Error); + assert.strictEqual(err.syscall, 'access'); + assert.strictEqual(err.errno, UV_ENOENT); + return true; + } +); + +assert.throws( + () => { fs.accessSync(Buffer.from(doesNotExist)); }, + (err) => { + assert.strictEqual(err.code, 'ENOENT'); + assert.strictEqual(err.path, doesNotExist); + assert.strictEqual( + err.message, + `ENOENT: no such file or directory, access '${doesNotExist}'` + ); + assert.strictEqual(err.constructor, Error); + assert.strictEqual(err.syscall, 'access'); + assert.strictEqual(err.errno, UV_ENOENT); + return true; + } +); diff --git a/test/js/node/test/parallel/test-fs-append-file-sync.js b/test/js/node/test/parallel/test-fs-append-file-sync.js new file mode 100644 index 0000000000..a3969b1a4a --- /dev/null +++ b/test/js/node/test/parallel/test-fs-append-file-sync.js @@ -0,0 +1,103 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +const currentFileData = 'ABCD'; +const m = 0o600; +const num = 220; +const tmpdir = require('../common/tmpdir'); +const fixtures = require('../common/fixtures'); +const data = fixtures.utf8TestText; + +tmpdir.refresh(); + +// Test that empty file will be created and have content added. +const filename = tmpdir.resolve('append-sync.txt'); + +fs.appendFileSync(filename, data); + +const fileData = fs.readFileSync(filename); + +assert.strictEqual(Buffer.byteLength(data), fileData.length); + +// Test that appends data to a non empty file. +const filename2 = tmpdir.resolve('append-sync2.txt'); +fs.writeFileSync(filename2, currentFileData); + +fs.appendFileSync(filename2, data); + +const fileData2 = fs.readFileSync(filename2); + +assert.strictEqual(Buffer.byteLength(data) + currentFileData.length, + fileData2.length); + +// Test that appendFileSync accepts buffers. +const filename3 = tmpdir.resolve('append-sync3.txt'); +fs.writeFileSync(filename3, currentFileData); + +const buf = Buffer.from(data, 'utf8'); +fs.appendFileSync(filename3, buf); + +const fileData3 = fs.readFileSync(filename3); + +assert.strictEqual(buf.length + currentFileData.length, fileData3.length); + +const filename4 = tmpdir.resolve('append-sync4.txt'); +fs.writeFileSync(filename4, currentFileData, common.mustNotMutateObjectDeep({ mode: m })); + +[ + true, false, 0, 1, Infinity, () => {}, {}, [], undefined, null, +].forEach((value) => { + console.log(value); + assert.throws( + () => fs.appendFileSync(filename4, value, common.mustNotMutateObjectDeep({ mode: m })), + { message: /data/, code: 'ERR_INVALID_ARG_TYPE' } + ); +}); +fs.appendFileSync(filename4, `${num}`, common.mustNotMutateObjectDeep({ mode: m })); + +// Windows permissions aren't Unix. +if (!common.isWindows) { + const st = fs.statSync(filename4); + assert.strictEqual(st.mode & 0o700, m); +} + +const fileData4 = fs.readFileSync(filename4); + +assert.strictEqual(Buffer.byteLength(String(num)) + currentFileData.length, + fileData4.length); + +// Test that appendFile accepts file descriptors. +const filename5 = tmpdir.resolve('append-sync5.txt'); +fs.writeFileSync(filename5, currentFileData); + +const filename5fd = fs.openSync(filename5, 'a+', 0o600); +fs.appendFileSync(filename5fd, data); +fs.closeSync(filename5fd); + +const fileData5 = fs.readFileSync(filename5); + +assert.strictEqual(Buffer.byteLength(data) + currentFileData.length, + fileData5.length); diff --git a/test/js/node/test/parallel/test-fs-append-file.js b/test/js/node/test/parallel/test-fs-append-file.js new file mode 100644 index 0000000000..1e20625e5b --- /dev/null +++ b/test/js/node/test/parallel/test-fs-append-file.js @@ -0,0 +1,187 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +const tmpdir = require('../common/tmpdir'); + +const currentFileData = 'ABCD'; +const fixtures = require('../common/fixtures'); +const s = fixtures.utf8TestText; + +tmpdir.refresh(); + +const throwNextTick = (e) => { process.nextTick(() => { throw e; }); }; + +// Test that empty file will be created and have content added (callback API). +{ + const filename = tmpdir.resolve('append.txt'); + + fs.appendFile(filename, s, common.mustSucceed(() => { + fs.readFile(filename, common.mustSucceed((buffer) => { + assert.strictEqual(Buffer.byteLength(s), buffer.length); + })); + })); +} + +// Test that empty file will be created and have content added (promise API). +{ + const filename = tmpdir.resolve('append-promise.txt'); + + fs.promises.appendFile(filename, s) + .then(common.mustCall(() => fs.promises.readFile(filename))) + .then((buffer) => { + assert.strictEqual(Buffer.byteLength(s), buffer.length); + }) + .catch(throwNextTick); +} + +// Test that appends data to a non-empty file (callback API). +{ + const filename = tmpdir.resolve('append-non-empty.txt'); + fs.writeFileSync(filename, currentFileData); + + fs.appendFile(filename, s, common.mustSucceed(() => { + fs.readFile(filename, common.mustSucceed((buffer) => { + assert.strictEqual(Buffer.byteLength(s) + currentFileData.length, + buffer.length); + })); + })); +} + +// Test that appends data to a non-empty file (promise API). +{ + const filename = tmpdir.resolve('append-non-empty-promise.txt'); + fs.writeFileSync(filename, currentFileData); + + fs.promises.appendFile(filename, s) + .then(common.mustCall(() => fs.promises.readFile(filename))) + .then((buffer) => { + assert.strictEqual(Buffer.byteLength(s) + currentFileData.length, + buffer.length); + }) + .catch(throwNextTick); +} + +// Test that appendFile accepts buffers (callback API). +{ + const filename = tmpdir.resolve('append-buffer.txt'); + fs.writeFileSync(filename, currentFileData); + + const buf = Buffer.from(s, 'utf8'); + + fs.appendFile(filename, buf, common.mustSucceed(() => { + fs.readFile(filename, common.mustSucceed((buffer) => { + assert.strictEqual(buf.length + currentFileData.length, buffer.length); + })); + })); +} + +// Test that appendFile accepts buffers (promises API). +{ + const filename = tmpdir.resolve('append-buffer-promises.txt'); + fs.writeFileSync(filename, currentFileData); + + const buf = Buffer.from(s, 'utf8'); + + fs.promises.appendFile(filename, buf) + .then(common.mustCall(() => fs.promises.readFile(filename))) + .then((buffer) => { + assert.strictEqual(buf.length + currentFileData.length, buffer.length); + }) + .catch(throwNextTick); +} + +// Test that appendFile does not accept invalid data type (callback API). +[false, 5, {}, null, undefined].forEach(async (data) => { + const errObj = { + code: 'ERR_INVALID_ARG_TYPE', + message: /"data"|"buffer"/ + }; + const filename = tmpdir.resolve('append-invalid-data.txt'); + + assert.throws( + () => fs.appendFile(filename, data, common.mustNotCall()), + errObj + ); + + assert.throws( + () => fs.appendFileSync(filename, data), + errObj + ); + + await assert.rejects( + fs.promises.appendFile(filename, data), + errObj + ); + // The filename shouldn't exist if throwing error. + assert.throws( + () => fs.statSync(filename), + { + code: 'ENOENT', + message: /no such file or directory/ + } + ); +}); + +// Test that appendFile accepts file descriptors (callback API). +{ + const filename = tmpdir.resolve('append-descriptors.txt'); + fs.writeFileSync(filename, currentFileData); + + fs.open(filename, 'a+', common.mustSucceed((fd) => { + fs.appendFile(fd, s, common.mustSucceed(() => { + fs.close(fd, common.mustSucceed(() => { + fs.readFile(filename, common.mustSucceed((buffer) => { + assert.strictEqual(Buffer.byteLength(s) + currentFileData.length, + buffer.length); + })); + })); + })); + })); +} + +// Test that appendFile accepts file descriptors (promises API). +{ + const filename = tmpdir.resolve('append-descriptors-promises.txt'); + fs.writeFileSync(filename, currentFileData); + + let fd; + fs.promises.open(filename, 'a+') + .then(common.mustCall((fileDescriptor) => { + fd = fileDescriptor; + return fs.promises.appendFile(fd, s); + })) + .then(common.mustCall(() => fd.close())) + .then(common.mustCall(() => fs.promises.readFile(filename))) + .then(common.mustCall((buffer) => { + assert.strictEqual(Buffer.byteLength(s) + currentFileData.length, + buffer.length); + })) + .catch(throwNextTick); +} + +assert.throws( + () => fs.appendFile(tmpdir.resolve('append6.txt'), console.log), + { code: 'ERR_INVALID_ARG_TYPE' }); diff --git a/test/js/node/test/parallel/test-fs-assert-encoding-error.js b/test/js/node/test/parallel/test-fs-assert-encoding-error.js new file mode 100644 index 0000000000..9b22e042c5 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-assert-encoding-error.js @@ -0,0 +1,80 @@ +'use strict'; +const common = require('../common'); +const assert = require('node:assert'); +const fs = require('node:fs'); +const tmpdir = require('../common/tmpdir'); + +const testPath = tmpdir.resolve('assert-encoding-error'); +const options = 'test'; +const expectedError = { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}; + +assert.throws(() => { + fs.readFile(testPath, options, common.mustNotCall()); +}, expectedError); + +assert.throws(() => { + fs.readFileSync(testPath, options); +}, expectedError); + +assert.throws(() => { + fs.readdir(testPath, options, common.mustNotCall()); +}, expectedError); + +assert.throws(() => { + fs.readdirSync(testPath, options); +}, expectedError); + +assert.throws(() => { + fs.readlink(testPath, options, common.mustNotCall()); +}, expectedError); + +assert.throws(() => { + fs.readlinkSync(testPath, options); +}, expectedError); + +assert.throws(() => { + fs.writeFile(testPath, 'data', options, common.mustNotCall()); +}, expectedError); + +assert.throws(() => { + fs.writeFileSync(testPath, 'data', options); +}, expectedError); + +assert.throws(() => { + fs.appendFile(testPath, 'data', options, common.mustNotCall()); +}, expectedError); + +assert.throws(() => { + fs.appendFileSync(testPath, 'data', options); +}, expectedError); + +assert.throws(() => { + fs.watch(testPath, options, common.mustNotCall()); +}, expectedError); + +assert.throws(() => { + fs.realpath(testPath, options, common.mustNotCall()); +}, expectedError); + +assert.throws(() => { + fs.realpathSync(testPath, options); +}, expectedError); + +assert.throws(() => { + fs.mkdtemp(testPath, options, common.mustNotCall()); +}, expectedError); + +assert.throws(() => { + fs.mkdtempSync(testPath, options); +}, expectedError); + +assert.throws(() => { + fs.ReadStream(testPath, options); +}, expectedError); + +assert.throws(() => { + fs.WriteStream(testPath, options); +}, expectedError); diff --git a/test/js/node/test/parallel/test-fs-chmod-mask.js b/test/js/node/test/parallel/test-fs-chmod-mask.js new file mode 100644 index 0000000000..53f1931be4 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-chmod-mask.js @@ -0,0 +1,89 @@ +'use strict'; + +// This tests that the lower bits of mode > 0o777 still works in fs APIs. + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +let mode; +// On Windows chmod is only able to manipulate write permission +if (common.isWindows) { + mode = 0o444; // read-only +} else { + mode = 0o777; +} + +const maskToIgnore = 0o10000; + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +function test(mode, asString) { + const suffix = asString ? 'str' : 'num'; + const input = asString ? + (mode | maskToIgnore).toString(8) : (mode | maskToIgnore); + + { + const file = tmpdir.resolve(`chmod-async-${suffix}.txt`); + fs.writeFileSync(file, 'test', 'utf-8'); + + fs.chmod(file, input, common.mustSucceed(() => { + assert.strictEqual(fs.statSync(file).mode & 0o777, mode); + })); + } + + { + const file = tmpdir.resolve(`chmodSync-${suffix}.txt`); + fs.writeFileSync(file, 'test', 'utf-8'); + + fs.chmodSync(file, input); + assert.strictEqual(fs.statSync(file).mode & 0o777, mode); + } + + { + const file = tmpdir.resolve(`fchmod-async-${suffix}.txt`); + fs.writeFileSync(file, 'test', 'utf-8'); + fs.open(file, 'w', common.mustSucceed((fd) => { + fs.fchmod(fd, input, common.mustSucceed(() => { + assert.strictEqual(fs.fstatSync(fd).mode & 0o777, mode); + fs.close(fd, assert.ifError); + })); + })); + } + + { + const file = tmpdir.resolve(`fchmodSync-${suffix}.txt`); + fs.writeFileSync(file, 'test', 'utf-8'); + const fd = fs.openSync(file, 'w'); + + fs.fchmodSync(fd, input); + assert.strictEqual(fs.fstatSync(fd).mode & 0o777, mode); + + fs.close(fd, assert.ifError); + } + + if (fs.lchmod) { + const link = tmpdir.resolve(`lchmod-src-${suffix}`); + const file = tmpdir.resolve(`lchmod-dest-${suffix}`); + fs.writeFileSync(file, 'test', 'utf-8'); + fs.symlinkSync(file, link); + + fs.lchmod(link, input, common.mustSucceed(() => { + assert.strictEqual(fs.lstatSync(link).mode & 0o777, mode); + })); + } + + if (fs.lchmodSync) { + const link = tmpdir.resolve(`lchmodSync-src-${suffix}`); + const file = tmpdir.resolve(`lchmodSync-dest-${suffix}`); + fs.writeFileSync(file, 'test', 'utf-8'); + fs.symlinkSync(file, link); + + fs.lchmodSync(link, input); + assert.strictEqual(fs.lstatSync(link).mode & 0o777, mode); + } +} + +test(mode, true); +test(mode, false); diff --git a/test/js/node/test/parallel/test-fs-chmod.js b/test/js/node/test/parallel/test-fs-chmod.js new file mode 100644 index 0000000000..5c5821df46 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-chmod.js @@ -0,0 +1,152 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +let mode_async; +let mode_sync; + +// Need to hijack fs.open/close to make sure that things +// get closed once they're opened. +fs._open = fs.open; +fs._openSync = fs.openSync; +fs.open = open; +fs.openSync = openSync; +fs._close = fs.close; +fs._closeSync = fs.closeSync; +fs.close = close; +fs.closeSync = closeSync; + +let openCount = 0; + +function open() { + openCount++; + return fs._open.apply(fs, arguments); +} + +function openSync() { + openCount++; + return fs._openSync.apply(fs, arguments); +} + +function close() { + openCount--; + return fs._close.apply(fs, arguments); +} + +function closeSync() { + openCount--; + return fs._closeSync.apply(fs, arguments); +} + + +// On Windows chmod is only able to manipulate write permission +if (common.isWindows) { + mode_async = 0o400; // read-only + mode_sync = 0o600; // read-write +} else { + mode_async = 0o777; + mode_sync = 0o644; +} + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +const file1 = tmpdir.resolve('a.js'); +const file2 = tmpdir.resolve('a1.js'); + +// Create file1. +fs.closeSync(fs.openSync(file1, 'w')); + +fs.chmod(file1, mode_async.toString(8), common.mustSucceed(() => { + if (common.isWindows) { + assert.ok((fs.statSync(file1).mode & 0o777) & mode_async); + } else { + assert.strictEqual(fs.statSync(file1).mode & 0o777, mode_async); + } + + fs.chmodSync(file1, mode_sync); + if (common.isWindows) { + assert.ok((fs.statSync(file1).mode & 0o777) & mode_sync); + } else { + assert.strictEqual(fs.statSync(file1).mode & 0o777, mode_sync); + } +})); + +fs.open(file2, 'w', common.mustSucceed((fd) => { + fs.fchmod(fd, mode_async.toString(8), common.mustSucceed(() => { + if (common.isWindows) { + assert.ok((fs.fstatSync(fd).mode & 0o777) & mode_async); + } else { + assert.strictEqual(fs.fstatSync(fd).mode & 0o777, mode_async); + } + + assert.throws( + () => fs.fchmod(fd, {}), + { + code: 'ERR_INVALID_ARG_TYPE', + } + ); + + fs.fchmodSync(fd, mode_sync); + if (common.isWindows) { + assert.ok((fs.fstatSync(fd).mode & 0o777) & mode_sync); + } else { + assert.strictEqual(fs.fstatSync(fd).mode & 0o777, mode_sync); + } + + fs.close(fd, assert.ifError); + })); +})); + +// lchmod +if (fs.lchmod) { + const link = tmpdir.resolve('symbolic-link'); + + fs.symlinkSync(file2, link); + + fs.lchmod(link, mode_async, common.mustSucceed(() => { + assert.strictEqual(fs.lstatSync(link).mode & 0o777, mode_async); + + fs.lchmodSync(link, mode_sync); + assert.strictEqual(fs.lstatSync(link).mode & 0o777, mode_sync); + + })); +} + +[false, 1, {}, [], null, undefined].forEach((input) => { + const errObj = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + // message: 'The "path" argument must be of type string or an instance ' + + // 'of Buffer or URL.' + + // common.invalidArgTypeHelper(input) + }; + assert.throws(() => fs.chmod(input, 1, common.mustNotCall()), errObj); + assert.throws(() => fs.chmodSync(input, 1), errObj); +}); + +process.on('exit', function() { + assert.strictEqual(openCount, 0); +}); diff --git a/test/js/node/test/parallel/test-fs-close-errors.js b/test/js/node/test/parallel/test-fs-close-errors.js new file mode 100644 index 0000000000..0c48d39cd9 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-close-errors.js @@ -0,0 +1,35 @@ +'use strict'; + +// This tests that the errors thrown from fs.close and fs.closeSync +// include the desired properties + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +['', false, null, undefined, {}, []].forEach((input) => { + const errObj = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + // message: 'The "fd" argument must be of type number.' + + // common.invalidArgTypeHelper(input) + }; + assert.throws(() => fs.close(input), errObj); + assert.throws(() => fs.closeSync(input), errObj); +}); + +{ + // Test error when cb is not a function + const fd = fs.openSync(__filename, 'r'); + + const errObj = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + }; + + ['', false, null, {}, []].forEach((input) => { + assert.throws(() => fs.close(fd, input), errObj); + }); + + fs.closeSync(fd); +} diff --git a/test/js/node/test/parallel/test-fs-close.js b/test/js/node/test/parallel/test-fs-close.js new file mode 100644 index 0000000000..da0d0dfdc8 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-close.js @@ -0,0 +1,12 @@ +'use strict'; + +const common = require('../common'); + +const assert = require('assert'); +const fs = require('fs'); + +const fd = fs.openSync(__filename, 'r'); + +fs.close(fd, common.mustCall(function(...args) { + assert.deepStrictEqual(args, [null]); +})); diff --git a/test/js/node/test/parallel/test-fs-copyfile.js b/test/js/node/test/parallel/test-fs-copyfile.js new file mode 100644 index 0000000000..4541ef76b0 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-copyfile.js @@ -0,0 +1,164 @@ +'use strict'; +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const tmpdir = require('../common/tmpdir'); +const assert = require('assert'); +const fs = require('fs'); +const { + UV_ENOENT, + UV_EEXIST +} = process.binding('uv'); +const src = fixtures.path('a.js'); +const dest = tmpdir.resolve('copyfile.out'); +const { + COPYFILE_EXCL, + COPYFILE_FICLONE, + COPYFILE_FICLONE_FORCE, + UV_FS_COPYFILE_EXCL, + UV_FS_COPYFILE_FICLONE, + UV_FS_COPYFILE_FICLONE_FORCE +} = fs.constants; + +function verify(src, dest) { + const srcData = fs.readFileSync(src, 'utf8'); + const srcStat = fs.statSync(src); + const destData = fs.readFileSync(dest, 'utf8'); + const destStat = fs.statSync(dest); + + assert.strictEqual(srcData, destData); + assert.strictEqual(srcStat.mode, destStat.mode); + assert.strictEqual(srcStat.size, destStat.size); +} + +tmpdir.refresh(); + +// Verify that flags are defined. +assert.strictEqual(typeof COPYFILE_EXCL, 'number'); +assert.strictEqual(typeof COPYFILE_FICLONE, 'number'); +assert.strictEqual(typeof COPYFILE_FICLONE_FORCE, 'number'); +assert.strictEqual(typeof UV_FS_COPYFILE_EXCL, 'number'); +assert.strictEqual(typeof UV_FS_COPYFILE_FICLONE, 'number'); +assert.strictEqual(typeof UV_FS_COPYFILE_FICLONE_FORCE, 'number'); +assert.strictEqual(COPYFILE_EXCL, UV_FS_COPYFILE_EXCL); +assert.strictEqual(COPYFILE_FICLONE, UV_FS_COPYFILE_FICLONE); +assert.strictEqual(COPYFILE_FICLONE_FORCE, UV_FS_COPYFILE_FICLONE_FORCE); + +// Verify that files are overwritten when no flags are provided. +fs.writeFileSync(dest, '', 'utf8'); +const result = fs.copyFileSync(src, dest); +assert.strictEqual(result, undefined); +verify(src, dest); + +// Verify that files are overwritten with default flags. +fs.copyFileSync(src, dest, 0); +verify(src, dest); + +// Verify that UV_FS_COPYFILE_FICLONE can be used. +fs.unlinkSync(dest); +fs.copyFileSync(src, dest, UV_FS_COPYFILE_FICLONE); +verify(src, dest); + +// Verify that COPYFILE_FICLONE_FORCE can be used. +try { + fs.unlinkSync(dest); + fs.copyFileSync(src, dest, COPYFILE_FICLONE_FORCE); + verify(src, dest); +} catch (err) { + assert.strictEqual(err.syscall, 'copyfile'); + assert(err.code === 'ENOTSUP' || err.code === 'ENOTTY' || + err.code === 'ENOSYS' || err.code === 'EXDEV'); + assert.strictEqual(err.path, src); + assert.strictEqual(err.dest, dest); +} + +// Copies asynchronously. +tmpdir.refresh(); // Don't use unlinkSync() since the last test may fail. +fs.copyFile(src, dest, common.mustSucceed(() => { + verify(src, dest); + + // Copy asynchronously with flags. + fs.copyFile(src, dest, COPYFILE_EXCL, common.mustCall((err) => { + if (err.code === 'ENOENT') { // Could be ENOENT or EEXIST + assert.strictEqual(err.message, + 'ENOENT: no such file or directory, copyfile ' + + `'${src}' -> '${dest}'`); + assert.strictEqual(err.errno, UV_ENOENT); + assert.strictEqual(err.code, 'ENOENT'); + assert.strictEqual(err.syscall, 'copyfile'); + } else { + assert.strictEqual(err.message, + 'EEXIST: file already exists, copyfile ' + + `'${src}' -> '${dest}'`); + assert.strictEqual(err.errno, UV_EEXIST); + assert.strictEqual(err.code, 'EEXIST'); + assert.strictEqual(err.syscall, 'copyfile'); + } + })); +})); + +// Throws if callback is not a function. +assert.throws(() => { + fs.copyFile(src, dest, 0, 0); +}, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' +}); + +// Throws if the source path is not a string. +[false, 1, {}, [], null, undefined].forEach((i) => { + assert.throws( + () => fs.copyFile(i, dest, common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /src/ + } + ); + assert.throws( + () => fs.copyFile(src, i, common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /dest/ + } + ); + assert.throws( + () => fs.copyFileSync(i, dest), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /src/ + } + ); + assert.throws( + () => fs.copyFileSync(src, i), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /dest/ + } + ); +}); + +assert.throws(() => { + fs.copyFileSync(src, dest, 'r'); +}, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /mode/ +}); + +assert.throws(() => { + fs.copyFileSync(src, dest, 8); +}, { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', +}); + +assert.throws(() => { + fs.copyFile(src, dest, 'r', common.mustNotCall()); +}, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /mode/ +}); diff --git a/test/js/node/test/parallel/test-fs-existssync-false.js b/test/js/node/test/parallel/test-fs-existssync-false.js deleted file mode 100644 index 7b266c0253..0000000000 --- a/test/js/node/test/parallel/test-fs-existssync-false.js +++ /dev/null @@ -1,35 +0,0 @@ -'use strict'; -const common = require('../common'); -if (common.isWindows) return; // TODO: BUN -const tmpdir = require('../common/tmpdir'); - -// This test ensures that fs.existsSync doesn't incorrectly return false. -// (especially on Windows) -// https://github.com/nodejs/node-v0.x-archive/issues/3739 - -const assert = require('assert'); -const fs = require('fs'); -const path = require('path'); - -let dir = path.resolve(tmpdir.path); - -// Make sure that the tmp directory is clean -tmpdir.refresh(); - -// Make a long path. -for (let i = 0; i < 50; i++) { - dir = `${dir}/1234567890`; - try { - fs.mkdirSync(dir, '0777'); - } catch (e) { - if (e.code !== 'EEXIST') { - throw e; - } - } -} - -// Test if file exists synchronously -assert(fs.existsSync(dir), 'Directory is not accessible'); - -// Test if file exists asynchronously -fs.access(dir, common.mustSucceed()); diff --git a/test/js/node/test/parallel/test-fs-fchmod.js b/test/js/node/test/parallel/test-fs-fchmod.js new file mode 100644 index 0000000000..b986183fa5 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-fchmod.js @@ -0,0 +1,84 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +// This test ensures that input for fchmod is valid, testing for valid +// inputs for fd and mode + +// Check input type +[false, null, undefined, {}, [], ''].forEach((input) => { + const errObj = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + // message: 'The "fd" argument must be of type number.' + + // common.invalidArgTypeHelper(input) + }; + assert.throws(() => fs.fchmod(input, 0o666, () => {}), errObj); + assert.throws(() => fs.fchmodSync(input, 0o666), errObj); +}); + + +[false, null, {}, []].forEach((input) => { + const errObj = { + code: 'ERR_INVALID_ARG_TYPE', + }; + assert.throws(() => fs.fchmod(1, input), errObj); + assert.throws(() => fs.fchmodSync(1, input), errObj); +}); + +// EDIT: Bun checks the callback first, then the mode. Original check did not have callback set. +assert.throws(() => fs.fchmod(1, '123x', common.mustNotCall()), { + code: 'ERR_INVALID_ARG_VALUE' +}); + +[-1, 2 ** 32].forEach((input) => { + const errObj = { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "fd" is out of range. It must be >= 0 && <= ' + + `2147483647. Received ${input}` + }; + assert.throws(() => fs.fchmod(input, 0o666, () => {}), errObj); + assert.throws(() => fs.fchmodSync(input, 0o666), errObj); +}); + +[-1, 2 ** 32].forEach((input) => { + const errObj = { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "mode" is out of range. It must be >= 0 && <= ' + + `4294967295. Received ${input}` + }; + + assert.throws(() => fs.fchmod(1, input, () => {}), errObj); + assert.throws(() => fs.fchmodSync(1, input), errObj); +}); + +[NaN, Infinity].forEach((input) => { + const errObj = { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "fd" is out of range. It must be an integer. ' + + `Received ${input}` + }; + assert.throws(() => fs.fchmod(input, 0o666, () => {}), errObj); + assert.throws(() => fs.fchmodSync(input, 0o666), errObj); + errObj.message = errObj.message.replace('fd', 'mode'); + assert.throws(() => fs.fchmod(1, input, () => {}), errObj); + assert.throws(() => fs.fchmodSync(1, input), errObj); +}); + +[1.5].forEach((input) => { + const errObj = { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "fd" is out of range. It must be an integer. ' + + `Received ${input}` + }; + assert.throws(() => fs.fchmod(input, 0o666, () => {}), errObj); + assert.throws(() => fs.fchmodSync(input, 0o666), errObj); + errObj.message = errObj.message.replace('fd', 'mode'); + assert.throws(() => fs.fchmod(1, input, () => {}), errObj); + assert.throws(() => fs.fchmodSync(1, input), errObj); +}); diff --git a/test/js/node/test/parallel/test-fs-fchown.js b/test/js/node/test/parallel/test-fs-fchown.js new file mode 100644 index 0000000000..10e6a977ca --- /dev/null +++ b/test/js/node/test/parallel/test-fs-fchown.js @@ -0,0 +1,61 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const common = require('../common'); + +function testFd(input, errObj) { + assert.throws(() => fs.fchown(input, 0, 0, () => {}), errObj); + assert.throws(() => fs.fchownSync(input, 0, 0), errObj); +} + +function testUid(input, errObj) { + assert.throws(() => fs.fchown(1, input, 0, common.mustNotCall()), errObj); + assert.throws(() => fs.fchownSync(1, input), errObj); +} + +function testGid(input, errObj) { + assert.throws(() => fs.fchown(1, 1, input, common.mustNotCall()), errObj); + assert.throws(() => fs.fchownSync(1, 1, input), errObj); +} + +['', false, null, undefined, {}, []].forEach((input) => { + const errObj = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /fd|uid|gid/ + }; + testFd(input, errObj); + testUid(input, errObj); + testGid(input, errObj); +}); + +[Infinity, NaN].forEach((input) => { + const errObj = { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "fd" is out of range. It must be an integer. ' + + `Received ${input}` + }; + testFd(input, errObj); + errObj.message = errObj.message.replace('fd', 'uid'); + testUid(input, errObj); + errObj.message = errObj.message.replace('uid', 'gid'); + testGid(input, errObj); +}); + +[-2, 2 ** 32].forEach((input) => { + const errObj = { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "fd" is out of range. It must be ' + + `>= 0 && <= 2147483647. Received ${input}` + }; + testFd(input, errObj); + errObj.message = 'The value of "uid" is out of range. It must be >= -1 && ' + + `<= 4294967295. Received ${input}`; + testUid(input, errObj); + errObj.message = errObj.message.replace('uid', 'gid'); + testGid(input, errObj); +}); diff --git a/test/js/node/test/parallel/test-fs-filehandle-use-after-close.js b/test/js/node/test/parallel/test-fs-filehandle-use-after-close.js new file mode 100644 index 0000000000..18216b4f41 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-filehandle-use-after-close.js @@ -0,0 +1,25 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs').promises; + +(async () => { + const filehandle = await fs.open(__filename); + + assert.notStrictEqual(filehandle.fd, -1); + await filehandle.close(); + assert.strictEqual(filehandle.fd, -1); + + // Open another file handle first. This would typically receive the fd + // that `filehandle` previously used. In earlier versions of Node.js, the + // .stat() call would then succeed because it still used the original fd; + // See https://github.com/nodejs/node/issues/31361 for more details. + const otherFilehandle = await fs.open(process.execPath); + + await assert.rejects(() => filehandle.stat(), { + code: 'EBADF', + syscall: 'fstat' + }); + + await otherFilehandle.close(); +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-fmap.js b/test/js/node/test/parallel/test-fs-fmap.js deleted file mode 100644 index 5e56bd79ee..0000000000 --- a/test/js/node/test/parallel/test-fs-fmap.js +++ /dev/null @@ -1,29 +0,0 @@ -'use strict'; -const common = require('../common'); -if (common.isWindows) return; // TODO: BUN -const assert = require('assert'); -const fs = require('fs'); - -const { - O_CREAT = 0, - O_RDONLY = 0, - O_TRUNC = 0, - O_WRONLY = 0, - UV_FS_O_FILEMAP = 0 -} = fs.constants; - -const tmpdir = require('../common/tmpdir'); -tmpdir.refresh(); - -// Run this test on all platforms. While UV_FS_O_FILEMAP is only available on -// Windows, it should be silently ignored on other platforms. - -const filename = tmpdir.resolve('fmap.txt'); -const text = 'Memory File Mapping Test'; - -const mw = UV_FS_O_FILEMAP | O_TRUNC | O_CREAT | O_WRONLY; -const mr = UV_FS_O_FILEMAP | O_RDONLY; - -fs.writeFileSync(filename, text, { flag: mw }); -const r1 = fs.readFileSync(filename, { encoding: 'utf8', flag: mr }); -assert.strictEqual(r1, text); diff --git a/test/js/node/test/parallel/test-fs-lchmod.js b/test/js/node/test/parallel/test-fs-lchmod.js new file mode 100644 index 0000000000..d439710291 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-lchmod.js @@ -0,0 +1,66 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const { promises } = fs; +const f = __filename; + +// This test ensures that input for lchmod is valid, testing for valid +// inputs for path, mode and callback + +if (!common.isMacOS) { + common.skip('lchmod is only available on macOS'); +} + +// Check callback +assert.throws(() => fs.lchmod(f), { code: 'ERR_INVALID_ARG_TYPE' }); +assert.throws(() => fs.lchmod(), { code: 'ERR_INVALID_ARG_TYPE' }); +assert.throws(() => fs.lchmod(f, {}), { code: 'ERR_INVALID_ARG_TYPE' }); + +// Check path +[false, 1, {}, [], null, undefined].forEach((i) => { + assert.throws( + () => fs.lchmod(i, 0o777, common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); + assert.throws( + () => fs.lchmodSync(i), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); +}); + +// Check mode +[false, null, {}, []].forEach((input) => { + const errObj = { + code: 'ERR_INVALID_ARG_TYPE', + }; + + assert.rejects(promises.lchmod(f, input, () => {}), errObj).then(common.mustCall()); + assert.throws(() => fs.lchmodSync(f, input), errObj); +}); + +assert.throws(() => fs.lchmod(f, '123x', common.mustNotCall()), { + code: 'ERR_INVALID_ARG_VALUE' +}); +assert.throws(() => fs.lchmodSync(f, '123x'), { + code: 'ERR_INVALID_ARG_VALUE' +}); + +[-1, 2 ** 32].forEach((input) => { + const errObj = { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "mode" is out of range. It must be >= 0 && <= ' + + `4294967295. Received ${input}` + }; + + assert.rejects(promises.lchmod(f, input, () => {}), errObj).then(common.mustCall()); + assert.throws(() => fs.lchmodSync(f, input), errObj); +}); diff --git a/test/js/node/test/parallel/test-fs-lchown.js b/test/js/node/test/parallel/test-fs-lchown.js new file mode 100644 index 0000000000..d2a9718685 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-lchown.js @@ -0,0 +1,64 @@ +'use strict'; + +const common = require('../common'); +const tmpdir = require('../common/tmpdir'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const { promises } = fs; + +// Validate the path argument. +[false, 1, {}, [], null, undefined].forEach((i) => { + const err = { name: 'TypeError', code: 'ERR_INVALID_ARG_TYPE' }; + + assert.throws(() => fs.lchown(i, 1, 1, common.mustNotCall()), err); + assert.throws(() => fs.lchownSync(i, 1, 1), err); + promises.lchown(false, 1, 1) + .then(common.mustNotCall()) + .catch(common.expectsError(err)); +}); + +// Validate the uid and gid arguments. +[false, 'test', {}, [], null, undefined].forEach((i) => { + const err = { name: 'TypeError', code: 'ERR_INVALID_ARG_TYPE' }; + + assert.throws( + () => fs.lchown('not_a_file_that_exists', i, 1, common.mustNotCall()), + err + ); + assert.throws( + () => fs.lchown('not_a_file_that_exists', 1, i, common.mustNotCall()), + err + ); + assert.throws(() => fs.lchownSync('not_a_file_that_exists', i, 1), err); + assert.throws(() => fs.lchownSync('not_a_file_that_exists', 1, i), err); + + promises.lchown('not_a_file_that_exists', i, 1) + .then(common.mustNotCall()) + .catch(common.expectsError(err)); + + promises.lchown('not_a_file_that_exists', 1, i) + .then(common.mustNotCall()) + .catch(common.expectsError(err)); +}); + +// Validate the callback argument. +[false, 1, 'test', {}, [], null, undefined].forEach((i) => { + assert.throws(() => fs.lchown('not_a_file_that_exists', 1, 1, i), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE' + }); +}); + +if (!common.isWindows) { + const testFile = tmpdir.resolve(path.basename(__filename)); + const uid = process.geteuid(); + const gid = process.getegid(); + + tmpdir.refresh(); + fs.copyFileSync(__filename, testFile); + fs.lchownSync(testFile, uid, gid); + fs.lchown(testFile, uid, gid, common.mustSucceed(async (err) => { + await promises.lchown(testFile, uid, gid); + })); +} diff --git a/test/js/node/test/parallel/test-fs-mkdir-mode-mask.js b/test/js/node/test/parallel/test-fs-mkdir-mode-mask.js new file mode 100644 index 0000000000..cca28ca5ff --- /dev/null +++ b/test/js/node/test/parallel/test-fs-mkdir-mode-mask.js @@ -0,0 +1,40 @@ +'use strict'; + +// This tests that the lower bits of mode > 0o777 still works in fs.mkdir(). + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +if (common.isWindows) { + common.skip('mode is not supported in mkdir on Windows'); + return; +} + +const mode = 0o644; +const maskToIgnore = 0o10000; + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +function test(mode, asString) { + const suffix = asString ? 'str' : 'num'; + const input = asString ? + (mode | maskToIgnore).toString(8) : (mode | maskToIgnore); + + { + const dir = tmpdir.resolve(`mkdirSync-${suffix}`); + fs.mkdirSync(dir, input); + assert.strictEqual(fs.statSync(dir).mode & 0o777, mode); + } + + { + const dir = tmpdir.resolve(`mkdir-${suffix}`); + fs.mkdir(dir, input, common.mustSucceed(() => { + assert.strictEqual(fs.statSync(dir).mode & 0o777, mode); + })); + } +} + +test(mode, true); +test(mode, false); diff --git a/test/js/node/test/parallel/test-fs-mkdir-rmdir.js b/test/js/node/test/parallel/test-fs-mkdir-rmdir.js new file mode 100644 index 0000000000..7fa3473f1c --- /dev/null +++ b/test/js/node/test/parallel/test-fs-mkdir-rmdir.js @@ -0,0 +1,37 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const tmpdir = require('../common/tmpdir'); +const d = tmpdir.resolve('dir'); + +tmpdir.refresh(); + +// Make sure the directory does not exist +assert(!fs.existsSync(d)); +// Create the directory now +fs.mkdirSync(d); +// Make sure the directory exists +assert(fs.existsSync(d)); +// Try creating again, it should fail with EEXIST +assert.throws(function() { + fs.mkdirSync(d); +}, /EEXIST: file already exists, mkdir/); +// Remove the directory now +fs.rmdirSync(d); +// Make sure the directory does not exist +assert(!fs.existsSync(d)); + +// Similarly test the Async version +fs.mkdir(d, 0o666, common.mustSucceed(() => { + fs.mkdir(d, 0o666, common.mustCall(function(err) { + assert.strictEqual(this, undefined); + assert.ok(err, 'got no error'); + assert.match(err.message, /^EEXIST/); + assert.strictEqual(err.code, 'EEXIST'); + assert.strictEqual(err.path, d); + + fs.rmdir(d, assert.ifError); + })); +})); diff --git a/test/js/node/test/parallel/test-fs-null-bytes.js b/test/js/node/test/parallel/test-fs-null-bytes.js new file mode 100644 index 0000000000..302d37196f --- /dev/null +++ b/test/js/node/test/parallel/test-fs-null-bytes.js @@ -0,0 +1,158 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +function check(async, sync) { + const argsSync = Array.prototype.slice.call(arguments, 2); + const argsAsync = argsSync.concat(common.mustNotCall()); + + if (sync) { + assert.throws( + () => { + sync.apply(null, argsSync); + }, + { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', + }); + } + + if (async) { + assert.throws( + () => { + async.apply(null, argsAsync); + }, + { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError' + }); + } +} + +check(fs.access, fs.accessSync, 'foo\u0000bar'); +check(fs.access, fs.accessSync, 'foo\u0000bar', fs.constants.F_OK); +check(fs.appendFile, fs.appendFileSync, 'foo\u0000bar', 'abc'); +check(fs.chmod, fs.chmodSync, 'foo\u0000bar', '0644'); +check(fs.chown, fs.chownSync, 'foo\u0000bar', 12, 34); +check(fs.copyFile, fs.copyFileSync, 'foo\u0000bar', 'abc'); +check(fs.copyFile, fs.copyFileSync, 'abc', 'foo\u0000bar'); +check(fs.lchown, fs.lchownSync, 'foo\u0000bar', 12, 34); +check(fs.link, fs.linkSync, 'foo\u0000bar', 'foobar'); +check(fs.link, fs.linkSync, 'foobar', 'foo\u0000bar'); +check(fs.lstat, fs.lstatSync, 'foo\u0000bar'); +check(fs.mkdir, fs.mkdirSync, 'foo\u0000bar', '0755'); +check(fs.open, fs.openSync, 'foo\u0000bar', 'r'); +check(fs.readFile, fs.readFileSync, 'foo\u0000bar'); +check(fs.readdir, fs.readdirSync, 'foo\u0000bar'); +check(fs.readdir, fs.readdirSync, 'foo\u0000bar', { recursive: true }); +check(fs.readlink, fs.readlinkSync, 'foo\u0000bar'); +check(fs.realpath, fs.realpathSync, 'foo\u0000bar'); +check(fs.rename, fs.renameSync, 'foo\u0000bar', 'foobar'); +check(fs.rename, fs.renameSync, 'foobar', 'foo\u0000bar'); +check(fs.rmdir, fs.rmdirSync, 'foo\u0000bar'); +check(fs.stat, fs.statSync, 'foo\u0000bar'); +check(fs.symlink, fs.symlinkSync, 'foo\u0000bar', 'foobar'); +check(fs.symlink, fs.symlinkSync, 'foobar', 'foo\u0000bar'); +check(fs.truncate, fs.truncateSync, 'foo\u0000bar'); +check(fs.unlink, fs.unlinkSync, 'foo\u0000bar'); +check(null, fs.unwatchFile, 'foo\u0000bar', common.mustNotCall()); +check(fs.utimes, fs.utimesSync, 'foo\u0000bar', 0, 0); +check(null, fs.watch, 'foo\u0000bar', common.mustNotCall()); +check(null, fs.watchFile, 'foo\u0000bar', common.mustNotCall()); +check(fs.writeFile, fs.writeFileSync, 'foo\u0000bar', 'abc'); + +const fileUrl = new URL('file:///C:/foo\u0000bar'); +const fileUrl2 = new URL('file:///C:/foo%00bar'); + +check(fs.access, fs.accessSync, fileUrl); +check(fs.access, fs.accessSync, fileUrl, fs.constants.F_OK); +check(fs.appendFile, fs.appendFileSync, fileUrl, 'abc'); +check(fs.chmod, fs.chmodSync, fileUrl, '0644'); +check(fs.chown, fs.chownSync, fileUrl, 12, 34); +check(fs.copyFile, fs.copyFileSync, fileUrl, 'abc'); +check(fs.copyFile, fs.copyFileSync, 'abc', fileUrl); +check(fs.lchown, fs.lchownSync, fileUrl, 12, 34); +check(fs.link, fs.linkSync, fileUrl, 'foobar'); +check(fs.link, fs.linkSync, 'foobar', fileUrl); +check(fs.lstat, fs.lstatSync, fileUrl); +check(fs.mkdir, fs.mkdirSync, fileUrl, '0755'); +check(fs.open, fs.openSync, fileUrl, 'r'); +check(fs.readFile, fs.readFileSync, fileUrl); +check(fs.readdir, fs.readdirSync, fileUrl); +check(fs.readdir, fs.readdirSync, fileUrl, { recursive: true }); +check(fs.readlink, fs.readlinkSync, fileUrl); +check(fs.realpath, fs.realpathSync, fileUrl); +check(fs.rename, fs.renameSync, fileUrl, 'foobar'); +check(fs.rename, fs.renameSync, 'foobar', fileUrl); +check(fs.rmdir, fs.rmdirSync, fileUrl); +check(fs.stat, fs.statSync, fileUrl); +check(fs.symlink, fs.symlinkSync, fileUrl, 'foobar'); +check(fs.symlink, fs.symlinkSync, 'foobar', fileUrl); +check(fs.truncate, fs.truncateSync, fileUrl); +check(fs.unlink, fs.unlinkSync, fileUrl); +check(null, fs.unwatchFile, fileUrl, assert.fail); +check(fs.utimes, fs.utimesSync, fileUrl, 0, 0); +check(null, fs.watch, fileUrl, assert.fail); +check(null, fs.watchFile, fileUrl, assert.fail); +check(fs.writeFile, fs.writeFileSync, fileUrl, 'abc'); + +check(fs.access, fs.accessSync, fileUrl2); +check(fs.access, fs.accessSync, fileUrl2, fs.constants.F_OK); +check(fs.appendFile, fs.appendFileSync, fileUrl2, 'abc'); +check(fs.chmod, fs.chmodSync, fileUrl2, '0644'); +check(fs.chown, fs.chownSync, fileUrl2, 12, 34); +check(fs.copyFile, fs.copyFileSync, fileUrl2, 'abc'); +check(fs.copyFile, fs.copyFileSync, 'abc', fileUrl2); +check(fs.lchown, fs.lchownSync, fileUrl2, 12, 34); +check(fs.link, fs.linkSync, fileUrl2, 'foobar'); +check(fs.link, fs.linkSync, 'foobar', fileUrl2); +check(fs.lstat, fs.lstatSync, fileUrl2); +check(fs.mkdir, fs.mkdirSync, fileUrl2, '0755'); +check(fs.open, fs.openSync, fileUrl2, 'r'); +check(fs.readFile, fs.readFileSync, fileUrl2); +check(fs.readdir, fs.readdirSync, fileUrl2); +check(fs.readdir, fs.readdirSync, fileUrl2, { recursive: true }); +check(fs.readlink, fs.readlinkSync, fileUrl2); +check(fs.realpath, fs.realpathSync, fileUrl2); +check(fs.rename, fs.renameSync, fileUrl2, 'foobar'); +check(fs.rename, fs.renameSync, 'foobar', fileUrl2); +check(fs.rmdir, fs.rmdirSync, fileUrl2); +check(fs.stat, fs.statSync, fileUrl2); +check(fs.symlink, fs.symlinkSync, fileUrl2, 'foobar'); +check(fs.symlink, fs.symlinkSync, 'foobar', fileUrl2); +check(fs.truncate, fs.truncateSync, fileUrl2); +check(fs.unlink, fs.unlinkSync, fileUrl2); +check(null, fs.unwatchFile, fileUrl2, assert.fail); +check(fs.utimes, fs.utimesSync, fileUrl2, 0, 0); +check(null, fs.watch, fileUrl2, assert.fail); +check(null, fs.watchFile, fileUrl2, assert.fail); +check(fs.writeFile, fs.writeFileSync, fileUrl2, 'abc'); + +// An 'error' for exists means that it doesn't exist. +// One of many reasons why this file is the absolute worst. +fs.exists('foo\u0000bar', common.mustCall((exists) => { + assert(!exists); +})); +assert(!fs.existsSync('foo\u0000bar')); diff --git a/test/js/node/test/parallel/test-fs-promises-file-handle-stream.js b/test/js/node/test/parallel/test-fs-promises-file-handle-stream.js new file mode 100644 index 0000000000..71f312b6f9 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-promises-file-handle-stream.js @@ -0,0 +1,48 @@ +'use strict'; + +const common = require('../common'); + +// The following tests validate base functionality for the fs.promises +// FileHandle.write method. + +const fs = require('fs'); +const { open } = fs.promises; +const path = require('path'); +const tmpdir = require('../common/tmpdir'); +const assert = require('assert'); +const { finished } = require('stream/promises'); +const { buffer } = require('stream/consumers'); +const tmpDir = tmpdir.path; + +tmpdir.refresh(); + +async function validateWrite() { + const filePathForHandle = path.resolve(tmpDir, 'tmp-write.txt'); + const fileHandle = await open(filePathForHandle, 'w'); + const buffer = Buffer.from('Hello world'.repeat(100), 'utf8'); + + const stream = fileHandle.createWriteStream(); + stream.end(buffer); + await finished(stream); + + const readFileData = fs.readFileSync(filePathForHandle); + assert.deepStrictEqual(buffer, readFileData); +} + +async function validateRead() { + const filePathForHandle = path.resolve(tmpDir, 'tmp-read.txt'); + const buf = Buffer.from('Hello world'.repeat(100), 'utf8'); + + fs.writeFileSync(filePathForHandle, buf); + + const fileHandle = await open(filePathForHandle); + assert.deepStrictEqual( + await buffer(fileHandle.createReadStream()), + buf + ); +} + +Promise.all([ + validateWrite(), + validateRead(), +]).then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-promises-file-handle-sync.js b/test/js/node/test/parallel/test-fs-promises-file-handle-sync.js new file mode 100644 index 0000000000..ac2f18e9bb --- /dev/null +++ b/test/js/node/test/parallel/test-fs-promises-file-handle-sync.js @@ -0,0 +1,35 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const fixtures = require('../common/fixtures'); +const tmpdir = require('../common/tmpdir'); + +const { access, copyFile, open } = require('fs').promises; + +async function validate() { + tmpdir.refresh(); + const dest = tmpdir.resolve('baz.js'); + await assert.rejects( + copyFile(fixtures.path('baz.js'), dest, 'r'), + { + code: 'ERR_INVALID_ARG_TYPE', + } + ); + await copyFile(fixtures.path('baz.js'), dest); + await assert.rejects( + access(dest, 'r'), + { code: 'ERR_INVALID_ARG_TYPE', message: /mode/ } + ); + await access(dest); + const handle = await open(dest, 'r+'); + await handle.datasync(); + await handle.sync(); + const buf = Buffer.from('hello world'); + await handle.write(buf); + const ret = await handle.read(Buffer.alloc(11), 0, 11, 0); + assert.strictEqual(ret.bytesRead, 11); + assert.deepStrictEqual(ret.buffer, buf); + await handle.close(); +} + +validate(); diff --git a/test/js/node/test/parallel/test-fs-promises-watch.js b/test/js/node/test/parallel/test-fs-promises-watch.js new file mode 100644 index 0000000000..692ed33dbc --- /dev/null +++ b/test/js/node/test/parallel/test-fs-promises-watch.js @@ -0,0 +1,136 @@ +'use strict'; +const common = require('../common'); + +if (common.isIBMi) + common.skip('IBMi does not support `fs.watch()`'); + +const { watch } = require('fs/promises'); +const fs = require('fs'); +const assert = require('assert'); +const { join } = require('path'); +const { setTimeout } = require('timers/promises'); +const tmpdir = require('../common/tmpdir'); + +class WatchTestCase { + constructor(shouldInclude, dirName, fileName, field) { + this.dirName = dirName; + this.fileName = fileName; + this.field = field; + this.shouldSkip = !shouldInclude; + } + get dirPath() { return tmpdir.resolve(this.dirName); } + get filePath() { return join(this.dirPath, this.fileName); } +} + +const kCases = [ + // Watch on a directory should callback with a filename on supported systems + new WatchTestCase( + common.isLinux || common.isMacOS || common.isWindows || common.isAIX, + 'watch1', + 'foo', + 'filePath' + ), + // Watch on a file should callback with a filename on supported systems + new WatchTestCase( + common.isLinux || common.isMacOS || common.isWindows, + 'watch2', + 'bar', + 'dirPath' + ), +]; + +tmpdir.refresh(); + +for (const testCase of kCases) { + if (testCase.shouldSkip) continue; + fs.mkdirSync(testCase.dirPath); + // Long content so it's actually flushed. + const content1 = Date.now() + testCase.fileName.toLowerCase().repeat(1e4); + fs.writeFileSync(testCase.filePath, content1); + + let interval; + async function test() { + if (common.isMacOS) { + // On macOS delay watcher start to avoid leaking previous events. + // Refs: https://github.com/libuv/libuv/pull/4503 + await setTimeout(common.platformTimeout(100)); + } + + const watcher = watch(testCase[testCase.field]); + for await (const { eventType, filename } of watcher) { + clearInterval(interval); + assert.strictEqual(['rename', 'change'].includes(eventType), true); + assert.strictEqual(filename, testCase.fileName); + break; + } + + // Waiting on it again is a non-op + // eslint-disable-next-line no-unused-vars + for await (const p of watcher) { + assert.fail('should not run'); + } + } + + // Long content so it's actually flushed. toUpperCase so there's real change. + const content2 = Date.now() + testCase.fileName.toUpperCase().repeat(1e4); + interval = setInterval(() => { + fs.writeFileSync(testCase.filePath, ''); + fs.writeFileSync(testCase.filePath, content2); + }, 100); + + test().then(common.mustCall()); +} + +assert.rejects( + async () => { + // eslint-disable-next-line no-unused-vars, no-empty + for await (const _ of watch(1)) { } + }, + { code: 'ERR_INVALID_ARG_TYPE' }).then(common.mustCall()); + +assert.rejects( + async () => { + // eslint-disable-next-line no-unused-vars, no-empty + for await (const _ of watch(__filename, 1)) { } + }, + { code: 'ERR_INVALID_ARG_TYPE' }).then(common.mustCall()); + +assert.rejects( + async () => { + // eslint-disable-next-line no-unused-vars, no-empty + for await (const _ of watch('', { persistent: 1 })) { } + }, + { code: 'ERR_INVALID_ARG_TYPE' }).then(common.mustCall()); + +assert.rejects( + async () => { + // eslint-disable-next-line no-unused-vars, no-empty + for await (const _ of watch('', { recursive: 1 })) { } + }, + { code: 'ERR_INVALID_ARG_TYPE' }).then(common.mustCall()); + +assert.rejects( + async () => { + // eslint-disable-next-line no-unused-vars, no-empty + for await (const _ of watch('', { encoding: 1 })) { } + }, + { code: 'ERR_INVALID_ARG_VALUE' }).then(common.mustCall()); + +assert.rejects( + async () => { + // eslint-disable-next-line no-unused-vars, no-empty + for await (const _ of watch('', { signal: 1 })) { } + }, + { code: 'ERR_INVALID_ARG_TYPE' }).then(common.mustCall()); + +(async () => { + const ac = new AbortController(); + const { signal } = ac; + setImmediate(() => ac.abort()); + try { + // eslint-disable-next-line no-unused-vars, no-empty + for await (const _ of watch(__filename, { signal })) { } + } catch (err) { + assert.strictEqual(err.name, 'AbortError'); + } +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-read-empty-buffer.js b/test/js/node/test/parallel/test-fs-read-empty-buffer.js new file mode 100644 index 0000000000..6abfcb5aae --- /dev/null +++ b/test/js/node/test/parallel/test-fs-read-empty-buffer.js @@ -0,0 +1,41 @@ +'use strict'; +require('../common'); +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const fs = require('fs'); +const filepath = fixtures.path('x.txt'); +const fd = fs.openSync(filepath, 'r'); +const fsPromises = fs.promises; + +const buffer = new Uint8Array(); + +assert.throws( + () => fs.readSync(fd, buffer, 0, 10, 0), + { + code: 'ERR_INVALID_ARG_VALUE', + message: 'The argument \'buffer\' is empty and cannot be written. ' + + 'Received Uint8Array(0) []' + } +); + +assert.throws( + () => fs.read(fd, buffer, 0, 1, 0, common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_VALUE', + message: 'The argument \'buffer\' is empty and cannot be written. ' + + 'Received Uint8Array(0) []' + } +); + +(async () => { + const filehandle = await fsPromises.open(filepath, 'r'); + assert.rejects( + () => filehandle.read(buffer, 0, 1, 0), + { + code: 'ERR_INVALID_ARG_VALUE', + message: 'The argument \'buffer\' is empty and cannot be written. ' + + 'Received Uint8Array(0) []' + } + ).then(common.mustCall()); +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-readdir-pipe.js b/test/js/node/test/parallel/test-fs-readdir-pipe.js new file mode 100644 index 0000000000..592e7a3d54 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-readdir-pipe.js @@ -0,0 +1,21 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { readdir, readdirSync } = require('fs'); + +if (!common.isWindows) { + common.skip('This test is specific to Windows to test enumerate pipes'); +} + +// Ref: https://github.com/nodejs/node/issues/56002 +// This test is specific to Windows. + +const pipe = '\\\\.\\pipe\\'; + +const { length } = readdirSync(pipe); +assert.ok(length >= 0, `${length} is not greater or equal to 0`); + +readdir(pipe, common.mustSucceed((files) => { + assert.ok(files.length >= 0, `${files.length} is not greater or equal to 0`); +})); diff --git a/test/js/node/test/parallel/test-fs-readfile-eof.js b/test/js/node/test/parallel/test-fs-readfile-eof.js index 94354b915b..d7f9e21c5b 100644 --- a/test/js/node/test/parallel/test-fs-readfile-eof.js +++ b/test/js/node/test/parallel/test-fs-readfile-eof.js @@ -25,21 +25,19 @@ const data2 = 'World'; const expected = `${data1}\n${data2}\n`; const exec = require('child_process').exec; -const f = JSON.stringify(__filename); -const node = JSON.stringify(process.execPath); function test(child) { - const cmd = `(echo ${data1}; sleep 0.5; echo ${data2}) | ${node} ${f} ${child}`; - exec(cmd, common.mustSucceed((stdout, stderr) => { - assert.strictEqual( - stdout, - expected, - `expected to read(${child === childType[0] ? 'with' : 'without'} encoding): '${expected}' but got: '${stdout}'`); - assert.strictEqual( - stderr, - '', - `expected not to read anything from stderr but got: '${stderr}'`); - })); + exec(...common.escapePOSIXShell`(echo "${data1}"; sleep 0.5; echo "${data2}") | "${process.execPath}" "${__filename}" "${child}"`, + common.mustSucceed((stdout, stderr) => { + assert.strictEqual( + stdout, + expected, + `expected to read(${child === childType[0] ? 'with' : 'without'} encoding): '${expected}' but got: '${stdout}'`); + assert.strictEqual( + stderr, + '', + `expected not to read anything from stderr but got: '${stderr}'`); + })); } test(childType[0]); diff --git a/test/js/node/test/parallel/test-fs-readfile-pipe-large.js b/test/js/node/test/parallel/test-fs-readfile-pipe-large.js index 4376774bb4..fa5fea3ca3 100644 --- a/test/js/node/test/parallel/test-fs-readfile-pipe-large.js +++ b/test/js/node/test/parallel/test-fs-readfile-pipe-large.js @@ -25,10 +25,8 @@ tmpdir.refresh(); fs.writeFileSync(filename, dataExpected); const exec = require('child_process').exec; -const f = JSON.stringify(__filename); -const node = JSON.stringify(process.execPath); -const cmd = `cat ${filename} | ${node} ${f} child`; -exec(cmd, { maxBuffer: 1000000 }, common.mustSucceed((stdout, stderr) => { +const [cmd, opts] = common.escapePOSIXShell`"${process.execPath}" "${__filename}" child < "${filename}"`; +exec(cmd, { ...opts, maxBuffer: 1000000 }, common.mustSucceed((stdout, stderr) => { assert.strictEqual( stdout, dataExpected, diff --git a/test/js/node/test/parallel/test-fs-readfile-pipe.js b/test/js/node/test/parallel/test-fs-readfile-pipe.js index 79d5699fef..782265e8ce 100644 --- a/test/js/node/test/parallel/test-fs-readfile-pipe.js +++ b/test/js/node/test/parallel/test-fs-readfile-pipe.js @@ -43,10 +43,7 @@ const filename = fixtures.path('readfile_pipe_test.txt'); const dataExpected = fs.readFileSync(filename).toString(); const exec = require('child_process').exec; -const f = JSON.stringify(__filename); -const node = JSON.stringify(process.execPath); -const cmd = `cat ${filename} | ${node} ${f} child`; -exec(cmd, common.mustSucceed((stdout, stderr) => { +exec(...common.escapePOSIXShell`"${process.execPath}" "${__filename}" child < "${filename}"`, common.mustSucceed((stdout, stderr) => { assert.strictEqual( stdout, dataExpected, diff --git a/test/js/node/test/parallel/test-fs-readfilesync-enoent.js b/test/js/node/test/parallel/test-fs-readfilesync-enoent.js deleted file mode 100644 index 1d9ad2532f..0000000000 --- a/test/js/node/test/parallel/test-fs-readfilesync-enoent.js +++ /dev/null @@ -1,33 +0,0 @@ -'use strict'; -const common = require('../common'); - -// This test is only relevant on Windows. -if (!common.isWindows) - common.skip('Windows specific test.'); -if (common.isWindows) return; // TODO: BUN - -// This test ensures fs.realpathSync works on properly on Windows without -// throwing ENOENT when the path involves a fileserver. -// https://github.com/nodejs/node-v0.x-archive/issues/3542 - -const assert = require('assert'); -const fs = require('fs'); -const os = require('os'); -const path = require('path'); - -function test(p) { - const result = fs.realpathSync(p); - assert.strictEqual(result.toLowerCase(), path.resolve(p).toLowerCase()); - - fs.realpath(p, common.mustSucceed((result) => { - assert.strictEqual(result.toLowerCase(), path.resolve(p).toLowerCase()); - })); -} - -test(`//${os.hostname()}/c$/Windows/System32`); -test(`//${os.hostname()}/c$/Windows`); -test(`//${os.hostname()}/c$/`); -test(`\\\\${os.hostname()}\\c$\\`); -test('C:\\'); -test('C:'); -test(process.env.windir); diff --git a/test/js/node/test/parallel/test-fs-readfilesync-pipe-large.js b/test/js/node/test/parallel/test-fs-readfilesync-pipe-large.js index 5450337c4f..60c7dccd1a 100644 --- a/test/js/node/test/parallel/test-fs-readfilesync-pipe-large.js +++ b/test/js/node/test/parallel/test-fs-readfilesync-pipe-large.js @@ -22,12 +22,10 @@ tmpdir.refresh(); fs.writeFileSync(filename, dataExpected); const exec = require('child_process').exec; -const f = JSON.stringify(__filename); -const node = JSON.stringify(process.execPath); -const cmd = `cat ${filename} | ${node} ${f} child`; +const [cmd, opts] = common.escapePOSIXShell`"${process.execPath}" "${__filename}" child < "${filename}"`; exec( cmd, - { maxBuffer: 1000000 }, + { ...opts, maxBuffer: 1_000_000 }, common.mustSucceed((stdout, stderr) => { assert.strictEqual(stdout, dataExpected); assert.strictEqual(stderr, ''); diff --git a/test/js/node/test/parallel/test-fs-readv-promisify.js b/test/js/node/test/parallel/test-fs-readv-promisify.js new file mode 100644 index 0000000000..2af418bcc2 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-readv-promisify.js @@ -0,0 +1,18 @@ +'use strict'; + +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const fs = require('fs'); +const readv = require('util').promisify(fs.readv); +const assert = require('assert'); +const filepath = fixtures.path('x.txt'); +const fd = fs.openSync(filepath, 'r'); + +const expected = [Buffer.from('xyz\n')]; + +readv(fd, expected) + .then(function({ bytesRead, buffers }) { + assert.deepStrictEqual(bytesRead, expected[0].length); + assert.deepStrictEqual(buffers, expected); + }) + .then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-realpath-native.js b/test/js/node/test/parallel/test-fs-realpath-native.js new file mode 100644 index 0000000000..0b51e6cc89 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-realpath-native.js @@ -0,0 +1,21 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +const filename = __filename.toLowerCase(); + +// Bun: fix current working directory +process.chdir(require('path').join(__dirname, '..', '..')); + +assert.strictEqual( + fs.realpathSync.native('./test/parallel/test-fs-realpath-native.js') + .toLowerCase(), + filename); + +fs.realpath.native( + './test/parallel/test-fs-realpath-native.js', + common.mustSucceed(function(res) { + assert.strictEqual(res.toLowerCase(), filename); + assert.strictEqual(this, undefined); + })); diff --git a/test/js/node/test/parallel/test-fs-realpath-on-substed-drive.js b/test/js/node/test/parallel/test-fs-realpath-on-substed-drive.js deleted file mode 100644 index 51bc18e18d..0000000000 --- a/test/js/node/test/parallel/test-fs-realpath-on-substed-drive.js +++ /dev/null @@ -1,52 +0,0 @@ -'use strict'; - -const common = require('../common'); -if (!common.isWindows) - common.skip('Test for Windows only'); -if (common.isWindows) return; // TODO: BUN - -const fixtures = require('../common/fixtures'); - -const assert = require('assert'); -const fs = require('fs'); -const spawnSync = require('child_process').spawnSync; - -let result; - -// Create a subst drive -const driveLetters = 'ABCDEFGHIJKLMNOPQRSTUWXYZ'; -let drive; -let i; -for (i = 0; i < driveLetters.length; ++i) { - drive = `${driveLetters[i]}:`; - result = spawnSync('subst', [drive, fixtures.fixturesDir]); - if (result.status === 0) - break; -} -if (i === driveLetters.length) - common.skip('Cannot create subst drive'); - -// Schedule cleanup (and check if all callbacks where called) -process.on('exit', function() { - spawnSync('subst', ['/d', drive]); -}); - -// test: -const filename = `${drive}\\empty.js`; -const filenameBuffer = Buffer.from(filename); - -result = fs.realpathSync(filename); -assert.strictEqual(result, filename); - -result = fs.realpathSync(filename, 'buffer'); -assert(Buffer.isBuffer(result)); -assert(result.equals(filenameBuffer)); - -fs.realpath(filename, common.mustSucceed((result) => { - assert.strictEqual(result, filename); -})); - -fs.realpath(filename, 'buffer', common.mustSucceed((result) => { - assert(Buffer.isBuffer(result)); - assert(result.equals(filenameBuffer)); -})); diff --git a/test/js/node/test/parallel/test-fs-realpath.js b/test/js/node/test/parallel/test-fs-realpath.js new file mode 100644 index 0000000000..d944195de3 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-realpath.js @@ -0,0 +1,618 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const tmpdir = require('../common/tmpdir'); + +if (!common.isMainThread) + common.skip('process.chdir is not available in Workers'); + +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +let async_completed = 0; +let async_expected = 0; +const unlink = []; +const skipSymlinks = !common.canCreateSymLink(); +const tmpDir = tmpdir.path; + +tmpdir.refresh(); + +let root = '/'; +let assertEqualPath = assert.strictEqual; +if (common.isWindows) { + // Something like "C:\\" + root = process.cwd().slice(0, 3); + assertEqualPath = function(path_left, path_right, message) { + assert + .strictEqual(path_left.toLowerCase(), path_right.toLowerCase(), message); + }; +} + +process.nextTick(runTest); + +function tmp(p) { + return path.join(tmpDir, p); +} + +const targetsAbsDir = path.join(tmpDir, 'targets'); +const tmpAbsDir = tmpDir; + +// Set up targetsAbsDir and expected subdirectories +fs.mkdirSync(targetsAbsDir); +fs.mkdirSync(path.join(targetsAbsDir, 'nested-index')); +fs.mkdirSync(path.join(targetsAbsDir, 'nested-index', 'one')); +fs.mkdirSync(path.join(targetsAbsDir, 'nested-index', 'two')); + +function asynctest(testBlock, args, callback, assertBlock) { + async_expected++; + testBlock.apply(testBlock, args.concat(function(err) { + let ignoreError = false; + if (assertBlock) { + try { + ignoreError = assertBlock.apply(assertBlock, arguments); + } catch (e) { + err = e; + } + } + async_completed++; + callback(ignoreError ? null : err); + })); +} + +// sub-tests: +function test_simple_error_callback(realpath, realpathSync, cb) { + realpath('/this/path/does/not/exist', common.mustCall(function(err, s) { + assert(err); + assert(!s); + cb(); + })); +} + +function test_simple_error_cb_with_null_options(realpath, realpathSync, cb) { + realpath('/this/path/does/not/exist', null, common.mustCall(function(err, s) { + assert(err); + assert(!s); + cb(); + })); +} + +function test_simple_relative_symlink(realpath, realpathSync, callback) { + console.log('test_simple_relative_symlink'); + if (skipSymlinks) { + common.printSkipMessage('symlink test (no privs)'); + return callback(); + } + const entry = `${tmpDir}/symlink`; + const expected = `${tmpDir}/cycles/root.js`; + [ + [entry, `../${path.basename(tmpDir)}/cycles/root.js`], + ].forEach(function(t) { + try { fs.unlinkSync(t[0]); } catch { + // Continue regardless of error. + } + console.log('fs.symlinkSync(%j, %j, %j)', t[1], t[0], 'file'); + fs.symlinkSync(t[1], t[0], 'file'); + unlink.push(t[0]); + }); + const result = realpathSync(entry); + assertEqualPath(result, path.resolve(expected)); + asynctest(realpath, [entry], callback, function(err, result) { + assertEqualPath(result, path.resolve(expected)); + }); +} + +function test_simple_absolute_symlink(realpath, realpathSync, callback) { + console.log('test_simple_absolute_symlink'); + + // This one should still run, even if skipSymlinks is set, + // because it uses a junction. + const type = skipSymlinks ? 'junction' : 'dir'; + + console.log('using type=%s', type); + + const entry = `${tmpAbsDir}/symlink`; + const expected = fixtures.path('nested-index', 'one'); + [ + [entry, expected], + ].forEach(function(t) { + try { fs.unlinkSync(t[0]); } catch { + // Continue regardless of error. + } + console.error('fs.symlinkSync(%j, %j, %j)', t[1], t[0], type); + fs.symlinkSync(t[1], t[0], type); + unlink.push(t[0]); + }); + const result = realpathSync(entry); + assertEqualPath(result, path.resolve(expected)); + asynctest(realpath, [entry], callback, function(err, result) { + assertEqualPath(result, path.resolve(expected)); + }); +} + +function test_deep_relative_file_symlink(realpath, realpathSync, callback) { + console.log('test_deep_relative_file_symlink'); + if (skipSymlinks) { + common.printSkipMessage('symlink test (no privs)'); + return callback(); + } + + const expected = fixtures.path('cycles', 'root.js'); + const linkData1 = path + .relative(path.join(targetsAbsDir, 'nested-index', 'one'), + expected); + const linkPath1 = path.join(targetsAbsDir, + 'nested-index', 'one', 'symlink1.js'); + try { fs.unlinkSync(linkPath1); } catch { + // Continue regardless of error. + } + fs.symlinkSync(linkData1, linkPath1, 'file'); + + const linkData2 = '../one/symlink1.js'; + const entry = path.join(targetsAbsDir, + 'nested-index', 'two', 'symlink1-b.js'); + try { fs.unlinkSync(entry); } catch { + // Continue regardless of error. + } + fs.symlinkSync(linkData2, entry, 'file'); + unlink.push(linkPath1); + unlink.push(entry); + + assertEqualPath(realpathSync(entry), path.resolve(expected)); + asynctest(realpath, [entry], callback, function(err, result) { + assertEqualPath(result, path.resolve(expected)); + }); +} + +function test_deep_relative_dir_symlink(realpath, realpathSync, callback) { + console.log('test_deep_relative_dir_symlink'); + if (skipSymlinks) { + common.printSkipMessage('symlink test (no privs)'); + return callback(); + } + const expected = fixtures.path('cycles', 'folder'); + const path1b = path.join(targetsAbsDir, 'nested-index', 'one'); + const linkPath1b = path.join(path1b, 'symlink1-dir'); + const linkData1b = path.relative(path1b, expected); + try { fs.unlinkSync(linkPath1b); } catch { + // Continue regardless of error. + } + fs.symlinkSync(linkData1b, linkPath1b, 'dir'); + + const linkData2b = '../one/symlink1-dir'; + const entry = path.join(targetsAbsDir, + 'nested-index', 'two', 'symlink12-dir'); + try { fs.unlinkSync(entry); } catch { + // Continue regardless of error. + } + fs.symlinkSync(linkData2b, entry, 'dir'); + unlink.push(linkPath1b); + unlink.push(entry); + + assertEqualPath(realpathSync(entry), path.resolve(expected)); + + asynctest(realpath, [entry], callback, function(err, result) { + assertEqualPath(result, path.resolve(expected)); + }); +} + +function test_cyclic_link_protection(realpath, realpathSync, callback) { + console.log('test_cyclic_link_protection'); + if (skipSymlinks) { + common.printSkipMessage('symlink test (no privs)'); + return callback(); + } + const entry = path.join(tmpDir, '/cycles/realpath-3a'); + [ + [entry, '../cycles/realpath-3b'], + [path.join(tmpDir, '/cycles/realpath-3b'), '../cycles/realpath-3c'], + [path.join(tmpDir, '/cycles/realpath-3c'), '../cycles/realpath-3a'], + ].forEach(function(t) { + try { fs.unlinkSync(t[0]); } catch { + // Continue regardless of error. + } + fs.symlinkSync(t[1], t[0], 'dir'); + unlink.push(t[0]); + }); + assert.throws(() => { + realpathSync(entry); + }, { code: 'ELOOP', name: 'Error' }); + asynctest( + realpath, [entry], callback, common.mustCall(function(err, result) { + assert.strictEqual(err.path, entry); + assert.strictEqual(result, undefined); + return true; + })); +} + +function test_cyclic_link_overprotection(realpath, realpathSync, callback) { + console.log('test_cyclic_link_overprotection'); + if (skipSymlinks) { + common.printSkipMessage('symlink test (no privs)'); + return callback(); + } + const cycles = `${tmpDir}/cycles`; + const expected = realpathSync(cycles); + const folder = `${cycles}/folder`; + const link = `${folder}/cycles`; + let testPath = cycles; + testPath += '/folder/cycles'.repeat(10); + try { fs.unlinkSync(link); } catch { + // Continue regardless of error. + } + fs.symlinkSync(cycles, link, 'dir'); + unlink.push(link); + assertEqualPath(realpathSync(testPath), path.resolve(expected)); + asynctest(realpath, [testPath], callback, function(er, res) { + assertEqualPath(res, path.resolve(expected)); + }); +} + +function test_relative_input_cwd(realpath, realpathSync, callback) { + console.log('test_relative_input_cwd'); + if (skipSymlinks) { + common.printSkipMessage('symlink test (no privs)'); + return callback(); + } + + // We need to calculate the relative path to the tmp dir from cwd + const entrydir = process.cwd(); + const entry = path.relative(entrydir, + path.join(`${tmpDir}/cycles/realpath-3a`)); + const expected = `${tmpDir}/cycles/root.js`; + [ + [entry, '../cycles/realpath-3b'], + [`${tmpDir}/cycles/realpath-3b`, '../cycles/realpath-3c'], + [`${tmpDir}/cycles/realpath-3c`, 'root.js'], + ].forEach(function(t) { + const fn = t[0]; + console.error('fn=%j', fn); + try { fs.unlinkSync(fn); } catch { + // Continue regardless of error. + } + const b = path.basename(t[1]); + const type = (b === 'root.js' ? 'file' : 'dir'); + console.log('fs.symlinkSync(%j, %j, %j)', t[1], fn, type); + fs.symlinkSync(t[1], fn, 'file'); + unlink.push(fn); + }); + + const origcwd = process.cwd(); + process.chdir(entrydir); + assertEqualPath(realpathSync(entry), path.resolve(expected)); + asynctest(realpath, [entry], callback, function(err, result) { + process.chdir(origcwd); + assertEqualPath(result, path.resolve(expected)); + return true; + }); +} + +function test_deep_symlink_mix(realpath, realpathSync, callback) { + console.log('test_deep_symlink_mix'); + if (common.isWindows) { + // This one is a mix of files and directories, and it's quite tricky + // to get the file/dir links sorted out correctly. + common.printSkipMessage('symlink test (no privs)'); + return callback(); + } + + // /tmp/node-test-realpath-f1 -> $tmpDir/node-test-realpath-d1/foo + // /tmp/node-test-realpath-d1 -> $tmpDir/node-test-realpath-d2 + // /tmp/node-test-realpath-d2/foo -> $tmpDir/node-test-realpath-f2 + // /tmp/node-test-realpath-f2 + // -> $tmpDir/targets/nested-index/one/realpath-c + // $tmpDir/targets/nested-index/one/realpath-c + // -> $tmpDir/targets/nested-index/two/realpath-c + // $tmpDir/targets/nested-index/two/realpath-c -> $tmpDir/cycles/root.js + // $tmpDir/targets/cycles/root.js (hard) + + const entry = tmp('node-test-realpath-f1'); + try { fs.unlinkSync(tmp('node-test-realpath-d2/foo')); } catch { + // Continue regardless of error. + } + try { fs.rmdirSync(tmp('node-test-realpath-d2')); } catch { + // Continue regardless of error. + } + fs.mkdirSync(tmp('node-test-realpath-d2'), 0o700); + try { + [ + [entry, `${tmpDir}/node-test-realpath-d1/foo`], + [tmp('node-test-realpath-d1'), + `${tmpDir}/node-test-realpath-d2`], + [tmp('node-test-realpath-d2/foo'), '../node-test-realpath-f2'], + [tmp('node-test-realpath-f2'), + `${targetsAbsDir}/nested-index/one/realpath-c`], + [`${targetsAbsDir}/nested-index/one/realpath-c`, + `${targetsAbsDir}/nested-index/two/realpath-c`], + [`${targetsAbsDir}/nested-index/two/realpath-c`, + `${tmpDir}/cycles/root.js`], + ].forEach(function(t) { + try { fs.unlinkSync(t[0]); } catch { + // Continue regardless of error. + } + fs.symlinkSync(t[1], t[0]); + unlink.push(t[0]); + }); + } finally { + unlink.push(tmp('node-test-realpath-d2')); + } + const expected = `${tmpAbsDir}/cycles/root.js`; + assertEqualPath(realpathSync(entry), path.resolve(expected)); + asynctest(realpath, [entry], callback, function(err, result) { + assertEqualPath(result, path.resolve(expected)); + return true; + }); +} + +function test_non_symlinks(realpath, realpathSync, callback) { + console.log('test_non_symlinks'); + const entrydir = path.dirname(tmpAbsDir); + const entry = `${tmpAbsDir.slice(entrydir.length + 1)}/cycles/root.js`; + const expected = `${tmpAbsDir}/cycles/root.js`; + const origcwd = process.cwd(); + process.chdir(entrydir); + assertEqualPath(realpathSync(entry), path.resolve(expected)); + asynctest(realpath, [entry], callback, function(err, result) { + process.chdir(origcwd); + assertEqualPath(result, path.resolve(expected)); + return true; + }); +} + +const upone = path.join(process.cwd(), '..'); +function test_escape_cwd(realpath, realpathSync, cb) { + console.log('test_escape_cwd'); + asynctest(realpath, ['..'], cb, function(er, uponeActual) { + assertEqualPath( + upone, uponeActual, + `realpath("..") expected: ${path.resolve(upone)} actual:${uponeActual}`); + }); +} + +function test_upone_actual(realpath, realpathSync, cb) { + console.log('test_upone_actual'); + const uponeActual = realpathSync('..'); + assertEqualPath(upone, uponeActual); + cb(); +} + +// Going up with .. multiple times +// . +// `-- a/ +// |-- b/ +// | `-- e -> .. +// `-- d -> .. +// realpath(a/b/e/d/a/b/e/d/a) ==> a +function test_up_multiple(realpath, realpathSync, cb) { + console.error('test_up_multiple'); + if (skipSymlinks) { + common.printSkipMessage('symlink test (no privs)'); + return cb(); + } + const tmpdir = require('../common/tmpdir'); + tmpdir.refresh(); + fs.mkdirSync(tmp('a'), 0o755); + fs.mkdirSync(tmp('a/b'), 0o755); + fs.symlinkSync('..', tmp('a/d'), 'dir'); + unlink.push(tmp('a/d')); + fs.symlinkSync('..', tmp('a/b/e'), 'dir'); + unlink.push(tmp('a/b/e')); + + const abedabed = tmp('abedabed'.split('').join('/')); + const abedabed_real = tmp(''); + + const abedabeda = tmp('abedabeda'.split('').join('/')); + const abedabeda_real = tmp('a'); + + assertEqualPath(realpathSync(abedabeda), abedabeda_real); + assertEqualPath(realpathSync(abedabed), abedabed_real); + + realpath(abedabeda, function(er, real) { + assert.ifError(er); + assertEqualPath(abedabeda_real, real); + realpath(abedabed, function(er, real) { + assert.ifError(er); + assertEqualPath(abedabed_real, real); + cb(); + }); + }); +} + + +// Going up with .. multiple times with options = null +// . +// `-- a/ +// |-- b/ +// | `-- e -> .. +// `-- d -> .. +// realpath(a/b/e/d/a/b/e/d/a) ==> a +function test_up_multiple_with_null_options(realpath, realpathSync, cb) { + console.error('test_up_multiple'); + if (skipSymlinks) { + common.printSkipMessage('symlink test (no privs)'); + return cb(); + } + const tmpdir = require('../common/tmpdir'); + tmpdir.refresh(); + fs.mkdirSync(tmp('a'), 0o755); + fs.mkdirSync(tmp('a/b'), 0o755); + fs.symlinkSync('..', tmp('a/d'), 'dir'); + unlink.push(tmp('a/d')); + fs.symlinkSync('..', tmp('a/b/e'), 'dir'); + unlink.push(tmp('a/b/e')); + + const abedabed = tmp('abedabed'.split('').join('/')); + const abedabed_real = tmp(''); + + const abedabeda = tmp('abedabeda'.split('').join('/')); + const abedabeda_real = tmp('a'); + + assertEqualPath(realpathSync(abedabeda), abedabeda_real); + assertEqualPath(realpathSync(abedabed), abedabed_real); + + realpath(abedabeda, null, function(er, real) { + assert.ifError(er); + assertEqualPath(abedabeda_real, real); + realpath(abedabed, null, function(er, real) { + assert.ifError(er); + assertEqualPath(abedabed_real, real); + cb(); + }); + }); +} + +// Absolute symlinks with children. +// . +// `-- a/ +// |-- b/ +// | `-- c/ +// | `-- x.txt +// `-- link -> /tmp/node-test-realpath-abs-kids/a/b/ +// realpath(root+'/a/link/c/x.txt') ==> root+'/a/b/c/x.txt' +function test_abs_with_kids(realpath, realpathSync, cb) { + console.log('test_abs_with_kids'); + + // This one should still run, even if skipSymlinks is set, + // because it uses a junction. + const type = skipSymlinks ? 'junction' : 'dir'; + + console.log('using type=%s', type); + + const root = `${tmpAbsDir}/node-test-realpath-abs-kids`; + function cleanup() { + ['/a/b/c/x.txt', + '/a/link', + ].forEach(function(file) { + try { fs.unlinkSync(root + file); } catch { + // Continue regardless of error. + } + }); + ['/a/b/c', + '/a/b', + '/a', + '', + ].forEach(function(folder) { + try { fs.rmdirSync(root + folder); } catch { + // Continue regardless of error. + } + }); + } + + function setup() { + cleanup(); + ['', + '/a', + '/a/b', + '/a/b/c', + ].forEach(function(folder) { + console.log(`mkdir ${root}${folder}`); + fs.mkdirSync(root + folder, 0o700); + }); + fs.writeFileSync(`${root}/a/b/c/x.txt`, 'foo'); + fs.symlinkSync(`${root}/a/b`, `${root}/a/link`, type); + } + setup(); + const linkPath = `${root}/a/link/c/x.txt`; + const expectPath = `${root}/a/b/c/x.txt`; + const actual = realpathSync(linkPath); + // console.log({link:linkPath,expect:expectPath,actual:actual},'sync'); + assertEqualPath(actual, path.resolve(expectPath)); + asynctest(realpath, [linkPath], cb, function(er, actual) { + // console.log({link:linkPath,expect:expectPath,actual:actual},'async'); + assertEqualPath(actual, path.resolve(expectPath)); + cleanup(); + }); +} + +function test_root(realpath, realpathSync, cb) { + assertEqualPath(root, realpathSync('/')); + realpath('/', function(err, result) { + assert.ifError(err); + assertEqualPath(root, result); + cb(); + }); +} + +function test_root_with_null_options(realpath, realpathSync, cb) { + realpath('/', null, function(err, result) { + assert.ifError(err); + assertEqualPath(root, result); + cb(); + }); +} + +// ---------------------------------------------------------------------------- + +const tests = [ + test_simple_error_callback, + test_simple_error_cb_with_null_options, + test_simple_relative_symlink, + test_simple_absolute_symlink, + test_deep_relative_file_symlink, + test_deep_relative_dir_symlink, + test_cyclic_link_protection, + test_cyclic_link_overprotection, + test_relative_input_cwd, + test_deep_symlink_mix, + test_non_symlinks, + test_escape_cwd, + test_upone_actual, + test_abs_with_kids, + test_up_multiple, + test_up_multiple_with_null_options, + test_root, + test_root_with_null_options, +]; +const numtests = tests.length; +let testsRun = 0; +function runNextTest(err) { + assert.ifError(err); + const test = tests.shift(); + if (!test) { + return console.log(`${numtests} subtests completed OK for fs.realpath`); + } + testsRun++; + test(fs.realpath, fs.realpathSync, common.mustSucceed(() => { + testsRun++; + test(fs.realpath.native, + fs.realpathSync.native, + common.mustCall(runNextTest)); + })); +} + +function runTest() { + const tmpDirs = ['cycles', 'cycles/folder']; + tmpDirs.forEach(function(t) { + t = tmp(t); + fs.mkdirSync(t, 0o700); + }); + fs.writeFileSync(tmp('cycles/root.js'), "console.error('roooot!');"); + console.error('start tests'); + runNextTest(); +} + + +process.on('exit', function() { + assert.strictEqual(2 * numtests, testsRun); + assert.strictEqual(async_completed, async_expected); +}); diff --git a/test/js/node/test/parallel/test-fs-symlink-dir-junction.js b/test/js/node/test/parallel/test-fs-symlink-dir-junction.js deleted file mode 100644 index 45495aadb2..0000000000 --- a/test/js/node/test/parallel/test-fs-symlink-dir-junction.js +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -'use strict'; -const common = require('../common'); -const fixtures = require('../common/fixtures'); -const assert = require('assert'); -const fs = require('fs'); - -const tmpdir = require('../common/tmpdir'); - -// Test creating and reading symbolic link -const linkData = fixtures.path('cycles/'); -const linkPath = tmpdir.resolve('cycles_link'); - -tmpdir.refresh(); - -fs.symlink(linkData, linkPath, 'junction', common.mustSucceed(() => { - fs.lstat(linkPath, common.mustSucceed((stats) => { - assert.ok(stats.isSymbolicLink()); - - fs.readlink(linkPath, common.mustSucceed((destination) => { - assert.strictEqual(destination, linkData); - - fs.unlink(linkPath, common.mustSucceed(() => { - assert(!fs.existsSync(linkPath)); - assert(fs.existsSync(linkData)); - })); - })); - })); -})); - -// Test invalid symlink -{ - const linkData = fixtures.path('/not/exists/dir'); - const linkPath = tmpdir.resolve('invalid_junction_link'); - - fs.symlink(linkData, linkPath, 'junction', common.mustSucceed(() => { - if (!common.isWindows) // TODO: BUN - assert(!fs.existsSync(linkPath)); - - fs.unlink(linkPath, common.mustSucceed(() => { - assert(!fs.existsSync(linkPath)); - })); - })); -} diff --git a/test/js/node/test/parallel/test-fs-symlink-dir.js b/test/js/node/test/parallel/test-fs-symlink-dir.js deleted file mode 100644 index 32e3897c92..0000000000 --- a/test/js/node/test/parallel/test-fs-symlink-dir.js +++ /dev/null @@ -1,84 +0,0 @@ -'use strict'; -const common = require('../common'); -if (common.isWindows) return; // TODO: BUN - -// Test creating a symbolic link pointing to a directory. -// Ref: https://github.com/nodejs/node/pull/23724 -// Ref: https://github.com/nodejs/node/issues/23596 - - -if (!common.canCreateSymLink()) - common.skip('insufficient privileges'); - -const assert = require('assert'); -const path = require('path'); -const fs = require('fs'); -const fsPromises = fs.promises; - -const tmpdir = require('../common/tmpdir'); -tmpdir.refresh(); - -const linkTargets = [ - 'relative-target', - tmpdir.resolve('absolute-target'), -]; -const linkPaths = [ - path.relative(process.cwd(), tmpdir.resolve('relative-path')), - tmpdir.resolve('absolute-path'), -]; - -function testSync(target, path) { - fs.symlinkSync(target, path); - fs.readdirSync(path); -} - -function testAsync(target, path) { - fs.symlink(target, path, common.mustSucceed(() => { - fs.readdirSync(path); - })); -} - -async function testPromises(target, path) { - await fsPromises.symlink(target, path); - fs.readdirSync(path); -} - -for (const linkTarget of linkTargets) { - fs.mkdirSync(tmpdir.resolve(linkTarget)); - for (const linkPath of linkPaths) { - testSync(linkTarget, `${linkPath}-${path.basename(linkTarget)}-sync`); - testAsync(linkTarget, `${linkPath}-${path.basename(linkTarget)}-async`); - testPromises(linkTarget, `${linkPath}-${path.basename(linkTarget)}-promises`) - .then(common.mustCall()); - } -} - -if (common.isWindows) return; // TODO: BUN - -// Test invalid symlink -{ - function testSync(target, path) { - fs.symlinkSync(target, path); - assert(!fs.existsSync(path)); - } - - function testAsync(target, path) { - fs.symlink(target, path, common.mustSucceed(() => { - assert(!fs.existsSync(path)); - })); - } - - async function testPromises(target, path) { - await fsPromises.symlink(target, path); - assert(!fs.existsSync(path)); - } - - for (const linkTarget of linkTargets.map((p) => p + '-broken')) { - for (const linkPath of linkPaths) { - testSync(linkTarget, `${linkPath}-${path.basename(linkTarget)}-sync`); - testAsync(linkTarget, `${linkPath}-${path.basename(linkTarget)}-async`); - testPromises(linkTarget, `${linkPath}-${path.basename(linkTarget)}-promises`) - .then(common.mustCall()); - } - } -} diff --git a/test/js/node/test/parallel/test-fs-symlink-longpath.js b/test/js/node/test/parallel/test-fs-symlink-longpath.js deleted file mode 100644 index 581ec4683e..0000000000 --- a/test/js/node/test/parallel/test-fs-symlink-longpath.js +++ /dev/null @@ -1,28 +0,0 @@ -'use strict'; - -const common = require('../common'); -if (common.isWindows) return; // TODO: BUN -const assert = require('assert'); -const path = require('path'); -const fs = require('fs'); - -const tmpdir = require('../common/tmpdir'); -tmpdir.refresh(); -const tmpDir = tmpdir.path; -const longPath = path.join(...[tmpDir].concat(Array(30).fill('1234567890'))); -fs.mkdirSync(longPath, { recursive: true }); - -// Test if we can have symlinks to files and folders with long filenames -const targetDirectory = path.join(longPath, 'target-directory'); -fs.mkdirSync(targetDirectory); -const pathDirectory = path.join(tmpDir, 'new-directory'); -fs.symlink(targetDirectory, pathDirectory, 'dir', common.mustSucceed(() => { - assert(fs.existsSync(pathDirectory)); -})); - -const targetFile = path.join(longPath, 'target-file'); -fs.writeFileSync(targetFile, 'data'); -const pathFile = path.join(tmpDir, 'new-file'); -fs.symlink(targetFile, pathFile, common.mustSucceed(() => { - assert(fs.existsSync(pathFile)); -})); diff --git a/test/js/node/test/parallel/test-fs-utimes-y2K38.js b/test/js/node/test/parallel/test-fs-utimes-y2K38.js index 5aa20c39a6..9e42e90feb 100644 --- a/test/js/node/test/parallel/test-fs-utimes-y2K38.js +++ b/test/js/node/test/parallel/test-fs-utimes-y2K38.js @@ -1,6 +1,5 @@ 'use strict'; const common = require('../common'); -if (common.isWindows) return; // TODO: BUN const tmpdir = require('../common/tmpdir'); tmpdir.refresh(); diff --git a/test/js/node/test/parallel/test-fs-long-path.js b/test/js/node/test/parallel/test-fs-watch-file-enoent-after-deletion.js similarity index 61% rename from test/js/node/test/parallel/test-fs-long-path.js rename to test/js/node/test/parallel/test-fs-watch-file-enoent-after-deletion.js index df37ac7672..e4baf90fd1 100644 --- a/test/js/node/test/parallel/test-fs-long-path.js +++ b/test/js/node/test/parallel/test-fs-watch-file-enoent-after-deletion.js @@ -21,33 +21,27 @@ 'use strict'; const common = require('../common'); -if (common.isWindows) return; // TODO: BUN -if (!common.isWindows) - common.skip('this test is Windows-specific.'); + +// Make sure the deletion event gets reported in the following scenario: +// 1. Watch a file. +// 2. The initial stat() goes okay. +// 3. Something deletes the watched file. +// 4. The second stat() fails with ENOENT. + +// The second stat() translates into the first 'change' event but a logic error +// stopped it from getting emitted. +// https://github.com/nodejs/node-v0.x-archive/issues/4027 const fs = require('fs'); -const path = require('path'); const tmpdir = require('../common/tmpdir'); - -// Make a path that will be at least 260 chars long. -const fileNameLen = Math.max(260 - tmpdir.path.length - 1, 1); -const fileName = tmpdir.resolve('x'.repeat(fileNameLen)); -const fullPath = path.resolve(fileName); - tmpdir.refresh(); -console.log({ - filenameLength: fileName.length, - fullPathLength: fullPath.length -}); +const filename = tmpdir.resolve('watched'); +fs.writeFileSync(filename, 'quis custodiet ipsos custodes'); -fs.writeFile(fullPath, 'ok', common.mustSucceed(() => { - fs.stat(fullPath, common.mustSucceed()); - - // Tests https://github.com/nodejs/node/issues/39721 - fs.realpath.native(fullPath, common.mustSucceed()); - - // Tests https://github.com/nodejs/node/issues/51031 - fs.promises.realpath(fullPath).then(common.mustCall(), common.mustNotCall()); +fs.watchFile(filename, { interval: 50 }, common.mustCall(function(curr, prev) { + fs.unwatchFile(filename); })); + +fs.unlinkSync(filename); diff --git a/test/js/node/test/parallel/test-fs-watch-recursive-add-file-to-existing-subfolder.js b/test/js/node/test/parallel/test-fs-watch-recursive-add-file-to-existing-subfolder.js index 628ca4b2fd..511829fa38 100644 --- a/test/js/node/test/parallel/test-fs-watch-recursive-add-file-to-existing-subfolder.js +++ b/test/js/node/test/parallel/test-fs-watch-recursive-add-file-to-existing-subfolder.js @@ -40,9 +40,8 @@ const relativePath = path.join(file, path.basename(subfolderPath), childrenFile) const watcher = fs.watch(testDirectory, { recursive: true }); let watcherClosed = false; watcher.on('change', function(event, filename) { - assert.strictEqual(event, 'rename'); - if (filename === relativePath) { + assert.strictEqual(event, 'rename'); watcher.close(); watcherClosed = true; } diff --git a/test/js/node/test/parallel/test-fs-watch-recursive-add-file-with-url.js b/test/js/node/test/parallel/test-fs-watch-recursive-add-file-with-url.js index ee726961c4..852c7088d5 100644 --- a/test/js/node/test/parallel/test-fs-watch-recursive-add-file-with-url.js +++ b/test/js/node/test/parallel/test-fs-watch-recursive-add-file-with-url.js @@ -35,9 +35,8 @@ tmpdir.refresh(); const watcher = fs.watch(url, { recursive: true }); let watcherClosed = false; watcher.on('change', function(event, filename) { - assert.strictEqual(event, 'rename'); - if (filename === path.basename(filePath)) { + assert.strictEqual(event, 'rename'); watcher.close(); watcherClosed = true; } diff --git a/test/js/node/test/parallel/test-fs-watch-recursive-add-file.js b/test/js/node/test/parallel/test-fs-watch-recursive-add-file.js index 27b933871c..e8724102c8 100644 --- a/test/js/node/test/parallel/test-fs-watch-recursive-add-file.js +++ b/test/js/node/test/parallel/test-fs-watch-recursive-add-file.js @@ -31,9 +31,8 @@ const testFile = path.join(testDirectory, 'file-1.txt'); const watcher = fs.watch(testDirectory, { recursive: true }); let watcherClosed = false; watcher.on('change', function(event, filename) { - assert.strictEqual(event, 'rename'); - if (filename === path.basename(testFile)) { + assert.strictEqual(event, 'rename'); watcher.close(); watcherClosed = true; } diff --git a/test/js/node/test/parallel/test-fs-watch-recursive-add-folder.js b/test/js/node/test/parallel/test-fs-watch-recursive-add-folder.js index 1851a7850f..1a6671de2f 100644 --- a/test/js/node/test/parallel/test-fs-watch-recursive-add-folder.js +++ b/test/js/node/test/parallel/test-fs-watch-recursive-add-folder.js @@ -33,9 +33,8 @@ tmpdir.refresh(); const watcher = fs.watch(testDirectory, { recursive: true }); let watcherClosed = false; watcher.on('change', function(event, filename) { - assert.strictEqual(event, 'rename'); - if (filename === path.basename(testFile)) { + assert.strictEqual(event, 'rename'); watcher.close(); watcherClosed = true; } diff --git a/test/js/node/test/parallel/test-fs-watch-recursive-linux-parallel-remove.js b/test/js/node/test/parallel/test-fs-watch-recursive-linux-parallel-remove.js deleted file mode 100644 index dbc8d069b2..0000000000 --- a/test/js/node/test/parallel/test-fs-watch-recursive-linux-parallel-remove.js +++ /dev/null @@ -1,35 +0,0 @@ -'use strict'; -const isCI = process.env.CI !== undefined; - -const common = require('../common'); -if (common.isLinux && isCI) return; // TODO: BUN - -if (!common.isLinux) - common.skip('This test can run only on Linux'); - -// Test that the watcher do not crash if the file "disappears" while -// watch is being set up. - -const path = require('node:path'); -const fs = require('node:fs'); -const { spawn } = require('node:child_process'); - -const tmpdir = require('../common/tmpdir'); -const testDir = tmpdir.path; -tmpdir.refresh(); - -const watcher = fs.watch(testDir, { recursive: true }); -watcher.on('change', function(event, filename) { - // This console.log makes the error happen - // do not remove - console.log(filename, event); -}); - -const testFile = path.join(testDir, 'a'); -const child = spawn(process.argv[0], ['-e', `const fs = require('node:fs'); for (let i = 0; i < 10000; i++) { const fd = fs.openSync('${testFile}', 'w'); fs.writeSync(fd, Buffer.from('hello')); fs.rmSync('${testFile}') }`], { - stdio: 'inherit' -}); - -child.on('exit', function() { - watcher.close(); -}); diff --git a/test/js/node/test/parallel/test-fs-watch-recursive-sync-write.js b/test/js/node/test/parallel/test-fs-watch-recursive-sync-write.js index ecc380d190..dd7a64e1f0 100644 --- a/test/js/node/test/parallel/test-fs-watch-recursive-sync-write.js +++ b/test/js/node/test/parallel/test-fs-watch-recursive-sync-write.js @@ -25,14 +25,24 @@ const keepalive = setTimeout(() => { throw new Error('timed out'); }, common.platformTimeout(30_000)); -const watcher = watch(tmpDir, { recursive: true }, common.mustCall((eventType, _filename) => { - clearTimeout(keepalive); - watcher.close(); - assert.strictEqual(eventType, 'rename'); - assert.strictEqual(join(tmpDir, _filename), filename); -})); +function doWatch() { + const watcher = watch(tmpDir, { recursive: true }, common.mustCall((eventType, _filename) => { + clearTimeout(keepalive); + watcher.close(); + assert.strictEqual(eventType, 'rename'); + assert.strictEqual(join(tmpDir, _filename), filename); + })); -// Do the write with a delay to ensure that the OS is ready to notify us. -setTimeout(() => { - writeFileSync(filename, 'foobar2'); -}, common.platformTimeout(200)); + // Do the write with a delay to ensure that the OS is ready to notify us. + setTimeout(() => { + writeFileSync(filename, 'foobar2'); + }, common.platformTimeout(200)); +} + +if (common.isMacOS) { + // On macOS delay watcher start to avoid leaking previous events. + // Refs: https://github.com/libuv/libuv/pull/4503 + setTimeout(doWatch, common.platformTimeout(100)); +} else { + doWatch(); +} diff --git a/test/js/node/test/parallel/test-fs-watch-recursive-update-file.js b/test/js/node/test/parallel/test-fs-watch-recursive-update-file.js index 7100b015ab..e27a4c37e4 100644 --- a/test/js/node/test/parallel/test-fs-watch-recursive-update-file.js +++ b/test/js/node/test/parallel/test-fs-watch-recursive-update-file.js @@ -31,7 +31,7 @@ fs.writeFileSync(testFile, 'hello'); const watcher = fs.watch(testDirectory, { recursive: true }); watcher.on('change', common.mustCallAtLeast(function(event, filename) { - // Libuv inconsistenly emits a rename event for the file we are watching + // Libuv inconsistently emits a rename event for the file we are watching assert.ok(event === 'change' || event === 'rename'); if (filename === path.basename(testFile)) { diff --git a/test/js/node/test/parallel/test-fs-watch.js b/test/js/node/test/parallel/test-fs-watch.js index a494b9f8df..5194e04fce 100644 --- a/test/js/node/test/parallel/test-fs-watch.js +++ b/test/js/node/test/parallel/test-fs-watch.js @@ -41,13 +41,7 @@ const cases = [ const tmpdir = require('../common/tmpdir'); tmpdir.refresh(); -for (const testCase of cases) { - if (testCase.shouldSkip) continue; - fs.mkdirSync(testCase.dirPath); - // Long content so it's actually flushed. - const content1 = Date.now() + testCase.fileName.toLowerCase().repeat(1e4); - fs.writeFileSync(testCase.filePath, content1); - +function doWatchTest(testCase) { let interval; const pathToWatch = testCase[testCase.field]; const watcher = fs.watch(pathToWatch); @@ -87,6 +81,23 @@ for (const testCase of cases) { }, 100); } +for (const testCase of cases) { + if (testCase.shouldSkip) continue; + fs.mkdirSync(testCase.dirPath); + // Long content so it's actually flushed. + const content1 = Date.now() + testCase.fileName.toLowerCase().repeat(1e4); + fs.writeFileSync(testCase.filePath, content1); + if (common.isMacOS) { + // On macOS delay watcher start to avoid leaking previous events. + // Refs: https://github.com/libuv/libuv/pull/4503 + setTimeout(() => { + doWatchTest(testCase); + }, common.platformTimeout(100)); + } else { + doWatchTest(testCase); + } +} + [false, 1, {}, [], null, undefined].forEach((input) => { assert.throws( () => fs.watch(input, common.mustNotCall()), diff --git a/test/js/node/test/parallel/test-fs-write-file-buffer.js b/test/js/node/test/parallel/test-fs-write-file-buffer.js index ec6c60e1a4..23dc48081a 100644 --- a/test/js/node/test/parallel/test-fs-write-file-buffer.js +++ b/test/js/node/test/parallel/test-fs-write-file-buffer.js @@ -21,7 +21,6 @@ 'use strict'; require('../common'); -const util = require('util'); const fs = require('fs'); let data = [ @@ -50,5 +49,3 @@ tmpdir.refresh(); const buf = Buffer.from(data, 'base64'); fs.writeFileSync(tmpdir.resolve('test.jpg'), buf); - -util.log('Done!'); diff --git a/test/js/node/test/parallel/test-fs-write-file-invalid-path.js b/test/js/node/test/parallel/test-fs-write-file-invalid-path.js index 1e110025a9..aaa7eacde5 100644 --- a/test/js/node/test/parallel/test-fs-write-file-invalid-path.js +++ b/test/js/node/test/parallel/test-fs-write-file-invalid-path.js @@ -6,7 +6,6 @@ const fs = require('fs'); if (!common.isWindows) common.skip('This test is for Windows only.'); -if (common.isWindows) return; // TODO: BUN const tmpdir = require('../common/tmpdir'); tmpdir.refresh(); diff --git a/test/js/node/test/parallel/test-http-chunk-problem.js b/test/js/node/test/parallel/test-http-chunk-problem.js index a3c354aecd..3629b75766 100644 --- a/test/js/node/test/parallel/test-http-chunk-problem.js +++ b/test/js/node/test/parallel/test-http-chunk-problem.js @@ -43,14 +43,21 @@ const filename = tmpdir.resolve('big'); let server; function executeRequest(cb) { - cp.exec([`"${process.execPath}"`, - `"${__filename}"`, + // The execPath might contain chars that should be escaped in a shell context. + // On non-Windows, we can pass the path via the env; `"` is not a valid char on + // Windows, so we can simply pass the path. + const node = `"${common.isWindows ? process.execPath : '$NODE'}"`; + const file = `"${common.isWindows ? __filename : '$FILE'}"`; + const env = common.isWindows ? process.env : { ...process.env, NODE: process.execPath, FILE: __filename }; + cp.exec([node, + file, 'request', server.address().port, '|', - `"${process.execPath}"`, - `"${__filename}"`, + node, + file, 'shasum' ].join(' '), + { env }, (err, stdout, stderr) => { if (stderr.trim() !== '') { console.log(stderr); diff --git a/test/js/node/test/parallel/test-process-chdir-errormessage.js b/test/js/node/test/parallel/test-process-chdir-errormessage.js index 16cdf4aa1d..0ed368287b 100644 --- a/test/js/node/test/parallel/test-process-chdir-errormessage.js +++ b/test/js/node/test/parallel/test-process-chdir-errormessage.js @@ -12,7 +12,7 @@ assert.throws( { name: 'Error', code: 'ENOENT', - // message: /ENOENT: No such file or directory, chdir .+ -> 'does-not-exist'/, + message: /ENOENT: no such file or directory, chdir .+ -> 'does-not-exist'/, path: process.cwd(), syscall: 'chdir', dest: 'does-not-exist' diff --git a/test/js/node/test/parallel/test-stream-base-prototype-accessors-enumerability.js b/test/js/node/test/parallel/test-stream-base-prototype-accessors-enumerability.js index 1a7f6808fe..7436428a30 100644 --- a/test/js/node/test/parallel/test-stream-base-prototype-accessors-enumerability.js +++ b/test/js/node/test/parallel/test-stream-base-prototype-accessors-enumerability.js @@ -1,4 +1,3 @@ -// Flags: --expose-internals 'use strict'; require('../common'); @@ -10,8 +9,7 @@ require('../common'); const assert = require('assert'); // Or anything that calls StreamBase::AddMethods when setting up its prototype -const { internalBinding } = require('internal/test/binding'); -const TTY = internalBinding('tty_wrap').TTY; +const TTY = process.binding('tty_wrap').TTY; { const ttyIsEnumerable = Object.prototype.propertyIsEnumerable.bind(TTY); diff --git a/test/js/node/watch/fs.watch.test.ts b/test/js/node/watch/fs.watch.test.ts index b758af71f0..d599ef3c0a 100644 --- a/test/js/node/watch/fs.watch.test.ts +++ b/test/js/node/watch/fs.watch.test.ts @@ -1,4 +1,4 @@ -import { pathToFileURL } from "bun"; +import { file, pathToFileURL } from "bun"; import { bunRun, bunRunAsScript, isWindows, tempDirWithFiles } from "harness"; import fs, { FSWatcher } from "node:fs"; import path from "path"; @@ -447,7 +447,8 @@ describe("fs.watch", () => { watcher.close(); expect.unreachable(); } catch (err: any) { - expect(err.message).toBe("Permission denied"); + expect(err.message).toBe(`EACCES: permission denied, open '${filepath}'`); + expect(err.path).toBe(filepath); expect(err.code).toBe("EACCES"); expect(err.syscall).toBe("open"); } @@ -463,7 +464,8 @@ describe("fs.watch", () => { watcher.close(); expect.unreachable(); } catch (err: any) { - expect(err.message).toBe("Permission denied"); + expect(err.message).toBe(`EACCES: permission denied, open '${filepath}'`); + expect(err.path).toBe(filepath); expect(err.code).toBe("EACCES"); expect(err.syscall).toBe("open"); } diff --git a/test/js/web/fetch/fetch.test.ts b/test/js/web/fetch/fetch.test.ts index 6c67d63859..6e8fc443ef 100644 --- a/test/js/web/fetch/fetch.test.ts +++ b/test/js/web/fetch/fetch.test.ts @@ -896,7 +896,7 @@ describe("Bun.file", () => { forEachMethod(m => () => { const file = Bun.file(path); - expect(async () => await file[m]()).toThrow("Permission denied"); + expect(async () => await file[m]()).toThrow("permission denied"); }); afterAll(() => { @@ -909,7 +909,7 @@ describe("Bun.file", () => { forEachMethod(m => async () => { const file = Bun.file(path); - expect(async () => await file[m]()).toThrow("No such file or directory"); + expect(async () => await file[m]()).toThrow("no such file or directory"); }); }); }); diff --git a/test/js/web/fetch/fetch.unix.test.ts b/test/js/web/fetch/fetch.unix.test.ts index 3722a2c372..c62ec91ed1 100644 --- a/test/js/web/fetch/fetch.unix.test.ts +++ b/test/js/web/fetch/fetch.unix.test.ts @@ -48,7 +48,7 @@ it("throws an error when the directory is not found", () => { return new Response("hello"); }, }), - ).toThrow("No such file or directory"); + ).toThrow("no such file or directory"); }); if (process.platform === "linux") {