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}
36
37/// Path syntax used by a target.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
39pub enum Kind {
40    /// Unix syntax: `/` separators, no drive letters or prefixes.
41    Unix,
42    /// Windows syntax: `\` and `/` separators, drive letters, UNC and verbatim
43    /// prefixes, and alternate data stream specifiers.
44    Windows,
45}
46
47impl Kind {
48    /// Returns the native host's path syntax.
49    pub const fn native() -> Self {
50        if cfg!(windows) {
51            Self::Windows
52        } else {
53            Self::Unix
54        }
55    }
56}
57
58/// A borrowed path in a target's syntax.
59#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
60pub struct Path<'a>(pub(crate) Utf8TypedPath<'a>);
61
62/// Applies the same expression to whichever concrete path a [`Path`] holds.
63///
64/// Going through the concrete type is what ties the result to the path's own
65/// `'a` rather than to the borrow of `self`, which is why these methods can
66/// hand out `&'a str` at all.
67macro_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/// An owned path in a target's syntax.
77#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
78pub struct PathBuf(pub(crate) Utf8TypedPathBuf);
79
80impl<'a> Path<'a> {
81    /// Creates a path with the given syntax.
82    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    /// Creates a path with Unix syntax.
90    pub fn unix(path: &'a (impl AsRef<str> + ?Sized)) -> Self {
91        Self(Utf8TypedPath::Unix(Utf8UnixPath::new(path.as_ref())))
92    }
93
94    /// Creates a path with Windows syntax.
95    pub fn windows(path: &'a (impl AsRef<str> + ?Sized)) -> Self {
96        Self(Utf8TypedPath::Windows(Utf8WindowsPath::new(path.as_ref())))
97    }
98
99    /// Returns this path's syntax.
100    pub fn kind(&self) -> Kind {
101        match self.0 {
102            Utf8TypedPath::Unix(_) => Kind::Unix,
103            Utf8TypedPath::Windows(_) => Kind::Windows,
104        }
105    }
106
107    /// Returns the path text.
108    pub fn as_str(&self) -> &'a str {
109        project!(self, |path| path.as_str())
110    }
111
112    /// Converts this path into an owned path.
113    pub fn to_path_buf(&self) -> PathBuf {
114        PathBuf(self.0.to_path_buf())
115    }
116
117    /// Converts this path into a native host path.
118    ///
119    /// # Errors
120    ///
121    /// Fails if this path's syntax is not the host's syntax.
122    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    /// Converts this path into the given syntax.
133    ///
134    /// Returns the path unchanged when it already uses `kind`.
135    ///
136    /// # Errors
137    ///
138    /// Only relative, unrooted paths carrying no alternate data stream
139    /// specifier can be converted between syntaxes.
140    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    /// Returns whether the path is absolute.
171    pub fn is_absolute(&self) -> bool {
172        self.0.is_absolute()
173    }
174
175    /// Returns whether the path is relative.
176    pub fn is_relative(&self) -> bool {
177        self.0.is_relative()
178    }
179
180    /// Returns whether the path has a root component.
181    pub fn has_root(&self) -> bool {
182        self.0.has_root()
183    }
184
185    /// Returns whether the path begins with `base`.
186    pub fn starts_with(&self, base: impl AsRef<str>) -> bool {
187        self.0.starts_with(base)
188    }
189
190    /// Returns whether the path ends with `child`.
191    pub fn ends_with(&self, child: impl AsRef<str>) -> bool {
192        self.0.ends_with(child)
193    }
194
195    /// Returns the path with `base` removed from its front.
196    ///
197    /// # Errors
198    ///
199    /// Fails if the path does not begin with `base`.
200    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    /// Returns the path without its final component.
219    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    /// Returns the final component, excluding any alternate data stream
228    /// suffix.
229    pub fn file_name(&self) -> Option<&'a str> {
230        self.split_stream().0
231    }
232
233    /// Returns the final component exactly as it is spelled, including any
234    /// alternate data stream suffix.
235    pub fn file_name_raw(&self) -> Option<&'a str> {
236        project!(self, |path| path.file_name())
237    }
238
239    /// Returns the final component without its extension or its alternate data
240    /// stream suffix.
241    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    /// Returns the extension of the final component, ignoring any alternate
249    /// data stream suffix.
250    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    /// Splits the final component into its base name and alternate data stream
266    /// specifier.
267    ///
268    /// A malformed suffix reads here as no stream at all, which is what keeps
269    /// the component accessors infallible; [`Path::stream`] is the accessor
270    /// that reports the difference.
271    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    /// Returns the alternate data stream specified by the final component.
285    ///
286    /// Always `None` for a Unix path.
287    ///
288    /// # Errors
289    ///
290    /// Fails if the final component carries a suffix that does not follow the
291    /// `name:stream[:$TYPE]` grammar.
292    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    /// Returns this path with its alternate data stream specifier replaced.
303    ///
304    /// Has no effect on a Unix path.
305    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    /// Returns this path with any alternate data stream specifier removed.
316    pub fn without_stream(&self) -> PathBuf {
317        self.with_stream(None)
318    }
319
320    /// Returns an iterator over the path's components.
321    pub fn components(&self) -> Components<'a> {
322        Components(self.0.components())
323    }
324
325    /// Returns the Windows prefix, if this is a prefixed Windows path.
326    pub fn windows_prefix(&self) -> Option<WindowsPrefix<'a>> {
327        match self.0 {
328            // Taking the prefix off the first component, rather than off the
329            // component iterator, is what keeps the borrow tied to `'a`.
330            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    /// Returns this path with `path` appended.
340    pub fn join(&self, path: impl AsRef<str>) -> PathBuf {
341        PathBuf(self.0.join(path))
342    }
343
344    /// Returns this path with its final component replaced, keeping any
345    /// alternate data stream suffix.
346    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    /// Returns this path with its final component replaced verbatim, dropping
356    /// any alternate data stream suffix along with the name it was attached to.
357    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    /// Returns this path with the extension of its final component replaced,
362    /// keeping any alternate data stream suffix.
363    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    /// Returns this path with `.` components removed and `..` components
372    /// resolved lexically.
373    ///
374    /// This is a purely textual operation: no symlink is resolved and the
375    /// target is never consulted.
376    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    /// Creates a path with the given syntax.
405    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    /// Creates a path with Unix syntax.
413    pub fn from_unix(path: impl AsRef<str>) -> Self {
414        Self(Utf8TypedPathBuf::from_unix(path))
415    }
416
417    /// Creates a path with Windows syntax.
418    pub fn from_windows(path: impl AsRef<str>) -> Self {
419        Self(Utf8TypedPathBuf::from_windows(path))
420    }
421
422    /// Creates an empty path with the given syntax.
423    pub fn empty(kind: Kind) -> Self {
424        Self::new("", kind)
425    }
426
427    /// Creates a path from a native host path.
428    ///
429    /// # Errors
430    ///
431    /// Fails if the path is not valid UTF-8.
432    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    /// Borrows this path.
441    pub fn to_path(&self) -> Path<'_> {
442        Path(self.0.to_path())
443    }
444
445    /// Appends `path`.
446    pub fn push(&mut self, path: impl AsRef<str>) {
447        self.0.push(path);
448    }
449
450    /// Removes the final component, returning whether one was removed.
451    pub fn pop(&mut self) -> bool {
452        self.0.pop()
453    }
454
455    /// Replaces the final component, keeping any alternate data stream suffix.
456    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    /// Replaces the extension of the final component, keeping any alternate
462    /// data stream suffix, and returns whether the path had a final component
463    /// to modify.
464    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    /// Replaces the alternate data stream specifier of the final component.
474    ///
475    /// Has no effect on a Unix path.
476    pub fn set_stream(&mut self, spec: Option<StreamSpec<'_>>) {
477        let next = self.to_path().with_stream(spec);
478        *self = next;
479    }
480}
481
482/// Methods shared with [`Path`], forwarded for convenience.
483impl PathBuf {
484    /// Returns this path's syntax.
485    pub fn kind(&self) -> Kind {
486        self.to_path().kind()
487    }
488
489    /// Returns the path text.
490    pub fn as_str(&self) -> &str {
491        self.0.as_str()
492    }
493
494    /// Converts this path into a native host path.
495    ///
496    /// # Errors
497    ///
498    /// Fails if this path's syntax is not the host's syntax.
499    pub fn to_native(&self) -> Result<std::path::PathBuf> {
500        self.to_path().to_native()
501    }
502
503    /// Converts this path into the given syntax.
504    ///
505    /// # Errors
506    ///
507    /// See [`Path::to_kind`].
508    pub fn to_kind(&self, kind: Kind) -> Result<Self> {
509        self.to_path().to_kind(kind)
510    }
511
512    /// Returns whether the path is absolute.
513    pub fn is_absolute(&self) -> bool {
514        self.0.is_absolute()
515    }
516
517    /// Returns whether the path is relative.
518    pub fn is_relative(&self) -> bool {
519        self.0.is_relative()
520    }
521
522    /// Returns whether the path has a root component.
523    pub fn has_root(&self) -> bool {
524        self.0.has_root()
525    }
526
527    /// Returns whether the path begins with `base`.
528    pub fn starts_with(&self, base: impl AsRef<str>) -> bool {
529        self.0.starts_with(base)
530    }
531
532    /// Returns whether the path ends with `child`.
533    pub fn ends_with(&self, child: impl AsRef<str>) -> bool {
534        self.0.ends_with(child)
535    }
536
537    /// Returns the path with `base` removed from its front.
538    ///
539    /// # Errors
540    ///
541    /// Fails if the path does not begin with `base`.
542    pub fn strip_prefix(&self, base: impl AsRef<str>) -> Result<Path<'_>> {
543        self.to_path().strip_prefix(base)
544    }
545
546    /// Returns the path without its final component.
547    pub fn parent(&self) -> Option<Path<'_>> {
548        self.0.parent().map(Path)
549    }
550
551    /// Returns the final component, excluding any alternate data stream
552    /// suffix.
553    pub fn file_name(&self) -> Option<&str> {
554        self.to_path().file_name()
555    }
556
557    /// Returns the final component exactly as it is spelled, including any
558    /// alternate data stream suffix.
559    pub fn file_name_raw(&self) -> Option<&str> {
560        self.0.file_name()
561    }
562
563    /// Returns the final component without its extension or its alternate data
564    /// stream suffix.
565    pub fn file_stem(&self) -> Option<&str> {
566        self.to_path().file_stem()
567    }
568
569    /// Returns the extension of the final component, ignoring any alternate
570    /// data stream suffix.
571    pub fn extension(&self) -> Option<&str> {
572        self.to_path().extension()
573    }
574
575    /// Returns the alternate data stream specified by the final component.
576    ///
577    /// # Errors
578    ///
579    /// See [`Path::stream`].
580    pub fn stream(&self) -> Result<Option<StreamSpec<'_>>> {
581        self.to_path().stream()
582    }
583
584    /// Returns this path with any alternate data stream specifier removed.
585    pub fn without_stream(&self) -> Self {
586        self.to_path().without_stream()
587    }
588
589    /// Returns an iterator over the path's components.
590    pub fn components(&self) -> Components<'_> {
591        Components(self.0.components())
592    }
593
594    /// Returns this path with `path` appended.
595    pub fn join(&self, path: impl AsRef<str>) -> Self {
596        self.to_path().join(path)
597    }
598
599    /// Returns this path with its final component replaced, keeping any
600    /// alternate data stream suffix.
601    pub fn with_file_name(&self, file_name: impl AsRef<str>) -> Self {
602        self.to_path().with_file_name(file_name)
603    }
604
605    /// Returns this path with the extension of its final component replaced,
606    /// keeping any alternate data stream suffix.
607    pub fn with_extension(&self, extension: impl AsRef<str>) -> Self {
608        self.to_path().with_extension(extension)
609    }
610
611    /// Returns this path with `.` components removed and `..` components
612    /// resolved lexically.
613    pub fn normalize(&self) -> Self {
614        self.to_path().normalize()
615    }
616
617    /// Returns the Windows prefix, if this is a prefixed Windows path.
618    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/// Wire representation: the syntax tag plus the literal path text.
676///
677/// Serializing through this shape keeps a path's syntax and its exact spelling
678/// intact across targets, which a plain string cannot do.
679#[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}