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). // (e.g. the NT \??\ prefix, the device \\.\ prefix, etc).
// Those path types are something of an unavoidable way to // Those path types are something of an unavoidable way to
// still hit unreachable during the openDir call. // 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| { while (component_iterator.next()) |component| {
// https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file // https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file
if (std.mem.indexOfAny(u8, component.name, "\x00<>:\"|?*") != null) return error.BadPathName; 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 { fn getPrefixSubpath(allocator: Allocator, prefix: []const u8, path: []u8) ![]u8 {
const relative = try fs.path.relative(allocator, prefix, path); const relative = try fs.path.relative(allocator, prefix, path);
errdefer allocator.free(relative); errdefer allocator.free(relative);
var component_iterator = fs.path.NativeComponentIterator.init(relative) catch { var component_iterator = fs.path.NativeComponentIterator.init(relative);
return error.NotASubPath;
};
if (component_iterator.root() != null) { if (component_iterator.root() != null) {
return error.NotASubPath; return error.NotASubPath;
} }

View file

@ -167,7 +167,7 @@ pub fn setPaths(fse: *FsEvents, gpa: Allocator, steps: []const *std.Build.Step)
}.lessThan); }.lessThan);
need_dirs.clearRetainingCapacity(); need_dirs.clearRetainingCapacity();
for (old_dirs) |dir_path| { 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| { while (it.next()) |component| {
if (need_dirs.contains(component.path)) { if (need_dirs.contains(component.path)) {
// this path is '/foo/bar/qux', but '/foo' or '/foo/bar' was already added // 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 /// Same as `makePath` except returns whether the path already existed or was
/// successfully created. /// successfully created.
pub fn makePathStatus(dir: Dir, io: Io, sub_path: []const u8) MakePathError!MakePathStatus { 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 status: MakePathStatus = .existed;
var component = it.last() orelse return error.BadPathName; var component = it.last() orelse return error.BadPathName;
while (true) { while (true) {

View file

@ -1210,7 +1210,7 @@ fn dirMakeOpenPathWindows(
w.SYNCHRONIZE | w.FILE_TRAVERSE | w.SYNCHRONIZE | w.FILE_TRAVERSE |
(if (options.iterate) w.FILE_LIST_DIRECTORY else @as(u32, 0)); (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. // 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 .{ var component: std.fs.path.NativeComponentIterator.Component = it.last() orelse .{
.name = "", .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. // 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; var fd_path_buf: [fs.max_path_bytes]u8 = undefined;
const dir_path = try std.os.getFdPath(dir.fd, &fd_path_buf); 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) { switch (windows_path_type) {
.unc_absolute => return fs.path.joinZ(allocator, &.{ dir_path, relative_path }), .unc_absolute => return fs.path.joinZ(allocator, &.{ dir_path, relative_path }),
.drive_absolute => { .drive_absolute => {

View file

@ -816,8 +816,11 @@ pub fn CreateSymbolicLink(
// https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createsymboliclinkw // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createsymboliclinkw
var is_target_absolute = false; var is_target_absolute = false;
const final_target_path = target_path: { const final_target_path = target_path: {
switch (getNamespacePrefix(u16, target_path)) { if (hasCommonNtPrefix(u16, target_path)) {
.none => switch (getUnprefixedPathType(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 // Rooted paths need to avoid getting put through wToPrefixedFileW
// (and they are treated as relative in this context) // (and they are treated as relative in this context)
// Note: It seems that rooted paths in symbolic links are relative to // 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. // Keep relative paths relative, but anything else needs to get NT-prefixed.
else => if (!std.fs.path.isAbsoluteWindowsWtf16(target_path)) else => if (!std.fs.path.isAbsoluteWindowsWtf16(target_path))
break :target_path 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); var prefixed_target_path = try wToPrefixedFileW(dir, target_path);
// We do this after prefixing to ensure that drive-relative paths are treated as absolute // 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 /// Compares two WTF16 strings using the equivalent functionality of
/// `RtlEqualUnicodeString` (with case insensitive comparison enabled). /// `RtlEqualUnicodeString` (with case insensitive comparison enabled).
/// This function can be called on any target. /// 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) { if (@inComptime() or builtin.os.tag != .windows) {
// This function compares the strings code unit by code unit (aka u16-to-u16), // 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 // 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 { 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, eqlIgnoreCaseWtf8(a, b));
try std.testing.expectEqual(expect_eql, eqlIgnoreCaseWTF16( try std.testing.expectEqual(expect_eql, eqlIgnoreCaseWtf16(
std.unicode.utf8ToUtf16LeStringLiteral(a), std.unicode.utf8ToUtf16LeStringLiteral(a),
std.unicode.utf8ToUtf16LeStringLiteral(b), std.unicode.utf8ToUtf16LeStringLiteral(b),
)); ));
try comptime std.testing.expect(expect_eql == eqlIgnoreCaseWtf8(a, 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(a),
std.unicode.utf8ToUtf16LeStringLiteral(b), std.unicode.utf8ToUtf16LeStringLiteral(b),
)); ));
} }
test "eqlIgnoreCaseWTF16/Wtf8" { test "eqlIgnoreCaseWtf16/Wtf8" {
try testEqlIgnoreCase(true, "\x01 a B Λ ɐ", "\x01 A b λ Ɐ"); try testEqlIgnoreCase(true, "\x01 a B Λ ɐ", "\x01 A b λ Ɐ");
// does not do case-insensitive comparison for codepoints >= U+10000 // does not do case-insensitive comparison for codepoints >= U+10000
try testEqlIgnoreCase(false, "𐓏", "𐓷"); try testEqlIgnoreCase(false, "𐓏", "𐓷");
@ -2365,158 +2365,309 @@ pub const Wtf16ToPrefixedFileWError = error{
/// - . and space are not stripped from the end of relative paths (potential TODO) /// - . and space are not stripped from the end of relative paths (potential TODO)
pub fn wToPrefixedFileW(dir: ?HANDLE, path: [:0]const u16) Wtf16ToPrefixedFileWError!PathSpace { pub fn wToPrefixedFileW(dir: ?HANDLE, path: [:0]const u16) Wtf16ToPrefixedFileWError!PathSpace {
const nt_prefix = [_]u16{ '\\', '?', '?', '\\' }; const nt_prefix = [_]u16{ '\\', '?', '?', '\\' };
switch (getNamespacePrefix(u16, path)) { if (hasCommonNtPrefix(u16, path)) {
// TODO: Figure out a way to design an API that can avoid the copy for .nt, // TODO: Figure out a way to design an API that can avoid the copy for NT,
// since it is always returned fully unmodified. // since it is always returned fully unmodified.
.nt, .verbatim => { var path_space: PathSpace = undefined;
var path_space: PathSpace = undefined; path_space.data[0..nt_prefix.len].* = nt_prefix;
path_space.data[0..nt_prefix.len].* = nt_prefix; const len_after_prefix = path.len - nt_prefix.len;
const len_after_prefix = path.len - nt_prefix.len; @memcpy(path_space.data[nt_prefix.len..][0..len_after_prefix], path[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.len = path.len; path_space.data[path_space.len] = 0;
path_space.data[path_space.len] = 0; return path_space;
return path_space; } else {
}, const path_type = getWin32PathType(u16, path);
.local_device, .fake_verbatim => { var path_space: PathSpace = undefined;
var path_space: PathSpace = undefined; if (path_type == .local_device) {
const path_byte_len = ntdll.RtlGetFullPathName_U( switch (getLocalDevicePathType(u16, path)) {
path.ptr, .verbatim => {
path_space.data.len * 2, path_space.data[0..nt_prefix.len].* = nt_prefix;
&path_space.data, const len_after_prefix = path.len - nt_prefix.len;
null, @memcpy(path_space.data[nt_prefix.len..][0..len_after_prefix], path[nt_prefix.len..]);
); path_space.len = path.len;
if (path_byte_len == 0) {
// TODO: This may not be the right error
return error.BadPathName;
} else if (path_byte_len / 2 > path_space.data.len) {
return error.NameTooLong;
}
path_space.len = path_byte_len / 2;
// Both prefixes will be normalized but retained, so all
// we need to do now is replace them with the NT prefix
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.
// See https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
// TODO: Potentially strip all trailing . and space characters from the
// end of the path. This is something that both RtlDosPathNameToNtPathName_U
// and RtlGetFullPathName_U do. Technically, trailing . and spaces
// are allowed, but such paths may not interact well with Windows (i.e.
// files with these paths can't be deleted from explorer.exe, etc).
// This could be something that normalizePath may want to do.
@memcpy(path_space.data[0..path.len], path);
// Try to normalize, but if we get too many parent directories,
// then we need to start over and use RtlGetFullPathName_U instead.
path_space.len = normalizePath(u16, path_space.data[0..path.len]) catch |err| switch (err) {
error.TooManyParentDirs => break :relative,
};
path_space.data[path_space.len] = 0; path_space.data[path_space.len] = 0;
return path_space; return path_space;
} },
.local_device, .fake_verbatim => {
const path_byte_len = ntdll.RtlGetFullPathName_U(
path.ptr,
path_space.data.len * 2,
&path_space.data,
null,
);
if (path_byte_len == 0) {
// TODO: This may not be the right error
return error.BadPathName;
} else if (path_byte_len / 2 > path_space.data.len) {
return error.NameTooLong;
}
path_space.len = path_byte_len / 2;
// Both prefixes will be normalized but retained, so all
// we need to do now is replace them with the NT prefix
path_space.data[0..nt_prefix.len].* = nt_prefix;
return path_space;
},
} }
// We now know we are going to return an absolute NT path, so }
// we can unconditionally prefix it with the NT prefix. relative: {
path_space.data[0..nt_prefix.len].* = nt_prefix; if (path_type == .relative) {
if (path_type == .root_local_device) { // TODO: Handle special case device names like COM1, AUX, NUL, CONIN$, CONOUT$, etc.
// `\\.` and `\\?` always get converted to `\??\` exactly, so // See https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
// we can just stop here
path_space.len = nt_prefix.len; // TODO: Potentially strip all trailing . and space characters from the
// end of the path. This is something that both RtlDosPathNameToNtPathName_U
// and RtlGetFullPathName_U do. Technically, trailing . and spaces
// are allowed, but such paths may not interact well with Windows (i.e.
// files with these paths can't be deleted from explorer.exe, etc).
// This could be something that normalizePath may want to do.
@memcpy(path_space.data[0..path.len], path);
// Try to normalize, but if we get too many parent directories,
// then we need to start over and use RtlGetFullPathName_U instead.
path_space.len = normalizePath(u16, path_space.data[0..path.len]) catch |err| switch (err) {
error.TooManyParentDirs => break :relative,
};
path_space.data[path_space.len] = 0; path_space.data[path_space.len] = 0;
return path_space; return path_space;
} }
const path_buf_offset = switch (path_type) { }
// UNC paths will always start with `\\`. However, we want to // We now know we are going to return an absolute NT path, so
// end up with something like `\??\UNC\server\share`, so to get // we can unconditionally prefix it with the NT prefix.
// RtlGetFullPathName to write into the spot we want the `server` path_space.data[0..nt_prefix.len].* = nt_prefix;
// part to end up, we need to provide an offset such that if (path_type == .root_local_device) {
// the `\\` part gets written where the `C\` of `UNC\` will be // `\\.` and `\\?` always get converted to `\??\` exactly, so
// in the final NT path. // we can just stop here
.unc_absolute => nt_prefix.len + 2, path_space.len = nt_prefix.len;
else => nt_prefix.len, path_space.data[path_space.len] = 0;
return path_space;
}
const path_buf_offset = switch (path_type) {
// UNC paths will always start with `\\`. However, we want to
// end up with something like `\??\UNC\server\share`, so to get
// RtlGetFullPathName to write into the spot we want the `server`
// part to end up, we need to provide an offset such that
// the `\\` part gets written where the `C\` of `UNC\` will be
// in the final NT path.
.unc_absolute => nt_prefix.len + 2,
else => nt_prefix.len,
};
const buf_len: u32 = @intCast(path_space.data.len - path_buf_offset);
const path_to_get: [:0]const u16 = path_to_get: {
// If dir is null, then we don't need to bother with GetFinalPathNameByHandle because
// RtlGetFullPathName_U will resolve relative paths against the CWD for us.
if (path_type != .relative or dir == null) {
break :path_to_get path;
}
// We can also skip GetFinalPathNameByHandle if the handle matches
// the handle returned by fs.cwd()
if (dir.? == std.fs.cwd().fd) {
break :path_to_get path;
}
// At this point, we know we have a relative path that had too many
// `..` components to be resolved by normalizePath, so we need to
// convert it into an absolute path and let RtlGetFullPathName_U
// canonicalize it. We do this by getting the path of the `dir`
// and appending the relative path to it.
var dir_path_buf: [PATH_MAX_WIDE:0]u16 = undefined;
const dir_path = GetFinalPathNameByHandle(dir.?, .{}, &dir_path_buf) catch |err| switch (err) {
// This mapping is not correct; it is actually expected
// that calling GetFinalPathNameByHandle might return
// error.UnrecognizedVolume, and in fact has been observed
// in the wild. The problem is that wToPrefixedFileW was
// never intended to make *any* OS syscall APIs. It's only
// supposed to convert a string to one that is eligible to
// be used in the ntdll syscalls.
//
// To solve this, this function needs to no longer call
// GetFinalPathNameByHandle under any conditions, or the
// calling function needs to get reworked to not need to
// call this function.
//
// This may involve making breaking API changes.
error.UnrecognizedVolume => return error.Unexpected,
else => |e| return e,
}; };
const buf_len: u32 = @intCast(path_space.data.len - path_buf_offset); if (dir_path.len + 1 + path.len > PATH_MAX_WIDE) {
const path_to_get: [:0]const u16 = path_to_get: {
// If dir is null, then we don't need to bother with GetFinalPathNameByHandle because
// RtlGetFullPathName_U will resolve relative paths against the CWD for us.
if (path_type != .relative or dir == null) {
break :path_to_get path;
}
// We can also skip GetFinalPathNameByHandle if the handle matches
// the handle returned by fs.cwd()
if (dir.? == std.fs.cwd().fd) {
break :path_to_get path;
}
// At this point, we know we have a relative path that had too many
// `..` components to be resolved by normalizePath, so we need to
// convert it into an absolute path and let RtlGetFullPathName_U
// canonicalize it. We do this by getting the path of the `dir`
// and appending the relative path to it.
var dir_path_buf: [PATH_MAX_WIDE:0]u16 = undefined;
const dir_path = GetFinalPathNameByHandle(dir.?, .{}, &dir_path_buf) catch |err| switch (err) {
// This mapping is not correct; it is actually expected
// that calling GetFinalPathNameByHandle might return
// error.UnrecognizedVolume, and in fact has been observed
// in the wild. The problem is that wToPrefixedFileW was
// never intended to make *any* OS syscall APIs. It's only
// supposed to convert a string to one that is eligible to
// be used in the ntdll syscalls.
//
// To solve this, this function needs to no longer call
// GetFinalPathNameByHandle under any conditions, or the
// calling function needs to get reworked to not need to
// call this function.
//
// This may involve making breaking API changes.
error.UnrecognizedVolume => return error.Unexpected,
else => |e| return e,
};
if (dir_path.len + 1 + path.len > PATH_MAX_WIDE) {
return error.NameTooLong;
}
// We don't have to worry about potentially doubling up path separators
// here since RtlGetFullPathName_U will handle canonicalizing it.
dir_path_buf[dir_path.len] = '\\';
@memcpy(dir_path_buf[dir_path.len + 1 ..][0..path.len], path);
const full_len = dir_path.len + 1 + path.len;
dir_path_buf[full_len] = 0;
break :path_to_get dir_path_buf[0..full_len :0];
};
const path_byte_len = ntdll.RtlGetFullPathName_U(
path_to_get.ptr,
buf_len * 2,
path_space.data[path_buf_offset..].ptr,
null,
);
if (path_byte_len == 0) {
// TODO: This may not be the right error
return error.BadPathName;
} else if (path_byte_len / 2 > buf_len) {
return error.NameTooLong; return error.NameTooLong;
} }
path_space.len = path_buf_offset + (path_byte_len / 2); // We don't have to worry about potentially doubling up path separators
if (path_type == .unc_absolute) { // here since RtlGetFullPathName_U will handle canonicalizing it.
// Now add in the UNC, the `C` should overwrite the first `\` of the dir_path_buf[dir_path.len] = '\\';
// FullPathName, ultimately resulting in `\??\UNC\<the rest of the path>` @memcpy(dir_path_buf[dir_path.len + 1 ..][0..path.len], path);
std.debug.assert(path_space.data[path_buf_offset] == '\\'); const full_len = dir_path.len + 1 + path.len;
std.debug.assert(path_space.data[path_buf_offset + 1] == '\\'); dir_path_buf[full_len] = 0;
const unc = [_]u16{ 'U', 'N', 'C' }; break :path_to_get dir_path_buf[0..full_len :0];
path_space.data[nt_prefix.len..][0..unc.len].* = unc; };
} const path_byte_len = ntdll.RtlGetFullPathName_U(
return path_space; path_to_get.ptr,
}, buf_len * 2,
path_space.data[path_buf_offset..].ptr,
null,
);
if (path_byte_len == 0) {
// TODO: This may not be the right error
return error.BadPathName;
} else if (path_byte_len / 2 > buf_len) {
return error.NameTooLong;
}
path_space.len = path_buf_offset + (path_byte_len / 2);
if (path_type == .unc_absolute) {
// Now add in the UNC, the `C` should overwrite the first `\` of the
// FullPathName, ultimately resulting in `\??\UNC\<the rest of the path>`
std.debug.assert(path_space.data[path_buf_offset] == '\\');
std.debug.assert(path_space.data[path_buf_offset + 1] == '\\');
const unc = [_]u16{ 'U', 'N', 'C' };
path_space.data[nt_prefix.len..][0..unc.len].* = unc;
}
return path_space;
} }
} }
pub const NamespacePrefix = enum { /// Similar to `RTL_PATH_TYPE`, but without the `UNKNOWN` path type.
none, 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 `/`) /// `\\.\` (path separators can be `\` or `/`)
local_device, local_device,
/// `\\?\` /// `\\?\`
@ -2529,107 +2680,24 @@ pub const NamespacePrefix = enum {
/// it will become `\??\C:\foo` [it will be canonicalized and the //?/ won't /// it will become `\??\C:\foo` [it will be canonicalized and the //?/ won't
/// be treated as part of the final path]) /// be treated as part of the final path])
fake_verbatim, fake_verbatim,
/// `\??\`
nt,
}; };
/// If `T` is `u16`, then `path` should be encoded as WTF-16LE. /// Only relevant for Win32 -> NT path conversion.
pub fn getNamespacePrefix(comptime T: type, path: []const T) NamespacePrefix { /// Asserts `path` is of type `Win32PathType.local_device`.
if (path.len < 4) return .none; fn getLocalDevicePathType(comptime T: type, path: []const T) LocalDevicePathType {
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;
if (std.debug.runtime_safety) { 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; const backslash = mem.nativeToLittle(T, '\\');
if (windows_path.isSep(T, mem.littleToNative(T, path[0]))) { const all_backslash = path[0] == backslash and
// \x path[1] == backslash and
if (path.len < 2 or !windows_path.isSep(T, mem.littleToNative(T, path[1]))) return .rooted; path[3] == backslash;
// exactly \\. or \\? with nothing trailing return switch (path[2]) {
if (path.len == 3 and (mem.littleToNative(T, path[2]) == '.' or mem.littleToNative(T, path[2]) == '?')) return .root_local_device; mem.nativeToLittle(T, '?') => if (all_backslash) .verbatim else .fake_verbatim,
// \\x mem.nativeToLittle(T, '.') => .local_device,
return .unc_absolute; else => unreachable,
} 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"));
} }
/// Similar to `RtlNtPathNameToDosPathName` but does not do any heap allocation. /// Similar to `RtlNtPathNameToDosPathName` but does not do any heap allocation.
@ -2646,30 +2714,25 @@ test getUnprefixedPathType {
/// Supports in-place modification (`path` and `out` may refer to the same slice). /// 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 { pub fn ntToWin32Namespace(path: []const u16, out: []u16) error{ NameTooLong, NotNtPath }![]u16 {
if (path.len > PATH_MAX_WIDE) return error.NameTooLong; if (path.len > PATH_MAX_WIDE) return error.NameTooLong;
if (!hasCommonNtPrefix(u16, path)) return error.NotNtPath;
const namespace_prefix = getNamespacePrefix(u16, path); var dest_index: usize = 0;
switch (namespace_prefix) { var after_prefix = path[4..]; // after the `\??\`
.nt => { // The prefix \??\UNC\ means this is a UNC path, in which case the
var dest_index: usize = 0; // `\??\UNC\` should be replaced by `\\` (two backslashes)
var after_prefix = path[4..]; // after the `\??\` const is_unc = after_prefix.len >= 4 and
// The prefix \??\UNC\ means this is a UNC path, in which case the eqlIgnoreCaseWtf16(after_prefix[0..3], std.unicode.utf8ToUtf16LeStringLiteral("UNC")) and
// `\??\UNC\` should be replaced by `\\` (two backslashes) std.fs.path.PathType.windows.isSep(u16, after_prefix[3]);
const is_unc = after_prefix.len >= 4 and const win32_len = path.len - @as(usize, if (is_unc) 6 else 4);
eqlIgnoreCaseWTF16(after_prefix[0..3], std.unicode.utf8ToUtf16LeStringLiteral("UNC")) and if (out.len < win32_len) return error.NameTooLong;
std.fs.path.PathType.windows.isSep(u16, std.mem.littleToNative(u16, after_prefix[3])); if (is_unc) {
const win32_len = path.len - @as(usize, if (is_unc) 6 else 4); out[0] = comptime std.mem.nativeToLittle(u16, '\\');
if (out.len < win32_len) return error.NameTooLong; dest_index += 1;
if (is_unc) { // We want to include the last `\` of `\??\UNC\`
out[0] = comptime std.mem.nativeToLittle(u16, '\\'); after_prefix = path[7..];
dest_index += 1;
// We want to include the last `\` of `\??\UNC\`
after_prefix = path[7..];
}
@memmove(out[dest_index..][0..after_prefix.len], after_prefix);
return out[0..win32_len];
},
else => return error.NotNtPath,
} }
@memmove(out[dest_index..][0..after_prefix.len], after_prefix);
return out[0..win32_len];
} }
test ntToWin32Namespace { test ntToWin32Namespace {

View file

@ -54,8 +54,7 @@ fn testToPrefixedFileOnlyOracle(comptime path: []const u8) !void {
} }
test "toPrefixedFileW" { test "toPrefixedFileW" {
if (builtin.os.tag != .windows) if (builtin.os.tag != .windows) return error.SkipZigTest;
return;
// Most test cases come from https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html // 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 // 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\\..\\", "a\\");
try testRemoveDotDirs("a\\b\\..\\c", "a\\c"); 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`. /// 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 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. /// 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); 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. /// Caller must free the returned memory.
/// On Windows, the result is encoded as [WTF-8](https://wtf-8.codeberg.page/). /// 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. /// 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 // 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. // in stack_buf, avoiding an extra allocation in the common case.
var stack_buf: [fs.max_path_bytes]u8 = undefined; 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. /// 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. /// 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]; 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 ..]; 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 app_name = app_buf.items[0..app_name_len];
const ext_start = std.mem.lastIndexOfScalar(u16, app_name, '.') orelse break :unappended err; const ext_start = std.mem.lastIndexOfScalar(u16, app_name, '.') orelse break :unappended err;
const ext = app_name[ext_start..]; const ext = app_name[ext_start..];
if (windows.eqlIgnoreCaseWTF16(ext, unicode.utf8ToUtf16LeStringLiteral(".EXE"))) { if (windows.eqlIgnoreCaseWtf16(ext, unicode.utf8ToUtf16LeStringLiteral(".EXE"))) {
return error.UnrecoverableInvalidExe; return error.UnrecoverableInvalidExe;
} }
break :unappended err; break :unappended err;
@ -1278,7 +1278,7 @@ fn windowsCreateProcessPathExt(
// On InvalidExe, if the extension of the app name is .exe then // On InvalidExe, if the extension of the app name is .exe then
// it's treated as an unrecoverable error. Otherwise, it'll be // it's treated as an unrecoverable error. Otherwise, it'll be
// skipped as normal. // skipped as normal.
if (windows.eqlIgnoreCaseWTF16(ext, unicode.utf8ToUtf16LeStringLiteral(".EXE"))) { if (windows.eqlIgnoreCaseWtf16(ext, unicode.utf8ToUtf16LeStringLiteral(".EXE"))) {
return error.UnrecoverableInvalidExe; return error.UnrecoverableInvalidExe;
} }
continue; continue;

View file

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

View file

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

View file

@ -126,6 +126,9 @@
.windows_bat_args = .{ .windows_bat_args = .{
.path = "windows_bat_args", .path = "windows_bat_args",
}, },
.windows_paths = .{
.path = "windows_paths",
},
.self_exe_symlink = .{ .self_exe_symlink = .{
.path = "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"));
}