test-fs-symlink.js

This commit is contained in:
chloe caruso
2025-01-17 16:27:30 -08:00
parent 7a58479305
commit 14ef956d35
7 changed files with 1028 additions and 78 deletions

View File

@@ -1787,14 +1787,14 @@ pub const Arguments = struct {
/// The path to create the symbolic link at.
new_path: PathLike,
/// Windows has multiple link types. By default, only junctions can be created by non-admin.
link_type: LinkType,
link_type: if (Environment.isWindows) LinkType else void,
const LinkType = if (Environment.isWindows) enum {
const LinkType = enum {
unspecified,
file,
dir,
junction,
} else enum { unspecified };
};
pub fn deinit(this: Symlink) void {
this.target_path.deinit();
@@ -1813,11 +1813,11 @@ pub const Arguments = struct {
pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!Symlink {
const old_path = try PathLike.fromJS(ctx, arguments) orelse {
return ctx.throwInvalidArguments("oldPath must be a string or TypedArray", .{});
return ctx.throwInvalidArguments("target must be a string or TypedArray", .{});
};
const new_path = try PathLike.fromJS(ctx, arguments) orelse {
return ctx.throwInvalidArguments("newPath must be a string or TypedArray", .{});
return ctx.throwInvalidArguments("path must be a string or TypedArray", .{});
};
// The type argument is only available on Windows and
@@ -1828,10 +1828,11 @@ pub const Arguments = struct {
// Windows junction points require the destination path to
// be absolute. When using 'junction', the target argument
// will automatically be normalized to absolute path.
const link_type: LinkType = if (!Environment.isWindows)
.unspecified
else link_type: {
const link_type: LinkType = link_type: {
if (arguments.next()) |next_val| {
if (next_val.isUndefined()) {
break :link_type .unspecified;
}
if (next_val.isString()) {
arguments.eat();
var str = next_val.toBunString(ctx);
@@ -1839,9 +1840,10 @@ pub const Arguments = struct {
if (str.eqlComptime("dir")) break :link_type .dir;
if (str.eqlComptime("file")) break :link_type .file;
if (str.eqlComptime("junction")) break :link_type .junction;
return ctx.throwInvalidArguments("Symlink type must be one of \"dir\", \"file\", or \"junction\". Received \"{}\"", .{str});
return ctx.ERR_INVALID_ARG_VALUE("Symlink type must be one of \"dir\", \"file\", or \"junction\". Received \"{}\"", .{str}).throw();
}
// not a string. fallthrough to auto detect.
return ctx.ERR_INVALID_ARG_VALUE("Symlink type must be one of \"dir\", \"file\", or \"junction\".", .{}).throw();
}
break :link_type .unspecified;
};
@@ -1849,7 +1851,7 @@ pub const Arguments = struct {
return Symlink{
.target_path = old_path,
.new_path = new_path,
.link_type = link_type,
.link_type = if (Environment.isWindows) link_type,
};
}
};
@@ -2007,24 +2009,46 @@ pub const Arguments = struct {
var recursive = false;
var force = false;
var max_retries: u32 = 0;
var retry_delay: c_uint = 100;
if (arguments.next()) |val| {
arguments.eat();
if (val.isObject()) {
if (try val.getBooleanStrict(ctx, "recursive")) |boolean| {
recursive = boolean;
if (try val.get(ctx, "recursive")) |boolean| {
if (boolean.isBoolean()) {
recursive = boolean.toBoolean();
} else {
return ctx.throwInvalidArguments("The \"options.recursive\" property must be of type boolean.", .{});
}
}
if (try val.getBooleanStrict(ctx, "force")) |boolean| {
force = boolean;
if (try val.get(ctx, "force")) |boolean| {
if (boolean.isBoolean()) {
force = boolean.toBoolean();
} else {
return ctx.throwInvalidArguments("The \"options.force\" property must be of type boolean.", .{});
}
}
if (try val.get(ctx, "retryDelay")) |delay| {
retry_delay = @intCast(try JSC.Node.validators.validateInteger(ctx, delay, "options.retryDelay", .{}, 0, std.math.maxInt(c_uint)));
}
if (try val.get(ctx, "maxRetries")) |retries| {
max_retries = @intCast(try JSC.Node.validators.validateInteger(ctx, retries, "options.maxRetries", .{}, 0, std.math.maxInt(u32)));
}
} else if (val != .undefined) {
return ctx.throwInvalidArguments("The \"options\" argument must be of type object.", .{});
}
}
return RmDir{
return .{
.path = path,
.recursive = recursive,
.force = force,
.max_retries = max_retries,
.retry_delay = retry_delay,
};
}
};
@@ -2090,8 +2114,8 @@ pub const Arguments = struct {
};
const MkdirTemp = struct {
prefix: StringOrBuffer = .{ .buffer = .{ .buffer = JSC.ArrayBuffer.empty } },
encoding: Encoding = Encoding.utf8,
prefix: PathLike = .{ .buffer = .{ .buffer = JSC.ArrayBuffer.empty } },
encoding: Encoding = .utf8,
pub fn deinit(this: MkdirTemp) void {
this.prefix.deinit();
@@ -2106,11 +2130,8 @@ pub const Arguments = struct {
}
pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!MkdirTemp {
const prefix_value = arguments.next() orelse return MkdirTemp{};
const prefix = StringOrBuffer.fromJS(ctx, bun.default_allocator, prefix_value) orelse {
return ctx.throwInvalidArguments("prefix must be a string or TypedArray", .{});
};
const prefix = try PathLike.fromJS(ctx, arguments) orelse
return .{};
errdefer prefix.deinit();
arguments.eat();
@@ -2132,7 +2153,7 @@ pub const Arguments = struct {
}
}
return MkdirTemp{
return .{
.prefix = prefix,
.encoding = encoding,
};
@@ -5341,8 +5362,8 @@ pub const NodeFS = struct {
pub fn rmdir(this: *NodeFS, args: Arguments.RmDir, _: Flavor) Maybe(Return.Rmdir) {
if (args.recursive) {
std.fs.cwd().deleteTree(args.path.slice()) catch |err| {
const errno: bun.C.E = switch (err) {
zigDeleteTree(std.fs.cwd(), args.path.slice()) catch |err| {
const errno: bun.C.E = switch (@as(anyerror, err)) {
error.AccessDenied => .PERM,
error.FileTooBig => .FBIG,
error.SymLinkLoop => .LOOP,
@@ -5366,6 +5387,9 @@ pub const NodeFS = struct {
// '/', '*', '?', '"', '<', '>', '|'
error.BadPathName => .INVAL,
error.FileNotFound => .NOENT,
error.IsDir => .ISDIR,
else => .FAULT,
};
return Maybe(Return.Rm){
@@ -5388,8 +5412,8 @@ pub const NodeFS = struct {
// We cannot use removefileat() on macOS because it does not handle write-protected files as expected.
if (args.recursive) {
// TODO: switch to an implementation which does not use any "unreachable"
std.fs.cwd().deleteTree(args.path.slice()) catch |err| {
const errno: E = switch (err) {
zigDeleteTree(std.fs.cwd(), args.path.slice()) catch |err| {
const errno: E = switch (@as(anyerror, err)) {
// error.InvalidHandle => .BADF,
error.AccessDenied => .PERM,
error.FileTooBig => .FBIG,
@@ -5414,13 +5438,16 @@ pub const NodeFS = struct {
// '/', '*', '?', '"', '<', '>', '|'
error.BadPathName => .INVAL,
error.FileNotFound => .NOENT,
error.IsDir => .ISDIR,
else => .FAULT,
};
if (args.force) {
return Maybe(Return.Rm).success;
}
return Maybe(Return.Rm){
.err = bun.sys.Error.fromCode(errno, .unlink),
.err = bun.sys.Error.fromCode(errno, .rm).withPath(args.path.slice()),
};
};
return Maybe(Return.Rm).success;
@@ -5454,10 +5481,7 @@ pub const NodeFS = struct {
};
return .{
.err = bun.sys.Error.fromCode(
code,
.rmdir,
),
.err = bun.sys.Error.fromCode(code, .rm).withPath(args.path.slice()),
};
};
@@ -5484,10 +5508,7 @@ pub const NodeFS = struct {
};
return .{
.err = bun.sys.Error.fromCode(
code,
.unlink,
),
.err = bun.sys.Error.fromCode(code, .rm).withPath(args.path.slice()),
};
}
};
@@ -6336,3 +6357,370 @@ comptime {
if (!JSC.is_bindgen)
_ = Bun__mkdirp;
}
/// Copied from std.fs.Dir.deleteTree. This function returns `FileNotFound` instead of ignoring it, which
/// is required to match the behavior of Node.js's `fs.rm` { recursive: true, force: false }.
pub fn zigDeleteTree(self: std.fs.Dir, sub_path: []const u8) !void {
var initial_iterable_dir = (try zigDeleteTreeOpenInitialSubpath(self, sub_path, .file)) orelse return;
const StackItem = struct {
name: []const u8,
parent_dir: std.fs.Dir,
iter: std.fs.Dir.Iterator,
fn closeAll(items: []@This()) void {
for (items) |*item| item.iter.dir.close();
}
};
var stack_buffer: [16]StackItem = undefined;
var stack = std.ArrayListUnmanaged(StackItem).initBuffer(&stack_buffer);
defer StackItem.closeAll(stack.items);
stack.appendAssumeCapacity(.{
.name = sub_path,
.parent_dir = self,
.iter = initial_iterable_dir.iterateAssumeFirstIteration(),
});
process_stack: while (stack.items.len != 0) {
var top = &stack.items[stack.items.len - 1];
while (try top.iter.next()) |entry| {
var treat_as_dir = entry.kind == .directory;
handle_entry: while (true) {
if (treat_as_dir) {
if (stack.unusedCapacitySlice().len >= 1) {
var iterable_dir = top.iter.dir.openDir(entry.name, .{
.no_follow = true,
.iterate = true,
}) catch |err| switch (err) {
error.NotDir => {
treat_as_dir = false;
continue :handle_entry;
},
error.FileNotFound,
error.AccessDenied,
error.SymLinkLoop,
error.ProcessFdQuotaExceeded,
error.NameTooLong,
error.SystemFdQuotaExceeded,
error.NoDevice,
error.SystemResources,
error.Unexpected,
error.InvalidUtf8,
error.InvalidWtf8,
error.BadPathName,
error.NetworkNotFound,
error.DeviceBusy,
=> |e| return e,
};
stack.appendAssumeCapacity(.{
.name = entry.name,
.parent_dir = top.iter.dir,
.iter = iterable_dir.iterateAssumeFirstIteration(),
});
continue :process_stack;
} else {
try zigDeleteTreeMinStackSizeWithKindHint(top.iter.dir, entry.name, entry.kind);
break :handle_entry;
}
} else {
if (top.iter.dir.deleteFile(entry.name)) {
break :handle_entry;
} else |err| switch (err) {
error.IsDir => {
treat_as_dir = true;
continue :handle_entry;
},
error.FileNotFound,
error.NotDir,
error.AccessDenied,
error.InvalidUtf8,
error.InvalidWtf8,
error.SymLinkLoop,
error.NameTooLong,
error.SystemResources,
error.ReadOnlyFileSystem,
error.FileSystem,
error.FileBusy,
error.BadPathName,
error.NetworkNotFound,
error.Unexpected,
=> |e| return e,
}
}
}
}
// On Windows, we can't delete until the dir's handle has been closed, so
// close it before we try to delete.
top.iter.dir.close();
// In order to avoid double-closing the directory when cleaning up
// the stack in the case of an error, we save the relevant portions and
// pop the value from the stack.
const parent_dir = top.parent_dir;
const name = top.name;
stack.items.len -= 1;
var need_to_retry: bool = false;
parent_dir.deleteDir(name) catch |err| switch (err) {
error.FileNotFound => {},
error.DirNotEmpty => need_to_retry = true,
else => |e| return e,
};
if (need_to_retry) {
// Since we closed the handle that the previous iterator used, we
// need to re-open the dir and re-create the iterator.
var iterable_dir = iterable_dir: {
var treat_as_dir = true;
handle_entry: while (true) {
if (treat_as_dir) {
break :iterable_dir parent_dir.openDir(name, .{
.no_follow = true,
.iterate = true,
}) catch |err| switch (err) {
error.NotDir => {
treat_as_dir = false;
continue :handle_entry;
},
error.FileNotFound => {
// That's fine, we were trying to remove this directory anyway.
continue :process_stack;
},
error.AccessDenied,
error.SymLinkLoop,
error.ProcessFdQuotaExceeded,
error.NameTooLong,
error.SystemFdQuotaExceeded,
error.NoDevice,
error.SystemResources,
error.Unexpected,
error.InvalidUtf8,
error.InvalidWtf8,
error.BadPathName,
error.NetworkNotFound,
error.DeviceBusy,
=> |e| return e,
};
} else {
if (parent_dir.deleteFile(name)) {
continue :process_stack;
} else |err| switch (err) {
error.FileNotFound => continue :process_stack,
// Impossible because we do not pass any path separators.
error.NotDir => unreachable,
error.IsDir => {
treat_as_dir = true;
continue :handle_entry;
},
error.AccessDenied,
error.InvalidUtf8,
error.InvalidWtf8,
error.SymLinkLoop,
error.NameTooLong,
error.SystemResources,
error.ReadOnlyFileSystem,
error.FileSystem,
error.FileBusy,
error.BadPathName,
error.NetworkNotFound,
error.Unexpected,
=> |e| return e,
}
}
}
};
// We know there is room on the stack since we are just re-adding
// the StackItem that we previously popped.
stack.appendAssumeCapacity(.{
.name = name,
.parent_dir = parent_dir,
.iter = iterable_dir.iterateAssumeFirstIteration(),
});
continue :process_stack;
}
}
}
fn zigDeleteTreeOpenInitialSubpath(self: std.fs.Dir, sub_path: []const u8, kind_hint: std.fs.File.Kind) !?std.fs.Dir {
return iterable_dir: {
// Treat as a file by default
var treat_as_dir = kind_hint == .directory;
handle_entry: while (true) {
if (treat_as_dir) {
break :iterable_dir self.openDir(sub_path, .{
.no_follow = true,
.iterate = true,
}) catch |err| switch (err) {
error.NotDir => {
treat_as_dir = false;
continue :handle_entry;
},
error.FileNotFound,
error.AccessDenied,
error.SymLinkLoop,
error.ProcessFdQuotaExceeded,
error.NameTooLong,
error.SystemFdQuotaExceeded,
error.NoDevice,
error.SystemResources,
error.Unexpected,
error.InvalidUtf8,
error.InvalidWtf8,
error.BadPathName,
error.DeviceBusy,
error.NetworkNotFound,
=> |e| return e,
};
} else {
if (self.deleteFile(sub_path)) {
return null;
} else |err| switch (err) {
error.IsDir => {
treat_as_dir = true;
continue :handle_entry;
},
error.FileNotFound,
error.AccessDenied,
error.InvalidUtf8,
error.InvalidWtf8,
error.SymLinkLoop,
error.NameTooLong,
error.SystemResources,
error.ReadOnlyFileSystem,
error.NotDir,
error.FileSystem,
error.FileBusy,
error.BadPathName,
error.NetworkNotFound,
error.Unexpected,
=> |e| return e,
}
}
}
};
}
fn zigDeleteTreeMinStackSizeWithKindHint(self: std.fs.Dir, sub_path: []const u8, kind_hint: std.fs.File.Kind) !void {
start_over: while (true) {
var dir = (try zigDeleteTreeOpenInitialSubpath(self, sub_path, kind_hint)) orelse return;
var cleanup_dir_parent: ?std.fs.Dir = null;
defer if (cleanup_dir_parent) |*d| d.close();
var cleanup_dir = true;
defer if (cleanup_dir) dir.close();
// Valid use of MAX_PATH_BYTES because dir_name_buf will only
// ever store a single path component that was returned from the
// filesystem.
var dir_name_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
var dir_name: []const u8 = sub_path;
// Here we must avoid recursion, in order to provide O(1) memory guarantee of this function.
// Go through each entry and if it is not a directory, delete it. If it is a directory,
// open it, and close the original directory. Repeat. Then start the entire operation over.
scan_dir: while (true) {
var dir_it = dir.iterateAssumeFirstIteration();
dir_it: while (try dir_it.next()) |entry| {
var treat_as_dir = entry.kind == .directory;
handle_entry: while (true) {
if (treat_as_dir) {
const new_dir = dir.openDir(entry.name, .{
.no_follow = true,
.iterate = true,
}) catch |err| switch (err) {
error.NotDir => {
treat_as_dir = false;
continue :handle_entry;
},
error.FileNotFound => {
// That's fine, we were trying to remove this directory anyway.
continue :dir_it;
},
error.AccessDenied,
error.SymLinkLoop,
error.ProcessFdQuotaExceeded,
error.NameTooLong,
error.SystemFdQuotaExceeded,
error.NoDevice,
error.SystemResources,
error.Unexpected,
error.InvalidUtf8,
error.InvalidWtf8,
error.BadPathName,
error.NetworkNotFound,
error.DeviceBusy,
=> |e| return e,
};
if (cleanup_dir_parent) |*d| d.close();
cleanup_dir_parent = dir;
dir = new_dir;
const result = dir_name_buf[0..entry.name.len];
@memcpy(result, entry.name);
dir_name = result;
continue :scan_dir;
} else {
if (dir.deleteFile(entry.name)) {
continue :dir_it;
} else |err| switch (err) {
error.FileNotFound => continue :dir_it,
// Impossible because we do not pass any path separators.
error.NotDir => unreachable,
error.IsDir => {
treat_as_dir = true;
continue :handle_entry;
},
error.AccessDenied,
error.InvalidUtf8,
error.InvalidWtf8,
error.SymLinkLoop,
error.NameTooLong,
error.SystemResources,
error.ReadOnlyFileSystem,
error.FileSystem,
error.FileBusy,
error.BadPathName,
error.NetworkNotFound,
error.Unexpected,
=> |e| return e,
}
}
}
}
// Reached the end of the directory entries, which means we successfully deleted all of them.
// Now to remove the directory itself.
dir.close();
cleanup_dir = false;
if (cleanup_dir_parent) |d| {
d.deleteDir(dir_name) catch |err| switch (err) {
// These two things can happen due to file system race conditions.
error.FileNotFound, error.DirNotEmpty => continue :start_over,
else => |e| return e,
};
continue :start_over;
} else {
self.deleteDir(sub_path) catch |err| switch (err) {
error.FileNotFound => return,
error.DirNotEmpty => continue :start_over,
else => |e| return e,
};
return;
}
}
}
}

View File

@@ -1046,17 +1046,6 @@ pub const PathLike = union(enum) {
};
pub const Valid = struct {
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 < 0 or fd > std.math.maxInt(fd_t)) {
return global.throwRangeError(fd, .{
.min = 0,
.max = std.math.maxInt(fd_t),
.field_name = "fd",
});
}
}
pub fn pathSlice(zig_str: JSC.ZigString.Slice, ctx: JSC.C.JSContextRef) bun.JSError!void {
switch (zig_str.len) {
0...bun.MAX_PATH_BYTES => return,

View File

@@ -2,16 +2,16 @@ const std = @import("std");
const posix = std.posix;
const bun = @import("root").bun;
const env = bun.Environment;
const environment = bun.Environment;
const JSC = bun.JSC;
const JSValue = JSC.JSValue;
const libuv = bun.windows.libuv;
const allow_assert = env.allow_assert;
const allow_assert = environment.allow_assert;
const log = bun.sys.syslog;
fn handleToNumber(handle: FDImpl.System) FDImpl.SystemAsInt {
if (env.os == .windows) {
if (environment.os == .windows) {
// intCast fails if 'fd > 2^62'
// possible with handleToNumber(GetCurrentProcess());
return @intCast(@intFromPtr(handle));
@@ -21,7 +21,7 @@ fn handleToNumber(handle: FDImpl.System) FDImpl.SystemAsInt {
}
fn numberToHandle(handle: FDImpl.SystemAsInt) FDImpl.System {
if (env.os == .windows) {
if (environment.os == .windows) {
if (!@inComptime()) {
bun.assert(handle != FDImpl.invalid_value);
}
@@ -69,22 +69,22 @@ pub const FDImpl = packed struct {
pub const System = posix.fd_t;
pub const SystemAsInt = switch (env.os) {
pub const SystemAsInt = switch (environment.os) {
.windows => u63,
else => System,
};
pub const UV = switch (env.os) {
pub const UV = switch (environment.os) {
.windows => bun.windows.libuv.uv_file,
else => System,
};
pub const Value = if (env.os == .windows)
pub const Value = if (environment.os == .windows)
packed union { as_system: SystemAsInt, as_uv: UV }
else
packed union { as_system: SystemAsInt };
pub const Kind = if (env.os == .windows)
pub const Kind = if (environment.os == .windows)
enum(u1) { system = 0, uv = 1 }
else
enum(u0) { system };
@@ -92,7 +92,7 @@ pub const FDImpl = packed struct {
comptime {
bun.assert(@sizeOf(FDImpl) == @sizeOf(System));
if (env.os == .windows) {
if (environment.os == .windows) {
// we want the conversion from FD to fd_t to be a integer truncate
bun.assert(@as(FDImpl, @bitCast(@as(u64, 512))).value.as_system == 512);
}
@@ -106,7 +106,7 @@ pub const FDImpl = packed struct {
}
pub fn fromSystem(system_fd: System) FDImpl {
if (env.os == .windows) {
if (environment.os == .windows) {
// the current process fd is max usize
// https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getcurrentprocess
bun.assert(@intFromPtr(system_fd) <= std.math.maxInt(SystemAsInt));
@@ -116,7 +116,7 @@ pub const FDImpl = packed struct {
}
pub fn fromUV(uv_fd: UV) FDImpl {
return switch (env.os) {
return switch (environment.os) {
else => FDImpl{
.kind = .system,
.value = .{ .as_system = uv_fd },
@@ -129,7 +129,7 @@ pub const FDImpl = packed struct {
}
pub fn isValid(this: FDImpl) bool {
return switch (env.os) {
return switch (environment.os) {
// the 'zero' value on posix is debatable. it can be standard in.
// TODO(@paperdave): steamroll away every use of bun.FileDescriptor.zero
else => this.value.as_system != invalid_value,
@@ -145,7 +145,7 @@ pub const FDImpl = packed struct {
/// When calling this function, you may not be able to close the returned fd.
/// To close the fd, you have to call `.close()` on the FD.
pub fn system(this: FDImpl) System {
return switch (env.os == .windows) {
return switch (environment.os == .windows) {
false => numberToHandle(this.value.as_system),
true => switch (this.kind) {
.system => numberToHandle(this.value.as_system),
@@ -167,7 +167,7 @@ pub const FDImpl = packed struct {
/// When calling this function, you should consider the FD struct to now be invalid.
/// Calling `.close()` on the FD at that point may not work.
pub fn uv(this: FDImpl) UV {
return switch (env.os) {
return switch (environment.os) {
else => numberToHandle(this.value.as_system),
.windows => switch (this.kind) {
.system => {
@@ -200,7 +200,7 @@ pub const FDImpl = packed struct {
/// This function will prevent stdout and stderr from being closed.
pub fn close(this: FDImpl) ?bun.sys.Error {
if (env.os != .windows or this.kind == .uv) {
if (environment.os != .windows or this.kind == .uv) {
// This branch executes always on linux (uv() is no-op),
// or on Windows when given a UV file descriptor.
const fd = this.uv();
@@ -216,7 +216,7 @@ pub const FDImpl = packed struct {
/// If error, the handle has not been closed
pub fn makeLibUVOwned(this: FDImpl) !FDImpl {
this.assertValid();
return switch (env.os) {
return switch (environment.os) {
else => this,
.windows => switch (this.kind) {
.system => fd: {
@@ -234,10 +234,10 @@ pub const FDImpl = packed struct {
// Format the file descriptor for logging BEFORE closing it.
// Otherwise the file descriptor is always invalid after closing it.
var buf: if (env.isDebug) [1050]u8 else void = undefined;
const this_fmt = if (env.isDebug) std.fmt.bufPrint(&buf, "{}", .{this}) catch unreachable;
var buf: if (environment.isDebug) [1050]u8 else void = undefined;
const this_fmt = if (environment.isDebug) std.fmt.bufPrint(&buf, "{}", .{this}) catch unreachable;
const result: ?bun.sys.Error = switch (env.os) {
const result: ?bun.sys.Error = switch (environment.os) {
.linux => result: {
const fd = this.encode();
bun.assert(fd != bun.invalid_fd);
@@ -284,7 +284,7 @@ pub const FDImpl = packed struct {
else => @compileError("FD.close() not implemented for this platform"),
};
if (env.isDebug) {
if (environment.isDebug) {
if (result) |err| {
if (err.errno == @intFromEnum(posix.E.BADF)) {
bun.Output.debugWarn("close({s}) = EBADF. This is an indication of a file descriptor UAF", .{this_fmt});
@@ -307,7 +307,7 @@ pub const FDImpl = packed struct {
return null;
}
const fd: i32 = @intCast(fd64);
if (comptime env.isWindows) {
if (comptime environment.isWindows) {
return switch (bun.FDTag.get(fd)) {
.stdin => FDImpl.decode(bun.STDIN_FD),
.stdout => FDImpl.decode(bun.STDOUT_FD),
@@ -324,14 +324,15 @@ pub const FDImpl = packed struct {
if (!value.isNumber()) {
return null;
}
if (!value.isAnyInt()) {
const float = value.asNumber();
if (@mod(float, 1) != 0) {
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);
if (comptime env.isWindows) {
if (float < 0 or float > std.math.maxInt(i32)) {
return global.ERR_OUT_OF_RANGE("The value of \"fd\" is out of range. It must be >= 0 and <= {}", .{std.math.maxInt(i32)}).throw();
}
const fd: c_int = @intFromFloat(float);
if (comptime environment.isWindows) {
return switch (bun.FDTag.get(fd)) {
.stdin => FDImpl.decode(bun.STDIN_FD),
.stdout => FDImpl.decode(bun.STDOUT_FD),
@@ -339,7 +340,6 @@ pub const FDImpl = packed struct {
else => FDImpl.fromUV(fd),
};
}
return FDImpl.fromUV(fd);
}
@@ -376,11 +376,11 @@ pub const FDImpl = packed struct {
@compileError("invalid format string for FDImpl.format. must be empty like '{}'");
}
switch (env.os) {
switch (environment.os) {
else => {
const fd = this.system();
try writer.print("{d}", .{fd});
if (env.isDebug and fd >= 3) print_with_path: {
if (environment.isDebug and fd >= 3) print_with_path: {
var path_buf: bun.PathBuffer = undefined;
const path = std.os.getFdPath(fd, &path_buf) catch break :print_with_path;
try writer.print("[{s}]", .{path});
@@ -389,7 +389,7 @@ pub const FDImpl = packed struct {
.windows => {
switch (this.kind) {
.system => {
if (env.isDebug) {
if (environment.isDebug) {
const peb = std.os.windows.peb();
const handle = this.system();
if (handle == peb.ProcessParameters.hStdInput) {

View File

@@ -259,6 +259,7 @@ pub const Tag = enum(u8) {
socketpair,
setsockopt,
statx,
rm,
uv_spawn,
uv_pipe,

View File

@@ -0,0 +1,363 @@
// 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 path = require('path');
const tmpdir = require('../common/tmpdir');
tmpdir.refresh();
let dirc = 0;
function nextdir() {
return `test${++dirc}`;
}
// fs.mkdir creates directory using assigned path
{
const pathname = tmpdir.resolve(nextdir());
fs.mkdir(pathname, common.mustCall(function(err) {
assert.strictEqual(err, null);
assert.strictEqual(fs.existsSync(pathname), true);
}));
}
// fs.mkdir creates directory with assigned mode value
{
const pathname = tmpdir.resolve(nextdir());
fs.mkdir(pathname, 0o777, common.mustCall(function(err) {
assert.strictEqual(err, null);
assert.strictEqual(fs.existsSync(pathname), true);
}));
}
// fs.mkdir creates directory with mode passed as an options object
{
const pathname = tmpdir.resolve(nextdir());
fs.mkdir(pathname, common.mustNotMutateObjectDeep({ mode: 0o777 }), common.mustCall(function(err) {
assert.strictEqual(err, null);
assert.strictEqual(fs.existsSync(pathname), true);
}));
}
// fs.mkdirSync creates directory with mode passed as an options object
{
const pathname = tmpdir.resolve(nextdir());
fs.mkdirSync(pathname, common.mustNotMutateObjectDeep({ mode: 0o777 }));
assert.strictEqual(fs.existsSync(pathname), true);
}
// mkdirSync successfully creates directory from given path
{
const pathname = tmpdir.resolve(nextdir());
fs.mkdirSync(pathname);
const exists = fs.existsSync(pathname);
assert.strictEqual(exists, true);
}
// mkdirSync and mkdir require path to be a string, buffer or url.
// Anything else generates an error.
[false, 1, {}, [], null, undefined].forEach((i) => {
assert.throws(
() => fs.mkdir(i, common.mustNotCall()),
{
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError'
}
);
assert.throws(
() => fs.mkdirSync(i),
{
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError'
}
);
});
// mkdirpSync when both top-level, and sub-folders do not exist.
{
const pathname = tmpdir.resolve(nextdir(), nextdir());
fs.mkdirSync(pathname, common.mustNotMutateObjectDeep({ recursive: true }));
const exists = fs.existsSync(pathname);
assert.strictEqual(exists, true);
assert.strictEqual(fs.statSync(pathname).isDirectory(), true);
}
// mkdirpSync when folder already exists.
{
const pathname = tmpdir.resolve(nextdir(), nextdir());
fs.mkdirSync(pathname, { recursive: true });
// Should not cause an error.
fs.mkdirSync(pathname, { recursive: true });
const exists = fs.existsSync(pathname);
assert.strictEqual(exists, true);
assert.strictEqual(fs.statSync(pathname).isDirectory(), true);
}
// mkdirpSync ../
{
const pathname = `${tmpdir.path}/${nextdir()}/../${nextdir()}/${nextdir()}`;
fs.mkdirSync(pathname, { recursive: true });
const exists = fs.existsSync(pathname);
assert.strictEqual(exists, true);
assert.strictEqual(fs.statSync(pathname).isDirectory(), true);
}
// mkdirpSync when path is a file.
{
const pathname = tmpdir.resolve(nextdir(), nextdir());
fs.mkdirSync(path.dirname(pathname));
fs.writeFileSync(pathname, '', 'utf8');
assert.throws(
() => { fs.mkdirSync(pathname, common.mustNotMutateObjectDeep({ recursive: true })); },
{
code: 'EEXIST',
message: /EEXIST: .*mkdir/,
name: 'Error',
syscall: 'mkdir',
}
);
}
// mkdirpSync when part of the path is a file.
{
const filename = tmpdir.resolve(nextdir(), nextdir());
const pathname = path.join(filename, nextdir(), nextdir());
fs.mkdirSync(path.dirname(filename));
fs.writeFileSync(filename, '', 'utf8');
assert.throws(
() => { fs.mkdirSync(pathname, { recursive: true }); },
{
code: 'ENOTDIR',
message: /ENOTDIR: .*mkdir/,
name: 'Error',
syscall: 'mkdir',
path: pathname // See: https://github.com/nodejs/node/issues/28015
}
);
}
// `mkdirp` when folder does not yet exist.
{
const pathname = tmpdir.resolve(nextdir(), nextdir());
fs.mkdir(pathname, common.mustNotMutateObjectDeep({ recursive: true }), common.mustCall(function(err) {
assert.strictEqual(err, null);
assert.strictEqual(fs.existsSync(pathname), true);
assert.strictEqual(fs.statSync(pathname).isDirectory(), true);
}));
}
// `mkdirp` when path is a file.
{
const pathname = tmpdir.resolve(nextdir(), nextdir());
fs.mkdirSync(path.dirname(pathname));
fs.writeFileSync(pathname, '', 'utf8');
fs.mkdir(pathname, common.mustNotMutateObjectDeep({ recursive: true }), common.mustCall((err) => {
assert.strictEqual(err.code, 'EEXIST');
assert.strictEqual(err.syscall, 'mkdir');
assert.strictEqual(fs.statSync(pathname).isDirectory(), false);
}));
}
// `mkdirp` when part of the path is a file.
{
const filename = tmpdir.resolve(nextdir(), nextdir());
const pathname = path.join(filename, nextdir(), nextdir());
fs.mkdirSync(path.dirname(filename));
fs.writeFileSync(filename, '', 'utf8');
fs.mkdir(pathname, common.mustNotMutateObjectDeep({ recursive: true }), common.mustCall((err) => {
assert.strictEqual(err.code, 'ENOTDIR');
assert.strictEqual(err.syscall, 'mkdir');
assert.strictEqual(fs.existsSync(pathname), false);
// See: https://github.com/nodejs/node/issues/28015
// The path field varies slightly in Windows errors, vs., other platforms
// see: https://github.com/libuv/libuv/issues/2661, for this reason we
// use startsWith() rather than comparing to the full "pathname".
assert(err.path.startsWith(filename));
}));
}
// mkdirpSync dirname loop
// XXX: windows and smartos have issues removing a directory that you're in.
if (common.isMainThread && (common.isLinux || common.isMacOS)) {
const pathname = tmpdir.resolve(nextdir());
fs.mkdirSync(pathname);
process.chdir(pathname);
fs.rmdirSync(pathname);
assert.throws(
() => { fs.mkdirSync('X', common.mustNotMutateObjectDeep({ recursive: true })); },
{
code: 'ENOENT',
message: /ENOENT: .*mkdir/,
name: 'Error',
syscall: 'mkdir',
}
);
fs.mkdir('X', common.mustNotMutateObjectDeep({ recursive: true }), (err) => {
assert.strictEqual(err.code, 'ENOENT');
assert.strictEqual(err.syscall, 'mkdir');
});
}
// mkdirSync and mkdir require options.recursive to be a boolean.
// Anything else generates an error.
{
const pathname = tmpdir.resolve(nextdir());
['', 1, {}, [], null, Symbol('test'), () => {}].forEach((recursive) => {
const received = common.invalidArgTypeHelper(recursive);
assert.throws(
() => fs.mkdir(pathname, common.mustNotMutateObjectDeep({ recursive }), common.mustNotCall()),
{
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError',
// message: 'The "options.recursive" property must be of type boolean.' +
// received
}
);
assert.throws(
() => fs.mkdirSync(pathname, common.mustNotMutateObjectDeep({ recursive })),
{
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError',
// message: 'The "options.recursive" property must be of type boolean.' +
// received
}
);
});
}
// `mkdirp` returns first folder created, when all folders are new.
{
const dir1 = nextdir();
const dir2 = nextdir();
const firstPathCreated = tmpdir.resolve(dir1);
const pathname = tmpdir.resolve(dir1, dir2);
fs.mkdir(pathname, common.mustNotMutateObjectDeep({ recursive: true }), common.mustCall(function(err, result) {
assert.strictEqual(err, null);
assert.strictEqual(fs.existsSync(pathname), true);
assert.strictEqual(fs.statSync(pathname).isDirectory(), true);
assert.strictEqual(result, path.toNamespacedPath(firstPathCreated));
}));
}
// `mkdirp` returns first folder created, when last folder is new.
{
const dir1 = nextdir();
const dir2 = nextdir();
const pathname = tmpdir.resolve(dir1, dir2);
fs.mkdirSync(tmpdir.resolve(dir1));
fs.mkdir(pathname, common.mustNotMutateObjectDeep({ recursive: true }), common.mustCall(function(err, result) {
assert.strictEqual(err, null);
assert.strictEqual(fs.existsSync(pathname), true);
assert.strictEqual(fs.statSync(pathname).isDirectory(), true);
assert.strictEqual(result, path.toNamespacedPath(pathname));
}));
}
// `mkdirp` returns undefined, when no new folders are created.
{
const dir1 = nextdir();
const dir2 = nextdir();
const pathname = tmpdir.resolve(dir1, dir2);
fs.mkdirSync(tmpdir.resolve(dir1, dir2), common.mustNotMutateObjectDeep({ recursive: true }));
fs.mkdir(pathname, common.mustNotMutateObjectDeep({ recursive: true }), common.mustCall(function(err, path) {
assert.strictEqual(err, null);
assert.strictEqual(fs.existsSync(pathname), true);
assert.strictEqual(fs.statSync(pathname).isDirectory(), true);
assert.strictEqual(path, undefined);
}));
}
// `mkdirp.sync` returns first folder created, when all folders are new.
{
const dir1 = nextdir();
const dir2 = nextdir();
const firstPathCreated = tmpdir.resolve(dir1);
const pathname = tmpdir.resolve(dir1, dir2);
const p = fs.mkdirSync(pathname, common.mustNotMutateObjectDeep({ recursive: true }));
assert.strictEqual(fs.existsSync(pathname), true);
assert.strictEqual(fs.statSync(pathname).isDirectory(), true);
assert.strictEqual(p, path.toNamespacedPath(firstPathCreated));
}
// `mkdirp.sync` returns first folder created, when last folder is new.
{
const dir1 = nextdir();
const dir2 = nextdir();
const pathname = tmpdir.resolve(dir1, dir2);
fs.mkdirSync(tmpdir.resolve(dir1), common.mustNotMutateObjectDeep({ recursive: true }));
const p = fs.mkdirSync(pathname, common.mustNotMutateObjectDeep({ recursive: true }));
assert.strictEqual(fs.existsSync(pathname), true);
assert.strictEqual(fs.statSync(pathname).isDirectory(), true);
assert.strictEqual(p, path.toNamespacedPath(pathname));
}
// `mkdirp.sync` returns undefined, when no new folders are created.
{
const dir1 = nextdir();
const dir2 = nextdir();
const pathname = tmpdir.resolve(dir1, dir2);
fs.mkdirSync(tmpdir.resolve(dir1, dir2), common.mustNotMutateObjectDeep({ recursive: true }));
const p = fs.mkdirSync(pathname, common.mustNotMutateObjectDeep({ recursive: true }));
assert.strictEqual(fs.existsSync(pathname), true);
assert.strictEqual(fs.statSync(pathname).isDirectory(), true);
assert.strictEqual(p, undefined);
}
// `mkdirp.promises` returns first folder created, when all folders are new.
{
const dir1 = nextdir();
const dir2 = nextdir();
const firstPathCreated = tmpdir.resolve(dir1);
const pathname = tmpdir.resolve(dir1, dir2);
async function testCase() {
const p = await fs.promises.mkdir(pathname, common.mustNotMutateObjectDeep({ recursive: true }));
assert.strictEqual(fs.existsSync(pathname), true);
assert.strictEqual(fs.statSync(pathname).isDirectory(), true);
assert.strictEqual(p, path.toNamespacedPath(firstPathCreated));
}
testCase();
}
// Keep the event loop alive so the async mkdir() requests
// have a chance to run (since they don't ref the event loop).
process.nextTick(() => {});

View File

@@ -0,0 +1,107 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const tmpdir = require('../common/tmpdir');
tmpdir.refresh();
function handler(err, folder) {
assert.ifError(err);
assert(fs.existsSync(folder));
assert.strictEqual(this, undefined);
}
// Test with plain string
{
const tmpFolder = fs.mkdtempSync(tmpdir.resolve('foo.'));
assert.strictEqual(path.basename(tmpFolder).length, 'foo.XXXXXX'.length);
assert(fs.existsSync(tmpFolder));
const utf8 = fs.mkdtempSync(tmpdir.resolve('\u0222abc.'));
assert.strictEqual(Buffer.byteLength(path.basename(utf8)),
Buffer.byteLength('\u0222abc.XXXXXX'));
assert(fs.existsSync(utf8));
fs.mkdtemp(tmpdir.resolve('bar.'), common.mustCall(handler));
// Same test as above, but making sure that passing an options object doesn't
// affect the way the callback function is handled.
fs.mkdtemp(tmpdir.resolve('bar.'), {}, common.mustCall(handler));
// const warningMsg = 'mkdtemp() templates ending with X are not portable. ' +
// 'For details see: https://nodejs.org/api/fs.html';
// common.expectWarning('Warning', warningMsg);
fs.mkdtemp(tmpdir.resolve('bar.X'), common.mustCall(handler));
}
// Test with URL object
{
const tmpFolder = fs.mkdtempSync(tmpdir.fileURL('foo.'));
assert.strictEqual(path.basename(tmpFolder).length, 'foo.XXXXXX'.length);
assert(fs.existsSync(tmpFolder));
const utf8 = fs.mkdtempSync(tmpdir.fileURL('\u0222abc.'));
assert.strictEqual(Buffer.byteLength(path.basename(utf8)),
Buffer.byteLength('\u0222abc.XXXXXX'));
assert(fs.existsSync(utf8));
fs.mkdtemp(tmpdir.fileURL('bar.'), common.mustCall(handler));
// Same test as above, but making sure that passing an options object doesn't
// affect the way the callback function is handled.
fs.mkdtemp(tmpdir.fileURL('bar.'), {}, common.mustCall(handler));
// Warning fires only once
fs.mkdtemp(tmpdir.fileURL('bar.X'), common.mustCall(handler));
}
// Test with Buffer
{
const tmpFolder = fs.mkdtempSync(Buffer.from(tmpdir.resolve('foo.')));
assert.strictEqual(path.basename(tmpFolder).length, 'foo.XXXXXX'.length);
assert(fs.existsSync(tmpFolder));
const utf8 = fs.mkdtempSync(Buffer.from(tmpdir.resolve('\u0222abc.')));
assert.strictEqual(Buffer.byteLength(path.basename(utf8)),
Buffer.byteLength('\u0222abc.XXXXXX'));
assert(fs.existsSync(utf8));
fs.mkdtemp(Buffer.from(tmpdir.resolve('bar.')), common.mustCall(handler));
// Same test as above, but making sure that passing an options object doesn't
// affect the way the callback function is handled.
fs.mkdtemp(Buffer.from(tmpdir.resolve('bar.')), {}, common.mustCall(handler));
// Warning fires only once
fs.mkdtemp(Buffer.from(tmpdir.resolve('bar.X')), common.mustCall(handler));
}
// Test with Uint8Array
{
const encoder = new TextEncoder();
const tmpFolder = fs.mkdtempSync(encoder.encode(tmpdir.resolve('foo.')));
assert.strictEqual(path.basename(tmpFolder).length, 'foo.XXXXXX'.length);
assert(fs.existsSync(tmpFolder));
const utf8 = fs.mkdtempSync(encoder.encode(tmpdir.resolve('\u0222abc.')));
assert.strictEqual(Buffer.byteLength(path.basename(utf8)),
Buffer.byteLength('\u0222abc.XXXXXX'));
assert(fs.existsSync(utf8));
fs.mkdtemp(encoder.encode(tmpdir.resolve('bar.')), common.mustCall(handler));
// Same test as above, but making sure that passing an options object doesn't
// affect the way the callback function is handled.
fs.mkdtemp(encoder.encode(tmpdir.resolve('bar.')), {}, common.mustCall(handler));
// Warning fires only once
fs.mkdtemp(encoder.encode(tmpdir.resolve('bar.X')), common.mustCall(handler));
}

View File

@@ -0,0 +1,102 @@
// 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');
if (!common.canCreateSymLink())
common.skip('insufficient privileges');
const assert = require('assert');
const fs = require('fs');
let linkTime;
let fileTime;
const tmpdir = require('../common/tmpdir');
tmpdir.refresh();
// Test creating and reading symbolic link
const linkData = fixtures.path('/cycles/root.js');
const linkPath = tmpdir.resolve('symlink1.js');
fs.symlink(linkData, linkPath, common.mustSucceed(() => {
fs.lstat(linkPath, common.mustSucceed((stats) => {
linkTime = stats.mtime.getTime();
}));
fs.stat(linkPath, common.mustSucceed((stats) => {
fileTime = stats.mtime.getTime();
}));
fs.readlink(linkPath, common.mustSucceed((destination) => {
assert.strictEqual(destination, linkData);
}));
}));
// Test invalid symlink
{
const linkData = fixtures.path('/not/exists/file');
const linkPath = tmpdir.resolve('symlink2.js');
fs.symlink(linkData, linkPath, common.mustSucceed(() => {
assert(!fs.existsSync(linkPath));
}));
}
[false, 1, {}, [], null, undefined].forEach((input) => {
const errObj = {
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError',
message: /target|path/
};
assert.throws(() => fs.symlink(input, '', common.mustNotCall()), errObj);
assert.throws(() => fs.symlinkSync(input, ''), errObj);
assert.throws(() => fs.symlink('', input, common.mustNotCall()), errObj);
assert.throws(() => fs.symlinkSync('', input), errObj);
});
const errObj = {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
};
assert.throws(() => fs.symlink('', '', '🍏', common.mustNotCall()), errObj);
assert.throws(() => fs.symlinkSync('', '', '🍏'), errObj);
assert.throws(() => fs.symlink('', '', 'nonExistentType', common.mustNotCall()), errObj);
assert.throws(() => fs.symlinkSync('', '', 'nonExistentType'), errObj);
assert.rejects(() => fs.promises.symlink('', '', 'nonExistentType'), errObj)
.then(common.mustCall());
assert.throws(() => fs.symlink('', '', false, common.mustNotCall()), errObj);
assert.throws(() => fs.symlinkSync('', '', false), errObj);
assert.rejects(() => fs.promises.symlink('', '', false), errObj)
.then(common.mustCall());
assert.throws(() => fs.symlink('', '', {}, common.mustNotCall()), errObj);
assert.throws(() => fs.symlinkSync('', '', {}), errObj);
assert.rejects(() => fs.promises.symlink('', '', {}), errObj)
.then(common.mustCall());
process.on('exit', () => {
assert.notStrictEqual(linkTime, fileTime);
});