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)]
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> {
346 let base = base.as_ref();
347
348 let diff_path = diff_utf8_paths(self, base);
349
350 diff_path.map(SPath::from)
351 }
352
353 pub fn try_diff(&self, base: impl AsRef<Utf8Path>) -> Result<SPath> {
354 self.diff(&base).ok_or_else(|| Error::CannotDiff {
355 path: self.to_string(),
356 base: base.as_ref().to_string(),
357 })
358 }
359
360 pub fn replace_prefix(&self, base: impl AsRef<str>, with: impl AsRef<str>) -> SPath {
365 let base = base.as_ref();
366 let with = with.as_ref();
367 let s = self.as_str();
368 if let Some(stripped) = s.strip_prefix(base) {
369 let joined = if with.is_empty() || with.ends_with('/') || stripped.starts_with('/') {
371 format!("{with}{stripped}")
372 } else {
373 format!("{with}/{stripped}")
374 };
375 SPath::new(joined)
376 } else {
377 self.clone()
378 }
379 }
380
381 pub fn into_replace_prefix(self, base: impl AsRef<str>, with: impl AsRef<str>) -> SPath {
382 let base = base.as_ref();
383 let with = with.as_ref();
384 let s = self.as_str();
385 if let Some(stripped) = s.strip_prefix(base) {
386 let joined = if with.is_empty() || with.ends_with('/') || stripped.starts_with('/') {
387 format!("{with}{stripped}")
388 } else {
389 format!("{with}/{stripped}")
390 };
391 SPath::new(joined)
392 } else {
393 self
394 }
395 }
396
397 }
399
400impl SPath {
402 pub fn as_std_path(&self) -> &Path {
403 self.std_path()
404 }
405
406 pub fn strip_prefix(&self, prefix: impl AsRef<Path>) -> Result<SPath> {
412 let prefix = prefix.as_ref();
413 let new_path = self.path_buf.strip_prefix(prefix).map_err(|_| Error::StripPrefix {
414 prefix: prefix.to_string_lossy().to_string(),
415 path: self.to_string(),
416 })?;
417
418 Ok(new_path.into())
419 }
420
421 pub fn starts_with(&self, base: impl AsRef<Path>) -> bool {
444 self.path_buf.starts_with(base)
445 }
446}
447
448impl SPath {
450 pub fn into_ensure_extension(mut self, ext: &str) -> Self {
457 if self.extension() != Some(ext) {
458 self.path_buf.set_extension(ext);
459 }
460 self
461 }
462
463 pub fn ensure_extension(&self, ext: &str) -> Self {
473 self.clone().into_ensure_extension(ext)
474 }
475
476 pub fn append_extension(&self, ext: &str) -> Self {
481 SPath::new(format!("{self}.{ext}"))
482 }
483}
484
485impl SPath {
487 pub fn dir_before_glob(&self) -> Option<SPath> {
496 let path_str = self.as_str();
497 let mut last_slash_idx = None;
498
499 for (i, c) in path_str.char_indices() {
500 if c == '/' {
501 last_slash_idx = Some(i);
502 } else if matches!(c, '*' | '?' | '[' | '{') {
503 return Some(SPath::from(&path_str[..last_slash_idx.unwrap_or(0)]));
504 }
505 }
506
507 None
508 }
509}
510
511impl fmt::Display for SPath {
514 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
515 write!(f, "{}", self.as_str())
516 }
517}
518
519impl AsRef<SPath> for SPath {
524 fn as_ref(&self) -> &SPath {
525 self
526 }
527}
528
529impl AsRef<Path> for SPath {
530 fn as_ref(&self) -> &Path {
531 self.path_buf.as_ref()
532 }
533}
534
535impl AsRef<Utf8Path> for SPath {
536 fn as_ref(&self) -> &Utf8Path {
537 self.path_buf.as_ref()
538 }
539}
540
541impl AsRef<str> for SPath {
542 fn as_ref(&self) -> &str {
543 self.as_str()
544 }
545}
546
547impl From<SPath> for String {
552 fn from(val: SPath) -> Self {
553 val.as_str().to_string()
554 }
555}
556
557impl From<&SPath> for String {
558 fn from(val: &SPath) -> Self {
559 val.as_str().to_string()
560 }
561}
562
563impl From<SPath> for PathBuf {
564 fn from(val: SPath) -> Self {
565 val.into_std_path_buf()
566 }
567}
568
569impl From<&SPath> for PathBuf {
570 fn from(val: &SPath) -> Self {
571 val.path_buf.clone().into()
572 }
573}
574
575impl From<SPath> for Utf8PathBuf {
576 fn from(val: SPath) -> Self {
577 val.path_buf
578 }
579}
580
581impl From<Utf8PathBuf> for SPath {
586 fn from(path_buf: Utf8PathBuf) -> Self {
587 SPath::new(path_buf)
588 }
589}
590
591impl From<&Utf8Path> for SPath {
592 fn from(path: &Utf8Path) -> Self {
593 SPath::new(path)
594 }
595}
596
597impl From<String> for SPath {
598 fn from(path: String) -> Self {
599 SPath::new(path)
600 }
601}
602
603impl From<&String> for SPath {
604 fn from(path: &String) -> Self {
605 SPath::new(path)
606 }
607}
608
609impl From<&str> for SPath {
610 fn from(path: &str) -> Self {
611 SPath::new(path)
612 }
613}
614
615impl TryFrom<PathBuf> for SPath {
620 type Error = Error;
621 fn try_from(path_buf: PathBuf) -> Result<SPath> {
622 SPath::from_std_path_buf(path_buf)
623 }
624}
625
626impl TryFrom<fs::DirEntry> for SPath {
627 type Error = Error;
628 fn try_from(fs_entry: fs::DirEntry) -> Result<SPath> {
629 SPath::from_std_path_buf(fs_entry.path())
630 }
631}
632
633impl TryFrom<walkdir::DirEntry> for SPath {
634 type Error = Error;
635 fn try_from(wd_entry: walkdir::DirEntry) -> Result<SPath> {
636 SPath::from_std_path(wd_entry.path())
637 }
638}
639
640pub(crate) fn validate_spath_for_result(path: impl Into<PathBuf>) -> Result<Utf8PathBuf> {
645 let path = path.into();
646 let path_buf =
647 Utf8PathBuf::from_path_buf(path).map_err(|err| Error::PathNotUtf8(err.to_string_lossy().to_string()))?;
648 Ok(path_buf)
649}
650
651pub(crate) fn validate_spath_for_option(path: impl Into<PathBuf>) -> Option<Utf8PathBuf> {
653 Utf8PathBuf::from_path_buf(path.into()).ok()
654}
655
656