Skip to main content

onelf_sysroot/
db.rs

1//! The pacman local database.
2//!
3//! Each installed package is a directory `<name>-<version>` under
4//! `var/lib/pacman/local`, holding a `desc` file of `%SECTION%` blocks and
5//! a `files` file listing what the package owns. Both are plain text.
6//! Dependency names carry version constraints and soname suffixes
7//! (`glibc>=2.39`, `libfoo.so=1-64`); only the name is kept, since the
8//! sysroot is pinned and what is installed is what there is.
9
10use std::collections::BTreeMap;
11use std::io;
12use std::path::Path;
13
14/// One installed package.
15#[derive(Debug, Clone, Default, PartialEq, Eq)]
16pub struct Package {
17    pub name: String,
18    pub version: String,
19    pub depends: Vec<String>,
20    pub optdepends: Vec<String>,
21    pub provides: Vec<String>,
22    /// Owned files relative to the root, directories excluded.
23    pub files: Vec<String>,
24}
25
26/// Every installed package, with indexes by provided name and by file.
27#[derive(Debug, Default)]
28pub struct Database {
29    packages: BTreeMap<String, Package>,
30    providers: BTreeMap<String, Vec<String>>,
31    owners: BTreeMap<String, String>,
32}
33
34/// Where the database sits under a root.
35pub const LOCAL_DB: &str = "var/lib/pacman/local";
36
37fn invalid(msg: String) -> io::Error {
38    io::Error::new(io::ErrorKind::InvalidData, msg)
39}
40
41impl Database {
42    /// Read the database of the sysroot at `root`.
43    pub fn read(root: &Path) -> io::Result<Self> {
44        let dir = root.join(LOCAL_DB);
45        let mut db = Database::default();
46        let mut entries: Vec<_> = std::fs::read_dir(&dir)
47            .map_err(|e| io::Error::new(e.kind(), format!("{}: {e}", dir.display())))?
48            .filter_map(Result::ok)
49            .map(|e| e.path())
50            .filter(|p| p.is_dir())
51            .collect();
52        entries.sort();
53        for entry in entries {
54            let package = read_package(&entry)?;
55            db.insert(package);
56        }
57        Ok(db)
58    }
59
60    /// Add a package, indexing what it provides and owns. Later packages
61    /// never displace an earlier owner of the same file.
62    pub fn insert(&mut self, package: Package) {
63        for provided in &package.provides {
64            self.providers
65                .entry(provided.clone())
66                .or_default()
67                .push(package.name.clone());
68        }
69        for file in &package.files {
70            self.owners
71                .entry(file.clone())
72                .or_insert_with(|| package.name.clone());
73        }
74        self.packages.insert(package.name.clone(), package);
75    }
76
77    pub fn package(&self, name: &str) -> Option<&Package> {
78        self.packages.get(name)
79    }
80
81    pub fn packages(&self) -> impl Iterator<Item = &Package> {
82        self.packages.values()
83    }
84
85    /// The package owning `path`, given relative to the root.
86    pub fn owner(&self, path: &str) -> Option<&Package> {
87        let path = path.trim_start_matches('/');
88        self.owners
89            .get(path)
90            .and_then(|name| self.packages.get(name))
91    }
92
93    /// The package satisfying a dependency name: the package itself, or
94    /// the first provider of a virtual name.
95    pub fn satisfier(&self, name: &str) -> Option<&Package> {
96        if let Some(p) = self.packages.get(name) {
97            return Some(p);
98        }
99        self.providers
100            .get(name)
101            .and_then(|list| list.iter().find_map(|n| self.packages.get(n)))
102    }
103}
104
105fn read_package(dir: &Path) -> io::Result<Package> {
106    let context = |what: &str| format!("{}: {what}", dir.display());
107    let desc = std::fs::read_to_string(dir.join("desc"))
108        .map_err(|e| io::Error::new(e.kind(), context(&format!("desc: {e}"))))?;
109    let sections = parse_sections(&desc);
110    let name = sections
111        .get("NAME")
112        .and_then(|v| v.first())
113        .cloned()
114        .ok_or_else(|| invalid(context("desc has no %NAME%")))?;
115    let version = sections
116        .get("VERSION")
117        .and_then(|v| v.first())
118        .cloned()
119        .ok_or_else(|| invalid(context("desc has no %VERSION%")))?;
120    let names = |key: &str| -> Vec<String> {
121        sections
122            .get(key)
123            .map(|v| v.iter().map(|s| dependency_name(s)).collect())
124            .unwrap_or_default()
125    };
126
127    let files_text = std::fs::read_to_string(dir.join("files"))
128        .map_err(|e| io::Error::new(e.kind(), context(&format!("files: {e}"))))?;
129    let files = parse_sections(&files_text)
130        .remove("FILES")
131        .unwrap_or_default()
132        .into_iter()
133        .filter(|f| !f.ends_with('/'))
134        .collect();
135
136    Ok(Package {
137        name,
138        version,
139        depends: names("DEPENDS"),
140        optdepends: names("OPTDEPENDS"),
141        provides: names("PROVIDES"),
142        files,
143    })
144}
145
146/// `%KEY%` blocks, each holding the non-empty lines up to the next blank
147/// line or the next key.
148fn parse_sections(text: &str) -> BTreeMap<String, Vec<String>> {
149    let mut out: BTreeMap<String, Vec<String>> = BTreeMap::new();
150    let mut current: Option<String> = None;
151    for line in text.lines() {
152        let line = line.trim_end();
153        if line.len() > 2 && line.starts_with('%') && line.ends_with('%') {
154            let key = line[1..line.len() - 1].to_string();
155            out.entry(key.clone()).or_default();
156            current = Some(key);
157        } else if line.is_empty() {
158            current = None;
159        } else if let Some(key) = &current {
160            out.get_mut(key).unwrap().push(line.to_string());
161        }
162    }
163    out
164}
165
166/// The bare name of a dependency, provision or optional dependency entry:
167/// `glibc>=2.39` is `glibc`, `libfoo.so=1-64` is `libfoo.so`, and
168/// `mesa: OpenGL support` is `mesa`.
169pub fn dependency_name(entry: &str) -> String {
170    let end = entry.find(['<', '>', '=', ':']).unwrap_or(entry.len());
171    entry[..end].trim().to_string()
172}
173
174#[cfg(test)]
175pub(crate) mod fixture {
176    use super::*;
177
178    /// Write a package entry into the database under `root`.
179    pub(crate) fn write_package(root: &Path, package: &Package, dirs: &[&str]) {
180        let dir = root
181            .join(LOCAL_DB)
182            .join(format!("{}-{}", package.name, package.version));
183        std::fs::create_dir_all(&dir).unwrap();
184        let mut desc = format!(
185            "%NAME%\n{}\n\n%VERSION%\n{}\n\n",
186            package.name, package.version
187        );
188        for (key, list) in [
189            ("DEPENDS", &package.depends),
190            ("OPTDEPENDS", &package.optdepends),
191            ("PROVIDES", &package.provides),
192        ] {
193            if !list.is_empty() {
194                desc.push_str(&format!("%{key}%\n{}\n\n", list.join("\n")));
195            }
196        }
197        std::fs::write(dir.join("desc"), desc).unwrap();
198        let mut files = String::from("%FILES%\n");
199        for d in dirs {
200            files.push_str(d);
201            files.push_str("/\n");
202        }
203        for f in &package.files {
204            files.push_str(f);
205            files.push('\n');
206        }
207        files.push_str("\n%BACKUP%\n");
208        std::fs::write(dir.join("files"), files).unwrap();
209    }
210
211    /// A fresh directory under the system temp dir, removed on drop.
212    pub(crate) struct TempRoot(std::path::PathBuf);
213
214    impl std::ops::Deref for TempRoot {
215        type Target = std::path::Path;
216        fn deref(&self) -> &std::path::Path {
217            &self.0
218        }
219    }
220
221    impl AsRef<std::path::Path> for TempRoot {
222        fn as_ref(&self) -> &std::path::Path {
223            &self.0
224        }
225    }
226
227    impl Drop for TempRoot {
228        fn drop(&mut self) {
229            let _ = std::fs::remove_dir_all(&self.0);
230        }
231    }
232
233    pub(crate) fn temp_root(tag: &str) -> TempRoot {
234        let dir = std::env::temp_dir().join(format!(
235            "onelf-sysroot-{tag}-{}-{}",
236            std::process::id(),
237            std::time::SystemTime::now()
238                .duration_since(std::time::UNIX_EPOCH)
239                .unwrap()
240                .as_nanos()
241        ));
242        std::fs::create_dir_all(&dir).unwrap();
243        TempRoot(dir)
244    }
245
246    pub(crate) fn pkg(name: &str, files: &[&str]) -> Package {
247        Package {
248            name: name.into(),
249            version: "1.0-1".into(),
250            files: files.iter().map(|s| s.to_string()).collect(),
251            ..Default::default()
252        }
253    }
254}
255
256#[cfg(test)]
257mod tests {
258    use super::fixture::*;
259    use super::*;
260
261    #[test]
262    fn dependency_names_drop_constraints_and_descriptions() {
263        assert_eq!(dependency_name("glibc>=2.39"), "glibc");
264        assert_eq!(dependency_name("libfoo.so=1-64"), "libfoo.so");
265        assert_eq!(dependency_name("mesa: OpenGL support"), "mesa");
266        assert_eq!(dependency_name("zlib"), "zlib");
267    }
268
269    #[test]
270    fn reads_packages_files_and_owners() {
271        let root = temp_root("db");
272        let mut app = pkg("app", &["usr/bin/app"]);
273        app.depends = vec!["libfixture>=1".into(), "libfoo.so=1-64".into()];
274        app.optdepends = vec!["extra: more".into()];
275        write_package(&root, &app, &["usr", "usr/bin"]);
276        let mut lib = pkg("libfixture", &["usr/lib/libfixture.so.1"]);
277        lib.provides = vec!["libfoo.so=1-64".into()];
278        write_package(&root, &lib, &["usr", "usr/lib"]);
279
280        let db = Database::read(&root).unwrap();
281        let app = db.package("app").unwrap();
282        assert_eq!(app.depends, ["libfixture", "libfoo.so"]);
283        assert_eq!(app.optdepends, ["extra"]);
284        assert_eq!(db.owner("/usr/bin/app").unwrap().name, "app");
285        assert_eq!(
286            db.owner("usr/lib/libfixture.so.1").unwrap().name,
287            "libfixture"
288        );
289        assert!(
290            db.owner("usr/lib").is_none(),
291            "directories are owned by no one"
292        );
293        assert_eq!(db.satisfier("libfoo.so").unwrap().name, "libfixture");
294        assert!(db.satisfier("nothing").is_none());
295    }
296
297    #[test]
298    fn a_truncated_desc_fails_naming_the_package() {
299        let root = temp_root("dbcut");
300        let dir = root.join(LOCAL_DB).join("broken-1.0-1");
301        std::fs::create_dir_all(&dir).unwrap();
302        std::fs::write(dir.join("desc"), "%NAME%\nbroken\n\n%VERSI").unwrap();
303        std::fs::write(dir.join("files"), "%FILES%\n").unwrap();
304        let err = Database::read(&root).unwrap_err();
305        assert!(err.to_string().contains("broken-1.0-1"), "{err}");
306    }
307
308    #[test]
309    fn a_missing_database_is_an_error() {
310        let root = temp_root("nodb");
311        assert!(Database::read(&root).is_err());
312    }
313}