From 25f39c857da591c665865b77483b79495caf5e00 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Wed, 12 Aug 2026 00:02:26 +0000 Subject: Replace legacy profile parser with typed image spec --- src/model.rs | 142 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 src/model.rs (limited to 'src/model.rs') diff --git a/src/model.rs b/src/model.rs new file mode 100644 index 0000000..13fa5e8 --- /dev/null +++ b/src/model.rs @@ -0,0 +1,142 @@ +use std::collections::HashSet; +use std::fs; +use std::path::Path; + +use anyhow::{bail, Context, Result}; +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(()) + } +} -- cgit v1.2.3