Skip to main content

dolang_vfs/path/
mod.rs

1//! Target-syntax paths, target-path conversion, and well-known locations.
2//!
3//! [`Path`] and [`PathBuf`] carry the path syntax ([`Kind`]) they were written
4//! in, so a path built for a Windows target keeps Windows semantics even when it
5//! is manipulated on a Unix host. They are the only path types that appear in
6//! this crate's public API.
7//!
8//! Knowing the syntax is also what lets the component accessors handle a
9//! Windows alternate data stream suffix (`file.txt:zone:$DATA`) as the two
10//! things it is — a file name and a [`StreamSpec`] — rather than as one opaque
11//! component. On a Unix path `:` is an ordinary filename character, so the
12//! stream accessors are all no-ops there.
13
14use 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/// A standard location resolved by a VFS target.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
27#[non_exhaustive]
28pub enum WellKnownPath {
29    /// User's home directory.
30    HomeDir,
31    /// Per-user cache directory.
32    CacheDir,
33    /// Directory for temporary files.
34    TempDir,
35    /// Per-user roaming application data directory.
36    DataDir,
37    /// Per-user local application data directory.
38    DataLocalDir,
39    /// Per-user state directory, where supported.
40    StateDir,
41}
42
43/// Path syntax used by a target.
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
45pub enum Kind {
46    /// Unix syntax: `/` separators, no drive letters or prefixes.
47    Unix,
48    /// Windows syntax: `\` and `/` separators, drive letters, UNC and verbatim
49    /// prefixes, and alternate data stream specifiers.
50    Windows,
51}
52
53impl Kind {
54    /// Returns the native host's path syntax.
55    pub const fn native() -> Self {
56        if cfg!(windows) {
57            Self::Windows
58        } else {
59            Self::Unix
60        }
61    }
62}
63
64/// A borrowed path in a target's syntax.
65#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
66pub struct Path<'a>(pub(crate) Utf8TypedPath<'a>);
67
68/// Applies the same expression to whichever concrete path a [`Path`] holds.
69///
70/// Going through the concrete type is what ties the result to the path's own
71/// `'a` rather than to the borrow of `self`, which is why these methods can
72/// hand out `&'a str` at all.
73macro_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/// An owned path in a target's syntax.
83#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
84pub struct PathBuf(pub(crate) Utf8TypedPathBuf);
85
86impl<'a> Path<'a> {
87    /// Creates a path with the given syntax.
88    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    /// Creates a path with Unix syntax.
96    pub fn unix(path: &'a (impl AsRef<str> + ?Sized)) -> Self {
97        Self(Utf8TypedPath::Unix(Utf8UnixPath::new(path.as_ref())))
98    }
99
100    /// Creates a path with Windows syntax.
101    pub fn windows(path: &'a (impl AsRef<str> + ?Sized)) -> Self {
102        Self(Utf8TypedPath::Windows(Utf8WindowsPath::new(path.as_ref())))
103    }
104
105    /// Returns this path's syntax.
106    pub fn kind(&self) -> Kind {
107        match self.0 {
108            Utf8TypedPath::Unix(_) => Kind::Unix,
109            Utf8TypedPath::Windows(_) => Kind::Windows,
110        }
111    }
112
113    /// Returns the path text.
114    pub fn as_str(&self) -> &'a str {
115        project!(self, |path| path.as_str())
116    }
117
118    /// Converts this path into an owned path.
119    pub fn to_path_buf(&self) -> PathBuf {
120        PathBuf(self.0.to_path_buf())
121    }
122
123    /// Converts this path into a native host path.
124    ///
125    /// # Errors
126    ///
127    /// Fails if this path's syntax is not the host's syntax.
128    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    /// Converts this path into the given syntax.
139    ///
140    /// Returns the path unchanged when it already uses `kind`.
141    ///
142    /// # Errors
143    ///
144    /// Only relative, unrooted paths carrying no alternate data stream
145    /// specifier can be converted between syntaxes.
146    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    /// Returns whether the path is absolute.
177    pub fn is_absolute(&self) -> bool {
178        self.0.is_absolute()
179    }
180
181    /// Returns whether the path is relative.
182    pub fn is_relative(&self) -> bool {
183        self.0.is_relative()
184    }
185
186    /// Returns whether the path has a root component.
187    pub fn has_root(&self) -> bool {
188        self.0.has_root()
189    }
190
191    /// Returns whether the path begins with `base`.
192    pub fn starts_with(&self, base: impl AsRef<str>) -> bool {
193        self.0.starts_with(base)
194    }
195
196    /// Returns whether the path ends with `child`.
197    pub fn ends_with(&self, child: impl AsRef<str>) -> bool {
198        self.0.ends_with(child)
199    }
200
201    /// Returns the path with `base` removed from its front.
202    ///
203    /// # Errors
204    ///
205    /// Fails if the path does not begin with `base`.
206    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    /// Returns the path without its final component.
225    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    /// Returns the final component, excluding any alternate data stream
234    /// suffix.
235    pub fn file_name(&self) -> Option<&'a str> {
236        self.split_stream().0
237    }
238
239    /// Returns the final component exactly as it is spelled, including any
240    /// alternate data stream suffix.
241    pub fn file_name_raw(&self) -> Option<&'a str> {
242        project!(self, |path| path.file_name())
243    }
244
245    /// Returns the final component without its extension or its alternate data
246    /// stream suffix.
247    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    /// Returns the extension of the final component, ignoring any alternate
255    /// data stream suffix.
256    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    /// Splits the final component into its base name and alternate data stream
272    /// specifier.
273    ///
274    /// A malformed suffix reads here as no stream at all, which is what keeps
275    /// the component accessors infallible; [`Path::stream`] is the accessor
276    /// that reports the difference.
277    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    /// Returns the alternate data stream specified by the final component.
291    ///
292    /// Always `None` for a Unix path.
293    ///
294    /// # Errors
295    ///
296    /// Fails if the final component carries a suffix that does not follow the
297    /// `name:stream[:$TYPE]` grammar.
298    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    /// Returns this path with its alternate data stream specifier replaced.
309    ///
310    /// Has no effect on a Unix path.
311    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    /// Returns this path with any alternate data stream specifier removed.
322    pub fn without_stream(&self) -> PathBuf {
323        self.with_stream(None)
324    }
325
326    /// Returns an iterator over the path's components.
327    pub fn components(&self) -> Components<'a> {
328        Components(self.0.components())
329    }
330
331    /// Returns the Windows prefix, if this is a prefixed Windows path.
332    pub fn windows_prefix(&self) -> Option<WindowsPrefix<'a>> {
333        match self.0 {
334            // Taking the prefix off the first component, rather than off the
335            // component iterator, is what keeps the borrow tied to `'a`.
336            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    /// Returns this path with `path` appended.
346    pub fn join(&self, path: impl AsRef<str>) -> PathBuf {
347        PathBuf(self.0.join(path))
348    }
349
350    /// Returns this path with its final component replaced, keeping any
351    /// alternate data stream suffix.
352    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    /// Returns this path with its final component replaced verbatim, dropping
362    /// any alternate data stream suffix along with the name it was attached to.
363    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    /// Returns this path with the extension of its final component replaced,
368    /// keeping any alternate data stream suffix.
369    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    /// Returns this path with `.` components removed and `..` components
378    /// resolved lexically.
379    ///
380    /// This is a purely textual operation: no symlink is resolved and the
381    /// target is never consulted.
382    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    /// Creates a path with the given syntax.
411    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    /// Creates a path with Unix syntax.
419    pub fn from_unix(path: impl AsRef<str>) -> Self {
420        Self(Utf8TypedPathBuf::from_unix(path))
421    }
422
423    /// Creates a path with Windows syntax.
424    pub fn from_windows(path: impl AsRef<str>) -> Self {
425        Self(Utf8TypedPathBuf::from_windows(path))
426    }
427
428    /// Creates an empty path with the given syntax.
429    pub fn empty(kind: Kind) -> Self {
430        Self::new("", kind)
431    }
432
433    /// Creates a path from a native host path.
434    ///
435    /// # Errors
436    ///
437    /// Fails if the path is not valid UTF-8.
438    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    /// Borrows this path.
447    pub fn to_path(&self) -> Path<'_> {
448        Path(self.0.to_path())
449    }
450
451    /// Appends `path`.
452    pub fn push(&mut self, path: impl AsRef<str>) {
453        self.0.push(path);
454    }
455
456    /// Removes the final component, returning whether one was removed.
457    pub fn pop(&mut self) -> bool {
458        self.0.pop()
459    }
460
461    /// Replaces the final component, keeping any alternate data stream suffix.
462    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    /// Replaces the extension of the final component, keeping any alternate
468    /// data stream suffix, and returns whether the path had a final component
469    /// to modify.
470    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    /// Replaces the alternate data stream specifier of the final component.
480    ///
481    /// Has no effect on a Unix path.
482    pub fn set_stream(&mut self, spec: Option<StreamSpec<'_>>) {
483        let next = self.to_path().with_stream(spec);
484        *self = next;
485    }
486}
487
488/// Methods shared with [`Path`], forwarded for convenience.
489impl PathBuf {
490    /// Returns this path's syntax.
491    pub fn kind(&self) -> Kind {
492        self.to_path().kind()
493    }
494
495    /// Returns the path text.
496    pub fn as_str(&self) -> &str {
497        self.0.as_str()
498    }
499
500    /// Converts this path into a native host path.
501    ///
502    /// # Errors
503    ///
504    /// Fails if this path's syntax is not the host's syntax.
505    pub fn to_native(&self) -> Result<std::path::PathBuf> {
506        self.to_path().to_native()
507    }
508
509    /// Converts this path into the given syntax.
510    ///
511    /// # Errors
512    ///
513    /// See [`Path::to_kind`].
514    pub fn to_kind(&self, kind: Kind) -> Result<Self> {
515        self.to_path().to_kind(kind)
516    }
517
518    /// Returns whether the path is absolute.
519    pub fn is_absolute(&self) -> bool {
520        self.0.is_absolute()
521    }
522
523    /// Returns whether the path is relative.
524    pub fn is_relative(&self) -> bool {
525        self.0.is_relative()
526    }
527
528    /// Returns whether the path has a root component.
529    pub fn has_root(&self) -> bool {
530        self.0.has_root()
531    }
532
533    /// Returns whether the path begins with `base`.
534    pub fn starts_with(&self, base: impl AsRef<str>) -> bool {
535        self.0.starts_with(base)
536    }
537
538    /// Returns whether the path ends with `child`.
539    pub fn ends_with(&self, child: impl AsRef<str>) -> bool {
540        self.0.ends_with(child)
541    }
542
543    /// Returns the path with `base` removed from its front.
544    ///
545    /// # Errors
546    ///
547    /// Fails if the path does not begin with `base`.
548    pub fn strip_prefix(&self, base: impl AsRef<str>) -> Result<Path<'_>> {
549        self.to_path().strip_prefix(base)
550    }
551
552    /// Returns the path without its final component.
553    pub fn parent(&self) -> Option<Path<'_>> {
554        self.0.parent().map(Path)
555    }
556
557    /// Returns the final component, excluding any alternate data stream
558    /// suffix.
559    pub fn file_name(&self) -> Option<&str> {
560        self.to_path().file_name()
561    }
562
563    /// Returns the final component exactly as it is spelled, including any
564    /// alternate data stream suffix.
565    pub fn file_name_raw(&self) -> Option<&str> {
566        self.0.file_name()
567    }
568
569    /// Returns the final component without its extension or its alternate data
570    /// stream suffix.
571    pub fn file_stem(&self) -> Option<&str> {
572        self.to_path().file_stem()
573    }
574
575    /// Returns the extension of the final component, ignoring any alternate
576    /// data stream suffix.
577    pub fn extension(&self) -> Option<&str> {
578        self.to_path().extension()
579    }
580
581    /// Returns the alternate data stream specified by the final component.
582    ///
583    /// # Errors
584    ///
585    /// See [`Path::stream`].
586    pub fn stream(&self) -> Result<Option<StreamSpec<'_>>> {
587        self.to_path().stream()
588    }
589
590    /// Returns this path with any alternate data stream specifier removed.
591    pub fn without_stream(&self) -> Self {
592        self.to_path().without_stream()
593    }
594
595    /// Returns an iterator over the path's components.
596    pub fn components(&self) -> Components<'_> {
597        Components(self.0.components())
598    }
599
600    /// Returns this path with `path` appended.
601    pub fn join(&self, path: impl AsRef<str>) -> Self {
602        self.to_path().join(path)
603    }
604
605    /// Returns this path with its final component replaced, keeping any
606    /// alternate data stream suffix.
607    pub fn with_file_name(&self, file_name: impl AsRef<str>) -> Self {
608        self.to_path().with_file_name(file_name)
609    }
610
611    /// Returns this path with the extension of its final component replaced,
612    /// keeping any alternate data stream suffix.
613    pub fn with_extension(&self, extension: impl AsRef<str>) -> Self {
614        self.to_path().with_extension(extension)
615    }
616
617    /// Returns this path with `.` components removed and `..` components
618    /// resolved lexically.
619    pub fn normalize(&self) -> Self {
620        self.to_path().normalize()
621    }
622
623    /// Returns the Windows prefix, if this is a prefixed Windows path.
624    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/// Wire representation: the syntax tag plus the literal path text.
682///
683/// Serializing through this shape keeps a path's syntax and its exact spelling
684/// intact across targets, which a plain string cannot do.
685#[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}