mz_ore/
path.rs

1// Copyright 2009 The Go Authors. All rights reserved.
2// Copyright Materialize, Inc. and contributors. All rights reserved.
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License in the LICENSE file at the
7// root of this repository, or online at
8//
9//     http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16//
17// Portions of this file are derived from the path.Clean function in the Go
18// project. The original source code was retrieved on January 19, 2022 from:
19//
20//     https://github.com/golang/go/blob/e7d5857a5a82551b8a70b6174ec73422442250ce/src/path/path.go
21//
22// The original source code is subject to the terms of the 3-clause BSD license, a copy
23// of which can be found in the LICENSE file at the root of this repository.
24
25//! Path utilities.
26
27use std::path::{Component, Path, PathBuf};
28
29/// Extension methods for [`Path`].
30pub trait PathExt {
31    /// Normalizes a path using purely lexical analysis.
32    ///
33    /// The following normalization rules are applied iteratively:
34    ///
35    ///   * Multiple contiguous path separators are replaced with a single
36    ///     [`MAIN_SEPARATOR`].
37    ///   * Current directory components (`.`) are removed.
38    ///   * Parent directory components (`..`) that do not occur at the
39    ///     beginning of the path are removed along with the preceding
40    ///     component.
41    ///    * Parent directory components at the start of a rooted path
42    ///      (e.g., `/..`) are removed.
43    ///    * Empty paths are replaced with ".".
44    ///
45    /// The returned path ends in a separator only if it represents the root
46    /// directory.
47    ///
48    /// This method is a port of Go's [`path.Clean`] function.
49    ///
50    /// [`path.Clean`]: https://pkg.go.dev/path#Clean
51    /// [`MAIN_SEPARATOR`]: std::path::MAIN_SEPARATOR
52    fn clean(&self) -> PathBuf;
53}
54
55impl PathExt for Path {
56    fn clean(&self) -> PathBuf {
57        let mut buf = PathBuf::new();
58        for component in self.components() {
59            match component {
60                // `.` elements are always redundant and can be dropped.
61                Component::CurDir => (),
62
63                // `..` elements require special handling.
64                Component::ParentDir => match buf.components().next_back() {
65                    // `..` at beginning or after another `..` needs to be
66                    // retained.
67                    None | Some(Component::ParentDir) => buf.push(Component::ParentDir),
68                    // `..` after a root is a no-op.
69                    Some(Component::RootDir) => (),
70                    // `..` after a normal component can be normalized by
71                    // dropping the prior component.
72                    _ => {
73                        buf.pop();
74                    }
75                },
76
77                // All other component types can be pushed verbatim.
78                Component::RootDir | Component::Prefix(_) | Component::Normal(_) => {
79                    buf.push(component);
80                }
81            }
82        }
83        if buf.as_os_str().is_empty() {
84            buf.push(".");
85        }
86        buf
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use std::path::Path;
93
94    use super::PathExt;
95
96    #[crate::test]
97    fn test_clean() {
98        // These test cases are imported from the Go standard library.
99        for (input, output) in &[
100            // Already clean.
101            ("", "."),
102            ("abc", "abc"),
103            ("abc/def", "abc/def"),
104            ("a/b/c", "a/b/c"),
105            (".", "."),
106            ("..", ".."),
107            ("../..", "../.."),
108            ("../../abc", "../../abc"),
109            ("/abc", "/abc"),
110            ("/", "/"),
111            // Remove trailing slash.
112            ("abc/", "abc"),
113            ("abc/def/", "abc/def"),
114            ("a/b/c/", "a/b/c"),
115            ("./", "."),
116            ("../", ".."),
117            ("../../", "../.."),
118            ("/abc/", "/abc"),
119            // Remove doubled slash.
120            ("abc//def//ghi", "abc/def/ghi"),
121            ("//abc", "/abc"),
122            ("///abc", "/abc"),
123            ("//abc//", "/abc"),
124            ("abc//", "abc"),
125            // Remove . elements.
126            ("abc/./def", "abc/def"),
127            ("/./abc/def", "/abc/def"),
128            ("abc/.", "abc"),
129            // Remove .. elements.
130            ("abc/def/ghi/../jkl", "abc/def/jkl"),
131            ("abc/def/../ghi/../jkl", "abc/jkl"),
132            ("abc/def/..", "abc"),
133            ("abc/def/../..", "."),
134            ("/abc/def/../..", "/"),
135            ("abc/def/../../..", ".."),
136            ("/abc/def/../../..", "/"),
137            ("abc/def/../../../ghi/jkl/../../../mno", "../../mno"),
138            // Combinations.
139            ("abc/./../def", "def"),
140            ("abc//./../def", "def"),
141            ("abc/../../././../def", "../../def"),
142        ] {
143            println!("clean({}) = {}", input, output);
144            assert_eq!(Path::new(input).clean(), Path::new(output));
145        }
146    }
147}