Skip to main content

apt_sources/
legacy.rs

1//! A module for parsing and manipulating APT source files that
2//! use the pre-DEB822 single line format to hold package repositories specifications.
3//!
4//! # Examples
5//! ```
6//! # use url::Url;
7//! # use apt_sources::legacy::LegacyRepositories;
8//! # use std::str::FromStr;
9//! let single_line = "deb http://archive.ubuntu.com/ubuntu jammy main restricted";
10//! let repositories = LegacyRepositories::from_str(single_line)
11//!     .expect("Shall not fail for correct list entry!");
12//! assert_eq!(repositories.len(), 1);
13//! let repository = repositories.iter().nth(0).expect("Shall not fail for first line");
14//! assert_eq!(repository.uri, "http://archive.ubuntu.com/ubuntu".parse::<Url>().unwrap());
15//! ```
16use super::RepositoryError;
17use super::RepositoryType;
18use super::Signature;
19use super::YesNoForce;
20use itertools::Itertools;
21use regex::Regex;
22use std::borrow::Cow;
23use std::collections::HashSet;
24use std::fmt::Display;
25use std::ops::Deref;
26use std::ops::Not;
27use std::path::PathBuf;
28use std::str::FromStr;
29use std::sync::LazyLock;
30use url::Url;
31
32/// A structure representing APT repository as declared by one-line-style `.list` file:
33/// ```text
34/// type [option=value option=value...] uri suite [component] [component] [...]
35/// ```
36/// According to `sources.list(5)` man pages, only four fields are mandatory:
37/// * `type` either `deb` or `deb-src`
38/// * `uri` to repository holding valid APT structure
39/// * `suite` usually being distribution codename
40/// * `component` most of the time `main`, but it's a section of the repository; multiple values allowed
41///
42/// The disabled field is just commented out with `#` followed by whitespaces at the beginning of the valid line
43///
44/// The manpage specifies following optional fields
45/// * `arch`           comma separated list of binary architectures
46/// * `lang`           comma separated list of supported natural languages
47/// * `target`
48/// * `pdiffs`         is a yes/no field
49/// * `by-hash`        is a yes/no/force field
50/// * `allow-insecure` is a yes/no field, default no
51/// * `allow-weak`     is a yes/no field, default no
52/// * `allow-downgrade-to-insecure` is a yes/no field, default no
53/// * `trusted`        us a yes/no field
54/// * `signed-by`      is a path to the key or fingerprint; optionally followed by exclamation mark
55/// * `check-valid-until` is a yes/no field
56/// * `valid-until-min`
57/// * `valid-until-max`
58/// * `check-date`     is a yes/no field
59/// * `date-max-future`
60/// * `inrelease-path` relative path
61/// * `snapshot`       either `enable` or a snapshot ID
62///
63/// Note: this module doesn't support undocumented options.
64#[derive(Clone, PartialEq, /*Eq,*/ Debug)]
65pub struct LegacyRepository {
66    /// This doesn't represent real field, but rather commented or uncommented line
67    enabled: bool,
68    /// Legacy lists format support one type per line
69    pub typ: RepositoryType,
70    /// Single repo address; according to Debian that's URI, but this type is more advanced than URI from `http` crate
71    pub uri: Url,
72    /// The distribution name as codename or suite type (like `stable` or `testing`)
73    pub suite: String,
74    /// (Optional) Section of the repository, usually `main`, `contrib` or `non-free`
75    /// return `None` if repository is Flat Repository Format (<https://wiki.debian.org/DebianRepository/Format#Flat_Repository_Format>)
76    pub components: Vec<String>,
77
78    /// (Optional) Architectures binaries from this repository run on
79    pub architectures: Vec<String>, // arch
80    /// (Optional) Translations support to download
81    pub languages: Vec<String>, // lang
82    /// (Optional) Download targets to acquire from this source
83    pub targets: Vec<String>, // target
84    /// (Optional) Controls if APT should try PDiffs instead of downloading indexes entirely; if not set defaults to configuration option `Acquire::PDiffs`
85    pub pdiffs: Option<bool>, // pdiffs
86    /// (Optional) Controls if APT should try to acquire indexes via a URI constructed from a hashsum of the expected file
87    pub by_hash: Option<YesNoForce>, // by-hash
88    /// (Optional) If yes circumvents parts of `apt-secure`, don't thread lightly
89    pub allow_insecure: bool, // allow-insecure, default no
90    /// (Optional) If yes circumvents parts of `apt-secure`, don't thread lightly
91    pub allow_weak: bool, // allow-weak, default no
92    /// (Optional) If yes circumvents parts of `apt-secure`, don't thread lightly
93    pub allow_downgrade_to_insecure: bool, // allow-downgrade-to-insecure, default no
94    /// (Optional) If set forces whether APT considers source as trusted or no (default not present is a third state)
95    pub trusted: Option<bool>, // trusted
96    /// (Optional) Contains either absolute path to GPG keyring or embedded GPG public key block, if not set APT uses all trusted keys;
97    /// I can't find example of using with fingerprints
98    pub signature: Option<Signature>, // signed-by
99}
100
101impl Default for LegacyRepository {
102    fn default() -> Self {
103        Self {
104            enabled: true,
105            typ: RepositoryType::Binary,
106            uri: "http://nowhere.com".parse().unwrap(),
107            suite: "none".to_string(),
108            components: vec![],
109            architectures: vec![],
110            languages: vec![],
111            targets: vec![],
112            pdiffs: None,
113            by_hash: None,
114            allow_insecure: false,
115            allow_weak: false,
116            allow_downgrade_to_insecure: false,
117            trusted: None,
118            signature: None,
119        }
120    }
121}
122
123impl LegacyRepository {
124    /// In the ideal world we'd manage to use deserialization from a new format handling, but I'm not there yet to lift this for this format
125    fn assign_option_field(&mut self, key: &str, value: &str) -> Result<(), RepositoryError> {
126        match key {
127            "arch" => self.architectures = value.split(',').map(|s| s.to_string()).collect(),
128            "lang" => self.languages = value.split(',').map(|s| s.to_string()).collect(),
129            "target" => self.targets = value.split(',').map(|s| s.to_string()).collect(),
130            "pdiffs" => self.pdiffs = Some(super::deserialize_yesno(value)?),
131            "by-hash" => self.by_hash = Some(YesNoForce::from_str(value)?),
132            "allow-insecure" => self.allow_insecure = super::deserialize_yesno(value)?, // , default no
133            "allow-weak" => self.allow_weak = super::deserialize_yesno(value)?, // , default no
134            "allow-downgrade-to-insecure" => {
135                self.allow_downgrade_to_insecure = super::deserialize_yesno(value)?
136            } // , default no
137            "trusted" => self.trusted = Some(super::deserialize_yesno(value)?), // default not present is a third state
138            "signed-by" => self.signature = Some(Signature::KeyPath(PathBuf::from(value))),
139            any => return Err(RepositoryError::UnrecognizedFieldName(any.to_string())),
140        };
141        Ok(())
142    }
143}
144
145/// Container for multiple `LegacyRepository` specifications as single `.list` file may contain as per specification
146#[derive(Debug, Clone, PartialEq)]
147pub struct LegacyRepositories(Vec<LegacyRepository>);
148
149impl LegacyRepositories {
150    /// Creates empty container of repositories
151    pub fn empty() -> Self {
152        Self(Vec::new())
153    }
154
155    /// Creates repositories from container consisting `Repository` instances
156    pub fn new<Container>(container: Container) -> Self
157    where
158        Container: Into<Vec<LegacyRepository>>,
159    {
160        Self(container.into())
161    }
162
163    /// Provides iterator over individual repositories in the whole file
164    pub fn repositories(&self) -> impl Iterator<Item = &LegacyRepository> {
165        // TODO: that's by ref, not compatible with lossless
166        self.0.iter()
167    }
168
169    /// Push a new repository
170    pub fn push(&mut self, repo: LegacyRepository) {
171        self.0.push(repo);
172    }
173
174    /// Retain repositories matching a predicate
175    pub fn retain<F>(&mut self, f: F)
176    where
177        F: FnMut(&LegacyRepository) -> bool,
178    {
179        self.0.retain(f);
180    }
181
182    /// Get mutable iterator over repositories
183    pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, LegacyRepository> {
184        self.0.iter_mut()
185    }
186
187    /// Extend with an iterator of repositories
188    pub fn extend<I>(&mut self, iter: I)
189    where
190        I: IntoIterator<Item = LegacyRepository>,
191    {
192        self.0.extend(iter);
193    }
194
195    /// Check if empty
196    pub fn is_empty(&self) -> bool {
197        self.0.is_empty()
198    }
199}
200
201static RE: LazyLock<Regex> = LazyLock::new(|| {
202    Regex::new(
203        r"(?xm)^
204        (?P<type>deb|deb-src)\s+                   # Catch repository type
205        (\[(?P<options>[^]]*)]\s+)?                  # Catch options
206        (?P<uri>\S+)\s+                            # Catch repository URI
207        (?P<suite>\S+)\s+                          # Catch suite/distribution
208        (?P<components>(?:(?P<component>\w+)\s?)+) # Catch components (multiple)
209        $",
210    )
211    .expect("Tested correct regular expression shall not fail!")
212});
213
214/// It only make sense to convert multiple lines at once as typical `.list` file has one uncommented
215/// and one commented (`deb-src`) entry
216impl FromStr for LegacyRepositories {
217    type Err = RepositoryError;
218
219    fn from_str(text: &str) -> Result<Self, Self::Err> {
220        let elements = RE
221            .captures_iter(text)
222            .map(|caps| {
223                let mut repository = LegacyRepository::default();
224                repository.typ = RepositoryType::from_str(&caps["type"])?;
225                let options = caps.name("options").map(|o| o.as_str()).unwrap_or("");
226                options
227                    .trim_matches(|c| c == '[' || c == ']')
228                    .split_whitespace()
229                    .map(|o| {
230                        o.splitn(2, '=')
231                            .collect_tuple::<(&str, &str)>()
232                            .ok_or(RepositoryError::InvalidFormat)
233                    })
234                    .collect::<Result<Vec<_>, _>>()?
235                    .into_iter()
236                    .try_for_each(|(k, v)| repository.assign_option_field(k, v))?;
237                repository.uri = Url::from_str(&caps["uri"])?;
238                repository.suite = caps["suite"].to_owned();
239                repository
240                    .components
241                    .extend(caps["components"].split_whitespace().map(|c| c.to_owned()));
242                <Result<LegacyRepository, Self::Err>>::Ok(repository)
243            })
244            .collect::<Result<Vec<_>, _>>()?;
245        Ok(Self(elements))
246    }
247}
248
249impl Deref for LegacyRepositories {
250    type Target = Vec<LegacyRepository>;
251
252    fn deref(&self) -> &Self::Target {
253        &self.0
254    }
255}
256
257impl From<&LegacyRepository> for super::Repository {
258    fn from(original: &LegacyRepository) -> Self {
259        Self {
260            enabled: Some(original.enabled), // TODO: more valid one would be if true -> None else `Some(false)`...
261            types: HashSet::from([original.typ.clone()]),
262            uris: vec![original.uri.clone()],
263            suites: vec![original.suite.clone()],
264            components: original.components.clone().into(),
265            architectures: (!original.architectures.is_empty())
266                .then_some(original.architectures.clone()),
267            languages: (!original.languages.is_empty()).then_some(original.languages.clone()),
268            targets: (!original.targets.is_empty()).then_some(original.targets.clone()),
269            pdiffs: original.pdiffs,
270            by_hash: original.by_hash,
271            allow_insecure: original.allow_insecure.then_some(true),
272            allow_weak: original.allow_weak.then_some(true),
273            allow_downgrade_to_insecure: original.allow_downgrade_to_insecure.then_some(true),
274            trusted: original.trusted,
275            signature: original.signature.clone(),
276            x_repolib_name: None,
277            description: None,
278        }
279    }
280}
281
282impl From<LegacyRepository> for super::Repository {
283    fn from(original: LegacyRepository) -> Self {
284        Self {
285            enabled: Some(original.enabled), // TODO: more valid one would be if true -> None else `Some(false)`...
286            types: HashSet::from([original.typ]),
287            uris: vec![original.uri],
288            suites: vec![original.suite],
289            components: original.components.into(),
290            architectures: (!original.architectures.is_empty()).then_some(original.architectures),
291            languages: (!original.languages.is_empty()).then_some(original.languages),
292            targets: (!original.targets.is_empty()).then_some(original.targets),
293            pdiffs: original.pdiffs,
294            by_hash: original.by_hash,
295            allow_insecure: original.allow_insecure.then_some(true),
296            allow_weak: original.allow_weak.then_some(true),
297            allow_downgrade_to_insecure: original.allow_downgrade_to_insecure.then_some(true),
298            trusted: original.trusted,
299            signature: original.signature,
300            x_repolib_name: None,
301            description: None,
302        }
303    }
304}
305
306impl From<&LegacyRepositories> for super::Repositories {
307    fn from(original: &LegacyRepositories) -> Self {
308        Self(original.iter().map(|v| v.into()).collect())
309    }
310}
311
312impl From<LegacyRepositories> for super::Repositories {
313    fn from(original: LegacyRepositories) -> Self {
314        Self(original.0.into_iter().map(|v| v.into()).collect())
315    }
316}
317
318impl From<&super::Repository> for LegacyRepositories {
319    /// Convert a DEB822 Repository to legacy format lines.
320    /// Since a Repository can have multiple types/uris/suites, this may produce multiple lines.
321    fn from(repo: &super::Repository) -> Self {
322        let mut repos = Vec::new();
323
324        for typ in &repo.types {
325            for uri in &repo.uris {
326                for suite in &repo.suites {
327                    repos.push(LegacyRepository {
328                        enabled: repo.enabled.unwrap_or(true),
329                        typ: typ.clone(),
330                        uri: uri.clone(),
331                        suite: suite.clone(),
332                        components: repo.components.clone().unwrap_or_default(),
333                        architectures: repo.architectures.clone().unwrap_or_default(),
334                        languages: repo.languages.clone().unwrap_or_default(),
335                        targets: repo.targets.clone().unwrap_or_default(),
336                        pdiffs: repo.pdiffs,
337                        by_hash: repo.by_hash,
338                        allow_insecure: repo.allow_insecure.unwrap_or(false),
339                        allow_weak: repo.allow_weak.unwrap_or(false),
340                        allow_downgrade_to_insecure: repo
341                            .allow_downgrade_to_insecure
342                            .unwrap_or(false),
343                        trusted: repo.trusted,
344                        signature: repo.signature.clone(),
345                    });
346                }
347            }
348        }
349
350        LegacyRepositories(repos)
351    }
352}
353
354fn option_output<O: AsRef<str> + Display>(name: &str, option: &[O]) -> Cow<'static, str> {
355    if option.is_empty() {
356        Cow::Borrowed("")
357    } else {
358        Cow::Owned(format!("{name}={}", option.iter().join(",")))
359    }
360}
361
362impl Display for LegacyRepository {
363    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
364        write!(f, "{}", self.typ)?;
365        // TODO: all options if any
366
367        let options = vec![
368            option_output("arch", &self.architectures),
369            option_output("lang", &self.languages),
370            option_output("target", &self.targets),
371            self.pdiffs
372                .map(|p| Cow::Owned(format!("pdiff={}", if p { "yes" } else { "no" })))
373                .unwrap_or(Cow::Borrowed("")),
374            self.by_hash
375                .map(|p| Cow::Owned(format!("by-hash={p}")))
376                .unwrap_or(Cow::Borrowed("")),
377            if self.allow_insecure {
378                Cow::Owned("allow-insecure=yes".to_string())
379            } else {
380                Cow::Borrowed("")
381            },
382            if self.allow_weak {
383                Cow::Owned("allow-weak=yes".to_string())
384            } else {
385                Cow::Borrowed("")
386            },
387            if self.allow_downgrade_to_insecure {
388                Cow::Owned("allow-downgrade-to-insecure=yes".to_string())
389            } else {
390                Cow::Borrowed("")
391            },
392            self.trusted
393                .map(|t| Cow::Owned(format!("trusted={}", if t { "yes" } else { "no" })))
394                .unwrap_or(Cow::Borrowed("")),
395            self.signature
396                .as_ref()
397                .map(|s| {
398                    if let Signature::KeyPath(ref p) = s {
399                        Cow::Owned(format!("signed-by={}", p.display()))
400                    } else {
401                        panic!("Short format not supported!") // TODO: design bug of LegacyRepository!
402                    }
403                })
404                .unwrap_or(Cow::Borrowed("")),
405        ];
406        let options = options.iter().filter(|s| !s.is_empty()).join(" ");
407        options.is_empty().not().then(|| write!(f, " [{options}]"));
408        write!(f, " {}", self.uri)?;
409        write!(f, " {}", self.suite)?;
410        write!(f, " {}", self.components.join(" "))
411    }
412}
413
414impl Display for LegacyRepositories {
415    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
416        for (i, repo) in self.0.iter().enumerate() {
417            if i > 0 {
418                writeln!(f)?;
419            }
420            write!(f, "{}", repo)?;
421        }
422        Ok(())
423    }
424}
425
426#[cfg(test)]
427mod tests {
428    use crate::Repository;
429
430    use super::*;
431    use indoc::indoc;
432
433    const LONG_SAMPLE: &str = indoc!("
434        deb [arch=arm64 signed-by=/usr/share/keyrings/rcn-ee-archive-keyring.gpg] http://debian.beagleboard.org/arm64/ jammy main
435    ");
436    const SHORT_SAMPLE: &str = indoc!(
437        "
438        deb http://archive.ubuntu.com/ubuntu jammy main restricted
439        deb-src http://archive.ubuntu.com/ubuntu jammy main restricted
440    "
441    );
442    const COMMENTED_SAMPLE: &str = indoc!(
443        "
444        deb http://archive.ubuntu.com/ubuntu jammy main restricted
445        # deb-src http://archive.ubuntu.com/ubuntu jammy main restricted
446    "
447    );
448
449    fn golden_sample() -> Repository {
450        // TODO: qualifies for lazy_static
451        Repository {
452            enabled: Some(true), // TODO: looks odd, as only `Enabled: no` in meaningful
453            types: HashSet::from([RepositoryType::Binary]),
454            architectures: Some(vec!["arm64".to_owned()]),
455            uris: vec![Url::from_str("http://debian.beagleboard.org/arm64/").unwrap()],
456            suites: vec!["jammy".to_owned()],
457            components: Some(vec!["main".to_owned()]),
458            signature: Some(Signature::KeyPath(PathBuf::from(
459                "/usr/share/keyrings/rcn-ee-archive-keyring.gpg",
460            ))),
461            x_repolib_name: None,
462            languages: None,
463            targets: None,
464            pdiffs: None,
465            ..Default::default()
466        }
467    }
468
469    #[test]
470    fn test_legacy_repositories_from_str() {
471        let repositories = LegacyRepositories::from_str(LONG_SAMPLE)
472            .expect("Shall not fail for correct list entry!");
473
474        assert_eq!(repositories.len(), 1);
475        let repository = repositories.iter().nth(0).unwrap();
476
477        assert_eq!(repository.enabled, true);
478        assert_eq!(repository.typ, RepositoryType::Binary);
479        assert_eq!(repository.architectures, vec!["arm64".to_owned()]);
480        assert_eq!(
481            repository.signature,
482            Some(Signature::KeyPath(PathBuf::from(
483                "/usr/share/keyrings/rcn-ee-archive-keyring.gpg"
484            )))
485        );
486        assert_eq!(repository.typ, RepositoryType::Binary);
487        assert_eq!(
488            repository.uri,
489            "http://debian.beagleboard.org/arm64/"
490                .parse::<Url>()
491                .unwrap()
492        );
493        assert_eq!(repository.suite, "jammy".to_owned());
494        assert_eq!(repository.components, vec!["main".to_owned()]);
495    }
496
497    #[test]
498    fn test_short_legacy_repositories_from_str() {
499        let repositories = LegacyRepositories::from_str(SHORT_SAMPLE)
500            .expect("Shall not fail for correct list entry!");
501
502        assert_eq!(repositories.len(), 2);
503        let bin_repository = repositories.iter().nth(0).unwrap();
504        let src_repository = repositories.iter().nth(1).unwrap();
505
506        assert_eq!(bin_repository.typ, RepositoryType::Binary);
507        assert_eq!(src_repository.typ, RepositoryType::Source);
508        assert_eq!(bin_repository.architectures.len(), 0);
509        assert_eq!(src_repository.architectures.len(), 0);
510        assert_eq!(bin_repository.components.len(), 2);
511        assert_eq!(src_repository.components.len(), 2);
512    }
513
514    #[test]
515    #[ignore = "commented lines support not yet implemented"]
516    fn test_commented_legacy_repositories_from_str() {
517        let repositories = LegacyRepositories::from_str(COMMENTED_SAMPLE)
518            .expect("Shall not fail for correct list entry!");
519
520        assert_eq!(repositories.len(), 2);
521        let bin_repository = repositories.iter().nth(0).unwrap();
522        let src_repository = repositories.iter().nth(1).unwrap();
523
524        assert_eq!(bin_repository.enabled, true);
525        assert_eq!(bin_repository.enabled, false);
526        assert_eq!(bin_repository.typ, RepositoryType::Binary);
527        assert_eq!(src_repository.typ, RepositoryType::Source);
528        assert_eq!(bin_repository.architectures.len(), 0);
529        assert_eq!(src_repository.architectures.len(), 0);
530        assert_eq!(bin_repository.components.len(), 2);
531        assert_eq!(src_repository.components.len(), 2);
532    }
533
534    #[test]
535    fn test_conversion_from_legacy_to_deb822() {
536        let repositories = LegacyRepositories::from_str(LONG_SAMPLE)
537            .expect("Shall not fail for correct list entry!");
538
539        assert_eq!(repositories.len(), 1);
540        let legacy_repository = repositories.iter().nth(0).unwrap();
541
542        let deb822_repository = Repository::from(legacy_repository);
543        let golden_sample = golden_sample();
544
545        assert_eq!(golden_sample, deb822_repository);
546    }
547
548    #[test]
549    fn test_moving_conversion_from_legacy_to_deb822() {
550        let mut repositories = LegacyRepositories::from_str(LONG_SAMPLE)
551            .expect("Shall not fail for correct list entry!");
552
553        assert_eq!(repositories.len(), 1);
554        let legacy_repository = repositories.0.pop().unwrap(); // TODO: To make it work for user we'd need `DerefMut` but I'm reluctant
555
556        let deb822_repository = Repository::from(legacy_repository);
557        let golden_sample = golden_sample();
558
559        assert_eq!(golden_sample, deb822_repository);
560    }
561
562    #[test]
563    fn test_display_of_simple_legacy_repository() {
564        let sample = LegacyRepository {
565            enabled: true,
566            typ: RepositoryType::Binary,
567            uri: "http://debian.beagleboard.org/arm64/".parse().unwrap(),
568            suite: "jammy".to_string(),
569            components: vec!["main".to_string()],
570            architectures: vec![],
571            languages: vec![],
572            targets: vec![],
573            pdiffs: None,
574            by_hash: None,
575            allow_insecure: false,
576            allow_weak: false,
577            allow_downgrade_to_insecure: false,
578            trusted: None,
579            signature: None,
580        };
581        let list_text = sample.to_string();
582
583        assert_eq!(
584            list_text,
585            "deb http://debian.beagleboard.org/arm64/ jammy main"
586        )
587    }
588
589    #[test]
590    fn test_display_of_legacy_repository_with_options() {
591        let sample = LegacyRepository {
592            enabled: true,
593            typ: RepositoryType::Binary,
594            uri: "http://debian.beagleboard.org/arm64/".parse().unwrap(),
595            suite: "jammy".to_string(),
596            components: vec!["main".to_string()],
597            architectures: vec!["amd64".to_string()],
598            languages: vec![],
599            targets: vec![],
600            pdiffs: None,
601            by_hash: None,
602            allow_insecure: false,
603            allow_weak: false,
604            allow_downgrade_to_insecure: false,
605            trusted: None,
606            signature: Some(Signature::KeyPath(PathBuf::from(
607                "/usr/share/keyrings/rcn-ee-archive-keyring.gpg",
608            ))), // TODO: `.list` supports only key files, no way to fit PGP block
609        };
610        let list_text = sample.to_string();
611
612        assert_eq!(
613            list_text,
614            "deb [arch=amd64 signed-by=/usr/share/keyrings/rcn-ee-archive-keyring.gpg] http://debian.beagleboard.org/arm64/ jammy main"
615        )
616    }
617
618    #[test]
619    fn test_conversion_from_deb822_to_legacy() {
620        use std::collections::HashSet;
621
622        let repo = Repository {
623            enabled: Some(true),
624            types: HashSet::from([RepositoryType::Binary, RepositoryType::Source]),
625            uris: vec!["http://archive.ubuntu.com/ubuntu".parse().unwrap()],
626            suites: vec!["jammy".to_string()],
627            components: Some(vec!["main".to_string(), "universe".to_string()]),
628            architectures: Some(vec!["amd64".to_string()]),
629            ..Default::default()
630        };
631
632        let legacy = LegacyRepositories::from(&repo);
633        assert_eq!(legacy.len(), 2); // One for deb, one for deb-src
634
635        let legacy_str = legacy.to_string();
636        assert!(legacy_str.contains("deb [arch=amd64]"));
637        assert!(legacy_str.contains("deb-src [arch=amd64]"));
638        assert!(legacy_str.contains("http://archive.ubuntu.com/ubuntu"));
639        assert!(legacy_str.contains("jammy main universe"));
640    }
641
642    #[test]
643    fn test_legacy_repositories_display() {
644        let repos = LegacyRepositories(vec![
645            LegacyRepository {
646                enabled: true,
647                typ: RepositoryType::Binary,
648                uri: "http://example.com/ubuntu".parse().unwrap(),
649                suite: "jammy".to_string(),
650                components: vec!["main".to_string()],
651                ..Default::default()
652            },
653            LegacyRepository {
654                enabled: true,
655                typ: RepositoryType::Source,
656                uri: "http://example.com/ubuntu".parse().unwrap(),
657                suite: "jammy".to_string(),
658                components: vec!["main".to_string()],
659                ..Default::default()
660            },
661        ]);
662
663        let display = repos.to_string();
664        assert_eq!(
665            display,
666            "deb http://example.com/ubuntu jammy main\ndeb-src http://example.com/ubuntu jammy main"
667        );
668    }
669
670    #[test]
671    fn test_allow_downgrade_to_insecure_parsing() {
672        let input = "deb [allow-downgrade-to-insecure=yes] http://example.com/ubuntu jammy main\n";
673        let repos = LegacyRepositories::from_str(input).unwrap();
674        assert_eq!(repos.len(), 1);
675        let repo = repos.iter().nth(0).unwrap();
676        assert!(repo.allow_downgrade_to_insecure);
677        assert!(!repo.allow_weak);
678    }
679
680    #[test]
681    fn test_allow_downgrade_to_insecure_display() {
682        let repo = LegacyRepository {
683            enabled: true,
684            typ: RepositoryType::Binary,
685            uri: "http://example.com/ubuntu".parse().unwrap(),
686            suite: "jammy".to_string(),
687            components: vec!["main".to_string()],
688            allow_downgrade_to_insecure: true,
689            ..Default::default()
690        };
691        let text = repo.to_string();
692        assert_eq!(
693            text,
694            "deb [allow-downgrade-to-insecure=yes] http://example.com/ubuntu jammy main"
695        );
696    }
697
698    #[test]
699    fn test_malformed_option_without_equals() {
700        let input = "deb [badoption] http://example.com/ubuntu jammy main\n";
701        let result = LegacyRepositories::from_str(input);
702        assert!(result.is_err());
703    }
704}