rs_release/
lib.rs

1//! os-release parser
2//!
3//! # Usage example
4//!
5//! ```
6//! use rs_release::parse_os_release;
7//!
8//! let os_release_path = "/etc/os-release";
9//! if let Ok(os_release) = parse_os_release(os_release_path) {
10//!     println!("Parsed os-release:");
11//!     for (k, v) in os_release {
12//!         println!("{}={}", k, v);
13//!     }
14//! } else {
15//!     eprintln!("Cannot parse {}", os_release_path);
16//! }
17//! ```
18#![deny(missing_docs)]
19
20use std::borrow::Cow;
21use std::collections::HashMap;
22use std::convert::From;
23use std::error::Error;
24use std::fmt;
25use std::fs::File;
26use std::io::{BufRead, BufReader};
27use std::path::Path;
28
29const PATHS: [&str; 2] = ["/etc/os-release", "/usr/lib/os-release"];
30const QUOTES: [&str; 2] = ["\"", "'"];
31
32const COMMON_KEYS: [&str; 33] = [
33    "ANSI_COLOR",
34    "ARCHITECTURE",
35    "BUG_REPORT_URL",
36    "BUILD_ID",
37    "CONFEXT_LEVEL",
38    "CONFEXT_SCOPE",
39    "CPE_NAME",
40    "DEFAULT_HOSTNAME",
41    "DOCUMENTATION_URL",
42    "EXPERIMENT",
43    "EXPERIMENT_URL",
44    "HOME_URL",
45    "ID",
46    "ID_LIKE",
47    "IMAGE_ID",
48    "IMAGE_VERSION",
49    "LOGO",
50    "NAME",
51    "PORTABLE_PREFIXES",
52    "PRETTY_NAME",
53    "PRIVACY_POLICY_URL",
54    "RELEASE_TYPE",
55    "SUPPORT_END",
56    "SUPPORT_URL",
57    "SYSEXT_LEVEL",
58    "SYSEXT_SCOPE",
59    "VARIANT",
60    "VARIANT_ID",
61    "VENDOR_NAME",
62    "VENDOR_URL",
63    "VERSION",
64    "VERSION_CODENAME",
65    "VERSION_ID",
66];
67
68/// Represents possible errors when parsing os-release file/string
69#[derive(Debug)]
70pub enum OsReleaseError {
71    /// Input-Output error (failed to read file)
72    Io(std::io::Error),
73    /// Failed to find os-release file in standard paths
74    NoFile,
75    /// File is malformed
76    ParseError,
77}
78
79impl PartialEq for OsReleaseError {
80    fn eq(&self, other: &Self) -> bool {
81        matches!(
82            (self, other),
83            (&Self::Io(_), &Self::Io(_))
84                | (&Self::NoFile, &Self::NoFile)
85                | (&Self::ParseError, &Self::ParseError)
86        )
87    }
88}
89
90impl fmt::Display for OsReleaseError {
91    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
92        match *self {
93            Self::Io(ref inner) => inner.fmt(fmt),
94            Self::NoFile => write!(fmt, "Failed to find os-release file"),
95            Self::ParseError => write!(fmt, "File is malformed"),
96        }
97    }
98}
99
100impl Error for OsReleaseError {
101    fn cause(&self) -> Option<&dyn Error> {
102        match *self {
103            Self::Io(ref err) => Some(err),
104            Self::NoFile | Self::ParseError => None,
105        }
106    }
107}
108
109impl From<std::io::Error> for OsReleaseError {
110    fn from(err: std::io::Error) -> Self {
111        Self::Io(err)
112    }
113}
114
115/// A specialized `Result` type for os-release parsing operations.
116pub type Result<T> = std::result::Result<T, OsReleaseError>;
117
118fn trim_quotes(s: &str) -> &str {
119    // TODO: is it malformed if we have only one quote?
120    if QUOTES.iter().any(|q| s.starts_with(q) && s.ends_with(q)) {
121        &s[1..s.len() - 1]
122    } else {
123        s
124    }
125}
126
127fn extract_variable_and_value(s: &str) -> Result<(Cow<'static, str>, String)> {
128    s.chars().position(|c| c == '=').map_or_else(
129        || Err(OsReleaseError::ParseError),
130        |equal| {
131            let variable = &s[..equal];
132            let variable = variable.trim();
133            let value = &s[equal + 1..];
134            let value = trim_quotes(value.trim()).to_string();
135
136            if let Ok(index) = COMMON_KEYS.binary_search(&variable) {
137                Ok((Cow::Borrowed(COMMON_KEYS[index]), value))
138            } else {
139                Ok((Cow::Owned(variable.to_string()), value))
140            }
141        },
142    )
143}
144
145fn parse_impl<S, L>(lines: L) -> Result<HashMap<Cow<'static, str>, String>>
146where
147    S: AsRef<str>,
148    L: Iterator<Item = S>,
149{
150    let mut os_release = HashMap::new();
151    for line in lines {
152        let line = line.as_ref().trim();
153
154        if line.starts_with('#') || line.is_empty() {
155            continue;
156        }
157        let var_val = extract_variable_and_value(line)?;
158        os_release.insert(var_val.0, var_val.1);
159    }
160    Ok(os_release)
161}
162
163/// Mapping of os-release variable names to values
164type OsReleaseVariables = HashMap<Cow<'static, str>, String>;
165
166/// Parses key-value pairs from `path`
167pub fn parse_os_release<P: AsRef<Path>>(path: P) -> Result<OsReleaseVariables> {
168    let file = File::open(path)?;
169    let reader = BufReader::new(file);
170    parse_impl(reader.lines().map(std::result::Result::unwrap_or_default))
171}
172
173/// Parses key-value pairs from `data` string
174pub fn parse_os_release_str(data: &str) -> Result<OsReleaseVariables> {
175    parse_impl(data.split('\n'))
176}
177
178/// Tries to find and parse os-release in common paths. Stops on success.
179pub fn get_os_release() -> Result<OsReleaseVariables> {
180    for file in &PATHS {
181        if let Ok(os_release) = parse_os_release(file) {
182            return Ok(os_release);
183        }
184    }
185    Err(OsReleaseError::NoFile)
186}