diff --git a/src/common.rs b/src/common.rs index bd40e76687..49c76afd2f 100644 --- a/src/common.rs +++ b/src/common.rs @@ -32,7 +32,7 @@ pub(crate) use snafu::{ResultExt, Snafu}; pub(crate) use unicode_width::UnicodeWidthChar; // modules -pub(crate) use crate::{config_error, keyword, search_error, setting}; +pub(crate) use crate::{config_error, keyword, setting}; // functions pub(crate) use crate::{default::default, empty::empty, load_dotenv::load_dotenv, output::output}; diff --git a/src/config_error.rs b/src/config_error.rs index 809b3dabf0..bc79bd57b2 100644 --- a/src/config_error.rs +++ b/src/config_error.rs @@ -9,8 +9,6 @@ pub(crate) enum ConfigError { message ))] Internal { message: String }, - #[snafu(display("Could not canonicalize justfile path `{}`: {}", path.display(), source))] - JustfilePathCanonicalize { path: PathBuf, source: io::Error }, #[snafu(display("Failed to get current directory: {}", source))] CurrentDir { source: io::Error }, #[snafu(display( diff --git a/src/search.rs b/src/search.rs index 6db3003592..4d6ead7c43 100644 --- a/src/search.rs +++ b/src/search.rs @@ -1,5 +1,7 @@ use crate::common::*; +use std::path::Component; + const FILENAME: &str = "justfile"; pub(crate) struct Search { @@ -25,12 +27,7 @@ impl Search { } SearchConfig::FromSearchDirectory { search_directory } => { - let search_directory = - search_directory - .canonicalize() - .context(search_error::Canonicalize { - path: search_directory, - })?; + let search_directory = Self::clean(invocation_directory, search_directory); let justfile = Self::justfile(&search_directory)?; @@ -43,7 +40,7 @@ impl Search { } SearchConfig::WithJustfile { justfile } => { - let justfile: PathBuf = justfile.to_path_buf(); + let justfile = Self::clean(invocation_directory, justfile); let working_directory = Self::working_directory_from_justfile(&justfile)?; @@ -57,8 +54,8 @@ impl Search { justfile, working_directory, } => Ok(Search { - justfile: justfile.to_path_buf(), - working_directory: working_directory.to_path_buf(), + justfile: Self::clean(invocation_directory, justfile), + working_directory: Self::clean(invocation_directory, working_directory), }), } } @@ -92,16 +89,30 @@ impl Search { } } - fn working_directory_from_justfile(justfile: &Path) -> SearchResult { - let justfile_canonical = justfile - .canonicalize() - .context(search_error::Canonicalize { path: justfile })?; + fn clean(invocation_directory: &Path, path: &Path) -> PathBuf { + let path = invocation_directory.join(path); + + let mut clean = Vec::new(); + + for component in path.components() { + if component == Component::ParentDir { + if let Some(Component::Normal(_)) = clean.last() { + clean.pop(); + } + } else { + clean.push(component); + } + } + + clean.into_iter().collect() + } + fn working_directory_from_justfile(justfile: &Path) -> SearchResult { Ok( - justfile_canonical + justfile .parent() .ok_or_else(|| SearchError::JustfileHadNoParent { - path: justfile_canonical.clone(), + path: justfile.to_path_buf(), })? .to_owned(), ) @@ -111,6 +122,7 @@ impl Search { #[cfg(test)] mod tests { use super::*; + use test_utilities::tmptree; #[test] fn not_found() { @@ -228,4 +240,51 @@ mod tests { _ => panic!("No errors were expected"), } } + + #[test] + fn justfile_symlink_parent() { + let tmp = tmptree! { + src: "", + sub: {}, + }; + + let src = tmp.path().join("src"); + let sub = tmp.path().join("sub"); + let justfile = sub.join("justfile"); + + #[cfg(unix)] + std::os::unix::fs::symlink(&src, &justfile).unwrap(); + + #[cfg(windows)] + std::os::windows::fs::symlink_file(&src, &justfile).unwrap(); + + let search_config = SearchConfig::FromInvocationDirectory; + + let search = Search::search(&search_config, &sub).unwrap(); + + assert_eq!(search.justfile, justfile); + assert_eq!(search.working_directory, sub); + } + + #[test] + fn clean() { + let cases = &[ + ("/", "foo", "/foo"), + ("/bar", "/foo", "/foo"), + ("//foo", "bar//baz", "/foo/bar/baz"), + ("/", "..", "/"), + ("/", "/..", "/"), + ("/..", "", "/"), + ("/../../../..", "../../../", "/"), + ("/.", "./", "/"), + ("/foo/../", "bar", "/bar"), + ("/foo/bar", "..", "/foo"), + ("/foo/bar/", "..", "/foo"), + ]; + + for (prefix, suffix, want) in cases { + let have = Search::clean(Path::new(prefix), Path::new(suffix)); + assert_eq!(have, Path::new(want)); + } + } } diff --git a/src/search_error.rs b/src/search_error.rs index d35f83d537..ba9ea64796 100644 --- a/src/search_error.rs +++ b/src/search_error.rs @@ -12,9 +12,7 @@ pub(crate) enum SearchError { .map(|candidate| candidate.file_name().unwrap().to_string_lossy()) ), ))] - MultipleCandidates { - candidates: Vec, - }, + MultipleCandidates { candidates: Vec }, #[snafu(display( "I/O error reading directory `{}`: {}", directory.display(), @@ -26,13 +24,8 @@ pub(crate) enum SearchError { }, #[snafu(display("No justfile found"))] NotFound, - Canonicalize { - path: PathBuf, - source: io::Error, - }, - JustfileHadNoParent { - path: PathBuf, - }, + #[snafu(display("Justfile path had no parent: {}", path.display()))] + JustfileHadNoParent { path: PathBuf }, } impl Error for SearchError {}