| ... | ... | @@ -952,8 +952,9 @@ fn windowsCreateProcessPathExt( |
| 952 | 952 | // for any directory that doesn't contain any possible matches, instead of having |
| 953 | 953 | // to use a separate look up for each individual filename combination (unappended + |
| 954 | 954 | // each PATHEXT appended). For directories where the wildcard *does* match something, |
| 955 | | // we only need to do a maximum of <number of supported PATHEXT extensions> more |
| 956 | | // NtQueryDirectoryFile calls. |
| 955 | // we iterate the matches and take note of any that are either the unappended version, |
| 956 | // or a version with a supported PATHEXT appended. We then try calling CreateProcessW |
| 957 | // with the found versions in the appropriate order. |
| 957 | 958 | |
| 958 | 959 | var dir = dir: { |
| 959 | 960 | // needs to be null-terminated |
| ... | ... | @@ -970,11 +971,26 @@ fn windowsCreateProcessPathExt( |
| 970 | 971 | try app_buf.append(allocator, 0); |
| 971 | 972 | const app_name_wildcard = app_buf.items[0 .. app_buf.items.len - 1 :0]; |
| 972 | 973 | |
| 973 | | // Enough for the FILE_DIRECTORY_INFORMATION + (NAME_MAX UTF-16 code units [2 bytes each]). |
| 974 | | const file_info_buf_size = @sizeOf(windows.FILE_DIRECTORY_INFORMATION) + (windows.NAME_MAX * 2); |
| 975 | | var file_information_buf: [file_info_buf_size]u8 align(@alignOf(os.windows.FILE_DIRECTORY_INFORMATION)) = undefined; |
| 974 | // This 2048 is arbitrary, we just want it to be large enough to get multiple FILE_DIRECTORY_INFORMATION entries |
| 975 | // returned per NtQueryDirectoryFile call. |
| 976 | var file_information_buf: [2048]u8 align(@alignOf(os.windows.FILE_DIRECTORY_INFORMATION)) = undefined; |
| 977 | const file_info_maximum_single_entry_size = @sizeOf(windows.FILE_DIRECTORY_INFORMATION) + (windows.NAME_MAX * 2); |
| 978 | if (file_information_buf.len < file_info_maximum_single_entry_size) { |
| 979 | @compileError("file_information_buf must be large enough to contain at least one maximum size FILE_DIRECTORY_INFORMATION entry"); |
| 980 | } |
| 976 | 981 | var io_status: windows.IO_STATUS_BLOCK = undefined; |
| 977 | | const found_name: ?[]const u16 = found_name: { |
| 982 | |
| 983 | const num_supported_pathext = @typeInfo(CreateProcessSupportedExtension).Enum.fields.len; |
| 984 | var pathext_seen = [_]bool{false} ** num_supported_pathext; |
| 985 | var any_pathext_seen = false; |
| 986 | var unappended_exists = false; |
| 987 | |
| 988 | // Fully iterate the wildcard matches via NtQueryDirectoryFile and take note of all versions |
| 989 | // of the app_name we should try to spawn. |
| 990 | // Note: This is necessary because the order of the files returned is filesystem-dependent: |
| 991 | // On NTFS, `blah.exe*` will always return `blah.exe` first if it exists. |
| 992 | // On FAT32, it's possible for something like `blah.exe.obj` to be returned first. |
| 993 | while (true) { |
| 978 | 994 | const app_name_len_bytes = math.cast(u16, app_name_wildcard.len * 2) orelse return error.NameTooLong; |
| 979 | 995 | var app_name_unicode_string = windows.UNICODE_STRING{ |
| 980 | 996 | .Length = app_name_len_bytes, |
| ... | ... | @@ -990,18 +1006,9 @@ fn windowsCreateProcessPathExt( |
| 990 | 1006 | &file_information_buf, |
| 991 | 1007 | file_information_buf.len, |
| 992 | 1008 | .FileDirectoryInformation, |
| 993 | | // TODO: It might be better to iterate over all wildcard matches and |
| 994 | | // only pick the ones that match an appended PATHEXT instead of only |
| 995 | | // using the wildcard as a lookup and then restarting iteration |
| 996 | | // on future NtQueryDirectoryFile calls. |
| 997 | | // |
| 998 | | // However, note that this could lead to worse outcomes in the |
| 999 | | // case of a very generic command name (e.g. "a"), so it might |
| 1000 | | // be better to only use the wildcard to determine if it's worth |
| 1001 | | // checking with PATHEXT (this is the current behavior). |
| 1002 | | windows.TRUE, // single result |
| 1009 | windows.FALSE, // single result |
| 1003 | 1010 | &app_name_unicode_string, |
| 1004 | | windows.TRUE, // restart iteration |
| 1011 | windows.FALSE, // restart iteration |
| 1005 | 1012 | ); |
| 1006 | 1013 | |
| 1007 | 1014 | // If we get nothing with the wildcard, then we can just bail out |
| ... | ... | @@ -1009,25 +1016,36 @@ fn windowsCreateProcessPathExt( |
| 1009 | 1016 | switch (rc) { |
| 1010 | 1017 | .SUCCESS => {}, |
| 1011 | 1018 | .NO_SUCH_FILE => return error.FileNotFound, |
| 1012 | | .NO_MORE_FILES => return error.FileNotFound, |
| 1019 | .NO_MORE_FILES => break, |
| 1013 | 1020 | .ACCESS_DENIED => return error.AccessDenied, |
| 1014 | 1021 | else => return windows.unexpectedStatus(rc), |
| 1015 | 1022 | } |
| 1016 | 1023 | |
| 1017 | | const dir_info = @as(*windows.FILE_DIRECTORY_INFORMATION, @ptrCast(&file_information_buf)); |
| 1018 | | if (dir_info.FileAttributes & windows.FILE_ATTRIBUTE_DIRECTORY != 0) { |
| 1019 | | break :found_name null; |
| 1024 | // According to the docs, this can only happen if there is not enough room in the |
| 1025 | // buffer to write at least one complete FILE_DIRECTORY_INFORMATION entry. |
| 1026 | // Therefore, this condition should not be possible to hit with the buffer size we use. |
| 1027 | std.debug.assert(io_status.Information != 0); |
| 1028 | |
| 1029 | var it = windows.FileInformationIterator(windows.FILE_DIRECTORY_INFORMATION){ .buf = &file_information_buf }; |
| 1030 | while (it.next()) |info| { |
| 1031 | // Skip directories |
| 1032 | if (info.FileAttributes & windows.FILE_ATTRIBUTE_DIRECTORY != 0) continue; |
| 1033 | const filename = @as([*]u16, @ptrCast(&info.FileName))[0 .. info.FileNameLength / 2]; |
| 1034 | // Because all results start with the app_name since we're using the wildcard `app_name*`, |
| 1035 | // if the length is equal to app_name then this is an exact match |
| 1036 | if (filename.len == app_name_len) { |
| 1037 | // Note: We can't break early here because it's possible that the unappended version |
| 1038 | // fails to spawn, in which case we still want to try the PATHEXT appended versions. |
| 1039 | unappended_exists = true; |
| 1040 | } else if (windowsCreateProcessSupportsExtension(filename[app_name_len..])) |pathext_ext| { |
| 1041 | pathext_seen[@intFromEnum(pathext_ext)] = true; |
| 1042 | any_pathext_seen = true; |
| 1043 | } |
| 1020 | 1044 | } |
| 1021 | | break :found_name @as([*]u16, @ptrCast(&dir_info.FileName))[0 .. dir_info.FileNameLength / 2]; |
| 1022 | | }; |
| 1045 | } |
| 1023 | 1046 | |
| 1024 | 1047 | const unappended_err = unappended: { |
| 1025 | | // NtQueryDirectoryFile returns results in order by filename, so the first result of |
| 1026 | | // the wildcard call will always be the unappended version if it exists. So, if found_name |
| 1027 | | // is not the unappended version, we can skip straight to trying versions with PATHEXT appended. |
| 1028 | | // TODO: This might depend on the filesystem, though; need to somehow verify that it always |
| 1029 | | // works this way. |
| 1030 | | if (found_name != null and windows.eqlIgnoreCaseWTF16(found_name.?, app_buf.items[0..app_name_len])) { |
| 1048 | if (unappended_exists) { |
| 1031 | 1049 | if (dir_path_len != 0) switch (dir_buf.items[dir_buf.items.len - 1]) { |
| 1032 | 1050 | '/', '\\' => {}, |
| 1033 | 1051 | else => try dir_buf.append(allocator, fs.path.sep), |
| ... | ... | @@ -1060,52 +1078,13 @@ fn windowsCreateProcessPathExt( |
| 1060 | 1078 | break :unappended error.FileNotFound; |
| 1061 | 1079 | }; |
| 1062 | 1080 | |
| 1063 | | // Now we know that at least *a* file matching the wildcard exists, we can loop |
| 1064 | | // through PATHEXT in order and exec any that exist |
| 1081 | if (!any_pathext_seen) return unappended_err; |
| 1065 | 1082 | |
| 1083 | // Now try any PATHEXT appended versions that we've seen |
| 1066 | 1084 | var ext_it = mem.tokenizeScalar(u16, pathext, ';'); |
| 1067 | 1085 | while (ext_it.next()) |ext| { |
| 1068 | | if (!windowsCreateProcessSupportsExtension(ext)) continue; |
| 1069 | | |
| 1070 | | app_buf.shrinkRetainingCapacity(app_name_len); |
| 1071 | | try app_buf.appendSlice(allocator, ext); |
| 1072 | | try app_buf.append(allocator, 0); |
| 1073 | | const app_name_appended = app_buf.items[0 .. app_buf.items.len - 1 :0]; |
| 1074 | | |
| 1075 | | const app_name_len_bytes = math.cast(u16, app_name_appended.len * 2) orelse return error.NameTooLong; |
| 1076 | | var app_name_unicode_string = windows.UNICODE_STRING{ |
| 1077 | | .Length = app_name_len_bytes, |
| 1078 | | .MaximumLength = app_name_len_bytes, |
| 1079 | | .Buffer = @constCast(app_name_appended.ptr), |
| 1080 | | }; |
| 1081 | | |
| 1082 | | // Re-use the directory handle but this time we call with the appended app name |
| 1083 | | // with no wildcard. |
| 1084 | | const rc = windows.ntdll.NtQueryDirectoryFile( |
| 1085 | | dir.fd, |
| 1086 | | null, |
| 1087 | | null, |
| 1088 | | null, |
| 1089 | | &io_status, |
| 1090 | | &file_information_buf, |
| 1091 | | file_information_buf.len, |
| 1092 | | .FileDirectoryInformation, |
| 1093 | | windows.TRUE, // single result |
| 1094 | | &app_name_unicode_string, |
| 1095 | | windows.TRUE, // restart iteration |
| 1096 | | ); |
| 1097 | | |
| 1098 | | switch (rc) { |
| 1099 | | .SUCCESS => {}, |
| 1100 | | .NO_SUCH_FILE => continue, |
| 1101 | | .NO_MORE_FILES => continue, |
| 1102 | | .ACCESS_DENIED => continue, |
| 1103 | | else => return windows.unexpectedStatus(rc), |
| 1104 | | } |
| 1105 | | |
| 1106 | | const dir_info = @as(*windows.FILE_DIRECTORY_INFORMATION, @ptrCast(&file_information_buf)); |
| 1107 | | // Skip directories |
| 1108 | | if (dir_info.FileAttributes & windows.FILE_ATTRIBUTE_DIRECTORY != 0) continue; |
| 1086 | const ext_enum = windowsCreateProcessSupportsExtension(ext) orelse continue; |
| 1087 | if (!pathext_seen[@intFromEnum(ext_enum)]) continue; |
| 1109 | 1088 | |
| 1110 | 1089 | dir_buf.shrinkRetainingCapacity(dir_path_len); |
| 1111 | 1090 | if (dir_path_len != 0) switch (dir_buf.items[dir_buf.items.len - 1]) { |
| ... | ... | @@ -1170,9 +1149,17 @@ fn windowsCreateProcess(app_name: [*:0]u16, cmd_line: [*:0]u16, envp_ptr: ?[*]u1 |
| 1170 | 1149 | ); |
| 1171 | 1150 | } |
| 1172 | 1151 | |
| 1152 | // Should be kept in sync with `windowsCreateProcessSupportsExtension` |
| 1153 | const CreateProcessSupportedExtension = enum { |
| 1154 | bat, |
| 1155 | cmd, |
| 1156 | com, |
| 1157 | exe, |
| 1158 | }; |
| 1159 | |
| 1173 | 1160 | /// Case-insensitive UTF-16 lookup |
| 1174 | | fn windowsCreateProcessSupportsExtension(ext: []const u16) bool { |
| 1175 | | if (ext.len != 4) return false; |
| 1161 | fn windowsCreateProcessSupportsExtension(ext: []const u16) ?CreateProcessSupportedExtension { |
| 1162 | if (ext.len != 4) return null; |
| 1176 | 1163 | const State = enum { |
| 1177 | 1164 | start, |
| 1178 | 1165 | dot, |
| ... | ... | @@ -1188,50 +1175,50 @@ fn windowsCreateProcessSupportsExtension(ext: []const u16) bool { |
| 1188 | 1175 | for (ext) |c| switch (state) { |
| 1189 | 1176 | .start => switch (c) { |
| 1190 | 1177 | '.' => state = .dot, |
| 1191 | | else => return false, |
| 1178 | else => return null, |
| 1192 | 1179 | }, |
| 1193 | 1180 | .dot => switch (c) { |
| 1194 | 1181 | 'b', 'B' => state = .b, |
| 1195 | 1182 | 'c', 'C' => state = .c, |
| 1196 | 1183 | 'e', 'E' => state = .e, |
| 1197 | | else => return false, |
| 1184 | else => return null, |
| 1198 | 1185 | }, |
| 1199 | 1186 | .b => switch (c) { |
| 1200 | 1187 | 'a', 'A' => state = .ba, |
| 1201 | | else => return false, |
| 1188 | else => return null, |
| 1202 | 1189 | }, |
| 1203 | 1190 | .c => switch (c) { |
| 1204 | 1191 | 'm', 'M' => state = .cm, |
| 1205 | 1192 | 'o', 'O' => state = .co, |
| 1206 | | else => return false, |
| 1193 | else => return null, |
| 1207 | 1194 | }, |
| 1208 | 1195 | .e => switch (c) { |
| 1209 | 1196 | 'x', 'X' => state = .ex, |
| 1210 | | else => return false, |
| 1197 | else => return null, |
| 1211 | 1198 | }, |
| 1212 | 1199 | .ba => switch (c) { |
| 1213 | | 't', 'T' => return true, // .BAT |
| 1214 | | else => return false, |
| 1200 | 't', 'T' => return .bat, |
| 1201 | else => return null, |
| 1215 | 1202 | }, |
| 1216 | 1203 | .cm => switch (c) { |
| 1217 | | 'd', 'D' => return true, // .CMD |
| 1218 | | else => return false, |
| 1204 | 'd', 'D' => return .cmd, |
| 1205 | else => return null, |
| 1219 | 1206 | }, |
| 1220 | 1207 | .co => switch (c) { |
| 1221 | | 'm', 'M' => return true, // .COM |
| 1222 | | else => return false, |
| 1208 | 'm', 'M' => return .com, |
| 1209 | else => return null, |
| 1223 | 1210 | }, |
| 1224 | 1211 | .ex => switch (c) { |
| 1225 | | 'e', 'E' => return true, // .EXE |
| 1226 | | else => return false, |
| 1212 | 'e', 'E' => return .exe, |
| 1213 | else => return null, |
| 1227 | 1214 | }, |
| 1228 | 1215 | }; |
| 1229 | | return false; |
| 1216 | return null; |
| 1230 | 1217 | } |
| 1231 | 1218 | |
| 1232 | 1219 | test "windowsCreateProcessSupportsExtension" { |
| 1233 | | try std.testing.expect(windowsCreateProcessSupportsExtension(&[_]u16{ '.', 'e', 'X', 'e' })); |
| 1234 | | try std.testing.expect(!windowsCreateProcessSupportsExtension(&[_]u16{ '.', 'e', 'X', 'e', 'c' })); |
| 1220 | try std.testing.expectEqual(CreateProcessSupportedExtension.exe, windowsCreateProcessSupportsExtension(&[_]u16{ '.', 'e', 'X', 'e' }).?); |
| 1221 | try std.testing.expect(windowsCreateProcessSupportsExtension(&[_]u16{ '.', 'e', 'X', 'e', 'c' }) == null); |
| 1235 | 1222 | } |
| 1236 | 1223 | |
| 1237 | 1224 | /// Caller must dealloc. |