From 43acf18824d755cab38d1eaf9d8a267ce6f5770c Mon Sep 17 00:00:00 2001 From: chloe caruso Date: Thu, 16 Jan 2025 14:26:13 -0800 Subject: [PATCH] more --- src/StandaloneModuleGraph.zig | 2 +- src/bun.js/node/node_fs.zig | 36 ++--- src/bun.js/node/types.zig | 116 ++++++------- src/deps/libuv.zig | 3 +- src/js/node/fs.ts | 295 +++++++++++++++++++++++++++++++--- src/output.zig | 4 +- src/string_immutable.zig | 14 +- src/sys.zig | 157 ++++++++++++++++-- 8 files changed, 493 insertions(+), 134 deletions(-) diff --git a/src/StandaloneModuleGraph.zig b/src/StandaloneModuleGraph.zig index aae3c32a17..77ba62260b 100644 --- a/src/StandaloneModuleGraph.zig +++ b/src/StandaloneModuleGraph.zig @@ -953,7 +953,7 @@ pub const StandaloneModuleGraph = struct { const image_path = image_path_unicode_string.Buffer.?[0 .. image_path_unicode_string.Length / 2]; var nt_path_buf: bun.WPathBuffer = undefined; - const nt_path = bun.strings.addNTPathPrefix(&nt_path_buf, image_path); + const nt_path = bun.strings.addNTPathPrefixIfNeeded(&nt_path_buf, image_path); const basename_start = std.mem.lastIndexOfScalar(u16, nt_path, '\\') orelse return error.FileNotFound; diff --git a/src/bun.js/node/node_fs.zig b/src/bun.js/node/node_fs.zig index 7cce3a38a2..1e5f489530 100644 --- a/src/bun.js/node/node_fs.zig +++ b/src/bun.js/node/node_fs.zig @@ -2234,7 +2234,7 @@ pub const Arguments = struct { pub const Open = struct { path: PathLike, - flags: FileSystemFlags = FileSystemFlags.r, + flags: FileSystemFlags = .r, mode: Mode = default_permission, pub fn deinit(this: Open) void { @@ -3191,7 +3191,7 @@ pub const NodeFS = struct { pub fn access(this: *NodeFS, args: Arguments.Access, _: Flavor) Maybe(Return.Access) { const path = args.path.osPathKernel32(&this.sync_error_buf); - return switch (Syscall.access(path, @intFromEnum(args.mode))) { + return switch (Syscall.access(path, args.mode.asInt())) { .err => |err| .{ .err = err.withPath(args.path.slice()) }, .result => .{ .result = .{} }, }; @@ -3362,7 +3362,7 @@ pub const NodeFS = struct { /// https://github.com/pnpm/pnpm/issues/2761 /// https://github.com/libuv/libuv/pull/2578 /// https://github.com/nodejs/node/issues/34624 - fn copyFileInner(this: *NodeFS, args: Arguments.CopyFile) Maybe(Return.CopyFile) { + fn copyFileInner(fs: *NodeFS, args: Arguments.CopyFile) Maybe(Return.CopyFile) { const ret = Maybe(Return.CopyFile); // TODO: do we need to fchown? @@ -3567,12 +3567,11 @@ pub const NodeFS = struct { } if (comptime Environment.isWindows) { - const src_buf = bun.OSPathBufferPool.get(); - defer bun.OSPathBufferPool.put(src_buf); const dest_buf = bun.OSPathBufferPool.get(); defer bun.OSPathBufferPool.put(dest_buf); - const src = strings.toWPathNormalizeAutoExtend(src_buf, args.src.sliceZ(&this.sync_error_buf)); - const dest = strings.toWPathNormalizeAutoExtend(dest_buf, args.dest.sliceZ(&this.sync_error_buf)); + + const src = bun.strings.toKernel32Path(bun.reinterpretSlice(u16, &fs.sync_error_buf), args.src.slice()); + const dest = bun.strings.toKernel32Path(dest_buf, args.dest.slice()); if (windows.CopyFileW(src.ptr, dest.ptr, if (args.mode.shouldntOverwrite()) 1 else 0) == windows.FALSE) { if (ret.errnoSysP(0, .copyfile, args.src.slice())) |rest| { return shouldIgnoreEbusy(args.src, args.dest, rest); @@ -3582,7 +3581,7 @@ pub const NodeFS = struct { return ret.success; } - return Maybe(Return.CopyFile).todo(); + @compileError(unreachable); } pub fn exists(this: *NodeFS, args: Arguments.Exists, _: Flavor) Maybe(Return.Exists) { @@ -3733,7 +3732,7 @@ pub const NodeFS = struct { const path = args.path.sliceZ(&this.sync_error_buf); return switch (Syscall.mkdir(path, args.mode)) { .result => Maybe(Return.Mkdir){ .result = .{ .none = {} } }, - .err => |err| Maybe(Return.Mkdir){ .err = err.withPath(path) }, + .err => |err| Maybe(Return.Mkdir){ .err = err.withPath(args.path.slice()) }, }; } @@ -3981,7 +3980,7 @@ pub const NodeFS = struct { else args.path.sliceZ(&this.sync_error_buf); - return switch (Syscall.open(path, @intFromEnum(args.flags), args.mode)) { + return switch (Syscall.open(path, args.flags.asInt(), args.mode)) { .err => |err| .{ .err = err.withPath(args.path.slice()), }, @@ -4499,10 +4498,9 @@ pub const NodeFS = struct { // Node doesn't gracefully handle errors like these. It fails the entire operation. .NOENT, .NOTDIR, .PERM => continue, else => { - const path_parts = [_]string{ args.path.slice(), basename }; - return .{ - .err = err.withPath(bun.default_allocator.dupe(u8, bun.path.joinZBuf(buf, &path_parts, .auto)) catch ""), - }; + // const path_parts = [_]string{ args.path.slice(), basename }; + // TODO: propagate file path (removed previously because it leaked the path) + return .{ .err = err }; }, } }, @@ -4580,7 +4578,7 @@ pub const NodeFS = struct { bun.String => { entries.append(bun.String.createUTF8(name_to_copy)) catch bun.outOfMemory(); }, - else => @compileError("Impossible"), + else => @compileError(unreachable), } } } @@ -4763,11 +4761,11 @@ pub const NodeFS = struct { break :brk switch (bun.sys.open( path, - @intFromEnum(args.flag) | bun.O.NOCTTY, + args.flag.asInt() | bun.O.NOCTTY, default_permission, )) { .err => |err| return .{ - .err = err.withPath(if (args.path == .path) args.path.path.slice() else ""), + .err = err.withPath(args.path.path.slice()), }, .result => |fd| fd, }; @@ -5066,10 +5064,10 @@ pub const NodeFS = struct { .path => brk: { const path = args.file.path.sliceZWithForceCopy(pathbuf, true); - const open_result = Syscall.openat( + const open_result = bun.sys.openat( args.dirfd, path, - @intFromEnum(args.flag) | bun.O.NOCTTY, + args.flag.asInt(), args.mode, ); diff --git a/src/bun.js/node/types.zig b/src/bun.js/node/types.zig index e653cfb7b2..2bc27342fd 100644 --- a/src/bun.js/node/types.zig +++ b/src/bun.js/node/types.zig @@ -1406,9 +1406,12 @@ pub const PathOrFileDescriptor = union(Tag) { } }; -pub const FileSystemFlags = enum(Mode) { +/// On windows, this uses the libuv specific mode flags. +pub const FileSystemFlags = enum(c_int) { + const O = bun.O; + /// Open file for appending. The file is created if it does not exist. - a = bun.O.APPEND | bun.O.WRONLY | bun.O.CREAT, + a = O.APPEND | O.WRONLY | O.CREAT, /// Like 'a' but fails if the path exists. // @"ax" = bun.O.APPEND | bun.O.EXCL, /// Open file for reading and appending. The file is created if it does not exist. @@ -1420,7 +1423,7 @@ pub const FileSystemFlags = enum(Mode) { /// Open file for reading and appending in synchronous mode. The file is created if it does not exist. // @"as+" = bun.O.APPEND | bun.O.RDWR, /// Open file for reading. An exception occurs if the file does not exist. - r = bun.O.RDONLY, + r = O.RDONLY, /// Open file for reading and writing. An exception occurs if the file does not exist. // @"r+" = bun.O.RDWR, /// Open file for reading and writing in synchronous mode. Instructs the operating system to bypass the local file system cache. @@ -1428,7 +1431,7 @@ pub const FileSystemFlags = enum(Mode) { /// This doesn't turn fs.open() or fsPromises.open() into a synchronous blocking call. If synchronous operation is desired, something like fs.openSync() should be used. // @"rs+" = bun.O.RDWR, /// Open file for writing. The file is created (if it does not exist) or truncated (if it exists). - w = bun.O.WRONLY | bun.O.CREAT, + w = O.WRONLY | O.CREAT, /// Like 'w' but fails if the path exists. // @"wx" = bun.O.WRONLY | bun.O.TRUNC, // /// Open file for reading and writing. The file is created (if it does not exist) or truncated (if it exists). @@ -1438,69 +1441,60 @@ pub const FileSystemFlags = enum(Mode) { _, - const O_RDONLY: Mode = bun.O.RDONLY; - const O_RDWR: Mode = bun.O.RDWR; - const O_APPEND: Mode = bun.O.APPEND; - const O_CREAT: Mode = bun.O.CREAT; - const O_WRONLY: Mode = bun.O.WRONLY; - const O_EXCL: Mode = bun.O.EXCL; - const O_SYNC: Mode = 0; - const O_TRUNC: Mode = bun.O.TRUNC; - const map = bun.ComptimeStringMap(Mode, .{ - .{ "r", O_RDONLY }, - .{ "rs", O_RDONLY | O_SYNC }, - .{ "sr", O_RDONLY | O_SYNC }, - .{ "r+", O_RDWR }, - .{ "rs+", O_RDWR | O_SYNC }, - .{ "sr+", O_RDWR | O_SYNC }, + .{ "r", O.RDONLY }, + .{ "rs", O.RDONLY | O.SYNC }, + .{ "sr", O.RDONLY | O.SYNC }, + .{ "r+", O.RDWR }, + .{ "rs+", O.RDWR | O.SYNC }, + .{ "sr+", O.RDWR | O.SYNC }, - .{ "R", O_RDONLY }, - .{ "RS", O_RDONLY | O_SYNC }, - .{ "SR", O_RDONLY | O_SYNC }, - .{ "R+", O_RDWR }, - .{ "RS+", O_RDWR | O_SYNC }, - .{ "SR+", O_RDWR | O_SYNC }, + .{ "R", O.RDONLY }, + .{ "RS", O.RDONLY | O.SYNC }, + .{ "SR", O.RDONLY | O.SYNC }, + .{ "R+", O.RDWR }, + .{ "RS+", O.RDWR | O.SYNC }, + .{ "SR+", O.RDWR | O.SYNC }, - .{ "w", O_TRUNC | O_CREAT | O_WRONLY }, - .{ "wx", O_TRUNC | O_CREAT | O_WRONLY | O_EXCL }, - .{ "xw", O_TRUNC | O_CREAT | O_WRONLY | O_EXCL }, + .{ "w", O.TRUNC | O.CREAT | O.WRONLY }, + .{ "wx", O.TRUNC | O.CREAT | O.WRONLY | O.EXCL }, + .{ "xw", O.TRUNC | O.CREAT | O.WRONLY | O.EXCL }, - .{ "W", O_TRUNC | O_CREAT | O_WRONLY }, - .{ "WX", O_TRUNC | O_CREAT | O_WRONLY | O_EXCL }, - .{ "XW", O_TRUNC | O_CREAT | O_WRONLY | O_EXCL }, + .{ "W", O.TRUNC | O.CREAT | O.WRONLY }, + .{ "WX", O.TRUNC | O.CREAT | O.WRONLY | O.EXCL }, + .{ "XW", O.TRUNC | O.CREAT | O.WRONLY | O.EXCL }, - .{ "w+", O_TRUNC | O_CREAT | O_RDWR }, - .{ "wx+", O_TRUNC | O_CREAT | O_RDWR | O_EXCL }, - .{ "xw+", O_TRUNC | O_CREAT | O_RDWR | O_EXCL }, + .{ "w+", O.TRUNC | O.CREAT | O.RDWR }, + .{ "wx+", O.TRUNC | O.CREAT | O.RDWR | O.EXCL }, + .{ "xw+", O.TRUNC | O.CREAT | O.RDWR | O.EXCL }, - .{ "W+", O_TRUNC | O_CREAT | O_RDWR }, - .{ "WX+", O_TRUNC | O_CREAT | O_RDWR | O_EXCL }, - .{ "XW+", O_TRUNC | O_CREAT | O_RDWR | O_EXCL }, + .{ "W+", O.TRUNC | O.CREAT | O.RDWR }, + .{ "WX+", O.TRUNC | O.CREAT | O.RDWR | O.EXCL }, + .{ "XW+", O.TRUNC | O.CREAT | O.RDWR | O.EXCL }, - .{ "a", O_APPEND | O_CREAT | O_WRONLY }, - .{ "ax", O_APPEND | O_CREAT | O_WRONLY | O_EXCL }, - .{ "xa", O_APPEND | O_CREAT | O_WRONLY | O_EXCL }, - .{ "as", O_APPEND | O_CREAT | O_WRONLY | O_SYNC }, - .{ "sa", O_APPEND | O_CREAT | O_WRONLY | O_SYNC }, + .{ "a", O.APPEND | O.CREAT | O.WRONLY }, + .{ "ax", O.APPEND | O.CREAT | O.WRONLY | O.EXCL }, + .{ "xa", O.APPEND | O.CREAT | O.WRONLY | O.EXCL }, + .{ "as", O.APPEND | O.CREAT | O.WRONLY | O.SYNC }, + .{ "sa", O.APPEND | O.CREAT | O.WRONLY | O.SYNC }, - .{ "A", O_APPEND | O_CREAT | O_WRONLY }, - .{ "AX", O_APPEND | O_CREAT | O_WRONLY | O_EXCL }, - .{ "XA", O_APPEND | O_CREAT | O_WRONLY | O_EXCL }, - .{ "AS", O_APPEND | O_CREAT | O_WRONLY | O_SYNC }, - .{ "SA", O_APPEND | O_CREAT | O_WRONLY | O_SYNC }, + .{ "A", O.APPEND | O.CREAT | O.WRONLY }, + .{ "AX", O.APPEND | O.CREAT | O.WRONLY | O.EXCL }, + .{ "XA", O.APPEND | O.CREAT | O.WRONLY | O.EXCL }, + .{ "AS", O.APPEND | O.CREAT | O.WRONLY | O.SYNC }, + .{ "SA", O.APPEND | O.CREAT | O.WRONLY | O.SYNC }, - .{ "a+", O_APPEND | O_CREAT | O_RDWR }, - .{ "ax+", O_APPEND | O_CREAT | O_RDWR | O_EXCL }, - .{ "xa+", O_APPEND | O_CREAT | O_RDWR | O_EXCL }, - .{ "as+", O_APPEND | O_CREAT | O_RDWR | O_SYNC }, - .{ "sa+", O_APPEND | O_CREAT | O_RDWR | O_SYNC }, + .{ "a+", O.APPEND | O.CREAT | O.RDWR }, + .{ "ax+", O.APPEND | O.CREAT | O.RDWR | O.EXCL }, + .{ "xa+", O.APPEND | O.CREAT | O.RDWR | O.EXCL }, + .{ "as+", O.APPEND | O.CREAT | O.RDWR | O.SYNC }, + .{ "sa+", O.APPEND | O.CREAT | O.RDWR | O.SYNC }, - .{ "A+", O_APPEND | O_CREAT | O_RDWR }, - .{ "AX+", O_APPEND | O_CREAT | O_RDWR | O_EXCL }, - .{ "XA+", O_APPEND | O_CREAT | O_RDWR | O_EXCL }, - .{ "AS+", O_APPEND | O_CREAT | O_RDWR | O_SYNC }, - .{ "SA+", O_APPEND | O_CREAT | O_RDWR | O_SYNC }, + .{ "A+", O.APPEND | O.CREAT | O.RDWR }, + .{ "AX+", O.APPEND | O.CREAT | O.RDWR | O.EXCL }, + .{ "XA+", O.APPEND | O.CREAT | O.RDWR | O.EXCL }, + .{ "AS+", O.APPEND | O.CREAT | O.RDWR | O.SYNC }, + .{ "SA+", O.APPEND | O.CREAT | O.RDWR | O.SYNC }, }); pub fn fromJS(ctx: JSC.C.JSContextRef, val: JSC.JSValue) bun.JSError!?FileSystemFlags { @@ -1580,6 +1574,14 @@ pub const FileSystemFlags = enum(Mode) { return @enumFromInt(@as(i32, @intFromFloat(float))); } } + + /// On Windows this is a libuv-styled O_* flags, on posix it is libc-style. + /// + /// On Windows, this does not perfectly line up with bun.O, as libuv doesnt + /// define O_DIRECTORY for example. + pub fn asInt(flags: FileSystemFlags) c_int { + return @intFromEnum(flags); + } }; /// Stats and BigIntStats classes from node:fs diff --git a/src/deps/libuv.zig b/src/deps/libuv.zig index 071b1862b1..36cd82fa92 100644 --- a/src/deps/libuv.zig +++ b/src/deps/libuv.zig @@ -241,7 +241,8 @@ const _O_SHORT_LIVED = 0x1000; const _O_SEQUENTIAL = 0x0020; const _O_RANDOM = 0x0010; -// These **do not** map to std.posix.O/bun.O! +// These **do not** map to std.posix.O/bun.O +// To use libuv O, use libuv.O. pub const UV_FS_O_APPEND = 0x0008; pub const UV_FS_O_CREAT = _O_CREAT; pub const UV_FS_O_EXCL = 0x0400; diff --git a/src/js/node/fs.ts b/src/js/node/fs.ts index e237064b55..c4e3338499 100644 --- a/src/js/node/fs.ts +++ b/src/js/node/fs.ts @@ -1,4 +1,5 @@ // Hardcoded module "node:fs" +import type { Stats as StatsType } from "fs"; var WriteStream; const EventEmitter = require("node:events"); const promises = require("node:fs/promises"); @@ -266,11 +267,14 @@ var access = function access(path, mode, callback) { fs.futimes(fd, atime, mtime).then(nullcallback(callback), callback); }, - lchmod = constants.O_SYMLINK !== undefined ? function lchmod(path, mode, callback) { - ensureCallback(callback); + lchmod = + constants.O_SYMLINK !== undefined + ? function lchmod(path, mode, callback) { + ensureCallback(callback); - fs.lchmod(path, mode).then(nullcallback(callback), callback); - } : undefined, // lchmod is only available on macOS + fs.lchmod(path, mode).then(nullcallback(callback), callback); + } + : undefined, // lchmod is only available on macOS lchown = function lchown(path, uid, gid, callback) { ensureCallback(callback); @@ -407,7 +411,7 @@ var access = function access(path, mode, callback) { fs.writeFile(path, data, options).then(nullcallback(callback), callback); }, - readlink = function readlink(path, options, callback) { + readlink = function readlink(path, options, callback?) { if ($isCallable(options)) { callback = options; options = undefined; @@ -419,24 +423,12 @@ var access = function access(path, mode, callback) { callback(null, linkString); }, callback); }, - realpath = function realpath(p, options, callback) { - if ($isCallable(options)) { - callback = options; - options = undefined; - } - - ensureCallback(callback); - - fs.realpath(p, options, false).then(function (resolvedPath) { - callback(null, resolvedPath); - }, callback); - }, rename = function rename(oldPath, newPath, callback) { ensureCallback(callback); fs.rename(oldPath, newPath).then(nullcallback(callback), callback); }, - lstat = function lstat(path, options, callback) { + lstat = function lstat(path, options, callback?) { if ($isCallable(options)) { callback = options; options = undefined; @@ -448,7 +440,7 @@ var access = function access(path, mode, callback) { callback(null, stats); }, callback); }, - stat = function stat(path, options, callback) { + stat = function stat(path, options, callback?) { if ($isCallable(options)) { callback = options; options = undefined; @@ -534,7 +526,6 @@ var access = function access(path, mode, callback) { fdatasyncSync = fs.fdatasyncSync.bind(fs), writeFileSync = fs.writeFileSync.bind(fs), readlinkSync = fs.readlinkSync.bind(fs), - realpathSync = fs.realpathSync.bind(fs), renameSync = fs.renameSync.bind(fs), statSync = fs.statSync.bind(fs), symlinkSync = fs.symlinkSync.bind(fs), @@ -616,11 +607,10 @@ readv[kCustomPromisifiedSymbol] = promises.readv; // listeners per StatWatcher with the current implementation in native code. the downside // of this means we need to do path validation in the js side of things const statWatchers = new Map(); -let _pathModule; function getValidatedPath(p) { if (p instanceof URL) return Bun.fileURLToPath(p); if (typeof p !== "string") throw new TypeError("Path must be a string or URL."); - return (_pathModule ??= require("node:path")).resolve(p); + return require("node:path").resolve(p); } function watchFile(filename, options, listener) { filename = getValidatedPath(filename); @@ -1384,7 +1374,266 @@ Object.defineProperties(fs, { }, }); -// @ts-ignore +const splitRootWindowsRe = /^(?:[a-zA-Z]:|[\\/]{2}[^\\/]+[\\/][^\\/]+)?[\\/]*/; +function splitRootWindows(str) { + return splitRootWindowsRe.exec(str)![0]; +} +function nextPartWindows(p, i) { + for (; i < p.length; ++i) { + const ch = p.$charCodeAt(i); + + // Check for a separator character + if (ch === "\\".charCodeAt(0) || ch === "/".charCodeAt(0)) return i; + } + return -1; +} + +function encodeRealpathResult(result, encoding) { + if (!encoding || encoding === "utf8") return result; + const asBuffer = Buffer.from(result); + if (encoding === "buffer") { + return asBuffer; + } + return asBuffer.toString(encoding); +} + +const realpathSync: any = + process.platform !== "win32" + ? fs.realpathSync.bind(fs) + : function (p, options) { + let encoding; + if (options) { + if (typeof options === "string") encoding = options; + else encoding = options?.encoding; + } + // This function is ported 1:1 from node.js, to emulate how it is unable to + // resolve subst drives to their underlying location. The native call is + // able to see through that. + p = getValidatedPath(p); + const knownHard = new Set(); + + // Current character position in p + let pos; + // The partial path so far, including a trailing slash if any + let current; + // The partial path without a trailing slash (except when pointing at a root) + let base; + // The partial path scanned in the previous round, with slash + let previous; + + // Skip over roots + current = base = splitRootWindows(p); + pos = current.length; + + // On windows, check that the root exists. On unix there is no need. + let lastStat: StatsType = lstatSync(base, { throwIfNoEntry: true }); + if (lastStat === undefined) return; + knownHard.$add(base); + + const pathModule = require("node:path"); + + // Walk down the path, swapping out linked path parts for their real + // values + // NB: p.length changes. + while (pos < p.length) { + // find the next part + const result = nextPartWindows(p, pos); + previous = current; + if (result === -1) { + const last = p.slice(pos); + current += last; + base = previous + last; + pos = p.length; + } else { + current += p.slice(pos, result + 1); + base = previous + p.slice(pos, result); + pos = result + 1; + } + + // Continue if not a symlink, break if a pipe/socket + if (knownHard.$has(base)) { + if (lastStat.isFIFO() || lastStat.isSocket()) { + break; + } + continue; + } + + let resolvedLink; + // const maybeCachedResolved = cache?.get(base); + // if (maybeCachedResolved) { + // resolvedLink = maybeCachedResolved; + // } else { + lastStat = fs.lstatSync(base, true, undefined, true /* throwIfNoEntry */); + if (lastStat === undefined) return; + + if (!lastStat.isSymbolicLink()) { + knownHard.$add(base); + // cache?.set(base, base); + continue; + } + + // Read the link if it wasn't read before + // dev/ino always return 0 on windows, so skip the check. + let linkTarget = null; + if (linkTarget === null) { + lastStat = fs.statSync(base, { throwIfNoEntry: true }); + linkTarget = fs.readlink(base); + } + resolvedLink = pathModule.resolve(previous, linkTarget); + // } + + // Resolve the link, then start over + p = pathModule.resolve(resolvedLink, p.slice(pos)); + + // Skip over roots + current = base = splitRootWindows(p); + pos = current.length; + + // On windows, check that the root exists. On unix there is no need. + if (!knownHard.$has(base)) { + lastStat = fs.lstatSync(base, { throwIfNoEntry: true }); + if (lastStat === undefined) return; + knownHard.$add(base); + } + } + + return encodeRealpathResult(p, encoding); + }; +const realpath: any = + process.platform !== "win32" + ? function realpath(p, options, callback) { + if ($isCallable(options)) { + callback = options; + options = undefined; + } + ensureCallback(callback); + + fs.realpath(p, options, false).then(function (resolvedPath) { + callback(null, resolvedPath); + }, callback); + } + : function (p, options, callback) { + if ($isCallable(options)) { + callback = options; + options = undefined; + } + ensureCallback(callback); + let encoding; + if (options) { + if (typeof options === "string") encoding = options; + else encoding = options?.encoding; + } + p = getValidatedPath(p); + + const knownHard = new Set(); + const pathModule = require("node:path"); + + // Current character position in p + let pos; + // The partial path so far, including a trailing slash if any + let current; + // The partial path without a trailing slash (except when pointing at a root) + let base; + // The partial path scanned in the previous round, with slash + let previous; + + current = base = splitRootWindows(p); + pos = current.length; + + let lastStat!: StatsType; + + // On windows, check that the root exists. On unix there is no need. + if (!knownHard.has(base)) { + lstat(base, (err, s) => { + lastStat = s; + if (err) return callback(err); + knownHard.add(base); + LOOP(); + }); + } else { + process.nextTick(LOOP); + } + + // Walk down the path, swapping out linked path parts for their real + // values + function LOOP() { + while (true) { + // Stop if scanned past end of path + if (pos >= p.length) { + return callback(null, encodeRealpathResult(p, encoding)); + } + + // find the next part + const result = nextPartWindows(p, pos); + previous = current; + if (result === -1) { + const last = p.slice(pos); + current += last; + base = previous + last; + pos = p.length; + } else { + current += p.slice(pos, result + 1); + base = previous + p.slice(pos, result); + pos = result + 1; + } + + // Continue if not a symlink, break if a pipe/socket + if (knownHard.has(base)) { + if (lastStat.isFIFO() || lastStat.isSocket()) { + return callback(null, encodeRealpathResult(p, encoding)); + } + continue; + } + + return lstat(base, { bigint: true }, gotStat); + } + } + + function gotStat(err, stats) { + if (err) return callback(err); + + // If not a symlink, skip to the next path part + if (!stats.isSymbolicLink()) { + knownHard.add(base); + return process.nextTick(LOOP); + } + + // Stat & read the link if not read before. + // Call `gotTarget()` as soon as the link target is known. + // `dev`/`ino` always return 0 on windows, so skip the check. + stat(base, (err, s) => { + if (err) return callback(err); + lastStat = s; + + readlink(base, (err, target) => { + gotTarget(err, target); + }); + }); + } + + function gotTarget(err, target) { + if (err) return callback(err); + gotResolvedLink(pathModule.resolve(previous, target)); + } + + function gotResolvedLink(resolvedLink) { + // Resolve the link, then start over + p = pathModule.resolve(resolvedLink, p.slice(pos)); + current = base = splitRootWindows(p); + pos = current.length; + + // On windows, check that the root exists. On unix there is no need. + if (!knownHard.has(base)) { + lstat(base, err => { + if (err) return callback(err); + knownHard.add(base); + LOOP(); + }); + } else { + process.nextTick(LOOP); + } + } + }; realpath.native = function realpath(p, options, callback) { if ($isCallable(options)) { callback = options; diff --git a/src/output.zig b/src/output.zig index 20abdd9cde..2d08b3f613 100644 --- a/src/output.zig +++ b/src/output.zig @@ -1045,11 +1045,9 @@ pub inline fn warn(comptime fmt: []const u8, args: anytype) void { prettyErrorln("warn: " ++ fmt, args); } -const debugWarnScope = Scoped("debug_warn", false); - /// Print a yellow warning message, only in debug mode pub inline fn debugWarn(comptime fmt: []const u8, args: anytype) void { - if (debugWarnScope.isVisible()) { + if (bun.Environment.isDebug) { prettyErrorln("debug warn: " ++ fmt, args); flush(); } diff --git a/src/string_immutable.zig b/src/string_immutable.zig index b95ddb39f1..3b045b05cd 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -1916,19 +1916,7 @@ pub fn toNTPath(wbuf: []u16, utf8: []const u8) [:0]u16 { return wbuf[0 .. toWPathNormalized(wbuf[prefix.len..], utf8).len + prefix.len :0]; } -pub fn toNTMaxPath(buf: []u8, utf8: []const u8) [:0]const u8 { - if (!std.fs.path.isAbsoluteWindows(utf8) or utf8.len <= 260) { - @memcpy(buf[0..utf8.len], utf8); - buf[utf8.len] = 0; - return buf[0..utf8.len :0]; - } - - const prefix = bun.windows.nt_maxpath_prefix_u8; - buf[0..prefix.len].* = prefix; - return buf[0 .. toPathNormalized(buf[prefix.len..], utf8).len + prefix.len :0]; -} - -pub fn addNTPathPrefix(wbuf: []u16, utf16: []const u16) [:0]u16 { +fn addNTPathPrefix(wbuf: []u16, utf16: []const u16) [:0]u16 { wbuf[0..bun.windows.nt_object_prefix.len].* = bun.windows.nt_object_prefix; @memcpy(wbuf[bun.windows.nt_object_prefix.len..][0..utf16.len], utf16); wbuf[utf16.len + bun.windows.nt_object_prefix.len] = 0; diff --git a/src/sys.zig b/src/sys.zig index e463e9aa7c..6b46c0063a 100644 --- a/src/sys.zig +++ b/src/sys.zig @@ -147,7 +147,7 @@ pub const O = switch (Environment.os) { pub const CREAT = 0o100; pub const EXCL = 0o200; - pub const NOCTTY = 0o400; + pub const NOCTTY = 0; pub const TRUNC = 0o1000; pub const APPEND = 0o2000; pub const NONBLOCK = 0o4000; @@ -1315,6 +1315,126 @@ pub fn openFileAtWindowsNtPath( } } +// Delete: this doesnt apply to NtCreateFile :( +// pub const WindowsOpenFlags = struct { +// access: w.DWORD, +// share: w.DWORD, +// disposition: w.DWORD, +// attributes: w.DWORD, + +// pub fn fromLibUV(flags_in: c_int) error{EINVAL}!WindowsOpenFlags { +// const uv = bun.windows.libuv; + +// var flags = flags_in; + +// // Adjust flags to be compatible with the memory file mapping. Save the +// // original flags to emulate the correct behavior +// if (flags & uv.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; +// } +// } + +// var access_flag: w.DWORD = switch (flags & (uv.UV_FS_O_RDONLY | uv.UV_FS_O_WRONLY | uv.UV_FS_O_RDWR)) { +// uv.UV_FS_O_RDONLY => w.FILE_GENERIC_READ, +// uv.UV_FS_O_WRONLY => w.FILE_GENERIC_WRITE, +// uv.UV_FS_O_RDWR => w.FILE_GENERIC_READ | w.FILE_GENERIC_WRITE, +// else => return error.EINVAL, +// }; +// if (flags & O.APPEND != 0) { +// access_flag &= ~@as(u32, w.FILE_WRITE_DATA); +// access_flag |= w.FILE_APPEND_DATA; +// } +// access_flag |= w.SYNCHRONIZE; + +// const share: w.DWORD = if (flags & uv.UV_FS_O_EXLOCK != 0) 0 else FILE_SHARE; + +// const disposition: w.DWORD = switch (flags & uv.UV_FS_O_CREAT | uv.UV_FS_O_EXCL | uv.UV_FS_O_TRUNC) { +// 0, +// uv.UV_FS_O_EXCL, +// => w.OPEN_EXISTING, +// uv.UV_FS_O_CREAT, +// => w.OPEN_ALWAYS, +// uv.UV_FS_O_CREAT | uv.UV_FS_O_EXCL, +// uv.UV_FS_O_CREAT | uv.UV_FS_O_EXCL | uv.UV_FS_O_TRUNC, +// => w.CREATE_NEW, +// uv.UV_FS_O_TRUNC, +// uv.UV_FS_O_TRUNC | uv.UV_FS_O_EXCL, +// => w.TRUNCATE_EXISTING, +// uv.UV_FS_O_CREAT | uv.UV_FS_O_TRUNC, +// => w.TRUNCATE_EXISTING, +// else => return error.EINVAL, +// }; +// var attributes: w.DWORD = w.FILE_ATTRIBUTE_NORMAL; +// if (flags & uv.UV_FS_O_CREAT != 0) { +// // if (!((req->fs.info.mode & ~current_umask) & _S_IWRITE)) { +// } +// if (flags & uv.UV_FS_O_TEMPORARY != 0) { +// attributes |= w.FILE_DELETE_ON_CLOSE; +// access_flag |= w.DELETE; +// } +// if (flags & uv.UV_FS_O_SHORT_LIVED != 0) { +// attributes |= w.FILE_ATTRIBUTE_TEMPORARY; +// } + +// switch (flags & (uv.UV_FS_O_SEQUENTIAL | uv.UV_FS_O_RANDOM)) { +// 0 => {}, +// uv.UV_FS_O_SEQUENTIAL => attributes |= w.FILE_FLAG_SEQUENTIAL_SCAN, +// uv.UV_FS_O_RANDOM => attributes |= w.FILE_FLAG_SEQUENTIAL_SCAN, +// else => return error.EINVAL, +// } + +// if (flags & uv.UV_FS_O_DIRECT != 0) { +// // FILE_APPEND_DATA and FILE_FLAG_NO_BUFFERING are mutually exclusive. +// // Windows returns 87, ERROR_INVALID_PARAMETER if these are combined. +// // +// // FILE_APPEND_DATA is included in FILE_GENERIC_WRITE: +// // +// // FILE_GENERIC_WRITE = STANDARD_RIGHTS_WRITE | +// // FILE_WRITE_DATA | +// // FILE_WRITE_ATTRIBUTES | +// // FILE_WRITE_EA | +// // FILE_APPEND_DATA | +// // SYNCHRONIZE +// // +// // Note: Appends are also permitted by FILE_WRITE_DATA. +// // +// // In order for direct writes and direct appends to succeed, we therefore +// // exclude FILE_APPEND_DATA if FILE_WRITE_DATA is specified, and otherwise +// // fail if the user's sole permission is a direct append, since this +// // particular combination is invalid. +// if (access_flag & w.FILE_APPEND_DATA != 0) { +// if (access_flag & w.FILE_WRITE_DATA != 0) { +// access_flag &= @as(u32, w.FILE_APPEND_DATA); +// } else { +// return error.EINVAL; +// } +// } +// attributes |= w.FILE_FLAG_NO_BUFFERING; +// } + +// switch (flags & uv.UV_FS_O_DSYNC | uv.UV_FS_O_SYNC) { +// 0 => {}, +// else => attributes |= w.FILE_FLAG_WRITE_THROUGH, +// } + +// // Setting this flag makes it possible to open a directory. +// attributes |= w.FILE_FLAG_BACKUP_SEMANTICS; + +// return .{ +// .access = access_flag, +// .share = share, +// .disposition = disposition, +// .attributes = attributes, +// }; +// } +// }; + pub fn openFileAtWindowsT( comptime T: type, dirFd: bun.FileDescriptor, @@ -1358,18 +1478,7 @@ 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_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; - } - } +fn openatWindowsTMaybeNormalize(comptime T: type, dir: bun.FileDescriptor, path: []const T, flags: bun.Mode, comptime normalize: bool) Maybe(bun.FileDescriptor) { if (flags & O.DIRECTORY != 0) { const windows_options: WindowsOpenDirOptions = .{ .iterable = flags & O.PATH == 0, @@ -1403,8 +1512,6 @@ 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) { @@ -1511,6 +1618,20 @@ pub fn openat(dirfd: bun.FileDescriptor, file_path: [:0]const u8, flags: bun.Mod } } +pub fn openatFileWithLibuvFlags(dirfd: bun.FileDescriptor, file_path: [:0]const u8, flags: bun.JSC.Node.FileSystemFlags, perm: bun.Mode) Maybe(bun.FileDescriptor) { + if (comptime Environment.isWindows) { + const f = flags.toWindows() catch return .{ .err = .{ + .errno = @intFromEnum(bun.C.E.INVAL), + .syscall = .open, + .path = file_path, + } }; + // TODO: pass f.share + return openFileAtWindowsT(u8, dirfd, file_path, f.access, f.disposition, f.attributes); + } else { + return openatOSPath(dirfd, file_path, flags.asPosix(), perm); + } +} + pub fn openatA(dirfd: bun.FileDescriptor, file_path: []const u8, flags: bun.Mode, perm: bun.Mode) Maybe(bun.FileDescriptor) { if (comptime Environment.isWindows) { return openatWindowsT(u8, dirfd, file_path, flags); @@ -1537,13 +1658,15 @@ pub fn openA(file_path: []const u8, flags: bun.Mode, perm: bun.Mode) Maybe(bun.F } pub fn open(file_path: [:0]const u8, flags: bun.Mode, perm: bun.Mode) Maybe(bun.FileDescriptor) { - // TODO(@paperdave): this should not need to use libuv + // TODO(@paperclover): this should not use libuv; when the libuv path is + // removed here, the call sites in node_fs.zig should make sure they parse + // the libuv specific file flags using the WindowsOpenFlags structure. if (comptime Environment.isWindows) { return sys_uv.open(file_path, flags, perm); } // this is what open() does anyway. - return openat(bun.toFD((std.fs.cwd().fd)), file_path, flags, perm); + return openat(bun.toFD(std.posix.AT.FDCWD), file_path, flags, perm); } /// This function will prevent stdout and stderr from being closed.