Skip to main content

apt_sources/
distribution.rs

1use std::fs;
2
3/// Represents a Linux distribution
4#[derive(Debug, Clone, PartialEq)]
5pub enum Distribution {
6    /// Ubuntu Linux
7    Ubuntu,
8    /// Debian Linux
9    Debian,
10    /// Other distribution
11    Other(String),
12}
13
14impl std::fmt::Display for Distribution {
15    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
16        match self {
17            Distribution::Ubuntu => write!(f, "Ubuntu"),
18            Distribution::Debian => write!(f, "Debian"),
19            Distribution::Other(name) => write!(f, "{}", name),
20        }
21    }
22}
23
24impl Distribution {
25    /// Get the current system's distribution information
26    ///
27    /// Returns None if /etc/os-release is not present or cannot be parsed.
28    pub fn current() -> Option<Distribution> {
29        let content = fs::read_to_string("/etc/os-release").ok()?;
30
31        for line in content.lines() {
32            if line.starts_with("ID=") {
33                let id = line
34                    .trim_start_matches("ID=")
35                    .trim_matches('"')
36                    .to_lowercase();
37
38                return Some(match id.as_str() {
39                    "ubuntu" => Distribution::Ubuntu,
40                    "debian" => Distribution::Debian,
41                    other => Distribution::Other(other.to_owned()),
42                });
43            }
44        }
45
46        None
47    }
48
49    /// Get default components for this distribution
50    pub fn default_components(&self) -> Vec<&'static str> {
51        match self {
52            Distribution::Ubuntu => vec!["main", "universe"],
53            Distribution::Debian => vec!["main"],
54            Distribution::Other(_) => vec!["main"],
55        }
56    }
57
58    /// Get the base name for the main sources file for this distribution
59    ///
60    /// For example, returns "ubuntu" for Ubuntu, "debian" for Debian.
61    /// This can be used to construct filenames like "ubuntu.sources".
62    pub fn sources_basename(&self) -> Option<&'static str> {
63        match self {
64            Distribution::Ubuntu => Some("ubuntu"),
65            Distribution::Debian => Some("debian"),
66            Distribution::Other(_) => None,
67        }
68    }
69
70    /// Check if a repository is a main distribution repository
71    pub fn is_main_repository(&self, repo: &crate::Repository) -> bool {
72        for uri in &repo.uris {
73            if let Some(host) = uri.host_str() {
74                match self {
75                    Distribution::Ubuntu => {
76                        if host.contains("ubuntu.com")
77                            || host.contains("canonical.com")
78                            || host == "archive.ubuntu.com"
79                            || host == "security.ubuntu.com"
80                            || host == "ports.ubuntu.com"
81                        {
82                            return true;
83                        }
84                    }
85                    Distribution::Debian => {
86                        if host.contains("debian.org")
87                            || host == "deb.debian.org"
88                            || host == "security.debian.org"
89                        {
90                            return true;
91                        }
92                    }
93                    _ => {}
94                }
95            }
96        }
97        false
98    }
99}
100
101/// Get system codename from /etc/os-release
102///
103/// Returns None if /etc/os-release is not present or doesn't contain VERSION_CODENAME.
104/// The architecture is always an empty string (APT will use the system's native architecture).
105pub fn get_system_info() -> Option<(String, String)> {
106    let content = fs::read_to_string("/etc/os-release").ok()?;
107
108    let codename = content
109        .lines()
110        .find(|line| line.starts_with("VERSION_CODENAME="))
111        .map(|line| {
112            line.trim_start_matches("VERSION_CODENAME=")
113                .trim_matches('"')
114                .to_string()
115        })?;
116
117    // Return empty string for architecture - APT will use the system's native architecture
118    // when the Architectures field is omitted from the sources file
119    Some((codename, String::new()))
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn test_is_main_repository() {
128        let dist = Distribution::Ubuntu;
129        let repo = crate::Repository {
130            uris: vec![url::Url::parse("http://archive.ubuntu.com/ubuntu").unwrap()],
131            suites: vec!["jammy".to_string()],
132            components: Some(vec!["main".to_string()]),
133            ..Default::default()
134        };
135        assert!(dist.is_main_repository(&repo));
136
137        let non_main_repo = crate::Repository {
138            uris: vec![url::Url::parse("http://example.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(&non_main_repo));
144    }
145
146    #[test]
147    fn test_is_main_repository_all_ubuntu_hosts() {
148        let dist = Distribution::Ubuntu;
149
150        // Test each Ubuntu host individually
151        let ubuntu_hosts = [
152            "http://archive.ubuntu.com/ubuntu",
153            "http://security.ubuntu.com/ubuntu",
154            "http://ports.ubuntu.com/ubuntu-ports",
155            "http://us.archive.ubuntu.com/ubuntu", // contains ubuntu.com
156            "http://mirrors.canonical.com/ubuntu", // contains canonical.com
157        ];
158
159        for host in &ubuntu_hosts {
160            let repo = crate::Repository {
161                uris: vec![url::Url::parse(host).unwrap()],
162                ..Default::default()
163            };
164            assert!(dist.is_main_repository(&repo), "Failed for host: {}", host);
165        }
166    }
167
168    #[test]
169    fn test_is_main_repository_all_debian_hosts() {
170        let dist = Distribution::Debian;
171
172        // Test each Debian host individually
173        let debian_hosts = [
174            "http://deb.debian.org/debian",
175            "http://security.debian.org/debian-security",
176            "http://ftp.debian.org/debian",     // contains debian.org
177            "http://mirrors.debian.org/debian", // contains debian.org
178        ];
179
180        for host in &debian_hosts {
181            let repo = crate::Repository {
182                uris: vec![url::Url::parse(host).unwrap()],
183                ..Default::default()
184            };
185            assert!(dist.is_main_repository(&repo), "Failed for host: {}", host);
186        }
187    }
188
189    #[test]
190    fn test_is_main_repository_other_distribution() {
191        let dist = Distribution::Other("mint".to_string());
192
193        // Other distributions should not match any repository
194        let repo = crate::Repository {
195            uris: vec![url::Url::parse("http://archive.ubuntu.com/ubuntu").unwrap()],
196            ..Default::default()
197        };
198        assert!(!dist.is_main_repository(&repo));
199
200        let repo2 = crate::Repository {
201            uris: vec![url::Url::parse("http://deb.debian.org/debian").unwrap()],
202            ..Default::default()
203        };
204        assert!(!dist.is_main_repository(&repo2));
205    }
206
207    #[test]
208    fn test_is_main_repository_empty_uris() {
209        let dist = Distribution::Ubuntu;
210        let repo = crate::Repository {
211            uris: vec![],
212            ..Default::default()
213        };
214        assert!(!dist.is_main_repository(&repo));
215    }
216
217    #[test]
218    fn test_is_main_repository_multiple_uris() {
219        let dist = Distribution::Ubuntu;
220        let repo = crate::Repository {
221            uris: vec![
222                url::Url::parse("http://example.com/ubuntu").unwrap(),
223                url::Url::parse("http://archive.ubuntu.com/ubuntu").unwrap(),
224            ],
225            ..Default::default()
226        };
227        // Should return true if ANY URI matches
228        assert!(dist.is_main_repository(&repo));
229    }
230
231    #[test]
232    fn test_default_components() {
233        assert_eq!(
234            Distribution::Ubuntu.default_components(),
235            vec!["main", "universe"]
236        );
237        assert_eq!(Distribution::Debian.default_components(), vec!["main"]);
238        assert_eq!(
239            Distribution::Other("mint".to_string()).default_components(),
240            vec!["main"]
241        );
242    }
243}