1use std::io::Cursor;
9
10use serde::{Deserialize, Serialize};
11use tar::{Builder, Header};
12
13use crate::artifact::{ArtifactKind, NativeArtifacts, RustCrateType};
14use crate::identity::{
15 CMetadata, CrateName, CrateVersion, DependencyCMetadataJson, FeaturesJson, TargetTriple,
16 WireRustcVersion,
17};
18use crate::platform::Profile;
19
20pub const STOW_BUNDLE_MEDIA_TYPE: &str = "application/vnd.stow.bundle.v1+tar";
22pub const STOW_RLIB_MEDIA_TYPE: &str = "application/vnd.stow.rlib.v1";
24pub const STOW_RMETA_MEDIA_TYPE: &str = "application/vnd.stow.rmeta.v1";
26pub const STOW_DYLIB_MEDIA_TYPE: &str = "application/vnd.stow.dylib.v1";
28pub const STOW_PROC_MACRO_MEDIA_TYPE: &str = "application/vnd.stow.proc-macro.v1";
30pub const STOW_NATIVE_ARCHIVE_MEDIA_TYPE: &str = "application/vnd.stow.native-out-dir.v1+tar";
32pub const STOW_ZSTD_MEDIA_TYPE_SUFFIX: &str = "+zstd";
34pub const STOW_BUNDLE_MANIFEST_PATH: &str = "manifest.json";
36pub const STOW_OCI_MANIFEST_PATH: &str = "oci/manifest.json";
38pub const STOW_OCI_CONFIG_PATH: &str = "oci/config.json";
41pub const STOW_SIGSTORE_PAYLOAD_DIR: &str = "sigstore";
43pub const STOW_BUNDLE_FILES_DIR: &str = "files";
45pub const STOW_ARTIFACT_CONFIG_MEDIA_TYPE: &str = "application/vnd.stow.artifact.config.v1+json";
47pub const STOW_BUNDLE_CONFIG_MEDIA_TYPE: &str = "application/vnd.stow.bundle.config.v1+json";
49pub const OCI_IMAGE_MANIFEST_MEDIA_TYPE: &str = "application/vnd.oci.image.manifest.v1+json";
51pub const SIGSTORE_OCI_MEDIA_TYPE: &str = "application/vnd.dev.cosign.simplesigning.v1+json";
53pub const SIGSTORE_SIGNATURE_ANNOTATION: &str = "dev.cosignproject.cosign/signature";
55pub const SIGSTORE_BUNDLE_ANNOTATION: &str = "dev.sigstore.cosign/bundle";
57pub const SIGSTORE_CERT_ANNOTATION: &str = "dev.sigstore.cosign/certificate";
59
60#[must_use]
63pub fn sigstore_signature_tag(oci_digest: &str) -> String {
64 format!("{}.sig", oci_digest.replace(':', "-"))
65}
66
67#[must_use]
69pub fn bundle_file_path(file_name: &str) -> String {
70 format!("{STOW_BUNDLE_FILES_DIR}/{file_name}")
71}
72
73#[must_use]
75pub fn sigstore_payload_path(index: usize) -> String {
76 format!("{STOW_SIGSTORE_PAYLOAD_DIR}/payload-{index}.json")
77}
78
79#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
82pub struct BundleArtifactConfig {
83 pub oci_reference: String,
85 pub oci_digest: String,
87}
88
89#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct BundleSignatureMaterial {
93 pub payload_path: String,
95 pub payload_bytes: Vec<u8>,
97 pub signature: String,
99 pub certificate_pem: String,
101 pub rekor_bundle_json: Option<String>,
103}
104
105#[derive(Debug, Clone)]
111pub struct BundleParts<'a> {
112 pub oci_reference: &'a str,
114 pub oci_digest: &'a str,
116 pub manifest_bytes: &'a [u8],
118 pub config_bytes: &'a [u8],
120 pub config: &'a ArtifactBlobConfig,
122 pub signatures: &'a [BundleSignatureMaterial],
124 pub layers: &'a [BundleLayer<'a>],
128}
129
130#[derive(Debug, Clone)]
132pub struct BundleLayer<'a> {
133 pub media_type: &'a str,
135 pub bytes: &'a [u8],
137}
138
139#[derive(Debug, thiserror::Error)]
141pub enum BundleAssemblyError {
142 #[error("serialize bundle manifest: {0}")]
144 SerializeManifest(serde_json::Error),
145 #[error("build bundle tar: {0}")]
147 BuildTar(std::io::Error),
148 #[error("bundle layers do not match config outputs: {0}")]
150 LayerMismatch(String),
151}
152
153pub fn assemble_bundle(parts: &BundleParts<'_>) -> Result<Vec<u8>, BundleAssemblyError> {
164 let expected = parts
165 .config
166 .outputs
167 .iter()
168 .chain(parts.config.native_archive.as_ref())
169 .collect::<Vec<_>>();
170 if expected.len() != parts.layers.len() {
171 return Err(BundleAssemblyError::LayerMismatch(format!(
172 "{} layers for {} declared files",
173 parts.layers.len(),
174 expected.len()
175 )));
176 }
177 for (file, layer) in expected.iter().zip(parts.layers) {
178 let storage_media_type = file.storage_media_type();
179 if layer.media_type != storage_media_type {
180 return Err(BundleAssemblyError::LayerMismatch(format!(
181 "{} is stored as {} but the layer is {}",
182 file.file_name, storage_media_type, layer.media_type
183 )));
184 }
185 }
186
187 let mut tar = Builder::new(Vec::new());
188 let manifest = ArtifactBundleManifest {
189 oci_reference: parts.oci_reference.to_owned(),
190 oci_digest: parts.oci_digest.to_owned(),
191 config: parts.config.clone(),
192 sigstore_signatures: parts
193 .signatures
194 .iter()
195 .map(|material| SigstoreSignature {
196 payload_path: material.payload_path.clone(),
197 signature: material.signature.clone(),
198 certificate_pem: material.certificate_pem.clone(),
199 rekor_bundle_json: material.rekor_bundle_json.clone(),
200 })
201 .collect(),
202 };
203 let manifest_json =
204 serde_json::to_vec(&manifest).map_err(BundleAssemblyError::SerializeManifest)?;
205 append_entry(&mut tar, STOW_BUNDLE_MANIFEST_PATH, &manifest_json)?;
206 append_entry(&mut tar, STOW_OCI_MANIFEST_PATH, parts.manifest_bytes)?;
207 append_entry(&mut tar, STOW_OCI_CONFIG_PATH, parts.config_bytes)?;
208 for material in parts.signatures {
209 append_entry(&mut tar, &material.payload_path, &material.payload_bytes)?;
210 }
211 for (file, layer) in expected.iter().zip(parts.layers) {
212 append_entry(&mut tar, &bundle_file_path(&file.file_name), layer.bytes)?;
213 }
214 tar.into_inner().map_err(BundleAssemblyError::BuildTar)
215}
216
217fn append_entry(
218 tar: &mut Builder<Vec<u8>>,
219 path: &str,
220 bytes: &[u8],
221) -> Result<(), BundleAssemblyError> {
222 let mut header = Header::new_gnu();
223 header.set_size(bytes.len() as u64);
224 header.set_mode(0o644);
225 header.set_cksum();
226 tar.append_data(&mut header, path, Cursor::new(bytes))
227 .map_err(BundleAssemblyError::BuildTar)
228}
229
230#[derive(Debug, Clone, Serialize, Deserialize)]
233pub struct ArtifactBundleManifest {
234 pub oci_reference: String,
236 pub oci_digest: String,
238 pub config: ArtifactBlobConfig,
241 pub sigstore_signatures: Vec<SigstoreSignature>,
243}
244
245#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
247pub struct ArtifactBlobConfig {
248 pub compile_key: String,
250 pub crate_name: CrateName,
252 pub crate_version: CrateVersion,
254 pub c_metadata: CMetadata,
256 pub extra_filename: String,
258 pub target: TargetTriple,
260 pub rustc_version: WireRustcVersion,
262 pub features_json: FeaturesJson,
264 pub dependency_c_metadata_json: DependencyCMetadataJson,
266 pub dependency_compile_keys_json: String,
268 pub profile: Profile,
270 pub emit: Vec<String>,
272 pub artifact_size: u64,
274 #[serde(default)]
278 pub compile_millis: u64,
279 pub kind: ArtifactKind,
281 pub crate_types: Vec<RustCrateType>,
283 pub outputs: Vec<ArtifactBundleFile>,
285 pub native: Option<NativeArtifacts>,
287 #[serde(default, skip_serializing_if = "Option::is_none")]
294 pub native_archive: Option<ArtifactBundleFile>,
295}
296
297#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
299pub struct ArtifactBundleFile {
300 pub file_name: String,
302 pub media_type: String,
304 pub sha256: String,
306}
307
308impl ArtifactBundleFile {
309 #[must_use]
312 pub fn storage_media_type(&self) -> String {
313 format!("{}{}", self.media_type, STOW_ZSTD_MEDIA_TYPE_SUFFIX)
314 }
315}
316
317#[derive(Debug, Clone, Serialize, Deserialize)]
319pub struct SigstoreSignature {
320 pub payload_path: String,
322 pub signature: String,
324 pub certificate_pem: String,
326 pub rekor_bundle_json: Option<String>,
328}