Skip to main content

apt_sources/
sources_manager.rs

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
8/// Default path for APT sources files
9pub const DEFAULT_SOURCES_PATH: &str = "/etc/apt/sources.list.d";
10/// Default path for APT keyring files
11pub const DEFAULT_KEYRING_PATH: &str = "/etc/apt/trusted.gpg.d";
12
13/// Manager for APT sources and keyrings
14#[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    /// Create a new SourcesManager with custom directories
31    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    /// Get the path to the sources directory
39    pub fn sources_dir(&self) -> &Path {
40        &self.sources_dir
41    }
42
43    /// Get the path to the keyring directory
44    pub fn keyring_dir(&self) -> &Path {
45        &self.keyring_dir
46    }
47
48    /// Generate a filename for a repository
49    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    /// Get the full path for a repository file
59    pub fn get_repository_path(&self, filename: &str) -> PathBuf {
60        self.sources_dir.join(filename)
61    }
62
63    /// Get the full path for a keyring file
64    pub fn get_keyring_path(&self, filename: &str) -> PathBuf {
65        self.keyring_dir.join(filename)
66    }
67
68    /// Write repositories to a file
69    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    /// Read repositories from a file
75    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    /// List all repository files in the sources directory
85    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    /// Scan all repository files and return their contents
106    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                    // Log error but continue scanning
118                    eprintln!("Warning: Failed to read {}: {}", file.display(), e);
119                }
120            }
121        }
122
123        Ok(results)
124    }
125
126    /// Check if a repository already exists in any file
127    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    /// Ensure the sources and keyring directories exist
142    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    /// Add a repository to a file, creating the file if it doesn't exist
149    pub fn add_repository(&self, repository: &Repository, filename: &str) -> Result<(), String> {
150        let path = self.get_repository_path(filename);
151
152        // Check if repository already exists
153        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        // Read existing repositories if file exists
161        let mut repositories = if path.exists() {
162            self.read_repositories(&path)?
163        } else {
164            Repositories::empty()
165        };
166
167        // Add the new repository
168        repositories.push(repository.clone());
169
170        // Write back to file
171        self.write_repositories(&path, &repositories)
172            .map_err(|e| format!("Failed to write repository: {e}"))
173    }
174
175    /// Remove a repository from all files
176    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                    // Remove empty file
188                    fs::remove_file(&path)
189                        .map_err(|e| format!("Failed to remove {}: {e}", path.display()))?;
190                } else {
191                    // Write updated repositories
192                    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    /// Enable or disable a repository
202    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    /// Add a component to all matching repositories
230    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    /// Enable source repositories
267    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                // Check if this repo has binary type but not source
281                if repo.types.contains(&RepositoryType::Binary)
282                    && !repo.types.contains(&RepositoryType::Source)
283                {
284                    if repo.enabled == Some(false) {
285                        // Just enable existing disabled source repo
286                        repo.types.insert(RepositoryType::Source);
287                        repo.enabled = Some(true);
288                        enabled_count += 1;
289                        changed = true;
290                    } else if create_if_missing {
291                        // Create a new source repository entry
292                        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            // Add any new repositories
302            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    /// List all repositories with their file paths
314    pub fn list_all_repositories(&self) -> Result<Vec<(PathBuf, Repository)>, String> {
315        let files = self.scan_all_repositories()?;
316
317        // Pre-calculate capacity to avoid reallocations
318        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    /// Generate a keyring filename for a repository
331    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    /// Save a GPG key to the keyring directory
337    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/// File format for APT source list files
345#[derive(Debug, Clone, Copy, PartialEq)]
346pub enum FileFormat {
347    /// Deb822 format (new style)
348    Deb822,
349    /// Legacy format (one-line style)
350    Legacy,
351}
352
353/// Check if two repositories match (have the same URIs, suites, and components)
354fn repos_match(repo1: &Repository, repo2: &Repository) -> bool {
355    // Compare types
356    if repo1.types != repo2.types {
357        return false;
358    }
359
360    // Compare URIs
361    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    // Compare suites
368    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    // Compare components
375    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        // Add repository
447        manager.add_repository(&repo, "test.sources").unwrap();
448
449        // Verify it was added
450        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        // Try to add duplicate - should fail
458        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        // Add repository
471        manager.add_repository(&repo, "test.sources").unwrap();
472
473        // Remove it
474        let removed = manager.remove_repository(&repo).unwrap();
475        assert!(removed);
476
477        // Verify file was removed
478        let path = manager.get_repository_path("test.sources");
479        assert!(!path.exists());
480
481        // Try to remove again - should return false
482        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        // Add repository
495        manager.add_repository(&repo, "test.sources").unwrap();
496
497        // Disable it
498        let modified = manager.set_repository_enabled(&repo, false).unwrap();
499        assert!(modified);
500
501        // Verify it was disabled
502        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        // Enable it again
507        let modified = manager.set_repository_enabled(&repo, true).unwrap();
508        assert!(modified);
509
510        // Verify it was enabled
511        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        // Add repository
523        manager.add_repository(&repo, "test.sources").unwrap();
524
525        // Add component to repositories from example.com
526        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        // Verify component was added
534        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        // Try to add same component again - should not modify
543        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        // Add repository
559        manager.add_repository(&repo, "test.sources").unwrap();
560
561        // Enable source repositories (create if missing)
562        let (enabled, created) = manager.enable_source_repositories(true).unwrap();
563        assert_eq!(enabled, 0);
564        assert_eq!(created, 1);
565
566        // Verify source repo was created
567        let path = manager.get_repository_path("test.sources");
568        let repos = manager.read_repositories(&path).unwrap();
569        assert_eq!(repos.len(), 2);
570
571        // Find the source repo
572        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        // Initially empty
586        let files = manager.list_repository_files().unwrap();
587        assert_eq!(files.len(), 0);
588
589        // Add some files
590        let repo1 = create_test_repository();
591        let mut repo2 = create_test_repository();
592        repo2.suites = vec!["jammy".to_string()]; // Make it different
593        manager.add_repository(&repo1, "test1.sources").unwrap();
594        manager.add_repository(&repo2, "test2.list").unwrap();
595
596        // Should find both files
597        let files = manager.list_repository_files().unwrap();
598        assert_eq!(files.len(), 2);
599
600        // Create a non-repository file
601        let non_repo = manager.get_repository_path("test.txt");
602        fs::write(&non_repo, "not a repo").unwrap();
603
604        // Should still only find 2 repository files
605        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        // Add repositories to different files
619        manager.add_repository(&repo1, "test1.sources").unwrap();
620        manager.add_repository(&repo2, "test2.sources").unwrap();
621
622        // Scan all
623        let all_repos = manager.scan_all_repositories().unwrap();
624        assert_eq!(all_repos.len(), 2);
625
626        // Each file should have one repository
627        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        // Should not exist initially
640        assert!(manager.repository_exists(&repo).unwrap().is_none());
641
642        // Add repository
643        manager.add_repository(&repo, "test.sources").unwrap();
644
645        // Should exist now
646        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        // Should match identical repos
673        assert!(repos_match(&repo1, &repo2));
674
675        // Different types
676        repo2.types.insert(RepositoryType::Source);
677        assert!(!repos_match(&repo1, &repo2));
678        repo2.types = repo1.types.clone();
679
680        // Different URIs
681        repo2.uris.push(Url::parse("http://extra.com").unwrap());
682        assert!(!repos_match(&repo1, &repo2));
683        repo2.uris = repo1.uris.clone();
684
685        // Different suites
686        repo2.suites.push("bionic".to_string());
687        assert!(!repos_match(&repo1, &repo2));
688        repo2.suites = repo1.suites.clone();
689
690        // Different components
691        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        // Test basic filename
702        assert_eq!(
703            manager.generate_keyring_filename("test-repo"),
704            "test-repo.gpg"
705        );
706
707        // Test with special characters that should be sanitized
708        assert_eq!(
709            manager.generate_keyring_filename("Test/Repo:Name With Spaces"),
710            "test-repo-name-with-spaces.gpg"
711        );
712
713        // Test empty string
714        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        // Initially empty
723        let all_repos = manager.list_all_repositories().unwrap();
724        assert!(all_repos.is_empty());
725
726        // Add some repositories
727        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        // Should list all repositories with their paths
735        let all_repos = manager.list_all_repositories().unwrap();
736        assert_eq!(all_repos.len(), 2);
737
738        // Check that paths are included
739        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        // Add a binary repository
753        let mut repo = create_test_repository();
754        repo.types = HashSet::from([RepositoryType::Binary]);
755        manager.add_repository(&repo, "test.sources").unwrap();
756
757        // Enable source repositories with creation
758        let (enabled, created) = manager.enable_source_repositories(true).unwrap();
759        assert_eq!(enabled, 0);
760        assert_eq!(created, 1);
761
762        // Check that source repo was actually created
763        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        // Add a disabled binary repository
774        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()]; // Make it different
778        manager
779            .add_repository(&disabled_repo, "test2.sources")
780            .unwrap();
781
782        // Enable source repositories again
783        // The first binary repo will create another source repo
784        // The disabled binary repo will have source type added and be enabled
785        let (enabled2, created2) = manager.enable_source_repositories(true).unwrap();
786        assert_eq!(enabled2, 1); // Should enable the disabled binary repo
787        assert_eq!(created2, 1); // Will create a source repo for the new binary repo
788    }
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        // Add repository
799        manager.add_repository(&repo, "test.sources").unwrap();
800
801        // Try to enable already enabled repo - should return false
802        let modified = manager.set_repository_enabled(&repo, true).unwrap();
803        assert!(!modified);
804
805        // Disable it
806        let modified = manager.set_repository_enabled(&repo, false).unwrap();
807        assert!(modified);
808
809        // Try to disable already disabled repo - should return false
810        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        // Add component that already exists - should not increment counter
825        let count = manager
826            .add_component_to_repositories("main", |_| true)
827            .unwrap();
828        assert_eq!(count, 0);
829
830        // Add new component
831        let count = manager
832            .add_component_to_repositories("universe", |_| true)
833            .unwrap();
834        assert_eq!(count, 1);
835
836        // Add to repo with no components initially
837        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}