mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
test-fs-symlink.js
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
62
src/fd.zig
62
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) {
|
||||
|
||||
@@ -259,6 +259,7 @@ pub const Tag = enum(u8) {
|
||||
socketpair,
|
||||
setsockopt,
|
||||
statx,
|
||||
rm,
|
||||
|
||||
uv_spawn,
|
||||
uv_pipe,
|
||||
|
||||
363
test/js/node/test/parallel/test-fs-mkdir.js
Normal file
363
test/js/node/test/parallel/test-fs-mkdir.js
Normal 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(() => {});
|
||||
107
test/js/node/test/parallel/test-fs-mkdtemp.js
Normal file
107
test/js/node/test/parallel/test-fs-mkdtemp.js
Normal 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));
|
||||
}
|
||||
102
test/js/node/test/parallel/test-fs-symlink.js
Normal file
102
test/js/node/test/parallel/test-fs-symlink.js
Normal 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);
|
||||
});
|
||||
Reference in New Issue
Block a user