use crate::subvolume_index::{StatFields, StoredStat};
use crate::subvolume_index_files::{fields_of, matches_stored};
use std::os::unix::fs::MetadataExt;
pub const ASSUME_VALID: u16 = 0x8000;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CleanRefusalKind {
OperationInProgress(&'static str),
Unreadable,
NotPlain,
StagedChange,
FileChanged,
StatStale,
Racy,
Untracked,
}
impl CleanRefusalKind {
pub fn restorable(self) -> bool {
return matches!(self, Self::StagedChange | Self::FileChanged | Self::StatStale | Self::Racy | Self::Untracked);
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CleanRefusal {
pub kind: CleanRefusalKind,
pub reason: String,
}
impl CleanRefusal {
pub fn new(kind: CleanRefusalKind, reason: impl Into<String>) -> CleanRefusal {
return CleanRefusal { kind, reason: reason.into() };
}
pub fn unreadable(reason: impl Into<String>) -> CleanRefusal {
return CleanRefusal::new(CleanRefusalKind::Unreadable, reason);
}
}
impl std::fmt::Display for CleanRefusal {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter.write_str(self.reason.as_str());
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct StatEntry {
pub path: Vec<u8>,
pub flags: u16,
pub mode: u32,
pub stored: StoredStat,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum StatVerdict {
Agrees,
Unproven(String),
Differs(String),
}
pub fn metadata_verdict(entry: &StatEntry, metadata: &std::fs::Metadata, index_seconds: u32) -> StatVerdict {
return match stat_finding(entry, metadata, index_seconds) {
None => StatVerdict::Agrees,
Some(CleanRefusal { kind: CleanRefusalKind::Racy | CleanRefusalKind::StatStale, reason }) => StatVerdict::Unproven(reason),
Some(refusal) => StatVerdict::Differs(refusal.reason),
};
}
pub fn stat_finding(entry: &StatEntry, metadata: &std::fs::Metadata, index_seconds: u32) -> Option<CleanRefusal> {
let StatEntry { path, flags, mode, stored }: &StatEntry = entry;
let shown: String = String::from_utf8_lossy(path).into_owned();
if flags & ASSUME_VALID != 0 {
return Some(CleanRefusal::new(CleanRefusalKind::NotPlain, format!("{shown} is marked assume-unchanged")));
}
let symbolic: bool = *mode & 0o170_000 == 0o120_000;
if symbolic != metadata.file_type().is_symlink() || (!symbolic && !metadata.file_type().is_file()) {
return Some(CleanRefusal::new(CleanRefusalKind::FileChanged, format!("{shown} changed its type")));
}
if !symbolic && (*mode & 0o100 != 0) != (metadata.mode() & 0o100 != 0) {
return Some(CleanRefusal::new(CleanRefusalKind::FileChanged, format!("{shown} changed its executable bit")));
}
if stored.mtime_seconds >= index_seconds {
return Some(CleanRefusal::new(CleanRefusalKind::Racy, format!("{shown} is as new as the index, so its stat fields prove nothing")));
}
let now: StatFields = fields_of(metadata);
if !matches_stored(&now, stored) {
return Some(CleanRefusal::new(CleanRefusalKind::StatStale, format!("{shown} differs from its index entry by size, times or inode")));
}
return None;
}
pub fn metadata_differs_from_entry(entry: &StatEntry, metadata: &std::fs::Metadata, index_seconds: u32) -> Option<String> {
return match metadata_verdict(entry, metadata, index_seconds) {
StatVerdict::Agrees => None,
StatVerdict::Unproven(reason) | StatVerdict::Differs(reason) => Some(reason),
};
}
pub fn file_changed(reason: String) -> CleanRefusal {
return CleanRefusal::new(CleanRefusalKind::FileChanged, reason);
}
pub fn changed_file(reason: String) -> Option<CleanRefusal> {
return Some(file_changed(reason));
}