#[cfg(test)]
mod tests;
use super::{BuildReuseError, add_optional, collect_files, hash_field};
use sha2::{Digest, Sha256};
use std::{collections::BTreeMap, fs, path::Path};
pub(super) struct BuildInputSnapshot {
pub(super) identity: String,
pub(super) files: BTreeMap<String, String>,
}
impl BuildInputSnapshot {
pub(super) fn digest(&self) -> String {
let mut digest = Sha256::new();
hash_field(&mut digest, self.identity.as_bytes());
for (path, hash) in &self.files {
hash_field(&mut digest, path.as_bytes());
hash_field(&mut digest, hash.as_bytes());
}
format!("{:x}", digest.finalize())
}
pub(super) fn validate_after(&self, after: &Self) -> Result<(), BuildReuseError> {
if self.identity != after.identity {
return Err(BuildReuseError::Changed);
}
let mut retained = BTreeMap::new();
for path in self
.files
.keys()
.filter(|path| !after.files.contains_key(*path))
{
if retained.contains_key(path) {
continue;
}
let input = Path::new(path);
if fs::symlink_metadata(input).is_ok_and(|metadata| metadata.is_dir()) {
collect_files(input, input, &mut retained, true)?;
} else {
add_optional(input, &mut retained)?;
}
}
for (path, before) in &self.files {
if after.files.get(path).or_else(|| retained.get(path)) != Some(before) {
return Err(BuildReuseError::ChangedInput(path.into()));
}
}
if let Some(path) = after
.files
.keys()
.chain(retained.keys())
.find(|path| !self.files.contains_key(*path))
{
if Path::new(path).ancestors().skip(1).any(|ancestor| {
ancestor
.to_str()
.and_then(|key| self.files.get(key))
.is_some_and(|value| value == "directory")
}) {
return Err(BuildReuseError::ChangedInput(path.into()));
}
return Err(BuildReuseError::UnobservedInput(path.into()));
}
Ok(())
}
}