use indexmap::IndexMap;
use url::Url;
use super::{error::UbuntuError, parse::UbuntuRepositoryEntry};
#[derive(Debug, Clone)]
pub struct PackageQuery {
pub package: String,
pub version: String,
pub dbgsym: bool,
pub unsigned_fallback: bool,
}
#[derive(Debug)]
pub struct PackageIndex {
host: Url,
packages: IndexMap<String, IndexMap<String, UbuntuRepositoryEntry>>,
}
impl PackageIndex {
pub fn new(
host: Url,
packages: IndexMap<String, IndexMap<String, UbuntuRepositoryEntry>>,
) -> Self {
Self { host, packages }
}
pub fn host(&self) -> &Url {
&self.host
}
pub fn find(
&self,
query: &PackageQuery,
) -> Result<Option<&UbuntuRepositoryEntry>, UbuntuError> {
if let Some(entry) = self.find_inner(&query.package, &query.version, query.dbgsym)? {
return Ok(Some(entry));
}
if query.unsigned_fallback
&& let Some(unsigned) = unsigned_variant(&query.package)
{
return self.find_inner(&unsigned, &query.version, query.dbgsym);
}
Ok(None)
}
pub fn resolve_url(&self, entry: &UbuntuRepositoryEntry) -> Result<Url, UbuntuError> {
match &entry.filename {
Some(filename) => Ok(self.host.join(filename)?),
None => Err(UbuntuError::PackageMissingFilename),
}
}
fn find_inner(
&self,
package: &str,
version: &str,
dbgsym: bool,
) -> Result<Option<&UbuntuRepositoryEntry>, UbuntuError> {
let mut candidates = Vec::new();
for (dist, packages) in &self.packages {
let entry = match packages.get(package) {
Some(entry) => entry,
None => continue,
};
let entry_version = match &entry.version {
Some(entry_version) => entry_version,
None => continue,
};
if entry_version != version {
continue;
}
if dbgsym && entry.depends.is_some() {
continue;
}
candidates.push((dist.as_str(), entry));
}
let candidate = match candidates.pop() {
Some(candidate) => candidate,
None => return Ok(None),
};
if !candidates.is_empty() {
let dists = std::iter::once(candidate.0)
.chain(candidates.into_iter().map(|(d, _)| d))
.collect::<Vec<_>>();
tracing::error!(?dists, "multiple candidates found");
return Err(UbuntuError::PackageMultipleCandidates);
}
Ok(Some(candidate.1))
}
}
fn unsigned_variant(package: &str) -> Option<String> {
package
.strip_prefix("linux-image-")
.map(|rest| format!("linux-image-unsigned-{rest}"))
}
#[cfg(test)]
mod tests {
use indexmap::IndexMap;
use super::*;
use crate::ubuntu::parse::UbuntuRepositoryEntry;
fn entry(package: &str, version: &str, filename: &str) -> UbuntuRepositoryEntry {
UbuntuRepositoryEntry {
package: Some(package.into()),
version: Some(version.into()),
filename: Some(filename.into()),
..Default::default()
}
}
fn index_with(entries: Vec<(&str, Vec<UbuntuRepositoryEntry>)>) -> PackageIndex {
let mut packages = IndexMap::new();
for (dist, dist_entries) in entries {
let mut map = IndexMap::new();
for e in dist_entries {
map.insert(e.package.clone().unwrap(), e);
}
packages.insert(dist.into(), map);
}
PackageIndex::new("http://example.com/ubuntu/".try_into().unwrap(), packages)
}
#[test]
fn finds_signed_kernel() {
let idx = index_with(vec![(
"noble",
vec![entry(
"linux-image-6.8.0-40-generic",
"6.8.0-40.40",
"pool/x.deb",
)],
)]);
let query = PackageQuery {
package: "linux-image-6.8.0-40-generic".into(),
version: "6.8.0-40.40".into(),
dbgsym: false,
unsigned_fallback: true,
};
let found = idx.find(&query).unwrap().unwrap();
assert_eq!(found.filename.as_deref(), Some("pool/x.deb"));
}
#[test]
fn falls_back_to_unsigned_when_signed_missing() {
let idx = index_with(vec![(
"noble",
vec![entry(
"linux-image-unsigned-6.8.0-40-generic",
"6.8.0-40.40",
"pool/u.deb",
)],
)]);
let query = PackageQuery {
package: "linux-image-6.8.0-40-generic".into(),
version: "6.8.0-40.40".into(),
dbgsym: false,
unsigned_fallback: true,
};
let found = idx.find(&query).unwrap().unwrap();
assert_eq!(found.filename.as_deref(), Some("pool/u.deb"));
}
#[test]
fn unsigned_fallback_disabled_returns_none() {
let idx = index_with(vec![(
"noble",
vec![entry(
"linux-image-unsigned-6.8.0-40-generic",
"6.8.0-40.40",
"pool/u.deb",
)],
)]);
let query = PackageQuery {
package: "linux-image-6.8.0-40-generic".into(),
version: "6.8.0-40.40".into(),
dbgsym: false,
unsigned_fallback: false,
};
assert!(idx.find(&query).unwrap().is_none());
}
#[test]
fn dbgsym_filter_skips_packages_with_depends() {
let mut wrapper = entry(
"linux-image-6.8.0-40-generic-dbgsym",
"6.8.0-40.40",
"pool/wrapper.deb",
);
wrapper.depends = Some("linux-image-unsigned-6.8.0-40-generic-dbgsym".into());
let real = entry(
"linux-image-unsigned-6.8.0-40-generic-dbgsym",
"6.8.0-40.40",
"pool/real.deb",
);
let idx = index_with(vec![("noble", vec![wrapper, real])]);
let query = PackageQuery {
package: "linux-image-6.8.0-40-generic-dbgsym".into(),
version: "6.8.0-40.40".into(),
dbgsym: true,
unsigned_fallback: true,
};
let found = idx.find(&query).unwrap().unwrap();
assert_eq!(found.filename.as_deref(), Some("pool/real.deb"));
}
#[test]
fn multiple_candidates_in_different_dists_errors() {
let idx = index_with(vec![
(
"noble",
vec![entry(
"linux-image-6.8.0-40-generic",
"6.8.0-40.40",
"pool/a.deb",
)],
),
(
"noble-updates",
vec![entry(
"linux-image-6.8.0-40-generic",
"6.8.0-40.40",
"pool/b.deb",
)],
),
]);
let query = PackageQuery {
package: "linux-image-6.8.0-40-generic".into(),
version: "6.8.0-40.40".into(),
dbgsym: false,
unsigned_fallback: false,
};
assert!(matches!(
idx.find(&query),
Err(UbuntuError::PackageMultipleCandidates)
));
}
#[test]
fn resolve_url_joins_host_and_filename() {
let idx = index_with(vec![(
"noble",
vec![entry("foo", "1.0", "pool/main/f/foo.deb")],
)]);
let entry = idx
.find(&PackageQuery {
package: "foo".into(),
version: "1.0".into(),
dbgsym: false,
unsigned_fallback: false,
})
.unwrap()
.unwrap();
let url = idx.resolve_url(entry).unwrap();
assert_eq!(
url.as_str(),
"http://example.com/ubuntu/pool/main/f/foo.deb"
);
}
}