Skip to content

Commit

Permalink
Implement -daystart (#413)
Browse files Browse the repository at this point in the history
  • Loading branch information
hanbings authored Jul 21, 2024
1 parent 4807264 commit 1b9904e
Show file tree
Hide file tree
Showing 4 changed files with 205 additions and 20 deletions.
11 changes: 9 additions & 2 deletions src/find/matchers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,7 @@ fn build_matcher_tree(
};
let days = convert_arg_to_comparable_value(args[i], args[i + 1])?;
i += 1;
Some(FileTimeMatcher::new(file_time_type, days).into_box())
Some(FileTimeMatcher::new(file_time_type, days, config.today_start).into_box())
}
"-amin" | "-cmin" | "-mmin" => {
if i >= args.len() - 1 {
Expand All @@ -470,7 +470,10 @@ fn build_matcher_tree(
};
let minutes = convert_arg_to_comparable_value(args[i], args[i + 1])?;
i += 1;
Some(FileAgeRangeMatcher::new(file_time_type, minutes).into_box())
Some(
FileAgeRangeMatcher::new(file_time_type, minutes, config.today_start)
.into_box(),
)
}
"-size" => {
if i >= args.len() - 1 {
Expand Down Expand Up @@ -692,6 +695,10 @@ fn build_matcher_tree(

return Ok((i, top_level_matcher.build()));
}
"-daystart" => {
config.today_start = true;
None
}
"-noleaf" => {
// No change of behavior
config.no_leaf_dirs = true;
Expand Down
151 changes: 133 additions & 18 deletions src/find/matchers/time.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
use std::error::Error;
use std::fs::{self, Metadata};
use std::io::{stderr, Write};
use std::time::{SystemTime, UNIX_EPOCH};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use walkdir::DirEntry;

#[cfg(unix)]
Expand All @@ -17,6 +17,18 @@ use super::{ComparableValue, Matcher, MatcherIO};

const SECONDS_PER_DAY: i64 = 60 * 60 * 24;

fn get_time(matcher_io: &mut MatcherIO, today_start: bool) -> SystemTime {
if today_start {
// the time at 00:00:00 of today
let duration = matcher_io.now().duration_since(UNIX_EPOCH).unwrap();
let seconds = duration.as_secs();
let midnight_seconds = seconds - (seconds % 86400);
UNIX_EPOCH + Duration::from_secs(midnight_seconds)
} else {
matcher_io.now()
}
}

/// This matcher checks whether a file is newer than the file the matcher is initialized with.
pub struct NewerMatcher {
given_modification_time: SystemTime,
Expand Down Expand Up @@ -258,11 +270,13 @@ impl FileTimeType {
pub struct FileTimeMatcher {
days: ComparableValue,
file_time_type: FileTimeType,
today_start: bool,
}

impl Matcher for FileTimeMatcher {
fn matches(&self, file_info: &DirEntry, matcher_io: &mut MatcherIO) -> bool {
match self.matches_impl(file_info, matcher_io.now()) {
let start_time = get_time(matcher_io, self.today_start);
match self.matches_impl(file_info, start_time) {
Err(e) => {
writeln!(
&mut stderr(),
Expand All @@ -282,44 +296,60 @@ impl Matcher for FileTimeMatcher {
impl FileTimeMatcher {
/// Implementation of matches that returns a result, allowing use to use try!
/// to deal with the errors.
fn matches_impl(&self, file_info: &DirEntry, now: SystemTime) -> Result<bool, Box<dyn Error>> {
fn matches_impl(
&self,
file_info: &DirEntry,
start_time: SystemTime,
) -> Result<bool, Box<dyn Error>> {
let this_time = self.file_time_type.get_file_time(file_info.metadata()?)?;
let mut is_negative = false;
// durations can't be negative. So duration_since returns a duration
// wrapped in an error if now < this_time.
let age = match now.duration_since(this_time) {
let age = match start_time.duration_since(this_time) {
Ok(duration) => duration,
Err(e) => {
is_negative = true;
e.duration()
}
};
let age_in_seconds: i64 = age.as_secs() as i64 * if is_negative { -1 } else { 1 };

// rust division truncates towards zero (see
// https://github.com/rust-lang/rust/blob/master/src/libcore/ops.rs#L580 )
// so a simple age_in_seconds / SECONDS_PER_DAY gives the wrong answer
// for negative ages: a file whose age is 1 second in the future needs to
// count as -1 day old, not 0.
let age_in_days = age_in_seconds / SECONDS_PER_DAY + if is_negative { -1 } else { 0 };
// If today_start is true, we should count it as 0 days old.
// because today is 00:00:00, so we need to subtract 1 day.
let negative_offset = if is_negative && !self.today_start {
-1
} else {
0
};

let age_in_days = age_in_seconds / SECONDS_PER_DAY + negative_offset;
Ok(self.days.imatches(age_in_days))
}

pub fn new(file_time_type: FileTimeType, days: ComparableValue) -> Self {
pub fn new(file_time_type: FileTimeType, days: ComparableValue, today_start: bool) -> Self {
Self {
days,
file_time_type,
today_start,
}
}
}

pub struct FileAgeRangeMatcher {
minutes: ComparableValue,
file_time_type: FileTimeType,
today_start: bool,
}

impl Matcher for FileAgeRangeMatcher {
fn matches(&self, file_info: &DirEntry, matcher_io: &mut MatcherIO) -> bool {
match self.matches_impl(file_info, matcher_io.now()) {
let start_time = get_time(matcher_io, self.today_start);
match self.matches_impl(file_info, start_time) {
Err(e) => {
writeln!(
&mut stderr(),
Expand All @@ -337,10 +367,14 @@ impl Matcher for FileAgeRangeMatcher {
}

impl FileAgeRangeMatcher {
fn matches_impl(&self, file_info: &DirEntry, now: SystemTime) -> Result<bool, Box<dyn Error>> {
fn matches_impl(
&self,
file_info: &DirEntry,
start_time: SystemTime,
) -> Result<bool, Box<dyn Error>> {
let this_time = self.file_time_type.get_file_time(file_info.metadata()?)?;
let mut is_negative = false;
let age = match now.duration_since(this_time) {
let age = match start_time.duration_since(this_time) {
Ok(duration) => duration,
Err(e) => {
is_negative = true;
Expand All @@ -352,10 +386,11 @@ impl FileAgeRangeMatcher {
Ok(self.minutes.imatches(age_in_minutes))
}

pub fn new(file_time_type: FileTimeType, minutes: ComparableValue) -> Self {
pub fn new(file_time_type: FileTimeType, minutes: ComparableValue, today_start: bool) -> Self {
Self {
minutes,
file_time_type,
today_start,
}
}
}
Expand Down Expand Up @@ -413,13 +448,13 @@ mod tests {
let files_mtime = file.metadata().unwrap().modified().unwrap();

let exactly_one_day_matcher =
FileTimeMatcher::new(FileTimeType::Modified, ComparableValue::EqualTo(1));
FileTimeMatcher::new(FileTimeType::Modified, ComparableValue::EqualTo(1), false);
let more_than_one_day_matcher =
FileTimeMatcher::new(FileTimeType::Modified, ComparableValue::MoreThan(1));
FileTimeMatcher::new(FileTimeType::Modified, ComparableValue::MoreThan(1), false);
let less_than_one_day_matcher =
FileTimeMatcher::new(FileTimeType::Modified, ComparableValue::LessThan(1));
FileTimeMatcher::new(FileTimeType::Modified, ComparableValue::LessThan(1), false);
let zero_day_matcher =
FileTimeMatcher::new(FileTimeType::Modified, ComparableValue::EqualTo(0));
FileTimeMatcher::new(FileTimeType::Modified, ComparableValue::EqualTo(0), false);

// set "now" to 2 days after the file was modified.
let mut deps = FakeDependencies::new();
Expand Down Expand Up @@ -500,6 +535,84 @@ mod tests {
);
}

#[test]
fn file_time_matcher_with_daystart() {
// this file should already exist
let file = get_dir_entry_for("test_data", "simple");

let mut deps = FakeDependencies::new();
let files_mtime = file.metadata().unwrap().modified().unwrap();

let exactly_one_day_matcher =
FileTimeMatcher::new(FileTimeType::Modified, ComparableValue::EqualTo(1), true);
let more_than_one_day_matcher =
FileTimeMatcher::new(FileTimeType::Modified, ComparableValue::MoreThan(1), true);
let less_than_one_day_matcher =
FileTimeMatcher::new(FileTimeType::Modified, ComparableValue::LessThan(1), true);
let zero_day_matcher =
FileTimeMatcher::new(FileTimeType::Modified, ComparableValue::EqualTo(0), true);

// set "now" to 3 days after the file was modified.
// Because daystart affects the time when the calculation starts,
// in order to avoid complicated assertions, it is set to 3 days later.
deps.set_time(files_mtime + Duration::new(3 * SECONDS_PER_DAY as u64, 0));
assert!(
!exactly_one_day_matcher.matches(&file, &mut deps.new_matcher_io()),
"3 day old file shouldn't match exactly 1 day old"
);
assert!(
more_than_one_day_matcher.matches(&file, &mut deps.new_matcher_io()),
"3 day old file should match more than 1 day old"
);
assert!(
!less_than_one_day_matcher.matches(&file, &mut deps.new_matcher_io()),
"3 day old file shouldn't match less than 1 day old"
);
assert!(
!zero_day_matcher.matches(&file, &mut deps.new_matcher_io()),
"3 day old file shouldn't match exactly 0 days old"
);

// set "now" to exactly the same time file was modified.
deps.set_time(files_mtime);
assert!(
!exactly_one_day_matcher.matches(&file, &mut deps.new_matcher_io()),
"0 day old file shouldn't match exactly 1 day old"
);
assert!(
!more_than_one_day_matcher.matches(&file, &mut deps.new_matcher_io()),
"0 day old file shouldn't match more than 1 day old"
);
assert!(
less_than_one_day_matcher.matches(&file, &mut deps.new_matcher_io()),
"0 day old file should match less than 1 day old"
);
assert!(
zero_day_matcher.matches(&file, &mut deps.new_matcher_io()),
"0 day old file should match exactly 0 days old"
);

// set "now" to a second before the file was modified (e.g. the file was
// modified after find started running
deps.set_time(files_mtime - Duration::new(1_u64, 0));
assert!(
!exactly_one_day_matcher.matches(&file, &mut deps.new_matcher_io()),
"future-modified file shouldn't match exactly 1 day old"
);
assert!(
!more_than_one_day_matcher.matches(&file, &mut deps.new_matcher_io()),
"future-modified file shouldn't match more than 1 day old"
);
assert!(
less_than_one_day_matcher.matches(&file, &mut deps.new_matcher_io()),
"future-modified file should match less than 1 day old"
);
assert!(
zero_day_matcher.matches(&file, &mut deps.new_matcher_io()),
"future-modified file should match exactly 0 days old"
);
}

#[test]
fn file_time_matcher_modified_changed_accessed() {
let temp_dir = Builder::new()
Expand Down Expand Up @@ -569,7 +682,7 @@ mod tests {
file_time_type: FileTimeType,
) {
{
let matcher = FileTimeMatcher::new(file_time_type, ComparableValue::EqualTo(0));
let matcher = FileTimeMatcher::new(file_time_type, ComparableValue::EqualTo(0), false);

let mut deps = FakeDependencies::new();
deps.set_time(file_time);
Expand Down Expand Up @@ -771,7 +884,8 @@ mod tests {
]
.iter()
.for_each(|time_type| {
let more_matcher = FileAgeRangeMatcher::new(*time_type, ComparableValue::MoreThan(1));
let more_matcher =
FileAgeRangeMatcher::new(*time_type, ComparableValue::MoreThan(1), true);
assert!(
!more_matcher.matches(&new_file, &mut FakeDependencies::new().new_matcher_io()),
"{}",
Expand Down Expand Up @@ -800,7 +914,8 @@ mod tests {
]
.iter()
.for_each(|time_type| {
let less_matcher = FileAgeRangeMatcher::new(*time_type, ComparableValue::LessThan(1));
let less_matcher =
FileAgeRangeMatcher::new(*time_type, ComparableValue::LessThan(1), true);
assert!(
less_matcher.matches(&new_file, &mut FakeDependencies::new().new_matcher_io()),
"{}",
Expand All @@ -818,7 +933,7 @@ mod tests {
// catch file error
let _ = fs::remove_file(&*new_file.path().to_string_lossy());
let matcher =
FileAgeRangeMatcher::new(FileTimeType::Modified, ComparableValue::MoreThan(1));
FileAgeRangeMatcher::new(FileTimeType::Modified, ComparableValue::MoreThan(1), true);
assert!(
!matcher.matches(&new_file, &mut FakeDependencies::new().new_matcher_io()),
"The correct situation is that the file reading here cannot be successful."
Expand Down
38 changes: 38 additions & 0 deletions src/find/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub struct Config {
sorted_output: bool,
help_requested: bool,
version_requested: bool,
today_start: bool,
no_leaf_dirs: bool,
}

Expand All @@ -34,6 +35,7 @@ impl Default for Config {
sorted_output: false,
help_requested: false,
version_requested: false,
today_start: false,
// Directory information and traversal are done by walkdir,
// and this configuration field will exist as
// a compatibility item for GNU findutils.
Expand Down Expand Up @@ -1262,4 +1264,40 @@ mod tests {
fix_up_slashes("./test_data/depth\n")
);
}

#[test]
#[cfg(unix)]
fn test_daystart() {
use crate::find::tests::FakeDependencies;

let deps = FakeDependencies::new();
let rc = find_main(
&[
"find",
"./test_data/simple/subdir",
"-daystart",
"-mtime",
"0",
],
&deps,
);

assert_eq!(rc, 0);

// twice -daystart should be matched
let deps = FakeDependencies::new();
let rc = find_main(
&[
"find",
"./test_data/simple/subdir",
"-daystart",
"-daystart",
"-mtime",
"1",
],
&deps,
);

assert_eq!(rc, 0);
}
}
25 changes: 25 additions & 0 deletions tests/find_cmd_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -921,3 +921,28 @@ fn find_noleaf() {
.stdout(predicate::str::contains("test_data/simple/subdir"))
.stderr(predicate::str::is_empty());
}

#[test]
#[serial(working_dir)]
fn find_daystart() {
Command::cargo_bin("find")
.expect("found binary")
.args(["./test_data/simple/subdir", "-daystart", "-mtime", "0"])
.assert()
.success()
.stderr(predicate::str::is_empty());

// twice -daystart should be matched
Command::cargo_bin("find")
.expect("found binary")
.args([
"./test_data/simple/subdir",
"-daystart",
"-daystart",
"-mtime",
"1",
])
.assert()
.success()
.stderr(predicate::str::is_empty());
}

0 comments on commit 1b9904e

Please sign in to comment.