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 1534Shell 200Rust 109Markdown 993713 Definiciones

⎇ 98f0b2d228ffebb06941dfc1f436167ab40d8f21 crates/hammer-core/src/hash.rs Rust · 178 líneas · 7.3 KB Historia Quién tocó qué crudo

core: ArtifactHash::of_tree — content-hash determinista de un árbol (pre-Stage 2) 9676fff0d · Sergio ·

1//! Direccionamiento por contenido (CAS). Ver `docs/02-build-lab.md` §2.
2//!
3//! El `ArtifactHash` identifica un artefacto por TODO lo que influye en su salida:
4//! commit fuente + parches + flags/compilador/target + hashes de dependencias.
5
6use std::os::unix::ffi::OsStrExt;
7use std::os::unix::fs::PermissionsExt;
8use std::path::{Path, PathBuf};
9
10use serde::{Deserialize, Serialize};
11
12/// Hash BLAKE3 de un artefacto, con prefijo legible `b3:`.
13#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
14pub struct ArtifactHash(String);
15
16impl ArtifactHash {
17    /// Construye desde bytes ya hasheados (representación hex).
18    pub fn from_hex(hex: impl Into<String>) -> Self {
19        ArtifactHash(format!("b3:{}", hex.into()))
20    }
21
22    /// Hashea un conjunto ordenado de entradas. El llamador es responsable de pasar las
23    /// entradas en orden canónico y estable (ver `docs/02-build-lab.md` §2).
24    pub fn of_inputs(inputs: &[&[u8]]) -> Self {
25        let mut hasher = blake3::Hasher::new();
26        for chunk in inputs {
27            // length-prefijado para evitar colisiones por concatenación ambigua.
28            hasher.update(&(chunk.len() as u64).to_le_bytes());
29            hasher.update(chunk);
30        }
31        ArtifactHash(format!("b3:{}", hasher.finalize().to_hex()))
32    }
33
34    /// Forma corta para directorios del store: `<hash-sin-prefijo>-<name>`.
35    pub fn store_dir_name(&self, name: &str) -> String {
36        let bare = self.0.strip_prefix("b3:").unwrap_or(&self.0);
37        format!("{bare}-{name}")
38    }
39
40    pub fn as_str(&self) -> &str {
41        &self.0
42    }
43
44    /// Hash de **contenido** de un árbol de archivos: BLAKE3 determinista sobre los bytes reales
45    /// (rutas relativas ordenadas + tipo + bit de ejecución + contenido / target de symlink).
46    ///
47    /// A diferencia de [`of_inputs`](Self::of_inputs) (input-addressed: identifica por *qué*
48    /// produjo el artefacto — fuente, flags, deps), esto identifica por *qué bytes son*. Es lo que
49    /// **Stage 2** ([SDD 11](../../docs/11-bootstrap.md)) necesita para verificar
50    /// bit-reproducibilidad: `of_tree(stage1) == of_tree(stage1')` ⇒ el sistema se reconstruye
51    /// idéntico. No sigue symlinks (hashea su target literal); el orden del filesystem no afecta
52    /// (se ordena por ruta). El modo se reduce al bit de ejecución (lo único semánticamente
53    /// relevante; el resto lo fija `seal` de forma consistente).
54    pub fn of_tree(root: &Path) -> std::io::Result<ArtifactHash> {
55        let mut rels: Vec<PathBuf> = Vec::new();
56        collect_rel(root, Path::new(""), &mut rels)?;
57        rels.sort();
58
59        let mut hasher = blake3::Hasher::new();
60        hasher.update(b"hammer-tree-v1");
61        for rel in &rels {
62            let abs = root.join(rel);
63            let meta = std::fs::symlink_metadata(&abs)?;
64            let relb = rel.as_os_str().as_bytes();
65            hasher.update(&(relb.len() as u64).to_le_bytes());
66            hasher.update(relb);
67
68            let ft = meta.file_type();
69            if ft.is_symlink() {
70                let tgt = std::fs::read_link(&abs)?;
71                let t = tgt.as_os_str().as_bytes();
72                hasher.update(b"L");
73                hasher.update(&(t.len() as u64).to_le_bytes());
74                hasher.update(t);
75            } else if ft.is_dir() {
76                hasher.update(b"D");
77            } else {
78                // Archivo regular: bit de ejecución + contenido.
79                let exec = meta.permissions().mode() & 0o111 != 0;
80                hasher.update(if exec { b"Fx" } else { b"F0" });
81                let bytes = std::fs::read(&abs)?;
82                hasher.update(&(bytes.len() as u64).to_le_bytes());
83                hasher.update(&bytes);
84            }
85        }
86        Ok(ArtifactHash(format!("b3:{}", hasher.finalize().to_hex())))
87    }
88}
89
90/// Recorre `root` recursivamente acumulando rutas **relativas a `root`** en `out`. No sigue
91/// symlinks (los registra como entrada, sin descender).
92fn collect_rel(root: &Path, rel: &Path, out: &mut Vec<PathBuf>) -> std::io::Result<()> {
93    for entry in std::fs::read_dir(root.join(rel))? {
94        let entry = entry?;
95        let child = rel.join(entry.file_name());
96        let is_dir = entry.file_type()?.is_dir();
97        out.push(child.clone());
98        if is_dir {
99            collect_rel(root, &child, out)?;
100        }
101    }
102    Ok(())
103}
104
105impl std::fmt::Display for ArtifactHash {
106    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107        f.write_str(&self.0)
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn deterministic_and_order_sensitive() {
117        let a = ArtifactHash::of_inputs(&[b"grep", b"abc123", b"--static"]);
118        let b = ArtifactHash::of_inputs(&[b"grep", b"abc123", b"--static"]);
119        assert_eq!(a, b, "misma entrada ⇒ mismo hash");
120
121        let c = ArtifactHash::of_inputs(&[b"abc123", b"grep", b"--static"]);
122        assert_ne!(a, c, "orden distinto ⇒ hash distinto");
123    }
124
125    #[test]
126    fn store_dir_name_strips_prefix() {
127        let h = ArtifactHash::from_hex("deadbeef");
128        assert_eq!(h.store_dir_name("grep"), "deadbeef-grep");
129    }
130
131    // --- of_tree: content-hash determinista (pre-Stage 2) ---
132
133    fn populate(d: &std::path::Path) {
134        use std::os::unix::fs::{symlink, PermissionsExt};
135        std::fs::create_dir_all(d.join("usr/bin")).unwrap();
136        std::fs::write(d.join("usr/bin/hello"), b"#!/bin/sh\necho hi\n").unwrap();
137        std::fs::set_permissions(d.join("usr/bin/hello"), std::fs::Permissions::from_mode(0o755)).unwrap();
138        std::fs::write(d.join("README"), b"docs").unwrap();
139        symlink("usr/bin/hello", d.join("link")).unwrap();
140    }
141
142    #[test]
143    fn of_tree_is_deterministic_across_two_identical_trees() {
144        let a = tempfile::tempdir().unwrap();
145        let b = tempfile::tempdir().unwrap();
146        populate(a.path());
147        populate(b.path());
148        // Mismo contenido en dos dirs distintos ⇒ mismo hash (independiente de la ruta raíz).
149        assert_eq!(ArtifactHash::of_tree(a.path()).unwrap(), ArtifactHash::of_tree(b.path()).unwrap());
150        assert!(ArtifactHash::of_tree(a.path()).unwrap().as_str().starts_with("b3:"));
151    }
152
153    #[test]
154    fn of_tree_detects_content_exec_and_symlink_changes() {
155        let base = tempfile::tempdir().unwrap();
156        populate(base.path());
157        let h0 = ArtifactHash::of_tree(base.path()).unwrap();
158
159        // (1) contenido distinto.
160        let c = tempfile::tempdir().unwrap();
161        populate(c.path());
162        std::fs::write(c.path().join("README"), b"otra cosa").unwrap();
163        assert_ne!(h0, ArtifactHash::of_tree(c.path()).unwrap(), "el contenido entra al hash");
164
165        // (2) bit de ejecución distinto.
166        let e = tempfile::tempdir().unwrap();
167        populate(e.path());
168        std::fs::set_permissions(e.path().join("usr/bin/hello"), std::fs::Permissions::from_mode(0o644)).unwrap();
169        assert_ne!(h0, ArtifactHash::of_tree(e.path()).unwrap(), "el bit de ejecución entra al hash");
170
171        // (3) target de symlink distinto.
172        let s = tempfile::tempdir().unwrap();
173        populate(s.path());
174        std::fs::remove_file(s.path().join("link")).unwrap();
175        std::os::unix::fs::symlink("README", s.path().join("link")).unwrap();
176        assert_ne!(h0, ArtifactHash::of_tree(s.path()).unwrap(), "el target del symlink entra al hash");
177    }
178}