1use serde::{Deserialize, Serialize};
15use typed_path::{Utf8TypedPath, Utf8TypedPathBuf, Utf8UnixPath, Utf8WindowsPath};
16
17use crate::error::{Error, ErrorKind, Result};
18
19mod components;
20pub(crate) mod stream;
21
22pub use components::{Component, Components, WindowsPrefix};
23pub use stream::{StreamSpec, StreamSpecBuf};
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
27#[non_exhaustive]
28pub enum WellKnownPath {
29 HomeDir,
31 CacheDir,
33 TempDir,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
39pub enum Kind {
40 Unix,
42 Windows,
45}
46
47impl Kind {
48 pub const fn native() -> Self {
50 if cfg!(windows) {
51 Self::Windows
52 } else {
53 Self::Unix
54 }
55 }
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
60pub struct Path<'a>(pub(crate) Utf8TypedPath<'a>);
61
62macro_rules! project {
68 ($self:expr, |$path:ident| $body:expr) => {
69 match $self.0 {
70 Utf8TypedPath::Unix($path) => $body,
71 Utf8TypedPath::Windows($path) => $body,
72 }
73 };
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
78pub struct PathBuf(pub(crate) Utf8TypedPathBuf);
79
80impl<'a> Path<'a> {
81 pub fn new(path: &'a (impl AsRef<str> + ?Sized), kind: Kind) -> Self {
83 match kind {
84 Kind::Unix => Self::unix(path),
85 Kind::Windows => Self::windows(path),
86 }
87 }
88
89 pub fn unix(path: &'a (impl AsRef<str> + ?Sized)) -> Self {
91 Self(Utf8TypedPath::Unix(Utf8UnixPath::new(path.as_ref())))
92 }
93
94 pub fn windows(path: &'a (impl AsRef<str> + ?Sized)) -> Self {
96 Self(Utf8TypedPath::Windows(Utf8WindowsPath::new(path.as_ref())))
97 }
98
99 pub fn kind(&self) -> Kind {
101 match self.0 {
102 Utf8TypedPath::Unix(_) => Kind::Unix,
103 Utf8TypedPath::Windows(_) => Kind::Windows,
104 }
105 }
106
107 pub fn as_str(&self) -> &'a str {
109 project!(self, |path| path.as_str())
110 }
111
112 pub fn to_path_buf(&self) -> PathBuf {
114 PathBuf(self.0.to_path_buf())
115 }
116
117 pub fn to_native(&self) -> Result<std::path::PathBuf> {
123 if self.kind() != Kind::native() {
124 return Err(Error::new(
125 ErrorKind::InvalidInput,
126 "path style does not match VFS target",
127 ));
128 }
129 Ok(std::path::PathBuf::from(self.as_str()))
130 }
131
132 pub fn to_kind(&self, kind: Kind) -> Result<PathBuf> {
141 if self.kind() == kind {
142 return Ok(self.to_path_buf());
143 }
144 let convertible = match self.0 {
145 Utf8TypedPath::Windows(path) => {
146 !path.has_root()
147 && path.components().prefix_kind().is_none()
148 && !path.file_name().is_some_and(|name| name.contains(':'))
149 }
150 Utf8TypedPath::Unix(path) => !path.has_root(),
151 };
152 if !convertible || self.is_absolute() {
153 return Err(Error::new(
154 ErrorKind::InvalidInput,
155 "only relative, unrooted paths can be converted between path types",
156 ));
157 }
158 let converted = match kind {
159 Kind::Unix => self.0.with_unix_encoding_checked(),
160 Kind::Windows => self.0.with_windows_encoding_checked(),
161 };
162 converted.map(PathBuf).map_err(|_| {
163 Error::new(
164 ErrorKind::InvalidInput,
165 "path cannot be converted between path types",
166 )
167 })
168 }
169
170 pub fn is_absolute(&self) -> bool {
172 self.0.is_absolute()
173 }
174
175 pub fn is_relative(&self) -> bool {
177 self.0.is_relative()
178 }
179
180 pub fn has_root(&self) -> bool {
182 self.0.has_root()
183 }
184
185 pub fn starts_with(&self, base: impl AsRef<str>) -> bool {
187 self.0.starts_with(base)
188 }
189
190 pub fn ends_with(&self, child: impl AsRef<str>) -> bool {
192 self.0.ends_with(child)
193 }
194
195 pub fn strip_prefix(&self, base: impl AsRef<str>) -> Result<Path<'a>> {
201 let base = base.as_ref();
202 let stripped = match self.0 {
203 Utf8TypedPath::Unix(path) => path
204 .strip_prefix(Utf8UnixPath::new(base))
205 .map(Utf8TypedPath::Unix),
206 Utf8TypedPath::Windows(path) => path
207 .strip_prefix(Utf8WindowsPath::new(base))
208 .map(Utf8TypedPath::Windows),
209 };
210 stripped.map(Path).map_err(|_| {
211 Error::new(
212 ErrorKind::InvalidInput,
213 "path does not start with the given prefix",
214 )
215 })
216 }
217
218 pub fn parent(&self) -> Option<Self> {
220 match self.0 {
221 Utf8TypedPath::Unix(path) => path.parent().map(Utf8TypedPath::Unix),
222 Utf8TypedPath::Windows(path) => path.parent().map(Utf8TypedPath::Windows),
223 }
224 .map(Self)
225 }
226
227 pub fn file_name(&self) -> Option<&'a str> {
230 self.split_stream().0
231 }
232
233 pub fn file_name_raw(&self) -> Option<&'a str> {
236 project!(self, |path| path.file_name())
237 }
238
239 pub fn file_stem(&self) -> Option<&'a str> {
242 match self.split_stream() {
243 (Some(base), Some(_)) => Path::new(base, self.kind()).file_stem_raw(),
244 _ => self.file_stem_raw(),
245 }
246 }
247
248 pub fn extension(&self) -> Option<&'a str> {
251 match self.split_stream() {
252 (Some(base), Some(_)) => Path::new(base, self.kind()).extension_raw(),
253 _ => self.extension_raw(),
254 }
255 }
256
257 fn file_stem_raw(&self) -> Option<&'a str> {
258 project!(self, |path| path.file_stem())
259 }
260
261 fn extension_raw(&self) -> Option<&'a str> {
262 project!(self, |path| path.extension())
263 }
264
265 fn split_stream(&self) -> (Option<&'a str>, Option<StreamSpec<'a>>) {
272 let Some(name) = self.file_name_raw() else {
273 return (None, None);
274 };
275 if self.kind() != Kind::Windows {
276 return (Some(name), None);
277 }
278 match stream::split_suffix(name) {
279 Ok((base, spec)) => (Some(base), spec),
280 Err(_) => (Some(name), None),
281 }
282 }
283
284 pub fn stream(&self) -> Result<Option<StreamSpec<'a>>> {
293 if self.kind() != Kind::Windows {
294 return Ok(None);
295 }
296 match self.file_name_raw() {
297 Some(name) => Ok(stream::split_suffix(name)?.1),
298 None => Ok(None),
299 }
300 }
301
302 pub fn with_stream(&self, spec: Option<StreamSpec<'_>>) -> PathBuf {
306 let (Some(base), _) = self.split_stream() else {
307 return self.to_path_buf();
308 };
309 if self.kind() != Kind::Windows {
310 return self.to_path_buf();
311 }
312 self.with_file_name_raw(stream::join_suffix(base, spec))
313 }
314
315 pub fn without_stream(&self) -> PathBuf {
317 self.with_stream(None)
318 }
319
320 pub fn components(&self) -> Components<'a> {
322 Components(self.0.components())
323 }
324
325 pub fn windows_prefix(&self) -> Option<WindowsPrefix<'a>> {
327 match self.0 {
328 Utf8TypedPath::Windows(path) => path
331 .components()
332 .next()
333 .and_then(|component| component.prefix_kind())
334 .map(WindowsPrefix::from),
335 Utf8TypedPath::Unix(_) => None,
336 }
337 }
338
339 pub fn join(&self, path: impl AsRef<str>) -> PathBuf {
341 PathBuf(self.0.join(path))
342 }
343
344 pub fn with_file_name(&self, file_name: impl AsRef<str>) -> PathBuf {
347 match self.split_stream() {
348 (_, Some(spec)) => {
349 self.with_file_name_raw(stream::join_suffix(file_name.as_ref(), Some(spec)))
350 }
351 _ => self.with_file_name_raw(file_name),
352 }
353 }
354
355 pub fn with_file_name_raw(&self, file_name: impl AsRef<str>) -> PathBuf {
358 PathBuf(self.0.with_file_name(file_name))
359 }
360
361 pub fn with_extension(&self, extension: impl AsRef<str>) -> PathBuf {
364 let (Some(base), Some(spec)) = self.split_stream() else {
365 return PathBuf(self.0.with_extension(extension));
366 };
367 let base = Path::new(base, self.kind()).0.with_extension(extension);
368 self.with_file_name_raw(stream::join_suffix(base.as_str(), Some(spec)))
369 }
370
371 pub fn normalize(&self) -> PathBuf {
377 let has_root = self.has_root();
378 let mut components = Vec::new();
379
380 for component in self.components() {
381 if component.is_current() {
382 continue;
383 }
384 if component.is_parent() {
385 if components.last().is_some_and(Component::is_normal) {
386 components.pop();
387 } else if !has_root {
388 components.push(component);
389 }
390 } else {
391 components.push(component);
392 }
393 }
394
395 let mut normalized = PathBuf::empty(self.kind());
396 for component in components {
397 normalized.push(component.as_str());
398 }
399 normalized
400 }
401}
402
403impl PathBuf {
404 pub fn new(path: impl AsRef<str>, kind: Kind) -> Self {
406 match kind {
407 Kind::Unix => Self::from_unix(path),
408 Kind::Windows => Self::from_windows(path),
409 }
410 }
411
412 pub fn from_unix(path: impl AsRef<str>) -> Self {
414 Self(Utf8TypedPathBuf::from_unix(path))
415 }
416
417 pub fn from_windows(path: impl AsRef<str>) -> Self {
419 Self(Utf8TypedPathBuf::from_windows(path))
420 }
421
422 pub fn empty(kind: Kind) -> Self {
424 Self::new("", kind)
425 }
426
427 pub fn from_native(path: std::path::PathBuf) -> Result<Self> {
433 let path = path
434 .into_os_string()
435 .into_string()
436 .map_err(|_| Error::new(ErrorKind::InvalidData, "path is not valid UTF-8"))?;
437 Ok(Self::new(path, Kind::native()))
438 }
439
440 pub fn to_path(&self) -> Path<'_> {
442 Path(self.0.to_path())
443 }
444
445 pub fn push(&mut self, path: impl AsRef<str>) {
447 self.0.push(path);
448 }
449
450 pub fn pop(&mut self) -> bool {
452 self.0.pop()
453 }
454
455 pub fn set_file_name(&mut self, file_name: impl AsRef<str>) {
457 let next = self.to_path().with_file_name(file_name);
458 *self = next;
459 }
460
461 pub fn set_extension(&mut self, extension: impl AsRef<str>) -> bool {
465 if self.to_path().split_stream().1.is_none() {
466 return self.0.set_extension(extension);
467 }
468 let next = self.to_path().with_extension(extension);
469 *self = next;
470 true
471 }
472
473 pub fn set_stream(&mut self, spec: Option<StreamSpec<'_>>) {
477 let next = self.to_path().with_stream(spec);
478 *self = next;
479 }
480}
481
482impl PathBuf {
484 pub fn kind(&self) -> Kind {
486 self.to_path().kind()
487 }
488
489 pub fn as_str(&self) -> &str {
491 self.0.as_str()
492 }
493
494 pub fn to_native(&self) -> Result<std::path::PathBuf> {
500 self.to_path().to_native()
501 }
502
503 pub fn to_kind(&self, kind: Kind) -> Result<Self> {
509 self.to_path().to_kind(kind)
510 }
511
512 pub fn is_absolute(&self) -> bool {
514 self.0.is_absolute()
515 }
516
517 pub fn is_relative(&self) -> bool {
519 self.0.is_relative()
520 }
521
522 pub fn has_root(&self) -> bool {
524 self.0.has_root()
525 }
526
527 pub fn starts_with(&self, base: impl AsRef<str>) -> bool {
529 self.0.starts_with(base)
530 }
531
532 pub fn ends_with(&self, child: impl AsRef<str>) -> bool {
534 self.0.ends_with(child)
535 }
536
537 pub fn strip_prefix(&self, base: impl AsRef<str>) -> Result<Path<'_>> {
543 self.to_path().strip_prefix(base)
544 }
545
546 pub fn parent(&self) -> Option<Path<'_>> {
548 self.0.parent().map(Path)
549 }
550
551 pub fn file_name(&self) -> Option<&str> {
554 self.to_path().file_name()
555 }
556
557 pub fn file_name_raw(&self) -> Option<&str> {
560 self.0.file_name()
561 }
562
563 pub fn file_stem(&self) -> Option<&str> {
566 self.to_path().file_stem()
567 }
568
569 pub fn extension(&self) -> Option<&str> {
572 self.to_path().extension()
573 }
574
575 pub fn stream(&self) -> Result<Option<StreamSpec<'_>>> {
581 self.to_path().stream()
582 }
583
584 pub fn without_stream(&self) -> Self {
586 self.to_path().without_stream()
587 }
588
589 pub fn components(&self) -> Components<'_> {
591 Components(self.0.components())
592 }
593
594 pub fn join(&self, path: impl AsRef<str>) -> Self {
596 self.to_path().join(path)
597 }
598
599 pub fn with_file_name(&self, file_name: impl AsRef<str>) -> Self {
602 self.to_path().with_file_name(file_name)
603 }
604
605 pub fn with_extension(&self, extension: impl AsRef<str>) -> Self {
608 self.to_path().with_extension(extension)
609 }
610
611 pub fn normalize(&self) -> Self {
614 self.to_path().normalize()
615 }
616
617 pub fn windows_prefix(&self) -> Option<WindowsPrefix<'_>> {
619 self.to_path().windows_prefix()
620 }
621}
622
623impl AsRef<str> for Path<'_> {
624 fn as_ref(&self) -> &str {
625 self.as_str()
626 }
627}
628
629impl AsRef<str> for PathBuf {
630 fn as_ref(&self) -> &str {
631 self.as_str()
632 }
633}
634
635impl std::fmt::Display for Path<'_> {
636 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
637 f.write_str(self.as_str())
638 }
639}
640
641impl std::fmt::Display for PathBuf {
642 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
643 f.write_str(self.as_str())
644 }
645}
646
647impl<'a> From<Path<'a>> for PathBuf {
648 fn from(path: Path<'a>) -> Self {
649 path.to_path_buf()
650 }
651}
652
653impl<'a> From<&'a PathBuf> for Path<'a> {
654 fn from(path: &'a PathBuf) -> Self {
655 path.to_path()
656 }
657}
658
659impl TryFrom<std::path::PathBuf> for PathBuf {
660 type Error = Error;
661
662 fn try_from(path: std::path::PathBuf) -> Result<Self> {
663 Self::from_native(path)
664 }
665}
666
667impl TryFrom<PathBuf> for std::path::PathBuf {
668 type Error = Error;
669
670 fn try_from(path: PathBuf) -> Result<Self> {
671 path.to_native()
672 }
673}
674
675#[derive(Serialize, Deserialize)]
680#[serde(rename = "PathBuf")]
681struct Wire {
682 kind: Kind,
683 path: String,
684}
685
686impl Serialize for PathBuf {
687 fn serialize<S: serde::Serializer>(
688 &self,
689 serializer: S,
690 ) -> std::result::Result<S::Ok, S::Error> {
691 Wire {
692 kind: self.kind(),
693 path: self.as_str().to_owned(),
694 }
695 .serialize(serializer)
696 }
697}
698
699impl<'de> Deserialize<'de> for PathBuf {
700 fn deserialize<D: serde::Deserializer<'de>>(
701 deserializer: D,
702 ) -> std::result::Result<Self, D::Error> {
703 let wire = Wire::deserialize(deserializer)?;
704 Ok(Self::new(wire.path, wire.kind))
705 }
706}
707
708#[cfg(test)]
709mod tests {
710 use super::{Kind, Path, PathBuf};
711
712 #[test]
713 fn round_trip_preserves_unix_kind_and_literal_form() {
714 let path = PathBuf::from_unix(r"foo\bar/baz");
715 let bytes = postcard::to_stdvec(&path).unwrap();
716 let decoded: PathBuf = postcard::from_bytes(&bytes).unwrap();
717 assert_eq!(decoded.kind(), Kind::Unix);
718 assert_eq!(decoded.as_str(), r"foo\bar/baz");
719 }
720
721 #[test]
722 fn round_trip_preserves_windows_kind_and_literal_form() {
723 let path = PathBuf::from_windows(r"C:\foo/bar");
724 let bytes = postcard::to_stdvec(&path).unwrap();
725 let decoded: PathBuf = postcard::from_bytes(&bytes).unwrap();
726 assert_eq!(decoded.kind(), Kind::Windows);
727 assert_eq!(decoded.as_str(), r"C:\foo/bar");
728 }
729
730 #[test]
731 fn native_conversion_rejects_the_other_path_kind() {
732 let path = if cfg!(windows) {
733 PathBuf::from_unix("foo")
734 } else {
735 PathBuf::from_windows("foo")
736 };
737 assert!(path.to_native().is_err());
738 }
739
740 #[test]
741 fn to_kind_rejects_rooted_and_stream_bearing_paths() {
742 assert!(Path::windows(r"C:\foo").to_kind(Kind::Unix).is_err());
743 assert!(Path::windows(r"\foo").to_kind(Kind::Unix).is_err());
744 assert!(Path::windows("file.txt:zone").to_kind(Kind::Unix).is_err());
745 assert!(Path::unix("/foo").to_kind(Kind::Windows).is_err());
746
747 let converted = Path::windows(r"foo\bar").to_kind(Kind::Unix).unwrap();
748 assert_eq!(converted.kind(), Kind::Unix);
749 assert_eq!(converted.as_str(), "foo/bar");
750 }
751
752 #[test]
753 fn to_kind_is_identity_for_the_same_kind() {
754 let path = Path::windows(r"C:\foo");
755 assert_eq!(path.to_kind(Kind::Windows).unwrap().as_str(), r"C:\foo");
756 }
757
758 #[test]
759 fn normalize_resolves_dot_and_dotdot() {
760 assert_eq!(Path::unix("a/./b/../c").normalize().as_str(), "a/c");
761 assert_eq!(Path::unix("../a/../b").normalize().as_str(), "../b");
762 assert_eq!(Path::unix("/../a").normalize().as_str(), "/a");
763 }
764}