master
   1//! POSIX paths are arbitrary sequences of `u8` with no particular encoding.
   2//!
   3//! Windows paths are arbitrary sequences of `u16` (WTF-16).
   4//! For cross-platform APIs that deal with sequences of `u8`, Windows
   5//! paths are encoded by Zig as [WTF-8](https://wtf-8.codeberg.page/).
   6//! WTF-8 is a superset of UTF-8 that allows encoding surrogate codepoints,
   7//! which enables lossless roundtripping when converting to/from WTF-16
   8//! (as long as the WTF-8 encoded surrogate codepoints do not form a pair).
   9//!
  10//! WASI paths are sequences of valid Unicode scalar values,
  11//! which means that WASI is unable to handle paths that cannot be
  12//! encoded as well-formed UTF-8/UTF-16.
  13//! https://github.com/WebAssembly/wasi-filesystem/issues/17#issuecomment-1430639353
  14
  15const builtin = @import("builtin");
  16const std = @import("../std.zig");
  17const debug = std.debug;
  18const assert = debug.assert;
  19const testing = std.testing;
  20const mem = std.mem;
  21const ascii = std.ascii;
  22const Allocator = mem.Allocator;
  23const windows = std.os.windows;
  24const process = std.process;
  25const native_os = builtin.target.os.tag;
  26
  27pub const sep_windows: u8 = '\\';
  28pub const sep_posix: u8 = '/';
  29pub const sep = switch (native_os) {
  30    .windows, .uefi => sep_windows,
  31    else => sep_posix,
  32};
  33
  34pub const sep_str_windows = "\\";
  35pub const sep_str_posix = "/";
  36pub const sep_str = switch (native_os) {
  37    .windows, .uefi => sep_str_windows,
  38    else => sep_str_posix,
  39};
  40
  41pub const delimiter_windows: u8 = ';';
  42pub const delimiter_posix: u8 = ':';
  43pub const delimiter = if (native_os == .windows) delimiter_windows else delimiter_posix;
  44
  45/// Returns if the given byte is a valid path separator
  46pub fn isSep(byte: u8) bool {
  47    return switch (native_os) {
  48        .windows => byte == '/' or byte == '\\',
  49        .uefi => byte == '\\',
  50        else => byte == '/',
  51    };
  52}
  53
  54pub const PathType = enum {
  55    windows,
  56    uefi,
  57    posix,
  58
  59    /// Returns true if `c` is a valid path separator for the `path_type`.
  60    /// If `T` is `u16`, `c` is assumed to be little-endian.
  61    pub inline fn isSep(comptime path_type: PathType, comptime T: type, c: T) bool {
  62        return switch (path_type) {
  63            .windows => c == mem.nativeToLittle(T, '/') or c == mem.nativeToLittle(T, '\\'),
  64            .posix => c == mem.nativeToLittle(T, '/'),
  65            .uefi => c == mem.nativeToLittle(T, '\\'),
  66        };
  67    }
  68};
  69
  70/// This is different from mem.join in that the separator will not be repeated if
  71/// it is found at the end or beginning of a pair of consecutive paths.
  72fn joinSepMaybeZ(allocator: Allocator, separator: u8, comptime sepPredicate: fn (u8) bool, paths: []const []const u8, zero: bool) ![]u8 {
  73    if (paths.len == 0) return if (zero) try allocator.dupe(u8, &[1]u8{0}) else &[0]u8{};
  74
  75    // Find first non-empty path index.
  76    const first_path_index = blk: {
  77        for (paths, 0..) |path, index| {
  78            if (path.len == 0) continue else break :blk index;
  79        }
  80
  81        // All paths provided were empty, so return early.
  82        return if (zero) try allocator.dupe(u8, &[1]u8{0}) else &[0]u8{};
  83    };
  84
  85    // Calculate length needed for resulting joined path buffer.
  86    const total_len = blk: {
  87        var sum: usize = paths[first_path_index].len;
  88        var prev_path = paths[first_path_index];
  89        assert(prev_path.len > 0);
  90        var i: usize = first_path_index + 1;
  91        while (i < paths.len) : (i += 1) {
  92            const this_path = paths[i];
  93            if (this_path.len == 0) continue;
  94            const prev_sep = sepPredicate(prev_path[prev_path.len - 1]);
  95            const this_sep = sepPredicate(this_path[0]);
  96            sum += @intFromBool(!prev_sep and !this_sep);
  97            sum += if (prev_sep and this_sep) this_path.len - 1 else this_path.len;
  98            prev_path = this_path;
  99        }
 100
 101        if (zero) sum += 1;
 102        break :blk sum;
 103    };
 104
 105    const buf = try allocator.alloc(u8, total_len);
 106    errdefer allocator.free(buf);
 107
 108    @memcpy(buf[0..paths[first_path_index].len], paths[first_path_index]);
 109    var buf_index: usize = paths[first_path_index].len;
 110    var prev_path = paths[first_path_index];
 111    assert(prev_path.len > 0);
 112    var i: usize = first_path_index + 1;
 113    while (i < paths.len) : (i += 1) {
 114        const this_path = paths[i];
 115        if (this_path.len == 0) continue;
 116        const prev_sep = sepPredicate(prev_path[prev_path.len - 1]);
 117        const this_sep = sepPredicate(this_path[0]);
 118        if (!prev_sep and !this_sep) {
 119            buf[buf_index] = separator;
 120            buf_index += 1;
 121        }
 122        const adjusted_path = if (prev_sep and this_sep) this_path[1..] else this_path;
 123        @memcpy(buf[buf_index..][0..adjusted_path.len], adjusted_path);
 124        buf_index += adjusted_path.len;
 125        prev_path = this_path;
 126    }
 127
 128    if (zero) buf[buf.len - 1] = 0;
 129
 130    // No need for shrink since buf is exactly the correct size.
 131    return buf;
 132}
 133
 134/// Naively combines a series of paths with the native path separator.
 135/// Allocates memory for the result, which must be freed by the caller.
 136pub fn join(allocator: Allocator, paths: []const []const u8) ![]u8 {
 137    return joinSepMaybeZ(allocator, sep, isSep, paths, false);
 138}
 139
 140/// Naively combines a series of paths with the native path separator and null terminator.
 141/// Allocates memory for the result, which must be freed by the caller.
 142pub fn joinZ(allocator: Allocator, paths: []const []const u8) ![:0]u8 {
 143    const out = try joinSepMaybeZ(allocator, sep, isSep, paths, true);
 144    return out[0 .. out.len - 1 :0];
 145}
 146
 147pub fn fmtJoin(paths: []const []const u8) std.fmt.Alt([]const []const u8, formatJoin) {
 148    return .{ .data = paths };
 149}
 150
 151fn formatJoin(paths: []const []const u8, w: *std.Io.Writer) std.Io.Writer.Error!void {
 152    const first_path_idx = for (paths, 0..) |p, idx| {
 153        if (p.len != 0) break idx;
 154    } else return;
 155
 156    try w.writeAll(paths[first_path_idx]); // first component
 157    var prev_path = paths[first_path_idx];
 158    for (paths[first_path_idx + 1 ..]) |this_path| {
 159        if (this_path.len == 0) continue; // skip empty components
 160        const prev_sep = isSep(prev_path[prev_path.len - 1]);
 161        const this_sep = isSep(this_path[0]);
 162        if (!prev_sep and !this_sep) {
 163            try w.writeByte(sep);
 164        }
 165        if (prev_sep and this_sep) {
 166            try w.writeAll(this_path[1..]); // skip redundant separator
 167        } else {
 168            try w.writeAll(this_path);
 169        }
 170        prev_path = this_path;
 171    }
 172}
 173
 174fn testJoinMaybeZUefi(paths: []const []const u8, expected: []const u8, zero: bool) !void {
 175    const uefiIsSep = struct {
 176        fn isSep(byte: u8) bool {
 177            return byte == '\\';
 178        }
 179    }.isSep;
 180    const actual = try joinSepMaybeZ(testing.allocator, sep_windows, uefiIsSep, paths, zero);
 181    defer testing.allocator.free(actual);
 182    try testing.expectEqualSlices(u8, expected, if (zero) actual[0 .. actual.len - 1 :0] else actual);
 183}
 184
 185fn testJoinMaybeZWindows(paths: []const []const u8, expected: []const u8, zero: bool) !void {
 186    const windowsIsSep = struct {
 187        fn isSep(byte: u8) bool {
 188            return byte == '/' or byte == '\\';
 189        }
 190    }.isSep;
 191    const actual = try joinSepMaybeZ(testing.allocator, sep_windows, windowsIsSep, paths, zero);
 192    defer testing.allocator.free(actual);
 193    try testing.expectEqualSlices(u8, expected, if (zero) actual[0 .. actual.len - 1 :0] else actual);
 194}
 195
 196fn testJoinMaybeZPosix(paths: []const []const u8, expected: []const u8, zero: bool) !void {
 197    const posixIsSep = struct {
 198        fn isSep(byte: u8) bool {
 199            return byte == '/';
 200        }
 201    }.isSep;
 202    const actual = try joinSepMaybeZ(testing.allocator, sep_posix, posixIsSep, paths, zero);
 203    defer testing.allocator.free(actual);
 204    try testing.expectEqualSlices(u8, expected, if (zero) actual[0 .. actual.len - 1 :0] else actual);
 205}
 206
 207test join {
 208    {
 209        const actual: []u8 = try join(testing.allocator, &[_][]const u8{});
 210        defer testing.allocator.free(actual);
 211        try testing.expectEqualSlices(u8, "", actual);
 212    }
 213    {
 214        const actual: [:0]u8 = try joinZ(testing.allocator, &[_][]const u8{});
 215        defer testing.allocator.free(actual);
 216        try testing.expectEqualSlices(u8, "", actual);
 217    }
 218    for (&[_]bool{ false, true }) |zero| {
 219        try testJoinMaybeZWindows(&[_][]const u8{}, "", zero);
 220        try testJoinMaybeZWindows(&[_][]const u8{ "c:\\a\\b", "c" }, "c:\\a\\b\\c", zero);
 221        try testJoinMaybeZWindows(&[_][]const u8{ "c:\\a\\b", "c" }, "c:\\a\\b\\c", zero);
 222        try testJoinMaybeZWindows(&[_][]const u8{ "c:\\a\\b\\", "\\c" }, "c:\\a\\b\\c", zero);
 223
 224        try testJoinMaybeZWindows(&[_][]const u8{ "c:\\", "a", "b\\", "c" }, "c:\\a\\b\\c", zero);
 225        try testJoinMaybeZWindows(&[_][]const u8{ "c:\\a\\", "b\\", "c" }, "c:\\a\\b\\c", zero);
 226
 227        try testJoinMaybeZWindows(
 228            &[_][]const u8{ "c:\\home\\andy\\dev\\zig\\build\\lib\\zig\\std", "ab.zig" },
 229            "c:\\home\\andy\\dev\\zig\\build\\lib\\zig\\std\\ab.zig",
 230            zero,
 231        );
 232
 233        try testJoinMaybeZUefi(&[_][]const u8{ "EFI", "Boot", "bootx64.efi" }, "EFI\\Boot\\bootx64.efi", zero);
 234        try testJoinMaybeZUefi(&[_][]const u8{ "EFI\\Boot", "bootx64.efi" }, "EFI\\Boot\\bootx64.efi", zero);
 235        try testJoinMaybeZUefi(&[_][]const u8{ "EFI\\", "\\Boot", "bootx64.efi" }, "EFI\\Boot\\bootx64.efi", zero);
 236        try testJoinMaybeZUefi(&[_][]const u8{ "EFI\\", "\\Boot\\", "\\bootx64.efi" }, "EFI\\Boot\\bootx64.efi", zero);
 237
 238        try testJoinMaybeZWindows(&[_][]const u8{ "c:\\", "a", "b/", "c" }, "c:\\a\\b/c", zero);
 239        try testJoinMaybeZWindows(&[_][]const u8{ "c:\\a/", "b\\", "/c" }, "c:\\a/b\\c", zero);
 240
 241        try testJoinMaybeZWindows(&[_][]const u8{ "", "c:\\", "", "", "a", "b\\", "c", "" }, "c:\\a\\b\\c", zero);
 242        try testJoinMaybeZWindows(&[_][]const u8{ "c:\\a/", "", "b\\", "", "/c" }, "c:\\a/b\\c", zero);
 243        try testJoinMaybeZWindows(&[_][]const u8{ "", "" }, "", zero);
 244
 245        try testJoinMaybeZPosix(&[_][]const u8{}, "", zero);
 246        try testJoinMaybeZPosix(&[_][]const u8{ "/a/b", "c" }, "/a/b/c", zero);
 247        try testJoinMaybeZPosix(&[_][]const u8{ "/a/b/", "c" }, "/a/b/c", zero);
 248
 249        try testJoinMaybeZPosix(&[_][]const u8{ "/", "a", "b/", "c" }, "/a/b/c", zero);
 250        try testJoinMaybeZPosix(&[_][]const u8{ "/a/", "b/", "c" }, "/a/b/c", zero);
 251
 252        try testJoinMaybeZPosix(
 253            &[_][]const u8{ "/home/andy/dev/zig/build/lib/zig/std", "ab.zig" },
 254            "/home/andy/dev/zig/build/lib/zig/std/ab.zig",
 255            zero,
 256        );
 257
 258        try testJoinMaybeZPosix(&[_][]const u8{ "a", "/c" }, "a/c", zero);
 259        try testJoinMaybeZPosix(&[_][]const u8{ "a/", "/c" }, "a/c", zero);
 260
 261        try testJoinMaybeZPosix(&[_][]const u8{ "", "/", "a", "", "b/", "c", "" }, "/a/b/c", zero);
 262        try testJoinMaybeZPosix(&[_][]const u8{ "/a/", "", "", "b/", "c" }, "/a/b/c", zero);
 263        try testJoinMaybeZPosix(&[_][]const u8{ "", "" }, "", zero);
 264    }
 265}
 266
 267pub fn isAbsoluteZ(path_c: [*:0]const u8) bool {
 268    if (native_os == .windows) {
 269        return isAbsoluteWindowsZ(path_c);
 270    } else {
 271        return isAbsolutePosixZ(path_c);
 272    }
 273}
 274
 275pub fn isAbsolute(path: []const u8) bool {
 276    if (native_os == .windows) {
 277        return isAbsoluteWindows(path);
 278    } else {
 279        return isAbsolutePosix(path);
 280    }
 281}
 282
 283fn isAbsoluteWindowsImpl(comptime T: type, path: []const T) bool {
 284    return switch (windows.getWin32PathType(T, path)) {
 285        // Unambiguously absolute
 286        .drive_absolute, .unc_absolute, .local_device, .root_local_device => true,
 287        // Unambiguously relative
 288        .relative => false,
 289        // Ambiguous, more absolute than relative
 290        .rooted => true,
 291        // Ambiguous, more relative than absolute
 292        .drive_relative => false,
 293    };
 294}
 295
 296pub fn isAbsoluteWindows(path: []const u8) bool {
 297    return isAbsoluteWindowsImpl(u8, path);
 298}
 299
 300pub fn isAbsoluteWindowsW(path_w: [*:0]const u16) bool {
 301    return isAbsoluteWindowsImpl(u16, mem.sliceTo(path_w, 0));
 302}
 303
 304pub fn isAbsoluteWindowsWtf16(path: []const u16) bool {
 305    return isAbsoluteWindowsImpl(u16, path);
 306}
 307
 308pub fn isAbsoluteWindowsZ(path_c: [*:0]const u8) bool {
 309    return isAbsoluteWindowsImpl(u8, mem.sliceTo(path_c, 0));
 310}
 311
 312pub fn isAbsolutePosix(path: []const u8) bool {
 313    return path.len > 0 and path[0] == sep_posix;
 314}
 315
 316pub fn isAbsolutePosixZ(path_c: [*:0]const u8) bool {
 317    return isAbsolutePosix(mem.sliceTo(path_c, 0));
 318}
 319
 320test isAbsoluteWindows {
 321    try testIsAbsoluteWindows("", false);
 322    try testIsAbsoluteWindows("/", true);
 323    try testIsAbsoluteWindows("//", true);
 324    try testIsAbsoluteWindows("//server", true);
 325    try testIsAbsoluteWindows("//server/file", true);
 326    try testIsAbsoluteWindows("\\\\server\\file", true);
 327    try testIsAbsoluteWindows("\\\\server", true);
 328    try testIsAbsoluteWindows("\\\\", true);
 329    try testIsAbsoluteWindows("c", false);
 330    try testIsAbsoluteWindows("c:", false);
 331    try testIsAbsoluteWindows("c:\\", true);
 332    try testIsAbsoluteWindows("c:/", true);
 333    try testIsAbsoluteWindows("c://", true);
 334    try testIsAbsoluteWindows("C:/Users/", true);
 335    try testIsAbsoluteWindows("C:\\Users\\", true);
 336    try testIsAbsoluteWindows("C:cwd/another", false);
 337    try testIsAbsoluteWindows("C:cwd\\another", false);
 338    try testIsAbsoluteWindows("λ:\\", true);
 339    try testIsAbsoluteWindows("λ:", false);
 340    try testIsAbsoluteWindows("\u{10000}:\\", false);
 341    try testIsAbsoluteWindows("directory/directory", false);
 342    try testIsAbsoluteWindows("directory\\directory", false);
 343    try testIsAbsoluteWindows("/usr/local", true);
 344}
 345
 346test isAbsolutePosix {
 347    try testIsAbsolutePosix("", false);
 348    try testIsAbsolutePosix("/home/foo", true);
 349    try testIsAbsolutePosix("/home/foo/..", true);
 350    try testIsAbsolutePosix("bar/", false);
 351    try testIsAbsolutePosix("./baz", false);
 352}
 353
 354fn testIsAbsoluteWindows(path: []const u8, expected_result: bool) !void {
 355    try testing.expectEqual(expected_result, isAbsoluteWindows(path));
 356    const path_w = try std.unicode.wtf8ToWtf16LeAllocZ(std.testing.allocator, path);
 357    defer std.testing.allocator.free(path_w);
 358    try testing.expectEqual(expected_result, isAbsoluteWindowsW(path_w));
 359    try testing.expectEqual(expected_result, isAbsoluteWindowsWtf16(path_w));
 360}
 361
 362fn testIsAbsolutePosix(path: []const u8, expected_result: bool) !void {
 363    try testing.expectEqual(expected_result, isAbsolutePosix(path));
 364}
 365
 366/// Deprecated; see `WindowsPath2`
 367pub const WindowsPath = struct {
 368    is_abs: bool,
 369    kind: Kind,
 370    disk_designator: []const u8,
 371
 372    pub const Kind = enum {
 373        None,
 374        Drive,
 375        NetworkShare,
 376    };
 377};
 378
 379/// Deprecated; see `parsePathWindows`
 380pub fn windowsParsePath(path: []const u8) WindowsPath {
 381    if (path.len >= 2 and path[1] == ':') {
 382        return WindowsPath{
 383            .is_abs = isAbsoluteWindows(path),
 384            .kind = WindowsPath.Kind.Drive,
 385            .disk_designator = path[0..2],
 386        };
 387    }
 388    if (path.len >= 1 and (path[0] == '/' or path[0] == '\\') and
 389        (path.len == 1 or (path[1] != '/' and path[1] != '\\')))
 390    {
 391        return WindowsPath{
 392            .is_abs = true,
 393            .kind = WindowsPath.Kind.None,
 394            .disk_designator = path[0..0],
 395        };
 396    }
 397    const relative_path = WindowsPath{
 398        .kind = WindowsPath.Kind.None,
 399        .disk_designator = &[_]u8{},
 400        .is_abs = false,
 401    };
 402
 403    if (path.len >= 2 and PathType.windows.isSep(u8, path[0]) and PathType.windows.isSep(u8, path[1])) {
 404        const root_end = root_end: {
 405            var server_end = mem.indexOfAnyPos(u8, path, 2, "/\\") orelse break :root_end path.len;
 406            while (server_end < path.len and PathType.windows.isSep(u8, path[server_end])) server_end += 1;
 407            break :root_end mem.indexOfAnyPos(u8, path, server_end, "/\\") orelse path.len;
 408        };
 409        return WindowsPath{
 410            .is_abs = true,
 411            .kind = WindowsPath.Kind.NetworkShare,
 412            .disk_designator = path[0..root_end],
 413        };
 414    }
 415    return relative_path;
 416}
 417
 418test windowsParsePath {
 419    {
 420        const parsed = windowsParsePath("//a/b");
 421        try testing.expect(parsed.is_abs);
 422        try testing.expect(parsed.kind == WindowsPath.Kind.NetworkShare);
 423        try testing.expect(mem.eql(u8, parsed.disk_designator, "//a/b"));
 424    }
 425    {
 426        const parsed = windowsParsePath("\\\\a\\b");
 427        try testing.expect(parsed.is_abs);
 428        try testing.expect(parsed.kind == WindowsPath.Kind.NetworkShare);
 429        try testing.expect(mem.eql(u8, parsed.disk_designator, "\\\\a\\b"));
 430    }
 431    {
 432        const parsed = windowsParsePath("\\\\a/b");
 433        try testing.expect(parsed.is_abs);
 434        try testing.expect(parsed.kind == WindowsPath.Kind.NetworkShare);
 435        try testing.expect(mem.eql(u8, parsed.disk_designator, "\\\\a/b"));
 436    }
 437    {
 438        const parsed = windowsParsePath("\\/a\\");
 439        try testing.expect(parsed.is_abs);
 440        try testing.expect(parsed.kind == WindowsPath.Kind.NetworkShare);
 441        try testing.expect(mem.eql(u8, parsed.disk_designator, "\\/a\\"));
 442    }
 443    {
 444        const parsed = windowsParsePath("\\\\a\\\\b");
 445        try testing.expect(parsed.is_abs);
 446        try testing.expect(parsed.kind == WindowsPath.Kind.NetworkShare);
 447        try testing.expect(mem.eql(u8, parsed.disk_designator, "\\\\a\\\\b"));
 448    }
 449    {
 450        const parsed = windowsParsePath("\\\\a\\\\b\\c");
 451        try testing.expect(parsed.is_abs);
 452        try testing.expect(parsed.kind == WindowsPath.Kind.NetworkShare);
 453        try testing.expect(mem.eql(u8, parsed.disk_designator, "\\\\a\\\\b"));
 454    }
 455    {
 456        const parsed = windowsParsePath("/usr/local");
 457        try testing.expect(parsed.is_abs);
 458        try testing.expect(parsed.kind == WindowsPath.Kind.None);
 459        try testing.expect(mem.eql(u8, parsed.disk_designator, ""));
 460    }
 461    {
 462        const parsed = windowsParsePath("c:../");
 463        try testing.expect(!parsed.is_abs);
 464        try testing.expect(parsed.kind == WindowsPath.Kind.Drive);
 465        try testing.expect(mem.eql(u8, parsed.disk_designator, "c:"));
 466    }
 467}
 468
 469/// On Windows, this calls `parsePathWindows` and on POSIX it calls `parsePathPosix`.
 470///
 471/// Returns a platform-specific struct with two fields: `root` and `kind`.
 472/// The `root` will be a slice of `path` (`/` for POSIX absolute paths, and things
 473/// like `C:\`, `\\server\share\`, etc for Windows paths).
 474/// If the path is of kind `.relative`, then `root` will be zero-length.
 475pub fn parsePath(path: []const u8) switch (native_os) {
 476    .windows => WindowsPath2(u8),
 477    else => PosixPath,
 478} {
 479    switch (native_os) {
 480        .windows => return parsePathWindows(u8, path),
 481        else => return parsePathPosix(path),
 482    }
 483}
 484
 485const PosixPath = struct {
 486    kind: enum { relative, absolute },
 487    root: []const u8,
 488};
 489
 490pub fn parsePathPosix(path: []const u8) PosixPath {
 491    const abs = isAbsolutePosix(path);
 492    return .{
 493        .kind = if (abs) .absolute else .relative,
 494        .root = if (abs) path[0..1] else path[0..0],
 495    };
 496}
 497
 498test parsePathPosix {
 499    {
 500        const parsed = parsePathPosix("a/b");
 501        try testing.expectEqual(.relative, parsed.kind);
 502        try testing.expectEqualStrings("", parsed.root);
 503    }
 504    {
 505        const parsed = parsePathPosix("/a/b");
 506        try testing.expectEqual(.absolute, parsed.kind);
 507        try testing.expectEqualStrings("/", parsed.root);
 508    }
 509    {
 510        const parsed = parsePathPosix("///a/b");
 511        try testing.expectEqual(.absolute, parsed.kind);
 512        try testing.expectEqualStrings("/", parsed.root);
 513    }
 514}
 515
 516pub fn WindowsPath2(comptime T: type) type {
 517    return struct {
 518        kind: windows.Win32PathType,
 519        root: []const T,
 520    };
 521}
 522
 523pub fn parsePathWindows(comptime T: type, path: []const T) WindowsPath2(T) {
 524    const kind = windows.getWin32PathType(T, path);
 525    const root = root: switch (kind) {
 526        .drive_absolute, .drive_relative => {
 527            const drive_letter_len = getDriveLetter(T, path).len;
 528            break :root path[0 .. drive_letter_len + @as(usize, if (kind == .drive_absolute) 2 else 1)];
 529        },
 530        .relative => path[0..0],
 531        .local_device => path[0..4],
 532        .root_local_device => path,
 533        .rooted => path[0..1],
 534        .unc_absolute => {
 535            const unc = parseUNC(T, path);
 536            // There may be any number of path separators between the server and the share,
 537            // so take that into account by using pointer math to get the difference.
 538            var root_len = 2 + (unc.share.ptr - unc.server.ptr) + unc.share.len;
 539            if (unc.sep_after_share) root_len += 1;
 540            break :root path[0..root_len];
 541        },
 542    };
 543    return .{
 544        .kind = kind,
 545        .root = root,
 546    };
 547}
 548
 549test parsePathWindows {
 550    {
 551        const path = "//a/b";
 552        const parsed = parsePathWindows(u8, path);
 553        try testing.expectEqual(.unc_absolute, parsed.kind);
 554        try testing.expectEqualStrings("//a/b", parsed.root);
 555        try testWindowsParsePathHarmony(path);
 556    }
 557    {
 558        const path = "\\\\a\\b";
 559        const parsed = parsePathWindows(u8, path);
 560        try testing.expectEqual(.unc_absolute, parsed.kind);
 561        try testing.expectEqualStrings("\\\\a\\b", parsed.root);
 562        try testWindowsParsePathHarmony(path);
 563    }
 564    {
 565        const path = "\\/a/b/c";
 566        const parsed = parsePathWindows(u8, path);
 567        try testing.expectEqual(.unc_absolute, parsed.kind);
 568        try testing.expectEqualStrings("\\/a/b/", parsed.root);
 569        try testWindowsParsePathHarmony(path);
 570    }
 571    {
 572        const path = "\\\\a\\";
 573        const parsed = parsePathWindows(u8, path);
 574        try testing.expectEqual(.unc_absolute, parsed.kind);
 575        try testing.expectEqualStrings("\\\\a\\", parsed.root);
 576        try testWindowsParsePathHarmony(path);
 577    }
 578    {
 579        const path = "\\\\a\\b\\";
 580        const parsed = parsePathWindows(u8, path);
 581        try testing.expectEqual(.unc_absolute, parsed.kind);
 582        try testing.expectEqualStrings("\\\\a\\b\\", parsed.root);
 583        try testWindowsParsePathHarmony(path);
 584    }
 585    {
 586        const path = "\\\\a\\/b\\/";
 587        const parsed = parsePathWindows(u8, path);
 588        try testing.expectEqual(.unc_absolute, parsed.kind);
 589        try testing.expectEqualStrings("\\\\a\\/b\\", parsed.root);
 590        try testWindowsParsePathHarmony(path);
 591    }
 592    {
 593        const path = "\\\\кириллица\\ελληνικά\\português";
 594        const parsed = parsePathWindows(u8, path);
 595        try testing.expectEqual(.unc_absolute, parsed.kind);
 596        try testing.expectEqualStrings("\\\\кириллица\\ελληνικά\\", parsed.root);
 597        try testWindowsParsePathHarmony(path);
 598    }
 599    {
 600        const path = "/usr/local";
 601        const parsed = parsePathWindows(u8, path);
 602        try testing.expectEqual(.rooted, parsed.kind);
 603        try testing.expectEqualStrings("/", parsed.root);
 604        try testWindowsParsePathHarmony(path);
 605    }
 606    {
 607        const path = "\\\\.";
 608        const parsed = parsePathWindows(u8, path);
 609        try testing.expectEqual(.root_local_device, parsed.kind);
 610        try testing.expectEqualStrings("\\\\.", parsed.root);
 611        try testWindowsParsePathHarmony(path);
 612    }
 613    {
 614        const path = "\\\\.\\a";
 615        const parsed = parsePathWindows(u8, path);
 616        try testing.expectEqual(.local_device, parsed.kind);
 617        try testing.expectEqualStrings("\\\\.\\", parsed.root);
 618        try testWindowsParsePathHarmony(path);
 619    }
 620    {
 621        const path = "c:../";
 622        const parsed = parsePathWindows(u8, path);
 623        try testing.expectEqual(.drive_relative, parsed.kind);
 624        try testing.expectEqualStrings("c:", parsed.root);
 625        try testWindowsParsePathHarmony(path);
 626    }
 627    {
 628        const path = "C:\\../";
 629        const parsed = parsePathWindows(u8, path);
 630        try testing.expectEqual(.drive_absolute, parsed.kind);
 631        try testing.expectEqualStrings("C:\\", parsed.root);
 632        try testWindowsParsePathHarmony(path);
 633    }
 634    {
 635        // Non-ASCII code point that is encoded as one WTF-16 code unit is considered a valid drive letter
 636        const path = "€:\\";
 637        const parsed = parsePathWindows(u8, path);
 638        try testing.expectEqual(.drive_absolute, parsed.kind);
 639        try testing.expectEqualStrings("€:\\", parsed.root);
 640        try testWindowsParsePathHarmony(path);
 641    }
 642    {
 643        const path = "€:";
 644        const parsed = parsePathWindows(u8, path);
 645        try testing.expectEqual(.drive_relative, parsed.kind);
 646        try testing.expectEqualStrings("€:", parsed.root);
 647        try testWindowsParsePathHarmony(path);
 648    }
 649    {
 650        // But code points that are encoded as two WTF-16 code units are not
 651        const path = "\u{10000}:\\";
 652        const parsed = parsePathWindows(u8, path);
 653        try testing.expectEqual(.relative, parsed.kind);
 654        try testing.expectEqualStrings("", parsed.root);
 655        try testWindowsParsePathHarmony(path);
 656    }
 657    {
 658        const path = "\u{10000}:";
 659        const parsed = parsePathWindows(u8, path);
 660        try testing.expectEqual(.relative, parsed.kind);
 661        try testing.expectEqualStrings("", parsed.root);
 662        try testWindowsParsePathHarmony(path);
 663    }
 664    {
 665        // Paths are assumed to be in the Win32 namespace, so while this is
 666        // likely a NT namespace path, it's treated as a rooted path.
 667        const path = "\\??\\foo";
 668        const parsed = parsePathWindows(u8, path);
 669        try testing.expectEqual(.rooted, parsed.kind);
 670        try testing.expectEqualStrings("\\", parsed.root);
 671        try testWindowsParsePathHarmony(path);
 672    }
 673}
 674
 675fn testWindowsParsePathHarmony(wtf8: []const u8) !void {
 676    var wtf16_buf: [256]u16 = undefined;
 677    const wtf16_len = try std.unicode.wtf8ToWtf16Le(&wtf16_buf, wtf8);
 678    const wtf16 = wtf16_buf[0..wtf16_len];
 679
 680    const wtf8_parsed = parsePathWindows(u8, wtf8);
 681    const wtf16_parsed = parsePathWindows(u16, wtf16);
 682
 683    var wtf8_buf: [256]u8 = undefined;
 684    const wtf16_root_as_wtf8_len = std.unicode.wtf16LeToWtf8(&wtf8_buf, wtf16_parsed.root);
 685    const wtf16_root_as_wtf8 = wtf8_buf[0..wtf16_root_as_wtf8_len];
 686
 687    try std.testing.expectEqual(wtf8_parsed.kind, wtf16_parsed.kind);
 688    try std.testing.expectEqualStrings(wtf8_parsed.root, wtf16_root_as_wtf8);
 689}
 690
 691/// Deprecated; use `parsePath`
 692pub fn diskDesignator(path: []const u8) []const u8 {
 693    if (native_os == .windows) {
 694        return diskDesignatorWindows(path);
 695    } else {
 696        return "";
 697    }
 698}
 699
 700/// Deprecated; use `parsePathWindows`
 701pub fn diskDesignatorWindows(path: []const u8) []const u8 {
 702    return windowsParsePath(path).disk_designator;
 703}
 704
 705fn WindowsUNC(comptime T: type) type {
 706    return struct {
 707        server: []const T,
 708        sep_after_server: bool,
 709        share: []const T,
 710        sep_after_share: bool,
 711    };
 712}
 713
 714/// Asserts that `path` starts with two path separators
 715fn parseUNC(comptime T: type, path: []const T) WindowsUNC(T) {
 716    assert(path.len >= 2 and PathType.windows.isSep(T, path[0]) and PathType.windows.isSep(T, path[1]));
 717    const any_sep = switch (T) {
 718        u8 => "/\\",
 719        u16 => std.unicode.wtf8ToWtf16LeStringLiteral("/\\"),
 720        else => @compileError("only u8 (WTF-8) and u16 (WTF-16LE) are supported"),
 721    };
 722    // For the server, the first path separator after the initial two is always
 723    // the terminator of the server name, even if that means the server name is
 724    // zero-length.
 725    const server_end = mem.indexOfAnyPos(T, path, 2, any_sep) orelse return .{
 726        .server = path[2..path.len],
 727        .sep_after_server = false,
 728        .share = path[path.len..path.len],
 729        .sep_after_share = false,
 730    };
 731    // For the share, there can be any number of path separators between the server
 732    // and the share, so we want to skip over all of them instead of just looking for
 733    // the first one.
 734    var it = std.mem.tokenizeAny(T, path[server_end + 1 ..], any_sep);
 735    const share = it.next() orelse return .{
 736        .server = path[2..server_end],
 737        .sep_after_server = true,
 738        .share = path[server_end + 1 .. server_end + 1],
 739        .sep_after_share = false,
 740    };
 741    return .{
 742        .server = path[2..server_end],
 743        .sep_after_server = true,
 744        .share = share,
 745        .sep_after_share = it.index != it.buffer.len,
 746    };
 747}
 748
 749test parseUNC {
 750    {
 751        const unc = parseUNC(u8, "//");
 752        try std.testing.expectEqualStrings("", unc.server);
 753        try std.testing.expect(!unc.sep_after_server);
 754        try std.testing.expectEqualStrings("", unc.share);
 755        try std.testing.expect(!unc.sep_after_share);
 756    }
 757    {
 758        const unc = parseUNC(u8, "\\\\s");
 759        try std.testing.expectEqualStrings("s", unc.server);
 760        try std.testing.expect(!unc.sep_after_server);
 761        try std.testing.expectEqualStrings("", unc.share);
 762        try std.testing.expect(!unc.sep_after_share);
 763    }
 764    {
 765        const unc = parseUNC(u8, "\\\\s/");
 766        try std.testing.expectEqualStrings("s", unc.server);
 767        try std.testing.expect(unc.sep_after_server);
 768        try std.testing.expectEqualStrings("", unc.share);
 769        try std.testing.expect(!unc.sep_after_share);
 770    }
 771    {
 772        const unc = parseUNC(u8, "\\/server\\share");
 773        try std.testing.expectEqualStrings("server", unc.server);
 774        try std.testing.expect(unc.sep_after_server);
 775        try std.testing.expectEqualStrings("share", unc.share);
 776        try std.testing.expect(!unc.sep_after_share);
 777    }
 778    {
 779        const unc = parseUNC(u8, "/\\server\\share/");
 780        try std.testing.expectEqualStrings("server", unc.server);
 781        try std.testing.expect(unc.sep_after_server);
 782        try std.testing.expectEqualStrings("share", unc.share);
 783        try std.testing.expect(unc.sep_after_share);
 784    }
 785    {
 786        const unc = parseUNC(u8, "\\\\server/\\share\\/");
 787        try std.testing.expectEqualStrings("server", unc.server);
 788        try std.testing.expect(unc.sep_after_server);
 789        try std.testing.expectEqualStrings("share", unc.share);
 790        try std.testing.expect(unc.sep_after_share);
 791    }
 792    {
 793        const unc = parseUNC(u8, "\\\\server\\/\\\\");
 794        try std.testing.expectEqualStrings("server", unc.server);
 795        try std.testing.expect(unc.sep_after_server);
 796        try std.testing.expectEqualStrings("", unc.share);
 797        try std.testing.expect(!unc.sep_after_share);
 798    }
 799}
 800
 801const DiskDesignatorKind = enum { drive, unc };
 802
 803/// `p1` and `p2` are both assumed to be the `kind` provided.
 804fn compareDiskDesignators(comptime T: type, kind: DiskDesignatorKind, p1: []const T, p2: []const T) bool {
 805    const eql = switch (T) {
 806        u8 => windows.eqlIgnoreCaseWtf8,
 807        u16 => windows.eqlIgnoreCaseWtf16,
 808        else => @compileError("only u8 (WTF-8) and u16 (WTF-16LE) is supported"),
 809    };
 810    switch (kind) {
 811        .drive => {
 812            const drive_letter1 = getDriveLetter(T, p1);
 813            const drive_letter2 = getDriveLetter(T, p2);
 814
 815            return eql(drive_letter1, drive_letter2);
 816        },
 817        .unc => {
 818            var unc1 = parseUNC(T, p1);
 819            var unc2 = parseUNC(T, p2);
 820
 821            return eql(unc1.server, unc2.server) and
 822                eql(unc1.share, unc2.share);
 823        },
 824    }
 825}
 826
 827/// `path` is assumed to be drive-relative or drive-absolute.
 828fn getDriveLetter(comptime T: type, path: []const T) []const T {
 829    const len: usize = switch (T) {
 830        // getWin32PathType will only return .drive_absolute/.drive_relative when there is
 831        // (1) a valid code point, and (2) a code point < U+10000, so we only need to
 832        // get the length determined by the first byte.
 833        u8 => std.unicode.utf8ByteSequenceLength(path[0]) catch unreachable,
 834        u16 => 1,
 835        else => @compileError("unsupported type: " ++ @typeName(T)),
 836    };
 837    return path[0..len];
 838}
 839
 840test compareDiskDesignators {
 841    try testCompareDiskDesignators(true, .drive, "c:", "C:\\");
 842    try testCompareDiskDesignators(true, .drive, "C:\\", "C:");
 843    try testCompareDiskDesignators(false, .drive, "C:\\", "D:\\");
 844    // Case-insensitivity technically applies to non-ASCII drive letters
 845    try testCompareDiskDesignators(true, .drive, "λ:\\", "Λ:");
 846
 847    try testCompareDiskDesignators(true, .unc, "\\\\server", "//server//");
 848    try testCompareDiskDesignators(true, .unc, "\\\\server\\\\share", "/\\server/share");
 849    try testCompareDiskDesignators(true, .unc, "\\\\server\\\\share", "/\\server/share\\\\foo");
 850    try testCompareDiskDesignators(false, .unc, "\\\\server\\sharefoo", "/\\server/share\\foo");
 851    try testCompareDiskDesignators(false, .unc, "\\\\serverfoo\\\\share", "//server/share");
 852    try testCompareDiskDesignators(false, .unc, "\\\\server\\", "//server/share");
 853}
 854
 855fn testCompareDiskDesignators(expected_result: bool, kind: DiskDesignatorKind, p1: []const u8, p2: []const u8) !void {
 856    var wtf16_buf1: [256]u16 = undefined;
 857    const w1_len = try std.unicode.wtf8ToWtf16Le(&wtf16_buf1, p1);
 858    var wtf16_buf2: [256]u16 = undefined;
 859    const w2_len = try std.unicode.wtf8ToWtf16Le(&wtf16_buf2, p2);
 860    try std.testing.expectEqual(expected_result, compareDiskDesignators(u8, kind, p1, p2));
 861    try std.testing.expectEqual(expected_result, compareDiskDesignators(u16, kind, wtf16_buf1[0..w1_len], wtf16_buf2[0..w2_len]));
 862}
 863
 864/// On Windows, this calls `resolveWindows` and on POSIX it calls `resolvePosix`.
 865pub fn resolve(allocator: Allocator, paths: []const []const u8) Allocator.Error![]u8 {
 866    if (native_os == .windows) {
 867        return resolveWindows(allocator, paths);
 868    } else {
 869        return resolvePosix(allocator, paths);
 870    }
 871}
 872
 873/// This function is like a series of `cd` statements executed one after another.
 874/// It resolves "." and ".." to the best of its ability, but will not convert relative paths to
 875/// an absolute path, use std.fs.Dir.realpath instead.
 876/// ".." components may persist in the resolved path if the resolved path is relative or drive-relative.
 877/// Path separators are canonicalized to '\\' and drives are canonicalized to capital letters.
 878///
 879/// The result will not have a trailing path separator, except for the following scenarios:
 880/// - The resolved path is drive-absolute with no components (e.g. `C:\`).
 881/// - The resolved path is a UNC path with only a server name, and the input path contained a trailing separator
 882///   (e.g. `\\server\`).
 883/// - The resolved path is a UNC path with no components after the share name, and the input path contained a
 884///   trailing separator (e.g. `\\server\share\`).
 885///
 886/// Each drive has its own current working directory, which is only resolved via the paths provided.
 887/// In the scenario that the resolved path contains a drive-relative path that can't be resolved using the paths alone,
 888/// the result will be a drive-relative path.
 889/// Similarly, in the scenario that the resolved path contains a rooted path that can't be resolved using the paths alone,
 890/// the result will be a rooted path.
 891///
 892/// Note: all usage of this function should be audited due to the existence of symlinks.
 893/// Without performing actual syscalls, resolving `..` could be incorrect.
 894/// This API may break in the future: https://github.com/ziglang/zig/issues/13613
 895pub fn resolveWindows(allocator: Allocator, paths: []const []const u8) Allocator.Error![]u8 {
 896    // Avoid heap allocation when paths.len is <= @bitSizeOf(usize) * 2
 897    // (we use `* 3` because stackFallback uses 1 usize as a length)
 898    var bit_set_allocator_state = std.heap.stackFallback(@sizeOf(usize) * 3, allocator);
 899    const bit_set_allocator = bit_set_allocator_state.get();
 900    var relevant_paths = try std.bit_set.DynamicBitSetUnmanaged.initEmpty(bit_set_allocator, paths.len);
 901    defer relevant_paths.deinit(bit_set_allocator);
 902
 903    // Iterate the paths backwards, marking the relevant paths along the way.
 904    // This also allows us to break from the loop whenever any earlier paths are known to be irrelevant.
 905    var first_path_i: usize = paths.len;
 906    const effective_root_path: WindowsPath2(u8) = root: {
 907        var last_effective_root_path: WindowsPath2(u8) = .{ .kind = .relative, .root = "" };
 908        var last_rooted_path_i: ?usize = null;
 909        var last_drive_relative_path_i: usize = undefined;
 910        while (first_path_i > 0) {
 911            first_path_i -= 1;
 912            const parsed = parsePathWindows(u8, paths[first_path_i]);
 913            switch (parsed.kind) {
 914                .unc_absolute, .root_local_device, .local_device => {
 915                    switch (last_effective_root_path.kind) {
 916                        .rooted => {},
 917                        .drive_relative => continue,
 918                        else => {
 919                            relevant_paths.set(first_path_i);
 920                        },
 921                    }
 922                    break :root parsed;
 923                },
 924                .drive_relative, .drive_absolute => {
 925                    switch (last_effective_root_path.kind) {
 926                        .drive_relative => if (!compareDiskDesignators(u8, .drive, parsed.root, last_effective_root_path.root)) {
 927                            continue;
 928                        } else if (last_rooted_path_i != null) {
 929                            break :root .{ .kind = .drive_absolute, .root = parsed.root };
 930                        },
 931                        .relative => last_effective_root_path = parsed,
 932                        .rooted => {
 933                            // This is the end of the line, since the rooted path will always be relative
 934                            // to this drive letter, and even if the current path is drive-relative, the
 935                            // rooted-ness makes that irrelevant.
 936                            //
 937                            // Therefore, force the kind of the effective root to be drive-absolute in order to
 938                            // properly resolve a rooted path against a drive-relative one, as the result should
 939                            // always be drive-absolute.
 940                            break :root .{ .kind = .drive_absolute, .root = parsed.root };
 941                        },
 942                        .drive_absolute, .unc_absolute, .root_local_device, .local_device => unreachable,
 943                    }
 944                    relevant_paths.set(first_path_i);
 945                    last_drive_relative_path_i = first_path_i;
 946                    if (parsed.kind == .drive_absolute) {
 947                        break :root parsed;
 948                    }
 949                },
 950                .relative => {
 951                    switch (last_effective_root_path.kind) {
 952                        .rooted => continue,
 953                        .relative => last_effective_root_path = parsed,
 954                        else => {},
 955                    }
 956                    relevant_paths.set(first_path_i);
 957                },
 958                .rooted => {
 959                    switch (last_effective_root_path.kind) {
 960                        .drive_relative => {},
 961                        .relative => last_effective_root_path = parsed,
 962                        .rooted => continue,
 963                        .drive_absolute, .unc_absolute, .root_local_device, .local_device => unreachable,
 964                    }
 965                    if (last_rooted_path_i == null) {
 966                        last_rooted_path_i = first_path_i;
 967                        relevant_paths.set(first_path_i);
 968                    }
 969                },
 970            }
 971        }
 972        // After iterating, if the pending effective root is drive-relative then that means
 973        // nothing has led to forcing a drive-absolute root (a path that allows resolving the
 974        // drive-specific CWD would cause an early break), so we now need to ignore all paths
 975        // before the most recent drive-relative one. For example, if we're resolving
 976        // { "\\rooted", "relative", "C:drive-relative" }
 977        // then the `\rooted` and `relative` needs to be ignored since we can't
 978        // know what the rooted path is rooted against as that'd require knowing the CWD.
 979        if (last_effective_root_path.kind == .drive_relative) {
 980            for (0..last_drive_relative_path_i) |i| {
 981                relevant_paths.unset(i);
 982            }
 983        }
 984        break :root last_effective_root_path;
 985    };
 986
 987    var result: std.ArrayList(u8) = .empty;
 988    defer result.deinit(allocator);
 989
 990    var want_path_sep_between_root_and_component = false;
 991    switch (effective_root_path.kind) {
 992        .root_local_device, .local_device => {
 993            try result.ensureUnusedCapacity(allocator, 3);
 994            result.appendSliceAssumeCapacity("\\\\");
 995            result.appendAssumeCapacity(effective_root_path.root[2]); // . or ?
 996            want_path_sep_between_root_and_component = true;
 997        },
 998        .drive_absolute, .drive_relative => {
 999            try result.ensureUnusedCapacity(allocator, effective_root_path.root.len);
1000            result.appendAssumeCapacity(std.ascii.toUpper(effective_root_path.root[0]));
1001            result.appendAssumeCapacity(':');
1002            if (effective_root_path.kind == .drive_absolute) {
1003                result.appendAssumeCapacity('\\');
1004            }
1005        },
1006        .unc_absolute => {
1007            const unc = parseUNC(u8, effective_root_path.root);
1008
1009            const root_len = len: {
1010                var len: usize = 2 + unc.server.len + unc.share.len;
1011                if (unc.sep_after_server) len += 1;
1012                if (unc.sep_after_share) len += 1;
1013                break :len len;
1014            };
1015            try result.ensureUnusedCapacity(allocator, root_len);
1016            result.appendSliceAssumeCapacity("\\\\");
1017            if (unc.server.len > 0 or unc.sep_after_server) {
1018                result.appendSliceAssumeCapacity(unc.server);
1019                if (unc.sep_after_server)
1020                    result.appendAssumeCapacity('\\')
1021                else
1022                    want_path_sep_between_root_and_component = true;
1023            }
1024            if (unc.share.len > 0) {
1025                result.appendSliceAssumeCapacity(unc.share);
1026                if (unc.sep_after_share)
1027                    result.appendAssumeCapacity('\\')
1028                else
1029                    want_path_sep_between_root_and_component = true;
1030            }
1031        },
1032        .rooted => {
1033            try result.append(allocator, '\\');
1034        },
1035        .relative => {},
1036    }
1037
1038    const root_len = result.items.len;
1039    var negative_count: usize = 0;
1040    for (paths[first_path_i..], first_path_i..) |path, i| {
1041        if (!relevant_paths.isSet(i)) continue;
1042
1043        const parsed = parsePathWindows(u8, path);
1044        const skip_len = parsed.root.len;
1045        var it = mem.tokenizeAny(u8, path[skip_len..], "/\\");
1046        while (it.next()) |component| {
1047            if (mem.eql(u8, component, ".")) {
1048                continue;
1049            } else if (mem.eql(u8, component, "..")) {
1050                if (result.items.len == 0 or (result.items.len == root_len and effective_root_path.kind == .drive_relative)) {
1051                    negative_count += 1;
1052                    continue;
1053                }
1054                while (true) {
1055                    if (result.items.len == root_len) {
1056                        break;
1057                    }
1058                    const end_with_sep = PathType.windows.isSep(u8, result.items[result.items.len - 1]);
1059                    result.items.len -= 1;
1060                    if (end_with_sep) break;
1061                }
1062            } else if (result.items.len == root_len and !want_path_sep_between_root_and_component) {
1063                try result.appendSlice(allocator, component);
1064            } else {
1065                try result.ensureUnusedCapacity(allocator, 1 + component.len);
1066                result.appendAssumeCapacity('\\');
1067                result.appendSliceAssumeCapacity(component);
1068            }
1069        }
1070    }
1071
1072    if (root_len != 0 and result.items.len == root_len and negative_count == 0) {
1073        return result.toOwnedSlice(allocator);
1074    }
1075
1076    if (result.items.len == root_len) {
1077        if (negative_count == 0) {
1078            return allocator.dupe(u8, ".");
1079        }
1080
1081        try result.ensureTotalCapacityPrecise(allocator, 3 * negative_count - 1);
1082        for (0..negative_count - 1) |_| {
1083            result.appendSliceAssumeCapacity("..\\");
1084        }
1085        result.appendSliceAssumeCapacity("..");
1086    } else {
1087        const dest = try result.addManyAt(allocator, root_len, 3 * negative_count);
1088        for (0..negative_count) |i| {
1089            dest[i * 3 ..][0..3].* = "..\\".*;
1090        }
1091    }
1092
1093    return result.toOwnedSlice(allocator);
1094}
1095
1096/// This function is like a series of `cd` statements executed one after another.
1097/// It resolves "." and ".." to the best of its ability, but will not convert relative paths to
1098/// an absolute path, use std.fs.Dir.realpath instead.
1099/// ".." components may persist in the resolved path if the resolved path is relative.
1100/// The result does not have a trailing path separator.
1101/// This function does not perform any syscalls. Executing this series of path
1102/// lookups on the actual filesystem may produce different results due to
1103/// symlinks.
1104pub fn resolvePosix(allocator: Allocator, paths: []const []const u8) Allocator.Error![]u8 {
1105    assert(paths.len > 0);
1106
1107    var result = std.array_list.Managed(u8).init(allocator);
1108    defer result.deinit();
1109
1110    var negative_count: usize = 0;
1111    var is_abs = false;
1112
1113    for (paths) |p| {
1114        if (isAbsolutePosix(p)) {
1115            is_abs = true;
1116            negative_count = 0;
1117            result.clearRetainingCapacity();
1118        }
1119        var it = mem.tokenizeScalar(u8, p, '/');
1120        while (it.next()) |component| {
1121            if (mem.eql(u8, component, ".")) {
1122                continue;
1123            } else if (mem.eql(u8, component, "..")) {
1124                if (result.items.len == 0) {
1125                    negative_count += @intFromBool(!is_abs);
1126                    continue;
1127                }
1128                while (true) {
1129                    const ends_with_slash = result.items[result.items.len - 1] == '/';
1130                    result.items.len -= 1;
1131                    if (ends_with_slash or result.items.len == 0) break;
1132                }
1133            } else if (result.items.len > 0 or is_abs) {
1134                try result.ensureUnusedCapacity(1 + component.len);
1135                result.appendAssumeCapacity('/');
1136                result.appendSliceAssumeCapacity(component);
1137            } else {
1138                try result.appendSlice(component);
1139            }
1140        }
1141    }
1142
1143    if (result.items.len == 0) {
1144        if (is_abs) {
1145            return allocator.dupe(u8, "/");
1146        }
1147        if (negative_count == 0) {
1148            return allocator.dupe(u8, ".");
1149        } else {
1150            const real_result = try allocator.alloc(u8, 3 * negative_count - 1);
1151            var count = negative_count - 1;
1152            var i: usize = 0;
1153            while (count > 0) : (count -= 1) {
1154                real_result[i..][0..3].* = "../".*;
1155                i += 3;
1156            }
1157            real_result[i..][0..2].* = "..".*;
1158            return real_result;
1159        }
1160    }
1161
1162    if (negative_count == 0) {
1163        return result.toOwnedSlice();
1164    } else {
1165        const real_result = try allocator.alloc(u8, 3 * negative_count + result.items.len);
1166        var count = negative_count;
1167        var i: usize = 0;
1168        while (count > 0) : (count -= 1) {
1169            real_result[i..][0..3].* = "../".*;
1170            i += 3;
1171        }
1172        @memcpy(real_result[i..][0..result.items.len], result.items);
1173        return real_result;
1174    }
1175}
1176
1177test resolve {
1178    try testResolveWindows(&[_][]const u8{ "a", "..\\..\\.." }, "..\\..");
1179    try testResolveWindows(&[_][]const u8{ "..", "", "..\\..\\foo" }, "..\\..\\..\\foo");
1180    try testResolveWindows(&[_][]const u8{ "a\\b\\c\\", "..\\..\\.." }, ".");
1181    try testResolveWindows(&[_][]const u8{"."}, ".");
1182    try testResolveWindows(&[_][]const u8{""}, ".");
1183
1184    try testResolvePosix(&[_][]const u8{ "a", "../../.." }, "../..");
1185    try testResolvePosix(&[_][]const u8{ "..", "", "../../foo" }, "../../../foo");
1186    try testResolvePosix(&[_][]const u8{ "a/b/c/", "../../.." }, ".");
1187    try testResolvePosix(&[_][]const u8{"."}, ".");
1188    try testResolvePosix(&[_][]const u8{""}, ".");
1189}
1190
1191test resolveWindows {
1192    try testResolveWindows(
1193        &[_][]const u8{ "Z:\\", "/usr/local", "lib\\zig\\std\\array_list.zig" },
1194        "Z:\\usr\\local\\lib\\zig\\std\\array_list.zig",
1195    );
1196    try testResolveWindows(
1197        &[_][]const u8{ "z:\\", "usr/local", "lib\\zig" },
1198        "Z:\\usr\\local\\lib\\zig",
1199    );
1200
1201    try testResolveWindows(&[_][]const u8{ "c:\\a\\b\\c", "/hi", "ok" }, "C:\\hi\\ok");
1202    try testResolveWindows(&[_][]const u8{ "c:\\a\\b\\c\\", ".\\..\\foo" }, "C:\\a\\b\\foo");
1203    try testResolveWindows(&[_][]const u8{ "c:/blah\\blah", "d:/games", "c:../a" }, "C:\\blah\\a");
1204    try testResolveWindows(&[_][]const u8{ "c:/blah\\blah", "d:/games", "C:../a" }, "C:\\blah\\a");
1205    try testResolveWindows(&[_][]const u8{ "c:/ignore", "d:\\a/b\\c/d", "\\e.exe" }, "D:\\e.exe");
1206    try testResolveWindows(&[_][]const u8{ "c:/ignore", "c:/some/file" }, "C:\\some\\file");
1207    // The first path "sets" the CWD, so the drive-relative path is then relative to that.
1208    try testResolveWindows(&[_][]const u8{ "d:/foo", "d:some/dir//", "D:another" }, "D:\\foo\\some\\dir\\another");
1209    try testResolveWindows(&[_][]const u8{ "//server/share", "..", "relative\\" }, "\\\\server\\share\\relative");
1210    try testResolveWindows(&[_][]const u8{ "\\\\server/share", "..", "relative\\" }, "\\\\server\\share\\relative");
1211    try testResolveWindows(&[_][]const u8{ "\\\\server/share/ignore", "//server/share/bar" }, "\\\\server\\share\\bar");
1212    try testResolveWindows(&[_][]const u8{ "\\/server\\share/", "..", "relative" }, "\\\\server\\share\\relative");
1213    try testResolveWindows(&[_][]const u8{ "\\\\server\\share", "C:drive-relative" }, "C:drive-relative");
1214    try testResolveWindows(&[_][]const u8{ "c:/", "//" }, "\\\\");
1215    try testResolveWindows(&[_][]const u8{ "c:/", "//server" }, "\\\\server");
1216    try testResolveWindows(&[_][]const u8{ "c:/", "//server/share" }, "\\\\server\\share");
1217    try testResolveWindows(&[_][]const u8{ "c:/", "//server//share////" }, "\\\\server\\share\\");
1218    try testResolveWindows(&[_][]const u8{ "c:/", "///some//dir" }, "\\\\\\some\\dir");
1219    try testResolveWindows(&[_][]const u8{ "c:foo", "bar" }, "C:foo\\bar");
1220    try testResolveWindows(&[_][]const u8{ "C:\\foo\\tmp.3\\", "..\\tmp.3\\cycles\\root.js" }, "C:\\foo\\tmp.3\\cycles\\root.js");
1221    // Drive-relative stays drive-relative if there's nothing to provide the drive-specific CWD
1222    try testResolveWindows(&[_][]const u8{ "relative", "d:foo" }, "D:foo");
1223    try testResolveWindows(&[_][]const u8{ "../..\\..", "d:foo" }, "D:foo");
1224    try testResolveWindows(&[_][]const u8{ "../..\\..", "\\rooted", "d:foo" }, "D:foo");
1225    try testResolveWindows(&[_][]const u8{ "C:\\foo", "../..\\..", "\\rooted", "d:foo" }, "D:foo");
1226    try testResolveWindows(&[_][]const u8{ "D:relevant", "../..\\..", "d:foo" }, "D:..\\..\\foo");
1227    try testResolveWindows(&[_][]const u8{ "D:relevant", "../..\\..", "\\\\.\\ignored", "C:\\ignored", "C:ignored", "\\\\ignored", "d:foo" }, "D:..\\..\\foo");
1228    try testResolveWindows(&[_][]const u8{ "ignored", "\\\\.\\ignored", "C:\\ignored", "C:ignored", "\\\\ignored", "d:foo" }, "D:foo");
1229    // Rooted paths remain rooted if there's no absolute path available to resolve the "root"
1230    try testResolveWindows(&[_][]const u8{ "/foo", "bar" }, "\\foo\\bar");
1231    // Rooted against a UNC path
1232    try testResolveWindows(&[_][]const u8{ "//server/share/ignore", "/foo", "bar" }, "\\\\server\\share\\foo\\bar");
1233    try testResolveWindows(&[_][]const u8{ "//server/share/", "/foo" }, "\\\\server\\share\\foo");
1234    try testResolveWindows(&[_][]const u8{ "//server/share", "/foo" }, "\\\\server\\share\\foo");
1235    try testResolveWindows(&[_][]const u8{ "//server/", "/foo" }, "\\\\server\\foo");
1236    try testResolveWindows(&[_][]const u8{ "//server", "/foo" }, "\\\\server\\foo");
1237    try testResolveWindows(&[_][]const u8{ "//", "/foo" }, "\\\\foo");
1238    // Rooted against a drive-relative path
1239    try testResolveWindows(&[_][]const u8{ "C:", "/foo", "bar" }, "C:\\foo\\bar");
1240    try testResolveWindows(&[_][]const u8{ "C:\\ignore", "C:", "/foo", "bar" }, "C:\\foo\\bar");
1241    try testResolveWindows(&[_][]const u8{ "C:\\ignore", "\\foo", "C:bar" }, "C:\\foo\\bar");
1242    // Only the last rooted path is relevant
1243    try testResolveWindows(&[_][]const u8{ "\\ignore", "\\foo" }, "\\foo");
1244    try testResolveWindows(&[_][]const u8{ "c:ignore", "ignore", "\\ignore", "\\foo" }, "C:\\foo");
1245    // Rooted is only relevant to a drive-relative if there's a previous drive-* path
1246    try testResolveWindows(&[_][]const u8{ "\\ignore", "C:foo" }, "C:foo");
1247    try testResolveWindows(&[_][]const u8{ "\\ignore", "\\ignore2", "C:foo" }, "C:foo");
1248    try testResolveWindows(&[_][]const u8{ "c:ignore", "\\ignore", "\\rooted", "C:foo" }, "C:\\rooted\\foo");
1249    try testResolveWindows(&[_][]const u8{ "c:\\ignore", "\\ignore", "\\rooted", "C:foo" }, "C:\\rooted\\foo");
1250    try testResolveWindows(&[_][]const u8{ "d:\\ignore", "\\ignore", "\\ignore2", "C:foo" }, "C:foo");
1251    // Root local device paths
1252    try testResolveWindows(&[_][]const u8{"\\/."}, "\\\\.");
1253    try testResolveWindows(&[_][]const u8{ "\\/.", "C:drive-relative" }, "C:drive-relative");
1254    try testResolveWindows(&[_][]const u8{"/\\?"}, "\\\\?");
1255    try testResolveWindows(&[_][]const u8{ "ignore", "c:\\ignore", "\\\\.", "foo" }, "\\\\.\\foo");
1256    try testResolveWindows(&[_][]const u8{ "ignore", "c:\\ignore", "\\\\?", "foo" }, "\\\\?\\foo");
1257    try testResolveWindows(&[_][]const u8{ "ignore", "c:\\ignore", "//.", "ignore", "\\foo" }, "\\\\.\\foo");
1258    try testResolveWindows(&[_][]const u8{ "ignore", "c:\\ignore", "\\\\?", "ignore", "\\foo" }, "\\\\?\\foo");
1259
1260    // Keep relative paths relative.
1261    try testResolveWindows(&[_][]const u8{"a/b"}, "a\\b");
1262    try testResolveWindows(&[_][]const u8{".."}, "..");
1263    try testResolveWindows(&[_][]const u8{"../.."}, "..\\..");
1264    try testResolveWindows(&[_][]const u8{ "C:foo", "../.." }, "C:..");
1265    try testResolveWindows(&[_][]const u8{ "d:foo", "../..\\.." }, "D:..\\..");
1266
1267    // Local device paths treat the \\.\ or \\?\ as the "root", everything afterwards is treated as a regular component.
1268    try testResolveWindows(&[_][]const u8{ "\\\\?\\C:\\foo", "../bar", "baz" }, "\\\\?\\C:\\bar\\baz");
1269    try testResolveWindows(&[_][]const u8{ "\\\\.\\C:/foo", "../../../../bar", "baz" }, "\\\\.\\bar\\baz");
1270    try testResolveWindows(&[_][]const u8{ "//./C:/foo", "../../../../bar", "baz" }, "\\\\.\\bar\\baz");
1271    try testResolveWindows(&[_][]const u8{ "\\\\.\\foo", ".." }, "\\\\.");
1272    try testResolveWindows(&[_][]const u8{ "\\\\.\\foo", "..\\.." }, "\\\\.");
1273
1274    // Paths are assumed to be Win32, so paths that are likely NT paths are treated as a rooted path.
1275    try testResolveWindows(&[_][]const u8{ "\\??\\C:\\foo", "/bar", "baz" }, "\\bar\\baz");
1276    try testResolveWindows(&[_][]const u8{ "C:\\", "\\??\\C:\\foo", "bar" }, "C:\\??\\C:\\foo\\bar");
1277}
1278
1279test resolvePosix {
1280    try testResolvePosix(&.{ "/a/b", "c" }, "/a/b/c");
1281    try testResolvePosix(&.{ "/a/b", "c", "//d", "e///" }, "/d/e");
1282    try testResolvePosix(&.{ "/a/b/c", "..", "../" }, "/a");
1283    try testResolvePosix(&.{ "/", "..", ".." }, "/");
1284    try testResolvePosix(&.{"/a/b/c/"}, "/a/b/c");
1285
1286    try testResolvePosix(&.{ "/var/lib", "../", "file/" }, "/var/file");
1287    try testResolvePosix(&.{ "/var/lib", "/../", "file/" }, "/file");
1288    try testResolvePosix(&.{ "/some/dir", ".", "/absolute/" }, "/absolute");
1289    try testResolvePosix(&.{ "/foo/tmp.3/", "../tmp.3/cycles/root.js" }, "/foo/tmp.3/cycles/root.js");
1290
1291    // Keep relative paths relative.
1292    try testResolvePosix(&.{"a/b"}, "a/b");
1293    try testResolvePosix(&.{"."}, ".");
1294    try testResolvePosix(&.{ ".", "src/test.zig", "..", "../test/cases.zig" }, "test/cases.zig");
1295}
1296
1297fn testResolveWindows(paths: []const []const u8, expected: []const u8) !void {
1298    const actual = try resolveWindows(testing.allocator, paths);
1299    defer testing.allocator.free(actual);
1300    try testing.expectEqualStrings(expected, actual);
1301}
1302
1303fn testResolvePosix(paths: []const []const u8, expected: []const u8) !void {
1304    const actual = try resolvePosix(testing.allocator, paths);
1305    defer testing.allocator.free(actual);
1306    try testing.expectEqualStrings(expected, actual);
1307}
1308
1309/// Strip the last component from a file path.
1310///
1311/// If the path is a file in the current directory (no directory component)
1312/// then returns null.
1313///
1314/// If the path is the root directory, returns null.
1315pub fn dirname(path: []const u8) ?[]const u8 {
1316    if (native_os == .windows) {
1317        return dirnameWindows(path);
1318    } else {
1319        return dirnamePosix(path);
1320    }
1321}
1322
1323pub fn dirnameWindows(path: []const u8) ?[]const u8 {
1324    return dirnameInner(.windows, path);
1325}
1326
1327pub fn dirnamePosix(path: []const u8) ?[]const u8 {
1328    return dirnameInner(.posix, path);
1329}
1330
1331fn dirnameInner(comptime path_type: PathType, path: []const u8) ?[]const u8 {
1332    var it = ComponentIterator(path_type, u8).init(path);
1333    _ = it.last() orelse return null;
1334    const up = it.previous() orelse return it.root();
1335    return up.path;
1336}
1337
1338test dirnamePosix {
1339    try testDirnamePosix("/a/b/c", "/a/b");
1340    try testDirnamePosix("/a/b/c///", "/a/b");
1341    try testDirnamePosix("/a", "/");
1342    try testDirnamePosix("/", null);
1343    try testDirnamePosix("//", null);
1344    try testDirnamePosix("///", null);
1345    try testDirnamePosix("////", null);
1346    try testDirnamePosix("", null);
1347    try testDirnamePosix("a", null);
1348    try testDirnamePosix("a/", null);
1349    try testDirnamePosix("a//", null);
1350}
1351
1352test dirnameWindows {
1353    try testDirnameWindows("c:\\", null);
1354    try testDirnameWindows("c:\\\\", null);
1355    try testDirnameWindows("c:\\foo", "c:\\");
1356    try testDirnameWindows("c:\\\\foo\\", "c:\\");
1357    try testDirnameWindows("c:\\foo\\bar", "c:\\foo");
1358    try testDirnameWindows("c:\\foo\\bar\\", "c:\\foo");
1359    try testDirnameWindows("c:\\\\foo\\bar\\baz", "c:\\\\foo\\bar");
1360    try testDirnameWindows("\\", null);
1361    try testDirnameWindows("\\foo", "\\");
1362    try testDirnameWindows("\\foo\\", "\\");
1363    try testDirnameWindows("\\foo\\bar", "\\foo");
1364    try testDirnameWindows("\\foo\\bar\\", "\\foo");
1365    try testDirnameWindows("\\foo\\bar\\baz", "\\foo\\bar");
1366    try testDirnameWindows("c:", null);
1367    try testDirnameWindows("c:foo", "c:");
1368    try testDirnameWindows("c:foo\\", "c:");
1369    try testDirnameWindows("c:foo\\bar", "c:foo");
1370    try testDirnameWindows("c:foo\\bar\\", "c:foo");
1371    try testDirnameWindows("c:foo\\bar\\baz", "c:foo\\bar");
1372    try testDirnameWindows("file:stream", null);
1373    try testDirnameWindows("dir\\file:stream", "dir");
1374    try testDirnameWindows("\\\\unc\\share", null);
1375    try testDirnameWindows("\\\\unc\\share\\\\", null);
1376    try testDirnameWindows("\\\\unc\\share\\foo", "\\\\unc\\share\\");
1377    try testDirnameWindows("\\\\unc\\share\\foo\\", "\\\\unc\\share\\");
1378    try testDirnameWindows("\\\\unc\\share\\foo\\bar", "\\\\unc\\share\\foo");
1379    try testDirnameWindows("\\\\unc\\share\\foo\\bar\\", "\\\\unc\\share\\foo");
1380    try testDirnameWindows("\\\\unc\\share\\foo\\bar\\baz", "\\\\unc\\share\\foo\\bar");
1381    try testDirnameWindows("\\\\.", null);
1382    try testDirnameWindows("\\\\.\\", null);
1383    try testDirnameWindows("\\\\.\\device", "\\\\.\\");
1384    try testDirnameWindows("\\\\.\\device\\", "\\\\.\\");
1385    try testDirnameWindows("\\\\.\\device\\foo", "\\\\.\\device");
1386    try testDirnameWindows("\\\\?", null);
1387    try testDirnameWindows("\\\\?\\", null);
1388    try testDirnameWindows("\\\\?\\device", "\\\\?\\");
1389    try testDirnameWindows("\\\\?\\device\\", "\\\\?\\");
1390    try testDirnameWindows("\\\\?\\device\\foo", "\\\\?\\device");
1391    try testDirnameWindows("/a/b/", "/a");
1392    try testDirnameWindows("/a/b", "/a");
1393    try testDirnameWindows("/a", "/");
1394    try testDirnameWindows("", null);
1395    try testDirnameWindows("/", null);
1396    try testDirnameWindows("////", null);
1397    try testDirnameWindows("foo", null);
1398}
1399
1400fn testDirnamePosix(input: []const u8, expected_output: ?[]const u8) !void {
1401    if (dirnamePosix(input)) |output| {
1402        try testing.expect(mem.eql(u8, output, expected_output.?));
1403    } else {
1404        try testing.expect(expected_output == null);
1405    }
1406}
1407
1408fn testDirnameWindows(input: []const u8, expected_output: ?[]const u8) !void {
1409    if (dirnameWindows(input)) |output| {
1410        try testing.expectEqualStrings(expected_output.?, output);
1411    } else {
1412        try testing.expect(expected_output == null);
1413    }
1414}
1415
1416pub fn basename(path: []const u8) []const u8 {
1417    if (native_os == .windows) {
1418        return basenameWindows(path);
1419    } else {
1420        return basenamePosix(path);
1421    }
1422}
1423
1424pub fn basenamePosix(path: []const u8) []const u8 {
1425    return basenameInner(.posix, path);
1426}
1427
1428pub fn basenameWindows(path: []const u8) []const u8 {
1429    return basenameInner(.windows, path);
1430}
1431
1432fn basenameInner(comptime path_type: PathType, path: []const u8) []const u8 {
1433    var it = ComponentIterator(path_type, u8).init(path);
1434    const last = it.last() orelse return &[_]u8{};
1435    return last.name;
1436}
1437
1438test basename {
1439    try testBasename("", "");
1440    try testBasename("/", "");
1441    try testBasename("/dir/basename.ext", "basename.ext");
1442    try testBasename("/basename.ext", "basename.ext");
1443    try testBasename("basename.ext", "basename.ext");
1444    try testBasename("basename.ext/", "basename.ext");
1445    try testBasename("basename.ext//", "basename.ext");
1446    try testBasename("/aaa/bbb", "bbb");
1447    try testBasename("/aaa/", "aaa");
1448    try testBasename("/aaa/b", "b");
1449    try testBasename("/a/b", "b");
1450
1451    // For Windows, this is a UNC path that only has a server name component.
1452    try testBasename("//a", if (native_os == .windows) "" else "a");
1453
1454    try testBasenamePosix("\\dir\\basename.ext", "\\dir\\basename.ext");
1455    try testBasenamePosix("\\basename.ext", "\\basename.ext");
1456    try testBasenamePosix("basename.ext", "basename.ext");
1457    try testBasenamePosix("basename.ext\\", "basename.ext\\");
1458    try testBasenamePosix("basename.ext\\\\", "basename.ext\\\\");
1459    try testBasenamePosix("foo", "foo");
1460
1461    try testBasenameWindows("\\dir\\basename.ext", "basename.ext");
1462    try testBasenameWindows("\\basename.ext", "basename.ext");
1463    try testBasenameWindows("basename.ext", "basename.ext");
1464    try testBasenameWindows("basename.ext\\", "basename.ext");
1465    try testBasenameWindows("basename.ext\\\\", "basename.ext");
1466    try testBasenameWindows("foo", "foo");
1467    try testBasenameWindows("C:", "");
1468    try testBasenameWindows("C:.", ".");
1469    try testBasenameWindows("C:\\", "");
1470    try testBasenameWindows("C:\\dir\\base.ext", "base.ext");
1471    try testBasenameWindows("C:\\basename.ext", "basename.ext");
1472    try testBasenameWindows("C:basename.ext", "basename.ext");
1473    try testBasenameWindows("C:basename.ext\\", "basename.ext");
1474    try testBasenameWindows("C:basename.ext\\\\", "basename.ext");
1475    try testBasenameWindows("\\\\.", "");
1476    try testBasenameWindows("\\\\.\\", "");
1477    try testBasenameWindows("\\\\.\\basename.ext", "basename.ext");
1478    try testBasenameWindows("\\\\?", "");
1479    try testBasenameWindows("\\\\?\\", "");
1480    try testBasenameWindows("\\\\?\\basename.ext", "basename.ext");
1481    try testBasenameWindows("C:foo", "foo");
1482    try testBasenameWindows("file:stream", "file:stream");
1483}
1484
1485fn testBasename(input: []const u8, expected_output: []const u8) !void {
1486    try testing.expectEqualSlices(u8, expected_output, basename(input));
1487}
1488
1489fn testBasenamePosix(input: []const u8, expected_output: []const u8) !void {
1490    try testing.expectEqualSlices(u8, expected_output, basenamePosix(input));
1491}
1492
1493fn testBasenameWindows(input: []const u8, expected_output: []const u8) !void {
1494    try testing.expectEqualSlices(u8, expected_output, basenameWindows(input));
1495}
1496
1497pub const RelativeError = std.process.GetCwdAllocError;
1498
1499/// Returns the relative path from `from` to `to`. If `from` and `to` each
1500/// resolve to the same path (after calling `resolve` on each), a zero-length
1501/// string is returned.
1502/// On Windows, the result is not guaranteed to be relative, as the paths may be
1503/// on different volumes. In that case, the result will be the canonicalized absolute
1504/// path of `to`.
1505pub fn relative(allocator: Allocator, from: []const u8, to: []const u8) RelativeError![]u8 {
1506    if (native_os == .windows) {
1507        return relativeWindows(allocator, from, to);
1508    } else {
1509        return relativePosix(allocator, from, to);
1510    }
1511}
1512
1513pub fn relativeWindows(allocator: Allocator, from: []const u8, to: []const u8) ![]u8 {
1514    if (native_os != .windows) @compileError("this function relies on Windows-specific semantics");
1515
1516    const parsed_from = parsePathWindows(u8, from);
1517    const parsed_to = parsePathWindows(u8, to);
1518
1519    const result_is_always_to = x: {
1520        if (parsed_from.kind != parsed_to.kind) {
1521            break :x false;
1522        }
1523        switch (parsed_from.kind) {
1524            .drive_relative, .drive_absolute => {
1525                break :x !compareDiskDesignators(u8, .drive, parsed_from.root, parsed_to.root);
1526            },
1527            .unc_absolute => {
1528                break :x !compareDiskDesignators(u8, .unc, parsed_from.root, parsed_to.root);
1529            },
1530            .relative, .rooted, .local_device => break :x false,
1531            .root_local_device => break :x true,
1532        }
1533    };
1534
1535    if (result_is_always_to) {
1536        return windowsResolveAgainstCwd(allocator, to, parsed_to);
1537    }
1538
1539    const resolved_from = try windowsResolveAgainstCwd(allocator, from, parsed_from);
1540    defer allocator.free(resolved_from);
1541    var clean_up_resolved_to = true;
1542    const resolved_to = try windowsResolveAgainstCwd(allocator, to, parsed_to);
1543    defer if (clean_up_resolved_to) allocator.free(resolved_to);
1544
1545    const parsed_resolved_from = parsePathWindows(u8, resolved_from);
1546    const parsed_resolved_to = parsePathWindows(u8, resolved_to);
1547
1548    const result_is_to = x: {
1549        if (parsed_resolved_from.kind != parsed_resolved_to.kind) {
1550            break :x true;
1551        }
1552        switch (parsed_resolved_from.kind) {
1553            .drive_absolute, .drive_relative => {
1554                break :x !compareDiskDesignators(u8, .drive, parsed_resolved_from.root, parsed_resolved_to.root);
1555            },
1556            .unc_absolute => {
1557                break :x !compareDiskDesignators(u8, .unc, parsed_resolved_from.root, parsed_resolved_to.root);
1558            },
1559            .relative, .rooted, .local_device => break :x false,
1560            .root_local_device => break :x true,
1561        }
1562    };
1563
1564    if (result_is_to) {
1565        clean_up_resolved_to = false;
1566        return resolved_to;
1567    }
1568
1569    var from_it = mem.tokenizeAny(u8, resolved_from[parsed_resolved_from.root.len..], "/\\");
1570    var to_it = mem.tokenizeAny(u8, resolved_to[parsed_resolved_to.root.len..], "/\\");
1571    while (true) {
1572        const from_component = from_it.next() orelse return allocator.dupe(u8, to_it.rest());
1573        const to_rest = to_it.rest();
1574        if (to_it.next()) |to_component| {
1575            if (windows.eqlIgnoreCaseWtf8(from_component, to_component))
1576                continue;
1577        }
1578        var up_index_end = "..".len;
1579        while (from_it.next()) |_| {
1580            up_index_end += "\\..".len;
1581        }
1582        const result = try allocator.alloc(u8, up_index_end + @intFromBool(to_rest.len > 0) + to_rest.len);
1583        errdefer allocator.free(result);
1584
1585        result[0..2].* = "..".*;
1586        var result_index: usize = 2;
1587        while (result_index < up_index_end) {
1588            result[result_index..][0..3].* = "\\..".*;
1589            result_index += 3;
1590        }
1591
1592        var rest_it = mem.tokenizeAny(u8, to_rest, "/\\");
1593        while (rest_it.next()) |to_component| {
1594            result[result_index] = '\\';
1595            result_index += 1;
1596            @memcpy(result[result_index..][0..to_component.len], to_component);
1597            result_index += to_component.len;
1598        }
1599
1600        return allocator.realloc(result, result_index);
1601    }
1602    return [_]u8{};
1603}
1604
1605fn windowsResolveAgainstCwd(allocator: Allocator, path: []const u8, parsed: WindowsPath2(u8)) ![]u8 {
1606    // Space for 256 WTF-16 code units; potentially 3 WTF-8 bytes per WTF-16 code unit
1607    var temp_allocator_state = std.heap.stackFallback(256 * 3, allocator);
1608    return switch (parsed.kind) {
1609        .drive_absolute,
1610        .unc_absolute,
1611        .root_local_device,
1612        .local_device,
1613        => try resolveWindows(allocator, &.{path}),
1614        .relative => blk: {
1615            const temp_allocator = temp_allocator_state.get();
1616
1617            const peb_cwd = windows.peb().ProcessParameters.CurrentDirectory.DosPath;
1618            const cwd_w = (peb_cwd.Buffer.?)[0 .. peb_cwd.Length / 2];
1619
1620            const wtf8_len = std.unicode.calcWtf8Len(cwd_w);
1621            const wtf8_buf = try temp_allocator.alloc(u8, wtf8_len);
1622            defer temp_allocator.free(wtf8_buf);
1623            assert(std.unicode.wtf16LeToWtf8(wtf8_buf, cwd_w) == wtf8_len);
1624
1625            break :blk try resolveWindows(allocator, &.{ wtf8_buf, path });
1626        },
1627        .rooted => blk: {
1628            const peb_cwd = windows.peb().ProcessParameters.CurrentDirectory.DosPath;
1629            const cwd_w = (peb_cwd.Buffer.?)[0 .. peb_cwd.Length / 2];
1630            const parsed_cwd = parsePathWindows(u16, cwd_w);
1631            switch (parsed_cwd.kind) {
1632                .drive_absolute => {
1633                    var drive_buf = "_:\\".*;
1634                    drive_buf[0] = @truncate(cwd_w[0]);
1635                    break :blk try resolveWindows(allocator, &.{ &drive_buf, path });
1636                },
1637                .unc_absolute => {
1638                    const temp_allocator = temp_allocator_state.get();
1639                    var root_buf = try temp_allocator.alloc(u8, parsed_cwd.root.len * 3);
1640                    defer temp_allocator.free(root_buf);
1641
1642                    const wtf8_len = std.unicode.wtf16LeToWtf8(root_buf, parsed_cwd.root);
1643                    const root = root_buf[0..wtf8_len];
1644                    break :blk try resolveWindows(allocator, &.{ root, path });
1645                },
1646                // Effectively a malformed CWD, give up and just return a normalized path
1647                else => break :blk try resolveWindows(allocator, &.{path}),
1648            }
1649        },
1650        .drive_relative => blk: {
1651            const temp_allocator = temp_allocator_state.get();
1652            const drive_cwd = drive_cwd: {
1653                const peb_cwd = windows.peb().ProcessParameters.CurrentDirectory.DosPath;
1654                const cwd_w = (peb_cwd.Buffer.?)[0 .. peb_cwd.Length / 2];
1655                const parsed_cwd = parsePathWindows(u16, cwd_w);
1656
1657                if (parsed_cwd.kind == .drive_absolute) {
1658                    const drive_letter_w = parsed_cwd.root[0];
1659                    const drive_letters_match = drive_letter_w <= 0x7F and
1660                        ascii.toUpper(@intCast(drive_letter_w)) == ascii.toUpper(parsed.root[0]);
1661                    if (drive_letters_match) {
1662                        const wtf8_len = std.unicode.calcWtf8Len(cwd_w);
1663                        const wtf8_buf = try temp_allocator.alloc(u8, wtf8_len);
1664                        assert(std.unicode.wtf16LeToWtf8(wtf8_buf, cwd_w) == wtf8_len);
1665                        break :drive_cwd wtf8_buf[0..];
1666                    }
1667
1668                    // Per-drive CWD's are stored in special semi-hidden environment variables
1669                    // of the format `=<drive-letter>:`, e.g. `=C:`. This type of CWD is
1670                    // purely a shell concept, so there's no guarantee that it'll be set
1671                    // or that it'll even be accurate.
1672                    var key_buf = std.unicode.wtf8ToWtf16LeStringLiteral("=_:").*;
1673                    key_buf[1] = parsed.root[0];
1674                    if (std.process.getenvW(&key_buf)) |drive_cwd_w| {
1675                        const wtf8_len = std.unicode.calcWtf8Len(drive_cwd_w);
1676                        const wtf8_buf = try temp_allocator.alloc(u8, wtf8_len);
1677                        assert(std.unicode.wtf16LeToWtf8(wtf8_buf, drive_cwd_w) == wtf8_len);
1678                        break :drive_cwd wtf8_buf[0..];
1679                    }
1680                }
1681
1682                const drive_buf = try temp_allocator.alloc(u8, 3);
1683                drive_buf[0] = parsed.root[0];
1684                drive_buf[1] = ':';
1685                drive_buf[2] = '\\';
1686                break :drive_cwd drive_buf;
1687            };
1688            defer temp_allocator.free(drive_cwd);
1689            break :blk try resolveWindows(allocator, &.{ drive_cwd, path });
1690        },
1691    };
1692}
1693
1694pub fn relativePosix(allocator: Allocator, from: []const u8, to: []const u8) ![]u8 {
1695    if (native_os == .windows) @compileError("this function relies on semantics that do not apply to Windows");
1696
1697    const cwd = try process.getCwdAlloc(allocator);
1698    defer allocator.free(cwd);
1699    const resolved_from = try resolvePosix(allocator, &[_][]const u8{ cwd, from });
1700    defer allocator.free(resolved_from);
1701    const resolved_to = try resolvePosix(allocator, &[_][]const u8{ cwd, to });
1702    defer allocator.free(resolved_to);
1703
1704    var from_it = mem.tokenizeScalar(u8, resolved_from, '/');
1705    var to_it = mem.tokenizeScalar(u8, resolved_to, '/');
1706    while (true) {
1707        const from_component = from_it.next() orelse return allocator.dupe(u8, to_it.rest());
1708        const to_rest = to_it.rest();
1709        if (to_it.next()) |to_component| {
1710            if (mem.eql(u8, from_component, to_component))
1711                continue;
1712        }
1713        var up_count: usize = 1;
1714        while (from_it.next()) |_| {
1715            up_count += 1;
1716        }
1717        const up_index_end = up_count * "../".len;
1718        const result = try allocator.alloc(u8, up_index_end + to_rest.len);
1719        errdefer allocator.free(result);
1720
1721        var result_index: usize = 0;
1722        while (result_index < up_index_end) {
1723            result[result_index..][0..3].* = "../".*;
1724            result_index += 3;
1725        }
1726        if (to_rest.len == 0) {
1727            // shave off the trailing slash
1728            return allocator.realloc(result, result_index - 1);
1729        }
1730
1731        @memcpy(result[result_index..][0..to_rest.len], to_rest);
1732        return result;
1733    }
1734
1735    return [_]u8{};
1736}
1737
1738test relative {
1739    if (native_os == .windows) {
1740        try testRelativeWindows("c:/blah\\blah", "d:/games", "D:\\games");
1741        try testRelativeWindows("c:/aaaa/bbbb", "c:/aaaa", "..");
1742        try testRelativeWindows("c:/aaaa/bbbb", "c:/cccc", "..\\..\\cccc");
1743        try testRelativeWindows("c:/aaaa/bbbb", "C:/aaaa/bbbb", "");
1744        try testRelativeWindows("c:/aaaa/bbbb", "c:/aaaa/cccc", "..\\cccc");
1745        try testRelativeWindows("c:/aaaa/", "c:/aaaa/cccc", "cccc");
1746        try testRelativeWindows("c:/", "c:\\aaaa\\bbbb", "aaaa\\bbbb");
1747        try testRelativeWindows("c:/aaaa/bbbb", "d:\\", "D:\\");
1748        try testRelativeWindows("c:/AaAa/bbbb", "c:/aaaa/bbbb", "");
1749        try testRelativeWindows("c:/aaaaa/", "c:/aaaa/cccc", "..\\aaaa\\cccc");
1750        try testRelativeWindows("C:\\foo\\bar\\baz\\quux", "C:\\", "..\\..\\..\\..");
1751        try testRelativeWindows("C:\\foo\\test", "C:\\foo\\test\\bar\\package.json", "bar\\package.json");
1752        try testRelativeWindows("C:\\foo\\bar\\baz-quux", "C:\\foo\\bar\\baz", "..\\baz");
1753        try testRelativeWindows("C:\\foo\\bar\\baz", "C:\\foo\\bar\\baz-quux", "..\\baz-quux");
1754        try testRelativeWindows("\\\\foo\\bar", "\\\\foo\\bar\\baz", "baz");
1755        try testRelativeWindows("\\\\foo\\bar\\baz", "\\\\foo\\bar", "..");
1756        try testRelativeWindows("\\\\foo\\bar\\baz-quux", "\\\\foo\\bar\\baz", "..\\baz");
1757        try testRelativeWindows("\\\\foo/bar\\baz-quux", "//foo\\bar/baz", "..\\baz");
1758        try testRelativeWindows("\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz-quux", "..\\baz-quux");
1759        try testRelativeWindows("C:\\baz-quux", "C:\\baz", "..\\baz");
1760        try testRelativeWindows("C:\\baz", "C:\\baz-quux", "..\\baz-quux");
1761        try testRelativeWindows("\\\\foo\\baz-quux", "\\\\foo\\baz", "\\\\foo\\baz");
1762        try testRelativeWindows("\\\\foo\\baz", "\\\\foo\\baz-quux", "\\\\foo\\baz-quux");
1763        try testRelativeWindows("C:\\baz", "\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz");
1764        try testRelativeWindows("\\\\foo\\bar\\baz", "C:\\baz", "C:\\baz");
1765
1766        try testRelativeWindows("c:blah\\blah", "c:foo", "..\\..\\foo");
1767        try testRelativeWindows("c:foo", "c:foo\\bar", "bar");
1768        try testRelativeWindows("\\blah\\blah", "\\foo", "..\\..\\foo");
1769        try testRelativeWindows("\\foo", "\\foo\\bar", "bar");
1770
1771        try testRelativeWindows("a/b/c", "a\\b", "..");
1772        try testRelativeWindows("a/b/c", "a", "..\\..");
1773        try testRelativeWindows("a/b/c", "a\\b\\c\\d", "d");
1774
1775        try testRelativeWindows("\\\\FOO\\bar\\baz", "\\\\foo\\BAR\\BAZ", "");
1776        // Unicode-aware case-insensitive path comparison
1777        try testRelativeWindows("\\\\кириллица\\ελληνικά\\português", "\\\\КИРИЛЛИЦА\\ΕΛΛΗΝΙΚΆ\\PORTUGUÊS", "");
1778    } else {
1779        try testRelativePosix("/var/lib", "/var", "..");
1780        try testRelativePosix("/var/lib", "/bin", "../../bin");
1781        try testRelativePosix("/var/lib", "/var/lib", "");
1782        try testRelativePosix("/var/lib", "/var/apache", "../apache");
1783        try testRelativePosix("/var/", "/var/lib", "lib");
1784        try testRelativePosix("/", "/var/lib", "var/lib");
1785        try testRelativePosix("/foo/test", "/foo/test/bar/package.json", "bar/package.json");
1786        try testRelativePosix("/Users/a/web/b/test/mails", "/Users/a/web/b", "../..");
1787        try testRelativePosix("/foo/bar/baz-quux", "/foo/bar/baz", "../baz");
1788        try testRelativePosix("/foo/bar/baz", "/foo/bar/baz-quux", "../baz-quux");
1789        try testRelativePosix("/baz-quux", "/baz", "../baz");
1790        try testRelativePosix("/baz", "/baz-quux", "../baz-quux");
1791    }
1792}
1793
1794fn testRelativePosix(from: []const u8, to: []const u8, expected_output: []const u8) !void {
1795    const result = try relativePosix(testing.allocator, from, to);
1796    defer testing.allocator.free(result);
1797    try testing.expectEqualStrings(expected_output, result);
1798}
1799
1800fn testRelativeWindows(from: []const u8, to: []const u8, expected_output: []const u8) !void {
1801    const result = try relativeWindows(testing.allocator, from, to);
1802    defer testing.allocator.free(result);
1803    try testing.expectEqualStrings(expected_output, result);
1804}
1805
1806/// Searches for a file extension separated by a `.` and returns the string after that `.`.
1807/// Files that end or start with `.` and have no other `.` in their name
1808/// are considered to have no extension, in which case this returns "".
1809/// Examples:
1810/// - `"main.zig"`      ⇒ `".zig"`
1811/// - `"src/main.zig"`  ⇒ `".zig"`
1812/// - `".gitignore"`    ⇒ `""`
1813/// - `".image.png"`    ⇒ `".png"`
1814/// - `"keep."`         ⇒ `"."`
1815/// - `"src.keep.me"`   ⇒ `".me"`
1816/// - `"/src/keep.me"`  ⇒ `".me"`
1817/// - `"/src/keep.me/"` ⇒ `".me"`
1818/// The returned slice is guaranteed to have its pointer within the start and end
1819/// pointer address range of `path`, even if it is length zero.
1820pub fn extension(path: []const u8) []const u8 {
1821    const filename = basename(path);
1822    const index = mem.lastIndexOfScalar(u8, filename, '.') orelse return path[path.len..];
1823    if (index == 0) return path[path.len..];
1824    return filename[index..];
1825}
1826
1827fn testExtension(path: []const u8, expected: []const u8) !void {
1828    try testing.expectEqualStrings(expected, extension(path));
1829}
1830
1831test extension {
1832    try testExtension("", "");
1833    try testExtension(".", "");
1834    try testExtension("a.", ".");
1835    try testExtension("abc.", ".");
1836    try testExtension(".a", "");
1837    try testExtension(".file", "");
1838    try testExtension(".gitignore", "");
1839    try testExtension(".image.png", ".png");
1840    try testExtension("file.ext", ".ext");
1841    try testExtension("file.ext.", ".");
1842    try testExtension("very-long-file.bruh", ".bruh");
1843    try testExtension("a.b.c", ".c");
1844    try testExtension("a.b.c/", ".c");
1845
1846    try testExtension("/", "");
1847    try testExtension("/.", "");
1848    try testExtension("/a.", ".");
1849    try testExtension("/abc.", ".");
1850    try testExtension("/.a", "");
1851    try testExtension("/.file", "");
1852    try testExtension("/.gitignore", "");
1853    try testExtension("/file.ext", ".ext");
1854    try testExtension("/file.ext.", ".");
1855    try testExtension("/very-long-file.bruh", ".bruh");
1856    try testExtension("/a.b.c", ".c");
1857    try testExtension("/a.b.c/", ".c");
1858
1859    try testExtension("/foo/bar/bam/", "");
1860    try testExtension("/foo/bar/bam/.", "");
1861    try testExtension("/foo/bar/bam/a.", ".");
1862    try testExtension("/foo/bar/bam/abc.", ".");
1863    try testExtension("/foo/bar/bam/.a", "");
1864    try testExtension("/foo/bar/bam/.file", "");
1865    try testExtension("/foo/bar/bam/.gitignore", "");
1866    try testExtension("/foo/bar/bam/file.ext", ".ext");
1867    try testExtension("/foo/bar/bam/file.ext.", ".");
1868    try testExtension("/foo/bar/bam/very-long-file.bruh", ".bruh");
1869    try testExtension("/foo/bar/bam/a.b.c", ".c");
1870    try testExtension("/foo/bar/bam/a.b.c/", ".c");
1871}
1872
1873/// Returns the last component of this path without its extension (if any):
1874/// - "hello/world/lib.tar.gz" ⇒ "lib.tar"
1875/// - "hello/world/lib.tar"    ⇒ "lib"
1876/// - "hello/world/lib"        ⇒ "lib"
1877pub fn stem(path: []const u8) []const u8 {
1878    const filename = basename(path);
1879    const index = mem.lastIndexOfScalar(u8, filename, '.') orelse return filename[0..];
1880    if (index == 0) return path;
1881    return filename[0..index];
1882}
1883
1884fn testStem(path: []const u8, expected: []const u8) !void {
1885    try testing.expectEqualStrings(expected, stem(path));
1886}
1887
1888test stem {
1889    try testStem("hello/world/lib.tar.gz", "lib.tar");
1890    try testStem("hello/world/lib.tar", "lib");
1891    try testStem("hello/world/lib", "lib");
1892    try testStem("hello/lib/", "lib");
1893    try testStem("hello...", "hello..");
1894    try testStem("hello.", "hello");
1895    try testStem("/hello.", "hello");
1896    try testStem(".gitignore", ".gitignore");
1897    try testStem(".image.png", ".image");
1898    try testStem("file.ext", "file");
1899    try testStem("file.ext.", "file.ext");
1900    try testStem("a.b.c", "a.b");
1901    try testStem("a.b.c/", "a.b");
1902    try testStem(".a", ".a");
1903    try testStem("///", "");
1904    try testStem("..", ".");
1905    try testStem(".", ".");
1906    try testStem(" ", " ");
1907    try testStem("", "");
1908}
1909
1910/// A path component iterator that can move forwards and backwards.
1911/// The 'root' of the path (`/` for POSIX, things like `C:\`, `\\server\share\`, etc
1912/// for Windows) is treated specially and will never be returned by any of the
1913/// `first`, `last`, `next`, or `previous` functions.
1914/// Multiple consecutive path separators are skipped (treated as a single separator)
1915/// when iterating.
1916/// All returned component names/paths are slices of the original path.
1917/// There is no normalization of paths performed while iterating.
1918pub fn ComponentIterator(comptime path_type: PathType, comptime T: type) type {
1919    return struct {
1920        path: []const T,
1921        /// Length of the root with at most one trailing path separator included (e.g. `C:/`).
1922        root_len: usize,
1923        /// Length of the root with all trailing path separators included (e.g. `C://///`).
1924        root_end_index: usize,
1925        start_index: usize = 0,
1926        end_index: usize = 0,
1927
1928        const Self = @This();
1929
1930        pub const Component = struct {
1931            /// The current component's path name, e.g. 'b'.
1932            /// This will never contain path separators.
1933            name: []const T,
1934            /// The full path up to and including the current component, e.g. '/a/b'
1935            /// This will never contain trailing path separators.
1936            path: []const T,
1937        };
1938
1939        /// After `init`, `next` will return the first component after the root
1940        /// (there is no need to call `first` after `init`).
1941        /// To iterate backwards (from the end of the path to the beginning), call `last`
1942        /// after `init` and then iterate via `previous` calls.
1943        /// For Windows paths, paths are assumed to be in the Win32 namespace.
1944        pub fn init(path: []const T) Self {
1945            const root_len: usize = switch (path_type) {
1946                .posix, .uefi => posix: {
1947                    // Root on UEFI and POSIX only differs by the path separator
1948                    break :posix if (path.len > 0 and path_type.isSep(T, path[0])) 1 else 0;
1949                },
1950                .windows => windows: {
1951                    break :windows parsePathWindows(T, path).root.len;
1952                },
1953            };
1954            // If there are repeated path separators directly after the root,
1955            // keep track of that info so that they don't have to be dealt with when
1956            // iterating components.
1957            var root_end_index = root_len;
1958            for (path[root_len..]) |c| {
1959                if (!path_type.isSep(T, c)) break;
1960                root_end_index += 1;
1961            }
1962            return .{
1963                .path = path,
1964                .root_len = root_len,
1965                .root_end_index = root_end_index,
1966                .start_index = root_end_index,
1967                .end_index = root_end_index,
1968            };
1969        }
1970
1971        /// Returns the root of the path if it is not a relative path, or null otherwise.
1972        /// For POSIX paths, this will be `/`.
1973        /// For Windows paths, this will be something like `C:\`, `\\server\share\`, etc.
1974        /// For UEFI paths, this will be `\`.
1975        pub fn root(self: Self) ?[]const T {
1976            if (self.root_end_index == 0) return null;
1977            return self.path[0..self.root_len];
1978        }
1979
1980        /// Returns the first component (from the beginning of the path).
1981        /// For example, if the path is `/a/b/c` then this will return the `a` component.
1982        /// After calling `first`, `previous` will always return `null`, and `next` will return
1983        /// the component to the right of the one returned by `first`, if any exist.
1984        pub fn first(self: *Self) ?Component {
1985            self.start_index = self.root_end_index;
1986            self.end_index = self.start_index;
1987            while (self.end_index < self.path.len and !path_type.isSep(T, self.path[self.end_index])) {
1988                self.end_index += 1;
1989            }
1990            if (self.end_index == self.start_index) return null;
1991            return .{
1992                .name = self.path[self.start_index..self.end_index],
1993                .path = self.path[0..self.end_index],
1994            };
1995        }
1996
1997        /// Returns the last component (from the end of the path).
1998        /// For example, if the path is `/a/b/c` then this will return the `c` component.
1999        /// After calling `last`, `next` will always return `null`, and `previous` will return
2000        /// the component to the left of the one returned by `last`, if any exist.
2001        pub fn last(self: *Self) ?Component {
2002            self.end_index = self.path.len;
2003            while (true) {
2004                if (self.end_index == self.root_end_index) {
2005                    self.start_index = self.end_index;
2006                    return null;
2007                }
2008                if (!path_type.isSep(T, self.path[self.end_index - 1])) break;
2009                self.end_index -= 1;
2010            }
2011            self.start_index = self.end_index;
2012            while (true) {
2013                if (self.start_index == self.root_end_index) break;
2014                if (path_type.isSep(T, self.path[self.start_index - 1])) break;
2015                self.start_index -= 1;
2016            }
2017            if (self.start_index == self.end_index) return null;
2018            return .{
2019                .name = self.path[self.start_index..self.end_index],
2020                .path = self.path[0..self.end_index],
2021            };
2022        }
2023
2024        /// Returns the next component (the component to the right of the most recently
2025        /// returned component), or null if no such component exists.
2026        /// For example, if the path is `/a/b/c` and the most recently returned component
2027        /// is `b`, then this will return the `c` component.
2028        pub fn next(self: *Self) ?Component {
2029            const peek_result = self.peekNext() orelse return null;
2030            self.start_index = peek_result.path.len - peek_result.name.len;
2031            self.end_index = peek_result.path.len;
2032            return peek_result;
2033        }
2034
2035        /// Like `next`, but does not modify the iterator state.
2036        pub fn peekNext(self: Self) ?Component {
2037            var start_index = self.end_index;
2038            while (start_index < self.path.len and path_type.isSep(T, self.path[start_index])) {
2039                start_index += 1;
2040            }
2041            var end_index = start_index;
2042            while (end_index < self.path.len and !path_type.isSep(T, self.path[end_index])) {
2043                end_index += 1;
2044            }
2045            if (start_index == end_index) return null;
2046            return .{
2047                .name = self.path[start_index..end_index],
2048                .path = self.path[0..end_index],
2049            };
2050        }
2051
2052        /// Returns the previous component (the component to the left of the most recently
2053        /// returned component), or null if no such component exists.
2054        /// For example, if the path is `/a/b/c` and the most recently returned component
2055        /// is `b`, then this will return the `a` component.
2056        pub fn previous(self: *Self) ?Component {
2057            const peek_result = self.peekPrevious() orelse return null;
2058            self.start_index = peek_result.path.len - peek_result.name.len;
2059            self.end_index = peek_result.path.len;
2060            return peek_result;
2061        }
2062
2063        /// Like `previous`, but does not modify the iterator state.
2064        pub fn peekPrevious(self: Self) ?Component {
2065            var end_index = self.start_index;
2066            while (true) {
2067                if (end_index == self.root_end_index) return null;
2068                if (!path_type.isSep(T, self.path[end_index - 1])) break;
2069                end_index -= 1;
2070            }
2071            var start_index = end_index;
2072            while (true) {
2073                if (start_index == self.root_end_index) break;
2074                if (path_type.isSep(T, self.path[start_index - 1])) break;
2075                start_index -= 1;
2076            }
2077            if (start_index == end_index) return null;
2078            return .{
2079                .name = self.path[start_index..end_index],
2080                .path = self.path[0..end_index],
2081            };
2082        }
2083    };
2084}
2085
2086pub const NativeComponentIterator = ComponentIterator(switch (native_os) {
2087    .windows => .windows,
2088    .uefi => .uefi,
2089    else => .posix,
2090}, u8);
2091
2092pub fn componentIterator(path: []const u8) NativeComponentIterator {
2093    return NativeComponentIterator.init(path);
2094}
2095
2096test "ComponentIterator posix" {
2097    const PosixComponentIterator = ComponentIterator(.posix, u8);
2098    {
2099        const path = "a/b/c/";
2100        var it = PosixComponentIterator.init(path);
2101        try std.testing.expectEqual(0, it.root_len);
2102        try std.testing.expectEqual(0, it.root_end_index);
2103        try std.testing.expect(null == it.root());
2104        {
2105            try std.testing.expect(null == it.previous());
2106
2107            const first_via_next = it.next().?;
2108            try std.testing.expectEqualStrings("a", first_via_next.name);
2109            try std.testing.expectEqualStrings("a", first_via_next.path);
2110
2111            const first = it.first().?;
2112            try std.testing.expectEqualStrings("a", first.name);
2113            try std.testing.expectEqualStrings("a", first.path);
2114
2115            try std.testing.expect(null == it.previous());
2116
2117            const second = it.next().?;
2118            try std.testing.expectEqualStrings("b", second.name);
2119            try std.testing.expectEqualStrings("a/b", second.path);
2120
2121            const third = it.next().?;
2122            try std.testing.expectEqualStrings("c", third.name);
2123            try std.testing.expectEqualStrings("a/b/c", third.path);
2124
2125            try std.testing.expect(null == it.next());
2126        }
2127        {
2128            const last = it.last().?;
2129            try std.testing.expectEqualStrings("c", last.name);
2130            try std.testing.expectEqualStrings("a/b/c", last.path);
2131
2132            try std.testing.expect(null == it.next());
2133
2134            const second_to_last = it.previous().?;
2135            try std.testing.expectEqualStrings("b", second_to_last.name);
2136            try std.testing.expectEqualStrings("a/b", second_to_last.path);
2137
2138            const third_to_last = it.previous().?;
2139            try std.testing.expectEqualStrings("a", third_to_last.name);
2140            try std.testing.expectEqualStrings("a", third_to_last.path);
2141
2142            try std.testing.expect(null == it.previous());
2143        }
2144    }
2145
2146    {
2147        const path = "/a/b/c/";
2148        var it = PosixComponentIterator.init(path);
2149        try std.testing.expectEqual(1, it.root_len);
2150        try std.testing.expectEqual(1, it.root_end_index);
2151        try std.testing.expectEqualStrings("/", it.root().?);
2152        {
2153            try std.testing.expect(null == it.previous());
2154
2155            const first_via_next = it.next().?;
2156            try std.testing.expectEqualStrings("a", first_via_next.name);
2157            try std.testing.expectEqualStrings("/a", first_via_next.path);
2158
2159            const first = it.first().?;
2160            try std.testing.expectEqualStrings("a", first.name);
2161            try std.testing.expectEqualStrings("/a", first.path);
2162
2163            try std.testing.expect(null == it.previous());
2164
2165            const second = it.next().?;
2166            try std.testing.expectEqualStrings("b", second.name);
2167            try std.testing.expectEqualStrings("/a/b", second.path);
2168
2169            const third = it.next().?;
2170            try std.testing.expectEqualStrings("c", third.name);
2171            try std.testing.expectEqualStrings("/a/b/c", third.path);
2172
2173            try std.testing.expect(null == it.next());
2174        }
2175        {
2176            const last = it.last().?;
2177            try std.testing.expectEqualStrings("c", last.name);
2178            try std.testing.expectEqualStrings("/a/b/c", last.path);
2179
2180            try std.testing.expect(null == it.next());
2181
2182            const second_to_last = it.previous().?;
2183            try std.testing.expectEqualStrings("b", second_to_last.name);
2184            try std.testing.expectEqualStrings("/a/b", second_to_last.path);
2185
2186            const third_to_last = it.previous().?;
2187            try std.testing.expectEqualStrings("a", third_to_last.name);
2188            try std.testing.expectEqualStrings("/a", third_to_last.path);
2189
2190            try std.testing.expect(null == it.previous());
2191        }
2192    }
2193
2194    {
2195        const path = "////a///b///c////";
2196        var it = PosixComponentIterator.init(path);
2197        try std.testing.expectEqual(1, it.root_len);
2198        try std.testing.expectEqual(4, it.root_end_index);
2199        try std.testing.expectEqualStrings("/", it.root().?);
2200        {
2201            try std.testing.expect(null == it.previous());
2202
2203            const first_via_next = it.next().?;
2204            try std.testing.expectEqualStrings("a", first_via_next.name);
2205            try std.testing.expectEqualStrings("////a", first_via_next.path);
2206
2207            const first = it.first().?;
2208            try std.testing.expectEqualStrings("a", first.name);
2209            try std.testing.expectEqualStrings("////a", first.path);
2210
2211            try std.testing.expect(null == it.previous());
2212
2213            const second = it.next().?;
2214            try std.testing.expectEqualStrings("b", second.name);
2215            try std.testing.expectEqualStrings("////a///b", second.path);
2216
2217            const third = it.next().?;
2218            try std.testing.expectEqualStrings("c", third.name);
2219            try std.testing.expectEqualStrings("////a///b///c", third.path);
2220
2221            try std.testing.expect(null == it.next());
2222        }
2223        {
2224            const last = it.last().?;
2225            try std.testing.expectEqualStrings("c", last.name);
2226            try std.testing.expectEqualStrings("////a///b///c", last.path);
2227
2228            try std.testing.expect(null == it.next());
2229
2230            const second_to_last = it.previous().?;
2231            try std.testing.expectEqualStrings("b", second_to_last.name);
2232            try std.testing.expectEqualStrings("////a///b", second_to_last.path);
2233
2234            const third_to_last = it.previous().?;
2235            try std.testing.expectEqualStrings("a", third_to_last.name);
2236            try std.testing.expectEqualStrings("////a", third_to_last.path);
2237
2238            try std.testing.expect(null == it.previous());
2239        }
2240    }
2241
2242    {
2243        const path = "/";
2244        var it = PosixComponentIterator.init(path);
2245        try std.testing.expectEqual(1, it.root_len);
2246        try std.testing.expectEqual(1, it.root_end_index);
2247        try std.testing.expectEqualStrings("/", it.root().?);
2248
2249        try std.testing.expect(null == it.first());
2250        try std.testing.expect(null == it.previous());
2251        try std.testing.expect(null == it.first());
2252        try std.testing.expect(null == it.next());
2253
2254        try std.testing.expect(null == it.last());
2255        try std.testing.expect(null == it.previous());
2256        try std.testing.expect(null == it.last());
2257        try std.testing.expect(null == it.next());
2258    }
2259
2260    {
2261        const path = "";
2262        var it = PosixComponentIterator.init(path);
2263        try std.testing.expectEqual(0, it.root_len);
2264        try std.testing.expectEqual(0, it.root_end_index);
2265        try std.testing.expect(null == it.root());
2266
2267        try std.testing.expect(null == it.first());
2268        try std.testing.expect(null == it.previous());
2269        try std.testing.expect(null == it.first());
2270        try std.testing.expect(null == it.next());
2271
2272        try std.testing.expect(null == it.last());
2273        try std.testing.expect(null == it.previous());
2274        try std.testing.expect(null == it.last());
2275        try std.testing.expect(null == it.next());
2276    }
2277}
2278
2279test "ComponentIterator windows" {
2280    const WindowsComponentIterator = ComponentIterator(.windows, u8);
2281    {
2282        const path = "a/b\\c//";
2283        var it = WindowsComponentIterator.init(path);
2284        try std.testing.expectEqual(0, it.root_len);
2285        try std.testing.expectEqual(0, it.root_end_index);
2286        try std.testing.expect(null == it.root());
2287        {
2288            try std.testing.expect(null == it.previous());
2289
2290            const first_via_next = it.next().?;
2291            try std.testing.expectEqualStrings("a", first_via_next.name);
2292            try std.testing.expectEqualStrings("a", first_via_next.path);
2293
2294            const first = it.first().?;
2295            try std.testing.expectEqualStrings("a", first.name);
2296            try std.testing.expectEqualStrings("a", first.path);
2297
2298            try std.testing.expect(null == it.previous());
2299
2300            const second = it.next().?;
2301            try std.testing.expectEqualStrings("b", second.name);
2302            try std.testing.expectEqualStrings("a/b", second.path);
2303
2304            const third = it.next().?;
2305            try std.testing.expectEqualStrings("c", third.name);
2306            try std.testing.expectEqualStrings("a/b\\c", third.path);
2307
2308            try std.testing.expect(null == it.next());
2309        }
2310        {
2311            const last = it.last().?;
2312            try std.testing.expectEqualStrings("c", last.name);
2313            try std.testing.expectEqualStrings("a/b\\c", last.path);
2314
2315            try std.testing.expect(null == it.next());
2316
2317            const second_to_last = it.previous().?;
2318            try std.testing.expectEqualStrings("b", second_to_last.name);
2319            try std.testing.expectEqualStrings("a/b", second_to_last.path);
2320
2321            const third_to_last = it.previous().?;
2322            try std.testing.expectEqualStrings("a", third_to_last.name);
2323            try std.testing.expectEqualStrings("a", third_to_last.path);
2324
2325            try std.testing.expect(null == it.previous());
2326        }
2327    }
2328
2329    {
2330        const path = "C:\\a/b/c/";
2331        var it = WindowsComponentIterator.init(path);
2332        try std.testing.expectEqual(3, it.root_len);
2333        try std.testing.expectEqual(3, it.root_end_index);
2334        try std.testing.expectEqualStrings("C:\\", it.root().?);
2335        {
2336            const first = it.first().?;
2337            try std.testing.expectEqualStrings("a", first.name);
2338            try std.testing.expectEqualStrings("C:\\a", first.path);
2339
2340            const second = it.next().?;
2341            try std.testing.expectEqualStrings("b", second.name);
2342            try std.testing.expectEqualStrings("C:\\a/b", second.path);
2343
2344            const third = it.next().?;
2345            try std.testing.expectEqualStrings("c", third.name);
2346            try std.testing.expectEqualStrings("C:\\a/b/c", third.path);
2347
2348            try std.testing.expect(null == it.next());
2349        }
2350        {
2351            const last = it.last().?;
2352            try std.testing.expectEqualStrings("c", last.name);
2353            try std.testing.expectEqualStrings("C:\\a/b/c", last.path);
2354
2355            const second_to_last = it.previous().?;
2356            try std.testing.expectEqualStrings("b", second_to_last.name);
2357            try std.testing.expectEqualStrings("C:\\a/b", second_to_last.path);
2358
2359            const third_to_last = it.previous().?;
2360            try std.testing.expectEqualStrings("a", third_to_last.name);
2361            try std.testing.expectEqualStrings("C:\\a", third_to_last.path);
2362
2363            try std.testing.expect(null == it.previous());
2364        }
2365    }
2366
2367    {
2368        const path = "C:\\\\//a/\\/\\b///c////";
2369        var it = WindowsComponentIterator.init(path);
2370        try std.testing.expectEqual(3, it.root_len);
2371        try std.testing.expectEqual(6, it.root_end_index);
2372        try std.testing.expectEqualStrings("C:\\", it.root().?);
2373        {
2374            const first = it.first().?;
2375            try std.testing.expectEqualStrings("a", first.name);
2376            try std.testing.expectEqualStrings("C:\\\\//a", first.path);
2377
2378            const second = it.next().?;
2379            try std.testing.expectEqualStrings("b", second.name);
2380            try std.testing.expectEqualStrings("C:\\\\//a/\\/\\b", second.path);
2381
2382            const third = it.next().?;
2383            try std.testing.expectEqualStrings("c", third.name);
2384            try std.testing.expectEqualStrings("C:\\\\//a/\\/\\b///c", third.path);
2385
2386            try std.testing.expect(null == it.next());
2387        }
2388        {
2389            const last = it.last().?;
2390            try std.testing.expectEqualStrings("c", last.name);
2391            try std.testing.expectEqualStrings("C:\\\\//a/\\/\\b///c", last.path);
2392
2393            const second_to_last = it.previous().?;
2394            try std.testing.expectEqualStrings("b", second_to_last.name);
2395            try std.testing.expectEqualStrings("C:\\\\//a/\\/\\b", second_to_last.path);
2396
2397            const third_to_last = it.previous().?;
2398            try std.testing.expectEqualStrings("a", third_to_last.name);
2399            try std.testing.expectEqualStrings("C:\\\\//a", third_to_last.path);
2400
2401            try std.testing.expect(null == it.previous());
2402        }
2403    }
2404
2405    {
2406        const path = "/";
2407        var it = WindowsComponentIterator.init(path);
2408        try std.testing.expectEqual(1, it.root_len);
2409        try std.testing.expectEqual(1, it.root_end_index);
2410        try std.testing.expectEqualStrings("/", it.root().?);
2411
2412        try std.testing.expect(null == it.first());
2413        try std.testing.expect(null == it.previous());
2414        try std.testing.expect(null == it.first());
2415        try std.testing.expect(null == it.next());
2416
2417        try std.testing.expect(null == it.last());
2418        try std.testing.expect(null == it.previous());
2419        try std.testing.expect(null == it.last());
2420        try std.testing.expect(null == it.next());
2421    }
2422
2423    {
2424        const path = "";
2425        var it = WindowsComponentIterator.init(path);
2426        try std.testing.expectEqual(0, it.root_len);
2427        try std.testing.expectEqual(0, it.root_end_index);
2428        try std.testing.expect(null == it.root());
2429
2430        try std.testing.expect(null == it.first());
2431        try std.testing.expect(null == it.previous());
2432        try std.testing.expect(null == it.first());
2433        try std.testing.expect(null == it.next());
2434
2435        try std.testing.expect(null == it.last());
2436        try std.testing.expect(null == it.previous());
2437        try std.testing.expect(null == it.last());
2438        try std.testing.expect(null == it.next());
2439    }
2440}
2441
2442test "ComponentIterator windows WTF-16" {
2443    const WindowsComponentIterator = ComponentIterator(.windows, u16);
2444    const L = std.unicode.utf8ToUtf16LeStringLiteral;
2445
2446    const path = L("C:\\a/b/c/");
2447    var it = WindowsComponentIterator.init(path);
2448    try std.testing.expectEqual(3, it.root_len);
2449    try std.testing.expectEqual(3, it.root_end_index);
2450    try std.testing.expectEqualSlices(u16, L("C:\\"), it.root().?);
2451    {
2452        const first = it.first().?;
2453        try std.testing.expectEqualSlices(u16, L("a"), first.name);
2454        try std.testing.expectEqualSlices(u16, L("C:\\a"), first.path);
2455
2456        const second = it.next().?;
2457        try std.testing.expectEqualSlices(u16, L("b"), second.name);
2458        try std.testing.expectEqualSlices(u16, L("C:\\a/b"), second.path);
2459
2460        const third = it.next().?;
2461        try std.testing.expectEqualSlices(u16, L("c"), third.name);
2462        try std.testing.expectEqualSlices(u16, L("C:\\a/b/c"), third.path);
2463
2464        try std.testing.expect(null == it.next());
2465    }
2466    {
2467        const last = it.last().?;
2468        try std.testing.expectEqualSlices(u16, L("c"), last.name);
2469        try std.testing.expectEqualSlices(u16, L("C:\\a/b/c"), last.path);
2470
2471        const second_to_last = it.previous().?;
2472        try std.testing.expectEqualSlices(u16, L("b"), second_to_last.name);
2473        try std.testing.expectEqualSlices(u16, L("C:\\a/b"), second_to_last.path);
2474
2475        const third_to_last = it.previous().?;
2476        try std.testing.expectEqualSlices(u16, L("a"), third_to_last.name);
2477        try std.testing.expectEqualSlices(u16, L("C:\\a"), third_to_last.path);
2478
2479        try std.testing.expect(null == it.previous());
2480    }
2481}
2482
2483test "ComponentIterator roots" {
2484    // UEFI
2485    {
2486        var it = ComponentIterator(.uefi, u8).init("\\\\a");
2487        try std.testing.expectEqualStrings("\\", it.root().?);
2488
2489        it = ComponentIterator(.uefi, u8).init("//a");
2490        try std.testing.expect(null == it.root());
2491    }
2492    // POSIX
2493    {
2494        var it = ComponentIterator(.posix, u8).init("//a");
2495        try std.testing.expectEqualStrings("/", it.root().?);
2496
2497        it = ComponentIterator(.posix, u8).init("\\\\a");
2498        try std.testing.expect(null == it.root());
2499    }
2500    // Windows
2501    {
2502        // Drive relative
2503        var it = ComponentIterator(.windows, u8).init("C:a");
2504        try std.testing.expectEqualStrings("C:", it.root().?);
2505
2506        // Drive absolute
2507        it = ComponentIterator(.windows, u8).init("C:/a");
2508        try std.testing.expectEqualStrings("C:/", it.root().?);
2509        it = ComponentIterator(.windows, u8).init("C:\\a");
2510        try std.testing.expectEqualStrings("C:\\", it.root().?);
2511        it = ComponentIterator(.windows, u8).init("C:///a");
2512        try std.testing.expectEqualStrings("C:/", it.root().?);
2513
2514        // Rooted
2515        it = ComponentIterator(.windows, u8).init("\\a");
2516        try std.testing.expectEqualStrings("\\", it.root().?);
2517        it = ComponentIterator(.windows, u8).init("/a");
2518        try std.testing.expectEqualStrings("/", it.root().?);
2519
2520        // Root local device
2521        it = ComponentIterator(.windows, u8).init("\\\\.");
2522        try std.testing.expectEqualStrings("\\\\.", it.root().?);
2523        it = ComponentIterator(.windows, u8).init("//?");
2524        try std.testing.expectEqualStrings("//?", it.root().?);
2525
2526        // UNC absolute
2527        it = ComponentIterator(.windows, u8).init("//");
2528        try std.testing.expectEqualStrings("//", it.root().?);
2529        it = ComponentIterator(.windows, u8).init("\\\\a");
2530        try std.testing.expectEqualStrings("\\\\a", it.root().?);
2531        it = ComponentIterator(.windows, u8).init("\\\\a\\b\\\\c");
2532        try std.testing.expectEqualStrings("\\\\a\\b\\", it.root().?);
2533        it = ComponentIterator(.windows, u8).init("//a");
2534        try std.testing.expectEqualStrings("//a", it.root().?);
2535        it = ComponentIterator(.windows, u8).init("//a/b//c");
2536        try std.testing.expectEqualStrings("//a/b/", it.root().?);
2537        // Malformed UNC path with empty server name
2538        it = ComponentIterator(.windows, u8).init("\\\\\\a\\b\\c");
2539        try std.testing.expectEqualStrings("\\\\\\a\\", it.root().?);
2540    }
2541}
2542
2543/// Format a path encoded as bytes for display as UTF-8.
2544/// Returns a Formatter for the given path. The path will be converted to valid UTF-8
2545/// during formatting. This is a lossy conversion if the path contains any ill-formed UTF-8.
2546/// Ill-formed UTF-8 byte sequences are replaced by the replacement character (U+FFFD)
2547/// according to "U+FFFD Substitution of Maximal Subparts" from Chapter 3 of
2548/// the Unicode standard, and as specified by https://encoding.spec.whatwg.org/#utf-8-decoder
2549pub const fmtAsUtf8Lossy = std.unicode.fmtUtf8;
2550
2551/// Format a path encoded as WTF-16 LE for display as UTF-8.
2552/// Return a Formatter for a (potentially ill-formed) UTF-16 LE path.
2553/// The path will be converted to valid UTF-8 during formatting. This is
2554/// a lossy conversion if the path contains any unpaired surrogates.
2555/// Unpaired surrogates are replaced by the replacement character (U+FFFD).
2556pub const fmtWtf16LeAsUtf8Lossy = std.unicode.fmtUtf16Le;