//
// Syd: rock-solid application kernel
// src/syd-sys.rs: Given a number, print the matching syscall name and exit.
//                 Given a regex, print case-insensitively matching syscall names and exit.
//
// Copyright (c) 2024, 2025 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

use std::{os::unix::ffi::OsStrExt, process::ExitCode, str::FromStr, time::Duration};

use nix::{
    errno::Errno,
    sys::{
        signal::{kill, Signal},
        stat::lstat,
        wait::{waitpid, WaitPidFlag, WaitStatus},
    },
    unistd::{fork, ForkResult},
};
use syd::{
    err::SydResult,
    libseccomp::{ScmpArch, ScmpSyscall},
    wildmatch::{is_literal, wildmatch},
};

fn main() -> SydResult<ExitCode> {
    use lexopt::prelude::*;

    syd::set_sigpipe_dfl()?;

    // Parse CLI options.
    let mut opt_arch = ScmpArch::Native; // -a
    let mut opt_ghost = false; // -g
    let mut opt_probe = false; // -p
    let mut opt_tmout = Duration::from_secs(3); // -t
    let mut opt_sys = None;
    let mut opt_arg = Vec::new();

    let mut parser = lexopt::Parser::from_env();
    while let Some(arg) = parser.next()? {
        match arg {
            Short('h') => {
                help();
                return Ok(ExitCode::SUCCESS);
            }
            Short('p') => opt_probe = true,
            Short('g') => {
                // -g doesn't make sense without -p.
                opt_ghost = true;
                opt_probe = true;
            }
            Short('t') => {
                opt_tmout = parser
                    .value()?
                    .parse::<f64>()
                    .map(Duration::from_secs_f64)?
            }
            Short('a') => {
                let value = parser.value()?.parse::<String>()?;
                if matches!(value.to_ascii_lowercase().as_str(), "help" | "list") {
                    syd::print_seccomp_architectures();
                    return Ok(ExitCode::SUCCESS);
                }
                opt_arch = match ScmpArch::from_str(&format!(
                    "SCMP_ARCH_{}",
                    value.to_ascii_uppercase()
                )) {
                    Ok(opt_arch) => opt_arch,
                    Err(_) => {
                        eprintln!("Invalid architecture `{value}', use `-a list' for a list.");
                        return Ok(ExitCode::FAILURE);
                    }
                };
            }
            Value(sys) if opt_sys.is_none() => opt_sys = Some(sys),
            Value(arg) => {
                opt_arg.push(arg);
                opt_arg.extend(parser.raw_args()?);
            }
            _ => return Err(arg.unexpected().into()),
        }
    }

    let sysarg = if let Some(value) = opt_sys {
        value
    } else {
        eprintln!("Expected syscall number or name regex as first argument!");
        return Ok(ExitCode::FAILURE);
    };

    let syscalls = match sysarg.parse::<i32>() {
        Ok(num) => {
            let syscall = ScmpSyscall::from(num);
            if !opt_probe {
                if let Ok(name) = syscall.get_name_by_arch(opt_arch) {
                    println!("{num}\t{name}");
                    return Ok(ExitCode::SUCCESS);
                } else {
                    return Ok(ExitCode::FAILURE);
                }
            }
            vec![syscall]
        }
        Err(_) => {
            let glob = if !is_literal(sysarg.as_bytes()) {
                sysarg.to_str().ok_or(Errno::EINVAL)?.to_string()
            } else {
                format!("*{}*", sysarg.to_str().ok_or(Errno::EINVAL)?)
            };
            let mut ok = false;
            let mut syscalls = vec![];
            for (call, name) in (0..1024)
                .map(|n| {
                    let call = ScmpSyscall::from(n);
                    (call, call.get_name_by_arch(opt_arch).unwrap_or_default())
                })
                .filter(|(_, name)| !name.is_empty())
            {
                if wildmatch(glob.as_bytes(), name.as_bytes()) {
                    if opt_probe {
                        syscalls.push(call);
                    } else {
                        let num = i32::from(call);
                        println!("{num}\t{name}");
                        ok = true;
                    }
                }
            }
            if !opt_probe {
                return Ok(if ok {
                    ExitCode::SUCCESS
                } else {
                    ExitCode::FAILURE
                });
            }
            syscalls
        }
    };

    // Probe
    if opt_ghost {
        if let Err(errno) = enable_ghost_mode() {
            eprintln!("syd-sys: Failed to enable Syd's Ghost mode: {errno}");
            if errno == Errno::ENOENT {
                eprintln!("sys-sys: Ensure you're running under Syd, and the sandbox lock is off.");
            }
            return Ok(ExitCode::FAILURE);
        }
    }

    // Prepare system call arguments.
    let mut args: [Option<libc::c_long>; 6] = [None; 6];
    #[allow(clippy::needless_range_loop)]
    for argc in 0..6 {
        if let Some(value) = opt_arg.get(argc) {
            args[argc] = match value.parse::<libc::c_long>() {
                Ok(value) => Some(value),
                Err(error) => {
                    eprintln!("Argument {argc} is invalid: {error}");
                    return Ok(ExitCode::FAILURE);
                }
            };
        } else {
            break;
        }
    }

    for syscall in syscalls {
        println!("{}", probe_syscall(syscall, &args, opt_tmout));
    }

    Ok(ExitCode::SUCCESS)
}

fn help() {
    println!("Usage: syd-sys [-hgpt] [-a list|native|x86|x86_64|aarch64...] number|name-glob [<probe-args>...]");
    println!("Given a number, print the matching syscall name and exit.");
    println!("Given a glob, print case-insensitively matching syscall names and exit.");
    println!("Given -p, probe the system call and print result.");
    println!("Given -g with -p, enable Syd's Ghost mode prior to probing.");
    println!("Specify syscall probe timeout in seconds, defaults to 3 seconds.");
}

fn probe_syscall(
    syscall: ScmpSyscall,
    args: &[Option<libc::c_long>; 6],
    timeout: Duration,
) -> String {
    let snum = i32::from(syscall);
    let name = syscall.get_name().unwrap_or(snum.to_string());
    let argc = args
        .iter()
        .enumerate()
        .rev()
        .find(|&(_, elem)| elem.is_some())
        .map_or(0, |(idx, _)| idx + 1);
    #[allow(clippy::disallowed_methods)]
    match unsafe { fork() }.expect("fork") {
        ForkResult::Child => unsafe {
            match argc {
                0 => libc::syscall(snum.into()),
                1 => libc::syscall(snum.into(), args[0].unwrap()),
                2 => libc::syscall(snum.into(), args[0].unwrap(), args[1].unwrap()),
                3 => libc::syscall(
                    snum.into(),
                    args[0].unwrap(),
                    args[1].unwrap(),
                    args[2].unwrap(),
                ),
                4 => libc::syscall(
                    snum.into(),
                    args[0].unwrap(),
                    args[1].unwrap(),
                    args[2].unwrap(),
                    args[3].unwrap(),
                ),
                5 => libc::syscall(
                    snum.into(),
                    args[0].unwrap(),
                    args[1].unwrap(),
                    args[2].unwrap(),
                    args[3].unwrap(),
                    args[4].unwrap(),
                ),
                6 => libc::syscall(
                    snum.into(),
                    args[0].unwrap(),
                    args[1].unwrap(),
                    args[2].unwrap(),
                    args[3].unwrap(),
                    args[4].unwrap(),
                    args[5].unwrap(),
                ),
                _ => unreachable!(),
            };
            libc::_exit(Errno::last() as i32);
        },
        ForkResult::Parent { child, .. } => {
            let start = std::time::Instant::now();

            let result = loop {
                match waitpid(child, Some(WaitPidFlag::WNOHANG)) {
                    Ok(WaitStatus::Exited(_, code)) => {
                        if code == 0 {
                            break "0".to_string();
                        } else {
                            break errstr(code).to_string();
                        }
                    }
                    Ok(WaitStatus::Signaled(_, sig, core)) => {
                        if core {
                            break format!("{sig}!");
                        } else {
                            break format!("{sig}");
                        }
                    }
                    Ok(WaitStatus::StillAlive) => {
                        if start.elapsed() >= timeout {
                            let _ = kill(child, Signal::SIGKILL);
                            break "TMOUT".to_string();
                        }
                    }
                    Err(Errno::ECHILD) => break "ECHILD".to_string(),
                    _ => {}
                }
            };

            match argc {
                0 => format!("{name}()={result}"),
                1 => format!("{name}(0x{:x})={result}", args[0].unwrap()),
                2 => format!(
                    "{name}(0x{:x}, 0x{:x})={result}",
                    args[0].unwrap(),
                    args[1].unwrap()
                ),
                3 => format!(
                    "{name}(0x{:x}, 0x{:x}, 0x{:x})={result}",
                    args[0].unwrap(),
                    args[1].unwrap(),
                    args[2].unwrap()
                ),
                4 => format!(
                    "{name}(0x{:x}, 0x{:x}, 0x{:x}, 0x{:x})={result}",
                    args[0].unwrap(),
                    args[1].unwrap(),
                    args[2].unwrap(),
                    args[3].unwrap()
                ),
                5 => format!(
                    "{name}(0x{:x}, 0x{:x}, 0x{:x}, 0x{:x}, 0x{:x})={result}",
                    args[0].unwrap(),
                    args[1].unwrap(),
                    args[2].unwrap(),
                    args[3].unwrap(),
                    args[4].unwrap()
                ),
                6 => format!(
                    "{name}(0x{:x}, 0x{:x}, 0x{:x}, 0x{:x}, 0x{:x}, 0x{:x})={result}",
                    args[0].unwrap(),
                    args[1].unwrap(),
                    args[2].unwrap(),
                    args[3].unwrap(),
                    args[4].unwrap(),
                    args[5].unwrap()
                ),
                _ => unreachable!(),
            }
        }
    }
}

fn enable_ghost_mode() -> Result<(), Errno> {
    match lstat("/dev/syd/ghost") {
        Err(Errno::EOWNERDEAD) => Ok(()),
        Err(errno) => Err(errno),
        Ok(_) => Err(Errno::EOWNERDEAD),
    }
}

fn errstr(errno: i32) -> String {
    if let Some((name, _)) = Errno::from_raw(errno).to_string().split_once(':') {
        name.to_string()
    } else {
        errno.to_string()
    }
}
