Skip to main content

ostool/boot/
payload.rs

1//! Host boot payload shared by QEMU, U-Boot, and board runners.
2
3use std::path::PathBuf;
4
5use anyhow::Result;
6use httpboot_protocol::valid_host_cmdline;
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9
10use crate::project::variables::{self, VariableScope};
11
12/// Optional host initramfs and kernel command line.
13#[derive(Clone, Debug, Default, Deserialize, Serialize, JsonSchema, PartialEq, Eq)]
14pub struct BootPayloadConfig {
15    /// Path to a host initramfs archive; guest initrds are configured separately.
16    #[serde(default, skip_serializing_if = "Option::is_none")]
17    pub initramfs: Option<String>,
18    /// Boot arguments passed to the host kernel.
19    #[serde(default, skip_serializing_if = "Option::is_none")]
20    pub cmdline: Option<String>,
21}
22
23impl BootPayloadConfig {
24    pub(crate) fn replace_strings(&mut self, scope: &VariableScope) -> Result<()> {
25        self.initramfs = self
26            .initramfs
27            .as_deref()
28            .map(|path| variables::expand_variables(path, scope))
29            .transpose()?;
30        self.cmdline = self
31            .cmdline
32            .as_deref()
33            .map(|value| variables::expand_variables(value, scope))
34            .transpose()?;
35        Ok(())
36    }
37
38    pub(crate) fn initramfs_path(&self) -> Option<PathBuf> {
39        self.initramfs.as_ref().map(PathBuf::from)
40    }
41
42    pub(crate) fn validate(&self) -> Result<()> {
43        anyhow::ensure!(
44            self.initramfs
45                .as_ref()
46                .is_none_or(|path| !path.trim().is_empty()),
47            "initramfs path cannot be empty"
48        );
49        anyhow::ensure!(
50            self.cmdline
51                .as_ref()
52                .is_none_or(|cmdline| valid_host_cmdline(cmdline)),
53            "kernel command line must be at most 4095 printable ASCII bytes"
54        );
55        Ok(())
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use super::BootPayloadConfig;
62
63    #[test]
64    fn serializes_all_optional_payload_combinations_without_null_fields() {
65        for (config, expected) in [
66            (BootPayloadConfig::default(), serde_json::json!({})),
67            (
68                BootPayloadConfig {
69                    cmdline: Some("console=ttyS0".into()),
70                    initramfs: None,
71                },
72                serde_json::json!({"cmdline": "console=ttyS0"}),
73            ),
74            (
75                BootPayloadConfig {
76                    cmdline: None,
77                    initramfs: Some("host.cpio".into()),
78                },
79                serde_json::json!({"initramfs": "host.cpio"}),
80            ),
81            (
82                BootPayloadConfig {
83                    cmdline: Some("console=ttyS0".into()),
84                    initramfs: Some("host.cpio".into()),
85                },
86                serde_json::json!({
87                    "cmdline": "console=ttyS0",
88                    "initramfs": "host.cpio",
89                }),
90            ),
91        ] {
92            let encoded = serde_json::to_value(&config).unwrap();
93            assert_eq!(encoded, expected);
94            assert_eq!(
95                serde_json::from_value::<BootPayloadConfig>(encoded).unwrap(),
96                config
97            );
98        }
99    }
100
101    #[test]
102    fn rejects_control_bytes_and_oversize() {
103        for value in [
104            "console=ttyS0\nreset".to_string(),
105            "console=ttyS0\x1b".to_string(),
106            "x".repeat(4096),
107        ] {
108            assert!(
109                BootPayloadConfig {
110                    cmdline: Some(value),
111                    ..Default::default()
112                }
113                .validate()
114                .is_err()
115            );
116        }
117        assert!(
118            BootPayloadConfig {
119                cmdline: Some("rdinit=\"/sbin/my init\" -- arg".into()),
120                ..Default::default()
121            }
122            .validate()
123            .is_ok()
124        );
125        assert!(
126            BootPayloadConfig {
127                cmdline: Some("value='literal'".into()),
128                ..Default::default()
129            }
130            .validate()
131            .is_ok()
132        );
133        assert!(
134            BootPayloadConfig {
135                cmdline: Some(String::new()),
136                ..Default::default()
137            }
138            .validate()
139            .is_ok()
140        );
141    }
142}