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