use crate::dep::{dependency_satisfied, parse_dependency};
use anyhow::{bail, Context, Result};
use std::collections::HashMap;
use std::env;
use std::fs;
use std::io::Read;
use std::path::Path;
pub(crate) const MAX_DESC_BYTES: u64 = 1024 * 1024;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LocalPackage {
pub name: String,
pub version: String,
pub desc: Option<String>,
pub provides: Vec<String>,
pub reason: Option<u8>,
}
#[derive(Debug, Clone, Default)]
pub struct LocalDb {
packages: HashMap<String, LocalPackage>,
provides: HashMap<String, Vec<LocalProvide>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct LocalProvide {
package: String,
version: Option<String>,
}
impl LocalDb {
pub fn load_default() -> Result<Self> {
let path = env::var("KNOTT_DBPATH")
.map(|path| Path::new(&path).join("local"))
.unwrap_or_else(|_| Path::new("/var/lib/pacman/local").to_path_buf());
Self::load_from_path(&path)
.with_context(|| format!("read local pacman db at {}", path.display()))
}
pub fn empty() -> Self {
Self::default()
}
pub fn load_from_path(path: &Path) -> Result<Self> {
let mut db = Self::default();
for entry in fs::read_dir(path)? {
let entry = entry?;
let desc = entry.path().join("desc");
if !desc.is_file() {
continue;
}
let data = read_desc(&desc)
.with_context(|| format!("read local package desc {}", desc.display()))?;
if let Some(pkg) = parse_desc(&data) {
for provide in &pkg.provides {
let dep = parse_dependency(provide);
db.provides.entry(dep.name).or_default().push(LocalProvide {
package: pkg.name.clone(),
version: dep.version,
});
}
db.packages.insert(pkg.name.clone(), pkg);
}
}
Ok(db)
}
pub fn get(&self, name: &str) -> Option<&LocalPackage> {
self.packages.get(name)
}
pub fn packages(&self) -> impl Iterator<Item = &LocalPackage> {
self.packages.values()
}
pub fn satisfies(&self, raw_dep: &str) -> bool {
let dep = parse_dependency(raw_dep);
if let Some(pkg) = self.packages.get(&dep.name) {
return dependency_satisfied(&pkg.version, &dep);
}
if let Some(providers) = self.provides.get(&dep.name) {
return providers.iter().any(|provider| {
if dep.op.is_none() {
return true;
}
if let Some(version) = provider.version.as_deref() {
return dependency_satisfied(version, &dep);
}
self.packages
.get(&provider.package)
.is_some_and(|pkg| dep.op.is_none() || dependency_satisfied(&pkg.version, &dep))
});
}
false
}
}
fn read_desc(path: &Path) -> Result<String> {
let file = fs::File::open(path)?;
let mut data = String::new();
let mut limited = file.take(MAX_DESC_BYTES + 1);
limited.read_to_string(&mut data)?;
if data.len() as u64 > MAX_DESC_BYTES {
bail!("local package desc exceeds {MAX_DESC_BYTES} bytes");
}
Ok(data)
}
pub fn parse_desc(data: &str) -> Option<LocalPackage> {
let mut section = "";
let mut name = None;
let mut version = None;
let mut desc = None;
let mut provides = Vec::new();
let mut reason = None;
for line in data.lines() {
if line.starts_with('%') && line.ends_with('%') && line.len() > 2 {
section = &line[1..line.len() - 1];
continue;
}
if line.is_empty() {
continue;
}
match section {
"NAME" => name = Some(line.to_string()),
"VERSION" => version = Some(line.to_string()),
"DESC" => desc = Some(line.to_string()),
"PROVIDES" => provides.push(line.to_string()),
"REASON" => reason = line.parse().ok(),
_ => {}
}
}
Some(LocalPackage {
name: name?,
version: version?,
desc,
provides,
reason,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_desc_file() {
let data = "%NAME%\nfoo\n\n%VERSION%\n1.0-1\n\n%DESC%\nA package\n\n%PROVIDES%\nbar=1.0\n\n%REASON%\n0\n";
let pkg = parse_desc(data).unwrap();
assert_eq!(pkg.name, "foo");
assert_eq!(pkg.version, "1.0-1");
assert_eq!(pkg.provides, ["bar=1.0"]);
assert_eq!(pkg.reason, Some(0));
}
#[test]
fn checks_provided_dependencies() {
let mut db = LocalDb::default();
let pkg = LocalPackage {
name: "foo".into(),
version: "1.2-1".into(),
desc: None,
provides: vec!["bar=1.2".into()],
reason: None,
};
db.provides.insert(
"bar".into(),
vec![LocalProvide {
package: "foo".into(),
version: Some("1.2".into()),
}],
);
db.packages.insert("foo".into(), pkg);
assert!(db.satisfies("bar>=1.0"));
assert!(!db.satisfies("bar>=2.0"));
}
}