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        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}