use crate::ops::DiffOp;
use anyhow::anyhow;
use std::cmp::Ordering;
use std::fs::File;
use std::fs::Metadata;
use std::io::ErrorKind;
use std::path::Path;
use std::path::PathBuf;
use std::str::FromStr;
use std::time::SystemTime;
#[derive(Debug)]
pub struct FileCmp {
path: PathBuf,
file: Option<File>,
metadata: Option<Metadata>,
}
impl TryFrom<PathBuf> for FileCmp {
type Error = std::io::Error;
fn try_from(path: PathBuf) -> Result<Self, Self::Error> {
match File::options()
.read(true)
.open(&path)
{
Ok(file) => Ok(Self {
path,
metadata: Some(file.metadata()?),
file: Some(file),
}),
Err(e) => match e.kind() {
ErrorKind::NotFound => Ok(Self::not_found(path)),
_ => Err(e),
},
}
}
}
impl FileCmp {
#[must_use]
pub fn not_found(path: PathBuf) -> Self {
Self {
path,
file: None,
metadata: None,
}
}
#[must_use]
pub fn is_found(&self) -> bool {
self.file.is_some()
}
#[must_use]
fn modified(&self) -> Option<SystemTime> {
self.metadata
.as_ref()
.map(|m| m.modified().expect("get file modified time"))
}
#[must_use]
pub fn partial_cmp(
&self,
other: &Self,
diff_op: &DiffOp,
promote_newest: bool)
-> Option<Ordering>
{
use Ordering::*;
if let Ok(false) = diff_op
.diff(self.path.as_path(), other.path.as_path())
{
return Some(Equal);
}
let file_cmp = match (&self.file, &other.file) {
(Some(_), Some(_)) => Equal,
(None, Some(_)) => if promote_newest { Greater } else { Less },
(Some(_), None) => if promote_newest { Less } else { Greater },
_ => return None,
};
let time_cmp = match (&self.modified(), &other.modified()) {
(Some(t1), Some(t2)) => t1.cmp(t2),
(None, Some(_)) => if promote_newest { Greater } else { Less },
(Some(_), None) => if promote_newest { Less } else { Greater },
_ => return None,
};
Some(file_cmp.then(time_cmp))
}
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(clap::ArgEnum)]
pub enum MissingFileBehavior {
Oldest,
Newest,
Ignore,
Error,
}
impl FromStr for MissingFileBehavior {
type Err = MissingFileBehaviorParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.eq_ignore_ascii_case("oldest") {
Ok(Self::Oldest)
} else if s.eq_ignore_ascii_case("newest") {
Ok(Self::Newest)
} else if s.eq_ignore_ascii_case("ignore") {
Ok(Self::Ignore)
} else if s.eq_ignore_ascii_case("error") {
Ok(Self::Error)
} else {
Err(MissingFileBehaviorParseError)
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MissingFileBehaviorParseError;
impl std::error::Error for MissingFileBehaviorParseError {}
impl std::fmt::Display for MissingFileBehaviorParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "failure to parse MissingFileBehavior")
}
}
pub fn partial_cmp_paths(
a: &Path,
b: &Path,
diff_op: &DiffOp,
missing: MissingFileBehavior)
-> Result<Option<Ordering>, anyhow::Error>
{
let promote_newest = matches!(missing, MissingFileBehavior::Newest);
if a == b { return Ok(Some(Ordering::Equal)); }
let a = match FileCmp::try_from(a.to_path_buf()) {
Ok(file_cmp) if !file_cmp.is_found() => match missing {
MissingFileBehavior::Error => return Err(
anyhow!("file '{}' not found", a.display())
),
MissingFileBehavior::Ignore => None,
_ => Some(file_cmp),
},
Ok(file_cmp) => Some(file_cmp),
Err(e) => return Err(e.into()),
};
let b = match FileCmp::try_from(b.to_path_buf()) {
Ok(file_cmp) if !file_cmp.is_found() => match missing {
MissingFileBehavior::Error => return Err(
anyhow!("file '{}' not found", b.display())
),
MissingFileBehavior::Ignore => None,
_ => Some(file_cmp),
},
Ok(file_cmp) => Some(file_cmp),
Err(e) => return Err(e.into()),
};
let ordering = match (a, b) {
(Some(a), Some(b)) => a.partial_cmp(&b, diff_op, promote_newest),
(None, None) => Some(Ordering::Equal),
(None, _) => Some(Ordering::Greater),
(_, None) => Some(Ordering::Less),
};
Ok(ordering)
}
pub fn compare_all<'p, P>(
paths: P,
reverse: bool,
diff_op: &DiffOp,
missing: MissingFileBehavior)
-> Result<usize, anyhow::Error>
where P: IntoIterator<Item=&'p Path>
{
let promote_newest = matches!(missing, MissingFileBehavior::Newest);
let mut max_idx = 0;
let mut prev_file_cmp: Option<FileCmp> = None;
for (idx, p) in paths.into_iter().enumerate() {
let curr = match FileCmp::try_from(p.to_path_buf()) {
Ok(file_cmp) if !file_cmp.is_found() => match missing {
MissingFileBehavior::Error => return Err(
anyhow!("file '{}' not found", p.display())
),
MissingFileBehavior::Ignore => continue,
_ => Some(file_cmp),
},
Ok(file_cmp) => Some(file_cmp),
Err(e) => return Err(e.into()),
};
match (prev_file_cmp.as_ref(), curr) {
(Some(prev), Some(curr)) => {
let cmp = prev.partial_cmp(&curr, diff_op, promote_newest)
.map(|o| if reverse { o } else { o.reverse() });
if cmp == Some(Ordering::Greater) {
prev_file_cmp = Some(curr);
max_idx = idx;
}
},
(None, curr) => {
prev_file_cmp = curr;
max_idx = idx;
},
_ => (),
}
}
Ok(max_idx)
}