//
// Syd: rock-solid application kernel
// src/syd-path.rs: Write Integrity Force rules for binaries and list executables under PATH
//
// Copyright (c) 2024, 2025 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::{BufReader, Seek, Write},
    os::unix::ffi::OsStrExt,
    path::Path,
    process::ExitCode,
};

use ahash::RandomState;
use hex::DisplayHex;
use nix::unistd::{access, AccessFlags};
use syd::{
    elf::{ElfType, ExecutableFile, LinkingType},
    err::SydResult,
    hash::HashAlgorithm,
    path::XPathBuf,
};

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

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

    syd::set_sigpipe_dfl()?;

    // Parse CLI options.
    let mut opt_dsyd = false;
    #[allow(clippy::disallowed_methods)]
    let mut opt_path = env::var("PATH").unwrap_or("/usr/bin:/bin".to_string());
    let mut opt_func = HashAlgorithm::Sha512;
    let mut opt_action = "kill";
    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('1') => opt_func = HashAlgorithm::Sha1,
            Short('2') => opt_func = HashAlgorithm::Sha256,
            Short('3') => opt_func = HashAlgorithm::Sha384,
            Short('5') => opt_func = HashAlgorithm::Sha512,
            Short('c') => opt_func = HashAlgorithm::Crc64,
            Short('C') => opt_func = HashAlgorithm::Crc32,
            Short('m') => opt_func = HashAlgorithm::Md5,
            Short('k') => opt_action = "kill",
            Short('w') => opt_action = "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()),
        }
    }

    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;
        }
        #[allow(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.
                            #[allow(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
                            };

                            #[allow(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) {
                                // Rewind the file for hash calculation.
                                if let Ok(()) = file.rewind() {
                                    // Write Integrity Force Rules.
                                    let reader = BufReader::new(file);
                                    if let Ok(key) = syd::hash::hash(reader, opt_func) {
                                        let prefix = if opt_dsyd { "/dev/syd/" } else { "" };
                                        println!(
                                            "{}force+{}:{:x}:{}",
                                            prefix,
                                            path,
                                            key.as_hex(),
                                            opt_action
                                        );
                                        if opt_limit > 0 {
                                            count += 1;
                                            if count >= opt_limit {
                                                return Ok(ExitCode::SUCCESS);
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    Ok(ExitCode::SUCCESS)
}

fn help() {
    println!("Usage: syd-path [-h12cCeklpsw]");
    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!("Use -c to calculate CRC64 checksum instead of SHA3-512 (\x1b[91minsecure\x1b[0m).");
    println!("Use -C to calculate CRC32 checksum instead of SHA3-512 (\x1b[91minsecure\x1b[0m).");
    println!("Use -m to calculate MD5 instead of SHA3-512 (\x1b[91minsecure\x1b[0m, \x1b[96mPortage\x1b[0m/\x1b[95mPaludis\x1b[0m vdb compat).");
    println!("Use -1 to calculate SHA1 instead of SHA3-512 (\x1b[91minsecure\x1b[0m).");
    println!("Use -2 to calculate SHA3-256 instead of SHA3-512.");
    println!("Use -k for kill (default) or -w for warn.");
    println!("Specify alternative PATH with -p.");
    println!("Use -l <num> to limit by number of entries.");
    println!("Use -s to prefix rules with /dev/syd.");
    println!("Use -e32 to list 32-bit ELF executables under PATH (conflicts with -e64).");
    println!("Use -e64 to list 64-bit ELF executables under PATH (conflicts with -e32).");
    println!("Use -ed to list dynamically linked ELF executables under PATH (conflicts with -es).");
    println!("Use -es to list statically linked ELF executables under PATH (conflicts with -ed).");
    println!("Use -ep to list PIE executables under PATH (conflicts with -eP).");
    println!("Use -eP to list non-PIE executables under PATH (conflicts with -ep).");
    println!("Use -ex to list scripts under PATH.");
    println!("Use -eX to list binaries with executable stack under PATH.");
}
