//! POSIX paths are arbitrary sequences of `u8` with no particular encoding.
//!
//! Windows paths are arbitrary sequences of `u16` (WTF-16).
//! For cross-platform APIs that deal with sequences of `u8`, Windows
//! paths are encoded by Zig as [WTF-8](https://wtf-8.codeberg.page/).
//! WTF-8 is a superset of UTF-8 that allows encoding surrogate codepoints,
//! which enables lossless roundtripping when converting to/from WTF-16
//! (as long as the WTF-8 encoded surrogate codepoints do not form a pair).
//!
//! WASI paths are sequences of valid Unicode scalar values,
//! which means that WASI is unable to handle paths that cannot be
//! encoded as well-formed UTF-8/UTF-16.
//! https://github.com/WebAssembly/wasi-filesystem/issues/17#issuecomment-1430639353

const builtin = @import("builtin");
const native_os = builtin.target.os.tag;

const std = @import("../std.zig");
const assert = std.debug.assert;
const testing = std.testing;
const mem = std.mem;
const Allocator = std.mem.Allocator;
const eqlIgnoreCaseWtf8 = std.os.windows.eqlIgnoreCaseWtf8;
const eqlIgnoreCaseWtf16 = std.os.windows.eqlIgnoreCaseWtf16;

pub const sep_windows: u8 = '\\';
pub const sep_posix: u8 = '/';
pub const sep = switch (native_os) {
    .windows, .uefi => sep_windows,
    else => sep_posix,
};

pub const sep_str_windows = "\\";
pub const sep_str_posix = "/";
pub const sep_str = switch (native_os) {
    .windows, .uefi => sep_str_windows,
    else => sep_str_posix,
};

pub const delimiter_windows: u8 = ';';
pub const delimiter_posix: u8 = ':';
pub const delimiter = if (native_os == .windows) delimiter_windows else delimiter_posix;

/// Returns if the given byte is a valid path separator
pub fn isSep(byte: u8) bool {
    return switch (native_os) {
        .windows => byte == '/' or byte == '\\',
        .uefi => byte == '\\',
        else => byte == '/',
    };
}

pub const PathType = enum {
    windows,
    uefi,
    posix,

    /// Returns true if `c` is a valid path separator for the `path_type`.
    /// If `T` is `u16`, `c` is assumed to be little-endian.
    pub inline fn isSep(comptime path_type: PathType, comptime T: type, c: T) bool {
        return switch (path_type) {
            .windows => c == mem.nativeToLittle(T, '/') or c == mem.nativeToLittle(T, '\\'),
            .posix => c == mem.nativeToLittle(T, '/'),
            .uefi => c == mem.nativeToLittle(T, '\\'),
        };
    }
};

/// This is different from mem.join in that the separator will not be repeated if
/// it is found at the end or beginning of a pair of consecutive paths.
fn joinSepMaybeZ(allocator: Allocator, separator: u8, comptime sepPredicate: fn (u8) bool, paths: []const []const u8, zero: bool) ![]u8 {
    if (paths.len == 0) return if (zero) try allocator.dupe(u8, &[1]u8{0}) else &[0]u8{};

    // Find first non-empty path index.
    const first_path_index = blk: {
        for (paths, 0..) |path, index| {
            if (path.len == 0) continue else break :blk index;
        }

        // All paths provided were empty, so return early.
        return if (zero) try allocator.dupe(u8, &[1]u8{0}) else &[0]u8{};
    };

    // Calculate length needed for resulting joined path buffer.
    const total_len = blk: {
        var sum: usize = paths[first_path_index].len;
        var prev_path = paths[first_path_index];
        assert(prev_path.len > 0);
        var i: usize = first_path_index + 1;
        while (i < paths.len) : (i += 1) {
            const this_path = paths[i];
            if (this_path.len == 0) continue;
            const prev_sep = sepPredicate(prev_path[prev_path.len - 1]);
            const this_sep = sepPredicate(this_path[0]);
            sum += @intFromBool(!prev_sep and !this_sep);
            sum += if (prev_sep and this_sep) this_path.len - 1 else this_path.len;
            prev_path = this_path;
        }

        if (zero) sum += 1;
        break :blk sum;
    };

    const buf = try allocator.alloc(u8, total_len);
    errdefer allocator.free(buf);

    @memcpy(buf[0..paths[first_path_index].len], paths[first_path_index]);
    var buf_index: usize = paths[first_path_index].len;
    var prev_path = paths[first_path_index];
    assert(prev_path.len > 0);
    var i: usize = first_path_index + 1;
    while (i < paths.len) : (i += 1) {
        const this_path = paths[i];
        if (this_path.len == 0) continue;
        const prev_sep = sepPredicate(prev_path[prev_path.len - 1]);
        const this_sep = sepPredicate(this_path[0]);
        if (!prev_sep and !this_sep) {
            buf[buf_index] = separator;
            buf_index += 1;
        }
        const adjusted_path = if (prev_sep and this_sep) this_path[1..] else this_path;
        @memcpy(buf[buf_index..][0..adjusted_path.len], adjusted_path);
        buf_index += adjusted_path.len;
        prev_path = this_path;
    }

    if (zero) buf[buf.len - 1] = 0;

    // No need for shrink since buf is exactly the correct size.
    return buf;
}

/// Naively combines a series of paths with the native path separator.
/// Allocates memory for the result, which must be freed by the caller.
pub fn join(allocator: Allocator, paths: []const []const u8) ![]u8 {
    return joinSepMaybeZ(allocator, sep, isSep, paths, false);
}

/// Naively combines a series of paths with the native path separator and null terminator.
/// Allocates memory for the result, which must be freed by the caller.
pub fn joinZ(allocator: Allocator, paths: []const []const u8) ![:0]u8 {
    const out = try joinSepMaybeZ(allocator, sep, isSep, paths, true);
    return out[0 .. out.len - 1 :0];
}

pub fn fmtJoin(paths: []const []const u8) std.fmt.Alt([]const []const u8, formatJoin) {
    return .{ .data = paths };
}

fn formatJoin(paths: []const []const u8, w: *std.Io.Writer) std.Io.Writer.Error!void {
    const first_path_idx = for (paths, 0..) |p, idx| {
        if (p.len != 0) break idx;
    } else return;

    try w.writeAll(paths[first_path_idx]); // first component
    var prev_path = paths[first_path_idx];
    for (paths[first_path_idx + 1 ..]) |this_path| {
        if (this_path.len == 0) continue; // skip empty components
        const prev_sep = isSep(prev_path[prev_path.len - 1]);
        const this_sep = isSep(this_path[0]);
        if (!prev_sep and !this_sep) {
            try w.writeByte(sep);
        }
        if (prev_sep and this_sep) {
            try w.writeAll(this_path[1..]); // skip redundant separator
        } else {
            try w.writeAll(this_path);
        }
        prev_path = this_path;
    }
}

fn testJoinMaybeZUefi(paths: []const []const u8, expected: []const u8, zero: bool) !void {
    const uefiIsSep = struct {
        fn isSep(byte: u8) bool {
            return byte == '\\';
        }
    }.isSep;
    const actual = try joinSepMaybeZ(testing.allocator, sep_windows, uefiIsSep, paths, zero);
    defer testing.allocator.free(actual);
    try testing.expectEqualSlices(u8, expected, if (zero) actual[0 .. actual.len - 1 :0] else actual);
}

fn testJoinMaybeZWindows(paths: []const []const u8, expected: []const u8, zero: bool) !void {
    const windowsIsSep = struct {
        fn isSep(byte: u8) bool {
            return byte == '/' or byte == '\\';
        }
    }.isSep;
    const actual = try joinSepMaybeZ(testing.allocator, sep_windows, windowsIsSep, paths, zero);
    defer testing.allocator.free(actual);
    try testing.expectEqualSlices(u8, expected, if (zero) actual[0 .. actual.len - 1 :0] else actual);
}

fn testJoinMaybeZPosix(paths: []const []const u8, expected: []const u8, zero: bool) !void {
    const posixIsSep = struct {
        fn isSep(byte: u8) bool {
            return byte == '/';
        }
    }.isSep;
    const actual = try joinSepMaybeZ(testing.allocator, sep_posix, posixIsSep, paths, zero);
    defer testing.allocator.free(actual);
    try testing.expectEqualSlices(u8, expected, if (zero) actual[0 .. actual.len - 1 :0] else actual);
}

test join {
    {
        const actual: []u8 = try join(testing.allocator, &[_][]const u8{});
        defer testing.allocator.free(actual);
        try testing.expectEqualSlices(u8, "", actual);
    }
    {
        const actual: [:0]u8 = try joinZ(testing.allocator, &[_][]const u8{});
        defer testing.allocator.free(actual);
        try testing.expectEqualSlices(u8, "", actual);
    }
    for (&[_]bool{ false, true }) |zero| {
        try testJoinMaybeZWindows(&[_][]const u8{}, "", zero);
        try testJoinMaybeZWindows(&[_][]const u8{ "c:\\a\\b", "c" }, "c:\\a\\b\\c", zero);
        try testJoinMaybeZWindows(&[_][]const u8{ "c:\\a\\b", "c" }, "c:\\a\\b\\c", zero);
        try testJoinMaybeZWindows(&[_][]const u8{ "c:\\a\\b\\", "\\c" }, "c:\\a\\b\\c", zero);

        try testJoinMaybeZWindows(&[_][]const u8{ "c:\\", "a", "b\\", "c" }, "c:\\a\\b\\c", zero);
        try testJoinMaybeZWindows(&[_][]const u8{ "c:\\a\\", "b\\", "c" }, "c:\\a\\b\\c", zero);

        try testJoinMaybeZWindows(
            &[_][]const u8{ "c:\\home\\andy\\dev\\zig\\build\\lib\\zig\\std", "ab.zig" },
            "c:\\home\\andy\\dev\\zig\\build\\lib\\zig\\std\\ab.zig",
            zero,
        );

        try testJoinMaybeZUefi(&[_][]const u8{ "EFI", "Boot", "bootx64.efi" }, "EFI\\Boot\\bootx64.efi", zero);
        try testJoinMaybeZUefi(&[_][]const u8{ "EFI\\Boot", "bootx64.efi" }, "EFI\\Boot\\bootx64.efi", zero);
        try testJoinMaybeZUefi(&[_][]const u8{ "EFI\\", "\\Boot", "bootx64.efi" }, "EFI\\Boot\\bootx64.efi", zero);
        try testJoinMaybeZUefi(&[_][]const u8{ "EFI\\", "\\Boot\\", "\\bootx64.efi" }, "EFI\\Boot\\bootx64.efi", zero);

        try testJoinMaybeZWindows(&[_][]const u8{ "c:\\", "a", "b/", "c" }, "c:\\a\\b/c", zero);
        try testJoinMaybeZWindows(&[_][]const u8{ "c:\\a/", "b\\", "/c" }, "c:\\a/b\\c", zero);

        try testJoinMaybeZWindows(&[_][]const u8{ "", "c:\\", "", "", "a", "b\\", "c", "" }, "c:\\a\\b\\c", zero);
        try testJoinMaybeZWindows(&[_][]const u8{ "c:\\a/", "", "b\\", "", "/c" }, "c:\\a/b\\c", zero);
        try testJoinMaybeZWindows(&[_][]const u8{ "", "" }, "", zero);

        try testJoinMaybeZPosix(&[_][]const u8{}, "", zero);
        try testJoinMaybeZPosix(&[_][]const u8{ "/a/b", "c" }, "/a/b/c", zero);
        try testJoinMaybeZPosix(&[_][]const u8{ "/a/b/", "c" }, "/a/b/c", zero);

        try testJoinMaybeZPosix(&[_][]const u8{ "/", "a", "b/", "c" }, "/a/b/c", zero);
        try testJoinMaybeZPosix(&[_][]const u8{ "/a/", "b/", "c" }, "/a/b/c", zero);

        try testJoinMaybeZPosix(
            &[_][]const u8{ "/home/andy/dev/zig/build/lib/zig/std", "ab.zig" },
            "/home/andy/dev/zig/build/lib/zig/std/ab.zig",
            zero,
        );

        try testJoinMaybeZPosix(&[_][]const u8{ "a", "/c" }, "a/c", zero);
        try testJoinMaybeZPosix(&[_][]const u8{ "a/", "/c" }, "a/c", zero);

        try testJoinMaybeZPosix(&[_][]const u8{ "", "/", "a", "", "b/", "c", "" }, "/a/b/c", zero);
        try testJoinMaybeZPosix(&[_][]const u8{ "/a/", "", "", "b/", "c" }, "/a/b/c", zero);
        try testJoinMaybeZPosix(&[_][]const u8{ "", "" }, "", zero);
    }
}

pub fn isAbsoluteZ(path_c: [*:0]const u8) bool {
    if (native_os == .windows) {
        return isAbsoluteWindowsZ(path_c);
    } else {
        return isAbsolutePosixZ(path_c);
    }
}

pub fn isAbsolute(path: []const u8) bool {
    if (native_os == .windows) {
        return isAbsoluteWindows(path);
    } else {
        return isAbsolutePosix(path);
    }
}

fn isAbsoluteWindowsImpl(comptime T: type, path: []const T) bool {
    return switch (getWin32PathType(T, path)) {
        // Unambiguously absolute
        .drive_absolute, .unc_absolute, .local_device, .root_local_device => true,
        // Unambiguously relative
        .relative => false,
        // Ambiguous, more absolute than relative
        .rooted => true,
        // Ambiguous, more relative than absolute
        .drive_relative => false,
    };
}

pub fn isAbsoluteWindows(path: []const u8) bool {
    return isAbsoluteWindowsImpl(u8, path);
}

pub fn isAbsoluteWindowsW(path_w: [*:0]const u16) bool {
    return isAbsoluteWindowsImpl(u16, mem.sliceTo(path_w, 0));
}

pub fn isAbsoluteWindowsWtf16(path: []const u16) bool {
    return isAbsoluteWindowsImpl(u16, path);
}

pub fn isAbsoluteWindowsZ(path_c: [*:0]const u8) bool {
    return isAbsoluteWindowsImpl(u8, mem.sliceTo(path_c, 0));
}

pub fn isAbsolutePosix(path: []const u8) bool {
    return path.len > 0 and path[0] == sep_posix;
}

pub fn isAbsolutePosixZ(path_c: [*:0]const u8) bool {
    return isAbsolutePosix(mem.sliceTo(path_c, 0));
}

test isAbsoluteWindows {
    try testIsAbsoluteWindows("", false);
    try testIsAbsoluteWindows("/", true);
    try testIsAbsoluteWindows("//", true);
    try testIsAbsoluteWindows("//server", true);
    try testIsAbsoluteWindows("//server/file", true);
    try testIsAbsoluteWindows("\\\\server\\file", true);
    try testIsAbsoluteWindows("\\\\server", true);
    try testIsAbsoluteWindows("\\\\", true);
    try testIsAbsoluteWindows("c", false);
    try testIsAbsoluteWindows("c:", false);
    try testIsAbsoluteWindows("c:\\", true);
    try testIsAbsoluteWindows("c:/", true);
    try testIsAbsoluteWindows("c://", true);
    try testIsAbsoluteWindows("C:/Users/", true);
    try testIsAbsoluteWindows("C:\\Users\\", true);
    try testIsAbsoluteWindows("C:cwd/another", false);
    try testIsAbsoluteWindows("C:cwd\\another", false);
    try testIsAbsoluteWindows("λ:\\", true);
    try testIsAbsoluteWindows("λ:", false);
    try testIsAbsoluteWindows("\u{10000}:\\", false);
    try testIsAbsoluteWindows("directory/directory", false);
    try testIsAbsoluteWindows("directory\\directory", false);
    try testIsAbsoluteWindows("/usr/local", true);
}

test isAbsolutePosix {
    try testIsAbsolutePosix("", false);
    try testIsAbsolutePosix("/home/foo", true);
    try testIsAbsolutePosix("/home/foo/..", true);
    try testIsAbsolutePosix("bar/", false);
    try testIsAbsolutePosix("./baz", false);
}

fn testIsAbsoluteWindows(path: []const u8, expected_result: bool) !void {
    try testing.expectEqual(expected_result, isAbsoluteWindows(path));
    const path_w = try std.unicode.wtf8ToWtf16LeAllocZ(std.testing.allocator, path);
    defer std.testing.allocator.free(path_w);
    try testing.expectEqual(expected_result, isAbsoluteWindowsW(path_w));
    try testing.expectEqual(expected_result, isAbsoluteWindowsWtf16(path_w));
}

fn testIsAbsolutePosix(path: []const u8, expected_result: bool) !void {
    try testing.expectEqual(expected_result, isAbsolutePosix(path));
}

/// Deprecated; see `WindowsPath2`
pub const WindowsPath = struct {
    is_abs: bool,
    kind: Kind,
    disk_designator: []const u8,

    pub const Kind = enum {
        None,
        Drive,
        NetworkShare,
    };
};

/// Deprecated; see `parsePathWindows`
pub fn windowsParsePath(path: []const u8) WindowsPath {
    if (path.len >= 2 and path[1] == ':') {
        return WindowsPath{
            .is_abs = isAbsoluteWindows(path),
            .kind = WindowsPath.Kind.Drive,
            .disk_designator = path[0..2],
        };
    }
    if (path.len >= 1 and (path[0] == '/' or path[0] == '\\') and
        (path.len == 1 or (path[1] != '/' and path[1] != '\\')))
    {
        return WindowsPath{
            .is_abs = true,
            .kind = WindowsPath.Kind.None,
            .disk_designator = path[0..0],
        };
    }
    const relative_path = WindowsPath{
        .kind = WindowsPath.Kind.None,
        .disk_designator = &[_]u8{},
        .is_abs = false,
    };

    if (path.len >= 2 and PathType.windows.isSep(u8, path[0]) and PathType.windows.isSep(u8, path[1])) {
        const root_end = root_end: {
            var server_end = mem.findAnyPos(u8, path, 2, "/\\") orelse break :root_end path.len;
            while (server_end < path.len and PathType.windows.isSep(u8, path[server_end])) server_end += 1;
            break :root_end mem.findAnyPos(u8, path, server_end, "/\\") orelse path.len;
        };
        return WindowsPath{
            .is_abs = true,
            .kind = WindowsPath.Kind.NetworkShare,
            .disk_designator = path[0..root_end],
        };
    }
    return relative_path;
}

test windowsParsePath {
    {
        const parsed = windowsParsePath("//a/b");
        try testing.expect(parsed.is_abs);
        try testing.expect(parsed.kind == WindowsPath.Kind.NetworkShare);
        try testing.expect(mem.eql(u8, parsed.disk_designator, "//a/b"));
    }
    {
        const parsed = windowsParsePath("\\\\a\\b");
        try testing.expect(parsed.is_abs);
        try testing.expect(parsed.kind == WindowsPath.Kind.NetworkShare);
        try testing.expect(mem.eql(u8, parsed.disk_designator, "\\\\a\\b"));
    }
    {
        const parsed = windowsParsePath("\\\\a/b");
        try testing.expect(parsed.is_abs);
        try testing.expect(parsed.kind == WindowsPath.Kind.NetworkShare);
        try testing.expect(mem.eql(u8, parsed.disk_designator, "\\\\a/b"));
    }
    {
        const parsed = windowsParsePath("\\/a\\");
        try testing.expect(parsed.is_abs);
        try testing.expect(parsed.kind == WindowsPath.Kind.NetworkShare);
        try testing.expect(mem.eql(u8, parsed.disk_designator, "\\/a\\"));
    }
    {
        const parsed = windowsParsePath("\\\\a\\\\b");
        try testing.expect(parsed.is_abs);
        try testing.expect(parsed.kind == WindowsPath.Kind.NetworkShare);
        try testing.expect(mem.eql(u8, parsed.disk_designator, "\\\\a\\\\b"));
    }
    {
        const parsed = windowsParsePath("\\\\a\\\\b\\c");
        try testing.expect(parsed.is_abs);
        try testing.expect(parsed.kind == WindowsPath.Kind.NetworkShare);
        try testing.expect(mem.eql(u8, parsed.disk_designator, "\\\\a\\\\b"));
    }
    {
        const parsed = windowsParsePath("/usr/local");
        try testing.expect(parsed.is_abs);
        try testing.expect(parsed.kind == WindowsPath.Kind.None);
        try testing.expect(mem.eql(u8, parsed.disk_designator, ""));
    }
    {
        const parsed = windowsParsePath("c:../");
        try testing.expect(!parsed.is_abs);
        try testing.expect(parsed.kind == WindowsPath.Kind.Drive);
        try testing.expect(mem.eql(u8, parsed.disk_designator, "c:"));
    }
}

/// On Windows, this calls `parsePathWindows` and on POSIX it calls `parsePathPosix`.
///
/// Returns a platform-specific struct with two fields: `root` and `kind`.
/// The `root` will be a slice of `path` (`/` for POSIX absolute paths, and things
/// like `C:\`, `\\server\share\`, etc for Windows paths).
/// If the path is of kind `.relative`, then `root` will be zero-length.
pub fn parsePath(path: []const u8) switch (native_os) {
    .windows => WindowsPath2(u8),
    else => PosixPath,
} {
    switch (native_os) {
        .windows => return parsePathWindows(u8, path),
        else => return parsePathPosix(path),
    }
}

const PosixPath = struct {
    kind: enum { relative, absolute },
    root: []const u8,
};

pub fn parsePathPosix(path: []const u8) PosixPath {
    const abs = isAbsolutePosix(path);
    return .{
        .kind = if (abs) .absolute else .relative,
        .root = if (abs) path[0..1] else path[0..0],
    };
}

test parsePathPosix {
    {
        const parsed = parsePathPosix("a/b");
        try testing.expectEqual(.relative, parsed.kind);
        try testing.expectEqualStrings("", parsed.root);
    }
    {
        const parsed = parsePathPosix("/a/b");
        try testing.expectEqual(.absolute, parsed.kind);
        try testing.expectEqualStrings("/", parsed.root);
    }
    {
        const parsed = parsePathPosix("///a/b");
        try testing.expectEqual(.absolute, parsed.kind);
        try testing.expectEqualStrings("/", parsed.root);
    }
}

pub fn WindowsPath2(comptime T: type) type {
    return struct {
        kind: Win32PathType,
        root: []const T,
    };
}

pub fn parsePathWindows(comptime T: type, path: []const T) WindowsPath2(T) {
    const kind = getWin32PathType(T, path);
    const root = root: switch (kind) {
        .drive_absolute, .drive_relative => {
            const drive_letter_len = getDriveLetter(T, path).len;
            break :root path[0 .. drive_letter_len + @as(usize, if (kind == .drive_absolute) 2 else 1)];
        },
        .relative => path[0..0],
        .local_device => path[0..4],
        .root_local_device => path,
        .rooted => path[0..1],
        .unc_absolute => {
            const unc = parseUNC(T, path);
            // There may be any number of path separators between the server and the share,
            // so take that into account by using pointer math to get the difference.
            var root_len = 2 + (unc.share.ptr - unc.server.ptr) + unc.share.len;
            if (unc.sep_after_share) root_len += 1;
            break :root path[0..root_len];
        },
    };
    return .{
        .kind = kind,
        .root = root,
    };
}

test parsePathWindows {
    {
        const path = "//a/b";
        const parsed = parsePathWindows(u8, path);
        try testing.expectEqual(.unc_absolute, parsed.kind);
        try testing.expectEqualStrings("//a/b", parsed.root);
        try testWindowsParsePathHarmony(path);
    }
    {
        const path = "\\\\a\\b";
        const parsed = parsePathWindows(u8, path);
        try testing.expectEqual(.unc_absolute, parsed.kind);
        try testing.expectEqualStrings("\\\\a\\b", parsed.root);
        try testWindowsParsePathHarmony(path);
    }
    {
        const path = "\\/a/b/c";
        const parsed = parsePathWindows(u8, path);
        try testing.expectEqual(.unc_absolute, parsed.kind);
        try testing.expectEqualStrings("\\/a/b/", parsed.root);
        try testWindowsParsePathHarmony(path);
    }
    {
        const path = "\\\\a\\";
        const parsed = parsePathWindows(u8, path);
        try testing.expectEqual(.unc_absolute, parsed.kind);
        try testing.expectEqualStrings("\\\\a\\", parsed.root);
        try testWindowsParsePathHarmony(path);
    }
    {
        const path = "\\\\a\\b\\";
        const parsed = parsePathWindows(u8, path);
        try testing.expectEqual(.unc_absolute, parsed.kind);
        try testing.expectEqualStrings("\\\\a\\b\\", parsed.root);
        try testWindowsParsePathHarmony(path);
    }
    {
        const path = "\\\\a\\/b\\/";
        const parsed = parsePathWindows(u8, path);
        try testing.expectEqual(.unc_absolute, parsed.kind);
        try testing.expectEqualStrings("\\\\a\\/b\\", parsed.root);
        try testWindowsParsePathHarmony(path);
    }
    {
        const path = "\\\\кириллица\\ελληνικά\\português";
        const parsed = parsePathWindows(u8, path);
        try testing.expectEqual(.unc_absolute, parsed.kind);
        try testing.expectEqualStrings("\\\\кириллица\\ελληνικά\\", parsed.root);
        try testWindowsParsePathHarmony(path);
    }
    {
        const path = "/usr/local";
        const parsed = parsePathWindows(u8, path);
        try testing.expectEqual(.rooted, parsed.kind);
        try testing.expectEqualStrings("/", parsed.root);
        try testWindowsParsePathHarmony(path);
    }
    {
        const path = "\\\\.";
        const parsed = parsePathWindows(u8, path);
        try testing.expectEqual(.root_local_device, parsed.kind);
        try testing.expectEqualStrings("\\\\.", parsed.root);
        try testWindowsParsePathHarmony(path);
    }
    {
        const path = "\\\\.\\a";
        const parsed = parsePathWindows(u8, path);
        try testing.expectEqual(.local_device, parsed.kind);
        try testing.expectEqualStrings("\\\\.\\", parsed.root);
        try testWindowsParsePathHarmony(path);
    }
    {
        const path = "c:../";
        const parsed = parsePathWindows(u8, path);
        try testing.expectEqual(.drive_relative, parsed.kind);
        try testing.expectEqualStrings("c:", parsed.root);
        try testWindowsParsePathHarmony(path);
    }
    {
        const path = "C:\\../";
        const parsed = parsePathWindows(u8, path);
        try testing.expectEqual(.drive_absolute, parsed.kind);
        try testing.expectEqualStrings("C:\\", parsed.root);
        try testWindowsParsePathHarmony(path);
    }
    {
        // Non-ASCII code point that is encoded as one WTF-16 code unit is considered a valid drive letter
        const path = "€:\\";
        const parsed = parsePathWindows(u8, path);
        try testing.expectEqual(.drive_absolute, parsed.kind);
        try testing.expectEqualStrings("€:\\", parsed.root);
        try testWindowsParsePathHarmony(path);
    }
    {
        const path = "€:";
        const parsed = parsePathWindows(u8, path);
        try testing.expectEqual(.drive_relative, parsed.kind);
        try testing.expectEqualStrings("€:", parsed.root);
        try testWindowsParsePathHarmony(path);
    }
    {
        // But code points that are encoded as two WTF-16 code units are not
        const path = "\u{10000}:\\";
        const parsed = parsePathWindows(u8, path);
        try testing.expectEqual(.relative, parsed.kind);
        try testing.expectEqualStrings("", parsed.root);
        try testWindowsParsePathHarmony(path);
    }
    {
        const path = "\u{10000}:";
        const parsed = parsePathWindows(u8, path);
        try testing.expectEqual(.relative, parsed.kind);
        try testing.expectEqualStrings("", parsed.root);
        try testWindowsParsePathHarmony(path);
    }
    {
        // Paths are assumed to be in the Win32 namespace, so while this is
        // likely a NT namespace path, it's treated as a rooted path.
        const path = "\\??\\foo";
        const parsed = parsePathWindows(u8, path);
        try testing.expectEqual(.rooted, parsed.kind);
        try testing.expectEqualStrings("\\", parsed.root);
        try testWindowsParsePathHarmony(path);
    }
}

fn testWindowsParsePathHarmony(wtf8: []const u8) !void {
    var wtf16_buf: [256]u16 = undefined;
    const wtf16_len = try std.unicode.wtf8ToWtf16Le(&wtf16_buf, wtf8);
    const wtf16 = wtf16_buf[0..wtf16_len];

    const wtf8_parsed = parsePathWindows(u8, wtf8);
    const wtf16_parsed = parsePathWindows(u16, wtf16);

    var wtf8_buf: [256]u8 = undefined;
    const wtf16_root_as_wtf8_len = std.unicode.wtf16LeToWtf8(&wtf8_buf, wtf16_parsed.root);
    const wtf16_root_as_wtf8 = wtf8_buf[0..wtf16_root_as_wtf8_len];

    try std.testing.expectEqual(wtf8_parsed.kind, wtf16_parsed.kind);
    try std.testing.expectEqualStrings(wtf8_parsed.root, wtf16_root_as_wtf8);
}

/// Deprecated; use `parsePath`
pub fn diskDesignator(path: []const u8) []const u8 {
    if (native_os == .windows) {
        return diskDesignatorWindows(path);
    } else {
        return "";
    }
}

/// Deprecated; use `parsePathWindows`
pub fn diskDesignatorWindows(path: []const u8) []const u8 {
    return windowsParsePath(path).disk_designator;
}

fn WindowsUNC(comptime T: type) type {
    return struct {
        server: []const T,
        sep_after_server: bool,
        share: []const T,
        sep_after_share: bool,
    };
}

/// Asserts that `path` starts with two path separators
fn parseUNC(comptime T: type, path: []const T) WindowsUNC(T) {
    assert(path.len >= 2 and PathType.windows.isSep(T, path[0]) and PathType.windows.isSep(T, path[1]));
    const any_sep = switch (T) {
        u8 => "/\\",
        u16 => std.unicode.wtf8ToWtf16LeStringLiteral("/\\"),
        else => @compileError("only u8 (WTF-8) and u16 (WTF-16LE) are supported"),
    };
    // For the server, the first path separator after the initial two is always
    // the terminator of the server name, even if that means the server name is
    // zero-length.
    const server_end = mem.findAnyPos(T, path, 2, any_sep) orelse return .{
        .server = path[2..path.len],
        .sep_after_server = false,
        .share = path[path.len..path.len],
        .sep_after_share = false,
    };
    // For the share, there can be any number of path separators between the server
    // and the share, so we want to skip over all of them instead of just looking for
    // the first one.
    var it = mem.tokenizeAny(T, path[server_end + 1 ..], any_sep);
    const share = it.next() orelse return .{
        .server = path[2..server_end],
        .sep_after_server = true,
        .share = path[server_end + 1 .. server_end + 1],
        .sep_after_share = false,
    };
    return .{
        .server = path[2..server_end],
        .sep_after_server = true,
        .share = share,
        .sep_after_share = it.index != it.buffer.len,
    };
}

test parseUNC {
    {
        const unc = parseUNC(u8, "//");
        try std.testing.expectEqualStrings("", unc.server);
        try std.testing.expect(!unc.sep_after_server);
        try std.testing.expectEqualStrings("", unc.share);
        try std.testing.expect(!unc.sep_after_share);
    }
    {
        const unc = parseUNC(u8, "\\\\s");
        try std.testing.expectEqualStrings("s", unc.server);
        try std.testing.expect(!unc.sep_after_server);
        try std.testing.expectEqualStrings("", unc.share);
        try std.testing.expect(!unc.sep_after_share);
    }
    {
        const unc = parseUNC(u8, "\\\\s/");
        try std.testing.expectEqualStrings("s", unc.server);
        try std.testing.expect(unc.sep_after_server);
        try std.testing.expectEqualStrings("", unc.share);
        try std.testing.expect(!unc.sep_after_share);
    }
    {
        const unc = parseUNC(u8, "\\/server\\share");
        try std.testing.expectEqualStrings("server", unc.server);
        try std.testing.expect(unc.sep_after_server);
        try std.testing.expectEqualStrings("share", unc.share);
        try std.testing.expect(!unc.sep_after_share);
    }
    {
        const unc = parseUNC(u8, "/\\server\\share/");
        try std.testing.expectEqualStrings("server", unc.server);
        try std.testing.expect(unc.sep_after_server);
        try std.testing.expectEqualStrings("share", unc.share);
        try std.testing.expect(unc.sep_after_share);
    }
    {
        const unc = parseUNC(u8, "\\\\server/\\share\\/");
        try std.testing.expectEqualStrings("server", unc.server);
        try std.testing.expect(unc.sep_after_server);
        try std.testing.expectEqualStrings("share", unc.share);
        try std.testing.expect(unc.sep_after_share);
    }
    {
        const unc = parseUNC(u8, "\\\\server\\/\\\\");
        try std.testing.expectEqualStrings("server", unc.server);
        try std.testing.expect(unc.sep_after_server);
        try std.testing.expectEqualStrings("", unc.share);
        try std.testing.expect(!unc.sep_after_share);
    }
}

const DiskDesignatorKind = enum { drive, unc };

/// `p1` and `p2` are both assumed to be the `kind` provided.
fn compareDiskDesignators(comptime T: type, kind: DiskDesignatorKind, p1: []const T, p2: []const T) bool {
    const eql = switch (T) {
        u8 => eqlIgnoreCaseWtf8,
        u16 => eqlIgnoreCaseWtf16,
        else => @compileError("only u8 (WTF-8) and u16 (WTF-16LE) is supported"),
    };
    switch (kind) {
        .drive => {
            const drive_letter1 = getDriveLetter(T, p1);
            const drive_letter2 = getDriveLetter(T, p2);

            return eql(drive_letter1, drive_letter2);
        },
        .unc => {
            const unc1 = parseUNC(T, p1);
            const unc2 = parseUNC(T, p2);

            return eql(unc1.server, unc2.server) and
                eql(unc1.share, unc2.share);
        },
    }
}

/// `path` is assumed to be drive-relative or drive-absolute.
fn getDriveLetter(comptime T: type, path: []const T) []const T {
    const len: usize = switch (T) {
        // getWin32PathType will only return .drive_absolute/.drive_relative when there is
        // (1) a valid code point, and (2) a code point < U+10000, so we only need to
        // get the length determined by the first byte.
        u8 => std.unicode.utf8ByteSequenceLength(path[0]) catch unreachable,
        u16 => 1,
        else => @compileError("unsupported type: " ++ @typeName(T)),
    };
    return path[0..len];
}

test compareDiskDesignators {
    try testCompareDiskDesignators(true, .drive, "c:", "C:\\");
    try testCompareDiskDesignators(true, .drive, "C:\\", "C:");
    try testCompareDiskDesignators(false, .drive, "C:\\", "D:\\");
    // Case-insensitivity technically applies to non-ASCII drive letters
    try testCompareDiskDesignators(true, .drive, "λ:\\", "Λ:");

    try testCompareDiskDesignators(true, .unc, "\\\\server", "//server//");
    try testCompareDiskDesignators(true, .unc, "\\\\server\\\\share", "/\\server/share");
    try testCompareDiskDesignators(true, .unc, "\\\\server\\\\share", "/\\server/share\\\\foo");
    try testCompareDiskDesignators(false, .unc, "\\\\server\\sharefoo", "/\\server/share\\foo");
    try testCompareDiskDesignators(false, .unc, "\\\\serverfoo\\\\share", "//server/share");
    try testCompareDiskDesignators(false, .unc, "\\\\server\\", "//server/share");
}

fn testCompareDiskDesignators(expected_result: bool, kind: DiskDesignatorKind, p1: []const u8, p2: []const u8) !void {
    var wtf16_buf1: [256]u16 = undefined;
    const w1_len = try std.unicode.wtf8ToWtf16Le(&wtf16_buf1, p1);
    var wtf16_buf2: [256]u16 = undefined;
    const w2_len = try std.unicode.wtf8ToWtf16Le(&wtf16_buf2, p2);
    try std.testing.expectEqual(expected_result, compareDiskDesignators(u8, kind, p1, p2));
    try std.testing.expectEqual(expected_result, compareDiskDesignators(u16, kind, wtf16_buf1[0..w1_len], wtf16_buf2[0..w2_len]));
}

/// On Windows, this calls `resolveWindows` and on POSIX it calls `resolvePosix`.
pub fn resolve(allocator: Allocator, paths: []const []const u8) Allocator.Error![]u8 {
    if (native_os == .windows) {
        return resolveWindows(allocator, paths);
    } else {
        return resolvePosix(allocator, paths);
    }
}

/// This function is like a series of `cd` statements executed one after another.
/// It resolves "." and ".." to the best of its ability, but will not convert relative paths to
/// an absolute path, use Io.Dir.realpath instead.
/// ".." components may persist in the resolved path if the resolved path is relative or drive-relative.
/// Path separators are canonicalized to '\\' and drives are canonicalized to capital letters.
///
/// The result will not have a trailing path separator, except for the following scenarios:
/// - The resolved path is drive-absolute with no components (e.g. `C:\`).
/// - The resolved path is a UNC path with only a server name, and the input path contained a trailing separator
///   (e.g. `\\server\`).
/// - The resolved path is a UNC path with no components after the share name, and the input path contained a
///   trailing separator (e.g. `\\server\share\`).
///
/// Each drive has its own current working directory, which is only resolved via the paths provided.
/// In the scenario that the resolved path contains a drive-relative path that can't be resolved using the paths alone,
/// the result will be a drive-relative path.
/// Similarly, in the scenario that the resolved path contains a rooted path that can't be resolved using the paths alone,
/// the result will be a rooted path.
///
/// Note: all usage of this function should be audited due to the existence of symlinks.
/// Without performing actual syscalls, resolving `..` could be incorrect.
/// This API may break in the future: https://github.com/ziglang/zig/issues/13613
pub fn resolveWindows(allocator: Allocator, paths: []const []const u8) Allocator.Error![]u8 {
    // Avoid heap allocation when paths.len is <= @bitSizeOf(usize) * 2
    // (we use `* 3` because stackFallback uses 1 usize as a length)
    var bit_set_allocator_state = std.heap.stackFallback(@sizeOf(usize) * 3, allocator);
    const bit_set_allocator = bit_set_allocator_state.get();
    var relevant_paths = try std.bit_set.DynamicBitSetUnmanaged.initEmpty(bit_set_allocator, paths.len);
    defer relevant_paths.deinit(bit_set_allocator);

    // Iterate the paths backwards, marking the relevant paths along the way.
    // This also allows us to break from the loop whenever any earlier paths are known to be irrelevant.
    var first_path_i: usize = paths.len;
    const effective_root_path: WindowsPath2(u8) = root: {
        var last_effective_root_path: WindowsPath2(u8) = .{ .kind = .relative, .root = "" };
        var last_rooted_path_i: ?usize = null;
        var last_drive_relative_path_i: usize = undefined;
        while (first_path_i > 0) {
            first_path_i -= 1;
            const parsed = parsePathWindows(u8, paths[first_path_i]);
            switch (parsed.kind) {
                .unc_absolute, .root_local_device, .local_device => {
                    switch (last_effective_root_path.kind) {
                        .rooted => {},
                        .drive_relative => continue,
                        else => {
                            relevant_paths.set(first_path_i);
                        },
                    }
                    break :root parsed;
                },
                .drive_relative, .drive_absolute => {
                    switch (last_effective_root_path.kind) {
                        .drive_relative => if (!compareDiskDesignators(u8, .drive, parsed.root, last_effective_root_path.root)) {
                            continue;
                        } else if (last_rooted_path_i != null) {
                            break :root .{ .kind = .drive_absolute, .root = parsed.root };
                        },
                        .relative => last_effective_root_path = parsed,
                        .rooted => {
                            // This is the end of the line, since the rooted path will always be relative
                            // to this drive letter, and even if the current path is drive-relative, the
                            // rooted-ness makes that irrelevant.
                            //
                            // Therefore, force the kind of the effective root to be drive-absolute in order to
                            // properly resolve a rooted path against a drive-relative one, as the result should
                            // always be drive-absolute.
                            break :root .{ .kind = .drive_absolute, .root = parsed.root };
                        },
                        .drive_absolute, .unc_absolute, .root_local_device, .local_device => unreachable,
                    }
                    relevant_paths.set(first_path_i);
                    last_drive_relative_path_i = first_path_i;
                    if (parsed.kind == .drive_absolute) {
                        break :root parsed;
                    }
                },
                .relative => {
                    switch (last_effective_root_path.kind) {
                        .rooted => continue,
                        .relative => last_effective_root_path = parsed,
                        else => {},
                    }
                    relevant_paths.set(first_path_i);
                },
                .rooted => {
                    switch (last_effective_root_path.kind) {
                        .drive_relative => {},
                        .relative => last_effective_root_path = parsed,
                        .rooted => continue,
                        .drive_absolute, .unc_absolute, .root_local_device, .local_device => unreachable,
                    }
                    if (last_rooted_path_i == null) {
                        last_rooted_path_i = first_path_i;
                        relevant_paths.set(first_path_i);
                    }
                },
            }
        }
        // After iterating, if the pending effective root is drive-relative then that means
        // nothing has led to forcing a drive-absolute root (a path that allows resolving the
        // drive-specific CWD would cause an early break), so we now need to ignore all paths
        // before the most recent drive-relative one. For example, if we're resolving
        // { "\\rooted", "relative", "C:drive-relative" }
        // then the `\rooted` and `relative` needs to be ignored since we can't
        // know what the rooted path is rooted against as that'd require knowing the CWD.
        if (last_effective_root_path.kind == .drive_relative) {
            for (0..last_drive_relative_path_i) |i| {
                relevant_paths.unset(i);
            }
        }
        break :root last_effective_root_path;
    };

    var result: std.ArrayList(u8) = .empty;
    defer result.deinit(allocator);

    var want_path_sep_between_root_and_component = false;
    switch (effective_root_path.kind) {
        .root_local_device, .local_device => {
            try result.ensureUnusedCapacity(allocator, 3);
            result.appendSliceAssumeCapacity("\\\\");
            result.appendAssumeCapacity(effective_root_path.root[2]); // . or ?
            want_path_sep_between_root_and_component = true;
        },
        .drive_absolute, .drive_relative => {
            try result.ensureUnusedCapacity(allocator, effective_root_path.root.len);
            result.appendAssumeCapacity(std.ascii.toUpper(effective_root_path.root[0]));
            result.appendAssumeCapacity(':');
            if (effective_root_path.kind == .drive_absolute) {
                result.appendAssumeCapacity('\\');
            }
        },
        .unc_absolute => {
            const unc = parseUNC(u8, effective_root_path.root);

            const root_len = len: {
                var len: usize = 2 + unc.server.len + unc.share.len;
                if (unc.sep_after_server) len += 1;
                if (unc.sep_after_share) len += 1;
                break :len len;
            };
            try result.ensureUnusedCapacity(allocator, root_len);
            result.appendSliceAssumeCapacity("\\\\");
            if (unc.server.len > 0 or unc.sep_after_server) {
                result.appendSliceAssumeCapacity(unc.server);
                if (unc.sep_after_server)
                    result.appendAssumeCapacity('\\')
                else
                    want_path_sep_between_root_and_component = true;
            }
            if (unc.share.len > 0) {
                result.appendSliceAssumeCapacity(unc.share);
                if (unc.sep_after_share)
                    result.appendAssumeCapacity('\\')
                else
                    want_path_sep_between_root_and_component = true;
            }
        },
        .rooted => {
            try result.append(allocator, '\\');
        },
        .relative => {},
    }

    const root_len = result.items.len;
    var negative_count: usize = 0;
    for (paths[first_path_i..], first_path_i..) |path, i| {
        if (!relevant_paths.isSet(i)) continue;

        const parsed = parsePathWindows(u8, path);
        const skip_len = parsed.root.len;
        var it = mem.tokenizeAny(u8, path[skip_len..], "/\\");
        while (it.next()) |component| {
            if (mem.eql(u8, component, ".")) {
                continue;
            } else if (mem.eql(u8, component, "..")) {
                if (result.items.len == 0 or (result.items.len == root_len and effective_root_path.kind == .drive_relative)) {
                    negative_count += 1;
                    continue;
                }
                while (true) {
                    if (result.items.len == root_len) {
                        break;
                    }
                    const end_with_sep = PathType.windows.isSep(u8, result.items[result.items.len - 1]);
                    result.items.len -= 1;
                    if (end_with_sep) break;
                }
            } else if (result.items.len == root_len and !want_path_sep_between_root_and_component) {
                try result.appendSlice(allocator, component);
            } else {
                try result.ensureUnusedCapacity(allocator, 1 + component.len);
                result.appendAssumeCapacity('\\');
                result.appendSliceAssumeCapacity(component);
            }
        }
    }

    if (root_len != 0 and result.items.len == root_len and negative_count == 0) {
        return result.toOwnedSlice(allocator);
    }

    if (result.items.len == root_len) {
        if (negative_count == 0) {
            return allocator.dupe(u8, ".");
        }

        try result.ensureTotalCapacityPrecise(allocator, 3 * negative_count - 1);
        for (0..negative_count - 1) |_| {
            result.appendSliceAssumeCapacity("..\\");
        }
        result.appendSliceAssumeCapacity("..");
    } else {
        const dest = try result.addManyAt(allocator, root_len, 3 * negative_count);
        for (0..negative_count) |i| {
            dest[i * 3 ..][0..3].* = "..\\".*;
        }
    }

    return result.toOwnedSlice(allocator);
}

/// This function is like a series of `cd` statements executed one after another.
///
/// It resolves "." and ".." to the best of its ability, but will not convert relative paths to
/// an absolute path, use Io.Dir.realpath instead.
///
/// ".." components may persist in the resolved path if the resolved path is relative.
///
/// The result does not have a trailing path separator.
///
/// This function does not perform any syscalls. Executing this series of path
/// lookups on the actual filesystem may produce different results due to
/// symlinks.
pub fn resolvePosix(allocator: Allocator, paths: []const []const u8) Allocator.Error![]u8 {
    assert(paths.len > 0);

    var result = std.array_list.Managed(u8).init(allocator);
    defer result.deinit();

    var negative_count: usize = 0;
    var is_abs = false;

    for (paths) |p| {
        if (isAbsolutePosix(p)) {
            is_abs = true;
            negative_count = 0;
            result.clearRetainingCapacity();
        }
        var it = mem.tokenizeScalar(u8, p, '/');
        while (it.next()) |component| {
            if (mem.eql(u8, component, ".")) {
                continue;
            } else if (mem.eql(u8, component, "..")) {
                if (result.items.len == 0) {
                    negative_count += @intFromBool(!is_abs);
                    continue;
                }
                while (true) {
                    const ends_with_slash = result.items[result.items.len - 1] == '/';
                    result.items.len -= 1;
                    if (ends_with_slash or result.items.len == 0) break;
                }
            } else if (result.items.len > 0 or is_abs) {
                try result.ensureUnusedCapacity(1 + component.len);
                result.appendAssumeCapacity('/');
                result.appendSliceAssumeCapacity(component);
            } else {
                try result.appendSlice(component);
            }
        }
    }

    if (result.items.len == 0) {
        if (is_abs) {
            return allocator.dupe(u8, "/");
        }
        if (negative_count == 0) {
            return allocator.dupe(u8, ".");
        } else {
            const real_result = try allocator.alloc(u8, 3 * negative_count - 1);
            var count = negative_count - 1;
            var i: usize = 0;
            while (count > 0) : (count -= 1) {
                real_result[i..][0..3].* = "../".*;
                i += 3;
            }
            real_result[i..][0..2].* = "..".*;
            return real_result;
        }
    }

    if (negative_count == 0) {
        return result.toOwnedSlice();
    } else {
        const real_result = try allocator.alloc(u8, 3 * negative_count + result.items.len);
        var count = negative_count;
        var i: usize = 0;
        while (count > 0) : (count -= 1) {
            real_result[i..][0..3].* = "../".*;
            i += 3;
        }
        @memcpy(real_result[i..][0..result.items.len], result.items);
        return real_result;
    }
}

test resolve {
    try testResolveWindows(&[_][]const u8{ "a", "..\\..\\.." }, "..\\..");
    try testResolveWindows(&[_][]const u8{ "..", "", "..\\..\\foo" }, "..\\..\\..\\foo");
    try testResolveWindows(&[_][]const u8{ "a\\b\\c\\", "..\\..\\.." }, ".");
    try testResolveWindows(&[_][]const u8{"."}, ".");
    try testResolveWindows(&[_][]const u8{""}, ".");

    try testResolvePosix(&[_][]const u8{ "a", "../../.." }, "../..");
    try testResolvePosix(&[_][]const u8{ "..", "", "../../foo" }, "../../../foo");
    try testResolvePosix(&[_][]const u8{ "a/b/c/", "../../.." }, ".");
    try testResolvePosix(&[_][]const u8{"."}, ".");
    try testResolvePosix(&[_][]const u8{""}, ".");
}

test resolveWindows {
    try testResolveWindows(
        &[_][]const u8{ "Z:\\", "/usr/local", "lib\\zig\\std\\array_list.zig" },
        "Z:\\usr\\local\\lib\\zig\\std\\array_list.zig",
    );
    try testResolveWindows(
        &[_][]const u8{ "z:\\", "usr/local", "lib\\zig" },
        "Z:\\usr\\local\\lib\\zig",
    );

    try testResolveWindows(&[_][]const u8{ "c:\\a\\b\\c", "/hi", "ok" }, "C:\\hi\\ok");
    try testResolveWindows(&[_][]const u8{ "c:\\a\\b\\c\\", ".\\..\\foo" }, "C:\\a\\b\\foo");
    try testResolveWindows(&[_][]const u8{ "c:/blah\\blah", "d:/games", "c:../a" }, "C:\\blah\\a");
    try testResolveWindows(&[_][]const u8{ "c:/blah\\blah", "d:/games", "C:../a" }, "C:\\blah\\a");
    try testResolveWindows(&[_][]const u8{ "c:/ignore", "d:\\a/b\\c/d", "\\e.exe" }, "D:\\e.exe");
    try testResolveWindows(&[_][]const u8{ "c:/ignore", "c:/some/file" }, "C:\\some\\file");
    // The first path "sets" the CWD, so the drive-relative path is then relative to that.
    try testResolveWindows(&[_][]const u8{ "d:/foo", "d:some/dir//", "D:another" }, "D:\\foo\\some\\dir\\another");
    try testResolveWindows(&[_][]const u8{ "//server/share", "..", "relative\\" }, "\\\\server\\share\\relative");
    try testResolveWindows(&[_][]const u8{ "\\\\server/share", "..", "relative\\" }, "\\\\server\\share\\relative");
    try testResolveWindows(&[_][]const u8{ "\\\\server/share/ignore", "//server/share/bar" }, "\\\\server\\share\\bar");
    try testResolveWindows(&[_][]const u8{ "\\/server\\share/", "..", "relative" }, "\\\\server\\share\\relative");
    try testResolveWindows(&[_][]const u8{ "\\\\server\\share", "C:drive-relative" }, "C:drive-relative");
    try testResolveWindows(&[_][]const u8{ "c:/", "//" }, "\\\\");
    try testResolveWindows(&[_][]const u8{ "c:/", "//server" }, "\\\\server");
    try testResolveWindows(&[_][]const u8{ "c:/", "//server/share" }, "\\\\server\\share");
    try testResolveWindows(&[_][]const u8{ "c:/", "//server//share////" }, "\\\\server\\share\\");
    try testResolveWindows(&[_][]const u8{ "c:/", "///some//dir" }, "\\\\\\some\\dir");
    try testResolveWindows(&[_][]const u8{ "c:foo", "bar" }, "C:foo\\bar");
    try testResolveWindows(&[_][]const u8{ "C:\\foo\\tmp.3\\", "..\\tmp.3\\cycles\\root.js" }, "C:\\foo\\tmp.3\\cycles\\root.js");
    // Drive-relative stays drive-relative if there's nothing to provide the drive-specific CWD
    try testResolveWindows(&[_][]const u8{ "relative", "d:foo" }, "D:foo");
    try testResolveWindows(&[_][]const u8{ "../..\\..", "d:foo" }, "D:foo");
    try testResolveWindows(&[_][]const u8{ "../..\\..", "\\rooted", "d:foo" }, "D:foo");
    try testResolveWindows(&[_][]const u8{ "C:\\foo", "../..\\..", "\\rooted", "d:foo" }, "D:foo");
    try testResolveWindows(&[_][]const u8{ "D:relevant", "../..\\..", "d:foo" }, "D:..\\..\\foo");
    try testResolveWindows(&[_][]const u8{ "D:relevant", "../..\\..", "\\\\.\\ignored", "C:\\ignored", "C:ignored", "\\\\ignored", "d:foo" }, "D:..\\..\\foo");
    try testResolveWindows(&[_][]const u8{ "ignored", "\\\\.\\ignored", "C:\\ignored", "C:ignored", "\\\\ignored", "d:foo" }, "D:foo");
    // Rooted paths remain rooted if there's no absolute path available to resolve the "root"
    try testResolveWindows(&[_][]const u8{ "/foo", "bar" }, "\\foo\\bar");
    // Rooted against a UNC path
    try testResolveWindows(&[_][]const u8{ "//server/share/ignore", "/foo", "bar" }, "\\\\server\\share\\foo\\bar");
    try testResolveWindows(&[_][]const u8{ "//server/share/", "/foo" }, "\\\\server\\share\\foo");
    try testResolveWindows(&[_][]const u8{ "//server/share", "/foo" }, "\\\\server\\share\\foo");
    try testResolveWindows(&[_][]const u8{ "//server/", "/foo" }, "\\\\server\\foo");
    try testResolveWindows(&[_][]const u8{ "//server", "/foo" }, "\\\\server\\foo");
    try testResolveWindows(&[_][]const u8{ "//", "/foo" }, "\\\\foo");
    // Rooted against a drive-relative path
    try testResolveWindows(&[_][]const u8{ "C:", "/foo", "bar" }, "C:\\foo\\bar");
    try testResolveWindows(&[_][]const u8{ "C:\\ignore", "C:", "/foo", "bar" }, "C:\\foo\\bar");
    try testResolveWindows(&[_][]const u8{ "C:\\ignore", "\\foo", "C:bar" }, "C:\\foo\\bar");
    // Only the last rooted path is relevant
    try testResolveWindows(&[_][]const u8{ "\\ignore", "\\foo" }, "\\foo");
    try testResolveWindows(&[_][]const u8{ "c:ignore", "ignore", "\\ignore", "\\foo" }, "C:\\foo");
    // Rooted is only relevant to a drive-relative if there's a previous drive-* path
    try testResolveWindows(&[_][]const u8{ "\\ignore", "C:foo" }, "C:foo");
    try testResolveWindows(&[_][]const u8{ "\\ignore", "\\ignore2", "C:foo" }, "C:foo");
    try testResolveWindows(&[_][]const u8{ "c:ignore", "\\ignore", "\\rooted", "C:foo" }, "C:\\rooted\\foo");
    try testResolveWindows(&[_][]const u8{ "c:\\ignore", "\\ignore", "\\rooted", "C:foo" }, "C:\\rooted\\foo");
    try testResolveWindows(&[_][]const u8{ "d:\\ignore", "\\ignore", "\\ignore2", "C:foo" }, "C:foo");
    // Root local device paths
    try testResolveWindows(&[_][]const u8{"\\/."}, "\\\\.");
    try testResolveWindows(&[_][]const u8{ "\\/.", "C:drive-relative" }, "C:drive-relative");
    try testResolveWindows(&[_][]const u8{"/\\?"}, "\\\\?");
    try testResolveWindows(&[_][]const u8{ "ignore", "c:\\ignore", "\\\\.", "foo" }, "\\\\.\\foo");
    try testResolveWindows(&[_][]const u8{ "ignore", "c:\\ignore", "\\\\?", "foo" }, "\\\\?\\foo");
    try testResolveWindows(&[_][]const u8{ "ignore", "c:\\ignore", "//.", "ignore", "\\foo" }, "\\\\.\\foo");
    try testResolveWindows(&[_][]const u8{ "ignore", "c:\\ignore", "\\\\?", "ignore", "\\foo" }, "\\\\?\\foo");

    // Keep relative paths relative.
    try testResolveWindows(&[_][]const u8{"a/b"}, "a\\b");
    try testResolveWindows(&[_][]const u8{".."}, "..");
    try testResolveWindows(&[_][]const u8{"../.."}, "..\\..");
    try testResolveWindows(&[_][]const u8{ "C:foo", "../.." }, "C:..");
    try testResolveWindows(&[_][]const u8{ "d:foo", "../..\\.." }, "D:..\\..");

    // Local device paths treat the \\.\ or \\?\ as the "root", everything afterwards is treated as a regular component.
    try testResolveWindows(&[_][]const u8{ "\\\\?\\C:\\foo", "../bar", "baz" }, "\\\\?\\C:\\bar\\baz");
    try testResolveWindows(&[_][]const u8{ "\\\\.\\C:/foo", "../../../../bar", "baz" }, "\\\\.\\bar\\baz");
    try testResolveWindows(&[_][]const u8{ "//./C:/foo", "../../../../bar", "baz" }, "\\\\.\\bar\\baz");
    try testResolveWindows(&[_][]const u8{ "\\\\.\\foo", ".." }, "\\\\.");
    try testResolveWindows(&[_][]const u8{ "\\\\.\\foo", "..\\.." }, "\\\\.");

    // Paths are assumed to be Win32, so paths that are likely NT paths are treated as a rooted path.
    try testResolveWindows(&[_][]const u8{ "\\??\\C:\\foo", "/bar", "baz" }, "\\bar\\baz");
    try testResolveWindows(&[_][]const u8{ "C:\\", "\\??\\C:\\foo", "bar" }, "C:\\??\\C:\\foo\\bar");
}

test resolvePosix {
    try testResolvePosix(&.{ "/a/b", "c" }, "/a/b/c");
    try testResolvePosix(&.{ "/a/b", "c", "//d", "e///" }, "/d/e");
    try testResolvePosix(&.{ "/a/b/c", "..", "../" }, "/a");
    try testResolvePosix(&.{ "/", "..", ".." }, "/");
    try testResolvePosix(&.{"/a/b/c/"}, "/a/b/c");

    try testResolvePosix(&.{ "/var/lib", "../", "file/" }, "/var/file");
    try testResolvePosix(&.{ "/var/lib", "/../", "file/" }, "/file");
    try testResolvePosix(&.{ "/some/dir", ".", "/absolute/" }, "/absolute");
    try testResolvePosix(&.{ "/foo/tmp.3/", "../tmp.3/cycles/root.js" }, "/foo/tmp.3/cycles/root.js");

    // Keep relative paths relative.
    try testResolvePosix(&.{"a/b"}, "a/b");
    try testResolvePosix(&.{"."}, ".");
    try testResolvePosix(&.{ ".", "src/test.zig", "..", "../test/cases.zig" }, "test/cases.zig");
}

fn testResolveWindows(paths: []const []const u8, expected: []const u8) !void {
    const actual = try resolveWindows(testing.allocator, paths);
    defer testing.allocator.free(actual);
    try testing.expectEqualStrings(expected, actual);
}

fn testResolvePosix(paths: []const []const u8, expected: []const u8) !void {
    const actual = try resolvePosix(testing.allocator, paths);
    defer testing.allocator.free(actual);
    try testing.expectEqualStrings(expected, actual);
}

/// Strip the last component from a file path.
///
/// If the path is a file in the current directory (no directory component)
/// then returns null.
///
/// If the path is the root directory, returns null.
pub fn dirname(path: []const u8) ?[]const u8 {
    if (native_os == .windows) {
        return dirnameWindows(path);
    } else {
        return dirnamePosix(path);
    }
}

pub fn dirnameWindows(path: []const u8) ?[]const u8 {
    return dirnameInner(.windows, path);
}

pub fn dirnamePosix(path: []const u8) ?[]const u8 {
    return dirnameInner(.posix, path);
}

fn dirnameInner(comptime path_type: PathType, path: []const u8) ?[]const u8 {
    var it = ComponentIterator(path_type, u8).init(path);
    _ = it.last() orelse return null;
    const up = it.previous() orelse return it.root();
    return up.path;
}

test dirnamePosix {
    try testDirnamePosix("/a/b/c", "/a/b");
    try testDirnamePosix("/a/b/c///", "/a/b");
    try testDirnamePosix("/a", "/");
    try testDirnamePosix("/", null);
    try testDirnamePosix("//", null);
    try testDirnamePosix("///", null);
    try testDirnamePosix("////", null);
    try testDirnamePosix("", null);
    try testDirnamePosix("a", null);
    try testDirnamePosix("a/", null);
    try testDirnamePosix("a//", null);
}

test dirnameWindows {
    try testDirnameWindows("c:\\", null);
    try testDirnameWindows("c:\\\\", null);
    try testDirnameWindows("c:\\foo", "c:\\");
    try testDirnameWindows("c:\\\\foo\\", "c:\\");
    try testDirnameWindows("c:\\foo\\bar", "c:\\foo");
    try testDirnameWindows("c:\\foo\\bar\\", "c:\\foo");
    try testDirnameWindows("c:\\\\foo\\bar\\baz", "c:\\\\foo\\bar");
    try testDirnameWindows("\\", null);
    try testDirnameWindows("\\foo", "\\");
    try testDirnameWindows("\\foo\\", "\\");
    try testDirnameWindows("\\foo\\bar", "\\foo");
    try testDirnameWindows("\\foo\\bar\\", "\\foo");
    try testDirnameWindows("\\foo\\bar\\baz", "\\foo\\bar");
    try testDirnameWindows("c:", null);
    try testDirnameWindows("c:foo", "c:");
    try testDirnameWindows("c:foo\\", "c:");
    try testDirnameWindows("c:foo\\bar", "c:foo");
    try testDirnameWindows("c:foo\\bar\\", "c:foo");
    try testDirnameWindows("c:foo\\bar\\baz", "c:foo\\bar");
    try testDirnameWindows("file:stream", null);
    try testDirnameWindows("dir\\file:stream", "dir");
    try testDirnameWindows("\\\\unc\\share", null);
    try testDirnameWindows("\\\\unc\\share\\\\", null);
    try testDirnameWindows("\\\\unc\\share\\foo", "\\\\unc\\share\\");
    try testDirnameWindows("\\\\unc\\share\\foo\\", "\\\\unc\\share\\");
    try testDirnameWindows("\\\\unc\\share\\foo\\bar", "\\\\unc\\share\\foo");
    try testDirnameWindows("\\\\unc\\share\\foo\\bar\\", "\\\\unc\\share\\foo");
    try testDirnameWindows("\\\\unc\\share\\foo\\bar\\baz", "\\\\unc\\share\\foo\\bar");
    try testDirnameWindows("\\\\.", null);
    try testDirnameWindows("\\\\.\\", null);
    try testDirnameWindows("\\\\.\\device", "\\\\.\\");
    try testDirnameWindows("\\\\.\\device\\", "\\\\.\\");
    try testDirnameWindows("\\\\.\\device\\foo", "\\\\.\\device");
    try testDirnameWindows("\\\\?", null);
    try testDirnameWindows("\\\\?\\", null);
    try testDirnameWindows("\\\\?\\device", "\\\\?\\");
    try testDirnameWindows("\\\\?\\device\\", "\\\\?\\");
    try testDirnameWindows("\\\\?\\device\\foo", "\\\\?\\device");
    try testDirnameWindows("/a/b/", "/a");
    try testDirnameWindows("/a/b", "/a");
    try testDirnameWindows("/a", "/");
    try testDirnameWindows("", null);
    try testDirnameWindows("/", null);
    try testDirnameWindows("////", null);
    try testDirnameWindows("foo", null);
}

fn testDirnamePosix(input: []const u8, expected_output: ?[]const u8) !void {
    if (dirnamePosix(input)) |output| {
        try testing.expect(mem.eql(u8, output, expected_output.?));
    } else {
        try testing.expect(expected_output == null);
    }
}

fn testDirnameWindows(input: []const u8, expected_output: ?[]const u8) !void {
    if (dirnameWindows(input)) |output| {
        try testing.expectEqualStrings(expected_output.?, output);
    } else {
        try testing.expect(expected_output == null);
    }
}

pub fn basename(path: []const u8) []const u8 {
    if (native_os == .windows) {
        return basenameWindows(path);
    } else {
        return basenamePosix(path);
    }
}

pub fn basenamePosix(path: []const u8) []const u8 {
    return basenameInner(.posix, path);
}

pub fn basenameWindows(path: []const u8) []const u8 {
    return basenameInner(.windows, path);
}

fn basenameInner(comptime path_type: PathType, path: []const u8) []const u8 {
    var it = ComponentIterator(path_type, u8).init(path);
    const last = it.last() orelse return &[_]u8{};
    return last.name;
}

test basename {
    try testBasename("", "");
    try testBasename("/", "");
    try testBasename("/dir/basename.ext", "basename.ext");
    try testBasename("/basename.ext", "basename.ext");
    try testBasename("basename.ext", "basename.ext");
    try testBasename("basename.ext/", "basename.ext");
    try testBasename("basename.ext//", "basename.ext");
    try testBasename("/aaa/bbb", "bbb");
    try testBasename("/aaa/", "aaa");
    try testBasename("/aaa/b", "b");
    try testBasename("/a/b", "b");

    // For Windows, this is a UNC path that only has a server name component.
    try testBasename("//a", if (native_os == .windows) "" else "a");

    try testBasenamePosix("\\dir\\basename.ext", "\\dir\\basename.ext");
    try testBasenamePosix("\\basename.ext", "\\basename.ext");
    try testBasenamePosix("basename.ext", "basename.ext");
    try testBasenamePosix("basename.ext\\", "basename.ext\\");
    try testBasenamePosix("basename.ext\\\\", "basename.ext\\\\");
    try testBasenamePosix("foo", "foo");

    try testBasenameWindows("\\dir\\basename.ext", "basename.ext");
    try testBasenameWindows("\\basename.ext", "basename.ext");
    try testBasenameWindows("basename.ext", "basename.ext");
    try testBasenameWindows("basename.ext\\", "basename.ext");
    try testBasenameWindows("basename.ext\\\\", "basename.ext");
    try testBasenameWindows("foo", "foo");
    try testBasenameWindows("C:", "");
    try testBasenameWindows("C:.", ".");
    try testBasenameWindows("C:\\", "");
    try testBasenameWindows("C:\\dir\\base.ext", "base.ext");
    try testBasenameWindows("C:\\basename.ext", "basename.ext");
    try testBasenameWindows("C:basename.ext", "basename.ext");
    try testBasenameWindows("C:basename.ext\\", "basename.ext");
    try testBasenameWindows("C:basename.ext\\\\", "basename.ext");
    try testBasenameWindows("\\\\.", "");
    try testBasenameWindows("\\\\.\\", "");
    try testBasenameWindows("\\\\.\\basename.ext", "basename.ext");
    try testBasenameWindows("\\\\?", "");
    try testBasenameWindows("\\\\?\\", "");
    try testBasenameWindows("\\\\?\\basename.ext", "basename.ext");
    try testBasenameWindows("C:foo", "foo");
    try testBasenameWindows("file:stream", "file:stream");
}

fn testBasename(input: []const u8, expected_output: []const u8) !void {
    try testing.expectEqualSlices(u8, expected_output, basename(input));
}

fn testBasenamePosix(input: []const u8, expected_output: []const u8) !void {
    try testing.expectEqualSlices(u8, expected_output, basenamePosix(input));
}

fn testBasenameWindows(input: []const u8, expected_output: []const u8) !void {
    try testing.expectEqualSlices(u8, expected_output, basenameWindows(input));
}

/// Returns the non-absolute path from `from` to `to`.
///
/// Other than memory allocation, this is a pure function; the result solely
/// depends on the input parameters.
///
/// If `from` and `to` each resolve to the same path (after calling `resolve`
/// on each), a zero-length string is returned.
///
/// See `relativePosix` and `relativeWindows` for operating system specific
/// details and for how `environ_map` is used.
pub fn relative(
    gpa: Allocator,
    cwd: []const u8,
    environ_map: ?*const std.process.Environ.Map,
    from: []const u8,
    to: []const u8,
) Allocator.Error![]u8 {
    if (native_os == .windows) {
        return relativeWindows(gpa, cwd, environ_map, from, to);
    } else {
        return relativePosix(gpa, cwd, from, to);
    }
}

/// Returns the non-absolute path from `from` to `to` according to Windows rules.
///
/// Other than memory allocation, this is a pure function; the result solely
/// depends on the input parameters.
///
/// If `from` and `to` each resolve to the same path (after calling `resolve`
/// on each), a zero-length string is returned.
///
/// The result is not guaranteed to be relative, as the paths may be on
/// different volumes. In that case, the result will be the canonicalized
/// absolute path of `to`.
///
/// Per-drive CWDs are stored in special semi-hidden environment variables of
/// the format `=<drive-letter>:`, e.g. `=C:`. This type of CWD is purely a
/// shell concept, so there's no guarantee that it'll be set or that it'll even
/// be accurate. This is the only reason for the `environ_map` parameter. `null` is
/// treated equivalent to the environment variable missing.
pub fn relativeWindows(
    gpa: Allocator,
    cwd: []const u8,
    environ_map: ?*const std.process.Environ.Map,
    from: []const u8,
    to: []const u8,
) Allocator.Error![]u8 {
    const parsed_from = parsePathWindows(u8, from);
    const parsed_to = parsePathWindows(u8, to);

    const result_is_always_to = x: {
        if (parsed_from.kind != parsed_to.kind) {
            break :x false;
        }
        switch (parsed_from.kind) {
            .drive_relative, .drive_absolute => {
                break :x !compareDiskDesignators(u8, .drive, parsed_from.root, parsed_to.root);
            },
            .unc_absolute => {
                break :x !compareDiskDesignators(u8, .unc, parsed_from.root, parsed_to.root);
            },
            .relative, .rooted, .local_device => break :x false,
            .root_local_device => break :x true,
        }
    };

    if (result_is_always_to) {
        return windowsResolveAgainstCwd(gpa, cwd, environ_map, to, parsed_to);
    }

    const resolved_from = try windowsResolveAgainstCwd(gpa, cwd, environ_map, from, parsed_from);
    defer gpa.free(resolved_from);
    var clean_up_resolved_to = true;
    const resolved_to = try windowsResolveAgainstCwd(gpa, cwd, environ_map, to, parsed_to);
    defer if (clean_up_resolved_to) gpa.free(resolved_to);

    const parsed_resolved_from = parsePathWindows(u8, resolved_from);
    const parsed_resolved_to = parsePathWindows(u8, resolved_to);

    const result_is_to = x: {
        if (parsed_resolved_from.kind != parsed_resolved_to.kind) {
            break :x true;
        }
        switch (parsed_resolved_from.kind) {
            .drive_absolute, .drive_relative => {
                break :x !compareDiskDesignators(u8, .drive, parsed_resolved_from.root, parsed_resolved_to.root);
            },
            .unc_absolute => {
                break :x !compareDiskDesignators(u8, .unc, parsed_resolved_from.root, parsed_resolved_to.root);
            },
            .relative, .rooted, .local_device => break :x false,
            .root_local_device => break :x true,
        }
    };

    if (result_is_to) {
        clean_up_resolved_to = false;
        return resolved_to;
    }

    var from_it = mem.tokenizeAny(u8, resolved_from[parsed_resolved_from.root.len..], "/\\");
    var to_it = mem.tokenizeAny(u8, resolved_to[parsed_resolved_to.root.len..], "/\\");
    while (true) {
        const from_component = from_it.next() orelse return gpa.dupe(u8, to_it.rest());
        const to_rest = to_it.rest();
        if (to_it.next()) |to_component| {
            if (eqlIgnoreCaseWtf8(from_component, to_component))
                continue;
        }
        var up_index_end = "..".len;
        while (from_it.next()) |_| {
            up_index_end += "\\..".len;
        }
        const result = try gpa.alloc(u8, up_index_end + @intFromBool(to_rest.len > 0) + to_rest.len);
        errdefer gpa.free(result);

        result[0..2].* = "..".*;
        var result_index: usize = 2;
        while (result_index < up_index_end) {
            result[result_index..][0..3].* = "\\..".*;
            result_index += 3;
        }

        var rest_it = mem.tokenizeAny(u8, to_rest, "/\\");
        while (rest_it.next()) |to_component| {
            result[result_index] = '\\';
            result_index += 1;
            @memcpy(result[result_index..][0..to_component.len], to_component);
            result_index += to_component.len;
        }

        return gpa.realloc(result, result_index);
    }
    return [_]u8{};
}

fn windowsResolveAgainstCwd(
    gpa: Allocator,
    cwd: []const u8,
    environ_map: ?*const std.process.Environ.Map,
    path: []const u8,
    parsed: WindowsPath2(u8),
) ![]u8 {
    // Space for 256 WTF-16 code units; potentially 3 WTF-8 bytes per WTF-16 code unit
    var temp_allocator_state = std.heap.stackFallback(256 * 3, gpa);
    return switch (parsed.kind) {
        .drive_absolute,
        .unc_absolute,
        .root_local_device,
        .local_device,
        => try resolveWindows(gpa, &.{path}),

        .relative => try resolveWindows(gpa, &.{ cwd, path }),

        .rooted => blk: {
            const parsed_cwd = parsePathWindows(u8, cwd);
            switch (parsed_cwd.kind) {
                .drive_absolute => {
                    var drive_buf = "_:\\".*;
                    drive_buf[0] = cwd[0];
                    break :blk try resolveWindows(gpa, &.{ &drive_buf, path });
                },
                .unc_absolute => {
                    break :blk try resolveWindows(gpa, &.{ parsed_cwd.root, path });
                },
                // Effectively a malformed CWD, give up and just return a normalized path
                else => break :blk try resolveWindows(gpa, &.{path}),
            }
        },
        .drive_relative => blk: {
            const temp_allocator = temp_allocator_state.get();
            const drive_cwd = drive_cwd: {
                const parsed_cwd = parsePathWindows(u8, cwd);

                if (parsed_cwd.kind == .drive_absolute) {
                    const drive_letter_w = parsed_cwd.root[0];
                    const drive_letters_match = drive_letter_w <= 0x7F and
                        std.ascii.toUpper(@intCast(drive_letter_w)) == std.ascii.toUpper(parsed.root[0]);
                    if (drive_letters_match)
                        break :drive_cwd cwd;

                    if (environ_map) |m| {
                        if (m.get(&.{ '=', parsed.root[0], ':' })) |v| {
                            break :drive_cwd try temp_allocator.dupe(u8, v);
                        }
                    }
                }

                const drive_buf = try temp_allocator.alloc(u8, 3);
                drive_buf[0] = parsed.root[0];
                drive_buf[1] = ':';
                drive_buf[2] = '\\';
                break :drive_cwd drive_buf;
            };
            defer temp_allocator.free(drive_cwd);
            break :blk try resolveWindows(gpa, &.{ drive_cwd, path });
        },
    };
}

/// Returns the non-absolute path from `from` to `to` according to Windows rules.
///
/// Other than memory allocation, this is a pure function; the result solely
/// depends on the input parameters.
///
/// If `from` and `to` each resolve to the same path (after calling `resolve`
/// on each), a zero-length string is returned.
///
pub fn relativePosix(allocator: Allocator, cwd: []const u8, from: []const u8, to: []const u8) Allocator.Error![]u8 {
    const resolved_from = try resolvePosix(allocator, &[_][]const u8{ cwd, from });
    defer allocator.free(resolved_from);
    const resolved_to = try resolvePosix(allocator, &[_][]const u8{ cwd, to });
    defer allocator.free(resolved_to);

    var from_it = mem.tokenizeScalar(u8, resolved_from, '/');
    var to_it = mem.tokenizeScalar(u8, resolved_to, '/');
    while (true) {
        const from_component = from_it.next() orelse return allocator.dupe(u8, to_it.rest());
        const to_rest = to_it.rest();
        if (to_it.next()) |to_component| {
            if (mem.eql(u8, from_component, to_component))
                continue;
        }
        var up_count: usize = 1;
        while (from_it.next()) |_| {
            up_count += 1;
        }
        const up_index_end = up_count * "../".len;
        const result = try allocator.alloc(u8, up_index_end + to_rest.len);
        errdefer allocator.free(result);

        var result_index: usize = 0;
        while (result_index < up_index_end) {
            result[result_index..][0..3].* = "../".*;
            result_index += 3;
        }
        if (to_rest.len == 0) {
            // shave off the trailing slash
            return allocator.realloc(result, result_index - 1);
        }

        @memcpy(result[result_index..][0..to_rest.len], to_rest);
        return result;
    }

    return [_]u8{};
}

test relative {
    try testRelativeWindows("c:/blah\\blah", "d:/games", "D:\\games");
    try testRelativeWindows("c:/aaaa/bbbb", "c:/aaaa", "..");
    try testRelativeWindows("c:/aaaa/bbbb", "c:/cccc", "..\\..\\cccc");
    try testRelativeWindows("c:/aaaa/bbbb", "C:/aaaa/bbbb", "");
    try testRelativeWindows("c:/aaaa/bbbb", "c:/aaaa/cccc", "..\\cccc");
    try testRelativeWindows("c:/aaaa/", "c:/aaaa/cccc", "cccc");
    try testRelativeWindows("c:/", "c:\\aaaa\\bbbb", "aaaa\\bbbb");
    try testRelativeWindows("c:/aaaa/bbbb", "d:\\", "D:\\");
    try testRelativeWindows("c:/AaAa/bbbb", "c:/aaaa/bbbb", "");
    try testRelativeWindows("c:/aaaaa/", "c:/aaaa/cccc", "..\\aaaa\\cccc");
    try testRelativeWindows("C:\\foo\\bar\\baz\\quux", "C:\\", "..\\..\\..\\..");
    try testRelativeWindows("C:\\foo\\test", "C:\\foo\\test\\bar\\package.json", "bar\\package.json");
    try testRelativeWindows("C:\\foo\\bar\\baz-quux", "C:\\foo\\bar\\baz", "..\\baz");
    try testRelativeWindows("C:\\foo\\bar\\baz", "C:\\foo\\bar\\baz-quux", "..\\baz-quux");
    try testRelativeWindows("\\\\foo\\bar", "\\\\foo\\bar\\baz", "baz");
    try testRelativeWindows("\\\\foo\\bar\\baz", "\\\\foo\\bar", "..");
    try testRelativeWindows("\\\\foo\\bar\\baz-quux", "\\\\foo\\bar\\baz", "..\\baz");
    try testRelativeWindows("\\\\foo/bar\\baz-quux", "//foo\\bar/baz", "..\\baz");
    try testRelativeWindows("\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz-quux", "..\\baz-quux");
    try testRelativeWindows("C:\\baz-quux", "C:\\baz", "..\\baz");
    try testRelativeWindows("C:\\baz", "C:\\baz-quux", "..\\baz-quux");
    try testRelativeWindows("\\\\foo\\baz-quux", "\\\\foo\\baz", "\\\\foo\\baz");
    try testRelativeWindows("\\\\foo\\baz", "\\\\foo\\baz-quux", "\\\\foo\\baz-quux");
    try testRelativeWindows("C:\\baz", "\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz");
    try testRelativeWindows("\\\\foo\\bar\\baz", "C:\\baz", "C:\\baz");

    try testRelativeWindows("c:blah\\blah", "c:foo", "..\\..\\foo");
    try testRelativeWindows("c:foo", "c:foo\\bar", "bar");
    try testRelativeWindows("\\blah\\blah", "\\foo", "..\\..\\foo");
    try testRelativeWindows("\\foo", "\\foo\\bar", "bar");

    try testRelativeWindows("a/b/c", "a\\b", "..");
    try testRelativeWindows("a/b/c", "a", "..\\..");
    try testRelativeWindows("a/b/c", "a\\b\\c\\d", "d");

    try testRelativeWindows("\\\\FOO\\bar\\baz", "\\\\foo\\BAR\\BAZ", "");
    // Unicode-aware case-insensitive path comparison
    try testRelativeWindows("\\\\кириллица\\ελληνικά\\português", "\\\\КИРИЛЛИЦА\\ΕΛΛΗΝΙΚΆ\\PORTUGUÊS", "");

    try testRelativePosix("/var/lib", "/var", "..");
    try testRelativePosix("/var/lib", "/bin", "../../bin");
    try testRelativePosix("/var/lib", "/var/lib", "");
    try testRelativePosix("/var/lib", "/var/apache", "../apache");
    try testRelativePosix("/var/", "/var/lib", "lib");
    try testRelativePosix("/", "/var/lib", "var/lib");
    try testRelativePosix("/foo/test", "/foo/test/bar/package.json", "bar/package.json");
    try testRelativePosix("/Users/a/web/b/test/mails", "/Users/a/web/b", "../..");
    try testRelativePosix("/foo/bar/baz-quux", "/foo/bar/baz", "../baz");
    try testRelativePosix("/foo/bar/baz", "/foo/bar/baz-quux", "../baz-quux");
    try testRelativePosix("/baz-quux", "/baz", "../baz");
    try testRelativePosix("/baz", "/baz-quux", "../baz-quux");
}

fn testRelativePosix(from: []const u8, to: []const u8, expected_output: []const u8) !void {
    const result = try relativePosix(testing.allocator, ".", from, to);
    defer testing.allocator.free(result);
    try testing.expectEqualStrings(expected_output, result);
}

fn testRelativeWindows(from: []const u8, to: []const u8, expected_output: []const u8) !void {
    const result = try relativeWindows(testing.allocator, ".", null, from, to);
    defer testing.allocator.free(result);
    try testing.expectEqualStrings(expected_output, result);
}

/// Searches for a file extension separated by a `.` and returns the string after that `.`.
/// Files that end or start with `.` and have no other `.` in their name
/// are considered to have no extension, in which case this returns "".
/// Examples:
/// - `"main.zig"`      ⇒ `".zig"`
/// - `"src/main.zig"`  ⇒ `".zig"`
/// - `".gitignore"`    ⇒ `""`
/// - `".image.png"`    ⇒ `".png"`
/// - `"keep."`         ⇒ `"."`
/// - `"src.keep.me"`   ⇒ `".me"`
/// - `"/src/keep.me"`  ⇒ `".me"`
/// - `"/src/keep.me/"` ⇒ `".me"`
/// The returned slice is guaranteed to have its pointer within the start and end
/// pointer address range of `path`, even if it is length zero.
pub fn extension(path: []const u8) []const u8 {
    const filename = basename(path);
    const index = mem.lastIndexOfScalar(u8, filename, '.') orelse return path[path.len..];
    if (index == 0) return path[path.len..];
    return filename[index..];
}

fn testExtension(path: []const u8, expected: []const u8) !void {
    try testing.expectEqualStrings(expected, extension(path));
}

test extension {
    try testExtension("", "");
    try testExtension(".", "");
    try testExtension("a.", ".");
    try testExtension("abc.", ".");
    try testExtension(".a", "");
    try testExtension(".file", "");
    try testExtension(".gitignore", "");
    try testExtension(".image.png", ".png");
    try testExtension("file.ext", ".ext");
    try testExtension("file.ext.", ".");
    try testExtension("very-long-file.bruh", ".bruh");
    try testExtension("a.b.c", ".c");
    try testExtension("a.b.c/", ".c");

    try testExtension("/", "");
    try testExtension("/.", "");
    try testExtension("/a.", ".");
    try testExtension("/abc.", ".");
    try testExtension("/.a", "");
    try testExtension("/.file", "");
    try testExtension("/.gitignore", "");
    try testExtension("/file.ext", ".ext");
    try testExtension("/file.ext.", ".");
    try testExtension("/very-long-file.bruh", ".bruh");
    try testExtension("/a.b.c", ".c");
    try testExtension("/a.b.c/", ".c");

    try testExtension("/foo/bar/bam/", "");
    try testExtension("/foo/bar/bam/.", "");
    try testExtension("/foo/bar/bam/a.", ".");
    try testExtension("/foo/bar/bam/abc.", ".");
    try testExtension("/foo/bar/bam/.a", "");
    try testExtension("/foo/bar/bam/.file", "");
    try testExtension("/foo/bar/bam/.gitignore", "");
    try testExtension("/foo/bar/bam/file.ext", ".ext");
    try testExtension("/foo/bar/bam/file.ext.", ".");
    try testExtension("/foo/bar/bam/very-long-file.bruh", ".bruh");
    try testExtension("/foo/bar/bam/a.b.c", ".c");
    try testExtension("/foo/bar/bam/a.b.c/", ".c");
}

/// Returns the last component of this path without its extension (if any):
/// - "hello/world/lib.tar.gz" ⇒ "lib.tar"
/// - "hello/world/lib.tar"    ⇒ "lib"
/// - "hello/world/lib"        ⇒ "lib"
pub fn stem(path: []const u8) []const u8 {
    const filename = basename(path);
    const index = mem.lastIndexOfScalar(u8, filename, '.') orelse return filename[0..];
    if (index == 0) return path;
    return filename[0..index];
}

fn testStem(path: []const u8, expected: []const u8) !void {
    try testing.expectEqualStrings(expected, stem(path));
}

test stem {
    try testStem("hello/world/lib.tar.gz", "lib.tar");
    try testStem("hello/world/lib.tar", "lib");
    try testStem("hello/world/lib", "lib");
    try testStem("hello/lib/", "lib");
    try testStem("hello...", "hello..");
    try testStem("hello.", "hello");
    try testStem("/hello.", "hello");
    try testStem(".gitignore", ".gitignore");
    try testStem(".image.png", ".image");
    try testStem("file.ext", "file");
    try testStem("file.ext.", "file.ext");
    try testStem("a.b.c", "a.b");
    try testStem("a.b.c/", "a.b");
    try testStem(".a", ".a");
    try testStem("///", "");
    try testStem("..", ".");
    try testStem(".", ".");
    try testStem(" ", " ");
    try testStem("", "");
}

/// A path component iterator that can move forwards and backwards.
/// The 'root' of the path (`/` for POSIX, things like `C:\`, `\\server\share\`, etc
/// for Windows) is treated specially and will never be returned by any of the
/// `first`, `last`, `next`, or `previous` functions.
/// Multiple consecutive path separators are skipped (treated as a single separator)
/// when iterating.
/// All returned component names/paths are slices of the original path.
/// There is no normalization of paths performed while iterating.
pub fn ComponentIterator(comptime path_type: PathType, comptime T: type) type {
    return struct {
        path: []const T,
        /// Length of the root with at most one trailing path separator included (e.g. `C:/`).
        root_len: usize,
        /// Length of the root with all trailing path separators included (e.g. `C://///`).
        root_end_index: usize,
        start_index: usize = 0,
        end_index: usize = 0,

        const Self = @This();

        pub const Component = struct {
            /// The current component's path name, e.g. 'b'.
            /// This will never contain path separators.
            name: []const T,
            /// The full path up to and including the current component, e.g. '/a/b'
            /// This will never contain trailing path separators.
            path: []const T,
        };

        /// After `init`, `next` will return the first component after the root
        /// (there is no need to call `first` after `init`).
        /// To iterate backwards (from the end of the path to the beginning), call `last`
        /// after `init` and then iterate via `previous` calls.
        /// For Windows paths, paths are assumed to be in the Win32 namespace.
        pub fn init(path: []const T) Self {
            const root_len: usize = switch (path_type) {
                .posix, .uefi => posix: {
                    // Root on UEFI and POSIX only differs by the path separator
                    break :posix if (path.len > 0 and path_type.isSep(T, path[0])) 1 else 0;
                },
                .windows => windows: {
                    break :windows parsePathWindows(T, path).root.len;
                },
            };
            // If there are repeated path separators directly after the root,
            // keep track of that info so that they don't have to be dealt with when
            // iterating components.
            var root_end_index = root_len;
            for (path[root_len..]) |c| {
                if (!path_type.isSep(T, c)) break;
                root_end_index += 1;
            }
            return .{
                .path = path,
                .root_len = root_len,
                .root_end_index = root_end_index,
                .start_index = root_end_index,
                .end_index = root_end_index,
            };
        }

        /// Returns the root of the path if it is not a relative path, or null otherwise.
        /// For POSIX paths, this will be `/`.
        /// For Windows paths, this will be something like `C:\`, `\\server\share\`, etc.
        /// For UEFI paths, this will be `\`.
        pub fn root(self: Self) ?[]const T {
            if (self.root_end_index == 0) return null;
            return self.path[0..self.root_len];
        }

        /// Returns the first component (from the beginning of the path).
        /// For example, if the path is `/a/b/c` then this will return the `a` component.
        /// After calling `first`, `previous` will always return `null`, and `next` will return
        /// the component to the right of the one returned by `first`, if any exist.
        pub fn first(self: *Self) ?Component {
            self.start_index = self.root_end_index;
            self.end_index = self.start_index;
            while (self.end_index < self.path.len and !path_type.isSep(T, self.path[self.end_index])) {
                self.end_index += 1;
            }
            if (self.end_index == self.start_index) return null;
            return .{
                .name = self.path[self.start_index..self.end_index],
                .path = self.path[0..self.end_index],
            };
        }

        /// Returns the last component (from the end of the path).
        /// For example, if the path is `/a/b/c` then this will return the `c` component.
        /// After calling `last`, `next` will always return `null`, and `previous` will return
        /// the component to the left of the one returned by `last`, if any exist.
        pub fn last(self: *Self) ?Component {
            self.end_index = self.path.len;
            while (true) {
                if (self.end_index == self.root_end_index) {
                    self.start_index = self.end_index;
                    return null;
                }
                if (!path_type.isSep(T, self.path[self.end_index - 1])) break;
                self.end_index -= 1;
            }
            self.start_index = self.end_index;
            while (true) {
                if (self.start_index == self.root_end_index) break;
                if (path_type.isSep(T, self.path[self.start_index - 1])) break;
                self.start_index -= 1;
            }
            if (self.start_index == self.end_index) return null;
            return .{
                .name = self.path[self.start_index..self.end_index],
                .path = self.path[0..self.end_index],
            };
        }

        /// Returns the next component (the component to the right of the most recently
        /// returned component), or null if no such component exists.
        /// For example, if the path is `/a/b/c` and the most recently returned component
        /// is `b`, then this will return the `c` component.
        pub fn next(self: *Self) ?Component {
            const peek_result = self.peekNext() orelse return null;
            self.start_index = peek_result.path.len - peek_result.name.len;
            self.end_index = peek_result.path.len;
            return peek_result;
        }

        /// Like `next`, but does not modify the iterator state.
        pub fn peekNext(self: Self) ?Component {
            var start_index = self.end_index;
            while (start_index < self.path.len and path_type.isSep(T, self.path[start_index])) {
                start_index += 1;
            }
            var end_index = start_index;
            while (end_index < self.path.len and !path_type.isSep(T, self.path[end_index])) {
                end_index += 1;
            }
            if (start_index == end_index) return null;
            return .{
                .name = self.path[start_index..end_index],
                .path = self.path[0..end_index],
            };
        }

        /// Returns the previous component (the component to the left of the most recently
        /// returned component), or null if no such component exists.
        /// For example, if the path is `/a/b/c` and the most recently returned component
        /// is `b`, then this will return the `a` component.
        pub fn previous(self: *Self) ?Component {
            const peek_result = self.peekPrevious() orelse return null;
            self.start_index = peek_result.path.len - peek_result.name.len;
            self.end_index = peek_result.path.len;
            return peek_result;
        }

        /// Like `previous`, but does not modify the iterator state.
        pub fn peekPrevious(self: Self) ?Component {
            var end_index = self.start_index;
            while (true) {
                if (end_index == self.root_end_index) return null;
                if (!path_type.isSep(T, self.path[end_index - 1])) break;
                end_index -= 1;
            }
            var start_index = end_index;
            while (true) {
                if (start_index == self.root_end_index) break;
                if (path_type.isSep(T, self.path[start_index - 1])) break;
                start_index -= 1;
            }
            if (start_index == end_index) return null;
            return .{
                .name = self.path[start_index..end_index],
                .path = self.path[0..end_index],
            };
        }
    };
}

pub const NativeComponentIterator = ComponentIterator(switch (native_os) {
    .windows => .windows,
    .uefi => .uefi,
    else => .posix,
}, u8);

pub fn componentIterator(path: []const u8) NativeComponentIterator {
    return NativeComponentIterator.init(path);
}

test "ComponentIterator posix" {
    const PosixComponentIterator = ComponentIterator(.posix, u8);
    {
        const path = "a/b/c/";
        var it = PosixComponentIterator.init(path);
        try std.testing.expectEqual(0, it.root_len);
        try std.testing.expectEqual(0, it.root_end_index);
        try std.testing.expect(null == it.root());
        {
            try std.testing.expect(null == it.previous());

            const first_via_next = it.next().?;
            try std.testing.expectEqualStrings("a", first_via_next.name);
            try std.testing.expectEqualStrings("a", first_via_next.path);

            const first = it.first().?;
            try std.testing.expectEqualStrings("a", first.name);
            try std.testing.expectEqualStrings("a", first.path);

            try std.testing.expect(null == it.previous());

            const second = it.next().?;
            try std.testing.expectEqualStrings("b", second.name);
            try std.testing.expectEqualStrings("a/b", second.path);

            const third = it.next().?;
            try std.testing.expectEqualStrings("c", third.name);
            try std.testing.expectEqualStrings("a/b/c", third.path);

            try std.testing.expect(null == it.next());
        }
        {
            const last = it.last().?;
            try std.testing.expectEqualStrings("c", last.name);
            try std.testing.expectEqualStrings("a/b/c", last.path);

            try std.testing.expect(null == it.next());

            const second_to_last = it.previous().?;
            try std.testing.expectEqualStrings("b", second_to_last.name);
            try std.testing.expectEqualStrings("a/b", second_to_last.path);

            const third_to_last = it.previous().?;
            try std.testing.expectEqualStrings("a", third_to_last.name);
            try std.testing.expectEqualStrings("a", third_to_last.path);

            try std.testing.expect(null == it.previous());
        }
    }

    {
        const path = "/a/b/c/";
        var it = PosixComponentIterator.init(path);
        try std.testing.expectEqual(1, it.root_len);
        try std.testing.expectEqual(1, it.root_end_index);
        try std.testing.expectEqualStrings("/", it.root().?);
        {
            try std.testing.expect(null == it.previous());

            const first_via_next = it.next().?;
            try std.testing.expectEqualStrings("a", first_via_next.name);
            try std.testing.expectEqualStrings("/a", first_via_next.path);

            const first = it.first().?;
            try std.testing.expectEqualStrings("a", first.name);
            try std.testing.expectEqualStrings("/a", first.path);

            try std.testing.expect(null == it.previous());

            const second = it.next().?;
            try std.testing.expectEqualStrings("b", second.name);
            try std.testing.expectEqualStrings("/a/b", second.path);

            const third = it.next().?;
            try std.testing.expectEqualStrings("c", third.name);
            try std.testing.expectEqualStrings("/a/b/c", third.path);

            try std.testing.expect(null == it.next());
        }
        {
            const last = it.last().?;
            try std.testing.expectEqualStrings("c", last.name);
            try std.testing.expectEqualStrings("/a/b/c", last.path);

            try std.testing.expect(null == it.next());

            const second_to_last = it.previous().?;
            try std.testing.expectEqualStrings("b", second_to_last.name);
            try std.testing.expectEqualStrings("/a/b", second_to_last.path);

            const third_to_last = it.previous().?;
            try std.testing.expectEqualStrings("a", third_to_last.name);
            try std.testing.expectEqualStrings("/a", third_to_last.path);

            try std.testing.expect(null == it.previous());
        }
    }

    {
        const path = "////a///b///c////";
        var it = PosixComponentIterator.init(path);
        try std.testing.expectEqual(1, it.root_len);
        try std.testing.expectEqual(4, it.root_end_index);
        try std.testing.expectEqualStrings("/", it.root().?);
        {
            try std.testing.expect(null == it.previous());

            const first_via_next = it.next().?;
            try std.testing.expectEqualStrings("a", first_via_next.name);
            try std.testing.expectEqualStrings("////a", first_via_next.path);

            const first = it.first().?;
            try std.testing.expectEqualStrings("a", first.name);
            try std.testing.expectEqualStrings("////a", first.path);

            try std.testing.expect(null == it.previous());

            const second = it.next().?;
            try std.testing.expectEqualStrings("b", second.name);
            try std.testing.expectEqualStrings("////a///b", second.path);

            const third = it.next().?;
            try std.testing.expectEqualStrings("c", third.name);
            try std.testing.expectEqualStrings("////a///b///c", third.path);

            try std.testing.expect(null == it.next());
        }
        {
            const last = it.last().?;
            try std.testing.expectEqualStrings("c", last.name);
            try std.testing.expectEqualStrings("////a///b///c", last.path);

            try std.testing.expect(null == it.next());

            const second_to_last = it.previous().?;
            try std.testing.expectEqualStrings("b", second_to_last.name);
            try std.testing.expectEqualStrings("////a///b", second_to_last.path);

            const third_to_last = it.previous().?;
            try std.testing.expectEqualStrings("a", third_to_last.name);
            try std.testing.expectEqualStrings("////a", third_to_last.path);

            try std.testing.expect(null == it.previous());
        }
    }

    {
        const path = "/";
        var it = PosixComponentIterator.init(path);
        try std.testing.expectEqual(1, it.root_len);
        try std.testing.expectEqual(1, it.root_end_index);
        try std.testing.expectEqualStrings("/", it.root().?);

        try std.testing.expect(null == it.first());
        try std.testing.expect(null == it.previous());
        try std.testing.expect(null == it.first());
        try std.testing.expect(null == it.next());

        try std.testing.expect(null == it.last());
        try std.testing.expect(null == it.previous());
        try std.testing.expect(null == it.last());
        try std.testing.expect(null == it.next());
    }

    {
        const path = "";
        var it = PosixComponentIterator.init(path);
        try std.testing.expectEqual(0, it.root_len);
        try std.testing.expectEqual(0, it.root_end_index);
        try std.testing.expect(null == it.root());

        try std.testing.expect(null == it.first());
        try std.testing.expect(null == it.previous());
        try std.testing.expect(null == it.first());
        try std.testing.expect(null == it.next());

        try std.testing.expect(null == it.last());
        try std.testing.expect(null == it.previous());
        try std.testing.expect(null == it.last());
        try std.testing.expect(null == it.next());
    }
}

test "ComponentIterator windows" {
    const WindowsComponentIterator = ComponentIterator(.windows, u8);
    {
        const path = "a/b\\c//";
        var it = WindowsComponentIterator.init(path);
        try std.testing.expectEqual(0, it.root_len);
        try std.testing.expectEqual(0, it.root_end_index);
        try std.testing.expect(null == it.root());
        {
            try std.testing.expect(null == it.previous());

            const first_via_next = it.next().?;
            try std.testing.expectEqualStrings("a", first_via_next.name);
            try std.testing.expectEqualStrings("a", first_via_next.path);

            const first = it.first().?;
            try std.testing.expectEqualStrings("a", first.name);
            try std.testing.expectEqualStrings("a", first.path);

            try std.testing.expect(null == it.previous());

            const second = it.next().?;
            try std.testing.expectEqualStrings("b", second.name);
            try std.testing.expectEqualStrings("a/b", second.path);

            const third = it.next().?;
            try std.testing.expectEqualStrings("c", third.name);
            try std.testing.expectEqualStrings("a/b\\c", third.path);

            try std.testing.expect(null == it.next());
        }
        {
            const last = it.last().?;
            try std.testing.expectEqualStrings("c", last.name);
            try std.testing.expectEqualStrings("a/b\\c", last.path);

            try std.testing.expect(null == it.next());

            const second_to_last = it.previous().?;
            try std.testing.expectEqualStrings("b", second_to_last.name);
            try std.testing.expectEqualStrings("a/b", second_to_last.path);

            const third_to_last = it.previous().?;
            try std.testing.expectEqualStrings("a", third_to_last.name);
            try std.testing.expectEqualStrings("a", third_to_last.path);

            try std.testing.expect(null == it.previous());
        }
    }

    {
        const path = "C:\\a/b/c/";
        var it = WindowsComponentIterator.init(path);
        try std.testing.expectEqual(3, it.root_len);
        try std.testing.expectEqual(3, it.root_end_index);
        try std.testing.expectEqualStrings("C:\\", it.root().?);
        {
            const first = it.first().?;
            try std.testing.expectEqualStrings("a", first.name);
            try std.testing.expectEqualStrings("C:\\a", first.path);

            const second = it.next().?;
            try std.testing.expectEqualStrings("b", second.name);
            try std.testing.expectEqualStrings("C:\\a/b", second.path);

            const third = it.next().?;
            try std.testing.expectEqualStrings("c", third.name);
            try std.testing.expectEqualStrings("C:\\a/b/c", third.path);

            try std.testing.expect(null == it.next());
        }
        {
            const last = it.last().?;
            try std.testing.expectEqualStrings("c", last.name);
            try std.testing.expectEqualStrings("C:\\a/b/c", last.path);

            const second_to_last = it.previous().?;
            try std.testing.expectEqualStrings("b", second_to_last.name);
            try std.testing.expectEqualStrings("C:\\a/b", second_to_last.path);

            const third_to_last = it.previous().?;
            try std.testing.expectEqualStrings("a", third_to_last.name);
            try std.testing.expectEqualStrings("C:\\a", third_to_last.path);

            try std.testing.expect(null == it.previous());
        }
    }

    {
        const path = "C:\\\\//a/\\/\\b///c////";
        var it = WindowsComponentIterator.init(path);
        try std.testing.expectEqual(3, it.root_len);
        try std.testing.expectEqual(6, it.root_end_index);
        try std.testing.expectEqualStrings("C:\\", it.root().?);
        {
            const first = it.first().?;
            try std.testing.expectEqualStrings("a", first.name);
            try std.testing.expectEqualStrings("C:\\\\//a", first.path);

            const second = it.next().?;
            try std.testing.expectEqualStrings("b", second.name);
            try std.testing.expectEqualStrings("C:\\\\//a/\\/\\b", second.path);

            const third = it.next().?;
            try std.testing.expectEqualStrings("c", third.name);
            try std.testing.expectEqualStrings("C:\\\\//a/\\/\\b///c", third.path);

            try std.testing.expect(null == it.next());
        }
        {
            const last = it.last().?;
            try std.testing.expectEqualStrings("c", last.name);
            try std.testing.expectEqualStrings("C:\\\\//a/\\/\\b///c", last.path);

            const second_to_last = it.previous().?;
            try std.testing.expectEqualStrings("b", second_to_last.name);
            try std.testing.expectEqualStrings("C:\\\\//a/\\/\\b", second_to_last.path);

            const third_to_last = it.previous().?;
            try std.testing.expectEqualStrings("a", third_to_last.name);
            try std.testing.expectEqualStrings("C:\\\\//a", third_to_last.path);

            try std.testing.expect(null == it.previous());
        }
    }

    {
        const path = "/";
        var it = WindowsComponentIterator.init(path);
        try std.testing.expectEqual(1, it.root_len);
        try std.testing.expectEqual(1, it.root_end_index);
        try std.testing.expectEqualStrings("/", it.root().?);

        try std.testing.expect(null == it.first());
        try std.testing.expect(null == it.previous());
        try std.testing.expect(null == it.first());
        try std.testing.expect(null == it.next());

        try std.testing.expect(null == it.last());
        try std.testing.expect(null == it.previous());
        try std.testing.expect(null == it.last());
        try std.testing.expect(null == it.next());
    }

    {
        const path = "";
        var it = WindowsComponentIterator.init(path);
        try std.testing.expectEqual(0, it.root_len);
        try std.testing.expectEqual(0, it.root_end_index);
        try std.testing.expect(null == it.root());

        try std.testing.expect(null == it.first());
        try std.testing.expect(null == it.previous());
        try std.testing.expect(null == it.first());
        try std.testing.expect(null == it.next());

        try std.testing.expect(null == it.last());
        try std.testing.expect(null == it.previous());
        try std.testing.expect(null == it.last());
        try std.testing.expect(null == it.next());
    }
}

test "ComponentIterator windows WTF-16" {
    const WindowsComponentIterator = ComponentIterator(.windows, u16);
    const L = std.unicode.utf8ToUtf16LeStringLiteral;

    const path = L("C:\\a/b/c/");
    var it = WindowsComponentIterator.init(path);
    try std.testing.expectEqual(3, it.root_len);
    try std.testing.expectEqual(3, it.root_end_index);
    try std.testing.expectEqualSlices(u16, L("C:\\"), it.root().?);
    {
        const first = it.first().?;
        try std.testing.expectEqualSlices(u16, L("a"), first.name);
        try std.testing.expectEqualSlices(u16, L("C:\\a"), first.path);

        const second = it.next().?;
        try std.testing.expectEqualSlices(u16, L("b"), second.name);
        try std.testing.expectEqualSlices(u16, L("C:\\a/b"), second.path);

        const third = it.next().?;
        try std.testing.expectEqualSlices(u16, L("c"), third.name);
        try std.testing.expectEqualSlices(u16, L("C:\\a/b/c"), third.path);

        try std.testing.expect(null == it.next());
    }
    {
        const last = it.last().?;
        try std.testing.expectEqualSlices(u16, L("c"), last.name);
        try std.testing.expectEqualSlices(u16, L("C:\\a/b/c"), last.path);

        const second_to_last = it.previous().?;
        try std.testing.expectEqualSlices(u16, L("b"), second_to_last.name);
        try std.testing.expectEqualSlices(u16, L("C:\\a/b"), second_to_last.path);

        const third_to_last = it.previous().?;
        try std.testing.expectEqualSlices(u16, L("a"), third_to_last.name);
        try std.testing.expectEqualSlices(u16, L("C:\\a"), third_to_last.path);

        try std.testing.expect(null == it.previous());
    }
}

test "ComponentIterator roots" {
    // UEFI
    {
        var it = ComponentIterator(.uefi, u8).init("\\\\a");
        try std.testing.expectEqualStrings("\\", it.root().?);

        it = ComponentIterator(.uefi, u8).init("//a");
        try std.testing.expect(null == it.root());
    }
    // POSIX
    {
        var it = ComponentIterator(.posix, u8).init("//a");
        try std.testing.expectEqualStrings("/", it.root().?);

        it = ComponentIterator(.posix, u8).init("\\\\a");
        try std.testing.expect(null == it.root());
    }
    // Windows
    {
        // Drive relative
        var it = ComponentIterator(.windows, u8).init("C:a");
        try std.testing.expectEqualStrings("C:", it.root().?);

        // Drive absolute
        it = ComponentIterator(.windows, u8).init("C:/a");
        try std.testing.expectEqualStrings("C:/", it.root().?);
        it = ComponentIterator(.windows, u8).init("C:\\a");
        try std.testing.expectEqualStrings("C:\\", it.root().?);
        it = ComponentIterator(.windows, u8).init("C:///a");
        try std.testing.expectEqualStrings("C:/", it.root().?);

        // Rooted
        it = ComponentIterator(.windows, u8).init("\\a");
        try std.testing.expectEqualStrings("\\", it.root().?);
        it = ComponentIterator(.windows, u8).init("/a");
        try std.testing.expectEqualStrings("/", it.root().?);

        // Root local device
        it = ComponentIterator(.windows, u8).init("\\\\.");
        try std.testing.expectEqualStrings("\\\\.", it.root().?);
        it = ComponentIterator(.windows, u8).init("//?");
        try std.testing.expectEqualStrings("//?", it.root().?);

        // UNC absolute
        it = ComponentIterator(.windows, u8).init("//");
        try std.testing.expectEqualStrings("//", it.root().?);
        it = ComponentIterator(.windows, u8).init("\\\\a");
        try std.testing.expectEqualStrings("\\\\a", it.root().?);
        it = ComponentIterator(.windows, u8).init("\\\\a\\b\\\\c");
        try std.testing.expectEqualStrings("\\\\a\\b\\", it.root().?);
        it = ComponentIterator(.windows, u8).init("//a");
        try std.testing.expectEqualStrings("//a", it.root().?);
        it = ComponentIterator(.windows, u8).init("//a/b//c");
        try std.testing.expectEqualStrings("//a/b/", it.root().?);
        // Malformed UNC path with empty server name
        it = ComponentIterator(.windows, u8).init("\\\\\\a\\b\\c");
        try std.testing.expectEqualStrings("\\\\\\a\\", it.root().?);
    }
}

/// Format a path encoded as bytes for display as UTF-8.
/// Returns a Formatter for the given path. The path will be converted to valid UTF-8
/// during formatting. This is a lossy conversion if the path contains any ill-formed UTF-8.
/// Ill-formed UTF-8 byte sequences are replaced by the replacement character (U+FFFD)
/// according to "U+FFFD Substitution of Maximal Subparts" from Chapter 3 of
/// the Unicode standard, and as specified by https://encoding.spec.whatwg.org/#utf-8-decoder
pub const fmtAsUtf8Lossy = std.unicode.fmtUtf8;

/// Format a path encoded as WTF-16 LE for display as UTF-8.
/// Return a Formatter for a (potentially ill-formed) UTF-16 LE path.
/// The path will be converted to valid UTF-8 during formatting. This is
/// a lossy conversion if the path contains any unpaired surrogates.
/// Unpaired surrogates are replaced by the replacement character (U+FFFD).
pub const fmtWtf16LeAsUtf8Lossy = std.unicode.fmtUtf16Le;

/// Similar to `RTL_PATH_TYPE`, but without the `UNKNOWN` path type.
pub const Win32PathType = enum {
    /// `\\server\share\foo`
    unc_absolute,
    /// `C:\foo`
    drive_absolute,
    /// `C:foo`
    drive_relative,
    /// `\foo`
    rooted,
    /// `foo`
    relative,
    /// `\\.\foo`, `\\?\foo`
    local_device,
    /// `\\.`, `\\?`
    root_local_device,
};

/// Get the path type of a Win32 namespace path.
/// Similar to `RtlDetermineDosPathNameType_U`.
/// If `T` is `u16`, then `path` should be encoded as WTF-16LE.
pub fn getWin32PathType(comptime T: type, path: []const T) Win32PathType {
    if (path.len < 1) return .relative;

    const windows_path = std.fs.path.PathType.windows;
    if (windows_path.isSep(T, path[0])) {
        // \x
        if (path.len < 2 or !windows_path.isSep(T, path[1])) return .rooted;
        // \\. or \\?
        if (path.len > 2 and (path[2] == mem.nativeToLittle(T, '.') or path[2] == mem.nativeToLittle(T, '?'))) {
            // exactly \\. or \\? with nothing trailing
            if (path.len == 3) return .root_local_device;
            // \\.\x or \\?\x
            if (windows_path.isSep(T, path[3])) return .local_device;
        }
        // \\x
        return .unc_absolute;
    } else {
        // Some choice has to be made about how non-ASCII code points as drive-letters are handled, since
        // path[0] is a different size for WTF-16 vs WTF-8, leading to a potential mismatch in classification
        // for a WTF-8 path and its WTF-16 equivalent. For example, `€:\` encoded in WTF-16 is three code
        // units `<0x20AC>:\` whereas `€:\` encoded as WTF-8 is 6 code units `<0xE2><0x82><0xAC>:\` so
        // checking path[0], path[1] and path[2] would not behave the same between WTF-8/WTF-16.
        //
        // `RtlDetermineDosPathNameType_U` exclusively deals with WTF-16 and considers
        // `€:\` a drive-absolute path, but code points that take two WTF-16 code units to encode get
        // classified as a relative path (e.g. with U+20000 as the drive-letter that'd be encoded
        // in WTF-16 as `<0xD840><0xDC00>:\` and be considered a relative path).
        //
        // The choice made here is to emulate the behavior of `RtlDetermineDosPathNameType_U` for both
        // WTF-16 and WTF-8. This is because, while unlikely and not supported by the Disk Manager GUI,
        // drive letters are not actually restricted to A-Z. Using `SetVolumeMountPointW` will allow you
        // to set any byte value as a drive letter, and going through `IOCTL_MOUNTMGR_CREATE_POINT` will
        // allow you to set any WTF-16 code unit as a drive letter.
        //
        // Non-A-Z drive letters don't interact well with most of Windows, but certain things do work, e.g.
        // `cd /D €:\` will work, filesystem functions still work, etc.
        //
        // The unfortunate part of this is that this makes handling WTF-8 more complicated as we can't
        // just check path[0], path[1], path[2].
        const colon_i: usize = switch (T) {
            u8 => i: {
                const code_point_len = std.unicode.utf8ByteSequenceLength(path[0]) catch return .relative;
                // Conveniently, 4-byte sequences in WTF-8 have the same starting code point
                // as 2-code-unit sequences in WTF-16.
                if (code_point_len > 3) return .relative;
                break :i code_point_len;
            },
            u16 => 1,
            else => @compileError("unsupported type: " ++ @typeName(T)),
        };
        // x
        if (path.len < colon_i + 1 or path[colon_i] != mem.nativeToLittle(T, ':')) return .relative;
        // x:\
        if (path.len > colon_i + 1 and windows_path.isSep(T, path[colon_i + 1])) return .drive_absolute;
        // x:
        return .drive_relative;
    }
}

test getWin32PathType {
    try std.testing.expectEqual(.relative, getWin32PathType(u8, ""));
    try std.testing.expectEqual(.relative, getWin32PathType(u8, "x"));
    try std.testing.expectEqual(.relative, getWin32PathType(u8, "x\\"));

    try std.testing.expectEqual(.root_local_device, getWin32PathType(u8, "//."));
    try std.testing.expectEqual(.root_local_device, getWin32PathType(u8, "/\\?"));
    try std.testing.expectEqual(.root_local_device, getWin32PathType(u8, "\\\\?"));

    try std.testing.expectEqual(.local_device, getWin32PathType(u8, "//./x"));
    try std.testing.expectEqual(.local_device, getWin32PathType(u8, "/\\?\\x"));
    try std.testing.expectEqual(.local_device, getWin32PathType(u8, "\\\\?\\x"));
    // local device paths require a path separator after the root, otherwise it is considered a UNC path
    try std.testing.expectEqual(.unc_absolute, getWin32PathType(u8, "\\\\?x"));
    try std.testing.expectEqual(.unc_absolute, getWin32PathType(u8, "//.x"));

    try std.testing.expectEqual(.unc_absolute, getWin32PathType(u8, "//"));
    try std.testing.expectEqual(.unc_absolute, getWin32PathType(u8, "\\\\x"));
    try std.testing.expectEqual(.unc_absolute, getWin32PathType(u8, "//x"));

    try std.testing.expectEqual(.rooted, getWin32PathType(u8, "\\x"));
    try std.testing.expectEqual(.rooted, getWin32PathType(u8, "/"));

    try std.testing.expectEqual(.drive_relative, getWin32PathType(u8, "x:"));
    try std.testing.expectEqual(.drive_relative, getWin32PathType(u8, "x:abc"));
    try std.testing.expectEqual(.drive_relative, getWin32PathType(u8, "x:a/b/c"));

    try std.testing.expectEqual(.drive_absolute, getWin32PathType(u8, "x:\\"));
    try std.testing.expectEqual(.drive_absolute, getWin32PathType(u8, "x:\\abc"));
    try std.testing.expectEqual(.drive_absolute, getWin32PathType(u8, "x:/a/b/c"));

    // Non-ASCII code point that is encoded as one WTF-16 code unit is considered a valid drive letter
    try std.testing.expectEqual(.drive_absolute, getWin32PathType(u8, "€:\\"));
    try std.testing.expectEqual(.drive_absolute, getWin32PathType(u16, std.unicode.wtf8ToWtf16LeStringLiteral("€:\\")));
    try std.testing.expectEqual(.drive_relative, getWin32PathType(u8, "€:"));
    try std.testing.expectEqual(.drive_relative, getWin32PathType(u16, std.unicode.wtf8ToWtf16LeStringLiteral("€:")));
    // But code points that are encoded as two WTF-16 code units are not
    try std.testing.expectEqual(.relative, getWin32PathType(u8, "\u{10000}:\\"));
    try std.testing.expectEqual(.relative, getWin32PathType(u16, std.unicode.wtf8ToWtf16LeStringLiteral("\u{10000}:\\")));
}
