| author | |
| committer | |
| log | 2a193a39871ad098931cecc1154cc42278c28a2b |
| tree | 2b94a0a50014442dfba75922392d34d840931f37 |
| parent | b49dc5eb70b804a2354a01bd60c686f0f2279f2d |
unfortunately this function calls NtCreateFile so it has to participate
in cancelation8 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; |
| 363 | 363 | ||
| 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; |
| 3259 | 3259 | ||
| 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 value | 3261 | _ = permissions; // TODO use this value |
| 3262 | 3262 | ||
| 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 | }; |
| 3364 | 3364 | ||
| 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( |
| 4064 | 4064 | ||
| 4065 | _ = options; // TODO | 4065 | _ = options; // TODO |
| 4066 | 4066 | ||
| 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(); |
| 4069 | 4069 | ||
| 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; |
| 4287 | 4287 | ||
| 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; |
| 4291 | 4291 | ||
| ... | @@ -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; |
| 5152 | 5152 | ||
| 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 | } |
| 5157 | 5157 | ||
| ... | @@ -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; |
| 5986 | 5986 | ||
| 5987 | var path_name_w = try windows.sliceToPrefixedFileW(dir.handle, sub_path); | 5987 | var path_name_w = try sliceToPrefixedFileW(dir.handle, sub_path); |
| 5988 | 5988 | ||
| 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, |
| 6016 | 6016 | ||
| 6017 | fn realPathWindows(h_file: windows.HANDLE, out_buffer: []u8) File.RealPathError!usize { | 6017 | fn 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 checks | 6019 | const wide_slice = try GetFinalPathNameByHandle(h_file, .{}, &wide_buf); |
| 6020 | try Thread.checkCancel(); | ||
| 6021 | const wide_slice = try windows.GetFinalPathNameByHandle(h_file, .{}, &wide_buf); | ||
| 6022 | 6020 | ||
| 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 | } |
| 6029 | 6027 | ||
| 6028 | /// Specifies how to format volume path in the result of `GetFinalPathNameByHandle`. | ||
| 6029 | /// Defaults to DOS volume names. | ||
| 6030 | pub 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 | |||
| 6039 | pub 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. | ||
| 6053 | pub 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 | |||
| 6242 | test 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 | ||
| 6267 | fn 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 | |||
| 6281 | test 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 | |||
| 6293 | pub const QueryObjectNameError = error{ | ||
| 6294 | AccessDenied, | ||
| 6295 | InvalidHandle, | ||
| 6296 | NameTooLong, | ||
| 6297 | Unexpected, | ||
| 6298 | }; | ||
| 6299 | |||
| 6300 | pub 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 | |||
| 6325 | test 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 | |||
| 6343 | const 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) | ||
| 6359 | pub 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 | |||
| 6519 | const 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`. | ||
| 6536 | fn 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 | |||
| 6552 | pub const Wtf8ToPrefixedFileWError = Wtf16ToPrefixedFileWError; | ||
| 6553 | |||
| 6554 | /// Same as `wToPrefixedFileW` but accepts a WTF-8 encoded path. | ||
| 6555 | /// https://wtf-8.codeberg.page/ | ||
| 6556 | pub 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 | |||
| 6565 | pub 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 | |||
| 6030 | fn dirRealPathFilePosix(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, out_buffer: []u8) Dir.RealPathFileError!usize { | 6574 | fn 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; |
| 6032 | 6576 | ||
| ... | @@ -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; |
| 6480 | 7024 | ||
| 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(); |
| 6483 | 7027 | ||
| 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(); |
| 6766 | 7310 | ||
| 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 paths | 7636 | // Target path does not use sliceToPrefixedFileW because certain paths |
| 7093 | // are handled differently when creating a symlink than they would be | 7637 | // 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 if | 7642 | // 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 | ); |
| 7106 | 7650 | ||
| 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); |
| 7108 | 7652 | ||
| 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-createsymboliclinkw | 7702 | // 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 it | 7706 | // 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 absolute | 7724 | // 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: [] |
| 7322 | fn dirReadLinkWindows(dir: Dir, sub_path: []const u8, buffer: []u8) Dir.ReadLinkError!usize { | 7866 | fn dirReadLinkWindows(dir: Dir, sub_path: []const u8, buffer: []u8) Dir.ReadLinkError!usize { |
| 7323 | // This gets used once for `sub_path` and then reused again temporarily | 7867 | // 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 the | 10338 | // 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 path | 10339 | // 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); |
| 9798 | 10342 | ||
| 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); |
| 9824 | 10368 | ||
| 9825 | // TODO move GetFinalPathNameByHandle logic into Io.Threaded and add cancel checks | 10369 | 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); | ||
| 9828 | 10370 | ||
| 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 |
| 13598 | 14140 | ||
| 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 checks | 14143 | 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 checks | 15869 | 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"); |
| 6 | const Io = std.Io; | 6 | const Io = std.Io; |
| 7 | const testing = std.testing; | 7 | const testing = std.testing; |
| 8 | const assert = std.debug.assert; | 8 | const assert = std.debug.assert; |
| 9 | const windows = std.os.windows; | ||
| 9 | 10 | ||
| 10 | test "concurrent vs main prevents deadlock via oversubscription" { | 11 | test "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. | ||
| 286 | fn 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). | ||
| 303 | fn 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. | ||
| 315 | fn 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. | ||
| 321 | fn 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 | |||
| 331 | test "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 | |||
| 456 | fn 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 | } | ||
| 462 | fn 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 | } | ||
| 467 | test "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 | |||
| 515 | const RTL_PATH_TYPE = enum(c_int) { | ||
| 516 | Unknown, | ||
| 517 | UncAbsolute, | ||
| 518 | DriveAbsolute, | ||
| 519 | DriveRelative, | ||
| 520 | Rooted, | ||
| 521 | Relative, | ||
| 522 | LocalDevice, | ||
| 523 | RootLocalDevice, | ||
| 524 | }; | ||
| 525 | |||
| 526 | pub extern "ntdll" fn RtlDetermineDosPathNameType_U( | ||
| 527 | Path: [*:0]const u16, | ||
| 528 | ) callconv(.winapi) RTL_PATH_TYPE; | ||
| 529 | |||
| 530 | test "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 | |||
| 573 | fn 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 | |||
| 586 | fn 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 | ElfDynLib | 17 | ElfDynLib |
| 18 | else | 18 | 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 { |
| 27 | 26 | ||
| 28 | inner: InnerType, | 27 | inner: InnerType, |
| 29 | 28 | ||
| 30 | pub const Error = ElfDynLibError || DlDynLibError || WindowsDynLibError; | 29 | pub const Error = ElfDynLibError || DlDynLibError; |
| 31 | 30 | ||
| 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 | } |
| 560 | 559 | ||
| 561 | /// Separated to avoid referencing `WindowsDynLib`, because its field types may not | ||
| 562 | /// be valid on other targets. | ||
| 563 | const WindowsDynLibError = error{ | ||
| 564 | FileNotFound, | ||
| 565 | InvalidPath, | ||
| 566 | } || windows.LoadLibraryError; | ||
| 567 | |||
| 568 | pub 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 not | 560 | /// Separated to avoid referencing `DlDynLib`, because its field types may not |
| 629 | /// be valid on other targets. | 561 | /// be valid on other targets. |
| 630 | const DlDynLibError = error{ FileNotFound, NameTooLong }; | 562 | const DlDynLibError = error{ FileNotFound, NameTooLong }; |
| ... | @@ -676,7 +608,6 @@ pub const DlDynLib = struct { | ... | @@ -676,7 +608,6 @@ pub const DlDynLib = struct { |
| 676 | test "dynamic_library" { | 608 | test "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; |
| 15 | const maxInt = std.math.maxInt; | 15 | const maxInt = std.math.maxInt; |
| 16 | const UnexpectedError = std.posix.UnexpectedError; | 16 | const UnexpectedError = std.posix.UnexpectedError; |
| 17 | 17 | ||
| 18 | test { | ||
| 19 | if (builtin.os.tag == .windows) { | ||
| 20 | _ = @import("windows/test.zig"); | ||
| 21 | } | ||
| 22 | } | ||
| 23 | |||
| 24 | pub const advapi32 = @import("windows/advapi32.zig"); | 18 | pub const advapi32 = @import("windows/advapi32.zig"); |
| 25 | pub const kernel32 = @import("windows/kernel32.zig"); | 19 | pub const kernel32 = @import("windows/kernel32.zig"); |
| 26 | pub const ntdll = @import("windows/ntdll.zig"); | 20 | pub 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 | } |
| 2672 | 2666 | ||
| 2673 | pub const QueryObjectNameError = error{ | ||
| 2674 | AccessDenied, | ||
| 2675 | InvalidHandle, | ||
| 2676 | NameTooLong, | ||
| 2677 | Unexpected, | ||
| 2678 | }; | ||
| 2679 | |||
| 2680 | pub 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 | |||
| 2705 | test 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 | |||
| 2723 | pub 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. | ||
| 2735 | pub 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 | ||
| 2751 | pub 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 | ||
| 2941 | fn 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 | |||
| 2955 | test 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 | |||
| 2967 | test 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 | |||
| 2991 | pub fn getpeername(s: ws2_32.SOCKET, name: *ws2_32.sockaddr, namelen: *ws2_32.socklen_t) i32 { | 2667 | pub 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 | } |
| 3421 | 3097 | ||
| 3422 | pub 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` |
| 3432 | pub const RemoveDotDirsError = error{TooManyParentDirs}; | 3099 | pub const RemoveDotDirsError = error{TooManyParentDirs}; |
| 3433 | 3100 | ||
| ... | @@ -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 | } |
| 3505 | 3172 | ||
| 3506 | pub 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/ | ||
| 3511 | pub 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/ | ||
| 3517 | pub 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 | |||
| 3526 | pub 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) | ||
| 3545 | pub 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 path | 3173 | /// 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, as | 3174 | /// 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 virtual | 3175 | /// `\??\` 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 | } |
| 3727 | 3195 | ||
| 3728 | const 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`. | ||
| 3745 | fn 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\Path | 3198 | /// \??\C:\Some\Path -> C:\Some\Path |
lib/std/os/windows/test.zig deleted-339| ... | @@ -1,339 +0,0 @@ | ||
| 1 | const std = @import("../../std.zig"); | ||
| 2 | const builtin = @import("builtin"); | ||
| 3 | const windows = std.os.windows; | ||
| 4 | const mem = std.mem; | ||
| 5 | const 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. | ||
| 11 | fn 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). | ||
| 28 | fn 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. | ||
| 40 | fn 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. | ||
| 46 | fn 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 | |||
| 56 | test "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 | |||
| 181 | fn 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 | } | ||
| 187 | fn 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 | } | ||
| 192 | test "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 | |||
| 240 | const RTL_PATH_TYPE = enum(c_int) { | ||
| 241 | Unknown, | ||
| 242 | UncAbsolute, | ||
| 243 | DriveAbsolute, | ||
| 244 | DriveRelative, | ||
| 245 | Rooted, | ||
| 246 | Relative, | ||
| 247 | LocalDevice, | ||
| 248 | RootLocalDevice, | ||
| 249 | }; | ||
| 250 | |||
| 251 | pub extern "ntdll" fn RtlDetermineDosPathNameType_U( | ||
| 252 | Path: [*:0]const u16, | ||
| 253 | ) callconv(.winapi) RTL_PATH_TYPE; | ||
| 254 | |||
| 255 | test "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 | |||
| 298 | fn 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 | |||
| 311 | fn 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" { |
| 639 | 639 | ||
| 640 | test "zig fmt: sentinel-terminated array type" { | 640 | test "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; |
| 10 | 10 | ||
| 11 | if (builtin.os.tag == .wasi) return; | 11 | if (builtin.os.tag == .wasi) return; |
| 12 | if (builtin.os.tag == .windows) return; | ||
| 12 | 13 | ||
| 13 | const lib = b.addLibrary(.{ | 14 | const lib = b.addLibrary(.{ |
| 14 | .linkage = .dynamic, | 15 | .linkage = .dynamic, |