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