1use super::{IndexError, PackageIndex, PackageMeta, VersionMeta};
30use crate::cache;
31use flate2::read::MultiGzDecoder;
32use rayon::prelude::*;
33use std::collections::HashMap;
34use std::io::{BufRead, BufReader, Cursor, Read};
35use std::time::Duration;
36use tar::Archive;
37
38const INDEX_CACHE_TTL: Duration = Duration::from_secs(60 * 60);
40
41const ALPINE_MIRROR: &str = "https://dl-cdn.alpinelinux.org/alpine";
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
46pub enum AlpineRepo {
47 EdgeMain,
50 EdgeCommunity,
52 EdgeTesting,
54
55 V321Main,
58 V321Community,
60
61 V320Main,
64 V320Community,
66
67 V319Main,
70 V319Community,
72
73 V318Main,
76 V318Community,
78}
79
80impl AlpineRepo {
81 fn parts(&self) -> (&'static str, &'static str) {
83 match self {
84 Self::EdgeMain => ("edge", "main"),
85 Self::EdgeCommunity => ("edge", "community"),
86 Self::EdgeTesting => ("edge", "testing"),
87 Self::V321Main => ("v3.21", "main"),
88 Self::V321Community => ("v3.21", "community"),
89 Self::V320Main => ("v3.20", "main"),
90 Self::V320Community => ("v3.20", "community"),
91 Self::V319Main => ("v3.19", "main"),
92 Self::V319Community => ("v3.19", "community"),
93 Self::V318Main => ("v3.18", "main"),
94 Self::V318Community => ("v3.18", "community"),
95 }
96 }
97
98 pub fn name(&self) -> String {
100 let (branch, repo) = self.parts();
101 format!("{}-{}", branch, repo)
102 }
103
104 pub fn all() -> &'static [AlpineRepo] {
106 &[
107 Self::EdgeMain,
108 Self::EdgeCommunity,
109 Self::EdgeTesting,
110 Self::V321Main,
111 Self::V321Community,
112 Self::V320Main,
113 Self::V320Community,
114 Self::V319Main,
115 Self::V319Community,
116 Self::V318Main,
117 Self::V318Community,
118 ]
119 }
120
121 pub fn edge() -> &'static [AlpineRepo] {
123 &[Self::EdgeMain, Self::EdgeCommunity, Self::EdgeTesting]
124 }
125
126 pub fn latest_stable() -> &'static [AlpineRepo] {
128 &[Self::V321Main, Self::V321Community]
129 }
130
131 pub fn stable() -> &'static [AlpineRepo] {
133 &[
134 Self::V321Main,
135 Self::V321Community,
136 Self::V320Main,
137 Self::V320Community,
138 Self::V319Main,
139 Self::V319Community,
140 Self::V318Main,
141 Self::V318Community,
142 ]
143 }
144}
145
146pub struct Apk {
148 repos: Vec<AlpineRepo>,
149 arch: &'static str,
150}
151
152impl Apk {
153 pub fn all() -> Self {
155 Self {
156 repos: AlpineRepo::all().to_vec(),
157 arch: "x86_64",
158 }
159 }
160
161 pub fn edge() -> Self {
163 Self {
164 repos: AlpineRepo::edge().to_vec(),
165 arch: "x86_64",
166 }
167 }
168
169 pub fn latest_stable() -> Self {
171 Self {
172 repos: AlpineRepo::latest_stable().to_vec(),
173 arch: "x86_64",
174 }
175 }
176
177 pub fn stable() -> Self {
179 Self {
180 repos: AlpineRepo::stable().to_vec(),
181 arch: "x86_64",
182 }
183 }
184
185 pub fn with_repos(repos: &[AlpineRepo]) -> Self {
187 Self {
188 repos: repos.to_vec(),
189 arch: "x86_64",
190 }
191 }
192
193 pub fn with_arch(mut self, arch: &'static str) -> Self {
195 self.arch = arch;
196 self
197 }
198
199 fn parse_apkindex<R: Read>(reader: R, repo: AlpineRepo) -> Vec<PackageMeta> {
201 let reader = BufReader::new(reader);
202 let mut packages = Vec::new();
203 let mut current = ApkPackageBuilder::new(repo);
204
205 for line in reader.lines().map_while(Result::ok) {
206 if line.is_empty() {
207 if let Some(pkg) = current.build() {
209 packages.push(pkg);
210 }
211 current = ApkPackageBuilder::new(repo);
212 continue;
213 }
214
215 if line.len() >= 2 && line.chars().nth(1) == Some(':') {
217 let key = line.chars().next().unwrap();
218 let value = &line[2..];
219
220 match key {
221 'P' => current.name = Some(value.to_string()),
222 'V' => current.version = Some(value.to_string()),
223 'T' => current.description = Some(value.to_string()),
224 'U' => current.homepage = Some(value.to_string()),
225 'L' => current.license = Some(value.to_string()),
226 'S' => current.size = value.parse().ok(),
227 'C' => current.checksum = Some(value.to_string()),
228 'D' => current.depends = Some(value.to_string()),
229 'm' => current.maintainer = Some(value.to_string()),
230 'o' => current.origin = Some(value.to_string()),
231 'A' => current.arch = Some(value.to_string()),
232 'p' => current.provides = Some(value.to_string()),
233 _ => {}
234 }
235 }
236 }
237
238 if let Some(pkg) = current.build() {
240 packages.push(pkg);
241 }
242
243 packages
244 }
245
246 fn load_repo(&self, repo: AlpineRepo) -> Result<Vec<PackageMeta>, IndexError> {
248 let (branch, repo_name) = repo.parts();
249 let url = format!(
250 "{}/{}/{}/{}/APKINDEX.tar.gz",
251 ALPINE_MIRROR, branch, repo_name, self.arch
252 );
253
254 let (data, _was_cached) = cache::fetch_with_cache(
256 "apk",
257 &format!("apkindex-{}-{}-{}", branch, repo_name, self.arch),
258 &url,
259 INDEX_CACHE_TTL,
260 )
261 .map_err(|e| IndexError::Network(e))?;
262
263 let tar_data = if data.len() >= 2 && data[0] == 0x1f && data[1] == 0x8b {
265 let mut decoder = MultiGzDecoder::new(Cursor::new(data));
266 let mut decompressed = Vec::new();
267 decoder
268 .read_to_end(&mut decompressed)
269 .map_err(|e| IndexError::Io(e))?;
270 decompressed
271 } else {
272 data
273 };
274
275 let mut archive = Archive::new(Cursor::new(tar_data));
276
277 for entry in archive.entries().map_err(|e| IndexError::Io(e))? {
278 let mut entry = entry.map_err(|e| IndexError::Io(e))?;
279 let path = entry
280 .path()
281 .map_err(|e| IndexError::Io(e))?
282 .to_string_lossy()
283 .to_string();
284
285 let mut content = Vec::new();
287 entry
288 .read_to_end(&mut content)
289 .map_err(|e| IndexError::Io(e))?;
290
291 if path == "APKINDEX" {
292 return Ok(Self::parse_apkindex(Cursor::new(content), repo));
293 }
294 }
295
296 Err(IndexError::Parse("APKINDEX not found in archive".into()))
297 }
298
299 fn load_packages(&self) -> Result<Vec<PackageMeta>, IndexError> {
301 let results: Vec<_> = self
302 .repos
303 .par_iter()
304 .map(|&repo| self.load_repo(repo))
305 .collect();
306
307 let mut packages = Vec::new();
308 for result in results {
309 match result {
310 Ok(pkgs) => packages.extend(pkgs),
311 Err(e) => {
312 eprintln!("Warning: failed to load Alpine repo: {}", e);
313 }
314 }
315 }
316
317 Ok(packages)
318 }
319}
320
321impl PackageIndex for Apk {
322 fn ecosystem(&self) -> &'static str {
323 "apk"
324 }
325
326 fn display_name(&self) -> &'static str {
327 "APK (Alpine Linux)"
328 }
329
330 fn fetch(&self, name: &str) -> Result<PackageMeta, IndexError> {
331 let packages = self.load_packages()?;
333
334 packages
335 .into_iter()
336 .find(|p| p.name == name)
337 .ok_or_else(|| IndexError::NotFound(name.to_string()))
338 }
339
340 fn fetch_versions(&self, name: &str) -> Result<Vec<VersionMeta>, IndexError> {
341 let packages = self.load_packages()?;
342
343 let versions: Vec<_> = packages
344 .into_iter()
345 .filter(|p| p.name == name)
346 .map(|p| VersionMeta {
347 version: p.version,
348 released: None,
349 yanked: false,
350 })
351 .collect();
352
353 if versions.is_empty() {
354 return Err(IndexError::NotFound(name.to_string()));
355 }
356
357 Ok(versions)
358 }
359
360 fn supports_fetch_all(&self) -> bool {
361 true
362 }
363
364 fn fetch_all(&self) -> Result<Vec<PackageMeta>, IndexError> {
365 self.load_packages()
366 }
367
368 fn search(&self, query: &str) -> Result<Vec<PackageMeta>, IndexError> {
369 let packages = self.load_packages()?;
370 let query_lower = query.to_lowercase();
371
372 Ok(packages
373 .into_iter()
374 .filter(|p| {
375 p.name.to_lowercase().contains(&query_lower)
376 || p.description
377 .as_ref()
378 .map(|d| d.to_lowercase().contains(&query_lower))
379 .unwrap_or(false)
380 })
381 .collect())
382 }
383}
384
385#[derive(Default)]
387struct ApkPackageBuilder {
388 repo: Option<AlpineRepo>,
389 name: Option<String>,
390 version: Option<String>,
391 description: Option<String>,
392 homepage: Option<String>,
393 license: Option<String>,
394 size: Option<u64>,
395 checksum: Option<String>,
396 depends: Option<String>,
397 maintainer: Option<String>,
398 origin: Option<String>,
399 arch: Option<String>,
400 provides: Option<String>,
401}
402
403impl ApkPackageBuilder {
404 fn new(repo: AlpineRepo) -> Self {
405 Self {
406 repo: Some(repo),
407 ..Default::default()
408 }
409 }
410
411 fn build(self) -> Option<PackageMeta> {
412 let name = self.name?;
413 let version = self.version?;
414 let repo = self.repo?;
415 let (branch, repo_name) = repo.parts();
416
417 let mut extra = HashMap::new();
418
419 if let Some(deps) = self.depends {
421 let parsed_deps: Vec<serde_json::Value> = deps
422 .split_whitespace()
423 .filter(|d| {
424 !d.starts_with("so:")
426 })
427 .map(|d| {
428 let name = d
430 .split(|c| c == '>' || c == '<' || c == '=' || c == '~')
431 .next()
432 .unwrap_or(d);
433 serde_json::Value::String(name.to_string())
434 })
435 .collect();
436 if !parsed_deps.is_empty() {
437 extra.insert("depends".to_string(), serde_json::Value::Array(parsed_deps));
438 }
439 }
440
441 if let Some(size) = self.size {
443 extra.insert("size".to_string(), serde_json::Value::Number(size.into()));
444 }
445
446 if let Some(origin) = self.origin {
448 extra.insert("origin".to_string(), serde_json::Value::String(origin));
449 }
450
451 if let Some(provides) = self.provides {
453 let parsed_provides: Vec<serde_json::Value> = provides
454 .split_whitespace()
455 .map(|p| {
456 let name = p
458 .split(|c| c == '>' || c == '<' || c == '=' || c == '~')
459 .next()
460 .unwrap_or(p);
461 serde_json::Value::String(name.to_string())
462 })
463 .collect();
464 if !parsed_provides.is_empty() {
465 extra.insert(
466 "provides".to_string(),
467 serde_json::Value::Array(parsed_provides),
468 );
469 }
470 }
471
472 extra.insert(
474 "source_repo".to_string(),
475 serde_json::Value::String(repo.name()),
476 );
477
478 let archive_url = Some(format!(
480 "{}/{}/{}/x86_64/{}-{}.apk",
481 ALPINE_MIRROR, branch, repo_name, name, version
482 ));
483
484 let checksum = self.checksum.map(|c| {
486 if c.starts_with("Q1") {
487 format!("sha1-base64:{}", &c[2..])
488 } else {
489 c
490 }
491 });
492
493 Some(PackageMeta {
494 name,
495 version,
496 description: self.description,
497 homepage: self.homepage,
498 repository: None,
499 license: self.license,
500 binaries: Vec::new(),
501 keywords: Vec::new(),
502 maintainers: self.maintainer.into_iter().collect(),
503 published: None,
504 downloads: None,
505 archive_url,
506 checksum,
507 extra,
508 })
509 }
510}