utf8path/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use std::borrow::{Borrow, Cow};
4use std::path::PathBuf;
5
6/////////////////////////////////////////////// Path ///////////////////////////////////////////////
7
8/// Path provides a copy-on-write-style path that is built around UTF8 strings.
9#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
10pub struct Path<'a> {
11    path: Cow<'a, str>,
12}
13
14impl<'a> Path<'a> {
15    /// Create a new path that borrows the provided string.
16    pub const fn new(s: &'a str) -> Self {
17        Self {
18            path: Cow::Borrowed(s),
19        }
20    }
21
22    /// Convert the path into an owned path.
23    pub fn into_owned(self) -> Path<'static> {
24        Path {
25            path: Cow::Owned(self.path.into_owned()),
26        }
27    }
28
29    /// Convert the path into a std::path::PathBuf.
30    pub fn into_std(&self) -> &std::path::Path {
31        std::path::Path::new::<str>(self.path.as_ref())
32    }
33
34    /// Convert the path to a str.
35    pub fn as_str(&self) -> &str {
36        &self.path
37    }
38
39    /// Is the path a directory?
40    pub fn is_dir(&self) -> bool {
41        std::path::Path::new(self.path.as_ref()).is_dir()
42    }
43
44    /// Compute the basename of the path.  This is guaraneed to be a non-empty path component
45    /// (falling back to "." for paths that end with "/").
46    pub fn basename(&self) -> Path<'_> {
47        self.split().1
48    }
49
50    /// Compute the dirname of the path.  This is guaranteed to be a non-empty path component
51    /// (falling back to "." or "/" for single-component paths).
52    pub fn dirname(&self) -> Path<'_> {
53        self.split().0
54    }
55
56    /// True if the path exists.
57    pub fn exists(&self) -> bool {
58        let path: &str = &self.path;
59        PathBuf::from(path).exists()
60    }
61
62    /// True if the path begins with some number of slashes, other than the POSIX-exception of //.
63    pub fn has_root(&self) -> bool {
64        self.path.starts_with('/') && !self.has_app_defined()
65    }
66
67    /// True if the path begins with //, but not ///.
68    pub fn has_app_defined(&self) -> bool {
69        self.path.starts_with("//") && (self.path.len() == 2 || &self.path[2..3] != "/")
70    }
71
72    /// True if the path is absolute.
73    pub fn is_abs(&self) -> bool {
74        self.has_root() || self.has_app_defined()
75    }
76
77    /// True if the path contains no "." components; and, is absolute and has no ".." components,
78    /// or is relative and has all ".." components at the start.
79    pub fn is_normal(&self) -> bool {
80        let start = if self.path.starts_with("//") {
81            2
82        } else if self.path.starts_with('/') {
83            1
84        } else {
85            0
86        };
87        if self.path[start..].is_empty() {
88            return start > 0;
89        }
90        let limit = if self.path[start..].ends_with('/') {
91            self.path.len() - 1
92        } else {
93            self.path.len()
94        };
95        let components: Vec<_> = self.path[start..limit].split('/').collect();
96        let mut parent_allowed = start == 0;
97        for component in components {
98            if parent_allowed {
99                if matches!(component, "." | "") {
100                    return false;
101                }
102                parent_allowed = component == "..";
103            } else if matches!(component, ".." | "." | "") {
104                return false;
105            }
106        }
107        true
108    }
109
110    /// Join to this path another path.  Follows standard path rules where if the joined-with path
111    /// is absolute, the first path is discarded.
112    pub fn join<'b, 'c>(&self, with: impl Into<Path<'b>>) -> Path<'c>
113    where
114        'a: 'c,
115        'b: 'c,
116    {
117        let with = with.into();
118        if with.is_abs() {
119            with.clone()
120        } else {
121            Path::from(format!("{}/{}", self.path, with.path))
122        }
123    }
124
125    /// Strip a prefix from the path.  The prefix and path are allowed to be non-normal and will
126    /// have "." components dropped from consideration.
127    pub fn strip_prefix<'b>(&self, prefix: impl Into<Path<'b>>) -> Option<Path> {
128        let prefix = prefix.into();
129        // NOTE(rescrv):  You might be tempted to use components() and zip() to solve and/or
130        // simplify this.  That fails for one reason:  "components()" intentionally rewrites `foo/`
131        // as `foo/.`, but this method should preserve the path that remains as much as possible,
132        // including `.` components.
133        if self.has_root() && !prefix.has_root() {
134            return None;
135        }
136        if self.has_app_defined() && !prefix.has_app_defined() {
137            return None;
138        }
139        let mut path = self.path[..].trim_start_matches('/');
140        let mut prefix = prefix.path[..].trim_start_matches('/');
141        loop {
142            if let Some(prefix_slash) = prefix.find('/') {
143                let path_slash = path.find('/')?;
144                if prefix[..prefix_slash] != path[..path_slash] {
145                    return None;
146                }
147                path = path[path_slash + 1..].trim_start_matches('/');
148                prefix = prefix[prefix_slash + 1..].trim_start_matches('/');
149            } else if prefix == path {
150                return Some(Path::new("."));
151            } else if let Some(path) = path.strip_prefix(prefix) {
152                let path = path.trim_start_matches('/');
153                if path.is_empty() {
154                    return Some(Path::new("."));
155                } else {
156                    return Some(Path::new(path));
157                }
158            } else if prefix.starts_with("./") {
159                prefix = prefix[2..].trim_start_matches('/');
160            } else if path.starts_with("./") {
161                path = path[2..].trim_start_matches('/');
162            } else if prefix.is_empty() || prefix == "." {
163                if path.is_empty() {
164                    return Some(Path::new("."));
165                } else {
166                    return Some(Path::new(path));
167                }
168            }
169        }
170    }
171
172    /// Split the path into basename and dirname components.
173    pub fn split(&self) -> (Path, Path) {
174        if let Some(index) = self.path.rfind('/') {
175            let dirname = if index == 0 {
176                Path::new("/")
177            } else if index == 1 && self.path.starts_with("//") {
178                Path::new("//")
179            } else if self.path[..index].chars().all(|c| c == '/') {
180                Path::new("/")
181            } else {
182                Path::new(self.path[..index].trim_end_matches('/'))
183            };
184            let basename = if index + 1 == self.path.len() {
185                Path::new(".")
186            } else {
187                Path::new(&self.path[index + 1..])
188            };
189            (dirname, basename)
190        } else {
191            (Path::new("."), Path::new(&self.path))
192        }
193    }
194
195    /// Return an iterator ovre the path components.  A path with a basename of "." will always end
196    /// with Component::CurDir.
197    pub fn components(&self) -> impl Iterator<Item = Component<'_>> {
198        let mut components = vec![];
199        let mut limit = self.path.len();
200        while let Some(slash) = self.path[..limit].rfind('/') {
201            if slash + 1 == limit {
202                components.push(Component::CurDir);
203            } else if &self.path[slash + 1..limit] == ".." {
204                components.push(Component::ParentDir);
205            } else if &self.path[slash + 1..limit] == "." {
206                components.push(Component::CurDir);
207            } else {
208                components.push(Component::Normal(Path::new(&self.path[slash + 1..limit])));
209            }
210            if slash == 0 {
211                components.push(Component::RootDir);
212                limit = 0;
213            } else if slash == 1 && self.path.starts_with("//") {
214                components.push(Component::AppDefined);
215                limit = 0;
216            } else if self.path[..slash].chars().all(|c| c == '/') {
217                components.push(Component::RootDir);
218                limit = 0;
219            } else {
220                limit = slash;
221                while limit > 0 && self.path[..limit].ends_with('/') {
222                    limit -= 1;
223                }
224            }
225        }
226        if limit > 0 {
227            if &self.path[..limit] == ".." {
228                components.push(Component::ParentDir);
229            } else if &self.path[..limit] == "." {
230                components.push(Component::CurDir);
231            } else {
232                components.push(Component::Normal(Path::new(&self.path[..limit])));
233            }
234        }
235        components.reverse();
236        components.into_iter()
237    }
238
239    /// Return the current working directory, if it can be fetched and converted to unicode without
240    /// error.
241    pub fn cwd() -> Option<Path<'a>> {
242        Path::try_from(std::env::current_dir().ok()?).ok()
243    }
244}
245
246impl AsRef<std::ffi::OsStr> for Path<'_> {
247    fn as_ref(&self) -> &std::ffi::OsStr {
248        let path: &std::ffi::OsStr = self.as_str().as_ref();
249        path
250    }
251}
252
253impl AsRef<std::path::Path> for Path<'_> {
254    fn as_ref(&self) -> &std::path::Path {
255        std::path::Path::new(self.as_str())
256    }
257}
258
259impl Borrow<std::path::Path> for Path<'_> {
260    fn borrow(&self) -> &std::path::Path {
261        std::path::Path::new(self.as_str())
262    }
263}
264
265impl std::fmt::Debug for Path<'_> {
266    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
267        write!(f, "{:?}", self.path)
268    }
269}
270
271impl std::fmt::Display for Path<'_> {
272    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
273        write!(f, "{}", self.path)
274    }
275}
276
277impl From<String> for Path<'_> {
278    fn from(s: String) -> Self {
279        Self {
280            path: Cow::Owned(s),
281        }
282    }
283}
284
285impl<'a> From<Path<'a>> for String {
286    fn from(path: Path<'a>) -> Self {
287        path.path.into_owned()
288    }
289}
290
291impl<'a> From<&'a String> for Path<'a> {
292    fn from(s: &'a String) -> Self {
293        Self {
294            path: Cow::Borrowed(s),
295        }
296    }
297}
298
299impl<'a> From<&'a str> for Path<'a> {
300    fn from(s: &'a str) -> Self {
301        Self {
302            path: Cow::Borrowed(s),
303        }
304    }
305}
306
307impl<'a> From<&'a Path<'a>> for &'a str {
308    fn from(path: &'a Path<'a>) -> Self {
309        &path.path
310    }
311}
312
313impl<'a> TryFrom<&'a std::path::Path> for Path<'a> {
314    type Error = std::str::Utf8Error;
315
316    fn try_from(p: &'a std::path::Path) -> Result<Self, Self::Error> {
317        Ok(Self {
318            path: Cow::Borrowed(<&str>::try_from(p.as_os_str())?),
319        })
320    }
321}
322
323impl TryFrom<std::path::PathBuf> for Path<'_> {
324    type Error = std::str::Utf8Error;
325
326    fn try_from(p: std::path::PathBuf) -> Result<Self, Self::Error> {
327        Ok(Self {
328            path: Cow::Owned(<&str>::try_from(p.as_os_str())?.to_string()),
329        })
330    }
331}
332
333impl TryFrom<std::ffi::OsString> for Path<'_> {
334    type Error = std::str::Utf8Error;
335
336    fn try_from(p: std::ffi::OsString) -> Result<Self, Self::Error> {
337        Ok(Self {
338            path: Cow::Owned(<&str>::try_from(p.as_os_str())?.to_string()),
339        })
340    }
341}
342
343impl<'a> From<Path<'a>> for std::path::PathBuf {
344    fn from(path: Path<'a>) -> Self {
345        PathBuf::from(path.path.to_string())
346    }
347}
348
349///////////////////////////////////////////// Component ////////////////////////////////////////////
350
351/// A component of a path.
352#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
353pub enum Component<'a> {
354    /// Signals the path component "/".
355    RootDir,
356    /// Signals the path component "//".
357    AppDefined,
358    /// Signals the "." path component.
359    CurDir,
360    /// Signals the ".." path component.
361    ParentDir,
362    /// Signals a component that doesn't match any of the special components.
363    Normal(Path<'a>),
364}
365
366/////////////////////////////////////////////// tests //////////////////////////////////////////////
367
368#[cfg(test)]
369mod tests {
370    use super::{Component, Path};
371
372    struct TestCase<'a> {
373        path: Path<'a>,
374        basename: Path<'a>,
375        dirname: Path<'a>,
376        is_abs: bool,
377        is_normal: bool,
378        components: &'a str,
379    }
380
381    static TEST_CASES: &[TestCase] = &[
382        TestCase {
383            path: Path::new("//"),
384            basename: Path::new("."),
385            dirname: Path::new("//"),
386            is_abs: true,
387            is_normal: true,
388            components: "AC",
389        },
390        TestCase {
391            path: Path::new("//."),
392            basename: Path::new("."),
393            dirname: Path::new("//"),
394            is_abs: true,
395            is_normal: false,
396            components: "AC",
397        },
398        TestCase {
399            path: Path::new("//.."),
400            basename: Path::new(".."),
401            dirname: Path::new("//"),
402            is_abs: true,
403            is_normal: false,
404            components: "AP",
405        },
406        TestCase {
407            path: Path::new("//foo"),
408            basename: Path::new("foo"),
409            dirname: Path::new("//"),
410            is_abs: true,
411            is_normal: true,
412            components: "AN",
413        },
414        TestCase {
415            path: Path::new("/./"),
416            basename: Path::new("."),
417            dirname: Path::new("/."),
418            is_abs: true,
419            is_normal: false,
420            components: "RCC",
421        },
422        TestCase {
423            path: Path::new("/./."),
424            basename: Path::new("."),
425            dirname: Path::new("/."),
426            is_abs: true,
427            is_normal: false,
428            components: "RCC",
429        },
430        TestCase {
431            path: Path::new("/./.."),
432            basename: Path::new(".."),
433            dirname: Path::new("/."),
434            is_abs: true,
435            is_normal: false,
436            components: "RCP",
437        },
438        TestCase {
439            path: Path::new("/./foo"),
440            basename: Path::new("foo"),
441            dirname: Path::new("/."),
442            is_abs: true,
443            is_normal: false,
444            components: "RCN",
445        },
446        TestCase {
447            path: Path::new("/../"),
448            basename: Path::new("."),
449            dirname: Path::new("/.."),
450            is_abs: true,
451            is_normal: false,
452            components: "RPC",
453        },
454        TestCase {
455            path: Path::new("/../."),
456            basename: Path::new("."),
457            dirname: Path::new("/.."),
458            is_abs: true,
459            is_normal: false,
460            components: "RPC",
461        },
462        TestCase {
463            path: Path::new("/../.."),
464            basename: Path::new(".."),
465            dirname: Path::new("/.."),
466            is_abs: true,
467            is_normal: false,
468            components: "RPP",
469        },
470        TestCase {
471            path: Path::new("/../foo"),
472            basename: Path::new("foo"),
473            dirname: Path::new("/.."),
474            is_abs: true,
475            is_normal: false,
476            components: "RPN",
477        },
478        TestCase {
479            path: Path::new("/foo/"),
480            basename: Path::new("."),
481            dirname: Path::new("/foo"),
482            is_abs: true,
483            is_normal: true,
484            components: "RNC",
485        },
486        TestCase {
487            path: Path::new("/foo/."),
488            basename: Path::new("."),
489            dirname: Path::new("/foo"),
490            is_abs: true,
491            is_normal: false,
492            components: "RNC",
493        },
494        TestCase {
495            path: Path::new("/foo/.."),
496            basename: Path::new(".."),
497            dirname: Path::new("/foo"),
498            is_abs: true,
499            is_normal: false,
500            components: "RNP",
501        },
502        TestCase {
503            path: Path::new("/foo/foo"),
504            basename: Path::new("foo"),
505            dirname: Path::new("/foo"),
506            is_abs: true,
507            is_normal: true,
508            components: "RNN",
509        },
510        TestCase {
511            path: Path::new(".//"),
512            basename: Path::new("."),
513            dirname: Path::new("."),
514            is_abs: false,
515            is_normal: false,
516            components: "CC",
517        },
518        TestCase {
519            path: Path::new(".//."),
520            basename: Path::new("."),
521            dirname: Path::new("."),
522            is_abs: false,
523            is_normal: false,
524            components: "CC",
525        },
526        TestCase {
527            path: Path::new(".//.."),
528            basename: Path::new(".."),
529            dirname: Path::new("."),
530            is_abs: false,
531            is_normal: false,
532            components: "CP",
533        },
534        TestCase {
535            path: Path::new(".//foo"),
536            basename: Path::new("foo"),
537            dirname: Path::new("."),
538            is_abs: false,
539            is_normal: false,
540            components: "CN",
541        },
542        TestCase {
543            path: Path::new("././"),
544            basename: Path::new("."),
545            dirname: Path::new("./."),
546            is_abs: false,
547            is_normal: false,
548            components: "CCC",
549        },
550        TestCase {
551            path: Path::new("././."),
552            basename: Path::new("."),
553            dirname: Path::new("./."),
554            is_abs: false,
555            is_normal: false,
556            components: "CCC",
557        },
558        TestCase {
559            path: Path::new("././.."),
560            basename: Path::new(".."),
561            dirname: Path::new("./."),
562            is_abs: false,
563            is_normal: false,
564            components: "CCP",
565        },
566        TestCase {
567            path: Path::new("././foo"),
568            basename: Path::new("foo"),
569            dirname: Path::new("./."),
570            is_abs: false,
571            is_normal: false,
572            components: "CCN",
573        },
574        TestCase {
575            path: Path::new("./../"),
576            basename: Path::new("."),
577            dirname: Path::new("./.."),
578            is_abs: false,
579            is_normal: false,
580            components: "CPC",
581        },
582        TestCase {
583            path: Path::new("./../."),
584            basename: Path::new("."),
585            dirname: Path::new("./.."),
586            is_abs: false,
587            is_normal: false,
588            components: "CPC",
589        },
590        TestCase {
591            path: Path::new("./../.."),
592            basename: Path::new(".."),
593            dirname: Path::new("./.."),
594            is_abs: false,
595            is_normal: false,
596            components: "CPP",
597        },
598        TestCase {
599            path: Path::new("./../foo"),
600            basename: Path::new("foo"),
601            dirname: Path::new("./.."),
602            is_abs: false,
603            is_normal: false,
604            components: "CPN",
605        },
606        TestCase {
607            path: Path::new("./foo/"),
608            basename: Path::new("."),
609            dirname: Path::new("./foo"),
610            is_abs: false,
611            is_normal: false,
612            components: "CNC",
613        },
614        TestCase {
615            path: Path::new("./foo/."),
616            basename: Path::new("."),
617            dirname: Path::new("./foo"),
618            is_abs: false,
619            is_normal: false,
620            components: "CNC",
621        },
622        TestCase {
623            path: Path::new("./foo/.."),
624            basename: Path::new(".."),
625            dirname: Path::new("./foo"),
626            is_abs: false,
627            is_normal: false,
628            components: "CNP",
629        },
630        TestCase {
631            path: Path::new("./foo/foo"),
632            basename: Path::new("foo"),
633            dirname: Path::new("./foo"),
634            is_abs: false,
635            is_normal: false,
636            components: "CNN",
637        },
638        TestCase {
639            path: Path::new("..//"),
640            basename: Path::new("."),
641            dirname: Path::new(".."),
642            is_abs: false,
643            is_normal: false,
644            components: "PC",
645        },
646        TestCase {
647            path: Path::new("..//."),
648            basename: Path::new("."),
649            dirname: Path::new(".."),
650            is_abs: false,
651            is_normal: false,
652            components: "PC",
653        },
654        TestCase {
655            path: Path::new("..//.."),
656            basename: Path::new(".."),
657            dirname: Path::new(".."),
658            is_abs: false,
659            is_normal: false,
660            components: "PP",
661        },
662        TestCase {
663            path: Path::new("..//foo"),
664            basename: Path::new("foo"),
665            dirname: Path::new(".."),
666            is_abs: false,
667            is_normal: false,
668            components: "PN",
669        },
670        TestCase {
671            path: Path::new(".././"),
672            basename: Path::new("."),
673            dirname: Path::new("../."),
674            is_abs: false,
675            is_normal: false,
676            components: "PCC",
677        },
678        TestCase {
679            path: Path::new(".././."),
680            basename: Path::new("."),
681            dirname: Path::new("../."),
682            is_abs: false,
683            is_normal: false,
684            components: "PCC",
685        },
686        TestCase {
687            path: Path::new(".././.."),
688            basename: Path::new(".."),
689            dirname: Path::new("../."),
690            is_abs: false,
691            is_normal: false,
692            components: "PCP",
693        },
694        TestCase {
695            path: Path::new(".././foo"),
696            basename: Path::new("foo"),
697            dirname: Path::new("../."),
698            is_abs: false,
699            is_normal: false,
700            components: "PCN",
701        },
702        TestCase {
703            path: Path::new("../../"),
704            basename: Path::new("."),
705            dirname: Path::new("../.."),
706            is_abs: false,
707            is_normal: true,
708            components: "PPC",
709        },
710        TestCase {
711            path: Path::new("../../."),
712            basename: Path::new("."),
713            dirname: Path::new("../.."),
714            is_abs: false,
715            is_normal: false,
716            components: "PPC",
717        },
718        TestCase {
719            path: Path::new("../../.."),
720            basename: Path::new(".."),
721            dirname: Path::new("../.."),
722            is_abs: false,
723            is_normal: true,
724            components: "PPP",
725        },
726        TestCase {
727            path: Path::new("../../foo"),
728            basename: Path::new("foo"),
729            dirname: Path::new("../.."),
730            is_abs: false,
731            is_normal: true,
732            components: "PPN",
733        },
734        TestCase {
735            path: Path::new("../foo/"),
736            basename: Path::new("."),
737            dirname: Path::new("../foo"),
738            is_abs: false,
739            is_normal: true,
740            components: "PNC",
741        },
742        TestCase {
743            path: Path::new("../foo/."),
744            basename: Path::new("."),
745            dirname: Path::new("../foo"),
746            is_abs: false,
747            is_normal: false,
748            components: "PNC",
749        },
750        TestCase {
751            path: Path::new("../foo/.."),
752            basename: Path::new(".."),
753            dirname: Path::new("../foo"),
754            is_abs: false,
755            is_normal: false,
756            components: "PNP",
757        },
758        TestCase {
759            path: Path::new("../foo/foo"),
760            basename: Path::new("foo"),
761            dirname: Path::new("../foo"),
762            is_abs: false,
763            is_normal: true,
764            components: "PNN",
765        },
766        TestCase {
767            path: Path::new("foo//"),
768            basename: Path::new("."),
769            dirname: Path::new("foo"),
770            is_abs: false,
771            is_normal: false,
772            components: "NC",
773        },
774        TestCase {
775            path: Path::new("foo//."),
776            basename: Path::new("."),
777            dirname: Path::new("foo"),
778            is_abs: false,
779            is_normal: false,
780            components: "NC",
781        },
782        TestCase {
783            path: Path::new("foo//.."),
784            basename: Path::new(".."),
785            dirname: Path::new("foo"),
786            is_abs: false,
787            is_normal: false,
788            components: "NP",
789        },
790        TestCase {
791            path: Path::new("foo//foo"),
792            basename: Path::new("foo"),
793            dirname: Path::new("foo"),
794            is_abs: false,
795            is_normal: false,
796            components: "NN",
797        },
798        TestCase {
799            path: Path::new("foo/./"),
800            basename: Path::new("."),
801            dirname: Path::new("foo/."),
802            is_abs: false,
803            is_normal: false,
804            components: "NCC",
805        },
806        TestCase {
807            path: Path::new("foo/./."),
808            basename: Path::new("."),
809            dirname: Path::new("foo/."),
810            is_abs: false,
811            is_normal: false,
812            components: "NCC",
813        },
814        TestCase {
815            path: Path::new("foo/./.."),
816            basename: Path::new(".."),
817            dirname: Path::new("foo/."),
818            is_abs: false,
819            is_normal: false,
820            components: "NCP",
821        },
822        TestCase {
823            path: Path::new("foo/./foo"),
824            basename: Path::new("foo"),
825            dirname: Path::new("foo/."),
826            is_abs: false,
827            is_normal: false,
828            components: "NCN",
829        },
830        TestCase {
831            path: Path::new("foo/../"),
832            basename: Path::new("."),
833            dirname: Path::new("foo/.."),
834            is_abs: false,
835            is_normal: false,
836            components: "NPC",
837        },
838        TestCase {
839            path: Path::new("foo/../."),
840            basename: Path::new("."),
841            dirname: Path::new("foo/.."),
842            is_abs: false,
843            is_normal: false,
844            components: "NPC",
845        },
846        TestCase {
847            path: Path::new("foo/../.."),
848            basename: Path::new(".."),
849            dirname: Path::new("foo/.."),
850            is_abs: false,
851            is_normal: false,
852            components: "NPP",
853        },
854        TestCase {
855            path: Path::new("foo/../foo"),
856            basename: Path::new("foo"),
857            dirname: Path::new("foo/.."),
858            is_abs: false,
859            is_normal: false,
860            components: "NPN",
861        },
862        TestCase {
863            path: Path::new("foo/foo/"),
864            basename: Path::new("."),
865            dirname: Path::new("foo/foo"),
866            is_abs: false,
867            is_normal: true,
868            components: "NNC",
869        },
870        TestCase {
871            path: Path::new("foo/foo/."),
872            basename: Path::new("."),
873            dirname: Path::new("foo/foo"),
874            is_abs: false,
875            is_normal: false,
876            components: "NNC",
877        },
878        TestCase {
879            path: Path::new("foo/foo/.."),
880            basename: Path::new(".."),
881            dirname: Path::new("foo/foo"),
882            is_abs: false,
883            is_normal: false,
884            components: "NNP",
885        },
886        TestCase {
887            path: Path::new("foo/foo/foo"),
888            basename: Path::new("foo"),
889            dirname: Path::new("foo/foo"),
890            is_abs: false,
891            is_normal: true,
892            components: "NNN",
893        },
894    ];
895
896    #[test]
897    fn basename() {
898        for tc in TEST_CASES.iter() {
899            assert_eq!(tc.basename, tc.path.basename(), "path: {:?}", tc.path);
900        }
901    }
902
903    #[test]
904    fn dirname() {
905        for tc in TEST_CASES.iter() {
906            assert_eq!(tc.dirname, tc.path.dirname(), "path: {:?}", tc.path);
907        }
908    }
909
910    #[test]
911    fn is_abs() {
912        for tc in TEST_CASES.iter() {
913            assert_eq!(tc.is_abs, tc.path.is_abs(), "path: {:?}", tc.path);
914        }
915    }
916
917    #[test]
918    fn is_normal() {
919        for tc in TEST_CASES.iter() {
920            assert_eq!(tc.is_normal, tc.path.is_normal(), "path: {:?}", tc.path);
921        }
922    }
923
924    #[test]
925    fn strip_prefix() {
926        for tc in TEST_CASES.iter() {
927            assert_eq!(
928                Some(tc.basename.clone()),
929                tc.path.strip_prefix(tc.dirname.clone()),
930                "path: {:?}",
931                tc.path
932            );
933        }
934    }
935
936    #[test]
937    fn split() {
938        for tc in TEST_CASES.iter() {
939            let (dirname, basename) = tc.path.split();
940            assert_eq!(tc.basename, basename, "path: {:?}", tc.path);
941            assert_eq!(tc.dirname, dirname, "path: {:?}", tc.path);
942        }
943    }
944
945    #[test]
946    fn components() {
947        fn component_to_char(c: Component) -> char {
948            match c {
949                Component::AppDefined => 'A',
950                Component::RootDir => 'R',
951                Component::CurDir => 'C',
952                Component::ParentDir => 'P',
953                Component::Normal(_) => 'N',
954            }
955        }
956        for tc in TEST_CASES.iter() {
957            let components: Vec<_> = tc.path.components().collect();
958            assert_eq!(
959                tc.components.chars().count(),
960                components.len(),
961                "path: {:?}",
962                tc.path
963            );
964            for (lhs, rhs) in std::iter::zip(tc.components.chars(), components.into_iter()) {
965                assert_eq!(lhs, component_to_char(rhs), "path: {:?}", tc.path);
966            }
967        }
968    }
969
970    #[test]
971    fn components_as_basename_dirname() {
972        for tc in TEST_CASES.iter() {
973            let mut components: Vec<_> = tc.path.components().collect();
974            fn basename_to_component(path: Path) -> Component {
975                if path.path == "." {
976                    Component::CurDir
977                } else if path.path == ".." {
978                    Component::ParentDir
979                } else {
980                    Component::Normal(path)
981                }
982            }
983            assert_eq!(
984                Some(basename_to_component(tc.basename.clone())),
985                components.pop()
986            );
987        }
988    }
989}