//
// Syd: rock-solid application kernel
// src/utils/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 libc::c_int;
use libseccomp::{ScmpArch, ScmpSyscall};
#[expect(clippy::disallowed_types)]
use nix::unistd::ForkResult;
use nix::{
    errno::Errno,
    fcntl::OFlag,
    sys::{
        signal::{kill, Signal},
        stat::lstat,
        wait::{waitpid, WaitPidFlag, WaitStatus},
    },
    unistd::{fork, Pid},
};
use syd::{
    config::proc_init_simple,
    confine::print_seccomp_architectures,
    fs::unix_inodes,
    ioctl::{Ioctl, IoctlMap},
    parsers::sandbox::str2u64,
    proc::proc_unix_inodes,
    wildmatch::inamematch,
};

// 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.
    let mut opt_arch = ScmpArch::native(); // -a
    let mut opt_errno = false; // -e
    let mut opt_ioctl = false; // -i
    let mut opt_ghost = false; // -g
    let mut opt_probe = false; // -p
    let mut opt_open = false; // -o
    let mut opt_signal = false; // -s
    let mut opt_unix_nl = false; // -u: netlink(7)
    let mut opt_unix_pn = false; // -U: proc_net(5)
    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('e') => opt_errno = true,
            Short('i') => opt_ioctl = true,
            Short('p') => opt_probe = true,
            Short('o') => opt_open = true,
            Short('s') => opt_signal = true,
            Short('u') => opt_unix_nl = true,
            Short('U') => opt_unix_pn = 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") {
                    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 flags = [opt_errno, opt_ioctl, opt_open, opt_signal, opt_unix_nl, opt_unix_pn];
    if flags.iter().filter(|&&flag| flag).count() > 1 {
        eprintln!("sys-sys: At most one of -e, -i, -o, -s, -u, and -U must be given!");
        return Err(Errno::EINVAL.into());
    }

    if opt_unix_nl {
        if opt_sys.is_some() {
            eprintln!("syd-sys: -u does not accept a parameter!");
            return Err(Errno::EINVAL.into());
        }

        for inode in unix_inodes()? {
            println!("{inode}");
        }

        return Ok(ExitCode::SUCCESS);
    } else if opt_unix_pn {
        if opt_sys.is_some() {
            eprintln!("syd-sys: -U does not accept a parameter!");
            return Err(Errno::EINVAL.into());
        }

        proc_init_simple()?;
        for inode in proc_unix_inodes(Pid::this())? {
            println!("{inode}");
        }

        return Ok(ExitCode::SUCCESS);
    }

    let sysarg = if let Some(value) = opt_sys {
        value
    } else {
        let what = if opt_errno {
            "errno"
        } else if opt_ioctl {
            "ioctl"
        } else if opt_open {
            "open"
        } else if opt_signal {
            "signal"
        } else {
            "syscall"
        };
        eprintln!("syd-sys: Expected {what} number or name regex as first argument!");
        return Ok(ExitCode::FAILURE);
    };

    if opt_errno {
        return match sysarg.parse::<u16>() {
            Ok(0) => Ok(ExitCode::FAILURE),
            Ok(num) => {
                // number -> name
                let errno = Errno::from_raw(i32::from(num));
                if errno == Errno::UnknownErrno {
                    return Ok(ExitCode::FAILURE);
                }
                let estr = errno.to_string();
                let mut iter = estr.split(": ");
                let name = iter.next().unwrap_or("?");
                let desc = iter.next().unwrap_or("?");
                println!("{num}\t{name}\t{desc}");
                Ok(ExitCode::SUCCESS)
            }
            Err(_) => {
                // glob -> [number]
                let glob = sysarg.to_str().ok_or(Errno::EINVAL)?;

                let mut ok = false;
                for errno in (1..=4096).map(Errno::from_raw) {
                    if errno == Errno::UnknownErrno {
                        continue;
                    }
                    let estr = errno.to_string();
                    let mut iter = estr.split(": ");
                    let name = iter.next().unwrap_or("?");
                    let desc = iter.next().unwrap_or("?");
                    if inamematch(glob, &estr) {
                        println!("{}\t{}\t{}", errno as i32, name, desc);
                        ok = true;
                    }
                }

                Ok(if ok {
                    ExitCode::SUCCESS
                } else {
                    ExitCode::FAILURE
                })
            }
        };
    } else if opt_ioctl {
        let ioctl = IoctlMap::new(Some(opt_arch), false);
        return match str2u64(sysarg.as_bytes()).map(|arg| arg as Ioctl) {
            Ok(num) => {
                if let Ok(Some(names)) = ioctl.get_names(num, opt_arch) {
                    for name in names {
                        println!("{name}\t{num}");
                    }
                    Ok(ExitCode::SUCCESS)
                } else {
                    Ok(ExitCode::FAILURE)
                }
            }
            Err(_) => {
                let glob = sysarg.to_str().ok_or(Errno::EINVAL)?;
                let iter = ioctl.iter(opt_arch).ok_or(Errno::EINVAL)?;

                let mut ok = false;
                for (name, num) in iter {
                    if inamematch(glob, name) {
                        println!("{name}\t{num}");
                        ok = true;
                    }
                }

                Ok(if ok {
                    ExitCode::SUCCESS
                } else {
                    ExitCode::FAILURE
                })
            }
        };
    } else if opt_open {
        return match sysarg
            .parse::<c_int>()
            .ok()
            .and_then(OFlag::from_bits)
        {
            Some(OFlag::O_RDONLY) => {
                println!("O_RDONLY\t0");
                Ok(ExitCode::SUCCESS)
            }
            Some(flags) => {
                for flag in flags {
                    let name = oflag_name(flag);
                    println!("{name}\t{}", flag.bits());
                }
                Ok(ExitCode::SUCCESS)
            }
            None => {
                let mut glob = sysarg
                    .to_str()
                    .ok_or(Errno::EINVAL)?
                    .to_ascii_uppercase();
                if !glob.starts_with("O_") {
                    glob.insert_str(0, "O_");
                }

                let mut ok = false;
                for flag in OFlag::all() {
                    if flag == OFlag::O_ACCMODE {
                        for flag in [OFlag::O_RDONLY, OFlag::O_RDWR, OFlag::O_WRONLY] {
                            let name = oflag_name(flag);
                            if inamematch(&glob, &name) {
                                println!("{name}\t{}", flag.bits());
                                ok = true;
                            }
                        }
                        continue;
                    }

                    let name = oflag_name(flag);
                    if inamematch(&glob, &name) {
                        println!("{name}\t{}", flag.bits());
                        ok = true;
                    }
                }

                Ok(if ok {
                    ExitCode::SUCCESS
                } else {
                    ExitCode::FAILURE
                })
            }
        };
    } else if opt_signal {
        return match sysarg
            .parse::<i32>()
            .ok()
            .and_then(|num| Signal::try_from(num).ok())
        {
            Some(sig) => {
                println!("{sig}\t{}", sig as i32);
                Ok(ExitCode::SUCCESS)
            }
            None => {
                let mut glob = sysarg
                    .to_str()
                    .ok_or(Errno::EINVAL)?
                    .to_ascii_uppercase();
                if !glob.starts_with("SIG") {
                    glob.insert_str(0, "SIG");
                }

                let mut ok = false;
                for sig in Signal::iterator() {
                    if inamematch(&glob, sig.as_str()) {
                        println!("{sig}\t{}", sig as i32);
                        ok = true;
                    }
                }

                Ok(if ok {
                    ExitCode::SUCCESS
                } else {
                    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!("{name}\t{num}");
                    return Ok(ExitCode::SUCCESS);
                } else {
                    return Ok(ExitCode::FAILURE);
                }
            }
            vec![syscall]
        }
        Err(_) => {
            let glob = 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 inamematch(glob, &name) {
                    if opt_probe {
                        syscalls.push(call);
                    } else {
                        let num = i32::from(call);
                        println!("{name}\t{num}");
                        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];
    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!("syd-sys: 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 [-hgeiopstuU] [-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 -e, query errnos rather than syscalls.");
    println!("Given -i, query ioctls rather than syscalls.");
    println!("Given -o, query open flags rather than syscalls.");
    println!("Given -s, query signals rather than syscalls.");
    println!("Given -p, probe the system call and print result.");
    println!("Given -g with -p, enable Syd's Ghost mode prior to probing.");
    println!("Use -t to specify syscall probe timeout in seconds, defaults to 3 seconds.");
    println!("Use -u to list UNIX domain socket inodes using netlink(7)");
    println!("Use -U to list UNIX domain socket inodes using proc_net(5)");
}

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);
    #[expect(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()
    }
}

fn oflag_name(flag: OFlag) -> String {
    if flag == OFlag::O_RDONLY {
        return "O_RDONLY".to_string();
    }
    let s = format!("{flag:?}");
    let s = s
        .strip_prefix("OFlag(")
        .and_then(|i| i.strip_suffix(")"))
        .unwrap_or(&s);
    s.replace(' ', "")
}
