1#![deny(missing_docs)]
2use 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
57pub mod distribution;
59pub mod error;
60#[cfg(feature = "key-management")]
62pub mod key_management;
63#[cfg(feature = "key-management")]
64pub mod keyserver;
65#[cfg(feature = "launchpad")]
67pub mod launchpad;
68#[cfg(feature = "legacy")]
69pub mod legacy;
70pub mod signature;
71pub mod sources_manager;
73pub mod utils;
75
76#[derive(PartialEq, Eq, Hash, Debug, Clone)]
79pub enum RepositoryType {
80 Binary,
82 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)]
118pub enum YesNoForce {
120 Yes,
122 No,
124 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 text.split_whitespace()
175 .map(Url::from_str)
176 .collect::<Result<Vec<Url>, _>>()
177 .map_err(|e| e.to_string()) }
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 Ok(text.split_whitespace().map(|x| x.to_string()).collect())
187}
188
189fn deserialize_yesno(text: &str) -> Result<bool, RepositoryError> {
190 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#[derive(FromDeb822, ToDeb822, Clone, PartialEq, Debug, Default)]
246pub struct Repository {
247 #[deb822(field = "Enabled", deserialize_with = deserialize_yesno, serialize_with = serializer_yesno)]
249 pub enabled: Option<bool>,
251
252 #[deb822(field = "Types", deserialize_with = deserialize_types, serialize_with = serialize_types)]
254 pub types: HashSet<RepositoryType>, #[deb822(field = "URIs", deserialize_with = deserialize_uris, serialize_with = serialize_uris)]
257 pub uris: Vec<Url>, #[deb822(field = "Suites", deserialize_with = deserialize_string_chain, serialize_with = serialize_string_chain)]
260 pub suites: Vec<String>,
261 #[deb822(field = "Components", deserialize_with = deserialize_string_chain, serialize_with = serialize_string_chain)]
264 pub components: Option<Vec<String>>,
265
266 #[deb822(field = "Architectures", deserialize_with = deserialize_string_chain, serialize_with = serialize_string_chain)]
268 pub architectures: Option<Vec<String>>,
269 #[deb822(field = "Languages", deserialize_with = deserialize_string_chain, serialize_with = serialize_string_chain)]
271 pub languages: Option<Vec<String>>, #[deb822(field = "Targets", deserialize_with = deserialize_string_chain, serialize_with = serialize_string_chain)]
274 pub targets: Option<Vec<String>>,
275 #[deb822(field = "PDiffs", deserialize_with = deserialize_yesno)]
277 pub pdiffs: Option<bool>,
278 #[deb822(field = "By-Hash")]
280 pub by_hash: Option<YesNoForce>,
281 #[deb822(field = "Allow-Insecure")]
283 pub allow_insecure: Option<bool>, #[deb822(field = "Allow-Weak")]
286 pub allow_weak: Option<bool>, #[deb822(field = "Allow-Downgrade-To-Insecure")]
289 pub allow_downgrade_to_insecure: Option<bool>, #[deb822(field = "Trusted")]
292 pub trusted: Option<bool>,
293 #[deb822(field = "Signed-By")]
296 pub signature: Option<Signature>,
297
298 #[deb822(field = "X-Repolib-Name")]
300 pub x_repolib_name: Option<String>, #[deb822(field = "Description")]
304 pub description: Option<String>, }
306
307impl Repository {
308 pub fn suites(&self) -> &[String] {
310 self.suites.as_slice()
311 }
312
313 pub fn types(&self) -> &HashSet<RepositoryType> {
315 &self.types
316 }
317
318 pub fn uris(&self) -> &[Url] {
320 &self.uris
321 }
322
323 pub fn components(&self) -> Option<&[String]> {
325 self.components.as_deref()
326 }
327
328 pub fn architectures(&self) -> &[String] {
330 self.architectures.as_deref().unwrap_or(&[])
331 }
332}
333
334#[derive(Debug, Clone, PartialEq)]
336pub struct Repositories(Vec<Repository>);
337
338impl Default for Repositories {
339 fn default() -> Self {
344 let (repos, errors) = Self::load_from_directory(std::path::Path::new("/etc/apt"));
345
346 #[cfg(feature = "tracing")]
348 for error in errors {
349 tracing::warn!("Failed to load APT source: {}", error);
350 }
351
352 #[cfg(not(feature = "tracing"))]
354 let _ = errors;
355
356 repos
357 }
358}
359
360impl Repositories {
361 pub fn empty() -> Self {
363 Repositories(Vec::new())
364 }
365
366 pub fn new<Container>(container: Container) -> Self
368 where
369 Container: Into<Vec<Repository>>,
370 {
371 Repositories(container.into())
372 }
373
374 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 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){
407 Ok(repos) => {
408 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(), }),
415 }
416 }
417 Err(e) => errors.push(LoadError::Io {
418 path: main_sources,
419 error: e,
420 }),
421 }
422 }
423
424 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 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(), })
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 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 pub fn push(&mut self, repo: Repository) {
518 self.0.push(repo);
519 }
520
521 pub fn retain<F>(&mut self, f: F)
523 where
524 F: FnMut(&Repository) -> bool,
525 {
526 self.0.retain(f);
527 }
528
529 pub fn iter(&self) -> std::slice::Iter<'_, Repository> {
531 self.0.iter()
532 }
533
534 pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, Repository> {
536 self.0.iter_mut()
537 }
538
539 pub fn extend<I>(&mut self, iter: I)
541 where
542 I: IntoIterator<Item = Repository>,
543 {
544 self.0.extend(iter);
545 }
546
547 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 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::new([Repository {
699 enabled: Some(true), 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 assert_eq!(
774 repo.types(),
775 &HashSet::from([RepositoryType::Binary, RepositoryType::Source])
776 );
777
778 assert_eq!(repo.uris().len(), 1);
780 assert_eq!(repo.uris()[0].to_string(), "http://example.com/debian");
781
782 assert_eq!(repo.suites(), vec!["stable"]);
784
785 assert_eq!(
787 repo.components(),
788 Some(vec!["main".to_string(), "contrib".to_string()].as_slice())
789 );
790
791 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 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 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}