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