use std::error::Error;
use std::fs::{self, Metadata};
use std::io::{stderr, Write};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use chrono::{DateTime, Local, Timelike};
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
use super::{ComparableValue, Follow, Matcher, MatcherIO, WalkEntry};
const SECONDS_PER_DAY: i64 = 60 * 60 * 24;
fn get_time(matcher_io: &mut MatcherIO, today_start: bool) -> SystemTime {
if today_start {
let duration_since_unix_epoch = matcher_io.now().duration_since(UNIX_EPOCH).unwrap();
let seconds_since_unix_epoch = duration_since_unix_epoch.as_secs();
let utc_time = DateTime::from_timestamp(seconds_since_unix_epoch as i64, 0).unwrap();
let local_time = utc_time.with_timezone(&Local);
let seconds_since_last_midnight = local_time.num_seconds_from_midnight();
let local_midnight_seconds = local_time.timestamp() - seconds_since_last_midnight as i64;
UNIX_EPOCH + Duration::from_secs(local_midnight_seconds as u64)
} else {
matcher_io.now()
}
}
pub struct NewerMatcher {
given_modification_time: SystemTime,
}
impl NewerMatcher {
pub fn new(path_to_file: &str, follow: Follow) -> Result<Self, Box<dyn Error>> {
let metadata = follow.root_metadata(path_to_file)?;
Ok(Self {
given_modification_time: metadata.modified()?,
})
}
fn matches_impl(&self, file_info: &WalkEntry) -> Result<bool, Box<dyn Error>> {
let this_time = file_info.metadata()?.modified()?;
Ok(self
.given_modification_time
.duration_since(this_time)
.is_err())
}
}
impl Matcher for NewerMatcher {
fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
match self.matches_impl(file_info) {
Err(e) => {
writeln!(
&mut stderr(),
"Error getting modification time for {}: {}",
file_info.path().to_string_lossy(),
e
)
.unwrap();
false
}
Ok(t) => t,
}
}
}
#[derive(Clone, Copy, Debug)]
pub enum NewerOptionType {
Accessed,
Birthed,
Changed,
Modified,
}
impl NewerOptionType {
#[allow(clippy::should_implement_trait)]
pub fn from_str(option: &str) -> Self {
match option {
"a" => Self::Accessed,
"B" => Self::Birthed,
"c" => Self::Changed,
_ => Self::Modified,
}
}
fn get_file_time(self, metadata: &Metadata) -> std::io::Result<SystemTime> {
match self {
Self::Accessed => metadata.accessed(),
Self::Birthed => metadata.created(),
Self::Changed => metadata.changed(),
Self::Modified => metadata.modified(),
}
}
}
pub struct NewerOptionMatcher {
x_option: NewerOptionType,
y_option: NewerOptionType,
given_modification_time: SystemTime,
}
impl NewerOptionMatcher {
pub fn new(x_option: &str, y_option: &str, path_to_file: &str) -> Result<Self, Box<dyn Error>> {
let metadata = fs::metadata(path_to_file)?;
let x_option = NewerOptionType::from_str(x_option);
let y_option = NewerOptionType::from_str(y_option);
Ok(Self {
x_option,
y_option,
given_modification_time: metadata.modified()?,
})
}
fn matches_impl(&self, file_info: &WalkEntry) -> Result<bool, Box<dyn Error>> {
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()?)?;
Ok(self
.given_modification_time
.duration_since(x_option_time)
.is_err()
&& self
.given_modification_time
.duration_since(y_option_time)
.is_err())
}
}
impl Matcher for NewerOptionMatcher {
fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
match self.matches_impl(file_info) {
Err(e) => {
writeln!(
&mut stderr(),
"Error getting {:?} and {:?} time for {}: {}",
self.x_option,
self.y_option,
file_info.path().to_string_lossy(),
e
)
.unwrap();
false
}
Ok(t) => t,
}
}
}
pub struct NewerTimeMatcher {
time: i64,
newer_time_type: NewerOptionType,
}
impl NewerTimeMatcher {
pub fn new(newer_time_type: NewerOptionType, time: i64) -> Self {
Self {
time,
newer_time_type,
}
}
fn matches_impl(&self, file_info: &WalkEntry) -> Result<bool, Box<dyn Error>> {
let this_time = self.newer_time_type.get_file_time(file_info.metadata()?)?;
let timestamp = this_time
.duration_since(UNIX_EPOCH)
.unwrap_or_else(|e| e.duration());
Ok(self.time
<= timestamp
.as_millis()
.try_into()
.expect("timestamp memory implications"))
}
}
impl Matcher for NewerTimeMatcher {
fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
match self.matches_impl(file_info) {
Err(e) => {
writeln!(
&mut stderr(),
"Error getting {:?} time for {}: {}",
self.newer_time_type,
file_info.path().to_string_lossy(),
e
)
.unwrap();
false
}
Ok(t) => t,
}
}
}
pub trait ChangeTime {
fn changed(&self) -> std::io::Result<SystemTime>;
}
#[cfg(unix)]
impl ChangeTime for Metadata {
fn changed(&self) -> std::io::Result<SystemTime> {
let ctime_sec = self.ctime();
let ctime_nsec = self.ctime_nsec() as u32;
let ctime = if ctime_sec >= 0 {
UNIX_EPOCH + std::time::Duration::new(ctime_sec as u64, ctime_nsec)
} else {
UNIX_EPOCH - std::time::Duration::new(-ctime_sec as u64, ctime_nsec)
};
Ok(ctime)
}
}
#[cfg(not(unix))]
impl ChangeTime for Metadata {
fn changed(&self) -> std::io::Result<SystemTime> {
Err(std::io::Error::from(std::io::ErrorKind::Unsupported))
}
}
#[derive(Clone, Copy, Debug)]
pub enum FileTimeType {
Accessed,
Changed,
Modified,
}
impl FileTimeType {
fn get_file_time(self, metadata: &Metadata) -> std::io::Result<SystemTime> {
match self {
Self::Accessed => metadata.accessed(),
Self::Changed => metadata.changed(),
Self::Modified => metadata.modified(),
}
}
}
pub struct FileTimeMatcher {
days: ComparableValue,
file_time_type: FileTimeType,
today_start: bool,
}
impl Matcher for FileTimeMatcher {
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) => {
writeln!(
&mut stderr(),
"Error getting {:?} time for {}: {}",
self.file_time_type,
file_info.path().to_string_lossy(),
e
)
.unwrap();
false
}
Ok(t) => t,
}
}
}
impl FileTimeMatcher {
fn matches_impl(
&self,
file_info: &WalkEntry,
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 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 };
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, 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: &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) => {
writeln!(
&mut stderr(),
"Error getting {:?} time for {}: {}",
self.file_time_type,
file_info.path().to_string_lossy(),
e
)
.unwrap();
false
}
Ok(t) => t,
}
}
}
impl FileAgeRangeMatcher {
fn matches_impl(
&self,
file_info: &WalkEntry,
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 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 };
let age_in_minutes = age_in_seconds / 60 + if is_negative { -1 } else { 0 };
Ok(self.minutes.imatches(age_in_minutes))
}
pub fn new(file_time_type: FileTimeType, minutes: ComparableValue, today_start: bool) -> Self {
Self {
minutes,
file_time_type,
today_start,
}
}
}
#[cfg(test)]
mod tests {
use chrono::NaiveTime;
use std::fs;
use std::fs::{File, OpenOptions};
use std::io::Read;
use std::thread;
use std::time::Duration;
use tempfile::Builder;
use super::*;
use crate::find::matchers::tests::get_dir_entry_for;
use crate::find::tests::FakeDependencies;
#[test]
fn newer_matcher() {
let old_file = get_dir_entry_for("test_data", "simple");
let temp_dir = Builder::new().prefix("example").tempdir().unwrap();
let temp_dir_path = temp_dir.path().to_string_lossy();
let new_file_name = "newFile";
File::create(temp_dir.path().join(new_file_name)).expect("create temp file");
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(),
Follow::Never,
)
.unwrap();
let matcher_for_old =
NewerMatcher::new(&old_file.path().to_string_lossy(), Follow::Never).unwrap();
let deps = FakeDependencies::new();
assert!(
!matcher_for_new.matches(&old_file, &mut deps.new_matcher_io()),
"old_file shouldn't be newer than new_dir"
);
assert!(
matcher_for_old.matches(&new_file, &mut deps.new_matcher_io()),
"new_file should be newer than old_dir"
);
assert!(
!matcher_for_old.matches(&old_file, &mut deps.new_matcher_io()),
"old_file shouldn't be newer than itself"
);
}
#[test]
fn file_time_matcher() {
let file = get_dir_entry_for("test_data", "simple");
let files_mtime = file.metadata().unwrap().modified().unwrap();
let exactly_one_day_matcher =
FileTimeMatcher::new(FileTimeType::Modified, ComparableValue::EqualTo(1), false);
let more_than_one_day_matcher =
FileTimeMatcher::new(FileTimeType::Modified, ComparableValue::MoreThan(1), false);
let less_than_one_day_matcher =
FileTimeMatcher::new(FileTimeType::Modified, ComparableValue::LessThan(1), false);
let zero_day_matcher =
FileTimeMatcher::new(FileTimeType::Modified, ComparableValue::EqualTo(0), false);
let mut deps = FakeDependencies::new();
deps.set_time(files_mtime + Duration::new(2 * SECONDS_PER_DAY as u64, 0));
assert!(
!exactly_one_day_matcher.matches(&file, &mut deps.new_matcher_io()),
"2 day old file shouldn't match exactly 1 day old"
);
assert!(
more_than_one_day_matcher.matches(&file, &mut deps.new_matcher_io()),
"2 day old file should match more than 1 day old"
);
assert!(
!less_than_one_day_matcher.matches(&file, &mut deps.new_matcher_io()),
"2 day old file shouldn't match less than 1 day old"
);
assert!(
!zero_day_matcher.matches(&file, &mut deps.new_matcher_io()),
"2 day old file shouldn't match exactly 0 days old"
);
deps.set_time(files_mtime + Duration::new((3 * SECONDS_PER_DAY / 2) as u64, 0));
assert!(
exactly_one_day_matcher.matches(&file, &mut deps.new_matcher_io()),
"1 day old file should match exactly 1 day old"
);
assert!(
!more_than_one_day_matcher.matches(&file, &mut deps.new_matcher_io()),
"1 day old file shouldn't match more than 1 day old"
);
assert!(
!less_than_one_day_matcher.matches(&file, &mut deps.new_matcher_io()),
"1 day old file shouldn't match less than 1 day old"
);
assert!(
!zero_day_matcher.matches(&file, &mut deps.new_matcher_io()),
"1 day old file shouldn't match exactly 0 days old"
);
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"
);
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 shouldn't match exactly 0 days old"
);
}
#[test]
fn file_time_matcher_with_daystart() {
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);
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"
);
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"
);
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 get_local_midnight() {
let deps = FakeDependencies::new();
let midnight = get_time(&mut deps.new_matcher_io(), true);
let midnight = DateTime::<Local>::from(midnight);
assert_eq!(midnight.time(), NaiveTime::from_hms_opt(0, 0, 0).unwrap());
}
#[test]
fn file_time_matcher_modified_changed_accessed() {
let temp_dir = Builder::new()
.prefix("file_time_matcher_modified_changed_accessed")
.tempdir()
.unwrap();
let foo_path = temp_dir.path().join("foo");
{
File::create(&foo_path).expect("create temp file");
}
thread::sleep(Duration::from_secs(2));
let mut buffer = [0; 10];
{
let mut f = File::open(&foo_path).expect("open temp file");
let _ = f.read(&mut buffer);
}
thread::sleep(Duration::from_secs(2));
let mut buffer = [0; 10];
{
let mut f = OpenOptions::new()
.read(true)
.write(true)
.open(&foo_path)
.expect("open temp file");
let _ = f.write(&buffer);
}
thread::sleep(Duration::from_secs(2));
{
let mut f = File::open(&foo_path).expect("open temp file");
let _ = f.read(&mut buffer);
}
let file_info = get_dir_entry_for(&temp_dir.path().to_string_lossy(), "foo");
let metadata = file_info.metadata().unwrap();
if let Ok(accessed_time) = metadata.accessed() {
test_matcher_for_file_time_type(&file_info, accessed_time, FileTimeType::Accessed);
}
if let Ok(creation_time) = metadata.changed() {
test_matcher_for_file_time_type(&file_info, creation_time, FileTimeType::Changed);
}
if let Ok(modified_time) = metadata.modified() {
test_matcher_for_file_time_type(&file_info, modified_time, FileTimeType::Modified);
}
}
fn test_matcher_for_file_time_type(
file_info: &WalkEntry,
file_time: SystemTime,
file_time_type: FileTimeType,
) {
{
let matcher = FileTimeMatcher::new(file_time_type, ComparableValue::EqualTo(0), false);
let mut deps = FakeDependencies::new();
deps.set_time(file_time);
assert!(
matcher.matches(file_info, &mut deps.new_matcher_io()),
"{file_time_type:?} time matcher should match"
);
deps.set_time(file_time - Duration::from_secs(1));
assert!(
!matcher.matches(file_info, &mut deps.new_matcher_io()),
"{file_time_type:?} time matcher shouldn't match a second before"
);
}
}
#[test]
fn newer_option_matcher() {
let options = [
"a",
#[cfg(not(target_os = "linux"))]
"B",
#[cfg(unix)]
"c",
"m",
];
for x_option in options {
for y_option in options {
let temp_dir = Builder::new().prefix("example").tempdir().unwrap();
let temp_dir_path = temp_dir.path().to_string_lossy();
let new_file_name = "newFile";
File::create(temp_dir.path().join(new_file_name)).expect("create temp file");
let new_file = get_dir_entry_for(&temp_dir_path, new_file_name);
let old_file = get_dir_entry_for("test_data", "simple");
let deps = FakeDependencies::new();
let matcher =
NewerOptionMatcher::new(x_option, y_option, &old_file.path().to_string_lossy());
assert!(
matcher
.unwrap()
.matches(&new_file, &mut deps.new_matcher_io()),
"new_file should be newer than old_dir"
);
}
}
}
#[test]
fn newer_time_matcher() {
let deps = FakeDependencies::new();
let time = deps
.new_matcher_io()
.now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_millis()
.try_into()
.unwrap();
let created_matcher = NewerTimeMatcher::new(NewerOptionType::Birthed, time);
thread::sleep(Duration::from_millis(100));
let temp_dir = Builder::new()
.prefix("newer_time_matcher")
.tempdir()
.unwrap();
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");
assert!(
created_matcher.matches(&file_info, &mut deps.new_matcher_io()),
"file created time should after 'time'"
);
let accessed_matcher = NewerTimeMatcher::new(NewerOptionType::Accessed, time);
let mut buffer = [0; 10];
{
let mut file = File::open(&foo_path).expect("open temp file");
let _ = file.read(&mut buffer);
}
assert!(
accessed_matcher.matches(&file_info, &mut deps.new_matcher_io()),
"file accessed time should after 'time'"
);
let modified_matcher = NewerTimeMatcher::new(NewerOptionType::Modified, time);
let buffer = [0; 10];
let mut file = OpenOptions::new()
.read(true)
.write(true)
.open(&foo_path)
.expect("open temp file");
let _ = file.write(&buffer);
assert!(
modified_matcher.matches(&file_info, &mut deps.new_matcher_io()),
"file modified time should after 'time'"
);
#[cfg(unix)]
{
let inode_changed_matcher = NewerTimeMatcher::new(NewerOptionType::Changed, time);
let _ =
File::create(temp_dir.path().join("inode_test_file")).expect("create temp file");
let _ = fs::copy("inode_test_file", "new_inode_test_file");
let _ = fs::remove_file("inode_test_file");
let _ = fs::rename("new_inode_test_file", "inode_test_file");
let file_info =
get_dir_entry_for(&temp_dir.path().to_string_lossy(), "inode_test_file");
assert!(
inode_changed_matcher.matches(&file_info, &mut deps.new_matcher_io()),
"file inode changed time should after 'std_time'"
);
}
}
#[test]
fn file_age_range_matcher() {
let temp_dir = Builder::new().prefix("example").tempdir().unwrap();
let temp_dir_path = temp_dir.path().to_string_lossy();
let new_file_name = "newFile";
File::create(temp_dir.path().join(new_file_name)).expect("create temp file");
let new_file = get_dir_entry_for(&temp_dir_path, new_file_name);
[
FileTimeType::Accessed,
FileTimeType::Changed,
FileTimeType::Modified,
]
.iter()
.for_each(|time_type| {
let more_matcher =
FileAgeRangeMatcher::new(*time_type, ComparableValue::MoreThan(1), true);
assert!(
!more_matcher.matches(&new_file, &mut FakeDependencies::new().new_matcher_io()),
"{}",
format!(
"more minutes old file should not match more than 1 minute old in {} test.",
match *time_type {
FileTimeType::Accessed => "accessed",
FileTimeType::Changed => "changed",
FileTimeType::Modified => "modified",
}
)
);
});
[
FileTimeType::Accessed,
#[cfg(unix)]
FileTimeType::Changed,
FileTimeType::Modified,
]
.iter()
.for_each(|time_type| {
let less_matcher =
FileAgeRangeMatcher::new(*time_type, ComparableValue::LessThan(1), true);
assert!(
less_matcher.matches(&new_file, &mut FakeDependencies::new().new_matcher_io()),
"{}",
format!(
"less minutes old file should match less than 1 minute old in {} test.",
match *time_type {
FileTimeType::Accessed => "accessed",
FileTimeType::Changed => "changed",
FileTimeType::Modified => "modified",
}
)
);
});
let _ = fs::remove_file(&*new_file.path().to_string_lossy());
let matcher =
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."
);
}
}