use super::*;
pub(super) fn extract_tar_archive(archive: &Path, dest: &Path) -> Result<(), String> {
let file = std::fs::File::open(archive)
.map_err(|e| format!("open archive {}: {e}", archive.display()))?;
let mut ar = tar::Archive::new(file);
ar.set_preserve_permissions(true);
ar.set_overwrite(true);
ar.unpack(dest)
.map_err(|e| format!("extract archive {}: {e}", archive.display()))?;
Ok(())
}
pub(super) fn extract_oci_archive(archive: &Path, work_dir: &Path) -> Result<PathBuf, String> {
let layout = work_dir.join("_oci");
let _ = std::fs::remove_dir_all(&layout);
std::fs::create_dir_all(&layout)
.map_err(|e| format!("create OCI archive extract dir {}: {e}", layout.display()))?;
extract_tar_archive(archive, &layout)?;
if !layout.join("index.json").is_file() {
return Err(format!(
"OCI archive {} did not contain index.json at archive root",
archive.display()
));
}
Ok(layout)
}
const DEFAULT_MAX_LAYER_BYTES: u64 = 16 * 1024 * 1024 * 1024;
fn layer_decompression_cap() -> u64 {
std::env::var("SUPERMACHINE_MAX_LAYER_BYTES")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(DEFAULT_MAX_LAYER_BYTES)
}
struct LimitedReader<R> {
inner: R,
limit: u64,
read_total: u64,
}
impl<R: std::io::Read> std::io::Read for LimitedReader<R> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let n = self.inner.read(buf)?;
self.read_total += n as u64;
if self.read_total > self.limit {
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
"decompressed layer exceeds cap",
));
}
Ok(n)
}
}
pub(super) fn extract_layer_tar(blob: &Path, dest: &Path) -> Result<(), String> {
use std::io::Read as _;
let cap = layer_decompression_cap();
let mut file =
std::fs::File::open(blob).map_err(|e| format!("open layer {}: {e}", blob.display()))?;
let mut magic = [0u8; 4];
let mut got = 0usize;
while got < 4 {
match file.read(&mut magic[got..]) {
Ok(0) => break,
Ok(n) => got += n,
Err(e) => return Err(format!("read layer magic: {e}")),
}
}
let head = magic[..got].to_vec();
let is_gzip = head.starts_with(&[0x1f, 0x8b]);
let is_zstd = head.starts_with(&[0x28, 0xb5, 0x2f, 0xfd]);
let rest = std::io::Cursor::new(head).chain(file);
let decoded: Box<dyn std::io::Read> = if is_gzip {
Box::new(flate2::read::GzDecoder::new(rest))
} else if is_zstd {
Box::new(
ruzstd::StreamingDecoder::new(rest)
.map_err(|e| format!("zstd layer decode init: {e}"))?,
)
} else {
Box::new(rest) };
let mut limited = LimitedReader {
inner: decoded,
limit: cap,
read_total: 0,
};
{
let mut archive = tar::Archive::new(&mut limited);
archive.set_preserve_permissions(true);
archive.set_preserve_mtime(false);
archive.set_overwrite(true);
if let Ok(entries) = archive.entries() {
for entry in entries {
let mut entry = match entry {
Ok(e) => e,
Err(_) => break,
};
let _ = entry.unpack_in(dest);
}
}
}
if limited.read_total > cap {
return Err(format!(
"OCI layer decompresses past the {cap}-byte cap (possible decompression \
bomb); raise SUPERMACHINE_MAX_LAYER_BYTES if intended"
));
}
Ok(())
}
pub(super) fn blob_path(layout: &Path, digest: &str) -> Result<PathBuf, String> {
Ok(layout
.join("blobs/sha256")
.join(sha256_path_component(digest)?))
}
pub(super) fn read_oci_json_blob(
layout: &Path,
digest: &str,
label: &str,
) -> Result<serde_json::Value, String> {
let path = blob_path(layout, digest)?;
let text = std::fs::read_to_string(&path)
.map_err(|e| format!("read OCI {label} {}: {e}", path.display()))?;
serde_json::from_str(&text).map_err(|e| format!("parse OCI {label} {}: {e}", path.display()))
}
pub(super) fn descriptor_platform_arch(desc: &serde_json::Value) -> Option<&str> {
desc.get("platform")
.and_then(|p| p.get("architecture"))
.and_then(|v| v.as_str())
}
pub(super) fn find_oci_manifest_descriptor(
layout: &Path,
index: &serde_json::Value,
want_arch: &str,
depth: usize,
) -> Result<serde_json::Value, String> {
if depth > 4 {
return Err("nested OCI image index too deep".to_owned());
}
let manifests = index
.get("manifests")
.and_then(|v| v.as_array())
.ok_or_else(|| "OCI index missing manifests".to_owned())?;
for desc in manifests {
let media_type = desc
.get("mediaType")
.and_then(|v| v.as_str())
.unwrap_or_default();
let is_nested_index =
media_type.contains("image.index") || media_type.contains("manifest.list");
let arch = descriptor_platform_arch(desc);
if is_nested_index && (arch == Some(want_arch) || arch.is_none()) {
let digest = desc
.get("digest")
.and_then(|v| v.as_str())
.ok_or_else(|| "nested OCI index descriptor missing digest".to_owned())?;
let nested = read_oci_json_blob(layout, digest, "nested index")?;
if let Ok(found) = find_oci_manifest_descriptor(layout, &nested, want_arch, depth + 1) {
return Ok(found);
}
continue;
}
if arch == Some(want_arch) {
return Ok(desc.clone());
}
}
Err(format!(
"OCI layout has no linux/{want_arch} image manifest"
))
}
pub(super) fn inspect_oci_layout(
image: &str,
layout: &Path,
want_arch: &str,
) -> Result<serde_json::Value, String> {
match inspect_oci_layout_via_index(image, layout, want_arch) {
Ok(v) => Ok(v),
Err(e) => {
if !e.contains("No such file or directory") && !e.contains("read OCI manifest") {
return Err(e);
}
inspect_legacy_docker_manifest(image, layout, want_arch).map_err(|legacy_err| {
format!(
"OCI inspect failed ({e}); legacy manifest.json \
fallback also failed: {legacy_err}"
)
})
}
}
}
pub(super) fn inspect_oci_layout_via_index(
image: &str,
layout: &Path,
want_arch: &str,
) -> Result<serde_json::Value, String> {
let index_text = std::fs::read_to_string(layout.join("index.json")).map_err(|e| {
format!(
"read OCI layout index {}: {e}",
layout.join("index.json").display()
)
})?;
let index: serde_json::Value =
serde_json::from_str(&index_text).map_err(|e| format!("parse OCI layout index: {e}"))?;
let manifest_desc = find_oci_manifest_descriptor(layout, &index, want_arch, 0)?;
let manifest_digest = manifest_desc
.get("digest")
.and_then(|v| v.as_str())
.ok_or_else(|| "OCI manifest descriptor missing digest".to_owned())?;
let manifest = read_oci_json_blob(layout, manifest_digest, "manifest")?;
let config_digest = manifest
.get("config")
.and_then(|v| v.get("digest"))
.and_then(|v| v.as_str())
.ok_or_else(|| "OCI manifest missing config digest".to_owned())?;
let config = read_oci_json_blob(layout, config_digest, "config")?;
let cfg = config
.get("config")
.cloned()
.unwrap_or_else(|| serde_json::json!({}));
let arch = config
.get("architecture")
.and_then(|v| v.as_str())
.or_else(|| descriptor_platform_arch(&manifest_desc))
.unwrap_or(want_arch);
Ok(serde_json::json!({
"Id": format!("sha256:{}", strip_sha256(config_digest)),
"Architecture": arch,
"RepoTags": [image],
"Config": {
"Env": cfg.get("Env").cloned().unwrap_or(serde_json::Value::Null),
"Entrypoint": cfg.get("Entrypoint").cloned().unwrap_or(serde_json::Value::Null),
"Cmd": cfg.get("Cmd").cloned().unwrap_or(serde_json::Value::Null),
"WorkingDir": cfg.get("WorkingDir").cloned().unwrap_or(serde_json::Value::Null),
"User": cfg.get("User").cloned().unwrap_or(serde_json::Value::Null),
}
}))
}
pub(super) fn read_legacy_docker_manifest_entry(
image: &str,
layout: &Path,
) -> Result<serde_json::Value, String> {
let path = layout.join("manifest.json");
let text = std::fs::read_to_string(&path)
.map_err(|e| format!("read legacy manifest.json {}: {e}", path.display()))?;
let entries: Vec<serde_json::Value> =
serde_json::from_str(&text).map_err(|e| format!("parse legacy manifest.json: {e}"))?;
if entries.is_empty() {
return Err("legacy manifest.json has no entries".to_owned());
}
let matched = entries.iter().find(|e| {
e.get("RepoTags")
.and_then(|v| v.as_array())
.map(|tags| tags.iter().any(|t| t.as_str() == Some(image)))
.unwrap_or(false)
});
Ok(matched.cloned().unwrap_or_else(|| entries[0].clone()))
}
pub(super) fn inspect_legacy_docker_manifest(
image: &str,
layout: &Path,
want_arch: &str,
) -> Result<serde_json::Value, String> {
let entry = read_legacy_docker_manifest_entry(image, layout)?;
let config_rel = entry
.get("Config")
.and_then(|v| v.as_str())
.ok_or_else(|| "legacy manifest entry missing Config".to_owned())?;
let config_path = layout.join(config_rel);
let config_text = std::fs::read_to_string(&config_path)
.map_err(|e| format!("read legacy config {}: {e}", config_path.display()))?;
let config: serde_json::Value =
serde_json::from_str(&config_text).map_err(|e| format!("parse legacy config: {e}"))?;
let actual_arch = config
.get("architecture")
.and_then(|v| v.as_str())
.unwrap_or("");
if actual_arch != want_arch {
return Err(format!(
"legacy manifest.json describes a linux/{actual_arch} image but \
the request asked for linux/{want_arch}; re-run `docker save` \
with `--platform linux/{want_arch}` against a multi-arch source"
));
}
let cfg = config
.get("config")
.cloned()
.unwrap_or_else(|| serde_json::json!({}));
let config_digest = config_rel
.strip_prefix("blobs/sha256/")
.unwrap_or(config_rel);
Ok(serde_json::json!({
"Id": format!("sha256:{}", config_digest),
"Architecture": actual_arch,
"RepoTags": [image],
"Config": {
"Env": cfg.get("Env").cloned().unwrap_or(serde_json::Value::Null),
"Entrypoint": cfg.get("Entrypoint").cloned().unwrap_or(serde_json::Value::Null),
"Cmd": cfg.get("Cmd").cloned().unwrap_or(serde_json::Value::Null),
"WorkingDir": cfg.get("WorkingDir").cloned().unwrap_or(serde_json::Value::Null),
"User": cfg.get("User").cloned().unwrap_or(serde_json::Value::Null),
}
}))
}
#[cfg(test)]
mod descriptor_tests {
use super::*;
use serde_json::json;
#[test]
fn descriptor_platform_arch_reads_nested_field() {
let d = json!({"platform": {"architecture": "amd64", "os": "linux"}});
assert_eq!(descriptor_platform_arch(&d), Some("amd64"));
assert_eq!(descriptor_platform_arch(&json!({"digest": "x"})), None);
assert_eq!(descriptor_platform_arch(&json!({"platform": {}})), None);
}
#[test]
fn find_manifest_picks_the_requested_arch_from_a_flat_index() {
let index = json!({"manifests": [
{"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:aaa", "platform": {"architecture": "arm64", "os": "linux"}},
{"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:bbb", "platform": {"architecture": "amd64", "os": "linux"}},
]});
let got = find_oci_manifest_descriptor(Path::new("/unused"), &index, "amd64", 0)
.expect("amd64 present");
assert_eq!(
got.get("digest").and_then(|v| v.as_str()),
Some("sha256:bbb")
);
let got = find_oci_manifest_descriptor(Path::new("/unused"), &index, "arm64", 0)
.expect("arm64 present");
assert_eq!(
got.get("digest").and_then(|v| v.as_str()),
Some("sha256:aaa")
);
}
#[test]
fn find_manifest_errors_when_arch_absent() {
let index = json!({"manifests": [
{"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:aaa", "platform": {"architecture": "arm64", "os": "linux"}},
]});
let err =
find_oci_manifest_descriptor(Path::new("/unused"), &index, "amd64", 0).unwrap_err();
assert!(err.contains("amd64"), "got: {err}");
}
#[test]
fn find_manifest_rejects_missing_manifests_and_excessive_nesting() {
assert!(find_oci_manifest_descriptor(Path::new("/x"), &json!({}), "amd64", 0).is_err());
assert!(find_oci_manifest_descriptor(
Path::new("/x"),
&json!({"manifests": []}),
"amd64",
5
)
.is_err());
}
#[test]
fn blob_path_rejects_traversal_digests() {
let layout = Path::new("/layout");
let hex = "a".repeat(64);
let p = blob_path(layout, &format!("sha256:{hex}")).unwrap();
assert!(p.ends_with(format!("blobs/sha256/{hex}")));
assert!(blob_path(layout, "sha256:../../etc/passwd").is_err());
assert!(blob_path(layout, "sha256:ab/cd").is_err());
assert!(blob_path(layout, "sha256:").is_err());
}
}