aboutsummaryrefslogtreecommitdiff
path: root/lib/std/fs/path.zig
diff options
context:
space:
mode:
authorAndrew Kelley <andrew@ziglang.org>2019-09-26 01:54:45 -0400
committerGitHub <noreply@github.com>2019-09-26 01:54:45 -0400
commit68bb3945708c43109c48bda3664176307d45b62c (patch)
treeafb9731e10cef9d192560b52cd9ae2cf179775c4 /lib/std/fs/path.zig
parent6128bc728d1e1024a178c16c2149f5b1a167a013 (diff)
parent4637e8f9699af9c3c6cf4df50ef5bb67c7a318a4 (diff)
downloadzig-68bb3945708c43109c48bda3664176307d45b62c.tar.gz
zig-68bb3945708c43109c48bda3664176307d45b62c.zip
Merge pull request #3315 from ziglang/mv-std-lib
Move std/ to lib/std/
Diffstat (limited to 'lib/std/fs/path.zig')
-rw-r--r--lib/std/fs/path.zig1144
1 files changed, 1144 insertions, 0 deletions
diff --git a/lib/std/fs/path.zig b/lib/std/fs/path.zig
new file mode 100644
index 0000000000..2bb23f04ce
--- /dev/null
+++ b/lib/std/fs/path.zig
@@ -0,0 +1,1144 @@
+const builtin = @import("builtin");
+const std = @import("../std.zig");
+const debug = std.debug;
+const assert = debug.assert;
+const testing = std.testing;
+const mem = std.mem;
+const fmt = std.fmt;
+const Allocator = mem.Allocator;
+const math = std.math;
+const windows = std.os.windows;
+const fs = std.fs;
+const process = std.process;
+
+pub const sep_windows = '\\';
+pub const sep_posix = '/';
+pub const sep = if (windows.is_the_target) sep_windows else sep_posix;
+
+pub const sep_str = [1]u8{sep};
+
+pub const delimiter_windows = ';';
+pub const delimiter_posix = ':';
+pub const delimiter = if (windows.is_the_target) delimiter_windows else delimiter_posix;
+
+pub fn isSep(byte: u8) bool {
+ if (windows.is_the_target) {
+ return byte == '/' or byte == '\\';
+ } else {
+ return byte == '/';
+ }
+}
+
+/// 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 joinSep(allocator: *Allocator, separator: u8, paths: []const []const u8) ![]u8 {
+ if (paths.len == 0) return (([*]u8)(undefined))[0..0];
+
+ const total_len = blk: {
+ var sum: usize = paths[0].len;
+ var i: usize = 1;
+ while (i < paths.len) : (i += 1) {
+ const prev_path = paths[i - 1];
+ const this_path = paths[i];
+ const prev_sep = (prev_path.len != 0 and prev_path[prev_path.len - 1] == separator);
+ const this_sep = (this_path.len != 0 and this_path[0] == separator);
+ sum += @boolToInt(!prev_sep and !this_sep);
+ sum += if (prev_sep and this_sep) this_path.len - 1 else this_path.len;
+ }
+ break :blk sum;
+ };
+
+ const buf = try allocator.alloc(u8, total_len);
+ errdefer allocator.free(buf);
+
+ mem.copy(u8, buf, paths[0]);
+ var buf_index: usize = paths[0].len;
+ var i: usize = 1;
+ while (i < paths.len) : (i += 1) {
+ const prev_path = paths[i - 1];
+ const this_path = paths[i];
+ const prev_sep = (prev_path.len != 0 and prev_path[prev_path.len - 1] == separator);
+ const this_sep = (this_path.len != 0 and this_path[0] == separator);
+ 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;
+ mem.copy(u8, buf[buf_index..], adjusted_path);
+ buf_index += adjusted_path.len;
+ }
+
+ // No need for shrink since buf is exactly the correct size.
+ return buf;
+}
+
+pub const join = if (windows.is_the_target) joinWindows else joinPosix;
+
+/// Naively combines a series of paths with the native path seperator.
+/// Allocates memory for the result, which must be freed by the caller.
+pub fn joinWindows(allocator: *Allocator, paths: []const []const u8) ![]u8 {
+ return joinSep(allocator, sep_windows, paths);
+}
+
+/// Naively combines a series of paths with the native path seperator.
+/// Allocates memory for the result, which must be freed by the caller.
+pub fn joinPosix(allocator: *Allocator, paths: []const []const u8) ![]u8 {
+ return joinSep(allocator, sep_posix, paths);
+}
+
+fn testJoinWindows(paths: []const []const u8, expected: []const u8) void {
+ var buf: [1024]u8 = undefined;
+ const a = &std.heap.FixedBufferAllocator.init(&buf).allocator;
+ const actual = joinWindows(a, paths) catch @panic("fail");
+ testing.expectEqualSlices(u8, expected, actual);
+}
+
+fn testJoinPosix(paths: []const []const u8, expected: []const u8) void {
+ var buf: [1024]u8 = undefined;
+ const a = &std.heap.FixedBufferAllocator.init(&buf).allocator;
+ const actual = joinPosix(a, paths) catch @panic("fail");
+ testing.expectEqualSlices(u8, expected, actual);
+}
+
+test "join" {
+ testJoinWindows([_][]const u8{ "c:\\a\\b", "c" }, "c:\\a\\b\\c");
+ testJoinWindows([_][]const u8{ "c:\\a\\b", "c" }, "c:\\a\\b\\c");
+ testJoinWindows([_][]const u8{ "c:\\a\\b\\", "c" }, "c:\\a\\b\\c");
+
+ testJoinWindows([_][]const u8{ "c:\\", "a", "b\\", "c" }, "c:\\a\\b\\c");
+ testJoinWindows([_][]const u8{ "c:\\a\\", "b\\", "c" }, "c:\\a\\b\\c");
+
+ testJoinWindows(
+ [_][]const u8{ "c:\\home\\andy\\dev\\zig\\build\\lib\\zig\\std", "io.zig" },
+ "c:\\home\\andy\\dev\\zig\\build\\lib\\zig\\std\\io.zig",
+ );
+
+ testJoinPosix([_][]const u8{ "/a/b", "c" }, "/a/b/c");
+ testJoinPosix([_][]const u8{ "/a/b/", "c" }, "/a/b/c");
+
+ testJoinPosix([_][]const u8{ "/", "a", "b/", "c" }, "/a/b/c");
+ testJoinPosix([_][]const u8{ "/a/", "b/", "c" }, "/a/b/c");
+
+ testJoinPosix(
+ [_][]const u8{ "/home/andy/dev/zig/build/lib/zig/std", "io.zig" },
+ "/home/andy/dev/zig/build/lib/zig/std/io.zig",
+ );
+
+ testJoinPosix([_][]const u8{ "a", "/c" }, "a/c");
+ testJoinPosix([_][]const u8{ "a/", "/c" }, "a/c");
+}
+
+pub fn isAbsolute(path: []const u8) bool {
+ if (windows.is_the_target) {
+ return isAbsoluteWindows(path);
+ } else {
+ return isAbsolutePosix(path);
+ }
+}
+
+pub fn isAbsoluteWindows(path: []const u8) bool {
+ if (path[0] == '/')
+ return true;
+
+ if (path[0] == '\\') {
+ return true;
+ }
+ if (path.len < 3) {
+ return false;
+ }
+ if (path[1] == ':') {
+ if (path[2] == '/')
+ return true;
+ if (path[2] == '\\')
+ return true;
+ }
+ return false;
+}
+
+pub fn isAbsolutePosix(path: []const u8) bool {
+ return path[0] == sep_posix;
+}
+
+test "isAbsoluteWindows" {
+ testIsAbsoluteWindows("/", true);
+ testIsAbsoluteWindows("//", true);
+ testIsAbsoluteWindows("//server", true);
+ testIsAbsoluteWindows("//server/file", true);
+ testIsAbsoluteWindows("\\\\server\\file", true);
+ testIsAbsoluteWindows("\\\\server", true);
+ testIsAbsoluteWindows("\\\\", true);
+ testIsAbsoluteWindows("c", false);
+ testIsAbsoluteWindows("c:", false);
+ testIsAbsoluteWindows("c:\\", true);
+ testIsAbsoluteWindows("c:/", true);
+ testIsAbsoluteWindows("c://", true);
+ testIsAbsoluteWindows("C:/Users/", true);
+ testIsAbsoluteWindows("C:\\Users\\", true);
+ testIsAbsoluteWindows("C:cwd/another", false);
+ testIsAbsoluteWindows("C:cwd\\another", false);
+ testIsAbsoluteWindows("directory/directory", false);
+ testIsAbsoluteWindows("directory\\directory", false);
+ testIsAbsoluteWindows("/usr/local", true);
+}
+
+test "isAbsolutePosix" {
+ testIsAbsolutePosix("/home/foo", true);
+ testIsAbsolutePosix("/home/foo/..", true);
+ testIsAbsolutePosix("bar/", false);
+ testIsAbsolutePosix("./baz", false);
+}
+
+fn testIsAbsoluteWindows(path: []const u8, expected_result: bool) void {
+ testing.expectEqual(expected_result, isAbsoluteWindows(path));
+}
+
+fn testIsAbsolutePosix(path: []const u8, expected_result: bool) void {
+ testing.expectEqual(expected_result, isAbsolutePosix(path));
+}
+
+pub const WindowsPath = struct {
+ is_abs: bool,
+ kind: Kind,
+ disk_designator: []const u8,
+
+ pub const Kind = enum {
+ None,
+ Drive,
+ NetworkShare,
+ };
+};
+
+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 < "//a/b".len) {
+ return relative_path;
+ }
+
+ // TODO when I combined these together with `inline for` the compiler crashed
+ {
+ const this_sep = '/';
+ const two_sep = [_]u8{ this_sep, this_sep };
+ if (mem.startsWith(u8, path, two_sep)) {
+ if (path[2] == this_sep) {
+ return relative_path;
+ }
+
+ var it = mem.tokenize(path, [_]u8{this_sep});
+ _ = (it.next() orelse return relative_path);
+ _ = (it.next() orelse return relative_path);
+ return WindowsPath{
+ .is_abs = isAbsoluteWindows(path),
+ .kind = WindowsPath.Kind.NetworkShare,
+ .disk_designator = path[0..it.index],
+ };
+ }
+ }
+ {
+ const this_sep = '\\';
+ const two_sep = [_]u8{ this_sep, this_sep };
+ if (mem.startsWith(u8, path, two_sep)) {
+ if (path[2] == this_sep) {
+ return relative_path;
+ }
+
+ var it = mem.tokenize(path, [_]u8{this_sep});
+ _ = (it.next() orelse return relative_path);
+ _ = (it.next() orelse return relative_path);
+ return WindowsPath{
+ .is_abs = isAbsoluteWindows(path),
+ .kind = WindowsPath.Kind.NetworkShare,
+ .disk_designator = path[0..it.index],
+ };
+ }
+ }
+ return relative_path;
+}
+
+test "windowsParsePath" {
+ {
+ const parsed = windowsParsePath("//a/b");
+ testing.expect(parsed.is_abs);
+ testing.expect(parsed.kind == WindowsPath.Kind.NetworkShare);
+ testing.expect(mem.eql(u8, parsed.disk_designator, "//a/b"));
+ }
+ {
+ const parsed = windowsParsePath("\\\\a\\b");
+ testing.expect(parsed.is_abs);
+ testing.expect(parsed.kind == WindowsPath.Kind.NetworkShare);
+ testing.expect(mem.eql(u8, parsed.disk_designator, "\\\\a\\b"));
+ }
+ {
+ const parsed = windowsParsePath("\\\\a\\");
+ testing.expect(!parsed.is_abs);
+ testing.expect(parsed.kind == WindowsPath.Kind.None);
+ testing.expect(mem.eql(u8, parsed.disk_designator, ""));
+ }
+ {
+ const parsed = windowsParsePath("/usr/local");
+ testing.expect(parsed.is_abs);
+ testing.expect(parsed.kind == WindowsPath.Kind.None);
+ testing.expect(mem.eql(u8, parsed.disk_designator, ""));
+ }
+ {
+ const parsed = windowsParsePath("c:../");
+ testing.expect(!parsed.is_abs);
+ testing.expect(parsed.kind == WindowsPath.Kind.Drive);
+ testing.expect(mem.eql(u8, parsed.disk_designator, "c:"));
+ }
+}
+
+pub fn diskDesignator(path: []const u8) []const u8 {
+ if (windows.is_the_target) {
+ return diskDesignatorWindows(path);
+ } else {
+ return "";
+ }
+}
+
+pub fn diskDesignatorWindows(path: []const u8) []const u8 {
+ return windowsParsePath(path).disk_designator;
+}
+
+fn networkShareServersEql(ns1: []const u8, ns2: []const u8) bool {
+ const sep1 = ns1[0];
+ const sep2 = ns2[0];
+
+ var it1 = mem.tokenize(ns1, [_]u8{sep1});
+ var it2 = mem.tokenize(ns2, [_]u8{sep2});
+
+ // TODO ASCII is wrong, we actually need full unicode support to compare paths.
+ return asciiEqlIgnoreCase(it1.next().?, it2.next().?);
+}
+
+fn compareDiskDesignators(kind: WindowsPath.Kind, p1: []const u8, p2: []const u8) bool {
+ switch (kind) {
+ WindowsPath.Kind.None => {
+ assert(p1.len == 0);
+ assert(p2.len == 0);
+ return true;
+ },
+ WindowsPath.Kind.Drive => {
+ return asciiUpper(p1[0]) == asciiUpper(p2[0]);
+ },
+ WindowsPath.Kind.NetworkShare => {
+ const sep1 = p1[0];
+ const sep2 = p2[0];
+
+ var it1 = mem.tokenize(p1, [_]u8{sep1});
+ var it2 = mem.tokenize(p2, [_]u8{sep2});
+
+ // TODO ASCII is wrong, we actually need full unicode support to compare paths.
+ return asciiEqlIgnoreCase(it1.next().?, it2.next().?) and asciiEqlIgnoreCase(it1.next().?, it2.next().?);
+ },
+ }
+}
+
+fn asciiUpper(byte: u8) u8 {
+ return switch (byte) {
+ 'a'...'z' => 'A' + (byte - 'a'),
+ else => byte,
+ };
+}
+
+fn asciiEqlIgnoreCase(s1: []const u8, s2: []const u8) bool {
+ if (s1.len != s2.len)
+ return false;
+ var i: usize = 0;
+ while (i < s1.len) : (i += 1) {
+ if (asciiUpper(s1[i]) != asciiUpper(s2[i]))
+ return false;
+ }
+ return true;
+}
+
+/// On Windows, this calls `resolveWindows` and on POSIX it calls `resolvePosix`.
+pub fn resolve(allocator: *Allocator, paths: []const []const u8) ![]u8 {
+ if (windows.is_the_target) {
+ 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 "..".
+/// The result does not have a trailing path separator.
+/// If all paths are relative it uses the current working directory as a starting point.
+/// Each drive has its own current working directory.
+/// Path separators are canonicalized to '\\' and drives are canonicalized to capital letters.
+/// Note: all usage of this function should be audited due to the existence of symlinks.
+/// Without performing actual syscalls, resolving `..` could be incorrect.
+pub fn resolveWindows(allocator: *Allocator, paths: []const []const u8) ![]u8 {
+ if (paths.len == 0) {
+ assert(windows.is_the_target); // resolveWindows called on non windows can't use getCwd
+ return process.getCwdAlloc(allocator);
+ }
+
+ // determine which disk designator we will result with, if any
+ var result_drive_buf = "_:";
+ var result_disk_designator: []const u8 = "";
+ var have_drive_kind = WindowsPath.Kind.None;
+ var have_abs_path = false;
+ var first_index: usize = 0;
+ var max_size: usize = 0;
+ for (paths) |p, i| {
+ const parsed = windowsParsePath(p);
+ if (parsed.is_abs) {
+ have_abs_path = true;
+ first_index = i;
+ max_size = result_disk_designator.len;
+ }
+ switch (parsed.kind) {
+ WindowsPath.Kind.Drive => {
+ result_drive_buf[0] = asciiUpper(parsed.disk_designator[0]);
+ result_disk_designator = result_drive_buf[0..];
+ have_drive_kind = WindowsPath.Kind.Drive;
+ },
+ WindowsPath.Kind.NetworkShare => {
+ result_disk_designator = parsed.disk_designator;
+ have_drive_kind = WindowsPath.Kind.NetworkShare;
+ },
+ WindowsPath.Kind.None => {},
+ }
+ max_size += p.len + 1;
+ }
+
+ // if we will result with a disk designator, loop again to determine
+ // which is the last time the disk designator is absolutely specified, if any
+ // and count up the max bytes for paths related to this disk designator
+ if (have_drive_kind != WindowsPath.Kind.None) {
+ have_abs_path = false;
+ first_index = 0;
+ max_size = result_disk_designator.len;
+ var correct_disk_designator = false;
+
+ for (paths) |p, i| {
+ const parsed = windowsParsePath(p);
+ if (parsed.kind != WindowsPath.Kind.None) {
+ if (parsed.kind == have_drive_kind) {
+ correct_disk_designator = compareDiskDesignators(have_drive_kind, result_disk_designator, parsed.disk_designator);
+ } else {
+ continue;
+ }
+ }
+ if (!correct_disk_designator) {
+ continue;
+ }
+ if (parsed.is_abs) {
+ first_index = i;
+ max_size = result_disk_designator.len;
+ have_abs_path = true;
+ }
+ max_size += p.len + 1;
+ }
+ }
+
+ // Allocate result and fill in the disk designator, calling getCwd if we have to.
+ var result: []u8 = undefined;
+ var result_index: usize = 0;
+
+ if (have_abs_path) {
+ switch (have_drive_kind) {
+ WindowsPath.Kind.Drive => {
+ result = try allocator.alloc(u8, max_size);
+
+ mem.copy(u8, result, result_disk_designator);
+ result_index += result_disk_designator.len;
+ },
+ WindowsPath.Kind.NetworkShare => {
+ result = try allocator.alloc(u8, max_size);
+ var it = mem.tokenize(paths[first_index], "/\\");
+ const server_name = it.next().?;
+ const other_name = it.next().?;
+
+ result[result_index] = '\\';
+ result_index += 1;
+ result[result_index] = '\\';
+ result_index += 1;
+ mem.copy(u8, result[result_index..], server_name);
+ result_index += server_name.len;
+ result[result_index] = '\\';
+ result_index += 1;
+ mem.copy(u8, result[result_index..], other_name);
+ result_index += other_name.len;
+
+ result_disk_designator = result[0..result_index];
+ },
+ WindowsPath.Kind.None => {
+ assert(windows.is_the_target); // resolveWindows called on non windows can't use getCwd
+ const cwd = try process.getCwdAlloc(allocator);
+ defer allocator.free(cwd);
+ const parsed_cwd = windowsParsePath(cwd);
+ result = try allocator.alloc(u8, max_size + parsed_cwd.disk_designator.len + 1);
+ mem.copy(u8, result, parsed_cwd.disk_designator);
+ result_index += parsed_cwd.disk_designator.len;
+ result_disk_designator = result[0..parsed_cwd.disk_designator.len];
+ if (parsed_cwd.kind == WindowsPath.Kind.Drive) {
+ result[0] = asciiUpper(result[0]);
+ }
+ have_drive_kind = parsed_cwd.kind;
+ },
+ }
+ } else {
+ assert(windows.is_the_target); // resolveWindows called on non windows can't use getCwd
+ // TODO call get cwd for the result_disk_designator instead of the global one
+ const cwd = try process.getCwdAlloc(allocator);
+ defer allocator.free(cwd);
+
+ result = try allocator.alloc(u8, max_size + cwd.len + 1);
+
+ mem.copy(u8, result, cwd);
+ result_index += cwd.len;
+ const parsed_cwd = windowsParsePath(result[0..result_index]);
+ result_disk_designator = parsed_cwd.disk_designator;
+ if (parsed_cwd.kind == WindowsPath.Kind.Drive) {
+ result[0] = asciiUpper(result[0]);
+ }
+ have_drive_kind = parsed_cwd.kind;
+ }
+ errdefer allocator.free(result);
+
+ // Now we know the disk designator to use, if any, and what kind it is. And our result
+ // is big enough to append all the paths to.
+ var correct_disk_designator = true;
+ for (paths[first_index..]) |p, i| {
+ const parsed = windowsParsePath(p);
+
+ if (parsed.kind != WindowsPath.Kind.None) {
+ if (parsed.kind == have_drive_kind) {
+ correct_disk_designator = compareDiskDesignators(have_drive_kind, result_disk_designator, parsed.disk_designator);
+ } else {
+ continue;
+ }
+ }
+ if (!correct_disk_designator) {
+ continue;
+ }
+ var it = mem.tokenize(p[parsed.disk_designator.len..], "/\\");
+ while (it.next()) |component| {
+ if (mem.eql(u8, component, ".")) {
+ continue;
+ } else if (mem.eql(u8, component, "..")) {
+ while (true) {
+ if (result_index == 0 or result_index == result_disk_designator.len)
+ break;
+ result_index -= 1;
+ if (result[result_index] == '\\' or result[result_index] == '/')
+ break;
+ }
+ } else {
+ result[result_index] = sep_windows;
+ result_index += 1;
+ mem.copy(u8, result[result_index..], component);
+ result_index += component.len;
+ }
+ }
+ }
+
+ if (result_index == result_disk_designator.len) {
+ result[result_index] = '\\';
+ result_index += 1;
+ }
+
+ return allocator.shrink(result, result_index);
+}
+
+/// This function is like a series of `cd` statements executed one after another.
+/// It resolves "." and "..".
+/// The result does not have a trailing path separator.
+/// If all paths are relative it uses the current working directory as a starting point.
+/// Note: all usage of this function should be audited due to the existence of symlinks.
+/// Without performing actual syscalls, resolving `..` could be incorrect.
+pub fn resolvePosix(allocator: *Allocator, paths: []const []const u8) ![]u8 {
+ if (paths.len == 0) {
+ assert(!windows.is_the_target); // resolvePosix called on windows can't use getCwd
+ return process.getCwdAlloc(allocator);
+ }
+
+ var first_index: usize = 0;
+ var have_abs = false;
+ var max_size: usize = 0;
+ for (paths) |p, i| {
+ if (isAbsolutePosix(p)) {
+ first_index = i;
+ have_abs = true;
+ max_size = 0;
+ }
+ max_size += p.len + 1;
+ }
+
+ var result: []u8 = undefined;
+ var result_index: usize = 0;
+
+ if (have_abs) {
+ result = try allocator.alloc(u8, max_size);
+ } else {
+ assert(!windows.is_the_target); // resolvePosix called on windows can't use getCwd
+ const cwd = try process.getCwdAlloc(allocator);
+ defer allocator.free(cwd);
+ result = try allocator.alloc(u8, max_size + cwd.len + 1);
+ mem.copy(u8, result, cwd);
+ result_index += cwd.len;
+ }
+ errdefer allocator.free(result);
+
+ for (paths[first_index..]) |p, i| {
+ var it = mem.tokenize(p, "/");
+ while (it.next()) |component| {
+ if (mem.eql(u8, component, ".")) {
+ continue;
+ } else if (mem.eql(u8, component, "..")) {
+ while (true) {
+ if (result_index == 0)
+ break;
+ result_index -= 1;
+ if (result[result_index] == '/')
+ break;
+ }
+ } else {
+ result[result_index] = '/';
+ result_index += 1;
+ mem.copy(u8, result[result_index..], component);
+ result_index += component.len;
+ }
+ }
+ }
+
+ if (result_index == 0) {
+ result[0] = '/';
+ result_index += 1;
+ }
+
+ return allocator.shrink(result, result_index);
+}
+
+test "resolve" {
+ const cwd = try process.getCwdAlloc(debug.global_allocator);
+ if (windows.is_the_target) {
+ if (windowsParsePath(cwd).kind == WindowsPath.Kind.Drive) {
+ cwd[0] = asciiUpper(cwd[0]);
+ }
+ testing.expect(mem.eql(u8, testResolveWindows([_][]const u8{"."}), cwd));
+ } else {
+ testing.expect(mem.eql(u8, testResolvePosix([_][]const u8{ "a/b/c/", "../../.." }), cwd));
+ testing.expect(mem.eql(u8, testResolvePosix([_][]const u8{"."}), cwd));
+ }
+}
+
+test "resolveWindows" {
+ if (@import("builtin").arch == .aarch64) {
+ // TODO https://github.com/ziglang/zig/issues/3288
+ return error.SkipZigTest;
+ }
+ if (windows.is_the_target) {
+ const cwd = try process.getCwdAlloc(debug.global_allocator);
+ const parsed_cwd = windowsParsePath(cwd);
+ {
+ const result = testResolveWindows([_][]const u8{ "/usr/local", "lib\\zig\\std\\array_list.zig" });
+ const expected = try join(debug.global_allocator, [_][]const u8{
+ parsed_cwd.disk_designator,
+ "usr\\local\\lib\\zig\\std\\array_list.zig",
+ });
+ if (parsed_cwd.kind == WindowsPath.Kind.Drive) {
+ expected[0] = asciiUpper(parsed_cwd.disk_designator[0]);
+ }
+ testing.expect(mem.eql(u8, result, expected));
+ }
+ {
+ const result = testResolveWindows([_][]const u8{ "usr/local", "lib\\zig" });
+ const expected = try join(debug.global_allocator, [_][]const u8{
+ cwd,
+ "usr\\local\\lib\\zig",
+ });
+ if (parsed_cwd.kind == WindowsPath.Kind.Drive) {
+ expected[0] = asciiUpper(parsed_cwd.disk_designator[0]);
+ }
+ testing.expect(mem.eql(u8, result, expected));
+ }
+ }
+
+ testing.expect(mem.eql(u8, testResolveWindows([_][]const u8{ "c:\\a\\b\\c", "/hi", "ok" }), "C:\\hi\\ok"));
+ testing.expect(mem.eql(u8, testResolveWindows([_][]const u8{ "c:/blah\\blah", "d:/games", "c:../a" }), "C:\\blah\\a"));
+ testing.expect(mem.eql(u8, testResolveWindows([_][]const u8{ "c:/blah\\blah", "d:/games", "C:../a" }), "C:\\blah\\a"));
+ testing.expect(mem.eql(u8, testResolveWindows([_][]const u8{ "c:/ignore", "d:\\a/b\\c/d", "\\e.exe" }), "D:\\e.exe"));
+ testing.expect(mem.eql(u8, testResolveWindows([_][]const u8{ "c:/ignore", "c:/some/file" }), "C:\\some\\file"));
+ testing.expect(mem.eql(u8, testResolveWindows([_][]const u8{ "d:/ignore", "d:some/dir//" }), "D:\\ignore\\some\\dir"));
+ testing.expect(mem.eql(u8, testResolveWindows([_][]const u8{ "//server/share", "..", "relative\\" }), "\\\\server\\share\\relative"));
+ testing.expect(mem.eql(u8, testResolveWindows([_][]const u8{ "c:/", "//" }), "C:\\"));
+ testing.expect(mem.eql(u8, testResolveWindows([_][]const u8{ "c:/", "//dir" }), "C:\\dir"));
+ testing.expect(mem.eql(u8, testResolveWindows([_][]const u8{ "c:/", "//server/share" }), "\\\\server\\share\\"));
+ testing.expect(mem.eql(u8, testResolveWindows([_][]const u8{ "c:/", "//server//share" }), "\\\\server\\share\\"));
+ testing.expect(mem.eql(u8, testResolveWindows([_][]const u8{ "c:/", "///some//dir" }), "C:\\some\\dir"));
+ testing.expect(mem.eql(u8, testResolveWindows([_][]const u8{ "C:\\foo\\tmp.3\\", "..\\tmp.3\\cycles\\root.js" }), "C:\\foo\\tmp.3\\cycles\\root.js"));
+}
+
+test "resolvePosix" {
+ testing.expect(mem.eql(u8, testResolvePosix([_][]const u8{ "/a/b", "c" }), "/a/b/c"));
+ testing.expect(mem.eql(u8, testResolvePosix([_][]const u8{ "/a/b", "c", "//d", "e///" }), "/d/e"));
+ testing.expect(mem.eql(u8, testResolvePosix([_][]const u8{ "/a/b/c", "..", "../" }), "/a"));
+ testing.expect(mem.eql(u8, testResolvePosix([_][]const u8{ "/", "..", ".." }), "/"));
+ testing.expect(mem.eql(u8, testResolvePosix([_][]const u8{"/a/b/c/"}), "/a/b/c"));
+
+ testing.expect(mem.eql(u8, testResolvePosix([_][]const u8{ "/var/lib", "../", "file/" }), "/var/file"));
+ testing.expect(mem.eql(u8, testResolvePosix([_][]const u8{ "/var/lib", "/../", "file/" }), "/file"));
+ testing.expect(mem.eql(u8, testResolvePosix([_][]const u8{ "/some/dir", ".", "/absolute/" }), "/absolute"));
+ testing.expect(mem.eql(u8, testResolvePosix([_][]const u8{ "/foo/tmp.3/", "../tmp.3/cycles/root.js" }), "/foo/tmp.3/cycles/root.js"));
+}
+
+fn testResolveWindows(paths: []const []const u8) []u8 {
+ return resolveWindows(debug.global_allocator, paths) catch unreachable;
+}
+
+fn testResolvePosix(paths: []const []const u8) []u8 {
+ return resolvePosix(debug.global_allocator, paths) catch unreachable;
+}
+
+/// If the path is a file in the current directory (no directory component)
+/// then returns null
+pub fn dirname(path: []const u8) ?[]const u8 {
+ if (windows.is_the_target) {
+ return dirnameWindows(path);
+ } else {
+ return dirnamePosix(path);
+ }
+}
+
+pub fn dirnameWindows(path: []const u8) ?[]const u8 {
+ if (path.len == 0)
+ return null;
+
+ const root_slice = diskDesignatorWindows(path);
+ if (path.len == root_slice.len)
+ return path;
+
+ const have_root_slash = path.len > root_slice.len and (path[root_slice.len] == '/' or path[root_slice.len] == '\\');
+
+ var end_index: usize = path.len - 1;
+
+ while ((path[end_index] == '/' or path[end_index] == '\\') and end_index > root_slice.len) {
+ if (end_index == 0)
+ return null;
+ end_index -= 1;
+ }
+
+ while (path[end_index] != '/' and path[end_index] != '\\' and end_index > root_slice.len) {
+ if (end_index == 0)
+ return null;
+ end_index -= 1;
+ }
+
+ if (have_root_slash and end_index == root_slice.len) {
+ end_index += 1;
+ }
+
+ if (end_index == 0)
+ return null;
+
+ return path[0..end_index];
+}
+
+pub fn dirnamePosix(path: []const u8) ?[]const u8 {
+ if (path.len == 0)
+ return null;
+
+ var end_index: usize = path.len - 1;
+ while (path[end_index] == '/') {
+ if (end_index == 0)
+ return path[0..1];
+ end_index -= 1;
+ }
+
+ while (path[end_index] != '/') {
+ if (end_index == 0)
+ return null;
+ end_index -= 1;
+ }
+
+ if (end_index == 0 and path[end_index] == '/')
+ return path[0..1];
+
+ if (end_index == 0)
+ return null;
+
+ return path[0..end_index];
+}
+
+test "dirnamePosix" {
+ testDirnamePosix("/a/b/c", "/a/b");
+ testDirnamePosix("/a/b/c///", "/a/b");
+ testDirnamePosix("/a", "/");
+ testDirnamePosix("/", "/");
+ testDirnamePosix("////", "/");
+ testDirnamePosix("", null);
+ testDirnamePosix("a", null);
+ testDirnamePosix("a/", null);
+ testDirnamePosix("a//", null);
+}
+
+test "dirnameWindows" {
+ testDirnameWindows("c:\\", "c:\\");
+ testDirnameWindows("c:\\foo", "c:\\");
+ testDirnameWindows("c:\\foo\\", "c:\\");
+ testDirnameWindows("c:\\foo\\bar", "c:\\foo");
+ testDirnameWindows("c:\\foo\\bar\\", "c:\\foo");
+ testDirnameWindows("c:\\foo\\bar\\baz", "c:\\foo\\bar");
+ testDirnameWindows("\\", "\\");
+ testDirnameWindows("\\foo", "\\");
+ testDirnameWindows("\\foo\\", "\\");
+ testDirnameWindows("\\foo\\bar", "\\foo");
+ testDirnameWindows("\\foo\\bar\\", "\\foo");
+ testDirnameWindows("\\foo\\bar\\baz", "\\foo\\bar");
+ testDirnameWindows("c:", "c:");
+ testDirnameWindows("c:foo", "c:");
+ testDirnameWindows("c:foo\\", "c:");
+ testDirnameWindows("c:foo\\bar", "c:foo");
+ testDirnameWindows("c:foo\\bar\\", "c:foo");
+ testDirnameWindows("c:foo\\bar\\baz", "c:foo\\bar");
+ testDirnameWindows("file:stream", null);
+ testDirnameWindows("dir\\file:stream", "dir");
+ testDirnameWindows("\\\\unc\\share", "\\\\unc\\share");
+ testDirnameWindows("\\\\unc\\share\\foo", "\\\\unc\\share\\");
+ testDirnameWindows("\\\\unc\\share\\foo\\", "\\\\unc\\share\\");
+ testDirnameWindows("\\\\unc\\share\\foo\\bar", "\\\\unc\\share\\foo");
+ testDirnameWindows("\\\\unc\\share\\foo\\bar\\", "\\\\unc\\share\\foo");
+ testDirnameWindows("\\\\unc\\share\\foo\\bar\\baz", "\\\\unc\\share\\foo\\bar");
+ testDirnameWindows("/a/b/", "/a");
+ testDirnameWindows("/a/b", "/a");
+ testDirnameWindows("/a", "/");
+ testDirnameWindows("", null);
+ testDirnameWindows("/", "/");
+ testDirnameWindows("////", "/");
+ testDirnameWindows("foo", null);
+}
+
+fn testDirnamePosix(input: []const u8, expected_output: ?[]const u8) void {
+ if (dirnamePosix(input)) |output| {
+ testing.expect(mem.eql(u8, output, expected_output.?));
+ } else {
+ testing.expect(expected_output == null);
+ }
+}
+
+fn testDirnameWindows(input: []const u8, expected_output: ?[]const u8) void {
+ if (dirnameWindows(input)) |output| {
+ testing.expect(mem.eql(u8, output, expected_output.?));
+ } else {
+ testing.expect(expected_output == null);
+ }
+}
+
+pub fn basename(path: []const u8) []const u8 {
+ if (windows.is_the_target) {
+ return basenameWindows(path);
+ } else {
+ return basenamePosix(path);
+ }
+}
+
+pub fn basenamePosix(path: []const u8) []const u8 {
+ if (path.len == 0)
+ return [_]u8{};
+
+ var end_index: usize = path.len - 1;
+ while (path[end_index] == '/') {
+ if (end_index == 0)
+ return [_]u8{};
+ end_index -= 1;
+ }
+ var start_index: usize = end_index;
+ end_index += 1;
+ while (path[start_index] != '/') {
+ if (start_index == 0)
+ return path[0..end_index];
+ start_index -= 1;
+ }
+
+ return path[start_index + 1 .. end_index];
+}
+
+pub fn basenameWindows(path: []const u8) []const u8 {
+ if (path.len == 0)
+ return [_]u8{};
+
+ var end_index: usize = path.len - 1;
+ while (true) {
+ const byte = path[end_index];
+ if (byte == '/' or byte == '\\') {
+ if (end_index == 0)
+ return [_]u8{};
+ end_index -= 1;
+ continue;
+ }
+ if (byte == ':' and end_index == 1) {
+ return [_]u8{};
+ }
+ break;
+ }
+
+ var start_index: usize = end_index;
+ end_index += 1;
+ while (path[start_index] != '/' and path[start_index] != '\\' and
+ !(path[start_index] == ':' and start_index == 1))
+ {
+ if (start_index == 0)
+ return path[0..end_index];
+ start_index -= 1;
+ }
+
+ return path[start_index + 1 .. end_index];
+}
+
+test "basename" {
+ testBasename("", "");
+ testBasename("/", "");
+ testBasename("/dir/basename.ext", "basename.ext");
+ testBasename("/basename.ext", "basename.ext");
+ testBasename("basename.ext", "basename.ext");
+ testBasename("basename.ext/", "basename.ext");
+ testBasename("basename.ext//", "basename.ext");
+ testBasename("/aaa/bbb", "bbb");
+ testBasename("/aaa/", "aaa");
+ testBasename("/aaa/b", "b");
+ testBasename("/a/b", "b");
+ testBasename("//a", "a");
+
+ testBasenamePosix("\\dir\\basename.ext", "\\dir\\basename.ext");
+ testBasenamePosix("\\basename.ext", "\\basename.ext");
+ testBasenamePosix("basename.ext", "basename.ext");
+ testBasenamePosix("basename.ext\\", "basename.ext\\");
+ testBasenamePosix("basename.ext\\\\", "basename.ext\\\\");
+ testBasenamePosix("foo", "foo");
+
+ testBasenameWindows("\\dir\\basename.ext", "basename.ext");
+ testBasenameWindows("\\basename.ext", "basename.ext");
+ testBasenameWindows("basename.ext", "basename.ext");
+ testBasenameWindows("basename.ext\\", "basename.ext");
+ testBasenameWindows("basename.ext\\\\", "basename.ext");
+ testBasenameWindows("foo", "foo");
+ testBasenameWindows("C:", "");
+ testBasenameWindows("C:.", ".");
+ testBasenameWindows("C:\\", "");
+ testBasenameWindows("C:\\dir\\base.ext", "base.ext");
+ testBasenameWindows("C:\\basename.ext", "basename.ext");
+ testBasenameWindows("C:basename.ext", "basename.ext");
+ testBasenameWindows("C:basename.ext\\", "basename.ext");
+ testBasenameWindows("C:basename.ext\\\\", "basename.ext");
+ testBasenameWindows("C:foo", "foo");
+ testBasenameWindows("file:stream", "file:stream");
+}
+
+fn testBasename(input: []const u8, expected_output: []const u8) void {
+ testing.expectEqualSlices(u8, expected_output, basename(input));
+}
+
+fn testBasenamePosix(input: []const u8, expected_output: []const u8) void {
+ testing.expectEqualSlices(u8, expected_output, basenamePosix(input));
+}
+
+fn testBasenameWindows(input: []const u8, expected_output: []const u8) void {
+ testing.expectEqualSlices(u8, expected_output, basenameWindows(input));
+}
+
+/// Returns the relative path from `from` to `to`. If `from` and `to` each
+/// resolve to the same path (after calling `resolve` on each), a zero-length
+/// string is returned.
+/// On Windows this canonicalizes the drive to a capital letter and paths to `\\`.
+pub fn relative(allocator: *Allocator, from: []const u8, to: []const u8) ![]u8 {
+ if (windows.is_the_target) {
+ return relativeWindows(allocator, from, to);
+ } else {
+ return relativePosix(allocator, from, to);
+ }
+}
+
+pub fn relativeWindows(allocator: *Allocator, from: []const u8, to: []const u8) ![]u8 {
+ const resolved_from = try resolveWindows(allocator, [_][]const u8{from});
+ defer allocator.free(resolved_from);
+
+ var clean_up_resolved_to = true;
+ const resolved_to = try resolveWindows(allocator, [_][]const u8{to});
+ defer if (clean_up_resolved_to) allocator.free(resolved_to);
+
+ const parsed_from = windowsParsePath(resolved_from);
+ const parsed_to = windowsParsePath(resolved_to);
+ const result_is_to = x: {
+ if (parsed_from.kind != parsed_to.kind) {
+ break :x true;
+ } else switch (parsed_from.kind) {
+ WindowsPath.Kind.NetworkShare => {
+ break :x !networkShareServersEql(parsed_to.disk_designator, parsed_from.disk_designator);
+ },
+ WindowsPath.Kind.Drive => {
+ break :x asciiUpper(parsed_from.disk_designator[0]) != asciiUpper(parsed_to.disk_designator[0]);
+ },
+ else => unreachable,
+ }
+ };
+
+ if (result_is_to) {
+ clean_up_resolved_to = false;
+ return resolved_to;
+ }
+
+ var from_it = mem.tokenize(resolved_from, "/\\");
+ var to_it = mem.tokenize(resolved_to, "/\\");
+ while (true) {
+ const from_component = from_it.next() orelse return mem.dupe(allocator, u8, to_it.rest());
+ const to_rest = to_it.rest();
+ if (to_it.next()) |to_component| {
+ // TODO ASCII is wrong, we actually need full unicode support to compare paths.
+ if (asciiEqlIgnoreCase(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] = '.';
+ result_index += 1;
+ result[result_index] = '.';
+ result_index += 1;
+ result[result_index] = '\\';
+ result_index += 1;
+ }
+ // shave off the trailing slash
+ result_index -= 1;
+
+ var rest_it = mem.tokenize(to_rest, "/\\");
+ while (rest_it.next()) |to_component| {
+ result[result_index] = '\\';
+ result_index += 1;
+ mem.copy(u8, result[result_index..], to_component);
+ result_index += to_component.len;
+ }
+
+ return result[0..result_index];
+ }
+
+ return [_]u8{};
+}
+
+pub fn relativePosix(allocator: *Allocator, from: []const u8, to: []const u8) ![]u8 {
+ const resolved_from = try resolvePosix(allocator, [_][]const u8{from});
+ defer allocator.free(resolved_from);
+
+ const resolved_to = try resolvePosix(allocator, [_][]const u8{to});
+ defer allocator.free(resolved_to);
+
+ var from_it = mem.tokenize(resolved_from, "/");
+ var to_it = mem.tokenize(resolved_to, "/");
+ while (true) {
+ const from_component = from_it.next() orelse return mem.dupe(allocator, 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] = '.';
+ result_index += 1;
+ result[result_index] = '.';
+ result_index += 1;
+ result[result_index] = '/';
+ result_index += 1;
+ }
+ if (to_rest.len == 0) {
+ // shave off the trailing slash
+ return result[0 .. result_index - 1];
+ }
+
+ mem.copy(u8, result[result_index..], to_rest);
+ return result;
+ }
+
+ return [_]u8{};
+}
+
+test "relative" {
+ if (@import("builtin").arch == .aarch64) {
+ // TODO https://github.com/ziglang/zig/issues/3288
+ return error.SkipZigTest;
+ }
+ testRelativeWindows("c:/blah\\blah", "d:/games", "D:\\games");
+ testRelativeWindows("c:/aaaa/bbbb", "c:/aaaa", "..");
+ testRelativeWindows("c:/aaaa/bbbb", "c:/cccc", "..\\..\\cccc");
+ testRelativeWindows("c:/aaaa/bbbb", "c:/aaaa/bbbb", "");
+ testRelativeWindows("c:/aaaa/bbbb", "c:/aaaa/cccc", "..\\cccc");
+ testRelativeWindows("c:/aaaa/", "c:/aaaa/cccc", "cccc");
+ testRelativeWindows("c:/", "c:\\aaaa\\bbbb", "aaaa\\bbbb");
+ testRelativeWindows("c:/aaaa/bbbb", "d:\\", "D:\\");
+ testRelativeWindows("c:/AaAa/bbbb", "c:/aaaa/bbbb", "");
+ testRelativeWindows("c:/aaaaa/", "c:/aaaa/cccc", "..\\aaaa\\cccc");
+ testRelativeWindows("C:\\foo\\bar\\baz\\quux", "C:\\", "..\\..\\..\\..");
+ testRelativeWindows("C:\\foo\\test", "C:\\foo\\test\\bar\\package.json", "bar\\package.json");
+ testRelativeWindows("C:\\foo\\bar\\baz-quux", "C:\\foo\\bar\\baz", "..\\baz");
+ testRelativeWindows("C:\\foo\\bar\\baz", "C:\\foo\\bar\\baz-quux", "..\\baz-quux");
+ testRelativeWindows("\\\\foo\\bar", "\\\\foo\\bar\\baz", "baz");
+ testRelativeWindows("\\\\foo\\bar\\baz", "\\\\foo\\bar", "..");
+ testRelativeWindows("\\\\foo\\bar\\baz-quux", "\\\\foo\\bar\\baz", "..\\baz");
+ testRelativeWindows("\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz-quux", "..\\baz-quux");
+ testRelativeWindows("C:\\baz-quux", "C:\\baz", "..\\baz");
+ testRelativeWindows("C:\\baz", "C:\\baz-quux", "..\\baz-quux");
+ testRelativeWindows("\\\\foo\\baz-quux", "\\\\foo\\baz", "..\\baz");
+ testRelativeWindows("\\\\foo\\baz", "\\\\foo\\baz-quux", "..\\baz-quux");
+ testRelativeWindows("C:\\baz", "\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz");
+ testRelativeWindows("\\\\foo\\bar\\baz", "C:\\baz", "C:\\baz");
+
+ testRelativePosix("/var/lib", "/var", "..");
+ testRelativePosix("/var/lib", "/bin", "../../bin");
+ testRelativePosix("/var/lib", "/var/lib", "");
+ testRelativePosix("/var/lib", "/var/apache", "../apache");
+ testRelativePosix("/var/", "/var/lib", "lib");
+ testRelativePosix("/", "/var/lib", "var/lib");
+ testRelativePosix("/foo/test", "/foo/test/bar/package.json", "bar/package.json");
+ testRelativePosix("/Users/a/web/b/test/mails", "/Users/a/web/b", "../..");
+ testRelativePosix("/foo/bar/baz-quux", "/foo/bar/baz", "../baz");
+ testRelativePosix("/foo/bar/baz", "/foo/bar/baz-quux", "../baz-quux");
+ testRelativePosix("/baz-quux", "/baz", "../baz");
+ testRelativePosix("/baz", "/baz-quux", "../baz-quux");
+}
+
+fn testRelativePosix(from: []const u8, to: []const u8, expected_output: []const u8) void {
+ const result = relativePosix(debug.global_allocator, from, to) catch unreachable;
+ testing.expectEqualSlices(u8, expected_output, result);
+}
+
+fn testRelativeWindows(from: []const u8, to: []const u8, expected_output: []const u8) void {
+ const result = relativeWindows(debug.global_allocator, from, to) catch unreachable;
+ testing.expectEqualSlices(u8, expected_output, result);
+}