1use 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#[derive(Clone, Debug, Default, Deserialize, Serialize, JsonSchema, PartialEq, Eq)]
14pub struct BootPayloadConfig {
15 #[serde(default, skip_serializing_if = "Option::is_none")]
17 pub initramfs: Option<String>,
18 #[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}