os_release/
lib.rs

1//! Type for parsing the `/etc/os-release` file.
2
3#[macro_use]
4extern crate lazy_static;
5
6use std::collections::BTreeMap;
7use std::fs::File;
8use std::io::{self, BufRead, BufReader};
9use std::iter::FromIterator;
10use std::path::Path;
11
12lazy_static! {
13    /// The OS release detected on this host's environment.
14    ///
15    /// # Notes
16    /// If an OS Release was not found, an error will be in its place.
17    pub static ref OS_RELEASE: io::Result<OsRelease> = OsRelease::new();
18}
19
20macro_rules! map_keys {
21    ($item:expr, { $($pat:expr => $field:expr),+ }) => {{
22        $(
23            if $item.starts_with($pat) {
24                $field = parse_line($item, $pat.len()).into();
25                continue;
26            }
27        )+
28    }};
29}
30
31fn is_enclosed_with(line: &str, pattern: char) -> bool {
32    line.starts_with(pattern) && line.ends_with(pattern)
33}
34
35fn parse_line(line: &str, skip: usize) -> &str {
36    let line = line[skip..].trim();
37    if is_enclosed_with(line, '"') || is_enclosed_with(line, '\'') {
38        &line[1..line.len() - 1]
39    } else {
40        line
41    }
42}
43
44/// Contents of the `/etc/os-release` file, as a data structure.
45#[derive(Clone, Debug, Default, PartialEq)]
46pub struct OsRelease {
47    /// The URL where bugs should be reported for this OS.
48    pub bug_report_url:     String,
49    /// The homepage of this OS.
50    pub home_url:           String,
51    /// Identifier of the original upstream OS that this release is a derivative of.
52    ///
53    /// **IE:** `debian`
54    pub id_like:            String,
55    /// An identifier which describes this release, such as `ubuntu`.
56    ///
57    /// **IE:** `ubuntu`
58    pub id:                 String,
59    /// The name of this release, without the version string.
60    ///
61    /// **IE:** `Ubuntu`
62    pub name:               String,
63    /// The name of this release, with th eversion stirng.
64    ///
65    /// **IE:** `Ubuntu 18.04 LTS`
66    pub pretty_name:        String,
67    /// The URL describing this OS's privacy policy.
68    pub privacy_policy_url: String,
69    /// The URL for seeking support with this OS release.
70    pub support_url:        String,
71    /// The codename of this version.
72    ///
73    /// **IE:** `bionic`
74    pub version_codename:   String,
75    /// The version of this OS release, with additional details about the release.
76    ///
77    /// **IE:** `18.04 LTS (Bionic Beaver)`
78    pub version_id:         String,
79    /// The version of this OS release.
80    ///
81    /// **IE:** `18.04`
82    pub version:            String,
83    /// Additional keys not covered by the API.
84    pub extra:              BTreeMap<String, String>
85}
86
87impl OsRelease {
88    /// Attempt to parse the contents of `/etc/os-release`.
89    pub fn new() -> io::Result<OsRelease> {
90        let file = BufReader::new(open("/etc/os-release")?);
91        Ok(OsRelease::from_iter(file.lines().flat_map(|line| line)))
92    }
93
94    /// Attempt to parse any `/etc/os-release`-like file.
95    pub fn new_from<P: AsRef<Path>>(path: P) -> io::Result<OsRelease> {
96        let file = BufReader::new(open(&path)?);
97        Ok(OsRelease::from_iter(file.lines().flat_map(|line| line)))
98    }
99}
100
101impl FromIterator<String> for OsRelease {
102    fn from_iter<I: IntoIterator<Item = String>>(lines: I) -> Self {
103        let mut os_release = Self::default();
104
105        for line in lines {
106            let line = line.trim();
107            map_keys!(line, {
108                "NAME=" => os_release.name,
109                "VERSION=" => os_release.version,
110                "ID=" => os_release.id,
111                "ID_LIKE=" => os_release.id_like,
112                "PRETTY_NAME=" => os_release.pretty_name,
113                "VERSION_ID=" => os_release.version_id,
114                "HOME_URL=" => os_release.home_url,
115                "SUPPORT_URL=" => os_release.support_url,
116                "BUG_REPORT_URL=" => os_release.bug_report_url,
117                "PRIVACY_POLICY_URL=" => os_release.privacy_policy_url,
118                "VERSION_CODENAME=" => os_release.version_codename
119            });
120
121            if let Some(pos) = line.find('=') {
122                if line.len() > pos+1 {
123                    os_release.extra.insert(line[..pos].to_owned(), line[pos+1..].to_owned());
124                }
125            }
126        }
127
128        os_release
129    }
130}
131
132fn open<P: AsRef<Path>>(path: P) -> io::Result<File> {
133    File::open(&path).map_err(|why| io::Error::new(
134        io::ErrorKind::Other,
135        format!("unable to open file at {:?}: {}", path.as_ref(), why)
136    ))
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    const EXAMPLE: &str = r#"NAME="Pop!_OS"
144VERSION="18.04 LTS"
145ID=ubuntu
146ID_LIKE=debian
147PRETTY_NAME="Pop!_OS 18.04 LTS"
148VERSION_ID="18.04"
149HOME_URL="https://system76.com/pop"
150SUPPORT_URL="http://support.system76.com"
151BUG_REPORT_URL="https://github.com/pop-os/pop/issues"
152PRIVACY_POLICY_URL="https://system76.com/privacy"
153VERSION_CODENAME=bionic
154EXTRA_KEY=thing
155ANOTHER_KEY="#;
156
157    #[test]
158    fn os_release() {
159        let os_release = OsRelease::from_iter(EXAMPLE.lines().map(|x| x.into()));
160
161        assert_eq!(
162            os_release,
163            OsRelease {
164                name:               "Pop!_OS".into(),
165                version:            "18.04 LTS".into(),
166                id:                 "ubuntu".into(),
167                id_like:            "debian".into(),
168                pretty_name:        "Pop!_OS 18.04 LTS".into(),
169                version_id:         "18.04".into(),
170                home_url:           "https://system76.com/pop".into(),
171                support_url:        "http://support.system76.com".into(),
172                bug_report_url:     "https://github.com/pop-os/pop/issues".into(),
173                privacy_policy_url: "https://system76.com/privacy".into(),
174                version_codename:   "bionic".into(),
175                extra: {
176                    let mut map = BTreeMap::new();
177                    map.insert("EXTRA_KEY".to_owned(), "thing".to_owned());
178                    map
179                }
180            }
181        )
182    }
183}