authorgravatar for rohlemF@gmail.comRohlem <rohlemF@gmail.com> 2020-12-23 02:39:02+01:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2021-01-11 17:48:18-07:00
logcb205039903868a6273b9f9d65916e943675d540
treebe53c9c8e421fe65bc7756340390ac3705f94382
parent64c5f4979e99b35ef8a2a2c200c095b4d8917328

std.os.windows.GetFinalPathNameByHandle: address non-structural review comments


5 files changed, 80 insertions(+), 87 deletions(-)

lib/std/os/windows.zig+59-58
...@@ -29,7 +29,8 @@ pub const gdi32 = @import("windows/gdi32.zig");...@@ -29,7 +29,8 @@ pub const gdi32 = @import("windows/gdi32.zig");
29pub usingnamespace @import("windows/bits.zig");29pub usingnamespace @import("windows/bits.zig");
3030
31//version detection31//version detection
32usingnamespace std.zig.system.windows;32const version = std.zig.system.windows;
33const WindowsVersion = version.WindowsVersion;
3334
34pub const self_process_handle = @intToPtr(HANDLE, maxInt(usize));35pub const self_process_handle = @intToPtr(HANDLE, maxInt(usize));
3536
...@@ -963,42 +964,44 @@ pub fn QueryObjectName(...@@ -963,42 +964,44 @@ pub fn QueryObjectName(
963 var full_buffer: [@sizeOf(OBJECT_NAME_INFORMATION) + PATH_MAX_WIDE * 2]u8 align(@alignOf(OBJECT_NAME_INFORMATION)) = undefined;964 var full_buffer: [@sizeOf(OBJECT_NAME_INFORMATION) + PATH_MAX_WIDE * 2]u8 align(@alignOf(OBJECT_NAME_INFORMATION)) = undefined;
964 var info = @ptrCast(*OBJECT_NAME_INFORMATION, &full_buffer);965 var info = @ptrCast(*OBJECT_NAME_INFORMATION, &full_buffer);
965 //buffer size is specified in bytes966 //buffer size is specified in bytes
966 const full_buffer_length = @intCast(ULONG, @sizeOf(OBJECT_NAME_INFORMATION) + std.math.min(PATH_MAX_WIDE, (out_buffer.len + 1) * 2));
967 //last argument would return the length required for full_buffer, not exposed here967 //last argument would return the length required for full_buffer, not exposed here
968 const rc = ntdll.NtQueryObject(handle, .ObjectNameInformation, full_buffer[0..], full_buffer_length, null);968 const rc = ntdll.NtQueryObject(handle, .ObjectNameInformation, &full_buffer, full_buffer.len, null);
969 return switch (rc) {969 switch (rc) {
970 .SUCCESS => if (@ptrCast(?[*]WCHAR, info.Name.Buffer)) |buffer| blk: {970 .SUCCESS => {
971 // info.Name.Buffer from ObQueryNameString is documented to be null (and MaximumLength == 0)
972 // if the object was "unnamed", not sure if this can happen for file handles
973 if (info.Name.MaximumLength == 0) return error.Unexpected;
971 //resulting string length is specified in bytes974 //resulting string length is specified in bytes
972 const path_length_unterminated = @divExact(info.Name.Length, 2);975 const path_length_unterminated = @divExact(info.Name.Length, 2);
973 if (out_buffer.len < path_length_unterminated) {976 if (out_buffer.len < path_length_unterminated) {
974 return error.NameTooLong;977 return error.NameTooLong;
975 }978 }
976 std.mem.copy(WCHAR, out_buffer[0..path_length_unterminated], buffer[0..path_length_unterminated :0]);979 mem.copy(WCHAR, out_buffer[0..path_length_unterminated], info.Name.Buffer[0..path_length_unterminated]);
977 break :blk out_buffer[0..path_length_unterminated];980 return out_buffer[0..path_length_unterminated];
978 } else error.Unexpected,981 },
979 .ACCESS_DENIED => error.AccessDenied,982 .ACCESS_DENIED => return error.AccessDenied,
980 .INVALID_HANDLE => error.InvalidHandle,983 .INVALID_HANDLE => return error.InvalidHandle,
981 .BUFFER_OVERFLOW, .BUFFER_TOO_SMALL => error.NameTooLong,984 .BUFFER_OVERFLOW, .BUFFER_TOO_SMALL => return error.NameTooLong,
982 //name_buffer.len >= @sizeOf(OBJECT_NAME_INFORMATION) holds statically985 //name_buffer.len >= @sizeOf(OBJECT_NAME_INFORMATION) holds statically
983 .INFO_LENGTH_MISMATCH => unreachable,986 .INFO_LENGTH_MISMATCH => unreachable,
984 else => |e| unexpectedStatus(e),987 else => |e| return unexpectedStatus(e),
985 };988 }
986}989}
987test "QueryObjectName" {990test "QueryObjectName" {
988 if (comptime builtin.os.tag != .windows)991 if (comptime builtin.os.tag != .windows)
989 return;992 return;
990993
991 //any file will do; canonicalization works on NTFS junctions and symlinks, hardlinks remain separate paths.994 //any file will do; canonicalization works on NTFS junctions and symlinks, hardlinks remain separate paths.
992 const file = try std.fs.openSelfExe(.{});995 var tmp = std.testing.tmpDir(.{});
993 defer file.close();996 defer tmp.cleanup();
994 //make this large enough for the test runner exe path997 const handle = tmp.dir.fd;
995 var out_buffer align(16) = std.mem.zeroes([1 << 10]u16);998 var out_buffer: [PATH_MAX_WIDE]u16 = undefined;
996999
997 var result_path = try QueryObjectName(file.handle, out_buffer[0..]);1000 var result_path = try QueryObjectName(handle, &out_buffer);
998 //insufficient size1001 //insufficient size
999 std.testing.expectError(error.NameTooLong, QueryObjectName(file.handle, out_buffer[0 .. result_path.len - 1]));1002 std.testing.expectError(error.NameTooLong, QueryObjectName(handle, out_buffer[0 .. result_path.len - 1]));
1000 //exactly-sufficient size1003 //exactly-sufficient size
1001 _ = try QueryObjectName(file.handle, out_buffer[0..result_path.len]);1004 _ = try QueryObjectName(handle, out_buffer[0..result_path.len]);
1002}1005}
10031006
1004pub const GetFinalPathNameByHandleError = error{1007pub const GetFinalPathNameByHandleError = error{
...@@ -1030,16 +1033,16 @@ pub fn GetFinalPathNameByHandle(...@@ -1030,16 +1033,16 @@ pub fn GetFinalPathNameByHandle(
1030 fmt: GetFinalPathNameByHandleFormat,1033 fmt: GetFinalPathNameByHandleFormat,
1031 out_buffer: []u16,1034 out_buffer: []u16,
1032) GetFinalPathNameByHandleError![]u16 {1035) GetFinalPathNameByHandleError![]u16 {
1033 var path_buffer: [std.math.max(@sizeOf(FILE_NAME_INFORMATION), @sizeOf(OBJECT_NAME_INFORMATION)) + PATH_MAX_WIDE * 2]u8 align(@alignOf(FILE_NAME_INFORMATION)) = undefined;1036 var path_buffer: [math.max(@sizeOf(FILE_NAME_INFORMATION), @sizeOf(OBJECT_NAME_INFORMATION)) + PATH_MAX_WIDE * 2]u8 align(@alignOf(FILE_NAME_INFORMATION)) = undefined;
1034 var volume_buffer: [@sizeOf(FILE_NAME_INFORMATION) + MAX_PATH]u8 align(@alignOf(FILE_NAME_INFORMATION)) = undefined; // MAX_PATH bytes should be enough since it's Windows-defined name1037 var volume_buffer: [@sizeOf(FILE_NAME_INFORMATION) + MAX_PATH]u8 align(@alignOf(FILE_NAME_INFORMATION)) = undefined; // MAX_PATH bytes should be enough since it's Windows-defined name
10351038
1036 var file_name_u16: []const u16 = undefined;1039 var file_name_u16: []const u16 = undefined;
1037 var volume_name_u16: []const u16 = undefined;1040 var volume_name_u16: []const u16 = undefined;
1038 if ((comptime (targetVersionIsAtLeast(WindowsVersion.win10_rs4) != true)) //need explicit comptime, because error returns affect return type1041 if ((comptime (std.builtin.os.version_range.windows.isAtLeast(WindowsVersion.win10_rs4) != true)) and !version.detectRuntimeVersion().isAtLeast(WindowsVersion.win10_rs4)) {
1039 and !runtimeVersionIsAtLeast(WindowsVersion.win10_rs4))1042 const final_path = QueryObjectName(hFile, mem.bytesAsSlice(u16, &path_buffer)) catch |err| return switch (err) {
1040 {1043 // we assume InvalidHandle is close enough to FileNotFound in semantics
1041 const final_path = QueryObjectName(hFile, std.mem.bytesAsSlice(u16, path_buffer[0..])) catch |err| return switch (err) {1044 // to not further complicate the error set
1042 error.InvalidHandle => error.FileNotFound, //close enough?1045 error.InvalidHandle => error.FileNotFound,
1043 else => |e| e,1046 else => |e| e,
1044 };1047 };
10451048
...@@ -1047,39 +1050,40 @@ pub fn GetFinalPathNameByHandle(...@@ -1047,39 +1050,40 @@ pub fn GetFinalPathNameByHandle(
1047 if (out_buffer.len < final_path.len) {1050 if (out_buffer.len < final_path.len) {
1048 return error.NameTooLong;1051 return error.NameTooLong;
1049 }1052 }
1050 std.mem.copy(u16, out_buffer[0..], final_path[0..]);1053 mem.copy(u16, out_buffer, final_path);
1051 return final_path; //we can directly return the slice we received1054 return out_buffer[0..final_path.len];
1052 }1055 }
10531056
1054 //otherwise we need to parse the string for volume path for the .Dos logic below to work1057 //otherwise we need to parse the string for volume path for the .Dos logic below to work
1055 const expected_prefix = std.unicode.utf8ToUtf16LeStringLiteral("\\Device\\");1058 const expected_prefix = std.unicode.utf8ToUtf16LeStringLiteral("\\Device\\");
1056 if (!std.mem.eql(u16, expected_prefix, final_path[0..expected_prefix.len])) {1059
1057 //TODO find out if this can occur, and if we need to handle it differently1060 // TODO find out if a path can start with something besides `\Device\<volume name>`,
1058 //(i.e. how to determine the end of a volume name)1061 // and if we need to handle it differently
1059 return error.BadPathName;1062 // (i.e. how to determine the start and end of the volume name in that case)
1060 }1063 if (!mem.eql(u16, expected_prefix, final_path[0..expected_prefix.len])) return error.Unexpected;
1061 const index = std.mem.indexOfPos(u16, final_path, expected_prefix.len, &[_]u16{'\\'}) orelse unreachable;1064
1065 const index = mem.indexOfPos(u16, final_path, expected_prefix.len, &[_]u16{'\\'}) orelse unreachable;
1062 volume_name_u16 = final_path[0..index];1066 volume_name_u16 = final_path[0..index];
1063 file_name_u16 = final_path[index..];1067 file_name_u16 = final_path[index..];
10641068
1065 //fallthrough for fmt.volume_name != .Nt1069 //fallthrough for fmt.volume_name != .Nt
1066 } else {1070 } else {
1067 // Get normalized path; doesn't include volume name though.1071 // Get normalized path; doesn't include volume name though.
1068 try QueryInformationFile(hFile, .FileNormalizedNameInformation, path_buffer[0..]);1072 try QueryInformationFile(hFile, .FileNormalizedNameInformation, &path_buffer);
1069 const file_name = @ptrCast(*const FILE_NAME_INFORMATION, &path_buffer[0]);1073 const file_name = @ptrCast(*const FILE_NAME_INFORMATION, &path_buffer);
1070 file_name_u16 = @ptrCast([*]const u16, &file_name.FileName[0])[0..@divExact(file_name.FileNameLength, 2)];1074 file_name_u16 = @ptrCast([*]const u16, &file_name.FileName)[0..@divExact(file_name.FileNameLength, 2)];
10711075
1072 // Get NT volume name.1076 // Get NT volume name.
1073 try QueryInformationFile(hFile, .FileVolumeNameInformation, volume_buffer[0..]);1077 try QueryInformationFile(hFile, .FileVolumeNameInformation, &volume_buffer);
1074 const volume_name_info = @ptrCast(*const FILE_NAME_INFORMATION, &volume_buffer[0]);1078 const volume_name_info = @ptrCast(*const FILE_NAME_INFORMATION, &volume_buffer);
1075 volume_name_u16 = @ptrCast([*]const u16, &volume_name_info.FileName[0])[0..@divExact(volume_name_info.FileNameLength, 2)];1079 volume_name_u16 = @ptrCast([*]const u16, &volume_name_info.FileName)[0..@divExact(volume_name_info.FileNameLength, 2)];
10761080
1077 if (fmt.volume_name == .Nt) {1081 if (fmt.volume_name == .Nt) {
1078 // Nothing to do, we simply copy the bytes to the user-provided buffer.1082 // Nothing to do, we simply copy the bytes to the user-provided buffer.
1079 if (out_buffer.len < volume_name_u16.len + file_name_u16.len) return error.NameTooLong;1083 if (out_buffer.len < volume_name_u16.len + file_name_u16.len) return error.NameTooLong;
10801084
1081 std.mem.copy(u16, out_buffer[0..], volume_name_u16);1085 mem.copy(u16, out_buffer, volume_name_u16);
1082 std.mem.copy(u16, out_buffer[volume_name_u16.len..], file_name_u16);1086 mem.copy(u16, out_buffer[volume_name_u16.len..], file_name_u16);
10831087
1084 return out_buffer[0 .. volume_name_u16.len + file_name_u16.len];1088 return out_buffer[0 .. volume_name_u16.len + file_name_u16.len];
1085 }1089 }
...@@ -1124,7 +1128,7 @@ pub fn GetFinalPathNameByHandle(...@@ -1124,7 +1128,7 @@ pub fn GetFinalPathNameByHandle(
1124 input_struct.DeviceNameLength = @intCast(USHORT, volume_name_u16.len * 2);1128 input_struct.DeviceNameLength = @intCast(USHORT, volume_name_u16.len * 2);
1125 @memcpy(input_buf[@sizeOf(MOUNTMGR_MOUNT_POINT)..], @ptrCast([*]const u8, volume_name_u16.ptr), volume_name_u16.len * 2);1129 @memcpy(input_buf[@sizeOf(MOUNTMGR_MOUNT_POINT)..], @ptrCast([*]const u8, volume_name_u16.ptr), volume_name_u16.len * 2);
11261130
1127 DeviceIoControl(mgmt_handle, IOCTL_MOUNTMGR_QUERY_POINTS, input_buf[0..], output_buf[0..]) catch |err| switch (err) {1131 DeviceIoControl(mgmt_handle, IOCTL_MOUNTMGR_QUERY_POINTS, &input_buf, &output_buf) catch |err| switch (err) {
1128 error.AccessDenied => unreachable,1132 error.AccessDenied => unreachable,
1129 else => |e| return e,1133 else => |e| return e,
1130 };1134 };
...@@ -1144,22 +1148,20 @@ pub fn GetFinalPathNameByHandle(...@@ -1144,22 +1148,20 @@ pub fn GetFinalPathNameByHandle(
11441148
1145 // Look for `\DosDevices\` prefix. We don't really care if there are more than one symlinks1149 // Look for `\DosDevices\` prefix. We don't really care if there are more than one symlinks
1146 // with traditional DOS drive letters, so pick the first one available.1150 // with traditional DOS drive letters, so pick the first one available.
1147 const prefix_u8 = "\\DosDevices\\";1151 var prefix_buf = std.unicode.utf8ToUtf16LeStringLiteral("\\DosDevices\\");
1148 var prefix_buf_u16: [prefix_u8.len]u16 = undefined;1152 const prefix = prefix_buf[0..prefix_buf.len];
1149 const prefix_len_u16 = std.unicode.utf8ToUtf16Le(prefix_buf_u16[0..], prefix_u8[0..]) catch unreachable;
1150 const prefix = prefix_buf_u16[0..prefix_len_u16];
11511153
1152 if (std.mem.startsWith(u16, symlink, prefix)) {1154 if (mem.startsWith(u16, symlink, prefix)) {
1153 const drive_letter = symlink[prefix.len..];1155 const drive_letter = symlink[prefix.len..];
11541156
1155 if (out_buffer.len < drive_letter.len + file_name_u16.len) return error.NameTooLong;1157 if (out_buffer.len < drive_letter.len + file_name_u16.len) return error.NameTooLong;
11561158
1157 std.mem.copy(u16, out_buffer[0..], drive_letter);1159 mem.copy(u16, out_buffer, drive_letter);
1158 std.mem.copy(u16, out_buffer[drive_letter.len..], file_name_u16);1160 mem.copy(u16, out_buffer[drive_letter.len..], file_name_u16);
1159 const total_len = drive_letter.len + file_name_u16.len;1161 const total_len = drive_letter.len + file_name_u16.len;
11601162
1161 // Validate that DOS does not contain any spurious nul bytes.1163 // Validate that DOS does not contain any spurious nul bytes.
1162 if (std.mem.indexOfScalar(u16, out_buffer[0..total_len], 0)) |_| {1164 if (mem.indexOfScalar(u16, out_buffer[0..total_len], 0)) |_| {
1163 return error.BadPathName;1165 return error.BadPathName;
1164 }1166 }
11651167
...@@ -1179,15 +1181,14 @@ test "GetFinalPathNameByHandle" {...@@ -1179,15 +1181,14 @@ test "GetFinalPathNameByHandle" {
1179 return;1181 return;
11801182
1181 //any file will do1183 //any file will do
1182 const file = try std.fs.openSelfExe(.{});1184 var tmp = std.testing.tmpDir(.{});
1183 defer file.close();1185 defer tmp.cleanup();
1184 const handle = file.handle;1186 const handle = tmp.dir.fd;
1185 //make this large enough for the test runner exe path1187 var buffer: [PATH_MAX_WIDE]u16 = undefined;
1186 var buffer = std.mem.zeroes([1 << 10]u16);
11871188
1188 //check with sufficient size1189 //check with sufficient size
1189 const nt_length = (try GetFinalPathNameByHandle(handle, .{ .volume_name = .Nt }, buffer[0..])).len;1190 const nt_length = (try GetFinalPathNameByHandle(handle, .{ .volume_name = .Nt }, &buffer)).len;
1190 const dos_length = (try GetFinalPathNameByHandle(handle, .{ .volume_name = .Dos }, buffer[0..])).len;1191 const dos_length = (try GetFinalPathNameByHandle(handle, .{ .volume_name = .Dos }, &buffer)).len;
11911192
1192 //check with insufficient size1193 //check with insufficient size
1193 std.testing.expectError(error.NameTooLong, GetFinalPathNameByHandle(handle, .{ .volume_name = .Nt }, buffer[0 .. nt_length - 1]));1194 std.testing.expectError(error.NameTooLong, GetFinalPathNameByHandle(handle, .{ .volume_name = .Nt }, buffer[0 .. nt_length - 1]));
lib/std/os/windows/bits.zig-1
...@@ -66,7 +66,6 @@ pub const SIZE_T = usize;...@@ -66,7 +66,6 @@ pub const SIZE_T = usize;
66pub const TCHAR = if (UNICODE) WCHAR else u8;66pub const TCHAR = if (UNICODE) WCHAR else u8;
67pub const UINT = c_uint;67pub const UINT = c_uint;
68pub const ULONG_PTR = usize;68pub const ULONG_PTR = usize;
69pub const PULONG = *ULONG;
70pub const LONG_PTR = isize;69pub const LONG_PTR = isize;
71pub const DWORD_PTR = ULONG_PTR;70pub const DWORD_PTR = ULONG_PTR;
72pub const UNICODE = false;71pub const UNICODE = false;
lib/std/os/windows/ntdll.zig+1-1
...@@ -119,5 +119,5 @@ pub extern "NtDll" fn NtQueryObject(...@@ -119,5 +119,5 @@ pub extern "NtDll" fn NtQueryObject(
119 ObjectInformationClass: OBJECT_INFORMATION_CLASS,119 ObjectInformationClass: OBJECT_INFORMATION_CLASS,
120 ObjectInformation: PVOID,120 ObjectInformation: PVOID,
121 ObjectInformationLength: ULONG,121 ObjectInformationLength: ULONG,
122 ReturnLength: ?PULONG,122 ReturnLength: ?*ULONG,
123) callconv(WINAPI) NTSTATUS;123) callconv(WINAPI) NTSTATUS;
lib/std/target.zig+20-11
...@@ -95,7 +95,7 @@ pub const Target = struct {...@@ -95,7 +95,7 @@ pub const Target = struct {
95 win7 = 0x06010000,95 win7 = 0x06010000,
96 win8 = 0x06020000,96 win8 = 0x06020000,
97 win8_1 = 0x06030000,97 win8_1 = 0x06030000,
98 win10 = 0x0A000000,98 win10 = 0x0A000000, //aka win10_th1
99 win10_th2 = 0x0A000001,99 win10_th2 = 0x0A000001,
100 win10_rs1 = 0x0A000002,100 win10_rs1 = 0x0A000002,
101 win10_rs2 = 0x0A000003,101 win10_rs2 = 0x0A000003,
...@@ -103,26 +103,35 @@ pub const Target = struct {...@@ -103,26 +103,35 @@ pub const Target = struct {
103 win10_rs4 = 0x0A000005,103 win10_rs4 = 0x0A000005,
104 win10_rs5 = 0x0A000006,104 win10_rs5 = 0x0A000006,
105 win10_19h1 = 0x0A000007,105 win10_19h1 = 0x0A000007,
106 win10_20h1 = 0x0A000008,106 win10_vb = 0x0A000008, //aka win10_19h2
107 win10_mn = 0x0A000009, //aka win10_20h1
108 win10_fe = 0x0A00000A, //aka win10_20h2
107 _,109 _,
108110
109 /// Latest Windows version that the Zig Standard Library is aware of111 /// Latest Windows version that the Zig Standard Library is aware of
110 pub const latest = WindowsVersion.win10_20h1;112 pub const latest = WindowsVersion.win10_fe;
111113
112 /// Compared against build numbers reported by the runtime to distinguish win10 versions,114 /// Compared against build numbers reported by the runtime to distinguish win10 versions,
113 /// where 0x0A000000 + index corresponds to the WindowsVersion u32 value.115 /// where 0x0A000000 + index corresponds to the WindowsVersion u32 value.
114 pub const known_win10_build_numbers = [_]u32{116 pub const known_win10_build_numbers = [_]u32{
115 10240, //win10117 10240, //win10 aka win10_th1
116 10586, //win10_th2118 10586, //win10_th2
117 14393, //win_rs1119 14393, //win10_rs1
118 15063, //win_rs2120 15063, //win10_rs2
119 16299, //win_rs3121 16299, //win10_rs3
120 17134, //win_rs4122 17134, //win10_rs4
121 17763, //win_rs5123 17763, //win10_rs5
122 18362, //win_19h1124 18362, //win10_19h1
123 19041, //win_20h1125 18363, //win10_vb aka win10_19h2
126 19041, //win10_mn aka win10_20h1
127 19042, //win10_fe aka win10_20h2
124 };128 };
125129
130 /// Returns whether the first version `self` is newer (greater) than or equal to the second version `ver`.
131 pub fn isAtLeast(self: WindowsVersion, ver: WindowsVersion) bool {
132 return @enumToInt(self) >= @enumToInt(ver);
133 }
134
126 pub const Range = struct {135 pub const Range = struct {
127 min: WindowsVersion,136 min: WindowsVersion,
128 max: WindowsVersion,137 max: WindowsVersion,
lib/std/zig/system/windows.zig-16
...@@ -43,19 +43,3 @@ pub fn detectRuntimeVersion() WindowsVersion {...@@ -43,19 +43,3 @@ pub fn detectRuntimeVersion() WindowsVersion {
4343
44 return @intToEnum(WindowsVersion, version);44 return @intToEnum(WindowsVersion, version);
45}45}
46
47/// Returns whether the target os versions are uniformly at least as new as the argument:
48/// true/false if this holds for the entire target range, null if it only holds for some.
49pub fn targetVersionIsAtLeast(requested_version: WindowsVersion) ?bool {
50 const requested = @enumToInt(requested_version);
51 const version_range = std.builtin.os.version_range.windows;
52 const target_min = @enumToInt(version_range.min);
53 const target_max = @enumToInt(version_range.max);
54 return if (target_max < requested) false else if (target_min >= requested) true else null;
55}
56
57/// Returns whether the runtime os version is at least as new as the argument.
58pub fn runtimeVersionIsAtLeast(requested_version: WindowsVersion) bool {
59 return targetVersionIsAtLeast(requested_version) orelse
60 (@enumToInt(detectRuntimeVersion()) >= @enumToInt(requested_version));
61}