use std::collections::HashSet; use std::fs; use std::path::Path; use anyhow::{Context, Result, bail}; use serde::Deserialize; #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] pub struct ImageSpec { pub target: Target, pub kernel: Kernel, pub boot: BootSpec, pub packages: PackageSpec, } #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] pub struct Target { pub name: String, pub architecture: Architecture, pub format: OutputFormat, } #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] pub struct Kernel { pub flavour: String, } #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] pub struct BootSpec { pub initrd_features: Vec, pub initrd_modules: Vec, } #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] pub struct PackageSpec { pub base: Vec, pub selectors: Vec, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct PackageSelector(String); impl PackageSelector { pub fn as_str(&self) -> &str { &self.0 } } impl<'de> Deserialize<'de> for PackageSelector { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let value = String::deserialize(deserializer)?; if value.trim().is_empty() { return Err(serde::de::Error::custom("package selector cannot be empty")); } Ok(Self(value)) } } #[derive(Debug, Clone, PartialEq, Eq)] pub enum Architecture { X86_64, } impl Architecture { pub fn as_str(&self) -> &'static str { match self { Self::X86_64 => "x86_64", } } } impl<'de> Deserialize<'de> for Architecture { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { match String::deserialize(deserializer)?.as_str() { "x86_64" => Ok(Self::X86_64), value => Err(serde::de::Error::custom(format!( "unsupported architecture: {value}" ))), } } } #[derive(Debug, Clone, PartialEq, Eq)] pub enum OutputFormat { Tar, } impl<'de> Deserialize<'de> for OutputFormat { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { match String::deserialize(deserializer)?.as_str() { "tar" => Ok(Self::Tar), value => Err(serde::de::Error::custom(format!( "unsupported output format: {value}" ))), } } } impl ImageSpec { pub fn load(path: &Path) -> Result { let contents = fs::read_to_string(path) .with_context(|| format!("read image specification {}", path.display()))?; let spec: Self = toml::from_str(&contents) .with_context(|| format!("parse image specification {}", path.display()))?; spec.validate()?; Ok(spec) } pub fn validate(&self) -> Result<()> { if self.target.name.trim().is_empty() { bail!("target name cannot be empty"); } let mut selectors = HashSet::new(); for selector in &self.packages.selectors { if !selectors.insert(selector.as_str()) { bail!("duplicate package selector: {}", selector.as_str()); } } for module in &self.boot.initrd_modules { if module.is_empty() || module.contains('/') || !module.chars().all(|character| { character.is_ascii_alphanumeric() || matches!(character, '.' | '_' | '-') }) { bail!("invalid initrd module: {module}"); } } Ok(()) } }