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