Teach fs.path about the wonderful world of Windows paths

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
This commit is contained in:
Ryan Liptak 2025-10-27 21:49:18 -07:00
parent 26afcdb7fe
commit 59b8bed222
15 changed files with 1690 additions and 766 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

@ -1154,7 +1154,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

@ -836,8 +836,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
@ -849,10 +852,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
@ -2308,158 +2308,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, mem.littleToNative(T, path[0]))) {
// \x
if (path.len < 2 or !windows_path.isSep(T, mem.littleToNative(T, path[1]))) return .rooted;
// \\. or \\?
if (path.len > 2 and (mem.littleToNative(T, path[2]) == '.' or mem.littleToNative(T, path[2]) == '?')) {
// exactly \\. or \\? with nothing trailing
if (path.len == 3) return .root_local_device;
// \\.\x or \\?\x
if (windows_path.isSep(T, mem.littleToNative(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 mem.littleToNative(T, path[colon_i]) != ':') return .relative;
// x:\
if (path.len > colon_i + 1 and windows_path.isSep(T, mem.littleToNative(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,
/// `\\?\` /// `\\?\`
@ -2472,109 +2623,25 @@ 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])) { if (std.debug.runtime_safety) {
'\\' => true, std.debug.assert(getWin32PathType(T, path) == .local_device);
'/' => 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,
} }
const all_backslash = mem.littleToNative(T, path[0]) == '\\' and
mem.littleToNative(T, path[1]) == '\\' and
mem.littleToNative(T, path[3]) == '\\';
return switch (mem.littleToNative(T, path[2])) { return switch (mem.littleToNative(T, path[2])) {
'?' => if (all_backslash) .verbatim else .fake_verbatim, '?' => if (all_backslash) .verbatim else .fake_verbatim,
'.' => .local_device, '.' => .local_device,
else => .none, else => unreachable,
}; };
} }
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) {
std.debug.assert(getNamespacePrefix(T, path) == .none);
}
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"));
}
/// Similar to `RtlNtPathNameToDosPathName` but does not do any heap allocation. /// Similar to `RtlNtPathNameToDosPathName` but does not do any heap allocation.
/// The possible transformations are: /// The possible transformations are:
/// \??\C:\Some\Path -> C:\Some\Path /// \??\C:\Some\Path -> C:\Some\Path
@ -2589,30 +2656,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, std.mem.littleToNative(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

@ -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"));
}