1use std::sync::OnceLock;
9
10use serde::{Deserialize, Serialize};
11use sha2::{Digest as _, Sha256};
12
13const FINGERPRINT_SCHEMA: &str = "harn.runtime_content_fingerprint.v1";
14const CONTENT_DIGEST_DOMAIN: &[u8] = b"harn.runtime-content.v1\0";
15
16#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
19#[serde(deny_unknown_fields)]
20pub struct RuntimeCompatibilityFingerprint {
21 pub codegen_fingerprint: String,
22 pub bytecode_schema_version: u32,
23 pub linked_program_schema_version: u32,
24 pub linker_algorithm_version: u32,
25 pub build_features: RuntimeBuildFeatures,
26}
27
28macro_rules! define_runtime_build_features {
29 ($( $field:ident => $cargo_name:literal ),+ $(,)?) => {
30 #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
35 #[serde(deny_unknown_fields)]
36 pub struct RuntimeBuildFeatures {
37 $(pub $field: bool),+
38 }
39
40 impl RuntimeBuildFeatures {
41 fn linked() -> Self {
42 Self {$($field: cfg!(feature = $cargo_name)),+}
43 }
44
45 fn update_digest(&self, hasher: &mut Sha256) {
46 $(update_bool_field(hasher, concat!("feature-", $cargo_name), self.$field);)+
47 }
48
49 #[cfg(test)]
50 const CARGO_NAMES: &'static [&'static str] = &[$($cargo_name),+];
51
52 #[cfg(test)]
53 fn none() -> Self {
54 Self {$($field: false),+}
55 }
56
57 #[cfg(test)]
58 fn enable(&mut self, cargo_name: &str) {
59 match cargo_name {
60 $($cargo_name => self.$field = true),+,
61 _ => unreachable!("feature list and typed fields share one declaration"),
62 }
63 }
64 }
65 };
66}
67
68define_runtime_build_features! {
69 default => "default",
70 full => "full",
71 llm_bench_internals => "llm-bench-internals",
72 vm_bench_internals => "vm-bench-internals",
73 content => "content",
74 compression => "compression",
75 http_compression => "http-compression",
76 cloud_aws => "cloud-aws",
77 native_keyring => "native-keyring",
78 postgres => "postgres",
79 sqlite => "sqlite",
80 otel => "otel",
81 testbench_wasi => "testbench-wasi",
82}
83
84#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
90#[serde(deny_unknown_fields)]
91pub struct RuntimeContentFingerprint {
92 pub schema: String,
93 pub content_sha256: String,
94 pub harn_version: String,
95 pub embedded_stdlib_sha256: String,
96 pub compatibility: RuntimeCompatibilityFingerprint,
97 pub source_revision: Option<String>,
98}
99
100#[must_use]
102pub fn runtime_content_fingerprint() -> &'static RuntimeContentFingerprint {
103 static FINGERPRINT: OnceLock<RuntimeContentFingerprint> = OnceLock::new();
104 FINGERPRINT.get_or_init(|| {
105 let compatibility = RuntimeCompatibilityFingerprint {
106 codegen_fingerprint: crate::bytecode_cache::CODEGEN_FINGERPRINT.to_string(),
107 bytecode_schema_version: crate::bytecode_cache::SCHEMA_VERSION,
108 linked_program_schema_version: crate::linked_program::LINKED_PROGRAM_SCHEMA_VERSION,
109 linker_algorithm_version: crate::linked_program::LINKER_ALGORITHM_VERSION,
110 build_features: RuntimeBuildFeatures::linked(),
111 };
112 fingerprint_from_parts(
113 crate::bytecode_cache::HARN_VERSION,
114 embedded_stdlib_digest_bytes(),
115 compatibility,
116 linked_source_revision(),
117 )
118 })
119}
120
121pub(crate) fn embedded_stdlib_digest_bytes() -> &'static [u8; 32] {
122 static DIGEST: OnceLock<[u8; 32]> = OnceLock::new();
123 DIGEST.get_or_init(|| {
124 embedded_stdlib_digest_from_sources(
125 harn_stdlib::STDLIB_SOURCES
126 .iter()
127 .map(|source| (source.module, source.source)),
128 )
129 })
130}
131
132fn embedded_stdlib_digest_from_sources<'a>(
133 sources: impl IntoIterator<Item = (&'a str, &'a str)>,
134) -> [u8; 32] {
135 let mut entries: Vec<(&str, &str)> = sources.into_iter().collect();
136 entries.sort_by(|left, right| left.0.cmp(right.0));
137 let mut hasher = Sha256::new();
138 for (module, source) in entries {
139 hasher.update(module.as_bytes());
140 hasher.update(b"\0");
141 hasher.update(source.as_bytes());
142 hasher.update(b"\0");
143 }
144 hasher.finalize().into()
145}
146
147fn fingerprint_from_parts(
148 harn_version: &str,
149 embedded_stdlib_digest: &[u8; 32],
150 compatibility: RuntimeCompatibilityFingerprint,
151 source_revision: Option<String>,
152) -> RuntimeContentFingerprint {
153 let mut hasher = Sha256::new();
154 hasher.update(CONTENT_DIGEST_DOMAIN);
155 update_field(&mut hasher, "harn-version", harn_version.as_bytes());
156 update_field(
157 &mut hasher,
158 "embedded-stdlib-sha256",
159 embedded_stdlib_digest,
160 );
161 update_field(
162 &mut hasher,
163 "codegen-fingerprint",
164 compatibility.codegen_fingerprint.as_bytes(),
165 );
166 update_field(
167 &mut hasher,
168 "bytecode-schema-version",
169 &compatibility.bytecode_schema_version.to_le_bytes(),
170 );
171 update_field(
172 &mut hasher,
173 "linked-program-schema-version",
174 &compatibility.linked_program_schema_version.to_le_bytes(),
175 );
176 update_field(
177 &mut hasher,
178 "linker-algorithm-version",
179 &compatibility.linker_algorithm_version.to_le_bytes(),
180 );
181 compatibility.build_features.update_digest(&mut hasher);
182 RuntimeContentFingerprint {
183 schema: FINGERPRINT_SCHEMA.to_string(),
184 content_sha256: hex(&hasher.finalize()),
185 harn_version: harn_version.to_string(),
186 embedded_stdlib_sha256: hex(embedded_stdlib_digest),
187 compatibility,
188 source_revision,
189 }
190}
191
192fn update_bool_field(hasher: &mut Sha256, name: &str, enabled: bool) {
193 update_field(hasher, name, &[u8::from(enabled)]);
194}
195
196fn update_field(hasher: &mut Sha256, name: &str, value: &[u8]) {
197 hasher.update(name.as_bytes());
198 hasher.update(b"\0");
199 hasher.update((value.len() as u64).to_le_bytes());
200 hasher.update(value);
201}
202
203fn linked_source_revision() -> Option<String> {
204 normalize_source_revision(option_env!("HARN_BUILD_REVISION")).map(str::to_string)
205}
206
207fn normalize_source_revision(raw: Option<&str>) -> Option<&str> {
208 let revision = raw?.trim();
209 if matches!(revision.len(), 40 | 64)
210 && revision
211 .bytes()
212 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
213 {
214 Some(revision)
215 } else {
216 None
217 }
218}
219
220fn hex(bytes: &[u8]) -> String {
221 let mut output = String::with_capacity(bytes.len() * 2);
222 for byte in bytes {
223 use std::fmt::Write as _;
224 write!(&mut output, "{byte:02x}").expect("writing to String cannot fail");
225 }
226 output
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232
233 fn lean_features() -> RuntimeBuildFeatures {
234 RuntimeBuildFeatures::none()
235 }
236
237 fn compatibility() -> RuntimeCompatibilityFingerprint {
238 RuntimeCompatibilityFingerprint {
239 codegen_fingerprint: "codegen-a".to_string(),
240 bytecode_schema_version: 14,
241 linked_program_schema_version: 1,
242 linker_algorithm_version: 1,
243 build_features: lean_features(),
244 }
245 }
246
247 #[test]
248 fn embedded_stdlib_digest_changes_when_one_source_byte_changes() {
249 let original = embedded_stdlib_digest_from_sources([("agent", "pub fn run() {}")]);
250 let changed = embedded_stdlib_digest_from_sources([("agent", "pub fn run() { }")]);
251 assert_ne!(original, changed);
252 }
253
254 #[test]
255 fn source_stamp_cannot_change_content_digest() {
256 let stdlib = embedded_stdlib_digest_from_sources([("agent", "source")]);
257 let first = fingerprint_from_parts(
258 "0.10.123-dev",
259 &stdlib,
260 compatibility(),
261 Some("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string()),
262 );
263 let second = fingerprint_from_parts(
264 "0.10.123-dev",
265 &stdlib,
266 compatibility(),
267 Some("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string()),
268 );
269 assert_eq!(first.content_sha256, second.content_sha256);
270 assert_ne!(first.source_revision, second.source_revision);
271 }
272
273 #[test]
274 fn compatibility_change_changes_content_digest() {
275 let stdlib = embedded_stdlib_digest_from_sources([("agent", "source")]);
276 let first = fingerprint_from_parts("0.10.123-dev", &stdlib, compatibility(), None);
277 let mut changed = compatibility();
278 changed.bytecode_schema_version += 1;
279 let second = fingerprint_from_parts("0.10.123-dev", &stdlib, changed, None);
280 assert_ne!(first.content_sha256, second.content_sha256);
281 }
282
283 #[test]
284 fn full_and_lean_builds_have_distinct_content_digests() {
285 let stdlib = embedded_stdlib_digest_from_sources([("agent", "source")]);
286 let lean = fingerprint_from_parts("0.10.123-dev", &stdlib, compatibility(), None);
287 let mut full_compatibility = compatibility();
288 for name in [
289 "default",
290 "full",
291 "postgres",
292 "sqlite",
293 "content",
294 "compression",
295 "http-compression",
296 "cloud-aws",
297 "native-keyring",
298 ] {
299 full_compatibility.build_features.enable(name);
300 }
301 let full = fingerprint_from_parts("0.10.123-dev", &stdlib, full_compatibility, None);
302 assert_ne!(lean.content_sha256, full.content_sha256);
303 }
304
305 #[test]
306 fn every_typed_build_feature_changes_content_digest() {
307 let stdlib = embedded_stdlib_digest_from_sources([("agent", "source")]);
308 let baseline = fingerprint_from_parts("0.10.123-dev", &stdlib, compatibility(), None);
309 for name in RuntimeBuildFeatures::CARGO_NAMES {
310 let mut changed = compatibility();
311 changed.build_features.enable(name);
312 let fingerprint = fingerprint_from_parts("0.10.123-dev", &stdlib, changed, None);
313 assert_ne!(
314 baseline.content_sha256, fingerprint.content_sha256,
315 "feature {name} must affect runtime content identity"
316 );
317 }
318 }
319
320 #[test]
321 fn typed_build_features_cover_every_declared_cargo_feature() {
322 let manifest: toml::Value =
323 toml::from_str(include_str!("../Cargo.toml")).expect("harn-vm Cargo.toml parses");
324 let mut declared: Vec<&str> = manifest["features"]
325 .as_table()
326 .expect("features table")
327 .keys()
328 .map(String::as_str)
329 .collect();
330 declared.sort_unstable();
331 let mut represented = RuntimeBuildFeatures::CARGO_NAMES.to_vec();
332 represented.sort_unstable();
333 assert_eq!(declared, represented);
334 }
335
336 #[test]
337 fn missing_source_revision_remains_absent() {
338 let stdlib = embedded_stdlib_digest_from_sources([("agent", "source")]);
339 let fingerprint = fingerprint_from_parts("0.10.123-dev", &stdlib, compatibility(), None);
340 assert_eq!(fingerprint.source_revision, None);
341 assert_eq!(normalize_source_revision(Some("")), None);
342 assert_eq!(normalize_source_revision(Some("not-a-revision")), None);
343 }
344
345 #[test]
346 fn public_fingerprint_matches_current_binary_inputs() {
347 let actual = runtime_content_fingerprint();
348 assert_eq!(actual.schema, FINGERPRINT_SCHEMA);
349 assert_eq!(actual.harn_version, crate::bytecode_cache::HARN_VERSION);
350 assert_eq!(
351 actual.embedded_stdlib_sha256,
352 hex(embedded_stdlib_digest_bytes())
353 );
354 assert_eq!(
355 actual.compatibility.codegen_fingerprint,
356 crate::bytecode_cache::CODEGEN_FINGERPRINT
357 );
358 assert_eq!(
359 actual.compatibility.build_features,
360 RuntimeBuildFeatures::linked()
361 );
362 assert_eq!(actual.content_sha256.len(), 64);
363 }
364}