//! Bindings for the FreeBSD `procctl` system call.
//!
//! There are similarities (but also differences) with Linux's `prctl` system
//! call, whose interface is located in the `prctl.rs` file.

#![allow(unsafe_code)]

#[cfg(feature = "alloc")]
use alloc::{vec, vec::Vec};
use core::mem::MaybeUninit;
use core::ptr;

use bitflags::bitflags;

use crate::backend::c::{c_int, c_uint, c_void};
use crate::backend::process::syscalls;
use crate::backend::process::types::RawId;
use crate::io;
use crate::process::{Pid, RawPid};
use crate::signal::Signal;
use crate::utils::{as_mut_ptr, as_ptr};

//
// Helper functions.
//

/// Subset of `idtype_t` C enum, with only the values allowed by `procctl`.
#[repr(i32)]
pub enum IdType {
    /// Process id.
    Pid = 0,
    /// Process group id.
    Pgid = 2,
}

/// A process selector for use with the `procctl` interface.
///
/// `None` represents the current process. `Some((IdType::Pid, pid))`
/// represents the process with pid `pid`. `Some((IdType::Pgid, pgid))`
/// represents the control processes belonging to the process group with id
/// `pgid`.
pub type ProcSelector = Option<(IdType, Pid)>;
fn proc_selector_to_raw(selector: ProcSelector) -> (IdType, RawPid) {
    match selector {
        Some((idtype, id)) => (idtype, id.as_raw_nonzero().get()),
        None => (IdType::Pid, 0),
    }
}

#[inline]
pub(crate) unsafe fn procctl(
    option: c_int,
    process: ProcSelector,
    data: *mut c_void,
) -> io::Result<()> {
    let (idtype, id) = proc_selector_to_raw(process);
    syscalls::procctl(idtype as c_uint, id as RawId, option, data)
}

#[inline]
pub(crate) unsafe fn procctl_set<P>(
    option: c_int,
    process: ProcSelector,
    data: &P,
) -> io::Result<()> {
    procctl(option, process, (as_ptr(data) as *mut P).cast())
}

#[inline]
pub(crate) unsafe fn procctl_get_optional<P>(
    option: c_int,
    process: ProcSelector,
) -> io::Result<P> {
    let mut value: MaybeUninit<P> = MaybeUninit::uninit();
    procctl(option, process, value.as_mut_ptr().cast())?;
    Ok(value.assume_init())
}

//
// PROC_PDEATHSIG_STATUS/PROC_PDEATHSIG_CTL
//

const PROC_PDEATHSIG_STATUS: c_int = 12;

/// Get the current value of the parent process death signal.
///
/// # References
///  - [Linux: `prctl(PR_GET_PDEATHSIG,...)`]
///  - [FreeBSD: `procctl(PROC_PDEATHSIG_STATUS,...)`]
///
/// [Linux: `prctl(PR_GET_PDEATHSIG,...)`]: https://man7.org/linux/man-pages/man2/prctl.2.html
/// [FreeBSD: `procctl(PROC_PDEATHSIG_STATUS,...)`]: https://man.freebsd.org/cgi/man.cgi?query=procctl&sektion=2
#[inline]
pub fn parent_process_death_signal() -> io::Result<Option<Signal>> {
    unsafe { procctl_get_optional::<c_int>(PROC_PDEATHSIG_STATUS, None) }.map(Signal::from_raw)
}

const PROC_PDEATHSIG_CTL: c_int = 11;

/// Set the parent-death signal of the calling process.
///
/// # References
///  - [Linux: `prctl(PR_SET_PDEATHSIG,...)`]
///  - [FreeBSD: `procctl(PROC_PDEATHSIG_CTL,...)`]
///
/// [Linux: `prctl(PR_SET_PDEATHSIG,...)`]: https://man7.org/linux/man-pages/man2/prctl.2.html
/// [FreeBSD: `procctl(PROC_PDEATHSIG_CTL,...)`]: https://man.freebsd.org/cgi/man.cgi?query=procctl&sektion=2
#[inline]
pub fn set_parent_process_death_signal(signal: Option<Signal>) -> io::Result<()> {
    let signal = signal.map_or(0, |signal| signal as c_int);
    unsafe { procctl_set::<c_int>(PROC_PDEATHSIG_CTL, None, &signal) }
}

//
// PROC_TRACE_CTL
//

const PROC_TRACE_CTL: c_int = 7;

const PROC_TRACE_CTL_ENABLE: i32 = 1;
const PROC_TRACE_CTL_DISABLE: i32 = 2;
const PROC_TRACE_CTL_DISABLE_EXEC: i32 = 3;

/// `PROC_TRACE_CTL_*`.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[repr(i32)]
pub enum DumpableBehavior {
    /// Not dumpable.
    NotDumpable = PROC_TRACE_CTL_DISABLE,
    /// Dumpable.
    Dumpable = PROC_TRACE_CTL_ENABLE,
    /// Not dumpable, and this behaviour is preserved across `execve` calls.
    NotDumpableExecPreserved = PROC_TRACE_CTL_DISABLE_EXEC,
}

/// Set the state of the `dumpable` attribute for the process indicated by
/// `idtype` and `id`. This determines whether the process can be traced and
/// whether core dumps are produced for the process upon delivery of a signal
/// whose default behavior is to produce a core dump.
///
/// This is similar to `set_dumpable_behavior` on Linux, with the exception
/// that on FreeBSD there is an extra argument `process`. When `process` is set
/// to `None`, the operation is performed for the current process, like on
/// Linux.
///
/// # References
///  - [FreeBSD `procctl(PROC_TRACE_CTL,...)`]
///
/// [FreeBSD `procctl(PROC_TRACE_CTL,...)`]: https://man.freebsd.org/cgi/man.cgi?query=procctl&sektion=2
#[inline]
pub fn set_dumpable_behavior(process: ProcSelector, config: DumpableBehavior) -> io::Result<()> {
    unsafe { procctl(PROC_TRACE_CTL, process, config as usize as *mut _) }
}

//
// PROC_TRACE_STATUS
//

const PROC_TRACE_STATUS: c_int = 8;

/// Tracing status as returned by [`trace_status`].
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum TracingStatus {
    /// Tracing is disabled for the process.
    NotTraceble,
    /// Tracing is not disabled for the process, but not debugger/tracer is
    /// attached.
    Tracable,
    /// The process is being traced by the process whose pid is stored in the
    /// first component of this variant.
    BeingTraced(Pid),
}

/// Get the tracing status of the process indicated by `idtype` and `id`.
///
/// # References
///  - [FreeBSD `procctl(PROC_TRACE_STATUS,...)`]
///
/// [FreeBSD `procctl(PROC_TRACE_STATUS,...)`]: https://man.freebsd.org/cgi/man.cgi?query=procctl&sektion=2
#[inline]
pub fn trace_status(process: ProcSelector) -> io::Result<TracingStatus> {
    let val = unsafe { procctl_get_optional::<c_int>(PROC_TRACE_STATUS, process) }?;
    match val {
        -1 => Ok(TracingStatus::NotTraceble),
        0 => Ok(TracingStatus::Tracable),
        pid => {
            let pid = Pid::from_raw(pid as RawPid).ok_or(io::Errno::RANGE)?;
            Ok(TracingStatus::BeingTraced(pid))
        }
    }
}

//
// PROC_REAP_*
//

const PROC_REAP_ACQUIRE: c_int = 2;
const PROC_REAP_RELEASE: c_int = 3;

/// Acquire or release the reaper status of the calling process.
///
/// # References
///  - [FreeBSD: `procctl(PROC_REAP_ACQUIRE/RELEASE,...)`]
///
/// [FreeBSD: `procctl(PROC_REAP_ACQUIRE/RELEASE,...)`]: https://man.freebsd.org/cgi/man.cgi?query=procctl&sektion=2
#[inline]
pub fn set_reaper_status(reaper: bool) -> io::Result<()> {
    unsafe {
        procctl(
            if reaper {
                PROC_REAP_ACQUIRE
            } else {
                PROC_REAP_RELEASE
            },
            None,
            ptr::null_mut(),
        )
    }
}

const PROC_REAP_STATUS: c_int = 4;

bitflags! {
    /// `REAPER_STATUS_*`.
    #[repr(transparent)]
    #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
    pub struct ReaperStatusFlags: c_uint {
        /// The process has acquired reaper status.
        const OWNED = 1;
        /// The process is the root of the reaper tree (pid 1).
        const REALINIT = 2;

        /// <https://docs.rs/bitflags/*/bitflags/#externally-defined-flags>
        const _ = !0;
    }
}

#[repr(C)]
struct procctl_reaper_status {
    rs_flags: c_uint,
    rs_children: c_uint,
    rs_descendants: c_uint,
    rs_reaper: RawPid,
    rs_pid: RawPid,
    rs_pad0: [c_uint; 15],
}

/// Reaper status as returned by [`get_reaper_status`].
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct ReaperStatus {
    /// The flags.
    pub flags: ReaperStatusFlags,
    /// The number of children of the reaper among the descendants.
    pub children: usize,
    /// The total number of descendants of the reaper(s), not counting
    /// descendants of the reaper in the subtree.
    pub descendants: usize,
    /// The pid of the reaper for the specified process id.
    pub reaper: Pid,
    /// The pid of one reaper child if there are any descendants.
    pub pid: Option<Pid>,
}

/// Get information about the reaper of the specified process (or the process
/// itself if it is a reaper).
///
/// # References
///  - [FreeBSD: `procctl(PROC_REAP_STATUS,...)`]
///
/// [FreeBSD: `procctl(PROC_REAP_STATUS,...)`]: https://man.freebsd.org/cgi/man.cgi?query=procctl&sektion=2
#[inline]
pub fn get_reaper_status(process: ProcSelector) -> io::Result<ReaperStatus> {
    let raw = unsafe { procctl_get_optional::<procctl_reaper_status>(PROC_REAP_STATUS, process) }?;
    Ok(ReaperStatus {
        flags: ReaperStatusFlags::from_bits_retain(raw.rs_flags),
        children: raw.rs_children as _,
        descendants: raw.rs_descendants as _,
        reaper: Pid::from_raw(raw.rs_reaper).ok_or(io::Errno::RANGE)?,
        pid: if raw.rs_pid == -1 {
            None
        } else {
            Some(Pid::from_raw(raw.rs_pid).ok_or(io::Errno::RANGE)?)
        },
    })
}

const PROC_REAP_GETPIDS: c_int = 5;

bitflags! {
    /// `REAPER_PIDINFO_*`.
    #[repr(transparent)]
    #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
    pub struct PidInfoFlags: c_uint {
        /// This structure was filled by the kernel.
        const VALID = 1;
        /// The pid field identifies a direct child of the reaper.
        const CHILD = 2;
        /// The reported process is itself a reaper. Descendants of a
        /// subordinate reaper are not reported.
        const REAPER = 4;
        /// The reported process is in the zombie state.
        const ZOMBIE = 8;
        /// The reported process is stopped by
        /// [`Signal::Stop`]/[`Signal::Tstp`].
        const STOPPED = 16;
        /// The reported process is in the process of exiting.
        const EXITING = 32;
    }
}

#[repr(C)]
#[derive(Default, Clone)]
struct procctl_reaper_pidinfo {
    pi_pid: RawPid,
    pi_subtree: RawPid,
    pi_flags: c_uint,
    pi_pad0: [c_uint; 15],
}

#[repr(C)]
struct procctl_reaper_pids {
    rp_count: c_uint,
    rp_pad0: [c_uint; 15],
    rp_pids: *mut procctl_reaper_pidinfo,
}

/// A child process of a reaper.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct PidInfo {
    /// The flags of the process.
    pub flags: PidInfoFlags,
    /// The pid of the process.
    pub pid: Pid,
    /// The pid of the child of the reaper which is the (grand-..)parent of the
    /// process.
    pub subtree: Pid,
}

/// Get the list of descendants of the specified reaper process.
///
/// # References
///  - [FreeBSD: `procctl(PROC_REAP_GETPIDS,...)`]
///
/// [FreeBSD: `procctl(PROC_REAP_GETPIDS,...)`]: https://man.freebsd.org/cgi/man.cgi?query=procctl&sektion=2
#[cfg(feature = "alloc")]
pub fn get_reaper_pids(process: ProcSelector) -> io::Result<Vec<PidInfo>> {
    // Sadly no better way to guarantee that we get all the results than to
    // allocate ~8MB of memory..
    const PID_MAX: usize = 99999;
    let mut pids: Vec<procctl_reaper_pidinfo> = vec![Default::default(); PID_MAX];
    let mut pinfo = procctl_reaper_pids {
        rp_count: PID_MAX as _,
        rp_pad0: [0; 15],
        rp_pids: pids.as_mut_slice().as_mut_ptr(),
    };
    unsafe { procctl(PROC_REAP_GETPIDS, process, as_mut_ptr(&mut pinfo).cast())? };
    let mut result = Vec::new();
    for raw in pids.into_iter() {
        let flags = PidInfoFlags::from_bits_retain(raw.pi_flags);
        if !flags.contains(PidInfoFlags::VALID) {
            break;
        }
        result.push(PidInfo {
            flags,
            subtree: Pid::from_raw(raw.pi_subtree).ok_or(io::Errno::RANGE)?,
            pid: Pid::from_raw(raw.pi_pid).ok_or(io::Errno::RANGE)?,
        });
    }
    Ok(result)
}

const PROC_REAP_KILL: c_int = 6;

bitflags! {
    /// `REAPER_KILL_*`.
    #[repr(transparent)]
    #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
    struct KillFlags: c_uint {
        const CHILDREN = 1;
        const SUBTREE = 2;
    }
}

#[repr(C)]
struct procctl_reaper_kill {
    rk_sig: c_int,
    rk_flags: c_uint,
    rk_subtree: RawPid,
    rk_killed: c_uint,
    rk_fpid: RawPid,
    rk_pad0: [c_uint; 15],
}

/// Reaper status as returned by [`get_reaper_status`].
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct KillResult {
    /// The number of processes that were signalled.
    pub killed: usize,
    /// The pid of the first process that wasn't successfully signalled.
    pub first_failed: Option<Pid>,
}

/// Deliver a signal to some subset of
///
/// # References
///  - [FreeBSD: `procctl(PROC_REAP_KILL,...)`]
///
/// [FreeBSD: `procctl(PROC_REAP_KILL,...)`]: https://man.freebsd.org/cgi/man.cgi?query=procctl&sektion=2
pub fn reaper_kill(
    process: ProcSelector,
    signal: Signal,
    direct_children: bool,
    subtree: Option<Pid>,
) -> io::Result<KillResult> {
    let mut flags = KillFlags::empty();
    flags.set(KillFlags::CHILDREN, direct_children);
    flags.set(KillFlags::SUBTREE, subtree.is_some());
    let mut req = procctl_reaper_kill {
        rk_sig: signal as c_int,
        rk_flags: flags.bits(),
        rk_subtree: subtree.map(|p| p.as_raw_nonzero().into()).unwrap_or(0),
        rk_killed: 0,
        rk_fpid: 0,
        rk_pad0: [0; 15],
    };
    unsafe { procctl(PROC_REAP_KILL, process, as_mut_ptr(&mut req).cast())? };
    Ok(KillResult {
        killed: req.rk_killed as _,
        first_failed: Pid::from_raw(req.rk_fpid),
    })
}

//
// PROC_TRAPCAP_STATUS/PROC_TRAPCAP_CTL
//

const PROC_TRAPCAP_CTL: c_int = 9;

const PROC_TRAPCAP_CTL_ENABLE: i32 = 1;
const PROC_TRAPCAP_CTL_DISABLE: i32 = 2;

/// `PROC_TRAPCAP_CTL_*`.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[repr(i32)]
pub enum TrapCapBehavior {
    /// Disable the [`Signal::Trap`] signal delivery on capability mode access
    /// violations.
    Disable = PROC_TRAPCAP_CTL_DISABLE,
    /// Enable the [`Signal::Trap`] signal delivery on capability mode access
    /// violations.
    Enable = PROC_TRAPCAP_CTL_ENABLE,
}

/// Set the current value of the capability mode violation trapping behavior.
/// If this behavior is enabled, the kernel would deliver a [`Signal::Trap`]
/// signal on any return from a system call that would result in a
/// [`io::Errno::NOTCAPABLE`]` or [`io::Errno::CAPMODE`] error.
///
/// This behavior is inherited by the children of the process and is kept
/// across `execve` calls.
///
/// # References
///  - [FreeBSD: `procctl(PROC_TRAPCAP_CTL,...)`]
///
/// [FreeBSD: `procctl(PROC_TRAPCAP_CTL,...)`]: https://man.freebsd.org/cgi/man.cgi?query=procctl&sektion=2
#[inline]
pub fn set_trap_cap_behavior(process: ProcSelector, config: TrapCapBehavior) -> io::Result<()> {
    let config = config as c_int;
    unsafe { procctl_set::<c_int>(PROC_TRAPCAP_CTL, process, &config) }
}

const PROC_TRAPCAP_STATUS: c_int = 10;

/// Get the current value of the capability mode violation trapping behavior.
///
/// # References
///  - [FreeBSD: `procctl(PROC_TRAPCAP_STATUS,...)`]
///
/// [FreeBSD: `procctl(PROC_TRAPCAP_STATUS,...)`]: https://man.freebsd.org/cgi/man.cgi?query=procctl&sektion=2
#[inline]
pub fn trap_cap_behavior(process: ProcSelector) -> io::Result<TrapCapBehavior> {
    let val = unsafe { procctl_get_optional::<c_int>(PROC_TRAPCAP_STATUS, process) }?;
    match val {
        PROC_TRAPCAP_CTL_DISABLE => Ok(TrapCapBehavior::Disable),
        PROC_TRAPCAP_CTL_ENABLE => Ok(TrapCapBehavior::Enable),
        _ => Err(io::Errno::RANGE),
    }
}

//
// PROC_NO_NEW_PRIVS_STATUS/PROC_NO_NEW_PRIVS_CTL
//

const PROC_NO_NEW_PRIVS_CTL: c_int = 19;

const PROC_NO_NEW_PRIVS_ENABLE: c_int = 1;

/// Enable the `no_new_privs` mode that ignores SUID and SGID bits on `execve`
/// in the specified process and its future descendants.
///
/// This is similar to `set_no_new_privs` on Linux, with the exception that on
/// FreeBSD there is no argument `no_new_privs` argument as it's only possible
/// to enable this mode and there's no going back.
///
/// # References
///  - [Linux: `prctl(PR_SET_NO_NEW_PRIVS,...)`]
///  - [FreeBSD: `procctl(PROC_NO_NEW_PRIVS_CTL,...)`]
///
/// [Linux: `prctl(PR_SET_NO_NEW_PRIVS,...)`]: https://man7.org/linux/man-pages/man2/prctl.2.html
/// [FreeBSD: `procctl(PROC_NO_NEW_PRIVS_CTL,...)`]: https://man.freebsd.org/cgi/man.cgi?query=procctl&sektion=2
#[inline]
pub fn set_no_new_privs(process: ProcSelector) -> io::Result<()> {
    unsafe { procctl_set::<c_int>(PROC_NO_NEW_PRIVS_CTL, process, &PROC_NO_NEW_PRIVS_ENABLE) }
}

const PROC_NO_NEW_PRIVS_STATUS: c_int = 20;

/// Check the `no_new_privs` mode of the specified process.
///
/// # References
///  - [Linux: `prctl(PR_GET_NO_NEW_PRIVS,...)`]
///  - [FreeBSD: `procctl(PROC_NO_NEW_PRIVS_STATUS,...)`]
///
/// [Linux: `prctl(PR_GET_NO_NEW_PRIVS,...)`]: https://man7.org/linux/man-pages/man2/prctl.2.html
/// [FreeBSD: `procctl(PROC_NO_NEW_PRIVS_STATUS,...)`]: https://man.freebsd.org/cgi/man.cgi?query=procctl&sektion=2
#[inline]
pub fn no_new_privs(process: ProcSelector) -> io::Result<bool> {
    unsafe { procctl_get_optional::<c_int>(PROC_NO_NEW_PRIVS_STATUS, process) }
        .map(|x| x == PROC_NO_NEW_PRIVS_ENABLE)
}
