authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-02-03 21:08:42-08:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-02-04 16:27:13-08:00
log2a193a39871ad098931cecc1154cc42278c28a2b
tree2b94a0a50014442dfba75922392d34d840931f37
parentb49dc5eb70b804a2354a01bd60c686f0f2279f2d

std: move GetFinalPathNameByHandle to Io.Threaded

unfortunately this function calls NtCreateFile so it has to participate in cancelation

8 files changed, 907 insertions(+), 1006 deletions(-)

lib/std/Build/Watch.zig+1-1
...@@ -358,7 +358,7 @@ const Os = switch (builtin.os.tag) {...@@ -358,7 +358,7 @@ const Os = switch (builtin.os.tag) {
358 var dir_handle: windows.HANDLE = undefined;358 var dir_handle: windows.HANDLE = undefined;
359 const root_fd = path.root_dir.handle.handle;359 const root_fd = path.root_dir.handle.handle;
360 const sub_path = path.subPathOrDot();360 const sub_path = path.subPathOrDot();
361 const sub_path_w = try windows.sliceToPrefixedFileW(root_fd, sub_path);361 const sub_path_w = try std.Io.Threaded.sliceToPrefixedFileW(root_fd, sub_path); // TODO eliminate this call
362 const path_len_bytes = std.math.cast(u16, sub_path_w.len * 2) orelse return error.NameTooLong;362 const path_len_bytes = std.math.cast(u16, sub_path_w.len * 2) orelse return error.NameTooLong;
363363
364 var nt_name = windows.UNICODE_STRING{364 var nt_name = windows.UNICODE_STRING{
lib/std/Io/Threaded.zig+568-30
...@@ -3257,7 +3257,7 @@ fn dirCreateDirWindows(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, pe...@@ -3257,7 +3257,7 @@ fn dirCreateDirWindows(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, pe
3257 const t: *Threaded = @ptrCast(@alignCast(userdata));3257 const t: *Threaded = @ptrCast(@alignCast(userdata));
3258 _ = t;3258 _ = t;
32593259
3260 const sub_path_w = try windows.sliceToPrefixedFileW(dir.handle, sub_path);3260 const sub_path_w = try sliceToPrefixedFileW(dir.handle, sub_path);
3261 _ = permissions; // TODO use this value3261 _ = permissions; // TODO use this value
32623262
3263 const syscall: Syscall = try .start();3263 const syscall: Syscall = try .start();
...@@ -3363,7 +3363,7 @@ fn dirCreateDirPathOpenWindows(...@@ -3363,7 +3363,7 @@ fn dirCreateDirPathOpenWindows(
3363 };3363 };
33643364
3365 components: while (true) {3365 components: while (true) {
3366 const sub_path_w_array = try w.sliceToPrefixedFileW(dir.handle, component.path);3366 const sub_path_w_array = try sliceToPrefixedFileW(dir.handle, component.path);
3367 const sub_path_w = sub_path_w_array.span();3367 const sub_path_w = sub_path_w_array.span();
3368 const is_last = it.peekNext() == null;3368 const is_last = it.peekNext() == null;
3369 const create_disposition: w.FILE.CREATE_DISPOSITION = if (is_last) .OPEN_IF else .CREATE;3369 const create_disposition: w.FILE.CREATE_DISPOSITION = if (is_last) .OPEN_IF else .CREATE;
...@@ -4064,7 +4064,7 @@ fn dirAccessWindows(...@@ -4064,7 +4064,7 @@ fn dirAccessWindows(
40644064
4065 _ = options; // TODO4065 _ = options; // TODO
40664066
4067 const sub_path_w_array = try windows.sliceToPrefixedFileW(dir.handle, sub_path);4067 const sub_path_w_array = try sliceToPrefixedFileW(dir.handle, sub_path);
4068 const sub_path_w = sub_path_w_array.span();4068 const sub_path_w = sub_path_w_array.span();
40694069
4070 if (sub_path_w[0] == '.' and sub_path_w[1] == 0) return;4070 if (sub_path_w[0] == '.' and sub_path_w[1] == 0) return;
...@@ -4285,7 +4285,7 @@ fn dirCreateFileWindows(...@@ -4285,7 +4285,7 @@ fn dirCreateFileWindows(
4285 if (std.mem.eql(u8, sub_path, ".")) return error.IsDir;4285 if (std.mem.eql(u8, sub_path, ".")) return error.IsDir;
4286 if (std.mem.eql(u8, sub_path, "..")) return error.IsDir;4286 if (std.mem.eql(u8, sub_path, "..")) return error.IsDir;
42874287
4288 const sub_path_w_array = try windows.sliceToPrefixedFileW(dir.handle, sub_path);4288 const sub_path_w_array = try sliceToPrefixedFileW(dir.handle, sub_path);
4289 const sub_path_w = sub_path_w_array.span();4289 const sub_path_w = sub_path_w_array.span();
4290 const path_len_bytes = std.math.cast(u16, sub_path_w.len * 2) orelse return error.NameTooLong;4290 const path_len_bytes = std.math.cast(u16, sub_path_w.len * 2) orelse return error.NameTooLong;
42914291
...@@ -4887,7 +4887,7 @@ fn dirOpenFileWindows(...@@ -4887,7 +4887,7 @@ fn dirOpenFileWindows(
4887) File.OpenError!File {4887) File.OpenError!File {
4888 const t: *Threaded = @ptrCast(@alignCast(userdata));4888 const t: *Threaded = @ptrCast(@alignCast(userdata));
4889 _ = t;4889 _ = t;
4890 const sub_path_w_array = try windows.sliceToPrefixedFileW(dir.handle, sub_path);4890 const sub_path_w_array = try sliceToPrefixedFileW(dir.handle, sub_path);
4891 const sub_path_w = sub_path_w_array.span();4891 const sub_path_w = sub_path_w_array.span();
4892 const dir_handle = if (Dir.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle;4892 const dir_handle = if (Dir.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle;
4893 return dirOpenFileWtf16(dir_handle, sub_path_w, flags);4893 return dirOpenFileWtf16(dir_handle, sub_path_w, flags);
...@@ -5151,7 +5151,7 @@ fn dirOpenDirPosix(...@@ -5151,7 +5151,7 @@ fn dirOpenDirPosix(
5151 _ = t;5151 _ = t;
51525152
5153 if (is_windows) {5153 if (is_windows) {
5154 const sub_path_w = try windows.sliceToPrefixedFileW(dir.handle, sub_path);5154 const sub_path_w = try sliceToPrefixedFileW(dir.handle, sub_path);
5155 return dirOpenDirWindows(dir, sub_path_w.span(), options);5155 return dirOpenDirWindows(dir, sub_path_w.span(), options);
5156 }5156 }
51575157
...@@ -5984,7 +5984,7 @@ fn dirRealPathFileWindows(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8,...@@ -5984,7 +5984,7 @@ fn dirRealPathFileWindows(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8,
5984 const t: *Threaded = @ptrCast(@alignCast(userdata));5984 const t: *Threaded = @ptrCast(@alignCast(userdata));
5985 _ = t;5985 _ = t;
59865986
5987 var path_name_w = try windows.sliceToPrefixedFileW(dir.handle, sub_path);5987 var path_name_w = try sliceToPrefixedFileW(dir.handle, sub_path);
59885988
5989 const h_file = handle: {5989 const h_file = handle: {
5990 const syscall: Syscall = try .start();5990 const syscall: Syscall = try .start();
...@@ -6016,9 +6016,7 @@ fn dirRealPathFileWindows(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8,...@@ -6016,9 +6016,7 @@ fn dirRealPathFileWindows(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8,
60166016
6017fn realPathWindows(h_file: windows.HANDLE, out_buffer: []u8) File.RealPathError!usize {6017fn realPathWindows(h_file: windows.HANDLE, out_buffer: []u8) File.RealPathError!usize {
6018 var wide_buf: [windows.PATH_MAX_WIDE]u16 = undefined;6018 var wide_buf: [windows.PATH_MAX_WIDE]u16 = undefined;
6019 // TODO move GetFinalPathNameByHandle logic into Io.Threaded and add cancel checks6019 const wide_slice = try GetFinalPathNameByHandle(h_file, .{}, &wide_buf);
6020 try Thread.checkCancel();
6021 const wide_slice = try windows.GetFinalPathNameByHandle(h_file, .{}, &wide_buf);
60226020
6023 const len = std.unicode.calcWtf8Len(wide_slice);6021 const len = std.unicode.calcWtf8Len(wide_slice);
6024 if (len > out_buffer.len)6022 if (len > out_buffer.len)
...@@ -6027,6 +6025,552 @@ fn realPathWindows(h_file: windows.HANDLE, out_buffer: []u8) File.RealPathError!...@@ -6027,6 +6025,552 @@ fn realPathWindows(h_file: windows.HANDLE, out_buffer: []u8) File.RealPathError!
6027 return std.unicode.wtf16LeToWtf8(out_buffer, wide_slice);6025 return std.unicode.wtf16LeToWtf8(out_buffer, wide_slice);
6028}6026}
60296027
6028/// Specifies how to format volume path in the result of `GetFinalPathNameByHandle`.
6029/// Defaults to DOS volume names.
6030pub const GetFinalPathNameByHandleFormat = struct {
6031 volume_name: enum {
6032 /// Format as DOS volume name
6033 Dos,
6034 /// Format as NT volume name
6035 Nt,
6036 } = .Dos,
6037};
6038
6039pub const GetFinalPathNameByHandleError = error{
6040 AccessDenied,
6041 FileNotFound,
6042 NameTooLong,
6043 /// The volume does not contain a recognized file system. File system
6044 /// drivers might not be loaded, or the volume may be corrupt.
6045 UnrecognizedVolume,
6046} || Io.Cancelable || Io.UnexpectedError;
6047
6048/// Returns canonical (normalized) path of handle.
6049/// Use `GetFinalPathNameByHandleFormat` to specify whether the path is meant to include
6050/// NT or DOS volume name (e.g., `\Device\HarddiskVolume0\foo.txt` versus `C:\foo.txt`).
6051/// If DOS volume name format is selected, note that this function does *not* prepend
6052/// `\\?\` prefix to the resultant path.
6053pub fn GetFinalPathNameByHandle(
6054 hFile: windows.HANDLE,
6055 fmt: GetFinalPathNameByHandleFormat,
6056 out_buffer: []u16,
6057) GetFinalPathNameByHandleError![]u16 {
6058 const final_path = QueryObjectName(hFile, out_buffer) catch |err| switch (err) {
6059 // we assume InvalidHandle is close enough to FileNotFound in semantics
6060 // to not further complicate the error set
6061 error.InvalidHandle => return error.FileNotFound,
6062 else => |e| return e,
6063 };
6064
6065 switch (fmt.volume_name) {
6066 .Nt => {
6067 // the returned path is already in .Nt format
6068 return final_path;
6069 },
6070 .Dos => {
6071 // parse the string to separate volume path from file path
6072 const device_prefix = std.unicode.utf8ToUtf16LeStringLiteral("\\Device\\");
6073
6074 // We aren't entirely sure of the structure of the path returned by
6075 // QueryObjectName in all contexts/environments.
6076 // This code is written to cover the various cases that have
6077 // been encountered and solved appropriately. But note that there's
6078 // no easy way to verify that they have all been tackled!
6079 // (Unless you, the reader knows of one then please do action that!)
6080 if (!std.mem.startsWith(u16, final_path, device_prefix)) {
6081 // Wine seems to return NT namespaced paths starting with \??\ from QueryObjectName
6082 // (e.g. `\??\Z:\some\path\to\a\file.txt`), in which case we can just strip the
6083 // prefix to turn it into an absolute path.
6084 // https://github.com/ziglang/zig/issues/26029
6085 // https://bugs.winehq.org/show_bug.cgi?id=39569
6086 return windows.ntToWin32Namespace(final_path, out_buffer) catch |err| switch (err) {
6087 error.NotNtPath => return error.Unexpected,
6088 error.NameTooLong => |e| return e,
6089 };
6090 }
6091
6092 const file_path_begin_index = std.mem.findPos(u16, final_path, device_prefix.len, &[_]u16{'\\'}) orelse unreachable;
6093 const volume_name_u16 = final_path[0..file_path_begin_index];
6094 const device_name_u16 = volume_name_u16[device_prefix.len..];
6095 const file_name_u16 = final_path[file_path_begin_index..];
6096
6097 // MUP is Multiple UNC Provider, and indicates that the path is a UNC
6098 // path. In this case, the canonical UNC path can be gotten by just
6099 // dropping the \Device\Mup\ and making sure the path begins with \\
6100 if (std.mem.eql(u16, device_name_u16, std.unicode.utf8ToUtf16LeStringLiteral("Mup"))) {
6101 out_buffer[0] = '\\';
6102 @memmove(out_buffer[1..][0..file_name_u16.len], file_name_u16);
6103 return out_buffer[0 .. 1 + file_name_u16.len];
6104 }
6105
6106 // Get DOS volume name. DOS volume names are actually symbolic link objects to the
6107 // actual NT volume. For example:
6108 // (NT) \Device\HarddiskVolume4 => (DOS) \DosDevices\C: == (DOS) C:
6109 const MIN_SIZE = @sizeOf(windows.MOUNTMGR_MOUNT_POINT) + windows.MAX_PATH;
6110 // We initialize the input buffer to all zeros for convenience since
6111 // `DeviceIoControl` with `IOCTL_MOUNTMGR_QUERY_POINTS` expects this.
6112 var input_buf: [MIN_SIZE]u8 align(@alignOf(windows.MOUNTMGR_MOUNT_POINT)) = [_]u8{0} ** MIN_SIZE;
6113 var output_buf: [MIN_SIZE * 4]u8 align(@alignOf(windows.MOUNTMGR_MOUNT_POINTS)) = undefined;
6114
6115 // This surprising path is a filesystem path to the mount manager on Windows.
6116 // Source: https://stackoverflow.com/questions/3012828/using-ioctl-mountmgr-query-points
6117 // This is the NT namespaced version of \\.\MountPointManager
6118 const mgmt_path_u16 = std.unicode.utf8ToUtf16LeStringLiteral("\\??\\MountPointManager");
6119 const mgmt_handle = windows.OpenFile(mgmt_path_u16, .{
6120 .access_mask = .{ .STANDARD = .{ .SYNCHRONIZE = true } },
6121 .creation = .OPEN,
6122 }) catch |err| switch (err) {
6123 error.IsDir => return error.Unexpected,
6124 error.NotDir => return error.Unexpected,
6125 error.NoDevice => return error.Unexpected,
6126 error.AccessDenied => return error.Unexpected,
6127 error.PipeBusy => return error.Unexpected,
6128 error.PathAlreadyExists => return error.Unexpected,
6129 error.WouldBlock => return error.Unexpected,
6130 error.NetworkNotFound => return error.Unexpected,
6131 error.AntivirusInterference => return error.Unexpected,
6132 error.BadPathName => return error.Unexpected,
6133 error.OperationCanceled => @panic("TODO: better integrate cancelation"),
6134 else => |e| return e,
6135 };
6136 defer windows.CloseHandle(mgmt_handle);
6137
6138 var input_struct: *windows.MOUNTMGR_MOUNT_POINT = @ptrCast(&input_buf[0]);
6139 input_struct.DeviceNameOffset = @sizeOf(windows.MOUNTMGR_MOUNT_POINT);
6140 input_struct.DeviceNameLength = @intCast(volume_name_u16.len * 2);
6141 @memcpy(input_buf[@sizeOf(windows.MOUNTMGR_MOUNT_POINT)..][0 .. volume_name_u16.len * 2], @as([*]const u8, @ptrCast(volume_name_u16.ptr)));
6142
6143 {
6144 const rc = windows.DeviceIoControl(mgmt_handle, windows.IOCTL.MOUNTMGR.QUERY_POINTS, .{ .in = &input_buf, .out = &output_buf });
6145 switch (rc) {
6146 .SUCCESS => {},
6147 .OBJECT_NAME_NOT_FOUND => return error.FileNotFound,
6148 else => return windows.unexpectedStatus(rc),
6149 }
6150 }
6151 const mount_points_struct: *const windows.MOUNTMGR_MOUNT_POINTS = @ptrCast(&output_buf[0]);
6152
6153 const mount_points = @as(
6154 [*]const windows.MOUNTMGR_MOUNT_POINT,
6155 @ptrCast(&mount_points_struct.MountPoints[0]),
6156 )[0..mount_points_struct.NumberOfMountPoints];
6157
6158 for (mount_points) |mount_point| {
6159 const symlink = @as(
6160 [*]const u16,
6161 @ptrCast(@alignCast(&output_buf[mount_point.SymbolicLinkNameOffset])),
6162 )[0 .. mount_point.SymbolicLinkNameLength / 2];
6163
6164 // Look for `\DosDevices\` prefix. We don't really care if there are more than one symlinks
6165 // with traditional DOS drive letters, so pick the first one available.
6166 var prefix_buf = std.unicode.utf8ToUtf16LeStringLiteral("\\DosDevices\\");
6167 const prefix = prefix_buf[0..prefix_buf.len];
6168
6169 if (std.mem.startsWith(u16, symlink, prefix)) {
6170 const drive_letter = symlink[prefix.len..];
6171
6172 if (out_buffer.len < drive_letter.len + file_name_u16.len) return error.NameTooLong;
6173
6174 @memcpy(out_buffer[0..drive_letter.len], drive_letter);
6175 @memmove(out_buffer[drive_letter.len..][0..file_name_u16.len], file_name_u16);
6176 const total_len = drive_letter.len + file_name_u16.len;
6177
6178 // Validate that DOS does not contain any spurious nul bytes.
6179 assert(std.mem.findScalar(u16, out_buffer[0..total_len], 0) == null);
6180
6181 return out_buffer[0..total_len];
6182 } else if (mountmgrIsVolumeName(symlink)) {
6183 // If the symlink is a volume GUID like \??\Volume{383da0b0-717f-41b6-8c36-00500992b58d},
6184 // then it is a volume mounted as a path rather than a drive letter. We need to
6185 // query the mount manager again to get the DOS path for the volume.
6186
6187 // 49 is the maximum length accepted by mountmgrIsVolumeName
6188 const vol_input_size = @sizeOf(windows.MOUNTMGR_TARGET_NAME) + (49 * 2);
6189 var vol_input_buf: [vol_input_size]u8 align(@alignOf(windows.MOUNTMGR_TARGET_NAME)) = [_]u8{0} ** vol_input_size;
6190 // Note: If the path exceeds MAX_PATH, the Disk Management GUI doesn't accept the full path,
6191 // and instead if must be specified using a shortened form (e.g. C:\FOO~1\BAR~1\<...>).
6192 // However, just to be sure we can handle any path length, we use PATH_MAX_WIDE here.
6193 const min_output_size = @sizeOf(windows.MOUNTMGR_VOLUME_PATHS) + (windows.PATH_MAX_WIDE * 2);
6194 var vol_output_buf: [min_output_size]u8 align(@alignOf(windows.MOUNTMGR_VOLUME_PATHS)) = undefined;
6195
6196 var vol_input_struct: *windows.MOUNTMGR_TARGET_NAME = @ptrCast(&vol_input_buf[0]);
6197 vol_input_struct.DeviceNameLength = @intCast(symlink.len * 2);
6198 @memcpy(@as([*]windows.WCHAR, &vol_input_struct.DeviceName)[0..symlink.len], symlink);
6199
6200 const rc = windows.DeviceIoControl(mgmt_handle, windows.IOCTL.MOUNTMGR.QUERY_DOS_VOLUME_PATH, .{ .in = &vol_input_buf, .out = &vol_output_buf });
6201 switch (rc) {
6202 .SUCCESS => {},
6203 .UNRECOGNIZED_VOLUME => return error.UnrecognizedVolume,
6204 else => return windows.unexpectedStatus(rc),
6205 }
6206 const volume_paths_struct: *const windows.MOUNTMGR_VOLUME_PATHS = @ptrCast(&vol_output_buf[0]);
6207 const volume_path = std.mem.sliceTo(@as(
6208 [*]const u16,
6209 &volume_paths_struct.MultiSz,
6210 )[0 .. volume_paths_struct.MultiSzLength / 2], 0);
6211
6212 if (out_buffer.len < volume_path.len + file_name_u16.len) return error.NameTooLong;
6213
6214 // `out_buffer` currently contains the memory of `file_name_u16`, so it can overlap with where
6215 // we want to place the filename before returning. Here are the possible overlapping cases:
6216 //
6217 // out_buffer: [filename]
6218 // dest: [___(a)___] [___(b)___]
6219 //
6220 // In the case of (a), we need to copy forwards, and in the case of (b) we need
6221 // to copy backwards. We also need to do this before copying the volume path because
6222 // it could overwrite the file_name_u16 memory.
6223 const file_name_dest = out_buffer[volume_path.len..][0..file_name_u16.len];
6224 @memmove(file_name_dest, file_name_u16);
6225 @memcpy(out_buffer[0..volume_path.len], volume_path);
6226 const total_len = volume_path.len + file_name_u16.len;
6227
6228 // Validate that DOS does not contain any spurious nul bytes.
6229 assert(std.mem.findScalar(u16, out_buffer[0..total_len], 0) == null);
6230
6231 return out_buffer[0..total_len];
6232 }
6233 }
6234
6235 // If we've ended up here, then something went wrong/is corrupted in the OS,
6236 // so error out!
6237 return error.FileNotFound;
6238 },
6239 }
6240}
6241
6242test GetFinalPathNameByHandle {
6243 if (builtin.os.tag != .windows)
6244 return;
6245
6246 //any file will do
6247 var tmp = std.testing.tmpDir(.{});
6248 defer tmp.cleanup();
6249 const handle = tmp.dir.handle;
6250 var buffer: [windows.PATH_MAX_WIDE]u16 = undefined;
6251
6252 //check with sufficient size
6253 const nt_path = try GetFinalPathNameByHandle(handle, .{ .volume_name = .Nt }, &buffer);
6254 _ = try GetFinalPathNameByHandle(handle, .{ .volume_name = .Dos }, &buffer);
6255
6256 const required_len_in_u16 = nt_path.len + @divExact(@intFromPtr(nt_path.ptr) - @intFromPtr(&buffer), 2) + 1;
6257 //check with insufficient size
6258 try std.testing.expectError(error.NameTooLong, GetFinalPathNameByHandle(handle, .{ .volume_name = .Nt }, buffer[0 .. required_len_in_u16 - 1]));
6259 try std.testing.expectError(error.NameTooLong, GetFinalPathNameByHandle(handle, .{ .volume_name = .Dos }, buffer[0 .. required_len_in_u16 - 1]));
6260
6261 //check with exactly-sufficient size
6262 _ = try GetFinalPathNameByHandle(handle, .{ .volume_name = .Nt }, buffer[0..required_len_in_u16]);
6263 _ = try GetFinalPathNameByHandle(handle, .{ .volume_name = .Dos }, buffer[0..required_len_in_u16]);
6264}
6265
6266/// Equivalent to the MOUNTMGR_IS_VOLUME_NAME macro in mountmgr.h
6267fn mountmgrIsVolumeName(name: []const u16) bool {
6268 return (name.len == 48 or (name.len == 49 and name[48] == std.mem.nativeToLittle(u16, '\\'))) and
6269 name[0] == std.mem.nativeToLittle(u16, '\\') and
6270 (name[1] == std.mem.nativeToLittle(u16, '?') or name[1] == std.mem.nativeToLittle(u16, '\\')) and
6271 name[2] == std.mem.nativeToLittle(u16, '?') and
6272 name[3] == std.mem.nativeToLittle(u16, '\\') and
6273 std.mem.startsWith(u16, name[4..], std.unicode.utf8ToUtf16LeStringLiteral("Volume{")) and
6274 name[19] == std.mem.nativeToLittle(u16, '-') and
6275 name[24] == std.mem.nativeToLittle(u16, '-') and
6276 name[29] == std.mem.nativeToLittle(u16, '-') and
6277 name[34] == std.mem.nativeToLittle(u16, '-') and
6278 name[47] == std.mem.nativeToLittle(u16, '}');
6279}
6280
6281test mountmgrIsVolumeName {
6282 @setEvalBranchQuota(2000);
6283 const L = std.unicode.utf8ToUtf16LeStringLiteral;
6284 try std.testing.expect(mountmgrIsVolumeName(L("\\\\?\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}")));
6285 try std.testing.expect(mountmgrIsVolumeName(L("\\??\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}")));
6286 try std.testing.expect(mountmgrIsVolumeName(L("\\\\?\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}\\")));
6287 try std.testing.expect(mountmgrIsVolumeName(L("\\??\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}\\")));
6288 try std.testing.expect(!mountmgrIsVolumeName(L("\\\\.\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}")));
6289 try std.testing.expect(!mountmgrIsVolumeName(L("\\??\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}\\foo")));
6290 try std.testing.expect(!mountmgrIsVolumeName(L("\\??\\Volume{383da0b0-717f-41b6-8c36-00500992b58}")));
6291}
6292
6293pub const QueryObjectNameError = error{
6294 AccessDenied,
6295 InvalidHandle,
6296 NameTooLong,
6297 Unexpected,
6298};
6299
6300pub fn QueryObjectName(handle: windows.HANDLE, out_buffer: []u16) QueryObjectNameError![]u16 {
6301 const out_buffer_aligned = std.mem.alignInSlice(out_buffer, @alignOf(windows.OBJECT_NAME_INFORMATION)) orelse return error.NameTooLong;
6302
6303 const info: *windows.OBJECT_NAME_INFORMATION = @ptrCast(out_buffer_aligned);
6304 // buffer size is specified in bytes
6305 const out_buffer_len = std.math.cast(windows.ULONG, out_buffer_aligned.len * 2) orelse std.math.maxInt(windows.ULONG);
6306 // last argument would return the length required for full_buffer, not exposed here
6307 return switch (windows.ntdll.NtQueryObject(handle, .ObjectNameInformation, info, out_buffer_len, null)) {
6308 .SUCCESS => blk: {
6309 // info.Name.Buffer from ObQueryNameString is documented to be null (and MaximumLength == 0)
6310 // if the object was "unnamed", not sure if this can happen for file handles
6311 if (info.Name.MaximumLength == 0) break :blk error.Unexpected;
6312 // resulting string length is specified in bytes
6313 const path_length_unterminated = @divExact(info.Name.Length, 2);
6314 break :blk info.Name.Buffer.?[0..path_length_unterminated];
6315 },
6316 .ACCESS_DENIED => error.AccessDenied,
6317 .INVALID_HANDLE => error.InvalidHandle,
6318 // triggered when the buffer is too small for the OBJECT_NAME_INFORMATION object (.INFO_LENGTH_MISMATCH),
6319 // or if the buffer is too small for the file path returned (.BUFFER_OVERFLOW, .BUFFER_TOO_SMALL)
6320 .INFO_LENGTH_MISMATCH, .BUFFER_OVERFLOW, .BUFFER_TOO_SMALL => error.NameTooLong,
6321 else => |e| windows.unexpectedStatus(e),
6322 };
6323}
6324
6325test QueryObjectName {
6326 if (builtin.os.tag != .windows)
6327 return;
6328
6329 //any file will do; canonicalization works on NTFS junctions and symlinks, hardlinks remain separate paths.
6330 var tmp = std.testing.tmpDir(.{});
6331 defer tmp.cleanup();
6332 const handle = tmp.dir.handle;
6333 var out_buffer: [windows.PATH_MAX_WIDE]u16 = undefined;
6334
6335 const result_path = try QueryObjectName(handle, &out_buffer);
6336 const required_len_in_u16 = result_path.len + @divExact(@intFromPtr(result_path.ptr) - @intFromPtr(&out_buffer), 2) + 1;
6337 //insufficient size
6338 try std.testing.expectError(error.NameTooLong, QueryObjectName(handle, out_buffer[0 .. required_len_in_u16 - 1]));
6339 //exactly-sufficient size
6340 _ = try QueryObjectName(handle, out_buffer[0..required_len_in_u16]);
6341}
6342
6343const Wtf16ToPrefixedFileWError = error{
6344 AccessDenied,
6345 FileNotFound,
6346} || Dir.PathNameError || Io.Cancelable || Io.UnexpectedError;
6347
6348/// Converts the `path` to WTF16, null-terminated. If the path contains any
6349/// namespace prefix, or is anything but a relative path (rooted, drive relative,
6350/// etc) the result will have the NT-style prefix `\??\`.
6351///
6352/// Similar to RtlDosPathNameToNtPathName_U with a few differences:
6353/// - Does not allocate on the heap.
6354/// - Relative paths are kept as relative unless they contain too many ..
6355/// components, in which case they are resolved against the `dir` if it
6356/// is non-null, or the CWD if it is null.
6357/// - Special case device names like COM1, NUL, etc are not handled specially (TODO)
6358/// - . and space are not stripped from the end of relative paths (potential TODO)
6359pub fn wToPrefixedFileW(dir: ?windows.HANDLE, path: [:0]const u16) Wtf16ToPrefixedFileWError!WindowsPathSpace {
6360 const nt_prefix = [_]u16{ '\\', '?', '?', '\\' };
6361 if (windows.hasCommonNtPrefix(u16, path)) {
6362 // TODO: Figure out a way to design an API that can avoid the copy for NT,
6363 // since it is always returned fully unmodified.
6364 var path_space: WindowsPathSpace = undefined;
6365 path_space.data[0..nt_prefix.len].* = nt_prefix;
6366 const len_after_prefix = path.len - nt_prefix.len;
6367 @memcpy(path_space.data[nt_prefix.len..][0..len_after_prefix], path[nt_prefix.len..]);
6368 path_space.len = path.len;
6369 path_space.data[path_space.len] = 0;
6370 return path_space;
6371 } else {
6372 const path_type = Dir.path.getWin32PathType(u16, path);
6373 var path_space: WindowsPathSpace = undefined;
6374 if (path_type == .local_device) {
6375 switch (getLocalDevicePathType(u16, path)) {
6376 .verbatim => {
6377 path_space.data[0..nt_prefix.len].* = nt_prefix;
6378 const len_after_prefix = path.len - nt_prefix.len;
6379 @memcpy(path_space.data[nt_prefix.len..][0..len_after_prefix], path[nt_prefix.len..]);
6380 path_space.len = path.len;
6381 path_space.data[path_space.len] = 0;
6382 return path_space;
6383 },
6384 .local_device, .fake_verbatim => {
6385 const path_byte_len = windows.ntdll.RtlGetFullPathName_U(
6386 path.ptr,
6387 path_space.data.len * 2,
6388 &path_space.data,
6389 null,
6390 );
6391 if (path_byte_len == 0) {
6392 // TODO: This may not be the right error
6393 return error.BadPathName;
6394 } else if (path_byte_len / 2 > path_space.data.len) {
6395 return error.NameTooLong;
6396 }
6397 path_space.len = path_byte_len / 2;
6398 // Both prefixes will be normalized but retained, so all
6399 // we need to do now is replace them with the NT prefix
6400 path_space.data[0..nt_prefix.len].* = nt_prefix;
6401 return path_space;
6402 },
6403 }
6404 }
6405 relative: {
6406 if (path_type == .relative) {
6407 // TODO: Handle special case device names like COM1, AUX, NUL, CONIN$, CONOUT$, etc.
6408 // See https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
6409
6410 // TODO: Potentially strip all trailing . and space characters from the
6411 // end of the path. This is something that both RtlDosPathNameToNtPathName_U
6412 // and RtlGetFullPathName_U do. Technically, trailing . and spaces
6413 // are allowed, but such paths may not interact well with Windows (i.e.
6414 // files with these paths can't be deleted from explorer.exe, etc).
6415 // This could be something that normalizePath may want to do.
6416
6417 @memcpy(path_space.data[0..path.len], path);
6418 // Try to normalize, but if we get too many parent directories,
6419 // then we need to start over and use RtlGetFullPathName_U instead.
6420 path_space.len = windows.normalizePath(u16, path_space.data[0..path.len]) catch |err| switch (err) {
6421 error.TooManyParentDirs => break :relative,
6422 };
6423 path_space.data[path_space.len] = 0;
6424 return path_space;
6425 }
6426 }
6427 // We now know we are going to return an absolute NT path, so
6428 // we can unconditionally prefix it with the NT prefix.
6429 path_space.data[0..nt_prefix.len].* = nt_prefix;
6430 if (path_type == .root_local_device) {
6431 // `\\.` and `\\?` always get converted to `\??\` exactly, so
6432 // we can just stop here
6433 path_space.len = nt_prefix.len;
6434 path_space.data[path_space.len] = 0;
6435 return path_space;
6436 }
6437 const path_buf_offset = switch (path_type) {
6438 // UNC paths will always start with `\\`. However, we want to
6439 // end up with something like `\??\UNC\server\share`, so to get
6440 // RtlGetFullPathName to write into the spot we want the `server`
6441 // part to end up, we need to provide an offset such that
6442 // the `\\` part gets written where the `C\` of `UNC\` will be
6443 // in the final NT path.
6444 .unc_absolute => nt_prefix.len + 2,
6445 else => nt_prefix.len,
6446 };
6447 const buf_len: u32 = @intCast(path_space.data.len - path_buf_offset);
6448 const path_to_get: [:0]const u16 = path_to_get: {
6449 // If dir is null, then we don't need to bother with GetFinalPathNameByHandle because
6450 // RtlGetFullPathName_U will resolve relative paths against the CWD for us.
6451 if (path_type != .relative or dir == null) {
6452 break :path_to_get path;
6453 }
6454 // We can also skip GetFinalPathNameByHandle if the handle matches
6455 // the handle returned by Io.Dir.cwd()
6456 if (dir.? == Io.Dir.cwd().handle) {
6457 break :path_to_get path;
6458 }
6459 // At this point, we know we have a relative path that had too many
6460 // `..` components to be resolved by normalizePath, so we need to
6461 // convert it into an absolute path and let RtlGetFullPathName_U
6462 // canonicalize it. We do this by getting the path of the `dir`
6463 // and appending the relative path to it.
6464 var dir_path_buf: [windows.PATH_MAX_WIDE:0]u16 = undefined;
6465 const dir_path = GetFinalPathNameByHandle(dir.?, .{}, &dir_path_buf) catch |err| switch (err) {
6466 // This mapping is not correct; it is actually expected
6467 // that calling GetFinalPathNameByHandle might return
6468 // error.UnrecognizedVolume, and in fact has been observed
6469 // in the wild. The problem is that wToPrefixedFileW was
6470 // never intended to make *any* OS syscall APIs. It's only
6471 // supposed to convert a string to one that is eligible to
6472 // be used in the ntdll syscalls.
6473 //
6474 // To solve this, this function needs to no longer call
6475 // GetFinalPathNameByHandle under any conditions, or the
6476 // calling function needs to get reworked to not need to
6477 // call this function.
6478 //
6479 // This may involve making breaking API changes.
6480 error.UnrecognizedVolume => return error.Unexpected,
6481 else => |e| return e,
6482 };
6483 if (dir_path.len + 1 + path.len > windows.PATH_MAX_WIDE) {
6484 return error.NameTooLong;
6485 }
6486 // We don't have to worry about potentially doubling up path separators
6487 // here since RtlGetFullPathName_U will handle canonicalizing it.
6488 dir_path_buf[dir_path.len] = '\\';
6489 @memcpy(dir_path_buf[dir_path.len + 1 ..][0..path.len], path);
6490 const full_len = dir_path.len + 1 + path.len;
6491 dir_path_buf[full_len] = 0;
6492 break :path_to_get dir_path_buf[0..full_len :0];
6493 };
6494 const path_byte_len = windows.ntdll.RtlGetFullPathName_U(
6495 path_to_get.ptr,
6496 buf_len * 2,
6497 path_space.data[path_buf_offset..].ptr,
6498 null,
6499 );
6500 if (path_byte_len == 0) {
6501 // TODO: This may not be the right error
6502 return error.BadPathName;
6503 } else if (path_byte_len / 2 > buf_len) {
6504 return error.NameTooLong;
6505 }
6506 path_space.len = path_buf_offset + (path_byte_len / 2);
6507 if (path_type == .unc_absolute) {
6508 // Now add in the UNC, the `C` should overwrite the first `\` of the
6509 // FullPathName, ultimately resulting in `\??\UNC\<the rest of the path>`
6510 assert(path_space.data[path_buf_offset] == '\\');
6511 assert(path_space.data[path_buf_offset + 1] == '\\');
6512 const unc = [_]u16{ 'U', 'N', 'C' };
6513 path_space.data[nt_prefix.len..][0..unc.len].* = unc;
6514 }
6515 return path_space;
6516 }
6517}
6518
6519const LocalDevicePathType = enum {
6520 /// `\\.\` (path separators can be `\` or `/`)
6521 local_device,
6522 /// `\\?\`
6523 /// When converted to an NT path, everything past the prefix is left
6524 /// untouched and `\\?\` is replaced by `\??\`.
6525 verbatim,
6526 /// `\\?\` without all path separators being `\`.
6527 /// This seems to be recognized as a prefix, but the 'verbatim' aspect
6528 /// is not respected (i.e. if `//?/C:/foo` is converted to an NT path,
6529 /// it will become `\??\C:\foo` [it will be canonicalized and the //?/ won't
6530 /// be treated as part of the final path])
6531 fake_verbatim,
6532};
6533
6534/// Only relevant for Win32 -> NT path conversion.
6535/// Asserts `path` is of type `Dir.path.Win32PathType.local_device`.
6536fn getLocalDevicePathType(comptime T: type, path: []const T) LocalDevicePathType {
6537 if (std.debug.runtime_safety) {
6538 assert(Dir.path.getWin32PathType(T, path) == .local_device);
6539 }
6540
6541 const backslash = std.mem.nativeToLittle(T, '\\');
6542 const all_backslash = path[0] == backslash and
6543 path[1] == backslash and
6544 path[3] == backslash;
6545 return switch (path[2]) {
6546 std.mem.nativeToLittle(T, '?') => if (all_backslash) .verbatim else .fake_verbatim,
6547 std.mem.nativeToLittle(T, '.') => .local_device,
6548 else => unreachable,
6549 };
6550}
6551
6552pub const Wtf8ToPrefixedFileWError = Wtf16ToPrefixedFileWError;
6553
6554/// Same as `wToPrefixedFileW` but accepts a WTF-8 encoded path.
6555/// https://wtf-8.codeberg.page/
6556pub fn sliceToPrefixedFileW(dir: ?windows.HANDLE, path: []const u8) Wtf8ToPrefixedFileWError!WindowsPathSpace {
6557 var temp_path: WindowsPathSpace = undefined;
6558 temp_path.len = std.unicode.wtf8ToWtf16Le(&temp_path.data, path) catch |err| switch (err) {
6559 error.InvalidWtf8 => return error.BadPathName,
6560 };
6561 temp_path.data[temp_path.len] = 0;
6562 return wToPrefixedFileW(dir, temp_path.span());
6563}
6564
6565pub const WindowsPathSpace = struct {
6566 data: [windows.PATH_MAX_WIDE:0]u16,
6567 len: usize,
6568
6569 pub fn span(self: *const WindowsPathSpace) [:0]const u16 {
6570 return self.data[0..self.len :0];
6571 }
6572};
6573
6030fn dirRealPathFilePosix(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, out_buffer: []u8) Dir.RealPathFileError!usize {6574fn dirRealPathFilePosix(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, out_buffer: []u8) Dir.RealPathFileError!usize {
6031 if (native_os == .wasi) return error.OperationUnsupported;6575 if (native_os == .wasi) return error.OperationUnsupported;
60326576
...@@ -6478,7 +7022,7 @@ fn dirDeleteWindows(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, remov...@@ -6478,7 +7022,7 @@ fn dirDeleteWindows(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, remov
6478 _ = t;7022 _ = t;
6479 const w = windows;7023 const w = windows;
64807024
6481 const sub_path_w_buf = try w.sliceToPrefixedFileW(dir.handle, sub_path);7025 const sub_path_w_buf = try sliceToPrefixedFileW(dir.handle, sub_path);
6482 const sub_path_w = sub_path_w_buf.span();7026 const sub_path_w = sub_path_w_buf.span();
64837027
6484 const path_len_bytes = @as(u16, @intCast(sub_path_w.len * 2));7028 const path_len_bytes = @as(u16, @intCast(sub_path_w.len * 2));
...@@ -6759,9 +7303,9 @@ fn dirRenameWindowsInner(...@@ -6759,9 +7303,9 @@ fn dirRenameWindowsInner(
6759 replace_if_exists: bool,7303 replace_if_exists: bool,
6760) Dir.RenamePreserveError!void {7304) Dir.RenamePreserveError!void {
6761 const w = windows;7305 const w = windows;
6762 const old_path_w_buf = try windows.sliceToPrefixedFileW(old_dir.handle, old_sub_path);7306 const old_path_w_buf = try sliceToPrefixedFileW(old_dir.handle, old_sub_path);
6763 const old_path_w = old_path_w_buf.span();7307 const old_path_w = old_path_w_buf.span();
6764 const new_path_w_buf = try windows.sliceToPrefixedFileW(new_dir.handle, new_sub_path);7308 const new_path_w_buf = try sliceToPrefixedFileW(new_dir.handle, new_sub_path);
6765 const new_path_w = new_path_w_buf.span();7309 const new_path_w = new_path_w_buf.span();
67667310
6767 const src_fd = src_fd: {7311 const src_fd = src_fd: {
...@@ -7092,7 +7636,7 @@ fn dirSymLinkWindows(...@@ -7092,7 +7636,7 @@ fn dirSymLinkWindows(
7092 // Target path does not use sliceToPrefixedFileW because certain paths7636 // Target path does not use sliceToPrefixedFileW because certain paths
7093 // are handled differently when creating a symlink than they would be7637 // are handled differently when creating a symlink than they would be
7094 // when converting to an NT namespaced path.7638 // when converting to an NT namespaced path.
7095 var target_path_w: w.PathSpace = undefined;7639 var target_path_w: WindowsPathSpace = undefined;
7096 target_path_w.len = try w.wtf8ToWtf16Le(&target_path_w.data, target_path);7640 target_path_w.len = try w.wtf8ToWtf16Le(&target_path_w.data, target_path);
7097 target_path_w.data[target_path_w.len] = 0;7641 target_path_w.data[target_path_w.len] = 0;
7098 // However, we need to canonicalize any path separators to `\`, since if7642 // However, we need to canonicalize any path separators to `\`, since if
...@@ -7104,7 +7648,7 @@ fn dirSymLinkWindows(...@@ -7104,7 +7648,7 @@ fn dirSymLinkWindows(
7104 std.mem.nativeToLittle(u16, '\\'),7648 std.mem.nativeToLittle(u16, '\\'),
7105 );7649 );
71067650
7107 const sym_link_path_w = try w.sliceToPrefixedFileW(dir.handle, sym_link_path);7651 const sym_link_path_w = try sliceToPrefixedFileW(dir.handle, sym_link_path);
71087652
7109 const SYMLINK_DATA = extern struct {7653 const SYMLINK_DATA = extern struct {
7110 ReparseTag: w.IO_REPARSE_TAG,7654 ReparseTag: w.IO_REPARSE_TAG,
...@@ -7158,7 +7702,7 @@ fn dirSymLinkWindows(...@@ -7158,7 +7702,7 @@ fn dirSymLinkWindows(
7158 // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createsymboliclinkw7702 // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createsymboliclinkw
7159 var is_target_absolute = false;7703 var is_target_absolute = false;
7160 const final_target_path = target_path: {7704 const final_target_path = target_path: {
7161 if (w.hasCommonNtPrefix(u16, target_path_w.span())) {7705 if (windows.hasCommonNtPrefix(u16, target_path_w.span())) {
7162 // Already an NT path, no need to do anything to it7706 // Already an NT path, no need to do anything to it
7163 break :target_path target_path_w.span();7707 break :target_path target_path_w.span();
7164 } else {7708 } else {
...@@ -7176,7 +7720,7 @@ fn dirSymLinkWindows(...@@ -7176,7 +7720,7 @@ fn dirSymLinkWindows(
7176 break :target_path target_path_w.span(),7720 break :target_path target_path_w.span(),
7177 }7721 }
7178 }7722 }
7179 var prefixed_target_path = try w.wToPrefixedFileW(dir.handle, target_path_w.span());7723 var prefixed_target_path = try wToPrefixedFileW(dir.handle, target_path_w.span());
7180 // We do this after prefixing to ensure that drive-relative paths are treated as absolute7724 // We do this after prefixing to ensure that drive-relative paths are treated as absolute
7181 is_target_absolute = Dir.path.isAbsoluteWindowsWtf16(prefixed_target_path.span());7725 is_target_absolute = Dir.path.isAbsoluteWindowsWtf16(prefixed_target_path.span());
7182 break :target_path prefixed_target_path.span();7726 break :target_path prefixed_target_path.span();
...@@ -7322,7 +7866,7 @@ fn dirReadLink(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, buffer: []...@@ -7322,7 +7866,7 @@ fn dirReadLink(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, buffer: []
7322fn dirReadLinkWindows(dir: Dir, sub_path: []const u8, buffer: []u8) Dir.ReadLinkError!usize {7866fn dirReadLinkWindows(dir: Dir, sub_path: []const u8, buffer: []u8) Dir.ReadLinkError!usize {
7323 // This gets used once for `sub_path` and then reused again temporarily7867 // This gets used once for `sub_path` and then reused again temporarily
7324 // before converting back to `buffer`.7868 // before converting back to `buffer`.
7325 var sub_path_w_buf = try windows.sliceToPrefixedFileW(dir.handle, sub_path);7869 var sub_path_w_buf = try sliceToPrefixedFileW(dir.handle, sub_path);
7326 const sub_path_w = sub_path_w_buf.span();7870 const sub_path_w = sub_path_w_buf.span();
7327 const path_len_bytes = std.math.cast(u16, sub_path_w.len * 2) orelse return error.NameTooLong;7871 const path_len_bytes = std.math.cast(u16, sub_path_w.len * 2) orelse return error.NameTooLong;
7328 var nt_name: windows.UNICODE_STRING = .{7872 var nt_name: windows.UNICODE_STRING = .{
...@@ -9586,7 +10130,7 @@ fn processExecutableOpen(userdata: ?*anyopaque, flags: File.OpenFlags) process.O...@@ -9586,7 +10130,7 @@ fn processExecutableOpen(userdata: ?*anyopaque, flags: File.OpenFlags) process.O
9586 // the file, we can let the openFileW call follow the symlink for us.10130 // the file, we can let the openFileW call follow the symlink for us.
9587 const image_path_unicode_string = &windows.peb().ProcessParameters.ImagePathName;10131 const image_path_unicode_string = &windows.peb().ProcessParameters.ImagePathName;
9588 const image_path_name = image_path_unicode_string.Buffer.?[0 .. image_path_unicode_string.Length / 2 :0];10132 const image_path_name = image_path_unicode_string.Buffer.?[0 .. image_path_unicode_string.Length / 2 :0];
9589 const prefixed_path_w = try windows.wToPrefixedFileW(null, image_path_name);10133 const prefixed_path_w = try wToPrefixedFileW(null, image_path_name);
9590 return dirOpenFileWtf16(null, prefixed_path_w.span(), flags);10134 return dirOpenFileWtf16(null, prefixed_path_w.span(), flags);
9591 },10135 },
9592 .driverkit,10136 .driverkit,
...@@ -9794,7 +10338,7 @@ fn processExecutablePath(userdata: ?*anyopaque, out_buffer: []u8) process.Execut...@@ -9794,7 +10338,7 @@ fn processExecutablePath(userdata: ?*anyopaque, out_buffer: []u8) process.Execut
9794 // If ImagePathName is a symlink, then it will contain the path of the10338 // If ImagePathName is a symlink, then it will contain the path of the
9795 // symlink, not the path that the symlink points to. We want the path10339 // symlink, not the path that the symlink points to. We want the path
9796 // that the symlink points to, though, so we need to get the realpath.10340 // that the symlink points to, though, so we need to get the realpath.
9797 var path_name_w_buf = try w.wToPrefixedFileW(null, image_path_name);10341 var path_name_w_buf = try wToPrefixedFileW(null, image_path_name);
979810342
9799 const h_file = handle: {10343 const h_file = handle: {
9800 const syscall: Syscall = try .start();10344 const syscall: Syscall = try .start();
...@@ -9822,9 +10366,7 @@ fn processExecutablePath(userdata: ?*anyopaque, out_buffer: []u8) process.Execut...@@ -9822,9 +10366,7 @@ fn processExecutablePath(userdata: ?*anyopaque, out_buffer: []u8) process.Execut
9822 };10366 };
9823 defer w.CloseHandle(h_file);10367 defer w.CloseHandle(h_file);
982410368
9825 // TODO move GetFinalPathNameByHandle logic into Io.Threaded and add cancel checks10369 const wide_slice = try GetFinalPathNameByHandle(h_file, .{}, &path_name_w_buf.data);
9826 try Thread.checkCancel();
9827 const wide_slice = try w.GetFinalPathNameByHandle(h_file, .{}, &path_name_w_buf.data);
982810370
9829 const len = std.unicode.calcWtf8Len(wide_slice);10371 const len = std.unicode.calcWtf8Len(wide_slice);
9830 if (len > out_buffer.len)10372 if (len > out_buffer.len)
...@@ -13598,9 +14140,7 @@ fn processSetCurrentDir(userdata: ?*anyopaque, dir: Dir) process.SetCurrentDirEr...@@ -13598,9 +14140,7 @@ fn processSetCurrentDir(userdata: ?*anyopaque, dir: Dir) process.SetCurrentDirEr
1359814140
13599 if (is_windows) {14141 if (is_windows) {
13600 var dir_path_buffer: [windows.PATH_MAX_WIDE]u16 = undefined;14142 var dir_path_buffer: [windows.PATH_MAX_WIDE]u16 = undefined;
13601 // TODO move GetFinalPathNameByHandle logic into Io.Threaded and add cancel checks14143 const dir_path = try GetFinalPathNameByHandle(dir.handle, .{}, &dir_path_buffer);
13602 try Thread.checkCancel();
13603 const dir_path = try windows.GetFinalPathNameByHandle(dir.handle, .{}, &dir_path_buffer);
13604 const path_len_bytes = std.math.cast(u16, dir_path.len * 2) orelse return error.NameTooLong;14144 const path_len_bytes = std.math.cast(u16, dir_path.len * 2) orelse return error.NameTooLong;
13605 var nt_name: windows.UNICODE_STRING = .{14145 var nt_name: windows.UNICODE_STRING = .{
13606 .Length = path_len_bytes,14146 .Length = path_len_bytes,
...@@ -15326,9 +15866,7 @@ fn processSpawnWindows(userdata: ?*anyopaque, options: process.SpawnOptions) pro...@@ -15326,9 +15866,7 @@ fn processSpawnWindows(userdata: ?*anyopaque, options: process.SpawnOptions) pro
15326 .inherit => break :cwd_w null,15866 .inherit => break :cwd_w null,
15327 .dir => |cwd_dir| {15867 .dir => |cwd_dir| {
15328 var dir_path_buffer = try arena.alloc(u16, windows.PATH_MAX_WIDE + 1);15868 var dir_path_buffer = try arena.alloc(u16, windows.PATH_MAX_WIDE + 1);
15329 // TODO move GetFinalPathNameByHandle logic into std.Io.Threaded and add cancel checks15869 const dir_path = try GetFinalPathNameByHandle(
15330 try Thread.checkCancel();
15331 const dir_path = try windows.GetFinalPathNameByHandle(
15332 cwd_dir.handle,15870 cwd_dir.handle,
15333 .{},15871 .{},
15334 dir_path_buffer[0..windows.PATH_MAX_WIDE],15872 dir_path_buffer[0..windows.PATH_MAX_WIDE],
...@@ -15752,7 +16290,7 @@ fn windowsCreateProcessPathExt(...@@ -15752,7 +16290,7 @@ fn windowsCreateProcessPathExt(
15752 try dir_buf.append(arena, 0);16290 try dir_buf.append(arena, 0);
15753 defer dir_buf.shrinkRetainingCapacity(dir_path_len);16291 defer dir_buf.shrinkRetainingCapacity(dir_path_len);
15754 const dir_path_z = dir_buf.items[0 .. dir_buf.items.len - 1 :0];16292 const dir_path_z = dir_buf.items[0 .. dir_buf.items.len - 1 :0];
15755 const prefixed_path = try windows.wToPrefixedFileW(null, dir_path_z);16293 const prefixed_path = try wToPrefixedFileW(null, dir_path_z);
15756 break :dir dirOpenDirWindows(.cwd(), prefixed_path.span(), .{16294 break :dir dirOpenDirWindows(.cwd(), prefixed_path.span(), .{
15757 .iterate = true,16295 .iterate = true,
15758 }) catch |err| switch (err) {16296 }) catch |err| switch (err) {
lib/std/Io/Threaded/test.zig+335
...@@ -6,6 +6,7 @@ const std = @import("std");...@@ -6,6 +6,7 @@ const std = @import("std");
6const Io = std.Io;6const Io = std.Io;
7const testing = std.testing;7const testing = std.testing;
8const assert = std.debug.assert;8const assert = std.debug.assert;
9const windows = std.os.windows;
910
10test "concurrent vs main prevents deadlock via oversubscription" {11test "concurrent vs main prevents deadlock via oversubscription" {
11 if (true) {12 if (true) {
...@@ -277,3 +278,337 @@ test "memory mapping fallback" {...@@ -277,3 +278,337 @@ test "memory mapping fallback" {
277 try testing.expectEqualStrings("this9is9my data123", mm.memory);278 try testing.expectEqualStrings("this9is9my data123", mm.memory);
278 }279 }
279}280}
281
282/// Wrapper around RtlDosPathNameToNtPathName_U for use in comparing
283/// the behavior of RtlDosPathNameToNtPathName_U with wToPrefixedFileW
284/// Note: RtlDosPathNameToNtPathName_U is not used in the Zig implementation
285// because it allocates.
286fn RtlDosPathNameToNtPathName_U(path: [:0]const u16) !Io.Threaded.WindowsPathSpace {
287 var out: windows.UNICODE_STRING = undefined;
288 const rc = windows.ntdll.RtlDosPathNameToNtPathName_U(path, &out, null, null);
289 if (rc != windows.TRUE) return error.BadPathName;
290 defer windows.ntdll.RtlFreeUnicodeString(&out);
291
292 var path_space: Io.Threaded.WindowsPathSpace = undefined;
293 const out_path = out.Buffer.?[0 .. out.Length / 2];
294 @memcpy(path_space.data[0..out_path.len], out_path);
295 path_space.len = out.Length / 2;
296 path_space.data[path_space.len] = 0;
297
298 return path_space;
299}
300
301/// Test that the Zig conversion matches the expected_path (for instances where
302/// the Zig implementation intentionally diverges from what RtlDosPathNameToNtPathName_U does).
303fn testToPrefixedFileNoOracle(comptime path: []const u8, comptime expected_path: []const u8) !void {
304 const path_utf16 = std.unicode.utf8ToUtf16LeStringLiteral(path);
305 const expected_path_utf16 = std.unicode.utf8ToUtf16LeStringLiteral(expected_path);
306 const actual_path = try Io.Threaded.wToPrefixedFileW(null, path_utf16);
307 std.testing.expectEqualSlices(u16, expected_path_utf16, actual_path.span()) catch |e| {
308 std.debug.print("got '{f}', expected '{f}'\n", .{ std.unicode.fmtUtf16Le(actual_path.span()), std.unicode.fmtUtf16Le(expected_path_utf16) });
309 return e;
310 };
311}
312
313/// Test that the Zig conversion matches the expected_path and that the
314/// expected_path matches the conversion that RtlDosPathNameToNtPathName_U does.
315fn testToPrefixedFileWithOracle(comptime path: []const u8, comptime expected_path: []const u8) !void {
316 try testToPrefixedFileNoOracle(path, expected_path);
317 try testToPrefixedFileOnlyOracle(path);
318}
319
320/// Test that the Zig conversion matches the conversion that RtlDosPathNameToNtPathName_U does.
321fn testToPrefixedFileOnlyOracle(comptime path: []const u8) !void {
322 const path_utf16 = std.unicode.utf8ToUtf16LeStringLiteral(path);
323 const zig_result = try Io.Threaded.wToPrefixedFileW(null, path_utf16);
324 const win32_api_result = try RtlDosPathNameToNtPathName_U(path_utf16);
325 std.testing.expectEqualSlices(u16, win32_api_result.span(), zig_result.span()) catch |e| {
326 std.debug.print("got '{f}', expected '{f}'\n", .{ std.unicode.fmtUtf16Le(zig_result.span()), std.unicode.fmtUtf16Le(win32_api_result.span()) });
327 return e;
328 };
329}
330
331test "toPrefixedFileW" {
332 if (builtin.os.tag != .windows) return error.SkipZigTest;
333
334 // Most test cases come from https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
335 // Note that these tests do not actually touch the filesystem or care about whether or not
336 // any of the paths actually exist or are otherwise valid.
337
338 // Drive Absolute
339 try testToPrefixedFileWithOracle("X:\\ABC\\DEF", "\\??\\X:\\ABC\\DEF");
340 try testToPrefixedFileWithOracle("X:\\", "\\??\\X:\\");
341 try testToPrefixedFileWithOracle("X:\\ABC\\", "\\??\\X:\\ABC\\");
342 // Trailing . and space characters are stripped
343 try testToPrefixedFileWithOracle("X:\\ABC\\DEF. .", "\\??\\X:\\ABC\\DEF");
344 try testToPrefixedFileWithOracle("X:/ABC/DEF", "\\??\\X:\\ABC\\DEF");
345 try testToPrefixedFileWithOracle("X:\\ABC\\..\\XYZ", "\\??\\X:\\XYZ");
346 try testToPrefixedFileWithOracle("X:\\ABC\\..\\..\\..", "\\??\\X:\\");
347 // Drive letter casing is unchanged
348 try testToPrefixedFileWithOracle("x:\\", "\\??\\x:\\");
349
350 // Drive Relative
351 // These tests depend on the CWD of the specified drive letter which can vary,
352 // so instead we just test that the Zig implementation matches the result of
353 // RtlDosPathNameToNtPathName_U.
354 // TODO: Setting the =X: environment variable didn't seem to affect
355 // RtlDosPathNameToNtPathName_U, not sure why that is but getting that
356 // to work could be an avenue to making these cases environment-independent.
357 // All -> are examples of the result if the X drive's cwd was X:\ABC
358 try testToPrefixedFileOnlyOracle("X:DEF\\GHI"); // -> \??\X:\ABC\DEF\GHI
359 try testToPrefixedFileOnlyOracle("X:"); // -> \??\X:\ABC
360 try testToPrefixedFileOnlyOracle("X:DEF. ."); // -> \??\X:\ABC\DEF
361 try testToPrefixedFileOnlyOracle("X:ABC\\..\\XYZ"); // -> \??\X:\ABC\XYZ
362 try testToPrefixedFileOnlyOracle("X:ABC\\..\\..\\.."); // -> \??\X:\
363 try testToPrefixedFileOnlyOracle("x:"); // -> \??\X:\ABC
364
365 // Rooted
366 // These tests depend on the drive letter of the CWD which can vary, so
367 // instead we just test that the Zig implementation matches the result of
368 // RtlDosPathNameToNtPathName_U.
369 // TODO: Getting the CWD path, getting the drive letter from it, and using it to
370 // construct the expected NT paths could be an avenue to making these cases
371 // environment-independent and therefore able to use testToPrefixedFileWithOracle.
372 // All -> are examples of the result if the CWD's drive letter was X
373 try testToPrefixedFileOnlyOracle("\\ABC\\DEF"); // -> \??\X:\ABC\DEF
374 try testToPrefixedFileOnlyOracle("\\"); // -> \??\X:\
375 try testToPrefixedFileOnlyOracle("\\ABC\\DEF. ."); // -> \??\X:\ABC\DEF
376 try testToPrefixedFileOnlyOracle("/ABC/DEF"); // -> \??\X:\ABC\DEF
377 try testToPrefixedFileOnlyOracle("\\ABC\\..\\XYZ"); // -> \??\X:\XYZ
378 try testToPrefixedFileOnlyOracle("\\ABC\\..\\..\\.."); // -> \??\X:\
379
380 // Relative
381 // These cases differ in functionality to RtlDosPathNameToNtPathName_U.
382 // Relative paths remain relative if they don't have enough .. components
383 // to error with TooManyParentDirs
384 try testToPrefixedFileNoOracle("ABC\\DEF", "ABC\\DEF");
385 // TODO: enable this if trailing . and spaces are stripped from relative paths
386 //try testToPrefixedFileNoOracle("ABC\\DEF. .", "ABC\\DEF");
387 try testToPrefixedFileNoOracle("ABC/DEF", "ABC\\DEF");
388 try testToPrefixedFileNoOracle("./ABC/.././DEF", "DEF");
389 // TooManyParentDirs, so resolved relative to the CWD
390 // All -> are examples of the result if the CWD was X:\ABC\DEF
391 try testToPrefixedFileOnlyOracle("..\\GHI"); // -> \??\X:\ABC\GHI
392 try testToPrefixedFileOnlyOracle("GHI\\..\\..\\.."); // -> \??\X:\
393
394 // UNC Absolute
395 try testToPrefixedFileWithOracle("\\\\server\\share\\ABC\\DEF", "\\??\\UNC\\server\\share\\ABC\\DEF");
396 try testToPrefixedFileWithOracle("\\\\server", "\\??\\UNC\\server");
397 try testToPrefixedFileWithOracle("\\\\server\\share", "\\??\\UNC\\server\\share");
398 try testToPrefixedFileWithOracle("\\\\server\\share\\ABC. .", "\\??\\UNC\\server\\share\\ABC");
399 try testToPrefixedFileWithOracle("//server/share/ABC/DEF", "\\??\\UNC\\server\\share\\ABC\\DEF");
400 try testToPrefixedFileWithOracle("\\\\server\\share\\ABC\\..\\XYZ", "\\??\\UNC\\server\\share\\XYZ");
401 try testToPrefixedFileWithOracle("\\\\server\\share\\ABC\\..\\..\\..", "\\??\\UNC\\server\\share");
402
403 // Local Device
404 try testToPrefixedFileWithOracle("\\\\.\\COM20", "\\??\\COM20");
405 try testToPrefixedFileWithOracle("\\\\.\\pipe\\mypipe", "\\??\\pipe\\mypipe");
406 try testToPrefixedFileWithOracle("\\\\.\\X:\\ABC\\DEF. .", "\\??\\X:\\ABC\\DEF");
407 try testToPrefixedFileWithOracle("\\\\.\\X:/ABC/DEF", "\\??\\X:\\ABC\\DEF");
408 try testToPrefixedFileWithOracle("\\\\.\\X:\\ABC\\..\\XYZ", "\\??\\X:\\XYZ");
409 // Can replace the first component of the path (contrary to drive absolute and UNC absolute paths)
410 try testToPrefixedFileWithOracle("\\\\.\\X:\\ABC\\..\\..\\C:\\", "\\??\\C:\\");
411 try testToPrefixedFileWithOracle("\\\\.\\pipe\\mypipe\\..\\notmine", "\\??\\pipe\\notmine");
412
413 // Special-case device names
414 // TODO: Enable once these are supported
415 // more cases to test here: https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
416 //try testToPrefixedFileWithOracle("COM1", "\\??\\COM1");
417 // Sometimes the special-cased device names are not respected
418 try testToPrefixedFileWithOracle("\\\\.\\X:\\COM1", "\\??\\X:\\COM1");
419 try testToPrefixedFileWithOracle("\\\\abc\\xyz\\COM1", "\\??\\UNC\\abc\\xyz\\COM1");
420
421 // Verbatim
422 // Left untouched except \\?\ is replaced by \??\
423 try testToPrefixedFileWithOracle("\\\\?\\X:", "\\??\\X:");
424 try testToPrefixedFileWithOracle("\\\\?\\X:\\COM1", "\\??\\X:\\COM1");
425 try testToPrefixedFileWithOracle("\\\\?\\X:/ABC/DEF. .", "\\??\\X:/ABC/DEF. .");
426 try testToPrefixedFileWithOracle("\\\\?\\X:\\ABC\\..\\..\\..", "\\??\\X:\\ABC\\..\\..\\..");
427 // NT Namespace
428 // Fully unmodified
429 try testToPrefixedFileWithOracle("\\??\\X:", "\\??\\X:");
430 try testToPrefixedFileWithOracle("\\??\\X:\\COM1", "\\??\\X:\\COM1");
431 try testToPrefixedFileWithOracle("\\??\\X:/ABC/DEF. .", "\\??\\X:/ABC/DEF. .");
432 try testToPrefixedFileWithOracle("\\??\\X:\\ABC\\..\\..\\..", "\\??\\X:\\ABC\\..\\..\\..");
433
434 // 'Fake' Verbatim
435 // If the prefix looks like the verbatim prefix but not all path separators in the
436 // prefix are backslashes, then it gets canonicalized and the prefix is dropped in favor
437 // of the NT prefix.
438 try testToPrefixedFileWithOracle("//?/C:/ABC", "\\??\\C:\\ABC");
439 // 'Fake' NT
440 // If the prefix looks like the NT prefix but not all path separators in the prefix
441 // are backslashes, then it gets canonicalized and the /??/ is not dropped but
442 // rather treated as part of the path. In other words, the path is treated
443 // as a rooted path, so the final path is resolved relative to the CWD's
444 // drive letter.
445 // The -> shows an example of the result if the CWD's drive letter was X
446 try testToPrefixedFileOnlyOracle("/??/C:/ABC"); // -> \??\X:\??\C:\ABC
447
448 // Root Local Device
449 // \\. and \\? always get converted to \??\
450 try testToPrefixedFileWithOracle("\\\\.", "\\??\\");
451 try testToPrefixedFileWithOracle("\\\\?", "\\??\\");
452 try testToPrefixedFileWithOracle("//?", "\\??\\");
453 try testToPrefixedFileWithOracle("//.", "\\??\\");
454}
455
456fn testRemoveDotDirs(str: []const u8, expected: []const u8) !void {
457 const mutable = try testing.allocator.dupe(u8, str);
458 defer testing.allocator.free(mutable);
459 const actual = mutable[0..try windows.removeDotDirsSanitized(u8, mutable)];
460 try testing.expect(std.mem.eql(u8, actual, expected));
461}
462fn testRemoveDotDirsError(err: anyerror, str: []const u8) !void {
463 const mutable = try testing.allocator.dupe(u8, str);
464 defer testing.allocator.free(mutable);
465 try testing.expectError(err, windows.removeDotDirsSanitized(u8, mutable));
466}
467test "removeDotDirs" {
468 try testRemoveDotDirs("", "");
469 try testRemoveDotDirs(".", "");
470 try testRemoveDotDirs(".\\", "");
471 try testRemoveDotDirs(".\\.", "");
472 try testRemoveDotDirs(".\\.\\", "");
473 try testRemoveDotDirs(".\\.\\.", "");
474
475 try testRemoveDotDirs("a", "a");
476 try testRemoveDotDirs("a\\", "a\\");
477 try testRemoveDotDirs("a\\b", "a\\b");
478 try testRemoveDotDirs("a\\.", "a\\");
479 try testRemoveDotDirs("a\\b\\.", "a\\b\\");
480 try testRemoveDotDirs("a\\.\\b", "a\\b");
481
482 try testRemoveDotDirs(".a", ".a");
483 try testRemoveDotDirs(".a\\", ".a\\");
484 try testRemoveDotDirs(".a\\.b", ".a\\.b");
485 try testRemoveDotDirs(".a\\.", ".a\\");
486 try testRemoveDotDirs(".a\\.\\.", ".a\\");
487 try testRemoveDotDirs(".a\\.\\.\\.b", ".a\\.b");
488 try testRemoveDotDirs(".a\\.\\.\\.b\\", ".a\\.b\\");
489
490 try testRemoveDotDirsError(error.TooManyParentDirs, "..");
491 try testRemoveDotDirsError(error.TooManyParentDirs, "..\\");
492 try testRemoveDotDirsError(error.TooManyParentDirs, ".\\..\\");
493 try testRemoveDotDirsError(error.TooManyParentDirs, ".\\.\\..\\");
494
495 try testRemoveDotDirs("a\\..", "");
496 try testRemoveDotDirs("a\\..\\", "");
497 try testRemoveDotDirs("a\\..\\.", "");
498 try testRemoveDotDirs("a\\..\\.\\", "");
499 try testRemoveDotDirs("a\\..\\.\\.", "");
500 try testRemoveDotDirsError(error.TooManyParentDirs, "a\\..\\.\\.\\..");
501
502 try testRemoveDotDirs("a\\..\\.\\.\\b", "b");
503 try testRemoveDotDirs("a\\..\\.\\.\\b\\", "b\\");
504 try testRemoveDotDirs("a\\..\\.\\.\\b\\.", "b\\");
505 try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\", "b\\");
506 try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\..", "");
507 try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\..\\", "");
508 try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\..\\.", "");
509 try testRemoveDotDirsError(error.TooManyParentDirs, "a\\..\\.\\.\\b\\.\\..\\.\\..");
510
511 try testRemoveDotDirs("a\\b\\..\\", "a\\");
512 try testRemoveDotDirs("a\\b\\..\\c", "a\\c");
513}
514
515const RTL_PATH_TYPE = enum(c_int) {
516 Unknown,
517 UncAbsolute,
518 DriveAbsolute,
519 DriveRelative,
520 Rooted,
521 Relative,
522 LocalDevice,
523 RootLocalDevice,
524};
525
526pub extern "ntdll" fn RtlDetermineDosPathNameType_U(
527 Path: [*:0]const u16,
528) callconv(.winapi) RTL_PATH_TYPE;
529
530test "getWin32PathType vs RtlDetermineDosPathNameType_U" {
531 if (builtin.os.tag != .windows) return error.SkipZigTest;
532
533 var buf: std.ArrayList(u16) = .empty;
534 defer buf.deinit(std.testing.allocator);
535
536 var wtf8_buf: std.ArrayList(u8) = .empty;
537 defer wtf8_buf.deinit(std.testing.allocator);
538
539 var random = std.Random.DefaultPrng.init(std.testing.random_seed);
540 const rand = random.random();
541
542 for (0..1000) |_| {
543 buf.clearRetainingCapacity();
544 const path = try getRandomWtf16Path(std.testing.allocator, &buf, rand);
545 wtf8_buf.clearRetainingCapacity();
546 const wtf8_len = std.unicode.calcWtf8Len(path);
547 try wtf8_buf.ensureTotalCapacity(std.testing.allocator, wtf8_len);
548 wtf8_buf.items.len = wtf8_len;
549 std.debug.assert(std.unicode.wtf16LeToWtf8(wtf8_buf.items, path) == wtf8_len);
550
551 const windows_type = RtlDetermineDosPathNameType_U(path);
552 const wtf16_type = std.fs.path.getWin32PathType(u16, path);
553 const wtf8_type = std.fs.path.getWin32PathType(u8, wtf8_buf.items);
554
555 checkPathType(windows_type, wtf16_type) catch |err| {
556 std.debug.print("expected type {}, got {} for path: {f}\n", .{ windows_type, wtf16_type, std.unicode.fmtUtf16Le(path) });
557 std.debug.print("path bytes:\n", .{});
558 std.debug.dumpHex(std.mem.sliceAsBytes(path));
559 return err;
560 };
561
562 if (wtf16_type != wtf8_type) {
563 std.debug.print("type mismatch between wtf8: {} and wtf16: {} for path: {f}\n", .{ wtf8_type, wtf16_type, std.unicode.fmtUtf16Le(path) });
564 std.debug.print("wtf-16 path bytes:\n", .{});
565 std.debug.dumpHex(std.mem.sliceAsBytes(path));
566 std.debug.print("wtf-8 path bytes:\n", .{});
567 std.debug.dumpHex(std.mem.sliceAsBytes(wtf8_buf.items));
568 return error.Wtf8Wtf16Mismatch;
569 }
570 }
571}
572
573fn checkPathType(windows_type: RTL_PATH_TYPE, zig_type: std.fs.path.Win32PathType) !void {
574 const expected_windows_type: RTL_PATH_TYPE = switch (zig_type) {
575 .unc_absolute => .UncAbsolute,
576 .drive_absolute => .DriveAbsolute,
577 .drive_relative => .DriveRelative,
578 .rooted => .Rooted,
579 .relative => .Relative,
580 .local_device => .LocalDevice,
581 .root_local_device => .RootLocalDevice,
582 };
583 if (windows_type != expected_windows_type) return error.PathTypeMismatch;
584}
585
586fn getRandomWtf16Path(allocator: std.mem.Allocator, buf: *std.ArrayList(u16), rand: std.Random) ![:0]const u16 {
587 const Choice = enum {
588 backslash,
589 slash,
590 control,
591 printable,
592 non_ascii,
593 };
594
595 const choices = rand.uintAtMostBiased(u16, 32);
596
597 for (0..choices) |_| {
598 const choice = rand.enumValue(Choice);
599 const code_unit = switch (choice) {
600 .backslash => '\\',
601 .slash => '/',
602 .control => switch (rand.uintAtMostBiased(u8, 0x20)) {
603 0x20 => '\x7F',
604 else => |b| b + 1, // no NUL
605 },
606 .printable => '!' + rand.uintAtMostBiased(u8, '~' - '!'),
607 .non_ascii => rand.intRangeAtMostBiased(u16, 0x80, 0xFFFF),
608 };
609 try buf.append(allocator, std.mem.nativeToLittle(u16, code_unit));
610 }
611
612 try buf.append(allocator, 0);
613 return buf.items[0 .. buf.items.len - 1 :0];
614}
lib/std/dynamic_library.zig+1-70
...@@ -17,7 +17,6 @@ pub const DynLib = struct {...@@ -17,7 +17,6 @@ pub const DynLib = struct {
17 ElfDynLib17 ElfDynLib
18 else18 else
19 DlDynLib,19 DlDynLib,
20 .windows => WindowsDynLib,
21 .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos, .freebsd, .netbsd, .openbsd, .dragonfly, .illumos => DlDynLib,20 .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos, .freebsd, .netbsd, .openbsd, .dragonfly, .illumos => DlDynLib,
22 else => struct {21 else => struct {
23 const open = @compileError("unsupported platform");22 const open = @compileError("unsupported platform");
...@@ -27,7 +26,7 @@ pub const DynLib = struct {...@@ -27,7 +26,7 @@ pub const DynLib = struct {
2726
28 inner: InnerType,27 inner: InnerType,
2928
30 pub const Error = ElfDynLibError || DlDynLibError || WindowsDynLibError;29 pub const Error = ElfDynLibError || DlDynLibError;
3130
32 /// Trusts the file. Malicious file will be able to execute arbitrary code.31 /// Trusts the file. Malicious file will be able to execute arbitrary code.
33 pub fn open(path: []const u8) Error!DynLib {32 pub fn open(path: []const u8) Error!DynLib {
...@@ -558,73 +557,6 @@ test "ElfDynLib" {...@@ -558,73 +557,6 @@ test "ElfDynLib" {
558 try testing.expectError(error.FileNotFound, ElfDynLib.openZ("invalid_so.so", null));557 try testing.expectError(error.FileNotFound, ElfDynLib.openZ("invalid_so.so", null));
559}558}
560559
561/// Separated to avoid referencing `WindowsDynLib`, because its field types may not
562/// be valid on other targets.
563const WindowsDynLibError = error{
564 FileNotFound,
565 InvalidPath,
566} || windows.LoadLibraryError;
567
568pub const WindowsDynLib = struct {
569 pub const Error = WindowsDynLibError;
570
571 dll: windows.HMODULE,
572
573 pub fn open(path: []const u8) Error!WindowsDynLib {
574 return openEx(path, .none);
575 }
576
577 /// WindowsDynLib specific
578 /// Opens dynamic library with specified library loading flags.
579 pub fn openEx(path: []const u8, flags: windows.LoadLibraryFlags) Error!WindowsDynLib {
580 const path_w = windows.sliceToPrefixedFileW(null, path) catch return error.InvalidPath;
581 return openExW(path_w.span().ptr, flags);
582 }
583
584 pub fn openZ(path_c: [*:0]const u8) Error!WindowsDynLib {
585 return openExZ(path_c, .none);
586 }
587
588 /// WindowsDynLib specific
589 /// Opens dynamic library with specified library loading flags.
590 pub fn openExZ(path_c: [*:0]const u8, flags: windows.LoadLibraryFlags) Error!WindowsDynLib {
591 const path_w = windows.cStrToPrefixedFileW(null, path_c) catch return error.InvalidPath;
592 return openExW(path_w.span().ptr, flags);
593 }
594
595 /// WindowsDynLib specific
596 pub fn openW(path_w: [*:0]const u16) Error!WindowsDynLib {
597 return openExW(path_w, .none);
598 }
599
600 /// WindowsDynLib specific
601 /// Opens dynamic library with specified library loading flags.
602 pub fn openExW(path_w: [*:0]const u16, flags: windows.LoadLibraryFlags) Error!WindowsDynLib {
603 var offset: usize = 0;
604 if (path_w[0] == '\\' and path_w[1] == '?' and path_w[2] == '?' and path_w[3] == '\\') {
605 // + 4 to skip over the \??\
606 offset = 4;
607 }
608
609 return .{
610 .dll = try windows.LoadLibraryExW(path_w + offset, flags),
611 };
612 }
613
614 pub fn close(self: *WindowsDynLib) void {
615 windows.FreeLibrary(self.dll);
616 self.* = undefined;
617 }
618
619 pub fn lookup(self: *WindowsDynLib, comptime T: type, name: [:0]const u8) ?T {
620 if (windows.kernel32.GetProcAddress(self.dll, name.ptr)) |addr| {
621 return @as(T, @ptrCast(@alignCast(addr)));
622 } else {
623 return null;
624 }
625 }
626};
627
628/// Separated to avoid referencing `DlDynLib`, because its field types may not560/// Separated to avoid referencing `DlDynLib`, because its field types may not
629/// be valid on other targets.561/// be valid on other targets.
630const DlDynLibError = error{ FileNotFound, NameTooLong };562const DlDynLibError = error{ FileNotFound, NameTooLong };
...@@ -676,7 +608,6 @@ pub const DlDynLib = struct {...@@ -676,7 +608,6 @@ pub const DlDynLib = struct {
676test "dynamic_library" {608test "dynamic_library" {
677 const libname = switch (native_os) {609 const libname = switch (native_os) {
678 .linux, .freebsd, .openbsd, .illumos => "invalid_so.so",610 .linux, .freebsd, .openbsd, .illumos => "invalid_so.so",
679 .windows => "invalid_dll.dll",
680 .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => "invalid_dylib.dylib",611 .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => "invalid_dylib.dylib",
681 else => return error.SkipZigTest,612 else => return error.SkipZigTest,
682 };613 };
lib/std/os/windows.zig-565
...@@ -15,12 +15,6 @@ const math = std.math;...@@ -15,12 +15,6 @@ const math = std.math;
15const maxInt = std.math.maxInt;15const maxInt = std.math.maxInt;
16const UnexpectedError = std.posix.UnexpectedError;16const UnexpectedError = std.posix.UnexpectedError;
1717
18test {
19 if (builtin.os.tag == .windows) {
20 _ = @import("windows/test.zig");
21 }
22}
23
24pub const advapi32 = @import("windows/advapi32.zig");18pub const advapi32 = @import("windows/advapi32.zig");
25pub const kernel32 = @import("windows/kernel32.zig");19pub const kernel32 = @import("windows/kernel32.zig");
26pub const ntdll = @import("windows/ntdll.zig");20pub const ntdll = @import("windows/ntdll.zig");
...@@ -2670,324 +2664,6 @@ pub fn CloseHandle(hObject: HANDLE) void {...@@ -2670,324 +2664,6 @@ pub fn CloseHandle(hObject: HANDLE) void {
2670 assert(ntdll.NtClose(hObject) == .SUCCESS);2664 assert(ntdll.NtClose(hObject) == .SUCCESS);
2671}2665}
26722666
2673pub const QueryObjectNameError = error{
2674 AccessDenied,
2675 InvalidHandle,
2676 NameTooLong,
2677 Unexpected,
2678};
2679
2680pub fn QueryObjectName(handle: HANDLE, out_buffer: []u16) QueryObjectNameError![]u16 {
2681 const out_buffer_aligned = mem.alignInSlice(out_buffer, @alignOf(OBJECT_NAME_INFORMATION)) orelse return error.NameTooLong;
2682
2683 const info = @as(*OBJECT_NAME_INFORMATION, @ptrCast(out_buffer_aligned));
2684 // buffer size is specified in bytes
2685 const out_buffer_len = std.math.cast(ULONG, out_buffer_aligned.len * 2) orelse maxInt(ULONG);
2686 // last argument would return the length required for full_buffer, not exposed here
2687 return switch (ntdll.NtQueryObject(handle, .ObjectNameInformation, info, out_buffer_len, null)) {
2688 .SUCCESS => blk: {
2689 // info.Name.Buffer from ObQueryNameString is documented to be null (and MaximumLength == 0)
2690 // if the object was "unnamed", not sure if this can happen for file handles
2691 if (info.Name.MaximumLength == 0) break :blk error.Unexpected;
2692 // resulting string length is specified in bytes
2693 const path_length_unterminated = @divExact(info.Name.Length, 2);
2694 break :blk info.Name.Buffer.?[0..path_length_unterminated];
2695 },
2696 .ACCESS_DENIED => error.AccessDenied,
2697 .INVALID_HANDLE => error.InvalidHandle,
2698 // triggered when the buffer is too small for the OBJECT_NAME_INFORMATION object (.INFO_LENGTH_MISMATCH),
2699 // or if the buffer is too small for the file path returned (.BUFFER_OVERFLOW, .BUFFER_TOO_SMALL)
2700 .INFO_LENGTH_MISMATCH, .BUFFER_OVERFLOW, .BUFFER_TOO_SMALL => error.NameTooLong,
2701 else => |e| unexpectedStatus(e),
2702 };
2703}
2704
2705test QueryObjectName {
2706 if (builtin.os.tag != .windows)
2707 return;
2708
2709 //any file will do; canonicalization works on NTFS junctions and symlinks, hardlinks remain separate paths.
2710 var tmp = std.testing.tmpDir(.{});
2711 defer tmp.cleanup();
2712 const handle = tmp.dir.handle;
2713 var out_buffer: [PATH_MAX_WIDE]u16 = undefined;
2714
2715 const result_path = try QueryObjectName(handle, &out_buffer);
2716 const required_len_in_u16 = result_path.len + @divExact(@intFromPtr(result_path.ptr) - @intFromPtr(&out_buffer), 2) + 1;
2717 //insufficient size
2718 try std.testing.expectError(error.NameTooLong, QueryObjectName(handle, out_buffer[0 .. required_len_in_u16 - 1]));
2719 //exactly-sufficient size
2720 _ = try QueryObjectName(handle, out_buffer[0..required_len_in_u16]);
2721}
2722
2723pub const GetFinalPathNameByHandleError = error{
2724 AccessDenied,
2725 FileNotFound,
2726 NameTooLong,
2727 /// The volume does not contain a recognized file system. File system
2728 /// drivers might not be loaded, or the volume may be corrupt.
2729 UnrecognizedVolume,
2730 Unexpected,
2731};
2732
2733/// Specifies how to format volume path in the result of `GetFinalPathNameByHandle`.
2734/// Defaults to DOS volume names.
2735pub const GetFinalPathNameByHandleFormat = struct {
2736 volume_name: enum {
2737 /// Format as DOS volume name
2738 Dos,
2739 /// Format as NT volume name
2740 Nt,
2741 } = .Dos,
2742};
2743
2744/// Returns canonical (normalized) path of handle.
2745/// Use `GetFinalPathNameByHandleFormat` to specify whether the path is meant to include
2746/// NT or DOS volume name (e.g., `\Device\HarddiskVolume0\foo.txt` versus `C:\foo.txt`).
2747/// If DOS volume name format is selected, note that this function does *not* prepend
2748/// `\\?\` prefix to the resultant path.
2749///
2750/// TODO move this function into std.Io.Threaded and add cancelation checks
2751pub fn GetFinalPathNameByHandle(
2752 hFile: HANDLE,
2753 fmt: GetFinalPathNameByHandleFormat,
2754 out_buffer: []u16,
2755) GetFinalPathNameByHandleError![]u16 {
2756 const final_path = QueryObjectName(hFile, out_buffer) catch |err| switch (err) {
2757 // we assume InvalidHandle is close enough to FileNotFound in semantics
2758 // to not further complicate the error set
2759 error.InvalidHandle => return error.FileNotFound,
2760 else => |e| return e,
2761 };
2762
2763 switch (fmt.volume_name) {
2764 .Nt => {
2765 // the returned path is already in .Nt format
2766 return final_path;
2767 },
2768 .Dos => {
2769 // parse the string to separate volume path from file path
2770 const device_prefix = std.unicode.utf8ToUtf16LeStringLiteral("\\Device\\");
2771
2772 // We aren't entirely sure of the structure of the path returned by
2773 // QueryObjectName in all contexts/environments.
2774 // This code is written to cover the various cases that have
2775 // been encountered and solved appropriately. But note that there's
2776 // no easy way to verify that they have all been tackled!
2777 // (Unless you, the reader knows of one then please do action that!)
2778 if (!mem.startsWith(u16, final_path, device_prefix)) {
2779 // Wine seems to return NT namespaced paths starting with \??\ from QueryObjectName
2780 // (e.g. `\??\Z:\some\path\to\a\file.txt`), in which case we can just strip the
2781 // prefix to turn it into an absolute path.
2782 // https://github.com/ziglang/zig/issues/26029
2783 // https://bugs.winehq.org/show_bug.cgi?id=39569
2784 return ntToWin32Namespace(final_path, out_buffer) catch |err| switch (err) {
2785 error.NotNtPath => return error.Unexpected,
2786 error.NameTooLong => |e| return e,
2787 };
2788 }
2789
2790 const file_path_begin_index = mem.findPos(u16, final_path, device_prefix.len, &[_]u16{'\\'}) orelse unreachable;
2791 const volume_name_u16 = final_path[0..file_path_begin_index];
2792 const device_name_u16 = volume_name_u16[device_prefix.len..];
2793 const file_name_u16 = final_path[file_path_begin_index..];
2794
2795 // MUP is Multiple UNC Provider, and indicates that the path is a UNC
2796 // path. In this case, the canonical UNC path can be gotten by just
2797 // dropping the \Device\Mup\ and making sure the path begins with \\
2798 if (mem.eql(u16, device_name_u16, std.unicode.utf8ToUtf16LeStringLiteral("Mup"))) {
2799 out_buffer[0] = '\\';
2800 @memmove(out_buffer[1..][0..file_name_u16.len], file_name_u16);
2801 return out_buffer[0 .. 1 + file_name_u16.len];
2802 }
2803
2804 // Get DOS volume name. DOS volume names are actually symbolic link objects to the
2805 // actual NT volume. For example:
2806 // (NT) \Device\HarddiskVolume4 => (DOS) \DosDevices\C: == (DOS) C:
2807 const MIN_SIZE = @sizeOf(MOUNTMGR_MOUNT_POINT) + MAX_PATH;
2808 // We initialize the input buffer to all zeros for convenience since
2809 // `DeviceIoControl` with `IOCTL_MOUNTMGR_QUERY_POINTS` expects this.
2810 var input_buf: [MIN_SIZE]u8 align(@alignOf(MOUNTMGR_MOUNT_POINT)) = [_]u8{0} ** MIN_SIZE;
2811 var output_buf: [MIN_SIZE * 4]u8 align(@alignOf(MOUNTMGR_MOUNT_POINTS)) = undefined;
2812
2813 // This surprising path is a filesystem path to the mount manager on Windows.
2814 // Source: https://stackoverflow.com/questions/3012828/using-ioctl-mountmgr-query-points
2815 // This is the NT namespaced version of \\.\MountPointManager
2816 const mgmt_path_u16 = std.unicode.utf8ToUtf16LeStringLiteral("\\??\\MountPointManager");
2817 const mgmt_handle = OpenFile(mgmt_path_u16, .{
2818 .access_mask = .{ .STANDARD = .{ .SYNCHRONIZE = true } },
2819 .creation = .OPEN,
2820 }) catch |err| switch (err) {
2821 error.IsDir => return error.Unexpected,
2822 error.NotDir => return error.Unexpected,
2823 error.NoDevice => return error.Unexpected,
2824 error.AccessDenied => return error.Unexpected,
2825 error.PipeBusy => return error.Unexpected,
2826 error.PathAlreadyExists => return error.Unexpected,
2827 error.WouldBlock => return error.Unexpected,
2828 error.NetworkNotFound => return error.Unexpected,
2829 error.AntivirusInterference => return error.Unexpected,
2830 error.BadPathName => return error.Unexpected,
2831 error.OperationCanceled => @panic("TODO: better integrate cancelation"),
2832 else => |e| return e,
2833 };
2834 defer CloseHandle(mgmt_handle);
2835
2836 var input_struct: *MOUNTMGR_MOUNT_POINT = @ptrCast(&input_buf[0]);
2837 input_struct.DeviceNameOffset = @sizeOf(MOUNTMGR_MOUNT_POINT);
2838 input_struct.DeviceNameLength = @intCast(volume_name_u16.len * 2);
2839 @memcpy(input_buf[@sizeOf(MOUNTMGR_MOUNT_POINT)..][0 .. volume_name_u16.len * 2], @as([*]const u8, @ptrCast(volume_name_u16.ptr)));
2840
2841 {
2842 const rc = DeviceIoControl(mgmt_handle, IOCTL.MOUNTMGR.QUERY_POINTS, .{ .in = &input_buf, .out = &output_buf });
2843 switch (rc) {
2844 .SUCCESS => {},
2845 .OBJECT_NAME_NOT_FOUND => return error.FileNotFound,
2846 else => return unexpectedStatus(rc),
2847 }
2848 }
2849 const mount_points_struct: *const MOUNTMGR_MOUNT_POINTS = @ptrCast(&output_buf[0]);
2850
2851 const mount_points = @as(
2852 [*]const MOUNTMGR_MOUNT_POINT,
2853 @ptrCast(&mount_points_struct.MountPoints[0]),
2854 )[0..mount_points_struct.NumberOfMountPoints];
2855
2856 for (mount_points) |mount_point| {
2857 const symlink = @as(
2858 [*]const u16,
2859 @ptrCast(@alignCast(&output_buf[mount_point.SymbolicLinkNameOffset])),
2860 )[0 .. mount_point.SymbolicLinkNameLength / 2];
2861
2862 // Look for `\DosDevices\` prefix. We don't really care if there are more than one symlinks
2863 // with traditional DOS drive letters, so pick the first one available.
2864 var prefix_buf = std.unicode.utf8ToUtf16LeStringLiteral("\\DosDevices\\");
2865 const prefix = prefix_buf[0..prefix_buf.len];
2866
2867 if (mem.startsWith(u16, symlink, prefix)) {
2868 const drive_letter = symlink[prefix.len..];
2869
2870 if (out_buffer.len < drive_letter.len + file_name_u16.len) return error.NameTooLong;
2871
2872 @memcpy(out_buffer[0..drive_letter.len], drive_letter);
2873 @memmove(out_buffer[drive_letter.len..][0..file_name_u16.len], file_name_u16);
2874 const total_len = drive_letter.len + file_name_u16.len;
2875
2876 // Validate that DOS does not contain any spurious nul bytes.
2877 assert(mem.findScalar(u16, out_buffer[0..total_len], 0) == null);
2878
2879 return out_buffer[0..total_len];
2880 } else if (mountmgrIsVolumeName(symlink)) {
2881 // If the symlink is a volume GUID like \??\Volume{383da0b0-717f-41b6-8c36-00500992b58d},
2882 // then it is a volume mounted as a path rather than a drive letter. We need to
2883 // query the mount manager again to get the DOS path for the volume.
2884
2885 // 49 is the maximum length accepted by mountmgrIsVolumeName
2886 const vol_input_size = @sizeOf(MOUNTMGR_TARGET_NAME) + (49 * 2);
2887 var vol_input_buf: [vol_input_size]u8 align(@alignOf(MOUNTMGR_TARGET_NAME)) = [_]u8{0} ** vol_input_size;
2888 // Note: If the path exceeds MAX_PATH, the Disk Management GUI doesn't accept the full path,
2889 // and instead if must be specified using a shortened form (e.g. C:\FOO~1\BAR~1\<...>).
2890 // However, just to be sure we can handle any path length, we use PATH_MAX_WIDE here.
2891 const min_output_size = @sizeOf(MOUNTMGR_VOLUME_PATHS) + (PATH_MAX_WIDE * 2);
2892 var vol_output_buf: [min_output_size]u8 align(@alignOf(MOUNTMGR_VOLUME_PATHS)) = undefined;
2893
2894 var vol_input_struct: *MOUNTMGR_TARGET_NAME = @ptrCast(&vol_input_buf[0]);
2895 vol_input_struct.DeviceNameLength = @intCast(symlink.len * 2);
2896 @memcpy(@as([*]WCHAR, &vol_input_struct.DeviceName)[0..symlink.len], symlink);
2897
2898 const rc = DeviceIoControl(mgmt_handle, IOCTL.MOUNTMGR.QUERY_DOS_VOLUME_PATH, .{ .in = &vol_input_buf, .out = &vol_output_buf });
2899 switch (rc) {
2900 .SUCCESS => {},
2901 .UNRECOGNIZED_VOLUME => return error.UnrecognizedVolume,
2902 else => return unexpectedStatus(rc),
2903 }
2904 const volume_paths_struct: *const MOUNTMGR_VOLUME_PATHS = @ptrCast(&vol_output_buf[0]);
2905 const volume_path = std.mem.sliceTo(@as(
2906 [*]const u16,
2907 &volume_paths_struct.MultiSz,
2908 )[0 .. volume_paths_struct.MultiSzLength / 2], 0);
2909
2910 if (out_buffer.len < volume_path.len + file_name_u16.len) return error.NameTooLong;
2911
2912 // `out_buffer` currently contains the memory of `file_name_u16`, so it can overlap with where
2913 // we want to place the filename before returning. Here are the possible overlapping cases:
2914 //
2915 // out_buffer: [filename]
2916 // dest: [___(a)___] [___(b)___]
2917 //
2918 // In the case of (a), we need to copy forwards, and in the case of (b) we need
2919 // to copy backwards. We also need to do this before copying the volume path because
2920 // it could overwrite the file_name_u16 memory.
2921 const file_name_dest = out_buffer[volume_path.len..][0..file_name_u16.len];
2922 @memmove(file_name_dest, file_name_u16);
2923 @memcpy(out_buffer[0..volume_path.len], volume_path);
2924 const total_len = volume_path.len + file_name_u16.len;
2925
2926 // Validate that DOS does not contain any spurious nul bytes.
2927 assert(mem.findScalar(u16, out_buffer[0..total_len], 0) == null);
2928
2929 return out_buffer[0..total_len];
2930 }
2931 }
2932
2933 // If we've ended up here, then something went wrong/is corrupted in the OS,
2934 // so error out!
2935 return error.FileNotFound;
2936 },
2937 }
2938}
2939
2940/// Equivalent to the MOUNTMGR_IS_VOLUME_NAME macro in mountmgr.h
2941fn mountmgrIsVolumeName(name: []const u16) bool {
2942 return (name.len == 48 or (name.len == 49 and name[48] == mem.nativeToLittle(u16, '\\'))) and
2943 name[0] == mem.nativeToLittle(u16, '\\') and
2944 (name[1] == mem.nativeToLittle(u16, '?') or name[1] == mem.nativeToLittle(u16, '\\')) and
2945 name[2] == mem.nativeToLittle(u16, '?') and
2946 name[3] == mem.nativeToLittle(u16, '\\') and
2947 mem.startsWith(u16, name[4..], std.unicode.utf8ToUtf16LeStringLiteral("Volume{")) and
2948 name[19] == mem.nativeToLittle(u16, '-') and
2949 name[24] == mem.nativeToLittle(u16, '-') and
2950 name[29] == mem.nativeToLittle(u16, '-') and
2951 name[34] == mem.nativeToLittle(u16, '-') and
2952 name[47] == mem.nativeToLittle(u16, '}');
2953}
2954
2955test mountmgrIsVolumeName {
2956 @setEvalBranchQuota(2000);
2957 const L = std.unicode.utf8ToUtf16LeStringLiteral;
2958 try std.testing.expect(mountmgrIsVolumeName(L("\\\\?\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}")));
2959 try std.testing.expect(mountmgrIsVolumeName(L("\\??\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}")));
2960 try std.testing.expect(mountmgrIsVolumeName(L("\\\\?\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}\\")));
2961 try std.testing.expect(mountmgrIsVolumeName(L("\\??\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}\\")));
2962 try std.testing.expect(!mountmgrIsVolumeName(L("\\\\.\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}")));
2963 try std.testing.expect(!mountmgrIsVolumeName(L("\\??\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}\\foo")));
2964 try std.testing.expect(!mountmgrIsVolumeName(L("\\??\\Volume{383da0b0-717f-41b6-8c36-00500992b58}")));
2965}
2966
2967test GetFinalPathNameByHandle {
2968 if (builtin.os.tag != .windows)
2969 return;
2970
2971 //any file will do
2972 var tmp = std.testing.tmpDir(.{});
2973 defer tmp.cleanup();
2974 const handle = tmp.dir.handle;
2975 var buffer: [PATH_MAX_WIDE]u16 = undefined;
2976
2977 //check with sufficient size
2978 const nt_path = try GetFinalPathNameByHandle(handle, .{ .volume_name = .Nt }, &buffer);
2979 _ = try GetFinalPathNameByHandle(handle, .{ .volume_name = .Dos }, &buffer);
2980
2981 const required_len_in_u16 = nt_path.len + @divExact(@intFromPtr(nt_path.ptr) - @intFromPtr(&buffer), 2) + 1;
2982 //check with insufficient size
2983 try std.testing.expectError(error.NameTooLong, GetFinalPathNameByHandle(handle, .{ .volume_name = .Nt }, buffer[0 .. required_len_in_u16 - 1]));
2984 try std.testing.expectError(error.NameTooLong, GetFinalPathNameByHandle(handle, .{ .volume_name = .Dos }, buffer[0 .. required_len_in_u16 - 1]));
2985
2986 //check with exactly-sufficient size
2987 _ = try GetFinalPathNameByHandle(handle, .{ .volume_name = .Nt }, buffer[0..required_len_in_u16]);
2988 _ = try GetFinalPathNameByHandle(handle, .{ .volume_name = .Dos }, buffer[0..required_len_in_u16]);
2989}
2990
2991pub fn getpeername(s: ws2_32.SOCKET, name: *ws2_32.sockaddr, namelen: *ws2_32.socklen_t) i32 {2667pub fn getpeername(s: ws2_32.SOCKET, name: *ws2_32.sockaddr, namelen: *ws2_32.socklen_t) i32 {
2992 return ws2_32.getpeername(s, name, @as(*i32, @ptrCast(namelen)));2668 return ws2_32.getpeername(s, name, @as(*i32, @ptrCast(namelen)));
2993}2669}
...@@ -3419,15 +3095,6 @@ test "eqlIgnoreCaseWtf16/Wtf8" {...@@ -3419,15 +3095,6 @@ test "eqlIgnoreCaseWtf16/Wtf8" {
3419 try testEqlIgnoreCase(false, "𐓏", "𐓷");3095 try testEqlIgnoreCase(false, "𐓏", "𐓷");
3420}3096}
34213097
3422pub const PathSpace = struct {
3423 data: [PATH_MAX_WIDE:0]u16,
3424 len: usize,
3425
3426 pub fn span(self: *const PathSpace) [:0]const u16 {
3427 return self.data[0..self.len :0];
3428 }
3429};
3430
3431/// The error type for `removeDotDirsSanitized`3098/// The error type for `removeDotDirsSanitized`
3432pub const RemoveDotDirsError = error{TooManyParentDirs};3099pub const RemoveDotDirsError = error{TooManyParentDirs};
34333100
...@@ -3503,205 +3170,6 @@ pub fn normalizePath(comptime T: type, path: []T) RemoveDotDirsError!usize {...@@ -3503,205 +3170,6 @@ pub fn normalizePath(comptime T: type, path: []T) RemoveDotDirsError!usize {
3503 return prefix_len + try removeDotDirsSanitized(T, path[prefix_len..new_len]);3170 return prefix_len + try removeDotDirsSanitized(T, path[prefix_len..new_len]);
3504}3171}
35053172
3506pub const Wtf8ToPrefixedFileWError = Wtf16ToPrefixedFileWError;
3507
3508/// Same as `sliceToPrefixedFileW` but accepts a pointer
3509/// to a null-terminated WTF-8 encoded path.
3510/// https://wtf-8.codeberg.page/
3511pub fn cStrToPrefixedFileW(dir: ?HANDLE, s: [*:0]const u8) Wtf8ToPrefixedFileWError!PathSpace {
3512 return sliceToPrefixedFileW(dir, mem.sliceTo(s, 0));
3513}
3514
3515/// Same as `wToPrefixedFileW` but accepts a WTF-8 encoded path.
3516/// https://wtf-8.codeberg.page/
3517pub fn sliceToPrefixedFileW(dir: ?HANDLE, path: []const u8) Wtf8ToPrefixedFileWError!PathSpace {
3518 var temp_path: PathSpace = undefined;
3519 temp_path.len = std.unicode.wtf8ToWtf16Le(&temp_path.data, path) catch |err| switch (err) {
3520 error.InvalidWtf8 => return error.BadPathName,
3521 };
3522 temp_path.data[temp_path.len] = 0;
3523 return wToPrefixedFileW(dir, temp_path.span());
3524}
3525
3526pub const Wtf16ToPrefixedFileWError = error{
3527 AccessDenied,
3528 BadPathName,
3529 FileNotFound,
3530 NameTooLong,
3531 Unexpected,
3532};
3533
3534/// Converts the `path` to WTF16, null-terminated. If the path contains any
3535/// namespace prefix, or is anything but a relative path (rooted, drive relative,
3536/// etc) the result will have the NT-style prefix `\??\`.
3537///
3538/// Similar to RtlDosPathNameToNtPathName_U with a few differences:
3539/// - Does not allocate on the heap.
3540/// - Relative paths are kept as relative unless they contain too many ..
3541/// components, in which case they are resolved against the `dir` if it
3542/// is non-null, or the CWD if it is null.
3543/// - Special case device names like COM1, NUL, etc are not handled specially (TODO)
3544/// - . and space are not stripped from the end of relative paths (potential TODO)
3545pub fn wToPrefixedFileW(dir: ?HANDLE, path: [:0]const u16) Wtf16ToPrefixedFileWError!PathSpace {
3546 const nt_prefix = [_]u16{ '\\', '?', '?', '\\' };
3547 if (hasCommonNtPrefix(u16, path)) {
3548 // TODO: Figure out a way to design an API that can avoid the copy for NT,
3549 // since it is always returned fully unmodified.
3550 var path_space: PathSpace = undefined;
3551 path_space.data[0..nt_prefix.len].* = nt_prefix;
3552 const len_after_prefix = path.len - nt_prefix.len;
3553 @memcpy(path_space.data[nt_prefix.len..][0..len_after_prefix], path[nt_prefix.len..]);
3554 path_space.len = path.len;
3555 path_space.data[path_space.len] = 0;
3556 return path_space;
3557 } else {
3558 const path_type = std.fs.path.getWin32PathType(u16, path);
3559 var path_space: PathSpace = undefined;
3560 if (path_type == .local_device) {
3561 switch (getLocalDevicePathType(u16, path)) {
3562 .verbatim => {
3563 path_space.data[0..nt_prefix.len].* = nt_prefix;
3564 const len_after_prefix = path.len - nt_prefix.len;
3565 @memcpy(path_space.data[nt_prefix.len..][0..len_after_prefix], path[nt_prefix.len..]);
3566 path_space.len = path.len;
3567 path_space.data[path_space.len] = 0;
3568 return path_space;
3569 },
3570 .local_device, .fake_verbatim => {
3571 const path_byte_len = ntdll.RtlGetFullPathName_U(
3572 path.ptr,
3573 path_space.data.len * 2,
3574 &path_space.data,
3575 null,
3576 );
3577 if (path_byte_len == 0) {
3578 // TODO: This may not be the right error
3579 return error.BadPathName;
3580 } else if (path_byte_len / 2 > path_space.data.len) {
3581 return error.NameTooLong;
3582 }
3583 path_space.len = path_byte_len / 2;
3584 // Both prefixes will be normalized but retained, so all
3585 // we need to do now is replace them with the NT prefix
3586 path_space.data[0..nt_prefix.len].* = nt_prefix;
3587 return path_space;
3588 },
3589 }
3590 }
3591 relative: {
3592 if (path_type == .relative) {
3593 // TODO: Handle special case device names like COM1, AUX, NUL, CONIN$, CONOUT$, etc.
3594 // See https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
3595
3596 // TODO: Potentially strip all trailing . and space characters from the
3597 // end of the path. This is something that both RtlDosPathNameToNtPathName_U
3598 // and RtlGetFullPathName_U do. Technically, trailing . and spaces
3599 // are allowed, but such paths may not interact well with Windows (i.e.
3600 // files with these paths can't be deleted from explorer.exe, etc).
3601 // This could be something that normalizePath may want to do.
3602
3603 @memcpy(path_space.data[0..path.len], path);
3604 // Try to normalize, but if we get too many parent directories,
3605 // then we need to start over and use RtlGetFullPathName_U instead.
3606 path_space.len = normalizePath(u16, path_space.data[0..path.len]) catch |err| switch (err) {
3607 error.TooManyParentDirs => break :relative,
3608 };
3609 path_space.data[path_space.len] = 0;
3610 return path_space;
3611 }
3612 }
3613 // We now know we are going to return an absolute NT path, so
3614 // we can unconditionally prefix it with the NT prefix.
3615 path_space.data[0..nt_prefix.len].* = nt_prefix;
3616 if (path_type == .root_local_device) {
3617 // `\\.` and `\\?` always get converted to `\??\` exactly, so
3618 // we can just stop here
3619 path_space.len = nt_prefix.len;
3620 path_space.data[path_space.len] = 0;
3621 return path_space;
3622 }
3623 const path_buf_offset = switch (path_type) {
3624 // UNC paths will always start with `\\`. However, we want to
3625 // end up with something like `\??\UNC\server\share`, so to get
3626 // RtlGetFullPathName to write into the spot we want the `server`
3627 // part to end up, we need to provide an offset such that
3628 // the `\\` part gets written where the `C\` of `UNC\` will be
3629 // in the final NT path.
3630 .unc_absolute => nt_prefix.len + 2,
3631 else => nt_prefix.len,
3632 };
3633 const buf_len: u32 = @intCast(path_space.data.len - path_buf_offset);
3634 const path_to_get: [:0]const u16 = path_to_get: {
3635 // If dir is null, then we don't need to bother with GetFinalPathNameByHandle because
3636 // RtlGetFullPathName_U will resolve relative paths against the CWD for us.
3637 if (path_type != .relative or dir == null) {
3638 break :path_to_get path;
3639 }
3640 // We can also skip GetFinalPathNameByHandle if the handle matches
3641 // the handle returned by Io.Dir.cwd()
3642 if (dir.? == Io.Dir.cwd().handle) {
3643 break :path_to_get path;
3644 }
3645 // At this point, we know we have a relative path that had too many
3646 // `..` components to be resolved by normalizePath, so we need to
3647 // convert it into an absolute path and let RtlGetFullPathName_U
3648 // canonicalize it. We do this by getting the path of the `dir`
3649 // and appending the relative path to it.
3650 var dir_path_buf: [PATH_MAX_WIDE:0]u16 = undefined;
3651 const dir_path = GetFinalPathNameByHandle(dir.?, .{}, &dir_path_buf) catch |err| switch (err) {
3652 // This mapping is not correct; it is actually expected
3653 // that calling GetFinalPathNameByHandle might return
3654 // error.UnrecognizedVolume, and in fact has been observed
3655 // in the wild. The problem is that wToPrefixedFileW was
3656 // never intended to make *any* OS syscall APIs. It's only
3657 // supposed to convert a string to one that is eligible to
3658 // be used in the ntdll syscalls.
3659 //
3660 // To solve this, this function needs to no longer call
3661 // GetFinalPathNameByHandle under any conditions, or the
3662 // calling function needs to get reworked to not need to
3663 // call this function.
3664 //
3665 // This may involve making breaking API changes.
3666 error.UnrecognizedVolume => return error.Unexpected,
3667 else => |e| return e,
3668 };
3669 if (dir_path.len + 1 + path.len > PATH_MAX_WIDE) {
3670 return error.NameTooLong;
3671 }
3672 // We don't have to worry about potentially doubling up path separators
3673 // here since RtlGetFullPathName_U will handle canonicalizing it.
3674 dir_path_buf[dir_path.len] = '\\';
3675 @memcpy(dir_path_buf[dir_path.len + 1 ..][0..path.len], path);
3676 const full_len = dir_path.len + 1 + path.len;
3677 dir_path_buf[full_len] = 0;
3678 break :path_to_get dir_path_buf[0..full_len :0];
3679 };
3680 const path_byte_len = ntdll.RtlGetFullPathName_U(
3681 path_to_get.ptr,
3682 buf_len * 2,
3683 path_space.data[path_buf_offset..].ptr,
3684 null,
3685 );
3686 if (path_byte_len == 0) {
3687 // TODO: This may not be the right error
3688 return error.BadPathName;
3689 } else if (path_byte_len / 2 > buf_len) {
3690 return error.NameTooLong;
3691 }
3692 path_space.len = path_buf_offset + (path_byte_len / 2);
3693 if (path_type == .unc_absolute) {
3694 // Now add in the UNC, the `C` should overwrite the first `\` of the
3695 // FullPathName, ultimately resulting in `\??\UNC\<the rest of the path>`
3696 assert(path_space.data[path_buf_offset] == '\\');
3697 assert(path_space.data[path_buf_offset + 1] == '\\');
3698 const unc = [_]u16{ 'U', 'N', 'C' };
3699 path_space.data[nt_prefix.len..][0..unc.len].* = unc;
3700 }
3701 return path_space;
3702 }
3703}
3704
3705/// Returns true if the path starts with `\??\`, which is indicative of an NT path3173/// Returns true if the path starts with `\??\`, which is indicative of an NT path
3706/// but is not enough to fully distinguish between NT paths and Win32 paths, as3174/// but is not enough to fully distinguish between NT paths and Win32 paths, as
3707/// `\??\` is not actually a distinct prefix but rather the path to a special virtual3175/// `\??\` 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 {...@@ -3725,39 +3193,6 @@ pub fn hasCommonNtPrefix(comptime T: type, path: []const T) bool {
3725 return mem.startsWith(T, path, expected_prefix);3193 return mem.startsWith(T, path, expected_prefix);
3726}3194}
37273195
3728const LocalDevicePathType = enum {
3729 /// `\\.\` (path separators can be `\` or `/`)
3730 local_device,
3731 /// `\\?\`
3732 /// When converted to an NT path, everything past the prefix is left
3733 /// untouched and `\\?\` is replaced by `\??\`.
3734 verbatim,
3735 /// `\\?\` without all path separators being `\`.
3736 /// This seems to be recognized as a prefix, but the 'verbatim' aspect
3737 /// is not respected (i.e. if `//?/C:/foo` is converted to an NT path,
3738 /// it will become `\??\C:\foo` [it will be canonicalized and the //?/ won't
3739 /// be treated as part of the final path])
3740 fake_verbatim,
3741};
3742
3743/// Only relevant for Win32 -> NT path conversion.
3744/// Asserts `path` is of type `std.fs.path.Win32PathType.local_device`.
3745fn getLocalDevicePathType(comptime T: type, path: []const T) LocalDevicePathType {
3746 if (std.debug.runtime_safety) {
3747 assert(std.fs.path.getWin32PathType(T, path) == .local_device);
3748 }
3749
3750 const backslash = mem.nativeToLittle(T, '\\');
3751 const all_backslash = path[0] == backslash and
3752 path[1] == backslash and
3753 path[3] == backslash;
3754 return switch (path[2]) {
3755 mem.nativeToLittle(T, '?') => if (all_backslash) .verbatim else .fake_verbatim,
3756 mem.nativeToLittle(T, '.') => .local_device,
3757 else => unreachable,
3758 };
3759}
3760
3761/// Similar to `RtlNtPathNameToDosPathName` but does not do any heap allocation.3196/// Similar to `RtlNtPathNameToDosPathName` but does not do any heap allocation.
3762/// The possible transformations are:3197/// The possible transformations are:
3763/// \??\C:\Some\Path -> C:\Some\Path3198/// \??\C:\Some\Path -> C:\Some\Path
lib/std/os/windows/test.zig deleted-339
...@@ -1,339 +0,0 @@
1const std = @import("../../std.zig");
2const builtin = @import("builtin");
3const windows = std.os.windows;
4const mem = std.mem;
5const testing = std.testing;
6
7/// Wrapper around RtlDosPathNameToNtPathName_U for use in comparing
8/// the behavior of RtlDosPathNameToNtPathName_U with wToPrefixedFileW
9/// Note: RtlDosPathNameToNtPathName_U is not used in the Zig implementation
10// because it allocates.
11fn RtlDosPathNameToNtPathName_U(path: [:0]const u16) !windows.PathSpace {
12 var out: windows.UNICODE_STRING = undefined;
13 const rc = windows.ntdll.RtlDosPathNameToNtPathName_U(path, &out, null, null);
14 if (rc != windows.TRUE) return error.BadPathName;
15 defer windows.ntdll.RtlFreeUnicodeString(&out);
16
17 var path_space: windows.PathSpace = undefined;
18 const out_path = out.Buffer.?[0 .. out.Length / 2];
19 @memcpy(path_space.data[0..out_path.len], out_path);
20 path_space.len = out.Length / 2;
21 path_space.data[path_space.len] = 0;
22
23 return path_space;
24}
25
26/// Test that the Zig conversion matches the expected_path (for instances where
27/// the Zig implementation intentionally diverges from what RtlDosPathNameToNtPathName_U does).
28fn testToPrefixedFileNoOracle(comptime path: []const u8, comptime expected_path: []const u8) !void {
29 const path_utf16 = std.unicode.utf8ToUtf16LeStringLiteral(path);
30 const expected_path_utf16 = std.unicode.utf8ToUtf16LeStringLiteral(expected_path);
31 const actual_path = try windows.wToPrefixedFileW(null, path_utf16);
32 std.testing.expectEqualSlices(u16, expected_path_utf16, actual_path.span()) catch |e| {
33 std.debug.print("got '{f}', expected '{f}'\n", .{ std.unicode.fmtUtf16Le(actual_path.span()), std.unicode.fmtUtf16Le(expected_path_utf16) });
34 return e;
35 };
36}
37
38/// Test that the Zig conversion matches the expected_path and that the
39/// expected_path matches the conversion that RtlDosPathNameToNtPathName_U does.
40fn testToPrefixedFileWithOracle(comptime path: []const u8, comptime expected_path: []const u8) !void {
41 try testToPrefixedFileNoOracle(path, expected_path);
42 try testToPrefixedFileOnlyOracle(path);
43}
44
45/// Test that the Zig conversion matches the conversion that RtlDosPathNameToNtPathName_U does.
46fn testToPrefixedFileOnlyOracle(comptime path: []const u8) !void {
47 const path_utf16 = std.unicode.utf8ToUtf16LeStringLiteral(path);
48 const zig_result = try windows.wToPrefixedFileW(null, path_utf16);
49 const win32_api_result = try RtlDosPathNameToNtPathName_U(path_utf16);
50 std.testing.expectEqualSlices(u16, win32_api_result.span(), zig_result.span()) catch |e| {
51 std.debug.print("got '{f}', expected '{f}'\n", .{ std.unicode.fmtUtf16Le(zig_result.span()), std.unicode.fmtUtf16Le(win32_api_result.span()) });
52 return e;
53 };
54}
55
56test "toPrefixedFileW" {
57 if (builtin.os.tag != .windows) return error.SkipZigTest;
58
59 // Most test cases come from https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
60 // Note that these tests do not actually touch the filesystem or care about whether or not
61 // any of the paths actually exist or are otherwise valid.
62
63 // Drive Absolute
64 try testToPrefixedFileWithOracle("X:\\ABC\\DEF", "\\??\\X:\\ABC\\DEF");
65 try testToPrefixedFileWithOracle("X:\\", "\\??\\X:\\");
66 try testToPrefixedFileWithOracle("X:\\ABC\\", "\\??\\X:\\ABC\\");
67 // Trailing . and space characters are stripped
68 try testToPrefixedFileWithOracle("X:\\ABC\\DEF. .", "\\??\\X:\\ABC\\DEF");
69 try testToPrefixedFileWithOracle("X:/ABC/DEF", "\\??\\X:\\ABC\\DEF");
70 try testToPrefixedFileWithOracle("X:\\ABC\\..\\XYZ", "\\??\\X:\\XYZ");
71 try testToPrefixedFileWithOracle("X:\\ABC\\..\\..\\..", "\\??\\X:\\");
72 // Drive letter casing is unchanged
73 try testToPrefixedFileWithOracle("x:\\", "\\??\\x:\\");
74
75 // Drive Relative
76 // These tests depend on the CWD of the specified drive letter which can vary,
77 // so instead we just test that the Zig implementation matches the result of
78 // RtlDosPathNameToNtPathName_U.
79 // TODO: Setting the =X: environment variable didn't seem to affect
80 // RtlDosPathNameToNtPathName_U, not sure why that is but getting that
81 // to work could be an avenue to making these cases environment-independent.
82 // All -> are examples of the result if the X drive's cwd was X:\ABC
83 try testToPrefixedFileOnlyOracle("X:DEF\\GHI"); // -> \??\X:\ABC\DEF\GHI
84 try testToPrefixedFileOnlyOracle("X:"); // -> \??\X:\ABC
85 try testToPrefixedFileOnlyOracle("X:DEF. ."); // -> \??\X:\ABC\DEF
86 try testToPrefixedFileOnlyOracle("X:ABC\\..\\XYZ"); // -> \??\X:\ABC\XYZ
87 try testToPrefixedFileOnlyOracle("X:ABC\\..\\..\\.."); // -> \??\X:\
88 try testToPrefixedFileOnlyOracle("x:"); // -> \??\X:\ABC
89
90 // Rooted
91 // These tests depend on the drive letter of the CWD which can vary, so
92 // instead we just test that the Zig implementation matches the result of
93 // RtlDosPathNameToNtPathName_U.
94 // TODO: Getting the CWD path, getting the drive letter from it, and using it to
95 // construct the expected NT paths could be an avenue to making these cases
96 // environment-independent and therefore able to use testToPrefixedFileWithOracle.
97 // All -> are examples of the result if the CWD's drive letter was X
98 try testToPrefixedFileOnlyOracle("\\ABC\\DEF"); // -> \??\X:\ABC\DEF
99 try testToPrefixedFileOnlyOracle("\\"); // -> \??\X:\
100 try testToPrefixedFileOnlyOracle("\\ABC\\DEF. ."); // -> \??\X:\ABC\DEF
101 try testToPrefixedFileOnlyOracle("/ABC/DEF"); // -> \??\X:\ABC\DEF
102 try testToPrefixedFileOnlyOracle("\\ABC\\..\\XYZ"); // -> \??\X:\XYZ
103 try testToPrefixedFileOnlyOracle("\\ABC\\..\\..\\.."); // -> \??\X:\
104
105 // Relative
106 // These cases differ in functionality to RtlDosPathNameToNtPathName_U.
107 // Relative paths remain relative if they don't have enough .. components
108 // to error with TooManyParentDirs
109 try testToPrefixedFileNoOracle("ABC\\DEF", "ABC\\DEF");
110 // TODO: enable this if trailing . and spaces are stripped from relative paths
111 //try testToPrefixedFileNoOracle("ABC\\DEF. .", "ABC\\DEF");
112 try testToPrefixedFileNoOracle("ABC/DEF", "ABC\\DEF");
113 try testToPrefixedFileNoOracle("./ABC/.././DEF", "DEF");
114 // TooManyParentDirs, so resolved relative to the CWD
115 // All -> are examples of the result if the CWD was X:\ABC\DEF
116 try testToPrefixedFileOnlyOracle("..\\GHI"); // -> \??\X:\ABC\GHI
117 try testToPrefixedFileOnlyOracle("GHI\\..\\..\\.."); // -> \??\X:\
118
119 // UNC Absolute
120 try testToPrefixedFileWithOracle("\\\\server\\share\\ABC\\DEF", "\\??\\UNC\\server\\share\\ABC\\DEF");
121 try testToPrefixedFileWithOracle("\\\\server", "\\??\\UNC\\server");
122 try testToPrefixedFileWithOracle("\\\\server\\share", "\\??\\UNC\\server\\share");
123 try testToPrefixedFileWithOracle("\\\\server\\share\\ABC. .", "\\??\\UNC\\server\\share\\ABC");
124 try testToPrefixedFileWithOracle("//server/share/ABC/DEF", "\\??\\UNC\\server\\share\\ABC\\DEF");
125 try testToPrefixedFileWithOracle("\\\\server\\share\\ABC\\..\\XYZ", "\\??\\UNC\\server\\share\\XYZ");
126 try testToPrefixedFileWithOracle("\\\\server\\share\\ABC\\..\\..\\..", "\\??\\UNC\\server\\share");
127
128 // Local Device
129 try testToPrefixedFileWithOracle("\\\\.\\COM20", "\\??\\COM20");
130 try testToPrefixedFileWithOracle("\\\\.\\pipe\\mypipe", "\\??\\pipe\\mypipe");
131 try testToPrefixedFileWithOracle("\\\\.\\X:\\ABC\\DEF. .", "\\??\\X:\\ABC\\DEF");
132 try testToPrefixedFileWithOracle("\\\\.\\X:/ABC/DEF", "\\??\\X:\\ABC\\DEF");
133 try testToPrefixedFileWithOracle("\\\\.\\X:\\ABC\\..\\XYZ", "\\??\\X:\\XYZ");
134 // Can replace the first component of the path (contrary to drive absolute and UNC absolute paths)
135 try testToPrefixedFileWithOracle("\\\\.\\X:\\ABC\\..\\..\\C:\\", "\\??\\C:\\");
136 try testToPrefixedFileWithOracle("\\\\.\\pipe\\mypipe\\..\\notmine", "\\??\\pipe\\notmine");
137
138 // Special-case device names
139 // TODO: Enable once these are supported
140 // more cases to test here: https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
141 //try testToPrefixedFileWithOracle("COM1", "\\??\\COM1");
142 // Sometimes the special-cased device names are not respected
143 try testToPrefixedFileWithOracle("\\\\.\\X:\\COM1", "\\??\\X:\\COM1");
144 try testToPrefixedFileWithOracle("\\\\abc\\xyz\\COM1", "\\??\\UNC\\abc\\xyz\\COM1");
145
146 // Verbatim
147 // Left untouched except \\?\ is replaced by \??\
148 try testToPrefixedFileWithOracle("\\\\?\\X:", "\\??\\X:");
149 try testToPrefixedFileWithOracle("\\\\?\\X:\\COM1", "\\??\\X:\\COM1");
150 try testToPrefixedFileWithOracle("\\\\?\\X:/ABC/DEF. .", "\\??\\X:/ABC/DEF. .");
151 try testToPrefixedFileWithOracle("\\\\?\\X:\\ABC\\..\\..\\..", "\\??\\X:\\ABC\\..\\..\\..");
152 // NT Namespace
153 // Fully unmodified
154 try testToPrefixedFileWithOracle("\\??\\X:", "\\??\\X:");
155 try testToPrefixedFileWithOracle("\\??\\X:\\COM1", "\\??\\X:\\COM1");
156 try testToPrefixedFileWithOracle("\\??\\X:/ABC/DEF. .", "\\??\\X:/ABC/DEF. .");
157 try testToPrefixedFileWithOracle("\\??\\X:\\ABC\\..\\..\\..", "\\??\\X:\\ABC\\..\\..\\..");
158
159 // 'Fake' Verbatim
160 // If the prefix looks like the verbatim prefix but not all path separators in the
161 // prefix are backslashes, then it gets canonicalized and the prefix is dropped in favor
162 // of the NT prefix.
163 try testToPrefixedFileWithOracle("//?/C:/ABC", "\\??\\C:\\ABC");
164 // 'Fake' NT
165 // If the prefix looks like the NT prefix but not all path separators in the prefix
166 // are backslashes, then it gets canonicalized and the /??/ is not dropped but
167 // rather treated as part of the path. In other words, the path is treated
168 // as a rooted path, so the final path is resolved relative to the CWD's
169 // drive letter.
170 // The -> shows an example of the result if the CWD's drive letter was X
171 try testToPrefixedFileOnlyOracle("/??/C:/ABC"); // -> \??\X:\??\C:\ABC
172
173 // Root Local Device
174 // \\. and \\? always get converted to \??\
175 try testToPrefixedFileWithOracle("\\\\.", "\\??\\");
176 try testToPrefixedFileWithOracle("\\\\?", "\\??\\");
177 try testToPrefixedFileWithOracle("//?", "\\??\\");
178 try testToPrefixedFileWithOracle("//.", "\\??\\");
179}
180
181fn testRemoveDotDirs(str: []const u8, expected: []const u8) !void {
182 const mutable = try testing.allocator.dupe(u8, str);
183 defer testing.allocator.free(mutable);
184 const actual = mutable[0..try windows.removeDotDirsSanitized(u8, mutable)];
185 try testing.expect(mem.eql(u8, actual, expected));
186}
187fn testRemoveDotDirsError(err: anyerror, str: []const u8) !void {
188 const mutable = try testing.allocator.dupe(u8, str);
189 defer testing.allocator.free(mutable);
190 try testing.expectError(err, windows.removeDotDirsSanitized(u8, mutable));
191}
192test "removeDotDirs" {
193 try testRemoveDotDirs("", "");
194 try testRemoveDotDirs(".", "");
195 try testRemoveDotDirs(".\\", "");
196 try testRemoveDotDirs(".\\.", "");
197 try testRemoveDotDirs(".\\.\\", "");
198 try testRemoveDotDirs(".\\.\\.", "");
199
200 try testRemoveDotDirs("a", "a");
201 try testRemoveDotDirs("a\\", "a\\");
202 try testRemoveDotDirs("a\\b", "a\\b");
203 try testRemoveDotDirs("a\\.", "a\\");
204 try testRemoveDotDirs("a\\b\\.", "a\\b\\");
205 try testRemoveDotDirs("a\\.\\b", "a\\b");
206
207 try testRemoveDotDirs(".a", ".a");
208 try testRemoveDotDirs(".a\\", ".a\\");
209 try testRemoveDotDirs(".a\\.b", ".a\\.b");
210 try testRemoveDotDirs(".a\\.", ".a\\");
211 try testRemoveDotDirs(".a\\.\\.", ".a\\");
212 try testRemoveDotDirs(".a\\.\\.\\.b", ".a\\.b");
213 try testRemoveDotDirs(".a\\.\\.\\.b\\", ".a\\.b\\");
214
215 try testRemoveDotDirsError(error.TooManyParentDirs, "..");
216 try testRemoveDotDirsError(error.TooManyParentDirs, "..\\");
217 try testRemoveDotDirsError(error.TooManyParentDirs, ".\\..\\");
218 try testRemoveDotDirsError(error.TooManyParentDirs, ".\\.\\..\\");
219
220 try testRemoveDotDirs("a\\..", "");
221 try testRemoveDotDirs("a\\..\\", "");
222 try testRemoveDotDirs("a\\..\\.", "");
223 try testRemoveDotDirs("a\\..\\.\\", "");
224 try testRemoveDotDirs("a\\..\\.\\.", "");
225 try testRemoveDotDirsError(error.TooManyParentDirs, "a\\..\\.\\.\\..");
226
227 try testRemoveDotDirs("a\\..\\.\\.\\b", "b");
228 try testRemoveDotDirs("a\\..\\.\\.\\b\\", "b\\");
229 try testRemoveDotDirs("a\\..\\.\\.\\b\\.", "b\\");
230 try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\", "b\\");
231 try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\..", "");
232 try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\..\\", "");
233 try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\..\\.", "");
234 try testRemoveDotDirsError(error.TooManyParentDirs, "a\\..\\.\\.\\b\\.\\..\\.\\..");
235
236 try testRemoveDotDirs("a\\b\\..\\", "a\\");
237 try testRemoveDotDirs("a\\b\\..\\c", "a\\c");
238}
239
240const RTL_PATH_TYPE = enum(c_int) {
241 Unknown,
242 UncAbsolute,
243 DriveAbsolute,
244 DriveRelative,
245 Rooted,
246 Relative,
247 LocalDevice,
248 RootLocalDevice,
249};
250
251pub extern "ntdll" fn RtlDetermineDosPathNameType_U(
252 Path: [*:0]const u16,
253) callconv(.winapi) RTL_PATH_TYPE;
254
255test "getWin32PathType vs RtlDetermineDosPathNameType_U" {
256 if (builtin.os.tag != .windows) return error.SkipZigTest;
257
258 var buf: std.ArrayList(u16) = .empty;
259 defer buf.deinit(std.testing.allocator);
260
261 var wtf8_buf: std.ArrayList(u8) = .empty;
262 defer wtf8_buf.deinit(std.testing.allocator);
263
264 var random = std.Random.DefaultPrng.init(std.testing.random_seed);
265 const rand = random.random();
266
267 for (0..1000) |_| {
268 buf.clearRetainingCapacity();
269 const path = try getRandomWtf16Path(std.testing.allocator, &buf, rand);
270 wtf8_buf.clearRetainingCapacity();
271 const wtf8_len = std.unicode.calcWtf8Len(path);
272 try wtf8_buf.ensureTotalCapacity(std.testing.allocator, wtf8_len);
273 wtf8_buf.items.len = wtf8_len;
274 std.debug.assert(std.unicode.wtf16LeToWtf8(wtf8_buf.items, path) == wtf8_len);
275
276 const windows_type = RtlDetermineDosPathNameType_U(path);
277 const wtf16_type = std.fs.path.getWin32PathType(u16, path);
278 const wtf8_type = std.fs.path.getWin32PathType(u8, wtf8_buf.items);
279
280 checkPathType(windows_type, wtf16_type) catch |err| {
281 std.debug.print("expected type {}, got {} for path: {f}\n", .{ windows_type, wtf16_type, std.unicode.fmtUtf16Le(path) });
282 std.debug.print("path bytes:\n", .{});
283 std.debug.dumpHex(std.mem.sliceAsBytes(path));
284 return err;
285 };
286
287 if (wtf16_type != wtf8_type) {
288 std.debug.print("type mismatch between wtf8: {} and wtf16: {} for path: {f}\n", .{ wtf8_type, wtf16_type, std.unicode.fmtUtf16Le(path) });
289 std.debug.print("wtf-16 path bytes:\n", .{});
290 std.debug.dumpHex(std.mem.sliceAsBytes(path));
291 std.debug.print("wtf-8 path bytes:\n", .{});
292 std.debug.dumpHex(std.mem.sliceAsBytes(wtf8_buf.items));
293 return error.Wtf8Wtf16Mismatch;
294 }
295 }
296}
297
298fn checkPathType(windows_type: RTL_PATH_TYPE, zig_type: std.fs.path.Win32PathType) !void {
299 const expected_windows_type: RTL_PATH_TYPE = switch (zig_type) {
300 .unc_absolute => .UncAbsolute,
301 .drive_absolute => .DriveAbsolute,
302 .drive_relative => .DriveRelative,
303 .rooted => .Rooted,
304 .relative => .Relative,
305 .local_device => .LocalDevice,
306 .root_local_device => .RootLocalDevice,
307 };
308 if (windows_type != expected_windows_type) return error.PathTypeMismatch;
309}
310
311fn getRandomWtf16Path(allocator: std.mem.Allocator, buf: *std.ArrayList(u16), rand: std.Random) ![:0]const u16 {
312 const Choice = enum {
313 backslash,
314 slash,
315 control,
316 printable,
317 non_ascii,
318 };
319
320 const choices = rand.uintAtMostBiased(u16, 32);
321
322 for (0..choices) |_| {
323 const choice = rand.enumValue(Choice);
324 const code_unit = switch (choice) {
325 .backslash => '\\',
326 .slash => '/',
327 .control => switch (rand.uintAtMostBiased(u8, 0x20)) {
328 0x20 => '\x7F',
329 else => |b| b + 1, // no NUL
330 },
331 .printable => '!' + rand.uintAtMostBiased(u8, '~' - '!'),
332 .non_ascii => rand.intRangeAtMostBiased(u16, 0x80, 0xFFFF),
333 };
334 try buf.append(allocator, std.mem.nativeToLittle(u16, code_unit));
335 }
336
337 try buf.append(allocator, 0);
338 return buf.items[0 .. buf.items.len - 1 :0];
339}
lib/std/zig/parser_test.zig+1-1
...@@ -639,7 +639,7 @@ test "zig fmt: array types last token" {...@@ -639,7 +639,7 @@ test "zig fmt: array types last token" {
639639
640test "zig fmt: sentinel-terminated array type" {640test "zig fmt: sentinel-terminated array type" {
641 try testCanonical(641 try testCanonical(
642 \\pub fn cStrToPrefixedFileW(s: [*:0]const u8) ![PATH_MAX_WIDE:0]u16 {642 \\pub fn foobar(s: [*:0]const u8) ![PATH_MAX_WIDE:0]u16 {
643 \\ return sliceToPrefixedFileW(mem.toSliceConst(u8, s));643 \\ return sliceToPrefixedFileW(mem.toSliceConst(u8, s));
644 \\}644 \\}
645 \\645 \\
test/standalone/load_dynamic_library/build.zig+1
...@@ -9,6 +9,7 @@ pub fn build(b: *std.Build) void {...@@ -9,6 +9,7 @@ pub fn build(b: *std.Build) void {
9 const target = b.graph.host;9 const target = b.graph.host;
1010
11 if (builtin.os.tag == .wasi) return;11 if (builtin.os.tag == .wasi) return;
12 if (builtin.os.tag == .windows) return;
1213
13 const lib = b.addLibrary(.{14 const lib = b.addLibrary(.{
14 .linkage = .dynamic,15 .linkage = .dynamic,