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 assert_eq!(
93 from_json
94 .specialisations
95 .get(&SpecialisationName("<name>".into()))
96 .unwrap()
97 .extensions
98 .get("org.nix-community.test")
99 .unwrap()
100 .as_object()
101 .unwrap()
102 .get("foo")
103 .unwrap()
104 .as_str(),
105 Some("bar")
106 )
107 }
108
109 #[test]
110 fn valid_v1_json_basic() {
111 let json = r#"{
112 "org.nixos.bootspec.v1": {
113 "init": "/nix/store/xxx-nixos-system-xxx/init",
114 "initrd": "/nix/store/xxx-initrd-linux/initrd",
115 "initrdSecrets": "/nix/store/xxx-append-secrets/bin/append-initrd-secrets",
116 "kernel": "/nix/store/xxx-linux/bzImage",
117 "kernelParams": [
118 "amd_iommu=on",
119 "amd_iommu=pt",
120 "iommu=pt",
121 "kvm.ignore_msrs=1",
122 "kvm.report_ignored_msrs=0",
123 "udev.log_priority=3",
124 "systemd.unified_cgroup_hierarchy=1",
125 "loglevel=4"
126 ],
127 "label": "NixOS 21.11.20210810.dirty (Linux 5.15.30)",
128 "system": "x86_64-linux",
129 "toplevel": "/nix/store/xxx-nixos-system-xxx"
130 },
131 "org.nixos.specialisation.v1": {}
132}"#;
133
134 let from_json: Generation = serde_json::from_str(json).unwrap();
135 let Generation::V1(from_json) = from_json;
136
137 let bootspec = BootSpecV1 {
138 system: String::from("x86_64-linux"),
139 label: String::from("NixOS 21.11.20210810.dirty (Linux 5.15.30)"),
140 kernel: PathBuf::from("/nix/store/xxx-linux/bzImage"),
141 kernel_params: [
142 "amd_iommu=on",
143 "amd_iommu=pt",
144 "iommu=pt",
145 "kvm.ignore_msrs=1",
146 "kvm.report_ignored_msrs=0",
147 "udev.log_priority=3",
148 "systemd.unified_cgroup_hierarchy=1",
149 "loglevel=4",
150 ]
151 .iter()
152 .map(ToString::to_string)
153 .collect(),
154 init: PathBuf::from("/nix/store/xxx-nixos-system-xxx/init"),
155 initrd: Some(PathBuf::from("/nix/store/xxx-initrd-linux/initrd")),
156 initrd_secrets: Some(PathBuf::from(
157 "/nix/store/xxx-append-secrets/bin/append-initrd-secrets",
158 )),
159 toplevel: SystemConfigurationRoot(PathBuf::from("/nix/store/xxx-nixos-system-xxx")),
160 };
161 let expected = GenerationV1 {
162 bootspec,
163 specialisations: HashMap::new(),
164 };
165
166 assert_eq!(from_json, expected);
167 }
168
169 #[test]
170 fn valid_v1_json_with_typed_extension() {
171 let json = r#"{
172 "org.nixos.bootspec.v1": {
173 "init": "/nix/store/xxx-nixos-system-xxx/init",
174 "initrd": "/nix/store/xxx-initrd-linux/initrd",
175 "initrdSecrets": "/nix/store/xxx-append-secrets/bin/append-initrd-secrets",
176 "kernel": "/nix/store/xxx-linux/bzImage",
177 "kernelParams": [
178 "amd_iommu=on",
179 "amd_iommu=pt",
180 "iommu=pt",
181 "kvm.ignore_msrs=1",
182 "kvm.report_ignored_msrs=0",
183 "udev.log_priority=3",
184 "systemd.unified_cgroup_hierarchy=1",
185 "loglevel=4"
186 ],
187 "label": "NixOS 21.11.20210810.dirty (Linux 5.15.30)",
188 "system": "x86_64-linux",
189 "toplevel": "/nix/store/xxx-nixos-system-xxx"
190 },
191 "org.nixos.specialisation.v1": {},
192 "org.test": { "key": "hello" }
193}"#;
194
195 let from_json: BootJson = serde_json::from_str(json).unwrap();
196
197 let bootspec = BootSpecV1 {
198 system: String::from("x86_64-linux"),
199 label: String::from("NixOS 21.11.20210810.dirty (Linux 5.15.30)"),
200 kernel: PathBuf::from("/nix/store/xxx-linux/bzImage"),
201 kernel_params: [
202 "amd_iommu=on",
203 "amd_iommu=pt",
204 "iommu=pt",
205 "kvm.ignore_msrs=1",
206 "kvm.report_ignored_msrs=0",
207 "udev.log_priority=3",
208 "systemd.unified_cgroup_hierarchy=1",
209 "loglevel=4",
210 ]
211 .iter()
212 .map(ToString::to_string)
213 .collect(),
214 init: PathBuf::from("/nix/store/xxx-nixos-system-xxx/init"),
215 initrd: Some(PathBuf::from("/nix/store/xxx-initrd-linux/initrd")),
216 initrd_secrets: Some(PathBuf::from(
217 "/nix/store/xxx-append-secrets/bin/append-initrd-secrets",
218 )),
219 toplevel: SystemConfigurationRoot(PathBuf::from("/nix/store/xxx-nixos-system-xxx")),
220 };
221 let generation = GenerationV1 {
222 bootspec,
223 specialisations: HashMap::new(),
224 };
225 let expected = BootJson {
226 generation: Generation::V1(generation),
227 extensions: HashMap::from([("org.test".into(), serde_json::json!({ "key": "hello" }))]),
228 };
229
230 let from_extension: TestExtension = Deserialize::deserialize(
231 from_json
232 .extensions
233 .get("org.test")
234 .unwrap()
235 .to_owned()
236 .into_deserializer(),
237 )
238 .unwrap();
239 let expected_extension = TestExtension {
240 test: "hello".into(),
241 };
242
243 assert_eq!(from_json, expected);
244 assert_eq!(from_extension, expected_extension);
245 }
246
247 #[test]
248 fn valid_v1_json_with_typed_optional_extension_fields_and_empty_object() {
249 let json = r#"{
250 "org.nixos.bootspec.v1": {
251 "init": "/nix/store/xxx-nixos-system-xxx/init",
252 "initrd": "/nix/store/xxx-initrd-linux/initrd",
253 "initrdSecrets": "/nix/store/xxx-append-secrets/bin/append-initrd-secrets",
254 "kernel": "/nix/store/xxx-linux/bzImage",
255 "kernelParams": [
256 "amd_iommu=on",
257 "amd_iommu=pt",
258 "iommu=pt",
259 "kvm.ignore_msrs=1",
260 "kvm.report_ignored_msrs=0",
261 "udev.log_priority=3",
262 "systemd.unified_cgroup_hierarchy=1",
263 "loglevel=4"
264 ],
265 "label": "NixOS 21.11.20210810.dirty (Linux 5.15.30)",
266 "system": "x86_64-linux",
267 "toplevel": "/nix/store/xxx-nixos-system-xxx"
268 },
269 "org.nixos.specialisation.v1": {}
270}"#;
271
272 let from_json: BootJson = serde_json::from_str(json).unwrap();
273
274 let bootspec = BootSpecV1 {
275 system: String::from("x86_64-linux"),
276 label: String::from("NixOS 21.11.20210810.dirty (Linux 5.15.30)"),
277 kernel: PathBuf::from("/nix/store/xxx-linux/bzImage"),
278 kernel_params: [
279 "amd_iommu=on",
280 "amd_iommu=pt",
281 "iommu=pt",
282 "kvm.ignore_msrs=1",
283 "kvm.report_ignored_msrs=0",
284 "udev.log_priority=3",
285 "systemd.unified_cgroup_hierarchy=1",
286 "loglevel=4",
287 ]
288 .iter()
289 .map(ToString::to_string)
290 .collect(),
291 init: PathBuf::from("/nix/store/xxx-nixos-system-xxx/init"),
292 initrd: Some(PathBuf::from("/nix/store/xxx-initrd-linux/initrd")),
293 initrd_secrets: Some(PathBuf::from(
294 "/nix/store/xxx-append-secrets/bin/append-initrd-secrets",
295 )),
296 toplevel: SystemConfigurationRoot(PathBuf::from("/nix/store/xxx-nixos-system-xxx")),
297 };
298 let generation = GenerationV1 {
299 bootspec,
300 specialisations: HashMap::new(),
301 };
302 let expected = BootJson {
303 generation: Generation::V1(generation),
304 extensions: HashMap::new(),
305 };
306
307 assert_eq!(from_json, expected);
308 }
309
310 #[test]
311 fn invalid_v1_json_with_null_extension() {
312 let json = r#"{
313 "org.nixos.bootspec.v1": {
314 "init": "/nix/store/xxx-nixos-system-xxx/init",
315 "initrd": "/nix/store/xxx-initrd-linux/initrd",
316 "initrdSecrets": "/nix/store/xxx-append-secrets/bin/append-initrd-secrets",
317 "kernel": "/nix/store/xxx-linux/bzImage",
318 "kernelParams": [
319 "amd_iommu=on",
320 "amd_iommu=pt",
321 "iommu=pt",
322 "kvm.ignore_msrs=1",
323 "kvm.report_ignored_msrs=0",
324 "udev.log_priority=3",
325 "systemd.unified_cgroup_hierarchy=1",
326 "loglevel=4"
327 ],
328 "label": "NixOS 21.11.20210810.dirty (Linux 5.15.30)",
329 "system": "x86_64-linux",
330 "toplevel": "/nix/store/xxx-nixos-system-xxx"
331 },
332 "org.nixos.specialisation.v1": {},
333 "org.test2": { "hi": null },
334 "org.test": null
335}"#;
336 let json_err = serde_json::from_str::<BootJson>(json).unwrap_err();
337 assert!(json_err
338 .to_string()
339 .contains("org.test was null, but null extensions are not allowed"));
340 }
341
342 #[test]
343 fn valid_v1_json_without_extension() {
344 let json = r#"{
345 "org.nixos.bootspec.v1": {
346 "init": "/nix/store/xxx-nixos-system-xxx/init",
347 "initrd": "/nix/store/xxx-initrd-linux/initrd",
348 "initrdSecrets": "/nix/store/xxx-append-secrets/bin/append-initrd-secrets",
349 "kernel": "/nix/store/xxx-linux/bzImage",
350 "kernelParams": [
351 "amd_iommu=on",
352 "amd_iommu=pt",
353 "iommu=pt",
354 "kvm.ignore_msrs=1",
355 "kvm.report_ignored_msrs=0",
356 "udev.log_priority=3",
357 "systemd.unified_cgroup_hierarchy=1",
358 "loglevel=4"
359 ],
360 "label": "NixOS 21.11.20210810.dirty (Linux 5.15.30)",
361 "system": "x86_64-linux",
362 "toplevel": "/nix/store/xxx-nixos-system-xxx"
363 },
364 "org.nixos.specialisation.v1": {}
365}"#;
366
367 let from_json: BootJson = serde_json::from_str(json).unwrap();
368
369 let bootspec = BootSpecV1 {
370 system: String::from("x86_64-linux"),
371 label: String::from("NixOS 21.11.20210810.dirty (Linux 5.15.30)"),
372 kernel: PathBuf::from("/nix/store/xxx-linux/bzImage"),
373 kernel_params: [
374 "amd_iommu=on",
375 "amd_iommu=pt",
376 "iommu=pt",
377 "kvm.ignore_msrs=1",
378 "kvm.report_ignored_msrs=0",
379 "udev.log_priority=3",
380 "systemd.unified_cgroup_hierarchy=1",
381 "loglevel=4",
382 ]
383 .iter()
384 .map(ToString::to_string)
385 .collect(),
386 init: PathBuf::from("/nix/store/xxx-nixos-system-xxx/init"),
387 initrd: Some(PathBuf::from("/nix/store/xxx-initrd-linux/initrd")),
388 initrd_secrets: Some(PathBuf::from(
389 "/nix/store/xxx-append-secrets/bin/append-initrd-secrets",
390 )),
391 toplevel: SystemConfigurationRoot(PathBuf::from("/nix/store/xxx-nixos-system-xxx")),
392 };
393 let generation = GenerationV1 {
394 bootspec,
395 specialisations: HashMap::new(),
396 };
397 let expected = BootJson {
398 generation: Generation::V1(generation),
399 extensions: HashMap::new(),
400 };
401
402 assert_eq!(from_json, expected);
403 }
404
405 #[test]
406 fn valid_v1_json_without_initrd_and_specialisation() {
407 let json = r#"{
408 "org.nixos.bootspec.v1": {
409 "init": "/nix/store/xxx-nixos-system-xxx/init",
410 "kernel": "/nix/store/xxx-linux/bzImage",
411 "kernelParams": [
412 "amd_iommu=on",
413 "amd_iommu=pt",
414 "iommu=pt",
415 "kvm.ignore_msrs=1",
416 "kvm.report_ignored_msrs=0",
417 "udev.log_priority=3",
418 "systemd.unified_cgroup_hierarchy=1",
419 "loglevel=4"
420 ],
421 "label": "NixOS 21.11.20210810.dirty (Linux 5.15.30)",
422 "system": "x86_64-linux",
423 "toplevel": "/nix/store/xxx-nixos-system-xxx"
424 }
425}"#;
426
427 let from_json: BootJson = serde_json::from_str(json).unwrap();
428
429 let bootspec = BootSpecV1 {
430 system: String::from("x86_64-linux"),
431 label: String::from("NixOS 21.11.20210810.dirty (Linux 5.15.30)"),
432 kernel: PathBuf::from("/nix/store/xxx-linux/bzImage"),
433 kernel_params: [
434 "amd_iommu=on",
435 "amd_iommu=pt",
436 "iommu=pt",
437 "kvm.ignore_msrs=1",
438 "kvm.report_ignored_msrs=0",
439 "udev.log_priority=3",
440 "systemd.unified_cgroup_hierarchy=1",
441 "loglevel=4",
442 ]
443 .iter()
444 .map(ToString::to_string)
445 .collect(),
446 init: PathBuf::from("/nix/store/xxx-nixos-system-xxx/init"),
447 initrd: None,
448 initrd_secrets: None,
449 toplevel: SystemConfigurationRoot(PathBuf::from("/nix/store/xxx-nixos-system-xxx")),
450 };
451 let generation = GenerationV1 {
452 bootspec,
453 specialisations: HashMap::new(),
454 };
455 let expected = BootJson {
456 generation: Generation::V1(generation),
457 extensions: HashMap::new(),
458 };
459
460 assert_eq!(from_json, expected);
461 }
462
463 #[test]
464 fn invalid_v1_json_with_null_specialisation() {
465 let json = r#"{
466 "org.nixos.bootspec.v1": {
467 "init": "/nix/store/xxx-nixos-system-xxx/init",
468 "initrd": "/nix/store/xxx-initrd-linux/initrd",
469 "initrdSecrets": "/nix/store/xxx-append-secrets/bin/append-initrd-secrets",
470 "kernel": "/nix/store/xxx-linux/bzImage",
471 "kernelParams": [
472 "amd_iommu=on",
473 "amd_iommu=pt",
474 "iommu=pt",
475 "kvm.ignore_msrs=1",
476 "kvm.report_ignored_msrs=0",
477 "udev.log_priority=3",
478 "systemd.unified_cgroup_hierarchy=1",
479 "loglevel=4"
480 ],
481 "label": "NixOS 21.11.20210810.dirty (Linux 5.15.30)",
482 "system": "x86_64-linux",
483 "toplevel": "/nix/store/xxx-nixos-system-xxx"
484 },
485 "org.nixos.specialisation.v1": null
486}"#;
487
488 let json_err = serde_json::from_str::<GenerationV1>(json).unwrap_err();
489 assert!(json_err.to_string().contains("expected a map"));
490 }
491
492 #[test]
493 fn invalid_json_invalid_version() {
494 let json = format!(
495 r#"{{
496 "org.nixos.bootspec.v{}": {{
497 "init": "/nix/store/xxx-nixos-system-xxx/init",
498 "initrd": "/nix/store/xxx-initrd-linux/initrd",
499 "initrdSecrets": "/nix/store/xxx-append-secrets/bin/append-initrd-secrets",
500 "kernel": "/nix/store/xxx-linux/bzImage",
501 "kernelParams": [
502 "amd_iommu=on",
503 "amd_iommu=pt",
504 "iommu=pt",
505 "kvm.ignore_msrs=1",
506 "kvm.report_ignored_msrs=0",
507 "udev.log_priority=3",
508 "systemd.unified_cgroup_hierarchy=1",
509 "loglevel=4"
510 ],
511 "label": "NixOS 21.11.20210810.dirty (Linux 5.15.30)",
512 "system": "x86_64-linux",
513 "toplevel": "/nix/store/xxx-nixos-system-xxx"
514 }},
515 "org.nixos.specialisation.v{}": {{}}
516}}"#,
517 SCHEMA_VERSION + 1,
518 SCHEMA_VERSION + 1
519 );
520
521 let json_err = serde_json::from_str::<Generation>(&json).unwrap_err();
522 assert!(json_err.to_string().contains("did not match any variant"));
523 }
524
525 #[test]
526 fn valid_v1_json_to_generation_via_try_into() {
527 let json = r#"{
528 "org.nixos.bootspec.v1": {
529 "init": "/nix/store/xxx-nixos-system-xxx/init",
530 "initrd": "/nix/store/xxx-initrd-linux/initrd",
531 "initrdSecrets": "/nix/store/xxx-append-secrets/bin/append-initrd-secrets",
532 "kernel": "/nix/store/xxx-linux/bzImage",
533 "kernelParams": [
534 "amd_iommu=on",
535 "amd_iommu=pt",
536 "iommu=pt",
537 "kvm.ignore_msrs=1",
538 "kvm.report_ignored_msrs=0",
539 "udev.log_priority=3",
540 "systemd.unified_cgroup_hierarchy=1",
541 "loglevel=4"
542 ],
543 "label": "NixOS 21.11.20210810.dirty (Linux 5.15.30)",
544 "system": "x86_64-linux",
545 "toplevel": "/nix/store/xxx-nixos-system-xxx"
546 },
547 "org.nixos.specialisation.v1": {}
548}"#;
549
550 let from_json: BootJson = serde_json::from_str(json).unwrap();
551 let _generation: GenerationV1 = from_json.generation.try_into().unwrap();
552 }
553}