//
// Syd: rock-solid application kernel
// src/utils/syd-x.rs: Check executability of the given files
//
// Copyright (c) 2025, 2026 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

use std::process::ExitCode;

use libc::pid_t;
use nix::{
    fcntl::{open, OFlag},
    sys::stat::Mode,
    unistd::Pid,
};
use serde_json::json;
use syd::{
    config::HAVE_AT_EXECVE_CHECK, fd::check_executable, path::XPathBuf, proc::proc_executables,
};

// Set global allocator to GrapheneOS allocator.
#[cfg(all(
    not(coverage),
    not(feature = "prof"),
    not(target_os = "android"),
    not(target_arch = "riscv64"),
    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_check = false; // -c
    let mut opt_print = false; // -v
    let mut opt_procs = vec![]; // -l pid...
    let mut opt_files = vec![];

    let mut parser = lexopt::Parser::from_env();
    while let Some(arg) = parser.next()? {
        match arg {
            Short('h') => {
                help();
                return Ok(ExitCode::SUCCESS);
            }
            Short('c') => opt_check = true,
            Short('v') => opt_print = true,
            Short('l') => {
                for pid in parser.raw_args()? {
                    opt_procs.push(pid.parse::<pid_t>().map(Pid::from_raw)?);
                }
            }
            Value(file) => {
                opt_files.push(file);
                opt_files.extend(parser.raw_args()?);
            }
            _ => return Err(arg.unexpected().into()),
        }
    }

    if opt_check {
        if !opt_files.is_empty() || !opt_procs.is_empty() {
            eprintln!("syd-x: -c accepts no arguments!");
            help();
            return Ok(ExitCode::FAILURE);
        }

        let ok = *HAVE_AT_EXECVE_CHECK;
        println!(
            "Current system {} AT_EXECVE_CHECK support.",
            if ok { "has" } else { "does not have" }
        );

        return Ok(if ok {
            ExitCode::SUCCESS
        } else {
            ExitCode::FAILURE
        });
    }

    if !opt_procs.is_empty() && !opt_files.is_empty() {
        eprintln!("Check and list mode are mutually exclusive!");
        help();
        return Ok(ExitCode::FAILURE);
    }

    if !opt_procs.is_empty() {
        // List mode.

        // Open FD to /proc.
        let _ = syd::log::log_init_simple(syd::syslog::LogLevel::Warn);
        syd::config::proc_init_simple()?;

        for pid in opt_procs {
            let bins = match proc_executables(pid) {
                Ok(bins) => bins,
                Err(errno) => {
                    #[expect(clippy::disallowed_methods)]
                    let err = json!({
                        "pid": pid.as_raw(),
                        "err": errno as i32,
                    });
                    #[expect(clippy::disallowed_methods)]
                    let err = serde_json::to_string(&err).expect("JSON");
                    println!("{err}");
                    continue;
                }
            };

            for bin in &bins {
                #[expect(clippy::disallowed_methods)]
                let msg = json!({
                    "pid": pid.as_raw(),
                    "dev": (bin.dev_major, bin.dev_minor),
                    "ino": bin.inode,
                    "exe": bin.path,
                });
                #[expect(clippy::disallowed_methods)]
                let msg = serde_json::to_string(&msg).expect("JSON");
                println!("{msg}");
            }
        }

        return Ok(ExitCode::SUCCESS);
    }

    if opt_files.is_empty() {
        help();
        return Ok(ExitCode::FAILURE);
    }

    // Check mode.
    for path in opt_files {
        let path = XPathBuf::from(path);
        #[expect(clippy::disallowed_methods)]
        let fd = match open(&path, OFlag::O_PATH | OFlag::O_CLOEXEC, Mode::empty()) {
            Ok(fd) => fd,
            Err(errno) => {
                if opt_print {
                    eprintln!("syd-x: Error opening file `{path}': {errno}!");
                }
                return Ok(ExitCode::from(errno as u8));
            }
        };

        if let Err(errno) = check_executable(fd) {
            if opt_print {
                eprintln!("syd-x: File `{path}' is not executable: {errno}!");
            }
            return Ok(ExitCode::from(errno as u8));
        }

        if opt_print {
            eprintln!("syd-x: File `{path}' is executable.");
        }
    }

    Ok(ExitCode::SUCCESS)
}

fn help() {
    println!("Usage: syd-x [-hcv] [-l pid...] {{files...}}");
    println!("Given filenames, check executability of files.");
    println!("Given process IDs with -l, list executable files of the processes.");
    println!("Exit with 0 on success or with errno on failure.");
    println!("Use execveat(2) with AT_EXECVE_CHECK on Linux>=6.14.");
    println!("Fallback to faccessat(2) with X_OK on older Linux.");
    println!("Use -c to check for AT_EXECVE_CHECK support.");
    println!("Use -v to print status information on standard error.");
}
