agent_skill_installer/
consent.rs1use std::collections::BTreeSet;
12use std::fs;
13use std::io;
14use std::path::{Path, PathBuf};
15
16const HEADER: &str = "# agent-skill-installer: targets the user declined, one per line";
17
18#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct Consent {
21 path: PathBuf,
22 declined: BTreeSet<PathBuf>,
23}
24
25impl Consent {
26 pub fn load(path: impl Into<PathBuf>) -> io::Result<Self> {
32 let path = path.into();
33 let declined = match fs::read_to_string(&path) {
34 Ok(text) => text
35 .lines()
36 .filter(|line| !line.is_empty() && !line.starts_with('#'))
37 .map(PathBuf::from)
38 .collect(),
39 Err(error) if error.kind() == io::ErrorKind::NotFound => BTreeSet::new(),
40 Err(error) => return Err(error),
41 };
42 Ok(Self { path, declined })
43 }
44
45 #[must_use]
47 pub fn is_declined(&self, target: &Path) -> bool {
48 self.declined.contains(target)
49 }
50
51 pub fn decline(&mut self, target: &Path) -> io::Result<()> {
56 let text = target.to_string_lossy();
57 if text.contains('\n') || text.contains('\r') || text.as_ref() != target.as_os_str() {
58 return Err(io::Error::new(
59 io::ErrorKind::InvalidInput,
60 format!("{} cannot be stored as one line of text", target.display()),
61 ));
62 }
63 self.declined.insert(target.to_path_buf());
64 Ok(())
65 }
66
67 pub fn withdraw(&mut self, target: &Path) {
69 self.declined.remove(target);
70 }
71
72 pub fn save(&self) -> io::Result<()> {
77 if let Some(parent) = self.path.parent() {
78 fs::create_dir_all(parent)?;
79 }
80 let mut text = format!("{HEADER}\n");
81 for target in &self.declined {
82 text.push_str(&target.to_string_lossy());
83 text.push('\n');
84 }
85 let staging = self.path.with_extension("tmp");
86 let mut file = fs::File::create(&staging)?;
87 std::io::Write::write_all(&mut file, text.as_bytes())?;
88 file.sync_all()?;
91 drop(file);
92 fs::rename(&staging, &self.path)
93 }
94}