Skip to main content

typed_path/typed/utf8/
path.rs

1use core::fmt;
2
3#[cfg(feature = "serde")]
4use serde_core::{
5    de,
6    de::{Unexpected, Visitor},
7    Deserialize, Deserializer, Serialize, Serializer,
8};
9
10use crate::common::{CheckedPathError, StripPrefixError, TryAsRef};
11use crate::typed::{
12    PathType, Utf8TypedAncestors, Utf8TypedComponents, Utf8TypedIter, Utf8TypedPathBuf,
13};
14use crate::unix::Utf8UnixPath;
15use crate::windows::Utf8WindowsPath;
16
17/// Represents a path with a known type that can be one of:
18///
19/// * [`Utf8UnixPath`]
20/// * [`Utf8WindowsPath`]
21#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
22pub enum Utf8TypedPath<'a> {
23    Unix(&'a Utf8UnixPath),
24    Windows(&'a Utf8WindowsPath),
25}
26
27impl<'a> Utf8TypedPath<'a> {
28    /// Creates a new path with the given type as its encoding.
29    pub fn new<S: AsRef<str> + ?Sized>(s: &'a S, r#type: PathType) -> Self {
30        match r#type {
31            PathType::Unix => Self::unix(s),
32            PathType::Windows => Self::windows(s),
33        }
34    }
35
36    /// Creates a new typed Unix path.
37    #[inline]
38    pub fn unix<S: AsRef<str> + ?Sized>(s: &'a S) -> Self {
39        Self::Unix(Utf8UnixPath::new(s))
40    }
41
42    /// Creates a new typed Windows path.
43    #[inline]
44    pub fn windows<S: AsRef<str> + ?Sized>(s: &'a S) -> Self {
45        Self::Windows(Utf8WindowsPath::new(s))
46    }
47
48    /// Creates a new typed path from a byte slice by determining if the path represents a Windows
49    /// or Unix path. This is accomplished by first trying to parse as a Windows path. If the
50    /// resulting path contains a prefix such as `C:` or begins with a `\`, it is assumed to be a
51    /// [`Utf8WindowsPath`]; otherwise, the slice will be represented as a [`Utf8UnixPath`].
52    ///
53    /// # Examples
54    ///
55    /// ```
56    /// use typed_path::Utf8TypedPath;
57    ///
58    /// assert!(Utf8TypedPath::derive(r#"C:\some\path\to\file.txt"#).is_windows());
59    /// assert!(Utf8TypedPath::derive(r#"\some\path\to\file.txt"#).is_windows());
60    /// assert!(Utf8TypedPath::derive(r#"/some/path/to/file.txt"#).is_unix());
61    ///
62    /// // NOTE: If we don't start with a backslash, it's too difficult to
63    /// //       determine and we therefore just assume a Unix/POSIX path.
64    /// assert!(Utf8TypedPath::derive(r#"some\path\to\file.txt"#).is_unix());
65    /// assert!(Utf8TypedPath::derive("file.txt").is_unix());
66    /// assert!(Utf8TypedPath::derive("").is_unix());
67    /// ```
68    pub fn derive<S: AsRef<str> + ?Sized>(s: &'a S) -> Self {
69        let winpath = Utf8WindowsPath::new(s);
70        if s.as_ref().starts_with('\\') || winpath.components().has_prefix() {
71            Self::Windows(winpath)
72        } else {
73            Self::Unix(Utf8UnixPath::new(s))
74        }
75    }
76
77    /// Yields the underlying [`str`] slice.
78    ///
79    /// # Examples
80    ///
81    /// ```
82    /// use typed_path::Utf8TypedPath;
83    ///
84    /// let string = Utf8TypedPath::derive("foo.txt").as_str().to_string();
85    /// assert_eq!(string, "foo.txt");
86    /// ```
87    pub fn as_str(&self) -> &str {
88        impl_typed_fn!(self, as_str)
89    }
90
91    /// Converts a [`Utf8TypedPath`] into a [`Utf8TypedPathBuf`].
92    ///
93    /// # Examples
94    ///
95    /// ```
96    /// use typed_path::{Utf8TypedPath, Utf8TypedPathBuf};
97    ///
98    /// let path_buf = Utf8TypedPath::derive("foo.txt").to_path_buf();
99    /// assert_eq!(path_buf, Utf8TypedPathBuf::from("foo.txt"));
100    /// ```
101    pub fn to_path_buf(&self) -> Utf8TypedPathBuf {
102        match self {
103            Self::Unix(path) => Utf8TypedPathBuf::Unix(path.to_path_buf()),
104            Self::Windows(path) => Utf8TypedPathBuf::Windows(path.to_path_buf()),
105        }
106    }
107
108    /// Returns `true` if the [`Utf8TypedPath`] is absolute, i.e., if it is independent of
109    /// the current directory.
110    ///
111    /// * On Unix ([`UnixPath`]]), a path is absolute if it starts with the root, so
112    ///   `is_absolute` and [`has_root`] are equivalent.
113    ///
114    /// * On Windows ([`WindowsPath`]), a path is absolute if it has a prefix and starts with the
115    ///   root: `c:\windows` is absolute, while `c:temp` and `\temp` are not.
116    ///
117    /// [`UnixPath`]: crate::UnixPath
118    /// [`WindowsPath`]: crate::WindowsPath
119    ///
120    /// # Examples
121    ///
122    /// ```
123    /// use typed_path::Utf8TypedPath;
124    ///
125    /// assert!(!Utf8TypedPath::derive("foo.txt").is_absolute());
126    /// ```
127    ///
128    /// [`has_root`]: Utf8TypedPath::has_root
129    pub fn is_absolute(&self) -> bool {
130        impl_typed_fn!(self, is_absolute)
131    }
132
133    /// Returns `true` if the [`Utf8TypedPath`] is relative, i.e., not absolute.
134    ///
135    /// See [`is_absolute`]'s documentation for more details.
136    ///
137    /// # Examples
138    ///
139    /// ```
140    /// use typed_path::Utf8TypedPath;
141    ///
142    /// assert!(Utf8TypedPath::derive("foo.txt").is_relative());
143    /// ```
144    ///
145    /// [`is_absolute`]: Utf8TypedPath::is_absolute
146    #[inline]
147    pub fn is_relative(&self) -> bool {
148        impl_typed_fn!(self, is_relative)
149    }
150
151    /// Returns `true` if the [`Utf8TypedPath`] has a root.
152    ///
153    /// * On Unix ([`Utf8UnixPath`]), a path has a root if it begins with `/`.
154    ///
155    /// * On Windows ([`Utf8WindowsPath`]), a path has a root if it:
156    ///     * has no prefix and begins with a separator, e.g., `\windows`
157    ///     * has a prefix followed by a separator, e.g., `c:\windows` but not `c:windows`
158    ///     * has any non-disk prefix, e.g., `\\server\share`
159    ///
160    /// [`Utf8UnixPath`]: crate::Utf8UnixPath
161    /// [`Utf8WindowsPath`]: crate::Utf8WindowsPath
162    ///
163    /// # Examples
164    ///
165    /// ```
166    /// use typed_path::Utf8TypedPath;
167    ///
168    /// assert!(Utf8TypedPath::derive("/etc/passwd").has_root());
169    /// ```
170    #[inline]
171    pub fn has_root(&self) -> bool {
172        impl_typed_fn!(self, has_root)
173    }
174
175    /// Returns the [`Utf8TypedPath`] without its final component, if there is one.
176    ///
177    /// Returns [`None`] if the path terminates in a root or prefix.
178    ///
179    /// # Examples
180    ///
181    /// ```
182    /// use typed_path::Utf8TypedPath;
183    ///
184    /// let path = Utf8TypedPath::derive("/foo/bar");
185    /// let parent = path.parent().unwrap();
186    /// assert_eq!(parent, Utf8TypedPath::derive("/foo"));
187    ///
188    /// let grand_parent = parent.parent().unwrap();
189    /// assert_eq!(grand_parent, Utf8TypedPath::derive("/"));
190    /// assert_eq!(grand_parent.parent(), None);
191    /// ```
192    pub fn parent(&self) -> Option<Self> {
193        match self {
194            Self::Unix(path) => path.parent().map(Self::Unix),
195            Self::Windows(path) => path.parent().map(Self::Windows),
196        }
197    }
198
199    /// Produces an iterator over [`Utf8TypedPath`] and its ancestors.
200    ///
201    /// The iterator will yield the [`Utf8TypedPath`] that is returned if the [`parent`] method is used
202    /// zero or more times. That means, the iterator will yield `&self`, `&self.parent().unwrap()`,
203    /// `&self.parent().unwrap().parent().unwrap()` and so on. If the [`parent`] method returns
204    /// [`None`], the iterator will do likewise. The iterator will always yield at least one value,
205    /// namely `&self`.
206    ///
207    /// # Examples
208    ///
209    /// ```
210    /// use typed_path::Utf8TypedPath;
211    ///
212    /// let mut ancestors = Utf8TypedPath::derive("/foo/bar").ancestors();
213    /// assert_eq!(ancestors.next(), Some(Utf8TypedPath::derive("/foo/bar")));
214    /// assert_eq!(ancestors.next(), Some(Utf8TypedPath::derive("/foo")));
215    /// assert_eq!(ancestors.next(), Some(Utf8TypedPath::derive("/")));
216    /// assert_eq!(ancestors.next(), None);
217    ///
218    /// let mut ancestors = Utf8TypedPath::derive("../foo/bar").ancestors();
219    /// assert_eq!(ancestors.next(), Some(Utf8TypedPath::derive("../foo/bar")));
220    /// assert_eq!(ancestors.next(), Some(Utf8TypedPath::derive("../foo")));
221    /// assert_eq!(ancestors.next(), Some(Utf8TypedPath::derive("..")));
222    /// assert_eq!(ancestors.next(), Some(Utf8TypedPath::derive("")));
223    /// assert_eq!(ancestors.next(), None);
224    /// ```
225    ///
226    /// [`parent`]: Utf8TypedPath::parent
227    #[inline]
228    pub fn ancestors(&self) -> Utf8TypedAncestors<'a> {
229        match self {
230            Self::Unix(p) => Utf8TypedAncestors::Unix(p.ancestors()),
231            Self::Windows(p) => Utf8TypedAncestors::Windows(p.ancestors()),
232        }
233    }
234
235    /// Returns the final component of the [`Utf8TypedPath`], if there is one.
236    ///
237    /// If the path is a normal file, this is the file name. If it's the path of a directory, this
238    /// is the directory name.
239    ///
240    /// Returns [`None`] if the path terminates in `..`.
241    ///
242    /// # Examples
243    ///
244    /// ```
245    /// use typed_path::Utf8TypedPath;
246    ///
247    /// assert_eq!(Some("bin"), Utf8TypedPath::derive("/usr/bin/").file_name());
248    /// assert_eq!(Some("foo.txt"), Utf8TypedPath::derive("tmp/foo.txt").file_name());
249    /// assert_eq!(Some("foo.txt"), Utf8TypedPath::derive("foo.txt/.").file_name());
250    /// assert_eq!(Some("foo.txt"), Utf8TypedPath::derive("foo.txt/.//").file_name());
251    /// assert_eq!(None, Utf8TypedPath::derive("foo.txt/..").file_name());
252    /// assert_eq!(None, Utf8TypedPath::derive("/").file_name());
253    /// ```
254    pub fn file_name(&self) -> Option<&str> {
255        impl_typed_fn!(self, file_name)
256    }
257
258    /// Returns a path that, when joined onto `base`, yields `self`.
259    ///
260    /// # Difference from Path
261    ///
262    /// Unlike [`Path::strip_prefix`], this implementation only supports types that implement
263    /// `AsRef<str>` instead of `AsRef<Path>`.
264    ///
265    /// [`Path::strip_prefix`]: crate::Path::strip_prefix
266    ///
267    /// # Errors
268    ///
269    /// If `base` is not a prefix of `self` (i.e., [`starts_with`]
270    /// returns `false`), returns [`Err`].
271    ///
272    /// [`starts_with`]: Utf8TypedPath::starts_with
273    ///
274    /// # Examples
275    ///
276    /// ```
277    /// use typed_path::{Utf8TypedPath, Utf8TypedPathBuf};
278    ///
279    /// let path = Utf8TypedPath::derive("/test/haha/foo.txt");
280    ///
281    /// assert_eq!(path.strip_prefix("/"), Ok(Utf8TypedPath::derive("test/haha/foo.txt")));
282    /// assert_eq!(path.strip_prefix("/test"), Ok(Utf8TypedPath::derive("haha/foo.txt")));
283    /// assert_eq!(path.strip_prefix("/test/"), Ok(Utf8TypedPath::derive("haha/foo.txt")));
284    /// assert_eq!(path.strip_prefix("/test/haha/foo.txt"), Ok(Utf8TypedPath::derive("")));
285    /// assert_eq!(path.strip_prefix("/test/haha/foo.txt/"), Ok(Utf8TypedPath::derive("")));
286    ///
287    /// assert!(path.strip_prefix("test").is_err());
288    /// assert!(path.strip_prefix("/haha").is_err());
289    ///
290    /// let prefix = Utf8TypedPathBuf::from("/test/");
291    /// assert_eq!(path.strip_prefix(prefix), Ok(Utf8TypedPath::derive("haha/foo.txt")));
292    /// ```
293    pub fn strip_prefix(
294        &self,
295        base: impl AsRef<str>,
296    ) -> Result<Utf8TypedPath<'_>, StripPrefixError> {
297        match self {
298            Self::Unix(p) => p
299                .strip_prefix(Utf8UnixPath::new(&base))
300                .map(Utf8TypedPath::Unix),
301            Self::Windows(p) => p
302                .strip_prefix(Utf8WindowsPath::new(&base))
303                .map(Utf8TypedPath::Windows),
304        }
305    }
306
307    /// Determines whether `base` is a prefix of `self`.
308    ///
309    /// Only considers whole path components to match.
310    ///
311    /// # Difference from Path
312    ///
313    /// Unlike [`Path::starts_with`], this implementation only supports types that implement
314    /// `AsRef<str>` instead of `AsRef<Path>`.
315    ///
316    /// [`Path::starts_with`]: crate::Path::starts_with
317    ///
318    /// # Examples
319    ///
320    /// ```
321    /// use typed_path::Utf8TypedPath;
322    ///
323    /// let path = Utf8TypedPath::derive("/etc/passwd");
324    ///
325    /// assert!(path.starts_with("/etc"));
326    /// assert!(path.starts_with("/etc/"));
327    /// assert!(path.starts_with("/etc/passwd"));
328    /// assert!(path.starts_with("/etc/passwd/")); // extra slash is okay
329    /// assert!(path.starts_with("/etc/passwd///")); // multiple extra slashes are okay
330    ///
331    /// assert!(!path.starts_with("/e"));
332    /// assert!(!path.starts_with("/etc/passwd.txt"));
333    ///
334    /// assert!(!Utf8TypedPath::derive("/etc/foo.rs").starts_with("/etc/foo"));
335    /// ```
336    pub fn starts_with(&self, base: impl AsRef<str>) -> bool {
337        match self {
338            Self::Unix(p) => p.starts_with(Utf8UnixPath::new(&base)),
339            Self::Windows(p) => p.starts_with(Utf8WindowsPath::new(&base)),
340        }
341    }
342
343    /// Determines whether `child` is a suffix of `self`.
344    ///
345    /// Only considers whole path components to match.
346    ///
347    /// # Difference from Path
348    ///
349    /// Unlike [`Path::ends_with`], this implementation only supports types that implement
350    /// `AsRef<str>` instead of `AsRef<Path>`.
351    ///
352    /// [`Path::ends_with`]: crate::Path::ends_with
353    ///
354    /// # Examples
355    ///
356    /// ```
357    /// use typed_path::Utf8TypedPath;
358    ///
359    /// let path = Utf8TypedPath::derive("/etc/resolv.conf");
360    ///
361    /// assert!(path.ends_with("resolv.conf"));
362    /// assert!(path.ends_with("etc/resolv.conf"));
363    /// assert!(path.ends_with("/etc/resolv.conf"));
364    ///
365    /// assert!(!path.ends_with("/resolv.conf"));
366    /// assert!(!path.ends_with("conf")); // use .extension() instead
367    /// ```
368    pub fn ends_with(&self, child: impl AsRef<str>) -> bool {
369        match self {
370            Self::Unix(p) => p.ends_with(Utf8UnixPath::new(&child)),
371            Self::Windows(p) => p.ends_with(Utf8WindowsPath::new(&child)),
372        }
373    }
374
375    /// Extracts the stem (non-extension) portion of [`self.file_name`].
376    ///
377    /// [`self.file_name`]: Utf8TypedPath::file_name
378    ///
379    /// The stem is:
380    ///
381    /// * [`None`], if there is no file name;
382    /// * The entire file name if there is no embedded `.`;
383    /// * The entire file name if the file name begins with `.` and has no other `.`s within;
384    /// * Otherwise, the portion of the file name before the final `.`
385    ///
386    /// # Examples
387    ///
388    /// ```
389    /// use typed_path::Utf8TypedPath;
390    ///
391    /// assert_eq!("foo", Utf8TypedPath::derive("foo.rs").file_stem().unwrap());
392    /// assert_eq!("foo.tar", Utf8TypedPath::derive("foo.tar.gz").file_stem().unwrap());
393    /// ```
394    ///
395    pub fn file_stem(&self) -> Option<&str> {
396        impl_typed_fn!(self, file_stem)
397    }
398
399    /// Extracts the extension of [`self.file_name`], if possible.
400    ///
401    /// The extension is:
402    ///
403    /// * [`None`], if there is no file name;
404    /// * [`None`], if there is no embedded `.`;
405    /// * [`None`], if the file name begins with `.` and has no other `.`s within;
406    /// * Otherwise, the portion of the file name after the final `.`
407    ///
408    /// [`self.file_name`]: Utf8TypedPath::file_name
409    ///
410    /// # Examples
411    ///
412    /// ```
413    /// use typed_path::Utf8TypedPath;
414    ///
415    /// assert_eq!("rs", Utf8TypedPath::derive("foo.rs").extension().unwrap());
416    /// assert_eq!("gz", Utf8TypedPath::derive("foo.tar.gz").extension().unwrap());
417    /// ```
418    pub fn extension(&self) -> Option<&str> {
419        impl_typed_fn!(self, extension)
420    }
421
422    /// Returns an owned [`Utf8TypedPathBuf`] by resolving `..` and `.` segments.
423    ///
424    /// When multiple, sequential path segment separation characters are found (e.g. `/` for Unix
425    /// and either `\` or `/` on Windows), they are replaced by a single instance of the
426    /// platform-specific path segment separator (`/` on Unix and `\` on Windows).
427    ///
428    /// # Examples
429    ///
430    /// ```
431    /// use typed_path::{Utf8TypedPath, Utf8TypedPathBuf};
432    ///
433    /// assert_eq!(
434    ///     Utf8TypedPath::derive("foo/bar//baz/./asdf/quux/..").normalize(),
435    ///     Utf8TypedPathBuf::from("foo/bar/baz/asdf"),
436    /// );
437    /// ```
438    ///
439    /// When starting with a root directory, any `..` segment whose parent is the root directory
440    /// will be filtered out:
441    ///
442    /// ```
443    /// use typed_path::{Utf8TypedPath, Utf8TypedPathBuf};
444    ///
445    /// // NOTE: A path cannot be created on its own without a defined encoding
446    /// assert_eq!(
447    ///     Utf8TypedPath::derive("/../foo").normalize(),
448    ///     Utf8TypedPathBuf::from("/foo"),
449    /// );
450    /// ```
451    ///
452    /// If any `..` is left unresolved as the path is relative and no parent is found, it is
453    /// discarded:
454    ///
455    /// ```
456    /// use typed_path::{Utf8TypedPath, Utf8TypedPathBuf};
457    ///
458    /// assert_eq!(
459    ///     Utf8TypedPath::derive("../foo/..").normalize(),
460    ///     Utf8TypedPathBuf::from(""),
461    /// );
462    ///
463    /// // Windows prefixes also count this way, but the prefix remains
464    /// assert_eq!(
465    ///     Utf8TypedPath::derive(r"C:..\foo\..").normalize(),
466    ///     Utf8TypedPathBuf::from(r"C:"),
467    /// );
468    /// ```
469    pub fn normalize(&self) -> Utf8TypedPathBuf {
470        match self {
471            Self::Unix(path) => Utf8TypedPathBuf::Unix(path.normalize()),
472            Self::Windows(path) => Utf8TypedPathBuf::Windows(path.normalize()),
473        }
474    }
475
476    /// Converts a path to an absolute form by [`normalizing`] the path, returning a
477    /// [`Utf8TypedPathBuf`].
478    ///
479    /// In the case that the path is relative, the current working directory is prepended prior to
480    /// normalizing.
481    ///
482    /// [`normalizing`]: Utf8TypedPath::normalize
483    ///
484    /// # Examples
485    ///
486    /// ```
487    /// use typed_path::{utils, Utf8TypedPath};
488    ///
489    /// // With an absolute path, it is just normalized
490    /// let path = Utf8TypedPath::derive("/a/b/../c/./d");
491    /// assert_eq!(path.absolutize().unwrap(), Utf8TypedPath::derive("/a/c/d"));
492    ///
493    /// // With a relative path, it is first joined with the current working directory
494    /// // and then normalized
495    /// let cwd = utils::current_dir().unwrap().with_unix_encoding().to_typed_path_buf();
496    /// let path = cwd.join("a/b/../c/./d");
497    /// assert_eq!(path.absolutize().unwrap(), cwd.join("a/c/d"));
498    /// ```
499    #[cfg(all(feature = "std", not(target_family = "wasm")))]
500    pub fn absolutize(&self) -> std::io::Result<Utf8TypedPathBuf> {
501        Ok(match self {
502            Self::Unix(path) => Utf8TypedPathBuf::Unix(path.absolutize()?),
503            Self::Windows(path) => Utf8TypedPathBuf::Windows(path.absolutize()?),
504        })
505    }
506
507    /// Creates an owned [`Utf8TypedPathBuf`] with `path` adjoined to `self`.
508    ///
509    /// See [`Utf8TypedPathBuf::push`] for more details on what it means to adjoin a path.
510    ///
511    /// # Difference from Path
512    ///
513    /// Unlike [`Utf8Path::join`], this implementation only supports types that implement
514    /// `AsRef<str>` instead of `AsRef<Path>`.
515    ///
516    /// [`Utf8Path::join`]: crate::Utf8Path::join
517    ///
518    /// # Examples
519    ///
520    /// ```
521    /// use typed_path::{Utf8TypedPath, Utf8TypedPathBuf};
522    ///
523    /// assert_eq!(
524    ///     Utf8TypedPath::derive("/etc").join("passwd"),
525    ///     Utf8TypedPathBuf::from("/etc/passwd"),
526    /// );
527    /// ```
528    pub fn join(&self, path: impl AsRef<str>) -> Utf8TypedPathBuf {
529        match self {
530            Self::Unix(p) => Utf8TypedPathBuf::Unix(p.join(Utf8UnixPath::new(&path))),
531            Self::Windows(p) => Utf8TypedPathBuf::Windows(p.join(Utf8WindowsPath::new(&path))),
532        }
533    }
534
535    /// Creates an owned [`Utf8TypedPathBuf`] with `path` adjoined to `self`, checking the `path` to
536    /// ensure it is safe to join. _When dealing with user-provided paths, this is the preferred
537    /// method._
538    ///
539    /// See [`Utf8TypedPathBuf::push_checked`] for more details on what it means to adjoin a path
540    /// safely.
541    ///
542    /// # Difference from Path
543    ///
544    /// Unlike [`Utf8Path::join_checked`], this implementation only supports types that implement
545    /// `AsRef<str>` instead of `AsRef<Path>`.
546    ///
547    /// [`Utf8Path::join_checked`]: crate::Utf8Path::join_checked
548    ///
549    /// # Examples
550    ///
551    /// ```
552    /// use typed_path::{CheckedPathError, Utf8TypedPath, Utf8TypedPathBuf};
553    ///
554    /// assert_eq!(
555    ///     Utf8TypedPath::derive("/etc").join_checked("passwd"),
556    ///     Ok(Utf8TypedPathBuf::from("/etc/passwd")),
557    /// );
558    ///
559    /// assert_eq!(
560    ///     Utf8TypedPath::derive("/etc").join_checked("/sneaky/path"),
561    ///     Err(CheckedPathError::UnexpectedRoot),
562    /// );
563    /// ```
564    pub fn join_checked(
565        &self,
566        path: impl AsRef<str>,
567    ) -> Result<Utf8TypedPathBuf, CheckedPathError> {
568        Ok(match self {
569            Self::Unix(p) => Utf8TypedPathBuf::Unix(p.join_checked(Utf8UnixPath::new(&path))?),
570            Self::Windows(p) => {
571                Utf8TypedPathBuf::Windows(p.join_checked(Utf8WindowsPath::new(&path))?)
572            }
573        })
574    }
575
576    /// Creates an owned [`Utf8TypedPathBuf`] like `self` but with the given file name.
577    ///
578    /// See [`Utf8TypedPathBuf::set_file_name`] for more details.
579    ///
580    /// # Examples
581    ///
582    /// ```
583    /// use typed_path::{Utf8TypedPath, Utf8TypedPathBuf};
584    ///
585    /// let path = Utf8TypedPath::derive("/tmp/foo.txt");
586    /// assert_eq!(path.with_file_name("bar.txt"), Utf8TypedPathBuf::from("/tmp/bar.txt"));
587    ///
588    /// let path = Utf8TypedPath::derive("/tmp");
589    /// assert_eq!(path.with_file_name("var"), Utf8TypedPathBuf::from("/var"));
590    /// ```
591    pub fn with_file_name<S: AsRef<str>>(&self, file_name: S) -> Utf8TypedPathBuf {
592        match self {
593            Self::Unix(path) => Utf8TypedPathBuf::Unix(path.with_file_name(file_name)),
594            Self::Windows(path) => Utf8TypedPathBuf::Windows(path.with_file_name(file_name)),
595        }
596    }
597
598    /// Creates an owned [`Utf8TypedPathBuf`] like `self` but with the given extension.
599    ///
600    /// See [`Utf8TypedPathBuf::set_extension`] for more details.
601    ///
602    /// # Examples
603    ///
604    /// ```
605    /// use typed_path::{Utf8TypedPath, Utf8TypedPathBuf};
606    ///
607    /// let path = Utf8TypedPath::derive("foo.rs");
608    /// assert_eq!(path.with_extension("txt"), Utf8TypedPathBuf::from("foo.txt"));
609    ///
610    /// let path = Utf8TypedPath::derive("foo.tar.gz");
611    /// assert_eq!(path.with_extension(""), Utf8TypedPathBuf::from("foo.tar"));
612    /// assert_eq!(path.with_extension("xz"), Utf8TypedPathBuf::from("foo.tar.xz"));
613    /// assert_eq!(path.with_extension("").with_extension("txt"), Utf8TypedPathBuf::from("foo.txt"));
614    /// ```
615    pub fn with_extension<S: AsRef<str>>(&self, extension: S) -> Utf8TypedPathBuf {
616        match self {
617            Self::Unix(path) => Utf8TypedPathBuf::Unix(path.with_extension(extension)),
618            Self::Windows(path) => Utf8TypedPathBuf::Windows(path.with_extension(extension)),
619        }
620    }
621
622    /// Produces an iterator over the [`Utf8TypedComponent`]s of the path.
623    ///
624    /// When parsing the path, there is a small amount of normalization:
625    ///
626    /// * Repeated separators are ignored, so `a/b` and `a//b` both have
627    ///   `a` and `b` as components.
628    ///
629    /// * Occurrences of `.` are normalized away, except if they are at the
630    ///   beginning of the path. For example, `a/./b`, `a/b/`, `a/b/.` and
631    ///   `a/b` all have `a` and `b` as components, but `./a/b` starts with
632    ///   an additional `CurDir` component.
633    ///
634    /// * A trailing slash is normalized away, `/a/b` and `/a/b/` are equivalent.
635    ///
636    /// Note that no other normalization takes place; in particular, `a/c`
637    /// and `a/b/../c` are distinct, to account for the possibility that `b`
638    /// is a symbolic link (so its parent isn't `a`).
639    ///
640    /// # Examples
641    ///
642    /// ```
643    /// use typed_path::{Utf8TypedPath, Utf8TypedComponent};
644    ///
645    /// let mut components = Utf8TypedPath::derive("/tmp/foo.txt").components();
646    ///
647    /// assert!(components.next().unwrap().is_root());
648    /// assert_eq!(components.next().unwrap().as_normal_str(), Some("tmp"));
649    /// assert_eq!(components.next().unwrap().as_normal_str(), Some("foo.txt"));
650    /// assert_eq!(components.next(), None)
651    /// ```
652    ///
653    ///[`Utf8TypedComponent`]: crate::Utf8TypedComponent
654    pub fn components(&self) -> Utf8TypedComponents<'a> {
655        match self {
656            Self::Unix(p) => Utf8TypedComponents::Unix(p.components()),
657            Self::Windows(p) => Utf8TypedComponents::Windows(p.components()),
658        }
659    }
660
661    /// Produces an iterator over the path's components viewed as [`str`] slices.
662    ///
663    /// For more information about the particulars of how the path is separated
664    /// into components, see [`components`].
665    ///
666    /// [`components`]: Utf8TypedPath::components
667    ///
668    /// # Examples
669    ///
670    /// ```
671    /// use typed_path::Utf8TypedPath;
672    ///
673    /// let mut it = Utf8TypedPath::derive("/tmp/foo.txt").iter();
674    ///
675    /// assert_eq!(it.next(), Some(typed_path::constants::unix::SEPARATOR_STR));
676    /// assert_eq!(it.next(), Some("tmp"));
677    /// assert_eq!(it.next(), Some("foo.txt"));
678    /// assert_eq!(it.next(), None)
679    /// ```
680    #[inline]
681    pub fn iter(&self) -> Utf8TypedIter<'a> {
682        match self {
683            Self::Unix(p) => Utf8TypedIter::Unix(p.iter()),
684            Self::Windows(p) => Utf8TypedIter::Windows(p.iter()),
685        }
686    }
687
688    /// Returns true if this path represents a Unix path.
689    #[inline]
690    pub fn is_unix(&self) -> bool {
691        matches!(self, Self::Unix(_))
692    }
693
694    /// Returns true if this path represents a Windows path.
695    #[inline]
696    pub fn is_windows(&self) -> bool {
697        matches!(self, Self::Windows(_))
698    }
699
700    /// Converts this [`Utf8TypedPath`] into the Unix variant of [`Utf8TypedPathBuf`].
701    pub fn with_unix_encoding(&self) -> Utf8TypedPathBuf {
702        match self {
703            Self::Windows(p) => Utf8TypedPathBuf::Unix(p.with_unix_encoding()),
704            _ => self.to_path_buf(),
705        }
706    }
707
708    /// Converts this [`Utf8TypedPath`] into the Unix variant of [`Utf8TypedPathBuf`], ensuring it
709    /// is a valid Unix path.
710    pub fn with_unix_encoding_checked(&self) -> Result<Utf8TypedPathBuf, CheckedPathError> {
711        Ok(match self {
712            Self::Unix(p) => Utf8TypedPathBuf::Unix(p.with_unix_encoding_checked()?),
713            Self::Windows(p) => Utf8TypedPathBuf::Unix(p.with_unix_encoding_checked()?),
714        })
715    }
716
717    /// Converts this [`Utf8TypedPath`] into the Windows variant of [`Utf8TypedPathBuf`].
718    pub fn with_windows_encoding(&self) -> Utf8TypedPathBuf {
719        match self {
720            Self::Unix(p) => Utf8TypedPathBuf::Windows(p.with_windows_encoding()),
721            _ => self.to_path_buf(),
722        }
723    }
724
725    /// Converts this [`Utf8TypedPath`] into the Windows variant of [`Utf8TypedPathBuf`], ensuring
726    /// it is a valid Windows path.
727    pub fn with_windows_encoding_checked(&self) -> Result<Utf8TypedPathBuf, CheckedPathError> {
728        Ok(match self {
729            Self::Unix(p) => Utf8TypedPathBuf::Windows(p.with_windows_encoding_checked()?),
730            Self::Windows(p) => Utf8TypedPathBuf::Windows(p.with_windows_encoding_checked()?),
731        })
732    }
733}
734
735impl fmt::Display for Utf8TypedPath<'_> {
736    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
737        match self {
738            Self::Unix(path) => fmt::Display::fmt(path, f),
739            Self::Windows(path) => fmt::Display::fmt(path, f),
740        }
741    }
742}
743
744impl<'a> From<&'a str> for Utf8TypedPath<'a> {
745    #[inline]
746    fn from(s: &'a str) -> Self {
747        Utf8TypedPath::derive(s)
748    }
749}
750
751impl AsRef<str> for Utf8TypedPath<'_> {
752    #[inline]
753    fn as_ref(&self) -> &str {
754        self.as_str()
755    }
756}
757
758impl TryAsRef<Utf8UnixPath> for Utf8TypedPath<'_> {
759    fn try_as_ref(&self) -> Option<&Utf8UnixPath> {
760        match self {
761            Self::Unix(path) => Some(path),
762            _ => None,
763        }
764    }
765}
766
767impl TryAsRef<Utf8WindowsPath> for Utf8TypedPath<'_> {
768    fn try_as_ref(&self) -> Option<&Utf8WindowsPath> {
769        match self {
770            Self::Windows(path) => Some(path),
771            _ => None,
772        }
773    }
774}
775
776impl PartialEq<Utf8TypedPathBuf> for Utf8TypedPath<'_> {
777    fn eq(&self, path: &Utf8TypedPathBuf) -> bool {
778        self.eq(&path.to_path())
779    }
780}
781
782impl PartialEq<str> for Utf8TypedPath<'_> {
783    fn eq(&self, path: &str) -> bool {
784        self.as_str() == path
785    }
786}
787
788impl PartialEq<Utf8TypedPath<'_>> for str {
789    fn eq(&self, path: &Utf8TypedPath<'_>) -> bool {
790        self == path.as_str()
791    }
792}
793
794impl<'a> PartialEq<&'a str> for Utf8TypedPath<'_> {
795    fn eq(&self, path: &&'a str) -> bool {
796        self.as_str() == *path
797    }
798}
799
800impl PartialEq<Utf8TypedPath<'_>> for &str {
801    fn eq(&self, path: &Utf8TypedPath<'_>) -> bool {
802        *self == path.as_str()
803    }
804}
805
806#[cfg(feature = "serde")]
807impl Serialize for Utf8TypedPath<'_> {
808    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
809    where
810        S: Serializer,
811    {
812        match self {
813            Self::Unix(unix_path) => unix_path.serialize(serializer),
814            Self::Windows(windows_path) => windows_path.serialize(serializer),
815        }
816    }
817}
818
819#[cfg(feature = "serde")]
820struct Utf8TypedPathVisitor;
821
822#[cfg(feature = "serde")]
823impl<'de> Visitor<'de> for Utf8TypedPathVisitor {
824    type Value = Utf8TypedPath<'de>;
825
826    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
827        formatter.write_str("a borrowed UTF-8 typed path")
828    }
829
830    fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
831    where
832        E: de::Error,
833    {
834        Ok(Utf8TypedPath::derive(v))
835    }
836
837    fn visit_borrowed_bytes<E>(self, v: &'de [u8]) -> Result<Self::Value, E>
838    where
839        E: de::Error,
840    {
841        core::str::from_utf8(v)
842            .map(Utf8TypedPath::derive)
843            .map_err(|_| de::Error::invalid_value(Unexpected::Bytes(v), &self))
844    }
845}
846
847#[cfg(feature = "serde")]
848impl<'de: 'a, 'a> Deserialize<'de> for Utf8TypedPath<'a> {
849    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
850    where
851        D: Deserializer<'de>,
852    {
853        deserializer.deserialize_str(Utf8TypedPathVisitor)
854    }
855}
856
857#[cfg(all(test, feature = "serde"))]
858mod tests {
859    use crate::Utf8TypedPath;
860
861    #[test]
862    fn serialize_unix() {
863        let path = Utf8TypedPath::unix("/tmp");
864
865        assert_eq!(serde_json::to_string(&path).unwrap(), r#""/tmp""#);
866    }
867
868    #[test]
869    fn serialize_windows() {
870        let path = Utf8TypedPath::windows(r"C:\tmp");
871
872        assert_eq!(serde_json::to_string(&path).unwrap(), r#""C:\\tmp""#);
873    }
874
875    #[test]
876    fn deserialize_unix_str() {
877        let path: Utf8TypedPath<'_> = serde_json::from_str(r#""/tmp""#).unwrap();
878
879        assert!(path.is_unix());
880        assert_eq!(path, Utf8TypedPath::unix("/tmp"));
881    }
882
883    #[test]
884    fn deserialize_unix_bytes() {
885        let path: Utf8TypedPath<'_> = serde_json::from_slice(br#""/tmp""#).unwrap();
886
887        assert!(path.is_unix());
888        assert_eq!(path, Utf8TypedPath::unix("/tmp"));
889    }
890
891    #[test]
892    fn deserialize_root_windows_path() {
893        // Windows paths can't be borrowed from JSON since the backslashes need to be unescaped
894        assert!(
895            serde_json::from_str::<Utf8TypedPath<'_>>(r#""\\some\\path\\to\\file.txt""#).is_err()
896        );
897    }
898}