microsandbox_core/config/
path_segment.rs

1use std::{
2    ffi::{OsStr, OsString},
3    fmt::{self, Display},
4    path::{Component, Path, PathBuf},
5    str::FromStr,
6};
7
8use crate::MicrosandboxError;
9
10//--------------------------------------------------------------------------------------------------
11// Types
12//--------------------------------------------------------------------------------------------------
13
14/// Represents a single segment of a path.
15///
16/// This struct provides a way to represent and manipulate individual components
17/// of a path, ensuring they are valid path segments.
18#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
19pub struct PathSegment(OsString);
20
21//--------------------------------------------------------------------------------------------------
22// Methods
23//--------------------------------------------------------------------------------------------------
24
25impl PathSegment {
26    /// Returns the OS string representation of the segment.
27    pub fn as_os_str(&self) -> &OsStr {
28        &self.0
29    }
30
31    /// Returns the bytes representation of the segment.
32    pub fn as_bytes(&self) -> &[u8] {
33        self.as_os_str().as_encoded_bytes()
34    }
35
36    /// Returns the length of the segment in bytes.
37    pub fn len(&self) -> usize {
38        self.as_bytes().len()
39    }
40
41    /// Returns `true` if the segment is empty.
42    pub fn is_empty(&self) -> bool {
43        self.len() == 0
44    }
45}
46
47//--------------------------------------------------------------------------------------------------
48// Trait Implementations
49//--------------------------------------------------------------------------------------------------
50
51impl FromStr for PathSegment {
52    type Err = MicrosandboxError;
53
54    fn from_str(s: &str) -> Result<Self, Self::Err> {
55        PathSegment::try_from(s)
56    }
57}
58
59impl Display for PathSegment {
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        write!(f, "{}", self.0.to_string_lossy())
62    }
63}
64
65impl TryFrom<&str> for PathSegment {
66    type Error = MicrosandboxError;
67
68    fn try_from(value: &str) -> Result<Self, Self::Error> {
69        if value.is_empty() {
70            return Err(MicrosandboxError::EmptyPathSegment);
71        }
72
73        #[cfg(unix)]
74        {
75            if value.contains('/') {
76                return Err(MicrosandboxError::InvalidPathComponent(value.to_string()));
77            }
78        }
79
80        #[cfg(windows)]
81        {
82            if value.contains('/') || value.contains('\\') {
83                return Err(MicrosandboxError::InvalidPathComponent(value.to_string()));
84            }
85        }
86
87        // At this point the string does not contain any separator characters
88        let mut components = Path::new(value).components();
89        let component = components
90            .next()
91            .ok_or_else(|| MicrosandboxError::InvalidPathComponent(value.to_string()))?;
92
93        // Ensure there are no additional components
94        if components.next().is_some() {
95            return Err(MicrosandboxError::InvalidPathComponent(value.to_string()));
96        }
97
98        match component {
99            Component::Normal(comp) => Ok(PathSegment(comp.to_os_string())),
100            _ => Err(MicrosandboxError::InvalidPathComponent(value.to_string())),
101        }
102    }
103}
104
105impl<'a> TryFrom<Component<'a>> for PathSegment {
106    type Error = MicrosandboxError;
107
108    fn try_from(component: Component<'a>) -> Result<Self, Self::Error> {
109        PathSegment::try_from(&component)
110    }
111}
112
113impl<'a> TryFrom<&Component<'a>> for PathSegment {
114    type Error = MicrosandboxError;
115
116    fn try_from(component: &Component<'a>) -> Result<Self, Self::Error> {
117        match component {
118            Component::Normal(component) => Ok(PathSegment(component.to_os_string())),
119            _ => Err(MicrosandboxError::InvalidPathComponent(
120                component.as_os_str().to_string_lossy().into_owned(),
121            )),
122        }
123    }
124}
125
126impl<'a> From<&'a PathSegment> for Component<'a> {
127    fn from(segment: &'a PathSegment) -> Self {
128        Component::Normal(segment.as_os_str())
129    }
130}
131
132impl From<PathSegment> for PathBuf {
133    #[inline]
134    fn from(segment: PathSegment) -> Self {
135        PathBuf::from(segment.0)
136    }
137}
138
139impl AsRef<[u8]> for PathSegment {
140    #[inline]
141    fn as_ref(&self) -> &[u8] {
142        self.as_bytes()
143    }
144}
145
146impl AsRef<OsStr> for PathSegment {
147    #[inline]
148    fn as_ref(&self) -> &OsStr {
149        self.as_os_str()
150    }
151}
152
153impl AsRef<Path> for PathSegment {
154    #[inline]
155    fn as_ref(&self) -> &Path {
156        Path::new(self.as_os_str())
157    }
158}
159
160//--------------------------------------------------------------------------------------------------
161// Tests
162//--------------------------------------------------------------------------------------------------
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    #[test]
169    fn test_segment_as_os_str() {
170        let segment = PathSegment::from_str("example").unwrap();
171        assert_eq!(segment.as_os_str(), OsStr::new("example"));
172    }
173
174    #[test]
175    fn test_segment_as_bytes() {
176        let segment = PathSegment::from_str("example").unwrap();
177        assert_eq!(segment.as_bytes(), b"example");
178    }
179
180    #[test]
181    fn test_segment_len() {
182        let segment = PathSegment::from_str("example").unwrap();
183        assert_eq!(segment.len(), 7);
184    }
185
186    #[test]
187    fn test_segment_display() {
188        let segment = PathSegment::from_str("example").unwrap();
189        assert_eq!(format!("{}", segment), "example");
190    }
191
192    #[test]
193    fn test_segment_try_from_str() {
194        assert!(PathSegment::try_from("example").is_ok());
195        assert!(PathSegment::from_str("example").is_ok());
196        assert!("example".parse::<PathSegment>().is_ok());
197
198        // Negative cases
199        assert!(PathSegment::from_str("").is_err());
200        assert!(PathSegment::from_str(".").is_err());
201        assert!(PathSegment::from_str("..").is_err());
202        assert!(".".parse::<PathSegment>().is_err());
203        assert!("..".parse::<PathSegment>().is_err());
204        assert!("".parse::<PathSegment>().is_err());
205        assert!(PathSegment::try_from(".").is_err());
206        assert!(PathSegment::try_from("..").is_err());
207        assert!(PathSegment::try_from("/").is_err());
208        assert!(PathSegment::try_from("").is_err());
209    }
210
211    #[test]
212    fn test_segment_from_path_segment_to_component() {
213        let segment = PathSegment::from_str("example").unwrap();
214        assert_eq!(
215            Component::from(&segment),
216            Component::Normal(OsStr::new("example"))
217        );
218    }
219
220    #[test]
221    fn test_segment_from_path_segment_to_path_buf() {
222        let segment = PathSegment::from_str("example").unwrap();
223        assert_eq!(PathBuf::from(segment), PathBuf::from("example"));
224    }
225
226    #[test]
227    fn test_segment_normal_with_special_characters() {
228        assert!(PathSegment::try_from("file.txt").is_ok());
229        assert!(PathSegment::try_from("file-name").is_ok());
230        assert!(PathSegment::try_from("file_name").is_ok());
231        assert!(PathSegment::try_from("file name").is_ok());
232        assert!(PathSegment::try_from("file:name").is_ok());
233        assert!(PathSegment::try_from("file*name").is_ok());
234        assert!(PathSegment::try_from("file?name").is_ok());
235    }
236
237    #[test]
238    #[cfg(unix)]
239    fn test_segment_with_unix_separator() {
240        // On Unix systems, forward slash is the main separator
241        assert!(PathSegment::try_from("file/name").is_err());
242        assert!(PathSegment::try_from("/").is_err());
243        assert!(PathSegment::try_from("///").is_err());
244        assert!(PathSegment::try_from("name/").is_err());
245        assert!(PathSegment::try_from("/name").is_err());
246    }
247
248    #[test]
249    #[cfg(windows)]
250    fn test_segment_with_windows_separators() {
251        // On Windows, both forward slash and backslash are separators
252        assert!(PathSegment::try_from("file\\name").is_err());
253        assert!(PathSegment::try_from("file/name").is_err());
254        assert!(PathSegment::try_from("\\").is_err());
255        assert!(PathSegment::try_from("/").is_err());
256        assert!(PathSegment::try_from("\\\\\\").is_err());
257        assert!(PathSegment::try_from("///").is_err());
258        assert!(PathSegment::try_from("name\\").is_err());
259        assert!(PathSegment::try_from("name/").is_err());
260        assert!(PathSegment::try_from("\\name").is_err());
261        assert!(PathSegment::try_from("/name").is_err());
262    }
263}