Etapa F refinamientos: uninstall poda dirs vacíos + install --require-signed

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 <noreply@anthropic.com>
This commit is contained in:
2026-06-21 07:12:58 -04:00
co-authored by Claude Opus 4.8
parent 910db267ef
commit f94ef01139
2 changed files with 46 additions and 6 deletions
+44 -5
View File
@@ -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<PathBuf> = 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(())
}