Skip to main content

simple_fs/
spath.rs

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::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/// - It does not collapse `..` segments by default, use collapse APIs for that
14/// - Garanteed to be UTF8
15#[derive(Debug, Clone, Eq, PartialEq, Hash)]
16pub struct SPath {
17	pub(crate) path_buf: Utf8PathBuf,
18}
19
20/// Constructors that guarantee the SPath contract described in the struct
21impl SPath {
22	/// Constructor for SPath accepting anything that implements Into<Utf8PathBuf>.
23	/// IMPORTANT: This will normalize the path (posix style), but does not collapse `..`
24	/// segments. Use collapse APIs when collapse behavior is desired.
25	pub fn new(path: impl Into<Utf8PathBuf>) -> Self {
26		let path_buf = path.into();
27		let path_buf = reshape::into_normalized(path_buf);
28		Self { path_buf }
29	}
30
31	/// Constructor from standard PathBuf.
32	pub fn from_std_path_buf(path_buf: PathBuf) -> Result<Self> {
33		let path_buf = validate_spath_for_result(path_buf)?;
34		Ok(SPath::new(path_buf))
35	}
36
37	/// Constructor from standard Path and all impl AsRef<Path>.
38	pub fn from_std_path(path: impl AsRef<Path>) -> Result<Self> {
39		let path = path.as_ref();
40		let path_buf = validate_spath_for_result(path)?;
41		Ok(SPath::new(path_buf))
42	}
43
44	/// Constructor from walkdir::DirEntry
45	pub fn from_walkdir_entry(wd_entry: walkdir::DirEntry) -> Result<Self> {
46		let path = wd_entry.into_path();
47		let path_buf = validate_spath_for_result(path)?;
48		Ok(SPath::new(path_buf))
49	}
50
51	/// Constructor from fs::DirEntry.
52	pub fn from_fs_entry(fs_entry: fs::DirEntry) -> Result<Self> {
53		let path = fs_entry.path();
54		let path_buf = validate_spath_for_result(path)?;
55		Ok(SPath::new(path_buf))
56	}
57
58	/// Constructor for anything that implements AsRef<Path>.
59	///
60	/// Returns Option<SPath>. Useful for filter_map.
61	pub fn from_std_path_ok(path: impl AsRef<Path>) -> Option<Self> {
62		let path = path.as_ref();
63		let path_buf = validate_spath_for_option(path)?;
64		Some(SPath::new(path_buf))
65	}
66
67	/// Constructed from PathBuf returns an Option, none if validation fails.
68	/// Useful for filter_map.
69	pub fn from_std_path_buf_ok(path_buf: PathBuf) -> Option<Self> {
70		let path_buf = validate_spath_for_option(&path_buf)?;
71		Some(SPath::new(path_buf))
72	}
73
74	/// Constructor from fs::DirEntry returning an Option, none if validation fails.
75	/// Useful for filter_map.
76	pub fn from_fs_entry_ok(fs_entry: fs::DirEntry) -> Option<Self> {
77		let path_buf = fs_entry.path();
78		let path_buf = validate_spath_for_option(&path_buf)?;
79		Some(SPath::new(path_buf))
80	}
81
82	/// Constructor from walkdir::DirEntry returning an Option, none if validation fails.
83	/// Useful for filter_map.
84	pub fn from_walkdir_entry_ok(wd_entry: walkdir::DirEntry) -> Option<Self> {
85		let path_buf = validate_spath_for_option(wd_entry.path())?;
86		Some(SPath::new(path_buf))
87	}
88}
89
90/// Public into path
91impl SPath {
92	/// Consumes the SPath and returns its PathBuf.
93	pub fn into_std_path_buf(self) -> PathBuf {
94		self.path_buf.into()
95	}
96
97	/// Returns a reference to the internal std Path.
98	pub fn std_path(&self) -> &Path {
99		self.path_buf.as_std_path()
100	}
101
102	/// Returns a reference to the internal Utf8Path.
103	pub fn path(&self) -> &Utf8Path {
104		&self.path_buf
105	}
106}
107
108/// Public getters
109impl SPath {
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 true if the internal path is absolute.
176	pub fn is_absolute(&self) -> bool {
177		self.path_buf.is_absolute()
178	}
179
180	/// Returns true if the internal path is relative.
181	pub fn is_relative(&self) -> bool {
182		self.path_buf.is_relative()
183	}
184}
185
186/// Mime
187impl SPath {
188	/// Returns the mime type as a &str if found.
189	///
190	/// This uses `mime_guess` under the hood.
191	pub fn mime_type(&self) -> Option<&'static str> {
192		mime_guess::from_path(self.path()).first_raw()
193	}
194
195	/// Returns true if the path is likely a text type.
196	///
197	/// This includes `text/*`, `application/json`, `application/javascript`,
198	/// `application/xml`, `application/toml`, `image/svg+xml`, known text extensions, etc.
199	pub fn is_likely_text(&self) -> bool {
200		// -- Check known text extensions first (fast path, covers gaps in mime_guess)
201		if let Some(ext) = self.extension() {
202			// -- Known text ext
203			let known_text_ext =
204				matches!(
205					ext,
206					"txt"
207						| "md" | "markdown"
208						| "csv" | "toml" | "yaml"
209						| "yml" | "json" | "jsonc"
210						| "json5" | "jsonl"
211						| "ndjson" | "jsonlines"
212						| "ldjson" | "xml" | "html"
213						| "htm" | "css" | "scss"
214						| "sass" | "less" | "js"
215						| "mjs" | "cjs" | "ts"
216						| "tsx" | "jsx" | "rs"
217						| "dart" | "py" | "rb"
218						| "go" | "java" | "c"
219						| "cpp" | "h" | "hpp"
220						| "sh" | "bash" | "zsh"
221						| "fish" | "php" | "lua"
222						| "ini" | "cfg" | "conf"
223						| "sql" | "graphql"
224						| "gql" | "svg" | "log"
225						| "env" | "tex"
226				);
227			if known_text_ext {
228				return true;
229			}
230
231			// -- Known binary ext
232			let known_binary_ext = matches!(ext, "lockb");
233			if known_binary_ext {
234				return false;
235			}
236		}
237
238		// -- Get the mime type and return if found
239		let mimes = mime_guess::from_path(self.path());
240		if mimes.is_empty() {
241			return true;
242		}
243
244		// -- Fall back to mime type detection
245		mimes.into_iter().any(|mime| {
246			let mime = mime.essence_str();
247			mime.starts_with("text/")
248				|| mime == "application/json"
249				|| mime == "application/javascript"
250				|| mime == "application/x-javascript"
251				|| mime == "application/ecmascript"
252				|| mime == "application/x-python"
253				|| mime == "application/xml"
254				|| mime == "application/toml"
255				|| mime == "application/x-toml"
256				|| mime == "application/x-yaml"
257				|| mime == "application/yaml"
258				|| mime == "application/sql"
259				|| mime == "application/graphql"
260				|| mime == "application/xml-dtd"
261				|| mime == "application/x-qml"
262				|| mime == "application/ini"
263				|| mime == "application/x-ini"
264				|| mime == "application/x-sh"
265				|| mime == "application/x-httpd-php"
266				|| mime == "application/x-lua"
267				|| mime == "application/vnd.dart"
268				|| mime.ends_with("+json")
269				|| mime.ends_with("+xml")
270				|| mime.ends_with("+yaml")
271		})
272	}
273}
274
275/// Meta
276impl SPath {
277	/// Get a Simple Metadata structure `SMeta` with
278	/// created_epoch_us, modified_epoch_us, and size (all i64)
279	/// (size will be '0' for any none file)
280	#[allow(clippy::fn_to_numeric_cast)]
281	pub fn meta(&self) -> Result<SMeta> {
282		let path = self;
283
284		let metadata = self.metadata()?;
285
286		// -- Get modified (failed if it cannot)
287		let modified = metadata.modified().map_err(|ex| Error::CantGetMetadata((path, ex).into()))?;
288		let modified_epoch_us: i64 = modified
289			.duration_since(UNIX_EPOCH)
290			.map_err(|ex| Error::CantGetMetadata((path, ex).into()))?
291			.as_micros()
292			.min(i64::MAX as u128) as i64;
293
294		// -- Get created (If not found, will get modified)
295		let created_epoch_us = metadata
296			.modified()
297			.ok()
298			.and_then(|c| c.duration_since(UNIX_EPOCH).ok())
299			.map(|c| c.as_micros().min(i64::MAX as u128) as i64);
300		let created_epoch_us = created_epoch_us.unwrap_or(modified_epoch_us);
301
302		// -- Get size
303		let size = if metadata.is_file() { metadata.len() } else { 0 };
304
305		Ok(SMeta {
306			created_epoch_us,
307			modified_epoch_us,
308			size,
309			is_file: metadata.is_file(),
310			is_dir: metadata.is_dir(),
311		})
312	}
313
314	/// Returns the std metadata
315	pub fn metadata(&self) -> Result<Metadata> {
316		fs::metadata(self).map_err(|ex| Error::CantGetMetadata((self, ex).into()))
317	}
318}
319
320/// Transformers
321impl SPath {
322	/// This perform a OS Canonicalization.
323	pub fn canonicalize(&self) -> Result<SPath> {
324		let path = self
325			.path_buf
326			.canonicalize_utf8()
327			.map_err(|err| Error::CannotCanonicalize((self.std_path(), err).into()))?;
328		Ok(SPath::new(path))
329	}
330
331	// region:    --- Collapse
332
333	/// Collapse a path without performing I/O.
334	///
335	/// All redundant separator and up-level references are collapsed.
336	///
337	/// However, this does not resolve links.
338	pub fn collapse(&self) -> SPath {
339		let path_buf = crate::into_collapsed(self.path_buf.clone());
340		SPath::new(path_buf)
341	}
342
343	/// Same as [`collapse`] but consume and create a new SPath only if needed
344	pub fn into_collapsed(self) -> SPath {
345		if self.is_collapsed() { self } else { self.collapse() }
346	}
347
348	/// Return `true` if the path is collapsed.
349	///
350	/// # Quirk
351	///
352	/// If the path does not start with `./` but contains `./` in the middle,
353	/// then this function might returns `true`.
354	pub fn is_collapsed(&self) -> bool {
355		crate::is_collapsed(self)
356	}
357
358	// endregion: --- Collapse
359
360	// region:    --- Parent & Join
361
362	/// Returns the parent directory as an Option<SPath>.
363	pub fn parent(&self) -> Option<SPath> {
364		self.path_buf.parent().map(SPath::from)
365	}
366
367	/// Returns a new SPath with the given suffix appended to the filename (after the eventual extension)
368	///
369	/// Use [`join`] to join path segments.
370	///
371	/// Example:
372	/// - `foo.rs` + `_backup` → `foo.rs_backup`
373	pub fn append_suffix(&self, suffix: &str) -> SPath {
374		SPath::new(format!("{self}{suffix}"))
375	}
376
377	/// Joins the provided path with the current path and returns an SPath.
378	pub fn join(&self, leaf_path: impl Into<Utf8PathBuf>) -> SPath {
379		let path_buf = self.path_buf.join(leaf_path.into());
380		SPath::from(path_buf)
381	}
382
383	/// Joins a standard Path to the path of this SPath.
384	pub fn join_std_path(&self, leaf_path: impl AsRef<Path>) -> Result<SPath> {
385		let leaf_path = leaf_path.as_ref();
386		let joined = self.std_path().join(leaf_path);
387		let path_buf = validate_spath_for_result(joined)?;
388		Ok(SPath::from(path_buf))
389	}
390
391	/// Creates a new sibling SPath with the given leaf_path.
392	pub fn new_sibling(&self, leaf_path: impl AsRef<str>) -> SPath {
393		let leaf_path = leaf_path.as_ref();
394		match self.path_buf.parent() {
395			Some(parent_dir) => SPath::new(parent_dir.join(leaf_path)),
396			None => SPath::new(leaf_path),
397		}
398	}
399
400	/// Creates a new sibling SPath with the given standard path.
401	pub fn new_sibling_std_path(&self, leaf_path: impl AsRef<Path>) -> Result<SPath> {
402		let leaf_path = leaf_path.as_ref();
403
404		match self.std_path().parent() {
405			Some(parent_dir) => SPath::from_std_path(parent_dir.join(leaf_path)),
406			None => SPath::from_std_path(leaf_path),
407		}
408	}
409
410	// endregion: --- Parent & Join
411
412	// region:    --- Diff
413
414	/// Returns the relative difference from `base` to this path as an [`SPath`].
415	///
416	/// This delegates to [`pathdiff::diff_utf8_paths`], so it never touches the file system and
417	/// simply subtracts `base` from `self` when `base` is a prefix.
418	/// The returned value preserves the crate-level normalization guarantees and can safely be
419	/// joined back onto `base`.
420	///
421	/// Returns `None` when the inputs cannot be related through a relative path (for example,
422	/// when they reside on different volumes or when normalization prevents a clean prefix match).
423	///
424	/// # Examples
425	/// ```
426	/// # use simple_fs::SPath;
427	/// let base = SPath::new("/workspace/project");
428	/// let file = SPath::new("/workspace/project/src/main.rs");
429	/// assert_eq!(file.diff(&base).map(|p| p.to_string()), Some("src/main.rs".into()));
430	/// ```
431	pub fn diff(&self, base: impl AsRef<Utf8Path>) -> Option<SPath> {
432		let base = base.as_ref();
433
434		let diff_path = diff_utf8_paths(self, base);
435
436		diff_path.map(SPath::from)
437	}
438
439	/// Returns the relative path from `base` to this path or an [`Error::CannotDiff`].
440	///
441	/// This is a fallible counterpart to [`SPath::diff`]. When the paths share a common prefix it
442	/// returns the diff, otherwise it raises [`Error::CannotDiff`] containing the original inputs,
443	/// making failures descriptive.
444	///
445	/// The computation still delegates to [`pathdiff::diff_utf8_paths`], so no filesystem access
446	/// occurs and the resulting [`SPath`] keeps its normalization guarantees.
447	///
448	/// # Errors
449	/// Returns [`Error::CannotDiff`] when `base` is not a prefix of `self` (for example, when the
450	/// inputs live on different volumes).
451	pub fn try_diff(&self, base: impl AsRef<Utf8Path>) -> Result<SPath> {
452		self.diff(&base).ok_or_else(|| Error::CannotDiff {
453			path: self.to_string(),
454			base: base.as_ref().to_string(),
455		})
456	}
457
458	// endregion: --- Diff
459
460	// region:    --- Replace
461
462	pub fn replace_prefix(&self, base: impl AsRef<str>, with: impl AsRef<str>) -> SPath {
463		let base = base.as_ref();
464		let with = with.as_ref();
465		let s = self.as_str();
466		if let Some(stripped) = s.strip_prefix(base) {
467			// Avoid introducing double slashes (is with.is_empty() because do not want to add a / if empty)
468			let joined = if with.is_empty() || with.ends_with('/') || stripped.starts_with('/') {
469				format!("{with}{stripped}")
470			} else {
471				format!("{with}/{stripped}")
472			};
473			SPath::new(joined)
474		} else {
475			self.clone()
476		}
477	}
478
479	pub fn into_replace_prefix(self, base: impl AsRef<str>, with: impl AsRef<str>) -> SPath {
480		let base = base.as_ref();
481		let with = with.as_ref();
482		let s = self.as_str();
483		if let Some(stripped) = s.strip_prefix(base) {
484			let joined = if with.is_empty() || with.ends_with('/') || stripped.starts_with('/') {
485				format!("{with}{stripped}")
486			} else {
487				format!("{with}/{stripped}")
488			};
489			SPath::new(joined)
490		} else {
491			self
492		}
493	}
494
495	// endregion: --- Replace
496}
497
498/// Path/UTF8Path/Camino passthrough
499impl SPath {
500	pub fn as_std_path(&self) -> &Path {
501		self.std_path()
502	}
503
504	/// Returns a path that, when joined onto `base`, yields `self`.
505	///
506	/// # Errors
507	///
508	/// If `base` is not a prefix of `self`
509	pub fn strip_prefix(&self, prefix: impl AsRef<str>) -> Result<SPath> {
510		let prefix = prefix.as_ref();
511		let new_path = self.path_buf.strip_prefix(prefix).map_err(|_| Error::StripPrefix {
512			prefix: prefix.to_string(),
513			path: self.to_string(),
514		})?;
515
516		Ok(new_path.into())
517	}
518
519	/// Determines whether `base` is a prefix of `self`.
520	///
521	/// Only considers whole path components to match.
522	///
523	/// # Examples
524	///
525	/// ```
526	/// use camino::Utf8Path;
527	///
528	/// let path = Utf8Path::new("/etc/passwd");
529	///
530	/// assert!(path.starts_with("/etc"));
531	/// assert!(path.starts_with("/etc/"));
532	/// assert!(path.starts_with("/etc/passwd"));
533	/// assert!(path.starts_with("/etc/passwd/")); // extra slash is okay
534	/// assert!(path.starts_with("/etc/passwd///")); // multiple extra slashes are okay
535	///
536	/// assert!(!path.starts_with("/e"));
537	/// assert!(!path.starts_with("/etc/passwd.txt"));
538	///
539	/// assert!(!Utf8Path::new("/etc/foo.rs").starts_with("/etc/foo"));
540	/// ```
541	pub fn starts_with(&self, base: impl AsRef<Path>) -> bool {
542		self.path_buf.starts_with(base)
543	}
544
545	pub fn starts_with_prefix(&self, base: impl AsRef<str>) -> bool {
546		self.path_buf.starts_with(base.as_ref())
547	}
548}
549
550/// Extensions
551impl SPath {
552	/// Consumes the SPath and returns one with the given extension ensured:
553	/// - Sets the extension if not already equal.
554	/// - Returns self if the extension is already present.
555	///
556	/// ## Params
557	/// - `ext` e.g. `html` (not . prefixed)
558	pub fn into_ensure_extension(mut self, ext: &str) -> Self {
559		if self.extension() != Some(ext) {
560			self.path_buf.set_extension(ext);
561		}
562		self
563	}
564
565	/// Returns a new SPath with the given extension ensured.
566	///
567	/// - Since this takes a reference, it will return a Clone no matter what.
568	/// - Use [`into_ensure_extension`] to consume and create a new SPath only if needed.
569	///
570	/// Delegates to `into_ensure_extension`.
571	///
572	/// ## Params
573	/// - `ext` e.g. `html` (not . prefixed)
574	pub fn ensure_extension(&self, ext: &str) -> Self {
575		self.clone().into_ensure_extension(ext)
576	}
577
578	/// Appends the extension, even if one already exists or is the same.
579	///
580	/// ## Params
581	/// - `ext` e.g. `html` (not . prefixed)
582	pub fn append_extension(&self, ext: &str) -> Self {
583		SPath::new(format!("{self}.{ext}"))
584	}
585}
586
587/// Other
588impl SPath {
589	/// Returns a new SPath for the eventual directory before the first glob expression.
590	///
591	/// If not a glob, will return none
592	///
593	/// ## Examples
594	/// - `/some/path/**/src/*.rs` → `/some/path`
595	/// - `**/src/*.rs` → `""`
596	/// - `/some/{src,doc}/**/*` → `/some`
597	pub fn dir_before_glob(&self) -> Option<SPath> {
598		let path_str = self.as_str();
599		let mut last_slash_idx = None;
600
601		for (i, c) in path_str.char_indices() {
602			if c == '/' {
603				last_slash_idx = Some(i);
604			} else if matches!(c, '*' | '?' | '[' | '{') {
605				return Some(SPath::from(&path_str[..last_slash_idx.unwrap_or(0)]));
606			}
607		}
608
609		None
610	}
611}
612
613// region:    --- Std Traits Impls
614
615impl fmt::Display for SPath {
616	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
617		write!(f, "{}", self.as_str())
618	}
619}
620
621// endregion: --- Std Traits Impls
622
623// region:    --- AsRefs
624
625impl AsRef<SPath> for SPath {
626	fn as_ref(&self) -> &SPath {
627		self
628	}
629}
630
631impl AsRef<Path> for SPath {
632	fn as_ref(&self) -> &Path {
633		self.path_buf.as_ref()
634	}
635}
636
637impl AsRef<Utf8Path> for SPath {
638	fn as_ref(&self) -> &Utf8Path {
639		self.path_buf.as_ref()
640	}
641}
642
643impl AsRef<str> for SPath {
644	fn as_ref(&self) -> &str {
645		self.as_str()
646	}
647}
648
649// endregion: --- AsRefs
650
651// region:    --- Froms (into other types)
652
653impl From<SPath> for String {
654	fn from(val: SPath) -> Self {
655		val.as_str().to_string()
656	}
657}
658
659impl From<&SPath> for String {
660	fn from(val: &SPath) -> Self {
661		val.as_str().to_string()
662	}
663}
664
665impl From<SPath> for PathBuf {
666	fn from(val: SPath) -> Self {
667		val.into_std_path_buf()
668	}
669}
670
671impl From<&SPath> for PathBuf {
672	fn from(val: &SPath) -> Self {
673		val.path_buf.clone().into()
674	}
675}
676
677impl From<SPath> for Utf8PathBuf {
678	fn from(val: SPath) -> Self {
679		val.path_buf
680	}
681}
682
683// endregion: --- Froms (into other types)
684
685// region:    --- Froms
686
687impl From<&SPath> for SPath {
688	fn from(path: &SPath) -> Self {
689		path.clone()
690	}
691}
692
693impl From<Utf8PathBuf> for SPath {
694	fn from(path_buf: Utf8PathBuf) -> Self {
695		SPath::new(path_buf)
696	}
697}
698
699impl From<&Utf8Path> for SPath {
700	fn from(path: &Utf8Path) -> Self {
701		SPath::new(path)
702	}
703}
704
705impl From<String> for SPath {
706	fn from(path: String) -> Self {
707		SPath::new(path)
708	}
709}
710
711impl From<&String> for SPath {
712	fn from(path: &String) -> Self {
713		SPath::new(path)
714	}
715}
716
717impl From<&str> for SPath {
718	fn from(path: &str) -> Self {
719		SPath::new(path)
720	}
721}
722
723// endregion: --- Froms
724
725// region:    --- TryFrom
726
727impl TryFrom<PathBuf> for SPath {
728	type Error = Error;
729	fn try_from(path_buf: PathBuf) -> Result<SPath> {
730		SPath::from_std_path_buf(path_buf)
731	}
732}
733
734impl TryFrom<fs::DirEntry> for SPath {
735	type Error = Error;
736	fn try_from(fs_entry: fs::DirEntry) -> Result<SPath> {
737		SPath::from_std_path_buf(fs_entry.path())
738	}
739}
740
741impl TryFrom<walkdir::DirEntry> for SPath {
742	type Error = Error;
743	fn try_from(wd_entry: walkdir::DirEntry) -> Result<SPath> {
744		SPath::from_std_path(wd_entry.path())
745	}
746}
747
748// endregion: --- TryFrom
749
750// region:    --- Path Validation
751
752pub(crate) fn validate_spath_for_result(path: impl Into<PathBuf>) -> Result<Utf8PathBuf> {
753	let path = path.into();
754	let path_buf =
755		Utf8PathBuf::from_path_buf(path).map_err(|err| Error::PathNotUtf8(err.to_string_lossy().to_string()))?;
756	Ok(path_buf)
757}
758
759/// Validate but without generating an error (good for the _ok constructors)
760pub(crate) fn validate_spath_for_option(path: impl Into<PathBuf>) -> Option<Utf8PathBuf> {
761	Utf8PathBuf::from_path_buf(path.into()).ok()
762}
763
764// endregion: --- Path Validation
765
766// region:    --- Tests
767
768#[cfg(test)]
769#[path = "_tests/tests_spath.rs"]
770mod tests_spath;
771
772// endregion: --- Tests