a3s_box_runtime/oci/build/cache/export/
model.rs1use 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#[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#[derive(Debug)]
62pub struct RecordedBuildCache {
63 pub receipt: BuildCacheReceipt,
64 pub layout_directory: PathBuf,
65}
66
67#[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}