Skip to main content

normalize_package_index/index/
apt.rs

1//! APT package index fetcher (Debian).
2//!
3//! Fetches package metadata from Debian repositories by parsing
4//! Packages files from mirror indices.
5//!
6//! ## API Strategy
7//! - **fetch**: Uses sources.debian.org API
8//! - **fetch_versions**: Same API
9//! - **search**: Filters cached Packages entries
10//! - **fetch_all**: Returns all packages from configured repos (cached 1 hour)
11//!
12//! ## Multi-repo Support
13//! ```rust,ignore
14//! use normalize_packages::index::apt::{Apt, AptRepo};
15//!
16//! // All repos (default)
17//! let all = Apt::all();
18//!
19//! // Stable repos only
20//! let stable = Apt::stable();
21//!
22//! // Testing repos
23//! let testing = Apt::testing();
24//!
25//! // Custom selection
26//! let custom = Apt::with_repos(&[AptRepo::StableMain, AptRepo::StableContrib]);
27//! ```
28
29use 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
37/// Default cache TTL for package indices (1 hour).
38const INDEX_CACHE_TTL: Duration = Duration::from_secs(60 * 60);
39
40/// Debian mirror URL.
41const DEBIAN_MIRROR: &str = "https://deb.debian.org/debian";
42
43/// Available Debian repositories.
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
45pub enum AptRepo {
46    // === Stable (bookworm) ===
47    /// Stable main repository
48    StableMain,
49    /// Stable contrib repository
50    StableContrib,
51    /// Stable non-free repository
52    StableNonFree,
53    /// Stable non-free-firmware repository
54    StableNonFreeFirmware,
55
56    // === Stable Backports ===
57    /// Stable backports main
58    StableBackportsMain,
59    /// Stable backports contrib
60    StableBackportsContrib,
61    /// Stable backports non-free
62    StableBackportsNonFree,
63
64    // === Testing (trixie) ===
65    /// Testing main repository
66    TestingMain,
67    /// Testing contrib repository
68    TestingContrib,
69    /// Testing non-free repository
70    TestingNonFree,
71    /// Testing non-free-firmware repository
72    TestingNonFreeFirmware,
73
74    // === Unstable (sid) ===
75    /// Unstable main repository
76    UnstableMain,
77    /// Unstable contrib repository
78    UnstableContrib,
79    /// Unstable non-free repository
80    UnstableNonFree,
81    /// Unstable non-free-firmware repository
82    UnstableNonFreeFirmware,
83
84    // === Experimental ===
85    /// Experimental main repository
86    ExperimentalMain,
87    /// Experimental contrib repository
88    ExperimentalContrib,
89    /// Experimental non-free repository
90    ExperimentalNonFree,
91
92    // === Oldstable (bullseye) ===
93    /// Oldstable main repository
94    OldstableMain,
95    /// Oldstable contrib repository
96    OldstableContrib,
97    /// Oldstable non-free repository
98    OldstableNonFree,
99}
100
101struct DistComponent {
102    dist: &'static str,
103    component: &'static str,
104}
105
106impl AptRepo {
107    /// Get the distribution and component parts.
108    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    /// Get the Packages.gz URL for this repository.
141    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    /// Get the repository name for tagging.
150    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    /// All available repositories.
182    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    /// Stable repositories only.
209    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    /// Testing repositories only.
222    pub fn testing() -> &'static [AptRepo] {
223        &[
224            Self::TestingMain,
225            Self::TestingContrib,
226            Self::TestingNonFree,
227            Self::TestingNonFreeFirmware,
228        ]
229    }
230
231    /// Unstable repositories only.
232    pub fn unstable() -> &'static [AptRepo] {
233        &[
234            Self::UnstableMain,
235            Self::UnstableContrib,
236            Self::UnstableNonFree,
237            Self::UnstableNonFreeFirmware,
238        ]
239    }
240
241    /// Free (main only) repositories.
242    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    /// Oldstable repositories only.
254    pub fn oldstable() -> &'static [AptRepo] {
255        &[
256            Self::OldstableMain,
257            Self::OldstableContrib,
258            Self::OldstableNonFree,
259        ]
260    }
261}
262
263/// APT package index fetcher with configurable repositories.
264pub struct Apt {
265    repos: Vec<AptRepo>,
266}
267
268impl Apt {
269    /// Create a fetcher with all repositories.
270    pub fn all() -> Self {
271        Self {
272            repos: AptRepo::all().to_vec(),
273        }
274    }
275
276    /// Create a fetcher with stable repositories only.
277    pub fn stable() -> Self {
278        Self {
279            repos: AptRepo::stable().to_vec(),
280        }
281    }
282
283    /// Create a fetcher with testing repositories only.
284    pub fn testing() -> Self {
285        Self {
286            repos: AptRepo::testing().to_vec(),
287        }
288    }
289
290    /// Create a fetcher with unstable repositories only.
291    pub fn unstable() -> Self {
292        Self {
293            repos: AptRepo::unstable().to_vec(),
294        }
295    }
296
297    /// Create a fetcher with free repositories only.
298    pub fn free() -> Self {
299        Self {
300            repos: AptRepo::free().to_vec(),
301        }
302    }
303
304    /// Create a fetcher with oldstable repositories only.
305    pub fn oldstable() -> Self {
306        Self {
307            repos: AptRepo::oldstable().to_vec(),
308        }
309    }
310
311    /// Create a fetcher with custom repository selection.
312    pub fn with_repos(repos: &[AptRepo]) -> Self {
313        Self {
314            repos: repos.to_vec(),
315        }
316    }
317
318    /// Parse a Packages file in Debian control format.
319    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                // End of stanza
327                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                // Continuation line - skip for now
337                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        // Handle last stanza
367        if let Some(builder) = current
368            && let Some(pkg) = builder.build(repo)
369        {
370            packages.push(pkg);
371        }
372
373        packages
374    }
375
376    /// Load packages from a single repository.
377    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        // Check if data is gzip compressed
389        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    /// Load packages from all configured repositories in parallel.
399    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    /// Fetch raw repo data without parsing.
420    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    /// Load raw data for all configured repos (parallel fetch).
433    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        // Use the API endpoint for single package lookup
452        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        // Load raw data for all repos (parallel), then stream parse sequentially
525        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        // Try loading from repos first
534        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        // Fall back to search API
554        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        // Store dependencies in extra
599        if let Some(deps) = self.depends {
600            let parsed_deps: Vec<String> = deps
601                .split(',')
602                .map(|d| {
603                    // Strip version constraints: "libc6 (>= 2.17)" -> "libc6"
604                    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        // Store provides (virtual packages and shared libraries)
624        if let Some(provides) = self.provides {
625            let parsed_provides: Vec<String> = provides
626                .split(',')
627                .map(|p| {
628                    // Strip version constraints: "libfoo.so.1 (= 1.0)" -> "libfoo.so.1"
629                    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        // Store size in extra
651        if let Some(size) = self.size {
652            extra.insert("size".to_string(), serde_json::Value::Number(size.into()));
653        }
654
655        // Tag with source repo
656        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
680/// Owning iterator over packages from multiple APT repos.
681/// Holds the loaded data and iterates through repos sequentially.
682pub struct AptPackageIter {
683    /// Loaded repo data: (data bytes, repo enum)
684    repos_data: Vec<(Vec<u8>, AptRepo)>,
685    /// Current repo index
686    current_repo_idx: usize,
687    /// Current reader (Box to handle different reader types)
688    current_reader: Option<Box<dyn BufRead + Send>>,
689    /// Current repo being processed
690    current_repo: Option<AptRepo>,
691    /// Package builder for current stanza
692    current_builder: Option<PackageBuilder>,
693    /// Whether we've finished all repos
694    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        // Create reader, handling gzip if needed
720        let reader: Box<dyn BufRead + Send> =
721            if data.len() >= 2 && data[0] == 0x1f && data[1] == 0x8b {
722                // Gzip compressed - decompress into memory first
723                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                    // Decompression failed, skip this repo
729                    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            // Initialize first repo if needed
750            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                    // EOF for this repo - flush builder and move to next
761                    if let Some(builder) = self.current_builder.take()
762                        && let Some(pkg) = builder.build(repo)
763                    {
764                        // Move to next repo before returning
765                        self.current_reader = None;
766                        return Some(Ok(pkg));
767                    }
768                    // No package to yield, advance to next repo
769                    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                        // End of stanza - yield package if complete
779                        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}