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…
⎇ 0a4b3abb461b5e9b6a68cbb0508d2eb7899acae3
crates/takana-build/tests/fetch_tarball.rs
History
Who touched what
raw
1//! Tests del modo tarball del fetch — completamente locales, sin red ni rootfs.
2//!
3//! Estrategia:
4//! - Genero un tarball en un tempdir con contenido conocido.
5//! - Calculo su sha256 real con la misma crate (`sha2`) que usa el fetch.
6//! - Sirvo el tarball vía URL `file://` (curl la maneja nativamente).
7//! - Llamo a `takana_build::fetch::fetch` con una receta TOML que apunta a esa URL.
8//!
9//! Cubre: happy path con strip_components, mismatch de sha256 no envenena la caché, reuso
10//! de caché en segunda llamada, strip_components = 0 vs 1.
11
12use std::path::{Path, PathBuf};
13use std::process::Command;
14
15use takana_build::fetch::fetch;
16use takana_core::Recipe;
17use sha2::{Digest, Sha256};
18
19/// Crea `<dir>/inner/{a.txt,sub/b.txt}` y empaqueta `inner/` en `<dir>/payload.tar.gz`.
20/// El tarball tiene UN componente top-level (`inner/`) — strip_components=1 lo elimina.
21fn make_tarball(dir: &Path) -> PathBuf {
22 let inner = dir.join("inner");
23 std::fs::create_dir_all(inner.join("sub")).unwrap();
24 std::fs::write(inner.join("a.txt"), b"hola desde a\n").unwrap();
25 std::fs::write(inner.join("sub/b.txt"), b"hola desde b\n").unwrap();
26
27 let tar_path = dir.join("payload.tar.gz");
28 let st = Command::new("tar")
29 .arg("-czf")
30 .arg(&tar_path)
31 .arg("-C")
32 .arg(dir)
33 .arg("inner")
34 .status()
35 .expect("spawn tar");
36 assert!(st.success(), "tar -czf falló");
37 tar_path
38}
39
40fn sha256_hex(path: &Path) -> String {
41 let mut f = std::fs::File::open(path).unwrap();
42 let mut hasher = Sha256::new();
43 std::io::copy(&mut f, &mut hasher).unwrap();
44 hex::encode(hasher.finalize())
45}
46
47fn recipe_for(url: &str, sha: &str, strip: Option<usize>) -> Recipe {
48 let strip_line = match strip {
49 Some(n) => format!("strip_components = {n}\n"),
50 None => String::new(),
51 };
52 let toml = format!(
53 r#"
54name = "fixture"
55version = "0"
56[source]
57tarball = "{url}"
58sha256 = "{sha}"
59{strip_line}
60[build]
61"#
62 );
63 Recipe::from_toml(&toml).unwrap()
64}
65
66#[test]
67fn happy_path_strip_components_default_one() {
68 let fix = tempfile::tempdir().unwrap();
69 let tar_path = make_tarball(fix.path());
70 let sha = sha256_hex(&tar_path);
71 let url = format!("file://{}", tar_path.display());
72
73 let recipe = recipe_for(&url, &sha, None);
74 let work_root = tempfile::tempdir().unwrap();
75
76 let (tree, _g) = fetch(&recipe, work_root.path()).expect("fetch tarball");
77 // strip_components=1 ⇒ no debe quedar `inner/`, los archivos suben un nivel.
78 assert!(tree.join("a.txt").is_file(), "a.txt debe existir en la raíz del tree");
79 assert!(tree.join("sub/b.txt").is_file(), "sub/b.txt debe existir");
80 assert!(!tree.join("inner").exists(), "inner/ debe haber sido stripped");
81
82 // El tarball quedó cacheado por su sha256.
83 let cached = work_root.path().join("tarballs").join(format!("{sha}.tar"));
84 assert!(cached.is_file(), "tarball debe quedar cacheado en {}", cached.display());
85}
86
87#[test]
88fn strip_components_zero_preserves_top_level() {
89 let fix = tempfile::tempdir().unwrap();
90 let tar_path = make_tarball(fix.path());
91 let sha = sha256_hex(&tar_path);
92 let url = format!("file://{}", tar_path.display());
93
94 let recipe = recipe_for(&url, &sha, Some(0));
95 let work_root = tempfile::tempdir().unwrap();
96 let (tree, _g) = fetch(&recipe, work_root.path()).expect("fetch tarball strip=0");
97 assert!(tree.join("inner/a.txt").is_file(), "con strip=0, inner/ debe existir");
98}
99
100#[test]
101fn sha256_mismatch_fails_and_does_not_poison_cache() {
102 let fix = tempfile::tempdir().unwrap();
103 let tar_path = make_tarball(fix.path());
104 let url = format!("file://{}", tar_path.display());
105 let wrong_sha = "0000000000000000000000000000000000000000000000000000000000000000";
106
107 let recipe = recipe_for(&url, wrong_sha, None);
108 let work_root = tempfile::tempdir().unwrap();
109 let err = fetch(&recipe, work_root.path()).unwrap_err().to_string();
110 assert!(err.contains("sha256 mismatch"), "msg = {err}");
111
112 // Crítico: la caché NO debe contener un archivo con el sha incorrecto, porque la próxima
113 // corrida lo daría por bueno sin re-verificar (sólo verificamos cuando lo descargamos).
114 let cached = work_root.path().join("tarballs").join(format!("{wrong_sha}.tar"));
115 assert!(!cached.exists(), "caché no debe contener {}", cached.display());
116 // Tampoco debe quedar el .partial colgado.
117 let partial = work_root
118 .path()
119 .join("tarballs")
120 .join(format!("{wrong_sha}.tar.partial"));
121 assert!(!partial.exists(), "no debe quedar partial");
122}
123
124#[test]
125fn second_fetch_reuses_cached_tarball() {
126 let fix = tempfile::tempdir().unwrap();
127 let tar_path = make_tarball(fix.path());
128 let sha = sha256_hex(&tar_path);
129 let url = format!("file://{}", tar_path.display());
130
131 let recipe = recipe_for(&url, &sha, None);
132 let work_root = tempfile::tempdir().unwrap();
133
134 let (_t1, g1) = fetch(&recipe, work_root.path()).expect("first fetch");
135 drop(g1); // el lock es por receta: soltarlo antes del segundo fetch, que es del mismo proceso
136
137 // Para forzar que la segunda corrida NO pueda descargar, "rompo" la fuente: muevo el
138 // tarball original a otro lado. Si el fetch va a la caché, sigue funcionando; si intenta
139 // re-descargar, falla.
140 std::fs::remove_file(&tar_path).unwrap();
141
142 let (tree2, _g2) = fetch(&recipe, work_root.path()).expect("second fetch (debe usar caché)");
143 assert!(tree2.join("a.txt").is_file());
144}
145
146#[test]
147fn cached_tarball_with_wrong_content_is_rejected() {
148 // Si un atacante o un bug deja en la caché un archivo con el nombre <sha>.tar pero
149 // contenido distinto, NO debemos confiar. Verificamos sha256 incluso para entradas
150 // de la caché.
151 let fix = tempfile::tempdir().unwrap();
152 let tar_path = make_tarball(fix.path());
153 let sha = sha256_hex(&tar_path);
154 let url = format!("file://{}", tar_path.display());
155
156 let work_root = tempfile::tempdir().unwrap();
157 let tarballs = work_root.path().join("tarballs");
158 std::fs::create_dir_all(&tarballs).unwrap();
159 // Plantar un archivo basura con el nombre del sha esperado.
160 std::fs::write(tarballs.join(format!("{sha}.tar")), b"contenido FALSO").unwrap();
161
162 let recipe = recipe_for(&url, &sha, None);
163 let err = fetch(&recipe, work_root.path()).unwrap_err().to_string();
164 assert!(err.contains("sha256 mismatch"), "msg = {err}");
165}