use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct OciDescriptor {
#[serde(rename = "mediaType")]
pub media_type: String,
pub digest: String,
pub size: u64,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct OciManifest {
#[serde(rename = "schemaVersion")]
pub schema_version: u32,
#[serde(rename = "mediaType")]
pub media_type: String,
pub config: OciDescriptor,
pub layers: Vec<OciDescriptor>,
pub annotations: Option<std::collections::HashMap<String, String>>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct OciConfig {
pub architecture: String,
pub os: String,
pub rootfs: OciRootFs,
pub created: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct OciRootFs {
pub r#type: String,
pub diff_ids: Vec<String>,
}
pub struct OciBuilder;
impl OciBuilder {
pub fn build_oci_layout(
output_dir: &Path,
app_name: &str,
artifacts: &[PathBuf],
) -> Result<PathBuf> {
fs::create_dir_all(output_dir)?;
let layout_path = output_dir.join("oci-layout");
let layout_content = serde_json::json!({
"imageLayoutVersion": "1.0.0"
});
fs::write(&layout_path, serde_json::to_string_pretty(&layout_content)?)?;
let blobs_dir = output_dir.join("blobs").join("sha256");
fs::create_dir_all(&blobs_dir)?;
let mut layer_bytes = Vec::new();
layer_bytes.extend_from_slice(format!("RIVOX_OCI_LAYER_HEADER:{}\n", app_name).as_bytes());
for artifact in artifacts {
layer_bytes.extend_from_slice(artifact.to_string_lossy().as_bytes());
layer_bytes.push(b'\n');
}
let layer_hash = hex::encode(Sha256::digest(&layer_bytes));
let layer_digest = format!("sha256:{}", layer_hash);
let layer_size = layer_bytes.len() as u64;
fs::write(blobs_dir.join(&layer_hash), &layer_bytes)?;
let config = OciConfig {
architecture: "amd64".to_string(),
os: "linux".to_string(),
rootfs: OciRootFs {
r#type: "layers".to_string(),
diff_ids: vec![layer_digest.clone()],
},
created: "1970-01-01T00:00:00Z".to_string(), };
let config_bytes = serde_json::to_vec_pretty(&config)?;
let config_hash = hex::encode(Sha256::digest(&config_bytes));
let config_digest = format!("sha256:{}", config_hash);
let config_size = config_bytes.len() as u64;
fs::write(blobs_dir.join(&config_hash), &config_bytes)?;
let mut annotations = std::collections::HashMap::new();
annotations.insert(
"org.opencontainers.image.title".to_string(),
app_name.to_string(),
);
annotations.insert(
"org.opencontainers.image.created".to_string(),
"1970-01-01T00:00:00Z".to_string(),
);
let manifest = OciManifest {
schema_version: 2,
media_type: "application/vnd.oci.image.manifest.v1+json".to_string(),
config: OciDescriptor {
media_type: "application/vnd.oci.image.config.v1+json".to_string(),
digest: config_digest,
size: config_size,
},
layers: vec![OciDescriptor {
media_type: "application/vnd.oci.image.layer.v1.tar+gzip".to_string(),
digest: layer_digest,
size: layer_size,
}],
annotations: Some(annotations),
};
let manifest_bytes = serde_json::to_vec_pretty(&manifest)?;
let manifest_hash = hex::encode(Sha256::digest(&manifest_bytes));
let manifest_digest = format!("sha256:{}", manifest_hash);
let manifest_size = manifest_bytes.len() as u64;
fs::write(blobs_dir.join(&manifest_hash), &manifest_bytes)?;
let index_content = serde_json::json!({
"schemaVersion": 2,
"manifests": [{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": manifest_digest,
"size": manifest_size
}]
});
fs::write(
output_dir.join("index.json"),
serde_json::to_string_pretty(&index_content)?,
)?;
Ok(output_dir.to_path_buf())
}
}