bootspec/
generation.rs

1//! Provides a helper enum for deserializing from all available bootspec versions.
2use serde::{Deserialize, Serialize};
3
4use crate::v1;
5
6/// An enum of all available bootspec versions.
7///
8/// This enum is nonexhaustive, because there may be future versions added at any point, and tools
9/// should explicitly handle them (e.g. by noting they're currently unsupported).
10///
11/// ## Warnings
12///
13/// If you attempt to deserialize using this struct, you will not get any information about
14/// user-provided extensions. For that, you must deserialize with [`crate::BootJson`].
15#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
16#[non_exhaustive]
17#[serde(untagged)]
18pub enum Generation {
19    // WARNING: Add new versions to the _top_ of this list. Untagged enums in `serde` always
20    // deserialize to the first variant that succeeds, and new versions should succeed before old
21    // versions.
22    V1(v1::GenerationV1),
23}
24
25impl Generation {
26    /// The version of the bootspec document.
27    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        // Adapted from the official JSON5 document from the RFC (converted to JSON and modified to
78        // have a valid `org.nixos.specialisation.v1`).
79        // https://github.com/NixOS/rfcs/blob/02458c2ecc9f915b143b1923213b40be8ac02a96/rfcs/0125-bootspec.md#bootspec-format-v1
80        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}