| ... | ... | @@ -902,135 +902,142 @@ pub const GetFinalPathNameByHandleError = error{ |
| 902 | 902 | Unexpected, |
| 903 | 903 | }; |
| 904 | 904 | |
| 905 | | /// Returns canonical (normalized) path of handle. The output path assumes |
| 906 | | /// Win32 namespace, however, '\\?\' prefix is *not* prepended to the result. |
| 907 | | /// TODO support other namespaces/volume names. |
| 908 | | pub fn GetFinalPathNameByHandle(hFile: HANDLE, out_buffer: []u16) GetFinalPathNameByHandleError![]u16 { |
| 909 | | // The implementation is based on implementation found in Wine sources, however, |
| 910 | | // we make the following tweaks. First of all, if `NtQueryInformationFile` supports |
| 911 | | // `FILE_INFORMATION_CLASS.FileNormalizedNameInformation` and `FileVolumeNameInformation`, |
| 912 | | // we use those two calls to generate a valid Win32/DOS path. Otherwise, we fallback to |
| 913 | | // more widely supported `NtQueryObject` generic routine. |
| 914 | | // Wine source: https://source.winehq.com/git/wine.git/blob/HEAD:/dlls/kernelbase/file.c#l1708 |
| 915 | | var buffer: [PATH_MAX_WIDE]u16 = undefined; |
| 916 | | const object_path = blk: { |
| 917 | | var path_buffer: [PATH_MAX_WIDE * 2]u8 = undefined; |
| 918 | | var io: IO_STATUS_BLOCK = undefined; |
| 919 | | var rc = ntdll.NtQueryInformationFile(hFile, &io, &path_buffer, path_buffer.len, FILE_INFORMATION_CLASS.FileNormalizedNameInformation); |
| 920 | | switch (rc) { |
| 921 | | .SUCCESS => { |
| 922 | | var nt_volume_buffer: [MAX_PATH]u8 = undefined; |
| 923 | | rc = ntdll.NtQueryInformationFile(hFile, &io, &nt_volume_buffer, nt_volume_buffer.len, FILE_INFORMATION_CLASS.FileVolumeNameInformation); |
| 924 | | switch (rc) { |
| 925 | | .SUCCESS => { |
| 926 | | const file_name = @ptrCast(*const FILE_NAME_INFORMATION, @alignCast(@alignOf(FILE_NAME_INFORMATION), &path_buffer[0])); |
| 927 | | const file_name_u16 = @ptrCast([*]const u16, &file_name.FileName[0])[0..file_name.FileNameLength / 2]; |
| 928 | | |
| 929 | | if (file_name_u16.len > PATH_MAX_WIDE) return error.NameTooLong; |
| 930 | | |
| 931 | | const volume_name = @ptrCast(*const FILE_NAME_INFORMATION, @alignCast(@alignOf(FILE_NAME_INFORMATION), &nt_volume_buffer[0])); |
| 932 | | const volume_name_u16 = @ptrCast([*]const u16, &volume_name.FileName[0])[0..volume_name.FileNameLength / 2]; |
| 933 | | |
| 934 | | std.mem.copy(u16, buffer[0..], volume_name_u16); |
| 935 | | std.mem.copy(u16, buffer[volume_name_u16.len..], file_name_u16); |
| 936 | | |
| 937 | | break :blk buffer[0..volume_name_u16.len + file_name_u16.len]; |
| 938 | | }, |
| 939 | | .INVALID_PARAMETER => {}, // fall through |
| 940 | | else => return unexpectedStatus(rc), |
| 941 | | } |
| 942 | | }, |
| 943 | | .INVALID_PARAMETER => {}, // fall through |
| 944 | | else => return unexpectedStatus(rc), |
| 945 | | } |
| 905 | /// Specifies how to format volume path in the result of `GetFinalPathNameByHandle`. |
| 906 | /// Defaults to DOS volume names. |
| 907 | pub const GetFinalPathNameByHandleFormat = struct { |
| 908 | volume_name: enum { |
| 909 | /// Format as DOS volume name |
| 910 | Dos, |
| 911 | /// Format as NT volume name |
| 912 | Nt, |
| 913 | } = .Dos, |
| 914 | }; |
| 946 | 915 | |
| 947 | | var dummy: ULONG = undefined; |
| 948 | | rc = ntdll.NtQueryObject(hFile, OBJECT_INFORMATION_CLASS.ObjectNameInformation, &path_buffer, path_buffer.len, &dummy); |
| 949 | | switch (rc) { |
| 950 | | .SUCCESS => {}, |
| 951 | | else => return unexpectedStatus(rc), |
| 952 | | } |
| 916 | /// Returns canonical (normalized) path of handle. |
| 917 | /// Use `GetFinalPathNameByHandleFormat` to specify whether the path is meant to include |
| 918 | /// NT or DOS volume name (e.g., `\Device\HarddiskVolume0\foo.txt` versus `C:\foo.txt`). |
| 919 | /// If DOS volume name format is selected, note that this function does *not* prepend |
| 920 | /// `\\?\` prefix to the resultant path. |
| 921 | pub fn GetFinalPathNameByHandle( |
| 922 | hFile: HANDLE, |
| 923 | fmt: GetFinalPathNameByHandleFormat, |
| 924 | out_buffer: []u16, |
| 925 | ) GetFinalPathNameByHandleError![]u16 { |
| 926 | // Get normalized path; doesn't include volume name though. |
| 927 | var path_buffer: [@sizeOf(FILE_NAME_INFORMATION) + PATH_MAX_WIDE * 2 + 2]u8 = undefined; |
| 928 | var io: IO_STATUS_BLOCK = undefined; |
| 929 | var rc = ntdll.NtQueryInformationFile(hFile, &io, &path_buffer, path_buffer.len, FILE_INFORMATION_CLASS.FileNormalizedNameInformation); |
| 930 | switch (rc) { |
| 931 | .SUCCESS => {}, |
| 932 | .INVALID_PARAMETER => unreachable, |
| 933 | else => return unexpectedStatus(rc), |
| 934 | } |
| 953 | 935 | |
| 954 | | const object_name = @ptrCast(*const OBJECT_NAME_INFORMATION, @alignCast(@alignOf(OBJECT_NAME_INFORMATION), &buffer)); |
| 936 | // Get NT volume name. |
| 937 | var volume_buffer: [MAX_PATH]u8 = undefined; // MAX_PATH bytes should be enough since it's Windows-defined name |
| 938 | rc = ntdll.NtQueryInformationFile(hFile, &io, &volume_buffer, volume_buffer.len, FILE_INFORMATION_CLASS.FileVolumeNameInformation); |
| 939 | switch (rc) { |
| 940 | .SUCCESS => {}, |
| 941 | .INVALID_PARAMETER => unreachable, |
| 942 | else => return unexpectedStatus(rc), |
| 943 | } |
| 955 | 944 | |
| 956 | | // Apparently, `NtQueryObject` has a bug when signalling an error condition. |
| 957 | | // To check for error, i.e., FileNotFound, we check if the result Buffer is non null |
| 958 | | // and Length is greater than zero. |
| 959 | | // Source: https://stackoverflow.com/questions/65170/how-to-get-name-associated-with-open-handle |
| 960 | | if (object_name.Name.Length == 0) return error.FileNotFound; |
| 945 | const file_name = @ptrCast(*const FILE_NAME_INFORMATION, @alignCast(@alignOf(FILE_NAME_INFORMATION), &path_buffer[0])); |
| 946 | const file_name_u16 = @ptrCast([*]const u16, &file_name.FileName[0])[0 .. file_name.FileNameLength / 2]; |
| 961 | 947 | |
| 962 | | break :blk @as([*]const u16, object_name.Name.Buffer)[0..object_name.Name.Length / 2]; |
| 963 | | }; |
| 948 | const volume_name = @ptrCast(*const FILE_NAME_INFORMATION, @alignCast(@alignOf(FILE_NAME_INFORMATION), &volume_buffer[0])); |
| 964 | 949 | |
| 965 | | // By now, we got a a fully-qualified NT path, which we need to translate |
| 966 | | // into a Win32/DOS path, for instance: |
| 967 | | // \Device\HarddiskVolume4\foo => C:\foo |
| 968 | | // |
| 969 | | // NOTE: |
| 970 | | // I couldn't figure out a better way of doing this unfortunately... |
| 971 | | // This snippet below is in part based around `QueryDosDeviceW` implementation |
| 972 | | // found in Wine. The trick with `NtQueryDirectoryObject` for some reason |
| 973 | | // only lists `\DosDevices\Global` as the only SymblinkObject available, which |
| 974 | | // means it's not possible to query the kernel for all available DOS volume name |
| 975 | | // symlinks. |
| 976 | | // TODO investigate! |
| 977 | | // Wine source: https://source.winehq.com/git/wine.git/blob/HEAD:/dlls/kernelbase/volume.c#l1009 |
| 978 | | const dos_drive_letters = &[_]u16{ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' }; |
| 979 | | var query_path = [_]u16{ '\\', 'D', 'o', 's', 'D', 'e', 'v', 'i', 'c', 'e', 's', '\\', 'C', ':' }; |
| 980 | | for (dos_drive_letters) |drive_letter| { |
| 981 | | const drive = &[_]u16{ drive_letter, ':' }; |
| 982 | | std.mem.copy(u16, query_path[query_path.len - 2..], drive[0..]); |
| 983 | | |
| 984 | | var sym_handle: HANDLE = undefined; |
| 985 | | const len_bytes = @intCast(u16, query_path.len) * 2; |
| 986 | | var nt_name = UNICODE_STRING{ |
| 987 | | .Length = len_bytes, |
| 988 | | .MaximumLength = len_bytes, |
| 989 | | .Buffer = @intToPtr([*]u16, @ptrToInt(&query_path)), |
| 990 | | }; |
| 991 | | var attr = OBJECT_ATTRIBUTES{ |
| 992 | | .Length = @sizeOf(OBJECT_ATTRIBUTES), |
| 993 | | .RootDirectory = null, |
| 994 | | .Attributes = 0, |
| 995 | | .ObjectName = &nt_name, |
| 996 | | .SecurityDescriptor = null, |
| 997 | | .SecurityQualityOfService = null, |
| 998 | | }; |
| 999 | | var rc = ntdll.NtOpenSymbolicLinkObject(&sym_handle, SYMBOLIC_LINK_QUERY, attr); |
| 1000 | | switch (rc) { |
| 1001 | | .SUCCESS => {}, |
| 1002 | | .OBJECT_NAME_NOT_FOUND => continue, |
| 1003 | | else => return unexpectedStatus(rc), |
| 1004 | | } |
| 950 | switch (fmt.volume_name) { |
| 951 | .Nt => { |
| 952 | // Nothing to do, we simply copy the bytes to the user-provided buffer. |
| 953 | const volume_name_u16 = @ptrCast([*]const u16, &volume_name.FileName[0])[0 .. volume_name.FileNameLength / 2]; |
| 1005 | 954 | |
| 1006 | | var link_buffer: [MAX_PATH]u8 = undefined; |
| 1007 | | var link = UNICODE_STRING{ |
| 1008 | | .Length = 0, |
| 1009 | | .MaximumLength = MAX_PATH, |
| 1010 | | .Buffer = @intToPtr([*]u16, @ptrToInt(&link_buffer[0])), |
| 1011 | | }; |
| 1012 | | rc = ntdll.NtQuerySymbolicLinkObject(sym_handle, &link, null); |
| 1013 | | CloseHandle(sym_handle); |
| 1014 | | switch (rc) { |
| 1015 | | .SUCCESS => {}, |
| 1016 | | else => return unexpectedStatus(rc), |
| 1017 | | } |
| 955 | if (out_buffer.len < volume_name_u16.len + file_name_u16.len) return error.NameTooLong; |
| 956 | |
| 957 | std.mem.copy(u16, out_buffer[0..], volume_name_u16); |
| 958 | std.mem.copy(u16, out_buffer[volume_name_u16.len..], file_name_u16); |
| 959 | |
| 960 | return out_buffer[0 .. volume_name_u16.len + file_name_u16.len]; |
| 961 | }, |
| 962 | .Dos => { |
| 963 | // Get DOS volume name. DOS volume names are actually symbolic link objects to the |
| 964 | // actual NT volume. For example: |
| 965 | // (NT) \Device\HarddiskVolume4 => (DOS) \DosDevices\C: == (DOS) C: |
| 966 | const MIN_SIZE = @sizeOf(MOUNTMGR_MOUNT_POINT) + MAX_PATH; |
| 967 | // We initialize the input buffer to all zeros for convenience since |
| 968 | // `DeviceIoControl` with `IOCTL_MOUNTMGR_QUERY_POINTS` expects this. |
| 969 | var input_buf = [_]u8{0} ** MIN_SIZE; |
| 970 | var output_buf: [MIN_SIZE * 4]u8 = undefined; |
| 971 | |
| 972 | // This surprising path is a filesystem path to the mount manager on Windows. |
| 973 | // Source: https://stackoverflow.com/questions/3012828/using-ioctl-mountmgr-query-points |
| 974 | const mgmt_path = "\\MountPointManager"; |
| 975 | const mgmt_path_u16 = sliceToPrefixedFileW(mgmt_path) catch unreachable; |
| 976 | const mgmt_handle = OpenFile(mgmt_path_u16.span(), .{ |
| 977 | .access_mask = SYNCHRONIZE, |
| 978 | .share_access = FILE_SHARE_READ | FILE_SHARE_WRITE, |
| 979 | .creation = FILE_OPEN, |
| 980 | .io_mode = .blocking, |
| 981 | }) catch |err| switch (err) { |
| 982 | error.IsDir => unreachable, |
| 983 | error.NotDir => unreachable, |
| 984 | error.NoDevice => unreachable, |
| 985 | error.AccessDenied => unreachable, |
| 986 | error.PipeBusy => unreachable, |
| 987 | error.PathAlreadyExists => unreachable, |
| 988 | error.WouldBlock => unreachable, |
| 989 | else => |e| return e, |
| 990 | }; |
| 991 | defer CloseHandle(mgmt_handle); |
| 992 | |
| 993 | var input_struct = @ptrCast(*MOUNTMGR_MOUNT_POINT, @alignCast(@alignOf(MOUNTMGR_MOUNT_POINT), &input_buf[0])); |
| 994 | input_struct.DeviceNameOffset = @sizeOf(MOUNTMGR_MOUNT_POINT); |
| 995 | input_struct.DeviceNameLength = @intCast(USHORT, volume_name.FileNameLength); |
| 996 | @memcpy(input_buf[@sizeOf(MOUNTMGR_MOUNT_POINT)..], @ptrCast([*]const u8, &volume_name.FileName[0]), volume_name.FileNameLength); |
| 997 | |
| 998 | try DeviceIoControl( |
| 999 | mgmt_handle, |
| 1000 | IOCTL_MOUNTMGR_QUERY_POINTS, |
| 1001 | input_buf[0 .. @sizeOf(MOUNTMGR_MOUNT_POINT) + volume_name.FileNameLength], |
| 1002 | output_buf[0..], |
| 1003 | ); |
| 1004 | const mount_points_struct = @ptrCast(*const MOUNTMGR_MOUNT_POINTS, @alignCast(@alignOf(MOUNTMGR_MOUNT_POINTS), &output_buf[0])); |
| 1018 | 1005 | |
| 1019 | | const link_path = @as([*]const u16, link.Buffer)[0..link.Length / 2]; |
| 1020 | | const idx = std.mem.indexOf(u16, object_path, link_path) orelse continue; |
| 1006 | const mount_points = @ptrCast( |
| 1007 | [*]const MOUNTMGR_MOUNT_POINT, |
| 1008 | @alignCast(@alignOf(MOUNTMGR_MOUNT_POINT), &mount_points_struct.MountPoints[0]), |
| 1009 | )[0..mount_points_struct.NumberOfMountPoints]; |
| 1021 | 1010 | |
| 1022 | | // TODO is this the most appropriate error here? |
| 1023 | | if (out_buffer.len < drive.len + object_path.len) return error.NameTooLong; |
| 1011 | var found: bool = false; |
| 1012 | for (mount_points) |mount_point| { |
| 1013 | const symlink = @ptrCast( |
| 1014 | [*]const u16, |
| 1015 | @alignCast(@alignOf(u16), &output_buf[mount_point.SymbolicLinkNameOffset]), |
| 1016 | )[0 .. mount_point.SymbolicLinkNameLength / 2]; |
| 1024 | 1017 | |
| 1025 | | std.mem.copy(u16, out_buffer[0..], drive[0..]); |
| 1026 | | std.mem.copy(u16, out_buffer[2..], object_path[link_path.len..]); |
| 1018 | // Look for `\DosDevices\` prefix. We don't really care if there are more than one symlinks |
| 1019 | // with traditional DOS drive letters, so pick the first one available. |
| 1020 | const prefix = &[_]u16{ '\\', 'D', 'o', 's', 'D', 'e', 'v', 'i', 'c', 'e', 's', '\\' }; |
| 1027 | 1021 | |
| 1028 | | return out_buffer[0..object_path.len - link_path.len + 2]; |
| 1029 | | } |
| 1022 | if (std.mem.indexOf(u16, symlink, prefix)) |idx| { |
| 1023 | if (idx != 0) continue; |
| 1024 | |
| 1025 | const drive_letter = symlink[prefix.len..]; |
| 1026 | |
| 1027 | if (out_buffer.len < drive_letter.len + file_name_u16.len) return error.NameTooLong; |
| 1028 | |
| 1029 | std.mem.copy(u16, out_buffer[0..], drive_letter); |
| 1030 | std.mem.copy(u16, out_buffer[drive_letter.len..], file_name_u16); |
| 1030 | 1031 | |
| 1031 | | // If we're here, that means there was no match so we panic! |
| 1032 | | // TODO should we actually panic here or return an error instead? |
| 1033 | | unreachable; |
| 1032 | return out_buffer[0 .. drive_letter.len + file_name_u16.len]; |
| 1033 | } |
| 1034 | } |
| 1035 | |
| 1036 | // If we've ended up here, then something went wrong/is corrupted in the OS, |
| 1037 | // so error out! |
| 1038 | return error.FileNotFound; |
| 1039 | }, |
| 1040 | } |
| 1034 | 1041 | } |
| 1035 | 1042 | |
| 1036 | 1043 | pub const GetFileSizeError = error{Unexpected}; |