Skip to main content

toolkit_zero/dependency-graph/
capture.rs

1//! Runtime reader for the `fingerprint.json` embedded in the binary.
2//!
3//! The companion `build` module (feature `dependency-graph-build`) writes
4//! `fingerprint.json` to `$OUT_DIR` at compile time. The downstream binary
5//! embeds it with:
6//!
7//! ```rust,ignore
8//! const BUILD_TIME_FINGERPRINT: &str = include_str!(concat!(env!("OUT_DIR"), "/fingerprint.json"));
9//! ```
10//!
11//! ## Functions
12//!
13//! * [`parse`] — deserialises the embedded JSON into a typed [`BuildTimeFingerprintData`]
14//!   struct. Returns [`CaptureError`] if the JSON is malformed or a required
15//!   section is absent.
16//!
17//! ## Concerns
18//!
19//! * The data is a **read-only snapshot** fixed at compile time. It reflects the
20//!   build environment at the time of compilation, not the current runtime state.
21//! * The fingerprint resides as plain text in the binary's read-only data
22//!   section. It is neither encrypted nor obfuscated and is visible to anyone
23//!   with access to the binary.
24
25use std::collections::BTreeMap;
26
27use serde_json::Value;
28
29// ─── error ────────────────────────────────────────────────────────────────────
30
31/// Errors that can occur while reading a captured fingerprint.
32#[derive(Debug)]
33pub enum CaptureError {
34    /// The embedded JSON is not valid.
35    InvalidJson(serde_json::Error),
36    /// A required top-level section is missing.
37    MissingSection(&'static str),
38}
39
40impl std::fmt::Display for CaptureError {
41    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42        match self {
43            Self::InvalidJson(e)      => write!(f, "fingerprint JSON is invalid: {e}"),
44            Self::MissingSection(key) => write!(f, "fingerprint is missing section: {key}"),
45        }
46    }
47}
48
49impl std::error::Error for CaptureError {
50    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
51        match self {
52            Self::InvalidJson(e)    => Some(e),
53            Self::MissingSection(_) => None,
54        }
55    }
56}
57
58// ─── typed data ───────────────────────────────────────────────────────────────
59
60/// Parsed contents of `fingerprint.json`.
61///
62/// All fields map directly onto the sections produced by
63/// [`build::generate_fingerprint`](super::build::generate_fingerprint).
64#[derive(Debug, Clone)]
65pub struct BuildTimeFingerprintData {
66    /// Package name and version.
67    pub package: PackageInfo,
68    /// Build-environment snapshot.
69    pub build: BuildInfo,
70    /// SHA-256 hex digest of `Cargo.lock` (comments stripped).
71    pub cargo_lock_sha256: String,
72    /// Normalised `cargo metadata` dependency graph (raw JSON value).
73    pub deps: Value,
74    /// Per-file SHA-256 digests of every `.rs` file under `src/`.
75    pub source: BTreeMap<String, String>,
76}
77
78/// Package identity section.
79#[derive(Debug, Clone)]
80pub struct PackageInfo {
81    pub name:    String,
82    pub version: String,
83}
84
85/// Build environment section.
86#[derive(Debug, Clone)]
87pub struct BuildInfo {
88    /// Active cargo feature names, sorted.
89    pub features:      Vec<String>,
90    /// Optimisation level (`"0"` / `"1"` / `"2"` / `"3"` / `"s"` / `"z"`).
91    pub opt_level:     String,
92    /// Cargo profile (`"debug"` / `"release"` / …).
93    pub profile:       String,
94    /// Full `rustc --version` string.
95    pub rustc_version: String,
96    /// Target triple, e.g. `"x86_64-apple-darwin"`.
97    pub target:        String,
98}
99
100// ─── public API ───────────────────────────────────────────────────────────────
101
102/// Parse the embedded fingerprint JSON into a typed [`BuildTimeFingerprintData`].
103///
104/// Pass the `&str` produced by
105/// `include_str!(concat!(env!("OUT_DIR"), "/fingerprint.json"))`.
106pub fn parse(json: &str) -> Result<BuildTimeFingerprintData, CaptureError> {
107    let root: Value = serde_json::from_str(json)
108        .map_err(CaptureError::InvalidJson)?;
109
110    let obj = root.as_object()
111        .ok_or(CaptureError::MissingSection("root (expected JSON object)"))? ;
112
113    // ── package ───────────────────────────────────────────────────────────────
114    let pkg = obj.get("package")
115        .and_then(|v| v.as_object())
116        .ok_or(CaptureError::MissingSection("package"))?;
117
118    let package = PackageInfo {
119        name:    pkg.get("name").and_then(|v| v.as_str()).unwrap_or("").to_owned(),
120        version: pkg.get("version").and_then(|v| v.as_str()).unwrap_or("").to_owned(),
121    };
122
123    // ── build ─────────────────────────────────────────────────────────────────
124    let bld = obj.get("build")
125        .and_then(|v| v.as_object())
126        .ok_or(CaptureError::MissingSection("build"))?;
127
128    let features = bld.get("features")
129        .and_then(|v| v.as_array())
130        .map(|arr| {
131            arr.iter()
132               .filter_map(|v| v.as_str())
133               .map(str::to_owned)
134               .collect()
135        })
136        .unwrap_or_default();
137
138    let build = BuildInfo {
139        features,
140        opt_level:     bld.get("opt_level").and_then(|v| v.as_str()).unwrap_or("").to_owned(),
141        profile:       bld.get("profile").and_then(|v| v.as_str()).unwrap_or("").to_owned(),
142        rustc_version: bld.get("rustc_version").and_then(|v| v.as_str()).unwrap_or("").to_owned(),
143        target:        bld.get("target").and_then(|v| v.as_str()).unwrap_or("").to_owned(),
144    };
145
146    // ── cargo_lock_sha256 ─────────────────────────────────────────────────────
147    let cargo_lock_sha256 = obj.get("cargo_lock_sha256")
148        .and_then(|v| v.as_str())
149        .ok_or(CaptureError::MissingSection("cargo_lock_sha256"))?
150        .to_owned();
151
152    // ── deps ──────────────────────────────────────────────────────────────────
153    let deps = obj.get("deps")
154        .ok_or(CaptureError::MissingSection("deps"))?
155        .clone();
156
157    // ── source ────────────────────────────────────────────────────────────────
158    let source = obj.get("source")
159        .and_then(|v| v.as_object())
160        .map(|map| {
161            map.iter()
162               .map(|(k, v)| (k.clone(), v.as_str().unwrap_or("").to_owned()))
163               .collect()
164        })
165        .unwrap_or_default();
166
167    Ok(BuildTimeFingerprintData { package, build, cargo_lock_sha256, deps, source })
168}
169
170/// Embed and bind the build-time `fingerprint.json` in one expression.
171///
172/// Apply to an empty `fn` inside a function body; the function name becomes
173/// the `let` binding in the expansion.
174///
175/// ```text
176/// #[dependencies]          // parse mode  → BuildTimeFingerprintData (propagates ?)
177/// #[dependencies(bytes)]   // bytes mode  → &'static [u8]
178/// ```
179///
180/// Requires the `dependency-graph-capture` feature.
181#[cfg(feature = "dependency-graph-capture")]
182pub use toolkit_zero_macros::dependencies;