flams_math_archives/utils/
path_ext.rs

1use std::{
2    borrow::Cow,
3    path::{Component, Path, PathBuf},
4};
5
6use ftml_uris::{ArchiveId, UriPath};
7
8use crate::utils::errors::FileError;
9
10/// A relative path normalized and always displayed with `/` as component separator
11#[derive(Copy, Clone, Debug, PartialEq, Hash, Eq, PartialOrd, Ord)]
12pub struct RelPath<'p>(&'p Path);
13
14impl std::ops::Deref for RelPath<'_> {
15    type Target = Path;
16    #[inline]
17    fn deref(&self) -> &Self::Target {
18        self.0
19    }
20}
21impl AsRef<Path> for RelPath<'_> {
22    #[inline]
23    fn as_ref(&self) -> &Path {
24        self.0
25    }
26}
27
28impl std::fmt::Display for RelPath<'_> {
29    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30        #[cfg(target_os = "windows")]
31        {
32            use std::fmt::Write;
33            let mut first = true;
34            for c in self.0.components() {
35                if let std::path::Component::Normal(s) = c {
36                    if !first {
37                        f.write_char('/')?;
38                    }
39                    s.display().fmt(f)?;
40                    first = false;
41                }
42            }
43            Ok(())
44        }
45        #[cfg(not(target_os = "windows"))]
46        {
47            self.0.as_os_str().display().fmt(f)
48        }
49    }
50}
51impl<'s> RelPath<'s> {
52    /// # Errors
53    pub fn parse<T: std::str::FromStr>(&self) -> Result<T, T::Err> {
54        #[cfg(target_os = "windows")]
55        {
56            self.to_string().parse()
57        }
58        #[cfg(not(target_os = "windows"))]
59        {
60            self.0.as_os_str().to_str().unwrap_or("").parse()
61        }
62    }
63
64    #[must_use]
65    pub fn steps(self) -> impl DoubleEndedIterator<Item = &'s str> {
66        self.0.components().filter_map(|s| match s {
67            Component::Normal(n) => n.to_str(),
68            _ => None,
69        })
70    }
71
72    #[must_use]
73    pub fn split_last(self) -> Option<(Self, &'s str)> {
74        let last = self.steps().last()?;
75        let first = self.0.parent()?;
76        Some((Self(first), last))
77    }
78
79    #[must_use]
80    pub fn from_id(id: &'s ArchiveId) -> Self {
81        Self(Path::new(id.as_ref()))
82    }
83
84    #[must_use]
85    pub fn from_path(path: &'s UriPath) -> Self {
86        Self(Path::new(path.as_ref()))
87    }
88    #[must_use]
89    pub fn new(path: &'s str) -> Self {
90        Self(Path::new(path))
91    }
92}
93
94/*
95/// A relative path normalized and always displayed with `/` as component separator
96#[derive(Clone, Debug, PartialEq, Hash, Eq, PartialOrd, Ord)]
97pub struct RelPathBuf(PathBuf);
98impl RelPathBuf {
99    #[inline]
100    #[must_use]
101    pub fn borrow(&self) -> RelPath<'_> {
102        RelPath(&self.0)
103    }
104}
105impl RelPath<'_> {
106    #[inline]
107    #[must_use]
108    pub fn to_buf(&self) -> RelPathBuf {
109        RelPathBuf(self.0.to_path_buf())
110    }
111}
112
113impl std::fmt::Display for RelPathBuf {
114    #[inline]
115    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116        self.borrow().fmt(f)
117    }
118}
119
120impl std::ops::Deref for RelPathBuf {
121    type Target = Path;
122    #[inline]
123    fn deref(&self) -> &Self::Target {
124        &self.0
125    }
126}
127impl AsRef<Path> for RelPathBuf {
128    #[inline]
129    fn as_ref(&self) -> &Path {
130        &self.0
131    }
132}
133 */
134
135impl PartialEq<str> for RelPath<'_> {
136    fn eq(&self, o: &str) -> bool {
137        let mut others = o.split('/');
138        for s in self.0.components() {
139            let std::path::Component::Normal(s) = s else {
140                continue;
141            };
142            let Some(o) = others.next() else { return false };
143            if s.to_str().is_none_or(|s| s != o) {
144                return false;
145            }
146        }
147        others.next().is_none()
148    }
149}
150
151pub trait PathExt {
152    const PATH_SEPARATOR: char;
153    //fn as_slash_str(&self) -> Cow<'_, str>;
154    fn relative_to<'s, P: AsRef<std::path::Path>>(&'s self, ancestor: &P) -> Option<RelPath<'s>>;
155    fn same_fs_as<P: AsRef<std::path::Path>>(&self, other: &P) -> bool;
156    /// ### Errors
157    fn rename_safe<P: AsRef<std::path::Path>>(&self, target: &P) -> Result<(), FileError>;
158    /// ### Errors
159    fn copy_dir_all<P: AsRef<std::path::Path>>(&self, target: &P) -> Result<(), FileError>;
160    fn join_uri_path(&self, path: &UriPath) -> PathBuf;
161    fn as_slash_str(&self) -> Cow<'_, str>;
162}
163impl<T: AsRef<std::path::Path>> PathExt for T {
164    #[cfg(target_os = "windows")]
165    const PATH_SEPARATOR: char = '\\';
166    #[cfg(not(target_os = "windows"))]
167    const PATH_SEPARATOR: char = '/';
168    fn relative_to<'s, P: AsRef<std::path::Path>>(&'s self, ancestor: &P) -> Option<RelPath<'s>> {
169        self.as_ref()
170            .strip_prefix(ancestor.as_ref())
171            .ok()
172            .map(RelPath)
173    }
174    fn join_uri_path(&self, path: &UriPath) -> PathBuf {
175        let mut steps = path.steps();
176        // SAFETY: UriPaths are non-empty
177        let ret = self
178            .as_ref()
179            .join(unsafe { steps.next().unwrap_unchecked() });
180        steps.fold(ret, |p, n| p.join(n))
181    }
182
183    fn as_slash_str(&self) -> Cow<'_, str> {
184        // SAFTEY: don't run this on weird OSes with entirely nonstandard filepaths
185        if cfg!(windows) {
186            Cow::Owned(unsafe {
187                self.as_ref()
188                    .as_os_str()
189                    .to_str()
190                    .unwrap_unchecked()
191                    .replace('\\', "/")
192            })
193        } else {
194            Cow::Borrowed(unsafe { self.as_ref().as_os_str().to_str().unwrap_unchecked() })
195        }
196    }
197
198    #[cfg(target_os = "windows")]
199    fn same_fs_as<P: AsRef<std::path::Path>>(&self, other: &P) -> bool {
200        let Some(p1) = self
201            .as_ref()
202            .components()
203            .next()
204            .and_then(|c| c.as_os_str().to_str())
205        else {
206            return false;
207        };
208        let Some(p2) = other
209            .as_ref()
210            .components()
211            .next()
212            .and_then(|c| c.as_os_str().to_str())
213        else {
214            return false;
215        };
216        p1 == p2
217    }
218    #[cfg(target_arch = "wasm32")]
219    fn same_fs_as<P: AsRef<std::path::Path>>(&self, other: &P) -> bool {
220        impossible!()
221    }
222
223    #[cfg(not(any(target_os = "windows", target_arch = "wasm32")))]
224    fn same_fs_as<P: AsRef<std::path::Path>>(&self, other: &P) -> bool {
225        use std::os::unix::fs::MetadataExt;
226        fn existent_parent(p: &std::path::Path) -> &std::path::Path {
227            if p.exists() {
228                return p;
229            }
230            existent_parent(p.parent().unwrap_or_else(|| unreachable!()))
231        }
232        let p1 = existent_parent(self.as_ref());
233        let p2 = existent_parent(other.as_ref());
234        let md1 = p1.metadata().unwrap_or_else(|_| unreachable!());
235        let md2 = p2.metadata().unwrap_or_else(|_| unreachable!());
236        md1.dev() == md2.dev()
237    }
238
239    fn rename_safe<P: AsRef<std::path::Path>>(&self, target: &P) -> Result<(), FileError> {
240        if self.same_fs_as(target) {
241            std::fs::rename(self.as_ref(), target.as_ref())
242                .map_err(|e| FileError::Rename(self.as_ref().to_path_buf(), e))
243        } else {
244            self.copy_dir_all(target)
245        }
246    }
247
248    /// #### Errors
249    fn copy_dir_all<P: AsRef<std::path::Path>>(&self, target: &P) -> Result<(), FileError> {
250        let dst = target.as_ref();
251        let src = self.as_ref();
252        std::fs::create_dir_all(dst).map_err(|e| FileError::Creation(dst.to_path_buf(), e))?;
253        for entry in src
254            .read_dir()
255            .map_err(|e| FileError::ReadDir(src.to_path_buf(), e))?
256        {
257            let entry = entry.map_err(|e| FileError::ReadEntry(src.to_path_buf(), e))?;
258            let ty = entry
259                .file_type()
260                .map_err(|e| FileError::FileType(entry.path(), e))?;
261            let target = dst.join(entry.file_name());
262            if ty.is_dir() {
263                entry.path().copy_dir_all(&target)?;
264            } else {
265                let md = entry
266                    .metadata()
267                    .map_err(|e| FileError::MetaData(entry.path(), e))?;
268                std::fs::copy(entry.path(), &target).map_err(|e| FileError::Copying {
269                    from: entry.path(),
270                    to: target.clone(),
271                    error: e,
272                })?;
273                let mtime = filetime::FileTime::from_last_modification_time(&md);
274                filetime::set_file_mtime(&target, mtime)
275                    .map_err(|e| FileError::SetFileModTime(entry.path(), e))?;
276            }
277        }
278        Ok(())
279    }
280}