1#![deny(missing_docs)]
2use deb822_fast::{FromDeb822, FromDeb822Paragraph, ToDeb822, ToDeb822Paragraph};
47use error::{LoadError, RepositoryError};
48use signature::Signature;
49use std::path::Path;
50use std::result::Result;
51use std::{collections::HashSet, ops::Deref, str::FromStr};
52use url::Url;
53
54pub mod distribution;
56pub mod error;
57#[cfg(feature = "key-management")]
58pub mod key_management;
60#[cfg(feature = "key-management")]
61pub mod keyserver;
62pub mod ppa;
63pub mod signature;
64pub mod sources_manager;
66
67#[derive(PartialEq, Eq, Hash, Debug, Clone)]
70pub enum RepositoryType {
71 Binary,
73 Source,
75}
76
77impl FromStr for RepositoryType {
78 type Err = RepositoryError;
79
80 fn from_str(s: &str) -> Result<Self, Self::Err> {
81 match s {
82 "deb" => Ok(RepositoryType::Binary),
83 "deb-src" => Ok(RepositoryType::Source),
84 _ => Err(RepositoryError::InvalidType),
85 }
86 }
87}
88
89impl From<&RepositoryType> for String {
90 fn from(value: &RepositoryType) -> Self {
91 match value {
92 RepositoryType::Binary => "deb".to_owned(),
93 RepositoryType::Source => "deb-src".to_owned(),
94 }
95 }
96}
97
98impl std::fmt::Display for RepositoryType {
99 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100 let s = match self {
101 RepositoryType::Binary => "deb",
102 RepositoryType::Source => "deb-src",
103 };
104 write!(f, "{}", s)
105 }
106}
107
108#[derive(Debug, Clone, PartialEq)]
109pub enum YesNoForce {
111 Yes,
113 No,
115 Force,
117}
118
119impl FromStr for YesNoForce {
120 type Err = RepositoryError;
121
122 fn from_str(s: &str) -> Result<Self, Self::Err> {
123 match s {
124 "yes" => Ok(Self::Yes),
125 "no" => Ok(Self::No),
126 "force" => Ok(Self::Force),
127 _ => Err(RepositoryError::InvalidType),
128 }
129 }
130}
131
132impl From<&YesNoForce> for String {
133 fn from(value: &YesNoForce) -> Self {
134 match value {
135 YesNoForce::Yes => "yes".to_owned(),
136 YesNoForce::No => "no".to_owned(),
137 YesNoForce::Force => "force".to_owned(),
138 }
139 }
140}
141
142impl std::fmt::Display for YesNoForce {
143 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144 let s = match self {
145 YesNoForce::Yes => "yes",
146 YesNoForce::No => "no",
147 YesNoForce::Force => "force",
148 };
149 write!(f, "{}", s)
150 }
151}
152
153fn deserialize_types(text: &str) -> Result<HashSet<RepositoryType>, RepositoryError> {
154 text.split_whitespace()
155 .map(RepositoryType::from_str)
156 .collect::<Result<HashSet<RepositoryType>, RepositoryError>>()
157}
158
159fn serialize_types(files: &HashSet<RepositoryType>) -> String {
160 use std::fmt::Write;
161 let mut result = String::new();
162 for (i, rt) in files.iter().enumerate() {
163 if i > 0 {
164 result.push('\n');
165 }
166 write!(&mut result, "{}", rt).unwrap();
167 }
168 result
169}
170
171fn deserialize_uris(text: &str) -> Result<Vec<Url>, String> {
172 text.split_whitespace()
174 .map(Url::from_str)
175 .collect::<Result<Vec<Url>, _>>()
176 .map_err(|e| e.to_string()) }
178
179fn serialize_uris(uris: &[Url]) -> String {
180 let mut result = String::new();
181 for (i, uri) in uris.iter().enumerate() {
182 if i > 0 {
183 result.push(' ');
184 }
185 result.push_str(uri.as_str());
186 }
187 result
188}
189
190fn deserialize_string_chain(text: &str) -> Result<Vec<String>, String> {
191 Ok(text.split_whitespace().map(|x| x.to_string()).collect())
193}
194
195fn deserialize_yesno(text: &str) -> Result<bool, String> {
196 match text {
198 "yes" => Ok(true),
199 "no" => Ok(false),
200 _ => Err("Invalid value for yes/no field".to_owned()),
201 }
202}
203
204fn serializer_yesno(value: &bool) -> String {
205 if *value {
206 "yes".to_string()
207 } else {
208 "no".to_string()
209 }
210}
211
212fn serialize_string_chain(chain: &[String]) -> String {
213 chain.join(" ")
214}
215
216#[derive(FromDeb822, ToDeb822, Clone, PartialEq, Debug, Default)]
252pub struct Repository {
253 #[deb822(field = "Enabled", deserialize_with = deserialize_yesno, serialize_with = serializer_yesno)]
255 pub enabled: Option<bool>,
257
258 #[deb822(field = "Types", deserialize_with = deserialize_types, serialize_with = serialize_types)]
260 pub types: HashSet<RepositoryType>, #[deb822(field = "URIs", deserialize_with = deserialize_uris, serialize_with = serialize_uris)]
263 pub uris: Vec<Url>, #[deb822(field = "Suites", deserialize_with = deserialize_string_chain, serialize_with = serialize_string_chain)]
266 pub suites: Vec<String>,
267 #[deb822(field = "Components", deserialize_with = deserialize_string_chain, serialize_with = serialize_string_chain)]
270 pub components: Option<Vec<String>>,
271
272 #[deb822(field = "Architectures", deserialize_with = deserialize_string_chain, serialize_with = serialize_string_chain)]
274 pub architectures: Vec<String>,
275 #[deb822(field = "Languages", deserialize_with = deserialize_string_chain, serialize_with = serialize_string_chain)]
277 pub languages: Option<Vec<String>>, #[deb822(field = "Targets", deserialize_with = deserialize_string_chain, serialize_with = serialize_string_chain)]
280 pub targets: Option<Vec<String>>,
281 #[deb822(field = "PDiffs", deserialize_with = deserialize_yesno)]
283 pub pdiffs: Option<bool>,
284 #[deb822(field = "By-Hash")]
286 pub by_hash: Option<YesNoForce>,
287 #[deb822(field = "Allow-Insecure")]
289 pub allow_insecure: Option<bool>, #[deb822(field = "Allow-Weak")]
292 pub allow_weak: Option<bool>, #[deb822(field = "Allow-Downgrade-To-Insecure")]
295 pub allow_downgrade_to_insecure: Option<bool>, #[deb822(field = "Trusted")]
298 pub trusted: Option<bool>,
299 #[deb822(field = "Signed-By")]
302 pub signature: Option<Signature>,
303
304 #[deb822(field = "X-Repolib-Name")]
306 pub x_repolib_name: Option<String>, #[deb822(field = "Description")]
310 pub description: Option<String>, }
312
313impl Repository {
314 pub fn suites(&self) -> &[String] {
316 self.suites.as_slice()
317 }
318
319 pub fn types(&self) -> &HashSet<RepositoryType> {
321 &self.types
322 }
323
324 pub fn uris(&self) -> &[Url] {
326 &self.uris
327 }
328
329 pub fn components(&self) -> Option<&[String]> {
331 self.components.as_deref()
332 }
333
334 pub fn architectures(&self) -> &[String] {
336 &self.architectures
337 }
338
339 pub fn to_legacy_format(&self) -> String {
341 let mut lines = Vec::new();
342
343 for repo_type in &self.types {
345 let type_str = match repo_type {
346 RepositoryType::Binary => "deb",
347 RepositoryType::Source => "deb-src",
348 };
349
350 for uri in &self.uris {
351 for suite in &self.suites {
352 let mut line = format!("{} {} {}", type_str, uri, suite);
353
354 if let Some(components) = &self.components {
356 for component in components {
357 line.push(' ');
358 line.push_str(component);
359 }
360 }
361
362 lines.push(line);
363 }
364 }
365 }
366
367 lines.join("\n") + "\n"
368 }
369
370 pub fn parse_legacy_line(line: &str) -> Result<Repository, String> {
372 let parts: Vec<&str> = line.split_whitespace().collect();
373
374 if parts.len() < 4 {
375 return Err("Invalid repository line format".to_string());
376 }
377
378 let repo_type = match parts[0] {
379 "deb" => RepositoryType::Binary,
380 "deb-src" => RepositoryType::Source,
381 _ => return Err("Line must start with 'deb' or 'deb-src'".to_string()),
382 };
383
384 let uri = Url::parse(parts[1]).map_err(|_| "Invalid URI".to_string())?;
385
386 let suite = parts[2].to_string();
387 let components: Vec<String> = parts[3..].iter().map(|s| s.to_string()).collect();
388
389 Ok(Repository {
390 enabled: Some(true),
391 types: HashSet::from([repo_type]),
392 uris: vec![uri],
393 suites: vec![suite],
394 components: Some(components),
395 architectures: vec![],
396 signature: None,
397 ..Default::default()
398 })
399 }
400}
401
402#[derive(Debug)]
404pub struct Repositories(Vec<Repository>);
405
406impl Default for Repositories {
407 fn default() -> Self {
412 let (repos, errors) = Self::load_from_directory(std::path::Path::new("/etc/apt"));
413
414 #[cfg(feature = "tracing")]
416 for error in errors {
417 tracing::warn!("Failed to load APT source: {}", error);
418 }
419
420 #[cfg(not(feature = "tracing"))]
422 let _ = errors;
423
424 repos
425 }
426}
427
428impl Repositories {
429 pub fn empty() -> Self {
431 Repositories(Vec::new())
432 }
433
434 pub fn new<Container>(container: Container) -> Self
436 where
437 Container: Into<Vec<Repository>>,
438 {
439 Repositories(container.into())
440 }
441
442 pub fn parse_legacy_format(content: &str) -> Result<Self, String> {
445 let mut repositories = Vec::new();
446
447 for line in content.lines() {
448 let trimmed = line.trim();
449
450 if trimmed.is_empty() || trimmed.starts_with('#') {
452 continue;
453 }
454
455 let repo = Repository::parse_legacy_line(trimmed)?;
457 repositories.push(repo);
458 }
459
460 Ok(Repositories(repositories))
461 }
462
463 pub fn load_from_directory(path: &Path) -> (Self, Vec<LoadError>) {
474 use std::fs;
475
476 let mut all_repositories = Repositories::empty();
477 let mut errors = Vec::new();
478
479 let main_sources = path.join("sources.list");
481 if main_sources.exists() {
482 match fs::read_to_string(&main_sources) {
483 Ok(content) => match Self::parse_legacy_format(&content) {
484 Ok(repos) => all_repositories.extend(repos.0),
485 Err(e) => errors.push(LoadError::Parse {
486 path: main_sources,
487 error: e,
488 }),
489 },
490 Err(e) => errors.push(LoadError::Io {
491 path: main_sources,
492 error: e,
493 }),
494 }
495 }
496
497 let sources_d = path.join("sources.list.d");
499 if !sources_d.is_dir() {
500 return (all_repositories, errors);
501 }
502
503 let entries = match fs::read_dir(&sources_d) {
504 Ok(entries) => entries,
505 Err(e) => {
506 errors.push(LoadError::DirectoryRead {
507 path: sources_d,
508 error: e,
509 });
510 return (all_repositories, errors);
511 }
512 };
513
514 let mut entry_paths: Vec<_> = entries
516 .filter_map(|entry| entry.ok())
517 .map(|entry| entry.path())
518 .filter(|p| p.is_file())
519 .filter(|p| {
520 p.file_name()
521 .and_then(|n| n.to_str())
522 .map(|n| n.ends_with(".list") || n.ends_with(".sources"))
523 .unwrap_or(false)
524 })
525 .collect();
526 entry_paths.sort();
527
528 for file_path in entry_paths {
529 let file_name = file_path.file_name().and_then(|n| n.to_str()).unwrap_or("");
530
531 let content = match fs::read_to_string(&file_path) {
532 Ok(content) => content,
533 Err(e) => {
534 errors.push(LoadError::Io {
535 path: file_path,
536 error: e,
537 });
538 continue;
539 }
540 };
541
542 let parse_result = if file_name.ends_with(".list") {
543 Self::parse_legacy_format(&content)
544 .map(|repos| repos.0)
545 .map_err(|e| LoadError::Parse {
546 path: file_path,
547 error: e,
548 })
549 } else if file_name.ends_with(".sources") {
550 content
551 .parse::<Repositories>()
552 .map(|repos| repos.0)
553 .map_err(|e| LoadError::Parse {
554 path: file_path,
555 error: e,
556 })
557 } else {
558 continue;
559 };
560
561 match parse_result {
562 Ok(repos) => all_repositories.extend(repos),
563 Err(e) => errors.push(e),
564 }
565 }
566
567 (all_repositories, errors)
568 }
569
570 pub fn load_from_directory_strict(path: &Path) -> Result<Self, LoadError> {
575 let (repos, errors) = Self::load_from_directory(path);
576 if let Some(error) = errors.into_iter().next() {
577 Err(error)
578 } else {
579 Ok(repos)
580 }
581 }
582
583 pub fn push(&mut self, repo: Repository) {
585 self.0.push(repo);
586 }
587
588 pub fn retain<F>(&mut self, f: F)
590 where
591 F: FnMut(&Repository) -> bool,
592 {
593 self.0.retain(f);
594 }
595
596 pub fn iter(&self) -> std::slice::Iter<Repository> {
598 self.0.iter()
599 }
600
601 pub fn iter_mut(&mut self) -> std::slice::IterMut<Repository> {
603 self.0.iter_mut()
604 }
605
606 pub fn extend<I>(&mut self, iter: I)
608 where
609 I: IntoIterator<Item = Repository>,
610 {
611 self.0.extend(iter);
612 }
613
614 pub fn is_empty(&self) -> bool {
616 self.0.is_empty()
617 }
618}
619
620impl std::str::FromStr for Repositories {
621 type Err = String;
622
623 fn from_str(s: &str) -> Result<Self, Self::Err> {
624 let deb822: deb822_fast::Deb822 =
625 s.parse().map_err(|e: deb822_fast::Error| e.to_string())?;
626
627 let repos = deb822
628 .iter()
629 .map(Repository::from_paragraph)
630 .collect::<Result<Vec<Repository>, Self::Err>>()?;
631 Ok(Repositories(repos))
632 }
633}
634
635impl std::fmt::Display for Repositories {
636 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
637 let result = self
638 .0
639 .iter()
640 .map(|r| {
641 let p: deb822_fast::Paragraph = r.to_paragraph();
642 p.to_string()
643 })
644 .collect::<Vec<_>>()
645 .join("\n");
646 write!(f, "{}", result)
647 }
648}
649
650impl Deref for Repositories {
651 type Target = Vec<Repository>;
652
653 fn deref(&self) -> &Self::Target {
654 &self.0
655 }
656}
657
658#[cfg(test)]
659mod tests {
660 use std::{collections::HashSet, str::FromStr};
661
662 use indoc::indoc;
663 use url::Url;
664
665 use crate::{signature::Signature, Repositories, Repository, RepositoryType};
666
667 #[test]
668 fn test_not_machine_readable() {
669 let s = indoc!(
670 r#"
671 deb [arch=arm64 signed-by=/usr/share/keyrings/docker.gpg] http://ports.ubuntu.com/ noble stable
672 "#
673 );
674 let ret = s.parse::<Repositories>();
675 assert!(ret.is_err());
676 assert_eq!(ret.unwrap_err(), "Unexpected token: ".to_string());
678 }
679
680 #[test]
681 fn test_parse_flat_repo() {
682 let s = indoc! {r#"
683 Types: deb
684 URIs: http://ports.ubuntu.com/
685 Suites: ./
686 Architectures: arm64
687 "#};
688
689 let repos = s
690 .parse::<Repositories>()
691 .expect("Shall be parsed flawlessly");
692 assert!(repos[0].types.contains(&super::RepositoryType::Binary));
693 }
694
695 #[test]
696 fn test_parse_w_keyblock() {
697 let s = indoc!(
698 r#"
699 Types: deb
700 URIs: http://ports.ubuntu.com/
701 Suites: noble
702 Components: stable
703 Architectures: arm64
704 Signed-By:
705 -----BEGIN PGP PUBLIC KEY BLOCK-----
706 .
707 mDMEY865UxYJKwYBBAHaRw8BAQdAd7Z0srwuhlB6JKFkcf4HU4SSS/xcRfwEQWzr
708 crf6AEq0SURlYmlhbiBTdGFibGUgUmVsZWFzZSBLZXkgKDEyL2Jvb2t3b3JtKSA8
709 ZGViaWFuLXJlbGVhc2VAbGlzdHMuZGViaWFuLm9yZz6IlgQTFggAPhYhBE1k/sEZ
710 wgKQZ9bnkfjSWFuHg9SBBQJjzrlTAhsDBQkPCZwABQsJCAcCBhUKCQgLAgQWAgMB
711 Ah4BAheAAAoJEPjSWFuHg9SBSgwBAP9qpeO5z1s5m4D4z3TcqDo1wez6DNya27QW
712 WoG/4oBsAQCEN8Z00DXagPHbwrvsY2t9BCsT+PgnSn9biobwX7bDDg==
713 =5NZE
714 -----END PGP PUBLIC KEY BLOCK-----
715 "#
716 );
717
718 let repos = s
719 .parse::<Repositories>()
720 .expect("Shall be parsed flawlessly");
721 assert!(repos[0].types.contains(&super::RepositoryType::Binary));
722 assert!(matches!(repos[0].signature, Some(Signature::KeyBlock(_))));
723 }
724
725 #[test]
726 fn test_parse_w_keypath() {
727 let s = indoc!(
728 r#"
729 Types: deb
730 URIs: http://ports.ubuntu.com/
731 Suites: noble
732 Components: stable
733 Architectures: arm64
734 Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
735 "#
736 );
737
738 let reps = s
739 .parse::<Repositories>()
740 .expect("Shall be parsed flawlessly");
741 assert!(reps[0].types.contains(&super::RepositoryType::Binary));
742 assert!(matches!(reps[0].signature, Some(Signature::KeyPath(_))));
743 }
744
745 #[test]
746 fn test_serialize() {
747 let repos = Repositories::new([Repository {
749 enabled: Some(true), types: HashSet::from([RepositoryType::Binary]),
751 architectures: vec!["arm64".to_owned()],
752 uris: vec![Url::from_str("https://deb.debian.org/debian").unwrap()],
753 suites: vec!["jammy".to_owned()],
754 components: Some(vec!["main".to_owned()]),
755 signature: None,
756 x_repolib_name: None,
757 languages: None,
758 targets: None,
759 pdiffs: None,
760 ..Default::default()
761 }]);
762 let text = repos.to_string();
763 assert_eq!(
764 text,
765 indoc! {r#"
766 Enabled: yes
767 Types: deb
768 URIs: https://deb.debian.org/debian
769 Suites: jammy
770 Components: main
771 Architectures: arm64
772 "#}
773 );
774 }
775
776 #[test]
777 fn test_yesnoforce_to_string() {
778 let yes = crate::YesNoForce::Yes;
779 assert_eq!(yes.to_string(), "yes");
780
781 let no = crate::YesNoForce::No;
782 assert_eq!(no.to_string(), "no");
783
784 let force = crate::YesNoForce::Force;
785 assert_eq!(force.to_string(), "force");
786 }
787
788 #[test]
789 fn test_parse_legacy_line() {
790 let line = "deb http://archive.ubuntu.com/ubuntu jammy main restricted";
791 let repo = Repository::parse_legacy_line(line).unwrap();
792 assert_eq!(repo.types.len(), 1);
793 assert!(repo.types.contains(&RepositoryType::Binary));
794 assert_eq!(repo.uris.len(), 1);
795 assert_eq!(repo.uris[0].to_string(), "http://archive.ubuntu.com/ubuntu");
796 assert_eq!(repo.suites, vec!["jammy".to_string()]);
797 assert_eq!(
798 repo.components,
799 Some(vec!["main".to_string(), "restricted".to_string()])
800 );
801 }
802
803 #[test]
804 fn test_parse_legacy_line_deb_src() {
805 let line = "deb-src http://archive.ubuntu.com/ubuntu jammy main";
806 let repo = Repository::parse_legacy_line(line).unwrap();
807 assert!(repo.types.contains(&RepositoryType::Source));
808 assert!(!repo.types.contains(&RepositoryType::Binary));
809 }
810
811 #[test]
812 fn test_parse_legacy_line_minimum_components() {
813 let line = "deb http://example.com/debian stable main";
815 let repo = Repository::parse_legacy_line(line).unwrap();
816 assert_eq!(repo.components, Some(vec!["main".to_string()]));
817 }
818
819 #[test]
820 fn test_parse_legacy_line_invalid() {
821 let line = "invalid line";
822 let result = Repository::parse_legacy_line(line);
823 assert!(result.is_err());
824 }
825
826 #[test]
827 fn test_parse_legacy_line_too_few_parts() {
828 let line = "deb http://example.com/debian";
830 let result = Repository::parse_legacy_line(line);
831 assert!(result.is_err());
832 assert_eq!(result.unwrap_err(), "Invalid repository line format");
833 }
834
835 #[test]
836 fn test_parse_legacy_line_invalid_type() {
837 let line = "invalid-type http://example.com/debian stable main";
838 let result = Repository::parse_legacy_line(line);
839 assert!(result.is_err());
840 assert_eq!(
841 result.unwrap_err(),
842 "Line must start with 'deb' or 'deb-src'"
843 );
844 }
845
846 #[test]
847 fn test_parse_legacy_line_invalid_uri() {
848 let line = "deb not-a-valid-uri stable main";
849 let result = Repository::parse_legacy_line(line);
850 assert!(result.is_err());
851 assert_eq!(result.unwrap_err(), "Invalid URI");
852 }
853
854 #[test]
855 fn test_to_legacy_format_single_type() {
856 let repo = Repository {
857 types: HashSet::from([RepositoryType::Binary]),
858 uris: vec![Url::parse("http://example.com/debian").unwrap()],
859 suites: vec!["stable".to_string()],
860 components: Some(vec!["main".to_string()]),
861 ..Default::default()
862 };
863
864 let legacy = repo.to_legacy_format();
865 assert_eq!(legacy, "deb http://example.com/debian stable main\n");
866 }
867
868 #[test]
869 fn test_to_legacy_format_both_types() {
870 let repo = Repository {
871 types: HashSet::from([RepositoryType::Binary, RepositoryType::Source]),
872 uris: vec![Url::parse("http://example.com/debian").unwrap()],
873 suites: vec!["stable".to_string()],
874 components: Some(vec!["main".to_string(), "contrib".to_string()]),
875 ..Default::default()
876 };
877
878 let legacy = repo.to_legacy_format();
879 assert!(legacy.contains("deb http://example.com/debian stable main contrib"));
881 assert!(legacy.contains("deb-src http://example.com/debian stable main contrib"));
882 }
883
884 #[test]
885 fn test_to_legacy_format_multiple_uris_and_suites() {
886 let repo = Repository {
887 types: HashSet::from([RepositoryType::Binary]),
888 uris: vec![
889 Url::parse("http://example1.com/debian").unwrap(),
890 Url::parse("http://example2.com/debian").unwrap(),
891 ],
892 suites: vec!["stable".to_string(), "testing".to_string()],
893 components: Some(vec!["main".to_string()]),
894 ..Default::default()
895 };
896
897 let legacy = repo.to_legacy_format();
898 assert!(legacy.contains("deb http://example1.com/debian stable main"));
900 assert!(legacy.contains("deb http://example1.com/debian testing main"));
901 assert!(legacy.contains("deb http://example2.com/debian stable main"));
902 assert!(legacy.contains("deb http://example2.com/debian testing main"));
903 }
904
905 #[test]
906 fn test_to_legacy_format_no_components() {
907 let repo = Repository {
908 types: HashSet::from([RepositoryType::Binary]),
909 uris: vec![Url::parse("http://example.com/debian").unwrap()],
910 suites: vec!["stable".to_string()],
911 components: None,
912 ..Default::default()
913 };
914
915 let legacy = repo.to_legacy_format();
916 assert_eq!(legacy, "deb http://example.com/debian stable\n");
917 }
918
919 #[test]
920 fn test_repository_type_display() {
921 assert_eq!(RepositoryType::Binary.to_string(), "deb");
922 assert_eq!(RepositoryType::Source.to_string(), "deb-src");
923 }
924
925 #[test]
926 fn test_yesnoforce_display() {
927 assert_eq!(crate::YesNoForce::Yes.to_string(), "yes");
928 assert_eq!(crate::YesNoForce::No.to_string(), "no");
929 assert_eq!(crate::YesNoForce::Force.to_string(), "force");
930 }
931
932 #[test]
933 fn test_repositories_is_empty() {
934 let empty_repos = Repositories::empty();
935 assert!(empty_repos.is_empty());
936
937 let mut repos = Repositories::empty();
938 repos.push(Repository::default());
939 assert!(!repos.is_empty());
940 }
941
942 #[test]
943 fn test_repository_getters() {
944 let repo = Repository {
945 types: HashSet::from([RepositoryType::Binary, RepositoryType::Source]),
946 uris: vec![Url::parse("http://example.com/debian").unwrap()],
947 suites: vec!["stable".to_string()],
948 components: Some(vec!["main".to_string(), "contrib".to_string()]),
949 architectures: vec!["amd64".to_string(), "arm64".to_string()],
950 ..Default::default()
951 };
952
953 assert_eq!(
955 repo.types(),
956 &HashSet::from([RepositoryType::Binary, RepositoryType::Source])
957 );
958
959 assert_eq!(repo.uris().len(), 1);
961 assert_eq!(repo.uris()[0].to_string(), "http://example.com/debian");
962
963 assert_eq!(repo.suites(), vec!["stable"]);
965
966 assert_eq!(
968 repo.components(),
969 Some(vec!["main".to_string(), "contrib".to_string()].as_slice())
970 );
971
972 assert_eq!(repo.architectures(), vec!["amd64", "arm64"]);
974 }
975
976 #[test]
977 fn test_repositories_iter() {
978 let mut repos = Repositories::empty();
979 repos.push(Repository {
980 suites: vec!["stable".to_string()],
981 ..Default::default()
982 });
983 repos.push(Repository {
984 suites: vec!["testing".to_string()],
985 ..Default::default()
986 });
987
988 let suites: Vec<_> = repos.iter().map(|r| r.suites()).collect();
990 assert_eq!(suites.len(), 2);
991 assert_eq!(suites[0], vec!["stable"]);
992 assert_eq!(suites[1], vec!["testing"]);
993
994 for repo in repos.iter_mut() {
996 repo.enabled = Some(false);
997 }
998
999 for repo in repos.iter() {
1000 assert_eq!(repo.enabled, Some(false));
1001 }
1002 }
1003
1004 #[test]
1005 fn test_parse_legacy_format() {
1006 let content = indoc! {r#"
1007 # This is a comment
1008 deb http://archive.ubuntu.com/ubuntu jammy main restricted
1009 deb-src http://archive.ubuntu.com/ubuntu jammy main restricted
1010
1011 # Another comment
1012 deb http://security.ubuntu.com/ubuntu jammy-security main
1013 "#};
1014
1015 let repos = Repositories::parse_legacy_format(content).unwrap();
1016 assert_eq!(repos.len(), 3);
1017
1018 assert!(repos[0].types().contains(&RepositoryType::Binary));
1020 assert_eq!(
1021 repos[0].uris()[0].to_string(),
1022 "http://archive.ubuntu.com/ubuntu"
1023 );
1024 assert_eq!(repos[0].suites(), vec!["jammy"]);
1025 assert_eq!(
1026 repos[0].components(),
1027 Some(vec!["main".to_string(), "restricted".to_string()].as_slice())
1028 );
1029
1030 assert!(repos[1].types().contains(&RepositoryType::Source));
1032
1033 assert_eq!(repos[2].suites(), vec!["jammy-security"]);
1035 assert_eq!(
1036 repos[2].components(),
1037 Some(vec!["main".to_string()].as_slice())
1038 );
1039 }
1040}