1use crate::{Repositories, Repository, RepositoryType};
2use std::collections::HashSet;
3use std::fs;
4use std::io;
5use std::io::Write;
6use std::path::{Path, PathBuf};
7
8pub const DEFAULT_SOURCES_PATH: &str = "/etc/apt/sources.list.d";
10pub const DEFAULT_KEYRING_PATH: &str = "/etc/apt/trusted.gpg.d";
12
13#[derive(Debug, Clone)]
15pub struct SourcesManager {
16 sources_dir: PathBuf,
17 keyring_dir: PathBuf,
18}
19
20impl Default for SourcesManager {
21 fn default() -> Self {
22 Self {
23 sources_dir: PathBuf::from(DEFAULT_SOURCES_PATH),
24 keyring_dir: PathBuf::from(DEFAULT_KEYRING_PATH),
25 }
26 }
27}
28
29impl SourcesManager {
30 pub fn new(sources_dir: impl Into<PathBuf>, keyring_dir: impl Into<PathBuf>) -> Self {
32 Self {
33 sources_dir: sources_dir.into(),
34 keyring_dir: keyring_dir.into(),
35 }
36 }
37
38 pub fn sources_dir(&self) -> &Path {
40 &self.sources_dir
41 }
42
43 pub fn keyring_dir(&self) -> &Path {
45 &self.keyring_dir
46 }
47
48 pub fn generate_filename(&self, name: &str, format: FileFormat) -> String {
50 let sanitized = name.replace(['/', ':', ' '], "-").to_lowercase();
51
52 match format {
53 FileFormat::Deb822 => format!("{sanitized}.sources"),
54 FileFormat::Legacy => format!("{sanitized}.list"),
55 }
56 }
57
58 pub fn get_repository_path(&self, filename: &str) -> PathBuf {
60 self.sources_dir.join(filename)
61 }
62
63 pub fn get_keyring_path(&self, filename: &str) -> PathBuf {
65 self.keyring_dir.join(filename)
66 }
67
68 pub fn write_repositories(&self, path: &Path, repositories: &Repositories) -> io::Result<()> {
70 let mut file = fs::File::create(path)?;
71 write!(file, "{repositories}")
72 }
73
74 pub fn read_repositories(&self, path: &Path) -> Result<Repositories, String> {
76 let content = fs::read_to_string(path)
77 .map_err(|e| format!("Failed to read file {}: {e}", path.display()))?;
78
79 content
80 .parse::<Repositories>()
81 .map_err(|e| format!("Failed to parse repositories: {e}"))
82 }
83
84 pub fn list_repository_files(&self) -> io::Result<Vec<PathBuf>> {
86 let mut files = Vec::new();
87
88 if self.sources_dir.exists() {
89 for entry in fs::read_dir(&self.sources_dir)? {
90 let entry = entry?;
91 let path = entry.path();
92 if path.is_file() {
93 if let Some(ext) = path.extension() {
94 if ext == "sources" || ext == "list" {
95 files.push(path);
96 }
97 }
98 }
99 }
100 }
101
102 Ok(files)
103 }
104
105 pub fn scan_all_repositories(&self) -> Result<Vec<(PathBuf, Repositories)>, String> {
107 let mut results = Vec::new();
108
109 let files = self
110 .list_repository_files()
111 .map_err(|e| format!("Failed to list repository files: {}", e))?;
112
113 for file in files {
114 match self.read_repositories(&file) {
115 Ok(repos) => results.push((file, repos)),
116 Err(e) => {
117 eprintln!("Warning: Failed to read {}: {}", file.display(), e);
119 }
120 }
121 }
122
123 Ok(results)
124 }
125
126 pub fn repository_exists(&self, repository: &Repository) -> Result<Option<PathBuf>, String> {
128 let all_repos = self.scan_all_repositories()?;
129
130 for (path, repos) in all_repos {
131 for repo in repos.iter() {
132 if repos_match(repo, repository) {
133 return Ok(Some(path));
134 }
135 }
136 }
137
138 Ok(None)
139 }
140
141 pub fn ensure_directories(&self) -> io::Result<()> {
143 fs::create_dir_all(&self.sources_dir)?;
144 fs::create_dir_all(&self.keyring_dir)?;
145 Ok(())
146 }
147
148 pub fn add_repository(&self, repository: &Repository, filename: &str) -> Result<(), String> {
150 let path = self.get_repository_path(filename);
151
152 if let Some(existing_path) = self.repository_exists(repository)? {
154 return Err(format!(
155 "Repository already exists in {}",
156 existing_path.display()
157 ));
158 }
159
160 let mut repositories = if path.exists() {
162 self.read_repositories(&path)?
163 } else {
164 Repositories::empty()
165 };
166
167 repositories.push(repository.clone());
169
170 self.write_repositories(&path, &repositories)
172 .map_err(|e| format!("Failed to write repository: {e}"))
173 }
174
175 pub fn remove_repository(&self, repository: &Repository) -> Result<bool, String> {
177 let mut removed = false;
178 let all_files = self.scan_all_repositories()?;
179
180 for (path, mut repos) in all_files {
181 let initial_count = repos.len();
182 repos.retain(|r| !repos_match(r, repository));
183
184 if repos.len() < initial_count {
185 removed = true;
186 if repos.is_empty() {
187 fs::remove_file(&path)
189 .map_err(|e| format!("Failed to remove {}: {e}", path.display()))?;
190 } else {
191 self.write_repositories(&path, &repos)
193 .map_err(|e| format!("Failed to update {}: {e}", path.display()))?;
194 }
195 }
196 }
197
198 Ok(removed)
199 }
200
201 pub fn set_repository_enabled(
203 &self,
204 repository: &Repository,
205 enabled: bool,
206 ) -> Result<bool, String> {
207 let mut modified = false;
208 let all_files = self.scan_all_repositories()?;
209
210 for (path, mut repos) in all_files {
211 let mut changed = false;
212 for repo in repos.iter_mut() {
213 if repos_match(repo, repository) && repo.enabled != Some(enabled) {
214 repo.enabled = Some(enabled);
215 changed = true;
216 modified = true;
217 }
218 }
219
220 if changed {
221 self.write_repositories(&path, &repos)
222 .map_err(|e| format!("Failed to update {}: {}", path.display(), e))?;
223 }
224 }
225
226 Ok(modified)
227 }
228
229 pub fn add_component_to_repositories(
231 &self,
232 component: &str,
233 filter: impl Fn(&Repository) -> bool,
234 ) -> Result<u32, String> {
235 let mut modified_count = 0;
236 let all_files = self.scan_all_repositories()?;
237
238 for (path, mut repos) in all_files {
239 let mut changed = false;
240
241 for repo in repos.iter_mut() {
242 if filter(repo) {
243 if let Some(components) = &mut repo.components {
244 if !components.contains(&component.to_string()) {
245 components.push(component.to_string());
246 changed = true;
247 modified_count += 1;
248 }
249 } else {
250 repo.components = Some(vec![component.to_string()]);
251 changed = true;
252 modified_count += 1;
253 }
254 }
255 }
256
257 if changed {
258 self.write_repositories(&path, &repos)
259 .map_err(|e| format!("Failed to update {}: {}", path.display(), e))?;
260 }
261 }
262
263 Ok(modified_count)
264 }
265
266 pub fn enable_source_repositories(
268 &self,
269 create_if_missing: bool,
270 ) -> Result<(u32, u32), String> {
271 let mut enabled_count = 0;
272 let mut created_count = 0;
273 let all_files = self.scan_all_repositories()?;
274
275 for (path, mut repos) in all_files {
276 let mut changed = false;
277 let mut new_repos = Vec::new();
278
279 for repo in repos.iter_mut() {
280 if repo.types.contains(&RepositoryType::Binary)
282 && !repo.types.contains(&RepositoryType::Source)
283 {
284 if repo.enabled == Some(false) {
285 repo.types.insert(RepositoryType::Source);
287 repo.enabled = Some(true);
288 enabled_count += 1;
289 changed = true;
290 } else if create_if_missing {
291 let mut source_repo = repo.clone();
293 source_repo.types = HashSet::from([RepositoryType::Source]);
294 new_repos.push(source_repo);
295 created_count += 1;
296 changed = true;
297 }
298 }
299 }
300
301 repos.extend(new_repos);
303
304 if changed {
305 self.write_repositories(&path, &repos)
306 .map_err(|e| format!("Failed to update {}: {}", path.display(), e))?;
307 }
308 }
309
310 Ok((enabled_count, created_count))
311 }
312
313 pub fn list_all_repositories(&self) -> Result<Vec<(PathBuf, Repository)>, String> {
315 let files = self.scan_all_repositories()?;
316
317 let total_repos: usize = files.iter().map(|(_, repos)| repos.len()).sum();
319 let mut all_repos = Vec::with_capacity(total_repos);
320
321 for (path, repos) in files {
322 for repo in repos.iter() {
323 all_repos.push((path.clone(), repo.clone()));
324 }
325 }
326
327 Ok(all_repos)
328 }
329
330 pub fn generate_keyring_filename(&self, repository_name: &str) -> String {
332 let sanitized = repository_name.replace(['/', ':', ' '], "-").to_lowercase();
333 format!("{sanitized}.gpg")
334 }
335
336 pub fn save_key(&self, key_data: &[u8], filename: &str) -> io::Result<PathBuf> {
338 let key_path = self.get_keyring_path(filename);
339 fs::write(&key_path, key_data)?;
340 Ok(key_path)
341 }
342}
343
344#[derive(Debug, Clone, Copy, PartialEq)]
346pub enum FileFormat {
347 Deb822,
349 Legacy,
351}
352
353fn repos_match(repo1: &Repository, repo2: &Repository) -> bool {
355 if repo1.types != repo2.types {
357 return false;
358 }
359
360 let uris1: HashSet<_> = repo1.uris.iter().collect();
362 let uris2: HashSet<_> = repo2.uris.iter().collect();
363 if uris1 != uris2 {
364 return false;
365 }
366
367 let suites1: HashSet<_> = repo1.suites.iter().collect();
369 let suites2: HashSet<_> = repo2.suites.iter().collect();
370 if suites1 != suites2 {
371 return false;
372 }
373
374 let components1: HashSet<_> = repo1.components.iter().collect();
376 let components2: HashSet<_> = repo2.components.iter().collect();
377 if components1 != components2 {
378 return false;
379 }
380
381 true
382}
383
384#[cfg(test)]
385mod tests {
386 use super::*;
387 use tempfile::TempDir;
388 use url::Url;
389
390 fn create_test_manager() -> (SourcesManager, TempDir) {
391 let temp_dir = TempDir::new().unwrap();
392 let sources_dir = temp_dir.path().join("sources.list.d");
393 let keyring_dir = temp_dir.path().join("trusted.gpg.d");
394
395 let manager = SourcesManager::new(&sources_dir, &keyring_dir);
396 (manager, temp_dir)
397 }
398
399 fn create_test_repository() -> Repository {
400 Repository {
401 enabled: Some(true),
402 types: HashSet::from([RepositoryType::Binary]),
403 uris: vec![Url::parse("http://example.com/ubuntu").unwrap()],
404 suites: vec!["focal".to_string()],
405 components: Some(vec!["main".to_string()]),
406 architectures: Some(vec!["amd64".to_string()]),
407 ..Default::default()
408 }
409 }
410
411 #[test]
412 fn test_ensure_directories() {
413 let (manager, _temp_dir) = create_test_manager();
414
415 assert!(!manager.sources_dir().exists());
416 assert!(!manager.keyring_dir().exists());
417
418 manager.ensure_directories().unwrap();
419
420 assert!(manager.sources_dir().exists());
421 assert!(manager.keyring_dir().exists());
422 }
423
424 #[test]
425 fn test_generate_filename() {
426 let (manager, _) = create_test_manager();
427
428 assert_eq!(
429 manager.generate_filename("test-repo", FileFormat::Deb822),
430 "test-repo.sources"
431 );
432
433 assert_eq!(
434 manager.generate_filename("Test/Repo:Name", FileFormat::Legacy),
435 "test-repo-name.list"
436 );
437 }
438
439 #[test]
440 fn test_add_repository() {
441 let (manager, _) = create_test_manager();
442 manager.ensure_directories().unwrap();
443
444 let repo = create_test_repository();
445
446 manager.add_repository(&repo, "test.sources").unwrap();
448
449 let path = manager.get_repository_path("test.sources");
451 assert!(path.exists());
452
453 let repos = manager.read_repositories(&path).unwrap();
454 assert_eq!(repos.len(), 1);
455 assert_eq!(repos[0].uris[0].as_str(), "http://example.com/ubuntu");
456
457 let result = manager.add_repository(&repo, "test2.sources");
459 assert!(result.is_err());
460 assert!(result.unwrap_err().contains("already exists"));
461 }
462
463 #[test]
464 fn test_remove_repository() {
465 let (manager, _) = create_test_manager();
466 manager.ensure_directories().unwrap();
467
468 let repo = create_test_repository();
469
470 manager.add_repository(&repo, "test.sources").unwrap();
472
473 let removed = manager.remove_repository(&repo).unwrap();
475 assert!(removed);
476
477 let path = manager.get_repository_path("test.sources");
479 assert!(!path.exists());
480
481 let removed = manager.remove_repository(&repo).unwrap();
483 assert!(!removed);
484 }
485
486 #[test]
487 fn test_set_repository_enabled() {
488 let (manager, _) = create_test_manager();
489 manager.ensure_directories().unwrap();
490
491 let mut repo = create_test_repository();
492 repo.enabled = Some(true);
493
494 manager.add_repository(&repo, "test.sources").unwrap();
496
497 let modified = manager.set_repository_enabled(&repo, false).unwrap();
499 assert!(modified);
500
501 let path = manager.get_repository_path("test.sources");
503 let repos = manager.read_repositories(&path).unwrap();
504 assert_eq!(repos[0].enabled, Some(false));
505
506 let modified = manager.set_repository_enabled(&repo, true).unwrap();
508 assert!(modified);
509
510 let repos = manager.read_repositories(&path).unwrap();
512 assert_eq!(repos[0].enabled, Some(true));
513 }
514
515 #[test]
516 fn test_add_component_to_repositories() {
517 let (manager, _) = create_test_manager();
518 manager.ensure_directories().unwrap();
519
520 let repo = create_test_repository();
521
522 manager.add_repository(&repo, "test.sources").unwrap();
524
525 let count = manager
527 .add_component_to_repositories("universe", |r| {
528 r.uris.iter().any(|u| u.host_str() == Some("example.com"))
529 })
530 .unwrap();
531 assert_eq!(count, 1);
532
533 let path = manager.get_repository_path("test.sources");
535 let repos = manager.read_repositories(&path).unwrap();
536 assert!(repos[0]
537 .components
538 .as_ref()
539 .unwrap()
540 .contains(&"universe".to_string()));
541
542 let count = manager
544 .add_component_to_repositories("universe", |r| {
545 r.uris.iter().any(|u| u.host_str() == Some("example.com"))
546 })
547 .unwrap();
548 assert_eq!(count, 0);
549 }
550
551 #[test]
552 fn test_enable_source_repositories() {
553 let (manager, _) = create_test_manager();
554 manager.ensure_directories().unwrap();
555
556 let repo = create_test_repository();
557
558 manager.add_repository(&repo, "test.sources").unwrap();
560
561 let (enabled, created) = manager.enable_source_repositories(true).unwrap();
563 assert_eq!(enabled, 0);
564 assert_eq!(created, 1);
565
566 let path = manager.get_repository_path("test.sources");
568 let repos = manager.read_repositories(&path).unwrap();
569 assert_eq!(repos.len(), 2);
570
571 let source_repo = repos
573 .iter()
574 .find(|r| r.types.contains(&RepositoryType::Source))
575 .unwrap();
576 assert!(source_repo.types.contains(&RepositoryType::Source));
577 assert!(!source_repo.types.contains(&RepositoryType::Binary));
578 }
579
580 #[test]
581 fn test_list_repository_files() {
582 let (manager, _) = create_test_manager();
583 manager.ensure_directories().unwrap();
584
585 let files = manager.list_repository_files().unwrap();
587 assert_eq!(files.len(), 0);
588
589 let repo1 = create_test_repository();
591 let mut repo2 = create_test_repository();
592 repo2.suites = vec!["jammy".to_string()]; manager.add_repository(&repo1, "test1.sources").unwrap();
594 manager.add_repository(&repo2, "test2.list").unwrap();
595
596 let files = manager.list_repository_files().unwrap();
598 assert_eq!(files.len(), 2);
599
600 let non_repo = manager.get_repository_path("test.txt");
602 fs::write(&non_repo, "not a repo").unwrap();
603
604 let files = manager.list_repository_files().unwrap();
606 assert_eq!(files.len(), 2);
607 }
608
609 #[test]
610 fn test_scan_all_repositories() {
611 let (manager, _) = create_test_manager();
612 manager.ensure_directories().unwrap();
613
614 let repo1 = create_test_repository();
615 let mut repo2 = create_test_repository();
616 repo2.uris = vec![Url::parse("http://example2.com/ubuntu").unwrap()];
617
618 manager.add_repository(&repo1, "test1.sources").unwrap();
620 manager.add_repository(&repo2, "test2.sources").unwrap();
621
622 let all_repos = manager.scan_all_repositories().unwrap();
624 assert_eq!(all_repos.len(), 2);
625
626 for (_, repos) in all_repos {
628 assert_eq!(repos.len(), 1);
629 }
630 }
631
632 #[test]
633 fn test_repository_exists() {
634 let (manager, _) = create_test_manager();
635 manager.ensure_directories().unwrap();
636
637 let repo = create_test_repository();
638
639 assert!(manager.repository_exists(&repo).unwrap().is_none());
641
642 manager.add_repository(&repo, "test.sources").unwrap();
644
645 let existing_path = manager.repository_exists(&repo).unwrap();
647 assert!(existing_path.is_some());
648 assert!(existing_path.unwrap().ends_with("test.sources"));
649 }
650
651 #[test]
652 fn test_save_key() {
653 let (manager, _) = create_test_manager();
654 manager.ensure_directories().unwrap();
655
656 let key_data =
657 b"-----BEGIN PGP PUBLIC KEY BLOCK-----\ntest key\n-----END PGP PUBLIC KEY BLOCK-----";
658
659 let key_path = manager.save_key(key_data, "test.gpg").unwrap();
660 assert!(key_path.exists());
661 assert_eq!(key_path.file_name().unwrap(), "test.gpg");
662
663 let saved_data = fs::read(&key_path).unwrap();
664 assert_eq!(saved_data, key_data);
665 }
666
667 #[test]
668 fn test_repos_match() {
669 let repo1 = create_test_repository();
670 let mut repo2 = repo1.clone();
671
672 assert!(repos_match(&repo1, &repo2));
674
675 repo2.types.insert(RepositoryType::Source);
677 assert!(!repos_match(&repo1, &repo2));
678 repo2.types = repo1.types.clone();
679
680 repo2.uris.push(Url::parse("http://extra.com").unwrap());
682 assert!(!repos_match(&repo1, &repo2));
683 repo2.uris = repo1.uris.clone();
684
685 repo2.suites.push("bionic".to_string());
687 assert!(!repos_match(&repo1, &repo2));
688 repo2.suites = repo1.suites.clone();
689
690 if let Some(ref mut components) = repo2.components {
692 components.push("universe".to_string());
693 }
694 assert!(!repos_match(&repo1, &repo2));
695 }
696
697 #[test]
698 fn test_generate_keyring_filename() {
699 let (manager, _) = create_test_manager();
700
701 assert_eq!(
703 manager.generate_keyring_filename("test-repo"),
704 "test-repo.gpg"
705 );
706
707 assert_eq!(
709 manager.generate_keyring_filename("Test/Repo:Name With Spaces"),
710 "test-repo-name-with-spaces.gpg"
711 );
712
713 assert_eq!(manager.generate_keyring_filename(""), ".gpg");
715 }
716
717 #[test]
718 fn test_list_all_repositories() {
719 let (manager, _) = create_test_manager();
720 manager.ensure_directories().unwrap();
721
722 let all_repos = manager.list_all_repositories().unwrap();
724 assert!(all_repos.is_empty());
725
726 let repo1 = create_test_repository();
728 let mut repo2 = create_test_repository();
729 repo2.suites = vec!["jammy".to_string()];
730
731 manager.add_repository(&repo1, "test1.sources").unwrap();
732 manager.add_repository(&repo2, "test2.sources").unwrap();
733
734 let all_repos = manager.list_all_repositories().unwrap();
736 assert_eq!(all_repos.len(), 2);
737
738 let paths: Vec<_> = all_repos
740 .iter()
741 .map(|(p, _)| p.file_name().unwrap())
742 .collect();
743 assert!(paths.contains(&std::ffi::OsStr::new("test1.sources")));
744 assert!(paths.contains(&std::ffi::OsStr::new("test2.sources")));
745 }
746
747 #[test]
748 fn test_enable_source_repositories_counter_edge_cases() {
749 let (manager, _) = create_test_manager();
750 manager.ensure_directories().unwrap();
751
752 let mut repo = create_test_repository();
754 repo.types = HashSet::from([RepositoryType::Binary]);
755 manager.add_repository(&repo, "test.sources").unwrap();
756
757 let (enabled, created) = manager.enable_source_repositories(true).unwrap();
759 assert_eq!(enabled, 0);
760 assert_eq!(created, 1);
761
762 let all_repos = manager.list_all_repositories().unwrap();
764 let source_repos: Vec<_> = all_repos
765 .iter()
766 .filter(|(_, r)| {
767 r.types.contains(&RepositoryType::Source)
768 && !r.types.contains(&RepositoryType::Binary)
769 })
770 .collect();
771 assert_eq!(source_repos.len(), 1);
772
773 let mut disabled_repo = create_test_repository();
775 disabled_repo.types = HashSet::from([RepositoryType::Binary]);
776 disabled_repo.enabled = Some(false);
777 disabled_repo.suites = vec!["jammy".to_string()]; manager
779 .add_repository(&disabled_repo, "test2.sources")
780 .unwrap();
781
782 let (enabled2, created2) = manager.enable_source_repositories(true).unwrap();
786 assert_eq!(enabled2, 1); assert_eq!(created2, 1); }
789
790 #[test]
791 fn test_set_repository_enabled_edge_cases() {
792 let (manager, _) = create_test_manager();
793 manager.ensure_directories().unwrap();
794
795 let mut repo = create_test_repository();
796 repo.enabled = Some(true);
797
798 manager.add_repository(&repo, "test.sources").unwrap();
800
801 let modified = manager.set_repository_enabled(&repo, true).unwrap();
803 assert!(!modified);
804
805 let modified = manager.set_repository_enabled(&repo, false).unwrap();
807 assert!(modified);
808
809 let modified = manager.set_repository_enabled(&repo, false).unwrap();
811 assert!(!modified);
812 }
813
814 #[test]
815 fn test_add_component_edge_cases() {
816 let (manager, _) = create_test_manager();
817 manager.ensure_directories().unwrap();
818
819 let mut repo = create_test_repository();
820 repo.components = Some(vec!["main".to_string()]);
821
822 manager.add_repository(&repo, "test.sources").unwrap();
823
824 let count = manager
826 .add_component_to_repositories("main", |_| true)
827 .unwrap();
828 assert_eq!(count, 0);
829
830 let count = manager
832 .add_component_to_repositories("universe", |_| true)
833 .unwrap();
834 assert_eq!(count, 1);
835
836 let mut repo2 = create_test_repository();
838 repo2.components = None;
839 manager.add_repository(&repo2, "test2.sources").unwrap();
840
841 let count = manager
842 .add_component_to_repositories("restricted", |r| r.components.is_none())
843 .unwrap();
844 assert_eq!(count, 1);
845 }
846}