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}