use super::{IndexError, PackageIndex, PackageIter, PackageMeta, VersionMeta};
use crate::cache;
use flate2::read::GzDecoder;
use rayon::prelude::*;
use std::collections::HashMap;
use std::io::{BufRead, BufReader, Cursor, Read};
use std::time::Duration;
const INDEX_CACHE_TTL: Duration = Duration::from_secs(60 * 60);
const DEBIAN_MIRROR: &str = "https://deb.debian.org/debian";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AptRepo {
StableMain,
StableContrib,
StableNonFree,
StableNonFreeFirmware,
StableBackportsMain,
StableBackportsContrib,
StableBackportsNonFree,
TestingMain,
TestingContrib,
TestingNonFree,
TestingNonFreeFirmware,
UnstableMain,
UnstableContrib,
UnstableNonFree,
UnstableNonFreeFirmware,
ExperimentalMain,
ExperimentalContrib,
ExperimentalNonFree,
OldstableMain,
OldstableContrib,
OldstableNonFree,
}
struct DistComponent {
dist: &'static str,
component: &'static str,
}
impl AptRepo {
fn parts(&self) -> DistComponent {
let (dist, component) = match self {
Self::StableMain => ("stable", "main"),
Self::StableContrib => ("stable", "contrib"),
Self::StableNonFree => ("stable", "non-free"),
Self::StableNonFreeFirmware => ("stable", "non-free-firmware"),
Self::StableBackportsMain => ("stable-backports", "main"),
Self::StableBackportsContrib => ("stable-backports", "contrib"),
Self::StableBackportsNonFree => ("stable-backports", "non-free"),
Self::TestingMain => ("testing", "main"),
Self::TestingContrib => ("testing", "contrib"),
Self::TestingNonFree => ("testing", "non-free"),
Self::TestingNonFreeFirmware => ("testing", "non-free-firmware"),
Self::UnstableMain => ("unstable", "main"),
Self::UnstableContrib => ("unstable", "contrib"),
Self::UnstableNonFree => ("unstable", "non-free"),
Self::UnstableNonFreeFirmware => ("unstable", "non-free-firmware"),
Self::ExperimentalMain => ("experimental", "main"),
Self::ExperimentalContrib => ("experimental", "contrib"),
Self::ExperimentalNonFree => ("experimental", "non-free"),
Self::OldstableMain => ("oldstable", "main"),
Self::OldstableContrib => ("oldstable", "contrib"),
Self::OldstableNonFree => ("oldstable", "non-free"),
};
DistComponent { dist, component }
}
fn packages_url(&self) -> String {
let parts = self.parts();
format!(
"{}/dists/{}/{}/binary-amd64/Packages.gz",
DEBIAN_MIRROR, parts.dist, parts.component
)
}
pub fn name(&self) -> &'static str {
match self {
Self::StableMain => "stable-main",
Self::StableContrib => "stable-contrib",
Self::StableNonFree => "stable-non-free",
Self::StableNonFreeFirmware => "stable-non-free-firmware",
Self::StableBackportsMain => "stable-backports-main",
Self::StableBackportsContrib => "stable-backports-contrib",
Self::StableBackportsNonFree => "stable-backports-non-free",
Self::TestingMain => "testing-main",
Self::TestingContrib => "testing-contrib",
Self::TestingNonFree => "testing-non-free",
Self::TestingNonFreeFirmware => "testing-non-free-firmware",
Self::UnstableMain => "unstable-main",
Self::UnstableContrib => "unstable-contrib",
Self::UnstableNonFree => "unstable-non-free",
Self::UnstableNonFreeFirmware => "unstable-non-free-firmware",
Self::ExperimentalMain => "experimental-main",
Self::ExperimentalContrib => "experimental-contrib",
Self::ExperimentalNonFree => "experimental-non-free",
Self::OldstableMain => "oldstable-main",
Self::OldstableContrib => "oldstable-contrib",
Self::OldstableNonFree => "oldstable-non-free",
}
}
pub fn all() -> &'static [AptRepo] {
&[
Self::StableMain,
Self::StableContrib,
Self::StableNonFree,
Self::StableNonFreeFirmware,
Self::StableBackportsMain,
Self::StableBackportsContrib,
Self::StableBackportsNonFree,
Self::TestingMain,
Self::TestingContrib,
Self::TestingNonFree,
Self::TestingNonFreeFirmware,
Self::UnstableMain,
Self::UnstableContrib,
Self::UnstableNonFree,
Self::UnstableNonFreeFirmware,
Self::ExperimentalMain,
Self::ExperimentalContrib,
Self::ExperimentalNonFree,
Self::OldstableMain,
Self::OldstableContrib,
Self::OldstableNonFree,
]
}
pub fn stable() -> &'static [AptRepo] {
&[
Self::StableMain,
Self::StableContrib,
Self::StableNonFree,
Self::StableNonFreeFirmware,
Self::StableBackportsMain,
Self::StableBackportsContrib,
Self::StableBackportsNonFree,
]
}
pub fn testing() -> &'static [AptRepo] {
&[
Self::TestingMain,
Self::TestingContrib,
Self::TestingNonFree,
Self::TestingNonFreeFirmware,
]
}
pub fn unstable() -> &'static [AptRepo] {
&[
Self::UnstableMain,
Self::UnstableContrib,
Self::UnstableNonFree,
Self::UnstableNonFreeFirmware,
]
}
pub fn free() -> &'static [AptRepo] {
&[
Self::StableMain,
Self::StableBackportsMain,
Self::TestingMain,
Self::UnstableMain,
Self::ExperimentalMain,
Self::OldstableMain,
]
}
pub fn oldstable() -> &'static [AptRepo] {
&[
Self::OldstableMain,
Self::OldstableContrib,
Self::OldstableNonFree,
]
}
}
pub struct Apt {
repos: Vec<AptRepo>,
}
impl Apt {
pub fn all() -> Self {
Self {
repos: AptRepo::all().to_vec(),
}
}
pub fn stable() -> Self {
Self {
repos: AptRepo::stable().to_vec(),
}
}
pub fn testing() -> Self {
Self {
repos: AptRepo::testing().to_vec(),
}
}
pub fn unstable() -> Self {
Self {
repos: AptRepo::unstable().to_vec(),
}
}
pub fn free() -> Self {
Self {
repos: AptRepo::free().to_vec(),
}
}
pub fn oldstable() -> Self {
Self {
repos: AptRepo::oldstable().to_vec(),
}
}
pub fn with_repos(repos: &[AptRepo]) -> Self {
Self {
repos: repos.to_vec(),
}
}
fn parse_control<R: Read>(reader: R, repo: AptRepo) -> Vec<PackageMeta> {
let reader = BufReader::new(reader);
let mut packages = Vec::new();
let mut current: Option<PackageBuilder> = None;
for line in reader.lines().map_while(Result::ok) {
if line.is_empty() {
if let Some(builder) = current.take()
&& let Some(pkg) = builder.build(repo)
{
packages.push(pkg);
}
continue;
}
if line.starts_with(' ') || line.starts_with('\t') {
continue;
}
if let Some((key, value)) = line.split_once(':') {
let key = key.trim();
let value = value.trim();
let builder = current.get_or_insert_with(PackageBuilder::new);
match key {
"Package" => builder.name = Some(value.to_string()),
"Version" => builder.version = Some(value.to_string()),
"Description" => builder.description = Some(value.to_string()),
"Homepage" => builder.homepage = Some(value.to_string()),
"Vcs-Git" | "Vcs-Browser" => {
if builder.repository.is_none() {
builder.repository = Some(value.to_string());
}
}
"Filename" => builder.filename = Some(value.to_string()),
"SHA256" => builder.sha256 = Some(value.to_string()),
"Depends" => builder.depends = Some(value.to_string()),
"Provides" => builder.provides = Some(value.to_string()),
"Size" => builder.size = value.parse().ok(),
_ => {}
}
}
}
if let Some(builder) = current
&& let Some(pkg) = builder.build(repo)
{
packages.push(pkg);
}
packages
}
fn load_repo(repo: AptRepo) -> Result<Vec<PackageMeta>, IndexError> {
let url = repo.packages_url();
let (data, _was_cached) = cache::fetch_with_cache(
"apt",
&format!("packages-{}", repo.name()),
&url,
INDEX_CACHE_TTL,
)
.map_err(IndexError::Network)?;
let reader: Box<dyn Read> = if data.len() >= 2 && data[0] == 0x1f && data[1] == 0x8b {
Box::new(GzDecoder::new(Cursor::new(data)))
} else {
Box::new(Cursor::new(data))
};
Ok(Self::parse_control(reader, repo))
}
fn load_packages(&self) -> Result<Vec<PackageMeta>, IndexError> {
let results: Vec<_> = self
.repos
.par_iter()
.map(|&repo| Self::load_repo(repo))
.collect();
let mut packages = Vec::new();
for result in results {
match result {
Ok(pkgs) => packages.extend(pkgs),
Err(e) => {
tracing::warn!("failed to load APT repo: {}", e);
}
}
}
Ok(packages)
}
fn fetch_repo_data(repo: AptRepo) -> Result<Vec<u8>, IndexError> {
let url = repo.packages_url();
let (data, _was_cached) = cache::fetch_with_cache(
"apt",
&format!("packages-{}", repo.name()),
&url,
INDEX_CACHE_TTL,
)
.map_err(IndexError::Network)?;
Ok(data)
}
fn load_repos_data(&self) -> Vec<(Vec<u8>, AptRepo)> {
self.repos
.par_iter()
.filter_map(|&repo| Self::fetch_repo_data(repo).ok().map(|data| (data, repo)))
.collect()
}
}
impl PackageIndex for Apt {
fn ecosystem(&self) -> &'static str {
"apt"
}
fn display_name(&self) -> &'static str {
"APT (Debian)"
}
fn fetch(&self, name: &str) -> Result<PackageMeta, IndexError> {
let url = format!(
"https://sources.debian.org/api/src/{}/",
urlencoding::encode(name)
);
let response: serde_json::Value = ureq::get(&url).call()?.into_json()?;
if response.get("error").is_some() {
return Err(IndexError::NotFound(name.to_string()));
}
let versions = response["versions"]
.as_array()
.ok_or_else(|| IndexError::Parse("missing versions".into()))?;
let latest = versions
.first()
.ok_or_else(|| IndexError::NotFound(name.to_string()))?;
Ok(PackageMeta {
name: name.to_string(),
version: latest["version"].as_str().unwrap_or("unknown").to_string(),
description: None,
homepage: response["homepage"].as_str().map(String::from),
repository: response["vcs_url"].as_str().map(String::from),
license: None,
binaries: Vec::new(),
keywords: Vec::new(),
maintainers: Vec::new(),
published: None,
downloads: None,
archive_url: None,
checksum: None,
extra: Default::default(),
})
}
fn fetch_versions(&self, name: &str) -> Result<Vec<VersionMeta>, IndexError> {
let url = format!(
"https://sources.debian.org/api/src/{}/",
urlencoding::encode(name)
);
let response: serde_json::Value = ureq::get(&url).call()?.into_json()?;
if response.get("error").is_some() {
return Err(IndexError::NotFound(name.to_string()));
}
let versions = response["versions"]
.as_array()
.ok_or_else(|| IndexError::Parse("missing versions".into()))?;
Ok(versions
.iter()
.map(|v| VersionMeta {
version: v["version"].as_str().unwrap_or("unknown").to_string(),
released: None,
yanked: false,
})
.collect())
}
fn supports_fetch_all(&self) -> bool {
true
}
fn fetch_all(&self) -> Result<Vec<PackageMeta>, IndexError> {
self.load_packages()
}
fn iter_all(&self) -> Result<PackageIter<'_>, IndexError> {
let repos_data = self.load_repos_data();
if repos_data.is_empty() {
return Err(IndexError::Network("Failed to load any APT repos".into()));
}
Ok(Box::new(AptPackageIter::new(repos_data)))
}
fn search(&self, query: &str) -> Result<Vec<PackageMeta>, IndexError> {
let packages = self.load_packages()?;
let query_lower = query.to_lowercase();
let results: Vec<PackageMeta> = packages
.into_iter()
.filter(|pkg| {
pkg.name.to_lowercase().contains(&query_lower)
|| pkg
.description
.as_ref()
.map(|d| d.to_lowercase().contains(&query_lower))
.unwrap_or(false)
})
.collect();
if !results.is_empty() {
return Ok(results);
}
let url = format!(
"https://sources.debian.org/api/search/{}/?suite=stable",
urlencoding::encode(query)
);
let response: serde_json::Value = ureq::get(&url).call()?.into_json()?;
let api_results = response["results"]["exact"]
.as_array()
.or_else(|| response["results"]["other"].as_array())
.ok_or_else(|| IndexError::Parse("missing results".into()))?;
api_results
.iter()
.map(|r| {
let name = r["name"].as_str().unwrap_or("").to_string();
self.fetch(&name)
})
.collect()
}
}
#[derive(Default)]
struct PackageBuilder {
name: Option<String>,
version: Option<String>,
description: Option<String>,
homepage: Option<String>,
repository: Option<String>,
filename: Option<String>,
sha256: Option<String>,
depends: Option<String>,
provides: Option<String>,
size: Option<u64>,
}
impl PackageBuilder {
fn new() -> Self {
Self::default()
}
fn build(self, repo: AptRepo) -> Option<PackageMeta> {
let mut extra = HashMap::new();
if let Some(deps) = self.depends {
let parsed_deps: Vec<String> = deps
.split(',')
.map(|d| {
d.trim()
.split_once(' ')
.map(|(name, _)| name)
.unwrap_or(d.trim())
.to_string()
})
.filter(|d| !d.is_empty())
.collect();
extra.insert(
"depends".to_string(),
serde_json::Value::Array(
parsed_deps
.into_iter()
.map(serde_json::Value::String)
.collect(),
),
);
}
if let Some(provides) = self.provides {
let parsed_provides: Vec<String> = provides
.split(',')
.map(|p| {
p.trim()
.split_once(' ')
.map(|(name, _)| name)
.unwrap_or(p.trim())
.to_string()
})
.filter(|p| !p.is_empty())
.collect();
if !parsed_provides.is_empty() {
extra.insert(
"provides".to_string(),
serde_json::Value::Array(
parsed_provides
.into_iter()
.map(serde_json::Value::String)
.collect(),
),
);
}
}
if let Some(size) = self.size {
extra.insert("size".to_string(), serde_json::Value::Number(size.into()));
}
extra.insert(
"source_repo".to_string(),
serde_json::Value::String(repo.name().to_string()),
);
Some(PackageMeta {
name: self.name?,
version: self.version?,
description: self.description,
homepage: self.homepage,
repository: self.repository,
license: None,
binaries: Vec::new(),
keywords: Vec::new(),
maintainers: Vec::new(),
published: None,
downloads: None,
archive_url: self.filename.map(|f| format!("{}/{}", DEBIAN_MIRROR, f)),
checksum: self.sha256.map(|h| format!("sha256:{}", h)),
extra,
})
}
}
pub struct AptPackageIter {
repos_data: Vec<(Vec<u8>, AptRepo)>,
current_repo_idx: usize,
current_reader: Option<Box<dyn BufRead + Send>>,
current_repo: Option<AptRepo>,
current_builder: Option<PackageBuilder>,
done: bool,
}
impl AptPackageIter {
fn new(repos_data: Vec<(Vec<u8>, AptRepo)>) -> Self {
Self {
repos_data,
current_repo_idx: 0,
current_reader: None,
current_repo: None,
current_builder: None,
done: false,
}
}
fn advance_to_next_repo(&mut self) -> bool {
if self.current_repo_idx >= self.repos_data.len() {
self.done = true;
return false;
}
let (data, repo) = &self.repos_data[self.current_repo_idx];
self.current_repo_idx += 1;
self.current_repo = Some(*repo);
let reader: Box<dyn BufRead + Send> =
if data.len() >= 2 && data[0] == 0x1f && data[1] == 0x8b {
let mut decoder = GzDecoder::new(Cursor::new(data.clone()));
let mut decompressed = Vec::new();
if decoder.read_to_end(&mut decompressed).is_ok() {
Box::new(BufReader::new(Cursor::new(decompressed)))
} else {
return self.advance_to_next_repo();
}
} else {
Box::new(BufReader::new(Cursor::new(data.clone())))
};
self.current_reader = Some(reader);
true
}
}
impl Iterator for AptPackageIter {
type Item = Result<PackageMeta, IndexError>;
fn next(&mut self) -> Option<Self::Item> {
loop {
if self.done {
return None;
}
if self.current_reader.is_none() && !self.advance_to_next_repo() {
return None;
}
let reader = self.current_reader.as_mut()?;
let repo = self.current_repo?;
let mut line = String::new();
match reader.read_line(&mut line) {
Ok(0) => {
if let Some(builder) = self.current_builder.take()
&& let Some(pkg) = builder.build(repo)
{
self.current_reader = None;
return Some(Ok(pkg));
}
if !self.advance_to_next_repo() {
return None;
}
continue;
}
Ok(_) => {
let line = line.trim_end_matches('\n');
if line.is_empty() {
if let Some(builder) = self.current_builder.take()
&& let Some(pkg) = builder.build(repo)
{
return Some(Ok(pkg));
}
continue;
}
if line.starts_with(' ') || line.starts_with('\t') {
continue;
}
if let Some((key, value)) = line.split_once(':') {
let key = key.trim();
let value = value.trim();
let builder = self.current_builder.get_or_insert_with(PackageBuilder::new);
match key {
"Package" => builder.name = Some(value.to_string()),
"Version" => builder.version = Some(value.to_string()),
"Description" => builder.description = Some(value.to_string()),
"Homepage" => builder.homepage = Some(value.to_string()),
"Vcs-Git" | "Vcs-Browser" => {
if builder.repository.is_none() {
builder.repository = Some(value.to_string());
}
}
"Filename" => builder.filename = Some(value.to_string()),
"SHA256" => builder.sha256 = Some(value.to_string()),
"Depends" => builder.depends = Some(value.to_string()),
"Provides" => builder.provides = Some(value.to_string()),
"Size" => builder.size = value.parse().ok(),
_ => {}
}
}
}
Err(e) => {
self.done = true;
return Some(Err(IndexError::Io(e)));
}
}
}
}
}
mod urlencoding {
pub fn encode(s: &str) -> String {
let mut result = String::with_capacity(s.len() * 3);
for c in s.chars() {
match c {
'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '.' | '~' => result.push(c),
_ => {
for b in c.to_string().bytes() {
result.push_str(&format!("%{:02X}", b));
}
}
}
}
result
}
}