use std::cell::Cell;
use std::collections::HashSet;
use std::fs;
use std::path::Path;
use crate::reachability::{NodeSource, ReachError, collect_reachable};
use crate::store::disk::decompress_node;
use crate::tree::{Hash, Node};
use super::error::{MarkSourceId, VacuumError};
use super::inventory::{NodeFile, StoreInventory};
pub type StoreMarks = std::collections::HashMap<Hash, u64>;
struct VerifiedFileSource<'a> {
inventory: &'a StoreInventory,
store_dir: &'a Path,
marks: &'a StoreMarks,
verified: Cell<usize>,
}
impl NodeSource for VerifiedFileSource<'_> {
type Error = VacuumError;
type Bytes = u64;
fn resolve(&self, hash: Hash, stack: &mut Vec<Hash>) -> Result<Option<u64>, VacuumError> {
if let Some(&bytes) = self.marks.get(&hash) {
return Ok(Some(bytes));
}
let Some(file) = self.inventory.nodes.get(&hash) else {
return Ok(None);
};
let node = read_verified(self.store_dir, file, hash)?;
self.verified.set(self.verified.get() + 1);
push_children(&node, stack);
Ok(Some(file.compressed_len))
}
}
pub fn walk_declared(
inventory: &StoreInventory,
store_dir: &Path,
root: Hash,
source: &MarkSourceId,
marks: &mut StoreMarks,
verified: &mut usize,
) -> Result<(), VacuumError> {
let reached = {
let src = VerifiedFileSource {
inventory,
store_dir,
marks,
verified: Cell::new(0),
};
let result = collect_reachable(&src, [root]);
*verified += src.verified.get();
match result {
Ok(reached) => reached,
Err(ReachError::Source(error)) => return Err(error),
Err(ReachError::Missing(missing)) => {
return Err(VacuumError::UnresolvableRoot {
source: source.clone(),
root,
missing,
});
}
}
};
for (hash, bytes) in reached {
marks.insert(hash, bytes);
}
Ok(())
}
#[derive(Debug)]
pub enum ProbeOutcome {
Resolved(HashSet<Hash>),
Missing(Hash),
}
pub fn probe_store(
inventory: &StoreInventory,
store_dir: &Path,
root: Hash,
marks: &StoreMarks,
verified: &mut usize,
) -> Result<ProbeOutcome, VacuumError> {
let src = VerifiedFileSource {
inventory,
store_dir,
marks,
verified: Cell::new(0),
};
let result = collect_reachable(&src, [root]);
*verified += src.verified.get();
match result {
Ok(reached) => Ok(ProbeOutcome::Resolved(reached.into_keys().collect())),
Err(ReachError::Source(error)) => Err(error),
Err(ReachError::Missing(missing)) => Ok(ProbeOutcome::Missing(missing)),
}
}
pub fn commit_marks(inventory: &StoreInventory, resolved: &HashSet<Hash>, marks: &mut StoreMarks) {
for hash in resolved {
if let Some(file) = inventory.nodes.get(hash) {
marks.insert(*hash, file.compressed_len);
}
}
}
fn push_children(node: &Node, stack: &mut Vec<Hash>) {
if let Node::Internal(internal) = node {
stack.extend(
internal
.children()
.iter()
.map(|(_lower_bound, child_hash)| *child_hash),
);
}
}
fn read_verified(store_dir: &Path, file: &NodeFile, expected: Hash) -> Result<Node, VacuumError> {
let mismatch = || VacuumError::NodeHashMismatch {
store: store_dir.to_path_buf(),
path: file.path.clone(),
};
let compressed = fs::read(&file.path).map_err(|error| VacuumError::Io {
path: file.path.clone(),
error,
})?;
let serialised = decompress_node(&compressed).map_err(|_error| mismatch())?;
let node = Node::deserialise(&serialised).map_err(|_error| mismatch())?;
let (canonical, computed) = node.serialise_and_hash();
if computed != expected || canonical != serialised {
return Err(mismatch());
}
Ok(node)
}