//
// Syd: rock-solid application kernel
// src/utils/syd-read.rs: Print the canonicalized path name followed by a newline and exit.
//
// Copyright (c) 2024, 2025 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

use std::{
    io::{stdout, Write},
    os::{fd::AsRawFd, unix::ffi::OsStrExt},
    process::ExitCode,
};

use bitflags::Flags as BitFlags;
use btoi::btoi;
use libc::pid_t;
use nix::{
    errno::Errno,
    fcntl::{open, OFlag},
    sys::stat::Mode,
    unistd::Pid,
};
use syd::{
    lookup::{safe_canonicalize, FsFlags},
    path::XPathBuf,
    sandbox::{Flags, Sandbox},
    syslog::LogLevel,
};

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

    // Set SIGPIPE handler to default.
    syd::set_sigpipe_dfl()?;

    // Initialize logging.
    syd::log::log_init_simple(LogLevel::Warn)?;

    // 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 paths = Vec::new();
    let mut flags = Flags::empty();
    let mut fsflags = FsFlags::empty();
    let mut opt_delimiter = b"\n";
    let mut opt_dtrailing = true;
    let mut opt_cnt = 1;
    let mut opt_dir = None;
    let mut opt_pid = Pid::this();

    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_cnt = btoi::<usize>(parser.value()?.as_bytes()).or(Err(Errno::EINVAL))?;
            }
            Short('d') => opt_dir = Some(parser.value().map(XPathBuf::from)?),
            Short('p') => {
                opt_pid = btoi::<pid_t>(parser.value()?.as_bytes())
                    .map(Pid::from_raw)
                    .or(Err(Errno::EINVAL))?
            }
            Short('B') => fsflags.insert(FsFlags::RESOLVE_BENEATH),
            Short('R') => fsflags.insert(FsFlags::RESOLVE_IN_ROOT),
            Short('D') => fsflags.insert(FsFlags::NO_RESOLVE_DOTDOT),
            Short('F') => fsflags.insert(FsFlags::NO_RESOLVE_PATH),
            Short('M') => fsflags.insert(FsFlags::MISS_LAST),
            Short('N') => fsflags.insert(FsFlags::NO_FOLLOW_LAST),
            Short('P') => fsflags.insert(FsFlags::NO_RESOLVE_PROC),
            Short('U') => flags.insert(Flags::FL_ALLOW_UNSAFE_MAGICLINKS),
            Short('X') => fsflags.insert(FsFlags::NO_RESOLVE_XDEV),
            Short('m') => fsflags.insert(FsFlags::MUST_PATH),
            Short('n') => opt_dtrailing = false,
            Short('z') => opt_delimiter = b"\0",
            Value(path) => {
                paths.push(XPathBuf::from(path));
                paths.extend(parser.raw_args()?.map(XPathBuf::from));
            }
            _ => return Err(arg.unexpected().into()),
        }
    }

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

    // -m conflicts with -M:
    if fsflags.contains(FsFlags::MUST_PATH | FsFlags::MISS_LAST) {
        eprintln!("syd-read: -m and -M options are mutually exclusive!");
        return Err(Errno::EINVAL.into());
    }

    // -B conflicts with -R:
    if fsflags.contains(FsFlags::RESOLVE_BENEATH | FsFlags::RESOLVE_IN_ROOT) {
        eprintln!("syd-read: -B and -R options are mutually exclusive!");
        return Err(Errno::EINVAL.into());
    }

    // Open beneath directory if given.
    #[expect(clippy::disallowed_methods)]
    let opt_dir = if let Some(ref dir) = opt_dir {
        Some(open(
            dir,
            OFlag::O_DIRECTORY | OFlag::O_PATH,
            Mode::empty(),
        )?)
    } else {
        None
    };

    // Ensure the static file descriptors are open
    // before calling `syd::fs::safe_canonicalize`
    // which is a requirement.
    syd::config::proc_init()?;

    let mut sandbox = Sandbox::default();
    sandbox.flags.clear();
    sandbox.flags.insert(flags);
    sandbox.state.clear();

    for (idx, path) in paths
        .iter()
        .cycle()
        .take(opt_cnt.saturating_mul(paths.len()))
        .enumerate()
    {
        let path = match safe_canonicalize(
            opt_pid,
            opt_dir.as_ref().map(|fd| fd.as_raw_fd()),
            path,
            fsflags,
            Some(&sandbox),
        ) {
            Ok(path) => path.take(),
            Err(errno) => {
                eprintln!("syd-read: Error canonicalizing path `{path}': {errno}!");
                return Err(errno.into());
            }
        };

        if idx > 0 {
            stdout().write_all(opt_delimiter)?;
        }
        stdout().write_all(path.as_bytes())?;
    }

    if opt_dtrailing {
        stdout().write_all(opt_delimiter)?;
    }

    // Close static file descriptors for clean exit.
    syd::config::proc_close();

    Ok(ExitCode::SUCCESS)
}

fn help() {
    println!("Usage: syd-read [-hmnzBDFMNPRUX] [-c n] [-d dir] [-p pid] path...");
    println!("Print resolved symbolic links or canonical file names.");
    println!("By default last component may exist, other components must exist.");
    println!("  -h       Print this help message and exit.");
    println!("  -c <n>   Cycle through the path list n times, useful for benchmarking.");
    println!("  -d <dir> Resolve relative to the given directory.");
    println!("  -p <pid> Resolve from the perspective of the given process ID.");
    println!("  -m       All components of the paths must exist, conflicts with -M.");
    println!(
        "  -M       Last component must not exist, other components must exist, conflicts with -m."
    );
    println!("  -B       Resolve beneath the given directory, useful with -d <dir>. Implies -P, conflicts with -R");
    println!("  -R       Treat the directory as root directory, useful with -d <dir>. Implies -P, conflicts with -B");
    println!("  -D       Do not traverse through `..` components.");
    println!("  -X       Do not traverse through mount points.");
    println!("  -F       Do not follow symbolic links for any of the path components.");
    println!("  -N       Do not follow symbolic links for the last path component.");
    println!("  -P       Do not resolve /proc magic symbolic links.");
    println!("  -U       Resolve unsafe /proc magic symbolic links.");
    println!("  -n       Do not output the trailing delimiter.");
    println!("  -z       End each output line with NUL not newline.");
}
