Wrap walkdir::DirEntry in a new type (#436)

* Add -follow support.

* tests/find: Serialize find_time()

find_time() relies on the working directory, but e.g.
delete_on_dot_dir() will temporarily change directories, causing
find_time() to fail when run in parallel.

* find: Don't use uutils::error::set_exit_code()

The global exit code can polute the results of other tests.

Link: https://github.com/uutils/coreutils/issues/5777

* find: New WalkEntry wrapper

The new type wraps DirEntry when possible, but also lets us pass a valid
entry to matchers when walkdir returns a broken symlink error.  It also
implements a Metadata cache (part of #430).

* find: Implement -H, -L, -P flags

* find: Fix -follow -samefile

* find: Fix -follow -newer

* find: Implement -xtype

* find: Fix -delete error handling

---------

Co-authored-by: hanbings <hanbings@hanbings.io>
This commit is contained in:
Tavian Barnes
2024-08-15 04:01:19 +08:00
committed by GitHub
co-authored by hanbings
parent 465856ce24
commit a7a73c325d
29 changed files with 948 additions and 356 deletions
Generated
+1 -1
View File
@@ -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"
+2 -3
View File
@@ -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 {
+9 -11
View File
@@ -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
}
+2 -2
View File
@@ -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,
+343
View File
@@ -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<fs::FileType> 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<PathBuf>,
/// The depth below the root path, if known.
depth: Option<usize>,
/// The io::Error::raw_os_error(), if known.
raw: Option<i32>,
}
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<usize> {
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<io::Error> 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<walkdir::Error> 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<WalkError> 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<Result<Metadata, WalkError>>,
}
impl WalkEntry {
/// Create a new WalkEntry for a specific file.
pub fn new(path: impl Into<PathBuf>, 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<DirEntry>,
follow: Follow,
) -> Result<WalkEntry, WalkError> {
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<Metadata, WalkError> {
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(),
}
}
}
+2 -3
View File
@@ -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<OsString>),
@@ -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() {
+2 -2
View File
@@ -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
+8 -8
View File
@@ -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()
+3 -5
View File
@@ -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<PathBuf> {
fn read_link_target(file_info: &WalkEntry) -> Option<PathBuf> {
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 {
+9 -10
View File
@@ -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<RefCell<u32>>);
impl Matcher for Counter {
fn matches(&self, _: &DirEntry, _: &mut MatcherIO) -> bool {
fn matches(&self, _: &WalkEntry, _: &mut MatcherIO) -> bool {
*self.0.borrow_mut() += 1;
true
}
+135 -25
View File
@@ -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<Metadata, WalkError> {
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<Path>) -> Result<Metadata, WalkError> {
self.metadata_at_depth(path, 0)
}
/// Get metadata for a path, following symlinks as necessary.
pub fn metadata_at_depth(
self,
path: impl AsRef<Path>,
depth: usize,
) -> Result<Metadata, WalkError> {
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<dyn Matcher> {
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!(
+2 -4
View File
@@ -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)
}
+2 -4
View File
@@ -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)
}
+3 -4
View File
@@ -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!"
+5 -10
View File
@@ -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;
+29 -60
View File
@@ -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<fs::Metadata>,
) -> Result<Cow<'entry, str>, Box<dyn Error>> {
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());
+2 -4
View File
@@ -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();
}
+2 -4
View File
@@ -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
}
+2 -2
View File
@@ -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())
}
+68 -20
View File
@@ -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<FileInformation, WalkError> {
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<Path>) -> Result<Self, Box<dyn Error>> {
let info = FileInformation::from_path(path, false)?;
pub fn new(path: impl AsRef<Path>, follow: Follow) -> Result<Self, Box<dyn Error>> {
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();
// 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 file_path = root_path.join("file");
File::create(&file_path).unwrap();
assert!(SameFileMatcher::new("test_data/links/hard_link").is_err());
let link_path = root_path.join("link");
fs::hard_link(&file_path, &link_path).unwrap();
fs::hard_link("test_data/links/abbbc", "test_data/links/hard_link").unwrap();
let other_path = root_path.join("other");
File::create(&other_path).unwrap();
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 matcher = SameFileMatcher::new(&file_path, Follow::Never).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 deps = FakeDependencies::new();
assert!(matcher.matches(&hard_link_file, &mut deps.new_matcher_io()));
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();
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()));
}
}

Some files were not shown because too many files have changed in this diff Show More