diff options
Diffstat (limited to 'src/model.rs')
| -rw-r--r-- | src/model.rs | 142 |
1 files changed, 142 insertions, 0 deletions
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<String>, + pub initrd_modules: Vec<String>, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +pub struct PackageSpec { + pub base: Vec<String>, + pub selectors: Vec<PackageSelector>, +} + +#[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<D>(deserializer: D) -> Result<Self, D::Error> + 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<D>(deserializer: D) -> Result<Self, D::Error> + 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<D>(deserializer: D) -> Result<Self, D::Error> + 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<Self> { + 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(()) + } +} |