Skip to content

Commit a6549a2

Browse files
phatedTomAFrench
andauthored
chore: Replace resolve_path function with a trait that impls normalize (#2157)
* chore: Replace `resolve_path` function with a trait that impls normalize * chore: add smoketests for path normalization * chore: remove unnecessary raw string * chore: remove test for windows prefixes * chore: cspell --------- Co-authored-by: Tom French <tom@tomfren.ch>
1 parent 1e79f4a commit a6549a2

File tree

4 files changed

+78
-34
lines changed

4 files changed

+78
-34
lines changed

Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/fm/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ wasm-bindgen.workspace = true
1717

1818
[dev-dependencies]
1919
tempfile = "3.2.0"
20+
iter-extended.workspace = true

crates/fm/src/lib.rs

+74-33
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ pub struct FileManager {
3030
impl FileManager {
3131
pub fn new(root: &Path) -> Self {
3232
Self {
33-
root: root.to_path_buf(),
33+
root: root.normalize(),
3434
file_map: Default::default(),
3535
id_to_path: Default::default(),
3636
path_to_id: Default::default(),
@@ -44,7 +44,7 @@ impl FileManager {
4444
// TODO: The stdlib path should probably be an absolute path rooted in something people would never create
4545
file_name.to_path_buf()
4646
} else {
47-
self.resolve_path(file_name)
47+
self.root.join(file_name).normalize()
4848
};
4949

5050
// Check that the resolved path already exists in the file map, if it is, we return it.
@@ -99,41 +99,82 @@ impl FileManager {
9999

100100
Err(candidate_files.remove(0).as_os_str().to_str().unwrap().to_owned())
101101
}
102+
}
102103

103-
/// Resolve a path within the FileManager, removing all `.` and `..` segments.
104-
/// Additionally, relative paths will be resolved against the FileManager's root.
105-
pub fn resolve_path(&self, path: &Path) -> PathBuf {
106-
// This is a replacement for `std::fs::canonicalize` that doesn't verify the path exists.
107-
//
108-
// Plucked from https://github.com/rust-lang/cargo/blob/fede83ccf973457de319ba6fa0e36ead454d2e20/src/cargo/util/paths.rs#L61
109-
// Advice from https://www.reddit.com/r/rust/comments/hkkquy/comment/fwtw53s/
110-
let mut components = path.components().peekable();
111-
let mut ret = match components.peek().cloned() {
112-
Some(c @ Component::Prefix(..)) => {
113-
components.next();
114-
PathBuf::from(c.as_os_str())
115-
}
116-
Some(Component::RootDir) => PathBuf::new(),
117-
// If the first component isn't a RootDir or a Prefix, we know it is relative and needs to be joined to root
118-
_ => self.root.clone(),
119-
};
104+
pub trait NormalizePath {
105+
/// Replacement for `std::fs::canonicalize` that doesn't verify the path exists.
106+
///
107+
/// Plucked from https://github.com/rust-lang/cargo/blob/fede83ccf973457de319ba6fa0e36ead454d2e20/src/cargo/util/paths.rs#L61
108+
/// Advice from https://www.reddit.com/r/rust/comments/hkkquy/comment/fwtw53s/
109+
fn normalize(&self) -> PathBuf;
110+
}
111+
112+
impl NormalizePath for PathBuf {
113+
fn normalize(&self) -> PathBuf {
114+
let components = self.components();
115+
resolve_components(components)
116+
}
117+
}
118+
119+
impl NormalizePath for &Path {
120+
fn normalize(&self) -> PathBuf {
121+
let components = self.components();
122+
resolve_components(components)
123+
}
124+
}
120125

121-
for component in components {
122-
match component {
123-
Component::Prefix(..) => unreachable!(),
124-
Component::RootDir => {
125-
ret.push(component.as_os_str());
126-
}
127-
Component::CurDir => {}
128-
Component::ParentDir => {
129-
ret.pop();
130-
}
131-
Component::Normal(c) => {
132-
ret.push(c);
133-
}
126+
fn resolve_components<'a>(components: impl Iterator<Item = Component<'a>>) -> PathBuf {
127+
let mut components = components.peekable();
128+
129+
// Preserve path prefix if one exists.
130+
let mut normalized_path = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
131+
components.next();
132+
PathBuf::from(c.as_os_str())
133+
} else {
134+
PathBuf::new()
135+
};
136+
137+
for component in components {
138+
match component {
139+
Component::Prefix(..) => unreachable!("Path cannot contain multiple prefixes"),
140+
Component::RootDir => {
141+
normalized_path.push(component.as_os_str());
142+
}
143+
Component::CurDir => {}
144+
Component::ParentDir => {
145+
normalized_path.pop();
146+
}
147+
Component::Normal(c) => {
148+
normalized_path.push(c);
134149
}
135150
}
136-
ret
151+
}
152+
153+
normalized_path
154+
}
155+
156+
#[cfg(test)]
157+
mod path_normalization {
158+
use iter_extended::vecmap;
159+
use std::path::PathBuf;
160+
161+
use crate::NormalizePath;
162+
163+
#[test]
164+
fn normalizes_paths_correctly() {
165+
// Note that tests are run on unix so prefix handling can't be tested (as these only exist on Windows)
166+
let test_cases = vecmap(
167+
[
168+
("/", "/"), // Handles root
169+
("/foo/bar/../baz/../bar", "/foo/bar"), // Handles backtracking
170+
("/././././././././baz", "/baz"), // Removes no-ops
171+
],
172+
|(unnormalized, normalized)| (PathBuf::from(unnormalized), PathBuf::from(normalized)),
173+
);
174+
175+
for (path, expected_result) in test_cases {
176+
assert_eq!(path.normalize(), expected_result);
177+
}
137178
}
138179
}
139180

cspell.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
"typevars",
6464
"udiv",
6565
"uninstantiated",
66+
"unnormalized",
6667
"urem",
6768
"vecmap",
6869
"direnv",
@@ -99,4 +100,4 @@
99100
"termcolor",
100101
"thiserror"
101102
]
102-
}
103+
}

0 commit comments

Comments
 (0)