1extern crate alloc;
26use alloc::vec::Vec;
27
28use crate::error::WireError;
29use crate::wire_types::GuidPrefix;
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
33pub struct GroupDigest(pub [u8; 16]);
34
35impl GroupDigest {
36 pub const WIRE_SIZE: usize = 16;
38
39 pub const UNKNOWN: Self = Self([0; 16]);
41
42 #[must_use]
44 pub fn is_unknown(self) -> bool {
45 self.0 == [0u8; 16]
46 }
47
48 #[must_use]
52 pub fn from_prefixes(prefixes: &[GuidPrefix]) -> Self {
53 let mut sorted: Vec<&GuidPrefix> = prefixes.iter().collect();
54 sorted.sort();
55 let mut concat = Vec::with_capacity(sorted.len() * GuidPrefix::WIRE_SIZE);
56 for p in sorted {
57 concat.extend_from_slice(&p.0);
58 }
59 Self(md5_128(&concat))
60 }
61
62 #[must_use]
65 pub fn to_bytes(self) -> [u8; 16] {
66 self.0
67 }
68
69 #[must_use]
71 pub fn from_bytes(bytes: [u8; 16]) -> Self {
72 Self(bytes)
73 }
74
75 pub fn read_from(bytes: &[u8]) -> Result<Self, WireError> {
80 if bytes.len() < 16 {
81 return Err(WireError::UnexpectedEof {
82 needed: 16,
83 offset: 0,
84 });
85 }
86 let mut out = [0u8; 16];
87 out.copy_from_slice(&bytes[..16]);
88 Ok(Self(out))
89 }
90}
91
92fn md5_128(input: &[u8]) -> [u8; 16] {
96 zerodds_foundation::md5(input)
97}
98
99#[cfg(test)]
100mod tests {
101 #![allow(clippy::expect_used, clippy::unwrap_used)]
102 use super::*;
103
104 #[test]
105 fn md5_empty_input_matches_rfc1321_test_vector() {
106 let h = md5_128(b"");
108 assert_eq!(
109 h,
110 [
111 0xd4, 0x1d, 0x8c, 0xd9, 0x8f, 0x00, 0xb2, 0x04, 0xe9, 0x80, 0x09, 0x98, 0xec, 0xf8,
112 0x42, 0x7e
113 ]
114 );
115 }
116
117 #[test]
118 fn md5_abc_matches_rfc1321_test_vector() {
119 let h = md5_128(b"abc");
121 assert_eq!(
122 h,
123 [
124 0x90, 0x01, 0x50, 0x98, 0x3c, 0xd2, 0x4f, 0xb0, 0xd6, 0x96, 0x3f, 0x7d, 0x28, 0xe1,
125 0x7f, 0x72
126 ]
127 );
128 }
129
130 #[test]
131 fn md5_message_digest_matches_rfc1321_test_vector() {
132 let h = md5_128(b"message digest");
135 assert_eq!(
136 h,
137 [
138 0xf9, 0x6b, 0x69, 0x7d, 0x7c, 0xb7, 0x93, 0x8d, 0x52, 0x5a, 0x2f, 0x31, 0xaa, 0xf1,
139 0x61, 0xd0
140 ]
141 );
142 }
143
144 #[test]
145 fn md5_long_input_matches_rfc1321_test_vector() {
146 let h = md5_128(b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789");
151 assert_eq!(
152 h,
153 [
154 0xd1, 0x74, 0xab, 0x98, 0xd2, 0x77, 0xd9, 0xf5, 0xa5, 0x61, 0x1c, 0x2c, 0x9f, 0x41,
155 0x9d, 0x9f
156 ]
157 );
158 }
159
160 #[test]
161 fn group_digest_handles_more_than_5_prefixes_two_block_md5() {
162 let prefixes: alloc::vec::Vec<GuidPrefix> =
166 (1u8..=6).map(|b| GuidPrefix::from_bytes([b; 12])).collect();
167 let d = GroupDigest::from_prefixes(&prefixes);
168 assert!(!d.is_unknown());
170 let mut reversed = prefixes.clone();
172 reversed.reverse();
173 let d2 = GroupDigest::from_prefixes(&reversed);
174 assert_eq!(d, d2);
175 }
176
177 #[test]
178 fn group_digest_unknown_is_zero() {
179 assert!(GroupDigest::UNKNOWN.is_unknown());
180 assert_eq!(GroupDigest::UNKNOWN.0, [0u8; 16]);
181 }
182
183 #[test]
184 fn group_digest_from_empty_prefixes_is_md5_of_empty() {
185 let d = GroupDigest::from_prefixes(&[]);
186 assert_eq!(
188 d.0,
189 [
190 0xd4, 0x1d, 0x8c, 0xd9, 0x8f, 0x00, 0xb2, 0x04, 0xe9, 0x80, 0x09, 0x98, 0xec, 0xf8,
191 0x42, 0x7e
192 ]
193 );
194 }
195
196 #[test]
197 fn group_digest_independent_of_input_order() {
198 let p1 = GuidPrefix::from_bytes([1; 12]);
199 let p2 = GuidPrefix::from_bytes([2; 12]);
200 let p3 = GuidPrefix::from_bytes([3; 12]);
201 let a = GroupDigest::from_prefixes(&[p1, p2, p3]);
202 let b = GroupDigest::from_prefixes(&[p3, p2, p1]);
203 assert_eq!(a, b);
204 }
205
206 #[test]
207 fn group_digest_distinguishes_different_groups() {
208 let g1 = GroupDigest::from_prefixes(&[GuidPrefix::from_bytes([1; 12])]);
209 let g2 = GroupDigest::from_prefixes(&[GuidPrefix::from_bytes([2; 12])]);
210 assert_ne!(g1, g2);
211 }
212
213 #[test]
214 fn group_digest_roundtrip() {
215 let d = GroupDigest::from_prefixes(&[
216 GuidPrefix::from_bytes([7; 12]),
217 GuidPrefix::from_bytes([8; 12]),
218 ]);
219 let bytes = d.to_bytes();
220 assert_eq!(GroupDigest::from_bytes(bytes), d);
221 assert_eq!(GroupDigest::read_from(&bytes).unwrap(), d);
222 }
223
224 #[test]
225 fn group_digest_read_from_truncated_rejects() {
226 let r = GroupDigest::read_from(&[0u8; 8]);
227 assert!(matches!(r, Err(WireError::UnexpectedEof { .. })));
228 }
229}