1use super::{IndexError, PackageIndex, PackageIter, PackageMeta, VersionMeta};
30use crate::cache;
31use flate2::read::GzDecoder;
32use rayon::prelude::*;
33use std::collections::HashMap;
34use std::io::{BufRead, BufReader, Cursor, Read};
35use std::time::Duration;
36
37const INDEX_CACHE_TTL: Duration = Duration::from_secs(60 * 60);
39
40const DEBIAN_MIRROR: &str = "https://deb.debian.org/debian";
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
45pub enum AptRepo {
46 StableMain,
49 StableContrib,
51 StableNonFree,
53 StableNonFreeFirmware,
55
56 StableBackportsMain,
59 StableBackportsContrib,
61 StableBackportsNonFree,
63
64 TestingMain,
67 TestingContrib,
69 TestingNonFree,
71 TestingNonFreeFirmware,
73
74 UnstableMain,
77 UnstableContrib,
79 UnstableNonFree,
81 UnstableNonFreeFirmware,
83
84 ExperimentalMain,
87 ExperimentalContrib,
89 ExperimentalNonFree,
91
92 OldstableMain,
95 OldstableContrib,
97 OldstableNonFree,
99}
100
101struct DistComponent {
102 dist: &'static str,
103 component: &'static str,
104}
105
106impl AptRepo {
107 fn parts(&self) -> DistComponent {
109 let (dist, component) = match self {
110 Self::StableMain => ("stable", "main"),
111 Self::StableContrib => ("stable", "contrib"),
112 Self::StableNonFree => ("stable", "non-free"),
113 Self::StableNonFreeFirmware => ("stable", "non-free-firmware"),
114
115 Self::StableBackportsMain => ("stable-backports", "main"),
116 Self::StableBackportsContrib => ("stable-backports", "contrib"),
117 Self::StableBackportsNonFree => ("stable-backports", "non-free"),
118
119 Self::TestingMain => ("testing", "main"),
120 Self::TestingContrib => ("testing", "contrib"),
121 Self::TestingNonFree => ("testing", "non-free"),
122 Self::TestingNonFreeFirmware => ("testing", "non-free-firmware"),
123
124 Self::UnstableMain => ("unstable", "main"),
125 Self::UnstableContrib => ("unstable", "contrib"),
126 Self::UnstableNonFree => ("unstable", "non-free"),
127 Self::UnstableNonFreeFirmware => ("unstable", "non-free-firmware"),
128
129 Self::ExperimentalMain => ("experimental", "main"),
130 Self::ExperimentalContrib => ("experimental", "contrib"),
131 Self::ExperimentalNonFree => ("experimental", "non-free"),
132
133 Self::OldstableMain => ("oldstable", "main"),
134 Self::OldstableContrib => ("oldstable", "contrib"),
135 Self::OldstableNonFree => ("oldstable", "non-free"),
136 };
137 DistComponent { dist, component }
138 }
139
140 fn packages_url(&self) -> String {
142 let parts = self.parts();
143 format!(
144 "{}/dists/{}/{}/binary-amd64/Packages.gz",
145 DEBIAN_MIRROR, parts.dist, parts.component
146 )
147 }
148
149 pub fn name(&self) -> &'static str {
151 match self {
152 Self::StableMain => "stable-main",
153 Self::StableContrib => "stable-contrib",
154 Self::StableNonFree => "stable-non-free",
155 Self::StableNonFreeFirmware => "stable-non-free-firmware",
156
157 Self::StableBackportsMain => "stable-backports-main",
158 Self::StableBackportsContrib => "stable-backports-contrib",
159 Self::StableBackportsNonFree => "stable-backports-non-free",
160
161 Self::TestingMain => "testing-main",
162 Self::TestingContrib => "testing-contrib",
163 Self::TestingNonFree => "testing-non-free",
164 Self::TestingNonFreeFirmware => "testing-non-free-firmware",
165
166 Self::UnstableMain => "unstable-main",
167 Self::UnstableContrib => "unstable-contrib",
168 Self::UnstableNonFree => "unstable-non-free",
169 Self::UnstableNonFreeFirmware => "unstable-non-free-firmware",
170
171 Self::ExperimentalMain => "experimental-main",
172 Self::ExperimentalContrib => "experimental-contrib",
173 Self::ExperimentalNonFree => "experimental-non-free",
174
175 Self::OldstableMain => "oldstable-main",
176 Self::OldstableContrib => "oldstable-contrib",
177 Self::OldstableNonFree => "oldstable-non-free",
178 }
179 }
180
181 pub fn all() -> &'static [AptRepo] {
183 &[
184 Self::StableMain,
185 Self::StableContrib,
186 Self::StableNonFree,
187 Self::StableNonFreeFirmware,
188 Self::StableBackportsMain,
189 Self::StableBackportsContrib,
190 Self::StableBackportsNonFree,
191 Self::TestingMain,
192 Self::TestingContrib,
193 Self::TestingNonFree,
194 Self::TestingNonFreeFirmware,
195 Self::UnstableMain,
196 Self::UnstableContrib,
197 Self::UnstableNonFree,
198 Self::UnstableNonFreeFirmware,
199 Self::ExperimentalMain,
200 Self::ExperimentalContrib,
201 Self::ExperimentalNonFree,
202 Self::OldstableMain,
203 Self::OldstableContrib,
204 Self::OldstableNonFree,
205 ]
206 }
207
208 pub fn stable() -> &'static [AptRepo] {
210 &[
211 Self::StableMain,
212 Self::StableContrib,
213 Self::StableNonFree,
214 Self::StableNonFreeFirmware,
215 Self::StableBackportsMain,
216 Self::StableBackportsContrib,
217 Self::StableBackportsNonFree,
218 ]
219 }
220
221 pub fn testing() -> &'static [AptRepo] {
223 &[
224 Self::TestingMain,
225 Self::TestingContrib,
226 Self::TestingNonFree,
227 Self::TestingNonFreeFirmware,
228 ]
229 }
230
231 pub fn unstable() -> &'static [AptRepo] {
233 &[
234 Self::UnstableMain,
235 Self::UnstableContrib,
236 Self::UnstableNonFree,
237 Self::UnstableNonFreeFirmware,
238 ]
239 }
240
241 pub fn free() -> &'static [AptRepo] {
243 &[
244 Self::StableMain,
245 Self::StableBackportsMain,
246 Self::TestingMain,
247 Self::UnstableMain,
248 Self::ExperimentalMain,
249 Self::OldstableMain,
250 ]
251 }
252
253 pub fn oldstable() -> &'static [AptRepo] {
255 &[
256 Self::OldstableMain,
257 Self::OldstableContrib,
258 Self::OldstableNonFree,
259 ]
260 }
261}
262
263pub struct Apt {
265 repos: Vec<AptRepo>,
266}
267
268impl Apt {
269 pub fn all() -> Self {
271 Self {
272 repos: AptRepo::all().to_vec(),
273 }
274 }
275
276 pub fn stable() -> Self {
278 Self {
279 repos: AptRepo::stable().to_vec(),
280 }
281 }
282
283 pub fn testing() -> Self {
285 Self {
286 repos: AptRepo::testing().to_vec(),
287 }
288 }
289
290 pub fn unstable() -> Self {
292 Self {
293 repos: AptRepo::unstable().to_vec(),
294 }
295 }
296
297 pub fn free() -> Self {
299 Self {
300 repos: AptRepo::free().to_vec(),
301 }
302 }
303
304 pub fn oldstable() -> Self {
306 Self {
307 repos: AptRepo::oldstable().to_vec(),
308 }
309 }
310
311 pub fn with_repos(repos: &[AptRepo]) -> Self {
313 Self {
314 repos: repos.to_vec(),
315 }
316 }
317
318 fn parse_control<R: Read>(reader: R, repo: AptRepo) -> Vec<PackageMeta> {
320 let reader = BufReader::new(reader);
321 let mut packages = Vec::new();
322 let mut current: Option<PackageBuilder> = None;
323
324 for line in reader.lines().map_while(Result::ok) {
325 if line.is_empty() {
326 if let Some(builder) = current.take()
328 && let Some(pkg) = builder.build(repo)
329 {
330 packages.push(pkg);
331 }
332 continue;
333 }
334
335 if line.starts_with(' ') || line.starts_with('\t') {
336 continue;
338 }
339
340 if let Some((key, value)) = line.split_once(':') {
341 let key = key.trim();
342 let value = value.trim();
343
344 let builder = current.get_or_insert_with(PackageBuilder::new);
345
346 match key {
347 "Package" => builder.name = Some(value.to_string()),
348 "Version" => builder.version = Some(value.to_string()),
349 "Description" => builder.description = Some(value.to_string()),
350 "Homepage" => builder.homepage = Some(value.to_string()),
351 "Vcs-Git" | "Vcs-Browser" => {
352 if builder.repository.is_none() {
353 builder.repository = Some(value.to_string());
354 }
355 }
356 "Filename" => builder.filename = Some(value.to_string()),
357 "SHA256" => builder.sha256 = Some(value.to_string()),
358 "Depends" => builder.depends = Some(value.to_string()),
359 "Provides" => builder.provides = Some(value.to_string()),
360 "Size" => builder.size = value.parse().ok(),
361 _ => {}
362 }
363 }
364 }
365
366 if let Some(builder) = current
368 && let Some(pkg) = builder.build(repo)
369 {
370 packages.push(pkg);
371 }
372
373 packages
374 }
375
376 fn load_repo(repo: AptRepo) -> Result<Vec<PackageMeta>, IndexError> {
378 let url = repo.packages_url();
379
380 let (data, _was_cached) = cache::fetch_with_cache(
381 "apt",
382 &format!("packages-{}", repo.name()),
383 &url,
384 INDEX_CACHE_TTL,
385 )
386 .map_err(IndexError::Network)?;
387
388 let reader: Box<dyn Read> = if data.len() >= 2 && data[0] == 0x1f && data[1] == 0x8b {
390 Box::new(GzDecoder::new(Cursor::new(data)))
391 } else {
392 Box::new(Cursor::new(data))
393 };
394
395 Ok(Self::parse_control(reader, repo))
396 }
397
398 fn load_packages(&self) -> Result<Vec<PackageMeta>, IndexError> {
400 let results: Vec<_> = self
401 .repos
402 .par_iter()
403 .map(|&repo| Self::load_repo(repo))
404 .collect();
405
406 let mut packages = Vec::new();
407 for result in results {
408 match result {
409 Ok(pkgs) => packages.extend(pkgs),
410 Err(e) => {
411 tracing::warn!("failed to load APT repo: {}", e);
412 }
413 }
414 }
415
416 Ok(packages)
417 }
418
419 fn fetch_repo_data(repo: AptRepo) -> Result<Vec<u8>, IndexError> {
421 let url = repo.packages_url();
422 let (data, _was_cached) = cache::fetch_with_cache(
423 "apt",
424 &format!("packages-{}", repo.name()),
425 &url,
426 INDEX_CACHE_TTL,
427 )
428 .map_err(IndexError::Network)?;
429 Ok(data)
430 }
431
432 fn load_repos_data(&self) -> Vec<(Vec<u8>, AptRepo)> {
434 self.repos
435 .par_iter()
436 .filter_map(|&repo| Self::fetch_repo_data(repo).ok().map(|data| (data, repo)))
437 .collect()
438 }
439}
440
441impl PackageIndex for Apt {
442 fn ecosystem(&self) -> &'static str {
443 "apt"
444 }
445
446 fn display_name(&self) -> &'static str {
447 "APT (Debian)"
448 }
449
450 fn fetch(&self, name: &str) -> Result<PackageMeta, IndexError> {
451 let url = format!(
453 "https://sources.debian.org/api/src/{}/",
454 urlencoding::encode(name)
455 );
456
457 let response: serde_json::Value = ureq::get(&url).call()?.into_json()?;
458
459 if response.get("error").is_some() {
460 return Err(IndexError::NotFound(name.to_string()));
461 }
462
463 let versions = response["versions"]
464 .as_array()
465 .ok_or_else(|| IndexError::Parse("missing versions".into()))?;
466
467 let latest = versions
468 .first()
469 .ok_or_else(|| IndexError::NotFound(name.to_string()))?;
470
471 Ok(PackageMeta {
472 name: name.to_string(),
473 version: latest["version"].as_str().unwrap_or("unknown").to_string(),
474 description: None,
475 homepage: response["homepage"].as_str().map(String::from),
476 repository: response["vcs_url"].as_str().map(String::from),
477 license: None,
478 binaries: Vec::new(),
479 keywords: Vec::new(),
480 maintainers: Vec::new(),
481 published: None,
482 downloads: None,
483 archive_url: None,
484 checksum: None,
485 extra: Default::default(),
486 })
487 }
488
489 fn fetch_versions(&self, name: &str) -> Result<Vec<VersionMeta>, IndexError> {
490 let url = format!(
491 "https://sources.debian.org/api/src/{}/",
492 urlencoding::encode(name)
493 );
494
495 let response: serde_json::Value = ureq::get(&url).call()?.into_json()?;
496
497 if response.get("error").is_some() {
498 return Err(IndexError::NotFound(name.to_string()));
499 }
500
501 let versions = response["versions"]
502 .as_array()
503 .ok_or_else(|| IndexError::Parse("missing versions".into()))?;
504
505 Ok(versions
506 .iter()
507 .map(|v| VersionMeta {
508 version: v["version"].as_str().unwrap_or("unknown").to_string(),
509 released: None,
510 yanked: false,
511 })
512 .collect())
513 }
514
515 fn supports_fetch_all(&self) -> bool {
516 true
517 }
518
519 fn fetch_all(&self) -> Result<Vec<PackageMeta>, IndexError> {
520 self.load_packages()
521 }
522
523 fn iter_all(&self) -> Result<PackageIter<'_>, IndexError> {
524 let repos_data = self.load_repos_data();
526 if repos_data.is_empty() {
527 return Err(IndexError::Network("Failed to load any APT repos".into()));
528 }
529 Ok(Box::new(AptPackageIter::new(repos_data)))
530 }
531
532 fn search(&self, query: &str) -> Result<Vec<PackageMeta>, IndexError> {
533 let packages = self.load_packages()?;
535 let query_lower = query.to_lowercase();
536
537 let results: Vec<PackageMeta> = packages
538 .into_iter()
539 .filter(|pkg| {
540 pkg.name.to_lowercase().contains(&query_lower)
541 || pkg
542 .description
543 .as_ref()
544 .map(|d| d.to_lowercase().contains(&query_lower))
545 .unwrap_or(false)
546 })
547 .collect();
548
549 if !results.is_empty() {
550 return Ok(results);
551 }
552
553 let url = format!(
555 "https://sources.debian.org/api/search/{}/?suite=stable",
556 urlencoding::encode(query)
557 );
558
559 let response: serde_json::Value = ureq::get(&url).call()?.into_json()?;
560
561 let api_results = response["results"]["exact"]
562 .as_array()
563 .or_else(|| response["results"]["other"].as_array())
564 .ok_or_else(|| IndexError::Parse("missing results".into()))?;
565
566 api_results
567 .iter()
568 .map(|r| {
569 let name = r["name"].as_str().unwrap_or("").to_string();
570 self.fetch(&name)
571 })
572 .collect()
573 }
574}
575
576#[derive(Default)]
577struct PackageBuilder {
578 name: Option<String>,
579 version: Option<String>,
580 description: Option<String>,
581 homepage: Option<String>,
582 repository: Option<String>,
583 filename: Option<String>,
584 sha256: Option<String>,
585 depends: Option<String>,
586 provides: Option<String>,
587 size: Option<u64>,
588}
589
590impl PackageBuilder {
591 fn new() -> Self {
592 Self::default()
593 }
594
595 fn build(self, repo: AptRepo) -> Option<PackageMeta> {
596 let mut extra = HashMap::new();
597
598 if let Some(deps) = self.depends {
600 let parsed_deps: Vec<String> = deps
601 .split(',')
602 .map(|d| {
603 d.trim()
605 .split_once(' ')
606 .map(|(name, _)| name)
607 .unwrap_or(d.trim())
608 .to_string()
609 })
610 .filter(|d| !d.is_empty())
611 .collect();
612 extra.insert(
613 "depends".to_string(),
614 serde_json::Value::Array(
615 parsed_deps
616 .into_iter()
617 .map(serde_json::Value::String)
618 .collect(),
619 ),
620 );
621 }
622
623 if let Some(provides) = self.provides {
625 let parsed_provides: Vec<String> = provides
626 .split(',')
627 .map(|p| {
628 p.trim()
630 .split_once(' ')
631 .map(|(name, _)| name)
632 .unwrap_or(p.trim())
633 .to_string()
634 })
635 .filter(|p| !p.is_empty())
636 .collect();
637 if !parsed_provides.is_empty() {
638 extra.insert(
639 "provides".to_string(),
640 serde_json::Value::Array(
641 parsed_provides
642 .into_iter()
643 .map(serde_json::Value::String)
644 .collect(),
645 ),
646 );
647 }
648 }
649
650 if let Some(size) = self.size {
652 extra.insert("size".to_string(), serde_json::Value::Number(size.into()));
653 }
654
655 extra.insert(
657 "source_repo".to_string(),
658 serde_json::Value::String(repo.name().to_string()),
659 );
660
661 Some(PackageMeta {
662 name: self.name?,
663 version: self.version?,
664 description: self.description,
665 homepage: self.homepage,
666 repository: self.repository,
667 license: None,
668 binaries: Vec::new(),
669 keywords: Vec::new(),
670 maintainers: Vec::new(),
671 published: None,
672 downloads: None,
673 archive_url: self.filename.map(|f| format!("{}/{}", DEBIAN_MIRROR, f)),
674 checksum: self.sha256.map(|h| format!("sha256:{}", h)),
675 extra,
676 })
677 }
678}
679
680pub struct AptPackageIter {
683 repos_data: Vec<(Vec<u8>, AptRepo)>,
685 current_repo_idx: usize,
687 current_reader: Option<Box<dyn BufRead + Send>>,
689 current_repo: Option<AptRepo>,
691 current_builder: Option<PackageBuilder>,
693 done: bool,
695}
696
697impl AptPackageIter {
698 fn new(repos_data: Vec<(Vec<u8>, AptRepo)>) -> Self {
699 Self {
700 repos_data,
701 current_repo_idx: 0,
702 current_reader: None,
703 current_repo: None,
704 current_builder: None,
705 done: false,
706 }
707 }
708
709 fn advance_to_next_repo(&mut self) -> bool {
710 if self.current_repo_idx >= self.repos_data.len() {
711 self.done = true;
712 return false;
713 }
714
715 let (data, repo) = &self.repos_data[self.current_repo_idx];
716 self.current_repo_idx += 1;
717 self.current_repo = Some(*repo);
718
719 let reader: Box<dyn BufRead + Send> =
721 if data.len() >= 2 && data[0] == 0x1f && data[1] == 0x8b {
722 let mut decoder = GzDecoder::new(Cursor::new(data.clone()));
724 let mut decompressed = Vec::new();
725 if decoder.read_to_end(&mut decompressed).is_ok() {
726 Box::new(BufReader::new(Cursor::new(decompressed)))
727 } else {
728 return self.advance_to_next_repo();
730 }
731 } else {
732 Box::new(BufReader::new(Cursor::new(data.clone())))
733 };
734
735 self.current_reader = Some(reader);
736 true
737 }
738}
739
740impl Iterator for AptPackageIter {
741 type Item = Result<PackageMeta, IndexError>;
742
743 fn next(&mut self) -> Option<Self::Item> {
744 loop {
745 if self.done {
746 return None;
747 }
748
749 if self.current_reader.is_none() && !self.advance_to_next_repo() {
751 return None;
752 }
753
754 let reader = self.current_reader.as_mut()?;
755 let repo = self.current_repo?;
756
757 let mut line = String::new();
758 match reader.read_line(&mut line) {
759 Ok(0) => {
760 if let Some(builder) = self.current_builder.take()
762 && let Some(pkg) = builder.build(repo)
763 {
764 self.current_reader = None;
766 return Some(Ok(pkg));
767 }
768 if !self.advance_to_next_repo() {
770 return None;
771 }
772 continue;
773 }
774 Ok(_) => {
775 let line = line.trim_end_matches('\n');
776
777 if line.is_empty() {
778 if let Some(builder) = self.current_builder.take()
780 && let Some(pkg) = builder.build(repo)
781 {
782 return Some(Ok(pkg));
783 }
784 continue;
785 }
786
787 if line.starts_with(' ') || line.starts_with('\t') {
788 continue;
789 }
790
791 if let Some((key, value)) = line.split_once(':') {
792 let key = key.trim();
793 let value = value.trim();
794 let builder = self.current_builder.get_or_insert_with(PackageBuilder::new);
795
796 match key {
797 "Package" => builder.name = Some(value.to_string()),
798 "Version" => builder.version = Some(value.to_string()),
799 "Description" => builder.description = Some(value.to_string()),
800 "Homepage" => builder.homepage = Some(value.to_string()),
801 "Vcs-Git" | "Vcs-Browser" => {
802 if builder.repository.is_none() {
803 builder.repository = Some(value.to_string());
804 }
805 }
806 "Filename" => builder.filename = Some(value.to_string()),
807 "SHA256" => builder.sha256 = Some(value.to_string()),
808 "Depends" => builder.depends = Some(value.to_string()),
809 "Provides" => builder.provides = Some(value.to_string()),
810 "Size" => builder.size = value.parse().ok(),
811 _ => {}
812 }
813 }
814 }
815 Err(e) => {
816 self.done = true;
817 return Some(Err(IndexError::Io(e)));
818 }
819 }
820 }
821 }
822}
823
824mod urlencoding {
825 pub fn encode(s: &str) -> String {
826 let mut result = String::with_capacity(s.len() * 3);
827 for c in s.chars() {
828 match c {
829 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '.' | '~' => result.push(c),
830 _ => {
831 for b in c.to_string().bytes() {
832 result.push_str(&format!("%{:02X}", b));
833 }
834 }
835 }
836 }
837 result
838 }
839}