summaryrefslogtreecommitdiff
path: root/src/model.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/model.rs')
-rw-r--r--src/model.rs142
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(())
+ }
+}