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
101impl AptRepo {
102    /// Get the distribution and component parts.
103    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    /// Get the Packages.gz URL for this repository.
135    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    /// Get the repository name for tagging.
144    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    /// All available repositories.
176    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    /// Stable repositories only.
203    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    /// Testing repositories only.
216    pub fn testing() -> &'static [AptRepo] {
217        &[
218            Self::TestingMain,
219            Self::TestingContrib,
220            Self::TestingNonFree,
221            Self::TestingNonFreeFirmware,
222        ]
223    }
224
225    /// Unstable repositories only.
226    pub fn unstable() -> &'static [AptRepo] {
227        &[
228            Self::UnstableMain,
229            Self::UnstableContrib,
230            Self::UnstableNonFree,
231            Self::UnstableNonFreeFirmware,
232        ]
233    }
234
235    /// Free (main only) repositories.
236    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    /// Oldstable repositories only.
248    pub fn oldstable() -> &'static [AptRepo] {
249        &[
250            Self::OldstableMain,
251            Self::OldstableContrib,
252            Self::OldstableNonFree,
253        ]
254    }
255}
256
257/// APT package index fetcher with configurable repositories.
258pub struct Apt {
259    repos: Vec<AptRepo>,
260}
261
262impl Apt {
263    /// Create a fetcher with all repositories.
264    pub fn all() -> Self {
265        Self {
266            repos: AptRepo::all().to_vec(),
267        }
268    }
269
270    /// Create a fetcher with stable repositories only.
271    pub fn stable() -> Self {
272        Self {
273            repos: AptRepo::stable().to_vec(),
274        }
275    }
276
277    /// Create a fetcher with testing repositories only.
278    pub fn testing() -> Self {
279        Self {
280            repos: AptRepo::testing().to_vec(),
281        }
282    }
283
284    /// Create a fetcher with unstable repositories only.
285    pub fn unstable() -> Self {
286        Self {
287            repos: AptRepo::unstable().to_vec(),
288        }
289    }
290
291    /// Create a fetcher with free repositories only.
292    pub fn free() -> Self {
293        Self {
294            repos: AptRepo::free().to_vec(),
295        }
296    }
297
298    /// Create a fetcher with oldstable repositories only.
299    pub fn oldstable() -> Self {
300        Self {
301            repos: AptRepo::oldstable().to_vec(),
302        }
303    }
304
305    /// Create a fetcher with custom repository selection.
306    pub fn with_repos(repos: &[AptRepo]) -> Self {
307        Self {
308            repos: repos.to_vec(),
309        }
310    }
311
312    /// Parse a Packages file in Debian control format.
313    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                // End of stanza
321                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                // Continuation line - skip for now
331                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        // Handle last stanza
361        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    /// Load packages from a single repository.
371    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        // Check if data is gzip compressed
383        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    /// Load packages from all configured repositories in parallel.
393    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    /// Fetch raw repo data without parsing.
414    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    /// Load raw data for all configured repos (parallel fetch).
427    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        // Use the API endpoint for single package lookup
446        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        // Load raw data for all repos (parallel), then stream parse sequentially
519        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        // Try loading from repos first
528        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        // Fall back to search API
548        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        // Store dependencies in extra
593        if let Some(deps) = self.depends {
594            let parsed_deps: Vec<String> = deps
595                .split(',')
596                .map(|d| {
597                    // Strip version constraints: "libc6 (>= 2.17)" -> "libc6"
598                    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        // Store provides (virtual packages and shared libraries)
618        if let Some(provides) = self.provides {
619            let parsed_provides: Vec<String> = provides
620                .split(',')
621                .map(|p| {
622                    // Strip version constraints: "libfoo.so.1 (= 1.0)" -> "libfoo.so.1"
623                    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        // Store size in extra
645        if let Some(size) = self.size {
646            extra.insert("size".to_string(), serde_json::Value::Number(size.into()));
647        }
648
649        // Tag with source repo
650        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
674/// Owning iterator over packages from multiple APT repos.
675/// Holds the loaded data and iterates through repos sequentially.
676pub struct AptPackageIter {
677    /// Loaded repo data: (data bytes, repo enum)
678    repos_data: Vec<(Vec<u8>, AptRepo)>,
679    /// Current repo index
680    current_repo_idx: usize,
681    /// Current reader (Box to handle different reader types)
682    current_reader: Option<Box<dyn BufRead + Send>>,
683    /// Current repo being processed
684    current_repo: Option<AptRepo>,
685    /// Package builder for current stanza
686    current_builder: Option<PackageBuilder>,
687    /// Whether we've finished all repos
688    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        // Create reader, handling gzip if needed
714        let reader: Box<dyn BufRead + Send> =
715            if data.len() >= 2 && data[0] == 0x1f && data[1] == 0x8b {
716                // Gzip compressed - decompress into memory first
717                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                    // Decompression failed, skip this repo
723                    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            // Initialize first repo if needed
744            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                    // EOF for this repo - flush builder and move to next
755                    if let Some(builder) = self.current_builder.take() {
756                        if let Some(pkg) = builder.build(repo) {
757                            // Move to next repo before returning
758                            self.current_reader = None;
759                            return Some(Ok(pkg));
760                        }
761                    }
762                    // No package to yield, advance to next repo
763                    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                        // End of stanza - yield package if complete
773                        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}