std: move GetFinalPathNameByHandle to Io.Threaded

unfortunately this function calls NtCreateFile so it has to participate
in cancelation
This commit is contained in:
Andrew Kelley 2026-02-03 21:08:42 -08:00
parent b49dc5eb70
commit 2a193a3987
8 changed files with 907 additions and 1006 deletions

View file

@ -358,7 +358,7 @@ const Os = switch (builtin.os.tag) {
var dir_handle: windows.HANDLE = undefined;
const root_fd = path.root_dir.handle.handle;
const sub_path = path.subPathOrDot();
const sub_path_w = try windows.sliceToPrefixedFileW(root_fd, sub_path);
const sub_path_w = try std.Io.Threaded.sliceToPrefixedFileW(root_fd, sub_path); // TODO eliminate this call
const path_len_bytes = std.math.cast(u16, sub_path_w.len * 2) orelse return error.NameTooLong;
var nt_name = windows.UNICODE_STRING{

View file

@ -3257,7 +3257,7 @@ fn dirCreateDirWindows(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, pe
const t: *Threaded = @ptrCast(@alignCast(userdata));
_ = t;
const sub_path_w = try windows.sliceToPrefixedFileW(dir.handle, sub_path);
const sub_path_w = try sliceToPrefixedFileW(dir.handle, sub_path);
_ = permissions; // TODO use this value
const syscall: Syscall = try .start();
@ -3363,7 +3363,7 @@ fn dirCreateDirPathOpenWindows(
};
components: while (true) {
const sub_path_w_array = try w.sliceToPrefixedFileW(dir.handle, component.path);
const sub_path_w_array = try sliceToPrefixedFileW(dir.handle, component.path);
const sub_path_w = sub_path_w_array.span();
const is_last = it.peekNext() == null;
const create_disposition: w.FILE.CREATE_DISPOSITION = if (is_last) .OPEN_IF else .CREATE;
@ -4064,7 +4064,7 @@ fn dirAccessWindows(
_ = options; // TODO
const sub_path_w_array = try windows.sliceToPrefixedFileW(dir.handle, sub_path);
const sub_path_w_array = try sliceToPrefixedFileW(dir.handle, sub_path);
const sub_path_w = sub_path_w_array.span();
if (sub_path_w[0] == '.' and sub_path_w[1] == 0) return;
@ -4285,7 +4285,7 @@ fn dirCreateFileWindows(
if (std.mem.eql(u8, sub_path, ".")) return error.IsDir;
if (std.mem.eql(u8, sub_path, "..")) return error.IsDir;
const sub_path_w_array = try windows.sliceToPrefixedFileW(dir.handle, sub_path);
const sub_path_w_array = try sliceToPrefixedFileW(dir.handle, sub_path);
const sub_path_w = sub_path_w_array.span();
const path_len_bytes = std.math.cast(u16, sub_path_w.len * 2) orelse return error.NameTooLong;
@ -4887,7 +4887,7 @@ fn dirOpenFileWindows(
) File.OpenError!File {
const t: *Threaded = @ptrCast(@alignCast(userdata));
_ = t;
const sub_path_w_array = try windows.sliceToPrefixedFileW(dir.handle, sub_path);
const sub_path_w_array = try sliceToPrefixedFileW(dir.handle, sub_path);
const sub_path_w = sub_path_w_array.span();
const dir_handle = if (Dir.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle;
return dirOpenFileWtf16(dir_handle, sub_path_w, flags);
@ -5151,7 +5151,7 @@ fn dirOpenDirPosix(
_ = t;
if (is_windows) {
const sub_path_w = try windows.sliceToPrefixedFileW(dir.handle, sub_path);
const sub_path_w = try sliceToPrefixedFileW(dir.handle, sub_path);
return dirOpenDirWindows(dir, sub_path_w.span(), options);
}
@ -5984,7 +5984,7 @@ fn dirRealPathFileWindows(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8,
const t: *Threaded = @ptrCast(@alignCast(userdata));
_ = t;
var path_name_w = try windows.sliceToPrefixedFileW(dir.handle, sub_path);
var path_name_w = try sliceToPrefixedFileW(dir.handle, sub_path);
const h_file = handle: {
const syscall: Syscall = try .start();
@ -6016,9 +6016,7 @@ fn dirRealPathFileWindows(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8,
fn realPathWindows(h_file: windows.HANDLE, out_buffer: []u8) File.RealPathError!usize {
var wide_buf: [windows.PATH_MAX_WIDE]u16 = undefined;
// TODO move GetFinalPathNameByHandle logic into Io.Threaded and add cancel checks
try Thread.checkCancel();
const wide_slice = try windows.GetFinalPathNameByHandle(h_file, .{}, &wide_buf);
const wide_slice = try GetFinalPathNameByHandle(h_file, .{}, &wide_buf);
const len = std.unicode.calcWtf8Len(wide_slice);
if (len > out_buffer.len)
@ -6027,6 +6025,552 @@ fn realPathWindows(h_file: windows.HANDLE, out_buffer: []u8) File.RealPathError!
return std.unicode.wtf16LeToWtf8(out_buffer, wide_slice);
}
/// Specifies how to format volume path in the result of `GetFinalPathNameByHandle`.
/// Defaults to DOS volume names.
pub const GetFinalPathNameByHandleFormat = struct {
volume_name: enum {
/// Format as DOS volume name
Dos,
/// Format as NT volume name
Nt,
} = .Dos,
};
pub const GetFinalPathNameByHandleError = error{
AccessDenied,
FileNotFound,
NameTooLong,
/// The volume does not contain a recognized file system. File system
/// drivers might not be loaded, or the volume may be corrupt.
UnrecognizedVolume,
} || Io.Cancelable || Io.UnexpectedError;
/// Returns canonical (normalized) path of handle.
/// Use `GetFinalPathNameByHandleFormat` to specify whether the path is meant to include
/// NT or DOS volume name (e.g., `\Device\HarddiskVolume0\foo.txt` versus `C:\foo.txt`).
/// If DOS volume name format is selected, note that this function does *not* prepend
/// `\\?\` prefix to the resultant path.
pub fn GetFinalPathNameByHandle(
hFile: windows.HANDLE,
fmt: GetFinalPathNameByHandleFormat,
out_buffer: []u16,
) GetFinalPathNameByHandleError![]u16 {
const final_path = QueryObjectName(hFile, out_buffer) catch |err| switch (err) {
// we assume InvalidHandle is close enough to FileNotFound in semantics
// to not further complicate the error set
error.InvalidHandle => return error.FileNotFound,
else => |e| return e,
};
switch (fmt.volume_name) {
.Nt => {
// the returned path is already in .Nt format
return final_path;
},
.Dos => {
// parse the string to separate volume path from file path
const device_prefix = std.unicode.utf8ToUtf16LeStringLiteral("\\Device\\");
// We aren't entirely sure of the structure of the path returned by
// QueryObjectName in all contexts/environments.
// This code is written to cover the various cases that have
// been encountered and solved appropriately. But note that there's
// no easy way to verify that they have all been tackled!
// (Unless you, the reader knows of one then please do action that!)
if (!std.mem.startsWith(u16, final_path, device_prefix)) {
// Wine seems to return NT namespaced paths starting with \??\ from QueryObjectName
// (e.g. `\??\Z:\some\path\to\a\file.txt`), in which case we can just strip the
// prefix to turn it into an absolute path.
// https://github.com/ziglang/zig/issues/26029
// https://bugs.winehq.org/show_bug.cgi?id=39569
return windows.ntToWin32Namespace(final_path, out_buffer) catch |err| switch (err) {
error.NotNtPath => return error.Unexpected,
error.NameTooLong => |e| return e,
};
}
const file_path_begin_index = std.mem.findPos(u16, final_path, device_prefix.len, &[_]u16{'\\'}) orelse unreachable;
const volume_name_u16 = final_path[0..file_path_begin_index];
const device_name_u16 = volume_name_u16[device_prefix.len..];
const file_name_u16 = final_path[file_path_begin_index..];
// MUP is Multiple UNC Provider, and indicates that the path is a UNC
// path. In this case, the canonical UNC path can be gotten by just
// dropping the \Device\Mup\ and making sure the path begins with \\
if (std.mem.eql(u16, device_name_u16, std.unicode.utf8ToUtf16LeStringLiteral("Mup"))) {
out_buffer[0] = '\\';
@memmove(out_buffer[1..][0..file_name_u16.len], file_name_u16);
return out_buffer[0 .. 1 + file_name_u16.len];
}
// Get DOS volume name. DOS volume names are actually symbolic link objects to the
// actual NT volume. For example:
// (NT) \Device\HarddiskVolume4 => (DOS) \DosDevices\C: == (DOS) C:
const MIN_SIZE = @sizeOf(windows.MOUNTMGR_MOUNT_POINT) + windows.MAX_PATH;
// We initialize the input buffer to all zeros for convenience since
// `DeviceIoControl` with `IOCTL_MOUNTMGR_QUERY_POINTS` expects this.
var input_buf: [MIN_SIZE]u8 align(@alignOf(windows.MOUNTMGR_MOUNT_POINT)) = [_]u8{0} ** MIN_SIZE;
var output_buf: [MIN_SIZE * 4]u8 align(@alignOf(windows.MOUNTMGR_MOUNT_POINTS)) = undefined;
// This surprising path is a filesystem path to the mount manager on Windows.
// Source: https://stackoverflow.com/questions/3012828/using-ioctl-mountmgr-query-points
// This is the NT namespaced version of \\.\MountPointManager
const mgmt_path_u16 = std.unicode.utf8ToUtf16LeStringLiteral("\\??\\MountPointManager");
const mgmt_handle = windows.OpenFile(mgmt_path_u16, .{
.access_mask = .{ .STANDARD = .{ .SYNCHRONIZE = true } },
.creation = .OPEN,
}) catch |err| switch (err) {
error.IsDir => return error.Unexpected,
error.NotDir => return error.Unexpected,
error.NoDevice => return error.Unexpected,
error.AccessDenied => return error.Unexpected,
error.PipeBusy => return error.Unexpected,
error.PathAlreadyExists => return error.Unexpected,
error.WouldBlock => return error.Unexpected,
error.NetworkNotFound => return error.Unexpected,
error.AntivirusInterference => return error.Unexpected,
error.BadPathName => return error.Unexpected,
error.OperationCanceled => @panic("TODO: better integrate cancelation"),
else => |e| return e,
};
defer windows.CloseHandle(mgmt_handle);
var input_struct: *windows.MOUNTMGR_MOUNT_POINT = @ptrCast(&input_buf[0]);
input_struct.DeviceNameOffset = @sizeOf(windows.MOUNTMGR_MOUNT_POINT);
input_struct.DeviceNameLength = @intCast(volume_name_u16.len * 2);
@memcpy(input_buf[@sizeOf(windows.MOUNTMGR_MOUNT_POINT)..][0 .. volume_name_u16.len * 2], @as([*]const u8, @ptrCast(volume_name_u16.ptr)));
{
const rc = windows.DeviceIoControl(mgmt_handle, windows.IOCTL.MOUNTMGR.QUERY_POINTS, .{ .in = &input_buf, .out = &output_buf });
switch (rc) {
.SUCCESS => {},
.OBJECT_NAME_NOT_FOUND => return error.FileNotFound,
else => return windows.unexpectedStatus(rc),
}
}
const mount_points_struct: *const windows.MOUNTMGR_MOUNT_POINTS = @ptrCast(&output_buf[0]);
const mount_points = @as(
[*]const windows.MOUNTMGR_MOUNT_POINT,
@ptrCast(&mount_points_struct.MountPoints[0]),
)[0..mount_points_struct.NumberOfMountPoints];
for (mount_points) |mount_point| {
const symlink = @as(
[*]const u16,
@ptrCast(@alignCast(&output_buf[mount_point.SymbolicLinkNameOffset])),
)[0 .. mount_point.SymbolicLinkNameLength / 2];
// Look for `\DosDevices\` prefix. We don't really care if there are more than one symlinks
// with traditional DOS drive letters, so pick the first one available.
var prefix_buf = std.unicode.utf8ToUtf16LeStringLiteral("\\DosDevices\\");
const prefix = prefix_buf[0..prefix_buf.len];
if (std.mem.startsWith(u16, symlink, prefix)) {
const drive_letter = symlink[prefix.len..];
if (out_buffer.len < drive_letter.len + file_name_u16.len) return error.NameTooLong;
@memcpy(out_buffer[0..drive_letter.len], drive_letter);
@memmove(out_buffer[drive_letter.len..][0..file_name_u16.len], file_name_u16);
const total_len = drive_letter.len + file_name_u16.len;
// Validate that DOS does not contain any spurious nul bytes.
assert(std.mem.findScalar(u16, out_buffer[0..total_len], 0) == null);
return out_buffer[0..total_len];
} else if (mountmgrIsVolumeName(symlink)) {
// If the symlink is a volume GUID like \??\Volume{383da0b0-717f-41b6-8c36-00500992b58d},
// then it is a volume mounted as a path rather than a drive letter. We need to
// query the mount manager again to get the DOS path for the volume.
// 49 is the maximum length accepted by mountmgrIsVolumeName
const vol_input_size = @sizeOf(windows.MOUNTMGR_TARGET_NAME) + (49 * 2);
var vol_input_buf: [vol_input_size]u8 align(@alignOf(windows.MOUNTMGR_TARGET_NAME)) = [_]u8{0} ** vol_input_size;
// Note: If the path exceeds MAX_PATH, the Disk Management GUI doesn't accept the full path,
// and instead if must be specified using a shortened form (e.g. C:\FOO~1\BAR~1\<...>).
// However, just to be sure we can handle any path length, we use PATH_MAX_WIDE here.
const min_output_size = @sizeOf(windows.MOUNTMGR_VOLUME_PATHS) + (windows.PATH_MAX_WIDE * 2);
var vol_output_buf: [min_output_size]u8 align(@alignOf(windows.MOUNTMGR_VOLUME_PATHS)) = undefined;
var vol_input_struct: *windows.MOUNTMGR_TARGET_NAME = @ptrCast(&vol_input_buf[0]);
vol_input_struct.DeviceNameLength = @intCast(symlink.len * 2);
@memcpy(@as([*]windows.WCHAR, &vol_input_struct.DeviceName)[0..symlink.len], symlink);
const rc = windows.DeviceIoControl(mgmt_handle, windows.IOCTL.MOUNTMGR.QUERY_DOS_VOLUME_PATH, .{ .in = &vol_input_buf, .out = &vol_output_buf });
switch (rc) {
.SUCCESS => {},
.UNRECOGNIZED_VOLUME => return error.UnrecognizedVolume,
else => return windows.unexpectedStatus(rc),
}
const volume_paths_struct: *const windows.MOUNTMGR_VOLUME_PATHS = @ptrCast(&vol_output_buf[0]);
const volume_path = std.mem.sliceTo(@as(
[*]const u16,
&volume_paths_struct.MultiSz,
)[0 .. volume_paths_struct.MultiSzLength / 2], 0);
if (out_buffer.len < volume_path.len + file_name_u16.len) return error.NameTooLong;
// `out_buffer` currently contains the memory of `file_name_u16`, so it can overlap with where
// we want to place the filename before returning. Here are the possible overlapping cases:
//
// out_buffer: [filename]
// dest: [___(a)___] [___(b)___]
//
// In the case of (a), we need to copy forwards, and in the case of (b) we need
// to copy backwards. We also need to do this before copying the volume path because
// it could overwrite the file_name_u16 memory.
const file_name_dest = out_buffer[volume_path.len..][0..file_name_u16.len];
@memmove(file_name_dest, file_name_u16);
@memcpy(out_buffer[0..volume_path.len], volume_path);
const total_len = volume_path.len + file_name_u16.len;
// Validate that DOS does not contain any spurious nul bytes.
assert(std.mem.findScalar(u16, out_buffer[0..total_len], 0) == null);
return out_buffer[0..total_len];
}
}
// If we've ended up here, then something went wrong/is corrupted in the OS,
// so error out!
return error.FileNotFound;
},
}
}
test GetFinalPathNameByHandle {
if (builtin.os.tag != .windows)
return;
//any file will do
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const handle = tmp.dir.handle;
var buffer: [windows.PATH_MAX_WIDE]u16 = undefined;
//check with sufficient size
const nt_path = try GetFinalPathNameByHandle(handle, .{ .volume_name = .Nt }, &buffer);
_ = try GetFinalPathNameByHandle(handle, .{ .volume_name = .Dos }, &buffer);
const required_len_in_u16 = nt_path.len + @divExact(@intFromPtr(nt_path.ptr) - @intFromPtr(&buffer), 2) + 1;
//check with insufficient size
try std.testing.expectError(error.NameTooLong, GetFinalPathNameByHandle(handle, .{ .volume_name = .Nt }, buffer[0 .. required_len_in_u16 - 1]));
try std.testing.expectError(error.NameTooLong, GetFinalPathNameByHandle(handle, .{ .volume_name = .Dos }, buffer[0 .. required_len_in_u16 - 1]));
//check with exactly-sufficient size
_ = try GetFinalPathNameByHandle(handle, .{ .volume_name = .Nt }, buffer[0..required_len_in_u16]);
_ = try GetFinalPathNameByHandle(handle, .{ .volume_name = .Dos }, buffer[0..required_len_in_u16]);
}
/// Equivalent to the MOUNTMGR_IS_VOLUME_NAME macro in mountmgr.h
fn mountmgrIsVolumeName(name: []const u16) bool {
return (name.len == 48 or (name.len == 49 and name[48] == std.mem.nativeToLittle(u16, '\\'))) and
name[0] == std.mem.nativeToLittle(u16, '\\') and
(name[1] == std.mem.nativeToLittle(u16, '?') or name[1] == std.mem.nativeToLittle(u16, '\\')) and
name[2] == std.mem.nativeToLittle(u16, '?') and
name[3] == std.mem.nativeToLittle(u16, '\\') and
std.mem.startsWith(u16, name[4..], std.unicode.utf8ToUtf16LeStringLiteral("Volume{")) and
name[19] == std.mem.nativeToLittle(u16, '-') and
name[24] == std.mem.nativeToLittle(u16, '-') and
name[29] == std.mem.nativeToLittle(u16, '-') and
name[34] == std.mem.nativeToLittle(u16, '-') and
name[47] == std.mem.nativeToLittle(u16, '}');
}
test mountmgrIsVolumeName {
@setEvalBranchQuota(2000);
const L = std.unicode.utf8ToUtf16LeStringLiteral;
try std.testing.expect(mountmgrIsVolumeName(L("\\\\?\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}")));
try std.testing.expect(mountmgrIsVolumeName(L("\\??\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}")));
try std.testing.expect(mountmgrIsVolumeName(L("\\\\?\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}\\")));
try std.testing.expect(mountmgrIsVolumeName(L("\\??\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}\\")));
try std.testing.expect(!mountmgrIsVolumeName(L("\\\\.\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}")));
try std.testing.expect(!mountmgrIsVolumeName(L("\\??\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}\\foo")));
try std.testing.expect(!mountmgrIsVolumeName(L("\\??\\Volume{383da0b0-717f-41b6-8c36-00500992b58}")));
}
pub const QueryObjectNameError = error{
AccessDenied,
InvalidHandle,
NameTooLong,
Unexpected,
};
pub fn QueryObjectName(handle: windows.HANDLE, out_buffer: []u16) QueryObjectNameError![]u16 {
const out_buffer_aligned = std.mem.alignInSlice(out_buffer, @alignOf(windows.OBJECT_NAME_INFORMATION)) orelse return error.NameTooLong;
const info: *windows.OBJECT_NAME_INFORMATION = @ptrCast(out_buffer_aligned);
// buffer size is specified in bytes
const out_buffer_len = std.math.cast(windows.ULONG, out_buffer_aligned.len * 2) orelse std.math.maxInt(windows.ULONG);
// last argument would return the length required for full_buffer, not exposed here
return switch (windows.ntdll.NtQueryObject(handle, .ObjectNameInformation, info, out_buffer_len, null)) {
.SUCCESS => blk: {
// info.Name.Buffer from ObQueryNameString is documented to be null (and MaximumLength == 0)
// if the object was "unnamed", not sure if this can happen for file handles
if (info.Name.MaximumLength == 0) break :blk error.Unexpected;
// resulting string length is specified in bytes
const path_length_unterminated = @divExact(info.Name.Length, 2);
break :blk info.Name.Buffer.?[0..path_length_unterminated];
},
.ACCESS_DENIED => error.AccessDenied,
.INVALID_HANDLE => error.InvalidHandle,
// triggered when the buffer is too small for the OBJECT_NAME_INFORMATION object (.INFO_LENGTH_MISMATCH),
// or if the buffer is too small for the file path returned (.BUFFER_OVERFLOW, .BUFFER_TOO_SMALL)
.INFO_LENGTH_MISMATCH, .BUFFER_OVERFLOW, .BUFFER_TOO_SMALL => error.NameTooLong,
else => |e| windows.unexpectedStatus(e),
};
}
test QueryObjectName {
if (builtin.os.tag != .windows)
return;
//any file will do; canonicalization works on NTFS junctions and symlinks, hardlinks remain separate paths.
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const handle = tmp.dir.handle;
var out_buffer: [windows.PATH_MAX_WIDE]u16 = undefined;
const result_path = try QueryObjectName(handle, &out_buffer);
const required_len_in_u16 = result_path.len + @divExact(@intFromPtr(result_path.ptr) - @intFromPtr(&out_buffer), 2) + 1;
//insufficient size
try std.testing.expectError(error.NameTooLong, QueryObjectName(handle, out_buffer[0 .. required_len_in_u16 - 1]));
//exactly-sufficient size
_ = try QueryObjectName(handle, out_buffer[0..required_len_in_u16]);
}
const Wtf16ToPrefixedFileWError = error{
AccessDenied,
FileNotFound,
} || Dir.PathNameError || Io.Cancelable || Io.UnexpectedError;
/// Converts the `path` to WTF16, null-terminated. If the path contains any
/// namespace prefix, or is anything but a relative path (rooted, drive relative,
/// etc) the result will have the NT-style prefix `\??\`.
///
/// Similar to RtlDosPathNameToNtPathName_U with a few differences:
/// - Does not allocate on the heap.
/// - Relative paths are kept as relative unless they contain too many ..
/// components, in which case they are resolved against the `dir` if it
/// is non-null, or the CWD if it is null.
/// - Special case device names like COM1, NUL, etc are not handled specially (TODO)
/// - . and space are not stripped from the end of relative paths (potential TODO)
pub fn wToPrefixedFileW(dir: ?windows.HANDLE, path: [:0]const u16) Wtf16ToPrefixedFileWError!WindowsPathSpace {
const nt_prefix = [_]u16{ '\\', '?', '?', '\\' };
if (windows.hasCommonNtPrefix(u16, path)) {
// TODO: Figure out a way to design an API that can avoid the copy for NT,
// since it is always returned fully unmodified.
var path_space: WindowsPathSpace = undefined;
path_space.data[0..nt_prefix.len].* = nt_prefix;
const len_after_prefix = path.len - nt_prefix.len;
@memcpy(path_space.data[nt_prefix.len..][0..len_after_prefix], path[nt_prefix.len..]);
path_space.len = path.len;
path_space.data[path_space.len] = 0;
return path_space;
} else {
const path_type = Dir.path.getWin32PathType(u16, path);
var path_space: WindowsPathSpace = undefined;
if (path_type == .local_device) {
switch (getLocalDevicePathType(u16, path)) {
.verbatim => {
path_space.data[0..nt_prefix.len].* = nt_prefix;
const len_after_prefix = path.len - nt_prefix.len;
@memcpy(path_space.data[nt_prefix.len..][0..len_after_prefix], path[nt_prefix.len..]);
path_space.len = path.len;
path_space.data[path_space.len] = 0;
return path_space;
},
.local_device, .fake_verbatim => {
const path_byte_len = windows.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;
},
}
}
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 = windows.normalizePath(u16, path_space.data[0..path.len]) catch |err| switch (err) {
error.TooManyParentDirs => break :relative,
};
path_space.data[path_space.len] = 0;
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.
path_space.data[0..nt_prefix.len].* = nt_prefix;
if (path_type == .root_local_device) {
// `\\.` and `\\?` always get converted to `\??\` exactly, so
// we can just stop here
path_space.len = 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 Io.Dir.cwd()
if (dir.? == Io.Dir.cwd().handle) {
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: [windows.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 > windows.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 = windows.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;
}
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>`
assert(path_space.data[path_buf_offset] == '\\');
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;
}
}
const LocalDevicePathType = enum {
/// `\\.\` (path separators can be `\` or `/`)
local_device,
/// `\\?\`
/// When converted to an NT path, everything past the prefix is left
/// untouched and `\\?\` is replaced by `\??\`.
verbatim,
/// `\\?\` without all path separators being `\`.
/// This seems to be recognized as a prefix, but the 'verbatim' aspect
/// is not respected (i.e. if `//?/C:/foo` is converted to an NT path,
/// it will become `\??\C:\foo` [it will be canonicalized and the //?/ won't
/// be treated as part of the final path])
fake_verbatim,
};
/// Only relevant for Win32 -> NT path conversion.
/// Asserts `path` is of type `Dir.path.Win32PathType.local_device`.
fn getLocalDevicePathType(comptime T: type, path: []const T) LocalDevicePathType {
if (std.debug.runtime_safety) {
assert(Dir.path.getWin32PathType(T, path) == .local_device);
}
const backslash = std.mem.nativeToLittle(T, '\\');
const all_backslash = path[0] == backslash and
path[1] == backslash and
path[3] == backslash;
return switch (path[2]) {
std.mem.nativeToLittle(T, '?') => if (all_backslash) .verbatim else .fake_verbatim,
std.mem.nativeToLittle(T, '.') => .local_device,
else => unreachable,
};
}
pub const Wtf8ToPrefixedFileWError = Wtf16ToPrefixedFileWError;
/// Same as `wToPrefixedFileW` but accepts a WTF-8 encoded path.
/// https://wtf-8.codeberg.page/
pub fn sliceToPrefixedFileW(dir: ?windows.HANDLE, path: []const u8) Wtf8ToPrefixedFileWError!WindowsPathSpace {
var temp_path: WindowsPathSpace = undefined;
temp_path.len = std.unicode.wtf8ToWtf16Le(&temp_path.data, path) catch |err| switch (err) {
error.InvalidWtf8 => return error.BadPathName,
};
temp_path.data[temp_path.len] = 0;
return wToPrefixedFileW(dir, temp_path.span());
}
pub const WindowsPathSpace = struct {
data: [windows.PATH_MAX_WIDE:0]u16,
len: usize,
pub fn span(self: *const WindowsPathSpace) [:0]const u16 {
return self.data[0..self.len :0];
}
};
fn dirRealPathFilePosix(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, out_buffer: []u8) Dir.RealPathFileError!usize {
if (native_os == .wasi) return error.OperationUnsupported;
@ -6478,7 +7022,7 @@ fn dirDeleteWindows(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, remov
_ = t;
const w = windows;
const sub_path_w_buf = try w.sliceToPrefixedFileW(dir.handle, sub_path);
const sub_path_w_buf = try sliceToPrefixedFileW(dir.handle, sub_path);
const sub_path_w = sub_path_w_buf.span();
const path_len_bytes = @as(u16, @intCast(sub_path_w.len * 2));
@ -6759,9 +7303,9 @@ fn dirRenameWindowsInner(
replace_if_exists: bool,
) Dir.RenamePreserveError!void {
const w = windows;
const old_path_w_buf = try windows.sliceToPrefixedFileW(old_dir.handle, old_sub_path);
const old_path_w_buf = try sliceToPrefixedFileW(old_dir.handle, old_sub_path);
const old_path_w = old_path_w_buf.span();
const new_path_w_buf = try windows.sliceToPrefixedFileW(new_dir.handle, new_sub_path);
const new_path_w_buf = try sliceToPrefixedFileW(new_dir.handle, new_sub_path);
const new_path_w = new_path_w_buf.span();
const src_fd = src_fd: {
@ -7092,7 +7636,7 @@ fn dirSymLinkWindows(
// Target path does not use sliceToPrefixedFileW because certain paths
// are handled differently when creating a symlink than they would be
// when converting to an NT namespaced path.
var target_path_w: w.PathSpace = undefined;
var target_path_w: WindowsPathSpace = undefined;
target_path_w.len = try w.wtf8ToWtf16Le(&target_path_w.data, target_path);
target_path_w.data[target_path_w.len] = 0;
// However, we need to canonicalize any path separators to `\`, since if
@ -7104,7 +7648,7 @@ fn dirSymLinkWindows(
std.mem.nativeToLittle(u16, '\\'),
);
const sym_link_path_w = try w.sliceToPrefixedFileW(dir.handle, sym_link_path);
const sym_link_path_w = try sliceToPrefixedFileW(dir.handle, sym_link_path);
const SYMLINK_DATA = extern struct {
ReparseTag: w.IO_REPARSE_TAG,
@ -7158,7 +7702,7 @@ fn dirSymLinkWindows(
// https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createsymboliclinkw
var is_target_absolute = false;
const final_target_path = target_path: {
if (w.hasCommonNtPrefix(u16, target_path_w.span())) {
if (windows.hasCommonNtPrefix(u16, target_path_w.span())) {
// Already an NT path, no need to do anything to it
break :target_path target_path_w.span();
} else {
@ -7176,7 +7720,7 @@ fn dirSymLinkWindows(
break :target_path target_path_w.span(),
}
}
var prefixed_target_path = try w.wToPrefixedFileW(dir.handle, target_path_w.span());
var prefixed_target_path = try wToPrefixedFileW(dir.handle, target_path_w.span());
// We do this after prefixing to ensure that drive-relative paths are treated as absolute
is_target_absolute = Dir.path.isAbsoluteWindowsWtf16(prefixed_target_path.span());
break :target_path prefixed_target_path.span();
@ -7322,7 +7866,7 @@ fn dirReadLink(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, buffer: []
fn dirReadLinkWindows(dir: Dir, sub_path: []const u8, buffer: []u8) Dir.ReadLinkError!usize {
// This gets used once for `sub_path` and then reused again temporarily
// before converting back to `buffer`.
var sub_path_w_buf = try windows.sliceToPrefixedFileW(dir.handle, sub_path);
var sub_path_w_buf = try sliceToPrefixedFileW(dir.handle, sub_path);
const sub_path_w = sub_path_w_buf.span();
const path_len_bytes = std.math.cast(u16, sub_path_w.len * 2) orelse return error.NameTooLong;
var nt_name: windows.UNICODE_STRING = .{
@ -9586,7 +10130,7 @@ fn processExecutableOpen(userdata: ?*anyopaque, flags: File.OpenFlags) process.O
// the file, we can let the openFileW call follow the symlink for us.
const image_path_unicode_string = &windows.peb().ProcessParameters.ImagePathName;
const image_path_name = image_path_unicode_string.Buffer.?[0 .. image_path_unicode_string.Length / 2 :0];
const prefixed_path_w = try windows.wToPrefixedFileW(null, image_path_name);
const prefixed_path_w = try wToPrefixedFileW(null, image_path_name);
return dirOpenFileWtf16(null, prefixed_path_w.span(), flags);
},
.driverkit,
@ -9794,7 +10338,7 @@ fn processExecutablePath(userdata: ?*anyopaque, out_buffer: []u8) process.Execut
// If ImagePathName is a symlink, then it will contain the path of the
// symlink, not the path that the symlink points to. We want the path
// that the symlink points to, though, so we need to get the realpath.
var path_name_w_buf = try w.wToPrefixedFileW(null, image_path_name);
var path_name_w_buf = try wToPrefixedFileW(null, image_path_name);
const h_file = handle: {
const syscall: Syscall = try .start();
@ -9822,9 +10366,7 @@ fn processExecutablePath(userdata: ?*anyopaque, out_buffer: []u8) process.Execut
};
defer w.CloseHandle(h_file);
// TODO move GetFinalPathNameByHandle logic into Io.Threaded and add cancel checks
try Thread.checkCancel();
const wide_slice = try w.GetFinalPathNameByHandle(h_file, .{}, &path_name_w_buf.data);
const wide_slice = try GetFinalPathNameByHandle(h_file, .{}, &path_name_w_buf.data);
const len = std.unicode.calcWtf8Len(wide_slice);
if (len > out_buffer.len)
@ -13598,9 +14140,7 @@ fn processSetCurrentDir(userdata: ?*anyopaque, dir: Dir) process.SetCurrentDirEr
if (is_windows) {
var dir_path_buffer: [windows.PATH_MAX_WIDE]u16 = undefined;
// TODO move GetFinalPathNameByHandle logic into Io.Threaded and add cancel checks
try Thread.checkCancel();
const dir_path = try windows.GetFinalPathNameByHandle(dir.handle, .{}, &dir_path_buffer);
const dir_path = try GetFinalPathNameByHandle(dir.handle, .{}, &dir_path_buffer);
const path_len_bytes = std.math.cast(u16, dir_path.len * 2) orelse return error.NameTooLong;
var nt_name: windows.UNICODE_STRING = .{
.Length = path_len_bytes,
@ -15326,9 +15866,7 @@ fn processSpawnWindows(userdata: ?*anyopaque, options: process.SpawnOptions) pro
.inherit => break :cwd_w null,
.dir => |cwd_dir| {
var dir_path_buffer = try arena.alloc(u16, windows.PATH_MAX_WIDE + 1);
// TODO move GetFinalPathNameByHandle logic into std.Io.Threaded and add cancel checks
try Thread.checkCancel();
const dir_path = try windows.GetFinalPathNameByHandle(
const dir_path = try GetFinalPathNameByHandle(
cwd_dir.handle,
.{},
dir_path_buffer[0..windows.PATH_MAX_WIDE],
@ -15752,7 +16290,7 @@ fn windowsCreateProcessPathExt(
try dir_buf.append(arena, 0);
defer dir_buf.shrinkRetainingCapacity(dir_path_len);
const dir_path_z = dir_buf.items[0 .. dir_buf.items.len - 1 :0];
const prefixed_path = try windows.wToPrefixedFileW(null, dir_path_z);
const prefixed_path = try wToPrefixedFileW(null, dir_path_z);
break :dir dirOpenDirWindows(.cwd(), prefixed_path.span(), .{
.iterate = true,
}) catch |err| switch (err) {

View file

@ -6,6 +6,7 @@ const std = @import("std");
const Io = std.Io;
const testing = std.testing;
const assert = std.debug.assert;
const windows = std.os.windows;
test "concurrent vs main prevents deadlock via oversubscription" {
if (true) {
@ -277,3 +278,337 @@ test "memory mapping fallback" {
try testing.expectEqualStrings("this9is9my data123", mm.memory);
}
}
/// Wrapper around RtlDosPathNameToNtPathName_U for use in comparing
/// the behavior of RtlDosPathNameToNtPathName_U with wToPrefixedFileW
/// Note: RtlDosPathNameToNtPathName_U is not used in the Zig implementation
// because it allocates.
fn RtlDosPathNameToNtPathName_U(path: [:0]const u16) !Io.Threaded.WindowsPathSpace {
var out: windows.UNICODE_STRING = undefined;
const rc = windows.ntdll.RtlDosPathNameToNtPathName_U(path, &out, null, null);
if (rc != windows.TRUE) return error.BadPathName;
defer windows.ntdll.RtlFreeUnicodeString(&out);
var path_space: Io.Threaded.WindowsPathSpace = undefined;
const out_path = out.Buffer.?[0 .. out.Length / 2];
@memcpy(path_space.data[0..out_path.len], out_path);
path_space.len = out.Length / 2;
path_space.data[path_space.len] = 0;
return path_space;
}
/// Test that the Zig conversion matches the expected_path (for instances where
/// the Zig implementation intentionally diverges from what RtlDosPathNameToNtPathName_U does).
fn testToPrefixedFileNoOracle(comptime path: []const u8, comptime expected_path: []const u8) !void {
const path_utf16 = std.unicode.utf8ToUtf16LeStringLiteral(path);
const expected_path_utf16 = std.unicode.utf8ToUtf16LeStringLiteral(expected_path);
const actual_path = try Io.Threaded.wToPrefixedFileW(null, path_utf16);
std.testing.expectEqualSlices(u16, expected_path_utf16, actual_path.span()) catch |e| {
std.debug.print("got '{f}', expected '{f}'\n", .{ std.unicode.fmtUtf16Le(actual_path.span()), std.unicode.fmtUtf16Le(expected_path_utf16) });
return e;
};
}
/// Test that the Zig conversion matches the expected_path and that the
/// expected_path matches the conversion that RtlDosPathNameToNtPathName_U does.
fn testToPrefixedFileWithOracle(comptime path: []const u8, comptime expected_path: []const u8) !void {
try testToPrefixedFileNoOracle(path, expected_path);
try testToPrefixedFileOnlyOracle(path);
}
/// Test that the Zig conversion matches the conversion that RtlDosPathNameToNtPathName_U does.
fn testToPrefixedFileOnlyOracle(comptime path: []const u8) !void {
const path_utf16 = std.unicode.utf8ToUtf16LeStringLiteral(path);
const zig_result = try Io.Threaded.wToPrefixedFileW(null, path_utf16);
const win32_api_result = try RtlDosPathNameToNtPathName_U(path_utf16);
std.testing.expectEqualSlices(u16, win32_api_result.span(), zig_result.span()) catch |e| {
std.debug.print("got '{f}', expected '{f}'\n", .{ std.unicode.fmtUtf16Le(zig_result.span()), std.unicode.fmtUtf16Le(win32_api_result.span()) });
return e;
};
}
test "toPrefixedFileW" {
if (builtin.os.tag != .windows) return error.SkipZigTest;
// Most test cases come from https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
// Note that these tests do not actually touch the filesystem or care about whether or not
// any of the paths actually exist or are otherwise valid.
// Drive Absolute
try testToPrefixedFileWithOracle("X:\\ABC\\DEF", "\\??\\X:\\ABC\\DEF");
try testToPrefixedFileWithOracle("X:\\", "\\??\\X:\\");
try testToPrefixedFileWithOracle("X:\\ABC\\", "\\??\\X:\\ABC\\");
// Trailing . and space characters are stripped
try testToPrefixedFileWithOracle("X:\\ABC\\DEF. .", "\\??\\X:\\ABC\\DEF");
try testToPrefixedFileWithOracle("X:/ABC/DEF", "\\??\\X:\\ABC\\DEF");
try testToPrefixedFileWithOracle("X:\\ABC\\..\\XYZ", "\\??\\X:\\XYZ");
try testToPrefixedFileWithOracle("X:\\ABC\\..\\..\\..", "\\??\\X:\\");
// Drive letter casing is unchanged
try testToPrefixedFileWithOracle("x:\\", "\\??\\x:\\");
// Drive Relative
// These tests depend on the CWD of the specified drive letter which can vary,
// so instead we just test that the Zig implementation matches the result of
// RtlDosPathNameToNtPathName_U.
// TODO: Setting the =X: environment variable didn't seem to affect
// RtlDosPathNameToNtPathName_U, not sure why that is but getting that
// to work could be an avenue to making these cases environment-independent.
// All -> are examples of the result if the X drive's cwd was X:\ABC
try testToPrefixedFileOnlyOracle("X:DEF\\GHI"); // -> \??\X:\ABC\DEF\GHI
try testToPrefixedFileOnlyOracle("X:"); // -> \??\X:\ABC
try testToPrefixedFileOnlyOracle("X:DEF. ."); // -> \??\X:\ABC\DEF
try testToPrefixedFileOnlyOracle("X:ABC\\..\\XYZ"); // -> \??\X:\ABC\XYZ
try testToPrefixedFileOnlyOracle("X:ABC\\..\\..\\.."); // -> \??\X:\
try testToPrefixedFileOnlyOracle("x:"); // -> \??\X:\ABC
// Rooted
// These tests depend on the drive letter of the CWD which can vary, so
// instead we just test that the Zig implementation matches the result of
// RtlDosPathNameToNtPathName_U.
// TODO: Getting the CWD path, getting the drive letter from it, and using it to
// construct the expected NT paths could be an avenue to making these cases
// environment-independent and therefore able to use testToPrefixedFileWithOracle.
// All -> are examples of the result if the CWD's drive letter was X
try testToPrefixedFileOnlyOracle("\\ABC\\DEF"); // -> \??\X:\ABC\DEF
try testToPrefixedFileOnlyOracle("\\"); // -> \??\X:\
try testToPrefixedFileOnlyOracle("\\ABC\\DEF. ."); // -> \??\X:\ABC\DEF
try testToPrefixedFileOnlyOracle("/ABC/DEF"); // -> \??\X:\ABC\DEF
try testToPrefixedFileOnlyOracle("\\ABC\\..\\XYZ"); // -> \??\X:\XYZ
try testToPrefixedFileOnlyOracle("\\ABC\\..\\..\\.."); // -> \??\X:\
// Relative
// These cases differ in functionality to RtlDosPathNameToNtPathName_U.
// Relative paths remain relative if they don't have enough .. components
// to error with TooManyParentDirs
try testToPrefixedFileNoOracle("ABC\\DEF", "ABC\\DEF");
// TODO: enable this if trailing . and spaces are stripped from relative paths
//try testToPrefixedFileNoOracle("ABC\\DEF. .", "ABC\\DEF");
try testToPrefixedFileNoOracle("ABC/DEF", "ABC\\DEF");
try testToPrefixedFileNoOracle("./ABC/.././DEF", "DEF");
// TooManyParentDirs, so resolved relative to the CWD
// All -> are examples of the result if the CWD was X:\ABC\DEF
try testToPrefixedFileOnlyOracle("..\\GHI"); // -> \??\X:\ABC\GHI
try testToPrefixedFileOnlyOracle("GHI\\..\\..\\.."); // -> \??\X:\
// UNC Absolute
try testToPrefixedFileWithOracle("\\\\server\\share\\ABC\\DEF", "\\??\\UNC\\server\\share\\ABC\\DEF");
try testToPrefixedFileWithOracle("\\\\server", "\\??\\UNC\\server");
try testToPrefixedFileWithOracle("\\\\server\\share", "\\??\\UNC\\server\\share");
try testToPrefixedFileWithOracle("\\\\server\\share\\ABC. .", "\\??\\UNC\\server\\share\\ABC");
try testToPrefixedFileWithOracle("//server/share/ABC/DEF", "\\??\\UNC\\server\\share\\ABC\\DEF");
try testToPrefixedFileWithOracle("\\\\server\\share\\ABC\\..\\XYZ", "\\??\\UNC\\server\\share\\XYZ");
try testToPrefixedFileWithOracle("\\\\server\\share\\ABC\\..\\..\\..", "\\??\\UNC\\server\\share");
// Local Device
try testToPrefixedFileWithOracle("\\\\.\\COM20", "\\??\\COM20");
try testToPrefixedFileWithOracle("\\\\.\\pipe\\mypipe", "\\??\\pipe\\mypipe");
try testToPrefixedFileWithOracle("\\\\.\\X:\\ABC\\DEF. .", "\\??\\X:\\ABC\\DEF");
try testToPrefixedFileWithOracle("\\\\.\\X:/ABC/DEF", "\\??\\X:\\ABC\\DEF");
try testToPrefixedFileWithOracle("\\\\.\\X:\\ABC\\..\\XYZ", "\\??\\X:\\XYZ");
// Can replace the first component of the path (contrary to drive absolute and UNC absolute paths)
try testToPrefixedFileWithOracle("\\\\.\\X:\\ABC\\..\\..\\C:\\", "\\??\\C:\\");
try testToPrefixedFileWithOracle("\\\\.\\pipe\\mypipe\\..\\notmine", "\\??\\pipe\\notmine");
// Special-case device names
// TODO: Enable once these are supported
// more cases to test here: https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
//try testToPrefixedFileWithOracle("COM1", "\\??\\COM1");
// Sometimes the special-cased device names are not respected
try testToPrefixedFileWithOracle("\\\\.\\X:\\COM1", "\\??\\X:\\COM1");
try testToPrefixedFileWithOracle("\\\\abc\\xyz\\COM1", "\\??\\UNC\\abc\\xyz\\COM1");
// Verbatim
// Left untouched except \\?\ is replaced by \??\
try testToPrefixedFileWithOracle("\\\\?\\X:", "\\??\\X:");
try testToPrefixedFileWithOracle("\\\\?\\X:\\COM1", "\\??\\X:\\COM1");
try testToPrefixedFileWithOracle("\\\\?\\X:/ABC/DEF. .", "\\??\\X:/ABC/DEF. .");
try testToPrefixedFileWithOracle("\\\\?\\X:\\ABC\\..\\..\\..", "\\??\\X:\\ABC\\..\\..\\..");
// NT Namespace
// Fully unmodified
try testToPrefixedFileWithOracle("\\??\\X:", "\\??\\X:");
try testToPrefixedFileWithOracle("\\??\\X:\\COM1", "\\??\\X:\\COM1");
try testToPrefixedFileWithOracle("\\??\\X:/ABC/DEF. .", "\\??\\X:/ABC/DEF. .");
try testToPrefixedFileWithOracle("\\??\\X:\\ABC\\..\\..\\..", "\\??\\X:\\ABC\\..\\..\\..");
// 'Fake' Verbatim
// If the prefix looks like the verbatim prefix but not all path separators in the
// prefix are backslashes, then it gets canonicalized and the prefix is dropped in favor
// of the NT prefix.
try testToPrefixedFileWithOracle("//?/C:/ABC", "\\??\\C:\\ABC");
// 'Fake' NT
// If the prefix looks like the NT prefix but not all path separators in the prefix
// are backslashes, then it gets canonicalized and the /??/ is not dropped but
// rather treated as part of the path. In other words, the path is treated
// as a rooted path, so the final path is resolved relative to the CWD's
// drive letter.
// The -> shows an example of the result if the CWD's drive letter was X
try testToPrefixedFileOnlyOracle("/??/C:/ABC"); // -> \??\X:\??\C:\ABC
// Root Local Device
// \\. and \\? always get converted to \??\
try testToPrefixedFileWithOracle("\\\\.", "\\??\\");
try testToPrefixedFileWithOracle("\\\\?", "\\??\\");
try testToPrefixedFileWithOracle("//?", "\\??\\");
try testToPrefixedFileWithOracle("//.", "\\??\\");
}
fn testRemoveDotDirs(str: []const u8, expected: []const u8) !void {
const mutable = try testing.allocator.dupe(u8, str);
defer testing.allocator.free(mutable);
const actual = mutable[0..try windows.removeDotDirsSanitized(u8, mutable)];
try testing.expect(std.mem.eql(u8, actual, expected));
}
fn testRemoveDotDirsError(err: anyerror, str: []const u8) !void {
const mutable = try testing.allocator.dupe(u8, str);
defer testing.allocator.free(mutable);
try testing.expectError(err, windows.removeDotDirsSanitized(u8, mutable));
}
test "removeDotDirs" {
try testRemoveDotDirs("", "");
try testRemoveDotDirs(".", "");
try testRemoveDotDirs(".\\", "");
try testRemoveDotDirs(".\\.", "");
try testRemoveDotDirs(".\\.\\", "");
try testRemoveDotDirs(".\\.\\.", "");
try testRemoveDotDirs("a", "a");
try testRemoveDotDirs("a\\", "a\\");
try testRemoveDotDirs("a\\b", "a\\b");
try testRemoveDotDirs("a\\.", "a\\");
try testRemoveDotDirs("a\\b\\.", "a\\b\\");
try testRemoveDotDirs("a\\.\\b", "a\\b");
try testRemoveDotDirs(".a", ".a");
try testRemoveDotDirs(".a\\", ".a\\");
try testRemoveDotDirs(".a\\.b", ".a\\.b");
try testRemoveDotDirs(".a\\.", ".a\\");
try testRemoveDotDirs(".a\\.\\.", ".a\\");
try testRemoveDotDirs(".a\\.\\.\\.b", ".a\\.b");
try testRemoveDotDirs(".a\\.\\.\\.b\\", ".a\\.b\\");
try testRemoveDotDirsError(error.TooManyParentDirs, "..");
try testRemoveDotDirsError(error.TooManyParentDirs, "..\\");
try testRemoveDotDirsError(error.TooManyParentDirs, ".\\..\\");
try testRemoveDotDirsError(error.TooManyParentDirs, ".\\.\\..\\");
try testRemoveDotDirs("a\\..", "");
try testRemoveDotDirs("a\\..\\", "");
try testRemoveDotDirs("a\\..\\.", "");
try testRemoveDotDirs("a\\..\\.\\", "");
try testRemoveDotDirs("a\\..\\.\\.", "");
try testRemoveDotDirsError(error.TooManyParentDirs, "a\\..\\.\\.\\..");
try testRemoveDotDirs("a\\..\\.\\.\\b", "b");
try testRemoveDotDirs("a\\..\\.\\.\\b\\", "b\\");
try testRemoveDotDirs("a\\..\\.\\.\\b\\.", "b\\");
try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\", "b\\");
try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\..", "");
try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\..\\", "");
try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\..\\.", "");
try testRemoveDotDirsError(error.TooManyParentDirs, "a\\..\\.\\.\\b\\.\\..\\.\\..");
try testRemoveDotDirs("a\\b\\..\\", "a\\");
try testRemoveDotDirs("a\\b\\..\\c", "a\\c");
}
const RTL_PATH_TYPE = enum(c_int) {
Unknown,
UncAbsolute,
DriveAbsolute,
DriveRelative,
Rooted,
Relative,
LocalDevice,
RootLocalDevice,
};
pub extern "ntdll" fn RtlDetermineDosPathNameType_U(
Path: [*:0]const u16,
) callconv(.winapi) RTL_PATH_TYPE;
test "getWin32PathType vs RtlDetermineDosPathNameType_U" {
if (builtin.os.tag != .windows) return error.SkipZigTest;
var buf: std.ArrayList(u16) = .empty;
defer buf.deinit(std.testing.allocator);
var wtf8_buf: std.ArrayList(u8) = .empty;
defer wtf8_buf.deinit(std.testing.allocator);
var random = std.Random.DefaultPrng.init(std.testing.random_seed);
const rand = random.random();
for (0..1000) |_| {
buf.clearRetainingCapacity();
const path = try getRandomWtf16Path(std.testing.allocator, &buf, rand);
wtf8_buf.clearRetainingCapacity();
const wtf8_len = std.unicode.calcWtf8Len(path);
try wtf8_buf.ensureTotalCapacity(std.testing.allocator, wtf8_len);
wtf8_buf.items.len = wtf8_len;
std.debug.assert(std.unicode.wtf16LeToWtf8(wtf8_buf.items, path) == wtf8_len);
const windows_type = RtlDetermineDosPathNameType_U(path);
const wtf16_type = std.fs.path.getWin32PathType(u16, path);
const wtf8_type = std.fs.path.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: std.fs.path.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

@ -17,7 +17,6 @@ pub const DynLib = struct {
ElfDynLib
else
DlDynLib,
.windows => WindowsDynLib,
.driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos, .freebsd, .netbsd, .openbsd, .dragonfly, .illumos => DlDynLib,
else => struct {
const open = @compileError("unsupported platform");
@ -27,7 +26,7 @@ pub const DynLib = struct {
inner: InnerType,
pub const Error = ElfDynLibError || DlDynLibError || WindowsDynLibError;
pub const Error = ElfDynLibError || DlDynLibError;
/// Trusts the file. Malicious file will be able to execute arbitrary code.
pub fn open(path: []const u8) Error!DynLib {
@ -558,73 +557,6 @@ test "ElfDynLib" {
try testing.expectError(error.FileNotFound, ElfDynLib.openZ("invalid_so.so", null));
}
/// Separated to avoid referencing `WindowsDynLib`, because its field types may not
/// be valid on other targets.
const WindowsDynLibError = error{
FileNotFound,
InvalidPath,
} || windows.LoadLibraryError;
pub const WindowsDynLib = struct {
pub const Error = WindowsDynLibError;
dll: windows.HMODULE,
pub fn open(path: []const u8) Error!WindowsDynLib {
return openEx(path, .none);
}
/// WindowsDynLib specific
/// Opens dynamic library with specified library loading flags.
pub fn openEx(path: []const u8, flags: windows.LoadLibraryFlags) Error!WindowsDynLib {
const path_w = windows.sliceToPrefixedFileW(null, path) catch return error.InvalidPath;
return openExW(path_w.span().ptr, flags);
}
pub fn openZ(path_c: [*:0]const u8) Error!WindowsDynLib {
return openExZ(path_c, .none);
}
/// WindowsDynLib specific
/// Opens dynamic library with specified library loading flags.
pub fn openExZ(path_c: [*:0]const u8, flags: windows.LoadLibraryFlags) Error!WindowsDynLib {
const path_w = windows.cStrToPrefixedFileW(null, path_c) catch return error.InvalidPath;
return openExW(path_w.span().ptr, flags);
}
/// WindowsDynLib specific
pub fn openW(path_w: [*:0]const u16) Error!WindowsDynLib {
return openExW(path_w, .none);
}
/// WindowsDynLib specific
/// Opens dynamic library with specified library loading flags.
pub fn openExW(path_w: [*:0]const u16, flags: windows.LoadLibraryFlags) Error!WindowsDynLib {
var offset: usize = 0;
if (path_w[0] == '\\' and path_w[1] == '?' and path_w[2] == '?' and path_w[3] == '\\') {
// + 4 to skip over the \??\
offset = 4;
}
return .{
.dll = try windows.LoadLibraryExW(path_w + offset, flags),
};
}
pub fn close(self: *WindowsDynLib) void {
windows.FreeLibrary(self.dll);
self.* = undefined;
}
pub fn lookup(self: *WindowsDynLib, comptime T: type, name: [:0]const u8) ?T {
if (windows.kernel32.GetProcAddress(self.dll, name.ptr)) |addr| {
return @as(T, @ptrCast(@alignCast(addr)));
} else {
return null;
}
}
};
/// Separated to avoid referencing `DlDynLib`, because its field types may not
/// be valid on other targets.
const DlDynLibError = error{ FileNotFound, NameTooLong };
@ -676,7 +608,6 @@ pub const DlDynLib = struct {
test "dynamic_library" {
const libname = switch (native_os) {
.linux, .freebsd, .openbsd, .illumos => "invalid_so.so",
.windows => "invalid_dll.dll",
.driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => "invalid_dylib.dylib",
else => return error.SkipZigTest,
};

View file

@ -15,12 +15,6 @@ const math = std.math;
const maxInt = std.math.maxInt;
const UnexpectedError = std.posix.UnexpectedError;
test {
if (builtin.os.tag == .windows) {
_ = @import("windows/test.zig");
}
}
pub const advapi32 = @import("windows/advapi32.zig");
pub const kernel32 = @import("windows/kernel32.zig");
pub const ntdll = @import("windows/ntdll.zig");
@ -2670,324 +2664,6 @@ pub fn CloseHandle(hObject: HANDLE) void {
assert(ntdll.NtClose(hObject) == .SUCCESS);
}
pub const QueryObjectNameError = error{
AccessDenied,
InvalidHandle,
NameTooLong,
Unexpected,
};
pub fn QueryObjectName(handle: HANDLE, out_buffer: []u16) QueryObjectNameError![]u16 {
const out_buffer_aligned = mem.alignInSlice(out_buffer, @alignOf(OBJECT_NAME_INFORMATION)) orelse return error.NameTooLong;
const info = @as(*OBJECT_NAME_INFORMATION, @ptrCast(out_buffer_aligned));
// buffer size is specified in bytes
const out_buffer_len = std.math.cast(ULONG, out_buffer_aligned.len * 2) orelse maxInt(ULONG);
// last argument would return the length required for full_buffer, not exposed here
return switch (ntdll.NtQueryObject(handle, .ObjectNameInformation, info, out_buffer_len, null)) {
.SUCCESS => blk: {
// info.Name.Buffer from ObQueryNameString is documented to be null (and MaximumLength == 0)
// if the object was "unnamed", not sure if this can happen for file handles
if (info.Name.MaximumLength == 0) break :blk error.Unexpected;
// resulting string length is specified in bytes
const path_length_unterminated = @divExact(info.Name.Length, 2);
break :blk info.Name.Buffer.?[0..path_length_unterminated];
},
.ACCESS_DENIED => error.AccessDenied,
.INVALID_HANDLE => error.InvalidHandle,
// triggered when the buffer is too small for the OBJECT_NAME_INFORMATION object (.INFO_LENGTH_MISMATCH),
// or if the buffer is too small for the file path returned (.BUFFER_OVERFLOW, .BUFFER_TOO_SMALL)
.INFO_LENGTH_MISMATCH, .BUFFER_OVERFLOW, .BUFFER_TOO_SMALL => error.NameTooLong,
else => |e| unexpectedStatus(e),
};
}
test QueryObjectName {
if (builtin.os.tag != .windows)
return;
//any file will do; canonicalization works on NTFS junctions and symlinks, hardlinks remain separate paths.
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const handle = tmp.dir.handle;
var out_buffer: [PATH_MAX_WIDE]u16 = undefined;
const result_path = try QueryObjectName(handle, &out_buffer);
const required_len_in_u16 = result_path.len + @divExact(@intFromPtr(result_path.ptr) - @intFromPtr(&out_buffer), 2) + 1;
//insufficient size
try std.testing.expectError(error.NameTooLong, QueryObjectName(handle, out_buffer[0 .. required_len_in_u16 - 1]));
//exactly-sufficient size
_ = try QueryObjectName(handle, out_buffer[0..required_len_in_u16]);
}
pub const GetFinalPathNameByHandleError = error{
AccessDenied,
FileNotFound,
NameTooLong,
/// The volume does not contain a recognized file system. File system
/// drivers might not be loaded, or the volume may be corrupt.
UnrecognizedVolume,
Unexpected,
};
/// Specifies how to format volume path in the result of `GetFinalPathNameByHandle`.
/// Defaults to DOS volume names.
pub const GetFinalPathNameByHandleFormat = struct {
volume_name: enum {
/// Format as DOS volume name
Dos,
/// Format as NT volume name
Nt,
} = .Dos,
};
/// Returns canonical (normalized) path of handle.
/// Use `GetFinalPathNameByHandleFormat` to specify whether the path is meant to include
/// NT or DOS volume name (e.g., `\Device\HarddiskVolume0\foo.txt` versus `C:\foo.txt`).
/// If DOS volume name format is selected, note that this function does *not* prepend
/// `\\?\` prefix to the resultant path.
///
/// TODO move this function into std.Io.Threaded and add cancelation checks
pub fn GetFinalPathNameByHandle(
hFile: HANDLE,
fmt: GetFinalPathNameByHandleFormat,
out_buffer: []u16,
) GetFinalPathNameByHandleError![]u16 {
const final_path = QueryObjectName(hFile, out_buffer) catch |err| switch (err) {
// we assume InvalidHandle is close enough to FileNotFound in semantics
// to not further complicate the error set
error.InvalidHandle => return error.FileNotFound,
else => |e| return e,
};
switch (fmt.volume_name) {
.Nt => {
// the returned path is already in .Nt format
return final_path;
},
.Dos => {
// parse the string to separate volume path from file path
const device_prefix = std.unicode.utf8ToUtf16LeStringLiteral("\\Device\\");
// We aren't entirely sure of the structure of the path returned by
// QueryObjectName in all contexts/environments.
// This code is written to cover the various cases that have
// been encountered and solved appropriately. But note that there's
// no easy way to verify that they have all been tackled!
// (Unless you, the reader knows of one then please do action that!)
if (!mem.startsWith(u16, final_path, device_prefix)) {
// Wine seems to return NT namespaced paths starting with \??\ from QueryObjectName
// (e.g. `\??\Z:\some\path\to\a\file.txt`), in which case we can just strip the
// prefix to turn it into an absolute path.
// https://github.com/ziglang/zig/issues/26029
// https://bugs.winehq.org/show_bug.cgi?id=39569
return ntToWin32Namespace(final_path, out_buffer) catch |err| switch (err) {
error.NotNtPath => return error.Unexpected,
error.NameTooLong => |e| return e,
};
}
const file_path_begin_index = mem.findPos(u16, final_path, device_prefix.len, &[_]u16{'\\'}) orelse unreachable;
const volume_name_u16 = final_path[0..file_path_begin_index];
const device_name_u16 = volume_name_u16[device_prefix.len..];
const file_name_u16 = final_path[file_path_begin_index..];
// MUP is Multiple UNC Provider, and indicates that the path is a UNC
// path. In this case, the canonical UNC path can be gotten by just
// dropping the \Device\Mup\ and making sure the path begins with \\
if (mem.eql(u16, device_name_u16, std.unicode.utf8ToUtf16LeStringLiteral("Mup"))) {
out_buffer[0] = '\\';
@memmove(out_buffer[1..][0..file_name_u16.len], file_name_u16);
return out_buffer[0 .. 1 + file_name_u16.len];
}
// Get DOS volume name. DOS volume names are actually symbolic link objects to the
// actual NT volume. For example:
// (NT) \Device\HarddiskVolume4 => (DOS) \DosDevices\C: == (DOS) C:
const MIN_SIZE = @sizeOf(MOUNTMGR_MOUNT_POINT) + MAX_PATH;
// We initialize the input buffer to all zeros for convenience since
// `DeviceIoControl` with `IOCTL_MOUNTMGR_QUERY_POINTS` expects this.
var input_buf: [MIN_SIZE]u8 align(@alignOf(MOUNTMGR_MOUNT_POINT)) = [_]u8{0} ** MIN_SIZE;
var output_buf: [MIN_SIZE * 4]u8 align(@alignOf(MOUNTMGR_MOUNT_POINTS)) = undefined;
// This surprising path is a filesystem path to the mount manager on Windows.
// Source: https://stackoverflow.com/questions/3012828/using-ioctl-mountmgr-query-points
// This is the NT namespaced version of \\.\MountPointManager
const mgmt_path_u16 = std.unicode.utf8ToUtf16LeStringLiteral("\\??\\MountPointManager");
const mgmt_handle = OpenFile(mgmt_path_u16, .{
.access_mask = .{ .STANDARD = .{ .SYNCHRONIZE = true } },
.creation = .OPEN,
}) catch |err| switch (err) {
error.IsDir => return error.Unexpected,
error.NotDir => return error.Unexpected,
error.NoDevice => return error.Unexpected,
error.AccessDenied => return error.Unexpected,
error.PipeBusy => return error.Unexpected,
error.PathAlreadyExists => return error.Unexpected,
error.WouldBlock => return error.Unexpected,
error.NetworkNotFound => return error.Unexpected,
error.AntivirusInterference => return error.Unexpected,
error.BadPathName => return error.Unexpected,
error.OperationCanceled => @panic("TODO: better integrate cancelation"),
else => |e| return e,
};
defer CloseHandle(mgmt_handle);
var input_struct: *MOUNTMGR_MOUNT_POINT = @ptrCast(&input_buf[0]);
input_struct.DeviceNameOffset = @sizeOf(MOUNTMGR_MOUNT_POINT);
input_struct.DeviceNameLength = @intCast(volume_name_u16.len * 2);
@memcpy(input_buf[@sizeOf(MOUNTMGR_MOUNT_POINT)..][0 .. volume_name_u16.len * 2], @as([*]const u8, @ptrCast(volume_name_u16.ptr)));
{
const rc = DeviceIoControl(mgmt_handle, IOCTL.MOUNTMGR.QUERY_POINTS, .{ .in = &input_buf, .out = &output_buf });
switch (rc) {
.SUCCESS => {},
.OBJECT_NAME_NOT_FOUND => return error.FileNotFound,
else => return unexpectedStatus(rc),
}
}
const mount_points_struct: *const MOUNTMGR_MOUNT_POINTS = @ptrCast(&output_buf[0]);
const mount_points = @as(
[*]const MOUNTMGR_MOUNT_POINT,
@ptrCast(&mount_points_struct.MountPoints[0]),
)[0..mount_points_struct.NumberOfMountPoints];
for (mount_points) |mount_point| {
const symlink = @as(
[*]const u16,
@ptrCast(@alignCast(&output_buf[mount_point.SymbolicLinkNameOffset])),
)[0 .. mount_point.SymbolicLinkNameLength / 2];
// Look for `\DosDevices\` prefix. We don't really care if there are more than one symlinks
// with traditional DOS drive letters, so pick the first one available.
var prefix_buf = std.unicode.utf8ToUtf16LeStringLiteral("\\DosDevices\\");
const prefix = prefix_buf[0..prefix_buf.len];
if (mem.startsWith(u16, symlink, prefix)) {
const drive_letter = symlink[prefix.len..];
if (out_buffer.len < drive_letter.len + file_name_u16.len) return error.NameTooLong;
@memcpy(out_buffer[0..drive_letter.len], drive_letter);
@memmove(out_buffer[drive_letter.len..][0..file_name_u16.len], file_name_u16);
const total_len = drive_letter.len + file_name_u16.len;
// Validate that DOS does not contain any spurious nul bytes.
assert(mem.findScalar(u16, out_buffer[0..total_len], 0) == null);
return out_buffer[0..total_len];
} else if (mountmgrIsVolumeName(symlink)) {
// If the symlink is a volume GUID like \??\Volume{383da0b0-717f-41b6-8c36-00500992b58d},
// then it is a volume mounted as a path rather than a drive letter. We need to
// query the mount manager again to get the DOS path for the volume.
// 49 is the maximum length accepted by mountmgrIsVolumeName
const vol_input_size = @sizeOf(MOUNTMGR_TARGET_NAME) + (49 * 2);
var vol_input_buf: [vol_input_size]u8 align(@alignOf(MOUNTMGR_TARGET_NAME)) = [_]u8{0} ** vol_input_size;
// Note: If the path exceeds MAX_PATH, the Disk Management GUI doesn't accept the full path,
// and instead if must be specified using a shortened form (e.g. C:\FOO~1\BAR~1\<...>).
// However, just to be sure we can handle any path length, we use PATH_MAX_WIDE here.
const min_output_size = @sizeOf(MOUNTMGR_VOLUME_PATHS) + (PATH_MAX_WIDE * 2);
var vol_output_buf: [min_output_size]u8 align(@alignOf(MOUNTMGR_VOLUME_PATHS)) = undefined;
var vol_input_struct: *MOUNTMGR_TARGET_NAME = @ptrCast(&vol_input_buf[0]);
vol_input_struct.DeviceNameLength = @intCast(symlink.len * 2);
@memcpy(@as([*]WCHAR, &vol_input_struct.DeviceName)[0..symlink.len], symlink);
const rc = DeviceIoControl(mgmt_handle, IOCTL.MOUNTMGR.QUERY_DOS_VOLUME_PATH, .{ .in = &vol_input_buf, .out = &vol_output_buf });
switch (rc) {
.SUCCESS => {},
.UNRECOGNIZED_VOLUME => return error.UnrecognizedVolume,
else => return unexpectedStatus(rc),
}
const volume_paths_struct: *const MOUNTMGR_VOLUME_PATHS = @ptrCast(&vol_output_buf[0]);
const volume_path = std.mem.sliceTo(@as(
[*]const u16,
&volume_paths_struct.MultiSz,
)[0 .. volume_paths_struct.MultiSzLength / 2], 0);
if (out_buffer.len < volume_path.len + file_name_u16.len) return error.NameTooLong;
// `out_buffer` currently contains the memory of `file_name_u16`, so it can overlap with where
// we want to place the filename before returning. Here are the possible overlapping cases:
//
// out_buffer: [filename]
// dest: [___(a)___] [___(b)___]
//
// In the case of (a), we need to copy forwards, and in the case of (b) we need
// to copy backwards. We also need to do this before copying the volume path because
// it could overwrite the file_name_u16 memory.
const file_name_dest = out_buffer[volume_path.len..][0..file_name_u16.len];
@memmove(file_name_dest, file_name_u16);
@memcpy(out_buffer[0..volume_path.len], volume_path);
const total_len = volume_path.len + file_name_u16.len;
// Validate that DOS does not contain any spurious nul bytes.
assert(mem.findScalar(u16, out_buffer[0..total_len], 0) == null);
return out_buffer[0..total_len];
}
}
// If we've ended up here, then something went wrong/is corrupted in the OS,
// so error out!
return error.FileNotFound;
},
}
}
/// Equivalent to the MOUNTMGR_IS_VOLUME_NAME macro in mountmgr.h
fn mountmgrIsVolumeName(name: []const u16) bool {
return (name.len == 48 or (name.len == 49 and name[48] == mem.nativeToLittle(u16, '\\'))) and
name[0] == mem.nativeToLittle(u16, '\\') and
(name[1] == mem.nativeToLittle(u16, '?') or name[1] == mem.nativeToLittle(u16, '\\')) and
name[2] == mem.nativeToLittle(u16, '?') and
name[3] == mem.nativeToLittle(u16, '\\') and
mem.startsWith(u16, name[4..], std.unicode.utf8ToUtf16LeStringLiteral("Volume{")) and
name[19] == mem.nativeToLittle(u16, '-') and
name[24] == mem.nativeToLittle(u16, '-') and
name[29] == mem.nativeToLittle(u16, '-') and
name[34] == mem.nativeToLittle(u16, '-') and
name[47] == mem.nativeToLittle(u16, '}');
}
test mountmgrIsVolumeName {
@setEvalBranchQuota(2000);
const L = std.unicode.utf8ToUtf16LeStringLiteral;
try std.testing.expect(mountmgrIsVolumeName(L("\\\\?\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}")));
try std.testing.expect(mountmgrIsVolumeName(L("\\??\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}")));
try std.testing.expect(mountmgrIsVolumeName(L("\\\\?\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}\\")));
try std.testing.expect(mountmgrIsVolumeName(L("\\??\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}\\")));
try std.testing.expect(!mountmgrIsVolumeName(L("\\\\.\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}")));
try std.testing.expect(!mountmgrIsVolumeName(L("\\??\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}\\foo")));
try std.testing.expect(!mountmgrIsVolumeName(L("\\??\\Volume{383da0b0-717f-41b6-8c36-00500992b58}")));
}
test GetFinalPathNameByHandle {
if (builtin.os.tag != .windows)
return;
//any file will do
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const handle = tmp.dir.handle;
var buffer: [PATH_MAX_WIDE]u16 = undefined;
//check with sufficient size
const nt_path = try GetFinalPathNameByHandle(handle, .{ .volume_name = .Nt }, &buffer);
_ = try GetFinalPathNameByHandle(handle, .{ .volume_name = .Dos }, &buffer);
const required_len_in_u16 = nt_path.len + @divExact(@intFromPtr(nt_path.ptr) - @intFromPtr(&buffer), 2) + 1;
//check with insufficient size
try std.testing.expectError(error.NameTooLong, GetFinalPathNameByHandle(handle, .{ .volume_name = .Nt }, buffer[0 .. required_len_in_u16 - 1]));
try std.testing.expectError(error.NameTooLong, GetFinalPathNameByHandle(handle, .{ .volume_name = .Dos }, buffer[0 .. required_len_in_u16 - 1]));
//check with exactly-sufficient size
_ = try GetFinalPathNameByHandle(handle, .{ .volume_name = .Nt }, buffer[0..required_len_in_u16]);
_ = try GetFinalPathNameByHandle(handle, .{ .volume_name = .Dos }, buffer[0..required_len_in_u16]);
}
pub fn getpeername(s: ws2_32.SOCKET, name: *ws2_32.sockaddr, namelen: *ws2_32.socklen_t) i32 {
return ws2_32.getpeername(s, name, @as(*i32, @ptrCast(namelen)));
}
@ -3419,15 +3095,6 @@ test "eqlIgnoreCaseWtf16/Wtf8" {
try testEqlIgnoreCase(false, "𐓏", "𐓷");
}
pub const PathSpace = struct {
data: [PATH_MAX_WIDE:0]u16,
len: usize,
pub fn span(self: *const PathSpace) [:0]const u16 {
return self.data[0..self.len :0];
}
};
/// The error type for `removeDotDirsSanitized`
pub const RemoveDotDirsError = error{TooManyParentDirs};
@ -3503,205 +3170,6 @@ pub fn normalizePath(comptime T: type, path: []T) RemoveDotDirsError!usize {
return prefix_len + try removeDotDirsSanitized(T, path[prefix_len..new_len]);
}
pub const Wtf8ToPrefixedFileWError = Wtf16ToPrefixedFileWError;
/// Same as `sliceToPrefixedFileW` but accepts a pointer
/// to a null-terminated WTF-8 encoded path.
/// https://wtf-8.codeberg.page/
pub fn cStrToPrefixedFileW(dir: ?HANDLE, s: [*:0]const u8) Wtf8ToPrefixedFileWError!PathSpace {
return sliceToPrefixedFileW(dir, mem.sliceTo(s, 0));
}
/// Same as `wToPrefixedFileW` but accepts a WTF-8 encoded path.
/// https://wtf-8.codeberg.page/
pub fn sliceToPrefixedFileW(dir: ?HANDLE, path: []const u8) Wtf8ToPrefixedFileWError!PathSpace {
var temp_path: PathSpace = undefined;
temp_path.len = std.unicode.wtf8ToWtf16Le(&temp_path.data, path) catch |err| switch (err) {
error.InvalidWtf8 => return error.BadPathName,
};
temp_path.data[temp_path.len] = 0;
return wToPrefixedFileW(dir, temp_path.span());
}
pub const Wtf16ToPrefixedFileWError = error{
AccessDenied,
BadPathName,
FileNotFound,
NameTooLong,
Unexpected,
};
/// Converts the `path` to WTF16, null-terminated. If the path contains any
/// namespace prefix, or is anything but a relative path (rooted, drive relative,
/// etc) the result will have the NT-style prefix `\??\`.
///
/// Similar to RtlDosPathNameToNtPathName_U with a few differences:
/// - Does not allocate on the heap.
/// - Relative paths are kept as relative unless they contain too many ..
/// components, in which case they are resolved against the `dir` if it
/// is non-null, or the CWD if it is null.
/// - Special case device names like COM1, NUL, etc are not handled specially (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 {
const nt_prefix = [_]u16{ '\\', '?', '?', '\\' };
if (hasCommonNtPrefix(u16, path)) {
// TODO: Figure out a way to design an API that can avoid the copy for NT,
// since it is always returned fully unmodified.
var path_space: PathSpace = undefined;
path_space.data[0..nt_prefix.len].* = nt_prefix;
const len_after_prefix = path.len - nt_prefix.len;
@memcpy(path_space.data[nt_prefix.len..][0..len_after_prefix], path[nt_prefix.len..]);
path_space.len = path.len;
path_space.data[path_space.len] = 0;
return path_space;
} else {
const path_type = std.fs.path.getWin32PathType(u16, path);
var path_space: PathSpace = undefined;
if (path_type == .local_device) {
switch (getLocalDevicePathType(u16, path)) {
.verbatim => {
path_space.data[0..nt_prefix.len].* = nt_prefix;
const len_after_prefix = path.len - nt_prefix.len;
@memcpy(path_space.data[nt_prefix.len..][0..len_after_prefix], path[nt_prefix.len..]);
path_space.len = path.len;
path_space.data[path_space.len] = 0;
return path_space;
},
.local_device, .fake_verbatim => {
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;
},
}
}
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;
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.
path_space.data[0..nt_prefix.len].* = nt_prefix;
if (path_type == .root_local_device) {
// `\\.` and `\\?` always get converted to `\??\` exactly, so
// we can just stop here
path_space.len = 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 Io.Dir.cwd()
if (dir.? == Io.Dir.cwd().handle) {
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;
}
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>`
assert(path_space.data[path_buf_offset] == '\\');
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;
}
}
/// 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
@ -3725,39 +3193,6 @@ pub fn hasCommonNtPrefix(comptime T: type, path: []const T) bool {
return mem.startsWith(T, path, expected_prefix);
}
const LocalDevicePathType = enum {
/// `\\.\` (path separators can be `\` or `/`)
local_device,
/// `\\?\`
/// When converted to an NT path, everything past the prefix is left
/// untouched and `\\?\` is replaced by `\??\`.
verbatim,
/// `\\?\` without all path separators being `\`.
/// This seems to be recognized as a prefix, but the 'verbatim' aspect
/// is not respected (i.e. if `//?/C:/foo` is converted to an NT path,
/// it will become `\??\C:\foo` [it will be canonicalized and the //?/ won't
/// be treated as part of the final path])
fake_verbatim,
};
/// Only relevant for Win32 -> NT path conversion.
/// Asserts `path` is of type `std.fs.path.Win32PathType.local_device`.
fn getLocalDevicePathType(comptime T: type, path: []const T) LocalDevicePathType {
if (std.debug.runtime_safety) {
assert(std.fs.path.getWin32PathType(T, path) == .local_device);
}
const backslash = mem.nativeToLittle(T, '\\');
const all_backslash = path[0] == backslash and
path[1] == backslash and
path[3] == backslash;
return switch (path[2]) {
mem.nativeToLittle(T, '?') => if (all_backslash) .verbatim else .fake_verbatim,
mem.nativeToLittle(T, '.') => .local_device,
else => unreachable,
};
}
/// Similar to `RtlNtPathNameToDosPathName` but does not do any heap allocation.
/// The possible transformations are:
/// \??\C:\Some\Path -> C:\Some\Path

View file

@ -1,339 +0,0 @@
const std = @import("../../std.zig");
const builtin = @import("builtin");
const windows = std.os.windows;
const mem = std.mem;
const testing = std.testing;
/// Wrapper around RtlDosPathNameToNtPathName_U for use in comparing
/// the behavior of RtlDosPathNameToNtPathName_U with wToPrefixedFileW
/// Note: RtlDosPathNameToNtPathName_U is not used in the Zig implementation
// because it allocates.
fn RtlDosPathNameToNtPathName_U(path: [:0]const u16) !windows.PathSpace {
var out: windows.UNICODE_STRING = undefined;
const rc = windows.ntdll.RtlDosPathNameToNtPathName_U(path, &out, null, null);
if (rc != windows.TRUE) return error.BadPathName;
defer windows.ntdll.RtlFreeUnicodeString(&out);
var path_space: windows.PathSpace = undefined;
const out_path = out.Buffer.?[0 .. out.Length / 2];
@memcpy(path_space.data[0..out_path.len], out_path);
path_space.len = out.Length / 2;
path_space.data[path_space.len] = 0;
return path_space;
}
/// Test that the Zig conversion matches the expected_path (for instances where
/// the Zig implementation intentionally diverges from what RtlDosPathNameToNtPathName_U does).
fn testToPrefixedFileNoOracle(comptime path: []const u8, comptime expected_path: []const u8) !void {
const path_utf16 = std.unicode.utf8ToUtf16LeStringLiteral(path);
const expected_path_utf16 = std.unicode.utf8ToUtf16LeStringLiteral(expected_path);
const actual_path = try windows.wToPrefixedFileW(null, path_utf16);
std.testing.expectEqualSlices(u16, expected_path_utf16, actual_path.span()) catch |e| {
std.debug.print("got '{f}', expected '{f}'\n", .{ std.unicode.fmtUtf16Le(actual_path.span()), std.unicode.fmtUtf16Le(expected_path_utf16) });
return e;
};
}
/// Test that the Zig conversion matches the expected_path and that the
/// expected_path matches the conversion that RtlDosPathNameToNtPathName_U does.
fn testToPrefixedFileWithOracle(comptime path: []const u8, comptime expected_path: []const u8) !void {
try testToPrefixedFileNoOracle(path, expected_path);
try testToPrefixedFileOnlyOracle(path);
}
/// Test that the Zig conversion matches the conversion that RtlDosPathNameToNtPathName_U does.
fn testToPrefixedFileOnlyOracle(comptime path: []const u8) !void {
const path_utf16 = std.unicode.utf8ToUtf16LeStringLiteral(path);
const zig_result = try windows.wToPrefixedFileW(null, path_utf16);
const win32_api_result = try RtlDosPathNameToNtPathName_U(path_utf16);
std.testing.expectEqualSlices(u16, win32_api_result.span(), zig_result.span()) catch |e| {
std.debug.print("got '{f}', expected '{f}'\n", .{ std.unicode.fmtUtf16Le(zig_result.span()), std.unicode.fmtUtf16Le(win32_api_result.span()) });
return e;
};
}
test "toPrefixedFileW" {
if (builtin.os.tag != .windows) return error.SkipZigTest;
// Most test cases come from https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
// Note that these tests do not actually touch the filesystem or care about whether or not
// any of the paths actually exist or are otherwise valid.
// Drive Absolute
try testToPrefixedFileWithOracle("X:\\ABC\\DEF", "\\??\\X:\\ABC\\DEF");
try testToPrefixedFileWithOracle("X:\\", "\\??\\X:\\");
try testToPrefixedFileWithOracle("X:\\ABC\\", "\\??\\X:\\ABC\\");
// Trailing . and space characters are stripped
try testToPrefixedFileWithOracle("X:\\ABC\\DEF. .", "\\??\\X:\\ABC\\DEF");
try testToPrefixedFileWithOracle("X:/ABC/DEF", "\\??\\X:\\ABC\\DEF");
try testToPrefixedFileWithOracle("X:\\ABC\\..\\XYZ", "\\??\\X:\\XYZ");
try testToPrefixedFileWithOracle("X:\\ABC\\..\\..\\..", "\\??\\X:\\");
// Drive letter casing is unchanged
try testToPrefixedFileWithOracle("x:\\", "\\??\\x:\\");
// Drive Relative
// These tests depend on the CWD of the specified drive letter which can vary,
// so instead we just test that the Zig implementation matches the result of
// RtlDosPathNameToNtPathName_U.
// TODO: Setting the =X: environment variable didn't seem to affect
// RtlDosPathNameToNtPathName_U, not sure why that is but getting that
// to work could be an avenue to making these cases environment-independent.
// All -> are examples of the result if the X drive's cwd was X:\ABC
try testToPrefixedFileOnlyOracle("X:DEF\\GHI"); // -> \??\X:\ABC\DEF\GHI
try testToPrefixedFileOnlyOracle("X:"); // -> \??\X:\ABC
try testToPrefixedFileOnlyOracle("X:DEF. ."); // -> \??\X:\ABC\DEF
try testToPrefixedFileOnlyOracle("X:ABC\\..\\XYZ"); // -> \??\X:\ABC\XYZ
try testToPrefixedFileOnlyOracle("X:ABC\\..\\..\\.."); // -> \??\X:\
try testToPrefixedFileOnlyOracle("x:"); // -> \??\X:\ABC
// Rooted
// These tests depend on the drive letter of the CWD which can vary, so
// instead we just test that the Zig implementation matches the result of
// RtlDosPathNameToNtPathName_U.
// TODO: Getting the CWD path, getting the drive letter from it, and using it to
// construct the expected NT paths could be an avenue to making these cases
// environment-independent and therefore able to use testToPrefixedFileWithOracle.
// All -> are examples of the result if the CWD's drive letter was X
try testToPrefixedFileOnlyOracle("\\ABC\\DEF"); // -> \??\X:\ABC\DEF
try testToPrefixedFileOnlyOracle("\\"); // -> \??\X:\
try testToPrefixedFileOnlyOracle("\\ABC\\DEF. ."); // -> \??\X:\ABC\DEF
try testToPrefixedFileOnlyOracle("/ABC/DEF"); // -> \??\X:\ABC\DEF
try testToPrefixedFileOnlyOracle("\\ABC\\..\\XYZ"); // -> \??\X:\XYZ
try testToPrefixedFileOnlyOracle("\\ABC\\..\\..\\.."); // -> \??\X:\
// Relative
// These cases differ in functionality to RtlDosPathNameToNtPathName_U.
// Relative paths remain relative if they don't have enough .. components
// to error with TooManyParentDirs
try testToPrefixedFileNoOracle("ABC\\DEF", "ABC\\DEF");
// TODO: enable this if trailing . and spaces are stripped from relative paths
//try testToPrefixedFileNoOracle("ABC\\DEF. .", "ABC\\DEF");
try testToPrefixedFileNoOracle("ABC/DEF", "ABC\\DEF");
try testToPrefixedFileNoOracle("./ABC/.././DEF", "DEF");
// TooManyParentDirs, so resolved relative to the CWD
// All -> are examples of the result if the CWD was X:\ABC\DEF
try testToPrefixedFileOnlyOracle("..\\GHI"); // -> \??\X:\ABC\GHI
try testToPrefixedFileOnlyOracle("GHI\\..\\..\\.."); // -> \??\X:\
// UNC Absolute
try testToPrefixedFileWithOracle("\\\\server\\share\\ABC\\DEF", "\\??\\UNC\\server\\share\\ABC\\DEF");
try testToPrefixedFileWithOracle("\\\\server", "\\??\\UNC\\server");
try testToPrefixedFileWithOracle("\\\\server\\share", "\\??\\UNC\\server\\share");
try testToPrefixedFileWithOracle("\\\\server\\share\\ABC. .", "\\??\\UNC\\server\\share\\ABC");
try testToPrefixedFileWithOracle("//server/share/ABC/DEF", "\\??\\UNC\\server\\share\\ABC\\DEF");
try testToPrefixedFileWithOracle("\\\\server\\share\\ABC\\..\\XYZ", "\\??\\UNC\\server\\share\\XYZ");
try testToPrefixedFileWithOracle("\\\\server\\share\\ABC\\..\\..\\..", "\\??\\UNC\\server\\share");
// Local Device
try testToPrefixedFileWithOracle("\\\\.\\COM20", "\\??\\COM20");
try testToPrefixedFileWithOracle("\\\\.\\pipe\\mypipe", "\\??\\pipe\\mypipe");
try testToPrefixedFileWithOracle("\\\\.\\X:\\ABC\\DEF. .", "\\??\\X:\\ABC\\DEF");
try testToPrefixedFileWithOracle("\\\\.\\X:/ABC/DEF", "\\??\\X:\\ABC\\DEF");
try testToPrefixedFileWithOracle("\\\\.\\X:\\ABC\\..\\XYZ", "\\??\\X:\\XYZ");
// Can replace the first component of the path (contrary to drive absolute and UNC absolute paths)
try testToPrefixedFileWithOracle("\\\\.\\X:\\ABC\\..\\..\\C:\\", "\\??\\C:\\");
try testToPrefixedFileWithOracle("\\\\.\\pipe\\mypipe\\..\\notmine", "\\??\\pipe\\notmine");
// Special-case device names
// TODO: Enable once these are supported
// more cases to test here: https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
//try testToPrefixedFileWithOracle("COM1", "\\??\\COM1");
// Sometimes the special-cased device names are not respected
try testToPrefixedFileWithOracle("\\\\.\\X:\\COM1", "\\??\\X:\\COM1");
try testToPrefixedFileWithOracle("\\\\abc\\xyz\\COM1", "\\??\\UNC\\abc\\xyz\\COM1");
// Verbatim
// Left untouched except \\?\ is replaced by \??\
try testToPrefixedFileWithOracle("\\\\?\\X:", "\\??\\X:");
try testToPrefixedFileWithOracle("\\\\?\\X:\\COM1", "\\??\\X:\\COM1");
try testToPrefixedFileWithOracle("\\\\?\\X:/ABC/DEF. .", "\\??\\X:/ABC/DEF. .");
try testToPrefixedFileWithOracle("\\\\?\\X:\\ABC\\..\\..\\..", "\\??\\X:\\ABC\\..\\..\\..");
// NT Namespace
// Fully unmodified
try testToPrefixedFileWithOracle("\\??\\X:", "\\??\\X:");
try testToPrefixedFileWithOracle("\\??\\X:\\COM1", "\\??\\X:\\COM1");
try testToPrefixedFileWithOracle("\\??\\X:/ABC/DEF. .", "\\??\\X:/ABC/DEF. .");
try testToPrefixedFileWithOracle("\\??\\X:\\ABC\\..\\..\\..", "\\??\\X:\\ABC\\..\\..\\..");
// 'Fake' Verbatim
// If the prefix looks like the verbatim prefix but not all path separators in the
// prefix are backslashes, then it gets canonicalized and the prefix is dropped in favor
// of the NT prefix.
try testToPrefixedFileWithOracle("//?/C:/ABC", "\\??\\C:\\ABC");
// 'Fake' NT
// If the prefix looks like the NT prefix but not all path separators in the prefix
// are backslashes, then it gets canonicalized and the /??/ is not dropped but
// rather treated as part of the path. In other words, the path is treated
// as a rooted path, so the final path is resolved relative to the CWD's
// drive letter.
// The -> shows an example of the result if the CWD's drive letter was X
try testToPrefixedFileOnlyOracle("/??/C:/ABC"); // -> \??\X:\??\C:\ABC
// Root Local Device
// \\. and \\? always get converted to \??\
try testToPrefixedFileWithOracle("\\\\.", "\\??\\");
try testToPrefixedFileWithOracle("\\\\?", "\\??\\");
try testToPrefixedFileWithOracle("//?", "\\??\\");
try testToPrefixedFileWithOracle("//.", "\\??\\");
}
fn testRemoveDotDirs(str: []const u8, expected: []const u8) !void {
const mutable = try testing.allocator.dupe(u8, str);
defer testing.allocator.free(mutable);
const actual = mutable[0..try windows.removeDotDirsSanitized(u8, mutable)];
try testing.expect(mem.eql(u8, actual, expected));
}
fn testRemoveDotDirsError(err: anyerror, str: []const u8) !void {
const mutable = try testing.allocator.dupe(u8, str);
defer testing.allocator.free(mutable);
try testing.expectError(err, windows.removeDotDirsSanitized(u8, mutable));
}
test "removeDotDirs" {
try testRemoveDotDirs("", "");
try testRemoveDotDirs(".", "");
try testRemoveDotDirs(".\\", "");
try testRemoveDotDirs(".\\.", "");
try testRemoveDotDirs(".\\.\\", "");
try testRemoveDotDirs(".\\.\\.", "");
try testRemoveDotDirs("a", "a");
try testRemoveDotDirs("a\\", "a\\");
try testRemoveDotDirs("a\\b", "a\\b");
try testRemoveDotDirs("a\\.", "a\\");
try testRemoveDotDirs("a\\b\\.", "a\\b\\");
try testRemoveDotDirs("a\\.\\b", "a\\b");
try testRemoveDotDirs(".a", ".a");
try testRemoveDotDirs(".a\\", ".a\\");
try testRemoveDotDirs(".a\\.b", ".a\\.b");
try testRemoveDotDirs(".a\\.", ".a\\");
try testRemoveDotDirs(".a\\.\\.", ".a\\");
try testRemoveDotDirs(".a\\.\\.\\.b", ".a\\.b");
try testRemoveDotDirs(".a\\.\\.\\.b\\", ".a\\.b\\");
try testRemoveDotDirsError(error.TooManyParentDirs, "..");
try testRemoveDotDirsError(error.TooManyParentDirs, "..\\");
try testRemoveDotDirsError(error.TooManyParentDirs, ".\\..\\");
try testRemoveDotDirsError(error.TooManyParentDirs, ".\\.\\..\\");
try testRemoveDotDirs("a\\..", "");
try testRemoveDotDirs("a\\..\\", "");
try testRemoveDotDirs("a\\..\\.", "");
try testRemoveDotDirs("a\\..\\.\\", "");
try testRemoveDotDirs("a\\..\\.\\.", "");
try testRemoveDotDirsError(error.TooManyParentDirs, "a\\..\\.\\.\\..");
try testRemoveDotDirs("a\\..\\.\\.\\b", "b");
try testRemoveDotDirs("a\\..\\.\\.\\b\\", "b\\");
try testRemoveDotDirs("a\\..\\.\\.\\b\\.", "b\\");
try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\", "b\\");
try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\..", "");
try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\..\\", "");
try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\..\\.", "");
try testRemoveDotDirsError(error.TooManyParentDirs, "a\\..\\.\\.\\b\\.\\..\\.\\..");
try testRemoveDotDirs("a\\b\\..\\", "a\\");
try testRemoveDotDirs("a\\b\\..\\c", "a\\c");
}
const RTL_PATH_TYPE = enum(c_int) {
Unknown,
UncAbsolute,
DriveAbsolute,
DriveRelative,
Rooted,
Relative,
LocalDevice,
RootLocalDevice,
};
pub extern "ntdll" fn RtlDetermineDosPathNameType_U(
Path: [*:0]const u16,
) callconv(.winapi) RTL_PATH_TYPE;
test "getWin32PathType vs RtlDetermineDosPathNameType_U" {
if (builtin.os.tag != .windows) return error.SkipZigTest;
var buf: std.ArrayList(u16) = .empty;
defer buf.deinit(std.testing.allocator);
var wtf8_buf: std.ArrayList(u8) = .empty;
defer wtf8_buf.deinit(std.testing.allocator);
var random = std.Random.DefaultPrng.init(std.testing.random_seed);
const rand = random.random();
for (0..1000) |_| {
buf.clearRetainingCapacity();
const path = try getRandomWtf16Path(std.testing.allocator, &buf, rand);
wtf8_buf.clearRetainingCapacity();
const wtf8_len = std.unicode.calcWtf8Len(path);
try wtf8_buf.ensureTotalCapacity(std.testing.allocator, wtf8_len);
wtf8_buf.items.len = wtf8_len;
std.debug.assert(std.unicode.wtf16LeToWtf8(wtf8_buf.items, path) == wtf8_len);
const windows_type = RtlDetermineDosPathNameType_U(path);
const wtf16_type = std.fs.path.getWin32PathType(u16, path);
const wtf8_type = std.fs.path.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: std.fs.path.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

@ -639,7 +639,7 @@ test "zig fmt: array types last token" {
test "zig fmt: sentinel-terminated array type" {
try testCanonical(
\\pub fn cStrToPrefixedFileW(s: [*:0]const u8) ![PATH_MAX_WIDE:0]u16 {
\\pub fn foobar(s: [*:0]const u8) ![PATH_MAX_WIDE:0]u16 {
\\ return sliceToPrefixedFileW(mem.toSliceConst(u8, s));
\\}
\\

View file

@ -9,6 +9,7 @@ pub fn build(b: *std.Build) void {
const target = b.graph.host;
if (builtin.os.tag == .wasi) return;
if (builtin.os.tag == .windows) return;
const lib = b.addLibrary(.{
.linkage = .dynamic,