use pelagos::image::{
self, blob_exists, extract_layer, layer_dirs, layer_exists, list_images, load_image,
remove_image, save_image, HealthConfig, ImageConfig, ImageManifest,
};
use std::io::{Read as _, Write};
use super::auth::{parse_docker_config, remove_docker_config, resolve_auth, write_docker_config};
fn oci_client_config(registry: &str, insecure: bool) -> oci_client::client::ClientConfig {
use oci_client::client::{ClientConfig, ClientProtocol};
let host = registry.split(':').next().unwrap_or(registry);
let auto_insecure = host == "localhost"
|| host == "127.0.0.1"
|| host == "::1"
|| host.starts_with("192.168.")
|| host.starts_with("10.")
|| host.starts_with("172.") && {
host.split('.')
.nth(1)
.and_then(|s| s.parse::<u8>().ok())
.map(|n| (16..=31).contains(&n))
.unwrap_or(false)
};
if insecure || auto_insecure {
ClientConfig {
protocol: ClientProtocol::HttpsExcept(vec![registry.to_string()]),
..Default::default()
}
} else {
ClientConfig::default()
}
}
pub fn cmd_image_pull(
reference: &str,
username: Option<&str>,
password: Option<&str>,
password_stdin: bool,
insecure: bool,
) -> Result<(), Box<dyn std::error::Error>> {
let full_ref = normalise_reference(reference);
println!("Pulling {}...", full_ref);
let resolved_password = resolve_password(password, password_stdin)?;
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?;
rt.block_on(async {
pull_image(&full_ref, username, resolved_password.as_deref(), insecure).await
})
}
pub fn cmd_image_ls(json: bool) -> Result<(), Box<dyn std::error::Error>> {
let images = list_images();
if json {
println!("{}", serde_json::to_string_pretty(&images)?);
return Ok(());
}
if images.is_empty() {
println!("No images found. Use 'pelagos image pull <name>' to pull one.");
return Ok(());
}
println!("{:<30} {:<15} DIGEST", "REFERENCE", "LAYERS");
for img in &images {
let short_digest = if img.digest.len() > 19 {
&img.digest[7..19]
} else {
&img.digest
};
println!(
"{:<30} {:<15} {}",
img.reference,
img.layers.len(),
short_digest
);
}
Ok(())
}
pub fn cmd_image_rm(reference: &str) -> Result<(), Box<dyn std::error::Error>> {
let local_ref = add_default_tag(reference);
if remove_image(&local_ref).is_ok() {
println!("Removed image: {}", local_ref);
return Ok(());
}
let full_ref = normalise_reference(reference);
remove_image(&full_ref)?;
println!("Removed image: {}", full_ref);
Ok(())
}
pub fn cmd_image_push(
reference: &str,
dest: Option<&str>,
username: Option<&str>,
password: Option<&str>,
password_stdin: bool,
insecure: bool,
) -> Result<(), Box<dyn std::error::Error>> {
let src_ref = resolve_local_reference(reference);
let dest_ref = dest
.map(normalise_reference)
.unwrap_or_else(|| src_ref.clone());
println!("Pushing {} → {}...", src_ref, dest_ref);
let resolved_password = resolve_password(password, password_stdin)?;
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?;
rt.block_on(async {
push_image(
&src_ref,
&dest_ref,
username,
resolved_password.as_deref(),
insecure,
)
.await
})
}
pub fn cmd_image_login(
registry: &str,
username: Option<&str>,
password_stdin: bool,
) -> Result<(), Box<dyn std::error::Error>> {
let user = match username {
Some(u) => u.to_string(),
None => {
eprint!("Username: ");
std::io::stderr().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
s.trim().to_string()
}
};
let pass = if password_stdin {
let mut s = String::new();
std::io::stdin().read_to_string(&mut s)?;
s.trim().to_string()
} else {
eprint!("Password: ");
std::io::stderr().flush()?;
read_password_from_tty()?
};
write_docker_config(registry, &user, &pass)?;
println!("Login Succeeded ({} as {})", registry, user);
Ok(())
}
pub fn cmd_image_logout(registry: &str) -> Result<(), Box<dyn std::error::Error>> {
remove_docker_config(registry)?;
println!("Removed login credentials for {}", registry);
Ok(())
}
async fn pull_image(
reference: &str,
username: Option<&str>,
password: Option<&str>,
insecure: bool,
) -> Result<(), Box<dyn std::error::Error>> {
use oci_client::{Client, Reference as OciRef};
let oci_ref: OciRef = reference
.parse()
.map_err(|e| format!("invalid image reference '{}': {:?}", reference, e))?;
let registry = oci_ref.resolve_registry();
let auth = resolve_auth(registry, username, password);
let client = Client::new(oci_client_config(registry, insecure));
let (manifest, digest, config_json) = client
.pull_manifest_and_config(&oci_ref, &auth)
.await
.map_err(|e| format!("failed to pull manifest: {}", e))?;
println!(
" Manifest: {} ({} layers)",
&digest[..19.min(digest.len())],
manifest.layers.len()
);
let config = parse_image_config(&config_json)?;
let mut cached = 0usize;
let mut downloaded = 0usize;
for (i, layer_desc) in manifest.layers.iter().enumerate() {
let layer_digest = &layer_desc.digest;
if layer_exists(layer_digest) && blob_exists(layer_digest) {
cached += 1;
println!(
" Layer {}/{}: {} (cached)",
i + 1,
manifest.layers.len(),
&layer_digest[7..19.min(layer_digest.len())]
);
continue;
}
println!(
" Layer {}/{}: {} (downloading...)",
i + 1,
manifest.layers.len(),
&layer_digest[7..19.min(layer_digest.len())]
);
let mut blob_data: Vec<u8> = Vec::new();
client
.pull_blob(&oci_ref, layer_desc, &mut blob_data)
.await
.map_err(|e| format!("failed to pull layer {}: {}", layer_digest, e))?;
image::save_blob(layer_digest, &blob_data)?;
if !layer_exists(layer_digest) {
let mut tmp = tempfile::NamedTempFile::new()?;
tmp.write_all(&blob_data)?;
tmp.flush()?;
extract_layer(layer_digest, tmp.path())?;
}
downloaded += 1;
}
let layer_digests: Vec<String> = manifest.layers.iter().map(|l| l.digest.clone()).collect();
let img_manifest = ImageManifest {
reference: reference.to_string(),
digest,
layers: layer_digests,
config,
};
save_image(&img_manifest)?;
image::save_oci_config(reference, &config_json)?;
println!("Done: {} layers downloaded, {} cached", downloaded, cached);
Ok(())
}
async fn push_image(
src_ref: &str,
dest_ref: &str,
username: Option<&str>,
password: Option<&str>,
insecure: bool,
) -> Result<(), Box<dyn std::error::Error>> {
use oci_client::client::{Config, ImageLayer};
use oci_client::{Client, Reference as OciRef};
let dest_oci_ref: OciRef = dest_ref
.parse()
.map_err(|e| format!("invalid destination reference '{}': {:?}", dest_ref, e))?;
let registry = dest_oci_ref.resolve_registry();
let auth = resolve_auth(registry, username, password);
let manifest =
load_image(src_ref).map_err(|_| format!("image '{}' not found locally", src_ref))?;
let config_json = image::load_oci_config(src_ref)
.map_err(|_| format!("OCI config not found for '{}' — re-pull or rebuild the image to populate the blob cache", src_ref))?;
let mut layers = Vec::with_capacity(manifest.layers.len());
for digest in &manifest.layers {
if !blob_exists(digest) {
return Err(format!(
"blob not found for layer {} — re-pull or rebuild the image to populate the blob cache",
&digest[..19.min(digest.len())]
).into());
}
let data = image::load_blob(digest)?;
println!(
" Layer {}: {} bytes",
&digest[7..19.min(digest.len())],
data.len()
);
layers.push(ImageLayer::oci_v1_gzip(data, None));
}
let config = Config::oci_v1(config_json.into_bytes(), None);
let client = Client::new(oci_client_config(registry, insecure));
let response = client
.push(&dest_oci_ref, &layers, config, &auth, None)
.await
.map_err(|e| format!("push failed: {}", e))?;
println!("Pushed {}", dest_ref);
println!(" manifest: {}", response.manifest_url);
println!(" config: {}", response.config_url);
Ok(())
}
fn add_default_tag(reference: &str) -> String {
if reference.contains(':') || reference.contains('@') {
reference.to_string()
} else {
format!("{}:latest", reference)
}
}
pub fn normalise_reference(reference: &str) -> String {
let r = reference.to_string();
let r = if !r.contains(':') && !r.contains('@') {
format!("{}:latest", r)
} else {
r
};
if !r.contains('/') {
format!("docker.io/library/{}", r)
} else {
r
}
}
fn resolve_local_reference(reference: &str) -> String {
let local = add_default_tag(reference);
if load_image(&local).is_ok() {
return local;
}
normalise_reference(reference)
}
fn resolve_password(
password: Option<&str>,
password_stdin: bool,
) -> Result<Option<String>, Box<dyn std::error::Error>> {
if let Some(p) = password {
return Ok(Some(p.to_string()));
}
if password_stdin {
let mut s = String::new();
std::io::stdin().read_to_string(&mut s)?;
return Ok(Some(s.trim().to_string()));
}
Ok(None)
}
fn read_password_from_tty() -> Result<String, Box<dyn std::error::Error>> {
if let Ok(mut tty) = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open("/dev/tty")
{
use std::os::unix::io::AsRawFd;
let fd = tty.as_raw_fd();
let mut termios: libc::termios = unsafe { std::mem::zeroed() };
let saved = unsafe {
let ok = libc::tcgetattr(fd, &mut termios) == 0;
ok.then_some(termios)
};
if let Some(saved_termios) = saved {
let mut raw = saved_termios;
raw.c_lflag &= !(libc::ECHO | libc::ECHOE | libc::ECHOK | libc::ECHONL);
unsafe { libc::tcsetattr(fd, libc::TCSANOW, &raw) };
}
let mut pass = String::new();
let result = tty.read_to_string(&mut pass);
if let Some(saved_termios) = saved {
unsafe { libc::tcsetattr(fd, libc::TCSANOW, &saved_termios) };
}
eprintln!();
result?;
return Ok(pass.trim().to_string());
}
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
Ok(s.trim().to_string())
}
fn parse_oci_healthcheck(container_config: Option<&serde_json::Value>) -> Option<HealthConfig> {
let hc = container_config?.get("Healthcheck")?;
let test = hc.get("Test").and_then(|v| v.as_array())?;
if test.is_empty() {
return None;
}
let first = test[0].as_str().unwrap_or("");
if first == "NONE" {
return None;
}
let cmd: Vec<String> = match first {
"CMD" => test[1..]
.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect(),
"CMD-SHELL" => {
let shell_cmd = test.get(1).and_then(|v| v.as_str()).unwrap_or("");
vec![
"/bin/sh".to_string(),
"-c".to_string(),
shell_cmd.to_string(),
]
}
_ => test
.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect(),
};
if cmd.is_empty() {
return None;
}
let ns_to_secs = |field: &str| -> u64 {
hc.get(field)
.and_then(|v| v.as_u64())
.map(|ns| ns / 1_000_000_000)
.unwrap_or(0)
};
let interval_secs = {
let v = ns_to_secs("Interval");
if v == 0 {
30
} else {
v
}
};
let timeout_secs = {
let v = ns_to_secs("Timeout");
if v == 0 {
10
} else {
v
}
};
let start_period_secs = ns_to_secs("StartPeriod");
let retries = hc
.get("Retries")
.and_then(|v| v.as_u64())
.map(|n| n as u32)
.unwrap_or(3);
Some(HealthConfig {
cmd,
interval_secs,
timeout_secs,
start_period_secs,
retries,
})
}
pub(crate) fn parse_image_config(
config_json: &str,
) -> Result<ImageConfig, Box<dyn std::error::Error>> {
let value: serde_json::Value = serde_json::from_str(config_json)
.map_err(|e| format!("invalid image config JSON: {}", e))?;
let container_config = value
.get("config")
.or_else(|| value.get("container_config"));
let env = container_config
.and_then(|c| c.get("Env"))
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
let cmd = container_config
.and_then(|c| c.get("Cmd"))
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
let entrypoint = container_config
.and_then(|c| c.get("Entrypoint"))
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
let working_dir = container_config
.and_then(|c| c.get("WorkingDir"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let user = container_config
.and_then(|c| c.get("User"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let labels = container_config
.and_then(|c| c.get("Labels"))
.and_then(|v| v.as_object())
.map(|obj| {
obj.iter()
.filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
.collect()
})
.unwrap_or_default();
let healthcheck = parse_oci_healthcheck(container_config);
Ok(ImageConfig {
env,
cmd,
entrypoint,
working_dir,
user,
labels,
healthcheck,
})
}
pub fn cmd_image_tag(source: &str, target: &str) -> Result<(), Box<dyn std::error::Error>> {
let src_ref = resolve_local_reference(source);
let manifest =
load_image(&src_ref).map_err(|_| format!("image '{}' not found locally", src_ref))?;
let config_json = image::load_oci_config(&src_ref).map_err(|_| {
format!(
"OCI config not found for '{}' — re-pull or rebuild to populate the blob cache",
src_ref
)
})?;
let target_ref = add_default_tag(target);
let new_manifest = ImageManifest {
reference: target_ref.clone(),
digest: manifest.digest,
layers: manifest.layers,
config: manifest.config,
};
save_image(&new_manifest)?;
image::save_oci_config(&target_ref, &config_json)?;
println!("{} → {}", src_ref, target_ref);
Ok(())
}
pub fn cmd_image_save(
reference: &str,
output: Option<&str>,
) -> Result<(), Box<dyn std::error::Error>> {
let src_ref = resolve_local_reference(reference);
let manifest =
load_image(&src_ref).map_err(|_| format!("image '{}' not found locally", src_ref))?;
let config_json = image::load_oci_config(&src_ref).map_err(|_| {
format!(
"OCI config not found for '{}' — re-pull or rebuild to populate the blob cache",
src_ref
)
})?;
let config_bytes = config_json.into_bytes();
let mut layer_blobs: Vec<(String, Vec<u8>)> = Vec::new();
for digest in &manifest.layers {
if !blob_exists(digest) {
return Err(format!(
"blob not found for layer {} — re-pull or rebuild to populate the blob cache",
&digest[..19.min(digest.len())]
)
.into());
}
layer_blobs.push((digest.clone(), image::load_blob(digest)?));
}
let tar_bytes = build_oci_tar(&src_ref, &config_bytes, &layer_blobs)?;
if let Some(path) = output {
std::fs::write(path, &tar_bytes).map_err(|e| format!("cannot write '{}': {}", path, e))?;
println!(
"Saved {} ({} layers) → {}",
src_ref,
layer_blobs.len(),
path
);
} else {
use std::io::Write as _;
std::io::stdout().write_all(&tar_bytes)?;
}
Ok(())
}
pub(crate) fn build_oci_tar(
reference: &str,
config_bytes: &[u8],
layer_blobs: &[(String, Vec<u8>)],
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
use sha2::{Digest as _, Sha256};
let config_digest = format!("sha256:{:x}", Sha256::digest(config_bytes));
let config_hex = config_digest.strip_prefix("sha256:").unwrap();
let layer_descriptors: Vec<serde_json::Value> = layer_blobs
.iter()
.map(|(digest, data)| {
serde_json::json!({
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": digest,
"size": data.len()
})
})
.collect();
let oci_manifest = serde_json::json!({
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"config": {
"mediaType": "application/vnd.oci.image.config.v1+json",
"digest": config_digest,
"size": config_bytes.len()
},
"layers": layer_descriptors
});
let manifest_bytes = serde_json::to_vec(&oci_manifest)?;
let manifest_digest = format!("sha256:{:x}", Sha256::digest(&manifest_bytes));
let manifest_hex = manifest_digest.strip_prefix("sha256:").unwrap();
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_bytes.len(),
"annotations": {
"org.opencontainers.image.ref.name": reference
}
}]
});
let index_bytes = serde_json::to_vec(&index)?;
let oci_layout_bytes = br#"{"imageLayoutVersion":"1.0.0"}"#;
let mut tar_buf: Vec<u8> = Vec::new();
{
let mut ar = tar::Builder::new(&mut tar_buf);
let add =
|ar: &mut tar::Builder<&mut Vec<u8>>, path: &str, data: &[u8]| -> std::io::Result<()> {
let mut hdr = tar::Header::new_gnu();
hdr.set_path(path)?;
hdr.set_size(data.len() as u64);
hdr.set_mode(0o644);
hdr.set_cksum();
ar.append(&hdr, data)
};
add(&mut ar, "oci-layout", oci_layout_bytes)?;
add(&mut ar, "index.json", &index_bytes)?;
add(
&mut ar,
&format!("blobs/sha256/{}", manifest_hex),
&manifest_bytes,
)?;
add(
&mut ar,
&format!("blobs/sha256/{}", config_hex),
config_bytes,
)?;
for (digest, data) in layer_blobs {
let hex = digest.strip_prefix("sha256:").unwrap_or(digest.as_str());
add(&mut ar, &format!("blobs/sha256/{}", hex), data)?;
}
ar.finish()?;
}
Ok(tar_buf)
}
pub fn cmd_image_load(
input: Option<&str>,
tag: Option<&str>,
) -> Result<(), Box<dyn std::error::Error>> {
use std::collections::HashMap;
use std::io::Read as _;
let tar_bytes: Vec<u8> = if let Some(path) = input {
std::fs::read(path).map_err(|e| format!("cannot read '{}': {}", path, e))?
} else {
let mut buf = Vec::new();
std::io::stdin().read_to_end(&mut buf)?;
buf
};
let mut entries: HashMap<String, Vec<u8>> = HashMap::new();
{
let cursor = std::io::Cursor::new(&tar_bytes);
let mut ar = tar::Archive::new(cursor);
for entry in ar.entries()? {
let mut entry = entry?;
let path = entry.path()?.to_string_lossy().into_owned();
let mut data = Vec::new();
entry.read_to_end(&mut data)?;
entries.insert(path, data);
}
}
let layout_data = entries
.get("oci-layout")
.ok_or("missing 'oci-layout' — not a valid OCI Image Layout archive")?;
let layout: serde_json::Value = serde_json::from_slice(layout_data)?;
if layout.get("imageLayoutVersion").and_then(|v| v.as_str()) != Some("1.0.0") {
return Err("unsupported OCI image layout version".into());
}
let index_data = entries
.get("index.json")
.ok_or("missing 'index.json' in archive")?;
let index: serde_json::Value = serde_json::from_slice(index_data)?;
let manifests = index
.get("manifests")
.and_then(|v| v.as_array())
.ok_or("index.json: missing 'manifests' array")?;
if manifests.is_empty() {
return Err("index.json: empty manifests array".into());
}
for manifest_desc in manifests {
let manifest_digest = manifest_desc
.get("digest")
.and_then(|v| v.as_str())
.ok_or("manifest descriptor missing 'digest'")?;
let ref_annotation = manifest_desc
.pointer("/annotations/org.opencontainers.image.ref.name")
.and_then(|v| v.as_str());
let manifest_hex = manifest_digest
.strip_prefix("sha256:")
.unwrap_or(manifest_digest);
let manifest_key = format!("blobs/sha256/{}", manifest_hex);
let manifest_data = entries
.get(&manifest_key)
.ok_or_else(|| format!("missing blob: {}", manifest_key))?;
let oci_manifest: serde_json::Value = serde_json::from_slice(manifest_data)?;
let config_desc = oci_manifest
.get("config")
.ok_or("manifest: missing 'config'")?;
let config_digest = config_desc
.get("digest")
.and_then(|v| v.as_str())
.ok_or("config descriptor: missing 'digest'")?;
let config_hex = config_digest
.strip_prefix("sha256:")
.unwrap_or(config_digest);
let config_key = format!("blobs/sha256/{}", config_hex);
let config_data = entries
.get(&config_key)
.ok_or_else(|| format!("missing blob: {}", config_key))?;
let config_json =
std::str::from_utf8(config_data).map_err(|_| "config blob is not valid UTF-8")?;
let layer_descs = oci_manifest
.get("layers")
.and_then(|v| v.as_array())
.ok_or("manifest: missing 'layers' array")?;
let mut layer_digests: Vec<String> = Vec::new();
for (i, layer_desc) in layer_descs.iter().enumerate() {
let layer_digest = layer_desc
.get("digest")
.and_then(|v| v.as_str())
.ok_or_else(|| format!("layer {}: missing 'digest'", i))?;
let layer_hex = layer_digest.strip_prefix("sha256:").unwrap_or(layer_digest);
let layer_key = format!("blobs/sha256/{}", layer_hex);
let layer_data = entries
.get(&layer_key)
.ok_or_else(|| format!("missing blob: {}", layer_key))?;
if !blob_exists(layer_digest) {
image::save_blob(layer_digest, layer_data)?;
}
if !layer_exists(layer_digest) {
let mut tmp = tempfile::NamedTempFile::new()?;
tmp.write_all(layer_data)?;
tmp.flush()?;
extract_layer(layer_digest, tmp.path())?;
}
layer_digests.push(layer_digest.to_string());
}
let reference = if let Some(t) = tag {
t.to_string()
} else if let Some(r) = ref_annotation {
r.to_string()
} else {
manifest_digest.to_string()
};
let config = parse_image_config(config_json)?;
let img_manifest = ImageManifest {
reference: reference.clone(),
digest: manifest_digest.to_string(),
layers: layer_digests,
config,
};
save_image(&img_manifest)?;
image::save_oci_config(&reference, config_json)?;
println!("Loaded {}", reference);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_oci_tar() {
use sha2::{Digest as _, Sha256};
use std::collections::HashMap;
use std::io::Read as _;
let reference = "test-save:latest";
let config_bytes =
br#"{"config":{"Env":["PATH=/usr/bin"],"Cmd":["/bin/sh"]},"rootfs":{"type":"layers","diff_ids":[]}}"#;
let config_digest = format!("sha256:{:x}", Sha256::digest(config_bytes.as_ref()));
let layer1_bytes: Vec<u8> = vec![0u8, 1, 2, 3];
let layer1_digest = format!("sha256:{:x}", Sha256::digest(&layer1_bytes));
let layer2_bytes: Vec<u8> = vec![4u8, 5, 6, 7];
let layer2_digest = format!("sha256:{:x}", Sha256::digest(&layer2_bytes));
let layer_blobs = vec![
(layer1_digest.clone(), layer1_bytes.clone()),
(layer2_digest.clone(), layer2_bytes.clone()),
];
let tar_bytes =
build_oci_tar(reference, config_bytes.as_ref(), &layer_blobs).expect("build_oci_tar");
let cursor = std::io::Cursor::new(&tar_bytes);
let mut ar = tar::Archive::new(cursor);
let mut found: HashMap<String, Vec<u8>> = HashMap::new();
for entry in ar.entries().unwrap() {
let mut entry = entry.unwrap();
let path = entry.path().unwrap().to_string_lossy().into_owned();
let mut data = Vec::new();
entry.read_to_end(&mut data).unwrap();
found.insert(path, data);
}
let layout: serde_json::Value =
serde_json::from_slice(found.get("oci-layout").expect("oci-layout")).unwrap();
assert_eq!(layout["imageLayoutVersion"].as_str().unwrap(), "1.0.0");
let index: serde_json::Value =
serde_json::from_slice(found.get("index.json").expect("index.json")).unwrap();
let ref_name = index
.pointer("/manifests/0/annotations/org.opencontainers.image.ref.name")
.and_then(|v| v.as_str())
.expect("ref.name annotation");
assert_eq!(ref_name, reference);
let config_hex = config_digest.strip_prefix("sha256:").unwrap();
assert!(
found.contains_key(&format!("blobs/sha256/{}", config_hex)),
"config blob missing"
);
for (digest, _) in &layer_blobs {
let hex = digest.strip_prefix("sha256:").unwrap();
assert!(
found.contains_key(&format!("blobs/sha256/{}", hex)),
"layer blob {} missing",
hex
);
}
let manifest_digest = index
.pointer("/manifests/0/digest")
.and_then(|v| v.as_str())
.expect("manifest digest in index.json");
let manifest_hex = manifest_digest.strip_prefix("sha256:").unwrap();
assert!(
found.contains_key(&format!("blobs/sha256/{}", manifest_hex)),
"manifest blob missing"
);
let manifest: serde_json::Value = serde_json::from_slice(
found
.get(&format!("blobs/sha256/{}", manifest_hex))
.unwrap(),
)
.unwrap();
assert_eq!(
manifest.pointer("/config/digest").and_then(|v| v.as_str()),
Some(config_digest.as_str())
);
assert_eq!(
manifest
.pointer("/layers/0/digest")
.and_then(|v| v.as_str()),
Some(layer1_digest.as_str())
);
assert_eq!(
manifest
.pointer("/layers/1/digest")
.and_then(|v| v.as_str()),
Some(layer2_digest.as_str())
);
}
}