use std::path::Path;
use tar::Builder;
use crate::error::{BuildError, Result};
pub(crate) struct ArchiveBlob<'a> {
pub digest: &'a str,
pub bytes: &'a [u8],
}
#[cfg_attr(target_os = "windows", allow(dead_code))]
pub(crate) fn write_oci_image_layout_archive(
dest: &Path,
tag: &str,
manifest: &ArchiveBlob<'_>,
manifest_size: i64,
config: &ArchiveBlob<'_>,
layer: &ArchiveBlob<'_>,
) -> Result<()> {
write_oci_image_layout_archive_multi(
dest,
tag,
manifest,
manifest_size,
&[
ArchiveBlob {
digest: config.digest,
bytes: config.bytes,
},
ArchiveBlob {
digest: layer.digest,
bytes: layer.bytes,
},
],
)
}
pub(crate) fn write_oci_image_layout_archive_multi(
dest: &Path,
tag: &str,
manifest: &ArchiveBlob<'_>,
manifest_size: i64,
blobs: &[ArchiveBlob<'_>],
) -> Result<()> {
let strip = |d: &str| d.strip_prefix("sha256:").unwrap_or(d).to_string();
let index = serde_json::json!({
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.index.v1+json",
"manifests": [{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": manifest.digest,
"size": manifest_size,
"annotations": { "org.opencontainers.image.ref.name": tag },
}],
});
let index_bytes = serde_json::to_vec(&index).map_err(|e| {
BuildError::IoError(std::io::Error::other(format!(
"failed to serialize OCI index.json: {e}"
)))
})?;
let file = std::fs::File::create(dest).map_err(|e| {
BuildError::IoError(std::io::Error::new(
e.kind(),
format!("failed to create OCI archive {}: {e}", dest.display()),
))
})?;
let mut ar = Builder::new(file);
append_entry(&mut ar, "oci-layout", br#"{"imageLayoutVersion":"1.0.0"}"#)?;
append_entry(&mut ar, "index.json", &index_bytes)?;
append_entry(
&mut ar,
&format!("blobs/sha256/{}", strip(manifest.digest)),
manifest.bytes,
)?;
let mut seen = std::collections::HashSet::new();
for blob in blobs {
let name = strip(blob.digest);
if !seen.insert(name.clone()) {
continue;
}
append_entry(&mut ar, &format!("blobs/sha256/{name}"), blob.bytes)?;
}
ar.finish().map_err(|e| {
BuildError::IoError(std::io::Error::new(
e.kind(),
format!("failed to finalize OCI archive: {e}"),
))
})?;
Ok(())
}
fn append_entry(ar: &mut Builder<std::fs::File>, name: &str, data: &[u8]) -> Result<()> {
let mut header = tar::Header::new_gnu();
header.set_size(data.len() as u64);
header.set_mode(0o644);
header.set_cksum();
ar.append_data(&mut header, name, data).map_err(|e| {
BuildError::IoError(std::io::Error::new(
e.kind(),
format!("failed to append {name} to OCI archive: {e}"),
))
})
}
#[cfg(test)]
mod tests {
use super::*;
use sha2::{Digest, Sha256};
use std::io::Read;
fn digest(bytes: &[u8]) -> String {
format!("sha256:{:x}", Sha256::digest(bytes))
}
#[test]
fn writes_importable_single_manifest_layout() {
let layer = b"fake-layer-tar-gz".as_slice();
let config = br#"{"architecture":"arm64","os":"linux"}"#.as_slice();
let manifest =
br#"{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json"}"#
.as_slice();
let (ld, cd, md) = (digest(layer), digest(config), digest(manifest));
let dir = tempfile::tempdir().unwrap();
let dest = dir.path().join("img.tar");
write_oci_image_layout_archive(
&dest,
"repro/buildah-fail:latest",
&ArchiveBlob {
digest: &md,
bytes: manifest,
},
i64::try_from(manifest.len()).unwrap(),
&ArchiveBlob {
digest: &cd,
bytes: config,
},
&ArchiveBlob {
digest: &ld,
bytes: layer,
},
)
.unwrap();
let f = std::fs::File::open(&dest).unwrap();
let mut ar = tar::Archive::new(f);
let mut files = std::collections::HashMap::new();
for entry in ar.entries().unwrap() {
let mut entry = entry.unwrap();
let path = entry.path().unwrap().to_string_lossy().to_string();
let mut buf = Vec::new();
entry.read_to_end(&mut buf).unwrap();
files.insert(path, buf);
}
assert_eq!(
files.get("oci-layout").map(Vec::as_slice),
Some(&b"{\"imageLayoutVersion\":\"1.0.0\"}"[..])
);
let strip = |d: &str| d.strip_prefix("sha256:").unwrap().to_string();
assert!(files.contains_key(&format!("blobs/sha256/{}", strip(&md))));
assert!(files.contains_key(&format!("blobs/sha256/{}", strip(&cd))));
assert!(files.contains_key(&format!("blobs/sha256/{}", strip(&ld))));
let index: serde_json::Value =
serde_json::from_slice(files.get("index.json").unwrap()).unwrap();
let m0 = &index["manifests"][0];
assert_eq!(m0["digest"], md);
assert_eq!(m0["size"], i64::try_from(manifest.len()).unwrap());
assert_eq!(
m0["annotations"]["org.opencontainers.image.ref.name"],
"repro/buildah-fail:latest"
);
}
}