Skip to main content

quant_eval/
fingerprint.rs

1//! Machine fingerprint for benchmark receipt provenance.
2
3use blake3::Hasher;
4use serde::{Deserialize, Serialize};
5use std::env;
6use std::fs;
7
8/// Machine fingerprint combines machine identity for benchmark receipts.
9///
10/// This enables reproducible comparisons across different machines
11/// by providing a stable identifier for the run environment.
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13pub struct MachineFingerprint {
14    /// Full fingerprint string (hex encoded)
15    fingerprint: String,
16}
17
18impl MachineFingerprint {
19    /// Create a new fingerprint from the current machine environment.
20    pub fn new() -> Self {
21        let mut hasher = Hasher::new();
22
23        // hostname
24        if let Ok(hostname) = env::var("HOSTNAME").or_else(|_| env::var("HOST")) {
25            hasher.update(b"hostname:");
26            hasher.update(hostname.as_bytes());
27        }
28
29        // username
30        if let Ok(user) = env::var("USER").or_else(|_| env::var("USERNAME")) {
31            hasher.update(b"user:");
32            hasher.update(user.as_bytes());
33        }
34
35        // architecture
36        hasher.update(b"arch:");
37        hasher.update(std::env::consts::ARCH.as_bytes());
38
39        // OS
40        hasher.update(b"os:");
41        hasher.update(std::env::consts::OS.as_bytes());
42
43        // cpu count
44        hasher.update(b"cpus:");
45        let cpus = std::thread::available_parallelism()
46            .map(|n| n.get())
47            .unwrap_or(1);
48        hasher.update(&cpus.to_le_bytes());
49
50        // machine id (if available)
51        if let Some(machine_id) = Self::machine_id() {
52            hasher.update(b"machine_id:");
53            hasher.update(machine_id.as_bytes());
54        }
55
56        let hash = hasher.finalize();
57        let fingerprint = hex::encode(hash.as_bytes());
58
59        Self { fingerprint }
60    }
61
62    /// Create a fingerprint from a hex string (for testing).
63    pub fn from_hex(hex: &str) -> Self {
64        Self {
65            fingerprint: hex.to_string(),
66        }
67    }
68
69    /// Return the fingerprint as a string slice.
70    pub fn as_str(&self) -> &str {
71        &self.fingerprint
72    }
73
74    /// Read machine ID from /etc/machine-id or similar.
75    fn machine_id() -> Option<String> {
76        let candidates = [
77            "/etc/machine-id",
78            "/var/lib/dbus/machine-id",
79            "/etc/arch-release",
80        ];
81
82        for path in candidates {
83            if let Ok(contents) = fs::read_to_string(path) {
84                let id = contents.trim().to_string();
85                if !id.is_empty() {
86                    return Some(id);
87                }
88            }
89        }
90        None
91    }
92}
93
94// Simple hex encoder
95mod hex {
96    const HEX_CHARS: &[u8; 16] = b"0123456789abcdef";
97
98    pub fn encode(data: impl AsRef<[u8]>) -> String {
99        let bytes = data.as_ref();
100        let mut s = String::with_capacity(bytes.len() * 2);
101        for &b in bytes {
102            s.push(HEX_CHARS[(b >> 4) as usize] as char);
103            s.push(HEX_CHARS[(b & 0xf) as usize] as char);
104        }
105        s
106    }
107}
108
109impl Default for MachineFingerprint {
110    fn default() -> Self {
111        Self::new()
112    }
113}
114
115impl std::fmt::Display for MachineFingerprint {
116    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117        write!(f, "{}", self.fingerprint)
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn test_fingerprint_creation() {
127        let fp = MachineFingerprint::new();
128        assert!(!fp.fingerprint.is_empty());
129        assert_eq!(fp.fingerprint.len(), 64); // 32 bytes = 64 hex chars
130    }
131
132    #[test]
133    fn test_fingerprint_from_hex() {
134        let fp = MachineFingerprint::from_hex("00".repeat(64).as_str());
135        assert_eq!(fp.as_str(), "00".repeat(64).as_str());
136    }
137
138    #[test]
139    fn test_fingerprint_display() {
140        let fp = MachineFingerprint::from_hex("ab".repeat(32).as_str());
141        let display = format!("{}", fp);
142        assert_eq!(display, "ab".repeat(32).as_str());
143    }
144}