mirror of
https://codeberg.org/ziglang/zig.git
synced 2026-04-27 19:09:47 +03:00
08447ca47e
Instead of querying the operating system for current working directory and environment variables, this function now accepts those things as inputs.
2687 lines
113 KiB
Zig
2687 lines
113 KiB
Zig
//! POSIX paths are arbitrary sequences of `u8` with no particular encoding.
|
|
//!
|
|
//! Windows paths are arbitrary sequences of `u16` (WTF-16).
|
|
//! For cross-platform APIs that deal with sequences of `u8`, Windows
|
|
//! paths are encoded by Zig as [WTF-8](https://wtf-8.codeberg.page/).
|
|
//! WTF-8 is a superset of UTF-8 that allows encoding surrogate codepoints,
|
|
//! which enables lossless roundtripping when converting to/from WTF-16
|
|
//! (as long as the WTF-8 encoded surrogate codepoints do not form a pair).
|
|
//!
|
|
//! WASI paths are sequences of valid Unicode scalar values,
|
|
//! which means that WASI is unable to handle paths that cannot be
|
|
//! encoded as well-formed UTF-8/UTF-16.
|
|
//! https://github.com/WebAssembly/wasi-filesystem/issues/17#issuecomment-1430639353
|
|
|
|
const builtin = @import("builtin");
|
|
const native_os = builtin.target.os.tag;
|
|
|
|
const std = @import("../std.zig");
|
|
const assert = std.debug.assert;
|
|
const testing = std.testing;
|
|
const mem = std.mem;
|
|
const Allocator = std.mem.Allocator;
|
|
const eqlIgnoreCaseWtf8 = std.os.windows.eqlIgnoreCaseWtf8;
|
|
const eqlIgnoreCaseWtf16 = std.os.windows.eqlIgnoreCaseWtf16;
|
|
|
|
pub const sep_windows: u8 = '\\';
|
|
pub const sep_posix: u8 = '/';
|
|
pub const sep = switch (native_os) {
|
|
.windows, .uefi => sep_windows,
|
|
else => sep_posix,
|
|
};
|
|
|
|
pub const sep_str_windows = "\\";
|
|
pub const sep_str_posix = "/";
|
|
pub const sep_str = switch (native_os) {
|
|
.windows, .uefi => sep_str_windows,
|
|
else => sep_str_posix,
|
|
};
|
|
|
|
pub const delimiter_windows: u8 = ';';
|
|
pub const delimiter_posix: u8 = ':';
|
|
pub const delimiter = if (native_os == .windows) delimiter_windows else delimiter_posix;
|
|
|
|
/// Returns if the given byte is a valid path separator
|
|
pub fn isSep(byte: u8) bool {
|
|
return switch (native_os) {
|
|
.windows => byte == '/' or byte == '\\',
|
|
.uefi => byte == '\\',
|
|
else => byte == '/',
|
|
};
|
|
}
|
|
|
|
pub const PathType = enum {
|
|
windows,
|
|
uefi,
|
|
posix,
|
|
|
|
/// Returns true if `c` is a valid path separator for the `path_type`.
|
|
/// If `T` is `u16`, `c` is assumed to be little-endian.
|
|
pub inline fn isSep(comptime path_type: PathType, comptime T: type, c: T) bool {
|
|
return switch (path_type) {
|
|
.windows => c == mem.nativeToLittle(T, '/') or c == mem.nativeToLittle(T, '\\'),
|
|
.posix => c == mem.nativeToLittle(T, '/'),
|
|
.uefi => c == mem.nativeToLittle(T, '\\'),
|
|
};
|
|
}
|
|
};
|
|
|
|
/// This is different from mem.join in that the separator will not be repeated if
|
|
/// it is found at the end or beginning of a pair of consecutive paths.
|
|
fn joinSepMaybeZ(allocator: Allocator, separator: u8, comptime sepPredicate: fn (u8) bool, paths: []const []const u8, zero: bool) ![]u8 {
|
|
if (paths.len == 0) return if (zero) try allocator.dupe(u8, &[1]u8{0}) else &[0]u8{};
|
|
|
|
// Find first non-empty path index.
|
|
const first_path_index = blk: {
|
|
for (paths, 0..) |path, index| {
|
|
if (path.len == 0) continue else break :blk index;
|
|
}
|
|
|
|
// All paths provided were empty, so return early.
|
|
return if (zero) try allocator.dupe(u8, &[1]u8{0}) else &[0]u8{};
|
|
};
|
|
|
|
// Calculate length needed for resulting joined path buffer.
|
|
const total_len = blk: {
|
|
var sum: usize = paths[first_path_index].len;
|
|
var prev_path = paths[first_path_index];
|
|
assert(prev_path.len > 0);
|
|
var i: usize = first_path_index + 1;
|
|
while (i < paths.len) : (i += 1) {
|
|
const this_path = paths[i];
|
|
if (this_path.len == 0) continue;
|
|
const prev_sep = sepPredicate(prev_path[prev_path.len - 1]);
|
|
const this_sep = sepPredicate(this_path[0]);
|
|
sum += @intFromBool(!prev_sep and !this_sep);
|
|
sum += if (prev_sep and this_sep) this_path.len - 1 else this_path.len;
|
|
prev_path = this_path;
|
|
}
|
|
|
|
if (zero) sum += 1;
|
|
break :blk sum;
|
|
};
|
|
|
|
const buf = try allocator.alloc(u8, total_len);
|
|
errdefer allocator.free(buf);
|
|
|
|
@memcpy(buf[0..paths[first_path_index].len], paths[first_path_index]);
|
|
var buf_index: usize = paths[first_path_index].len;
|
|
var prev_path = paths[first_path_index];
|
|
assert(prev_path.len > 0);
|
|
var i: usize = first_path_index + 1;
|
|
while (i < paths.len) : (i += 1) {
|
|
const this_path = paths[i];
|
|
if (this_path.len == 0) continue;
|
|
const prev_sep = sepPredicate(prev_path[prev_path.len - 1]);
|
|
const this_sep = sepPredicate(this_path[0]);
|
|
if (!prev_sep and !this_sep) {
|
|
buf[buf_index] = separator;
|
|
buf_index += 1;
|
|
}
|
|
const adjusted_path = if (prev_sep and this_sep) this_path[1..] else this_path;
|
|
@memcpy(buf[buf_index..][0..adjusted_path.len], adjusted_path);
|
|
buf_index += adjusted_path.len;
|
|
prev_path = this_path;
|
|
}
|
|
|
|
if (zero) buf[buf.len - 1] = 0;
|
|
|
|
// No need for shrink since buf is exactly the correct size.
|
|
return buf;
|
|
}
|
|
|
|
/// Naively combines a series of paths with the native path separator.
|
|
/// Allocates memory for the result, which must be freed by the caller.
|
|
pub fn join(allocator: Allocator, paths: []const []const u8) ![]u8 {
|
|
return joinSepMaybeZ(allocator, sep, isSep, paths, false);
|
|
}
|
|
|
|
/// Naively combines a series of paths with the native path separator and null terminator.
|
|
/// Allocates memory for the result, which must be freed by the caller.
|
|
pub fn joinZ(allocator: Allocator, paths: []const []const u8) ![:0]u8 {
|
|
const out = try joinSepMaybeZ(allocator, sep, isSep, paths, true);
|
|
return out[0 .. out.len - 1 :0];
|
|
}
|
|
|
|
pub fn fmtJoin(paths: []const []const u8) std.fmt.Alt([]const []const u8, formatJoin) {
|
|
return .{ .data = paths };
|
|
}
|
|
|
|
fn formatJoin(paths: []const []const u8, w: *std.Io.Writer) std.Io.Writer.Error!void {
|
|
const first_path_idx = for (paths, 0..) |p, idx| {
|
|
if (p.len != 0) break idx;
|
|
} else return;
|
|
|
|
try w.writeAll(paths[first_path_idx]); // first component
|
|
var prev_path = paths[first_path_idx];
|
|
for (paths[first_path_idx + 1 ..]) |this_path| {
|
|
if (this_path.len == 0) continue; // skip empty components
|
|
const prev_sep = isSep(prev_path[prev_path.len - 1]);
|
|
const this_sep = isSep(this_path[0]);
|
|
if (!prev_sep and !this_sep) {
|
|
try w.writeByte(sep);
|
|
}
|
|
if (prev_sep and this_sep) {
|
|
try w.writeAll(this_path[1..]); // skip redundant separator
|
|
} else {
|
|
try w.writeAll(this_path);
|
|
}
|
|
prev_path = this_path;
|
|
}
|
|
}
|
|
|
|
fn testJoinMaybeZUefi(paths: []const []const u8, expected: []const u8, zero: bool) !void {
|
|
const uefiIsSep = struct {
|
|
fn isSep(byte: u8) bool {
|
|
return byte == '\\';
|
|
}
|
|
}.isSep;
|
|
const actual = try joinSepMaybeZ(testing.allocator, sep_windows, uefiIsSep, paths, zero);
|
|
defer testing.allocator.free(actual);
|
|
try testing.expectEqualSlices(u8, expected, if (zero) actual[0 .. actual.len - 1 :0] else actual);
|
|
}
|
|
|
|
fn testJoinMaybeZWindows(paths: []const []const u8, expected: []const u8, zero: bool) !void {
|
|
const windowsIsSep = struct {
|
|
fn isSep(byte: u8) bool {
|
|
return byte == '/' or byte == '\\';
|
|
}
|
|
}.isSep;
|
|
const actual = try joinSepMaybeZ(testing.allocator, sep_windows, windowsIsSep, paths, zero);
|
|
defer testing.allocator.free(actual);
|
|
try testing.expectEqualSlices(u8, expected, if (zero) actual[0 .. actual.len - 1 :0] else actual);
|
|
}
|
|
|
|
fn testJoinMaybeZPosix(paths: []const []const u8, expected: []const u8, zero: bool) !void {
|
|
const posixIsSep = struct {
|
|
fn isSep(byte: u8) bool {
|
|
return byte == '/';
|
|
}
|
|
}.isSep;
|
|
const actual = try joinSepMaybeZ(testing.allocator, sep_posix, posixIsSep, paths, zero);
|
|
defer testing.allocator.free(actual);
|
|
try testing.expectEqualSlices(u8, expected, if (zero) actual[0 .. actual.len - 1 :0] else actual);
|
|
}
|
|
|
|
test join {
|
|
{
|
|
const actual: []u8 = try join(testing.allocator, &[_][]const u8{});
|
|
defer testing.allocator.free(actual);
|
|
try testing.expectEqualSlices(u8, "", actual);
|
|
}
|
|
{
|
|
const actual: [:0]u8 = try joinZ(testing.allocator, &[_][]const u8{});
|
|
defer testing.allocator.free(actual);
|
|
try testing.expectEqualSlices(u8, "", actual);
|
|
}
|
|
for (&[_]bool{ false, true }) |zero| {
|
|
try testJoinMaybeZWindows(&[_][]const u8{}, "", zero);
|
|
try testJoinMaybeZWindows(&[_][]const u8{ "c:\\a\\b", "c" }, "c:\\a\\b\\c", zero);
|
|
try testJoinMaybeZWindows(&[_][]const u8{ "c:\\a\\b", "c" }, "c:\\a\\b\\c", zero);
|
|
try testJoinMaybeZWindows(&[_][]const u8{ "c:\\a\\b\\", "\\c" }, "c:\\a\\b\\c", zero);
|
|
|
|
try testJoinMaybeZWindows(&[_][]const u8{ "c:\\", "a", "b\\", "c" }, "c:\\a\\b\\c", zero);
|
|
try testJoinMaybeZWindows(&[_][]const u8{ "c:\\a\\", "b\\", "c" }, "c:\\a\\b\\c", zero);
|
|
|
|
try testJoinMaybeZWindows(
|
|
&[_][]const u8{ "c:\\home\\andy\\dev\\zig\\build\\lib\\zig\\std", "ab.zig" },
|
|
"c:\\home\\andy\\dev\\zig\\build\\lib\\zig\\std\\ab.zig",
|
|
zero,
|
|
);
|
|
|
|
try testJoinMaybeZUefi(&[_][]const u8{ "EFI", "Boot", "bootx64.efi" }, "EFI\\Boot\\bootx64.efi", zero);
|
|
try testJoinMaybeZUefi(&[_][]const u8{ "EFI\\Boot", "bootx64.efi" }, "EFI\\Boot\\bootx64.efi", zero);
|
|
try testJoinMaybeZUefi(&[_][]const u8{ "EFI\\", "\\Boot", "bootx64.efi" }, "EFI\\Boot\\bootx64.efi", zero);
|
|
try testJoinMaybeZUefi(&[_][]const u8{ "EFI\\", "\\Boot\\", "\\bootx64.efi" }, "EFI\\Boot\\bootx64.efi", zero);
|
|
|
|
try testJoinMaybeZWindows(&[_][]const u8{ "c:\\", "a", "b/", "c" }, "c:\\a\\b/c", zero);
|
|
try testJoinMaybeZWindows(&[_][]const u8{ "c:\\a/", "b\\", "/c" }, "c:\\a/b\\c", zero);
|
|
|
|
try testJoinMaybeZWindows(&[_][]const u8{ "", "c:\\", "", "", "a", "b\\", "c", "" }, "c:\\a\\b\\c", zero);
|
|
try testJoinMaybeZWindows(&[_][]const u8{ "c:\\a/", "", "b\\", "", "/c" }, "c:\\a/b\\c", zero);
|
|
try testJoinMaybeZWindows(&[_][]const u8{ "", "" }, "", zero);
|
|
|
|
try testJoinMaybeZPosix(&[_][]const u8{}, "", zero);
|
|
try testJoinMaybeZPosix(&[_][]const u8{ "/a/b", "c" }, "/a/b/c", zero);
|
|
try testJoinMaybeZPosix(&[_][]const u8{ "/a/b/", "c" }, "/a/b/c", zero);
|
|
|
|
try testJoinMaybeZPosix(&[_][]const u8{ "/", "a", "b/", "c" }, "/a/b/c", zero);
|
|
try testJoinMaybeZPosix(&[_][]const u8{ "/a/", "b/", "c" }, "/a/b/c", zero);
|
|
|
|
try testJoinMaybeZPosix(
|
|
&[_][]const u8{ "/home/andy/dev/zig/build/lib/zig/std", "ab.zig" },
|
|
"/home/andy/dev/zig/build/lib/zig/std/ab.zig",
|
|
zero,
|
|
);
|
|
|
|
try testJoinMaybeZPosix(&[_][]const u8{ "a", "/c" }, "a/c", zero);
|
|
try testJoinMaybeZPosix(&[_][]const u8{ "a/", "/c" }, "a/c", zero);
|
|
|
|
try testJoinMaybeZPosix(&[_][]const u8{ "", "/", "a", "", "b/", "c", "" }, "/a/b/c", zero);
|
|
try testJoinMaybeZPosix(&[_][]const u8{ "/a/", "", "", "b/", "c" }, "/a/b/c", zero);
|
|
try testJoinMaybeZPosix(&[_][]const u8{ "", "" }, "", zero);
|
|
}
|
|
}
|
|
|
|
pub fn isAbsoluteZ(path_c: [*:0]const u8) bool {
|
|
if (native_os == .windows) {
|
|
return isAbsoluteWindowsZ(path_c);
|
|
} else {
|
|
return isAbsolutePosixZ(path_c);
|
|
}
|
|
}
|
|
|
|
pub fn isAbsolute(path: []const u8) bool {
|
|
if (native_os == .windows) {
|
|
return isAbsoluteWindows(path);
|
|
} else {
|
|
return isAbsolutePosix(path);
|
|
}
|
|
}
|
|
|
|
fn isAbsoluteWindowsImpl(comptime T: type, path: []const T) bool {
|
|
return switch (getWin32PathType(T, path)) {
|
|
// Unambiguously absolute
|
|
.drive_absolute, .unc_absolute, .local_device, .root_local_device => true,
|
|
// Unambiguously relative
|
|
.relative => false,
|
|
// Ambiguous, more absolute than relative
|
|
.rooted => true,
|
|
// Ambiguous, more relative than absolute
|
|
.drive_relative => false,
|
|
};
|
|
}
|
|
|
|
pub fn isAbsoluteWindows(path: []const u8) bool {
|
|
return isAbsoluteWindowsImpl(u8, path);
|
|
}
|
|
|
|
pub fn isAbsoluteWindowsW(path_w: [*:0]const u16) bool {
|
|
return isAbsoluteWindowsImpl(u16, mem.sliceTo(path_w, 0));
|
|
}
|
|
|
|
pub fn isAbsoluteWindowsWtf16(path: []const u16) bool {
|
|
return isAbsoluteWindowsImpl(u16, path);
|
|
}
|
|
|
|
pub fn isAbsoluteWindowsZ(path_c: [*:0]const u8) bool {
|
|
return isAbsoluteWindowsImpl(u8, mem.sliceTo(path_c, 0));
|
|
}
|
|
|
|
pub fn isAbsolutePosix(path: []const u8) bool {
|
|
return path.len > 0 and path[0] == sep_posix;
|
|
}
|
|
|
|
pub fn isAbsolutePosixZ(path_c: [*:0]const u8) bool {
|
|
return isAbsolutePosix(mem.sliceTo(path_c, 0));
|
|
}
|
|
|
|
test isAbsoluteWindows {
|
|
try testIsAbsoluteWindows("", false);
|
|
try testIsAbsoluteWindows("/", true);
|
|
try testIsAbsoluteWindows("//", true);
|
|
try testIsAbsoluteWindows("//server", true);
|
|
try testIsAbsoluteWindows("//server/file", true);
|
|
try testIsAbsoluteWindows("\\\\server\\file", true);
|
|
try testIsAbsoluteWindows("\\\\server", true);
|
|
try testIsAbsoluteWindows("\\\\", true);
|
|
try testIsAbsoluteWindows("c", false);
|
|
try testIsAbsoluteWindows("c:", false);
|
|
try testIsAbsoluteWindows("c:\\", true);
|
|
try testIsAbsoluteWindows("c:/", true);
|
|
try testIsAbsoluteWindows("c://", true);
|
|
try testIsAbsoluteWindows("C:/Users/", true);
|
|
try testIsAbsoluteWindows("C:\\Users\\", true);
|
|
try testIsAbsoluteWindows("C:cwd/another", false);
|
|
try testIsAbsoluteWindows("C:cwd\\another", false);
|
|
try testIsAbsoluteWindows("λ:\\", true);
|
|
try testIsAbsoluteWindows("λ:", false);
|
|
try testIsAbsoluteWindows("\u{10000}:\\", false);
|
|
try testIsAbsoluteWindows("directory/directory", false);
|
|
try testIsAbsoluteWindows("directory\\directory", false);
|
|
try testIsAbsoluteWindows("/usr/local", true);
|
|
}
|
|
|
|
test isAbsolutePosix {
|
|
try testIsAbsolutePosix("", false);
|
|
try testIsAbsolutePosix("/home/foo", true);
|
|
try testIsAbsolutePosix("/home/foo/..", true);
|
|
try testIsAbsolutePosix("bar/", false);
|
|
try testIsAbsolutePosix("./baz", false);
|
|
}
|
|
|
|
fn testIsAbsoluteWindows(path: []const u8, expected_result: bool) !void {
|
|
try testing.expectEqual(expected_result, isAbsoluteWindows(path));
|
|
const path_w = try std.unicode.wtf8ToWtf16LeAllocZ(std.testing.allocator, path);
|
|
defer std.testing.allocator.free(path_w);
|
|
try testing.expectEqual(expected_result, isAbsoluteWindowsW(path_w));
|
|
try testing.expectEqual(expected_result, isAbsoluteWindowsWtf16(path_w));
|
|
}
|
|
|
|
fn testIsAbsolutePosix(path: []const u8, expected_result: bool) !void {
|
|
try testing.expectEqual(expected_result, isAbsolutePosix(path));
|
|
}
|
|
|
|
/// Deprecated; see `WindowsPath2`
|
|
pub const WindowsPath = struct {
|
|
is_abs: bool,
|
|
kind: Kind,
|
|
disk_designator: []const u8,
|
|
|
|
pub const Kind = enum {
|
|
None,
|
|
Drive,
|
|
NetworkShare,
|
|
};
|
|
};
|
|
|
|
/// Deprecated; see `parsePathWindows`
|
|
pub fn windowsParsePath(path: []const u8) WindowsPath {
|
|
if (path.len >= 2 and path[1] == ':') {
|
|
return WindowsPath{
|
|
.is_abs = isAbsoluteWindows(path),
|
|
.kind = WindowsPath.Kind.Drive,
|
|
.disk_designator = path[0..2],
|
|
};
|
|
}
|
|
if (path.len >= 1 and (path[0] == '/' or path[0] == '\\') and
|
|
(path.len == 1 or (path[1] != '/' and path[1] != '\\')))
|
|
{
|
|
return WindowsPath{
|
|
.is_abs = true,
|
|
.kind = WindowsPath.Kind.None,
|
|
.disk_designator = path[0..0],
|
|
};
|
|
}
|
|
const relative_path = WindowsPath{
|
|
.kind = WindowsPath.Kind.None,
|
|
.disk_designator = &[_]u8{},
|
|
.is_abs = false,
|
|
};
|
|
|
|
if (path.len >= 2 and PathType.windows.isSep(u8, path[0]) and PathType.windows.isSep(u8, path[1])) {
|
|
const root_end = root_end: {
|
|
var server_end = mem.findAnyPos(u8, path, 2, "/\\") orelse break :root_end path.len;
|
|
while (server_end < path.len and PathType.windows.isSep(u8, path[server_end])) server_end += 1;
|
|
break :root_end mem.findAnyPos(u8, path, server_end, "/\\") orelse path.len;
|
|
};
|
|
return WindowsPath{
|
|
.is_abs = true,
|
|
.kind = WindowsPath.Kind.NetworkShare,
|
|
.disk_designator = path[0..root_end],
|
|
};
|
|
}
|
|
return relative_path;
|
|
}
|
|
|
|
test windowsParsePath {
|
|
{
|
|
const parsed = windowsParsePath("//a/b");
|
|
try testing.expect(parsed.is_abs);
|
|
try testing.expect(parsed.kind == WindowsPath.Kind.NetworkShare);
|
|
try testing.expect(mem.eql(u8, parsed.disk_designator, "//a/b"));
|
|
}
|
|
{
|
|
const parsed = windowsParsePath("\\\\a\\b");
|
|
try testing.expect(parsed.is_abs);
|
|
try testing.expect(parsed.kind == WindowsPath.Kind.NetworkShare);
|
|
try testing.expect(mem.eql(u8, parsed.disk_designator, "\\\\a\\b"));
|
|
}
|
|
{
|
|
const parsed = windowsParsePath("\\\\a/b");
|
|
try testing.expect(parsed.is_abs);
|
|
try testing.expect(parsed.kind == WindowsPath.Kind.NetworkShare);
|
|
try testing.expect(mem.eql(u8, parsed.disk_designator, "\\\\a/b"));
|
|
}
|
|
{
|
|
const parsed = windowsParsePath("\\/a\\");
|
|
try testing.expect(parsed.is_abs);
|
|
try testing.expect(parsed.kind == WindowsPath.Kind.NetworkShare);
|
|
try testing.expect(mem.eql(u8, parsed.disk_designator, "\\/a\\"));
|
|
}
|
|
{
|
|
const parsed = windowsParsePath("\\\\a\\\\b");
|
|
try testing.expect(parsed.is_abs);
|
|
try testing.expect(parsed.kind == WindowsPath.Kind.NetworkShare);
|
|
try testing.expect(mem.eql(u8, parsed.disk_designator, "\\\\a\\\\b"));
|
|
}
|
|
{
|
|
const parsed = windowsParsePath("\\\\a\\\\b\\c");
|
|
try testing.expect(parsed.is_abs);
|
|
try testing.expect(parsed.kind == WindowsPath.Kind.NetworkShare);
|
|
try testing.expect(mem.eql(u8, parsed.disk_designator, "\\\\a\\\\b"));
|
|
}
|
|
{
|
|
const parsed = windowsParsePath("/usr/local");
|
|
try testing.expect(parsed.is_abs);
|
|
try testing.expect(parsed.kind == WindowsPath.Kind.None);
|
|
try testing.expect(mem.eql(u8, parsed.disk_designator, ""));
|
|
}
|
|
{
|
|
const parsed = windowsParsePath("c:../");
|
|
try testing.expect(!parsed.is_abs);
|
|
try testing.expect(parsed.kind == WindowsPath.Kind.Drive);
|
|
try testing.expect(mem.eql(u8, parsed.disk_designator, "c:"));
|
|
}
|
|
}
|
|
|
|
/// On Windows, this calls `parsePathWindows` and on POSIX it calls `parsePathPosix`.
|
|
///
|
|
/// Returns a platform-specific struct with two fields: `root` and `kind`.
|
|
/// The `root` will be a slice of `path` (`/` for POSIX absolute paths, and things
|
|
/// like `C:\`, `\\server\share\`, etc for Windows paths).
|
|
/// If the path is of kind `.relative`, then `root` will be zero-length.
|
|
pub fn parsePath(path: []const u8) switch (native_os) {
|
|
.windows => WindowsPath2(u8),
|
|
else => PosixPath,
|
|
} {
|
|
switch (native_os) {
|
|
.windows => return parsePathWindows(u8, path),
|
|
else => return parsePathPosix(path),
|
|
}
|
|
}
|
|
|
|
const PosixPath = struct {
|
|
kind: enum { relative, absolute },
|
|
root: []const u8,
|
|
};
|
|
|
|
pub fn parsePathPosix(path: []const u8) PosixPath {
|
|
const abs = isAbsolutePosix(path);
|
|
return .{
|
|
.kind = if (abs) .absolute else .relative,
|
|
.root = if (abs) path[0..1] else path[0..0],
|
|
};
|
|
}
|
|
|
|
test parsePathPosix {
|
|
{
|
|
const parsed = parsePathPosix("a/b");
|
|
try testing.expectEqual(.relative, parsed.kind);
|
|
try testing.expectEqualStrings("", parsed.root);
|
|
}
|
|
{
|
|
const parsed = parsePathPosix("/a/b");
|
|
try testing.expectEqual(.absolute, parsed.kind);
|
|
try testing.expectEqualStrings("/", parsed.root);
|
|
}
|
|
{
|
|
const parsed = parsePathPosix("///a/b");
|
|
try testing.expectEqual(.absolute, parsed.kind);
|
|
try testing.expectEqualStrings("/", parsed.root);
|
|
}
|
|
}
|
|
|
|
pub fn WindowsPath2(comptime T: type) type {
|
|
return struct {
|
|
kind: Win32PathType,
|
|
root: []const T,
|
|
};
|
|
}
|
|
|
|
pub fn parsePathWindows(comptime T: type, path: []const T) WindowsPath2(T) {
|
|
const kind = getWin32PathType(T, path);
|
|
const root = root: switch (kind) {
|
|
.drive_absolute, .drive_relative => {
|
|
const drive_letter_len = getDriveLetter(T, path).len;
|
|
break :root path[0 .. drive_letter_len + @as(usize, if (kind == .drive_absolute) 2 else 1)];
|
|
},
|
|
.relative => path[0..0],
|
|
.local_device => path[0..4],
|
|
.root_local_device => path,
|
|
.rooted => path[0..1],
|
|
.unc_absolute => {
|
|
const unc = parseUNC(T, path);
|
|
// There may be any number of path separators between the server and the share,
|
|
// so take that into account by using pointer math to get the difference.
|
|
var root_len = 2 + (unc.share.ptr - unc.server.ptr) + unc.share.len;
|
|
if (unc.sep_after_share) root_len += 1;
|
|
break :root path[0..root_len];
|
|
},
|
|
};
|
|
return .{
|
|
.kind = kind,
|
|
.root = root,
|
|
};
|
|
}
|
|
|
|
test parsePathWindows {
|
|
{
|
|
const path = "//a/b";
|
|
const parsed = parsePathWindows(u8, path);
|
|
try testing.expectEqual(.unc_absolute, parsed.kind);
|
|
try testing.expectEqualStrings("//a/b", parsed.root);
|
|
try testWindowsParsePathHarmony(path);
|
|
}
|
|
{
|
|
const path = "\\\\a\\b";
|
|
const parsed = parsePathWindows(u8, path);
|
|
try testing.expectEqual(.unc_absolute, parsed.kind);
|
|
try testing.expectEqualStrings("\\\\a\\b", parsed.root);
|
|
try testWindowsParsePathHarmony(path);
|
|
}
|
|
{
|
|
const path = "\\/a/b/c";
|
|
const parsed = parsePathWindows(u8, path);
|
|
try testing.expectEqual(.unc_absolute, parsed.kind);
|
|
try testing.expectEqualStrings("\\/a/b/", parsed.root);
|
|
try testWindowsParsePathHarmony(path);
|
|
}
|
|
{
|
|
const path = "\\\\a\\";
|
|
const parsed = parsePathWindows(u8, path);
|
|
try testing.expectEqual(.unc_absolute, parsed.kind);
|
|
try testing.expectEqualStrings("\\\\a\\", parsed.root);
|
|
try testWindowsParsePathHarmony(path);
|
|
}
|
|
{
|
|
const path = "\\\\a\\b\\";
|
|
const parsed = parsePathWindows(u8, path);
|
|
try testing.expectEqual(.unc_absolute, parsed.kind);
|
|
try testing.expectEqualStrings("\\\\a\\b\\", parsed.root);
|
|
try testWindowsParsePathHarmony(path);
|
|
}
|
|
{
|
|
const path = "\\\\a\\/b\\/";
|
|
const parsed = parsePathWindows(u8, path);
|
|
try testing.expectEqual(.unc_absolute, parsed.kind);
|
|
try testing.expectEqualStrings("\\\\a\\/b\\", parsed.root);
|
|
try testWindowsParsePathHarmony(path);
|
|
}
|
|
{
|
|
const path = "\\\\кириллица\\ελληνικά\\português";
|
|
const parsed = parsePathWindows(u8, path);
|
|
try testing.expectEqual(.unc_absolute, parsed.kind);
|
|
try testing.expectEqualStrings("\\\\кириллица\\ελληνικά\\", parsed.root);
|
|
try testWindowsParsePathHarmony(path);
|
|
}
|
|
{
|
|
const path = "/usr/local";
|
|
const parsed = parsePathWindows(u8, path);
|
|
try testing.expectEqual(.rooted, parsed.kind);
|
|
try testing.expectEqualStrings("/", parsed.root);
|
|
try testWindowsParsePathHarmony(path);
|
|
}
|
|
{
|
|
const path = "\\\\.";
|
|
const parsed = parsePathWindows(u8, path);
|
|
try testing.expectEqual(.root_local_device, parsed.kind);
|
|
try testing.expectEqualStrings("\\\\.", parsed.root);
|
|
try testWindowsParsePathHarmony(path);
|
|
}
|
|
{
|
|
const path = "\\\\.\\a";
|
|
const parsed = parsePathWindows(u8, path);
|
|
try testing.expectEqual(.local_device, parsed.kind);
|
|
try testing.expectEqualStrings("\\\\.\\", parsed.root);
|
|
try testWindowsParsePathHarmony(path);
|
|
}
|
|
{
|
|
const path = "c:../";
|
|
const parsed = parsePathWindows(u8, path);
|
|
try testing.expectEqual(.drive_relative, parsed.kind);
|
|
try testing.expectEqualStrings("c:", parsed.root);
|
|
try testWindowsParsePathHarmony(path);
|
|
}
|
|
{
|
|
const path = "C:\\../";
|
|
const parsed = parsePathWindows(u8, path);
|
|
try testing.expectEqual(.drive_absolute, parsed.kind);
|
|
try testing.expectEqualStrings("C:\\", parsed.root);
|
|
try testWindowsParsePathHarmony(path);
|
|
}
|
|
{
|
|
// Non-ASCII code point that is encoded as one WTF-16 code unit is considered a valid drive letter
|
|
const path = "€:\\";
|
|
const parsed = parsePathWindows(u8, path);
|
|
try testing.expectEqual(.drive_absolute, parsed.kind);
|
|
try testing.expectEqualStrings("€:\\", parsed.root);
|
|
try testWindowsParsePathHarmony(path);
|
|
}
|
|
{
|
|
const path = "€:";
|
|
const parsed = parsePathWindows(u8, path);
|
|
try testing.expectEqual(.drive_relative, parsed.kind);
|
|
try testing.expectEqualStrings("€:", parsed.root);
|
|
try testWindowsParsePathHarmony(path);
|
|
}
|
|
{
|
|
// But code points that are encoded as two WTF-16 code units are not
|
|
const path = "\u{10000}:\\";
|
|
const parsed = parsePathWindows(u8, path);
|
|
try testing.expectEqual(.relative, parsed.kind);
|
|
try testing.expectEqualStrings("", parsed.root);
|
|
try testWindowsParsePathHarmony(path);
|
|
}
|
|
{
|
|
const path = "\u{10000}:";
|
|
const parsed = parsePathWindows(u8, path);
|
|
try testing.expectEqual(.relative, parsed.kind);
|
|
try testing.expectEqualStrings("", parsed.root);
|
|
try testWindowsParsePathHarmony(path);
|
|
}
|
|
{
|
|
// Paths are assumed to be in the Win32 namespace, so while this is
|
|
// likely a NT namespace path, it's treated as a rooted path.
|
|
const path = "\\??\\foo";
|
|
const parsed = parsePathWindows(u8, path);
|
|
try testing.expectEqual(.rooted, parsed.kind);
|
|
try testing.expectEqualStrings("\\", parsed.root);
|
|
try testWindowsParsePathHarmony(path);
|
|
}
|
|
}
|
|
|
|
fn testWindowsParsePathHarmony(wtf8: []const u8) !void {
|
|
var wtf16_buf: [256]u16 = undefined;
|
|
const wtf16_len = try std.unicode.wtf8ToWtf16Le(&wtf16_buf, wtf8);
|
|
const wtf16 = wtf16_buf[0..wtf16_len];
|
|
|
|
const wtf8_parsed = parsePathWindows(u8, wtf8);
|
|
const wtf16_parsed = parsePathWindows(u16, wtf16);
|
|
|
|
var wtf8_buf: [256]u8 = undefined;
|
|
const wtf16_root_as_wtf8_len = std.unicode.wtf16LeToWtf8(&wtf8_buf, wtf16_parsed.root);
|
|
const wtf16_root_as_wtf8 = wtf8_buf[0..wtf16_root_as_wtf8_len];
|
|
|
|
try std.testing.expectEqual(wtf8_parsed.kind, wtf16_parsed.kind);
|
|
try std.testing.expectEqualStrings(wtf8_parsed.root, wtf16_root_as_wtf8);
|
|
}
|
|
|
|
/// Deprecated; use `parsePath`
|
|
pub fn diskDesignator(path: []const u8) []const u8 {
|
|
if (native_os == .windows) {
|
|
return diskDesignatorWindows(path);
|
|
} else {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
/// Deprecated; use `parsePathWindows`
|
|
pub fn diskDesignatorWindows(path: []const u8) []const u8 {
|
|
return windowsParsePath(path).disk_designator;
|
|
}
|
|
|
|
fn WindowsUNC(comptime T: type) type {
|
|
return struct {
|
|
server: []const T,
|
|
sep_after_server: bool,
|
|
share: []const T,
|
|
sep_after_share: bool,
|
|
};
|
|
}
|
|
|
|
/// Asserts that `path` starts with two path separators
|
|
fn parseUNC(comptime T: type, path: []const T) WindowsUNC(T) {
|
|
assert(path.len >= 2 and PathType.windows.isSep(T, path[0]) and PathType.windows.isSep(T, path[1]));
|
|
const any_sep = switch (T) {
|
|
u8 => "/\\",
|
|
u16 => std.unicode.wtf8ToWtf16LeStringLiteral("/\\"),
|
|
else => @compileError("only u8 (WTF-8) and u16 (WTF-16LE) are supported"),
|
|
};
|
|
// For the server, the first path separator after the initial two is always
|
|
// the terminator of the server name, even if that means the server name is
|
|
// zero-length.
|
|
const server_end = mem.findAnyPos(T, path, 2, any_sep) orelse return .{
|
|
.server = path[2..path.len],
|
|
.sep_after_server = false,
|
|
.share = path[path.len..path.len],
|
|
.sep_after_share = false,
|
|
};
|
|
// For the share, there can be any number of path separators between the server
|
|
// and the share, so we want to skip over all of them instead of just looking for
|
|
// the first one.
|
|
var it = mem.tokenizeAny(T, path[server_end + 1 ..], any_sep);
|
|
const share = it.next() orelse return .{
|
|
.server = path[2..server_end],
|
|
.sep_after_server = true,
|
|
.share = path[server_end + 1 .. server_end + 1],
|
|
.sep_after_share = false,
|
|
};
|
|
return .{
|
|
.server = path[2..server_end],
|
|
.sep_after_server = true,
|
|
.share = share,
|
|
.sep_after_share = it.index != it.buffer.len,
|
|
};
|
|
}
|
|
|
|
test parseUNC {
|
|
{
|
|
const unc = parseUNC(u8, "//");
|
|
try std.testing.expectEqualStrings("", unc.server);
|
|
try std.testing.expect(!unc.sep_after_server);
|
|
try std.testing.expectEqualStrings("", unc.share);
|
|
try std.testing.expect(!unc.sep_after_share);
|
|
}
|
|
{
|
|
const unc = parseUNC(u8, "\\\\s");
|
|
try std.testing.expectEqualStrings("s", unc.server);
|
|
try std.testing.expect(!unc.sep_after_server);
|
|
try std.testing.expectEqualStrings("", unc.share);
|
|
try std.testing.expect(!unc.sep_after_share);
|
|
}
|
|
{
|
|
const unc = parseUNC(u8, "\\\\s/");
|
|
try std.testing.expectEqualStrings("s", unc.server);
|
|
try std.testing.expect(unc.sep_after_server);
|
|
try std.testing.expectEqualStrings("", unc.share);
|
|
try std.testing.expect(!unc.sep_after_share);
|
|
}
|
|
{
|
|
const unc = parseUNC(u8, "\\/server\\share");
|
|
try std.testing.expectEqualStrings("server", unc.server);
|
|
try std.testing.expect(unc.sep_after_server);
|
|
try std.testing.expectEqualStrings("share", unc.share);
|
|
try std.testing.expect(!unc.sep_after_share);
|
|
}
|
|
{
|
|
const unc = parseUNC(u8, "/\\server\\share/");
|
|
try std.testing.expectEqualStrings("server", unc.server);
|
|
try std.testing.expect(unc.sep_after_server);
|
|
try std.testing.expectEqualStrings("share", unc.share);
|
|
try std.testing.expect(unc.sep_after_share);
|
|
}
|
|
{
|
|
const unc = parseUNC(u8, "\\\\server/\\share\\/");
|
|
try std.testing.expectEqualStrings("server", unc.server);
|
|
try std.testing.expect(unc.sep_after_server);
|
|
try std.testing.expectEqualStrings("share", unc.share);
|
|
try std.testing.expect(unc.sep_after_share);
|
|
}
|
|
{
|
|
const unc = parseUNC(u8, "\\\\server\\/\\\\");
|
|
try std.testing.expectEqualStrings("server", unc.server);
|
|
try std.testing.expect(unc.sep_after_server);
|
|
try std.testing.expectEqualStrings("", unc.share);
|
|
try std.testing.expect(!unc.sep_after_share);
|
|
}
|
|
}
|
|
|
|
const DiskDesignatorKind = enum { drive, unc };
|
|
|
|
/// `p1` and `p2` are both assumed to be the `kind` provided.
|
|
fn compareDiskDesignators(comptime T: type, kind: DiskDesignatorKind, p1: []const T, p2: []const T) bool {
|
|
const eql = switch (T) {
|
|
u8 => eqlIgnoreCaseWtf8,
|
|
u16 => eqlIgnoreCaseWtf16,
|
|
else => @compileError("only u8 (WTF-8) and u16 (WTF-16LE) is supported"),
|
|
};
|
|
switch (kind) {
|
|
.drive => {
|
|
const drive_letter1 = getDriveLetter(T, p1);
|
|
const drive_letter2 = getDriveLetter(T, p2);
|
|
|
|
return eql(drive_letter1, drive_letter2);
|
|
},
|
|
.unc => {
|
|
var unc1 = parseUNC(T, p1);
|
|
var unc2 = parseUNC(T, p2);
|
|
|
|
return eql(unc1.server, unc2.server) and
|
|
eql(unc1.share, unc2.share);
|
|
},
|
|
}
|
|
}
|
|
|
|
/// `path` is assumed to be drive-relative or drive-absolute.
|
|
fn getDriveLetter(comptime T: type, path: []const T) []const T {
|
|
const len: usize = switch (T) {
|
|
// getWin32PathType will only return .drive_absolute/.drive_relative when there is
|
|
// (1) a valid code point, and (2) a code point < U+10000, so we only need to
|
|
// get the length determined by the first byte.
|
|
u8 => std.unicode.utf8ByteSequenceLength(path[0]) catch unreachable,
|
|
u16 => 1,
|
|
else => @compileError("unsupported type: " ++ @typeName(T)),
|
|
};
|
|
return path[0..len];
|
|
}
|
|
|
|
test compareDiskDesignators {
|
|
try testCompareDiskDesignators(true, .drive, "c:", "C:\\");
|
|
try testCompareDiskDesignators(true, .drive, "C:\\", "C:");
|
|
try testCompareDiskDesignators(false, .drive, "C:\\", "D:\\");
|
|
// Case-insensitivity technically applies to non-ASCII drive letters
|
|
try testCompareDiskDesignators(true, .drive, "λ:\\", "Λ:");
|
|
|
|
try testCompareDiskDesignators(true, .unc, "\\\\server", "//server//");
|
|
try testCompareDiskDesignators(true, .unc, "\\\\server\\\\share", "/\\server/share");
|
|
try testCompareDiskDesignators(true, .unc, "\\\\server\\\\share", "/\\server/share\\\\foo");
|
|
try testCompareDiskDesignators(false, .unc, "\\\\server\\sharefoo", "/\\server/share\\foo");
|
|
try testCompareDiskDesignators(false, .unc, "\\\\serverfoo\\\\share", "//server/share");
|
|
try testCompareDiskDesignators(false, .unc, "\\\\server\\", "//server/share");
|
|
}
|
|
|
|
fn testCompareDiskDesignators(expected_result: bool, kind: DiskDesignatorKind, p1: []const u8, p2: []const u8) !void {
|
|
var wtf16_buf1: [256]u16 = undefined;
|
|
const w1_len = try std.unicode.wtf8ToWtf16Le(&wtf16_buf1, p1);
|
|
var wtf16_buf2: [256]u16 = undefined;
|
|
const w2_len = try std.unicode.wtf8ToWtf16Le(&wtf16_buf2, p2);
|
|
try std.testing.expectEqual(expected_result, compareDiskDesignators(u8, kind, p1, p2));
|
|
try std.testing.expectEqual(expected_result, compareDiskDesignators(u16, kind, wtf16_buf1[0..w1_len], wtf16_buf2[0..w2_len]));
|
|
}
|
|
|
|
/// On Windows, this calls `resolveWindows` and on POSIX it calls `resolvePosix`.
|
|
pub fn resolve(allocator: Allocator, paths: []const []const u8) Allocator.Error![]u8 {
|
|
if (native_os == .windows) {
|
|
return resolveWindows(allocator, paths);
|
|
} else {
|
|
return resolvePosix(allocator, paths);
|
|
}
|
|
}
|
|
|
|
/// This function is like a series of `cd` statements executed one after another.
|
|
/// It resolves "." and ".." to the best of its ability, but will not convert relative paths to
|
|
/// an absolute path, use Io.Dir.realpath instead.
|
|
/// ".." components may persist in the resolved path if the resolved path is relative or drive-relative.
|
|
/// Path separators are canonicalized to '\\' and drives are canonicalized to capital letters.
|
|
///
|
|
/// The result will not have a trailing path separator, except for the following scenarios:
|
|
/// - The resolved path is drive-absolute with no components (e.g. `C:\`).
|
|
/// - The resolved path is a UNC path with only a server name, and the input path contained a trailing separator
|
|
/// (e.g. `\\server\`).
|
|
/// - The resolved path is a UNC path with no components after the share name, and the input path contained a
|
|
/// trailing separator (e.g. `\\server\share\`).
|
|
///
|
|
/// Each drive has its own current working directory, which is only resolved via the paths provided.
|
|
/// In the scenario that the resolved path contains a drive-relative path that can't be resolved using the paths alone,
|
|
/// the result will be a drive-relative path.
|
|
/// Similarly, in the scenario that the resolved path contains a rooted path that can't be resolved using the paths alone,
|
|
/// the result will be a rooted path.
|
|
///
|
|
/// Note: all usage of this function should be audited due to the existence of symlinks.
|
|
/// Without performing actual syscalls, resolving `..` could be incorrect.
|
|
/// This API may break in the future: https://github.com/ziglang/zig/issues/13613
|
|
pub fn resolveWindows(allocator: Allocator, paths: []const []const u8) Allocator.Error![]u8 {
|
|
// Avoid heap allocation when paths.len is <= @bitSizeOf(usize) * 2
|
|
// (we use `* 3` because stackFallback uses 1 usize as a length)
|
|
var bit_set_allocator_state = std.heap.stackFallback(@sizeOf(usize) * 3, allocator);
|
|
const bit_set_allocator = bit_set_allocator_state.get();
|
|
var relevant_paths = try std.bit_set.DynamicBitSetUnmanaged.initEmpty(bit_set_allocator, paths.len);
|
|
defer relevant_paths.deinit(bit_set_allocator);
|
|
|
|
// Iterate the paths backwards, marking the relevant paths along the way.
|
|
// This also allows us to break from the loop whenever any earlier paths are known to be irrelevant.
|
|
var first_path_i: usize = paths.len;
|
|
const effective_root_path: WindowsPath2(u8) = root: {
|
|
var last_effective_root_path: WindowsPath2(u8) = .{ .kind = .relative, .root = "" };
|
|
var last_rooted_path_i: ?usize = null;
|
|
var last_drive_relative_path_i: usize = undefined;
|
|
while (first_path_i > 0) {
|
|
first_path_i -= 1;
|
|
const parsed = parsePathWindows(u8, paths[first_path_i]);
|
|
switch (parsed.kind) {
|
|
.unc_absolute, .root_local_device, .local_device => {
|
|
switch (last_effective_root_path.kind) {
|
|
.rooted => {},
|
|
.drive_relative => continue,
|
|
else => {
|
|
relevant_paths.set(first_path_i);
|
|
},
|
|
}
|
|
break :root parsed;
|
|
},
|
|
.drive_relative, .drive_absolute => {
|
|
switch (last_effective_root_path.kind) {
|
|
.drive_relative => if (!compareDiskDesignators(u8, .drive, parsed.root, last_effective_root_path.root)) {
|
|
continue;
|
|
} else if (last_rooted_path_i != null) {
|
|
break :root .{ .kind = .drive_absolute, .root = parsed.root };
|
|
},
|
|
.relative => last_effective_root_path = parsed,
|
|
.rooted => {
|
|
// This is the end of the line, since the rooted path will always be relative
|
|
// to this drive letter, and even if the current path is drive-relative, the
|
|
// rooted-ness makes that irrelevant.
|
|
//
|
|
// Therefore, force the kind of the effective root to be drive-absolute in order to
|
|
// properly resolve a rooted path against a drive-relative one, as the result should
|
|
// always be drive-absolute.
|
|
break :root .{ .kind = .drive_absolute, .root = parsed.root };
|
|
},
|
|
.drive_absolute, .unc_absolute, .root_local_device, .local_device => unreachable,
|
|
}
|
|
relevant_paths.set(first_path_i);
|
|
last_drive_relative_path_i = first_path_i;
|
|
if (parsed.kind == .drive_absolute) {
|
|
break :root parsed;
|
|
}
|
|
},
|
|
.relative => {
|
|
switch (last_effective_root_path.kind) {
|
|
.rooted => continue,
|
|
.relative => last_effective_root_path = parsed,
|
|
else => {},
|
|
}
|
|
relevant_paths.set(first_path_i);
|
|
},
|
|
.rooted => {
|
|
switch (last_effective_root_path.kind) {
|
|
.drive_relative => {},
|
|
.relative => last_effective_root_path = parsed,
|
|
.rooted => continue,
|
|
.drive_absolute, .unc_absolute, .root_local_device, .local_device => unreachable,
|
|
}
|
|
if (last_rooted_path_i == null) {
|
|
last_rooted_path_i = first_path_i;
|
|
relevant_paths.set(first_path_i);
|
|
}
|
|
},
|
|
}
|
|
}
|
|
// After iterating, if the pending effective root is drive-relative then that means
|
|
// nothing has led to forcing a drive-absolute root (a path that allows resolving the
|
|
// drive-specific CWD would cause an early break), so we now need to ignore all paths
|
|
// before the most recent drive-relative one. For example, if we're resolving
|
|
// { "\\rooted", "relative", "C:drive-relative" }
|
|
// then the `\rooted` and `relative` needs to be ignored since we can't
|
|
// know what the rooted path is rooted against as that'd require knowing the CWD.
|
|
if (last_effective_root_path.kind == .drive_relative) {
|
|
for (0..last_drive_relative_path_i) |i| {
|
|
relevant_paths.unset(i);
|
|
}
|
|
}
|
|
break :root last_effective_root_path;
|
|
};
|
|
|
|
var result: std.ArrayList(u8) = .empty;
|
|
defer result.deinit(allocator);
|
|
|
|
var want_path_sep_between_root_and_component = false;
|
|
switch (effective_root_path.kind) {
|
|
.root_local_device, .local_device => {
|
|
try result.ensureUnusedCapacity(allocator, 3);
|
|
result.appendSliceAssumeCapacity("\\\\");
|
|
result.appendAssumeCapacity(effective_root_path.root[2]); // . or ?
|
|
want_path_sep_between_root_and_component = true;
|
|
},
|
|
.drive_absolute, .drive_relative => {
|
|
try result.ensureUnusedCapacity(allocator, effective_root_path.root.len);
|
|
result.appendAssumeCapacity(std.ascii.toUpper(effective_root_path.root[0]));
|
|
result.appendAssumeCapacity(':');
|
|
if (effective_root_path.kind == .drive_absolute) {
|
|
result.appendAssumeCapacity('\\');
|
|
}
|
|
},
|
|
.unc_absolute => {
|
|
const unc = parseUNC(u8, effective_root_path.root);
|
|
|
|
const root_len = len: {
|
|
var len: usize = 2 + unc.server.len + unc.share.len;
|
|
if (unc.sep_after_server) len += 1;
|
|
if (unc.sep_after_share) len += 1;
|
|
break :len len;
|
|
};
|
|
try result.ensureUnusedCapacity(allocator, root_len);
|
|
result.appendSliceAssumeCapacity("\\\\");
|
|
if (unc.server.len > 0 or unc.sep_after_server) {
|
|
result.appendSliceAssumeCapacity(unc.server);
|
|
if (unc.sep_after_server)
|
|
result.appendAssumeCapacity('\\')
|
|
else
|
|
want_path_sep_between_root_and_component = true;
|
|
}
|
|
if (unc.share.len > 0) {
|
|
result.appendSliceAssumeCapacity(unc.share);
|
|
if (unc.sep_after_share)
|
|
result.appendAssumeCapacity('\\')
|
|
else
|
|
want_path_sep_between_root_and_component = true;
|
|
}
|
|
},
|
|
.rooted => {
|
|
try result.append(allocator, '\\');
|
|
},
|
|
.relative => {},
|
|
}
|
|
|
|
const root_len = result.items.len;
|
|
var negative_count: usize = 0;
|
|
for (paths[first_path_i..], first_path_i..) |path, i| {
|
|
if (!relevant_paths.isSet(i)) continue;
|
|
|
|
const parsed = parsePathWindows(u8, path);
|
|
const skip_len = parsed.root.len;
|
|
var it = mem.tokenizeAny(u8, path[skip_len..], "/\\");
|
|
while (it.next()) |component| {
|
|
if (mem.eql(u8, component, ".")) {
|
|
continue;
|
|
} else if (mem.eql(u8, component, "..")) {
|
|
if (result.items.len == 0 or (result.items.len == root_len and effective_root_path.kind == .drive_relative)) {
|
|
negative_count += 1;
|
|
continue;
|
|
}
|
|
while (true) {
|
|
if (result.items.len == root_len) {
|
|
break;
|
|
}
|
|
const end_with_sep = PathType.windows.isSep(u8, result.items[result.items.len - 1]);
|
|
result.items.len -= 1;
|
|
if (end_with_sep) break;
|
|
}
|
|
} else if (result.items.len == root_len and !want_path_sep_between_root_and_component) {
|
|
try result.appendSlice(allocator, component);
|
|
} else {
|
|
try result.ensureUnusedCapacity(allocator, 1 + component.len);
|
|
result.appendAssumeCapacity('\\');
|
|
result.appendSliceAssumeCapacity(component);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (root_len != 0 and result.items.len == root_len and negative_count == 0) {
|
|
return result.toOwnedSlice(allocator);
|
|
}
|
|
|
|
if (result.items.len == root_len) {
|
|
if (negative_count == 0) {
|
|
return allocator.dupe(u8, ".");
|
|
}
|
|
|
|
try result.ensureTotalCapacityPrecise(allocator, 3 * negative_count - 1);
|
|
for (0..negative_count - 1) |_| {
|
|
result.appendSliceAssumeCapacity("..\\");
|
|
}
|
|
result.appendSliceAssumeCapacity("..");
|
|
} else {
|
|
const dest = try result.addManyAt(allocator, root_len, 3 * negative_count);
|
|
for (0..negative_count) |i| {
|
|
dest[i * 3 ..][0..3].* = "..\\".*;
|
|
}
|
|
}
|
|
|
|
return result.toOwnedSlice(allocator);
|
|
}
|
|
|
|
/// This function is like a series of `cd` statements executed one after another.
|
|
///
|
|
/// It resolves "." and ".." to the best of its ability, but will not convert relative paths to
|
|
/// an absolute path, use Io.Dir.realpath instead.
|
|
///
|
|
/// ".." components may persist in the resolved path if the resolved path is relative.
|
|
///
|
|
/// The result does not have a trailing path separator.
|
|
///
|
|
/// This function does not perform any syscalls. Executing this series of path
|
|
/// lookups on the actual filesystem may produce different results due to
|
|
/// symlinks.
|
|
pub fn resolvePosix(allocator: Allocator, paths: []const []const u8) Allocator.Error![]u8 {
|
|
assert(paths.len > 0);
|
|
|
|
var result = std.array_list.Managed(u8).init(allocator);
|
|
defer result.deinit();
|
|
|
|
var negative_count: usize = 0;
|
|
var is_abs = false;
|
|
|
|
for (paths) |p| {
|
|
if (isAbsolutePosix(p)) {
|
|
is_abs = true;
|
|
negative_count = 0;
|
|
result.clearRetainingCapacity();
|
|
}
|
|
var it = mem.tokenizeScalar(u8, p, '/');
|
|
while (it.next()) |component| {
|
|
if (mem.eql(u8, component, ".")) {
|
|
continue;
|
|
} else if (mem.eql(u8, component, "..")) {
|
|
if (result.items.len == 0) {
|
|
negative_count += @intFromBool(!is_abs);
|
|
continue;
|
|
}
|
|
while (true) {
|
|
const ends_with_slash = result.items[result.items.len - 1] == '/';
|
|
result.items.len -= 1;
|
|
if (ends_with_slash or result.items.len == 0) break;
|
|
}
|
|
} else if (result.items.len > 0 or is_abs) {
|
|
try result.ensureUnusedCapacity(1 + component.len);
|
|
result.appendAssumeCapacity('/');
|
|
result.appendSliceAssumeCapacity(component);
|
|
} else {
|
|
try result.appendSlice(component);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (result.items.len == 0) {
|
|
if (is_abs) {
|
|
return allocator.dupe(u8, "/");
|
|
}
|
|
if (negative_count == 0) {
|
|
return allocator.dupe(u8, ".");
|
|
} else {
|
|
const real_result = try allocator.alloc(u8, 3 * negative_count - 1);
|
|
var count = negative_count - 1;
|
|
var i: usize = 0;
|
|
while (count > 0) : (count -= 1) {
|
|
real_result[i..][0..3].* = "../".*;
|
|
i += 3;
|
|
}
|
|
real_result[i..][0..2].* = "..".*;
|
|
return real_result;
|
|
}
|
|
}
|
|
|
|
if (negative_count == 0) {
|
|
return result.toOwnedSlice();
|
|
} else {
|
|
const real_result = try allocator.alloc(u8, 3 * negative_count + result.items.len);
|
|
var count = negative_count;
|
|
var i: usize = 0;
|
|
while (count > 0) : (count -= 1) {
|
|
real_result[i..][0..3].* = "../".*;
|
|
i += 3;
|
|
}
|
|
@memcpy(real_result[i..][0..result.items.len], result.items);
|
|
return real_result;
|
|
}
|
|
}
|
|
|
|
test resolve {
|
|
try testResolveWindows(&[_][]const u8{ "a", "..\\..\\.." }, "..\\..");
|
|
try testResolveWindows(&[_][]const u8{ "..", "", "..\\..\\foo" }, "..\\..\\..\\foo");
|
|
try testResolveWindows(&[_][]const u8{ "a\\b\\c\\", "..\\..\\.." }, ".");
|
|
try testResolveWindows(&[_][]const u8{"."}, ".");
|
|
try testResolveWindows(&[_][]const u8{""}, ".");
|
|
|
|
try testResolvePosix(&[_][]const u8{ "a", "../../.." }, "../..");
|
|
try testResolvePosix(&[_][]const u8{ "..", "", "../../foo" }, "../../../foo");
|
|
try testResolvePosix(&[_][]const u8{ "a/b/c/", "../../.." }, ".");
|
|
try testResolvePosix(&[_][]const u8{"."}, ".");
|
|
try testResolvePosix(&[_][]const u8{""}, ".");
|
|
}
|
|
|
|
test resolveWindows {
|
|
try testResolveWindows(
|
|
&[_][]const u8{ "Z:\\", "/usr/local", "lib\\zig\\std\\array_list.zig" },
|
|
"Z:\\usr\\local\\lib\\zig\\std\\array_list.zig",
|
|
);
|
|
try testResolveWindows(
|
|
&[_][]const u8{ "z:\\", "usr/local", "lib\\zig" },
|
|
"Z:\\usr\\local\\lib\\zig",
|
|
);
|
|
|
|
try testResolveWindows(&[_][]const u8{ "c:\\a\\b\\c", "/hi", "ok" }, "C:\\hi\\ok");
|
|
try testResolveWindows(&[_][]const u8{ "c:\\a\\b\\c\\", ".\\..\\foo" }, "C:\\a\\b\\foo");
|
|
try testResolveWindows(&[_][]const u8{ "c:/blah\\blah", "d:/games", "c:../a" }, "C:\\blah\\a");
|
|
try testResolveWindows(&[_][]const u8{ "c:/blah\\blah", "d:/games", "C:../a" }, "C:\\blah\\a");
|
|
try testResolveWindows(&[_][]const u8{ "c:/ignore", "d:\\a/b\\c/d", "\\e.exe" }, "D:\\e.exe");
|
|
try testResolveWindows(&[_][]const u8{ "c:/ignore", "c:/some/file" }, "C:\\some\\file");
|
|
// The first path "sets" the CWD, so the drive-relative path is then relative to that.
|
|
try testResolveWindows(&[_][]const u8{ "d:/foo", "d:some/dir//", "D:another" }, "D:\\foo\\some\\dir\\another");
|
|
try testResolveWindows(&[_][]const u8{ "//server/share", "..", "relative\\" }, "\\\\server\\share\\relative");
|
|
try testResolveWindows(&[_][]const u8{ "\\\\server/share", "..", "relative\\" }, "\\\\server\\share\\relative");
|
|
try testResolveWindows(&[_][]const u8{ "\\\\server/share/ignore", "//server/share/bar" }, "\\\\server\\share\\bar");
|
|
try testResolveWindows(&[_][]const u8{ "\\/server\\share/", "..", "relative" }, "\\\\server\\share\\relative");
|
|
try testResolveWindows(&[_][]const u8{ "\\\\server\\share", "C:drive-relative" }, "C:drive-relative");
|
|
try testResolveWindows(&[_][]const u8{ "c:/", "//" }, "\\\\");
|
|
try testResolveWindows(&[_][]const u8{ "c:/", "//server" }, "\\\\server");
|
|
try testResolveWindows(&[_][]const u8{ "c:/", "//server/share" }, "\\\\server\\share");
|
|
try testResolveWindows(&[_][]const u8{ "c:/", "//server//share////" }, "\\\\server\\share\\");
|
|
try testResolveWindows(&[_][]const u8{ "c:/", "///some//dir" }, "\\\\\\some\\dir");
|
|
try testResolveWindows(&[_][]const u8{ "c:foo", "bar" }, "C:foo\\bar");
|
|
try testResolveWindows(&[_][]const u8{ "C:\\foo\\tmp.3\\", "..\\tmp.3\\cycles\\root.js" }, "C:\\foo\\tmp.3\\cycles\\root.js");
|
|
// Drive-relative stays drive-relative if there's nothing to provide the drive-specific CWD
|
|
try testResolveWindows(&[_][]const u8{ "relative", "d:foo" }, "D:foo");
|
|
try testResolveWindows(&[_][]const u8{ "../..\\..", "d:foo" }, "D:foo");
|
|
try testResolveWindows(&[_][]const u8{ "../..\\..", "\\rooted", "d:foo" }, "D:foo");
|
|
try testResolveWindows(&[_][]const u8{ "C:\\foo", "../..\\..", "\\rooted", "d:foo" }, "D:foo");
|
|
try testResolveWindows(&[_][]const u8{ "D:relevant", "../..\\..", "d:foo" }, "D:..\\..\\foo");
|
|
try testResolveWindows(&[_][]const u8{ "D:relevant", "../..\\..", "\\\\.\\ignored", "C:\\ignored", "C:ignored", "\\\\ignored", "d:foo" }, "D:..\\..\\foo");
|
|
try testResolveWindows(&[_][]const u8{ "ignored", "\\\\.\\ignored", "C:\\ignored", "C:ignored", "\\\\ignored", "d:foo" }, "D:foo");
|
|
// Rooted paths remain rooted if there's no absolute path available to resolve the "root"
|
|
try testResolveWindows(&[_][]const u8{ "/foo", "bar" }, "\\foo\\bar");
|
|
// Rooted against a UNC path
|
|
try testResolveWindows(&[_][]const u8{ "//server/share/ignore", "/foo", "bar" }, "\\\\server\\share\\foo\\bar");
|
|
try testResolveWindows(&[_][]const u8{ "//server/share/", "/foo" }, "\\\\server\\share\\foo");
|
|
try testResolveWindows(&[_][]const u8{ "//server/share", "/foo" }, "\\\\server\\share\\foo");
|
|
try testResolveWindows(&[_][]const u8{ "//server/", "/foo" }, "\\\\server\\foo");
|
|
try testResolveWindows(&[_][]const u8{ "//server", "/foo" }, "\\\\server\\foo");
|
|
try testResolveWindows(&[_][]const u8{ "//", "/foo" }, "\\\\foo");
|
|
// Rooted against a drive-relative path
|
|
try testResolveWindows(&[_][]const u8{ "C:", "/foo", "bar" }, "C:\\foo\\bar");
|
|
try testResolveWindows(&[_][]const u8{ "C:\\ignore", "C:", "/foo", "bar" }, "C:\\foo\\bar");
|
|
try testResolveWindows(&[_][]const u8{ "C:\\ignore", "\\foo", "C:bar" }, "C:\\foo\\bar");
|
|
// Only the last rooted path is relevant
|
|
try testResolveWindows(&[_][]const u8{ "\\ignore", "\\foo" }, "\\foo");
|
|
try testResolveWindows(&[_][]const u8{ "c:ignore", "ignore", "\\ignore", "\\foo" }, "C:\\foo");
|
|
// Rooted is only relevant to a drive-relative if there's a previous drive-* path
|
|
try testResolveWindows(&[_][]const u8{ "\\ignore", "C:foo" }, "C:foo");
|
|
try testResolveWindows(&[_][]const u8{ "\\ignore", "\\ignore2", "C:foo" }, "C:foo");
|
|
try testResolveWindows(&[_][]const u8{ "c:ignore", "\\ignore", "\\rooted", "C:foo" }, "C:\\rooted\\foo");
|
|
try testResolveWindows(&[_][]const u8{ "c:\\ignore", "\\ignore", "\\rooted", "C:foo" }, "C:\\rooted\\foo");
|
|
try testResolveWindows(&[_][]const u8{ "d:\\ignore", "\\ignore", "\\ignore2", "C:foo" }, "C:foo");
|
|
// Root local device paths
|
|
try testResolveWindows(&[_][]const u8{"\\/."}, "\\\\.");
|
|
try testResolveWindows(&[_][]const u8{ "\\/.", "C:drive-relative" }, "C:drive-relative");
|
|
try testResolveWindows(&[_][]const u8{"/\\?"}, "\\\\?");
|
|
try testResolveWindows(&[_][]const u8{ "ignore", "c:\\ignore", "\\\\.", "foo" }, "\\\\.\\foo");
|
|
try testResolveWindows(&[_][]const u8{ "ignore", "c:\\ignore", "\\\\?", "foo" }, "\\\\?\\foo");
|
|
try testResolveWindows(&[_][]const u8{ "ignore", "c:\\ignore", "//.", "ignore", "\\foo" }, "\\\\.\\foo");
|
|
try testResolveWindows(&[_][]const u8{ "ignore", "c:\\ignore", "\\\\?", "ignore", "\\foo" }, "\\\\?\\foo");
|
|
|
|
// Keep relative paths relative.
|
|
try testResolveWindows(&[_][]const u8{"a/b"}, "a\\b");
|
|
try testResolveWindows(&[_][]const u8{".."}, "..");
|
|
try testResolveWindows(&[_][]const u8{"../.."}, "..\\..");
|
|
try testResolveWindows(&[_][]const u8{ "C:foo", "../.." }, "C:..");
|
|
try testResolveWindows(&[_][]const u8{ "d:foo", "../..\\.." }, "D:..\\..");
|
|
|
|
// Local device paths treat the \\.\ or \\?\ as the "root", everything afterwards is treated as a regular component.
|
|
try testResolveWindows(&[_][]const u8{ "\\\\?\\C:\\foo", "../bar", "baz" }, "\\\\?\\C:\\bar\\baz");
|
|
try testResolveWindows(&[_][]const u8{ "\\\\.\\C:/foo", "../../../../bar", "baz" }, "\\\\.\\bar\\baz");
|
|
try testResolveWindows(&[_][]const u8{ "//./C:/foo", "../../../../bar", "baz" }, "\\\\.\\bar\\baz");
|
|
try testResolveWindows(&[_][]const u8{ "\\\\.\\foo", ".." }, "\\\\.");
|
|
try testResolveWindows(&[_][]const u8{ "\\\\.\\foo", "..\\.." }, "\\\\.");
|
|
|
|
// Paths are assumed to be Win32, so paths that are likely NT paths are treated as a rooted path.
|
|
try testResolveWindows(&[_][]const u8{ "\\??\\C:\\foo", "/bar", "baz" }, "\\bar\\baz");
|
|
try testResolveWindows(&[_][]const u8{ "C:\\", "\\??\\C:\\foo", "bar" }, "C:\\??\\C:\\foo\\bar");
|
|
}
|
|
|
|
test resolvePosix {
|
|
try testResolvePosix(&.{ "/a/b", "c" }, "/a/b/c");
|
|
try testResolvePosix(&.{ "/a/b", "c", "//d", "e///" }, "/d/e");
|
|
try testResolvePosix(&.{ "/a/b/c", "..", "../" }, "/a");
|
|
try testResolvePosix(&.{ "/", "..", ".." }, "/");
|
|
try testResolvePosix(&.{"/a/b/c/"}, "/a/b/c");
|
|
|
|
try testResolvePosix(&.{ "/var/lib", "../", "file/" }, "/var/file");
|
|
try testResolvePosix(&.{ "/var/lib", "/../", "file/" }, "/file");
|
|
try testResolvePosix(&.{ "/some/dir", ".", "/absolute/" }, "/absolute");
|
|
try testResolvePosix(&.{ "/foo/tmp.3/", "../tmp.3/cycles/root.js" }, "/foo/tmp.3/cycles/root.js");
|
|
|
|
// Keep relative paths relative.
|
|
try testResolvePosix(&.{"a/b"}, "a/b");
|
|
try testResolvePosix(&.{"."}, ".");
|
|
try testResolvePosix(&.{ ".", "src/test.zig", "..", "../test/cases.zig" }, "test/cases.zig");
|
|
}
|
|
|
|
fn testResolveWindows(paths: []const []const u8, expected: []const u8) !void {
|
|
const actual = try resolveWindows(testing.allocator, paths);
|
|
defer testing.allocator.free(actual);
|
|
try testing.expectEqualStrings(expected, actual);
|
|
}
|
|
|
|
fn testResolvePosix(paths: []const []const u8, expected: []const u8) !void {
|
|
const actual = try resolvePosix(testing.allocator, paths);
|
|
defer testing.allocator.free(actual);
|
|
try testing.expectEqualStrings(expected, actual);
|
|
}
|
|
|
|
/// Strip the last component from a file path.
|
|
///
|
|
/// If the path is a file in the current directory (no directory component)
|
|
/// then returns null.
|
|
///
|
|
/// If the path is the root directory, returns null.
|
|
pub fn dirname(path: []const u8) ?[]const u8 {
|
|
if (native_os == .windows) {
|
|
return dirnameWindows(path);
|
|
} else {
|
|
return dirnamePosix(path);
|
|
}
|
|
}
|
|
|
|
pub fn dirnameWindows(path: []const u8) ?[]const u8 {
|
|
return dirnameInner(.windows, path);
|
|
}
|
|
|
|
pub fn dirnamePosix(path: []const u8) ?[]const u8 {
|
|
return dirnameInner(.posix, path);
|
|
}
|
|
|
|
fn dirnameInner(comptime path_type: PathType, path: []const u8) ?[]const u8 {
|
|
var it = ComponentIterator(path_type, u8).init(path);
|
|
_ = it.last() orelse return null;
|
|
const up = it.previous() orelse return it.root();
|
|
return up.path;
|
|
}
|
|
|
|
test dirnamePosix {
|
|
try testDirnamePosix("/a/b/c", "/a/b");
|
|
try testDirnamePosix("/a/b/c///", "/a/b");
|
|
try testDirnamePosix("/a", "/");
|
|
try testDirnamePosix("/", null);
|
|
try testDirnamePosix("//", null);
|
|
try testDirnamePosix("///", null);
|
|
try testDirnamePosix("////", null);
|
|
try testDirnamePosix("", null);
|
|
try testDirnamePosix("a", null);
|
|
try testDirnamePosix("a/", null);
|
|
try testDirnamePosix("a//", null);
|
|
}
|
|
|
|
test dirnameWindows {
|
|
try testDirnameWindows("c:\\", null);
|
|
try testDirnameWindows("c:\\\\", null);
|
|
try testDirnameWindows("c:\\foo", "c:\\");
|
|
try testDirnameWindows("c:\\\\foo\\", "c:\\");
|
|
try testDirnameWindows("c:\\foo\\bar", "c:\\foo");
|
|
try testDirnameWindows("c:\\foo\\bar\\", "c:\\foo");
|
|
try testDirnameWindows("c:\\\\foo\\bar\\baz", "c:\\\\foo\\bar");
|
|
try testDirnameWindows("\\", null);
|
|
try testDirnameWindows("\\foo", "\\");
|
|
try testDirnameWindows("\\foo\\", "\\");
|
|
try testDirnameWindows("\\foo\\bar", "\\foo");
|
|
try testDirnameWindows("\\foo\\bar\\", "\\foo");
|
|
try testDirnameWindows("\\foo\\bar\\baz", "\\foo\\bar");
|
|
try testDirnameWindows("c:", null);
|
|
try testDirnameWindows("c:foo", "c:");
|
|
try testDirnameWindows("c:foo\\", "c:");
|
|
try testDirnameWindows("c:foo\\bar", "c:foo");
|
|
try testDirnameWindows("c:foo\\bar\\", "c:foo");
|
|
try testDirnameWindows("c:foo\\bar\\baz", "c:foo\\bar");
|
|
try testDirnameWindows("file:stream", null);
|
|
try testDirnameWindows("dir\\file:stream", "dir");
|
|
try testDirnameWindows("\\\\unc\\share", null);
|
|
try testDirnameWindows("\\\\unc\\share\\\\", null);
|
|
try testDirnameWindows("\\\\unc\\share\\foo", "\\\\unc\\share\\");
|
|
try testDirnameWindows("\\\\unc\\share\\foo\\", "\\\\unc\\share\\");
|
|
try testDirnameWindows("\\\\unc\\share\\foo\\bar", "\\\\unc\\share\\foo");
|
|
try testDirnameWindows("\\\\unc\\share\\foo\\bar\\", "\\\\unc\\share\\foo");
|
|
try testDirnameWindows("\\\\unc\\share\\foo\\bar\\baz", "\\\\unc\\share\\foo\\bar");
|
|
try testDirnameWindows("\\\\.", null);
|
|
try testDirnameWindows("\\\\.\\", null);
|
|
try testDirnameWindows("\\\\.\\device", "\\\\.\\");
|
|
try testDirnameWindows("\\\\.\\device\\", "\\\\.\\");
|
|
try testDirnameWindows("\\\\.\\device\\foo", "\\\\.\\device");
|
|
try testDirnameWindows("\\\\?", null);
|
|
try testDirnameWindows("\\\\?\\", null);
|
|
try testDirnameWindows("\\\\?\\device", "\\\\?\\");
|
|
try testDirnameWindows("\\\\?\\device\\", "\\\\?\\");
|
|
try testDirnameWindows("\\\\?\\device\\foo", "\\\\?\\device");
|
|
try testDirnameWindows("/a/b/", "/a");
|
|
try testDirnameWindows("/a/b", "/a");
|
|
try testDirnameWindows("/a", "/");
|
|
try testDirnameWindows("", null);
|
|
try testDirnameWindows("/", null);
|
|
try testDirnameWindows("////", null);
|
|
try testDirnameWindows("foo", null);
|
|
}
|
|
|
|
fn testDirnamePosix(input: []const u8, expected_output: ?[]const u8) !void {
|
|
if (dirnamePosix(input)) |output| {
|
|
try testing.expect(mem.eql(u8, output, expected_output.?));
|
|
} else {
|
|
try testing.expect(expected_output == null);
|
|
}
|
|
}
|
|
|
|
fn testDirnameWindows(input: []const u8, expected_output: ?[]const u8) !void {
|
|
if (dirnameWindows(input)) |output| {
|
|
try testing.expectEqualStrings(expected_output.?, output);
|
|
} else {
|
|
try testing.expect(expected_output == null);
|
|
}
|
|
}
|
|
|
|
pub fn basename(path: []const u8) []const u8 {
|
|
if (native_os == .windows) {
|
|
return basenameWindows(path);
|
|
} else {
|
|
return basenamePosix(path);
|
|
}
|
|
}
|
|
|
|
pub fn basenamePosix(path: []const u8) []const u8 {
|
|
return basenameInner(.posix, path);
|
|
}
|
|
|
|
pub fn basenameWindows(path: []const u8) []const u8 {
|
|
return basenameInner(.windows, path);
|
|
}
|
|
|
|
fn basenameInner(comptime path_type: PathType, path: []const u8) []const u8 {
|
|
var it = ComponentIterator(path_type, u8).init(path);
|
|
const last = it.last() orelse return &[_]u8{};
|
|
return last.name;
|
|
}
|
|
|
|
test basename {
|
|
try testBasename("", "");
|
|
try testBasename("/", "");
|
|
try testBasename("/dir/basename.ext", "basename.ext");
|
|
try testBasename("/basename.ext", "basename.ext");
|
|
try testBasename("basename.ext", "basename.ext");
|
|
try testBasename("basename.ext/", "basename.ext");
|
|
try testBasename("basename.ext//", "basename.ext");
|
|
try testBasename("/aaa/bbb", "bbb");
|
|
try testBasename("/aaa/", "aaa");
|
|
try testBasename("/aaa/b", "b");
|
|
try testBasename("/a/b", "b");
|
|
|
|
// For Windows, this is a UNC path that only has a server name component.
|
|
try testBasename("//a", if (native_os == .windows) "" else "a");
|
|
|
|
try testBasenamePosix("\\dir\\basename.ext", "\\dir\\basename.ext");
|
|
try testBasenamePosix("\\basename.ext", "\\basename.ext");
|
|
try testBasenamePosix("basename.ext", "basename.ext");
|
|
try testBasenamePosix("basename.ext\\", "basename.ext\\");
|
|
try testBasenamePosix("basename.ext\\\\", "basename.ext\\\\");
|
|
try testBasenamePosix("foo", "foo");
|
|
|
|
try testBasenameWindows("\\dir\\basename.ext", "basename.ext");
|
|
try testBasenameWindows("\\basename.ext", "basename.ext");
|
|
try testBasenameWindows("basename.ext", "basename.ext");
|
|
try testBasenameWindows("basename.ext\\", "basename.ext");
|
|
try testBasenameWindows("basename.ext\\\\", "basename.ext");
|
|
try testBasenameWindows("foo", "foo");
|
|
try testBasenameWindows("C:", "");
|
|
try testBasenameWindows("C:.", ".");
|
|
try testBasenameWindows("C:\\", "");
|
|
try testBasenameWindows("C:\\dir\\base.ext", "base.ext");
|
|
try testBasenameWindows("C:\\basename.ext", "basename.ext");
|
|
try testBasenameWindows("C:basename.ext", "basename.ext");
|
|
try testBasenameWindows("C:basename.ext\\", "basename.ext");
|
|
try testBasenameWindows("C:basename.ext\\\\", "basename.ext");
|
|
try testBasenameWindows("\\\\.", "");
|
|
try testBasenameWindows("\\\\.\\", "");
|
|
try testBasenameWindows("\\\\.\\basename.ext", "basename.ext");
|
|
try testBasenameWindows("\\\\?", "");
|
|
try testBasenameWindows("\\\\?\\", "");
|
|
try testBasenameWindows("\\\\?\\basename.ext", "basename.ext");
|
|
try testBasenameWindows("C:foo", "foo");
|
|
try testBasenameWindows("file:stream", "file:stream");
|
|
}
|
|
|
|
fn testBasename(input: []const u8, expected_output: []const u8) !void {
|
|
try testing.expectEqualSlices(u8, expected_output, basename(input));
|
|
}
|
|
|
|
fn testBasenamePosix(input: []const u8, expected_output: []const u8) !void {
|
|
try testing.expectEqualSlices(u8, expected_output, basenamePosix(input));
|
|
}
|
|
|
|
fn testBasenameWindows(input: []const u8, expected_output: []const u8) !void {
|
|
try testing.expectEqualSlices(u8, expected_output, basenameWindows(input));
|
|
}
|
|
|
|
/// Returns the non-absolute path from `from` to `to`.
|
|
///
|
|
/// Other than memory allocation, this is a pure function; the result solely
|
|
/// depends on the input parameters.
|
|
///
|
|
/// If `from` and `to` each resolve to the same path (after calling `resolve`
|
|
/// on each), a zero-length string is returned.
|
|
///
|
|
/// See `relativePosix` and `relativeWindows` for operating system specific
|
|
/// details and for how `env_map` is used.
|
|
pub fn relative(
|
|
gpa: Allocator,
|
|
cwd: []const u8,
|
|
env_map: ?*const std.process.Environ.Map,
|
|
from: []const u8,
|
|
to: []const u8,
|
|
) Allocator.Error![]u8 {
|
|
if (native_os == .windows) {
|
|
return relativeWindows(gpa, cwd, env_map, from, to);
|
|
} else {
|
|
return relativePosix(gpa, cwd, from, to);
|
|
}
|
|
}
|
|
|
|
/// Returns the non-absolute path from `from` to `to` according to Windows rules.
|
|
///
|
|
/// Other than memory allocation, this is a pure function; the result solely
|
|
/// depends on the input parameters.
|
|
///
|
|
/// If `from` and `to` each resolve to the same path (after calling `resolve`
|
|
/// on each), a zero-length string is returned.
|
|
///
|
|
/// The result is not guaranteed to be relative, as the paths may be on
|
|
/// different volumes. In that case, the result will be the canonicalized
|
|
/// absolute path of `to`.
|
|
///
|
|
/// Per-drive CWDs are stored in special semi-hidden environment variables of
|
|
/// the format `=<drive-letter>:`, e.g. `=C:`. This type of CWD is purely a
|
|
/// shell concept, so there's no guarantee that it'll be set or that it'll even
|
|
/// be accurate. This is the only reason for the `env_map` parameter. `null` is
|
|
/// treated equivalent to the environment variable missing.
|
|
pub fn relativeWindows(
|
|
gpa: Allocator,
|
|
cwd: []const u8,
|
|
env_map: ?*const std.process.Environ.Map,
|
|
from: []const u8,
|
|
to: []const u8,
|
|
) Allocator.Error![]u8 {
|
|
const parsed_from = parsePathWindows(u8, from);
|
|
const parsed_to = parsePathWindows(u8, to);
|
|
|
|
const result_is_always_to = x: {
|
|
if (parsed_from.kind != parsed_to.kind) {
|
|
break :x false;
|
|
}
|
|
switch (parsed_from.kind) {
|
|
.drive_relative, .drive_absolute => {
|
|
break :x !compareDiskDesignators(u8, .drive, parsed_from.root, parsed_to.root);
|
|
},
|
|
.unc_absolute => {
|
|
break :x !compareDiskDesignators(u8, .unc, parsed_from.root, parsed_to.root);
|
|
},
|
|
.relative, .rooted, .local_device => break :x false,
|
|
.root_local_device => break :x true,
|
|
}
|
|
};
|
|
|
|
if (result_is_always_to) {
|
|
return windowsResolveAgainstCwd(gpa, cwd, env_map, to, parsed_to);
|
|
}
|
|
|
|
const resolved_from = try windowsResolveAgainstCwd(gpa, cwd, env_map, from, parsed_from);
|
|
defer gpa.free(resolved_from);
|
|
var clean_up_resolved_to = true;
|
|
const resolved_to = try windowsResolveAgainstCwd(gpa, cwd, env_map, to, parsed_to);
|
|
defer if (clean_up_resolved_to) gpa.free(resolved_to);
|
|
|
|
const parsed_resolved_from = parsePathWindows(u8, resolved_from);
|
|
const parsed_resolved_to = parsePathWindows(u8, resolved_to);
|
|
|
|
const result_is_to = x: {
|
|
if (parsed_resolved_from.kind != parsed_resolved_to.kind) {
|
|
break :x true;
|
|
}
|
|
switch (parsed_resolved_from.kind) {
|
|
.drive_absolute, .drive_relative => {
|
|
break :x !compareDiskDesignators(u8, .drive, parsed_resolved_from.root, parsed_resolved_to.root);
|
|
},
|
|
.unc_absolute => {
|
|
break :x !compareDiskDesignators(u8, .unc, parsed_resolved_from.root, parsed_resolved_to.root);
|
|
},
|
|
.relative, .rooted, .local_device => break :x false,
|
|
.root_local_device => break :x true,
|
|
}
|
|
};
|
|
|
|
if (result_is_to) {
|
|
clean_up_resolved_to = false;
|
|
return resolved_to;
|
|
}
|
|
|
|
var from_it = mem.tokenizeAny(u8, resolved_from[parsed_resolved_from.root.len..], "/\\");
|
|
var to_it = mem.tokenizeAny(u8, resolved_to[parsed_resolved_to.root.len..], "/\\");
|
|
while (true) {
|
|
const from_component = from_it.next() orelse return gpa.dupe(u8, to_it.rest());
|
|
const to_rest = to_it.rest();
|
|
if (to_it.next()) |to_component| {
|
|
if (eqlIgnoreCaseWtf8(from_component, to_component))
|
|
continue;
|
|
}
|
|
var up_index_end = "..".len;
|
|
while (from_it.next()) |_| {
|
|
up_index_end += "\\..".len;
|
|
}
|
|
const result = try gpa.alloc(u8, up_index_end + @intFromBool(to_rest.len > 0) + to_rest.len);
|
|
errdefer gpa.free(result);
|
|
|
|
result[0..2].* = "..".*;
|
|
var result_index: usize = 2;
|
|
while (result_index < up_index_end) {
|
|
result[result_index..][0..3].* = "\\..".*;
|
|
result_index += 3;
|
|
}
|
|
|
|
var rest_it = mem.tokenizeAny(u8, to_rest, "/\\");
|
|
while (rest_it.next()) |to_component| {
|
|
result[result_index] = '\\';
|
|
result_index += 1;
|
|
@memcpy(result[result_index..][0..to_component.len], to_component);
|
|
result_index += to_component.len;
|
|
}
|
|
|
|
return gpa.realloc(result, result_index);
|
|
}
|
|
return [_]u8{};
|
|
}
|
|
|
|
fn windowsResolveAgainstCwd(
|
|
gpa: Allocator,
|
|
cwd: []const u8,
|
|
env_map: ?*const std.process.Environ.Map,
|
|
path: []const u8,
|
|
parsed: WindowsPath2(u8),
|
|
) ![]u8 {
|
|
// Space for 256 WTF-16 code units; potentially 3 WTF-8 bytes per WTF-16 code unit
|
|
var temp_allocator_state = std.heap.stackFallback(256 * 3, gpa);
|
|
return switch (parsed.kind) {
|
|
.drive_absolute,
|
|
.unc_absolute,
|
|
.root_local_device,
|
|
.local_device,
|
|
=> try resolveWindows(gpa, &.{path}),
|
|
|
|
.relative => try resolveWindows(gpa, &.{ cwd, path }),
|
|
|
|
.rooted => blk: {
|
|
const parsed_cwd = parsePathWindows(u8, cwd);
|
|
switch (parsed_cwd.kind) {
|
|
.drive_absolute => {
|
|
var drive_buf = "_:\\".*;
|
|
drive_buf[0] = cwd[0];
|
|
break :blk try resolveWindows(gpa, &.{ &drive_buf, path });
|
|
},
|
|
.unc_absolute => {
|
|
break :blk try resolveWindows(gpa, &.{ parsed_cwd.root, path });
|
|
},
|
|
// Effectively a malformed CWD, give up and just return a normalized path
|
|
else => break :blk try resolveWindows(gpa, &.{path}),
|
|
}
|
|
},
|
|
.drive_relative => blk: {
|
|
const temp_allocator = temp_allocator_state.get();
|
|
const drive_cwd = drive_cwd: {
|
|
const parsed_cwd = parsePathWindows(u8, cwd);
|
|
|
|
if (parsed_cwd.kind == .drive_absolute) {
|
|
const drive_letter_w = parsed_cwd.root[0];
|
|
const drive_letters_match = drive_letter_w <= 0x7F and
|
|
std.ascii.toUpper(@intCast(drive_letter_w)) == std.ascii.toUpper(parsed.root[0]);
|
|
if (drive_letters_match)
|
|
break :drive_cwd cwd;
|
|
|
|
if (env_map) |m| {
|
|
if (m.get(&.{ '=', parsed.root[0], ':' })) |v| {
|
|
break :drive_cwd try temp_allocator.dupe(u8, v);
|
|
}
|
|
}
|
|
}
|
|
|
|
const drive_buf = try temp_allocator.alloc(u8, 3);
|
|
drive_buf[0] = parsed.root[0];
|
|
drive_buf[1] = ':';
|
|
drive_buf[2] = '\\';
|
|
break :drive_cwd drive_buf;
|
|
};
|
|
defer temp_allocator.free(drive_cwd);
|
|
break :blk try resolveWindows(gpa, &.{ drive_cwd, path });
|
|
},
|
|
};
|
|
}
|
|
|
|
/// Returns the non-absolute path from `from` to `to` according to Windows rules.
|
|
///
|
|
/// Other than memory allocation, this is a pure function; the result solely
|
|
/// depends on the input parameters.
|
|
///
|
|
/// If `from` and `to` each resolve to the same path (after calling `resolve`
|
|
/// on each), a zero-length string is returned.
|
|
///
|
|
pub fn relativePosix(allocator: Allocator, cwd: []const u8, from: []const u8, to: []const u8) Allocator.Error![]u8 {
|
|
const resolved_from = try resolvePosix(allocator, &[_][]const u8{ cwd, from });
|
|
defer allocator.free(resolved_from);
|
|
const resolved_to = try resolvePosix(allocator, &[_][]const u8{ cwd, to });
|
|
defer allocator.free(resolved_to);
|
|
|
|
var from_it = mem.tokenizeScalar(u8, resolved_from, '/');
|
|
var to_it = mem.tokenizeScalar(u8, resolved_to, '/');
|
|
while (true) {
|
|
const from_component = from_it.next() orelse return allocator.dupe(u8, to_it.rest());
|
|
const to_rest = to_it.rest();
|
|
if (to_it.next()) |to_component| {
|
|
if (mem.eql(u8, from_component, to_component))
|
|
continue;
|
|
}
|
|
var up_count: usize = 1;
|
|
while (from_it.next()) |_| {
|
|
up_count += 1;
|
|
}
|
|
const up_index_end = up_count * "../".len;
|
|
const result = try allocator.alloc(u8, up_index_end + to_rest.len);
|
|
errdefer allocator.free(result);
|
|
|
|
var result_index: usize = 0;
|
|
while (result_index < up_index_end) {
|
|
result[result_index..][0..3].* = "../".*;
|
|
result_index += 3;
|
|
}
|
|
if (to_rest.len == 0) {
|
|
// shave off the trailing slash
|
|
return allocator.realloc(result, result_index - 1);
|
|
}
|
|
|
|
@memcpy(result[result_index..][0..to_rest.len], to_rest);
|
|
return result;
|
|
}
|
|
|
|
return [_]u8{};
|
|
}
|
|
|
|
test relative {
|
|
try testRelativeWindows("c:/blah\\blah", "d:/games", "D:\\games");
|
|
try testRelativeWindows("c:/aaaa/bbbb", "c:/aaaa", "..");
|
|
try testRelativeWindows("c:/aaaa/bbbb", "c:/cccc", "..\\..\\cccc");
|
|
try testRelativeWindows("c:/aaaa/bbbb", "C:/aaaa/bbbb", "");
|
|
try testRelativeWindows("c:/aaaa/bbbb", "c:/aaaa/cccc", "..\\cccc");
|
|
try testRelativeWindows("c:/aaaa/", "c:/aaaa/cccc", "cccc");
|
|
try testRelativeWindows("c:/", "c:\\aaaa\\bbbb", "aaaa\\bbbb");
|
|
try testRelativeWindows("c:/aaaa/bbbb", "d:\\", "D:\\");
|
|
try testRelativeWindows("c:/AaAa/bbbb", "c:/aaaa/bbbb", "");
|
|
try testRelativeWindows("c:/aaaaa/", "c:/aaaa/cccc", "..\\aaaa\\cccc");
|
|
try testRelativeWindows("C:\\foo\\bar\\baz\\quux", "C:\\", "..\\..\\..\\..");
|
|
try testRelativeWindows("C:\\foo\\test", "C:\\foo\\test\\bar\\package.json", "bar\\package.json");
|
|
try testRelativeWindows("C:\\foo\\bar\\baz-quux", "C:\\foo\\bar\\baz", "..\\baz");
|
|
try testRelativeWindows("C:\\foo\\bar\\baz", "C:\\foo\\bar\\baz-quux", "..\\baz-quux");
|
|
try testRelativeWindows("\\\\foo\\bar", "\\\\foo\\bar\\baz", "baz");
|
|
try testRelativeWindows("\\\\foo\\bar\\baz", "\\\\foo\\bar", "..");
|
|
try testRelativeWindows("\\\\foo\\bar\\baz-quux", "\\\\foo\\bar\\baz", "..\\baz");
|
|
try testRelativeWindows("\\\\foo/bar\\baz-quux", "//foo\\bar/baz", "..\\baz");
|
|
try testRelativeWindows("\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz-quux", "..\\baz-quux");
|
|
try testRelativeWindows("C:\\baz-quux", "C:\\baz", "..\\baz");
|
|
try testRelativeWindows("C:\\baz", "C:\\baz-quux", "..\\baz-quux");
|
|
try testRelativeWindows("\\\\foo\\baz-quux", "\\\\foo\\baz", "\\\\foo\\baz");
|
|
try testRelativeWindows("\\\\foo\\baz", "\\\\foo\\baz-quux", "\\\\foo\\baz-quux");
|
|
try testRelativeWindows("C:\\baz", "\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz");
|
|
try testRelativeWindows("\\\\foo\\bar\\baz", "C:\\baz", "C:\\baz");
|
|
|
|
try testRelativeWindows("c:blah\\blah", "c:foo", "..\\..\\foo");
|
|
try testRelativeWindows("c:foo", "c:foo\\bar", "bar");
|
|
try testRelativeWindows("\\blah\\blah", "\\foo", "..\\..\\foo");
|
|
try testRelativeWindows("\\foo", "\\foo\\bar", "bar");
|
|
|
|
try testRelativeWindows("a/b/c", "a\\b", "..");
|
|
try testRelativeWindows("a/b/c", "a", "..\\..");
|
|
try testRelativeWindows("a/b/c", "a\\b\\c\\d", "d");
|
|
|
|
try testRelativeWindows("\\\\FOO\\bar\\baz", "\\\\foo\\BAR\\BAZ", "");
|
|
// Unicode-aware case-insensitive path comparison
|
|
try testRelativeWindows("\\\\кириллица\\ελληνικά\\português", "\\\\КИРИЛЛИЦА\\ΕΛΛΗΝΙΚΆ\\PORTUGUÊS", "");
|
|
|
|
try testRelativePosix("/var/lib", "/var", "..");
|
|
try testRelativePosix("/var/lib", "/bin", "../../bin");
|
|
try testRelativePosix("/var/lib", "/var/lib", "");
|
|
try testRelativePosix("/var/lib", "/var/apache", "../apache");
|
|
try testRelativePosix("/var/", "/var/lib", "lib");
|
|
try testRelativePosix("/", "/var/lib", "var/lib");
|
|
try testRelativePosix("/foo/test", "/foo/test/bar/package.json", "bar/package.json");
|
|
try testRelativePosix("/Users/a/web/b/test/mails", "/Users/a/web/b", "../..");
|
|
try testRelativePosix("/foo/bar/baz-quux", "/foo/bar/baz", "../baz");
|
|
try testRelativePosix("/foo/bar/baz", "/foo/bar/baz-quux", "../baz-quux");
|
|
try testRelativePosix("/baz-quux", "/baz", "../baz");
|
|
try testRelativePosix("/baz", "/baz-quux", "../baz-quux");
|
|
}
|
|
|
|
fn testRelativePosix(from: []const u8, to: []const u8, expected_output: []const u8) !void {
|
|
const result = try relativePosix(testing.allocator, ".", from, to);
|
|
defer testing.allocator.free(result);
|
|
try testing.expectEqualStrings(expected_output, result);
|
|
}
|
|
|
|
fn testRelativeWindows(from: []const u8, to: []const u8, expected_output: []const u8) !void {
|
|
const result = try relativeWindows(testing.allocator, ".", null, from, to);
|
|
defer testing.allocator.free(result);
|
|
try testing.expectEqualStrings(expected_output, result);
|
|
}
|
|
|
|
/// Searches for a file extension separated by a `.` and returns the string after that `.`.
|
|
/// Files that end or start with `.` and have no other `.` in their name
|
|
/// are considered to have no extension, in which case this returns "".
|
|
/// Examples:
|
|
/// - `"main.zig"` ⇒ `".zig"`
|
|
/// - `"src/main.zig"` ⇒ `".zig"`
|
|
/// - `".gitignore"` ⇒ `""`
|
|
/// - `".image.png"` ⇒ `".png"`
|
|
/// - `"keep."` ⇒ `"."`
|
|
/// - `"src.keep.me"` ⇒ `".me"`
|
|
/// - `"/src/keep.me"` ⇒ `".me"`
|
|
/// - `"/src/keep.me/"` ⇒ `".me"`
|
|
/// The returned slice is guaranteed to have its pointer within the start and end
|
|
/// pointer address range of `path`, even if it is length zero.
|
|
pub fn extension(path: []const u8) []const u8 {
|
|
const filename = basename(path);
|
|
const index = mem.lastIndexOfScalar(u8, filename, '.') orelse return path[path.len..];
|
|
if (index == 0) return path[path.len..];
|
|
return filename[index..];
|
|
}
|
|
|
|
fn testExtension(path: []const u8, expected: []const u8) !void {
|
|
try testing.expectEqualStrings(expected, extension(path));
|
|
}
|
|
|
|
test extension {
|
|
try testExtension("", "");
|
|
try testExtension(".", "");
|
|
try testExtension("a.", ".");
|
|
try testExtension("abc.", ".");
|
|
try testExtension(".a", "");
|
|
try testExtension(".file", "");
|
|
try testExtension(".gitignore", "");
|
|
try testExtension(".image.png", ".png");
|
|
try testExtension("file.ext", ".ext");
|
|
try testExtension("file.ext.", ".");
|
|
try testExtension("very-long-file.bruh", ".bruh");
|
|
try testExtension("a.b.c", ".c");
|
|
try testExtension("a.b.c/", ".c");
|
|
|
|
try testExtension("/", "");
|
|
try testExtension("/.", "");
|
|
try testExtension("/a.", ".");
|
|
try testExtension("/abc.", ".");
|
|
try testExtension("/.a", "");
|
|
try testExtension("/.file", "");
|
|
try testExtension("/.gitignore", "");
|
|
try testExtension("/file.ext", ".ext");
|
|
try testExtension("/file.ext.", ".");
|
|
try testExtension("/very-long-file.bruh", ".bruh");
|
|
try testExtension("/a.b.c", ".c");
|
|
try testExtension("/a.b.c/", ".c");
|
|
|
|
try testExtension("/foo/bar/bam/", "");
|
|
try testExtension("/foo/bar/bam/.", "");
|
|
try testExtension("/foo/bar/bam/a.", ".");
|
|
try testExtension("/foo/bar/bam/abc.", ".");
|
|
try testExtension("/foo/bar/bam/.a", "");
|
|
try testExtension("/foo/bar/bam/.file", "");
|
|
try testExtension("/foo/bar/bam/.gitignore", "");
|
|
try testExtension("/foo/bar/bam/file.ext", ".ext");
|
|
try testExtension("/foo/bar/bam/file.ext.", ".");
|
|
try testExtension("/foo/bar/bam/very-long-file.bruh", ".bruh");
|
|
try testExtension("/foo/bar/bam/a.b.c", ".c");
|
|
try testExtension("/foo/bar/bam/a.b.c/", ".c");
|
|
}
|
|
|
|
/// Returns the last component of this path without its extension (if any):
|
|
/// - "hello/world/lib.tar.gz" ⇒ "lib.tar"
|
|
/// - "hello/world/lib.tar" ⇒ "lib"
|
|
/// - "hello/world/lib" ⇒ "lib"
|
|
pub fn stem(path: []const u8) []const u8 {
|
|
const filename = basename(path);
|
|
const index = mem.lastIndexOfScalar(u8, filename, '.') orelse return filename[0..];
|
|
if (index == 0) return path;
|
|
return filename[0..index];
|
|
}
|
|
|
|
fn testStem(path: []const u8, expected: []const u8) !void {
|
|
try testing.expectEqualStrings(expected, stem(path));
|
|
}
|
|
|
|
test stem {
|
|
try testStem("hello/world/lib.tar.gz", "lib.tar");
|
|
try testStem("hello/world/lib.tar", "lib");
|
|
try testStem("hello/world/lib", "lib");
|
|
try testStem("hello/lib/", "lib");
|
|
try testStem("hello...", "hello..");
|
|
try testStem("hello.", "hello");
|
|
try testStem("/hello.", "hello");
|
|
try testStem(".gitignore", ".gitignore");
|
|
try testStem(".image.png", ".image");
|
|
try testStem("file.ext", "file");
|
|
try testStem("file.ext.", "file.ext");
|
|
try testStem("a.b.c", "a.b");
|
|
try testStem("a.b.c/", "a.b");
|
|
try testStem(".a", ".a");
|
|
try testStem("///", "");
|
|
try testStem("..", ".");
|
|
try testStem(".", ".");
|
|
try testStem(" ", " ");
|
|
try testStem("", "");
|
|
}
|
|
|
|
/// A path component iterator that can move forwards and backwards.
|
|
/// The 'root' of the path (`/` for POSIX, things like `C:\`, `\\server\share\`, etc
|
|
/// for Windows) is treated specially and will never be returned by any of the
|
|
/// `first`, `last`, `next`, or `previous` functions.
|
|
/// Multiple consecutive path separators are skipped (treated as a single separator)
|
|
/// when iterating.
|
|
/// All returned component names/paths are slices of the original path.
|
|
/// There is no normalization of paths performed while iterating.
|
|
pub fn ComponentIterator(comptime path_type: PathType, comptime T: type) type {
|
|
return struct {
|
|
path: []const T,
|
|
/// Length of the root with at most one trailing path separator included (e.g. `C:/`).
|
|
root_len: usize,
|
|
/// Length of the root with all trailing path separators included (e.g. `C://///`).
|
|
root_end_index: usize,
|
|
start_index: usize = 0,
|
|
end_index: usize = 0,
|
|
|
|
const Self = @This();
|
|
|
|
pub const Component = struct {
|
|
/// The current component's path name, e.g. 'b'.
|
|
/// This will never contain path separators.
|
|
name: []const T,
|
|
/// The full path up to and including the current component, e.g. '/a/b'
|
|
/// This will never contain trailing path separators.
|
|
path: []const T,
|
|
};
|
|
|
|
/// After `init`, `next` will return the first component after the root
|
|
/// (there is no need to call `first` after `init`).
|
|
/// To iterate backwards (from the end of the path to the beginning), call `last`
|
|
/// after `init` and then iterate via `previous` calls.
|
|
/// For Windows paths, paths are assumed to be in the Win32 namespace.
|
|
pub fn init(path: []const T) Self {
|
|
const root_len: usize = switch (path_type) {
|
|
.posix, .uefi => posix: {
|
|
// Root on UEFI and POSIX only differs by the path separator
|
|
break :posix if (path.len > 0 and path_type.isSep(T, path[0])) 1 else 0;
|
|
},
|
|
.windows => windows: {
|
|
break :windows parsePathWindows(T, path).root.len;
|
|
},
|
|
};
|
|
// If there are repeated path separators directly after the root,
|
|
// keep track of that info so that they don't have to be dealt with when
|
|
// iterating components.
|
|
var root_end_index = root_len;
|
|
for (path[root_len..]) |c| {
|
|
if (!path_type.isSep(T, c)) break;
|
|
root_end_index += 1;
|
|
}
|
|
return .{
|
|
.path = path,
|
|
.root_len = root_len,
|
|
.root_end_index = root_end_index,
|
|
.start_index = root_end_index,
|
|
.end_index = root_end_index,
|
|
};
|
|
}
|
|
|
|
/// Returns the root of the path if it is not a relative path, or null otherwise.
|
|
/// For POSIX paths, this will be `/`.
|
|
/// For Windows paths, this will be something like `C:\`, `\\server\share\`, etc.
|
|
/// For UEFI paths, this will be `\`.
|
|
pub fn root(self: Self) ?[]const T {
|
|
if (self.root_end_index == 0) return null;
|
|
return self.path[0..self.root_len];
|
|
}
|
|
|
|
/// Returns the first component (from the beginning of the path).
|
|
/// For example, if the path is `/a/b/c` then this will return the `a` component.
|
|
/// After calling `first`, `previous` will always return `null`, and `next` will return
|
|
/// the component to the right of the one returned by `first`, if any exist.
|
|
pub fn first(self: *Self) ?Component {
|
|
self.start_index = self.root_end_index;
|
|
self.end_index = self.start_index;
|
|
while (self.end_index < self.path.len and !path_type.isSep(T, self.path[self.end_index])) {
|
|
self.end_index += 1;
|
|
}
|
|
if (self.end_index == self.start_index) return null;
|
|
return .{
|
|
.name = self.path[self.start_index..self.end_index],
|
|
.path = self.path[0..self.end_index],
|
|
};
|
|
}
|
|
|
|
/// Returns the last component (from the end of the path).
|
|
/// For example, if the path is `/a/b/c` then this will return the `c` component.
|
|
/// After calling `last`, `next` will always return `null`, and `previous` will return
|
|
/// the component to the left of the one returned by `last`, if any exist.
|
|
pub fn last(self: *Self) ?Component {
|
|
self.end_index = self.path.len;
|
|
while (true) {
|
|
if (self.end_index == self.root_end_index) {
|
|
self.start_index = self.end_index;
|
|
return null;
|
|
}
|
|
if (!path_type.isSep(T, self.path[self.end_index - 1])) break;
|
|
self.end_index -= 1;
|
|
}
|
|
self.start_index = self.end_index;
|
|
while (true) {
|
|
if (self.start_index == self.root_end_index) break;
|
|
if (path_type.isSep(T, self.path[self.start_index - 1])) break;
|
|
self.start_index -= 1;
|
|
}
|
|
if (self.start_index == self.end_index) return null;
|
|
return .{
|
|
.name = self.path[self.start_index..self.end_index],
|
|
.path = self.path[0..self.end_index],
|
|
};
|
|
}
|
|
|
|
/// Returns the next component (the component to the right of the most recently
|
|
/// returned component), or null if no such component exists.
|
|
/// For example, if the path is `/a/b/c` and the most recently returned component
|
|
/// is `b`, then this will return the `c` component.
|
|
pub fn next(self: *Self) ?Component {
|
|
const peek_result = self.peekNext() orelse return null;
|
|
self.start_index = peek_result.path.len - peek_result.name.len;
|
|
self.end_index = peek_result.path.len;
|
|
return peek_result;
|
|
}
|
|
|
|
/// Like `next`, but does not modify the iterator state.
|
|
pub fn peekNext(self: Self) ?Component {
|
|
var start_index = self.end_index;
|
|
while (start_index < self.path.len and path_type.isSep(T, self.path[start_index])) {
|
|
start_index += 1;
|
|
}
|
|
var end_index = start_index;
|
|
while (end_index < self.path.len and !path_type.isSep(T, self.path[end_index])) {
|
|
end_index += 1;
|
|
}
|
|
if (start_index == end_index) return null;
|
|
return .{
|
|
.name = self.path[start_index..end_index],
|
|
.path = self.path[0..end_index],
|
|
};
|
|
}
|
|
|
|
/// Returns the previous component (the component to the left of the most recently
|
|
/// returned component), or null if no such component exists.
|
|
/// For example, if the path is `/a/b/c` and the most recently returned component
|
|
/// is `b`, then this will return the `a` component.
|
|
pub fn previous(self: *Self) ?Component {
|
|
const peek_result = self.peekPrevious() orelse return null;
|
|
self.start_index = peek_result.path.len - peek_result.name.len;
|
|
self.end_index = peek_result.path.len;
|
|
return peek_result;
|
|
}
|
|
|
|
/// Like `previous`, but does not modify the iterator state.
|
|
pub fn peekPrevious(self: Self) ?Component {
|
|
var end_index = self.start_index;
|
|
while (true) {
|
|
if (end_index == self.root_end_index) return null;
|
|
if (!path_type.isSep(T, self.path[end_index - 1])) break;
|
|
end_index -= 1;
|
|
}
|
|
var start_index = end_index;
|
|
while (true) {
|
|
if (start_index == self.root_end_index) break;
|
|
if (path_type.isSep(T, self.path[start_index - 1])) break;
|
|
start_index -= 1;
|
|
}
|
|
if (start_index == end_index) return null;
|
|
return .{
|
|
.name = self.path[start_index..end_index],
|
|
.path = self.path[0..end_index],
|
|
};
|
|
}
|
|
};
|
|
}
|
|
|
|
pub const NativeComponentIterator = ComponentIterator(switch (native_os) {
|
|
.windows => .windows,
|
|
.uefi => .uefi,
|
|
else => .posix,
|
|
}, u8);
|
|
|
|
pub fn componentIterator(path: []const u8) NativeComponentIterator {
|
|
return NativeComponentIterator.init(path);
|
|
}
|
|
|
|
test "ComponentIterator posix" {
|
|
const PosixComponentIterator = ComponentIterator(.posix, u8);
|
|
{
|
|
const path = "a/b/c/";
|
|
var it = PosixComponentIterator.init(path);
|
|
try std.testing.expectEqual(0, it.root_len);
|
|
try std.testing.expectEqual(0, it.root_end_index);
|
|
try std.testing.expect(null == it.root());
|
|
{
|
|
try std.testing.expect(null == it.previous());
|
|
|
|
const first_via_next = it.next().?;
|
|
try std.testing.expectEqualStrings("a", first_via_next.name);
|
|
try std.testing.expectEqualStrings("a", first_via_next.path);
|
|
|
|
const first = it.first().?;
|
|
try std.testing.expectEqualStrings("a", first.name);
|
|
try std.testing.expectEqualStrings("a", first.path);
|
|
|
|
try std.testing.expect(null == it.previous());
|
|
|
|
const second = it.next().?;
|
|
try std.testing.expectEqualStrings("b", second.name);
|
|
try std.testing.expectEqualStrings("a/b", second.path);
|
|
|
|
const third = it.next().?;
|
|
try std.testing.expectEqualStrings("c", third.name);
|
|
try std.testing.expectEqualStrings("a/b/c", third.path);
|
|
|
|
try std.testing.expect(null == it.next());
|
|
}
|
|
{
|
|
const last = it.last().?;
|
|
try std.testing.expectEqualStrings("c", last.name);
|
|
try std.testing.expectEqualStrings("a/b/c", last.path);
|
|
|
|
try std.testing.expect(null == it.next());
|
|
|
|
const second_to_last = it.previous().?;
|
|
try std.testing.expectEqualStrings("b", second_to_last.name);
|
|
try std.testing.expectEqualStrings("a/b", second_to_last.path);
|
|
|
|
const third_to_last = it.previous().?;
|
|
try std.testing.expectEqualStrings("a", third_to_last.name);
|
|
try std.testing.expectEqualStrings("a", third_to_last.path);
|
|
|
|
try std.testing.expect(null == it.previous());
|
|
}
|
|
}
|
|
|
|
{
|
|
const path = "/a/b/c/";
|
|
var it = PosixComponentIterator.init(path);
|
|
try std.testing.expectEqual(1, it.root_len);
|
|
try std.testing.expectEqual(1, it.root_end_index);
|
|
try std.testing.expectEqualStrings("/", it.root().?);
|
|
{
|
|
try std.testing.expect(null == it.previous());
|
|
|
|
const first_via_next = it.next().?;
|
|
try std.testing.expectEqualStrings("a", first_via_next.name);
|
|
try std.testing.expectEqualStrings("/a", first_via_next.path);
|
|
|
|
const first = it.first().?;
|
|
try std.testing.expectEqualStrings("a", first.name);
|
|
try std.testing.expectEqualStrings("/a", first.path);
|
|
|
|
try std.testing.expect(null == it.previous());
|
|
|
|
const second = it.next().?;
|
|
try std.testing.expectEqualStrings("b", second.name);
|
|
try std.testing.expectEqualStrings("/a/b", second.path);
|
|
|
|
const third = it.next().?;
|
|
try std.testing.expectEqualStrings("c", third.name);
|
|
try std.testing.expectEqualStrings("/a/b/c", third.path);
|
|
|
|
try std.testing.expect(null == it.next());
|
|
}
|
|
{
|
|
const last = it.last().?;
|
|
try std.testing.expectEqualStrings("c", last.name);
|
|
try std.testing.expectEqualStrings("/a/b/c", last.path);
|
|
|
|
try std.testing.expect(null == it.next());
|
|
|
|
const second_to_last = it.previous().?;
|
|
try std.testing.expectEqualStrings("b", second_to_last.name);
|
|
try std.testing.expectEqualStrings("/a/b", second_to_last.path);
|
|
|
|
const third_to_last = it.previous().?;
|
|
try std.testing.expectEqualStrings("a", third_to_last.name);
|
|
try std.testing.expectEqualStrings("/a", third_to_last.path);
|
|
|
|
try std.testing.expect(null == it.previous());
|
|
}
|
|
}
|
|
|
|
{
|
|
const path = "////a///b///c////";
|
|
var it = PosixComponentIterator.init(path);
|
|
try std.testing.expectEqual(1, it.root_len);
|
|
try std.testing.expectEqual(4, it.root_end_index);
|
|
try std.testing.expectEqualStrings("/", it.root().?);
|
|
{
|
|
try std.testing.expect(null == it.previous());
|
|
|
|
const first_via_next = it.next().?;
|
|
try std.testing.expectEqualStrings("a", first_via_next.name);
|
|
try std.testing.expectEqualStrings("////a", first_via_next.path);
|
|
|
|
const first = it.first().?;
|
|
try std.testing.expectEqualStrings("a", first.name);
|
|
try std.testing.expectEqualStrings("////a", first.path);
|
|
|
|
try std.testing.expect(null == it.previous());
|
|
|
|
const second = it.next().?;
|
|
try std.testing.expectEqualStrings("b", second.name);
|
|
try std.testing.expectEqualStrings("////a///b", second.path);
|
|
|
|
const third = it.next().?;
|
|
try std.testing.expectEqualStrings("c", third.name);
|
|
try std.testing.expectEqualStrings("////a///b///c", third.path);
|
|
|
|
try std.testing.expect(null == it.next());
|
|
}
|
|
{
|
|
const last = it.last().?;
|
|
try std.testing.expectEqualStrings("c", last.name);
|
|
try std.testing.expectEqualStrings("////a///b///c", last.path);
|
|
|
|
try std.testing.expect(null == it.next());
|
|
|
|
const second_to_last = it.previous().?;
|
|
try std.testing.expectEqualStrings("b", second_to_last.name);
|
|
try std.testing.expectEqualStrings("////a///b", second_to_last.path);
|
|
|
|
const third_to_last = it.previous().?;
|
|
try std.testing.expectEqualStrings("a", third_to_last.name);
|
|
try std.testing.expectEqualStrings("////a", third_to_last.path);
|
|
|
|
try std.testing.expect(null == it.previous());
|
|
}
|
|
}
|
|
|
|
{
|
|
const path = "/";
|
|
var it = PosixComponentIterator.init(path);
|
|
try std.testing.expectEqual(1, it.root_len);
|
|
try std.testing.expectEqual(1, it.root_end_index);
|
|
try std.testing.expectEqualStrings("/", it.root().?);
|
|
|
|
try std.testing.expect(null == it.first());
|
|
try std.testing.expect(null == it.previous());
|
|
try std.testing.expect(null == it.first());
|
|
try std.testing.expect(null == it.next());
|
|
|
|
try std.testing.expect(null == it.last());
|
|
try std.testing.expect(null == it.previous());
|
|
try std.testing.expect(null == it.last());
|
|
try std.testing.expect(null == it.next());
|
|
}
|
|
|
|
{
|
|
const path = "";
|
|
var it = PosixComponentIterator.init(path);
|
|
try std.testing.expectEqual(0, it.root_len);
|
|
try std.testing.expectEqual(0, it.root_end_index);
|
|
try std.testing.expect(null == it.root());
|
|
|
|
try std.testing.expect(null == it.first());
|
|
try std.testing.expect(null == it.previous());
|
|
try std.testing.expect(null == it.first());
|
|
try std.testing.expect(null == it.next());
|
|
|
|
try std.testing.expect(null == it.last());
|
|
try std.testing.expect(null == it.previous());
|
|
try std.testing.expect(null == it.last());
|
|
try std.testing.expect(null == it.next());
|
|
}
|
|
}
|
|
|
|
test "ComponentIterator windows" {
|
|
const WindowsComponentIterator = ComponentIterator(.windows, u8);
|
|
{
|
|
const path = "a/b\\c//";
|
|
var it = WindowsComponentIterator.init(path);
|
|
try std.testing.expectEqual(0, it.root_len);
|
|
try std.testing.expectEqual(0, it.root_end_index);
|
|
try std.testing.expect(null == it.root());
|
|
{
|
|
try std.testing.expect(null == it.previous());
|
|
|
|
const first_via_next = it.next().?;
|
|
try std.testing.expectEqualStrings("a", first_via_next.name);
|
|
try std.testing.expectEqualStrings("a", first_via_next.path);
|
|
|
|
const first = it.first().?;
|
|
try std.testing.expectEqualStrings("a", first.name);
|
|
try std.testing.expectEqualStrings("a", first.path);
|
|
|
|
try std.testing.expect(null == it.previous());
|
|
|
|
const second = it.next().?;
|
|
try std.testing.expectEqualStrings("b", second.name);
|
|
try std.testing.expectEqualStrings("a/b", second.path);
|
|
|
|
const third = it.next().?;
|
|
try std.testing.expectEqualStrings("c", third.name);
|
|
try std.testing.expectEqualStrings("a/b\\c", third.path);
|
|
|
|
try std.testing.expect(null == it.next());
|
|
}
|
|
{
|
|
const last = it.last().?;
|
|
try std.testing.expectEqualStrings("c", last.name);
|
|
try std.testing.expectEqualStrings("a/b\\c", last.path);
|
|
|
|
try std.testing.expect(null == it.next());
|
|
|
|
const second_to_last = it.previous().?;
|
|
try std.testing.expectEqualStrings("b", second_to_last.name);
|
|
try std.testing.expectEqualStrings("a/b", second_to_last.path);
|
|
|
|
const third_to_last = it.previous().?;
|
|
try std.testing.expectEqualStrings("a", third_to_last.name);
|
|
try std.testing.expectEqualStrings("a", third_to_last.path);
|
|
|
|
try std.testing.expect(null == it.previous());
|
|
}
|
|
}
|
|
|
|
{
|
|
const path = "C:\\a/b/c/";
|
|
var it = WindowsComponentIterator.init(path);
|
|
try std.testing.expectEqual(3, it.root_len);
|
|
try std.testing.expectEqual(3, it.root_end_index);
|
|
try std.testing.expectEqualStrings("C:\\", it.root().?);
|
|
{
|
|
const first = it.first().?;
|
|
try std.testing.expectEqualStrings("a", first.name);
|
|
try std.testing.expectEqualStrings("C:\\a", first.path);
|
|
|
|
const second = it.next().?;
|
|
try std.testing.expectEqualStrings("b", second.name);
|
|
try std.testing.expectEqualStrings("C:\\a/b", second.path);
|
|
|
|
const third = it.next().?;
|
|
try std.testing.expectEqualStrings("c", third.name);
|
|
try std.testing.expectEqualStrings("C:\\a/b/c", third.path);
|
|
|
|
try std.testing.expect(null == it.next());
|
|
}
|
|
{
|
|
const last = it.last().?;
|
|
try std.testing.expectEqualStrings("c", last.name);
|
|
try std.testing.expectEqualStrings("C:\\a/b/c", last.path);
|
|
|
|
const second_to_last = it.previous().?;
|
|
try std.testing.expectEqualStrings("b", second_to_last.name);
|
|
try std.testing.expectEqualStrings("C:\\a/b", second_to_last.path);
|
|
|
|
const third_to_last = it.previous().?;
|
|
try std.testing.expectEqualStrings("a", third_to_last.name);
|
|
try std.testing.expectEqualStrings("C:\\a", third_to_last.path);
|
|
|
|
try std.testing.expect(null == it.previous());
|
|
}
|
|
}
|
|
|
|
{
|
|
const path = "C:\\\\//a/\\/\\b///c////";
|
|
var it = WindowsComponentIterator.init(path);
|
|
try std.testing.expectEqual(3, it.root_len);
|
|
try std.testing.expectEqual(6, it.root_end_index);
|
|
try std.testing.expectEqualStrings("C:\\", it.root().?);
|
|
{
|
|
const first = it.first().?;
|
|
try std.testing.expectEqualStrings("a", first.name);
|
|
try std.testing.expectEqualStrings("C:\\\\//a", first.path);
|
|
|
|
const second = it.next().?;
|
|
try std.testing.expectEqualStrings("b", second.name);
|
|
try std.testing.expectEqualStrings("C:\\\\//a/\\/\\b", second.path);
|
|
|
|
const third = it.next().?;
|
|
try std.testing.expectEqualStrings("c", third.name);
|
|
try std.testing.expectEqualStrings("C:\\\\//a/\\/\\b///c", third.path);
|
|
|
|
try std.testing.expect(null == it.next());
|
|
}
|
|
{
|
|
const last = it.last().?;
|
|
try std.testing.expectEqualStrings("c", last.name);
|
|
try std.testing.expectEqualStrings("C:\\\\//a/\\/\\b///c", last.path);
|
|
|
|
const second_to_last = it.previous().?;
|
|
try std.testing.expectEqualStrings("b", second_to_last.name);
|
|
try std.testing.expectEqualStrings("C:\\\\//a/\\/\\b", second_to_last.path);
|
|
|
|
const third_to_last = it.previous().?;
|
|
try std.testing.expectEqualStrings("a", third_to_last.name);
|
|
try std.testing.expectEqualStrings("C:\\\\//a", third_to_last.path);
|
|
|
|
try std.testing.expect(null == it.previous());
|
|
}
|
|
}
|
|
|
|
{
|
|
const path = "/";
|
|
var it = WindowsComponentIterator.init(path);
|
|
try std.testing.expectEqual(1, it.root_len);
|
|
try std.testing.expectEqual(1, it.root_end_index);
|
|
try std.testing.expectEqualStrings("/", it.root().?);
|
|
|
|
try std.testing.expect(null == it.first());
|
|
try std.testing.expect(null == it.previous());
|
|
try std.testing.expect(null == it.first());
|
|
try std.testing.expect(null == it.next());
|
|
|
|
try std.testing.expect(null == it.last());
|
|
try std.testing.expect(null == it.previous());
|
|
try std.testing.expect(null == it.last());
|
|
try std.testing.expect(null == it.next());
|
|
}
|
|
|
|
{
|
|
const path = "";
|
|
var it = WindowsComponentIterator.init(path);
|
|
try std.testing.expectEqual(0, it.root_len);
|
|
try std.testing.expectEqual(0, it.root_end_index);
|
|
try std.testing.expect(null == it.root());
|
|
|
|
try std.testing.expect(null == it.first());
|
|
try std.testing.expect(null == it.previous());
|
|
try std.testing.expect(null == it.first());
|
|
try std.testing.expect(null == it.next());
|
|
|
|
try std.testing.expect(null == it.last());
|
|
try std.testing.expect(null == it.previous());
|
|
try std.testing.expect(null == it.last());
|
|
try std.testing.expect(null == it.next());
|
|
}
|
|
}
|
|
|
|
test "ComponentIterator windows WTF-16" {
|
|
const WindowsComponentIterator = ComponentIterator(.windows, u16);
|
|
const L = std.unicode.utf8ToUtf16LeStringLiteral;
|
|
|
|
const path = L("C:\\a/b/c/");
|
|
var it = WindowsComponentIterator.init(path);
|
|
try std.testing.expectEqual(3, it.root_len);
|
|
try std.testing.expectEqual(3, it.root_end_index);
|
|
try std.testing.expectEqualSlices(u16, L("C:\\"), it.root().?);
|
|
{
|
|
const first = it.first().?;
|
|
try std.testing.expectEqualSlices(u16, L("a"), first.name);
|
|
try std.testing.expectEqualSlices(u16, L("C:\\a"), first.path);
|
|
|
|
const second = it.next().?;
|
|
try std.testing.expectEqualSlices(u16, L("b"), second.name);
|
|
try std.testing.expectEqualSlices(u16, L("C:\\a/b"), second.path);
|
|
|
|
const third = it.next().?;
|
|
try std.testing.expectEqualSlices(u16, L("c"), third.name);
|
|
try std.testing.expectEqualSlices(u16, L("C:\\a/b/c"), third.path);
|
|
|
|
try std.testing.expect(null == it.next());
|
|
}
|
|
{
|
|
const last = it.last().?;
|
|
try std.testing.expectEqualSlices(u16, L("c"), last.name);
|
|
try std.testing.expectEqualSlices(u16, L("C:\\a/b/c"), last.path);
|
|
|
|
const second_to_last = it.previous().?;
|
|
try std.testing.expectEqualSlices(u16, L("b"), second_to_last.name);
|
|
try std.testing.expectEqualSlices(u16, L("C:\\a/b"), second_to_last.path);
|
|
|
|
const third_to_last = it.previous().?;
|
|
try std.testing.expectEqualSlices(u16, L("a"), third_to_last.name);
|
|
try std.testing.expectEqualSlices(u16, L("C:\\a"), third_to_last.path);
|
|
|
|
try std.testing.expect(null == it.previous());
|
|
}
|
|
}
|
|
|
|
test "ComponentIterator roots" {
|
|
// UEFI
|
|
{
|
|
var it = ComponentIterator(.uefi, u8).init("\\\\a");
|
|
try std.testing.expectEqualStrings("\\", it.root().?);
|
|
|
|
it = ComponentIterator(.uefi, u8).init("//a");
|
|
try std.testing.expect(null == it.root());
|
|
}
|
|
// POSIX
|
|
{
|
|
var it = ComponentIterator(.posix, u8).init("//a");
|
|
try std.testing.expectEqualStrings("/", it.root().?);
|
|
|
|
it = ComponentIterator(.posix, u8).init("\\\\a");
|
|
try std.testing.expect(null == it.root());
|
|
}
|
|
// Windows
|
|
{
|
|
// Drive relative
|
|
var it = ComponentIterator(.windows, u8).init("C:a");
|
|
try std.testing.expectEqualStrings("C:", it.root().?);
|
|
|
|
// Drive absolute
|
|
it = ComponentIterator(.windows, u8).init("C:/a");
|
|
try std.testing.expectEqualStrings("C:/", it.root().?);
|
|
it = ComponentIterator(.windows, u8).init("C:\\a");
|
|
try std.testing.expectEqualStrings("C:\\", it.root().?);
|
|
it = ComponentIterator(.windows, u8).init("C:///a");
|
|
try std.testing.expectEqualStrings("C:/", it.root().?);
|
|
|
|
// Rooted
|
|
it = ComponentIterator(.windows, u8).init("\\a");
|
|
try std.testing.expectEqualStrings("\\", it.root().?);
|
|
it = ComponentIterator(.windows, u8).init("/a");
|
|
try std.testing.expectEqualStrings("/", it.root().?);
|
|
|
|
// Root local device
|
|
it = ComponentIterator(.windows, u8).init("\\\\.");
|
|
try std.testing.expectEqualStrings("\\\\.", it.root().?);
|
|
it = ComponentIterator(.windows, u8).init("//?");
|
|
try std.testing.expectEqualStrings("//?", it.root().?);
|
|
|
|
// UNC absolute
|
|
it = ComponentIterator(.windows, u8).init("//");
|
|
try std.testing.expectEqualStrings("//", it.root().?);
|
|
it = ComponentIterator(.windows, u8).init("\\\\a");
|
|
try std.testing.expectEqualStrings("\\\\a", it.root().?);
|
|
it = ComponentIterator(.windows, u8).init("\\\\a\\b\\\\c");
|
|
try std.testing.expectEqualStrings("\\\\a\\b\\", it.root().?);
|
|
it = ComponentIterator(.windows, u8).init("//a");
|
|
try std.testing.expectEqualStrings("//a", it.root().?);
|
|
it = ComponentIterator(.windows, u8).init("//a/b//c");
|
|
try std.testing.expectEqualStrings("//a/b/", it.root().?);
|
|
// Malformed UNC path with empty server name
|
|
it = ComponentIterator(.windows, u8).init("\\\\\\a\\b\\c");
|
|
try std.testing.expectEqualStrings("\\\\\\a\\", it.root().?);
|
|
}
|
|
}
|
|
|
|
/// Format a path encoded as bytes for display as UTF-8.
|
|
/// Returns a Formatter for the given path. The path will be converted to valid UTF-8
|
|
/// during formatting. This is a lossy conversion if the path contains any ill-formed UTF-8.
|
|
/// Ill-formed UTF-8 byte sequences are replaced by the replacement character (U+FFFD)
|
|
/// according to "U+FFFD Substitution of Maximal Subparts" from Chapter 3 of
|
|
/// the Unicode standard, and as specified by https://encoding.spec.whatwg.org/#utf-8-decoder
|
|
pub const fmtAsUtf8Lossy = std.unicode.fmtUtf8;
|
|
|
|
/// Format a path encoded as WTF-16 LE for display as UTF-8.
|
|
/// Return a Formatter for a (potentially ill-formed) UTF-16 LE path.
|
|
/// The path will be converted to valid UTF-8 during formatting. This is
|
|
/// a lossy conversion if the path contains any unpaired surrogates.
|
|
/// Unpaired surrogates are replaced by the replacement character (U+FFFD).
|
|
pub const fmtWtf16LeAsUtf8Lossy = std.unicode.fmtUtf16Le;
|
|
|
|
/// Similar to `RTL_PATH_TYPE`, but without the `UNKNOWN` path type.
|
|
pub const Win32PathType = enum {
|
|
/// `\\server\share\foo`
|
|
unc_absolute,
|
|
/// `C:\foo`
|
|
drive_absolute,
|
|
/// `C:foo`
|
|
drive_relative,
|
|
/// `\foo`
|
|
rooted,
|
|
/// `foo`
|
|
relative,
|
|
/// `\\.\foo`, `\\?\foo`
|
|
local_device,
|
|
/// `\\.`, `\\?`
|
|
root_local_device,
|
|
};
|
|
|
|
/// Get the path type of a Win32 namespace path.
|
|
/// Similar to `RtlDetermineDosPathNameType_U`.
|
|
/// If `T` is `u16`, then `path` should be encoded as WTF-16LE.
|
|
pub fn getWin32PathType(comptime T: type, path: []const T) Win32PathType {
|
|
if (path.len < 1) return .relative;
|
|
|
|
const windows_path = std.fs.path.PathType.windows;
|
|
if (windows_path.isSep(T, path[0])) {
|
|
// \x
|
|
if (path.len < 2 or !windows_path.isSep(T, path[1])) return .rooted;
|
|
// \\. or \\?
|
|
if (path.len > 2 and (path[2] == mem.nativeToLittle(T, '.') or path[2] == mem.nativeToLittle(T, '?'))) {
|
|
// exactly \\. or \\? with nothing trailing
|
|
if (path.len == 3) return .root_local_device;
|
|
// \\.\x or \\?\x
|
|
if (windows_path.isSep(T, path[3])) return .local_device;
|
|
}
|
|
// \\x
|
|
return .unc_absolute;
|
|
} else {
|
|
// Some choice has to be made about how non-ASCII code points as drive-letters are handled, since
|
|
// path[0] is a different size for WTF-16 vs WTF-8, leading to a potential mismatch in classification
|
|
// for a WTF-8 path and its WTF-16 equivalent. For example, `€:\` encoded in WTF-16 is three code
|
|
// units `<0x20AC>:\` whereas `€:\` encoded as WTF-8 is 6 code units `<0xE2><0x82><0xAC>:\` so
|
|
// checking path[0], path[1] and path[2] would not behave the same between WTF-8/WTF-16.
|
|
//
|
|
// `RtlDetermineDosPathNameType_U` exclusively deals with WTF-16 and considers
|
|
// `€:\` a drive-absolute path, but code points that take two WTF-16 code units to encode get
|
|
// classified as a relative path (e.g. with U+20000 as the drive-letter that'd be encoded
|
|
// in WTF-16 as `<0xD840><0xDC00>:\` and be considered a relative path).
|
|
//
|
|
// The choice made here is to emulate the behavior of `RtlDetermineDosPathNameType_U` for both
|
|
// WTF-16 and WTF-8. This is because, while unlikely and not supported by the Disk Manager GUI,
|
|
// drive letters are not actually restricted to A-Z. Using `SetVolumeMountPointW` will allow you
|
|
// to set any byte value as a drive letter, and going through `IOCTL_MOUNTMGR_CREATE_POINT` will
|
|
// allow you to set any WTF-16 code unit as a drive letter.
|
|
//
|
|
// Non-A-Z drive letters don't interact well with most of Windows, but certain things do work, e.g.
|
|
// `cd /D €:\` will work, filesystem functions still work, etc.
|
|
//
|
|
// The unfortunate part of this is that this makes handling WTF-8 more complicated as we can't
|
|
// just check path[0], path[1], path[2].
|
|
const colon_i: usize = switch (T) {
|
|
u8 => i: {
|
|
const code_point_len = std.unicode.utf8ByteSequenceLength(path[0]) catch return .relative;
|
|
// Conveniently, 4-byte sequences in WTF-8 have the same starting code point
|
|
// as 2-code-unit sequences in WTF-16.
|
|
if (code_point_len > 3) return .relative;
|
|
break :i code_point_len;
|
|
},
|
|
u16 => 1,
|
|
else => @compileError("unsupported type: " ++ @typeName(T)),
|
|
};
|
|
// x
|
|
if (path.len < colon_i + 1 or path[colon_i] != mem.nativeToLittle(T, ':')) return .relative;
|
|
// x:\
|
|
if (path.len > colon_i + 1 and windows_path.isSep(T, path[colon_i + 1])) return .drive_absolute;
|
|
// x:
|
|
return .drive_relative;
|
|
}
|
|
}
|
|
|
|
test getWin32PathType {
|
|
try std.testing.expectEqual(.relative, getWin32PathType(u8, ""));
|
|
try std.testing.expectEqual(.relative, getWin32PathType(u8, "x"));
|
|
try std.testing.expectEqual(.relative, getWin32PathType(u8, "x\\"));
|
|
|
|
try std.testing.expectEqual(.root_local_device, getWin32PathType(u8, "//."));
|
|
try std.testing.expectEqual(.root_local_device, getWin32PathType(u8, "/\\?"));
|
|
try std.testing.expectEqual(.root_local_device, getWin32PathType(u8, "\\\\?"));
|
|
|
|
try std.testing.expectEqual(.local_device, getWin32PathType(u8, "//./x"));
|
|
try std.testing.expectEqual(.local_device, getWin32PathType(u8, "/\\?\\x"));
|
|
try std.testing.expectEqual(.local_device, getWin32PathType(u8, "\\\\?\\x"));
|
|
// local device paths require a path separator after the root, otherwise it is considered a UNC path
|
|
try std.testing.expectEqual(.unc_absolute, getWin32PathType(u8, "\\\\?x"));
|
|
try std.testing.expectEqual(.unc_absolute, getWin32PathType(u8, "//.x"));
|
|
|
|
try std.testing.expectEqual(.unc_absolute, getWin32PathType(u8, "//"));
|
|
try std.testing.expectEqual(.unc_absolute, getWin32PathType(u8, "\\\\x"));
|
|
try std.testing.expectEqual(.unc_absolute, getWin32PathType(u8, "//x"));
|
|
|
|
try std.testing.expectEqual(.rooted, getWin32PathType(u8, "\\x"));
|
|
try std.testing.expectEqual(.rooted, getWin32PathType(u8, "/"));
|
|
|
|
try std.testing.expectEqual(.drive_relative, getWin32PathType(u8, "x:"));
|
|
try std.testing.expectEqual(.drive_relative, getWin32PathType(u8, "x:abc"));
|
|
try std.testing.expectEqual(.drive_relative, getWin32PathType(u8, "x:a/b/c"));
|
|
|
|
try std.testing.expectEqual(.drive_absolute, getWin32PathType(u8, "x:\\"));
|
|
try std.testing.expectEqual(.drive_absolute, getWin32PathType(u8, "x:\\abc"));
|
|
try std.testing.expectEqual(.drive_absolute, getWin32PathType(u8, "x:/a/b/c"));
|
|
|
|
// Non-ASCII code point that is encoded as one WTF-16 code unit is considered a valid drive letter
|
|
try std.testing.expectEqual(.drive_absolute, getWin32PathType(u8, "€:\\"));
|
|
try std.testing.expectEqual(.drive_absolute, getWin32PathType(u16, std.unicode.wtf8ToWtf16LeStringLiteral("€:\\")));
|
|
try std.testing.expectEqual(.drive_relative, getWin32PathType(u8, "€:"));
|
|
try std.testing.expectEqual(.drive_relative, getWin32PathType(u16, std.unicode.wtf8ToWtf16LeStringLiteral("€:")));
|
|
// But code points that are encoded as two WTF-16 code units are not
|
|
try std.testing.expectEqual(.relative, getWin32PathType(u8, "\u{10000}:\\"));
|
|
try std.testing.expectEqual(.relative, getWin32PathType(u16, std.unicode.wtf8ToWtf16LeStringLiteral("\u{10000}:\\")));
|
|
}
|