diff --git a/src/bun.js/bindings/Path.cpp b/src/bun.js/bindings/Path.cpp index 4cd252c3292e33..d0764909a828d2 100644 --- a/src/bun.js/bindings/Path.cpp +++ b/src/bun.js/bindings/Path.cpp @@ -122,9 +122,7 @@ JSC_DEFINE_HOST_FUNCTION(Path_functionResolve, JSC_DEFINE_HOST_FUNCTION(Path_functionToNamespacedPath, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { - auto argCount = static_cast(callFrame->argumentCount()); - // TODO: - return JSC::JSValue::encode(callFrame->argument(0)); + DEFINE_CALLBACK_FUNCTION_BODY(Bun__Path__toNamespacedPath); } static JSC::JSObject* createPath(JSGlobalObject* globalThis, bool isWindows) diff --git a/src/bun.js/bindings/headers.h b/src/bun.js/bindings/headers.h index 526732facb1421..355ec21ca547cd 100644 --- a/src/bun.js/bindings/headers.h +++ b/src/bun.js/bindings/headers.h @@ -596,6 +596,7 @@ ZIG_DECL JSC__JSValue Bun__Path__normalize(JSC__JSGlobalObject* arg0, bool arg1, ZIG_DECL JSC__JSValue Bun__Path__parse(JSC__JSGlobalObject* arg0, bool arg1, JSC__JSValue* arg2, uint16_t arg3); ZIG_DECL JSC__JSValue Bun__Path__relative(JSC__JSGlobalObject* arg0, bool arg1, JSC__JSValue* arg2, uint16_t arg3); ZIG_DECL JSC__JSValue Bun__Path__resolve(JSC__JSGlobalObject* arg0, bool arg1, JSC__JSValue* arg2, uint16_t arg3); +ZIG_DECL JSC__JSValue Bun__Path__toNamespacedPath(JSC__JSGlobalObject* arg0, bool arg1, JSC__JSValue* arg2, uint16_t arg3); #endif diff --git a/src/bun.js/node/types.zig b/src/bun.js/node/types.zig index 7dc0946cc6e858..6a8526c5fd65ef 100644 --- a/src/bun.js/node/types.zig +++ b/src/bun.js/node/types.zig @@ -1,40 +1,50 @@ const std = @import("std"); const builtin = @import("builtin"); const bun = @import("root").bun; +const heap_allocator = bun.default_allocator; +const is_bindgen: bool = std.meta.globalOption("bindgen", bool) orelse false; +const logger = bun.logger; +const Fs = @import("../../fs.zig"); +const IdentityContext = @import("../../identity_context.zig").IdentityContext; +const URL = @import("../../url.zig").URL; +const Shimmer = @import("../bindings/shimmer.zig").Shimmer; +const meta = bun.meta; +const os = std.os; +const path_handler = bun.path; const strings = bun.strings; const string = bun.string; -const JSC = @import("root").bun.JSC; -const PathString = JSC.PathString; -const Environment = bun.Environment; +const validators = @import("./util/validators.zig"); +const validateObject = validators.validateObject; +const validateString = validators.validateString; + const C = bun.C; +const PathString = bun.PathString; +const Environment = bun.Environment; +const JSC = bun.JSC; +const Mode = bun.Mode; +const ObjectMap = std.json.ObjectMap; +const PathBuffer = bun.PathBuffer; const Syscall = bun.sys; -const os = std.os; +const Value = std.json.Value; + +const stack_fallback_size_small = 4096; // up to 4 KB on the stack +const stack_fallback_size_large = 32 * @sizeOf(string); // up to 32 strings on the stack + pub const Buffer = JSC.MarkedArrayBuffer; -const IdentityContext = @import("../../identity_context.zig").IdentityContext; -const logger = @import("root").bun.logger; -const Fs = @import("../../fs.zig"); -const URL = @import("../../url.zig").URL; -const Shimmer = @import("../bindings/shimmer.zig").Shimmer; -const is_bindgen: bool = std.meta.globalOption("bindgen", bool) orelse false; -const resolve_path = @import("../../resolver/resolve_path.zig"); -const meta = bun.meta; /// On windows, this is what libuv expects /// On unix it is what the utimens api expects pub const TimeLike = if (Environment.isWindows) f64 else std.os.timespec; -const Mode = bun.Mode; -const heap_allocator = bun.default_allocator; - pub const Flavor = enum { sync, promise, callback, - pub fn Wrap(comptime this: Flavor, comptime Type: type) type { + pub fn Wrap(comptime this: Flavor, comptime T: type) type { return comptime brk: { switch (this) { - .sync => break :brk Type, + .sync => break :brk T, // .callback => { // const Callback = CallbackTask(Type); // }, @@ -520,7 +530,6 @@ pub const Encoding = enum(u8) { .buffer => { return JSC.ArrayBuffer.createBuffer(globalThis, input); }, - inline else => |enc| { const res = JSC.WebCore.Encoder.toString(input.ptr, size, globalThis, enc); if (res.isError()) { @@ -560,7 +569,6 @@ pub const Encoding = enum(u8) { }, inline else => |enc| { const res = JSC.WebCore.Encoder.toString(input.ptr, input.len, globalThis, enc); - if (res.isError()) { globalThis.throwValue(res); return .zero; @@ -597,7 +605,7 @@ pub fn CallbackTask(comptime Result: type) type { } pub const PathLike = union(enum) { - string: bun.PathString, + string: PathString, buffer: Buffer, slice_with_underlying_string: bun.SliceWithUnderlyingString, threadsafe_string: bun.SliceWithUnderlyingString, @@ -654,12 +662,12 @@ pub const PathLike = union(enum) { }; } - pub fn sliceZWithForceCopy(this: PathLike, buf: *[bun.MAX_PATH_BYTES]u8, comptime force: bool) [:0]const u8 { + pub fn sliceZWithForceCopy(this: PathLike, buf: *PathBuffer, comptime force: bool) [:0]const u8 { const sliced = this.slice(); if (Environment.isWindows) { if (std.fs.path.isAbsolute(sliced)) { - return resolve_path.PosixToWinNormalizer.resolveCWDWithExternalBufZ(buf, sliced) catch @panic("Error while resolving path."); + return path_handler.PosixToWinNormalizer.resolveCWDWithExternalBufZ(buf, sliced) catch @panic("Error while resolving path."); } } @@ -676,15 +684,22 @@ pub const PathLike = union(enum) { return buf[0..sliced.len :0]; } - pub inline fn sliceZ(this: PathLike, buf: *[bun.MAX_PATH_BYTES]u8) [:0]const u8 { + pub inline fn sliceZ(this: PathLike, buf: *PathBuffer) [:0]const u8 { + if (comptime Environment.isWindows) { + const data = this.slice(); + if (!std.fs.path.isAbsolute(data)) { + return sliceZWithForceCopy(this, buf, false); + } + return path_handler.PosixToWinNormalizer.resolveCWDWithExternalBufZ(buf, data) catch @panic("Error while resolving path."); + } return sliceZWithForceCopy(this, buf, false); } - pub inline fn sliceW(this: PathLike, buf: *[bun.MAX_PATH_BYTES]u8) [:0]const u16 { + pub inline fn sliceW(this: PathLike, buf: *PathBuffer) [:0]const u16 { return bun.strings.toWPath(@alignCast(std.mem.bytesAsSlice(u16, buf)), this.slice()); } - pub inline fn osPath(this: PathLike, buf: *[bun.MAX_PATH_BYTES]u8) bun.OSPathSliceZ { + pub inline fn osPath(this: PathLike, buf: *PathBuffer) bun.OSPathSliceZ { if (comptime Environment.isWindows) { return sliceW(this, buf); } @@ -940,7 +955,7 @@ pub const VectorArrayBuffer = struct { pub const ArgumentsSlice = struct { remaining: []const JSC.JSValue, vm: *JSC.VirtualMachine, - arena: @import("root").bun.ArenaAllocator = @import("root").bun.ArenaAllocator.init(bun.default_allocator), + arena: bun.ArenaAllocator = bun.ArenaAllocator.init(bun.default_allocator), all: []const JSC.JSValue, threw: bool = false, protected: std.bit_set.IntegerBitSet(32) = std.bit_set.IntegerBitSet(32).initEmpty(), @@ -981,7 +996,7 @@ pub const ArgumentsSlice = struct { .remaining = arguments, .vm = vm, .all = arguments, - .arena = @import("root").bun.ArenaAllocator.init(vm.allocator), + .arena = bun.ArenaAllocator.init(vm.allocator), }; } @@ -1039,7 +1054,7 @@ pub fn timeLikeFromJS(globalThis: *JSC.JSGlobalObject, value: JSC.JSValue, _: JS return null; } - if (Environment.isWindows) { + if (comptime Environment.isWindows) { return milliseconds / 1000.0; } @@ -1058,7 +1073,7 @@ pub fn timeLikeFromJS(globalThis: *JSC.JSGlobalObject, value: JSC.JSValue, _: JS return null; } - if (Environment.isWindows) { + if (comptime Environment.isWindows) { return seconds; } @@ -1080,7 +1095,7 @@ pub fn modeFromJS(ctx: JSC.C.JSContextRef, value: JSC.JSValue, exception: JSC.C. // the example), specifies permissions for the group. The right-most // digit (5 in the example), specifies the permissions for others. - var zig_str = JSC.ZigString.init(""); + var zig_str = JSC.ZigString.Empty; value.toZigString(&zig_str, ctx.ptr()); var slice = zig_str.slice(); if (strings.hasPrefix(slice, "0o")) { @@ -1106,7 +1121,6 @@ pub const PathOrFileDescriptor = union(Tag) { path: PathLike, pub const Tag = enum { fd, path }; - pub const SerializeTag = enum(u8) { fd, path }; /// This will unref() the path string if it is a PathLike. @@ -1532,8 +1546,8 @@ pub fn StatType(comptime Big: bool) type { const cTime = stat_.ctime(); return .{ - .dev = if (Environment.isWindows) stat_.dev else @truncate(@as(i64, @intCast(stat_.dev))), - .ino = if (Environment.isWindows) stat_.ino else @truncate(@as(i64, @intCast(stat_.ino))), + .dev = if (comptime Environment.isWindows) stat_.dev else @truncate(@as(i64, @intCast(stat_.dev))), + .ino = if (comptime Environment.isWindows) stat_.ino else @truncate(@as(i64, @intCast(stat_.ino))), .mode = @truncate(@as(i64, @intCast(stat_.mode))), .nlink = @truncate(@as(i64, @intCast(stat_.nlink))), .uid = @truncate(@as(i64, @intCast(stat_.uid))), @@ -1835,548 +1849,2846 @@ pub const Emitter = struct { }; pub const Path = struct { + const CHAR_BACKWARD_SLASH = '\\'; + const CHAR_COLON = ':'; + const CHAR_DOT = '.'; + const CHAR_FORWARD_SLASH = '/'; + const CHAR_QUESTION_MARK = '?'; + + const CHAR_STR_BACKWARD_SLASH = "\\"; + const CHAR_STR_FORWARD_SLASH = "/"; + const CHAR_STR_DOT = "."; + + const StringBuilder = @import("../../string_builder.zig"); + + // tmpBuf1 and tmpBuf2 can be expanded by path.join and path.resolve. + var _tmpBuf1: PathBuffer = undefined; + var tmpBuf1: []u8 = &_tmpBuf1; + + var _tmpBuf2: PathBuffer = undefined; + var tmpBuf2: []u8 = &_tmpBuf2; + + var smallTmpBuf1: PathBuffer = undefined; + var smallTmpBuf2: PathBuffer = undefined; + var smallTmpBuf3: PathBuffer = undefined; + + /// Based on Node v21.6.1 path.parse: + /// https://github.com/nodejs/node/blob/6ae20aa63de78294b18d5015481485b7cd8fbb60/lib/path.js#L919 + /// The structs returned by parse methods. + const ParsedPath = struct { + root: []const u8, + dir: []const u8, + base: []const u8, + ext: []const u8, + name: []const u8, + }; + + const ParsedPathW = struct { + root: []const u16, + dir: []const u16, + base: []const u16, + ext: []const u16, + name: []const u16, + }; + + const ParsedPathUnion = union { + u8: ParsedPath, + u16: ParsedPathW, + }; + + pub const MAX_PATH_BYTES = bun.MAX_PATH_BYTES; pub const shim = Shimmer("Bun", "Path", @This()); pub const name = "Bun__Path"; pub const include = "Path.h"; pub const namespace = shim.namespace; - const PathHandler = @import("../../resolver/resolve_path.zig"); - const StringBuilder = @import("../../string_builder.zig"); - pub const code = @embedFile("../path.exports.js"); + pub const delimiter = CHAR_COLON; + pub const sep_posix = CHAR_FORWARD_SLASH; + pub const sep_windows = CHAR_BACKWARD_SLASH; + pub const sep_str_posix = CHAR_STR_FORWARD_SLASH; + pub const sep_str_windows = CHAR_STR_BACKWARD_SLASH; + + inline fn ParsedPathImpl(comptime T: type, root: []const T, dir: []const T, base: []const T, ext: []const T, _name: []const T) ParsedPathUnion { + const ParsedPathT = if (@TypeOf(T) != u16) ParsedPath else ParsedPathW; + const result = ParsedPathT{ .root = root, .dir = dir, .base = base, .ext = ext, .name = _name }; + return if (@TypeOf(T) != u16) ParsedPathUnion{ .u8 = result } else ParsedPathUnion{ .u16 = result }; + } - pub fn create(globalObject: *JSC.JSGlobalObject, isWindows: bool) callconv(.C) JSC.JSValue { - return shim.cppFn("create", .{ globalObject, isWindows }); + /// Taken from Zig 0.11.0 zig/src/resinator/rc.zig + /// https://github.com/ziglang/zig/blob/776cd673f206099012d789fd5d05d49dd72b9faa/src/resinator/rc.zig#L266 + /// + /// Compares ASCII values case-insensitively, non-ASCII values are compared directly + fn eqlIgnoreCaseImpl(comptime T: type, a: []const T, b: []const T) bool { + if (@TypeOf(T) != u16) { + return std.ascii.eqlIgnoreCase(a, b); + } + if (a.len != b.len) return false; + for (a, b) |a_c, b_c| { + if (a_c < 128) { + if (std.ascii.toLower(@intCast(a_c)) != std.ascii.toLower(@intCast(b_c))) return false; + } else { + if (a_c != b_c) return false; + } + } + return true; } - pub fn basename(globalThis: *JSC.JSGlobalObject, isWindows: bool, args_ptr: [*]JSC.JSValue, args_len: u16) callconv(.C) JSC.JSValue { - if (comptime is_bindgen) return JSC.JSValue.jsUndefined(); - if (args_len == 0) { - return JSC.toInvalidArguments("path is required", .{}, globalThis); + /// Based on Node v21.6.1 private helper formatExt: + /// https://github.com/nodejs/node/blob/6ae20aa63de78294b18d5015481485b7cd8fbb60/lib/path.js#L130C10-L130C19 + inline fn formatExtImpl(comptime T: type, ext: []const T, buf: []T) []const T { + const len = ext.len; + if (len == 0) { + return ""; } - var stack_fallback = std.heap.stackFallback(4096, JSC.getAllocator(globalThis)); - const allocator = stack_fallback.get(); + if (ext[0] == CHAR_DOT) { + return ext; + } + const bufSize = len + 1; + buf[0] = CHAR_DOT; + @memcpy(buf[1 ..bufSize], ext); + return buf[0 ..bufSize]; + } - var arguments: []JSC.JSValue = args_ptr[0..args_len]; - var path = arguments[0].toSlice(globalThis, allocator); + inline fn getCwdImpl(comptime T: type) []const T { + const cwd = Fs.FileSystem.instance.top_level_dir; + return if (@TypeOf(T) == u16) std.unicode.utf8ToUtf16LeStringLiteral(cwd) else cwd; + } - defer path.deinit(); - var extname_ = if (args_len > 1) arguments[1].toSlice(globalThis, allocator) else JSC.ZigString.Slice.empty; - defer extname_.deinit(); + /// Caller must free returned memory. + fn getBufAlloc(allocator: std.mem.Allocator, len: usize) ![]u8 { + if (len > MAX_PATH_BYTES) { + const maybeBuf: ?[]u8 = allocator.alloc(u8, len) catch null; + const maybeTmpBuf1: ?[]u8 = allocator.alloc(u8, len) catch null; + const maybeTmpBuf2: ?[]u8 = allocator.alloc(u8, len) catch null; + if (maybeBuf == null or maybeTmpBuf1 == null or maybeTmpBuf2 == null) { + if (maybeBuf != null) allocator.free(maybeBuf.?); + if (maybeTmpBuf1 != null) allocator.free(maybeTmpBuf1.?); + if (maybeTmpBuf2 != null) allocator.free(maybeTmpBuf2.?); + return error.OutOfMemory; + } + tmpBuf1 = maybeTmpBuf1.?; + tmpBuf2 = maybeTmpBuf2.?; + return maybeBuf.?; + } + return allocator.alloc(u8, MAX_PATH_BYTES) catch unreachable; + } - const base_slice = path.slice(); - var out: []const u8 = base_slice; + /// Taken from Zig 0.11.0 zig/lib/std/process.zig + /// https://github.com/ziglang/zig/blob/776cd673f206099012d789fd5d05d49dd72b9faa/lib/std/process.zig#L363 + /// + /// Caller must free returned memory. + fn getEnvVarOwnedW(allocator: std.mem.Allocator, key_w: []const u8) std.process.GetEnvVarOwnedError![]u8 { + const result_w = blk: { + break :blk std.os.getenvW(key_w) orelse return error.EnvironmentVariableNotFound; + }; + return std.unicode.utf16leToUtf8Alloc(allocator, result_w) catch |err| switch (err) { + error.DanglingSurrogateHalf => return error.InvalidUtf8, + error.ExpectedSecondSurrogateHalf => return error.InvalidUtf8, + error.UnexpectedSecondSurrogateHalf => return error.InvalidUtf8, + else => |e| return e, + }; + } - if (!isWindows) { - out = std.fs.path.basenamePosix(base_slice); - } else { - out = std.fs.path.basenameWindows(base_slice); + /// Based on Node v21.6.1 private helper posixCwd: + /// https://github.com/nodejs/node/blob/6ae20aa63de78294b18d5015481485b7cd8fbb60/lib/path.js#L1074 + inline fn posixCwdImpl(comptime T: type, buf: []T) []const T { + const cwd = Fs.FileSystem.instance.top_level_dir; + const len = cwd.len; + if (len == 0) { + return ""; } - const ext = extname_.slice(); - - if ((ext.len != out.len or out.len == base_slice.len) and strings.endsWith(out, ext)) { - out = out[0 .. out.len - ext.len]; + if (comptime Environment.isWindows) { + // Converts Windows' backslash path separators to POSIX forward slashes + // and truncates any drive indicator + for (0.. len) |i| { + if (cwd[i] == CHAR_BACKWARD_SLASH) { + buf[i] = CHAR_FORWARD_SLASH; + } else { + buf[i] = cwd[i]; + } + } + var normalizedCwd = buf[0 ..len]; + + // Translated from the following JS code: + // const cwd = StringPrototypeReplace(process.cwd(), regexp, '/'); + // return StringPrototypeSlice(cwd, StringPrototypeIndexOf(cwd, '/')); + const index = std.mem.indexOfScalar(u8, normalizedCwd, CHAR_FORWARD_SLASH); + // Account for the -1 case of String#slice in JS land + const trimmed = if (index != null) normalizedCwd[index.? ..len] else normalizedCwd[len - 1 ..len]; + return if (@TypeOf(T) == u16) std.unicode.utf8ToUtf16LeStringLiteral(trimmed) else trimmed; } - return JSC.ZigString.init(out).withEncoding().toValueGC(globalThis); + // We're already on POSIX, no need for any transformations + return if (@TypeOf(T) == u16) std.unicode.utf8ToUtf16LeStringLiteral(cwd) else cwd; } - fn dirnameWindows(path: []const u8) []const u8 { - if (path.len == 0) - return "."; - - const root_slice = std.fs.path.diskDesignatorWindows(path); - if (path.len == root_slice.len) - return root_slice; - - const have_root_slash = path.len > root_slice.len and (path[root_slice.len] == '/' or path[root_slice.len] == '\\'); + /// Taken from Zig 0.11.0 zig/src/resinator/rc.zig + /// https://github.com/ziglang/zig/blob/776cd673f206099012d789fd5d05d49dd72b9faa/src/resinator/rc.zig#L266 + /// + /// Lowers ASCII values, non-ASCII values are returned directly + inline fn toLowerImpl(comptime T: type, a_c: T) T { + if (@TypeOf(T) != u16) { + return std.ascii.toLower(a_c); + } + return if (a_c < 128) @intCast(std.ascii.toLower(@intCast(a_c))) else a_c; + } - var end_index: usize = path.len - 1; + /// Based on Node v21.6.1 path.posix.basename: + /// https://github.com/nodejs/node/blob/6ae20aa63de78294b18d5015481485b7cd8fbb60/lib/path.js#L1309 + pub fn basenamePosixImpl(comptime T: type, path: []const T, suffix: ?[]const T) []const T { + // validateString of `path` is performed in fn pub basename. + const len = path.len; + // Exit early for easier number type use. + if (len == 0) { + return ""; + } + var start: usize = 0; + // We use an optional value instead of -1, as in Node code, for easier number type use. + var end: ?usize = null; + var matchedSlash: bool = true; + + if (suffix != null and suffix.?.len > 0 and suffix.?.len <= len) { + const suffix_not_null = suffix.?; + if (std.mem.eql(T, suffix_not_null, path)) { + return ""; + } + // We use an optional value instead of -1, as in Node code, for easier number type use. + var extIdx: ?usize = suffix_not_null.len - 1; + // We use an optional value instead of -1, as in Node code, for easier number type use. + var firstNonSlashEnd: ?usize = null; + var i: usize = len - 1; + while (i >= 0) : (i -= if (i > 0) 1 else break) { + const byte = path[i]; + if (byte == CHAR_FORWARD_SLASH) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + start = i + 1; + break; + } + } else { + if (firstNonSlashEnd == null) { + // We saw the first non-path separator, remember this index in case + // we need it if the extension ends up not matching + matchedSlash = false; + firstNonSlashEnd = i + 1; + } + if (extIdx != null) { + // Try to match the explicit extension + if (byte == suffix_not_null[extIdx.?]) { + if (extIdx.? == 0) { + // We matched the extension, so mark this as the end of our path + // component + end = i; + extIdx = null; + } else { + extIdx = extIdx.? - 1; + } + } else { + // Extension does not match, so our result is the entire path + // component + extIdx = null; + end = firstNonSlashEnd; + } + } + } + } - while (path[end_index] == '/' or path[end_index] == '\\') { - // e.g. '\\' => "\\" - if (end_index == 0) { - return path[0..1]; + if (end == null) { + end = len; + } else if (start == end.?) { + end = firstNonSlashEnd; } - end_index -= 1; + + return path[start ..end.?]; } - while (path[end_index] != '/' and path[end_index] != '\\') { - if (end_index == 0) { - if (root_slice.len == 0) { - return "."; - } - if (have_root_slash) { - // e.g. "c:\\" => "c:\\" - return path[0 .. root_slice.len + 1]; - } else { - // e.g. "c:foo" => "c:" - return root_slice; + // Add a block to isolate `i`. + { + var i: usize = len - 1; + while (i >= 0) : (i -= if (i > 0) 1 else break) { + const byte = path[i]; + if (byte == CHAR_FORWARD_SLASH) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + start = i + 1; + break; + } + } else if (end == null) { + // We saw the first non-path separator, mark this as the end of our + // path component + matchedSlash = false; + end = i + 1; } } - end_index -= 1; } - if (have_root_slash and end_index == root_slice.len) { - end_index += 1; + if (end == null) { + return ""; } - return path[0..end_index]; + return path[start ..end.?]; } - fn dirnamePosix(path: []const u8) []const u8 { - if (path.len == 0) - return "."; + /// Based on Node v21.6.1 path.win32.basename: + /// https://github.com/nodejs/node/blob/6ae20aa63de78294b18d5015481485b7cd8fbb60/lib/path.js#L753 + pub fn basenameWindowsImpl(comptime T: type, path: []const T, suffix: ?[]const T) []const T { + // validateString of `path` is performed in fn pub basename. + const len = path.len; + // Exit early for easier number type use. + if (len == 0) { + return ""; + } - var end_index: usize = path.len - 1; + const isSepImpl = @This().isSepWindowsImpl; - while (path[end_index] == '/') { - // e.g. "////" => "/" - if (end_index == 0) { - return "/"; - } - end_index -= 1; + var start: usize = 0; + // We use an optional value instead of -1, as in Node code, for easier number type use. + var end: ?usize = null; + var matchedSlash: bool = true; + + // Check for a drive letter prefix so as not to mistake the following + // path separator as an extra separator at the end of the path that can be + // disregarded + if (len >= 2 and @This().isWindowsDeviceRootImpl(T, path[0]) and path[1] == CHAR_COLON) { + start = 2; } - while (path[end_index] != '/') { - if (end_index == 0) { - // e.g. "a/", "a" - return "."; + if (suffix != null and suffix.?.len > 0 and suffix.?.len <= len) { + const suffix_not_null = suffix.?; + if (std.mem.eql(T, suffix_not_null, path)) { + return ""; + } + // We use an optional value instead of -1, as in Node code, for easier number type use. + var extIdx: ?usize = suffix_not_null.len - 1; + // We use an optional value instead of -1, as in Node code, for easier number type use. + var firstNonSlashEnd: ?usize = null; + var i: usize = len - 1; + while (i >= start) : (i -= if (i > 0) 1 else break) { + const byte = path[i]; + if (isSepImpl(T, byte)) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + start = i + 1; + break; + } + } else { + if (firstNonSlashEnd == null) { + // We saw the first non-path separator, remember this index in case + // we need it if the extension ends up not matching + matchedSlash = false; + firstNonSlashEnd = i + 1; + } + if (extIdx != null) { + // Try to match the explicit extension + if (byte == suffix_not_null[extIdx.?]) { + if (extIdx.? == 0) { + // We matched the extension, so mark this as the end of our path + // component + end = i; + extIdx = null; + } else { + extIdx = extIdx.? - 1; + } + } else { + // Extension does not match, so our result is the entire path + // component + extIdx = null; + end = firstNonSlashEnd; + } + } + } + } + + if (end == null) { + end = len; + } else if (start == end.?) { + end = firstNonSlashEnd; } - end_index -= 1; - } - // e.g. "/a/" => "/" - if (end_index == 0 and path[0] == '/') { - return "/"; + return path[start ..end.?]; } - // "a/b" => "a" or "//b" => "//" - if (end_index <= 1) { - if (path[0] == '/' and path[1] == '/') { - end_index += 1; + // Add a block to isolate `i`. + { + var i: usize = len - 1; + while (i >= start) : (i -= if (i > 0) 1 else break) { + const byte = path[i]; + if (isSepImpl(T, byte)) { + if (!matchedSlash) { + start = i + 1; + break; + } + } else if (end == null) { + matchedSlash = false; + end = i + 1; + } } } - return path[0..end_index]; - } - - pub fn dirname(globalThis: *JSC.JSGlobalObject, isWindows: bool, args_ptr: [*]JSC.JSValue, args_len: u16) callconv(.C) JSC.JSValue { - if (comptime is_bindgen) return JSC.JSValue.jsUndefined(); - if (args_len == 0) { - return JSC.toInvalidArguments("path is required", .{}, globalThis); + if (end == null) { + return ""; } - var stack_fallback = std.heap.stackFallback(4096, JSC.getAllocator(globalThis)); - const allocator = stack_fallback.get(); - - var arguments: []JSC.JSValue = args_ptr[0..args_len]; - var path = arguments[0].toSlice(globalThis, allocator); - defer path.deinit(); - const base_slice = path.slice(); + return path[start ..end.?]; + } - const out = if (isWindows) - @This().dirnameWindows(base_slice) - else - @This().dirnamePosix(base_slice); + pub inline fn basenamePosix(path: []const u8, suffix: ?[]const u8) []const u8 { + return basenamePosixImpl(u8, path, suffix); + } - return JSC.ZigString.init(out).withEncoding().toValueGC(globalThis); + pub inline fn basenameWindows(path: []const u8, suffix: ?[]const u8) []const u8 { + return basenameWindowsImpl(u8, path, suffix); } - pub fn extname(globalThis: *JSC.JSGlobalObject, _: bool, args_ptr: [*]JSC.JSValue, args_len: u16) callconv(.C) JSC.JSValue { + pub fn basename(globalThis: *JSC.JSGlobalObject, isWindows: bool, args_ptr: [*]JSC.JSValue, args_len: u16) callconv(.C) JSC.JSValue { if (comptime is_bindgen) return JSC.JSValue.jsUndefined(); - if (args_len == 0) { - return JSC.toInvalidArguments("path is required", .{}, globalThis); + const suffix_ptr: ?JSC.JSValue = if (args_len > 1) args_ptr[1] else null; + + if (suffix_ptr != null) { + // Supress exeption in zig. It does throw in JS land. + validateString(globalThis, suffix_ptr.?, "ext", .{}) catch { + return JSC.JSValue.jsUndefined(); + }; } - var stack_fallback = std.heap.stackFallback(4096, JSC.getAllocator(globalThis)); + + const path_ptr = if (args_len > 0) args_ptr[0] else JSC.JSValue.jsUndefined(); + // Supress exeption in zig. It does throw in JS land. + validateString(globalThis, path_ptr, "path", .{}) catch { + return JSC.JSValue.jsUndefined(); + }; + + var stack_fallback = std.heap.stackFallback( + stack_fallback_size_small, + JSC.getAllocator(globalThis) + ); const allocator = stack_fallback.get(); - var arguments: []JSC.JSValue = args_ptr[0..args_len]; - var path = arguments[0].toSlice(globalThis, allocator); + var path = path_ptr.toSlice(globalThis, allocator); defer path.deinit(); - const base_slice = path.slice(); + if (suffix_ptr == null) { + const out = + if (!isWindows) + @This().basenamePosix(path.slice(), null) + else + @This().basenameWindows(path.slice(), null); + return JSC.ZigString.init(out).withEncoding().toValueGC(globalThis); + } - return JSC.ZigString.init(std.fs.path.extension(base_slice)).withEncoding().toValueGC(globalThis); + var suffix = suffix_ptr.?.toSlice(globalThis, allocator); + defer suffix.deinit(); + const out = + if (!isWindows) + @This().basenamePosix(path.slice(), suffix.slice()) + else + @This().basenameWindows(path.slice(), suffix.slice()); + return JSC.ZigString.init(out).withEncoding().toValueGC(globalThis); } - pub fn format(globalThis: *JSC.JSGlobalObject, isWindows: bool, args_ptr: [*]JSC.JSValue, args_len: u16) callconv(.C) JSC.JSValue { - if (comptime is_bindgen) return JSC.JSValue.jsUndefined(); - if (args_len == 0) { - return JSC.toInvalidArguments("pathObject is required", .{}, globalThis); - } - var path_object: JSC.JSValue = args_ptr[0]; - const js_type = path_object.jsType(); - if (!js_type.isObject()) { - return JSC.toInvalidArguments("pathObject is required", .{}, globalThis); - } - - var stack_fallback = std.heap.stackFallback(4096, JSC.getAllocator(globalThis)); - var allocator = stack_fallback.get(); - var dir = JSC.ZigString.Empty; - var name_ = JSC.ZigString.Empty; - var ext = JSC.ZigString.Empty; - var name_with_ext = JSC.ZigString.Empty; + pub fn create(globalObject: *JSC.JSGlobalObject, isWindows: bool) callconv(.C) JSC.JSValue { + return shim.cppFn("create", .{ globalObject, isWindows }); + } - var insert_separator = true; - if (path_object.getTruthy(globalThis, "dir")) |prop| { - prop.toZigString(&dir, globalThis); + /// Based on Node v21.6.1 path.posix.dirname: + /// https://github.com/nodejs/node/blob/6ae20aa63de78294b18d5015481485b7cd8fbb60/lib/path.js#L1278 + pub fn dirnamePosixImpl(comptime T: type, path: []const T) []const T { + // validateString of `path` is performed in fn pub dirname. + const len = path.len; + if (len == 0) { + return CHAR_STR_DOT; } - if (dir.isEmpty()) { - if (path_object.getTruthy(globalThis, "root")) |prop| { - prop.toZigString(&dir, globalThis); + + const hasRoot = path[0] == CHAR_FORWARD_SLASH; + // We use an optional value instead of -1, as in Node code, for easier number type use. + var end: ?usize = null; + var matchedSlash: bool = true; + var i: usize = len - 1; + while (i >= 1) : (i -= 1) { + if (path[i] == CHAR_FORWARD_SLASH) { + if (!matchedSlash) { + end = i; + break; + } + } else { + // We saw the first non-path separator + matchedSlash = false; } } - if (path_object.getTruthy(globalThis, "base")) |prop| { - prop.toZigString(&name_with_ext, globalThis); + if (end == null) { + return if (hasRoot) CHAR_STR_FORWARD_SLASH else CHAR_STR_DOT; } - if (name_with_ext.isEmpty()) { - var had_ext = false; - if (path_object.getTruthy(globalThis, "ext")) |prop| { - prop.toZigString(&ext, globalThis); - had_ext = !ext.isEmpty(); - } - if (path_object.getTruthy(globalThis, "name")) |prop| { - if (had_ext) { - prop.toZigString(&name_, globalThis); - } else { - prop.toZigString(&name_with_ext, globalThis); - } - } + if (hasRoot and end != null and end.? == 1) { + return "//"; } - if (dir.isEmpty()) { - if (!name_with_ext.isEmpty()) { - return name_with_ext.toValueAuto(globalThis); - } + return path[0 ..end.?]; + } - if (name_.isEmpty()) { - return JSC.ZigString.Empty.toValue(globalThis); - } + /// Based on Node v21.6.1 path.win32.dirname: + /// https://github.com/nodejs/node/blob/6ae20aa63de78294b18d5015481485b7cd8fbb60/lib/path.js#L657 + pub fn dirnameWindowsImpl(comptime T: type, path: []const T) []const T { + // validateString of `path` is performed in fn pub dirname. + const len = path.len; + if (len == 0) { + return CHAR_STR_DOT; + } - const out = std.fmt.allocPrint(allocator, "{s}{s}", .{ name_, ext }) catch unreachable; - defer allocator.free(out); + const isSepImpl = @This().isSepWindowsImpl; - return JSC.ZigString.init(out).withEncoding().toValueGC(globalThis); - } else { - if (!isWindows) { - if (dir.eqlComptime("/")) { - insert_separator = false; - } - } else { - if (dir.eqlComptime("\\")) { - insert_separator = false; - } - } + // We use an optional value instead of -1, as in Node code, for easier number type use. + var rootEnd: ?usize = null; + var offset: usize = 0; + const byte0 = path[0]; + + if (len == 1) { + // `path` contains just a path separator, exit early to avoid + // unnecessary work or a dot. + return if (isSepImpl(T, byte0)) path else CHAR_STR_DOT; } - if (insert_separator) { - const separator = if (!isWindows) "/" else "\\"; - if (name_with_ext.isEmpty()) { - const out = std.fmt.allocPrint(allocator, "{}{s}{}{}", .{ dir, separator, name_, ext }) catch unreachable; - defer allocator.free(out); - return JSC.ZigString.init(out).withEncoding().toValueGC(globalThis); - } + // Try to match a root + if (isSepImpl(T, byte0)) { + // Possible UNC root - { - const out = std.fmt.allocPrint(allocator, "{}{s}{}", .{ - dir, - separator, - name_with_ext, - }) catch unreachable; - defer allocator.free(out); - return JSC.ZigString.init(out).withEncoding().toValueGC(globalThis); - } - } + rootEnd = 1; + offset = 1; - if (name_with_ext.isEmpty()) { - const out = std.fmt.allocPrint(allocator, "{}{}{}", .{ dir, name_, ext }) catch unreachable; - defer allocator.free(out); - return JSC.ZigString.init(out).withEncoding().toValueGC(globalThis); - } + if (isSepImpl(T, path[1])) { + // Matched double path separator at the beginning + var j: usize = 2; + var last: usize = j; - { - const out = std.fmt.allocPrint(allocator, "{}{}", .{ - dir, - name_with_ext, - }) catch unreachable; - defer allocator.free(out); - return JSC.ZigString.init(out).withEncoding().toValueGC(globalThis); - } - } + // Match 1 or more non-path separators + while (j < len and !isSepImpl(T, path[j])) { + j += 1; + } - fn isAbsoluteString(path: JSC.ZigString, windows: bool) bool { - if (!windows) return path.hasPrefixChar('/'); - return isZigStringAbsoluteWindows(path); - } + if (j < len and j != last) { + // Matched! + last = j; - pub fn isAbsolute(globalThis: *JSC.JSGlobalObject, isWindows: bool, args_ptr: [*]JSC.JSValue, args_len: u16) callconv(.C) JSC.JSValue { - if (comptime is_bindgen) return JSC.JSValue.jsUndefined(); - const arg = if (args_len > 0) args_ptr[0] else JSC.JSValue.undefined; - if (!arg.isString()) { - globalThis.throwInvalidArgumentType("isAbsolute", "path", "string"); - return JSC.JSValue.undefined; - } - const zig_str = arg.getZigString(globalThis); - return JSC.JSValue.jsBoolean(zig_str.len > 0 and isAbsoluteString(zig_str, isWindows)); - } + // Match 1 or more path separators + while (j < len and isSepImpl(T, path[j])) { + j += 1; + } - fn isZigStringAbsoluteWindows(zig_str: JSC.ZigString) bool { - std.debug.assert(zig_str.len > 0); // caller must check - if (zig_str.is16Bit()) { - var buf = [4]u16{ 0, 0, 0, 0 }; - const u16_slice = zig_str.utf16Slice(); + if (j < len and j != last) { + // Matched! + last = j; - buf[0] = u16_slice[0]; - if (u16_slice.len > 1) - buf[1] = u16_slice[1]; + // Match 1 or more non-path separators + while (j < len and !isSepImpl(T, path[j])) { + j += 1; + } - if (u16_slice.len > 2) - buf[2] = u16_slice[2]; + if (j == len) { + // We matched a UNC root only + return path; + } - if (u16_slice.len > 3) - buf[3] = u16_slice[3]; + if (j != last) { + // We matched a UNC root with leftovers - return std.fs.path.isAbsoluteWindowsWTF16(buf[0..@min(u16_slice.len, buf.len)]); + // Offset by 1 to include the separator after the UNC root to + // treat it as a "normal root" on top of a (UNC) root + rootEnd = j + 1; + offset = rootEnd.?; + } + } + } + } + // Possible device root + } else if (@This().isWindowsDeviceRootImpl(T, byte0) and path[1] == CHAR_COLON) { + rootEnd = if (len > 2 and isSepImpl(T, path[2])) 3 else 2; + offset = rootEnd.?; } - return std.fs.path.isAbsoluteWindows(zig_str.slice()); - } - pub fn join( - globalThis: *JSC.JSGlobalObject, - isWindows: bool, - args_ptr: [*]JSC.JSValue, - args_len: u16, - ) callconv(.C) JSC.JSValue { - if (comptime is_bindgen) return JSC.JSValue.jsUndefined(); - if (args_len == 0) return JSC.ZigString.init(".").toValue(globalThis); - var arena = @import("root").bun.ArenaAllocator.init(heap_allocator); - defer arena.deinit(); - - const arena_allocator = arena.allocator(); - var stack_fallback_allocator = std.heap.stackFallback( - ((32 * @sizeOf(string)) + 1024), - arena_allocator, - ); - var allocator = stack_fallback_allocator.get(); + // We use an optional value instead of -1, as in Node code, for easier number type use. + var end: ?usize = null; + var matchedSlash: bool = true; - var buf: [bun.MAX_PATH_BYTES]u8 = undefined; - var count: usize = 0; - var i: u16 = 0; - var to_join = allocator.alloc(string, args_len) catch unreachable; - for (args_ptr[0..args_len]) |arg| { - const zig_str: JSC.ZigString = arg.getZigString(globalThis); - // Windows path joining code expects the first path to exist - // to be used for UNC path detection. - if (zig_str.len > 0) { - to_join[i] = zig_str.toSlice(allocator).slice(); - count += to_join[i].len; - i += 1; + var i: usize = len - 1; + while (i >= offset) : (i -= if (i > 0) 1 else break) { + if (isSepImpl(T, path[i])) { + if (!matchedSlash) { + end = i; + break; + } + } else { + // We saw the first non-path separator + matchedSlash = false; } } - if (count == 0) return JSC.ZigString.init(".").toValue(globalThis); + if (end == null) { + if (rootEnd == null) { + return CHAR_STR_DOT; + } - var buf_to_use: []u8 = &buf; - if (count * 2 >= buf.len) { - buf_to_use = allocator.alloc(u8, count * 2) catch { - globalThis.throwOutOfMemory(); - return .zero; - }; + end = rootEnd.?; } - const out = if (!isWindows) - PathHandler.joinStringBuf(buf_to_use, to_join[0..i], .posix) - else - PathHandler.joinStringBuf(buf_to_use, to_join[0..i], .windows); + return path[0 ..end.?]; + } - var str = bun.String.createUTF8(out); - defer str.deref(); - return str.toJS(globalThis); + pub inline fn dirnamePosix(path: []const u8) []const u8 { + return @This().dirnamePosixImpl(u8, path); } - pub fn normalize(globalThis: *JSC.JSGlobalObject, isWindows: bool, args_ptr: [*]JSC.JSValue, args_len: u16) callconv(.C) JSC.JSValue { + pub inline fn dirnameWindows(path: []const u8) []const u8 { + return @This().dirnameWindowsImpl(u8, path); + } + + pub fn dirname(globalThis: *JSC.JSGlobalObject, isWindows: bool, args_ptr: [*]JSC.JSValue, args_len: u16) callconv(.C) JSC.JSValue { if (comptime is_bindgen) return JSC.JSValue.jsUndefined(); - if (args_len == 0) return JSC.ZigString.init("").toValue(globalThis); + const path_ptr = if (args_len > 0) args_ptr[0] else JSC.JSValue.jsUndefined(); + // Supress exeption in zig. It does throw in JS land. + validateString(globalThis, path_ptr, "path", .{}) catch { + return JSC.JSValue.jsUndefined(); + }; - var zig_str: JSC.ZigString = args_ptr[0].getZigString(globalThis); - if (zig_str.len == 0) return JSC.ZigString.init(".").toValue(globalThis); + var stack_fallback = std.heap.stackFallback( + stack_fallback_size_small, + JSC.getAllocator(globalThis) + ); + const allocator = stack_fallback.get(); - var buf: [bun.MAX_PATH_BYTES]u8 = undefined; - var str_slice = zig_str.toSlice(heap_allocator); - defer str_slice.deinit(); - const str = str_slice.slice(); + var path = path_ptr.toSlice(globalThis, allocator); + defer path.deinit(); const out = if (!isWindows) - PathHandler.normalizeStringNode(str, &buf, .posix) + @This().dirnamePosix(path.slice()) else - PathHandler.normalizeStringNode(str, &buf, .windows); - - var out_str = JSC.ZigString.init(out); - if (str_slice.isAllocated()) out_str.setOutputEncoding(); - return out_str.toValueGC(globalThis); + @This().dirnameWindows(path.slice()); + return JSC.ZigString.init(out).withEncoding().toValueGC(globalThis); } + + /// Based on Node v21.6.1 path.posix.extname: + /// https://github.com/nodejs/node/blob/6ae20aa63de78294b18d5015481485b7cd8fbb60/lib/path.js#L1278 + pub fn extnamePosixImpl(comptime T: type, path: []const T) []const T { + // validateString of `path` is performed in fn pub extname. + const len = path.len; + // Exit early for easier number type use. + if (len == 0) { + return ""; + } + // We use an optional value instead of -1, as in Node code, for easier number type use. + var startDot: ?usize = null; + var startPart: usize = 0; + // We use an optional value instead of -1, as in Node code, for easier number type use. + var end: ?usize = null; + var matchedSlash: bool = true; + // Track the state of characters (if any) we see before our first dot and + // after any path separator we find + // + // We use an optional value instead of -1, as in Node code, for easier number type use. + var preDotState: ?usize = 0; + + var i: usize = len - 1; + while (i >= 0) : (i -= if (i > 0) 1 else break) { + const byte = path[i]; + if (byte == CHAR_FORWARD_SLASH) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + startPart = i + 1; + break; + } + continue; + } + + if (end == null) { + // We saw the first non-path separator, mark this as the end of our + // extension + matchedSlash = false; + end = i + 1; + } + + if (byte == CHAR_DOT) { + // If this is our first dot, mark it as the start of our extension + if (startDot == null) { + startDot = i; + } else if (preDotState != null and preDotState.? != 1) { + preDotState = 1; + } + } else if (startDot != null) { + // We saw a non-dot and non-path separator before our dot, so we should + // have a good chance at having a non-empty extension + preDotState = null; + } + } - pub fn parse(globalThis: *JSC.JSGlobalObject, win32: bool, args_ptr: [*]JSC.JSValue, args_len: u16) callconv(.C) JSC.JSValue { - return switch (win32) { - inline else => |use_win32| parseWithComptimePlatform(globalThis, use_win32, args_ptr, args_len), - }; + if (startDot == null or + end == null or + // We saw a non-dot character immediately before the dot + (preDotState != null and preDotState.? == 0) or + // The (right-most) trimmed path component is exactly '..' + (preDotState != null and preDotState.? == 1 and + startDot.? == end.? - 1 and + startDot.? == startPart + 1) + ) { + return ""; + } + + return path[startDot.? ..end.?]; } - pub fn parseWithComptimePlatform(globalThis: *JSC.JSGlobalObject, comptime win32: bool, args_ptr: [*]JSC.JSValue, args_len: u16) JSC.JSValue { - if (comptime is_bindgen) return JSC.JSValue.jsUndefined(); - if (args_len == 0 or !args_ptr[0].jsType().isStringLike()) { - return JSC.toInvalidArguments("path string is required", .{}, globalThis); + /// Based on Node v21.6.1 path.win32.extname: + /// https://github.com/nodejs/node/blob/6ae20aa63de78294b18d5015481485b7cd8fbb60/lib/path.js#L840 + pub fn extnameWindowsImpl(comptime T: type, path: []const T) []const T { + // validateString of `path` is performed in fn pub extname. + const len = path.len; + // Exit early for easier number type use. + if (len == 0) { + return ""; + } + var start: usize = 0; + // We use an optional value instead of -1, as in Node code, for easier number type use. + var startDot: ?usize = null; + var startPart: usize = 0; + // We use an optional value instead of -1, as in Node code, for easier number type use. + var end: ?usize = null; + var matchedSlash: bool = true; + // Track the state of characters (if any) we see before our first dot and + // after any path separator we find + // + // We use an optional value instead of -1, as in Node code, for easier number type use. + var preDotState: ?usize = 0; + + // Check for a drive letter prefix so as not to mistake the following + // path separator as an extra separator at the end of the path that can be + // disregarded + + if (len >= 2 and + path[1] == CHAR_COLON and + @This().isWindowsDeviceRootImpl(T, path[0])) { + start = 2; + startPart = start; } - var path_slice: JSC.ZigString.Slice = args_ptr[0].toSlice(globalThis, heap_allocator); - defer path_slice.deinit(); - var path = path_slice.slice(); - - const is_absolute = switch (win32) { - true => std.fs.path.isAbsoluteWindows(path), - false => std.fs.path.isAbsolutePosix(path), - }; - // if its not absolute root must be empty - var root = JSC.ZigString.Empty; - if (is_absolute) { - std.debug.assert(path.len > 0); - root = JSC.ZigString.init( - if (win32) root: { - // On Win32, the root is a substring of the input, containing just the root dir. Aka: - // - Unix Absolute path - // "\" or "/" - // - Drive letter - // "C:\" or "C:" if no slash - // - UNC paths must start with \\ and then include another \ somewhere - // they can also use forward slashes anywhere - // "\\server\share" - // "//server/share" - // "/\server\share" lol - // "\\?\" lol - if (path.len > 0 and strings.charIsAnySlash(path[0])) { - // minimum length for a unc path is 5 - if (path.len >= 5 and - strings.charIsAnySlash(path[1]) and - !strings.charIsAnySlash(path[2])) - { - if (strings.indexOfAny(path[3..], "/\\")) |first_slash| { - if (strings.indexOfAny(path[3 + first_slash + 1 ..], "/\\")) |second_slash| { - const len = 3 + 1 + first_slash + second_slash; - // case given for input "//hello/world/" - // this is not considered a unc path - if (path.len > len) { - break :root path[0 .. len + 1]; - } - } - } - } - // return the un-normalized slash - break :root path[0..1]; - } - if (path.len > 2 and path[1] == ':') { - // would not be an absolute path if it was just "C:" - std.debug.assert(strings.charIsAnySlash(path[2])); - break :root path[0..3]; - } - break :root path[0..1]; - } else - // Unix does not make it possible to have a root that isnt `/` - std.fs.path.sep_str_posix, - ); - } else if (win32) { - if (path.len > 1 and path[1] == ':') { - // for input "C:hello" which is not considered absolute - comptime std.debug.assert(!std.fs.path.isAbsoluteWindows("C:hello")); - comptime std.debug.assert(std.fs.path.isAbsoluteWindows("/:/")); - root = JSC.ZigString.init(path[0..2]); + var i: usize = len - 1; + while (i >= start) : (i -= if (i > 0) 1 else break) { + const byte = path[i]; + if (@This().isSepWindowsImpl(T, byte)) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + startPart = i + 1; + break; + } + continue; + } + if (end == null) { + // We saw the first non-path separator, mark this as the end of our + // extension + matchedSlash = false; + end = i + 1; + } + if (byte == CHAR_DOT) { + // If this is our first dot, mark it as the start of our extension + if (startDot == null) { + startDot = i; + } else if (preDotState != null and preDotState.? != 1) { + preDotState = 1; + } + } else if (startDot != null) { + // We saw a non-dot and non-path separator before our dot, so we should + // have a good chance at having a non-empty extension + preDotState = null; } } - const path_name = Fs.NodeJSPathName.init( - if (win32) path[root.len..] else path, - win32, - ); - var dir = JSC.ZigString.init(path_name.dir); - - // if is absolute and dir is empty, then dir = root - if (is_absolute and path_name.dir.len == 0) { - dir = root; + if (startDot == null or + end == null or + // We saw a non-dot character immediately before the dot + (preDotState != null and preDotState.? == 0) or + // The (right-most) trimmed path component is exactly '..' + (preDotState != null and preDotState.? == 1 and + startDot.? == end.? - 1 and + startDot.? == startPart + 1) + ) { + return ""; } - var base = JSC.ZigString.init(path_name.base); - var name_ = JSC.ZigString.init(path_name.filename); - var ext = JSC.ZigString.init(path_name.ext); - dir.setOutputEncoding(); - root.setOutputEncoding(); - base.setOutputEncoding(); - name_.setOutputEncoding(); - ext.setOutputEncoding(); - - var result = JSC.JSValue.createEmptyObject(globalThis, 5); - result.put(globalThis, JSC.ZigString.static("root"), root.toValueGC(globalThis)); - result.put(globalThis, JSC.ZigString.static("dir"), dir.toValueGC(globalThis)); - result.put(globalThis, JSC.ZigString.static("base"), base.toValueGC(globalThis)); - result.put(globalThis, JSC.ZigString.static("ext"), ext.toValueGC(globalThis)); - result.put(globalThis, JSC.ZigString.static("name"), name_.toValueGC(globalThis)); - return result; + return path[startDot.? ..end.?]; + } + + pub inline fn extnamePosix(path: []const u8) []const u8 { + return @This().extnamePosixImpl(u8, path); } - pub fn relative(globalThis: *JSC.JSGlobalObject, isWindows: bool, args_ptr: [*]JSC.JSValue, args_len: u16) callconv(.C) JSC.JSValue { - if (comptime is_bindgen) return JSC.JSValue.jsUndefined(); - var arguments = args_ptr[0..args_len]; - if (args_len > 1 and JSC.JSValue.eqlValue(args_ptr[0], args_ptr[1])) - return JSC.ZigString.init("").toValue(globalThis); + pub inline fn extnameWindows(path: []const u8) []const u8 { + return @This().extnameWindowsImpl(u8, path); + } - var from_slice: JSC.ZigString.Slice = if (args_len > 0) arguments[0].toSlice(globalThis, heap_allocator) else JSC.ZigString.Slice.empty; - defer from_slice.deinit(); - var to_slice: JSC.ZigString.Slice = if (args_len > 1) arguments[1].toSlice(globalThis, heap_allocator) else JSC.ZigString.Slice.empty; - defer to_slice.deinit(); + pub fn extname(globalThis: *JSC.JSGlobalObject, isWindows: bool, args_ptr: [*]JSC.JSValue, args_len: u16) callconv(.C) JSC.JSValue { + if (comptime is_bindgen) return JSC.JSValue.jsUndefined(); + const path_ptr = if (args_len > 0) args_ptr[0] else JSC.JSValue.jsUndefined(); + // Supress exeption in zig. It does throw in JS land. + validateString(globalThis, path_ptr, "path", .{}) catch { + return JSC.JSValue.jsUndefined(); + }; + + var stack_fallback = std.heap.stackFallback( + stack_fallback_size_small, + JSC.getAllocator(globalThis) + ); + const allocator = stack_fallback.get(); - const from = from_slice.slice(); - const to = to_slice.slice(); + var path = path_ptr.toSlice(globalThis, allocator); + defer path.deinit(); const out = if (!isWindows) - PathHandler.relativePlatform(from, to, .posix, true) + @This().extnamePosix(path.slice()) else - PathHandler.relativePlatform(from, to, .windows, true); - - var out_str = JSC.ZigString.init(out); - if (from_slice.isAllocated() or to_slice.isAllocated()) out_str.setOutputEncoding(); - return out_str.toValueGC(globalThis); + @This().extnameWindows(path.slice()); + return JSC.ZigString.init(out).withEncoding().toValueGC(globalThis); } - pub fn resolve(globalThis: *JSC.JSGlobalObject, isWindows: bool, args_ptr: [*]JSC.JSValue, args_len: u16) callconv(.C) JSC.JSValue { + /// Based on Node v21.6.1 private helper _format: + /// https://github.com/nodejs/node/blob/6ae20aa63de78294b18d5015481485b7cd8fbb60/lib/path.js#L145 + fn _format(comptime T: type, root: []const T, dir: []const T, base: []const T, ext: []const T, _name: []const T, sep: T, buf: []T) []const T { + // validateObject of `pathObject` is performed in fn pub parse. + + // Translated from the following JS code: + // const dir = pathObject.dir || pathObject.root; + const dirOrRoot = if (dir.len > 0) dir else root; + const dirLen = dirOrRoot.len; + + var bufSize: usize = 0; + + // Translated from the following JS code: + // const base = pathObject.base || + // `${pathObject.name || ''}${formatExt(pathObject.ext)}`; + var baseLen = base.len; + var baseOrNameExt = base; + if (baseLen > 0) { + @memcpy(buf[0 ..baseLen], base); + } else { + const formattedExt = @This().formatExtImpl(T, ext, buf); + const nameLen = _name.len; + const extLen = formattedExt.len; + if (extLen > 0) { + // Move all bytes to the right by nameLen + var i: usize = extLen - 1; + while (i >= 0) : (i -= if (i > 0) 1 else break) { + buf[i + nameLen] = formattedExt[i]; + } + } + if (nameLen > 0) { + @memcpy(buf[0 ..nameLen], _name); + } + bufSize = nameLen + extLen; + if (bufSize > 0) { + baseOrNameExt = buf[0 ..bufSize]; + } + } + + // Translated from the following JS code: + // if (!dir) { + // return base; + // } + if (dirLen == 0) { + return baseOrNameExt; + } + + // Translated from the following JS code: + // return dir === pathObject.root ? `${dir}${base}` : `${dir}${sep}${base}`; + const dirIsRoot = std.mem.eql(u8, dirOrRoot, root); + baseLen = baseOrNameExt.len; + if (baseLen > 0) { + const moveBy = if (dirIsRoot) dirLen else dirLen + 1; + // Move all bytes to the right by baseOffset + var i: usize = baseLen - 1; + while (i >= 0) : (i -= if (i > 0) 1 else break) { + buf[i + moveBy] = baseOrNameExt[i]; + } + } + @memcpy(buf[0 ..dirLen], dirOrRoot); + bufSize = dirLen + baseLen; + if (!dirIsRoot) { + bufSize += 1; + buf[dirLen] = sep; + } + return buf[0 ..bufSize]; + } + + pub inline fn formatPosix(root: []const u8, dir: []const u8, base: []const u8, ext: []const u8, _name: []const u8, buf: []u8) []const u8 { + return @This()._format(u8, root, dir, base, ext, _name, CHAR_FORWARD_SLASH, buf); + } + + pub inline fn formatWindow(root: []const u8, dir: []const u8, base: []const u8, ext: []const u8, _name: []const u8, buf: []u8) []const u8 { + return @This()._format(u8, root, dir, base, ext, _name, CHAR_BACKWARD_SLASH, buf); + } + + pub fn format(globalThis: *JSC.JSGlobalObject, isWindows: bool, args_ptr: [*]JSC.JSValue, args_len: u16) callconv(.C) JSC.JSValue { if (comptime is_bindgen) return JSC.JSValue.jsUndefined(); + const pathObject_ptr = if (args_len > 0) args_ptr[0] else JSC.JSValue.jsUndefined(); + // Supress exeption in zig. It does throw in JS land. + validateObject(globalThis, pathObject_ptr, "pathObject", .{}, .{}) catch { + return JSC.JSValue.jsUndefined(); + }; - var stack_fallback_allocator = std.heap.stackFallback( - (32 * @sizeOf(string)), - heap_allocator, + var stack_fallback = std.heap.stackFallback( + stack_fallback_size_small, + JSC.getAllocator(globalThis) ); - var allocator = stack_fallback_allocator.get(); - var out_buf: [bun.MAX_PATH_BYTES * 2]u8 = undefined; + const allocator = stack_fallback.get(); + + var lenSum: usize = 0; + + var root: []const u8 = ""; + if (pathObject_ptr.getTruthy(globalThis, "root")) |jsValue| { + root = jsValue.toSlice(globalThis, allocator).slice(); + } + var dir: []const u8 = ""; + if (pathObject_ptr.getTruthy(globalThis, "dir")) |jsValue| { + dir = jsValue.toSlice(globalThis, allocator).slice(); + } + + lenSum += dir.len; + + var base: []const u8 = ""; + if (pathObject_ptr.getTruthy(globalThis, "base")) |jsValue| { + base = jsValue.toSlice(globalThis, allocator).slice(); + lenSum += base.len; + } + // Prefix with _ to avoid shadowing the identifier in the outer scope. + var _name: []const u8 = ""; + if (pathObject_ptr.getTruthy(globalThis, "name")) |jsValue| { + _name = jsValue.toSlice(globalThis, allocator).slice(); + if (base.len == 0) lenSum += _name.len; + } + var ext: []const u8 = ""; + if (pathObject_ptr.getTruthy(globalThis, "ext")) |jsValue| { + ext = jsValue.toSlice(globalThis, allocator).slice(); + if (base.len == 0) lenSum += ext.len; + } + + // Add one for the possible separator. + lenSum += 1; + + const buf = getBufAlloc(allocator, lenSum) catch { + // Supress exeption in zig. It does throw in JS land. + globalThis.throwOutOfMemory(); + return JSC.JSValue.jsUndefined(); + }; + defer allocator.free(buf); + + const out = if (!isWindows) + @This().formatPosix(root, dir, base, ext, _name, buf) + else + @This().formatWindow(root, dir, base, ext, _name, buf); + return JSC.ZigString.init(out).withEncoding().toValueGC(globalThis); + } - var parts = allocator.alloc(string, args_len) catch unreachable; - defer allocator.free(parts); + /// Based on Node v21.6.1 path.posix.isAbsolute: + /// https://github.com/nodejs/node/blob/6ae20aa63de78294b18d5015481485b7cd8fbb60/lib/path.js#L1159 + pub inline fn isAbsolutePosixImpl(comptime T: type, path: []const T) bool { + // validateString of `path` is performed in fn pub isAbsolute. + return path.len > 0 and path[0] == CHAR_FORWARD_SLASH; + } + + /// Based on Node v21.6.1 path.win32.isAbsolute: + /// https://github.com/nodejs/node/blob/6ae20aa63de78294b18d5015481485b7cd8fbb60/lib/path.js#L406 + pub fn isAbsoluteWindowsImpl(comptime T: type, path: []const T) bool { + // validateString of `path` is performed in fn pub isAbsolute. + const len = path.len; + if (len == 0) + return false; + + const byte0 = path[0]; + return @This().isSepWindowsImpl(T, byte0) or + // Possible device root + (len > 2 and + @This().isWindowsDeviceRootImpl(T, byte0) and + path[1] == CHAR_COLON and + @This().isSepWindowsImpl(T, path[2])); + } + + pub inline fn isAbsolutePosix(path: []const u8) bool { + return @This().isAbsolutePosixImpl(u8, path); + } + + pub fn isAbsoluteWindowsZigString(zig_str: JSC.ZigString) bool { + if (zig_str.len > 0 and zig_str.is16Bit()) { + var buf = [4]u16{ 0, 0, 0, 0 }; + const u16_slice = zig_str.utf16Slice(); + const u16_len = u16_slice.len; + + buf[0] = u16_slice[0]; + if (u16_len > 1) + buf[1] = u16_slice[1]; + + if (u16_len > 2) + buf[2] = u16_slice[2]; + + if (u16_len > 3) + buf[3] = u16_slice[3]; + + return @This().isAbsoluteWindowsImpl(u16, buf[0 ..@min(u16_len, buf.len)]); + } + + return @This().isAbsoluteWindowsImpl(u8, zig_str.slice()); + } + + pub fn isAbsolute(globalThis: *JSC.JSGlobalObject, isWindows: bool, args_ptr: [*]JSC.JSValue, args_len: u16) callconv(.C) JSC.JSValue { + if (comptime is_bindgen) return JSC.JSValue.jsUndefined(); + const path_ptr = if (args_len > 0) args_ptr[0] else JSC.JSValue.jsUndefined(); + // Supress exeption in zig. It does throw in JS land. + validateString(globalThis, path_ptr, "path", .{}) catch { + return JSC.JSValue.jsUndefined(); + }; + + var pathZigStr = path_ptr.getZigString(globalThis); + if (!isWindows) { + var stack_fallback = std.heap.stackFallback( + stack_fallback_size_small, + JSC.getAllocator(globalThis) + ); + const allocator = stack_fallback.get(); + var path = pathZigStr.toSlice(allocator); + defer path.deinit(); + return JSC.JSValue.jsBoolean(@This().isAbsolutePosix(path.slice())); + } + return JSC.JSValue.jsBoolean(@This().isAbsoluteWindowsZigString(pathZigStr)); + } + + pub inline fn isSepPosixImpl(comptime T: type, byte: T) bool { + return byte == CHAR_FORWARD_SLASH; + } + + pub inline fn isSepWindowsImpl(comptime T: type, byte: T) bool { + return byte == CHAR_FORWARD_SLASH or byte == CHAR_BACKWARD_SLASH; + } + + /// Based on Node v21.6.1 private helper isWindowsDeviceRoot: + /// https://github.com/nodejs/node/blob/6ae20aa63de78294b18d5015481485b7cd8fbb60/lib/path.js#L60C10-L60C29 + inline fn isWindowsDeviceRootImpl(comptime T: type, byte: T) bool { + return (byte >= 'A' and byte <= 'Z') or (byte >= 'a' and byte <= 'z'); + } + + /// Based on Node v21.6.1 path.posix.join: + /// https://github.com/nodejs/node/blob/6ae20aa63de78294b18d5015481485b7cd8fbb60/lib/path.js#L1169 + pub inline fn joinPosixImpl(comptime T: type, paths: []const []const T, buf: []T) []const T { + if (paths.len == 0) { + return CHAR_STR_DOT; + } + + var bufSize: usize = 0; + var bufOffset: usize = 0; + + for (paths) |path| { + // validateString of `path is performed in fn pub join. + const len = path.len; + if (len > 0) { + // Translated from the following JS code: + // if (joined === undefined) + // joined = arg; + // else + // joined += `/${arg}`; + if (bufSize != 0) { + bufOffset = bufSize; + bufSize += 1; + tmpBuf1[bufOffset] = CHAR_FORWARD_SLASH; + } + bufOffset = bufSize; + bufSize += len; + @memcpy(tmpBuf1[bufOffset ..bufSize], path); + } + } + if (bufSize == 0) { + return CHAR_STR_DOT; + } + return @This().normalizePosixImpl(T, tmpBuf1[0.. bufSize], buf); + } + + /// Based on Node v21.6.1 path.win32.join: + /// https://github.com/nodejs/node/blob/6ae20aa63de78294b18d5015481485b7cd8fbb60/lib/path.js#L425 + pub fn joinWindowsImpl(comptime T: type, paths: []const []const T, buf: []T) []const T { + if (paths.len == 0) { + return CHAR_STR_DOT; + } + + const isSepImpl = @This().isSepWindowsImpl; + + var bufSize: usize = 0; + var bufOffset: usize = 0; + + var joined: []const T = ""; + var firstPart: []const T = ""; + + for (paths) |path| { + // validateString of `path` is performed in fn pub join. + const len = path.len; + if (len > 0) { + // Translated from the following JS code: + // if (joined === undefined) + // joined = firstPart = arg; + // else + // joined += `\\${arg}`; + bufOffset = bufSize; + if (bufSize == 0) { + bufSize = len; + @memcpy(tmpBuf1[0 ..bufSize], path); + + joined = tmpBuf1[0 ..bufSize]; + firstPart = joined; + } else { + bufOffset = bufSize; + bufSize += 1; + tmpBuf1[bufOffset] = CHAR_BACKWARD_SLASH; + bufOffset = bufSize; + bufSize += len; + @memcpy(tmpBuf1[bufOffset ..bufSize], path); + + joined = tmpBuf1[0 ..bufSize]; + } + } + } + if (bufSize == 0) { + return CHAR_STR_DOT; + } + + // Make sure that the joined path doesn't start with two slashes, because + // normalize() will mistake it for a UNC path then. + // + // This step is skipped when it is very clear that the user actually + // intended to point at a UNC path. This is assumed when the first + // non-empty string arguments starts with exactly two slashes followed by + // at least one more non-slash character. + // + // Note that for normalize() to treat a path as a UNC path it needs to + // have at least 2 components, so we don't filter for that here. + // This means that the user can use join to construct UNC paths from + // a server name and a share name; for example: + // path.join('//server', 'share') -> '\\\\server\\share\\') + var needsReplace: bool = true; + var slashCount: usize = 0; + if (isSepImpl(T, firstPart[0])) { + slashCount += 1; + const firstLen = firstPart.len; + if (firstLen > 1 and + isSepImpl(T, firstPart[1])) { + slashCount += 1; + if (firstLen > 2) { + if (isSepImpl(T, firstPart[2])) { + slashCount += 1; + } else { + // We matched a UNC path in the first part + needsReplace = false; + } + } + } + } + if (needsReplace) { + // Find any more consecutive slashes we need to replace + while (slashCount < bufSize and + isSepImpl(T, joined[slashCount])) { + slashCount += 1; + } + // Replace the slashes if needed + if (slashCount >= 2) { + // Translated from the following JS code: + // joined = `\\${StringPrototypeSlice(joined, slashCount)}`; + const sliceLen = bufSize - slashCount; + const sliceOffset = slashCount - 1; + var i: usize = slashCount; + while (i < bufSize) : (i += 1) { + tmpBuf1[i - sliceOffset] = tmpBuf1[i]; + } + // Prepend the separator. + tmpBuf1[0] = CHAR_BACKWARD_SLASH; + bufSize = sliceLen + 1; + + joined = tmpBuf1[0 ..bufSize]; + } + } + return @This().normalizeWindowsImpl(T, joined, buf); + } + + pub inline fn joinPosix(paths: []const []const u8, buf: []u8) []const u8 { + return @This().joinPosixImpl(u8, paths, buf); + } + + pub inline fn joinWindows(paths: []const []const u8, buf: []u8) []const u8 { + return @This().joinWindowsImpl(u8, paths, buf); + } + + pub fn join(globalThis: *JSC.JSGlobalObject, isWindows: bool, args_ptr: [*]JSC.JSValue, args_len: u16) callconv(.C) JSC.JSValue { + if (comptime is_bindgen) return JSC.JSValue.jsUndefined(); + if (args_len == 0) return JSC.ZigString.initUTF8(CHAR_STR_DOT).toValueGC(globalThis); var arena = bun.ArenaAllocator.init(heap_allocator); - const arena_allocator = arena.allocator(); defer arena.deinit(); + const arena_allocator = arena.allocator(); + + var stack_fallback = std.heap.stackFallback( + stack_fallback_size_large, + JSC.getAllocator(globalThis) + ); + var allocator = stack_fallback.get(); + + var paths = allocator.alloc(string, args_len) catch unreachable; + defer allocator.free(paths); + + // Adding 2 bytes when Windows for the possible UNC root, i.e. "\\\\", addition. + var lenSum: usize = if (isWindows) 2 else 0; + for (0 ..args_len) |i| { + const path_ptr = args_ptr[i]; + // Supress exeption in zig. It does throw in JS land. + validateString(globalThis, path_ptr, "paths[{d}]", .{i}) catch { + return JSC.JSValue.jsUndefined(); + }; + const path = path_ptr.toSlice(globalThis, arena_allocator).slice(); + const len = path.len; + paths[i] = path; + // Add 1 for the separator. + lenSum += if (len > 0 and lenSum > 0) len + 1 else len; + } + + const buf = getBufAlloc(allocator, lenSum) catch { + // Supress exeption in zig. It does throw in JS land. + globalThis.throwOutOfMemory(); + return JSC.JSValue.jsUndefined(); + }; + defer allocator.free(buf); + + const out = if (!isWindows) + @This().joinPosix(paths, buf) + else + @This().joinWindows(paths, buf); + return JSC.ZigString.init(out).withEncoding().toValueGC(globalThis); + } + + /// Based on Node v21.6.1 private helper normalizeString: + /// https://github.com/nodejs/node/blob/6ae20aa63de78294b18d5015481485b7cd8fbb60/lib/path.js#L65C1-L66C77 + /// + /// Resolves . and .. elements in a path with directory names + fn normalizeStringImpl( + comptime T: type, + path: []const T, + allowAboveRoot: bool, + separator: T, + comptime platform: path_handler.Platform, + buf: []T + ) []const T { + const len = path.len; + const isSepImpl = + if (platform == .posix) + @This().isSepPosixImpl + else + @This().isSepWindowsImpl; + + var bufOffset: usize = 0; + var bufSize: usize = 0; + + var res: []const T = ""; + var lastSegmentLength: usize = 0; + // We use an optional value instead of -1, as in Node code, for easier number type use. + var lastSlash: ?usize = null; + // We use an optional value instead of -1, as in Node code, for easier number type use. + var dots: ?usize = 0; + var byte: T = 0; + + var i: usize = 0; + while (i <= len) : (i += 1) { + if (i < len) { + byte = path[i]; + } else if (isSepImpl(T, byte)) { + break; + } else { + byte = CHAR_FORWARD_SLASH; + } + + if (isSepImpl(T, byte)) { + // Translated from the following JS code: + // if (lastSlash === i - 1 || dots === 1) { + if ( + (lastSlash == null and i == 0) or + (lastSlash != null and i > 0 and lastSlash.? == i - 1) or + (dots != null and dots.? == 1)) { + // NOOP + } else if (dots != null and dots.? == 2) { + if ( + bufSize < 2 or + lastSegmentLength != 2 or + buf[bufSize - 1] != CHAR_DOT or + buf[bufSize - 2] != CHAR_DOT + ) { + if (bufSize > 2) { + const lastSlashIndex = std.mem.lastIndexOfScalar(T, buf[0 ..bufSize], separator); + if (lastSlashIndex == null) { + res = ""; + bufSize = 0; + lastSegmentLength = 0; + } else { + bufSize = lastSlashIndex.?; + res = buf[0 ..bufSize]; + // Translated from the following JS code: + // lastSegmentLength = + // res.length - 1 - StringPrototypeLastIndexOf(res, separator); + const lastIndexOfSep = std.mem.lastIndexOfScalar(T, buf[0 ..bufSize], separator); + if (lastIndexOfSep == null) { + // Yes (>ლ), Node relies on the -1 result of + // StringPrototypeLastIndexOf(res, separator). + // A - -1 is a positive 1. + // So the code becomes + // lastSegmentLength = res.length - 1 + 1; + // or + // lastSegmentLength = res.length; + lastSegmentLength = bufSize; + } else { + lastSegmentLength = bufSize - 1 - lastIndexOfSep.?; + } + } + lastSlash = i; + dots = 0; + continue; + } else if (bufSize != 0) { + res = ""; + bufSize = 0; + lastSegmentLength = 0; + lastSlash = i; + dots = 0; + continue; + } + } + if (allowAboveRoot) { + // Translated from the following JS code: + // res += res.length > 0 ? `${separator}..` : '..'; + if (bufSize > 0) { + bufOffset = bufSize; + bufSize += 1; + buf[bufOffset] = separator; + bufOffset = bufSize; + bufSize += 2; + buf[bufOffset] = CHAR_DOT; + buf[bufOffset + 1] = CHAR_DOT; + } else { + bufSize = 2; + buf[0] = CHAR_DOT; + buf[1] = CHAR_DOT; + } + + res = buf[0 ..bufSize]; + lastSegmentLength = 2; + } + } else { + // Translated from the following JS code: + // if (res.length > 0) + // res += `${separator}${StringPrototypeSlice(path, lastSlash + 1, i)}`; + // else + // res = StringPrototypeSlice(path, lastSlash + 1, i); + if (bufSize > 0) { + bufOffset = bufSize; + bufSize += 1; + buf[bufOffset] = separator; + } + const sliceStart = if (lastSlash != null) lastSlash.? + 1 else 0; + const slice = path[sliceStart ..i]; + + bufOffset = bufSize; + bufSize += slice.len; + @memcpy(buf[bufOffset ..bufSize], slice); + + res = buf[0 ..bufSize]; + + // Translated from the following JS code: + // lastSegmentLength = i - lastSlash - 1; + const subtract = if (lastSlash != null) lastSlash.? + 1 else 2; + lastSegmentLength = if (i >= subtract) i - subtract else 0; + } + lastSlash = i; + dots = 0; + continue; + } else if (byte == CHAR_DOT and dots != null) { + dots = if (dots != null) dots.? + 1 else 0; + continue; + } else { + dots = null; + } + } + + return res; + } + + // Based on Node v21.6.1 path.posix.normalize + // https://github.com/nodejs/node/blob/6ae20aa63de78294b18d5015481485b7cd8fbb60/lib/path.js#L1130 + pub fn normalizePosixImpl(comptime T: type, path: []const T, buf: []T) []const T { + // validateString of `path` is performed in fn pub normalize. + + const len = path.len; + if (len == 0) { + return CHAR_STR_DOT; + } + + // Prefix with _ to avoid shadowing the identifier in the outer scope. + const _isAbsolute = path[0] == CHAR_FORWARD_SLASH; + const trailingSeparator = path[len - 1] == CHAR_FORWARD_SLASH; + + // Normalize the path + var normalizedPath = @This().normalizeStringImpl(T, path, !_isAbsolute, CHAR_FORWARD_SLASH, .posix, buf); + + var bufSize: usize = normalizedPath.len; + + if (bufSize == 0) { + if (_isAbsolute) { + return CHAR_STR_FORWARD_SLASH; + } + return if (trailingSeparator) "./" else CHAR_STR_DOT; + } + + var bufOffset: usize = 0; + + // Translated from the following JS code: + // if (trailingSeparator) + // path += '/'; + if (trailingSeparator) { + bufOffset = bufSize; + bufSize += 1; + buf[bufOffset] = CHAR_FORWARD_SLASH; + normalizedPath = buf[0 ..bufSize]; + } + + // Translated from the following JS code: + // return isAbsolute ? `/${path}` : path; + if (_isAbsolute) { + // Move all bytes to the right by 1 + var i: usize = bufSize - 1; + while (i >= 0) : (i -= if (i > 0) 1 else break) { + buf[i + 1] = normalizedPath[i]; + } + // Prepend the separator. + bufSize += 1; + buf[0] = CHAR_FORWARD_SLASH; + normalizedPath = buf[0 ..bufSize]; + } + return normalizedPath[0.. bufSize]; + } + + // Based on Node v21.6.1 path.win32.normalize + // https://github.com/nodejs/node/blob/6ae20aa63de78294b18d5015481485b7cd8fbb60/lib/path.js#L308 + pub fn normalizeWindowsImpl(comptime T: type, path: []const T, buf: []T) []const T { + // validateString of `path` is performed in fn pub normalize. + const len = path.len; + if (len == 0) { + return CHAR_STR_DOT; + } + + const isSepImpl = @This().isSepWindowsImpl; + + // Moved `rootEnd`, `device`, and `_isAbsolute` initialization after + // the `if (len == 1)` check. + const byte0: T = path[0]; + + // Try to match a root + if (len == 1) { + // `path` contains just a single char, exit early to avoid + // unnecessary work + return if (isSepImpl(T, byte0)) CHAR_STR_BACKWARD_SLASH else path; + } + + var rootEnd: usize = 0; + // Backed by smallTmpBuf3 + var device: ?[]const T = null; + // Prefix with _ to avoid shadowing the identifier in the outer scope. + var _isAbsolute: bool = false; + + var bufOffset: usize = 0; + var bufSize: usize = 0; + + if (isSepImpl(T, byte0)) { + // Possible UNC root + + // If we started with a separator, we know we at least have an absolute + // path of some kind (UNC or otherwise) + _isAbsolute = true; + + if (isSepImpl(T, path[1])) { + // Matched double path separator at beginning + var j: usize = 2; + var last: usize = j; + // Match 1 or more non-path separators + while (j < len and + !isSepImpl(T, path[j])) { + j += 1; + } + if (j < len and j != last) { + const firstPart: []const u8 = path[last ..j]; + // Matched! + last = j; + // Match 1 or more path separators + while (j < len and + isSepImpl(T, path[j])) { + j += 1; + } + if (j < len and j != last) { + // Matched! + last = j; + // Match 1 or more non-path separators + while (j < len and + !isSepImpl(T, path[j])) { + j += 1; + } + if (j == len) { + // We matched a UNC root only + // Return the normalized version of the UNC root since there + // is nothing left to process + // + // Translated from the following JS code: + // return `\\\\${firstPart}\\${StringPrototypeSlice(path, last)}\\`; + bufSize = 2; + buf[0] = CHAR_BACKWARD_SLASH; + buf[1] = CHAR_BACKWARD_SLASH; + bufOffset = bufSize; + bufSize += firstPart.len; + @memcpy(buf[bufOffset ..bufSize], firstPart); + bufOffset = bufSize; + bufSize += 1; + buf[bufOffset] = CHAR_BACKWARD_SLASH; + bufOffset = bufSize; + bufSize += len - last; + @memcpy(buf[bufOffset ..bufSize], path[last ..len]); + bufOffset = bufSize; + bufSize += 1; + buf[bufOffset] = CHAR_BACKWARD_SLASH; + return buf[0 ..bufSize]; + } + if (j != last) { + // We matched a UNC root with leftovers + // + // Translated from the following JS code: + // device = + // `\\\\${firstPart}\\${StringPrototypeSlice(path, last, j)}`; + // rootEnd = j; + bufSize = 2; + smallTmpBuf3[0] = CHAR_BACKWARD_SLASH; + smallTmpBuf3[1] = CHAR_BACKWARD_SLASH; + bufOffset = bufSize; + bufSize += firstPart.len; + @memcpy(smallTmpBuf3[bufOffset ..bufSize], firstPart); + bufOffset = bufSize; + bufSize += 1; + smallTmpBuf3[bufOffset] = CHAR_BACKWARD_SLASH; + bufOffset = bufSize; + bufSize += j - last; + @memcpy(smallTmpBuf3[bufOffset ..bufSize], path[last ..j]); + + device = smallTmpBuf3[0 ..bufSize]; + rootEnd = j; + } + } + } + } else { + rootEnd = 1; + } + } else if (@This().isWindowsDeviceRootImpl(T, byte0) and + path[1] == CHAR_COLON) { + // Possible device root + bufSize = 2; + smallTmpBuf3[0] = byte0; + smallTmpBuf3[1] = CHAR_COLON; + + device = smallTmpBuf3[0 ..bufSize]; + rootEnd = bufSize; + if (len > 2 and isSepImpl(T, path[2])) { + // Treat separator following drive name as an absolute path + // indicator + _isAbsolute = true; + rootEnd = 3; + } + } + + var tail = + if (rootEnd < len) + @This().normalizeStringImpl(T, path[rootEnd ..len], + !_isAbsolute, CHAR_BACKWARD_SLASH, .windows, buf) + else ""; + if (tail.len == 0 and !_isAbsolute) { + tail = CHAR_STR_DOT; + buf[0] = CHAR_DOT; + } + + var tailLen = tail.len; + bufSize = tailLen; + + if (tailLen > 0 and + isSepImpl(T, path[len - 1])) { + // Translated from the following JS code: + // tail += '\\'; + bufOffset = bufSize; + bufSize += 1; + buf[bufOffset] = CHAR_BACKWARD_SLASH; + tail = buf[0 ..bufSize]; + tailLen = bufSize; + } + if (device == null) { + // Translated from the following JS code: + // return isAbsolute ? `\\${tail}` : tail; + if (_isAbsolute) { + if (tailLen > 0) { + // Move all bytes to the right by 1 + var i: usize = tailLen - 1; + while (i >= 0) : (i -= if (i > 0) 1 else break) { + buf[i + 1] = tail[i]; + } + } + bufSize += 1; + // Prepend the separator. + buf[0] = CHAR_BACKWARD_SLASH; + } + return buf[0 ..bufSize]; + } + + // Translated from the following JS code: + // return isAbsolute ? `${device}\\${tail}` : `${device}${tail}`; + { + const deviceLen = device.?.len; + const moveBy = if (_isAbsolute) deviceLen + 1 else deviceLen; + // Move all bytes to the right by `moveBy` + if (tailLen > 0) { + var i: usize = tailLen - 1; + while (i >= 0) : (i -= if (i > 0) 1 else break) { + buf[i + moveBy] = tail[i]; + } + } + bufSize += moveBy; + @memcpy(buf[0 .. deviceLen], device.?); + if (_isAbsolute) { + buf[deviceLen] = CHAR_BACKWARD_SLASH; + } + } + return buf[0 ..bufSize]; + } + + pub inline fn normalizePosix(path: []const u8, buf: []u8) []const u8 { + return @This().normalizePosixImpl(u8, path, buf); + } + + pub inline fn normalizeWindows(path: []const u8, buf: []u8) []const u8 { + return @This().normalizeWindowsImpl(u8, path, buf); + } + + pub fn normalize(globalThis: *JSC.JSGlobalObject, isWindows: bool, args_ptr: [*]JSC.JSValue, args_len: u16) callconv(.C) JSC.JSValue { + if (comptime is_bindgen) return JSC.JSValue.jsUndefined(); + const path_ptr = if (args_len > 0) args_ptr[0] else JSC.JSValue.jsUndefined(); + // Supress exeption in zig. It does throw in JS land. + validateString(globalThis, path_ptr, "path", .{}) catch { + return JSC.JSValue.jsUndefined(); + }; + + var stack_fallback = std.heap.stackFallback( + stack_fallback_size_small, + JSC.getAllocator(globalThis) + ); + const allocator = stack_fallback.get(); + + var path = path_ptr.toSlice(globalThis, allocator); + defer path.deinit(); + + const buf = getBufAlloc(allocator, path.len) catch { + // Supress exeption in zig. It does throw in JS land. + globalThis.throwOutOfMemory(); + return JSC.JSValue.jsUndefined(); + }; + defer allocator.free(buf); + + const out = if (!isWindows) + @This().normalizePosix(path.slice(), buf) + else + @This().normalizeWindows(path.slice(), buf); + return JSC.ZigString.init(out).withEncoding().toValueGC(globalThis); + } + + // Based on Node v21.6.1 path.posix.parse + // https://github.com/nodejs/node/blob/6ae20aa63de78294b18d5015481485b7cd8fbb60/lib/path.js#L1452 + pub fn parsePosixImpl(comptime T: type, path: []const T) ParsedPathUnion { + // validateString of `path` is performed in fn pub parse. + var root: []const T = ""; + var dir: []const T = ""; + var base: []const T = ""; + var ext: []const T = ""; + // Prefix with _ to avoid shadowing the identifier in the outer scope. + var _name: []const T = ""; + + const len = path.len; + if (len == 0) { + return @This().ParsedPathImpl(T, root, dir, base, ext, _name); + } + + // Prefix with _ to avoid shadowing the identifier in the outer scope. + const _isAbsolute = path[0] == CHAR_FORWARD_SLASH; + var start: usize = 0; + if (_isAbsolute) { + root = "/"; + start = 1; + } + + // We use an optional value instead of -1, as in Node code, for easier number type use. + var startDot: ?usize = null; + var startPart: usize = 0; + // We use an optional value instead of -1, as in Node code, for easier number type use. + var end: ?usize = null; + var matchedSlash = true; + var i: usize = len - 1; + + // Track the state of characters (if any) we see before our first dot and + // after any path separator we find + // + // We use an optional value instead of -1, as in Node code, for easier number type use. + var preDotState: ?usize = 0; + + // Get non-dir info + while (i >= start) : (i -= if (i > 0) 1 else break) { + const byte = path[i]; + if (byte == CHAR_FORWARD_SLASH) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + startPart = i + 1; + break; + } + continue; + } + if (end == null) { + // We saw the first non-path separator, mark this as the end of our + // extension + matchedSlash = false; + end = i + 1; + } + + if (byte == CHAR_DOT) { + // If this is our first dot, mark it as the start of our extension + if (startDot == null) { + startDot = i; + } else if (preDotState != null and preDotState.? != 1) { + preDotState = 1; + } + } else if (startDot != null) { + // We saw a non-dot and non-path separator before our dot, so we should + // have a good chance at having a non-empty extension + preDotState = null; + } + } + + + if (end != null) { + // Prefix with _ to avoid shadowing the identifier in the outer scope. + const _start = if (startPart == 0 and _isAbsolute) 1 else startPart; + if (startDot == null or + // We saw a non-dot character immediately before the dot + (preDotState != null and preDotState.? == 0) or + // The (right-most) trimmed path component is exactly '..' + (preDotState != null and preDotState.? == 1 and + startDot.? == end.? - 1 and + startDot.? == startPart + 1) + ) { + _name = path[_start ..end.?]; + base = _name; + } else { + _name = path[_start ..startDot.?]; + base = path[_start ..end.?]; + ext = path[startDot.? ..end.?]; + } + } + + if (startPart > 0) { + dir = path[0 ..(startPart - 1)]; + } else if (_isAbsolute) { + dir = "/"; + } + + return @This().ParsedPathImpl(T, root, dir, base, ext, _name); + } + + // Based on Node v21.6.1 path.win32.parse + // https://github.com/nodejs/node/blob/6ae20aa63de78294b18d5015481485b7cd8fbb60/lib/path.js#L916 + pub fn parseWindowsImpl(comptime T: type, path: []const T) ParsedPathUnion { + // validateString of `path` is performed in fn pub parse. + var root: []const T = ""; + var dir: []const T = ""; + var base: []const T = ""; + var ext: []const T = ""; + // Prefix with _ to avoid shadowing the identifier in the outer scope. + var _name: []const T = ""; + + const len = path.len; + if (len == 0) { + return @This().ParsedPathImpl(T, root, dir, base, ext, _name); + } + + const isSepImpl = @This().isSepWindowsImpl; + + var rootEnd: usize = 0; + var byte = path[0]; + + if (len == 1) { + if (isSepImpl(T, byte)) { + // `path` contains just a path separator, exit early to avoid + // unnecessary work + root = path; + dir = path; + } else { + base = path; + _name = path; + } + return @This().ParsedPathImpl(T, root, dir, base, ext, _name); + } + + // Try to match a root + if (isSepImpl(T, byte)) { + // Possible UNC root + + rootEnd = 1; + if (isSepImpl(T, path[1])) { + // Matched double path separator at the beginning + var j: usize = 2; + var last: usize = j; + // Match 1 or more non-path separators + while (j < len and + !isSepImpl(T, path[j])) { + j += 1; + } + if (j < len and j != last) { + // Matched! + last = j; + // Match 1 or more path separators + while (j < len and + isSepImpl(T, path[j])) { + j += 1; + } + if (j < len and j != last) { + // Matched! + last = j; + // Match 1 or more non-path separators + while (j < len and + !isSepImpl(T, path[j])) { + j += 1; + } + if (j == len) { + // We matched a UNC root only + rootEnd = j; + } else if (j != last) { + // We matched a UNC root with leftovers + rootEnd = j + 1; + } + } + } + } + } else if (@This().isWindowsDeviceRootImpl(T, byte) and + path[1] == CHAR_COLON) { + // Possible device root + if (len <= 2) { + // `path` contains just a drive root, exit early to avoid + // unnecessary work + root = path; + dir = path; + return @This().ParsedPathImpl(T, root, dir, base, ext, _name); + } + rootEnd = 2; + if (isSepImpl(T, path[2])) { + if (len == 3) { + // `path` contains just a drive root, exit early to avoid + // unnecessary work + root = path; + dir = path; + return @This().ParsedPathImpl(T, root, dir, base, ext, _name); + } + rootEnd = 3; + } + } + if (rootEnd > 0) { + root = path[0 ..rootEnd]; + } + + // We use an optional value instead of -1, as in Node code, for easier number type use. + var startDot: ?usize = null; + var startPart = rootEnd; + // We use an optional value instead of -1, as in Node code, for easier number type use. + var end: ?usize = null; + var matchedSlash = true; + var i: usize = len - 1; + + // Track the state of characters (if any) we see before our first dot and + // after any path separator we find + // + // We use an optional value instead of -1, as in Node code, for easier number type use. + var preDotState: ?usize = 0; + + // Get non-dir info + while (i >= rootEnd) : (i -= if (i > 0) 1 else break) { + byte = path[i]; + if (isSepImpl(T, byte)) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + startPart = i + 1; + break; + } + continue; + } + if (end == null) { + // We saw the first non-path separator, mark this as the end of our + // extension + matchedSlash = false; + end = i + 1; + } + if (byte == CHAR_DOT) { + // If this is our first dot, mark it as the start of our extension + if (startDot == null) { + startDot = i; + } else if (preDotState != null and preDotState != 1) { + preDotState = 1; + } + } else if (startDot != null) { + // We saw a non-dot and non-path separator before our dot, so we should + // have a good chance at having a non-empty extension + preDotState = null; + } + } + + if (end != null) { + if (startDot == null or + // We saw a non-dot character immediately before the dot + (preDotState != null and preDotState.? == 0) or + // The (right-most) trimmed path component is exactly '..' + (preDotState != null and preDotState.? == 1 and + startDot.? == end.? - 1 and + startDot.? == startPart + 1) + ) { + // Prefix with _ to avoid shadowing the identifier in the outer scope. + _name = path[startPart ..end.?]; + base = _name; + } else { + _name = path[startPart ..startDot.?]; + base = path[startPart ..end.?]; + ext = path[startDot.? ..end.?]; + } + } + + // If the directory is the root, use the entire root as the `dir` including + // the trailing slash if any (`C:\abc` -> `C:\`). Otherwise, strip out the + // trailing slash (`C:\abc\def` -> `C:\abc`). + if (startPart > 0 and startPart != rootEnd) { + dir = path[0 ..(startPart - 1)]; + } else { + dir = root; + } + + return @This().ParsedPathImpl(T, root, dir, base, ext, _name); + } + + pub inline fn parsePosix(path: []const u8) ParsedPathUnion { + return @This().parsePosixImpl(u8, path); + } + + pub inline fn parseWindows(path: []const u8) ParsedPathUnion { + return @This().parseWindowsImpl(u8, path); + } + + pub fn parse(globalThis: *JSC.JSGlobalObject, isWindows: bool, args_ptr: [*]JSC.JSValue, args_len: u16) callconv(.C) JSC.JSValue { + if (comptime is_bindgen) return JSC.JSValue.jsUndefined(); + const path_ptr = if (args_len > 0) args_ptr[0] else JSC.JSValue.jsUndefined(); + // Supress exeption in zig. It does throw in JS land. + validateString(globalThis, path_ptr, "path", .{}) catch { + return JSC.JSValue.jsUndefined(); + }; - var i: u16 = 0; - while (i < args_len) : (i += 1) { - parts[i] = args_ptr[i].toSlice(globalThis, arena_allocator).slice(); + var stack_fallback = std.heap.stackFallback( + stack_fallback_size_small, + JSC.getAllocator(globalThis) + ); + const allocator = stack_fallback.get(); + + var path = path_ptr.toSlice(globalThis, allocator); + defer path.deinit(); + + const out = if (!isWindows) + @This().parsePosix(path.slice()).u8 + else + @This().parseWindows(path.slice()).u8; + + var jsObject = JSC.JSValue.createEmptyObject(globalThis, 5); + jsObject.put(globalThis, JSC.ZigString.static("root"), JSC.ZigString.init(out.root).withEncoding().toValueGC(globalThis)); + jsObject.put(globalThis, JSC.ZigString.static("dir"), JSC.ZigString.init(out.dir).withEncoding().toValueGC(globalThis)); + jsObject.put(globalThis, JSC.ZigString.static("base"), JSC.ZigString.init(out.base).withEncoding().toValueGC(globalThis)); + jsObject.put(globalThis, JSC.ZigString.static("ext"), JSC.ZigString.init(out.ext).withEncoding().toValueGC(globalThis)); + jsObject.put(globalThis, JSC.ZigString.static("name"), JSC.ZigString.init(out.name).withEncoding().toValueGC(globalThis)); + return jsObject; + } + + /// Based on Node v21.6.1 path.posix.relative: + /// https://github.com/nodejs/node/blob/6ae20aa63de78294b18d5015481485b7cd8fbb60/lib/path.js#L1193 + pub fn relativePosixImpl(comptime T: type, from: []const T, to: []const T, buf: []T) []const T { + // validateString of `from` and `to` are performed in fn pub relative. + + if (std.mem.eql(T, from, to)) { + return ""; + } + + // Trim leading forward slashes. + var fromOrig = @This().resolvePosixImpl(T, &.{ from }, buf); + const fromOrigLen = fromOrig.len; + // Backed by tmpBuf2 + @memcpy(tmpBuf2[0 ..fromOrigLen], fromOrig); + fromOrig = tmpBuf2[0 ..fromOrigLen]; + // Leave `toOrig` as a slice of `buf`. + const toOrig = @This().resolvePosixImpl(T, &.{ to }, buf); + + if (std.mem.eql(T, fromOrig, toOrig)) { + return ""; } - var out: JSC.ZigString = if (!isWindows) - JSC.ZigString.init(strings.withoutTrailingSlash(PathHandler.joinAbsStringBuf(Fs.FileSystem.instance.top_level_dir, &out_buf, parts, .posix))) + const fromStart = 1; + const fromEnd = fromOrigLen; + const fromLen = fromEnd - fromStart; + const toOrigLen = toOrig.len; + var toStart: usize = 1; + const toLen = toOrigLen - toStart; + + // Compare paths to find the longest common path from root + const smallestLength = @min(fromLen, toLen); + // We use an optional value instead of -1, as in Node code, for easier number type use. + var lastCommonSep: ?usize = null; + + var matchesAllOfSmallest = false; + // Add a block to isolate `i`. + { + var i: usize = 0; + while (i < smallestLength) : (i += 1) { + const fromByte = fromOrig[fromStart + i]; + if (fromByte != toOrig[toStart + i]) { + break; + } else if (fromByte == CHAR_FORWARD_SLASH) { + lastCommonSep = i; + } + } + matchesAllOfSmallest = i == smallestLength; + } + if (matchesAllOfSmallest) { + if (toLen > smallestLength) { + if (toOrig[toStart + smallestLength] == CHAR_FORWARD_SLASH) { + // We get here if `from` is the exact base path for `to`. + // For example: from='/foo/bar'; to='/foo/bar/baz' + return toOrig[toStart + smallestLength + 1 ..toOrigLen]; + } + if (smallestLength == 0) { + // We get here if `from` is the root + // For example: from='/'; to='/foo' + return toOrig[toStart + smallestLength ..toOrigLen]; + } + } else if (fromLen > smallestLength) { + if (fromOrig[fromStart + smallestLength] == CHAR_FORWARD_SLASH) { + // We get here if `to` is the exact base path for `from`. + // For example: from='/foo/bar/baz'; to='/foo/bar' + lastCommonSep = smallestLength; + } else if (smallestLength == 0) { + // We get here if `to` is the root. + // For example: from='/foo/bar'; to='/' + lastCommonSep = 0; + } + } + } + + var bufOffset: usize = 0; + var bufSize: usize = 0; + + var out: []const T = ""; + // Add a block to isolate `i`. + { + // Generate the relative path based on the path difference between `to` + // and `from`. + // + // Translated from the following JS code: + // for (i = fromStart + lastCommonSep + 1; i <= fromEnd; ++i) { + var i: usize = fromStart + (if (lastCommonSep != null) lastCommonSep.? + 1 else 0); + while (i <= fromEnd) : (i += 1) { + if (i == fromEnd or fromOrig[i] == CHAR_FORWARD_SLASH) { + // Translated from the following JS code: + // out += out.length === 0 ? '..' : '/..'; + if (out.len > 0) { + bufOffset = bufSize; + bufSize += 3; + smallTmpBuf1[bufOffset] = CHAR_FORWARD_SLASH; + smallTmpBuf1[bufOffset + 1] = CHAR_DOT; + smallTmpBuf1[bufOffset + 2] = CHAR_DOT; + } else { + bufSize = 2; + smallTmpBuf1[0] = CHAR_DOT; + smallTmpBuf1[1] = CHAR_DOT; + } + out = smallTmpBuf1[0 ..bufSize]; + } + } + } + + // Lastly, append the rest of the destination (`to`) path that comes after + // the common path parts. + + // Translated from the following JS code: + // return `${out}${StringPrototypeSlice(to, toStart + lastCommonSep)}`; + toStart = if (lastCommonSep != null) toStart + lastCommonSep.? else 0; + const sliceSize = toOrigLen - toStart; + const outLen = out.len; + bufSize = outLen; + if (sliceSize > 0) { + bufOffset = bufSize; + bufSize += sliceSize; + // Copy from toOrig, backed by buf, to tmpBuf2 + // since fromOrig is no longer being referenced. + @memcpy(tmpBuf2[0 ..sliceSize], toOrig[toStart ..toOrigLen]); + @memcpy(buf[bufOffset ..bufSize], tmpBuf2[0 ..sliceSize]); + } + if (outLen > 0) { + @memcpy(buf[0 ..outLen], out); + } + const result = buf[0 ..bufSize]; + return result; + } + + /// Based on Node v21.6.1 path.win32.relative: + /// https://github.com/nodejs/node/blob/6ae20aa63de78294b18d5015481485b7cd8fbb60/lib/path.js#L500 + pub fn relativeWindowsImpl(comptime T: type, from: []const T, to: []const T, buf: []T) []const T { + // validateString of `from` and `to` are performed in fn pub relative. + + if (std.mem.eql(T, from, to)) { + return ""; + } + + var fromOrig = @This().resolveWindowsImpl(T, &.{ from }, buf); + const fromOrigLen = fromOrig.len; + // Backed by tmpBuf2 + @memcpy(tmpBuf2[0 ..fromOrigLen], fromOrig); + fromOrig = tmpBuf2[0 ..fromOrigLen]; + // Leave `toOrig` as a slice of `buf`. + const toOrig = @This().resolveWindowsImpl(T, &.{ to }, buf); + + if ( + std.mem.eql(T, fromOrig, toOrig) or + @This().eqlIgnoreCaseImpl(T, fromOrig, toOrig) + ) { + return ""; + } + + const toOrigLen = toOrig.len; + + // Trim leading backslashes + var fromStart: usize = 0; + while (fromStart < fromOrigLen and + fromOrig[fromStart] == CHAR_BACKWARD_SLASH) { + fromStart += 1; + } + + // Trim trailing backslashes (applicable to UNC paths only) + var fromEnd = fromOrigLen; + while (fromEnd - 1 > fromStart and + fromOrig[fromEnd - 1] == CHAR_BACKWARD_SLASH) { + fromEnd -= 1; + } + + const fromLen = fromEnd - fromStart; + + // Trim leading backslashes + var toStart: usize = 0; + while (toStart < toOrigLen and + toOrig[toStart] == CHAR_BACKWARD_SLASH) { + toStart = toStart + 1; + } + + // Trim trailing backslashes (applicable to UNC paths only) + var toEnd = toOrigLen; + while (toEnd - 1 > toStart and + toOrig[toEnd - 1] == CHAR_BACKWARD_SLASH) { + toEnd -= 1; + } + + const toLen = toEnd - toStart; + + // Compare paths to find the longest common path from root + const smallestLength = @min(fromLen, toLen); + // We use an optional value instead of -1, as in Node code, for easier number type use. + var lastCommonSep: ?usize = null; + + var matchesAllOfSmallest = false; + // Add a block to isolate `i`. + { + var i: usize = 0; + while (i < smallestLength) : (i += 1) { + const fromByte = fromOrig[fromStart + i]; + if (@This().toLowerImpl(T, fromByte) != @This().toLowerImpl(T, toOrig[toStart + i])) { + break; + } else if (fromByte == CHAR_BACKWARD_SLASH) { + lastCommonSep = i; + } + } + matchesAllOfSmallest = i == smallestLength; + } + + // We found a mismatch before the first common path separator was seen, so + // return the original `to`. + if (!matchesAllOfSmallest) { + if (lastCommonSep == null) { + return toOrig; + } + } else { + if (toLen > smallestLength) { + if (toOrig[toStart + smallestLength] == CHAR_BACKWARD_SLASH) { + // We get here if `from` is the exact base path for `to`. + // For example: from='C:\foo\bar'; to='C:\foo\bar\baz' + return toOrig[toStart + smallestLength + 1 ..toOrigLen]; + } + if (smallestLength == 2) { + // We get here if `from` is the device root. + // For example: from='C:\'; to='C:\foo' + return toOrig[toStart + smallestLength ..toOrigLen]; + } + } + if (fromLen > smallestLength) { + if (fromOrig[fromStart + smallestLength] == CHAR_BACKWARD_SLASH) { + // We get here if `to` is the exact base path for `from`. + // For example: from='C:\foo\bar'; to='C:\foo' + lastCommonSep = smallestLength; + } else if (smallestLength == 2) { + // We get here if `to` is the device root. + // For example: from='C:\foo\bar'; to='C:\' + lastCommonSep = 3; + } + } + if (lastCommonSep == null) { + lastCommonSep = 0; + } + } + + var bufOffset: usize = 0; + var bufSize: usize = 0; + + var out: []const T = ""; + // Add a block to isolate `i`. + { + // Generate the relative path based on the path difference between `to` and + // `from` + var i: usize = fromStart + (if (lastCommonSep != null) lastCommonSep.? + 1 else 0); + while (i <= fromEnd) : (i += 1) { + if (i == fromEnd or fromOrig[i] == CHAR_BACKWARD_SLASH) { + // Translated from the following JS code: + // out += out.length === 0 ? '..' : '\\..'; + if (out.len > 0) { + bufOffset = bufSize; + bufSize += 3; + smallTmpBuf1[bufOffset] = CHAR_BACKWARD_SLASH; + smallTmpBuf1[bufOffset + 1] = CHAR_DOT; + smallTmpBuf1[bufOffset + 2] = CHAR_DOT; + } else { + bufSize = 2; + smallTmpBuf1[0] = CHAR_DOT; + smallTmpBuf1[1] = CHAR_DOT; + } + out = smallTmpBuf1[0 ..bufSize]; + } + } + } + + // Translated from the following JS code: + // toStart += lastCommonSep; + if (lastCommonSep == null) { + // If toStart would go negative make it toOrigLen - 1 to + // mimic String#slice with a negative start. + toStart = if (toStart > 0) toStart - 1 else toOrigLen - 1; + } else { + toStart += lastCommonSep.?; + } + + // Lastly, append the rest of the destination (`to`) path that comes after + // the common path parts + const outLen = out.len; + if (outLen > 0) { + const sliceSize = toEnd - toStart; + bufSize = outLen; + if (sliceSize > 0) { + bufOffset = bufSize; + bufSize += sliceSize; + // Copy from toOrig, backed by buf, to tmpBuf2 + // since fromOrig is no longer being referenced. + @memcpy(tmpBuf2[0 ..sliceSize], toOrig[toStart..toEnd]); + @memcpy(buf[bufOffset ..bufSize], tmpBuf2[0 ..sliceSize]); + } + @memcpy(buf[0 ..outLen], out); + return buf[0 ..bufSize]; + } + + if (toOrig[toStart] == CHAR_BACKWARD_SLASH) { + toStart += 1; + } + return toOrig[toStart..toEnd]; + } + + pub inline fn relativePosix(from: []const u8, to: []const u8, buf: []u8) []const u8 { + return @This().relativePosixImpl(u8, from, to, buf); + } + + pub inline fn relativeWindows(from: []const u8, to: []const u8, buf: []u8) []const u8 { + return @This().relativeWindowsImpl(u8, from, to, buf); + } + + pub fn relative(globalThis: *JSC.JSGlobalObject, isWindows: bool, args_ptr: [*]JSC.JSValue, args_len: u16) callconv(.C) JSC.JSValue { + if (comptime is_bindgen) return JSC.JSValue.jsUndefined(); + const from_ptr = if (args_len > 0) args_ptr[0] else JSC.JSValue.jsUndefined(); + // Supress exeption in zig. It does throw in JS land. + validateString(globalThis, from_ptr, "from", .{}) catch { + return JSC.JSValue.jsUndefined(); + }; + const to_ptr = if (args_len > 1) args_ptr[1] else JSC.JSValue.jsUndefined(); + // Supress exeption in zig. It does throw in JS land. + validateString(globalThis, to_ptr, "to", .{}) catch { + return JSC.JSValue.jsUndefined(); + }; + + var stack_fallback = std.heap.stackFallback( + stack_fallback_size_small, + JSC.getAllocator(globalThis) + ); + const allocator = stack_fallback.get(); + + var from = from_ptr.toSlice(globalThis, allocator); + defer from.deinit(); + + var to = to_ptr.toSlice(globalThis, allocator); + defer from.deinit(); + + const buf = getBufAlloc(allocator, from.len + to.len) catch { + // Supress exeption in zig. It does throw in JS land. + globalThis.throwOutOfMemory(); + return JSC.JSValue.jsUndefined(); + }; + defer allocator.free(buf); + + const out = if (!isWindows) + @This().relativePosix(from.slice(), to.slice(), buf) else - JSC.ZigString.init(strings.withoutTrailingSlashWindowsPath(PathHandler.joinAbsStringBuf(Fs.FileSystem.instance.top_level_dir, &out_buf, parts, .windows))); + @This().relativeWindows(from.slice(), to.slice(), buf); + return JSC.ZigString.init(out).withEncoding().toValueGC(globalThis); + } - if (arena.state.buffer_list.first != null) - out.setOutputEncoding(); + /// Based on Node v21.6.1 path.posix.resolve: + /// https://github.com/nodejs/node/blob/6ae20aa63de78294b18d5015481485b7cd8fbb60/lib/path.js#L1095 + pub fn resolvePosixImpl(comptime T: type, paths: []const []const T, buf: []T) []const T { + // Backed by tmpBuf1 + var resolvedPath: []const T = ""; + var resolvedAbsolute: bool = false; + + var bufOffset: usize = 0; + var bufSize: usize = 0; + + var i: i64 = if (paths.len == 0) -1 else @as(i64, @intCast(paths.len - 1)); + while (i >= -1 and !resolvedAbsolute) : (i -= 1) { + var path: []const T = ""; + if (i >= 0) { + path = paths[@as(usize, @intCast(i))]; + } else { + path = @This().posixCwdImpl(T, buf); + } + // validateString of `path` is performed in fn pub resolve. + const len = path.len; - return out.toValueGC(globalThis); + // Skip empty entries + if (len == 0) { + continue; + } + + // Translated from the following JS code: + // resolvedPath = `${path}/${resolvedPath}`; + const resolvedPathLen = resolvedPath.len; + if (resolvedPathLen > 0) { + // Move all bytes to the right by path.len + 1 + bufOffset = len + 1; + var j: usize = resolvedPathLen - 1; + while (j >= 0) : (j -= if (j > 0) 1 else break) { + tmpBuf1[j + bufOffset] = resolvedPath[j]; + } + } + bufSize = len; + @memcpy(tmpBuf1[0 ..bufSize], path); + bufSize += 1; + tmpBuf1[len] = CHAR_FORWARD_SLASH; + bufSize += resolvedPathLen; + + resolvedPath = tmpBuf1[0 ..bufSize]; + resolvedAbsolute = path[0] == CHAR_FORWARD_SLASH; + } + + // Exit early for empty path. + if (resolvedPath.len == 0) { + return CHAR_STR_DOT; + } + + // At this point the path should be resolved to a full absolute path, but + // handle relative paths to be safe (might happen when process.cwd() fails) + + // Normalize the path + const normalizedPath = @This().normalizeStringImpl(T, resolvedPath, !resolvedAbsolute, CHAR_FORWARD_SLASH, .posix, buf); + bufSize = normalizedPath.len; + @memcpy(tmpBuf1[0 ..bufSize], normalizedPath); + resolvedPath = tmpBuf1[0 ..bufSize]; + + // Translated from the following JS code: + // if (resolvedAbsolute) { + // return `/${resolvedPath}`; + // } + if (resolvedAbsolute) { + bufSize = 1; + buf[0] = CHAR_FORWARD_SLASH; + bufOffset = bufSize; + bufSize += resolvedPath.len; + @memcpy(buf[bufOffset ..bufSize], resolvedPath); + return buf[0 ..bufSize]; + } + // Translated from the following JS code: + // return resolvedPath.length > 0 ? resolvedPath : '.'; + return if (resolvedPath.len > 0) resolvedPath else CHAR_STR_DOT; + } + + /// Based on Node v21.6.1 path.win32.resolve: + /// https://github.com/nodejs/node/blob/6ae20aa63de78294b18d5015481485b7cd8fbb60/lib/path.js#L162 + pub fn resolveWindowsImpl(comptime T: type, paths: []const []const T, buf: []T) []const T { + const isSepImpl = @This().isSepWindowsImpl; + + // Backed by smallTmpBuf1 + var resolvedDevice: []const T = ""; + // Backed by smallTmpBuf2 + var resolvedTail: []const T = ""; + var resolvedAbsolute: bool = false; + + var bufOffset: usize = 0; + var bufSize: usize = 0; + + var envPath: ?[]const T = null; + var arena: ?bun.ArenaAllocator = null; + var arena_allocator: ?std.mem.Allocator = null; + + defer { + if (arena != null) { + arena.?.deinit(); + } + if (envPath != null) { + arena_allocator.?.free(envPath.?); + } + } + + var i: i64 = if (paths.len == 0) -1 else @as(i64, @intCast(paths.len - 1)); + while (i >= -1) : (i -= 1) { + // Backed by tmpBuf1 + var path: []const T = ""; + if (i >= 0) { + path = paths[@as(usize, @intCast(i))]; + // validateString of `path` is performed in fn pub resolve. + + // Skip empty entries + if (path.len == 0) { + continue; + } + } else if (resolvedDevice.len == 0) { + path = @This().getCwdImpl(T); + } else { + // Windows has the concept of drive-specific current working + // directories. If we've resolved a drive letter but not yet an + // absolute path, get cwd for that drive, or the process cwd if + // the drive cwd is not available. We're sure the device is not + // a UNC path at this points, because UNC paths are always absolute. + + // Translated from the following JS code: + // path = process.env[`=${resolvedDevice}`] || process.cwd(); + envPath = brk: { + bufSize = 1; + tmpBuf1[0] = '='; + bufOffset = bufSize; + bufSize += resolvedDevice.len; + @memcpy(tmpBuf1[bufOffset ..bufSize], resolvedDevice); + const key = tmpBuf1[0 ..bufSize]; + if (arena_allocator == null) { + arena = bun.ArenaAllocator.init(heap_allocator); + arena_allocator = arena.?.allocator(); + } + // @TODO: This needs a unit test. Not sure if even Node has one: + // https://github.com/nodejs/node/commit/d91bc7cb0977b058750183199906590d95003fc0 + if (@TypeOf(T) == u16) { + break :brk @This().getEnvVarOwnedW(arena_allocator.?, key) catch null orelse null; + } + break :brk std.process.getEnvVarOwned(arena_allocator.?, key) catch null orelse null; + }; + if (envPath == null) { + path = @This().getCwdImpl(T); + } else { + bufSize = envPath.?.len; + @memcpy(tmpBuf1[0 ..bufSize], envPath.?); + path = tmpBuf1[0 ..bufSize]; + } + + // Verify that a cwd was found and that it actually points + // to our drive. If not, default to the drive's root. + if ( + envPath == null or + path[2] != CHAR_BACKWARD_SLASH or + !@This().eqlIgnoreCaseImpl(T, path[0 ..2], resolvedDevice) + ) { + // Translated from the following JS code: + // path = `${resolvedDevice}\\`; + bufSize = resolvedDevice.len; + @memcpy(tmpBuf1[0 ..bufSize], resolvedDevice); + bufOffset = bufSize; + bufSize += 1; + tmpBuf1[bufOffset] = CHAR_BACKWARD_SLASH; + path = tmpBuf1[0 ..bufSize]; + } + } + + var rootEnd: usize = 0; + // Backed by smallTmpBuf3 + var device: []const T = ""; + // Prefix with _ to avoid shadowing the identifier in the outer scope. + var _isAbsolute: bool = false; + + const len = path.len; + // Guard against path being empty which could happen if cwd() returns an empty string. + if (len > 0) { + const byte0 = path[0]; + + // Try to match a root + if (len == 1) { + if (isSepImpl(T, byte0)) { + // `path` contains just a path separator + rootEnd = 1; + _isAbsolute = true; + } + } else if (isSepImpl(T, byte0)) { + // Possible UNC root + + // If we started with a separator, we know we at least have an + // absolute path of some kind (UNC or otherwise) + _isAbsolute = true; + + if (isSepImpl(T, path[1])) { + // Matched double path separator at the beginning + var j: usize = 2; + var last: usize = j; + + // Match 1 or more non-path separators + while (j < len and !isSepImpl(T, path[j])) : (j += 1) {} + + if (j < len and j != last) { + const firstPart = path[last .. j]; + + // Matched! + last = j; + + // Match 1 or more path separators + while (j < len and isSepImpl(T, path[j])) : (j += 1) {} + + if (j < len and j != last) { + // Matched! + last = j; + + // Match 1 or more non-path separators + while (j < len and !isSepImpl(T, path[j])) : (j += 1) {} + + if (j == len or j != last) { + // We matched a UNC root + + // Translated from the following JS code: + // device = + // `\\\\${firstPart}\\${StringPrototypeSlice(path, last, j)}`; + // rootEnd = j; + bufSize = 2; + smallTmpBuf3[0] = CHAR_BACKWARD_SLASH; + smallTmpBuf3[1] = CHAR_BACKWARD_SLASH; + bufOffset = bufSize; + bufSize += firstPart.len; + @memcpy(smallTmpBuf3[bufOffset ..bufSize], firstPart); + bufOffset = bufSize; + bufSize += 1; + smallTmpBuf3[bufOffset] = CHAR_BACKWARD_SLASH; + const slice = path[last .. j]; + bufOffset = bufSize; + bufSize += slice.len; + @memcpy(smallTmpBuf3[bufOffset ..bufSize], slice); + + device = smallTmpBuf3[0 ..bufSize]; + rootEnd = j; + } + } + } + } else { + rootEnd = 1; + } + } else if (@This().isWindowsDeviceRootImpl(T, byte0) and + path[1] == CHAR_COLON) { + // Possible device root + bufSize = 2; + smallTmpBuf3[0] = path[0]; + smallTmpBuf3[1] = CHAR_COLON; + + device = smallTmpBuf3[0 ..bufSize]; + rootEnd = bufSize; + + if (len > 2 and isSepImpl(T, path[2])) { + // Treat separator following the drive name as an absolute path indicator + _isAbsolute = true; + rootEnd = 3; + } + } + + if (device.len > 0) { + if (resolvedDevice.len > 0) { + if (!@This().eqlIgnoreCaseImpl(T, device, resolvedDevice)) { + // This path points to another device, so it is not applicable + continue; + } + } else { + bufSize = device.len; + @memcpy(smallTmpBuf1[0 ..bufSize], device); + + resolvedDevice = smallTmpBuf1[0 ..bufSize]; + } + } + } + + if (resolvedAbsolute) { + if (resolvedDevice.len > 0) break; + } else { + // Translated from the following JS code: + // resolvedTail = `${StringPrototypeSlice(path, rootEnd)}\\${resolvedTail}`; + const sliceLen = len - rootEnd; + const resolvedTailLen = resolvedTail.len; + if (resolvedTailLen > 0) { + // Move all bytes to the right by path slice.len + 1 + bufOffset = sliceLen + 1; + var j: usize = resolvedTailLen - 1; + while (j >= 0) : (j -= if (j > 0) 1 else break) { + smallTmpBuf2[j + bufOffset] = resolvedTail[j]; + } + } + bufSize = sliceLen; + if (sliceLen > 0) { + @memcpy(smallTmpBuf2[0 ..bufSize], path[rootEnd ..len]); + } + bufOffset = bufSize; + bufSize += 1; + smallTmpBuf2[bufOffset] = CHAR_BACKWARD_SLASH; + bufSize += resolvedTailLen; + + resolvedTail = smallTmpBuf2[0 ..bufSize]; + resolvedAbsolute = _isAbsolute; + + if (_isAbsolute and resolvedDevice.len > 0) { + break; + } + } + } + + // Exit early for empty path. + if (resolvedTail.len == 0) { + return CHAR_STR_DOT; + } + + // At this point, the path should be resolved to a full absolute path, + // but handle relative paths to be safe (might happen when std.process.cwdAlloc() + // fails) + + // Normalize the tail path + const normalizedTail = @This().normalizeStringImpl(T, resolvedTail, !resolvedAbsolute, CHAR_BACKWARD_SLASH, .windows, buf); + bufSize = normalizedTail.len; + @memcpy(smallTmpBuf2[0 ..bufSize], normalizedTail); + resolvedTail = smallTmpBuf2[0 ..bufSize]; + + // Translated from the following JS code: + // resolvedAbsolute ? `${resolvedDevice}\\${resolvedTail}` + var result: []const T = ""; + if (resolvedAbsolute) { + bufSize = resolvedDevice.len; + @memcpy(buf[0 ..resolvedDevice.len], resolvedDevice); + bufOffset = bufSize; + bufSize += 1; + buf[bufOffset] = CHAR_BACKWARD_SLASH; + bufOffset = bufSize; + bufSize += resolvedTail.len; + @memcpy(buf[bufOffset ..bufSize], resolvedTail); + result = buf[0 ..bufSize]; + } + // Translated from the following JS code: + // : `${resolvedDevice}${resolvedTail}` || '.' + else if ((resolvedDevice.len + resolvedTail.len) > 0) { + bufSize = resolvedDevice.len; + @memcpy(buf[0 ..bufSize], resolvedDevice); + bufOffset = bufSize; + bufSize += resolvedTail.len; + @memcpy(buf[bufOffset ..bufSize], resolvedTail); + result = buf[0 ..bufSize]; + } else { + result = CHAR_STR_DOT; + } + return result; + } + + pub inline fn resolvePosix(paths: []const []const u8, buf: []u8) []const u8 { + return @This().resolvePosixImpl(u8, paths, buf); + } + + pub inline fn resolveWindows(paths: []const []const u8, buf: []u8) []const u8 { + return @This().resolveWindowsImpl(u8, paths, buf); + } + + pub fn resolve(globalThis: *JSC.JSGlobalObject, isWindows: bool, args_ptr: [*]JSC.JSValue, args_len: u16) callconv(.C) JSC.JSValue { + if (comptime is_bindgen) return JSC.JSValue.jsUndefined(); + + var arena = bun.ArenaAllocator.init(heap_allocator); + defer arena.deinit(); + const arena_allocator = arena.allocator(); + + var stack_fallback = std.heap.stackFallback( + stack_fallback_size_large, + JSC.getAllocator(globalThis) + ); + var allocator = stack_fallback.get(); + + var paths = allocator.alloc(string, args_len) catch unreachable; + defer allocator.free(paths); + + // Adding 2 bytes when Windows for the possible UNC root, i.e. "\\\\", addition. + var lenSum: usize = if (isWindows) 2 else 0; + for (0 ..args_len) |i| { + const path_ptr = args_ptr[i]; + // Supress exeption in zig. It does throw in JS land. + validateString(globalThis, path_ptr, "paths[{d}]", .{i}) catch { + return JSC.JSValue.jsUndefined(); + }; + const path = path_ptr.toSlice(globalThis, arena_allocator).slice(); + const len = path.len; + paths[i] = path; + // Add 1 for the separator. + lenSum += if (len > 0 and lenSum > 0) len + 1 else len; + } + + const buf = getBufAlloc(allocator, lenSum) catch { + // Supress exeption in zig. It does throw in JS land. + globalThis.throwOutOfMemory(); + return JSC.JSValue.jsUndefined(); + }; + defer allocator.free(buf); + + const out = if (!isWindows) + @This().resolvePosix(paths, buf) + else + @This().resolveWindows(paths, buf); + return JSC.ZigString.init(out).withEncoding().toValueGC(globalThis); + } + + /// Based on Node v21.6.1 path.win32.toNamespacedPath: + /// https://github.com/nodejs/node/blob/6ae20aa63de78294b18d5015481485b7cd8fbb60/lib/path.js#L622 + pub fn toNamespacedPathWindowsImpl(comptime T: type, path: []const T, buf: []T) []const T { + // Type checkng of `path` is performed in fn pub toNamespacedPath. + const resolvedPath = @This().resolveWindowsImpl(T, &.{ path }, buf); + const len = resolvedPath.len; + + if (len <= 2) { + return path; + } + + var bufSize: usize = 0; + + const byte0 = resolvedPath[0]; + if (byte0 == CHAR_BACKWARD_SLASH) { + // Possible UNC root + if (resolvedPath[1] == CHAR_BACKWARD_SLASH) { + const byte2 = resolvedPath[2]; + if (byte2 != CHAR_QUESTION_MARK and byte2 != CHAR_DOT) { + // Matched non-long UNC root, convert the path to a long UNC path + + // Translated from the following JS code: + // return `\\\\?\\UNC\\${StringPrototypeSlice(resolvedPath, 2)}`; + // + // Move all bytes to the right by 6 so that the first two bytes are + // overwritten by "\\\\?\\UNC\\" which is 8 bytes long. + var i: usize = len - 1; + while (i >= 0) : (i -= if (i > 0) 1 else break) { + buf[i + 6] = resolvedPath[i]; + } + // Equiv to std.os.windows.NamespacePrefix.verbatim + // https://github.com/ziglang/zig/blob/dcaf43674e35372e1d28ab12c4c4ff9af9f3d646/lib/std/os/windows.zig#L2358-L2374 + bufSize = len + 6; + buf[0] = CHAR_BACKWARD_SLASH; + buf[1] = CHAR_BACKWARD_SLASH; + buf[2] = CHAR_QUESTION_MARK; + buf[3] = CHAR_BACKWARD_SLASH; + buf[4] = 'U'; + buf[5] = 'N'; + buf[6] = 'C'; + buf[7] = CHAR_BACKWARD_SLASH; + return buf[0 ..bufSize]; + } + } + } else if ( + @This().isWindowsDeviceRootImpl(T, byte0) and + resolvedPath[1] == CHAR_COLON and + resolvedPath[2] == CHAR_BACKWARD_SLASH + ) { + // Matched device root, convert the path to a long UNC path + + // Translated from the following JS code: + // return `\\\\?\\${resolvedPath}` + // + // Move all bytes to the right by 4 + var i: usize = len - 1; + while (i >= 0) : (i -= if (i > 0) 1 else break) { + buf[i + 4] = resolvedPath[i]; + } + // Equiv to std.os.windows.NamespacePrefix.verbatim + // https://github.com/ziglang/zig/blob/dcaf43674e35372e1d28ab12c4c4ff9af9f3d646/lib/std/os/windows.zig#L2358-L2374 + bufSize = len + 4; + buf[0] = CHAR_BACKWARD_SLASH; + buf[1] = CHAR_BACKWARD_SLASH; + buf[2] = CHAR_QUESTION_MARK; + buf[3] = CHAR_BACKWARD_SLASH; + return buf[0 ..bufSize]; + } + return path; + } + + pub inline fn toNamespacedPathWindows(path: []const u8, buf: []u8) []const u8 { + return @This().toNamespacedPathWindowsImpl(u8, path, buf); + } + + pub fn toNamespacedPath(globalThis: *JSC.JSGlobalObject, isWindows: bool, args_ptr: [*]JSC.JSValue, args_len: u16) callconv(.C) JSC.JSValue { + if (comptime is_bindgen) return JSC.JSValue.jsUndefined(); + if (args_len == 0) return JSC.JSValue.jsUndefined(); + var path_ptr = args_ptr[0]; + // Act as an identity function for non-string values and non-Windows platforms. + // Note: this will *probably* throw somewhere. + if (!isWindows or !path_ptr.isString()) return path_ptr; + + var stack_fallback = std.heap.stackFallback( + stack_fallback_size_small, + JSC.getAllocator(globalThis) + ); + const allocator = stack_fallback.get(); + + var path = path_ptr.toSlice(globalThis, allocator); + defer path.deinit(); + const len = path.len; + if (len == 0) return path_ptr; + + const buf = getBufAlloc(allocator, len) catch { + // Supress exeption in zig. It does throw in JS land. + globalThis.throwOutOfMemory(); + return JSC.JSValue.jsUndefined(); + }; + defer allocator.free(buf); + + const out = @This().toNamespacedPathWindows(path.slice(), buf); + return JSC.ZigString.init(out).withEncoding().toValueGC(globalThis); } pub const Export = shim.exportFunctions(.{ @@ -2390,6 +4702,7 @@ pub const Path = struct { .parse = parse, .relative = relative, .resolve = resolve, + .toNamespacedPath = toNamespacedPath, }); pub const Extern = [_][]const u8{"create"}; @@ -2426,6 +4739,9 @@ pub const Path = struct { @export(Path.resolve, .{ .name = Export[9].symbol_name, }); + @export(Path.toNamespacedPath, .{ + .name = Export[10].symbol_name, + }); } } }; @@ -2436,7 +4752,7 @@ pub const Process = struct { } pub fn getExecPath(globalObject: *JSC.JSGlobalObject) callconv(.C) JSC.JSValue { - var buf: [bun.MAX_PATH_BYTES]u8 = undefined; + var buf: PathBuffer = undefined; const out = std.fs.selfExePath(&buf) catch { // if for any reason we are unable to get the executable path, we just return argv[0] return getArgv0(globalObject); @@ -2468,7 +4784,7 @@ pub const Process = struct { ) catch unreachable; defer allocator.free(args); var used: usize = 0; - const offset: usize = 1; + const offset = 1; for (bun.argv()[@min(bun.argv().len, offset)..]) |arg| { if (arg.len == 0) @@ -2548,8 +4864,8 @@ pub const Process = struct { } pub fn getCwd(globalObject: *JSC.JSGlobalObject) callconv(.C) JSC.JSValue { - var buffer: [bun.MAX_PATH_BYTES]u8 = undefined; - switch (Syscall.getcwd(&buffer)) { + var buf: PathBuffer = undefined; + switch (Syscall.getcwd(&buf)) { .err => |err| { return err.toJSC(globalObject); }, @@ -2568,7 +4884,7 @@ pub const Process = struct { return JSC.toInvalidArguments("path is required", .{}, globalObject.ref()); } - var buf: [bun.MAX_PATH_BYTES]u8 = undefined; + var buf: PathBuffer = undefined; const slice = to.sliceZBuf(&buf) catch { return JSC.toInvalidArguments("Invalid path", .{}, globalObject.ref()); }; diff --git a/src/js/node/path.ts b/src/js/node/path.ts index ba797774011ae0..11af76f295cefb 100644 --- a/src/js/node/path.ts +++ b/src/js/node/path.ts @@ -17,6 +17,7 @@ function bound(obj) { delimiter: obj.delimiter, win32: undefined, posix: undefined, + // Legacy internal API, docs-only deprecated: DEP0080 _makeLong: toNamespacedPath, }; return result; diff --git a/test/js/node/path/basename.test.js b/test/js/node/path/basename.test.js new file mode 100644 index 00000000000000..7d53a9909c265f --- /dev/null +++ b/test/js/node/path/basename.test.js @@ -0,0 +1,83 @@ +import { test, describe } from "bun:test"; +import assert from "node:assert"; +import path from "node:path"; + +describe("path.dirname", () => { + test("platform", () => { + assert.strictEqual(path.basename(__filename), "basename.test.js"); + assert.strictEqual(path.basename(__filename, ".js"), "basename.test"); + assert.strictEqual(path.basename(".js", ".js"), ""); + assert.strictEqual(path.basename("js", ".js"), "js"); + assert.strictEqual(path.basename("file.js", ".ts"), "file.js"); + assert.strictEqual(path.basename("file", ".js"), "file"); + assert.strictEqual(path.basename("file.js.old", ".js.old"), "file"); + assert.strictEqual(path.basename(""), ""); + assert.strictEqual(path.basename("/dir/basename.ext"), "basename.ext"); + assert.strictEqual(path.basename("/basename.ext"), "basename.ext"); + assert.strictEqual(path.basename("basename.ext"), "basename.ext"); + assert.strictEqual(path.basename("basename.ext/"), "basename.ext"); + assert.strictEqual(path.basename("basename.ext//"), "basename.ext"); + assert.strictEqual(path.basename("aaa/bbb", "/bbb"), "bbb"); + assert.strictEqual(path.basename("aaa/bbb", "a/bbb"), "bbb"); + assert.strictEqual(path.basename("aaa/bbb", "bbb"), "bbb"); + assert.strictEqual(path.basename("aaa/bbb//", "bbb"), "bbb"); + assert.strictEqual(path.basename("aaa/bbb", "bb"), "b"); + assert.strictEqual(path.basename("aaa/bbb", "b"), "bb"); + assert.strictEqual(path.basename("/aaa/bbb", "/bbb"), "bbb"); + assert.strictEqual(path.basename("/aaa/bbb", "a/bbb"), "bbb"); + assert.strictEqual(path.basename("/aaa/bbb", "bbb"), "bbb"); + assert.strictEqual(path.basename("/aaa/bbb//", "bbb"), "bbb"); + assert.strictEqual(path.basename("/aaa/bbb", "bb"), "b"); + assert.strictEqual(path.basename("/aaa/bbb", "b"), "bb"); + assert.strictEqual(path.basename("/aaa/bbb"), "bbb"); + assert.strictEqual(path.basename("/aaa/"), "aaa"); + assert.strictEqual(path.basename("/aaa/b"), "b"); + assert.strictEqual(path.basename("/a/b"), "b"); + assert.strictEqual(path.basename("//a"), "a"); + assert.strictEqual(path.basename("a", "a"), ""); + }); + + test("win32", () => { + // On Windows a backslash acts as a path separator. + assert.strictEqual(path.win32.basename("\\dir\\basename.ext"), "basename.ext"); + assert.strictEqual(path.win32.basename("\\basename.ext"), "basename.ext"); + assert.strictEqual(path.win32.basename("basename.ext"), "basename.ext"); + assert.strictEqual(path.win32.basename("basename.ext\\"), "basename.ext"); + assert.strictEqual(path.win32.basename("basename.ext\\\\"), "basename.ext"); + assert.strictEqual(path.win32.basename("foo"), "foo"); + assert.strictEqual(path.win32.basename("aaa\\bbb", "\\bbb"), "bbb"); + assert.strictEqual(path.win32.basename("aaa\\bbb", "a\\bbb"), "bbb"); + assert.strictEqual(path.win32.basename("aaa\\bbb", "bbb"), "bbb"); + assert.strictEqual(path.win32.basename("aaa\\bbb\\\\\\\\", "bbb"), "bbb"); + assert.strictEqual(path.win32.basename("aaa\\bbb", "bb"), "b"); + assert.strictEqual(path.win32.basename("aaa\\bbb", "b"), "bb"); + assert.strictEqual(path.win32.basename("C:"), ""); + assert.strictEqual(path.win32.basename("C:."), "."); + assert.strictEqual(path.win32.basename("C:\\"), ""); + assert.strictEqual(path.win32.basename("C:\\dir\\base.ext"), "base.ext"); + assert.strictEqual(path.win32.basename("C:\\basename.ext"), "basename.ext"); + assert.strictEqual(path.win32.basename("C:basename.ext"), "basename.ext"); + assert.strictEqual(path.win32.basename("C:basename.ext\\"), "basename.ext"); + assert.strictEqual(path.win32.basename("C:basename.ext\\\\"), "basename.ext"); + assert.strictEqual(path.win32.basename("C:foo"), "foo"); + assert.strictEqual(path.win32.basename("file:stream"), "file:stream"); + assert.strictEqual(path.win32.basename("a", "a"), ""); + }); + + test("posix", () => { + // On unix a backslash is just treated as any other character. + assert.strictEqual(path.posix.basename("\\dir\\basename.ext"), "\\dir\\basename.ext"); + assert.strictEqual(path.posix.basename("\\basename.ext"), "\\basename.ext"); + assert.strictEqual(path.posix.basename("basename.ext"), "basename.ext"); + assert.strictEqual(path.posix.basename("basename.ext\\"), "basename.ext\\"); + assert.strictEqual(path.posix.basename("basename.ext\\\\"), "basename.ext\\\\"); + assert.strictEqual(path.posix.basename("foo"), "foo"); + }); + + test("posix with control characters", () => { + // POSIX filenames may include control characters + // c.f. http://www.dwheeler.com/essays/fixing-unix-linux-filenames.html + const controlCharFilename = `Icon${String.fromCharCode(13)}`; + assert.strictEqual(path.posix.basename(`/a/b/${controlCharFilename}`), controlCharFilename); + }); +}); diff --git a/test/js/node/path/browserify.test.js b/test/js/node/path/browserify.test.js new file mode 100644 index 00000000000000..84e3fa908616f0 --- /dev/null +++ b/test/js/node/path/browserify.test.js @@ -0,0 +1,902 @@ +import { describe, it, expect, test } from "bun:test"; +import path from "node:path"; +import assert from "assert"; + +const { file } = import.meta; +const isWindows = process.platform === "win32"; +const sep = isWindows ? "\\" : "/"; + +describe("browserify path tests", () => { + const strictEqual = (...args) => { + assert.strictEqual(...args); + expect(true).toBe(true); + }; + + const expectStrictEqual = (actual, expected) => { + expect(actual).toBe(expected); + }; + + describe("dirname", () => { + it("path.dirname", () => { + const fixtures = [ + ["yo", "."], + ["/yo", "/"], + ["/yo/", "/"], + ["/yo/123", "/yo"], + [".", "."], + ["../", "."], + ["../../", ".."], + ["../../foo", "../.."], + ["../../foo/../", "../../foo"], + ["/foo/../", "/foo"], + ["../../foo/../bar", "../../foo/.."], + ]; + for (const [input, expected] of fixtures) { + expect(path.posix.dirname(input)).toBe(expected); + if (!isWindows) { + expect(path.dirname(input)).toBe(expected); + } + } + }); + it("path.posix.dirname", () => { + expect(path.posix.dirname("/a/b/")).toBe("/a"); + expect(path.posix.dirname("/a/b")).toBe("/a"); + expect(path.posix.dirname("/a")).toBe("/"); + expect(path.posix.dirname("/a/")).toBe("/"); + expect(path.posix.dirname("")).toBe("."); + expect(path.posix.dirname("/")).toBe("/"); + expect(path.posix.dirname("//")).toBe("/"); + expect(path.posix.dirname("///")).toBe("/"); + expect(path.posix.dirname("////")).toBe("/"); + expect(path.posix.dirname("//a")).toBe("//"); + expect(path.posix.dirname("//ab")).toBe("//"); + expect(path.posix.dirname("///a")).toBe("//"); + expect(path.posix.dirname("////a")).toBe("///"); + expect(path.posix.dirname("/////a")).toBe("////"); + expect(path.posix.dirname("foo")).toBe("."); + expect(path.posix.dirname("foo/")).toBe("."); + expect(path.posix.dirname("a/b")).toBe("a"); + expect(path.posix.dirname("a/")).toBe("."); + expect(path.posix.dirname("a///b")).toBe("a//"); + expect(path.posix.dirname("a//b")).toBe("a/"); + expect(path.posix.dirname("\\")).toBe("."); + expect(path.posix.dirname("\\a")).toBe("."); + expect(path.posix.dirname("a")).toBe("."); + expect(path.posix.dirname("/a/b//c")).toBe("/a/b/"); + expect(path.posix.dirname("/文檔")).toBe("/"); + expect(path.posix.dirname("/文檔/")).toBe("/"); + expect(path.posix.dirname("/文檔/新建文件夾")).toBe("/文檔"); + expect(path.posix.dirname("/文檔/新建文件夾/")).toBe("/文檔"); + expect(path.posix.dirname("//新建文件夾")).toBe("//"); + expect(path.posix.dirname("///新建文件夾")).toBe("//"); + expect(path.posix.dirname("////新建文件夾")).toBe("///"); + expect(path.posix.dirname("/////新建文件夾")).toBe("////"); + expect(path.posix.dirname("新建文件夾")).toBe("."); + expect(path.posix.dirname("新建文件夾/")).toBe("."); + expect(path.posix.dirname("文檔/新建文件夾")).toBe("文檔"); + expect(path.posix.dirname("文檔/")).toBe("."); + expect(path.posix.dirname("文檔///新建文件夾")).toBe("文檔//"); + expect(path.posix.dirname("文檔//新建文件夾")).toBe("文檔/"); + }); + it("path.win32.dirname", () => { + expect(path.win32.dirname("c:\\")).toBe("c:\\"); + expect(path.win32.dirname("c:\\foo")).toBe("c:\\"); + expect(path.win32.dirname("c:\\foo\\")).toBe("c:\\"); + expect(path.win32.dirname("c:\\foo\\bar")).toBe("c:\\foo"); + expect(path.win32.dirname("c:\\foo\\bar\\")).toBe("c:\\foo"); + expect(path.win32.dirname("c:\\foo\\bar\\baz")).toBe("c:\\foo\\bar"); + expect(path.win32.dirname("c:\\foo bar\\baz")).toBe("c:\\foo bar"); + expect(path.win32.dirname("c:\\\\foo")).toBe("c:\\"); + expect(path.win32.dirname("\\")).toBe("\\"); + expect(path.win32.dirname("\\foo")).toBe("\\"); + expect(path.win32.dirname("\\foo\\")).toBe("\\"); + expect(path.win32.dirname("\\foo\\bar")).toBe("\\foo"); + expect(path.win32.dirname("\\foo\\bar\\")).toBe("\\foo"); + expect(path.win32.dirname("\\foo\\bar\\baz")).toBe("\\foo\\bar"); + expect(path.win32.dirname("\\foo bar\\baz")).toBe("\\foo bar"); + expect(path.win32.dirname("c:")).toBe("c:"); + expect(path.win32.dirname("c:foo")).toBe("c:"); + expect(path.win32.dirname("c:foo\\")).toBe("c:"); + expect(path.win32.dirname("c:foo\\bar")).toBe("c:foo"); + expect(path.win32.dirname("c:foo\\bar\\")).toBe("c:foo"); + expect(path.win32.dirname("c:foo\\bar\\baz")).toBe("c:foo\\bar"); + expect(path.win32.dirname("c:foo bar\\baz")).toBe("c:foo bar"); + expect(path.win32.dirname("file:stream")).toBe("."); + expect(path.win32.dirname("dir\\file:stream")).toBe("dir"); + expect(path.win32.dirname("\\\\unc\\share")).toBe("\\\\unc\\share"); + expect(path.win32.dirname("\\\\unc\\share\\foo")).toBe("\\\\unc\\share\\"); + expect(path.win32.dirname("\\\\unc\\share\\foo\\")).toBe("\\\\unc\\share\\"); + expect(path.win32.dirname("\\\\unc\\share\\foo\\bar")).toBe("\\\\unc\\share\\foo"); + expect(path.win32.dirname("\\\\unc\\share\\foo\\bar\\")).toBe("\\\\unc\\share\\foo"); + expect(path.win32.dirname("\\\\unc\\share\\foo\\bar\\baz")).toBe("\\\\unc\\share\\foo\\bar"); + expect(path.win32.dirname("/a/b/")).toBe("/a"); + expect(path.win32.dirname("/a/b")).toBe("/a"); + expect(path.win32.dirname("/a")).toBe("/"); + expect(path.win32.dirname("")).toBe("."); + expect(path.win32.dirname("/")).toBe("/"); + expect(path.win32.dirname("////")).toBe("/"); + expect(path.win32.dirname("foo")).toBe("."); + expect(path.win32.dirname("c:\\")).toBe("c:\\"); + expect(path.win32.dirname("c:\\文檔")).toBe("c:\\"); + expect(path.win32.dirname("c:\\文檔\\")).toBe("c:\\"); + expect(path.win32.dirname("c:\\文檔\\新建文件夾")).toBe("c:\\文檔"); + expect(path.win32.dirname("c:\\文檔\\新建文件夾\\")).toBe("c:\\文檔"); + expect(path.win32.dirname("c:\\文檔\\新建文件夾\\baz")).toBe("c:\\文檔\\新建文件夾"); + expect(path.win32.dirname("c:\\文檔 1\\新建文件夾")).toBe("c:\\文檔 1"); + expect(path.win32.dirname("c:\\\\文檔")).toBe("c:\\"); + expect(path.win32.dirname("\\文檔")).toBe("\\"); + expect(path.win32.dirname("\\文檔\\")).toBe("\\"); + expect(path.win32.dirname("\\文檔\\新建文件夾")).toBe("\\文檔"); + expect(path.win32.dirname("\\文檔\\新建文件夾\\")).toBe("\\文檔"); + expect(path.win32.dirname("\\文檔\\新建文件夾\\baz")).toBe("\\文檔\\新建文件夾"); + expect(path.win32.dirname("\\文檔 1\\baz")).toBe("\\文檔 1"); + expect(path.win32.dirname("c:")).toBe("c:"); + expect(path.win32.dirname("c:文檔")).toBe("c:"); + expect(path.win32.dirname("c:文檔\\")).toBe("c:"); + expect(path.win32.dirname("c:文檔\\新建文件夾")).toBe("c:文檔"); + expect(path.win32.dirname("c:文檔\\新建文件夾\\")).toBe("c:文檔"); + expect(path.win32.dirname("c:文檔\\新建文件夾\\baz")).toBe("c:文檔\\新建文件夾"); + expect(path.win32.dirname("c:文檔 1\\baz")).toBe("c:文檔 1"); + expect(path.win32.dirname("/文檔/新建文件夾/")).toBe("/文檔"); + expect(path.win32.dirname("/文檔/新建文件夾")).toBe("/文檔"); + expect(path.win32.dirname("/文檔")).toBe("/"); + expect(path.win32.dirname("新建文件夾")).toBe("."); + }); + }); + + it("path.parse().name", () => { + expectStrictEqual(path.parse(file).name, "browserify.test"); + expectStrictEqual(path.parse(".js").name, ".js"); + expectStrictEqual(path.parse("..js").name, "."); + expectStrictEqual(path.parse("").name, ""); + expectStrictEqual(path.parse(".").name, "."); + expectStrictEqual(path.parse("dir/name.ext").name, "name"); + expectStrictEqual(path.parse("/dir/name.ext").name, "name"); + expectStrictEqual(path.parse("/name.ext").name, "name"); + expectStrictEqual(path.parse("name.ext").name, "name"); + expectStrictEqual(path.parse("name.ext/").name, "name"); + expectStrictEqual(path.parse("name.ext//").name, "name"); + expectStrictEqual(path.parse("aaa/bbb").name, "bbb"); + expectStrictEqual(path.parse("aaa/bbb/").name, "bbb"); + expectStrictEqual(path.parse("aaa/bbb//").name, "bbb"); + expectStrictEqual(path.parse("/aaa/bbb").name, "bbb"); + expectStrictEqual(path.parse("/aaa/bbb/").name, "bbb"); + expectStrictEqual(path.parse("/aaa/bbb//").name, "bbb"); + expectStrictEqual(path.parse("//aaa/bbb").name, ""); + expectStrictEqual(path.parse("///aaa").name, "aaa"); + expectStrictEqual(path.parse("//aaa").name, "aaa"); + expectStrictEqual(path.parse("/aaa").name, "aaa"); + expectStrictEqual(path.parse("aaa.").name, "aaa"); + + // Windows parses these as UNC roots, so name is empty there. + expectStrictEqual(path.posix.parse("//aaa/bbb/").name, "bbb"); + expectStrictEqual(path.posix.parse("//aaa/bbb//").name, "bbb"); + expectStrictEqual(path.win32.parse("//aaa/bbb/").name, ""); + expectStrictEqual(path.win32.parse("//aaa/bbb//").name, ""); + + // On unix a backslash is just treated as any other character. + expectStrictEqual(path.posix.parse("\\dir\\name.ext").name, "\\dir\\name"); + expectStrictEqual(path.posix.parse("\\name.ext").name, "\\name"); + expectStrictEqual(path.posix.parse("name.ext").name, "name"); + expectStrictEqual(path.posix.parse("name.ext\\").name, "name"); + expectStrictEqual(path.posix.parse("name.ext\\\\").name, "name"); + }); + + it("path.parse() windows edition", () => { + // On Windows a backslash acts as a path separator. + expectStrictEqual(path.win32.parse("\\dir\\name.ext").name, "name"); + expectStrictEqual(path.win32.parse("\\name.ext").name, "name"); + expectStrictEqual(path.win32.parse("name.ext").name, "name"); + expectStrictEqual(path.win32.parse("name.ext\\").name, "name"); + expectStrictEqual(path.win32.parse("name.ext\\\\").name, "name"); + expectStrictEqual(path.win32.parse("name").name, "name"); + expectStrictEqual(path.win32.parse(".name").name, ".name"); + expectStrictEqual(path.win32.parse("file:stream").name, "file:stream"); + }); + + it("path.parse() windows edition - drive letter", () => { + expectStrictEqual(path.win32.parse("C:").name, ""); + expectStrictEqual(path.win32.parse("C:.").name, "."); + expectStrictEqual(path.win32.parse("C:\\").name, ""); + expectStrictEqual(path.win32.parse("C:\\.").name, "."); + expectStrictEqual(path.win32.parse("C:\\.ext").name, ".ext"); + expectStrictEqual(path.win32.parse("C:\\dir\\name.ext").name, "name"); + expectStrictEqual(path.win32.parse("C:name.ext").name, "name"); + expectStrictEqual(path.win32.parse("C:name.ext\\").name, "name"); + expectStrictEqual(path.win32.parse("C:name.ext\\\\").name, "name"); + expectStrictEqual(path.win32.parse("C:foo").name, "foo"); + expectStrictEqual(path.win32.parse("C:.foo").name, ".foo"); + }); + + it("path.parse() windows edition - .root", () => { + expectStrictEqual(path.win32.parse("C:").root, "C:"); + expectStrictEqual(path.win32.parse("C:.").root, "C:"); + expectStrictEqual(path.win32.parse("C:\\").root, "C:\\"); + expectStrictEqual(path.win32.parse("C:\\.").root, "C:\\"); + expectStrictEqual(path.win32.parse("C:\\.ext").root, "C:\\"); + expectStrictEqual(path.win32.parse("C:\\dir\\name.ext").root, "C:\\"); + expectStrictEqual(path.win32.parse("C:name.ext").root, "C:"); + expectStrictEqual(path.win32.parse("C:name.ext\\").root, "C:"); + expectStrictEqual(path.win32.parse("C:name.ext\\\\").root, "C:"); + expectStrictEqual(path.win32.parse("C:foo").root, "C:"); + expectStrictEqual(path.win32.parse("C:.foo").root, "C:"); + expectStrictEqual(path.win32.parse("/:.foo").root, "/"); + }); + + it("path.basename", () => { + strictEqual(path.basename(file), "browserify.test.js"); + strictEqual(path.basename(file, ".js"), "browserify.test"); + strictEqual(path.basename(".js", ".js"), ""); + strictEqual(path.basename(""), ""); + strictEqual(path.basename("/dir/basename.ext"), "basename.ext"); + strictEqual(path.basename("/basename.ext"), "basename.ext"); + strictEqual(path.basename("basename.ext"), "basename.ext"); + strictEqual(path.basename("basename.ext/"), "basename.ext"); + strictEqual(path.basename("basename.ext//"), "basename.ext"); + strictEqual(path.basename("aaa/bbb", "/bbb"), "bbb"); + strictEqual(path.basename("aaa/bbb", "a/bbb"), "bbb"); + strictEqual(path.basename("aaa/bbb", "bbb"), "bbb"); + strictEqual(path.basename("aaa/bbb//", "bbb"), "bbb"); + strictEqual(path.basename("aaa/bbb", "bb"), "b"); + strictEqual(path.basename("aaa/bbb", "b"), "bb"); + strictEqual(path.basename("/aaa/bbb", "/bbb"), "bbb"); + strictEqual(path.basename("/aaa/bbb", "a/bbb"), "bbb"); + strictEqual(path.basename("/aaa/bbb", "bbb"), "bbb"); + strictEqual(path.basename("/aaa/bbb//", "bbb"), "bbb"); + strictEqual(path.basename("/aaa/bbb", "bb"), "b"); + strictEqual(path.basename("/aaa/bbb", "b"), "bb"); + strictEqual(path.basename("/aaa/bbb"), "bbb"); + strictEqual(path.basename("/aaa/"), "aaa"); + strictEqual(path.basename("/aaa/b"), "b"); + strictEqual(path.basename("/a/b"), "b"); + strictEqual(path.basename("//a"), "a"); + strictEqual(path.basename("a", "a"), ""); + + // On Windows a backslash acts as a path separator. + strictEqual(path.win32.basename("\\dir\\basename.ext"), "basename.ext"); + strictEqual(path.win32.basename("\\basename.ext"), "basename.ext"); + strictEqual(path.win32.basename("basename.ext"), "basename.ext"); + strictEqual(path.win32.basename("basename.ext\\"), "basename.ext"); + strictEqual(path.win32.basename("basename.ext\\\\"), "basename.ext"); + strictEqual(path.win32.basename("foo"), "foo"); + strictEqual(path.win32.basename("aaa\\bbb", "\\bbb"), "bbb"); + strictEqual(path.win32.basename("aaa\\bbb", "a\\bbb"), "bbb"); + strictEqual(path.win32.basename("aaa\\bbb", "bbb"), "bbb"); + strictEqual(path.win32.basename("aaa\\bbb\\\\\\\\", "bbb"), "bbb"); + strictEqual(path.win32.basename("aaa\\bbb", "bb"), "b"); + strictEqual(path.win32.basename("aaa\\bbb", "b"), "bb"); + strictEqual(path.win32.basename("C:"), ""); + strictEqual(path.win32.basename("C:."), "."); + strictEqual(path.win32.basename("C:\\"), ""); + strictEqual(path.win32.basename("C:\\dir\\base.ext"), "base.ext"); + strictEqual(path.win32.basename("C:\\basename.ext"), "basename.ext"); + strictEqual(path.win32.basename("C:basename.ext"), "basename.ext"); + strictEqual(path.win32.basename("C:basename.ext\\"), "basename.ext"); + strictEqual(path.win32.basename("C:basename.ext\\\\"), "basename.ext"); + strictEqual(path.win32.basename("C:foo"), "foo"); + strictEqual(path.win32.basename("file:stream"), "file:stream"); + strictEqual(path.win32.basename("a", "a"), ""); + + // On unix a backslash is just treated as any other character. + strictEqual(path.posix.basename("\\dir\\basename.ext"), "\\dir\\basename.ext"); + strictEqual(path.posix.basename("\\basename.ext"), "\\basename.ext"); + strictEqual(path.posix.basename("basename.ext"), "basename.ext"); + strictEqual(path.posix.basename("basename.ext\\"), "basename.ext\\"); + strictEqual(path.posix.basename("basename.ext\\\\"), "basename.ext\\\\"); + strictEqual(path.posix.basename("foo"), "foo"); + + // POSIX filenames may include control characters + // c.f. http://www.dwheeler.com/essays/fixing-unix-linux-filenames.html + const controlCharFilename = `Icon${String.fromCharCode(13)}`; + strictEqual(path.posix.basename(`/a/b/${controlCharFilename}`), controlCharFilename); + }); + + describe("path.join #5769", () => { + for (let length of [4096, 4095, 4097, 65_432, 65_431, 65_433]) { + it("length " + length, () => { + const tooLengthyFolderName = Array.from({ length }).fill("b").join(""); + expect(path.join(tooLengthyFolderName)).toEqual("b".repeat(length)); + }); + it("length " + length + "joined", () => { + const tooLengthyFolderName = Array.from({ length }).fill("b"); + expect(path.join(...tooLengthyFolderName)).toEqual(("b" + sep).repeat(length).substring(0, 2 * length - 1)); + }); + } + }); + + it("path.join", () => { + const failures = []; + const backslashRE = /\\/g; + + const joinTests = [ + [ + [path.posix.join], + // Arguments result + [ + [[".", "x/b", "..", "/b/c.js"], "x/b/c.js"], + [[], "."], + [["/.", "x/b", "..", "/b/c.js"], "/x/b/c.js"], + [["/foo", "../../../bar"], "/bar"], + [["foo", "../../../bar"], "../../bar"], + [["foo/", "../../../bar"], "../../bar"], + [["foo/x", "../../../bar"], "../bar"], + [["foo/x", "./bar"], "foo/x/bar"], + [["foo/x/", "./bar"], "foo/x/bar"], + [["foo/x/", ".", "bar"], "foo/x/bar"], + [["./"], "./"], + [[".", "./"], "./"], + [[".", ".", "."], "."], + [[".", "./", "."], "."], + [[".", "/./", "."], "."], + [[".", "/////./", "."], "."], + [["."], "."], + [["", "."], "."], + [["", "foo"], "foo"], + [["foo", "/bar"], "foo/bar"], + [["", "/foo"], "/foo"], + [["", "", "/foo"], "/foo"], + [["", "", "foo"], "foo"], + [["foo", ""], "foo"], + [["foo/", ""], "foo/"], + [["foo", "", "/bar"], "foo/bar"], + [["./", "..", "/foo"], "../foo"], + [["./", "..", "..", "/foo"], "../../foo"], + [[".", "..", "..", "/foo"], "../../foo"], + [["", "..", "..", "/foo"], "../../foo"], + [["/"], "/"], + [["/", "."], "/"], + [["/", ".."], "/"], + [["/", "..", ".."], "/"], + [[""], "."], + [["", ""], "."], + [[" /foo"], " /foo"], + [[" ", "foo"], " /foo"], + [[" ", "."], " "], + [[" ", "/"], " /"], + [[" ", ""], " "], + [["/", "foo"], "/foo"], + [["/", "/foo"], "/foo"], + [["/", "//foo"], "/foo"], + [["/", "", "/foo"], "/foo"], + [["", "/", "foo"], "/foo"], + [["", "/", "/foo"], "/foo"], + ], + ], + ]; + + // Windows-specific join tests + joinTests.push([ + path.win32.join, + joinTests[0][1].slice(0).concat([ + // Arguments result + // UNC path expected + [["//foo/bar"], "\\\\foo\\bar\\"], + [["\\/foo/bar"], "\\\\foo\\bar\\"], + [["\\\\foo/bar"], "\\\\foo\\bar\\"], + // UNC path expected - server and share separate + [["//foo", "bar"], "\\\\foo\\bar\\"], + [["//foo/", "bar"], "\\\\foo\\bar\\"], + [["//foo", "/bar"], "\\\\foo\\bar\\"], + // UNC path expected - questionable + [["//foo", "", "bar"], "\\\\foo\\bar\\"], + // [["//foo/", "", "bar"], "\\\\foo\\bar\\"], + [["//foo/", "", "/bar"], "\\\\foo\\bar\\"], + // UNC path expected - even more questionable + [["", "//foo", "bar"], "\\\\foo\\bar\\"], + [["", "//foo/", "bar"], "\\\\foo\\bar\\"], + [["", "//foo/", "/bar"], "\\\\foo\\bar\\"], + // No UNC path expected (no double slash in first component) + [["\\", "foo/bar"], "\\foo\\bar"], + [["\\", "/foo/bar"], "\\foo\\bar"], + [["", "/", "/foo/bar"], "\\foo\\bar"], + // No UNC path expected (no non-slashes in first component - + // questionable) + [["//", "foo/bar"], "\\foo\\bar"], + [["//", "/foo/bar"], "\\foo\\bar"], + [["\\\\", "/", "/foo/bar"], "\\foo\\bar"], + [["//"], "\\"], + // No UNC path expected (share name missing - questionable). + [["//foo"], "\\foo"], + [["//foo/"], "\\foo\\"], + [["//foo", "/"], "\\foo\\"], + [["//foo", "", "/"], "\\foo\\"], + // No UNC path expected (too many leading slashes - questionable) + [["///foo/bar"], "\\foo\\bar"], + [["////foo", "bar"], "\\foo\\bar"], + [["\\\\\\/foo/bar"], "\\foo\\bar"], + // Drive-relative vs drive-absolute paths. This merely describes the + // status quo, rather than being obviously right + [["c:"], "c:."], + [["c:."], "c:."], + [["c:", ""], "c:."], + [["", "c:"], "c:."], + [["c:.", "/"], "c:.\\"], + [["c:.", "file"], "c:file"], + [["c:", "/"], "c:\\"], + [["c:", "file"], "c:\\file"], + ]), + ]); + joinTests.forEach(test => { + if (!Array.isArray(test[0])) test[0] = [test[0]]; + test[0].forEach(join => { + test[1].forEach(test => { + const actual = join.apply(null, test[0]); + const expected = test[1]; + // For non-Windows specific tests with the Windows join(), we need to try + // replacing the slashes since the non-Windows specific tests' `expected` + // use forward slashes + let actualAlt; + let os; + let displayExpected = expected; + if (join === path.win32.join) { + actualAlt = actual.replace(backslashRE, "/"); + displayExpected = expected.replace(/\//g, "\\"); + os = "win32"; + } else { + os = "posix"; + } + if (actual !== expected && actualAlt !== expected) { + const delimiter = test[0].map(JSON.stringify).join(","); + const message = `path.${os}.join(${delimiter})\n expect=${JSON.stringify( + displayExpected, + )}\n actual=${JSON.stringify(actual)}`; + failures.push(`\n${message}`); + } + }); + }); + }); + strictEqual(failures.length, 0, failures.join("")); + }); + + it("path.relative", () => { + const failures = []; + const cwd = process.cwd(); + const cwdParent = path.dirname(cwd); + const parentIsRoot = isWindows ? cwdParent.match(/^[A-Z]:\\$/) : cwdParent === "/"; + + const relativeTests = [ + [ + path.win32.relative, + // Arguments result + [ + ["c:/blah\\blah", "d:/games", "d:\\games"], + ["c:/aaaa/bbbb", "c:/aaaa", ".."], + ["c:/aaaa/bbbb", "c:/cccc", "..\\..\\cccc"], + ["c:/aaaa/bbbb", "c:/aaaa/bbbb", ""], + ["c:/aaaa/bbbb", "c:/aaaa/cccc", "..\\cccc"], + ["c:/aaaa/", "c:/aaaa/cccc", "cccc"], + ["c:/", "c:\\aaaa\\bbbb", "aaaa\\bbbb"], + ["c:/aaaa/bbbb", "d:\\", "d:\\"], + ["c:/AaAa/bbbb", "c:/aaaa/bbbb", ""], + ["c:/aaaaa/", "c:/aaaa/cccc", "..\\aaaa\\cccc"], + ["C:\\foo\\bar\\baz\\quux", "C:\\", "..\\..\\..\\.."], + ["C:\\foo\\test", "C:\\foo\\test\\bar\\package.json", "bar\\package.json"], + ["C:\\foo\\bar\\baz-quux", "C:\\foo\\bar\\baz", "..\\baz"], + ["C:\\foo\\bar\\baz", "C:\\foo\\bar\\baz-quux", "..\\baz-quux"], + ["\\\\foo\\bar", "\\\\foo\\bar\\baz", "baz"], + ["\\\\foo\\bar\\baz", "\\\\foo\\bar", ".."], + ["\\\\foo\\bar\\baz-quux", "\\\\foo\\bar\\baz", "..\\baz"], + ["\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz-quux", "..\\baz-quux"], + ["C:\\baz-quux", "C:\\baz", "..\\baz"], + ["C:\\baz", "C:\\baz-quux", "..\\baz-quux"], + ["\\\\foo\\baz-quux", "\\\\foo\\baz", "..\\baz"], + ["\\\\foo\\baz", "\\\\foo\\baz-quux", "..\\baz-quux"], + // ["C:\\baz", "\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz"], + ["\\\\foo\\bar\\baz", "C:\\baz", "C:\\baz"], + ["C:\\dev\\test", "C:\\dev\\test\\hello.test.ts", "hello.test.ts"], + ], + ], + [ + path.posix.relative, + // Arguments result + [ + ["/var/lib", "/var", ".."], + ["/var/lib", "/bin", "../../bin"], + ["/var/lib", "/var/lib", ""], + ["/var/lib", "/var/apache", "../apache"], + ["/var/", "/var/lib", "lib"], + ["/", "/var/lib", "var/lib"], + ["/foo/test", "/foo/test/bar/package.json", "bar/package.json"], + ["/Users/a/web/b/test/mails", "/Users/a/web/b", "../.."], + ["/foo/bar/baz-quux", "/foo/bar/baz", "../baz"], + ["/foo/bar/baz", "/foo/bar/baz-quux", "../baz-quux"], + ["/baz-quux", "/baz", "../baz"], + ["/baz", "/baz-quux", "../baz-quux"], + ["/page1/page2/foo", "/", "../../.."], + [path.posix.resolve("."), "foo", "foo"], + ["/webpack", "/webpack", ""], + ["/webpack/", "/webpack", ""], + ["/webpack", "/webpack/", ""], + ["/webpack/", "/webpack/", ""], + ["/webpack-hot-middleware", "/webpack/buildin/module.js", "../webpack/buildin/module.js"], + ["/webp4ck-hot-middleware", "/webpack/buildin/module.js", "../webpack/buildin/module.js"], + ["/webpack-hot-middleware", "/webp4ck/buildin/module.js", "../webp4ck/buildin/module.js"], + ["/var/webpack-hot-middleware", "/var/webpack/buildin/module.js", "../webpack/buildin/module.js"], + ["/app/node_modules/pkg", "../static", `../../..${parentIsRoot ? "" : path.posix.resolve("../")}/static`], + [ + "/app/node_modules/pkg", + "../../static", + `../../..${parentIsRoot ? "" : path.posix.resolve("../../")}/static`, + ], + ["/app", "../static", `..${parentIsRoot ? "" : path.posix.resolve("../")}/static`], + ["/app", "../".repeat(64) + "static", "../static"], + [".", "../static", cwd == "/" ? "static" : "../static"], + ["/", "../static", parentIsRoot ? "static" : `${path.posix.resolve("../")}/static`.slice(1)], + ["../", "../", ""], + ["../", "../../", parentIsRoot ? "" : ".."], + ["../../", "../", parentIsRoot ? "" : path.basename(cwdParent)], + ["../../", "../../", ""], + ], + ], + ]; + + relativeTests.forEach(test => { + const relative = test[0]; + test[1].forEach(test => { + const actual = relative(test[0], test[1]); + const expected = test[2]; + if (actual !== expected) { + const os = relative === path.win32.relative ? "win32" : "posix"; + const message = `path.${os}.relative(${test + .slice(0, 2) + .map(JSON.stringify) + .join(",")})\n expect=${JSON.stringify(expected)}\n actual=${JSON.stringify(actual)}`; + failures.push(`\n${message}`); + } + }); + }); + + strictEqual(failures.length, 0, failures.join("")); + expect(true).toBe(true); + }); + + it("path.normalize", () => { + strictEqual(path.win32.normalize("./fixtures///b/../b/c.js"), "fixtures\\b\\c.js"); + strictEqual(path.win32.normalize("/foo/../../../bar"), "\\bar"); + strictEqual(path.win32.normalize("a//b//../b"), "a\\b"); + strictEqual(path.win32.normalize("a//b//./c"), "a\\b\\c"); + strictEqual(path.win32.normalize("a//b//."), "a\\b"); + strictEqual(path.win32.normalize("//server/share/dir/file.ext"), "\\\\server\\share\\dir\\file.ext"); + strictEqual(path.win32.normalize("/a/b/c/../../../x/y/z"), "\\x\\y\\z"); + strictEqual(path.win32.normalize("C:"), "C:."); + strictEqual(path.win32.normalize("C:..\\abc"), "C:..\\abc"); + strictEqual(path.win32.normalize("C:..\\..\\abc\\..\\def"), "C:..\\..\\def"); + strictEqual(path.win32.normalize("C:\\."), "C:\\"); + strictEqual(path.win32.normalize("file:stream"), "file:stream"); + strictEqual(path.win32.normalize("bar\\foo..\\..\\"), "bar\\"); + strictEqual(path.win32.normalize("bar\\foo..\\.."), "bar"); + strictEqual(path.win32.normalize("bar\\foo..\\..\\baz"), "bar\\baz"); + strictEqual(path.win32.normalize("bar\\foo..\\"), "bar\\foo..\\"); + strictEqual(path.win32.normalize("bar\\foo.."), "bar\\foo.."); + strictEqual(path.win32.normalize("..\\foo..\\..\\..\\bar"), "..\\..\\bar"); + strictEqual(path.win32.normalize("..\\...\\..\\.\\...\\..\\..\\bar"), "..\\..\\bar"); + strictEqual(path.win32.normalize("../../../foo/../../../bar"), "..\\..\\..\\..\\..\\bar"); + strictEqual(path.win32.normalize("../../../foo/../../../bar/../../"), "..\\..\\..\\..\\..\\..\\"); + strictEqual(path.win32.normalize("../foobar/barfoo/foo/../../../bar/../../"), "..\\..\\"); + strictEqual(path.win32.normalize("../.../../foobar/../../../bar/../../baz"), "..\\..\\..\\..\\baz"); + strictEqual(path.win32.normalize("foo/bar\\baz"), "foo\\bar\\baz"); + strictEqual(path.posix.normalize("./fixtures///b/../b/c.js"), "fixtures/b/c.js"); + strictEqual(path.posix.normalize("/foo/../../../bar"), "/bar"); + strictEqual(path.posix.normalize("a//b//../b"), "a/b"); + strictEqual(path.posix.normalize("a//b//./c"), "a/b/c"); + strictEqual(path.posix.normalize("a//b//."), "a/b"); + strictEqual(path.posix.normalize("/a/b/c/../../../x/y/z"), "/x/y/z"); + strictEqual(path.posix.normalize("///..//./foo/.//bar"), "/foo/bar"); + strictEqual(path.posix.normalize("bar/foo../../"), "bar/"); + strictEqual(path.posix.normalize("bar/foo../.."), "bar"); + strictEqual(path.posix.normalize("bar/foo../../baz"), "bar/baz"); + strictEqual(path.posix.normalize("bar/foo../"), "bar/foo../"); + strictEqual(path.posix.normalize("bar/foo.."), "bar/foo.."); + strictEqual(path.posix.normalize("../foo../../../bar"), "../../bar"); + strictEqual(path.posix.normalize("../.../.././.../../../bar"), "../../bar"); + strictEqual(path.posix.normalize("../../../foo/../../../bar"), "../../../../../bar"); + strictEqual(path.posix.normalize("../../../foo/../../../bar/../../"), "../../../../../../"); + strictEqual(path.posix.normalize("../foobar/barfoo/foo/../../../bar/../../"), "../../"); + strictEqual(path.posix.normalize("../.../../foobar/../../../bar/../../baz"), "../../../../baz"); + strictEqual(path.posix.normalize("foo/bar\\baz"), "foo/bar\\baz"); + strictEqual(path.posix.normalize(""), "."); + }); + + it("path.resolve", () => { + const failures = []; + const slashRE = /\//g; + const backslashRE = /\\/g; + + const resolveTests = [ + [ + path.win32.resolve, + // Arguments result + [ + [["c:/blah\\blah", "d:/games", "c:../a"], "c:\\blah\\a"], + [["c:/ignore", "d:\\a/b\\c/d", "\\e.exe"], "d:\\e.exe"], + [["c:/ignore", "c:/some/file"], "c:\\some\\file"], + [["d:/ignore", "d:some/dir//"], "d:\\ignore\\some\\dir"], + [["."], process.cwd()], + [["//server/share", "..", "relative\\"], "\\\\server\\share\\relative"], + [["c:/", "//"], "c:\\"], + [["c:/", "//dir"], "c:\\dir"], + [["c:/", "//server/share"], "\\\\server\\share\\"], + [["c:/", "//server//share"], "\\\\server\\share\\"], + [["c:/", "///some//dir"], "c:\\some\\dir"], + [["C:\\foo\\tmp.3\\", "..\\tmp.3\\cycles\\root.js"], "C:\\foo\\tmp.3\\cycles\\root.js"], + ], + ], + [ + path.posix.resolve, + // Arguments result + [ + [["/var/lib", "../", "file/"], "/var/file"], + [["/var/lib", "/../", "file/"], "/file"], + [["a/b/c/", "../../.."], isWindows ? process.cwd().slice(2).replaceAll("\\", "/") : process.cwd()], + [["."], isWindows ? process.cwd().slice(2).replaceAll("\\", "/") : process.cwd()], + [["/some/dir", ".", "/absolute/"], "/absolute"], + [["/foo/tmp.3/", "../tmp.3/cycles/root.js"], "/foo/tmp.3/cycles/root.js"], + ], + ], + ]; + resolveTests.forEach(([resolve, tests]) => { + tests.forEach(([test, expected]) => { + const actual = resolve.apply(null, test); + let actualAlt; + const os = resolve === path.win32.resolve ? "win32" : "posix"; + if (resolve === path.win32.resolve && !isWindows) actualAlt = actual.replace(backslashRE, "/"); + else if (resolve !== path.win32.resolve && isWindows) actualAlt = actual.replace(slashRE, "\\"); + + const message = `path.${os}.resolve(${test.map(JSON.stringify).join(",")})\n expect=${JSON.stringify( + expected, + )}\n actual=${JSON.stringify(actual)}`; + if (actual !== expected && actualAlt !== expected) failures.push(message); + }); + }); + strictEqual(failures.length, 0, failures.join("\n")); + }); + + describe("path.posix.parse and path.posix.format", () => { + const testCases = [ + { + input: "/tmp/test.txt", + expected: { + root: "/", + dir: "/tmp", + base: "test.txt", + ext: ".txt", + name: "test", + }, + }, + { + input: "/tmp/test/file.txt", + expected: { + root: "/", + dir: "/tmp/test", + base: "file.txt", + ext: ".txt", + name: "file", + }, + }, + { + input: "/tmp/test/dir", + expected: { + root: "/", + dir: "/tmp/test", + base: "dir", + ext: "", + name: "dir", + }, + }, + { + input: "/tmp/test/dir/", + expected: { + root: "/", + dir: "/tmp/test", + base: "dir", + ext: "", + name: "dir", + }, + }, + { + input: ".", + expected: { + root: "", + dir: "", + base: ".", + ext: "", + name: ".", + }, + }, + { + input: "./", + expected: { + root: "", + dir: "", + base: ".", + ext: "", + name: ".", + }, + }, + { + input: "/.", + expected: { + root: "/", + dir: "/", + base: ".", + ext: "", + name: ".", + }, + }, + { + input: "/../", + expected: { + root: "/", + dir: "/", + base: "..", + ext: ".", + name: ".", + }, + }, + { + input: "./file.txt", + expected: { + root: "", + dir: ".", + base: "file.txt", + ext: ".txt", + name: "file", + }, + }, + { + input: "../file.txt", + expected: { + root: "", + dir: "..", + base: "file.txt", + ext: ".txt", + name: "file", + }, + }, + { + input: "../test/file.txt", + expected: { + root: "", + dir: "../test", + base: "file.txt", + ext: ".txt", + name: "file", + }, + }, + { + input: "test/file.txt", + expected: { + root: "", + dir: "test", + base: "file.txt", + ext: ".txt", + name: "file", + }, + }, + { + input: "test/dir", + expected: { + root: "", + dir: "test", + base: "dir", + ext: "", + name: "dir", + }, + }, + { + input: "test/dir/another_dir", + expected: { + root: "", + dir: "test/dir", + base: "another_dir", + ext: "", + name: "another_dir", + }, + }, + { + input: "./dir", + expected: { + root: "", + dir: ".", + base: "dir", + ext: "", + name: "dir", + }, + }, + { + input: "../dir", + expected: { + root: "", + dir: "..", + base: "dir", + ext: "", + name: "dir", + }, + }, + { + input: "../dir/another_dir", + expected: { + root: "", + dir: "../dir", + base: "another_dir", + ext: "", + name: "another_dir", + }, + }, + { + // https://github.com/oven-sh/bun/issues/4954 + input: "/test/Ł.txt", + expected: { + root: "/", + dir: "/test", + base: "Ł.txt", + ext: ".txt", + name: "Ł", + }, + }, + { + // https://github.com/oven-sh/bun/issues/8090 + input: ".prettierrc", + expected: { + root: "", + dir: "", + base: ".prettierrc", + ext: "", + name: ".prettierrc", + }, + }, + ]; + testCases.forEach(({ input, expected }) => { + it(`case ${input}`, () => { + const parsed = path.posix.parse(input); + expect(parsed).toStrictEqual(expected); + + const formatted = path.posix.format(parsed); + expect(formatted).toStrictEqual(input.slice(-1) === "/" ? input.slice(0, -1) : input); + }); + }); + it("empty string arguments, issue #4005", () => { + expect( + path.posix.format({ + root: "", + dir: "", + base: "", + name: "foo", + ext: ".ts", + }), + ).toStrictEqual("foo.ts"); + expect( + path.posix.format({ + name: "foo", + ext: ".ts", + }), + ).toStrictEqual("foo.ts"); + }); + }); + + test("path.format works for vite's example", () => { + expect( + path.format({ + root: "", + dir: "", + name: "index", + base: undefined, + ext: ".css", + }), + ).toBe("index.css"); + }); + + it("path.extname", () => { + expect(path.extname("index.js")).toBe(".js"); + expect(path.extname("make_plot.🔥")).toBe(".🔥"); + }); + + describe("isAbsolute", () => { + it("win32 /foo/bar", () => expect(path.win32.isAbsolute("/foo/bar")).toBe(true)); + it("posix /foo/bar", () => expect(path.posix.isAbsolute("/foo/bar")).toBe(true)); + it("win32 \\hello\\world", () => expect(path.win32.isAbsolute("\\hello\\world")).toBe(true)); + it("posix \\hello\\world", () => expect(path.posix.isAbsolute("\\hello\\world")).toBe(false)); + it("win32 C:\\hello\\world", () => expect(path.win32.isAbsolute("C:\\hello\\world")).toBe(true)); + it("posix C:\\hello\\world", () => expect(path.posix.isAbsolute("C:\\hello\\world")).toBe(false)); + }); +}); diff --git a/test/js/node/path/dirname.test.js b/test/js/node/path/dirname.test.js new file mode 100644 index 00000000000000..8e0ebde3074c38 --- /dev/null +++ b/test/js/node/path/dirname.test.js @@ -0,0 +1,61 @@ +import { test, describe } from "bun:test"; +import assert from "node:assert"; +import path from "node:path"; + +const isWindows = process.platform === "win32"; + +describe("path.dirname", () => { + test("platform", () => { + assert.strictEqual(path.dirname(__filename).substr(-9), isWindows ? "node\\path" : "node/path"); + }); + + test("win32", () => { + assert.strictEqual(path.win32.dirname("c:\\"), "c:\\"); + assert.strictEqual(path.win32.dirname("c:\\foo"), "c:\\"); + assert.strictEqual(path.win32.dirname("c:\\foo\\"), "c:\\"); + assert.strictEqual(path.win32.dirname("c:\\foo\\bar"), "c:\\foo"); + assert.strictEqual(path.win32.dirname("c:\\foo\\bar\\"), "c:\\foo"); + assert.strictEqual(path.win32.dirname("c:\\foo\\bar\\baz"), "c:\\foo\\bar"); + assert.strictEqual(path.win32.dirname("c:\\foo bar\\baz"), "c:\\foo bar"); + assert.strictEqual(path.win32.dirname("\\"), "\\"); + assert.strictEqual(path.win32.dirname("\\foo"), "\\"); + assert.strictEqual(path.win32.dirname("\\foo\\"), "\\"); + assert.strictEqual(path.win32.dirname("\\foo\\bar"), "\\foo"); + assert.strictEqual(path.win32.dirname("\\foo\\bar\\"), "\\foo"); + assert.strictEqual(path.win32.dirname("\\foo\\bar\\baz"), "\\foo\\bar"); + assert.strictEqual(path.win32.dirname("\\foo bar\\baz"), "\\foo bar"); + assert.strictEqual(path.win32.dirname("c:"), "c:"); + assert.strictEqual(path.win32.dirname("c:foo"), "c:"); + assert.strictEqual(path.win32.dirname("c:foo\\"), "c:"); + assert.strictEqual(path.win32.dirname("c:foo\\bar"), "c:foo"); + assert.strictEqual(path.win32.dirname("c:foo\\bar\\"), "c:foo"); + assert.strictEqual(path.win32.dirname("c:foo\\bar\\baz"), "c:foo\\bar"); + assert.strictEqual(path.win32.dirname("c:foo bar\\baz"), "c:foo bar"); + assert.strictEqual(path.win32.dirname("file:stream"), "."); + assert.strictEqual(path.win32.dirname("dir\\file:stream"), "dir"); + assert.strictEqual(path.win32.dirname("\\\\unc\\share"), "\\\\unc\\share"); + assert.strictEqual(path.win32.dirname("\\\\unc\\share\\foo"), "\\\\unc\\share\\"); + assert.strictEqual(path.win32.dirname("\\\\unc\\share\\foo\\"), "\\\\unc\\share\\"); + assert.strictEqual(path.win32.dirname("\\\\unc\\share\\foo\\bar"), "\\\\unc\\share\\foo"); + assert.strictEqual(path.win32.dirname("\\\\unc\\share\\foo\\bar\\"), "\\\\unc\\share\\foo"); + assert.strictEqual(path.win32.dirname("\\\\unc\\share\\foo\\bar\\baz"), "\\\\unc\\share\\foo\\bar"); + assert.strictEqual(path.win32.dirname("/a/b/"), "/a"); + assert.strictEqual(path.win32.dirname("/a/b"), "/a"); + assert.strictEqual(path.win32.dirname("/a"), "/"); + assert.strictEqual(path.win32.dirname(""), "."); + assert.strictEqual(path.win32.dirname("/"), "/"); + assert.strictEqual(path.win32.dirname("////"), "/"); + assert.strictEqual(path.win32.dirname("foo"), "."); + }); + + test("posix", () => { + assert.strictEqual(path.posix.dirname("/a/b/"), "/a"); + assert.strictEqual(path.posix.dirname("/a/b"), "/a"); + assert.strictEqual(path.posix.dirname("/a"), "/"); + assert.strictEqual(path.posix.dirname(""), "."); + assert.strictEqual(path.posix.dirname("/"), "/"); + assert.strictEqual(path.posix.dirname("////"), "/"); + assert.strictEqual(path.posix.dirname("//a"), "//"); + assert.strictEqual(path.posix.dirname("foo"), "."); + }); +}); diff --git a/test/js/node/path/extname.test.js b/test/js/node/path/extname.test.js new file mode 100644 index 00000000000000..58f95661911cf6 --- /dev/null +++ b/test/js/node/path/extname.test.js @@ -0,0 +1,107 @@ +import { test, describe } from "bun:test"; +import assert from "node:assert"; +import path from "node:path"; + +describe("path.extname", () => { + test("general", () => { + const failures = []; + const slashRE = /\//g; + + const testPaths = [ + [__filename, ".js"], + ["", ""], + ["/path/to/file", ""], + ["/path/to/file.ext", ".ext"], + ["/path.to/file.ext", ".ext"], + ["/path.to/file", ""], + ["/path.to/.file", ""], + ["/path.to/.file.ext", ".ext"], + ["/path/to/f.ext", ".ext"], + ["/path/to/..ext", ".ext"], + ["/path/to/..", ""], + ["file", ""], + ["file.ext", ".ext"], + [".file", ""], + [".file.ext", ".ext"], + ["/file", ""], + ["/file.ext", ".ext"], + ["/.file", ""], + ["/.file.ext", ".ext"], + [".path/file.ext", ".ext"], + ["file.ext.ext", ".ext"], + ["file.", "."], + [".", ""], + ["./", ""], + [".file.ext", ".ext"], + [".file", ""], + [".file.", "."], + [".file..", "."], + ["..", ""], + ["../", ""], + ["..file.ext", ".ext"], + ["..file", ".file"], + ["..file.", "."], + ["..file..", "."], + ["...", "."], + ["...ext", ".ext"], + ["....", "."], + ["file.ext/", ".ext"], + ["file.ext//", ".ext"], + ["file/", ""], + ["file//", ""], + ["file./", "."], + ["file.//", "."], + ]; + + for (const testPath of testPaths) { + const expected = testPath[1]; + const extNames = [path.posix.extname, path.win32.extname]; + for (const extname of extNames) { + let input = testPath[0]; + let os; + if (extname === path.win32.extname) { + input = input.replace(slashRE, "\\"); + os = "win32"; + } else { + os = "posix"; + } + const actual = extname(input); + const message = `path.${os}.extname(${JSON.stringify(input)})\n expect=${JSON.stringify( + expected, + )}\n actual=${JSON.stringify(actual)}`; + if (actual !== expected) failures.push(`\n${message}`); + } + const input = `C:${testPath[0].replace(slashRE, "\\")}`; + const actual = path.win32.extname(input); + const message = `path.win32.extname(${JSON.stringify(input)})\n expect=${JSON.stringify( + expected, + )}\n actual=${JSON.stringify(actual)}`; + if (actual !== expected) failures.push(`\n${message}`); + } + assert.strictEqual(failures.length, 0, failures.join("")); + }); + + test("win32", () => { + // On Windows, backslash is a path separator. + assert.strictEqual(path.win32.extname(".\\"), ""); + assert.strictEqual(path.win32.extname("..\\"), ""); + assert.strictEqual(path.win32.extname("file.ext\\"), ".ext"); + assert.strictEqual(path.win32.extname("file.ext\\\\"), ".ext"); + assert.strictEqual(path.win32.extname("file\\"), ""); + assert.strictEqual(path.win32.extname("file\\\\"), ""); + assert.strictEqual(path.win32.extname("file.\\"), "."); + assert.strictEqual(path.win32.extname("file.\\\\"), "."); + }); + + test("posix", () => { + // On *nix, backslash is a valid name component like any other character. + assert.strictEqual(path.posix.extname(".\\"), ""); + assert.strictEqual(path.posix.extname("..\\"), ".\\"); + assert.strictEqual(path.posix.extname("file.ext\\"), ".ext\\"); + assert.strictEqual(path.posix.extname("file.ext\\\\"), ".ext\\\\"); + assert.strictEqual(path.posix.extname("file\\"), ""); + assert.strictEqual(path.posix.extname("file\\\\"), ""); + assert.strictEqual(path.posix.extname("file.\\"), ".\\"); + assert.strictEqual(path.posix.extname("file.\\\\"), ".\\\\"); + }); +}); diff --git a/test/js/node/path/fixtures/a.js b/test/js/node/path/fixtures/a.js new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/test/js/node/path/fixtures/path-resolve.js b/test/js/node/path/fixtures/path-resolve.js new file mode 100644 index 00000000000000..95f52331e9ae01 --- /dev/null +++ b/test/js/node/path/fixtures/path-resolve.js @@ -0,0 +1,4 @@ +// Tests resolving a path in the context of a spawned process. +// See https://github.com/nodejs/node/issues/7215 +var path = require('path'); +console.log(path.resolve(process.argv[2])); \ No newline at end of file diff --git a/test/js/node/path/is-absolute.test.js b/test/js/node/path/is-absolute.test.js new file mode 100644 index 00000000000000..07248477421de4 --- /dev/null +++ b/test/js/node/path/is-absolute.test.js @@ -0,0 +1,33 @@ +import { test, describe } from "bun:test"; +import assert from "node:assert"; +import path from "node:path"; + +describe("path.isAbsolute", () => { + test("win32", () => { + assert.strictEqual(path.win32.isAbsolute("/"), true); + assert.strictEqual(path.win32.isAbsolute("//"), true); + assert.strictEqual(path.win32.isAbsolute("//server"), true); + assert.strictEqual(path.win32.isAbsolute("//server/file"), true); + assert.strictEqual(path.win32.isAbsolute("\\\\server\\file"), true); + assert.strictEqual(path.win32.isAbsolute("\\\\server"), true); + assert.strictEqual(path.win32.isAbsolute("\\\\"), true); + assert.strictEqual(path.win32.isAbsolute("c"), false); + assert.strictEqual(path.win32.isAbsolute("c:"), false); + assert.strictEqual(path.win32.isAbsolute("c:\\"), true); + assert.strictEqual(path.win32.isAbsolute("c:/"), true); + assert.strictEqual(path.win32.isAbsolute("c://"), true); + assert.strictEqual(path.win32.isAbsolute("C:/Users/"), true); + assert.strictEqual(path.win32.isAbsolute("C:\\Users\\"), true); + assert.strictEqual(path.win32.isAbsolute("C:cwd/another"), false); + assert.strictEqual(path.win32.isAbsolute("C:cwd\\another"), false); + assert.strictEqual(path.win32.isAbsolute("directory/directory"), false); + assert.strictEqual(path.win32.isAbsolute("directory\\directory"), false); + }); + + test("posix", () => { + assert.strictEqual(path.posix.isAbsolute("/home/foo"), true); + assert.strictEqual(path.posix.isAbsolute("/home/foo/.."), true); + assert.strictEqual(path.posix.isAbsolute("bar/"), false); + assert.strictEqual(path.posix.isAbsolute("./baz"), false); + }); +}); diff --git a/test/js/node/path/join.test.js b/test/js/node/path/join.test.js new file mode 100644 index 00000000000000..853fac201fc070 --- /dev/null +++ b/test/js/node/path/join.test.js @@ -0,0 +1,148 @@ +import { test, describe } from "bun:test"; +import assert from "node:assert"; +import path from "node:path"; + +describe("path.join", () => { + test("general", () => { + const failures = []; + const backslashRE = /\\/g; + + const joinTests = [ + [ + [path.posix.join, path.win32.join], + // Arguments result + [ + [[".", "x/b", "..", "/b/c.js"], "x/b/c.js"], + [[], "."], + [["/.", "x/b", "..", "/b/c.js"], "/x/b/c.js"], + [["/foo", "../../../bar"], "/bar"], + [["foo", "../../../bar"], "../../bar"], + [["foo/", "../../../bar"], "../../bar"], + [["foo/x", "../../../bar"], "../bar"], + [["foo/x", "./bar"], "foo/x/bar"], + [["foo/x/", "./bar"], "foo/x/bar"], + [["foo/x/", ".", "bar"], "foo/x/bar"], + [["./"], "./"], + [[".", "./"], "./"], + [[".", ".", "."], "."], + [[".", "./", "."], "."], + [[".", "/./", "."], "."], + [[".", "/////./", "."], "."], + [["."], "."], + [["", "."], "."], + [["", "foo"], "foo"], + [["foo", "/bar"], "foo/bar"], + [["", "/foo"], "/foo"], + [["", "", "/foo"], "/foo"], + [["", "", "foo"], "foo"], + [["foo", ""], "foo"], + [["foo/", ""], "foo/"], + [["foo", "", "/bar"], "foo/bar"], + [["./", "..", "/foo"], "../foo"], + [["./", "..", "..", "/foo"], "../../foo"], + [[".", "..", "..", "/foo"], "../../foo"], + [["", "..", "..", "/foo"], "../../foo"], + [["/"], "/"], + [["/", "."], "/"], + [["/", ".."], "/"], + [["/", "..", ".."], "/"], + [[""], "."], + [["", ""], "."], + [[" /foo"], " /foo"], + [[" ", "foo"], " /foo"], + [[" ", "."], " "], + [[" ", "/"], " /"], + [[" ", ""], " "], + [["/", "foo"], "/foo"], + [["/", "/foo"], "/foo"], + [["/", "//foo"], "/foo"], + [["/", "", "/foo"], "/foo"], + [["", "/", "foo"], "/foo"], + [["", "/", "/foo"], "/foo"], + ], + ], + ]; + + // Windows-specific join tests + joinTests.push([ + path.win32.join, + joinTests[0][1].slice(0).concat([ + // Arguments result + // UNC path expected + [["//foo/bar"], "\\\\foo\\bar\\"], + [["\\/foo/bar"], "\\\\foo\\bar\\"], + [["\\\\foo/bar"], "\\\\foo\\bar\\"], + // UNC path expected - server and share separate + [["//foo", "bar"], "\\\\foo\\bar\\"], + [["//foo/", "bar"], "\\\\foo\\bar\\"], + [["//foo", "/bar"], "\\\\foo\\bar\\"], + // UNC path expected - questionable + [["//foo", "", "bar"], "\\\\foo\\bar\\"], + [["//foo/", "", "bar"], "\\\\foo\\bar\\"], + [["//foo/", "", "/bar"], "\\\\foo\\bar\\"], + // UNC path expected - even more questionable + [["", "//foo", "bar"], "\\\\foo\\bar\\"], + [["", "//foo/", "bar"], "\\\\foo\\bar\\"], + [["", "//foo/", "/bar"], "\\\\foo\\bar\\"], + // No UNC path expected (no double slash in first component) + [["\\", "foo/bar"], "\\foo\\bar"], + [["\\", "/foo/bar"], "\\foo\\bar"], + [["", "/", "/foo/bar"], "\\foo\\bar"], + // No UNC path expected (no non-slashes in first component - + // questionable) + [["//", "foo/bar"], "\\foo\\bar"], + [["//", "/foo/bar"], "\\foo\\bar"], + [["\\\\", "/", "/foo/bar"], "\\foo\\bar"], + [["//"], "\\"], + // No UNC path expected (share name missing - questionable). + [["//foo"], "\\foo"], + [["//foo/"], "\\foo\\"], + [["//foo", "/"], "\\foo\\"], + [["//foo", "", "/"], "\\foo\\"], + // No UNC path expected (too many leading slashes - questionable) + [["///foo/bar"], "\\foo\\bar"], + [["////foo", "bar"], "\\foo\\bar"], + [["\\\\\\/foo/bar"], "\\foo\\bar"], + // Drive-relative vs drive-absolute paths. This merely describes the + // status quo, rather than being obviously right + [["c:"], "c:."], + [["c:."], "c:."], + [["c:", ""], "c:."], + [["", "c:"], "c:."], + [["c:.", "/"], "c:.\\"], + [["c:.", "file"], "c:file"], + [["c:", "/"], "c:\\"], + [["c:", "file"], "c:\\file"], + ]), + ]); + joinTests.forEach(test => { + if (!Array.isArray(test[0])) test[0] = [test[0]]; + test[0].forEach(join => { + test[1].forEach(test => { + const actual = join.apply(null, test[0]); + const expected = test[1]; + // For non-Windows specific tests with the Windows join(), we need to try + // replacing the slashes since the non-Windows specific tests' `expected` + // use forward slashes + let actualAlt; + let os; + if (join === path.win32.join) { + actualAlt = actual.replace(backslashRE, "/"); + os = "win32"; + } else { + os = "posix"; + } + if (actual !== expected && actualAlt !== expected) { + const delimiter = test[0].map(JSON.stringify).join(","); + const message = `path.${os}.join(${delimiter})\n expect=${JSON.stringify( + expected, + )}\n actual=${JSON.stringify(actual)}`; + failures.push(`\n${message}`); + } + }); + }); + }); + + assert.strictEqual(failures.length, 0, failures.join("")); + }); +}); diff --git a/test/js/node/path/normalize.test.js b/test/js/node/path/normalize.test.js new file mode 100644 index 00000000000000..1423cfe3119972 --- /dev/null +++ b/test/js/node/path/normalize.test.js @@ -0,0 +1,54 @@ +import { test, describe } from "bun:test"; +import assert from "node:assert"; +import path from "node:path"; + +describe("path.normalize", () => { + test("win32", () => { + assert.strictEqual(path.win32.normalize("./fixtures///b/../b/c.js"), "fixtures\\b\\c.js"); + assert.strictEqual(path.win32.normalize("/foo/../../../bar"), "\\bar"); + assert.strictEqual(path.win32.normalize("a//b//../b"), "a\\b"); + assert.strictEqual(path.win32.normalize("a//b//./c"), "a\\b\\c"); + assert.strictEqual(path.win32.normalize("a//b//."), "a\\b"); + assert.strictEqual(path.win32.normalize("//server/share/dir/file.ext"), "\\\\server\\share\\dir\\file.ext"); + assert.strictEqual(path.win32.normalize("/a/b/c/../../../x/y/z"), "\\x\\y\\z"); + assert.strictEqual(path.win32.normalize("C:"), "C:."); + assert.strictEqual(path.win32.normalize("C:..\\abc"), "C:..\\abc"); + assert.strictEqual(path.win32.normalize("C:..\\..\\abc\\..\\def"), "C:..\\..\\def"); + assert.strictEqual(path.win32.normalize("C:\\."), "C:\\"); + assert.strictEqual(path.win32.normalize("file:stream"), "file:stream"); + assert.strictEqual(path.win32.normalize("bar\\foo..\\..\\"), "bar\\"); + assert.strictEqual(path.win32.normalize("bar\\foo..\\.."), "bar"); + assert.strictEqual(path.win32.normalize("bar\\foo..\\..\\baz"), "bar\\baz"); + assert.strictEqual(path.win32.normalize("bar\\foo..\\"), "bar\\foo..\\"); + assert.strictEqual(path.win32.normalize("bar\\foo.."), "bar\\foo.."); + assert.strictEqual(path.win32.normalize("..\\foo..\\..\\..\\bar"), "..\\..\\bar"); + assert.strictEqual(path.win32.normalize("..\\...\\..\\.\\...\\..\\..\\bar"), "..\\..\\bar"); + assert.strictEqual(path.win32.normalize("../../../foo/../../../bar"), "..\\..\\..\\..\\..\\bar"); + assert.strictEqual(path.win32.normalize("../../../foo/../../../bar/../../"), "..\\..\\..\\..\\..\\..\\"); + assert.strictEqual(path.win32.normalize("../foobar/barfoo/foo/../../../bar/../../"), "..\\..\\"); + assert.strictEqual(path.win32.normalize("../.../../foobar/../../../bar/../../baz"), "..\\..\\..\\..\\baz"); + assert.strictEqual(path.win32.normalize("foo/bar\\baz"), "foo\\bar\\baz"); + }); + + test("posix", () => { + assert.strictEqual(path.posix.normalize("./fixtures///b/../b/c.js"), "fixtures/b/c.js"); + assert.strictEqual(path.posix.normalize("/foo/../../../bar"), "/bar"); + assert.strictEqual(path.posix.normalize("a//b//../b"), "a/b"); + assert.strictEqual(path.posix.normalize("a//b//./c"), "a/b/c"); + assert.strictEqual(path.posix.normalize("a//b//."), "a/b"); + assert.strictEqual(path.posix.normalize("/a/b/c/../../../x/y/z"), "/x/y/z"); + assert.strictEqual(path.posix.normalize("///..//./foo/.//bar"), "/foo/bar"); + assert.strictEqual(path.posix.normalize("bar/foo../../"), "bar/"); + assert.strictEqual(path.posix.normalize("bar/foo../.."), "bar"); + assert.strictEqual(path.posix.normalize("bar/foo../../baz"), "bar/baz"); + assert.strictEqual(path.posix.normalize("bar/foo../"), "bar/foo../"); + assert.strictEqual(path.posix.normalize("bar/foo.."), "bar/foo.."); + assert.strictEqual(path.posix.normalize("../foo../../../bar"), "../../bar"); + assert.strictEqual(path.posix.normalize("../.../.././.../../../bar"), "../../bar"); + assert.strictEqual(path.posix.normalize("../../../foo/../../../bar"), "../../../../../bar"); + assert.strictEqual(path.posix.normalize("../../../foo/../../../bar/../../"), "../../../../../../"); + assert.strictEqual(path.posix.normalize("../foobar/barfoo/foo/../../../bar/../../"), "../../"); + assert.strictEqual(path.posix.normalize("../.../../foobar/../../../bar/../../baz"), "../../../../baz"); + assert.strictEqual(path.posix.normalize("foo/bar\\baz"), "foo/bar\\baz"); + }); +}); diff --git a/test/js/node/path/parse-format.test.js b/test/js/node/path/parse-format.test.js new file mode 100644 index 00000000000000..543f129c64413a --- /dev/null +++ b/test/js/node/path/parse-format.test.js @@ -0,0 +1,223 @@ +import { test, describe } from "bun:test"; +import assert from "node:assert"; +import path from "node:path"; + +describe("path.parse", () => { + test("general", () => { + const winPaths = [ + // [path, root] + ["C:\\path\\dir\\index.html", "C:\\"], + ["C:\\another_path\\DIR\\1\\2\\33\\\\index", "C:\\"], + ["another_path\\DIR with spaces\\1\\2\\33\\index", ""], + ["\\", "\\"], + ["\\foo\\C:", "\\"], + ["file", ""], + ["file:stream", ""], + [".\\file", ""], + ["C:", "C:"], + ["C:.", "C:"], + ["C:..", "C:"], + ["C:abc", "C:"], + ["C:\\", "C:\\"], + ["C:\\abc", "C:\\"], + ["", ""], + + // unc + ["\\\\server\\share\\file_path", "\\\\server\\share\\"], + ["\\\\server two\\shared folder\\file path.zip", "\\\\server two\\shared folder\\"], + ["\\\\teela\\admin$\\system32", "\\\\teela\\admin$\\"], + ["\\\\?\\UNC\\server\\share", "\\\\?\\UNC\\"], + ]; + + const winSpecialCaseParseTests = [ + ["t", { base: "t", name: "t", root: "", dir: "", ext: "" }], + ["/foo/bar", { root: "/", dir: "/foo", base: "bar", ext: "", name: "bar" }], + ]; + + const winSpecialCaseFormatTests = [ + [{ dir: "some\\dir" }, "some\\dir\\"], + [{ base: "index.html" }, "index.html"], + [{ root: "C:\\" }, "C:\\"], + [{ name: "index", ext: ".html" }, "index.html"], + [{ dir: "some\\dir", name: "index", ext: ".html" }, "some\\dir\\index.html"], + [{ root: "C:\\", name: "index", ext: ".html" }, "C:\\index.html"], + [{}, ""], + ]; + + const unixPaths = [ + // [path, root] + ["/home/user/dir/file.txt", "/"], + ["/home/user/a dir/another File.zip", "/"], + ["/home/user/a dir//another&File.", "/"], + ["/home/user/a$$$dir//another File.zip", "/"], + ["user/dir/another File.zip", ""], + ["file", ""], + [".\\file", ""], + ["./file", ""], + ["C:\\foo", ""], + ["/", "/"], + ["", ""], + [".", ""], + ["..", ""], + ["/foo", "/"], + ["/foo.", "/"], + ["/foo.bar", "/"], + ["/.", "/"], + ["/.foo", "/"], + ["/.foo.bar", "/"], + ["/foo/bar.baz", "/"], + ]; + + const unixSpecialCaseFormatTests = [ + [{ dir: "some/dir" }, "some/dir/"], + [{ base: "index.html" }, "index.html"], + [{ root: "/" }, "/"], + [{ name: "index", ext: ".html" }, "index.html"], + [{ dir: "some/dir", name: "index", ext: ".html" }, "some/dir/index.html"], + [{ root: "/", name: "index", ext: ".html" }, "/index.html"], + [{}, ""], + ]; + + const errors = [ + { method: "parse", input: [null] }, + { method: "parse", input: [{}] }, + { method: "parse", input: [true] }, + { method: "parse", input: [1] }, + { method: "parse", input: [] }, + { method: "format", input: [null] }, + { method: "format", input: [""] }, + { method: "format", input: [true] }, + { method: "format", input: [1] }, + ]; + + checkParseFormat(path.win32, winPaths); + checkParseFormat(path.posix, unixPaths); + checkSpecialCaseParseFormat(path.win32, winSpecialCaseParseTests); + checkErrors(path.win32); + checkErrors(path.posix); + checkFormat(path.win32, winSpecialCaseFormatTests); + checkFormat(path.posix, unixSpecialCaseFormatTests); + + // Test removal of trailing path separators + const trailingTests = [ + [ + path.win32.parse, + [ + [".\\", { root: "", dir: "", base: ".", ext: "", name: "." }], + ["\\\\", { root: "\\", dir: "\\", base: "", ext: "", name: "" }], + ["\\\\", { root: "\\", dir: "\\", base: "", ext: "", name: "" }], + ["c:\\foo\\\\\\", { root: "c:\\", dir: "c:\\", base: "foo", ext: "", name: "foo" }], + ["D:\\foo\\\\\\bar.baz", { root: "D:\\", dir: "D:\\foo\\\\", base: "bar.baz", ext: ".baz", name: "bar" }], + ], + ], + [ + path.posix.parse, + [ + ["./", { root: "", dir: "", base: ".", ext: "", name: "." }], + ["//", { root: "/", dir: "/", base: "", ext: "", name: "" }], + ["///", { root: "/", dir: "/", base: "", ext: "", name: "" }], + ["/foo///", { root: "/", dir: "/", base: "foo", ext: "", name: "foo" }], + ["/foo///bar.baz", { root: "/", dir: "/foo//", base: "bar.baz", ext: ".baz", name: "bar" }], + ], + ], + ]; + const failures = []; + trailingTests.forEach(test => { + const parse = test[0]; + const os = parse === path.win32.parse ? "win32" : "posix"; + test[1].forEach(test => { + const actual = parse(test[0]); + const expected = test[1]; + const message = `path.${os}.parse(${JSON.stringify(test[0])})\n expect=${JSON.stringify( + expected, + )}\n actual=${JSON.stringify(actual)}`; + const actualKeys = Object.keys(actual); + const expectedKeys = Object.keys(expected); + let failed = actualKeys.length !== expectedKeys.length; + if (!failed) { + for (let i = 0; i < actualKeys.length; ++i) { + const key = actualKeys[i]; + if (!expectedKeys.includes(key) || actual[key] !== expected[key]) { + failed = true; + break; + } + } + } + if (failed) failures.push(`\n${message}`); + }); + }); + assert.strictEqual(failures.length, 0, failures.join("")); + + function checkErrors(path) { + errors.forEach(({ method, input }) => { + assert.throws( + () => { + path[method].apply(path, input); + }, + { + code: "ERR_INVALID_ARG_TYPE", + name: "TypeError", + }, + ); + }); + } + + function checkParseFormat(path, paths) { + paths.forEach(([element, root]) => { + const output = path.parse(element); + assert.strictEqual(typeof output.root, "string"); + assert.strictEqual(typeof output.dir, "string"); + assert.strictEqual(typeof output.base, "string"); + assert.strictEqual(typeof output.ext, "string"); + assert.strictEqual(typeof output.name, "string"); + assert.strictEqual(path.format(output), element); + assert.strictEqual(output.root, root); + assert(output.dir.startsWith(output.root)); + assert.strictEqual(output.dir, output.dir ? path.dirname(element) : ""); + assert.strictEqual(output.base, path.basename(element)); + assert.strictEqual(output.ext, path.extname(element)); + }); + } + + function checkSpecialCaseParseFormat(path, testCases) { + testCases.forEach(([element, expect]) => { + assert.deepStrictEqual(path.parse(element), expect); + }); + } + + function checkFormat(path, testCases) { + testCases.forEach(([element, expect]) => { + assert.strictEqual(path.format(element), expect); + }); + + [null, undefined, 1, true, false, "string"].forEach(pathObject => { + assert.throws( + () => { + path.format(pathObject); + }, + { + code: "ERR_INVALID_ARG_TYPE", + name: "TypeError", + // @TODO: Make our error messages use util.inspect like Node: + // https://github.com/nodejs/node/blob/68885d512640556ba95b18f5ab2e0b9e76013399/lib/internal/errors.js#L1370-L1440 + // https://github.com/nodejs/node/blob/68885d512640556ba95b18f5ab2e0b9e76013399/test/common/index.js#L815 + // + // Node's error message template is: + // `The "pathObject" argument must be of type object. Received ${inspect(input, { depth: -1 })}` + // + // For example, when we throw for path.format(null) our error message is: + // The "pathObject" property must be of type object, got object + // + // While Node's error message is: + // The "pathObject" argument must be of type object. Received nullå + message: `"pathObject" property must be of type object, got ${typeof pathObject}`, + }, + ); + }); + } + + // See https://github.com/nodejs/node/issues/44343 + assert.strictEqual(path.format({ name: "x", ext: "png" }), "x.png"); + assert.strictEqual(path.format({ name: "x", ext: ".png" }), "x.png"); + }); +}); diff --git a/test/js/node/path/path.test.js b/test/js/node/path/path.test.js index 0234141253386b..8eccc2d6e17cfa 100644 --- a/test/js/node/path/path.test.js +++ b/test/js/node/path/path.test.js @@ -1,902 +1,62 @@ -const { file } = import.meta; - -import { describe, it, expect, test } from "bun:test"; +import { test, describe } from "bun:test"; +import assert from "node:assert"; import path from "node:path"; -import assert from "assert"; -const sep = process.platform === "win32" ? "\\" : "/"; +const isWindows = process.platform === "win32"; -const strictEqual = (...args) => { - assert.strictEqual(...args); - expect(true).toBe(true); -}; +describe("path", () => { + test("errors", () => { + // Test thrown TypeErrors + const typeErrorTests = [true, false, 7, null, {}, undefined, [], NaN]; -const expectStrictEqual = (actual, expected) => { - expect(actual).toBe(expected); -}; + function fail(fn) { + const args = Array.from(arguments).slice(1); -describe("dirname", () => { - it("path.dirname", () => { - const fixtures = [ - ["yo", "."], - ["/yo", "/"], - ["/yo/", "/"], - ["/yo/123", "/yo"], - [".", "."], - ["../", "."], - ["../../", ".."], - ["../../foo", "../.."], - ["../../foo/../", "../../foo"], - ["/foo/../", "/foo"], - ["../../foo/../bar", "../../foo/.."], - ]; - for (const [input, expected] of fixtures) { - expect(path.posix.dirname(input)).toBe(expected); - if (process.platform !== "win32") { - expect(path.dirname(input)).toBe(expected); - } + assert.throws( + () => { + fn.apply(null, args); + }, + { code: "ERR_INVALID_ARG_TYPE", name: "TypeError" }, + ); } - }); - it("path.posix.dirname", () => { - expect(path.posix.dirname("/a/b/")).toBe("/a"); - expect(path.posix.dirname("/a/b")).toBe("/a"); - expect(path.posix.dirname("/a")).toBe("/"); - expect(path.posix.dirname("/a/")).toBe("/"); - expect(path.posix.dirname("")).toBe("."); - expect(path.posix.dirname("/")).toBe("/"); - expect(path.posix.dirname("//")).toBe("/"); - expect(path.posix.dirname("///")).toBe("/"); - expect(path.posix.dirname("////")).toBe("/"); - expect(path.posix.dirname("//a")).toBe("//"); - expect(path.posix.dirname("//ab")).toBe("//"); - expect(path.posix.dirname("///a")).toBe("//"); - expect(path.posix.dirname("////a")).toBe("///"); - expect(path.posix.dirname("/////a")).toBe("////"); - expect(path.posix.dirname("foo")).toBe("."); - expect(path.posix.dirname("foo/")).toBe("."); - expect(path.posix.dirname("a/b")).toBe("a"); - expect(path.posix.dirname("a/")).toBe("."); - expect(path.posix.dirname("a///b")).toBe("a//"); - expect(path.posix.dirname("a//b")).toBe("a/"); - expect(path.posix.dirname("\\")).toBe("."); - expect(path.posix.dirname("\\a")).toBe("."); - expect(path.posix.dirname("a")).toBe("."); - expect(path.posix.dirname("/a/b//c")).toBe("/a/b/"); - expect(path.posix.dirname("/文檔")).toBe("/"); - expect(path.posix.dirname("/文檔/")).toBe("/"); - expect(path.posix.dirname("/文檔/新建文件夾")).toBe("/文檔"); - expect(path.posix.dirname("/文檔/新建文件夾/")).toBe("/文檔"); - expect(path.posix.dirname("//新建文件夾")).toBe("//"); - expect(path.posix.dirname("///新建文件夾")).toBe("//"); - expect(path.posix.dirname("////新建文件夾")).toBe("///"); - expect(path.posix.dirname("/////新建文件夾")).toBe("////"); - expect(path.posix.dirname("新建文件夾")).toBe("."); - expect(path.posix.dirname("新建文件夾/")).toBe("."); - expect(path.posix.dirname("文檔/新建文件夾")).toBe("文檔"); - expect(path.posix.dirname("文檔/")).toBe("."); - expect(path.posix.dirname("文檔///新建文件夾")).toBe("文檔//"); - expect(path.posix.dirname("文檔//新建文件夾")).toBe("文檔/"); - }); - it("path.win32.dirname", () => { - expect(path.win32.dirname("c:\\")).toBe("c:\\"); - expect(path.win32.dirname("c:\\foo")).toBe("c:\\"); - expect(path.win32.dirname("c:\\foo\\")).toBe("c:\\"); - expect(path.win32.dirname("c:\\foo\\bar")).toBe("c:\\foo"); - expect(path.win32.dirname("c:\\foo\\bar\\")).toBe("c:\\foo"); - expect(path.win32.dirname("c:\\foo\\bar\\baz")).toBe("c:\\foo\\bar"); - expect(path.win32.dirname("c:\\foo bar\\baz")).toBe("c:\\foo bar"); - expect(path.win32.dirname("c:\\\\foo")).toBe("c:\\"); - expect(path.win32.dirname("\\")).toBe("\\"); - expect(path.win32.dirname("\\foo")).toBe("\\"); - expect(path.win32.dirname("\\foo\\")).toBe("\\"); - expect(path.win32.dirname("\\foo\\bar")).toBe("\\foo"); - expect(path.win32.dirname("\\foo\\bar\\")).toBe("\\foo"); - expect(path.win32.dirname("\\foo\\bar\\baz")).toBe("\\foo\\bar"); - expect(path.win32.dirname("\\foo bar\\baz")).toBe("\\foo bar"); - expect(path.win32.dirname("c:")).toBe("c:"); - expect(path.win32.dirname("c:foo")).toBe("c:"); - expect(path.win32.dirname("c:foo\\")).toBe("c:"); - expect(path.win32.dirname("c:foo\\bar")).toBe("c:foo"); - expect(path.win32.dirname("c:foo\\bar\\")).toBe("c:foo"); - expect(path.win32.dirname("c:foo\\bar\\baz")).toBe("c:foo\\bar"); - expect(path.win32.dirname("c:foo bar\\baz")).toBe("c:foo bar"); - expect(path.win32.dirname("file:stream")).toBe("."); - expect(path.win32.dirname("dir\\file:stream")).toBe("dir"); - expect(path.win32.dirname("\\\\unc\\share")).toBe("\\\\unc\\share"); - expect(path.win32.dirname("\\\\unc\\share\\foo")).toBe("\\\\unc\\share\\"); - expect(path.win32.dirname("\\\\unc\\share\\foo\\")).toBe("\\\\unc\\share\\"); - expect(path.win32.dirname("\\\\unc\\share\\foo\\bar")).toBe("\\\\unc\\share\\foo"); - expect(path.win32.dirname("\\\\unc\\share\\foo\\bar\\")).toBe("\\\\unc\\share\\foo"); - expect(path.win32.dirname("\\\\unc\\share\\foo\\bar\\baz")).toBe("\\\\unc\\share\\foo\\bar"); - expect(path.win32.dirname("/a/b/")).toBe("/a"); - expect(path.win32.dirname("/a/b")).toBe("/a"); - expect(path.win32.dirname("/a")).toBe("/"); - expect(path.win32.dirname("")).toBe("."); - expect(path.win32.dirname("/")).toBe("/"); - expect(path.win32.dirname("////")).toBe("/"); - expect(path.win32.dirname("foo")).toBe("."); - expect(path.win32.dirname("c:\\")).toBe("c:\\"); - expect(path.win32.dirname("c:\\文檔")).toBe("c:\\"); - expect(path.win32.dirname("c:\\文檔\\")).toBe("c:\\"); - expect(path.win32.dirname("c:\\文檔\\新建文件夾")).toBe("c:\\文檔"); - expect(path.win32.dirname("c:\\文檔\\新建文件夾\\")).toBe("c:\\文檔"); - expect(path.win32.dirname("c:\\文檔\\新建文件夾\\baz")).toBe("c:\\文檔\\新建文件夾"); - expect(path.win32.dirname("c:\\文檔 1\\新建文件夾")).toBe("c:\\文檔 1"); - expect(path.win32.dirname("c:\\\\文檔")).toBe("c:\\"); - expect(path.win32.dirname("\\文檔")).toBe("\\"); - expect(path.win32.dirname("\\文檔\\")).toBe("\\"); - expect(path.win32.dirname("\\文檔\\新建文件夾")).toBe("\\文檔"); - expect(path.win32.dirname("\\文檔\\新建文件夾\\")).toBe("\\文檔"); - expect(path.win32.dirname("\\文檔\\新建文件夾\\baz")).toBe("\\文檔\\新建文件夾"); - expect(path.win32.dirname("\\文檔 1\\baz")).toBe("\\文檔 1"); - expect(path.win32.dirname("c:")).toBe("c:"); - expect(path.win32.dirname("c:文檔")).toBe("c:"); - expect(path.win32.dirname("c:文檔\\")).toBe("c:"); - expect(path.win32.dirname("c:文檔\\新建文件夾")).toBe("c:文檔"); - expect(path.win32.dirname("c:文檔\\新建文件夾\\")).toBe("c:文檔"); - expect(path.win32.dirname("c:文檔\\新建文件夾\\baz")).toBe("c:文檔\\新建文件夾"); - expect(path.win32.dirname("c:文檔 1\\baz")).toBe("c:文檔 1"); - expect(path.win32.dirname("/文檔/新建文件夾/")).toBe("/文檔"); - expect(path.win32.dirname("/文檔/新建文件夾")).toBe("/文檔"); - expect(path.win32.dirname("/文檔")).toBe("/"); - expect(path.win32.dirname("新建文件夾")).toBe("."); - }); -}); - -it("path.parse().name", () => { - expectStrictEqual(path.parse(file).name, "path.test"); - expectStrictEqual(path.parse(".js").name, ".js"); - expectStrictEqual(path.parse("..js").name, "."); - expectStrictEqual(path.parse("").name, ""); - expectStrictEqual(path.parse(".").name, "."); - expectStrictEqual(path.parse("dir/name.ext").name, "name"); - expectStrictEqual(path.parse("/dir/name.ext").name, "name"); - expectStrictEqual(path.parse("/name.ext").name, "name"); - expectStrictEqual(path.parse("name.ext").name, "name"); - expectStrictEqual(path.parse("name.ext/").name, "name"); - expectStrictEqual(path.parse("name.ext//").name, "name"); - expectStrictEqual(path.parse("aaa/bbb").name, "bbb"); - expectStrictEqual(path.parse("aaa/bbb/").name, "bbb"); - expectStrictEqual(path.parse("aaa/bbb//").name, "bbb"); - expectStrictEqual(path.parse("/aaa/bbb").name, "bbb"); - expectStrictEqual(path.parse("/aaa/bbb/").name, "bbb"); - expectStrictEqual(path.parse("/aaa/bbb//").name, "bbb"); - expectStrictEqual(path.parse("//aaa/bbb").name, "bbb"); - expectStrictEqual(path.parse("///aaa").name, "aaa"); - expectStrictEqual(path.parse("//aaa").name, "aaa"); - expectStrictEqual(path.parse("/aaa").name, "aaa"); - expectStrictEqual(path.parse("aaa.").name, "aaa"); - - // Windows parses these as UNC roots, so name is empty there. - expectStrictEqual(path.posix.parse("//aaa/bbb/").name, "bbb"); - expectStrictEqual(path.posix.parse("//aaa/bbb//").name, "bbb"); - expectStrictEqual(path.win32.parse("//aaa/bbb/").name, ""); - expectStrictEqual(path.win32.parse("//aaa/bbb//").name, ""); - - // On unix a backslash is just treated as any other character. - expectStrictEqual(path.posix.parse("\\dir\\name.ext").name, "\\dir\\name"); - expectStrictEqual(path.posix.parse("\\name.ext").name, "\\name"); - expectStrictEqual(path.posix.parse("name.ext").name, "name"); - expectStrictEqual(path.posix.parse("name.ext\\").name, "name"); - expectStrictEqual(path.posix.parse("name.ext\\\\").name, "name"); -}); - -it("path.parse() windows edition", () => { - // On Windows a backslash acts as a path separator. - expectStrictEqual(path.win32.parse("\\dir\\name.ext").name, "name"); - expectStrictEqual(path.win32.parse("\\name.ext").name, "name"); - expectStrictEqual(path.win32.parse("name.ext").name, "name"); - expectStrictEqual(path.win32.parse("name.ext\\").name, "name"); - expectStrictEqual(path.win32.parse("name.ext\\\\").name, "name"); - expectStrictEqual(path.win32.parse("name").name, "name"); - expectStrictEqual(path.win32.parse(".name").name, ".name"); - expectStrictEqual(path.win32.parse("file:stream").name, "file:stream"); -}); - -it("path.parse() windows edition - drive letter", () => { - expectStrictEqual(path.win32.parse("C:").name, ""); - expectStrictEqual(path.win32.parse("C:.").name, "."); - expectStrictEqual(path.win32.parse("C:\\").name, ""); - expectStrictEqual(path.win32.parse("C:\\.").name, "."); - expectStrictEqual(path.win32.parse("C:\\.ext").name, ".ext"); - expectStrictEqual(path.win32.parse("C:\\dir\\name.ext").name, "name"); - expectStrictEqual(path.win32.parse("C:name.ext").name, "name"); - expectStrictEqual(path.win32.parse("C:name.ext\\").name, "name"); - expectStrictEqual(path.win32.parse("C:name.ext\\\\").name, "name"); - expectStrictEqual(path.win32.parse("C:foo").name, "foo"); - expectStrictEqual(path.win32.parse("C:.foo").name, ".foo"); -}); - -it("path.parse() windows edition - .root", () => { - expectStrictEqual(path.win32.parse("C:").root, "C:"); - expectStrictEqual(path.win32.parse("C:.").root, "C:"); - expectStrictEqual(path.win32.parse("C:\\").root, "C:\\"); - expectStrictEqual(path.win32.parse("C:\\.").root, "C:\\"); - expectStrictEqual(path.win32.parse("C:\\.ext").root, "C:\\"); - expectStrictEqual(path.win32.parse("C:\\dir\\name.ext").root, "C:\\"); - expectStrictEqual(path.win32.parse("C:name.ext").root, "C:"); - expectStrictEqual(path.win32.parse("C:name.ext\\").root, "C:"); - expectStrictEqual(path.win32.parse("C:name.ext\\\\").root, "C:"); - expectStrictEqual(path.win32.parse("C:foo").root, "C:"); - expectStrictEqual(path.win32.parse("C:.foo").root, "C:"); - expectStrictEqual(path.win32.parse("/:.foo").root, "/"); -}); - -it("path.basename", () => { - strictEqual(path.basename(file), "path.test.js"); - strictEqual(path.basename(file, ".js"), "path.test"); - strictEqual(path.basename(".js", ".js"), ""); - strictEqual(path.basename(""), ""); - strictEqual(path.basename("/dir/basename.ext"), "basename.ext"); - strictEqual(path.basename("/basename.ext"), "basename.ext"); - strictEqual(path.basename("basename.ext"), "basename.ext"); - strictEqual(path.basename("basename.ext/"), "basename.ext"); - strictEqual(path.basename("basename.ext//"), "basename.ext"); - strictEqual(path.basename("aaa/bbb", "/bbb"), "bbb"); - strictEqual(path.basename("aaa/bbb", "a/bbb"), "bbb"); - strictEqual(path.basename("aaa/bbb", "bbb"), "bbb"); - strictEqual(path.basename("aaa/bbb//", "bbb"), "bbb"); - strictEqual(path.basename("aaa/bbb", "bb"), "b"); - strictEqual(path.basename("aaa/bbb", "b"), "bb"); - strictEqual(path.basename("/aaa/bbb", "/bbb"), "bbb"); - strictEqual(path.basename("/aaa/bbb", "a/bbb"), "bbb"); - strictEqual(path.basename("/aaa/bbb", "bbb"), "bbb"); - strictEqual(path.basename("/aaa/bbb//", "bbb"), "bbb"); - strictEqual(path.basename("/aaa/bbb", "bb"), "b"); - strictEqual(path.basename("/aaa/bbb", "b"), "bb"); - strictEqual(path.basename("/aaa/bbb"), "bbb"); - strictEqual(path.basename("/aaa/"), "aaa"); - strictEqual(path.basename("/aaa/b"), "b"); - strictEqual(path.basename("/a/b"), "b"); - strictEqual(path.basename("//a"), "a"); - strictEqual(path.basename("a", "a"), ""); - - // On Windows a backslash acts as a path separator. - strictEqual(path.win32.basename("\\dir\\basename.ext"), "basename.ext"); - strictEqual(path.win32.basename("\\basename.ext"), "basename.ext"); - strictEqual(path.win32.basename("basename.ext"), "basename.ext"); - strictEqual(path.win32.basename("basename.ext\\"), "basename.ext"); - strictEqual(path.win32.basename("basename.ext\\\\"), "basename.ext"); - strictEqual(path.win32.basename("foo"), "foo"); - strictEqual(path.win32.basename("aaa\\bbb", "\\bbb"), "bbb"); - strictEqual(path.win32.basename("aaa\\bbb", "a\\bbb"), "bbb"); - strictEqual(path.win32.basename("aaa\\bbb", "bbb"), "bbb"); - strictEqual(path.win32.basename("aaa\\bbb\\\\\\\\", "bbb"), "bbb"); - strictEqual(path.win32.basename("aaa\\bbb", "bb"), "b"); - strictEqual(path.win32.basename("aaa\\bbb", "b"), "bb"); - strictEqual(path.win32.basename("C:"), ""); - strictEqual(path.win32.basename("C:."), "."); - strictEqual(path.win32.basename("C:\\"), ""); - strictEqual(path.win32.basename("C:\\dir\\base.ext"), "base.ext"); - strictEqual(path.win32.basename("C:\\basename.ext"), "basename.ext"); - strictEqual(path.win32.basename("C:basename.ext"), "basename.ext"); - strictEqual(path.win32.basename("C:basename.ext\\"), "basename.ext"); - strictEqual(path.win32.basename("C:basename.ext\\\\"), "basename.ext"); - strictEqual(path.win32.basename("C:foo"), "foo"); - strictEqual(path.win32.basename("file:stream"), "file:stream"); - strictEqual(path.win32.basename("a", "a"), ""); - - // On unix a backslash is just treated as any other character. - strictEqual(path.posix.basename("\\dir\\basename.ext"), "\\dir\\basename.ext"); - strictEqual(path.posix.basename("\\basename.ext"), "\\basename.ext"); - strictEqual(path.posix.basename("basename.ext"), "basename.ext"); - strictEqual(path.posix.basename("basename.ext\\"), "basename.ext\\"); - strictEqual(path.posix.basename("basename.ext\\\\"), "basename.ext\\\\"); - strictEqual(path.posix.basename("foo"), "foo"); - - // POSIX filenames may include control characters - // c.f. http://www.dwheeler.com/essays/fixing-unix-linux-filenames.html - const controlCharFilename = `Icon${String.fromCharCode(13)}`; - strictEqual(path.posix.basename(`/a/b/${controlCharFilename}`), controlCharFilename); -}); - -describe("path.join #5769", () => { - for (let length of [4096, 4095, 4097, 65_432, 65_431, 65_433]) { - it("length " + length, () => { - const tooLengthyFolderName = Array.from({ length }).fill("b").join(""); - expect(path.join(tooLengthyFolderName)).toEqual("b".repeat(length)); - }); - it("length " + length + "joined", () => { - const tooLengthyFolderName = Array.from({ length }).fill("b"); - expect(path.join(...tooLengthyFolderName)).toEqual(("b" + sep).repeat(length).substring(0, 2 * length - 1)); - }); - } -}); -it("path.join", () => { - const failures = []; - const backslashRE = /\\/g; - - const joinTests = [ - [ - [path.posix.join], - // Arguments result - [ - [[".", "x/b", "..", "/b/c.js"], "x/b/c.js"], - [[], "."], - [["/.", "x/b", "..", "/b/c.js"], "/x/b/c.js"], - [["/foo", "../../../bar"], "/bar"], - [["foo", "../../../bar"], "../../bar"], - [["foo/", "../../../bar"], "../../bar"], - [["foo/x", "../../../bar"], "../bar"], - [["foo/x", "./bar"], "foo/x/bar"], - [["foo/x/", "./bar"], "foo/x/bar"], - [["foo/x/", ".", "bar"], "foo/x/bar"], - [["./"], "./"], - [[".", "./"], "./"], - [[".", ".", "."], "."], - [[".", "./", "."], "."], - [[".", "/./", "."], "."], - [[".", "/////./", "."], "."], - [["."], "."], - [["", "."], "."], - [["", "foo"], "foo"], - [["foo", "/bar"], "foo/bar"], - [["", "/foo"], "/foo"], - [["", "", "/foo"], "/foo"], - [["", "", "foo"], "foo"], - [["foo", ""], "foo"], - [["foo/", ""], "foo/"], - [["foo", "", "/bar"], "foo/bar"], - [["./", "..", "/foo"], "../foo"], - [["./", "..", "..", "/foo"], "../../foo"], - [[".", "..", "..", "/foo"], "../../foo"], - [["", "..", "..", "/foo"], "../../foo"], - [["/"], "/"], - [["/", "."], "/"], - [["/", ".."], "/"], - [["/", "..", ".."], "/"], - [[""], "."], - [["", ""], "."], - [[" /foo"], " /foo"], - [[" ", "foo"], " /foo"], - [[" ", "."], " "], - [[" ", "/"], " /"], - [[" ", ""], " "], - [["/", "foo"], "/foo"], - [["/", "/foo"], "/foo"], - [["/", "//foo"], "/foo"], - [["/", "", "/foo"], "/foo"], - [["", "/", "foo"], "/foo"], - [["", "/", "/foo"], "/foo"], - ], - ], - ]; - - // Windows-specific join tests - joinTests.push([ - path.win32.join, - joinTests[0][1].slice(0).concat([ - // Arguments result - // UNC path expected - [["//foo/bar"], "\\\\foo\\bar\\"], - [["\\/foo/bar"], "\\\\foo\\bar\\"], - [["\\\\foo/bar"], "\\\\foo\\bar\\"], - // UNC path expected - server and share separate - [["//foo", "bar"], "\\\\foo\\bar\\"], - // TODO: [["//foo/", "bar"], "\\\\foo\\bar\\"], - // TODO: [["//foo", "/bar"], "\\\\foo\\bar\\"], - // UNC path expected - questionable - [["//foo", "", "bar"], "\\\\foo\\bar\\"], - // TODO: // [["//foo/", "", "bar"], "\\\\foo\\bar\\"], - // TODO: [["//foo/", "", "/bar"], "\\\\foo\\bar\\"], - // UNC path expected - even more questionable - [["", "//foo", "bar"], "\\\\foo\\bar\\"], - // TODO: [["", "//foo/", "bar"], "\\\\foo\\bar\\"], - // TODO: [["", "//foo/", "/bar"], "\\\\foo\\bar\\"], - // No UNC path expected (no double slash in first component) - // TODO: [["\\", "foo/bar"], "\\foo\\bar"], - [["\\", "/foo/bar"], "\\foo\\bar"], - [["", "/", "/foo/bar"], "\\foo\\bar"], - // No UNC path expected (no non-slashes in first component - - // questionable) - [["//", "foo/bar"], "\\foo\\bar"], - [["//", "/foo/bar"], "\\foo\\bar"], - [["\\\\", "/", "/foo/bar"], "\\foo\\bar"], - [["//"], "\\"], - // No UNC path expected (share name missing - questionable). - [["//foo"], "\\foo"], - [["//foo/"], "\\foo\\"], - [["//foo", "/"], "\\foo\\"], - [["//foo", "", "/"], "\\foo\\"], - // No UNC path expected (too many leading slashes - questionable) - [["///foo/bar"], "\\foo\\bar"], - [["////foo", "bar"], "\\foo\\bar"], - [["\\\\\\/foo/bar"], "\\foo\\bar"], - // Drive-relative vs drive-absolute paths. This merely describes the - // status quo, rather than being obviously right - // TODO: fix these - // [["c:"], "c:."], - // [["c:."], "c:."], - // [["c:", ""], "c:."], - // [["", "c:"], "c:."], - // [["c:.", "/"], "c:.\\"], - // [["c:.", "file"], "c:file"], - // [["c:", "/"], "c:\\"], - // [["c:", "file"], "c:\\file"], - ]), - ]); - joinTests.forEach(test => { - if (!Array.isArray(test[0])) test[0] = [test[0]]; - test[0].forEach(join => { - test[1].forEach(test => { - const actual = join.apply(null, test[0]); - const expected = test[1]; - // For non-Windows specific tests with the Windows join(), we need to try - // replacing the slashes since the non-Windows specific tests' `expected` - // use forward slashes - let actualAlt; - let os; - let displayExpected = expected; - if (join === path.win32.join) { - actualAlt = actual.replace(backslashRE, "/"); - displayExpected = expected.replace(/\//g, "\\"); - os = "win32"; - } else { - os = "posix"; - } - if (actual !== expected && actualAlt !== expected) { - const delimiter = test[0].map(JSON.stringify).join(","); - const message = `path.${os}.join(${delimiter})\n expect=${JSON.stringify( - displayExpected, - )}\n actual=${JSON.stringify(actual)}`; - failures.push(`\n${message}`); + for (const test of typeErrorTests) { + for (const namespace of [path.posix, path.win32]) { + fail(namespace.join, test); + fail(namespace.resolve, test); + fail(namespace.normalize, test); + fail(namespace.isAbsolute, test); + //fail(namespace.relative, test, "foo"); + //fail(namespace.relative, "foo", test); + fail(namespace.parse, test); + fail(namespace.dirname, test); + fail(namespace.basename, test); + fail(namespace.extname, test); + + // Undefined is a valid value as the second argument to basename + if (test !== undefined) { + fail(namespace.basename, "foo", test); } - }); - }); - }); - strictEqual(failures.length, 0, failures.join("")); -}); - -it("path.relative", () => { - const failures = []; - const cwd = process.cwd(); - const cwdParent = path.dirname(cwd); - const parentIsRoot = process.platform === "win32" ? cwdParent.match(/^[A-Z]:\\$/) : cwdParent === "/"; - - const relativeTests = [ - [ - path.win32.relative, - // Arguments result - [ - ["c:/blah\\blah", "d:/games", "d:\\games"], - ["c:/aaaa/bbbb", "c:/aaaa", ".."], - ["c:/aaaa/bbbb", "c:/cccc", "..\\..\\cccc"], - ["c:/aaaa/bbbb", "c:/aaaa/bbbb", ""], - ["c:/aaaa/bbbb", "c:/aaaa/cccc", "..\\cccc"], - ["c:/aaaa/", "c:/aaaa/cccc", "cccc"], - ["c:/", "c:\\aaaa\\bbbb", "aaaa\\bbbb"], - ["c:/aaaa/bbbb", "d:\\", "d:\\"], - ["c:/AaAa/bbbb", "c:/aaaa/bbbb", ""], - ["c:/aaaaa/", "c:/aaaa/cccc", "..\\aaaa\\cccc"], - ["C:\\foo\\bar\\baz\\quux", "C:\\", "..\\..\\..\\.."], - ["C:\\foo\\test", "C:\\foo\\test\\bar\\package.json", "bar\\package.json"], - ["C:\\foo\\bar\\baz-quux", "C:\\foo\\bar\\baz", "..\\baz"], - ["C:\\foo\\bar\\baz", "C:\\foo\\bar\\baz-quux", "..\\baz-quux"], - ["\\\\foo\\bar", "\\\\foo\\bar\\baz", "baz"], - ["\\\\foo\\bar\\baz", "\\\\foo\\bar", ".."], - ["\\\\foo\\bar\\baz-quux", "\\\\foo\\bar\\baz", "..\\baz"], - ["\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz-quux", "..\\baz-quux"], - ["C:\\baz-quux", "C:\\baz", "..\\baz"], - ["C:\\baz", "C:\\baz-quux", "..\\baz-quux"], - ["\\\\foo\\baz-quux", "\\\\foo\\baz", "..\\baz"], - ["\\\\foo\\baz", "\\\\foo\\baz-quux", "..\\baz-quux"], - // ["C:\\baz", "\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz"], - ["\\\\foo\\bar\\baz", "C:\\baz", "C:\\baz"], - ["C:\\dev\\test", "C:\\dev\\test\\hello.test.ts", "hello.test.ts"], - ], - ], - [ - path.posix.relative, - // Arguments result - [ - ["/var/lib", "/var", ".."], - ["/var/lib", "/bin", "../../bin"], - ["/var/lib", "/var/lib", ""], - ["/var/lib", "/var/apache", "../apache"], - ["/var/", "/var/lib", "lib"], - ["/", "/var/lib", "var/lib"], - ["/foo/test", "/foo/test/bar/package.json", "bar/package.json"], - ["/Users/a/web/b/test/mails", "/Users/a/web/b", "../.."], - ["/foo/bar/baz-quux", "/foo/bar/baz", "../baz"], - ["/foo/bar/baz", "/foo/bar/baz-quux", "../baz-quux"], - ["/baz-quux", "/baz", "../baz"], - ["/baz", "/baz-quux", "../baz-quux"], - ["/page1/page2/foo", "/", "../../.."], - [path.posix.resolve("."), "foo", "foo"], - ["/webpack", "/webpack", ""], - ["/webpack/", "/webpack", ""], - ["/webpack", "/webpack/", ""], - ["/webpack/", "/webpack/", ""], - ["/webpack-hot-middleware", "/webpack/buildin/module.js", "../webpack/buildin/module.js"], - ["/webp4ck-hot-middleware", "/webpack/buildin/module.js", "../webpack/buildin/module.js"], - ["/webpack-hot-middleware", "/webp4ck/buildin/module.js", "../webp4ck/buildin/module.js"], - ["/var/webpack-hot-middleware", "/var/webpack/buildin/module.js", "../webpack/buildin/module.js"], - ["/app/node_modules/pkg", "../static", `../../..${parentIsRoot ? "" : path.posix.resolve("../")}/static`], - ["/app/node_modules/pkg", "../../static", `../../..${parentIsRoot ? "" : path.posix.resolve("../../")}/static`], - ["/app", "../static", `..${parentIsRoot ? "" : path.posix.resolve("../")}/static`], - ["/app", "../".repeat(64) + "static", "../static"], - [".", "../static", cwd == "/" ? "static" : "../static"], - ["/", "../static", parentIsRoot ? "static" : `${path.posix.resolve("../")}/static`.slice(1)], - ["../", "../", ""], - ["../", "../../", parentIsRoot ? "" : ".."], - ["../../", "../", parentIsRoot ? "" : path.basename(cwdParent)], - ["../../", "../../", ""], - ], - ], - ]; - - relativeTests.forEach(test => { - const relative = test[0]; - test[1].forEach(test => { - const actual = relative(test[0], test[1]); - const expected = test[2]; - if (actual !== expected) { - const os = relative === path.win32.relative ? "win32" : "posix"; - const message = `path.${os}.relative(${test - .slice(0, 2) - .map(JSON.stringify) - .join(",")})\n expect=${JSON.stringify(expected)}\n actual=${JSON.stringify(actual)}`; - failures.push(`\n${message}`); } - }); + } }); - strictEqual(failures.length, 0, failures.join("")); - expect(true).toBe(true); -}); - -it("path.normalize", () => { - strictEqual(path.win32.normalize("./fixtures///b/../b/c.js"), "fixtures\\b\\c.js"); - strictEqual(path.win32.normalize("/foo/../../../bar"), "\\bar"); - strictEqual(path.win32.normalize("a//b//../b"), "a\\b"); - strictEqual(path.win32.normalize("a//b//./c"), "a\\b\\c"); - strictEqual(path.win32.normalize("a//b//."), "a\\b"); - strictEqual(path.win32.normalize("//server/share/dir/file.ext"), "\\\\server\\share\\dir\\file.ext"); - strictEqual(path.win32.normalize("/a/b/c/../../../x/y/z"), "\\x\\y\\z"); - strictEqual(path.win32.normalize("C:"), "C:."); - strictEqual(path.win32.normalize("C:..\\abc"), "C:..\\abc"); - strictEqual(path.win32.normalize("C:..\\..\\abc\\..\\def"), "C:..\\..\\def"); - strictEqual(path.win32.normalize("C:\\."), "C:\\"); - strictEqual(path.win32.normalize("file:stream"), "file:stream"); - strictEqual(path.win32.normalize("bar\\foo..\\..\\"), "bar\\"); - strictEqual(path.win32.normalize("bar\\foo..\\.."), "bar"); - strictEqual(path.win32.normalize("bar\\foo..\\..\\baz"), "bar\\baz"); - strictEqual(path.win32.normalize("bar\\foo..\\"), "bar\\foo..\\"); - strictEqual(path.win32.normalize("bar\\foo.."), "bar\\foo.."); - strictEqual(path.win32.normalize("..\\foo..\\..\\..\\bar"), "..\\..\\bar"); - strictEqual(path.win32.normalize("..\\...\\..\\.\\...\\..\\..\\bar"), "..\\..\\bar"); - strictEqual(path.win32.normalize("../../../foo/../../../bar"), "..\\..\\..\\..\\..\\bar"); - strictEqual(path.win32.normalize("../../../foo/../../../bar/../../"), "..\\..\\..\\..\\..\\..\\"); - strictEqual(path.win32.normalize("../foobar/barfoo/foo/../../../bar/../../"), "..\\..\\"); - strictEqual(path.win32.normalize("../.../../foobar/../../../bar/../../baz"), "..\\..\\..\\..\\baz"); - strictEqual(path.win32.normalize("foo/bar\\baz"), "foo\\bar\\baz"); - strictEqual(path.posix.normalize("./fixtures///b/../b/c.js"), "fixtures/b/c.js"); - strictEqual(path.posix.normalize("/foo/../../../bar"), "/bar"); - strictEqual(path.posix.normalize("a//b//../b"), "a/b"); - strictEqual(path.posix.normalize("a//b//./c"), "a/b/c"); - strictEqual(path.posix.normalize("a//b//."), "a/b"); - strictEqual(path.posix.normalize("/a/b/c/../../../x/y/z"), "/x/y/z"); - strictEqual(path.posix.normalize("///..//./foo/.//bar"), "/foo/bar"); - strictEqual(path.posix.normalize("bar/foo../../"), "bar/"); - strictEqual(path.posix.normalize("bar/foo../.."), "bar"); - strictEqual(path.posix.normalize("bar/foo../../baz"), "bar/baz"); - strictEqual(path.posix.normalize("bar/foo../"), "bar/foo../"); - strictEqual(path.posix.normalize("bar/foo.."), "bar/foo.."); - strictEqual(path.posix.normalize("../foo../../../bar"), "../../bar"); - strictEqual(path.posix.normalize("../.../.././.../../../bar"), "../../bar"); - strictEqual(path.posix.normalize("../../../foo/../../../bar"), "../../../../../bar"); - strictEqual(path.posix.normalize("../../../foo/../../../bar/../../"), "../../../../../../"); - strictEqual(path.posix.normalize("../foobar/barfoo/foo/../../../bar/../../"), "../../"); - strictEqual(path.posix.normalize("../.../../foobar/../../../bar/../../baz"), "../../../../baz"); - strictEqual(path.posix.normalize("foo/bar\\baz"), "foo/bar\\baz"); - strictEqual(path.posix.normalize(""), "."); -}); - -it("path.resolve", () => { - const failures = []; - const slashRE = /\//g; - const backslashRE = /\\/g; - - const resolveTests = [ - [ - path.win32.resolve, - // Arguments result - [ - [["c:/blah\\blah", "d:/games", "c:../a"], "c:\\blah\\a"], - [["c:/ignore", "d:\\a/b\\c/d", "\\e.exe"], "d:\\e.exe"], - [["c:/ignore", "c:/some/file"], "c:\\some\\file"], - [["d:/ignore", "d:some/dir//"], "d:\\ignore\\some\\dir"], - [["."], process.cwd()], - [["//server/share", "..", "relative\\"], "\\\\server\\share\\relative"], - [["c:/", "//"], "c:\\"], - [["c:/", "//dir"], "c:\\dir"], - // TODO: - // [["c:/", "//server/share"], "\\\\server\\share\\"], - // [["c:/", "//server//share"], "\\\\server\\share\\"], - [["c:/", "///some//dir"], "c:\\some\\dir"], - [["C:\\foo\\tmp.3\\", "..\\tmp.3\\cycles\\root.js"], "C:\\foo\\tmp.3\\cycles\\root.js"], - ], - ], - [ - path.posix.resolve, - // Arguments result - [ - [["/var/lib", "../", "file/"], "/var/file"], - [["/var/lib", "/../", "file/"], "/file"], - [ - ["a/b/c/", "../../.."], - process.platform === "win32" ? process.cwd().slice(2).replaceAll("\\", "/") : process.cwd(), - ], - [["."], process.platform === "win32" ? process.cwd().slice(2).replaceAll("\\", "/") : process.cwd()], - [["/some/dir", ".", "/absolute/"], "/absolute"], - [["/foo/tmp.3/", "../tmp.3/cycles/root.js"], "/foo/tmp.3/cycles/root.js"], - ], - ], - ]; - const isWindows = false; - resolveTests.forEach(([resolve, tests]) => { - tests.forEach(([test, expected]) => { - const actual = resolve.apply(null, test); - let actualAlt; - const os = resolve === path.win32.resolve ? "win32" : "posix"; - if (resolve === path.win32.resolve && !isWindows) actualAlt = actual.replace(backslashRE, "/"); - else if (resolve !== path.win32.resolve && isWindows) actualAlt = actual.replace(slashRE, "\\"); - - const message = `path.${os}.resolve(${test.map(JSON.stringify).join(",")})\n expect=${JSON.stringify( - expected, - )}\n actual=${JSON.stringify(actual)}`; - if (actual !== expected && actualAlt !== expected) failures.push(message); - }); + test("path.sep", () => { + // path.sep tests + // windows + assert.strictEqual(path.win32.sep, "\\"); + // posix + assert.strictEqual(path.posix.sep, "/"); }); - strictEqual(failures.length, 0, failures.join("\n")); -}); -describe("path.posix.parse and path.posix.format", () => { - const testCases = [ - { - input: "/tmp/test.txt", - expected: { - root: "/", - dir: "/tmp", - base: "test.txt", - ext: ".txt", - name: "test", - }, - }, - { - input: "/tmp/test/file.txt", - expected: { - root: "/", - dir: "/tmp/test", - base: "file.txt", - ext: ".txt", - name: "file", - }, - }, - { - input: "/tmp/test/dir", - expected: { - root: "/", - dir: "/tmp/test", - base: "dir", - ext: "", - name: "dir", - }, - }, - { - input: "/tmp/test/dir/", - expected: { - root: "/", - dir: "/tmp/test", - base: "dir", - ext: "", - name: "dir", - }, - }, - { - input: ".", - expected: { - root: "", - dir: "", - base: ".", - ext: "", - name: ".", - }, - }, - { - input: "./", - expected: { - root: "", - dir: "", - base: ".", - ext: "", - name: ".", - }, - }, - { - input: "/.", - expected: { - root: "/", - dir: "/", - base: ".", - ext: "", - name: ".", - }, - }, - { - input: "/../", - expected: { - root: "/", - dir: "/", - base: "..", - ext: ".", - name: ".", - }, - }, - { - input: "./file.txt", - expected: { - root: "", - dir: ".", - base: "file.txt", - ext: ".txt", - name: "file", - }, - }, - { - input: "../file.txt", - expected: { - root: "", - dir: "..", - base: "file.txt", - ext: ".txt", - name: "file", - }, - }, - { - input: "../test/file.txt", - expected: { - root: "", - dir: "../test", - base: "file.txt", - ext: ".txt", - name: "file", - }, - }, - { - input: "test/file.txt", - expected: { - root: "", - dir: "test", - base: "file.txt", - ext: ".txt", - name: "file", - }, - }, - { - input: "test/dir", - expected: { - root: "", - dir: "test", - base: "dir", - ext: "", - name: "dir", - }, - }, - { - input: "test/dir/another_dir", - expected: { - root: "", - dir: "test/dir", - base: "another_dir", - ext: "", - name: "another_dir", - }, - }, - { - input: "./dir", - expected: { - root: "", - dir: ".", - base: "dir", - ext: "", - name: "dir", - }, - }, - { - input: "../dir", - expected: { - root: "", - dir: "..", - base: "dir", - ext: "", - name: "dir", - }, - }, - { - input: "../dir/another_dir", - expected: { - root: "", - dir: "../dir", - base: "another_dir", - ext: "", - name: "another_dir", - }, - }, - { - // https://github.com/oven-sh/bun/issues/4954 - input: "/test/Ł.txt", - expected: { - root: "/", - dir: "/test", - base: "Ł.txt", - ext: ".txt", - name: "Ł", - }, - }, - { - // https://github.com/oven-sh/bun/issues/8090 - input: ".prettierrc", - expected: { - root: "", - dir: "", - base: ".prettierrc", - ext: "", - name: ".prettierrc", - }, - }, - ]; - testCases.forEach(({ input, expected }) => { - it(`case ${input}`, () => { - const parsed = path.posix.parse(input); - expect(parsed).toStrictEqual(expected); + test("path.delimiter", () => { + // path.delimiter tests + // windows + assert.strictEqual(path.win32.delimiter, ";"); + // posix + assert.strictEqual(path.posix.delimiter, ":"); - const formatted = path.posix.format(parsed); - expect(formatted).toStrictEqual(input.slice(-1) === "/" ? input.slice(0, -1) : input); - }); - }); - it("empty string arguments, issue #4005", () => { - expect( - path.posix.format({ - root: "", - dir: "", - base: "", - name: "foo", - ext: ".ts", - }), - ).toStrictEqual("foo.ts"); - expect( - path.posix.format({ - name: "foo", - ext: ".ts", - }), - ).toStrictEqual("foo.ts"); + if (isWindows) assert.strictEqual(path, path.win32); + else assert.strictEqual(path, path.posix); }); }); - -test("path.format works for vite's example", () => { - expect( - path.format({ - root: "", - dir: "", - name: "index", - base: undefined, - ext: ".css", - }), - ).toBe("index.css"); -}); - -it("path.extname", () => { - expect(path.extname("index.js")).toBe(".js"); - expect(path.extname("make_plot.🔥")).toBe(".🔥"); -}); - -describe("isAbsolute", () => { - it("win32 /foo/bar", () => expect(path.win32.isAbsolute("/foo/bar")).toBe(true)); - it("posix /foo/bar", () => expect(path.posix.isAbsolute("/foo/bar")).toBe(true)); - it("win32 \\hello\\world", () => expect(path.win32.isAbsolute("\\hello\\world")).toBe(true)); - it("posix \\hello\\world", () => expect(path.posix.isAbsolute("\\hello\\world")).toBe(false)); - it("win32 C:\\hello\\world", () => expect(path.win32.isAbsolute("C:\\hello\\world")).toBe(true)); - it("posix C:\\hello\\world", () => expect(path.posix.isAbsolute("C:\\hello\\world")).toBe(false)); -}); diff --git a/test/js/node/path/posix-exists.test.js b/test/js/node/path/posix-exists.test.js new file mode 100644 index 00000000000000..5523c2f6d30715 --- /dev/null +++ b/test/js/node/path/posix-exists.test.js @@ -0,0 +1,8 @@ +import { test, describe } from "bun:test"; +import assert from "node:assert"; + +describe("path.posix", () => { + test("exists", () => { + assert.strictEqual(require("path/posix"), require("path").posix); + }); +}); diff --git a/test/js/node/path/posix-relative-on-windows.test.js b/test/js/node/path/posix-relative-on-windows.test.js new file mode 100644 index 00000000000000..d91393c2f82bd6 --- /dev/null +++ b/test/js/node/path/posix-relative-on-windows.test.js @@ -0,0 +1,14 @@ +import { test, describe } from "bun:test"; +import assert from "node:assert"; +import path from "node:path"; + +const isWindows = process.platform === "win32"; + +describe("path.posix.relative", () => { + test.skipIf(!isWindows)("on windows", () => { + // Refs: https://github.com/nodejs/node/issues/13683 + + const relativePath = path.posix.relative("a/b/c", "../../x"); + assert.match(relativePath, /^(\.\.\/){3,5}x$/); + }); +}); diff --git a/test/js/node/path/relative.test.js b/test/js/node/path/relative.test.js new file mode 100644 index 00000000000000..44fd66d6fa6fec --- /dev/null +++ b/test/js/node/path/relative.test.js @@ -0,0 +1,77 @@ +import { test, describe } from "bun:test"; +import assert from "node:assert"; +import path from "node:path"; + +describe("path.relative", () => { + test("general", () => { + const failures = []; + + const relativeTests = [ + [ + path.win32.relative, + // Arguments result + [ + ["c:/blah\\blah", "d:/games", "d:\\games"], + ["c:/aaaa/bbbb", "c:/aaaa", ".."], + ["c:/aaaa/bbbb", "c:/cccc", "..\\..\\cccc"], + ["c:/aaaa/bbbb", "c:/aaaa/bbbb", ""], + ["c:/aaaa/bbbb", "c:/aaaa/cccc", "..\\cccc"], + ["c:/aaaa/", "c:/aaaa/cccc", "cccc"], + ["c:/", "c:\\aaaa\\bbbb", "aaaa\\bbbb"], + ["c:/aaaa/bbbb", "d:\\", "d:\\"], + ["c:/AaAa/bbbb", "c:/aaaa/bbbb", ""], + ["c:/aaaaa/", "c:/aaaa/cccc", "..\\aaaa\\cccc"], + ["C:\\foo\\bar\\baz\\quux", "C:\\", "..\\..\\..\\.."], + ["C:\\foo\\test", "C:\\foo\\test\\bar\\package.json", "bar\\package.json"], + ["C:\\foo\\bar\\baz-quux", "C:\\foo\\bar\\baz", "..\\baz"], + ["C:\\foo\\bar\\baz", "C:\\foo\\bar\\baz-quux", "..\\baz-quux"], + ["\\\\foo\\bar", "\\\\foo\\bar\\baz", "baz"], + ["\\\\foo\\bar\\baz", "\\\\foo\\bar", ".."], + ["\\\\foo\\bar\\baz-quux", "\\\\foo\\bar\\baz", "..\\baz"], + ["\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz-quux", "..\\baz-quux"], + ["C:\\baz-quux", "C:\\baz", "..\\baz"], + ["C:\\baz", "C:\\baz-quux", "..\\baz-quux"], + ["\\\\foo\\baz-quux", "\\\\foo\\baz", "..\\baz"], + ["\\\\foo\\baz", "\\\\foo\\baz-quux", "..\\baz-quux"], + ["C:\\baz", "\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz"], + ["\\\\foo\\bar\\baz", "C:\\baz", "C:\\baz"], + ], + ], + [ + path.posix.relative, + // Arguments result + [ + ["/var/lib", "/var", ".."], + ["/var/lib", "/bin", "../../bin"], + ["/var/lib", "/var/lib", ""], + ["/var/lib", "/var/apache", "../apache"], + ["/var/", "/var/lib", "lib"], + ["/", "/var/lib", "var/lib"], + ["/foo/test", "/foo/test/bar/package.json", "bar/package.json"], + ["/Users/a/web/b/test/mails", "/Users/a/web/b", "../.."], + ["/foo/bar/baz-quux", "/foo/bar/baz", "../baz"], + ["/foo/bar/baz", "/foo/bar/baz-quux", "../baz-quux"], + ["/baz-quux", "/baz", "../baz"], + ["/baz", "/baz-quux", "../baz-quux"], + ["/page1/page2/foo", "/", "../../.."], + ], + ], + ]; + relativeTests.forEach(test => { + const relative = test[0]; + test[1].forEach(test => { + const actual = relative(test[0], test[1]); + const expected = test[2]; + if (actual !== expected) { + const os = relative === path.win32.relative ? "win32" : "posix"; + const message = `path.${os}.relative(${test + .slice(0, 2) + .map(JSON.stringify) + .join(",")})\n expect=${JSON.stringify(expected)}\n actual=${JSON.stringify(actual)}`; + failures.push(`\n${message}`); + } + }); + }); + assert.strictEqual(failures.length, 0, failures.join("")); + }); +}); diff --git a/test/js/node/path/resolve.test.js b/test/js/node/path/resolve.test.js new file mode 100644 index 00000000000000..3d51b63d84d5ec --- /dev/null +++ b/test/js/node/path/resolve.test.js @@ -0,0 +1,96 @@ +import { test, describe } from "bun:test"; +import assert from "node:assert"; +import child from "node:child_process"; +import path from "node:path"; + +const isWindows = process.platform === "win32"; + +describe("path.resolve", () => { + test("general", () => { + const failures = []; + const slashRE = /\//g; + const backslashRE = /\\/g; + + const posixyCwd = isWindows + ? (() => { + const _ = process.cwd().replaceAll(path.sep, path.posix.sep); + return _.slice(_.indexOf(path.posix.sep)); + })() + : process.cwd(); + + const resolveTests = [ + [ + path.win32.resolve, + // Arguments result + [ + [["c:/blah\\blah", "d:/games", "c:../a"], "c:\\blah\\a"], + [["c:/ignore", "d:\\a/b\\c/d", "\\e.exe"], "d:\\e.exe"], + [["c:/ignore", "c:/some/file"], "c:\\some\\file"], + [["d:/ignore", "d:some/dir//"], "d:\\ignore\\some\\dir"], + [["."], process.cwd()], + [["//server/share", "..", "relative\\"], "\\\\server\\share\\relative"], + [["c:/", "//"], "c:\\"], + [["c:/", "//dir"], "c:\\dir"], + [["c:/", "//server/share"], "\\\\server\\share\\"], + [["c:/", "//server//share"], "\\\\server\\share\\"], + [["c:/", "///some//dir"], "c:\\some\\dir"], + [["C:\\foo\\tmp.3\\", "..\\tmp.3\\cycles\\root.js"], "C:\\foo\\tmp.3\\cycles\\root.js"], + ], + ], + [ + path.posix.resolve, + // Arguments result + [ + [["/var/lib", "../", "file/"], "/var/file"], + [["/var/lib", "/../", "file/"], "/file"], + [["a/b/c/", "../../.."], posixyCwd], + [["."], posixyCwd], + [["/some/dir", ".", "/absolute/"], "/absolute"], + [["/foo/tmp.3/", "../tmp.3/cycles/root.js"], "/foo/tmp.3/cycles/root.js"], + ], + ], + ]; + resolveTests.forEach(([resolve, tests]) => { + tests.forEach(([test, expected]) => { + const actual = resolve.apply(null, test); + let actualAlt; + const os = resolve === path.win32.resolve ? "win32" : "posix"; + if (resolve === path.win32.resolve && !isWindows) actualAlt = actual.replace(backslashRE, "/"); + else if (resolve !== path.win32.resolve && isWindows) actualAlt = actual.replace(slashRE, "\\"); + + const message = `path.${os}.resolve(${test.map(JSON.stringify).join(",")})\n expect=${JSON.stringify( + expected, + )}\n actual=${JSON.stringify(actual)}`; + if (actual !== expected && actualAlt !== expected) failures.push(message); + }); + }); + assert.strictEqual(failures.length, 0, failures.join("\n")); + + // @TODO: Enable test once spawnResult.stdout.toString().trim() works on Windows. + // if (isWindows) { + // // Test resolving the current Windows drive letter from a spawned process. + // // See https://github.com/nodejs/node/issues/7215 + // const currentDriveLetter = path.parse(process.cwd()).root.substring(0, 2); + // const relativeFixture = ".\\fixtures\\path-resolve.js"; + + // const spawnResult = child.spawnSync(process.argv[0], [relativeFixture, currentDriveLetter]); + // const resolvedPath = spawnResult.stdout.toString().trim(); + // assert.strictEqual(resolvedPath.toLowerCase(), process.cwd().toLowerCase()); + // } + + // @TODO: Enable once support for customizing process.cwd lands. + // if (!isWindows) { + // // Test handling relative paths to be safe when process.cwd() fails. + // const cwd = process.cwd; + // process.cwd = () => ""; + // try { + // assert.strictEqual(process.cwd(), ""); + // const resolved = path.resolve(); + // const expected = "."; + // assert.strictEqual(resolved, expected); + // } finally { + // process.cwd = cwd; + // } + // } + }); +}); diff --git a/test/js/node/path/to-namespaced-path.test.js b/test/js/node/path/to-namespaced-path.test.js new file mode 100644 index 00000000000000..32a8c002e2780d --- /dev/null +++ b/test/js/node/path/to-namespaced-path.test.js @@ -0,0 +1,82 @@ +import { test, describe } from "bun:test"; +import assert from "node:assert"; +import path from "node:path"; + +const isWindows = process.platform === "win32"; + +describe("path.toNamespacedPath", () => { + const emptyObj = {}; + + test("platform", () => { + assert.strictEqual(path.toNamespacedPath(""), ""); + assert.strictEqual(path.toNamespacedPath(null), null); + assert.strictEqual(path.toNamespacedPath(100), 100); + assert.strictEqual(path.toNamespacedPath(path), path); + assert.strictEqual(path.toNamespacedPath(false), false); + assert.strictEqual(path.toNamespacedPath(true), true); + + if (isWindows) { + // These tests cause resolve() to insert the cwd, so we cannot test them from + // non-Windows platforms (easily) + assert.strictEqual(path.toNamespacedPath(""), ""); + assert.strictEqual( + path.win32.toNamespacedPath("foo\\bar").toLowerCase(), + `\\\\?\\${process.cwd().toLowerCase()}\\foo\\bar`, + ); + assert.strictEqual( + path.win32.toNamespacedPath("foo/bar").toLowerCase(), + `\\\\?\\${process.cwd().toLowerCase()}\\foo\\bar`, + ); + // @TODO: Enable once process.cwd() is plumped. + // const currentDeviceLetter = path.parse(process.cwd()).root.substring(0, 2); + // assert.strictEqual( + // path.win32.toNamespacedPath(currentDeviceLetter).toLowerCase(), + // `\\\\?\\${process.cwd().toLowerCase()}`, + // ); + assert.strictEqual(path.win32.toNamespacedPath("C").toLowerCase(), `\\\\?\\${process.cwd().toLowerCase()}\\c`); + + // @TODO: Enable once process.cwd() is plumped. + // const relativeFixture = ".\\test\\js\\node\\path\\fixtures\\a.js"; + // const resolvedFixture = path.resolve(relativeFixture); + + // assert.strictEqual(path.toNamespacedPath(relativeFixture), `\\\\?\\${resolvedFixture}`); + // assert.strictEqual(path.toNamespacedPath(`\\\\?\\${relativeFixture}`), `\\\\?\\${resolvedFixture}`); + assert.strictEqual( + path.toNamespacedPath("\\\\someserver\\someshare\\somefile"), + "\\\\?\\UNC\\someserver\\someshare\\somefile", + ); + assert.strictEqual( + path.toNamespacedPath("\\\\?\\UNC\\someserver\\someshare\\somefile"), + "\\\\?\\UNC\\someserver\\someshare\\somefile", + ); + assert.strictEqual(path.toNamespacedPath("\\\\.\\pipe\\somepipe"), "\\\\.\\pipe\\somepipe"); + } + }); + + test("alias as _makeLong", () => { + assert.strictEqual(path._makeLong, path.toNamespacedPath); + }); + + test("win32", () => { + assert.strictEqual(path.win32.toNamespacedPath("C:\\foo"), "\\\\?\\C:\\foo"); + assert.strictEqual(path.win32.toNamespacedPath("C:/foo"), "\\\\?\\C:\\foo"); + assert.strictEqual(path.win32.toNamespacedPath("\\\\foo\\bar"), "\\\\?\\UNC\\foo\\bar\\"); + assert.strictEqual(path.win32.toNamespacedPath("//foo//bar"), "\\\\?\\UNC\\foo\\bar\\"); + assert.strictEqual(path.win32.toNamespacedPath("\\\\?\\foo"), "\\\\?\\foo"); + assert.strictEqual(path.win32.toNamespacedPath(null), null); + assert.strictEqual(path.win32.toNamespacedPath(true), true); + assert.strictEqual(path.win32.toNamespacedPath(1), 1); + assert.strictEqual(path.win32.toNamespacedPath(), undefined); + assert.strictEqual(path.win32.toNamespacedPath(emptyObj), emptyObj); + }); + + test("posix", () => { + assert.strictEqual(path.posix.toNamespacedPath("/foo/bar"), "/foo/bar"); + assert.strictEqual(path.posix.toNamespacedPath("foo/bar"), "foo/bar"); + assert.strictEqual(path.posix.toNamespacedPath(null), null); + assert.strictEqual(path.posix.toNamespacedPath(true), true); + assert.strictEqual(path.posix.toNamespacedPath(1), 1); + assert.strictEqual(path.posix.toNamespacedPath(), undefined); + assert.strictEqual(path.posix.toNamespacedPath(emptyObj), emptyObj); + }); +}); diff --git a/test/js/node/path/win32-exists.test.js b/test/js/node/path/win32-exists.test.js new file mode 100644 index 00000000000000..7637e537129b4c --- /dev/null +++ b/test/js/node/path/win32-exists.test.js @@ -0,0 +1,8 @@ +import { test, describe } from "bun:test"; +import assert from "node:assert"; + +describe("path.win32", () => { + test("exists", () => { + assert.strictEqual(require("path/win32"), require("path").win32); + }); +}); diff --git a/test/js/node/path/zero-length-strings.test.js b/test/js/node/path/zero-length-strings.test.js new file mode 100644 index 00000000000000..4a1088a127d2c5 --- /dev/null +++ b/test/js/node/path/zero-length-strings.test.js @@ -0,0 +1,42 @@ +import { test, describe } from "bun:test"; +import assert from "node:assert"; +import path from "node:path"; + +// These testcases are specific to one uncommon behavior in path module. Few +// of the functions in path module, treat '' strings as current working +// directory. This test makes sure that the behavior is intact between commits. +// See: https://github.com/nodejs/node/pull/2106 + +const pwd = process.cwd(); + +describe("path", () => { + test("zero length strings", () => { + // Join will internally ignore all the zero-length strings and it will return + // '.' if the joined string is a zero-length string. + assert.strictEqual(path.posix.join(""), "."); + assert.strictEqual(path.posix.join("", ""), "."); + assert.strictEqual(path.win32.join(""), "."); + assert.strictEqual(path.win32.join("", ""), "."); + assert.strictEqual(path.join(pwd), pwd); + assert.strictEqual(path.join(pwd, ""), pwd); + + // Normalize will return '.' if the input is a zero-length string + assert.strictEqual(path.posix.normalize(""), "."); + assert.strictEqual(path.win32.normalize(""), "."); + assert.strictEqual(path.normalize(pwd), pwd); + + // Since '' is not a valid path in any of the common environments, return false + assert.strictEqual(path.posix.isAbsolute(""), false); + assert.strictEqual(path.win32.isAbsolute(""), false); + + // Resolve, internally ignores all the zero-length strings and returns the + // current working directory + assert.strictEqual(path.resolve(""), pwd); + assert.strictEqual(path.resolve("", ""), pwd); + + // Relative, internally calls resolve. So, '' is actually the current directory + assert.strictEqual(path.relative("", pwd), ""); + assert.strictEqual(path.relative(pwd, ""), ""); + assert.strictEqual(path.relative(pwd, pwd), ""); + }); +});