use super::{IndexError, PackageIndex, 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 UBUNTU_MIRROR: &str = "https://archive.ubuntu.com/ubuntu";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum UbuntuRepo {
NobleMain,
NobleRestricted,
NobleUniverse,
NobleMultiverse,
NobleUpdatesMain,
NobleUpdatesUniverse,
NobleSecurityMain,
NobleSecurityUniverse,
NobleBackportsMain,
NobleBackportsUniverse,
JammyMain,
JammyRestricted,
JammyUniverse,
JammyMultiverse,
JammyUpdatesMain,
JammyUpdatesUniverse,
JammySecurityMain,
JammySecurityUniverse,
JammyBackportsMain,
JammyBackportsUniverse,
OracularMain,
OracularUniverse,
}
struct DistComponent {
dist: &'static str,
component: &'static str,
}
impl UbuntuRepo {
fn parts(&self) -> DistComponent {
let (dist, component) = match self {
Self::NobleMain => ("noble", "main"),
Self::NobleRestricted => ("noble", "restricted"),
Self::NobleUniverse => ("noble", "universe"),
Self::NobleMultiverse => ("noble", "multiverse"),
Self::NobleUpdatesMain => ("noble-updates", "main"),
Self::NobleUpdatesUniverse => ("noble-updates", "universe"),
Self::NobleSecurityMain => ("noble-security", "main"),
Self::NobleSecurityUniverse => ("noble-security", "universe"),
Self::NobleBackportsMain => ("noble-backports", "main"),
Self::NobleBackportsUniverse => ("noble-backports", "universe"),
Self::JammyMain => ("jammy", "main"),
Self::JammyRestricted => ("jammy", "restricted"),
Self::JammyUniverse => ("jammy", "universe"),
Self::JammyMultiverse => ("jammy", "multiverse"),
Self::JammyUpdatesMain => ("jammy-updates", "main"),
Self::JammyUpdatesUniverse => ("jammy-updates", "universe"),
Self::JammySecurityMain => ("jammy-security", "main"),
Self::JammySecurityUniverse => ("jammy-security", "universe"),
Self::JammyBackportsMain => ("jammy-backports", "main"),
Self::JammyBackportsUniverse => ("jammy-backports", "universe"),
Self::OracularMain => ("oracular", "main"),
Self::OracularUniverse => ("oracular", "universe"),
};
DistComponent { dist, component }
}
fn packages_url(&self) -> String {
let parts = self.parts();
format!(
"{}/dists/{}/{}/binary-amd64/Packages.gz",
UBUNTU_MIRROR, parts.dist, parts.component
)
}
pub fn name(&self) -> &'static str {
match self {
Self::NobleMain => "noble-main",
Self::NobleRestricted => "noble-restricted",
Self::NobleUniverse => "noble-universe",
Self::NobleMultiverse => "noble-multiverse",
Self::NobleUpdatesMain => "noble-updates-main",
Self::NobleUpdatesUniverse => "noble-updates-universe",
Self::NobleSecurityMain => "noble-security-main",
Self::NobleSecurityUniverse => "noble-security-universe",
Self::NobleBackportsMain => "noble-backports-main",
Self::NobleBackportsUniverse => "noble-backports-universe",
Self::JammyMain => "jammy-main",
Self::JammyRestricted => "jammy-restricted",
Self::JammyUniverse => "jammy-universe",
Self::JammyMultiverse => "jammy-multiverse",
Self::JammyUpdatesMain => "jammy-updates-main",
Self::JammyUpdatesUniverse => "jammy-updates-universe",
Self::JammySecurityMain => "jammy-security-main",
Self::JammySecurityUniverse => "jammy-security-universe",
Self::JammyBackportsMain => "jammy-backports-main",
Self::JammyBackportsUniverse => "jammy-backports-universe",
Self::OracularMain => "oracular-main",
Self::OracularUniverse => "oracular-universe",
}
}
pub fn all() -> &'static [UbuntuRepo] {
&[
Self::NobleMain,
Self::NobleRestricted,
Self::NobleUniverse,
Self::NobleMultiverse,
Self::NobleUpdatesMain,
Self::NobleUpdatesUniverse,
Self::NobleSecurityMain,
Self::NobleSecurityUniverse,
Self::NobleBackportsMain,
Self::NobleBackportsUniverse,
Self::JammyMain,
Self::JammyRestricted,
Self::JammyUniverse,
Self::JammyMultiverse,
Self::JammyUpdatesMain,
Self::JammyUpdatesUniverse,
Self::JammySecurityMain,
Self::JammySecurityUniverse,
Self::JammyBackportsMain,
Self::JammyBackportsUniverse,
Self::OracularMain,
Self::OracularUniverse,
]
}
pub fn noble() -> &'static [UbuntuRepo] {
&[
Self::NobleMain,
Self::NobleRestricted,
Self::NobleUniverse,
Self::NobleMultiverse,
Self::NobleUpdatesMain,
Self::NobleUpdatesUniverse,
Self::NobleSecurityMain,
Self::NobleSecurityUniverse,
Self::NobleBackportsMain,
Self::NobleBackportsUniverse,
]
}
pub fn jammy() -> &'static [UbuntuRepo] {
&[
Self::JammyMain,
Self::JammyRestricted,
Self::JammyUniverse,
Self::JammyMultiverse,
Self::JammyUpdatesMain,
Self::JammyUpdatesUniverse,
Self::JammySecurityMain,
Self::JammySecurityUniverse,
Self::JammyBackportsMain,
Self::JammyBackportsUniverse,
]
}
pub fn lts() -> &'static [UbuntuRepo] {
&[
Self::NobleMain,
Self::NobleUniverse,
Self::NobleUpdatesMain,
Self::NobleUpdatesUniverse,
Self::JammyMain,
Self::JammyUniverse,
Self::JammyUpdatesMain,
Self::JammyUpdatesUniverse,
]
}
pub fn main_only() -> &'static [UbuntuRepo] {
&[
Self::NobleMain,
Self::NobleUpdatesMain,
Self::NobleSecurityMain,
Self::JammyMain,
Self::JammyUpdatesMain,
Self::JammySecurityMain,
Self::OracularMain,
]
}
}
pub struct Ubuntu {
repos: Vec<UbuntuRepo>,
}
impl Ubuntu {
pub fn all() -> Self {
Self {
repos: UbuntuRepo::all().to_vec(),
}
}
pub fn noble() -> Self {
Self {
repos: UbuntuRepo::noble().to_vec(),
}
}
pub fn jammy() -> Self {
Self {
repos: UbuntuRepo::jammy().to_vec(),
}
}
pub fn lts() -> Self {
Self {
repos: UbuntuRepo::lts().to_vec(),
}
}
pub fn main_only() -> Self {
Self {
repos: UbuntuRepo::main_only().to_vec(),
}
}
pub fn with_repos(repos: &[UbuntuRepo]) -> Self {
Self {
repos: repos.to_vec(),
}
}
fn parse_control<R: Read>(reader: R, repo: UbuntuRepo) -> 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()),
"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: UbuntuRepo) -> Result<Vec<PackageMeta>, IndexError> {
let url = repo.packages_url();
let (data, _was_cached) = cache::fetch_with_cache(
"ubuntu",
&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 Ubuntu repo: {}", e);
}
}
}
Ok(packages)
}
}
impl PackageIndex for Ubuntu {
fn ecosystem(&self) -> &'static str {
"ubuntu"
}
fn display_name(&self) -> &'static str {
"Ubuntu"
}
fn fetch(&self, name: &str) -> Result<PackageMeta, IndexError> {
let url = format!(
"https://api.launchpad.net/1.0/ubuntu/+archive/primary?ws.op=getPublishedSources&source_name={}&exact_match=true",
urlencoding::encode(name)
);
let response: serde_json::Value = ureq::get(&url).call()?.into_json()?;
let entries = response["entries"]
.as_array()
.ok_or_else(|| IndexError::NotFound(name.to_string()))?;
let latest = entries
.first()
.ok_or_else(|| IndexError::NotFound(name.to_string()))?;
Ok(PackageMeta {
name: latest["source_package_name"]
.as_str()
.unwrap_or(name)
.to_string(),
version: latest["source_package_version"]
.as_str()
.unwrap_or("unknown")
.to_string(),
description: None,
homepage: None,
repository: None,
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://api.launchpad.net/1.0/ubuntu/+archive/primary?ws.op=getPublishedSources&source_name={}&exact_match=true",
urlencoding::encode(name)
);
let response: serde_json::Value = ureq::get(&url).call()?.into_json()?;
let entries = response["entries"]
.as_array()
.ok_or_else(|| IndexError::NotFound(name.to_string()))?;
if entries.is_empty() {
return Err(IndexError::NotFound(name.to_string()));
}
Ok(entries
.iter()
.filter_map(|e| {
Some(VersionMeta {
version: e["source_package_version"].as_str()?.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 search(&self, query: &str) -> Result<Vec<PackageMeta>, IndexError> {
let packages = self.load_packages()?;
let query_lower = query.to_lowercase();
Ok(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())
}
}
#[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>,
size: Option<u64>,
}
impl PackageBuilder {
fn new() -> Self {
Self::default()
}
fn build(self, repo: UbuntuRepo) -> 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(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(),
archive_url: self.filename.map(|f| format!("{}/{}", UBUNTU_MIRROR, f)),
checksum: self.sha256.map(|h| format!("sha256:{}", h)),
keywords: Vec::new(),
maintainers: Vec::new(),
published: None,
downloads: None,
extra,
})
}
}
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
}
}