apt_sources/
sources_manager.rs

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