syd/
path.rs

1//
2// Syd: rock-solid application kernel
3// src/path.rs: Path handling for UNIX
4//
5// Copyright (c) 2024, 2025 Ali Polatel <alip@chesswob.org>
6// Based in part upon David A. Wheeler's SafeName LSM patches which is:
7//   Copyright (C) 2016 David A. Wheeler
8//   SPDX-License-Identifier: GPL-2.0
9//
10// SPDX-License-Identifier: GPL-3.0
11
12use std::{
13    borrow::{Borrow, Cow},
14    cmp::Ordering,
15    collections::VecDeque,
16    ffi::{CStr, OsStr, OsString},
17    ops::Deref,
18    os::{
19        fd::RawFd,
20        unix::ffi::{OsStrExt, OsStringExt},
21    },
22    path::{Path, PathBuf},
23};
24
25use btoi::btoi;
26use memchr::{
27    arch::all::{is_equal, is_prefix, is_suffix, memchr::One},
28    memchr, memmem, memrchr,
29};
30use nix::{
31    errno::Errno,
32    fcntl::{openat2, OFlag, OpenHow, ResolveFlag, AT_FDCWD},
33    libc::pid_t,
34    unistd::Pid,
35    NixPath,
36};
37use once_cell::sync::Lazy;
38
39use crate::{
40    config::MAGIC_PREFIX,
41    fs::{retry_on_eintr, tgkill, FileType},
42    log::log_untrusted_buf,
43};
44
45/// Generate a formatted `XPathBuf`.
46#[macro_export]
47macro_rules! xpath {
48    ($($arg:tt)*) => {
49        XPathBuf::from(format!($($arg)*))
50    };
51}
52
53/// A safe constant to use as PATH_MAX without relying on libc.
54pub const PATH_MAX: usize = 4096;
55
56/// A safe default size to use for paths.
57pub const PATH_MIN: usize = 64;
58
59// This pointer is confined by seccomp for use with openat(2) for getdir_long().
60static DOTDOT: Lazy<u64> = Lazy::new(|| c"..".as_ptr() as *const libc::c_char as u64);
61
62#[inline(always)]
63pub(crate) fn dotdot_with_nul() -> u64 {
64    *DOTDOT
65}
66
67/// `PathBuf` for UNIX.
68// SAFETY: k1 == k2 ⇒ hash(k1) == hash(k2) always holds for our PartialEq impl.
69#[allow(clippy::derived_hash_with_manual_eq)]
70#[derive(Clone, Hash, Ord, PartialOrd)]
71pub struct XPathBuf(Vec<u8>);
72
73impl Default for XPathBuf {
74    fn default() -> Self {
75        Self(Vec::with_capacity(PATH_MIN))
76    }
77}
78
79impl Eq for XPathBuf {}
80
81impl PartialEq for XPathBuf {
82    fn eq(&self, other: &Self) -> bool {
83        is_equal(&self.0, &other.0)
84    }
85}
86
87impl PartialEq<XPath> for XPathBuf {
88    fn eq(&self, other: &XPath) -> bool {
89        is_equal(self.as_bytes(), other.as_bytes())
90    }
91}
92
93impl PartialEq<XPathBuf> for XPath {
94    fn eq(&self, other: &XPathBuf) -> bool {
95        is_equal(self.as_bytes(), other.as_bytes())
96    }
97}
98
99impl Deref for XPathBuf {
100    type Target = XPath;
101
102    #[inline]
103    fn deref(&self) -> &XPath {
104        XPath::from_bytes(&self.0)
105    }
106}
107
108impl Borrow<XPath> for XPathBuf {
109    #[inline]
110    fn borrow(&self) -> &XPath {
111        self.deref()
112    }
113}
114
115/// A borrowed slice of an XPathBuf.
116// SAFETY: k1 == k2 ⇒ hash(k1) == hash(k2) always holds for our PartialEq impl.
117#[allow(clippy::derived_hash_with_manual_eq)]
118#[repr(transparent)]
119#[derive(Hash, Ord, PartialOrd)]
120pub struct XPath(OsStr);
121
122impl Eq for XPath {}
123
124impl PartialEq for XPath {
125    fn eq(&self, other: &Self) -> bool {
126        is_equal(self.0.as_bytes(), other.0.as_bytes())
127    }
128}
129
130impl ToOwned for XPath {
131    type Owned = XPathBuf;
132
133    fn to_owned(&self) -> Self::Owned {
134        XPathBuf::from(self.as_bytes())
135    }
136}
137
138impl AsRef<XPath> for XPathBuf {
139    fn as_ref(&self) -> &XPath {
140        self.as_xpath()
141    }
142}
143
144impl AsRef<Path> for XPathBuf {
145    fn as_ref(&self) -> &Path {
146        self.as_path()
147    }
148}
149
150impl AsRef<OsStr> for XPathBuf {
151    fn as_ref(&self) -> &OsStr {
152        self.as_os_str()
153    }
154}
155
156impl From<&XPath> for XPathBuf {
157    fn from(path: &XPath) -> Self {
158        path.as_bytes().into()
159    }
160}
161
162impl From<PathBuf> for XPathBuf {
163    fn from(pbuf: PathBuf) -> Self {
164        pbuf.into_os_string().into()
165    }
166}
167
168impl From<&OsStr> for XPathBuf {
169    fn from(ostr: &OsStr) -> Self {
170        ostr.as_bytes().into()
171    }
172}
173
174impl From<OsString> for XPathBuf {
175    fn from(os: OsString) -> Self {
176        Self(os.into_vec())
177    }
178}
179
180impl From<String> for XPathBuf {
181    fn from(s: String) -> Self {
182        Self(s.as_bytes().into())
183    }
184}
185
186impl From<&str> for XPathBuf {
187    fn from(s: &str) -> Self {
188        Self(s.as_bytes().into())
189    }
190}
191
192impl From<Cow<'_, str>> for XPathBuf {
193    fn from(cow: Cow<'_, str>) -> Self {
194        match cow {
195            Cow::Borrowed(s) => Self(s.as_bytes().to_vec()),
196            Cow::Owned(s) => Self(s.into_bytes()),
197        }
198    }
199}
200
201impl From<&[u8]> for XPathBuf {
202    fn from(bytes: &[u8]) -> Self {
203        Self(bytes.to_vec())
204    }
205}
206
207impl From<Vec<u8>> for XPathBuf {
208    fn from(vec: Vec<u8>) -> Self {
209        Self(vec)
210    }
211}
212
213impl From<VecDeque<u8>> for XPathBuf {
214    fn from(vec: VecDeque<u8>) -> Self {
215        Self(vec.into())
216    }
217}
218
219impl From<pid_t> for XPathBuf {
220    fn from(pid: pid_t) -> Self {
221        let mut buf = itoa::Buffer::new();
222        buf.format(pid).into()
223    }
224}
225
226impl std::ops::Deref for XPath {
227    type Target = Path;
228
229    fn deref(&self) -> &Self::Target {
230        self.as_path()
231    }
232}
233
234impl AsRef<Path> for XPath {
235    fn as_ref(&self) -> &Path {
236        self.as_path()
237    }
238}
239
240impl AsRef<OsStr> for XPath {
241    fn as_ref(&self) -> &OsStr {
242        self.as_os_str()
243    }
244}
245
246impl std::fmt::Display for XPathBuf {
247    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
248        // SAFETY: Mask control characters in path.
249        write!(f, "{}", mask_path(self.as_path()))
250    }
251}
252
253impl std::fmt::Debug for XPathBuf {
254    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
255        // SAFETY: Mask control characters in path.
256        write!(f, "{}", mask_path(self.as_path()))
257    }
258}
259
260impl serde::Serialize for XPathBuf {
261    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
262    where
263        S: serde::Serializer,
264    {
265        // SAFETY: Display masks control characters.
266        serializer.serialize_str(&format!("{self}"))
267    }
268}
269
270impl NixPath for XPathBuf {
271    fn is_empty(&self) -> bool {
272        self.0.is_empty()
273    }
274
275    fn len(&self) -> usize {
276        self.0.len()
277    }
278
279    fn with_nix_path<T, F>(&self, f: F) -> Result<T, Errno>
280    where
281        F: FnOnce(&CStr) -> T,
282    {
283        self.as_os_str().with_nix_path(f)
284    }
285}
286
287impl std::fmt::Display for XPath {
288    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
289        // SAFETY: Mask control characters in path.
290        write!(f, "{}", mask_path(self.as_path()))
291    }
292}
293
294impl std::fmt::Debug for XPath {
295    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
296        // SAFETY: Mask control characters in path.
297        write!(f, "{}", mask_path(self.as_path()))
298    }
299}
300
301impl serde::Serialize for XPath {
302    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
303    where
304        S: serde::Serializer,
305    {
306        // SAFETY: Display masks control characters.
307        serializer.serialize_str(&format!("{self}"))
308    }
309}
310
311impl NixPath for XPath {
312    fn is_empty(&self) -> bool {
313        self.0.is_empty()
314    }
315
316    fn len(&self) -> usize {
317        self.0.len()
318    }
319
320    fn with_nix_path<T, F>(&self, f: F) -> Result<T, Errno>
321    where
322        F: FnOnce(&CStr) -> T,
323    {
324        self.as_os_str().with_nix_path(f)
325    }
326}
327
328impl XPath {
329    /// Detects unsafe paths.
330    ///
331    /// List of restrictions:
332    /// 1. Block devices can not be listed with readdir() regardless of path.
333    /// 2. readdir(/proc) returns current pid as the only process id.
334    /// 3. /proc/$pid where $pid == Syd -> ENOENT.
335    /// 4. File name must not contain forbidden characters if `safe_name` is true.
336    ///
337    /// # SAFETY
338    /// 1. `self` must be an absolute pathname.
339    /// 2. `self` must be canonicalized and normalized.
340    ///
341    /// Note, returning error here denies access,
342    /// regardless of the state of sandboxing.
343    #[inline(always)]
344    pub fn check(
345        &self,
346        pid: Pid,
347        file_type: Option<&FileType>,
348        dir_entry: Option<&XPath>,
349        safe_name: bool,
350    ) -> Result<(), Errno> {
351        //
352        // RESTRICTION 1: Prevent listing block devices and files of unknown type.
353        //
354        // SAFETY: Prevent listing block devices and files of unknown type.
355        if matches!(file_type, Some(FileType::Blk | FileType::Unk)) {
356            return Err(Errno::ENOENT);
357        }
358        // END OF RESTRICTION 1
359
360        //
361        // RESTRICTION 2: Restrict file names to allowed characters as necessary.
362        //
363        // SAFETY: Prevent accessing file names which may be misinterpreted by shells.
364        // Note, we skip checking procfs so we don't prevent pipe/socket access
365        // unintentionally. Similarly we skip checking memory fd names which do not
366        // represent actual file paths.
367        let is_mfd = matches!(file_type, Some(FileType::Mfd));
368        let is_proc_dir = self.starts_with(b"/proc");
369        if safe_name && !is_mfd && !is_proc_dir && self.check_name().is_err() {
370            return Err(Errno::EINVAL);
371        }
372        // END OF RESTRICTION 2
373
374        // Remaining restrictions apply to procfs only.
375        let (is_proc, proc_pid) = if is_proc_dir {
376            const LEN: usize = b"/proc".len();
377            let mut proc_pid = None;
378            let is_proc = self.len() == LEN;
379
380            if is_proc {
381                // If this is `/proc' directory entries may refer to PIDs.
382                if let Some(p) = dir_entry {
383                    proc_pid = btoi::<libc::pid_t>(p.as_bytes()).ok();
384                }
385            }
386
387            if proc_pid.is_none()
388                && self
389                    .get(LEN + 1)
390                    .map(|c| c.is_ascii_digit())
391                    .unwrap_or(false)
392            {
393                let path = self.as_bytes();
394                let path = &path[LEN + 1..];
395                let pidx = memchr(b'/', path).unwrap_or(path.len());
396                proc_pid = btoi::<libc::pid_t>(&path[..pidx]).ok();
397            }
398
399            (is_proc, proc_pid)
400        } else {
401            return Ok(());
402        };
403        let proc_pid = if let Some(pid) = proc_pid {
404            pid
405        } else {
406            return Ok(());
407        };
408
409        //
410        // RESTRICTION 2: Protect readdir(/proc).
411        //
412        // SAFETY: Prevent /proc process tree traversal.
413        if is_proc && proc_pid != pid.as_raw() {
414            return Err(Errno::ENOENT);
415        }
416        // END OF RESTRICTION 2
417
418        //
419        // RESTRICTION 3: Protect Syd procfs.
420        //
421        // SAFETY: Protect Syd /proc directory!
422        //
423        // Step 1: Protect Syd thread group.
424        let syd_pid = Pid::this();
425        let proc_pid = Pid::from_raw(proc_pid);
426        if proc_pid == syd_pid {
427            return Err(Errno::ENOENT);
428        }
429        //
430        // Step 2: Protect all Syd threads.
431        if tgkill(syd_pid, proc_pid, 0).is_ok() {
432            return Err(Errno::ENOENT);
433        }
434        // END OF RESTRICTION 3
435
436        // TODO: Add more restrictions as needed.
437        Ok(())
438    }
439
440    /// Validates a filename based on David A. Wheeler's Safename Linux
441    /// Security Module (LSM) rules.
442    ///
443    /// This function checks if a given filename (not the entire path)
444    /// adheres to specific security policies inspired by Wheeler's
445    /// Safename LSM. These policies are designed to prevent the
446    /// creation of filenames that could be used for malicious purposes,
447    /// such as exploiting poorly written scripts or programs.
448    ///
449    /// The validation rules are:
450    ///
451    /// 1. **Non-Empty Filename**: The filename must not be empty.
452    ///
453    /// 2. **Valid UTF-8 Encoding**: The filename must be valid UTF-8.
454    ///
455    /// 3. **Permitted Characters**:
456    ///    - **Initial Byte**: Must be an allowed character, but cannot be:
457    ///        - Space `' '` (0x20)
458    ///        - Hyphen `'-'` (0x2D)
459    ///        - Tilde `'~'` (0x7E)
460    ///    - **Middle Bytes**: Each must be an allowed character (if any).
461    ///    - **Final Byte**: Must be an allowed character, but cannot be:
462    ///        - Space `' '` (0x20)
463    ///
464    /// 4. **Allowed Character Set**:
465    ///    - ASCII printable characters from space `' '` (0x20) to tilde `'~'` (0x7E), inclusive.
466    ///    - Extended ASCII characters from 0x80 to 0xFE, inclusive.
467    ///    - **Excludes** control characters (0x00-0x1F), delete (0x7F), and 0xFF.
468    ///
469    /// # Returns
470    ///
471    /// * `Ok(())` if the filename is valid and safe.
472    /// * `Err(Errno::EINVAL)` if the filename is invalid or unsafe.
473    ///
474    /// # Errors
475    ///
476    /// Returns `Err(Errno::EINVAL)` if any of the validation rules are not met.
477    ///
478    /// # Security
479    ///
480    /// Enforcing these rules helps prevent security vulnerabilities
481    /// arising from unexpected or malicious filenames, such as command
482    /// injection, denial of service, or arbitrary file manipulation.
483    #[allow(clippy::arithmetic_side_effects)]
484    pub fn check_name(&self) -> Result<(), Errno> {
485        let (_, name) = self.split();
486        let name = name.as_bytes();
487        let len = name.len();
488
489        if len == 0 {
490            return Err(Errno::EINVAL);
491        }
492
493        // Check if the filename is valid UTF-8.
494        let name_utf8 = std::str::from_utf8(name).or(Err(Errno::EINVAL))?;
495
496        // Check if first and last character is not whitespace.
497        // This includes UTF-8 whitespace.
498        if name_utf8
499            .chars()
500            .nth(0)
501            .map(|c| c.is_whitespace())
502            .unwrap_or(false)
503        {
504            return Err(Errno::EINVAL);
505        }
506        if name_utf8
507            .chars()
508            .last()
509            .map(|c| c.is_whitespace())
510            .unwrap_or(false)
511        {
512            return Err(Errno::EINVAL);
513        }
514
515        let first_byte = name[0];
516        let last_byte = name[len - 1];
517
518        // Check the first byte.
519        if !is_permitted_initial(first_byte) {
520            return Err(Errno::EINVAL);
521        }
522
523        // Check the middle bytes (if any).
524        match len {
525            2 => {
526                // Only one middle byte to check.
527                let middle_byte = name[1];
528                if !is_permitted_middle(middle_byte) {
529                    return Err(Errno::EINVAL);
530                }
531            }
532            n if n > 2 => {
533                for &b in &name[1..len - 1] {
534                    if !is_permitted_middle(b) {
535                        return Err(Errno::EINVAL);
536                    }
537                }
538            }
539            _ => {}
540        }
541
542        // Check the last byte.
543        if !is_permitted_final(last_byte) {
544            return Err(Errno::EINVAL);
545        }
546
547        Ok(())
548    }
549
550    /// Returns a path that, when joined onto `base`, yields `self`.
551    ///
552    /// Expects normalized, canonical path.
553    #[allow(clippy::arithmetic_side_effects)]
554    pub fn split_prefix(&self, base: &[u8]) -> Option<&Self> {
555        let mut len = base.len();
556        if len == 0 {
557            return None;
558        } else if base == b"/" {
559            return Some(self);
560        }
561
562        let base = if base[len - 1] == b'/' {
563            len -= 1;
564            &base[..len - 1]
565        } else {
566            base
567        };
568
569        if !self.starts_with(base) {
570            return None;
571        }
572
573        let raw = self.as_bytes();
574        let len_raw = raw.len();
575        if len == len_raw {
576            Some(XPath::from_bytes(b""))
577        } else if len_raw < len + 1 || raw[len] != b'/' {
578            None
579        } else {
580            Some(XPath::from_bytes(&raw[len + 1..]))
581        }
582    }
583
584    /// Splits a given path into the parent path and the file name.
585    ///
586    /// - The function efficiently finds the last `/` in the path and splits at that point.
587    /// - Trailing slashes are included in the filename to indicate directory paths.
588    /// - For the root path `/`, both parent and filename are the original path reference.
589    #[allow(clippy::arithmetic_side_effects)]
590    pub fn split(&self) -> (&Self, &Self) {
591        // Special cases for the empty and root paths.
592        let bytes = match self.get(0) {
593            None => return (XPath::from_bytes(b""), XPath::from_bytes(b"")),
594            Some(b'/') if self.0.len() == 1 => {
595                return (
596                    XPath::from_bytes(&self.as_bytes()[..1]),
597                    XPath::from_bytes(&self.as_bytes()[..1]),
598                )
599            }
600            _ => self.as_bytes(),
601        };
602
603        // Determine if the path ends with a trailing slash.
604        let has_trailing_slash = bytes[bytes.len() - 1] == b'/';
605        let effective_length = if has_trailing_slash && bytes.len() > 1 {
606            bytes.len() - 1
607        } else {
608            bytes.len()
609        };
610        let last_slash_index = memrchr(b'/', &bytes[..effective_length]);
611
612        if let Some(idx) = last_slash_index {
613            let parent_path = if idx == 0 {
614                // The slash is at the beginning, so the parent is root.
615                XPath::from_bytes(b"/")
616            } else {
617                // Take everything up to the last non-trailing slash.
618                XPath::from_bytes(&bytes[..idx])
619            };
620
621            let filename_start = idx + 1;
622            let filename_end = if has_trailing_slash {
623                bytes.len()
624            } else {
625                effective_length
626            };
627            let filename_path = XPath::from_bytes(&bytes[filename_start..filename_end]);
628
629            return (parent_path, filename_path);
630        }
631
632        // If no slash is found, the whole thing is the filename!
633        (XPath::from_bytes(b""), self)
634    }
635
636    /// Returns a reference to the file extension.
637    pub fn extension(&self) -> Option<&Self> {
638        let dot = memrchr(b'.', self.as_bytes())?;
639        // dot==Some means len>=1.
640        #[allow(clippy::arithmetic_side_effects)]
641        if dot < self.0.len() - 1 {
642            Some(Self::from_bytes(&self.as_bytes()[dot + 1..]))
643        } else {
644            None
645        }
646    }
647
648    /// Returns a reference to the parent path.
649    pub fn parent(&self) -> &Self {
650        Self::from_bytes(&self.as_bytes()[..self.parent_len()])
651    }
652
653    /// Determines the length of the parent path.
654    #[allow(clippy::arithmetic_side_effects)]
655    pub fn parent_len(&self) -> usize {
656        // Special cases for the empty and root paths.
657        let bytes = match self.get(0) {
658            None => return 0,
659            Some(b'/') if self.len() == 1 => return 1,
660            _ => self.as_bytes(),
661        };
662
663        // Determine if the path ends with a trailing slash.
664        let has_trailing_slash = bytes[bytes.len() - 1] == b'/';
665        let effective_length = if has_trailing_slash && bytes.len() > 1 {
666            bytes.len() - 1
667        } else {
668            bytes.len()
669        };
670        let last_slash_index = memrchr(b'/', &bytes[..effective_length]);
671
672        if let Some(idx) = last_slash_index {
673            return if idx == 0 {
674                // The slash is at the beginning, so the parent is root.
675                1
676            } else {
677                // Take everything up to the last non-trailing slash.
678                idx
679            };
680        }
681
682        // If no slash is found, the whole thing is the filename!
683        0
684    }
685
686    /// Return the depth of the path.
687    ///
688    /// The depth of a path is equal to the number of directory separators in it.
689    pub fn depth(&self) -> usize {
690        memchr::arch::all::memchr::One::new(b'/').count(self.as_bytes())
691    }
692
693    /// Check if path is a descendant of the given `root` path (RESOLVE_BENEATH compatible).
694    /// Both paths must be canonicalized.
695    pub fn descendant_of(&self, root: &[u8]) -> bool {
696        if is_equal(root, b"/") {
697            // Every absolute path is a descendant of "/".
698            return true;
699        } else if !self.starts_with(root) {
700            // `self` does not begin with `root`.
701            return false;
702        }
703
704        let slen = self.len();
705        let rlen = root.len();
706
707        match slen.cmp(&rlen) {
708            Ordering::Less => false,
709            Ordering::Equal => true,
710            Ordering::Greater => self.get(rlen) == Some(b'/'),
711        }
712    }
713
714    /// Returns a path that, when joined onto `base`, yields `self`.
715    ///
716    /// # Safety
717    ///
718    /// Assumes `self` is normalized.
719    ///
720    /// # Errors
721    ///
722    /// If `base` is not a prefix of self (i.e., `starts_with` returns
723    /// `false`), returns `Err`.
724    pub fn strip_prefix(&self, base: &[u8]) -> Option<&Self> {
725        if !self.starts_with(base) {
726            return None;
727        }
728
729        // Determine the remainder after the base.
730        let remainder = &self.as_bytes()[base.len()..];
731
732        // Check if there is anything left after the base.
733        if remainder.is_empty() {
734            // If the remainder is empty, return an empty path.
735            Some(Self::from_bytes(b""))
736        } else if remainder[0] == b'/' {
737            // Return the slice after the '/', ensuring no leading '/' in the result
738            // This is safe due to the assumption of normalized paths.
739            Some(Self::from_bytes(&remainder[1..]))
740        } else {
741            // If the path doesn't start with '/', it means base is not a directory prefix.
742            None
743        }
744    }
745
746    /// Checks if the path ends with a dot component.
747    ///
748    /// This function iterates through the bytes of the path from end to
749    /// start, and determines whether the last component before any
750    /// slashes is a dot.
751    #[allow(clippy::arithmetic_side_effects)]
752    #[allow(clippy::if_same_then_else)]
753    pub fn ends_with_dot(&self) -> bool {
754        let bytes = self.as_bytes();
755
756        // Start from the end of the string and move backwards.
757        let mut index = bytes.len();
758        if index == 0 {
759            return false;
760        }
761
762        // Skip trailing slashes.
763        while index > 0 && bytes[index - 1] == b'/' {
764            index -= 1;
765        }
766
767        // If the path is empty after removing trailing slashes,
768        // it does not end with a dot.
769        if index == 0 {
770            return false;
771        }
772
773        // Check for '.' or '..'
774        if bytes[index - 1] == b'.' {
775            if index == 1 || bytes[index - 2] == b'/' {
776                return true; // Matches '.' or '*/.'
777            } else if index > 1
778                && bytes[index - 2] == b'.'
779                && (index == 2 || bytes[index - 3] == b'/')
780            {
781                return true; // Matches '..' or '*/..'
782            }
783        }
784
785        false
786    }
787
788    /// Returns true if the path ends with a slash.
789    pub fn ends_with_slash(&self) -> bool {
790        !self.is_rootfs() && self.last() == Some(b'/')
791    }
792
793    /// Check if path has a parent dir component, ie `..`.
794    pub fn has_parent_dot(&self) -> bool {
795        self.contains(b"/..") || self.is_equal(b"..")
796    }
797
798    /// Check if path starts with the `MAGIC_PREFIX`.
799    pub fn is_magic(&self) -> bool {
800        self.starts_with(MAGIC_PREFIX)
801    }
802
803    /// Check if path is the root path, ie `/`.
804    pub fn is_rootfs(&self) -> bool {
805        self.as_bytes().iter().all(|b| matches!(*b, b'/' | b'.'))
806    }
807
808    /// Check if path points to procfs root dir, ie. `/proc`.
809    ///
810    /// `self` must be canonicalized.
811    pub fn is_procfs(&self) -> bool {
812        const PROC_LEN: usize = b"/proc".len();
813        const PROC_DIR_LEN: usize = b"/proc/".len();
814
815        match self.len() {
816            PROC_LEN if self.is_equal(b"/proc") => true,
817            PROC_DIR_LEN if self.is_equal(b"/proc/") => true,
818            _ => false,
819        }
820    }
821
822    /// Check if path points to devfs, ie. starts with `/dev`.
823    /// The literal path `/dev` returns false.
824    ///
825    /// `self` must be canonicalized.
826    pub fn is_dev(&self) -> bool {
827        self.starts_with(b"/dev/")
828    }
829
830    /// Check if path points to procfs, ie. starts with `/proc`.
831    /// The literal path `/proc` returns false.
832    ///
833    /// `self` must be canonicalized.
834    pub fn is_proc(&self) -> bool {
835        self.starts_with(b"/proc/")
836    }
837
838    /// Check if path points to a static path.
839    /// See proc_init in config.rs
840    pub fn is_static(&self) -> bool {
841        self.is_rootfs() || self.is_procfs() || self.is_equal(b"/dev/null")
842    }
843
844    /// Check if path points to per-process procfs directory, ie. starts with `/proc/$pid`.
845    /// `/proc/$pid` is also accepted among with all descendants of it.
846    pub fn is_proc_pid(&self) -> bool {
847        if !self.is_proc() {
848            return false;
849        }
850        match self.get("/proc/".len()) {
851            Some(n) => n.is_ascii_digit(),
852            None => false,
853        }
854    }
855
856    /// Check if path points to the `/proc/self` link.
857    /// If `thread` is true, checks for `/proc/thread-self`.
858    pub fn is_proc_self(&self, thread: bool) -> bool {
859        if thread {
860            is_equal(self.as_bytes(), b"/proc/thread-self")
861        } else {
862            is_equal(self.as_bytes(), b"/proc/self")
863        }
864    }
865
866    /// Check if path exists.
867    #[allow(clippy::disallowed_methods)]
868    pub fn exists(&self, follow: bool) -> bool {
869        let flags = if self.is_empty() {
870            return false;
871        } else if !follow {
872            OFlag::O_NOFOLLOW
873        } else {
874            OFlag::empty()
875        };
876
877        let mut how = OpenHow::new().flags(flags | OFlag::O_PATH | OFlag::O_CLOEXEC);
878        if !follow {
879            how =
880                how.resolve(ResolveFlag::RESOLVE_NO_MAGICLINKS | ResolveFlag::RESOLVE_NO_SYMLINKS);
881        }
882
883        retry_on_eintr(|| openat2(AT_FDCWD, self, how))
884            .map(drop)
885            .is_ok()
886    }
887
888    /// Check if path is a symlink.
889    pub fn is_symlink(&self) -> bool {
890        self.as_path().is_symlink()
891    }
892
893    /// Check if path is a dir.
894    pub fn is_dir(&self) -> bool {
895        self.as_path().is_dir()
896    }
897
898    /// Check if path is a file.
899    pub fn is_file(&self) -> bool {
900        self.as_path().is_file()
901    }
902
903    /// Check if path is absolute.
904    pub fn is_absolute(&self) -> bool {
905        self.first() == Some(b'/')
906    }
907
908    /// Check if path is relative.
909    ///
910    /// Empty path is considered relative.
911    pub fn is_relative(&self) -> bool {
912        !self.is_absolute()
913    }
914
915    /// Checks if the path consists only of "." components.
916    pub fn is_dot(&self) -> bool {
917        let bytes = self.as_bytes();
918
919        if bytes.is_empty() {
920            return false;
921        }
922
923        let mut has_component = false;
924        let mut start = 0;
925
926        // Iterate over '/' positions without allocating.
927        #[allow(clippy::arithmetic_side_effects)]
928        for sep in One::new(b'/').iter(bytes) {
929            if sep > start {
930                // Non-empty component [start, sep).
931                let component = &bytes[start..sep];
932                has_component = true;
933                if component != b"." {
934                    return false;
935                }
936            }
937
938            // Move past '/'.
939            start = sep + 1;
940        }
941
942        // Handle the trailing component after the last '/',
943        // or the whole slice if none.
944        if start < bytes.len() {
945            let component = &bytes[start..];
946            has_component = true;
947            if component != b"." {
948                return false;
949            }
950        }
951
952        has_component
953    }
954
955    /// Determine whether path is equal to the given string.
956    pub fn is_equal(&self, s: &[u8]) -> bool {
957        is_equal(self.as_bytes(), s)
958    }
959
960    /// Determine whether base is a prefix of path.
961    pub fn starts_with(&self, base: &[u8]) -> bool {
962        is_prefix(self.as_bytes(), base)
963    }
964
965    /// Determine whether base is a suffix of path.
966    pub fn ends_with(&self, base: &[u8]) -> bool {
967        is_suffix(self.as_bytes(), base)
968    }
969
970    /// Determine whether path contains the given substring.
971    pub fn contains(&self, sub: &[u8]) -> bool {
972        memmem::find_iter(self.as_bytes(), sub).next().is_some()
973    }
974
975    /// Determine whether path contains the given character.
976    pub fn contains_char(&self, c: u8) -> bool {
977        memchr(c, self.as_bytes()).is_some()
978    }
979
980    /// Returns the first character of the path.
981    /// Empty path returns None.
982    pub fn first(&self) -> Option<u8> {
983        self.as_bytes().first().copied()
984    }
985
986    /// Returns the last character of the path.
987    /// Empty path returns None.
988    pub fn last(&self) -> Option<u8> {
989        self.as_bytes().last().copied()
990    }
991
992    /// Returns the character at the specified index.
993    /// Returns None if path is shorter.
994    pub fn get(&self, index: usize) -> Option<u8> {
995        self.as_bytes().get(index).copied()
996    }
997
998    /// Convert to a `Path`.
999    pub fn as_path(&self) -> &Path {
1000        Path::new(self.as_os_str())
1001    }
1002
1003    /// Creates an owned `XPathBuf` with path adjoined to `self`.
1004    /// If `path` is absolute, it replaces the current path.
1005    pub fn join(&self, path: &[u8]) -> XPathBuf {
1006        let mut owned = self.to_owned();
1007        owned.push(path);
1008        owned
1009    }
1010
1011    /// Returns an immutable slice of the buffer.
1012    pub fn as_bytes(&self) -> &[u8] {
1013        self.0.as_bytes()
1014    }
1015
1016    /// Convert to a `OsStr`.
1017    pub fn as_os_str(&self) -> &OsStr {
1018        &self.0
1019    }
1020
1021    /// Create a new `XPath` from a byte slice.
1022    pub const fn from_bytes(slice: &[u8]) -> &XPath {
1023        // SAFETY: XPath has repr(transparent)
1024        unsafe { std::mem::transmute(slice) }
1025    }
1026
1027    /// Create a new `XPath` for the dotdot path, aka `..`
1028    pub fn dotdot() -> &'static XPath {
1029        XPath::from_bytes(b"..")
1030    }
1031
1032    /// Create a new `XPath` for the dot path, aka `.`
1033    pub fn dot() -> &'static XPath {
1034        XPath::from_bytes(b".")
1035    }
1036
1037    /// Create a new `XPath` for the root path, aka `/`
1038    pub fn root() -> &'static XPath {
1039        XPath::from_bytes(b"/")
1040    }
1041
1042    /// Create a new, empty `XPath`
1043    pub fn empty() -> &'static XPath {
1044        XPath::from_bytes(b"")
1045    }
1046
1047    /// Create a new `XPath` from a byte slice.
1048    pub fn new<S: AsRef<OsStr> + ?Sized>(s: &S) -> &XPath {
1049        // SAFETY: XPath has repr(transparent).
1050        unsafe { &*(s.as_ref() as *const OsStr as *const XPath) }
1051    }
1052}
1053
1054impl XPathBuf {
1055    /// Removes consecutive slashes (`/`) from the path in-place,
1056    /// replacing them with a single slash.
1057    ///
1058    /// This method modifies `self` directly.
1059    pub fn clean_consecutive_slashes(&mut self) {
1060        let len = match self.len() {
1061            0 | 1 => return,
1062            n => n,
1063        };
1064
1065        let mut write_pos = 0;
1066        let mut read_pos = 0;
1067        #[allow(clippy::arithmetic_side_effects)]
1068        while read_pos < len {
1069            if self.0[read_pos] == b'/' {
1070                // Write a single slash.
1071                self.0[write_pos] = b'/';
1072                write_pos += 1;
1073                read_pos += 1;
1074
1075                // Skip over consecutive slashes.
1076                while read_pos < len && self.0[read_pos] == b'/' {
1077                    read_pos += 1;
1078                }
1079            } else {
1080                // Find the next slash using memchr for efficiency.
1081                let next_slash = memchr(b'/', &self.0[read_pos..])
1082                    .map(|pos| pos + read_pos)
1083                    .unwrap_or(len);
1084
1085                let segment_len = next_slash - read_pos;
1086
1087                // Copy the segment of non-slash bytes to the write position if needed.
1088                if read_pos != write_pos {
1089                    self.0.copy_within(read_pos..next_slash, write_pos);
1090                }
1091
1092                write_pos += segment_len;
1093                read_pos = next_slash;
1094            }
1095        }
1096
1097        // Truncate the vector to the new length.
1098        self.0.truncate(write_pos);
1099    }
1100
1101    /// Create a path from the given PID.
1102    pub fn from_pid(pid: Pid) -> Self {
1103        let mut buf = itoa::Buffer::new();
1104        buf.format(pid.as_raw()).as_bytes().into()
1105    }
1106
1107    /// Create a path from the given FD.
1108    pub fn from_fd(fd: RawFd) -> Self {
1109        let mut buf = itoa::Buffer::new();
1110        buf.format(fd).as_bytes().into()
1111    }
1112
1113    /// Create a path for the given self-FD.
1114    ///
1115    /// Used for _procfs_(5) indirection.
1116    pub fn from_self_fd(fd: RawFd) -> Self {
1117        // SAFETY:
1118        // Use /proc/thread-self rather than /proc/self
1119        // because CLONE_FILES may be in effect!
1120        let mut pfd = Self::from("thread-self/fd");
1121        pfd.push_fd(fd);
1122        pfd
1123    }
1124
1125    /// Append the formatted FD as a new component.
1126    pub fn push_pid(&mut self, pid: Pid) {
1127        let mut buf = itoa::Buffer::new();
1128        self.push(buf.format(pid.as_raw()).as_bytes())
1129    }
1130
1131    /// Append the formatted FD as a new component.
1132    pub fn push_fd(&mut self, fd: RawFd) {
1133        let mut buf = itoa::Buffer::new();
1134        self.push(buf.format(fd).as_bytes())
1135    }
1136
1137    /// Append a path component, managing separators correctly.
1138    pub fn push(&mut self, path: &[u8]) {
1139        if path.first() == Some(&b'/') {
1140            // Absolute path replaces pbuf.
1141            self.0.clear();
1142        } else if self.last().map(|c| c != b'/').unwrap_or(true) {
1143            // Add separator if needed (last!=/ or empty path).
1144            self.append_byte(b'/');
1145        }
1146        // Append new path part.
1147        self.append_bytes(path);
1148    }
1149
1150    /// Remove the last path component.
1151    pub fn pop(&mut self) {
1152        self.truncate(self.parent_len());
1153    }
1154
1155    /// Remove the last path component without checks.
1156    ///
1157    /// # Safety
1158    ///
1159    /// 1. Path must be a normalized absolute path!
1160    /// 2. Path must not have a trailing slash!
1161    #[inline]
1162    pub unsafe fn pop_unchecked(&mut self) {
1163        #[allow(clippy::arithmetic_side_effects)]
1164        if let Some(idx) = memrchr(b'/', &self.as_bytes()[1..]) {
1165            self.0.truncate(idx + 1);
1166        } else if self.0.len() > 1 {
1167            self.0.truncate(1);
1168        }
1169    }
1170
1171    /// Append raw bytes to the path buffer.
1172    pub fn append_bytes(&mut self, bytes: &[u8]) {
1173        self.0.extend(bytes.iter().copied())
1174    }
1175
1176    /// Append a raw byte to the path buffer.
1177    pub fn append_byte(&mut self, byte: u8) {
1178        self.0.push(byte)
1179    }
1180
1181    /// Remove the last byte and return it or None if path is empty.
1182    pub fn pop_last(&mut self) -> Option<u8> {
1183        self.0.pop()
1184    }
1185
1186    /// Convert a `XPathBuf` to a `Vec`.
1187    pub fn into_vec(self) -> Vec<u8> {
1188        self.0
1189    }
1190
1191    /// Convert a `XPathBuf` to an `OsString`.
1192    pub fn into_os_string(self) -> OsString {
1193        OsString::from_vec(self.0)
1194    }
1195
1196    /// Shorten the vector, keeping the first len elements and dropping
1197    /// the rest. If len is greater than or equal to the vector’s
1198    /// current length, this has no effect.
1199    pub fn truncate(&mut self, len: usize) {
1200        self.0.truncate(len)
1201    }
1202
1203    /// Removes and returns the element at position index within the
1204    /// vector, shifting all elements after it to the left.
1205    pub fn remove(&mut self, index: usize) -> u8 {
1206        self.0.remove(index)
1207    }
1208
1209    /// Shrink the capacity of the vector as much as possible.
1210    ///
1211    /// When possible, this will move data from an external heap buffer
1212    /// to the vector’s inline storage.
1213    pub fn shrink_to_fit(&mut self) {
1214        self.0.shrink_to_fit()
1215    }
1216
1217    /// Reserve capacity for additional more bytes to be inserted.
1218    /// May reserve more space to avoid frequent allocations.
1219    pub fn try_reserve(&mut self, additional: usize) -> Result<(), Errno> {
1220        self.0.try_reserve(additional).or(Err(Errno::ENOMEM))
1221    }
1222
1223    /// Create a new, empty `XPathBuf`.
1224    pub fn empty() -> Self {
1225        Self(vec![])
1226    }
1227
1228    /// Construct an empty `XPathBuf` with capacity pre-allocated.
1229    pub fn with_capacity(n: usize) -> Self {
1230        Self(Vec::with_capacity(n))
1231    }
1232
1233    /// Report capacity of path.
1234    pub fn capacity(&self) -> usize {
1235        self.0.capacity()
1236    }
1237
1238    /// Creates an owned `XPathBuf` with path adjoined to `self`.
1239    /// If `path` is absolute, it replaces the current path.
1240    pub fn join(&self, path: &[u8]) -> XPathBuf {
1241        let mut owned = self.clone();
1242        owned.push(path);
1243        owned
1244    }
1245
1246    /// Returns an immutable slice of the buffer.
1247    pub fn as_bytes(&self) -> &[u8] {
1248        &self.0
1249    }
1250
1251    /// Convert to a `OsStr`.
1252    pub fn as_os_str(&self) -> &OsStr {
1253        OsStr::from_bytes(&self.0)
1254    }
1255
1256    /// Convert to a `Path`.
1257    pub fn as_path(&self) -> &Path {
1258        Path::new(self.as_os_str())
1259    }
1260
1261    /// Convert to a `XPath`.
1262    pub fn as_xpath(&self) -> &XPath {
1263        XPath::new(self.as_os_str())
1264    }
1265
1266    /// Check if path is a symlink.
1267    pub fn is_symlink(&self) -> bool {
1268        self.as_path().is_symlink()
1269    }
1270
1271    /// Check if path is a dir.
1272    pub fn is_dir(&self) -> bool {
1273        self.as_path().is_dir()
1274    }
1275
1276    /// Check if path is a file.
1277    pub fn is_file(&self) -> bool {
1278        self.as_path().is_file()
1279    }
1280
1281    /// Returns a slice containing the entire path buffer.
1282    #[inline]
1283    pub fn as_slice(&self) -> &[u8] {
1284        self.0.as_slice()
1285    }
1286
1287    /// Returns a mutable slice containing the entire path buffer.
1288    #[inline]
1289    pub fn as_mut_slice(&mut self) -> &mut [u8] {
1290        self.0.as_mut_slice()
1291    }
1292
1293    /// Returns a pointer to the internal `Vec`.
1294    #[inline]
1295    pub fn as_ptr(&self) -> *const u8 {
1296        self.0.as_ptr()
1297    }
1298
1299    /// Returns a mutable pointer to the internal `Vec`.
1300    #[inline]
1301    pub fn as_mut_ptr(&mut self) -> *mut u8 {
1302        self.0.as_mut_ptr()
1303    }
1304
1305    /// Forces the length of the internal `Vec`.
1306    ///
1307    /// # Safety
1308    ///
1309    /// - `new_len` must be less than or equal to `capacity()`.
1310    /// - The elements at `old_len..new_len` must be initialized.
1311    #[inline]
1312    pub unsafe fn set_len(&mut self, new_len: usize) {
1313        self.0.set_len(new_len)
1314    }
1315}
1316
1317/// Logs an untrusted Path, escaping it as hex if it contains control
1318/// characters.
1319#[inline]
1320pub fn mask_path(path: &Path) -> String {
1321    let (mask, _) = log_untrusted_buf(path.as_os_str().as_bytes());
1322    mask
1323}
1324
1325#[inline]
1326fn is_permitted_initial(b: u8) -> bool {
1327    is_permitted_byte(b) && !matches!(b, b'-' | b' ' | b'~')
1328}
1329
1330#[inline]
1331fn is_permitted_middle(b: u8) -> bool {
1332    is_permitted_byte(b)
1333}
1334
1335#[inline]
1336fn is_permitted_final(b: u8) -> bool {
1337    is_permitted_byte(b) && b != b' '
1338}
1339
1340#[inline]
1341fn is_permitted_byte(b: u8) -> bool {
1342    match b {
1343        b'*' | b'?' | b':' | b'[' | b']' | b'"' | b'<' | b'>' | b'|' | b'(' | b')' | b'{'
1344        | b'}' | b'&' | b'\'' | b'!' | b'\\' | b';' | b'$' | b'`' => false,
1345        0x20..=0x7E => true,
1346        0x80..=0xFE => true,
1347        _ => false,
1348    }
1349}
1350
1351#[cfg(test)]
1352mod tests {
1353    use std::{sync::mpsc, thread};
1354
1355    use nix::unistd::{gettid, pause};
1356
1357    use super::*;
1358
1359    struct CCSTestCase<'a> {
1360        src: &'a str,
1361        dst: &'a str,
1362    }
1363
1364    const CCS_TESTS: &[CCSTestCase] = &[
1365        CCSTestCase { src: "/", dst: "/" },
1366        CCSTestCase {
1367            src: "///",
1368            dst: "/",
1369        },
1370        CCSTestCase {
1371            src: "////",
1372            dst: "/",
1373        },
1374        CCSTestCase {
1375            src: "//home/alip///",
1376            dst: "/home/alip/",
1377        },
1378        CCSTestCase {
1379            src: "//home/alip///.config///",
1380            dst: "/home/alip/.config/",
1381        },
1382        CCSTestCase {
1383            src: "//home/alip///.config///htop////",
1384            dst: "/home/alip/.config/htop/",
1385        },
1386        CCSTestCase {
1387            src: "//home/alip///.config///htop////htoprc",
1388            dst: "/home/alip/.config/htop/htoprc",
1389        },
1390    ];
1391
1392    #[test]
1393    fn test_clean_consecutive_slashes() {
1394        for (idx, test) in CCS_TESTS.iter().enumerate() {
1395            let mut path = XPathBuf::from(test.src);
1396            path.clean_consecutive_slashes();
1397            assert_eq!(
1398                path,
1399                XPathBuf::from(test.dst),
1400                "Test {idx}: {} -> {path} != {}",
1401                test.src,
1402                test.dst
1403            );
1404        }
1405    }
1406
1407    struct EndsWithDotTestCase<'a> {
1408        path: &'a str,
1409        test: bool,
1410    }
1411
1412    const ENDS_WITH_DOT_TESTS: &[EndsWithDotTestCase] = &[
1413        EndsWithDotTestCase {
1414            path: ".",
1415            test: true,
1416        },
1417        EndsWithDotTestCase {
1418            path: "..",
1419            test: true,
1420        },
1421        EndsWithDotTestCase {
1422            path: "...",
1423            test: false,
1424        },
1425        EndsWithDotTestCase {
1426            path: "/.",
1427            test: true,
1428        },
1429        EndsWithDotTestCase {
1430            path: "/..",
1431            test: true,
1432        },
1433        EndsWithDotTestCase {
1434            path: "/...",
1435            test: false,
1436        },
1437        EndsWithDotTestCase {
1438            path: "foo.",
1439            test: false,
1440        },
1441        EndsWithDotTestCase {
1442            path: "foo./.",
1443            test: true,
1444        },
1445        EndsWithDotTestCase {
1446            path: "foo/./././/./",
1447            test: true,
1448        },
1449        EndsWithDotTestCase {
1450            path: "conftest.dir/././././////",
1451            test: true,
1452        },
1453    ];
1454
1455    #[test]
1456    fn test_ends_with_dot() {
1457        for (idx, test) in ENDS_WITH_DOT_TESTS.iter().enumerate() {
1458            let ends = XPath::from_bytes(test.path.as_bytes()).ends_with_dot();
1459            assert_eq!(
1460                test.test, ends,
1461                "EndsWithDotTestCase {} -> \"{}\": {} != {}",
1462                idx, test.path, test.test, ends
1463            );
1464        }
1465    }
1466
1467    #[test]
1468    fn test_is_dot() {
1469        let cases = [
1470            (".", true),
1471            ("./", true),
1472            (".///", true),
1473            ("././", true),
1474            ("./././", true),
1475            ("././././", true),
1476            ("/././", true),
1477            ("/./././", true),
1478            (".//././", true),
1479            ("", false),
1480            ("/", false),
1481            ("..", false),
1482            ("./..", false),
1483            ("../", false),
1484            ("././..", false),
1485            ("./../", false),
1486            ("./a", false),
1487            ("a/.", false),
1488            ("././a", false),
1489            ("a/./.", false),
1490            ("./././..", false),
1491            ("./.hidden", false),
1492            ("././.hidden", false),
1493            ("some/./path", false),
1494            ("./some/path", false),
1495            ("some/path/.", false),
1496            ("/some/path", false),
1497        ];
1498
1499        for &(input, expected) in &cases {
1500            let path = XPath::from_bytes(input.as_bytes());
1501            assert_eq!(path.is_dot(), expected, "Failed on input: {:?}", input);
1502        }
1503    }
1504
1505    #[test]
1506    fn test_descendant_of() {
1507        let cases = [
1508            ("/", "/", true),
1509            ("/foo", "/", true),
1510            ("/foo/bar", "/", true),
1511            ("/foo", "/foo", true),
1512            ("/foo/bar", "/foo", true),
1513            ("/foo2", "/foo", false),
1514            ("/foot", "/foo", false),
1515            ("/fo", "/foo", false),
1516            ("/", "/foo", false),
1517            ("/foo/bar", "/foo/bar", true),
1518            ("/foo/bar/baz", "/foo/bar", true),
1519            ("/foo/barbaz", "/foo/bar", false),
1520            ("/foo", "/foo/bar", false),
1521        ];
1522
1523        for &(path, root, expected) in &cases {
1524            let path = XPath::from_bytes(path.as_bytes());
1525            assert_eq!(
1526                path.descendant_of(root.as_bytes()),
1527                expected,
1528                "Failed on input: {path:?} of {root}!"
1529            );
1530        }
1531    }
1532
1533    #[test]
1534    fn test_path_check_file_type() {
1535        assert!(XPathBuf::from("/proc")
1536            .check(Pid::from_raw(1), Some(&FileType::Dir), None, true)
1537            .is_ok());
1538        assert!(XPathBuf::from("/proc")
1539            .check(
1540                Pid::from_raw(1),
1541                Some(&FileType::Dir),
1542                Some(&XPath::from_bytes(b"self")),
1543                true,
1544            )
1545            .is_ok());
1546        assert!(XPathBuf::from("/proc")
1547            .check(
1548                Pid::from_raw(1),
1549                Some(&FileType::Reg),
1550                Some(&XPath::from_bytes(b"uptime")),
1551                true,
1552            )
1553            .is_ok());
1554        assert!(XPathBuf::from("/dev/null")
1555            .check(Pid::from_raw(1), Some(&FileType::Chr), None, true)
1556            .is_ok());
1557        assert!(XPathBuf::from("/dev/log")
1558            .check(Pid::from_raw(1), Some(&FileType::Sock), None, true)
1559            .is_ok());
1560        assert!(XPathBuf::from("/dev/fifo")
1561            .check(Pid::from_raw(1), Some(&FileType::Fifo), None, true)
1562            .is_ok());
1563        assert!(XPathBuf::from("/dev/sda1")
1564            .check(Pid::from_raw(1), Some(&FileType::Blk), None, true)
1565            .is_err());
1566        assert!(XPathBuf::from("/dev/lmao")
1567            .check(Pid::from_raw(1), Some(&FileType::Unk), None, true)
1568            .is_err());
1569    }
1570
1571    #[test]
1572    fn test_path_check_procfs() {
1573        let this = Pid::from_raw(128);
1574        let that = Pid::from_raw(256);
1575        assert!(XPathBuf::from("/proc")
1576            .check(this, Some(&FileType::Dir), Some(&xpath!("{this}")), true,)
1577            .is_ok());
1578        assert!(XPathBuf::from(format!("/proc/{this}"))
1579            .check(
1580                this,
1581                Some(&FileType::Reg),
1582                Some(&XPath::from_bytes(b"mem")),
1583                true,
1584            )
1585            .is_ok());
1586        assert!(XPathBuf::from(format!("/proc/{this}"))
1587            .check(
1588                this,
1589                Some(&FileType::Dir),
1590                Some(&XPath::from_bytes(b"")),
1591                true,
1592            )
1593            .is_ok());
1594        assert!(XPathBuf::from(format!("/proc/{this}/task"))
1595            .check(this, Some(&FileType::Dir), Some(&xpath!("{this}")), true,)
1596            .is_ok());
1597        assert!(XPathBuf::from("/proc")
1598            .check(this, Some(&FileType::Dir), Some(&xpath!("{that}")), true,)
1599            .is_err());
1600        assert!(XPathBuf::from(format!("/proc/{that}"))
1601            .check(
1602                this,
1603                Some(&FileType::Reg),
1604                Some(&XPath::from_bytes(b"")),
1605                true,
1606            )
1607            .is_ok());
1608        assert!(XPathBuf::from(format!("/proc/{that}"))
1609            .check(
1610                this,
1611                Some(&FileType::Dir),
1612                Some(&XPath::from_bytes(b"")),
1613                true,
1614            )
1615            .is_ok());
1616        assert!(XPathBuf::from(format!("/proc/{that}/task"))
1617            .check(this, Some(&FileType::Dir), Some(&xpath!("{that}")), true,)
1618            .is_ok());
1619    }
1620
1621    #[test]
1622    fn test_path_check_procfs_syd_leader() {
1623        let syd = Pid::this();
1624        assert!(XPathBuf::from("/proc")
1625            .check(syd, Some(&FileType::Dir), Some(&xpath!("{syd}")), true,)
1626            .is_err());
1627        assert!(XPathBuf::from(format!("/proc/{syd}"))
1628            .check(
1629                syd,
1630                Some(&FileType::Reg),
1631                Some(&XPath::from_bytes(b"")),
1632                true,
1633            )
1634            .is_err());
1635        assert!(XPathBuf::from(format!("/proc/{syd}"))
1636            .check(
1637                syd,
1638                Some(&FileType::Dir),
1639                Some(&XPath::from_bytes(b"")),
1640                true,
1641            )
1642            .is_err());
1643        assert!(XPathBuf::from(format!("/proc/{syd}/task"))
1644            .check(syd, Some(&FileType::Dir), Some(&xpath!("{syd}")), true,)
1645            .is_err());
1646    }
1647
1648    #[test]
1649    fn test_path_check_procfs_syd_thread() {
1650        // Spawn a new thread.
1651        let tid = {
1652            let (tx, rx) = mpsc::channel();
1653            thread::spawn(move || {
1654                tx.send(gettid()).unwrap();
1655                pause();
1656            });
1657            rx.recv().unwrap()
1658        };
1659        assert!(XPathBuf::from("/proc")
1660            .check(tid, Some(&FileType::Dir), Some(&xpath!("{tid}")), true,)
1661            .is_err());
1662        assert!(XPathBuf::from(format!("/proc/{tid}"))
1663            .check(
1664                tid,
1665                Some(&FileType::Reg),
1666                Some(&XPath::from_bytes(b"")),
1667                true,
1668            )
1669            .is_err());
1670        assert!(XPathBuf::from(format!("/proc/{tid}"))
1671            .check(
1672                tid,
1673                Some(&FileType::Dir),
1674                Some(&XPath::from_bytes(b"")),
1675                true,
1676            )
1677            .is_err());
1678        assert!(XPathBuf::from(format!("/proc/{tid}/task"))
1679            .check(tid, Some(&FileType::Dir), Some(&xpath!("{tid}")), true,)
1680            .is_err());
1681    }
1682
1683    #[test]
1684    fn test_path_split_prefix_absolute() {
1685        let path = XPathBuf::from("/tmp/foo/bar/baz");
1686
1687        assert_eq!(path.split_prefix(b"/").unwrap().as_bytes(), path.as_bytes());
1688
1689        assert!(path.split_prefix(b"/tm").is_none());
1690        assert_eq!(
1691            path.split_prefix(b"/tmp").unwrap().as_bytes(),
1692            b"foo/bar/baz"
1693        );
1694
1695        assert!(path.split_prefix(b"/tmp/f").is_none());
1696        assert_eq!(
1697            path.split_prefix(b"/tmp/foo/").unwrap().as_bytes(),
1698            b"bar/baz"
1699        );
1700
1701        assert_eq!(
1702            path.split_prefix(b"/tmp/foo/bar/baz").unwrap().as_bytes(),
1703            b""
1704        );
1705    }
1706
1707    #[test]
1708    fn test_path_split_prefix_relative() {
1709        let path = XPathBuf::from("tmp/foo/bar/baz");
1710
1711        assert!(path.split_prefix(b"t").is_none());
1712        assert!(path.split_prefix(b"tm").is_none());
1713
1714        assert_eq!(
1715            path.split_prefix(b"tmp").unwrap().as_bytes(),
1716            b"foo/bar/baz"
1717        );
1718        assert_eq!(
1719            path.split_prefix(b"tmp/").unwrap().as_bytes(),
1720            b"foo/bar/baz"
1721        );
1722
1723        assert_eq!(
1724            path.split_prefix(b"tmp/foo/bar/baz").unwrap().as_bytes(),
1725            b""
1726        );
1727    }
1728
1729    #[test]
1730    fn test_path_pop_unchecked() {
1731        let mut path = XPathBuf::from("/usr/host/bin/id");
1732        unsafe { path.pop_unchecked() };
1733        assert_eq!(path, XPathBuf::from("/usr/host/bin"));
1734        unsafe { path.pop_unchecked() };
1735        assert_eq!(path, XPathBuf::from("/usr/host"));
1736        unsafe { path.pop_unchecked() };
1737        assert_eq!(path, XPathBuf::from("/usr"));
1738        unsafe { path.pop_unchecked() };
1739        assert_eq!(path, XPathBuf::from("/"));
1740        unsafe { path.pop_unchecked() };
1741        assert_eq!(path, XPathBuf::from("/"));
1742    }
1743
1744    #[test]
1745    fn test_path_pop() {
1746        // Truncates self to self.parent.
1747        // Popping `/' gives itself back.
1748        let mut path = XPathBuf::from("/spirited/away.rs");
1749        path.pop();
1750        assert_eq!(path, XPathBuf::from("/spirited"));
1751        path.pop();
1752        assert_eq!(path, XPathBuf::from("/"));
1753        path.pop();
1754        assert_eq!(path, XPathBuf::from("/"));
1755    }
1756
1757    #[test]
1758    fn test_path_push() {
1759        // Pushing a relative path extends the existing path.
1760        let mut path = XPathBuf::from("/tmp");
1761        path.push(b"file.bk");
1762        assert_eq!(path, XPathBuf::from("/tmp/file.bk"));
1763
1764        // Pushing an absolute path replaces the existing path
1765        let mut path = XPathBuf::from("/tmp");
1766        path.push(b"/etc");
1767        assert_eq!(path, XPathBuf::from("/etc"));
1768
1769        let mut path = XPathBuf::from("/tmp/bar");
1770        path.push(b"baz/");
1771        assert_eq!(path, XPathBuf::from("/tmp/bar/baz/"));
1772
1773        // Pushing an empty string appends a trailing slash.
1774        let mut path = XPathBuf::from("/tmp");
1775        path.push(b"");
1776        assert_eq!(path, XPathBuf::from("/tmp/"));
1777        assert_eq!(path.as_os_str().as_bytes(), b"/tmp/");
1778    }
1779
1780    #[test]
1781    fn test_path_split() {
1782        // Test typical path without trailing slash
1783        let path = XPathBuf::from("/foo/bar/baz");
1784        let (parent, file_name) = path.split();
1785        assert_eq!(parent, XPath::from_bytes(b"/foo/bar"));
1786        assert_eq!(file_name, XPath::from_bytes(b"baz"));
1787
1788        // Test path with trailing slash
1789        let path = XPathBuf::from("/foo/bar/baz/");
1790        let (parent, file_name) = path.split();
1791        assert_eq!(parent, XPath::from_bytes(b"/foo/bar"));
1792        assert_eq!(file_name, XPath::from_bytes(b"baz/"));
1793
1794        // Test root path "/"
1795        let path = XPathBuf::from("/");
1796        let (parent, file_name) = path.split();
1797        assert_eq!(parent, XPath::from_bytes(b"/"));
1798        assert_eq!(file_name, XPath::from_bytes(b"/"));
1799
1800        // Test single level path without trailing slash
1801        let path = XPathBuf::from("/foo");
1802        let (parent, file_name) = path.split();
1803        assert_eq!(parent, XPath::from_bytes(b"/"));
1804        assert_eq!(file_name, XPath::from_bytes(b"foo"));
1805
1806        // Test single level path with trailing slash
1807        let path = XPathBuf::from("/foo/");
1808        let (parent, file_name) = path.split();
1809        assert_eq!(parent, XPath::from_bytes(b"/"));
1810        assert_eq!(file_name, XPath::from_bytes(b"foo/"));
1811    }
1812
1813    #[test]
1814    fn test_path_is_proc_pid() {
1815        assert!(XPathBuf::from("/proc/1").is_proc_pid());
1816        assert!(XPathBuf::from("/proc/1/").is_proc_pid());
1817
1818        assert!(XPathBuf::from("/proc/123456789").is_proc_pid());
1819        assert!(XPathBuf::from("/proc/123456789/task").is_proc_pid());
1820
1821        assert!(!XPathBuf::from("/proc").is_proc_pid());
1822        assert!(!XPathBuf::from("/proc/").is_proc_pid());
1823
1824        assert!(!XPathBuf::from("/proc/acpi").is_proc_pid());
1825        assert!(!XPathBuf::from("/proc/keys").is_proc_pid());
1826
1827        // FIXME: This should return false, but it does not matter in practise.
1828        assert!(XPathBuf::from("/proc/0keys").is_proc_pid());
1829
1830        assert!(!XPathBuf::from("/dev").is_proc_pid());
1831        assert!(!XPathBuf::from("/dev/0").is_proc_pid());
1832
1833        assert!(!XPathBuf::from("/pro").is_proc_pid());
1834        assert!(!XPathBuf::from("/pro/").is_proc_pid());
1835        assert!(!XPathBuf::from("/pro/1").is_proc_pid());
1836    }
1837
1838    #[test]
1839    fn test_check_name_valid() {
1840        let valid_filenames = [
1841            "valid_filename.txt",
1842            "hello_world",
1843            "File123",
1844            "こんにちは", // Japanese characters
1845            "文件",       // Chinese characters
1846            "emoji😀",    // Starts with permitted character
1847            "valid~name", // '~' allowed in middle
1848            "name~",      // '~' allowed at end
1849            "a",
1850            "normal",
1851            "test-file",
1852            "test_file",
1853            "file name",
1854            "file☃name",    // Snowman character
1855            "name\u{0080}", // Contains 0x80 (allowed)
1856            "name\u{00FE}", // Contains 0xFE (allowed)
1857            "😀name",       // Multi-byte character at start
1858            "name😀",       // Multi-byte character at end
1859            "😀",           // Single multi-byte character
1860            "name😀name",   // Multi-byte character in middle
1861            "na~me",        // '~' allowed in middle
1862            "name-",        // Hyphen at end (allowed)
1863            "name_",        // Underscore at end (allowed)
1864            "name.",        // Period at end (allowed)
1865            "a\u{0020}b",   // SPACE in the middle (allowed)
1866            "a\u{00A0}b",   // NO-BREAK SPACE in the middle (allowed)
1867            "a\u{1680}b",   // OGHAM SPACE MARK in the middle (allowed)
1868            "a\u{2007}b",   // FIGURE SPACE in the middle (allowed)
1869            "a\u{202F}b",   // NARROW NO-BREAK SPACE in the middle (allowed)
1870            "a\u{3000}b",   // IDEOGRAPHIC SPACE in the middle (allowed)
1871        ];
1872
1873        for (idx, name) in valid_filenames.iter().enumerate() {
1874            let name = XPath::new(name);
1875            assert!(
1876                name.check_name().is_ok(),
1877                "Filename {idx} '{name}' should be valid"
1878            );
1879        }
1880    }
1881
1882    #[test]
1883    fn test_check_name_invalid() {
1884        let invalid_filenames: &[&[u8]] = &[
1885            b"",                             // Empty filename
1886            b"-",                            // Starts with '-'
1887            b"*",                            // Starts with '*'
1888            b"?",                            // Starts with '?'
1889            b"!",                            // Starts with '!'
1890            b"$",                            // Starts with '$'
1891            b"`",                            // Starts with '`'
1892            b" -",                           // Starts with space
1893            b"~home",                        // Starts with '~'
1894            b"*home",                        // Starts with '*'
1895            b"?home",                        // Starts with '?'
1896            b"!home",                        // Starts with '!'
1897            b"$home",                        // Starts with '$'
1898            b"`home",                        // Starts with '`'
1899            b"file ",                        // Ends with space
1900            b"file*",                        // Ends with '*'
1901            b"file?",                        // Ends with '?'
1902            b"file!",                        // Ends with '!'
1903            b"file$",                        // Ends with '$'
1904            b"file`",                        // Ends with '`'
1905            b"bad*name",                     // Contains '*'
1906            b"bad?name",                     // Contains '?'
1907            b"bad!name",                     // Contains '!'
1908            b"bad$name",                     // Contains '$'
1909            b"bad`name",                     // Contains '`'
1910            b"bad\nname",                    // Contains newline
1911            b"\0",                           // Null byte
1912            b"bad\0name",                    // Contains null byte
1913            b"bad\x7Fname",                  // Contains delete character
1914            b"bad\xFFname",                  // Contains 0xFF
1915            b"\x1Fcontrol",                  // Starts with control character
1916            b"name\x1F",                     // Ends with control character
1917            b"name\x7F",                     // Ends with delete character
1918            b"name\xFF",                     // Ends with 0xFF
1919            b"name ",                        // Ends with space
1920            b"-name",                        // Starts with '-'
1921            b" name",                        // Starts with space
1922            b"~name",                        // Starts with '~'
1923            b"*name",                        // Starts with '*'
1924            b"?name",                        // Starts with '?'
1925            b"!name",                        // Starts with '!'
1926            b"$name",                        // Starts with '$'
1927            b"`name",                        // Starts with '`'
1928            b"name\x19",                     // Contains control character
1929            b"name\n",                       // Ends with newline
1930            b"\nname",                       // Starts with newline
1931            b"na\nme",                       // Contains newline
1932            b"name\t",                       // Contains tab
1933            b"name\r",                       // Contains carriage return
1934            b"name\x1B",                     // Contains escape character
1935            b"name\x00",                     // Contains null byte
1936            b"name\x7F",                     // Contains delete character
1937            b"name\xFF",                     // Contains 0xFF (disallowed)
1938            b"\xFF",                         // Single byte 0xFF
1939            b"name\x80\xFF",                 // Contains valid and invalid extended ASCII
1940            b"name\xC0\xAF",                 // Invalid UTF-8 sequence
1941            b"\xF0\x28\x8C\xBC",             // Invalid UTF-8 sequence
1942            b"\xF0\x90\x28\xBC",             // Invalid UTF-8 sequence
1943            b"\xF0\x28\x8C\x28",             // Invalid UTF-8 sequence
1944            b"name\xFFname",                 // Contains 0xFF
1945            b"name\xC3\x28",                 // Invalid UTF-8 sequence
1946            b"name\xA0\xA1",                 // Invalid UTF-8 sequence
1947            b"\xE2\x28\xA1",                 // Invalid UTF-8 sequence
1948            b"\xE2\x82\x28",                 // Invalid UTF-8 sequence
1949            b"\xF0\x28\x8C\xBC",             // Invalid UTF-8 sequence
1950            b"\xF0\x90\x28\xBC",             // Invalid UTF-8 sequence
1951            b"\xF0\x28\x8C\x28",             // Invalid UTF-8 sequence
1952            b"\xC2\xA0",                     // Non-breaking space
1953            b"\x20file",                     // leading SPACE U+0020
1954            b"file\x20",                     // trailing SPACE U+0020
1955            b"\xC2\xA0file",                 // leading NO-BREAK SPACE U+00A0
1956            b"file\xE3\x80\x80",             // trailing IDEOGRAPHIC SPACE U+3000
1957            b"\xE2\x80\xAFfile",             // leading NARROW NO-BREAK SPACE U+202F
1958            b"\xE2\x81\x9Ffile\xE2\x81\x9F", // both sides MEDIUM MATHEMATICAL SPACE U+205F
1959        ];
1960
1961        for (idx, name) in invalid_filenames.iter().enumerate() {
1962            let name = XPath::from_bytes(name);
1963            assert!(
1964                name.check_name().is_err(),
1965                "Filename {idx} '{name}' should not be valid"
1966            );
1967        }
1968    }
1969
1970    #[test]
1971    fn test_check_name_control_characters() {
1972        for b in 0x00..=0x1F {
1973            if let Some(c) = char::from_u32(b as u32) {
1974                let name = format!("name{c}char");
1975                let name = XPath::new(&name);
1976                assert!(
1977                    name.check_name().is_err(),
1978                    "Filename with control character '\\x{b:02X}' should be invalid",
1979                );
1980            }
1981        }
1982    }
1983
1984    #[test]
1985    fn test_check_name_extended_ascii_characters() {
1986        for b in 0x80..=0xFE {
1987            if b == 0xFF {
1988                continue; // 0xFF is disallowed.
1989            }
1990            let mut bytes = b"name".to_vec();
1991            bytes.push(b);
1992            bytes.extend_from_slice(b"char");
1993            let name = OsStr::from_bytes(&bytes);
1994            let name = XPath::new(name);
1995            let result = name.check_name();
1996            if std::str::from_utf8(&bytes).is_ok() {
1997                assert!(result.is_ok(), "Filename with byte 0x{b:X} should be valid",);
1998            } else {
1999                assert!(
2000                    result.is_err(),
2001                    "Filename with invalid UTF-8 byte 0x{b:X} should be invalid",
2002                );
2003            }
2004        }
2005    }
2006
2007    #[test]
2008    fn test_check_name_edge_cases() {
2009        // Filenames with length 1
2010        let valid_single_chars = [
2011            "a", "b", "Z", "9", "_", ".", "😀", // Valid multi-byte character
2012        ];
2013
2014        for (idx, name) in valid_single_chars.iter().enumerate() {
2015            let name = XPath::new(name);
2016            assert!(
2017                name.check_name().is_ok(),
2018                "Single-character filename {idx} '{name}' should be valid",
2019            );
2020        }
2021
2022        let invalid_single_chars: &[&[u8]] = &[
2023            b"-",            // Starts with '-'
2024            b" ",            // Space character
2025            b"~",            // Tilde character
2026            b"*",            // Starts with '*'
2027            b"?",            // Starts with '?'
2028            b"\n",           // Newline character
2029            b"\r",           // Newline character
2030            b"\x7F",         // Delete character
2031            b"\x1F",         // Control character
2032            b"\xFF",         // 0xFF disallowed
2033            b"\0",           // Null byte
2034            b"\xC2\xA0",     // Non-breaking space
2035            b"\x20",         // SPACE U+0020
2036            b"\xC2\xA0",     // NO-BREAK SPACE U+00A0
2037            b"\xE1\x9A\x80", // OGHAM SPACE MARK U+1680
2038            b"\xE2\x80\x87", // FIGURE SPACE U+2007
2039            b"\xE2\x80\xAF", // NARROW NO-BREAK SPACE U+202F
2040            b"\xE3\x80\x80", // IDEOGRAPHIC SPACE U+3000
2041        ];
2042
2043        for (idx, name) in invalid_single_chars.iter().enumerate() {
2044            let name = XPath::from_bytes(name);
2045            assert!(
2046                name.check_name().is_err(),
2047                "Single-character filename {idx} '{name}' should be invalid",
2048            );
2049        }
2050    }
2051}