2023-01-01 18:54:57 -06:00
|
|
|
use std::fmt;
|
2023-09-14 04:12:34 -05:00
|
|
|
use std::{collections::BTreeMap, str::FromStr};
|
2023-01-01 18:54:57 -06:00
|
|
|
|
|
|
|
use anyhow::{Context, Result};
|
|
|
|
|
2023-07-22 13:49:46 -05:00
|
|
|
use crate::generation::Generation;
|
2023-01-01 18:54:57 -06:00
|
|
|
|
|
|
|
/// An os-release file represented by a BTreeMap.
|
|
|
|
///
|
|
|
|
/// This is implemented using a map, so that it can be easily extended in the future (e.g. by
|
|
|
|
/// reading the original os-release and patching it).
|
|
|
|
///
|
|
|
|
/// The BTreeMap is used over a HashMap, so that the keys are ordered. This is irrelevant for
|
|
|
|
/// systemd-boot (which does not care about order when reading the os-release file) but is useful
|
|
|
|
/// for testing. Ordered keys allow using snapshot tests.
|
2023-01-17 18:58:45 -06:00
|
|
|
pub struct OsRelease(pub BTreeMap<String, String>);
|
2023-01-01 18:54:57 -06:00
|
|
|
|
|
|
|
impl OsRelease {
|
|
|
|
pub fn from_generation(generation: &Generation) -> Result<Self> {
|
|
|
|
let mut map = BTreeMap::new();
|
|
|
|
|
|
|
|
// Because of a null pointer dereference, `bootctl` segfaults when no ID field is present
|
|
|
|
// in the .osrel section of the stub.
|
|
|
|
// Fixed in https://github.com/systemd/systemd/pull/25953
|
|
|
|
//
|
|
|
|
// Because the ID field here does not have the same meaning as in a real os-release file,
|
|
|
|
// it is fine to use a dummy value.
|
2023-01-17 18:58:45 -06:00
|
|
|
map.insert("ID".into(), String::from("lanza"));
|
2023-05-17 14:40:03 -05:00
|
|
|
map.insert(
|
|
|
|
"PRETTY_NAME".into(),
|
|
|
|
generation.spec.bootspec.bootspec.label.clone(),
|
|
|
|
);
|
2023-02-08 17:19:33 -06:00
|
|
|
map.insert("VERSION_ID".into(), generation.describe());
|
2023-01-01 18:54:57 -06:00
|
|
|
|
|
|
|
Ok(Self(map))
|
|
|
|
}
|
2023-09-14 04:12:34 -05:00
|
|
|
}
|
2023-01-17 18:58:45 -06:00
|
|
|
|
2023-09-14 04:12:34 -05:00
|
|
|
impl FromStr for OsRelease {
|
|
|
|
type Err = anyhow::Error;
|
2023-01-17 18:58:45 -06:00
|
|
|
/// Parse the string representation of a os-release file.
|
|
|
|
///
|
|
|
|
/// **Beware before reusing this function!**
|
|
|
|
///
|
|
|
|
/// This parser might not parse all valid os-release files correctly. It is only designed to
|
|
|
|
/// read the `VERSION` key from the os-release of a systemd-boot binary.
|
2023-09-14 04:12:34 -05:00
|
|
|
fn from_str(value: &str) -> Result<Self> {
|
2023-01-17 18:58:45 -06:00
|
|
|
let mut map = BTreeMap::new();
|
|
|
|
|
2023-01-30 04:46:48 -06:00
|
|
|
// All valid lines
|
|
|
|
let lines = value
|
|
|
|
.lines()
|
|
|
|
.map(str::trim)
|
|
|
|
.filter(|x| !x.starts_with('#') && !x.is_empty());
|
|
|
|
// Split into keys/values
|
|
|
|
let key_value_lines = lines.map(|x| x.split('=').collect::<Vec<&str>>());
|
2023-01-17 18:58:45 -06:00
|
|
|
for kv in key_value_lines {
|
|
|
|
let k = kv
|
|
|
|
.first()
|
|
|
|
.with_context(|| format!("Failed to get first element from {kv:?}"))?;
|
|
|
|
let v = kv
|
|
|
|
.get(1)
|
2023-01-30 04:46:48 -06:00
|
|
|
.map(|s| s.strip_prefix(|c| c == '"' || c == '\'').unwrap_or(s))
|
|
|
|
.map(|s| s.strip_suffix(|c| c == '"' || c == '\'').unwrap_or(s))
|
2023-01-17 18:58:45 -06:00
|
|
|
.with_context(|| format!("Failed to get second element from {kv:?}"))?;
|
2023-01-30 04:46:48 -06:00
|
|
|
// Clean up the value. We already have the value without leading/tailing "
|
|
|
|
// so we just need to unescape the string.
|
|
|
|
let v = v
|
|
|
|
.replace("\\$", "$")
|
|
|
|
.replace("\\\"", "\"")
|
|
|
|
.replace("\\`", "`")
|
|
|
|
.replace("\\\\", "\\");
|
|
|
|
|
|
|
|
map.insert(String::from(*k), v);
|
2023-01-17 18:58:45 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
Ok(Self(map))
|
|
|
|
}
|
2023-01-01 18:54:57 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Display OsRelease in the format of an os-release file.
|
|
|
|
impl fmt::Display for OsRelease {
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
|
|
for (key, value) in &self.0 {
|
|
|
|
writeln!(f, "{}={}", key, value)?
|
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|
2023-01-17 18:58:45 -06:00
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
use std::ffi::CStr;
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn parses_correctly_from_str() -> Result<()> {
|
|
|
|
let os_release_cstr = CStr::from_bytes_with_nul(b"ID=systemd-boot\nVERSION=\"252.1\"\n\0")?;
|
|
|
|
let os_release_str = os_release_cstr.to_str()?;
|
|
|
|
let os_release = OsRelease::from_str(os_release_str)?;
|
|
|
|
|
|
|
|
assert!(os_release.0["ID"] == "systemd-boot");
|
|
|
|
assert!(os_release.0["VERSION"] == "252.1");
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
2023-01-30 04:46:48 -06:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn escaping_works() -> Result<()> {
|
|
|
|
let teststring = r#"
|
|
|
|
NO_QUOTES=systemd-boot
|
|
|
|
DOUBLE_QUOTES="systemd-boot"
|
|
|
|
SINGLE_QUOTES='systemd-boot'
|
|
|
|
UNESCAPED_DOLLAR=$1.2
|
|
|
|
ESCAPED_DOLLAR=\$1.2
|
|
|
|
UNESCAPED_BACKTICK=`1.2
|
|
|
|
ESCAPED_BACKTICK=\`1.2
|
|
|
|
UNESCAPED_QUOTE=""1.2"
|
|
|
|
ESCAPED_QUOTE=\"1.2
|
|
|
|
"#;
|
|
|
|
let os_release = OsRelease::from_str(teststring)?;
|
|
|
|
|
|
|
|
assert!(os_release.0["NO_QUOTES"] == "systemd-boot");
|
|
|
|
assert!(os_release.0["DOUBLE_QUOTES"] == "systemd-boot");
|
|
|
|
assert!(os_release.0["SINGLE_QUOTES"] == "systemd-boot");
|
|
|
|
assert!(os_release.0["UNESCAPED_DOLLAR"] == "$1.2");
|
|
|
|
assert!(os_release.0["ESCAPED_DOLLAR"] == "$1.2");
|
|
|
|
assert!(os_release.0["UNESCAPED_BACKTICK"] == "`1.2");
|
|
|
|
assert!(os_release.0["ESCAPED_BACKTICK"] == "`1.2");
|
|
|
|
assert!(os_release.0["UNESCAPED_QUOTE"] == "\"1.2");
|
|
|
|
assert!(os_release.0["ESCAPED_QUOTE"] == "\"1.2");
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
2023-01-17 18:58:45 -06:00
|
|
|
}
|