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;