//
// Syd: rock-solid unikernel
// src/syd.rs: Main entry point
//
// Copyright (c) 2023, 2024 Ali Polatel <alip@chesswob.org>
// Proxying code is based in part upon socksns crate which is:
//     Copyright (c) 2020 Steven Engler
//     SPDX-License-Identifier: MIT
//
// SPDX-License-Identifier: GPL-3.0-or-later

//! Syd: rock-solid unikernel
//! Main entry point.

// We like clean and simple code with documentation.
#![deny(missing_docs)]
#![deny(clippy::allow_attributes_without_reason)]
#![deny(clippy::arithmetic_side_effects)]
#![deny(clippy::as_ptr_cast_mut)]
#![deny(clippy::as_underscore)]
#![deny(clippy::assertions_on_result_states)]
#![deny(clippy::borrow_as_ptr)]
#![deny(clippy::branches_sharing_code)]
#![deny(clippy::case_sensitive_file_extension_comparisons)]
#![deny(clippy::cast_lossless)]
#![deny(clippy::cast_possible_truncation)]
#![deny(clippy::cast_possible_wrap)]
#![deny(clippy::cast_precision_loss)]
#![deny(clippy::cast_ptr_alignment)]
#![deny(clippy::cast_sign_loss)]
#![deny(clippy::checked_conversions)]
#![deny(clippy::clear_with_drain)]
#![deny(clippy::clone_on_ref_ptr)]
#![deny(clippy::cloned_instead_of_copied)]
#![deny(clippy::cognitive_complexity)]
#![deny(clippy::collection_is_never_read)]
#![deny(clippy::copy_iterator)]
#![deny(clippy::create_dir)]
#![deny(clippy::dbg_macro)]
#![deny(clippy::debug_assert_with_mut_call)]
#![deny(clippy::decimal_literal_representation)]
#![deny(clippy::default_trait_access)]
#![deny(clippy::default_union_representation)]
#![deny(clippy::derive_partial_eq_without_eq)]
#![deny(clippy::doc_link_with_quotes)]
#![deny(clippy::doc_markdown)]
#![deny(clippy::explicit_into_iter_loop)]
#![deny(clippy::explicit_iter_loop)]
#![deny(clippy::fallible_impl_from)]
#![deny(clippy::missing_safety_doc)]
#![deny(clippy::undocumented_unsafe_blocks)]

use std::{
    env,
    ffi::{CString, OsString},
    fs::File,
    io::Write,
    net::TcpListener,
    os::{
        fd::{AsRawFd, RawFd},
        unix::{ffi::OsStrExt, net::UnixStream, process::CommandExt},
    },
    path::Path,
    process::{ExitCode, Stdio},
    str::FromStr,
};

use getargs::{Opt, Options};
use libseccomp::{scmp_cmp, ScmpAction, ScmpFilterContext, ScmpSyscall};
use nix::{
    errno::Errno,
    libc::setdomainname,
    sched::{unshare, CloneFlags},
    sys::wait::{Id, WaitPidFlag},
    time::{clock_gettime, ClockId},
    unistd::{close, fork, getgid, getpid, getuid, sethostname, ForkResult},
    NixPath,
};
use rustc_hash::{FxHashMap, FxHashSet};
use sendfd::SendWithFd;
use syd::{
    bring_up_loopback, caps,
    compat::{waitid, WaitStatus},
    config::*,
    err::SydResult,
    error,
    fs::set_cloexec,
    hook::{ExportMode, Supervisor},
    info,
    log::JsonLinesLogger,
    nsflags_name,
    sandbox::Sandbox,
    unshare::{GidMap, UidMap},
};

// Set global allocator to mimalloc.
#[cfg(not(feature = "prof"))]
#[global_allocator]
static GLOBAL: mimalloc2_rust::GlobalMiMalloc = mimalloc2_rust::GlobalMiMalloc;

// Set global allocator to tcmalloc if profiling is enabled.
#[cfg(feature = "prof")]
#[global_allocator]
static GLOBAL: tcmalloc::TCMalloc = tcmalloc::TCMalloc;

// This line will include the content of "esyd.sh" at compile-time
const ESYD: &str = include_str!("esyd.sh");

#[allow(clippy::cognitive_complexity)]
fn main() -> SydResult<ExitCode> {
    // Set SIGPIPE handler to default.
    syd::set_sigpipe_dfl()?;

    let mut argv = argv::iter()
        .map(|os| os.to_str().ok_or(Errno::EINVAL))
        .collect::<Result<Vec<&str>, Errno>>()?
        .into_iter()
        .peekable();
    // SAFETY: Safe to assume argv has at least one element.
    // On the off chance that it's empty, we panic.
    #[allow(clippy::disallowed_methods)]
    let is_login = argv.next().expect("arg0").starts_with('-');
    let mut is_quick = env::var_os(ENV_QUICK_BOOT).is_some();

    // Handle quick options early before reexecution for convenience.
    if !is_login {
        if let Some(arg) = argv.peek() {
            match *arg {
                "-h" | "--help" => {
                    help();
                    return Ok(ExitCode::SUCCESS);
                }
                "-V" | "--version" => {
                    syd::syd_info();
                    return Ok(ExitCode::SUCCESS);
                }
                "--sh" => {
                    println!("{ESYD}");
                    return Ok(ExitCode::SUCCESS);
                }
                "-q" => is_quick = true,
                _ => {}
            }
        }
    }

    // Guard against CVE-2019-5736:
    // Copy /proc/self/exe in an anonymous fd (created via memfd_create), seal it and re-execute it.
    // See:
    // - https://github.com/opencontainers/runc/commit/0a8e4117e7f715d5fbeef398405813ce8e88558b
    // - https://github.com/lxc/lxc/commit/6400238d08cdf1ca20d49bafb85f4e224348bf9d
    // Note: syd's procfs protections is another layer of defense against this.
    if !is_quick {
        syd::seal::ensure_sealed()?;
    }

    // Configure mimalloc.
    syd::configure_mimalloc();

    // Ignore all signals except SIG{KILL,STOP,PIPE,CHLD,Core}.
    // This is used to ensure we can deny {rt_,}sigreturn(2) to mitigate SROP.
    syd::ignore_signals()?;

    // Initialize logging.
    JsonLinesLogger::init(None, "warn")?;

    // Parse CLI arguments
    let mut export: Option<ExportMode> = None;
    let mut sandbox: Sandbox = Sandbox::default();
    let mut cmd_arg0: Option<OsString> = None;
    let mut cmd_argv: Vec<OsString> = vec![];
    let mut cmd_envp: FxHashSet<OsString> = FxHashSet::default();

    // Initialize Options.
    let user_done = if is_login || argv.peek().is_none() {
        sandbox.parse_profile("user")?;
        true
    } else {
        false
    };
    let mut user_parse = false;
    let mut opts = Options::new(argv);

    // Local options handled by this function.
    let sh = env::var_os(ENV_SH).unwrap_or(OsString::from(SYD_SH));

    while let Some(opt) = opts.next_opt()? {
        match opt {
            /*
             * Basic options
             */
            Opt::Short('h') | Opt::Long("help") => {
                help();
                return Ok(ExitCode::SUCCESS);
            }
            Opt::Short('V') | Opt::Long("version") => {
                syd::syd_info();
                return Ok(ExitCode::SUCCESS);
            }
            Opt::Long("sh") => {
                println!("{ESYD}");
                return Ok(ExitCode::SUCCESS);
            }
            Opt::Short('q') => {} // ignore, must be first!

            /*
             * Sandbox options
             */
            Opt::Short('E') => {
                export = Some(ExportMode::from_str(opts.value()?)?);
                sandbox.parse_profile("lib")?;
                if export == Some(ExportMode::PseudoFiltercode) {
                    // This variable makes setup_seccomp_parent print rules.
                    env::set_var("SYD_SECX", "1");
                } else {
                    env::remove_var("SYD_SECX");
                }
            }
            Opt::Short('x') => {
                env::set_var(ENV_NO_SYSLOG, "1");
                sandbox.parse_profile("trace")?;
            }
            Opt::Short('m') => {
                let cmd = opts.value()?;
                if sandbox.locked() {
                    eprintln!("Failed to execute magic command `{cmd}': sandbox locked!");
                    return Err(Errno::EBUSY.into());
                } else {
                    sandbox.config(cmd)?;
                }
            }
            Opt::Short('f') => {
                // Login shell compatibility:
                // Parse user profile as necessary.
                user_parse = true;
            }
            Opt::Short('l') | Opt::Long("login") => {
                // Login shell compatibility:
                // Parse user profile as necessary.
                user_parse = true;
            }
            Opt::Short('c') => {
                // When multiple -c arguments are given,
                // only the first one is honoured and
                // the rest is ignored in consistency
                // with how bash and dash behaves.
                user_parse = true;
                if cmd_argv.is_empty() {
                    cmd_argv.push(sh.clone());
                    cmd_argv.push(OsString::from("-c"));
                    cmd_argv.push(opts.value().map(OsString::from)?);
                }
            }
            Opt::Short('P') => {
                let path = opts.value()?;
                if sandbox.locked() {
                    eprintln!("Failed to parse config file `{path}': sandbox locked!");
                    return Err(Errno::EBUSY.into());
                }
                sandbox.parse_config_file(path)?;
            }
            /* We keep --profile for syd-1 compatibility.
             * It's undocumented. */
            Opt::Short('p') | Opt::Long("profile") => {
                let profile = opts.value()?;
                if sandbox.locked() {
                    eprintln!("Failed to parse profile `{profile}': sandbox locked!");
                    return Err(Errno::EBUSY.into());
                }
                sandbox.parse_profile(profile)?;
            }

            /*
             * Unshare options
             */
            Opt::Short('a') => {
                let val = opts.value().map(OsString::from)?;
                cmd_arg0 = Some(val);
            }
            Opt::Short('e') => {
                let value = opts.value()?;
                match value.split_once('=') {
                    Some((var, val)) => {
                        cmd_envp.insert(OsString::from(var));
                        if !val.is_empty() {
                            // This way we give the user the chance to pass-through
                            // denylisted environment variables e.g.
                            //      syd -eLD_LIBRARY_PATH= cmd
                            // is equivalent to
                            //      syd -eLD_LIBRARY_PATH=$LD_LIBRARY_PATH cmd
                            env::set_var(var, val);
                        }
                    }
                    None => {
                        cmd_envp.remove(&OsString::from(value));
                        env::remove_var(value);
                    }
                }
            }

            // Profiling options.
            #[cfg(feature = "prof")]
            Opt::Long("prof") => match opts.value()? {
                "cpu" => env::set_var("SYD_PROF", "cpu"),
                "mem" => env::set_var("SYD_PROF", "mem"),
                val => {
                    eprintln!("Invalid profile mode `{val}'!");
                    eprintln!("Expected exactly one of `cpu' or `mem'!");
                    help();
                    return Ok(ExitCode::FAILURE);
                }
            },

            Opt::Short(opt) => {
                eprintln!("Unknown option: -{opt}!");
                help();
                return Ok(ExitCode::FAILURE);
            }
            Opt::Long(opt) => {
                eprintln!("Unknown option: --{opt}!");
                help();
                return Ok(ExitCode::FAILURE);
            }
        }
    }

    if user_parse && !user_done && !sandbox.locked() {
        sandbox.parse_profile("user")?;
    }

    // Prepare the command to execute, which may be a login shell.
    cmd_argv.extend(opts.positionals().map(OsString::from));
    let argv0 = if !cmd_argv.is_empty() {
        Some(cmd_argv.remove(0))
    } else {
        None
    };

    let argv0 = match (export.is_some(), argv0, is_login) {
        (true, _, _) => OsString::from("true"),
        (false, Some(argv0), false) => argv0,
        (false, None, false) | (false, _, true) => {
            #[allow(clippy::disallowed_methods)]
            if cmd_arg0.is_none() {
                // Allow user to override with -a.
                cmd_arg0 = Some(match Path::new(&sh).file_name() {
                    None => OsString::from("-"),
                    Some(name) => {
                        let mut p = OsString::from("-");
                        p.push(name);
                        p
                    }
                });
            }
            sh
        }
    };

    // Keep standard output open if we're exporting.
    if export.is_some() {
        env::set_var("SYD_KEEP_STDOUT", "1");
    }

    // time implies unshare/time:1
    if sandbox.time.is_some() {
        sandbox.set_unshare_time(true);
    }

    // root/map:1 implies unshare/user:1
    // We do this here rather than in sandbox.rs,
    // because the user may toggle this flag many times.
    let map_root = sandbox.map_root();
    if map_root {
        sandbox.set_unshare_user(true);
    }

    // Set unshare-mount if we've any bind mounts.
    if sandbox.has_bind_mounts() {
        sandbox.set_unshare_mount(true);
    }

    // root: implies unshare/mount:1 and unshare/pid:1.
    if sandbox.root.is_some() {
        sandbox.set_unshare_mount(true);
        sandbox.set_unshare_pid(true);
    }

    // SAFETY: We cannot support NEWPID without NEWNS.
    // ie, pid namespace must have its own private /proc.
    if sandbox.unshare_pid() {
        sandbox.set_unshare_mount(true);
    }

    let proxy = if sandbox.has_proxy() {
        // sandbox/proxy:on implies unshare/net:1
        sandbox.set_unshare_net(true);

        // Step 1: Create a PIDFd of this process.
        // SAFETY: nix has no wrapper for pidfd_open.
        let pidfd = match unsafe {
            nix::libc::syscall(
                nix::libc::SYS_pidfd_open,
                getpid().as_raw(),
                nix::libc::PIDFD_NONBLOCK,
            )
        } {
            e if e < 0 => return Err(Errno::last().into()),
            fd => fd as RawFd,
        };

        // Step 2: Create a UNIX socket pair.
        let (stream_parent, stream_child) = UnixStream::pair()?;

        // Step 3: Unset the CLOEXEC flags on the file descriptors.
        // PIDFds and Rust sockets are automatically CLOEXEC.
        set_cloexec(&pidfd, false)?;
        let fd = stream_parent.as_raw_fd(); // Borrows FD.
        set_cloexec(&fd, false)?;

        // Step 4: Prepare environment of the syd-tor process.
        // Filter the environment variables to only include PATH and LD_LIBRARY_PATH.
        let safe_env: FxHashMap<_, _> = env::vars_os()
            .filter(|(key, _)| matches!(key.as_bytes(), b"PATH" | b"LD_LIBRARY_PATH"))
            .collect();

        // Step 5: Spawn syd-tor process outside the namespace.
        // Pass one end of the socket-pair to it.
        let mut cmd = std::process::Command::new(
            env::var_os("CARGO_BIN_EXE_syd-tor").unwrap_or(OsString::from("syd-tor")),
        );
        if sandbox.allow_unsafe_debug() {
            cmd.arg("-d");
        }
        cmd.arg(format!("-p{pidfd}"));
        cmd.arg(format!("-i{fd}"));
        cmd.arg(format!(
            "-o{}:{}",
            sandbox.proxy_ext_addr, sandbox.proxy_ext_port
        ));
        //cmd.arg(format!("-b{}", PIPE_BUF));
        cmd.env_clear();
        cmd.envs(safe_env);
        cmd.current_dir("/");
        cmd.process_group(0);
        cmd.stdin(Stdio::null());
        cmd.stdout(Stdio::null());
        if !sandbox.allow_unsafe_debug() {
            cmd.stderr(Stdio::null());
        }
        cmd.spawn()?;
        close(pidfd)?;
        info!("ctx": "init", "op": "spawn_proxy",
            "addr": format!("{}!{}", sandbox.proxy_ext_addr, sandbox.proxy_ext_port));

        // Step 4: Pass the other end of the socket-pair
        // to the new namespace.
        Some((stream_child, sandbox.proxy_port))
    } else {
        None
    };

    // Set up Linux namespaces if requested. Note,
    // we set it up here before spawning the child so as to
    // include the syd process into the pid namespace as well
    // such that the sandbox process and syd have the identical
    // view of /proc.
    let namespaces = sandbox.namespaces();
    if namespaces == 0 {
        // No namespace arguments passed, run normally.
        return match Supervisor::run(sandbox, &argv0, cmd_argv, Some(&cmd_envp), cmd_arg0, export) {
            Ok(code) => Ok(ExitCode::from(code)),
            Err(error) => {
                let errno = Errno::last();
                eprintln!("{error:?}");
                Ok(ExitCode::from(u8::try_from(errno as i32).unwrap_or(127)))
            }
        };
    } else {
        info!("ctx": "init", "op": "unshare",
            "ns": namespaces,
            "namespaces": nsflags_name(namespaces));
    }

    let id_buf = if sandbox.unshare_user() {
        // create the UID and GID mappings.
        let uid = getuid().as_raw();
        let gid = getgid().as_raw();

        let uid_buf = {
            let uid_maps = vec![
                UidMap {
                    inside_uid: if map_root { 0 } else { uid },
                    outside_uid: uid,
                    count: 1,
                }, // Map the current user.
            ];
            let mut buf = Vec::new();
            for map in uid_maps {
                writeln!(
                    &mut buf,
                    "{} {} {}",
                    map.inside_uid, map.outside_uid, map.count
                )?;
            }
            buf
        };

        let gid_buf = {
            let gid_maps = vec![
                GidMap {
                    inside_gid: if map_root { 0 } else { gid },
                    outside_gid: gid,
                    count: 1,
                }, // Map the current group.
            ];
            let mut buf = Vec::new();
            for map in gid_maps {
                writeln!(
                    &mut buf,
                    "{} {} {}",
                    map.inside_gid, map.outside_gid, map.count
                )?;
            }
            buf
        };
        Some((uid_buf, gid_buf))
    } else {
        None
    };

    // Tell the kernel to keep the capabilities after the unshare call.
    // This is important because unshare() can change the user
    // namespace, which often leads to a loss of capabilities.
    caps::securebits::set_keepcaps(true)?;

    // CLONE_NEWTIME may only be used with unshare(2).
    // CloneFlags don't support CLONE_NEWTIME directly so we use retain.
    unshare(CloneFlags::from_bits_retain(namespaces))?;

    // Write uid/gid map for user namespace.
    if let Some((ref uid_buf, ref gid_buf)) = id_buf {
        // Write "deny" to /proc/self/setgroups before writing to gid_map.
        File::create("/proc/self/setgroups").and_then(|mut f| f.write_all(b"deny"))?;
        File::create("/proc/self/gid_map").and_then(|mut f| f.write_all(&gid_buf[..]))?;
        File::create("/proc/self/uid_map").and_then(|mut f| f.write_all(&uid_buf[..]))?;

        // Set inheritable mask and ambient caps to retain caps after execve(2).
        caps::securebits::set_keepcaps(true)?;
        let permitted_caps = caps::read(None, caps::CapSet::Permitted)?;
        caps::set(None, caps::CapSet::Inheritable, &permitted_caps)?;
        // Set the same capabilities as ambient, if necessary.
        for cap in permitted_caps {
            caps::raise(None, caps::CapSet::Ambient, cap)?;
        }
    }

    // Bring up loopback device for net namespace.
    if sandbox.unshare_net() {
        // Set up the loopback interface.
        // Ignore errors.
        match bring_up_loopback() {
            Ok(_) => info!("ctx": "init", "op": "start_loopback"),
            Err(errno) => {
                error!("ctx": "init", "op": "start_loopback", "errno": errno as i32)
            }
        }

        // Handle proxy sandboxing.
        if let Some((stream_child, proxy_port)) = proxy {
            let proxy_addr = sandbox.proxy_addr;
            let ipv = if proxy_addr.is_ipv6() { 6 } else { 4 };
            match TcpListener::bind((proxy_addr, proxy_port)) {
                Ok(listener) => {
                    info!("ctx": "init", "op": "bind_proxy", "ipv": ipv,
                        "addr": format!("{proxy_addr}!{proxy_port}"));
                    let buf = [0u8; 1];
                    let lfd = [listener.as_raw_fd()];
                    match stream_child.send_with_fd(&buf, &lfd) {
                        Ok(_) => {
                            info!("ctx": "init", "op": "send_proxy_fd",
                                "fd": listener.as_raw_fd());
                        }
                        Err(error) => {
                            error!("ctx": "init", "op": "send_proxy_fd",
                                "fd": listener.as_raw_fd(),
                                "error": error.to_string());
                        }
                    }
                }
                Err(error) => {
                    error!("ctx": "init", "op": "bind_proxy", "ipv": ipv,
                        "addr": format!("{proxy_addr}!{proxy_port}"),
                        "error": error.to_string());
                }
            }
            drop(stream_child);
        }
    }

    // Set host and domain name for uts namespace.
    // Ignore errors.
    if sandbox.unshare_uts() {
        // SAFETY: Domain name must not contain a nul-byte.
        #[allow(clippy::disallowed_methods)]
        let domainname = CString::new(sandbox.domainname.clone()).unwrap();

        // SAFETY: There's no setdomainname wrapper in nix.
        let _ = unsafe { setdomainname(domainname.as_ptr() as *const _, domainname.len()) };
        let _ = sethostname(&sandbox.hostname);
    }

    if sandbox.unshare_time() {
        if let Some(offset) = sandbox.time {
            let mut file = File::create("/proc/self/timens_offsets")?;
            let contents = format!("monotonic {offset} 0\nboottime {offset} 0\n");
            file.write_all(contents.as_bytes())?;
        } else if let Ok(boottime) = clock_gettime(ClockId::CLOCK_BOOTTIME) {
            // Set uptime to 0 for time namespace. Ignore errors.
            if let Ok(mut file) = File::create("/proc/self/timens_offsets") {
                #[allow(clippy::arithmetic_side_effects)]
                let contents = format!("boottime {} 0\n", -boottime.tv_sec());
                let _ = file.write_all(contents.as_bytes());
            }
        }
    }

    // SAFETY: fork is our safest option here.
    let child = match unsafe { fork() }? {
        ForkResult::Child => {
            let retval =
                match Supervisor::run(sandbox, &argv0, cmd_argv, Some(&cmd_envp), cmd_arg0, export)
                {
                    Ok(retval) => nix::libc::c_int::from(retval),
                    Err(error) => {
                        let errno = Errno::last();
                        eprintln!("{error:?}");
                        errno as i32
                    }
                };
            // SAFETY: _exit is safe to call in child.
            unsafe { nix::libc::_exit(retval) };
        }
        ForkResult::Parent { child, .. } => {
            // Ensure we release all unneeded resources.
            drop(cmd_arg0);
            drop(cmd_argv);
            drop(cmd_envp);
            drop(sandbox);
            // SAFETY: nix does not have a close_range wrapper yet.
            let _ = unsafe {
                nix::libc::syscall(
                    nix::libc::SYS_close_range,
                    nix::libc::STDERR_FILENO + 1,
                    nix::libc::c_int::MAX,
                    0,
                )
            };
            child
        }
    };

    // Only parent ends up here and `child` is child pid.
    // SAFETY: Set up a Landlock sandbox to disallow all access.
    let p = vec![];
    let _ = syd::landlock_operation(&p, &p, LANDLOCK_ABI);

    // SAFETY: Set up a seccomp filter which only allows
    // 1. write to standard error.
    // 2. waitid and exit.
    // 3. memory allocation syscalls
    // 4. signal handling syscalls
    let mut filter = ScmpFilterContext::new_filter(ScmpAction::KillProcess)?;
    let allow_call = [
        "exit",
        "exit_group",
        "waitid",
        "brk",
        "madvise",
        "mremap",
        "munmap",
        "sigaction",
        "sigaltstack",
        "sigpending",
        "sigprocmask",
        "sigsuspend",
        "sigreturn",
        "rt_sigaction",
        "rt_sigpending",
        "rt_sigprocmask",
        "rt_sigqueueinfo",
        "rt_sigreturn",
        "rt_sigtimedwait",
        "rt_sigtimedwait_time64",
    ];
    for name in allow_call {
        if let Ok(syscall) = ScmpSyscall::from_name(name) {
            filter.add_rule(ScmpAction::Allow, syscall)?;
        }
    }

    // Allow write to standard error.
    for name in ["write", "writev"] {
        if let Ok(syscall) = ScmpSyscall::from_name(name) {
            filter.add_rule_conditional(
                ScmpAction::Allow,
                syscall,
                &[scmp_cmp!($arg0 == nix::libc::STDERR_FILENO as u64)],
            )?;
        }
    }

    // Prevent executable memory.
    const PROT_EXEC: u64 = nix::libc::PROT_EXEC as u64;
    for name in ["mmap", "mmap2", "mprotect"] {
        #[allow(clippy::disallowed_methods)]
        filter.add_rule_conditional(
            ScmpAction::Allow,
            ScmpSyscall::from_name(name).unwrap(),
            &[scmp_cmp!($arg2 & PROT_EXEC == 0)],
        )?;
    }

    filter.load()?;

    loop {
        #[allow(clippy::cast_possible_truncation)]
        #[allow(clippy::cast_sign_loss)]
        break match waitid(Id::Pid(child), WaitPidFlag::WEXITED) {
            Ok(WaitStatus::Exited(_, code)) =>
            {
                #[allow(clippy::cast_possible_truncation)]
                #[allow(clippy::cast_sign_loss)]
                Ok(ExitCode::from(code as u8))
            }
            Ok(WaitStatus::Signaled(_, signal, _)) => {
                Ok(ExitCode::from(128_u8.saturating_add(signal as u8)))
            }
            Ok(WaitStatus::StillAlive) | Err(Errno::EINTR) => continue,
            Ok(_status) => Err(Errno::EINVAL.into()),
            Err(errno) => Err(errno.into()),
        };
    }
}

fn help() {
    println!("syd [-acefhlmpqxEPV] [--] {{command [arg...]}}");
}
