Skip to main content

wowlab_fs/
path.rs

1//! Lexical native-path types.
2
3use std::{
4    borrow::Borrow,
5    convert::Infallible,
6    ffi::{OsStr, OsString},
7    fmt,
8    ops::Deref,
9    path::{
10        Ancestors as NativeAncestors, Components as NativeComponents, Path as NativePath,
11        PathBuf as NativePathBuf,
12    },
13    str::FromStr,
14};
15
16use ref_cast::RefCast;
17use serde::{Deserialize, Deserializer, Serialize, Serializer};
18
19/// A borrowed native path with lexical operations only.
20///
21/// Filesystem-query methods are deliberately absent.
22/// Use this crate's operation modules to inspect or mutate the host filesystem.
23#[derive(Eq, Hash, Ord, PartialEq, PartialOrd, RefCast)]
24#[repr(transparent)]
25pub struct Path(NativePath);
26
27impl Path {
28    /// Borrow a native path without allocating.
29    #[must_use]
30    pub fn new<S>(path: &S) -> &Self
31    where
32        S: AsRef<OsStr> + ?Sized,
33    {
34        Self::ref_cast(NativePath::new(path))
35    }
36
37    /// Return the underlying operating-system string.
38    #[must_use]
39    pub fn as_os_str(&self) -> &OsStr {
40        self.0.as_os_str()
41    }
42
43    /// Return this path as UTF-8 when every byte is valid UTF-8.
44    #[must_use]
45    pub fn to_str(&self) -> Option<&str> {
46        self.0.to_str()
47    }
48
49    /// Return a display adapter that replaces invalid UTF-8 lossily.
50    #[must_use]
51    pub fn display(&self) -> Display<'_> {
52        Display(self)
53    }
54
55    /// Return this path as a potentially lossy string.
56    #[must_use]
57    pub fn to_string_lossy(&self) -> std::borrow::Cow<'_, str> {
58        self.0.to_string_lossy()
59    }
60
61    /// Allocate an owned copy.
62    #[must_use]
63    pub fn to_path_buf(&self) -> PathBuf {
64        PathBuf(self.0.to_path_buf())
65    }
66
67    /// Join a lexical path segment.
68    #[must_use]
69    pub fn join<S>(&self, path: S) -> PathBuf
70    where
71        S: AsRef<NativePath>,
72    {
73        PathBuf(self.0.join(path))
74    }
75
76    /// Return the parent path.
77    #[must_use]
78    pub fn parent(&self) -> Option<&Self> {
79        self.0.parent().map(Self::ref_cast)
80    }
81
82    /// Iterate over this path and each lexical parent.
83    #[must_use]
84    pub fn ancestors(&self) -> Ancestors<'_> {
85        Ancestors(self.0.ancestors())
86    }
87
88    /// Return the final component.
89    #[must_use]
90    pub fn file_name(&self) -> Option<&OsStr> {
91        self.0.file_name()
92    }
93
94    /// Return the final component without its last extension.
95    #[must_use]
96    pub fn file_stem(&self) -> Option<&OsStr> {
97        self.0.file_stem()
98    }
99
100    /// Return the final extension.
101    #[must_use]
102    pub fn extension(&self) -> Option<&OsStr> {
103        self.0.extension()
104    }
105
106    /// Return a path with a different final component.
107    #[must_use]
108    pub fn with_file_name<S>(&self, file_name: S) -> PathBuf
109    where
110        S: AsRef<OsStr>,
111    {
112        PathBuf(self.0.with_file_name(file_name))
113    }
114
115    /// Return a path with a different extension.
116    #[must_use]
117    pub fn with_extension<S>(&self, extension: S) -> PathBuf
118    where
119        S: AsRef<OsStr>,
120    {
121        PathBuf(self.0.with_extension(extension))
122    }
123
124    /// Iterate over normalized lexical components.
125    #[must_use]
126    pub fn components(&self) -> Components<'_> {
127        Components(self.0.components())
128    }
129
130    /// Report whether this path begins with `base` on component boundaries.
131    #[must_use]
132    pub fn starts_with<S>(&self, base: S) -> bool
133    where
134        S: AsRef<NativePath>,
135    {
136        self.0.starts_with(base)
137    }
138
139    /// Remove a component-aligned prefix.
140    ///
141    /// # Errors
142    ///
143    /// Returns an error when `base` is not a lexical prefix of this path.
144    pub fn strip_prefix<S>(&self, base: S) -> Result<&Self, StripPrefixError>
145    where
146        S: AsRef<NativePath>,
147    {
148        let path = self.0.strip_prefix(base)?;
149
150        Ok(Self::ref_cast(path))
151    }
152
153    /// Report whether the path is absolute for the current platform.
154    #[must_use]
155    pub fn is_absolute(&self) -> bool {
156        self.0.is_absolute()
157    }
158
159    /// Report whether the path has no components.
160    #[must_use]
161    pub fn is_empty(&self) -> bool {
162        self.as_os_str().is_empty()
163    }
164}
165
166impl AsRef<NativePath> for Path {
167    fn as_ref(&self) -> &NativePath {
168        &self.0
169    }
170}
171
172impl AsRef<Path> for Path {
173    fn as_ref(&self) -> &Path {
174        self
175    }
176}
177
178impl AsRef<OsStr> for Path {
179    fn as_ref(&self) -> &OsStr {
180        self.as_os_str()
181    }
182}
183
184impl fmt::Debug for Path {
185    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186        self.0.fmt(f)
187    }
188}
189
190/// An owned native path with lexical operations only.
191#[derive(Clone, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
192#[repr(transparent)]
193pub struct PathBuf(NativePathBuf);
194
195impl PathBuf {
196    /// Create an empty path.
197    #[must_use]
198    pub fn new() -> Self {
199        Self(NativePathBuf::new())
200    }
201
202    /// Borrow this owned path.
203    #[must_use]
204    pub fn as_path(&self) -> &Path {
205        self
206    }
207
208    /// Append a lexical path.
209    pub fn push<S>(&mut self, path: S)
210    where
211        S: AsRef<NativePath>,
212    {
213        self.0.push(path);
214    }
215
216    /// Remove the final component.
217    pub fn pop(&mut self) -> bool {
218        self.0.pop()
219    }
220
221    /// Replace the final extension.
222    pub fn set_extension<S>(&mut self, extension: S) -> bool
223    where
224        S: AsRef<OsStr>,
225    {
226        self.0.set_extension(extension)
227    }
228
229    /// Consume this path as an operating-system string.
230    #[must_use]
231    pub fn into_os_string(self) -> OsString {
232        self.0.into_os_string()
233    }
234}
235
236impl AsRef<NativePath> for PathBuf {
237    fn as_ref(&self) -> &NativePath {
238        &self.0
239    }
240}
241
242impl AsRef<OsStr> for PathBuf {
243    fn as_ref(&self) -> &OsStr {
244        self.as_os_str()
245    }
246}
247
248impl AsRef<Path> for PathBuf {
249    fn as_ref(&self) -> &Path {
250        self
251    }
252}
253
254impl Borrow<Path> for PathBuf {
255    fn borrow(&self) -> &Path {
256        self
257    }
258}
259
260impl Deref for PathBuf {
261    type Target = Path;
262
263    fn deref(&self) -> &Self::Target {
264        Path::ref_cast(self.0.as_path())
265    }
266}
267
268impl fmt::Debug for PathBuf {
269    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
270        self.0.fmt(f)
271    }
272}
273
274impl fmt::Display for PathBuf {
275    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
276        self.display().fmt(f)
277    }
278}
279
280impl From<NativePathBuf> for PathBuf {
281    fn from(path: NativePathBuf) -> Self {
282        Self(path)
283    }
284}
285
286impl From<&NativePath> for PathBuf {
287    fn from(path: &NativePath) -> Self {
288        Self(path.to_path_buf())
289    }
290}
291
292impl From<&Path> for PathBuf {
293    fn from(path: &Path) -> Self {
294        path.to_path_buf()
295    }
296}
297
298impl From<OsString> for PathBuf {
299    fn from(path: OsString) -> Self {
300        Self(NativePathBuf::from(path))
301    }
302}
303
304impl From<&OsStr> for PathBuf {
305    fn from(path: &OsStr) -> Self {
306        Self(NativePathBuf::from(path))
307    }
308}
309
310impl From<String> for PathBuf {
311    fn from(path: String) -> Self {
312        Self(NativePathBuf::from(path))
313    }
314}
315
316impl From<&str> for PathBuf {
317    fn from(path: &str) -> Self {
318        Self(NativePathBuf::from(path))
319    }
320}
321
322impl FromStr for PathBuf {
323    type Err = Infallible;
324
325    fn from_str(path: &str) -> Result<Self, Self::Err> {
326        Ok(Self(NativePathBuf::from(path)))
327    }
328}
329
330impl From<PathBuf> for OsString {
331    fn from(path: PathBuf) -> Self {
332        path.into_os_string()
333    }
334}
335
336impl Serialize for PathBuf {
337    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
338    where
339        S: Serializer,
340    {
341        self.0.serialize(serializer)
342    }
343}
344
345impl<'de> Deserialize<'de> for PathBuf {
346    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
347    where
348        D: Deserializer<'de>,
349    {
350        NativePathBuf::deserialize(deserializer).map(Self)
351    }
352}
353
354/// One normalized lexical component.
355#[derive(Clone, Copy, Debug, Eq, PartialEq)]
356#[non_exhaustive]
357pub enum Component<'a> {
358    /// A platform prefix such as a Windows drive or UNC share.
359    Prefix(&'a OsStr),
360    /// The root directory separator.
361    Root,
362    /// A current-directory component.
363    Current,
364    /// A parent-directory component.
365    Parent,
366    /// An ordinary file-name component.
367    Normal(&'a OsStr),
368}
369
370impl Component<'_> {
371    /// Return the operating-system string represented by this component.
372    #[must_use]
373    pub fn as_os_str(&self) -> &OsStr {
374        match self {
375            Self::Prefix(value) | Self::Normal(value) => value,
376            Self::Root => std::path::MAIN_SEPARATOR_STR.as_ref(),
377            Self::Current => ".".as_ref(),
378            Self::Parent => "..".as_ref(),
379        }
380    }
381}
382
383/// Iterator over a path's normalized lexical components.
384#[derive(Clone, Debug)]
385pub struct Components<'a>(NativeComponents<'a>);
386
387impl<'a> Iterator for Components<'a> {
388    type Item = Component<'a>;
389
390    fn next(&mut self) -> Option<Self::Item> {
391        self.0.next().map(|component| match component {
392            std::path::Component::Prefix(prefix) => Component::Prefix(prefix.as_os_str()),
393            std::path::Component::RootDir => Component::Root,
394            std::path::Component::CurDir => Component::Current,
395            std::path::Component::ParentDir => Component::Parent,
396            std::path::Component::Normal(value) => Component::Normal(value),
397        })
398    }
399}
400
401impl DoubleEndedIterator for Components<'_> {
402    fn next_back(&mut self) -> Option<Self::Item> {
403        self.0.next_back().map(|component| match component {
404            std::path::Component::Prefix(prefix) => Component::Prefix(prefix.as_os_str()),
405            std::path::Component::RootDir => Component::Root,
406            std::path::Component::CurDir => Component::Current,
407            std::path::Component::ParentDir => Component::Parent,
408            std::path::Component::Normal(value) => Component::Normal(value),
409        })
410    }
411}
412
413/// Iterator over a path and its lexical parents.
414#[derive(Clone, Debug)]
415pub struct Ancestors<'a>(NativeAncestors<'a>);
416
417impl<'a> Iterator for Ancestors<'a> {
418    type Item = &'a Path;
419
420    fn next(&mut self) -> Option<Self::Item> {
421        self.0.next().map(Path::ref_cast)
422    }
423}
424
425/// Failure to remove a lexical path prefix.
426#[derive(Debug, thiserror::Error)]
427#[error(transparent)]
428pub struct StripPrefixError(#[from] std::path::StripPrefixError);
429
430/// Display adapter for a native path.
431#[derive(Clone, Copy, Debug)]
432pub struct Display<'a>(&'a Path);
433
434impl fmt::Display for Display<'_> {
435    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
436        self.0.0.display().fmt(f)
437    }
438}
439
440#[cfg(test)]
441mod tests {
442    use googletest::prelude::*;
443
444    use super::*;
445
446    #[gtest]
447    fn lexical_api_preserves_components_and_parents() -> Result<()> {
448        let path = Path::new("crates/fs/src/lib.rs");
449        let components = path
450            .components()
451            .map(|component| component.as_os_str().to_string_lossy().into_owned())
452            .collect::<Vec<_>>();
453
454        verify_that!(
455            components,
456            elements_are![eq("crates"), eq("fs"), eq("src"), eq("lib.rs"),]
457        )?;
458
459        verify_that!(
460            path.parent()
461                .map(Path::display)
462                .map(|value| value.to_string()),
463            some(eq("crates/fs/src"))
464        )
465    }
466
467    #[gtest]
468    fn owned_path_dereferences_only_to_workspace_path() -> Result<()> {
469        let path = PathBuf::from("crates").join("fs");
470
471        verify_that!(path.as_path(), eq(Path::new("crates/fs")))?;
472        verify_that!(path.file_name(), some(eq(OsStr::new("fs"))))?;
473
474        verify_that!(path.strip_prefix("crates").or_fail()?, eq(Path::new("fs")))
475    }
476
477    #[gtest]
478    fn owned_path_parses_lexically_without_filesystem_access() -> Result<()> {
479        let expected = PathBuf::from("crates/fs/src");
480
481        verify_that!("crates/fs/src".parse::<PathBuf>(), ok(eq(&expected)))
482    }
483}