use smol_str::SmolStr;
const OS_RELEASE_PATH: &str = "/etc/os-release";
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Identity {
pub distro_id: Option<SmolStr>,
pub distro_id_like: Vec<SmolStr>,
pub distro_version_id: Option<SmolStr>,
}
impl Identity {
pub fn detect() -> Self {
match std::fs::read_to_string(OS_RELEASE_PATH) {
Ok(body) => parse(&body),
Err(_) => Identity::default(),
}
}
}
pub(crate) fn parse(body: &str) -> Identity {
let mut id = None;
let mut id_like = Vec::new();
let mut version_id = None;
for line in body.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let Some((key, value)) = line.split_once('=') else {
continue;
};
let value = value.trim().trim_matches('"');
if value.is_empty() {
continue;
}
match key.trim() {
"ID" => id = Some(SmolStr::new(value)),
"ID_LIKE" => {
id_like = value.split_whitespace().map(SmolStr::new).collect();
}
"VERSION_ID" => version_id = Some(SmolStr::new(value)),
_ => {}
}
}
Identity {
distro_id: id,
distro_id_like: id_like,
distro_version_id: version_id,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_fedora_sample() {
let body = r#"NAME="Fedora Linux"
VERSION="42 (Cloud Edition)"
ID=fedora
VERSION_ID=42
PRETTY_NAME="Fedora Linux 42 (Cloud Edition)"
"#;
let id = parse(body);
assert_eq!(id.distro_id.as_deref(), Some("fedora"));
assert!(id.distro_id_like.is_empty());
assert_eq!(id.distro_version_id.as_deref(), Some("42"));
}
#[test]
fn parses_ubuntu_sample() {
let body = r#"PRETTY_NAME="Ubuntu 24.04.1 LTS"
NAME="Ubuntu"
VERSION_ID="24.04"
VERSION="24.04.1 LTS (Noble Numbat)"
ID=ubuntu
ID_LIKE=debian
"#;
let id = parse(body);
assert_eq!(id.distro_id.as_deref(), Some("ubuntu"));
assert_eq!(id.distro_id_like, vec![SmolStr::new("debian")]);
assert_eq!(id.distro_version_id.as_deref(), Some("24.04"));
}
#[test]
fn parses_arch_sample_no_version_id() {
let body = r#"NAME="Arch Linux"
ID=arch
BUILD_ID=rolling
"#;
let id = parse(body);
assert_eq!(id.distro_id.as_deref(), Some("arch"));
assert!(id.distro_id_like.is_empty());
assert_eq!(id.distro_version_id, None);
}
#[test]
fn parses_endeavouros_id_like_arch() {
let body = r#"NAME="EndeavourOS"
PRETTY_NAME="EndeavourOS"
ID=endeavouros
ID_LIKE=arch
BUILD_ID=rolling
"#;
let id = parse(body);
assert_eq!(id.distro_id.as_deref(), Some("endeavouros"));
assert_eq!(id.distro_id_like, vec![SmolStr::new("arch")]);
assert_eq!(id.distro_version_id, None);
}
#[test]
fn parses_popos_multi_id_like() {
let body = r#"NAME="Pop!_OS"
ID=pop
ID_LIKE="ubuntu debian"
VERSION_ID="22.04"
"#;
let id = parse(body);
assert_eq!(id.distro_id.as_deref(), Some("pop"));
assert_eq!(
id.distro_id_like,
vec![SmolStr::new("ubuntu"), SmolStr::new("debian")]
);
assert_eq!(id.distro_version_id.as_deref(), Some("22.04"));
}
#[test]
fn parses_debian_sample() {
let body = r#"PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"
NAME="Debian GNU/Linux"
VERSION_ID="12"
VERSION="12 (bookworm)"
ID=debian
"#;
let id = parse(body);
assert_eq!(id.distro_id.as_deref(), Some("debian"));
assert!(id.distro_id_like.is_empty());
assert_eq!(id.distro_version_id.as_deref(), Some("12"));
}
#[test]
fn parses_nixos_sample() {
let body = r#"NAME=NixOS
ID=nixos
VERSION="24.05 (Uakari)"
VERSION_CODENAME=uakari
VERSION_ID="24.05"
BUILD_ID="24.05.20240601.abcdef0"
"#;
let id = parse(body);
assert_eq!(id.distro_id.as_deref(), Some("nixos"));
assert!(id.distro_id_like.is_empty());
assert_eq!(id.distro_version_id.as_deref(), Some("24.05"));
}
#[test]
fn parses_opensuse_tumbleweed_with_dashed_id() {
let body = r#"NAME="openSUSE Tumbleweed"
ID="opensuse-tumbleweed"
ID_LIKE="opensuse suse"
VERSION_ID="20240601"
"#;
let id = parse(body);
assert_eq!(id.distro_id.as_deref(), Some("opensuse-tumbleweed"));
assert_eq!(
id.distro_id_like,
vec![SmolStr::new("opensuse"), SmolStr::new("suse")]
);
assert_eq!(id.distro_version_id.as_deref(), Some("20240601"));
}
#[test]
fn parses_bazzite_sample() {
let body = r#"NAME="Bazzite"
ID=bazzite
ID_LIKE="fedora"
VERSION_ID="40"
VARIANT_ID="bazzite"
"#;
let id = parse(body);
assert_eq!(id.distro_id.as_deref(), Some("bazzite"));
assert_eq!(id.distro_id_like, vec![SmolStr::new("fedora")]);
assert_eq!(id.distro_version_id.as_deref(), Some("40"));
}
#[test]
fn empty_body_returns_blank_identity() {
let id = parse("");
assert_eq!(id, Identity::default());
}
#[test]
fn unknown_keys_silently_ignored() {
let id = parse("FOO=bar\nBAZ=qux\nID=fedora\n");
assert_eq!(id.distro_id.as_deref(), Some("fedora"));
}
#[test]
fn comments_and_blank_lines_ignored() {
let body = "# a comment\n\n \nID=ubuntu\n # another\nVERSION_ID=24.04\n";
let id = parse(body);
assert_eq!(id.distro_id.as_deref(), Some("ubuntu"));
assert_eq!(id.distro_version_id.as_deref(), Some("24.04"));
}
#[test]
fn empty_string_values_treated_as_absent() {
let id = parse("ID=\"\"\nVERSION_ID=\"\"\n");
assert_eq!(id.distro_id, None);
assert_eq!(id.distro_version_id, None);
}
#[test]
fn double_quoted_values_stripped() {
let id = parse("ID=\"fedora\"\nVERSION_ID=\"42\"\n");
assert_eq!(id.distro_id.as_deref(), Some("fedora"));
assert_eq!(id.distro_version_id.as_deref(), Some("42"));
}
#[test]
fn rolling_version_id_string_kept_verbatim() {
let id = parse("ID=fedora\nVERSION_ID=rawhide\n");
assert_eq!(id.distro_version_id.as_deref(), Some("rawhide"));
}
}