use anyhow::{Context, ensure};
use concepts::ContentDigest;
use concepts::cas::Cas;
use concepts::component_id::Digest;
use sha2::{Digest as _, Sha256};
use std::path::PathBuf;
use std::sync::Arc;
#[async_trait::async_trait]
pub(crate) trait FileProvider: Send + Sync {
async fn read(&self, path: &str, digest: Option<&ContentDigest>) -> anyhow::Result<Vec<u8>>;
}
pub(crate) struct DiskProvider {
pub(crate) deployment_dir: PathBuf,
}
#[async_trait::async_trait]
impl FileProvider for DiskProvider {
async fn read(&self, path: &str, digest: Option<&ContentDigest>) -> anyhow::Result<Vec<u8>> {
let full = self.deployment_dir.join(path);
let bytes = tokio::fs::read(&full)
.await
.with_context(|| format!("cannot read file {full:?}"))?;
verify_content_digest(&bytes, digest, path)?;
Ok(bytes)
}
}
pub(crate) struct CasFileProvider {
pub(crate) cas: Arc<dyn Cas>,
}
#[async_trait::async_trait]
impl FileProvider for CasFileProvider {
async fn read(&self, path: &str, digest: Option<&ContentDigest>) -> anyhow::Result<Vec<u8>> {
let digest = digest.with_context(|| {
format!("CAS-backed canonicalization requires a content digest for `{path}`")
})?;
self.cas
.read_blob(digest)
.await?
.with_context(|| format!("blob {digest} for `{path}` not present in the CAS"))
}
}
pub(crate) fn verify_content_digest(
bytes: &[u8],
expected: Option<&ContentDigest>,
what: &str,
) -> anyhow::Result<()> {
if let Some(expected) = expected {
let hash: [u8; 32] = Sha256::digest(bytes).into();
let actual = ContentDigest(Digest(hash));
ensure!(
*expected == actual,
"content digest mismatch for {what}: expected {expected}, got {actual}"
);
}
Ok(())
}