Skip to main content

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 truncate(&mut self, length: usize) {
64        self.0.truncate(length);
65    }
66
67    pub fn append(self, path: &str) -> Option<Self> {
68        self.join(Path::from_str(path))
69    }
70
71    pub fn revert_parent_directory(&mut self) -> &mut Self {
72        let mut last_index = 0;
73        for (i, c) in self.0.chars().enumerate() {
74            if c == SEPARATOR {
75                last_index = i;
76            }
77        }
78        if last_index == 0 {
79            self.0.clear();
80            return self;
81        }
82
83        self.0.truncate(last_index);
84        self
85    }
86
87    pub fn get_extension(&self) -> Option<&str> {
88        let mut extension = None;
89
90        for (i, c) in self.0.char_indices() {
91            if c == EXTENSION_SEPARATOR {
92                extension = Some(&self.0[i..]);
93            }
94        }
95        extension
96    }
97
98    pub fn get_file_name(&self) -> &str {
99        let mut last_index = 0;
100        for (i, c) in self.0.chars().enumerate() {
101            if c == SEPARATOR {
102                last_index = i;
103            }
104        }
105        if last_index >= self.0.len() {
106            return &self.0[last_index..];
107        }
108        &self.0[last_index + 1..]
109    }
110
111    pub fn get_relative_to(&self, path: &PathOwned) -> Option<PathOwned> {
112        if !self.0.starts_with(path.0.as_str()) {
113            return None;
114        }
115
116        Some(PathOwned(self.0[path.0.len()..].to_string()))
117    }
118
119    pub fn canonicalize(mut self) -> Self {
120        let mut stack: Vec<&str> = Vec::new();
121
122        if self.is_absolute() {
123            stack.push("");
124        }
125
126        for component in self.0.split(SEPARATOR) {
127            match component {
128                ".." => {
129                    stack.pop();
130                }
131                "." | "" => continue,
132                _ => stack.push(component),
133            }
134        }
135
136        self.0 = stack.join("/");
137
138        if self.0.is_empty() {
139            self.0.push('/');
140        }
141
142        self
143    }
144}
145
146pub fn is_valid_string(string: &str) -> bool {
147    let invalid = ['\0', ':', '*', '?', '"', '<', '>', '|', ' '];
148
149    for character in string.chars() {
150        // Check if the string contains invalid characters.
151        if invalid.contains(&character) {
152            return false;
153        }
154    }
155
156    if string.ends_with(SEPARATOR) && string.len() > 1 {
157        // Check if the string ends with a separator and is not the root directory.
158        return false;
159    }
160
161    true
162}
163
164impl TryFrom<&str> for PathOwned {
165    type Error = ();
166
167    fn try_from(item: &str) -> Result<Self, Self::Error> {
168        if is_valid_string(item) {
169            Ok(PathOwned(item.to_string()))
170        } else {
171            Err(())
172        }
173    }
174}
175
176impl TryFrom<String> for PathOwned {
177    type Error = ();
178
179    fn try_from(item: String) -> Result<Self, Self::Error> {
180        if is_valid_string(&item) {
181            Ok(PathOwned(item))
182        } else {
183            Err(())
184        }
185    }
186}
187
188impl Display for PathOwned {
189    fn fmt(&self, formatter: &mut Formatter) -> Result<(), core::fmt::Error> {
190        write!(formatter, "{}", self.0)
191    }
192}
193
194impl AsRef<str> for PathOwned {
195    fn as_ref(&self) -> &str {
196        self.0.as_str()
197    }
198}
199
200impl Deref for PathOwned {
201    type Target = Path;
202
203    fn deref(&self) -> &Self::Target {
204        Path::from_str(self.0.as_str())
205    }
206}
207
208impl AsRef<Path> for PathOwned {
209    fn as_ref(&self) -> &Path {
210        self
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217
218    #[test]
219    fn test_path_addition() {
220        let path = PathOwned::try_from("/").unwrap();
221        assert_eq!(path.as_str(), "/");
222        let path = path.append("Folder").unwrap();
223        assert_eq!(path.as_str(), "/Folder");
224        let path = path.append("File").unwrap();
225        assert_eq!(path.as_str(), "/Folder/File");
226    }
227
228    #[test]
229    fn test_valid_string() {
230        assert!(is_valid_string("Hello"));
231        assert!(is_valid_string("Hello/World"));
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<"));
239        assert!(!is_valid_string("Hello/World.txt>"));
240        assert!(!is_valid_string("Hello/World.txt|"));
241        assert!(!is_valid_string("Hello/World.txt "));
242        assert!(!is_valid_string("Hello/World.txt\0"));
243        assert!(is_valid_string(""));
244        assert!(!is_valid_string("Hello/Wo rld.txt/"));
245    }
246
247    #[test]
248    fn test_canonicalize() {
249        let path = PathOwned::try_from("/home/../home/user/./file.txt").unwrap();
250        assert_eq!(path.canonicalize().as_str(), "/home/user/file.txt");
251
252        let path = PathOwned::try_from("./home/../home/user/./file.txt").unwrap();
253        assert_eq!(path.canonicalize().as_str(), "home/user/file.txt");
254    }
255}