//
// Syd: rock-solid application kernel
// src/utils/syd-ls.rs: Print the names of the system calls which belong to the given set and exit
//                If set is prctl, print the list of allowed prctl options
//                If set is personality, print the list of allowed personalities.
//
// Copyright (c) 2024, 2025, 2026 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

use std::{
    ffi::OsStr,
    fs::OpenOptions,
    os::unix::{ffi::OsStrExt, fs::OpenOptionsExt},
    path::Path,
    process::ExitCode,
    time::Instant,
};

use data_encoding::HEXLOWER;
use libseccomp::ScmpSyscall;
use nix::{errno::Errno, unistd::isatty};
use syd::{
    compat::getdents64, config::DIRENT_BUF_SIZE, err::SydResult, hash::SydHashSet, path::mask_path,
};

// 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! {
    syd::set_sigpipe_dfl()?;

    let mut args = std::env::args();

    match args.nth(1).as_deref() {
        None => {
            // Given no arguments, list current directory using getdents64(2).
            readdir_cwd()?;
            return Ok(ExitCode::SUCCESS);
        }
        Some("-h") => {
            println!("Usage: syd-ls [set]");
            println!("Print the names of the system calls which belong to the given set and exit.");
            println!("If set is drop, print the list of capabilities that are dropped at startup.");
            println!("If set is env, print the list of unsafe environment variables.");
            println!("If set is fs, print the list of known filesystem types.");
            println!("If set is madvise, print the list of allowed madvise(2) advice.");
            println!("If set is prctl, print the list of allowed prctl(2) options.");
            println!("If set is personality, print the list of allowed personalities.");
            println!("If set is setsockopt, print the list of denied setsockopt(2) options.");
            println!("Available sets are:");
            println!("- cpu");
            println!("- dead");
            println!("- deny");
            println!("- deprecated");
            println!("- ebpf");
            println!("- futex");
            println!("- getid");
            println!("- hook");
            println!("- keyring");
            println!("- kill");
            println!("- mount");
            println!("- msgqueue");
            println!("- nice");
            println!("- page_cache");
            println!("- perf");
            println!("- pkey");
            println!("- ptrace");
            println!("- safe");
            println!("- setid");
            println!("- shm");
            println!("- time");
            println!("- uring");
            println!("- uts");
            println!("Given no set, list all files in the current working directory.");
            println!("In this mode, getdents64(2) is used directly.");
            println!("Use to list files in untrusted directories with huge number of files.");
            println!("File names are printed hex-encoded, delimited by newline, use syd-hex(1) to decode.");
            println!("See EXAMPLES section in syd-ls(1) manual page.");
        }
        Some("deny") => {
            let mut syscall_set: SydHashSet<_> = syd::config::SAFE_SYSCALLS
                .iter()
                .map(|&s| String::from(s))
                .collect();
            for syscall in syd::config::HOOK_SYSCALLS {
                syscall_set.insert(syscall.to_string());
            }
            let mut list = vec![];
            for syscall_number in 0..=600 {
                let syscall = ScmpSyscall::from(syscall_number);
                if let Ok(name) = syscall.get_name() {
                    if !syscall_set.contains(&name) {
                        list.push(name);
                    }
                }
            }
            list.sort_unstable();
            for name in list {
                println!("{name}");
            }
        }
        Some("cpu") => {
            for name in syd::config::CPU_SYSCALLS {
                println!("{name}");
            }
        }
        Some("dead") => {
            for name in syd::config::DEAD_SYSCALLS {
                println!("{name}");
            }
        }
        Some("deprecated") => {
            for name in syd::config::DEPRECATED_SYSCALLS {
                println!("{name}");
            }
        }
        Some("ebpf") => {
            for name in syd::config::EBPF_SYSCALLS {
                println!("{name}");
            }
        }
        Some("futex") => {
            for name in syd::config::FUTEX_SYSCALLS {
                println!("{name}");
            }
        }
        Some("getid") => {
            for name in syd::config::GETID_SYSCALLS {
                println!("{name}");
            }
        }
        Some("hook") => {
            for name in syd::config::HOOK_SYSCALLS {
                println!("{name}");
            }
        }
        Some("keyring") => {
            for name in syd::config::KEYRING_SYSCALLS {
                println!("{name}");
            }
        }
        Some("kill") => {
            for name in syd::config::KILL_SYSCALLS {
                println!("{name}");
            }
        }
        Some("mount") => {
            for name in syd::config::MOUNT_SYSCALLS {
                println!("{name}");
            }
        }
        Some("msgqueue") => {
            for name in syd::config::MSGQUEUE_SYSCALLS {
                println!("{name}");
            }
        }
        Some("nice") => {
            for name in syd::config::NICE_SYSCALLS {
                println!("{name}");
            }
        }
        Some("page_cache") => {
            for name in syd::config::PAGE_CACHE_SYSCALLS {
                println!("{name}");
            }
        }
        Some("perf") => {
            for name in syd::config::PERF_SYSCALLS {
                println!("{name}");
            }
        }
        Some("pkey") => {
            for name in syd::config::PKEY_SYSCALLS {
                println!("{name}");
            }
        }
        Some("ptrace") => {
            for name in syd::config::PTRACE_SYSCALLS {
                println!("{name}");
            }
        }
        Some("safe") | Some("allow") => {
            for name in syd::config::SAFE_SYSCALLS {
                println!("{name}");
            }
        }
        Some("setid") => {
            for name in syd::config::SET_ID_SYSCALLS {
                println!("{name}");
            }
        }
        Some("shm") => {
            for name in syd::config::SHM_SYSCALLS {
                println!("{name}");
            }
        }
        Some("time") => {
            for name in syd::config::TIME_SYSCALLS {
                println!("{name}");
            }
        }
        Some("uring") => {
            for name in syd::config::IOURING_SYSCALLS {
                println!("{name}");
            }
        }
        Some("uts") => {
            for name in syd::config::UTS_SYSCALLS {
                println!("{name}");
            }
        }
        Some("env") => {
            for env in syd::config::UNSAFE_ENV {
                let env = mask_path(Path::new(OsStr::from_bytes(env)));
                println!("{env}");
            }
        }
        Some("fs") => {
            for (fstype, fstype_id) in syd::config::FS_MAGIC {
                println!("{fstype_id:#x}\t{fstype}");
            }
        }
        Some("personality") => {
            for (name, _) in syd::config::SAFE_PERSONAS {
                println!("{name}");
            }
        }
        Some("madvise") => {
            for (name, _) in syd::config::ALLOW_MADVISE {
                println!("{name}");
            }
        }
        Some("prctl") => {
            for (name, _) in syd::config::ALLOW_PRCTL {
                println!("{name}");
            }
        }
        Some("setsockopt") => {
            for (level, optname) in syd::config::DENY_SETSOCKOPT {
                println!("{level:#x}:{optname:#x}");
            }
        }
        // Syscall sets for Syd threads, undocumented.
        Some("syd_emu") => {
            for name in syd::config::EMU_SYSCALLS {
                println!("{name}");
            }
        }
        Some("syd_int") => {
            for name in syd::config::INT_SYSCALLS {
                println!("{name}");
            }
        }
        Some("syd_out") => {
            for name in syd::config::OUT_SYSCALLS {
                println!("{name}");
            }
        }
        Some("syd_ipc") => {
            for name in syd::config::IPC_SYSCALLS {
                println!("{name}");
            }
        }
        Some("syd_aes") => {
            for name in syd::config::AES_SYSCALLS {
                println!("{name}");
            }
        }
        Some("syd_main") => {
            for name in syd::config::MAIN_SYSCALLS {
                println!("{name}");
            }
        }
        Some("syd_oci") => {
            for name in syd::config::OCI_SYSCALLS {
                println!("{name}");
            }
        }
        Some(set) => {
            eprintln!("No such set: '{set}'");
            return Ok(ExitCode::FAILURE);
        }
    }

    Ok(ExitCode::SUCCESS)
}

fn readdir_cwd() -> SydResult<()> {
    // Open a file descriptor to the current directory.
    #[expect(clippy::disallowed_methods)]
    let cwd = OpenOptions::new()
        .read(true)
        .custom_flags(libc::O_DIRECTORY)
        .open(".")?;

    let report_progress = isatty(std::io::stderr())?;
    let epoch = if report_progress {
        Some(Instant::now())
    } else {
        None
    };

    let mut count: u64 = 0;
    loop {
        let mut entries = match getdents64(&cwd, DIRENT_BUF_SIZE) {
            Ok(entries) => entries,
            Err(Errno::ECANCELED) => break, // EOF or empty directory
            Err(errno) => return Err(errno.into()),
        };

        for entry in &mut entries {
            // SAFETY: Hex-encode filename to mitigate terminal vulnerabilities.
            let name = HEXLOWER.encode(entry.name_bytes());
            println!("{name}");

            if report_progress {
                count = count.saturating_add(1);
                if count % 25000 == 0 {
                    eprint!("\r\x1b[Ksyd-ls: {count} files");
                }
            }
        }
    }

    if let Some(epoch) = epoch {
        let dur = epoch.elapsed().as_secs_f64();
        eprintln!("\r\x1b[Ksyd-ls: Listed {count} files in {dur} seconds.");
    }

    Ok(())
}
