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…
⎇ 84e3737e10e17927380ed3e232e50cebfdd39123
crates/takana-build/tests/hidratacion_no_escapa.rs
History
Who touched what
raw
takana etapa 5d: comentarios de crates, CLAUDE.md y el skill de granja
1//! La hidratación no escribe fuera de su root, aunque el FHS ya traiga un symlink que salga.
2//!
3//! EL FALLO (medido 2026-08-30, antes de `no_escapa`): hidratar es `target_fhs.join(rel)` y
4//! escribir por ruta. Un artefacto que trae `usr/share/pkg → /algún/lado` deja ese symlink puesto
5//! —los symlinks se replican literales, y así debe ser— y la hidratación SIGUIENTE escribía
6//! `usr/share/pkg/archivo` **fuera del root**, devolviendo `Ok(1)`. Con `takana hydrate --into`
7//! sobre una imagen, eso es escribir en el sistema anfitrión diciendo que todo fue bien.
8//!
9//! El corpus no lo disparaba (de 160 symlinks absolutos ninguno apunta a un directorio, y de
10//! ~42 000 relativos ninguno sale de su artefacto), así que esto es una mina desactivada, no un
11//! incendio apagado. Por eso el test: lo que no dispara nadie tampoco lo nota nadie.
12
13use std::path::Path;
14
15use takana_build::hydrate::hydrate;
16use takana_core::LinkMode;
17
18/// Artefacto que trae `usr/share/pkg` como symlink a `destino`.
19fn artefacto_con_symlink(dir: &Path, destino: &Path) {
20 std::fs::create_dir_all(dir.join("usr/share")).unwrap();
21 std::os::unix::fs::symlink(destino, dir.join("usr/share/pkg")).unwrap();
22}
23
24/// Artefacto que trae un fichero DEBAJO de esa misma ruta.
25fn artefacto_con_fichero(dir: &Path) {
26 std::fs::create_dir_all(dir.join("usr/share/pkg")).unwrap();
27 std::fs::write(dir.join("usr/share/pkg/archivo"), b"contenido").unwrap();
28}
29
30#[test]
31fn un_symlink_del_fhs_que_sale_del_root_no_deja_escribir() {
32 let fuera = tempfile::tempdir().unwrap();
33 let fhs = tempfile::tempdir().unwrap();
34 let a = tempfile::tempdir().unwrap();
35 let b = tempfile::tempdir().unwrap();
36
37 artefacto_con_symlink(a.path(), fuera.path());
38 hydrate(a.path(), fhs.path(), LinkMode::Static, None).unwrap();
39
40 artefacto_con_fichero(b.path());
41 let r = hydrate(b.path(), fhs.path(), LinkMode::Static, None);
42
43 assert!(r.is_err(), "hidratar a través del symlink tiene que FALLAR, no devolver Ok");
44 let msg = format!("{}", r.unwrap_err());
45 assert!(msg.contains("se saldría del root"), "el error tiene que decir qué pasó: {msg}");
46 assert!(
47 !fuera.path().join("archivo").exists(),
48 "ESCAPÓ: el fichero se escribió fuera del target_fhs"
49 );
50}
51
52/// El contrapeso: un symlink de directorio que se queda DENTRO tiene que seguir funcionando.
53/// Es el caso de usr-merge (`/lib → usr/lib`) y el de los temas de iconos (`22@2x → 22`), o sea
54/// la mayoría de los symlinks de directorio que hay en el corpus. Un guardián que los rompiera
55/// sería peor que el agujero que tapa.
56#[test]
57fn un_symlink_que_se_queda_dentro_sigue_hidratando() {
58 let fhs = tempfile::tempdir().unwrap();
59 let a = tempfile::tempdir().unwrap();
60 let b = tempfile::tempdir().unwrap();
61
62 // `usr/share/pkg → ../otro` (relativo y dentro), con el destino existiendo.
63 std::fs::create_dir_all(a.path().join("usr/otro")).unwrap();
64 std::fs::write(a.path().join("usr/otro/.marca"), b"x").unwrap();
65 std::fs::create_dir_all(a.path().join("usr/share")).unwrap();
66 std::os::unix::fs::symlink("../otro", a.path().join("usr/share/pkg")).unwrap();
67 hydrate(a.path(), fhs.path(), LinkMode::Static, None).unwrap();
68
69 artefacto_con_fichero(b.path());
70 let r = hydrate(b.path(), fhs.path(), LinkMode::Static, None);
71 assert!(r.is_ok(), "un symlink que no sale del root no puede bloquear la hidratación: {r:?}");
72 assert!(
73 fhs.path().join("usr/otro/archivo").exists(),
74 "el fichero tiene que aterrizar donde apunta el symlink, dentro del root"
75 );
76}