apt_sources/
distribution.rs

1use std::fs;
2use std::process::Command;
3
4/// Represents a Linux distribution
5#[derive(Debug, Clone, PartialEq)]
6pub enum Distribution {
7    /// Ubuntu Linux
8    Ubuntu,
9    /// Debian Linux
10    Debian,
11    /// Other distribution
12    Other(String),
13}
14
15impl Distribution {
16    /// Get the current system's distribution information
17    pub fn current() -> Result<Distribution, String> {
18        // First try lsb_release for distribution ID
19        let lsb_id = Command::new("lsb_release").args(["-i", "-s"]).output();
20
21        if let Ok(output) = lsb_id {
22            if output.status.success() {
23                let distro_id = String::from_utf8_lossy(&output.stdout)
24                    .trim()
25                    .to_lowercase();
26
27                return Ok(match distro_id.as_str() {
28                    "ubuntu" => Distribution::Ubuntu,
29                    "debian" => Distribution::Debian,
30                    other => Distribution::Other(other.to_owned()),
31                });
32            }
33        }
34
35        // Fall back to /etc/os-release if lsb_release fails
36        if let Ok(content) = fs::read_to_string("/etc/os-release") {
37            for line in content.lines() {
38                if line.starts_with("ID=") {
39                    let id = line
40                        .trim_start_matches("ID=")
41                        .trim_matches('"')
42                        .to_lowercase();
43
44                    return Ok(match id.as_str() {
45                        "ubuntu" => Distribution::Ubuntu,
46                        "debian" => Distribution::Debian,
47                        other => Distribution::Other(other.to_owned()),
48                    });
49                }
50            }
51        }
52
53        // If all else fails, assume Debian-based
54        Ok(Distribution::Other("unknown".to_owned()))
55    }
56
57    /// Get default components for this distribution
58    pub fn default_components(&self) -> Vec<&'static str> {
59        match self {
60            Distribution::Ubuntu => vec!["main", "universe"],
61            Distribution::Debian => vec!["main"],
62            Distribution::Other(_) => vec!["main"],
63        }
64    }
65
66    /// Check if a repository is a main distribution repository
67    pub fn is_main_repository(&self, repo: &crate::Repository) -> bool {
68        for uri in &repo.uris {
69            if let Some(host) = uri.host_str() {
70                match self {
71                    Distribution::Ubuntu => {
72                        if host.contains("ubuntu.com")
73                            || host.contains("canonical.com")
74                            || host == "archive.ubuntu.com"
75                            || host == "security.ubuntu.com"
76                            || host == "ports.ubuntu.com"
77                        {
78                            return true;
79                        }
80                    }
81                    Distribution::Debian => {
82                        if host.contains("debian.org")
83                            || host == "deb.debian.org"
84                            || host == "security.debian.org"
85                        {
86                            return true;
87                        }
88                    }
89                    _ => {}
90                }
91            }
92        }
93        false
94    }
95}
96
97/// Get system information (codename and architecture)
98pub fn get_system_info() -> Result<(String, String), String> {
99    // Get distribution codename
100    let lsb_release = Command::new("lsb_release")
101        .args(["-c", "-s"])
102        .output()
103        .map_err(|e| format!("Failed to run lsb_release: {}", e))?;
104
105    if !lsb_release.status.success() {
106        return Err("Failed to determine distribution codename".to_string());
107    }
108
109    let codename = String::from_utf8_lossy(&lsb_release.stdout)
110        .trim()
111        .to_string();
112
113    // Get architecture
114    let dpkg_arch = Command::new("dpkg")
115        .arg("--print-architecture")
116        .output()
117        .map_err(|e| format!("Failed to run dpkg: {}", e))?;
118
119    if !dpkg_arch.status.success() {
120        return Err("Failed to determine system architecture".to_string());
121    }
122
123    let arch = String::from_utf8_lossy(&dpkg_arch.stdout)
124        .trim()
125        .to_string();
126
127    Ok((codename, arch))
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn test_is_main_repository() {
136        let dist = Distribution::Ubuntu;
137        let repo = crate::Repository {
138            uris: vec![url::Url::parse("http://archive.ubuntu.com/ubuntu").unwrap()],
139            suites: vec!["jammy".to_string()],
140            components: Some(vec!["main".to_string()]),
141            ..Default::default()
142        };
143        assert!(dist.is_main_repository(&repo));
144
145        let non_main_repo = crate::Repository {
146            uris: vec![url::Url::parse("http://example.com/ubuntu").unwrap()],
147            suites: vec!["jammy".to_string()],
148            components: Some(vec!["main".to_string()]),
149            ..Default::default()
150        };
151        assert!(!dist.is_main_repository(&non_main_repo));
152    }
153
154    #[test]
155    fn test_is_main_repository_all_ubuntu_hosts() {
156        let dist = Distribution::Ubuntu;
157
158        // Test each Ubuntu host individually
159        let ubuntu_hosts = [
160            "http://archive.ubuntu.com/ubuntu",
161            "http://security.ubuntu.com/ubuntu",
162            "http://ports.ubuntu.com/ubuntu-ports",
163            "http://us.archive.ubuntu.com/ubuntu", // contains ubuntu.com
164            "http://mirrors.canonical.com/ubuntu", // contains canonical.com
165        ];
166
167        for host in &ubuntu_hosts {
168            let repo = crate::Repository {
169                uris: vec![url::Url::parse(host).unwrap()],
170                ..Default::default()
171            };
172            assert!(dist.is_main_repository(&repo), "Failed for host: {}", host);
173        }
174    }
175
176    #[test]
177    fn test_is_main_repository_all_debian_hosts() {
178        let dist = Distribution::Debian;
179
180        // Test each Debian host individually
181        let debian_hosts = [
182            "http://deb.debian.org/debian",
183            "http://security.debian.org/debian-security",
184            "http://ftp.debian.org/debian",     // contains debian.org
185            "http://mirrors.debian.org/debian", // contains debian.org
186        ];
187
188        for host in &debian_hosts {
189            let repo = crate::Repository {
190                uris: vec![url::Url::parse(host).unwrap()],
191                ..Default::default()
192            };
193            assert!(dist.is_main_repository(&repo), "Failed for host: {}", host);
194        }
195    }
196
197    #[test]
198    fn test_is_main_repository_other_distribution() {
199        let dist = Distribution::Other("mint".to_string());
200
201        // Other distributions should not match any repository
202        let repo = crate::Repository {
203            uris: vec![url::Url::parse("http://archive.ubuntu.com/ubuntu").unwrap()],
204            ..Default::default()
205        };
206        assert!(!dist.is_main_repository(&repo));
207
208        let repo2 = crate::Repository {
209            uris: vec![url::Url::parse("http://deb.debian.org/debian").unwrap()],
210            ..Default::default()
211        };
212        assert!(!dist.is_main_repository(&repo2));
213    }
214
215    #[test]
216    fn test_is_main_repository_empty_uris() {
217        let dist = Distribution::Ubuntu;
218        let repo = crate::Repository {
219            uris: vec![],
220            ..Default::default()
221        };
222        assert!(!dist.is_main_repository(&repo));
223    }
224
225    #[test]
226    fn test_is_main_repository_multiple_uris() {
227        let dist = Distribution::Ubuntu;
228        let repo = crate::Repository {
229            uris: vec![
230                url::Url::parse("http://example.com/ubuntu").unwrap(),
231                url::Url::parse("http://archive.ubuntu.com/ubuntu").unwrap(),
232            ],
233            ..Default::default()
234        };
235        // Should return true if ANY URI matches
236        assert!(dist.is_main_repository(&repo));
237    }
238
239    #[test]
240    fn test_default_components() {
241        assert_eq!(
242            Distribution::Ubuntu.default_components(),
243            vec!["main", "universe"]
244        );
245        assert_eq!(Distribution::Debian.default_components(), vec!["main"]);
246        assert_eq!(
247            Distribution::Other("mint".to_string()).default_components(),
248            vec!["main"]
249        );
250    }
251}