threadlocal var parser_join_input_buffer: [4096]u8 = undefined; threadlocal var parser_buffer: [1024]u8 = undefined; pub fn z(input: []const u8, output: *bun.PathBuffer) [:0]const u8 { if (input.len > bun.MAX_PATH_BYTES) { if (comptime bun.Environment.allow_assert) @panic("path too long"); return ""; } @memcpy(output[0..input.len], input); output[input.len] = 0; return output[0..input.len :0]; } inline fn nqlAtIndex(comptime string_count: comptime_int, index: usize, input: []const []const u8) bool { comptime var string_index = 1; inline while (string_index < string_count) : (string_index += 1) { if (input[0][index] != input[string_index][index]) { return true; } } return false; } inline fn nqlAtIndexCaseInsensitive(comptime string_count: comptime_int, index: usize, input: []const []const u8) bool { comptime var string_index = 1; inline while (string_index < string_count) : (string_index += 1) { if (std.ascii.toLower(input[0][index]) != std.ascii.toLower(input[string_index][index])) { return true; } } return false; } const IsSeparatorFunc = fn (char: u8) bool; const IsSeparatorFuncT = fn (comptime T: type, char: anytype) bool; const LastSeparatorFunction = fn (slice: []const u8) ?usize; const LastSeparatorFunctionT = fn (comptime T: type, slice: anytype) ?usize; inline fn @"is .."(slice: []const u8) bool { return slice.len >= 2 and @as(u16, @bitCast(slice[0..2].*)) == comptime std.mem.readInt(u16, "..", .little); } inline fn @"is .. with type"(comptime T: type, slice: []const T) bool { if (comptime T == u8) return @"is .."(slice); return slice.len >= 2 and slice[0] == '.' and slice[1] == '.'; } inline fn @"is ../"(slice: []const u8) bool { return strings.hasPrefixComptime(slice, "../"); } const ParentEqual = enum { parent, equal, unrelated, }; pub fn isParentOrEqual(parent_: []const u8, child: []const u8) ParentEqual { var parent = parent_; while (parent.len > 0 and isSepAny(parent[parent.len - 1])) { parent = parent[0 .. parent.len - 1]; } const contains = if (comptime !bun.Environment.isLinux) strings.containsCaseInsensitiveASCII else strings.contains; if (!contains(child, parent)) return .unrelated; if (child.len == parent.len) return .equal; if (child.len > parent.len and isSepAny(child[parent.len])) return .parent; return .unrelated; } pub fn getIfExistsLongestCommonPathGeneric(input: []const []const u8, comptime platform: Platform) ?[]const u8 { const separator = comptime platform.separator(); const isPathSeparator = comptime platform.getSeparatorFunc(); const nqlAtIndexFn = switch (platform) { else => nqlAtIndex, .windows => nqlAtIndexCaseInsensitive, }; var min_length: usize = std.math.maxInt(usize); for (input) |str| { min_length = @min(str.len, min_length); } var index: usize = 0; var last_common_separator: ?usize = null; // try to use an unrolled version of this loop switch (input.len) { 0 => { return ""; }, 1 => { return input[0]; }, inline 2, 3, 4, 5, 6, 7, 8 => |N| { while (index < min_length) : (index += 1) { if (nqlAtIndexFn(comptime N, index, input)) { if (last_common_separator == null) return null; break; } if (@call(bun.callmod_inline, isPathSeparator, .{input[0][index]})) { last_common_separator = index; } } }, else => { var string_index: usize = 1; while (string_index < input.len) : (string_index += 1) { while (index < min_length) : (index += 1) { if (platform == .windows) { if (std.ascii.toLower(input[0][index]) != std.ascii.toLower(input[string_index][index])) { if (last_common_separator == null) return null; break; } } else { if (input[0][index] != input[string_index][index]) { if (last_common_separator == null) return null; break; } } } if (index == min_length) index -= 1; if (@call(bun.callmod_inline, isPathSeparator, .{input[0][index]})) { last_common_separator = index; } } }, } if (index == 0) { return &([_]u8{separator}); } if (last_common_separator == null) { return &([_]u8{'.'}); } // The above won't work for a case like this: // /app/public/index.js // /app/public // It will return: // /app/ // It should return: // /app/public/ // To detect /app/public is actually a folder, we do one more loop through the strings // and say, "do one of you have a path separator after what we thought was the end?" for (input) |str| { if (str.len > index) { if (@call(bun.callmod_inline, isPathSeparator, .{str[index]})) { return str[0 .. index + 1]; } } } return input[0][0 .. last_common_separator.? + 1]; } // TODO: is it faster to determine longest_common_separator in the while loop // or as an extra step at the end? // only boether to check if this function appears in benchmarking pub fn longestCommonPathGeneric(input: []const []const u8, comptime platform: Platform) []const u8 { const separator = comptime platform.separator(); const isPathSeparator = comptime platform.getSeparatorFunc(); const nqlAtIndexFn = switch (platform) { else => nqlAtIndex, .windows => nqlAtIndexCaseInsensitive, }; var min_length: usize = std.math.maxInt(usize); for (input) |str| { min_length = @min(str.len, min_length); } var index: usize = 0; var last_common_separator: usize = 0; // try to use an unrolled version of this loop switch (input.len) { 0 => { return ""; }, 1 => { return input[0]; }, inline 2, 3, 4, 5, 6, 7, 8 => |n| { // If volume IDs do not match on windows, we can't have a common path if (platform == .windows) { const first_root = windowsFilesystemRoot(input[0]); comptime var i = 1; inline while (i < n) : (i += 1) { const root = windowsFilesystemRoot(input[i]); if (!strings.eqlCaseInsensitiveASCIIICheckLength(first_root, root)) { return ""; } } } while (index < min_length) : (index += 1) { if (nqlAtIndexFn(comptime n, index, input)) { break; } if (@call(bun.callmod_inline, isPathSeparator, .{input[0][index]})) { last_common_separator = index; } } }, else => { // If volume IDs do not match on windows, we can't have a common path if (platform == .windows) { const first_root = windowsFilesystemRoot(input[0]); var i: usize = 1; while (i < input.len) : (i += 1) { const root = windowsFilesystemRoot(input[i]); if (!strings.eqlCaseInsensitiveASCIIICheckLength(first_root, root)) { return ""; } } } var string_index: usize = 1; while (string_index < input.len) : (string_index += 1) { while (index < min_length) : (index += 1) { if (platform == .windows) { if (std.ascii.toLower(input[0][index]) != std.ascii.toLower(input[string_index][index])) { break; } } else { if (input[0][index] != input[string_index][index]) { break; } } } if (index == min_length) index -= 1; if (@call(bun.callmod_inline, isPathSeparator, .{input[0][index]})) { last_common_separator = index; } } }, } if (index == 0) { return &([_]u8{separator}); } // The above won't work for a case like this: // /app/public/index.js // /app/public // It will return: // /app/ // It should return: // /app/public/ // To detect /app/public is actually a folder, we do one more loop through the strings // and say, "do one of you have a path separator after what we thought was the end?" var idx = input.len; // Use this value as an invalid value. for (input, 0..) |str, i| { if (str.len > index) { if (@call(bun.callmod_inline, isPathSeparator, .{str[index]})) { idx = i; } else { idx = input.len; break; } } } if (idx != input.len) { return input[idx][0 .. index + 1]; } return input[0][0 .. last_common_separator + 1]; } pub fn longestCommonPath(input: []const []const u8) []const u8 { return longestCommonPathGeneric(input, .loose); } pub fn getIfExistsLongestCommonPath(input: []const []const u8) ?[]const u8 { return getIfExistsLongestCommonPathGeneric(input, .loose); } pub fn longestCommonPathWindows(input: []const []const u8) []const u8 { return longestCommonPathGeneric(input, .windows); } pub fn longestCommonPathPosix(input: []const []const u8) []const u8 { return longestCommonPathGeneric(input, .posix); } pub threadlocal var relative_to_common_path_buf: bun.PathBuffer = undefined; /// Find a relative path from a common path // Loosely based on Node.js' implementation of path.relative // https://github.com/nodejs/node/blob/9a7cbe25de88d87429a69050a1a1971234558d97/lib/path.js#L1250-L1259 pub fn relativeToCommonPath( common_path_: []const u8, normalized_from_: []const u8, normalized_to_: []const u8, buf: []u8, comptime always_copy: bool, comptime platform: Platform, ) []const u8 { var normalized_from = normalized_from_; var normalized_to = normalized_to_; const win_root_len = if (platform == .windows) k: { const from_root = windowsFilesystemRoot(normalized_from_); const to_root = windowsFilesystemRoot(normalized_to_); if (common_path_.len == 0) { // the only case path.relative can return not a relative string if (!strings.eqlCaseInsensitiveASCIIICheckLength(from_root, to_root)) { if (normalized_to_.len > to_root.len and normalized_to_[normalized_to_.len - 1] == '\\') { if (always_copy) { bun.copy(u8, buf, normalized_to_[0 .. normalized_to_.len - 1]); return buf[0 .. normalized_to_.len - 1]; } else { return normalized_to_[0 .. normalized_to_.len - 1]; } } else { if (always_copy) { bun.copy(u8, buf, normalized_to_); return buf[0..normalized_to_.len]; } else { return normalized_to_; } } } } normalized_from = normalized_from_[from_root.len..]; normalized_to = normalized_to_[to_root.len..]; break :k from_root.len; } else null; const separator = comptime platform.separator(); const common_path = if (platform == .windows) common_path_[win_root_len..] else if (std.fs.path.isAbsolutePosix(common_path_)) common_path_[1..] else common_path_; const shortest = @min(normalized_from.len, normalized_to.len); if (shortest == common_path.len) { if (normalized_to.len >= normalized_from.len) { if (common_path.len == 0) { if (platform == .windows and normalized_to.len > 3 and normalized_to[normalized_to.len - 1] == separator) { normalized_to.len -= 1; } // We get here if `from` is the root // For example: from='/'; to='/foo' if (always_copy) { bun.copy(u8, buf, normalized_to); return buf[0..normalized_to.len]; } else { return normalized_to; } } if (normalized_to[common_path.len - 1] == separator) { const slice = normalized_to[common_path.len..]; const without_trailing_slash = if (platform == .windows and slice.len > 3 and slice[slice.len - 1] == separator) slice[0 .. slice.len - 1] else slice; if (always_copy) { // We get here if `from` is the exact base path for `to`. // For example: from='/foo/bar'; to='/foo/bar/baz' bun.copy(u8, buf, without_trailing_slash); return buf[0..without_trailing_slash.len]; } else { return without_trailing_slash; } } } } const last_common_separator = strings.lastIndexOfChar( if (platform == .windows) common_path else common_path_, separator, ) orelse 0; // Generate the relative path based on the path difference between `to` // and `from`. var out_slice: []u8 = buf[0..0]; if (normalized_from.len > 0) { var i: usize = @as(usize, @intCast(@intFromBool(platform.isSeparator(normalized_from[0])))) + 1 + last_common_separator; while (i <= normalized_from.len) : (i += 1) { if (i == normalized_from.len or (normalized_from[i] == separator and i + 1 < normalized_from.len)) { if (out_slice.len == 0) { buf[0..2].* = "..".*; out_slice.len = 2; } else { buf[out_slice.len..][0..3].* = (&[_]u8{separator} ++ "..").*; out_slice.len += 3; } } } } if (normalized_to.len > last_common_separator + 1) { var tail = normalized_to[last_common_separator..]; if (normalized_from.len > 0 and (last_common_separator == normalized_from.len or (last_common_separator == normalized_from.len - 1))) { if (platform.isSeparator(tail[0])) { tail = tail[1..]; } } // avoid making non-absolute paths absolute const insert_leading_slash = !platform.isSeparator(tail[0]) and out_slice.len > 0 and !platform.isSeparator(out_slice[out_slice.len - 1]); if (insert_leading_slash) { buf[out_slice.len] = separator; out_slice.len += 1; } // Lastly, append the rest of the destination (`to`) path that comes after // the common path parts. bun.copy(u8, buf[out_slice.len..], tail); out_slice.len += tail.len; } if (out_slice.len > 3 and out_slice[out_slice.len - 1] == separator) { out_slice.len -= 1; } return out_slice; } pub fn relativeNormalizedBuf(buf: []u8, from: []const u8, to: []const u8, comptime platform: Platform, comptime always_copy: bool) []const u8 { if ((if (platform == .windows) strings.eqlCaseInsensitiveASCII(from, to, true) else from.len == to.len and strings.eqlLong(from, to, true))) { return ""; } const two = [_][]const u8{ from, to }; const common_path = longestCommonPathGeneric(&two, platform); return relativeToCommonPath(common_path, from, to, buf, always_copy, platform); } pub fn relativeNormalized(from: []const u8, to: []const u8, comptime platform: Platform, comptime always_copy: bool) []const u8 { return relativeNormalizedBuf(&relative_to_common_path_buf, from, to, platform, always_copy); } pub fn dirname(str: []const u8, comptime platform: Platform) []const u8 { switch (platform) { .loose => { const separator = lastIndexOfSeparatorLoose(str) orelse return ""; return str[0..separator]; }, .posix => { const separator = lastIndexOfSeparatorPosix(str) orelse return ""; if (separator == 0) return "/"; if (separator == str.len - 1) return dirname(str[0 .. str.len - 1], platform); return str[0..separator]; }, .windows => { const separator = lastIndexOfSeparatorWindows(str) orelse return std.fs.path.diskDesignatorWindows(str); return str[0..separator]; }, else => @compileError("not implemented"), } } pub fn dirnameW(str: []const u16) []const u16 { const separator = lastIndexOfSeparatorWindowsT(u16, str) orelse { // return disk designator instead if (str.len < 2) return &.{}; if (!(str[1] == ':')) return &.{}; if (!bun.path.isDriveLetterT(u16, str[0])) return &.{}; return str[0..2]; }; return str[0..separator]; } threadlocal var relative_from_buf: bun.PathBuffer = undefined; threadlocal var relative_to_buf: bun.PathBuffer = undefined; pub fn relative(from: []const u8, to: []const u8) []const u8 { return relativePlatform(from, to, .auto, false); } pub fn relativeZ(from: []const u8, to: []const u8) [:0]const u8 { return relativeBufZ(&relative_to_common_path_buf, from, to, .auto, true); } pub fn relativeBufZ(buf: []u8, from: []const u8, to: []const u8) [:0]const u8 { const rel = relativePlatformBuf(buf, from, to, .auto, true); buf[rel.len] = 0; return buf[0..rel.len :0]; } pub fn relativePlatformBuf(buf: []u8, from: []const u8, to: []const u8, comptime platform: Platform, comptime always_copy: bool) []const u8 { const normalized_from = if (platform.isAbsolute(from)) brk: { if (platform == .loose and bun.Environment.isWindows) { // we want to invoke the windows resolution behavior but end up with a // string with forward slashes. const normalized = normalizeStringBuf(from, relative_from_buf[1..], true, .windows, true); platformToPosixInPlace(u8, normalized); break :brk normalized; } const path = normalizeStringBuf(from, relative_from_buf[1..], true, platform, true); if (platform == .windows) break :brk path; relative_from_buf[0] = platform.separator(); break :brk relative_from_buf[0 .. path.len + 1]; } else joinAbsStringBuf( Fs.FileSystem.instance.top_level_dir, &relative_from_buf, &[_][]const u8{ normalizeStringBuf(from, relative_from_buf[1..], true, platform, true), }, platform, ); const normalized_to = if (platform.isAbsolute(to)) brk: { if (platform == .loose and bun.Environment.isWindows) { const normalized = normalizeStringBuf(to, relative_to_buf[1..], true, .windows, true); platformToPosixInPlace(u8, normalized); break :brk normalized; } const path = normalizeStringBuf(to, relative_to_buf[1..], true, platform, true); if (platform == .windows) break :brk path; relative_to_buf[0] = platform.separator(); break :brk relative_to_buf[0 .. path.len + 1]; } else joinAbsStringBuf( Fs.FileSystem.instance.top_level_dir, &relative_to_buf, &[_][]const u8{ normalizeStringBuf(to, relative_to_buf[1..], true, platform, true), }, platform, ); return relativeNormalizedBuf(buf, normalized_from, normalized_to, platform, always_copy); } pub fn relativePlatform(from: []const u8, to: []const u8, comptime platform: Platform, comptime always_copy: bool) []const u8 { return relativePlatformBuf(&relative_to_common_path_buf, from, to, platform, always_copy); } pub fn relativeAlloc(allocator: std.mem.Allocator, from: []const u8, to: []const u8) ![]const u8 { const result = relativePlatform(from, to, .auto, false); return try allocator.dupe(u8, result); } // This function is based on Go's volumeNameLen function // https://cs.opensource.google/go/go/+/refs/tags/go1.17.6:src/path/filepath/path_windows.go;l=57 // volumeNameLen returns length of the leading volume name on Windows. pub fn windowsVolumeNameLen(path: []const u8) struct { usize, usize } { return windowsVolumeNameLenT(u8, path); } pub fn windowsVolumeNameLenT(comptime T: type, path: []const T) struct { usize, usize } { if (path.len < 2) return .{ 0, 0 }; // with drive letter const c = path[0]; if (path[1] == ':') { if (isDriveLetterT(T, c)) { return .{ 2, 0 }; } } // UNC if (path.len >= 5 and Platform.windows.isSeparatorT(T, path[0]) and Platform.windows.isSeparatorT(T, path[1]) and !Platform.windows.isSeparatorT(T, path[2]) and path[2] != '.') { if (T == u8) { if (strings.indexOfAny(path[3..], "/\\")) |idx| { // TODO: handle input "//abc//def" should be picked up as a unc path if (path.len > idx + 4 and !Platform.windows.isSeparatorT(T, path[idx + 4])) { if (strings.indexOfAny(path[idx + 4 ..], "/\\")) |idx2| { return .{ idx + idx2 + 4, idx + 3 }; } else { return .{ path.len, idx + 3 }; } } } return .{ path.len, 0 }; } else { if (bun.strings.indexAnyComptimeT(T, path[3..], strings.literal(T, "/\\"))) |idx| { // TODO: handle input "//abc//def" should be picked up as a unc path if (path.len > idx + 4 and !Platform.windows.isSeparatorT(T, path[idx + 4])) { if (bun.strings.indexAnyComptimeT(T, path[idx + 4 ..], strings.literal(T, "/\\"))) |idx2| { return .{ idx + idx2 + 4, idx + 3 }; } else { return .{ path.len, idx + 3 }; } } } return .{ path.len, 0 }; } } return .{ 0, 0 }; } pub fn windowsVolumeName(path: []const u8) []const u8 { return path[0..@call(bun.callmod_inline, windowsVolumeNameLen, .{path})[0]]; } pub fn windowsFilesystemRoot(path: []const u8) []const u8 { return windowsFilesystemRootT(u8, path); } pub fn isDriveLetter(c: u8) bool { return isDriveLetterT(u8, c); } pub fn isDriveLetterT(comptime T: type, c: T) bool { return 'a' <= c and c <= 'z' or 'A' <= c and c <= 'Z'; } pub fn hasAnyIllegalChars(maybe_path: []const u8) bool { if (!bun.Environment.isWindows) return false; var maybe_path_ = maybe_path; // check for disk discrimnator; remove it since it has a ':' if (startsWithDiskDiscriminator(maybe_path_)) maybe_path_ = maybe_path_[2..]; // guard against OBJECT_NAME_INVALID => unreachable return bun.strings.indexAnyComptime(maybe_path_, "<>:\"|?*") != null; } pub fn startsWithDiskDiscriminator(maybe_path: []const u8) bool { if (!bun.Environment.isWindows) return false; if (maybe_path.len < 3) return false; if (!isDriveLetter(maybe_path[0])) return false; if (maybe_path[1] != ':') return false; if (maybe_path[2] != '\\') return false; return true; } // path.relative lets you do relative across different share drives pub fn windowsFilesystemRootT(comptime T: type, path: []const T) []const T { // minimum: `C:` if (path.len < 2) return if (isSepAnyT(T, path[0])) path[0..1] else path[0..0]; // with drive letter const c = path[0]; if (path[1] == ':') { if (isDriveLetterT(T, c)) { if (path.len > 2 and isSepAnyT(T, path[2])) return path[0..3] else return path[0..2]; } } // UNC and device paths if (path.len >= 5 and Platform.windows.isSeparatorT(T, path[0]) and Platform.windows.isSeparatorT(T, path[1]) and !Platform.windows.isSeparatorT(T, path[2])) { // device path if (path[2] == '.' and Platform.windows.isSeparatorT(T, path[3])) return path[0..4]; // UNC if (bun.strings.indexOfAnyT(T, path[3..], "/\\")) |idx| { if (bun.strings.indexOfAnyT(T, path[4 + idx ..], "/\\")) |idx_second| { return path[0 .. idx + idx_second + 4 + 1]; // +1 to skip second separator } } return path[0..]; } if (isSepAnyT(T, path[0])) return path[0..1]; return path[0..0]; } // This function is based on Go's filepath.Clean function // https://cs.opensource.google/go/go/+/refs/tags/go1.17.6:src/path/filepath/path.go;l=89 pub fn normalizeStringGeneric( path_: []const u8, buf: []u8, comptime allow_above_root: bool, comptime separator: u8, comptime isSeparator: anytype, comptime preserve_trailing_slash: bool, ) []u8 { return normalizeStringGenericT(u8, path_, buf, allow_above_root, separator, isSeparator, preserve_trailing_slash); } fn separatorAdapter(comptime T: type, func: anytype) fn (T) bool { return struct { fn call(char: T) bool { return func(T, char); } }.call; } pub fn normalizeStringGenericT( comptime T: type, path_: []const T, buf: []T, comptime allow_above_root: bool, comptime separator: T, comptime isSeparatorT: anytype, comptime preserve_trailing_slash: bool, ) []T { return normalizeStringGenericTZ(T, path_, buf, .{ .allow_above_root = allow_above_root, .separator = separator, .isSeparator = separatorAdapter(T, isSeparatorT), .preserve_trailing_slash = preserve_trailing_slash, .zero_terminate = false, .add_nt_prefix = false, }); } pub fn NormalizeOptions(comptime T: type) type { return struct { allow_above_root: bool = false, separator: T = std.fs.path.sep, isSeparator: fn (T) bool = struct { fn call(char: T) bool { return if (comptime std.fs.path.sep == std.fs.path.sep_windows) char == '\\' or char == '/' else char == '/'; } }.call, preserve_trailing_slash: bool = false, zero_terminate: bool = false, add_nt_prefix: bool = false, }; } pub fn normalizeStringGenericTZ( comptime T: type, path_: []const T, buf: []T, comptime options: NormalizeOptions(T), ) if (options.zero_terminate) [:0]T else []T { const isWindows, const sep_str = comptime .{ options.separator == std.fs.path.sep_windows, &[_]u8{options.separator} }; if (isWindows and bun.Environment.isDebug) { // this is here to catch a potential mistake by the caller // // since it is theoretically possible to get here in release // we will not do this check in release. assert(!strings.hasPrefixComptimeType(T, path_, strings.literal(T, ":\\"))); } var buf_i: usize = 0; var dotdot: usize = 0; var path_begin: usize = 0; const volLen, const indexOfThirdUNCSlash = if (isWindows) windowsVolumeNameLenT(T, path_) else .{ 0, 0 }; if (isWindows) { if (volLen > 0) { if (options.add_nt_prefix) { @memcpy(buf[buf_i .. buf_i + 4], strings.literal(T, "\\??\\")); buf_i += 4; } if (path_[1] != ':') { // UNC paths if (options.add_nt_prefix) { @memcpy(buf[buf_i .. buf_i + 4], strings.literal(T, "UNC" ++ sep_str)); buf_i += 2; } else { @memcpy(buf[buf_i .. buf_i + 2], strings.literal(T, sep_str ++ sep_str)); } if (indexOfThirdUNCSlash > 0) { // we have the ending slash @memcpy(buf[buf_i + 2 .. buf_i + indexOfThirdUNCSlash + 1], path_[2 .. indexOfThirdUNCSlash + 1]); buf[buf_i + indexOfThirdUNCSlash] = options.separator; @memcpy( buf[buf_i + indexOfThirdUNCSlash + 1 .. buf_i + volLen], path_[indexOfThirdUNCSlash + 1 .. volLen], ); } else { // we dont have the ending slash @memcpy(buf[buf_i + 2 .. buf_i + volLen], path_[2..volLen]); } buf[buf_i + volLen] = options.separator; buf_i += volLen + 1; path_begin = volLen + 1; // it is just a volume name if (path_begin >= path_.len) { if (options.zero_terminate) { buf[buf_i] = 0; return buf[0..buf_i :0]; } else { return buf[0..buf_i]; } } } else { // drive letter buf[buf_i] = std.ascii.toUpper(@truncate(path_[0])); buf[buf_i + 1] = ':'; buf_i += 2; dotdot = buf_i; path_begin = 2; } } else if (path_.len > 0 and options.isSeparator(path_[0])) { buf[buf_i] = options.separator; buf_i += 1; dotdot = buf_i; path_begin = 1; } } var r: usize = 0; var path, const buf_start = if (isWindows) .{ path_[path_begin..], buf_i } else .{ path_, 0 }; const n = path.len; if (isWindows and (options.allow_above_root or volLen > 0)) { // consume leading slashes on windows if (r < n and options.isSeparator(path[r])) { r += 1; buf[buf_i] = options.separator; buf_i += 1; // win32.resolve("C:\\Users\\bun", "C:\\Users\\bun", "/..\\bar") // should be "C:\\bar" not "C:bar" dotdot = buf_i; } } while (r < n) { // empty path element // or // . element if (options.isSeparator(path[r])) { r += 1; continue; } if (path[r] == '.' and (r + 1 == n or options.isSeparator(path[r + 1]))) { // skipping two is a windows-specific bugfix r += 1; continue; } if (@"is .. with type"(T, path[r..]) and (r + 2 == n or options.isSeparator(path[r + 2]))) { r += 2; // .. element: remove to last separator if (buf_i > dotdot) { buf_i -= 1; while (buf_i > dotdot and !options.isSeparator(buf[buf_i])) { buf_i -= 1; } } else if (options.allow_above_root) { if (buf_i > buf_start) { buf[buf_i..][0..3].* = (strings.literal(T, sep_str ++ "..")).*; buf_i += 3; } else { buf[buf_i..][0..2].* = (strings.literal(T, "..")).*; buf_i += 2; } dotdot = buf_i; } continue; } // real path element. // add slash if needed if (buf_i != buf_start and buf_i > 0 and !options.isSeparator(buf[buf_i - 1])) { buf[buf_i] = options.separator; buf_i += 1; } const from = r; while (r < n and !options.isSeparator(path[r])) : (r += 1) {} const count = r - from; @memcpy(buf[buf_i..][0..count], path[from..][0..count]); buf_i += count; } if (options.preserve_trailing_slash) { // Was there a trailing slash? Let's keep it. if (buf_i > 0 and path_[path_.len - 1] == options.separator and buf[buf_i - 1] != options.separator) { buf[buf_i] = options.separator; buf_i += 1; } } if (isWindows and buf_i == 2 and buf[1] == ':') { // If the original path is just a relative path with a drive letter, // add . buf[buf_i] = if (path.len > 0 and path[0] == '\\') '\\' else '.'; buf_i += 1; } if (options.zero_terminate) { buf[buf_i] = 0; } const result = if (options.zero_terminate) buf[0..buf_i :0] else buf[0..buf_i]; if (bun.Environment.allow_assert and isWindows) { assert(!strings.hasPrefixComptimeType(T, result, strings.literal(T, "\\:\\"))); } return result; } pub const Platform = enum { loose, windows, posix, nt, pub const auto: Platform = switch (bun.Environment.os) { .windows => .windows, .linux, .mac => .posix, .wasm => .loose, }; pub fn isAbsolute(comptime platform: Platform, path: []const u8) bool { return isAbsoluteT(platform, u8, path); } pub fn isAbsoluteT(comptime platform: Platform, comptime T: type, path: []const T) bool { if (T != u8 and T != u16) @compileError("Unsupported type given to isAbsoluteT"); return switch (platform) { .posix => path.len > 0 and path[0] == '/', .nt, .windows, .loose, => if (T == u8) std.fs.path.isAbsoluteWindows(path) else std.fs.path.isAbsoluteWindowsWTF16(path), }; } pub inline fn separator(comptime platform: Platform) u8 { return switch (platform) { .loose, .posix => std.fs.path.sep_posix, .nt, .windows => std.fs.path.sep_windows, }; } pub inline fn separatorString(comptime platform: Platform) []const u8 { return switch (platform) { .loose, .posix => std.fs.path.sep_str_posix, .nt, .windows => std.fs.path.sep_str_windows, }; } pub fn getSeparatorFunc(comptime platform: Platform) IsSeparatorFunc { return switch (platform) { .loose => isSepAny, .nt, .windows => isSepAny, .posix => isSepPosix, }; } pub fn getSeparatorFuncT(comptime platform: Platform) IsSeparatorFuncT { return switch (platform) { .loose => isSepAnyT, .nt, .windows => isSepAnyT, .posix => isSepPosixT, }; } pub fn getLastSeparatorFunc(comptime platform: Platform) LastSeparatorFunction { return switch (platform) { .loose => lastIndexOfSeparatorLoose, .nt, .windows => lastIndexOfSeparatorWindows, .posix => lastIndexOfSeparatorPosix, }; } pub fn getLastSeparatorFuncT(comptime platform: Platform) LastSeparatorFunctionT { return switch (platform) { .loose => lastIndexOfSeparatorLooseT, .nt, .windows => lastIndexOfSeparatorWindowsT, .posix => lastIndexOfSeparatorPosixT, }; } pub inline fn isSeparator(comptime platform: Platform, char: u8) bool { return isSeparatorT(platform, u8, char); } pub inline fn isSeparatorT(comptime platform: Platform, comptime T: type, char: T) bool { return switch (platform) { .loose => isSepAnyT(T, char), .nt, .windows => isSepAnyT(T, char), .posix => isSepPosixT(T, char), }; } pub fn trailingSeparator(comptime platform: Platform) [2]u8 { return switch (platform) { .nt, .windows => ".\\".*, .posix, .loose => "./".*, }; } pub fn leadingSeparatorIndex(comptime platform: Platform, path: anytype) ?usize { switch (platform) { .nt, .windows => { if (path.len < 1) return null; if (path[0] == '/') return 0; if (path[0] == '\\') return 0; if (path.len < 3) return null; // C:\ // C:/ if (path[0] >= 'A' and path[0] <= 'Z' and path[1] == ':') { if (path[2] == '/') return 2; if (path[2] == '\\') return 2; return 1; } return null; }, .posix => { if (path.len > 0 and path[0] == '/') { return 0; } else { return null; } }, .loose => return leadingSeparatorIndex(.windows, path) orelse leadingSeparatorIndex(.posix, path), } } }; pub fn normalizeString(str: []const u8, comptime allow_above_root: bool, comptime platform: Platform) []u8 { return normalizeStringBuf(str, &parser_buffer, allow_above_root, platform, false); } pub fn normalizeStringZ(str: []const u8, comptime allow_above_root: bool, comptime platform: Platform) [:0]u8 { const normalized = normalizeStringBuf(str, &parser_buffer, allow_above_root, platform, false); parser_buffer[normalized.len] = 0; return parser_buffer[0..normalized.len :0]; } pub fn normalizeBuf(str: []const u8, buf: []u8, comptime platform: Platform) []u8 { return normalizeBufT(u8, str, buf, platform); } pub fn normalizeBufZ(str: []const u8, buf: []u8, comptime platform: Platform) [:0]u8 { const norm = normalizeBufT(u8, str, buf, platform); buf[norm.len] = 0; return buf[0..norm.len :0]; } pub fn normalizeBufT(comptime T: type, str: []const T, buf: []T, comptime platform: Platform) []T { if (str.len == 0) { buf[0] = '.'; return buf[0..1]; } const is_absolute = platform.isAbsoluteT(T, str); const trailing_separator = platform.getLastSeparatorFuncT()(T, str) == str.len - 1; if (is_absolute and trailing_separator) return normalizeStringBufT(T, str, buf, true, platform, true); if (is_absolute and !trailing_separator) return normalizeStringBufT(T, str, buf, true, platform, false); if (!is_absolute and !trailing_separator) return normalizeStringBufT(T, str, buf, false, platform, false); return normalizeStringBufT(T, str, buf, false, platform, true); } pub fn normalizeStringBuf( str: []const u8, buf: []u8, comptime allow_above_root: bool, comptime platform: Platform, comptime preserve_trailing_slash: bool, ) []u8 { return normalizeStringBufT(u8, str, buf, allow_above_root, platform, preserve_trailing_slash); } pub fn normalizeStringBufT( comptime T: type, str: []const T, buf: []T, comptime allow_above_root: bool, comptime platform: Platform, comptime preserve_trailing_slash: bool, ) []T { switch (platform) { .nt => @compileError("not implemented"), .windows => { return normalizeStringWindowsT( T, str, buf, allow_above_root, preserve_trailing_slash, ); }, .posix => { return normalizeStringLooseBufT( T, str, buf, allow_above_root, preserve_trailing_slash, ); }, .loose => { return normalizeStringLooseBufT( T, str, buf, allow_above_root, preserve_trailing_slash, ); }, } } pub fn normalizeStringAlloc(allocator: std.mem.Allocator, str: []const u8, comptime allow_above_root: bool, comptime platform: Platform) ![]const u8 { return try allocator.dupe(u8, normalizeString(str, allow_above_root, platform)); } pub fn joinAbs2(_cwd: []const u8, comptime platform: Platform, part: anytype, part2: anytype) []const u8 { const parts = [_][]const u8{ part, part2 }; const slice = joinAbsString(_cwd, &parts, platform); return slice; } pub fn joinAbs(cwd: []const u8, comptime platform: Platform, part: []const u8) []const u8 { return joinAbsString(cwd, &.{part}, platform); } /// Convert parts of potentially invalid file paths into a single valid filpeath /// without querying the filesystem /// This is the equivalent of path.resolve /// /// Returned path is stored in a temporary buffer. It must be copied if it needs to be stored. pub fn joinAbsString(_cwd: []const u8, parts: anytype, comptime platform: Platform) []const u8 { return joinAbsStringBuf( _cwd, &parser_join_input_buffer, parts, platform, ); } /// Convert parts of potentially invalid file paths into a single valid filpeath /// without querying the filesystem /// This is the equivalent of path.resolve /// /// Returned path is stored in a temporary buffer. It must be copied if it needs to be stored. pub fn joinAbsStringZ(_cwd: []const u8, parts: anytype, comptime platform: Platform) [:0]const u8 { return joinAbsStringBufZ( _cwd, &parser_join_input_buffer, parts, platform, ); } pub threadlocal var join_buf: [4096]u8 = undefined; pub fn join(_parts: anytype, comptime platform: Platform) []const u8 { return joinStringBuf(&join_buf, _parts, platform); } pub fn joinZ(_parts: anytype, comptime platform: Platform) [:0]const u8 { return joinZBuf(&join_buf, _parts, platform); } pub fn joinZBuf(buf: []u8, _parts: anytype, comptime platform: Platform) [:0]const u8 { const joined = joinStringBuf(buf[0 .. buf.len - 1], _parts, platform); assert(bun.isSliceInBuffer(joined, buf)); const start_offset = @intFromPtr(joined.ptr) - @intFromPtr(buf.ptr); buf[joined.len + start_offset] = 0; return buf[start_offset..][0..joined.len :0]; } pub fn joinStringBuf(buf: []u8, parts: anytype, comptime platform: Platform) []const u8 { return joinStringBufT(u8, buf, parts, platform); } pub fn joinStringBufW(buf: []u16, parts: anytype, comptime platform: Platform) []const u16 { return joinStringBufT(u16, buf, parts, platform); } pub fn joinStringBufWZ(buf: []u16, parts: anytype, comptime platform: Platform) [:0]const u16 { const joined = joinStringBufT(u16, buf[0 .. buf.len - 1], parts, platform); assert(bun.isSliceInBufferT(u16, joined, buf)); const start_offset = @intFromPtr(joined.ptr) / 2 - @intFromPtr(buf.ptr) / 2; buf[joined.len + start_offset] = 0; return buf[start_offset..][0..joined.len :0]; } pub fn joinStringBufZ(buf: []u8, parts: anytype, comptime platform: Platform) [:0]const u8 { const joined = joinStringBufT(u8, buf[0 .. buf.len - 1], parts, platform); assert(bun.isSliceInBufferT(u8, joined, buf)); const start_offset = @intFromPtr(joined.ptr) - @intFromPtr(buf.ptr); buf[joined.len + start_offset] = 0; return buf[start_offset..][0..joined.len :0]; } pub fn joinStringBufT(comptime T: type, buf: []T, parts: anytype, comptime platform: Platform) []const T { var written: usize = 0; var temp_buf_: [4096]T = undefined; var temp_buf: []T = &temp_buf_; var free_temp_buf = false; defer { if (free_temp_buf) { bun.default_allocator.free(temp_buf); } } var count: usize = 0; for (parts) |part| { if (part.len == 0) continue; count += part.len + 1; } if (count * 2 > temp_buf.len) { temp_buf = bun.handleOom(bun.default_allocator.alloc(T, count * 2)); free_temp_buf = true; } temp_buf[0] = 0; for (parts) |part| { if (part.len == 0) continue; if (written > 0) { temp_buf[written] = platform.separator(); written += 1; } const Element = std.meta.Elem(@TypeOf(part)); if (comptime T == u16 and Element == u8) { const wrote = bun.strings.convertUTF8toUTF16InBuffer(temp_buf[written..], part); written += wrote.len; } else { bun.copy(T, temp_buf[written..], part); written += part.len; } } if (written == 0) { buf[0] = '.'; return buf[0..1]; } return normalizeStringNodeT(T, temp_buf[0..written], buf, platform); } pub fn joinAbsStringBuf(cwd: []const u8, buf: []u8, _parts: anytype, comptime platform: Platform) []const u8 { return _joinAbsStringBuf(false, []const u8, cwd, buf, _parts, platform); } pub fn joinAbsStringBufZ(cwd: []const u8, buf: []u8, _parts: anytype, comptime platform: Platform) [:0]const u8 { return _joinAbsStringBuf(true, [:0]const u8, cwd, buf, _parts, platform); } pub fn joinAbsStringBufZNT(cwd: []const u8, buf: []u8, _parts: anytype, comptime platform: Platform) [:0]const u8 { if ((platform == .auto or platform == .loose or platform == .windows) and bun.Environment.isWindows) { return _joinAbsStringBuf(true, [:0]const u8, cwd, buf, _parts, .nt); } return _joinAbsStringBuf(true, [:0]const u8, cwd, buf, _parts, platform); } pub fn joinAbsStringBufZTrailingSlash(cwd: []const u8, buf: []u8, _parts: anytype, comptime platform: Platform) [:0]const u8 { const out = _joinAbsStringBuf(true, [:0]const u8, cwd, buf, _parts, platform); if (out.len + 2 < buf.len and out.len > 0 and out[out.len - 1] != platform.separator()) { buf[out.len] = platform.separator(); buf[out.len + 1] = 0; return buf[0 .. out.len + 1 :0]; } return out; } fn _joinAbsStringBuf(comptime is_sentinel: bool, comptime ReturnType: type, _cwd: []const u8, buf: []u8, _parts: anytype, comptime platform: Platform) ReturnType { if (platform == .windows or (bun.Environment.os == .windows and platform == .loose)) { return _joinAbsStringBufWindows(is_sentinel, ReturnType, _cwd, buf, _parts); } if (platform == .nt) { const end_path = _joinAbsStringBufWindows(is_sentinel, ReturnType, _cwd, buf[4..], _parts); buf[0..4].* = "\\\\?\\".*; if (comptime is_sentinel) { buf[end_path.len + 4] = 0; return buf[0 .. end_path.len + 4 :0]; } return buf[0 .. end_path.len + 4]; } var parts: []const []const u8 = _parts; var temp_buf: [bun.MAX_PATH_BYTES * 2]u8 = undefined; if (parts.len == 0) { if (comptime is_sentinel) { unreachable; } return _cwd; } if ((comptime platform == .loose or platform == .posix) and parts.len == 1 and parts[0].len == 1 and parts[0][0] == std.fs.path.sep_posix) { return "/"; } var out: usize = 0; var cwd = if (bun.Environment.isWindows and _cwd.len >= 3 and _cwd[1] == ':') _cwd[2..] else _cwd; { var part_i: u16 = 0; var part_len: u16 = @as(u16, @truncate(parts.len)); while (part_i < part_len) { if (platform.isAbsolute(parts[part_i])) { cwd = parts[part_i]; parts = parts[part_i + 1 ..]; part_len = @as(u16, @truncate(parts.len)); part_i = 0; continue; } part_i += 1; } } bun.copy(u8, &temp_buf, cwd); out = cwd.len; for (parts) |_part| { if (_part.len == 0) { continue; } const part = _part; if (out > 0 and temp_buf[out - 1] != platform.separator()) { temp_buf[out] = platform.separator(); out += 1; } bun.copy(u8, temp_buf[out..], part); out += part.len; } const leading_separator: []const u8 = if (platform.leadingSeparatorIndex(temp_buf[0..out])) |i| brk: { const outdir = temp_buf[0 .. i + 1]; if (platform == .loose) { for (outdir) |*c| { if (c.* == '\\') { c.* = '/'; } } } break :brk outdir; } else "/"; const result = normalizeStringBuf( temp_buf[leading_separator.len..out], buf[leading_separator.len..], false, platform, true, ); bun.copy(u8, buf, leading_separator); if (comptime is_sentinel) { buf.ptr[result.len + leading_separator.len] = 0; return buf[0 .. result.len + leading_separator.len :0]; } else { return buf[0 .. result.len + leading_separator.len]; } } fn _joinAbsStringBufWindows( comptime is_sentinel: bool, comptime ReturnType: type, cwd: []const u8, buf: []u8, parts: []const []const u8, ) ReturnType { assert(std.fs.path.isAbsoluteWindows(cwd)); if (parts.len == 0) { if (comptime is_sentinel) { unreachable; } return cwd; } // path.resolve is a bit different on Windows, as there are multiple possible filesystem roots. // When you resolve(`C:\hello`, `C:world`), the second arg is a drive letter relative path, so // the result of such is `C:\hello\world`, but if you used D:world, you would switch roots and // end up with `D:\world`. this root handling basically means a different algorithm. // // to complicate things, it seems node.js will first figure out what the last root is, then // in a separate search, figure out the last absolute path. // // Given the case `resolve("/one", "D:two", "three", "F:four", "five")` // Root is "F:", cwd is "/one", then join all paths that dont exist on other drives. // // Also, the special root "/" can match into anything, but we have to resolve it to a real // root at some point. That is what the `root_of_part.len == 0` check is doing. const root, const set_cwd, const n_start = base: { const root = root: { var n = parts.len; while (n > 0) { n -= 1; const len = windowsVolumeNameLen(parts[n])[0]; if (len > 0) { break :root parts[n][0..len]; } } // use cwd const len = windowsVolumeNameLen(cwd)[0]; break :root cwd[0..len]; }; var n = parts.len; while (n > 0) { n -= 1; if (std.fs.path.isAbsoluteWindows(parts[n])) { const root_of_part = parts[n][0..windowsVolumeNameLen(parts[n])[0]]; if (root_of_part.len == 0 or strings.eql(root_of_part, root)) { break :base .{ root, parts[n][root_of_part.len..], n + 1 }; } } } // use cwd only if the root matches const cwd_root = cwd[0..windowsVolumeNameLen(cwd)[0]]; if (strings.eql(cwd_root, root)) { break :base .{ root, cwd[cwd_root.len..], 0 }; } else { break :base .{ root, "/", 0 }; } }; if (set_cwd.len > 0) assert(isSepAny(set_cwd[0])); var temp_buf: [bun.MAX_PATH_BYTES * 2]u8 = undefined; @memcpy(temp_buf[0..root.len], root); @memcpy(temp_buf[root.len .. root.len + set_cwd.len], set_cwd); var out: usize = root.len + set_cwd.len; if (set_cwd.len == 0) { // when cwd is `//server/share` without a suffix `/`, the path is considered absolute temp_buf[out] = '\\'; out += 1; } for (parts[n_start..]) |part| { if (part.len == 0) continue; if (out > 0 and temp_buf[out - 1] != '\\') { temp_buf[out] = '\\'; out += 1; } // skip over volume name const volume = part[0..windowsVolumeNameLen(part)[0]]; if (volume.len > 0 and !strings.eqlLong(volume, root, true)) continue; const part_without_vol = part[volume.len..]; @memcpy(temp_buf[out .. out + part_without_vol.len], part_without_vol); out += part_without_vol.len; } // if (out > 0 and temp_buf[out - 1] != '\\') { // temp_buf[out] = '\\'; // out += 1; // } const result = normalizeStringBuf( temp_buf[0..out], buf, false, .windows, true, ); if (comptime is_sentinel) { buf.ptr[result.len] = 0; return buf[0..result.len :0]; } else { return buf[0..result.len]; } } pub fn isSepPosix(char: u8) bool { return isSepPosixT(u8, char); } pub fn isSepPosixT(comptime T: type, char: T) bool { return char == std.fs.path.sep_posix; } pub fn isSepWin32(char: u8) bool { return isSepWin32T(u8, char); } pub fn isSepWin32T(comptime T: type, char: T) bool { return char == std.fs.path.sep_windows; } pub fn isSepAny(char: u8) bool { return isSepAnyT(u8, char); } pub fn isSepAnyT(comptime T: type, char: T) bool { return @call(bun.callmod_inline, isSepPosixT, .{ T, char }) or @call(bun.callmod_inline, isSepWin32T, .{ T, char }); } pub fn lastIndexOfSeparatorWindows(slice: []const u8) ?usize { return lastIndexOfSeparatorWindowsT(u8, slice); } pub fn lastIndexOfSeparatorWindowsT(comptime T: type, slice: []const T) ?usize { return std.mem.lastIndexOfAny(T, slice, strings.literal(T, "\\/")); } pub fn lastIndexOfSeparatorPosix(slice: []const u8) ?usize { return lastIndexOfSeparatorPosixT(u8, slice); } pub fn lastIndexOfSeparatorPosixT(comptime T: type, slice: []const T) ?usize { return std.mem.lastIndexOfScalar(T, slice, std.fs.path.sep_posix); } pub fn lastIndexOfNonSeparatorPosix(slice: []const u8) ?u32 { var i: usize = slice.len; while (i != 0) : (i -= 1) { if (slice[i] != std.fs.path.sep_posix) { return @as(u32, @intCast(i)); } } return null; } pub fn lastIndexOfSeparatorLoose(slice: []const u8) ?usize { return lastIndexOfSeparatorLooseT(u8, slice); } pub fn lastIndexOfSeparatorLooseT(comptime T: type, slice: []const T) ?usize { return lastIndexOfSepT(T, slice); } pub fn normalizeStringLooseBuf( str: []const u8, buf: []u8, comptime allow_above_root: bool, comptime preserve_trailing_slash: bool, ) []u8 { return normalizeStringLooseBufT(u8, str, buf, allow_above_root, preserve_trailing_slash); } pub fn normalizeStringLooseBufT( comptime T: type, str: []const T, buf: []T, comptime allow_above_root: bool, comptime preserve_trailing_slash: bool, ) []T { return normalizeStringGenericT( T, str, buf, allow_above_root, std.fs.path.sep_posix, isSepAnyT, preserve_trailing_slash, ); } pub fn normalizeStringWindows( str: []const u8, buf: []u8, comptime allow_above_root: bool, comptime preserve_trailing_slash: bool, ) []u8 { return normalizeStringWindowsT(u8, str, buf, allow_above_root, preserve_trailing_slash); } pub fn normalizeStringWindowsT( comptime T: type, str: []const T, buf: []T, comptime allow_above_root: bool, comptime preserve_trailing_slash: bool, ) []T { return normalizeStringGenericT( T, str, buf, allow_above_root, std.fs.path.sep_windows, isSepAnyT, preserve_trailing_slash, ); } pub fn normalizeStringNode( str: []const u8, buf: []u8, comptime platform: Platform, ) []u8 { return normalizeStringNodeT(u8, str, buf, platform); } pub fn normalizeStringNodeT( comptime T: type, str: []const T, buf: []T, comptime platform: Platform, ) []const T { if (str.len == 0) { buf[0] = '.'; return buf[0..1]; } const is_absolute = platform.isAbsoluteT(T, str); const trailing_separator = platform.isSeparatorT(T, str[str.len - 1]); // `normalizeStringGeneric` handles absolute path cases for windows // we should not prefix with / var buf_ = if (platform == .windows) buf else buf[1..]; var out = if (!is_absolute) normalizeStringGenericT( T, str, buf_, true, comptime platform.separator(), comptime platform.getSeparatorFuncT(), false, ) else normalizeStringGenericT( T, str, buf_, false, comptime platform.separator(), comptime platform.getSeparatorFuncT(), false, ); if (out.len == 0) { if (is_absolute) { buf[0] = platform.separator(); return buf[0..1]; } if (trailing_separator) { const sep = platform.trailingSeparator(); buf[0..2].* = .{ sep[0], sep[1] }; return buf[0..2]; } buf[0] = '.'; return buf[0..1]; } if (trailing_separator) { if (!platform.isSeparatorT(T, out[out.len - 1])) { buf_[out.len] = platform.separator(); out = buf_[0 .. out.len + 1]; } } if (is_absolute) { if (platform == .windows) { return out; } buf[0] = platform.separator(); out = buf[0 .. out.len + 1]; } return out; } pub fn basename(path: []const u8) []const u8 { if (path.len == 0) return &[_]u8{}; var end_index: usize = path.len - 1; while (isSepAny(path[end_index])) { if (end_index == 0) return "/"; end_index -= 1; } var start_index: usize = end_index; end_index += 1; while (!isSepAny(path[start_index])) { if (start_index == 0) return path[0..end_index]; start_index -= 1; } return path[start_index + 1 .. end_index]; } pub fn lastIndexOfSep(path: []const u8) ?usize { return lastIndexOfSepT(u8, path); } pub fn lastIndexOfSepT(comptime T: type, path: []const T) ?usize { if (comptime !bun.Environment.isWindows) { return strings.lastIndexOfCharT(T, path, '/'); } return std.mem.lastIndexOfAny(T, path, "/\\"); } pub fn nextDirname(path_: []const u8) ?[]const u8 { var path = path_; var root_prefix: []const u8 = ""; if (path.len > 3) { // disk designator if (path[1] == ':' and isSepAny(path[2])) { root_prefix = path[0..3]; } // TODO: unc path } if (path.len == 0) return if (root_prefix.len > 0) root_prefix else null; var end_index: usize = path.len - 1; while (isSepAny(path[end_index])) { if (end_index == 0) return if (root_prefix.len > 0) root_prefix else null; end_index -= 1; } while (!isSepAny(path[end_index])) { if (end_index == 0) return if (root_prefix.len > 0) root_prefix else null; end_index -= 1; } if (end_index == 0 and isSepAny(path[0])) return path[0..1]; if (end_index == 0) return if (root_prefix.len > 0) root_prefix else null; return path[0 .. end_index + 1]; } /// The use case of this is when you do /// "import '/hello/world'" /// The windows disk designator is missing! /// /// Defaulting to C would work but the correct behavior is to use a known disk designator, /// via an absolute path from the referrer or what not. /// /// I've made it so that trying to read a file with a posix path is a debug assertion failure. /// /// To use this, stack allocate the following struct, and then call `resolve`. /// /// var normalizer = PosixToWinNormalizer{}; /// const result = normalizer.resolve("C:\\dev\\bun", "/dev/bun/test/etc.js"); /// /// When you are certain that using the current working directory is fine, you can use /// /// const result = normalizer.resolveCWD("/dev/bun/test/etc.js"); /// /// This API does nothing on Linux (it has a size of zero) pub const PosixToWinNormalizer = struct { const Buf = if (bun.Environment.isWindows) bun.PathBuffer else void; _raw_bytes: Buf = undefined, // methods on PosixToWinNormalizer, to be minimal yet stack allocate the PathBuffer // these do not force inline of much code pub inline fn resolve( this: *PosixToWinNormalizer, source_dir: []const u8, maybe_posix_path: []const u8, ) []const u8 { return resolveWithExternalBuf(&this._raw_bytes, source_dir, maybe_posix_path); } pub inline fn resolveZ( this: *PosixToWinNormalizer, source_dir: []const u8, maybe_posix_path: [:0]const u8, ) [:0]const u8 { return resolveWithExternalBufZ(&this._raw_bytes, source_dir, maybe_posix_path); } pub inline fn resolveCWD( this: *PosixToWinNormalizer, maybe_posix_path: []const u8, ) ![]const u8 { return resolveCWDWithExternalBuf(&this._raw_bytes, maybe_posix_path); } pub inline fn resolveCWDZ( this: *PosixToWinNormalizer, maybe_posix_path: []const u8, ) ![:0]const u8 { return resolveCWDWithExternalBufZ(&this._raw_bytes, maybe_posix_path); } // underlying implementation: fn resolveWithExternalBuf( buf: *Buf, source_dir: []const u8, maybe_posix_path: []const u8, ) []const u8 { assert(std.fs.path.isAbsoluteWindows(maybe_posix_path)); if (bun.Environment.isWindows) { const root = windowsFilesystemRoot(maybe_posix_path); if (root.len == 1) { assert(isSepAny(root[0])); if (bun.strings.isWindowsAbsolutePathMissingDriveLetter(u8, maybe_posix_path)) { const source_root = windowsFilesystemRoot(source_dir); @memcpy(buf[0..source_root.len], source_root); @memcpy(buf[source_root.len..][0 .. maybe_posix_path.len - 1], maybe_posix_path[1..]); const res = buf[0 .. source_root.len + maybe_posix_path.len - 1]; assert(!bun.strings.isWindowsAbsolutePathMissingDriveLetter(u8, res)); assert(std.fs.path.isAbsoluteWindows(res)); return res; } } assert(!bun.strings.isWindowsAbsolutePathMissingDriveLetter(u8, maybe_posix_path)); } return maybe_posix_path; } fn resolveWithExternalBufZ( buf: *Buf, source_dir: []const u8, maybe_posix_path: [:0]const u8, ) [:0]const u8 { assert(std.fs.path.isAbsoluteWindows(maybe_posix_path)); if (bun.Environment.isWindows) { const root = windowsFilesystemRoot(maybe_posix_path); if (root.len == 1) { assert(isSepAny(root[0])); if (bun.strings.isWindowsAbsolutePathMissingDriveLetter(u8, maybe_posix_path)) { const source_root = windowsFilesystemRoot(source_dir); @memcpy(buf[0..source_root.len], source_root); @memcpy(buf[source_root.len..][0 .. maybe_posix_path.len - 1], maybe_posix_path[1..]); buf[source_root.len + maybe_posix_path.len - 1] = 0; const res = buf[0 .. source_root.len + maybe_posix_path.len - 1 :0]; assert(!bun.strings.isWindowsAbsolutePathMissingDriveLetter(u8, res)); assert(std.fs.path.isAbsoluteWindows(res)); return res; } } assert(!bun.strings.isWindowsAbsolutePathMissingDriveLetter(u8, maybe_posix_path)); } return maybe_posix_path; } pub fn resolveCWDWithExternalBuf( buf: *Buf, maybe_posix_path: []const u8, ) ![]const u8 { assert(std.fs.path.isAbsoluteWindows(maybe_posix_path)); if (bun.Environment.isWindows) { const root = windowsFilesystemRoot(maybe_posix_path); if (root.len == 1) { assert(isSepAny(root[0])); if (bun.strings.isWindowsAbsolutePathMissingDriveLetter(u8, maybe_posix_path)) { const cwd = try std.posix.getcwd(buf); assert(cwd.ptr == buf.ptr); const source_root = windowsFilesystemRoot(cwd); assert(source_root.ptr == source_root.ptr); @memcpy(buf[source_root.len..][0 .. maybe_posix_path.len - 1], maybe_posix_path[1..]); const res = buf[0 .. source_root.len + maybe_posix_path.len - 1]; assert(!bun.strings.isWindowsAbsolutePathMissingDriveLetter(u8, res)); assert(std.fs.path.isAbsoluteWindows(res)); return res; } } assert(!bun.strings.isWindowsAbsolutePathMissingDriveLetter(u8, maybe_posix_path)); } return maybe_posix_path; } pub fn resolveCWDWithExternalBufZ( buf: *bun.PathBuffer, maybe_posix_path: []const u8, ) ![:0]u8 { assert(std.fs.path.isAbsoluteWindows(maybe_posix_path)); if (bun.Environment.isWindows) { const root = windowsFilesystemRoot(maybe_posix_path); if (root.len == 1) { assert(isSepAny(root[0])); if (bun.strings.isWindowsAbsolutePathMissingDriveLetter(u8, maybe_posix_path)) { const cwd = try std.posix.getcwd(buf); assert(cwd.ptr == buf.ptr); const source_root = windowsFilesystemRoot(cwd); assert(source_root.ptr == source_root.ptr); @memcpy(buf[source_root.len..][0 .. maybe_posix_path.len - 1], maybe_posix_path[1..]); buf[source_root.len + maybe_posix_path.len - 1] = 0; const res = buf[0 .. source_root.len + maybe_posix_path.len - 1 :0]; assert(!bun.strings.isWindowsAbsolutePathMissingDriveLetter(u8, res)); assert(std.fs.path.isAbsoluteWindows(res)); return res; } } assert(!bun.strings.isWindowsAbsolutePathMissingDriveLetter(u8, maybe_posix_path)); } @memcpy(buf.ptr, maybe_posix_path); buf[maybe_posix_path.len] = 0; return buf[0..maybe_posix_path.len :0]; } }; /// Used in PathInlines.h /// gets cwd off of the global object export fn ResolvePath__joinAbsStringBufCurrentPlatformBunString( globalObject: *bun.jsc.JSGlobalObject, in: bun.String, ) bun.String { const str = in.toUTF8WithoutRef(bun.default_allocator); defer str.deinit(); const out_slice = joinAbsStringBuf( globalObject.bunVM().transpiler.fs.top_level_dir, &join_buf, &.{str.slice()}, .auto, ); return bun.String.cloneUTF8(out_slice); } pub fn platformToPosixInPlace(comptime T: type, path_buffer: []T) void { if (std.fs.path.sep == '/') return; var idx: usize = 0; while (std.mem.indexOfScalarPos(T, path_buffer, idx, std.fs.path.sep)) |index| : (idx = index + 1) { path_buffer[index] = '/'; } } pub fn dangerouslyConvertPathToPosixInPlace(comptime T: type, path: []T) void { var idx: usize = 0; if (comptime bun.Environment.isWindows) { if (path.len > "C:".len and isDriveLetter(path[0]) and path[1] == ':' and isSepAny(path[2])) { // Uppercase drive letter switch (path[0]) { 'a'...'z' => path[0] = 'A' + (path[0] - 'a'), 'A'...'Z' => {}, else => unreachable, } } } while (std.mem.indexOfScalarPos(T, path, idx, std.fs.path.sep_windows)) |index| : (idx = index + 1) { path[index] = '/'; } } pub fn dangerouslyConvertPathToWindowsInPlace(comptime T: type, path: []T) void { var idx: usize = 0; while (std.mem.indexOfScalarPos(T, path, idx, std.fs.path.sep_posix)) |index| : (idx = index + 1) { path[index] = '\\'; } } pub fn pathToPosixBuf(comptime T: type, path: []const T, buf: []T) []T { var idx: usize = 0; while (std.mem.indexOfScalarPos(T, path, idx, std.fs.path.sep_windows)) |index| : (idx = index + 1) { @memcpy(buf[idx..index], path[idx..index]); buf[index] = std.fs.path.sep_posix; } @memcpy(buf[idx..path.len], path[idx..path.len]); return buf[0..path.len]; } pub fn platformToPosixBuf(comptime T: type, path: []const T, buf: []T) []const T { if (std.fs.path.sep == '/') return path; var idx: usize = 0; while (std.mem.indexOfScalarPos(T, path, idx, std.fs.path.sep)) |index| : (idx = index + 1) { @memcpy(buf[idx..index], path[idx..index]); buf[index] = '/'; } @memcpy(buf[idx..path.len], path[idx..path.len]); return buf[0..path.len]; } pub fn posixToPlatformInPlace(comptime T: type, path_buffer: []T) void { if (std.fs.path.sep == '/') return; var idx: usize = 0; while (std.mem.indexOfScalarPos(T, path_buffer, idx, '/')) |index| : (idx = index + 1) { path_buffer[index] = std.fs.path.sep; } } const Fs = @import("../fs.zig"); const std = @import("std"); const bun = @import("bun"); const assert = bun.assert; const strings = bun.strings;