Skip to main content

apt_sources/
lib.rs

1#![deny(missing_docs)]
2//! A library for parsing and manipulating APT source files that
3//! use the DEB822 format to hold package repositories specifications.
4//!
5//! <div class="warning">
6//!
7//! Currently only lossy _serialization_ is implemented, lossless support
8//! retaining file sequence and comments would come at later date.
9//!
10//! </div>
11//!
12//! # Examples
13//!
14//! ```rust
15//!
16//! use apt_sources::Repositories;
17//! use std::path::Path;
18//!
19//! let text = r#"Types: deb
20//! URIs: http://ports.ubuntu.com/
21//! Suites: noble
22//! Components: stable
23//! Architectures: arm64
24//! Signed-By:
25//!  -----BEGIN PGP PUBLIC KEY BLOCK-----
26//!  .
27//!  mDMEY865UxYJKwYBBAHaRw8BAQdAd7Z0srwuhlB6JKFkcf4HU4SSS/xcRfwEQWzr
28//!  crf6AEq0SURlYmlhbiBTdGFibGUgUmVsZWFzZSBLZXkgKDEyL2Jvb2t3b3JtKSA8
29//!  ZGViaWFuLXJlbGVhc2VAbGlzdHMuZGViaWFuLm9yZz6IlgQTFggAPhYhBE1k/sEZ
30//!  wgKQZ9bnkfjSWFuHg9SBBQJjzrlTAhsDBQkPCZwABQsJCAcCBhUKCQgLAgQWAgMB
31//!  Ah4BAheAAAoJEPjSWFuHg9SBSgwBAP9qpeO5z1s5m4D4z3TcqDo1wez6DNya27QW
32//!  WoG/4oBsAQCEN8Z00DXagPHbwrvsY2t9BCsT+PgnSn9biobwX7bDDg==
33//!  =5NZE
34//!  -----END PGP PUBLIC KEY BLOCK-----"#;
35//!
36//! let r = text.parse::<Repositories>().unwrap();
37//! let suites = r[0].suites();
38//! assert_eq!(suites[0], "noble");
39//! ```
40//!
41// TODO: Not supported yet:
42// See the ``lossless`` module (behind the ``lossless`` feature) for a more forgiving parser that
43// allows partial parsing, parsing files with errors and unknown fields and editing while
44// preserving formatting.
45
46use deb822_fast::{FromDeb822, FromDeb822Paragraph, ToDeb822, ToDeb822Paragraph};
47use error::{LoadError, RepositoryError};
48use itertools::Itertools;
49#[cfg(feature = "legacy")]
50use legacy::LegacyRepositories;
51use signature::Signature;
52use std::path::Path;
53use std::result::Result;
54use std::{collections::HashSet, ops::Deref, str::FromStr};
55use url::Url;
56
57/// Distribution detection and utilities
58pub mod distribution;
59pub mod error;
60/// Key management utilities for GPG keys
61#[cfg(feature = "key-management")]
62pub mod key_management;
63#[cfg(feature = "key-management")]
64pub mod keyserver;
65/// Launchpad PPA (Personal Package Archive) integration
66#[cfg(feature = "launchpad")]
67pub mod launchpad;
68#[cfg(feature = "legacy")]
69pub mod legacy;
70pub mod signature;
71/// Module for managing APT source lists
72pub mod sources_manager;
73/// General utilities
74pub mod utils;
75
76/// A representation of the repository type, by role of packages it can provide, either `Binary`
77/// (indicated by `deb`) or `Source` (indicated by `deb-src`).
78#[derive(PartialEq, Eq, Hash, Debug, Clone)]
79pub enum RepositoryType {
80    /// Repository with binary packages, indicated as `deb`
81    Binary,
82    /// Repository with source packages, indicated as `deb-src`
83    Source,
84}
85
86impl FromStr for RepositoryType {
87    type Err = RepositoryError;
88
89    fn from_str(s: &str) -> Result<Self, Self::Err> {
90        match s {
91            "deb" => Ok(RepositoryType::Binary),
92            "deb-src" => Ok(RepositoryType::Source),
93            _ => Err(RepositoryError::InvalidType),
94        }
95    }
96}
97
98impl From<&RepositoryType> for String {
99    fn from(value: &RepositoryType) -> Self {
100        match value {
101            RepositoryType::Binary => "deb".to_owned(),
102            RepositoryType::Source => "deb-src".to_owned(),
103        }
104    }
105}
106
107impl std::fmt::Display for RepositoryType {
108    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109        let s = match self {
110            RepositoryType::Binary => "deb",
111            RepositoryType::Source => "deb-src",
112        };
113        write!(f, "{s}")
114    }
115}
116
117#[derive(Debug, Clone, Copy, PartialEq)]
118/// Enumeration for fields like `By-Hash` which have third value of `force`
119pub enum YesNoForce {
120    /// True
121    Yes,
122    /// False
123    No,
124    /// Forced
125    Force,
126}
127
128impl FromStr for YesNoForce {
129    type Err = RepositoryError;
130
131    fn from_str(s: &str) -> Result<Self, Self::Err> {
132        match s {
133            "yes" => Ok(Self::Yes),
134            "no" => Ok(Self::No),
135            "force" => Ok(Self::Force),
136            _ => Err(RepositoryError::YesNoForceFieldInvalid),
137        }
138    }
139}
140
141impl From<&YesNoForce> for String {
142    fn from(value: &YesNoForce) -> Self {
143        match value {
144            YesNoForce::Yes => "yes".to_owned(),
145            YesNoForce::No => "no".to_owned(),
146            YesNoForce::Force => "force".to_owned(),
147        }
148    }
149}
150
151impl std::fmt::Display for YesNoForce {
152    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153        let s = match self {
154            YesNoForce::Yes => "yes",
155            YesNoForce::No => "no",
156            YesNoForce::Force => "force",
157        };
158        write!(f, "{s}")
159    }
160}
161
162fn deserialize_types(text: &str) -> Result<HashSet<RepositoryType>, RepositoryError> {
163    text.split_whitespace()
164        .map(RepositoryType::from_str)
165        .collect::<Result<HashSet<RepositoryType>, RepositoryError>>()
166}
167
168fn serialize_types(files: &HashSet<RepositoryType>) -> String {
169    files.iter().map(|rt| rt.to_string()).join("\n")
170}
171
172fn deserialize_uris(text: &str) -> Result<Vec<Url>, String> {
173    // TODO: bad error type
174    text.split_whitespace()
175        .map(Url::from_str)
176        .collect::<Result<Vec<Url>, _>>()
177        .map_err(|e| e.to_string()) // TODO: bad error type
178}
179
180fn serialize_uris(uris: &[Url]) -> String {
181    uris.iter().map(|u| u.as_str()).join(" ")
182}
183
184fn deserialize_string_chain(text: &str) -> Result<Vec<String>, String> {
185    // TODO: bad error type
186    Ok(text.split_whitespace().map(|x| x.to_string()).collect())
187}
188
189fn deserialize_yesno(text: &str) -> Result<bool, RepositoryError> {
190    // TODO: bad error type
191    match text {
192        "yes" => Ok(true),
193        "no" => Ok(false),
194        _ => Err(RepositoryError::YesNoFieldInvalid),
195    }
196}
197
198fn serializer_yesno(value: &bool) -> String {
199    if *value {
200        "yes".to_string()
201    } else {
202        "no".to_string()
203    }
204}
205
206fn serialize_string_chain(chain: &[String]) -> String {
207    chain.join(" ")
208}
209
210/// A structure representing APT repository as declared by DEB822 source file
211///
212/// According to `sources.list(5)` man pages, only four fields are mandatory:
213/// * `Types` either `deb` or/and `deb-src`
214/// * `URIs` to repositories holding valid APT structure (unclear if multiple are allowed)
215/// * `Suites` usually being distribution codenames
216/// * `Component` most of the time `main`, but it's a section of the repository
217///
218/// The manpage specifies following optional fields
219/// * `Enabled`        is a yes/no field, default yes
220/// * `Architectures`
221/// * `Languages`
222/// * `Targets`
223/// * `PDiffs`         is a yes/no field
224/// * `By-Hash`        is a yes/no/force field
225/// * `Allow-Insecure` is a yes/no field, default no
226/// * `Allow-Weak`     is a yes/no field, default no
227/// * `Allow-Downgrade-To-Insecure` is a yes/no field, default no
228/// * `Trusted`        us a yes/no field
229/// * `Signed-By`      is either path to the key or PGP key block
230/// * `Check-Valid-Until` is a yes/no field
231/// * `Valid-Until-Min`
232/// * `Valid-Until-Max`
233/// * `Check-Date`     is a yes/no field
234/// * `Date-Max-Future`
235/// * `InRelease-Path` relative path
236/// * `Snapshot`       either `enable` or a snapshot ID
237///
238/// The unit tests of APT use:
239/// * `Description`
240///
241/// The RepoLib tool uses:
242/// * `X-Repolib-Name` identifier for own reference, meaningless for APT
243///
244/// Note: Multivalues `*-Add` & `*-Remove` semantics aren't supported.
245#[derive(FromDeb822, ToDeb822, Clone, PartialEq, /*Eq,*/ Debug, Default)]
246pub struct Repository {
247    /// If `no` (false) the repository is ignored by APT
248    #[deb822(field = "Enabled", deserialize_with = deserialize_yesno, serialize_with = serializer_yesno)]
249    // TODO: support for `default` if omitted is missing
250    pub enabled: Option<bool>,
251
252    /// The value `RepositoryType::Binary` (`deb`) or/and `RepositoryType::Source` (`deb-src`)
253    #[deb822(field = "Types", deserialize_with = deserialize_types, serialize_with = serialize_types)]
254    pub types: HashSet<RepositoryType>, // consider alternative, closed set
255    /// The address of the repository
256    #[deb822(field = "URIs", deserialize_with = deserialize_uris, serialize_with = serialize_uris)]
257    pub uris: Vec<Url>, // according to Debian that's URI, but this type is more advanced than URI from `http` crate
258    /// The distribution name as codename or suite type (like `stable` or `testing`)
259    #[deb822(field = "Suites", deserialize_with = deserialize_string_chain, serialize_with = serialize_string_chain)]
260    pub suites: Vec<String>,
261    /// (Optional) Section of the repository, usually `main`, `contrib` or `non-free`
262    /// return `None` if repository is Flat Repository Format (<https://wiki.debian.org/DebianRepository/Format#Flat_Repository_Format>)
263    #[deb822(field = "Components", deserialize_with = deserialize_string_chain, serialize_with = serialize_string_chain)]
264    pub components: Option<Vec<String>>,
265
266    /// (Optional) Architectures binaries from this repository run on
267    #[deb822(field = "Architectures", deserialize_with = deserialize_string_chain, serialize_with = serialize_string_chain)]
268    pub architectures: Option<Vec<String>>,
269    /// (Optional) Translations support to download
270    #[deb822(field = "Languages", deserialize_with = deserialize_string_chain, serialize_with = serialize_string_chain)]
271    pub languages: Option<Vec<String>>, // TODO: Option is redundant to empty vectors
272    /// (Optional) Download targets to acquire from this source
273    #[deb822(field = "Targets", deserialize_with = deserialize_string_chain, serialize_with = serialize_string_chain)]
274    pub targets: Option<Vec<String>>,
275    /// (Optional) Controls if APT should try PDiffs instead of downloading indexes entirely; if not set defaults to configuration option `Acquire::PDiffs`
276    #[deb822(field = "PDiffs", deserialize_with = deserialize_yesno)]
277    pub pdiffs: Option<bool>,
278    /// (Optional) Controls if APT should try to acquire indexes via a URI constructed from a hashsum of the expected file
279    #[deb822(field = "By-Hash")]
280    pub by_hash: Option<YesNoForce>,
281    /// (Optional) If yes circumvents parts of `apt-secure`, don't thread lightly
282    #[deb822(field = "Allow-Insecure")]
283    pub allow_insecure: Option<bool>, // TODO: redundant option, not present = default no
284    /// (Optional) If yes circumvents parts of `apt-secure`, don't thread lightly
285    #[deb822(field = "Allow-Weak")]
286    pub allow_weak: Option<bool>, // TODO: redundant option, not present = default no
287    /// (Optional) If yes circumvents parts of `apt-secure`, don't thread lightly
288    #[deb822(field = "Allow-Downgrade-To-Insecure")]
289    pub allow_downgrade_to_insecure: Option<bool>, // TODO: redundant option, not present = default no
290    /// (Optional) If set forces whether APT considers source as rusted or no (default not present is a third state)
291    #[deb822(field = "Trusted")]
292    pub trusted: Option<bool>,
293    /// (Optional) Contains either absolute path to GPG keyring or embedded GPG public key block, if not set APT uses all trusted keys;
294    /// I can't find example of using with fingerprints
295    #[deb822(field = "Signed-By")]
296    pub signature: Option<Signature>,
297
298    /// (Optional) Field ignored by APT but used by RepoLib to identify repositories, Ubuntu sources contain them
299    #[deb822(field = "X-Repolib-Name")]
300    pub x_repolib_name: Option<String>, // this supports RepoLib still used by PopOS, even if removed from Debian/Ubuntu
301
302    /// (Optional) Field not present in the man page, but used in APT unit tests, potentially to hold the repository description
303    #[deb822(field = "Description")]
304    pub description: Option<String>, // options: HashMap<String, String> // My original parser kept remaining optional fields in the hash map, is this right approach?
305}
306
307impl Repository {
308    /// Returns slice of strings containing suites for which this repository provides
309    pub fn suites(&self) -> &[String] {
310        self.suites.as_slice()
311    }
312
313    /// Returns the repository types (deb/deb-src)
314    pub fn types(&self) -> &HashSet<RepositoryType> {
315        &self.types
316    }
317
318    /// Returns the repository URIs
319    pub fn uris(&self) -> &[Url] {
320        &self.uris
321    }
322
323    /// Returns the repository components
324    pub fn components(&self) -> Option<&[String]> {
325        self.components.as_deref()
326    }
327
328    /// Returns the repository architectures
329    pub fn architectures(&self) -> &[String] {
330        self.architectures.as_deref().unwrap_or(&[])
331    }
332}
333
334/// Container for multiple `Repository` specifications as single `.sources` file may contain as per specification
335#[derive(Debug, Clone, PartialEq)]
336pub struct Repositories(Vec<Repository>);
337
338impl Default for Repositories {
339    /// Creates a default instance by loading repositories from /etc/apt/
340    ///
341    /// Errors are logged as warnings (if the `tracing` feature is enabled) but
342    /// don't prevent loading valid repositories (like APT does)
343    fn default() -> Self {
344        let (repos, errors) = Self::load_from_directory(std::path::Path::new("/etc/apt"));
345
346        // Log any errors encountered, but continue (like APT)
347        #[cfg(feature = "tracing")]
348        for error in errors {
349            tracing::warn!("Failed to load APT source: {}", error);
350        }
351
352        // Without tracing feature, errors are silently ignored
353        #[cfg(not(feature = "tracing"))]
354        let _ = errors;
355
356        repos
357    }
358}
359
360impl Repositories {
361    /// Creates empty container of repositories
362    pub fn empty() -> Self {
363        Repositories(Vec::new())
364    }
365
366    /// Creates repositories from container consisting `Repository` instances
367    pub fn new<Container>(container: Container) -> Self
368    where
369        Container: Into<Vec<Repository>>,
370    {
371        Repositories(container.into())
372    }
373
374    /// Load repositories from a directory (e.g., /etc/apt/)
375    ///
376    /// This will load:
377    /// - sources.list file from the directory
378    /// - All *.list files from sources.list.d/ subdirectory (in lexicographical order)
379    /// - All *.sources files from sources.list.d/ subdirectory (in lexicographical order)
380    ///
381    /// Returns a tuple of (successfully loaded repositories, errors encountered).
382    /// This method is resilient like APT - errors in individual files don't prevent
383    /// loading other valid repositories.
384    ///
385    /// <div class="warning">
386    /// This loads all repositories from all files, but information about which file they're
387    /// loaded from is **lost** in the process.u
388    /// </div>
389    pub fn load_from_directory(path: &Path) -> (Self, Vec<LoadError>) {
390        use std::fs;
391
392        let mut all_repositories = Repositories::empty();
393        let mut errors = Vec::new();
394
395        // Process main sources.list file if it exists
396        let main_sources = path.join("sources.list");
397        #[cfg(not(feature = "legacy"))]
398        eprintln!(
399            "WARNING! `{}` hasn't been read as `legacy` support hadn't been enabled during build.",
400            main_sources.display()
401        );
402        #[cfg(feature = "legacy")]
403        if main_sources.exists() {
404            match fs::read_to_string(&main_sources) {
405                Ok(content) => {
406                    match LegacyRepositories::from_str(&content)/*Self::parse_legacy_format(&content)*/ {
407                        Ok(repos) => {
408                           // let legacy_repos = .collect::<Vec<Repository>, _>>();
409                            all_repositories.extend(repos.repositories().map(|l| l.into()))
410                        },
411                        Err(e) => errors.push(LoadError::Parse {
412                            path: main_sources,
413                            error: e.to_string(), // TODO [MF]: doesn't look right, we shall have error type for every kind
414                        }),
415                    }
416                }
417                Err(e) => errors.push(LoadError::Io {
418                    path: main_sources,
419                    error: e,
420                }),
421            }
422        }
423
424        // Process files from sources.list.d/ directory
425        let sources_d = path.join("sources.list.d");
426        if !sources_d.is_dir() {
427            return (all_repositories, errors);
428        }
429
430        let entries = match fs::read_dir(&sources_d) {
431            Ok(entries) => entries,
432            Err(e) => {
433                errors.push(LoadError::DirectoryRead {
434                    path: sources_d,
435                    error: e,
436                });
437                return (all_repositories, errors);
438            }
439        };
440
441        // Collect and sort entries lexicographically like APT does
442        let mut entry_paths: Vec<_> = entries
443            .filter_map(|entry| entry.ok())
444            .map(|entry| entry.path())
445            .filter(|p| p.is_file())
446            .filter(|p| {
447                p.file_name()
448                    .and_then(|n| n.to_str())
449                    .map(|n| n.ends_with(".list") || n.ends_with(".sources"))
450                    .unwrap_or(false)
451            })
452            .collect();
453        entry_paths.sort();
454
455        for file_path in entry_paths {
456            let file_name = file_path.file_name().and_then(|n| n.to_str()).unwrap_or("");
457
458            let content = match fs::read_to_string(&file_path) {
459                Ok(content) => content,
460                Err(e) => {
461                    errors.push(LoadError::Io {
462                        path: file_path,
463                        error: e,
464                    });
465                    continue;
466                }
467            };
468
469            let parse_result = if file_name.ends_with(".list") {
470                #[cfg(not(feature = "legacy"))]
471                {
472                    eprintln!("WARNING! `{file_name}` hasn't been read as `legacy` support hadn't been enabled during build.");
473                    Err(LoadError::UnsupportedLegacyFormat)
474                }
475                #[cfg(feature = "legacy")]
476                LegacyRepositories::from_str(&content)
477                    .map(|repos| repos.repositories().map(|l| l.into()).collect())
478                    .map_err(|e| LoadError::Parse {
479                        path: file_path,
480                        error: e.to_string(), // TODO [MF]: looks like it's time for `thiserror`
481                    })
482            } else if file_name.ends_with(".sources") {
483                content
484                    .parse::<Repositories>()
485                    .map(|repos| repos.0)
486                    .map_err(|e| LoadError::Parse {
487                        path: file_path,
488                        error: e,
489                    })
490            } else {
491                continue;
492            };
493
494            match parse_result {
495                Ok(repos) => all_repositories.extend(repos),
496                Err(e) => errors.push(e),
497            }
498        }
499
500        (all_repositories, errors)
501    }
502
503    /// Load repositories from a directory, failing on first error
504    ///
505    /// Use this when you want strict error handling and need the loading
506    /// to stop at the first problem encountered.
507    pub fn load_from_directory_strict(path: &Path) -> Result<Self, LoadError> {
508        let (repos, errors) = Self::load_from_directory(path);
509        if let Some(error) = errors.into_iter().next() {
510            Err(error)
511        } else {
512            Ok(repos)
513        }
514    }
515
516    /// Push a new repository
517    pub fn push(&mut self, repo: Repository) {
518        self.0.push(repo);
519    }
520
521    /// Retain repositories matching a predicate
522    pub fn retain<F>(&mut self, f: F)
523    where
524        F: FnMut(&Repository) -> bool,
525    {
526        self.0.retain(f);
527    }
528
529    /// Get iterator over repositories
530    pub fn iter(&self) -> std::slice::Iter<'_, Repository> {
531        self.0.iter()
532    }
533
534    /// Get mutable iterator over repositories
535    pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, Repository> {
536        self.0.iter_mut()
537    }
538
539    /// Extend with an iterator of repositories
540    pub fn extend<I>(&mut self, iter: I)
541    where
542        I: IntoIterator<Item = Repository>,
543    {
544        self.0.extend(iter);
545    }
546
547    /// Check if empty
548    pub fn is_empty(&self) -> bool {
549        self.0.is_empty()
550    }
551}
552
553impl std::str::FromStr for Repositories {
554    type Err = String;
555
556    fn from_str(s: &str) -> Result<Self, Self::Err> {
557        let deb822: deb822_fast::Deb822 =
558            s.parse().map_err(|e: deb822_fast::Error| e.to_string())?;
559
560        let repos = deb822
561            .iter()
562            .map(Repository::from_paragraph)
563            .collect::<Result<Vec<Repository>, Self::Err>>()?;
564        Ok(Repositories(repos))
565    }
566}
567
568impl std::fmt::Display for Repositories {
569    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
570        let result = self
571            .0
572            .iter()
573            .map(|r| {
574                let p: deb822_fast::Paragraph = r.to_paragraph();
575                p.to_string()
576            })
577            .collect::<Vec<_>>()
578            .join("\n");
579        f.write_str(&result)
580    }
581}
582
583impl Deref for Repositories {
584    type Target = Vec<Repository>;
585
586    fn deref(&self) -> &Self::Target {
587        &self.0
588    }
589}
590
591#[cfg(test)]
592mod tests {
593    use std::{collections::HashSet, str::FromStr};
594
595    use indoc::indoc;
596    use url::Url;
597
598    use crate::{signature::Signature, Repositories, Repository, RepositoryType};
599
600    #[test]
601    fn test_not_machine_readable() {
602        let s = indoc!(
603            r#"
604            deb [arch=arm64 signed-by=/usr/share/keyrings/docker.gpg] http://ports.ubuntu.com/ noble stable
605        "#
606        );
607        let ret = s.parse::<Repositories>();
608        assert!(ret.is_err());
609        assert_eq!(ret.unwrap_err(), "missing field: Types".to_string());
610    }
611
612    #[test]
613    fn test_parse_flat_repo() {
614        let s = indoc! {r#"
615            Types: deb
616            URIs: http://ports.ubuntu.com/
617            Suites: ./
618            Architectures: arm64
619        "#};
620
621        let repos = s
622            .parse::<Repositories>()
623            .expect("Shall be parsed flawlessly");
624        assert!(repos[0].types.contains(&super::RepositoryType::Binary));
625    }
626
627    #[test]
628    fn test_parse_without_architectures() {
629        // Architectures is optional; Debian's own default .sources omits it.
630        let s = indoc! {r#"
631            Types: deb
632            URIs: http://deb.debian.org/debian
633            Suites: trixie
634            Components: main
635            Signed-By: /usr/share/keyrings/debian-archive-keyring.pgp
636        "#};
637
638        let repos = s
639            .parse::<Repositories>()
640            .expect("Shall be parsed flawlessly");
641        assert_eq!(repos[0].architectures, None);
642        assert_eq!(repos[0].architectures(), &[] as &[String]);
643    }
644
645    #[test]
646    fn test_parse_w_keyblock() {
647        let s = indoc!(
648            r#"
649            Types: deb
650            URIs: http://ports.ubuntu.com/
651            Suites: noble
652            Components: stable
653            Architectures: arm64
654            Signed-By:
655             -----BEGIN PGP PUBLIC KEY BLOCK-----
656             .
657             mDMEY865UxYJKwYBBAHaRw8BAQdAd7Z0srwuhlB6JKFkcf4HU4SSS/xcRfwEQWzr
658             crf6AEq0SURlYmlhbiBTdGFibGUgUmVsZWFzZSBLZXkgKDEyL2Jvb2t3b3JtKSA8
659             ZGViaWFuLXJlbGVhc2VAbGlzdHMuZGViaWFuLm9yZz6IlgQTFggAPhYhBE1k/sEZ
660             wgKQZ9bnkfjSWFuHg9SBBQJjzrlTAhsDBQkPCZwABQsJCAcCBhUKCQgLAgQWAgMB
661             Ah4BAheAAAoJEPjSWFuHg9SBSgwBAP9qpeO5z1s5m4D4z3TcqDo1wez6DNya27QW
662             WoG/4oBsAQCEN8Z00DXagPHbwrvsY2t9BCsT+PgnSn9biobwX7bDDg==
663             =5NZE
664             -----END PGP PUBLIC KEY BLOCK-----
665        "#
666        );
667
668        let repos = s
669            .parse::<Repositories>()
670            .expect("Shall be parsed flawlessly");
671        assert!(repos[0].types.contains(&super::RepositoryType::Binary));
672        assert!(matches!(repos[0].signature, Some(Signature::KeyBlock(_))));
673    }
674
675    #[test]
676    fn test_parse_w_keypath() {
677        let s = indoc!(
678            r#"
679            Types: deb
680            URIs: http://ports.ubuntu.com/
681            Suites: noble
682            Components: stable
683            Architectures: arm64
684            Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
685        "#
686        );
687
688        let reps = s
689            .parse::<Repositories>()
690            .expect("Shall be parsed flawlessly");
691        assert!(reps[0].types.contains(&super::RepositoryType::Binary));
692        assert!(matches!(reps[0].signature, Some(Signature::KeyPath(_))));
693    }
694
695    #[test]
696    fn test_serialize() {
697        //let repos = Repositories::empty();
698        let repos = Repositories::new([Repository {
699            enabled: Some(true), // TODO: looks odd, as only `Enabled: no` in meaningful
700            types: HashSet::from([RepositoryType::Binary]),
701            architectures: Some(vec!["arm64".to_owned()]),
702            uris: vec![Url::from_str("https://deb.debian.org/debian").unwrap()],
703            suites: vec!["jammy".to_owned()],
704            components: Some(vec!["main".to_owned()]),
705            signature: None,
706            x_repolib_name: None,
707            languages: None,
708            targets: None,
709            pdiffs: None,
710            ..Default::default()
711        }]);
712        let text = repos.to_string();
713        assert_eq!(
714            text,
715            indoc! {r#"
716            Enabled: yes
717            Types: deb
718            URIs: https://deb.debian.org/debian
719            Suites: jammy
720            Components: main
721            Architectures: arm64
722        "#}
723        );
724    }
725
726    #[test]
727    fn test_yesnoforce_to_string() {
728        let yes = crate::YesNoForce::Yes;
729        assert_eq!(yes.to_string(), "yes");
730
731        let no = crate::YesNoForce::No;
732        assert_eq!(no.to_string(), "no");
733
734        let force = crate::YesNoForce::Force;
735        assert_eq!(force.to_string(), "force");
736    }
737
738    #[test]
739    fn test_repository_type_display() {
740        assert_eq!(RepositoryType::Binary.to_string(), "deb");
741        assert_eq!(RepositoryType::Source.to_string(), "deb-src");
742    }
743
744    #[test]
745    fn test_yesnoforce_display() {
746        assert_eq!(crate::YesNoForce::Yes.to_string(), "yes");
747        assert_eq!(crate::YesNoForce::No.to_string(), "no");
748        assert_eq!(crate::YesNoForce::Force.to_string(), "force");
749    }
750
751    #[test]
752    fn test_repositories_is_empty() {
753        let empty_repos = Repositories::empty();
754        assert!(empty_repos.is_empty());
755
756        let mut repos = Repositories::empty();
757        repos.push(Repository::default());
758        assert!(!repos.is_empty());
759    }
760
761    #[test]
762    fn test_repository_getters() {
763        let repo = Repository {
764            types: HashSet::from([RepositoryType::Binary, RepositoryType::Source]),
765            uris: vec![Url::parse("http://example.com/debian").unwrap()],
766            suites: vec!["stable".to_string()],
767            components: Some(vec!["main".to_string(), "contrib".to_string()]),
768            architectures: Some(vec!["amd64".to_string(), "arm64".to_string()]),
769            ..Default::default()
770        };
771
772        // Test types getter
773        assert_eq!(
774            repo.types(),
775            &HashSet::from([RepositoryType::Binary, RepositoryType::Source])
776        );
777
778        // Test uris getter
779        assert_eq!(repo.uris().len(), 1);
780        assert_eq!(repo.uris()[0].to_string(), "http://example.com/debian");
781
782        // Test suites getter (existing)
783        assert_eq!(repo.suites(), vec!["stable"]);
784
785        // Test components getter
786        assert_eq!(
787            repo.components(),
788            Some(vec!["main".to_string(), "contrib".to_string()].as_slice())
789        );
790
791        // Test architectures getter
792        assert_eq!(repo.architectures(), vec!["amd64", "arm64"]);
793    }
794
795    #[test]
796    fn test_repositories_iter() {
797        let mut repos = Repositories::empty();
798        repos.push(Repository {
799            suites: vec!["stable".to_string()],
800            ..Default::default()
801        });
802        repos.push(Repository {
803            suites: vec!["testing".to_string()],
804            ..Default::default()
805        });
806
807        // Test iter()
808        let suites: Vec<_> = repos.iter().map(|r| r.suites()).collect();
809        assert_eq!(suites.len(), 2);
810        assert_eq!(suites[0], vec!["stable"]);
811        assert_eq!(suites[1], vec!["testing"]);
812
813        // Test iter_mut() - modifying through mutable iterator
814        for repo in repos.iter_mut() {
815            repo.enabled = Some(false);
816        }
817
818        for repo in repos.iter() {
819            assert_eq!(repo.enabled, Some(false));
820        }
821    }
822}