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}