//
// Syd: rock-solid application kernel
// src/utils/syd-path.rs: Write Integrity Force rules for binaries and list executables under PATH
//
// Copyright (c) 2024, 2025, 2026 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

#![allow(clippy::disallowed_types)]

use std::{
    collections::HashSet,
    env,
    fs::{canonicalize, read_dir, File},
    io::Write,
    os::unix::ffi::OsStrExt,
    path::Path,
    process::ExitCode,
};

use ahash::RandomState;
use data_encoding::HEXLOWER;
use nix::unistd::{access, AccessFlags};
use syd::{
    elf::{ElfType, ExecutableFile, LinkingType},
    fd::open_static_proc,
    hash::{hash, hash_auto, hash_list},
    path::XPathBuf,
};

// 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;

#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
enum Filter {
    ElfFilter32,
    ElfFilter64,
    ElfFilterDynamic,
    ElfFilterStatic,
    ElfFilterPIE,
    ElfFilterNoPIE,
    ElfFilterXStack,
    Script,
}

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

    syd::set_sigpipe_dfl()?;

    // Parse CLI options.
    let mut opt_dsyd = false;
    #[expect(clippy::disallowed_methods)]
    let mut opt_path = env::var("PATH").unwrap_or("/usr/bin:/bin".to_string());
    let mut opt_func = "auto".to_string();
    let mut opt_action = None;
    let mut opt_limit = 0usize; // 0 means no limit.
    let mut elf_set: HashSet<Filter, RandomState> = HashSet::default();

    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_func = parser.value()?.to_str().ok_or(nix::errno::Errno::EINVAL)?.to_string(),
            Short('k') => opt_action = Some("kill"),
            Short('w') => opt_action = Some("warn"),
            Short('p') => opt_path = parser.value()?.parse::<String>()?,
            Short('l') => opt_limit = parser.value()?.parse::<usize>()?,
            Short('s') => opt_dsyd = true,
            Short('e') => match parser.value()?.parse::<String>()?.as_str() {
                "32" => {
                    if elf_set.contains(&Filter::ElfFilter64) {
                        eprintln!("The option -e32 conflicts with -e64!");
                        return Ok(ExitCode::FAILURE);
                    }
                    elf_set.insert(Filter::ElfFilter32);
                }
                "64" => {
                    if elf_set.contains(&Filter::ElfFilter32) {
                        eprintln!("The option -e64 conflicts with -e32!");
                        return Ok(ExitCode::FAILURE);
                    }
                    elf_set.insert(Filter::ElfFilter64);
                }
                "d" => {
                    if elf_set.contains(&Filter::ElfFilterStatic) {
                        eprintln!("The option -ed conflicts with -es!");
                        return Ok(ExitCode::FAILURE);
                    }
                    elf_set.insert(Filter::ElfFilterDynamic);
                }
                "s" => {
                    if elf_set.contains(&Filter::ElfFilterDynamic) {
                        eprintln!("The option -es conflicts with -ed!");
                        return Ok(ExitCode::FAILURE);
                    }
                    elf_set.insert(Filter::ElfFilterStatic);
                }
                "p" => {
                    if elf_set.contains(&Filter::ElfFilterNoPIE) {
                        eprintln!("The option -ep conflicts with -eP!");
                        return Ok(ExitCode::FAILURE);
                    }
                    elf_set.insert(Filter::ElfFilterPIE);
                }
                "P" => {
                    if elf_set.contains(&Filter::ElfFilterPIE) {
                        eprintln!("The option -eP conflicts with -ep!");
                        return Ok(ExitCode::FAILURE);
                    }
                    elf_set.insert(Filter::ElfFilterNoPIE);
                }
                "x" => {
                    elf_set.insert(Filter::Script);
                }
                "X" => {
                    elf_set.insert(Filter::ElfFilterXStack);
                }
                value => {
                    eprintln!("Unknown ELF option: -e{value}");
                    return Ok(ExitCode::FAILURE);
                }
            },
            _ => return Err(arg.unexpected().into()),
        }
    }

    // -a list: print available algorithms and exit.
    if opt_func == "list" {
        open_static_proc()?;
        for name in hash_list()? {
            println!("{name}");
        }
        return Ok(ExitCode::SUCCESS);
    }

    // -a is required unless we're only listing ELF info.
    // Default to "auto" which detects the best available algorithm.
    if elf_set.is_empty() && opt_func == "auto" {
        match hash_auto() {
            Some(func) => opt_func = func,
            None => {
                eprintln!("Error: No supported hash algorithm found!");
                return Ok(ExitCode::FAILURE);
            }
        }
    }

    let mut count = 0usize;
    let mut path_set: HashSet<XPathBuf, RandomState> = HashSet::default();
    let dirs = opt_path.split(':');
    for dir in dirs {
        if !Path::new(dir).is_dir() {
            continue;
        }
        #[expect(clippy::disallowed_methods)]
        if let Ok(entries) = read_dir(dir) {
            for entry in entries.flatten() {
                // Ensure the file is executable.
                let path = entry.path();
                if path.is_file() && access(&path, AccessFlags::X_OK).is_ok() {
                    if let Ok(path) = canonicalize(path).map(XPathBuf::from) {
                        if !path_set.insert(path.clone()) {
                            // Path already seen before.
                            continue;
                        }
                        if !elf_set.is_empty() {
                            // Filter ELF files.
                            #[expect(non_snake_case)]
                            let filter = if let Ok(file) = File::open(&path) {
                                let filter_32 = elf_set.contains(&Filter::ElfFilter32);
                                let filter_64 = elf_set.contains(&Filter::ElfFilter64);
                                let filter_d = elf_set.contains(&Filter::ElfFilterDynamic);
                                let filter_s = elf_set.contains(&Filter::ElfFilterStatic);
                                let filter_p = elf_set.contains(&Filter::ElfFilterPIE);
                                let filter_P = elf_set.contains(&Filter::ElfFilterNoPIE);
                                let filter_x = elf_set.contains(&Filter::Script);
                                let filter_X = elf_set.contains(&Filter::ElfFilterXStack);
                                let check_linking =
                                    filter_d || filter_s || filter_p || filter_P || filter_X;
                                if let Ok(exe) = ExecutableFile::parse(file, check_linking) {
                                    match exe {
                                        ExecutableFile::Elf {
                                            elf_type: ElfType::Elf32,
                                            ..
                                        } if filter_32 => true,
                                        ExecutableFile::Elf {
                                            elf_type: ElfType::Elf64,
                                            ..
                                        } if filter_64 => true,
                                        ExecutableFile::Elf {
                                            linking_type: Some(LinkingType::Dynamic),
                                            ..
                                        } if filter_d => true,
                                        ExecutableFile::Elf {
                                            linking_type: Some(LinkingType::Static),
                                            ..
                                        } if filter_s => true,
                                        ExecutableFile::Elf { pie: true, .. } if filter_p => true,
                                        ExecutableFile::Elf { pie: false, .. } if filter_P => true,
                                        ExecutableFile::Elf { xs: true, .. } if filter_X => true,
                                        ExecutableFile::Script if filter_x => true,
                                        _ => false,
                                    }
                                } else {
                                    false
                                }
                            } else {
                                false
                            };

                            #[expect(clippy::disallowed_methods)]
                            if filter {
                                let stdout = std::io::stdout();
                                let mut handle = stdout.lock();
                                handle.write_all(path.as_os_str().as_bytes()).unwrap();
                                handle.write_all(b"\n").unwrap();
                            }
                        } else if let Ok(mut file) = File::open(&path) {
                            // Filter ELF files.
                            // Force sandboxing does not apply to scripts.
                            if let Ok(true) = ExecutableFile::is_elf_file(&mut file) {
                                // Write Integrity Force Rules.
                                if let Ok(key) = hash(&opt_func, &file) {
                                    let key = HEXLOWER.encode(&key);
                                    let pre = if opt_dsyd { "/dev/syd/" } else { "" };
                                    if let Some(act) = opt_action {
                                        println!("{pre}force+{path}:{opt_func}:{key}:{act}");
                                    } else {
                                        println!("{pre}force+{path}:{opt_func}:{key}");
                                    }
                                    if opt_limit > 0 {
                                        count += 1;
                                        if count >= opt_limit {
                                            return Ok(ExitCode::SUCCESS);
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    Ok(ExitCode::SUCCESS)
}

fn help() {
    println!("Usage: syd-path [-a <algorithm>] [-heklpsw]");
    println!("Write Integrity Force rules for binaries under PATH.");
    println!("If at least one of the various *-e* options is specified,");
    println!("List executables with specified information under PATH.");
    println!();
    println!("  -a <alg>  Hash algorithm (default: auto-detect best available).");
    println!("            Any algorithm listed in proc_crypto(5) with type ahash or shash.");
    println!("            Use `-a list' to list available algorithms.");
    println!("            Use `-a auto' to auto-detect the best algorithm (default).");
    println!("            Examples: sha256, sha512, sha3-512, blake2b-256, md5, crc32c");
    println!("  -k        Use action kill (default).");
    println!("  -w        Use action warn.");
    println!("  -p <path> Specify alternative PATH.");
    println!("  -l <num>  Limit by number of entries.");
    println!("  -s        Prefix rules with /dev/syd/.");
    println!("  -e32      List 32-bit ELF executables (conflicts with -e64).");
    println!("  -e64      List 64-bit ELF executables (conflicts with -e32).");
    println!("  -ed       List dynamically linked ELF executables (conflicts with -es).");
    println!("  -es       List statically linked ELF executables (conflicts with -ed).");
    println!("  -ep       List PIE executables (conflicts with -eP).");
    println!("  -eP       List non-PIE executables (conflicts with -ep).");
    println!("  -ex       List scripts under PATH.");
    println!("  -eX       List binaries with executable stack.");
    println!("  -h        Display this help.");
}
