authorgravatar for kubkon@jakubkonka.comJakub Konka <kubkon@jakubkonka.com> 2020-08-06 18:51:17+02:00
committergravatar for kubkon@jakubkonka.comJakub Konka <kubkon@jakubkonka.com> 2020-08-07 11:33:05+02:00
logbdda8fa7a818c90803fb64f9784040bf3b9f0a5e
treea4df7ab11a30dd02d1593d74b6576f53ebbb1517
parente8abfef2aa8579121fbd13b8df84e8f68b533fa4

Redo GetFinalPathNameByHandle using DeviceIoControl

This commit reimagines `std.os.windows.GetFinalPathNameByHandle` using `DeviceIoControl` to query the OS mount manager for the DOS (symlink) paths for the given NT volume name. In particular, it uses `IOCTL_MOUNTMGR_QUERY_POINTS` ioctl opcode to query the manager for the available moount points.

4 files changed, 141 insertions(+), 176 deletions(-)

lib/std/os.zig+1-1
......@@ -4094,7 +4094,7 @@ pub fn realpathW(pathname: []const u16, out_buffer: *[MAX_PATH_BYTES]u8) RealPat
40944094 defer w.CloseHandle(h_file);
40954095
40964096 var wide_buf: [w.PATH_MAX_WIDE]u16 = undefined;
4097 const wide_slice = try w.GetFinalPathNameByHandle(h_file, wide_buf[0..]);
4097 const wide_slice = try w.GetFinalPathNameByHandle(h_file, .{}, wide_buf[0..]);
40984098
40994099 // Trust that Windows gives us valid UTF-16LE.
41004100 const end_index = std.unicode.utf16leToUtf8(out_buffer, wide_slice) catch unreachable;
lib/std/os/windows.zig+125-118
......@@ -902,135 +902,142 @@ pub const GetFinalPathNameByHandleError = error{
902902 Unexpected,
903903};
904904
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.
908pub 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.
907pub 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};
946915
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.
921pub 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 }
953935
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 }
955944
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];
961947
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]));
964949
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];
1005954
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]));
10181005
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];
10211010
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];
10241017
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', '\\' };
10271021
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);
10301031
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 }
10341041}
10351042
10361043pub const GetFileSizeError = error{Unexpected};
lib/std/os/windows/bits.zig+15-23
......@@ -1574,28 +1574,20 @@ pub const SYMLINK_FLAG_RELATIVE: ULONG = 0x1;
15741574pub const SYMBOLIC_LINK_FLAG_DIRECTORY: DWORD = 0x1;
15751575pub const SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE: DWORD = 0x2;
15761576
1577pub const OBJECT_INFORMATION_CLASS = extern enum {
1578 ObjectBasicInformation,
1579 ObjectNameInformation,
1580 ObjectTypeInformation,
1581 ObjectAllInformation,
1582 ObjectDataInformation,
1577pub const MOUNTMGR_MOUNT_POINT = extern struct {
1578 SymbolicLinkNameOffset: ULONG,
1579 SymbolicLinkNameLength: USHORT,
1580 Reserved1: USHORT,
1581 UniqueIdOffset: ULONG,
1582 UniqueIdLength: USHORT,
1583 Reserved2: USHORT,
1584 DeviceNameOffset: ULONG,
1585 DeviceNameLength: USHORT,
1586 Reserved3: USHORT,
15831587};
1584pub const OBJECT_NAME_INFORMATION = extern struct {
1585 Name: UNICODE_STRING,
1586};
1587
1588pub const DIRECTORY_QUERY: DWORD = 0x0001;
1589pub const DIRECTORY_TRAVERSE: DWORD = 0x0002;
1590pub const DIRECTORY_CREATE_OBJECT: DWORD = 0x0004;
1591pub const DIRECTORY_CREATE_SUBDIRECTORY: DWORD = 0x0008;
1592pub const DIRECTORY_ALL_ACCESS: DWORD = STANDARD_RIGHTS_REQUIRED | 0xF;
1593
1594pub const OBJDIR_INFORMATION = extern struct {
1595 ObjectName: UNICODE_STRING,
1596 ObjectTypeName: UNICODE_STRING,
1597 Data: [1]BYTE,
1588pub const MOUNTMGR_MOUNT_POINTS = extern struct {
1589 Size: ULONG,
1590 NumberOfMountPoints: ULONG,
1591 MountPoints: [1]MOUNTMGR_MOUNT_POINT,
15981592};
1599
1600pub const SYMBOLIC_LINK_QUERY: DWORD = 0x0001;
1601pub const SYMBOLIC_LINK_ALL_ACCESS: DWORD = STANDARD_RIGHTS_REQUIRED | 0x1;
\ No newline at end of file
1593pub const IOCTL_MOUNTMGR_QUERY_POINTS: ULONG = 0x6d0008;
\ No newline at end of file
lib/std/os/windows/ntdll.zig-34
......@@ -106,37 +106,3 @@ pub extern "NtDll" fn NtWaitForKeyedEvent(
106106 Alertable: BOOLEAN,
107107 Timeout: ?*LARGE_INTEGER,
108108) callconv(.Stdcall) NTSTATUS;
109
110pub extern "NtDll" fn NtQueryObject(
111 Handle: HANDLE,
112 ObjectInformationClass: OBJECT_INFORMATION_CLASS,
113 ObjectInformation: *c_void,
114 ObjectInformationLength: ULONG,
115 ReturnLength: *ULONG,
116) callconv(.Stdcall) NTSTATUS;
117
118pub extern "NtDll" fn NtOpenSymbolicLinkObject(
119 pHandle: *HANDLE,
120 DesiredAccess: DWORD,
121 ObjectAttributes: OBJECT_ATTRIBUTES,
122) callconv(.Stdcall) NTSTATUS;
123pub extern "NtDll" fn NtQuerySymbolicLinkObject(
124 SymbolicLinkHandle: HANDLE,
125 pLinkName: *UNICODE_STRING,
126 pDataWritten: ?*ULONG,
127) callconv(.Stdcall) NTSTATUS;
128
129pub extern "NtDll" fn NtOpenDirectoryObject(
130 DirectoryObjectHandle: *HANDLE,
131 DesiredAccess: DWORD,
132 ObjectAttributes: OBJECT_ATTRIBUTES,
133) callconv(.Stdcall) NTSTATUS;
134pub extern "NtDll" fn NtQueryDirectoryObject(
135 DirectoryHandle: HANDLE,
136 Buffer: ?*c_void,
137 Length: ULONG,
138 ReturnSingleEntry: BOOLEAN,
139 RestartScan: BOOLEAN,
140 Context: *ULONG,
141 ReturnLength: *ULONG,
142) callconv(.Stdcall) NTSTATUS;
\ No newline at end of file