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}");
}
}