//
// Syd: rock-solid application kernel
// src/path.rs: Path handling for UNIX
//
// Copyright (c) 2024, 2025 Ali Polatel <alip@chesswob.org>
// Based in part upon David A. Wheeler's SafeName LSM patches which is:
//   Copyright (C) 2016 David A. Wheeler
//   SPDX-License-Identifier: GPL-2.0
//
// SPDX-License-Identifier: GPL-3.0

use std::{
    borrow::{Borrow, Cow},
    cmp::Ordering,
    collections::VecDeque,
    ffi::{CStr, OsStr, OsString},
    ops::Deref,
    os::{
        fd::RawFd,
        unix::ffi::{OsStrExt, OsStringExt},
    },
    path::{Path, PathBuf},
};

use btoi::btoi;
use memchr::{
    arch::all::{is_equal, is_prefix, is_suffix},
    memchr, memmem, memrchr,
};
use nix::{
    errno::Errno,
    fcntl::{openat2, OFlag, OpenHow, ResolveFlag},
    libc::pid_t,
    unistd::{close, Pid},
    NixPath,
};
use once_cell::sync::Lazy;

use crate::{
    config::MAGIC_PREFIX,
    fs::{retry_on_eintr, FileType},
    log::log_untrusted_buf,
};

/// Generate a formatted `XPathBuf`.
#[macro_export]
macro_rules! xpath {
    ($($arg:tt)*) => {
        XPathBuf::from(format!($($arg)*))
    };
}

/// A safe constant to use as PATH_MAX without relying on libc.
pub const PATH_MAX: usize = 4096;

/// A safe default size to use for paths.
pub const PATH_MIN: usize = 128;

// This pointer is confined by seccomp for use with openat(2) for getdir_long().
static DOTDOT: Lazy<u64> = Lazy::new(|| c"..".as_ptr() as *const libc::c_char as u64);

#[inline(always)]
pub(crate) fn dotdot_with_nul() -> u64 {
    *DOTDOT
}

/// `PathBuf` for UNIX.
// SAFETY: k1 == k2 ⇒ hash(k1) == hash(k2) always holds for our PartialEq impl.
#[allow(clippy::derived_hash_with_manual_eq)]
#[derive(Clone, Hash, Ord, PartialOrd)]
pub struct XPathBuf(Vec<u8>);

impl Default for XPathBuf {
    fn default() -> Self {
        Self(Vec::with_capacity(PATH_MIN))
    }
}

impl Eq for XPathBuf {}

impl PartialEq for XPathBuf {
    fn eq(&self, other: &Self) -> bool {
        is_equal(&self.0, &other.0)
    }
}

impl PartialEq<XPath> for XPathBuf {
    fn eq(&self, other: &XPath) -> bool {
        is_equal(self.as_bytes(), other.as_bytes())
    }
}

impl PartialEq<XPathBuf> for XPath {
    fn eq(&self, other: &XPathBuf) -> bool {
        is_equal(self.as_bytes(), other.as_bytes())
    }
}

impl Deref for XPathBuf {
    type Target = XPath;

    #[inline]
    fn deref(&self) -> &XPath {
        XPath::from_bytes(&self.0)
    }
}

impl Borrow<XPath> for XPathBuf {
    #[inline]
    fn borrow(&self) -> &XPath {
        self.deref()
    }
}

/// A borrowed slice of an XPathBuf.
// SAFETY: k1 == k2 ⇒ hash(k1) == hash(k2) always holds for our PartialEq impl.
#[allow(clippy::derived_hash_with_manual_eq)]
#[repr(transparent)]
#[derive(Hash, Ord, PartialOrd)]
pub struct XPath(OsStr);

impl Eq for XPath {}

impl PartialEq for XPath {
    fn eq(&self, other: &Self) -> bool {
        is_equal(self.0.as_bytes(), other.0.as_bytes())
    }
}

impl ToOwned for XPath {
    type Owned = XPathBuf;

    fn to_owned(&self) -> Self::Owned {
        XPathBuf::from(self.as_bytes())
    }
}

impl AsRef<Path> for XPathBuf {
    fn as_ref(&self) -> &Path {
        self.as_path()
    }
}

impl AsRef<OsStr> for XPathBuf {
    fn as_ref(&self) -> &OsStr {
        self.as_os_str()
    }
}

impl From<&XPath> for XPathBuf {
    fn from(path: &XPath) -> Self {
        path.as_bytes().into()
    }
}

impl From<PathBuf> for XPathBuf {
    fn from(pbuf: PathBuf) -> Self {
        pbuf.into_os_string().into()
    }
}

impl From<&OsStr> for XPathBuf {
    fn from(ostr: &OsStr) -> Self {
        ostr.as_bytes().into()
    }
}

impl From<OsString> for XPathBuf {
    fn from(os: OsString) -> Self {
        Self(os.into_vec())
    }
}

impl From<String> for XPathBuf {
    fn from(s: String) -> Self {
        Self(s.as_bytes().into())
    }
}

impl From<&str> for XPathBuf {
    fn from(s: &str) -> Self {
        Self(s.as_bytes().into())
    }
}

impl From<Cow<'_, str>> for XPathBuf {
    fn from(cow: Cow<'_, str>) -> Self {
        match cow {
            Cow::Borrowed(s) => Self(s.as_bytes().to_vec()),
            Cow::Owned(s) => Self(s.into_bytes()),
        }
    }
}

impl From<&[u8]> for XPathBuf {
    fn from(bytes: &[u8]) -> Self {
        Self(bytes.to_vec())
    }
}

impl From<Vec<u8>> for XPathBuf {
    fn from(vec: Vec<u8>) -> Self {
        Self(vec)
    }
}

impl From<VecDeque<u8>> for XPathBuf {
    fn from(vec: VecDeque<u8>) -> Self {
        Self(vec.into())
    }
}

impl From<pid_t> for XPathBuf {
    fn from(pid: pid_t) -> Self {
        let mut buf = itoa::Buffer::new();
        buf.format(pid).into()
    }
}

impl std::ops::Deref for XPath {
    type Target = Path;

    fn deref(&self) -> &Self::Target {
        self.as_path()
    }
}

impl AsRef<Path> for XPath {
    fn as_ref(&self) -> &Path {
        self.as_path()
    }
}

impl AsRef<OsStr> for XPath {
    fn as_ref(&self) -> &OsStr {
        self.as_os_str()
    }
}

impl std::fmt::Display for XPathBuf {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        // SAFETY: Mask control characters in path.
        write!(f, "{}", mask_path(self.as_path()))
    }
}

impl std::fmt::Debug for XPathBuf {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        // SAFETY: Mask control characters in path.
        write!(f, "{}", mask_path(self.as_path()))
    }
}

impl serde::Serialize for XPathBuf {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        // SAFETY: Display masks control characters.
        serializer.serialize_str(&format!("{self}"))
    }
}

impl NixPath for XPathBuf {
    fn is_empty(&self) -> bool {
        self.0.is_empty()
    }

    fn len(&self) -> usize {
        self.0.len()
    }

    fn with_nix_path<T, F>(&self, f: F) -> Result<T, Errno>
    where
        F: FnOnce(&CStr) -> T,
    {
        self.as_os_str().with_nix_path(f)
    }
}

impl std::fmt::Display for XPath {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        // SAFETY: Mask control characters in path.
        write!(f, "{}", mask_path(self.as_path()))
    }
}

impl std::fmt::Debug for XPath {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        // SAFETY: Mask control characters in path.
        write!(f, "{}", mask_path(self.as_path()))
    }
}

impl serde::Serialize for XPath {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        // SAFETY: Display masks control characters.
        serializer.serialize_str(&format!("{self}"))
    }
}

impl NixPath for XPath {
    fn is_empty(&self) -> bool {
        self.0.is_empty()
    }

    fn len(&self) -> usize {
        self.0.len()
    }

    fn with_nix_path<T, F>(&self, f: F) -> Result<T, Errno>
    where
        F: FnOnce(&CStr) -> T,
    {
        self.as_os_str().with_nix_path(f)
    }
}

impl XPath {
    /// Detects unsafe paths.
    ///
    /// List of restrictions:
    /// 1. Block devices can not be listed with readdir() regardless of path.
    /// 2. readdir(/proc) returns current pid as the only process id.
    /// 3. /proc/$pid where $pid == Syd -> ENOENT.
    /// 4. File name must not contain forbidden characters if `safe_name` is true.
    ///
    /// # SAFETY
    /// 1. `self` must be an absolute pathname.
    /// 2. `self` must be canonicalized and normalized.
    ///
    /// Note, returning error here denies access,
    /// regardless of the state of sandboxing.
    #[inline(always)]
    pub fn check(
        &self,
        pid: Pid,
        file_type: Option<&FileType>,
        dir_entry: Option<&XPath>,
        safe_name: bool,
    ) -> Result<(), Errno> {
        //
        // RESTRICTION 1: Prevent listing block devices and files of unknown type.
        //
        // SAFETY: Prevent listing block devices and files of unknown type.
        if matches!(file_type, Some(FileType::Blk | FileType::Unk)) {
            return Err(Errno::ENOENT);
        }
        // END OF RESTRICTION 1

        //
        // RESTRICTION 2: Restrict file names to allowed characters as necessary.
        //
        // SAFETY: Prevent accessing file names which may be misinterpreted by shells.
        // Note, we skip checking procfs so we don't prevent pipe/socket access
        // unintentionally.
        let is_proc_dir = self.starts_with(b"/proc");
        if !is_proc_dir && safe_name && self.check_name().is_err() {
            return Err(Errno::EINVAL);
        }
        // END OF RESTRICTION 2

        // Remaining restrictions apply to procfs only.
        let (is_proc, proc_pid) = if is_proc_dir {
            const LEN: usize = b"/proc".len();
            let mut proc_pid = None;
            let is_proc = self.len() == LEN;

            if is_proc {
                // If this is `/proc' directory entries may refer to PIDs.
                if let Some(p) = dir_entry {
                    proc_pid = btoi::<libc::pid_t>(p.as_bytes()).ok();
                }
            }

            if proc_pid.is_none()
                && self
                    .get(LEN + 1)
                    .map(|c| c.is_ascii_digit())
                    .unwrap_or(false)
            {
                let path = self.as_bytes();
                let path = &path[LEN + 1..];
                let pidx = memchr(b'/', path).unwrap_or(path.len());
                proc_pid = btoi::<libc::pid_t>(&path[..pidx]).ok();
            }

            (is_proc, proc_pid)
        } else {
            return Ok(());
        };
        let proc_pid = if let Some(pid) = proc_pid {
            pid
        } else {
            return Ok(());
        };

        //
        // RESTRICTION 2: Protect readdir(/proc).
        //
        // SAFETY: Prevent /proc process tree traversal.
        if is_proc && proc_pid != pid.as_raw() {
            return Err(Errno::ENOENT);
        }
        // END OF RESTRICTION 2

        //
        // RESTRICTION 3: Protect Syd procfs.
        //
        // SAFETY: Protect Syd /proc directory!
        //
        // Step 1: Protect Syd thread group.
        let syd_pid = Pid::this().as_raw();
        if proc_pid == syd_pid {
            return Err(Errno::ENOENT);
        }
        //
        // Step 2: Protect all Syd threads.
        //
        // SAFETY: In libc we trust.
        if unsafe { libc::syscall(nix::libc::SYS_tgkill, syd_pid, proc_pid, 0) } == 0 {
            return Err(Errno::ENOENT);
        }
        // END OF RESTRICTION 3

        // TODO: Add more restrictions as needed.
        Ok(())
    }

    /// Validates a filename based on David A. Wheeler's Safename Linux
    /// Security Module (LSM) rules.
    ///
    /// This function checks if a given filename (not the entire path)
    /// adheres to specific security policies inspired by Wheeler's
    /// Safename LSM. These policies are designed to prevent the
    /// creation of filenames that could be used for malicious purposes,
    /// such as exploiting poorly written scripts or programs.
    ///
    /// The validation rules are:
    ///
    /// 1. **Non-Empty Filename**: The filename must not be empty.
    ///
    /// 2. **Valid UTF-8 Encoding**: The filename must be valid UTF-8.
    ///
    /// 3. **Permitted Characters**:
    ///    - **Initial Byte**: Must be an allowed character, but cannot be:
    ///        - Space `' '` (0x20)
    ///        - Hyphen `'-'` (0x2D)
    ///        - Tilde `'~'` (0x7E)
    ///    - **Middle Bytes**: Each must be an allowed character (if any).
    ///    - **Final Byte**: Must be an allowed character, but cannot be:
    ///        - Space `' '` (0x20)
    ///
    /// 4. **Allowed Character Set**:
    ///    - ASCII printable characters from space `' '` (0x20) to tilde `'~'` (0x7E), inclusive.
    ///    - Extended ASCII characters from 0x80 to 0xFE, inclusive.
    ///    - **Excludes** control characters (0x00-0x1F), delete (0x7F), and 0xFF.
    ///
    /// # Returns
    ///
    /// * `Ok(())` if the filename is valid and safe.
    /// * `Err(Errno::EINVAL)` if the filename is invalid or unsafe.
    ///
    /// # Errors
    ///
    /// Returns `Err(Errno::EINVAL)` if any of the validation rules are not met.
    ///
    /// # Security
    ///
    /// Enforcing these rules helps prevent security vulnerabilities
    /// arising from unexpected or malicious filenames, such as command
    /// injection, denial of service, or arbitrary file manipulation.
    #[allow(clippy::arithmetic_side_effects)]
    pub fn check_name(&self) -> Result<(), Errno> {
        let (_, name) = self.split();
        let name = name.as_bytes();
        let len = name.len();

        if len == 0 {
            return Err(Errno::EINVAL);
        }

        // Check if the filename is valid UTF-8.
        if std::str::from_utf8(name).is_err() {
            return Err(Errno::EINVAL);
        }

        let first_byte = name[0];
        let last_byte = name[len - 1];

        // Check the first byte.
        if !is_permitted_initial(first_byte) {
            return Err(Errno::EINVAL);
        }

        // Check the middle bytes (if any).
        match len {
            2 => {
                // Only one middle byte to check.
                let middle_byte = name[1];
                if !is_permitted_middle(middle_byte) {
                    return Err(Errno::EINVAL);
                }
            }
            n if n > 2 => {
                for &b in &name[1..len - 1] {
                    if !is_permitted_middle(b) {
                        return Err(Errno::EINVAL);
                    }
                }
            }
            _ => {}
        }

        // Check the last byte.
        if !is_permitted_final(last_byte) {
            return Err(Errno::EINVAL);
        }

        Ok(())
    }

    /// Returns a path that, when joined onto `base`, yields `self`.
    ///
    /// Expects normalized, canonical path.
    #[allow(clippy::arithmetic_side_effects)]
    pub fn split_prefix(&self, base: &[u8]) -> Option<&Self> {
        let mut len = base.len();
        if len == 0 {
            return None;
        } else if base == b"/" {
            return Some(self);
        }

        let base = if base[len - 1] == b'/' {
            len -= 1;
            &base[..len - 1]
        } else {
            base
        };

        if !self.starts_with(base) {
            return None;
        }

        let raw = self.as_bytes();
        let len_raw = raw.len();
        if len == len_raw {
            Some(XPath::from_bytes(b""))
        } else if len_raw < len + 1 || raw[len] != b'/' {
            None
        } else {
            Some(XPath::from_bytes(&raw[len + 1..]))
        }
    }

    /// Splits a given path into the parent path and the file name.
    ///
    /// - The function efficiently finds the last `/` in the path and splits at that point.
    /// - Trailing slashes are included in the filename to indicate directory paths.
    /// - For the root path `/`, both parent and filename are the original path reference.
    #[allow(clippy::arithmetic_side_effects)]
    pub fn split(&self) -> (&Self, &Self) {
        // Special cases for the empty and root paths.
        let bytes = match self.get(0) {
            None => return (XPath::from_bytes(b""), XPath::from_bytes(b"")),
            Some(b'/') if self.0.len() == 1 => {
                return (
                    XPath::from_bytes(&self.as_bytes()[..1]),
                    XPath::from_bytes(&self.as_bytes()[..1]),
                )
            }
            _ => self.as_bytes(),
        };

        // Determine if the path ends with a trailing slash.
        let has_trailing_slash = bytes[bytes.len() - 1] == b'/';
        let effective_length = if has_trailing_slash && bytes.len() > 1 {
            bytes.len() - 1
        } else {
            bytes.len()
        };
        let last_slash_index = memrchr(b'/', &bytes[..effective_length]);

        if let Some(idx) = last_slash_index {
            let parent_path = if idx == 0 {
                // The slash is at the beginning, so the parent is root.
                XPath::from_bytes(b"/")
            } else {
                // Take everything up to the last non-trailing slash.
                XPath::from_bytes(&bytes[..idx])
            };

            let filename_start = idx + 1;
            let filename_end = if has_trailing_slash {
                bytes.len()
            } else {
                effective_length
            };
            let filename_path = XPath::from_bytes(&bytes[filename_start..filename_end]);

            return (parent_path, filename_path);
        }

        // If no slash is found, the whole thing is the filename!
        (XPath::from_bytes(b""), self)
    }

    /// Returns a reference to the file extension.
    pub fn extension(&self) -> Option<&Self> {
        let dot = memrchr(b'.', self.as_bytes())?;
        // dot==Some means len>=1.
        #[allow(clippy::arithmetic_side_effects)]
        if dot < self.0.len() - 1 {
            Some(Self::from_bytes(&self.as_bytes()[dot + 1..]))
        } else {
            None
        }
    }

    /// Returns a reference to the parent path.
    pub fn parent(&self) -> &Self {
        Self::from_bytes(&self.as_bytes()[..self.parent_len()])
    }

    /// Determines the length of the parent path.
    #[allow(clippy::arithmetic_side_effects)]
    pub fn parent_len(&self) -> usize {
        // Special cases for the empty and root paths.
        let bytes = match self.get(0) {
            None => return 0,
            Some(b'/') if self.len() == 1 => return 1,
            _ => self.as_bytes(),
        };

        // Determine if the path ends with a trailing slash.
        let has_trailing_slash = bytes[bytes.len() - 1] == b'/';
        let effective_length = if has_trailing_slash && bytes.len() > 1 {
            bytes.len() - 1
        } else {
            bytes.len()
        };
        let last_slash_index = memrchr(b'/', &bytes[..effective_length]);

        if let Some(idx) = last_slash_index {
            return if idx == 0 {
                // The slash is at the beginning, so the parent is root.
                1
            } else {
                // Take everything up to the last non-trailing slash.
                idx
            };
        }

        // If no slash is found, the whole thing is the filename!
        0
    }

    /// Return the depth of the path.
    ///
    /// The depth of a path is equal to the number of directory separators in it.
    pub fn depth(&self) -> usize {
        memchr::arch::all::memchr::One::new(b'/').count(self.as_bytes())
    }

    /// Check if path is a descendant of the given `root` path (RESOLVE_BENEATH compatible).
    /// Both paths must be canonicalized.
    pub fn descendant_of(&self, root: &[u8]) -> bool {
        if is_equal(root, b"/") {
            // Every absolute path is a descendant of "/".
            return true;
        } else if !self.starts_with(root) {
            // `self` does not begin with `root`.
            return false;
        }

        let slen = self.len();
        let rlen = root.len();

        match slen.cmp(&rlen) {
            Ordering::Less => false,
            Ordering::Equal => true,
            Ordering::Greater => self.get(rlen) == Some(b'/'),
        }
    }

    /// Returns a path that, when joined onto `base`, yields `self`.
    ///
    /// # Safety
    ///
    /// Assumes `self` is normalized.
    ///
    /// # Errors
    ///
    /// If `base` is not a prefix of self (i.e., `starts_with` returns
    /// `false`), returns `Err`.
    pub fn strip_prefix(&self, base: &[u8]) -> Option<&Self> {
        if !self.starts_with(base) {
            return None;
        }

        // Determine the remainder after the base.
        let remainder = &self.as_bytes()[base.len()..];

        // Check if there is anything left after the base.
        if remainder.is_empty() {
            // If the remainder is empty, return an empty path.
            Some(Self::from_bytes(b""))
        } else if remainder[0] == b'/' {
            // Return the slice after the '/', ensuring no leading '/' in the result
            // This is safe due to the assumption of normalized paths.
            Some(Self::from_bytes(&remainder[1..]))
        } else {
            // If the path doesn't start with '/', it means base is not a directory prefix.
            None
        }
    }

    /// Checks if the path ends with a dot component.
    ///
    /// This function iterates through the bytes of the path from end to
    /// start, and determines whether the last component before any
    /// slashes is a dot.
    #[allow(clippy::arithmetic_side_effects)]
    #[allow(clippy::if_same_then_else)]
    pub fn ends_with_dot(&self) -> bool {
        let bytes = self.as_bytes();

        // Start from the end of the string and move backwards.
        let mut index = bytes.len();
        if index == 0 {
            return false;
        }

        // Skip trailing slashes.
        while index > 0 && bytes[index - 1] == b'/' {
            index -= 1;
        }

        // If the path is empty after removing trailing slashes,
        // it does not end with a dot.
        if index == 0 {
            return false;
        }

        // Check for '.' or '..'
        if bytes[index - 1] == b'.' {
            if index == 1 || bytes[index - 2] == b'/' {
                return true; // Matches '.' or '*/.'
            } else if index > 1
                && bytes[index - 2] == b'.'
                && (index == 2 || bytes[index - 3] == b'/')
            {
                return true; // Matches '..' or '*/..'
            }
        }

        false
    }

    /// Returns true if the path ends with a slash.
    pub fn ends_with_slash(&self) -> bool {
        !self.is_rootfs() && self.last() == Some(b'/')
    }

    /// Check if path has a parent dir component, ie `..`.
    pub fn has_parent_dot(&self) -> bool {
        self.contains(b"/..") || self.is_equal(b"..")
    }

    /// Check if path starts with the `MAGIC_PREFIX`.
    pub fn is_magic(&self) -> bool {
        self.starts_with(MAGIC_PREFIX)
    }

    /// Check if path is the root path, ie `/`.
    pub fn is_rootfs(&self) -> bool {
        self.as_bytes().iter().all(|b| *b == b'/')
    }

    /// Check if path points to devfs root dir, ie. `/dev`.
    pub fn is_devfs(&self) -> bool {
        const DEV_LEN: usize = b"/dev".len();
        const DEV_DIR_LEN: usize = b"/dev/".len();

        match self.len() {
            DEV_LEN if self.is_equal(b"/dev") => true,
            DEV_DIR_LEN if self.is_equal(b"/dev/") => true,
            _ => false,
        }
    }

    /// Check if path points to procfs root dir, ie. `/proc`.
    pub fn is_procfs(&self) -> bool {
        const PROC_LEN: usize = b"/proc".len();
        const PROC_DIR_LEN: usize = b"/proc/".len();

        match self.len() {
            PROC_LEN if self.is_equal(b"/proc") => true,
            PROC_DIR_LEN if self.is_equal(b"/proc/") => true,
            _ => false,
        }
    }

    /// Check if path points to sysfs root dir, ie. `/sys`.
    pub fn is_sysfs(&self) -> bool {
        const SYS_LEN: usize = b"/sys".len();
        const SYS_DIR_LEN: usize = b"/sys/".len();

        match self.len() {
            SYS_LEN if self.is_equal(b"/sys") => true,
            SYS_DIR_LEN if self.is_equal(b"/sys/") => true,
            _ => false,
        }
    }

    /// Check if path points to devfs, ie. starts with `/dev`.
    /// The literal path `/dev` returns false.
    pub fn is_dev(&self) -> bool {
        self.starts_with(b"/dev/")
    }

    /// Check if path points to procfs, ie. starts with `/proc`.
    /// The literal path `/proc` returns false.
    pub fn is_proc(&self) -> bool {
        self.starts_with(b"/proc/")
    }

    /// Check if path points to sysfs, ie. starts with `/sys`.
    /// The literal path `/sys` returns false.
    pub fn is_sys(&self) -> bool {
        self.starts_with(b"/sys/")
    }

    /// Check if path points to a static path.
    /// See proc_init in config.rs
    pub fn is_static(&self) -> bool {
        self.is_rootfs()
            || self.is_devfs()
            || self.is_procfs()
            || self.is_sysfs()
            || self.is_equal(b"/dev/null")
    }

    /// Check if path points to per-process procfs directory, ie. starts with `/proc/$pid`.
    /// `/proc/$pid` is also accepted among with all descendants of it.
    pub fn is_proc_pid(&self) -> bool {
        if !self.is_proc() {
            return false;
        }
        match self.get("/proc/".len()) {
            Some(n) => n.is_ascii_digit(),
            None => false,
        }
    }

    /// Check if path points to the `/proc/self` link.
    /// If `thread` is true, checks for `/proc/thread-self`.
    pub fn is_proc_self(&self, thread: bool) -> bool {
        if thread {
            is_equal(self.as_bytes(), b"/proc/thread-self")
        } else {
            is_equal(self.as_bytes(), b"/proc/self")
        }
    }

    /// Check if path exists.
    #[allow(clippy::disallowed_methods)]
    pub fn exists(&self, follow: bool) -> bool {
        let flags = if self.is_empty() {
            return false;
        } else if !follow {
            OFlag::O_NOFOLLOW
        } else {
            OFlag::empty()
        };

        let mut how = OpenHow::new().flags(flags | OFlag::O_PATH | OFlag::O_CLOEXEC);
        if !follow {
            how =
                how.resolve(ResolveFlag::RESOLVE_NO_MAGICLINKS | ResolveFlag::RESOLVE_NO_SYMLINKS);
        }

        retry_on_eintr(|| openat2(libc::AT_FDCWD, self, how))
            .map(close)
            .is_ok()
    }

    /// Check if path is a symlink.
    pub fn is_symlink(&self) -> bool {
        self.as_path().is_symlink()
    }

    /// Check if path is a dir.
    pub fn is_dir(&self) -> bool {
        self.as_path().is_dir()
    }

    /// Check if path is a file.
    pub fn is_file(&self) -> bool {
        self.as_path().is_file()
    }

    /// Check if path is absolute.
    pub fn is_absolute(&self) -> bool {
        self.first() == Some(b'/')
    }

    /// Check if path is relative.
    ///
    /// Empty path is considered relative.
    pub fn is_relative(&self) -> bool {
        !self.is_absolute()
    }

    /// Checks if the path consists only of "." components.
    pub fn is_dot(&self) -> bool {
        let bytes = self.as_bytes();

        if bytes.is_empty() {
            return false;
        }

        let mut has_component = false;

        for component in bytes.split(|&b| b == b'/') {
            if component.is_empty() {
                continue;
            }
            has_component = true;
            if component != b"." {
                return false;
            }
        }

        has_component
    }

    /// Determine whether path is equal to the given string.
    pub fn is_equal(&self, s: &[u8]) -> bool {
        is_equal(self.as_bytes(), s)
    }

    /// Determine whether base is a prefix of path.
    pub fn starts_with(&self, base: &[u8]) -> bool {
        is_prefix(self.as_bytes(), base)
    }

    /// Determine whether base is a suffix of path.
    pub fn ends_with(&self, base: &[u8]) -> bool {
        is_suffix(self.as_bytes(), base)
    }

    /// Determine whether path contains the given substring.
    pub fn contains(&self, sub: &[u8]) -> bool {
        memmem::find_iter(self.as_bytes(), sub).next().is_some()
    }

    /// Determine whether path contains the given character.
    pub fn contains_char(&self, c: u8) -> bool {
        memchr(c, self.as_bytes()).is_some()
    }

    /// Returns the first character of the path.
    /// Empty path returns None.
    pub fn first(&self) -> Option<u8> {
        self.as_bytes().first().copied()
    }

    /// Returns the last character of the path.
    /// Empty path returns None.
    pub fn last(&self) -> Option<u8> {
        self.as_bytes().last().copied()
    }

    /// Returns the character at the specified index.
    /// Returns None if path is shorter.
    pub fn get(&self, index: usize) -> Option<u8> {
        self.as_bytes().get(index).copied()
    }

    /// Convert to a `Path`.
    pub fn as_path(&self) -> &Path {
        Path::new(self.as_os_str())
    }

    /// Creates an owned `XPathBuf` with path adjoined to `self`.
    /// If `path` is absolute, it replaces the current path.
    pub fn join(&self, path: &[u8]) -> XPathBuf {
        let mut owned = self.to_owned();
        owned.push(path);
        owned
    }

    /// Returns an immutable slice of the buffer.
    pub fn as_bytes(&self) -> &[u8] {
        self.0.as_bytes()
    }

    /// Convert to a `OsStr`.
    pub fn as_os_str(&self) -> &OsStr {
        &self.0
    }

    /// Create a new `XPath` from a byte slice.
    pub fn from_bytes(slice: &[u8]) -> &XPath {
        // SAFETY: XPath has repr(transparent)
        unsafe { std::mem::transmute(slice) }
    }

    /// Create a new `XPath` for the dotdot path, aka `..`
    pub fn dotdot() -> &'static XPath {
        XPath::from_bytes(b"..")
    }

    /// Create a new `XPath` for the dot path, aka `.`
    pub fn dot() -> &'static XPath {
        XPath::from_bytes(b".")
    }

    /// Create a new `XPath` for the root path, aka `/`
    pub fn root() -> &'static XPath {
        XPath::from_bytes(b"/")
    }

    /// Create a new, empty `XPath`
    pub fn empty() -> &'static XPath {
        XPath::from_bytes(b"")
    }

    /// Create a new `XPath` from a byte slice.
    pub fn new<S: AsRef<OsStr> + ?Sized>(s: &S) -> &XPath {
        // SAFETY: XPath has repr(transparent).
        unsafe { &*(s.as_ref() as *const OsStr as *const XPath) }
    }
}

impl XPathBuf {
    /// Removes consecutive slashes (`/`) from the path in-place,
    /// replacing them with a single slash.
    ///
    /// This method modifies `self` directly.
    pub fn clean_consecutive_slashes(&mut self) {
        let len = match self.len() {
            0 | 1 => return,
            n => n,
        };

        let mut write_pos = 0;
        let mut read_pos = 0;
        #[allow(clippy::arithmetic_side_effects)]
        while read_pos < len {
            if self.0[read_pos] == b'/' {
                // Write a single slash.
                self.0[write_pos] = b'/';
                write_pos += 1;
                read_pos += 1;

                // Skip over consecutive slashes.
                while read_pos < len && self.0[read_pos] == b'/' {
                    read_pos += 1;
                }
            } else {
                // Find the next slash using memchr for efficiency.
                let next_slash = memchr(b'/', &self.0[read_pos..])
                    .map(|pos| pos + read_pos)
                    .unwrap_or(len);

                let segment_len = next_slash - read_pos;

                // Copy the segment of non-slash bytes to the write position if needed.
                if read_pos != write_pos {
                    self.0.copy_within(read_pos..next_slash, write_pos);
                }

                write_pos += segment_len;
                read_pos = next_slash;
            }
        }

        // Truncate the vector to the new length.
        self.0.truncate(write_pos);
    }

    /// Create a path from the given PID.
    pub fn from_pid(pid: Pid) -> Self {
        let mut buf = itoa::Buffer::new();
        buf.format(pid.as_raw()).as_bytes().into()
    }

    /// Create a path from the given FD.
    pub fn from_fd(fd: RawFd) -> Self {
        let mut buf = itoa::Buffer::new();
        buf.format(fd).as_bytes().into()
    }

    /// Append the formatted FD as a new component.
    pub fn push_pid(&mut self, pid: Pid) {
        let mut buf = itoa::Buffer::new();
        self.push(buf.format(pid.as_raw()).as_bytes())
    }

    /// Append the formatted FD as a new component.
    pub fn push_fd(&mut self, fd: RawFd) {
        let mut buf = itoa::Buffer::new();
        self.push(buf.format(fd).as_bytes())
    }

    /// Append a path component, managing separators correctly.
    pub fn push(&mut self, path: &[u8]) {
        if path.first() == Some(&b'/') {
            // Absolute path replaces pbuf.
            self.0.clear();
        } else if self.last().map(|c| c != b'/').unwrap_or(true) {
            // Add separator if needed (last!=/ or empty path).
            self.append_byte(b'/');
        }
        // Append new path part.
        self.append_bytes(path);
    }

    /// Remove the last path component.
    pub fn pop(&mut self) {
        self.truncate(self.parent_len());
    }

    /// Remove the last path component without checks.
    ///
    /// # Safety
    ///
    /// 1. Path must be a normalized absolute path!
    /// 2. Path must not have a trailing slash!
    #[inline]
    pub unsafe fn pop_unchecked(&mut self) {
        #[allow(clippy::arithmetic_side_effects)]
        if let Some(idx) = memrchr(b'/', &self.as_bytes()[1..]) {
            self.0.truncate(idx + 1);
        } else if self.0.len() > 1 {
            self.0.truncate(1);
        }
    }

    /// Append raw bytes to the path buffer.
    pub fn append_bytes(&mut self, bytes: &[u8]) {
        self.0.extend(bytes.iter().copied())
    }

    /// Append a raw byte to the path buffer.
    pub fn append_byte(&mut self, byte: u8) {
        self.0.push(byte)
    }

    /// Remove the last byte and return it or None if path is empty.
    pub fn pop_last(&mut self) -> Option<u8> {
        self.0.pop()
    }

    /// Convert a `XPathBuf` to a `Vec`.
    pub fn into_vec(self) -> Vec<u8> {
        self.0
    }

    /// Convert a `XPathBuf` to an `OsString`.
    pub fn into_os_string(self) -> OsString {
        OsString::from_vec(self.0)
    }

    /// Shorten the vector, keeping the first len elements and dropping
    /// the rest. If len is greater than or equal to the vector’s
    /// current length, this has no effect.
    pub fn truncate(&mut self, len: usize) {
        self.0.truncate(len)
    }

    /// Removes and returns the element at position index within the
    /// vector, shifting all elements after it to the left.
    pub fn remove(&mut self, index: usize) -> u8 {
        self.0.remove(index)
    }

    /// Shrink the capacity of the vector as much as possible.
    ///
    /// When possible, this will move data from an external heap buffer
    /// to the vector’s inline storage.
    pub fn shrink_to_fit(&mut self) {
        self.0.shrink_to_fit()
    }

    /// Reserve capacity for additional more bytes to be inserted.
    /// May reserve more space to avoid frequent allocations.
    pub fn try_reserve(&mut self, additional: usize) -> Result<(), Errno> {
        self.0.try_reserve(additional).or(Err(Errno::ENOMEM))
    }

    /// Create a new, empty `XPathBuf`.
    pub fn empty() -> Self {
        Self(vec![])
    }

    /// Construct an empty `XPathBuf` with capacity pre-allocated.
    pub fn with_capacity(n: usize) -> Self {
        Self(Vec::with_capacity(n))
    }

    /// Report capacity of path.
    pub fn capacity(&self) -> usize {
        self.0.capacity()
    }

    /// Creates an owned `XPathBuf` with path adjoined to `self`.
    /// If `path` is absolute, it replaces the current path.
    pub fn join(&self, path: &[u8]) -> XPathBuf {
        let mut owned = self.clone();
        owned.push(path);
        owned
    }

    /// Returns an immutable slice of the buffer.
    pub fn as_bytes(&self) -> &[u8] {
        &self.0
    }

    /// Convert to a `OsStr`.
    pub fn as_os_str(&self) -> &OsStr {
        OsStr::from_bytes(&self.0)
    }

    /// Convert to a `Path`.
    pub fn as_path(&self) -> &Path {
        Path::new(self.as_os_str())
    }

    /// Check if path is a symlink.
    pub fn is_symlink(&self) -> bool {
        self.as_path().is_symlink()
    }

    /// Check if path is a dir.
    pub fn is_dir(&self) -> bool {
        self.as_path().is_dir()
    }

    /// Check if path is a file.
    pub fn is_file(&self) -> bool {
        self.as_path().is_file()
    }
}

/// Logs an untrusted Path, escaping it as hex if it contains control
/// characters.
#[inline]
pub fn mask_path(path: &Path) -> String {
    let (mask, _) = log_untrusted_buf(path.as_os_str().as_bytes());
    mask
}

#[inline]
fn is_permitted_initial(b: u8) -> bool {
    is_permitted_byte(b) && !matches!(b, b'-' | b' ' | b'~')
}

#[inline]
fn is_permitted_middle(b: u8) -> bool {
    is_permitted_byte(b)
}

#[inline]
fn is_permitted_final(b: u8) -> bool {
    is_permitted_byte(b) && b != b' '
}

#[inline]
fn is_permitted_byte(b: u8) -> bool {
    match b {
        b'*' | b'?' | b':' | b'[' | b']' | b'"' | b'<' | b'>' | b'|' | b'(' | b')' | b'{'
        | b'}' | b'&' | b'\'' | b'!' | b'\\' | b';' | b'$' | b'`' => false,
        0x20..=0x7E => true,
        0x80..=0xFE => true,
        _ => false,
    }
}

#[cfg(test)]
mod tests {
    use std::{sync::mpsc, thread};

    use nix::unistd::{gettid, pause};

    use super::*;

    struct CCSTestCase<'a> {
        src: &'a str,
        dst: &'a str,
    }

    const CCS_TESTS: &[CCSTestCase] = &[
        CCSTestCase { src: "/", dst: "/" },
        CCSTestCase {
            src: "///",
            dst: "/",
        },
        CCSTestCase {
            src: "////",
            dst: "/",
        },
        CCSTestCase {
            src: "//home/alip///",
            dst: "/home/alip/",
        },
        CCSTestCase {
            src: "//home/alip///.config///",
            dst: "/home/alip/.config/",
        },
        CCSTestCase {
            src: "//home/alip///.config///htop////",
            dst: "/home/alip/.config/htop/",
        },
        CCSTestCase {
            src: "//home/alip///.config///htop////htoprc",
            dst: "/home/alip/.config/htop/htoprc",
        },
    ];

    #[test]
    fn test_clean_consecutive_slashes() {
        for (idx, test) in CCS_TESTS.iter().enumerate() {
            let mut path = XPathBuf::from(test.src);
            path.clean_consecutive_slashes();
            assert_eq!(
                path,
                XPathBuf::from(test.dst),
                "Test {idx}: {} -> {path} != {}",
                test.src,
                test.dst
            );
        }
    }

    struct EndsWithDotTestCase<'a> {
        path: &'a str,
        test: bool,
    }

    const ENDS_WITH_DOT_TESTS: &[EndsWithDotTestCase] = &[
        EndsWithDotTestCase {
            path: ".",
            test: true,
        },
        EndsWithDotTestCase {
            path: "..",
            test: true,
        },
        EndsWithDotTestCase {
            path: "...",
            test: false,
        },
        EndsWithDotTestCase {
            path: "/.",
            test: true,
        },
        EndsWithDotTestCase {
            path: "/..",
            test: true,
        },
        EndsWithDotTestCase {
            path: "/...",
            test: false,
        },
        EndsWithDotTestCase {
            path: "foo.",
            test: false,
        },
        EndsWithDotTestCase {
            path: "foo./.",
            test: true,
        },
        EndsWithDotTestCase {
            path: "foo/./././/./",
            test: true,
        },
        EndsWithDotTestCase {
            path: "conftest.dir/././././////",
            test: true,
        },
    ];

    #[test]
    fn test_ends_with_dot() {
        for (idx, test) in ENDS_WITH_DOT_TESTS.iter().enumerate() {
            let ends = XPath::from_bytes(test.path.as_bytes()).ends_with_dot();
            assert_eq!(
                test.test, ends,
                "EndsWithDotTestCase {} -> \"{}\": {} != {}",
                idx, test.path, test.test, ends
            );
        }
    }

    #[test]
    fn test_is_dot() {
        let cases = [
            (".", true),
            ("./", true),
            (".///", true),
            ("././", true),
            ("./././", true),
            ("././././", true),
            ("/././", true),
            ("/./././", true),
            (".//././", true),
            ("", false),
            ("/", false),
            ("..", false),
            ("./..", false),
            ("../", false),
            ("././..", false),
            ("./../", false),
            ("./a", false),
            ("a/.", false),
            ("././a", false),
            ("a/./.", false),
            ("./././..", false),
            ("./.hidden", false),
            ("././.hidden", false),
            ("some/./path", false),
            ("./some/path", false),
            ("some/path/.", false),
            ("/some/path", false),
        ];

        for &(input, expected) in &cases {
            let path = XPath::from_bytes(input.as_bytes());
            assert_eq!(path.is_dot(), expected, "Failed on input: {:?}", input);
        }
    }

    #[test]
    fn test_descendant_of() {
        let cases = [
            ("/", "/", true),
            ("/foo", "/", true),
            ("/foo/bar", "/", true),
            ("/foo", "/foo", true),
            ("/foo/bar", "/foo", true),
            ("/foo2", "/foo", false),
            ("/foot", "/foo", false),
            ("/fo", "/foo", false),
            ("/", "/foo", false),
            ("/foo/bar", "/foo/bar", true),
            ("/foo/bar/baz", "/foo/bar", true),
            ("/foo/barbaz", "/foo/bar", false),
            ("/foo", "/foo/bar", false),
        ];

        for &(path, root, expected) in &cases {
            let path = XPath::from_bytes(path.as_bytes());
            assert_eq!(
                path.descendant_of(root.as_bytes()),
                expected,
                "Failed on input: {path:?} of {root}!"
            );
        }
    }

    #[test]
    fn test_path_check_file_type() {
        assert!(XPathBuf::from("/proc")
            .check(Pid::from_raw(1), Some(&FileType::Dir), None, true)
            .is_ok());
        assert!(XPathBuf::from("/proc")
            .check(
                Pid::from_raw(1),
                Some(&FileType::Dir),
                Some(&XPath::from_bytes(b"self")),
                true,
            )
            .is_ok());
        assert!(XPathBuf::from("/proc")
            .check(
                Pid::from_raw(1),
                Some(&FileType::Reg),
                Some(&XPath::from_bytes(b"uptime")),
                true,
            )
            .is_ok());
        assert!(XPathBuf::from("/dev/null")
            .check(Pid::from_raw(1), Some(&FileType::Chr), None, true)
            .is_ok());
        assert!(XPathBuf::from("/dev/log")
            .check(Pid::from_raw(1), Some(&FileType::Sock), None, true)
            .is_ok());
        assert!(XPathBuf::from("/dev/fifo")
            .check(Pid::from_raw(1), Some(&FileType::Fifo), None, true)
            .is_ok());
        assert!(XPathBuf::from("/dev/sda1")
            .check(Pid::from_raw(1), Some(&FileType::Blk), None, true)
            .is_err());
        assert!(XPathBuf::from("/dev/lmao")
            .check(Pid::from_raw(1), Some(&FileType::Unk), None, true)
            .is_err());
    }

    #[test]
    fn test_path_check_procfs() {
        let this = Pid::from_raw(128);
        let that = Pid::from_raw(256);
        assert!(XPathBuf::from("/proc")
            .check(this, Some(&FileType::Dir), Some(&xpath!("{this}")), true,)
            .is_ok());
        assert!(XPathBuf::from(format!("/proc/{this}"))
            .check(
                this,
                Some(&FileType::Reg),
                Some(&XPath::from_bytes(b"mem")),
                true,
            )
            .is_ok());
        assert!(XPathBuf::from(format!("/proc/{this}"))
            .check(
                this,
                Some(&FileType::Dir),
                Some(&XPath::from_bytes(b"")),
                true,
            )
            .is_ok());
        assert!(XPathBuf::from(format!("/proc/{this}/task"))
            .check(this, Some(&FileType::Dir), Some(&xpath!("{this}")), true,)
            .is_ok());
        assert!(XPathBuf::from("/proc")
            .check(this, Some(&FileType::Dir), Some(&xpath!("{that}")), true,)
            .is_err());
        assert!(XPathBuf::from(format!("/proc/{that}"))
            .check(
                this,
                Some(&FileType::Reg),
                Some(&XPath::from_bytes(b"")),
                true,
            )
            .is_ok());
        assert!(XPathBuf::from(format!("/proc/{that}"))
            .check(
                this,
                Some(&FileType::Dir),
                Some(&XPath::from_bytes(b"")),
                true,
            )
            .is_ok());
        assert!(XPathBuf::from(format!("/proc/{that}/task"))
            .check(this, Some(&FileType::Dir), Some(&xpath!("{that}")), true,)
            .is_ok());
    }

    #[test]
    fn test_path_check_procfs_syd_leader() {
        let syd = Pid::this();
        assert!(XPathBuf::from("/proc")
            .check(syd, Some(&FileType::Dir), Some(&xpath!("{syd}")), true,)
            .is_err());
        assert!(XPathBuf::from(format!("/proc/{syd}"))
            .check(
                syd,
                Some(&FileType::Reg),
                Some(&XPath::from_bytes(b"")),
                true,
            )
            .is_err());
        assert!(XPathBuf::from(format!("/proc/{syd}"))
            .check(
                syd,
                Some(&FileType::Dir),
                Some(&XPath::from_bytes(b"")),
                true,
            )
            .is_err());
        assert!(XPathBuf::from(format!("/proc/{syd}/task"))
            .check(syd, Some(&FileType::Dir), Some(&xpath!("{syd}")), true,)
            .is_err());
    }

    #[test]
    fn test_path_check_procfs_syd_thread() {
        // Spawn a new thread.
        let tid = {
            let (tx, rx) = mpsc::channel();
            thread::spawn(move || {
                tx.send(gettid()).unwrap();
                pause();
            });
            rx.recv().unwrap()
        };
        assert!(XPathBuf::from("/proc")
            .check(tid, Some(&FileType::Dir), Some(&xpath!("{tid}")), true,)
            .is_err());
        assert!(XPathBuf::from(format!("/proc/{tid}"))
            .check(
                tid,
                Some(&FileType::Reg),
                Some(&XPath::from_bytes(b"")),
                true,
            )
            .is_err());
        assert!(XPathBuf::from(format!("/proc/{tid}"))
            .check(
                tid,
                Some(&FileType::Dir),
                Some(&XPath::from_bytes(b"")),
                true,
            )
            .is_err());
        assert!(XPathBuf::from(format!("/proc/{tid}/task"))
            .check(tid, Some(&FileType::Dir), Some(&xpath!("{tid}")), true,)
            .is_err());
    }

    #[test]
    fn test_path_split_prefix_absolute() {
        let path = XPathBuf::from("/tmp/foo/bar/baz");

        assert_eq!(path.split_prefix(b"/").unwrap().as_bytes(), path.as_bytes());

        assert!(path.split_prefix(b"/tm").is_none());
        assert_eq!(
            path.split_prefix(b"/tmp").unwrap().as_bytes(),
            b"foo/bar/baz"
        );

        assert!(path.split_prefix(b"/tmp/f").is_none());
        assert_eq!(
            path.split_prefix(b"/tmp/foo/").unwrap().as_bytes(),
            b"bar/baz"
        );

        assert_eq!(
            path.split_prefix(b"/tmp/foo/bar/baz").unwrap().as_bytes(),
            b""
        );
    }

    #[test]
    fn test_path_split_prefix_relative() {
        let path = XPathBuf::from("tmp/foo/bar/baz");

        assert!(path.split_prefix(b"t").is_none());
        assert!(path.split_prefix(b"tm").is_none());

        assert_eq!(
            path.split_prefix(b"tmp").unwrap().as_bytes(),
            b"foo/bar/baz"
        );
        assert_eq!(
            path.split_prefix(b"tmp/").unwrap().as_bytes(),
            b"foo/bar/baz"
        );

        assert_eq!(
            path.split_prefix(b"tmp/foo/bar/baz").unwrap().as_bytes(),
            b""
        );
    }

    #[test]
    fn test_path_pop_unchecked() {
        let mut path = XPathBuf::from("/usr/host/bin/id");
        unsafe { path.pop_unchecked() };
        assert_eq!(path, XPathBuf::from("/usr/host/bin"));
        unsafe { path.pop_unchecked() };
        assert_eq!(path, XPathBuf::from("/usr/host"));
        unsafe { path.pop_unchecked() };
        assert_eq!(path, XPathBuf::from("/usr"));
        unsafe { path.pop_unchecked() };
        assert_eq!(path, XPathBuf::from("/"));
        unsafe { path.pop_unchecked() };
        assert_eq!(path, XPathBuf::from("/"));
    }

    #[test]
    fn test_path_pop() {
        // Truncates self to self.parent.
        // Popping `/' gives itself back.
        let mut path = XPathBuf::from("/spirited/away.rs");
        path.pop();
        assert_eq!(path, XPathBuf::from("/spirited"));
        path.pop();
        assert_eq!(path, XPathBuf::from("/"));
        path.pop();
        assert_eq!(path, XPathBuf::from("/"));
    }

    #[test]
    fn test_path_push() {
        // Pushing a relative path extends the existing path.
        let mut path = XPathBuf::from("/tmp");
        path.push(b"file.bk");
        assert_eq!(path, XPathBuf::from("/tmp/file.bk"));

        // Pushing an absolute path replaces the existing path
        let mut path = XPathBuf::from("/tmp");
        path.push(b"/etc");
        assert_eq!(path, XPathBuf::from("/etc"));

        let mut path = XPathBuf::from("/tmp/bar");
        path.push(b"baz/");
        assert_eq!(path, XPathBuf::from("/tmp/bar/baz/"));

        // Pushing an empty string appends a trailing slash.
        let mut path = XPathBuf::from("/tmp");
        path.push(b"");
        assert_eq!(path, XPathBuf::from("/tmp/"));
        assert_eq!(path.as_os_str().as_bytes(), b"/tmp/");
    }

    #[test]
    fn test_path_split() {
        // Test typical path without trailing slash
        let path = XPathBuf::from("/foo/bar/baz");
        let (parent, file_name) = path.split();
        assert_eq!(parent, XPath::from_bytes(b"/foo/bar"));
        assert_eq!(file_name, XPath::from_bytes(b"baz"));

        // Test path with trailing slash
        let path = XPathBuf::from("/foo/bar/baz/");
        let (parent, file_name) = path.split();
        assert_eq!(parent, XPath::from_bytes(b"/foo/bar"));
        assert_eq!(file_name, XPath::from_bytes(b"baz/"));

        // Test root path "/"
        let path = XPathBuf::from("/");
        let (parent, file_name) = path.split();
        assert_eq!(parent, XPath::from_bytes(b"/"));
        assert_eq!(file_name, XPath::from_bytes(b"/"));

        // Test single level path without trailing slash
        let path = XPathBuf::from("/foo");
        let (parent, file_name) = path.split();
        assert_eq!(parent, XPath::from_bytes(b"/"));
        assert_eq!(file_name, XPath::from_bytes(b"foo"));

        // Test single level path with trailing slash
        let path = XPathBuf::from("/foo/");
        let (parent, file_name) = path.split();
        assert_eq!(parent, XPath::from_bytes(b"/"));
        assert_eq!(file_name, XPath::from_bytes(b"foo/"));
    }

    #[test]
    fn test_path_is_proc_pid() {
        assert!(XPathBuf::from("/proc/1").is_proc_pid());
        assert!(XPathBuf::from("/proc/1/").is_proc_pid());

        assert!(XPathBuf::from("/proc/123456789").is_proc_pid());
        assert!(XPathBuf::from("/proc/123456789/task").is_proc_pid());

        assert!(!XPathBuf::from("/proc").is_proc_pid());
        assert!(!XPathBuf::from("/proc/").is_proc_pid());

        assert!(!XPathBuf::from("/proc/acpi").is_proc_pid());
        assert!(!XPathBuf::from("/proc/keys").is_proc_pid());

        // FIXME: This should return false, but it does not matter in practise.
        assert!(XPathBuf::from("/proc/0keys").is_proc_pid());

        assert!(!XPathBuf::from("/dev").is_proc_pid());
        assert!(!XPathBuf::from("/dev/0").is_proc_pid());

        assert!(!XPathBuf::from("/pro").is_proc_pid());
        assert!(!XPathBuf::from("/pro/").is_proc_pid());
        assert!(!XPathBuf::from("/pro/1").is_proc_pid());
    }

    #[test]
    fn test_check_name_valid() {
        let valid_filenames = [
            "valid_filename.txt",
            "hello_world",
            "File123",
            "こんにちは", // Japanese characters
            "文件",       // Chinese characters
            "emoji😀",    // Starts with permitted character
            "valid~name", // '~' allowed in middle
            "name~",      // '~' allowed at end
            "a",
            "normal",
            "test-file",
            "test_file",
            "file name",
            "file☃name",    // Snowman character
            "\u{00A0}",     // Non-breaking space
            "name\u{0080}", // Contains 0x80 (allowed)
            "name\u{00FE}", // Contains 0xFE (allowed)
            "😀name",       // Multi-byte character at start
            "name😀",       // Multi-byte character at end
            "😀",           // Single multi-byte character
            "name😀name",   // Multi-byte character in middle
            "na~me",        // '~' allowed in middle
            "name-",        // Hyphen at end (allowed)
            "name_",        // Underscore at end (allowed)
            "name.",        // Period at end (allowed)
        ];

        for (idx, name) in valid_filenames.iter().enumerate() {
            let name = XPath::new(name);
            assert!(
                name.check_name().is_ok(),
                "Filename {idx} '{name}' should be valid"
            );
        }
    }

    #[test]
    fn test_check_name_invalid() {
        let invalid_filenames: &[&[u8]] = &[
            b"",                 // Empty filename
            b"-",                // Starts with '-'
            b"*",                // Starts with '*'
            b"?",                // Starts with '?'
            b"!",                // Starts with '!'
            b"$",                // Starts with '$'
            b"`",                // Starts with '`'
            b" -",               // Starts with space
            b"~home",            // Starts with '~'
            b"*home",            // Starts with '*'
            b"?home",            // Starts with '?'
            b"!home",            // Starts with '!'
            b"$home",            // Starts with '$'
            b"`home",            // Starts with '`'
            b"file ",            // Ends with space
            b"file*",            // Ends with '*'
            b"file?",            // Ends with '?'
            b"file!",            // Ends with '!'
            b"file$",            // Ends with '$'
            b"file`",            // Ends with '`'
            b"bad*name",         // Contains '*'
            b"bad?name",         // Contains '?'
            b"bad!name",         // Contains '!'
            b"bad$name",         // Contains '$'
            b"bad`name",         // Contains '`'
            b"bad\nname",        // Contains newline
            b"\0",               // Null byte
            b"bad\0name",        // Contains null byte
            b"bad\x7Fname",      // Contains delete character
            b"bad\xFFname",      // Contains 0xFF
            b"\x1Fcontrol",      // Starts with control character
            b"name\x1F",         // Ends with control character
            b"name\x7F",         // Ends with delete character
            b"name\xFF",         // Ends with 0xFF
            b"name ",            // Ends with space
            b"-name",            // Starts with '-'
            b" name",            // Starts with space
            b"~name",            // Starts with '~'
            b"*name",            // Starts with '*'
            b"?name",            // Starts with '?'
            b"!name",            // Starts with '!'
            b"$name",            // Starts with '$'
            b"`name",            // Starts with '`'
            b"name\x19",         // Contains control character
            b"name\n",           // Ends with newline
            b"\nname",           // Starts with newline
            b"na\nme",           // Contains newline
            b"name\t",           // Contains tab
            b"name\r",           // Contains carriage return
            b"name\x1B",         // Contains escape character
            b"name\x00",         // Contains null byte
            b"name\x7F",         // Contains delete character
            b"name\xFF",         // Contains 0xFF (disallowed)
            b"\xFF",             // Single byte 0xFF
            b"name\x80\xFF",     // Contains valid and invalid extended ASCII
            b"name\xC0\xAF",     // Invalid UTF-8 sequence
            b"\xF0\x28\x8C\xBC", // Invalid UTF-8 sequence
            b"\xF0\x90\x28\xBC", // Invalid UTF-8 sequence
            b"\xF0\x28\x8C\x28", // Invalid UTF-8 sequence
            b"name\xFFname",     // Contains 0xFF
            b"name\xC3\x28",     // Invalid UTF-8 sequence
            b"name\xA0\xA1",     // Invalid UTF-8 sequence
            b"\xE2\x28\xA1",     // Invalid UTF-8 sequence
            b"\xE2\x82\x28",     // Invalid UTF-8 sequence
            b"\xF0\x28\x8C\xBC", // Invalid UTF-8 sequence
            b"\xF0\x90\x28\xBC", // Invalid UTF-8 sequence
            b"\xF0\x28\x8C\x28", // Invalid UTF-8 sequence
        ];

        for (idx, name) in invalid_filenames.iter().enumerate() {
            let name = XPath::from_bytes(name);
            assert!(
                name.check_name().is_err(),
                "Filename {idx} '{name}' should not be valid"
            );
        }
    }

    #[test]
    fn test_check_name_control_characters() {
        for b in 0x00..=0x1F {
            if let Some(c) = char::from_u32(b as u32) {
                let name = format!("name{c}char");
                let name = XPath::new(&name);
                assert!(
                    name.check_name().is_err(),
                    "Filename with control character '\\x{b:02X}' should be invalid",
                );
            }
        }
    }

    #[test]
    fn test_check_name_extended_ascii_characters() {
        for b in 0x80..=0xFE {
            if b == 0xFF {
                continue; // 0xFF is disallowed.
            }
            let mut bytes = b"name".to_vec();
            bytes.push(b);
            bytes.extend_from_slice(b"char");
            let name = OsStr::from_bytes(&bytes);
            let name = XPath::new(name);
            let result = name.check_name();
            if std::str::from_utf8(&bytes).is_ok() {
                assert!(result.is_ok(), "Filename with byte 0x{b:X} should be valid",);
            } else {
                assert!(
                    result.is_err(),
                    "Filename with invalid UTF-8 byte 0x{b:X} should be invalid",
                );
            }
        }
    }

    #[test]
    fn test_check_name_edge_cases() {
        // Filenames with length 1
        let valid_single_chars = [
            "a", "b", "Z", "9", "_", ".", "😀",       // Valid multi-byte character
            "\u{00A0}", // Non-breaking space
        ];

        for (idx, name) in valid_single_chars.iter().enumerate() {
            let name = XPath::new(name);
            assert!(
                name.check_name().is_ok(),
                "Single-character filename {idx} '{name}' should be valid",
            );
        }

        let invalid_single_chars: &[&[u8]] = &[
            b"-",    // Starts with '-'
            b" ",    // Space character
            b"~",    // Tilde character
            b"*",    // Starts with '*'
            b"?",    // Starts with '?'
            b"\n",   // Newline character
            b"\r",   // Newline character
            b"\x7F", // Delete character
            b"\x1F", // Control character
            b"\xFF", // 0xFF disallowed
            b"\0",   // Null byte
        ];

        for (idx, name) in invalid_single_chars.iter().enumerate() {
            let name = XPath::from_bytes(name);
            assert!(
                name.check_name().is_err(),
                "Single-character filename {idx} '{name}' should be invalid",
            );
        }
    }
}
