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}
27impl 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 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
100impl 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 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 fn rename_safe<P: AsRef<std::path::Path>>(&self, target: &P) -> Result<(), FileError>;
164 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 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 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 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}