file_system/fundamentals/path/
path_owned.rs

1use core::{
2    fmt::{Display, Formatter},
3    ops::Deref,
4};
5
6use alloc::{
7    string::{String, ToString},
8    vec::Vec,
9};
10
11use super::{EXTENSION_SEPARATOR, Path, SEPARATOR};
12
13#[derive(Clone, PartialEq, Eq, Debug, Hash)]
14#[repr(transparent)]
15pub struct PathOwned(String);
16
17impl PathOwned {
18    /// # Safety
19    /// The caller must ensure that the string is valid path string.
20    pub unsafe fn new_unchecked(path: String) -> Self {
21        PathOwned(path)
22    }
23
24    pub fn new(path: String) -> Option<Self> {
25        let path = if path.ends_with(SEPARATOR) && path.len() > 1 {
26            path[..path.len() - 1].to_string()
27        } else {
28            path
29        };
30
31        if is_valid_string(&path) {
32            Some(PathOwned(path))
33        } else {
34            None
35        }
36    }
37
38    pub fn new_with_capacity(capacity: usize) -> Self {
39        PathOwned(String::with_capacity(capacity))
40    }
41
42    pub fn root() -> PathOwned {
43        PathOwned("/".to_string())
44    }
45
46    pub fn join(mut self, path: impl AsRef<Path>) -> Option<Self> {
47        if path.as_ref().is_absolute() {
48            return None;
49        }
50
51        if path.as_ref().is_empty() {
52            return Some(self);
53        }
54
55        if !self.0.ends_with(SEPARATOR) {
56            self.0.push(SEPARATOR);
57        }
58        self.0.push_str(path.as_ref().as_str());
59
60        Some(self)
61    }
62
63    pub fn append(self, path: &str) -> Option<Self> {
64        self.join(Path::from_str(path))
65    }
66
67    pub fn revert_parent_directory(&mut self) -> &mut Self {
68        let mut last_index = 0;
69        for (i, c) in self.0.chars().enumerate() {
70            if c == SEPARATOR {
71                last_index = i;
72            }
73        }
74        if last_index == 0 {
75            self.0.clear();
76            return self;
77        }
78
79        self.0.truncate(last_index);
80        self
81    }
82
83    pub fn get_extension(&self) -> Option<&str> {
84        let mut extension = None;
85
86        for (i, c) in self.0.char_indices() {
87            if c == EXTENSION_SEPARATOR {
88                extension = Some(&self.0[i..]);
89            }
90        }
91        extension
92    }
93
94    pub fn get_file_name(&self) -> &str {
95        let mut last_index = 0;
96        for (i, c) in self.0.chars().enumerate() {
97            if c == SEPARATOR {
98                last_index = i;
99            }
100        }
101        if last_index >= self.0.len() {
102            return &self.0[last_index..];
103        }
104        &self.0[last_index + 1..]
105    }
106
107    pub fn get_relative_to(&self, path: &PathOwned) -> Option<PathOwned> {
108        if !self.0.starts_with(path.0.as_str()) {
109            return None;
110        }
111
112        Some(PathOwned(self.0[path.0.len()..].to_string()))
113    }
114
115    pub fn canonicalize(mut self) -> Self {
116        let mut stack: Vec<&str> = Vec::new();
117
118        if self.is_absolute() {
119            stack.push("");
120        }
121
122        for component in self.0.split(SEPARATOR) {
123            match component {
124                ".." => {
125                    stack.pop();
126                }
127                "." | "" => continue,
128                _ => stack.push(component),
129            }
130        }
131
132        self.0 = stack.join("/");
133
134        if self.0.is_empty() {
135            self.0.push('/');
136        }
137
138        self
139    }
140}
141
142pub fn is_valid_string(string: &str) -> bool {
143    let invalid = ['\0', ':', '*', '?', '"', '<', '>', '|', ' '];
144
145    for character in string.chars() {
146        // Check if the string contains invalid characters.
147        if invalid.contains(&character) {
148            return false;
149        }
150    }
151
152    if string.ends_with(SEPARATOR) && string.len() > 1 {
153        // Check if the string ends with a separator and is not the root directory.
154        return false;
155    }
156
157    true
158}
159
160impl TryFrom<&str> for PathOwned {
161    type Error = ();
162
163    fn try_from(item: &str) -> Result<Self, Self::Error> {
164        if is_valid_string(item) {
165            Ok(PathOwned(item.to_string()))
166        } else {
167            Err(())
168        }
169    }
170}
171
172impl TryFrom<String> for PathOwned {
173    type Error = ();
174
175    fn try_from(item: String) -> Result<Self, Self::Error> {
176        if is_valid_string(&item) {
177            Ok(PathOwned(item))
178        } else {
179            Err(())
180        }
181    }
182}
183
184impl Display for PathOwned {
185    fn fmt(&self, formatter: &mut Formatter) -> Result<(), core::fmt::Error> {
186        write!(formatter, "{}", self.0)
187    }
188}
189
190impl AsRef<str> for PathOwned {
191    fn as_ref(&self) -> &str {
192        self.0.as_str()
193    }
194}
195
196impl Deref for PathOwned {
197    type Target = Path;
198
199    fn deref(&self) -> &Self::Target {
200        Path::from_str(self.0.as_str())
201    }
202}
203
204impl AsRef<Path> for PathOwned {
205    fn as_ref(&self) -> &Path {
206        self
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn test_path_addition() {
216        let path = PathOwned::try_from("/").unwrap();
217        assert_eq!(path.as_str(), "/");
218        let path = path.append("Folder").unwrap();
219        assert_eq!(path.as_str(), "/Folder");
220        let path = path.append("File").unwrap();
221        assert_eq!(path.as_str(), "/Folder/File");
222    }
223
224    #[test]
225    fn test_valid_string() {
226        assert!(is_valid_string("Hello"));
227        assert!(is_valid_string("Hello/World"));
228        assert!(is_valid_string("Hello/World.txt"));
229        assert!(!is_valid_string("Hello/World.txt/"));
230        assert!(!is_valid_string("Hello/World.txt:"));
231        assert!(!is_valid_string("Hello/World.txt*"));
232        assert!(!is_valid_string("Hello/World.txt?"));
233        assert!(!is_valid_string("Hello/World.txt\""));
234        assert!(!is_valid_string("Hello/World.txt<"));
235        assert!(!is_valid_string("Hello/World.txt>"));
236        assert!(!is_valid_string("Hello/World.txt|"));
237        assert!(!is_valid_string("Hello/World.txt "));
238        assert!(!is_valid_string("Hello/World.txt\0"));
239        assert!(is_valid_string(""));
240        assert!(!is_valid_string("Hello/Wo rld.txt/"));
241    }
242
243    #[test]
244    fn test_canonicalize() {
245        let path = PathOwned::try_from("/home/../home/user/./file.txt").unwrap();
246        assert_eq!(path.canonicalize().as_str(), "/home/user/file.txt");
247
248        let path = PathOwned::try_from("./home/../home/user/./file.txt").unwrap();
249        assert_eq!(path.canonicalize().as_str(), "home/user/file.txt");
250    }
251}