use super::arch_common;
use super::{IndexError, PackageIndex, PackageMeta, VersionMeta};
use crate::cache;
use flate2::read::GzDecoder;
use rayon::prelude::*;
use std::collections::HashMap;
use std::io::{Cursor, Read};
use std::time::Duration;
use tar::Archive;
const CACHE_TTL: Duration = Duration::from_secs(60 * 60);
const MANJARO_MIRROR: &str = "https://repo.manjaro.org/repo";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ManjaroRepo {
StableCore,
StableExtra,
StableMultilib,
TestingCore,
TestingExtra,
TestingMultilib,
UnstableCore,
UnstableExtra,
UnstableMultilib,
Aur,
}
impl ManjaroRepo {
fn parts(&self) -> Option<(&'static str, &'static str)> {
match self {
Self::StableCore => Some(("stable", "core")),
Self::StableExtra => Some(("stable", "extra")),
Self::StableMultilib => Some(("stable", "multilib")),
Self::TestingCore => Some(("testing", "core")),
Self::TestingExtra => Some(("testing", "extra")),
Self::TestingMultilib => Some(("testing", "multilib")),
Self::UnstableCore => Some(("unstable", "core")),
Self::UnstableExtra => Some(("unstable", "extra")),
Self::UnstableMultilib => Some(("unstable", "multilib")),
Self::Aur => None,
}
}
fn db_url(&self) -> Option<String> {
let (branch, repo) = self.parts()?;
Some(format!(
"{}/{}/{}/x86_64/{}.db",
MANJARO_MIRROR, branch, repo, repo
))
}
pub fn name(&self) -> &'static str {
match self {
Self::StableCore => "stable-core",
Self::StableExtra => "stable-extra",
Self::StableMultilib => "stable-multilib",
Self::TestingCore => "testing-core",
Self::TestingExtra => "testing-extra",
Self::TestingMultilib => "testing-multilib",
Self::UnstableCore => "unstable-core",
Self::UnstableExtra => "unstable-extra",
Self::UnstableMultilib => "unstable-multilib",
Self::Aur => "aur",
}
}
pub fn all() -> &'static [ManjaroRepo] {
&[
Self::StableCore,
Self::StableExtra,
Self::StableMultilib,
Self::TestingCore,
Self::TestingExtra,
Self::TestingMultilib,
Self::UnstableCore,
Self::UnstableExtra,
Self::UnstableMultilib,
Self::Aur,
]
}
pub fn stable() -> &'static [ManjaroRepo] {
&[
Self::StableCore,
Self::StableExtra,
Self::StableMultilib,
Self::Aur,
]
}
pub fn testing() -> &'static [ManjaroRepo] {
&[Self::TestingCore, Self::TestingExtra, Self::TestingMultilib]
}
pub fn unstable() -> &'static [ManjaroRepo] {
&[
Self::UnstableCore,
Self::UnstableExtra,
Self::UnstableMultilib,
]
}
pub fn official() -> &'static [ManjaroRepo] {
&[
Self::StableCore,
Self::StableExtra,
Self::StableMultilib,
Self::TestingCore,
Self::TestingExtra,
Self::TestingMultilib,
Self::UnstableCore,
Self::UnstableExtra,
Self::UnstableMultilib,
]
}
}
pub struct Manjaro {
repos: Vec<ManjaroRepo>,
}
impl Manjaro {
pub fn all() -> Self {
Self {
repos: ManjaroRepo::all().to_vec(),
}
}
pub fn stable() -> Self {
Self {
repos: ManjaroRepo::stable().to_vec(),
}
}
pub fn testing() -> Self {
Self {
repos: ManjaroRepo::testing().to_vec(),
}
}
pub fn unstable() -> Self {
Self {
repos: ManjaroRepo::unstable().to_vec(),
}
}
pub fn official() -> Self {
Self {
repos: ManjaroRepo::official().to_vec(),
}
}
pub fn with_repos(repos: &[ManjaroRepo]) -> Self {
Self {
repos: repos.to_vec(),
}
}
fn load_repo(repo: ManjaroRepo) -> Result<Vec<PackageMeta>, IndexError> {
if repo == ManjaroRepo::Aur {
return arch_common::fetch_all_aur();
}
let url = repo
.db_url()
.ok_or_else(|| IndexError::NotFound(format!("no db URL for {}", repo.name())))?;
let (data, _was_cached) =
cache::fetch_with_cache("manjaro", &format!("{}-db", repo.name()), &url, CACHE_TTL)
.map_err(IndexError::Network)?;
Self::parse_db(&data, repo)
}
fn parse_db(data: &[u8], repo: ManjaroRepo) -> Result<Vec<PackageMeta>, IndexError> {
let tar_data = if data.len() >= 2 && data[0] == 0x1f && data[1] == 0x8b {
let mut decoder = GzDecoder::new(Cursor::new(data));
let mut decompressed = Vec::new();
decoder
.read_to_end(&mut decompressed)
.map_err(IndexError::Io)?;
decompressed
} else {
data.to_vec()
};
let mut archive = Archive::new(Cursor::new(tar_data));
let mut packages = Vec::new();
for entry in archive.entries().map_err(IndexError::Io)? {
let mut entry = entry.map_err(IndexError::Io)?;
let path = entry
.path()
.map_err(IndexError::Io)?
.to_string_lossy()
.to_string();
if !path.ends_with("/desc") {
continue;
}
let mut content = String::new();
entry.read_to_string(&mut content).map_err(IndexError::Io)?;
if let Some(pkg) = Self::parse_desc(&content, repo) {
packages.push(pkg);
}
}
Ok(packages)
}
fn parse_desc(content: &str, repo: ManjaroRepo) -> Option<PackageMeta> {
let mut fields: HashMap<String, String> = HashMap::new();
let mut current_key: Option<String> = None;
let mut current_value = String::new();
for line in content.lines() {
if line.starts_with('%') && line.ends_with('%') {
if let Some(key) = current_key.take() {
fields.insert(key, current_value.trim().to_string());
}
current_key = Some(line[1..line.len() - 1].to_string());
current_value.clear();
} else if current_key.is_some() {
if !current_value.is_empty() {
current_value.push('\n');
}
current_value.push_str(line);
}
}
if let Some(key) = current_key {
fields.insert(key, current_value.trim().to_string());
}
let name = fields.get("NAME")?.clone();
let version = fields.get("VERSION")?.clone();
let mut extra = HashMap::new();
if let Some(deps) = fields.get("DEPENDS") {
let parsed_deps: Vec<serde_json::Value> = deps
.lines()
.filter(|l| !l.is_empty())
.map(|d| {
let name = d.split(['>', '<', '=', ':']).next().unwrap_or(d);
serde_json::Value::String(name.to_string())
})
.collect();
if !parsed_deps.is_empty() {
extra.insert("depends".to_string(), serde_json::Value::Array(parsed_deps));
}
}
if let Some(size) = fields.get("CSIZE").and_then(|s| s.parse::<u64>().ok()) {
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()),
);
let filename = fields.get("FILENAME")?;
let (branch, repo_name) = repo.parts()?;
let archive_url = Some(format!(
"{}/{}/{}/x86_64/{}",
MANJARO_MIRROR, branch, repo_name, filename
));
let checksum = fields
.get("SHA256SUM")
.map(|s| format!("sha256:{}", s))
.or_else(|| fields.get("MD5SUM").map(|s| format!("md5:{}", s)));
Some(PackageMeta {
name,
version,
description: fields.get("DESC").cloned(),
homepage: fields.get("URL").cloned(),
repository: None,
license: fields.get("LICENSE").cloned(),
binaries: Vec::new(),
keywords: Vec::new(),
maintainers: Vec::new(),
published: None,
downloads: None,
archive_url,
checksum,
extra,
})
}
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 Manjaro repo: {}", e);
}
}
}
Ok(packages)
}
}
impl PackageIndex for Manjaro {
fn ecosystem(&self) -> &'static str {
"manjaro"
}
fn display_name(&self) -> &'static str {
"Manjaro Linux"
}
fn fetch(&self, name: &str) -> Result<PackageMeta, IndexError> {
let packages = self.load_packages()?;
packages
.into_iter()
.find(|p| p.name == 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 versions: Vec<_> = packages
.into_iter()
.filter(|p| p.name == name)
.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 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(|p| {
p.name.to_lowercase().contains(&query_lower)
|| p.description
.as_ref()
.map(|d| d.to_lowercase().contains(&query_lower))
.unwrap_or(false)
})
.collect())
}
}