Skip to main content

tendrils_core/
path_ext.rs

1use 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    /// Appends the given `path` to `self`, regardless of whether the given
11    /// path is absolute or relative. Any directory separators at the
12    /// end of `self` or start of `path` are preserved. If neither `self` ends
13    /// with, or `path` starts with a directory separator, one is added. On
14    /// Unix, only `/` is considered a directory separator, but on Windows both
15    /// `/` and `\` are.
16    fn join_raw(&self, path: &Path) -> PathBuf;
17
18    /// Returns the type of the file system object that
19    /// the path points to, or returns `None` if the FSO
20    /// does not exist.
21    fn get_type(&self) -> Option<FsoType>;
22
23    #[cfg(windows)]
24    /// Replaces all forward slashes (`/`) with backslashes (`\`).
25    fn replace_dir_seps(&self) -> PathBuf;
26
27    /// Replaces the first instance of `~` with the `HOME` variable
28    /// and returns the replaced string. If `HOME` doesn't exist,
29    /// `HOMEDRIVE` and `HOMEPATH` will be combined provided they both exist,
30    /// otherwise it returns `self`. This fallback is mainly a Windows specific
31    /// issue, but is supported on all platforms either way.
32    fn resolve_tilde(&self) -> PathBuf;
33
34    /// Replaces all environment variables in the format `<varname>` in the
35    /// given path with their values. If the variable is not found, the
36    /// `<varname>` is left as-is in the path.
37    ///
38    /// # Limitations
39    /// If the path contains the `<pattern>` and the pattern corresponds to
40    /// an environment variable, there is no way to escape the brackets
41    /// to force it to use the raw path. This should only be an issue
42    /// on Unix (as Windows doesn't allow `<` or `>` in paths anyways),
43    /// and only when the variable exists (otherwise it uses the raw
44    /// path). A work-around is to set the variable value to `<pattern>`.
45    /// In the future, an escape character such as `|` could be
46    /// implemented, but this added complexity was avoided for now.
47    fn resolve_env_variables(&self) -> PathBuf;
48
49    /// Converts a non-rooted path to rooted by prepending it with the given
50    /// `root`. If the given `root` is not rooted either, then the default root
51    /// of `/` on Unix or `\` on Windows is used. Returns `self` if the path is
52    /// already rooted. What counts as rooted varies by platform - for example
53    /// `C:\Path` and `\\MyServer\Share\Path` are rooted on Windows but not on
54    /// Unix. This function does *not* take the current directory into account.
55    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            // All bytes were originally from an OsString, or are the known path
126            // separators so this call is safe.
127            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            // Continue
138        }
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                    // All bytes were originally from an OsString so this call
148                    // is safe.
149                    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                // All bytes were originally from an OsString so this call
170                // is safe.
171                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
209/// Returns the `(start index, end index)` of the next environment variable
210/// name, including the surrounding brackets, starting the search from the
211/// `search_start_idx`. Returns `None` if no variables remain at or after the
212/// start index.
213fn 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/// A [`PathBuf`] wrapper that guarantees that the path has been resolved in
236/// this particular order:
237///     1. Any environment variables have been resolved
238///     2. A leading tilde has been resolved
239///     3. A non-rooted path has been rooted. The default conversion to rooted
240/// occurs by prepending `/` on Unix or `\` on Windows. A different root can be
241/// provided by constructing the [`UniPath`] using [`UniPath::new_with_root`].
242///     4. Unix style path separators (`/`) have been replaced with `\` (Windows
243/// only)
244#[derive(Clone, Debug, PartialEq, Eq)]
245pub struct UniPath(PathBuf);
246
247impl UniPath {
248    /// Converts the given `path` to a [`UniPath`] using the standard rules, but
249    /// converts relative paths to absolute by appending them to the given
250    /// `root`. If the given `root` is not absolute either, it will default to
251    /// using `/` on Unix and `\` on Windows.
252    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    /// The wrapped [`PathBuf`] that has been sanitized.
272    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}