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

nordisk/Skidbladnir

crates/skidbladnir-systemd/src/dropin.rs

//! **DROP-IN CONFLICT — an install that is silently overridden must not report
//! success.**
//!
//! systemd resolves a unit as the fragment PLUS every `*.conf` in
//! `<unit-dir>/<unit>.service.d/`, applied in lexical order, with the drop-in
//! winning for a scalar setting like `User=`. Skidbladnir renders and writes the
//! fragment — and, until this module existed, then printed
//! `✓ installed korp-server systemd service (user korp)` while a drop-in written
//! months earlier had already decided the service runs as `alice`.
//!
//! Measured on oden 2026-08-03: `/etc/systemd/system/korp-server.service` declared
//! `User=korp`, `korp-server --install --system` reported `user korp`, and
//! `systemctl show -p User korp-server` answered `alice` — because
//! `korp-server.service.d/rootless-podman.conf` says `User=alice`. A second
//! drop-in (`cases.conf`) pinned `KORP_CASES`/`KORP_STATE` into
//! `/home/alice/.korp`, i.e. into a *different account's home* than the unit
//! claimed to run as. Both were invisible to the install.
//!
//! ## The stance this module takes
//!
//! **Report, do not refuse, and never delete.** A drop-in is systemd's own,
//! documented extension seam: an installer that refuses to manage a correctly
//! extended unit is broken, and one that takes ownership of `<unit>.service.d/`
//! silently destroys deliberate operator intent (`rootless-podman.conf` exists
//! because rootless podman genuinely needs the invoking user's runtime dir). The
//! defect was never the drop-in. The defect was the *claim*.
//!
//! So the install still happens, and then:
//!
//! 1. every contradiction is named — file, verbatim directive, what the spec
//!    declared, what actually takes effect ([`DropInScan::conflict_report`]);
//! 2. the success line is derived from what systemd RESOLVED, not from what the
//!    renderer wrote (see `install_systemd_scoped`), so it can never again print a
//!    user the service does not run as;
//! 3. the `install_unit` surface goes RED, so a machine-readable consumer sees the
//!    override too.
//!
//! And the uninstall is symmetric with what the install claims to own: Skidbladnir
//! writes the fragment and only the fragment, so the uninstall removes the fragment
//! and **keeps** every drop-in — but NAMES them in the report, because a
//! `<unit>.service.d/` left standing after the unit is gone is a landmine armed for
//! the next install. An empty (or emptied) drop-in dir is removed, since there is
//! nothing there to preserve.
//!
//! Everything below the `scan_dropins_in` seam is pure: it reads a directory and a
//! passwd-format file that the caller chooses, so the whole contract is testable
//! without root, without a bus, and without the live `/etc`.
//!
//! ## What this scan does NOT see, and what covers it
//!
//! systemd also merges drop-ins from `/run/systemd/system/<unit>.d/`,
//! `/usr/lib/systemd/system/<unit>.d/`, `.control/` dirs and `*.conf` under a
//! type-wide `service.d/`. This module reads the ONE directory that sits next to
//! the unit it just wrote — the only one an installer can meaningfully name. The
//! backstop for every other source is that `install_systemd_scoped` derives its
//! success line from
//! [`Systemd::service_user`](crate::systemd::Systemd::service_user), which is what
//! the daemon RESOLVED from all of them; a conflict this scan misses still cannot
//! produce a success line naming the wrong account.

use std::path::{Path, PathBuf};

/// What kind of contradiction a drop-in raises against the unit the spec declared.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConflictKind {
    /// `User=` — the account the service actually runs as is not the declared one.
    /// The one that moves `$HOME`, and therefore every `$HOME`-derived data root.
    User,
    /// `Group=` — likewise for the primary group.
    Group,
    /// `Environment=KEY=…` where the spec declares `KEY` with a DIFFERENT value.
    /// The drop-in silently replaces a value the installer just rendered.
    EnvOverride,
    /// `Environment=KEY=…` whose value points at an absolute path inside some real
    /// account's home directory that is not the declared user's. This is the shape
    /// that made korp-server serve out of `/home/alice/.korp` under a unit that
    /// said `User=korp`.
    ForeignHome,
    /// `MemoryHigh=` / `MemoryMax=` — a hand-written **resource ceiling**, the
    /// setting `/etc/systemd/system/dwarves-server.service.d/10-bounded.conf` was
    /// created to carry on 2026-08-20 because
    /// [`crate::service::ServiceSpec`] had no memory axis at all.
    ///
    /// Now that it does, a drop-in ceiling is a contradiction worth naming for the
    /// same reason `User=` is: the resolved bound is not the declared bound, the
    /// install writes only the fragment, and the uninstall therefore leaves the
    /// ceiling behind to re-apply to the next install. Compared **by bytes**, not by
    /// text, so `MemoryMax=24G` does not "contradict" a rendered
    /// `MemoryMax=25769803776`.
    MemoryCeiling,
    /// `OOMPolicy=` — a hand-written **OOM REACTION**: what systemd does to the unit
    /// when the kernel OOM killer (or systemd-oomd) reaps one of its processes.
    ///
    /// The other half of [`ConflictKind::MemoryCeiling`], and unowned for the same
    /// three reasons: the install writes only the fragment, the uninstall leaves the
    /// drop-in behind, and every `--refresh` renders around it. Worse than a ceiling
    /// in one respect — a wrong reaction is INVISIBLE until the day the service is
    /// actually OOM-killed, and then it either takes the box's work down with it or
    /// limps on half-reaped.
    ///
    /// Compared by MEANING via [`crate::systemd::OomPolicy::parse`], not by text; a
    /// value systemd itself would refuse is reported rather than guessed at.
    OomPolicy,
    /// A drop-in `StartLimitIntervalSec=` or `StartLimitBurst=` that contradicts the
    /// declared [`crate::systemd::StartLimit`].
    ///
    /// The nastiest of the four to leave unnamed, because it changes the OUTCOME OF A
    /// FAILURE rather than the behaviour of a healthy service: the same spec, with one
    /// number moved by an operator, flips between "parks in `failed` after five tries"
    /// and "retries forever". Nothing about the running service looks different until
    /// the day it starts failing, and then it does the opposite of what the spec says.
    ///
    /// Both keys report under this one kind because they are one declaration; the
    /// `directive` field carries which of the two was written.
    StartLimit,
    /// A drop-in `OOMScoreAdjust=` that contradicts the declared bias — a change to
    /// WHICH service the kernel reaps when the box is short, i.e. a decision about the
    /// unit's NEIGHBOURS made in a file none of them can see.
    OomScoreAdjust,
}

impl ConflictKind {
    /// A short human label for the report.
    pub fn label(self) -> &'static str {
        match self {
            ConflictKind::User => "USER OVERRIDE",
            ConflictKind::Group => "GROUP OVERRIDE",
            ConflictKind::EnvOverride => "ENV OVERRIDE",
            ConflictKind::MemoryCeiling => "MEMORY CEILING",
            ConflictKind::OomPolicy => "OOM POLICY",
            ConflictKind::StartLimit => "START LIMIT",
            ConflictKind::OomScoreAdjust => "OOM SCORE",
            ConflictKind::ForeignHome => "FOREIGN HOME",
        }
    }
}

/// ONE contradiction between a drop-in file and the spec being installed.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DropInConflict {
    /// The drop-in that carries it.
    pub file: PathBuf,
    /// The setting, as an operator would grep for it (`User`, `Environment:KORP_CASES`).
    pub key: String,
    /// The verbatim directive line from the drop-in (`User=alice`).
    pub directive: String,
    /// What the spec asked for (empty ⇒ the spec declared nothing for this key).
    pub declared: String,
    /// What will actually take effect once systemd merges the drop-ins.
    pub effective: String,
    /// Which rule fired.
    pub kind: ConflictKind,
}

impl DropInConflict {
    /// One report line: file, directive, and the two values that disagree.
    pub fn line(&self) -> String {
        let declared = if self.declared.is_empty() {
            "(not declared by the spec)".to_string()
        } else {
            self.declared.clone()
        };
        format!(
            "{}: {} says `{}` — spec declared {}, EFFECTIVE is {}",
            self.kind.label(),
            self.file.display(),
            self.directive,
            declared,
            self.effective,
        )
    }
}

/// The result of reading a unit's `<unit>.service.d/` directory.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DropInScan {
    /// The directory scanned (whether or not it exists).
    pub dir: PathBuf,
    /// Every `*.conf` found, in the lexical order systemd applies them.
    pub files: Vec<PathBuf>,
    /// Every contradiction, in file order.
    pub conflicts: Vec<DropInConflict>,
    /// The account the unit will actually run as once the drop-ins are merged —
    /// `None` when no drop-in overrides `User=` (⇒ the spec's own user stands).
    pub effective_user: Option<String>,
}

impl DropInScan {
    /// Does this unit have any drop-in at all? (Independent of conflicts — a
    /// drop-in dir with only additive settings is legitimate and still worth
    /// naming on uninstall.)
    pub fn has_dropins(&self) -> bool {
        !self.files.is_empty()
    }

    /// Does any drop-in contradict the spec?
    pub fn has_conflicts(&self) -> bool {
        !self.conflicts.is_empty()
    }

    /// The loud, multi-line block an install prints when it is being overridden.
    /// Empty string when there is nothing to say — a caller can print it
    /// unconditionally.
    ///
    /// It names the file, the directive and the winner for every conflict, so an
    /// operator can act without going and reading five files.
    pub fn conflict_report(&self, service: &str) -> String {
        if self.conflicts.is_empty() {
            return String::new();
        }
        let mut s = format!(
            // Deliberately NOT "the unit this install just wrote": the same writer is
            // now read by `dropin-check`, which writes nothing and may be run months
            // after the install. A report that misstates its own context is the exact
            // class of defect this module exists to remove.
            "⚠ `{service}`: {} drop-in directive(s) in {} OVERRIDE the unit fragment. \
             systemd applies the fragment FIRST and every drop-in after it, so these win:",
            self.conflicts.len(),
            self.dir.display(),
        );
        for c in &self.conflicts {
            s.push_str("\n    ! ");
            s.push_str(&c.line());
        }
        s.push_str(
            "\n  The drop-ins were KEPT (they are systemd's own extension seam and this \
             install does not own them). Nothing above was applied by skidbladnir — edit or \
             remove the named file(s) if the spec's values are the ones you want.",
        );
        s
    }

    /// The line an uninstall prints about drop-ins it deliberately left behind.
    /// Empty when the unit had none.
    pub fn kept_report(&self, service: &str) -> String {
        if self.files.is_empty() {
            return String::new();
        }
        let names: Vec<String> = self
            .files
            .iter()
            .map(|p| {
                p.file_name()
                    .map(|n| n.to_string_lossy().into_owned())
                    .unwrap_or_default()
            })
            .collect();
        format!(
            "drop-in dir {} KEPT with {} file(s) [{}] — skidbladnir installs the unit \
             fragment only and never wrote these, so `{service}`'s uninstall does not delete \
             them. ⚠ They will apply again to the NEXT install of this unit.",
            self.dir.display(),
            names.len(),
            names.join(", "),
        )
    }
}

/// The declaration an install is making, as the drop-in scanner needs to see it.
/// Kept primitive (not a `SystemdServiceSpec`) so the whole rule set is exercisable
/// from a test with three string literals.
#[derive(Debug, Clone, Default)]
pub struct Declared {
    /// `User=` the spec renders.
    pub user: String,
    /// `Group=` the spec renders.
    pub group: String,
    /// Every `Environment=KEY=VALUE` the spec renders inline. Secrets are NOT here
    /// (they never reach the world-readable unit) and must not be — a conflict
    /// report is printed to a terminal.
    pub env: Vec<(String, String)>,
    /// `MemoryHigh=` the spec renders — the SOFT resource ceiling, **verbatim in the
    /// spelling the fragment carries** (`"16G"`). Empty ⇒ the spec declares no soft
    /// ceiling. Compared against a drop-in's value by BYTES
    /// ([`crate::systemd::MemoryBound::parse_bytes`]), so two spellings of one size
    /// are not a contradiction.
    #[doc(alias = "MemoryHigh")]
    pub memory_high: String,
    /// `MemoryMax=` the spec renders — the HARD resource ceiling. Empty ⇒ none
    /// declared. Same byte-wise comparison as [`Declared::memory_high`].
    #[doc(alias = "MemoryMax")]
    pub memory_max: String,
    /// `OOMPolicy=` the spec renders — THE REACTION to being OOM-killed, verbatim
    /// (`"continue"`). Empty ⇒ the spec declares none, and the unit then inherits the
    /// manager's `DefaultOOMPolicy=`, which makes any drop-in reaction an override
    /// worth naming. Compared by MEANING
    /// ([`crate::systemd::OomPolicy::parse`]), not by text.
    #[doc(alias = "OOMPolicy")]
    pub oom_policy: String,
    /// `StartLimitIntervalSec=` the spec renders, verbatim (`"12"`). Empty ⇒ the spec
    /// declares no rate limit, and the unit inherits the manager's
    /// `DefaultStartLimitIntervalSec=` — which makes any drop-in one an override worth
    /// naming, exactly as an undeclared ceiling makes any drop-in ceiling one.
    #[doc(alias = "StartLimitIntervalSec")]
    pub start_limit_interval: String,
    /// `StartLimitBurst=` the spec renders, verbatim (`"5"`). Empty ⇒ undeclared; see
    /// [`start_limit_interval`](Declared::start_limit_interval).
    #[doc(alias = "StartLimitBurst")]
    pub start_limit_burst: String,
    /// `OOMScoreAdjust=` the spec renders, verbatim (`"500"`). Empty ⇒ the spec
    /// declares none and the unit inherits the manager's level (measured `0`).
    #[doc(alias = "OOMScoreAdjust")]
    pub oom_score_adjust: String,
}

/// The directory systemd reads drop-ins from for `unit_file_name` under `unit_dir`
/// — `<unit_dir>/<unit>.service.d`. The ONE place that name is built.
pub fn dropin_dir(unit_dir: &Path, unit_file_name: &str) -> PathBuf {
    unit_dir.join(format!("{unit_file_name}.d"))
}

/// **AUDIT SEAM — read a [`Declared`] back out of an INSTALLED unit fragment.**
///
/// [`scan_dropins_in`] answers "is this install being overridden?" and until now the
/// only caller who could ask was the install itself, which holds the
/// `SystemdServiceSpec` it just rendered. That makes the check unavailable exactly
/// when it is cheapest and safest to run — on a running box, months later, changing
/// nothing.
///
/// A fragment on disk carries the same three facts a spec declares (`User=`,
/// `Group=`, the inline `Environment=` lines), so this reconstructs the declaration
/// from the file with the SAME parser the drop-ins go through
/// ([`parse_service_directives`] + [`split_env_assignments`]) — never a second,
/// drifting notion of what a unit says.
///
/// Two deliberate details:
///
/// * `Group=` follows systemd's own default (`Group=` absent ⇒ the primary group of
///   `User=`), so a unit that names only a user does not produce a phantom
///   GROUP OVERRIDE against the empty string.
/// * A fragment with NO `User=` yields an empty [`Declared::user`], which is exactly
///   what a `Scope::User` spec renders — the foreign-home rule then correctly stands
///   down (see [`scan_dropins_in`]).
///
/// A `.service` file whose LAST `User=` is the effective one is what is recorded:
/// within a single file systemd also takes the last assignment of a scalar.
pub fn declared_from_unit_file(unit_path: &Path) -> std::io::Result<Declared> {
    let text = std::fs::read_to_string(unit_path)?;
    Ok(declared_from_unit_text(&text))
}

/// The pure half of [`declared_from_unit_file`] — testable with a string literal,
/// no disk.
pub fn declared_from_unit_text(text: &str) -> Declared {
    let mut d = Declared::default();
    let mut group_declared = false;
    for (key, value) in parse_service_directives(text)
        .into_iter()
        .chain(parse_directives_in(text, "[Unit]"))
    {
        match key.as_str() {
            // Last assignment wins for a scalar, inside one file as well as across
            // drop-ins.
            "User" => d.user = value,
            // The RESOURCE CEILING as the fragment carries it. Read back so an
            // identical drop-in ceiling is not reported as a contradiction, and a
            // DIFFERENT one is. Last assignment of a scalar wins, here as in systemd.
            "MemoryHigh" => d.memory_high = value,
            "MemoryMax" => d.memory_max = value,
            // …and THE REACTION the fragment carries, read back for the same reason:
            // so a drop-in that AGREES with the declaration is not reported, and one
            // that changes it is.
            "OOMPolicy" => d.oom_policy = value,
            "OOMScoreAdjust" => d.oom_score_adjust = value,
            "StartLimitIntervalSec" => d.start_limit_interval = value,
            "StartLimitBurst" => d.start_limit_burst = value,
            "Group" => {
                d.group = value;
                group_declared = true;
            }
            "Environment" => {
                for (k, v) in split_env_assignments(&value) {
                    // A repeated key in one fragment: the last assignment is the one
                    // that takes effect, so replace rather than push a duplicate that
                    // would make the conflict rules compare against a stale value.
                    match d.env.iter_mut().find(|(ek, _)| *ek == k) {
                        Some(slot) => slot.1 = v,
                        None => d.env.push((k, v)),
                    }
                }
            }
            _ => {}
        }
    }
    // systemd: no `Group=` ⇒ the user's primary group. Mirror that, or every unit
    // that names only `User=` reports a GROUP OVERRIDE against "".
    if !group_declared {
        d.group = d.user.clone();
    }
    d
}

/// Parse a systemd unit/drop-in body into its `[Service]` directives, in file order.
///
/// Handles what a real drop-in uses: section headers, `#`/`;` comments, blank
/// lines, and backslash line-continuation. Returns `(key, value)` with surrounding
/// whitespace trimmed.
///
/// Kept as-is for every caller that wants the service body; [`parse_directives_in`]
/// is the general form. The old comment here said directives outside `[Service]` are
/// ignored because "a `[Unit]` `After=` cannot contradict `User=`" — true of the
/// rules that existed then, and FALSE as of the start-limit axis: `StartLimitBurst=`
/// and `StartLimitIntervalSec=` are `[Unit]` keys, so a `[Service]`-only parser made
/// the whole rate limit unauditable.
pub fn parse_service_directives(text: &str) -> Vec<(String, String)> {
    parse_directives_in(text, "[Service]")
}

/// **Parse one named section's directives** — the general form of
/// [`parse_service_directives`].
///
/// It exists because the audit was `[Service]`-only while systemd spreads ONE
/// service's behaviour across sections: `RestartSec=` is a `[Service]` key and the
/// `StartLimitIntervalSec=`/`StartLimitBurst=` it runs into are `[Unit]` keys. A
/// scanner that could not read `[Unit]` could not see a drop-in changing the outcome
/// of every failure — it would report the drop-in file as present and find nothing
/// wrong in it.
///
/// `section` is matched case-insensitively, whole, including the brackets
/// (`"[Unit]"`, `"[Service]"`).
pub fn parse_directives_in(text: &str, section: &str) -> Vec<(String, String)> {
    let mut out = Vec::new();
    let mut in_service = false;
    let mut pending = String::new();

    for raw in text.lines() {
        let line = raw.trim();
        // A continuation in progress swallows the line whole (including a line that
        // looks like a comment — systemd continues first).
        if !pending.is_empty() {
            let (body, more) = strip_continuation(line);
            pending.push(' ');
            pending.push_str(body);
            if !more {
                let done = std::mem::take(&mut pending);
                push_directive(&mut out, &done, in_service);
            }
            continue;
        }
        if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
            continue;
        }
        if line.starts_with('[') {
            in_service = line.eq_ignore_ascii_case(section);
            continue;
        }
        let (body, more) = strip_continuation(line);
        if more {
            pending.push_str(body);
        } else {
            push_directive(&mut out, body, in_service);
        }
    }
    if !pending.is_empty() {
        push_directive(&mut out, &pending, in_service);
    }
    out
}

/// `(line without a trailing `\`, does it continue?)`.
fn strip_continuation(line: &str) -> (&str, bool) {
    match line.strip_suffix('\\') {
        Some(body) => (body.trim_end(), true),
        None => (line, false),
    }
}

fn push_directive(out: &mut Vec<(String, String)>, line: &str, in_service: bool) {
    if !in_service {
        return;
    }
    if let Some((k, v)) = line.split_once('=') {
        out.push((k.trim().to_string(), v.trim().to_string()));
    }
}

/// Split an `Environment=` value into the `KEY=VALUE` pairs it carries.
///
/// One `Environment=` line may hold several space-separated assignments
/// (`Environment=A=1 B=2`) — the live korp drop-ins use one per line, but systemd
/// permits both and a scanner that missed the packed form would be a hole. Quoted
/// values keep their spaces.
pub fn split_env_assignments(value: &str) -> Vec<(String, String)> {
    let mut out = Vec::new();
    let mut cur = String::new();
    let mut quote: Option<char> = None;
    for ch in value.chars() {
        match quote {
            Some(q) => {
                if ch == q {
                    quote = None;
                } else {
                    cur.push(ch);
                }
            }
            None => match ch {
                '"' | '\'' => quote = Some(ch),
                c if c.is_whitespace() => {
                    if !cur.is_empty() {
                        out.push(std::mem::take(&mut cur));
                    }
                }
                c => cur.push(c),
            },
        }
    }
    if !cur.is_empty() {
        out.push(cur);
    }
    out.into_iter()
        .filter_map(|tok| {
            tok.split_once('=')
                .map(|(k, v)| (k.to_string(), v.to_string()))
        })
        .collect()
}

/// Every absolute path that appears inside an env VALUE (`/a/b`, and the path part
/// of a `unix:///a/b` URL). Used to spot a value that pins state into a home.
fn absolute_paths_in(value: &str) -> Vec<&str> {
    let mut out = Vec::new();
    let mut rest = value;
    while let Some(i) = rest.find('/') {
        let tail = &rest[i..];
        let end = tail
            .find(|c: char| c.is_whitespace() || c == ':' || c == ',')
            .unwrap_or(tail.len());
        let cand = &tail[..end];
        if cand.len() > 1 {
            out.push(cand);
        }
        rest = &tail[end..];
        if rest.is_empty() {
            break;
        }
        // step past the delimiter so `find('/')` cannot loop on the same index
        rest = &rest[1..];
    }
    out
}

/// **Do a declared and a drop-in resource ceiling mean the SAME size?**
///
/// By bytes, not by text: systemd takes `16G`, `16384M` and `17179869184` as one
/// number, and a scan that string-compared them would report a contradiction where
/// there is none — which is how a conflict report becomes noise an operator learns to
/// skip. An unparseable value on either side (`infinity`, a typo) is treated as
/// "different unless textually identical", because guessing what an unparseable
/// ceiling means is the one thing worse than naming it.
fn same_ceiling(declared: &str, dropin: &str) -> bool {
    if declared.trim() == dropin.trim() {
        return true;
    }
    match (
        crate::systemd::MemoryBound::parse_bytes(declared),
        crate::systemd::MemoryBound::parse_bytes(dropin),
    ) {
        (Some(a), Some(b)) => a == b,
        _ => false,
    }
}

/// **Do a declared and a drop-in OOM reaction mean the SAME thing?**
///
/// The same rule [`same_ceiling`] applies to sizes, one axis over. Text equality
/// first (so a spec and a drop-in that both say `stop` agree without a parse), then
/// by MEANING through [`crate::systemd::OomPolicy::parse`]. A value on either side
/// that systemd itself would refuse — `Stop`, `continu` — is treated as DIFFERENT
/// unless textually identical: a directive that would stop the unit from loading is
/// worth naming, and guessing what it meant is the one thing worse than naming it.
/// **Do a declared and a drop-in rate-limit number mean the SAME thing?**
///
/// By VALUE, not by text — `12`, `12s` and `12sec` are one number to systemd, and a
/// bare integer is seconds (`man systemd.time`). Only the plain-seconds spellings are
/// understood; anything richer (`1min`, `infinity`) falls back to a textual compare,
/// the same "guessing is worse than naming it" rule
/// [`crate::systemd::MemoryBound::parse_bytes`] applies to sizes.
fn same_start_limit(declared: &str, dropin: &str) -> bool {
    if declared.trim() == dropin.trim() {
        return true;
    }
    let secs = |t: &str| -> Option<u64> {
        let t = t.trim();
        let n = t
            .strip_suffix("sec")
            .or_else(|| t.strip_suffix('s'))
            .unwrap_or(t);
        n.trim().parse::<u64>().ok()
    };
    match (secs(declared), secs(dropin)) {
        (Some(a), Some(b)) => a == b,
        _ => false,
    }
}

fn same_oom_policy(declared: &str, dropin: &str) -> bool {
    if declared.trim() == dropin.trim() {
        return true;
    }
    match (
        crate::systemd::OomPolicy::parse(declared),
        crate::systemd::OomPolicy::parse(dropin),
    ) {
        (Some(a), Some(b)) => a == b,
        _ => false,
    }
}
/// **SCAN — read `dir` and report every drop-in directive that contradicts
/// `declared`.** Pure: it touches only `dir` and `passwd_path`, so a test points it
/// at a temp dir and a fixture passwd and exercises the whole rule set with no
/// root, no bus and no live `/etc`.
///
/// A missing `dir` is not an error — it is the normal case, and yields an empty
/// scan.
///
/// Rules, in the order systemd itself resolves them (last writer wins for a
/// scalar):
///
/// * `User=`/`Group=` that differ from the declaration;
/// * `MemoryHigh=`/`MemoryMax=` that differ from the declared ceiling, compared by
///   BYTES via [`same_ceiling`] so two spellings of one size are not a contradiction;
/// * `OOMPolicy=` that differs from the declared reaction, compared by MEANING via
///   [`same_oom_policy`];
/// * `StartLimitIntervalSec=`/`StartLimitBurst=` that differ from the declared rate
///   limit, compared as NUMBERS via [`same_start_limit`] (`12`, `12s` and `12sec` are
///   one value). These are `[Unit]` keys, not `[Service]` keys — see below;
/// * `OOMScoreAdjust=` that differs from the declared bias, compared as a NUMBER so
///   `+500` and `500` are one value;
/// * an `Environment=` key the spec also declares, with a different value;
/// * an `Environment=` value holding an absolute path under a real account's home
///   that is not the declared user's.
///
/// An EMPTY [`Declared::user`] means "this unit runs as whoever invoked it" (a
/// `Scope::User` install, which renders no `User=` at all). The foreign-home rule
/// is then off — a per-user service keeping state in its own operator's home is the
/// normal case — while a `User=` drop-in is still reported, because a `--user` unit
/// that tries to switch account is broken either way.
///
/// An empty declared CEILING, REACTION or RATE LIMIT is not the same stand-down:
/// those three mean "the spec declares none", and a drop-in that supplies one has
/// decided something the install did not — which is the `10-bounded.conf` case
/// exactly.
///
/// **Both `[Service]` AND `[Unit]` are read.** One service's behaviour is spread
/// across sections by systemd: `RestartSec=` is a `[Service]` key while the
/// `StartLimitIntervalSec=`/`StartLimitBurst=` it runs into are `[Unit]` keys. Until
/// the start-limit axis this scan parsed `[Service]` only, so a drop-in rewriting the
/// outcome of every failure was reported as a drop-in with nothing wrong in it.
pub fn scan_dropins_in(dir: &Path, declared: &Declared, passwd_path: &Path) -> DropInScan {
    let mut scan = DropInScan {
        dir: dir.to_path_buf(),
        ..Default::default()
    };

    let mut files: Vec<PathBuf> = match std::fs::read_dir(dir) {
        Ok(rd) => rd
            .filter_map(|e| e.ok())
            .map(|e| e.path())
            .filter(|p| p.extension().is_some_and(|x| x == "conf") && p.is_file())
            .collect(),
        Err(_) => return scan, // no dir ⇒ no drop-ins; the normal, quiet case
    };
    // systemd applies drop-ins in lexical order of file name; so do we, and the LAST
    // value of a scalar is the one that takes effect.
    files.sort();
    scan.files = files.clone();

    // The homes of real accounts, from the SAME passwd parser the chown/delete guard
    // reads (`systemd::passwd_homes_of`) — never a second, drifting notion of "home".
    let homes = crate::systemd::passwd_homes_of(passwd_path);
    let declared_home: Option<PathBuf> = homes
        .iter()
        .find(|(n, _, _)| *n == declared.user)
        .map(|(_, _, h)| h.clone());

    for file in &files {
        let Ok(text) = std::fs::read_to_string(file) else {
            continue;
        };
        // BOTH sections. systemd spreads one service's behaviour across them — the
        // restart cadence is a `[Service]` key and the rate limit it runs into is a
        // `[Unit]` key — so a `[Service]`-only scan reported the drop-in as present
        // and found nothing wrong inside it. Chained rather than merged so `[Service]`
        // keys keep their existing report order and no existing conflict moves.
        let directives = parse_service_directives(&text)
            .into_iter()
            .chain(parse_directives_in(&text, "[Unit]"));
        for (key, value) in directives {
            match key.as_str() {
                "User" if value != declared.user => {
                    scan.effective_user = Some(value.clone());
                    scan.conflicts.push(DropInConflict {
                        file: file.clone(),
                        key: "User".into(),
                        directive: format!("User={value}"),
                        declared: declared.user.clone(),
                        effective: value.clone(),
                        kind: ConflictKind::User,
                    });
                }
                "Group" if value != declared.group => {
                    scan.conflicts.push(DropInConflict {
                        file: file.clone(),
                        key: "Group".into(),
                        directive: format!("Group={value}"),
                        declared: declared.group.clone(),
                        effective: value.clone(),
                        kind: ConflictKind::Group,
                    });
                }
                // The RESOURCE CEILING. A hand-written `MemoryHigh=`/`MemoryMax=` is
                // exactly the artefact the 2026-08-20 incident produced, and once the
                // spec can DECLARE a ceiling, a drop-in that sets a different one is a
                // contradiction of the same class as `User=`: the install writes only
                // the fragment, so the uninstall leaves the drop-in behind to re-apply
                // to the next install.
                //
                // Compared by BYTES, never by text — `MemoryMax=24G` and
                // `MemoryMax=25769803776` are the same ceiling, and reporting those two
                // as a conflict would train an operator to ignore the report.
                "MemoryHigh" | "MemoryMax" => {
                    let declared_val = if key == "MemoryHigh" {
                        &declared.memory_high
                    } else {
                        &declared.memory_max
                    };
                    if !same_ceiling(declared_val, &value) {
                        scan.conflicts.push(DropInConflict {
                            file: file.clone(),
                            key: key.clone(),
                            directive: format!("{key}={value}"),
                            declared: declared_val.clone(),
                            effective: value.clone(),
                            kind: ConflictKind::MemoryCeiling,
                        });
                    }
                }
                // THE REACTION. A hand-written `OOMPolicy=` is the ceiling's other
                // half and is unowned in exactly the same way — and it is the harder
                // one to notice, because a wrong ceiling shows up as a service that
                // gets killed too early, while a wrong reaction shows up only on the
                // day it is actually killed.
                //
                // An EMPTY declaration is not "no opinion, so anything goes": the unit
                // inherits the manager's `DefaultOOMPolicy=` (measured `stop`), and a
                // drop-in that replaces that with `continue` has changed what happens
                // to the service under memory pressure without the install knowing.
                "OOMPolicy" => {
                    if !same_oom_policy(&declared.oom_policy, &value) {
                        scan.conflicts.push(DropInConflict {
                            file: file.clone(),
                            key: "OOMPolicy".into(),
                            directive: format!("OOMPolicy={value}"),
                            declared: declared.oom_policy.clone(),
                            effective: value.clone(),
                            kind: ConflictKind::OomPolicy,
                        });
                    }
                }
                // THE RATE LIMIT. Compared as NUMBERS, not text, for the same reason
                // the ceiling is compared as bytes: systemd accepts `12`, `12s` and
                // `12sec` as one value, and a scan that string-compared them would
                // report a contradiction where there is none.
                //
                // Reported even when the spec declares NOTHING, and that is the case
                // that matters most here — an undeclared spec inherits the box's 10s/5
                // window, so a drop-in supplying either number has decided between
                // "park" and "retry forever" on behalf of an install that never had an
                // opinion. That is precisely how dwarves-server ended up thrashing.
                "StartLimitIntervalSec" | "StartLimitBurst" => {
                    let declared_val = if key == "StartLimitIntervalSec" {
                        &declared.start_limit_interval
                    } else {
                        &declared.start_limit_burst
                    };
                    if !same_start_limit(declared_val, &value) {
                        scan.conflicts.push(DropInConflict {
                            file: file.clone(),
                            key: key.clone(),
                            directive: format!("{key}={value}"),
                            declared: declared_val.clone(),
                            effective: value.clone(),
                            kind: ConflictKind::StartLimit,
                        });
                    }
                }
                // WHICH unit the kernel picks. Compared as a NUMBER so `+500` and
                // `500` are one value. Reported against an empty declaration too: the
                // unit then sits at the manager's level with every other service, and
                // a drop-in moving it has re-ranked this service against its
                // neighbours — which is what nobody could see on 2026-08-20.
                "OOMScoreAdjust" => {
                    let same = declared.oom_score_adjust.trim() == value.trim()
                        || matches!(
                            (
                                declared.oom_score_adjust.trim().parse::<i32>(),
                                value.trim().parse::<i32>()
                            ),
                            (Ok(a), Ok(b)) if a == b
                        );
                    if !same {
                        scan.conflicts.push(DropInConflict {
                            file: file.clone(),
                            key: "OOMScoreAdjust".into(),
                            directive: format!("OOMScoreAdjust={value}"),
                            declared: declared.oom_score_adjust.clone(),
                            effective: value.clone(),
                            kind: ConflictKind::OomScoreAdjust,
                        });
                    }
                }
                "Environment" => {
                    for (k, v) in split_env_assignments(&value) {
                        // (a) the spec declares this key with a different value
                        if let Some((_, spec_v)) = declared.env.iter().find(|(dk, _)| *dk == k) {
                            if *spec_v != v {
                                scan.conflicts.push(DropInConflict {
                                    file: file.clone(),
                                    key: format!("Environment:{k}"),
                                    directive: format!("Environment={k}={v}"),
                                    declared: spec_v.clone(),
                                    effective: v.clone(),
                                    kind: ConflictKind::EnvOverride,
                                });
                                continue;
                            }
                        }
                        // (b) the value pins a path into somebody else's home.
                        // Skipped when no account is declared (a `Scope::User` unit
                        // runs as WHOEVER invoked it, so a path in that person's
                        // home is the normal case, not a contradiction).
                        if declared.user.is_empty() {
                            continue;
                        }
                        if let Some(owner) =
                            foreign_home_owner(&v, &homes, &declared.user, declared_home.as_deref())
                        {
                            scan.conflicts.push(DropInConflict {
                                file: file.clone(),
                                key: format!("Environment:{k}"),
                                directive: format!("Environment={k}={v}"),
                                declared: declared_home
                                    .as_ref()
                                    .map(|h| format!("a path under {}", h.display()))
                                    .unwrap_or_else(|| {
                                        format!("a path owned by `{}`", declared.user)
                                    }),
                                effective: format!("{v} (inside `{owner}`'s home)"),
                                kind: ConflictKind::ForeignHome,
                            });
                        }
                    }
                }
                _ => {}
            }
        }
    }
    scan
}

/// One installed unit, read off disk: what its fragment DECLARES and what its
/// `<unit>.service.d/` does to that. The read-only diagnostic form of the check
/// [`install_systemd_scoped`](crate::systemd::install_systemd_scoped) runs inline.
#[derive(Debug, Clone)]
pub struct UnitAudit {
    /// `<unit>.service` — the name systemd knows it by.
    pub unit_file_name: String,
    /// The fragment that was read.
    pub unit_path: PathBuf,
    /// What the fragment declares (`User=`, `Group=`, inline `Environment=`).
    pub declared: Declared,
    /// What its drop-in dir does to that.
    pub scan: DropInScan,
}

/// **Audit an INSTALLED unit — read its fragment, then scan its drop-in dir against
/// it.** Read-only: it opens two paths and writes nothing.
///
/// `Ok(None)` when `unit_dir/<unit_file_name>` does not exist — a unit this scope
/// does not have is not an error, it is simply not ours to audit.
pub fn audit_unit_in(
    unit_dir: &Path,
    unit_file_name: &str,
    passwd_path: &Path,
) -> std::io::Result<Option<UnitAudit>> {
    let unit_path = unit_dir.join(unit_file_name);
    if !unit_path.is_file() {
        return Ok(None);
    }
    let declared = declared_from_unit_file(&unit_path)?;
    let scan = scan_dropins_in(
        &dropin_dir(unit_dir, unit_file_name),
        &declared,
        passwd_path,
    );
    Ok(Some(UnitAudit {
        unit_file_name: unit_file_name.to_string(),
        unit_path,
        declared,
        scan,
    }))
}

/// Every `*.service` fragment in `unit_dir` that is a REGULAR FILE, sorted.
///
/// Symlinks are skipped deliberately: `/etc/systemd/system` is full of enablement
/// links into `/usr/lib/systemd/system` (and `→ /dev/null` masks), and neither is a
/// unit an installer wrote there.
pub fn unit_files_in(unit_dir: &Path) -> Vec<String> {
    let Ok(rd) = std::fs::read_dir(unit_dir) else {
        return Vec::new();
    };
    let mut names: Vec<String> = rd
        .filter_map(|e| e.ok())
        .filter(|e| {
            // `file_type()` does NOT follow the link, which is the point.
            e.file_type().map(|t| t.is_file()).unwrap_or(false)
        })
        .filter_map(|e| e.file_name().into_string().ok())
        .filter(|n| n.ends_with(".service"))
        .collect();
    names.sort();
    names
}

/// The account whose home contains an absolute path in `value`, when that account
/// is NOT `declared_user`. `None` when the value holds no such path.
///
/// Deliberately conservative: only a path under a home listed in passwd counts, and
/// the declared user's own home never does. `/var/lib/foo` is not a conflict.
fn foreign_home_owner(
    value: &str,
    homes: &[(String, u32, PathBuf)],
    declared_user: &str,
    declared_home: Option<&Path>,
) -> Option<String> {
    for cand in absolute_paths_in(value) {
        let p = Path::new(cand);
        if declared_home.is_some_and(|h| p.starts_with(h)) {
            continue;
        }
        for (name, _, home) in homes {
            if name == declared_user {
                continue;
            }
            // A home of `/` or `/nonexistent` is not a real home and would match
            // everything / nothing useful.
            if home.parent().is_none() || !home.is_absolute() {
                continue;
            }
            if p.starts_with(home) && p != home.as_path() {
                return Some(name.clone());
            }
        }
    }
    None
}

// ─────────────────────────────────── tests ───────────────────────────────────

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

    /// The five drop-ins that were live in `/etc/systemd/system/korp-server.service.d/`
    /// on oden, 2026-08-03, verbatim. This is the RED fixture: the whole point of the
    /// module is that these files must not be able to sit silently under an install
    /// that prints `user korp`.
    const ROOTLESS_PODMAN: &str = "\
# Run korp-server as `alice` so its infra-autostart (draupnir) can drive the
# ROOTLESS podman engine that owns the korp zone containers.
[Service]
User=alice
Group=alice
Environment=XDG_RUNTIME_DIR=/run/user/1000
Environment=DOCKER_HOST=unix:///run/user/1000/podman/podman.sock
";
    const CASES: &str = "\
[Service]
Environment=KORP_CASES=/home/alice/.korp/korp_cases.redb
Environment=KORP_STATE=/home/alice/.korp/korp_server_state.json
";
    const ADDR: &str = "[Service]\nEnvironment=KORP_SERVER_ADDR=0.0.0.0:50051\n";
    const MINIO: &str = "\
[Service]
Environment=KORP_MINIO_ENDPOINT=http://localhost:9000
Environment=KORP_MINIO_ACCESS_KEY=minioadmin
";
    const REPACK: &str = "[Service]\nEnvironment=KORP_MAP_REPACK=sweden\n";

    const PASSWD: &str = "\
root:x:0:0:root:/root:/bin/bash
alice:x:1000:1000:Alice:/home/alice:/bin/bash
korp:x:111:111::/home/korp:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
";

    fn korp_declared() -> Declared {
        Declared {
            user: "korp".into(),
            group: "korp".into(),
            // No ceiling declared — the state every unit was in before the memory axis
            // existed, and the state the 2026-08-20 drop-in was written against.
            memory_high: String::new(),
            memory_max: String::new(),
            // No reaction declared either — the unit inherits the box's
            // `DefaultOOMPolicy=`, which is the state every unit was in before the
            // axis existed.
            oom_policy: String::new(),
            // No rate limit declared — the state EVERY unit shipped by this crate is
            // in until a spec opts in, and the state dwarves-server was in for all
            // 217830 of its restarts.
            start_limit_interval: String::new(),
            start_limit_burst: String::new(),
            oom_score_adjust: String::new(),
            env: vec![("KORP_SERVER_ADDR".into(), "127.0.0.1:50051".into())],
        }
    }

    struct Bed {
        _tmp: tempfile::TempDir,
        dir: PathBuf,
        passwd: PathBuf,
    }

    fn bed(files: &[(&str, &str)]) -> Bed {
        let tmp = tempfile::tempdir().expect("tempdir");
        let dir = tmp.path().join("korp-server.service.d");
        std::fs::create_dir_all(&dir).unwrap();
        for (name, body) in files {
            std::fs::write(dir.join(name), body).unwrap();
        }
        let passwd = tmp.path().join("passwd");
        std::fs::write(&passwd, PASSWD).unwrap();
        Bed {
            _tmp: tmp,
            dir,
            passwd,
        }
    }

    fn oden_bed() -> Bed {
        bed(&[
            ("20-repack.conf", REPACK),
            ("addr.conf", ADDR),
            ("cases.conf", CASES),
            ("minio.conf", MINIO),
            ("rootless-podman.conf", ROOTLESS_PODMAN),
        ])
    }

    /// ★ RED — the live oden drop-in set, scanned against the spec korp installs.
    /// Every directive that decided the 2026-08-03 incident must be named.
    #[test]
    fn oden_dropins_contradict_the_korp_spec() {
        let b = oden_bed();
        let scan = scan_dropins_in(&b.dir, &korp_declared(), &b.passwd);

        assert!(scan.has_dropins(), "five .conf files must be seen");
        assert_eq!(scan.files.len(), 5);
        assert!(scan.has_conflicts(), "the install MUST NOT look clean here");

        // The one that moved $HOME and therefore the whole data root.
        let user_c = scan
            .conflicts
            .iter()
            .find(|c| c.kind == ConflictKind::User)
            .expect("the User= override must be caught");
        assert!(
            user_c.file.ends_with("rootless-podman.conf"),
            "named the wrong file"
        );
        assert_eq!(user_c.directive, "User=alice");
        assert_eq!(user_c.declared, "korp");
        assert_eq!(user_c.effective, "alice");
        assert_eq!(scan.effective_user.as_deref(), Some("alice"));

        assert!(
            scan.conflicts
                .iter()
                .any(|c| c.kind == ConflictKind::Group && c.directive == "Group=alice"),
            "the Group= override must be caught"
        );

        // The absolute paths into ANOTHER account's home — the actual data-root bug.
        let foreign: Vec<&str> = scan
            .conflicts
            .iter()
            .filter(|c| c.kind == ConflictKind::ForeignHome)
            .map(|c| c.key.as_str())
            .collect();
        assert!(
            foreign.contains(&"Environment:KORP_CASES")
                && foreign.contains(&"Environment:KORP_STATE"),
            "both /home/alice/.korp pins must be caught, got {foreign:?}"
        );
        assert!(
            scan.conflicts
                .iter()
                .any(|c| c.kind == ConflictKind::ForeignHome && c.file.ends_with("cases.conf")),
            "the foreign-home conflict must name cases.conf"
        );

        // The listen address the spec rendered, silently replaced.
        let addr = scan
            .conflicts
            .iter()
            .find(|c| c.key == "Environment:KORP_SERVER_ADDR")
            .expect("the addr override must be caught");
        assert_eq!(addr.kind, ConflictKind::EnvOverride);
        assert_eq!(addr.declared, "127.0.0.1:50051");
        assert_eq!(addr.effective, "0.0.0.0:50051");

        // The report an operator reads must carry file + directive + winner.
        let rep = scan.conflict_report("korp-server");
        assert!(
            rep.contains("rootless-podman.conf"),
            "report hides the file: {rep}"
        );
        assert!(
            rep.contains("User=alice"),
            "report hides the directive: {rep}"
        );
        assert!(rep.contains("cases.conf"), "report hides cases.conf: {rep}");
        assert!(
            rep.contains("KEPT"),
            "report must say the drop-ins were kept, not deleted: {rep}"
        );
    }

    /// ★ GREEN — the SAME scanner, the SAME spec, with the drop-in dir gone. The
    /// guard has to be able to say "clean", or it is just noise.
    #[test]
    fn no_dropin_dir_is_clean_and_says_nothing() {
        let tmp = tempfile::tempdir().unwrap();
        let passwd = tmp.path().join("passwd");
        std::fs::write(&passwd, PASSWD).unwrap();
        let scan = scan_dropins_in(
            &tmp.path().join("absent.service.d"),
            &korp_declared(),
            &passwd,
        );
        assert!(!scan.has_dropins());
        assert!(!scan.has_conflicts());
        assert_eq!(
            scan.effective_user, None,
            "no override ⇒ the spec's user stands"
        );
        assert_eq!(scan.conflict_report("korp-server"), "");
        assert_eq!(scan.kept_report("korp-server"), "");
    }

    /// ★ GREEN — a drop-in dir that exists but only ADDS is not a conflict. Without
    /// this the check would fire on every extended unit and be turned off.
    #[test]
    fn additive_dropins_are_not_conflicts_but_are_still_named() {
        let b = bed(&[
            ("20-repack.conf", REPACK),
            ("minio.conf", MINIO),
            (
                "extra.conf",
                "[Service]\nEnvironment=KORP_EXTRA=/var/lib/korp/extra\n",
            ),
        ]);
        let scan = scan_dropins_in(&b.dir, &korp_declared(), &b.passwd);
        assert!(scan.has_dropins());
        assert!(
            !scan.has_conflicts(),
            "additive-only drop-ins must be quiet, got {:?}",
            scan.conflicts
        );
        // …but the UNINSTALL still names them, because they re-arm the next install.
        let kept = scan.kept_report("korp-server");
        assert!(
            kept.contains("extra.conf"),
            "uninstall must name what it kept: {kept}"
        );
        assert!(
            kept.contains("NEXT install"),
            "uninstall must say why it matters: {kept}"
        );
    }

    /// A drop-in that re-states the spec's OWN values contradicts nothing — the
    /// identity-value trap, checked deliberately.
    #[test]
    fn a_dropin_restating_the_spec_is_not_a_conflict() {
        let b = bed(&[(
            "same.conf",
            "[Service]\nUser=korp\nGroup=korp\nEnvironment=KORP_SERVER_ADDR=127.0.0.1:50051\n",
        )]);
        let scan = scan_dropins_in(&b.dir, &korp_declared(), &b.passwd);
        assert!(!scan.has_conflicts(), "got {:?}", scan.conflicts);
        assert_eq!(scan.effective_user, None);
    }

    /// The declared user's OWN home is not a foreign home — a system service whose
    /// env legitimately points at `/home/korp/...` must not be flagged.
    #[test]
    fn the_declared_users_own_home_is_not_foreign() {
        let b = bed(&[(
            "own.conf",
            "[Service]\nEnvironment=KORP_CASES=/home/korp/.korp/korp_cases.redb\n",
        )]);
        let scan = scan_dropins_in(&b.dir, &korp_declared(), &b.passwd);
        assert!(!scan.has_conflicts(), "got {:?}", scan.conflicts);
    }

    /// Lexical order decides: the LAST `User=` is the effective one, exactly as
    /// systemd merges them.
    #[test]
    fn the_last_user_override_wins_like_systemd() {
        let b = bed(&[
            ("10-a.conf", "[Service]\nUser=nobody\n"),
            ("90-z.conf", "[Service]\nUser=alice\n"),
        ]);
        let scan = scan_dropins_in(&b.dir, &korp_declared(), &b.passwd);
        assert_eq!(scan.effective_user.as_deref(), Some("alice"));
        assert_eq!(
            scan.conflicts.len(),
            2,
            "both are reported, the last one wins"
        );
    }

    /// Parsing is not a `contains("User=")` — comments, other sections and
    /// continuations must all be handled, or the check is fooled by its own input.
    #[test]
    fn parser_ignores_comments_and_other_sections_and_joins_continuations() {
        let text = "\
[Unit]
Description=not a service directive
# User=commented-out
[Service]
; also a comment
User=alice
ExecStart=/usr/bin/thing \\
  --flag one \\
  --flag two
Environment=A=1 B=2
";
        let d = parse_service_directives(text);
        assert!(
            !d.iter().any(|(k, _)| k == "Description"),
            "a [Unit] directive leaked into [Service]"
        );
        assert!(!d.iter().any(|(_, v)| v.contains("commented-out")));
        assert_eq!(
            d.iter().find(|(k, _)| k == "User").map(|(_, v)| v.as_str()),
            Some("alice")
        );
        let exec = d
            .iter()
            .find(|(k, _)| k == "ExecStart")
            .expect("ExecStart")
            .1
            .clone();
        assert!(
            exec.contains("--flag one") && exec.contains("--flag two"),
            "continuation not joined: {exec}"
        );
        let env = d
            .iter()
            .find(|(k, _)| k == "Environment")
            .unwrap()
            .1
            .clone();
        assert_eq!(
            split_env_assignments(&env),
            vec![
                ("A".to_string(), "1".to_string()),
                ("B".to_string(), "2".to_string())
            ],
            "a packed Environment= line must split into both assignments"
        );
    }

    /// A packed `Environment=` line hides a foreign-home pin behind a harmless one —
    /// the scanner must still find it.
    #[test]
    fn a_packed_environment_line_cannot_hide_a_foreign_home() {
        let b = bed(&[(
            "packed.conf",
            "[Service]\nEnvironment=SAFE=/var/lib/korp KORP_CASES=/home/alice/.korp/c.redb\n",
        )]);
        let scan = scan_dropins_in(&b.dir, &korp_declared(), &b.passwd);
        assert_eq!(scan.conflicts.len(), 1, "got {:?}", scan.conflicts);
        assert_eq!(scan.conflicts[0].key, "Environment:KORP_CASES");
        assert_eq!(scan.conflicts[0].kind, ConflictKind::ForeignHome);
    }

    /// The `korp-server.service` fragment that was live in `/etc/systemd/system` on
    /// oden, 2026-08-03, verbatim. The audit seam has to reconstruct korp's
    /// declaration from THIS, with no `SystemdServiceSpec` in hand.
    const KORP_FRAGMENT: &str = "\
[Unit]
Description=korp-server service
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=korp
Group=korp
WorkingDirectory=/home/korp/.korp/software/current/korp-server
Environment=DATA_DIR=/home/korp/.korp/data/korp-server
Environment=LOG_DIR=/home/korp/.korp/log/korp-server
Environment=KORP_SERVER_DATA=/home/korp/.korp/data/korp-server
Environment=KORP_SERVER_ADDR=127.0.0.1:50051
ExecStart=/home/korp/.korp/software/current/korp-server/korp-server
Restart=on-failure
RestartSec=2
NoNewPrivileges=true

[Install]
WantedBy=multi-user.target
";

    /// ★ RED — the audit seam, run on the REAL oden pair (fragment + the three
    /// drop-ins that were live), must reach the SAME verdict the install-time check
    /// reaches. Before `declared_from_unit_file` existed there was no way to ask this
    /// question at all without re-running the install.
    #[test]
    fn auditing_the_live_korp_unit_finds_the_override_without_a_spec() {
        let tmp = tempfile::tempdir().unwrap();
        let unit_dir = tmp.path().join("system");
        std::fs::create_dir_all(&unit_dir).unwrap();
        std::fs::write(unit_dir.join("korp-server.service"), KORP_FRAGMENT).unwrap();
        let d = unit_dir.join("korp-server.service.d");
        std::fs::create_dir_all(&d).unwrap();
        std::fs::write(d.join("addr.conf"), ADDR).unwrap();
        std::fs::write(d.join("cases.conf"), CASES).unwrap();
        std::fs::write(d.join("rootless-podman.conf"), ROOTLESS_PODMAN).unwrap();
        let passwd = tmp.path().join("passwd");
        std::fs::write(&passwd, PASSWD).unwrap();

        let audit = audit_unit_in(&unit_dir, "korp-server.service", &passwd)
            .expect("read")
            .expect("the fragment is there");

        // Reconstructed declaration == what korp's own spec renders.
        assert_eq!(audit.declared.user, "korp");
        assert_eq!(audit.declared.group, "korp");
        assert_eq!(
            audit
                .declared
                .env
                .iter()
                .find(|(k, _)| k == "KORP_SERVER_ADDR")
                .map(|(_, v)| v.as_str()),
            Some("127.0.0.1:50051"),
            "the fragment's own addr must be read, or the ENV OVERRIDE cannot be seen"
        );

        // …and therefore the same four contradictions the install-time check names.
        assert_eq!(audit.scan.effective_user.as_deref(), Some("alice"));
        let kinds: Vec<ConflictKind> = audit.scan.conflicts.iter().map(|c| c.kind).collect();
        assert!(kinds.contains(&ConflictKind::User), "{kinds:?}");
        assert!(kinds.contains(&ConflictKind::Group), "{kinds:?}");
        assert!(kinds.contains(&ConflictKind::EnvOverride), "{kinds:?}");
        assert_eq!(
            kinds
                .iter()
                .filter(|k| **k == ConflictKind::ForeignHome)
                .count(),
            2,
            "both /home/alice/.korp pins: {:?}",
            audit.scan.conflicts
        );
    }

    /// ★ GREEN — the SAME audit on the SAME fragment with the drop-in dir absent is
    /// silent. Without this the verb is a rubber stamp that always says RED.
    #[test]
    fn auditing_a_unit_with_no_dropins_is_clean() {
        let tmp = tempfile::tempdir().unwrap();
        let unit_dir = tmp.path().join("system");
        std::fs::create_dir_all(&unit_dir).unwrap();
        std::fs::write(unit_dir.join("korp-server.service"), KORP_FRAGMENT).unwrap();
        let passwd = tmp.path().join("passwd");
        std::fs::write(&passwd, PASSWD).unwrap();

        let audit = audit_unit_in(&unit_dir, "korp-server.service", &passwd)
            .unwrap()
            .unwrap();
        assert!(!audit.scan.has_dropins());
        assert!(!audit.scan.has_conflicts(), "{:?}", audit.scan.conflicts);

        // A unit that is not installed here is not an error — it is None.
        assert!(
            audit_unit_in(&unit_dir, "absent.service", &passwd)
                .unwrap()
                .is_none()
        );
    }

    /// **THE 2026-08-20 ARTEFACT ITSELF — a hand-written resource ceiling is now a
    /// named conflict instead of an invisible one.**
    ///
    /// `/etc/systemd/system/dwarves-server.service.d/10-bounded.conf` exists because
    /// `ServiceSpec` had no memory axis: `MemoryHigh=16G` / `MemoryMax=24G` had nowhere
    /// else to live after dwarves-server hit 33 GB RSS and took the box down with
    /// `gunnar-service`. Now that the axis exists, that file is exactly the shape this
    /// module was built for — a setting the install does not write, the uninstall does
    /// not remove, and every `--refresh` renders around without knowing it is there.
    ///
    /// RED direction: drop the `"MemoryHigh" | "MemoryMax"` arm from `scan_dropins_in`
    /// and the scan reports the drop-in as harmless (`has_conflicts() == false`) while
    /// the drop-in still decides what the service's real ceiling is — the exact silent
    /// override the module exists to end.
    #[test]
    fn a_hand_written_memory_ceiling_is_named_as_a_conflict() {
        let b = bed(&[(
            "10-bounded.conf",
            "[Service]\nMemoryHigh=16G\nMemoryMax=24G\n",
        )]);
        // The declaration as it stood on 2026-08-20: no ceiling of any kind.
        let scan = scan_dropins_in(&b.dir, &korp_declared(), &b.passwd);

        assert_eq!(
            scan.conflicts.len(),
            2,
            "both ceilings are separate facts and both are unowned:\n{:?}",
            scan.conflicts
        );
        let report = scan.conflict_report("korp-server");
        assert!(
            report.contains("MEMORY CEILING") && report.contains("16G") && report.contains("24G"),
            "the report must name the setting AND both numbers:\n{report}"
        );
        assert!(
            scan.conflicts
                .iter()
                .all(|c| c.kind == ConflictKind::MemoryCeiling),
            "a ceiling is not a user/env override:\n{:?}",
            scan.conflicts
        );
    }

    /// **Ceilings are compared by BYTES, never by text.**
    ///
    /// systemd reads `24G`, `24576M` and `25769803776` as one number. A scan that
    /// string-compared them would report a contradiction where there is none, and a
    /// conflict report with false positives in it is a report operators learn to skip
    /// — which costs exactly the signal this module exists to produce.
    ///
    /// RED direction: replace `same_ceiling` with `declared == value` and the
    /// "agreeing spelling" half fails while the "genuinely different" half still passes
    /// — a guard that fires on everything is as useless as one that fires on nothing.
    #[test]
    fn the_same_ceiling_in_another_spelling_is_not_a_conflict() {
        let mut declared = korp_declared();
        declared.memory_high = "16G".into();
        declared.memory_max = "24G".into();

        // A drop-in that AGREES, spelled in bytes and in MiB.
        let agreeing = bed(&[(
            "10-bounded.conf",
            "[Service]\nMemoryHigh=16384M\nMemoryMax=25769803776\n",
        )]);
        let scan = scan_dropins_in(&agreeing.dir, &declared, &agreeing.passwd);
        assert!(
            !scan.has_conflicts(),
            "same number, different spelling — not a contradiction:\n{:?}",
            scan.conflicts
        );
        assert!(
            scan.has_dropins(),
            "…but the drop-in is still NAMED as present"
        );

        // A drop-in that genuinely raises the hard ceiling IS a conflict.
        let raising = bed(&[("10-bounded.conf", "[Service]\nMemoryMax=48G\n")]);
        let scan = scan_dropins_in(&raising.dir, &declared, &raising.passwd);
        assert_eq!(scan.conflicts.len(), 1, "{:?}", scan.conflicts);
        assert_eq!(scan.conflicts[0].declared, "24G");
        assert_eq!(scan.conflicts[0].effective, "48G");
    }

    /// **The audit could not read `[Unit]` at all, so a whole section was unaudited.**
    ///
    /// `parse_service_directives` filtered to `[Service]`, on the reasoning — written
    /// in its own doc comment — that "a `[Unit]` `After=` cannot contradict `User=`".
    /// That held for the rules of the day and stopped holding the moment an axis
    /// landed whose keys live in `[Unit]`. The scan would list the drop-in file as
    /// PRESENT and find nothing wrong inside it, which reads as a clean audit.
    ///
    /// RED direction: point the scan back at `parse_service_directives` alone and
    /// `a_hand_written_start_limit_is_named_as_a_conflict` finds zero conflicts in a
    /// file that rewrites the outcome of every failure.
    #[test]
    fn directives_are_read_from_unit_as_well_as_service() {
        let text = "[Unit]\nStartLimitBurst=99\nAfter=network.target\n\
                    \n[Service]\nRestartSec=2\n";

        let svc = parse_service_directives(text);
        assert_eq!(svc, vec![("RestartSec".to_string(), "2".to_string())]);

        let unit = parse_directives_in(text, "[Unit]");
        assert_eq!(
            unit,
            vec![
                ("StartLimitBurst".to_string(), "99".to_string()),
                ("After".to_string(), "network.target".to_string()),
            ]
        );

        // Section matching is case-insensitive, as systemd's is.
        assert_eq!(parse_directives_in("[unit]\nAfter=x\n", "[Unit]").len(), 1);
        // …and continuation handling is not lost by the generalisation.
        let cont = parse_directives_in("[Unit]\nAfter=a.target \\\n  b.target\n", "[Unit]");
        assert_eq!(cont[0].1, "a.target b.target");
    }

    /// **A hand-written OOM SCORE re-ranks this service against its NEIGHBOURS.**
    ///
    /// The only one of the four axes whose drop-in changes something about OTHER
    /// services: moving this unit's score decides who the kernel reaches for when the
    /// box is short, and none of the affected services can see the file that did it.
    /// Exactly the 2026-08-20 shape, where every unit sat at the same score and
    /// `gunnar-service` went down with the forge.
    #[test]
    fn a_hand_written_oom_score_is_named_as_a_conflict() {
        // Undeclared spec ⇒ the unit sits at the manager's level with everything else,
        // so a drop-in moving it is an override worth naming.
        let declared = korp_declared();
        let b = bed(&[("10-rank.conf", "[Service]\nOOMScoreAdjust=-900\n")]);
        let scan = scan_dropins_in(&b.dir, &declared, &b.passwd);
        assert_eq!(scan.conflicts.len(), 1, "{:?}", scan.conflicts);
        assert_eq!(scan.conflicts[0].kind, ConflictKind::OomScoreAdjust);
        assert_eq!(scan.conflicts[0].effective, "-900");
        assert_eq!(ConflictKind::OomScoreAdjust.label(), "OOM SCORE");

        // Declared, and the drop-in agrees in another legal spelling — not a conflict.
        let mut declared = korp_declared();
        declared.oom_score_adjust = "500".into();
        let b = bed(&[("10-rank.conf", "[Service]\nOOMScoreAdjust=+500\n")]);
        let scan = scan_dropins_in(&b.dir, &declared, &b.passwd);
        assert!(
            !scan.has_conflicts(),
            "`+500` is the declared 500: {:?}",
            scan.conflicts
        );
        assert!(
            scan.has_dropins(),
            "…but the drop-in is still NAMED as present"
        );

        // A genuine re-rank IS a conflict, and names both sides.
        let b = bed(&[("10-rank.conf", "[Service]\nOOMScoreAdjust=-1000\n")]);
        let scan = scan_dropins_in(&b.dir, &declared, &b.passwd);
        assert_eq!(scan.conflicts.len(), 1);
        assert_eq!(scan.conflicts[0].declared, "500");
        assert_eq!(scan.conflicts[0].effective, "-1000");
    }

    /// **A hand-written START LIMIT is the same story, with the worst blast radius.**
    ///
    /// The ceiling and the reaction change how a running service behaves. This one
    /// changes what happens when it FAILS — the same spec, with one number moved by an
    /// operator, flips between "parks in `failed` after five tries" and "retries
    /// forever". Nothing looks different until the day it starts failing, and then it
    /// does the opposite of what the spec says.
    ///
    /// The empty-declaration case is the one that actually bit: with no declared limit
    /// the unit inherits the box's `10s`/`5`, so a drop-in supplying either number has
    /// decided the outcome on behalf of an install that never had an opinion. That is
    /// exactly the state `dwarves-server.service` was in for 217830 restarts.
    ///
    /// RED direction: drop the `StartLimitIntervalSec | StartLimitBurst` arm from
    /// `scan_dropins_in` and every assertion here fails while the unit still installs.
    #[test]
    fn a_hand_written_start_limit_is_named_as_a_conflict() {
        // (a) UNDECLARED spec, drop-in supplies a window — an override worth naming.
        let declared = korp_declared();
        let bed_a = bed(&[("10-nolimit.conf", "[Unit]\nStartLimitIntervalSec=0\n")]);
        let scan = scan_dropins_in(&bed_a.dir, &declared, &bed_a.passwd);
        assert_eq!(scan.conflicts.len(), 1, "{:?}", scan.conflicts);
        assert_eq!(scan.conflicts[0].kind, ConflictKind::StartLimit);
        assert_eq!(scan.conflicts[0].declared, "");
        assert_eq!(scan.conflicts[0].effective, "0");
        assert!(
            scan.conflicts[0]
                .directive
                .contains("StartLimitIntervalSec=0"),
            "the report must say WHICH of the two keys was written: {:?}",
            scan.conflicts[0]
        );

        // (b) DECLARED, and the drop-in agrees in another legal spelling. systemd reads
        // a bare integer as seconds, so `12`, `12s` and `12sec` are one value — a scan
        // that string-compared them would cry wolf.
        let mut declared = korp_declared();
        declared.start_limit_interval = "12".into();
        declared.start_limit_burst = "5".into();
        for spelling in ["12", "12s", "12sec"] {
            let b = bed(&[(
                "10-limit.conf",
                &format!("[Unit]\nStartLimitIntervalSec={spelling}\nStartLimitBurst=5\n"),
            )]);
            let scan = scan_dropins_in(&b.dir, &declared, &b.passwd);
            assert!(
                !scan.has_conflicts(),
                "`{spelling}` is the declared 12s in another spelling:\n{:?}",
                scan.conflicts
            );
            assert!(
                scan.has_dropins(),
                "…but the drop-in is still NAMED as present"
            );
        }

        // (c) A drop-in that flips the OUTCOME is a conflict, and both keys report.
        let b = bed(&[(
            "10-limit.conf",
            "[Unit]\nStartLimitIntervalSec=0\nStartLimitBurst=99\n",
        )]);
        let scan = scan_dropins_in(&b.dir, &declared, &b.passwd);
        assert_eq!(scan.conflicts.len(), 2, "{:?}", scan.conflicts);
        assert!(
            scan.conflicts
                .iter()
                .all(|c| c.kind == ConflictKind::StartLimit)
        );
        assert_eq!(ConflictKind::StartLimit.label(), "START LIMIT");
    }

    /// **The SAME story one axis over — a hand-written OOM REACTION.**
    ///
    /// `10-bounded.conf` was written because the ceiling had nowhere to live. The
    /// reaction to hitting that ceiling had nowhere to live either, so the next
    /// hand-written drop-in is `OOMPolicy=continue` — invisible to the install for
    /// exactly the same three reasons: the install writes only the fragment, the
    /// uninstall leaves the drop-in, and every `--refresh` renders around it.
    ///
    /// RED direction: drop the `"OOMPolicy"` arm from `scan_dropins_in` and a drop-in
    /// that changes what happens to the unit when the kernel reaps it reports as
    /// harmless.
    #[test]
    fn a_hand_written_oom_policy_is_named_as_a_conflict() {
        let b = bed(&[("20-oom.conf", "[Service]\nOOMPolicy=continue\n")]);
        // The declaration as it stands with no reaction declared: whatever
        // DefaultOOMPolicy= is on this box (MEASURED `stop`, systemd 259, t14s).
        let scan = scan_dropins_in(&b.dir, &korp_declared(), &b.passwd);

        assert_eq!(
            scan.conflicts.len(),
            1,
            "the reaction is unowned and must be named:\n{:?}",
            scan.conflicts
        );
        assert_eq!(scan.conflicts[0].kind, ConflictKind::OomPolicy);
        let report = scan.conflict_report("korp-server");
        assert!(
            report.contains("OOM POLICY") && report.contains("continue"),
            "the report must name the setting AND the value taking effect:\n{report}"
        );
    }

    /// **A reaction is compared by MEANING, not by text — and an unparseable one is
    /// never guessed at.**
    ///
    /// Same rule `same_ceiling` applies to sizes: a drop-in that repeats the declared
    /// policy is not a contradiction, a drop-in that changes it is, and a value
    /// systemd itself would refuse (`OOMPolicy=Stop` — its parser is case-sensitive)
    /// is reported rather than silently read as agreeing.
    ///
    /// RED direction: compare with `==` on the raw strings and the "agrees" half still
    /// passes while a spec-declared `stop` vs a drop-in `stop` with trailing
    /// whitespace goes red for nothing.
    #[test]
    fn an_oom_policy_that_repeats_the_declaration_is_not_a_conflict() {
        let mut declared = korp_declared();
        declared.oom_policy = "stop".into();

        let agreeing = bed(&[("20-oom.conf", "[Service]\nOOMPolicy=stop\n")]);
        let scan = scan_dropins_in(&agreeing.dir, &declared, &agreeing.passwd);
        assert!(
            !scan.has_conflicts(),
            "the same reaction, repeated, is not a contradiction:\n{:?}",
            scan.conflicts
        );
        assert!(
            scan.has_dropins(),
            "…but the drop-in is still NAMED as present"
        );

        let differing = bed(&[("20-oom.conf", "[Service]\nOOMPolicy=kill\n")]);
        let scan = scan_dropins_in(&differing.dir, &declared, &differing.passwd);
        assert_eq!(scan.conflicts.len(), 1, "{:?}", scan.conflicts);
        assert_eq!(scan.conflicts[0].declared, "stop");
        assert_eq!(scan.conflicts[0].effective, "kill");

        // A value systemd would refuse is NOT quietly treated as agreeing with the
        // declaration — guessing what a broken directive means is worse than naming it.
        let broken = bed(&[("20-oom.conf", "[Service]\nOOMPolicy=Stop\n")]);
        let scan = scan_dropins_in(&broken.dir, &declared, &broken.passwd);
        assert_eq!(scan.conflicts.len(), 1, "{:?}", scan.conflicts);
    }

    /// A fragment's OWN reaction is read back out of it, exactly as its ceiling is,
    /// so a spec that declares `OOMPolicy=` and a drop-in that repeats it do not
    /// produce a phantom conflict.
    #[test]
    fn a_fragment_carries_its_own_oom_policy_into_the_comparison() {
        let d = declared_from_unit_text(
            "[Service]\nUser=dwarves\nOOMPolicy=continue\nExecStart=/bin/true\n",
        );
        assert_eq!(d.oom_policy, "continue");
        // A unit that declares no reaction gets the manager's DefaultOOMPolicy= —
        // which is what makes ANY drop-in reaction an override worth naming.
        let none = declared_from_unit_text("[Service]\nUser=dwarves\nExecStart=/bin/true\n");
        assert!(none.oom_policy.is_empty());
    }

    /// A fragment's OWN ceiling is read back out of it, so a spec that declares
    /// `MemoryHigh=`/`MemoryMax=` and a drop-in that repeats them do not produce a
    /// phantom conflict — the same reason `Group=` defaults to `User=` above.
    #[test]
    fn a_fragment_carries_its_own_declared_ceiling_into_the_comparison() {
        let d = declared_from_unit_text(
            "[Service]\nUser=dwarves\nMemoryHigh=16G\nMemoryMax=24G\nExecStart=/bin/true\n",
        );
        assert_eq!(d.memory_high, "16G");
        assert_eq!(d.memory_max, "24G");
        // A unit with no ceiling declares none — which is what makes ANY drop-in
        // ceiling an override worth naming.
        let none = declared_from_unit_text("[Service]\nUser=dwarves\nExecStart=/bin/true\n");
        assert!(none.memory_high.is_empty() && none.memory_max.is_empty());
    }
    /// A fragment that names only `User=` must NOT report a phantom GROUP OVERRIDE:
    /// systemd defaults `Group=` to the user's primary group, and the reconstruction
    /// has to default it the same way or every such unit goes red for nothing.
    #[test]
    fn a_fragment_without_group_defaults_to_the_user_not_to_empty() {
        let d = declared_from_unit_text("[Service]\nUser=korp\nExecStart=/bin/true\n");
        assert_eq!(d.user, "korp");
        assert_eq!(
            d.group, "korp",
            "an absent Group= is the user's group, not \"\""
        );

        // …and the scan therefore stays quiet on a drop-in that restates it.
        let tmp = tempfile::tempdir().unwrap();
        let dir = tmp.path().join("x.service.d");
        std::fs::create_dir_all(&dir).unwrap();
        std::fs::write(dir.join("g.conf"), "[Service]\nGroup=korp\n").unwrap();
        let passwd = tmp.path().join("passwd");
        std::fs::write(&passwd, PASSWD).unwrap();
        assert!(!scan_dropins_in(&dir, &d, &passwd).has_conflicts());
    }

    /// A fragment that assigns the same key twice keeps the LAST value — systemd's
    /// own rule inside one file. Keeping the first would compare the drop-in against
    /// a value that never takes effect, and report a conflict backwards.
    #[test]
    fn a_repeated_key_in_one_fragment_keeps_the_last_value() {
        let d = declared_from_unit_text(
            "[Service]\nUser=a\nUser=b\nEnvironment=K=1\nEnvironment=K=2\n",
        );
        assert_eq!(d.user, "b");
        assert_eq!(
            d.env.iter().filter(|(k, _)| k == "K").count(),
            1,
            "no duplicate slot"
        );
        assert_eq!(d.env.iter().find(|(k, _)| k == "K").unwrap().1, "2");
    }

    /// `unit_files_in` lists REGULAR `.service` files only. `/etc/systemd/system` is
    /// mostly enablement symlinks into `/usr/lib` plus `→ /dev/null` masks, and an
    /// audit that walked those would report on units no installer here ever wrote.
    #[cfg(unix)]
    #[test]
    fn unit_files_skips_symlinks_and_non_services() {
        let tmp = tempfile::tempdir().unwrap();
        let dir = tmp.path();
        std::fs::write(dir.join("real.service"), "[Service]\n").unwrap();
        std::fs::write(dir.join("other.timer"), "[Timer]\n").unwrap();
        std::os::unix::fs::symlink("/dev/null", dir.join("masked.service")).unwrap();
        std::os::unix::fs::symlink(dir.join("real.service"), dir.join("enabled.service")).unwrap();

        let names = unit_files_in(dir);
        assert_eq!(names, vec!["real.service".to_string()], "got {names:?}");
    }

    /// `dropin_dir` builds systemd's own name and nothing else.
    #[test]
    fn dropin_dir_is_the_systemd_name() {
        assert_eq!(
            dropin_dir(Path::new("/etc/systemd/system"), "korp-server.service"),
            PathBuf::from("/etc/systemd/system/korp-server.service.d")
        );
    }
}