//
// Syd: rock-solid application kernel
// src/xattr.rs: Extended attribute utilities
//
// Copyright (c) 2025 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

use std::{
    ffi::CStr,
    os::fd::{AsFd, AsRawFd},
};

use libc::{c_char, c_int, c_void, size_t};
use memchr::{arch::all::is_prefix, memchr};
use nix::{errno::Errno, NixPath};

use crate::config::MMAP_MIN_ADDR;

/// Get an extended attribute value.
pub fn fgetxattr<Fd: AsFd, P: ?Sized + NixPath>(
    fd: Fd,
    name: &P,
    value: Option<&mut [u8]>,
) -> Result<usize, Errno> {
    let (value, len) = match value {
        Some(v) => (v.as_mut_ptr() as *mut c_void, v.len() as size_t),
        None => (std::ptr::null_mut(), 0),
    };

    // SAFETY: nix lacks a wrapper for fgetxattr.
    let res = name.with_nix_path(|name_ptr| unsafe {
        libc::fgetxattr(fd.as_fd().as_raw_fd(), name_ptr.as_ptr(), value, len)
    })?;

    #[expect(clippy::cast_sign_loss)]
    Errno::result(res).map(|res| res as usize)
}

/// Set an extended attribute value.
pub fn fsetxattr<Fd: AsFd, P: ?Sized + NixPath>(
    fd: Fd,
    name: &P,
    value: &[u8],
    flags: i32,
) -> Result<(), Errno> {
    // SAFETY: nix lacks a wrapper for fsetxattr.
    let res = name.with_nix_path(|name_ptr| unsafe {
        libc::fsetxattr(
            fd.as_fd().as_raw_fd(),
            name_ptr.as_ptr(),
            value.as_ptr() as *const c_void,
            value.len() as size_t,
            flags as c_int,
        )
    })?;

    Errno::result(res).map(drop)
}

/// Remove an extended attribute value.
pub fn fremovexattr<Fd: AsFd, P: ?Sized + NixPath>(fd: Fd, name: &P) -> Result<(), Errno> {
    // SAFETY: nix lacks a wrapper for fremovexattr.
    let res = name.with_nix_path(|name_ptr| unsafe {
        libc::fremovexattr(fd.as_fd().as_raw_fd(), name_ptr.as_ptr())
    })?;

    Errno::result(res).map(drop)
}

const SEC_XATTR: &[u8] = b"security.";
const SYD_XATTR: &[u8] = b"user.syd.";
const TRU_XATTR: &[u8] = b"trusted.";
const XATTR_SEC: &[&[u8]] = &[SEC_XATTR, SYD_XATTR, TRU_XATTR];

/// Deny access to the following extended attribute prefixes:
///
/// 1. security.*
/// 2. trusted.*
/// 3. user.syd.*
///
/// # Safety
///
/// Dereferences name after a NULL check.
/// If name is not NULL, it must be a valid NUL-terminated C-String.
///
/// # Security
///
/// Denies with ENODATA for stealth.
pub unsafe fn denyxattr(name: *const c_char) -> Result<(), Errno> {
    if name.is_null() {
        return Ok(());
    }
    if (name as u64) < *MMAP_MIN_ADDR {
        return Err(Errno::EFAULT);
    }

    // SAFETY: The pointer from CStr is guaranteed
    // to be valid and null-terminated.
    let name = CStr::from_ptr(name);
    let name = name.to_bytes();

    for prefix in XATTR_SEC {
        if is_prefix(name, prefix) {
            return Err(Errno::ENODATA);
        }
    }

    Ok(())
}

/// Filters out the following extended attribute prefixes:
///
/// 1. security.*
/// 2. trusted.*
/// 3. user.syd.*
///
/// # Arguments
///
/// * `buf` - A buffer containing the extended attribute names as
///   null-terminated strings.
/// * `n` - The length of valid data in the buffer.
pub fn filterxattr(buf: &[u8], n: usize) -> Result<Vec<u8>, Errno> {
    let mut soff = 0;
    let mut fbuf = Vec::new();
    while soff < n {
        let end = if let Some(end) = memchr(0, &buf[soff..]) {
            end
        } else {
            break;
        };

        // Add +1 to include the NUL byte.
        let eoff = soff
            .checked_add(end)
            .ok_or(Errno::EOVERFLOW)?
            .checked_add(1)
            .ok_or(Errno::EOVERFLOW)?;
        let name = &buf[soff..eoff];

        // SAFETY: memchr check above guarantees:
        // 1. The slice is nul-terminated.
        // 2. The slice has no interior nul bytes.
        let cstr = unsafe { CStr::from_bytes_with_nul_unchecked(name) };
        let cstr = cstr.to_bytes();

        let mut filter = false;
        for prefix in XATTR_SEC {
            if is_prefix(cstr, prefix) {
                filter = true;
                break;
            }
        }

        if !filter {
            fbuf.try_reserve(name.len()).or(Err(Errno::ENOMEM))?;
            fbuf.extend_from_slice(name);
        }

        soff = eoff;
    }

    Ok(fbuf)
}
