1#[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 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#[derive(Clone, Debug, Default, PartialEq)]
46pub struct OsRelease {
47 pub bug_report_url: String,
49 pub home_url: String,
51 pub id_like: String,
55 pub id: String,
59 pub name: String,
63 pub pretty_name: String,
67 pub privacy_policy_url: String,
69 pub support_url: String,
71 pub version_codename: String,
75 pub version_id: String,
79 pub version: String,
83 pub extra: BTreeMap<String, String>
85}
86
87impl OsRelease {
88 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 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}