//
// Syd: rock-solid application kernel
// src/fs.rs: Filesystem utilities
//
// Copyright (c) 2023, 2024, 2025 Ali Polatel <alip@chesswob.org>
// Based in part upon uutils coreutils package's src/lib/features/fs.rs which is:
//   (c) Joseph Crail <jbcrail@gmail.com>
//   (c) Jian Zeng <anonymousknight96 AT gmail.com>
// Tests base based in part upon gnulib packages' tests/test-canonicalize.c which is:
//   (c) Free Software Foundation, Inc.
// {chdir,getdir}_long() functions are based in part upon zsh/Src/compat.c which is:
//   (c) 1992-1997 Paul Falstad
//   SPDX-License-Identifier: ZSH
//
// SPDX-License-Identifier: GPL-3.0

//! Set of functions to manage files and symlinks

use std::{
    fs::{metadata, set_permissions, File},
    io::{Read, Seek, SeekFrom, Write},
    os::{
        fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd, RawFd},
        unix::fs::PermissionsExt,
    },
    path::Path,
    sync::LazyLock,
};

use btoi::btoi;
use libc::{
    c_char, c_int, c_long, c_uint, c_ulong, c_void, clone, off64_t, seccomp_notif,
    seccomp_notif_addfd, seccomp_notif_resp, siginfo_t, syscall, SYS_close_range, SYS_execveat,
    SYS_faccessat2, SYS_ioctl, SYS_kcmp, SYS_pidfd_getfd, SYS_pidfd_open, SYS_pidfd_send_signal,
    SYS_tgkill, CLONE_PIDFD, EBADF, O_NONBLOCK, _IO, _IOR, _IOW, _IOWR,
};
use libseccomp::{ScmpFilterContext, ScmpSyscall};
use nix::{
    errno::Errno,
    fcntl::{
        fcntl, openat2, AtFlags, FallocateFlags, FcntlArg, FdFlag, OFlag, OpenHow, ResolveFlag,
        SealFlag, AT_FDCWD,
    },
    pty::PtyMaster,
    sched::{CloneCb, CloneFlags},
    sys::{
        signal::{SigSet, Signal},
        socket::{
            getsockopt,
            sockopt::{PeerCredentials, ReceiveTimeout, SendTimeout},
            UnixCredentials,
        },
        stat::Mode,
        time::TimeSpec,
    },
    unistd::{read, write, AccessFlags, Pid},
    NixPath,
};

use crate::{
    compat::{
        fstatfs64, fstatx, getdents64, statx, AddWatchFlags, STATX_BASIC_STATS, STATX_INO,
        STATX_SIZE,
    },
    config::*,
    confine::CLONE_NEWTIME,
    cookie::{
        safe_socket, SECCOMP_IOCTL_NOTIF_ADDFD_COOKIE_ARG3, SECCOMP_IOCTL_NOTIF_ADDFD_COOKIE_ARG4,
        SECCOMP_IOCTL_NOTIF_ADDFD_COOKIE_ARG5, SECCOMP_IOCTL_NOTIF_SEND_COOKIE_ARG3,
        SECCOMP_IOCTL_NOTIF_SEND_COOKIE_ARG4, SECCOMP_IOCTL_NOTIF_SEND_COOKIE_ARG5,
    },
    err::err2no,
    hash::SydHashSet,
    ioctl::{TIOCEXCL, TIOCGEXCL, TIOCGPTPEER, TIOCNXCL},
    path::{XPath, XPathBuf, PATH_MAX},
    proc::{proc_tgid, PROCMAP_QUERY},
    retry::retry_on_eintr,
};

/// SAFETY: AT_BADFD to be used a safe alternative to AT_FDCWD.
pub const AT_BADFD: BorrowedFd<'static> = unsafe { BorrowedFd::borrow_raw(-EBADF) };

/// Seals the memfd for writing.
pub fn seal_memfd<Fd: AsFd>(fd: Fd) -> Result<(), Errno> {
    // Seal memory fd.
    fcntl(
        fd,
        FcntlArg::F_ADD_SEALS(
            SealFlag::F_SEAL_SEAL
                | SealFlag::F_SEAL_WRITE
                | SealFlag::F_SEAL_SHRINK
                | SealFlag::F_SEAL_GROW,
        ),
    )
    .map(drop)
}

/// Sets or clears the append (O_APPEND) flag on a file descriptor.
pub fn set_append<Fd: AsFd>(fd: Fd, state: bool) -> Result<(), Errno> {
    let flags = fcntl(&fd, FcntlArg::F_GETFL)?;

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

    fcntl(&fd, FcntlArg::F_SETFL(OFlag::from_bits_truncate(new_flags))).map(drop)
}

/// Returns `true` if the given file descriptor is set to non-blocking mode.
pub fn get_nonblock<Fd: AsFd>(fd: Fd) -> Result<bool, Errno> {
    fcntl(fd, FcntlArg::F_GETFL).map(|flags| flags & O_NONBLOCK != 0)
}

/// Sets or clears the non-blocking (O_NONBLOCK) flag on a file descriptor.
pub fn set_nonblock<Fd: AsFd>(fd: Fd, state: bool) -> Result<(), Errno> {
    let flags = fcntl(&fd, FcntlArg::F_GETFL)?;

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

    fcntl(&fd, FcntlArg::F_SETFL(OFlag::from_bits_truncate(new_flags))).map(drop)
}

/// Sets or clears the close-on-exec (FD_CLOEXEC) flag on a file descriptor.
pub 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)
}

/// Set pipe max size of the given pipe.
pub fn set_pipemax<Fd: AsFd>(fd: Fd, size: c_int) -> Result<usize, Errno> {
    #[expect(clippy::cast_sign_loss)]
    fcntl(fd, FcntlArg::F_SETPIPE_SZ(size)).map(|r| r as usize)
}

/// Get exclusive mode for the given terminal.
pub fn get_exclusive<Fd: AsFd>(fd: Fd) -> Result<bool, Errno> {
    let mut set: c_int = 0;
    let fd = fd.as_fd().as_raw_fd();
    let req = TIOCGEXCL.ok_or(Errno::ENOTTY)?;

    // SAFETY: TIOCGEXCL takes an int* to return 0 or nonzero.
    Errno::result(unsafe { syscall(SYS_ioctl, fd, req, std::ptr::addr_of_mut!(set)) })
        .map(|_| set != 0)
}

/// Set given terminal to exclusive mode, or disable exclusive mode.
pub fn set_exclusive<Fd: AsFd>(fd: Fd, enable: bool) -> Result<(), Errno> {
    let fd = fd.as_fd().as_raw_fd();
    let req = if enable { *TIOCEXCL } else { *TIOCNXCL }.ok_or(Errno::ENOTTY)?;

    // SAFETY: TIOC{E,N}XCL take no extra arguments.
    Errno::result(unsafe { syscall(SYS_ioctl, fd, req) }).map(drop)
}

/// Given the main PTY device returns a FD to the peer PTY.
///
/// This is safer than using open(2) on the return value of ptsname(3).
pub fn openpts<Fd: AsFd>(fd: Fd, flags: OFlag) -> Result<OwnedFd, Errno> {
    let fd = fd.as_fd().as_raw_fd();
    let flags = flags.bits();
    let req = TIOCGPTPEER.ok_or(Errno::ENOTTY)?;

    // SAFETY: In libc we trust.
    #[expect(clippy::cast_possible_truncation)]
    Errno::result(unsafe { syscall(SYS_ioctl, fd, req, flags) }).map(|fd| {
        // SAFETY: TIOCGPTPEER returns a valid fd on success.
        unsafe { OwnedFd::from_raw_fd(fd as RawFd) }
    })
}

/// Open the PTY device.
pub fn openpt(flags: OFlag) -> Result<PtyMaster, Errno> {
    // 1. This function is called early at startup before proc_init,
    //    so we cannot use safe_open with RESOLVE_BENEATH.
    // 2. `/dev/ptmx` may be a symbolic link to `/dev/pts/ptmx`,
    //    so we cannot use safe_open_abs with RESOLVE_NO_SYMLINKS.
    //    This is the case on Gentoo Linux.
    // 3. We cannot directly open `/dev/pts/ptmx` either,
    //    because we may not have sufficient permissions.
    //    This is the case on Arch Linux and Fedora Linux.
    let how = OpenHow::new()
        .flags(flags)
        .resolve(ResolveFlag::RESOLVE_NO_MAGICLINKS);
    #[expect(clippy::disallowed_methods)]
    let fd = retry_on_eintr(|| openat2(AT_BADFD, c"/dev/ptmx", how))?;

    // SAFETY: Validate what we've opened is indeed `/dev/ptmx`.
    // This guard against potential symlink issues.
    if !is_dev_ptmx(&fd).unwrap_or(false) {
        return Err(Errno::ENODEV);
    }

    // SAFETY: fd is a valid PTY device.
    Ok(unsafe { PtyMaster::from_owned_fd(fd) })
}

const KCMP_FILE: c_long = 0;

/// Check if the given file descriptor is open for the given process.
pub fn is_open_fd(pid: Pid, fd: RawFd) -> Result<bool, Errno> {
    #[expect(clippy::cast_lossless)]
    #[expect(clippy::cast_possible_wrap)]
    #[expect(clippy::cast_sign_loss)]
    // SAFETY: There's no libc wrapper for kcmp.
    match Errno::result(unsafe {
        syscall(
            SYS_kcmp,
            pid.as_raw() as c_long,
            pid.as_raw() as c_long,
            KCMP_FILE,
            fd as c_ulong as c_long,
            fd as c_ulong as c_long,
        )
    }) {
        Ok(_) => Ok(true),
        Err(Errno::EBADF) => Ok(false),
        Err(errno) => Err(errno),
    }
}

/// Check two fds point to the same open file description for the given processes.
pub fn is_same_fd(pid1: Pid, pid2: Pid, fd1: RawFd, fd2: RawFd) -> Result<bool, Errno> {
    if pid1 == pid2 && fd1 == fd2 {
        // We do not check for open/valid FD in this function,
        // so we short-circuit here for efficiency.
        return Ok(true);
    }

    // SAFETY: There's no libc wrapper for kcmp.
    #[expect(clippy::cast_lossless)]
    #[expect(clippy::cast_possible_wrap)]
    #[expect(clippy::cast_sign_loss)]
    Ok(Errno::result(unsafe {
        syscall(
            SYS_kcmp,
            pid1.as_raw() as c_long,
            pid2.as_raw() as c_long,
            KCMP_FILE,
            fd1 as c_ulong as c_long,
            fd2 as c_ulong as c_long,
        )
    })? == 0)
}

/// Check two processes share the same address space.
pub fn is_same_vm(pid1: Pid, pid2: Pid) -> Result<bool, Errno> {
    const KCMP_VM: u64 = 1;
    // SAFETY: There's no libc wrapper for kcmp.
    Ok(Errno::result(unsafe { syscall(SYS_kcmp, pid1.as_raw(), pid2.as_raw(), KCMP_VM) })? == 0)
}

/// Check if file resides on a hugetlbfs (e.g. memfds with MFD_HUGETLB)
pub fn is_huge_file<Fd: AsFd>(fd: Fd) -> Result<bool, Errno> {
    fstatfs64(fd.as_fd()).map(|st| st.is_huge_file())
}

/// Check if file resides inside procfs(5).
pub fn is_proc<Fd: AsFd>(fd: Fd) -> Result<bool, Errno> {
    fstatfs64(fd.as_fd()).map(|st| st.is_proc())
}

/// Check if file is the /dev/null character device.
pub fn is_dev_null<Fd: AsFd>(fd: Fd) -> Result<bool, Errno> {
    const NULL_MAJOR: u32 = 1;
    const NULL_MINOR: u32 = 3;
    is_char_dev(fd, NULL_MAJOR, NULL_MINOR)
}

/// Check if file is the AMD KFD character device (/dev/kfd).
pub fn is_dev_kfd<Fd: AsFd>(fd: Fd) -> Result<bool, Errno> {
    const KFD_MAJOR: u32 = 238;
    const KFD_MINOR: u32 = 0;
    is_char_dev(fd, KFD_MAJOR, KFD_MINOR)
}

/// Check if file is the /dev/ptmx character device.
pub fn is_dev_ptmx<Fd: AsFd>(fd: Fd) -> Result<bool, Errno> {
    const PTMX_MAJOR: u32 = 5;
    const PTMX_MINOR: u32 = 2;
    is_char_dev(fd, PTMX_MAJOR, PTMX_MINOR)
}

/// Check if file is a character device with the given major/minor numbers.
pub fn is_char_dev<Fd: AsFd>(fd: Fd, major: u32, minor: u32) -> Result<bool, Errno> {
    #[expect(clippy::cast_possible_truncation)]
    const S_IFCHR: u16 = libc::S_IFCHR as u16;

    let statx = fstatx(fd, STATX_BASIC_STATS)?;

    // Check if file is a character device,
    // and its device major/minor numbers
    // match the given parameters.
    Ok(statx.stx_mode & S_IFCHR == S_IFCHR
        && statx.stx_rdev_major == major
        && statx.stx_rdev_minor == minor)
}

/// Check if the given file is a regular file.
pub fn is_file<Fd: AsFd>(fd: Fd) -> Result<bool, Errno> {
    #[expect(clippy::cast_possible_truncation)]
    const S_IFREG: u16 = libc::S_IFREG as u16;

    let statx = fstatx(&fd, STATX_BASIC_STATS)?;

    Ok(statx.stx_mode & S_IFREG == S_IFREG)
}

/// Check if the given file is a regular empty file.
pub fn is_empty_file<Fd: AsFd>(fd: Fd) -> Result<bool, Errno> {
    #[expect(clippy::cast_possible_truncation)]
    const S_IFREG: u16 = libc::S_IFREG as u16;

    let statx = fstatx(&fd, STATX_BASIC_STATS)?;

    Ok(statx.stx_size == 0 && statx.stx_mode & S_IFREG == S_IFREG)
}

/// Checks if the given file descriptor has a send timeout set.
pub fn has_send_timeout<F: AsFd>(fd: &F) -> Result<bool, Errno> {
    let tv = getsockopt(fd, SendTimeout)?;
    Ok(tv.tv_sec() != 0 || tv.tv_usec() != 0)
}

/// Checks if the given file descriptor has a receive timeout set.
pub fn has_recv_timeout<F: AsFd>(fd: &F) -> Result<bool, Errno> {
    let tv = getsockopt(fd, ReceiveTimeout)?;
    Ok(tv.tv_sec() != 0 || tv.tv_usec() != 0)
}

/// Safe wrapper for inotify_add_watch.
pub fn inotify_add_watch<Fd: AsFd, P: ?Sized + NixPath>(
    fd: Fd,
    path: &P,
    mask: AddWatchFlags,
) -> Result<c_int, Errno> {
    // SAFETY: We need this because nix' `WatchDescriptor` is opaque...
    let res = path.with_nix_path(|cstr| unsafe {
        libc::inotify_add_watch(fd.as_fd().as_raw_fd(), cstr.as_ptr(), mask.bits())
    })?;

    Errno::result(res).map(|wd| wd as c_int)
}

/// Safe wrapper for fallocate64.
pub fn fallocate64<Fd: AsFd>(
    fd: Fd,
    mode: FallocateFlags,
    off: off64_t,
    len: off64_t,
) -> Result<(), Errno> {
    // SAFETY: nix does not have an interface for fallocate64 yet.
    Errno::result(unsafe { libc::fallocate64(fd.as_fd().as_raw_fd(), mode.bits(), off, len) })
        .map(drop)
}

/// Safe truncate64() wrapper.
pub fn truncate64<P: ?Sized + NixPath>(path: &P, len: off64_t) -> Result<(), Errno> {
    // SAFETY: nix does not have a truncate64 wrapper.
    Errno::result(path.with_nix_path(|cstr| unsafe { libc::truncate64(cstr.as_ptr(), len) })?)
        .map(drop)
}

/// Safe ftruncate64() wrapper.
pub fn ftruncate64<Fd: AsFd>(fd: Fd, len: off64_t) -> Result<(), Errno> {
    // SAFETY: nix does not have a ftruncate64 wrapper.
    Errno::result(unsafe { libc::ftruncate64(fd.as_fd().as_raw_fd(), len) }).map(drop)
}

// Description of one file extent.
#[repr(C)]
#[derive(Debug, Clone, Copy)]
struct fiemap_extent {
    // Byte offset of the extent in the file.
    fe_logical: u64,
    // Byte offset of the extent on disk.
    fe_physical: u64,
    // Length in bytes for this extent.
    fe_length: u64,
    _fe_reserved64: [u64; 2],
    // FIEMAP_EXTENT_* flags for this extent.
    fe_flags: u32,
    _fe_reserved: [u32; 3],
}

// File extent mappings.
//
// - `fm_start`: byte offset (inclusive) at which to start mapping (in)
// - `fm_length`: logical length of mapping which userspace wants (in)
// - `fm_flags`: FIEMAP_FLAG_* flags for request (in/out)
// - `fm_mapped_extents`: number of extents that were mapped (out)
// - `fm_extent_count`: size of `fm_extents` array (in)
// - `fm_reserved`: reserved
// - `fm_extents`: array of mapped extents (out)
#[repr(C)]
struct fiemap {
    fm_start: u64,
    fm_length: u64,
    fm_flags: u32,
    fm_mapped_extents: u32,
    fm_extent_count: u32,
    _fm_reserved: u32,
    // Flexible array of extents; actual length is `fm_extent_count`.
    fm_extents: [fiemap_extent; 0],
}

/// FS_IOC_FIEMAP ioctl(2) request.
pub const FS_IOC_FIEMAP: c_ulong = _IOWR::<fiemap>(b'f' as u32, 11) as c_ulong;

/// FIGETBSZ ioctl(2) request.
pub const FIGETBSZ: c_ulong = _IO(0x00, 2) as c_ulong;

// Information for a single dedupe operation on a destination file.
#[repr(C)]
#[derive(Debug, Clone, Copy)]
struct file_dedupe_range_info {
    // Destination file descriptor.
    dest_fd: i64,
    // Start offset of the extent in the destination file.
    dest_offset: u64,
    // Number of bytes successfully deduped.
    bytes_deduped: u64,
    // Status of this dedupe operation:
    // < 0 for error,
    // == FILE_DEDUPE_RANGE_SAME if dedupe succeeds,
    // == FILE_DEDUPE_RANGE_DIFFERS if data differs.
    status: i32,
    // Must be zero.
    reserved: u32,
}

// Arguments for a range of dedupe operations from a source file.
//
// - `src_offset`: start of the extent in the source file (in)
// - `src_length`: length of the extent (in)
// - `dest_count`: number of elements in the `info` array (in)
// - `reserved1`: must be zero
// - `reserved2`: must be zero
// - `info`: array of `file_dedupe_range_info` of length `dest_count` (out)
#[repr(C)]
struct file_dedupe_range {
    src_offset: u64,
    src_length: u64,
    dest_count: u16,
    reserved1: u16,
    reserved2: u32,
    // Flexible array member of length `dest_count`.
    info: [file_dedupe_range_info; 0],
}

/// FIDEDUPERANGE ioctl(2) request.
pub const FIDEDUPERANGE: c_ulong = _IOWR::<file_dedupe_range>(0x94, 54) as c_ulong;

// Filesystem UUID structure with fixed 16-byte buffer.
//
// - `len`: actual length of the UUID (≤16)
// - `uuid`: UUID bytes
#[repr(C)]
#[derive(Debug, Clone, Copy)]
struct fsuuid2 {
    len: u8,
    uuid: [u8; 16],
}

/// FS_IOC_GETFSUUID ioctl(2) request.
pub const FS_IOC_GETFSUUID: c_ulong = _IOR::<fsuuid2>(0x15, 0) as c_ulong;

// Filesystem sysfs path structure.
//
// - `len`: length of the returned name (≤128)
// - `name`: NUL-terminated path component under `/sys/fs/` or `/sys/kernel/debug/`
#[repr(C)]
#[derive(Debug, Clone, Copy)]
struct fs_sysfs_path {
    len: u8,
    name: [u8; 128],
}

/// FS_IOC_GETFSSYSFSPATH ioctl(2) request.
pub const FS_IOC_GETFSSYSFSPATH: c_ulong = _IOR::<fs_sysfs_path>(0x15, 1) as c_ulong;

/// FIBMAP ioctl(2) request.
pub const FIBMAP: c_ulong = _IO(0x00, 1) as c_ulong;

/// KDSETKEYCODE ioctl(2) request.
pub const KDSETKEYCODE: c_ulong = 0x4B4D;

/// KDSIGACCEPT ioctl(2) request.
pub const KDSIGACCEPT: c_ulong = 0x4B4E;

// File system extended attribute operations.
//
// Used with `FS_IOC_FSGETXATTR` and `FS_IOC_FSSETXATTR`.
#[repr(C)]
#[derive(Debug, Clone, Copy)]
struct fsxattr {
    // xflags field value (get/set)
    fsx_xflags: u32,
    // extsize field value (get/set)
    fsx_extsize: u32,
    // nextents field value (get)
    fsx_nextents: u32,
    // project identifier (get/set)
    fsx_projid: u32,
    // CoW extsize field value (get/set)
    fsx_cowextsize: u32,
    // Padding
    fsx_pad: [u8; 8],
}

/// FS_IOC_FSGETXATTR ioctl(2) request.
pub const FS_IOC_FSGETXATTR: c_ulong = _IOR::<fsxattr>(b'X' as u32, 31) as c_ulong;

/// FS_IOC_FSSETXATTR ioctl(2) request.
pub const FS_IOC_FSSETXATTR: c_ulong = _IOW::<fsxattr>(b'X' as u32, 32) as c_ulong;

/// FS_IOC_SETFLAGS ioctl(2) request.
pub const FS_IOC_SETFLAGS: c_ulong = _IOW::<c_long>(b'f' as u32, 2) as c_ulong;

/*
 * Seccomp constants
 */

/// '!' magic number for seccomp ioctls.
pub const SECCOMP_IOCTL_MAGIC: u32 = b'!' as u32;

/// SECCOMP_IOCTL_NOTIF_RECV ioctl(2) request.
pub const SECCOMP_IOCTL_NOTIF_RECV: c_ulong =
    _IOWR::<seccomp_notif>(SECCOMP_IOCTL_MAGIC, 0) as c_ulong;

/// SECCOMP_IOCTL_NOTIF_SEND ioctl(2) request.
pub const SECCOMP_IOCTL_NOTIF_SEND: c_ulong =
    _IOWR::<seccomp_notif_resp>(SECCOMP_IOCTL_MAGIC, 1) as c_ulong;

/// SECCOMP_IOCTL_NOTIF_ID_VALID ioctl(2) request.
pub const SECCOMP_IOCTL_NOTIF_ID_VALID: c_ulong = _IOW::<u64>(SECCOMP_IOCTL_MAGIC, 2) as c_ulong;

/// SECCOMP_IOCTL_NOTIF_ADDFD ioctl(2) request.
pub const SECCOMP_IOCTL_NOTIF_ADDFD: c_ulong =
    _IOW::<seccomp_notif_addfd>(SECCOMP_IOCTL_MAGIC, 3) as c_ulong;

/// SECCOMP_IOCTL_NOTIF_SET_FLAGS ioctl(2) request.
pub const SECCOMP_IOCTL_NOTIF_SET_FLAGS: c_ulong = _IOW::<u64>(SECCOMP_IOCTL_MAGIC, 4) as c_ulong;

pub(crate) const SECCOMP_IOCTL_NOTIF_LIST: &[c_ulong] = &[
    SECCOMP_IOCTL_NOTIF_RECV,
    SECCOMP_IOCTL_NOTIF_SEND,
    SECCOMP_IOCTL_NOTIF_ID_VALID,
    SECCOMP_IOCTL_NOTIF_ADDFD,
    SECCOMP_IOCTL_NOTIF_SET_FLAGS,
];

/// Flag to set synchronous mode for the seccomp notify fd.
pub(crate) const SECCOMP_USER_NOTIF_FD_SYNC_WAKE_UP: u32 = 1;

/// Export a seccomp filter in pretty-printed PFC.
///
/// Mostly equivalent to _seccomp_export_pfc(3) with
/// some convenience replacements for seccomp constants,
/// and returns a String rather than a FD.
///
/// The temporary file is created in `/tmp` directory
/// with O_TMPFILE|O_EXCL flags and empty/zero Mode.
pub(crate) fn seccomp_export_pfc(ctx: &ScmpFilterContext) -> Result<String, Errno> {
    // SAFETY: This runs at startup before confinement!
    #[expect(clippy::disallowed_methods)]
    let mut file = nix::fcntl::openat(
        AT_FDCWD,
        "/tmp",
        OFlag::O_TMPFILE | OFlag::O_EXCL | OFlag::O_RDWR,
        Mode::empty(),
    )
    .map(File::from)?;
    ctx.export_pfc(&mut file).or(Err(Errno::EFAULT))?;

    file.seek(SeekFrom::Start(0)).map_err(|err| err2no(&err))?;
    let mut buf = Vec::new();
    file.read_to_end(&mut buf).map_err(|err| err2no(&err))?;

    // from_utf8_lossy_to_owned() is nightly...
    let mut pfc = String::from_utf8_lossy(&buf).into_owned();
    for &(from, to) in &[
        ("0x7fc00000", "NOTIFY"),
        (
            &format!("{SECCOMP_IOCTL_NOTIF_RECV}"),
            "SECCOMP_IOCTL_NOTIF_RECV",
        ),
        (
            &format!("{SECCOMP_IOCTL_NOTIF_SEND}"),
            "SECCOMP_IOCTL_NOTIF_SEND",
        ),
        (
            &format!("{SECCOMP_IOCTL_NOTIF_ID_VALID}"),
            "SECCOMP_IOCTL_NOTIF_ID_VALID",
        ),
        (
            &format!("{SECCOMP_IOCTL_NOTIF_ADDFD}"),
            "SECCOMP_IOCTL_NOTIF_ADDFD",
        ),
        (
            &format!("{SECCOMP_IOCTL_NOTIF_SET_FLAGS}"),
            "SECCOMP_IOCTL_NOTIF_SET_FLAGS",
        ),
        (&format!("{PROCMAP_QUERY}"), "PROCMAP_QUERY"),
    ] {
        pfc = pfc.replace(from, to);
    }

    Ok(pfc)
}

/// Set seccomp notify fd flags, useful to set synchronous mode.
pub(crate) fn seccomp_notify_set_flags(fd: RawFd, flags: u32) -> Result<(), Errno> {
    if !*HAVE_SECCOMP_USER_NOTIF_FD_SYNC_WAKE_UP {
        return Err(Errno::ENOSYS);
    }

    retry_on_eintr(|| {
        // SAFETY: In libc we trust.
        Errno::result(unsafe {
            syscall(
                SYS_ioctl,
                fd,
                SECCOMP_IOCTL_NOTIF_SET_FLAGS as c_ulong,
                flags,
            )
        })
    })
    .map(drop)
}

/// Wrapper for SECCOMP_IOCTL_NOTIF_ID_VALID ioctl(2),
/// aka _seccomp_notify_id_valid_(3) of libseccomp.
pub(crate) fn seccomp_notify_id_valid(fd: RawFd, id: u64) -> Result<(), Errno> {
    retry_on_eintr(|| {
        // SAFETY: Validate request ID against the seccomp-notify fd:
        // 1. This function is a hot path where we don't want to run
        //    notify_supported() on each call.
        // 2. We want to reliably handle EAGAIN and EINTR.
        // 3. ENOENT means child died mid-way.
        // libseccomp::notify_id_valid(fd, id).is_ok().
        Errno::result(unsafe {
            syscall(SYS_ioctl, fd, SECCOMP_IOCTL_NOTIF_ID_VALID as c_ulong, &id)
        })
    })
    .map(drop)
}

/// Wrapper for SECCOMP_IOCTL_NOTIF_SEND ioctl(2),
/// aka _seccomp_notify_respond_(3) of libseccomp.
pub(crate) fn seccomp_notify_respond(
    fd: RawFd,
    response: *const seccomp_notif_resp,
) -> Result<(), Errno> {
    retry_on_eintr(|| {
        // SAFETY:
        // 1. libseccomp's version allocates needlessly, and
        // 2. libseccomp-sys's version requires a mutable pointer, and
        // 3. libseccomp does not export EINTR to user API, but returns EFAULT as catch-all:
        //    https://github.com/seccomp/libseccomp/blob/5491c4b931431bec489dd78247ef675fc1b49797/src/api.c#L92-L95
        //    So we use _ioctl_(2) directly.
        // 4. EINTR may mean `syd_int` thread misfired us.
        // 5. ENOENT means child-died mid-way.
        // 6. Ok() is all good!
        // 7. We protect SECCOMP_IOCTL_NOTIF_SEND with system call argument cookies,
        //    to raise the bar against an attacker who has compromised Syd and aims
        //    to inject the flag SECCOMP_USER_NOTIF_FLAG_CONTINUE to this response
        //    in order to pass-through a system call to the host Linux kernel.
        // 8. Randomizing the seccomp-fd at startup is another mitigation against this.
        Errno::result(unsafe {
            syscall(
                SYS_ioctl,
                fd,
                SECCOMP_IOCTL_NOTIF_SEND as c_ulong,
                response,
                *SECCOMP_IOCTL_NOTIF_SEND_COOKIE_ARG3,
                *SECCOMP_IOCTL_NOTIF_SEND_COOKIE_ARG4,
                *SECCOMP_IOCTL_NOTIF_SEND_COOKIE_ARG5,
            )
        })
    })
    .map(drop)
}

/// Wrapper for SECCOMP_IOCTL_NOTIF_ADDFD ioctl(2).
pub(crate) fn seccomp_notify_addfd(
    fd: RawFd,
    addfd: *const seccomp_notif_addfd,
) -> Result<RawFd, Errno> {
    #[expect(clippy::cast_possible_truncation)]
    retry_on_eintr(|| {
        // SAFETY:
        // 1. libseccomp has no wrapper for ADDFD yet, and
        // 2. libseccomp does not export EINTR to user API, but returns EFAULT as catch-all:
        //    https://github.com/seccomp/libseccomp/blob/5491c4b931431bec489dd78247ef675fc1b49797/src/api.c#L92-L95
        //    So we use _ioctl_(2) directly.
        // 3. EINTR may mean `syd_int` thread misfired us.
        // 4. ENOENT means child-died mid-way.
        // 5. Ok() is all good!
        // 6. We protect SECCOMP_IOCTL_NOTIF_ADDFD with system call argument cookies,
        //    to raise the bar against an attacker who has compromised Syd and aims
        //    steal file descriptors.
        // 7. Randomizing the seccomp-fd at startup is another mitigation against this.
        Errno::result(unsafe {
            syscall(
                SYS_ioctl,
                fd,
                SECCOMP_IOCTL_NOTIF_ADDFD as c_ulong,
                addfd,
                *SECCOMP_IOCTL_NOTIF_ADDFD_COOKIE_ARG3,
                *SECCOMP_IOCTL_NOTIF_ADDFD_COOKIE_ARG4,
                *SECCOMP_IOCTL_NOTIF_ADDFD_COOKIE_ARG5,
            )
        })
    })
    .map(|fd| fd as RawFd)
}

/// Returns true if the given file descriptor is active.
pub fn is_active_fd<Fd: AsFd>(fd: Fd) -> bool {
    fcntl(fd, FcntlArg::F_GETFD).is_ok()
}

/// Returns true if the given file descriptor is syntactically valid.
///
/// Negative values, including AT_FDCWD, are not syntactically valid.
pub fn is_valid_fd(fd: u64) -> bool {
    to_valid_fd(fd).map(|fd| fd >= 0).unwrap_or(false)
}

/// Converts a system call argument to a RawFd.
///
/// Negative values, excluding AT_FDCWD, return an error.
#[expect(clippy::cast_possible_truncation)]
pub fn to_valid_fd(fd: u64) -> Result<RawFd, Errno> {
    let fd = fd as RawFd;

    if fd == libc::AT_FDCWD || fd >= 0 {
        Ok(fd)
    } else {
        Err(Errno::EBADF)
    }
}

/// Returns file access mode in status flags.
pub fn fd_status_flags<Fd: AsFd>(fd: Fd) -> Result<OFlag, Errno> {
    fcntl(fd, FcntlArg::F_GETFL).map(OFlag::from_bits_truncate)
}

/// Returns true if file is writable.
pub fn is_writable_fd<Fd: AsFd>(fd: Fd) -> Result<bool, Errno> {
    fd_status_flags(fd)
        .map(oflag_accmode)
        .map(|mode| !mode.is_empty())
}

/// Returns the access mode from the given `OFlag`.
pub fn oflag_accmode(flags: OFlag) -> OFlag {
    // 1. glibc does not include O_PATH to O_ACCMODE.
    // 2. musl defines O_PATH equal to O_EXEC and O_SEARCH,
    //    and O_ACCMODE is defined as O_ACCMODE|O_SEARCH.
    // Here we force the second behaviour by explicitly
    // adding O_PATH into O_ACCMODE. This works on both libcs.
    // See: https://www.openwall.com/lists/musl/2013/02/22/1
    flags & (OFlag::O_ACCMODE | OFlag::O_PATH)
}

/// Returns true if the open flags refer to nonblocking i/o.
pub fn oflag_nonblock(flags: OFlag) -> bool {
    !(flags & (OFlag::O_NONBLOCK | OFlag::O_NDELAY)).is_empty()
}

/// Get number of open file descriptors.
pub fn fd_count(pid: Option<Pid>) -> Result<u64, Errno> {
    let mut pfd = XPathBuf::from("/proc");
    if let Some(pid) = pid {
        pfd.push_pid(pid);
    } else {
        pfd.push(b"thread-self");
    }
    pfd.push(b"fd");

    if *HAVE_PROC_PID_FD_STAT_SIZE {
        let stx = statx(AT_BADFD, &pfd, 0, STATX_SIZE)?;
        return Ok(stx.stx_size);
    }

    #[expect(clippy::disallowed_methods)]
    let fd = nix::fcntl::openat(
        AT_BADFD,
        &pfd,
        OFlag::O_RDONLY | OFlag::O_DIRECTORY | OFlag::O_CLOEXEC,
        Mode::empty(),
    )?;
    let mut nfds: u64 = 0;
    loop {
        match getdents64(&fd, DIRENT_BUF_SIZE) {
            Ok(entries) => {
                nfds = nfds
                    .checked_add(entries.count() as u64)
                    .ok_or(Errno::ERANGE)?
            }
            Err(Errno::ECANCELED) => break, // EOF or empty directory.
            Err(errno) => return Err(errno),
        };
    }

    Ok(nfds.saturating_sub(2))
}

/// A safe version of clone that returns a PidFD,
/// and therefore is not subject to PID-recycling
/// races.
pub fn safe_clone(
    mut cb: CloneCb,
    stack: &mut [u8],
    flags: c_int,
    signal: Option<c_int>,
) -> Result<OwnedFd, Errno> {
    #[expect(clippy::cast_possible_truncation)]
    extern "C" fn callback(data: *mut CloneCb) -> c_int {
        // SAFETY: nix' version does not support CLONE_PIDFD.
        let cb: &mut CloneCb = unsafe { &mut *data };
        (*cb)() as c_int
    }

    let mut pid_fd: c_int = -1;
    let combined: c_int = flags | CLONE_PIDFD | signal.unwrap_or(0);
    // SAFETY: ditto.
    #[expect(clippy::missing_transmute_annotations)]
    let res = unsafe {
        let ptr = stack.as_mut_ptr().add(stack.len());
        let ptr_aligned = ptr.sub(ptr as usize % 16);
        clone(
            std::mem::transmute(callback as extern "C" fn(*mut Box<dyn FnMut() -> isize>) -> i32),
            ptr_aligned as *mut c_void,
            combined,
            std::ptr::addr_of_mut!(cb) as *mut c_void,
            &mut pid_fd,
        )
    };

    Errno::result(res).map(|_| {
        // SAFETY: clone with CLONE_PIDFD returns a valid FD.
        unsafe { OwnedFd::from_raw_fd(pid_fd) }
    })
}

/// Safe wrapper for faccessat2(2) with AT_EMPTY_PATH.
pub fn fdaccess<Fd: AsFd>(fd: Fd, mode: AccessFlags, mut flags: AtFlags) -> Result<(), Errno> {
    // Remove AT_SYMLINK_NOFOLLOW and add AT_EMPTY_PATH to flags.
    flags.remove(AtFlags::AT_SYMLINK_NOFOLLOW);
    flags.insert(AtFlags::AT_EMPTY_PATH);

    // SAFETY: No libc wrapper for faccessat2 yet.
    Errno::result(unsafe {
        syscall(
            SYS_faccessat2,
            fd.as_fd().as_raw_fd(),
            c"".as_ptr(),
            mode.bits(),
            flags.bits(),
        )
    })
    .map(drop)
}

// execveat(2): Only perform a check if execution would be allowed.
// Requires Linux>=6.14.
pub(crate) const AT_EXECVE_CHECK: AtFlags = AtFlags::from_bits_retain(0x10000);

/// Return true if the given File is executable.
pub fn is_executable<Fd: AsFd>(file: Fd) -> bool {
    check_executable(file).is_ok()
}

/// Check if the given File is executable.
pub fn check_executable<Fd: AsFd>(file: Fd) -> Result<(), Errno> {
    if *HAVE_AT_EXECVE_CHECK {
        let argv: [*const c_char; 2] = [c"".as_ptr(), std::ptr::null()];
        let envp: [*const c_char; 1] = [std::ptr::null()];
        // SAFETY: In libc we trust.
        Errno::result(unsafe {
            syscall(
                SYS_execveat,
                file.as_fd().as_raw_fd(),
                c"".as_ptr(),
                argv.as_ptr(),
                envp.as_ptr(),
                (AT_EXECVE_CHECK | AtFlags::AT_EMPTY_PATH).bits(),
            )
        })
        .map(drop)
    } else {
        fdaccess(file, AccessFlags::X_OK, AtFlags::AT_EACCESS)
    }
}

/// PIDFD_THREAD flag for pidfd_open(2).
#[expect(clippy::cast_sign_loss)]
pub const PIDFD_THREAD: u32 = OFlag::O_EXCL.bits() as u32;

/// Safe wrapper for pidfd_open(2).
///
/// This function requires Linux 5.3+.
pub fn pidfd_open(pid: Pid, mut flags: u32) -> Result<OwnedFd, Errno> {
    // Use PIDFD_THREAD if available.
    // Pass-through PIDFD_NONBLOCK.
    let pid = if *HAVE_PIDFD_THREAD || flags & PIDFD_THREAD == 0 {
        pid
    } else {
        flags &= !PIDFD_THREAD;
        proc_tgid(pid)?
    };

    // SAFETY: libc does not have a pidfd_open(2) wrapper yet.
    #[expect(clippy::cast_possible_truncation)]
    Errno::result(unsafe { syscall(SYS_pidfd_open, pid.as_raw(), flags) }).map(|fd| {
        // SAFETY: pidfd_open(2) returned success, fd is valid.
        unsafe { OwnedFd::from_raw_fd(fd as RawFd) }
    })
}

/// Safe wrapper for pidfd_getfd(2).
///
/// This function requires Linux 5.6+.
pub fn pidfd_getfd<Fd: AsFd>(pid_fd: Fd, remote_fd: RawFd) -> Result<OwnedFd, Errno> {
    // SAFETY: libc does not have a pidfd_getfd(2) wrapper yet.
    #[expect(clippy::cast_possible_truncation)]
    Errno::result(unsafe { syscall(SYS_pidfd_getfd, pid_fd.as_fd().as_raw_fd(), remote_fd, 0) })
        .map(|fd| {
            // SAFETY: pidfd_getfd(2) returned success, fd is valid.
            unsafe { OwnedFd::from_raw_fd(fd as RawFd) }
        })
}

/// Safe wrapper for pidfd_send_signal(2).
///
/// This function requires Linux 5.1+.
pub fn pidfd_send_signal<Fd: AsFd>(pid_fd: Fd, sig: i32) -> Result<(), Errno> {
    // SAFETY: libc does not have a wrapper for pidfd_send_signal yet.
    Errno::result(unsafe { syscall(SYS_pidfd_send_signal, pid_fd.as_fd().as_raw_fd(), sig, 0, 0) })
        .map(drop)
}

/// Safe wrapper for pidfd_send_signal(2) with signal 0.
///
/// This function requires Linux 5.1+.
pub fn pidfd_is_alive<Fd: AsFd>(pid_fd: Fd) -> Result<(), Errno> {
    pidfd_send_signal(pid_fd, 0)
}

/// libc may not define process_mrelease yet (e.g. musl on riscv64).
static SYS_PROCESS_MRELEASE: LazyLock<Option<c_long>> = LazyLock::new(|| {
    match ScmpSyscall::from_name("process_mrelease")
        .map(i32::from)
        .map(c_long::from)
        .ok()
    {
        Some(n) if n < 0 => None,
        Some(n) => Some(n),
        None => None,
    }
});

/// Safe wrapper for process_mrelease(2).
///
/// This function requires Linux 5.15+.
pub fn process_mrelease<Fd: AsFd>(pid_fd: Fd) -> Result<(), Errno> {
    let sysnum = SYS_PROCESS_MRELEASE.ok_or(Errno::ENOSYS)?;

    // SAFETY:
    // 1. libc does not have a wrapper for process_mrelease yet.
    // 2. libc may not define SYS_process_mrelease yet.
    Errno::result(unsafe { syscall(sysnum, pid_fd.as_fd().as_raw_fd(), 0) }).map(drop)
}

/// Safe wrapper for tgkill(2).
pub fn tgkill(tgid: Pid, tid: Pid, sig: i32) -> Result<(), Errno> {
    // SAFETY: There's no libc wrapper for tgkill.
    Errno::result(unsafe { syscall(SYS_tgkill, tgid.as_raw(), tid.as_raw(), sig) }).map(drop)
}

/// Safe wrapper for sigwaitinfo(2).
pub fn sigwaitinfo(set: &SigSet, info: Option<&mut siginfo_t>) -> Result<i32, Errno> {
    let info = info.map(|si| si as *mut _).unwrap_or(std::ptr::null_mut());

    // SAFETY: In libc we trust.
    Errno::result(unsafe { libc::sigwaitinfo(set.as_ref(), info) })
}

/// Safe wrapper for sigtimedwait(2).
pub fn sigtimedwait(
    set: &SigSet,
    info: Option<&mut siginfo_t>,
    timeout: TimeSpec,
) -> Result<i32, Errno> {
    let info = info.map(|si| si as *mut _).unwrap_or(std::ptr::null_mut());

    // SAFETY: In libc we trust.
    Errno::result(unsafe { libc::sigtimedwait(set.as_ref(), info, timeout.as_ref()) })
}

/// Convenience wrapper for sigtimedwait with zero TimeSpec.
pub fn sigtimedpoll(set: &SigSet, info: Option<&mut siginfo_t>) -> Result<i32, Errno> {
    sigtimedwait(set, info, TimeSpec::new(0, 0))
}

/// Convenience wrapper to block a single Signal.
pub fn block_signal(sig: Signal) -> Result<(), Errno> {
    let mut mask = SigSet::empty();
    mask.add(sig);
    mask.thread_block()
}

/// Convenience wrapper to unblock a single Signal.
pub fn unblock_signal(sig: Signal) -> Result<(), Errno> {
    let mut mask = SigSet::empty();
    mask.add(sig);
    mask.thread_unblock()
}

/// Safe wrapper for close_range(2).
pub fn close_range(first: c_uint, last: c_uint, flags: c_uint) -> Result<(), Errno> {
    // SAFETY: nix does not have a close_range wrapper yet.
    Errno::result(unsafe { syscall(SYS_close_range, first, last, flags) }).map(drop)
}

/// Close all file descriptors >= `fd`, equivalent to BSD's closefrom(2).
///
/// # Errors
///
/// Propagates any error returned by `close_range`.
pub fn closefrom(fd: c_uint) -> Result<(), Errno> {
    close_range(fd, RawFd::MAX as c_uint, 0)
}

/// Close all file descriptors except those in `exceptions`.
///
/// `exceptions` must be sorted ascending and contain no duplicates;
/// otherwise returns `Err(Errno::EINVAL)`.
///
/// Uses `close_range(2)` under the hood to efficiently close the
/// non-exempt descriptors.
///
/// # Errors
///
/// Returns on the first syscall error encountered, or
/// `Err(Errno::EINVAL)` if `exceptions` is not strictly ascending.
pub fn closeexcept(exceptions: &[c_uint]) -> Result<(), Errno> {
    // Validate that `exceptions` is strictly ascending and unique.
    if exceptions.windows(2).any(|w| w[0] >= w[1]) {
        return Err(Errno::EINVAL);
    }

    // If no exceptions, close everything.
    if exceptions.is_empty() {
        return closefrom(0);
    }

    // Use a wider integer for range computations to avoid overflow.
    let mut next: u64 = 0;

    #[expect(clippy::arithmetic_side_effects)]
    #[expect(clippy::cast_possible_truncation)]
    for &ex_fd in exceptions {
        let ex_fd = u64::from(ex_fd);

        // Close [next .. ex_fd - 1], if non-empty.
        if next < ex_fd {
            let first = next as c_uint;
            // Safe: ex_fd >= next + 1 ensures no underflow.
            let last = (ex_fd - 1) as c_uint;
            close_range(first, last, 0)?;
        }

        // Skip the exception itself.
        next = ex_fd.saturating_add(1);
    }

    // Finally close [next .. MAX_FD], if any remain.
    #[expect(clippy::cast_possible_truncation)]
    if next <= RawFd::MAX as u64 {
        let first = next as c_uint;
        closefrom(first)?;
    }

    Ok(())
}

/// Get peer credentials for the given UNIX socket.
pub fn peer_cred<Fd: AsFd>(fd: Fd) -> Result<UnixCredentials, Errno> {
    getsockopt(&fd, PeerCredentials)
}

/// Netlink alignment helper: nlmsg_align.
#[expect(clippy::arithmetic_side_effects)]
pub fn nlmsg_align(v: usize) -> usize {
    (v + 3) & !3usize
}

/// Netlink alignment helper: nla_align.
#[expect(clippy::arithmetic_side_effects)]
pub fn nla_align(v: usize) -> usize {
    (v + 3) & !3usize
}

// Constants:
// SOCK_DIAG_BY_FAMILY is 20 in the kernel uapi.
const SOCK_DIAG_BY_FAMILY: u16 = 20;

// Netlink special message types.
#[expect(clippy::cast_possible_truncation)]
const NLMSG_DONE: u16 = libc::NLMSG_DONE as u16;
#[expect(clippy::cast_possible_truncation)]
const NLMSG_ERROR: u16 = libc::NLMSG_ERROR as u16;

// nlmsghdr (16) + unix_diag_req (24) = 40 bytes.
const NL_HDR_LEN: usize = 16;
const UD_REQ_LEN: usize = 24;
#[expect(clippy::cast_possible_truncation)]
const NL_MSG_LEN: u32 = (NL_HDR_LEN + UD_REQ_LEN) as u32;

// udiag flags / attributes
const UNIX_DIAG_VFS: u16 = 1;
const UNIX_DIAG_PEER: u16 = 2;
const UDIAG_SHOW_VFS: u32 = 0x0000_0002;
const UDIAG_SHOW_PEER: u32 = 0x0000_0004;

/// Return the peer socket inode (low 32 bits zero-extended) for a UNIX-domain
/// socket endpoint `fd`. Uses NETLINK_SOCK_DIAG / unix diag and requests the
/// peer attribute. If peer socket inode is not available, returns local socket
/// inode as fallback.
#[expect(clippy::arithmetic_side_effects)]
#[expect(clippy::cast_possible_truncation)]
pub fn peer_inode<Fd: AsFd>(fd: Fd) -> Result<u64, Errno> {
    // Get local inode to filter diag results.
    let stx = fstatx(fd, STATX_INO)?;
    let local_ino = stx.stx_ino;
    let local_ino32 = (local_ino & 0xffff_ffff) as u32;

    // Open NETLINK_SOCK_DIAG socket.
    let nl = safe_socket(
        libc::AF_NETLINK,
        libc::SOCK_DGRAM | libc::SOCK_CLOEXEC,
        libc::NETLINK_SOCK_DIAG,
    )?;

    // Build request into a stack buffer.
    let mut req = [0u8; NL_HDR_LEN + UD_REQ_LEN];

    // Fill nlmsghdr.
    let mut p = 0usize;
    req[p..p + 4].copy_from_slice(&NL_MSG_LEN.to_ne_bytes()); // nlmsg_len
    p += 4;
    req[p..p + 2].copy_from_slice(&SOCK_DIAG_BY_FAMILY.to_ne_bytes()); // nlmsg_type
    p += 2;
    let nl_flags = (libc::NLM_F_REQUEST | libc::NLM_F_ROOT | libc::NLM_F_MATCH) as u16;
    req[p..p + 2].copy_from_slice(&nl_flags.to_ne_bytes()); // nlmsg_flags
    p += 2;
    req[p..p + 4].copy_from_slice(&1u32.to_ne_bytes()); // nlmsg_seq
    p += 4;
    req[p..p + 4].copy_from_slice(&0u32.to_ne_bytes()); // nlmsg_pid
    p += 4;

    // Fill unix_diag_req.
    req[p] = libc::AF_UNIX as u8;
    p += 1; // sdiag_family
    req[p] = 0u8;
    p += 1; // sdiag_protocol
    req[p..p + 2].copy_from_slice(&0u16.to_ne_bytes());
    p += 2; // pad
    req[p..p + 4].copy_from_slice(&u32::MAX.to_ne_bytes());
    p += 4; // udiag_states
    req[p..p + 4].copy_from_slice(&local_ino32.to_ne_bytes());
    p += 4; // udiag_ino
    req[p..p + 4].copy_from_slice(&UDIAG_SHOW_PEER.to_ne_bytes());
    p += 4; // udiag_show
    req[p..p + 4].copy_from_slice(&0u32.to_ne_bytes());
    p += 4; // cookie[0]
    req[p..p + 4].copy_from_slice(&0u32.to_ne_bytes());
    p += 4; // cookie[1]
    assert_eq!(p, req.len());

    // Send loop: Retry short writes until full message sent.
    let mut sent_total = 0usize;
    while sent_total < req.len() {
        let slice = &req[sent_total..];
        let sent = retry_on_eintr(|| write(&nl, slice))?;
        if sent == 0 {
            return Err(Errno::EIO);
        }
        sent_total = sent_total.saturating_add(sent);
    }

    // Recv loop: Parse netlink messages until we find UNIX_DIAG_PEER or finish.
    //
    // Quoting https://docs.kernel.org/userspace-api/netlink/intro.html
    // Netlink expects that the user buffer will be at least 8kB or a page size
    // of the CPU architecture, whichever is bigger. Particular Netlink families
    // may, however, require a larger buffer. 32kB buffer is recommended for most
    // efficient handling of dumps (larger buffer fits more dumped objects and
    // therefore fewer recvmsg() calls are needed).
    let mut rbuf = [0u8; 0x8000];
    loop {
        let n = retry_on_eintr(|| read(&nl, &mut rbuf))?;
        if n == 0 {
            return Err(Errno::EIO);
        }
        let mut off = 0usize;
        while off + NL_HDR_LEN <= n {
            // Read nlmsg_len (u32) and nlmsg_type (u16) safely.
            let nlmsg_len = {
                let b: [u8; 4] = rbuf[off..off + 4].try_into().or(Err(Errno::EOVERFLOW))?;
                u32::from_ne_bytes(b) as usize
            };
            if nlmsg_len == 0 || off + nlmsg_len > n {
                return Err(Errno::EIO);
            }
            let nlmsg_type = {
                let b: [u8; 2] = rbuf[off + 4..off + 6]
                    .try_into()
                    .or(Err(Errno::EOVERFLOW))?;
                u16::from_ne_bytes(b)
            };

            if nlmsg_type == NLMSG_DONE {
                //
                // return Err(Errno::ENODATA);
                //
                // Best effort, return local inode.
                return Ok(local_ino);
            } else if nlmsg_type == NLMSG_ERROR {
                if nlmsg_len >= NL_HDR_LEN + 4 {
                    let err_b: [u8; 4] = rbuf[off + NL_HDR_LEN..off + NL_HDR_LEN + 4]
                        .try_into()
                        .or(Err(Errno::EOVERFLOW))?;
                    let nl_err = i32::from_ne_bytes(err_b);
                    // nlmsgerr.error is negative errno.
                    return Err(Errno::from_raw(-nl_err));
                } else {
                    return Err(Errno::EIO);
                }
            } else if nlmsg_type == SOCK_DIAG_BY_FAMILY {
                let payload_off = off + NL_HDR_LEN;
                let ud_min = 16usize;
                if payload_off + ud_min > off + nlmsg_len {
                    return Err(Errno::EIO);
                }
                // udiag_ino at payload_off + 4 (u32)
                let found_ino32 = {
                    let b: [u8; 4] = rbuf[payload_off + 4..payload_off + 8]
                        .try_into()
                        .or(Err(Errno::EOVERFLOW))?;
                    u64::from(u32::from_ne_bytes(b))
                };
                if (found_ino32 & 0xffff_ffff) != (local_ino & 0xffff_ffff) {
                    off = nlmsg_align(off + nlmsg_len);
                    continue;
                }

                // Parse attributes.
                let mut attr_off = payload_off + ud_min;
                while attr_off + 4 <= off + nlmsg_len {
                    let nla_len = {
                        let b: [u8; 2] = rbuf[attr_off..attr_off + 2]
                            .try_into()
                            .or(Err(Errno::EOVERFLOW))?;
                        u16::from_ne_bytes(b) as usize
                    };
                    let nla_type = {
                        let b: [u8; 2] = rbuf[attr_off + 2..attr_off + 4]
                            .try_into()
                            .or(Err(Errno::EOVERFLOW))?;
                        u16::from_ne_bytes(b)
                    };
                    if nla_len < 4 {
                        break;
                    }
                    let payload_start = attr_off + 4;
                    let payload_len = nla_len - 4;
                    if payload_start + payload_len > off + nlmsg_len {
                        break;
                    }

                    if nla_type == UNIX_DIAG_PEER && payload_len >= 4 {
                        let peer_b: [u8; 4] = rbuf[payload_start..payload_start + 4]
                            .try_into()
                            .or(Err(Errno::EOVERFLOW))?;
                        let peer_ino = u64::from(u32::from_ne_bytes(peer_b));
                        return Ok(peer_ino);
                    }

                    attr_off = attr_off.saturating_add(nla_align(nla_len));
                }
            }

            off = nlmsg_align(off + nlmsg_len);
        }
        // Continue read loop for multipart replies.
    }
}

/// Returns a set of all UNIX domain sockets using NETLINK_SOCK_DIAG.
#[expect(clippy::arithmetic_side_effects)]
#[expect(clippy::cast_possible_truncation)]
pub fn unix_inodes() -> Result<SydHashSet<u64>, Errno> {
    // Open NETLINK_SOCK_DIAG socket.
    let nl = safe_socket(
        libc::AF_NETLINK,
        libc::SOCK_DGRAM | libc::SOCK_CLOEXEC,
        libc::NETLINK_SOCK_DIAG,
    )?;

    // Build request into a stack buffer.
    let mut req = [0u8; NL_HDR_LEN + UD_REQ_LEN];

    // Fill nlmsghdr.
    let mut p = 0usize;
    req[p..p + 4].copy_from_slice(&NL_MSG_LEN.to_ne_bytes()); // nlmsg_len
    p += 4;
    req[p..p + 2].copy_from_slice(&SOCK_DIAG_BY_FAMILY.to_ne_bytes()); // nlmsg_type
    p += 2;
    let nl_flags = (libc::NLM_F_REQUEST | libc::NLM_F_ROOT | libc::NLM_F_MATCH) as u16;
    req[p..p + 2].copy_from_slice(&nl_flags.to_ne_bytes()); // nlmsg_flags
    p += 2;
    req[p..p + 4].copy_from_slice(&1u32.to_ne_bytes()); // nlmsg_seq
    p += 4;
    req[p..p + 4].copy_from_slice(&0u32.to_ne_bytes()); // nlmsg_pid
    p += 4;

    // Fill unix_diag_req for a full dump of AF_UNIX sockets.
    req[p] = libc::AF_UNIX as u8;
    p += 1; // sdiag_family
    req[p] = 0u8;
    p += 1; // sdiag_protocol
    req[p..p + 2].copy_from_slice(&0u16.to_ne_bytes());
    p += 2; // pad
    req[p..p + 4].copy_from_slice(&u32::MAX.to_ne_bytes());
    p += 4; // udiag_states (all)
    req[p..p + 4].copy_from_slice(&0u32.to_ne_bytes());
    p += 4; // udiag_ino (0 => no inode filter; dump)
    req[p..p + 4].copy_from_slice(&UDIAG_SHOW_VFS.to_ne_bytes());
    p += 4; // udiag_show (no attributes needed)
    req[p..p + 4].copy_from_slice(&0u32.to_ne_bytes());
    p += 4; // cookie[0]
    req[p..p + 4].copy_from_slice(&0u32.to_ne_bytes());
    p += 4; // cookie[1]
    assert_eq!(p, req.len());

    // Send loop: retry short writes until full message is sent.
    let mut sent_total = 0usize;
    while sent_total < req.len() {
        let slice = &req[sent_total..];
        let sent = retry_on_eintr(|| write(&nl, slice))?;
        if sent == 0 {
            return Err(Errno::EIO);
        }
        sent_total = sent_total.saturating_add(sent);
    }

    // Recv loop: collect all udiag_ino values directly into a HashSet.
    //
    // Quoting https://docs.kernel.org/userspace-api/netlink/intro.html :
    // Use at least an 8kB buffer; 32kB recommended for dumps.
    let mut rbuf = [0u8; 0x8000];
    let mut iset = SydHashSet::default();
    'recv: loop {
        let n = retry_on_eintr(|| read(&nl, &mut rbuf))?;
        if n == 0 {
            return Err(Errno::EIO);
        }

        let mut off = 0usize;
        while off + NL_HDR_LEN <= n {
            // Read nlmsg_len (u32) and nlmsg_type (u16) safely.
            let nlmsg_len = {
                let b: [u8; 4] = rbuf[off..off + 4].try_into().or(Err(Errno::EOVERFLOW))?;
                u32::from_ne_bytes(b) as usize
            };
            if nlmsg_len == 0 || off + nlmsg_len > n {
                return Err(Errno::EIO);
            }
            let nlmsg_type = {
                let b: [u8; 2] = rbuf[off + 4..off + 6]
                    .try_into()
                    .or(Err(Errno::EOVERFLOW))?;
                u16::from_ne_bytes(b)
            };

            if nlmsg_type == NLMSG_DONE {
                break 'recv;
            } else if nlmsg_type == NLMSG_ERROR {
                if nlmsg_len >= NL_HDR_LEN + 4 {
                    let err_b: [u8; 4] = rbuf[off + NL_HDR_LEN..off + NL_HDR_LEN + 4]
                        .try_into()
                        .or(Err(Errno::EOVERFLOW))?;
                    let nl_err = i32::from_ne_bytes(err_b);
                    // nlmsgerr.error is negative errno.
                    return Err(Errno::from_raw(-nl_err));
                } else {
                    return Err(Errno::EIO);
                }
            } else if nlmsg_type == SOCK_DIAG_BY_FAMILY {
                // unix_diag_msg minimal payload is 16 bytes.
                let payload_off = off + NL_HDR_LEN;
                let ud_min = 16usize;
                if payload_off + ud_min > off + nlmsg_len {
                    return Err(Errno::EIO);
                }

                // udiag_ino (u32) at payload_off + 4
                let ino32 = {
                    let b: [u8; 4] = rbuf[payload_off + 4..payload_off + 8]
                        .try_into()
                        .or(Err(Errno::EOVERFLOW))?;
                    u32::from_ne_bytes(b)
                };

                // Walk NLAs; presence of UNIX_DIAG_VFS => path-based socket.
                let mut has_vfs = false;
                let mut attr_off = payload_off + ud_min;
                let attrs_end = off + nlmsg_len;
                while attr_off + 4 <= attrs_end {
                    let nla_len = {
                        let b: [u8; 2] = rbuf[attr_off..attr_off + 2]
                            .try_into()
                            .or(Err(Errno::EOVERFLOW))?;
                        u16::from_ne_bytes(b) as usize
                    };
                    let nla_type = {
                        let b: [u8; 2] = rbuf[attr_off + 2..attr_off + 4]
                            .try_into()
                            .or(Err(Errno::EOVERFLOW))?;
                        u16::from_ne_bytes(b)
                    };

                    if nla_len < 4 {
                        // Malformed NLA header;
                        // Stop parsing this message to avoid overrun.
                        break;
                    }

                    // Bounds-check this attribute's payload region. If it would overflow
                    // this message, break the attribute loop to skip the rest of this
                    // message safely (staying synchronized with the outer message parser).
                    let payload_start = attr_off + 4;
                    let payload_len = nla_len - 4;
                    if payload_start > attrs_end || payload_start + payload_len > attrs_end {
                        // Malformed/overrun; skip remainder of this message
                        break;
                    }

                    if nla_type == UNIX_DIAG_VFS {
                        has_vfs = true;
                        break;
                    }

                    // Advance to next attribute (aligned).
                    let next = attr_off.saturating_add(nla_align(nla_len));
                    if next <= attr_off {
                        break;
                    } // overflow guard
                    attr_off = next;
                }

                // Insert path-based sockets into the set.
                if has_vfs {
                    iset.try_reserve(1).or(Err(Errno::ENOMEM))?;
                    let _ = iset.insert(ino32.into());
                }
            }

            off = nlmsg_align(off + nlmsg_len);
        }
        // Continue read loop for multipart replies.
    }

    Ok(iset)
}

/// Parse a FD from a Path.
pub fn parse_fd(path: &XPath) -> Result<RawFd, Errno> {
    btoi::<RawFd>(path.as_bytes()).or(Err(Errno::EBADF))
}

/// Read a symbolic link and return a `XPathBuf`.
// TODO: Move to compat.rs
pub fn readlinkat<Fd: AsFd, P: NixPath + ?Sized>(fd: Fd, base: &P) -> Result<XPathBuf, Errno> {
    // Initialize target on the stack.
    let mut target = [0u8; PATH_MAX];

    let n = base.with_nix_path(|cstr| {
        // SAFETY: We don't want nix' extra handling around readlink here.
        #[expect(clippy::cast_sign_loss)]
        Errno::result(unsafe {
            libc::readlinkat(
                fd.as_fd().as_raw_fd(),
                cstr.as_ptr(),
                target.as_mut_ptr().cast(),
                target.len(),
            )
        })
        .map(|n| n as usize)
    })??;

    if n >= target.len() {
        // Truncation occurred!
        return Err(Errno::ENAMETOOLONG);
    }

    Ok(target[..n].into())
}

/// Read a symbolic link from FD and return a `XPathBuf`.
pub fn readlinkfd<Fd: AsFd>(fd: Fd) -> Result<XPathBuf, Errno> {
    readlinkat(fd, c"").map_err(|errno| {
        // FD-only readlinkat(2) returns ENOENT,
        // when FD is not a symbolic link.
        if errno == Errno::ENOENT {
            Errno::EINVAL
        } else {
            errno
        }
    })
}

/// Create file and write the given content.
#[expect(clippy::disallowed_methods)]
pub fn cat<P: AsRef<Path>, T: AsRef<[u8]>>(path: P, content: T) -> std::io::Result<()> {
    let mut file = File::create(path)?;
    file.write_all(content.as_ref())?;
    Ok(())
}

/// Make a file executable.
pub fn chmod_x<P: AsRef<Path>>(path: P) -> std::io::Result<()> {
    // Set permissions to make path executable.
    let metadata = metadata(path.as_ref())?;
    let mut permissions = metadata.permissions();
    permissions.set_mode(0o700); // This sets the file executable for the owner (rwx).
    set_permissions(path.as_ref(), permissions)
}

/// Format open(2) flags for serialization.
pub fn format_oflags(flags: OFlag) -> Vec<String> {
    let count = flags.into_iter().count();
    if count == 0 {
        return vec![];
    }

    let mut fmt = Vec::with_capacity(count);
    for flag in flags.iter() {
        fmt.push(format_oflag(flag));
    }

    fmt
}

/// Format a single open(2) flag for serialization.
pub fn format_oflag(flag: OFlag) -> String {
    let flag = format!("{flag:?}");

    if !flag.starts_with("OFlag(O_") || !flag.ends_with(')') {
        return "?".to_string();
    }

    #[expect(clippy::arithmetic_side_effects)]
    String::from_utf8_lossy(&flag.as_bytes()[8..flag.len() - 1]).to_ascii_lowercase()
}

/// Format unshare(2) flags for serialization.
pub fn format_clone_flags(flags: CloneFlags) -> Vec<&'static str> {
    let mut names = vec![];

    if flags.is_empty() {
        return names;
    }
    if flags.contains(CloneFlags::CLONE_NEWUSER) {
        names.push("user");
    }
    if flags.contains(CloneFlags::CLONE_NEWNS) {
        names.push("mount");
    }
    if flags.contains(CloneFlags::CLONE_NEWUTS) {
        names.push("uts");
    }
    if flags.contains(CloneFlags::CLONE_NEWIPC) {
        names.push("ipc");
    }
    if flags.contains(CloneFlags::CLONE_NEWPID) {
        names.push("pid");
    }
    if flags.contains(CloneFlags::CLONE_NEWNET) {
        names.push("net");
    }
    if flags.contains(CloneFlags::CLONE_NEWCGROUP) {
        names.push("cgroup");
    }
    if flags.contains(CLONE_NEWTIME) {
        names.push("time");
    }

    names
}

/// Format the return vector from `format_clone_flags` into a `String`.
pub fn format_clone_names(clone_names: &[&str]) -> String {
    match clone_names.len() {
        0 => "no namespaces".to_string(),
        1 => format!("{} namespace", clone_names[0]),
        2 => format!("{} and {} namespaces", clone_names[0], clone_names[1]),
        _ => {
            let mut s = clone_names.join(", ");
            #[expect(clippy::arithmetic_side_effects)]
            if let Some(pos) = s.rfind(", ") {
                s.replace_range(pos..pos + 2, ", and ");
            }
            format!("{s} namespaces")
        }
    }
}

/// Searches for a name within a directory.
///
/// `name` is matched literally and exactly against file names.
/// Directory entries are appended a trailing slash before matching.
/// Symlink entries are appended an `@` character before matching.
#[expect(clippy::disallowed_methods)]
pub fn grep(dir: &XPath, name: &[u8]) -> Option<XPathBuf> {
    let dir = File::open(dir.as_path()).ok()?;
    let name = XPath::from_bytes(name);
    loop {
        let mut entries = getdents64(&dir, 128).ok()?;
        for entry in &mut entries {
            let mut path = XPathBuf::from(entry.name_bytes());
            // Append a trailing slash for directories.
            if entry.is_dir() {
                path.append_byte(b'/');
            } else if entry.is_symlink() {
                path.append_byte(b'@');
            } else if entry.is_block_device() {
                path.append_byte(b'!');
            } else if entry.is_char_device() {
                path.append_byte(b'$');
            } else if entry.is_fifo() {
                path.append_byte(b'|');
            } else if entry.is_socket() {
                path.append_byte(b'~');
            }
            if *path == *name || (name.len() == 1 && path.ends_with(name.as_bytes())) {
                return Some(path);
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use std::{
        fs::OpenOptions,
        io::ErrorKind,
        os::{
            fd::AsRawFd,
            unix::{
                ffi::OsStrExt,
                fs::OpenOptionsExt,
                net::{UnixListener, UnixStream},
            },
        },
        sync::mpsc,
        thread,
        time::Duration,
    };

    use nix::{
        fcntl::open,
        sys::{
            socket::{
                accept, bind, connect, listen, socket, socketpair, AddressFamily, Backlog,
                SockFlag, SockType, UnixAddr,
            },
            stat::Mode,
        },
        unistd::{dup, pipe},
    };
    use tempfile::NamedTempFile;

    use super::*;
    use crate::confine::check_unix_diag;

    fn tempdir() -> Result<XPathBuf, Box<dyn std::error::Error>> {
        let tmp = tempfile::Builder::new()
            .disable_cleanup(true)
            .tempdir_in(".")?;
        let _ = OpenOptions::new()
            .write(true)
            .create(true)
            .mode(0o600)
            .open(tmp.path().join("test"))?;
        Ok(tmp
            .path()
            .to_path_buf()
            .file_name()
            .unwrap()
            .as_bytes()
            .into())
    }

    #[test]
    fn test_fd_status_flags_file_read_only() {
        let temp = NamedTempFile::new().unwrap();
        let file = OpenOptions::new().read(true).open(temp.path()).unwrap();
        let flags = fd_status_flags(&file).unwrap();

        assert!(!flags.contains(OFlag::O_WRONLY));
        assert!(!flags.contains(OFlag::O_RDWR));
    }

    #[test]
    fn test_fd_status_flags_file_write_only() {
        let temp = NamedTempFile::new().unwrap();
        let file = OpenOptions::new().write(true).open(temp.path()).unwrap();
        let flags = fd_status_flags(&file).unwrap();

        assert!(flags.contains(OFlag::O_WRONLY));
        assert!(!flags.contains(OFlag::O_RDWR));
    }

    #[test]
    fn test_fd_status_flags_file_read_write() {
        let temp = NamedTempFile::new().unwrap();
        let file = OpenOptions::new()
            .read(true)
            .write(true)
            .open(temp.path())
            .unwrap();
        let flags = fd_status_flags(&file).unwrap();

        assert!(flags.contains(OFlag::O_RDWR));
        assert!(!flags.contains(OFlag::O_WRONLY));
    }

    #[test]
    fn test_fd_status_flags_owned_fd_read_only() {
        let temp = NamedTempFile::new().unwrap();
        let file = OpenOptions::new().read(true).open(temp.path()).unwrap();
        let owned_fd = unsafe { OwnedFd::from_raw_fd(file.as_raw_fd()) };
        std::mem::forget(file);

        let flags = fd_status_flags(&owned_fd).unwrap();
        assert!(!flags.contains(OFlag::O_WRONLY));
        assert!(!flags.contains(OFlag::O_RDWR));
    }

    #[test]
    fn test_fd_status_flags_owned_fd_write_only() {
        let temp = NamedTempFile::new().unwrap();
        let file = OpenOptions::new().write(true).open(temp.path()).unwrap();
        let owned_fd = unsafe { OwnedFd::from_raw_fd(file.as_raw_fd()) };
        std::mem::forget(file);

        let flags = fd_status_flags(&owned_fd).unwrap();
        assert!(flags.contains(OFlag::O_WRONLY));
        assert!(!flags.contains(OFlag::O_RDWR));
    }

    #[test]
    fn test_fd_status_flags_owned_fd_read_write() {
        let temp = NamedTempFile::new().unwrap();
        let file = OpenOptions::new()
            .read(true)
            .write(true)
            .open(temp.path())
            .unwrap();
        let owned_fd = unsafe { OwnedFd::from_raw_fd(file.as_raw_fd()) };
        std::mem::forget(file);

        let flags = fd_status_flags(&owned_fd).unwrap();
        assert!(flags.contains(OFlag::O_RDWR));
        assert!(!flags.contains(OFlag::O_WRONLY));
    }

    #[test]
    fn test_fd_status_flags_borrowed_fd_read_only() {
        let temp = NamedTempFile::new().unwrap();
        let file = OpenOptions::new().read(true).open(temp.path()).unwrap();
        let borrowed_fd = file.as_fd();

        let flags = fd_status_flags(borrowed_fd).unwrap();
        assert!(!flags.contains(OFlag::O_WRONLY));
        assert!(!flags.contains(OFlag::O_RDWR));
    }

    #[test]
    fn test_fd_status_flags_borrowed_fd_write_only() {
        let temp = NamedTempFile::new().unwrap();
        let file = OpenOptions::new().write(true).open(temp.path()).unwrap();
        let borrowed_fd = file.as_fd();

        let flags = fd_status_flags(borrowed_fd).unwrap();
        assert!(flags.contains(OFlag::O_WRONLY));
        assert!(!flags.contains(OFlag::O_RDWR));
    }

    #[test]
    fn test_fd_status_flags_borrowed_fd_read_write() {
        let temp = NamedTempFile::new().unwrap();
        let file = OpenOptions::new()
            .read(true)
            .write(true)
            .open(temp.path())
            .unwrap();
        let borrowed_fd = file.as_fd();

        let flags = fd_status_flags(borrowed_fd).unwrap();
        assert!(flags.contains(OFlag::O_RDWR));
        assert!(!flags.contains(OFlag::O_WRONLY));
    }

    #[test]
    fn test_fd_status_flags_dev_null_read() {
        let file = OpenOptions::new().read(true).open("/dev/null").unwrap();
        let flags = fd_status_flags(&file).unwrap();

        assert!(!flags.contains(OFlag::O_WRONLY));
        assert!(!flags.contains(OFlag::O_RDWR));
    }

    #[test]
    fn test_fd_status_flags_dev_null_write() {
        let file = OpenOptions::new().write(true).open("/dev/null").unwrap();
        let flags = fd_status_flags(&file).unwrap();

        assert!(flags.contains(OFlag::O_WRONLY));
        assert!(!flags.contains(OFlag::O_RDWR));
    }

    #[test]
    fn test_fd_status_flags_dev_null_read_write() {
        let file = OpenOptions::new()
            .read(true)
            .write(true)
            .open("/dev/null")
            .unwrap();
        let flags = fd_status_flags(&file).unwrap();

        assert!(flags.contains(OFlag::O_RDWR));
        assert!(!flags.contains(OFlag::O_WRONLY));
    }

    #[test]
    fn test_fd_status_flags_pipe_read_end() {
        let (read_fd, _) = pipe().unwrap();

        let flags = fd_status_flags(&read_fd).unwrap();
        assert!(!flags.contains(OFlag::O_WRONLY));
        assert!(!flags.contains(OFlag::O_RDWR));
    }

    #[test]
    fn test_fd_status_flags_pipe_write_end() {
        let (_, write_fd) = pipe().unwrap();

        let flags = fd_status_flags(&write_fd).unwrap();
        assert!(flags.contains(OFlag::O_WRONLY));
        assert!(!flags.contains(OFlag::O_RDWR));
    }

    #[test]
    fn test_fd_status_flags_append_mode() {
        let temp = NamedTempFile::new().unwrap();
        let file = OpenOptions::new()
            .write(true)
            .append(true)
            .open(temp.path())
            .unwrap();
        let flags = fd_status_flags(&file).unwrap();

        assert!(flags.contains(OFlag::O_WRONLY));
        assert!(flags.contains(OFlag::O_APPEND));
    }

    #[test]
    fn test_fd_status_flags_create_mode() {
        let temp = NamedTempFile::new().unwrap();
        let file = OpenOptions::new()
            .write(true)
            .create(true)
            .open(temp.path())
            .unwrap();
        let flags = fd_status_flags(&file).unwrap();

        assert!(flags.contains(OFlag::O_WRONLY));
    }

    #[test]
    fn test_fd_status_flags_truncate_mode() {
        let temp = NamedTempFile::new().unwrap();
        let file = OpenOptions::new()
            .write(true)
            .truncate(true)
            .open(temp.path())
            .unwrap();
        let flags = fd_status_flags(&file).unwrap();

        assert!(flags.contains(OFlag::O_WRONLY));
    }

    #[test]
    fn test_fd_status_flags_read_append_mode() {
        let temp = NamedTempFile::new().unwrap();
        let file = OpenOptions::new()
            .read(true)
            .append(true)
            .open(temp.path())
            .unwrap();
        let flags = fd_status_flags(&file).unwrap();

        assert!(flags.contains(OFlag::O_RDWR));
        assert!(flags.contains(OFlag::O_APPEND));
    }

    #[test]
    fn test_fd_status_flags_create_new_mode() {
        let temp = NamedTempFile::new().unwrap();
        std::fs::remove_file(temp.path()).unwrap();
        let file = OpenOptions::new()
            .write(true)
            .create_new(true)
            .open(temp.path())
            .unwrap();
        let flags = fd_status_flags(&file).unwrap();

        assert!(flags.contains(OFlag::O_WRONLY));
    }

    #[test]
    fn test_fd_status_flags_reference_to_file() {
        let temp = NamedTempFile::new().unwrap();
        let file = OpenOptions::new().read(true).open(temp.path()).unwrap();
        let file_ref = &file;

        let flags = fd_status_flags(file_ref).unwrap();
        assert!(!flags.contains(OFlag::O_WRONLY));
        assert!(!flags.contains(OFlag::O_RDWR));
    }

    #[test]
    fn test_fd_status_flags_mutable_reference_to_file() {
        let temp = NamedTempFile::new().unwrap();
        let mut file = OpenOptions::new().write(true).open(temp.path()).unwrap();
        let file_ref = &mut file;

        let flags = fd_status_flags(file_ref).unwrap();
        assert!(flags.contains(OFlag::O_WRONLY));
        assert!(!flags.contains(OFlag::O_RDWR));
    }

    #[test]
    fn test_fd_status_flags_box_file() {
        let temp = NamedTempFile::new().unwrap();
        let file = Box::new(OpenOptions::new().read(true).open(temp.path()).unwrap());

        let flags = fd_status_flags(&file).unwrap();
        assert!(!flags.contains(OFlag::O_WRONLY));
        assert!(!flags.contains(OFlag::O_RDWR));
    }

    #[test]
    fn test_fd_status_flags_arc_file() {
        use std::sync::Arc;
        let temp = NamedTempFile::new().unwrap();
        let file = Arc::new(OpenOptions::new().read(true).open(temp.path()).unwrap());

        let flags = fd_status_flags(&file).unwrap();
        assert!(!flags.contains(OFlag::O_WRONLY));
        assert!(!flags.contains(OFlag::O_RDWR));
    }

    #[test]
    fn test_fd_status_flags_rc_file() {
        use std::rc::Rc;
        let temp = NamedTempFile::new().unwrap();
        let file = Rc::new(OpenOptions::new().read(true).open(temp.path()).unwrap());

        let flags = fd_status_flags(&file).unwrap();
        assert!(!flags.contains(OFlag::O_WRONLY));
        assert!(!flags.contains(OFlag::O_RDWR));
    }

    #[test]
    fn test_fd_status_flags_invalid_fd() {
        let result = fd_status_flags(AT_BADFD);

        assert!(result.is_err());
        assert_eq!(result.unwrap_err(), Errno::EBADF);
    }

    #[test]
    fn test_fd_status_flags_multiple_calls_consistency() {
        let temp = NamedTempFile::new().unwrap();
        let file = OpenOptions::new()
            .read(true)
            .write(true)
            .open(temp.path())
            .unwrap();

        let flags1 = fd_status_flags(&file).unwrap();
        let flags2 = fd_status_flags(&file).unwrap();
        let flags3 = fd_status_flags(&file).unwrap();

        assert_eq!(flags1, flags2);
        assert_eq!(flags2, flags3);
    }

    #[test]
    fn test_fd_status_flags_different_file_types() {
        let temp = NamedTempFile::new().unwrap();
        let file1 = OpenOptions::new().write(true).open(temp.path()).unwrap();
        let file2 = OpenOptions::new().write(true).open("/dev/null").unwrap();

        let flags1 = fd_status_flags(&file1).unwrap();
        let flags2 = fd_status_flags(&file2).unwrap();

        assert!(flags1.contains(OFlag::O_WRONLY));
        assert!(flags2.contains(OFlag::O_WRONLY));
    }

    #[test]
    fn test_fd_status_flags_dup_file_descriptor() {
        let temp = NamedTempFile::new().unwrap();
        let file = OpenOptions::new().read(true).open(temp.path()).unwrap();
        let duped_fd = dup(&file).unwrap();

        let flags = fd_status_flags(&duped_fd).unwrap();
        assert!(!flags.contains(OFlag::O_WRONLY));
        assert!(!flags.contains(OFlag::O_RDWR));
    }

    #[test]
    fn test_is_writable_fd_file_read_only() {
        let temp = NamedTempFile::new().unwrap();
        let file = OpenOptions::new().read(true).open(temp.path()).unwrap();
        let result = is_writable_fd(&file).unwrap();

        assert!(!result);
    }

    #[test]
    fn test_is_writable_fd_file_write_only() {
        let temp = NamedTempFile::new().unwrap();
        let file = OpenOptions::new().write(true).open(temp.path()).unwrap();
        let result = is_writable_fd(&file).unwrap();

        assert!(result);
    }

    #[test]
    fn test_is_writable_fd_file_read_write() {
        let temp = NamedTempFile::new().unwrap();
        let file = OpenOptions::new()
            .read(true)
            .write(true)
            .open(temp.path())
            .unwrap();
        let result = is_writable_fd(&file).unwrap();

        assert!(result);
    }

    #[test]
    fn test_is_writable_fd_owned_fd_read_only() {
        let temp = NamedTempFile::new().unwrap();
        let file = OpenOptions::new().read(true).open(temp.path()).unwrap();
        let owned_fd = unsafe { OwnedFd::from_raw_fd(file.as_raw_fd()) };
        std::mem::forget(file);

        let result = is_writable_fd(&owned_fd).unwrap();
        assert!(!result);
    }

    #[test]
    fn test_is_writable_fd_owned_fd_write_only() {
        let temp = NamedTempFile::new().unwrap();
        let file = OpenOptions::new().write(true).open(temp.path()).unwrap();
        let owned_fd = unsafe { OwnedFd::from_raw_fd(file.as_raw_fd()) };
        std::mem::forget(file);

        let result = is_writable_fd(&owned_fd).unwrap();
        assert!(result);
    }

    #[test]
    fn test_is_writable_fd_owned_fd_read_write() {
        let temp = NamedTempFile::new().unwrap();
        let file = OpenOptions::new()
            .read(true)
            .write(true)
            .open(temp.path())
            .unwrap();
        let owned_fd = unsafe { OwnedFd::from_raw_fd(file.as_raw_fd()) };
        std::mem::forget(file);

        let result = is_writable_fd(&owned_fd).unwrap();
        assert!(result);
    }

    #[test]
    fn test_is_writable_fd_borrowed_fd_read_only() {
        let temp = NamedTempFile::new().unwrap();
        let file = OpenOptions::new().read(true).open(temp.path()).unwrap();
        let borrowed_fd = file.as_fd();

        let result = is_writable_fd(borrowed_fd).unwrap();
        assert!(!result);
    }

    #[test]
    fn test_is_writable_fd_borrowed_fd_write_only() {
        let temp = NamedTempFile::new().unwrap();
        let file = OpenOptions::new().write(true).open(temp.path()).unwrap();
        let borrowed_fd = file.as_fd();

        let result = is_writable_fd(borrowed_fd).unwrap();
        assert!(result);
    }

    #[test]
    fn test_is_writable_fd_borrowed_fd_read_write() {
        let temp = NamedTempFile::new().unwrap();
        let file = OpenOptions::new()
            .read(true)
            .write(true)
            .open(temp.path())
            .unwrap();
        let borrowed_fd = file.as_fd();

        let result = is_writable_fd(borrowed_fd).unwrap();
        assert!(result);
    }

    #[test]
    fn test_is_writable_fd_dev_null_read() {
        let file = OpenOptions::new().read(true).open("/dev/null").unwrap();
        let result = is_writable_fd(&file).unwrap();

        assert!(!result);
    }

    #[test]
    fn test_is_writable_fd_dev_null_write() {
        let file = OpenOptions::new().write(true).open("/dev/null").unwrap();
        let result = is_writable_fd(&file).unwrap();

        assert!(result);
    }

    #[test]
    fn test_is_writable_fd_dev_null_read_write() {
        let file = OpenOptions::new()
            .read(true)
            .write(true)
            .open("/dev/null")
            .unwrap();
        let result = is_writable_fd(&file).unwrap();

        assert!(result);
    }

    #[test]
    fn test_is_writable_fd_pipe_read_end() {
        let (read_fd, _) = pipe().unwrap();

        let result = is_writable_fd(&read_fd).unwrap();
        assert!(!result);
    }

    #[test]
    fn test_is_writable_fd_pipe_write_end() {
        let (_, write_fd) = pipe().unwrap();

        let result = is_writable_fd(&write_fd).unwrap();
        assert!(result);
    }

    #[test]
    fn test_is_writable_fd_append_mode() {
        let temp = NamedTempFile::new().unwrap();
        let file = OpenOptions::new()
            .write(true)
            .append(true)
            .open(temp.path())
            .unwrap();
        let result = is_writable_fd(&file).unwrap();

        assert!(result);
    }

    #[test]
    fn test_is_writable_fd_create_mode() {
        let temp = NamedTempFile::new().unwrap();
        let file = OpenOptions::new()
            .write(true)
            .create(true)
            .open(temp.path())
            .unwrap();
        let result = is_writable_fd(&file).unwrap();

        assert!(result);
    }

    #[test]
    fn test_is_writable_fd_truncate_mode() {
        let temp = NamedTempFile::new().unwrap();
        let file = OpenOptions::new()
            .write(true)
            .truncate(true)
            .open(temp.path())
            .unwrap();
        let result = is_writable_fd(&file).unwrap();

        assert!(result);
    }

    #[test]
    fn test_is_writable_fd_read_append_mode() {
        let temp = NamedTempFile::new().unwrap();
        let file = OpenOptions::new()
            .read(true)
            .append(true)
            .open(temp.path())
            .unwrap();
        let result = is_writable_fd(&file).unwrap();

        assert!(result);
    }

    #[test]
    fn test_is_writable_fd_create_new_mode() {
        let temp = NamedTempFile::new().unwrap();
        std::fs::remove_file(temp.path()).unwrap();
        let file = OpenOptions::new()
            .write(true)
            .create_new(true)
            .open(temp.path())
            .unwrap();
        let result = is_writable_fd(&file).unwrap();

        assert!(result);
    }

    #[test]
    fn test_is_writable_fd_read_only_with_create() {
        let temp = NamedTempFile::new().unwrap();
        let file = open(
            temp.path(),
            OFlag::O_RDONLY | OFlag::O_CREAT | OFlag::O_TRUNC,
            Mode::empty(),
        )
        .map(File::from)
        .unwrap();
        let result = is_writable_fd(&file).unwrap();

        assert!(!result);
    }

    #[test]
    fn test_is_writable_fd_reference_to_file() {
        let temp = NamedTempFile::new().unwrap();
        let file = OpenOptions::new().read(true).open(temp.path()).unwrap();
        let file_ref = &file;

        let result = is_writable_fd(file_ref).unwrap();
        assert!(!result);
    }

    #[test]
    fn test_is_writable_fd_mutable_reference_to_file() {
        let temp = NamedTempFile::new().unwrap();
        let mut file = OpenOptions::new().write(true).open(temp.path()).unwrap();
        let file_ref = &mut file;

        let result = is_writable_fd(file_ref).unwrap();
        assert!(result);
    }

    #[test]
    fn test_is_writable_fd_box_file() {
        let temp = NamedTempFile::new().unwrap();
        let file = Box::new(OpenOptions::new().read(true).open(temp.path()).unwrap());

        let result = is_writable_fd(&file).unwrap();
        assert!(!result);
    }

    #[test]
    fn test_is_writable_fd_arc_file() {
        use std::sync::Arc;
        let temp = NamedTempFile::new().unwrap();
        let file = Arc::new(OpenOptions::new().read(true).open(temp.path()).unwrap());

        let result = is_writable_fd(&file).unwrap();
        assert!(!result);
    }

    #[test]
    fn test_is_writable_fd_rc_file() {
        use std::rc::Rc;
        let temp = NamedTempFile::new().unwrap();
        let file = Rc::new(OpenOptions::new().read(true).open(temp.path()).unwrap());

        let result = is_writable_fd(&file).unwrap();
        assert!(!result);
    }

    #[test]
    fn test_is_writable_fd_invalid_fd() {
        let result = is_writable_fd(AT_BADFD);

        assert!(result.is_err());
        assert_eq!(result.unwrap_err(), Errno::EBADF);
    }

    #[test]
    fn test_is_writable_fd_multiple_calls_consistency() {
        let temp = NamedTempFile::new().unwrap();
        let file = OpenOptions::new()
            .read(true)
            .write(true)
            .open(temp.path())
            .unwrap();

        let result1 = is_writable_fd(&file).unwrap();
        let result2 = is_writable_fd(&file).unwrap();
        let result3 = is_writable_fd(&file).unwrap();

        assert_eq!(result1, result2);
        assert_eq!(result2, result3);
    }

    #[test]
    fn test_is_writable_fd_different_file_types() {
        let temp = NamedTempFile::new().unwrap();
        let file1 = OpenOptions::new().write(true).open(temp.path()).unwrap();
        let file2 = OpenOptions::new().write(true).open("/dev/null").unwrap();

        let result1 = is_writable_fd(&file1).unwrap();
        let result2 = is_writable_fd(&file2).unwrap();

        assert!(result1);
        assert!(result2);
    }

    #[test]
    fn test_is_writable_fd_dup_file_descriptor() {
        let temp = NamedTempFile::new().unwrap();
        let file = OpenOptions::new().read(true).open(temp.path()).unwrap();
        let duped_fd = dup(&file).unwrap();

        let result = is_writable_fd(&duped_fd).unwrap();
        assert!(!result);
    }

    #[test]
    fn test_base_offset_root_and_non_root() {
        // Mirrors the computation used in CanonicalPath::new.
        let off = |parent_len: usize| parent_len + usize::from(parent_len > 1);

        assert_eq!(off(1), 1, "root parent must not drop first byte");
        assert_eq!(off(5), 6, "non-root parent must skip one separator");
    }

    #[test]
    fn test_peer_inode_socketpair() {
        if !check_unix_diag().unwrap_or(false) {
            eprintln!("UNIX socket diagnostics are not supported, skipping!");
            return;
        }

        // Create a unix-domain socketpair (two connected endpoints).
        let (a_fd, b_fd) = socketpair(
            AddressFamily::Unix,
            SockType::Stream,
            None,
            SockFlag::SOCK_CLOEXEC,
        )
        .expect("socketpair failed");

        // Expected peer inode (low 32 bits).
        let stx_b = fstatx(&b_fd, STATX_INO).expect("fstatx on b failed");
        let expected = (stx_b.stx_ino & 0xffff_ffff) as u64;

        // Call peer_inode on the other side and compare.
        let got = peer_inode(&a_fd).expect("peer_inode failed for socketpair");
        assert_eq!(
            got, expected,
            "peer_inode returned unexpected inode for socketpair"
        );
    }

    #[test]
    fn test_peer_inode_listener_filesystem() {
        if !check_unix_diag().unwrap_or(false) {
            eprintln!("UNIX socket diagnostics are not supported, skipping!");
            return;
        }

        // Create a temporary directory for a unique socket path.
        let td = tempdir().expect("tempdir failed");
        let sock_path = td.as_path().join("peer_inode.sock");

        // Spawn server thread:
        // bind, listen, accept, compute peer_inode on accepted socket and send it back.
        let (tx_ready, rx_ready) = mpsc::channel::<()>();
        let (tx_peer, rx_peer) = mpsc::channel::<u64>();
        let sock_path_clone = sock_path.clone();
        let server = thread::spawn(move || {
            // bind & listen using std APIs to get a UnixListener.
            let listener = UnixListener::bind(&sock_path_clone).expect("bind failed in server");
            // Inform main thread we're listening.
            tx_ready.send(()).expect("notify failed");
            // Accept one connection (blocking).
            let (accepted, _addr) = listener.accept().expect("accept failed in server");
            // Call peer_inode on accepted stream.
            let peer = peer_inode(&accepted).expect("peer_inode failed on accepted socket");
            // Send result back.
            tx_peer.send(peer).expect("send peer failed");
            // accepted and listener drop here.
        });

        // Wait for server to be ready
        rx_ready
            .recv_timeout(Duration::from_secs(10))
            .expect("server did not signal ready");

        // Connect client to socket path (retries briefly if necessary).
        let client = loop {
            match UnixStream::connect(&sock_path) {
                Ok(s) => break s,
                Err(e) => {
                    if e.kind() == ErrorKind::NotFound || e.kind() == ErrorKind::ConnectionRefused {
                        thread::sleep(Duration::from_millis(10));
                        continue;
                    } else {
                        panic!("connect failed: {e:?}");
                    }
                }
            }
        };

        // Compute expected from client fd.
        let stx_client = fstatx(&client, STATX_INO).expect("fstatx client failed");
        let expected = (stx_client.stx_ino & 0xffff_ffff) as u64;

        // Receive peer inode computed by server.
        let got = rx_peer
            .recv_timeout(Duration::from_secs(10))
            .expect("server thread did not send peer inode");

        // Cleanup socket file by dropping tempdir.
        drop(td);

        assert_eq!(got, expected, "peer_inode mismatch for filesystem listener");
        server.join().expect("server thread panicked");
    }

    #[test]
    fn test_peer_inode_listener_abstract() {
        if !check_unix_diag().unwrap_or(false) {
            eprintln!("UNIX socket diagnostics are not supported, skipping!");
            return;
        }

        // Create an abstract socket name (no filesystem path).
        let name = b"peer_inode_test_abstract_12345";

        // Create server socket.
        let srv_fd = socket(
            AddressFamily::Unix,
            SockType::Stream,
            SockFlag::SOCK_CLOEXEC,
            None,
        )
        .expect("socket failed for abstract server");

        // Construct abstract address and bind/listen.
        let sockaddr = UnixAddr::new_abstract(name).expect("new_abstract failed");
        bind(srv_fd.as_raw_fd(), &sockaddr).expect("bind abstract failed");
        listen(&srv_fd, Backlog::new(1).unwrap()).expect("listen failed for abstract");

        // Create client socket and connect.
        let cli_fd = socket(
            AddressFamily::Unix,
            SockType::Stream,
            SockFlag::SOCK_CLOEXEC,
            None,
        )
        .expect("socket failed for abstract client");
        connect(cli_fd.as_raw_fd(), &sockaddr).expect("connect abstract failed");

        // Server accept.
        let acc_fd = accept(srv_fd.as_raw_fd()).expect("accept failed for abstract");
        let acc_fd = unsafe { OwnedFd::from_raw_fd(acc_fd) };

        // Expected is client's inode low 32 bits.
        let stx_client = fstatx(&cli_fd, STATX_INO).expect("fstatx client failed");
        let expected = (stx_client.stx_ino & 0xffff_ffff) as u64;

        // Call peer_inode on the accepted/server side.
        let got = peer_inode(&acc_fd).expect("peer_inode failed for abstract");

        // Close server FD (cleanup).
        drop(srv_fd);

        assert_eq!(got, expected, "peer_inode mismatch for abstract socket");
    }

    #[test]
    fn test_peer_inode_symmetry_socketpair() {
        if !check_unix_diag().unwrap_or(false) {
            eprintln!("UNIX socket diagnostics are not supported, skipping!");
            return;
        }

        // Create socketpair and verify mutual mapping.
        let (a_fd, b_fd) = socketpair(
            AddressFamily::Unix,
            SockType::Stream,
            None,
            SockFlag::SOCK_CLOEXEC,
        )
        .expect("socketpair failed");

        // Expected low-32 inodes.
        let stx_a = fstatx(&a_fd, STATX_INO).expect("fstatx a failed");
        let stx_b = fstatx(&b_fd, STATX_INO).expect("fstatx b failed");
        let expected_a = (stx_a.stx_ino & 0xffff_ffff) as u64;
        let expected_b = (stx_b.stx_ino & 0xffff_ffff) as u64;

        let got_from_a = peer_inode(&a_fd).expect("peer_inode on a failed");
        let got_from_b = peer_inode(&b_fd).expect("peer_inode on b failed");

        assert_eq!(
            got_from_a, expected_b,
            "peer_inode(a) should equal inode(b)"
        );
        assert_eq!(
            got_from_b, expected_a,
            "peer_inode(b) should equal inode(a)"
        );
    }

    #[test]
    fn test_oflag_rdonly_is_empty() {
        let mut flags = OFlag::empty();
        flags.insert(OFlag::O_RDONLY);
        assert!(flags.is_empty());
    }

    #[test]
    fn test_oflag_accmode() {
        assert_eq!(oflag_accmode(OFlag::empty()), OFlag::O_RDONLY);
        assert_eq!(oflag_accmode(OFlag::O_RDONLY), OFlag::O_RDONLY);
        assert_eq!(oflag_accmode(OFlag::O_WRONLY), OFlag::O_WRONLY);
        assert_eq!(oflag_accmode(OFlag::O_RDWR), OFlag::O_RDWR);
        assert_eq!(oflag_accmode(OFlag::O_PATH), OFlag::O_PATH);

        assert_eq!(
            oflag_accmode(OFlag::empty() | OFlag::O_APPEND),
            OFlag::O_RDONLY
        );
        assert_eq!(
            oflag_accmode(OFlag::O_RDONLY | OFlag::O_ASYNC),
            OFlag::O_RDONLY
        );
        assert_eq!(
            oflag_accmode(OFlag::O_WRONLY | OFlag::O_CREAT | OFlag::O_EXCL | OFlag::O_TRUNC),
            OFlag::O_WRONLY
        );
        assert_eq!(
            oflag_accmode(OFlag::O_RDWR | OFlag::O_CLOEXEC | OFlag::O_DIRECTORY),
            OFlag::O_RDWR
        );
        assert_eq!(
            oflag_accmode(OFlag::O_PATH | OFlag::O_NOFOLLOW),
            OFlag::O_PATH
        );
    }
}
