//
// Syd: rock-solid application kernel
// src/utils/syd-ofd.rs: Take a lock on a file, then execute into another program
//
// Copyright (c) 2025, 2026 Ali Polatel <alip@chesswob.org>
// Based in part upon s6-setlock utility of skarnet s6 suite which is:
//   Copyright (c) 2011-2025 Laurent Bercot <ska-skaware@skarnet.org>
//   SPDX-License-Identifier: ISC
//
// SPDX-License-Identifier: GPL-3.0

// SAFETY: This module has (almost) been liberated from unsafe code!
// 1. We call into sigaction(2) to set SIGALRM handler with -t timeout which needs unsafe.
// Use deny rather than forbid so we can allow this case.
#![deny(unsafe_code)]

use std::{
    env,
    ffi::OsString,
    os::{
        fd::{AsRawFd, RawFd},
        unix::ffi::OsStrExt,
    },
    process::{Command, ExitCode},
};

use btoi::btoi;
use nix::{
    errno::Errno,
    fcntl::{OFlag, AT_FDCWD},
    sys::{
        signal::{sigaction, SaFlags, SigAction, SigHandler, Signal},
        signalfd::SigSet,
        stat::Mode,
    },
};
use syd::{
    compat::{dup3, openat2, OpenHow, ResolveFlag},
    config::{ENV_SH, SYD_SH},
    confine::run_cmd,
    fd::set_cloexec,
    ofd::lock_fd,
    path::XPathBuf,
    retry::retry_on_eintr,
    timer::AlarmTimer,
};

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

// Signal handler function for SIGALRM.
extern "C" fn handle_sigalrm(_: libc::c_int) {}

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

    // Set SIGPIPE handler to default.
    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_block = true;
    let mut opt_fdset = None;
    let mut opt_tmout = None;
    let mut opt_plock = None;
    let mut opt_wlock = true;
    let mut opt_cmd = env::var_os(ENV_SH).unwrap_or(OsString::from(SYD_SH));
    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('n') => opt_block = false,
            Short('N') => opt_block = true,
            Short('r' | 's') => opt_wlock = false,
            Short('w' | 'x') => opt_wlock = true,
            Short('d') => opt_fdset = Some(btoi::<RawFd>(parser.value()?.as_bytes())?),
            Short('t') => opt_tmout = Some(btoi::<u64>(parser.value()?.as_bytes())?),
            Value(lock) => {
                opt_plock = Some(XPathBuf::from(lock));

                let mut raw = parser.raw_args()?;
                if let Some(cmd) = raw.next() {
                    opt_cmd = cmd;
                    opt_arg.extend(raw);
                }
            }
            _ => return Err(arg.unexpected().into()),
        }
    }

    let opt_plock = if let Some(opt_plock) = opt_plock {
        opt_plock
    } else {
        eprintln!("syd-ofd: Lock path is required!");
        return Err(Errno::ENOENT.into());
    };

    if opt_plock.has_parent_dot() {
        eprintln!("syd-ofd: Parent directory (..) components aren't permitted in lock path!");
        return Err(Errno::EACCES.into());
    }

    // Open the lock file:
    // 1. Exclusive lock opens for write+create with mode 0600.
    // 2. Shared lock open for read+create with mode 0600.
    //
    // SAFETY:
    // 1. Do not follow symlinks in any of the path components.
    // 2. Do not follow symlinks in last path component.
    // 3. Do not acquire a controlling TTY.
    // 4. Do not block on FIFOs.
    let mode = Mode::from_bits_truncate(0o600);
    let mut flags = OFlag::O_CREAT | OFlag::O_CLOEXEC | OFlag::O_NONBLOCK | OFlag::O_NOCTTY | OFlag::O_NOFOLLOW;
    if opt_wlock {
        flags.insert(OFlag::O_WRONLY);
    } else {
        flags.insert(OFlag::O_RDONLY);
    }
    let how = OpenHow::new()
        .flags(flags)
        .mode(mode)
        .resolve(ResolveFlag::RESOLVE_NO_MAGICLINKS | ResolveFlag::RESOLVE_NO_SYMLINKS);
    #[expect(clippy::disallowed_methods)]
    let mut fd = retry_on_eintr(|| openat2(AT_FDCWD, &opt_plock, how))?;

    // Create a timer as necessary and hold a reference to it,
    // because timer_delete(2) is called on Drop.
    let timer = if let Some(tmout) = opt_tmout {
        // Ensure -t timeout uses blocking call regardless of -n.
        opt_block = true;

        // Set up the signal handler for SIGALRM.
        let sig_action = SigAction::new(
            SigHandler::Handler(handle_sigalrm),
            SaFlags::empty(),
            SigSet::empty(),
        );

        // SAFETY: Register the handler for SIGALRM.
        // This handler is per-process.
        #[expect(unsafe_code)]
        unsafe { sigaction(Signal::SIGALRM, &sig_action) }?;

        // Set up an alarm timer and start it.
        let mut timer = AlarmTimer::from_milliseconds(tmout)?;
        timer.start()?;

        Some(timer)
    } else {
        None
    };

    // Lock file descriptor.
    //
    // We do NOT retry on EINTR because it's the AlarmTimer.
    lock_fd(&fd, opt_wlock, opt_block)?;

    // Delete the timer which is no longer needed.
    drop(timer);

    // Prepare to pass fd to the child process.
    if let Some(opt_fdset) = opt_fdset {
        if opt_fdset != fd.as_raw_fd() {
            // Atomically duplicate onto the exact fd number.
            // Note we move the old fd into the function so it's dropped on return.
            fd = dup3(fd.as_raw_fd(), opt_fdset, OFlag::O_CLOEXEC.bits())?;
        }
    }
    set_cloexec(&fd, false)?;

    let mut cmd = Command::new(opt_cmd);
    let cmd = cmd.args(opt_arg);
    Ok(ExitCode::from(run_cmd(cmd)))
}

fn help() {
    println!(
        "Usage: syd-ofd [-n | -N] [-t timeout] [-d fd] [-s=-r | -x=-w] file {{command [arg...]}}"
    );
    println!("Take a lock on a file, then execute into another program.");
    println!("Use -n to take a nonblocking lock.");
    println!("Use -N to take a blocking lock. This is the default.");
    println!("Use -t timeout to specify a timeout in milliseconds.");
    println!("Use -s or -r to take a shared lock.");
    println!("Use -x or -w to take an exclusive lock. This is the default.");
    println!("Use -d fd to make the lock visible to program on file descriptor fd.");
}
