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
80struct RepoParts {
81 branch: &'static str,
82 repo: &'static str,
83}
84
85impl AlpineRepo {
86 fn parts(&self) -> RepoParts {
88 let (branch, repo) = match self {
89 Self::EdgeMain => ("edge", "main"),
90 Self::EdgeCommunity => ("edge", "community"),
91 Self::EdgeTesting => ("edge", "testing"),
92 Self::V321Main => ("v3.21", "main"),
93 Self::V321Community => ("v3.21", "community"),
94 Self::V320Main => ("v3.20", "main"),
95 Self::V320Community => ("v3.20", "community"),
96 Self::V319Main => ("v3.19", "main"),
97 Self::V319Community => ("v3.19", "community"),
98 Self::V318Main => ("v3.18", "main"),
99 Self::V318Community => ("v3.18", "community"),
100 };
101 RepoParts { branch, repo }
102 }
103
104 pub fn name(&self) -> String {
106 let parts = self.parts();
107 format!("{}-{}", parts.branch, parts.repo)
108 }
109
110 pub fn all() -> &'static [AlpineRepo] {
112 &[
113 Self::EdgeMain,
114 Self::EdgeCommunity,
115 Self::EdgeTesting,
116 Self::V321Main,
117 Self::V321Community,
118 Self::V320Main,
119 Self::V320Community,
120 Self::V319Main,
121 Self::V319Community,
122 Self::V318Main,
123 Self::V318Community,
124 ]
125 }
126
127 pub fn edge() -> &'static [AlpineRepo] {
129 &[Self::EdgeMain, Self::EdgeCommunity, Self::EdgeTesting]
130 }
131
132 pub fn latest_stable() -> &'static [AlpineRepo] {
134 &[Self::V321Main, Self::V321Community]
135 }
136
137 pub fn stable() -> &'static [AlpineRepo] {
139 &[
140 Self::V321Main,
141 Self::V321Community,
142 Self::V320Main,
143 Self::V320Community,
144 Self::V319Main,
145 Self::V319Community,
146 Self::V318Main,
147 Self::V318Community,
148 ]
149 }
150}
151
152pub struct Apk {
154 repos: Vec<AlpineRepo>,
155 arch: &'static str,
156}
157
158impl Apk {
159 pub fn all() -> Self {
161 Self {
162 repos: AlpineRepo::all().to_vec(),
163 arch: "x86_64",
164 }
165 }
166
167 pub fn edge() -> Self {
169 Self {
170 repos: AlpineRepo::edge().to_vec(),
171 arch: "x86_64",
172 }
173 }
174
175 pub fn latest_stable() -> Self {
177 Self {
178 repos: AlpineRepo::latest_stable().to_vec(),
179 arch: "x86_64",
180 }
181 }
182
183 pub fn stable() -> Self {
185 Self {
186 repos: AlpineRepo::stable().to_vec(),
187 arch: "x86_64",
188 }
189 }
190
191 pub fn with_repos(repos: &[AlpineRepo]) -> Self {
193 Self {
194 repos: repos.to_vec(),
195 arch: "x86_64",
196 }
197 }
198
199 pub fn with_arch(mut self, arch: &'static str) -> Self {
201 self.arch = arch;
202 self
203 }
204
205 fn parse_apkindex<R: Read>(reader: R, repo: AlpineRepo) -> Vec<PackageMeta> {
207 let reader = BufReader::new(reader);
208 let mut packages = Vec::new();
209 let mut current = ApkPackageBuilder::new(repo);
210
211 for line in reader.lines().map_while(Result::ok) {
212 if line.is_empty() {
213 if let Some(pkg) = current.build() {
215 packages.push(pkg);
216 }
217 current = ApkPackageBuilder::new(repo);
218 continue;
219 }
220
221 if line.len() >= 2 && line.chars().nth(1) == Some(':') {
223 let key = line.chars().next().unwrap();
225 let value = &line[2..];
226
227 match key {
228 'P' => current.name = Some(value.to_string()),
229 'V' => current.version = Some(value.to_string()),
230 'T' => current.description = Some(value.to_string()),
231 'U' => current.homepage = Some(value.to_string()),
232 'L' => current.license = Some(value.to_string()),
233 'S' => current.size = value.parse().ok(),
234 'C' => current.checksum = Some(value.to_string()),
235 'D' => current.depends = Some(value.to_string()),
236 'm' => current.maintainer = Some(value.to_string()),
237 'o' => current.origin = Some(value.to_string()),
238 'A' => current.arch = Some(value.to_string()),
239 'p' => current.provides = Some(value.to_string()),
240 _ => {}
241 }
242 }
243 }
244
245 if let Some(pkg) = current.build() {
247 packages.push(pkg);
248 }
249
250 packages
251 }
252
253 fn load_repo(&self, repo: AlpineRepo) -> Result<Vec<PackageMeta>, IndexError> {
255 let parts = repo.parts();
256 let url = format!(
257 "{}/{}/{}/{}/APKINDEX.tar.gz",
258 ALPINE_MIRROR, parts.branch, parts.repo, self.arch
259 );
260
261 let (data, _was_cached) = cache::fetch_with_cache(
263 "apk",
264 &format!("apkindex-{}-{}-{}", parts.branch, parts.repo, self.arch),
265 &url,
266 INDEX_CACHE_TTL,
267 )
268 .map_err(IndexError::Network)?;
269
270 let tar_data = if data.len() >= 2 && data[0] == 0x1f && data[1] == 0x8b {
272 let mut decoder = MultiGzDecoder::new(Cursor::new(data));
273 let mut decompressed = Vec::new();
274 decoder
275 .read_to_end(&mut decompressed)
276 .map_err(IndexError::Io)?;
277 decompressed
278 } else {
279 data
280 };
281
282 let mut archive = Archive::new(Cursor::new(tar_data));
283
284 for entry in archive.entries().map_err(IndexError::Io)? {
285 let mut entry = entry.map_err(IndexError::Io)?;
286 let path = entry
287 .path()
288 .map_err(IndexError::Io)?
289 .to_string_lossy()
290 .to_string();
291
292 let mut content = Vec::new();
294 entry.read_to_end(&mut content).map_err(IndexError::Io)?;
295
296 if path == "APKINDEX" {
297 return Ok(Self::parse_apkindex(Cursor::new(content), repo));
298 }
299 }
300
301 Err(IndexError::Parse("APKINDEX not found in archive".into()))
302 }
303
304 fn load_packages(&self) -> Result<Vec<PackageMeta>, IndexError> {
306 let results: Vec<_> = self
307 .repos
308 .par_iter()
309 .map(|&repo| self.load_repo(repo))
310 .collect();
311
312 let mut packages = Vec::new();
313 for result in results {
314 match result {
315 Ok(pkgs) => packages.extend(pkgs),
316 Err(e) => {
317 tracing::warn!("failed to load Alpine repo: {}", e);
318 }
319 }
320 }
321
322 Ok(packages)
323 }
324}
325
326impl PackageIndex for Apk {
327 fn ecosystem(&self) -> &'static str {
328 "apk"
329 }
330
331 fn display_name(&self) -> &'static str {
332 "APK (Alpine Linux)"
333 }
334
335 fn fetch(&self, name: &str) -> Result<PackageMeta, IndexError> {
336 let packages = self.load_packages()?;
338
339 packages
340 .into_iter()
341 .find(|p| p.name == name)
342 .ok_or_else(|| IndexError::NotFound(name.to_string()))
343 }
344
345 fn fetch_versions(&self, name: &str) -> Result<Vec<VersionMeta>, IndexError> {
346 let packages = self.load_packages()?;
347
348 let versions: Vec<_> = packages
349 .into_iter()
350 .filter(|p| p.name == name)
351 .map(|p| VersionMeta {
352 version: p.version,
353 released: None,
354 yanked: false,
355 })
356 .collect();
357
358 if versions.is_empty() {
359 return Err(IndexError::NotFound(name.to_string()));
360 }
361
362 Ok(versions)
363 }
364
365 fn supports_fetch_all(&self) -> bool {
366 true
367 }
368
369 fn fetch_all(&self) -> Result<Vec<PackageMeta>, IndexError> {
370 self.load_packages()
371 }
372
373 fn search(&self, query: &str) -> Result<Vec<PackageMeta>, IndexError> {
374 let packages = self.load_packages()?;
375 let query_lower = query.to_lowercase();
376
377 Ok(packages
378 .into_iter()
379 .filter(|p| {
380 p.name.to_lowercase().contains(&query_lower)
381 || p.description
382 .as_ref()
383 .map(|d| d.to_lowercase().contains(&query_lower))
384 .unwrap_or(false)
385 })
386 .collect())
387 }
388}
389
390#[derive(Default)]
392struct ApkPackageBuilder {
393 repo: Option<AlpineRepo>,
394 name: Option<String>,
395 version: Option<String>,
396 description: Option<String>,
397 homepage: Option<String>,
398 license: Option<String>,
399 size: Option<u64>,
400 checksum: Option<String>,
401 depends: Option<String>,
402 maintainer: Option<String>,
403 origin: Option<String>,
404 arch: Option<String>,
405 provides: Option<String>,
406}
407
408impl ApkPackageBuilder {
409 fn new(repo: AlpineRepo) -> Self {
410 Self {
411 repo: Some(repo),
412 ..Default::default()
413 }
414 }
415
416 fn build(self) -> Option<PackageMeta> {
417 let name = self.name?;
418 let version = self.version?;
419 let repo = self.repo?;
420 let repo_parts = repo.parts();
421 let (branch, repo_name) = (repo_parts.branch, repo_parts.repo);
422
423 let mut extra = HashMap::new();
424
425 if let Some(deps) = self.depends {
427 let parsed_deps: Vec<serde_json::Value> = deps
428 .split_whitespace()
429 .filter(|d| {
430 !d.starts_with("so:")
432 })
433 .map(|d| {
434 let name = d.split(['>', '<', '=', '~']).next().unwrap_or(d);
436 serde_json::Value::String(name.to_string())
437 })
438 .collect();
439 if !parsed_deps.is_empty() {
440 extra.insert("depends".to_string(), serde_json::Value::Array(parsed_deps));
441 }
442 }
443
444 if let Some(size) = self.size {
446 extra.insert("size".to_string(), serde_json::Value::Number(size.into()));
447 }
448
449 if let Some(origin) = self.origin {
451 extra.insert("origin".to_string(), serde_json::Value::String(origin));
452 }
453
454 if let Some(provides) = self.provides {
456 let parsed_provides: Vec<serde_json::Value> = provides
457 .split_whitespace()
458 .map(|p| {
459 let name = p.split(['>', '<', '=', '~']).next().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 let Some(stripped) = c.strip_prefix("Q1") {
487 format!("sha1-base64:{}", stripped)
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}