diff --git a/lib/std/Build/Watch.zig b/lib/std/Build/Watch.zig index 5920a227cb..15ccfcfdf9 100644 --- a/lib/std/Build/Watch.zig +++ b/lib/std/Build/Watch.zig @@ -358,7 +358,7 @@ const Os = switch (builtin.os.tag) { var dir_handle: windows.HANDLE = undefined; const root_fd = path.root_dir.handle.handle; const sub_path = path.subPathOrDot(); - const sub_path_w = try windows.sliceToPrefixedFileW(root_fd, sub_path); + const sub_path_w = try std.Io.Threaded.sliceToPrefixedFileW(root_fd, sub_path); // TODO eliminate this call const path_len_bytes = std.math.cast(u16, sub_path_w.len * 2) orelse return error.NameTooLong; var nt_name = windows.UNICODE_STRING{ diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index fdef4ef47e..1c171d7321 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -3257,7 +3257,7 @@ fn dirCreateDirWindows(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, pe const t: *Threaded = @ptrCast(@alignCast(userdata)); _ = t; - const sub_path_w = try windows.sliceToPrefixedFileW(dir.handle, sub_path); + const sub_path_w = try sliceToPrefixedFileW(dir.handle, sub_path); _ = permissions; // TODO use this value const syscall: Syscall = try .start(); @@ -3363,7 +3363,7 @@ fn dirCreateDirPathOpenWindows( }; components: while (true) { - const sub_path_w_array = try w.sliceToPrefixedFileW(dir.handle, component.path); + const sub_path_w_array = try sliceToPrefixedFileW(dir.handle, component.path); const sub_path_w = sub_path_w_array.span(); const is_last = it.peekNext() == null; const create_disposition: w.FILE.CREATE_DISPOSITION = if (is_last) .OPEN_IF else .CREATE; @@ -4064,7 +4064,7 @@ fn dirAccessWindows( _ = options; // TODO - const sub_path_w_array = try windows.sliceToPrefixedFileW(dir.handle, sub_path); + const sub_path_w_array = try sliceToPrefixedFileW(dir.handle, sub_path); const sub_path_w = sub_path_w_array.span(); if (sub_path_w[0] == '.' and sub_path_w[1] == 0) return; @@ -4285,7 +4285,7 @@ fn dirCreateFileWindows( if (std.mem.eql(u8, sub_path, ".")) return error.IsDir; if (std.mem.eql(u8, sub_path, "..")) return error.IsDir; - const sub_path_w_array = try windows.sliceToPrefixedFileW(dir.handle, sub_path); + const sub_path_w_array = try sliceToPrefixedFileW(dir.handle, sub_path); const sub_path_w = sub_path_w_array.span(); const path_len_bytes = std.math.cast(u16, sub_path_w.len * 2) orelse return error.NameTooLong; @@ -4887,7 +4887,7 @@ fn dirOpenFileWindows( ) File.OpenError!File { const t: *Threaded = @ptrCast(@alignCast(userdata)); _ = t; - const sub_path_w_array = try windows.sliceToPrefixedFileW(dir.handle, sub_path); + const sub_path_w_array = try sliceToPrefixedFileW(dir.handle, sub_path); const sub_path_w = sub_path_w_array.span(); const dir_handle = if (Dir.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle; return dirOpenFileWtf16(dir_handle, sub_path_w, flags); @@ -5151,7 +5151,7 @@ fn dirOpenDirPosix( _ = t; if (is_windows) { - const sub_path_w = try windows.sliceToPrefixedFileW(dir.handle, sub_path); + const sub_path_w = try sliceToPrefixedFileW(dir.handle, sub_path); return dirOpenDirWindows(dir, sub_path_w.span(), options); } @@ -5984,7 +5984,7 @@ fn dirRealPathFileWindows(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, const t: *Threaded = @ptrCast(@alignCast(userdata)); _ = t; - var path_name_w = try windows.sliceToPrefixedFileW(dir.handle, sub_path); + var path_name_w = try sliceToPrefixedFileW(dir.handle, sub_path); const h_file = handle: { const syscall: Syscall = try .start(); @@ -6016,9 +6016,7 @@ fn dirRealPathFileWindows(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, fn realPathWindows(h_file: windows.HANDLE, out_buffer: []u8) File.RealPathError!usize { var wide_buf: [windows.PATH_MAX_WIDE]u16 = undefined; - // TODO move GetFinalPathNameByHandle logic into Io.Threaded and add cancel checks - try Thread.checkCancel(); - const wide_slice = try windows.GetFinalPathNameByHandle(h_file, .{}, &wide_buf); + const wide_slice = try GetFinalPathNameByHandle(h_file, .{}, &wide_buf); const len = std.unicode.calcWtf8Len(wide_slice); if (len > out_buffer.len) @@ -6027,6 +6025,552 @@ fn realPathWindows(h_file: windows.HANDLE, out_buffer: []u8) File.RealPathError! return std.unicode.wtf16LeToWtf8(out_buffer, wide_slice); } +/// Specifies how to format volume path in the result of `GetFinalPathNameByHandle`. +/// Defaults to DOS volume names. +pub const GetFinalPathNameByHandleFormat = struct { + volume_name: enum { + /// Format as DOS volume name + Dos, + /// Format as NT volume name + Nt, + } = .Dos, +}; + +pub const GetFinalPathNameByHandleError = error{ + AccessDenied, + FileNotFound, + NameTooLong, + /// The volume does not contain a recognized file system. File system + /// drivers might not be loaded, or the volume may be corrupt. + UnrecognizedVolume, +} || Io.Cancelable || Io.UnexpectedError; + +/// Returns canonical (normalized) path of handle. +/// Use `GetFinalPathNameByHandleFormat` to specify whether the path is meant to include +/// NT or DOS volume name (e.g., `\Device\HarddiskVolume0\foo.txt` versus `C:\foo.txt`). +/// If DOS volume name format is selected, note that this function does *not* prepend +/// `\\?\` prefix to the resultant path. +pub fn GetFinalPathNameByHandle( + hFile: windows.HANDLE, + fmt: GetFinalPathNameByHandleFormat, + out_buffer: []u16, +) GetFinalPathNameByHandleError![]u16 { + const final_path = QueryObjectName(hFile, out_buffer) catch |err| switch (err) { + // we assume InvalidHandle is close enough to FileNotFound in semantics + // to not further complicate the error set + error.InvalidHandle => return error.FileNotFound, + else => |e| return e, + }; + + switch (fmt.volume_name) { + .Nt => { + // the returned path is already in .Nt format + return final_path; + }, + .Dos => { + // parse the string to separate volume path from file path + const device_prefix = std.unicode.utf8ToUtf16LeStringLiteral("\\Device\\"); + + // We aren't entirely sure of the structure of the path returned by + // QueryObjectName in all contexts/environments. + // This code is written to cover the various cases that have + // been encountered and solved appropriately. But note that there's + // no easy way to verify that they have all been tackled! + // (Unless you, the reader knows of one then please do action that!) + if (!std.mem.startsWith(u16, final_path, device_prefix)) { + // Wine seems to return NT namespaced paths starting with \??\ from QueryObjectName + // (e.g. `\??\Z:\some\path\to\a\file.txt`), in which case we can just strip the + // prefix to turn it into an absolute path. + // https://github.com/ziglang/zig/issues/26029 + // https://bugs.winehq.org/show_bug.cgi?id=39569 + return windows.ntToWin32Namespace(final_path, out_buffer) catch |err| switch (err) { + error.NotNtPath => return error.Unexpected, + error.NameTooLong => |e| return e, + }; + } + + const file_path_begin_index = std.mem.findPos(u16, final_path, device_prefix.len, &[_]u16{'\\'}) orelse unreachable; + const volume_name_u16 = final_path[0..file_path_begin_index]; + const device_name_u16 = volume_name_u16[device_prefix.len..]; + const file_name_u16 = final_path[file_path_begin_index..]; + + // MUP is Multiple UNC Provider, and indicates that the path is a UNC + // path. In this case, the canonical UNC path can be gotten by just + // dropping the \Device\Mup\ and making sure the path begins with \\ + if (std.mem.eql(u16, device_name_u16, std.unicode.utf8ToUtf16LeStringLiteral("Mup"))) { + out_buffer[0] = '\\'; + @memmove(out_buffer[1..][0..file_name_u16.len], file_name_u16); + return out_buffer[0 .. 1 + file_name_u16.len]; + } + + // Get DOS volume name. DOS volume names are actually symbolic link objects to the + // actual NT volume. For example: + // (NT) \Device\HarddiskVolume4 => (DOS) \DosDevices\C: == (DOS) C: + const MIN_SIZE = @sizeOf(windows.MOUNTMGR_MOUNT_POINT) + windows.MAX_PATH; + // We initialize the input buffer to all zeros for convenience since + // `DeviceIoControl` with `IOCTL_MOUNTMGR_QUERY_POINTS` expects this. + var input_buf: [MIN_SIZE]u8 align(@alignOf(windows.MOUNTMGR_MOUNT_POINT)) = [_]u8{0} ** MIN_SIZE; + var output_buf: [MIN_SIZE * 4]u8 align(@alignOf(windows.MOUNTMGR_MOUNT_POINTS)) = undefined; + + // This surprising path is a filesystem path to the mount manager on Windows. + // Source: https://stackoverflow.com/questions/3012828/using-ioctl-mountmgr-query-points + // This is the NT namespaced version of \\.\MountPointManager + const mgmt_path_u16 = std.unicode.utf8ToUtf16LeStringLiteral("\\??\\MountPointManager"); + const mgmt_handle = windows.OpenFile(mgmt_path_u16, .{ + .access_mask = .{ .STANDARD = .{ .SYNCHRONIZE = true } }, + .creation = .OPEN, + }) catch |err| switch (err) { + error.IsDir => return error.Unexpected, + error.NotDir => return error.Unexpected, + error.NoDevice => return error.Unexpected, + error.AccessDenied => return error.Unexpected, + error.PipeBusy => return error.Unexpected, + error.PathAlreadyExists => return error.Unexpected, + error.WouldBlock => return error.Unexpected, + error.NetworkNotFound => return error.Unexpected, + error.AntivirusInterference => return error.Unexpected, + error.BadPathName => return error.Unexpected, + error.OperationCanceled => @panic("TODO: better integrate cancelation"), + else => |e| return e, + }; + defer windows.CloseHandle(mgmt_handle); + + var input_struct: *windows.MOUNTMGR_MOUNT_POINT = @ptrCast(&input_buf[0]); + input_struct.DeviceNameOffset = @sizeOf(windows.MOUNTMGR_MOUNT_POINT); + input_struct.DeviceNameLength = @intCast(volume_name_u16.len * 2); + @memcpy(input_buf[@sizeOf(windows.MOUNTMGR_MOUNT_POINT)..][0 .. volume_name_u16.len * 2], @as([*]const u8, @ptrCast(volume_name_u16.ptr))); + + { + const rc = windows.DeviceIoControl(mgmt_handle, windows.IOCTL.MOUNTMGR.QUERY_POINTS, .{ .in = &input_buf, .out = &output_buf }); + switch (rc) { + .SUCCESS => {}, + .OBJECT_NAME_NOT_FOUND => return error.FileNotFound, + else => return windows.unexpectedStatus(rc), + } + } + const mount_points_struct: *const windows.MOUNTMGR_MOUNT_POINTS = @ptrCast(&output_buf[0]); + + const mount_points = @as( + [*]const windows.MOUNTMGR_MOUNT_POINT, + @ptrCast(&mount_points_struct.MountPoints[0]), + )[0..mount_points_struct.NumberOfMountPoints]; + + for (mount_points) |mount_point| { + const symlink = @as( + [*]const u16, + @ptrCast(@alignCast(&output_buf[mount_point.SymbolicLinkNameOffset])), + )[0 .. mount_point.SymbolicLinkNameLength / 2]; + + // Look for `\DosDevices\` prefix. We don't really care if there are more than one symlinks + // with traditional DOS drive letters, so pick the first one available. + var prefix_buf = std.unicode.utf8ToUtf16LeStringLiteral("\\DosDevices\\"); + const prefix = prefix_buf[0..prefix_buf.len]; + + if (std.mem.startsWith(u16, symlink, prefix)) { + const drive_letter = symlink[prefix.len..]; + + if (out_buffer.len < drive_letter.len + file_name_u16.len) return error.NameTooLong; + + @memcpy(out_buffer[0..drive_letter.len], drive_letter); + @memmove(out_buffer[drive_letter.len..][0..file_name_u16.len], file_name_u16); + const total_len = drive_letter.len + file_name_u16.len; + + // Validate that DOS does not contain any spurious nul bytes. + assert(std.mem.findScalar(u16, out_buffer[0..total_len], 0) == null); + + return out_buffer[0..total_len]; + } else if (mountmgrIsVolumeName(symlink)) { + // If the symlink is a volume GUID like \??\Volume{383da0b0-717f-41b6-8c36-00500992b58d}, + // then it is a volume mounted as a path rather than a drive letter. We need to + // query the mount manager again to get the DOS path for the volume. + + // 49 is the maximum length accepted by mountmgrIsVolumeName + const vol_input_size = @sizeOf(windows.MOUNTMGR_TARGET_NAME) + (49 * 2); + var vol_input_buf: [vol_input_size]u8 align(@alignOf(windows.MOUNTMGR_TARGET_NAME)) = [_]u8{0} ** vol_input_size; + // Note: If the path exceeds MAX_PATH, the Disk Management GUI doesn't accept the full path, + // and instead if must be specified using a shortened form (e.g. C:\FOO~1\BAR~1\<...>). + // However, just to be sure we can handle any path length, we use PATH_MAX_WIDE here. + const min_output_size = @sizeOf(windows.MOUNTMGR_VOLUME_PATHS) + (windows.PATH_MAX_WIDE * 2); + var vol_output_buf: [min_output_size]u8 align(@alignOf(windows.MOUNTMGR_VOLUME_PATHS)) = undefined; + + var vol_input_struct: *windows.MOUNTMGR_TARGET_NAME = @ptrCast(&vol_input_buf[0]); + vol_input_struct.DeviceNameLength = @intCast(symlink.len * 2); + @memcpy(@as([*]windows.WCHAR, &vol_input_struct.DeviceName)[0..symlink.len], symlink); + + const rc = windows.DeviceIoControl(mgmt_handle, windows.IOCTL.MOUNTMGR.QUERY_DOS_VOLUME_PATH, .{ .in = &vol_input_buf, .out = &vol_output_buf }); + switch (rc) { + .SUCCESS => {}, + .UNRECOGNIZED_VOLUME => return error.UnrecognizedVolume, + else => return windows.unexpectedStatus(rc), + } + const volume_paths_struct: *const windows.MOUNTMGR_VOLUME_PATHS = @ptrCast(&vol_output_buf[0]); + const volume_path = std.mem.sliceTo(@as( + [*]const u16, + &volume_paths_struct.MultiSz, + )[0 .. volume_paths_struct.MultiSzLength / 2], 0); + + if (out_buffer.len < volume_path.len + file_name_u16.len) return error.NameTooLong; + + // `out_buffer` currently contains the memory of `file_name_u16`, so it can overlap with where + // we want to place the filename before returning. Here are the possible overlapping cases: + // + // out_buffer: [filename] + // dest: [___(a)___] [___(b)___] + // + // In the case of (a), we need to copy forwards, and in the case of (b) we need + // to copy backwards. We also need to do this before copying the volume path because + // it could overwrite the file_name_u16 memory. + const file_name_dest = out_buffer[volume_path.len..][0..file_name_u16.len]; + @memmove(file_name_dest, file_name_u16); + @memcpy(out_buffer[0..volume_path.len], volume_path); + const total_len = volume_path.len + file_name_u16.len; + + // Validate that DOS does not contain any spurious nul bytes. + assert(std.mem.findScalar(u16, out_buffer[0..total_len], 0) == null); + + return out_buffer[0..total_len]; + } + } + + // If we've ended up here, then something went wrong/is corrupted in the OS, + // so error out! + return error.FileNotFound; + }, + } +} + +test GetFinalPathNameByHandle { + if (builtin.os.tag != .windows) + return; + + //any file will do + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + const handle = tmp.dir.handle; + var buffer: [windows.PATH_MAX_WIDE]u16 = undefined; + + //check with sufficient size + const nt_path = try GetFinalPathNameByHandle(handle, .{ .volume_name = .Nt }, &buffer); + _ = try GetFinalPathNameByHandle(handle, .{ .volume_name = .Dos }, &buffer); + + const required_len_in_u16 = nt_path.len + @divExact(@intFromPtr(nt_path.ptr) - @intFromPtr(&buffer), 2) + 1; + //check with insufficient size + try std.testing.expectError(error.NameTooLong, GetFinalPathNameByHandle(handle, .{ .volume_name = .Nt }, buffer[0 .. required_len_in_u16 - 1])); + try std.testing.expectError(error.NameTooLong, GetFinalPathNameByHandle(handle, .{ .volume_name = .Dos }, buffer[0 .. required_len_in_u16 - 1])); + + //check with exactly-sufficient size + _ = try GetFinalPathNameByHandle(handle, .{ .volume_name = .Nt }, buffer[0..required_len_in_u16]); + _ = try GetFinalPathNameByHandle(handle, .{ .volume_name = .Dos }, buffer[0..required_len_in_u16]); +} + +/// Equivalent to the MOUNTMGR_IS_VOLUME_NAME macro in mountmgr.h +fn mountmgrIsVolumeName(name: []const u16) bool { + return (name.len == 48 or (name.len == 49 and name[48] == std.mem.nativeToLittle(u16, '\\'))) and + name[0] == std.mem.nativeToLittle(u16, '\\') and + (name[1] == std.mem.nativeToLittle(u16, '?') or name[1] == std.mem.nativeToLittle(u16, '\\')) and + name[2] == std.mem.nativeToLittle(u16, '?') and + name[3] == std.mem.nativeToLittle(u16, '\\') and + std.mem.startsWith(u16, name[4..], std.unicode.utf8ToUtf16LeStringLiteral("Volume{")) and + name[19] == std.mem.nativeToLittle(u16, '-') and + name[24] == std.mem.nativeToLittle(u16, '-') and + name[29] == std.mem.nativeToLittle(u16, '-') and + name[34] == std.mem.nativeToLittle(u16, '-') and + name[47] == std.mem.nativeToLittle(u16, '}'); +} + +test mountmgrIsVolumeName { + @setEvalBranchQuota(2000); + const L = std.unicode.utf8ToUtf16LeStringLiteral; + try std.testing.expect(mountmgrIsVolumeName(L("\\\\?\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}"))); + try std.testing.expect(mountmgrIsVolumeName(L("\\??\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}"))); + try std.testing.expect(mountmgrIsVolumeName(L("\\\\?\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}\\"))); + try std.testing.expect(mountmgrIsVolumeName(L("\\??\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}\\"))); + try std.testing.expect(!mountmgrIsVolumeName(L("\\\\.\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}"))); + try std.testing.expect(!mountmgrIsVolumeName(L("\\??\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}\\foo"))); + try std.testing.expect(!mountmgrIsVolumeName(L("\\??\\Volume{383da0b0-717f-41b6-8c36-00500992b58}"))); +} + +pub const QueryObjectNameError = error{ + AccessDenied, + InvalidHandle, + NameTooLong, + Unexpected, +}; + +pub fn QueryObjectName(handle: windows.HANDLE, out_buffer: []u16) QueryObjectNameError![]u16 { + const out_buffer_aligned = std.mem.alignInSlice(out_buffer, @alignOf(windows.OBJECT_NAME_INFORMATION)) orelse return error.NameTooLong; + + const info: *windows.OBJECT_NAME_INFORMATION = @ptrCast(out_buffer_aligned); + // buffer size is specified in bytes + const out_buffer_len = std.math.cast(windows.ULONG, out_buffer_aligned.len * 2) orelse std.math.maxInt(windows.ULONG); + // last argument would return the length required for full_buffer, not exposed here + return switch (windows.ntdll.NtQueryObject(handle, .ObjectNameInformation, info, out_buffer_len, null)) { + .SUCCESS => blk: { + // info.Name.Buffer from ObQueryNameString is documented to be null (and MaximumLength == 0) + // if the object was "unnamed", not sure if this can happen for file handles + if (info.Name.MaximumLength == 0) break :blk error.Unexpected; + // resulting string length is specified in bytes + const path_length_unterminated = @divExact(info.Name.Length, 2); + break :blk info.Name.Buffer.?[0..path_length_unterminated]; + }, + .ACCESS_DENIED => error.AccessDenied, + .INVALID_HANDLE => error.InvalidHandle, + // triggered when the buffer is too small for the OBJECT_NAME_INFORMATION object (.INFO_LENGTH_MISMATCH), + // or if the buffer is too small for the file path returned (.BUFFER_OVERFLOW, .BUFFER_TOO_SMALL) + .INFO_LENGTH_MISMATCH, .BUFFER_OVERFLOW, .BUFFER_TOO_SMALL => error.NameTooLong, + else => |e| windows.unexpectedStatus(e), + }; +} + +test QueryObjectName { + if (builtin.os.tag != .windows) + return; + + //any file will do; canonicalization works on NTFS junctions and symlinks, hardlinks remain separate paths. + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + const handle = tmp.dir.handle; + var out_buffer: [windows.PATH_MAX_WIDE]u16 = undefined; + + const result_path = try QueryObjectName(handle, &out_buffer); + const required_len_in_u16 = result_path.len + @divExact(@intFromPtr(result_path.ptr) - @intFromPtr(&out_buffer), 2) + 1; + //insufficient size + try std.testing.expectError(error.NameTooLong, QueryObjectName(handle, out_buffer[0 .. required_len_in_u16 - 1])); + //exactly-sufficient size + _ = try QueryObjectName(handle, out_buffer[0..required_len_in_u16]); +} + +const Wtf16ToPrefixedFileWError = error{ + AccessDenied, + FileNotFound, +} || Dir.PathNameError || Io.Cancelable || Io.UnexpectedError; + +/// Converts the `path` to WTF16, null-terminated. If the path contains any +/// namespace prefix, or is anything but a relative path (rooted, drive relative, +/// etc) the result will have the NT-style prefix `\??\`. +/// +/// Similar to RtlDosPathNameToNtPathName_U with a few differences: +/// - Does not allocate on the heap. +/// - Relative paths are kept as relative unless they contain too many .. +/// components, in which case they are resolved against the `dir` if it +/// is non-null, or the CWD if it is null. +/// - Special case device names like COM1, NUL, etc are not handled specially (TODO) +/// - . and space are not stripped from the end of relative paths (potential TODO) +pub fn wToPrefixedFileW(dir: ?windows.HANDLE, path: [:0]const u16) Wtf16ToPrefixedFileWError!WindowsPathSpace { + const nt_prefix = [_]u16{ '\\', '?', '?', '\\' }; + if (windows.hasCommonNtPrefix(u16, path)) { + // TODO: Figure out a way to design an API that can avoid the copy for NT, + // since it is always returned fully unmodified. + var path_space: WindowsPathSpace = undefined; + path_space.data[0..nt_prefix.len].* = nt_prefix; + const len_after_prefix = path.len - nt_prefix.len; + @memcpy(path_space.data[nt_prefix.len..][0..len_after_prefix], path[nt_prefix.len..]); + path_space.len = path.len; + path_space.data[path_space.len] = 0; + return path_space; + } else { + const path_type = Dir.path.getWin32PathType(u16, path); + var path_space: WindowsPathSpace = undefined; + if (path_type == .local_device) { + switch (getLocalDevicePathType(u16, path)) { + .verbatim => { + path_space.data[0..nt_prefix.len].* = nt_prefix; + const len_after_prefix = path.len - nt_prefix.len; + @memcpy(path_space.data[nt_prefix.len..][0..len_after_prefix], path[nt_prefix.len..]); + path_space.len = path.len; + path_space.data[path_space.len] = 0; + return path_space; + }, + .local_device, .fake_verbatim => { + const path_byte_len = windows.ntdll.RtlGetFullPathName_U( + path.ptr, + path_space.data.len * 2, + &path_space.data, + null, + ); + if (path_byte_len == 0) { + // TODO: This may not be the right error + return error.BadPathName; + } else if (path_byte_len / 2 > path_space.data.len) { + return error.NameTooLong; + } + path_space.len = path_byte_len / 2; + // Both prefixes will be normalized but retained, so all + // we need to do now is replace them with the NT prefix + path_space.data[0..nt_prefix.len].* = nt_prefix; + return path_space; + }, + } + } + relative: { + if (path_type == .relative) { + // TODO: Handle special case device names like COM1, AUX, NUL, CONIN$, CONOUT$, etc. + // See https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html + + // TODO: Potentially strip all trailing . and space characters from the + // end of the path. This is something that both RtlDosPathNameToNtPathName_U + // and RtlGetFullPathName_U do. Technically, trailing . and spaces + // are allowed, but such paths may not interact well with Windows (i.e. + // files with these paths can't be deleted from explorer.exe, etc). + // This could be something that normalizePath may want to do. + + @memcpy(path_space.data[0..path.len], path); + // Try to normalize, but if we get too many parent directories, + // then we need to start over and use RtlGetFullPathName_U instead. + path_space.len = windows.normalizePath(u16, path_space.data[0..path.len]) catch |err| switch (err) { + error.TooManyParentDirs => break :relative, + }; + path_space.data[path_space.len] = 0; + return path_space; + } + } + // We now know we are going to return an absolute NT path, so + // we can unconditionally prefix it with the NT prefix. + path_space.data[0..nt_prefix.len].* = nt_prefix; + if (path_type == .root_local_device) { + // `\\.` and `\\?` always get converted to `\??\` exactly, so + // we can just stop here + path_space.len = nt_prefix.len; + path_space.data[path_space.len] = 0; + return path_space; + } + const path_buf_offset = switch (path_type) { + // UNC paths will always start with `\\`. However, we want to + // end up with something like `\??\UNC\server\share`, so to get + // RtlGetFullPathName to write into the spot we want the `server` + // part to end up, we need to provide an offset such that + // the `\\` part gets written where the `C\` of `UNC\` will be + // in the final NT path. + .unc_absolute => nt_prefix.len + 2, + else => nt_prefix.len, + }; + const buf_len: u32 = @intCast(path_space.data.len - path_buf_offset); + const path_to_get: [:0]const u16 = path_to_get: { + // If dir is null, then we don't need to bother with GetFinalPathNameByHandle because + // RtlGetFullPathName_U will resolve relative paths against the CWD for us. + if (path_type != .relative or dir == null) { + break :path_to_get path; + } + // We can also skip GetFinalPathNameByHandle if the handle matches + // the handle returned by Io.Dir.cwd() + if (dir.? == Io.Dir.cwd().handle) { + break :path_to_get path; + } + // At this point, we know we have a relative path that had too many + // `..` components to be resolved by normalizePath, so we need to + // convert it into an absolute path and let RtlGetFullPathName_U + // canonicalize it. We do this by getting the path of the `dir` + // and appending the relative path to it. + var dir_path_buf: [windows.PATH_MAX_WIDE:0]u16 = undefined; + const dir_path = GetFinalPathNameByHandle(dir.?, .{}, &dir_path_buf) catch |err| switch (err) { + // This mapping is not correct; it is actually expected + // that calling GetFinalPathNameByHandle might return + // error.UnrecognizedVolume, and in fact has been observed + // in the wild. The problem is that wToPrefixedFileW was + // never intended to make *any* OS syscall APIs. It's only + // supposed to convert a string to one that is eligible to + // be used in the ntdll syscalls. + // + // To solve this, this function needs to no longer call + // GetFinalPathNameByHandle under any conditions, or the + // calling function needs to get reworked to not need to + // call this function. + // + // This may involve making breaking API changes. + error.UnrecognizedVolume => return error.Unexpected, + else => |e| return e, + }; + if (dir_path.len + 1 + path.len > windows.PATH_MAX_WIDE) { + return error.NameTooLong; + } + // We don't have to worry about potentially doubling up path separators + // here since RtlGetFullPathName_U will handle canonicalizing it. + dir_path_buf[dir_path.len] = '\\'; + @memcpy(dir_path_buf[dir_path.len + 1 ..][0..path.len], path); + const full_len = dir_path.len + 1 + path.len; + dir_path_buf[full_len] = 0; + break :path_to_get dir_path_buf[0..full_len :0]; + }; + const path_byte_len = windows.ntdll.RtlGetFullPathName_U( + path_to_get.ptr, + buf_len * 2, + path_space.data[path_buf_offset..].ptr, + null, + ); + if (path_byte_len == 0) { + // TODO: This may not be the right error + return error.BadPathName; + } else if (path_byte_len / 2 > buf_len) { + return error.NameTooLong; + } + path_space.len = path_buf_offset + (path_byte_len / 2); + if (path_type == .unc_absolute) { + // Now add in the UNC, the `C` should overwrite the first `\` of the + // FullPathName, ultimately resulting in `\??\UNC\` + assert(path_space.data[path_buf_offset] == '\\'); + assert(path_space.data[path_buf_offset + 1] == '\\'); + const unc = [_]u16{ 'U', 'N', 'C' }; + path_space.data[nt_prefix.len..][0..unc.len].* = unc; + } + return path_space; + } +} + +const LocalDevicePathType = enum { + /// `\\.\` (path separators can be `\` or `/`) + local_device, + /// `\\?\` + /// When converted to an NT path, everything past the prefix is left + /// untouched and `\\?\` is replaced by `\??\`. + verbatim, + /// `\\?\` without all path separators being `\`. + /// This seems to be recognized as a prefix, but the 'verbatim' aspect + /// is not respected (i.e. if `//?/C:/foo` is converted to an NT path, + /// it will become `\??\C:\foo` [it will be canonicalized and the //?/ won't + /// be treated as part of the final path]) + fake_verbatim, +}; + +/// Only relevant for Win32 -> NT path conversion. +/// Asserts `path` is of type `Dir.path.Win32PathType.local_device`. +fn getLocalDevicePathType(comptime T: type, path: []const T) LocalDevicePathType { + if (std.debug.runtime_safety) { + assert(Dir.path.getWin32PathType(T, path) == .local_device); + } + + const backslash = std.mem.nativeToLittle(T, '\\'); + const all_backslash = path[0] == backslash and + path[1] == backslash and + path[3] == backslash; + return switch (path[2]) { + std.mem.nativeToLittle(T, '?') => if (all_backslash) .verbatim else .fake_verbatim, + std.mem.nativeToLittle(T, '.') => .local_device, + else => unreachable, + }; +} + +pub const Wtf8ToPrefixedFileWError = Wtf16ToPrefixedFileWError; + +/// Same as `wToPrefixedFileW` but accepts a WTF-8 encoded path. +/// https://wtf-8.codeberg.page/ +pub fn sliceToPrefixedFileW(dir: ?windows.HANDLE, path: []const u8) Wtf8ToPrefixedFileWError!WindowsPathSpace { + var temp_path: WindowsPathSpace = undefined; + temp_path.len = std.unicode.wtf8ToWtf16Le(&temp_path.data, path) catch |err| switch (err) { + error.InvalidWtf8 => return error.BadPathName, + }; + temp_path.data[temp_path.len] = 0; + return wToPrefixedFileW(dir, temp_path.span()); +} + +pub const WindowsPathSpace = struct { + data: [windows.PATH_MAX_WIDE:0]u16, + len: usize, + + pub fn span(self: *const WindowsPathSpace) [:0]const u16 { + return self.data[0..self.len :0]; + } +}; + fn dirRealPathFilePosix(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, out_buffer: []u8) Dir.RealPathFileError!usize { if (native_os == .wasi) return error.OperationUnsupported; @@ -6478,7 +7022,7 @@ fn dirDeleteWindows(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, remov _ = t; const w = windows; - const sub_path_w_buf = try w.sliceToPrefixedFileW(dir.handle, sub_path); + const sub_path_w_buf = try sliceToPrefixedFileW(dir.handle, sub_path); const sub_path_w = sub_path_w_buf.span(); const path_len_bytes = @as(u16, @intCast(sub_path_w.len * 2)); @@ -6759,9 +7303,9 @@ fn dirRenameWindowsInner( replace_if_exists: bool, ) Dir.RenamePreserveError!void { const w = windows; - const old_path_w_buf = try windows.sliceToPrefixedFileW(old_dir.handle, old_sub_path); + const old_path_w_buf = try sliceToPrefixedFileW(old_dir.handle, old_sub_path); const old_path_w = old_path_w_buf.span(); - const new_path_w_buf = try windows.sliceToPrefixedFileW(new_dir.handle, new_sub_path); + const new_path_w_buf = try sliceToPrefixedFileW(new_dir.handle, new_sub_path); const new_path_w = new_path_w_buf.span(); const src_fd = src_fd: { @@ -7092,7 +7636,7 @@ fn dirSymLinkWindows( // Target path does not use sliceToPrefixedFileW because certain paths // are handled differently when creating a symlink than they would be // when converting to an NT namespaced path. - var target_path_w: w.PathSpace = undefined; + var target_path_w: WindowsPathSpace = undefined; target_path_w.len = try w.wtf8ToWtf16Le(&target_path_w.data, target_path); target_path_w.data[target_path_w.len] = 0; // However, we need to canonicalize any path separators to `\`, since if @@ -7104,7 +7648,7 @@ fn dirSymLinkWindows( std.mem.nativeToLittle(u16, '\\'), ); - const sym_link_path_w = try w.sliceToPrefixedFileW(dir.handle, sym_link_path); + const sym_link_path_w = try sliceToPrefixedFileW(dir.handle, sym_link_path); const SYMLINK_DATA = extern struct { ReparseTag: w.IO_REPARSE_TAG, @@ -7158,7 +7702,7 @@ fn dirSymLinkWindows( // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createsymboliclinkw var is_target_absolute = false; const final_target_path = target_path: { - if (w.hasCommonNtPrefix(u16, target_path_w.span())) { + if (windows.hasCommonNtPrefix(u16, target_path_w.span())) { // Already an NT path, no need to do anything to it break :target_path target_path_w.span(); } else { @@ -7176,7 +7720,7 @@ fn dirSymLinkWindows( break :target_path target_path_w.span(), } } - var prefixed_target_path = try w.wToPrefixedFileW(dir.handle, target_path_w.span()); + var prefixed_target_path = try wToPrefixedFileW(dir.handle, target_path_w.span()); // We do this after prefixing to ensure that drive-relative paths are treated as absolute is_target_absolute = Dir.path.isAbsoluteWindowsWtf16(prefixed_target_path.span()); break :target_path prefixed_target_path.span(); @@ -7322,7 +7866,7 @@ fn dirReadLink(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, buffer: [] fn dirReadLinkWindows(dir: Dir, sub_path: []const u8, buffer: []u8) Dir.ReadLinkError!usize { // This gets used once for `sub_path` and then reused again temporarily // before converting back to `buffer`. - var sub_path_w_buf = try windows.sliceToPrefixedFileW(dir.handle, sub_path); + var sub_path_w_buf = try sliceToPrefixedFileW(dir.handle, sub_path); const sub_path_w = sub_path_w_buf.span(); const path_len_bytes = std.math.cast(u16, sub_path_w.len * 2) orelse return error.NameTooLong; var nt_name: windows.UNICODE_STRING = .{ @@ -9586,7 +10130,7 @@ fn processExecutableOpen(userdata: ?*anyopaque, flags: File.OpenFlags) process.O // the file, we can let the openFileW call follow the symlink for us. const image_path_unicode_string = &windows.peb().ProcessParameters.ImagePathName; const image_path_name = image_path_unicode_string.Buffer.?[0 .. image_path_unicode_string.Length / 2 :0]; - const prefixed_path_w = try windows.wToPrefixedFileW(null, image_path_name); + const prefixed_path_w = try wToPrefixedFileW(null, image_path_name); return dirOpenFileWtf16(null, prefixed_path_w.span(), flags); }, .driverkit, @@ -9794,7 +10338,7 @@ fn processExecutablePath(userdata: ?*anyopaque, out_buffer: []u8) process.Execut // If ImagePathName is a symlink, then it will contain the path of the // symlink, not the path that the symlink points to. We want the path // that the symlink points to, though, so we need to get the realpath. - var path_name_w_buf = try w.wToPrefixedFileW(null, image_path_name); + var path_name_w_buf = try wToPrefixedFileW(null, image_path_name); const h_file = handle: { const syscall: Syscall = try .start(); @@ -9822,9 +10366,7 @@ fn processExecutablePath(userdata: ?*anyopaque, out_buffer: []u8) process.Execut }; defer w.CloseHandle(h_file); - // TODO move GetFinalPathNameByHandle logic into Io.Threaded and add cancel checks - try Thread.checkCancel(); - const wide_slice = try w.GetFinalPathNameByHandle(h_file, .{}, &path_name_w_buf.data); + const wide_slice = try GetFinalPathNameByHandle(h_file, .{}, &path_name_w_buf.data); const len = std.unicode.calcWtf8Len(wide_slice); if (len > out_buffer.len) @@ -13598,9 +14140,7 @@ fn processSetCurrentDir(userdata: ?*anyopaque, dir: Dir) process.SetCurrentDirEr if (is_windows) { var dir_path_buffer: [windows.PATH_MAX_WIDE]u16 = undefined; - // TODO move GetFinalPathNameByHandle logic into Io.Threaded and add cancel checks - try Thread.checkCancel(); - const dir_path = try windows.GetFinalPathNameByHandle(dir.handle, .{}, &dir_path_buffer); + const dir_path = try GetFinalPathNameByHandle(dir.handle, .{}, &dir_path_buffer); const path_len_bytes = std.math.cast(u16, dir_path.len * 2) orelse return error.NameTooLong; var nt_name: windows.UNICODE_STRING = .{ .Length = path_len_bytes, @@ -15326,9 +15866,7 @@ fn processSpawnWindows(userdata: ?*anyopaque, options: process.SpawnOptions) pro .inherit => break :cwd_w null, .dir => |cwd_dir| { var dir_path_buffer = try arena.alloc(u16, windows.PATH_MAX_WIDE + 1); - // TODO move GetFinalPathNameByHandle logic into std.Io.Threaded and add cancel checks - try Thread.checkCancel(); - const dir_path = try windows.GetFinalPathNameByHandle( + const dir_path = try GetFinalPathNameByHandle( cwd_dir.handle, .{}, dir_path_buffer[0..windows.PATH_MAX_WIDE], @@ -15752,7 +16290,7 @@ fn windowsCreateProcessPathExt( try dir_buf.append(arena, 0); defer dir_buf.shrinkRetainingCapacity(dir_path_len); const dir_path_z = dir_buf.items[0 .. dir_buf.items.len - 1 :0]; - const prefixed_path = try windows.wToPrefixedFileW(null, dir_path_z); + const prefixed_path = try wToPrefixedFileW(null, dir_path_z); break :dir dirOpenDirWindows(.cwd(), prefixed_path.span(), .{ .iterate = true, }) catch |err| switch (err) { diff --git a/lib/std/Io/Threaded/test.zig b/lib/std/Io/Threaded/test.zig index 81c7be9170..1c9b188584 100644 --- a/lib/std/Io/Threaded/test.zig +++ b/lib/std/Io/Threaded/test.zig @@ -6,6 +6,7 @@ const std = @import("std"); const Io = std.Io; const testing = std.testing; const assert = std.debug.assert; +const windows = std.os.windows; test "concurrent vs main prevents deadlock via oversubscription" { if (true) { @@ -277,3 +278,337 @@ test "memory mapping fallback" { try testing.expectEqualStrings("this9is9my data123", mm.memory); } } + +/// Wrapper around RtlDosPathNameToNtPathName_U for use in comparing +/// the behavior of RtlDosPathNameToNtPathName_U with wToPrefixedFileW +/// Note: RtlDosPathNameToNtPathName_U is not used in the Zig implementation +// because it allocates. +fn RtlDosPathNameToNtPathName_U(path: [:0]const u16) !Io.Threaded.WindowsPathSpace { + var out: windows.UNICODE_STRING = undefined; + const rc = windows.ntdll.RtlDosPathNameToNtPathName_U(path, &out, null, null); + if (rc != windows.TRUE) return error.BadPathName; + defer windows.ntdll.RtlFreeUnicodeString(&out); + + var path_space: Io.Threaded.WindowsPathSpace = undefined; + const out_path = out.Buffer.?[0 .. out.Length / 2]; + @memcpy(path_space.data[0..out_path.len], out_path); + path_space.len = out.Length / 2; + path_space.data[path_space.len] = 0; + + return path_space; +} + +/// Test that the Zig conversion matches the expected_path (for instances where +/// the Zig implementation intentionally diverges from what RtlDosPathNameToNtPathName_U does). +fn testToPrefixedFileNoOracle(comptime path: []const u8, comptime expected_path: []const u8) !void { + const path_utf16 = std.unicode.utf8ToUtf16LeStringLiteral(path); + const expected_path_utf16 = std.unicode.utf8ToUtf16LeStringLiteral(expected_path); + const actual_path = try Io.Threaded.wToPrefixedFileW(null, path_utf16); + std.testing.expectEqualSlices(u16, expected_path_utf16, actual_path.span()) catch |e| { + std.debug.print("got '{f}', expected '{f}'\n", .{ std.unicode.fmtUtf16Le(actual_path.span()), std.unicode.fmtUtf16Le(expected_path_utf16) }); + return e; + }; +} + +/// Test that the Zig conversion matches the expected_path and that the +/// expected_path matches the conversion that RtlDosPathNameToNtPathName_U does. +fn testToPrefixedFileWithOracle(comptime path: []const u8, comptime expected_path: []const u8) !void { + try testToPrefixedFileNoOracle(path, expected_path); + try testToPrefixedFileOnlyOracle(path); +} + +/// Test that the Zig conversion matches the conversion that RtlDosPathNameToNtPathName_U does. +fn testToPrefixedFileOnlyOracle(comptime path: []const u8) !void { + const path_utf16 = std.unicode.utf8ToUtf16LeStringLiteral(path); + const zig_result = try Io.Threaded.wToPrefixedFileW(null, path_utf16); + const win32_api_result = try RtlDosPathNameToNtPathName_U(path_utf16); + std.testing.expectEqualSlices(u16, win32_api_result.span(), zig_result.span()) catch |e| { + std.debug.print("got '{f}', expected '{f}'\n", .{ std.unicode.fmtUtf16Le(zig_result.span()), std.unicode.fmtUtf16Le(win32_api_result.span()) }); + return e; + }; +} + +test "toPrefixedFileW" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + // Most test cases come from https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html + // Note that these tests do not actually touch the filesystem or care about whether or not + // any of the paths actually exist or are otherwise valid. + + // Drive Absolute + try testToPrefixedFileWithOracle("X:\\ABC\\DEF", "\\??\\X:\\ABC\\DEF"); + try testToPrefixedFileWithOracle("X:\\", "\\??\\X:\\"); + try testToPrefixedFileWithOracle("X:\\ABC\\", "\\??\\X:\\ABC\\"); + // Trailing . and space characters are stripped + try testToPrefixedFileWithOracle("X:\\ABC\\DEF. .", "\\??\\X:\\ABC\\DEF"); + try testToPrefixedFileWithOracle("X:/ABC/DEF", "\\??\\X:\\ABC\\DEF"); + try testToPrefixedFileWithOracle("X:\\ABC\\..\\XYZ", "\\??\\X:\\XYZ"); + try testToPrefixedFileWithOracle("X:\\ABC\\..\\..\\..", "\\??\\X:\\"); + // Drive letter casing is unchanged + try testToPrefixedFileWithOracle("x:\\", "\\??\\x:\\"); + + // Drive Relative + // These tests depend on the CWD of the specified drive letter which can vary, + // so instead we just test that the Zig implementation matches the result of + // RtlDosPathNameToNtPathName_U. + // TODO: Setting the =X: environment variable didn't seem to affect + // RtlDosPathNameToNtPathName_U, not sure why that is but getting that + // to work could be an avenue to making these cases environment-independent. + // All -> are examples of the result if the X drive's cwd was X:\ABC + try testToPrefixedFileOnlyOracle("X:DEF\\GHI"); // -> \??\X:\ABC\DEF\GHI + try testToPrefixedFileOnlyOracle("X:"); // -> \??\X:\ABC + try testToPrefixedFileOnlyOracle("X:DEF. ."); // -> \??\X:\ABC\DEF + try testToPrefixedFileOnlyOracle("X:ABC\\..\\XYZ"); // -> \??\X:\ABC\XYZ + try testToPrefixedFileOnlyOracle("X:ABC\\..\\..\\.."); // -> \??\X:\ + try testToPrefixedFileOnlyOracle("x:"); // -> \??\X:\ABC + + // Rooted + // These tests depend on the drive letter of the CWD which can vary, so + // instead we just test that the Zig implementation matches the result of + // RtlDosPathNameToNtPathName_U. + // TODO: Getting the CWD path, getting the drive letter from it, and using it to + // construct the expected NT paths could be an avenue to making these cases + // environment-independent and therefore able to use testToPrefixedFileWithOracle. + // All -> are examples of the result if the CWD's drive letter was X + try testToPrefixedFileOnlyOracle("\\ABC\\DEF"); // -> \??\X:\ABC\DEF + try testToPrefixedFileOnlyOracle("\\"); // -> \??\X:\ + try testToPrefixedFileOnlyOracle("\\ABC\\DEF. ."); // -> \??\X:\ABC\DEF + try testToPrefixedFileOnlyOracle("/ABC/DEF"); // -> \??\X:\ABC\DEF + try testToPrefixedFileOnlyOracle("\\ABC\\..\\XYZ"); // -> \??\X:\XYZ + try testToPrefixedFileOnlyOracle("\\ABC\\..\\..\\.."); // -> \??\X:\ + + // Relative + // These cases differ in functionality to RtlDosPathNameToNtPathName_U. + // Relative paths remain relative if they don't have enough .. components + // to error with TooManyParentDirs + try testToPrefixedFileNoOracle("ABC\\DEF", "ABC\\DEF"); + // TODO: enable this if trailing . and spaces are stripped from relative paths + //try testToPrefixedFileNoOracle("ABC\\DEF. .", "ABC\\DEF"); + try testToPrefixedFileNoOracle("ABC/DEF", "ABC\\DEF"); + try testToPrefixedFileNoOracle("./ABC/.././DEF", "DEF"); + // TooManyParentDirs, so resolved relative to the CWD + // All -> are examples of the result if the CWD was X:\ABC\DEF + try testToPrefixedFileOnlyOracle("..\\GHI"); // -> \??\X:\ABC\GHI + try testToPrefixedFileOnlyOracle("GHI\\..\\..\\.."); // -> \??\X:\ + + // UNC Absolute + try testToPrefixedFileWithOracle("\\\\server\\share\\ABC\\DEF", "\\??\\UNC\\server\\share\\ABC\\DEF"); + try testToPrefixedFileWithOracle("\\\\server", "\\??\\UNC\\server"); + try testToPrefixedFileWithOracle("\\\\server\\share", "\\??\\UNC\\server\\share"); + try testToPrefixedFileWithOracle("\\\\server\\share\\ABC. .", "\\??\\UNC\\server\\share\\ABC"); + try testToPrefixedFileWithOracle("//server/share/ABC/DEF", "\\??\\UNC\\server\\share\\ABC\\DEF"); + try testToPrefixedFileWithOracle("\\\\server\\share\\ABC\\..\\XYZ", "\\??\\UNC\\server\\share\\XYZ"); + try testToPrefixedFileWithOracle("\\\\server\\share\\ABC\\..\\..\\..", "\\??\\UNC\\server\\share"); + + // Local Device + try testToPrefixedFileWithOracle("\\\\.\\COM20", "\\??\\COM20"); + try testToPrefixedFileWithOracle("\\\\.\\pipe\\mypipe", "\\??\\pipe\\mypipe"); + try testToPrefixedFileWithOracle("\\\\.\\X:\\ABC\\DEF. .", "\\??\\X:\\ABC\\DEF"); + try testToPrefixedFileWithOracle("\\\\.\\X:/ABC/DEF", "\\??\\X:\\ABC\\DEF"); + try testToPrefixedFileWithOracle("\\\\.\\X:\\ABC\\..\\XYZ", "\\??\\X:\\XYZ"); + // Can replace the first component of the path (contrary to drive absolute and UNC absolute paths) + try testToPrefixedFileWithOracle("\\\\.\\X:\\ABC\\..\\..\\C:\\", "\\??\\C:\\"); + try testToPrefixedFileWithOracle("\\\\.\\pipe\\mypipe\\..\\notmine", "\\??\\pipe\\notmine"); + + // Special-case device names + // TODO: Enable once these are supported + // more cases to test here: https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html + //try testToPrefixedFileWithOracle("COM1", "\\??\\COM1"); + // Sometimes the special-cased device names are not respected + try testToPrefixedFileWithOracle("\\\\.\\X:\\COM1", "\\??\\X:\\COM1"); + try testToPrefixedFileWithOracle("\\\\abc\\xyz\\COM1", "\\??\\UNC\\abc\\xyz\\COM1"); + + // Verbatim + // Left untouched except \\?\ is replaced by \??\ + try testToPrefixedFileWithOracle("\\\\?\\X:", "\\??\\X:"); + try testToPrefixedFileWithOracle("\\\\?\\X:\\COM1", "\\??\\X:\\COM1"); + try testToPrefixedFileWithOracle("\\\\?\\X:/ABC/DEF. .", "\\??\\X:/ABC/DEF. ."); + try testToPrefixedFileWithOracle("\\\\?\\X:\\ABC\\..\\..\\..", "\\??\\X:\\ABC\\..\\..\\.."); + // NT Namespace + // Fully unmodified + try testToPrefixedFileWithOracle("\\??\\X:", "\\??\\X:"); + try testToPrefixedFileWithOracle("\\??\\X:\\COM1", "\\??\\X:\\COM1"); + try testToPrefixedFileWithOracle("\\??\\X:/ABC/DEF. .", "\\??\\X:/ABC/DEF. ."); + try testToPrefixedFileWithOracle("\\??\\X:\\ABC\\..\\..\\..", "\\??\\X:\\ABC\\..\\..\\.."); + + // 'Fake' Verbatim + // If the prefix looks like the verbatim prefix but not all path separators in the + // prefix are backslashes, then it gets canonicalized and the prefix is dropped in favor + // of the NT prefix. + try testToPrefixedFileWithOracle("//?/C:/ABC", "\\??\\C:\\ABC"); + // 'Fake' NT + // If the prefix looks like the NT prefix but not all path separators in the prefix + // are backslashes, then it gets canonicalized and the /??/ is not dropped but + // rather treated as part of the path. In other words, the path is treated + // as a rooted path, so the final path is resolved relative to the CWD's + // drive letter. + // The -> shows an example of the result if the CWD's drive letter was X + try testToPrefixedFileOnlyOracle("/??/C:/ABC"); // -> \??\X:\??\C:\ABC + + // Root Local Device + // \\. and \\? always get converted to \??\ + try testToPrefixedFileWithOracle("\\\\.", "\\??\\"); + try testToPrefixedFileWithOracle("\\\\?", "\\??\\"); + try testToPrefixedFileWithOracle("//?", "\\??\\"); + try testToPrefixedFileWithOracle("//.", "\\??\\"); +} + +fn testRemoveDotDirs(str: []const u8, expected: []const u8) !void { + const mutable = try testing.allocator.dupe(u8, str); + defer testing.allocator.free(mutable); + const actual = mutable[0..try windows.removeDotDirsSanitized(u8, mutable)]; + try testing.expect(std.mem.eql(u8, actual, expected)); +} +fn testRemoveDotDirsError(err: anyerror, str: []const u8) !void { + const mutable = try testing.allocator.dupe(u8, str); + defer testing.allocator.free(mutable); + try testing.expectError(err, windows.removeDotDirsSanitized(u8, mutable)); +} +test "removeDotDirs" { + try testRemoveDotDirs("", ""); + try testRemoveDotDirs(".", ""); + try testRemoveDotDirs(".\\", ""); + try testRemoveDotDirs(".\\.", ""); + try testRemoveDotDirs(".\\.\\", ""); + try testRemoveDotDirs(".\\.\\.", ""); + + try testRemoveDotDirs("a", "a"); + try testRemoveDotDirs("a\\", "a\\"); + try testRemoveDotDirs("a\\b", "a\\b"); + try testRemoveDotDirs("a\\.", "a\\"); + try testRemoveDotDirs("a\\b\\.", "a\\b\\"); + try testRemoveDotDirs("a\\.\\b", "a\\b"); + + try testRemoveDotDirs(".a", ".a"); + try testRemoveDotDirs(".a\\", ".a\\"); + try testRemoveDotDirs(".a\\.b", ".a\\.b"); + try testRemoveDotDirs(".a\\.", ".a\\"); + try testRemoveDotDirs(".a\\.\\.", ".a\\"); + try testRemoveDotDirs(".a\\.\\.\\.b", ".a\\.b"); + try testRemoveDotDirs(".a\\.\\.\\.b\\", ".a\\.b\\"); + + try testRemoveDotDirsError(error.TooManyParentDirs, ".."); + try testRemoveDotDirsError(error.TooManyParentDirs, "..\\"); + try testRemoveDotDirsError(error.TooManyParentDirs, ".\\..\\"); + try testRemoveDotDirsError(error.TooManyParentDirs, ".\\.\\..\\"); + + try testRemoveDotDirs("a\\..", ""); + try testRemoveDotDirs("a\\..\\", ""); + try testRemoveDotDirs("a\\..\\.", ""); + try testRemoveDotDirs("a\\..\\.\\", ""); + try testRemoveDotDirs("a\\..\\.\\.", ""); + try testRemoveDotDirsError(error.TooManyParentDirs, "a\\..\\.\\.\\.."); + + try testRemoveDotDirs("a\\..\\.\\.\\b", "b"); + try testRemoveDotDirs("a\\..\\.\\.\\b\\", "b\\"); + try testRemoveDotDirs("a\\..\\.\\.\\b\\.", "b\\"); + try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\", "b\\"); + try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\..", ""); + try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\..\\", ""); + try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\..\\.", ""); + try testRemoveDotDirsError(error.TooManyParentDirs, "a\\..\\.\\.\\b\\.\\..\\.\\.."); + + try testRemoveDotDirs("a\\b\\..\\", "a\\"); + try testRemoveDotDirs("a\\b\\..\\c", "a\\c"); +} + +const RTL_PATH_TYPE = enum(c_int) { + Unknown, + UncAbsolute, + DriveAbsolute, + DriveRelative, + Rooted, + Relative, + LocalDevice, + RootLocalDevice, +}; + +pub extern "ntdll" fn RtlDetermineDosPathNameType_U( + Path: [*:0]const u16, +) callconv(.winapi) RTL_PATH_TYPE; + +test "getWin32PathType vs RtlDetermineDosPathNameType_U" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + var buf: std.ArrayList(u16) = .empty; + defer buf.deinit(std.testing.allocator); + + var wtf8_buf: std.ArrayList(u8) = .empty; + defer wtf8_buf.deinit(std.testing.allocator); + + var random = std.Random.DefaultPrng.init(std.testing.random_seed); + const rand = random.random(); + + for (0..1000) |_| { + buf.clearRetainingCapacity(); + const path = try getRandomWtf16Path(std.testing.allocator, &buf, rand); + wtf8_buf.clearRetainingCapacity(); + const wtf8_len = std.unicode.calcWtf8Len(path); + try wtf8_buf.ensureTotalCapacity(std.testing.allocator, wtf8_len); + wtf8_buf.items.len = wtf8_len; + std.debug.assert(std.unicode.wtf16LeToWtf8(wtf8_buf.items, path) == wtf8_len); + + const windows_type = RtlDetermineDosPathNameType_U(path); + const wtf16_type = std.fs.path.getWin32PathType(u16, path); + const wtf8_type = std.fs.path.getWin32PathType(u8, wtf8_buf.items); + + checkPathType(windows_type, wtf16_type) catch |err| { + std.debug.print("expected type {}, got {} for path: {f}\n", .{ windows_type, wtf16_type, std.unicode.fmtUtf16Le(path) }); + std.debug.print("path bytes:\n", .{}); + std.debug.dumpHex(std.mem.sliceAsBytes(path)); + return err; + }; + + if (wtf16_type != wtf8_type) { + std.debug.print("type mismatch between wtf8: {} and wtf16: {} for path: {f}\n", .{ wtf8_type, wtf16_type, std.unicode.fmtUtf16Le(path) }); + std.debug.print("wtf-16 path bytes:\n", .{}); + std.debug.dumpHex(std.mem.sliceAsBytes(path)); + std.debug.print("wtf-8 path bytes:\n", .{}); + std.debug.dumpHex(std.mem.sliceAsBytes(wtf8_buf.items)); + return error.Wtf8Wtf16Mismatch; + } + } +} + +fn checkPathType(windows_type: RTL_PATH_TYPE, zig_type: std.fs.path.Win32PathType) !void { + const expected_windows_type: RTL_PATH_TYPE = switch (zig_type) { + .unc_absolute => .UncAbsolute, + .drive_absolute => .DriveAbsolute, + .drive_relative => .DriveRelative, + .rooted => .Rooted, + .relative => .Relative, + .local_device => .LocalDevice, + .root_local_device => .RootLocalDevice, + }; + if (windows_type != expected_windows_type) return error.PathTypeMismatch; +} + +fn getRandomWtf16Path(allocator: std.mem.Allocator, buf: *std.ArrayList(u16), rand: std.Random) ![:0]const u16 { + const Choice = enum { + backslash, + slash, + control, + printable, + non_ascii, + }; + + const choices = rand.uintAtMostBiased(u16, 32); + + for (0..choices) |_| { + const choice = rand.enumValue(Choice); + const code_unit = switch (choice) { + .backslash => '\\', + .slash => '/', + .control => switch (rand.uintAtMostBiased(u8, 0x20)) { + 0x20 => '\x7F', + else => |b| b + 1, // no NUL + }, + .printable => '!' + rand.uintAtMostBiased(u8, '~' - '!'), + .non_ascii => rand.intRangeAtMostBiased(u16, 0x80, 0xFFFF), + }; + try buf.append(allocator, std.mem.nativeToLittle(u16, code_unit)); + } + + try buf.append(allocator, 0); + return buf.items[0 .. buf.items.len - 1 :0]; +} diff --git a/lib/std/dynamic_library.zig b/lib/std/dynamic_library.zig index 16a82c874b..d7cea3b3cd 100644 --- a/lib/std/dynamic_library.zig +++ b/lib/std/dynamic_library.zig @@ -17,7 +17,6 @@ pub const DynLib = struct { ElfDynLib else DlDynLib, - .windows => WindowsDynLib, .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos, .freebsd, .netbsd, .openbsd, .dragonfly, .illumos => DlDynLib, else => struct { const open = @compileError("unsupported platform"); @@ -27,7 +26,7 @@ pub const DynLib = struct { inner: InnerType, - pub const Error = ElfDynLibError || DlDynLibError || WindowsDynLibError; + pub const Error = ElfDynLibError || DlDynLibError; /// Trusts the file. Malicious file will be able to execute arbitrary code. pub fn open(path: []const u8) Error!DynLib { @@ -558,73 +557,6 @@ test "ElfDynLib" { try testing.expectError(error.FileNotFound, ElfDynLib.openZ("invalid_so.so", null)); } -/// Separated to avoid referencing `WindowsDynLib`, because its field types may not -/// be valid on other targets. -const WindowsDynLibError = error{ - FileNotFound, - InvalidPath, -} || windows.LoadLibraryError; - -pub const WindowsDynLib = struct { - pub const Error = WindowsDynLibError; - - dll: windows.HMODULE, - - pub fn open(path: []const u8) Error!WindowsDynLib { - return openEx(path, .none); - } - - /// WindowsDynLib specific - /// Opens dynamic library with specified library loading flags. - pub fn openEx(path: []const u8, flags: windows.LoadLibraryFlags) Error!WindowsDynLib { - const path_w = windows.sliceToPrefixedFileW(null, path) catch return error.InvalidPath; - return openExW(path_w.span().ptr, flags); - } - - pub fn openZ(path_c: [*:0]const u8) Error!WindowsDynLib { - return openExZ(path_c, .none); - } - - /// WindowsDynLib specific - /// Opens dynamic library with specified library loading flags. - pub fn openExZ(path_c: [*:0]const u8, flags: windows.LoadLibraryFlags) Error!WindowsDynLib { - const path_w = windows.cStrToPrefixedFileW(null, path_c) catch return error.InvalidPath; - return openExW(path_w.span().ptr, flags); - } - - /// WindowsDynLib specific - pub fn openW(path_w: [*:0]const u16) Error!WindowsDynLib { - return openExW(path_w, .none); - } - - /// WindowsDynLib specific - /// Opens dynamic library with specified library loading flags. - pub fn openExW(path_w: [*:0]const u16, flags: windows.LoadLibraryFlags) Error!WindowsDynLib { - var offset: usize = 0; - if (path_w[0] == '\\' and path_w[1] == '?' and path_w[2] == '?' and path_w[3] == '\\') { - // + 4 to skip over the \??\ - offset = 4; - } - - return .{ - .dll = try windows.LoadLibraryExW(path_w + offset, flags), - }; - } - - pub fn close(self: *WindowsDynLib) void { - windows.FreeLibrary(self.dll); - self.* = undefined; - } - - pub fn lookup(self: *WindowsDynLib, comptime T: type, name: [:0]const u8) ?T { - if (windows.kernel32.GetProcAddress(self.dll, name.ptr)) |addr| { - return @as(T, @ptrCast(@alignCast(addr))); - } else { - return null; - } - } -}; - /// Separated to avoid referencing `DlDynLib`, because its field types may not /// be valid on other targets. const DlDynLibError = error{ FileNotFound, NameTooLong }; @@ -676,7 +608,6 @@ pub const DlDynLib = struct { test "dynamic_library" { const libname = switch (native_os) { .linux, .freebsd, .openbsd, .illumos => "invalid_so.so", - .windows => "invalid_dll.dll", .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => "invalid_dylib.dylib", else => return error.SkipZigTest, }; diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index dcb9087f13..ac36e77ac3 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -15,12 +15,6 @@ const math = std.math; const maxInt = std.math.maxInt; const UnexpectedError = std.posix.UnexpectedError; -test { - if (builtin.os.tag == .windows) { - _ = @import("windows/test.zig"); - } -} - pub const advapi32 = @import("windows/advapi32.zig"); pub const kernel32 = @import("windows/kernel32.zig"); pub const ntdll = @import("windows/ntdll.zig"); @@ -2670,324 +2664,6 @@ pub fn CloseHandle(hObject: HANDLE) void { assert(ntdll.NtClose(hObject) == .SUCCESS); } -pub const QueryObjectNameError = error{ - AccessDenied, - InvalidHandle, - NameTooLong, - Unexpected, -}; - -pub fn QueryObjectName(handle: HANDLE, out_buffer: []u16) QueryObjectNameError![]u16 { - const out_buffer_aligned = mem.alignInSlice(out_buffer, @alignOf(OBJECT_NAME_INFORMATION)) orelse return error.NameTooLong; - - const info = @as(*OBJECT_NAME_INFORMATION, @ptrCast(out_buffer_aligned)); - // buffer size is specified in bytes - const out_buffer_len = std.math.cast(ULONG, out_buffer_aligned.len * 2) orelse maxInt(ULONG); - // last argument would return the length required for full_buffer, not exposed here - return switch (ntdll.NtQueryObject(handle, .ObjectNameInformation, info, out_buffer_len, null)) { - .SUCCESS => blk: { - // info.Name.Buffer from ObQueryNameString is documented to be null (and MaximumLength == 0) - // if the object was "unnamed", not sure if this can happen for file handles - if (info.Name.MaximumLength == 0) break :blk error.Unexpected; - // resulting string length is specified in bytes - const path_length_unterminated = @divExact(info.Name.Length, 2); - break :blk info.Name.Buffer.?[0..path_length_unterminated]; - }, - .ACCESS_DENIED => error.AccessDenied, - .INVALID_HANDLE => error.InvalidHandle, - // triggered when the buffer is too small for the OBJECT_NAME_INFORMATION object (.INFO_LENGTH_MISMATCH), - // or if the buffer is too small for the file path returned (.BUFFER_OVERFLOW, .BUFFER_TOO_SMALL) - .INFO_LENGTH_MISMATCH, .BUFFER_OVERFLOW, .BUFFER_TOO_SMALL => error.NameTooLong, - else => |e| unexpectedStatus(e), - }; -} - -test QueryObjectName { - if (builtin.os.tag != .windows) - return; - - //any file will do; canonicalization works on NTFS junctions and symlinks, hardlinks remain separate paths. - var tmp = std.testing.tmpDir(.{}); - defer tmp.cleanup(); - const handle = tmp.dir.handle; - var out_buffer: [PATH_MAX_WIDE]u16 = undefined; - - const result_path = try QueryObjectName(handle, &out_buffer); - const required_len_in_u16 = result_path.len + @divExact(@intFromPtr(result_path.ptr) - @intFromPtr(&out_buffer), 2) + 1; - //insufficient size - try std.testing.expectError(error.NameTooLong, QueryObjectName(handle, out_buffer[0 .. required_len_in_u16 - 1])); - //exactly-sufficient size - _ = try QueryObjectName(handle, out_buffer[0..required_len_in_u16]); -} - -pub const GetFinalPathNameByHandleError = error{ - AccessDenied, - FileNotFound, - NameTooLong, - /// The volume does not contain a recognized file system. File system - /// drivers might not be loaded, or the volume may be corrupt. - UnrecognizedVolume, - Unexpected, -}; - -/// Specifies how to format volume path in the result of `GetFinalPathNameByHandle`. -/// Defaults to DOS volume names. -pub const GetFinalPathNameByHandleFormat = struct { - volume_name: enum { - /// Format as DOS volume name - Dos, - /// Format as NT volume name - Nt, - } = .Dos, -}; - -/// Returns canonical (normalized) path of handle. -/// Use `GetFinalPathNameByHandleFormat` to specify whether the path is meant to include -/// NT or DOS volume name (e.g., `\Device\HarddiskVolume0\foo.txt` versus `C:\foo.txt`). -/// If DOS volume name format is selected, note that this function does *not* prepend -/// `\\?\` prefix to the resultant path. -/// -/// TODO move this function into std.Io.Threaded and add cancelation checks -pub fn GetFinalPathNameByHandle( - hFile: HANDLE, - fmt: GetFinalPathNameByHandleFormat, - out_buffer: []u16, -) GetFinalPathNameByHandleError![]u16 { - const final_path = QueryObjectName(hFile, out_buffer) catch |err| switch (err) { - // we assume InvalidHandle is close enough to FileNotFound in semantics - // to not further complicate the error set - error.InvalidHandle => return error.FileNotFound, - else => |e| return e, - }; - - switch (fmt.volume_name) { - .Nt => { - // the returned path is already in .Nt format - return final_path; - }, - .Dos => { - // parse the string to separate volume path from file path - const device_prefix = std.unicode.utf8ToUtf16LeStringLiteral("\\Device\\"); - - // We aren't entirely sure of the structure of the path returned by - // QueryObjectName in all contexts/environments. - // This code is written to cover the various cases that have - // been encountered and solved appropriately. But note that there's - // no easy way to verify that they have all been tackled! - // (Unless you, the reader knows of one then please do action that!) - if (!mem.startsWith(u16, final_path, device_prefix)) { - // Wine seems to return NT namespaced paths starting with \??\ from QueryObjectName - // (e.g. `\??\Z:\some\path\to\a\file.txt`), in which case we can just strip the - // prefix to turn it into an absolute path. - // https://github.com/ziglang/zig/issues/26029 - // https://bugs.winehq.org/show_bug.cgi?id=39569 - return ntToWin32Namespace(final_path, out_buffer) catch |err| switch (err) { - error.NotNtPath => return error.Unexpected, - error.NameTooLong => |e| return e, - }; - } - - const file_path_begin_index = mem.findPos(u16, final_path, device_prefix.len, &[_]u16{'\\'}) orelse unreachable; - const volume_name_u16 = final_path[0..file_path_begin_index]; - const device_name_u16 = volume_name_u16[device_prefix.len..]; - const file_name_u16 = final_path[file_path_begin_index..]; - - // MUP is Multiple UNC Provider, and indicates that the path is a UNC - // path. In this case, the canonical UNC path can be gotten by just - // dropping the \Device\Mup\ and making sure the path begins with \\ - if (mem.eql(u16, device_name_u16, std.unicode.utf8ToUtf16LeStringLiteral("Mup"))) { - out_buffer[0] = '\\'; - @memmove(out_buffer[1..][0..file_name_u16.len], file_name_u16); - return out_buffer[0 .. 1 + file_name_u16.len]; - } - - // Get DOS volume name. DOS volume names are actually symbolic link objects to the - // actual NT volume. For example: - // (NT) \Device\HarddiskVolume4 => (DOS) \DosDevices\C: == (DOS) C: - const MIN_SIZE = @sizeOf(MOUNTMGR_MOUNT_POINT) + MAX_PATH; - // We initialize the input buffer to all zeros for convenience since - // `DeviceIoControl` with `IOCTL_MOUNTMGR_QUERY_POINTS` expects this. - var input_buf: [MIN_SIZE]u8 align(@alignOf(MOUNTMGR_MOUNT_POINT)) = [_]u8{0} ** MIN_SIZE; - var output_buf: [MIN_SIZE * 4]u8 align(@alignOf(MOUNTMGR_MOUNT_POINTS)) = undefined; - - // This surprising path is a filesystem path to the mount manager on Windows. - // Source: https://stackoverflow.com/questions/3012828/using-ioctl-mountmgr-query-points - // This is the NT namespaced version of \\.\MountPointManager - const mgmt_path_u16 = std.unicode.utf8ToUtf16LeStringLiteral("\\??\\MountPointManager"); - const mgmt_handle = OpenFile(mgmt_path_u16, .{ - .access_mask = .{ .STANDARD = .{ .SYNCHRONIZE = true } }, - .creation = .OPEN, - }) catch |err| switch (err) { - error.IsDir => return error.Unexpected, - error.NotDir => return error.Unexpected, - error.NoDevice => return error.Unexpected, - error.AccessDenied => return error.Unexpected, - error.PipeBusy => return error.Unexpected, - error.PathAlreadyExists => return error.Unexpected, - error.WouldBlock => return error.Unexpected, - error.NetworkNotFound => return error.Unexpected, - error.AntivirusInterference => return error.Unexpected, - error.BadPathName => return error.Unexpected, - error.OperationCanceled => @panic("TODO: better integrate cancelation"), - else => |e| return e, - }; - defer CloseHandle(mgmt_handle); - - var input_struct: *MOUNTMGR_MOUNT_POINT = @ptrCast(&input_buf[0]); - input_struct.DeviceNameOffset = @sizeOf(MOUNTMGR_MOUNT_POINT); - input_struct.DeviceNameLength = @intCast(volume_name_u16.len * 2); - @memcpy(input_buf[@sizeOf(MOUNTMGR_MOUNT_POINT)..][0 .. volume_name_u16.len * 2], @as([*]const u8, @ptrCast(volume_name_u16.ptr))); - - { - const rc = DeviceIoControl(mgmt_handle, IOCTL.MOUNTMGR.QUERY_POINTS, .{ .in = &input_buf, .out = &output_buf }); - switch (rc) { - .SUCCESS => {}, - .OBJECT_NAME_NOT_FOUND => return error.FileNotFound, - else => return unexpectedStatus(rc), - } - } - const mount_points_struct: *const MOUNTMGR_MOUNT_POINTS = @ptrCast(&output_buf[0]); - - const mount_points = @as( - [*]const MOUNTMGR_MOUNT_POINT, - @ptrCast(&mount_points_struct.MountPoints[0]), - )[0..mount_points_struct.NumberOfMountPoints]; - - for (mount_points) |mount_point| { - const symlink = @as( - [*]const u16, - @ptrCast(@alignCast(&output_buf[mount_point.SymbolicLinkNameOffset])), - )[0 .. mount_point.SymbolicLinkNameLength / 2]; - - // Look for `\DosDevices\` prefix. We don't really care if there are more than one symlinks - // with traditional DOS drive letters, so pick the first one available. - var prefix_buf = std.unicode.utf8ToUtf16LeStringLiteral("\\DosDevices\\"); - const prefix = prefix_buf[0..prefix_buf.len]; - - if (mem.startsWith(u16, symlink, prefix)) { - const drive_letter = symlink[prefix.len..]; - - if (out_buffer.len < drive_letter.len + file_name_u16.len) return error.NameTooLong; - - @memcpy(out_buffer[0..drive_letter.len], drive_letter); - @memmove(out_buffer[drive_letter.len..][0..file_name_u16.len], file_name_u16); - const total_len = drive_letter.len + file_name_u16.len; - - // Validate that DOS does not contain any spurious nul bytes. - assert(mem.findScalar(u16, out_buffer[0..total_len], 0) == null); - - return out_buffer[0..total_len]; - } else if (mountmgrIsVolumeName(symlink)) { - // If the symlink is a volume GUID like \??\Volume{383da0b0-717f-41b6-8c36-00500992b58d}, - // then it is a volume mounted as a path rather than a drive letter. We need to - // query the mount manager again to get the DOS path for the volume. - - // 49 is the maximum length accepted by mountmgrIsVolumeName - const vol_input_size = @sizeOf(MOUNTMGR_TARGET_NAME) + (49 * 2); - var vol_input_buf: [vol_input_size]u8 align(@alignOf(MOUNTMGR_TARGET_NAME)) = [_]u8{0} ** vol_input_size; - // Note: If the path exceeds MAX_PATH, the Disk Management GUI doesn't accept the full path, - // and instead if must be specified using a shortened form (e.g. C:\FOO~1\BAR~1\<...>). - // However, just to be sure we can handle any path length, we use PATH_MAX_WIDE here. - const min_output_size = @sizeOf(MOUNTMGR_VOLUME_PATHS) + (PATH_MAX_WIDE * 2); - var vol_output_buf: [min_output_size]u8 align(@alignOf(MOUNTMGR_VOLUME_PATHS)) = undefined; - - var vol_input_struct: *MOUNTMGR_TARGET_NAME = @ptrCast(&vol_input_buf[0]); - vol_input_struct.DeviceNameLength = @intCast(symlink.len * 2); - @memcpy(@as([*]WCHAR, &vol_input_struct.DeviceName)[0..symlink.len], symlink); - - const rc = DeviceIoControl(mgmt_handle, IOCTL.MOUNTMGR.QUERY_DOS_VOLUME_PATH, .{ .in = &vol_input_buf, .out = &vol_output_buf }); - switch (rc) { - .SUCCESS => {}, - .UNRECOGNIZED_VOLUME => return error.UnrecognizedVolume, - else => return unexpectedStatus(rc), - } - const volume_paths_struct: *const MOUNTMGR_VOLUME_PATHS = @ptrCast(&vol_output_buf[0]); - const volume_path = std.mem.sliceTo(@as( - [*]const u16, - &volume_paths_struct.MultiSz, - )[0 .. volume_paths_struct.MultiSzLength / 2], 0); - - if (out_buffer.len < volume_path.len + file_name_u16.len) return error.NameTooLong; - - // `out_buffer` currently contains the memory of `file_name_u16`, so it can overlap with where - // we want to place the filename before returning. Here are the possible overlapping cases: - // - // out_buffer: [filename] - // dest: [___(a)___] [___(b)___] - // - // In the case of (a), we need to copy forwards, and in the case of (b) we need - // to copy backwards. We also need to do this before copying the volume path because - // it could overwrite the file_name_u16 memory. - const file_name_dest = out_buffer[volume_path.len..][0..file_name_u16.len]; - @memmove(file_name_dest, file_name_u16); - @memcpy(out_buffer[0..volume_path.len], volume_path); - const total_len = volume_path.len + file_name_u16.len; - - // Validate that DOS does not contain any spurious nul bytes. - assert(mem.findScalar(u16, out_buffer[0..total_len], 0) == null); - - return out_buffer[0..total_len]; - } - } - - // If we've ended up here, then something went wrong/is corrupted in the OS, - // so error out! - return error.FileNotFound; - }, - } -} - -/// Equivalent to the MOUNTMGR_IS_VOLUME_NAME macro in mountmgr.h -fn mountmgrIsVolumeName(name: []const u16) bool { - return (name.len == 48 or (name.len == 49 and name[48] == mem.nativeToLittle(u16, '\\'))) and - name[0] == mem.nativeToLittle(u16, '\\') and - (name[1] == mem.nativeToLittle(u16, '?') or name[1] == mem.nativeToLittle(u16, '\\')) and - name[2] == mem.nativeToLittle(u16, '?') and - name[3] == mem.nativeToLittle(u16, '\\') and - mem.startsWith(u16, name[4..], std.unicode.utf8ToUtf16LeStringLiteral("Volume{")) and - name[19] == mem.nativeToLittle(u16, '-') and - name[24] == mem.nativeToLittle(u16, '-') and - name[29] == mem.nativeToLittle(u16, '-') and - name[34] == mem.nativeToLittle(u16, '-') and - name[47] == mem.nativeToLittle(u16, '}'); -} - -test mountmgrIsVolumeName { - @setEvalBranchQuota(2000); - const L = std.unicode.utf8ToUtf16LeStringLiteral; - try std.testing.expect(mountmgrIsVolumeName(L("\\\\?\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}"))); - try std.testing.expect(mountmgrIsVolumeName(L("\\??\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}"))); - try std.testing.expect(mountmgrIsVolumeName(L("\\\\?\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}\\"))); - try std.testing.expect(mountmgrIsVolumeName(L("\\??\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}\\"))); - try std.testing.expect(!mountmgrIsVolumeName(L("\\\\.\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}"))); - try std.testing.expect(!mountmgrIsVolumeName(L("\\??\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}\\foo"))); - try std.testing.expect(!mountmgrIsVolumeName(L("\\??\\Volume{383da0b0-717f-41b6-8c36-00500992b58}"))); -} - -test GetFinalPathNameByHandle { - if (builtin.os.tag != .windows) - return; - - //any file will do - var tmp = std.testing.tmpDir(.{}); - defer tmp.cleanup(); - const handle = tmp.dir.handle; - var buffer: [PATH_MAX_WIDE]u16 = undefined; - - //check with sufficient size - const nt_path = try GetFinalPathNameByHandle(handle, .{ .volume_name = .Nt }, &buffer); - _ = try GetFinalPathNameByHandle(handle, .{ .volume_name = .Dos }, &buffer); - - const required_len_in_u16 = nt_path.len + @divExact(@intFromPtr(nt_path.ptr) - @intFromPtr(&buffer), 2) + 1; - //check with insufficient size - try std.testing.expectError(error.NameTooLong, GetFinalPathNameByHandle(handle, .{ .volume_name = .Nt }, buffer[0 .. required_len_in_u16 - 1])); - try std.testing.expectError(error.NameTooLong, GetFinalPathNameByHandle(handle, .{ .volume_name = .Dos }, buffer[0 .. required_len_in_u16 - 1])); - - //check with exactly-sufficient size - _ = try GetFinalPathNameByHandle(handle, .{ .volume_name = .Nt }, buffer[0..required_len_in_u16]); - _ = try GetFinalPathNameByHandle(handle, .{ .volume_name = .Dos }, buffer[0..required_len_in_u16]); -} - pub fn getpeername(s: ws2_32.SOCKET, name: *ws2_32.sockaddr, namelen: *ws2_32.socklen_t) i32 { return ws2_32.getpeername(s, name, @as(*i32, @ptrCast(namelen))); } @@ -3419,15 +3095,6 @@ test "eqlIgnoreCaseWtf16/Wtf8" { try testEqlIgnoreCase(false, "𐓏", "𐓷"); } -pub const PathSpace = struct { - data: [PATH_MAX_WIDE:0]u16, - len: usize, - - pub fn span(self: *const PathSpace) [:0]const u16 { - return self.data[0..self.len :0]; - } -}; - /// The error type for `removeDotDirsSanitized` pub const RemoveDotDirsError = error{TooManyParentDirs}; @@ -3503,205 +3170,6 @@ pub fn normalizePath(comptime T: type, path: []T) RemoveDotDirsError!usize { return prefix_len + try removeDotDirsSanitized(T, path[prefix_len..new_len]); } -pub const Wtf8ToPrefixedFileWError = Wtf16ToPrefixedFileWError; - -/// Same as `sliceToPrefixedFileW` but accepts a pointer -/// to a null-terminated WTF-8 encoded path. -/// https://wtf-8.codeberg.page/ -pub fn cStrToPrefixedFileW(dir: ?HANDLE, s: [*:0]const u8) Wtf8ToPrefixedFileWError!PathSpace { - return sliceToPrefixedFileW(dir, mem.sliceTo(s, 0)); -} - -/// Same as `wToPrefixedFileW` but accepts a WTF-8 encoded path. -/// https://wtf-8.codeberg.page/ -pub fn sliceToPrefixedFileW(dir: ?HANDLE, path: []const u8) Wtf8ToPrefixedFileWError!PathSpace { - var temp_path: PathSpace = undefined; - temp_path.len = std.unicode.wtf8ToWtf16Le(&temp_path.data, path) catch |err| switch (err) { - error.InvalidWtf8 => return error.BadPathName, - }; - temp_path.data[temp_path.len] = 0; - return wToPrefixedFileW(dir, temp_path.span()); -} - -pub const Wtf16ToPrefixedFileWError = error{ - AccessDenied, - BadPathName, - FileNotFound, - NameTooLong, - Unexpected, -}; - -/// Converts the `path` to WTF16, null-terminated. If the path contains any -/// namespace prefix, or is anything but a relative path (rooted, drive relative, -/// etc) the result will have the NT-style prefix `\??\`. -/// -/// Similar to RtlDosPathNameToNtPathName_U with a few differences: -/// - Does not allocate on the heap. -/// - Relative paths are kept as relative unless they contain too many .. -/// components, in which case they are resolved against the `dir` if it -/// is non-null, or the CWD if it is null. -/// - Special case device names like COM1, NUL, etc are not handled specially (TODO) -/// - . and space are not stripped from the end of relative paths (potential TODO) -pub fn wToPrefixedFileW(dir: ?HANDLE, path: [:0]const u16) Wtf16ToPrefixedFileWError!PathSpace { - const nt_prefix = [_]u16{ '\\', '?', '?', '\\' }; - if (hasCommonNtPrefix(u16, path)) { - // TODO: Figure out a way to design an API that can avoid the copy for NT, - // since it is always returned fully unmodified. - var path_space: PathSpace = undefined; - path_space.data[0..nt_prefix.len].* = nt_prefix; - const len_after_prefix = path.len - nt_prefix.len; - @memcpy(path_space.data[nt_prefix.len..][0..len_after_prefix], path[nt_prefix.len..]); - path_space.len = path.len; - path_space.data[path_space.len] = 0; - return path_space; - } else { - const path_type = std.fs.path.getWin32PathType(u16, path); - var path_space: PathSpace = undefined; - if (path_type == .local_device) { - switch (getLocalDevicePathType(u16, path)) { - .verbatim => { - path_space.data[0..nt_prefix.len].* = nt_prefix; - const len_after_prefix = path.len - nt_prefix.len; - @memcpy(path_space.data[nt_prefix.len..][0..len_after_prefix], path[nt_prefix.len..]); - path_space.len = path.len; - path_space.data[path_space.len] = 0; - return path_space; - }, - .local_device, .fake_verbatim => { - const path_byte_len = ntdll.RtlGetFullPathName_U( - path.ptr, - path_space.data.len * 2, - &path_space.data, - null, - ); - if (path_byte_len == 0) { - // TODO: This may not be the right error - return error.BadPathName; - } else if (path_byte_len / 2 > path_space.data.len) { - return error.NameTooLong; - } - path_space.len = path_byte_len / 2; - // Both prefixes will be normalized but retained, so all - // we need to do now is replace them with the NT prefix - path_space.data[0..nt_prefix.len].* = nt_prefix; - return path_space; - }, - } - } - relative: { - if (path_type == .relative) { - // TODO: Handle special case device names like COM1, AUX, NUL, CONIN$, CONOUT$, etc. - // See https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html - - // TODO: Potentially strip all trailing . and space characters from the - // end of the path. This is something that both RtlDosPathNameToNtPathName_U - // and RtlGetFullPathName_U do. Technically, trailing . and spaces - // are allowed, but such paths may not interact well with Windows (i.e. - // files with these paths can't be deleted from explorer.exe, etc). - // This could be something that normalizePath may want to do. - - @memcpy(path_space.data[0..path.len], path); - // Try to normalize, but if we get too many parent directories, - // then we need to start over and use RtlGetFullPathName_U instead. - path_space.len = normalizePath(u16, path_space.data[0..path.len]) catch |err| switch (err) { - error.TooManyParentDirs => break :relative, - }; - path_space.data[path_space.len] = 0; - return path_space; - } - } - // We now know we are going to return an absolute NT path, so - // we can unconditionally prefix it with the NT prefix. - path_space.data[0..nt_prefix.len].* = nt_prefix; - if (path_type == .root_local_device) { - // `\\.` and `\\?` always get converted to `\??\` exactly, so - // we can just stop here - path_space.len = nt_prefix.len; - path_space.data[path_space.len] = 0; - return path_space; - } - const path_buf_offset = switch (path_type) { - // UNC paths will always start with `\\`. However, we want to - // end up with something like `\??\UNC\server\share`, so to get - // RtlGetFullPathName to write into the spot we want the `server` - // part to end up, we need to provide an offset such that - // the `\\` part gets written where the `C\` of `UNC\` will be - // in the final NT path. - .unc_absolute => nt_prefix.len + 2, - else => nt_prefix.len, - }; - const buf_len: u32 = @intCast(path_space.data.len - path_buf_offset); - const path_to_get: [:0]const u16 = path_to_get: { - // If dir is null, then we don't need to bother with GetFinalPathNameByHandle because - // RtlGetFullPathName_U will resolve relative paths against the CWD for us. - if (path_type != .relative or dir == null) { - break :path_to_get path; - } - // We can also skip GetFinalPathNameByHandle if the handle matches - // the handle returned by Io.Dir.cwd() - if (dir.? == Io.Dir.cwd().handle) { - break :path_to_get path; - } - // At this point, we know we have a relative path that had too many - // `..` components to be resolved by normalizePath, so we need to - // convert it into an absolute path and let RtlGetFullPathName_U - // canonicalize it. We do this by getting the path of the `dir` - // and appending the relative path to it. - var dir_path_buf: [PATH_MAX_WIDE:0]u16 = undefined; - const dir_path = GetFinalPathNameByHandle(dir.?, .{}, &dir_path_buf) catch |err| switch (err) { - // This mapping is not correct; it is actually expected - // that calling GetFinalPathNameByHandle might return - // error.UnrecognizedVolume, and in fact has been observed - // in the wild. The problem is that wToPrefixedFileW was - // never intended to make *any* OS syscall APIs. It's only - // supposed to convert a string to one that is eligible to - // be used in the ntdll syscalls. - // - // To solve this, this function needs to no longer call - // GetFinalPathNameByHandle under any conditions, or the - // calling function needs to get reworked to not need to - // call this function. - // - // This may involve making breaking API changes. - error.UnrecognizedVolume => return error.Unexpected, - else => |e| return e, - }; - if (dir_path.len + 1 + path.len > PATH_MAX_WIDE) { - return error.NameTooLong; - } - // We don't have to worry about potentially doubling up path separators - // here since RtlGetFullPathName_U will handle canonicalizing it. - dir_path_buf[dir_path.len] = '\\'; - @memcpy(dir_path_buf[dir_path.len + 1 ..][0..path.len], path); - const full_len = dir_path.len + 1 + path.len; - dir_path_buf[full_len] = 0; - break :path_to_get dir_path_buf[0..full_len :0]; - }; - const path_byte_len = ntdll.RtlGetFullPathName_U( - path_to_get.ptr, - buf_len * 2, - path_space.data[path_buf_offset..].ptr, - null, - ); - if (path_byte_len == 0) { - // TODO: This may not be the right error - return error.BadPathName; - } else if (path_byte_len / 2 > buf_len) { - return error.NameTooLong; - } - path_space.len = path_buf_offset + (path_byte_len / 2); - if (path_type == .unc_absolute) { - // Now add in the UNC, the `C` should overwrite the first `\` of the - // FullPathName, ultimately resulting in `\??\UNC\` - assert(path_space.data[path_buf_offset] == '\\'); - assert(path_space.data[path_buf_offset + 1] == '\\'); - const unc = [_]u16{ 'U', 'N', 'C' }; - path_space.data[nt_prefix.len..][0..unc.len].* = unc; - } - return path_space; - } -} - /// Returns true if the path starts with `\??\`, which is indicative of an NT path /// but is not enough to fully distinguish between NT paths and Win32 paths, as /// `\??\` is not actually a distinct prefix but rather the path to a special virtual @@ -3725,39 +3193,6 @@ pub fn hasCommonNtPrefix(comptime T: type, path: []const T) bool { return mem.startsWith(T, path, expected_prefix); } -const LocalDevicePathType = enum { - /// `\\.\` (path separators can be `\` or `/`) - local_device, - /// `\\?\` - /// When converted to an NT path, everything past the prefix is left - /// untouched and `\\?\` is replaced by `\??\`. - verbatim, - /// `\\?\` without all path separators being `\`. - /// This seems to be recognized as a prefix, but the 'verbatim' aspect - /// is not respected (i.e. if `//?/C:/foo` is converted to an NT path, - /// it will become `\??\C:\foo` [it will be canonicalized and the //?/ won't - /// be treated as part of the final path]) - fake_verbatim, -}; - -/// Only relevant for Win32 -> NT path conversion. -/// Asserts `path` is of type `std.fs.path.Win32PathType.local_device`. -fn getLocalDevicePathType(comptime T: type, path: []const T) LocalDevicePathType { - if (std.debug.runtime_safety) { - assert(std.fs.path.getWin32PathType(T, path) == .local_device); - } - - const backslash = mem.nativeToLittle(T, '\\'); - const all_backslash = path[0] == backslash and - path[1] == backslash and - path[3] == backslash; - return switch (path[2]) { - mem.nativeToLittle(T, '?') => if (all_backslash) .verbatim else .fake_verbatim, - mem.nativeToLittle(T, '.') => .local_device, - else => unreachable, - }; -} - /// Similar to `RtlNtPathNameToDosPathName` but does not do any heap allocation. /// The possible transformations are: /// \??\C:\Some\Path -> C:\Some\Path diff --git a/lib/std/os/windows/test.zig b/lib/std/os/windows/test.zig deleted file mode 100644 index 58eb4e0b97..0000000000 --- a/lib/std/os/windows/test.zig +++ /dev/null @@ -1,339 +0,0 @@ -const std = @import("../../std.zig"); -const builtin = @import("builtin"); -const windows = std.os.windows; -const mem = std.mem; -const testing = std.testing; - -/// Wrapper around RtlDosPathNameToNtPathName_U for use in comparing -/// the behavior of RtlDosPathNameToNtPathName_U with wToPrefixedFileW -/// Note: RtlDosPathNameToNtPathName_U is not used in the Zig implementation -// because it allocates. -fn RtlDosPathNameToNtPathName_U(path: [:0]const u16) !windows.PathSpace { - var out: windows.UNICODE_STRING = undefined; - const rc = windows.ntdll.RtlDosPathNameToNtPathName_U(path, &out, null, null); - if (rc != windows.TRUE) return error.BadPathName; - defer windows.ntdll.RtlFreeUnicodeString(&out); - - var path_space: windows.PathSpace = undefined; - const out_path = out.Buffer.?[0 .. out.Length / 2]; - @memcpy(path_space.data[0..out_path.len], out_path); - path_space.len = out.Length / 2; - path_space.data[path_space.len] = 0; - - return path_space; -} - -/// Test that the Zig conversion matches the expected_path (for instances where -/// the Zig implementation intentionally diverges from what RtlDosPathNameToNtPathName_U does). -fn testToPrefixedFileNoOracle(comptime path: []const u8, comptime expected_path: []const u8) !void { - const path_utf16 = std.unicode.utf8ToUtf16LeStringLiteral(path); - const expected_path_utf16 = std.unicode.utf8ToUtf16LeStringLiteral(expected_path); - const actual_path = try windows.wToPrefixedFileW(null, path_utf16); - std.testing.expectEqualSlices(u16, expected_path_utf16, actual_path.span()) catch |e| { - std.debug.print("got '{f}', expected '{f}'\n", .{ std.unicode.fmtUtf16Le(actual_path.span()), std.unicode.fmtUtf16Le(expected_path_utf16) }); - return e; - }; -} - -/// Test that the Zig conversion matches the expected_path and that the -/// expected_path matches the conversion that RtlDosPathNameToNtPathName_U does. -fn testToPrefixedFileWithOracle(comptime path: []const u8, comptime expected_path: []const u8) !void { - try testToPrefixedFileNoOracle(path, expected_path); - try testToPrefixedFileOnlyOracle(path); -} - -/// Test that the Zig conversion matches the conversion that RtlDosPathNameToNtPathName_U does. -fn testToPrefixedFileOnlyOracle(comptime path: []const u8) !void { - const path_utf16 = std.unicode.utf8ToUtf16LeStringLiteral(path); - const zig_result = try windows.wToPrefixedFileW(null, path_utf16); - const win32_api_result = try RtlDosPathNameToNtPathName_U(path_utf16); - std.testing.expectEqualSlices(u16, win32_api_result.span(), zig_result.span()) catch |e| { - std.debug.print("got '{f}', expected '{f}'\n", .{ std.unicode.fmtUtf16Le(zig_result.span()), std.unicode.fmtUtf16Le(win32_api_result.span()) }); - return e; - }; -} - -test "toPrefixedFileW" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - // Most test cases come from https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html - // Note that these tests do not actually touch the filesystem or care about whether or not - // any of the paths actually exist or are otherwise valid. - - // Drive Absolute - try testToPrefixedFileWithOracle("X:\\ABC\\DEF", "\\??\\X:\\ABC\\DEF"); - try testToPrefixedFileWithOracle("X:\\", "\\??\\X:\\"); - try testToPrefixedFileWithOracle("X:\\ABC\\", "\\??\\X:\\ABC\\"); - // Trailing . and space characters are stripped - try testToPrefixedFileWithOracle("X:\\ABC\\DEF. .", "\\??\\X:\\ABC\\DEF"); - try testToPrefixedFileWithOracle("X:/ABC/DEF", "\\??\\X:\\ABC\\DEF"); - try testToPrefixedFileWithOracle("X:\\ABC\\..\\XYZ", "\\??\\X:\\XYZ"); - try testToPrefixedFileWithOracle("X:\\ABC\\..\\..\\..", "\\??\\X:\\"); - // Drive letter casing is unchanged - try testToPrefixedFileWithOracle("x:\\", "\\??\\x:\\"); - - // Drive Relative - // These tests depend on the CWD of the specified drive letter which can vary, - // so instead we just test that the Zig implementation matches the result of - // RtlDosPathNameToNtPathName_U. - // TODO: Setting the =X: environment variable didn't seem to affect - // RtlDosPathNameToNtPathName_U, not sure why that is but getting that - // to work could be an avenue to making these cases environment-independent. - // All -> are examples of the result if the X drive's cwd was X:\ABC - try testToPrefixedFileOnlyOracle("X:DEF\\GHI"); // -> \??\X:\ABC\DEF\GHI - try testToPrefixedFileOnlyOracle("X:"); // -> \??\X:\ABC - try testToPrefixedFileOnlyOracle("X:DEF. ."); // -> \??\X:\ABC\DEF - try testToPrefixedFileOnlyOracle("X:ABC\\..\\XYZ"); // -> \??\X:\ABC\XYZ - try testToPrefixedFileOnlyOracle("X:ABC\\..\\..\\.."); // -> \??\X:\ - try testToPrefixedFileOnlyOracle("x:"); // -> \??\X:\ABC - - // Rooted - // These tests depend on the drive letter of the CWD which can vary, so - // instead we just test that the Zig implementation matches the result of - // RtlDosPathNameToNtPathName_U. - // TODO: Getting the CWD path, getting the drive letter from it, and using it to - // construct the expected NT paths could be an avenue to making these cases - // environment-independent and therefore able to use testToPrefixedFileWithOracle. - // All -> are examples of the result if the CWD's drive letter was X - try testToPrefixedFileOnlyOracle("\\ABC\\DEF"); // -> \??\X:\ABC\DEF - try testToPrefixedFileOnlyOracle("\\"); // -> \??\X:\ - try testToPrefixedFileOnlyOracle("\\ABC\\DEF. ."); // -> \??\X:\ABC\DEF - try testToPrefixedFileOnlyOracle("/ABC/DEF"); // -> \??\X:\ABC\DEF - try testToPrefixedFileOnlyOracle("\\ABC\\..\\XYZ"); // -> \??\X:\XYZ - try testToPrefixedFileOnlyOracle("\\ABC\\..\\..\\.."); // -> \??\X:\ - - // Relative - // These cases differ in functionality to RtlDosPathNameToNtPathName_U. - // Relative paths remain relative if they don't have enough .. components - // to error with TooManyParentDirs - try testToPrefixedFileNoOracle("ABC\\DEF", "ABC\\DEF"); - // TODO: enable this if trailing . and spaces are stripped from relative paths - //try testToPrefixedFileNoOracle("ABC\\DEF. .", "ABC\\DEF"); - try testToPrefixedFileNoOracle("ABC/DEF", "ABC\\DEF"); - try testToPrefixedFileNoOracle("./ABC/.././DEF", "DEF"); - // TooManyParentDirs, so resolved relative to the CWD - // All -> are examples of the result if the CWD was X:\ABC\DEF - try testToPrefixedFileOnlyOracle("..\\GHI"); // -> \??\X:\ABC\GHI - try testToPrefixedFileOnlyOracle("GHI\\..\\..\\.."); // -> \??\X:\ - - // UNC Absolute - try testToPrefixedFileWithOracle("\\\\server\\share\\ABC\\DEF", "\\??\\UNC\\server\\share\\ABC\\DEF"); - try testToPrefixedFileWithOracle("\\\\server", "\\??\\UNC\\server"); - try testToPrefixedFileWithOracle("\\\\server\\share", "\\??\\UNC\\server\\share"); - try testToPrefixedFileWithOracle("\\\\server\\share\\ABC. .", "\\??\\UNC\\server\\share\\ABC"); - try testToPrefixedFileWithOracle("//server/share/ABC/DEF", "\\??\\UNC\\server\\share\\ABC\\DEF"); - try testToPrefixedFileWithOracle("\\\\server\\share\\ABC\\..\\XYZ", "\\??\\UNC\\server\\share\\XYZ"); - try testToPrefixedFileWithOracle("\\\\server\\share\\ABC\\..\\..\\..", "\\??\\UNC\\server\\share"); - - // Local Device - try testToPrefixedFileWithOracle("\\\\.\\COM20", "\\??\\COM20"); - try testToPrefixedFileWithOracle("\\\\.\\pipe\\mypipe", "\\??\\pipe\\mypipe"); - try testToPrefixedFileWithOracle("\\\\.\\X:\\ABC\\DEF. .", "\\??\\X:\\ABC\\DEF"); - try testToPrefixedFileWithOracle("\\\\.\\X:/ABC/DEF", "\\??\\X:\\ABC\\DEF"); - try testToPrefixedFileWithOracle("\\\\.\\X:\\ABC\\..\\XYZ", "\\??\\X:\\XYZ"); - // Can replace the first component of the path (contrary to drive absolute and UNC absolute paths) - try testToPrefixedFileWithOracle("\\\\.\\X:\\ABC\\..\\..\\C:\\", "\\??\\C:\\"); - try testToPrefixedFileWithOracle("\\\\.\\pipe\\mypipe\\..\\notmine", "\\??\\pipe\\notmine"); - - // Special-case device names - // TODO: Enable once these are supported - // more cases to test here: https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html - //try testToPrefixedFileWithOracle("COM1", "\\??\\COM1"); - // Sometimes the special-cased device names are not respected - try testToPrefixedFileWithOracle("\\\\.\\X:\\COM1", "\\??\\X:\\COM1"); - try testToPrefixedFileWithOracle("\\\\abc\\xyz\\COM1", "\\??\\UNC\\abc\\xyz\\COM1"); - - // Verbatim - // Left untouched except \\?\ is replaced by \??\ - try testToPrefixedFileWithOracle("\\\\?\\X:", "\\??\\X:"); - try testToPrefixedFileWithOracle("\\\\?\\X:\\COM1", "\\??\\X:\\COM1"); - try testToPrefixedFileWithOracle("\\\\?\\X:/ABC/DEF. .", "\\??\\X:/ABC/DEF. ."); - try testToPrefixedFileWithOracle("\\\\?\\X:\\ABC\\..\\..\\..", "\\??\\X:\\ABC\\..\\..\\.."); - // NT Namespace - // Fully unmodified - try testToPrefixedFileWithOracle("\\??\\X:", "\\??\\X:"); - try testToPrefixedFileWithOracle("\\??\\X:\\COM1", "\\??\\X:\\COM1"); - try testToPrefixedFileWithOracle("\\??\\X:/ABC/DEF. .", "\\??\\X:/ABC/DEF. ."); - try testToPrefixedFileWithOracle("\\??\\X:\\ABC\\..\\..\\..", "\\??\\X:\\ABC\\..\\..\\.."); - - // 'Fake' Verbatim - // If the prefix looks like the verbatim prefix but not all path separators in the - // prefix are backslashes, then it gets canonicalized and the prefix is dropped in favor - // of the NT prefix. - try testToPrefixedFileWithOracle("//?/C:/ABC", "\\??\\C:\\ABC"); - // 'Fake' NT - // If the prefix looks like the NT prefix but not all path separators in the prefix - // are backslashes, then it gets canonicalized and the /??/ is not dropped but - // rather treated as part of the path. In other words, the path is treated - // as a rooted path, so the final path is resolved relative to the CWD's - // drive letter. - // The -> shows an example of the result if the CWD's drive letter was X - try testToPrefixedFileOnlyOracle("/??/C:/ABC"); // -> \??\X:\??\C:\ABC - - // Root Local Device - // \\. and \\? always get converted to \??\ - try testToPrefixedFileWithOracle("\\\\.", "\\??\\"); - try testToPrefixedFileWithOracle("\\\\?", "\\??\\"); - try testToPrefixedFileWithOracle("//?", "\\??\\"); - try testToPrefixedFileWithOracle("//.", "\\??\\"); -} - -fn testRemoveDotDirs(str: []const u8, expected: []const u8) !void { - const mutable = try testing.allocator.dupe(u8, str); - defer testing.allocator.free(mutable); - const actual = mutable[0..try windows.removeDotDirsSanitized(u8, mutable)]; - try testing.expect(mem.eql(u8, actual, expected)); -} -fn testRemoveDotDirsError(err: anyerror, str: []const u8) !void { - const mutable = try testing.allocator.dupe(u8, str); - defer testing.allocator.free(mutable); - try testing.expectError(err, windows.removeDotDirsSanitized(u8, mutable)); -} -test "removeDotDirs" { - try testRemoveDotDirs("", ""); - try testRemoveDotDirs(".", ""); - try testRemoveDotDirs(".\\", ""); - try testRemoveDotDirs(".\\.", ""); - try testRemoveDotDirs(".\\.\\", ""); - try testRemoveDotDirs(".\\.\\.", ""); - - try testRemoveDotDirs("a", "a"); - try testRemoveDotDirs("a\\", "a\\"); - try testRemoveDotDirs("a\\b", "a\\b"); - try testRemoveDotDirs("a\\.", "a\\"); - try testRemoveDotDirs("a\\b\\.", "a\\b\\"); - try testRemoveDotDirs("a\\.\\b", "a\\b"); - - try testRemoveDotDirs(".a", ".a"); - try testRemoveDotDirs(".a\\", ".a\\"); - try testRemoveDotDirs(".a\\.b", ".a\\.b"); - try testRemoveDotDirs(".a\\.", ".a\\"); - try testRemoveDotDirs(".a\\.\\.", ".a\\"); - try testRemoveDotDirs(".a\\.\\.\\.b", ".a\\.b"); - try testRemoveDotDirs(".a\\.\\.\\.b\\", ".a\\.b\\"); - - try testRemoveDotDirsError(error.TooManyParentDirs, ".."); - try testRemoveDotDirsError(error.TooManyParentDirs, "..\\"); - try testRemoveDotDirsError(error.TooManyParentDirs, ".\\..\\"); - try testRemoveDotDirsError(error.TooManyParentDirs, ".\\.\\..\\"); - - try testRemoveDotDirs("a\\..", ""); - try testRemoveDotDirs("a\\..\\", ""); - try testRemoveDotDirs("a\\..\\.", ""); - try testRemoveDotDirs("a\\..\\.\\", ""); - try testRemoveDotDirs("a\\..\\.\\.", ""); - try testRemoveDotDirsError(error.TooManyParentDirs, "a\\..\\.\\.\\.."); - - try testRemoveDotDirs("a\\..\\.\\.\\b", "b"); - try testRemoveDotDirs("a\\..\\.\\.\\b\\", "b\\"); - try testRemoveDotDirs("a\\..\\.\\.\\b\\.", "b\\"); - try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\", "b\\"); - try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\..", ""); - try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\..\\", ""); - try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\..\\.", ""); - try testRemoveDotDirsError(error.TooManyParentDirs, "a\\..\\.\\.\\b\\.\\..\\.\\.."); - - try testRemoveDotDirs("a\\b\\..\\", "a\\"); - try testRemoveDotDirs("a\\b\\..\\c", "a\\c"); -} - -const RTL_PATH_TYPE = enum(c_int) { - Unknown, - UncAbsolute, - DriveAbsolute, - DriveRelative, - Rooted, - Relative, - LocalDevice, - RootLocalDevice, -}; - -pub extern "ntdll" fn RtlDetermineDosPathNameType_U( - Path: [*:0]const u16, -) callconv(.winapi) RTL_PATH_TYPE; - -test "getWin32PathType vs RtlDetermineDosPathNameType_U" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - var buf: std.ArrayList(u16) = .empty; - defer buf.deinit(std.testing.allocator); - - var wtf8_buf: std.ArrayList(u8) = .empty; - defer wtf8_buf.deinit(std.testing.allocator); - - var random = std.Random.DefaultPrng.init(std.testing.random_seed); - const rand = random.random(); - - for (0..1000) |_| { - buf.clearRetainingCapacity(); - const path = try getRandomWtf16Path(std.testing.allocator, &buf, rand); - wtf8_buf.clearRetainingCapacity(); - const wtf8_len = std.unicode.calcWtf8Len(path); - try wtf8_buf.ensureTotalCapacity(std.testing.allocator, wtf8_len); - wtf8_buf.items.len = wtf8_len; - std.debug.assert(std.unicode.wtf16LeToWtf8(wtf8_buf.items, path) == wtf8_len); - - const windows_type = RtlDetermineDosPathNameType_U(path); - const wtf16_type = std.fs.path.getWin32PathType(u16, path); - const wtf8_type = std.fs.path.getWin32PathType(u8, wtf8_buf.items); - - checkPathType(windows_type, wtf16_type) catch |err| { - std.debug.print("expected type {}, got {} for path: {f}\n", .{ windows_type, wtf16_type, std.unicode.fmtUtf16Le(path) }); - std.debug.print("path bytes:\n", .{}); - std.debug.dumpHex(std.mem.sliceAsBytes(path)); - return err; - }; - - if (wtf16_type != wtf8_type) { - std.debug.print("type mismatch between wtf8: {} and wtf16: {} for path: {f}\n", .{ wtf8_type, wtf16_type, std.unicode.fmtUtf16Le(path) }); - std.debug.print("wtf-16 path bytes:\n", .{}); - std.debug.dumpHex(std.mem.sliceAsBytes(path)); - std.debug.print("wtf-8 path bytes:\n", .{}); - std.debug.dumpHex(std.mem.sliceAsBytes(wtf8_buf.items)); - return error.Wtf8Wtf16Mismatch; - } - } -} - -fn checkPathType(windows_type: RTL_PATH_TYPE, zig_type: std.fs.path.Win32PathType) !void { - const expected_windows_type: RTL_PATH_TYPE = switch (zig_type) { - .unc_absolute => .UncAbsolute, - .drive_absolute => .DriveAbsolute, - .drive_relative => .DriveRelative, - .rooted => .Rooted, - .relative => .Relative, - .local_device => .LocalDevice, - .root_local_device => .RootLocalDevice, - }; - if (windows_type != expected_windows_type) return error.PathTypeMismatch; -} - -fn getRandomWtf16Path(allocator: std.mem.Allocator, buf: *std.ArrayList(u16), rand: std.Random) ![:0]const u16 { - const Choice = enum { - backslash, - slash, - control, - printable, - non_ascii, - }; - - const choices = rand.uintAtMostBiased(u16, 32); - - for (0..choices) |_| { - const choice = rand.enumValue(Choice); - const code_unit = switch (choice) { - .backslash => '\\', - .slash => '/', - .control => switch (rand.uintAtMostBiased(u8, 0x20)) { - 0x20 => '\x7F', - else => |b| b + 1, // no NUL - }, - .printable => '!' + rand.uintAtMostBiased(u8, '~' - '!'), - .non_ascii => rand.intRangeAtMostBiased(u16, 0x80, 0xFFFF), - }; - try buf.append(allocator, std.mem.nativeToLittle(u16, code_unit)); - } - - try buf.append(allocator, 0); - return buf.items[0 .. buf.items.len - 1 :0]; -} diff --git a/lib/std/zig/parser_test.zig b/lib/std/zig/parser_test.zig index 1f11956330..46aa82f10f 100644 --- a/lib/std/zig/parser_test.zig +++ b/lib/std/zig/parser_test.zig @@ -639,7 +639,7 @@ test "zig fmt: array types last token" { test "zig fmt: sentinel-terminated array type" { try testCanonical( - \\pub fn cStrToPrefixedFileW(s: [*:0]const u8) ![PATH_MAX_WIDE:0]u16 { + \\pub fn foobar(s: [*:0]const u8) ![PATH_MAX_WIDE:0]u16 { \\ return sliceToPrefixedFileW(mem.toSliceConst(u8, s)); \\} \\ diff --git a/test/standalone/load_dynamic_library/build.zig b/test/standalone/load_dynamic_library/build.zig index 305ffde565..2160b2b3e4 100644 --- a/test/standalone/load_dynamic_library/build.zig +++ b/test/standalone/load_dynamic_library/build.zig @@ -9,6 +9,7 @@ pub fn build(b: *std.Build) void { const target = b.graph.host; if (builtin.os.tag == .wasi) return; + if (builtin.os.tag == .windows) return; const lib = b.addLibrary(.{ .linkage = .dynamic,