Skip to main content

lunar_lib/
paths.rs

1use std::{
2    env, fs, io, path,
3    path::{Path, PathBuf},
4};
5
6#[cfg(unix)]
7pub mod unix;
8
9#[cfg(windows)]
10pub mod windows;
11
12pub mod sys {
13    use std::{
14        ffi::{OsStr, OsString},
15        path::{Path, PathBuf},
16    };
17
18    #[cfg(unix)]
19    use super::unix as sys;
20
21    #[cfg(windows)]
22    use super::windows as sys;
23
24    /// Returns the null device for the current platform
25    ///
26    /// On unix, this is `/dev/null`
27    /// On windows, this is `NUL`
28    pub fn null_path() -> &'static Path {
29        Path::new(sys::NULL_PATH)
30    }
31
32    /// Wrapper around [`sanitize_os_str()`], but returns a [`String`] instead of an [`OsString`]
33    pub fn sanitize_str(str: impl AsRef<str>) -> String {
34        sanitize_os_str(str.as_ref())
35            .into_string()
36            .expect("Put UTF-8 in, got non-UTF-8 out")
37    }
38
39    /// Sanitizes a string, removing platform-specific illegal characters from it
40    pub fn sanitize_os_str(os_str: impl AsRef<OsStr>) -> OsString {
41        sys::sanitize_string(os_str)
42    }
43
44    /// Sanitizes a path, removing platform-specific illegal characters from its components
45    pub fn sanitize_path(path: impl AsRef<Path>) -> PathBuf {
46        path.as_ref()
47            .components()
48            .map(|c| match c {
49                std::path::Component::Normal(os_str) => sanitize_os_str(os_str),
50                _ => c.as_os_str().to_owned(),
51            })
52            .collect()
53    }
54
55    /// Returns all paths which 'protect' the input path, if the path is not protected, this returns [`None`]
56    ///
57    /// The path is protected if is contained inside [`sys::protected_paths()`] or starts with a path in [`sys::protected_directories()`]
58    pub fn is_protected_path(item: impl AsRef<Path>) -> Vec<PathBuf> {
59        let item = item.as_ref();
60
61        let mut protected_by = Vec::new();
62
63        if let Some(path) = sys::protected_paths().iter().find(|p| *p == item) {
64            protected_by.push(path.to_owned());
65        }
66
67        sys::protected_directories().iter().for_each(|path| {
68            if item.starts_with(path) {
69                protected_by.push(path.to_owned());
70            }
71        });
72
73        protected_by
74    }
75}
76
77/// Absolutizes a path to the current working directory of the application
78///
79/// # Errors
80///
81/// Errors if [`std::env::current_dir()`] errors.
82pub fn absolutize_to_cwd(path: impl AsRef<Path>) -> io::Result<PathBuf> {
83    let abs = env::current_dir()?.join(path);
84    Ok(abs)
85}
86
87/// Returns the latest existing ancestor (directory) of a path
88///
89/// If you had `/foo/exists/doesnt_exist/bar`, this function would return `/foo/exists`
90/// If you had `/foo/file`, this function would return `/foo`
91pub fn latest_existing_ancestor(path: impl AsRef<Path>) -> io::Result<PathBuf> {
92    let mut latest = Some(path.as_ref());
93
94    while let Some(path) = latest {
95        match path.symlink_metadata() {
96            Ok(meta) => {
97                if meta.is_dir() {
98                    return Ok(path.to_path_buf());
99                }
100                latest = path.parent()
101            }
102            Err(err) if matches!(err.kind(), io::ErrorKind::NotFound) => latest = path.parent(),
103            Err(err) => return Err(err),
104        }
105    }
106
107    Err(io::Error::new(
108        io::ErrorKind::NotFound,
109        format!(
110            "No ancestor was found for {path}",
111            path = path.as_ref().display()
112        ),
113    ))
114}
115
116/// Normalizes a path, removing `.` and `..`
117pub fn normalize_path(path: impl AsRef<Path>) -> PathBuf {
118    let mut normalized = PathBuf::new();
119
120    for component in path.as_ref().components() {
121        match component {
122            path::Component::CurDir => {}
123            path::Component::ParentDir => {
124                if !normalized.pop() {
125                    normalized.push("..");
126                }
127            }
128            _ => normalized.push(component),
129        }
130    }
131
132    normalized.iter().collect()
133}
134
135/// Follows a symlink to the target it points to
136///
137/// # Errors
138///
139/// Errors if [`std::fs::read_link()`] or [`std::fs::symlink_metadata()`] returns an error
140pub fn follow_symlink(symlink: impl AsRef<Path>) -> io::Result<PathBuf> {
141    let target = fs::read_link(symlink.as_ref())?;
142
143    let real_target = if target.is_absolute() {
144        target
145    } else {
146        symlink
147            .as_ref()
148            .parent()
149            .unwrap_or(Path::new("/"))
150            .join(target)
151    };
152
153    if let Err(err) = real_target.symlink_metadata() {
154        if matches!(err.kind(), io::ErrorKind::NotFound) {
155            return Err(io::Error::new(
156                io::ErrorKind::NotFound,
157                format!(
158                    "Target of symlink '{symlink}' does not exist ({target})",
159                    symlink = symlink.as_ref().display(),
160                    target = real_target.display()
161                ),
162            ));
163        } else {
164            return Err(err);
165        }
166    };
167
168    Ok(real_target)
169}
170
171/// Follows an entire symlink chain until it reaches a file, directory, or a loop
172///
173/// Paths are normalized via [`normalize_path()`] to prevent cyclical symlink chains
174///
175/// # Errors
176///
177/// Errors if [`follow_symlink()`] errors
178pub fn follow_symlink_chain(symlink: impl AsRef<Path>) -> (Vec<PathBuf>, Option<io::Error>) {
179    let mut stack = vec![symlink.as_ref().to_path_buf()];
180
181    loop {
182        let target = match follow_symlink(stack.last().unwrap()) {
183            Ok(target) => normalize_path(target),
184            Err(err) => return (stack, Some(err)),
185        };
186
187        if stack.contains(&target) {
188            stack.push(target);
189            return (
190                stack,
191                Some(io::Error::new(
192                    io::ErrorKind::TooManyLinks,
193                    format!(
194                        "{symlink} is a cyclical",
195                        symlink = symlink.as_ref().display()
196                    ),
197                )),
198            );
199        }
200
201        if target.is_symlink() {
202            stack.push(target);
203            continue;
204        } else {
205            stack.push(target);
206            return (stack, None);
207        }
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    #[test]
214    fn sanitize_is_lossless() {
215        use std::ffi::OsString;
216
217        use crate::paths::sys::sanitize_os_str;
218
219        let os_str = OsString::from("nothing_to_replace");
220        let sanitized = sanitize_os_str(os_str);
221
222        assert_eq!(sanitized, OsString::from("nothing_to_replace"))
223    }
224}