use std::path::PathBuf;
use anyhow::Result;
use httpboot_protocol::valid_host_cmdline;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::project::variables::{self, VariableScope};
#[derive(Clone, Debug, Default, Deserialize, Serialize, JsonSchema, PartialEq, Eq)]
pub struct BootPayloadConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub initramfs: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cmdline: Option<String>,
}
impl BootPayloadConfig {
pub(crate) fn replace_strings(&mut self, scope: &VariableScope) -> Result<()> {
self.initramfs = self
.initramfs
.as_deref()
.map(|path| variables::expand_variables(path, scope))
.transpose()?;
self.cmdline = self
.cmdline
.as_deref()
.map(|value| variables::expand_variables(value, scope))
.transpose()?;
Ok(())
}
pub(crate) fn initramfs_path(&self) -> Option<PathBuf> {
self.initramfs.as_ref().map(PathBuf::from)
}
pub(crate) fn validate(&self) -> Result<()> {
anyhow::ensure!(
self.initramfs
.as_ref()
.is_none_or(|path| !path.trim().is_empty()),
"initramfs path cannot be empty"
);
anyhow::ensure!(
self.cmdline
.as_ref()
.is_none_or(|cmdline| valid_host_cmdline(cmdline)),
"kernel command line must be at most 4095 printable ASCII bytes"
);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::BootPayloadConfig;
#[test]
fn serializes_all_optional_payload_combinations_without_null_fields() {
for (config, expected) in [
(BootPayloadConfig::default(), serde_json::json!({})),
(
BootPayloadConfig {
cmdline: Some("console=ttyS0".into()),
initramfs: None,
},
serde_json::json!({"cmdline": "console=ttyS0"}),
),
(
BootPayloadConfig {
cmdline: None,
initramfs: Some("host.cpio".into()),
},
serde_json::json!({"initramfs": "host.cpio"}),
),
(
BootPayloadConfig {
cmdline: Some("console=ttyS0".into()),
initramfs: Some("host.cpio".into()),
},
serde_json::json!({
"cmdline": "console=ttyS0",
"initramfs": "host.cpio",
}),
),
] {
let encoded = serde_json::to_value(&config).unwrap();
assert_eq!(encoded, expected);
assert_eq!(
serde_json::from_value::<BootPayloadConfig>(encoded).unwrap(),
config
);
}
}
#[test]
fn rejects_control_bytes_and_oversize() {
for value in [
"console=ttyS0\nreset".to_string(),
"console=ttyS0\x1b".to_string(),
"x".repeat(4096),
] {
assert!(
BootPayloadConfig {
cmdline: Some(value),
..Default::default()
}
.validate()
.is_err()
);
}
assert!(
BootPayloadConfig {
cmdline: Some("rdinit=\"/sbin/my init\" -- arg".into()),
..Default::default()
}
.validate()
.is_ok()
);
assert!(
BootPayloadConfig {
cmdline: Some("value='literal'".into()),
..Default::default()
}
.validate()
.is_ok()
);
assert!(
BootPayloadConfig {
cmdline: Some(String::new()),
..Default::default()
}
.validate()
.is_ok()
);
}
}