1use crate::{Error, Result, reshape};
2use camino::{Utf8Path, Utf8PathBuf};
3use core::fmt;
4use pathdiff::diff_utf8_paths;
5use std::fs;
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 modified(&self) -> Result<SystemTime> {
177 let path = self.std_path();
178 let metadata = fs::metadata(path).map_err(|ex| Error::CantGetMetadata((path, ex).into()))?;
179 let last_modified = metadata
180 .modified()
181 .map_err(|ex| Error::CantGetMetadataModified((path, ex).into()))?;
182 Ok(last_modified)
183 }
184
185 pub fn modified_us(&self) -> Result<i64> {
189 let modified = self.modified()?;
190 let since_the_epoch = modified
191 .duration_since(UNIX_EPOCH)
192 .map_err(Error::CantGetDurationSystemTimeError)?;
193
194 let modified_us = since_the_epoch.as_micros().min(i64::MAX as u128) as i64;
195
196 Ok(modified_us)
197 }
198
199 pub fn is_absolute(&self) -> bool {
201 self.path_buf.is_absolute()
202 }
203
204 pub fn is_relative(&self) -> bool {
206 self.path_buf.is_relative()
207 }
208}
209
210impl SPath {
212 pub fn canonicalize(&self) -> Result<SPath> {
214 let path = self
215 .path_buf
216 .canonicalize_utf8()
217 .map_err(|err| Error::CannotCanonicalize((self.std_path(), err).into()))?;
218 Ok(SPath::new(path))
219 }
220
221 pub fn collapse(&self) -> SPath {
229 let path_buf = crate::into_collapsed(self.path_buf.clone());
230 SPath::new(path_buf)
231 }
232
233 pub fn into_collapsed(self) -> SPath {
235 if self.is_collapsed() { self } else { self.collapse() }
236 }
237
238 pub fn is_collapsed(&self) -> bool {
245 crate::is_collapsed(self)
246 }
247
248 pub fn parent(&self) -> Option<SPath> {
254 self.path_buf.parent().map(SPath::from)
255 }
256
257 pub fn append_suffix(&self, suffix: &str) -> SPath {
264 SPath::new(format!("{self}{suffix}"))
265 }
266
267 pub fn join(&self, leaf_path: impl Into<Utf8PathBuf>) -> SPath {
269 let path_buf = self.path_buf.join(leaf_path.into());
270 SPath::from(path_buf)
271 }
272
273 pub fn join_std_path(&self, leaf_path: impl AsRef<Path>) -> Result<SPath> {
275 let leaf_path = leaf_path.as_ref();
276 let joined = self.std_path().join(leaf_path);
277 let path_buf = validate_spath_for_result(joined)?;
278 Ok(SPath::from(path_buf))
279 }
280
281 pub fn new_sibling(&self, leaf_path: impl AsRef<str>) -> SPath {
283 let leaf_path = leaf_path.as_ref();
284 match self.path_buf.parent() {
285 Some(parent_dir) => SPath::new(parent_dir.join(leaf_path)),
286 None => SPath::new(leaf_path),
287 }
288 }
289
290 pub fn new_sibling_std_path(&self, leaf_path: impl AsRef<Path>) -> Result<SPath> {
292 let leaf_path = leaf_path.as_ref();
293
294 match self.std_path().parent() {
295 Some(parent_dir) => SPath::from_std_path(parent_dir.join(leaf_path)),
296 None => SPath::from_std_path(leaf_path),
297 }
298 }
299
300 pub fn diff(&self, base: impl AsRef<Utf8Path>) -> Option<SPath> {
305 let base = base.as_ref();
306
307 let diff_path = diff_utf8_paths(self, base);
308
309 diff_path.map(SPath::from)
310 }
311
312 pub fn try_diff(&self, base: impl AsRef<Utf8Path>) -> Result<SPath> {
313 self.diff(&base).ok_or_else(|| Error::CannotDiff {
314 path: self.to_string(),
315 base: base.as_ref().to_string(),
316 })
317 }
318
319 }
321
322impl SPath {
324 pub fn into_ensure_extension(mut self, ext: &str) -> Self {
331 if self.extension() != Some(ext) {
332 self.path_buf.set_extension(ext);
333 }
334 self
335 }
336
337 pub fn ensure_extension(&self, ext: &str) -> Self {
346 self.clone().into_ensure_extension(ext)
347 }
348
349 pub fn append_extension(&self, ext: &str) -> Self {
354 SPath::new(format!("{}.{ext}", self))
355 }
356}
357
358impl SPath {
360 pub fn dir_before_glob(&self) -> Option<SPath> {
369 let path_str = self.as_str();
370 let mut last_slash_idx = None;
371
372 for (i, c) in path_str.char_indices() {
373 if c == '/' {
374 last_slash_idx = Some(i);
375 } else if matches!(c, '*' | '?' | '[' | '{') {
376 return Some(SPath::from(&path_str[..last_slash_idx.unwrap_or(0)]));
377 }
378 }
379
380 None
381 }
382}
383
384impl fmt::Display for SPath {
387 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
388 write!(f, "{}", self.as_str())
389 }
390}
391
392impl AsRef<SPath> for SPath {
397 fn as_ref(&self) -> &SPath {
398 self
399 }
400}
401
402impl AsRef<Path> for SPath {
403 fn as_ref(&self) -> &Path {
404 self.path_buf.as_ref()
405 }
406}
407
408impl AsRef<Utf8Path> for SPath {
409 fn as_ref(&self) -> &Utf8Path {
410 self.path_buf.as_ref()
411 }
412}
413
414impl AsRef<str> for SPath {
415 fn as_ref(&self) -> &str {
416 self.as_str()
417 }
418}
419
420impl From<SPath> for String {
425 fn from(val: SPath) -> Self {
426 val.as_str().to_string()
427 }
428}
429
430impl From<&SPath> for String {
431 fn from(val: &SPath) -> Self {
432 val.as_str().to_string()
433 }
434}
435
436impl From<SPath> for PathBuf {
437 fn from(val: SPath) -> Self {
438 val.into_std_path_buf()
439 }
440}
441
442impl From<&SPath> for PathBuf {
443 fn from(val: &SPath) -> Self {
444 val.path_buf.clone().into()
445 }
446}
447
448impl From<SPath> for Utf8PathBuf {
449 fn from(val: SPath) -> Self {
450 val.path_buf
451 }
452}
453
454impl From<Utf8PathBuf> for SPath {
459 fn from(path_buf: Utf8PathBuf) -> Self {
460 SPath::new(path_buf)
461 }
462}
463
464impl From<&Utf8Path> for SPath {
465 fn from(path: &Utf8Path) -> Self {
466 SPath::new(path)
467 }
468}
469
470impl From<String> for SPath {
471 fn from(path: String) -> Self {
472 SPath::new(path)
473 }
474}
475
476impl From<&String> for SPath {
477 fn from(path: &String) -> Self {
478 SPath::new(path)
479 }
480}
481
482impl From<&str> for SPath {
483 fn from(path: &str) -> Self {
484 SPath::new(path)
485 }
486}
487
488impl TryFrom<PathBuf> for SPath {
493 type Error = Error;
494 fn try_from(path_buf: PathBuf) -> Result<SPath> {
495 SPath::from_std_path_buf(path_buf)
496 }
497}
498
499impl TryFrom<fs::DirEntry> for SPath {
500 type Error = Error;
501 fn try_from(fs_entry: fs::DirEntry) -> Result<SPath> {
502 SPath::from_std_path_buf(fs_entry.path())
503 }
504}
505
506impl TryFrom<walkdir::DirEntry> for SPath {
507 type Error = Error;
508 fn try_from(wd_entry: walkdir::DirEntry) -> Result<SPath> {
509 SPath::from_std_path(wd_entry.path())
510 }
511}
512
513pub(crate) fn validate_spath_for_result(path: impl Into<PathBuf>) -> Result<Utf8PathBuf> {
518 let path = path.into();
519 let path_buf =
520 Utf8PathBuf::from_path_buf(path).map_err(|err| Error::PathNotUtf8(err.to_string_lossy().to_string()))?;
521 Ok(path_buf)
522}
523
524pub(crate) fn validate_spath_for_option(path: impl Into<PathBuf>) -> Option<Utf8PathBuf> {
526 Utf8PathBuf::from_path_buf(path.into()).ok()
527}
528
529