file_system/fundamentals/path/
path_owned.rs1use 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 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 if invalid.contains(&character) {
152 return false;
153 }
154 }
155
156 if string.ends_with(SEPARATOR) && string.len() > 1 {
157 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}