agent_mesh_protocol/
fingerprint.rs1use crate::MeshError;
9use serde::{Deserialize, Serialize};
10use std::fmt;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
15pub struct Fingerprint(pub [u8; 32]);
16
17impl Fingerprint {
18 #[must_use]
21 pub fn of_bytes(data: &[u8]) -> Self {
22 let h = blake3::hash(data);
23 Self(*h.as_bytes())
24 }
25
26 #[must_use]
30 pub fn short(&self) -> String {
31 hex::encode(&self.0[..6])
32 }
33
34 #[must_use]
36 pub fn hex(&self) -> String {
37 hex::encode(self.0)
38 }
39}
40
41impl fmt::Display for Fingerprint {
42 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43 write!(f, "{}", self.short())
44 }
45}
46
47impl std::str::FromStr for Fingerprint {
48 type Err = MeshError;
49
50 fn from_str(s: &str) -> Result<Self, Self::Err> {
51 let bytes = hex::decode(s).map_err(|e| MeshError::Encoding(e.to_string()))?;
52 if bytes.len() != 32 {
53 return Err(MeshError::Encoding(format!(
54 "expected 32 bytes, got {}",
55 bytes.len()
56 )));
57 }
58 let mut arr = [0u8; 32];
59 arr.copy_from_slice(&bytes);
60 Ok(Self(arr))
61 }
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67 use std::str::FromStr;
68
69 #[test]
70 fn short_truncates_to_12_hex_chars() {
71 let fp = Fingerprint::of_bytes(b"hello world");
72 let s = fp.short();
73 assert_eq!(s.len(), 12, "short should be 12 hex chars (6 bytes)");
74 assert!(s.chars().all(|c| c.is_ascii_hexdigit()));
75 }
76
77 #[test]
78 fn roundtrip_hex() {
79 let fp = Fingerprint::of_bytes(b"some bytes");
80 let h = fp.hex();
81 assert_eq!(h.len(), 64);
82 let parsed = Fingerprint::from_str(&h).expect("parse roundtrip");
83 assert_eq!(fp, parsed);
84 }
85
86 #[test]
87 fn equality() {
88 let a = Fingerprint::of_bytes(b"x");
89 let b = Fingerprint::of_bytes(b"x");
90 let c = Fingerprint::of_bytes(b"y");
91 assert_eq!(a, b);
92 assert_ne!(a, c);
93 }
94
95 #[test]
96 fn from_str_rejects_wrong_length() {
97 let err = Fingerprint::from_str("deadbeef").expect_err("too short");
98 match err {
99 MeshError::Encoding(_) => {}
100 other => panic!("expected Encoding, got {other:?}"),
101 }
102 }
103
104 #[test]
105 fn from_str_rejects_non_hex() {
106 let err = Fingerprint::from_str("zz").expect_err("not hex");
107 match err {
108 MeshError::Encoding(_) => {}
109 other => panic!("expected Encoding, got {other:?}"),
110 }
111 }
112
113 #[test]
114 fn display_matches_short() {
115 let fp = Fingerprint::of_bytes(b"display test");
116 assert_eq!(format!("{fp}"), fp.short());
117 }
118
119 #[test]
120 fn debug_does_not_panic() {
121 let fp = Fingerprint::of_bytes(b"debug");
122 let _ = format!("{fp:?}");
123 }
124
125 #[test]
126 fn hash_in_collection() {
127 use std::collections::HashSet;
128 let mut set = HashSet::new();
129 set.insert(Fingerprint::of_bytes(b"a"));
130 set.insert(Fingerprint::of_bytes(b"a"));
131 set.insert(Fingerprint::of_bytes(b"b"));
132 assert_eq!(set.len(), 2);
133 }
134}