//
// Syd: rock-solid application kernel
// src/utils/syd-run.rs: Run a program inside a container (requires Linux-5.8 or newer).
//
// Copyright (c) 2024, 2025 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

use std::{
    os::unix::ffi::OsStrExt,
    process::{Command, ExitCode},
};

use nix::{
    errno::Errno,
    libc::pid_t,
    sched::{setns, CloneFlags},
    unistd::Pid,
};
use syd::{
    config::SYD_SH,
    confine::{run_cmd, CLONE_NEWTIME},
    err::SydResult,
    fd::pidfd_open,
    path::{XPath, XPathBuf},
    proc::proc_namespaces,
};

// Set global allocator to GrapheneOS allocator.
#[cfg(all(
    not(coverage),
    not(feature = "prof"),
    not(target_os = "android"),
    target_page_size_4k,
    target_pointer_width = "64"
))]
#[global_allocator]
static GLOBAL: hardened_malloc::HardenedMalloc = hardened_malloc::HardenedMalloc;

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

syd::main! {
    use lexopt::prelude::*;

    syd::set_sigpipe_dfl()?;

    // Parse CLI options.
    //
    // Note, option parsing is POSIXly correct:
    // POSIX recommends that no more options are parsed after the first
    // positional argument. The other arguments are then all treated as
    // positional arguments.
    // See: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html#tag_12_02
    //
    // Empty clone flags is the default and imply all except pid and time.
    let mut opt_cfl = CloneFlags::empty();
    let mut opt_pid = None;
    let mut opt_cmd = vec![];
    let mut opt_log = false;

    let mut parser = lexopt::Parser::from_env();
    while let Some(arg) = parser.next()? {
        match arg {
            Short('h') => {
                help();
                return Ok(ExitCode::SUCCESS);
            }
            Short('a') => opt_cfl = CloneFlags::empty(),
            Short('c') => opt_cfl |= CloneFlags::CLONE_NEWCGROUP,
            Short('i') => opt_cfl |= CloneFlags::CLONE_NEWIPC,
            Short('m') => opt_cfl |= CloneFlags::CLONE_NEWNS,
            Short('n') => opt_cfl |= CloneFlags::CLONE_NEWNET,
            Short('p') => opt_cfl |= CloneFlags::CLONE_NEWPID,
            Short('t') => opt_cfl |= CLONE_NEWTIME,
            Short('u') => opt_cfl |= CloneFlags::CLONE_NEWUTS,
            Short('U') => opt_cfl |= CloneFlags::CLONE_NEWUSER,
            Short('v') => opt_log = true,
            Value(pid) => {
                opt_pid = Some(pid);
                opt_cmd.extend(parser.raw_args()?);
            }
            _ => return Err(arg.unexpected().into()),
        }
    }

    let pid = if let Some(pid) = opt_pid {
        let pid = pid.parse::<pid_t>()?;
        if pid <= 0 {
            return Err(Errno::EINVAL.into());
        }
        pid
    } else {
        help();
        return Ok(ExitCode::FAILURE);
    };

    let namespaces = if opt_cfl.is_empty() {
        match nsget(pid, opt_log) {
            Ok(namespaces) => namespaces,
            Err(errno) => {
                eprintln!("syd-run: nsget: {errno}!");
                return Ok(ExitCode::FAILURE);
            }
        }
    } else {
        opt_cfl
    };

    if !namespaces.is_empty() {
        if let Err(errno) = nsenter(pid, namespaces) {
            eprintln!("syd-run: nsenter: {errno}!");
            return Ok(ExitCode::FAILURE);
        }
    }

    // Execute command, /bin/sh by default.
    if opt_cmd.is_empty() {
        opt_cmd = vec![SYD_SH.into()];
    }
    let cmd = XPathBuf::from(opt_cmd.remove(0));

    if opt_log {
        eprintln!("syd-run: exec command `{cmd}'...",);
    }

    let mut cmd = Command::new(cmd);
    let cmd = cmd.args(opt_cmd);
    Ok(ExitCode::from(run_cmd(cmd)))
}

fn help() {
    println!("Usage: syd-run [-hvacimnptuU] pid [<program> [<argument>...]]");
    println!("Run a program inside a container (requires Linux-5.8 or newer).");
}

fn nsenter(pid: pid_t, namespaces: CloneFlags) -> Result<(), Errno> {
    setns(pidfd_open(Pid::from_raw(pid), 0)?, namespaces)
}

fn nsget(pid: pid_t, log: bool) -> SydResult<CloneFlags> {
    let current_pid = Pid::this();
    let current_namespaces = proc_namespaces(current_pid)?;

    let target_pid = Pid::from_raw(pid);
    let target_namespaces = proc_namespaces(target_pid)?.0;

    let mut flags = CloneFlags::empty();

    for (name, target_ns) in target_namespaces {
        if let Some(current_ns) = current_namespaces.0.get(&name) {
            if target_ns.identifier != current_ns.identifier {
                let name = name.as_bytes();
                flags |= match name {
                    b"cgroup" => CloneFlags::CLONE_NEWCGROUP,
                    b"ipc" => CloneFlags::CLONE_NEWIPC,
                    b"mnt" => CloneFlags::CLONE_NEWNS,
                    b"net" => CloneFlags::CLONE_NEWNET,
                    b"user" => CloneFlags::CLONE_NEWUSER,
                    b"uts" => CloneFlags::CLONE_NEWUTS,
                    // Entering pid or time is privileged, so we only enter
                    // them in case user explicitly specified them.
                    b"pid_for_children" => continue, // CloneFlags::CLONE_NEWPID,
                    b"time_for_children" => continue, // CLONE_NEWTIME,
                    _ => {
                        if log {
                            eprintln!(
                                "syd-run: skip unsupported {} namespace switch from id:{} to id:{}!",
                                XPath::from_bytes(name),
                                current_ns.identifier,
                                target_ns.identifier
                            );
                        }
                        continue;
                    }
                };
                if log {
                    eprintln!(
                        "syd-run: switch {} namespace from id:{} to id:{}...",
                        XPath::from_bytes(name),
                        current_ns.identifier,
                        target_ns.identifier
                    );
                }
            }
        }
    }

    Ok(flags)
}
