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 DataDir,
37 DataLocalDir,
39 StateDir,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
45pub enum Kind {
46 Unix,
48 Windows,
51}
52
53impl Kind {
54 pub const fn native() -> Self {
56 if cfg!(windows) {
57 Self::Windows
58 } else {
59 Self::Unix
60 }
61 }
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
66pub struct Path<'a>(pub(crate) Utf8TypedPath<'a>);
67
68macro_rules! project {
74 ($self:expr, |$path:ident| $body:expr) => {
75 match $self.0 {
76 Utf8TypedPath::Unix($path) => $body,
77 Utf8TypedPath::Windows($path) => $body,
78 }
79 };
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
84pub struct PathBuf(pub(crate) Utf8TypedPathBuf);
85
86impl<'a> Path<'a> {
87 pub fn new(path: &'a (impl AsRef<str> + ?Sized), kind: Kind) -> Self {
89 match kind {
90 Kind::Unix => Self::unix(path),
91 Kind::Windows => Self::windows(path),
92 }
93 }
94
95 pub fn unix(path: &'a (impl AsRef<str> + ?Sized)) -> Self {
97 Self(Utf8TypedPath::Unix(Utf8UnixPath::new(path.as_ref())))
98 }
99
100 pub fn windows(path: &'a (impl AsRef<str> + ?Sized)) -> Self {
102 Self(Utf8TypedPath::Windows(Utf8WindowsPath::new(path.as_ref())))
103 }
104
105 pub fn kind(&self) -> Kind {
107 match self.0 {
108 Utf8TypedPath::Unix(_) => Kind::Unix,
109 Utf8TypedPath::Windows(_) => Kind::Windows,
110 }
111 }
112
113 pub fn as_str(&self) -> &'a str {
115 project!(self, |path| path.as_str())
116 }
117
118 pub fn to_path_buf(&self) -> PathBuf {
120 PathBuf(self.0.to_path_buf())
121 }
122
123 pub fn to_native(&self) -> Result<std::path::PathBuf> {
129 if self.kind() != Kind::native() {
130 return Err(Error::new(
131 ErrorKind::InvalidInput,
132 "path style does not match VFS target",
133 ));
134 }
135 Ok(std::path::PathBuf::from(self.as_str()))
136 }
137
138 pub fn to_kind(&self, kind: Kind) -> Result<PathBuf> {
147 if self.kind() == kind {
148 return Ok(self.to_path_buf());
149 }
150 let convertible = match self.0 {
151 Utf8TypedPath::Windows(path) => {
152 !path.has_root()
153 && path.components().prefix_kind().is_none()
154 && !path.file_name().is_some_and(|name| name.contains(':'))
155 }
156 Utf8TypedPath::Unix(path) => !path.has_root(),
157 };
158 if !convertible || self.is_absolute() {
159 return Err(Error::new(
160 ErrorKind::InvalidInput,
161 "only relative, unrooted paths can be converted between path types",
162 ));
163 }
164 let converted = match kind {
165 Kind::Unix => self.0.with_unix_encoding_checked(),
166 Kind::Windows => self.0.with_windows_encoding_checked(),
167 };
168 converted.map(PathBuf).map_err(|_| {
169 Error::new(
170 ErrorKind::InvalidInput,
171 "path cannot be converted between path types",
172 )
173 })
174 }
175
176 pub fn is_absolute(&self) -> bool {
178 self.0.is_absolute()
179 }
180
181 pub fn is_relative(&self) -> bool {
183 self.0.is_relative()
184 }
185
186 pub fn has_root(&self) -> bool {
188 self.0.has_root()
189 }
190
191 pub fn starts_with(&self, base: impl AsRef<str>) -> bool {
193 self.0.starts_with(base)
194 }
195
196 pub fn ends_with(&self, child: impl AsRef<str>) -> bool {
198 self.0.ends_with(child)
199 }
200
201 pub fn strip_prefix(&self, base: impl AsRef<str>) -> Result<Path<'a>> {
207 let base = base.as_ref();
208 let stripped = match self.0 {
209 Utf8TypedPath::Unix(path) => path
210 .strip_prefix(Utf8UnixPath::new(base))
211 .map(Utf8TypedPath::Unix),
212 Utf8TypedPath::Windows(path) => path
213 .strip_prefix(Utf8WindowsPath::new(base))
214 .map(Utf8TypedPath::Windows),
215 };
216 stripped.map(Path).map_err(|_| {
217 Error::new(
218 ErrorKind::InvalidInput,
219 "path does not start with the given prefix",
220 )
221 })
222 }
223
224 pub fn parent(&self) -> Option<Self> {
226 match self.0 {
227 Utf8TypedPath::Unix(path) => path.parent().map(Utf8TypedPath::Unix),
228 Utf8TypedPath::Windows(path) => path.parent().map(Utf8TypedPath::Windows),
229 }
230 .map(Self)
231 }
232
233 pub fn file_name(&self) -> Option<&'a str> {
236 self.split_stream().0
237 }
238
239 pub fn file_name_raw(&self) -> Option<&'a str> {
242 project!(self, |path| path.file_name())
243 }
244
245 pub fn file_stem(&self) -> Option<&'a str> {
248 match self.split_stream() {
249 (Some(base), Some(_)) => Path::new(base, self.kind()).file_stem_raw(),
250 _ => self.file_stem_raw(),
251 }
252 }
253
254 pub fn extension(&self) -> Option<&'a str> {
257 match self.split_stream() {
258 (Some(base), Some(_)) => Path::new(base, self.kind()).extension_raw(),
259 _ => self.extension_raw(),
260 }
261 }
262
263 fn file_stem_raw(&self) -> Option<&'a str> {
264 project!(self, |path| path.file_stem())
265 }
266
267 fn extension_raw(&self) -> Option<&'a str> {
268 project!(self, |path| path.extension())
269 }
270
271 fn split_stream(&self) -> (Option<&'a str>, Option<StreamSpec<'a>>) {
278 let Some(name) = self.file_name_raw() else {
279 return (None, None);
280 };
281 if self.kind() != Kind::Windows {
282 return (Some(name), None);
283 }
284 match stream::split_suffix(name) {
285 Ok((base, spec)) => (Some(base), spec),
286 Err(_) => (Some(name), None),
287 }
288 }
289
290 pub fn stream(&self) -> Result<Option<StreamSpec<'a>>> {
299 if self.kind() != Kind::Windows {
300 return Ok(None);
301 }
302 match self.file_name_raw() {
303 Some(name) => Ok(stream::split_suffix(name)?.1),
304 None => Ok(None),
305 }
306 }
307
308 pub fn with_stream(&self, spec: Option<StreamSpec<'_>>) -> PathBuf {
312 let (Some(base), _) = self.split_stream() else {
313 return self.to_path_buf();
314 };
315 if self.kind() != Kind::Windows {
316 return self.to_path_buf();
317 }
318 self.with_file_name_raw(stream::join_suffix(base, spec))
319 }
320
321 pub fn without_stream(&self) -> PathBuf {
323 self.with_stream(None)
324 }
325
326 pub fn components(&self) -> Components<'a> {
328 Components(self.0.components())
329 }
330
331 pub fn windows_prefix(&self) -> Option<WindowsPrefix<'a>> {
333 match self.0 {
334 Utf8TypedPath::Windows(path) => path
337 .components()
338 .next()
339 .and_then(|component| component.prefix_kind())
340 .map(WindowsPrefix::from),
341 Utf8TypedPath::Unix(_) => None,
342 }
343 }
344
345 pub fn join(&self, path: impl AsRef<str>) -> PathBuf {
347 PathBuf(self.0.join(path))
348 }
349
350 pub fn with_file_name(&self, file_name: impl AsRef<str>) -> PathBuf {
353 match self.split_stream() {
354 (_, Some(spec)) => {
355 self.with_file_name_raw(stream::join_suffix(file_name.as_ref(), Some(spec)))
356 }
357 _ => self.with_file_name_raw(file_name),
358 }
359 }
360
361 pub fn with_file_name_raw(&self, file_name: impl AsRef<str>) -> PathBuf {
364 PathBuf(self.0.with_file_name(file_name))
365 }
366
367 pub fn with_extension(&self, extension: impl AsRef<str>) -> PathBuf {
370 let (Some(base), Some(spec)) = self.split_stream() else {
371 return PathBuf(self.0.with_extension(extension));
372 };
373 let base = Path::new(base, self.kind()).0.with_extension(extension);
374 self.with_file_name_raw(stream::join_suffix(base.as_str(), Some(spec)))
375 }
376
377 pub fn normalize(&self) -> PathBuf {
383 let has_root = self.has_root();
384 let mut components = Vec::new();
385
386 for component in self.components() {
387 if component.is_current() {
388 continue;
389 }
390 if component.is_parent() {
391 if components.last().is_some_and(Component::is_normal) {
392 components.pop();
393 } else if !has_root {
394 components.push(component);
395 }
396 } else {
397 components.push(component);
398 }
399 }
400
401 let mut normalized = PathBuf::empty(self.kind());
402 for component in components {
403 normalized.push(component.as_str());
404 }
405 normalized
406 }
407}
408
409impl PathBuf {
410 pub fn new(path: impl AsRef<str>, kind: Kind) -> Self {
412 match kind {
413 Kind::Unix => Self::from_unix(path),
414 Kind::Windows => Self::from_windows(path),
415 }
416 }
417
418 pub fn from_unix(path: impl AsRef<str>) -> Self {
420 Self(Utf8TypedPathBuf::from_unix(path))
421 }
422
423 pub fn from_windows(path: impl AsRef<str>) -> Self {
425 Self(Utf8TypedPathBuf::from_windows(path))
426 }
427
428 pub fn empty(kind: Kind) -> Self {
430 Self::new("", kind)
431 }
432
433 pub fn from_native(path: std::path::PathBuf) -> Result<Self> {
439 let path = path
440 .into_os_string()
441 .into_string()
442 .map_err(|_| Error::new(ErrorKind::InvalidData, "path is not valid UTF-8"))?;
443 Ok(Self::new(path, Kind::native()))
444 }
445
446 pub fn to_path(&self) -> Path<'_> {
448 Path(self.0.to_path())
449 }
450
451 pub fn push(&mut self, path: impl AsRef<str>) {
453 self.0.push(path);
454 }
455
456 pub fn pop(&mut self) -> bool {
458 self.0.pop()
459 }
460
461 pub fn set_file_name(&mut self, file_name: impl AsRef<str>) {
463 let next = self.to_path().with_file_name(file_name);
464 *self = next;
465 }
466
467 pub fn set_extension(&mut self, extension: impl AsRef<str>) -> bool {
471 if self.to_path().split_stream().1.is_none() {
472 return self.0.set_extension(extension);
473 }
474 let next = self.to_path().with_extension(extension);
475 *self = next;
476 true
477 }
478
479 pub fn set_stream(&mut self, spec: Option<StreamSpec<'_>>) {
483 let next = self.to_path().with_stream(spec);
484 *self = next;
485 }
486}
487
488impl PathBuf {
490 pub fn kind(&self) -> Kind {
492 self.to_path().kind()
493 }
494
495 pub fn as_str(&self) -> &str {
497 self.0.as_str()
498 }
499
500 pub fn to_native(&self) -> Result<std::path::PathBuf> {
506 self.to_path().to_native()
507 }
508
509 pub fn to_kind(&self, kind: Kind) -> Result<Self> {
515 self.to_path().to_kind(kind)
516 }
517
518 pub fn is_absolute(&self) -> bool {
520 self.0.is_absolute()
521 }
522
523 pub fn is_relative(&self) -> bool {
525 self.0.is_relative()
526 }
527
528 pub fn has_root(&self) -> bool {
530 self.0.has_root()
531 }
532
533 pub fn starts_with(&self, base: impl AsRef<str>) -> bool {
535 self.0.starts_with(base)
536 }
537
538 pub fn ends_with(&self, child: impl AsRef<str>) -> bool {
540 self.0.ends_with(child)
541 }
542
543 pub fn strip_prefix(&self, base: impl AsRef<str>) -> Result<Path<'_>> {
549 self.to_path().strip_prefix(base)
550 }
551
552 pub fn parent(&self) -> Option<Path<'_>> {
554 self.0.parent().map(Path)
555 }
556
557 pub fn file_name(&self) -> Option<&str> {
560 self.to_path().file_name()
561 }
562
563 pub fn file_name_raw(&self) -> Option<&str> {
566 self.0.file_name()
567 }
568
569 pub fn file_stem(&self) -> Option<&str> {
572 self.to_path().file_stem()
573 }
574
575 pub fn extension(&self) -> Option<&str> {
578 self.to_path().extension()
579 }
580
581 pub fn stream(&self) -> Result<Option<StreamSpec<'_>>> {
587 self.to_path().stream()
588 }
589
590 pub fn without_stream(&self) -> Self {
592 self.to_path().without_stream()
593 }
594
595 pub fn components(&self) -> Components<'_> {
597 Components(self.0.components())
598 }
599
600 pub fn join(&self, path: impl AsRef<str>) -> Self {
602 self.to_path().join(path)
603 }
604
605 pub fn with_file_name(&self, file_name: impl AsRef<str>) -> Self {
608 self.to_path().with_file_name(file_name)
609 }
610
611 pub fn with_extension(&self, extension: impl AsRef<str>) -> Self {
614 self.to_path().with_extension(extension)
615 }
616
617 pub fn normalize(&self) -> Self {
620 self.to_path().normalize()
621 }
622
623 pub fn windows_prefix(&self) -> Option<WindowsPrefix<'_>> {
625 self.to_path().windows_prefix()
626 }
627}
628
629impl AsRef<str> for Path<'_> {
630 fn as_ref(&self) -> &str {
631 self.as_str()
632 }
633}
634
635impl AsRef<str> for PathBuf {
636 fn as_ref(&self) -> &str {
637 self.as_str()
638 }
639}
640
641impl std::fmt::Display for Path<'_> {
642 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
643 f.write_str(self.as_str())
644 }
645}
646
647impl std::fmt::Display for PathBuf {
648 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
649 f.write_str(self.as_str())
650 }
651}
652
653impl<'a> From<Path<'a>> for PathBuf {
654 fn from(path: Path<'a>) -> Self {
655 path.to_path_buf()
656 }
657}
658
659impl<'a> From<&'a PathBuf> for Path<'a> {
660 fn from(path: &'a PathBuf) -> Self {
661 path.to_path()
662 }
663}
664
665impl TryFrom<std::path::PathBuf> for PathBuf {
666 type Error = Error;
667
668 fn try_from(path: std::path::PathBuf) -> Result<Self> {
669 Self::from_native(path)
670 }
671}
672
673impl TryFrom<PathBuf> for std::path::PathBuf {
674 type Error = Error;
675
676 fn try_from(path: PathBuf) -> Result<Self> {
677 path.to_native()
678 }
679}
680
681#[derive(Serialize, Deserialize)]
686#[serde(rename = "PathBuf")]
687struct Wire {
688 kind: Kind,
689 path: String,
690}
691
692impl Serialize for PathBuf {
693 fn serialize<S: serde::Serializer>(
694 &self,
695 serializer: S,
696 ) -> std::result::Result<S::Ok, S::Error> {
697 Wire {
698 kind: self.kind(),
699 path: self.as_str().to_owned(),
700 }
701 .serialize(serializer)
702 }
703}
704
705impl<'de> Deserialize<'de> for PathBuf {
706 fn deserialize<D: serde::Deserializer<'de>>(
707 deserializer: D,
708 ) -> std::result::Result<Self, D::Error> {
709 let wire = Wire::deserialize(deserializer)?;
710 Ok(Self::new(wire.path, wire.kind))
711 }
712}
713
714#[cfg(test)]
715mod tests {
716 use super::{Kind, Path, PathBuf};
717
718 #[test]
719 fn round_trip_preserves_unix_kind_and_literal_form() {
720 let path = PathBuf::from_unix(r"foo\bar/baz");
721 let bytes = postcard::to_stdvec(&path).unwrap();
722 let decoded: PathBuf = postcard::from_bytes(&bytes).unwrap();
723 assert_eq!(decoded.kind(), Kind::Unix);
724 assert_eq!(decoded.as_str(), r"foo\bar/baz");
725 }
726
727 #[test]
728 fn round_trip_preserves_windows_kind_and_literal_form() {
729 let path = PathBuf::from_windows(r"C:\foo/bar");
730 let bytes = postcard::to_stdvec(&path).unwrap();
731 let decoded: PathBuf = postcard::from_bytes(&bytes).unwrap();
732 assert_eq!(decoded.kind(), Kind::Windows);
733 assert_eq!(decoded.as_str(), r"C:\foo/bar");
734 }
735
736 #[test]
737 fn native_conversion_rejects_the_other_path_kind() {
738 let path = if cfg!(windows) {
739 PathBuf::from_unix("foo")
740 } else {
741 PathBuf::from_windows("foo")
742 };
743 assert!(path.to_native().is_err());
744 }
745
746 #[test]
747 fn to_kind_rejects_rooted_and_stream_bearing_paths() {
748 assert!(Path::windows(r"C:\foo").to_kind(Kind::Unix).is_err());
749 assert!(Path::windows(r"\foo").to_kind(Kind::Unix).is_err());
750 assert!(Path::windows("file.txt:zone").to_kind(Kind::Unix).is_err());
751 assert!(Path::unix("/foo").to_kind(Kind::Windows).is_err());
752
753 let converted = Path::windows(r"foo\bar").to_kind(Kind::Unix).unwrap();
754 assert_eq!(converted.kind(), Kind::Unix);
755 assert_eq!(converted.as_str(), "foo/bar");
756 }
757
758 #[test]
759 fn to_kind_is_identity_for_the_same_kind() {
760 let path = Path::windows(r"C:\foo");
761 assert_eq!(path.to_kind(Kind::Windows).unwrap().as_str(), r"C:\foo");
762 }
763
764 #[test]
765 fn normalize_resolves_dot_and_dotdot() {
766 assert_eq!(Path::unix("a/./b/../c").normalize().as_str(), "a/c");
767 assert_eq!(Path::unix("../a/../b").normalize().as_str(), "../b");
768 assert_eq!(Path::unix("/../a").normalize().as_str(), "/a");
769 }
770}