mirror of
https://github.com/ziglang/zig.git
synced 2025-12-12 17:23:09 +00:00
Previously, fs.path handled a few of the Windows path types, but not all of them, and only a few of them correctly/consistently. This commit aims to make `std.fs.path` correct and consistent in handling all possible Win32 path types. This commit also slightly nudges the codebase towards a separation of Win32 paths and NT paths, as NT paths are not actually distinguishable from Win32 paths from looking at their contents alone (i.e. `\Device\Foo` could be an NT path or a Win32 rooted path, no way to tell without external context). This commit formalizes `std.fs.path` being fully concerned with Win32 paths, and having no special detection/handling of NT paths. Resources on Windows path types, and Win32 vs NT paths: - https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html - https://chrisdenton.github.io/omnipath/Overview.html - https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file API additions/changes/deprecations - `std.os.windows.getWin32PathType` was added (it is analogous to `RtlDetermineDosPathNameType_U`), while `std.os.windows.getNamespacePrefix` and `std.os.windows.getUnprefixedPathType` were deleted. `getWin32PathType` forms the basis on which the updated `std.fs.path` functions operate. - `std.fs.path.parsePath`, `std.fs.path.parsePathPosix`, and `std.fs.path.parsePathWindows` were added, while `std.fs.path.windowsParsePath` was deprecated. The new `parsePath` functions provide the "root" and the "kind" of a path, which is platform-specific. The now-deprecated `windowsParsePath` did not handle all possible path types, while the new `parsePathWindows` does. - `std.fs.path.diskDesignator` has been deprecated in favor of `std.fs.path.parsePath`, and same deal with `diskDesignatorWindows` -> `parsePathWindows` - `relativeWindows` is now a compile error when *not* targeting Windows, while `relativePosix` is now a compile error when targeting Windows. This is because those functions read/use the CWD path which will behave improperly when used from a system with different path semantics (e.g. calling `relativePosix` from a Windows system with a CWD like `C:\foo\bar` will give you a bogus result since that'd be treated as a single relative component when using POSIX semantics). This also allows `relativeWindows` to use Windows-specific APIs for getting the CWD and environment variables to cut down on allocations. - `componentIterator`/`ComponentIterator.init` have been made infallible. These functions used to be able to error on UNC paths with an empty server component, and on paths that were assumed to be NT paths, but now: + We follow the lead of `RtlDetermineDosPathNameType_U`/`RtlGetFullPathName_U` in how it treats a UNC path with an empty server name (e.g. `\\\share`) and allow it, even if it'll be invalid at the time of usage + Now that `std.fs.path` assumes paths are Win32 paths and not NT paths, we don't have to worry about NT paths Behavior changes - `std.fs.path` generally: any combinations of mixed path separators for UNC paths are universally supported, e.g. `\/server/share`, `/\server\share`, `/\server/\\//share` are all seen as equivalent UNC paths - `resolveWindows` handles all path types more appropriately/consistently. + `//` and `//foo` used to be treated as a relative path, but are now seen as UNC paths + If a rooted/drive-relative path cannot be resolved against anything more definite, the result will remain a rooted/drive-relative path. + I've created [a script to generate the results of a huge number of permutations of different path types](https://gist.github.com/squeek502/9eba7f19cad0d0d970ccafbc30f463bf) (the result of running the script is also included for anyone that'd like to vet the behavior). - `dirnameWindows` now treats the drive-relative root as the dirname of a drive-relative path with a component, e.g. `dirname("C:foo")` is now `C:`, whereas before it would return null. `dirnameWindows` also handles local device paths appropriately now. - `basenameWindows` now handles all path types more appropriately. The most notable change here is `//a` being treated as a partial UNC path now and therefore `basename` will return `""` for it, whereas before it would return `"a"` - `relativeWindows` will now do its best to resolve against the most appropriate CWD for each path, e.g. relative for `D:foo` will look at the CWD to check if the drive letter matches, and if not, look at the special environment variable `=D:` to get the shell-defined CWD for that drive, and if that doesn't exist, then it'll resolve against `D:\`. Implementation details - `resolveWindows` previously looped through the paths twice to build up the relevant info before doing the actual resolution. Now, `resolveWindows` iterates backwards once and keeps track of which paths are actually relevant using a bit set, which also allows it to break from the loop when it's no longer possible for earlier paths to matter. - A standalone test was added to test parts of `relativeWindows` since the CWD resolution logic depends on CWD information from the PEB and environment variables Edge cases worth noting - A strange piece of trivia that I found out while working on this is that it's technically possible to have a drive letter that it outside the intended A-Z range, or even outside the ASCII range entirely. Since we deal with both WTF-8 and WTF-16 paths, `path[0]`/`path[1]`/`path[2]` will not always refer to the same bits of information, so to get consistent behavior, some decision about how to deal with this edge case had to be made. I've made the choice to conform with how `RtlDetermineDosPathNameType_U` works, i.e. treat the first WTF-16 code unit as the drive letter. This means that when working with WTF-8, checking for drive-relative/drive-absolute paths is a bit more complicated. For more details, see the lengthy comment in `std.os.windows.getWin32PathType` - `relativeWindows` will now almost always be able to return either a fully-qualified absolute path or a relative path, but there's one scenario where it may return a rooted path: when the CWD gotten from the PEB is not a drive-absolute or UNC path (if that's actually feasible/possible?). An alternative approach to this scenario might be to resolve against the `HOMEDRIVE` env var if available, and/or default to `C:\` as a last resort in order to guarantee the result of `relative` is never a rooted path. - Partial UNC paths (e.g. `\\server` instead of `\\server\share`) are a bit awkward to handle, generally. Not entirely sure how best to handle them, so there may need to be another pass in the future to iron out any issues that arise. As of now the behavior is: + For `relative`, any part of a UNC disk designator is treated as the "root" and therefore isn't applicable for relative paths, e.g. calling `relative` with `\\server` and `\\server\share` will result in `\\server\share` rather than just `share` and if `relative` is called with `\\server\foo` and `\\server\bar` the result will be `\\server\bar` rather than `..\bar` + For `resolve`, any part of a UNC disk designator is also treated as the "root", but relative and rooted paths are still elligable for filling in missing portions of the disk designator, e.g. `resolve` with `\\server` and `foo` or `\foo` will result in `\\server\foo` Fixes #25703 Closes #25702
2561 lines
108 KiB
Zig
2561 lines
108 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 std = @import("../std.zig");
|
|
const debug = std.debug;
|
|
const assert = debug.assert;
|
|
const testing = std.testing;
|
|
const mem = std.mem;
|
|
const ascii = std.ascii;
|
|
const Allocator = mem.Allocator;
|
|
const windows = std.os.windows;
|
|
const process = std.process;
|
|
const native_os = builtin.target.os.tag;
|
|
|
|
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`.
|
|
pub inline fn isSep(comptime path_type: PathType, comptime T: type, c: T) bool {
|
|
return switch (path_type) {
|
|
.windows => c == '/' or c == '\\',
|
|
.posix => c == '/',
|
|
.uefi => c == '\\',
|
|
};
|
|
}
|
|
};
|
|
|
|
/// 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 (windows.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.indexOfAnyPos(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.indexOfAnyPos(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: windows.Win32PathType,
|
|
root: []const T,
|
|
};
|
|
}
|
|
|
|
pub fn parsePathWindows(comptime T: type, path: []const T) WindowsPath2(T) {
|
|
const kind = windows.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.indexOfAnyPos(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 = std.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 => windows.eqlIgnoreCaseWtf8,
|
|
u16 => windows.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 std.fs.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 std.fs.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));
|
|
}
|
|
|
|
pub const RelativeError = std.process.GetCwdAllocError;
|
|
|
|
/// Returns the relative path from `from` to `to`. If `from` and `to` each
|
|
/// resolve to the same path (after calling `resolve` on each), a zero-length
|
|
/// string is returned.
|
|
/// On Windows, 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`.
|
|
pub fn relative(allocator: Allocator, from: []const u8, to: []const u8) RelativeError![]u8 {
|
|
if (native_os == .windows) {
|
|
return relativeWindows(allocator, from, to);
|
|
} else {
|
|
return relativePosix(allocator, from, to);
|
|
}
|
|
}
|
|
|
|
pub fn relativeWindows(allocator: Allocator, from: []const u8, to: []const u8) ![]u8 {
|
|
if (native_os != .windows) @compileError("this function relies on Windows-specific semantics");
|
|
|
|
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(allocator, to, parsed_to);
|
|
}
|
|
|
|
const resolved_from = try windowsResolveAgainstCwd(allocator, from, parsed_from);
|
|
defer allocator.free(resolved_from);
|
|
var clean_up_resolved_to = true;
|
|
const resolved_to = try windowsResolveAgainstCwd(allocator, to, parsed_to);
|
|
defer if (clean_up_resolved_to) allocator.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 allocator.dupe(u8, to_it.rest());
|
|
const to_rest = to_it.rest();
|
|
if (to_it.next()) |to_component| {
|
|
if (windows.eqlIgnoreCaseWtf8(from_component, to_component))
|
|
continue;
|
|
}
|
|
var up_index_end = "..".len;
|
|
while (from_it.next()) |_| {
|
|
up_index_end += "\\..".len;
|
|
}
|
|
const result = try allocator.alloc(u8, up_index_end + @intFromBool(to_rest.len > 0) + to_rest.len);
|
|
errdefer allocator.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 allocator.realloc(result, result_index);
|
|
}
|
|
return [_]u8{};
|
|
}
|
|
|
|
fn windowsResolveAgainstCwd(allocator: Allocator, 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, allocator);
|
|
return switch (parsed.kind) {
|
|
.drive_absolute,
|
|
.unc_absolute,
|
|
.root_local_device,
|
|
.local_device,
|
|
=> try resolveWindows(allocator, &.{path}),
|
|
.relative => blk: {
|
|
const temp_allocator = temp_allocator_state.get();
|
|
|
|
const peb_cwd = windows.peb().ProcessParameters.CurrentDirectory.DosPath;
|
|
const cwd_w = (peb_cwd.Buffer.?)[0 .. peb_cwd.Length / 2];
|
|
|
|
const wtf8_len = std.unicode.calcWtf8Len(cwd_w);
|
|
const wtf8_buf = try temp_allocator.alloc(u8, wtf8_len);
|
|
defer temp_allocator.free(wtf8_buf);
|
|
assert(std.unicode.wtf16LeToWtf8(wtf8_buf, cwd_w) == wtf8_len);
|
|
|
|
break :blk try resolveWindows(allocator, &.{ wtf8_buf, path });
|
|
},
|
|
.rooted => blk: {
|
|
const peb_cwd = windows.peb().ProcessParameters.CurrentDirectory.DosPath;
|
|
const cwd_w = (peb_cwd.Buffer.?)[0 .. peb_cwd.Length / 2];
|
|
const parsed_cwd = parsePathWindows(u16, cwd_w);
|
|
switch (parsed_cwd.kind) {
|
|
.drive_absolute => {
|
|
var drive_buf = "_:\\".*;
|
|
drive_buf[0] = @truncate(cwd_w[0]);
|
|
break :blk try resolveWindows(allocator, &.{ &drive_buf, path });
|
|
},
|
|
.unc_absolute => {
|
|
const temp_allocator = temp_allocator_state.get();
|
|
var root_buf = try temp_allocator.alloc(u8, parsed_cwd.root.len * 3);
|
|
defer temp_allocator.free(root_buf);
|
|
|
|
const wtf8_len = std.unicode.wtf16LeToWtf8(root_buf, parsed_cwd.root);
|
|
const root = root_buf[0..wtf8_len];
|
|
break :blk try resolveWindows(allocator, &.{ root, path });
|
|
},
|
|
// Effectively a malformed CWD, give up and just return a normalized path
|
|
else => break :blk try resolveWindows(allocator, &.{path}),
|
|
}
|
|
},
|
|
.drive_relative => blk: {
|
|
const temp_allocator = temp_allocator_state.get();
|
|
const drive_cwd = drive_cwd: {
|
|
const peb_cwd = windows.peb().ProcessParameters.CurrentDirectory.DosPath;
|
|
const cwd_w = (peb_cwd.Buffer.?)[0 .. peb_cwd.Length / 2];
|
|
const parsed_cwd = parsePathWindows(u16, cwd_w);
|
|
|
|
if (parsed_cwd.kind == .drive_absolute) {
|
|
const drive_letter_w = parsed_cwd.root[0];
|
|
const drive_letters_match = drive_letter_w <= 0x7F and
|
|
ascii.toUpper(@intCast(drive_letter_w)) == ascii.toUpper(parsed.root[0]);
|
|
if (drive_letters_match) {
|
|
const wtf8_len = std.unicode.calcWtf8Len(cwd_w);
|
|
const wtf8_buf = try temp_allocator.alloc(u8, wtf8_len);
|
|
assert(std.unicode.wtf16LeToWtf8(wtf8_buf, cwd_w) == wtf8_len);
|
|
break :drive_cwd wtf8_buf[0..];
|
|
}
|
|
|
|
// Per-drive CWD's 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.
|
|
var key_buf = std.unicode.wtf8ToWtf16LeStringLiteral("=_:").*;
|
|
key_buf[1] = parsed.root[0];
|
|
if (std.process.getenvW(&key_buf)) |drive_cwd_w| {
|
|
const wtf8_len = std.unicode.calcWtf8Len(drive_cwd_w);
|
|
const wtf8_buf = try temp_allocator.alloc(u8, wtf8_len);
|
|
assert(std.unicode.wtf16LeToWtf8(wtf8_buf, drive_cwd_w) == wtf8_len);
|
|
break :drive_cwd wtf8_buf[0..];
|
|
}
|
|
}
|
|
|
|
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(allocator, &.{ drive_cwd, path });
|
|
},
|
|
};
|
|
}
|
|
|
|
pub fn relativePosix(allocator: Allocator, from: []const u8, to: []const u8) ![]u8 {
|
|
if (native_os == .windows) @compileError("this function relies on semantics that do not apply to Windows");
|
|
|
|
const cwd = try process.getCwdAlloc(allocator);
|
|
defer allocator.free(cwd);
|
|
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 {
|
|
if (native_os == .windows) {
|
|
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", "");
|
|
} else {
|
|
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, 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" {
|
|
// TODO: Fix on big endian architectures
|
|
if (builtin.cpu.arch.endian() != .little) {
|
|
return error.SkipZigTest;
|
|
}
|
|
|
|
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;
|