use super::{local_registry::OCI_IMAGE_REF_NAME_ANNOTATION, LocalArtifact};
use anyhow::Context;
use oci_spec::image::{
Descriptor, DescriptorBuilder, Digest, ImageIndexBuilder, ImageManifest, MediaType,
OciLayoutBuilder,
};
use std::{
collections::HashMap,
fs::OpenOptions,
io::{BufWriter, Cursor, Write},
path::Path,
};
use tar::{EntryType, Header};
const OCI_LAYOUT_VERSION: &str = "1.0.0";
impl LocalArtifact<'_> {
pub fn save(&self, output: &Path) -> crate::Result<()> {
let manifest_digest = self.manifest_digest().clone();
let manifest_bytes = self.read_blob_by_digest(&manifest_digest)?;
let manifest: ImageManifest = serde_json::from_slice(&manifest_bytes)
.context("Failed to parse manifest from SQLite Local Registry")?;
let file = OpenOptions::new()
.write(true)
.create_new(true)
.open(output)
.with_context(|| {
format!(
"Failed to create archive at {} (file already exists or path is invalid)",
output.display()
)
})?;
let mut tar = tar::Builder::new(BufWriter::new(file));
tar.mode(tar::HeaderMode::Deterministic);
let oci_layout = OciLayoutBuilder::default()
.image_layout_version(OCI_LAYOUT_VERSION.to_string())
.build()
.context("Failed to build oci-layout JSON")?;
let layout_bytes =
serde_json::to_vec(&oci_layout).context("Failed to serialise oci-layout JSON")?;
append_tar_file(&mut tar, "oci-layout", &layout_bytes)?;
append_blob_entry(&mut tar, &manifest_digest, &manifest_bytes)?;
let config = self
.registry()
.stored_descriptor(manifest.config().clone())?;
let config_bytes = self.get_blob(&config)?;
verify_blob(&config, config_bytes.len(), "config")?;
append_blob_entry(&mut tar, config.digest(), &config_bytes)?;
for layer in manifest.layers() {
let layer = self.registry().stored_descriptor(layer.clone())?;
let layer_bytes = self.get_blob(&layer)?;
verify_blob(&layer, layer_bytes.len(), "layer")?;
append_blob_entry(&mut tar, layer.digest(), &layer_bytes)?;
}
let manifest_descriptor = DescriptorBuilder::default()
.media_type(MediaType::ImageManifest)
.digest(manifest_digest)
.size(manifest_bytes.len() as u64)
.annotations({
let mut map = HashMap::new();
map.insert(
OCI_IMAGE_REF_NAME_ANNOTATION.to_string(),
self.image_name().to_string(),
);
map
})
.build()
.context("Failed to build manifest descriptor for archive index.json")?;
let image_index = ImageIndexBuilder::default()
.schema_version(2u32)
.media_type(MediaType::ImageIndex)
.manifests(vec![manifest_descriptor])
.build()
.context("Failed to build OCI Image Index for archive index.json")?;
let index_bytes = serde_json::to_vec(&image_index)
.context("Failed to serialise OCI Image Index for archive index.json")?;
append_tar_file(&mut tar, "index.json", &index_bytes)?;
let mut writer = tar.into_inner().context("Failed to finalise tar archive")?;
writer
.flush()
.with_context(|| format!("Failed to flush archive writer to {}", output.display()))?;
Ok(())
}
}
fn append_tar_file<W: Write>(
tar: &mut tar::Builder<W>,
path: &str,
bytes: &[u8],
) -> crate::Result<()> {
let mut header = Header::new_ustar();
header.set_size(bytes.len() as u64);
header.set_mode(0o644);
header.set_mtime(0);
header.set_entry_type(EntryType::Regular);
header
.set_path(path)
.with_context(|| format!("Tar entry path {path} is not representable in USTAR"))?;
header.set_cksum();
tar.append(&header, Cursor::new(bytes))
.with_context(|| format!("Failed to append tar entry {path}"))
}
fn append_blob_entry<W: Write>(
tar: &mut tar::Builder<W>,
digest: &Digest,
bytes: &[u8],
) -> crate::Result<()> {
let path = format!("blobs/{}/{}", digest.algorithm().as_ref(), digest.digest());
append_tar_file(tar, &path, bytes)
}
fn verify_blob(descriptor: &Descriptor, actual_size: usize, kind: &str) -> crate::Result<()> {
crate::ensure!(
actual_size as u64 == descriptor.size(),
"{kind} size mismatch on save: manifest claims {}, blob is {} bytes",
descriptor.size(),
actual_size,
);
Ok(())
}