Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/Skidbladnir

src/hostfamily.rs

//! **What KIND of host is this** — the distro/init/logind concept skidbladnir did
//! not have, and the reason [`crate::rootless`] can refuse instead of guess.
//!
//! # Why this exists
//!
//! Until now the crate modelled the *supervisor* ([`crate::service::Backend`] —
//! `SystemdSystem` / `SystemdUser` / `OpenRc` / `Kubernetes`) and nothing else.
//! [`crate::platform::Platform`] looks like a distro type but is not one: it has
//! three values (`Rhel` / `Debian` / `Other`), exists solely to decide whether the
//! SELinux toggle applies, and puts **Alpine in `Other`** — which is precisely the
//! host where the interesting difference lives. `matrix.rs`'s `OsImage.name` is a
//! free string used for reporting.
//!
//! So nothing in the crate could answer: which package supplies `newuidmap`, is
//! there a logind on this box at all, is `/etc` writable on this distro. Those are
//! the questions a rootless-container host setup is made of.
//!
//! # The design decision that matters: family does NOT determine init
//!
//! The tempting model is one enum where `Debian ⇒ systemd` and `Alpine ⇒ OpenRC`.
//! It is wrong, and wrong in ways that ship:
//!
//! * **Devuan** is `ID_LIKE=debian` and runs **sysvinit** — a systemd assumption
//!   derived from the family is simply false there.
//! * **Gentoo** ships an OpenRC profile *and* a systemd profile from the same `ID`.
//! * **Alpine** has no logind by default, but `elogind` is a real, packaged,
//!   in-repo option that implements the very `org.freedesktop.login1` interface
//!   lingering is set through — so "Alpine cannot linger" is false whenever
//!   somebody installed it.
//!
//! Therefore: **[`Family`] is parsed from `/etc/os-release` and says what to
//! INSTALL; [`Init`] and [`Logind`] are PROBED from the running host and say what
//! is actually there.** A family's [`Family::expected_init`] is documentation, not
//! authority — nothing in [`crate::rootless`] branches on it.
//!
//! # Everything here is pure
//!
//! [`HostFacts`] is a plain struct of strings and bools. [`HostFacts::probe`] is
//! the only function that touches the filesystem; every classification below is a
//! total function of the facts, so the whole family/init/logind matrix is
//! unit-testable against fixtures without booting a single VM. (`matrix.rs` is
//! where a real multi-OS boot would prove the probe itself — that is a separate,
//! much more expensive assertion.)

use std::path::Path;

// ---------------------------------------------------------------------------
// Package managers
// ---------------------------------------------------------------------------

/// The package manager a family installs with.
///
/// Carried as a TYPE rather than a string because the one thing skidbladnir
/// legitimately needs it for is naming the package that supplies a missing
/// prerequisite in an error the operator can act on. It is deliberately NOT a
/// driver: this crate does not install packages (that is a host decision, and
/// every one of these is a subprocess under the zero-shell law).
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum PackageManager {
    /// Debian family — `apt-get install <pkg>`.
    Apt,
    /// RHEL family — `dnf install <pkg>` (`yum` on EL7 and older).
    Dnf,
    /// Alpine — `apk add <pkg>`.
    Apk,
    /// SUSE family — `zypper install <pkg>`.
    Zypper,
    /// Arch family — `pacman -S <pkg>`.
    Pacman,
    /// Gentoo — `emerge <pkg>`.
    Portage,
    /// Void — `xbps-install <pkg>`.
    Xbps,
    /// NixOS — no imperative install; the package is declared in the system
    /// configuration and materialised by `nixos-rebuild`.
    Nix,
}

impl PackageManager {
    /// The command an operator would type. Returned as text to PRINT, never to run.
    pub fn install_command(self, pkg: &str) -> String {
        match self {
            PackageManager::Apt => format!("apt-get install -y {pkg}"),
            PackageManager::Dnf => format!("dnf install -y {pkg}"),
            PackageManager::Apk => format!("apk add {pkg}"),
            PackageManager::Zypper => format!("zypper install -y {pkg}"),
            PackageManager::Pacman => format!("pacman -S --noconfirm {pkg}"),
            PackageManager::Portage => format!("emerge {pkg}"),
            PackageManager::Xbps => format!("xbps-install -y {pkg}"),
            PackageManager::Nix => format!(
                "add `{pkg}` to environment.systemPackages and run nixos-rebuild switch"
            ),
        }
    }
}

// ---------------------------------------------------------------------------
// The family
// ---------------------------------------------------------------------------

/// A distro **family** — the packaging lineage, parsed from `/etc/os-release`.
///
/// The roster is not "the four the owner named plus whatever": each member earns its
/// place by differing on a fact [`crate::rootless`] actually reads.
///
/// | family | packages | `newuidmap` from | why it is a separate arm |
/// |---|---|---|---|
/// | [`Debian`](Family::Debian) | apt | `uidmap` | the tools are a SEPARATE package and are NOT installed by default — the single most common cause of a failed rootless setup |
/// | [`Rhel`](Family::Rhel) | dnf | `shadow-utils` | same tools, different package name, and normally already present |
/// | [`Suse`](Family::Suse) | zypper | `shadow` | third spelling of the same package |
/// | [`Arch`](Family::Arch) | pacman | `shadow` | `shadow` is in `base`, so the tools are always there |
/// | [`Alpine`](Family::Alpine) | apk | `shadow-uidmap` | **OpenRC, no logind** — the whole lingering mechanism is absent, see [`Logind`] |
/// | [`Gentoo`](Family::Gentoo) | portage | `shadow` | OpenRC *or* systemd from one `ID`: proof that family cannot imply init |
/// | [`Void`](Family::Void) | xbps | `shadow` | runit — a THIRD init, so "systemd or OpenRC" is not the whole world |
/// | [`NixOs`](Family::NixOs) | nix | (declared) | `/etc/subuid` is GENERATED; writing it is reverted on the next rebuild |
///
/// Anything else is [`Family::Unknown`], carrying the `ID` it saw. That is a
/// refusal, not a fallback — see [`crate::rootless`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Family {
    /// Debian, Ubuntu, Linux Mint, Pop!_OS, Raspbian, Devuan …
    Debian,
    /// RHEL, Fedora, CentOS/Stream, Rocky, AlmaLinux, Oracle Linux, Amazon Linux …
    Rhel,
    /// openSUSE Leap/Tumbleweed, SLES, SLE Micro …
    Suse,
    /// Arch, Manjaro, EndeavourOS, CachyOS …
    Arch,
    /// Alpine.
    Alpine,
    /// Gentoo (and Funtoo).
    Gentoo,
    /// Void.
    Void,
    /// NixOS.
    NixOs,
    /// An `ID` this crate does not model, carried verbatim so the refusal can name it.
    Unknown(String),
}

impl Family {
    /// Detect from the real `/etc/os-release`.
    pub fn detect() -> Family {
        Family::from_os_release(&read_os_release())
    }

    /// Parse a family from `/etc/os-release` CONTENTS (pure → fixture-testable).
    ///
    /// `ID` is consulted first and `ID_LIKE` only as a fallback, which is the order
    /// the freedesktop spec intends and the order that matters here: Ubuntu's
    /// `ID=ubuntu ID_LIKE=debian` and Rocky's `ID=rocky ID_LIKE="rhel centos fedora"`
    /// both resolve on `ID_LIKE`, while a derivative that shares `ID_LIKE` with a
    /// family it does NOT share packaging with resolves on its own `ID` first.
    ///
    /// Note this does NOT do what [`crate::platform::Platform::from_os_release`]
    /// does — that one substring-matches the whole file, so a `HOME_URL` mentioning
    /// `debian` would decide it. Here the keys are parsed.
    pub fn from_os_release(contents: &str) -> Family {
        let id = os_release_value(contents, "ID").unwrap_or_default();
        if let Some(f) = Family::from_id(&id) {
            return f;
        }
        for like in os_release_value(contents, "ID_LIKE")
            .unwrap_or_default()
            .split_whitespace()
        {
            if let Some(f) = Family::from_id(like) {
                return f;
            }
        }
        Family::Unknown(if id.is_empty() {
            "<no ID in /etc/os-release>".to_string()
        } else {
            id
        })
    }

    /// One `ID` (or one `ID_LIKE` word) → a family, or `None` if unmodelled.
    fn from_id(id: &str) -> Option<Family> {
        match id.trim().trim_matches('"').to_ascii_lowercase().as_str() {
            "debian" | "ubuntu" | "linuxmint" | "pop" | "raspbian" | "devuan" | "elementary"
            | "kali" | "neon" => Some(Family::Debian),
            "rhel" | "fedora" | "centos" | "rocky" | "almalinux" | "ol" | "oracle" | "amzn"
            | "amazon" | "scientific" => Some(Family::Rhel),
            "opensuse" | "opensuse-leap" | "opensuse-tumbleweed" | "sles" | "sled"
            | "sle-micro" | "suse" => Some(Family::Suse),
            "arch" | "archarm" | "manjaro" | "endeavouros" | "cachyos" | "garuda" => {
                Some(Family::Arch)
            }
            "alpine" => Some(Family::Alpine),
            "gentoo" | "funtoo" => Some(Family::Gentoo),
            "void" => Some(Family::Void),
            "nixos" => Some(Family::NixOs),
            _ => None,
        }
    }

    /// A short stable identifier for reports and test names.
    pub fn id(&self) -> &str {
        match self {
            Family::Debian => "debian",
            Family::Rhel => "rhel",
            Family::Suse => "suse",
            Family::Arch => "arch",
            Family::Alpine => "alpine",
            Family::Gentoo => "gentoo",
            Family::Void => "void",
            Family::NixOs => "nixos",
            Family::Unknown(id) => id,
        }
    }

    /// The family's package manager — `None` for [`Family::Unknown`], because a
    /// guess here would produce an install command that does not exist.
    pub fn package_manager(&self) -> Option<PackageManager> {
        Some(match self {
            Family::Debian => PackageManager::Apt,
            Family::Rhel => PackageManager::Dnf,
            Family::Suse => PackageManager::Zypper,
            Family::Arch => PackageManager::Pacman,
            Family::Alpine => PackageManager::Apk,
            Family::Gentoo => PackageManager::Portage,
            Family::Void => PackageManager::Xbps,
            Family::NixOs => PackageManager::Nix,
            Family::Unknown(_) => return None,
        })
    }

    /// **Which package supplies `newuidmap`/`newgidmap`** — the setuid helpers
    /// without which a rootless container gets exactly one uid (its own) and any
    /// image whose files are owned by a second uid fails to unpack.
    ///
    /// The names genuinely differ per family and this is the fact most worth having
    /// typed: on Debian/Ubuntu the tools live in a package (`uidmap`) that is NOT
    /// pulled in by a default install, which is why "podman is installed" and
    /// "rootless podman works" are different statements there and largely the same
    /// statement on Arch.
    pub fn uidmap_package(&self) -> Option<&'static str> {
        Some(match self {
            Family::Debian => "uidmap",
            Family::Rhel => "shadow-utils",
            Family::Suse => "shadow",
            Family::Arch => "shadow",
            Family::Alpine => "shadow-uidmap",
            Family::Gentoo => "sys-apps/shadow",
            Family::Void => "shadow",
            // NixOS enables the helpers through a config option rather than a
            // package the operator installs; naming a package would be wrong advice.
            Family::NixOs => return None,
            Family::Unknown(_) => return None,
        })
    }

    /// The package that would give a logind-less host the `org.freedesktop.login1`
    /// interface — i.e. the ability to linger at all. `None` where the family
    /// already has systemd-logind by default.
    pub fn elogind_package(&self) -> Option<&'static str> {
        match self {
            Family::Alpine => Some("elogind"),
            Family::Gentoo => Some("sys-auth/elogind"),
            Family::Void => Some("elogind"),
            _ => None,
        }
    }

    /// The package that supplies a ROOTLESS container engine — and with it the
    /// user-scope `podman.socket` unit [`crate::rootless::Step::ContainerSocket`]
    /// enables. `None` where this crate does not know the name and will not guess one,
    /// which is the same rule [`Family::uidmap_package`] follows.
    ///
    /// podman rather than docker, deliberately: the socket this step enables lives in a
    /// USER manager, and podman is the engine that ships one as a user unit on every
    /// family below. Rootless docker exists, but it is set up per-account by its own
    /// script (`dockerd-rootless-setuptool.sh`) — that is not a package name, and not
    /// something a declaration can stand behind.
    pub fn container_engine_package(&self) -> Option<&'static str> {
        Some(match self {
            Family::Debian => "podman",
            Family::Rhel => "podman",
            Family::Suse => "podman",
            Family::Arch => "podman",
            Family::Alpine => "podman",
            Family::Gentoo => "app-containers/podman",
            Family::Void => "podman",
            // Same reason as `uidmap_package`: NixOS turns this on through a config
            // option (`virtualisation.podman.enable`), so naming a package to install
            // would be wrong advice rather than merely unhelpful.
            Family::NixOs => return None,
            Family::Unknown(_) => return None,
        })
    }

    /// **The init this family ships by default — DOCUMENTATION, never authority.**
    ///
    /// Nothing in [`crate::rootless`] branches on this; it branches on
    /// [`Init::probe`]. It is here so a report can say "expected systemd, found
    /// OpenRC", which is a far more useful sentence than either fact alone, and so
    /// the Devuan/Gentoo counterexamples have somewhere to be written down.
    pub fn expected_init(&self) -> Init {
        match self {
            Family::Debian | Family::Rhel | Family::Suse | Family::Arch | Family::NixOs => {
                Init::Systemd
            }
            Family::Alpine | Family::Gentoo => Init::OpenRc,
            Family::Void => Init::Runit,
            Family::Unknown(_) => Init::Unknown,
        }
    }

    /// **Is `/etc` GENERATED on this distro?** — true only for NixOS.
    ///
    /// This is not a stylistic note. `/etc/subuid` on NixOS is produced from the
    /// system configuration (`users.users.<name>.subUidRanges` / `autoSubUidGidRange`)
    /// and materialised into the store; a hand-appended line survives until the next
    /// `nixos-rebuild` and then silently vanishes, taking every container that
    /// depended on it. A setup tool that "succeeds" there is worse than one that
    /// refuses, so [`crate::rootless`] refuses.
    pub fn etc_is_generated(&self) -> bool {
        matches!(self, Family::NixOs)
    }

    /// Bridge to the pre-existing narrow [`crate::platform::Platform`] (the SELinux
    /// switch). Kept one-way and explicit rather than collapsing the two types:
    /// `Platform` answers "does `setenforce` apply", this answers "what host is
    /// this", and only the first three families have an opinion the older type can
    /// represent.
    pub fn platform(&self) -> crate::platform::Platform {
        match self {
            Family::Rhel => crate::platform::Platform::Rhel,
            Family::Debian => crate::platform::Platform::Debian,
            _ => crate::platform::Platform::Other,
        }
    }
}

impl std::fmt::Display for Family {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(self.id())
    }
}

/// Read one `KEY=value` out of `/etc/os-release` text, unquoting a `"…"` value.
///
/// A real parser rather than `contents.contains("debian")`, because os-release is
/// full of URLs and pretty names that contain other distros' names.
fn os_release_value(contents: &str, key: &str) -> Option<String> {
    for line in contents.lines() {
        let line = line.trim();
        if let Some(rest) = line.strip_prefix(key) {
            if let Some(v) = rest.strip_prefix('=') {
                return Some(v.trim().trim_matches('"').trim_matches('\'').to_string());
            }
        }
    }
    None
}

fn read_os_release() -> String {
    // `/etc/os-release` is the symlink; `/usr/lib/os-release` is the real file and
    // the documented fallback for a host with a stripped `/etc`.
    std::fs::read_to_string("/etc/os-release")
        .or_else(|_| std::fs::read_to_string("/usr/lib/os-release"))
        .unwrap_or_default()
}

// ---------------------------------------------------------------------------
// Init + logind — PROBED, not inferred
// ---------------------------------------------------------------------------

/// The init/service manager actually running as PID 1.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Init {
    Systemd,
    OpenRc,
    Runit,
    SysVinit,
    /// Nothing recognisable — a container with PID 1 = the app, a chroot, a distro
    /// with an init this crate does not model.
    Unknown,
}

impl Init {
    /// Probe the running host.
    pub fn probe() -> Init {
        Init::from_facts(&HostFacts::probe())
    }

    /// Classify from facts (pure).
    ///
    /// The systemd test is the one `sd_booted(3)` documents — the existence of the
    /// directory `/run/systemd/system` — and NOT "is there a `/usr/bin/systemctl`",
    /// which is true on any box where somebody installed the package.
    pub fn from_facts(f: &HostFacts) -> Init {
        if f.systemd_run_system {
            Init::Systemd
        } else if f.openrc_softlevel {
            Init::OpenRc
        } else if f.runit_runsvdir {
            Init::Runit
        } else if f.sysvinit_initctl {
            Init::SysVinit
        } else {
            Init::Unknown
        }
    }

    pub fn id(self) -> &'static str {
        match self {
            Init::Systemd => "systemd",
            Init::OpenRc => "openrc",
            Init::Runit => "runit",
            Init::SysVinit => "sysvinit",
            Init::Unknown => "unknown",
        }
    }

    /// Whether this init has per-user service managers at all — the thing a
    /// `systemctl --user enable --now podman.socket` presupposes.
    pub fn has_user_manager(self) -> bool {
        matches!(self, Init::Systemd)
    }
}

impl std::fmt::Display for Init {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(self.id())
    }
}

/// **Which login manager, if any, is running** — the single fact that decides
/// whether "linger" is a concept on this host.
///
/// `loginctl enable-linger` is not a generic Linux operation. It is a method on
/// `org.freedesktop.login1.Manager`, and lingering means one specific thing: the
/// login manager keeps a `user@<uid>.service` and a `/run/user/<uid>` alive for an
/// account with no session. Two implementations provide that interface —
/// systemd-logind and elogind, the latter being systemd-logind extracted to run
/// under OpenRC or runit — and on a host with neither, the operation does not
/// exist and no amount of interface design makes it exist.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Logind {
    /// systemd-logind.
    Systemd,
    /// elogind — the standalone port. Same bus name, same `SetUserLinger` method,
    /// same `/var/lib/elogind/linger` marker semantics (under its own state dir).
    Elogind,
}

impl Logind {
    /// Probe the running host; `None` means there is no login manager and therefore
    /// no lingering.
    pub fn probe() -> Option<Logind> {
        Logind::from_facts(&HostFacts::probe())
    }

    /// Classify from facts (pure).
    pub fn from_facts(f: &HostFacts) -> Option<Logind> {
        if f.systemd_seats {
            Some(Logind::Systemd)
        } else if f.elogind_run {
            Some(Logind::Elogind)
        } else {
            None
        }
    }

    /// Where the implementation persists the per-account linger marker.
    ///
    /// VERIFIED on t14s (Ubuntu 26.04, systemd): `/var/lib/systemd/linger/alice`
    /// is a zero-byte file, mode 0644 in a 0755 directory — so the state is
    /// READABLE without root and without a bus connection, which is what lets
    /// [`crate::rootless`] answer "is this host already set up?" from an
    /// unprivileged process.
    pub fn linger_dir(self) -> &'static str {
        match self {
            Logind::Systemd => "/var/lib/systemd/linger",
            Logind::Elogind => "/var/lib/elogind/linger",
        }
    }

    pub fn id(self) -> &'static str {
        match self {
            Logind::Systemd => "systemd-logind",
            Logind::Elogind => "elogind",
        }
    }
}

// ---------------------------------------------------------------------------
// The facts
// ---------------------------------------------------------------------------

/// **Everything the classification above reads, as plain data.**
///
/// One struct, so a test can construct "Alpine with elogind and no `newuidmap`"
/// in four lines and assert the exact refusal — no VM, no container, no root. The
/// only impure function in this module is [`HostFacts::probe`].
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct HostFacts {
    /// Contents of `/etc/os-release`.
    pub os_release: String,
    /// `/run/systemd/system` exists — the documented `sd_booted(3)` test.
    pub systemd_run_system: bool,
    /// `/run/systemd/seats` exists — systemd-logind is running (not merely installed).
    pub systemd_seats: bool,
    /// `/run/elogind` exists — elogind is running.
    pub elogind_run: bool,
    /// `/run/openrc/softlevel` exists — OpenRC has booted.
    pub openrc_softlevel: bool,
    /// `/run/runit` exists — runit is supervising.
    pub runit_runsvdir: bool,
    /// `/run/initctl` exists — classic sysvinit's control fifo.
    pub sysvinit_initctl: bool,
    /// `newuidmap` is on the host.
    pub newuidmap: bool,
    /// `newgidmap` is on the host.
    pub newgidmap: bool,
}

impl HostFacts {
    /// Read the facts off the running host. Every field is an existence test, so
    /// this is cheap, needs no privileges and cannot fail — an unreadable path is
    /// simply `false`, which the classifiers already treat as "not there".
    pub fn probe() -> HostFacts {
        HostFacts {
            os_release: read_os_release(),
            systemd_run_system: Path::new("/run/systemd/system").exists(),
            systemd_seats: Path::new("/run/systemd/seats").exists(),
            elogind_run: Path::new("/run/elogind").exists(),
            openrc_softlevel: Path::new("/run/openrc/softlevel").exists(),
            runit_runsvdir: Path::new("/run/runit").exists(),
            sysvinit_initctl: Path::new("/run/initctl").exists(),
            newuidmap: which_uidmap("newuidmap").is_some(),
            newgidmap: which_uidmap("newgidmap").is_some(),
        }
    }
}

/// Locate `newuidmap`/`newgidmap` without a `which` subprocess and without `$PATH`
/// guesswork: the setuid helpers are installed into one of three fixed prefixes on
/// every distro in the roster, so the honest search is that list.
fn which_uidmap(tool: &str) -> Option<String> {
    for dir in ["/usr/bin", "/bin", "/usr/sbin", "/sbin", "/usr/local/bin"] {
        let p = format!("{dir}/{tool}");
        if Path::new(&p).exists() {
            return Some(p);
        }
    }
    None
}

// ---------------------------------------------------------------------------
// The host, assembled
// ---------------------------------------------------------------------------

/// A classified host: what family it is, what init is really running, and whether
/// there is a login manager.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Host {
    pub family: Family,
    pub init: Init,
    pub logind: Option<Logind>,
    pub facts: HostFacts,
}

impl Host {
    /// Classify the running host.
    pub fn detect() -> Host {
        Host::from_facts(HostFacts::probe())
    }

    /// Classify from facts (pure → fixture-testable).
    pub fn from_facts(facts: HostFacts) -> Host {
        Host {
            family: Family::from_os_release(&facts.os_release),
            init: Init::from_facts(&facts),
            logind: Logind::from_facts(&facts),
            facts,
        }
    }

    /// True when the running init is not the one the family ships — the Devuan /
    /// Gentoo-systemd / container-without-init case. Worth REPORTING (it explains a
    /// surprising refusal) and worth never BRANCHING on.
    pub fn init_is_unexpected(&self) -> bool {
        self.init != self.family.expected_init() && self.init != Init::Unknown
    }

    /// One line for a report.
    pub fn summary(&self) -> String {
        let logind = match self.logind {
            Some(l) => l.id(),
            None => "none",
        };
        let mut s = format!(
            "{} (init: {}, logind: {})",
            self.family, self.init, logind
        );
        if self.init_is_unexpected() {
            s.push_str(&format!(
                " — NOTE: the {} family normally runs {}",
                self.family,
                self.family.expected_init()
            ));
        }
        s
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn facts(os_release: &str) -> HostFacts {
        HostFacts {
            os_release: os_release.to_string(),
            ..Default::default()
        }
    }

    /// **`ID` and `ID_LIKE` are PARSED, not substring-matched.**
    ///
    /// The pre-existing [`crate::platform::Platform::from_os_release`] lowercases the
    /// whole file and asks `contains("debian")`, so any host whose `HOME_URL`,
    /// `PRETTY_NAME` or `BUG_REPORT_URL` mentions another distro is misclassified —
    /// and derivatives' os-release files mention their upstream constantly. This
    /// asserts the key-parsed behaviour directly against that shape.
    ///
    /// RED direction: replace the parse with a `contains` and the Alpine case
    /// resolves to `Debian` on its `HOME_URL`.
    #[test]
    fn family_parses_keys_and_is_not_fooled_by_urls() {
        let alpine_with_a_misleading_url = "\
NAME=\"Alpine Linux\"
ID=alpine
VERSION_ID=3.21.0
HOME_URL=\"https://alpinelinux.org/debian-comparison\"
";
        assert_eq!(
            Family::from_os_release(alpine_with_a_misleading_url),
            Family::Alpine
        );
        assert_eq!(
            crate::platform::Platform::from_os_release(alpine_with_a_misleading_url),
            crate::platform::Platform::Debian,
            "the OLD narrow type really is fooled by this — that is why a real \
             parser was needed, and this asserts the difference rather than \
             asserting a hope"
        );
    }

    /// The five families the owner named, plus the three the evidence added, each
    /// from a REAL `/etc/os-release` shape.
    ///
    /// Ubuntu is the one VERIFIED against this very box (`ID=ubuntu ID_LIKE=debian`,
    /// Ubuntu 26.04); the rest are the documented `ID` values of each distro.
    #[test]
    fn the_roster_resolves_from_real_os_release_ids() {
        let cases: &[(&str, Family)] = &[
            ("ID=ubuntu\nID_LIKE=debian\n", Family::Debian),
            ("ID=debian\n", Family::Debian),
            // Rocky resolves through ID_LIKE — its own ID is not in the table.
            (
                "ID=\"rocky\"\nID_LIKE=\"rhel centos fedora\"\n",
                Family::Rhel,
            ),
            ("ID=fedora\n", Family::Rhel),
            ("ID=\"opensuse-tumbleweed\"\nID_LIKE=\"opensuse suse\"\n", Family::Suse),
            ("ID=arch\n", Family::Arch),
            ("ID=alpine\n", Family::Alpine),
            ("ID=gentoo\n", Family::Gentoo),
            ("ID=void\n", Family::Void),
            ("ID=nixos\n", Family::NixOs),
        ];
        for (text, want) in cases {
            assert_eq!(&Family::from_os_release(text), want, "for {text:?}");
        }
    }

    /// An unmodelled distro is [`Family::Unknown`] **carrying its ID**, so the
    /// refusal downstream can name what it refused. A silent fallback to Debian is
    /// the failure this type exists to prevent.
    #[test]
    fn an_unmodelled_distro_is_unknown_and_says_which() {
        let f = Family::from_os_release("ID=slackware\nPRETTY_NAME=\"Slackware 15\"\n");
        assert_eq!(f, Family::Unknown("slackware".into()));
        assert_eq!(f.id(), "slackware");
        assert_eq!(f.package_manager(), None, "no guessed install command");
        assert_eq!(f.uidmap_package(), None);
    }

    /// **The `newuidmap` package name really does differ per family**, and that is
    /// the fact worth typing. Debian is the interesting one: `uidmap` is a separate
    /// package that a default install does NOT pull in, so "podman is installed"
    /// and "rootless podman works" are different statements there.
    #[test]
    fn each_family_names_its_own_uidmap_package() {
        assert_eq!(Family::Debian.uidmap_package(), Some("uidmap"));
        assert_eq!(Family::Rhel.uidmap_package(), Some("shadow-utils"));
        assert_eq!(Family::Suse.uidmap_package(), Some("shadow"));
        assert_eq!(Family::Arch.uidmap_package(), Some("shadow"));
        assert_eq!(Family::Alpine.uidmap_package(), Some("shadow-uidmap"));
        assert_eq!(
            Family::Debian.package_manager().unwrap().install_command("uidmap"),
            "apt-get install -y uidmap"
        );
        assert_eq!(
            Family::Alpine.package_manager().unwrap().install_command("shadow-uidmap"),
            "apk add shadow-uidmap"
        );
    }

    /// **Family must NOT determine init.** Devuan is `ID_LIKE=debian` and runs
    /// sysvinit; Gentoo ships both profiles. The classifier reads the RUNNING
    /// host, and the family's expectation is reported as a note, never obeyed.
    ///
    /// RED direction: implement `Init` as `family.expected_init()` and both
    /// assertions here fail while every systemd-on-Debian test still passes — the
    /// silent half.
    #[test]
    fn init_is_probed_from_the_host_not_derived_from_the_family() {
        // Devuan: Debian packaging, sysvinit PID 1.
        let devuan = Host::from_facts(HostFacts {
            os_release: "ID=devuan\nID_LIKE=debian\n".into(),
            sysvinit_initctl: true,
            ..Default::default()
        });
        assert_eq!(devuan.family, Family::Debian);
        assert_eq!(devuan.init, Init::SysVinit, "PID 1 decides, not packaging");
        assert!(devuan.init_is_unexpected());
        assert!(
            devuan.summary().contains("normally runs systemd"),
            "the surprise must be reported: {}",
            devuan.summary()
        );

        // Gentoo with the systemd profile: same ID, other init.
        let gentoo_systemd = Host::from_facts(HostFacts {
            os_release: "ID=gentoo\n".into(),
            systemd_run_system: true,
            systemd_seats: true,
            ..Default::default()
        });
        assert_eq!(gentoo_systemd.init, Init::Systemd);
        assert_eq!(gentoo_systemd.logind, Some(Logind::Systemd));
    }

    /// **The Alpine case, in full — and the part everybody gets wrong.**
    ///
    /// Stock Alpine is OpenRC with NO login manager, so lingering does not exist:
    /// there is no `loginctl`, no `org.freedesktop.login1`, and nothing creates
    /// `/run/user/<uid>`. But `elogind` is a real Alpine package implementing that
    /// exact interface, and with it installed the SAME `SetUserLinger` call works.
    /// So the honest model is "logind is a probed capability", not "Alpine cannot".
    ///
    /// RED direction: model lingering as `family == Alpine ⇒ unsupported` and the
    /// second half fails — the crate would refuse on a host that can do it.
    #[test]
    fn alpine_has_no_logind_by_default_but_elogind_is_a_real_answer() {
        let stock = Host::from_facts(HostFacts {
            os_release: "ID=alpine\n".into(),
            openrc_softlevel: true,
            ..Default::default()
        });
        assert_eq!(stock.init, Init::OpenRc);
        assert_eq!(stock.logind, None, "no login manager ⇒ no lingering concept");
        assert!(!stock.init.has_user_manager(), "and no `systemctl --user`");
        assert!(!stock.init_is_unexpected(), "OpenRC is exactly what Alpine ships");
        assert_eq!(stock.family.elogind_package(), Some("elogind"));

        let with_elogind = Host::from_facts(HostFacts {
            os_release: "ID=alpine\n".into(),
            openrc_softlevel: true,
            elogind_run: true,
            ..Default::default()
        });
        assert_eq!(
            with_elogind.logind,
            Some(Logind::Elogind),
            "elogind provides the very interface linger is set through"
        );
        assert_eq!(
            with_elogind.logind.unwrap().linger_dir(),
            "/var/lib/elogind/linger",
            "…under its own state dir, not systemd's"
        );
        // Still OpenRC: a logind does not give the host per-user service managers.
        assert!(!with_elogind.init.has_user_manager());
    }

    /// systemd-logind's marker directory, as VERIFIED on t14s: a zero-byte file per
    /// lingering account, world-readable. That readability is load-bearing — it is
    /// how the inspection path answers "already set up?" with no root and no bus.
    #[test]
    fn linger_state_lives_in_a_readable_marker_directory() {
        assert_eq!(Logind::Systemd.linger_dir(), "/var/lib/systemd/linger");
        assert_eq!(Logind::Elogind.linger_dir(), "/var/lib/elogind/linger");
    }

    /// NixOS is recognised specifically so it can be REFUSED: `/etc/subuid` there is
    /// generated from the system configuration, and an appended line survives only
    /// until the next `nixos-rebuild`.
    #[test]
    fn nixos_is_recognised_as_a_generated_etc() {
        assert!(Family::NixOs.etc_is_generated());
        for f in [
            Family::Debian,
            Family::Rhel,
            Family::Suse,
            Family::Arch,
            Family::Alpine,
            Family::Gentoo,
            Family::Void,
        ] {
            assert!(!f.etc_is_generated(), "{f} is a mutable /etc");
        }
    }

    /// The bridge to the older narrow type is one-way and only where it has an
    /// opinion — everything else is `Other`, exactly as before.
    #[test]
    fn the_platform_bridge_keeps_the_old_types_meaning() {
        use crate::platform::Platform;
        assert_eq!(Family::Rhel.platform(), Platform::Rhel);
        assert_eq!(Family::Debian.platform(), Platform::Debian);
        assert_eq!(Family::Alpine.platform(), Platform::Other);
        assert_eq!(Family::Arch.platform(), Platform::Other);
    }

    /// The probe runs on whatever box the tests run on and must classify it without
    /// panicking. Asserted loosely on purpose — this is the one test whose answer
    /// depends on the host. (On t14s it VERIFIED as debian/systemd/systemd-logind.)
    #[test]
    fn probing_the_running_host_classifies_it() {
        let h = Host::detect();
        assert!(!h.summary().is_empty());
        if h.init == Init::Systemd {
            assert!(
                h.facts.systemd_run_system,
                "the systemd verdict must come from /run/systemd/system"
            );
        }
    }

    #[test]
    fn empty_os_release_is_unknown_and_says_so() {
        let f = Family::from_os_release("");
        assert!(matches!(f, Family::Unknown(_)));
        assert!(f.id().contains("no ID"));
    }

    /// `facts()` helper coverage: a bare os-release with no runtime markers is an
    /// unknown init and no logind — the shape of a build container.
    #[test]
    fn a_host_with_no_init_markers_is_unknown_not_systemd() {
        let h = Host::from_facts(facts("ID=debian\n"));
        assert_eq!(h.init, Init::Unknown);
        assert_eq!(h.logind, None);
        assert!(
            !h.init_is_unexpected(),
            "`Unknown` is ignorance, not a contradiction — do not report it as a surprise"
        );
    }
}