Expects app_buf to contain exactly the app name, and dir_buf to contain exactly the dir path.
After return, app_buf will always contain exactly the app name and dir_buf will always contain exactly the dir path.
Note: app_buf should not contain any leading path separators.
Note: If the dir is the cwd, dir_buf should be empty (len = 0).
fn windowsCreateProcessPathExt(
arena: Allocator,
dir_buf: *std.ArrayList(u16),
app_buf: *std.ArrayList(u16),
pathext: [:0]const u16,
cmd_line_cache: *WindowsCommandLineCache,
env_block: ?process.Environ.WindowsBlock,
cwd_ptr: ?[*:0]u16,
flags: windows.CreateProcessFlags,
lpStartupInfo: *windows.STARTUPINFOW,
lpProcessInformation: *windows.PROCESS.INFORMATION,
) !void
fn windowsCreateProcessPathExt(
arena: Allocator,
dir_buf: *std.ArrayList(u16),
app_buf: *std.ArrayList(u16),
pathext: [:0]const u16,
cmd_line_cache: *WindowsCommandLineCache,
env_block: ?process.Environ.WindowsBlock,
cwd_ptr: ?[*:0]u16,
flags: windows.CreateProcessFlags,
lpStartupInfo: *windows.STARTUPINFOW,
lpProcessInformation: *windows.PROCESS.INFORMATION,
) !void {
const app_name_len = app_buf.items.len;
const dir_path_len = dir_buf.items.len;
if (app_name_len == 0) return error.FileNotFound;
defer app_buf.shrinkRetainingCapacity(app_name_len);
defer dir_buf.shrinkRetainingCapacity(dir_path_len);
// The name of the game here is to avoid CreateProcessW calls at all costs,
// and only ever try calling it when we have a real candidate for execution.
// Secondarily, we want to minimize the number of syscalls used when checking
// for each PATHEXT-appended version of the app name.
//
// An overview of the technique used:
// - Open the search directory for iteration (either cwd or a path from PATH)
// - Use NtQueryDirectoryFile with a wildcard filename of `<app name>*` to
// check if anything that could possibly match either the unappended version
// of the app name or any of the versions with a PATHEXT value appended exists.
// - If the wildcard NtQueryDirectoryFile call found nothing, we can exit early
// without needing to use PATHEXT at all.
//
// This allows us to use a <open dir, NtQueryDirectoryFile, close dir> sequence
// for any directory that doesn't contain any possible matches, instead of having
// to use a separate look up for each individual filename combination (unappended +
// each PATHEXT appended). For directories where the wildcard *does* match something,
// we iterate the matches and take note of any that are either the unappended version,
// or a version with a supported PATHEXT appended. We then try calling CreateProcessW
// with the found versions in the appropriate order.
const dir = dir: {
// needs to be null-terminated
try dir_buf.append(arena, 0);
defer dir_buf.shrinkRetainingCapacity(dir_path_len);
const dir_path_z = dir_buf.items[0 .. dir_buf.items.len - 1 :0];
const prefixed_path = try wToPrefixedFileW(null, dir_path_z, .{});
break :dir dirOpenDirWindows(.cwd(), prefixed_path.span(), .{
.iterate = true,
}) catch |err| switch (err) {
// These errors must not be ignored because they should not be able
// to affect which file is chosen to execute. Also `error.Canceled`
// must never be swallowed.
error.Canceled,
error.SystemResources,
error.Unexpected,
error.ProcessFdQuotaExceeded,
error.SystemFdQuotaExceeded,
=> |e| return e,
error.AccessDenied,
error.PermissionDenied,
error.SymLinkLoop,
error.FileNotFound,
error.NotDir,
error.NoDevice,
error.NetworkNotFound,
error.NameTooLong,
error.BadPathName,
=> return error.FileNotFound,
};
};
defer windows.CloseHandle(dir.handle);
// Add wildcard and null-terminator
try app_buf.append(arena, '*');
try app_buf.append(arena, 0);
const app_name_wildcard = app_buf.items[0 .. app_buf.items.len - 1 :0];
// This 2048 is arbitrary, we just want it to be large enough to get multiple FILE_DIRECTORY_INFORMATION entries
// returned per NtQueryDirectoryFile call.
var file_information_buf: [2048]u8 align(@alignOf(windows.FILE_DIRECTORY_INFORMATION)) = undefined;
const file_info_maximum_single_entry_size = @sizeOf(windows.FILE_DIRECTORY_INFORMATION) + (windows.NAME_MAX * 2);
if (file_information_buf.len < file_info_maximum_single_entry_size) {
@compileError("file_information_buf must be large enough to contain at least one maximum size FILE_DIRECTORY_INFORMATION entry");
}
var io_status: windows.IO_STATUS_BLOCK = undefined;
const num_supported_pathext = @typeInfo(process.WindowsExtension).@"enum".field_names.len;
var pathext_seen: [num_supported_pathext]bool = @splat(false);
var any_pathext_seen = false;
var unappended_exists = false;
// Fully iterate the wildcard matches via NtQueryDirectoryFile and take note of all versions
// of the app_name we should try to spawn.
// Note: This is necessary because the order of the files returned is filesystem-dependent:
// On NTFS, `blah.exe*` will always return `blah.exe` first if it exists.
// On FAT32, it's possible for something like `blah.exe.obj` to be returned first.
while (true) {
// If we get nothing with the wildcard, then we can just bail out
// as we know appending PATHEXT will not yield anything.
switch (windows.ntdll.NtQueryDirectoryFile(
dir.handle,
null,
null,
null,
&io_status,
&file_information_buf,
file_information_buf.len,
.Directory,
.FALSE, // single result
&.init(app_name_wildcard),
.FALSE, // restart iteration
)) {
.SUCCESS => {},
.NO_SUCH_FILE => return error.FileNotFound,
.NO_MORE_FILES => break,
.ACCESS_DENIED => return error.AccessDenied,
else => |status| return windows.unexpectedStatus(status),
}
// According to the docs, this can only happen if there is not enough room in the
// buffer to write at least one complete FILE_DIRECTORY_INFORMATION entry.
// Therefore, this condition should not be possible to hit with the buffer size we use.
std.debug.assert(io_status.Information != 0);
var it = windows.FileInformationIterator(windows.FILE_DIRECTORY_INFORMATION){ .buf = &file_information_buf };
while (it.next()) |info| {
// Skip directories
if (info.FileAttributes.DIRECTORY) continue;
const filename = @as([*]u16, @ptrCast(&info.FileName))[0 .. info.FileNameLength / 2];
// Because all results start with the app_name since we're using the wildcard `app_name*`,
// if the length is equal to app_name then this is an exact match
if (filename.len == app_name_len) {
// Note: We can't break early here because it's possible that the unappended version
// fails to spawn, in which case we still want to try the PATHEXT appended versions.
unappended_exists = true;
} else if (windowsCreateProcessSupportsExtension(filename[app_name_len..])) |pathext_ext| {
pathext_seen[@backingInt(pathext_ext)] = true;
any_pathext_seen = true;
}
}
}
const unappended_err = unappended: {
if (unappended_exists) {
if (dir_path_len != 0) switch (dir_buf.items[dir_buf.items.len - 1]) {
'/', '\\' => {},
else => try dir_buf.append(arena, Dir.path.sep),
};
try dir_buf.appendSlice(arena, app_buf.items[0..app_name_len]);
try dir_buf.append(arena, 0);
const full_app_name = dir_buf.items[0 .. dir_buf.items.len - 1 :0];
const is_bat_or_cmd = bat_or_cmd: {
const app_name = app_buf.items[0..app_name_len];
const ext_start = std.mem.lastIndexOfScalar(u16, app_name, '.') orelse break :bat_or_cmd false;
const ext = app_name[ext_start..];
const ext_enum = windowsCreateProcessSupportsExtension(ext) orelse break :bat_or_cmd false;
switch (ext_enum) {
.cmd, .bat => break :bat_or_cmd true,
else => break :bat_or_cmd false,
}
};
const cmd_line_w = if (is_bat_or_cmd)
try cmd_line_cache.scriptCommandLine(full_app_name)
else
try cmd_line_cache.commandLine();
const app_name_w = if (is_bat_or_cmd)
try cmd_line_cache.cmdExePath()
else
full_app_name;
if (windowsCreateProcess(
app_name_w.ptr,
cmd_line_w.ptr,
env_block,
cwd_ptr,
flags,
lpStartupInfo,
lpProcessInformation,
)) |_| {
return;
} else |err| switch (err) {
error.FileNotFound,
error.AccessDenied,
=> break :unappended err,
error.InvalidExe => {
// On InvalidExe, if the extension of the app name is .exe then
// it's treated as an unrecoverable error. Otherwise, it'll be
// skipped as normal.
const app_name = app_buf.items[0..app_name_len];
const ext_start = std.mem.lastIndexOfScalar(u16, app_name, '.') orelse break :unappended err;
const ext = app_name[ext_start..];
if (windows.eqlIgnoreCaseWtf16(ext, std.unicode.utf8ToUtf16LeStringLiteral(".EXE"))) {
return error.UnrecoverableInvalidExe;
}
break :unappended err;
},
else => return err,
}
}
break :unappended error.FileNotFound;
};
if (!any_pathext_seen) return unappended_err;
// Now try any PATHEXT appended versions that we've seen
var ext_it = std.mem.tokenizeScalar(u16, pathext, ';');
while (ext_it.next()) |ext| {
const ext_enum = windowsCreateProcessSupportsExtension(ext) orelse continue;
if (!pathext_seen[@backingInt(ext_enum)]) continue;
dir_buf.shrinkRetainingCapacity(dir_path_len);
if (dir_path_len != 0) switch (dir_buf.items[dir_buf.items.len - 1]) {
'/', '\\' => {},
else => try dir_buf.append(arena, Dir.path.sep),
};
try dir_buf.appendSlice(arena, app_buf.items[0..app_name_len]);
try dir_buf.appendSlice(arena, ext);
try dir_buf.append(arena, 0);
const full_app_name = dir_buf.items[0 .. dir_buf.items.len - 1 :0];
const is_bat_or_cmd = switch (ext_enum) {
.cmd, .bat => true,
else => false,
};
const cmd_line_w = if (is_bat_or_cmd)
try cmd_line_cache.scriptCommandLine(full_app_name)
else
try cmd_line_cache.commandLine();
const app_name_w = if (is_bat_or_cmd)
try cmd_line_cache.cmdExePath()
else
full_app_name;
if (windowsCreateProcess(app_name_w.ptr, cmd_line_w.ptr, env_block, cwd_ptr, flags, lpStartupInfo, lpProcessInformation)) |_| {
return;
} else |err| switch (err) {
error.FileNotFound => continue,
error.AccessDenied => continue,
error.InvalidExe => {
// On InvalidExe, if the extension of the app name is .exe then
// it's treated as an unrecoverable error. Otherwise, it'll be
// skipped as normal.
if (windows.eqlIgnoreCaseWtf16(ext, std.unicode.utf8ToUtf16LeStringLiteral(".EXE"))) {
return error.UnrecoverableInvalidExe;
}
continue;
},
else => return err,
}
}
return unappended_err;
}