1use crate::{Error, Result, SMeta, reshape};
2use camino::{Utf8Path, Utf8PathBuf};
3use core::fmt;
4use pathdiff::diff_utf8_paths;
5use std::fs::{self, Metadata};
6use std::path::{Path, PathBuf};
7use std::time::{SystemTime, UNIX_EPOCH};
8
9#[derive(Debug, Clone, Eq, PartialEq, Hash)]
15pub struct SPath {
16 pub(crate) path_buf: Utf8PathBuf,
17}
18
19impl SPath {
21 pub fn new(path: impl Into<Utf8PathBuf>) -> Self {
24 let path_buf = path.into();
25 let path_buf = reshape::into_normalized(path_buf);
26 Self { path_buf }
27 }
28
29 pub fn from_std_path_buf(path_buf: PathBuf) -> Result<Self> {
31 let path_buf = validate_spath_for_result(path_buf)?;
32 Ok(SPath::new(path_buf))
33 }
34
35 pub fn from_std_path(path: impl AsRef<Path>) -> Result<Self> {
37 let path = path.as_ref();
38 let path_buf = validate_spath_for_result(path)?;
39 Ok(SPath::new(path_buf))
40 }
41
42 pub fn from_walkdir_entry(wd_entry: walkdir::DirEntry) -> Result<Self> {
44 let path = wd_entry.into_path();
45 let path_buf = validate_spath_for_result(path)?;
46 Ok(SPath::new(path_buf))
47 }
48
49 pub fn from_std_path_ok(path: impl AsRef<Path>) -> Option<Self> {
53 let path = path.as_ref();
54 let path_buf = validate_spath_for_option(path)?;
55 Some(SPath::new(path_buf))
56 }
57
58 pub fn from_std_path_buf_ok(path_buf: PathBuf) -> Option<Self> {
61 let path_buf = validate_spath_for_option(&path_buf)?;
62 Some(SPath::new(path_buf))
63 }
64
65 pub fn from_fs_entry_ok(fs_entry: fs::DirEntry) -> Option<Self> {
68 let path_buf = fs_entry.path();
69 let path_buf = validate_spath_for_option(&path_buf)?;
70 Some(SPath::new(path_buf))
71 }
72
73 pub fn from_walkdir_entry_ok(wd_entry: walkdir::DirEntry) -> Option<Self> {
76 let path_buf = validate_spath_for_option(wd_entry.path())?;
77 Some(SPath::new(path_buf))
78 }
79}
80
81impl SPath {
83 pub fn into_std_path_buf(self) -> PathBuf {
85 self.path_buf.into()
86 }
87
88 pub fn std_path(&self) -> &Path {
90 self.path_buf.as_std_path()
91 }
92
93 pub fn path(&self) -> &Utf8Path {
95 &self.path_buf
96 }
97}
98
99impl SPath {
101 #[deprecated(note = "use as_str()")]
106 pub fn to_str(&self) -> &str {
107 self.path_buf.as_str()
108 }
109
110 pub fn as_str(&self) -> &str {
112 self.path_buf.as_str()
113 }
114
115 pub fn file_name(&self) -> Option<&str> {
118 self.path_buf.file_name()
119 }
120
121 pub fn name(&self) -> &str {
125 self.file_name().unwrap_or_default()
126 }
127
128 pub fn parent_name(&self) -> &str {
130 self.path_buf.parent().and_then(|p| p.file_name()).unwrap_or_default()
131 }
132
133 pub fn file_stem(&self) -> Option<&str> {
137 self.path_buf.file_stem()
138 }
139
140 pub fn stem(&self) -> &str {
144 self.file_stem().unwrap_or_default()
145 }
146
147 pub fn extension(&self) -> Option<&str> {
152 self.path_buf.extension()
153 }
154
155 pub fn ext(&self) -> &str {
157 self.extension().unwrap_or_default()
158 }
159
160 pub fn is_dir(&self) -> bool {
162 self.path_buf.is_dir()
163 }
164
165 pub fn is_file(&self) -> bool {
167 self.path_buf.is_file()
168 }
169
170 pub fn exists(&self) -> bool {
172 self.path_buf.exists()
173 }
174
175 pub fn is_absolute(&self) -> bool {
177 self.path_buf.is_absolute()
178 }
179
180 pub fn is_relative(&self) -> bool {
182 self.path_buf.is_relative()
183 }
184}
185
186impl SPath {
188 #[allow(clippy::fn_to_numeric_cast)]
192 pub fn meta(&self) -> Result<SMeta> {
193 let path = self;
194
195 let metadata = self.metadata()?;
196
197 let modified = metadata.modified().map_err(|ex| Error::CantGetMetadata((path, ex).into()))?;
199 let modified_epoch_us: i64 = modified
200 .duration_since(UNIX_EPOCH)
201 .map_err(|ex| Error::CantGetMetadata((path, ex).into()))?
202 .as_micros()
203 .min(i64::MAX as u128) as i64;
204
205 let created_epoch_us = metadata
207 .modified()
208 .ok()
209 .and_then(|c| c.duration_since(UNIX_EPOCH).ok())
210 .map(|c| c.as_micros().min(i64::MAX as u128) as i64);
211 let created_epoch_us = created_epoch_us.unwrap_or(modified_epoch_us);
212
213 let size = if metadata.is_file() { metadata.len() } else { 0 };
215
216 Ok(SMeta {
217 created_epoch_us,
218 modified_epoch_us,
219 size,
220 is_file: metadata.is_file(),
221 is_dir: metadata.is_dir(),
222 })
223 }
224
225 pub fn metadata(&self) -> Result<Metadata> {
227 fs::metadata(self).map_err(|ex| Error::CantGetMetadata((self, ex).into()))
228 }
229
230 #[deprecated = "use spath.meta()"]
233 pub fn modified(&self) -> Result<SystemTime> {
234 let path = self.std_path();
235 let metadata = fs::metadata(path).map_err(|ex| Error::CantGetMetadata((path, ex).into()))?;
236 let last_modified = metadata
237 .modified()
238 .map_err(|ex| Error::CantGetMetadataModified((path, ex).into()))?;
239 Ok(last_modified)
240 }
241
242 #[deprecated = "use spath.meta()"]
246 pub fn modified_us(&self) -> Result<i64> {
247 Ok(self.meta()?.modified_epoch_us)
248 }
249}
250
251impl SPath {
253 pub fn canonicalize(&self) -> Result<SPath> {
255 let path = self
256 .path_buf
257 .canonicalize_utf8()
258 .map_err(|err| Error::CannotCanonicalize((self.std_path(), err).into()))?;
259 Ok(SPath::new(path))
260 }
261
262 pub fn collapse(&self) -> SPath {
270 let path_buf = crate::into_collapsed(self.path_buf.clone());
271 SPath::new(path_buf)
272 }
273
274 pub fn into_collapsed(self) -> SPath {
276 if self.is_collapsed() { self } else { self.collapse() }
277 }
278
279 pub fn is_collapsed(&self) -> bool {
286 crate::is_collapsed(self)
287 }
288
289 pub fn parent(&self) -> Option<SPath> {
295 self.path_buf.parent().map(SPath::from)
296 }
297
298 pub fn append_suffix(&self, suffix: &str) -> SPath {
305 SPath::new(format!("{self}{suffix}"))
306 }
307
308 pub fn join(&self, leaf_path: impl Into<Utf8PathBuf>) -> SPath {
310 let path_buf = self.path_buf.join(leaf_path.into());
311 SPath::from(path_buf)
312 }
313
314 pub fn join_std_path(&self, leaf_path: impl AsRef<Path>) -> Result<SPath> {
316 let leaf_path = leaf_path.as_ref();
317 let joined = self.std_path().join(leaf_path);
318 let path_buf = validate_spath_for_result(joined)?;
319 Ok(SPath::from(path_buf))
320 }
321
322 pub fn new_sibling(&self, leaf_path: impl AsRef<str>) -> SPath {
324 let leaf_path = leaf_path.as_ref();
325 match self.path_buf.parent() {
326 Some(parent_dir) => SPath::new(parent_dir.join(leaf_path)),
327 None => SPath::new(leaf_path),
328 }
329 }
330
331 pub fn new_sibling_std_path(&self, leaf_path: impl AsRef<Path>) -> Result<SPath> {
333 let leaf_path = leaf_path.as_ref();
334
335 match self.std_path().parent() {
336 Some(parent_dir) => SPath::from_std_path(parent_dir.join(leaf_path)),
337 None => SPath::from_std_path(leaf_path),
338 }
339 }
340
341 pub fn diff(&self, base: impl AsRef<Utf8Path>) -> Option<SPath> {
363 let base = base.as_ref();
364
365 let diff_path = diff_utf8_paths(self, base);
366
367 diff_path.map(SPath::from)
368 }
369
370 pub fn try_diff(&self, base: impl AsRef<Utf8Path>) -> Result<SPath> {
383 self.diff(&base).ok_or_else(|| Error::CannotDiff {
384 path: self.to_string(),
385 base: base.as_ref().to_string(),
386 })
387 }
388
389 pub fn replace_prefix(&self, base: impl AsRef<str>, with: impl AsRef<str>) -> SPath {
394 let base = base.as_ref();
395 let with = with.as_ref();
396 let s = self.as_str();
397 if let Some(stripped) = s.strip_prefix(base) {
398 let joined = if with.is_empty() || with.ends_with('/') || stripped.starts_with('/') {
400 format!("{with}{stripped}")
401 } else {
402 format!("{with}/{stripped}")
403 };
404 SPath::new(joined)
405 } else {
406 self.clone()
407 }
408 }
409
410 pub fn into_replace_prefix(self, base: impl AsRef<str>, with: impl AsRef<str>) -> SPath {
411 let base = base.as_ref();
412 let with = with.as_ref();
413 let s = self.as_str();
414 if let Some(stripped) = s.strip_prefix(base) {
415 let joined = if with.is_empty() || with.ends_with('/') || stripped.starts_with('/') {
416 format!("{with}{stripped}")
417 } else {
418 format!("{with}/{stripped}")
419 };
420 SPath::new(joined)
421 } else {
422 self
423 }
424 }
425
426 }
428
429impl SPath {
431 pub fn as_std_path(&self) -> &Path {
432 self.std_path()
433 }
434
435 pub fn strip_prefix(&self, prefix: impl AsRef<Path>) -> Result<SPath> {
441 let prefix = prefix.as_ref();
442 let new_path = self.path_buf.strip_prefix(prefix).map_err(|_| Error::StripPrefix {
443 prefix: prefix.to_string_lossy().to_string(),
444 path: self.to_string(),
445 })?;
446
447 Ok(new_path.into())
448 }
449
450 pub fn starts_with(&self, base: impl AsRef<Path>) -> bool {
473 self.path_buf.starts_with(base)
474 }
475}
476
477impl SPath {
479 pub fn into_ensure_extension(mut self, ext: &str) -> Self {
486 if self.extension() != Some(ext) {
487 self.path_buf.set_extension(ext);
488 }
489 self
490 }
491
492 pub fn ensure_extension(&self, ext: &str) -> Self {
502 self.clone().into_ensure_extension(ext)
503 }
504
505 pub fn append_extension(&self, ext: &str) -> Self {
510 SPath::new(format!("{self}.{ext}"))
511 }
512}
513
514impl SPath {
516 pub fn dir_before_glob(&self) -> Option<SPath> {
525 let path_str = self.as_str();
526 let mut last_slash_idx = None;
527
528 for (i, c) in path_str.char_indices() {
529 if c == '/' {
530 last_slash_idx = Some(i);
531 } else if matches!(c, '*' | '?' | '[' | '{') {
532 return Some(SPath::from(&path_str[..last_slash_idx.unwrap_or(0)]));
533 }
534 }
535
536 None
537 }
538}
539
540impl fmt::Display for SPath {
543 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
544 write!(f, "{}", self.as_str())
545 }
546}
547
548impl AsRef<SPath> for SPath {
553 fn as_ref(&self) -> &SPath {
554 self
555 }
556}
557
558impl AsRef<Path> for SPath {
559 fn as_ref(&self) -> &Path {
560 self.path_buf.as_ref()
561 }
562}
563
564impl AsRef<Utf8Path> for SPath {
565 fn as_ref(&self) -> &Utf8Path {
566 self.path_buf.as_ref()
567 }
568}
569
570impl AsRef<str> for SPath {
571 fn as_ref(&self) -> &str {
572 self.as_str()
573 }
574}
575
576impl From<SPath> for String {
581 fn from(val: SPath) -> Self {
582 val.as_str().to_string()
583 }
584}
585
586impl From<&SPath> for String {
587 fn from(val: &SPath) -> Self {
588 val.as_str().to_string()
589 }
590}
591
592impl From<SPath> for PathBuf {
593 fn from(val: SPath) -> Self {
594 val.into_std_path_buf()
595 }
596}
597
598impl From<&SPath> for PathBuf {
599 fn from(val: &SPath) -> Self {
600 val.path_buf.clone().into()
601 }
602}
603
604impl From<SPath> for Utf8PathBuf {
605 fn from(val: SPath) -> Self {
606 val.path_buf
607 }
608}
609
610impl From<Utf8PathBuf> for SPath {
615 fn from(path_buf: Utf8PathBuf) -> Self {
616 SPath::new(path_buf)
617 }
618}
619
620impl From<&Utf8Path> for SPath {
621 fn from(path: &Utf8Path) -> Self {
622 SPath::new(path)
623 }
624}
625
626impl From<String> for SPath {
627 fn from(path: String) -> Self {
628 SPath::new(path)
629 }
630}
631
632impl From<&String> for SPath {
633 fn from(path: &String) -> Self {
634 SPath::new(path)
635 }
636}
637
638impl From<&str> for SPath {
639 fn from(path: &str) -> Self {
640 SPath::new(path)
641 }
642}
643
644impl TryFrom<PathBuf> for SPath {
649 type Error = Error;
650 fn try_from(path_buf: PathBuf) -> Result<SPath> {
651 SPath::from_std_path_buf(path_buf)
652 }
653}
654
655impl TryFrom<fs::DirEntry> for SPath {
656 type Error = Error;
657 fn try_from(fs_entry: fs::DirEntry) -> Result<SPath> {
658 SPath::from_std_path_buf(fs_entry.path())
659 }
660}
661
662impl TryFrom<walkdir::DirEntry> for SPath {
663 type Error = Error;
664 fn try_from(wd_entry: walkdir::DirEntry) -> Result<SPath> {
665 SPath::from_std_path(wd_entry.path())
666 }
667}
668
669pub(crate) fn validate_spath_for_result(path: impl Into<PathBuf>) -> Result<Utf8PathBuf> {
674 let path = path.into();
675 let path_buf =
676 Utf8PathBuf::from_path_buf(path).map_err(|err| Error::PathNotUtf8(err.to_string_lossy().to_string()))?;
677 Ok(path_buf)
678}
679
680pub(crate) fn validate_spath_for_option(path: impl Into<PathBuf>) -> Option<Utf8PathBuf> {
682 Utf8PathBuf::from_path_buf(path.into()).ok()
683}
684
685