1const builtin = @import("builtin");
2const std = @import("../std.zig");
3const debug = std.debug;
4const assert = debug.assert;
5const testing = std.testing;
6const mem = std.mem;
7const fmt = std.fmt;
8const Allocator = mem.Allocator;
9const math = std.math;
10const windows = std.os.windows;
11const fs = std.fs;
12const process = std.process;
13const native_os = builtin.target.os.tag;
14
15pub const sep_windows = '\\';
16pub const sep_posix = '/';
17pub const sep = if (native_os == .windows) sep_windows else sep_posix;
18
19pub const sep_str_windows = "\\";
20pub const sep_str_posix = "/";
21pub const sep_str = if (native_os == .windows) sep_str_windows else sep_str_posix;
22
23pub const delimiter_windows = ';';
24pub const delimiter_posix = ':';
25pub const delimiter = if (native_os == .windows) delimiter_windows else delimiter_posix;
26
27/// Returns if the given byte is a valid path separator
28pub fn isSep(byte: u8) bool {
29    if (native_os == .windows) {
30        return byte == '/' or byte == '\\';
31    } else {
32        return byte == '/';
33    }
34}
35
36/// This is different from mem.join in that the separator will not be repeated if
37/// it is found at the end or beginning of a pair of consecutive paths.
38fn joinSepMaybeZ(allocator: Allocator, separator: u8, sepPredicate: fn (u8) bool, paths: []const []const u8, zero: bool) ![]u8 {
39    if (paths.len == 0) return if (zero) try allocator.dupe(u8, &[1]u8{0}) else &[0]u8{};
40
41    // Find first non-empty path index.
42    const first_path_index = blk: {
43        for (paths) |path, index| {
44            if (path.len == 0) continue else break :blk index;
45        }
46
47        // All paths provided were empty, so return early.
48        return if (zero) try allocator.dupe(u8, &[1]u8{0}) else &[0]u8{};
49    };
50
51    // Calculate length needed for resulting joined path buffer.
52    const total_len = blk: {
53        var sum: usize = paths[first_path_index].len;
54        var prev_path = paths[first_path_index];
55        assert(prev_path.len > 0);
56        var i: usize = first_path_index + 1;
57        while (i < paths.len) : (i += 1) {
58            const this_path = paths[i];
59            if (this_path.len == 0) continue;
60            const prev_sep = sepPredicate(prev_path[prev_path.len - 1]);
61            const this_sep = sepPredicate(this_path[0]);
62            sum += @boolToInt(!prev_sep and !this_sep);
63            sum += if (prev_sep and this_sep) this_path.len - 1 else this_path.len;
64            prev_path = this_path;
65        }
66
67        if (zero) sum += 1;
68        break :blk sum;
69    };
70
71    const buf = try allocator.alloc(u8, total_len);
72    errdefer allocator.free(buf);
73
74    mem.copy(u8, buf, paths[first_path_index]);
75    var buf_index: usize = paths[first_path_index].len;
76    var prev_path = paths[first_path_index];
77    assert(prev_path.len > 0);
78    var i: usize = first_path_index + 1;
79    while (i < paths.len) : (i += 1) {
80        const this_path = paths[i];
81        if (this_path.len == 0) continue;
82        const prev_sep = sepPredicate(prev_path[prev_path.len - 1]);
83        const this_sep = sepPredicate(this_path[0]);
84        if (!prev_sep and !this_sep) {
85            buf[buf_index] = separator;
86            buf_index += 1;
87        }
88        const adjusted_path = if (prev_sep and this_sep) this_path[1..] else this_path;
89        mem.copy(u8, buf[buf_index..], adjusted_path);
90        buf_index += adjusted_path.len;
91        prev_path = this_path;
92    }
93
94    if (zero) buf[buf.len - 1] = 0;
95
96    // No need for shrink since buf is exactly the correct size.
97    return buf;
98}
99
100/// Naively combines a series of paths with the native path seperator.
101/// Allocates memory for the result, which must be freed by the caller.
102pub fn join(allocator: Allocator, paths: []const []const u8) ![]u8 {
103    return joinSepMaybeZ(allocator, sep, isSep, paths, false);
104}
105
106/// Naively combines a series of paths with the native path seperator and null terminator.
107/// Allocates memory for the result, which must be freed by the caller.
108pub fn joinZ(allocator: Allocator, paths: []const []const u8) ![:0]u8 {
109    const out = try joinSepMaybeZ(allocator, sep, isSep, paths, true);
110    return out[0 .. out.len - 1 :0];
111}
112
113fn testJoinMaybeZWindows(paths: []const []const u8, expected: []const u8, zero: bool) !void {
114    const windowsIsSep = struct {
115        fn isSep(byte: u8) bool {
116            return byte == '/' or byte == '\\';
117        }
118    }.isSep;
119    const actual = try joinSepMaybeZ(testing.allocator, sep_windows, windowsIsSep, paths, zero);
120    defer testing.allocator.free(actual);
121    try testing.expectEqualSlices(u8, expected, if (zero) actual[0 .. actual.len - 1 :0] else actual);
122}
123
124fn testJoinMaybeZPosix(paths: []const []const u8, expected: []const u8, zero: bool) !void {
125    const posixIsSep = struct {
126        fn isSep(byte: u8) bool {
127            return byte == '/';
128        }
129    }.isSep;
130    const actual = try joinSepMaybeZ(testing.allocator, sep_posix, posixIsSep, paths, zero);
131    defer testing.allocator.free(actual);
132    try testing.expectEqualSlices(u8, expected, if (zero) actual[0 .. actual.len - 1 :0] else actual);
133}
134
135test "join" {
136    {
137        const actual: []u8 = try join(testing.allocator, &[_][]const u8{});
138        defer testing.allocator.free(actual);
139        try testing.expectEqualSlices(u8, "", actual);
140    }
141    {
142        const actual: [:0]u8 = try joinZ(testing.allocator, &[_][]const u8{});
143        defer testing.allocator.free(actual);
144        try testing.expectEqualSlices(u8, "", actual);
145    }
146    for (&[_]bool{ false, true }) |zero| {
147        try testJoinMaybeZWindows(&[_][]const u8{}, "", zero);
148        try testJoinMaybeZWindows(&[_][]const u8{ "c:\\a\\b", "c" }, "c:\\a\\b\\c", zero);
149        try testJoinMaybeZWindows(&[_][]const u8{ "c:\\a\\b", "c" }, "c:\\a\\b\\c", zero);
150        try testJoinMaybeZWindows(&[_][]const u8{ "c:\\a\\b\\", "c" }, "c:\\a\\b\\c", zero);
151
152        try testJoinMaybeZWindows(&[_][]const u8{ "c:\\", "a", "b\\", "c" }, "c:\\a\\b\\c", zero);
153        try testJoinMaybeZWindows(&[_][]const u8{ "c:\\a\\", "b\\", "c" }, "c:\\a\\b\\c", zero);
154
155        try testJoinMaybeZWindows(
156            &[_][]const u8{ "c:\\home\\andy\\dev\\zig\\build\\lib\\zig\\std", "io.zig" },
157            "c:\\home\\andy\\dev\\zig\\build\\lib\\zig\\std\\io.zig",
158            zero,
159        );
160
161        try testJoinMaybeZWindows(&[_][]const u8{ "c:\\", "a", "b/", "c" }, "c:\\a\\b/c", zero);
162        try testJoinMaybeZWindows(&[_][]const u8{ "c:\\a/", "b\\", "/c" }, "c:\\a/b\\c", zero);
163
164        try testJoinMaybeZWindows(&[_][]const u8{ "", "c:\\", "", "", "a", "b\\", "c", "" }, "c:\\a\\b\\c", zero);
165        try testJoinMaybeZWindows(&[_][]const u8{ "c:\\a/", "", "b\\", "", "/c" }, "c:\\a/b\\c", zero);
166        try testJoinMaybeZWindows(&[_][]const u8{ "", "" }, "", zero);
167
168        try testJoinMaybeZPosix(&[_][]const u8{}, "", zero);
169        try testJoinMaybeZPosix(&[_][]const u8{ "/a/b", "c" }, "/a/b/c", zero);
170        try testJoinMaybeZPosix(&[_][]const u8{ "/a/b/", "c" }, "/a/b/c", zero);
171
172        try testJoinMaybeZPosix(&[_][]const u8{ "/", "a", "b/", "c" }, "/a/b/c", zero);
173        try testJoinMaybeZPosix(&[_][]const u8{ "/a/", "b/", "c" }, "/a/b/c", zero);
174
175        try testJoinMaybeZPosix(
176            &[_][]const u8{ "/home/andy/dev/zig/build/lib/zig/std", "io.zig" },
177            "/home/andy/dev/zig/build/lib/zig/std/io.zig",
178            zero,
179        );
180
181        try testJoinMaybeZPosix(&[_][]const u8{ "a", "/c" }, "a/c", zero);
182        try testJoinMaybeZPosix(&[_][]const u8{ "a/", "/c" }, "a/c", zero);
183
184        try testJoinMaybeZPosix(&[_][]const u8{ "", "/", "a", "", "b/", "c", "" }, "/a/b/c", zero);
185        try testJoinMaybeZPosix(&[_][]const u8{ "/a/", "", "", "b/", "c" }, "/a/b/c", zero);
186        try testJoinMaybeZPosix(&[_][]const u8{ "", "" }, "", zero);
187    }
188}
189
190pub fn isAbsoluteZ(path_c: [*:0]const u8) bool {
191    if (native_os == .windows) {
192        return isAbsoluteWindowsZ(path_c);
193    } else {
194        return isAbsolutePosixZ(path_c);
195    }
196}
197
198pub fn isAbsolute(path: []const u8) bool {
199    if (native_os == .windows) {
200        return isAbsoluteWindows(path);
201    } else {
202        return isAbsolutePosix(path);
203    }
204}
205
206fn isAbsoluteWindowsImpl(comptime T: type, path: []const T) bool {
207    if (path.len < 1)
208        return false;
209
210    if (path[0] == '/')
211        return true;
212
213    if (path[0] == '\\')
214        return true;
215
216    if (path.len < 3)
217        return false;
218
219    if (path[1] == ':') {
220        if (path[2] == '/')
221            return true;
222        if (path[2] == '\\')
223            return true;
224    }
225
226    return false;
227}
228
229pub fn isAbsoluteWindows(path: []const u8) bool {
230    return isAbsoluteWindowsImpl(u8, path);
231}
232
233pub fn isAbsoluteWindowsW(path_w: [*:0]const u16) bool {
234    return isAbsoluteWindowsImpl(u16, mem.sliceTo(path_w, 0));
235}
236
237pub fn isAbsoluteWindowsWTF16(path: []const u16) bool {
238    return isAbsoluteWindowsImpl(u16, path);
239}
240
241pub fn isAbsoluteWindowsZ(path_c: [*:0]const u8) bool {
242    return isAbsoluteWindowsImpl(u8, mem.sliceTo(path_c, 0));
243}
244
245pub fn isAbsolutePosix(path: []const u8) bool {
246    return path.len > 0 and path[0] == sep_posix;
247}
248
249pub fn isAbsolutePosixZ(path_c: [*:0]const u8) bool {
250    return isAbsolutePosix(mem.sliceTo(path_c, 0));
251}
252
253test "isAbsoluteWindows" {
254    try testIsAbsoluteWindows("", false);
255    try testIsAbsoluteWindows("/", true);
256    try testIsAbsoluteWindows("//", true);
257    try testIsAbsoluteWindows("//server", true);
258    try testIsAbsoluteWindows("//server/file", true);
259    try testIsAbsoluteWindows("\\\\server\\file", true);
260    try testIsAbsoluteWindows("\\\\server", true);
261    try testIsAbsoluteWindows("\\\\", true);
262    try testIsAbsoluteWindows("c", false);
263    try testIsAbsoluteWindows("c:", false);
264    try testIsAbsoluteWindows("c:\\", true);
265    try testIsAbsoluteWindows("c:/", true);
266    try testIsAbsoluteWindows("c://", true);
267    try testIsAbsoluteWindows("C:/Users/", true);
268    try testIsAbsoluteWindows("C:\\Users\\", true);
269    try testIsAbsoluteWindows("C:cwd/another", false);
270    try testIsAbsoluteWindows("C:cwd\\another", false);
271    try testIsAbsoluteWindows("directory/directory", false);
272    try testIsAbsoluteWindows("directory\\directory", false);
273    try testIsAbsoluteWindows("/usr/local", true);
274}
275
276test "isAbsolutePosix" {
277    try testIsAbsolutePosix("", false);
278    try testIsAbsolutePosix("/home/foo", true);
279    try testIsAbsolutePosix("/home/foo/..", true);
280    try testIsAbsolutePosix("bar/", false);
281    try testIsAbsolutePosix("./baz", false);
282}
283
284fn testIsAbsoluteWindows(path: []const u8, expected_result: bool) !void {
285    try testing.expectEqual(expected_result, isAbsoluteWindows(path));
286}
287
288fn testIsAbsolutePosix(path: []const u8, expected_result: bool) !void {
289    try testing.expectEqual(expected_result, isAbsolutePosix(path));
290}
291
292pub const WindowsPath = struct {
293    is_abs: bool,
294    kind: Kind,
295    disk_designator: []const u8,
296
297    pub const Kind = enum {
298        None,
299        Drive,
300        NetworkShare,
301    };
302};
303
304pub fn windowsParsePath(path: []const u8) WindowsPath {
305    if (path.len >= 2 and path[1] == ':') {
306        return WindowsPath{
307            .is_abs = isAbsoluteWindows(path),
308            .kind = WindowsPath.Kind.Drive,
309            .disk_designator = path[0..2],
310        };
311    }
312    if (path.len >= 1 and (path[0] == '/' or path[0] == '\\') and
313        (path.len == 1 or (path[1] != '/' and path[1] != '\\')))
314    {
315        return WindowsPath{
316            .is_abs = true,
317            .kind = WindowsPath.Kind.None,
318            .disk_designator = path[0..0],
319        };
320    }
321    const relative_path = WindowsPath{
322        .kind = WindowsPath.Kind.None,
323        .disk_designator = &[_]u8{},
324        .is_abs = false,
325    };
326    if (path.len < "//a/b".len) {
327        return relative_path;
328    }
329
330    inline for ("/\\") |this_sep| {
331        const two_sep = [_]u8{ this_sep, this_sep };
332        if (mem.startsWith(u8, path, &two_sep)) {
333            if (path[2] == this_sep) {
334                return relative_path;
335            }
336
337            var it = mem.tokenize(u8, path, &[_]u8{this_sep});
338            _ = (it.next() orelse return relative_path);
339            _ = (it.next() orelse return relative_path);
340            return WindowsPath{
341                .is_abs = isAbsoluteWindows(path),
342                .kind = WindowsPath.Kind.NetworkShare,
343                .disk_designator = path[0..it.index],
344            };
345        }
346    }
347    return relative_path;
348}
349
350test "windowsParsePath" {
351    {
352        const parsed = windowsParsePath("//a/b");
353        try testing.expect(parsed.is_abs);
354        try testing.expect(parsed.kind == WindowsPath.Kind.NetworkShare);
355        try testing.expect(mem.eql(u8, parsed.disk_designator, "//a/b"));
356    }
357    {
358        const parsed = windowsParsePath("\\\\a\\b");
359        try testing.expect(parsed.is_abs);
360        try testing.expect(parsed.kind == WindowsPath.Kind.NetworkShare);
361        try testing.expect(mem.eql(u8, parsed.disk_designator, "\\\\a\\b"));
362    }
363    {
364        const parsed = windowsParsePath("\\\\a\\");
365        try testing.expect(!parsed.is_abs);
366        try testing.expect(parsed.kind == WindowsPath.Kind.None);
367        try testing.expect(mem.eql(u8, parsed.disk_designator, ""));
368    }
369    {
370        const parsed = windowsParsePath("/usr/local");
371        try testing.expect(parsed.is_abs);
372        try testing.expect(parsed.kind == WindowsPath.Kind.None);
373        try testing.expect(mem.eql(u8, parsed.disk_designator, ""));
374    }
375    {
376        const parsed = windowsParsePath("c:../");
377        try testing.expect(!parsed.is_abs);
378        try testing.expect(parsed.kind == WindowsPath.Kind.Drive);
379        try testing.expect(mem.eql(u8, parsed.disk_designator, "c:"));
380    }
381}
382
383pub fn diskDesignator(path: []const u8) []const u8 {
384    if (native_os == .windows) {
385        return diskDesignatorWindows(path);
386    } else {
387        return "";
388    }
389}
390
391pub fn diskDesignatorWindows(path: []const u8) []const u8 {
392    return windowsParsePath(path).disk_designator;
393}
394
395fn networkShareServersEql(ns1: []const u8, ns2: []const u8) bool {
396    const sep1 = ns1[0];
397    const sep2 = ns2[0];
398
399    var it1 = mem.tokenize(u8, ns1, &[_]u8{sep1});
400    var it2 = mem.tokenize(u8, ns2, &[_]u8{sep2});
401
402    // TODO ASCII is wrong, we actually need full unicode support to compare paths.
403    return asciiEqlIgnoreCase(it1.next().?, it2.next().?);
404}
405
406fn compareDiskDesignators(kind: WindowsPath.Kind, p1: []const u8, p2: []const u8) bool {
407    switch (kind) {
408        WindowsPath.Kind.None => {
409            assert(p1.len == 0);
410            assert(p2.len == 0);
411            return true;
412        },
413        WindowsPath.Kind.Drive => {
414            return asciiUpper(p1[0]) == asciiUpper(p2[0]);
415        },
416        WindowsPath.Kind.NetworkShare => {
417            const sep1 = p1[0];
418            const sep2 = p2[0];
419
420            var it1 = mem.tokenize(u8, p1, &[_]u8{sep1});
421            var it2 = mem.tokenize(u8, p2, &[_]u8{sep2});
422
423            // TODO ASCII is wrong, we actually need full unicode support to compare paths.
424            return asciiEqlIgnoreCase(it1.next().?, it2.next().?) and asciiEqlIgnoreCase(it1.next().?, it2.next().?);
425        },
426    }
427}
428
429fn asciiUpper(byte: u8) u8 {
430    return switch (byte) {
431        'a'...'z' => 'A' + (byte - 'a'),
432        else => byte,
433    };
434}
435
436fn asciiEqlIgnoreCase(s1: []const u8, s2: []const u8) bool {
437    if (s1.len != s2.len)
438        return false;
439    var i: usize = 0;
440    while (i < s1.len) : (i += 1) {
441        if (asciiUpper(s1[i]) != asciiUpper(s2[i]))
442            return false;
443    }
444    return true;
445}
446
447/// On Windows, this calls `resolveWindows` and on POSIX it calls `resolvePosix`.
448pub fn resolve(allocator: Allocator, paths: []const []const u8) ![]u8 {
449    if (native_os == .windows) {
450        return resolveWindows(allocator, paths);
451    } else {
452        return resolvePosix(allocator, paths);
453    }
454}
455
456/// This function is like a series of `cd` statements executed one after another.
457/// It resolves "." and "..".
458/// The result does not have a trailing path separator.
459/// If all paths are relative it uses the current working directory as a starting point.
460/// Each drive has its own current working directory.
461/// Path separators are canonicalized to '\\' and drives are canonicalized to capital letters.
462/// Note: all usage of this function should be audited due to the existence of symlinks.
463/// Without performing actual syscalls, resolving `..` could be incorrect.
464pub fn resolveWindows(allocator: Allocator, paths: []const []const u8) ![]u8 {
465    if (paths.len == 0) {
466        assert(native_os == .windows); // resolveWindows called on non windows can't use getCwd
467        return process.getCwdAlloc(allocator);
468    }
469
470    // determine which disk designator we will result with, if any
471    var result_drive_buf = "_:".*;
472    var result_disk_designator: []const u8 = "";
473    var have_drive_kind = WindowsPath.Kind.None;
474    var have_abs_path = false;
475    var first_index: usize = 0;
476    var max_size: usize = 0;
477    for (paths) |p, i| {
478        const parsed = windowsParsePath(p);
479        if (parsed.is_abs) {
480            have_abs_path = true;
481            first_index = i;
482            max_size = result_disk_designator.len;
483        }
484        switch (parsed.kind) {
485            WindowsPath.Kind.Drive => {
486                result_drive_buf[0] = asciiUpper(parsed.disk_designator[0]);
487                result_disk_designator = result_drive_buf[0..];
488                have_drive_kind = WindowsPath.Kind.Drive;
489            },
490            WindowsPath.Kind.NetworkShare => {
491                result_disk_designator = parsed.disk_designator;
492                have_drive_kind = WindowsPath.Kind.NetworkShare;
493            },
494            WindowsPath.Kind.None => {},
495        }
496        max_size += p.len + 1;
497    }
498
499    // if we will result with a disk designator, loop again to determine
500    // which is the last time the disk designator is absolutely specified, if any
501    // and count up the max bytes for paths related to this disk designator
502    if (have_drive_kind != WindowsPath.Kind.None) {
503        have_abs_path = false;
504        first_index = 0;
505        max_size = result_disk_designator.len;
506        var correct_disk_designator = false;
507
508        for (paths) |p, i| {
509            const parsed = windowsParsePath(p);
510            if (parsed.kind != WindowsPath.Kind.None) {
511                if (parsed.kind == have_drive_kind) {
512                    correct_disk_designator = compareDiskDesignators(have_drive_kind, result_disk_designator, parsed.disk_designator);
513                } else {
514                    continue;
515                }
516            }
517            if (!correct_disk_designator) {
518                continue;
519            }
520            if (parsed.is_abs) {
521                first_index = i;
522                max_size = result_disk_designator.len;
523                have_abs_path = true;
524            }
525            max_size += p.len + 1;
526        }
527    }
528
529    // Allocate result and fill in the disk designator, calling getCwd if we have to.
530    var result: []u8 = undefined;
531    var result_index: usize = 0;
532
533    if (have_abs_path) {
534        switch (have_drive_kind) {
535            WindowsPath.Kind.Drive => {
536                result = try allocator.alloc(u8, max_size);
537
538                mem.copy(u8, result, result_disk_designator);
539                result_index += result_disk_designator.len;
540            },
541            WindowsPath.Kind.NetworkShare => {
542                result = try allocator.alloc(u8, max_size);
543                var it = mem.tokenize(u8, paths[first_index], "/\\");
544                const server_name = it.next().?;
545                const other_name = it.next().?;
546
547                result[result_index] = '\\';
548                result_index += 1;
549                result[result_index] = '\\';
550                result_index += 1;
551                mem.copy(u8, result[result_index..], server_name);
552                result_index += server_name.len;
553                result[result_index] = '\\';
554                result_index += 1;
555                mem.copy(u8, result[result_index..], other_name);
556                result_index += other_name.len;
557
558                result_disk_designator = result[0..result_index];
559            },
560            WindowsPath.Kind.None => {
561                assert(native_os == .windows); // resolveWindows called on non windows can't use getCwd
562                const cwd = try process.getCwdAlloc(allocator);
563                defer allocator.free(cwd);
564                const parsed_cwd = windowsParsePath(cwd);
565                result = try allocator.alloc(u8, max_size + parsed_cwd.disk_designator.len + 1);
566                mem.copy(u8, result, parsed_cwd.disk_designator);
567                result_index += parsed_cwd.disk_designator.len;
568                result_disk_designator = result[0..parsed_cwd.disk_designator.len];
569                if (parsed_cwd.kind == WindowsPath.Kind.Drive) {
570                    result[0] = asciiUpper(result[0]);
571                }
572                have_drive_kind = parsed_cwd.kind;
573            },
574        }
575    } else {
576        assert(native_os == .windows); // resolveWindows called on non windows can't use getCwd
577        // TODO call get cwd for the result_disk_designator instead of the global one
578        const cwd = try process.getCwdAlloc(allocator);
579        defer allocator.free(cwd);
580
581        result = try allocator.alloc(u8, max_size + cwd.len + 1);
582
583        mem.copy(u8, result, cwd);
584        result_index += cwd.len;
585        const parsed_cwd = windowsParsePath(result[0..result_index]);
586        result_disk_designator = parsed_cwd.disk_designator;
587        if (parsed_cwd.kind == WindowsPath.Kind.Drive) {
588            result[0] = asciiUpper(result[0]);
589            // Remove the trailing slash if present, eg. if the cwd is a root
590            // directory.
591            if (cwd.len > 0 and cwd[cwd.len - 1] == sep_windows) {
592                result_index -= 1;
593            }
594        }
595        have_drive_kind = parsed_cwd.kind;
596    }
597    errdefer allocator.free(result);
598
599    // Now we know the disk designator to use, if any, and what kind it is. And our result
600    // is big enough to append all the paths to.
601    var correct_disk_designator = true;
602    for (paths[first_index..]) |p| {
603        const parsed = windowsParsePath(p);
604
605        if (parsed.kind != WindowsPath.Kind.None) {
606            if (parsed.kind == have_drive_kind) {
607                correct_disk_designator = compareDiskDesignators(have_drive_kind, result_disk_designator, parsed.disk_designator);
608            } else {
609                continue;
610            }
611        }
612        if (!correct_disk_designator) {
613            continue;
614        }
615        var it = mem.tokenize(u8, p[parsed.disk_designator.len..], "/\\");
616        while (it.next()) |component| {
617            if (mem.eql(u8, component, ".")) {
618                continue;
619            } else if (mem.eql(u8, component, "..")) {
620                while (true) {
621                    if (result_index == 0 or result_index == result_disk_designator.len)
622                        break;
623                    result_index -= 1;
624                    if (result[result_index] == '\\' or result[result_index] == '/')
625                        break;
626                }
627            } else {
628                result[result_index] = sep_windows;
629                result_index += 1;
630                mem.copy(u8, result[result_index..], component);
631                result_index += component.len;
632            }
633        }
634    }
635
636    if (result_index == result_disk_designator.len) {
637        result[result_index] = '\\';
638        result_index += 1;
639    }
640
641    return allocator.shrink(result, result_index);
642}
643
644/// This function is like a series of `cd` statements executed one after another.
645/// It resolves "." and "..".
646/// The result does not have a trailing path separator.
647/// If all paths are relative it uses the current working directory as a starting point.
648/// Note: all usage of this function should be audited due to the existence of symlinks.
649/// Without performing actual syscalls, resolving `..` could be incorrect.
650pub fn resolvePosix(allocator: Allocator, paths: []const []const u8) ![]u8 {
651    if (paths.len == 0) {
652        assert(native_os != .windows); // resolvePosix called on windows can't use getCwd
653        return process.getCwdAlloc(allocator);
654    }
655
656    var first_index: usize = 0;
657    var have_abs = false;
658    var max_size: usize = 0;
659    for (paths) |p, i| {
660        if (isAbsolutePosix(p)) {
661            first_index = i;
662            have_abs = true;
663            max_size = 0;
664        }
665        max_size += p.len + 1;
666    }
667
668    var result: []u8 = undefined;
669    var result_index: usize = 0;
670
671    if (have_abs) {
672        result = try allocator.alloc(u8, max_size);
673    } else {
674        assert(native_os != .windows); // resolvePosix called on windows can't use getCwd
675        const cwd = try process.getCwdAlloc(allocator);
676        defer allocator.free(cwd);
677        result = try allocator.alloc(u8, max_size + cwd.len + 1);
678        mem.copy(u8, result, cwd);
679        result_index += cwd.len;
680    }
681    errdefer allocator.free(result);
682
683    for (paths[first_index..]) |p| {
684        var it = mem.tokenize(u8, p, "/");
685        while (it.next()) |component| {
686            if (mem.eql(u8, component, ".")) {
687                continue;
688            } else if (mem.eql(u8, component, "..")) {
689                while (true) {
690                    if (result_index == 0)
691                        break;
692                    result_index -= 1;
693                    if (result[result_index] == '/')
694                        break;
695                }
696            } else {
697                result[result_index] = '/';
698                result_index += 1;
699                mem.copy(u8, result[result_index..], component);
700                result_index += component.len;
701            }
702        }
703    }
704
705    if (result_index == 0) {
706        result[0] = '/';
707        result_index += 1;
708    }
709
710    return allocator.shrink(result, result_index);
711}
712
713test "resolve" {
714    if (native_os == .wasi) return error.SkipZigTest;
715
716    const cwd = try process.getCwdAlloc(testing.allocator);
717    defer testing.allocator.free(cwd);
718    if (native_os == .windows) {
719        if (windowsParsePath(cwd).kind == WindowsPath.Kind.Drive) {
720            cwd[0] = asciiUpper(cwd[0]);
721        }
722        try testResolveWindows(&[_][]const u8{"."}, cwd);
723    } else {
724        try testResolvePosix(&[_][]const u8{ "a/b/c/", "../../.." }, cwd);
725        try testResolvePosix(&[_][]const u8{"."}, cwd);
726    }
727}
728
729test "resolveWindows" {
730    if (builtin.target.cpu.arch == .aarch64) {
731        // TODO https://github.com/ziglang/zig/issues/3288
732        return error.SkipZigTest;
733    }
734    if (native_os == .wasi) return error.SkipZigTest;
735    if (native_os == .windows) {
736        const cwd = try process.getCwdAlloc(testing.allocator);
737        defer testing.allocator.free(cwd);
738        const parsed_cwd = windowsParsePath(cwd);
739        {
740            const expected = try join(testing.allocator, &[_][]const u8{
741                parsed_cwd.disk_designator,
742                "usr\\local\\lib\\zig\\std\\array_list.zig",
743            });
744            defer testing.allocator.free(expected);
745            if (parsed_cwd.kind == WindowsPath.Kind.Drive) {
746                expected[0] = asciiUpper(parsed_cwd.disk_designator[0]);
747            }
748            try testResolveWindows(&[_][]const u8{ "/usr/local", "lib\\zig\\std\\array_list.zig" }, expected);
749        }
750        {
751            const expected = try join(testing.allocator, &[_][]const u8{
752                cwd,
753                "usr\\local\\lib\\zig",
754            });
755            defer testing.allocator.free(expected);
756            if (parsed_cwd.kind == WindowsPath.Kind.Drive) {
757                expected[0] = asciiUpper(parsed_cwd.disk_designator[0]);
758            }
759            try testResolveWindows(&[_][]const u8{ "usr/local", "lib\\zig" }, expected);
760        }
761    }
762
763    try testResolveWindows(&[_][]const u8{ "c:\\a\\b\\c", "/hi", "ok" }, "C:\\hi\\ok");
764    try testResolveWindows(&[_][]const u8{ "c:/blah\\blah", "d:/games", "c:../a" }, "C:\\blah\\a");
765    try testResolveWindows(&[_][]const u8{ "c:/blah\\blah", "d:/games", "C:../a" }, "C:\\blah\\a");
766    try testResolveWindows(&[_][]const u8{ "c:/ignore", "d:\\a/b\\c/d", "\\e.exe" }, "D:\\e.exe");
767    try testResolveWindows(&[_][]const u8{ "c:/ignore", "c:/some/file" }, "C:\\some\\file");
768    try testResolveWindows(&[_][]const u8{ "d:/ignore", "d:some/dir//" }, "D:\\ignore\\some\\dir");
769    try testResolveWindows(&[_][]const u8{ "//server/share", "..", "relative\\" }, "\\\\server\\share\\relative");
770    try testResolveWindows(&[_][]const u8{ "c:/", "//" }, "C:\\");
771    try testResolveWindows(&[_][]const u8{ "c:/", "//dir" }, "C:\\dir");
772    try testResolveWindows(&[_][]const u8{ "c:/", "//server/share" }, "\\\\server\\share\\");
773    try testResolveWindows(&[_][]const u8{ "c:/", "//server//share" }, "\\\\server\\share\\");
774    try testResolveWindows(&[_][]const u8{ "c:/", "///some//dir" }, "C:\\some\\dir");
775    try testResolveWindows(&[_][]const u8{ "C:\\foo\\tmp.3\\", "..\\tmp.3\\cycles\\root.js" }, "C:\\foo\\tmp.3\\cycles\\root.js");
776}
777
778test "resolvePosix" {
779    if (native_os == .wasi) return error.SkipZigTest;
780
781    try testResolvePosix(&[_][]const u8{ "/a/b", "c" }, "/a/b/c");
782    try testResolvePosix(&[_][]const u8{ "/a/b", "c", "//d", "e///" }, "/d/e");
783    try testResolvePosix(&[_][]const u8{ "/a/b/c", "..", "../" }, "/a");
784    try testResolvePosix(&[_][]const u8{ "/", "..", ".." }, "/");
785    try testResolvePosix(&[_][]const u8{"/a/b/c/"}, "/a/b/c");
786
787    try testResolvePosix(&[_][]const u8{ "/var/lib", "../", "file/" }, "/var/file");
788    try testResolvePosix(&[_][]const u8{ "/var/lib", "/../", "file/" }, "/file");
789    try testResolvePosix(&[_][]const u8{ "/some/dir", ".", "/absolute/" }, "/absolute");
790    try testResolvePosix(&[_][]const u8{ "/foo/tmp.3/", "../tmp.3/cycles/root.js" }, "/foo/tmp.3/cycles/root.js");
791}
792
793fn testResolveWindows(paths: []const []const u8, expected: []const u8) !void {
794    const actual = try resolveWindows(testing.allocator, paths);
795    defer testing.allocator.free(actual);
796    try testing.expect(mem.eql(u8, actual, expected));
797}
798
799fn testResolvePosix(paths: []const []const u8, expected: []const u8) !void {
800    const actual = try resolvePosix(testing.allocator, paths);
801    defer testing.allocator.free(actual);
802    try testing.expect(mem.eql(u8, actual, expected));
803}
804
805/// Strip the last component from a file path.
806///
807/// If the path is a file in the current directory (no directory component)
808/// then returns null.
809///
810/// If the path is the root directory, returns null.
811pub fn dirname(path: []const u8) ?[]const u8 {
812    if (native_os == .windows) {
813        return dirnameWindows(path);
814    } else {
815        return dirnamePosix(path);
816    }
817}
818
819pub fn dirnameWindows(path: []const u8) ?[]const u8 {
820    if (path.len == 0)
821        return null;
822
823    const root_slice = diskDesignatorWindows(path);
824    if (path.len == root_slice.len)
825        return null;
826
827    const have_root_slash = path.len > root_slice.len and (path[root_slice.len] == '/' or path[root_slice.len] == '\\');
828
829    var end_index: usize = path.len - 1;
830
831    while (path[end_index] == '/' or path[end_index] == '\\') {
832        if (end_index == 0)
833            return null;
834        end_index -= 1;
835    }
836
837    while (path[end_index] != '/' and path[end_index] != '\\') {
838        if (end_index == 0)
839            return null;
840        end_index -= 1;
841    }
842
843    if (have_root_slash and end_index == root_slice.len) {
844        end_index += 1;
845    }
846
847    if (end_index == 0)
848        return null;
849
850    return path[0..end_index];
851}
852
853pub fn dirnamePosix(path: []const u8) ?[]const u8 {
854    if (path.len == 0)
855        return null;
856
857    var end_index: usize = path.len - 1;
858    while (path[end_index] == '/') {
859        if (end_index == 0)
860            return null;
861        end_index -= 1;
862    }
863
864    while (path[end_index] != '/') {
865        if (end_index == 0)
866            return null;
867        end_index -= 1;
868    }
869
870    if (end_index == 0 and path[0] == '/')
871        return path[0..1];
872
873    if (end_index == 0)
874        return null;
875
876    return path[0..end_index];
877}
878
879test "dirnamePosix" {
880    try testDirnamePosix("/a/b/c", "/a/b");
881    try testDirnamePosix("/a/b/c///", "/a/b");
882    try testDirnamePosix("/a", "/");
883    try testDirnamePosix("/", null);
884    try testDirnamePosix("//", null);
885    try testDirnamePosix("///", null);
886    try testDirnamePosix("////", null);
887    try testDirnamePosix("", null);
888    try testDirnamePosix("a", null);
889    try testDirnamePosix("a/", null);
890    try testDirnamePosix("a//", null);
891}
892
893test "dirnameWindows" {
894    try testDirnameWindows("c:\\", null);
895    try testDirnameWindows("c:\\foo", "c:\\");
896    try testDirnameWindows("c:\\foo\\", "c:\\");
897    try testDirnameWindows("c:\\foo\\bar", "c:\\foo");
898    try testDirnameWindows("c:\\foo\\bar\\", "c:\\foo");
899    try testDirnameWindows("c:\\foo\\bar\\baz", "c:\\foo\\bar");
900    try testDirnameWindows("\\", null);
901    try testDirnameWindows("\\foo", "\\");
902    try testDirnameWindows("\\foo\\", "\\");
903    try testDirnameWindows("\\foo\\bar", "\\foo");
904    try testDirnameWindows("\\foo\\bar\\", "\\foo");
905    try testDirnameWindows("\\foo\\bar\\baz", "\\foo\\bar");
906    try testDirnameWindows("c:", null);
907    try testDirnameWindows("c:foo", null);
908    try testDirnameWindows("c:foo\\", null);
909    try testDirnameWindows("c:foo\\bar", "c:foo");
910    try testDirnameWindows("c:foo\\bar\\", "c:foo");
911    try testDirnameWindows("c:foo\\bar\\baz", "c:foo\\bar");
912    try testDirnameWindows("file:stream", null);
913    try testDirnameWindows("dir\\file:stream", "dir");
914    try testDirnameWindows("\\\\unc\\share", null);
915    try testDirnameWindows("\\\\unc\\share\\foo", "\\\\unc\\share\\");
916    try testDirnameWindows("\\\\unc\\share\\foo\\", "\\\\unc\\share\\");
917    try testDirnameWindows("\\\\unc\\share\\foo\\bar", "\\\\unc\\share\\foo");
918    try testDirnameWindows("\\\\unc\\share\\foo\\bar\\", "\\\\unc\\share\\foo");
919    try testDirnameWindows("\\\\unc\\share\\foo\\bar\\baz", "\\\\unc\\share\\foo\\bar");
920    try testDirnameWindows("/a/b/", "/a");
921    try testDirnameWindows("/a/b", "/a");
922    try testDirnameWindows("/a", "/");
923    try testDirnameWindows("", null);
924    try testDirnameWindows("/", null);
925    try testDirnameWindows("////", null);
926    try testDirnameWindows("foo", null);
927}
928
929fn testDirnamePosix(input: []const u8, expected_output: ?[]const u8) !void {
930    if (dirnamePosix(input)) |output| {
931        try testing.expect(mem.eql(u8, output, expected_output.?));
932    } else {
933        try testing.expect(expected_output == null);
934    }
935}
936
937fn testDirnameWindows(input: []const u8, expected_output: ?[]const u8) !void {
938    if (dirnameWindows(input)) |output| {
939        try testing.expect(mem.eql(u8, output, expected_output.?));
940    } else {
941        try testing.expect(expected_output == null);
942    }
943}
944
945pub fn basename(path: []const u8) []const u8 {
946    if (native_os == .windows) {
947        return basenameWindows(path);
948    } else {
949        return basenamePosix(path);
950    }
951}
952
953pub fn basenamePosix(path: []const u8) []const u8 {
954    if (path.len == 0)
955        return &[_]u8{};
956
957    var end_index: usize = path.len - 1;
958    while (path[end_index] == '/') {
959        if (end_index == 0)
960            return &[_]u8{};
961        end_index -= 1;
962    }
963    var start_index: usize = end_index;
964    end_index += 1;
965    while (path[start_index] != '/') {
966        if (start_index == 0)
967            return path[0..end_index];
968        start_index -= 1;
969    }
970
971    return path[start_index + 1 .. end_index];
972}
973
974pub fn basenameWindows(path: []const u8) []const u8 {
975    if (path.len == 0)
976        return &[_]u8{};
977
978    var end_index: usize = path.len - 1;
979    while (true) {
980        const byte = path[end_index];
981        if (byte == '/' or byte == '\\') {
982            if (end_index == 0)
983                return &[_]u8{};
984            end_index -= 1;
985            continue;
986        }
987        if (byte == ':' and end_index == 1) {
988            return &[_]u8{};
989        }
990        break;
991    }
992
993    var start_index: usize = end_index;
994    end_index += 1;
995    while (path[start_index] != '/' and path[start_index] != '\\' and
996        !(path[start_index] == ':' and start_index == 1))
997    {
998        if (start_index == 0)
999            return path[0..end_index];
1000        start_index -= 1;
1001    }
1002
1003    return path[start_index + 1 .. end_index];
1004}
1005
1006test "basename" {
1007    try testBasename("", "");
1008    try testBasename("/", "");
1009    try testBasename("/dir/basename.ext", "basename.ext");
1010    try testBasename("/basename.ext", "basename.ext");
1011    try testBasename("basename.ext", "basename.ext");
1012    try testBasename("basename.ext/", "basename.ext");
1013    try testBasename("basename.ext//", "basename.ext");
1014    try testBasename("/aaa/bbb", "bbb");
1015    try testBasename("/aaa/", "aaa");
1016    try testBasename("/aaa/b", "b");
1017    try testBasename("/a/b", "b");
1018    try testBasename("//a", "a");
1019
1020    try testBasenamePosix("\\dir\\basename.ext", "\\dir\\basename.ext");
1021    try testBasenamePosix("\\basename.ext", "\\basename.ext");
1022    try testBasenamePosix("basename.ext", "basename.ext");
1023    try testBasenamePosix("basename.ext\\", "basename.ext\\");
1024    try testBasenamePosix("basename.ext\\\\", "basename.ext\\\\");
1025    try testBasenamePosix("foo", "foo");
1026
1027    try testBasenameWindows("\\dir\\basename.ext", "basename.ext");
1028    try testBasenameWindows("\\basename.ext", "basename.ext");
1029    try testBasenameWindows("basename.ext", "basename.ext");
1030    try testBasenameWindows("basename.ext\\", "basename.ext");
1031    try testBasenameWindows("basename.ext\\\\", "basename.ext");
1032    try testBasenameWindows("foo", "foo");
1033    try testBasenameWindows("C:", "");
1034    try testBasenameWindows("C:.", ".");
1035    try testBasenameWindows("C:\\", "");
1036    try testBasenameWindows("C:\\dir\\base.ext", "base.ext");
1037    try testBasenameWindows("C:\\basename.ext", "basename.ext");
1038    try testBasenameWindows("C:basename.ext", "basename.ext");
1039    try testBasenameWindows("C:basename.ext\\", "basename.ext");
1040    try testBasenameWindows("C:basename.ext\\\\", "basename.ext");
1041    try testBasenameWindows("C:foo", "foo");
1042    try testBasenameWindows("file:stream", "file:stream");
1043}
1044
1045fn testBasename(input: []const u8, expected_output: []const u8) !void {
1046    try testing.expectEqualSlices(u8, expected_output, basename(input));
1047}
1048
1049fn testBasenamePosix(input: []const u8, expected_output: []const u8) !void {
1050    try testing.expectEqualSlices(u8, expected_output, basenamePosix(input));
1051}
1052
1053fn testBasenameWindows(input: []const u8, expected_output: []const u8) !void {
1054    try testing.expectEqualSlices(u8, expected_output, basenameWindows(input));
1055}
1056
1057/// Returns the relative path from `from` to `to`. If `from` and `to` each
1058/// resolve to the same path (after calling `resolve` on each), a zero-length
1059/// string is returned.
1060/// On Windows this canonicalizes the drive to a capital letter and paths to `\\`.
1061pub fn relative(allocator: Allocator, from: []const u8, to: []const u8) ![]u8 {
1062    if (native_os == .windows) {
1063        return relativeWindows(allocator, from, to);
1064    } else {
1065        return relativePosix(allocator, from, to);
1066    }
1067}
1068
1069pub fn relativeWindows(allocator: Allocator, from: []const u8, to: []const u8) ![]u8 {
1070    const resolved_from = try resolveWindows(allocator, &[_][]const u8{from});
1071    defer allocator.free(resolved_from);
1072
1073    var clean_up_resolved_to = true;
1074    const resolved_to = try resolveWindows(allocator, &[_][]const u8{to});
1075    defer if (clean_up_resolved_to) allocator.free(resolved_to);
1076
1077    const parsed_from = windowsParsePath(resolved_from);
1078    const parsed_to = windowsParsePath(resolved_to);
1079    const result_is_to = x: {
1080        if (parsed_from.kind != parsed_to.kind) {
1081            break :x true;
1082        } else switch (parsed_from.kind) {
1083            WindowsPath.Kind.NetworkShare => {
1084                break :x !networkShareServersEql(parsed_to.disk_designator, parsed_from.disk_designator);
1085            },
1086            WindowsPath.Kind.Drive => {
1087                break :x asciiUpper(parsed_from.disk_designator[0]) != asciiUpper(parsed_to.disk_designator[0]);
1088            },
1089            else => unreachable,
1090        }
1091    };
1092
1093    if (result_is_to) {
1094        clean_up_resolved_to = false;
1095        return resolved_to;
1096    }
1097
1098    var from_it = mem.tokenize(u8, resolved_from, "/\\");
1099    var to_it = mem.tokenize(u8, resolved_to, "/\\");
1100    while (true) {
1101        const from_component = from_it.next() orelse return allocator.dupe(u8, to_it.rest());
1102        const to_rest = to_it.rest();
1103        if (to_it.next()) |to_component| {
1104            // TODO ASCII is wrong, we actually need full unicode support to compare paths.
1105            if (asciiEqlIgnoreCase(from_component, to_component))
1106                continue;
1107        }
1108        var up_count: usize = 1;
1109        while (from_it.next()) |_| {
1110            up_count += 1;
1111        }
1112        const up_index_end = up_count * "..\\".len;
1113        const result = try allocator.alloc(u8, up_index_end + to_rest.len);
1114        errdefer allocator.free(result);
1115
1116        var result_index: usize = 0;
1117        while (result_index < up_index_end) {
1118            result[result_index] = '.';
1119            result_index += 1;
1120            result[result_index] = '.';
1121            result_index += 1;
1122            result[result_index] = '\\';
1123            result_index += 1;
1124        }
1125        // shave off the trailing slash
1126        result_index -= 1;
1127
1128        var rest_it = mem.tokenize(u8, to_rest, "/\\");
1129        while (rest_it.next()) |to_component| {
1130            result[result_index] = '\\';
1131            result_index += 1;
1132            mem.copy(u8, result[result_index..], to_component);
1133            result_index += to_component.len;
1134        }
1135
1136        return result[0..result_index];
1137    }
1138
1139    return [_]u8{};
1140}
1141
1142pub fn relativePosix(allocator: Allocator, from: []const u8, to: []const u8) ![]u8 {
1143    const resolved_from = try resolvePosix(allocator, &[_][]const u8{from});
1144    defer allocator.free(resolved_from);
1145
1146    const resolved_to = try resolvePosix(allocator, &[_][]const u8{to});
1147    defer allocator.free(resolved_to);
1148
1149    var from_it = mem.tokenize(u8, resolved_from, "/");
1150    var to_it = mem.tokenize(u8, resolved_to, "/");
1151    while (true) {
1152        const from_component = from_it.next() orelse return allocator.dupe(u8, to_it.rest());
1153        const to_rest = to_it.rest();
1154        if (to_it.next()) |to_component| {
1155            if (mem.eql(u8, from_component, to_component))
1156                continue;
1157        }
1158        var up_count: usize = 1;
1159        while (from_it.next()) |_| {
1160            up_count += 1;
1161        }
1162        const up_index_end = up_count * "../".len;
1163        const result = try allocator.alloc(u8, up_index_end + to_rest.len);
1164        errdefer allocator.free(result);
1165
1166        var result_index: usize = 0;
1167        while (result_index < up_index_end) {
1168            result[result_index] = '.';
1169            result_index += 1;
1170            result[result_index] = '.';
1171            result_index += 1;
1172            result[result_index] = '/';
1173            result_index += 1;
1174        }
1175        if (to_rest.len == 0) {
1176            // shave off the trailing slash
1177            return allocator.shrink(result, result_index - 1);
1178        }
1179
1180        mem.copy(u8, result[result_index..], to_rest);
1181        return result;
1182    }
1183
1184    return [_]u8{};
1185}
1186
1187test "relative" {
1188    if (builtin.target.cpu.arch == .aarch64) {
1189        // TODO https://github.com/ziglang/zig/issues/3288
1190        return error.SkipZigTest;
1191    }
1192    if (native_os == .wasi) return error.SkipZigTest;
1193
1194    try testRelativeWindows("c:/blah\\blah", "d:/games", "D:\\games");
1195    try testRelativeWindows("c:/aaaa/bbbb", "c:/aaaa", "..");
1196    try testRelativeWindows("c:/aaaa/bbbb", "c:/cccc", "..\\..\\cccc");
1197    try testRelativeWindows("c:/aaaa/bbbb", "c:/aaaa/bbbb", "");
1198    try testRelativeWindows("c:/aaaa/bbbb", "c:/aaaa/cccc", "..\\cccc");
1199    try testRelativeWindows("c:/aaaa/", "c:/aaaa/cccc", "cccc");
1200    try testRelativeWindows("c:/", "c:\\aaaa\\bbbb", "aaaa\\bbbb");
1201    try testRelativeWindows("c:/aaaa/bbbb", "d:\\", "D:\\");
1202    try testRelativeWindows("c:/AaAa/bbbb", "c:/aaaa/bbbb", "");
1203    try testRelativeWindows("c:/aaaaa/", "c:/aaaa/cccc", "..\\aaaa\\cccc");
1204    try testRelativeWindows("C:\\foo\\bar\\baz\\quux", "C:\\", "..\\..\\..\\..");
1205    try testRelativeWindows("C:\\foo\\test", "C:\\foo\\test\\bar\\package.json", "bar\\package.json");
1206    try testRelativeWindows("C:\\foo\\bar\\baz-quux", "C:\\foo\\bar\\baz", "..\\baz");
1207    try testRelativeWindows("C:\\foo\\bar\\baz", "C:\\foo\\bar\\baz-quux", "..\\baz-quux");
1208    try testRelativeWindows("\\\\foo\\bar", "\\\\foo\\bar\\baz", "baz");
1209    try testRelativeWindows("\\\\foo\\bar\\baz", "\\\\foo\\bar", "..");
1210    try testRelativeWindows("\\\\foo\\bar\\baz-quux", "\\\\foo\\bar\\baz", "..\\baz");
1211    try testRelativeWindows("\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz-quux", "..\\baz-quux");
1212    try testRelativeWindows("C:\\baz-quux", "C:\\baz", "..\\baz");
1213    try testRelativeWindows("C:\\baz", "C:\\baz-quux", "..\\baz-quux");
1214    try testRelativeWindows("\\\\foo\\baz-quux", "\\\\foo\\baz", "..\\baz");
1215    try testRelativeWindows("\\\\foo\\baz", "\\\\foo\\baz-quux", "..\\baz-quux");
1216    try testRelativeWindows("C:\\baz", "\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz");
1217    try testRelativeWindows("\\\\foo\\bar\\baz", "C:\\baz", "C:\\baz");
1218
1219    try testRelativePosix("/var/lib", "/var", "..");
1220    try testRelativePosix("/var/lib", "/bin", "../../bin");
1221    try testRelativePosix("/var/lib", "/var/lib", "");
1222    try testRelativePosix("/var/lib", "/var/apache", "../apache");
1223    try testRelativePosix("/var/", "/var/lib", "lib");
1224    try testRelativePosix("/", "/var/lib", "var/lib");
1225    try testRelativePosix("/foo/test", "/foo/test/bar/package.json", "bar/package.json");
1226    try testRelativePosix("/Users/a/web/b/test/mails", "/Users/a/web/b", "../..");
1227    try testRelativePosix("/foo/bar/baz-quux", "/foo/bar/baz", "../baz");
1228    try testRelativePosix("/foo/bar/baz", "/foo/bar/baz-quux", "../baz-quux");
1229    try testRelativePosix("/baz-quux", "/baz", "../baz");
1230    try testRelativePosix("/baz", "/baz-quux", "../baz-quux");
1231}
1232
1233fn testRelativePosix(from: []const u8, to: []const u8, expected_output: []const u8) !void {
1234    const result = try relativePosix(testing.allocator, from, to);
1235    defer testing.allocator.free(result);
1236    try testing.expectEqualSlices(u8, expected_output, result);
1237}
1238
1239fn testRelativeWindows(from: []const u8, to: []const u8, expected_output: []const u8) !void {
1240    const result = try relativeWindows(testing.allocator, from, to);
1241    defer testing.allocator.free(result);
1242    try testing.expectEqualSlices(u8, expected_output, result);
1243}
1244
1245/// Returns the extension of the file name (if any).
1246/// This function will search for the file extension (separated by a `.`) and will return the text after the `.`.
1247/// Files that end with `.` are considered to have no extension, files that start with `.`
1248/// Examples:
1249/// - `"main.zig"`     ⇒ `".zig"`
1250/// - `"src/main.zig"` ⇒ `".zig"`
1251/// - `".gitignore"`   ⇒ `""`
1252/// - `"keep."`        ⇒ `"."`
1253/// - `"src.keep.me"`  ⇒ `".me"`
1254/// - `"/src/keep.me"`  ⇒ `".me"`
1255/// - `"/src/keep.me/"`  ⇒ `".me"`
1256/// The returned slice is guaranteed to have its pointer within the start and end
1257/// pointer address range of `path`, even if it is length zero.
1258pub fn extension(path: []const u8) []const u8 {
1259    const filename = basename(path);
1260    const index = mem.lastIndexOfScalar(u8, filename, '.') orelse return path[path.len..];
1261    if (index == 0) return path[path.len..];
1262    return filename[index..];
1263}
1264
1265fn testExtension(path: []const u8, expected: []const u8) !void {
1266    try std.testing.expectEqualStrings(expected, extension(path));
1267}
1268
1269test "extension" {
1270    try testExtension("", "");
1271    try testExtension(".", "");
1272    try testExtension("a.", ".");
1273    try testExtension("abc.", ".");
1274    try testExtension(".a", "");
1275    try testExtension(".file", "");
1276    try testExtension(".gitignore", "");
1277    try testExtension("file.ext", ".ext");
1278    try testExtension("file.ext.", ".");
1279    try testExtension("very-long-file.bruh", ".bruh");
1280    try testExtension("a.b.c", ".c");
1281    try testExtension("a.b.c/", ".c");
1282
1283    try testExtension("/", "");
1284    try testExtension("/.", "");
1285    try testExtension("/a.", ".");
1286    try testExtension("/abc.", ".");
1287    try testExtension("/.a", "");
1288    try testExtension("/.file", "");
1289    try testExtension("/.gitignore", "");
1290    try testExtension("/file.ext", ".ext");
1291    try testExtension("/file.ext.", ".");
1292    try testExtension("/very-long-file.bruh", ".bruh");
1293    try testExtension("/a.b.c", ".c");
1294    try testExtension("/a.b.c/", ".c");
1295
1296    try testExtension("/foo/bar/bam/", "");
1297    try testExtension("/foo/bar/bam/.", "");
1298    try testExtension("/foo/bar/bam/a.", ".");
1299    try testExtension("/foo/bar/bam/abc.", ".");
1300    try testExtension("/foo/bar/bam/.a", "");
1301    try testExtension("/foo/bar/bam/.file", "");
1302    try testExtension("/foo/bar/bam/.gitignore", "");
1303    try testExtension("/foo/bar/bam/file.ext", ".ext");
1304    try testExtension("/foo/bar/bam/file.ext.", ".");
1305    try testExtension("/foo/bar/bam/very-long-file.bruh", ".bruh");
1306    try testExtension("/foo/bar/bam/a.b.c", ".c");
1307    try testExtension("/foo/bar/bam/a.b.c/", ".c");
1308}
1309