tawasuyu

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1572Shell 203Rust 109Markdown 993728 Sutichaykuna

⎇ ba4d520c44ef442f0c2cb29c3fb578fd9e5e3b7d crates/hammer-bootstrap/src/manifest.rs Rust · 189 siq'ikuna · 7.3 KB Ñawpa willay Pi imata llamkarqan ch'uya

Bootstrap: manifiesto versionado (bootstrap.json) + Stage 0 anota su línea ba4d520c4 · Sergio ·

1//! Manifiesto de bootstrap — el embrión del log de transparencia ([SDD 11 §4](../../docs/11-bootstrap.md)).
2//!
3//! Cada etapa del bootstrap anota una línea inmutable `(stage, recipe_hash, artifact_hash,
4//! seed_hash, ts)` en un `bootstrap.json` versionado en la raíz del store. Un tercero reproduce
5//! el bootstrap y verifica que sus hashes coinciden con los publicados, sin confiar en el
6//! binario del autor ([SDD 09 §4](../../docs/09-trust-model.md)).
7//!
8//! El manifiesto es **append-only e idempotente**: re-correr una etapa (Stage 0 es idempotente
9//! por diseño) no duplica su línea. El `ts` es puramente informativo y no entra en ningún hash.
10
11use std::path::PathBuf;
12
13use hammer_core::{ArtifactHash, Store};
14use serde::{Deserialize, Serialize};
15
16use crate::{Error, Result};
17
18/// Versión del formato del manifiesto. Subir si cambia el esquema de [`StageEntry`].
19pub const MANIFEST_VERSION: u32 = 1;
20
21/// Nombre del archivo del manifiesto, en la raíz del store. Los directorios del store llevan
22/// nombre `<hash>-<name>`, así que un `bootstrap.json` plano nunca colisiona con un artefacto.
23pub const MANIFEST_FILE: &str = "bootstrap.json";
24
25/// Una línea del manifiesto: el registro inmutable de lo que produjo una etapa del bootstrap.
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27pub struct StageEntry {
28    /// 0 = semilla ingerida, 1 = userland mínimo, 2 = rebuild nativo.
29    pub stage: u8,
30    /// Hash de la receta que produjo el artefacto. `None` en Stage 0: la semilla se **ingiere**
31    /// (fuente fijada por sha256), no se compila desde una receta.
32    #[serde(default, skip_serializing_if = "Option::is_none")]
33    pub recipe_hash: Option<ArtifactHash>,
34    /// Artefacto que produjo la etapa (la semilla en Stage 0, el rootfs sellado en Stage 1).
35    pub artifact_hash: ArtifactHash,
36    /// Semilla de la que depende la etapa. En Stage 0 coincide con `artifact_hash` (la semilla
37    /// es a la vez insumo y producto).
38    #[serde(default, skip_serializing_if = "Option::is_none")]
39    pub seed_hash: Option<ArtifactHash>,
40    /// Segundos Unix en que se anotó la línea. Solo informativo: **no** entra en ningún hash, de
41    /// modo que dos reproducciones del mismo bootstrap difieren a lo sumo en este campo.
42    pub ts: u64,
43}
44
45/// El manifiesto completo: una secuencia ordenada de líneas, una por etapa ejecutada.
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
47pub struct BootstrapManifest {
48    pub version: u32,
49    #[serde(default)]
50    pub entries: Vec<StageEntry>,
51}
52
53impl Default for BootstrapManifest {
54    fn default() -> Self {
55        Self { version: MANIFEST_VERSION, entries: Vec::new() }
56    }
57}
58
59impl BootstrapManifest {
60    /// Ruta del manifiesto para un store dado.
61    pub fn path_for(store: &Store) -> PathBuf {
62        store.root().join(MANIFEST_FILE)
63    }
64
65    /// Carga el manifiesto del store; devuelve uno vacío si aún no existe (el caso normal antes
66    /// de la primera etapa).
67    pub fn load(store: &Store) -> Result<Self> {
68        let path = Self::path_for(store);
69        match std::fs::read(&path) {
70            Ok(bytes) => serde_json::from_slice(&bytes)
71                .map_err(|e| Error::Other(format!("bootstrap.json corrupto: {e}"))),
72            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Self::default()),
73            Err(e) => Err(Error::Io(e)),
74        }
75    }
76
77    /// Inserta una línea si no estaba ya (de-dup por `(stage, artifact_hash)`), preservando el
78    /// orden de inserción. Devuelve `true` si añadió algo nuevo. Idempotente: re-correr una
79    /// etapa que produjo el mismo artefacto no duplica su línea.
80    pub fn record(&mut self, entry: StageEntry) -> bool {
81        let dup = self
82            .entries
83            .iter()
84            .any(|e| e.stage == entry.stage && e.artifact_hash == entry.artifact_hash);
85        if dup {
86            return false;
87        }
88        self.entries.push(entry);
89        true
90    }
91
92    /// Persiste el manifiesto al store de forma atómica (write a `.tmp` + rename), para que un
93    /// fallo a media escritura no deje un `bootstrap.json` truncado.
94    pub fn save(&self, store: &Store) -> Result<()> {
95        let path = Self::path_for(store);
96        let tmp = path.with_extension("json.tmp");
97        let json = serde_json::to_vec_pretty(self)
98            .map_err(|e| Error::Other(format!("serializar bootstrap.json: {e}")))?;
99        std::fs::write(&tmp, &json)?;
100        std::fs::rename(&tmp, &path)?;
101        Ok(())
102    }
103}
104
105/// Anota una línea en el manifiesto del store (load → record → save), de forma idempotente: si
106/// la línea ya estaba no reescribe el archivo. Lo usan las etapas tras sellar su artefacto.
107pub(crate) fn append_line(store: &Store, entry: StageEntry) -> Result<()> {
108    let mut m = BootstrapManifest::load(store)?;
109    if m.record(entry) {
110        m.save(store)?;
111    }
112    Ok(())
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    fn h(hex: &str) -> ArtifactHash {
120        ArtifactHash::from_hex(hex)
121    }
122
123    fn entry(stage: u8, artifact: &str, ts: u64) -> StageEntry {
124        StageEntry {
125            stage,
126            recipe_hash: None,
127            artifact_hash: h(artifact),
128            seed_hash: Some(h(artifact)),
129            ts,
130        }
131    }
132
133    #[test]
134    fn load_missing_returns_empty_versioned() {
135        let tmp = tempfile::tempdir().unwrap();
136        let store = Store::open(tmp.path().join("store")).unwrap();
137        let m = BootstrapManifest::load(&store).unwrap();
138        assert_eq!(m.version, MANIFEST_VERSION);
139        assert!(m.entries.is_empty());
140    }
141
142    #[test]
143    fn record_dedups_by_stage_and_artifact() {
144        let mut m = BootstrapManifest::default();
145        assert!(m.record(entry(0, "aaaa", 1)));
146        assert!(!m.record(entry(0, "aaaa", 999)), "mismo (stage, artifact) ⇒ no re-anota");
147        assert_eq!(m.entries.len(), 1);
148        // El ts del intento duplicado se descarta: gana la primera línea.
149        assert_eq!(m.entries[0].ts, 1);
150
151        // Mismo artefacto pero otra etapa sí es una línea distinta.
152        assert!(m.record(entry(1, "aaaa", 2)));
153        assert_eq!(m.entries.len(), 2);
154    }
155
156    #[test]
157    fn record_preserves_insertion_order() {
158        let mut m = BootstrapManifest::default();
159        m.record(entry(0, "aaaa", 1));
160        m.record(entry(1, "bbbb", 2));
161        m.record(entry(2, "cccc", 3));
162        let stages: Vec<u8> = m.entries.iter().map(|e| e.stage).collect();
163        assert_eq!(stages, vec![0, 1, 2]);
164    }
165
166    #[test]
167    fn save_then_load_roundtrips() {
168        let tmp = tempfile::tempdir().unwrap();
169        let store = Store::open(tmp.path().join("store")).unwrap();
170        let mut m = BootstrapManifest::default();
171        m.record(entry(0, "aaaa", 10));
172        m.record(entry(1, "bbbb", 20));
173        m.save(&store).unwrap();
174
175        let loaded = BootstrapManifest::load(&store).unwrap();
176        assert_eq!(loaded, m);
177    }
178
179    #[test]
180    fn append_line_is_idempotent_on_disk() {
181        let tmp = tempfile::tempdir().unwrap();
182        let store = Store::open(tmp.path().join("store")).unwrap();
183        append_line(&store, entry(0, "aaaa", 1)).unwrap();
184        append_line(&store, entry(0, "aaaa", 2)).unwrap(); // mismo (stage, artifact)
185        let m = BootstrapManifest::load(&store).unwrap();
186        assert_eq!(m.entries.len(), 1, "la segunda anotación no debe duplicar");
187        assert_eq!(m.entries[0].ts, 1, "gana la primera línea");
188    }
189}