//! Direccionamiento por contenido (CAS). Ver `docs/02-build-lab.md` §2. //! //! El `ArtifactHash` identifica un artefacto por TODO lo que influye en su salida: //! commit fuente + parches + flags/compilador/target + hashes de dependencias. use serde::{Deserialize, Serialize}; /// Hash BLAKE3 de un artefacto, con prefijo legible `b3:`. #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct ArtifactHash(String); impl ArtifactHash { /// Construye desde bytes ya hasheados (representación hex). pub fn from_hex(hex: impl Into) -> Self { ArtifactHash(format!("b3:{}", hex.into())) } /// Hashea un conjunto ordenado de entradas. El llamador es responsable de pasar las /// entradas en orden canónico y estable (ver `docs/02-build-lab.md` §2). pub fn of_inputs(inputs: &[&[u8]]) -> Self { let mut hasher = blake3::Hasher::new(); for chunk in inputs { // length-prefijado para evitar colisiones por concatenación ambigua. hasher.update(&(chunk.len() as u64).to_le_bytes()); hasher.update(chunk); } ArtifactHash(format!("b3:{}", hasher.finalize().to_hex())) } /// Forma corta para directorios del store: `-`. pub fn store_dir_name(&self, name: &str) -> String { let bare = self.0.strip_prefix("b3:").unwrap_or(&self.0); format!("{bare}-{name}") } pub fn as_str(&self) -> &str { &self.0 } } impl std::fmt::Display for ArtifactHash { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(&self.0) } } #[cfg(test)] mod tests { use super::*; #[test] fn deterministic_and_order_sensitive() { let a = ArtifactHash::of_inputs(&[b"grep", b"abc123", b"--static"]); let b = ArtifactHash::of_inputs(&[b"grep", b"abc123", b"--static"]); assert_eq!(a, b, "misma entrada ⇒ mismo hash"); let c = ArtifactHash::of_inputs(&[b"abc123", b"grep", b"--static"]); assert_ne!(a, c, "orden distinto ⇒ hash distinto"); } #[test] fn store_dir_name_strips_prefix() { let h = ArtifactHash::from_hex("deadbeef"); assert_eq!(h.store_dir_name("grep"), "deadbeef-grep"); } }