tokenless_core/
safe_path.rs1use std::path::{Path, PathBuf};
2
3#[derive(Debug, Clone, PartialEq, Eq, Hash)]
8pub struct SafePath(PathBuf);
9
10impl SafePath {
11 pub fn new(path: impl AsRef<Path>) -> crate::Result<Self> {
22 let path = path.as_ref();
23
24 if path.is_absolute() {
25 return Err(crate::CoreError::Path(
26 "absolute paths are not allowed".into(),
27 ));
28 }
29
30 for component in path.components() {
31 use std::path::Component;
32 match component {
33 Component::ParentDir => {
34 return Err(crate::CoreError::Path(
35 "'..' components are not allowed".into(),
36 ));
37 }
38 Component::Normal(os_str) => {
39 let s = os_str.to_string_lossy();
40 if s.contains('\0') {
41 return Err(crate::CoreError::Path("null bytes are not allowed".into()));
42 }
43 if os_str.len() > 255 {
44 return Err(crate::CoreError::Path(format!(
45 "path component exceeds 255 bytes: '{s}'"
46 )));
47 }
48 if s.contains('\u{202A}')
50 || s.contains('\u{202B}')
51 || s.contains('\u{202C}')
52 || s.contains('\u{202D}')
53 || s.contains('\u{202E}')
54 || s.contains('\u{2066}')
55 || s.contains('\u{2067}')
56 || s.contains('\u{2068}')
57 || s.contains('\u{2069}')
58 {
59 return Err(crate::CoreError::Path(
60 "Unicode bidirectional control characters are not allowed".into(),
61 ));
62 }
63 }
64 _ => {}
65 }
66 }
67
68 Ok(Self(path.to_path_buf()))
69 }
70
71 #[must_use]
73 pub fn as_path(&self) -> &Path {
74 &self.0
75 }
76}
77
78impl AsRef<Path> for SafePath {
79 fn as_ref(&self) -> &Path {
80 &self.0
81 }
82}
83
84impl std::fmt::Display for SafePath {
85 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86 write!(f, "{}", self.0.display())
87 }
88}