//
// pandora: Syd's Dump Inspector & Profile Writer
// sys.rs: Syscall interface
//
// Copyright (c) 2023, 2024, 2025, 2026 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

use std::{io, io::Read, os::fd::AsFd};

use nix::{
    errno::Errno,
    fcntl::{fcntl, FcntlArg, FdFlag},
};
#[cfg(feature = "force")]
use nix::{
    fcntl::{OFlag, OpenHow, ResolveFlag},
    sys::stat::Mode,
};

// Define SYSLOG_ACTION_* constants.
// libc does not have to define these.
pub(crate) const SYSLOG_ACTION_READ_ALL: libc::c_int = 3;
pub(crate) const SYSLOG_ACTION_SIZE_BUFFER: libc::c_int = 10;

pub(crate) struct Syslog;

impl Syslog {
    pub(crate) fn open() -> io::Result<io::Cursor<Vec<u8>>> {
        let mut buf = vec![0u8; Self::capacity()?];
        loop {
            return match Syslog.read(&mut buf) {
                Ok(n) => {
                    buf.truncate(n);
                    Ok(io::Cursor::new(buf))
                }
                Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
                Err(e) => return Err(e),
            };
        }
    }

    fn capacity() -> io::Result<usize> {
        // Retrieve total size of kernel log buffer.
        //
        // SAFETY:
        // 1. klogctl(2) with SIZE_BUFFER takes no buffer.
        // 2. nix has no wrapper for syslog(2).
        loop {
            return match Errno::result(unsafe {
                libc::klogctl(SYSLOG_ACTION_SIZE_BUFFER, std::ptr::null_mut(), 0)
            }) {
                Ok(n) => usize::try_from(n)
                    .or(Err(io::Error::from_raw_os_error(Errno::EOVERFLOW as i32))),
                Err(Errno::EINTR) => continue,
                Err(errno) => Err(io::Error::from_raw_os_error(errno as i32)),
            };
        }
    }
}

impl Read for Syslog {
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        // SAFETY:
        // 1. klogctl(2) with READ_ALL writes at most len bytes into buffer.
        // 2. nix has no wrapper for syslog(2).
        let len = libc::c_int::try_from(buf.len()).unwrap_or(libc::c_int::MAX);
        Errno::result(unsafe {
            libc::klogctl(SYSLOG_ACTION_READ_ALL, buf.as_mut_ptr().cast(), len)
        })
        .map(|size| size as usize)
        .map_err(|errno| io::Error::from_raw_os_error(errno as i32))
    }
}

// Return a safe OpenHow structure.
#[cfg(feature = "force")]
pub(crate) fn safe_open_how(flags: OFlag) -> OpenHow {
    // Leave caller to handle O_NOCTTY,
    // because its use is invalid with O_PATH.
    let mode = if flags.contains(OFlag::O_CREAT) || flags.contains(OFlag::O_TMPFILE) {
        Mode::from_bits_truncate(0o600)
    } else {
        Mode::empty()
    };
    OpenHow::new()
        .flags(flags | OFlag::O_CLOEXEC | OFlag::O_NOFOLLOW)
        .mode(mode)
        .resolve(ResolveFlag::RESOLVE_NO_MAGICLINKS | ResolveFlag::RESOLVE_NO_SYMLINKS)
}

// Returns true if Syd binary in PATH was built with "force" feature.
#[cfg(feature = "force")]
pub(crate) fn syd_have_force() -> bool {
    std::process::Command::new("syd")
        .arg("--version")
        .output()
        .map(|output| {
            const KEY: &[u8] = b"+force";
            output.status.success() && output.stdout.windows(KEY.len()).any(|window| window == KEY)
        })
        .unwrap_or(false)
}

// Returns landlock(7) ABI version or None.
pub(crate) fn landlock_abi() -> Option<libc::c_long> {
    const LANDLOCK_CREATE_RULESET_VERSION: libc::c_uint = 1;

    // SAFETY: Safe version check call, no attributes or pointers.
    let abi = unsafe {
        libc::syscall(
            libc::SYS_landlock_create_ruleset,
            std::ptr::null::<libc::c_void>(),
            0usize,
            LANDLOCK_CREATE_RULESET_VERSION,
        )
    };

    (abi > 0).then_some(abi)
}

// Sets or clears the close-on-exec (FD_CLOEXEC) flag on a file descriptor.
pub(crate) fn set_cloexec<Fd: AsFd>(fd: Fd, state: bool) -> Result<(), Errno> {
    let flags = fcntl(&fd, FcntlArg::F_GETFD)?;

    let mut new_flags = flags;
    if state {
        new_flags |= FdFlag::FD_CLOEXEC.bits();
    } else {
        new_flags &= !FdFlag::FD_CLOEXEC.bits();
    }

    fcntl(
        &fd,
        FcntlArg::F_SETFD(FdFlag::from_bits_truncate(new_flags)),
    )
    .map(drop)
}

// Retries a closure on `EINTR` error.
//
// This function will call the provided closure, and if the closure
// returns `EINTR` error, it will retry the operation until it succeeds
// or fails with a different error.
#[cfg(feature = "force")]
pub(crate) fn retry_on_eintr<F, T>(mut f: F) -> Result<T, Errno>
where
    F: FnMut() -> Result<T, Errno>,
{
    loop {
        match f() {
            Err(Errno::EINTR) => continue,
            result => return result,
        }
    }
}

// Retries a closure on `EAGAIN` and `EINTR` errors.
//
// This function will call the provided closure, and if the closure
// returns `EAGAIN` or `EINTR` error, it will retry the operation until it
// succeeds or fails with a different error.
pub(crate) fn retry_on_intr<F, T>(mut f: F) -> Result<T, Errno>
where
    F: FnMut() -> Result<T, Errno>,
{
    loop {
        match f() {
            Err(Errno::EAGAIN | Errno::EINTR) => continue,
            result => return result,
        }
    }
}

// Allocate a Vec from a slice using fallible allocation.
#[cfg(feature = "force")]
pub(crate) fn try_to_vec(data: &[u8]) -> Result<Vec<u8>, Errno> {
    let mut buf = Vec::new();
    buf.try_reserve_exact(data.len()).or(Err(Errno::ENOMEM))?;
    buf.extend_from_slice(data);
    Ok(buf)
}
