//
// Syd: rock-solid application kernel
// src/utils/syd-sec.rs: Print secure bits or run command with secure bits set
//
// Copyright (c) 2025 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

use std::{
    io::{stdout, Write},
    os::unix::process::CommandExt,
    process::{Command, ExitCode},
};

use nix::errno::Errno;
use serde_json::json;
use syd::{
    caps::securebits::{get_securebits, set_securebits, SecureBits},
    compat::{get_no_new_privs, set_no_new_privs},
};

// 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
    let mut opt_nnp = false;
    let mut opt_sec = SecureBits::empty();
    let mut opt_cmd = 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' | 'P') => opt_nnp = true,
            Short('r') => opt_sec.insert(SecureBits::SECBIT_NOROOT),
            Short('R') => opt_sec.insert(SecureBits::SECBIT_NOROOT_LOCKED),
            Short('s') => opt_sec.insert(SecureBits::SECBIT_NO_SETUID_FIXUP),
            Short('S') => opt_sec.insert(SecureBits::SECBIT_NO_SETUID_FIXUP_LOCKED),
            Short('k') => opt_sec.insert(SecureBits::SECBIT_KEEP_CAPS),
            Short('K') => opt_sec.insert(SecureBits::SECBIT_KEEP_CAPS_LOCKED),
            Short('a') => opt_sec.insert(SecureBits::SECBIT_NO_CAP_AMBIENT_RAISE),
            Short('A') => opt_sec.insert(SecureBits::SECBIT_NO_CAP_AMBIENT_RAISE_LOCKED),
            Short('x') => opt_sec.insert(SecureBits::SECBIT_EXEC_RESTRICT_FILE),
            Short('X') => opt_sec.insert(SecureBits::SECBIT_EXEC_RESTRICT_FILE_LOCKED),
            Short('i') => opt_sec.insert(SecureBits::SECBIT_EXEC_DENY_INTERACTIVE),
            Short('I') => opt_sec.insert(SecureBits::SECBIT_EXEC_DENY_INTERACTIVE_LOCKED),
            Value(prog) => {
                opt_cmd = Some(prog);
                opt_arg.extend(parser.raw_args()?);
            }
            _ => return Err(arg.unexpected().into()),
        }
    }

    let cmd = if let Some(cmd) = opt_cmd {
        // Run a command with secure bits set.
        if !opt_nnp && opt_sec.is_empty() {
            eprintln!("syd-sec: No secure bits specified for command!");
            return Err(Errno::EINVAL.into());
        }
        cmd
    } else if !opt_nnp && opt_sec.is_empty() {
        // Print information on process secure bits.
        let nnp = get_no_new_privs()?;
        let sec = get_securebits()?;

        #[expect(clippy::disallowed_methods)]
        let data = json!({
            "nnp": nnp,
            "sec": sec,
        });

        #[expect(clippy::disallowed_methods)]
        let mut data = serde_json::to_string(&data).expect("JSON");
        data.push('\n');
        stdout().write_all(data.as_bytes())?;

        return Ok(ExitCode::SUCCESS);
    } else {
        // Test given secure bits against process secure bits.
        if opt_nnp && !get_no_new_privs()? {
            return Ok(ExitCode::FAILURE);
        }
        if !opt_sec.is_empty() && !get_securebits()?.contains(opt_sec) {
            return Ok(ExitCode::FAILURE);
        }

        return Ok(ExitCode::SUCCESS);
    };

    // Set given secure bits.
    if opt_nnp {
        set_no_new_privs()?;
    }
    if !opt_sec.is_empty() {
        opt_sec.insert(get_securebits()?);
        set_securebits(opt_sec)?;
    }

    // Execute command.
    //
    // We do not use run_cmd here for simplicity.
    Ok(ExitCode::from(
        127 + Command::new(cmd)
            .args(opt_arg)
            .exec()
            .raw_os_error()
            .unwrap_or(0) as u8,
    ))
}

fn help() {
    println!("Usage: syd-sec [-ahikprsxAIKPRSX] {{command [args...]}}");
    println!("Print secure bits or run command with secure bits set.");
    println!("Given no arguments, print information on process secure bits in compact JSON.");
    println!("Given command with arguments, set given secure bits and execute the command.");
    println!("Given no commands and some arguments, test given secure bits and exit with success if all are set.");
    println!("Use -p, -P to set/test no_new_privs attribute");
    println!("Use -r, -R to set/test bit SECBIT_NOROOT");
    println!("Use -s, -S to set/test bit SECBIT_NO_SETUID_FIXUP");
    println!("Use -k, -K to set/test bit SECBIT_KEEP_CAPS");
    println!("Use -a, -A to set/test bit SECBIT_NO_CAP_AMBIENT_RAISE");
    println!("Use -x, -X to set/test bit SECBIT_EXEC_RESTRICT_FILE");
    println!("Use -i, -I to set/test bit SECBIT_DENY_INTERACTIVE");
    println!("Capital letter options set/test locked version of the respective secure bit.");
}
