use std::collections::HashMap;
fn dequote(value: &str) -> Option<String> {
let mut result = String::new();
let mut iter = value.trim().chars();
while let Some(c) = iter.next() {
match c {
'"' => loop {
result.push(match iter.next()? {
'\\' => iter.next()?,
'"' => break,
other => other,
});
},
'\'' => loop {
result.push(match iter.next()? {
'\'' => break,
other => other,
});
},
other => result.push(other),
}
}
Some(result)
}
#[derive(Debug)]
pub struct OsReleaseInfo<'a> {
map: HashMap<&'a str, &'a str>,
}
impl<'a> OsReleaseInfo<'a> {
pub fn parse(content: &'a str) -> Self {
let map = HashMap::from_iter(
content
.lines()
.filter(|line| !line.trim().starts_with('#'))
.filter_map(|line| line.split_once('=')),
);
Self { map }
}
pub fn get_value(&self, keys: &[&str]) -> Option<String> {
keys.iter()
.find_map(|key| self.map.get(key).and_then(|v| dequote(v)))
}
pub fn get_pretty_name(&self) -> Option<String> {
self.get_value(&["PRETTY_NAME", "NAME", "ID"])
}
pub fn get_version(&self) -> Option<String> {
self.get_value(&["VERSION_ID", "VERSION"])
}
pub fn get_boot_label(&self) -> Option<String> {
let mut result = self.get_pretty_name()?;
if let Some(version) = self.get_version() {
result.push_str(&format!(" {version}"));
}
Some(result)
}
}
#[cfg(test)]
mod test {
use similar_asserts::assert_eq;
use super::*;
#[test]
fn test_dequote() {
let cases = r##"
We encode the testcases inside of a custom string format to give
us more flexibility and less visual noise. Lines with 4 pipes
are successful testcases (left is quoted, right is unquoted):
|"example"| |example|
and lines with 2 pipes are failing testcases:
|"broken example|
Lines with no pipes are ignored as comments. Now, the cases:
|| || Empty is empty...
|""| ||
|''| ||
|""''""| ||
Unquoted stuff
|hello| |hello|
|1234| |1234|
|\\\\| |\\\\| ...this is non-POSIX...
|\$\`\\| |\$\`\\| ...this too...
Double quotes
|"closed"| |closed|
|"closed\\"| |closed\|
|"a"| |a|
|" "| | |
|"\""| |"|
|"\\"| |\|
|"\$5"| |$5|
|"$5"| |$5| non-POSIX
|"\`tick\`"| |`tick`|
|"`tick`"| |`tick`| non-POSIX
|"\'"| |'| non-POSIX
|"\'"| |'| non-POSIX
...failures...
|"not closed|
|"not closed\"|
|"|
|"\\|
|"\"|
Single quotes
|'a'| |a|
|' '| | |
|'\'| |\|
|'\$'| |\$|
|'closed\'| |closed\|
...failures...
|'| not closed
|'not closed|
|'\''| this is '\' + a second unclosed quote '
"##;
for case in cases.lines() {
match case.split('|').collect::<Vec<&str>>()[..] {
[_comment] => {}
[_, quoted, _, result, _] => assert_eq!(dequote(quoted).as_deref(), Some(result)),
[_, quoted, _] => assert_eq!(dequote(quoted), None),
_ => unreachable!("Invalid test line {case:?}"),
}
}
}
#[test]
fn test_fallbacks() {
let cases = [
(
r#"
PRETTY_NAME='prettyOS'
VERSION_ID="Rocky Racoon"
VERSION=42
ID=pretty-os
"#,
"prettyOS Rocky Racoon",
),
(
r#"
PRETTY_NAME='prettyOS
VERSION_ID="Rocky Racoon"
VERSION=42
ID=pretty-os
"#,
"pretty-os Rocky Racoon",
),
(
r#"
PRETTY_NAME='prettyOS
VERSION=42
ID=pretty-os
"#,
"pretty-os 42",
),
(
r#"
PRETTY_NAME='prettyOS
VERSION=42
ID=pretty-os
"#,
"pretty-os 42",
),
(
r#"
PRETTY_NAME='prettyOS'
ID=pretty-os
"#,
"prettyOS",
),
(
r#"
ID=pretty-os
"#,
"pretty-os",
),
];
for (osrel, label) in cases {
let info = OsReleaseInfo::parse(osrel);
assert_eq!(info.get_boot_label().unwrap(), label);
}
}
}