use super::{IndexError, PackageIndex, PackageMeta, VersionMeta};
use crate::cache;
use rayon::prelude::*;
use std::collections::HashMap;
use std::time::Duration;
const CACHE_TTL: Duration = Duration::from_secs(60 * 60);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum OpenSuseRepo {
TumbleweedOss,
TumbleweedNonOss,
TumbleweedUpdate,
Leap160Oss,
Leap160NonOss,
Leap156Oss,
Leap156NonOss,
Leap156UpdateOss,
Leap156UpdateNonOss,
Leap156Backports,
Leap156Sle,
TumbleweedSrcOss,
TumbleweedSrcNonOss,
Leap156SrcOss,
Leap156SrcNonOss,
TumbleweedDebug,
Leap160Debug,
Leap156Debug,
Leap156DebugUpdate,
FactoryOss,
GamesTumbleweed,
GamesLeap156,
KdeExtraTumbleweed,
KdeFrameworksTumbleweed,
KdeQt6Tumbleweed,
GnomeAppsLeap160,
GnomeAppsLeap156,
GnomeFactory,
XfceTumbleweed,
XfceLeap160,
XfceLeap156,
MozillaTumbleweed,
ScienceTumbleweed,
WineTumbleweed,
ServerHttpTumbleweed,
ServerDatabaseTumbleweed,
}
impl OpenSuseRepo {
fn id(&self) -> &'static str {
match self {
Self::TumbleweedOss => "tumbleweed-oss",
Self::TumbleweedNonOss => "tumbleweed-non-oss",
Self::TumbleweedUpdate => "tumbleweed-update",
Self::Leap160Oss => "leap-16.0-oss",
Self::Leap160NonOss => "leap-16.0-non-oss",
Self::Leap156Oss => "leap-15.6-oss",
Self::Leap156NonOss => "leap-15.6-non-oss",
Self::Leap156UpdateOss => "leap-15.6-update-oss",
Self::Leap156UpdateNonOss => "leap-15.6-update-non-oss",
Self::Leap156Backports => "leap-15.6-backports",
Self::Leap156Sle => "leap-15.6-sle",
Self::TumbleweedSrcOss => "tumbleweed-src-oss",
Self::TumbleweedSrcNonOss => "tumbleweed-src-non-oss",
Self::Leap156SrcOss => "leap-15.6-src-oss",
Self::Leap156SrcNonOss => "leap-15.6-src-non-oss",
Self::TumbleweedDebug => "tumbleweed-debug",
Self::Leap160Debug => "leap-16.0-debug",
Self::Leap156Debug => "leap-15.6-debug",
Self::Leap156DebugUpdate => "leap-15.6-debug-update",
Self::FactoryOss => "factory-oss",
Self::GamesTumbleweed => "games-tumbleweed",
Self::GamesLeap156 => "games-leap-15.6",
Self::KdeExtraTumbleweed => "kde-extra-tumbleweed",
Self::KdeFrameworksTumbleweed => "kde-frameworks-tumbleweed",
Self::KdeQt6Tumbleweed => "kde-qt6-tumbleweed",
Self::GnomeAppsLeap160 => "gnome-apps-leap-16.0",
Self::GnomeAppsLeap156 => "gnome-apps-leap-15.6",
Self::GnomeFactory => "gnome-factory",
Self::XfceTumbleweed => "xfce-tumbleweed",
Self::XfceLeap160 => "xfce-leap-16.0",
Self::XfceLeap156 => "xfce-leap-15.6",
Self::MozillaTumbleweed => "mozilla-tumbleweed",
Self::ScienceTumbleweed => "science-tumbleweed",
Self::WineTumbleweed => "wine-tumbleweed",
Self::ServerHttpTumbleweed => "server-http-tumbleweed",
Self::ServerDatabaseTumbleweed => "server-database-tumbleweed",
}
}
fn base_url(&self) -> &'static str {
match self {
Self::TumbleweedOss => "https://download.opensuse.org/tumbleweed/repo/oss/repodata",
Self::TumbleweedNonOss => {
"https://download.opensuse.org/tumbleweed/repo/non-oss/repodata"
}
Self::TumbleweedUpdate => "https://download.opensuse.org/update/tumbleweed/repodata",
Self::Leap160Oss => {
"https://download.opensuse.org/distribution/leap/16.0/repo/oss/repodata"
}
Self::Leap160NonOss => {
"https://download.opensuse.org/distribution/leap/16.0/repo/non-oss/repodata"
}
Self::Leap156Oss => {
"https://download.opensuse.org/distribution/leap/15.6/repo/oss/repodata"
}
Self::Leap156NonOss => {
"https://download.opensuse.org/distribution/leap/15.6/repo/non-oss/repodata"
}
Self::Leap156UpdateOss => "https://download.opensuse.org/update/leap/15.6/oss/repodata",
Self::Leap156UpdateNonOss => {
"https://download.opensuse.org/update/leap/15.6/non-oss/repodata"
}
Self::Leap156Backports => {
"https://download.opensuse.org/update/leap/15.6/backports/repodata"
}
Self::Leap156Sle => "https://download.opensuse.org/update/leap/15.6/sle/repodata",
Self::TumbleweedSrcOss => {
"https://download.opensuse.org/tumbleweed/repo/src-oss/repodata"
}
Self::TumbleweedSrcNonOss => {
"https://download.opensuse.org/tumbleweed/repo/src-non-oss/repodata"
}
Self::Leap156SrcOss => {
"https://download.opensuse.org/source/distribution/leap/15.6/repo/oss/repodata"
}
Self::Leap156SrcNonOss => {
"https://download.opensuse.org/source/distribution/leap/15.6/repo/non-oss/repodata"
}
Self::TumbleweedDebug => {
"https://download.opensuse.org/debug/tumbleweed/repo/oss/repodata"
}
Self::Leap160Debug => {
"https://download.opensuse.org/debug/distribution/leap/16.0/repo/oss/repodata"
}
Self::Leap156Debug => {
"https://download.opensuse.org/debug/distribution/leap/15.6/repo/oss/repodata"
}
Self::Leap156DebugUpdate => {
"https://download.opensuse.org/debug/update/leap/15.6/oss/repodata"
}
Self::FactoryOss => "https://download.opensuse.org/factory/repo/oss/repodata",
Self::GamesTumbleweed => {
"https://download.opensuse.org/repositories/games/openSUSE_Tumbleweed/repodata"
}
Self::GamesLeap156 => "https://download.opensuse.org/repositories/games/15.6/repodata",
Self::KdeExtraTumbleweed => {
"https://download.opensuse.org/repositories/KDE:/Extra/openSUSE_Tumbleweed/repodata"
}
Self::KdeFrameworksTumbleweed => {
"https://download.opensuse.org/repositories/KDE:/Frameworks/openSUSE_Tumbleweed/repodata"
}
Self::KdeQt6Tumbleweed => {
"https://download.opensuse.org/repositories/KDE:/Qt6/openSUSE_Tumbleweed/repodata"
}
Self::GnomeAppsLeap160 => {
"https://download.opensuse.org/repositories/GNOME:/Apps/16.0/repodata"
}
Self::GnomeAppsLeap156 => {
"https://download.opensuse.org/repositories/GNOME:/Apps/15.6/repodata"
}
Self::GnomeFactory => {
"https://download.opensuse.org/repositories/GNOME:/Factory/openSUSE_Factory/repodata"
}
Self::XfceTumbleweed => {
"https://download.opensuse.org/repositories/X11:/xfce/openSUSE_Tumbleweed/repodata"
}
Self::XfceLeap160 => {
"https://download.opensuse.org/repositories/X11:/xfce/16.0/repodata"
}
Self::XfceLeap156 => {
"https://download.opensuse.org/repositories/X11:/xfce/15.6/repodata"
}
Self::MozillaTumbleweed => {
"https://download.opensuse.org/repositories/mozilla/openSUSE_Tumbleweed/repodata"
}
Self::ScienceTumbleweed => {
"https://download.opensuse.org/repositories/science/openSUSE_Tumbleweed/repodata"
}
Self::WineTumbleweed => {
"https://download.opensuse.org/repositories/Emulators:/Wine/openSUSE_Tumbleweed/repodata"
}
Self::ServerHttpTumbleweed => {
"https://download.opensuse.org/repositories/server:/http/openSUSE_Tumbleweed/repodata"
}
Self::ServerDatabaseTumbleweed => {
"https://download.opensuse.org/repositories/server:/database/openSUSE_Tumbleweed/repodata"
}
}
}
pub fn all() -> &'static [OpenSuseRepo] {
&[
Self::TumbleweedOss,
Self::TumbleweedNonOss,
Self::TumbleweedUpdate,
Self::Leap160Oss,
Self::Leap160NonOss,
Self::Leap156Oss,
Self::Leap156NonOss,
Self::Leap156UpdateOss,
Self::Leap156UpdateNonOss,
Self::Leap156Backports,
Self::Leap156Sle,
Self::TumbleweedSrcOss,
Self::TumbleweedSrcNonOss,
Self::Leap156SrcOss,
Self::Leap156SrcNonOss,
Self::TumbleweedDebug,
Self::Leap160Debug,
Self::Leap156Debug,
Self::Leap156DebugUpdate,
Self::FactoryOss,
Self::GamesTumbleweed,
Self::GamesLeap156,
Self::KdeExtraTumbleweed,
Self::KdeFrameworksTumbleweed,
Self::KdeQt6Tumbleweed,
Self::GnomeAppsLeap160,
Self::GnomeAppsLeap156,
Self::GnomeFactory,
Self::XfceTumbleweed,
Self::XfceLeap160,
Self::XfceLeap156,
Self::MozillaTumbleweed,
Self::ScienceTumbleweed,
Self::WineTumbleweed,
Self::ServerHttpTumbleweed,
Self::ServerDatabaseTumbleweed,
]
}
pub fn official() -> &'static [OpenSuseRepo] {
&[
Self::TumbleweedOss,
Self::TumbleweedNonOss,
Self::TumbleweedUpdate,
Self::Leap160Oss,
Self::Leap160NonOss,
Self::Leap156Oss,
Self::Leap156NonOss,
Self::Leap156UpdateOss,
Self::Leap156UpdateNonOss,
Self::Leap156Backports,
Self::Leap156Sle,
Self::TumbleweedSrcOss,
Self::TumbleweedSrcNonOss,
Self::Leap156SrcOss,
Self::Leap156SrcNonOss,
Self::TumbleweedDebug,
Self::Leap160Debug,
Self::Leap156Debug,
Self::Leap156DebugUpdate,
Self::FactoryOss,
]
}
pub fn binary_only() -> &'static [OpenSuseRepo] {
&[
Self::TumbleweedOss,
Self::TumbleweedNonOss,
Self::TumbleweedUpdate,
Self::Leap160Oss,
Self::Leap160NonOss,
Self::Leap156Oss,
Self::Leap156NonOss,
Self::Leap156UpdateOss,
Self::Leap156UpdateNonOss,
Self::Leap156Backports,
Self::Leap156Sle,
Self::FactoryOss,
]
}
pub fn tumbleweed() -> &'static [OpenSuseRepo] {
&[
Self::TumbleweedOss,
Self::TumbleweedNonOss,
Self::TumbleweedUpdate,
]
}
pub fn leap_15_6() -> &'static [OpenSuseRepo] {
&[
Self::Leap156Oss,
Self::Leap156NonOss,
Self::Leap156UpdateOss,
Self::Leap156UpdateNonOss,
Self::Leap156Backports,
Self::Leap156Sle,
]
}
pub fn leap_16_0() -> &'static [OpenSuseRepo] {
&[Self::Leap160Oss, Self::Leap160NonOss]
}
}
pub struct OpenSuse {
repos: Vec<OpenSuseRepo>,
}
impl Default for OpenSuse {
fn default() -> Self {
Self::all()
}
}
impl OpenSuse {
pub fn all() -> Self {
Self {
repos: OpenSuseRepo::all().to_vec(),
}
}
pub fn with_repos(repos: &[OpenSuseRepo]) -> Self {
Self {
repos: repos.to_vec(),
}
}
pub fn tumbleweed() -> Self {
Self {
repos: OpenSuseRepo::tumbleweed().to_vec(),
}
}
pub fn leap_15_6() -> Self {
Self {
repos: OpenSuseRepo::leap_15_6().to_vec(),
}
}
pub fn leap_16_0() -> Self {
Self {
repos: OpenSuseRepo::leap_16_0().to_vec(),
}
}
fn find_primary_url(repo: OpenSuseRepo) -> Result<String, IndexError> {
let repomd_url = format!("{}/repomd.xml", repo.base_url());
let cache_key = format!("repomd-{}", repo.id());
let (data, _) = cache::fetch_with_cache("opensuse", &cache_key, &repomd_url, CACHE_TTL)
.map_err(IndexError::Network)?;
let xml = String::from_utf8_lossy(&data);
for line in xml.lines() {
if (line.contains("primary.xml.zst") || line.contains("primary.xml.gz"))
&& let Some(start) = line.find("href=\"")
{
let rest = &line[start + 6..];
if let Some(end) = rest.find('"') {
let href = &rest[..end];
let base = repo.base_url().trim_end_matches("/repodata");
return Ok(format!("{}/{}", base, href));
}
}
}
Err(IndexError::Parse(format!(
"primary.xml not found in repomd.xml for {}",
repo.id()
)))
}
fn parse_primary(xml: &str, repo_id: &str) -> Vec<PackageMeta> {
let mut packages = Vec::new();
let mut in_package = false;
let mut name = String::new();
let mut version = String::new();
let mut release = String::new();
let mut summary = String::new();
let mut url = String::new();
let mut license = String::new();
for line in xml.lines() {
let line = line.trim();
if line.starts_with("<package type=\"rpm\">") {
in_package = true;
name.clear();
version.clear();
release.clear();
summary.clear();
url.clear();
license.clear();
} else if line == "</package>" && in_package {
if !name.is_empty() {
let mut extra = HashMap::new();
extra.insert(
"source_repo".to_string(),
serde_json::Value::String(repo_id.to_string()),
);
let full_version = if release.is_empty() {
version.clone()
} else {
format!("{}-{}", version, release)
};
packages.push(PackageMeta {
name: name.clone(),
version: full_version,
description: if summary.is_empty() {
None
} else {
Some(summary.clone())
},
homepage: if url.is_empty() {
None
} else {
Some(url.clone())
},
repository: Some(
"https://build.opensuse.org/project/show/openSUSE:Factory".to_string(),
),
license: if license.is_empty() {
None
} else {
Some(license.clone())
},
binaries: Vec::new(),
keywords: Vec::new(),
maintainers: Vec::new(),
published: None,
downloads: None,
archive_url: None,
checksum: None,
extra,
});
}
in_package = false;
} else if in_package {
if line.starts_with("<name>") && line.ends_with("</name>") {
name = line[6..line.len() - 7].to_string();
} else if line.starts_with("<summary>") && line.ends_with("</summary>") {
summary = line[9..line.len() - 10].to_string();
} else if line.starts_with("<url>") && line.ends_with("</url>") {
url = line[5..line.len() - 6].to_string();
} else if line.starts_with("<rpm:license>") && line.ends_with("</rpm:license>") {
license = line[13..line.len() - 14].to_string();
} else if line.starts_with("<version ") {
if let Some(ver_start) = line.find("ver=\"") {
let rest = &line[ver_start + 5..];
if let Some(ver_end) = rest.find('"') {
version = rest[..ver_end].to_string();
}
}
if let Some(rel_start) = line.find("rel=\"") {
let rest = &line[rel_start + 5..];
if let Some(rel_end) = rest.find('"') {
release = rest[..rel_end].to_string();
}
}
}
}
}
packages
}
fn load_repo(repo: OpenSuseRepo) -> Result<Vec<PackageMeta>, IndexError> {
let primary_url = Self::find_primary_url(repo)?;
let cache_key = format!("primary-{}", repo.id());
let (data, _was_cached) =
cache::fetch_with_cache("opensuse", &cache_key, &primary_url, CACHE_TTL)
.map_err(IndexError::Network)?;
let decompressed = if primary_url.ends_with(".zst") {
zstd::decode_all(std::io::Cursor::new(&data))
.map_err(|e| IndexError::Decompress(e.to_string()))?
} else {
use flate2::read::GzDecoder;
use std::io::Read;
let mut decoder = GzDecoder::new(&data[..]);
let mut decompressed = Vec::new();
decoder
.read_to_end(&mut decompressed)
.map_err(|e| IndexError::Decompress(e.to_string()))?;
decompressed
};
let xml = String::from_utf8_lossy(&decompressed);
Ok(Self::parse_primary(&xml, repo.id()))
}
fn load_packages(&self) -> Result<Vec<PackageMeta>, IndexError> {
let results: Vec<_> = self
.repos
.par_iter()
.map(|&repo| Self::load_repo(repo))
.collect();
let mut all_packages = Vec::new();
for (repo, result) in self.repos.iter().zip(results) {
match result {
Ok(packages) => {
all_packages.extend(packages);
}
Err(e) => {
tracing::warn!("failed to load openSUSE repo {}: {}", repo.id(), e);
}
}
}
if all_packages.is_empty() {
return Err(IndexError::Network(
"failed to load any openSUSE repos".into(),
));
}
Ok(all_packages)
}
}
impl PackageIndex for OpenSuse {
fn ecosystem(&self) -> &'static str {
"opensuse"
}
fn display_name(&self) -> &'static str {
"openSUSE (zypper)"
}
fn fetch(&self, name: &str) -> Result<PackageMeta, IndexError> {
let packages = self.load_packages()?;
packages
.into_iter()
.find(|p| p.name.eq_ignore_ascii_case(name))
.ok_or_else(|| IndexError::NotFound(name.to_string()))
}
fn fetch_versions(&self, name: &str) -> Result<Vec<VersionMeta>, IndexError> {
let packages = self.load_packages()?;
let name_lower = name.to_lowercase();
let versions: Vec<VersionMeta> = packages
.into_iter()
.filter(|p| p.name.to_lowercase() == name_lower)
.map(|p| VersionMeta {
version: p.version,
released: None,
yanked: false,
})
.collect();
if versions.is_empty() {
return Err(IndexError::NotFound(name.to_string()));
}
Ok(versions)
}
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(|p| {
p.name.to_lowercase().contains(&query_lower)
|| p.description
.as_ref()
.map(|d| d.to_lowercase().contains(&query_lower))
.unwrap_or(false)
})
.collect())
}
fn fetch_all(&self) -> Result<Vec<PackageMeta>, IndexError> {
self.load_packages()
}
}