#[cfg(test)]
mod tests;
use super::{BuildReuseError, add_optional, collect_files, hash_field, source_entry_is_excluded};
use canic_core::cdk::utils::hash::hex_bytes;
use sha2_host::{Digest, Sha256};
use std::{
collections::BTreeMap,
fs,
path::{Component, 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());
}
hex_bytes(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()));
}
}
for (path, value) in after
.files
.iter()
.chain(&retained)
.filter(|(path, _)| !self.files.contains_key(*path))
{
if self.observed_absence(Path::new(path)) {
if value == "absent" {
continue;
}
return Err(BuildReuseError::ChangedInput(path.into()));
}
return Err(BuildReuseError::UnobservedInput(path.into()));
}
Ok(())
}
fn observed_absence(&self, path: &Path) -> bool {
let Some(directory) = path.ancestors().skip(1).find(|ancestor| {
ancestor
.to_str()
.and_then(|key| self.files.get(key))
.is_some_and(|value| value == "directory")
}) else {
return false;
};
let suffix = path.strip_prefix(directory).expect("path ancestor");
if !suffix
.components()
.all(|component| matches!(component, Component::Normal(_)))
{
return false;
}
let Some(Component::Normal(name)) = suffix.components().next() else {
return false;
};
!source_entry_is_excluded(name)
&& directory
.join(name)
.to_str()
.is_some_and(|key| !self.files.contains_key(key))
}
}