Skip to main content

docker_registry_client/
manifest.rs

1use std::collections::BTreeMap;
2
3use chrono::{
4    DateTime,
5    Utc,
6};
7use serde::{
8    Deserialize,
9    Serialize,
10    de::{
11        self,
12        Deserializer,
13    },
14};
15use url::Url;
16
17#[derive(Debug, Deserialize, Serialize, Clone)]
18#[serde(untagged)]
19pub enum Manifest {
20    Image(Image),
21    List(List),
22    Single(Single),
23}
24
25#[derive(Debug, Deserialize, Serialize, Clone)]
26pub struct Image {
27    #[serde(rename = "schemaVersion")]
28    pub schema_version: SchemaVersion,
29
30    #[serde(rename = "mediaType")]
31    pub media_type: String,
32
33    pub config: Config,
34
35    pub layers: Vec<Layer>,
36}
37
38#[derive(Debug, Deserialize, Serialize, Clone)]
39pub struct List {
40    #[serde(rename = "schemaVersion")]
41    schema_version: SchemaVersion,
42
43    #[serde(rename = "mediaType")]
44    media_type: String,
45
46    pub manifests: Vec<Entry>,
47}
48
49#[derive(Debug, Deserialize, Serialize, Clone)]
50pub struct Single {
51    #[serde(rename = "schemaVersion")]
52    pub schema_version: SchemaVersion,
53
54    pub name: String,
55    pub tag: String,
56    pub architecture: Architecture,
57
58    #[serde(rename = "fsLayers")]
59    pub fs_layers: Vec<FsLayer>,
60
61    pub history: Vec<History>,
62}
63
64#[derive(Debug, Clone)]
65pub enum SchemaVersion {
66    V1,
67    V2,
68}
69
70#[derive(Debug, Deserialize, Serialize, Clone)]
71pub struct Entry {
72    #[serde(rename = "mediaType")]
73    pub media_type: String,
74    pub size: u64,
75    pub digest: String,
76    pub platform: Platform,
77}
78
79#[derive(Debug, Deserialize, Serialize, Clone)]
80pub struct Platform {
81    pub architecture: Architecture,
82    pub os: OperatingSystem,
83
84    #[serde(rename = "os.version")]
85    #[serde(skip_serializing_if = "Option::is_none")]
86    os_version: Option<String>,
87
88    #[serde(rename = "os.features")]
89    #[serde(skip_serializing_if = "Option::is_none")]
90    os_features: Option<String>,
91
92    #[serde(skip_serializing_if = "Option::is_none")]
93    variant: Option<String>,
94
95    #[serde(default)]
96    #[serde(skip_serializing_if = "Option::is_none")]
97    features: Option<Vec<String>>,
98}
99
100#[derive(Debug, Deserialize, Serialize, Clone)]
101#[serde(rename_all = "lowercase")]
102pub enum Architecture {
103    #[serde(rename = "386")]
104    I386,
105
106    Amd64,
107    Arm,
108    Arm64,
109    Loong64,
110    Mips,
111    Mips64,
112    Mips64le,
113    Mipsle,
114    Ppc64,
115    Ppc64le,
116    Riscv64,
117    S390x,
118    Wasm,
119
120    Unknown,
121}
122
123#[derive(Debug, Deserialize, Serialize, Clone)]
124#[serde(rename_all = "lowercase")]
125pub enum OperatingSystem {
126    Aix,
127    Android,
128    Darwin,
129    Dragonfly,
130    Freebsd,
131    Illumos,
132    Ios,
133    Js,
134    Linux,
135    Netbsd,
136    Openbsd,
137    Plan9,
138    Solaris,
139    Wasip1,
140    Windows,
141
142    Unknown,
143}
144
145#[derive(Debug, Deserialize, Serialize, Clone)]
146pub struct Config {
147    #[serde(rename = "mediaType")]
148    pub media_type: String,
149    pub size: u64,
150    pub digest: String,
151}
152
153#[derive(Debug, Deserialize, Serialize, Clone)]
154pub struct Layer {
155    #[serde(rename = "mediaType")]
156    pub media_type: String,
157    pub size: u64,
158    pub digest: String,
159
160    #[serde(default)]
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub urls: Option<Vec<Url>>,
163
164    #[serde(default)]
165    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
166    pub annotations: BTreeMap<String, String>,
167}
168
169#[derive(Debug, Deserialize, Serialize, Clone)]
170pub struct FsLayer {
171    #[serde(rename = "blobSum")]
172    pub blob_sum: String,
173}
174
175#[derive(Debug, Deserialize, Serialize, Clone)]
176pub struct History {
177    #[serde(
178        rename = "v1Compatibility",
179        deserialize_with = "deserialize_v1_compatibility"
180    )]
181    pub v1_compatibility: V1Compatibility,
182}
183
184#[derive(Debug, Deserialize, Serialize, Clone)]
185pub struct V1Compatibility {
186    pub id: String,
187    pub created: DateTime<Utc>,
188
189    #[serde(skip_serializing_if = "Option::is_none")]
190    pub container: Option<String>,
191
192    #[serde(skip_serializing_if = "Option::is_none")]
193    pub container_config: Option<ContainerConfig>,
194}
195
196#[derive(Debug, Deserialize, Serialize, Clone)]
197pub struct ContainerConfig {
198    #[serde(rename = "Hostname")]
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub hostname: Option<String>,
201
202    #[serde(rename = "Domainname")]
203    #[serde(skip_serializing_if = "Option::is_none")]
204    pub domainname: Option<String>,
205
206    #[serde(rename = "User")]
207    #[serde(skip_serializing_if = "Option::is_none")]
208    pub user: Option<String>,
209
210    #[serde(rename = "AttachStdin")]
211    #[serde(skip_serializing_if = "Option::is_none")]
212    pub attach_stdin: Option<bool>,
213
214    #[serde(rename = "AttachStdout")]
215    #[serde(skip_serializing_if = "Option::is_none")]
216    pub attach_stdout: Option<bool>,
217
218    #[serde(rename = "AttachStderr")]
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub attach_stderr: Option<bool>,
221
222    #[serde(rename = "Tty")]
223    #[serde(skip_serializing_if = "Option::is_none")]
224    pub tty: Option<bool>,
225
226    #[serde(rename = "OpenStdin")]
227    #[serde(skip_serializing_if = "Option::is_none")]
228    pub open_stdin: Option<bool>,
229
230    #[serde(rename = "StdinOnce")]
231    #[serde(skip_serializing_if = "Option::is_none")]
232    pub stdin_once: Option<bool>,
233
234    #[serde(rename = "Env")]
235    #[serde(skip_serializing_if = "Option::is_none")]
236    pub env: Option<Vec<String>>,
237
238    #[serde(rename = "Cmd")]
239    #[serde(skip_serializing_if = "Option::is_none")]
240    pub cmd: Option<Vec<String>>,
241
242    #[serde(rename = "Image")]
243    #[serde(skip_serializing_if = "Option::is_none")]
244    pub image: Option<String>,
245
246    #[serde(rename = "Volumes")]
247    #[serde(skip_serializing_if = "Option::is_none")]
248    pub volumes: Option<BTreeMap<String, String>>,
249
250    #[serde(rename = "WorkingDir")]
251    #[serde(skip_serializing_if = "Option::is_none")]
252    pub working_dir: Option<String>,
253
254    #[serde(rename = "Entrypoint")]
255    #[serde(skip_serializing_if = "Option::is_none")]
256    pub entrypoint: Option<Vec<String>>,
257
258    #[serde(rename = "OnBuild")]
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub on_build: Option<Vec<String>>,
261
262    #[serde(rename = "Labels")]
263    #[serde(skip_serializing_if = "Option::is_none")]
264    pub labels: Option<BTreeMap<String, String>>,
265}
266
267fn deserialize_v1_compatibility<'de, D>(deserializer: D) -> Result<V1Compatibility, D::Error>
268where
269    D: Deserializer<'de>,
270{
271    let s: String = Deserialize::deserialize(deserializer)?;
272    serde_json::from_str(&s).map_err(de::Error::custom)
273}
274
275impl Serialize for SchemaVersion {
276    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
277    where
278        S: serde::Serializer,
279    {
280        match self {
281            SchemaVersion::V1 => 1i32.serialize(serializer),
282            SchemaVersion::V2 => 2i32.serialize(serializer),
283        }
284    }
285}
286
287impl<'de> Deserialize<'de> for SchemaVersion {
288    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
289    where
290        D: serde::Deserializer<'de>,
291    {
292        let value = i32::deserialize(deserializer)?;
293
294        match value {
295            1 => Ok(Self::V1),
296            2 => Ok(Self::V2),
297            _ => Err(serde::de::Error::custom("invalid enum value")),
298        }
299    }
300}
301
302impl std::fmt::Display for Architecture {
303    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
304        let out = match self {
305            Self::I386 => "386",
306            Self::Amd64 => "amd64",
307            Self::Arm => "arm",
308            Self::Arm64 => "arm64",
309            Self::Loong64 => "loong64",
310            Self::Mips => "mips",
311            Self::Mips64 => "mips64",
312            Self::Mips64le => "mips64le",
313            Self::Mipsle => "mipsle",
314            Self::Ppc64 => "ppc64",
315            Self::Ppc64le => "ppc64le",
316            Self::Riscv64 => "riscv64",
317            Self::S390x => "s390x",
318            Self::Wasm => "wasm",
319            Self::Unknown => "unknown",
320        };
321
322        f.write_str(out)
323    }
324}
325
326impl std::fmt::Display for OperatingSystem {
327    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
328        let out = match self {
329            Self::Aix => "aix",
330            Self::Android => "android",
331            Self::Darwin => "darwin",
332            Self::Dragonfly => "dragonfly",
333            Self::Freebsd => "freebsd",
334            Self::Illumos => "illumos",
335            Self::Ios => "ios",
336            Self::Js => "js",
337            Self::Linux => "linux",
338            Self::Netbsd => "netbsd",
339            Self::Openbsd => "openbsd",
340            Self::Plan9 => "plan9",
341            Self::Solaris => "solaris",
342            Self::Wasip1 => "wasip1",
343            Self::Windows => "windows",
344            Self::Unknown => "unknown",
345        };
346
347        f.write_str(out)
348    }
349}
350
351#[cfg(test)]
352#[expect(clippy::unwrap_used, reason = "unwrap use in tests is fine")]
353mod tests {
354    mod list {
355        mod deserialize {
356            use crate::manifest::List;
357
358            #[test]
359            fn example() {
360                const INPUT: &str = include_str!("../resources/manifest/list/example.json");
361
362                let out: List = serde_json::from_str(INPUT).unwrap();
363
364                insta::assert_json_snapshot!(out);
365            }
366
367            #[test]
368            fn trivy() {
369                const INPUT: &str = include_str!("../resources/manifest/list/trivy.json");
370
371                let out: List = serde_json::from_str(INPUT).unwrap();
372
373                insta::assert_json_snapshot!(out);
374            }
375
376            #[test]
377            fn vaultwarden() {
378                const INPUT: &str = include_str!("../resources/manifest/list/vaultwarden.json");
379
380                let out: List = serde_json::from_str(INPUT).unwrap();
381
382                insta::assert_json_snapshot!(out);
383            }
384        }
385    }
386
387    mod image {
388        mod deserialize {
389            use crate::manifest::Image;
390
391            #[test]
392            fn example() {
393                const INPUT: &str = include_str!("../resources/manifest/image/example.json");
394
395                let out: Image = serde_json::from_str(INPUT).unwrap();
396
397                insta::assert_json_snapshot!(out);
398            }
399        }
400    }
401
402    mod single {
403        mod deserialize {
404            use crate::manifest::Single;
405
406            #[test]
407            fn example() {
408                const INPUT: &str =
409                    include_str!("../resources/manifest/single/external-secrets-operator.json");
410
411                let out: Single = serde_json::from_str(INPUT).unwrap();
412
413                insta::assert_json_snapshot!(out);
414            }
415        }
416    }
417
418    mod v1_compatibility {
419        mod deserialize {
420            use crate::manifest::V1Compatibility;
421
422            #[test]
423            fn example() {
424                const INPUT: &str = include_str!(
425                    "../resources/manifest/v1_compatibility/external-secrets-operator.json"
426                );
427
428                let out: Vec<V1Compatibility> = serde_json::from_str(INPUT).unwrap();
429
430                insta::assert_json_snapshot!(out);
431            }
432        }
433    }
434}