Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Replace resolve_path function with a trait that impls normalize #2157

Merged
merged 5 commits into from
Aug 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/fm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ wasm-bindgen.workspace = true

[dev-dependencies]
tempfile = "3.2.0"
iter-extended.workspace = true
107 changes: 74 additions & 33 deletions crates/fm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ pub struct FileManager {
impl FileManager {
pub fn new(root: &Path) -> Self {
Self {
root: root.to_path_buf(),
root: root.normalize(),
file_map: Default::default(),
id_to_path: Default::default(),
path_to_id: Default::default(),
Expand All @@ -44,7 +44,7 @@ impl FileManager {
// TODO: The stdlib path should probably be an absolute path rooted in something people would never create
file_name.to_path_buf()
} else {
self.resolve_path(file_name)
self.root.join(file_name).normalize()
};

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

Err(candidate_files.remove(0).as_os_str().to_str().unwrap().to_owned())
}
}

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

impl NormalizePath for PathBuf {
fn normalize(&self) -> PathBuf {
let components = self.components();
resolve_components(components)
}
}

impl NormalizePath for &Path {
fn normalize(&self) -> PathBuf {
let components = self.components();
resolve_components(components)
}
}

for component in components {
match component {
Component::Prefix(..) => unreachable!(),
Component::RootDir => {
ret.push(component.as_os_str());
}
Component::CurDir => {}
Component::ParentDir => {
ret.pop();
}
Component::Normal(c) => {
ret.push(c);
}
fn resolve_components<'a>(components: impl Iterator<Item = Component<'a>>) -> PathBuf {
let mut components = components.peekable();

// Preserve path prefix if one exists.
let mut normalized_path = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
components.next();
PathBuf::from(c.as_os_str())
} else {
PathBuf::new()
};

for component in components {
match component {
Component::Prefix(..) => unreachable!("Path cannot contain multiple prefixes"),
Component::RootDir => {
normalized_path.push(component.as_os_str());
}
Component::CurDir => {}
Component::ParentDir => {
normalized_path.pop();
}
Component::Normal(c) => {
normalized_path.push(c);
}
}
ret
}

normalized_path
}

#[cfg(test)]
mod path_normalization {
use iter_extended::vecmap;
use std::path::PathBuf;

use crate::NormalizePath;

#[test]
fn normalizes_paths_correctly() {
// Note that tests are run on unix so prefix handling can't be tested (as these only exist on Windows)
let test_cases = vecmap(
[
("/", "/"), // Handles root
("/foo/bar/../baz/../bar", "/foo/bar"), // Handles backtracking
("/././././././././baz", "/baz"), // Removes no-ops
],
|(unnormalized, normalized)| (PathBuf::from(unnormalized), PathBuf::from(normalized)),
);

for (path, expected_result) in test_cases {
assert_eq!(path.normalize(), expected_result);
}
}
}

Expand Down
3 changes: 2 additions & 1 deletion cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"typevars",
"udiv",
"uninstantiated",
"unnormalized",
"urem",
"vecmap",
"direnv",
Expand Down Expand Up @@ -99,4 +100,4 @@
"termcolor",
"thiserror"
]
}
}