file_system/fundamentals/path/
path_reference.rs

1use core::borrow::Borrow;
2
3use alloc::{borrow::ToOwned, format, string::ToString};
4
5use super::*;
6
7/// A borrowed path type.
8/// The implementation is very similar to the standard library's `std::path::Path`.
9/// However, this implementation is more lightweight and allows for std-less usage.
10#[derive(Debug, PartialEq, Eq, Hash)]
11#[repr(transparent)]
12pub struct Path(str);
13
14impl Path {
15    pub const ROOT: &'static Path = Self::from_str("/");
16    pub const EMPTY: &'static Path = Self::from_str("");
17
18    /// Contains the OS core, including the kernel, init system, and critical drivers.
19    /// Prevents modification by regular users.
20    pub const SYSTEM: &'static Path = Self::from_str("/system");
21
22    /// Stores system-wide settings in a structured format (e.g., JSON, TOML).
23    pub const DEVICES: &'static Path = Self::from_str("/devices");
24
25    /// Hardware devices, symlinks for human-friendly names.
26    pub const CONFIGURATION: &'static Path = Self::from_str("/configuration");
27
28    /// Contains the shared configurations between applications.
29    pub const SHARED_CONFIGURATION: &'static Path = Self::from_str("/configuration/shared");
30
31    /// Binaries data.
32    pub const DATA: &'static Path = Self::from_str("/data");
33
34    /// Shared data between binaries.
35    pub const SHARED_DATA: &'static Path = Self::from_str("/data/shared");
36
37    /// Contains the system's binaries, including the shell and other executables.
38    pub const BINARIES: &'static Path = Self::from_str("/binaries");
39
40    /// Contains the user's data, including documents, downloads, and other files.
41    pub const USERS: &'static Path = Self::from_str("/users");
42
43    /// Contains temporary files, including logs and caches.
44    pub const TEMPORARY: &'static Path = Self::from_str("/temporary");
45
46    /// Contains logs, including system logs and application logs.
47    pub const LOGS: &'static Path = Self::from_str("/temporary/logs");
48
49    /// # Safety
50    /// The caller must ensure that the string is a valid path string.
51    pub const fn from_str(path: &str) -> &Path {
52        unsafe { &*(path as *const str as *const Path) }
53    }
54
55    /// # Safety
56    /// The caller must ensure that the string is a valid path string.
57    pub fn new<S: AsRef<str> + ?Sized>(path: &S) -> &Path {
58        unsafe { &*(path.as_ref() as *const str as *const Path) }
59    }
60
61    pub fn is_valid(&self) -> bool {
62        is_valid_string(&self.0)
63    }
64
65    pub fn is_absolute(&self) -> bool {
66        self.0.starts_with('/')
67    }
68
69    pub fn is_root(&self) -> bool {
70        &self.0 == "/"
71    }
72
73    pub fn is_empty(&self) -> bool {
74        self.0.is_empty()
75    }
76
77    pub fn is_canonical(&self) -> bool {
78        self.0.chars().all(|c| c != '.')
79    }
80
81    pub fn go_parent(&self) -> Option<&Path> {
82        let characters_to_remove = match self.0.rfind(SEPARATOR) {
83            Some(index) => index,
84            None => {
85                // If there is no separator, the path is either empty or relative to the current directory.
86                if self.get_length() > 0 {
87                    // Relative to the current directory.
88                    return Some(Self::EMPTY);
89                } else {
90                    return None;
91                }
92            }
93        };
94
95        if characters_to_remove == 0 {
96            if self.get_length() == 1 {
97                return None;
98            }
99
100            if self.is_absolute() {
101                return Some(Self::ROOT);
102            } else {
103                return Some(Self::from_str(""));
104            }
105        }
106
107        Some(Self::from_str(&self.0[..characters_to_remove]))
108    }
109
110    pub fn get_file_prefix(&self) -> Option<&str> {
111        let extension_start = self
112            .0
113            .rfind(EXTENSION_SEPARATOR)
114            .or_else(|| Some(self.get_length()))?; // Find the extension separator.
115        let file_prefix_start = self.0.rfind(SEPARATOR).map(|i| i + 1).unwrap_or(0); // Find the file prefix start.
116
117        if extension_start <= file_prefix_start {
118            return None;
119        }
120
121        Some(&self.0[file_prefix_start..extension_start])
122    }
123
124    pub fn get_file_name(&self) -> Option<&str> {
125        let file_prefix_start = self.0.rfind(SEPARATOR).map(|i| i + 1).unwrap_or(0); // Find the file prefix start.
126
127        if file_prefix_start >= self.get_length() {
128            return None;
129        }
130
131        Some(&self.0[file_prefix_start..])
132    }
133
134    pub fn get_extension(&self) -> Option<&str> {
135        let extension_start = self.0.rfind(EXTENSION_SEPARATOR)?;
136
137        Some(&self.0[extension_start + 1..])
138    }
139
140    pub fn set_extension(&self, extension: &str) -> Option<PathOwned> {
141        let extension_start = self
142            .0
143            .rfind(EXTENSION_SEPARATOR)
144            .unwrap_or(self.get_length());
145
146        Some(unsafe {
147            PathOwned::new_unchecked(format!("{}{}", &self.0[..extension_start], extension))
148        })
149    }
150
151    pub fn strip_prefix<'b>(&'b self, path_prefix: &Path) -> Option<&'b Path> {
152        let mut stripped_prefix = self.0.strip_prefix(&path_prefix.0)?;
153
154        if stripped_prefix.starts_with(SEPARATOR) {
155            stripped_prefix = &stripped_prefix[1..]
156        }
157
158        Some(Self::from_str(stripped_prefix))
159    }
160
161    pub fn strip_prefix_absolute<'b>(&'b self, path_prefix: &Path) -> Option<&'b Path> {
162        if path_prefix.is_root() {
163            return Some(self);
164        }
165
166        let stripped_prefix = self.0.strip_prefix(&path_prefix.0)?;
167
168        if stripped_prefix.is_empty() {
169            return Some(Self::ROOT);
170        }
171
172        Some(Self::from_str(stripped_prefix))
173    }
174
175    pub fn strip_suffix<'b>(&'b self, path_suffix: &Path) -> Option<&'b Path> {
176        let stripped_suffix = self.0.strip_suffix(&path_suffix.0)?;
177
178        if stripped_suffix.ends_with(SEPARATOR) {
179            return Some(Self::from_str(
180                &stripped_suffix[..stripped_suffix.len() - 1],
181            ));
182        }
183
184        Some(Self::from_str(stripped_suffix))
185    }
186
187    pub fn get_components(&'_ self) -> Components<'_> {
188        Components::new(self)
189    }
190
191    pub fn join(&self, path: &Path) -> Option<PathOwned> {
192        self.to_owned().join(path)
193    }
194
195    pub fn append(&self, path: &str) -> Option<PathOwned> {
196        self.to_owned().append(path)
197    }
198
199    pub fn get_length(&self) -> usize {
200        self.0.len()
201    }
202
203    pub fn as_str(&self) -> &str {
204        &self.0
205    }
206}
207
208#[cfg(feature = "std")]
209impl AsRef<std::path::Path> for Path {
210    fn as_ref(&self) -> &std::path::Path {
211        std::path::Path::new(&self.0)
212    }
213}
214
215impl ToOwned for Path {
216    type Owned = PathOwned;
217
218    fn to_owned(&self) -> Self::Owned {
219        unsafe { PathOwned::new_unchecked(self.0.to_string()) }
220    }
221
222    fn clone_into(&self, target: &mut Self::Owned) {
223        *target = self.to_owned();
224    }
225}
226
227impl Borrow<Path> for PathOwned {
228    fn borrow(&self) -> &Path {
229        Path::from_str(&self.0)
230    }
231}
232
233impl AsRef<Path> for str {
234    fn as_ref(&self) -> &Path {
235        Path::from_str(self)
236    }
237}
238
239impl AsRef<str> for &Path {
240    fn as_ref(&self) -> &str {
241        &self.0
242    }
243}
244
245impl AsRef<Path> for Path {
246    fn as_ref(&self) -> &Path {
247        self
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254
255    #[test]
256    fn test_strip_prefix() {
257        let path = Path::from_str("/home/user/file.txt");
258        let prefix = Path::from_str("/home/user");
259        assert_eq!(path.strip_prefix(prefix).unwrap().as_str(), "file.txt");
260
261        let path = Path::from_str("/home/user/file.txt");
262        let prefix = Path::from_str("/");
263        assert_eq!(
264            path.strip_prefix(prefix).unwrap().as_str(),
265            "home/user/file.txt"
266        );
267
268        let invalid_prefix = Path::from_str("/home/invalid/");
269        assert_eq!(path.strip_prefix(invalid_prefix), None);
270    }
271
272    #[test]
273    fn test_strip_prefix_absolute() {
274        let path = Path::from_str("/");
275        let prefix = Path::from_str("/");
276        assert_eq!(path.strip_prefix_absolute(prefix).unwrap().as_str(), "/");
277
278        let path = Path::from_str("/Foo/Bar");
279        let prefix = Path::from_str("/Foo/Bar");
280        assert_eq!(path.strip_prefix_absolute(prefix).unwrap().as_str(), "/");
281
282        let path = Path::from_str("/home/user/file.txt");
283        let prefix = Path::from_str("/home/user");
284        assert_eq!(
285            path.strip_prefix_absolute(prefix).unwrap().as_str(),
286            "/file.txt"
287        );
288
289        let path = Path::from_str("/home/user/file.txt");
290        let prefix = Path::from_str("/");
291        assert_eq!(
292            path.strip_prefix_absolute(prefix).unwrap().as_str(),
293            "/home/user/file.txt"
294        );
295
296        let invalid_prefix = Path::from_str("/home/invalid/");
297        assert_eq!(path.strip_prefix_absolute(invalid_prefix), None);
298    }
299
300    #[test]
301    fn test_strip_suffix() {
302        let path = Path::from_str("/home/user/file.txt");
303        let suffix = Path::from_str("user/file.txt");
304        assert_eq!(path.strip_suffix(suffix).unwrap().as_str(), "/home");
305
306        let invalid_suffix = Path::from_str("user/invalid.txt");
307        assert_eq!(path.strip_suffix(invalid_suffix), None);
308    }
309
310    #[test]
311    fn test_go_parent() {
312        let path = Path::from_str("/home/user/file.txt");
313        assert_eq!(&path.go_parent().unwrap().0, "/home/user");
314        assert_eq!(&path.go_parent().unwrap().go_parent().unwrap().0, "/home");
315        assert_eq!(
316            &path
317                .go_parent()
318                .unwrap()
319                .go_parent()
320                .unwrap()
321                .go_parent()
322                .unwrap()
323                .0,
324            "/"
325        );
326        assert_eq!(
327            path.go_parent()
328                .unwrap()
329                .go_parent()
330                .unwrap()
331                .go_parent()
332                .unwrap()
333                .go_parent(),
334            None
335        );
336
337        let path = Path::from_str("home/user/file.txt");
338        assert_eq!(&path.go_parent().unwrap().0, "home/user");
339        assert_eq!(&path.go_parent().unwrap().go_parent().unwrap().0, "home");
340        assert_eq!(
341            &path
342                .go_parent()
343                .unwrap()
344                .go_parent()
345                .unwrap()
346                .go_parent()
347                .unwrap()
348                .0,
349            ""
350        );
351    }
352
353    #[test]
354    fn test_path_file() {
355        // Regular case
356        let path = Path::from_str("/Directory/File.txt");
357        assert_eq!(path.get_extension(), Some("txt"));
358        assert_eq!(path.get_file_prefix(), Some("File"));
359        assert_eq!(path.get_file_name(), Some("File.txt"));
360
361        // No extension
362        let path = Path::from_str("/Directory/File");
363        assert_eq!(path.get_extension(), None);
364        assert_eq!(path.get_file_prefix(), Some("File"));
365        assert_eq!(path.get_file_name(), Some("File"));
366
367        // No file prefix
368        let path = Path::from_str("File.txt");
369        assert_eq!(path.get_extension(), Some("txt"));
370        assert_eq!(path.get_file_prefix(), Some("File"));
371        assert_eq!(path.get_file_name(), Some("File.txt"));
372
373        // No file prefix or extension
374        let path = Path::from_str("/");
375        assert_eq!(path.get_extension(), None);
376        assert_eq!(path.get_file_prefix(), None);
377        assert_eq!(path.get_file_name(), None);
378
379        // No file prefix or extension
380        let path = Path::from_str("File");
381        assert_eq!(path.get_extension(), None);
382        assert_eq!(path.get_file_prefix(), Some("File"));
383        assert_eq!(path.get_file_name(), Some("File"));
384    }
385}