tawasuyu

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…

TOML 1385Shell 122Rust 77Markdown 682241 Sutichaykuna

⎇ 8913eae26bb010a2de486dfd9f317c4921c6edc4 crates/hammer-core/src/installed.rs Rust · 142 siq'ikuna · 5.5 KB Ñawpa willay Pi imata llamkarqan ch'uya

Etapa F paquetería #5: DB de instalados + `hammer uninstall`/`installed` 9d1b2d4a7 · sergio ·

1//! La **base de datos de paquetes instalados** (Etapa F): qué paquetes hay puestos en un root y
2//! qué ficheros aportó cada uno. Es lo que hace posible `hammer uninstall`: sin un registro de
3//! "este paquete creó estos ficheros", quitar un paquete sería adivinar.
4//!
5//! Modelo simple y honesto: `install` registra los ficheros que CREÓ (los hidratados del
6//! `source_patch` + `file_drop` + `init_rule`; un `config_edit` MODIFICA un fichero ajeno, no lo
7//! crea, así que no se registra ni se deshace). `uninstall` borra los ficheros del paquete, salvo
8//! los que OTRO paquete instalado también aporta (refcount por ruta) — nunca rompe a un tercero.
9//!
10//! Las rutas se guardan ABSOLUTAS (bajo el prefix o `/` según donde se instaló), así `uninstall`
11//! no necesita saber el root: borra exactamente lo que registró el `install`.
12
13use std::path::Path;
14
15use serde::{Deserialize, Serialize};
16
17/// Nombre por defecto de la base de datos de instalados.
18pub const DEFAULT_DB: &str = "/var/lib/hammer/installed.json";
19
20#[derive(Debug, Clone, Default, Serialize, Deserialize)]
21pub struct InstalledDb {
22    #[serde(default)]
23    pub packages: Vec<InstalledPackage>,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27pub struct InstalledPackage {
28    pub name: String,
29    pub version: String,
30    /// `expected_hash` del paquete (si lo tenía) — sólo informativo en la DB.
31    #[serde(default, skip_serializing_if = "Option::is_none")]
32    pub hash: Option<String>,
33    /// Rutas ABSOLUTAS que este paquete creó (orden de instalación). Lo que `uninstall` borra.
34    pub files: Vec<String>,
35}
36
37impl InstalledDb {
38    /// Carga la DB. Un fichero ausente es una DB VACÍA válida (nada instalado todavía).
39    pub fn load(path: impl AsRef<Path>) -> crate::Result<InstalledDb> {
40        match std::fs::read_to_string(path.as_ref()) {
41            Ok(text) => serde_json::from_str(&text)
42                .map_err(|e| crate::Error::Serde(format!("{}: {e}", path.as_ref().display()))),
43            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(InstalledDb::default()),
44            Err(e) => Err(crate::Error::Io(e)),
45        }
46    }
47
48    /// Escribe la DB (creando el directorio padre si hace falta). JSON con saltos, diffeable.
49    pub fn save(&self, path: impl AsRef<Path>) -> crate::Result<()> {
50        let path = path.as_ref();
51        if let Some(parent) = path.parent() {
52            std::fs::create_dir_all(parent)?;
53        }
54        let text = serde_json::to_string_pretty(self)
55            .map_err(|e| crate::Error::Serde(e.to_string()))?;
56        std::fs::write(path, text)?;
57        Ok(())
58    }
59
60    pub fn find(&self, name: &str) -> Option<&InstalledPackage> {
61        self.packages.iter().find(|p| p.name == name)
62    }
63
64    /// Registra (o reemplaza por nombre) un paquete instalado. Re-instalar actualiza la lista de
65    /// ficheros (una versión nueva puede aportar otros).
66    pub fn record(&mut self, pkg: InstalledPackage) {
67        if let Some(slot) = self.packages.iter_mut().find(|p| p.name == pkg.name) {
68            *slot = pkg;
69        } else {
70            self.packages.push(pkg);
71            self.packages.sort_by(|a, b| a.name.cmp(&b.name));
72        }
73    }
74
75    /// Quita el registro de un paquete y lo devuelve (para que el caller borre sus ficheros).
76    pub fn remove(&mut self, name: &str) -> Option<InstalledPackage> {
77        let i = self.packages.iter().position(|p| p.name == name)?;
78        Some(self.packages.remove(i))
79    }
80
81    /// ¿Algún OTRO paquete instalado (≠ `name`) aporta también `file`? Si sí, `uninstall` no debe
82    /// borrarlo (lo seguiría necesitando ese tercero).
83    pub fn owned_by_others(&self, name: &str, file: &str) -> bool {
84        self.packages
85            .iter()
86            .any(|p| p.name != name && p.files.iter().any(|f| f == file))
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    fn pkg(name: &str, files: &[&str]) -> InstalledPackage {
95        InstalledPackage {
96            name: name.into(),
97            version: "1".into(),
98            hash: None,
99            files: files.iter().map(|s| s.to_string()).collect(),
100        }
101    }
102
103    #[test]
104    fn load_missing_is_empty() {
105        let d = tempfile::tempdir().unwrap();
106        let db = InstalledDb::load(d.path().join("installed.json")).unwrap();
107        assert!(db.packages.is_empty());
108    }
109
110    #[test]
111    fn record_save_load_find() {
112        let d = tempfile::tempdir().unwrap();
113        let p = d.path().join("installed.json");
114        let mut db = InstalledDb::default();
115        db.record(pkg("ripgrep", &["/usr/bin/rg"]));
116        db.save(&p).unwrap();
117        let back = InstalledDb::load(&p).unwrap();
118        assert_eq!(back.find("ripgrep").unwrap().files, vec!["/usr/bin/rg"]);
119        assert!(back.find("nope").is_none());
120    }
121
122    #[test]
123    fn remove_returns_entry() {
124        let mut db = InstalledDb::default();
125        db.record(pkg("rg", &["/usr/bin/rg"]));
126        let removed = db.remove("rg").unwrap();
127        assert_eq!(removed.files, vec!["/usr/bin/rg"]);
128        assert!(db.find("rg").is_none());
129        assert!(db.remove("rg").is_none());
130    }
131
132    #[test]
133    fn owned_by_others_refcount() {
134        let mut db = InstalledDb::default();
135        db.record(pkg("a", &["/usr/lib/libshared.so", "/usr/bin/a"]));
136        db.record(pkg("b", &["/usr/lib/libshared.so", "/usr/bin/b"]));
137        // El .so lo aportan ambos: al desinstalar 'a' no debe borrarse.
138        assert!(db.owned_by_others("a", "/usr/lib/libshared.so"));
139        // El binario propio de 'a' no lo aporta nadie más.
140        assert!(!db.owned_by_others("a", "/usr/bin/a"));
141    }
142}