simple_fs/
spath.rs

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/// An SPath is a posix normalized Path using camino Utf8PathBuf as strogate.
10/// It can be constructed from a String, Path, io::DirEntry, or walkdir::DirEntry
11///
12/// - It's Posix normalized `/`, all redundant `//` and `/./` are removed
13/// - Garanteed to be UTF8
14#[derive(Debug, Clone)]
15pub struct SPath {
16	pub(crate) path_buf: Utf8PathBuf,
17}
18
19/// Constructors that guarantee the SPath contract described in the struct
20impl SPath {
21	/// Constructor for SPath accepting anything that implements Into<Utf8PathBuf>.
22	/// IMPORTANT: This will normalize the path (posix style
23	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	/// Constructor from standard PathBuf.
30	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	/// Constructor from standard Path and all impl AsRef<Path>.
36	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	/// Constructor from walkdir::DirEntry
43	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	/// Constructor for anything that implements AsRef<Path>.
50	///
51	/// Returns Option<SPath>. Useful for filter_map.
52	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	/// Constructed from PathBuf returns an Option, none if validation fails.
59	/// Useful for filter_map.
60	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	/// Constructor from fs::DirEntry returning an Option, none if validation fails.
66	/// Useful for filter_map.
67	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	/// Constructor from walkdir::DirEntry returning an Option, none if validation fails.
74	/// Useful for filter_map.
75	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
81/// Public into path
82impl SPath {
83	/// Consumes the SPath and returns its PathBuf.
84	pub fn into_std_path_buf(self) -> PathBuf {
85		self.path_buf.into()
86	}
87
88	/// Returns a reference to the internal std Path.
89	pub fn std_path(&self) -> &Path {
90		self.path_buf.as_std_path()
91	}
92
93	/// Returns a reference to the internal Utf8Path.
94	pub fn path(&self) -> &Utf8Path {
95		&self.path_buf
96	}
97}
98
99/// Public getters
100impl SPath {
101	/// Returns the &str of the path.
102	///
103	/// NOTE: We know that this must be Some() since the SPath constructor guarantees that
104	///       the path.as_str() is valid.
105	#[deprecated(note = "use as_str()")]
106	pub fn to_str(&self) -> &str {
107		self.path_buf.as_str()
108	}
109
110	/// Returns the &str of the path.
111	pub fn as_str(&self) -> &str {
112		self.path_buf.as_str()
113	}
114
115	/// Returns the Option<&str> representation of the `path.file_name()`
116	///
117	pub fn file_name(&self) -> Option<&str> {
118		self.path_buf.file_name()
119	}
120
121	/// Returns the &str representation of the `path.file_name()`
122	///
123	/// Note: If no file name will be an empty string
124	pub fn name(&self) -> &str {
125		self.file_name().unwrap_or_default()
126	}
127
128	/// Returns the parent name, and empty static &str if no present
129	pub fn parent_name(&self) -> &str {
130		self.path_buf.parent().and_then(|p| p.file_name()).unwrap_or_default()
131	}
132
133	/// Returns the Option<&str> representation of the file_stem()
134	///
135	/// Note: if the `OsStr` cannot be made into utf8 will be None
136	pub fn file_stem(&self) -> Option<&str> {
137		self.path_buf.file_stem()
138	}
139
140	/// Returns the &str representation of the `file_name()`
141	///
142	/// Note: If no stem, will be an empty string
143	pub fn stem(&self) -> &str {
144		self.file_stem().unwrap_or_default()
145	}
146
147	/// Returns the Option<&str> representation of the extension().
148	///
149	/// NOTE: This should never be a non-UTF-8 string
150	///       as the path was validated during SPath construction.
151	pub fn extension(&self) -> Option<&str> {
152		self.path_buf.extension()
153	}
154
155	/// Returns the extension or "" if no extension
156	pub fn ext(&self) -> &str {
157		self.extension().unwrap_or_default()
158	}
159
160	/// Returns true if the path represents a directory.
161	pub fn is_dir(&self) -> bool {
162		self.path_buf.is_dir()
163	}
164
165	/// Returns true if the path represents a file.
166	pub fn is_file(&self) -> bool {
167		self.path_buf.is_file()
168	}
169
170	/// Checks if the path exists.
171	pub fn exists(&self) -> bool {
172		self.path_buf.exists()
173	}
174
175	/// Returns the path.metadata modified.
176	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	/// Returns the epoch duration in microseconds.
186	/// Note: The maximum UTC date would be approximately `2262-04-11`.
187	///       Thus, for all intents and purposes, it is far enough to not worry.
188	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	/// Returns true if the internal path is absolute.
200	pub fn is_absolute(&self) -> bool {
201		self.path_buf.is_absolute()
202	}
203
204	/// Returns true if the internal path is relative.
205	pub fn is_relative(&self) -> bool {
206		self.path_buf.is_relative()
207	}
208}
209
210/// Transformers
211impl SPath {
212	/// This perform a OS Canonicalization.
213	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	// region:    --- Collapse
222
223	/// Collapse a path without performing I/O.
224	///
225	/// All redundant separator and up-level references are collapsed.
226	///
227	/// However, this does not resolve links.
228	pub fn collapse(&self) -> SPath {
229		let path_buf = crate::into_collapsed(self.path_buf.clone());
230		SPath::new(path_buf)
231	}
232
233	/// Same as [`collapse`] but consume and create a new SPath only if needed
234	pub fn into_collapsed(self) -> SPath {
235		if self.is_collapsed() { self } else { self.collapse() }
236	}
237
238	/// Return `true` if the path is collapsed.
239	///
240	/// # Quirk
241	///
242	/// If the path does not start with `./` but contains `./` in the middle,
243	/// then this function might returns `true`.
244	pub fn is_collapsed(&self) -> bool {
245		crate::is_collapsed(self)
246	}
247
248	// endregion: --- Collapse
249
250	// region:    --- Parent & Join
251
252	/// Returns the parent directory as an Option<SPath>.
253	pub fn parent(&self) -> Option<SPath> {
254		self.path_buf.parent().map(SPath::from)
255	}
256
257	/// Returns a new SPath with the given suffix appended to the filename (after the eventual extension)
258	///
259	/// Use [`join`] to join path segments.
260	///
261	/// Example:
262	/// - `foo.rs` + `_backup` → `foo.rs_backup`
263	pub fn append_suffix(&self, suffix: &str) -> SPath {
264		SPath::new(format!("{self}{suffix}"))
265	}
266
267	/// Joins the provided path with the current path and returns an SPath.
268	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	/// Joins a standard Path to the path of this SPath.
274	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	/// Creates a new sibling SPath with the given leaf_path.
282	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	/// Creates a new sibling SPath with the given standard path.
291	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	// endregion: --- Parent & Join
301
302	// region:    --- Diff
303
304	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	// endregion: --- Diff
320}
321
322/// Extensions
323impl SPath {
324	/// Consumes the SPath and returns one with the given extension ensured:
325	/// - Sets the extension if not already equal.
326	/// - Returns self if the extension is already present.
327	///
328	/// ## Params
329	/// - `ext` e.g. `html` (not . prefixed)
330	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	/// Returns a new SPath with the given extension ensured:
338	/// - Returns self if the extension is already set,
339	/// - Otherwise sets it.
340	///
341	/// Delegates to `into_ensure_extension`.
342	///
343	/// ## Params
344	/// - `ext` e.g. `html` (not . prefixed)
345	pub fn ensure_extension(&self, ext: &str) -> Self {
346		self.clone().into_ensure_extension(ext)
347	}
348
349	/// Appends the extension, even if one already exists or is the same.
350	///
351	/// ## Params
352	/// - `ext` e.g. `html` (not . prefixed)
353	pub fn append_extension(&self, ext: &str) -> Self {
354		SPath::new(format!("{}.{ext}", self))
355	}
356}
357
358/// Other
359impl SPath {
360	/// Returns a new SPath for the eventual directory before the first glob expression.
361	///
362	/// If not a glob, will return none
363	///
364	/// ## Examples
365	/// - `/some/path/**/src/*.rs` → `/some/path`
366	/// - `**/src/*.rs` → `""`
367	/// - `/some/{src,doc}/**/*` → `/some`
368	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
384// region:    --- Std Traits Impls
385
386impl fmt::Display for SPath {
387	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
388		write!(f, "{}", self.as_str())
389	}
390}
391
392// endregion: --- Std Traits Impls
393
394// region:    --- AsRefs
395
396impl 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
420// endregion: --- AsRefs
421
422// region:    --- Froms (into other types)
423
424impl 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
454// endregion: --- Froms (into other types)
455
456// region:    --- Froms
457
458impl 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
488// endregion: --- Froms
489
490// region:    --- TryFrom
491
492impl 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
513// endregion: --- TryFrom
514
515// region:    --- Path Validation
516
517pub(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
524/// Validate but without generating an error (good for the _ok constructors)
525pub(crate) fn validate_spath_for_option(path: impl Into<PathBuf>) -> Option<Utf8PathBuf> {
526	Utf8PathBuf::from_path_buf(path.into()).ok()
527}
528
529// endregion: --- Path Validation