Skip to main content

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