docker_image_pusher/image/
manifest.rs

1use crate::error::{RegistryError, Result};
2use serde_json::{self, Value};
3
4/// Supported manifest types
5#[derive(Debug, Clone, PartialEq)]
6pub enum ManifestType {
7    DockerV2,
8    DockerList,
9    OciManifest,
10    OciIndex,
11}
12
13/// Parsed manifest information
14#[derive(Debug, Clone)]
15pub struct ParsedManifest {
16    pub manifest_type: ManifestType,
17    pub config_digest: Option<String>,
18    pub layer_digests: Vec<String>,
19    pub raw_data: Vec<u8>,
20    pub platform_manifests: Option<Vec<PlatformManifest>>, // For index/list types
21}
22
23/// Platform-specific manifest reference (for OCI index/Docker manifest list)
24#[derive(Debug, Clone)]
25pub struct PlatformManifest {
26    pub digest: String,
27    pub media_type: String,
28    pub platform: Option<Platform>,
29}
30
31/// Platform information
32#[derive(Debug, Clone)]
33pub struct Platform {
34    pub architecture: String,
35    pub os: String,
36    pub variant: Option<String>,
37}
38
39impl ManifestType {
40    pub fn from_media_type(media_type: &str) -> ManifestType {
41        match media_type {
42            "application/vnd.docker.distribution.manifest.v2+json" => ManifestType::DockerV2,
43            "application/vnd.docker.distribution.manifest.list.v2+json" => ManifestType::DockerList,
44            "application/vnd.oci.image.manifest.v1+json" => ManifestType::OciManifest,
45            "application/vnd.oci.image.index.v1+json" => ManifestType::OciIndex,
46            _ => ManifestType::DockerV2, // Default fallback
47        }
48    }
49
50    pub fn is_index_type(&self) -> bool {
51        matches!(self, ManifestType::DockerList | ManifestType::OciIndex)
52    }
53
54    pub fn to_content_type(&self) -> &'static str {
55        match self {
56            ManifestType::DockerV2 => "application/vnd.docker.distribution.manifest.v2+json",
57            ManifestType::DockerList => "application/vnd.docker.distribution.manifest.list.v2+json",
58            ManifestType::OciManifest => "application/vnd.oci.image.manifest.v1+json",
59            ManifestType::OciIndex => "application/vnd.oci.image.index.v1+json",
60        }
61    }
62}
63
64pub fn parse_manifest(manifest_bytes: &[u8]) -> Result<Value> {
65    serde_json::from_slice(manifest_bytes).map_err(|e| RegistryError::Parse(e.to_string()))
66}
67
68/// Parse manifest and determine type and contents
69pub fn parse_manifest_with_type(manifest_bytes: &[u8]) -> Result<ParsedManifest> {
70    let manifest: Value = parse_manifest(manifest_bytes)?;
71
72    // Determine manifest type from mediaType field
73    let media_type = manifest
74        .get("mediaType")
75        .and_then(|m| m.as_str())
76        .unwrap_or("application/vnd.docker.distribution.manifest.v2+json");
77
78    let manifest_type = ManifestType::from_media_type(media_type);
79
80    match manifest_type {
81        ManifestType::OciIndex | ManifestType::DockerList => {
82            // Parse index/list manifest
83            let platform_manifests = parse_index_manifests(&manifest)?;
84            Ok(ParsedManifest {
85                manifest_type,
86                config_digest: None,       // Index doesn't have config
87                layer_digests: Vec::new(), // Index doesn't have layers directly
88                raw_data: manifest_bytes.to_vec(),
89                platform_manifests: Some(platform_manifests),
90            })
91        }
92        ManifestType::DockerV2 | ManifestType::OciManifest => {
93            // Parse single-platform manifest
94            let config_digest = extract_config_digest(&manifest)?;
95            let layer_digests = extract_layer_digests(&manifest)?;
96            Ok(ParsedManifest {
97                manifest_type,
98                config_digest: Some(config_digest),
99                layer_digests,
100                raw_data: manifest_bytes.to_vec(),
101                platform_manifests: None,
102            })
103        }
104    }
105}
106
107/// Extract config digest from single-platform manifest
108pub fn extract_config_digest(manifest: &Value) -> Result<String> {
109    manifest
110        .get("config")
111        .and_then(|c| c.get("digest"))
112        .and_then(|d| d.as_str())
113        .map(|s| s.to_string())
114        .ok_or_else(|| RegistryError::Parse("Missing config digest in manifest".to_string()))
115}
116
117/// Extract layer digests from single-platform manifest
118pub fn extract_layer_digests(manifest: &Value) -> Result<Vec<String>> {
119    let layers = manifest
120        .get("layers")
121        .and_then(|l| l.as_array())
122        .ok_or_else(|| RegistryError::Parse("Missing layers in manifest".to_string()))?;
123
124    let mut digests = Vec::new();
125    for layer in layers {
126        if let Some(digest) = layer.get("digest").and_then(|d| d.as_str()) {
127            digests.push(digest.to_string());
128        }
129    }
130
131    if digests.is_empty() {
132        return Err(RegistryError::Parse(
133            "No layer digests found in manifest".to_string(),
134        ));
135    }
136
137    Ok(digests)
138}
139
140/// Parse platform manifests from index/list
141fn parse_index_manifests(manifest: &Value) -> Result<Vec<PlatformManifest>> {
142    let manifests = manifest
143        .get("manifests")
144        .and_then(|m| m.as_array())
145        .ok_or_else(|| RegistryError::Parse("Missing manifests array in index".to_string()))?;
146
147    let mut platform_manifests = Vec::new();
148    for m in manifests {
149        let digest = m
150            .get("digest")
151            .and_then(|d| d.as_str())
152            .ok_or_else(|| RegistryError::Parse("Missing digest in manifest entry".to_string()))?;
153
154        let media_type = m
155            .get("mediaType")
156            .and_then(|mt| mt.as_str())
157            .unwrap_or("application/vnd.docker.distribution.manifest.v2+json");
158
159        let platform = if let Some(platform_obj) = m.get("platform") {
160            Some(Platform {
161                architecture: platform_obj
162                    .get("architecture")
163                    .and_then(|a| a.as_str())
164                    .unwrap_or("amd64")
165                    .to_string(),
166                os: platform_obj
167                    .get("os")
168                    .and_then(|o| o.as_str())
169                    .unwrap_or("linux")
170                    .to_string(),
171                variant: platform_obj
172                    .get("variant")
173                    .and_then(|v| v.as_str())
174                    .map(|s| s.to_string()),
175            })
176        } else {
177            None
178        };
179
180        platform_manifests.push(PlatformManifest {
181            digest: digest.to_string(),
182            media_type: media_type.to_string(),
183            platform,
184        });
185    }
186
187    Ok(platform_manifests)
188}
189
190// Get layer digests from manifest (legacy function for compatibility)
191pub fn get_layers(manifest: &Value) -> Result<Vec<String>> {
192    extract_layer_digests(manifest)
193}
194
195// Check if blob is already gzipped
196pub fn is_gzipped(blob: &[u8]) -> bool {
197    blob.len() >= 2 && blob[0] == 0x1f && blob[1] == 0x8b
198}