diff --git a/Cargo.lock b/Cargo.lock index ea0206fb..c595982e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1130,4 +1130,4 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" name = "yansi" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" \ No newline at end of file diff --git a/src/find/matchers/access.rs b/src/find/matchers/access.rs index 3d8c695f..e707c35d 100644 --- a/src/find/matchers/access.rs +++ b/src/find/matchers/access.rs @@ -5,9 +5,8 @@ // https://opensource.org/licenses/MIT. use faccess::PathExt; -use walkdir::DirEntry; -use super::{Matcher, MatcherIO}; +use super::{Matcher, MatcherIO, WalkEntry}; /// Matcher for -{read,writ,execut}able. pub enum AccessMatcher { @@ -17,7 +16,7 @@ pub enum AccessMatcher { } impl Matcher for AccessMatcher { - fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool { + fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool { let path = file_info.path(); match self { diff --git a/src/find/matchers/delete.rs b/src/find/matchers/delete.rs index cd9ba7b5..8511a67e 100644 --- a/src/find/matchers/delete.rs +++ b/src/find/matchers/delete.rs @@ -7,13 +7,10 @@ * file that was distributed with this source code. */ -use std::fs::{self, FileType}; +use std::fs; use std::io::{self, stderr, Write}; -use std::path::Path; -use walkdir::DirEntry; - -use super::{Matcher, MatcherIO}; +use super::{Matcher, MatcherIO, WalkEntry}; pub struct DeleteMatcher; @@ -22,17 +19,17 @@ impl DeleteMatcher { DeleteMatcher } - fn delete(&self, file_path: &Path, file_type: FileType) -> io::Result<()> { - if file_type.is_dir() { - fs::remove_dir(file_path) + fn delete(&self, entry: &WalkEntry) -> io::Result<()> { + if entry.file_type().is_dir() && !entry.path_is_symlink() { + fs::remove_dir(entry.path()) } else { - fs::remove_file(file_path) + fs::remove_file(entry.path()) } } } impl Matcher for DeleteMatcher { - fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool { + fn matches(&self, file_info: &WalkEntry, matcher_io: &mut MatcherIO) -> bool { let path = file_info.path(); let path_str = path.to_string_lossy(); @@ -43,9 +40,10 @@ impl Matcher for DeleteMatcher { return true; } - match self.delete(path, file_info.file_type()) { + match self.delete(file_info) { Ok(()) => true, Err(e) => { + matcher_io.set_exit_code(1); writeln!(&mut stderr(), "Failed to delete {path_str}: {e}").unwrap(); false } diff --git a/src/find/matchers/empty.rs b/src/find/matchers/empty.rs index 9b6431d5..6726c17d 100644 --- a/src/find/matchers/empty.rs +++ b/src/find/matchers/empty.rs @@ -9,7 +9,7 @@ use std::{ io::{stderr, Write}, }; -use super::Matcher; +use super::{Matcher, MatcherIO, WalkEntry}; pub struct EmptyMatcher; @@ -20,7 +20,7 @@ impl EmptyMatcher { } impl Matcher for EmptyMatcher { - fn matches(&self, file_info: &walkdir::DirEntry, _: &mut super::MatcherIO) -> bool { + fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool { if file_info.file_type().is_file() { match file_info.metadata() { Ok(meta) => meta.len() == 0, diff --git a/src/find/matchers/entry.rs b/src/find/matchers/entry.rs new file mode 100644 index 00000000..e2b5cdaf --- /dev/null +++ b/src/find/matchers/entry.rs @@ -0,0 +1,343 @@ +//! Paths encountered during a walk. + +use std::cell::OnceCell; +use std::error::Error; +use std::ffi::OsStr; +use std::fmt::{self, Display, Formatter}; +use std::fs::{self, Metadata}; +use std::io::{self, ErrorKind}; +#[cfg(unix)] +use std::os::unix::fs::FileTypeExt; +use std::path::{Path, PathBuf}; + +use walkdir::DirEntry; + +use super::Follow; + +/// Wrapper for a directory entry. +#[derive(Debug)] +enum Entry { + /// Wraps an explicit path and depth. + Explicit(PathBuf, usize), + /// Wraps a WalkDir entry. + WalkDir(DirEntry), +} + +/// File types. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum FileType { + Unknown, + Fifo, + CharDevice, + Directory, + BlockDevice, + Regular, + Symlink, + Socket, +} + +impl FileType { + pub fn is_dir(self) -> bool { + self == Self::Directory + } + + pub fn is_file(self) -> bool { + self == Self::Regular + } + + pub fn is_symlink(self) -> bool { + self == Self::Symlink + } +} + +impl From for FileType { + fn from(t: fs::FileType) -> FileType { + if t.is_dir() { + return FileType::Directory; + } + if t.is_file() { + return FileType::Regular; + } + if t.is_symlink() { + return FileType::Symlink; + } + + #[cfg(unix)] + { + if t.is_fifo() { + return FileType::Fifo; + } + if t.is_char_device() { + return FileType::CharDevice; + } + if t.is_block_device() { + return FileType::BlockDevice; + } + if t.is_socket() { + return FileType::Socket; + } + } + + FileType::Unknown + } +} + +/// An error encountered while walking a file system. +#[derive(Clone, Debug)] +pub struct WalkError { + /// The path that caused the error, if known. + path: Option, + /// The depth below the root path, if known. + depth: Option, + /// The io::Error::raw_os_error(), if known. + raw: Option, +} + +impl WalkError { + /// Get the path this error occurred on, if known. + pub fn path(&self) -> Option<&Path> { + self.path.as_deref() + } + + /// Get the traversal depth when this error occurred, if known. + pub fn depth(&self) -> Option { + self.depth + } + + /// Get the kind of I/O error. + pub fn kind(&self) -> ErrorKind { + io::Error::from(self).kind() + } + + /// Check for ErrorKind::{NotFound,NotADirectory}. + pub fn is_not_found(&self) -> bool { + if self.kind() == ErrorKind::NotFound { + return true; + } + + // NotADirectory is nightly-only + #[cfg(unix)] + { + if self.raw == Some(uucore::libc::ENOTDIR) { + return true; + } + } + + false + } + + /// Check for ErrorKind::FilesystemLoop. + pub fn is_loop(&self) -> bool { + #[cfg(unix)] + return self.raw == Some(uucore::libc::ELOOP); + + #[cfg(not(unix))] + return false; + } +} + +impl Display for WalkError { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> { + let ioe = io::Error::from(self); + if let Some(path) = &self.path { + write!(f, "{}: {}", path.display(), ioe) + } else { + write!(f, "{}", ioe) + } + } +} + +impl Error for WalkError {} + +impl From for WalkError { + fn from(e: io::Error) -> WalkError { + WalkError::from(&e) + } +} + +impl From<&io::Error> for WalkError { + fn from(e: &io::Error) -> WalkError { + WalkError { + path: None, + depth: None, + raw: e.raw_os_error(), + } + } +} + +impl From for WalkError { + fn from(e: walkdir::Error) -> WalkError { + WalkError::from(&e) + } +} + +impl From<&walkdir::Error> for WalkError { + fn from(e: &walkdir::Error) -> WalkError { + WalkError { + path: e.path().map(|p| p.to_owned()), + depth: Some(e.depth()), + raw: e.io_error().and_then(|e| e.raw_os_error()), + } + } +} + +impl From for io::Error { + fn from(e: WalkError) -> io::Error { + io::Error::from(&e) + } +} + +impl From<&WalkError> for io::Error { + fn from(e: &WalkError) -> io::Error { + e.raw + .map(io::Error::from_raw_os_error) + .unwrap_or_else(|| ErrorKind::Other.into()) + } +} + +/// A path encountered while walking a file system. +#[derive(Debug)] +pub struct WalkEntry { + /// The wrapped path/dirent. + inner: Entry, + /// Whether to follow symlinks. + follow: Follow, + /// Cached metadata. + meta: OnceCell>, +} + +impl WalkEntry { + /// Create a new WalkEntry for a specific file. + pub fn new(path: impl Into, depth: usize, follow: Follow) -> Self { + Self { + inner: Entry::Explicit(path.into(), depth), + follow, + meta: OnceCell::new(), + } + } + + /// Convert a [walkdir::DirEntry] to a [WalkEntry]. Errors due to broken symbolic links will be + /// converted to valid entries, but other errors will be propagated. + pub fn from_walkdir( + result: walkdir::Result, + follow: Follow, + ) -> Result { + let result = result.map_err(WalkError::from); + + match result { + Ok(entry) => { + let ret = if entry.depth() == 0 && follow != Follow::Never { + // DirEntry::file_type() is wrong for root symlinks when follow_root_links is set + Self::new(entry.path(), 0, follow) + } else { + Self { + inner: Entry::WalkDir(entry), + follow, + meta: OnceCell::new(), + } + }; + Ok(ret) + } + Err(e) if e.is_not_found() => { + // Detect broken symlinks and replace them with explicit entries + if let (Some(path), Some(depth)) = (e.path(), e.depth()) { + if let Ok(meta) = path.symlink_metadata() { + return Ok(WalkEntry { + inner: Entry::Explicit(path.into(), depth), + follow: Follow::Never, + meta: Ok(meta).into(), + }); + } + } + + Err(e) + } + Err(e) => Err(e), + } + } + + /// Get the path to this entry. + pub fn path(&self) -> &Path { + match &self.inner { + Entry::Explicit(path, _) => path.as_path(), + Entry::WalkDir(ent) => ent.path(), + } + } + + /// Get the path to this entry. + pub fn into_path(self) -> PathBuf { + match self.inner { + Entry::Explicit(path, _) => path, + Entry::WalkDir(ent) => ent.into_path(), + } + } + + /// Get the name of this entry. + pub fn file_name(&self) -> &OsStr { + match &self.inner { + Entry::Explicit(path, _) => { + // Path::file_name() only works if the last component is normal + path.components() + .last() + .map(|c| c.as_os_str()) + .unwrap_or_else(|| path.as_os_str()) + } + Entry::WalkDir(ent) => ent.file_name(), + } + } + + /// Get the depth of this entry below the root. + pub fn depth(&self) -> usize { + match &self.inner { + Entry::Explicit(_, depth) => *depth, + Entry::WalkDir(ent) => ent.depth(), + } + } + + /// Get whether symbolic links are followed for this entry. + pub fn follow(&self) -> bool { + self.follow.follow_at_depth(self.depth()) + } + + /// Get the metadata on a cache miss. + fn get_metadata(&self) -> Result { + self.follow.metadata_at_depth(self.path(), self.depth()) + } + + /// Get the [Metadata] for this entry, following symbolic links if appropriate. + /// Multiple calls to this function will cache and re-use the same [Metadata]. + pub fn metadata(&self) -> Result<&Metadata, WalkError> { + let result = self.meta.get_or_init(|| match &self.inner { + Entry::Explicit(_, _) => Ok(self.get_metadata()?), + Entry::WalkDir(ent) => Ok(ent.metadata()?), + }); + result.as_ref().map_err(|e| e.clone()) + } + + /// Get the file type of this entry. + pub fn file_type(&self) -> FileType { + match &self.inner { + Entry::Explicit(_, _) => self + .metadata() + .map(|m| m.file_type().into()) + .unwrap_or(FileType::Unknown), + Entry::WalkDir(ent) => ent.file_type().into(), + } + } + + /// Check whether this entry is a symbolic link, regardless of whether links + /// are being followed. + pub fn path_is_symlink(&self) -> bool { + match &self.inner { + Entry::Explicit(path, _) => { + if self.follow() { + path.symlink_metadata() + .is_ok_and(|m| m.file_type().is_symlink()) + } else { + self.file_type().is_symlink() + } + } + Entry::WalkDir(ent) => ent.path_is_symlink(), + } + } +} diff --git a/src/find/matchers/exec.rs b/src/find/matchers/exec.rs index 487cd332..23055e06 100644 --- a/src/find/matchers/exec.rs +++ b/src/find/matchers/exec.rs @@ -9,9 +9,8 @@ use std::ffi::OsString; use std::io::{stderr, Write}; use std::path::Path; use std::process::Command; -use walkdir::DirEntry; -use super::{Matcher, MatcherIO}; +use super::{Matcher, MatcherIO, WalkEntry}; enum Arg { FileArg(Vec), @@ -52,7 +51,7 @@ impl SingleExecMatcher { } impl Matcher for SingleExecMatcher { - fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool { + fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool { let mut command = Command::new(&self.executable); let path_to_file = if self.exec_in_parent_dir { if let Some(f) = file_info.path().file_name() { diff --git a/src/find/matchers/fs.rs b/src/find/matchers/fs.rs index 40090f77..6229efef 100644 --- a/src/find/matchers/fs.rs +++ b/src/find/matchers/fs.rs @@ -10,7 +10,7 @@ use std::{ io::{stderr, Write}, }; -use super::Matcher; +use super::{Matcher, MatcherIO, WalkEntry}; /// The latest mapping from dev_id to fs_type, used for saving mount info reads pub struct Cache { @@ -88,7 +88,7 @@ impl FileSystemMatcher { } impl Matcher for FileSystemMatcher { - fn matches(&self, file_info: &walkdir::DirEntry, _: &mut super::MatcherIO) -> bool { + fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool { #[cfg(not(unix))] { false diff --git a/src/find/matchers/group.rs b/src/find/matchers/group.rs index bceb80b9..d97525cc 100644 --- a/src/find/matchers/group.rs +++ b/src/find/matchers/group.rs @@ -3,7 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use super::Matcher; +use super::{Matcher, MatcherIO, WalkEntry}; #[cfg(unix)] use nix::unistd::Group; @@ -57,8 +57,8 @@ impl GroupMatcher { impl Matcher for GroupMatcher { #[cfg(unix)] - fn matches(&self, file_info: &walkdir::DirEntry, _: &mut super::MatcherIO) -> bool { - let Ok(metadata) = file_info.path().metadata() else { + fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool { + let Ok(metadata) = file_info.metadata() else { return false; }; @@ -71,7 +71,7 @@ impl Matcher for GroupMatcher { } #[cfg(windows)] - fn matches(&self, _file_info: &walkdir::DirEntry, _: &mut super::MatcherIO) -> bool { + fn matches(&self, _file_info: &WalkEntry, _: &mut MatcherIO) -> bool { // The user group acquisition function for Windows systems is not implemented in MetadataExt, // so it is somewhat difficult to implement it. :( false @@ -82,14 +82,14 @@ pub struct NoGroupMatcher {} impl Matcher for NoGroupMatcher { #[cfg(unix)] - fn matches(&self, file_info: &walkdir::DirEntry, _: &mut super::MatcherIO) -> bool { + fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool { use nix::unistd::Gid; if file_info.path().is_symlink() { return false; } - let Ok(metadata) = file_info.path().metadata() else { + let Ok(metadata) = file_info.metadata() else { return true; }; @@ -105,7 +105,7 @@ impl Matcher for NoGroupMatcher { } #[cfg(windows)] - fn matches(&self, _file_info: &walkdir::DirEntry, _: &mut super::MatcherIO) -> bool { + fn matches(&self, _file_info: &WalkEntry, _: &mut MatcherIO) -> bool { false } } @@ -130,7 +130,7 @@ mod tests { let foo_path = temp_dir.path().join("foo"); let _ = File::create(foo_path).expect("create temp file"); let file_info = get_dir_entry_for(&temp_dir.path().to_string_lossy(), "foo"); - let file_gid = file_info.path().metadata().unwrap().gid(); + let file_gid = file_info.metadata().unwrap().gid(); let file_group = Group::from_gid(Gid::from_raw(file_gid)) .unwrap() .unwrap() diff --git a/src/find/matchers/lname.rs b/src/find/matchers/lname.rs index 95192392..dcbcd500 100644 --- a/src/find/matchers/lname.rs +++ b/src/find/matchers/lname.rs @@ -7,12 +7,10 @@ use std::io::{stderr, Write}; use std::path::PathBuf; -use walkdir::DirEntry; - use super::glob::Pattern; -use super::{Matcher, MatcherIO}; +use super::{Matcher, MatcherIO, WalkEntry}; -fn read_link_target(file_info: &DirEntry) -> Option { +fn read_link_target(file_info: &WalkEntry) -> Option { match file_info.path().read_link() { Ok(target) => Some(target), Err(err) => { @@ -47,7 +45,7 @@ impl LinkNameMatcher { } impl Matcher for LinkNameMatcher { - fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool { + fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool { if let Some(target) = read_link_target(file_info) { self.pattern.matches(&target.to_string_lossy()) } else { diff --git a/src/find/matchers/logical_matchers.rs b/src/find/matchers/logical_matchers.rs index 349b4876..8ec70f6f 100644 --- a/src/find/matchers/logical_matchers.rs +++ b/src/find/matchers/logical_matchers.rs @@ -11,9 +11,8 @@ //! to "-foo -o ( -bar -baz )", not "( -foo -o -bar ) -baz"). use std::error::Error; use std::path::Path; -use walkdir::DirEntry; -use super::{Matcher, MatcherIO}; +use super::{Matcher, MatcherIO, WalkEntry}; /// This matcher contains a collection of other matchers. A file only matches /// if it matches ALL the contained sub-matchers. For sub-matchers that have @@ -33,7 +32,7 @@ impl Matcher for AndMatcher { /// Returns true if all sub-matchers return true. Short-circuiting does take /// place. If the nth sub-matcher returns false, then we immediately return /// and don't make any further calls. - fn matches(&self, dir_entry: &DirEntry, matcher_io: &mut MatcherIO) -> bool { + fn matches(&self, dir_entry: &WalkEntry, matcher_io: &mut MatcherIO) -> bool { for matcher in &self.submatchers { if !matcher.matches(dir_entry, matcher_io) { return false; @@ -109,7 +108,7 @@ impl Matcher for OrMatcher { /// Returns true if any sub-matcher returns true. Short-circuiting does take /// place. If the nth sub-matcher returns true, then we immediately return /// and don't make any further calls. - fn matches(&self, dir_entry: &DirEntry, matcher_io: &mut MatcherIO) -> bool { + fn matches(&self, dir_entry: &WalkEntry, matcher_io: &mut MatcherIO) -> bool { for matcher in &self.submatchers { if matcher.matches(dir_entry, matcher_io) { return true; @@ -206,7 +205,7 @@ impl ListMatcher { impl Matcher for ListMatcher { /// Calls matches on all submatcher objects, with no short-circuiting. /// Returns the result of the call to the final submatcher - fn matches(&self, dir_entry: &DirEntry, matcher_io: &mut MatcherIO) -> bool { + fn matches(&self, dir_entry: &WalkEntry, matcher_io: &mut MatcherIO) -> bool { let mut rc = false; for matcher in &self.submatchers { rc = matcher.matches(dir_entry, matcher_io); @@ -311,7 +310,7 @@ impl ListMatcherBuilder { pub struct TrueMatcher; impl Matcher for TrueMatcher { - fn matches(&self, _dir_entry: &DirEntry, _: &mut MatcherIO) -> bool { + fn matches(&self, _dir_entry: &WalkEntry, _: &mut MatcherIO) -> bool { true } } @@ -320,7 +319,7 @@ impl Matcher for TrueMatcher { pub struct FalseMatcher; impl Matcher for FalseMatcher { - fn matches(&self, _dir_entry: &DirEntry, _: &mut MatcherIO) -> bool { + fn matches(&self, _dir_entry: &WalkEntry, _: &mut MatcherIO) -> bool { false } } @@ -339,7 +338,7 @@ impl NotMatcher { } impl Matcher for NotMatcher { - fn matches(&self, dir_entry: &DirEntry, matcher_io: &mut MatcherIO) -> bool { + fn matches(&self, dir_entry: &WalkEntry, matcher_io: &mut MatcherIO) -> bool { !self.submatcher.matches(dir_entry, matcher_io) } @@ -370,7 +369,7 @@ mod tests { pub struct HasSideEffects; impl Matcher for HasSideEffects { - fn matches(&self, _: &DirEntry, _: &mut MatcherIO) -> bool { + fn matches(&self, _: &WalkEntry, _: &mut MatcherIO) -> bool { false } @@ -383,7 +382,7 @@ mod tests { struct Counter(Rc>); impl Matcher for Counter { - fn matches(&self, _: &DirEntry, _: &mut MatcherIO) -> bool { + fn matches(&self, _: &WalkEntry, _: &mut MatcherIO) -> bool { *self.0.borrow_mut() += 1; true } diff --git a/src/find/matchers/mod.rs b/src/find/matchers/mod.rs index 108d5fc7..c7a5a6ac 100644 --- a/src/find/matchers/mod.rs +++ b/src/find/matchers/mod.rs @@ -7,6 +7,7 @@ mod access; mod delete; mod empty; +mod entry; pub mod exec; pub mod fs; mod glob; @@ -32,11 +33,10 @@ mod user; use ::regex::Regex; use chrono::{DateTime, Datelike, NaiveDateTime, Utc}; use fs::FileSystemMatcher; -use std::fs::File; +use std::fs::{File, Metadata}; use std::path::Path; use std::time::SystemTime; use std::{error::Error, str::FromStr}; -use walkdir::DirEntry; use self::access::AccessMatcher; use self::delete::DeleteMatcher; @@ -63,15 +63,80 @@ use self::time::{ FileAgeRangeMatcher, FileTimeMatcher, FileTimeType, NewerMatcher, NewerOptionMatcher, NewerOptionType, NewerTimeMatcher, }; -use self::type_matcher::TypeMatcher; +use self::type_matcher::{TypeMatcher, XtypeMatcher}; use self::user::{NoUserMatcher, UserMatcher}; use super::{Config, Dependencies}; +pub use entry::{FileType, WalkEntry, WalkError}; + +/// Symlink following mode. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum Follow { + /// Never follow symlinks (-P; default). + Never, + /// Follow symlinks on root paths only (-H). + Roots, + /// Always follow symlinks (-L). + Always, +} + +impl Follow { + /// Check whether to follow a path of the given depth. + pub fn follow_at_depth(self, depth: usize) -> bool { + match self { + Follow::Never => false, + Follow::Roots => depth == 0, + Follow::Always => true, + } + } + + /// Get metadata for a [WalkEntry]. + pub fn metadata(self, entry: &WalkEntry) -> Result { + if self.follow_at_depth(entry.depth()) == entry.follow() { + // Same follow flag, re-use cached metadata + entry.metadata().cloned() + } else if !entry.follow() && !entry.file_type().is_symlink() { + // Not a symlink, re-use cached metadata + entry.metadata().cloned() + } else if entry.follow() && entry.file_type().is_symlink() { + // Broken symlink, re-use cached metadata + entry.metadata().cloned() + } else { + self.metadata_at_depth(entry.path(), entry.depth()) + } + } + + /// Get metadata for a path from the command line. + pub fn root_metadata(self, path: impl AsRef) -> Result { + self.metadata_at_depth(path, 0) + } + + /// Get metadata for a path, following symlinks as necessary. + pub fn metadata_at_depth( + self, + path: impl AsRef, + depth: usize, + ) -> Result { + let path = path.as_ref(); + + if self.follow_at_depth(depth) { + match path.metadata().map_err(WalkError::from) { + Ok(meta) => return Ok(meta), + Err(e) if !e.is_not_found() => return Err(e), + _ => {} + } + } + + Ok(path.symlink_metadata()?) + } +} + /// Struct holding references to outputs and any inputs that can't be derived /// from the file/directory info. pub struct MatcherIO<'a> { should_skip_dir: bool, + exit_code: i32, quit: bool, deps: &'a dyn Dependencies, } @@ -79,9 +144,10 @@ pub struct MatcherIO<'a> { impl<'a> MatcherIO<'a> { pub fn new(deps: &dyn Dependencies) -> MatcherIO<'_> { MatcherIO { - deps, should_skip_dir: false, + exit_code: 0, quit: false, + deps, } } @@ -94,6 +160,15 @@ impl<'a> MatcherIO<'a> { self.should_skip_dir } + pub fn set_exit_code(&mut self, code: i32) { + self.exit_code = code; + } + + #[must_use] + pub fn exit_code(&self) -> i32 { + self.exit_code + } + pub fn quit(&mut self) { self.quit = true; } @@ -123,7 +198,7 @@ pub trait Matcher: 'static { } /// Returns whether the given file matches the object's predicate. - fn matches(&self, file_info: &DirEntry, matcher_io: &mut MatcherIO) -> bool; + fn matches(&self, entry: &WalkEntry, matcher_io: &mut MatcherIO) -> bool; /// Returns whether the matcher has any side-effects (e.g. executing a /// command, deleting a file). Iff no such matcher exists in the chain, then @@ -149,8 +224,8 @@ impl Matcher for Box { self } - fn matches(&self, file_info: &DirEntry, matcher_io: &mut MatcherIO) -> bool { - (**self).matches(file_info, matcher_io) + fn matches(&self, entry: &WalkEntry, matcher_io: &mut MatcherIO) -> bool { + (**self).matches(entry, matcher_io) } fn has_side_effects(&self) -> bool { @@ -440,6 +515,13 @@ fn build_matcher_tree( i += 1; Some(TypeMatcher::new(args[i])?.into_box()) } + "-xtype" => { + if i >= args.len() - 1 { + return Err(From::from(format!("missing argument to {}", args[i]))); + } + i += 1; + Some(XtypeMatcher::new(args[i])?.into_box()) + } "-fstype" => { if i >= args.len() - 1 { return Err(From::from(format!("missing argument to {}", args[i]))); @@ -457,7 +539,7 @@ fn build_matcher_tree( return Err(From::from(format!("missing argument to {}", args[i]))); } i += 1; - Some(NewerMatcher::new(args[i])?.into_box()) + Some(NewerMatcher::new(args[i], config.follow)?.into_box()) } "-mtime" | "-atime" | "-ctime" => { if i >= args.len() - 1 { @@ -562,7 +644,8 @@ fn build_matcher_tree( } i += 1; let path = args[i]; - let matcher = SameFileMatcher::new(path).map_err(|e| format!("{path}: {e}"))?; + let matcher = SameFileMatcher::new(path, config.follow) + .map_err(|e| format!("{path}: {e}"))?; Some(matcher.into_box()) } "-user" => { @@ -712,6 +795,21 @@ fn build_matcher_tree( return Ok((i, top_level_matcher.build())); } + "-follow" => { + // This option affects multiple matchers. + // 1. It will use noleaf by default. (but -noleaf No change of behavior) + // Unless -L or -H is specified: + // 2. changes the behaviour of the -newer predicate. + // 3. consideration applies to -newerXY, -anewer and -cnewer + // 4. -type predicate will always match against the type of + // the file that a symbolic link points to rather than the link itself. + // + // 5. causes the -lname and -ilname predicates always to return false. + // (unless they happen to match broken symbolic links) + config.follow = Follow::Always; + config.no_leaf_dirs = true; + Some(TrueMatcher.into_box()) + } "-daystart" => { config.today_start = true; Some(TrueMatcher.into_box()) @@ -825,25 +923,28 @@ mod tests { use super::*; use crate::find::tests::fix_up_slashes; use crate::find::tests::FakeDependencies; - use walkdir::WalkDir; - /// Helper function for tests to get a `DirEntry` object. directory should + /// Helper function for tests to get a [WalkEntry] object. root should /// probably be a string starting with `test_data/` (cargo's tests run with /// a working directory set to the root findutils folder). - pub fn get_dir_entry_for(directory: &str, filename: &str) -> DirEntry { - for wrapped_dir_entry in WalkDir::new(fix_up_slashes(directory)) { - let dir_entry = wrapped_dir_entry.unwrap(); - if dir_entry - .path() - .strip_prefix(directory) - .unwrap() - .to_string_lossy() - == fix_up_slashes(filename) - { - return dir_entry; - } - } - panic!("Couldn't find {filename} in {directory}"); + pub fn get_dir_entry_for(root: &str, path: &str) -> WalkEntry { + get_dir_entry_follow(root, path, Follow::Never) + } + + /// Get a [WalkEntry] with an explicit [Follow] flag. + pub fn get_dir_entry_follow(root: &str, path: &str, follow: Follow) -> WalkEntry { + let root = fix_up_slashes(root); + let root = Path::new(&root); + + let path = fix_up_slashes(path); + let path = if path.is_empty() { + root.to_owned() + } else { + root.join(path) + }; + + let depth = path.components().count() - root.components().count(); + WalkEntry::new(path, depth, follow) } #[test] @@ -1204,6 +1305,15 @@ mod tests { } } + #[test] + fn build_top_level_matcher_follow_config() { + let mut config = Config::default(); + + build_top_level_matcher(&["-follow"], &mut config).unwrap(); + + assert_eq!(config.follow, Follow::Always); + } + #[test] fn comparable_value_matches() { assert!( diff --git a/src/find/matchers/name.rs b/src/find/matchers/name.rs index a455937a..80b2be13 100644 --- a/src/find/matchers/name.rs +++ b/src/find/matchers/name.rs @@ -4,10 +4,8 @@ // license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -use walkdir::DirEntry; - use super::glob::Pattern; -use super::{Matcher, MatcherIO}; +use super::{Matcher, MatcherIO, WalkEntry}; /// This matcher makes a comparison of the name against a shell wildcard /// pattern. See `glob::Pattern` for details on the exact syntax. @@ -23,7 +21,7 @@ impl NameMatcher { } impl Matcher for NameMatcher { - fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool { + fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool { let name = file_info.file_name().to_string_lossy(); self.pattern.matches(&name) } diff --git a/src/find/matchers/path.rs b/src/find/matchers/path.rs index df31b263..c9db3498 100644 --- a/src/find/matchers/path.rs +++ b/src/find/matchers/path.rs @@ -4,10 +4,8 @@ // license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -use walkdir::DirEntry; - use super::glob::Pattern; -use super::{Matcher, MatcherIO}; +use super::{Matcher, MatcherIO, WalkEntry}; /// This matcher makes a comparison of the path against a shell wildcard /// pattern. See `glob::Pattern` for details on the exact syntax. @@ -23,7 +21,7 @@ impl PathMatcher { } impl Matcher for PathMatcher { - fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool { + fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool { let path = file_info.path().to_string_lossy(); self.pattern.matches(&path) } diff --git a/src/find/matchers/perm.rs b/src/find/matchers/perm.rs index 2ece4530..075d34b4 100644 --- a/src/find/matchers/perm.rs +++ b/src/find/matchers/perm.rs @@ -12,9 +12,8 @@ use std::error::Error; use std::io::{stderr, Write}; #[cfg(unix)] use uucore::mode::{parse_numeric, parse_symbolic}; -use walkdir::DirEntry; -use super::{Matcher, MatcherIO}; +use super::{Matcher, MatcherIO, WalkEntry}; #[derive(Clone, Copy, Debug, Eq, PartialEq)] #[cfg(unix)] @@ -101,7 +100,7 @@ impl PermMatcher { impl Matcher for PermMatcher { #[cfg(unix)] - fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool { + fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool { use std::os::unix::fs::PermissionsExt; match file_info.metadata() { Ok(metadata) => { @@ -127,7 +126,7 @@ impl Matcher for PermMatcher { } #[cfg(not(unix))] - fn matches(&self, _dummy_file_info: &DirEntry, _: &mut MatcherIO) -> bool { + fn matches(&self, _dummy_file_info: &WalkEntry, _: &mut MatcherIO) -> bool { writeln!( &mut stderr(), "Permission matching not available on this platform!" diff --git a/src/find/matchers/printer.rs b/src/find/matchers/printer.rs index 6e01a84f..52646e4b 100644 --- a/src/find/matchers/printer.rs +++ b/src/find/matchers/printer.rs @@ -4,14 +4,10 @@ // license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -use std::{ - fs::File, - io::{stderr, Write}, -}; +use std::fs::File; +use std::io::{stderr, Write}; -use walkdir::DirEntry; - -use super::{Matcher, MatcherIO}; +use super::{Matcher, MatcherIO, WalkEntry}; pub enum PrintDelimiter { Newline, @@ -41,7 +37,7 @@ impl Printer { } } - fn print(&self, file_info: &DirEntry, mut out: impl Write, print_error_message: bool) { + fn print(&self, file_info: &WalkEntry, mut out: impl Write, print_error_message: bool) { match write!( out, "{}{}", @@ -67,7 +63,7 @@ impl Printer { } impl Matcher for Printer { - fn matches(&self, file_info: &DirEntry, matcher_io: &mut MatcherIO) -> bool { + fn matches(&self, file_info: &WalkEntry, matcher_io: &mut MatcherIO) -> bool { if let Some(file) = &self.output_file { self.print(file_info, file, true); } else { @@ -86,7 +82,6 @@ impl Matcher for Printer { } #[cfg(test)] - mod tests { use super::*; use crate::find::matchers::tests::get_dir_entry_for; diff --git a/src/find/matchers/printf.rs b/src/find/matchers/printf.rs index 6929ccc7..831a3c58 100644 --- a/src/find/matchers/printf.rs +++ b/src/find/matchers/printf.rs @@ -4,15 +4,18 @@ // license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -use std::{borrow::Cow, error::Error, fs, path::Path, time::SystemTime}; +use std::borrow::Cow; +use std::error::Error; +use std::fs; +use std::path::Path; +use std::time::SystemTime; use chrono::{format::StrftimeItems, DateTime, Local}; -use once_cell::unsync::OnceCell; -use super::{Matcher, MatcherIO}; +use super::{FileType, Matcher, MatcherIO, WalkEntry, WalkError}; #[cfg(unix)] -use std::os::unix::prelude::{FileTypeExt, MetadataExt}; +use std::os::unix::prelude::MetadataExt; const STANDARD_BLOCK_SIZE: u64 = 512; @@ -340,7 +343,7 @@ impl FormatString { } } -fn get_starting_point(file_info: &walkdir::DirEntry) -> &Path { +fn get_starting_point(file_info: &WalkEntry) -> &Path { file_info .path() .ancestors() @@ -350,47 +353,23 @@ fn get_starting_point(file_info: &walkdir::DirEntry) -> &Path { .unwrap() } -fn format_non_link_file_type(file_type: fs::FileType) -> char { - if file_type.is_file() { - 'f' - } else if file_type.is_dir() { - 'd' - } else { - #[cfg(unix)] - if file_type.is_block_device() { - 'b' - } else if file_type.is_char_device() { - 'c' - } else if file_type.is_fifo() { - 'p' - } else if file_type.is_socket() { - 's' - } else { - 'U' - } - #[cfg(not(unix))] - 'U' +fn format_non_link_file_type(file_type: FileType) -> char { + match file_type { + FileType::Regular => 'f', + FileType::Directory => 'd', + FileType::BlockDevice => 'b', + FileType::CharDevice => 'c', + FileType::Fifo => 'p', + FileType::Socket => 's', + _ => 'U', } } fn format_directive<'entry>( - file_info: &'entry walkdir::DirEntry, + file_info: &'entry WalkEntry, directive: &FormatDirective, - meta_cell: &OnceCell, ) -> Result, Box> { - let meta = || { - meta_cell.get_or_try_init(|| { - if file_info.path_is_symlink() && !file_info.file_type().is_symlink() { - // The file_info already followed the symlink, meaning that the - // metadata will be for the target file, which isn't the - // behavior we want, so manually re-compute the metadata for the - // symlink itself instead. - file_info.path().symlink_metadata() - } else { - file_info.metadata().map_err(std::convert::Into::into) - } - }) - }; + let meta = || file_info.metadata(); // NOTE ON QUOTING: // GNU find's man page claims that several directives that print names (like @@ -556,17 +535,10 @@ fn format_directive<'entry>( FormatDirective::Type { follow_links } => if file_info.path_is_symlink() { if *follow_links { - match file_info.path().metadata() { - Ok(meta) => format_non_link_file_type(meta.file_type()), - Err(e) if e.kind() == std::io::ErrorKind::NotFound => 'N', - // The ErrorKinds corresponding to ELOOP and ENOTDIR are - // nightly-only: - // https://doc.rust-lang.org/std/io/enum.ErrorKind.html#variant.FilesystemLoop - // so we need to use the raw errno values instead. - #[cfg(unix)] - Err(e) if e.raw_os_error().unwrap_or(0) == uucore::libc::ENOTDIR => 'N', - #[cfg(unix)] - Err(e) if e.raw_os_error().unwrap_or(0) == uucore::libc::ELOOP => 'L', + match file_info.path().metadata().map_err(WalkError::from) { + Ok(meta) => format_non_link_file_type(meta.file_type().into()), + Err(e) if e.is_not_found() => 'N', + Err(e) if e.is_loop() => 'L', Err(_) => '?', } } else { @@ -610,11 +582,8 @@ impl Printf { } impl Matcher for Printf { - fn matches(&self, file_info: &walkdir::DirEntry, matcher_io: &mut MatcherIO) -> bool { + fn matches(&self, file_info: &WalkEntry, matcher_io: &mut MatcherIO) -> bool { let mut out = matcher_io.deps.get_output().borrow_mut(); - // The metadata is computed lazily, so that anything being printed - // without needing metadata won't incur any performance overhead. - let meta_cell = OnceCell::new(); for component in &self.format.components { match component { @@ -624,7 +593,7 @@ impl Matcher for Printf { directive, width, justify, - } => match format_directive(file_info, directive, &meta_cell) { + } => match format_directive(file_info, directive) { Ok(content) => { if let Some(width) = width { match justify { @@ -1110,13 +1079,13 @@ mod tests { let new_file_name = "newFile"; let file = File::create(temp_dir.path().join(new_file_name)).expect("create temp file"); - let file_info = get_dir_entry_for(&temp_dir_path, new_file_name); - let deps = FakeDependencies::new(); - - let mut perms = file_info.metadata().unwrap().permissions(); + let mut perms = file.metadata().unwrap().permissions(); perms.set_mode(0o755); file.set_permissions(perms).unwrap(); + let file_info = get_dir_entry_for(&temp_dir_path, new_file_name); + let deps = FakeDependencies::new(); + let matcher = Printf::new("%m %M").unwrap(); assert!(matcher.matches(&file_info, &mut deps.new_matcher_io())); assert_eq!("755 -rwxr-xr-x", deps.get_output_as_string()); diff --git a/src/find/matchers/prune.rs b/src/find/matchers/prune.rs index 623b417d..36e2cffa 100644 --- a/src/find/matchers/prune.rs +++ b/src/find/matchers/prune.rs @@ -4,9 +4,7 @@ // license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -use walkdir::DirEntry; - -use super::{Matcher, MatcherIO}; +use super::{Matcher, MatcherIO, WalkEntry}; /// This matcher checks the type of the file. pub struct PruneMatcher; @@ -18,7 +16,7 @@ impl PruneMatcher { } impl Matcher for PruneMatcher { - fn matches(&self, file_info: &DirEntry, matcher_io: &mut MatcherIO) -> bool { + fn matches(&self, file_info: &WalkEntry, matcher_io: &mut MatcherIO) -> bool { if file_info.file_type().is_dir() { matcher_io.mark_current_dir_to_be_skipped(); } diff --git a/src/find/matchers/quit.rs b/src/find/matchers/quit.rs index 36e1acfa..57e541ed 100644 --- a/src/find/matchers/quit.rs +++ b/src/find/matchers/quit.rs @@ -4,15 +4,13 @@ // license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -use walkdir::DirEntry; - -use super::{Matcher, MatcherIO}; +use super::{Matcher, MatcherIO, WalkEntry}; /// This matcher quits the search immediately. pub struct QuitMatcher; impl Matcher for QuitMatcher { - fn matches(&self, _: &DirEntry, matcher_io: &mut MatcherIO) -> bool { + fn matches(&self, _: &WalkEntry, matcher_io: &mut MatcherIO) -> bool { matcher_io.quit(); true } diff --git a/src/find/matchers/regex.rs b/src/find/matchers/regex.rs index 99590877..7c01fc79 100644 --- a/src/find/matchers/regex.rs +++ b/src/find/matchers/regex.rs @@ -8,7 +8,7 @@ use std::{error::Error, fmt, str::FromStr}; use onig::{Regex, RegexOptions, Syntax}; -use super::Matcher; +use super::{Matcher, MatcherIO, WalkEntry}; #[derive(Debug)] pub struct ParseRegexTypeError(String); @@ -111,7 +111,7 @@ impl RegexMatcher { } impl Matcher for RegexMatcher { - fn matches(&self, file_info: &walkdir::DirEntry, _: &mut super::MatcherIO) -> bool { + fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool { self.regex .is_match(file_info.path().to_string_lossy().as_ref()) } diff --git a/src/find/matchers/samefile.rs b/src/find/matchers/samefile.rs index 7d98d784..4b7d554e 100644 --- a/src/find/matchers/samefile.rs +++ b/src/find/matchers/samefile.rs @@ -3,7 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use super::Matcher; +use super::{Follow, Matcher, MatcherIO, WalkEntry, WalkError}; use std::error::Error; use std::path::Path; use uucore::fs::FileInformation; @@ -12,16 +12,32 @@ pub struct SameFileMatcher { info: FileInformation, } +/// Gets FileInformation, possibly following symlinks, but falling back on +/// broken links. +fn get_file_info(path: &Path, follow: bool) -> Result { + if follow { + let result = FileInformation::from_path(path, true).map_err(WalkError::from); + + match result { + Ok(info) => return Ok(info), + Err(e) if !e.is_not_found() => return Err(e), + _ => {} + } + } + + Ok(FileInformation::from_path(path, false)?) +} + impl SameFileMatcher { - pub fn new(path: impl AsRef) -> Result> { - let info = FileInformation::from_path(path, false)?; + pub fn new(path: impl AsRef, follow: Follow) -> Result> { + let info = get_file_info(path.as_ref(), follow != Follow::Never)?; Ok(Self { info }) } } impl Matcher for SameFileMatcher { - fn matches(&self, file_info: &walkdir::DirEntry, _matcher_io: &mut super::MatcherIO) -> bool { - if let Ok(info) = FileInformation::from_path(file_info.path(), false) { + fn matches(&self, file_info: &WalkEntry, _matcher_io: &mut MatcherIO) -> bool { + if let Ok(info) = get_file_info(file_info.path(), file_info.follow()) { info == self.info } else { false @@ -31,29 +47,61 @@ impl Matcher for SameFileMatcher { #[cfg(test)] mod tests { - use std::fs; + use super::*; + + use crate::find::matchers::tests::{get_dir_entry_follow, get_dir_entry_for}; + use crate::find::tests::FakeDependencies; + use std::fs::{self, File}; + use tempfile::Builder; #[test] fn test_samefile() { - use crate::find::{ - matchers::{samefile::SameFileMatcher, tests::get_dir_entry_for, Matcher}, - tests::FakeDependencies, - }; + let root = Builder::new().prefix("example").tempdir().unwrap(); + let root_path = root.path(); + + let file_path = root_path.join("file"); + File::create(&file_path).unwrap(); + + let link_path = root_path.join("link"); + fs::hard_link(&file_path, &link_path).unwrap(); - // remove file if hard link file exist. - // But you can't delete a file that doesn't exist, - // so ignore the error returned here. - let _ = fs::remove_file("test_data/links/hard_link"); + let other_path = root_path.join("other"); + File::create(&other_path).unwrap(); - assert!(SameFileMatcher::new("test_data/links/hard_link").is_err()); + let matcher = SameFileMatcher::new(&file_path, Follow::Never).unwrap(); - fs::hard_link("test_data/links/abbbc", "test_data/links/hard_link").unwrap(); + let root_path = root_path.to_string_lossy(); + let file_entry = get_dir_entry_for(&root_path, "file"); + let link_entry = get_dir_entry_for(&root_path, "link"); + let other_entry = get_dir_entry_for(&root_path, "other"); - let file = get_dir_entry_for("test_data/links", "abbbc"); - let hard_link_file = get_dir_entry_for("test_data/links", "hard_link"); - let matcher = SameFileMatcher::new(file.into_path()).unwrap(); + let deps = FakeDependencies::new(); + assert!(matcher.matches(&file_entry, &mut deps.new_matcher_io())); + assert!(matcher.matches(&link_entry, &mut deps.new_matcher_io())); + assert!(!matcher.matches(&other_entry, &mut deps.new_matcher_io())); + } + #[test] + fn test_follow() { let deps = FakeDependencies::new(); - assert!(matcher.matches(&hard_link_file, &mut deps.new_matcher_io())); + let matcher = SameFileMatcher::new("test_data/links/link-f", Follow::Roots).unwrap(); + + let entry = get_dir_entry_follow("test_data/links", "link-f", Follow::Never); + assert!(!matcher.matches(&entry, &mut deps.new_matcher_io())); + + let entry = get_dir_entry_follow("test_data/links", "abbbc", Follow::Never); + assert!(matcher.matches(&entry, &mut deps.new_matcher_io())); + + let entry = get_dir_entry_follow("test_data/links", "link-f", Follow::Roots); + assert!(!matcher.matches(&entry, &mut deps.new_matcher_io())); + + let entry = get_dir_entry_follow("test_data/links", "abbbc", Follow::Roots); + assert!(matcher.matches(&entry, &mut deps.new_matcher_io())); + + let entry = get_dir_entry_follow("test_data/links", "link-f", Follow::Always); + assert!(matcher.matches(&entry, &mut deps.new_matcher_io())); + + let entry = get_dir_entry_follow("test_data/links", "abbbc", Follow::Always); + assert!(matcher.matches(&entry, &mut deps.new_matcher_io())); } } diff --git a/src/find/matchers/size.rs b/src/find/matchers/size.rs index 5c6a9e1b..5d2d0615 100644 --- a/src/find/matchers/size.rs +++ b/src/find/matchers/size.rs @@ -7,9 +7,8 @@ use std::error::Error; use std::io::{stderr, Write}; use std::str::FromStr; -use walkdir::DirEntry; -use super::{ComparableValue, Matcher, MatcherIO}; +use super::{ComparableValue, Matcher, MatcherIO, WalkEntry}; #[derive(Clone, Copy, Debug)] enum Unit { @@ -83,7 +82,7 @@ impl SizeMatcher { } impl Matcher for SizeMatcher { - fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool { + fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool { match file_info.metadata() { Ok(metadata) => self .value_to_match diff --git a/src/find/matchers/stat.rs b/src/find/matchers/stat.rs index e48738c6..a4ddea34 100644 --- a/src/find/matchers/stat.rs +++ b/src/find/matchers/stat.rs @@ -6,9 +6,7 @@ use std::os::unix::fs::MetadataExt; -use walkdir::DirEntry; - -use super::{ComparableValue, Matcher, MatcherIO}; +use super::{ComparableValue, Matcher, MatcherIO, WalkEntry}; /// Inode number matcher. pub struct InodeMatcher { @@ -22,7 +20,7 @@ impl InodeMatcher { } impl Matcher for InodeMatcher { - fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool { + fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool { match file_info.metadata() { Ok(metadata) => self.ino.matches(metadata.ino()), Err(_) => false, @@ -42,7 +40,7 @@ impl LinksMatcher { } impl Matcher for LinksMatcher { - fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool { + fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool { match file_info.metadata() { Ok(metadata) => self.nlink.matches(metadata.nlink()), Err(_) => false, diff --git a/src/find/matchers/time.rs b/src/find/matchers/time.rs index 7c19e820..260219bc 100644 --- a/src/find/matchers/time.rs +++ b/src/find/matchers/time.rs @@ -8,12 +8,11 @@ use std::error::Error; use std::fs::{self, Metadata}; use std::io::{stderr, Write}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; -use walkdir::DirEntry; #[cfg(unix)] use std::os::unix::fs::MetadataExt; -use super::{ComparableValue, Matcher, MatcherIO}; +use super::{ComparableValue, Follow, Matcher, MatcherIO, WalkEntry}; const SECONDS_PER_DAY: i64 = 60 * 60 * 24; @@ -35,8 +34,8 @@ pub struct NewerMatcher { } impl NewerMatcher { - pub fn new(path_to_file: &str) -> Result> { - let metadata = fs::metadata(path_to_file)?; + pub fn new(path_to_file: &str, follow: Follow) -> Result> { + let metadata = follow.root_metadata(path_to_file)?; Ok(Self { given_modification_time: metadata.modified()?, }) @@ -44,7 +43,7 @@ impl NewerMatcher { /// Implementation of matches that returns a result, allowing use to use try! /// to deal with the errors. - fn matches_impl(&self, file_info: &DirEntry) -> Result> { + fn matches_impl(&self, file_info: &WalkEntry) -> Result> { let this_time = file_info.metadata()?.modified()?; // duration_since returns an Ok duration if this_time <= given_modification_time // and returns an Err (with a duration) otherwise. So if this_time > @@ -58,7 +57,7 @@ impl NewerMatcher { } impl Matcher for NewerMatcher { - fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool { + fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool { match self.matches_impl(file_info) { Err(e) => { writeln!( @@ -100,7 +99,7 @@ impl NewerOptionType { } } - fn get_file_time(self, metadata: Metadata) -> std::io::Result { + fn get_file_time(self, metadata: &Metadata) -> std::io::Result { match self { NewerOptionType::Accessed => metadata.accessed(), NewerOptionType::Birthed => metadata.created(), @@ -134,7 +133,7 @@ impl NewerOptionMatcher { }) } - fn matches_impl(&self, file_info: &DirEntry) -> Result> { + fn matches_impl(&self, file_info: &WalkEntry) -> Result> { let x_option_time = self.x_option.get_file_time(file_info.metadata()?)?; let y_option_time = self.y_option.get_file_time(file_info.metadata()?)?; @@ -150,7 +149,7 @@ impl NewerOptionMatcher { } impl Matcher for NewerOptionMatcher { - fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool { + fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool { match self.matches_impl(file_info) { Err(e) => { writeln!( @@ -184,7 +183,7 @@ impl NewerTimeMatcher { } } - fn matches_impl(&self, file_info: &DirEntry) -> Result> { + fn matches_impl(&self, file_info: &WalkEntry) -> Result> { let this_time = self.newer_time_type.get_file_time(file_info.metadata()?)?; let timestamp = this_time .duration_since(UNIX_EPOCH) @@ -201,7 +200,7 @@ impl NewerTimeMatcher { } impl Matcher for NewerTimeMatcher { - fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool { + fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool { match self.matches_impl(file_info) { Err(e) => { writeln!( @@ -256,7 +255,7 @@ pub enum FileTimeType { } impl FileTimeType { - fn get_file_time(self, metadata: Metadata) -> std::io::Result { + fn get_file_time(self, metadata: &Metadata) -> std::io::Result { match self { FileTimeType::Accessed => metadata.accessed(), FileTimeType::Changed => metadata.changed(), @@ -274,7 +273,7 @@ pub struct FileTimeMatcher { } impl Matcher for FileTimeMatcher { - fn matches(&self, file_info: &DirEntry, matcher_io: &mut MatcherIO) -> bool { + fn matches(&self, file_info: &WalkEntry, matcher_io: &mut MatcherIO) -> bool { let start_time = get_time(matcher_io, self.today_start); match self.matches_impl(file_info, start_time) { Err(e) => { @@ -298,7 +297,7 @@ impl FileTimeMatcher { /// to deal with the errors. fn matches_impl( &self, - file_info: &DirEntry, + file_info: &WalkEntry, start_time: SystemTime, ) -> Result> { let this_time = self.file_time_type.get_file_time(file_info.metadata()?)?; @@ -347,7 +346,7 @@ pub struct FileAgeRangeMatcher { } impl Matcher for FileAgeRangeMatcher { - fn matches(&self, file_info: &DirEntry, matcher_io: &mut MatcherIO) -> bool { + fn matches(&self, file_info: &WalkEntry, matcher_io: &mut MatcherIO) -> bool { let start_time = get_time(matcher_io, self.today_start); match self.matches_impl(file_info, start_time) { Err(e) => { @@ -369,7 +368,7 @@ impl Matcher for FileAgeRangeMatcher { impl FileAgeRangeMatcher { fn matches_impl( &self, - file_info: &DirEntry, + file_info: &WalkEntry, start_time: SystemTime, ) -> Result> { let this_time = self.file_time_type.get_file_time(file_info.metadata()?)?; @@ -421,9 +420,13 @@ mod tests { let new_file = get_dir_entry_for(&temp_dir_path, new_file_name); - let matcher_for_new = - NewerMatcher::new(&temp_dir.path().join(new_file_name).to_string_lossy()).unwrap(); - let matcher_for_old = NewerMatcher::new(&old_file.path().to_string_lossy()).unwrap(); + let matcher_for_new = NewerMatcher::new( + &temp_dir.path().join(new_file_name).to_string_lossy(), + Follow::Never, + ) + .unwrap(); + let matcher_for_old = + NewerMatcher::new(&old_file.path().to_string_lossy(), Follow::Never).unwrap(); let deps = FakeDependencies::new(); assert!( @@ -677,7 +680,7 @@ mod tests { /// helper function for `file_time_matcher_modified_changed_accessed` fn test_matcher_for_file_time_type( - file_info: &DirEntry, + file_info: &WalkEntry, file_time: SystemTime, file_time_type: FileTimeType, ) { @@ -733,30 +736,6 @@ mod tests { .matches(&new_file, &mut deps.new_matcher_io()), "new_file should be newer than old_dir" ); - - // After the file is deleted, DirEntry will point to an empty file location, - // thus causing the Matcher to generate an IO error after matching. - // - // Note: This test is nondeterministic on Windows, - // because fs::remove_file may not actually remove the file from - // the file system even if it returns Ok. - // Therefore, this test will only be performed on Linux/Unix. - let _ = fs::remove_file(&*new_file.path().to_string_lossy()); - - #[cfg(unix)] - { - let matcher = NewerOptionMatcher::new( - x_option.to_string(), - y_option.to_string(), - &old_file.path().to_string_lossy(), - ); - assert!( - !matcher - .unwrap() - .matches(&new_file, &mut deps.new_matcher_io()), - "The correct situation is that the file reading here cannot be successful." - ); - } } } } @@ -836,29 +815,6 @@ mod tests { inode_changed_matcher.matches(&file_info, &mut deps.new_matcher_io()), "file inode changed time should after 'std_time'" ); - - // After the file is deleted, DirEntry will point to an empty file location, - // thus causing the Matcher to generate an IO error after matching. - // - // Note: This test is nondeterministic on Windows, - // because fs::remove_file may not actually remove the file from - // the file system even if it returns Ok. - // Therefore, this test will only be performed on Linux/Unix. - let _ = fs::remove_file(&*file_info.path().to_string_lossy()); - - let matchers = [ - &created_matcher, - &accessed_matcher, - &modified_matcher, - &inode_changed_matcher, - ]; - - for matcher in &matchers { - assert!( - !matcher.matches(&file_info, &mut deps.new_matcher_io()), - "The correct situation is that the file reading here cannot be successful." - ); - } } } diff --git a/src/find/matchers/type_matcher.rs b/src/find/matchers/type_matcher.rs index b923215d..017839c7 100644 --- a/src/find/matchers/type_matcher.rs +++ b/src/find/matchers/type_matcher.rs @@ -5,70 +5,89 @@ // https://opensource.org/licenses/MIT. use std::error::Error; -use std::fs::FileType; -use walkdir::DirEntry; -#[cfg(unix)] -use std::os::unix::fs::FileTypeExt; - -use super::{Matcher, MatcherIO}; +use super::{FileType, Follow, Matcher, MatcherIO, WalkEntry}; /// This matcher checks the type of the file. pub struct TypeMatcher { - file_type_fn: fn(&FileType) -> bool, + file_type: FileType, +} + +fn parse(type_string: &str) -> Result> { + let file_type = match type_string { + "f" => FileType::Regular, + "d" => FileType::Directory, + "l" => FileType::Symlink, + "b" => FileType::BlockDevice, + "c" => FileType::CharDevice, + "p" => FileType::Fifo, // named pipe (FIFO) + "s" => FileType::Socket, + // D: door (Solaris) + "D" => { + return Err(From::from(format!( + "Type argument {type_string} not supported yet" + ))) + } + _ => { + return Err(From::from(format!( + "Unrecognised type argument {type_string}" + ))) + } + }; + Ok(file_type) } impl TypeMatcher { pub fn new(type_string: &str) -> Result> { - #[cfg(unix)] - let function = match type_string { - "f" => FileType::is_file, - "d" => FileType::is_dir, - "l" => FileType::is_symlink, - "b" => FileType::is_block_device, - "c" => FileType::is_char_device, - "p" => FileType::is_fifo, // named pipe (FIFO) - "s" => FileType::is_socket, - // D: door (Solaris) - "D" => { - return Err(From::from(format!( - "Type argument {type_string} not supported yet" - ))) - } - _ => { - return Err(From::from(format!( - "Unrecognised type argument {type_string}" - ))) - } - }; - #[cfg(not(unix))] - let function = match type_string { - "f" => FileType::is_file, - "d" => FileType::is_dir, - "l" => FileType::is_symlink, - _ => { - return Err(From::from(format!( - "Unrecognised type argument {}", - type_string - ))) - } - }; - Ok(Self { - file_type_fn: function, - }) + let file_type = parse(type_string)?; + Ok(Self { file_type }) } } impl Matcher for TypeMatcher { - fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool { - (self.file_type_fn)(&file_info.file_type()) + fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool { + file_info.file_type() == self.file_type + } +} + +/// Like [TypeMatcher], but toggles whether symlinks are followed. +pub struct XtypeMatcher { + file_type: FileType, +} + +impl XtypeMatcher { + pub fn new(type_string: &str) -> Result> { + let file_type = parse(type_string)?; + Ok(Self { file_type }) + } +} + +impl Matcher for XtypeMatcher { + fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool { + let follow = if file_info.follow() { + Follow::Never + } else { + Follow::Always + }; + + let file_type = follow + .metadata(file_info) + .map(|m| m.file_type()) + .map(FileType::from); + + match file_type { + Ok(file_type) if file_type == self.file_type => true, + // Since GNU find 4.10, ELOOP will match -xtype l + Err(e) if self.file_type.is_symlink() && e.is_loop() => true, + _ => false, + } } } #[cfg(test)] mod tests { use super::*; - use crate::find::matchers::tests::get_dir_entry_for; + use crate::find::matchers::tests::{get_dir_entry_follow, get_dir_entry_for}; use crate::find::tests::FakeDependencies; use std::io::ErrorKind; @@ -169,4 +188,51 @@ mod tests { let result = TypeMatcher::new("xxx"); assert!(result.is_err()); } + + #[cfg(unix)] + #[test] + fn xtype_file() { + let matcher = XtypeMatcher::new("f").unwrap(); + let deps = FakeDependencies::new(); + + let entry = get_dir_entry_follow("test_data/links", "abbbc", Follow::Never); + assert!(matcher.matches(&entry, &mut deps.new_matcher_io())); + + let entry = get_dir_entry_follow("test_data/links", "link-f", Follow::Never); + assert!(matcher.matches(&entry, &mut deps.new_matcher_io())); + + let entry = get_dir_entry_follow("test_data/links", "link-f", Follow::Always); + assert!(!matcher.matches(&entry, &mut deps.new_matcher_io())); + } + + #[cfg(unix)] + #[test] + fn xtype_link() { + let matcher = XtypeMatcher::new("l").unwrap(); + let deps = FakeDependencies::new(); + + let entry = get_dir_entry_follow("test_data/links", "abbbc", Follow::Never); + assert!(!matcher.matches(&entry, &mut deps.new_matcher_io())); + + let entry = get_dir_entry_follow("test_data/links", "link-f", Follow::Never); + assert!(!matcher.matches(&entry, &mut deps.new_matcher_io())); + + let entry = get_dir_entry_follow("test_data/links", "link-missing", Follow::Never); + assert!(matcher.matches(&entry, &mut deps.new_matcher_io())); + + let entry = get_dir_entry_follow("test_data/links", "link-notdir", Follow::Never); + assert!(matcher.matches(&entry, &mut deps.new_matcher_io())); + + let entry = get_dir_entry_follow("test_data/links", "link-f", Follow::Always); + assert!(matcher.matches(&entry, &mut deps.new_matcher_io())); + } + + #[cfg(unix)] + #[test] + fn xtype_loop() { + let matcher = XtypeMatcher::new("l").unwrap(); + let entry = get_dir_entry_for("test_data/links", "link-loop"); + let deps = FakeDependencies::new(); + assert!(matcher.matches(&entry, &mut deps.new_matcher_io())); + } } diff --git a/src/find/matchers/user.rs b/src/find/matchers/user.rs index 0b881db1..f1a321c7 100644 --- a/src/find/matchers/user.rs +++ b/src/find/matchers/user.rs @@ -3,7 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use super::Matcher; +use super::{Matcher, MatcherIO, WalkEntry}; #[cfg(unix)] use nix::unistd::User; @@ -57,8 +57,8 @@ impl UserMatcher { impl Matcher for UserMatcher { #[cfg(unix)] - fn matches(&self, file_info: &walkdir::DirEntry, _: &mut super::MatcherIO) -> bool { - let Ok(metadata) = file_info.path().metadata() else { + fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool { + let Ok(metadata) = file_info.metadata() else { return false; }; @@ -71,7 +71,7 @@ impl Matcher for UserMatcher { } #[cfg(windows)] - fn matches(&self, _file_info: &walkdir::DirEntry, _: &mut super::MatcherIO) -> bool { + fn matches(&self, _file_info: &WalkEntry, _: &mut MatcherIO) -> bool { false } } @@ -80,14 +80,14 @@ pub struct NoUserMatcher {} impl Matcher for NoUserMatcher { #[cfg(unix)] - fn matches(&self, file_info: &walkdir::DirEntry, _: &mut super::MatcherIO) -> bool { + fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool { use nix::unistd::Uid; if file_info.path().is_symlink() { return false; } - let Ok(metadata) = file_info.path().metadata() else { + let Ok(metadata) = file_info.metadata() else { return true; }; @@ -103,7 +103,7 @@ impl Matcher for NoUserMatcher { } #[cfg(windows)] - fn matches(&self, _file_info: &walkdir::DirEntry, _: &mut super::MatcherIO) -> bool { + fn matches(&self, _file_info: &WalkEntry, _: &mut MatcherIO) -> bool { false } } @@ -128,7 +128,7 @@ mod tests { let foo_path = temp_dir.path().join("foo"); let _ = File::create(foo_path).expect("create temp file"); let file_info = get_dir_entry_for(&temp_dir.path().to_string_lossy(), "foo"); - let file_uid = file_info.path().metadata().unwrap().uid(); + let file_uid = file_info.metadata().unwrap().uid(); let file_user = User::from_uid(Uid::from_raw(file_uid)) .unwrap() .unwrap() diff --git a/src/find/mod.rs b/src/find/mod.rs index 54c8f65c..649550ca 100644 --- a/src/find/mod.rs +++ b/src/find/mod.rs @@ -6,6 +6,7 @@ pub mod matchers; +use matchers::{Follow, WalkEntry}; use std::cell::RefCell; use std::error::Error; use std::io::{stderr, stdout, Write}; @@ -23,6 +24,7 @@ pub struct Config { version_requested: bool, today_start: bool, no_leaf_dirs: bool, + follow: Follow, } impl Default for Config { @@ -40,6 +42,7 @@ impl Default for Config { // and this configuration field will exist as // a compatibility item for GNU findutils. no_leaf_dirs: false, + follow: Follow::Never, } } } @@ -101,9 +104,9 @@ fn parse_args(args: &[&str]) -> Result> { "-O0" | "-O1" | "-O2" | "-O3" => { // GNU find optimization level flag (ignored) } - "-P" => { - // Never follow symlinks (the default) - } + "-H" => config.follow = Follow::Roots, + "-L" => config.follow = Follow::Always, + "-P" => config.follow = Follow::Never, "--" => { // End of flags i += 1; @@ -141,31 +144,36 @@ fn process_dir( deps: &dyn Dependencies, matcher: &dyn matchers::Matcher, quit: &mut bool, -) -> u64 { - let mut found_count: u64 = 0; +) -> i32 { let mut walkdir = WalkDir::new(dir) .contents_first(config.depth_first) .max_depth(config.max_depth) .min_depth(config.min_depth) - .same_file_system(config.same_file_system); + .same_file_system(config.same_file_system) + .follow_links(config.follow == Follow::Always) + .follow_root_links(config.follow != Follow::Never); if config.sorted_output { walkdir = walkdir.sort_by(|a, b| a.file_name().cmp(b.file_name())); } + let mut ret = 0; + // Slightly yucky loop handling here :-(. See docs for // WalkDirIterator::skip_current_dir for explanation. let mut it = walkdir.into_iter(); while let Some(result) = it.next() { - match result { + match WalkEntry::from_walkdir(result, config.follow) { Err(err) => { - uucore::error::set_exit_code(1); - writeln!(&mut stderr(), "Error: {dir}: {err}").unwrap() + ret = 1; + writeln!(&mut stderr(), "Error: {err}").unwrap() } Ok(entry) => { let mut matcher_io = matchers::MatcherIO::new(deps); - if matcher.matches(&entry, &mut matcher_io) { - found_count += 1; + matcher.matches(&entry, &mut matcher_io); + match matcher_io.exit_code() { + 0 => {} + code => ret = code, } if matcher_io.should_quit() { *quit = true; @@ -177,10 +185,11 @@ fn process_dir( } } } - found_count + + ret } -fn do_find(args: &[&str], deps: &dyn Dependencies) -> Result> { +fn do_find(args: &[&str], deps: &dyn Dependencies) -> Result> { let paths_and_matcher = parse_args(args)?; if paths_and_matcher.config.help_requested { print_help(); @@ -191,21 +200,25 @@ fn do_find(args: &[&str], deps: &dyn Dependencies) -> Result return Ok(0); } - let mut found_count: u64 = 0; + let mut ret = 0; let mut quit = false; for path in paths_and_matcher.paths { - found_count += process_dir( + let dir_ret = process_dir( &path, &paths_and_matcher.config, deps, &*paths_and_matcher.matcher, &mut quit, ); + if dir_ret != 0 { + ret = dir_ret; + } if quit { break; } } - Ok(found_count) + + Ok(ret) } fn print_help() { @@ -265,7 +278,7 @@ fn print_version() { /// the name of the executable. pub fn find_main(args: &[&str], deps: &dyn Dependencies) -> i32 { match do_find(&args[1..], deps) { - Ok(_) => uucore::error::get_exit_code(), + Ok(ret) => ret, Err(e) => { writeln!(&mut stderr(), "Error: {e}").unwrap(); 1 @@ -389,9 +402,22 @@ mod tests { assert_eq!(parsed_info.paths, ["."]); } + #[test] + fn parse_h_flag() { + let parsed_info = super::parse_args(&["-H"]).expect("parsing should succeed"); + assert_eq!(parsed_info.config.follow, Follow::Roots); + } + + #[test] + fn parse_l_flag() { + let parsed_info = super::parse_args(&["-L"]).expect("parsing should succeed"); + assert_eq!(parsed_info.config.follow, Follow::Always); + } + #[test] fn parse_p_flag() { - super::parse_args(&["-P"]).expect("parsing should succeed"); + let parsed_info = super::parse_args(&["-P"]).expect("parsing should succeed"); + assert_eq!(parsed_info.config.follow, Follow::Never); } #[test] @@ -921,6 +947,20 @@ mod tests { ); assert_eq!(rc, 0); + + let arg = &format!("-follow -newer{x}{y}").to_string(); + let deps = FakeDependencies::new(); + let rc = find_main( + &[ + "find", + "./test_data/simple/subdir", + arg, + "./test_data/simple/subdir/ABBBC", + ], + &deps, + ); + + assert_eq!(rc, 0); } } } @@ -1050,10 +1090,6 @@ mod tests { assert_eq!(rc, 1); - // Reset the exit code global variable in case we run another test after this one - // See https://github.com/uutils/coreutils/issues/5777 - uucore::error::set_exit_code(0); - if path.exists() { let _result = fs::create_dir(path); // Remove the unreadable and writable status of the file to avoid affecting other tests. @@ -1317,4 +1353,80 @@ mod tests { let _ = fs::remove_file("test_data/find_fprint"); } + + #[test] + fn test_follow() { + let deps = FakeDependencies::new(); + let rc = find_main(&["find", "./test_data/simple", "-follow"], &deps); + assert_eq!(rc, 0); + } + + #[cfg(unix)] + #[test] + fn test_h_flag() { + let deps = FakeDependencies::new(); + + let rc = find_main( + &["find", "-H", &fix_up_slashes("./test_data/links/link-d")], + &deps, + ); + + assert_eq!(rc, 0); + assert_eq!( + deps.get_output_as_string(), + fix_up_slashes( + "./test_data/links/link-d\n\ + ./test_data/links/link-d/test\n" + ) + ); + } + + #[cfg(unix)] + #[test] + fn test_l_flag() { + let deps = FakeDependencies::new(); + + let rc = find_main( + &[ + "find", + "-L", + &fix_up_slashes("./test_data/links"), + "-sorted", + ], + &deps, + ); + + assert_eq!(rc, 1); + assert_eq!( + deps.get_output_as_string(), + fix_up_slashes( + "./test_data/links\n\ + ./test_data/links/abbbc\n\ + ./test_data/links/link-d\n\ + ./test_data/links/link-d/test\n\ + ./test_data/links/link-f\n\ + ./test_data/links/link-missing\n\ + ./test_data/links/link-notdir\n\ + ./test_data/links/subdir\n\ + ./test_data/links/subdir/test\n" + ) + ); + } + + #[cfg(unix)] + #[test] + fn test_p_flag() { + let deps = FakeDependencies::new(); + + let rc = find_main( + &["find", "-P", &fix_up_slashes("./test_data/links/link-d")], + &deps, + ); + + assert_eq!(rc, 0); + assert_eq!( + deps.get_output_as_string(), + fix_up_slashes("./test_data/links/link-d\n") + ); + } } diff --git a/tests/common/test_helpers.rs b/tests/common/test_helpers.rs index d39d8609..0f69a305 100644 --- a/tests/common/test_helpers.rs +++ b/tests/common/test_helpers.rs @@ -7,10 +7,10 @@ use std::cell::RefCell; use std::env; use std::io::{Cursor, Read, Write}; +use std::path::Path; use std::time::SystemTime; -use walkdir::{DirEntry, WalkDir}; -use findutils::find::matchers::MatcherIO; +use findutils::find::matchers::{Follow, MatcherIO, WalkEntry}; use findutils::find::Dependencies; /// A copy of `find::tests::FakeDependencies`. @@ -80,15 +80,21 @@ pub fn fix_up_slashes(path: &str) -> String { path.to_string() } -/// A copy of `find::tests::FakeDependencies`. +/// A copy of `find::matchers::tests::get_dir_entry_for`. /// TODO: find out how to share #[cfg(test)] functions/structs between unit /// and integration tests. -pub fn get_dir_entry_for(directory: &str, filename: &str) -> DirEntry { - for wrapped_dir_entry in WalkDir::new(fix_up_slashes(directory)) { - let dir_entry = wrapped_dir_entry.unwrap(); - if dir_entry.file_name().to_string_lossy() == filename { - return dir_entry; - } - } - panic!("Couldn't find {directory} in {filename}"); +pub fn get_dir_entry_for(root: &str, path: &str) -> WalkEntry { + let root = fix_up_slashes(root); + let root = Path::new(&root); + + let path = fix_up_slashes(path); + let path = if path.is_empty() { + root.to_owned() + } else { + root.join(path) + }; + + let depth = path.components().count() - root.components().count(); + + WalkEntry::new(path, depth, Follow::Never) } diff --git a/tests/exec_unit_tests.rs b/tests/exec_unit_tests.rs index 409a2f0b..7f1502c4 100644 --- a/tests/exec_unit_tests.rs +++ b/tests/exec_unit_tests.rs @@ -12,7 +12,6 @@ use std::env; use std::fs::File; use std::io::Read; use tempfile::Builder; -use walkdir::WalkDir; use common::test_helpers::{ fix_up_slashes, get_dir_entry_for, path_to_testing_commandline, FakeDependencies, @@ -125,11 +124,7 @@ fn execdir_in_current_directory() { .unwrap(); let temp_dir_path = temp_dir.path().to_string_lossy(); - let current_dir_entry = WalkDir::new(".") - .into_iter() - .next() - .expect("iterator was empty") - .expect("result wasn't OK"); + let current_dir_entry = get_dir_entry_for(".", ""); let matcher = SingleExecMatcher::new( &path_to_testing_commandline(), &[temp_dir_path.as_ref(), "abc", "{}", "xyz"], @@ -166,11 +161,7 @@ fn execdir_in_root_directory() { .ancestors() .last() .expect("current directory has no root"); - let root_dir_entry = WalkDir::new(root_dir) - .into_iter() - .next() - .expect("iterator was empty") - .expect("result wasn't OK"); + let root_dir_entry = get_dir_entry_for(root_dir.to_str().unwrap(), ""); let matcher = SingleExecMatcher::new( &path_to_testing_commandline(), diff --git a/tests/find_cmd_tests.rs b/tests/find_cmd_tests.rs index 364d484f..bc47c7d8 100644 --- a/tests/find_cmd_tests.rs +++ b/tests/find_cmd_tests.rs @@ -530,6 +530,7 @@ fn find_accessible() { .stdout(predicate::str::contains("abbbc").not()); } +#[serial(working_dir)] #[test] fn find_time() { let args = ["1", "+1", "-1"]; @@ -545,7 +546,7 @@ fn find_time() { args.iter().for_each(|arg| { Command::cargo_bin("find") .expect("found binary") - .args([".", flag, arg]) + .args(["./test_data/simple", flag, arg]) .assert() .success() .stderr(predicate::str::is_empty()); @@ -908,6 +909,8 @@ fn find_samefile() { .failure() .stdout(predicate::str::is_empty()) .stderr(predicate::str::contains("not-exist-file")); + + fs::remove_file("test_data/links/hard_link").unwrap(); } #[test] @@ -968,3 +971,15 @@ fn find_fprint() { let _ = fs::remove_file("test_data/find_fprint"); } + +#[test] +#[serial(working_dir)] +fn find_follow() { + Command::cargo_bin("find") + .expect("found binary") + .args(["test_data/links/link-f", "-follow"]) + .assert() + .success() + .stdout(predicate::str::contains("test_data/links/link-f")) + .stderr(predicate::str::is_empty()); +}