fidius_core/hash.rs
1// Copyright 2026 Colliery, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! FNV-1a interface hashing for compile-time ABI drift detection.
16//!
17//! The proc macro computes an `interface_hash` from the sorted required method
18//! signatures of a trait. The host checks this hash at load time to reject
19//! plugins compiled against a different interface.
20
21/// FNV-1a 64-bit offset basis.
22const FNV_OFFSET_BASIS: u64 = 0xcbf29ce484222325;
23
24/// FNV-1a 64-bit prime.
25const FNV_PRIME: u64 = 0x100000001b3;
26
27/// Compute the FNV-1a 64-bit hash of a byte slice.
28pub const fn fnv1a(bytes: &[u8]) -> u64 {
29 let mut hash = FNV_OFFSET_BASIS;
30 let mut i = 0;
31 while i < bytes.len() {
32 hash ^= bytes[i] as u64;
33 hash = hash.wrapping_mul(FNV_PRIME);
34 i += 1;
35 }
36 hash
37}
38
39/// Compute the interface hash from a set of method signatures.
40///
41/// Signatures are sorted lexicographically before hashing to ensure
42/// order-independence. Each signature is joined with `\n` as a separator.
43///
44/// This function is **not** `const` because it allocates for sorting.
45/// The proc macro calls this at compile time via a build-script-like pattern,
46/// or uses `fnv1a` directly on pre-sorted, concatenated signatures.
47pub fn interface_hash(signatures: &[&str]) -> u64 {
48 let mut sorted: Vec<&str> = signatures.to_vec();
49 sorted.sort();
50 let combined = sorted.join("\n");
51 fnv1a(combined.as_bytes())
52}
53
54#[cfg(test)]
55mod tests {
56 use super::*;
57
58 #[test]
59 fn empty_input() {
60 // Empty string should produce the offset basis XOR'd with nothing,
61 // which is just the offset basis.
62 assert_eq!(fnv1a(b""), FNV_OFFSET_BASIS);
63 }
64
65 #[test]
66 fn known_vector() {
67 // FNV-1a("fidius") — precomputed reference value
68 let hash = fnv1a(b"fidius");
69 // Just verify it's deterministic and non-zero
70 assert_ne!(hash, 0);
71 assert_eq!(hash, fnv1a(b"fidius"));
72 }
73
74 #[test]
75 fn order_independence() {
76 let a = interface_hash(&[
77 "process:&[u8],Value->Result<Vec<u8>,PluginError>",
78 "name:->String",
79 ]);
80 let b = interface_hash(&[
81 "name:->String",
82 "process:&[u8],Value->Result<Vec<u8>,PluginError>",
83 ]);
84 assert_eq!(a, b);
85 }
86
87 #[test]
88 fn sensitivity() {
89 let a = interface_hash(&["name:->String"]);
90 let b = interface_hash(&["name:->string"]); // lowercase 's'
91 assert_ne!(a, b);
92 }
93
94 #[test]
95 fn different_signatures_differ() {
96 let a = interface_hash(&["foo:->i32"]);
97 let b = interface_hash(&["bar:->i32"]);
98 let c = interface_hash(&["foo:->u32"]);
99 assert_ne!(a, b);
100 assert_ne!(a, c);
101 assert_ne!(b, c);
102 }
103}