use std::{
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
fs::File,
path::Path,
};
use anyhow::{Context, Error, format_err};
use deb822_fast::FromDeb822Paragraph;
use debian_control::{
lossy::{
self, Relation, SourceRelation,
apt::{Package, Source},
},
relations::VersionConstraint,
};
use debversion::Version;
use regex::Regex;
use tracing::{debug, info, warn};
pub struct PackageIndex {
#[expect(dead_code)]
pub package_versions: HashMap<String, HashSet<Version>>,
pub packages: HashMap<(String, Version), Package>,
}
impl PackageIndex {
pub fn new(
package_versions: HashMap<String, HashSet<Version>>,
packages: HashMap<(String, Version), Package>,
) -> Self {
Self {
package_versions,
packages,
}
}
}
pub struct SourceIndex {
#[expect(dead_code)]
pub source_versions: HashMap<String, HashSet<Version>>,
pub sources: HashMap<(String, Version), Source>,
}
impl SourceIndex {
pub fn new(
source_versions: HashMap<String, HashSet<Version>>,
sources: HashMap<(String, Version), Source>,
) -> Self {
Self {
source_versions,
sources,
}
}
}
pub fn parse_packages_index<P>(packages_paths: &[P]) -> Result<PackageIndex, Error>
where
P: AsRef<Path> + std::fmt::Debug,
{
let mut package_versions = HashMap::new();
let mut packages = HashMap::new();
for path in packages_paths {
let old_count = package_versions.len();
let file = File::open(path)
.with_context(|| format!("Failed to open Packages index at {path:?}"))?;
let deb822 = deb822_fast::Deb822::from_reader(&file)
.map_err(|err| format_err!("Failed to parse Packages index at {path:?} - {err:?}"))?;
for package_paragraph in deb822.iter() {
match lossy::apt::Package::from_paragraph(package_paragraph) {
Ok(pkg) => {
debug!("Found package {name}", name = &pkg.name);
let versions_entry = package_versions.entry(pkg.name.clone());
let versions_set = versions_entry.or_insert(HashSet::new());
versions_set.insert(pkg.version.clone());
packages.insert((pkg.name.clone(), pkg.version.clone()), pkg);
}
Err(err) => warn!("Failed to parse paragraph in {path:?} - {err:?}"),
}
}
info!(
"Found {count} packages in {path:?}",
count = package_versions.len() - old_count,
);
}
Ok(PackageIndex::new(package_versions, packages))
}
pub fn parse_sources_index<P>(sources_paths: &[P]) -> Result<SourceIndex, Error>
where
P: AsRef<Path> + std::fmt::Debug,
{
let mut source_versions = HashMap::new();
let mut sources = HashMap::new();
for path in sources_paths {
let old_count = source_versions.len();
let file = File::open(path)
.with_context(|| format!("Failed to open Sources index at {path:?}"))?;
let deb822 = deb822_fast::Deb822::from_reader(&file)
.map_err(|err| format_err!("Failed to parse Sources index at {path:?} - {err:?}"))?;
for source_paragraph in deb822.iter() {
match lossy::apt::Source::from_paragraph(source_paragraph) {
Ok(pkg) => {
let name = pkg.package.clone();
debug!("Found source package {name}");
let versions_entry = source_versions.entry(name.clone());
let versions_set = versions_entry.or_insert(HashSet::new());
versions_set.insert(pkg.version.clone());
sources.insert((name.clone(), pkg.version.clone()), pkg);
}
Err(err) => debug!("Failed to parse paragraph in {path:?} - {err:?}"),
}
}
info!(
"Found {count} source packages in {path:?}",
count = source_versions.len() - old_count
);
}
Ok(SourceIndex::new(source_versions, sources))
}
#[derive(Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Clone)]
pub(crate) enum PackageKind {
Binary,
Source,
}
#[derive(Debug, Clone)]
pub(crate) struct RealPackage {
pub(crate) name: String,
pub(crate) version: Version,
pub(crate) source: Option<SourceRelation>,
pub(crate) kind: PackageKind,
}
impl Ord for RealPackage {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.name
.cmp(&other.name)
.then(self.version.cmp(&other.version))
.then(self.kind.cmp(&other.kind))
}
}
impl PartialOrd for RealPackage {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Eq for RealPackage {}
impl PartialEq for RealPackage {
fn eq(&self, other: &Self) -> bool {
self.cmp(other) == std::cmp::Ordering::Equal && self.source == other.source
}
}
impl std::hash::Hash for RealPackage {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.name.hash(state);
self.version.hash(state);
self.source.hash(state);
self.kind.hash(state);
}
}
#[derive(Debug, Hash, PartialEq, Eq, Clone)]
pub(crate) struct VirtualPackage {
pub(crate) name: String,
pub(crate) version: Option<Version>,
pub(crate) provider: RealPackage,
}
#[derive(Debug, Hash, PartialEq, Eq, Clone)]
pub(crate) struct UnknownPackage {
pub(crate) name: String,
pub(crate) version: Option<(VersionConstraint, Version)>,
}
#[derive(Debug, Hash, PartialEq, Eq, Ord, PartialOrd, Clone)]
pub enum DepKind {
Depends,
PreDepends,
Recommends,
Suggests,
BuildDepends,
BuildDependsArch,
BuildDependsIndep,
TestSuiteTriggers,
}
fn map_relation(regex: &Regex, relation: &Relation) -> Option<UnknownPackage> {
if regex.is_match(&relation.name) {
Some(UnknownPackage {
name: relation.name.clone(),
version: relation.version.clone(),
})
} else {
None
}
}
fn handle_relation(
reverse_deps: &mut BTreeMap<DepKind, BTreeMap<RealPackage, Vec<UnknownPackage>>>,
found: UnknownPackage,
real_package: &RealPackage,
kind: DepKind,
) {
debug!(
"package {package_name} {kind:?} {relation} {relation_ver:?}",
package_name = real_package.name,
relation = found.name,
relation_ver = found.version,
);
let dep_entry = reverse_deps.entry(kind).or_default();
let package_entry = dep_entry.entry(real_package.clone()).or_default();
package_entry.push(found);
}
#[derive(Debug, Clone, Default)]
pub(crate) struct FoundPackages {
pub(crate) source_packages: BTreeSet<RealPackage>,
pub(crate) binary_packages: BTreeSet<RealPackage>,
pub(crate) virtual_packages: HashSet<VirtualPackage>,
pub(crate) reverse_depends: BTreeMap<DepKind, BTreeMap<RealPackage, Vec<UnknownPackage>>>,
pub(crate) statically_linked: BTreeMap<RealPackage, UnknownPackage>,
}
#[tracing::instrument(skip(packages, sources))]
pub fn get_all_packages_for_crate(
crate_name: &str,
packages: &HashMap<(String, Version), Package>,
sources: &HashMap<(String, Version), Source>,
) -> Result<FoundPackages, Error> {
let package_name_re_str = format!(
r"^librust-{crate_name}(?:-([0-9]+(?:\.[0-9]+(?:\.[0-9]+)?)?)?)?(\+[0-9a-zA-Z-]+)??-dev$"
);
let package_name_regex = Regex::new(&package_name_re_str.to_string())?;
let source_package_name_re_str = format!(r"^rust-{crate_name}(?:-([0-9]+(?:\.[0-9]+)))?$");
let source_package_name_regex = Regex::new(&source_package_name_re_str.to_string())?;
let mut found_packages = FoundPackages::default();
for ((package_name, version), package) in packages.iter() {
let real_package = RealPackage {
name: package_name.clone(),
version: version.clone(),
source: package.source.clone(),
kind: PackageKind::Binary,
};
if package_name_regex.is_match(package_name) {
info!("Found package {package_name}");
found_packages.binary_packages.insert(real_package.clone());
}
if let Some(ref provides) = package.provides {
for relation in provides.iter().flatten() {
if package_name_regex.is_match(&relation.name) {
debug!("package {package_name} provides {relation}");
found_packages.virtual_packages.insert(VirtualPackage {
name: relation.name.clone(),
version: relation
.version
.as_ref()
.map(|(_constraint, ver)| ver.clone()),
provider: real_package.clone(),
});
}
}
}
if let Some(ref depends) = package.depends {
depends
.iter()
.flatten()
.filter_map(|r| map_relation(&package_name_regex, r))
.for_each(|r| {
handle_relation(
&mut found_packages.reverse_depends,
r,
&real_package,
DepKind::Depends,
)
});
}
if let Some(ref pre_depends) = package.pre_depends {
pre_depends
.iter()
.flatten()
.filter_map(|r| map_relation(&package_name_regex, r))
.for_each(|r| {
handle_relation(
&mut found_packages.reverse_depends,
r,
&real_package,
DepKind::PreDepends,
)
});
}
if let Some(ref rev_depends) = package.recommends {
rev_depends
.iter()
.flatten()
.filter_map(|r| map_relation(&package_name_regex, r))
.for_each(|r| {
handle_relation(
&mut found_packages.reverse_depends,
r,
&real_package,
DepKind::Recommends,
)
});
}
if let Some(ref rev_depends) = package.suggests {
rev_depends
.iter()
.flatten()
.filter_map(|r| map_relation(&package_name_regex, r))
.for_each(|r| {
handle_relation(
&mut found_packages.reverse_depends,
r,
&real_package,
DepKind::Suggests,
)
});
}
if let Some(ref sbu) = package.static_built_using {
for relation in sbu.iter().flatten() {
if source_package_name_regex.is_match(&relation.name) {
debug!("package {package_name} statically links {relation}");
let linking_package = UnknownPackage {
name: relation.name.clone(),
version: relation.version.clone(),
};
found_packages
.statically_linked
.insert(real_package.clone(), linking_package);
}
}
}
}
for ((source_name, version), source_package) in sources.iter() {
let real_package = RealPackage {
name: source_name.clone(),
version: version.clone(),
source: None,
kind: PackageKind::Source,
};
if source_package_name_regex.is_match(source_name) {
info!("Found source package {source_name}");
found_packages.source_packages.insert(real_package.clone());
}
if let Some(ref rev_depends) = source_package.build_depends {
rev_depends
.iter()
.flatten()
.filter_map(|r| map_relation(&package_name_regex, r))
.for_each(|r| {
handle_relation(
&mut found_packages.reverse_depends,
r,
&real_package,
DepKind::BuildDepends,
)
});
}
if let Some(ref rev_depends) = source_package.build_depends_arch {
rev_depends
.iter()
.flatten()
.filter_map(|r| map_relation(&package_name_regex, r))
.for_each(|r| {
handle_relation(
&mut found_packages.reverse_depends,
r,
&real_package,
DepKind::BuildDependsArch,
)
});
}
if let Some(ref rev_depends) = source_package.build_depends_indep {
rev_depends
.iter()
.flatten()
.filter_map(|r| map_relation(&package_name_regex, r))
.for_each(|r| {
handle_relation(
&mut found_packages.reverse_depends,
r,
&real_package,
DepKind::BuildDependsIndep,
)
});
}
if let Some(ref testsuite_triggers) = source_package.testsuite_triggers {
for trigger in testsuite_triggers {
if package_name_regex.is_match(trigger) {
let r = UnknownPackage {
name: trigger.clone(),
version: None,
};
handle_relation(
&mut found_packages.reverse_depends,
r,
&real_package,
DepKind::TestSuiteTriggers,
);
}
}
}
}
let src_count = found_packages.source_packages.len();
let bin_count = found_packages.binary_packages.len();
info!(
"Found {count} matching packages for {crate_name} (source: {src_count}, binary: {bin_count}.",
count = src_count + bin_count,
);
info!(
"Found {virtual_count} provided packages",
virtual_count = found_packages.virtual_packages.len()
);
debug!("Packages: {found_packages:?}");
Ok(found_packages)
}