use std::collections::{HashMap, HashSet};
use std::sync::{OnceLock, RwLock};
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Default)]
pub struct OfficialIndex {
pub pkgs: Vec<OfficialPkg>,
#[serde(skip)]
pub name_to_idx: HashMap<String, usize>,
}
impl OfficialIndex {
pub fn rebuild_name_index(&mut self) {
self.name_to_idx.clear();
self.name_to_idx.reserve(self.pkgs.len());
for (i, pkg) in self.pkgs.iter().enumerate() {
self.name_to_idx.insert(pkg.name.to_lowercase(), i);
}
}
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct OfficialPkg {
pub name: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub repo: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub arch: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub version: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub description: String,
}
static OFFICIAL_INDEX: OnceLock<RwLock<OfficialIndex>> = OnceLock::new();
static INSTALLED_SET: OnceLock<RwLock<HashSet<String>>> = OnceLock::new();
static EXPLICIT_SET: OnceLock<RwLock<HashSet<String>>> = OnceLock::new();
mod distro;
pub use distro::{
is_artix_galaxy, is_artix_lib32, is_artix_omniverse, is_artix_repo, is_artix_system,
is_artix_universe, is_artix_world, is_blackarch_repo, is_cachyos_repo, is_eos_name,
is_eos_repo, is_manjaro_name_or_owner, is_name_manjaro,
};
fn idx() -> &'static RwLock<OfficialIndex> {
OFFICIAL_INDEX.get_or_init(|| {
RwLock::new(OfficialIndex {
pkgs: Vec::new(),
name_to_idx: HashMap::new(),
})
})
}
fn installed_lock() -> &'static RwLock<HashSet<String>> {
INSTALLED_SET.get_or_init(|| RwLock::new(HashSet::new()))
}
fn explicit_lock() -> &'static RwLock<HashSet<String>> {
EXPLICIT_SET.get_or_init(|| RwLock::new(HashSet::new()))
}
mod enrich;
mod explicit;
mod fetch;
mod installed;
mod persist;
mod query;
#[cfg(windows)]
mod mirrors;
mod update;
pub use enrich::*;
pub use explicit::*;
pub use installed::*;
#[cfg(windows)]
pub use mirrors::*;
pub use persist::*;
pub use query::*;
#[cfg(not(windows))]
pub use update::update_in_background;
#[must_use]
pub fn find_package_by_name(name: &str) -> Option<crate::state::PackageItem> {
use crate::state::{PackageItem, Source};
if let Ok(g) = idx().read() {
let name_lower = name.to_lowercase();
if let Some(&idx) = g.name_to_idx.get(&name_lower)
&& let Some(p) = g.pkgs.get(idx)
{
return Some(PackageItem {
name: p.name.clone(),
version: p.version.clone(),
description: p.description.clone(),
source: Source::Official {
repo: p.repo.clone(),
arch: p.arch.clone(),
},
popularity: None,
out_of_date: None,
orphaned: false,
});
}
for p in &g.pkgs {
if p.name.eq_ignore_ascii_case(name) {
return Some(PackageItem {
name: p.name.clone(),
version: p.version.clone(),
description: p.description.clone(),
source: Source::Official {
repo: p.repo.clone(),
arch: p.arch.clone(),
},
popularity: None,
out_of_date: None,
orphaned: false,
});
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rebuild_name_index_populates_hashmap() {
let mut index = OfficialIndex {
pkgs: vec![
OfficialPkg {
name: "PackageA".to_string(),
repo: "core".to_string(),
arch: "x86_64".to_string(),
version: "1.0".to_string(),
description: "Desc A".to_string(),
},
OfficialPkg {
name: "PackageB".to_string(),
repo: "extra".to_string(),
arch: "any".to_string(),
version: "2.0".to_string(),
description: "Desc B".to_string(),
},
],
name_to_idx: HashMap::new(),
};
index.rebuild_name_index();
assert_eq!(index.name_to_idx.len(), 2);
assert_eq!(index.name_to_idx.get("packagea"), Some(&0));
assert_eq!(index.name_to_idx.get("packageb"), Some(&1));
assert_eq!(index.name_to_idx.get("PackageA"), None);
}
#[test]
fn find_package_by_name_uses_hashmap() {
let _guard = crate::global_test_mutex_lock();
if let Ok(mut g) = idx().write() {
g.pkgs = vec![
OfficialPkg {
name: "ripgrep".to_string(),
repo: "extra".to_string(),
arch: "x86_64".to_string(),
version: "14.0.0".to_string(),
description: "Fast grep".to_string(),
},
OfficialPkg {
name: "vim".to_string(),
repo: "extra".to_string(),
arch: "x86_64".to_string(),
version: "9.0".to_string(),
description: "Text editor".to_string(),
},
];
g.rebuild_name_index();
}
let result = find_package_by_name("ripgrep");
assert!(result.is_some());
assert_eq!(result.as_ref().map(|p| p.name.as_str()), Some("ripgrep"));
let result_upper = find_package_by_name("RIPGREP");
assert!(result_upper.is_some());
assert_eq!(
result_upper.as_ref().map(|p| p.name.as_str()),
Some("ripgrep")
);
let not_found = find_package_by_name("nonexistent");
assert!(not_found.is_none());
}
}