tendrils_core/
path_ext.rs1use crate::enums::FsoType;
2use crate::env_ext::get_home_dir;
3use std::ffi::OsString;
4use std::path::{Path, PathBuf, MAIN_SEPARATOR_STR};
5
6#[cfg(test)]
7mod tests;
8
9pub(crate) trait PathExt {
10 fn join_raw(&self, path: &Path) -> PathBuf;
17
18 fn get_type(&self) -> Option<FsoType>;
22
23 #[cfg(windows)]
24 fn replace_dir_seps(&self) -> PathBuf;
26
27 fn resolve_tilde(&self) -> PathBuf;
33
34 fn resolve_env_variables(&self) -> PathBuf;
48
49 fn root(&self, root: &Path) -> PathBuf;
56}
57
58impl PathExt for Path {
59 fn join_raw(&self, path: &Path) -> PathBuf {
60 let parent_bytes = self.as_os_str().as_encoded_bytes();
61 let child_bytes = path.as_os_str().as_encoded_bytes();
62 let mut raw_str = std::ffi::OsString::from(&self);
63
64 #[cfg(not(windows))]
65 if parent_bytes.ends_with(&['/' as u8])
66 || child_bytes.starts_with(&['/' as u8]) {
67 raw_str.push(path);
68 }
69 else {
70 raw_str.push(std::path::MAIN_SEPARATOR_STR);
71 raw_str.push(path);
72 }
73
74 #[cfg(windows)]
75 if parent_bytes.ends_with(&['/' as u8])
76 || parent_bytes.ends_with(&['\\' as u8])
77 || child_bytes.starts_with(&['/' as u8])
78 || child_bytes.starts_with(&['\\' as u8]) {
79 raw_str.push(path);
80 }
81 else {
82 raw_str.push(std::path::MAIN_SEPARATOR_STR);
83 raw_str.push(path);
84 }
85
86 PathBuf::from(raw_str)
87 }
88
89 fn get_type(&self) -> Option<FsoType> {
90 if self.is_file() {
91 if self.is_symlink() {
92 Some(FsoType::SymFile)
93 }
94 else {
95 Some(FsoType::File)
96 }
97 }
98 else if self.is_dir() {
99 if self.is_symlink() {
100 Some(FsoType::SymDir)
101 }
102 else {
103 Some(FsoType::Dir)
104 }
105 }
106 else if self.is_symlink() {
107 Some(FsoType::BrokenSym)
108 }
109 else {
110 None
111 }
112 }
113
114 #[cfg(windows)]
115 fn replace_dir_seps(&self) -> PathBuf {
116 let mut bytes = Vec::from(self.as_os_str().as_encoded_bytes());
117
118 for b in bytes.iter_mut() {
119 if *b == '/' as u8 {
120 *b = std::path::MAIN_SEPARATOR as u8;
121 }
122 }
123
124 unsafe {
125 OsString::from_encoded_bytes_unchecked(bytes)
128 }.into()
129 }
130
131 fn resolve_tilde(&self) -> PathBuf {
132 let path_bytes = self.as_os_str().as_encoded_bytes();
133
134 if path_bytes == &['~' as u8]
135 || path_bytes.starts_with(&['~' as u8, '/' as u8])
136 || path_bytes.starts_with(&['~' as u8, '\\' as u8]) {
137 }
139 else {
140 return PathBuf::from(self);
141 }
142
143 match get_home_dir() {
144 Some(mut v) => {
145 let trimmed_str;
146 unsafe {
147 trimmed_str = OsString::from_encoded_bytes_unchecked(
150 path_bytes[1..].to_vec()
151 );
152 }
153
154 v.push(trimmed_str);
155 PathBuf::from(v)
156 }
157 None => PathBuf::from(self),
158 }
159 }
160
161 fn resolve_env_variables(&self) -> PathBuf {
162 let given_bytes = self.as_os_str().as_encoded_bytes();
163 let mut search_start_idx = 0;
164 let mut resolved_bytes: Vec<u8> = vec![];
165
166 while let Some(next) = next_env_var(given_bytes, search_start_idx) {
167 let var_no_brkts = &given_bytes[next.0 + 1..next.1];
168 let var_name_no_brkts = unsafe {
169 OsString::from_encoded_bytes_unchecked(var_no_brkts.to_vec())
172 };
173 if let Some(v) = std::env::var_os(var_name_no_brkts) {
174 resolved_bytes.extend(&given_bytes[search_start_idx..next.0]);
175 resolved_bytes.extend(v.as_encoded_bytes());
176 }
177 else {
178 resolved_bytes.extend(&given_bytes[search_start_idx..next.1 + 1]);
179 }
180 search_start_idx = next.1 + 1;
181 }
182
183 if search_start_idx == 0 {
184 return PathBuf::from(self);
185 }
186 else {
187 resolved_bytes.extend(&given_bytes[search_start_idx..]);
188
189 let resolved_str = unsafe {
190 OsString::from_encoded_bytes_unchecked(resolved_bytes)
191 };
192 PathBuf::from(resolved_str)
193 }
194 }
195
196 fn root(&self, root: &Path) -> PathBuf {
197 if self.has_root() {
198 PathBuf::from(self)
199 }
200 else if root.has_root() {
201 root.join_raw(self)
202 }
203 else {
204 Path::new(MAIN_SEPARATOR_STR).join_raw(self)
205 }
206 }
207}
208
209fn next_env_var(bytes: &[u8], search_start_idx: usize) -> Option<(usize, usize)> {
214 let mut var_start = 0;
215 let mut has_start = false;
216
217 for (i, b) in bytes[search_start_idx..].iter().enumerate() {
218 if *b == '<' as u8 {
219 var_start = i;
220 has_start = true;
221 }
222 else if *b == '>' as u8 && has_start {
223 return Some((search_start_idx + var_start, search_start_idx + i))
224 }
225 }
226
227 None
228}
229
230#[cfg(test)]
231pub fn contains_env_var(input: &Path) -> bool {
232 next_env_var(input.as_os_str().as_encoded_bytes(), 0).is_some()
233}
234
235#[derive(Clone, Debug, PartialEq, Eq)]
245pub struct UniPath(PathBuf);
246
247impl UniPath {
248 pub fn new_with_root(path: &Path, root: &Path) -> Self {
253 #[cfg(windows)]
254 return UniPath(
255 path
256 .resolve_env_variables()
257 .resolve_tilde()
258 .root(root)
259 .replace_dir_seps()
260 );
261
262 #[cfg(not(windows))]
263 return UniPath(
264 path
265 .resolve_env_variables()
266 .resolve_tilde()
267 .root(root)
268 );
269 }
270
271 pub fn inner(&self) -> &Path {
273 &self.0
274 }
275}
276
277impl From<&Path> for UniPath {
278 fn from(value: &Path) -> Self {
279 Self::new_with_root(value, &Path::new(MAIN_SEPARATOR_STR))
280 }
281}
282
283impl From<&PathBuf> for UniPath {
284 fn from(value: &PathBuf) -> Self {
285 Self::from(value.as_path())
286 }
287}
288
289impl From<PathBuf> for UniPath {
290 fn from(value: PathBuf) -> Self {
291 Self::from(value.as_path())
292 }
293}
294
295impl AsRef<UniPath> for UniPath {
296 fn as_ref(&self) -> &UniPath {
297 &self
298 }
299}