//! Descarga remota para `.swm`: `patch_url` (source_patch) y `content_url` (file_drop). //! Ver `docs/06-swm-format.md` §2. La red sólo se toca aquí y en `fetch` — nunca dentro del //! sandbox de build. //! //! Usa `curl` (ya presente en el rootfs del lab). Acepta cualquier esquema que curl entienda; //! en tests usamos `file://` para no depender de la red. La integridad la garantiza el //! llamador: el `file_drop` verifica BLAKE3 (`content_hash`) tras descargar; el `patch_url` //! se cubre indirectamente por el build reproducible + `expected_hash` del artefacto. use std::path::Path; use std::process::Command; /// Descarga `url` y devuelve su contenido en memoria. Falla con el stderr de curl si el /// código de salida no es 0 (incluye errores HTTP gracias a `-f`). pub fn fetch_url_bytes(url: &str) -> hammer_core::Result> { let out = Command::new("curl") .args([ "-fsSL", "--retry", "2", "--connect-timeout", "30", "--max-time", "300", url, ]) .output() .map_err(|e| hammer_core::Error::Other(anyhow::anyhow!("spawn curl: {e}")))?; if !out.status.success() { return Err(hammer_core::Error::Other(anyhow::anyhow!( "curl {url} falló (exit {}): {}", out.status.code().map(|c| c.to_string()).unwrap_or_else(|| "?".into()), String::from_utf8_lossy(&out.stderr).trim() ))); } Ok(out.stdout) } /// Descarga `url` y lo escribe en `dst` (creando los directorios padre). pub fn fetch_url_to_file(url: &str, dst: &Path) -> hammer_core::Result<()> { let bytes = fetch_url_bytes(url)?; if let Some(parent) = dst.parent() { std::fs::create_dir_all(parent)?; } std::fs::write(dst, &bytes)?; Ok(()) } /// Verifica que el sha256 de `path` coincide con `expected` (hex, case-insensitive). Lo usan /// los consumidores que ingieren una fuente fijada (p. ej. el toolchain semilla del bootstrap, /// [SDD 11](../../docs/11-bootstrap.md)) y necesitan comprobar integridad **antes** de sellar. pub fn verify_sha256(path: &Path, expected: &str) -> hammer_core::Result<()> { use sha2::{Digest, Sha256}; let mut f = std::fs::File::open(path)?; let mut hasher = Sha256::new(); std::io::copy(&mut f, &mut hasher)?; let got = hex::encode(hasher.finalize()); if !got.eq_ignore_ascii_case(expected) { return Err(hammer_core::Error::Other(anyhow::anyhow!( "sha256 mismatch en {}: esperado {expected}, obtenido {got}", path.display() ))); } Ok(()) } #[cfg(test)] mod tests { use super::*; #[test] fn fetch_file_url_roundtrip() { let d = tempfile::tempdir().unwrap(); let src = d.path().join("payload.txt"); std::fs::write(&src, b"contenido remoto\n").unwrap(); let url = format!("file://{}", src.display()); let got = fetch_url_bytes(&url).unwrap(); assert_eq!(got, b"contenido remoto\n"); } #[test] fn fetch_to_file_writes_and_creates_parents() { let d = tempfile::tempdir().unwrap(); let src = d.path().join("p.txt"); std::fs::write(&src, b"abc").unwrap(); let dst = d.path().join("nested/dir/out.txt"); fetch_url_to_file(&format!("file://{}", src.display()), &dst).unwrap(); assert_eq!(std::fs::read(&dst).unwrap(), b"abc"); } #[test] fn missing_url_is_error() { let err = fetch_url_bytes("file:///no/existe/aqui.bin").unwrap_err().to_string(); assert!(err.contains("curl"), "{err}"); } }