use std::collections::BTreeMap;
use std::io;
use std::path::Path;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Package {
pub name: String,
pub version: String,
pub depends: Vec<String>,
pub optdepends: Vec<String>,
pub provides: Vec<String>,
pub files: Vec<String>,
}
#[derive(Debug, Default)]
pub struct Database {
packages: BTreeMap<String, Package>,
providers: BTreeMap<String, Vec<String>>,
owners: BTreeMap<String, String>,
}
pub const LOCAL_DB: &str = "var/lib/pacman/local";
fn invalid(msg: String) -> io::Error {
io::Error::new(io::ErrorKind::InvalidData, msg)
}
impl Database {
pub fn read(root: &Path) -> io::Result<Self> {
let dir = root.join(LOCAL_DB);
let mut db = Database::default();
let mut entries: Vec<_> = std::fs::read_dir(&dir)
.map_err(|e| io::Error::new(e.kind(), format!("{}: {e}", dir.display())))?
.filter_map(Result::ok)
.map(|e| e.path())
.filter(|p| p.is_dir())
.collect();
entries.sort();
for entry in entries {
let package = read_package(&entry)?;
db.insert(package);
}
Ok(db)
}
pub fn insert(&mut self, package: Package) {
for provided in &package.provides {
self.providers
.entry(provided.clone())
.or_default()
.push(package.name.clone());
}
for file in &package.files {
self.owners
.entry(file.clone())
.or_insert_with(|| package.name.clone());
}
self.packages.insert(package.name.clone(), package);
}
pub fn package(&self, name: &str) -> Option<&Package> {
self.packages.get(name)
}
pub fn packages(&self) -> impl Iterator<Item = &Package> {
self.packages.values()
}
pub fn owner(&self, path: &str) -> Option<&Package> {
let path = path.trim_start_matches('/');
self.owners
.get(path)
.and_then(|name| self.packages.get(name))
}
pub fn satisfier(&self, name: &str) -> Option<&Package> {
if let Some(p) = self.packages.get(name) {
return Some(p);
}
self.providers
.get(name)
.and_then(|list| list.iter().find_map(|n| self.packages.get(n)))
}
}
fn read_package(dir: &Path) -> io::Result<Package> {
let context = |what: &str| format!("{}: {what}", dir.display());
let desc = std::fs::read_to_string(dir.join("desc"))
.map_err(|e| io::Error::new(e.kind(), context(&format!("desc: {e}"))))?;
let sections = parse_sections(&desc);
let name = sections
.get("NAME")
.and_then(|v| v.first())
.cloned()
.ok_or_else(|| invalid(context("desc has no %NAME%")))?;
let version = sections
.get("VERSION")
.and_then(|v| v.first())
.cloned()
.ok_or_else(|| invalid(context("desc has no %VERSION%")))?;
let names = |key: &str| -> Vec<String> {
sections
.get(key)
.map(|v| v.iter().map(|s| dependency_name(s)).collect())
.unwrap_or_default()
};
let files_text = std::fs::read_to_string(dir.join("files"))
.map_err(|e| io::Error::new(e.kind(), context(&format!("files: {e}"))))?;
let files = parse_sections(&files_text)
.remove("FILES")
.unwrap_or_default()
.into_iter()
.filter(|f| !f.ends_with('/'))
.collect();
Ok(Package {
name,
version,
depends: names("DEPENDS"),
optdepends: names("OPTDEPENDS"),
provides: names("PROVIDES"),
files,
})
}
fn parse_sections(text: &str) -> BTreeMap<String, Vec<String>> {
let mut out: BTreeMap<String, Vec<String>> = BTreeMap::new();
let mut current: Option<String> = None;
for line in text.lines() {
let line = line.trim_end();
if line.len() > 2 && line.starts_with('%') && line.ends_with('%') {
let key = line[1..line.len() - 1].to_string();
out.entry(key.clone()).or_default();
current = Some(key);
} else if line.is_empty() {
current = None;
} else if let Some(key) = ¤t {
out.get_mut(key).unwrap().push(line.to_string());
}
}
out
}
pub fn dependency_name(entry: &str) -> String {
let end = entry.find(['<', '>', '=', ':']).unwrap_or(entry.len());
entry[..end].trim().to_string()
}
#[cfg(test)]
pub(crate) mod fixture {
use super::*;
pub(crate) fn write_package(root: &Path, package: &Package, dirs: &[&str]) {
let dir = root
.join(LOCAL_DB)
.join(format!("{}-{}", package.name, package.version));
std::fs::create_dir_all(&dir).unwrap();
let mut desc = format!(
"%NAME%\n{}\n\n%VERSION%\n{}\n\n",
package.name, package.version
);
for (key, list) in [
("DEPENDS", &package.depends),
("OPTDEPENDS", &package.optdepends),
("PROVIDES", &package.provides),
] {
if !list.is_empty() {
desc.push_str(&format!("%{key}%\n{}\n\n", list.join("\n")));
}
}
std::fs::write(dir.join("desc"), desc).unwrap();
let mut files = String::from("%FILES%\n");
for d in dirs {
files.push_str(d);
files.push_str("/\n");
}
for f in &package.files {
files.push_str(f);
files.push('\n');
}
files.push_str("\n%BACKUP%\n");
std::fs::write(dir.join("files"), files).unwrap();
}
pub(crate) struct TempRoot(std::path::PathBuf);
impl std::ops::Deref for TempRoot {
type Target = std::path::Path;
fn deref(&self) -> &std::path::Path {
&self.0
}
}
impl AsRef<std::path::Path> for TempRoot {
fn as_ref(&self) -> &std::path::Path {
&self.0
}
}
impl Drop for TempRoot {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
pub(crate) fn temp_root(tag: &str) -> TempRoot {
let dir = std::env::temp_dir().join(format!(
"onelf-sysroot-{tag}-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&dir).unwrap();
TempRoot(dir)
}
pub(crate) fn pkg(name: &str, files: &[&str]) -> Package {
Package {
name: name.into(),
version: "1.0-1".into(),
files: files.iter().map(|s| s.to_string()).collect(),
..Default::default()
}
}
}
#[cfg(test)]
mod tests {
use super::fixture::*;
use super::*;
#[test]
fn dependency_names_drop_constraints_and_descriptions() {
assert_eq!(dependency_name("glibc>=2.39"), "glibc");
assert_eq!(dependency_name("libfoo.so=1-64"), "libfoo.so");
assert_eq!(dependency_name("mesa: OpenGL support"), "mesa");
assert_eq!(dependency_name("zlib"), "zlib");
}
#[test]
fn reads_packages_files_and_owners() {
let root = temp_root("db");
let mut app = pkg("app", &["usr/bin/app"]);
app.depends = vec!["libfixture>=1".into(), "libfoo.so=1-64".into()];
app.optdepends = vec!["extra: more".into()];
write_package(&root, &app, &["usr", "usr/bin"]);
let mut lib = pkg("libfixture", &["usr/lib/libfixture.so.1"]);
lib.provides = vec!["libfoo.so=1-64".into()];
write_package(&root, &lib, &["usr", "usr/lib"]);
let db = Database::read(&root).unwrap();
let app = db.package("app").unwrap();
assert_eq!(app.depends, ["libfixture", "libfoo.so"]);
assert_eq!(app.optdepends, ["extra"]);
assert_eq!(db.owner("/usr/bin/app").unwrap().name, "app");
assert_eq!(
db.owner("usr/lib/libfixture.so.1").unwrap().name,
"libfixture"
);
assert!(
db.owner("usr/lib").is_none(),
"directories are owned by no one"
);
assert_eq!(db.satisfier("libfoo.so").unwrap().name, "libfixture");
assert!(db.satisfier("nothing").is_none());
}
#[test]
fn a_truncated_desc_fails_naming_the_package() {
let root = temp_root("dbcut");
let dir = root.join(LOCAL_DB).join("broken-1.0-1");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("desc"), "%NAME%\nbroken\n\n%VERSI").unwrap();
std::fs::write(dir.join("files"), "%FILES%\n").unwrap();
let err = Database::read(&root).unwrap_err();
assert!(err.to_string().contains("broken-1.0-1"), "{err}");
}
#[test]
fn a_missing_database_is_an_error() {
let root = temp_root("nodb");
assert!(Database::read(&root).is_err());
}
}