use std::collections::BTreeMap;
use oci_spec::image::{Arch, Os};
use crate::{
client::{Config, ImageLayer},
sha256_digest,
};
pub const WASM_LAYER_MEDIA_TYPE: &str = "application/vnd.wasm.content.layer.v1+wasm";
pub const WASM_CONFIG_MEDIA_TYPE: &str = "application/vnd.wasm.config.v1+json";
pub const IMAGE_MANIFEST_MEDIA_TYPE: &str = "application/vnd.docker.distribution.manifest.v2+json";
pub const IMAGE_MANIFEST_LIST_MEDIA_TYPE: &str =
"application/vnd.docker.distribution.manifest.list.v2+json";
pub const OCI_IMAGE_INDEX_MEDIA_TYPE: &str = "application/vnd.oci.image.index.v1+json";
pub const OCI_IMAGE_MEDIA_TYPE: &str = "application/vnd.oci.image.manifest.v1+json";
pub const IMAGE_CONFIG_MEDIA_TYPE: &str = "application/vnd.oci.image.config.v1+json";
pub const IMAGE_DOCKER_CONFIG_MEDIA_TYPE: &str = "application/vnd.docker.container.image.v1+json";
pub const IMAGE_LAYER_MEDIA_TYPE: &str = "application/vnd.oci.image.layer.v1.tar";
pub const IMAGE_LAYER_GZIP_MEDIA_TYPE: &str = "application/vnd.oci.image.layer.v1.tar+gzip";
pub const IMAGE_DOCKER_LAYER_TAR_MEDIA_TYPE: &str = "application/vnd.docker.image.rootfs.diff.tar";
pub const IMAGE_DOCKER_LAYER_GZIP_MEDIA_TYPE: &str =
"application/vnd.docker.image.rootfs.diff.tar.gzip";
pub const IMAGE_LAYER_NONDISTRIBUTABLE_MEDIA_TYPE: &str =
"application/vnd.oci.image.layer.nondistributable.v1.tar";
pub const IMAGE_LAYER_NONDISTRIBUTABLE_GZIP_MEDIA_TYPE: &str =
"application/vnd.oci.image.layer.nondistributable.v1.tar+gzip";
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
#[serde(untagged)]
#[allow(clippy::large_enum_variant)]
pub enum OciManifest {
Image(OciImageManifest),
ImageIndex(OciImageIndex),
}
impl OciManifest {
pub fn content_type(&self) -> &str {
match self {
OciManifest::Image(image) => {
image.media_type.as_deref().unwrap_or(OCI_IMAGE_MEDIA_TYPE)
}
OciManifest::ImageIndex(image) => image
.media_type
.as_deref()
.unwrap_or(IMAGE_MANIFEST_LIST_MEDIA_TYPE),
}
}
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OciImageManifest {
pub schema_version: u8,
#[serde(skip_serializing_if = "Option::is_none")]
pub media_type: Option<String>,
pub config: OciDescriptor,
pub layers: Vec<OciDescriptor>,
#[serde(skip_serializing_if = "Option::is_none")]
pub subject: Option<OciDescriptor>,
#[serde(skip_serializing_if = "Option::is_none")]
pub artifact_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<BTreeMap<String, String>>,
}
impl Default for OciImageManifest {
fn default() -> Self {
OciImageManifest {
schema_version: 2,
media_type: None,
config: OciDescriptor::default(),
layers: vec![],
subject: None,
artifact_type: None,
annotations: None,
}
}
}
impl OciImageManifest {
pub fn build(
layers: &[ImageLayer],
config: &Config,
annotations: Option<BTreeMap<String, String>>,
) -> Self {
let mut manifest = OciImageManifest::default();
manifest.config.media_type = config.media_type.to_string();
manifest.config.size = config.data.len() as i64;
manifest.config.digest = sha256_digest(&config.data);
manifest.annotations = annotations;
for layer in layers {
let digest = sha256_digest(&layer.data);
let descriptor = OciDescriptor {
size: layer.data.len() as i64,
digest,
media_type: layer.media_type.to_string(),
annotations: layer.annotations.clone(),
..Default::default()
};
manifest.layers.push(descriptor);
}
manifest
}
}
impl From<OciImageIndex> for OciManifest {
fn from(m: OciImageIndex) -> Self {
Self::ImageIndex(m)
}
}
impl From<OciImageManifest> for OciManifest {
fn from(m: OciImageManifest) -> Self {
Self::Image(m)
}
}
impl std::fmt::Display for OciManifest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
OciManifest::Image(oci_image_manifest) => write!(f, "{oci_image_manifest}"),
OciManifest::ImageIndex(oci_image_index) => write!(f, "{oci_image_index}"),
}
}
}
impl std::fmt::Display for OciImageIndex {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let media_type = self
.media_type
.clone()
.unwrap_or_else(|| String::from("N/A"));
let manifests: Vec<String> = self.manifests.iter().map(|m| m.to_string()).collect();
write!(
f,
"OCI Image Index( schema-version: '{}', media-type: '{}', manifests: '{}' )",
self.schema_version,
media_type,
manifests.join(","),
)
}
}
impl std::fmt::Display for OciImageManifest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let media_type = self
.media_type
.clone()
.unwrap_or_else(|| String::from("N/A"));
let annotations = self.annotations.clone().unwrap_or_default();
let layers: Vec<String> = self.layers.iter().map(|l| l.to_string()).collect();
write!(
f,
"OCI Image Manifest( schema-version: '{}', media-type: '{}', config: '{}', artifact-type: '{:?}', layers: '{:?}', annotations: '{:?}' )",
self.schema_version,
media_type,
self.config,
self.artifact_type,
layers,
annotations,
)
}
}
#[derive(Clone, Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Versioned {
pub schema_version: i32,
#[serde(skip_serializing_if = "Option::is_none")]
pub media_type: Option<String>,
}
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OciDescriptor {
pub media_type: String,
pub digest: String,
pub size: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub urls: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<BTreeMap<String, String>>,
}
impl std::fmt::Display for OciDescriptor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let urls = self.urls.clone().unwrap_or_default();
let annotations = self.annotations.clone().unwrap_or_default();
write!(
f,
"( media-type: '{}', digest: '{}', size: '{}', urls: '{:?}', annotations: '{:?}' )",
self.media_type, self.digest, self.size, urls, annotations,
)
}
}
impl Default for OciDescriptor {
fn default() -> Self {
OciDescriptor {
media_type: IMAGE_CONFIG_MEDIA_TYPE.to_owned(),
digest: "".to_owned(),
size: 0,
urls: None,
annotations: None,
}
}
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OciImageIndex {
pub schema_version: u8,
#[serde(skip_serializing_if = "Option::is_none")]
pub media_type: Option<String>,
pub manifests: Vec<ImageIndexEntry>,
#[serde(skip_serializing_if = "Option::is_none")]
pub artifact_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<BTreeMap<String, String>>,
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ImageIndexEntry {
pub media_type: String,
pub digest: String,
pub size: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub platform: Option<Platform>,
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<BTreeMap<String, String>>,
}
impl std::fmt::Display for ImageIndexEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let platform = self
.platform
.clone()
.map(|p| p.to_string())
.unwrap_or_else(|| String::from("N/A"));
let annotations = self.annotations.clone().unwrap_or_default();
write!(
f,
"(media-type: '{}', digest: '{}', size: '{}', platform: '{}', annotations: {:?})",
self.media_type, self.digest, self.size, platform, annotations,
)
}
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Platform {
pub architecture: Arch,
pub os: Os,
#[serde(rename = "os.version")]
#[serde(skip_serializing_if = "Option::is_none")]
pub os_version: Option<String>,
#[serde(rename = "os.features")]
#[serde(skip_serializing_if = "Option::is_none")]
pub os_features: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub variant: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub features: Option<Vec<String>>,
}
impl std::fmt::Display for Platform {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let os_version = self
.os_version
.clone()
.unwrap_or_else(|| String::from("N/A"));
let os_features = self.os_features.clone().unwrap_or_default();
let variant = self.variant.clone().unwrap_or_else(|| String::from("N/A"));
let features = self.os_features.clone().unwrap_or_default();
write!(f, "( architecture: '{}', os: '{}', os-version: '{}', os-features: '{:?}', variant: '{}', features: '{:?}' )",
self.architecture,
self.os,
os_version,
os_features,
variant,
features,
)
}
}
#[cfg(test)]
mod test {
use super::*;
const TEST_MANIFEST: &str = r#"{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"size": 2,
"digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"
},
"artifactType": "application/vnd.wasm.component.v1+wasm",
"layers": [
{
"mediaType": "application/vnd.wasm.content.layer.v1+wasm",
"size": 1615998,
"digest": "sha256:f9c91f4c280ab92aff9eb03b279c4774a80b84428741ab20855d32004b2b983f",
"annotations": {
"org.opencontainers.image.title": "module.wasm"
}
}
]
}
"#;
#[test]
fn test_manifest() {
let manifest: OciImageManifest =
serde_json::from_str(TEST_MANIFEST).expect("parsed manifest");
assert_eq!(2, manifest.schema_version);
assert_eq!(
Some(IMAGE_MANIFEST_MEDIA_TYPE.to_owned()),
manifest.media_type
);
let config = manifest.config;
assert_eq!(IMAGE_DOCKER_CONFIG_MEDIA_TYPE.to_owned(), config.media_type);
assert_eq!(2, config.size);
assert_eq!(
"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a".to_owned(),
config.digest
);
assert_eq!(
"application/vnd.wasm.component.v1+wasm".to_owned(),
manifest.artifact_type.unwrap()
);
assert_eq!(1, manifest.layers.len());
let wasm_layer = &manifest.layers[0];
assert_eq!(1_615_998, wasm_layer.size);
assert_eq!(WASM_LAYER_MEDIA_TYPE.to_owned(), wasm_layer.media_type);
assert_eq!(
1,
wasm_layer
.annotations
.as_ref()
.expect("annotations map")
.len()
);
}
}