normalize_package_index/index/
void.rs1use super::{IndexError, PackageIndex, PackageMeta, VersionMeta};
26use crate::cache;
27use rayon::prelude::*;
28use std::collections::HashMap;
29use std::io::Read;
30use std::time::Duration;
31
32const CACHE_TTL: Duration = Duration::from_secs(60 * 60);
34
35const VOID_MIRROR: &str = "https://repo-default.voidlinux.org/current";
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
40pub enum VoidRepo {
41 X86_64,
44 X86_64Nonfree,
46
47 X86_64Musl,
50 X86_64MuslNonfree,
52
53 Aarch64,
56 Aarch64Nonfree,
58
59 Aarch64Musl,
62 Aarch64MuslNonfree,
64}
65
66impl VoidRepo {
67 fn url(&self) -> String {
69 match self {
70 Self::X86_64 => format!("{}/x86_64-repodata", VOID_MIRROR),
71 Self::X86_64Nonfree => format!("{}/nonfree/x86_64-repodata", VOID_MIRROR),
72 Self::X86_64Musl => format!("{}/musl/x86_64-repodata", VOID_MIRROR),
73 Self::X86_64MuslNonfree => format!("{}/musl/nonfree/x86_64-repodata", VOID_MIRROR),
74 Self::Aarch64 => format!("{}/aarch64-repodata", VOID_MIRROR),
75 Self::Aarch64Nonfree => format!("{}/nonfree/aarch64-repodata", VOID_MIRROR),
76 Self::Aarch64Musl => format!("{}/musl/aarch64-repodata", VOID_MIRROR),
77 Self::Aarch64MuslNonfree => format!("{}/musl/nonfree/aarch64-repodata", VOID_MIRROR),
78 }
79 }
80
81 pub fn name(&self) -> &'static str {
83 match self {
84 Self::X86_64 => "x86_64",
85 Self::X86_64Nonfree => "x86_64-nonfree",
86 Self::X86_64Musl => "x86_64-musl",
87 Self::X86_64MuslNonfree => "x86_64-musl-nonfree",
88 Self::Aarch64 => "aarch64",
89 Self::Aarch64Nonfree => "aarch64-nonfree",
90 Self::Aarch64Musl => "aarch64-musl",
91 Self::Aarch64MuslNonfree => "aarch64-musl-nonfree",
92 }
93 }
94
95 pub fn all() -> &'static [VoidRepo] {
97 &[
98 Self::X86_64,
99 Self::X86_64Nonfree,
100 Self::X86_64Musl,
101 Self::X86_64MuslNonfree,
102 Self::Aarch64,
103 Self::Aarch64Nonfree,
104 Self::Aarch64Musl,
105 Self::Aarch64MuslNonfree,
106 ]
107 }
108
109 pub fn x86_64() -> &'static [VoidRepo] {
111 &[Self::X86_64, Self::X86_64Nonfree]
112 }
113
114 pub fn x86_64_musl() -> &'static [VoidRepo] {
116 &[Self::X86_64Musl, Self::X86_64MuslNonfree]
117 }
118
119 pub fn musl() -> &'static [VoidRepo] {
121 &[
122 Self::X86_64Musl,
123 Self::X86_64MuslNonfree,
124 Self::Aarch64Musl,
125 Self::Aarch64MuslNonfree,
126 ]
127 }
128
129 pub fn glibc() -> &'static [VoidRepo] {
131 &[
132 Self::X86_64,
133 Self::X86_64Nonfree,
134 Self::Aarch64,
135 Self::Aarch64Nonfree,
136 ]
137 }
138
139 pub fn free() -> &'static [VoidRepo] {
141 &[
142 Self::X86_64,
143 Self::X86_64Musl,
144 Self::Aarch64,
145 Self::Aarch64Musl,
146 ]
147 }
148}
149
150pub struct Void {
152 repos: Vec<VoidRepo>,
153}
154
155impl Void {
156 pub fn all() -> Self {
158 Self {
159 repos: VoidRepo::all().to_vec(),
160 }
161 }
162
163 pub fn x86_64() -> Self {
165 Self {
166 repos: VoidRepo::x86_64().to_vec(),
167 }
168 }
169
170 pub fn x86_64_musl() -> Self {
172 Self {
173 repos: VoidRepo::x86_64_musl().to_vec(),
174 }
175 }
176
177 pub fn musl() -> Self {
179 Self {
180 repos: VoidRepo::musl().to_vec(),
181 }
182 }
183
184 pub fn glibc() -> Self {
186 Self {
187 repos: VoidRepo::glibc().to_vec(),
188 }
189 }
190
191 pub fn free() -> Self {
193 Self {
194 repos: VoidRepo::free().to_vec(),
195 }
196 }
197
198 pub fn with_repos(repos: &[VoidRepo]) -> Self {
200 Self {
201 repos: repos.to_vec(),
202 }
203 }
204
205 fn parse_plist(xml: &str, repo: VoidRepo) -> Result<Vec<PackageMeta>, IndexError> {
207 let mut packages = Vec::new();
208 let mut current_name: Option<String> = None;
209 let mut in_package = false;
210 let mut current_field: Option<String> = None;
211
212 let mut version = String::new();
213 let mut homepage = String::new();
214 let mut description = String::new();
215 let mut license = String::new();
216 let mut maintainer = String::new();
217
218 for line in xml.lines() {
219 let line = line.trim();
220
221 if line.starts_with("<key>") && line.ends_with("</key>") {
222 let key = &line[5..line.len() - 6];
223 if !in_package {
224 current_name = Some(key.to_string());
225 in_package = false;
226 version.clear();
227 homepage.clear();
228 description.clear();
229 license.clear();
230 maintainer.clear();
231 } else {
232 current_field = Some(key.to_string());
233 }
234 } else if line == "<dict>" && current_name.is_some() && !in_package {
235 in_package = true;
236 } else if line == "</dict>" && in_package {
237 if let Some(name) = current_name.take() {
238 let (pkg_name, ver) = if version.contains('-') {
239 let parts: Vec<&str> = version.rsplitn(2, '-').collect();
240 if parts.len() == 2 {
241 (parts[1].to_string(), parts[0].to_string())
242 } else {
243 (name.clone(), version.clone())
244 }
245 } else {
246 (name.clone(), version.clone())
247 };
248
249 let mut extra = HashMap::new();
250 extra.insert(
251 "source_repo".to_string(),
252 serde_json::Value::String(repo.name().to_string()),
253 );
254
255 packages.push(PackageMeta {
256 name: pkg_name,
257 version: ver,
258 description: if description.is_empty() {
259 None
260 } else {
261 Some(description.clone())
262 },
263 homepage: if homepage.is_empty() {
264 None
265 } else {
266 Some(homepage.clone())
267 },
268 repository: Some("https://github.com/void-linux/void-packages".to_string()),
269 license: if license.is_empty() {
270 None
271 } else {
272 Some(license.clone())
273 },
274 maintainers: if maintainer.is_empty() {
275 Vec::new()
276 } else {
277 vec![maintainer.clone()]
278 },
279 binaries: Vec::new(),
280 keywords: Vec::new(),
281 published: None,
282 downloads: None,
283 archive_url: None,
284 checksum: None,
285 extra,
286 });
287 }
288 in_package = false;
289 } else if line.starts_with("<string>") && line.ends_with("</string>") {
290 let value = &line[8..line.len() - 9];
291 if let Some(field) = ¤t_field {
292 match field.as_str() {
293 "pkgver" => version = value.to_string(),
294 "homepage" => homepage = value.to_string(),
295 "short_desc" => description = value.to_string(),
296 "license" => license = value.to_string(),
297 "maintainer" => maintainer = value.to_string(),
298 _ => {}
299 }
300 }
301 current_field = None;
302 }
303 }
304
305 Ok(packages)
306 }
307
308 fn load_repo(repo: VoidRepo) -> Result<Vec<PackageMeta>, IndexError> {
310 let url = repo.url();
311
312 let (data, _was_cached) = cache::fetch_with_cache(
313 "void",
314 &format!("repodata-{}", repo.name()),
315 &url,
316 CACHE_TTL,
317 )
318 .map_err(IndexError::Network)?;
319
320 let decompressed = zstd::decode_all(std::io::Cursor::new(&data))
322 .map_err(|e| IndexError::Decompress(e.to_string()))?;
323
324 let mut archive = tar::Archive::new(std::io::Cursor::new(decompressed));
326
327 for entry in archive.entries().map_err(IndexError::Io)? {
328 let mut entry = entry.map_err(IndexError::Io)?;
329 let path = entry.path().map_err(IndexError::Io)?;
330
331 if path.to_string_lossy() == "index.plist" {
332 let mut xml = String::new();
333 entry.read_to_string(&mut xml).map_err(IndexError::Io)?;
334 return Self::parse_plist(&xml, repo);
335 }
336 }
337
338 Err(IndexError::Parse("index.plist not found in archive".into()))
339 }
340
341 fn load_packages(&self) -> Result<Vec<PackageMeta>, IndexError> {
343 let results: Vec<_> = self
344 .repos
345 .par_iter()
346 .map(|&repo| Self::load_repo(repo))
347 .collect();
348
349 let mut packages = Vec::new();
350 for result in results {
351 match result {
352 Ok(pkgs) => packages.extend(pkgs),
353 Err(e) => {
354 tracing::warn!("failed to load Void repo: {}", e);
355 }
356 }
357 }
358
359 Ok(packages)
360 }
361}
362
363impl PackageIndex for Void {
364 fn ecosystem(&self) -> &'static str {
365 "void"
366 }
367
368 fn display_name(&self) -> &'static str {
369 "Void Linux (xbps)"
370 }
371
372 fn fetch(&self, name: &str) -> Result<PackageMeta, IndexError> {
373 let packages = self.load_packages()?;
374
375 packages
376 .into_iter()
377 .find(|p| p.name.eq_ignore_ascii_case(name))
378 .ok_or_else(|| IndexError::NotFound(name.to_string()))
379 }
380
381 fn fetch_versions(&self, name: &str) -> Result<Vec<VersionMeta>, IndexError> {
382 let packages = self.load_packages()?;
383
384 let versions: Vec<_> = packages
385 .into_iter()
386 .filter(|p| p.name.eq_ignore_ascii_case(name))
387 .map(|p| VersionMeta {
388 version: p.version,
389 released: None,
390 yanked: false,
391 })
392 .collect();
393
394 if versions.is_empty() {
395 return Err(IndexError::NotFound(name.to_string()));
396 }
397
398 Ok(versions)
399 }
400
401 fn search(&self, query: &str) -> Result<Vec<PackageMeta>, IndexError> {
402 let packages = self.load_packages()?;
403 let query_lower = query.to_lowercase();
404
405 Ok(packages
406 .into_iter()
407 .filter(|p| {
408 p.name.to_lowercase().contains(&query_lower)
409 || p.description
410 .as_ref()
411 .map(|d| d.to_lowercase().contains(&query_lower))
412 .unwrap_or(false)
413 })
414 .collect())
415 }
416
417 fn supports_fetch_all(&self) -> bool {
418 true
419 }
420
421 fn fetch_all(&self) -> Result<Vec<PackageMeta>, IndexError> {
422 self.load_packages()
423 }
424}