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}