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}