flams_math_archives/utils/
path_ext.rs1use std::{
2 borrow::Cow,
3 path::{Component, Path, PathBuf},
4};
5
6use ftml_uris::{ArchiveId, UriPath};
7
8use crate::utils::errors::FileError;
9
10#[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 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
94impl 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 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 fn rename_safe<P: AsRef<std::path::Path>>(&self, target: &P) -> Result<(), FileError>;
158 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 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 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 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}