From 99ed7b278d4efdb9a2f9dea3e1beba12c3666d7c Mon Sep 17 00:00:00 2001 From: chloe caruso Date: Wed, 15 Jan 2025 19:35:10 -0800 Subject: [PATCH] we are now long paths --- src/bun.js/node/node_fs.zig | 8 +- src/bun.js/node/types.zig | 8 +- src/resolver/resolve_path.zig | 2 + src/string_immutable.zig | 11 ++- src/sys.zig | 90 ++++++++++++++++--- src/sys_uv.zig | 14 +-- .../node/test/parallel/test-fs-long-path.js | 54 +++++++++++ 7 files changed, 155 insertions(+), 32 deletions(-) create mode 100644 test/js/node/test/parallel/test-fs-long-path.js diff --git a/src/bun.js/node/node_fs.zig b/src/bun.js/node/node_fs.zig index 15c90ad802..37d60cd3ec 100644 --- a/src/bun.js/node/node_fs.zig +++ b/src/bun.js/node/node_fs.zig @@ -3184,15 +3184,15 @@ pub const NodeFS = struct { /// We want to avoid allocating a new path buffer for every error message so that JSC can clone + GC it. /// That means a stack-allocated buffer won't suffice. Instead, we re-use /// the heap allocated buffer on the NodeFS struct - sync_error_buf: bun.PathBuffer = undefined, + sync_error_buf: bun.PathBuffer align(@alignOf(u16)) = undefined, vm: ?*JSC.VirtualMachine = null, pub const ReturnType = Return; pub fn access(this: *NodeFS, args: Arguments.Access, _: Flavor) Maybe(Return.Access) { - const path = args.path.sliceZ(&this.sync_error_buf); + const path = args.path.osPathKernel32(&this.sync_error_buf); return switch (Syscall.access(path, @intFromEnum(args.mode))) { - .err => |err| .{ .err = err }, + .err => |err| .{ .err = err.withPath(args.path.slice()) }, .result => .{ .result = .{} }, }; } @@ -5075,7 +5075,7 @@ pub const NodeFS = struct { break :brk switch (open_result) { .err => |err| return .{ - .err = err.withPath(path), + .err = err.withPath(args.file.path.slice()), }, .result => |fd| fd, }; diff --git a/src/bun.js/node/types.zig b/src/bun.js/node/types.zig index 5f3a6194a1..c8b277d4a6 100644 --- a/src/bun.js/node/types.zig +++ b/src/bun.js/node/types.zig @@ -888,6 +888,12 @@ pub const PathLike = union(enum) { if (Environment.isWindows) { if (std.fs.path.isAbsolute(sliced)) { + if (sliced.len > 2 and bun.path.isDriveLetter(sliced[0]) and sliced[1] == ':' and bun.path.isSepAny(sliced[2])) { + // Add the long path syntax. This affects most of node:fs + const rest = path_handler.PosixToWinNormalizer.resolveCWDWithExternalBufZ(@ptrCast(buf[4..]), sliced) catch @panic("Error while resolving path."); + buf[0..4].* = bun.windows.nt_maxpath_prefix_u8; + return buf[0 .. 4 + rest.len :0]; + } return path_handler.PosixToWinNormalizer.resolveCWDWithExternalBufZ(buf, sliced) catch @panic("Error while resolving path."); } } @@ -928,7 +934,7 @@ pub const PathLike = union(enum) { pub inline fn osPathKernel32(this: PathLike, buf: *bun.PathBuffer) bun.OSPathSliceZ { if (comptime Environment.isWindows) { - return strings.toWPath(@alignCast(std.mem.bytesAsSlice(u16, buf)), this.slice()); + return strings.toKernel32Path(@alignCast(std.mem.bytesAsSlice(u16, buf)), this.slice()); } return sliceZWithForceCopy(this, buf, false); diff --git a/src/resolver/resolve_path.zig b/src/resolver/resolve_path.zig index dec3d1bc49..62b4c2ff00 100644 --- a/src/resolver/resolve_path.zig +++ b/src/resolver/resolve_path.zig @@ -2095,6 +2095,8 @@ pub fn dangerouslyConvertPathToPosixInPlace(comptime T: type, path: []T) void { pub fn dangerouslyConvertPathToWindowsInPlace(comptime T: type, path: []T) void { var idx: usize = 0; + if (T == u16) + std.debug.print("ugh {}\n", .{bun.fmt.utf16(path)}); while (std.mem.indexOfScalarPos(T, path, idx, std.fs.path.sep_posix)) |index| : (idx = index + 1) { path[index] = '\\'; } diff --git a/src/string_immutable.zig b/src/string_immutable.zig index 8c7e4c2356..857f16a724 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -2041,7 +2041,7 @@ pub fn toWDirPath(wbuf: []u16, utf8: []const u8) [:0]const u16 { return toWPathMaybeDir(wbuf, utf8, true); } -pub fn toKernel32Path(wbuf: []u16, utf8: []const u8) [:0]const u16 { +pub fn toKernel32Path(wbuf: []u16, utf8: []const u8) [:0]u16 { const path = if (hasPrefixComptime(utf8, bun.windows.nt_object_prefix_u8)) utf8[bun.windows.nt_object_prefix_u8.len..] else @@ -2088,6 +2088,15 @@ pub fn toWPathMaybeDir(wbuf: []u16, utf8: []const u8, comptime add_trailing_lash wbuf[0..wbuf.len -| (1 + @as(usize, @intFromBool(add_trailing_lash)))], ); + // Many Windows APIs expect normalized path slashes, particularly when the + // long path prefix is added or the nt object prefix. To make this easier, + // but a little redundant, this function always normalizes the slashes here. + // + // An example of this is GetFileAttributesW(L"C:\\hello/world.txt") being OK + // but GetFileAttributesW(L"\\\\?\\C:\\hello/world.txt") is NOT + if (Environment.isWindows) + bun.path.dangerouslyConvertPathToWindowsInPlace(u16, wbuf[0..result.count]); + if (add_trailing_lash and result.count > 0 and wbuf[result.count - 1] != '\\') { wbuf[result.count] = '\\'; result.count += 1; diff --git a/src/sys.zig b/src/sys.zig index 5fb343dcf0..e463e9aa7c 100644 --- a/src/sys.zig +++ b/src/sys.zig @@ -27,6 +27,11 @@ const linux = syscall; pub const sys_uv = if (Environment.isWindows) @import("./sys_uv.zig") else Syscall; +pub const F_OK = 0; +pub const X_OK = 1; +pub const W_OK = 2; +pub const R_OK = 4; + const log = bun.Output.scoped(.SYS, false); pub const syslog = log; @@ -884,6 +889,9 @@ pub fn getErrno(rc: anytype) bun.C.E { const w = std.os.windows; +/// Normalizes for ntdll.dll APIs. Replaces long-path prefixes with nt object +/// prefixes, which may not function properly in kernel32 APIs. +// TODO: Rename to normalizePathWindowsForNtdll pub fn normalizePathWindows( comptime T: type, dir_fd: bun.FileDescriptor, @@ -907,14 +915,22 @@ pub fn normalizePathWindows( return .{ .result = buf[0..bun.strings.w("\\??\\NUL").len :0] }; } if ((path[1] == '/' or path[1] == '\\') and - (path[2] == '.' or path[2] == '?') and (path[3] == '/' or path[3] == '\\')) { - buf[0..4].* = .{ '\\', '\\', path[2], '\\' }; - const rest = path[4..]; - @memcpy(buf[4..][0..rest.len], rest); - buf[path.len] = 0; - return .{ .result = buf[0..path.len :0] }; + // Preserve the device path, instead of resolving '.' as a relative + // path. This prevents simplifying the path '\\.\pipe' into '\pipe' + if (path[2] == '.') { + buf[0..4].* = .{ '\\', '\\', '.', '\\' }; + const rest = path[4..]; + @memcpy(buf[4..][0..rest.len], rest); + buf[path.len] = 0; + return .{ .result = buf[0..path.len :0] }; + } + // For long paths and nt object paths, conver the prefix into an nt object, then resolve. + // TODO: NT object paths technically mean they are already resolved. Will that break? + if (path[2] == '?' and (path[1] == '?' or path[1] == '/' or path[1] == '\\')) { + path = path[4..]; + } } } @@ -1342,9 +1358,24 @@ pub fn openatWindowsT(comptime T: type, dir: bun.FileDescriptor, path: []const T return openatWindowsTMaybeNormalize(T, dir, path, flags, true); } -fn openatWindowsTMaybeNormalize(comptime T: type, dir: bun.FileDescriptor, path: []const T, flags: bun.Mode, comptime normalize: bool) Maybe(bun.FileDescriptor) { +fn openatWindowsTMaybeNormalize(comptime T: type, dir: bun.FileDescriptor, path: []const T, flags_in: bun.Mode, comptime normalize: bool) Maybe(bun.FileDescriptor) { + var flags = flags_in; + if (flags & bun.windows.libuv.UV_FS_O_FILEMAP != 0) { + if (flags & (O.RDONLY | O.WRONLY | O.RDWR) != 0) { + flags = (flags & ~@as(c_int, O.WRONLY)) | O.RDWR; + } + if (flags & O.APPEND != 0) { + flags &= ~@as(c_int, O.APPEND); + flags &= ~@as(c_int, O.RDONLY | O.WRONLY | O.RDWR); + flags |= O.RDWR; + } + } if (flags & O.DIRECTORY != 0) { - const windows_options: WindowsOpenDirOptions = .{ .iterable = flags & O.PATH == 0, .no_follow = flags & O.NOFOLLOW != 0, .can_rename_or_delete = false }; + const windows_options: WindowsOpenDirOptions = .{ + .iterable = flags & O.PATH == 0, + .no_follow = flags & O.NOFOLLOW != 0, + .can_rename_or_delete = false, + }; if (comptime !normalize and T == u16) { return openDirAtWindowsNtPath(dir, path, windows_options); } @@ -1372,6 +1403,8 @@ fn openatWindowsTMaybeNormalize(comptime T: type, dir: bun.FileDescriptor, path: access_mask |= w.GENERIC_READ; } + // TODO: UV_FS_O_EXLOCK must set share access to 0. + const creation: w.ULONG = blk: { if (flags & O.CREAT != 0) { if (flags & O.EXCL != 0) { @@ -1447,6 +1480,26 @@ pub fn openatOSPath(dirfd: bun.FileDescriptor, file_path: bun.OSPathSliceZ, flag } pub fn access(path: bun.OSPathSliceZ, mode: bun.Mode) Maybe(void) { + if (Environment.isWindows) { + const attrs = getFileAttributes(path) orelse { + return .{ .err = .{ + .errno = @intFromEnum(bun.windows.getLastErrno()), + .syscall = .access, + } }; + }; + + if (!((mode & W_OK) > 0) or + !(attrs.is_readonly) or + (attrs.is_directory)) + { + return .{ .result = {} }; + } else { + return .{ .err = .{ + .errno = @intFromEnum(bun.C.E.PERM), + .syscall = .access, + } }; + } + } return Maybe(void).errnoSysP(syscall.access(path, mode), .access, path) orelse .{ .result = {} }; } @@ -2762,7 +2815,7 @@ pub fn getFileAttributes(path: anytype) ?WindowsFileAttributes { } else { const wbuf = bun.WPathBufferPool.get(); defer bun.WPathBufferPool.put(wbuf); - const path_to_use = bun.strings.toWPath(wbuf, path); + const path_to_use = bun.strings.toKernel32Path(wbuf, path); return getFileAttributes(path_to_use); } } @@ -2778,13 +2831,24 @@ pub fn existsOSPath(path: bun.OSPathSliceZ, file_only: bool) bool { if (Environment.isWindows) { const attributes = getFileAttributes(path) orelse return false; - if (file_only and attributes.is_directory) { return false; } - - std.debug.print("{}\n", .{attributes}); - + if (attributes.is_reparse_point) { + // Check if the underlying file exists by opening it. + const rc = std.os.windows.kernel32.CreateFileW( + path, + 0, + 0, + null, + w.OPEN_EXISTING, + w.FILE_FLAG_BACKUP_SEMANTICS, + null, + ); + if (rc == w.INVALID_HANDLE_VALUE) return false; + defer _ = std.os.windows.kernel32.CloseHandle(rc); + return true; + } return true; } diff --git a/src/sys_uv.zig b/src/sys_uv.zig index 27b87a19aa..c9ea205a3b 100644 --- a/src/sys_uv.zig +++ b/src/sys_uv.zig @@ -34,6 +34,7 @@ pub const getFdPath = bun.sys.getFdPath; pub const setFileOffset = bun.sys.setFileOffset; pub const openatOSPath = bun.sys.openatOSPath; pub const mkdirOSPath = bun.sys.mkdirOSPath; +pub const access = bun.sys.access; // Note: `req = undefined; req.deinit()` has a saftey-check in a debug build @@ -126,19 +127,6 @@ pub fn fchown(fd: FileDescriptor, uid: uv.uv_uid_t, gid: uv.uv_uid_t) Maybe(void .{ .result = {} }; } -pub fn access(file_path: [:0]const u8, flags: bun.Mode) Maybe(void) { - assertIsValidWindowsPath(u8, file_path); - var req: uv.fs_t = uv.fs_t.uninitialized; - defer req.deinit(); - const rc = uv.uv_fs_access(uv.Loop.get(), &req, file_path.ptr, flags, null); - - log("uv access({s}, {d}) = {d}", .{ file_path, flags, rc.int() }); - return if (rc.errno()) |errno| - .{ .err = .{ .errno = errno, .syscall = .access, .path = file_path } } - else - .{ .result = {} }; -} - pub fn rmdir(file_path: [:0]const u8) Maybe(void) { assertIsValidWindowsPath(u8, file_path); var req: uv.fs_t = uv.fs_t.uninitialized; diff --git a/test/js/node/test/parallel/test-fs-long-path.js b/test/js/node/test/parallel/test-fs-long-path.js new file mode 100644 index 0000000000..a544cffd2e --- /dev/null +++ b/test/js/node/test/parallel/test-fs-long-path.js @@ -0,0 +1,54 @@ +// 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'); +if (!common.isWindows) + common.skip('this test is Windows-specific.'); + +const fs = require('fs'); +const path = require('path'); + +const tmpdir = require('../common/tmpdir'); + +// Make a path that will be at least 260 chars long. +const fileNameLen = Math.max(260 - tmpdir.path.length - 1, 1); +const fileName = tmpdir.resolve('x'.repeat(fileNameLen)); +const fullPath = path.resolve(fileName); + +tmpdir.refresh(); + +console.log({ + filenameLength: fileName.length, + fullPathLength: fullPath.length +}); + +console.log(1); +fs.writeFile(fullPath, 'ok', common.mustSucceed(() => { + console.log(2); + fs.stat(fullPath, common.mustSucceed()); + + // Tests https://github.com/nodejs/node/issues/39721 + // fs.realpath.native(fullPath, common.mustSucceed()); + + // Tests https://github.com/nodejs/node/issues/51031 + // fs.promises.realpath(fullPath).then(common.mustCall(), common.mustNotCall()); +}));