From 14ef956d35bcbbd89acbcdce82ae947b6bf6ea9f Mon Sep 17 00:00:00 2001 From: chloe caruso Date: Fri, 17 Jan 2025 16:27:30 -0800 Subject: [PATCH] test-fs-symlink.js --- src/bun.js/node/node_fs.zig | 460 ++++++++++++++++-- src/bun.js/node/types.zig | 11 - src/fd.zig | 62 +-- src/sys.zig | 1 + test/js/node/test/parallel/test-fs-mkdir.js | 363 ++++++++++++++ test/js/node/test/parallel/test-fs-mkdtemp.js | 107 ++++ test/js/node/test/parallel/test-fs-symlink.js | 102 ++++ 7 files changed, 1028 insertions(+), 78 deletions(-) create mode 100644 test/js/node/test/parallel/test-fs-mkdir.js create mode 100644 test/js/node/test/parallel/test-fs-mkdtemp.js create mode 100644 test/js/node/test/parallel/test-fs-symlink.js diff --git a/src/bun.js/node/node_fs.zig b/src/bun.js/node/node_fs.zig index bf19a70130..cfa0f2e164 100644 --- a/src/bun.js/node/node_fs.zig +++ b/src/bun.js/node/node_fs.zig @@ -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; + } + } + } +} diff --git a/src/bun.js/node/types.zig b/src/bun.js/node/types.zig index 3899a6797f..179b071678 100644 --- a/src/bun.js/node/types.zig +++ b/src/bun.js/node/types.zig @@ -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, diff --git a/src/fd.zig b/src/fd.zig index 24c36a965f..6249b74ece 100644 --- a/src/fd.zig +++ b/src/fd.zig @@ -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) { diff --git a/src/sys.zig b/src/sys.zig index 080d8ef7fe..9cb006c8b9 100644 --- a/src/sys.zig +++ b/src/sys.zig @@ -259,6 +259,7 @@ pub const Tag = enum(u8) { socketpair, setsockopt, statx, + rm, uv_spawn, uv_pipe, diff --git a/test/js/node/test/parallel/test-fs-mkdir.js b/test/js/node/test/parallel/test-fs-mkdir.js new file mode 100644 index 0000000000..61c4790c0a --- /dev/null +++ b/test/js/node/test/parallel/test-fs-mkdir.js @@ -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(() => {}); diff --git a/test/js/node/test/parallel/test-fs-mkdtemp.js b/test/js/node/test/parallel/test-fs-mkdtemp.js new file mode 100644 index 0000000000..60cbcf805a --- /dev/null +++ b/test/js/node/test/parallel/test-fs-mkdtemp.js @@ -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)); +} diff --git a/test/js/node/test/parallel/test-fs-symlink.js b/test/js/node/test/parallel/test-fs-symlink.js new file mode 100644 index 0000000000..de122020f0 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-symlink.js @@ -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); +});