Refactor path handling (fixes #113)

* Fix path check
* Fix full path matching
* Allow more simple driver names in Windows tests
* Factor out special is_dir() check for "." and ".."
This commit is contained in:
J.W 2017-10-19 02:04:34 +08:00 committed by David Peter
parent 37def9bb83
commit 8d85debc12
9 changed files with 112 additions and 126 deletions

View file

@ -9,7 +9,6 @@ use super::TokenizedCommand;
/// be executed, and this process will continue until the receiver's sender has closed. /// be executed, and this process will continue until the receiver's sender has closed.
pub fn job( pub fn job(
rx: Arc<Mutex<Receiver<PathBuf>>>, rx: Arc<Mutex<Receiver<PathBuf>>>,
base: Arc<Option<PathBuf>>,
cmd: Arc<TokenizedCommand>, cmd: Arc<TokenizedCommand>,
out_perm: Arc<Mutex<()>>, out_perm: Arc<Mutex<()>>,
) { ) {
@ -23,12 +22,7 @@ pub fn job(
// Obtain the next path from the receiver, else if the channel // Obtain the next path from the receiver, else if the channel
// has closed, exit from the loop // has closed, exit from the loop
let value: PathBuf = match lock.recv() { let value: PathBuf = match lock.recv() {
Ok(value) => { Ok(value) => value,
match *base {
Some(ref base) => base.join(&value),
None => value,
}
}
Err(_) => break, Err(_) => break,
}; };

View file

@ -109,6 +109,8 @@ impl TokenizedCommand {
input: &Path, input: &Path,
out_perm: Arc<Mutex<()>>, out_perm: Arc<Mutex<()>>,
) -> CommandTicket<'a> { ) -> CommandTicket<'a> {
let input = input.strip_prefix(".").unwrap_or(input);
for token in &self.tokens { for token in &self.tokens {
match *token { match *token {
Token::Basename => *command += basename(&input.to_string_lossy()), Token::Basename => *command += basename(&input.to_string_lossy()),

View file

@ -1,50 +1,18 @@
use std::env::current_dir;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::io; use std::io;
/// Get a relative path with respect to a certain base path. pub fn path_absolute_form(path: &Path) -> io::Result<PathBuf> {
/// See: https://stackoverflow.com/a/39343127/704831 if path.is_absolute() {
pub fn path_relative_from(path: &Path, base: &Path) -> Option<PathBuf> { Ok(path.to_path_buf())
use std::path::Component;
if path.is_absolute() != base.is_absolute() {
if path.is_absolute() {
Some(PathBuf::from(path))
} else {
Some(PathBuf::from(base.join(path)))
}
} else { } else {
let mut ita = path.components(); let path = path.strip_prefix(".").unwrap_or(path);
let mut itb = base.components(); current_dir().map(|path_buf| path_buf.join(path))
let mut comps: Vec<Component> = vec![];
loop {
match (ita.next(), itb.next()) {
(None, None) => break,
(Some(a), None) => {
comps.push(a);
comps.extend(ita.by_ref());
break;
}
(None, _) => comps.push(Component::ParentDir),
(Some(a), Some(b)) if comps.is_empty() && a == b => (),
(Some(a), Some(b)) if b == Component::CurDir => comps.push(a),
(Some(_), Some(b)) if b == Component::ParentDir => return None,
(Some(a), Some(_)) => {
comps.push(Component::ParentDir);
for _ in itb {
comps.push(Component::ParentDir);
}
comps.push(a);
comps.extend(ita.by_ref());
break;
}
}
}
Some(comps.iter().map(|c| c.as_os_str()).collect())
} }
} }
pub fn absolute_path(path: &Path) -> io::Result<PathBuf> { pub fn absolute_path(path: &Path) -> io::Result<PathBuf> {
let path_buf = path.canonicalize()?; let path_buf = path_absolute_form(path)?;
#[cfg(windows)] #[cfg(windows)]
let path_buf = Path::new(path_buf.as_path().to_string_lossy().trim_left_matches( let path_buf = Path::new(path_buf.as_path().to_string_lossy().trim_left_matches(
@ -53,3 +21,13 @@ pub fn absolute_path(path: &Path) -> io::Result<PathBuf> {
Ok(path_buf) Ok(path_buf)
} }
// Path::is_dir() is not guarandteed to be intuitively correct for "." and ".."
// See: https://github.com/rust-lang/rust/issues/45302
pub fn is_dir(path: &Path) -> bool {
if path.file_name().is_some() {
path.is_dir()
} else {
path.is_dir() && path.canonicalize().is_ok()
}
}

View file

@ -7,13 +7,6 @@ use lscolors::LsColors;
use walk::FileType; use walk::FileType;
use regex_syntax::{Expr, ExprBuilder}; use regex_syntax::{Expr, ExprBuilder};
/// Root directory
#[cfg(unix)]
pub static ROOT_DIR: &'static str = "/";
#[cfg(windows)]
pub static ROOT_DIR: &'static str = "";
/// Defines how to display search result paths. /// Defines how to display search result paths.
#[derive(PartialEq)] #[derive(PartialEq)]
pub enum PathDisplay { pub enum PathDisplay {

View file

@ -29,7 +29,7 @@ use atty::Stream;
use regex::RegexBuilder; use regex::RegexBuilder;
use exec::TokenizedCommand; use exec::TokenizedCommand;
use internal::{error, pattern_has_uppercase_char, FdOptions, PathDisplay, ROOT_DIR}; use internal::{error, pattern_has_uppercase_char, FdOptions, PathDisplay};
use lscolors::LsColors; use lscolors::LsColors;
use walk::FileType; use walk::FileType;
@ -42,8 +42,7 @@ fn main() {
// Get the current working directory // Get the current working directory
let current_dir = Path::new("."); let current_dir = Path::new(".");
// .is_dir() is not guarandteed to be intuitively correct for "." and ".." if !fshelper::is_dir(&current_dir) {
if let Err(_) = current_dir.canonicalize() {
error("Error: could not get current directory."); error("Error: could not get current directory.");
} }
@ -52,7 +51,7 @@ fn main() {
Some(path) => PathBuf::from(path), Some(path) => PathBuf::from(path),
None => current_dir.to_path_buf(), None => current_dir.to_path_buf(),
}; };
if let Err(_) = root_dir_buf.canonicalize() { if !fshelper::is_dir(&root_dir_buf) {
error(&format!( error(&format!(
"Error: '{}' is not a directory.", "Error: '{}' is not a directory.",
root_dir_buf.to_string_lossy() root_dir_buf.to_string_lossy()
@ -132,21 +131,11 @@ fn main() {
command, command,
}; };
// If base_dir is ROOT_DIR, then root_dir must be absolute.
// Otherwise root_dir/entry cannot be turned into an existing relative path from base_dir.
//
// We utilize ROOT_DIR to avoid resolving the components of root_dir.
let base_dir_buf = match config.path_display {
PathDisplay::Relative => current_dir.to_path_buf(),
PathDisplay::Absolute => PathBuf::from(ROOT_DIR),
};
let base_dir = base_dir_buf.as_path();
match RegexBuilder::new(pattern) match RegexBuilder::new(pattern)
.case_insensitive(!config.case_sensitive) .case_insensitive(!config.case_sensitive)
.dot_matches_new_line(true) .dot_matches_new_line(true)
.build() { .build() {
Ok(re) => walk::scan(root_dir, Arc::new(re), base_dir, Arc::new(config)), Ok(re) => walk::scan(root_dir, Arc::new(re), Arc::new(config)),
Err(err) => error(err.description()), Err(err) => error(err.description()),
} }
} }

View file

@ -1,4 +1,4 @@
use internal::{FdOptions, PathDisplay}; use internal::FdOptions;
use lscolors::LsColors; use lscolors::LsColors;
use std::{fs, process}; use std::{fs, process};
@ -10,23 +10,13 @@ use std::os::unix::fs::PermissionsExt;
use ansi_term; use ansi_term;
pub fn print_entry(base: &Path, entry: &PathBuf, config: &FdOptions) { pub fn print_entry(entry: &PathBuf, config: &FdOptions) {
let path_full = if !entry.as_os_str().is_empty() { let path = entry.strip_prefix(".").unwrap_or(entry);
base.join(entry)
} else {
base.to_path_buf()
};
let path_to_print = if config.path_display == PathDisplay::Absolute {
&path_full
} else {
entry
};
let r = if let Some(ref ls_colors) = config.ls_colors { let r = if let Some(ref ls_colors) = config.ls_colors {
print_entry_colorized(base, path_to_print, config, ls_colors) print_entry_colorized(path, config, ls_colors)
} else { } else {
print_entry_uncolorized(path_to_print, config) print_entry_uncolorized(path, config)
}; };
if r.is_err() { if r.is_err() {
@ -35,12 +25,7 @@ pub fn print_entry(base: &Path, entry: &PathBuf, config: &FdOptions) {
} }
} }
fn print_entry_colorized( fn print_entry_colorized(path: &Path, config: &FdOptions, ls_colors: &LsColors) -> io::Result<()> {
base: &Path,
path: &Path,
config: &FdOptions,
ls_colors: &LsColors,
) -> io::Result<()> {
let default_style = ansi_term::Style::default(); let default_style = ansi_term::Style::default();
let stdout = io::stdout(); let stdout = io::stdout();
@ -50,7 +35,7 @@ fn print_entry_colorized(
let mut separator = String::new(); let mut separator = String::new();
// Full path to the current component. // Full path to the current component.
let mut component_path = base.to_path_buf(); let mut component_path = PathBuf::new();
// Traverse the path and colorize each component // Traverse the path and colorize each component
for component in path.components() { for component in path.components() {

View file

@ -1,6 +1,6 @@
use exec::{self, TokenizedCommand}; use exec::{self, TokenizedCommand};
use fshelper; use fshelper;
use internal::{error, FdOptions, PathDisplay}; use internal::{error, FdOptions};
use output; use output;
use std::path::Path; use std::path::Path;
@ -36,7 +36,7 @@ pub enum FileType {
/// If the `--exec` argument was supplied, this will create a thread pool for executing /// If the `--exec` argument was supplied, this will create a thread pool for executing
/// jobs in parallel from a given command line and the discovered paths. Otherwise, each /// jobs in parallel from a given command line and the discovered paths. Otherwise, each
/// path will simply be written to standard output. /// path will simply be written to standard output.
pub fn scan(root: &Path, pattern: Arc<Regex>, base: &Path, config: Arc<FdOptions>) { pub fn scan(root: &Path, pattern: Arc<Regex>, config: Arc<FdOptions>) {
let (tx, rx) = channel(); let (tx, rx) = channel();
let threads = config.threads; let threads = config.threads;
@ -54,8 +54,6 @@ pub fn scan(root: &Path, pattern: Arc<Regex>, base: &Path, config: Arc<FdOptions
// Spawn the thread that receives all results through the channel. // Spawn the thread that receives all results through the channel.
let rx_config = Arc::clone(&config); let rx_config = Arc::clone(&config);
let rx_base = base.to_owned();
let is_absolute = config.path_display == PathDisplay::Absolute;
let receiver_thread = thread::spawn(move || { let receiver_thread = thread::spawn(move || {
// This will be set to `Some` if the `--exec` argument was supplied. // This will be set to `Some` if the `--exec` argument was supplied.
if let Some(ref cmd) = rx_config.command { if let Some(ref cmd) = rx_config.command {
@ -63,8 +61,6 @@ pub fn scan(root: &Path, pattern: Arc<Regex>, base: &Path, config: Arc<FdOptions
let out_perm = Arc::new(Mutex::new(())); let out_perm = Arc::new(Mutex::new(()));
let base = Arc::new(if is_absolute { Some(rx_base) } else { None });
// This is safe because `cmd` will exist beyond the end of this scope. // This is safe because `cmd` will exist beyond the end of this scope.
// It's required to tell Rust that it's safe to share across threads. // It's required to tell Rust that it's safe to share across threads.
let cmd = unsafe { Arc::from_raw(cmd as *const TokenizedCommand) }; let cmd = unsafe { Arc::from_raw(cmd as *const TokenizedCommand) };
@ -74,11 +70,10 @@ pub fn scan(root: &Path, pattern: Arc<Regex>, base: &Path, config: Arc<FdOptions
for _ in 0..threads { for _ in 0..threads {
let rx = shared_rx.clone(); let rx = shared_rx.clone();
let cmd = cmd.clone(); let cmd = cmd.clone();
let base = base.clone();
let out_perm = out_perm.clone(); let out_perm = out_perm.clone();
// Spawn a job thread that will listen for and execute inputs. // Spawn a job thread that will listen for and execute inputs.
let handle = thread::spawn(move || exec::job(rx, base, cmd, out_perm)); let handle = thread::spawn(move || exec::job(rx, cmd, out_perm));
// Push the handle of the spawned thread into the vector for later joining. // Push the handle of the spawned thread into the vector for later joining.
handles.push(handle); handles.push(handle);
@ -110,7 +105,7 @@ pub fn scan(root: &Path, pattern: Arc<Regex>, base: &Path, config: Arc<FdOptions
if time::Instant::now() - start > max_buffer_time { if time::Instant::now() - start > max_buffer_time {
// Flush the buffer // Flush the buffer
for v in &buffer { for v in &buffer {
output::print_entry(&rx_base, v, &rx_config); output::print_entry(&v, &rx_config);
} }
buffer.clear(); buffer.clear();
@ -119,7 +114,7 @@ pub fn scan(root: &Path, pattern: Arc<Regex>, base: &Path, config: Arc<FdOptions
} }
} }
ReceiverMode::Streaming => { ReceiverMode::Streaming => {
output::print_entry(&rx_base, &value, &rx_config); output::print_entry(&value, &rx_config);
} }
} }
} }
@ -129,7 +124,7 @@ pub fn scan(root: &Path, pattern: Arc<Regex>, base: &Path, config: Arc<FdOptions
if !buffer.is_empty() { if !buffer.is_empty() {
buffer.sort(); buffer.sort();
for value in buffer { for value in buffer {
output::print_entry(&rx_base, &value, &rx_config); output::print_entry(&value, &rx_config);
} }
} }
} }
@ -137,7 +132,6 @@ pub fn scan(root: &Path, pattern: Arc<Regex>, base: &Path, config: Arc<FdOptions
// Spawn the sender threads. // Spawn the sender threads.
walker.run(|| { walker.run(|| {
let base = base.to_owned();
let config = Arc::clone(&config); let config = Arc::clone(&config);
let pattern = Arc::clone(&pattern); let pattern = Arc::clone(&pattern);
let tx_thread = tx.clone(); let tx_thread = tx.clone();
@ -186,21 +180,19 @@ pub fn scan(root: &Path, pattern: Arc<Regex>, base: &Path, config: Arc<FdOptions
} }
let search_str_o = if config.search_full_path { let search_str_o = if config.search_full_path {
Some(entry_path.to_string_lossy()) match fshelper::path_absolute_form(&entry_path) {
Ok(path_abs_buf) => Some(path_abs_buf.to_string_lossy().into_owned().into()),
Err(_) => error("Error: unable to get full path."),
}
} else { } else {
entry_path.file_name().map(|f| f.to_string_lossy()) entry_path.file_name().map(|f| f.to_string_lossy())
}; };
if let Some(search_str) = search_str_o { if let Some(search_str) = search_str_o {
pattern.find(&*search_str).map(|_| { if pattern.is_match(&*search_str) {
let path_rel_buf = match fshelper::path_relative_from(entry_path, &*base) {
Some(p) => p,
None => error("Error: could not get relative path for directory entry."),
};
// TODO: take care of the unwrap call // TODO: take care of the unwrap call
tx_thread.send(path_rel_buf.to_owned()).unwrap() tx_thread.send(entry_path.to_owned()).unwrap()
}); }
} }
ignore::WalkState::Continue ignore::WalkState::Continue

View file

@ -136,10 +136,16 @@ impl TestEnv {
} }
/// Get the root directory for the tests. /// Get the root directory for the tests.
pub fn root(&self) -> PathBuf { pub fn test_root(&self) -> PathBuf {
self.temp_dir.path().to_path_buf() self.temp_dir.path().to_path_buf()
} }
/// Get the root directory of the file system.
pub fn system_root(&self) -> PathBuf {
let mut components = self.temp_dir.path().components();
PathBuf::from(components.next().expect("root directory").as_os_str())
}
/// Assert that calling *fd* with the specified arguments produces the expected output. /// Assert that calling *fd* with the specified arguments produces the expected output.
pub fn assert_output(&self, args: &[&str], expected: &str) { pub fn assert_output(&self, args: &[&str], expected: &str) {
self.assert_output_subdirectory(".", args, expected) self.assert_output_subdirectory(".", args, expected)

View file

@ -1,11 +1,14 @@
//! Integration tests for the CLI interface of fd. //! Integration tests for the CLI interface of fd.
extern crate regex;
mod testenv; mod testenv;
use testenv::TestEnv; use testenv::TestEnv;
use regex::escape;
fn get_absolute_root_path(env: &TestEnv) -> String { fn get_absolute_root_path(env: &TestEnv) -> String {
let path = env.root() let path = env.test_root()
.canonicalize() .canonicalize()
.expect("absolute path") .expect("absolute path")
.to_str() .to_str()
@ -177,8 +180,14 @@ fn test_case_insensitive() {
fn test_full_path() { fn test_full_path() {
let te = TestEnv::new(); let te = TestEnv::new();
let root = te.system_root();
let prefix = escape(&root.to_string_lossy());
te.assert_output( te.assert_output(
&["--full-path", "three.*foo"], &[
"--full-path",
&format!("^{prefix}.*three.*foo$", prefix = prefix),
],
"one/two/three/d.foo "one/two/three/d.foo
one/two/three/directory_foo", one/two/three/directory_foo",
); );
@ -343,7 +352,7 @@ fn test_absolute_path() {
{abs_path}/one/two/three/d.foo {abs_path}/one/two/three/d.foo
{abs_path}/one/two/three/directory_foo {abs_path}/one/two/three/directory_foo
{abs_path}/symlink", {abs_path}/symlink",
abs_path = abs_path abs_path = &abs_path
), ),
); );
@ -356,7 +365,7 @@ fn test_absolute_path() {
{abs_path}/one/two/C.Foo2 {abs_path}/one/two/C.Foo2
{abs_path}/one/two/three/d.foo {abs_path}/one/two/three/d.foo
{abs_path}/one/two/three/directory_foo", {abs_path}/one/two/three/directory_foo",
abs_path = abs_path abs_path = &abs_path
), ),
); );
@ -369,7 +378,7 @@ fn test_absolute_path() {
{abs_path}/one/two/C.Foo2 {abs_path}/one/two/C.Foo2
{abs_path}/one/two/three/d.foo {abs_path}/one/two/three/d.foo
{abs_path}/one/two/three/directory_foo", {abs_path}/one/two/three/directory_foo",
abs_path = abs_path abs_path = &abs_path
), ),
); );
} }
@ -435,9 +444,13 @@ fn test_symlink() {
// the array pointed to by buf, and return buf. The pathname shall contain no components that // the array pointed to by buf, and return buf. The pathname shall contain no components that
// are dot or dot-dot, or are symbolic links. // are dot or dot-dot, or are symbolic links.
// //
// Symlinks on Unix are aliases to real paths, only has one redirection. // Key points:
// 1. The path of the current working directory of a Unix process cannot contain symlinks.
// 2. The path of the current working directory of a Windows process can contain symlinks.
// //
// Symlinks on Windows can refer to symlinks, and are resolved after logical step "..". // More:
// 1. On Windows, symlinks are resolved after the ".." component.
// 2. On Unix, symlinks are resolved immediately as encountered.
let parent_parent = if cfg!(windows) { ".." } else { "../.." }; let parent_parent = if cfg!(windows) { ".." } else { "../.." };
te.assert_output_subdirectory( te.assert_output_subdirectory(
@ -462,12 +475,13 @@ fn test_symlink() {
"symlink", "symlink",
&["--absolute-path"], &["--absolute-path"],
&format!( &format!(
"{abs_path}/one/two/c.foo "{abs_path}/{dir}/c.foo
{abs_path}/one/two/C.Foo2 {abs_path}/{dir}/C.Foo2
{abs_path}/one/two/three {abs_path}/{dir}/three
{abs_path}/one/two/three/d.foo {abs_path}/{dir}/three/d.foo
{abs_path}/one/two/three/directory_foo", {abs_path}/{dir}/three/directory_foo",
abs_path = abs_path dir = if cfg!(windows) { "symlink" } else { "one/two" },
abs_path = &abs_path
), ),
); );
@ -479,7 +493,40 @@ fn test_symlink() {
{abs_path}/symlink/three {abs_path}/symlink/three
{abs_path}/symlink/three/d.foo {abs_path}/symlink/three/d.foo
{abs_path}/symlink/three/directory_foo", {abs_path}/symlink/three/directory_foo",
abs_path = abs_path abs_path = &abs_path
),
);
let root = te.system_root();
let prefix = escape(&root.to_string_lossy());
te.assert_output_subdirectory(
"symlink",
&[
"--absolute-path",
"--full-path",
&format!("^{prefix}.*three", prefix = prefix),
],
&format!(
"{abs_path}/{dir}/three
{abs_path}/{dir}/three/d.foo
{abs_path}/{dir}/three/directory_foo",
dir = if cfg!(windows) { "symlink" } else { "one/two" },
abs_path = &abs_path
),
);
te.assert_output(
&[
"--full-path",
&format!("^{prefix}.*symlink.*three", prefix = prefix),
&format!("{abs_path}/symlink", abs_path = abs_path),
],
&format!(
"{abs_path}/symlink/three
{abs_path}/symlink/three/d.foo
{abs_path}/symlink/three/directory_foo",
abs_path = &abs_path
), ),
); );
} }