master
   1const ChildProcess = @This();
   2
   3const builtin = @import("builtin");
   4const native_os = builtin.os.tag;
   5
   6const std = @import("../std.zig");
   7const unicode = std.unicode;
   8const fs = std.fs;
   9const process = std.process;
  10const File = std.fs.File;
  11const windows = std.os.windows;
  12const linux = std.os.linux;
  13const posix = std.posix;
  14const mem = std.mem;
  15const EnvMap = std.process.EnvMap;
  16const maxInt = std.math.maxInt;
  17const assert = std.debug.assert;
  18const Allocator = std.mem.Allocator;
  19const ArrayList = std.ArrayList;
  20
  21pub const Id = switch (native_os) {
  22    .windows => windows.HANDLE,
  23    .wasi => void,
  24    else => posix.pid_t,
  25};
  26
  27/// Available after calling `spawn()`. This becomes `undefined` after calling `wait()`.
  28/// On Windows this is the hProcess.
  29/// On POSIX this is the pid.
  30id: Id,
  31thread_handle: if (native_os == .windows) windows.HANDLE else void,
  32
  33allocator: mem.Allocator,
  34
  35/// The writing end of the child process's standard input pipe.
  36/// Usage requires `stdin_behavior == StdIo.Pipe`.
  37/// Available after calling `spawn()`.
  38stdin: ?File,
  39
  40/// The reading end of the child process's standard output pipe.
  41/// Usage requires `stdout_behavior == StdIo.Pipe`.
  42/// Available after calling `spawn()`.
  43stdout: ?File,
  44
  45/// The reading end of the child process's standard error pipe.
  46/// Usage requires `stderr_behavior == StdIo.Pipe`.
  47/// Available after calling `spawn()`.
  48stderr: ?File,
  49
  50/// Terminated state of the child process.
  51/// Available after calling `wait()`.
  52term: ?(SpawnError!Term),
  53
  54argv: []const []const u8,
  55
  56/// Leave as null to use the current env map using the supplied allocator.
  57/// Required if unable to access the current env map (e.g. building a library on
  58/// some platforms).
  59env_map: ?*const EnvMap,
  60
  61stdin_behavior: StdIo,
  62stdout_behavior: StdIo,
  63stderr_behavior: StdIo,
  64
  65/// Set to change the user id when spawning the child process.
  66uid: if (native_os == .windows or native_os == .wasi) void else ?posix.uid_t,
  67
  68/// Set to change the group id when spawning the child process.
  69gid: if (native_os == .windows or native_os == .wasi) void else ?posix.gid_t,
  70
  71/// Set to change the process group id when spawning the child process.
  72pgid: if (native_os == .windows or native_os == .wasi) void else ?posix.pid_t,
  73
  74/// Set to change the current working directory when spawning the child process.
  75cwd: ?[]const u8,
  76/// Set to change the current working directory when spawning the child process.
  77/// This is not yet implemented for Windows. See https://github.com/ziglang/zig/issues/5190
  78/// Once that is done, `cwd` will be deprecated in favor of this field.
  79cwd_dir: ?fs.Dir = null,
  80
  81err_pipe: if (native_os == .windows) void else ?posix.fd_t,
  82
  83expand_arg0: Arg0Expand,
  84
  85/// Darwin-only. Disable ASLR for the child process.
  86disable_aslr: bool = false,
  87
  88/// Start child process in suspended state.
  89/// For Posix systems it's started as if SIGSTOP was sent.
  90start_suspended: bool = false,
  91
  92/// Windows-only. Sets the CREATE_NO_WINDOW flag in CreateProcess.
  93create_no_window: bool = false,
  94
  95/// Set to true to obtain rusage information for the child process.
  96/// Depending on the target platform and implementation status, the
  97/// requested statistics may or may not be available. If they are
  98/// available, then the `resource_usage_statistics` field will be populated
  99/// after calling `wait`.
 100/// On Linux and Darwin, this obtains rusage statistics from wait4().
 101request_resource_usage_statistics: bool = false,
 102
 103/// This is available after calling wait if
 104/// `request_resource_usage_statistics` was set to `true` before calling
 105/// `spawn`.
 106resource_usage_statistics: ResourceUsageStatistics = .{},
 107
 108/// When populated, a pipe will be created for the child process to
 109/// communicate progress back to the parent. The file descriptor of the
 110/// write end of the pipe will be specified in the `ZIG_PROGRESS`
 111/// environment variable inside the child process. The progress reported by
 112/// the child will be attached to this progress node in the parent process.
 113///
 114/// The child's progress tree will be grafted into the parent's progress tree,
 115/// by substituting this node with the child's root node.
 116progress_node: std.Progress.Node = std.Progress.Node.none,
 117
 118pub const ResourceUsageStatistics = struct {
 119    rusage: @TypeOf(rusage_init) = rusage_init,
 120
 121    /// Returns the peak resident set size of the child process, in bytes,
 122    /// if available.
 123    pub inline fn getMaxRss(rus: ResourceUsageStatistics) ?usize {
 124        switch (native_os) {
 125            .linux => {
 126                if (rus.rusage) |ru| {
 127                    return @as(usize, @intCast(ru.maxrss)) * 1024;
 128                } else {
 129                    return null;
 130                }
 131            },
 132            .windows => {
 133                if (rus.rusage) |ru| {
 134                    return ru.PeakWorkingSetSize;
 135                } else {
 136                    return null;
 137                }
 138            },
 139            .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => {
 140                if (rus.rusage) |ru| {
 141                    // Darwin oddly reports in bytes instead of kilobytes.
 142                    return @as(usize, @intCast(ru.maxrss));
 143                } else {
 144                    return null;
 145                }
 146            },
 147            else => return null,
 148        }
 149    }
 150
 151    const rusage_init = switch (native_os) {
 152        .linux, .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => @as(?posix.rusage, null),
 153        .windows => @as(?windows.VM_COUNTERS, null),
 154        else => {},
 155    };
 156};
 157
 158pub const Arg0Expand = posix.Arg0Expand;
 159
 160pub const SpawnError = error{
 161    OutOfMemory,
 162
 163    /// POSIX-only. `StdIo.Ignore` was selected and opening `/dev/null` returned ENODEV.
 164    NoDevice,
 165
 166    /// Windows-only. `cwd` or `argv` was provided and it was invalid WTF-8.
 167    /// https://wtf-8.codeberg.page/
 168    InvalidWtf8,
 169
 170    /// Windows-only. `cwd` was provided, but the path did not exist when spawning the child process.
 171    CurrentWorkingDirectoryUnlinked,
 172
 173    /// Windows-only. NUL (U+0000), LF (U+000A), CR (U+000D) are not allowed
 174    /// within arguments when executing a `.bat`/`.cmd` script.
 175    /// - NUL/LF signifiies end of arguments, so anything afterwards
 176    ///   would be lost after execution.
 177    /// - CR is stripped by `cmd.exe`, so any CR codepoints
 178    ///   would be lost after execution.
 179    InvalidBatchScriptArg,
 180} ||
 181    posix.ExecveError ||
 182    posix.SetIdError ||
 183    posix.SetPgidError ||
 184    posix.ChangeCurDirError ||
 185    windows.CreateProcessError ||
 186    windows.GetProcessMemoryInfoError ||
 187    windows.WaitForSingleObjectError;
 188
 189pub const Term = union(enum) {
 190    Exited: u8,
 191    Signal: u32,
 192    Stopped: u32,
 193    Unknown: u32,
 194};
 195
 196/// Behavior of the child process's standard input, output, and error
 197/// streams.
 198pub const StdIo = enum {
 199    /// Inherit the stream from the parent process.
 200    Inherit,
 201
 202    /// Pass a null stream to the child process.
 203    /// This is /dev/null on POSIX and NUL on Windows.
 204    Ignore,
 205
 206    /// Create a pipe for the stream.
 207    /// The corresponding field (`stdout`, `stderr`, or `stdin`)
 208    /// will be assigned a `File` object that can be used
 209    /// to read from or write to the pipe.
 210    Pipe,
 211
 212    /// Close the stream after the child process spawns.
 213    Close,
 214};
 215
 216/// First argument in argv is the executable.
 217pub fn init(argv: []const []const u8, allocator: mem.Allocator) ChildProcess {
 218    return .{
 219        .allocator = allocator,
 220        .argv = argv,
 221        .id = undefined,
 222        .thread_handle = undefined,
 223        .err_pipe = if (native_os == .windows) {} else null,
 224        .term = null,
 225        .env_map = null,
 226        .cwd = null,
 227        .uid = if (native_os == .windows or native_os == .wasi) {} else null,
 228        .gid = if (native_os == .windows or native_os == .wasi) {} else null,
 229        .pgid = if (native_os == .windows or native_os == .wasi) {} else null,
 230        .stdin = null,
 231        .stdout = null,
 232        .stderr = null,
 233        .stdin_behavior = .Inherit,
 234        .stdout_behavior = .Inherit,
 235        .stderr_behavior = .Inherit,
 236        .expand_arg0 = .no_expand,
 237    };
 238}
 239
 240pub fn setUserName(self: *ChildProcess, name: []const u8) !void {
 241    const user_info = try process.getUserInfo(name);
 242    self.uid = user_info.uid;
 243    self.gid = user_info.gid;
 244}
 245
 246/// On success must call `kill` or `wait`.
 247/// After spawning the `id` is available.
 248pub fn spawn(self: *ChildProcess) SpawnError!void {
 249    if (!process.can_spawn) {
 250        @compileError("the target operating system cannot spawn processes");
 251    }
 252
 253    if (native_os == .windows) {
 254        return self.spawnWindows();
 255    } else {
 256        return self.spawnPosix();
 257    }
 258}
 259
 260pub fn spawnAndWait(self: *ChildProcess) SpawnError!Term {
 261    try self.spawn();
 262    return self.wait();
 263}
 264
 265/// Forcibly terminates child process and then cleans up all resources.
 266pub fn kill(self: *ChildProcess) !Term {
 267    if (native_os == .windows) {
 268        return self.killWindows(1);
 269    } else {
 270        return self.killPosix();
 271    }
 272}
 273
 274pub fn killWindows(self: *ChildProcess, exit_code: windows.UINT) !Term {
 275    if (self.term) |term| {
 276        self.cleanupStreams();
 277        return term;
 278    }
 279
 280    windows.TerminateProcess(self.id, exit_code) catch |err| switch (err) {
 281        error.AccessDenied => {
 282            // Usually when TerminateProcess triggers a ACCESS_DENIED error, it
 283            // indicates that the process has already exited, but there may be
 284            // some rare edge cases where our process handle no longer has the
 285            // PROCESS_TERMINATE access right, so let's do another check to make
 286            // sure the process is really no longer running:
 287            windows.WaitForSingleObjectEx(self.id, 0, false) catch return err;
 288            return error.AlreadyTerminated;
 289        },
 290        else => return err,
 291    };
 292    try self.waitUnwrappedWindows();
 293    return self.term.?;
 294}
 295
 296pub fn killPosix(self: *ChildProcess) !Term {
 297    if (self.term) |term| {
 298        self.cleanupStreams();
 299        return term;
 300    }
 301    posix.kill(self.id, posix.SIG.TERM) catch |err| switch (err) {
 302        error.ProcessNotFound => return error.AlreadyTerminated,
 303        else => return err,
 304    };
 305    self.waitUnwrappedPosix();
 306    return self.term.?;
 307}
 308
 309pub const WaitError = SpawnError || std.os.windows.GetProcessMemoryInfoError;
 310
 311/// On some targets, `spawn` may not report all spawn errors, such as `error.InvalidExe`.
 312/// This function will block until any spawn errors can be reported, and return them.
 313pub fn waitForSpawn(self: *ChildProcess) SpawnError!void {
 314    if (native_os == .windows) return; // `spawn` reports everything
 315    if (self.term) |term| {
 316        _ = term catch |spawn_err| return spawn_err;
 317        return;
 318    }
 319
 320    const err_pipe = self.err_pipe orelse return;
 321    self.err_pipe = null;
 322    // Wait for the child to report any errors in or before `execvpe`.
 323    const report = readIntFd(err_pipe);
 324    posix.close(err_pipe);
 325    if (report) |child_err_int| {
 326        const child_err: SpawnError = @errorCast(@errorFromInt(child_err_int));
 327        self.term = child_err;
 328        return child_err;
 329    } else |read_err| switch (read_err) {
 330        error.EndOfStream => {
 331            // Write end closed by CLOEXEC at the time of the `execvpe` call,
 332            // indicating success.
 333        },
 334        else => {
 335            // Problem reading the error from the error reporting pipe. We
 336            // don't know if the child is alive or dead. Better to assume it is
 337            // alive so the resource does not risk being leaked.
 338        },
 339    }
 340}
 341
 342/// Blocks until child process terminates and then cleans up all resources.
 343pub fn wait(self: *ChildProcess) WaitError!Term {
 344    try self.waitForSpawn(); // report spawn errors
 345    if (self.term) |term| {
 346        self.cleanupStreams();
 347        return term;
 348    }
 349    switch (native_os) {
 350        .windows => try self.waitUnwrappedWindows(),
 351        else => self.waitUnwrappedPosix(),
 352    }
 353    self.id = undefined;
 354    return self.term.?;
 355}
 356
 357pub const RunResult = struct {
 358    term: Term,
 359    stdout: []u8,
 360    stderr: []u8,
 361};
 362
 363/// Collect the output from the process's stdout and stderr. Will return once all output
 364/// has been collected. This does not mean that the process has ended. `wait` should still
 365/// be called to wait for and clean up the process.
 366///
 367/// The process must be started with stdout_behavior and stderr_behavior == .Pipe
 368pub fn collectOutput(
 369    child: ChildProcess,
 370    /// Used for `stdout` and `stderr`.
 371    allocator: Allocator,
 372    stdout: *ArrayList(u8),
 373    stderr: *ArrayList(u8),
 374    max_output_bytes: usize,
 375) !void {
 376    assert(child.stdout_behavior == .Pipe);
 377    assert(child.stderr_behavior == .Pipe);
 378
 379    var poller = std.Io.poll(allocator, enum { stdout, stderr }, .{
 380        .stdout = child.stdout.?,
 381        .stderr = child.stderr.?,
 382    });
 383    defer poller.deinit();
 384
 385    const stdout_r = poller.reader(.stdout);
 386    stdout_r.buffer = stdout.allocatedSlice();
 387    stdout_r.seek = 0;
 388    stdout_r.end = stdout.items.len;
 389
 390    const stderr_r = poller.reader(.stderr);
 391    stderr_r.buffer = stderr.allocatedSlice();
 392    stderr_r.seek = 0;
 393    stderr_r.end = stderr.items.len;
 394
 395    defer {
 396        stdout.* = .{
 397            .items = stdout_r.buffer[0..stdout_r.end],
 398            .capacity = stdout_r.buffer.len,
 399        };
 400        stderr.* = .{
 401            .items = stderr_r.buffer[0..stderr_r.end],
 402            .capacity = stderr_r.buffer.len,
 403        };
 404        stdout_r.buffer = &.{};
 405        stderr_r.buffer = &.{};
 406    }
 407
 408    while (try poller.poll()) {
 409        if (stdout_r.bufferedLen() > max_output_bytes)
 410            return error.StdoutStreamTooLong;
 411        if (stderr_r.bufferedLen() > max_output_bytes)
 412            return error.StderrStreamTooLong;
 413    }
 414}
 415
 416pub const RunError = posix.GetCwdError || posix.ReadError || SpawnError || posix.PollError || error{
 417    StdoutStreamTooLong,
 418    StderrStreamTooLong,
 419};
 420
 421/// Spawns a child process, waits for it, collecting stdout and stderr, and then returns.
 422/// If it succeeds, the caller owns result.stdout and result.stderr memory.
 423pub fn run(args: struct {
 424    allocator: mem.Allocator,
 425    argv: []const []const u8,
 426    cwd: ?[]const u8 = null,
 427    cwd_dir: ?fs.Dir = null,
 428    /// Required if unable to access the current env map (e.g. building a
 429    /// library on some platforms).
 430    env_map: ?*const EnvMap = null,
 431    max_output_bytes: usize = 50 * 1024,
 432    expand_arg0: Arg0Expand = .no_expand,
 433    progress_node: std.Progress.Node = std.Progress.Node.none,
 434}) RunError!RunResult {
 435    var child = ChildProcess.init(args.argv, args.allocator);
 436    child.stdin_behavior = .Ignore;
 437    child.stdout_behavior = .Pipe;
 438    child.stderr_behavior = .Pipe;
 439    child.cwd = args.cwd;
 440    child.cwd_dir = args.cwd_dir;
 441    child.env_map = args.env_map;
 442    child.expand_arg0 = args.expand_arg0;
 443    child.progress_node = args.progress_node;
 444
 445    var stdout: ArrayList(u8) = .empty;
 446    defer stdout.deinit(args.allocator);
 447    var stderr: ArrayList(u8) = .empty;
 448    defer stderr.deinit(args.allocator);
 449
 450    try child.spawn();
 451    errdefer {
 452        _ = child.kill() catch {};
 453    }
 454    try child.collectOutput(args.allocator, &stdout, &stderr, args.max_output_bytes);
 455
 456    return .{
 457        .stdout = try stdout.toOwnedSlice(args.allocator),
 458        .stderr = try stderr.toOwnedSlice(args.allocator),
 459        .term = try child.wait(),
 460    };
 461}
 462
 463fn waitUnwrappedWindows(self: *ChildProcess) WaitError!void {
 464    const result = windows.WaitForSingleObjectEx(self.id, windows.INFINITE, false);
 465
 466    self.term = @as(SpawnError!Term, x: {
 467        var exit_code: windows.DWORD = undefined;
 468        if (windows.kernel32.GetExitCodeProcess(self.id, &exit_code) == 0) {
 469            break :x Term{ .Unknown = 0 };
 470        } else {
 471            break :x Term{ .Exited = @as(u8, @truncate(exit_code)) };
 472        }
 473    });
 474
 475    if (self.request_resource_usage_statistics) {
 476        self.resource_usage_statistics.rusage = try windows.GetProcessMemoryInfo(self.id);
 477    }
 478
 479    posix.close(self.id);
 480    posix.close(self.thread_handle);
 481    self.cleanupStreams();
 482    return result;
 483}
 484
 485fn waitUnwrappedPosix(self: *ChildProcess) void {
 486    const res: posix.WaitPidResult = res: {
 487        if (self.request_resource_usage_statistics) {
 488            switch (native_os) {
 489                .linux, .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => {
 490                    var ru: posix.rusage = undefined;
 491                    const res = posix.wait4(self.id, 0, &ru);
 492                    self.resource_usage_statistics.rusage = ru;
 493                    break :res res;
 494                },
 495                else => {},
 496            }
 497        }
 498
 499        break :res posix.waitpid(self.id, 0);
 500    };
 501    const status = res.status;
 502    self.cleanupStreams();
 503    self.handleWaitResult(status);
 504}
 505
 506fn handleWaitResult(self: *ChildProcess, status: u32) void {
 507    self.term = statusToTerm(status);
 508}
 509
 510fn cleanupStreams(self: *ChildProcess) void {
 511    if (self.stdin) |*stdin| {
 512        stdin.close();
 513        self.stdin = null;
 514    }
 515    if (self.stdout) |*stdout| {
 516        stdout.close();
 517        self.stdout = null;
 518    }
 519    if (self.stderr) |*stderr| {
 520        stderr.close();
 521        self.stderr = null;
 522    }
 523}
 524
 525fn statusToTerm(status: u32) Term {
 526    return if (posix.W.IFEXITED(status))
 527        Term{ .Exited = posix.W.EXITSTATUS(status) }
 528    else if (posix.W.IFSIGNALED(status))
 529        Term{ .Signal = posix.W.TERMSIG(status) }
 530    else if (posix.W.IFSTOPPED(status))
 531        Term{ .Stopped = posix.W.STOPSIG(status) }
 532    else
 533        Term{ .Unknown = status };
 534}
 535
 536fn spawnPosix(self: *ChildProcess) SpawnError!void {
 537    // The child process does need to access (one end of) these pipes. However,
 538    // we must initially set CLOEXEC to avoid a race condition. If another thread
 539    // is racing to spawn a different child process, we don't want it to inherit
 540    // these FDs in any scenario; that would mean that, for instance, calls to
 541    // `poll` from the parent would not report the child's stdout as closing when
 542    // expected, since the other child may retain a reference to the write end of
 543    // the pipe. So, we create the pipes with CLOEXEC initially. After fork, we
 544    // need to do something in the new child to make sure we preserve the reference
 545    // we want. We could use `fcntl` to remove CLOEXEC from the FD, but as it
 546    // turns out, we `dup2` everything anyway, so there's no need!
 547    const pipe_flags: posix.O = .{ .CLOEXEC = true };
 548
 549    const stdin_pipe = if (self.stdin_behavior == .Pipe) try posix.pipe2(pipe_flags) else undefined;
 550    errdefer if (self.stdin_behavior == .Pipe) {
 551        destroyPipe(stdin_pipe);
 552    };
 553
 554    const stdout_pipe = if (self.stdout_behavior == .Pipe) try posix.pipe2(pipe_flags) else undefined;
 555    errdefer if (self.stdout_behavior == .Pipe) {
 556        destroyPipe(stdout_pipe);
 557    };
 558
 559    const stderr_pipe = if (self.stderr_behavior == .Pipe) try posix.pipe2(pipe_flags) else undefined;
 560    errdefer if (self.stderr_behavior == .Pipe) {
 561        destroyPipe(stderr_pipe);
 562    };
 563
 564    const any_ignore = (self.stdin_behavior == .Ignore or self.stdout_behavior == .Ignore or self.stderr_behavior == .Ignore);
 565    const dev_null_fd = if (any_ignore)
 566        posix.openZ("/dev/null", .{ .ACCMODE = .RDWR }, 0) catch |err| switch (err) {
 567            error.PathAlreadyExists => unreachable,
 568            error.NoSpaceLeft => unreachable,
 569            error.FileTooBig => unreachable,
 570            error.DeviceBusy => unreachable,
 571            error.FileLocksNotSupported => unreachable,
 572            error.BadPathName => unreachable, // Windows-only
 573            error.WouldBlock => unreachable,
 574            error.NetworkNotFound => unreachable, // Windows-only
 575            error.Canceled => unreachable, // temporarily in the posix error set
 576            error.SharingViolation => unreachable, // Windows-only
 577            error.PipeBusy => unreachable, // not a pipe
 578            error.AntivirusInterference => unreachable, // Windows-only
 579            else => |e| return e,
 580        }
 581    else
 582        undefined;
 583    defer {
 584        if (any_ignore) posix.close(dev_null_fd);
 585    }
 586
 587    const prog_pipe: [2]posix.fd_t = p: {
 588        if (self.progress_node.index == .none) {
 589            break :p .{ -1, -1 };
 590        } else {
 591            // We use CLOEXEC for the same reason as in `pipe_flags`.
 592            break :p try posix.pipe2(.{ .NONBLOCK = true, .CLOEXEC = true });
 593        }
 594    };
 595    errdefer destroyPipe(prog_pipe);
 596
 597    var arena_allocator = std.heap.ArenaAllocator.init(self.allocator);
 598    defer arena_allocator.deinit();
 599    const arena = arena_allocator.allocator();
 600
 601    // The POSIX standard does not allow malloc() between fork() and execve(),
 602    // and `self.allocator` may be a libc allocator.
 603    // I have personally observed the child process deadlocking when it tries
 604    // to call malloc() due to a heap allocation between fork() and execve(),
 605    // in musl v1.1.24.
 606    // Additionally, we want to reduce the number of possible ways things
 607    // can fail between fork() and execve().
 608    // Therefore, we do all the allocation for the execve() before the fork().
 609    // This means we must do the null-termination of argv and env vars here.
 610    const argv_buf = try arena.allocSentinel(?[*:0]const u8, self.argv.len, null);
 611    for (self.argv, 0..) |arg, i| argv_buf[i] = (try arena.dupeZ(u8, arg)).ptr;
 612
 613    const prog_fileno = 3;
 614    comptime assert(@max(posix.STDIN_FILENO, posix.STDOUT_FILENO, posix.STDERR_FILENO) + 1 == prog_fileno);
 615
 616    const envp: [*:null]const ?[*:0]const u8 = m: {
 617        const prog_fd: i32 = if (prog_pipe[1] == -1) -1 else prog_fileno;
 618        if (self.env_map) |env_map| {
 619            break :m (try process.createEnvironFromMap(arena, env_map, .{
 620                .zig_progress_fd = prog_fd,
 621            })).ptr;
 622        } else if (builtin.link_libc) {
 623            break :m (try process.createEnvironFromExisting(arena, std.c.environ, .{
 624                .zig_progress_fd = prog_fd,
 625            })).ptr;
 626        } else if (builtin.output_mode == .Exe) {
 627            // Then we have Zig start code and this works.
 628            // TODO type-safety for null-termination of `os.environ`.
 629            break :m (try process.createEnvironFromExisting(arena, @ptrCast(std.os.environ.ptr), .{
 630                .zig_progress_fd = prog_fd,
 631            })).ptr;
 632        } else {
 633            // TODO come up with a solution for this.
 634            @panic("missing std lib enhancement: ChildProcess implementation has no way to collect the environment variables to forward to the child process");
 635        }
 636    };
 637
 638    // This pipe communicates to the parent errors in the child between `fork` and `execvpe`.
 639    // It is closed by the child (via CLOEXEC) without writing if `execvpe` succeeds.
 640    const err_pipe: [2]posix.fd_t = try posix.pipe2(.{ .CLOEXEC = true });
 641    errdefer destroyPipe(err_pipe);
 642
 643    const pid_result = try posix.fork();
 644    if (pid_result == 0) {
 645        // we are the child
 646        setUpChildIo(self.stdin_behavior, stdin_pipe[0], posix.STDIN_FILENO, dev_null_fd) catch |err| forkChildErrReport(err_pipe[1], err);
 647        setUpChildIo(self.stdout_behavior, stdout_pipe[1], posix.STDOUT_FILENO, dev_null_fd) catch |err| forkChildErrReport(err_pipe[1], err);
 648        setUpChildIo(self.stderr_behavior, stderr_pipe[1], posix.STDERR_FILENO, dev_null_fd) catch |err| forkChildErrReport(err_pipe[1], err);
 649
 650        if (self.cwd_dir) |cwd| {
 651            posix.fchdir(cwd.fd) catch |err| forkChildErrReport(err_pipe[1], err);
 652        } else if (self.cwd) |cwd| {
 653            posix.chdir(cwd) catch |err| forkChildErrReport(err_pipe[1], err);
 654        }
 655
 656        // Must happen after fchdir above, the cwd file descriptor might be
 657        // equal to prog_fileno and be clobbered by this dup2 call.
 658        if (prog_pipe[1] != -1) posix.dup2(prog_pipe[1], prog_fileno) catch |err| forkChildErrReport(err_pipe[1], err);
 659
 660        if (self.gid) |gid| {
 661            posix.setregid(gid, gid) catch |err| forkChildErrReport(err_pipe[1], err);
 662        }
 663
 664        if (self.uid) |uid| {
 665            posix.setreuid(uid, uid) catch |err| forkChildErrReport(err_pipe[1], err);
 666        }
 667
 668        if (self.pgid) |pid| {
 669            posix.setpgid(0, pid) catch |err| forkChildErrReport(err_pipe[1], err);
 670        }
 671
 672        if (self.start_suspended) {
 673            posix.kill(posix.getpid(), .STOP) catch |err| forkChildErrReport(err_pipe[1], err);
 674        }
 675
 676        const err = switch (self.expand_arg0) {
 677            .expand => posix.execvpeZ_expandArg0(.expand, argv_buf.ptr[0].?, argv_buf.ptr, envp),
 678            .no_expand => posix.execvpeZ_expandArg0(.no_expand, argv_buf.ptr[0].?, argv_buf.ptr, envp),
 679        };
 680        forkChildErrReport(err_pipe[1], err);
 681    }
 682
 683    // we are the parent
 684    errdefer comptime unreachable; // The child is forked; we must not error from now on
 685
 686    posix.close(err_pipe[1]); // make sure only the child holds the write end open
 687    self.err_pipe = err_pipe[0];
 688
 689    const pid: i32 = @intCast(pid_result);
 690    if (self.stdin_behavior == .Pipe) {
 691        self.stdin = .{ .handle = stdin_pipe[1] };
 692    } else {
 693        self.stdin = null;
 694    }
 695    if (self.stdout_behavior == .Pipe) {
 696        self.stdout = .{ .handle = stdout_pipe[0] };
 697    } else {
 698        self.stdout = null;
 699    }
 700    if (self.stderr_behavior == .Pipe) {
 701        self.stderr = .{ .handle = stderr_pipe[0] };
 702    } else {
 703        self.stderr = null;
 704    }
 705
 706    self.id = pid;
 707    self.term = null;
 708
 709    if (self.stdin_behavior == .Pipe) {
 710        posix.close(stdin_pipe[0]);
 711    }
 712    if (self.stdout_behavior == .Pipe) {
 713        posix.close(stdout_pipe[1]);
 714    }
 715    if (self.stderr_behavior == .Pipe) {
 716        posix.close(stderr_pipe[1]);
 717    }
 718
 719    if (prog_pipe[1] != -1) {
 720        posix.close(prog_pipe[1]);
 721    }
 722    self.progress_node.setIpcFd(prog_pipe[0]);
 723}
 724
 725fn spawnWindows(self: *ChildProcess) SpawnError!void {
 726    var saAttr = windows.SECURITY_ATTRIBUTES{
 727        .nLength = @sizeOf(windows.SECURITY_ATTRIBUTES),
 728        .bInheritHandle = windows.TRUE,
 729        .lpSecurityDescriptor = null,
 730    };
 731
 732    const any_ignore = (self.stdin_behavior == StdIo.Ignore or self.stdout_behavior == StdIo.Ignore or self.stderr_behavior == StdIo.Ignore);
 733
 734    const nul_handle = if (any_ignore)
 735        // "\Device\Null" or "\??\NUL"
 736        windows.OpenFile(&[_]u16{ '\\', 'D', 'e', 'v', 'i', 'c', 'e', '\\', 'N', 'u', 'l', 'l' }, .{
 737            .access_mask = windows.GENERIC_READ | windows.GENERIC_WRITE | windows.SYNCHRONIZE,
 738            .share_access = windows.FILE_SHARE_READ | windows.FILE_SHARE_WRITE | windows.FILE_SHARE_DELETE,
 739            .sa = &saAttr,
 740            .creation = windows.OPEN_EXISTING,
 741        }) catch |err| switch (err) {
 742            error.PathAlreadyExists => return error.Unexpected, // not possible for "NUL"
 743            error.PipeBusy => return error.Unexpected, // not possible for "NUL"
 744            error.NoDevice => return error.Unexpected, // not possible for "NUL"
 745            error.FileNotFound => return error.Unexpected, // not possible for "NUL"
 746            error.AccessDenied => return error.Unexpected, // not possible for "NUL"
 747            error.NameTooLong => return error.Unexpected, // not possible for "NUL"
 748            error.WouldBlock => return error.Unexpected, // not possible for "NUL"
 749            error.NetworkNotFound => return error.Unexpected, // not possible for "NUL"
 750            error.AntivirusInterference => return error.Unexpected, // not possible for "NUL"
 751            else => |e| return e,
 752        }
 753    else
 754        undefined;
 755    defer {
 756        if (any_ignore) posix.close(nul_handle);
 757    }
 758
 759    var g_hChildStd_IN_Rd: ?windows.HANDLE = null;
 760    var g_hChildStd_IN_Wr: ?windows.HANDLE = null;
 761    switch (self.stdin_behavior) {
 762        StdIo.Pipe => {
 763            try windowsMakePipeIn(&g_hChildStd_IN_Rd, &g_hChildStd_IN_Wr, &saAttr);
 764        },
 765        StdIo.Ignore => {
 766            g_hChildStd_IN_Rd = nul_handle;
 767        },
 768        StdIo.Inherit => {
 769            g_hChildStd_IN_Rd = windows.GetStdHandle(windows.STD_INPUT_HANDLE) catch null;
 770        },
 771        StdIo.Close => {
 772            g_hChildStd_IN_Rd = null;
 773        },
 774    }
 775    errdefer if (self.stdin_behavior == StdIo.Pipe) {
 776        windowsDestroyPipe(g_hChildStd_IN_Rd, g_hChildStd_IN_Wr);
 777    };
 778
 779    var g_hChildStd_OUT_Rd: ?windows.HANDLE = null;
 780    var g_hChildStd_OUT_Wr: ?windows.HANDLE = null;
 781    switch (self.stdout_behavior) {
 782        StdIo.Pipe => {
 783            try windowsMakeAsyncPipe(&g_hChildStd_OUT_Rd, &g_hChildStd_OUT_Wr, &saAttr);
 784        },
 785        StdIo.Ignore => {
 786            g_hChildStd_OUT_Wr = nul_handle;
 787        },
 788        StdIo.Inherit => {
 789            g_hChildStd_OUT_Wr = windows.GetStdHandle(windows.STD_OUTPUT_HANDLE) catch null;
 790        },
 791        StdIo.Close => {
 792            g_hChildStd_OUT_Wr = null;
 793        },
 794    }
 795    errdefer if (self.stdout_behavior == StdIo.Pipe) {
 796        windowsDestroyPipe(g_hChildStd_OUT_Rd, g_hChildStd_OUT_Wr);
 797    };
 798
 799    var g_hChildStd_ERR_Rd: ?windows.HANDLE = null;
 800    var g_hChildStd_ERR_Wr: ?windows.HANDLE = null;
 801    switch (self.stderr_behavior) {
 802        StdIo.Pipe => {
 803            try windowsMakeAsyncPipe(&g_hChildStd_ERR_Rd, &g_hChildStd_ERR_Wr, &saAttr);
 804        },
 805        StdIo.Ignore => {
 806            g_hChildStd_ERR_Wr = nul_handle;
 807        },
 808        StdIo.Inherit => {
 809            g_hChildStd_ERR_Wr = windows.GetStdHandle(windows.STD_ERROR_HANDLE) catch null;
 810        },
 811        StdIo.Close => {
 812            g_hChildStd_ERR_Wr = null;
 813        },
 814    }
 815    errdefer if (self.stderr_behavior == StdIo.Pipe) {
 816        windowsDestroyPipe(g_hChildStd_ERR_Rd, g_hChildStd_ERR_Wr);
 817    };
 818
 819    var siStartInfo = windows.STARTUPINFOW{
 820        .cb = @sizeOf(windows.STARTUPINFOW),
 821        .hStdError = g_hChildStd_ERR_Wr,
 822        .hStdOutput = g_hChildStd_OUT_Wr,
 823        .hStdInput = g_hChildStd_IN_Rd,
 824        .dwFlags = windows.STARTF_USESTDHANDLES,
 825
 826        .lpReserved = null,
 827        .lpDesktop = null,
 828        .lpTitle = null,
 829        .dwX = 0,
 830        .dwY = 0,
 831        .dwXSize = 0,
 832        .dwYSize = 0,
 833        .dwXCountChars = 0,
 834        .dwYCountChars = 0,
 835        .dwFillAttribute = 0,
 836        .wShowWindow = 0,
 837        .cbReserved2 = 0,
 838        .lpReserved2 = null,
 839    };
 840    var piProcInfo: windows.PROCESS_INFORMATION = undefined;
 841
 842    const cwd_w = if (self.cwd) |cwd| try unicode.wtf8ToWtf16LeAllocZ(self.allocator, cwd) else null;
 843    defer if (cwd_w) |cwd| self.allocator.free(cwd);
 844    const cwd_w_ptr = if (cwd_w) |cwd| cwd.ptr else null;
 845
 846    const maybe_envp_buf = if (self.env_map) |env_map| try process.createWindowsEnvBlock(self.allocator, env_map) else null;
 847    defer if (maybe_envp_buf) |envp_buf| self.allocator.free(envp_buf);
 848    const envp_ptr = if (maybe_envp_buf) |envp_buf| envp_buf.ptr else null;
 849
 850    const app_name_wtf8 = self.argv[0];
 851    const app_name_is_absolute = fs.path.isAbsolute(app_name_wtf8);
 852
 853    // the cwd set in ChildProcess is in effect when choosing the executable path
 854    // to match posix semantics
 855    var cwd_path_w_needs_free = false;
 856    const cwd_path_w = x: {
 857        // If the app name is absolute, then we need to use its dirname as the cwd
 858        if (app_name_is_absolute) {
 859            cwd_path_w_needs_free = true;
 860            const dir = fs.path.dirname(app_name_wtf8).?;
 861            break :x try unicode.wtf8ToWtf16LeAllocZ(self.allocator, dir);
 862        } else if (self.cwd) |cwd| {
 863            cwd_path_w_needs_free = true;
 864            break :x try unicode.wtf8ToWtf16LeAllocZ(self.allocator, cwd);
 865        } else {
 866            break :x &[_:0]u16{}; // empty for cwd
 867        }
 868    };
 869    defer if (cwd_path_w_needs_free) self.allocator.free(cwd_path_w);
 870
 871    // If the app name has more than just a filename, then we need to separate that
 872    // into the basename and dirname and use the dirname as an addition to the cwd
 873    // path. This is because NtQueryDirectoryFile cannot accept FileName params with
 874    // path separators.
 875    const app_basename_wtf8 = fs.path.basename(app_name_wtf8);
 876    // If the app name is absolute, then the cwd will already have the app's dirname in it,
 877    // so only populate app_dirname if app name is a relative path with > 0 path separators.
 878    const maybe_app_dirname_wtf8 = if (!app_name_is_absolute) fs.path.dirname(app_name_wtf8) else null;
 879    const app_dirname_w: ?[:0]u16 = x: {
 880        if (maybe_app_dirname_wtf8) |app_dirname_wtf8| {
 881            break :x try unicode.wtf8ToWtf16LeAllocZ(self.allocator, app_dirname_wtf8);
 882        }
 883        break :x null;
 884    };
 885    defer if (app_dirname_w != null) self.allocator.free(app_dirname_w.?);
 886
 887    const app_name_w = try unicode.wtf8ToWtf16LeAllocZ(self.allocator, app_basename_wtf8);
 888    defer self.allocator.free(app_name_w);
 889
 890    const flags: windows.CreateProcessFlags = .{
 891        .create_suspended = self.start_suspended,
 892        .create_unicode_environment = true,
 893        .create_no_window = self.create_no_window,
 894    };
 895
 896    run: {
 897        const PATH: [:0]const u16 = process.getenvW(unicode.utf8ToUtf16LeStringLiteral("PATH")) orelse &[_:0]u16{};
 898        const PATHEXT: [:0]const u16 = process.getenvW(unicode.utf8ToUtf16LeStringLiteral("PATHEXT")) orelse &[_:0]u16{};
 899
 900        // In case the command ends up being a .bat/.cmd script, we need to escape things using the cmd.exe rules
 901        // and invoke cmd.exe ourselves in order to mitigate arbitrary command execution from maliciously
 902        // constructed arguments.
 903        //
 904        // We'll need to wait until we're actually trying to run the command to know for sure
 905        // if the resolved command has the `.bat` or `.cmd` extension, so we defer actually
 906        // serializing the command line until we determine how it should be serialized.
 907        var cmd_line_cache = WindowsCommandLineCache.init(self.allocator, self.argv);
 908        defer cmd_line_cache.deinit();
 909
 910        var app_buf: ArrayList(u16) = .empty;
 911        defer app_buf.deinit(self.allocator);
 912
 913        try app_buf.appendSlice(self.allocator, app_name_w);
 914
 915        var dir_buf: ArrayList(u16) = .empty;
 916        defer dir_buf.deinit(self.allocator);
 917
 918        if (cwd_path_w.len > 0) {
 919            try dir_buf.appendSlice(self.allocator, cwd_path_w);
 920        }
 921        if (app_dirname_w) |app_dir| {
 922            if (dir_buf.items.len > 0) try dir_buf.append(self.allocator, fs.path.sep);
 923            try dir_buf.appendSlice(self.allocator, app_dir);
 924        }
 925
 926        windowsCreateProcessPathExt(self.allocator, &dir_buf, &app_buf, PATHEXT, &cmd_line_cache, envp_ptr, cwd_w_ptr, flags, &siStartInfo, &piProcInfo) catch |no_path_err| {
 927            const original_err = switch (no_path_err) {
 928                // argv[0] contains unsupported characters that will never resolve to a valid exe.
 929                error.InvalidArg0 => return error.FileNotFound,
 930                error.FileNotFound, error.InvalidExe, error.AccessDenied => |e| e,
 931                error.UnrecoverableInvalidExe => return error.InvalidExe,
 932                else => |e| return e,
 933            };
 934
 935            // If the app name had path separators, that disallows PATH searching,
 936            // and there's no need to search the PATH if the app name is absolute.
 937            // We still search the path if the cwd is absolute because of the
 938            // "cwd set in ChildProcess is in effect when choosing the executable path
 939            // to match posix semantics" behavior--we don't want to skip searching
 940            // the PATH just because we were trying to set the cwd of the child process.
 941            if (app_dirname_w != null or app_name_is_absolute) {
 942                return original_err;
 943            }
 944
 945            var it = mem.tokenizeScalar(u16, PATH, ';');
 946            while (it.next()) |search_path| {
 947                dir_buf.clearRetainingCapacity();
 948                try dir_buf.appendSlice(self.allocator, search_path);
 949
 950                if (windowsCreateProcessPathExt(self.allocator, &dir_buf, &app_buf, PATHEXT, &cmd_line_cache, envp_ptr, cwd_w_ptr, flags, &siStartInfo, &piProcInfo)) {
 951                    break :run;
 952                } else |err| switch (err) {
 953                    // argv[0] contains unsupported characters that will never resolve to a valid exe.
 954                    error.InvalidArg0 => return error.FileNotFound,
 955                    error.FileNotFound, error.AccessDenied, error.InvalidExe => continue,
 956                    error.UnrecoverableInvalidExe => return error.InvalidExe,
 957                    else => |e| return e,
 958                }
 959            } else {
 960                return original_err;
 961            }
 962        };
 963    }
 964
 965    if (g_hChildStd_IN_Wr) |h| {
 966        self.stdin = File{ .handle = h };
 967    } else {
 968        self.stdin = null;
 969    }
 970    if (g_hChildStd_OUT_Rd) |h| {
 971        self.stdout = File{ .handle = h };
 972    } else {
 973        self.stdout = null;
 974    }
 975    if (g_hChildStd_ERR_Rd) |h| {
 976        self.stderr = File{ .handle = h };
 977    } else {
 978        self.stderr = null;
 979    }
 980
 981    self.id = piProcInfo.hProcess;
 982    self.thread_handle = piProcInfo.hThread;
 983    self.term = null;
 984
 985    if (self.stdin_behavior == StdIo.Pipe) {
 986        posix.close(g_hChildStd_IN_Rd.?);
 987    }
 988    if (self.stderr_behavior == StdIo.Pipe) {
 989        posix.close(g_hChildStd_ERR_Wr.?);
 990    }
 991    if (self.stdout_behavior == StdIo.Pipe) {
 992        posix.close(g_hChildStd_OUT_Wr.?);
 993    }
 994}
 995
 996fn setUpChildIo(stdio: StdIo, pipe_fd: i32, std_fileno: i32, dev_null_fd: i32) !void {
 997    switch (stdio) {
 998        .Pipe => try posix.dup2(pipe_fd, std_fileno),
 999        .Close => posix.close(std_fileno),
1000        .Inherit => {},
1001        .Ignore => try posix.dup2(dev_null_fd, std_fileno),
1002    }
1003}
1004
1005fn destroyPipe(pipe: [2]posix.fd_t) void {
1006    if (pipe[0] != -1) posix.close(pipe[0]);
1007    if (pipe[0] != pipe[1]) posix.close(pipe[1]);
1008}
1009
1010// Child of fork calls this to report an error to the fork parent.
1011// Then the child exits.
1012fn forkChildErrReport(fd: i32, err: ChildProcess.SpawnError) noreturn {
1013    writeIntFd(fd, @as(ErrInt, @intFromError(err))) catch {};
1014    // If we're linking libc, some naughty applications may have registered atexit handlers
1015    // which we really do not want to run in the fork child. I caught LLVM doing this and
1016    // it caused a deadlock instead of doing an exit syscall. In the words of Avril Lavigne,
1017    // "Why'd you have to go and make things so complicated?"
1018    if (builtin.link_libc) {
1019        // The _exit(2) function does nothing but make the exit syscall, unlike exit(3)
1020        std.c._exit(1);
1021    }
1022    posix.exit(1);
1023}
1024
1025fn writeIntFd(fd: i32, value: ErrInt) !void {
1026    var buffer: [8]u8 = undefined;
1027    var fw: std.fs.File.Writer = .initStreaming(.{ .handle = fd }, &buffer);
1028    fw.interface.writeInt(u64, value, .little) catch unreachable;
1029    fw.interface.flush() catch return error.SystemResources;
1030}
1031
1032fn readIntFd(fd: i32) !ErrInt {
1033    var buffer: [8]u8 = undefined;
1034    var i: usize = 0;
1035    while (i < buffer.len) {
1036        const n = try std.posix.read(fd, buffer[i..]);
1037        if (n == 0) return error.EndOfStream;
1038        i += n;
1039    }
1040    const int = mem.readInt(u64, &buffer, .little);
1041    return @intCast(int);
1042}
1043
1044const ErrInt = std.meta.Int(.unsigned, @sizeOf(anyerror) * 8);
1045
1046/// Expects `app_buf` to contain exactly the app name, and `dir_buf` to contain exactly the dir path.
1047/// After return, `app_buf` will always contain exactly the app name and `dir_buf` will always contain exactly the dir path.
1048/// Note: `app_buf` should not contain any leading path separators.
1049/// Note: If the dir is the cwd, dir_buf should be empty (len = 0).
1050fn windowsCreateProcessPathExt(
1051    allocator: mem.Allocator,
1052    dir_buf: *ArrayList(u16),
1053    app_buf: *ArrayList(u16),
1054    pathext: [:0]const u16,
1055    cmd_line_cache: *WindowsCommandLineCache,
1056    envp_ptr: ?[*]u16,
1057    cwd_ptr: ?[*:0]u16,
1058    flags: windows.CreateProcessFlags,
1059    lpStartupInfo: *windows.STARTUPINFOW,
1060    lpProcessInformation: *windows.PROCESS_INFORMATION,
1061) !void {
1062    const app_name_len = app_buf.items.len;
1063    const dir_path_len = dir_buf.items.len;
1064
1065    if (app_name_len == 0) return error.FileNotFound;
1066
1067    defer app_buf.shrinkRetainingCapacity(app_name_len);
1068    defer dir_buf.shrinkRetainingCapacity(dir_path_len);
1069
1070    // The name of the game here is to avoid CreateProcessW calls at all costs,
1071    // and only ever try calling it when we have a real candidate for execution.
1072    // Secondarily, we want to minimize the number of syscalls used when checking
1073    // for each PATHEXT-appended version of the app name.
1074    //
1075    // An overview of the technique used:
1076    // - Open the search directory for iteration (either cwd or a path from PATH)
1077    // - Use NtQueryDirectoryFile with a wildcard filename of `<app name>*` to
1078    //   check if anything that could possibly match either the unappended version
1079    //   of the app name or any of the versions with a PATHEXT value appended exists.
1080    // - If the wildcard NtQueryDirectoryFile call found nothing, we can exit early
1081    //   without needing to use PATHEXT at all.
1082    //
1083    // This allows us to use a <open dir, NtQueryDirectoryFile, close dir> sequence
1084    // for any directory that doesn't contain any possible matches, instead of having
1085    // to use a separate look up for each individual filename combination (unappended +
1086    // each PATHEXT appended). For directories where the wildcard *does* match something,
1087    // we iterate the matches and take note of any that are either the unappended version,
1088    // or a version with a supported PATHEXT appended. We then try calling CreateProcessW
1089    // with the found versions in the appropriate order.
1090
1091    // In the future, child process execution needs to move to Io implementation.
1092    // Under those conditions, here we will have access to lower level directory
1093    // opening function knowing which implementation we are in. Here, we imitate
1094    // that scenario.
1095    var threaded: std.Io.Threaded = .init_single_threaded;
1096    const io = threaded.ioBasic();
1097
1098    var dir = dir: {
1099        // needs to be null-terminated
1100        try dir_buf.append(allocator, 0);
1101        defer dir_buf.shrinkRetainingCapacity(dir_path_len);
1102        const dir_path_z = dir_buf.items[0 .. dir_buf.items.len - 1 :0];
1103        const prefixed_path = try windows.wToPrefixedFileW(null, dir_path_z);
1104        break :dir threaded.dirOpenDirWindows(.cwd(), prefixed_path.span(), .{
1105            .iterate = true,
1106        }) catch return error.FileNotFound;
1107    };
1108    defer dir.close(io);
1109
1110    // Add wildcard and null-terminator
1111    try app_buf.append(allocator, '*');
1112    try app_buf.append(allocator, 0);
1113    const app_name_wildcard = app_buf.items[0 .. app_buf.items.len - 1 :0];
1114
1115    // This 2048 is arbitrary, we just want it to be large enough to get multiple FILE_DIRECTORY_INFORMATION entries
1116    // returned per NtQueryDirectoryFile call.
1117    var file_information_buf: [2048]u8 align(@alignOf(windows.FILE_DIRECTORY_INFORMATION)) = undefined;
1118    const file_info_maximum_single_entry_size = @sizeOf(windows.FILE_DIRECTORY_INFORMATION) + (windows.NAME_MAX * 2);
1119    if (file_information_buf.len < file_info_maximum_single_entry_size) {
1120        @compileError("file_information_buf must be large enough to contain at least one maximum size FILE_DIRECTORY_INFORMATION entry");
1121    }
1122    var io_status: windows.IO_STATUS_BLOCK = undefined;
1123
1124    const num_supported_pathext = @typeInfo(WindowsExtension).@"enum".fields.len;
1125    var pathext_seen = [_]bool{false} ** num_supported_pathext;
1126    var any_pathext_seen = false;
1127    var unappended_exists = false;
1128
1129    // Fully iterate the wildcard matches via NtQueryDirectoryFile and take note of all versions
1130    // of the app_name we should try to spawn.
1131    // Note: This is necessary because the order of the files returned is filesystem-dependent:
1132    //       On NTFS, `blah.exe*` will always return `blah.exe` first if it exists.
1133    //       On FAT32, it's possible for something like `blah.exe.obj` to be returned first.
1134    while (true) {
1135        const app_name_len_bytes = std.math.cast(u16, app_name_wildcard.len * 2) orelse return error.NameTooLong;
1136        var app_name_unicode_string = windows.UNICODE_STRING{
1137            .Length = app_name_len_bytes,
1138            .MaximumLength = app_name_len_bytes,
1139            .Buffer = @constCast(app_name_wildcard.ptr),
1140        };
1141        const rc = windows.ntdll.NtQueryDirectoryFile(
1142            dir.handle,
1143            null,
1144            null,
1145            null,
1146            &io_status,
1147            &file_information_buf,
1148            file_information_buf.len,
1149            .FileDirectoryInformation,
1150            windows.FALSE, // single result
1151            &app_name_unicode_string,
1152            windows.FALSE, // restart iteration
1153        );
1154
1155        // If we get nothing with the wildcard, then we can just bail out
1156        // as we know appending PATHEXT will not yield anything.
1157        switch (rc) {
1158            .SUCCESS => {},
1159            .NO_SUCH_FILE => return error.FileNotFound,
1160            .NO_MORE_FILES => break,
1161            .ACCESS_DENIED => return error.AccessDenied,
1162            else => return windows.unexpectedStatus(rc),
1163        }
1164
1165        // According to the docs, this can only happen if there is not enough room in the
1166        // buffer to write at least one complete FILE_DIRECTORY_INFORMATION entry.
1167        // Therefore, this condition should not be possible to hit with the buffer size we use.
1168        std.debug.assert(io_status.Information != 0);
1169
1170        var it = windows.FileInformationIterator(windows.FILE_DIRECTORY_INFORMATION){ .buf = &file_information_buf };
1171        while (it.next()) |info| {
1172            // Skip directories
1173            if (info.FileAttributes & windows.FILE_ATTRIBUTE_DIRECTORY != 0) continue;
1174            const filename = @as([*]u16, @ptrCast(&info.FileName))[0 .. info.FileNameLength / 2];
1175            // Because all results start with the app_name since we're using the wildcard `app_name*`,
1176            // if the length is equal to app_name then this is an exact match
1177            if (filename.len == app_name_len) {
1178                // Note: We can't break early here because it's possible that the unappended version
1179                //       fails to spawn, in which case we still want to try the PATHEXT appended versions.
1180                unappended_exists = true;
1181            } else if (windowsCreateProcessSupportsExtension(filename[app_name_len..])) |pathext_ext| {
1182                pathext_seen[@intFromEnum(pathext_ext)] = true;
1183                any_pathext_seen = true;
1184            }
1185        }
1186    }
1187
1188    const unappended_err = unappended: {
1189        if (unappended_exists) {
1190            if (dir_path_len != 0) switch (dir_buf.items[dir_buf.items.len - 1]) {
1191                '/', '\\' => {},
1192                else => try dir_buf.append(allocator, fs.path.sep),
1193            };
1194            try dir_buf.appendSlice(allocator, app_buf.items[0..app_name_len]);
1195            try dir_buf.append(allocator, 0);
1196            const full_app_name = dir_buf.items[0 .. dir_buf.items.len - 1 :0];
1197
1198            const is_bat_or_cmd = bat_or_cmd: {
1199                const app_name = app_buf.items[0..app_name_len];
1200                const ext_start = std.mem.lastIndexOfScalar(u16, app_name, '.') orelse break :bat_or_cmd false;
1201                const ext = app_name[ext_start..];
1202                const ext_enum = windowsCreateProcessSupportsExtension(ext) orelse break :bat_or_cmd false;
1203                switch (ext_enum) {
1204                    .cmd, .bat => break :bat_or_cmd true,
1205                    else => break :bat_or_cmd false,
1206                }
1207            };
1208            const cmd_line_w = if (is_bat_or_cmd)
1209                try cmd_line_cache.scriptCommandLine(full_app_name)
1210            else
1211                try cmd_line_cache.commandLine();
1212            const app_name_w = if (is_bat_or_cmd)
1213                try cmd_line_cache.cmdExePath()
1214            else
1215                full_app_name;
1216
1217            if (windowsCreateProcess(app_name_w.ptr, cmd_line_w.ptr, envp_ptr, cwd_ptr, flags, lpStartupInfo, lpProcessInformation)) |_| {
1218                return;
1219            } else |err| switch (err) {
1220                error.FileNotFound,
1221                error.AccessDenied,
1222                => break :unappended err,
1223                error.InvalidExe => {
1224                    // On InvalidExe, if the extension of the app name is .exe then
1225                    // it's treated as an unrecoverable error. Otherwise, it'll be
1226                    // skipped as normal.
1227                    const app_name = app_buf.items[0..app_name_len];
1228                    const ext_start = std.mem.lastIndexOfScalar(u16, app_name, '.') orelse break :unappended err;
1229                    const ext = app_name[ext_start..];
1230                    if (windows.eqlIgnoreCaseWtf16(ext, unicode.utf8ToUtf16LeStringLiteral(".EXE"))) {
1231                        return error.UnrecoverableInvalidExe;
1232                    }
1233                    break :unappended err;
1234                },
1235                else => return err,
1236            }
1237        }
1238        break :unappended error.FileNotFound;
1239    };
1240
1241    if (!any_pathext_seen) return unappended_err;
1242
1243    // Now try any PATHEXT appended versions that we've seen
1244    var ext_it = mem.tokenizeScalar(u16, pathext, ';');
1245    while (ext_it.next()) |ext| {
1246        const ext_enum = windowsCreateProcessSupportsExtension(ext) orelse continue;
1247        if (!pathext_seen[@intFromEnum(ext_enum)]) continue;
1248
1249        dir_buf.shrinkRetainingCapacity(dir_path_len);
1250        if (dir_path_len != 0) switch (dir_buf.items[dir_buf.items.len - 1]) {
1251            '/', '\\' => {},
1252            else => try dir_buf.append(allocator, fs.path.sep),
1253        };
1254        try dir_buf.appendSlice(allocator, app_buf.items[0..app_name_len]);
1255        try dir_buf.appendSlice(allocator, ext);
1256        try dir_buf.append(allocator, 0);
1257        const full_app_name = dir_buf.items[0 .. dir_buf.items.len - 1 :0];
1258
1259        const is_bat_or_cmd = switch (ext_enum) {
1260            .cmd, .bat => true,
1261            else => false,
1262        };
1263        const cmd_line_w = if (is_bat_or_cmd)
1264            try cmd_line_cache.scriptCommandLine(full_app_name)
1265        else
1266            try cmd_line_cache.commandLine();
1267        const app_name_w = if (is_bat_or_cmd)
1268            try cmd_line_cache.cmdExePath()
1269        else
1270            full_app_name;
1271
1272        if (windowsCreateProcess(app_name_w.ptr, cmd_line_w.ptr, envp_ptr, cwd_ptr, flags, lpStartupInfo, lpProcessInformation)) |_| {
1273            return;
1274        } else |err| switch (err) {
1275            error.FileNotFound => continue,
1276            error.AccessDenied => continue,
1277            error.InvalidExe => {
1278                // On InvalidExe, if the extension of the app name is .exe then
1279                // it's treated as an unrecoverable error. Otherwise, it'll be
1280                // skipped as normal.
1281                if (windows.eqlIgnoreCaseWtf16(ext, unicode.utf8ToUtf16LeStringLiteral(".EXE"))) {
1282                    return error.UnrecoverableInvalidExe;
1283                }
1284                continue;
1285            },
1286            else => return err,
1287        }
1288    }
1289
1290    return unappended_err;
1291}
1292
1293fn windowsCreateProcess(
1294    app_name: [*:0]u16,
1295    cmd_line: [*:0]u16,
1296    envp_ptr: ?[*]u16,
1297    cwd_ptr: ?[*:0]u16,
1298    flags: windows.CreateProcessFlags,
1299    lpStartupInfo: *windows.STARTUPINFOW,
1300    lpProcessInformation: *windows.PROCESS_INFORMATION,
1301) !void {
1302    // TODO the docs for environment pointer say:
1303    // > A pointer to the environment block for the new process. If this parameter
1304    // > is NULL, the new process uses the environment of the calling process.
1305    // > ...
1306    // > An environment block can contain either Unicode or ANSI characters. If
1307    // > the environment block pointed to by lpEnvironment contains Unicode
1308    // > characters, be sure that dwCreationFlags includes CREATE_UNICODE_ENVIRONMENT.
1309    // > If this parameter is NULL and the environment block of the parent process
1310    // > contains Unicode characters, you must also ensure that dwCreationFlags
1311    // > includes CREATE_UNICODE_ENVIRONMENT.
1312    // This seems to imply that we have to somehow know whether our process parent passed
1313    // CREATE_UNICODE_ENVIRONMENT if we want to pass NULL for the environment parameter.
1314    // Since we do not know this information that would imply that we must not pass NULL
1315    // for the parameter.
1316    // However this would imply that programs compiled with -DUNICODE could not pass
1317    // environment variables to programs that were not, which seems unlikely.
1318    // More investigation is needed.
1319    return windows.CreateProcessW(
1320        app_name,
1321        cmd_line,
1322        null,
1323        null,
1324        windows.TRUE,
1325        flags,
1326        @as(?*anyopaque, @ptrCast(envp_ptr)),
1327        cwd_ptr,
1328        lpStartupInfo,
1329        lpProcessInformation,
1330    );
1331}
1332
1333fn windowsMakePipeIn(rd: *?windows.HANDLE, wr: *?windows.HANDLE, sattr: *const windows.SECURITY_ATTRIBUTES) !void {
1334    var rd_h: windows.HANDLE = undefined;
1335    var wr_h: windows.HANDLE = undefined;
1336    try windows.CreatePipe(&rd_h, &wr_h, sattr);
1337    errdefer windowsDestroyPipe(rd_h, wr_h);
1338    try windows.SetHandleInformation(wr_h, windows.HANDLE_FLAG_INHERIT, 0);
1339    rd.* = rd_h;
1340    wr.* = wr_h;
1341}
1342
1343fn windowsDestroyPipe(rd: ?windows.HANDLE, wr: ?windows.HANDLE) void {
1344    if (rd) |h| posix.close(h);
1345    if (wr) |h| posix.close(h);
1346}
1347
1348fn windowsMakeAsyncPipe(rd: *?windows.HANDLE, wr: *?windows.HANDLE, sattr: *const windows.SECURITY_ATTRIBUTES) !void {
1349    var tmp_bufw: [128]u16 = undefined;
1350
1351    // Anonymous pipes are built upon Named pipes.
1352    // https://docs.microsoft.com/en-us/windows/win32/api/namedpipeapi/nf-namedpipeapi-createpipe
1353    // Asynchronous (overlapped) read and write operations are not supported by anonymous pipes.
1354    // https://docs.microsoft.com/en-us/windows/win32/ipc/anonymous-pipe-operations
1355    const pipe_path = blk: {
1356        var tmp_buf: [128]u8 = undefined;
1357        // Forge a random path for the pipe.
1358        const pipe_path = std.fmt.bufPrintSentinel(
1359            &tmp_buf,
1360            "\\\\.\\pipe\\zig-childprocess-{d}-{d}",
1361            .{ windows.GetCurrentProcessId(), pipe_name_counter.fetchAdd(1, .monotonic) },
1362            0,
1363        ) catch unreachable;
1364        const len = std.unicode.wtf8ToWtf16Le(&tmp_bufw, pipe_path) catch unreachable;
1365        tmp_bufw[len] = 0;
1366        break :blk tmp_bufw[0..len :0];
1367    };
1368
1369    // Create the read handle that can be used with overlapped IO ops.
1370    const read_handle = windows.kernel32.CreateNamedPipeW(
1371        pipe_path.ptr,
1372        windows.PIPE_ACCESS_INBOUND | windows.FILE_FLAG_OVERLAPPED,
1373        windows.PIPE_TYPE_BYTE,
1374        1,
1375        4096,
1376        4096,
1377        0,
1378        sattr,
1379    );
1380    if (read_handle == windows.INVALID_HANDLE_VALUE) {
1381        switch (windows.GetLastError()) {
1382            else => |err| return windows.unexpectedError(err),
1383        }
1384    }
1385    errdefer posix.close(read_handle);
1386
1387    var sattr_copy = sattr.*;
1388    const write_handle = windows.kernel32.CreateFileW(
1389        pipe_path.ptr,
1390        windows.GENERIC_WRITE,
1391        0,
1392        &sattr_copy,
1393        windows.OPEN_EXISTING,
1394        windows.FILE_ATTRIBUTE_NORMAL,
1395        null,
1396    );
1397    if (write_handle == windows.INVALID_HANDLE_VALUE) {
1398        switch (windows.GetLastError()) {
1399            else => |err| return windows.unexpectedError(err),
1400        }
1401    }
1402    errdefer posix.close(write_handle);
1403
1404    try windows.SetHandleInformation(read_handle, windows.HANDLE_FLAG_INHERIT, 0);
1405
1406    rd.* = read_handle;
1407    wr.* = write_handle;
1408}
1409
1410var pipe_name_counter = std.atomic.Value(u32).init(1);
1411
1412/// File name extensions supported natively by `CreateProcess()` on Windows.
1413// Should be kept in sync with `windowsCreateProcessSupportsExtension`.
1414pub const WindowsExtension = enum {
1415    bat,
1416    cmd,
1417    com,
1418    exe,
1419};
1420
1421/// Case-insensitive WTF-16 lookup
1422fn windowsCreateProcessSupportsExtension(ext: []const u16) ?WindowsExtension {
1423    if (ext.len != 4) return null;
1424    const State = enum {
1425        start,
1426        dot,
1427        b,
1428        ba,
1429        c,
1430        cm,
1431        co,
1432        e,
1433        ex,
1434    };
1435    var state: State = .start;
1436    for (ext) |c| switch (state) {
1437        .start => switch (c) {
1438            '.' => state = .dot,
1439            else => return null,
1440        },
1441        .dot => switch (c) {
1442            'b', 'B' => state = .b,
1443            'c', 'C' => state = .c,
1444            'e', 'E' => state = .e,
1445            else => return null,
1446        },
1447        .b => switch (c) {
1448            'a', 'A' => state = .ba,
1449            else => return null,
1450        },
1451        .c => switch (c) {
1452            'm', 'M' => state = .cm,
1453            'o', 'O' => state = .co,
1454            else => return null,
1455        },
1456        .e => switch (c) {
1457            'x', 'X' => state = .ex,
1458            else => return null,
1459        },
1460        .ba => switch (c) {
1461            't', 'T' => return .bat,
1462            else => return null,
1463        },
1464        .cm => switch (c) {
1465            'd', 'D' => return .cmd,
1466            else => return null,
1467        },
1468        .co => switch (c) {
1469            'm', 'M' => return .com,
1470            else => return null,
1471        },
1472        .ex => switch (c) {
1473            'e', 'E' => return .exe,
1474            else => return null,
1475        },
1476    };
1477    return null;
1478}
1479
1480test windowsCreateProcessSupportsExtension {
1481    try std.testing.expectEqual(WindowsExtension.exe, windowsCreateProcessSupportsExtension(&[_]u16{ '.', 'e', 'X', 'e' }).?);
1482    try std.testing.expect(windowsCreateProcessSupportsExtension(&[_]u16{ '.', 'e', 'X', 'e', 'c' }) == null);
1483}
1484
1485/// Serializes argv into a WTF-16 encoded command-line string for use with CreateProcessW.
1486///
1487/// Serialization is done on-demand and the result is cached in order to allow for:
1488/// - Only serializing the particular type of command line needed (`.bat`/`.cmd`
1489///   command line serialization is different from `.exe`/etc)
1490/// - Reusing the serialized command lines if necessary (i.e. if the execution
1491///   of a command fails and the PATH is going to be continued to be searched
1492///   for more candidates)
1493const WindowsCommandLineCache = struct {
1494    cmd_line: ?[:0]u16 = null,
1495    script_cmd_line: ?[:0]u16 = null,
1496    cmd_exe_path: ?[:0]u16 = null,
1497    argv: []const []const u8,
1498    allocator: mem.Allocator,
1499
1500    fn init(allocator: mem.Allocator, argv: []const []const u8) WindowsCommandLineCache {
1501        return .{
1502            .allocator = allocator,
1503            .argv = argv,
1504        };
1505    }
1506
1507    fn deinit(self: *WindowsCommandLineCache) void {
1508        if (self.cmd_line) |cmd_line| self.allocator.free(cmd_line);
1509        if (self.script_cmd_line) |script_cmd_line| self.allocator.free(script_cmd_line);
1510        if (self.cmd_exe_path) |cmd_exe_path| self.allocator.free(cmd_exe_path);
1511    }
1512
1513    fn commandLine(self: *WindowsCommandLineCache) ![:0]u16 {
1514        if (self.cmd_line == null) {
1515            self.cmd_line = try argvToCommandLineWindows(self.allocator, self.argv);
1516        }
1517        return self.cmd_line.?;
1518    }
1519
1520    /// Not cached, since the path to the batch script will change during PATH searching.
1521    /// `script_path` should be as qualified as possible, e.g. if the PATH is being searched,
1522    /// then script_path should include both the search path and the script filename
1523    /// (this allows avoiding cmd.exe having to search the PATH again).
1524    fn scriptCommandLine(self: *WindowsCommandLineCache, script_path: []const u16) ![:0]u16 {
1525        if (self.script_cmd_line) |v| self.allocator.free(v);
1526        self.script_cmd_line = try argvToScriptCommandLineWindows(
1527            self.allocator,
1528            script_path,
1529            self.argv[1..],
1530        );
1531        return self.script_cmd_line.?;
1532    }
1533
1534    fn cmdExePath(self: *WindowsCommandLineCache) ![:0]u16 {
1535        if (self.cmd_exe_path == null) {
1536            self.cmd_exe_path = try windowsCmdExePath(self.allocator);
1537        }
1538        return self.cmd_exe_path.?;
1539    }
1540};
1541
1542/// Returns the absolute path of `cmd.exe` within the Windows system directory.
1543/// The caller owns the returned slice.
1544fn windowsCmdExePath(allocator: mem.Allocator) error{ OutOfMemory, Unexpected }![:0]u16 {
1545    var buf = try ArrayList(u16).initCapacity(allocator, 128);
1546    errdefer buf.deinit(allocator);
1547    while (true) {
1548        const unused_slice = buf.unusedCapacitySlice();
1549        // TODO: Get the system directory from PEB.ReadOnlyStaticServerData
1550        const len = windows.kernel32.GetSystemDirectoryW(@ptrCast(unused_slice), @intCast(unused_slice.len));
1551        if (len == 0) {
1552            switch (windows.GetLastError()) {
1553                else => |err| return windows.unexpectedError(err),
1554            }
1555        }
1556        if (len > unused_slice.len) {
1557            try buf.ensureUnusedCapacity(allocator, len);
1558        } else {
1559            buf.items.len = len;
1560            break;
1561        }
1562    }
1563    switch (buf.items[buf.items.len - 1]) {
1564        '/', '\\' => {},
1565        else => try buf.append(allocator, fs.path.sep),
1566    }
1567    try buf.appendSlice(allocator, unicode.utf8ToUtf16LeStringLiteral("cmd.exe"));
1568    return try buf.toOwnedSliceSentinel(allocator, 0);
1569}
1570
1571const ArgvToCommandLineError = error{ OutOfMemory, InvalidWtf8, InvalidArg0 };
1572
1573/// Serializes `argv` to a Windows command-line string suitable for passing to a child process and
1574/// parsing by the `CommandLineToArgvW` algorithm. The caller owns the returned slice.
1575///
1576/// To avoid arbitrary command execution, this function should not be used when spawning `.bat`/`.cmd` scripts.
1577/// https://flatt.tech/research/posts/batbadbut-you-cant-securely-execute-commands-on-windows/
1578///
1579/// When executing `.bat`/`.cmd` scripts, use `argvToScriptCommandLineWindows` instead.
1580fn argvToCommandLineWindows(
1581    allocator: mem.Allocator,
1582    argv: []const []const u8,
1583) ArgvToCommandLineError![:0]u16 {
1584    var buf = std.array_list.Managed(u8).init(allocator);
1585    defer buf.deinit();
1586
1587    if (argv.len != 0) {
1588        const arg0 = argv[0];
1589
1590        // The first argument must be quoted if it contains spaces or ASCII control characters
1591        // (excluding DEL). It also follows special quoting rules where backslashes have no special
1592        // interpretation, which makes it impossible to pass certain first arguments containing
1593        // double quotes to a child process without characters from the first argument leaking into
1594        // subsequent ones (which could have security implications).
1595        //
1596        // Empty arguments technically don't need quotes, but we quote them anyway for maximum
1597        // compatibility with different implementations of the 'CommandLineToArgvW' algorithm.
1598        //
1599        // Double quotes are illegal in paths on Windows, so for the sake of simplicity we reject
1600        // all first arguments containing double quotes, even ones that we could theoretically
1601        // serialize in unquoted form.
1602        var needs_quotes = arg0.len == 0;
1603        for (arg0) |c| {
1604            if (c <= ' ') {
1605                needs_quotes = true;
1606            } else if (c == '"') {
1607                return error.InvalidArg0;
1608            }
1609        }
1610        if (needs_quotes) {
1611            try buf.append('"');
1612            try buf.appendSlice(arg0);
1613            try buf.append('"');
1614        } else {
1615            try buf.appendSlice(arg0);
1616        }
1617
1618        for (argv[1..]) |arg| {
1619            try buf.append(' ');
1620
1621            // Subsequent arguments must be quoted if they contain spaces, tabs or double quotes,
1622            // or if they are empty. For simplicity and for maximum compatibility with different
1623            // implementations of the 'CommandLineToArgvW' algorithm, we also quote all ASCII
1624            // control characters (again, excluding DEL).
1625            needs_quotes = for (arg) |c| {
1626                if (c <= ' ' or c == '"') {
1627                    break true;
1628                }
1629            } else arg.len == 0;
1630            if (!needs_quotes) {
1631                try buf.appendSlice(arg);
1632                continue;
1633            }
1634
1635            try buf.append('"');
1636            var backslash_count: usize = 0;
1637            for (arg) |byte| {
1638                switch (byte) {
1639                    '\\' => {
1640                        backslash_count += 1;
1641                    },
1642                    '"' => {
1643                        try buf.appendNTimes('\\', backslash_count * 2 + 1);
1644                        try buf.append('"');
1645                        backslash_count = 0;
1646                    },
1647                    else => {
1648                        try buf.appendNTimes('\\', backslash_count);
1649                        try buf.append(byte);
1650                        backslash_count = 0;
1651                    },
1652                }
1653            }
1654            try buf.appendNTimes('\\', backslash_count * 2);
1655            try buf.append('"');
1656        }
1657    }
1658
1659    return try unicode.wtf8ToWtf16LeAllocZ(allocator, buf.items);
1660}
1661
1662test argvToCommandLineWindows {
1663    const t = testArgvToCommandLineWindows;
1664
1665    try t(&.{
1666        \\C:\Program Files\zig\zig.exe
1667        ,
1668        \\run
1669        ,
1670        \\.\src\main.zig
1671        ,
1672        \\-target
1673        ,
1674        \\x86_64-windows-gnu
1675        ,
1676        \\-O
1677        ,
1678        \\ReleaseSafe
1679        ,
1680        \\--
1681        ,
1682        \\--emoji=๐Ÿ—ฟ
1683        ,
1684        \\--eval=new Regex("Dwayne \"The Rock\" Johnson")
1685        ,
1686    },
1687        \\"C:\Program Files\zig\zig.exe" run .\src\main.zig -target x86_64-windows-gnu -O ReleaseSafe -- --emoji=๐Ÿ—ฟ "--eval=new Regex(\"Dwayne \\\"The Rock\\\" Johnson\")"
1688    );
1689
1690    try t(&.{}, "");
1691    try t(&.{""}, "\"\"");
1692    try t(&.{" "}, "\" \"");
1693    try t(&.{"\t"}, "\"\t\"");
1694    try t(&.{"\x07"}, "\"\x07\"");
1695    try t(&.{"๐ŸฆŽ"}, "๐ŸฆŽ");
1696
1697    try t(
1698        &.{ "zig", "aa aa", "bb\tbb", "cc\ncc", "dd\r\ndd", "ee\x7Fee" },
1699        "zig \"aa aa\" \"bb\tbb\" \"cc\ncc\" \"dd\r\ndd\" ee\x7Fee",
1700    );
1701
1702    try t(
1703        &.{ "\\\\foo bar\\foo bar\\", "\\\\zig zag\\zig zag\\" },
1704        "\"\\\\foo bar\\foo bar\\\" \"\\\\zig zag\\zig zag\\\\\"",
1705    );
1706
1707    try std.testing.expectError(
1708        error.InvalidArg0,
1709        argvToCommandLineWindows(std.testing.allocator, &.{"\"quotes\"quotes\""}),
1710    );
1711    try std.testing.expectError(
1712        error.InvalidArg0,
1713        argvToCommandLineWindows(std.testing.allocator, &.{"quotes\"quotes"}),
1714    );
1715    try std.testing.expectError(
1716        error.InvalidArg0,
1717        argvToCommandLineWindows(std.testing.allocator, &.{"q u o t e s \" q u o t e s"}),
1718    );
1719}
1720
1721fn testArgvToCommandLineWindows(argv: []const []const u8, expected_cmd_line: []const u8) !void {
1722    const cmd_line_w = try argvToCommandLineWindows(std.testing.allocator, argv);
1723    defer std.testing.allocator.free(cmd_line_w);
1724
1725    const cmd_line = try unicode.wtf16LeToWtf8Alloc(std.testing.allocator, cmd_line_w);
1726    defer std.testing.allocator.free(cmd_line);
1727
1728    try std.testing.expectEqualStrings(expected_cmd_line, cmd_line);
1729}
1730
1731const ArgvToScriptCommandLineError = error{
1732    OutOfMemory,
1733    InvalidWtf8,
1734    /// NUL (U+0000), LF (U+000A), CR (U+000D) are not allowed
1735    /// within arguments when executing a `.bat`/`.cmd` script.
1736    /// - NUL/LF signifiies end of arguments, so anything afterwards
1737    ///   would be lost after execution.
1738    /// - CR is stripped by `cmd.exe`, so any CR codepoints
1739    ///   would be lost after execution.
1740    InvalidBatchScriptArg,
1741};
1742
1743/// Serializes `argv` to a Windows command-line string that uses `cmd.exe /c` and `cmd.exe`-specific
1744/// escaping rules. The caller owns the returned slice.
1745///
1746/// Escapes `argv` using the suggested mitigation against arbitrary command execution from:
1747/// https://flatt.tech/research/posts/batbadbut-you-cant-securely-execute-commands-on-windows/
1748///
1749/// The return of this function will look like
1750/// `cmd.exe /d /e:ON /v:OFF /c "<escaped command line>"`
1751/// and should be used as the `lpCommandLine` of `CreateProcessW`, while the
1752/// return of `windowsCmdExePath` should be used as `lpApplicationName`.
1753///
1754/// Should only be used when spawning `.bat`/`.cmd` scripts, see `argvToCommandLineWindows` otherwise.
1755/// The `.bat`/`.cmd` file must be known to both have the `.bat`/`.cmd` extension and exist on the filesystem.
1756fn argvToScriptCommandLineWindows(
1757    allocator: mem.Allocator,
1758    /// Path to the `.bat`/`.cmd` script. If this path is relative, it is assumed to be relative to the CWD.
1759    /// The script must have been verified to exist at this path before calling this function.
1760    script_path: []const u16,
1761    /// Arguments, not including the script name itself. Expected to be encoded as WTF-8.
1762    script_args: []const []const u8,
1763) ArgvToScriptCommandLineError![:0]u16 {
1764    var buf = try std.array_list.Managed(u8).initCapacity(allocator, 64);
1765    defer buf.deinit();
1766
1767    // `/d` disables execution of AutoRun commands.
1768    // `/e:ON` and `/v:OFF` are needed for BatBadBut mitigation:
1769    // > If delayed expansion is enabled via the registry value DelayedExpansion,
1770    // > it must be disabled by explicitly calling cmd.exe with the /V:OFF option.
1771    // > Escaping for % requires the command extension to be enabled.
1772    // > If itโ€™s disabled via the registry value EnableExtensions, it must be enabled with the /E:ON option.
1773    // https://flatt.tech/research/posts/batbadbut-you-cant-securely-execute-commands-on-windows/
1774    buf.appendSliceAssumeCapacity("cmd.exe /d /e:ON /v:OFF /c \"");
1775
1776    // Always quote the path to the script arg
1777    buf.appendAssumeCapacity('"');
1778    // We always want the path to the batch script to include a path separator in order to
1779    // avoid cmd.exe searching the PATH for the script. This is not part of the arbitrary
1780    // command execution mitigation, we just know exactly what script we want to execute
1781    // at this point, and potentially making cmd.exe re-find it is unnecessary.
1782    //
1783    // If the script path does not have a path separator, then we know its relative to CWD and
1784    // we can just put `.\` in the front.
1785    if (mem.indexOfAny(u16, script_path, &[_]u16{ mem.nativeToLittle(u16, '\\'), mem.nativeToLittle(u16, '/') }) == null) {
1786        try buf.appendSlice(".\\");
1787    }
1788    // Note that we don't do any escaping/mitigations for this argument, since the relevant
1789    // characters (", %, etc) are illegal in file paths and this function should only be called
1790    // with script paths that have been verified to exist.
1791    try unicode.wtf16LeToWtf8ArrayList(&buf, script_path);
1792    buf.appendAssumeCapacity('"');
1793
1794    for (script_args) |arg| {
1795        // Literal carriage returns get stripped when run through cmd.exe
1796        // and NUL/newlines act as 'end of command.' Because of this, it's basically
1797        // always a mistake to include these characters in argv, so it's
1798        // an error condition in order to ensure that the return of this
1799        // function can always roundtrip through cmd.exe.
1800        if (std.mem.indexOfAny(u8, arg, "\x00\r\n") != null) {
1801            return error.InvalidBatchScriptArg;
1802        }
1803
1804        // Separate args with a space.
1805        try buf.append(' ');
1806
1807        // Need to quote if the argument is empty (otherwise the arg would just be lost)
1808        // or if the last character is a `\`, since then something like "%~2" in a .bat
1809        // script would cause the closing " to be escaped which we don't want.
1810        var needs_quotes = arg.len == 0 or arg[arg.len - 1] == '\\';
1811        if (!needs_quotes) {
1812            for (arg) |c| {
1813                switch (c) {
1814                    // Known good characters that don't need to be quoted
1815                    'A'...'Z', 'a'...'z', '0'...'9', '#', '$', '*', '+', '-', '.', '/', ':', '?', '@', '\\', '_' => {},
1816                    // When in doubt, quote
1817                    else => {
1818                        needs_quotes = true;
1819                        break;
1820                    },
1821                }
1822            }
1823        }
1824        if (needs_quotes) {
1825            try buf.append('"');
1826        }
1827        var backslashes: usize = 0;
1828        for (arg) |c| {
1829            switch (c) {
1830                '\\' => {
1831                    backslashes += 1;
1832                },
1833                '"' => {
1834                    try buf.appendNTimes('\\', backslashes);
1835                    try buf.append('"');
1836                    backslashes = 0;
1837                },
1838                // Replace `%` with `%%cd:~,%`.
1839                //
1840                // cmd.exe allows extracting a substring from an environment
1841                // variable with the syntax: `%foo:~<start_index>,<end_index>%`.
1842                // Therefore, `%cd:~,%` will always expand to an empty string
1843                // since both the start and end index are blank, and it is assumed
1844                // that `%cd%` is always available since it is a built-in variable
1845                // that corresponds to the current directory.
1846                //
1847                // This means that replacing `%foo%` with `%%cd:~,%foo%%cd:~,%`
1848                // will stop `%foo%` from being expanded and *after* expansion
1849                // we'll still be left with `%foo%` (the literal string).
1850                '%' => {
1851                    // the trailing `%` is appended outside the switch
1852                    try buf.appendSlice("%%cd:~,");
1853                    backslashes = 0;
1854                },
1855                else => {
1856                    backslashes = 0;
1857                },
1858            }
1859            try buf.append(c);
1860        }
1861        if (needs_quotes) {
1862            try buf.appendNTimes('\\', backslashes);
1863            try buf.append('"');
1864        }
1865    }
1866
1867    try buf.append('"');
1868
1869    return try unicode.wtf8ToWtf16LeAllocZ(allocator, buf.items);
1870}