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

nordisk/edda

crates/nornir-build-thing/src/receipt.rs

//! **The receipt, written into an artifact and read back out of it.**
//!
//! [`contract`](crate::contract) defines *what* a [`Receipt`] is. This module is
//! the two halves that make it real:
//!
//! ```text
//! writer   a build.rs helper emits a #[link_section] static -> .nordisk.buildthing
//! reader   an object parser lifts it back out, WITHOUT running the artifact
//! ```
//!
//! # Why the writer is a `build.rs` helper and not a runtime call
//!
//! The receipt has to be *in the artifact*, in a section, before the artifact
//! exists. Nothing at runtime can put it there. `build.rs` is the only place that
//! runs inside the compilation that produces the binary, and it is also the only
//! place that can see **resolved** values: `CARGO_FEATURE_*` is the feature set
//! after cargo's unification, not the set anybody typed.
//!
//! # Resolved values, not defaults — paid for in [`ReceiptBuilder::from_cargo_env`]
//!
//! Reading `CARGO_FEATURE_*` alone gets you the features that are ON. That is not
//! enough: a receipt that omits `gpu` because gpu was off cannot be told apart
//! from one written before `gpu` existed, and the day the default flips, every old
//! receipt becomes retroactively false. So the builder reads the **declared**
//! feature universe out of `Cargo.toml` and records every one of them as `"true"`
//! or `"false"`. `gpu = false` is written by a build that never mentioned gpu,
//! which is exactly the claim [`receipt_proof`](crate::receipt_proof) then goes and
//! checks against the symbol table.
//!
//! # No absolute paths, no timestamps — enforced, not requested
//!
//! Both make two otherwise identical builds produce different receipts, and a
//! receipt that differs for no reason cannot be compared. [`encode`] REFUSES to
//! serialise a receipt carrying either; see [`ReceiptFlaw`]. This is a
//! by-construction guard rather than a lint: the only writer routes through
//! [`encode`], so there is no second path that could skip it.

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

use anyhow::{bail, Context, Result};

use crate::contract::{Coordinate, Receipt, RECEIPT_SECTION, RECEIPT_VERSION};

/// The file [`emit`] writes into `OUT_DIR`, and [`embed_build_receipt`] includes.
pub const GENERATED_FILE: &str = "nordisk_buildthing_receipt.rs";

/// The static the generated source declares. Named so a human reading
/// `nm` output, or this crate's own tests, can say what they are looking at —
/// nothing depends on the name, because the reader finds the SECTION, not the
/// symbol. That distinction is load-bearing: a `strip = true` binary has no
/// symbol table at all and still carries its receipt.
pub const RECEIPT_STATIC: &str = "NORDISK_BUILDTHING_RECEIPT";

// ── the one section reader ───────────────────────────────────────────────────

/// Lift the raw bytes of `section` out of the object file at `artifact` —
/// **without executing it**.
///
/// This is the single object-parsing section read in the crate;
/// [`crate::plugin_host::read_section`] delegates here rather than carrying a
/// second copy. It matters that there is exactly one: the property being relied
/// on (parsing is not running, so this works for a binary this host could never
/// execute) has to hold for every caller, and a twin is where it stops holding.
///
/// `Ok(None)` means the file parsed as an object file and has no such section.
///
/// # Errors
///
/// When the file cannot be read, or cannot be parsed as an object file at all —
/// which is a distinct outcome from "no section", and callers must keep it
/// distinct: it is UNMEASURED, never a pass.
pub fn read_section_named(artifact: &Path, section: &str) -> Result<Option<Vec<u8>>> {
    use object::{Object, ObjectSection};
    let bytes =
        std::fs::read(artifact).with_context(|| format!("read artifact {}", artifact.display()))?;
    let file = object::File::parse(&*bytes)
        .with_context(|| format!("parse artifact {}", artifact.display()))?;
    for s in file.sections() {
        if s.name().ok() == Some(section) {
            let data = s.data().context("read build-thing section")?;
            return Ok(Some(data.to_vec()));
        }
    }
    Ok(None)
}

// ── the reader ───────────────────────────────────────────────────────────────

/// Read the [`Receipt`] out of the artifact at `artifact`.
///
/// Parses the object file; never runs it. `Ok(None)` is "this artifact carries no
/// receipt", which is a fact, not an error.
///
/// # Errors
///
/// When the artifact cannot be read or parsed, or when the section is there but
/// does not decode — see [`decode`]. A misread receipt answers the rebuild
/// question *wrongly*, which is worse than not answering it, so every ambiguity
/// here is an error rather than a guess.
pub fn read_receipt(artifact: &Path) -> Result<Option<Receipt>> {
    match read_section_named(artifact, RECEIPT_SECTION)? {
        None => Ok(None),
        Some(bytes) => Ok(Some(decode(&bytes).with_context(|| {
            format!("decode {RECEIPT_SECTION} of {}", artifact.display())
        })?)),
    }
}

/// Decode the raw section bytes into a [`Receipt`].
///
/// # What the trimming is for
///
/// A linker may pad a section to alignment, so trailing NUL bytes are expected
/// and are not corruption. An *interior* NUL is a different thing entirely: it
/// means two writers each contributed a receipt and the section holds both. That
/// is reported by name rather than resolved by picking one, because picking one
/// silently is how an artifact ends up attributed to the wrong recipe.
///
/// # Errors
///
/// On an empty section, on two concatenated receipts, on malformed JSON, and on a
/// `receipt_version` above [`RECEIPT_VERSION`] — a reader that meets a schema from
/// the future must refuse rather than interpret the fields it happens to
/// recognise.
pub fn decode(bytes: &[u8]) -> Result<Receipt> {
    let trimmed = trim_padding(bytes);
    if trimmed.is_empty() {
        bail!("{RECEIPT_SECTION} is present but empty");
    }
    if trimmed.contains(&0) {
        bail!(
            "{RECEIPT_SECTION} carries more than one receipt ({} bytes with an interior NUL) — \
             two build-thing writers were linked into one artifact, and which of them built it \
             is exactly the question the receipt exists to answer",
            trimmed.len()
        );
    }
    let receipt: Receipt = serde_json::from_slice(trimmed)
        .with_context(|| format!("{RECEIPT_SECTION} is not a receipt ({} bytes)", trimmed.len()))?;
    if receipt.receipt_version > RECEIPT_VERSION {
        bail!(
            "receipt schema version {} is newer than this reader understands ({RECEIPT_VERSION}) \
             — refusing to guess at fields it may have redefined",
            receipt.receipt_version
        );
    }
    Ok(receipt)
}

/// Strip the linker's alignment padding and any surrounding ASCII whitespace.
fn trim_padding(bytes: &[u8]) -> &[u8] {
    let is_pad = |b: u8| b == 0 || b.is_ascii_whitespace();
    let start = bytes.iter().position(|&b| !is_pad(b)).unwrap_or(bytes.len());
    let end = bytes.iter().rposition(|&b| !is_pad(b)).map_or(start, |i| i + 1);
    &bytes[start..end]
}

// ── what makes a receipt unfit to write ──────────────────────────────────────

/// A reason a [`Receipt`] must not be written into an artifact.
///
/// Every variant is something that makes two identical builds produce different
/// receipts, or makes the receipt unable to answer the question it exists for.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ReceiptFlaw {
    /// A value is an absolute filesystem path. It records where *this* machine
    /// happened to keep the tree, which is not a property of the build.
    AbsolutePath {
        /// `settings` or `inputs`.
        map: &'static str,
        /// The offending key.
        key: String,
        /// The offending value.
        value: String,
    },
    /// A value looks like a wall-clock timestamp. Two identical builds differ by
    /// it and by nothing else, which is precisely the comparison the receipt is
    /// for.
    Timestamp {
        /// `settings` or `inputs`.
        map: &'static str,
        /// The offending key.
        key: String,
        /// The offending value, or the key name when the key itself named a time.
        value: String,
    },
    /// The recipe's content hash is missing. `krate@version` says which recipe was
    /// *intended*; only the hash says which bytes ran. A receipt is written after
    /// the build, so there is never a reason for it to be absent here.
    MissingRecipeHash,
    /// `atom` or `target` is empty. An artifact that does not say what it is
    /// cannot be matched to a rebuild.
    EmptyField(&'static str),
}

impl std::fmt::Display for ReceiptFlaw {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ReceiptFlaw::AbsolutePath { map, key, value } => write!(
                f,
                "{map}[{key}] = {value:?} is an absolute path — it records this machine, \
                 not this build"
            ),
            ReceiptFlaw::Timestamp { map, key, value } => write!(
                f,
                "{map}[{key}] = {value:?} is a timestamp — two identical builds would differ \
                 by it and by nothing else"
            ),
            ReceiptFlaw::MissingRecipeHash => write!(
                f,
                "build_thing.hash is missing — the version says which recipe was intended, \
                 only the hash says which one ran"
            ),
            ReceiptFlaw::EmptyField(name) => {
                write!(f, "{name} is empty — the artifact does not say what it is")
            }
        }
    }
}

/// Does `v` name an absolute location on some filesystem?
///
/// Unix `/…`, a shell `~/…`, a Windows drive `C:\…`, and a UNC `\\host\…`. A
/// relative path is fine and common (`target/release/korp-server`), so only the
/// rooted forms are rejected.
fn is_absolute_path(v: &str) -> bool {
    v.starts_with('/')
        || v.starts_with("~/")
        || v.starts_with("\\\\")
        || {
            let b = v.as_bytes();
            b.len() >= 3 && b[0].is_ascii_alphabetic() && b[1] == b':' && (b[2] == b'\\' || b[2] == b'/')
        }
}

/// Does this key/value pair carry wall-clock time?
///
/// Two independent tests, because either alone has a hole:
///
/// * **the key** — `built_at`, `timestamp`, `date`, `time`. A key that names a
///   time is a time whatever it holds, including a value shaped like nothing in
///   particular.
/// * **the value** — an ISO-8601 date, with or without a `T`, and a bare 10- or
///   13-digit integer in the epoch-seconds / epoch-millis range.
///
/// The value test deliberately does not fire on `1.2.3`, on a target triple, or
/// on a hex digest: a 40-char sha is not 10 digits, and a version has dots in the
/// wrong places. What it *can* misfire on is an all-decimal 10-digit token that
/// is not a time; that is documented rather than papered over, and the key test
/// is the one doing most of the work.
fn is_timestamp(key: &str, v: &str) -> bool {
    let k = key.to_ascii_lowercase();
    let key_names_a_time = ["timestamp", "built_at", "built-at", "date", "_time", "time_"]
        .iter()
        .any(|p| k.contains(p))
        || k == "time";
    if key_names_a_time {
        return true;
    }
    // ISO-8601: 4 digits, '-', 2 digits, '-', 2 digits.
    let b = v.as_bytes();
    if b.len() >= 10
        && b[0..4].iter().all(u8::is_ascii_digit)
        && b[4] == b'-'
        && b[5..7].iter().all(u8::is_ascii_digit)
        && b[7] == b'-'
        && b[8..10].iter().all(u8::is_ascii_digit)
    {
        return true;
    }
    // Epoch seconds (10 digits) or millis (13 digits), and nothing else.
    matches!(v.len(), 10 | 13) && b.iter().all(u8::is_ascii_digit)
}

/// Every reason this receipt must not be written, in the order it was found.
///
/// An empty vector is the only thing [`encode`] will serialise.
#[must_use]
pub fn audit_receipt(r: &Receipt) -> Vec<ReceiptFlaw> {
    let mut flaws = Vec::new();
    if r.build_thing.hash.is_none() {
        flaws.push(ReceiptFlaw::MissingRecipeHash);
    }
    if r.atom.trim().is_empty() {
        flaws.push(ReceiptFlaw::EmptyField("atom"));
    }
    if r.target.trim().is_empty() {
        flaws.push(ReceiptFlaw::EmptyField("target"));
    }
    for (map, entries) in [("settings", &r.settings), ("inputs", &r.inputs)] {
        for (k, v) in entries.iter() {
            if is_absolute_path(v) {
                flaws.push(ReceiptFlaw::AbsolutePath {
                    map,
                    key: k.clone(),
                    value: v.clone(),
                });
            }
            if is_timestamp(k, v) {
                flaws.push(ReceiptFlaw::Timestamp {
                    map,
                    key: k.clone(),
                    value: v.clone(),
                });
            }
        }
    }
    flaws
}

// ── the writer ───────────────────────────────────────────────────────────────

/// Serialise a receipt to the exact bytes that go in [`RECEIPT_SECTION`].
///
/// # Errors
///
/// When [`audit_receipt`] finds anything. Every flaw is listed, not just the
/// first: a build.rs that has to be re-run once per problem is a build.rs nobody
/// keeps.
pub fn encode(r: &Receipt) -> Result<Vec<u8>> {
    let flaws = audit_receipt(r);
    if !flaws.is_empty() {
        bail!(
            "refusing to write a receipt with {} flaw(s):\n  {}",
            flaws.len(),
            flaws.iter().map(ToString::to_string).collect::<Vec<_>>().join("\n  ")
        );
    }
    serde_json::to_vec(r).context("serialise receipt")
}

/// The Rust source that puts `r` in the artifact's [`RECEIPT_SECTION`].
///
/// Exposed (rather than only [`emit`]) so it can be tested against a real
/// compiler and a real linker without a build.rs in the way — which is how the
/// section is known to survive `--release`, `lto = true` and `strip = true`
/// rather than assumed to.
///
/// # `#[used]` is load-bearing, and that was measured
///
/// Nothing references the static, so `--gc-sections` is entitled to drop it.
/// Measured on oden 2026-08-06, `opt-level = 3` + `lto = true` + `strip = true`,
/// same source both times:
///
/// ```text
/// with    #[used]     .nordisk.buildthing present, 0x15 bytes
/// without #[used]     no such section at all
/// ```
///
/// So it is not decoration. `tests/receipt_end_to_end.rs` is what keeps it
/// honest: drop the attribute from this generator and the receipt stops coming
/// back out of a real binary.
///
/// # Errors
///
/// Whatever [`encode`] refuses.
pub fn emit_source(r: &Receipt) -> Result<String> {
    let bytes = encode(r)?;
    Ok(format!(
        "// Generated by nornir-build-thing. The receipt for this artifact, in the\n\
         // `{section}` linker section. `#[used]` keeps it through --gc-sections;\n\
         // it is data, referenced by nothing, and read with an object parser.\n\
         #[used]\n\
         #[unsafe(link_section = \"{section}\")]\n\
         static {name}: [u8; {len}] = *b\"{body}\";\n",
        section = RECEIPT_SECTION,
        name = RECEIPT_STATIC,
        len = bytes.len(),
        body = escape_byte_string(&bytes),
    ))
}

/// Escape `bytes` for a Rust byte-string literal.
///
/// Everything outside printable ASCII becomes `\xNN`, so a receipt carrying a
/// non-ASCII atom name still produces source that compiles.
fn escape_byte_string(bytes: &[u8]) -> String {
    let mut s = String::with_capacity(bytes.len() + 16);
    for &b in bytes {
        match b {
            b'"' => s.push_str("\\\""),
            b'\\' => s.push_str("\\\\"),
            0x20..=0x7e => s.push(b as char),
            _ => s.push_str(&format!("\\x{b:02x}")),
        }
    }
    s
}

/// **The build.rs call.** Write the generated source into `OUT_DIR`.
///
/// ```no_run
/// // build.rs
/// # use nornir_build_thing::{contract::Coordinate, receipt::ReceiptBuilder};
/// # fn main() -> anyhow::Result<()> {
/// let receipt = ReceiptBuilder::from_cargo_env(
///     Coordinate::parse("korp-build-thing:0.3.1:a91f4c2e").unwrap(),
///     "korp-server",
///     "exe",
/// )?
/// .input("source-commit", "9f2c1ab")
/// .build()?;
/// nornir_build_thing::receipt::emit(&receipt)?;
/// # Ok(()) }
/// ```
///
/// and in the crate's own source, once:
///
/// ```ignore
/// nornir_build_thing::embed_build_receipt!();
/// ```
///
/// # Errors
///
/// When `OUT_DIR` is unset (this is not being called from a build script), when
/// the receipt is refused, or when the file cannot be written.
pub fn emit(r: &Receipt) -> Result<PathBuf> {
    let out_dir = std::env::var("OUT_DIR")
        .context("OUT_DIR is unset — receipt::emit is a build.rs call")?;
    emit_into(Path::new(&out_dir), r)
}

/// [`emit`] with the output directory named rather than read from `OUT_DIR`.
///
/// Exists so the write can be exercised against a real compiler without mutating
/// the process environment — `emit` is then one `env::var` over this, and there
/// is still exactly one place that decides the file name.
///
/// # Errors
///
/// When the receipt is refused, or the file cannot be written.
pub fn emit_into(out_dir: &Path, r: &Receipt) -> Result<PathBuf> {
    let path = out_dir.join(GENERATED_FILE);
    std::fs::write(&path, emit_source(r)?)
        .with_context(|| format!("write {}", path.display()))?;
    Ok(path)
}

/// Include the receipt [`emit`] generated. One line, once, anywhere in the crate.
///
/// Kept as a macro rather than documented as an `include!` so the OUT_DIR file
/// name is not a string every consumer has to get right.
///
/// The literal below cannot be [`GENERATED_FILE`] — `concat!` takes literals
/// only — so `the_macro_and_the_writer_agree_on_the_file_name` asserts the two
/// spell the same thing.
#[macro_export]
macro_rules! embed_build_receipt {
    () => {
        include!(concat!(env!("OUT_DIR"), "/nordisk_buildthing_receipt.rs"));
    };
}

/// Assemble a [`Receipt`] out of what cargo resolved, plus what the caller knows.
///
/// The point of the type is [`from_cargo_env`](Self::from_cargo_env): settings a
/// caller types by hand are declarations, and declarations are what the receipt
/// exists to stop trusting.
#[derive(Debug, Clone)]
pub struct ReceiptBuilder {
    build_thing: Coordinate,
    atom: String,
    target: String,
    settings: BTreeMap<String, String>,
    inputs: BTreeMap<String, String>,
}

impl ReceiptBuilder {
    /// A builder with nothing resolved — the caller supplies every setting.
    ///
    /// Prefer [`from_cargo_env`](Self::from_cargo_env). This exists for the paths
    /// that do not run inside a cargo build at all (an ISO, a container image),
    /// where there is no `CARGO_FEATURE_*` to read.
    pub fn new(build_thing: Coordinate, atom: impl Into<String>, target: impl Into<String>) -> Self {
        Self {
            build_thing,
            atom: atom.into(),
            target: target.into(),
            settings: BTreeMap::new(),
            inputs: BTreeMap::new(),
        }
    }

    /// **The resolved-values constructor.** Reads the compilation cargo is
    /// actually performing.
    ///
    /// * `settings` gets `feature.<name>` = `"true"` / `"false"` for **every
    ///   feature the manifest declares**, not only the enabled ones. That is the
    ///   difference between a receipt and a copy of the command line: a build
    ///   that never mentioned `gpu` still records `feature.gpu = "false"`, and
    ///   [`receipt_proof`](crate::receipt_proof) can then go and check it.
    /// * `inputs` gets the target triple, profile, opt-level, debug flag and the
    ///   rustc version string. Not `RUSTC` itself — that is an absolute path to a
    ///   toolchain on this machine, and [`encode`] would refuse it.
    ///
    /// `source-commit` is not read here: this crate does not decide what counts
    /// as the source of a build (a submodule? a vendored tree?). Add it with
    /// [`input`](Self::input).
    ///
    /// # Errors
    ///
    /// When `CARGO_MANIFEST_DIR` is unset (not a build script) or its
    /// `Cargo.toml` cannot be read or parsed. A silently empty feature universe
    /// would make every feature claim vanish rather than read `false`, which is
    /// the failure this whole module is built to prevent.
    pub fn from_cargo_env(
        build_thing: Coordinate,
        atom: impl Into<String>,
        target: impl Into<String>,
    ) -> Result<Self> {
        let manifest_dir = std::env::var("CARGO_MANIFEST_DIR")
            .context("CARGO_MANIFEST_DIR is unset — from_cargo_env is a build.rs call")?;
        let manifest = Path::new(&manifest_dir).join("Cargo.toml");
        let text = std::fs::read_to_string(&manifest)
            .with_context(|| format!("read {}", manifest.display()))?;
        let settings = resolved_feature_settings(&text, |var| std::env::var(var).is_ok())
            .with_context(|| format!("parse [features] of {}", manifest.display()))?;

        let mut inputs = BTreeMap::new();
        for (key, var) in [
            ("target", "TARGET"),
            ("profile", "PROFILE"),
            ("opt-level", "OPT_LEVEL"),
            ("debug", "DEBUG"),
            ("target-arch", "CARGO_CFG_TARGET_ARCH"),
            ("target-os", "CARGO_CFG_TARGET_OS"),
            ("target-env", "CARGO_CFG_TARGET_ENV"),
        ] {
            if let Ok(v) = std::env::var(var) {
                if !v.is_empty() {
                    inputs.insert(key.to_string(), v);
                }
            }
        }
        if let Some(v) = rustc_version() {
            inputs.insert("rustc".to_string(), v);
        }

        Ok(Self {
            build_thing,
            atom: atom.into(),
            target: target.into(),
            settings,
            inputs,
        })
    }

    /// Record a resolved setting. Overwrites whatever was there.
    #[must_use]
    pub fn setting(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
        self.settings.insert(key.into(), value.into());
        self
    }

    /// Record a build input — `source-commit`, a base image tag, a toolchain pin.
    #[must_use]
    pub fn input(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
        self.inputs.insert(key.into(), value.into());
        self
    }

    /// Finish, or say why not.
    ///
    /// # Errors
    ///
    /// When [`audit_receipt`] finds a flaw. Failing here rather than at
    /// [`emit`] time keeps the refusal next to the line that caused it.
    pub fn build(self) -> Result<Receipt> {
        let r = Receipt {
            receipt_version: RECEIPT_VERSION,
            build_thing: self.build_thing,
            atom: self.atom,
            target: self.target,
            settings: self.settings,
            inputs: self.inputs,
        };
        let flaws = audit_receipt(&r);
        if !flaws.is_empty() {
            bail!(
                "receipt has {} flaw(s):\n  {}",
                flaws.len(),
                flaws.iter().map(ToString::to_string).collect::<Vec<_>>().join("\n  ")
            );
        }
        Ok(r)
    }
}

/// **Every feature the manifest declares, with the value the build resolved for
/// it** — `feature.<name>` = `"true"` or `"false"`.
///
/// This is the "resolved values, not defaults" rule, and it is one function so
/// there is one rule. `is_on` is handed the `CARGO_FEATURE_*` variable name and
/// answers whether cargo set it; splitting it out from the environment is what
/// makes the property testable, because the property is about the features that
/// are **off**.
///
/// A feature that is off is recorded as `"false"`, not omitted. Omitting it would
/// make a receipt written today indistinguishable from one written before the
/// feature existed, and would make every old receipt retroactively false the day
/// a default flips.
///
/// # Errors
///
/// When the manifest does not parse. An empty universe is not an error — a crate
/// may declare no features — but a manifest we cannot read is, because the
/// silent-empty result looks exactly like a clean one.
pub fn resolved_feature_settings(
    manifest_toml: &str,
    is_on: impl Fn(&str) -> bool,
) -> Result<BTreeMap<String, String>> {
    let mut settings = BTreeMap::new();
    for name in declared_features(manifest_toml)? {
        let on = is_on(&feature_env_key(&name));
        settings.insert(format!("feature.{name}"), on.to_string());
    }
    Ok(settings)
}

/// The `CARGO_FEATURE_*` variable cargo sets for a declared feature name.
///
/// Cargo uppercases and maps every non-alphanumeric character to `_`. Going this
/// direction — from the declared name to the variable — is exact; the reverse is
/// not, which is why the feature universe is read from the manifest rather than
/// reconstructed from the environment.
fn feature_env_key(name: &str) -> String {
    let mut s = String::from("CARGO_FEATURE_");
    for c in name.chars() {
        if c.is_ascii_alphanumeric() {
            s.push(c.to_ascii_uppercase());
        } else {
            s.push('_');
        }
    }
    s
}

/// Every feature name a manifest declares, including `default` if present, and
/// including the implicit features an optional dependency creates.
///
/// # `dep:<name>` is what makes the implicit feature NOT exist
///
/// An `optional = true` dependency creates a feature of the same name **unless
/// some feature refers to it as `dep:<name>`**. That second half was missing, and
/// it is not a corner case: it is how every manifest that gates dependencies
/// behind a named axis is written.
///
/// MEASURED 2026-08-06 on gunnar-cli, whose `server` feature is a list of
/// `dep:gunnar-core`, `dep:gunnar-store`, …: the first real receipt written by a
/// real build carried **33** `feature.*` entries of which **27** were
/// dependencies, and every one of them read `"false"` — because cargo sets no
/// `CARGO_FEATURE_*` for something that is not a feature. So the receipt of a
/// binary that links `gunnar-core` said `feature.gunnar-core = "false"`.
///
/// That is a receipt that lies, in the direction this module exists to prevent:
/// not silence, but a confident wrong answer that nothing downstream can
/// contradict.
fn declared_features(manifest_toml: &str) -> Result<Vec<String>> {
    let doc: toml::Value = manifest_toml.parse::<toml::Table>().map(toml::Value::Table).context("parse Cargo.toml")?;
    let features = doc.get("features").and_then(toml::Value::as_table);
    let mut names: Vec<String> = features
        .map(|t| t.keys().cloned().collect())
        .unwrap_or_default();

    // Every `dep:<name>` any feature mentions. Those dependencies get NO implicit
    // feature of their own.
    let mut hidden: Vec<&str> = Vec::new();
    if let Some(t) = features {
        for value in t.values() {
            let Some(list) = value.as_array() else { continue };
            for entry in list {
                if let Some(dep) = entry.as_str().and_then(|s| s.strip_prefix("dep:")) {
                    // `dep:foo?/bar` and `dep:foo/bar` both name `foo`.
                    hidden.push(dep.split(['/', '?']).next().unwrap_or(dep));
                }
            }
        }
    }

    // The three dependency tables, because an optional dev- or build-dependency
    // creates an implicit feature exactly as a normal one does.
    for table in ["dependencies", "dev-dependencies", "build-dependencies"] {
        let Some(deps) = doc.get(table).and_then(toml::Value::as_table) else {
            continue;
        };
        for (name, spec) in deps {
            let optional = spec
                .get("optional")
                .and_then(toml::Value::as_bool)
                .unwrap_or(false);
            if optional && !hidden.contains(&name.as_str()) && !names.iter().any(|n| n == name) {
                names.push(name.clone());
            }
        }
    }
    names.sort();
    names.dedup();
    Ok(names)
}

/// `rustc -V` as a version string, or `None` when it cannot be run.
///
/// Uses the `RUSTC` cargo sets so the answer is the compiler doing THIS build,
/// not whatever is first on `PATH`. The path itself is never recorded — it is
/// absolute, and [`encode`] refuses absolute paths.
fn rustc_version() -> Option<String> {
    let rustc = std::env::var("RUSTC").unwrap_or_else(|_| "rustc".to_string());
    let out = std::process::Command::new(rustc).arg("-V").output().ok()?;
    if !out.status.success() {
        return None;
    }
    let s = String::from_utf8(out.stdout).ok()?;
    let s = s.trim();
    if s.is_empty() {
        None
    } else {
        Some(s.to_string())
    }
}

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

    fn good_receipt() -> Receipt {
        ReceiptBuilder::new(
            Coordinate::parse("korp-build-thing:0.3.1:a91f4c2e").unwrap(),
            "korp-server",
            "exe",
        )
        .setting("feature.gpu", "false")
        .setting("feature.default", "true")
        .input("source-commit", "9f2c1ab")
        .input("target", "x86_64-unknown-linux-gnu")
        .build()
        .expect("the fixture is meant to be clean")
    }

    /// LAW 2 on the absolute-path guard: it fires on the shapes that matter AND
    /// stays quiet on the ones that do not.
    ///
    /// The second half is the part worth having. A detector that answered `true`
    /// for everything would pass the first half and make every receipt
    /// unwritable — which is a guard that has stopped measuring anything.
    #[test]
    fn the_absolute_path_detector_reports_red_only_on_rooted_paths() {
        for rooted in [
            "/home/alice/git/korp",
            "/",
            "~/git/korp",
            "C:\\Users\\r\\korp",
            "D:/korp",
            "\\\\host\\share\\korp",
        ] {
            assert!(is_absolute_path(rooted), "{rooted:?} is an absolute path");
        }
        for fine in [
            "target/release/korp-server",
            "x86_64-unknown-linux-gnu",
            "false",
            "tunnr",
            "rustc 1.97.1 (8bab26f4f 2026-07-14)",
            "9f2c1ab",
            "",
        ] {
            assert!(
                !is_absolute_path(fine),
                "{fine:?} is not an absolute path, and a detector that says it is \
                 refuses every receipt there is"
            );
        }
    }

    /// LAW 2 on the timestamp guard, both directions for the same reason.
    #[test]
    fn the_timestamp_detector_reports_red_only_on_wall_clock_values() {
        for (k, v) in [
            ("built_at", "whenever"),
            ("build-timestamp", "x"),
            ("date", "n/a"),
            ("source-date", "n/a"),
            ("x", "2026-08-06T12:00:00Z"),
            ("x", "2026-08-06"),
            ("x", "1785000000"),
            ("x", "1785000000000"),
        ] {
            assert!(is_timestamp(k, v), "{k}={v:?} carries wall-clock time");
        }
        for (k, v) in [
            ("target", "x86_64-unknown-linux-gnu"),
            ("version", "1.2.3"),
            ("source-commit", "9f2c1ab"),
            ("source-commit", "a91f4c2e9f2c1ab0d4e5f6a7b8c9d0e1f2a3b4c5"),
            ("feature.gpu", "false"),
            ("opt-level", "3"),
            ("rustc", "rustc 1.97.1 (8bab26f4f 2026-07-14)"),
        ] {
            assert!(
                !is_timestamp(k, v),
                "{k}={v:?} is not a timestamp, and flagging it would refuse every \
                 receipt a real build writes"
            );
        }
    }

    /// The encoder refuses each flaw, and NAMES it. Watched failing one flaw at a
    /// time, because a refusal that fires for the wrong reason is a guard nobody
    /// can act on.
    #[test]
    fn encode_refuses_every_flaw_and_says_which() {
        let clean = good_receipt();
        assert!(audit_receipt(&clean).is_empty(), "{:?}", audit_receipt(&clean));
        assert!(encode(&clean).is_ok());

        let mut with_path = clean.clone();
        with_path
            .settings
            .insert("out".into(), "/home/alice/git/korp".into());
        let err = format!("{:#}", encode(&with_path).unwrap_err());
        assert!(err.contains("absolute path"), "{err}");
        assert!(err.contains("/home/alice/git/korp"), "the value is named: {err}");

        let mut with_time = clean.clone();
        with_time
            .inputs
            .insert("built_at".into(), "2026-08-06T12:00:00Z".into());
        let err = format!("{:#}", encode(&with_time).unwrap_err());
        assert!(err.contains("timestamp"), "{err}");

        let mut no_hash = clean.clone();
        no_hash.build_thing.hash = None;
        let err = format!("{:#}", encode(&no_hash).unwrap_err());
        assert!(err.contains("build_thing.hash is missing"), "{err}");

        let mut no_atom = clean;
        no_atom.atom = "  ".into();
        let err = format!("{:#}", encode(&no_atom).unwrap_err());
        assert!(err.contains("atom is empty"), "{err}");
    }

    /// A receipt survives encode → decode. Asserted against the WHOLE value, so
    /// a field silently dropped by the wire format is caught.
    #[test]
    fn a_receipt_survives_the_section_round_trip() {
        let r = good_receipt();
        let bytes = encode(&r).expect("encode");
        assert_eq!(decode(&bytes).expect("decode"), r);
        // Linker alignment padding is not corruption.
        let mut padded = bytes.clone();
        padded.extend_from_slice(&[0, 0, 0, 0]);
        assert_eq!(decode(&padded).expect("decode padded"), r);
    }

    /// LAW 2: the decoder's refusals, each watched going red for its own reason.
    #[test]
    fn the_decoder_refuses_what_it_cannot_answer_honestly() {
        let bytes = encode(&good_receipt()).expect("encode");

        let err = format!("{:#}", decode(b"").unwrap_err());
        assert!(err.contains("empty"), "{err}");

        // Two writers, one section. Must be named, not silently resolved to one.
        let mut two = bytes.clone();
        two.push(0);
        two.extend_from_slice(&bytes);
        let err = format!("{:#}", decode(&two).unwrap_err());
        assert!(err.contains("more than one receipt"), "{err}");

        let err = format!("{:#}", decode(b"{not json").unwrap_err());
        assert!(err.contains("is not a receipt"), "{err}");

        // A schema from the future is refused rather than partially believed.
        let mut future = good_receipt();
        future.receipt_version = RECEIPT_VERSION + 1;
        let raw = serde_json::to_vec(&future).unwrap();
        let err = format!("{:#}", decode(&raw).unwrap_err());
        assert!(err.contains("newer than this reader understands"), "{err}");
    }

    /// The generated source carries the receipt bytes verbatim, escaped for a
    /// byte-string literal.
    #[test]
    fn the_generated_source_declares_the_section_and_the_exact_length() {
        let r = good_receipt();
        let bytes = encode(&r).expect("encode");
        let src = emit_source(&r).expect("emit");
        assert!(src.contains(&format!("link_section = \"{RECEIPT_SECTION}\"")), "{src}");
        assert!(src.contains(&format!("[u8; {}]", bytes.len())), "{src}");
        assert!(src.contains("#[used]"), "{src}");
    }

    /// The escaper is total: every byte 0..=255 comes back out of the literal it
    /// produced. Proven by re-parsing rather than by inspection.
    #[test]
    fn the_byte_string_escaper_round_trips_every_byte() {
        let all: Vec<u8> = (0u8..=255).collect();
        let escaped = escape_byte_string(&all);
        // Undo the escaping the same way rustc would, and compare.
        let mut out = Vec::new();
        let mut it = escaped.chars().peekable();
        while let Some(c) = it.next() {
            if c != '\\' {
                out.push(c as u8);
                continue;
            }
            match it.next().expect("dangling escape") {
                '"' => out.push(b'"'),
                '\\' => out.push(b'\\'),
                'x' => {
                    let hi = it.next().unwrap();
                    let lo = it.next().unwrap();
                    out.push(u8::from_str_radix(&format!("{hi}{lo}"), 16).unwrap());
                }
                other => panic!("unknown escape \\{other}"),
            }
        }
        assert_eq!(out, all, "the escaper is not reversible");
    }

    /// The macro's `concat!` literal and [`GENERATED_FILE`] must name one file.
    ///
    /// They cannot be the same token — `concat!` refuses a const — so a drift
    /// between them would show up as a consumer's `include!` failing to find a
    /// file that was written under a different name. Cheaper to assert here.
    #[test]
    fn the_macro_and_the_writer_agree_on_the_file_name() {
        let expanded = stringify!(embed_build_receipt);
        assert_eq!(expanded, "embed_build_receipt");
        assert!(
            include_str!("receipt.rs").contains(&format!("\"/{GENERATED_FILE}\"")),
            "the macro must include the file `emit_into` writes, `{GENERATED_FILE}`"
        );
    }

    /// `CARGO_FEATURE_*` naming, in the exact direction the builder uses it.
    #[test]
    fn a_declared_feature_maps_to_the_variable_cargo_sets() {
        assert_eq!(feature_env_key("gpu"), "CARGO_FEATURE_GPU");
        assert_eq!(feature_env_key("build-info-arrow"), "CARGO_FEATURE_BUILD_INFO_ARROW");
        assert_eq!(feature_env_key("plugin_host"), "CARGO_FEATURE_PLUGIN_HOST");
        // The collision the reverse direction would have: two distinct feature
        // names, one variable. Reading the universe from the manifest is what
        // avoids having to guess.
        assert_eq!(feature_env_key("a-b"), feature_env_key("a_b"));
    }

    /// The feature universe comes out of the manifest, including the implicit
    /// feature an optional dependency creates — and **excluding** the one it does
    /// not create.
    ///
    /// `wgpu` is optional and named `dep:wgpu`, so cargo makes no `wgpu` feature
    /// and there is no `CARGO_FEATURE_WGPU` to read. Listing it would put
    /// `feature.wgpu = "false"` in the receipt of a binary that links wgpu.
    /// `nvml-wrapper` is optional and named by nobody, so cargo DOES make the
    /// implicit feature and it belongs in the universe.
    #[test]
    fn the_declared_feature_universe_includes_optional_dependencies() {
        let manifest = r#"
[package]
name = "x"
[features]
default = ["gpu"]
gpu = ["dep:wgpu"]
[dependencies]
serde = "1"
wgpu = { version = "0.20", optional = true }
nvml-wrapper = { version = "0.10", optional = true }
"#;
        let mut names = declared_features(manifest).expect("parse");
        names.sort();
        assert_eq!(names, vec!["default", "gpu", "nvml-wrapper"]);
        assert!(
            !names.contains(&"wgpu".to_string()),
            "a dependency named `dep:wgpu` was counted as a feature; cargo sets no \
             CARGO_FEATURE_WGPU, so it would be recorded false in a binary that links it"
        );

        // Watched red: drop the `dep:` reference and the SAME dependency becomes a
        // real implicit feature. Without this half the exclusion above could be a
        // rule that hides every optional dependency, which is the other way to be
        // wrong.
        let unnamed = manifest.replace("gpu = [\"dep:wgpu\"]", "gpu = []");
        let names = declared_features(&unnamed).expect("parse");
        assert!(
            names.contains(&"wgpu".to_string()),
            "an optional dependency nobody names as `dep:` HAS an implicit feature, \
             and the universe dropped it: {names:?}"
        );

        // The `dep:foo?/bar` weak-dependency spelling names `foo` too.
        let weak = manifest.replace("gpu = [\"dep:wgpu\"]", "gpu = [\"dep:wgpu?/spirv\"]");
        assert!(
            !declared_features(&weak).unwrap().contains(&"wgpu".to_string()),
            "the weak-dependency spelling was not recognised"
        );

        // An optional BUILD dependency gets an implicit feature exactly as a
        // normal one does, and used to be missed entirely.
        let build_dep = r#"
[package]
name = "x"
[build-dependencies]
cc = { version = "1", optional = true }
"#;
        assert_eq!(declared_features(build_dep).unwrap(), vec!["cc"]);

        // A manifest with no [features] at all is an empty universe, not an error.
        assert!(declared_features("[package]\nname = \"x\"\n").unwrap().is_empty());
    }

    /// **The "resolved values, not defaults" property, asserted where it bites.**
    ///
    /// The interesting entry is the one nobody enabled: `gpu` must land as
    /// `"false"` rather than be absent. A receipt that only listed the features
    /// that were ON would be a copy of the command line, and the claim
    /// [`crate::receipt_proof`] goes and checks against the symbol table would not
    /// exist to check.
    #[test]
    fn a_feature_that_was_off_is_recorded_as_false_rather_than_omitted() {
        let manifest = r#"
[package]
name = "korp"
[features]
default = []
gpu = []
metrics = []
"#;
        // Only `metrics` was enabled by this build.
        let on = |var: &str| var == "CARGO_FEATURE_METRICS";
        let s = resolved_feature_settings(manifest, on).expect("parse");
        assert_eq!(s.get("feature.metrics").map(String::as_str), Some("true"));
        assert_eq!(
            s.get("feature.gpu").map(String::as_str),
            Some("false"),
            "a feature nobody mentioned must be RESOLVED to false, not left out: {s:?}"
        );
        assert_eq!(s.get("feature.default").map(String::as_str), Some("false"));
        assert_eq!(s.len(), 3, "the universe is the manifest's, not the environment's: {s:?}");

        // Red side: with nothing enabled, every declared feature is still present
        // and every one of them is false. A map that shrank here would be the
        // omission this test exists to forbid.
        let none = resolved_feature_settings(manifest, |_| false).expect("parse");
        assert_eq!(none.len(), 3, "{none:?}");
        assert!(none.values().all(|v| v == "false"), "{none:?}");
    }

    /// LAW 2: the settings map is a `BTreeMap`, so this test cannot be the blind
    /// "serialise one map twice" shape. Insert in reverse and compare the WIRE.
    #[test]
    fn the_feature_universe_lands_sorted_on_the_wire() {
        let r = ReceiptBuilder::new(
            Coordinate::parse("x:1:h").unwrap(),
            "a",
            "exe",
        )
        .setting("feature.zulu", "false")
        .setting("feature.alpha", "true")
        .build()
        .unwrap();
        let json = String::from_utf8(encode(&r).unwrap()).unwrap();
        let alpha = json.find("feature.alpha").expect("alpha on the wire");
        let zulu = json.find("feature.zulu").expect("zulu on the wire");
        assert!(alpha < zulu, "insertion order leaked into the receipt: {json}");
    }
}