//! El manifiesto `.swm` (Software Mutación). Ver `docs/06-swm-format.md`. //! //! Es la unidad de intercambio: receta de transformación sobre fuente pública + ediciones de //! config. NUNCA transporta binarios cocidos (salvo `FileDrop` con hash declarado). use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Swm { pub swm_version: u32, pub base: Base, pub mutations: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] pub signature: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Base { pub distro_version: String, #[serde(default)] pub pins: std::collections::BTreeMap, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum Mutation { SourcePatch { repo: String, commit: String, #[serde(default, skip_serializing_if = "Option::is_none")] patch: Option, #[serde(default, skip_serializing_if = "Option::is_none")] patch_url: Option, build: SwmBuild, target_bin: String, #[serde(default, skip_serializing_if = "Option::is_none")] expected_hash: Option, }, ConfigEdit { file: String, inline_diff: String, }, InitRule { action: String, service: String, command: String, }, FileDrop { path: String, /// hash BLAKE3 del contenido, declarado para verificación. content_hash: String, #[serde(default, skip_serializing_if = "Option::is_none")] content_url: Option, }, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SwmBuild { #[serde(default = "default_compiler")] pub compiler: String, #[serde(default = "default_target")] pub target: String, #[serde(default = "default_link")] pub link: String, #[serde(default)] pub flags: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Signature { pub by: String, pub alg: String, pub sig: String, } fn default_compiler() -> String { "zig-cc".into() } fn default_target() -> String { "x86_64-linux-musl".into() } fn default_link() -> String { "static".into() } impl Swm { pub fn from_yaml(s: &str) -> crate::Result { serde_yaml::from_str(s).map_err(|e| crate::Error::Serde(e.to_string())) } pub fn to_yaml(&self) -> crate::Result { serde_yaml::to_string(self).map_err(|e| crate::Error::Serde(e.to_string())) } } #[cfg(test)] mod tests { use super::*; #[test] fn roundtrip_yaml() { let yaml = r#" swm_version: 1 base: distro_version: "2026-06-06" pins: grep: "a1b2c3d" mutations: - type: config_edit file: "/etc/network.conf" inline_diff: | - DHCP=yes + IP=192.168.1.100 "#; let swm = Swm::from_yaml(yaml).expect("parse"); assert_eq!(swm.swm_version, 1); assert_eq!(swm.mutations.len(), 1); let back = swm.to_yaml().expect("serialize"); assert!(back.contains("config_edit")); } }