Skip to main content

a3s_box_runtime/oci/build/cache/export/
model.rs

1//! Durable identity and receipt types for one native cache export.
2
3use std::path::PathBuf;
4
5use a3s_box_core::error::Result;
6use a3s_box_core::platform::Platform;
7use oci_spec::image::MediaType;
8use serde::{Deserialize, Serialize};
9use sha2::{Digest, Sha256};
10
11use super::{cache_error, CACHE_CONFIG_SCHEMA, MAX_CACHE_ENTRIES};
12use crate::oci::build::BuildOutputDescriptor;
13use crate::oci::image::canonical_sha256_digest_hex;
14
15const CACHE_KEY_PROFILE: &str = "a3s.box.build-cache-key.v1";
16
17/// Path-independent evidence for one portable native cache artifact.
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(rename_all = "camelCase", deny_unknown_fields)]
20pub struct BuildCacheReceipt {
21    pub schema: String,
22    pub key: String,
23    pub source_digest: String,
24    pub plan_digest: String,
25    pub descriptor: BuildOutputDescriptor,
26    pub platform: Platform,
27    pub content_bytes: u64,
28    pub entry_count: u64,
29    pub blob_count: u64,
30    pub blob_inventory_digest: String,
31}
32
33impl BuildCacheReceipt {
34    pub const SCHEMA: &'static str = "a3s.box.build-cache-receipt.v1";
35
36    pub(in crate::oci::build) fn validate(&self) -> Result<()> {
37        if self.schema != Self::SCHEMA
38            || canonical_sha256_digest_hex(&self.key).is_err()
39            || canonical_sha256_digest_hex(&self.source_digest).is_err()
40            || canonical_sha256_digest_hex(&self.plan_digest).is_err()
41            || canonical_sha256_digest_hex(&self.descriptor.digest).is_err()
42            || canonical_sha256_digest_hex(&self.blob_inventory_digest).is_err()
43            || self.descriptor.media_type != MediaType::ImageManifest.as_ref()
44            || self.descriptor.size == 0
45            || self.content_bytes < self.descriptor.size
46            || self.entry_count > MAX_CACHE_ENTRIES as u64
47            || self.blob_count < 2
48            || self.platform.os != "linux"
49            || self.platform.architecture.trim().is_empty()
50            || self.key != cache_key(&self.source_digest, &self.plan_digest, &self.platform)?
51        {
52            return Err(cache_error(
53                "native build cache receipt violates its closed identity",
54            ));
55        }
56        Ok(())
57    }
58}
59
60/// Revalidated portable cache artifact owned by a recorded operation.
61#[derive(Debug)]
62pub struct RecordedBuildCache {
63    pub receipt: BuildCacheReceipt,
64    pub layout_directory: PathBuf,
65}
66
67/// Immutable identity compiled into the cache artifact itself.
68#[derive(Debug, Clone)]
69pub(in crate::oci::build) struct BuildCacheExportIdentity {
70    pub(super) source_digest: String,
71    pub(super) plan_digest: String,
72    pub(super) platform: Platform,
73    pub(super) key: String,
74}
75
76impl BuildCacheExportIdentity {
77    pub(in crate::oci::build) fn new(
78        source_digest: impl Into<String>,
79        plan_digest: impl Into<String>,
80        platform: Platform,
81    ) -> Result<Self> {
82        let source_digest = source_digest.into();
83        let plan_digest = plan_digest.into();
84        canonical_sha256_digest_hex(&source_digest)?;
85        canonical_sha256_digest_hex(&plan_digest)?;
86        if platform.os != "linux" || platform.architecture.trim().is_empty() {
87            return Err(cache_error("native build cache platform is invalid"));
88        }
89        let key = cache_key(&source_digest, &plan_digest, &platform)?;
90        Ok(Self {
91            source_digest,
92            plan_digest,
93            platform,
94            key,
95        })
96    }
97}
98
99fn cache_key(source_digest: &str, plan_digest: &str, platform: &Platform) -> Result<String> {
100    let mut digest = Sha256::new();
101    for value in [
102        CACHE_KEY_PROFILE,
103        CACHE_CONFIG_SCHEMA,
104        source_digest,
105        plan_digest,
106        platform.os.as_str(),
107        platform.architecture.as_str(),
108        platform.variant.as_deref().unwrap_or(""),
109    ] {
110        let length = u64::try_from(value.len())
111            .map_err(|_| cache_error("native cache key field exceeds its bound"))?;
112        digest.update(length.to_be_bytes());
113        digest.update(value.as_bytes());
114    }
115    Ok(format!("sha256:{:x}", digest.finalize()))
116}