use std::path::{Path, PathBuf};
use aion_awl::command::recipe::{ArtifactSource, MeasuredTree, ReceiptError, SourceUnreadable};
pub(crate) struct WorkingTree {
root: PathBuf,
}
impl WorkingTree {
pub(crate) fn rooted(root: &Path) -> Self {
Self {
root: root.to_path_buf(),
}
}
fn resolve(&self, address: &str) -> Result<PathBuf, SourceUnreadable> {
let candidate = self.root.join(address);
if Path::new(address).is_absolute()
|| Path::new(address)
.components()
.any(|component| matches!(component, std::path::Component::ParentDir))
{
return Err(SourceUnreadable::Refused {
address: address.to_owned(),
detail: format!(
"the address leaves the root {}; a declared artifact names bytes inside the \
tree the recipe was declared against",
self.root.display()
),
});
}
Ok(candidate)
}
pub(crate) fn measure(&self) -> Result<MeasuredTree, ReceiptError> {
let head = git(&self.root, &["rev-parse", "HEAD"]).unwrap_or_else(|| "unknown".to_owned());
let clean = git(&self.root, &["status", "--porcelain"])
.is_some_and(|status| status.trim().is_empty());
MeasuredTree::new(head, self.root.display().to_string(), clean)
}
}
impl ArtifactSource for WorkingTree {
fn read(&self, address: &str) -> Result<Vec<u8>, SourceUnreadable> {
let path = self.resolve(address)?;
if !path.exists() {
return Err(SourceUnreadable::Absent {
address: address.to_owned(),
});
}
std::fs::read(&path).map_err(|error| SourceUnreadable::Refused {
address: address.to_owned(),
detail: error.to_string(),
})
}
}
fn git(root: &Path, arguments: &[&str]) -> Option<String> {
let output = std::process::Command::new("git")
.arg("-C")
.arg(root)
.args(arguments)
.output()
.ok()?;
output
.status
.success()
.then(|| String::from_utf8_lossy(&output.stdout).trim().to_owned())
}