use std::io::Cursor;
use serde::{Deserialize, Serialize};
use tar::{Builder, Header};
use crate::artifact::{ArtifactKind, NativeArtifacts, RustCrateType};
use crate::identity::{
CMetadata, CrateName, CrateVersion, DependencyCMetadataJson, FeaturesJson, TargetTriple,
WireRustcVersion,
};
use crate::platform::Profile;
pub const STOW_BUNDLE_MEDIA_TYPE: &str = "application/vnd.stow.bundle.v1+tar";
pub const STOW_RLIB_MEDIA_TYPE: &str = "application/vnd.stow.rlib.v1";
pub const STOW_RMETA_MEDIA_TYPE: &str = "application/vnd.stow.rmeta.v1";
pub const STOW_DYLIB_MEDIA_TYPE: &str = "application/vnd.stow.dylib.v1";
pub const STOW_PROC_MACRO_MEDIA_TYPE: &str = "application/vnd.stow.proc-macro.v1";
pub const STOW_NATIVE_ARCHIVE_MEDIA_TYPE: &str = "application/vnd.stow.native-out-dir.v1+tar";
pub const STOW_ZSTD_MEDIA_TYPE_SUFFIX: &str = "+zstd";
pub const STOW_BUNDLE_MANIFEST_PATH: &str = "manifest.json";
pub const STOW_OCI_MANIFEST_PATH: &str = "oci/manifest.json";
pub const STOW_OCI_CONFIG_PATH: &str = "oci/config.json";
pub const STOW_SIGSTORE_PAYLOAD_DIR: &str = "sigstore";
pub const STOW_BUNDLE_FILES_DIR: &str = "files";
pub const STOW_ARTIFACT_CONFIG_MEDIA_TYPE: &str = "application/vnd.stow.artifact.config.v1+json";
pub const STOW_BUNDLE_CONFIG_MEDIA_TYPE: &str = "application/vnd.stow.bundle.config.v1+json";
pub const OCI_IMAGE_MANIFEST_MEDIA_TYPE: &str = "application/vnd.oci.image.manifest.v1+json";
pub const SIGSTORE_OCI_MEDIA_TYPE: &str = "application/vnd.dev.cosign.simplesigning.v1+json";
pub const SIGSTORE_SIGNATURE_ANNOTATION: &str = "dev.cosignproject.cosign/signature";
pub const SIGSTORE_BUNDLE_ANNOTATION: &str = "dev.sigstore.cosign/bundle";
pub const SIGSTORE_CERT_ANNOTATION: &str = "dev.sigstore.cosign/certificate";
#[must_use]
pub fn sigstore_signature_tag(oci_digest: &str) -> String {
format!("{}.sig", oci_digest.replace(':', "-"))
}
#[must_use]
pub fn bundle_file_path(file_name: &str) -> String {
format!("{STOW_BUNDLE_FILES_DIR}/{file_name}")
}
#[must_use]
pub fn sigstore_payload_path(index: usize) -> String {
format!("{STOW_SIGSTORE_PAYLOAD_DIR}/payload-{index}.json")
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BundleArtifactConfig {
pub oci_reference: String,
pub oci_digest: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BundleSignatureMaterial {
pub payload_path: String,
pub payload_bytes: Vec<u8>,
pub signature: String,
pub certificate_pem: String,
pub rekor_bundle_json: Option<String>,
}
#[derive(Debug, Clone)]
pub struct BundleParts<'a> {
pub oci_reference: &'a str,
pub oci_digest: &'a str,
pub manifest_bytes: &'a [u8],
pub config_bytes: &'a [u8],
pub config: &'a ArtifactBlobConfig,
pub signatures: &'a [BundleSignatureMaterial],
pub layers: &'a [BundleLayer<'a>],
}
#[derive(Debug, Clone)]
pub struct BundleLayer<'a> {
pub media_type: &'a str,
pub bytes: &'a [u8],
}
#[derive(Debug, thiserror::Error)]
pub enum BundleAssemblyError {
#[error("serialize bundle manifest: {0}")]
SerializeManifest(serde_json::Error),
#[error("build bundle tar: {0}")]
BuildTar(std::io::Error),
#[error("bundle layers do not match config outputs: {0}")]
LayerMismatch(String),
}
pub fn assemble_bundle(parts: &BundleParts<'_>) -> Result<Vec<u8>, BundleAssemblyError> {
let expected = parts
.config
.outputs
.iter()
.chain(parts.config.native_archive.as_ref())
.collect::<Vec<_>>();
if expected.len() != parts.layers.len() {
return Err(BundleAssemblyError::LayerMismatch(format!(
"{} layers for {} declared files",
parts.layers.len(),
expected.len()
)));
}
for (file, layer) in expected.iter().zip(parts.layers) {
let storage_media_type = file.storage_media_type();
if layer.media_type != storage_media_type {
return Err(BundleAssemblyError::LayerMismatch(format!(
"{} is stored as {} but the layer is {}",
file.file_name, storage_media_type, layer.media_type
)));
}
}
let mut tar = Builder::new(Vec::new());
let manifest = ArtifactBundleManifest {
oci_reference: parts.oci_reference.to_owned(),
oci_digest: parts.oci_digest.to_owned(),
config: parts.config.clone(),
sigstore_signatures: parts
.signatures
.iter()
.map(|material| SigstoreSignature {
payload_path: material.payload_path.clone(),
signature: material.signature.clone(),
certificate_pem: material.certificate_pem.clone(),
rekor_bundle_json: material.rekor_bundle_json.clone(),
})
.collect(),
};
let manifest_json =
serde_json::to_vec(&manifest).map_err(BundleAssemblyError::SerializeManifest)?;
append_entry(&mut tar, STOW_BUNDLE_MANIFEST_PATH, &manifest_json)?;
append_entry(&mut tar, STOW_OCI_MANIFEST_PATH, parts.manifest_bytes)?;
append_entry(&mut tar, STOW_OCI_CONFIG_PATH, parts.config_bytes)?;
for material in parts.signatures {
append_entry(&mut tar, &material.payload_path, &material.payload_bytes)?;
}
for (file, layer) in expected.iter().zip(parts.layers) {
append_entry(&mut tar, &bundle_file_path(&file.file_name), layer.bytes)?;
}
tar.into_inner().map_err(BundleAssemblyError::BuildTar)
}
fn append_entry(
tar: &mut Builder<Vec<u8>>,
path: &str,
bytes: &[u8],
) -> Result<(), BundleAssemblyError> {
let mut header = Header::new_gnu();
header.set_size(bytes.len() as u64);
header.set_mode(0o644);
header.set_cksum();
tar.append_data(&mut header, path, Cursor::new(bytes))
.map_err(BundleAssemblyError::BuildTar)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArtifactBundleManifest {
pub oci_reference: String,
pub oci_digest: String,
pub config: ArtifactBlobConfig,
pub sigstore_signatures: Vec<SigstoreSignature>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ArtifactBlobConfig {
pub compile_key: String,
pub crate_name: CrateName,
pub crate_version: CrateVersion,
pub c_metadata: CMetadata,
pub extra_filename: String,
pub target: TargetTriple,
pub rustc_version: WireRustcVersion,
pub features_json: FeaturesJson,
pub dependency_c_metadata_json: DependencyCMetadataJson,
pub dependency_compile_keys_json: String,
pub profile: Profile,
pub emit: Vec<String>,
pub artifact_size: u64,
#[serde(default)]
pub compile_millis: u64,
pub kind: ArtifactKind,
pub crate_types: Vec<RustCrateType>,
pub outputs: Vec<ArtifactBundleFile>,
pub native: Option<NativeArtifacts>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub native_archive: Option<ArtifactBundleFile>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ArtifactBundleFile {
pub file_name: String,
pub media_type: String,
pub sha256: String,
}
impl ArtifactBundleFile {
#[must_use]
pub fn storage_media_type(&self) -> String {
format!("{}{}", self.media_type, STOW_ZSTD_MEDIA_TYPE_SUFFIX)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SigstoreSignature {
pub payload_path: String,
pub signature: String,
pub certificate_pem: String,
pub rekor_bundle_json: Option<String>,
}