use provenance_macros::rule;
use std::path::{Path, PathBuf};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum TargetState {
Clear,
Ours,
Foreign,
ForeignDirectory,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum InstallVerdict {
Write,
Ours,
Overwrite,
Refuse,
CopyInto,
}
#[rule("rule_install_never_clobbers")]
pub const fn classify_install(state: TargetState, force: bool) -> InstallVerdict {
match state {
TargetState::Clear => InstallVerdict::Write,
TargetState::Ours => InstallVerdict::Ours,
TargetState::Foreign | TargetState::ForeignDirectory if force => InstallVerdict::Overwrite,
TargetState::Foreign => InstallVerdict::Refuse,
TargetState::ForeignDirectory => InstallVerdict::CopyInto,
}
}
pub enum TargetEntry {
Vacant,
Symlink(PathBuf),
Directory,
Other,
}
impl TargetEntry {
pub fn read(path: &Path) -> anyhow::Result<Self> {
let Ok(metadata) = std::fs::symlink_metadata(path) else {
return Ok(Self::Vacant);
};
if metadata.file_type().is_symlink() {
Ok(Self::Symlink(std::fs::read_link(path)?))
} else if metadata.is_dir() {
Ok(Self::Directory)
} else {
Ok(Self::Other)
}
}
pub fn remove(&self, path: &Path) -> std::io::Result<()> {
match self {
Self::Vacant => Ok(()),
Self::Directory => std::fs::remove_dir_all(path),
Self::Symlink(_) | Self::Other => std::fs::remove_file(path),
}
}
}
#[cfg(test)]
mod tests {
use provenance_macros::verifies;
use super::*;
fn all_states() -> Vec<TargetState> {
let mut all = vec![TargetState::Clear];
while let Some(next) = match all.last().unwrap() {
TargetState::Clear => Some(TargetState::Ours),
TargetState::Ours => Some(TargetState::Foreign),
TargetState::Foreign => Some(TargetState::ForeignDirectory),
TargetState::ForeignDirectory => None,
} {
all.push(next);
}
all
}
const fn holds_user_content(state: TargetState) -> bool {
matches!(state, TargetState::Foreign | TargetState::ForeignDirectory)
}
const fn destroys_target(verdict: InstallVerdict) -> bool {
matches!(verdict, InstallVerdict::Overwrite)
}
#[test]
#[verifies("rule_install_never_clobbers", exhaustion)]
fn nothing_the_install_did_not_write_is_destroyed_without_force() {
for state in all_states() {
for force in [false, true] {
let verdict = classify_install(state, force);
if destroys_target(verdict) {
assert!(
force && holds_user_content(state),
"{state:?} with force={force} is overwritten"
);
}
if holds_user_content(state) && !force {
assert!(
!destroys_target(verdict),
"{state:?} loses the user's content without --force"
);
}
}
}
}
#[test]
#[verifies("rule_install_never_clobbers", exhaustion)]
fn force_changes_nothing_where_the_user_has_nothing_at_stake() {
for state in all_states() {
if holds_user_content(state) {
continue;
}
let withheld = classify_install(state, false);
assert_eq!(
withheld,
classify_install(state, true),
"--force changes the verdict for {state:?}"
);
assert_ne!(
withheld,
InstallVerdict::Refuse,
"{state:?} demands --force although nothing of the user's is there"
);
}
}
}