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::path::{Path, PathBuf};
14
15    #[cfg(unix)]
16    use super::unix as sys;
17
18    #[cfg(windows)]
19    use super::windows as sys;
20
21    /// Returns all paths which 'protect' the input path, if the path is not protected, this returns [`None`]
22    ///
23    /// The path is protected if is contained inside [`sys::protected_paths()`] or starts with a path in [`sys::protected_directories()`]
24    pub fn is_protected_path(item: impl AsRef<Path>) -> Vec<PathBuf> {
25        let item = item.as_ref();
26
27        let mut protected_by = Vec::new();
28
29        if let Some(path) = sys::protected_paths().iter().find(|p| *p == item) {
30            protected_by.push(path.to_owned());
31        }
32
33        sys::protected_directories().iter().for_each(|path| {
34            if item.starts_with(path) {
35                protected_by.push(path.to_owned());
36            }
37        });
38
39        protected_by
40    }
41}
42
43/// Absolutizes a path to the current working directory of the application
44///
45/// # Errors
46///
47/// Errors if [`std::env::current_dir()`] errors.
48pub fn absolutize_to_cwd(path: impl AsRef<Path>) -> io::Result<PathBuf> {
49    let abs = env::current_dir()?.join(path);
50    Ok(abs)
51}
52
53/// Returns the latest existing ancestor (directory) of a path
54///
55/// If you had `/foo/exists/doesnt_exist/bar`, this function would return `/foo/exists`
56/// If you had `/foo/file`, this function would return `/foo`
57pub fn latest_existing_ancestor(path: impl AsRef<Path>) -> io::Result<PathBuf> {
58    let mut latest = Some(path.as_ref());
59
60    while let Some(path) = latest {
61        match path.symlink_metadata() {
62            Ok(meta) => {
63                if meta.is_dir() {
64                    return Ok(path.to_path_buf());
65                }
66                latest = path.parent()
67            }
68            Err(err) if matches!(err.kind(), io::ErrorKind::NotFound) => latest = path.parent(),
69            Err(err) => return Err(err),
70        }
71    }
72
73    Err(io::Error::new(
74        io::ErrorKind::NotFound,
75        format!(
76            "No ancestor was found for {path}",
77            path = path.as_ref().display()
78        ),
79    ))
80}
81
82/// Normalizes a path, removing `.` and `..`
83pub fn normalize_path(path: impl AsRef<Path>) -> PathBuf {
84    let mut normalized = PathBuf::new();
85
86    for component in path.as_ref().components() {
87        match component {
88            path::Component::CurDir => {}
89            path::Component::ParentDir => {
90                if !normalized.pop() {
91                    normalized.push("..");
92                }
93            }
94            _ => normalized.push(component),
95        }
96    }
97
98    normalized.iter().collect()
99}
100
101/// Follows a symlink to the target it points to
102///
103/// # Errors
104///
105/// Errors if [`std::fs::read_link()`] or [`std::fs::symlink_metadata()`] returns an error
106pub fn follow_symlink(symlink: impl AsRef<Path>) -> io::Result<PathBuf> {
107    let target = fs::read_link(symlink.as_ref())?;
108
109    let real_target = if target.is_absolute() {
110        target
111    } else {
112        symlink
113            .as_ref()
114            .parent()
115            .unwrap_or(Path::new("/"))
116            .join(target)
117    };
118
119    if let Err(err) = real_target.symlink_metadata() {
120        if matches!(err.kind(), io::ErrorKind::NotFound) {
121            return Err(io::Error::new(
122                io::ErrorKind::NotFound,
123                format!(
124                    "Target of symlink '{symlink}' does not exist ({target})",
125                    symlink = symlink.as_ref().display(),
126                    target = real_target.display()
127                ),
128            ));
129        } else {
130            return Err(err);
131        }
132    };
133
134    Ok(real_target)
135}
136
137/// Follows an entire symlink chain until it reaches a file, directory, or a loop
138///
139/// Paths are normalized via [`normalize_path()`] to prevent cyclical symlink chains
140///
141/// # Errors
142///
143/// Errors if [`follow_symlink()`] errors
144pub fn follow_symlink_chain(symlink: impl AsRef<Path>) -> (Vec<PathBuf>, Option<io::Error>) {
145    let mut stack = vec![symlink.as_ref().to_path_buf()];
146
147    loop {
148        let target = match follow_symlink(stack.last().unwrap()) {
149            Ok(target) => normalize_path(target),
150            Err(err) => return (stack, Some(err)),
151        };
152
153        if stack.contains(&target) {
154            stack.push(target);
155            return (
156                stack,
157                Some(io::Error::new(
158                    io::ErrorKind::TooManyLinks,
159                    format!(
160                        "{symlink} is a cyclical",
161                        symlink = symlink.as_ref().display()
162                    ),
163                )),
164            );
165        }
166
167        if target.is_symlink() {
168            stack.push(target);
169            continue;
170        } else {
171            stack.push(target);
172            return (stack, None);
173        }
174    }
175}