| ... | @@ -902,135 +902,142 @@ pub const GetFinalPathNameByHandleError = error{ | ... | @@ -902,135 +902,142 @@ pub const GetFinalPathNameByHandleError = error{ |
| 902 | Unexpected, | 902 | Unexpected, |
| 903 | }; | 903 | }; |
| 904 | | 904 | |
| 905 | /// Returns canonical (normalized) path of handle. The output path assumes | 905 | /// Specifies how to format volume path in the result of `GetFinalPathNameByHandle`. |
| 906 | /// Win32 namespace, however, '\\?\' prefix is *not* prepended to the result. | 906 | /// Defaults to DOS volume names. |
| 907 | /// TODO support other namespaces/volume names. | 907 | pub const GetFinalPathNameByHandleFormat = struct { |
| 908 | pub fn GetFinalPathNameByHandle(hFile: HANDLE, out_buffer: []u16) GetFinalPathNameByHandleError![]u16 { | 908 | volume_name: enum { |
| 909 | // The implementation is based on implementation found in Wine sources, however, | 909 | /// Format as DOS volume name |
| 910 | // we make the following tweaks. First of all, if `NtQueryInformationFile` supports | 910 | Dos, |
| 911 | // `FILE_INFORMATION_CLASS.FileNormalizedNameInformation` and `FileVolumeNameInformation`, | 911 | /// Format as NT volume name |
| 912 | // we use those two calls to generate a valid Win32/DOS path. Otherwise, we fallback to | 912 | Nt, |
| 913 | // more widely supported `NtQueryObject` generic routine. | 913 | } = .Dos, |
| 914 | // Wine source: https://source.winehq.com/git/wine.git/blob/HEAD:/dlls/kernelbase/file.c#l1708 | 914 | }; |
| 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 | } | | |
| 946 | | 915 | |
| 947 | var dummy: ULONG = undefined; | 916 | /// Returns canonical (normalized) path of handle. |
| 948 | rc = ntdll.NtQueryObject(hFile, OBJECT_INFORMATION_CLASS.ObjectNameInformation, &path_buffer, path_buffer.len, &dummy); | 917 | /// Use `GetFinalPathNameByHandleFormat` to specify whether the path is meant to include |
| 949 | switch (rc) { | 918 | /// NT or DOS volume name (e.g., `\Device\HarddiskVolume0\foo.txt` versus `C:\foo.txt`). |
| 950 | .SUCCESS => {}, | 919 | /// If DOS volume name format is selected, note that this function does *not* prepend |
| 951 | else => return unexpectedStatus(rc), | 920 | /// `\\?\` prefix to the resultant path. |
| 952 | } | 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. | 945 | const file_name = @ptrCast(*const FILE_NAME_INFORMATION, @alignCast(@alignOf(FILE_NAME_INFORMATION), &path_buffer[0])); |
| 957 | // To check for error, i.e., FileNotFound, we check if the result Buffer is non null | 946 | const file_name_u16 = @ptrCast([*]const u16, &file_name.FileName[0])[0 .. file_name.FileNameLength / 2]; |
| 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; | | |
| 961 | | 947 | |
| 962 | break :blk @as([*]const u16, object_name.Name.Buffer)[0..object_name.Name.Length / 2]; | 948 | const volume_name = @ptrCast(*const FILE_NAME_INFORMATION, @alignCast(@alignOf(FILE_NAME_INFORMATION), &volume_buffer[0])); |
| 963 | }; | | |
| 964 | | 949 | |
| 965 | // By now, we got a a fully-qualified NT path, which we need to translate | 950 | switch (fmt.volume_name) { |
| 966 | // into a Win32/DOS path, for instance: | 951 | .Nt => { |
| 967 | // \Device\HarddiskVolume4\foo => C:\foo | 952 | // Nothing to do, we simply copy the bytes to the user-provided buffer. |
| 968 | // | 953 | const volume_name_u16 = @ptrCast([*]const u16, &volume_name.FileName[0])[0 .. volume_name.FileNameLength / 2]; |
| 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 | } | | |
| 1005 | | 954 | |
| 1006 | var link_buffer: [MAX_PATH]u8 = undefined; | 955 | if (out_buffer.len < volume_name_u16.len + file_name_u16.len) return error.NameTooLong; |
| 1007 | var link = UNICODE_STRING{ | 956 | |
| 1008 | .Length = 0, | 957 | std.mem.copy(u16, out_buffer[0..], volume_name_u16); |
| 1009 | .MaximumLength = MAX_PATH, | 958 | std.mem.copy(u16, out_buffer[volume_name_u16.len..], file_name_u16); |
| 1010 | .Buffer = @intToPtr([*]u16, @ptrToInt(&link_buffer[0])), | 959 | |
| 1011 | }; | 960 | return out_buffer[0 .. volume_name_u16.len + file_name_u16.len]; |
| 1012 | rc = ntdll.NtQuerySymbolicLinkObject(sym_handle, &link, null); | 961 | }, |
| 1013 | CloseHandle(sym_handle); | 962 | .Dos => { |
| 1014 | switch (rc) { | 963 | // Get DOS volume name. DOS volume names are actually symbolic link objects to the |
| 1015 | .SUCCESS => {}, | 964 | // actual NT volume. For example: |
| 1016 | else => return unexpectedStatus(rc), | 965 | // (NT) \Device\HarddiskVolume4 => (DOS) \DosDevices\C: == (DOS) C: |
| 1017 | } | 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]; | 1006 | const mount_points = @ptrCast( |
| 1020 | const idx = std.mem.indexOf(u16, object_path, link_path) orelse continue; | 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? | 1011 | var found: bool = false; |
| 1023 | if (out_buffer.len < drive.len + object_path.len) return error.NameTooLong; | 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..]); | 1018 | // Look for `\DosDevices\` prefix. We don't really care if there are more than one symlinks |
| 1026 | std.mem.copy(u16, out_buffer[2..], object_path[link_path.len..]); | 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]; | 1022 | if (std.mem.indexOf(u16, symlink, prefix)) |idx| { |
| 1029 | } | 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 | return out_buffer[0 .. drive_letter.len + file_name_u16.len]; |
| 1032 | // TODO should we actually panic here or return an error instead? | 1033 | } |
| 1033 | unreachable; | 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 | pub const GetFileSizeError = error{Unexpected}; | 1043 | pub const GetFileSizeError = error{Unexpected}; |