os_release_rs/lib.rs
1use std::collections::BTreeMap;
2use std::fs::File;
3use std::io::{self, BufRead, BufReader};
4use std::iter::FromIterator;
5use std::path::Path;
6
7/// Map keys to values.
8/// For each key in the file, add a key to the map with the value of the key.
9macro_rules! map_keys {
10 ($item:expr, { $($pat:expr => $field:expr),+ }) => {{
11 $(
12 if $item.starts_with($pat) {
13 $field = parse_line($item, $pat.len()).into();
14 continue;
15 }
16 )+
17 }};
18}
19
20fn is_enclosed_with(line: &str, pattern: char) -> bool {
21 line.starts_with(pattern) && line.ends_with(pattern)
22}
23
24/// Parse a line of the form `<key> = <value>`
25/// The key is expected to be a single word or something like MY_KEY_NAME.
26/// The line is returned as a `&str`.
27fn parse_line(line: &str, skip: usize) -> &str {
28 let line = line[skip..].trim();
29 if is_enclosed_with(line, '"') || is_enclosed_with(line, '\'') {
30 &line[1..line.len() - 1]
31 } else {
32 line
33 }
34}
35
36#[derive(Clone, Debug, Default, PartialEq)]
37pub struct OsRelease {
38 /// ANSI color code for the distribution.
39 /// This is a six numbers.
40 /// For example, on ArchLinux, this is "38;2;23;147;209.
41 pub ansi_color: String,
42 /// If the distro is a rolling release, it will be "rolling".
43 pub build_id: String,
44 /// Url of bug reporting system.
45 /// This is the URL of the bug reporting system for the distribution.
46 /// For example, on ArchLinux, this is "https://bugs.archlinux.org".
47 pub bug_report_url: String,
48 /// Url of the documentation for the distribution.
49 /// This is the URL of the documentation for the distribution.
50 /// For example, on ArchLinux, this is "https://wiki.archlinux.org".
51 /// The ArchWiki is the biggest documentation of every open source project.
52 /// This is not the same as the URL of the distribution's website.
53 pub documentation_url: String,
54 /// Extra keys will be stored in this map.
55 pub extra: BTreeMap<String, String>,
56 /// Homepage of the distribution.
57 /// This is the homepage of the distribution.
58 /// For example, on ArchLinux, this is "https://www.archlinux.org/".
59 pub home_url: String,
60 /// The name of the distribution in the form of a codename.
61 /// For example, on ArchLinux, this is "archlinux".
62 pub id: String,
63 /// Related distribution id.
64 /// If the distro is derived from another distro, it will be the id of the parent distro.
65 /// For example, on Manjaro, this is "arch".
66 pub id_like: String,
67 /// The name of the operating system.
68 /// This is the name of the operating system as it appears to the user.
69 /// For example, on ArchLinux, this is "Arch Linux".
70 pub logo: String,
71 /// Logo of the distribution.
72 /// This is the logo of the distribution.
73 /// For example, on ArchLinux, this is "archlinux-logo".
74 pub name: String,
75 /// The pretty name of the operating system.
76 pub pretty_name: String,
77 /// Privacy policy url.
78 /// This is the URL of the privacy policy of the distribution.
79 /// For example, on ArchLinux, this is "https://www.archlinux.org/legal/privacy-policy/".
80 pub privacy_policy_url: String,
81 /// The version of the distribution.
82 /// This is the version of the distribution.
83 /// For example, on ArchLinux, this is "" because ArchLinux is rolling release so ArchLinux doesn't have version.
84 pub version: String,
85 /// The version codename of the distribution.
86 /// This is the version codename of the distribution.
87 /// For example, on ArchLinux, this is "" because ArchLinux is rolling release so ArchLinux doesn't have version.
88 pub version_codename: String,
89 /// The version id of the distribution.
90 /// This is the version id of the distribution.
91 /// For example, on ArchLinux, this is "" because ArchLinux is rolling release so ArchLinux doesn't have version.
92 pub version_id: String,
93 /// The support url of the distribution.
94 /// This is the support url of the distribution.
95 /// For example, on ArchLinux, this is "https://bbs.archlinux.org/"
96 pub support_url: String,
97}
98
99impl OsRelease {
100 /// Reads the `/etc/os-release` file and returns a `OsRelease` struct.
101 /// If `/etc/os-release` does not exist, searches for `/usr/lib/os-release`
102 pub fn new() -> io::Result<OsRelease> {
103 let file = BufReader::new(open("/etc/os-release").unwrap_or(open("/usr/lib/os-release")?));
104 Ok(OsRelease::from_iter(file.lines().flat_map(|line| line)))
105 }
106
107 /// Attempt to parse any `/etc/os-release`-like file.
108 pub fn new_from<P: AsRef<Path>>(path: P) -> io::Result<OsRelease> {
109 let file = BufReader::new(open(&path)?);
110 Ok(OsRelease::from_iter(file.lines().flat_map(|line| line)))
111 }
112}
113
114impl FromIterator<String> for OsRelease {
115 /// Parse the lines of the `/etc/os-release` file.
116 /// The lines are expected to be in the form of `<key> = <value>`.
117 /// If keys aren't in the list of standard keys, there will be in `extra` field.
118 /// See the `OsRelease` struct for the list of standard keys.
119 fn from_iter<I: IntoIterator<Item = String>>(lines: I) -> Self {
120 let mut os_release = Self::default();
121
122 for line in lines {
123 let line = line.trim();
124 map_keys!(line, {
125 "ANSI_COLOR=" => os_release.ansi_color,
126 "BUILD_ID=" => os_release.build_id,
127 "BUG_REPORT_URL=" => os_release.bug_report_url,
128 "DOCUMENTATION_URL=" => os_release.documentation_url,
129 "HOME_URL=" => os_release.home_url,
130 "ID=" => os_release.id,
131 "ID_LIKE=" => os_release.id_like,
132 "LOGO=" => os_release.logo,
133 "NAME=" => os_release.name,
134 "PRETTY_NAME=" => os_release.pretty_name,
135 "PRIVACY_POLICY_URL=" => os_release.privacy_policy_url,
136 "SUPPORT_URL=" => os_release.support_url,
137 "VERSION=" => os_release.version,
138 "VERSION_ID=" => os_release.version_id,
139 "VERSION_CODENAME=" => os_release.version_codename
140 });
141
142 if let Some(pos) = line.find('=') {
143 if line.len() > pos+1 {
144 os_release.extra.insert(line[..pos].to_owned(), line[pos+1..].to_owned());
145 }
146 }
147 }
148
149 os_release
150 }
151}
152
153/// Open the file at the given path.
154/// If the file does not exist, return an error.
155fn open<P: AsRef<Path>>(path: P) -> io::Result<File> {
156 File::open(&path).map_err(|why| io::Error::new(
157 io::ErrorKind::Other,
158 format!("unable to open file at {:?}: {}", path.as_ref(), why)
159 ))
160}
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 const EXAMPLE: &str = r#"NAME="Arch Linux"
166PRETTY_NAME="Arch Linux"
167ID=arch
168BUILD_ID=rolling
169ANSI_COLOR="38;2;23;147;209"
170HOME_URL="https://archlinux.org/"
171DOCUMENTATION_URL="https://wiki.archlinux.org/"
172SUPPORT_URL="https://archlinux.org/"
173BUG_REPORT_URL="https://bugs.archlinux.org/"
174LOGO=archlinux-logo
175EXTRA_KEY=thing"#;
176
177 #[test]
178 fn os_release() {
179 let os_release = OsRelease::from_iter(EXAMPLE.lines().map(|x| x.into()));
180
181 assert_eq!(
182 os_release,
183 OsRelease {
184 name: "Arch Linux".into(),
185 pretty_name: "Arch Linux".into(),
186 version: "".into(),
187 id: "arch".into(),
188 id_like: "".into(),
189 version_id: "".into(),
190 home_url: "https://archlinux.org/".into(),
191 support_url: "https://archlinux.org/".into(),
192 bug_report_url: "https://bugs.archlinux.org/".into(),
193 privacy_policy_url: "".into(),
194 version_codename: "".into(),
195 logo: "archlinux-logo".into(),
196 build_id: "rolling".into(),
197 ansi_color: "38;2;23;147;209".into(),
198 documentation_url: "https://wiki.archlinux.org/".into(),
199 extra: {
200 let mut map = BTreeMap::new();
201 map.insert("EXTRA_KEY".to_owned(), "thing".to_owned());
202 map
203 }
204 }
205 )
206 }
207}