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(|e| IndexError::Io(e))? {
328 let mut entry = entry.map_err(|e| IndexError::Io(e))?;
329 let path = entry.path().map_err(|e| IndexError::Io(e))?;
330
331 if path.to_string_lossy() == "index.plist" {
332 let mut xml = String::new();
333 entry
334 .read_to_string(&mut xml)
335 .map_err(|e| IndexError::Io(e))?;
336 return Self::parse_plist(&xml, repo);
337 }
338 }
339
340 Err(IndexError::Parse("index.plist not found in archive".into()))
341 }
342
343 fn load_packages(&self) -> Result<Vec<PackageMeta>, IndexError> {
345 let results: Vec<_> = self
346 .repos
347 .par_iter()
348 .map(|&repo| Self::load_repo(repo))
349 .collect();
350
351 let mut packages = Vec::new();
352 for result in results {
353 match result {
354 Ok(pkgs) => packages.extend(pkgs),
355 Err(e) => {
356 eprintln!("Warning: failed to load Void repo: {}", e);
357 }
358 }
359 }
360
361 Ok(packages)
362 }
363}
364
365impl PackageIndex for Void {
366 fn ecosystem(&self) -> &'static str {
367 "void"
368 }
369
370 fn display_name(&self) -> &'static str {
371 "Void Linux (xbps)"
372 }
373
374 fn fetch(&self, name: &str) -> Result<PackageMeta, IndexError> {
375 let packages = self.load_packages()?;
376
377 packages
378 .into_iter()
379 .find(|p| p.name.eq_ignore_ascii_case(name))
380 .ok_or_else(|| IndexError::NotFound(name.to_string()))
381 }
382
383 fn fetch_versions(&self, name: &str) -> Result<Vec<VersionMeta>, IndexError> {
384 let packages = self.load_packages()?;
385
386 let versions: Vec<_> = packages
387 .into_iter()
388 .filter(|p| p.name.eq_ignore_ascii_case(name))
389 .map(|p| VersionMeta {
390 version: p.version,
391 released: None,
392 yanked: false,
393 })
394 .collect();
395
396 if versions.is_empty() {
397 return Err(IndexError::NotFound(name.to_string()));
398 }
399
400 Ok(versions)
401 }
402
403 fn search(&self, query: &str) -> Result<Vec<PackageMeta>, IndexError> {
404 let packages = self.load_packages()?;
405 let query_lower = query.to_lowercase();
406
407 Ok(packages
408 .into_iter()
409 .filter(|p| {
410 p.name.to_lowercase().contains(&query_lower)
411 || p.description
412 .as_ref()
413 .map(|d| d.to_lowercase().contains(&query_lower))
414 .unwrap_or(false)
415 })
416 .collect())
417 }
418
419 fn supports_fetch_all(&self) -> bool {
420 true
421 }
422
423 fn fetch_all(&self) -> Result<Vec<PackageMeta>, IndexError> {
424 self.load_packages()
425 }
426}