diff --git a/src/find/matchers/mod.rs b/src/find/matchers/mod.rs index ef575cf..abcda37 100644 --- a/src/find/matchers/mod.rs +++ b/src/find/matchers/mod.rs @@ -20,6 +20,7 @@ mod prune; mod quit; mod regex; mod size; +mod stat; mod time; mod type_matcher; @@ -46,6 +47,7 @@ use self::prune::PruneMatcher; use self::quit::QuitMatcher; use self::regex::RegexMatcher; use self::size::SizeMatcher; +use self::stat::{InodeMatcher, LinksMatcher}; use self::time::{FileTimeMatcher, FileTimeType, NewerMatcher}; use self::type_matcher::TypeMatcher; @@ -401,6 +403,22 @@ fn build_matcher_tree( .into_box(), ) } + "-inum" => { + if i >= args.len() - 1 { + return Err(From::from(format!("missing argument to {}", args[i]))); + } + let inum = convert_arg_to_comparable_value(args[i], args[i + 1])?; + i += 1; + Some(InodeMatcher::new(inum)?.into_box()) + } + "-links" => { + if i >= args.len() - 1 { + return Err(From::from(format!("missing argument to {}", args[i]))); + } + let inum = convert_arg_to_comparable_value(args[i], args[i + 1])?; + i += 1; + Some(LinksMatcher::new(inum)?.into_box()) + } "-executable" => Some(AccessMatcher::Executable.into_box()), "-perm" => { if i >= args.len() - 1 { diff --git a/src/find/matchers/stat.rs b/src/find/matchers/stat.rs new file mode 100644 index 0000000..419aeec --- /dev/null +++ b/src/find/matchers/stat.rs @@ -0,0 +1,138 @@ +// Copyright 2022 Tavian Barnes +// +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +#[cfg(unix)] +use std::os::unix::fs::MetadataExt; + +use std::error::Error; +use walkdir::DirEntry; + +use super::{ComparableValue, Matcher, MatcherIO}; + +/// Inode number matcher. +pub struct InodeMatcher { + ino: ComparableValue, +} + +impl InodeMatcher { + #[cfg(unix)] + pub fn new(ino: ComparableValue) -> Result> { + Ok(Self { ino }) + } + + #[cfg(not(unix))] + pub fn new(_ino: ComparableValue) -> Result> { + Err(From::from( + "Inode numbers are not available on this platform", + )) + } +} + +impl Matcher for InodeMatcher { + #[cfg(unix)] + fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool { + match file_info.metadata() { + Ok(metadata) => self.ino.matches(metadata.ino()), + Err(_) => false, + } + } + + #[cfg(not(unix))] + fn matches(&self, _: &DirEntry, _: &mut MatcherIO) -> bool { + unreachable!("Inode numbers are not available on this platform") + } +} + +/// Link count matcher. +pub struct LinksMatcher { + nlink: ComparableValue, +} + +impl LinksMatcher { + #[cfg(unix)] + pub fn new(nlink: ComparableValue) -> Result> { + Ok(Self { nlink }) + } + + #[cfg(not(unix))] + pub fn new(_nlink: ComparableValue) -> Result> { + Err(From::from("Link counts are not available on this platform")) + } +} + +impl Matcher for LinksMatcher { + #[cfg(unix)] + fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool { + match file_info.metadata() { + Ok(metadata) => self.nlink.matches(metadata.nlink()), + Err(_) => false, + } + } + + #[cfg(not(unix))] + fn matches(&self, _: &DirEntry, _: &mut MatcherIO) -> bool { + unreachable!("Link counts are not available on this platform") + } +} + +#[cfg(test)] +#[cfg(unix)] +mod tests { + use super::*; + + use crate::find::matchers::tests::get_dir_entry_for; + use crate::find::matchers::Matcher; + use crate::find::tests::FakeDependencies; + + #[test] + fn inode_matcher() { + let file_info = get_dir_entry_for("test_data/simple", "abbbc"); + let metadata = file_info.metadata().unwrap(); + let deps = FakeDependencies::new(); + + let matcher = InodeMatcher::new(ComparableValue::EqualTo(metadata.ino())).unwrap(); + assert!( + matcher.matches(&file_info, &mut deps.new_matcher_io()), + "inode number should match" + ); + + let matcher = InodeMatcher::new(ComparableValue::LessThan(metadata.ino())).unwrap(); + assert!( + !matcher.matches(&file_info, &mut deps.new_matcher_io()), + "inode number should not match" + ); + + let matcher = InodeMatcher::new(ComparableValue::MoreThan(metadata.ino())).unwrap(); + assert!( + !matcher.matches(&file_info, &mut deps.new_matcher_io()), + "inode number should not match" + ); + } + + #[test] + fn links_matcher() { + let file_info = get_dir_entry_for("test_data/simple", "abbbc"); + let deps = FakeDependencies::new(); + + let matcher = LinksMatcher::new(ComparableValue::EqualTo(1)).unwrap(); + assert!( + matcher.matches(&file_info, &mut deps.new_matcher_io()), + "link count should match" + ); + + let matcher = LinksMatcher::new(ComparableValue::LessThan(1)).unwrap(); + assert!( + !matcher.matches(&file_info, &mut deps.new_matcher_io()), + "link count should not match" + ); + + let matcher = LinksMatcher::new(ComparableValue::MoreThan(1)).unwrap(); + assert!( + !matcher.matches(&file_info, &mut deps.new_matcher_io()), + "link count should not match" + ); + } +} diff --git a/tests/find_cmd_tests.rs b/tests/find_cmd_tests.rs index e64db3a..d2821a4 100644 --- a/tests/find_cmd_tests.rs +++ b/tests/find_cmd_tests.rs @@ -407,6 +407,40 @@ fn find_perm() { .success(); } +#[cfg(unix)] +#[serial(working_dir)] +#[test] +fn find_inum() { + use std::fs::metadata; + use std::os::unix::fs::MetadataExt; + + let inum = metadata("test_data/simple/abbbc") + .expect("metadata for abbbc") + .ino() + .to_string(); + + Command::cargo_bin("find") + .expect("found binary") + .args(&["test_data", "-inum", &inum]) + .assert() + .success() + .stderr(predicate::str::is_empty()) + .stdout(predicate::str::contains("abbbc")); +} + +#[cfg(unix)] +#[serial(working_dir)] +#[test] +fn find_links() { + Command::cargo_bin("find") + .expect("found binary") + .args(&["test_data", "-links", "1"]) + .assert() + .success() + .stderr(predicate::str::is_empty()) + .stdout(predicate::str::contains("abbbc")); +} + #[serial(working_dir)] #[test] fn find_mount_xdev() {