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 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 if invalid.contains(&character) {
148 return false;
149 }
150 }
151
152 if string.ends_with(SEPARATOR) && string.len() > 1 {
153 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}