Merge pull request #25993 from squeek502/windows-paths

Teach `std.fs.path` about the wonderful world of Windows paths
This commit is contained in:
Ryan Liptak 2025-11-24 15:27:24 -08:00 committed by GitHub
commit 53e615b920
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 1709 additions and 786 deletions

View File

@ -2914,7 +2914,7 @@ fn validateSearchPath(path: []const u8) error{BadPathName}!void {
// (e.g. the NT \??\ prefix, the device \\.\ prefix, etc).
// Those path types are something of an unavoidable way to
// still hit unreachable during the openDir call.
var component_iterator = try std.fs.path.componentIterator(path);
var component_iterator = std.fs.path.componentIterator(path);
while (component_iterator.next()) |component| {
// https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file
if (std.mem.indexOfAny(u8, component.name, "\x00<>:\"|?*") != null) return error.BadPathName;

View File

@ -104,9 +104,7 @@ fn findPrefixResolved(cache: *const Cache, resolved_path: []u8) !PrefixedPath {
fn getPrefixSubpath(allocator: Allocator, prefix: []const u8, path: []u8) ![]u8 {
const relative = try fs.path.relative(allocator, prefix, path);
errdefer allocator.free(relative);
var component_iterator = fs.path.NativeComponentIterator.init(relative) catch {
return error.NotASubPath;
};
var component_iterator = fs.path.NativeComponentIterator.init(relative);
if (component_iterator.root() != null) {
return error.NotASubPath;
}

View File

@ -167,7 +167,7 @@ pub fn setPaths(fse: *FsEvents, gpa: Allocator, steps: []const *std.Build.Step)
}.lessThan);
need_dirs.clearRetainingCapacity();
for (old_dirs) |dir_path| {
var it: std.fs.path.ComponentIterator(.posix, u8) = try .init(dir_path);
var it: std.fs.path.ComponentIterator(.posix, u8) = .init(dir_path);
while (it.next()) |component| {
if (need_dirs.contains(component.path)) {
// this path is '/foo/bar/qux', but '/foo' or '/foo/bar' was already added

View File

@ -318,7 +318,7 @@ pub const MakePathStatus = enum { existed, created };
/// Same as `makePath` except returns whether the path already existed or was
/// successfully created.
pub fn makePathStatus(dir: Dir, io: Io, sub_path: []const u8) MakePathError!MakePathStatus {
var it = try std.fs.path.componentIterator(sub_path);
var it = std.fs.path.componentIterator(sub_path);
var status: MakePathStatus = .existed;
var component = it.last() orelse return error.BadPathName;
while (true) {

View File

@ -1210,7 +1210,7 @@ fn dirMakeOpenPathWindows(
w.SYNCHRONIZE | w.FILE_TRAVERSE |
(if (options.iterate) w.FILE_LIST_DIRECTORY else @as(u32, 0));
var it = try std.fs.path.componentIterator(sub_path);
var it = std.fs.path.componentIterator(sub_path);
// If there are no components in the path, then create a dummy component with the full path.
var component: std.fs.path.NativeComponentIterator.Component = it.last() orelse .{
.name = "",

File diff suppressed because it is too large Load Diff

View File

@ -56,7 +56,7 @@ const PathType = enum {
// using '127.0.0.1' as the server name and '<drive letter>$' as the share name.
var fd_path_buf: [fs.max_path_bytes]u8 = undefined;
const dir_path = try std.os.getFdPath(dir.fd, &fd_path_buf);
const windows_path_type = windows.getUnprefixedPathType(u8, dir_path);
const windows_path_type = windows.getWin32PathType(u8, dir_path);
switch (windows_path_type) {
.unc_absolute => return fs.path.joinZ(allocator, &.{ dir_path, relative_path }),
.drive_absolute => {

View File

@ -816,8 +816,11 @@ pub fn CreateSymbolicLink(
// https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createsymboliclinkw
var is_target_absolute = false;
const final_target_path = target_path: {
switch (getNamespacePrefix(u16, target_path)) {
.none => switch (getUnprefixedPathType(u16, target_path)) {
if (hasCommonNtPrefix(u16, target_path)) {
// Already an NT path, no need to do anything to it
break :target_path target_path;
} else {
switch (getWin32PathType(u16, target_path)) {
// Rooted paths need to avoid getting put through wToPrefixedFileW
// (and they are treated as relative in this context)
// Note: It seems that rooted paths in symbolic links are relative to
@ -829,10 +832,7 @@ pub fn CreateSymbolicLink(
// Keep relative paths relative, but anything else needs to get NT-prefixed.
else => if (!std.fs.path.isAbsoluteWindowsWtf16(target_path))
break :target_path target_path,
},
// Already an NT path, no need to do anything to it
.nt => break :target_path target_path,
else => {},
}
}
var prefixed_target_path = try wToPrefixedFileW(dir, target_path);
// We do this after prefixing to ensure that drive-relative paths are treated as absolute
@ -2145,7 +2145,7 @@ pub fn nanoSecondsToFileTime(ns: Io.Timestamp) FILETIME {
/// Compares two WTF16 strings using the equivalent functionality of
/// `RtlEqualUnicodeString` (with case insensitive comparison enabled).
/// This function can be called on any target.
pub fn eqlIgnoreCaseWTF16(a: []const u16, b: []const u16) bool {
pub fn eqlIgnoreCaseWtf16(a: []const u16, b: []const u16) bool {
if (@inComptime() or builtin.os.tag != .windows) {
// This function compares the strings code unit by code unit (aka u16-to-u16),
// so any length difference implies inequality. In other words, there's no possible
@ -2222,19 +2222,19 @@ pub fn eqlIgnoreCaseWtf8(a: []const u8, b: []const u8) bool {
fn testEqlIgnoreCase(comptime expect_eql: bool, comptime a: []const u8, comptime b: []const u8) !void {
try std.testing.expectEqual(expect_eql, eqlIgnoreCaseWtf8(a, b));
try std.testing.expectEqual(expect_eql, eqlIgnoreCaseWTF16(
try std.testing.expectEqual(expect_eql, eqlIgnoreCaseWtf16(
std.unicode.utf8ToUtf16LeStringLiteral(a),
std.unicode.utf8ToUtf16LeStringLiteral(b),
));
try comptime std.testing.expect(expect_eql == eqlIgnoreCaseWtf8(a, b));
try comptime std.testing.expect(expect_eql == eqlIgnoreCaseWTF16(
try comptime std.testing.expect(expect_eql == eqlIgnoreCaseWtf16(
std.unicode.utf8ToUtf16LeStringLiteral(a),
std.unicode.utf8ToUtf16LeStringLiteral(b),
));
}
test "eqlIgnoreCaseWTF16/Wtf8" {
test "eqlIgnoreCaseWtf16/Wtf8" {
try testEqlIgnoreCase(true, "\x01 a B Λ ɐ", "\x01 A b λ Ɐ");
// does not do case-insensitive comparison for codepoints >= U+10000
try testEqlIgnoreCase(false, "𐓏", "𐓷");
@ -2365,10 +2365,9 @@ pub const Wtf16ToPrefixedFileWError = error{
/// - . and space are not stripped from the end of relative paths (potential TODO)
pub fn wToPrefixedFileW(dir: ?HANDLE, path: [:0]const u16) Wtf16ToPrefixedFileWError!PathSpace {
const nt_prefix = [_]u16{ '\\', '?', '?', '\\' };
switch (getNamespacePrefix(u16, path)) {
// TODO: Figure out a way to design an API that can avoid the copy for .nt,
if (hasCommonNtPrefix(u16, path)) {
// TODO: Figure out a way to design an API that can avoid the copy for NT,
// since it is always returned fully unmodified.
.nt, .verbatim => {
var path_space: PathSpace = undefined;
path_space.data[0..nt_prefix.len].* = nt_prefix;
const len_after_prefix = path.len - nt_prefix.len;
@ -2376,9 +2375,20 @@ pub fn wToPrefixedFileW(dir: ?HANDLE, path: [:0]const u16) Wtf16ToPrefixedFileWE
path_space.len = path.len;
path_space.data[path_space.len] = 0;
return path_space;
} else {
const path_type = getWin32PathType(u16, path);
var path_space: PathSpace = undefined;
if (path_type == .local_device) {
switch (getLocalDevicePathType(u16, path)) {
.verbatim => {
path_space.data[0..nt_prefix.len].* = nt_prefix;
const len_after_prefix = path.len - nt_prefix.len;
@memcpy(path_space.data[nt_prefix.len..][0..len_after_prefix], path[nt_prefix.len..]);
path_space.len = path.len;
path_space.data[path_space.len] = 0;
return path_space;
},
.local_device, .fake_verbatim => {
var path_space: PathSpace = undefined;
const path_byte_len = ntdll.RtlGetFullPathName_U(
path.ptr,
path_space.data.len * 2,
@ -2397,9 +2407,8 @@ pub fn wToPrefixedFileW(dir: ?HANDLE, path: [:0]const u16) Wtf16ToPrefixedFileWE
path_space.data[0..nt_prefix.len].* = nt_prefix;
return path_space;
},
.none => {
const path_type = getUnprefixedPathType(u16, path);
var path_space: PathSpace = undefined;
}
}
relative: {
if (path_type == .relative) {
// TODO: Handle special case device names like COM1, AUX, NUL, CONIN$, CONOUT$, etc.
@ -2511,12 +2520,154 @@ pub fn wToPrefixedFileW(dir: ?HANDLE, path: [:0]const u16) Wtf16ToPrefixedFileWE
path_space.data[nt_prefix.len..][0..unc.len].* = unc;
}
return path_space;
},
}
}
pub const NamespacePrefix = enum {
none,
/// Similar to `RTL_PATH_TYPE`, but without the `UNKNOWN` path type.
pub const Win32PathType = enum {
/// `\\server\share\foo`
unc_absolute,
/// `C:\foo`
drive_absolute,
/// `C:foo`
drive_relative,
/// `\foo`
rooted,
/// `foo`
relative,
/// `\\.\foo`, `\\?\foo`
local_device,
/// `\\.`, `\\?`
root_local_device,
};
/// Get the path type of a Win32 namespace path.
/// Similar to `RtlDetermineDosPathNameType_U`.
/// If `T` is `u16`, then `path` should be encoded as WTF-16LE.
pub fn getWin32PathType(comptime T: type, path: []const T) Win32PathType {
if (path.len < 1) return .relative;
const windows_path = std.fs.path.PathType.windows;
if (windows_path.isSep(T, path[0])) {
// \x
if (path.len < 2 or !windows_path.isSep(T, path[1])) return .rooted;
// \\. or \\?
if (path.len > 2 and (path[2] == mem.nativeToLittle(T, '.') or path[2] == mem.nativeToLittle(T, '?'))) {
// exactly \\. or \\? with nothing trailing
if (path.len == 3) return .root_local_device;
// \\.\x or \\?\x
if (windows_path.isSep(T, path[3])) return .local_device;
}
// \\x
return .unc_absolute;
} else {
// Some choice has to be made about how non-ASCII code points as drive-letters are handled, since
// path[0] is a different size for WTF-16 vs WTF-8, leading to a potential mismatch in classification
// for a WTF-8 path and its WTF-16 equivalent. For example, `:\` encoded in WTF-16 is three code
// units `<0x20AC>:\` whereas `:\` encoded as WTF-8 is 6 code units `<0xE2><0x82><0xAC>:\` so
// checking path[0], path[1] and path[2] would not behave the same between WTF-8/WTF-16.
//
// `RtlDetermineDosPathNameType_U` exclusively deals with WTF-16 and considers
// `:\` a drive-absolute path, but code points that take two WTF-16 code units to encode get
// classified as a relative path (e.g. with U+20000 as the drive-letter that'd be encoded
// in WTF-16 as `<0xD840><0xDC00>:\` and be considered a relative path).
//
// The choice made here is to emulate the behavior of `RtlDetermineDosPathNameType_U` for both
// WTF-16 and WTF-8. This is because, while unlikely and not supported by the Disk Manager GUI,
// drive letters are not actually restricted to A-Z. Using `SetVolumeMountPointW` will allow you
// to set any byte value as a drive letter, and going through `IOCTL_MOUNTMGR_CREATE_POINT` will
// allow you to set any WTF-16 code unit as a drive letter.
//
// Non-A-Z drive letters don't interact well with most of Windows, but certain things do work, e.g.
// `cd /D :\` will work, filesystem functions still work, etc.
//
// The unfortunate part of this is that this makes handling WTF-8 more complicated as we can't
// just check path[0], path[1], path[2].
const colon_i: usize = switch (T) {
u8 => i: {
const code_point_len = std.unicode.utf8ByteSequenceLength(path[0]) catch return .relative;
// Conveniently, 4-byte sequences in WTF-8 have the same starting code point
// as 2-code-unit sequences in WTF-16.
if (code_point_len > 3) return .relative;
break :i code_point_len;
},
u16 => 1,
else => @compileError("unsupported type: " ++ @typeName(T)),
};
// x
if (path.len < colon_i + 1 or path[colon_i] != mem.nativeToLittle(T, ':')) return .relative;
// x:\
if (path.len > colon_i + 1 and windows_path.isSep(T, path[colon_i + 1])) return .drive_absolute;
// x:
return .drive_relative;
}
}
test getWin32PathType {
try std.testing.expectEqual(.relative, getWin32PathType(u8, ""));
try std.testing.expectEqual(.relative, getWin32PathType(u8, "x"));
try std.testing.expectEqual(.relative, getWin32PathType(u8, "x\\"));
try std.testing.expectEqual(.root_local_device, getWin32PathType(u8, "//."));
try std.testing.expectEqual(.root_local_device, getWin32PathType(u8, "/\\?"));
try std.testing.expectEqual(.root_local_device, getWin32PathType(u8, "\\\\?"));
try std.testing.expectEqual(.local_device, getWin32PathType(u8, "//./x"));
try std.testing.expectEqual(.local_device, getWin32PathType(u8, "/\\?\\x"));
try std.testing.expectEqual(.local_device, getWin32PathType(u8, "\\\\?\\x"));
// local device paths require a path separator after the root, otherwise it is considered a UNC path
try std.testing.expectEqual(.unc_absolute, getWin32PathType(u8, "\\\\?x"));
try std.testing.expectEqual(.unc_absolute, getWin32PathType(u8, "//.x"));
try std.testing.expectEqual(.unc_absolute, getWin32PathType(u8, "//"));
try std.testing.expectEqual(.unc_absolute, getWin32PathType(u8, "\\\\x"));
try std.testing.expectEqual(.unc_absolute, getWin32PathType(u8, "//x"));
try std.testing.expectEqual(.rooted, getWin32PathType(u8, "\\x"));
try std.testing.expectEqual(.rooted, getWin32PathType(u8, "/"));
try std.testing.expectEqual(.drive_relative, getWin32PathType(u8, "x:"));
try std.testing.expectEqual(.drive_relative, getWin32PathType(u8, "x:abc"));
try std.testing.expectEqual(.drive_relative, getWin32PathType(u8, "x:a/b/c"));
try std.testing.expectEqual(.drive_absolute, getWin32PathType(u8, "x:\\"));
try std.testing.expectEqual(.drive_absolute, getWin32PathType(u8, "x:\\abc"));
try std.testing.expectEqual(.drive_absolute, getWin32PathType(u8, "x:/a/b/c"));
// Non-ASCII code point that is encoded as one WTF-16 code unit is considered a valid drive letter
try std.testing.expectEqual(.drive_absolute, getWin32PathType(u8, "€:\\"));
try std.testing.expectEqual(.drive_absolute, getWin32PathType(u16, std.unicode.wtf8ToWtf16LeStringLiteral("€:\\")));
try std.testing.expectEqual(.drive_relative, getWin32PathType(u8, "€:"));
try std.testing.expectEqual(.drive_relative, getWin32PathType(u16, std.unicode.wtf8ToWtf16LeStringLiteral("€:")));
// But code points that are encoded as two WTF-16 code units are not
try std.testing.expectEqual(.relative, getWin32PathType(u8, "\u{10000}:\\"));
try std.testing.expectEqual(.relative, getWin32PathType(u16, std.unicode.wtf8ToWtf16LeStringLiteral("\u{10000}:\\")));
}
/// Returns true if the path starts with `\??\`, which is indicative of an NT path
/// but is not enough to fully distinguish between NT paths and Win32 paths, as
/// `\??\` is not actually a distinct prefix but rather the path to a special virtual
/// folder in the Object Manager.
///
/// For example, `\Device\HarddiskVolume2` and `\DosDevices\C:` are also NT paths but
/// cannot be distinguished as such by their prefix.
///
/// So, inferring whether a path is an NT path or a Win32 path is usually a mistake;
/// that information should instead be known ahead-of-time.
///
/// If `T` is `u16`, then `path` should be encoded as WTF-16LE.
pub fn hasCommonNtPrefix(comptime T: type, path: []const T) bool {
// Must be exactly \??\, forward slashes are not allowed
const expected_wtf8_prefix = "\\??\\";
const expected_prefix = switch (T) {
u8 => expected_wtf8_prefix,
u16 => std.unicode.wtf8ToWtf16LeStringLiteral(expected_wtf8_prefix),
else => @compileError("unsupported type: " ++ @typeName(T)),
};
return mem.startsWith(T, path, expected_prefix);
}
const LocalDevicePathType = enum {
/// `\\.\` (path separators can be `\` or `/`)
local_device,
/// `\\?\`
@ -2529,107 +2680,24 @@ pub const NamespacePrefix = enum {
/// it will become `\??\C:\foo` [it will be canonicalized and the //?/ won't
/// be treated as part of the final path])
fake_verbatim,
/// `\??\`
nt,
};
/// If `T` is `u16`, then `path` should be encoded as WTF-16LE.
pub fn getNamespacePrefix(comptime T: type, path: []const T) NamespacePrefix {
if (path.len < 4) return .none;
var all_backslash = switch (mem.littleToNative(T, path[0])) {
'\\' => true,
'/' => false,
else => return .none,
};
all_backslash = all_backslash and switch (mem.littleToNative(T, path[3])) {
'\\' => true,
'/' => false,
else => return .none,
};
switch (mem.littleToNative(T, path[1])) {
'?' => if (mem.littleToNative(T, path[2]) == '?' and all_backslash) return .nt else return .none,
'\\' => {},
'/' => all_backslash = false,
else => return .none,
}
return switch (mem.littleToNative(T, path[2])) {
'?' => if (all_backslash) .verbatim else .fake_verbatim,
'.' => .local_device,
else => .none,
};
}
test getNamespacePrefix {
try std.testing.expectEqual(NamespacePrefix.none, getNamespacePrefix(u8, ""));
try std.testing.expectEqual(NamespacePrefix.nt, getNamespacePrefix(u8, "\\??\\"));
try std.testing.expectEqual(NamespacePrefix.none, getNamespacePrefix(u8, "/??/"));
try std.testing.expectEqual(NamespacePrefix.none, getNamespacePrefix(u8, "/??\\"));
try std.testing.expectEqual(NamespacePrefix.none, getNamespacePrefix(u8, "\\?\\\\"));
try std.testing.expectEqual(NamespacePrefix.local_device, getNamespacePrefix(u8, "\\\\.\\"));
try std.testing.expectEqual(NamespacePrefix.local_device, getNamespacePrefix(u8, "\\\\./"));
try std.testing.expectEqual(NamespacePrefix.local_device, getNamespacePrefix(u8, "/\\./"));
try std.testing.expectEqual(NamespacePrefix.local_device, getNamespacePrefix(u8, "//./"));
try std.testing.expectEqual(NamespacePrefix.none, getNamespacePrefix(u8, "/.//"));
try std.testing.expectEqual(NamespacePrefix.verbatim, getNamespacePrefix(u8, "\\\\?\\"));
try std.testing.expectEqual(NamespacePrefix.fake_verbatim, getNamespacePrefix(u8, "\\/?\\"));
try std.testing.expectEqual(NamespacePrefix.fake_verbatim, getNamespacePrefix(u8, "\\/?/"));
try std.testing.expectEqual(NamespacePrefix.fake_verbatim, getNamespacePrefix(u8, "//?/"));
}
pub const UnprefixedPathType = enum {
unc_absolute,
drive_absolute,
drive_relative,
rooted,
relative,
root_local_device,
};
/// Get the path type of a path that is known to not have any namespace prefixes
/// (`\\?\`, `\\.\`, `\??\`).
/// If `T` is `u16`, then `path` should be encoded as WTF-16LE.
pub fn getUnprefixedPathType(comptime T: type, path: []const T) UnprefixedPathType {
if (path.len < 1) return .relative;
/// Only relevant for Win32 -> NT path conversion.
/// Asserts `path` is of type `Win32PathType.local_device`.
fn getLocalDevicePathType(comptime T: type, path: []const T) LocalDevicePathType {
if (std.debug.runtime_safety) {
std.debug.assert(getNamespacePrefix(T, path) == .none);
std.debug.assert(getWin32PathType(T, path) == .local_device);
}
const windows_path = std.fs.path.PathType.windows;
if (windows_path.isSep(T, mem.littleToNative(T, path[0]))) {
// \x
if (path.len < 2 or !windows_path.isSep(T, mem.littleToNative(T, path[1]))) return .rooted;
// exactly \\. or \\? with nothing trailing
if (path.len == 3 and (mem.littleToNative(T, path[2]) == '.' or mem.littleToNative(T, path[2]) == '?')) return .root_local_device;
// \\x
return .unc_absolute;
} else {
// x
if (path.len < 2 or mem.littleToNative(T, path[1]) != ':') return .relative;
// x:\
if (path.len > 2 and windows_path.isSep(T, mem.littleToNative(T, path[2]))) return .drive_absolute;
// x:
return .drive_relative;
}
}
test getUnprefixedPathType {
try std.testing.expectEqual(UnprefixedPathType.relative, getUnprefixedPathType(u8, ""));
try std.testing.expectEqual(UnprefixedPathType.relative, getUnprefixedPathType(u8, "x"));
try std.testing.expectEqual(UnprefixedPathType.relative, getUnprefixedPathType(u8, "x\\"));
try std.testing.expectEqual(UnprefixedPathType.root_local_device, getUnprefixedPathType(u8, "//."));
try std.testing.expectEqual(UnprefixedPathType.root_local_device, getUnprefixedPathType(u8, "/\\?"));
try std.testing.expectEqual(UnprefixedPathType.root_local_device, getUnprefixedPathType(u8, "\\\\?"));
try std.testing.expectEqual(UnprefixedPathType.unc_absolute, getUnprefixedPathType(u8, "\\\\x"));
try std.testing.expectEqual(UnprefixedPathType.unc_absolute, getUnprefixedPathType(u8, "//x"));
try std.testing.expectEqual(UnprefixedPathType.rooted, getUnprefixedPathType(u8, "\\x"));
try std.testing.expectEqual(UnprefixedPathType.rooted, getUnprefixedPathType(u8, "/"));
try std.testing.expectEqual(UnprefixedPathType.drive_relative, getUnprefixedPathType(u8, "x:"));
try std.testing.expectEqual(UnprefixedPathType.drive_relative, getUnprefixedPathType(u8, "x:abc"));
try std.testing.expectEqual(UnprefixedPathType.drive_relative, getUnprefixedPathType(u8, "x:a/b/c"));
try std.testing.expectEqual(UnprefixedPathType.drive_absolute, getUnprefixedPathType(u8, "x:\\"));
try std.testing.expectEqual(UnprefixedPathType.drive_absolute, getUnprefixedPathType(u8, "x:\\abc"));
try std.testing.expectEqual(UnprefixedPathType.drive_absolute, getUnprefixedPathType(u8, "x:/a/b/c"));
const backslash = mem.nativeToLittle(T, '\\');
const all_backslash = path[0] == backslash and
path[1] == backslash and
path[3] == backslash;
return switch (path[2]) {
mem.nativeToLittle(T, '?') => if (all_backslash) .verbatim else .fake_verbatim,
mem.nativeToLittle(T, '.') => .local_device,
else => unreachable,
};
}
/// Similar to `RtlNtPathNameToDosPathName` but does not do any heap allocation.
@ -2646,17 +2714,15 @@ test getUnprefixedPathType {
/// Supports in-place modification (`path` and `out` may refer to the same slice).
pub fn ntToWin32Namespace(path: []const u16, out: []u16) error{ NameTooLong, NotNtPath }![]u16 {
if (path.len > PATH_MAX_WIDE) return error.NameTooLong;
if (!hasCommonNtPrefix(u16, path)) return error.NotNtPath;
const namespace_prefix = getNamespacePrefix(u16, path);
switch (namespace_prefix) {
.nt => {
var dest_index: usize = 0;
var after_prefix = path[4..]; // after the `\??\`
// The prefix \??\UNC\ means this is a UNC path, in which case the
// `\??\UNC\` should be replaced by `\\` (two backslashes)
const is_unc = after_prefix.len >= 4 and
eqlIgnoreCaseWTF16(after_prefix[0..3], std.unicode.utf8ToUtf16LeStringLiteral("UNC")) and
std.fs.path.PathType.windows.isSep(u16, std.mem.littleToNative(u16, after_prefix[3]));
eqlIgnoreCaseWtf16(after_prefix[0..3], std.unicode.utf8ToUtf16LeStringLiteral("UNC")) and
std.fs.path.PathType.windows.isSep(u16, after_prefix[3]);
const win32_len = path.len - @as(usize, if (is_unc) 6 else 4);
if (out.len < win32_len) return error.NameTooLong;
if (is_unc) {
@ -2667,9 +2733,6 @@ pub fn ntToWin32Namespace(path: []const u16, out: []u16) error{ NameTooLong, Not
}
@memmove(out[dest_index..][0..after_prefix.len], after_prefix);
return out[0..win32_len];
},
else => return error.NotNtPath,
}
}
test ntToWin32Namespace {

View File

@ -54,8 +54,7 @@ fn testToPrefixedFileOnlyOracle(comptime path: []const u8) !void {
}
test "toPrefixedFileW" {
if (builtin.os.tag != .windows)
return;
if (builtin.os.tag != .windows) return error.SkipZigTest;
// Most test cases come from https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
// Note that these tests do not actually touch the filesystem or care about whether or not
@ -237,3 +236,104 @@ test "removeDotDirs" {
try testRemoveDotDirs("a\\b\\..\\", "a\\");
try testRemoveDotDirs("a\\b\\..\\c", "a\\c");
}
const RTL_PATH_TYPE = enum(c_int) {
Unknown,
UncAbsolute,
DriveAbsolute,
DriveRelative,
Rooted,
Relative,
LocalDevice,
RootLocalDevice,
};
pub extern "ntdll" fn RtlDetermineDosPathNameType_U(
Path: [*:0]const u16,
) callconv(.winapi) RTL_PATH_TYPE;
test "getWin32PathType vs RtlDetermineDosPathNameType_U" {
if (builtin.os.tag != .windows) return error.SkipZigTest;
var buf: std.ArrayList(u16) = .empty;
defer buf.deinit(std.testing.allocator);
var wtf8_buf: std.ArrayList(u8) = .empty;
defer wtf8_buf.deinit(std.testing.allocator);
var random = std.Random.DefaultPrng.init(std.testing.random_seed);
const rand = random.random();
for (0..1000) |_| {
buf.clearRetainingCapacity();
const path = try getRandomWtf16Path(std.testing.allocator, &buf, rand);
wtf8_buf.clearRetainingCapacity();
const wtf8_len = std.unicode.calcWtf8Len(path);
try wtf8_buf.ensureTotalCapacity(std.testing.allocator, wtf8_len);
wtf8_buf.items.len = wtf8_len;
std.debug.assert(std.unicode.wtf16LeToWtf8(wtf8_buf.items, path) == wtf8_len);
const windows_type = RtlDetermineDosPathNameType_U(path);
const wtf16_type = windows.getWin32PathType(u16, path);
const wtf8_type = windows.getWin32PathType(u8, wtf8_buf.items);
checkPathType(windows_type, wtf16_type) catch |err| {
std.debug.print("expected type {}, got {} for path: {f}\n", .{ windows_type, wtf16_type, std.unicode.fmtUtf16Le(path) });
std.debug.print("path bytes:\n", .{});
std.debug.dumpHex(std.mem.sliceAsBytes(path));
return err;
};
if (wtf16_type != wtf8_type) {
std.debug.print("type mismatch between wtf8: {} and wtf16: {} for path: {f}\n", .{ wtf8_type, wtf16_type, std.unicode.fmtUtf16Le(path) });
std.debug.print("wtf-16 path bytes:\n", .{});
std.debug.dumpHex(std.mem.sliceAsBytes(path));
std.debug.print("wtf-8 path bytes:\n", .{});
std.debug.dumpHex(std.mem.sliceAsBytes(wtf8_buf.items));
return error.Wtf8Wtf16Mismatch;
}
}
}
fn checkPathType(windows_type: RTL_PATH_TYPE, zig_type: windows.Win32PathType) !void {
const expected_windows_type: RTL_PATH_TYPE = switch (zig_type) {
.unc_absolute => .UncAbsolute,
.drive_absolute => .DriveAbsolute,
.drive_relative => .DriveRelative,
.rooted => .Rooted,
.relative => .Relative,
.local_device => .LocalDevice,
.root_local_device => .RootLocalDevice,
};
if (windows_type != expected_windows_type) return error.PathTypeMismatch;
}
fn getRandomWtf16Path(allocator: std.mem.Allocator, buf: *std.ArrayList(u16), rand: std.Random) ![:0]const u16 {
const Choice = enum {
backslash,
slash,
control,
printable,
non_ascii,
};
const choices = rand.uintAtMostBiased(u16, 32);
for (0..choices) |_| {
const choice = rand.enumValue(Choice);
const code_unit = switch (choice) {
.backslash => '\\',
.slash => '/',
.control => switch (rand.uintAtMostBiased(u8, 0x20)) {
0x20 => '\x7F',
else => |b| b + 1, // no NUL
},
.printable => '!' + rand.uintAtMostBiased(u8, '~' - '!'),
.non_ascii => rand.intRangeAtMostBiased(u16, 0x80, 0xFFFF),
};
try buf.append(allocator, std.mem.nativeToLittle(u16, code_unit));
}
try buf.append(allocator, 0);
return buf.items[0 .. buf.items.len - 1 :0];
}

View File

@ -22,16 +22,17 @@ pub const GetCwdError = posix.GetCwdError;
/// The result is a slice of `out_buffer`, from index `0`.
/// On Windows, the result is encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On other platforms, the result is an opaque sequence of bytes with no particular encoding.
pub fn getCwd(out_buffer: []u8) ![]u8 {
pub fn getCwd(out_buffer: []u8) GetCwdError![]u8 {
return posix.getcwd(out_buffer);
}
pub const GetCwdAllocError = Allocator.Error || posix.GetCwdError;
// Same as GetCwdError, minus error.NameTooLong + Allocator.Error
pub const GetCwdAllocError = Allocator.Error || error{CurrentWorkingDirectoryUnlinked} || posix.UnexpectedError;
/// Caller must free the returned memory.
/// On Windows, the result is encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On other platforms, the result is an opaque sequence of bytes with no particular encoding.
pub fn getCwdAlloc(allocator: Allocator) ![]u8 {
pub fn getCwdAlloc(allocator: Allocator) GetCwdAllocError![]u8 {
// The use of max_path_bytes here is just a heuristic: most paths will fit
// in stack_buf, avoiding an extra allocation in the common case.
var stack_buf: [fs.max_path_bytes]u8 = undefined;
@ -529,6 +530,7 @@ pub fn hasNonEmptyEnvVar(allocator: Allocator, key: []const u8) HasEnvVarError!b
}
/// Windows-only. Get an environment variable with a null-terminated, WTF-16 encoded name.
/// The returned slice points to memory in the PEB.
///
/// This function performs a Unicode-aware case-insensitive lookup using RtlEqualUnicodeString.
///
@ -564,7 +566,7 @@ pub fn getenvW(key: [*:0]const u16) ?[:0]const u16 {
};
const this_key = key_value[0..equal_index];
if (windows.eqlIgnoreCaseWTF16(key_slice, this_key)) {
if (windows.eqlIgnoreCaseWtf16(key_slice, this_key)) {
return key_value[equal_index + 1 ..];
}

View File

@ -1227,7 +1227,7 @@ fn windowsCreateProcessPathExt(
const app_name = app_buf.items[0..app_name_len];
const ext_start = std.mem.lastIndexOfScalar(u16, app_name, '.') orelse break :unappended err;
const ext = app_name[ext_start..];
if (windows.eqlIgnoreCaseWTF16(ext, unicode.utf8ToUtf16LeStringLiteral(".EXE"))) {
if (windows.eqlIgnoreCaseWtf16(ext, unicode.utf8ToUtf16LeStringLiteral(".EXE"))) {
return error.UnrecoverableInvalidExe;
}
break :unappended err;
@ -1278,7 +1278,7 @@ fn windowsCreateProcessPathExt(
// On InvalidExe, if the extension of the app name is .exe then
// it's treated as an unrecoverable error. Otherwise, it'll be
// skipped as normal.
if (windows.eqlIgnoreCaseWTF16(ext, unicode.utf8ToUtf16LeStringLiteral(".EXE"))) {
if (windows.eqlIgnoreCaseWtf16(ext, unicode.utf8ToUtf16LeStringLiteral(".EXE"))) {
return error.UnrecoverableInvalidExe;
}
continue;

View File

@ -643,7 +643,7 @@ const MsvcLibDir = struct {
if (!std.fs.path.isAbsolute(dll_path)) return error.PathNotFound;
var path_it = std.fs.path.componentIterator(dll_path) catch return error.PathNotFound;
var path_it = std.fs.path.componentIterator(dll_path);
// the .dll filename
_ = path_it.last();
const root_path = while (path_it.previous()) |dir_component| {

View File

@ -3883,7 +3883,7 @@ fn createModule(
if (create_module.sysroot) |root| {
for (create_module.lib_dir_args.items) |lib_dir_arg| {
if (fs.path.isAbsolute(lib_dir_arg)) {
const stripped_dir = lib_dir_arg[fs.path.diskDesignator(lib_dir_arg).len..];
const stripped_dir = lib_dir_arg[fs.path.parsePath(lib_dir_arg).root.len..];
const full_path = try fs.path.join(arena, &[_][]const u8{ root, stripped_dir });
addLibDirectoryWarn(&create_module.lib_directories, full_path);
} else {

View File

@ -126,6 +126,9 @@
.windows_bat_args = .{
.path = "windows_bat_args",
},
.windows_paths = .{
.path = "windows_paths",
},
.self_exe_symlink = .{
.path = "self_exe_symlink",
},

View File

@ -0,0 +1,37 @@
const std = @import("std");
const builtin = @import("builtin");
pub fn build(b: *std.Build) void {
const test_step = b.step("test", "Test it");
b.default_step = test_step;
const optimize: std.builtin.OptimizeMode = .Debug;
const target = b.graph.host;
if (builtin.os.tag != .windows) return;
const relative = b.addExecutable(.{
.name = "relative",
.root_module = b.createModule(.{
.root_source_file = b.path("relative.zig"),
.optimize = optimize,
.target = target,
}),
});
const main = b.addExecutable(.{
.name = "test",
.root_module = b.createModule(.{
.root_source_file = b.path("test.zig"),
.optimize = optimize,
.target = target,
}),
});
const run = b.addRunArtifact(main);
run.addArtifactArg(relative);
run.expectExitCode(0);
run.skip_foreign_checks = true;
test_step.dependOn(&run.step);
}

View File

@ -0,0 +1,19 @@
const std = @import("std");
pub fn main() !void {
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
defer std.debug.assert(gpa.deinit() == .ok);
const allocator = gpa.allocator();
const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);
if (args.len < 3) return error.MissingArgs;
const relative = try std.fs.path.relative(allocator, args[1], args[2]);
defer allocator.free(relative);
var stdout_writer = std.fs.File.stdout().writerStreaming(&.{});
const stdout = &stdout_writer.interface;
try stdout.writeAll(relative);
}

View File

@ -0,0 +1,131 @@
const std = @import("std");
pub fn main() anyerror!void {
var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena_state.deinit();
const arena = arena_state.allocator();
const args = try std.process.argsAlloc(arena);
if (args.len < 2) return error.MissingArgs;
const exe_path = args[1];
const cwd_path = try std.process.getCwdAlloc(arena);
const parsed_cwd_path = std.fs.path.parsePathWindows(u8, cwd_path);
if (parsed_cwd_path.kind == .drive_absolute and !std.ascii.isAlphabetic(cwd_path[0])) {
// Technically possible, but not worth supporting here
return error.NonAlphabeticDriveLetter;
}
const alt_drive_letter = try getAltDriveLetter(cwd_path);
const alt_drive_cwd_key = try std.fmt.allocPrint(arena, "={c}:", .{alt_drive_letter});
const alt_drive_cwd = try std.fmt.allocPrint(arena, "{c}:\\baz", .{alt_drive_letter});
var alt_drive_env_map = std.process.EnvMap.init(arena);
try alt_drive_env_map.put(alt_drive_cwd_key, alt_drive_cwd);
const empty_env = std.process.EnvMap.init(arena);
{
const drive_rel = try std.fmt.allocPrint(arena, "{c}:foo", .{alt_drive_letter});
const drive_abs = try std.fmt.allocPrint(arena, "{c}:\\bar", .{alt_drive_letter});
// With the special =X: environment variable set, drive-relative paths that
// don't match the CWD's drive letter are resolved against that env var.
try checkRelative(arena, "..\\..\\bar", &.{ exe_path, drive_rel, drive_abs }, null, &alt_drive_env_map);
try checkRelative(arena, "..\\baz\\foo", &.{ exe_path, drive_abs, drive_rel }, null, &alt_drive_env_map);
// Without that environment variable set, drive-relative paths that don't match the
// CWD's drive letter are resolved against the root of the drive.
try checkRelative(arena, "..\\bar", &.{ exe_path, drive_rel, drive_abs }, null, &empty_env);
try checkRelative(arena, "..\\foo", &.{ exe_path, drive_abs, drive_rel }, null, &empty_env);
// Bare drive-relative path with no components
try checkRelative(arena, "bar", &.{ exe_path, drive_rel[0..2], drive_abs }, null, &empty_env);
try checkRelative(arena, "..", &.{ exe_path, drive_abs, drive_rel[0..2] }, null, &empty_env);
// Bare drive-relative path with no components, drive-CWD set
try checkRelative(arena, "..\\bar", &.{ exe_path, drive_rel[0..2], drive_abs }, null, &alt_drive_env_map);
try checkRelative(arena, "..\\baz", &.{ exe_path, drive_abs, drive_rel[0..2] }, null, &alt_drive_env_map);
// Bare drive-relative path relative to the CWD should be equivalent if drive-CWD is set
try checkRelative(arena, "", &.{ exe_path, alt_drive_cwd, drive_rel[0..2] }, null, &alt_drive_env_map);
try checkRelative(arena, "", &.{ exe_path, drive_rel[0..2], alt_drive_cwd }, null, &alt_drive_env_map);
// Bare drive-relative should always be equivalent to itself
try checkRelative(arena, "", &.{ exe_path, drive_rel[0..2], drive_rel[0..2] }, null, &alt_drive_env_map);
try checkRelative(arena, "", &.{ exe_path, drive_rel[0..2], drive_rel[0..2] }, null, &alt_drive_env_map);
try checkRelative(arena, "", &.{ exe_path, drive_rel[0..2], drive_rel[0..2] }, null, &empty_env);
try checkRelative(arena, "", &.{ exe_path, drive_rel[0..2], drive_rel[0..2] }, null, &empty_env);
}
if (parsed_cwd_path.kind == .unc_absolute) {
const drive_abs_path = try std.fmt.allocPrint(arena, "{c}:\\foo\\bar", .{alt_drive_letter});
{
try checkRelative(arena, drive_abs_path, &.{ exe_path, cwd_path, drive_abs_path }, null, &empty_env);
try checkRelative(arena, cwd_path, &.{ exe_path, drive_abs_path, cwd_path }, null, &empty_env);
}
} else if (parsed_cwd_path.kind == .drive_absolute) {
const cur_drive_letter = parsed_cwd_path.root[0];
const path_beyond_root = cwd_path[3..];
const unc_cwd = try std.fmt.allocPrint(arena, "\\\\127.0.0.1\\{c}$\\{s}", .{ cur_drive_letter, path_beyond_root });
{
try checkRelative(arena, cwd_path, &.{ exe_path, unc_cwd, cwd_path }, null, &empty_env);
try checkRelative(arena, unc_cwd, &.{ exe_path, cwd_path, unc_cwd }, null, &empty_env);
}
{
const drive_abs = cwd_path;
const drive_rel = parsed_cwd_path.root[0..2];
try checkRelative(arena, "", &.{ exe_path, drive_abs, drive_rel }, null, &empty_env);
try checkRelative(arena, "", &.{ exe_path, drive_rel, drive_abs }, null, &empty_env);
}
} else {
return error.UnexpectedPathType;
}
}
fn checkRelative(
allocator: std.mem.Allocator,
expected_stdout: []const u8,
argv: []const []const u8,
cwd: ?[]const u8,
env_map: ?*const std.process.EnvMap,
) !void {
const result = try std.process.Child.run(.{
.allocator = allocator,
.argv = argv,
.cwd = cwd,
.env_map = env_map,
});
defer allocator.free(result.stdout);
defer allocator.free(result.stderr);
try std.testing.expectEqualStrings("", result.stderr);
try std.testing.expectEqualStrings(expected_stdout, result.stdout);
}
fn getAltDriveLetter(path: []const u8) !u8 {
const parsed = std.fs.path.parsePathWindows(u8, path);
return switch (parsed.kind) {
.drive_absolute => {
const cur_drive_letter = parsed.root[0];
const next_drive_letter_index = (std.ascii.toUpper(cur_drive_letter) - 'A' + 1) % 26;
const next_drive_letter = next_drive_letter_index + 'A';
return next_drive_letter;
},
.unc_absolute => {
return 'C';
},
else => return error.UnexpectedPathType,
};
}
test getAltDriveLetter {
try std.testing.expectEqual('D', try getAltDriveLetter("C:\\"));
try std.testing.expectEqual('B', try getAltDriveLetter("a:\\"));
try std.testing.expectEqual('A', try getAltDriveLetter("Z:\\"));
try std.testing.expectEqual('C', try getAltDriveLetter("\\\\foo\\bar"));
}