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…
⎇ 1728a77a3ae6e102ec913322325c55892da95bc9
crates/takana-build/src/download.rs
Ñawpa willay
Pi imata llamkarqan
ch'uya
takana etapa 4: los 10 crates de librería y el CLI pasan a takana-*
1//! Descarga remota para `.swm`: `patch_url` (source_patch) y `content_url` (file_drop).
2//! Ver `docs/06-swm-format.md` §2. La red sólo se toca aquí y en `fetch` — nunca dentro del
3//! sandbox de build.
4//!
5//! Usa `curl` (ya presente en el rootfs del lab). Acepta cualquier esquema que curl entienda;
6//! en tests usamos `file://` para no depender de la red. La integridad la garantiza el
7//! llamador: el `file_drop` verifica BLAKE3 (`content_hash`) tras descargar; el `patch_url`
8//! se cubre indirectamente por el build reproducible + `expected_hash` del artefacto.
9
10use std::path::Path;
11use std::process::Command;
12
13/// Descarga `url` y devuelve su contenido en memoria. Falla con el stderr de curl si el
14/// código de salida no es 0 (incluye errores HTTP gracias a `-f`).
15pub fn fetch_url_bytes(url: &str) -> takana_core::Result<Vec<u8>> {
16 let out = Command::new("curl")
17 .args([
18 "-fsSL",
19 "--retry",
20 "2",
21 "--connect-timeout",
22 "30",
23 "--max-time",
24 "300",
25 url,
26 ])
27 .output()
28 .map_err(|e| takana_core::Error::Other(anyhow::anyhow!("spawn curl: {e}")))?;
29 if !out.status.success() {
30 return Err(takana_core::Error::Other(anyhow::anyhow!(
31 "curl {url} falló (exit {}): {}",
32 out.status.code().map(|c| c.to_string()).unwrap_or_else(|| "?".into()),
33 String::from_utf8_lossy(&out.stderr).trim()
34 )));
35 }
36 Ok(out.stdout)
37}
38
39/// Descarga `url` y lo escribe en `dst` (creando los directorios padre).
40pub fn fetch_url_to_file(url: &str, dst: &Path) -> takana_core::Result<()> {
41 let bytes = fetch_url_bytes(url)?;
42 if let Some(parent) = dst.parent() {
43 std::fs::create_dir_all(parent)?;
44 }
45 std::fs::write(dst, &bytes)?;
46 Ok(())
47}
48
49/// Verifica que el sha256 de `path` coincide con `expected` (hex, case-insensitive). Lo usan
50/// los consumidores que ingieren una fuente fijada (p. ej. el toolchain semilla del bootstrap,
51/// [SDD 11](../../docs/11-bootstrap.md)) y necesitan comprobar integridad **antes** de sellar.
52pub fn verify_sha256(path: &Path, expected: &str) -> takana_core::Result<()> {
53 use sha2::{Digest, Sha256};
54 let mut f = std::fs::File::open(path)?;
55 let mut hasher = Sha256::new();
56 std::io::copy(&mut f, &mut hasher)?;
57 let got = hex::encode(hasher.finalize());
58 if !got.eq_ignore_ascii_case(expected) {
59 return Err(takana_core::Error::Other(anyhow::anyhow!(
60 "sha256 mismatch en {}: esperado {expected}, obtenido {got}",
61 path.display()
62 )));
63 }
64 Ok(())
65}
66
67#[cfg(test)]
68mod tests {
69 use super::*;
70
71 #[test]
72 fn fetch_file_url_roundtrip() {
73 let d = tempfile::tempdir().unwrap();
74 let src = d.path().join("payload.txt");
75 std::fs::write(&src, b"contenido remoto\n").unwrap();
76 let url = format!("file://{}", src.display());
77 let got = fetch_url_bytes(&url).unwrap();
78 assert_eq!(got, b"contenido remoto\n");
79 }
80
81 #[test]
82 fn fetch_to_file_writes_and_creates_parents() {
83 let d = tempfile::tempdir().unwrap();
84 let src = d.path().join("p.txt");
85 std::fs::write(&src, b"abc").unwrap();
86 let dst = d.path().join("nested/dir/out.txt");
87 fetch_url_to_file(&format!("file://{}", src.display()), &dst).unwrap();
88 assert_eq!(std::fs::read(&dst).unwrap(), b"abc");
89 }
90
91 #[test]
92 fn missing_url_is_error() {
93 let err = fetch_url_bytes("file:///no/existe/aqui.bin").unwrap_err().to_string();
94 assert!(err.contains("curl"), "{err}");
95 }
96}