use std::collections::BTreeSet;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
const HEADER: &str = "# agent-skill-installer: targets the user declined, one per line";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Consent {
path: PathBuf,
declined: BTreeSet<PathBuf>,
}
impl Consent {
pub fn load(path: impl Into<PathBuf>) -> io::Result<Self> {
let path = path.into();
let declined = match fs::read_to_string(&path) {
Ok(text) => text
.lines()
.filter(|line| !line.is_empty() && !line.starts_with('#'))
.map(PathBuf::from)
.collect(),
Err(error) if error.kind() == io::ErrorKind::NotFound => BTreeSet::new(),
Err(error) => return Err(error),
};
Ok(Self { path, declined })
}
#[must_use]
pub fn is_declined(&self, target: &Path) -> bool {
self.declined.contains(target)
}
pub fn decline(&mut self, target: &Path) -> io::Result<()> {
let text = target.to_string_lossy();
if text.contains('\n') || text.contains('\r') || text.as_ref() != target.as_os_str() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("{} cannot be stored as one line of text", target.display()),
));
}
self.declined.insert(target.to_path_buf());
Ok(())
}
pub fn withdraw(&mut self, target: &Path) {
self.declined.remove(target);
}
pub fn save(&self) -> io::Result<()> {
if let Some(parent) = self.path.parent() {
fs::create_dir_all(parent)?;
}
let mut text = format!("{HEADER}\n");
for target in &self.declined {
text.push_str(&target.to_string_lossy());
text.push('\n');
}
let staging = self.path.with_extension("tmp");
let mut file = fs::File::create(&staging)?;
std::io::Write::write_all(&mut file, text.as_bytes())?;
file.sync_all()?;
drop(file);
fs::rename(&staging, &self.path)
}
}