use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
use super::digest::{ReadOnce, is_claude_settings};
use super::{Drift, Lock, key, keys, spec_keys};
use crate::home::Home;
pub(super) const ABSENT: &str = "absent";
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Kept {
intact: BTreeMap<PathBuf, Vec<u8>>,
drifted: BTreeMap<PathBuf, Vec<u8>>,
}
impl Kept {
pub fn get(&self, path: &Path) -> Option<&[u8]> {
self.intact.get(path).map(Vec::as_slice)
}
pub(super) fn drifted(&self, path: &Path) -> Option<&[u8]> {
self.drifted.get(path).map(Vec::as_slice)
}
}
pub fn verify_state(home: &Home, lock: &Lock) -> (Vec<Drift>, Home) {
let mut files = super::state_files(home);
files.push(home.packs_path());
let (drift, kept) = lock.verify_keeping(&files, &[home.packs_dir()]);
(drift, home.keeping(kept))
}
impl Lock {
pub fn verify_keeping(&self, files: &[PathBuf], dirs: &[PathBuf]) -> (Vec<Drift>, Kept) {
let wanted: BTreeMap<String, &PathBuf> = files.iter().map(|p| (key(p), p)).collect();
let dirs: Vec<(PathBuf, &PathBuf)> = dirs
.iter()
.filter_map(|d| Some((PathBuf::from(key(&d.join("x"))).parent()?.to_owned(), d)))
.collect();
let requested = |stored: &str| -> Option<PathBuf> {
if let Some(path) = wanted.get(stored) {
return Some((*path).clone());
}
let stored = Path::new(stored);
let (parent, name) = (stored.parent()?, stored.file_name()?);
let (_, dir) = dirs.iter().find(|(canonical, _)| canonical == parent)?;
Some(dir.join(name))
};
let mut kept = Kept::default();
let mut drift = Vec::new();
for (stored, expected) in &self.entries {
if key(Path::new(stored)) != *stored {
drift.push(Drift::Modified(PathBuf::from(stored)));
continue;
}
match self.check_one(Path::new(stored), expected) {
Ok(read) => {
if let (Some(path), Some(read)) = (requested(stored), read) {
kept.intact.insert(path, read.contents);
}
}
Err((found, read)) => {
if let (Drift::KeysChanged(path, _), Some(read)) = (&found, read)
&& read.regular
&& (is_claude_settings(path) || spec_keys::listed(path).is_some())
{
kept.drifted.insert(path.clone(), read.contents);
}
drift.push(found);
}
}
}
drift.extend(self.codex_drift());
(drift, kept)
}
fn codex_drift(&self) -> impl Iterator<Item = Drift> + '_ {
self.codex_profiles.iter().filter_map(|(stored, expected)| {
let path = PathBuf::from(stored);
if key(&path) != *stored {
return Some(Drift::Modified(path));
}
if fs::symlink_metadata(&path).is_err() {
return Some(Drift::Missing(path));
}
match crate::sandbox::install::codex_part_digest(&path) {
Ok(actual) if &actual == expected => None,
Ok(_) => Some(Drift::Modified(path)),
Err(e) => Some(Drift::Unreadable(path, format!("{e:#}"))),
}
})
}
pub(super) fn check_one(
&self,
path: &Path,
expected: &str,
) -> Result<Option<ReadOnce>, (Drift, Option<ReadOnce>)> {
if fs::symlink_metadata(path).is_err() {
if expected == ABSENT {
return Ok(None);
}
return Err((Drift::Missing(path.to_path_buf()), None));
}
let read = ReadOnce::from(path).map_err(|e| {
(
Drift::Unreadable(path.to_path_buf(), format!("{e:#}")),
None,
)
})?;
if read
.digests(is_claude_settings(path), self.form(&key(path)))
.iter()
.any(|d| d == expected)
{
Ok(Some(read))
} else {
Err((
self.modified(path.to_path_buf(), &read.contents),
Some(read),
))
}
}
fn modified(&self, path: PathBuf, contents: &[u8]) -> Drift {
let now = keys::key_digests_of(&path, contents);
let pinned = self.keys.get(&key(&path));
match pinned.zip(now).and_then(|(p, n)| keys::describe(p, &n)) {
Some(keys) => Drift::KeysChanged(path, keys),
None => Drift::Modified(path),
}
}
}
#[cfg(test)]
mod tests;