diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index 04339e6e3c..008b90b9f3 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -2296,13 +2296,20 @@ pub const AbortSignal = extern opaque { extern fn WebCore__AbortSignal__reasonIfAborted(*AbortSignal, *JSC.JSGlobalObject, *u8) JSValue; pub const AbortReason = union(enum) { - CommonAbortReason: CommonAbortReason, - JSValue: JSValue, + common: CommonAbortReason, + js: JSValue, pub fn toBodyValueError(this: AbortReason, globalObject: *JSC.JSGlobalObject) JSC.WebCore.Body.Value.ValueError { return switch (this) { - .CommonAbortReason => |reason| .{ .AbortReason = reason }, - .JSValue => |value| .{ .JSValue = JSC.Strong.create(value, globalObject) }, + .common => |reason| .{ .AbortReason = reason }, + .js => |value| .{ .JSValue = JSC.Strong.create(value, globalObject) }, + }; + } + + pub fn toJS(this: AbortReason, global: *JSC.JSGlobalObject) JSValue { + return switch (this) { + .common => |reason| reason.toJS(global), + .js => |value| value, }; } }; @@ -2312,25 +2319,19 @@ pub const AbortSignal = extern opaque { const js_reason = WebCore__AbortSignal__reasonIfAborted(this, global, &reason); if (reason > 0) { bun.debugAssert(js_reason == .undefined); - return AbortReason{ .CommonAbortReason = @enumFromInt(reason) }; + return .{ .common = @enumFromInt(reason) }; } - if (js_reason == .zero) { - return null; + return null; // not aborted } - - return AbortReason{ .JSValue = js_reason }; + return .{ .js = js_reason }; } - pub fn ref( - this: *AbortSignal, - ) *AbortSignal { + pub fn ref(this: *AbortSignal) *AbortSignal { return cppFn("ref", .{this}); } - pub fn unref( - this: *AbortSignal, - ) void { + pub fn unref(this: *AbortSignal) void { cppFn("unref", .{this}); } diff --git a/src/bun.js/node/node_fs.zig b/src/bun.js/node/node_fs.zig index 5e64e82746..82e1e93f14 100644 --- a/src/bun.js/node/node_fs.zig +++ b/src/bun.js/node/node_fs.zig @@ -17,6 +17,7 @@ const PosixToWinNormalizer = bun.path.PosixToWinNormalizer; const FileDescriptor = bun.FileDescriptor; const FDImpl = bun.FDImpl; +const AbortSignal = JSC.AbortSignal; const Syscall = if (Environment.isWindows) bun.sys.sys_uv else bun.sys; @@ -351,8 +352,10 @@ pub const Async = struct { }; } - fn NewAsyncFSTask(comptime ReturnType: type, comptime ArgumentType: type, comptime Function: anytype) type { + fn NewAsyncFSTask(comptime ReturnType: type, comptime ArgumentType: type, comptime function: anytype) type { return struct { + pub const Task = @This(); + promise: JSC.JSPromise.Strong, args: ArgumentType, globalObject: *JSC.JSGlobalObject, @@ -361,8 +364,12 @@ pub const Async = struct { ref: bun.Async.KeepAlive = .{}, tracker: JSC.AsyncTaskTracker, - pub const Task = @This(); - + /// NewAsyncFSTask supports cancelable operations via AbortSignal, + /// so long as a "signal" field exists. The task wrapper will ensure + /// a promise rejection happens if signaled, but if `function` is + /// already called, no guarantees are made. It is recommended for + /// the functions to check .signal.aborted() for early returns. + pub const have_abort_signal = @hasField(ArgumentType, "signal"); pub const heap_label = "Async" ++ bun.meta.typeBaseName(@typeName(ArgumentType)) ++ "Task"; pub fn create( @@ -371,21 +378,17 @@ pub const Async = struct { args: ArgumentType, vm: *JSC.VirtualMachine, ) JSC.JSValue { - var task = bun.new( - Task, - Task{ - .promise = JSC.JSPromise.Strong.init(globalObject), - .args = args, - .result = undefined, - .globalObject = globalObject, - .tracker = JSC.AsyncTaskTracker.init(vm), - }, - ); + var task = bun.new(Task, .{ + .promise = JSC.JSPromise.Strong.init(globalObject), + .args = args, + .result = undefined, + .globalObject = globalObject, + .tracker = JSC.AsyncTaskTracker.init(vm), + }); task.ref.ref(vm); task.args.toThreadSafe(); task.tracker.didSchedule(globalObject); JSC.WorkPool.schedule(&task.task); - return task.promise.value(); } @@ -393,7 +396,7 @@ pub const Async = struct { var this: *Task = @alignCast(@fieldParentPtr("task", task)); var node_fs = NodeFS{}; - this.result = Function(&node_fs, this.args, .@"async"); + 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 ""; @@ -411,7 +414,6 @@ pub const Async = struct { .result => |*res| brk: { const out = globalObject.toJS(res, .temporary); success = out != .zero; - break :brk out; }, }; @@ -423,6 +425,15 @@ pub const Async = struct { tracker.willDispatch(globalObject); defer tracker.didDispatch(globalObject); + if (have_abort_signal) check_abort: { + const signal = this.args.signal orelse break :check_abort; + if (signal.reasonIfAborted(globalObject)) |reason| { + this.deinit(); + promise.reject(globalObject, reason.toJS(globalObject)); + return; + } + } + this.deinit(); switch (success) { false => { @@ -2620,8 +2631,13 @@ pub const Arguments = struct { flag: FileSystemFlags = FileSystemFlags.r, + signal: ?*AbortSignal = null, + pub fn deinit(self: ReadFile) void { self.path.deinit(); + if (self.signal) |signal| { + signal.unref(); + } } pub fn deinitAndUnprotect(self: ReadFile) void { @@ -2641,6 +2657,9 @@ pub const Arguments = struct { var encoding = Encoding.buffer; var flag = FileSystemFlags.r; + var abort_signal: ?*AbortSignal = null; + errdefer if (abort_signal) |signal| signal.unref(); + if (arguments.next()) |arg| { arguments.eat(); if (arg.isString()) { @@ -2653,17 +2672,32 @@ pub const Arguments = struct { return ctx.throwInvalidArguments("Invalid flag", .{}); }; } + + if (try arg.getTruthy(ctx, "signal")) |signal| { + if (AbortSignal.fromJS(signal)) |signal_| { + abort_signal = signal_.ref(); + } else { + return ctx.ERR_INVALID_ARG_TYPE("The \"signal\" argument must be an instance of AbortSignal", .{}).throw(); + } + } } } - // Note: Signal is not implemented - return ReadFile{ + return .{ .path = path, .encoding = encoding, .flag = flag, .limit_size_for_javascript = true, + .signal = abort_signal, }; } + + pub fn aborted(self: ReadFile) bool { + if (self.signal) |signal| { + return signal.aborted(); + } + return false; + } }; pub const WriteFile = struct { @@ -4742,24 +4776,16 @@ pub const NodeFS = struct { const ret = readFileWithOptions(this, args, flavor, .default); return switch (ret) { .err => .{ .err = ret.err }, - .result => switch (ret.result) { - .buffer => .{ - .result = .{ - .buffer = ret.result.buffer, - }, + .result => |result| switch (result) { + .buffer => |buffer| .{ + .result = .{ .buffer = buffer }, }, .transcoded_string => |str| { if (str.tag == .Dead) { return .{ .err = Syscall.Error.fromCode(.NOMEM, .read).withPathLike(args.path) }; } - return .{ - .result = .{ - .string = .{ - .underlying = str, - }, - }, - }; + return .{ .result = .{ .string = .{ .underlying = str } } }; }, .string => brk: { const str = bun.SliceWithUnderlyingString.transcodeFromOwnedSlice(@constCast(ret.result.string), args.encoding); @@ -4839,6 +4865,8 @@ pub const NodeFS = struct { _ = Syscall.close(fd); } + if (args.aborted()) return Maybe(Return.ReadFileWithOptions).aborted; + // Only used in DOMFormData if (args.offset > 0) { _ = Syscall.setFileOffset(fd, args.offset); @@ -4864,9 +4892,7 @@ pub const NodeFS = struct { var available = temporary_read_buffer; while (available.len > 0) { switch (Syscall.read(fd, available)) { - .err => |err| return .{ - .err = err, - }, + .err => |err| return .{ .err = err }, .result => |amt| { if (amt == 0) { did_succeed = true; @@ -4940,6 +4966,8 @@ pub const NodeFS = struct { }; // ---------------------------- + if (args.aborted()) return Maybe(Return.ReadFileWithOptions).aborted; + const stat_ = switch (Syscall.fstat(fd)) { .err => |err| return .{ .err = err, @@ -4982,9 +5010,7 @@ pub const NodeFS = struct { max_size, 1024 * 1024 * 1024 * 8, ), - ) catch return .{ - .err = Syscall.Error.fromCode(.NOMEM, .read).withPathLike(args.path), - }; + ) catch return .{ .err = Syscall.Error.fromCode(.NOMEM, .read).withPathLike(args.path) }; if (temporary_read_buffer_before_stat_call.len > 0) { buf.appendSlice(temporary_read_buffer_before_stat_call) catch return .{ .err = Syscall.Error.fromCode(.NOMEM, .read).withPathLike(args.path), @@ -4993,10 +5019,9 @@ pub const NodeFS = struct { buf.expandToCapacity(); while (total < size) { + if (args.aborted()) return Maybe(Return.ReadFileWithOptions).aborted; switch (Syscall.read(fd, buf.items.ptr[total..@min(buf.capacity, max_size)])) { - .err => |err| return .{ - .err = err, - }, + .err => |err| return .{ .err = err }, .result => |amt| { total += amt; @@ -5025,10 +5050,9 @@ pub const NodeFS = struct { } } else { while (true) { + if (args.aborted()) return Maybe(Return.ReadFileWithOptions).aborted; switch (Syscall.read(fd, buf.items.ptr[total..@min(buf.capacity, max_size)])) { - .err => |err| return .{ - .err = err, - }, + .err => |err| return .{ .err = err }, .result => |amt| { total += amt; @@ -5598,7 +5622,7 @@ pub const NodeFS = struct { }, .windows, ); - this.sync_error_buf[0..4].* = bun.windows.nt_maxpath_prefix_u8; + this.sync_error_buf[0..4].* = bun.windows.long_path_prefix_u8; this.sync_error_buf[4 + target.len] = 0; break :target this.sync_error_buf[0 .. 4 + target.len :0]; } diff --git a/src/bun.js/node/node_fs_binding.zig b/src/bun.js/node/node_fs_binding.zig index 89e4bea172..c8ee2aced3 100644 --- a/src/bun.js/node/node_fs_binding.zig +++ b/src/bun.js/node/node_fs_binding.zig @@ -61,6 +61,15 @@ fn Bindings(comptime function_name: NodeFSFunctionEnum) type { return .zero; } + const have_abort_signal = @hasField(Arguments, "signal"); + if (have_abort_signal) check_early_abort: { + const signal = args.signal orelse break :check_early_abort; + if (signal.reasonIfAborted(globalObject)) |reason| { + slice.deinit(); + return JSC.JSPromise.rejectedPromiseValue(globalObject, reason.toJS(globalObject)); + } + } + const Task = @field(JSC.Node.Async, @tagName(function_name)); switch (comptime function_name) { .cp => return Task.create(globalObject, this, args, globalObject.bunVM(), slice.arena), diff --git a/src/bun.js/node/node_fs_watcher.zig b/src/bun.js/node/node_fs_watcher.zig index 2fa14493bf..3c8188fe46 100644 --- a/src/bun.js/node/node_fs_watcher.zig +++ b/src/bun.js/node/node_fs_watcher.zig @@ -415,7 +415,7 @@ pub const FSWatcher = struct { should_deinit_path = false; - return Arguments{ + return .{ .path = path, .listener = listener, .global_this = ctx, diff --git a/src/bun.js/node/types.zig b/src/bun.js/node/types.zig index 8c768f5eee..8a4e74caa1 100644 --- a/src/bun.js/node/types.zig +++ b/src/bun.js/node/types.zig @@ -42,8 +42,8 @@ pub const TimeLike = if (Environment.isWindows) f64 else std.posix.timespec; /// - "path" /// - "errno" pub fn Maybe(comptime ReturnTypeT: type, comptime ErrorTypeT: type) type { - const hasRetry = ErrorTypeT != void and @hasDecl(ErrorTypeT, "retry"); - const hasTodo = ErrorTypeT != void and @hasDecl(ErrorTypeT, "todo"); + const has_retry = ErrorTypeT != void and @hasDecl(ErrorTypeT, "retry"); + const has_todo = ErrorTypeT != void and @hasDecl(ErrorTypeT, "todo"); return union(Tag) { pub const ErrorType = ErrorTypeT; @@ -60,11 +60,17 @@ pub fn Maybe(comptime ReturnTypeT: type, comptime ErrorTypeT: type) type { /// we (Zack, Dylan, Dave, Mason) observed that it was set to 0xFF in ReleaseFast in the debugger pub const Tag = enum(u8) { err, result }; - pub const retry: @This() = if (hasRetry) .{ .err = ErrorType.retry } else .{ .err = ErrorType{} }; - - pub const success: @This() = @This(){ + pub const retry: @This() = if (has_retry) .{ .err = ErrorType.retry } else .{ .err = .{} }; + pub const success: @This() = .{ .result = std.mem.zeroes(ReturnType), }; + /// This value is technically garbage, but that is okay as `.aborted` is + /// only meant to be returned in an operation when there is an aborted + /// `AbortSignal` object associated with the operation. + pub const aborted: @This() = .{ .err = .{ + .errno = @intFromEnum(posix.E.INTR), + .syscall = .access, + } }; pub fn assert(this: @This()) ReturnType { switch (this) { @@ -82,7 +88,7 @@ pub fn Maybe(comptime ReturnTypeT: type, comptime ErrorTypeT: type) type { } @panic(comptime "TODO: Maybe(" ++ typeBaseNameT(ReturnType) ++ ")"); } - if (hasTodo) { + if (has_todo) { return .{ .err = ErrorType.todo() }; } return .{ .err = ErrorType{} }; @@ -900,7 +906,7 @@ pub const PathLike = union(enum) { if (sliced.len > 2 and bun.path.isDriveLetter(sliced[0]) and sliced[1] == ':' and bun.path.isSepAny(sliced[2])) { // Add the long path syntax. This affects most of node:fs const rest = path_handler.PosixToWinNormalizer.resolveCWDWithExternalBufZ(@ptrCast(buf[4..]), sliced) catch @panic("Error while resolving path."); - buf[0..4].* = bun.windows.nt_maxpath_prefix_u8; + buf[0..4].* = bun.windows.long_path_prefix_u8; // When long path syntax is used, the slashes must be facing the correct direction. bun.path.dangerouslyConvertPathToWindowsInPlace(u8, buf[4..][0..rest.len]); return buf[0 .. 4 + rest.len :0]; diff --git a/src/fd.zig b/src/fd.zig index 7cfc907c67..5f9872255e 100644 --- a/src/fd.zig +++ b/src/fd.zig @@ -324,6 +324,7 @@ pub const FDImpl = packed struct { if (!value.isNumber()) { return null; } + const float = value.asNumber(); if (@mod(float, 1) != 0) { return global.throwRangeError(float, .{ .field_name = "fd", .msg = "an integer" }); diff --git a/src/string_immutable.zig b/src/string_immutable.zig index 3b045b05cd..ae53bcce9c 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -1893,7 +1893,8 @@ pub fn isWindowsAbsolutePathMissingDriveLetter(comptime T: type, chars: []const pub fn fromWPath(buf: []u8, utf16: []const u16) [:0]const u8 { bun.unsafeAssert(buf.len > 0); - const encode_into_result = copyUTF16IntoUTF8(buf[0 .. buf.len - 1], []const u16, utf16, false); + const to_copy = trimPrefixComptime(u16, utf16, bun.windows.long_path_prefix); + const encode_into_result = copyUTF16IntoUTF8(buf[0 .. buf.len - 1], []const u16, to_copy, false); bun.unsafeAssert(encode_into_result.written < buf.len); buf[encode_into_result.written] = 0; return buf[0..encode_into_result.written :0]; @@ -1929,9 +1930,9 @@ pub fn addNTPathPrefixIfNeeded(wbuf: []u16, utf16: []const u16) [:0]u16 { wbuf[utf16.len] = 0; return wbuf[0..utf16.len :0]; } - if (hasPrefixComptimeType(u16, utf16, bun.windows.nt_maxpath_prefix)) { + if (hasPrefixComptimeType(u16, utf16, bun.windows.long_path_prefix)) { // Replace prefix - return addNTPathPrefix(wbuf, utf16[bun.windows.nt_maxpath_prefix.len..]); + return addNTPathPrefix(wbuf, utf16[bun.windows.long_path_prefix.len..]); } return addNTPathPrefix(wbuf, utf16); } @@ -1941,7 +1942,7 @@ pub const toNTDir = toNTPath; pub fn toExtendedPathNormalized(wbuf: []u16, utf8: []const u8) [:0]const u16 { bun.unsafeAssert(wbuf.len > 4); - wbuf[0..4].* = bun.windows.nt_maxpath_prefix; + wbuf[0..4].* = bun.windows.long_path_prefix; return wbuf[0 .. toWPathNormalized(wbuf[4..], utf8).len + 4 :0]; } @@ -2034,11 +2035,11 @@ pub fn toKernel32Path(wbuf: []u16, utf8: []const u8) [:0]u16 { utf8[bun.windows.nt_object_prefix_u8.len..] else utf8; - if (hasPrefixComptime(path, bun.windows.nt_maxpath_prefix_u8)) { + if (hasPrefixComptime(path, bun.windows.long_path_prefix_u8)) { return toWPath(wbuf, path); } if (utf8.len > 2 and bun.path.isDriveLetter(utf8[0]) and utf8[1] == ':' and bun.path.isSepAny(utf8[2])) { - wbuf[0..4].* = bun.windows.nt_maxpath_prefix; + wbuf[0..4].* = bun.windows.long_path_prefix; const wpath = toWPath(wbuf[4..], path); return wbuf[0 .. wpath.len + 4 :0]; } diff --git a/src/sys.zig b/src/sys.zig index 672a4821f6..4fbe664bbe 100644 --- a/src/sys.zig +++ b/src/sys.zig @@ -788,13 +788,14 @@ pub fn fstatat(fd: bun.FileDescriptor, path: [:0]const u8) Maybe(bun.Stat) { .err => |err| Maybe(bun.Stat){ .err = err }, }; } - var stat_ = mem.zeroes(bun.Stat); - if (Maybe(bun.Stat).errnoSysFP(syscall.fstatat(fd.int(), path, &stat_, 0), .fstatat, fd, path)) |err| { + var stat_buf = mem.zeroes(bun.Stat); + const fd_valid = if (fd == bun.invalid_fd) std.posix.AT.FDCWD else fd.int(); + if (Maybe(bun.Stat).errnoSysFP(syscall.fstatat(fd_valid, path, &stat_buf, 0), .fstatat, fd, path)) |err| { log("fstatat({}, {s}) = {s}", .{ fd, path, @tagName(err.getErrno()) }); return err; } log("fstatat({}, {s}) = 0", .{ fd, path }); - return Maybe(bun.Stat){ .result = stat_ }; + return Maybe(bun.Stat){ .result = stat_buf }; } pub fn mkdir(file_path: [:0]const u8, flags: bun.Mode) Maybe(void) { diff --git a/src/windows.zig b/src/windows.zig index 0433aec031..bee2fd3c3b 100644 --- a/src/windows.zig +++ b/src/windows.zig @@ -70,13 +70,11 @@ pub const INVALID_FILE_ATTRIBUTES: u32 = std.math.maxInt(u32); pub const nt_object_prefix = [4]u16{ '\\', '?', '?', '\\' }; pub const nt_unc_object_prefix = [8]u16{ '\\', '?', '?', '\\', 'U', 'N', 'C', '\\' }; -// TODO: rename to long_path_prefix -pub const nt_maxpath_prefix = [4]u16{ '\\', '\\', '?', '\\' }; +pub const long_path_prefix = [4]u16{ '\\', '\\', '?', '\\' }; pub const nt_object_prefix_u8 = [4]u8{ '\\', '?', '?', '\\' }; pub const nt_unc_object_prefix_u8 = [8]u8{ '\\', '?', '?', '\\', 'U', 'N', 'C', '\\' }; -// TODO: rename to long_path_prefix_u8 -pub const nt_maxpath_prefix_u8 = [4]u8{ '\\', '\\', '?', '\\' }; +pub const long_path_prefix_u8 = [4]u8{ '\\', '\\', '?', '\\' }; const std = @import("std"); const Environment = bun.Environment; @@ -3427,7 +3425,7 @@ pub fn GetFinalPathNameByHandle( bun.sys.syslog("GetFinalPathNameByHandleW({*p}) = {}", .{ hFile, bun.fmt.utf16(ret) }); - if (bun.strings.hasPrefixComptimeType(u16, ret, nt_maxpath_prefix)) { + if (bun.strings.hasPrefixComptimeType(u16, ret, long_path_prefix)) { // '\\?\C:\absolute\path' -> 'C:\absolute\path' ret = ret[4..]; if (bun.strings.hasPrefixComptimeUTF16(ret, "UNC\\")) { diff --git a/test/js/node/test/parallel/test-fs-whatwg-url.js b/test/js/node/test/parallel/test-fs-whatwg-url.js new file mode 100644 index 0000000000..7401ed7e76 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-whatwg-url.js @@ -0,0 +1,106 @@ +'use strict'; + +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const fs = require('fs'); +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +const url = fixtures.fileURL('a.js'); + +assert(url instanceof URL); + +// Check that we can pass in a URL object successfully +fs.readFile(url, common.mustSucceed((data) => { + assert(Buffer.isBuffer(data)); +})); + +// Check that using a non file:// URL reports an error +const httpUrl = new URL('http://example.org'); + +assert.throws( + () => { + fs.readFile(httpUrl, common.mustNotCall()); + }, + { + code: 'ERR_INVALID_URL_SCHEME', + name: 'TypeError', + }); + +// pct-encoded characters in the path will be decoded and checked +if (common.isWindows) { + // Encoded back and forward slashes are not permitted on windows + ['%2f', '%2F', '%5c', '%5C'].forEach((i) => { + assert.throws( + () => { + fs.readFile(new URL(`file:///c:/tmp/${i}`), common.mustNotCall()); + }, + { + code: 'ERR_INVALID_FILE_URL_PATH', + name: 'TypeError', + } + ); + }); + assert.throws( + () => { + fs.readFile(new URL('file:///c:/tmp/%00test'), common.mustNotCall()); + }, + { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', + } + ); +} else { + // Encoded forward slashes are not permitted on other platforms + ['%2f', '%2F'].forEach((i) => { + assert.throws( + () => { + fs.readFile(new URL(`file:///c:/tmp/${i}`), common.mustNotCall()); + }, + { + code: 'ERR_INVALID_FILE_URL_PATH', + name: 'TypeError', + }); + }); + assert.throws( + () => { + fs.readFile(new URL('file://hostname/a/b/c'), common.mustNotCall()); + }, + { + code: 'ERR_INVALID_FILE_URL_HOST', + name: 'TypeError', + } + ); + assert.throws( + () => { + fs.readFile(new URL('file:///tmp/%00test'), common.mustNotCall()); + }, + { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', + } + ); +} + +// Test that strings are interpreted as paths and not as URL +// Can't use process.chdir in Workers +// Please avoid testing fs.rmdir('file:') or using it as cleanup +if (common.isMainThread && !common.isWindows) { + const oldCwd = process.cwd(); + process.chdir(tmpdir.path); + + for (let slashCount = 0; slashCount < 9; slashCount++) { + const slashes = '/'.repeat(slashCount); + + const dirname = `file:${slashes}thisDirectoryWasMadeByFailingNodeJSTestSorry/subdir`; + fs.mkdirSync(dirname, { recursive: true }); + fs.writeFileSync(`${dirname}/file`, `test failed with ${slashCount} slashes`); + + const expected = fs.readFileSync(tmpdir.resolve(dirname, 'file')); + const actual = fs.readFileSync(`${dirname}/file`); + assert.deepStrictEqual(actual, expected); + } + + process.chdir(oldCwd); +}