From 1a695f13bf776c1be2241fd2538104f72a78d632 Mon Sep 17 00:00:00 2001 From: Georgijs <48869301+gvilums@users.noreply.github.com> Date: Sat, 3 Feb 2024 22:33:35 -0800 Subject: [PATCH] Async file copying on windows (#8649) * rework and simplify file opening in windows * fix tests * symlink tests pass * update error handling * remove outdated normalization check * fix mac build * apply suggested fixes * fix path tests * remove debug print * fix windows compile --- src/StandaloneModuleGraph.zig | 4 +- src/bun.js/node/dir_iterator.zig | 8 +- src/bun.js/node/node_fs.zig | 227 +++++++++++++++++---------- src/cli/run_command.zig | 2 +- src/resolver/resolve_path.zig | 138 ++++++++++++----- src/sys.zig | 258 ++++++++++++++++++------------- src/windows.zig | 6 + src/windows_c.zig | 2 +- test/js/node/fs/cp.test.ts | 9 +- 9 files changed, 424 insertions(+), 230 deletions(-) diff --git a/src/StandaloneModuleGraph.zig b/src/StandaloneModuleGraph.zig index a2fffb6c332159..75ea62add98954 100644 --- a/src/StandaloneModuleGraph.zig +++ b/src/StandaloneModuleGraph.zig @@ -271,7 +271,7 @@ pub const StandaloneModuleGraph = struct { Global.exit(1); }; - const file = bun.sys.ntCreateFile( + const file = bun.sys.openFileAtWindows( bun.invalid_fd, out, // access_mask @@ -716,7 +716,7 @@ pub const StandaloneModuleGraph = struct { var nt_path_buf: bun.WPathBuffer = undefined; const nt_path = bun.strings.addNTPathPrefix(&nt_path_buf, image_path); - return bun.sys.ntCreateFile( + return bun.sys.openFileAtWindows( bun.invalid_fd, nt_path, // access_mask diff --git a/src/bun.js/node/dir_iterator.zig b/src/bun.js/node/dir_iterator.zig index a5912b148a2fe8..cef5d72c3bfd88 100644 --- a/src/bun.js/node/dir_iterator.zig +++ b/src/bun.js/node/dir_iterator.zig @@ -284,8 +284,12 @@ pub fn NewIterator(comptime use_windows_ospath: bool) type { const kind = blk: { const attrs = dir_info.FileAttributes; - if (attrs & w.FILE_ATTRIBUTE_DIRECTORY != 0) break :blk Entry.Kind.directory; - if (attrs & w.FILE_ATTRIBUTE_REPARSE_POINT != 0) break :blk Entry.Kind.sym_link; + const isdir = attrs & w.FILE_ATTRIBUTE_DIRECTORY != 0; + const islink = attrs & w.FILE_ATTRIBUTE_REPARSE_POINT != 0; + // on windows symlinks can be directories, too. We prioritize the + // "sym_link" kind over the "directory" kind + if (islink) break :blk Entry.Kind.sym_link; + if (isdir) break :blk Entry.Kind.directory; break :blk Entry.Kind.file; }; diff --git a/src/bun.js/node/node_fs.zig b/src/bun.js/node/node_fs.zig index ac9b00aef1244d..143d13e331c584 100644 --- a/src/bun.js/node/node_fs.zig +++ b/src/bun.js/node/node_fs.zig @@ -263,11 +263,6 @@ pub const AsyncCpTask = struct { vm: *JSC.VirtualMachine, arena: bun.ArenaAllocator, ) JSC.JSValue { - if (comptime Environment.isWindows) { - globalObject.throwTODO("fs.promises.cp is not implemented on Windows yet"); - return .zero; - } - var task = bun.new( AsyncCpTask, AsyncCpTask{ @@ -695,14 +690,14 @@ pub const AsyncReaddirRecursiveTask = struct { /// When clonefile cannot be used, this task is started once per file. pub const AsyncCpSingleFileTask = struct { cp_task: *AsyncCpTask, - src: [:0]const u8, - dest: [:0]const u8, + src: bun.OSPathSliceZ, + dest: bun.OSPathSliceZ, task: JSC.WorkPoolTask = .{ .callback = &workPoolCallback }, pub fn create( parent: *AsyncCpTask, - src: [:0]const u8, - dest: [:0]const u8, + src: bun.OSPathSliceZ, + dest: bun.OSPathSliceZ, ) void { var task = bun.new(AsyncCpSingleFileTask, .{ .cp_task = parent, @@ -6029,13 +6024,9 @@ pub const NodeFS = struct { } const flags = os.O.DIRECTORY | os.O.RDONLY; - var wbuf: if (Environment.isWindows) bun.WPathBuffer else void = undefined; const fd = switch (Syscall.openatOSPath( bun.toFD((std.fs.cwd().fd)), - if (Environment.isWindows and std.fs.path.isAbsoluteWindowsWTF16(src)) - bun.strings.addNTPathPrefixIfNeeded(&wbuf, src) - else - src, + src, flags, 0, )) { @@ -6374,14 +6365,57 @@ pub const NodeFS = struct { } if (Environment.isWindows) { - const result = windows.CopyFileW(src, dest, @intFromBool(mode.shouldntOverwrite())); - if (result == bun.windows.FALSE) { - if (Maybe(Return.CopyFile).errnoSysP(result, .copyfile, this.osPathIntoSyncErrorBuf(src))) |e| { - return e; + const stat_ = reuse_stat orelse switch (windows.GetFileAttributesW(src)) { + windows.INVALID_FILE_ATTRIBUTES => return .{ .err = .{ + .errno = @intFromEnum(C.SystemErrno.ENOENT), + .syscall = .copyfile, + .path = this.osPathIntoSyncErrorBuf(src), + } }, + else => |result| result, + }; + if (stat_ & windows.FILE_ATTRIBUTE_REPARSE_POINT == 0) { + if (windows.CopyFileW(src, dest, @intFromBool(mode.shouldntOverwrite())) == 0) { + const err = windows.GetLastError(); + const errpath = switch (err) { + .FILE_EXISTS, .ALREADY_EXISTS => dest, + else => src, + }; + return Maybe(Return.CopyFile).errnoSysP(0, .copyfile, this.osPathIntoSyncErrorBuf(errpath)) orelse .{ .err = .{ + .errno = @intFromEnum(C.SystemErrno.ENOENT), + .syscall = .copyfile, + .path = this.osPathIntoSyncErrorBuf(src), + } }; + } + return ret.success; + } else { + const handle = switch (bun.sys.openatWindows(bun.invalid_fd, src, os.O.RDONLY)) { + .err => |err| return .{ .err = err }, + .result => |src_fd| src_fd, + }; + var wbuf: bun.WPathBuffer = undefined; + const len = bun.windows.GetFinalPathNameByHandleW(handle.cast(), &wbuf, wbuf.len, 0); + if (len == 0) { + return Maybe(Return.CopyFile).errnoSysP(0, .copyfile, this.osPathIntoSyncErrorBuf(dest)) orelse .{ .err = .{ + .errno = @intFromEnum(C.SystemErrno.ENOENT), + .syscall = .copyfile, + .path = this.osPathIntoSyncErrorBuf(dest), + } }; } + const flags = if (stat_ & windows.FILE_ATTRIBUTE_DIRECTORY != 0) + std.os.windows.SYMBOLIC_LINK_FLAG_DIRECTORY + else + 0; + if (windows.CreateSymbolicLinkW(dest, wbuf[0..len :0], flags) == 0) { + return Maybe(Return.CopyFile).errnoSysP(0, .copyfile, this.osPathIntoSyncErrorBuf(dest)) orelse .{ + .err = .{ + .errno = @intFromEnum(C.SystemErrno.ENOENT), + .syscall = .copyfile, + .path = this.osPathIntoSyncErrorBuf(dest), + }, + }; + } + return ret.success; } - - return ret.success; } return ret.todo(); @@ -6390,48 +6424,68 @@ pub const NodeFS = struct { /// Directory scanning + clonefile will block this thread, then each individual file copy (what the sync version /// calls "_copySingleFileSync") will be dispatched as a separate task. pub fn cpAsync(this: *NodeFS, task: *AsyncCpTask) void { - if (comptime Environment.isWindows) { - task.finishConcurrently(Maybe(Return.Cp).todo()); - return; - } - const args = task.args; - var src_buf: [bun.MAX_PATH_BYTES]u8 = undefined; - var dest_buf: [bun.MAX_PATH_BYTES]u8 = undefined; - const src = args.src.sliceZ(&src_buf); - const dest = args.dest.sliceZ(&dest_buf); + var src_buf: bun.OSPathBuffer = undefined; + var dest_buf: bun.OSPathBuffer = undefined; + const src = args.src.osPath(@ptrCast(&src_buf)); + const dest = args.dest.osPath(@ptrCast(&dest_buf)); - const stat_ = switch (Syscall.lstat(src)) { - .result => |result| result, - .err => |err| { - @memcpy(this.sync_error_buf[0..src.len], src); - task.finishConcurrently(.{ .err = err.withPath(this.sync_error_buf[0..src.len]) }); + if (Environment.isWindows) { + const attributes = windows.GetFileAttributesW(src); + if (attributes == windows.INVALID_FILE_ATTRIBUTES) { + task.finishConcurrently(.{ .err = .{ + .errno = @intFromEnum(C.SystemErrno.ENOENT), + .syscall = .copyfile, + .path = this.osPathIntoSyncErrorBuf(src), + } }); return; - }, - }; + } + const file_or_symlink = (attributes & windows.FILE_ATTRIBUTE_DIRECTORY) == 0 or (attributes & windows.FILE_ATTRIBUTE_REPARSE_POINT) != 0; + if (file_or_symlink) { + const r = this._copySingleFileSync( + src, + dest, + @enumFromInt((if (args.flags.errorOnExist or !args.flags.force) Constants.COPYFILE_EXCL else @as(u8, 0))), + attributes, + ); + if (r == .err and r.err.errno == @intFromEnum(E.EXIST) and !args.flags.errorOnExist) { + task.finishConcurrently(Maybe(Return.Cp).success); + return; + } + task.finishConcurrently(r); + return; + } + } else { + const stat_ = switch (Syscall.lstat(src)) { + .result => |result| result, + .err => |err| { + @memcpy(this.sync_error_buf[0..src.len], src); + task.finishConcurrently(.{ .err = err.withPath(this.sync_error_buf[0..src.len]) }); + return; + }, + }; - if (!os.S.ISDIR(stat_.mode)) { - // This is the only file, there is no point in dispatching subtasks - const r = this._copySingleFileSync( - src, - dest, - @enumFromInt((if (args.flags.errorOnExist or !args.flags.force) Constants.COPYFILE_EXCL else @as(u8, 0))), - stat_, - ); - if (r == .err and r.err.errno == @intFromEnum(E.EXIST) and !args.flags.errorOnExist) { - task.finishConcurrently(Maybe(Return.Cp).success); + if (!os.S.ISDIR(stat_.mode)) { + // This is the only file, there is no point in dispatching subtasks + const r = this._copySingleFileSync( + src, + dest, + @enumFromInt((if (args.flags.errorOnExist or !args.flags.force) Constants.COPYFILE_EXCL else @as(u8, 0))), + stat_, + ); + if (r == .err and r.err.errno == @intFromEnum(E.EXIST) and !args.flags.errorOnExist) { + task.finishConcurrently(Maybe(Return.Cp).success); + return; + } + task.finishConcurrently(r); return; } - task.finishConcurrently(r); - return; } - if (!args.flags.recursive) { - @memcpy(this.sync_error_buf[0..src.len], src); task.finishConcurrently(.{ .err = .{ .errno = @intFromEnum(E.ISDIR), .syscall = .copyfile, - .path = this.sync_error_buf[0..src.len], + .path = this.osPathIntoSyncErrorBuf(src), } }); return; } @@ -6448,9 +6502,9 @@ pub const NodeFS = struct { this: *NodeFS, args: Arguments.Cp.Flags, task: *AsyncCpTask, - src_buf: *[bun.MAX_PATH_BYTES]u8, + src_buf: *bun.OSPathBuffer, src_dir_len: PathString.PathInt, - dest_buf: *[bun.MAX_PATH_BYTES]u8, + dest_buf: *bun.OSPathBuffer, dest_dir_len: PathString.PathInt, ) bool { const src = src_buf[0..src_dir_len :0]; @@ -6479,20 +6533,30 @@ pub const NodeFS = struct { } const open_flags = os.O.DIRECTORY | os.O.RDONLY; - const fd = switch (Syscall.open(src, open_flags, 0)) { + const fd = switch (Syscall.openatOSPath(bun.invalid_fd, src, open_flags, 0)) { .err => |err| { - @memcpy(this.sync_error_buf[0..src.len], src); - task.finishConcurrently(.{ .err = err.withPath(this.sync_error_buf[0..src.len]) }); + task.finishConcurrently(.{ .err = err.withPath(this.osPathIntoSyncErrorBuf(src)) }); return false; }, .result => |fd_| fd_, }; defer _ = Syscall.close(fd); - const mkdir_ = this.mkdirRecursive(.{ - .path = PathLike{ .string = PathString.init(dest) }, - .recursive = true, - }, .sync); + var buf: bun.OSPathBuffer = undefined; + + // const normdest = bun.path.normalizeStringGenericTZ(bun.OSPathChar, dest, &buf, true, std.fs.path.sep, bun.path.isSepAnyT, false, true); + const normdest: bun.OSPathSliceZ = if (Environment.isWindows) + switch (bun.sys.normalizePathWindows(u16, bun.invalid_fd, dest, &buf)) { + .err => |err| { + task.finishConcurrently(.{ .err = err }); + return false; + }, + .result => |normdest| normdest, + } + else + dest; + + const mkdir_ = this.mkdirRecursiveOSPath(normdest, Arguments.Mkdir.DefaultMode, false); switch (mkdir_) { .err => |err| { task.finishConcurrently(.{ .err = err }); @@ -6502,59 +6566,62 @@ pub const NodeFS = struct { } const dir = fd.asDir(); - var iterator = DirIterator.iterate(dir, .u8); + var iterator = DirIterator.iterate(dir, if (Environment.isWindows) .u16 else .u8); var entry = iterator.next(); while (switch (entry) { .err => |err| { - @memcpy(this.sync_error_buf[0..src.len], src); - task.finishConcurrently(.{ .err = err.withPath(this.sync_error_buf[0..src.len]) }); + // @memcpy(this.sync_error_buf[0..src.len], src); + task.finishConcurrently(.{ .err = err.withPath(this.osPathIntoSyncErrorBuf(src)) }); return false; }, .result => |ent| ent, }) |current| : (entry = iterator.next()) { switch (current.kind) { .directory => { - @memcpy(src_buf[src_dir_len + 1 .. src_dir_len + 1 + current.name.len], current.name.slice()); + const cname = current.name.slice(); + @memcpy(src_buf[src_dir_len + 1 .. src_dir_len + 1 + cname.len], cname); src_buf[src_dir_len] = std.fs.path.sep; - src_buf[src_dir_len + 1 + current.name.len] = 0; + src_buf[src_dir_len + 1 + cname.len] = 0; - @memcpy(dest_buf[dest_dir_len + 1 .. dest_dir_len + 1 + current.name.len], current.name.slice()); + @memcpy(dest_buf[dest_dir_len + 1 .. dest_dir_len + 1 + cname.len], cname); dest_buf[dest_dir_len] = std.fs.path.sep; - dest_buf[dest_dir_len + 1 + current.name.len] = 0; + dest_buf[dest_dir_len + 1 + cname.len] = 0; const should_continue = this._cpAsyncDirectory( args, task, src_buf, - src_dir_len + 1 + current.name.len, + @truncate(src_dir_len + 1 + cname.len), dest_buf, - dest_dir_len + 1 + current.name.len, + @truncate(dest_dir_len + 1 + cname.len), ); if (!should_continue) return false; }, else => { _ = task.subtask_count.fetchAdd(1, .Monotonic); + const cname = current.name.slice(); + // Allocate a path buffer for the path data var path_buf = bun.default_allocator.alloc( - u8, - src_dir_len + 1 + current.name.len + 1 + dest_dir_len + 1 + current.name.len + 1, - ) catch @panic("Out of memory"); + bun.OSPathChar, + src_dir_len + 1 + cname.len + 1 + dest_dir_len + 1 + cname.len + 1, + ) catch bun.outOfMemory(); @memcpy(path_buf[0..src_dir_len], src_buf[0..src_dir_len]); path_buf[src_dir_len] = std.fs.path.sep; - @memcpy(path_buf[src_dir_len + 1 .. src_dir_len + 1 + current.name.len], current.name.slice()); - path_buf[src_dir_len + 1 + current.name.len] = 0; + @memcpy(path_buf[src_dir_len + 1 .. src_dir_len + 1 + cname.len], cname); + path_buf[src_dir_len + 1 + cname.len] = 0; - @memcpy(path_buf[src_dir_len + 1 + current.name.len + 1 .. src_dir_len + 1 + current.name.len + 1 + dest_dir_len], dest_buf[0..dest_dir_len]); - path_buf[src_dir_len + 1 + current.name.len + 1 + dest_dir_len] = std.fs.path.sep; - @memcpy(path_buf[src_dir_len + 1 + current.name.len + 1 + dest_dir_len + 1 .. src_dir_len + 1 + current.name.len + 1 + dest_dir_len + 1 + current.name.len], current.name.slice()); - path_buf[src_dir_len + 1 + current.name.len + 1 + dest_dir_len + 1 + current.name.len] = 0; + @memcpy(path_buf[src_dir_len + 1 + cname.len + 1 .. src_dir_len + 1 + cname.len + 1 + dest_dir_len], dest_buf[0..dest_dir_len]); + path_buf[src_dir_len + 1 + cname.len + 1 + dest_dir_len] = std.fs.path.sep; + @memcpy(path_buf[src_dir_len + 1 + cname.len + 1 + dest_dir_len + 1 .. src_dir_len + 1 + cname.len + 1 + dest_dir_len + 1 + cname.len], cname); + path_buf[src_dir_len + 1 + cname.len + 1 + dest_dir_len + 1 + cname.len] = 0; AsyncCpSingleFileTask.create( task, - path_buf[0 .. src_dir_len + 1 + current.name.len :0], - path_buf[src_dir_len + 1 + current.name.len + 1 .. src_dir_len + 1 + current.name.len + 1 + dest_dir_len + 1 + current.name.len :0], + path_buf[0 .. src_dir_len + 1 + cname.len :0], + path_buf[src_dir_len + 1 + cname.len + 1 .. src_dir_len + 1 + cname.len + 1 + dest_dir_len + 1 + cname.len :0], ); }, } diff --git a/src/cli/run_command.zig b/src/cli/run_command.zig index 963500fcb117c3..956a99dad9c663 100644 --- a/src/cli/run_command.zig +++ b/src/cli/run_command.zig @@ -1377,7 +1377,7 @@ pub const RunCommand = struct { if (Environment.allow_assert) { std.debug.assert(std.fs.path.isAbsoluteWindowsWTF16(path_to_use)); } - const handle = (bun.sys.ntCreateFile( + const handle = (bun.sys.openFileAtWindows( bun.invalid_fd, // absolute path is given path_to_use, w.STANDARD_RIGHTS_READ | w.FILE_READ_DATA | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA | w.SYNCHRONIZE, diff --git a/src/resolver/resolve_path.zig b/src/resolver/resolve_path.zig index 2f52c1195f2335..7b62d4a681f422 100644 --- a/src/resolver/resolve_path.zig +++ b/src/resolver/resolve_path.zig @@ -635,6 +635,15 @@ pub fn normalizeStringGeneric( ) []u8 { return normalizeStringGenericT(u8, path_, buf, allow_above_root, separator, isSeparator, preserve_trailing_slash); } + +fn separatorAdapter(comptime T: type, func: anytype) fn (T) bool { + return struct { + fn call(char: T) bool { + return func(T, char); + } + }.call; +} + pub fn normalizeStringGenericT( comptime T: type, path_: []const T, @@ -644,7 +653,41 @@ pub fn normalizeStringGenericT( comptime isSeparatorT: anytype, comptime preserve_trailing_slash: bool, ) []T { - const isWindows, const sep_str = comptime .{ separator == std.fs.path.sep_windows, &[_]u8{separator} }; + return normalizeStringGenericTZ(T, path_, buf, .{ + .allow_above_root = allow_above_root, + .separator = separator, + .isSeparator = separatorAdapter(T, isSeparatorT), + .preserve_trailing_slash = preserve_trailing_slash, + .zero_terminate = false, + .add_nt_prefix = false, + }); +} + +pub fn NormalizeOptions(comptime T: type) type { + return struct { + allow_above_root: bool = false, + separator: T = std.fs.path.sep, + isSeparator: fn (T) bool = struct { + fn call(char: T) bool { + return if (comptime std.fs.path.sep == std.fs.path.sep_windows) + char == '\\' or char == '/' + else + char == '/'; + } + }.call, + preserve_trailing_slash: bool = false, + zero_terminate: bool = false, + add_nt_prefix: bool = false, + }; +} + +pub fn normalizeStringGenericTZ( + comptime T: type, + path_: []const T, + buf: []T, + comptime options: NormalizeOptions(T), +) if (options.zero_terminate) [:0]T else []T { + const isWindows, const sep_str = comptime .{ options.separator == std.fs.path.sep_windows, &[_]u8{options.separator} }; if (isWindows and bun.Environment.isDebug) { // this is here to catch a potential mistake by the caller @@ -656,64 +699,83 @@ pub fn normalizeStringGenericT( var buf_i: usize = 0; var dotdot: usize = 0; + var path_begin: usize = 0; - const volLen, const indexOfThirdUNCSlash = if (isWindows and !allow_above_root) + const volLen, const indexOfThirdUNCSlash = if (isWindows and !options.allow_above_root) windowsVolumeNameLenT(T, path_) else .{ 0, 0 }; - if (isWindows and !allow_above_root) { + if (isWindows and !options.allow_above_root) { if (volLen > 0) { + if (options.add_nt_prefix) { + @memcpy(buf[buf_i .. buf_i + 4], &comptime strings.literalBuf(T, "\\??\\")); + buf_i += 4; + } if (path_[1] != ':') { // UNC paths - buf[0..2].* = comptime strings.literalBuf(T, sep_str ++ sep_str); - @memcpy(buf[2 .. indexOfThirdUNCSlash + 1], path_[2 .. indexOfThirdUNCSlash + 1]); - buf[indexOfThirdUNCSlash] = separator; + @memcpy(buf[buf_i .. buf_i + 2], &comptime strings.literalBuf(T, sep_str ++ sep_str)); + @memcpy(buf[buf_i + 2 .. buf_i + indexOfThirdUNCSlash + 1], path_[2 .. indexOfThirdUNCSlash + 1]); + buf[buf_i + indexOfThirdUNCSlash] = options.separator; @memcpy( - buf[indexOfThirdUNCSlash + 1 .. volLen], + buf[buf_i + indexOfThirdUNCSlash + 1 .. buf_i + volLen], path_[indexOfThirdUNCSlash + 1 .. volLen], ); - buf[volLen] = separator; - buf_i = volLen + 1; + buf[buf_i + volLen] = options.separator; + buf_i += volLen + 1; + path_begin = volLen + 1; // it is just a volume name - if (buf_i >= path_.len) - return buf[0..buf_i]; + if (path_begin >= path_.len) { + if (options.zero_terminate) { + buf[buf_i] = 0; + return buf[0..buf_i :0]; + } else { + return buf[0..buf_i]; + } + } } else { // drive letter - buf[0] = path_[0]; - buf[1] = ':'; - buf_i = 2; + buf[buf_i] = path_[0]; + buf[buf_i + 1] = ':'; + buf_i += 2; dotdot = buf_i; + path_begin = 2; } - } else if (path_.len > 0 and isSeparatorT(T, path_[0])) { - buf[buf_i] = separator; + } else if (path_.len > 0 and options.isSeparator(path_[0])) { + buf[buf_i] = options.separator; buf_i += 1; dotdot = 1; + path_begin = 1; } } - if (isWindows and allow_above_root) { + if (isWindows and options.allow_above_root) { if (path_.len >= 2 and path_[1] == ':') { - buf[0] = path_[0]; - buf[1] = ':'; - buf_i = 2; + if (options.add_nt_prefix) { + @memcpy(buf[buf_i .. buf_i + 4], &comptime strings.literalBuf(T, "\\??\\")); + buf_i += 4; + } + buf[buf_i] = path_[0]; + buf[buf_i + 1] = ':'; + buf_i += 2; dotdot = buf_i; + path_begin = 2; } } var r: usize = 0; var path, const buf_start = if (isWindows) - .{ path_[buf_i..], buf_i } + .{ path_[path_begin..], buf_i } else .{ path_, 0 }; const n = path.len; - if (isWindows and (allow_above_root or volLen > 0)) { + if (isWindows and (options.allow_above_root or volLen > 0)) { // consume leading slashes on windows - if (r < n and isSeparatorT(T, path[r])) { + if (r < n and options.isSeparator(path[r])) { r += 1; - buf[buf_i] = separator; + buf[buf_i] = options.separator; buf_i += 1; } } @@ -722,26 +784,26 @@ pub fn normalizeStringGenericT( // empty path element // or // . element - if (isSeparatorT(T, path[r])) { + if (options.isSeparator(path[r])) { r += 1; continue; } - if (path[r] == '.' and (r + 1 == n or isSeparatorT(T, path[r + 1]))) { + if (path[r] == '.' and (r + 1 == n or options.isSeparator(path[r + 1]))) { // skipping two is a windows-specific bugfix r += 1; continue; } - if (@"is .. with type"(T, path[r..]) and (r + 2 == n or isSeparatorT(T, path[r + 2]))) { + if (@"is .. with type"(T, path[r..]) and (r + 2 == n or options.isSeparator(path[r + 2]))) { r += 2; // .. element: remove to last separator if (buf_i > dotdot) { buf_i -= 1; - while (buf_i > dotdot and !isSeparatorT(T, buf[buf_i])) { + while (buf_i > dotdot and !options.isSeparator(buf[buf_i])) { buf_i -= 1; } - } else if (allow_above_root) { + } else if (options.allow_above_root) { if (buf_i > buf_start) { buf[buf_i..][0..3].* = comptime strings.literalBuf(T, sep_str ++ ".."); buf_i += 3; @@ -757,22 +819,22 @@ pub fn normalizeStringGenericT( // real path element. // add slash if needed - if (buf_i != buf_start and !isSeparatorT(T, buf[buf_i - 1])) { - buf[buf_i] = separator; + if (buf_i != buf_start and !options.isSeparator(buf[buf_i - 1])) { + buf[buf_i] = options.separator; buf_i += 1; } const from = r; - while (r < n and !isSeparatorT(T, path[r])) : (r += 1) {} + while (r < n and !options.isSeparator(path[r])) : (r += 1) {} const count = r - from; @memcpy(buf[buf_i..][0..count], path[from..][0..count]); buf_i += count; } - if (preserve_trailing_slash) { + if (options.preserve_trailing_slash) { // Was there a trailing slash? Let's keep it. - if (buf_i > 0 and path_[path_.len - 1] == separator and buf[buf_i - 1] != separator) { - buf[buf_i] = separator; + if (buf_i > 0 and path_[path_.len - 1] == options.separator and buf[buf_i - 1] != options.separator) { + buf[buf_i] = options.separator; buf_i += 1; } } @@ -784,7 +846,11 @@ pub fn normalizeStringGenericT( buf_i += 1; } - const result = buf[0..buf_i]; + if (options.zero_terminate) { + buf[buf_i] = 0; + } + + const result = if (options.zero_terminate) buf[0..buf_i :0] else buf[0..buf_i]; if (bun.Environment.allow_assert and isWindows) { std.debug.assert(!strings.hasPrefixComptimeType(T, result, comptime strings.literal(T, "\\:\\"))); diff --git a/src/sys.zig b/src/sys.zig index 5a436707ea57db..fc0bd3be0611b3 100644 --- a/src/sys.zig +++ b/src/sys.zig @@ -357,44 +357,22 @@ pub fn getErrno(rc: anytype) bun.C.E { const O = std.os.O; const w = std.os.windows; -fn normalizePathWindows( +pub fn normalizePathWindows( + comptime T: type, dir_fd: bun.FileDescriptor, - path: []const u8, + path_: []const T, buf: *bun.WPathBuffer, ) Maybe([:0]const u16) { - const slash = bun.strings.charIsAnySlash; - const ok = brk: { - var slash_or_start = true; - for (0..path.len) |i| { - // if we're starting with a dot or just saw a slash and now a dot - if (slash_or_start and path[i] == '.') { - // just '.' or ending on '/.' - if (i + 1 == path.len) break :brk false; - // starting with './' or containing '/./' - if (slash(path[i + 1])) break :brk false; - if (path.len > i + 2) { - // starting with '../'' or containing '/../' - if (path[i + 1] == '.' and slash(path[i + 2])) break :brk false; - } - } - // two slashes in a row - if (slash_or_start and slash(path[i])) break :brk false; - slash_or_start = slash(path[i]); - } - break :brk true; - }; - if (ok) { - // no need to normalize, proceed normally - return .{ - .result = bun.strings.toNTPath(buf, path), - }; + if (comptime T != u8 and T != u16) { + @compileError("normalizePathWindows only supports u8 and u16 character types"); } - var buf1: bun.PathBuffer = undefined; - var buf2: bun.PathBuffer = undefined; - if (std.fs.path.isAbsoluteWindows(path)) { - const norm = bun.path.normalizeStringWindows(path, &buf1, false, false); + var wbuf: if (T == u16) void else bun.WPathBuffer = undefined; + const path = if (T == u16) path_ else bun.strings.convertUTF8toUTF16InBuffer(&wbuf, path_); + + if (std.fs.path.isAbsoluteWindowsWTF16(path)) { + const norm = bun.path.normalizeStringGenericTZ(u16, path, buf, .{ .add_nt_prefix = true, .zero_terminate = true }); return .{ - .result = bun.strings.toNTPath(buf, norm), + .result = norm, }; } @@ -409,14 +387,18 @@ fn normalizePathWindows( .syscall = .open, } }; }; - const base_count = bun.simdutf.convert.utf16.to.utf8.le(base_path, &buf1); - const norm = bun.path.joinAbsStringBuf(buf1[0..base_count], &buf2, &[_][]const u8{path}, .windows); + + var buf1: bun.WPathBuffer = undefined; + @memcpy(buf1[0..base_path.len], base_path); + buf1[base_path.len] = '\\'; + @memcpy(buf1[base_path.len + 1 .. base_path.len + 1 + path.len], path); + const norm = bun.path.normalizeStringGenericTZ(u16, buf1[0 .. base_path.len + 1 + path.len], buf, .{ .add_nt_prefix = true, .zero_terminate = true }); return .{ - .result = bun.strings.toNTPath(buf, norm), + .result = norm, }; } -pub fn openDirAtWindows( +pub fn openDirAtWindowsNtPath( dirFd: bun.FileDescriptor, path: []const u16, iterable: bool, @@ -493,58 +475,39 @@ pub fn openDirAtWindows( } } -pub noinline fn openDirAtWindowsA( +pub fn openDirAtWindowsT( + comptime T: type, dirFd: bun.FileDescriptor, - path: []const u8, + path: []const T, iterable: bool, no_follow: bool, ) Maybe(bun.FileDescriptor) { var wbuf: bun.WPathBuffer = undefined; - const norm = switch (normalizePathWindows(dirFd, path, &wbuf)) { + const norm = switch (normalizePathWindows(T, dirFd, path, &wbuf)) { .err => |err| return .{ .err = err }, .result => |norm| norm, }; - return openDirAtWindows(dirFd, norm, iterable, no_follow); + return openDirAtWindowsNtPath(dirFd, norm, iterable, no_follow); } -pub fn openatWindows(dir: bun.FileDescriptor, path: []const u16, flags: bun.Mode) Maybe(bun.FileDescriptor) { - const nonblock = flags & O.NONBLOCK != 0; - const overwrite = flags & O.WRONLY != 0 and flags & O.APPEND == 0; - - var access_mask: w.ULONG = w.READ_CONTROL | w.FILE_WRITE_ATTRIBUTES | w.SYNCHRONIZE; - if (flags & O.RDWR != 0) { - access_mask |= w.GENERIC_READ | w.GENERIC_WRITE; - } else if (flags & O.APPEND != 0) { - access_mask |= w.GENERIC_WRITE | w.FILE_APPEND_DATA; - } else if (flags & O.WRONLY != 0) { - access_mask |= w.GENERIC_WRITE; - } else { - access_mask |= w.GENERIC_READ; - } - - const creation: w.ULONG = blk: { - if (flags & O.CREAT != 0) { - if (flags & O.EXCL != 0) { - break :blk w.FILE_CREATE; - } - break :blk if (overwrite) w.FILE_OVERWRITE_IF else w.FILE_OPEN_IF; - } - break :blk if (overwrite) w.FILE_OVERWRITE else w.FILE_OPEN; - }; - - const blocking_flag: windows.ULONG = if (!nonblock) windows.FILE_SYNCHRONOUS_IO_NONALERT else 0; - const file_or_dir_flag: windows.ULONG = switch (flags & O.DIRECTORY != 0) { - // .file_only => windows.FILE_NON_DIRECTORY_FILE, - true => windows.FILE_DIRECTORY_FILE, - false => 0, - }; - const follow_symlinks = flags & O.NOFOLLOW == 0; - - const options: windows.ULONG = if (follow_symlinks) file_or_dir_flag | blocking_flag else file_or_dir_flag | windows.FILE_OPEN_REPARSE_POINT; +pub fn openDirAtWindows( + dirFd: bun.FileDescriptor, + path: []const u16, + iterable: bool, + no_follow: bool, +) Maybe(bun.FileDescriptor) { + return openDirAtWindowsT(u16, dirFd, path, iterable, no_follow); +} - return ntCreateFile(dir, path, access_mask, creation, options); +pub noinline fn openDirAtWindowsA( + dirFd: bun.FileDescriptor, + path: []const u8, + iterable: bool, + no_follow: bool, +) Maybe(bun.FileDescriptor) { + return openDirAtWindowsT(u8, dirFd, path, iterable, no_follow); } /// For this function to open an absolute path, it must start with "\??\". Otherwise @@ -562,9 +525,9 @@ pub fn openatWindows(dir: bun.FileDescriptor, path: []const u16, flags: bun.Mode /// /// In the zig standard library, messing up the input to their equivalent /// will trigger `unreachable`. Here there will be a debug log with the path. -pub fn ntCreateFile( +pub fn openFileAtWindowsNtPath( dir: bun.FileDescriptor, - path_maybe_leading_dot: []const u16, + path: []const u16, access_mask: w.ULONG, disposition: w.ULONG, options: w.ULONG, @@ -573,8 +536,8 @@ pub fn ntCreateFile( // Another problem re: normalization is that you can use relative paths, but no leading '.\' or './'' // this path is probably already backslash normalized so we're only going to check for '.\' - const path = if (bun.strings.hasPrefixComptimeUTF16(path_maybe_leading_dot, ".\\")) path_maybe_leading_dot[2..] else path_maybe_leading_dot; - std.debug.assert(!bun.strings.hasPrefixComptimeUTF16(path_maybe_leading_dot, "./")); + // const path = if (bun.strings.hasPrefixComptimeUTF16(path_maybe_leading_dot, ".\\")) path_maybe_leading_dot[2..] else path_maybe_leading_dot; + // std.debug.assert(!bun.strings.hasPrefixComptimeUTF16(path_maybe_leading_dot, "./")); assertIsValidWindowsPath(u16, path); const path_len_bytes = std.math.cast(u16, path.len * 2) orelse return .{ @@ -680,6 +643,103 @@ pub fn ntCreateFile( } } +pub fn openFileAtWindowsT( + comptime T: type, + dirFd: bun.FileDescriptor, + path: []const T, + access_mask: w.ULONG, + disposition: w.ULONG, + options: w.ULONG, +) Maybe(bun.FileDescriptor) { + var wbuf: bun.WPathBuffer = undefined; + + const norm = switch (normalizePathWindows(T, dirFd, path, &wbuf)) { + .err => |err| return .{ .err = err }, + .result => |norm| norm, + }; + + return openFileAtWindowsNtPath(dirFd, norm, access_mask, disposition, options); +} + +pub fn openFileAtWindows( + dirFd: bun.FileDescriptor, + path: []const u16, + access_mask: w.ULONG, + disposition: w.ULONG, + options: w.ULONG, +) Maybe(bun.FileDescriptor) { + return openFileAtWindowsT(u16, dirFd, path, access_mask, disposition, options); +} + +pub noinline fn openFileAtWindowsA( + dirFd: bun.FileDescriptor, + path: []const u8, + access_mask: w.ULONG, + disposition: w.ULONG, + options: w.ULONG, +) Maybe(bun.FileDescriptor) { + return openFileAtWindowsT(u8, dirFd, path, access_mask, disposition, options); +} + +pub fn openatWindowsT(comptime T: type, dir: bun.FileDescriptor, path: []const T, flags: bun.Mode) Maybe(bun.FileDescriptor) { + if (flags & O.DIRECTORY != 0) { + // we interpret O_PATH as meaning that we don't want iteration + return openDirAtWindowsT(T, dir, path, flags & O.PATH == 0, flags & O.NOFOLLOW != 0); + } + + const nonblock = flags & O.NONBLOCK != 0; + const overwrite = flags & O.WRONLY != 0 and flags & O.APPEND == 0; + + var access_mask: w.ULONG = w.READ_CONTROL | w.FILE_WRITE_ATTRIBUTES | w.SYNCHRONIZE; + if (flags & O.RDWR != 0) { + access_mask |= w.GENERIC_READ | w.GENERIC_WRITE; + } else if (flags & O.APPEND != 0) { + access_mask |= w.GENERIC_WRITE | w.FILE_APPEND_DATA; + } else if (flags & O.WRONLY != 0) { + access_mask |= w.GENERIC_WRITE; + } else { + access_mask |= w.GENERIC_READ; + } + + const creation: w.ULONG = blk: { + if (flags & O.CREAT != 0) { + if (flags & O.EXCL != 0) { + break :blk w.FILE_CREATE; + } + break :blk if (overwrite) w.FILE_OVERWRITE_IF else w.FILE_OPEN_IF; + } + break :blk if (overwrite) w.FILE_OVERWRITE else w.FILE_OPEN; + }; + + const blocking_flag: windows.ULONG = if (!nonblock) windows.FILE_SYNCHRONOUS_IO_NONALERT else 0; + const file_or_dir_flag: windows.ULONG = switch (flags & O.DIRECTORY != 0) { + // .file_only => windows.FILE_NON_DIRECTORY_FILE, + true => windows.FILE_DIRECTORY_FILE, + false => 0, + }; + const follow_symlinks = flags & O.NOFOLLOW == 0; + + const options: windows.ULONG = if (follow_symlinks) file_or_dir_flag | blocking_flag else file_or_dir_flag | windows.FILE_OPEN_REPARSE_POINT; + + return openFileAtWindowsT(T, dir, path, access_mask, creation, options); +} + +pub fn openatWindows( + dir: bun.FileDescriptor, + path: []const u16, + flags: bun.Mode, +) Maybe(bun.FileDescriptor) { + return openatWindowsT(u16, dir, path, flags); +} + +pub fn openatWindowsA( + dir: bun.FileDescriptor, + path: []const u8, + flags: bun.Mode, +) Maybe(bun.FileDescriptor) { + return openatWindowsT(u8, dir, path, flags); +} + pub fn openatOSPath(dirfd: bun.FileDescriptor, file_path: bun.OSPathSliceZ, flags: bun.Mode, perm: bun.Mode) Maybe(bun.FileDescriptor) { if (comptime Environment.isMac) { // https://opensource.apple.com/source/xnu/xnu-7195.81.3/libsyscall/wrappers/open-base.c @@ -688,10 +748,8 @@ pub fn openatOSPath(dirfd: bun.FileDescriptor, file_path: bun.OSPathSliceZ, flag log("openat({d}, {s}) = {d}", .{ dirfd, bun.sliceTo(file_path, 0), rc }); return Maybe(bun.FileDescriptor).errnoSys(rc, .open) orelse .{ .result = bun.toFD(rc) }; - } - - if (comptime Environment.isWindows) { - return openatWindows(dirfd, file_path, flags); + } else if (comptime Environment.isWindows) { + return openatWindowsT(bun.OSPathChar, dirfd, file_path, flags); } while (true) { @@ -717,35 +775,27 @@ pub fn openatOSPath(dirfd: bun.FileDescriptor, file_path: bun.OSPathSliceZ, flag pub fn openat(dirfd: bun.FileDescriptor, file_path: [:0]const u8, flags: bun.Mode, perm: bun.Mode) Maybe(bun.FileDescriptor) { if (comptime Environment.isWindows) { - if (flags & O.DIRECTORY != 0) { - return openDirAtWindowsA(dirfd, file_path, false, flags & O.NOFOLLOW != 0); - } - - var wbuf: bun.WPathBuffer = undefined; - return openatWindows(dirfd, bun.strings.toNTPath(&wbuf, file_path), flags); + return openatWindowsT(u8, dirfd, file_path, flags); + } else { + return openatOSPath(dirfd, file_path, flags, perm); } - - return openatOSPath(dirfd, file_path, flags, perm); } pub fn openatA(dirfd: bun.FileDescriptor, file_path: []const u8, flags: bun.Mode, perm: bun.Mode) Maybe(bun.FileDescriptor) { if (comptime Environment.isWindows) { - if (flags & O.DIRECTORY != 0) { - return openDirAtWindowsA(dirfd, file_path, false, flags & O.NOFOLLOW != 0); - } - - var wbuf: bun.WPathBuffer = undefined; - return openatWindows(dirfd, bun.strings.toNTPath(&wbuf, file_path), flags); + return openatWindowsT(u8, dirfd, file_path, flags); } + const pathZ = std.os.toPosixPath(file_path) catch return Maybe(bun.FileDescriptor){ + .err = .{ + .errno = @intFromEnum(bun.C.E.NOMEM), + .syscall = .open, + }, + }; + return openatOSPath( dirfd, - &(std.os.toPosixPath(file_path) catch return Maybe(bun.FileDescriptor){ - .err = .{ - .errno = @intFromEnum(bun.C.E.NOMEM), - .syscall = .open, - }, - }), + &pathZ, flags, perm, ); diff --git a/src/windows.zig b/src/windows.zig index ccfb7112fd0fb3..cbf6a7c8879aa5 100644 --- a/src/windows.zig +++ b/src/windows.zig @@ -73,6 +73,12 @@ pub const WPathBuffer = if (Environment.isWindows) bun.WPathBuffer else void; pub const HANDLE = win32.HANDLE; +/// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfileinformationbyhandle +pub extern "kernel32" fn GetFileInformationByHandle( + hFile: HANDLE, + lpFileInformation: *windows.BY_HANDLE_FILE_INFORMATION, +) callconv(windows.WINAPI) BOOL; + /// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-setfilevaliddata pub extern "kernel32" fn SetFileValidData( hFile: win32.HANDLE, diff --git a/src/windows_c.zig b/src/windows_c.zig index 7221835e3068b5..6a1e2b15d9b322 100644 --- a/src/windows_c.zig +++ b/src/windows_c.zig @@ -1275,7 +1275,7 @@ pub fn renameAtW( std.debug.assert(!std.fs.path.isAbsoluteWindowsWTF16(new_path_w)); } } - const src_fd = switch (bun.sys.ntCreateFile( + const src_fd = switch (bun.sys.openFileAtWindows( old_dir_fd, old_path_w, // access_mask diff --git a/test/js/node/fs/cp.test.ts b/test/js/node/fs/cp.test.ts index 3ec54d35111f65..25110d42b9a1fa 100644 --- a/test/js/node/fs/cp.test.ts +++ b/test/js/node/fs/cp.test.ts @@ -1,5 +1,6 @@ // @known-failing-on-windows: 1 failing import fs from "fs"; +import { join } from "path"; import { describe, test, expect, jest } from "bun:test"; import { tempDirWithFiles } from "harness"; @@ -246,7 +247,7 @@ for (const [name, copy] of impls) { let prev = process.cwd(); process.chdir(basename); - await copy(basename + "/from", basename + "/result", { + await copy(join(basename, "from"), join(basename, "result"), { filter, recursive: true, }); @@ -254,9 +255,9 @@ for (const [name, copy] of impls) { process.chdir(prev); expect(filter.mock.calls.sort((a, b) => a[0].localeCompare(b[0]))).toEqual([ - [basename + "/from", basename + "/result"], - [basename + "/from/a.txt", basename + "/result/a.txt"], - [basename + "/from/b.txt", basename + "/result/b.txt"], + [join(basename, "from"), join(basename, "result")], + [join(basename, "from", "a.txt"), join(basename, "result", "a.txt")], + [join(basename, "from", "b.txt"), join(basename, "result", "b.txt")], ]); });