1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
//! This crate exports a single trait, `Lexiclean`, with a single method,
//! `lexiclean`, implemented on `&Path`, that performs lexical path cleaning.
//!
//! Lexical path cleaning simplifies paths without looking at the underlying
//! filesystem. This means:
//!
//! - Normally, if `file` is a file and not a directory, the path `file/..` will
//!   fail to resolve to. Lexiclean resolves this to `.`
//!
//! - `Path::canonicalize` returns `io::Result<PathBuf>`, because it must make
//!   system calls, that might fail. Lexiclean does not make system calls, and
//!   thus cannot fail.
//!
//! - The path returned by lexiclean will only contain components present in the
//!   input path. This can make the resultant paths more legible for users,
//!   since `foo/..` will resolve to `.`, and not `/Some/absolute/directory`.
//!
//! - Lexiclean does not respect symlinks.
//!
//! - Lexiclean has only been lightly tested. In particular, it has not been
//!   tested with windows paths, which are very complicated, and can contain
//!   many types of components that the author of this crate never contemplated.
//!
//!   Additional test cases and bug fixes are most welcome!
use std::path::{Component, Path, PathBuf};

pub trait Lexiclean {
  fn lexiclean(self) -> PathBuf;
}

impl Lexiclean for &Path {
  fn lexiclean(self) -> PathBuf {
    if self.components().count() <= 1 {
      return self.to_owned();
    }

    let mut components = Vec::new();

    for component in self
      .components()
      .filter(|component| component != &Component::CurDir)
    {
      if component == Component::ParentDir {
        match components.last() {
          Some(Component::Normal(_)) => {
            components.pop();
          }
          Some(Component::ParentDir) | None => components.push(component),
          _ => {}
        }
      } else {
        components.push(component);
      }
    }

    components.into_iter().collect()
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  #[rustfmt::skip]
  fn simple() {
    fn case(path: &str, want: &str) {
      assert_eq!(Path::new(path).lexiclean(), Path::new(want));
    }

    case("",                       "");
    case(".",                      ".");
    case("..",                     "..");
    case("../../../",              "../../..");
    case("./",                     ".");
    case("./..",                   "..");
    case("./../.",                 "..");
    case("./././.",                ".");
    case("/." ,                    "/");
    case("/..",                    "/");
    case("/../../../../../../../", "/");
    case("/././",                  "/");
    case("//foo/bar//baz",         "/foo/bar/baz");
    case("/foo",                   "/foo");
    case("/foo/../bar",            "/bar");
    case("/foo/./bar/.",           "/foo/bar");
    case("/foo/bar/..",            "/foo");
    case("bar//baz",               "bar/baz");
    case("foo",                    "foo");
    case("foo/./bar",              "foo/bar");
  }
}