1use serde::{Deserialize, Serialize};
3
4use crate::v1;
5
6#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
16#[non_exhaustive]
17#[serde(untagged)]
18pub enum Generation {
19 V1(v1::GenerationV1),
23}
24
25impl Generation {
26 pub fn version(&self) -> u64 {
28 use Generation::*;
29
30 match self {
31 V1(_) => v1::SCHEMA_VERSION,
32 }
33 }
34}
35
36impl TryFrom<Generation> for v1::GenerationV1 {
37 type Error = crate::BootspecError;
38
39 fn try_from(value: Generation) -> Result<Self, Self::Error> {
40 #[allow(clippy::infallible_destructuring_match)]
41 let ret = match value {
42 Generation::V1(v1) => v1,
43 };
44
45 Ok(ret)
46 }
47}
48
49#[cfg(test)]
50mod tests {
51 use std::collections::HashMap;
52 use std::path::PathBuf;
53
54 use serde::de::IntoDeserializer;
55 use serde::{Deserialize, Serialize};
56
57 use super::Generation;
58 use crate::{
59 v1::{BootSpecV1, GenerationV1},
60 BootJson, SpecialisationName, SystemConfigurationRoot, SCHEMA_VERSION,
61 };
62
63 #[derive(Debug, Deserialize, Serialize, PartialEq, Default)]
64 struct TestExtension {
65 #[serde(rename = "key")]
66 test: String,
67 }
68
69 #[derive(Debug, Deserialize, Serialize, PartialEq, Default)]
70 struct TestOptionalExtension {
71 #[serde(rename = "key")]
72 test: Option<String>,
73 }
74
75 #[test]
76 fn valid_v1_rfc0125_json() {
77 let rfc_json = include_str!("../rfc0125_spec.json");
81 let from_json = serde_json::from_str::<Generation>(&rfc_json).unwrap();
82 assert_eq!(from_json.version(), 1);
83
84 let Generation::V1(from_json) = from_json;
85 let keys = from_json
86 .specialisations
87 .keys()
88 .map(ToOwned::to_owned)
89 .collect::<Vec<_>>();
90 assert!(keys.contains(&SpecialisationName(String::from("<name>"))));
91 }
92
93 #[test]
94 fn valid_v1_json_basic() {
95 let json = r#"{
96 "org.nixos.bootspec.v1": {
97 "init": "/nix/store/xxx-nixos-system-xxx/init",
98 "initrd": "/nix/store/xxx-initrd-linux/initrd",
99 "initrdSecrets": "/nix/store/xxx-append-secrets/bin/append-initrd-secrets",
100 "kernel": "/nix/store/xxx-linux/bzImage",
101 "kernelParams": [
102 "amd_iommu=on",
103 "amd_iommu=pt",
104 "iommu=pt",
105 "kvm.ignore_msrs=1",
106 "kvm.report_ignored_msrs=0",
107 "udev.log_priority=3",
108 "systemd.unified_cgroup_hierarchy=1",
109 "loglevel=4"
110 ],
111 "label": "NixOS 21.11.20210810.dirty (Linux 5.15.30)",
112 "system": "x86_64-linux",
113 "toplevel": "/nix/store/xxx-nixos-system-xxx"
114 },
115 "org.nixos.specialisation.v1": {}
116}"#;
117
118 let from_json: Generation = serde_json::from_str(&json).unwrap();
119 let Generation::V1(from_json) = from_json;
120
121 let bootspec = BootSpecV1 {
122 system: String::from("x86_64-linux"),
123 label: String::from("NixOS 21.11.20210810.dirty (Linux 5.15.30)"),
124 kernel: PathBuf::from("/nix/store/xxx-linux/bzImage"),
125 kernel_params: vec![
126 "amd_iommu=on",
127 "amd_iommu=pt",
128 "iommu=pt",
129 "kvm.ignore_msrs=1",
130 "kvm.report_ignored_msrs=0",
131 "udev.log_priority=3",
132 "systemd.unified_cgroup_hierarchy=1",
133 "loglevel=4",
134 ]
135 .iter()
136 .map(ToString::to_string)
137 .collect(),
138 init: PathBuf::from("/nix/store/xxx-nixos-system-xxx/init"),
139 initrd: Some(PathBuf::from("/nix/store/xxx-initrd-linux/initrd")),
140 initrd_secrets: Some(PathBuf::from(
141 "/nix/store/xxx-append-secrets/bin/append-initrd-secrets",
142 )),
143 toplevel: SystemConfigurationRoot(PathBuf::from("/nix/store/xxx-nixos-system-xxx")),
144 };
145 let expected = GenerationV1 {
146 bootspec,
147 specialisations: HashMap::new(),
148 };
149
150 assert_eq!(from_json, expected);
151 }
152
153 #[test]
154 fn valid_v1_json_with_typed_extension() {
155 let json = r#"{
156 "org.nixos.bootspec.v1": {
157 "init": "/nix/store/xxx-nixos-system-xxx/init",
158 "initrd": "/nix/store/xxx-initrd-linux/initrd",
159 "initrdSecrets": "/nix/store/xxx-append-secrets/bin/append-initrd-secrets",
160 "kernel": "/nix/store/xxx-linux/bzImage",
161 "kernelParams": [
162 "amd_iommu=on",
163 "amd_iommu=pt",
164 "iommu=pt",
165 "kvm.ignore_msrs=1",
166 "kvm.report_ignored_msrs=0",
167 "udev.log_priority=3",
168 "systemd.unified_cgroup_hierarchy=1",
169 "loglevel=4"
170 ],
171 "label": "NixOS 21.11.20210810.dirty (Linux 5.15.30)",
172 "system": "x86_64-linux",
173 "toplevel": "/nix/store/xxx-nixos-system-xxx"
174 },
175 "org.nixos.specialisation.v1": {},
176 "org.test": { "key": "hello" }
177}"#;
178
179 let from_json: BootJson = serde_json::from_str(&json).unwrap();
180
181 let bootspec = BootSpecV1 {
182 system: String::from("x86_64-linux"),
183 label: String::from("NixOS 21.11.20210810.dirty (Linux 5.15.30)"),
184 kernel: PathBuf::from("/nix/store/xxx-linux/bzImage"),
185 kernel_params: vec![
186 "amd_iommu=on",
187 "amd_iommu=pt",
188 "iommu=pt",
189 "kvm.ignore_msrs=1",
190 "kvm.report_ignored_msrs=0",
191 "udev.log_priority=3",
192 "systemd.unified_cgroup_hierarchy=1",
193 "loglevel=4",
194 ]
195 .iter()
196 .map(ToString::to_string)
197 .collect(),
198 init: PathBuf::from("/nix/store/xxx-nixos-system-xxx/init"),
199 initrd: Some(PathBuf::from("/nix/store/xxx-initrd-linux/initrd")),
200 initrd_secrets: Some(PathBuf::from(
201 "/nix/store/xxx-append-secrets/bin/append-initrd-secrets",
202 )),
203 toplevel: SystemConfigurationRoot(PathBuf::from("/nix/store/xxx-nixos-system-xxx")),
204 };
205 let generation = GenerationV1 {
206 bootspec,
207 specialisations: HashMap::new(),
208 };
209 let expected = BootJson {
210 generation: Generation::V1(generation),
211 extensions: HashMap::from([("org.test".into(), serde_json::json!({ "key": "hello" }))]),
212 };
213
214 let from_extension: TestExtension = Deserialize::deserialize(
215 from_json
216 .extensions
217 .get("org.test")
218 .unwrap()
219 .to_owned()
220 .into_deserializer(),
221 )
222 .unwrap();
223 let expected_extension = TestExtension {
224 test: "hello".into(),
225 };
226
227 assert_eq!(from_json, expected);
228 assert_eq!(from_extension, expected_extension);
229 }
230
231 #[test]
232 fn valid_v1_json_with_typed_optional_extension_fields_and_empty_object() {
233 let json = r#"{
234 "org.nixos.bootspec.v1": {
235 "init": "/nix/store/xxx-nixos-system-xxx/init",
236 "initrd": "/nix/store/xxx-initrd-linux/initrd",
237 "initrdSecrets": "/nix/store/xxx-append-secrets/bin/append-initrd-secrets",
238 "kernel": "/nix/store/xxx-linux/bzImage",
239 "kernelParams": [
240 "amd_iommu=on",
241 "amd_iommu=pt",
242 "iommu=pt",
243 "kvm.ignore_msrs=1",
244 "kvm.report_ignored_msrs=0",
245 "udev.log_priority=3",
246 "systemd.unified_cgroup_hierarchy=1",
247 "loglevel=4"
248 ],
249 "label": "NixOS 21.11.20210810.dirty (Linux 5.15.30)",
250 "system": "x86_64-linux",
251 "toplevel": "/nix/store/xxx-nixos-system-xxx"
252 },
253 "org.nixos.specialisation.v1": {}
254}"#;
255
256 let from_json: BootJson = serde_json::from_str(&json).unwrap();
257
258 let bootspec = BootSpecV1 {
259 system: String::from("x86_64-linux"),
260 label: String::from("NixOS 21.11.20210810.dirty (Linux 5.15.30)"),
261 kernel: PathBuf::from("/nix/store/xxx-linux/bzImage"),
262 kernel_params: vec![
263 "amd_iommu=on",
264 "amd_iommu=pt",
265 "iommu=pt",
266 "kvm.ignore_msrs=1",
267 "kvm.report_ignored_msrs=0",
268 "udev.log_priority=3",
269 "systemd.unified_cgroup_hierarchy=1",
270 "loglevel=4",
271 ]
272 .iter()
273 .map(ToString::to_string)
274 .collect(),
275 init: PathBuf::from("/nix/store/xxx-nixos-system-xxx/init"),
276 initrd: Some(PathBuf::from("/nix/store/xxx-initrd-linux/initrd")),
277 initrd_secrets: Some(PathBuf::from(
278 "/nix/store/xxx-append-secrets/bin/append-initrd-secrets",
279 )),
280 toplevel: SystemConfigurationRoot(PathBuf::from("/nix/store/xxx-nixos-system-xxx")),
281 };
282 let generation = GenerationV1 {
283 bootspec,
284 specialisations: HashMap::new(),
285 };
286 let expected = BootJson {
287 generation: Generation::V1(generation),
288 extensions: HashMap::new(),
289 };
290
291 assert_eq!(from_json, expected);
292 }
293
294 #[test]
295 fn invalid_v1_json_with_null_extension() {
296 let json = r#"{
297 "org.nixos.bootspec.v1": {
298 "init": "/nix/store/xxx-nixos-system-xxx/init",
299 "initrd": "/nix/store/xxx-initrd-linux/initrd",
300 "initrdSecrets": "/nix/store/xxx-append-secrets/bin/append-initrd-secrets",
301 "kernel": "/nix/store/xxx-linux/bzImage",
302 "kernelParams": [
303 "amd_iommu=on",
304 "amd_iommu=pt",
305 "iommu=pt",
306 "kvm.ignore_msrs=1",
307 "kvm.report_ignored_msrs=0",
308 "udev.log_priority=3",
309 "systemd.unified_cgroup_hierarchy=1",
310 "loglevel=4"
311 ],
312 "label": "NixOS 21.11.20210810.dirty (Linux 5.15.30)",
313 "system": "x86_64-linux",
314 "toplevel": "/nix/store/xxx-nixos-system-xxx"
315 },
316 "org.nixos.specialisation.v1": {},
317 "org.test2": { "hi": null },
318 "org.test": null
319}"#;
320 let json_err = serde_json::from_str::<BootJson>(&json).unwrap_err();
321 assert!(json_err
322 .to_string()
323 .contains("org.test was null, but null extensions are not allowed"));
324 }
325
326 #[test]
327 fn valid_v1_json_without_extension() {
328 let json = r#"{
329 "org.nixos.bootspec.v1": {
330 "init": "/nix/store/xxx-nixos-system-xxx/init",
331 "initrd": "/nix/store/xxx-initrd-linux/initrd",
332 "initrdSecrets": "/nix/store/xxx-append-secrets/bin/append-initrd-secrets",
333 "kernel": "/nix/store/xxx-linux/bzImage",
334 "kernelParams": [
335 "amd_iommu=on",
336 "amd_iommu=pt",
337 "iommu=pt",
338 "kvm.ignore_msrs=1",
339 "kvm.report_ignored_msrs=0",
340 "udev.log_priority=3",
341 "systemd.unified_cgroup_hierarchy=1",
342 "loglevel=4"
343 ],
344 "label": "NixOS 21.11.20210810.dirty (Linux 5.15.30)",
345 "system": "x86_64-linux",
346 "toplevel": "/nix/store/xxx-nixos-system-xxx"
347 },
348 "org.nixos.specialisation.v1": {}
349}"#;
350
351 let from_json: BootJson = serde_json::from_str(&json).unwrap();
352
353 let bootspec = BootSpecV1 {
354 system: String::from("x86_64-linux"),
355 label: String::from("NixOS 21.11.20210810.dirty (Linux 5.15.30)"),
356 kernel: PathBuf::from("/nix/store/xxx-linux/bzImage"),
357 kernel_params: vec![
358 "amd_iommu=on",
359 "amd_iommu=pt",
360 "iommu=pt",
361 "kvm.ignore_msrs=1",
362 "kvm.report_ignored_msrs=0",
363 "udev.log_priority=3",
364 "systemd.unified_cgroup_hierarchy=1",
365 "loglevel=4",
366 ]
367 .iter()
368 .map(ToString::to_string)
369 .collect(),
370 init: PathBuf::from("/nix/store/xxx-nixos-system-xxx/init"),
371 initrd: Some(PathBuf::from("/nix/store/xxx-initrd-linux/initrd")),
372 initrd_secrets: Some(PathBuf::from(
373 "/nix/store/xxx-append-secrets/bin/append-initrd-secrets",
374 )),
375 toplevel: SystemConfigurationRoot(PathBuf::from("/nix/store/xxx-nixos-system-xxx")),
376 };
377 let generation = GenerationV1 {
378 bootspec,
379 specialisations: HashMap::new(),
380 };
381 let expected = BootJson {
382 generation: Generation::V1(generation),
383 extensions: HashMap::new(),
384 };
385
386 assert_eq!(from_json, expected);
387 }
388
389 #[test]
390 fn valid_v1_json_without_initrd_and_specialisation() {
391 let json = r#"{
392 "org.nixos.bootspec.v1": {
393 "init": "/nix/store/xxx-nixos-system-xxx/init",
394 "kernel": "/nix/store/xxx-linux/bzImage",
395 "kernelParams": [
396 "amd_iommu=on",
397 "amd_iommu=pt",
398 "iommu=pt",
399 "kvm.ignore_msrs=1",
400 "kvm.report_ignored_msrs=0",
401 "udev.log_priority=3",
402 "systemd.unified_cgroup_hierarchy=1",
403 "loglevel=4"
404 ],
405 "label": "NixOS 21.11.20210810.dirty (Linux 5.15.30)",
406 "system": "x86_64-linux",
407 "toplevel": "/nix/store/xxx-nixos-system-xxx"
408 }
409}"#;
410
411 let from_json: BootJson = serde_json::from_str(&json).unwrap();
412
413 let bootspec = BootSpecV1 {
414 system: String::from("x86_64-linux"),
415 label: String::from("NixOS 21.11.20210810.dirty (Linux 5.15.30)"),
416 kernel: PathBuf::from("/nix/store/xxx-linux/bzImage"),
417 kernel_params: vec![
418 "amd_iommu=on",
419 "amd_iommu=pt",
420 "iommu=pt",
421 "kvm.ignore_msrs=1",
422 "kvm.report_ignored_msrs=0",
423 "udev.log_priority=3",
424 "systemd.unified_cgroup_hierarchy=1",
425 "loglevel=4",
426 ]
427 .iter()
428 .map(ToString::to_string)
429 .collect(),
430 init: PathBuf::from("/nix/store/xxx-nixos-system-xxx/init"),
431 initrd: None,
432 initrd_secrets: None,
433 toplevel: SystemConfigurationRoot(PathBuf::from("/nix/store/xxx-nixos-system-xxx")),
434 };
435 let generation = GenerationV1 {
436 bootspec,
437 specialisations: HashMap::new(),
438 };
439 let expected = BootJson {
440 generation: Generation::V1(generation),
441 extensions: HashMap::new(),
442 };
443
444 assert_eq!(from_json, expected);
445 }
446
447 #[test]
448 fn invalid_v1_json_with_null_specialisation() {
449 let json = r#"{
450 "org.nixos.bootspec.v1": {
451 "init": "/nix/store/xxx-nixos-system-xxx/init",
452 "initrd": "/nix/store/xxx-initrd-linux/initrd",
453 "initrdSecrets": "/nix/store/xxx-append-secrets/bin/append-initrd-secrets",
454 "kernel": "/nix/store/xxx-linux/bzImage",
455 "kernelParams": [
456 "amd_iommu=on",
457 "amd_iommu=pt",
458 "iommu=pt",
459 "kvm.ignore_msrs=1",
460 "kvm.report_ignored_msrs=0",
461 "udev.log_priority=3",
462 "systemd.unified_cgroup_hierarchy=1",
463 "loglevel=4"
464 ],
465 "label": "NixOS 21.11.20210810.dirty (Linux 5.15.30)",
466 "system": "x86_64-linux",
467 "toplevel": "/nix/store/xxx-nixos-system-xxx"
468 },
469 "org.nixos.specialisation.v1": null
470}"#;
471
472 let json_err = serde_json::from_str::<GenerationV1>(&json).unwrap_err();
473 assert!(json_err.to_string().contains("expected a map"));
474 }
475
476 #[test]
477 fn invalid_json_invalid_version() {
478 let json = format!(
479 r#"{{
480 "org.nixos.bootspec.v{}": {{
481 "init": "/nix/store/xxx-nixos-system-xxx/init",
482 "initrd": "/nix/store/xxx-initrd-linux/initrd",
483 "initrdSecrets": "/nix/store/xxx-append-secrets/bin/append-initrd-secrets",
484 "kernel": "/nix/store/xxx-linux/bzImage",
485 "kernelParams": [
486 "amd_iommu=on",
487 "amd_iommu=pt",
488 "iommu=pt",
489 "kvm.ignore_msrs=1",
490 "kvm.report_ignored_msrs=0",
491 "udev.log_priority=3",
492 "systemd.unified_cgroup_hierarchy=1",
493 "loglevel=4"
494 ],
495 "label": "NixOS 21.11.20210810.dirty (Linux 5.15.30)",
496 "system": "x86_64-linux",
497 "toplevel": "/nix/store/xxx-nixos-system-xxx"
498 }},
499 "org.nixos.specialisation.v{}": {{}}
500}}"#,
501 SCHEMA_VERSION + 1,
502 SCHEMA_VERSION + 1
503 );
504
505 let json_err = serde_json::from_str::<Generation>(&json).unwrap_err();
506 assert!(json_err.to_string().contains("did not match any variant"));
507 }
508
509 #[test]
510 fn valid_v1_json_to_generation_via_try_into() {
511 let json = r#"{
512 "org.nixos.bootspec.v1": {
513 "init": "/nix/store/xxx-nixos-system-xxx/init",
514 "initrd": "/nix/store/xxx-initrd-linux/initrd",
515 "initrdSecrets": "/nix/store/xxx-append-secrets/bin/append-initrd-secrets",
516 "kernel": "/nix/store/xxx-linux/bzImage",
517 "kernelParams": [
518 "amd_iommu=on",
519 "amd_iommu=pt",
520 "iommu=pt",
521 "kvm.ignore_msrs=1",
522 "kvm.report_ignored_msrs=0",
523 "udev.log_priority=3",
524 "systemd.unified_cgroup_hierarchy=1",
525 "loglevel=4"
526 ],
527 "label": "NixOS 21.11.20210810.dirty (Linux 5.15.30)",
528 "system": "x86_64-linux",
529 "toplevel": "/nix/store/xxx-nixos-system-xxx"
530 },
531 "org.nixos.specialisation.v1": {}
532}"#;
533
534 let from_json: BootJson = serde_json::from_str(&json).unwrap();
535 let _generation: GenerationV1 = from_json.generation.try_into().unwrap();
536 }
537}