From f94ef011392b6ffae1e94967abfd015be4299f0b Mon Sep 17 00:00:00 2001 From: sergio Date: Sun, 21 Jun 2026 07:12:58 -0400 Subject: [PATCH] =?UTF-8?q?Etapa=20F=20refinamientos:=20uninstall=20poda?= =?UTF-8?q?=20dirs=20vac=C3=ADos=20+=20`install=20--require-signed`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dos bordes ásperos de la paquetería: - uninstall ahora retira los directorios que quedaron VACÍOS por el borrado (rmdir de abajo arriba; remove_dir sólo borra dirs vacíos ⇒ se detiene solo al toparse con contenido de otro paquete). Antes dejaba /usr/bin, etc. huérfanos. - `install --require-signed`: modo estricto que ABORTA si el release no está firmado por una clave confiada (Unsigned o UnknownKey ⇒ error). No basta con que el .swm reproduzca: exige autoría verificada del catálogo. Default off (no rompe flujos sin firma). Validado E2E: uninstall bwrap poda 4 dirs; --require-signed aborta sin firma y procede con release trusted. 31 suites verde. Co-Authored-By: Claude Opus 4.8 --- crates/hammer-cli/src/main.rs | 49 +++++++++++++++++++++++++++++++---- docs/06-swm-format.md | 3 ++- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/crates/hammer-cli/src/main.rs b/crates/hammer-cli/src/main.rs index 478b6cd9..6deef097 100644 --- a/crates/hammer-cli/src/main.rs +++ b/crates/hammer-cli/src/main.rs @@ -230,6 +230,10 @@ enum Cmd { /// Base de datos de instalados donde registrar el paquete (para `uninstall`). #[arg(long, default_value = hammer_core::installed::DEFAULT_DB)] db: PathBuf, + /// Modo estricto: abortar si el release del repo no está firmado por una clave confiada + /// (no basta con que el `.swm` reproduzca — exige autoría del catálogo verificada). + #[arg(long)] + require_signed: bool, }, /// [Etapa F] Desinstala un paquete: borra los ficheros que registró (refcount: respeta los /// que otro paquete instalado también aporta) y lo quita de la DB de instalados. @@ -791,6 +795,7 @@ fn main() -> anyhow::Result<()> { trust, state_root, db, + require_signed, } => { run_install( &cli.store, @@ -802,6 +807,7 @@ fn main() -> anyhow::Result<()> { trust.as_deref(), state_root.as_deref(), &db, + require_signed, )?; } Cmd::Uninstall { name, db } => run_uninstall(&name, &db)?, @@ -1426,6 +1432,7 @@ fn run_install( trust: Option<&std::path::Path>, state_root: Option<&std::path::Path>, db_path: &std::path::Path, + require_signed: bool, ) -> anyhow::Result<()> { let source = RepoSource::parse(repo); let index = source.read_index()?; @@ -1439,12 +1446,20 @@ fn run_install( match index.verify_signature(&store) { hammer_core::SigStatus::Trusted { by } => eprintln!("release: trusted (by {by})"), hammer_core::SigStatus::UnknownKey { by } => { - eprintln!("release: unknown-key (by {by}) — autoría del catálogo no confiada") + eprintln!("release: unknown-key (by {by}) — autoría del catálogo no confiada"); + if require_signed { + anyhow::bail!("--require-signed: el release está firmado por '{by}', clave no confiada en {}", trust_dir.display()); + } } hammer_core::SigStatus::BadSig { by, reason } => { anyhow::bail!("release: BAD-SIG (by {by}): {reason} — el índice fue manipulado") } - hammer_core::SigStatus::Unsigned => eprintln!("release: sin firmar"), + hammer_core::SigStatus::Unsigned => { + eprintln!("release: sin firmar"); + if require_signed { + anyhow::bail!("--require-signed: el repo no tiene firma de release (firmalo con `hammer repo sign`)"); + } + } } } @@ -1592,6 +1607,7 @@ fn run_uninstall(name: &str, db_path: &std::path::Path) -> anyhow::Result<()> { }; let (mut removed, mut kept) = (0usize, 0usize); + let mut touched_dirs: Vec = Vec::new(); for f in &pkg.files { // Refcount: no borramos un fichero que otro paquete instalado también aporta. if idb.owned_by_others(name, f) { @@ -1599,17 +1615,40 @@ fn run_uninstall(name: &str, db_path: &std::path::Path) -> anyhow::Result<()> { continue; } match std::fs::remove_file(f) { - Ok(()) => removed += 1, + Ok(()) => { + removed += 1; + if let Some(parent) = std::path::Path::new(f).parent() { + touched_dirs.push(parent.to_path_buf()); + } + } Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} // ya no estaba Err(e) => eprintln!(" aviso: no pude borrar {f}: {e}"), } } + // Recoge los directorios que quedaron vacíos por el borrado (y sus padres, hacia arriba): + // `remove_dir` sólo borra un dir VACÍO ⇒ se detiene solo al toparse con uno que aún tiene + // contenido de otros paquetes. Procesamos de más profundo a menos para vaciar de abajo arriba. + let mut pruned = 0usize; + touched_dirs.sort_by_key(|d| std::cmp::Reverse(d.components().count())); + touched_dirs.dedup(); + for dir in touched_dirs { + let mut cur = dir; + while std::fs::remove_dir(&cur).is_ok() { + pruned += 1; + match cur.parent() { + Some(p) if !p.as_os_str().is_empty() => cur = p.to_path_buf(), + _ => break, + } + } + } + idb.remove(name); idb.save(db_path)?; eprintln!( - "desinstalado {name} {} — {removed} fichero(s) borrado(s){}", + "desinstalado {name} {} — {removed} fichero(s) borrado(s){}{}", pkg.version, - if kept > 0 { format!(", {kept} conservado(s) (los aporta otro paquete)") } else { String::new() } + if kept > 0 { format!(", {kept} conservado(s) (los aporta otro paquete)") } else { String::new() }, + if pruned > 0 { format!(", {pruned} dir(s) vacío(s) retirado(s)") } else { String::new() }, ); Ok(()) } diff --git a/docs/06-swm-format.md b/docs/06-swm-format.md index 2d8f1741..78159c01 100644 --- a/docs/06-swm-format.md +++ b/docs/06-swm-format.md @@ -145,7 +145,8 @@ CLI: un binario ajeno. Registra el paquete en la DB de instalados (`--db`). `--repo` admite un directorio local **o una URL HTTP(S)** (el repo son ficheros estáticos: `index.json` + los `.swm`; con URL baja el índice + el cierre de deps a un temporal y procede igual). - `--prefix`/`--skip-source-patch` para staging y dry-run de schema. + `--prefix`/`--skip-source-patch` para staging y dry-run de schema. `--require-signed` exige que + el release esté firmado por una clave confiada (modo estricto: no basta con reproducir). - `hammer repo list [--repo DIR]` — lista el catálogo (`/index.json`). - `hammer repo sign --repo DIR --key KEY` — firma el ÍNDICE entero (release). Re-firmá tras publicar (cada `pack --repo` invalida la firma del release).