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> =
165 (1u8..=6).map(|b| GuidPrefix::from_bytes([b; 12])).collect();
166 let d = GroupDigest::from_prefixes(&prefixes);
167 assert!(!d.is_unknown());
169 let mut reversed = prefixes.clone();
171 reversed.reverse();
172 let d2 = GroupDigest::from_prefixes(&reversed);
173 assert_eq!(d, d2);
174 }
175
176 #[test]
177 fn group_digest_unknown_is_zero() {
178 assert!(GroupDigest::UNKNOWN.is_unknown());
179 assert_eq!(GroupDigest::UNKNOWN.0, [0u8; 16]);
180 }
181
182 #[test]
183 fn group_digest_from_empty_prefixes_is_md5_of_empty() {
184 let d = GroupDigest::from_prefixes(&[]);
185 assert_eq!(
187 d.0,
188 [
189 0xd4, 0x1d, 0x8c, 0xd9, 0x8f, 0x00, 0xb2, 0x04, 0xe9, 0x80, 0x09, 0x98, 0xec, 0xf8,
190 0x42, 0x7e
191 ]
192 );
193 }
194
195 #[test]
196 fn group_digest_independent_of_input_order() {
197 let p1 = GuidPrefix::from_bytes([1; 12]);
198 let p2 = GuidPrefix::from_bytes([2; 12]);
199 let p3 = GuidPrefix::from_bytes([3; 12]);
200 let a = GroupDigest::from_prefixes(&[p1, p2, p3]);
201 let b = GroupDigest::from_prefixes(&[p3, p2, p1]);
202 assert_eq!(a, b);
203 }
204
205 #[test]
206 fn group_digest_distinguishes_different_groups() {
207 let g1 = GroupDigest::from_prefixes(&[GuidPrefix::from_bytes([1; 12])]);
208 let g2 = GroupDigest::from_prefixes(&[GuidPrefix::from_bytes([2; 12])]);
209 assert_ne!(g1, g2);
210 }
211
212 #[test]
213 fn group_digest_roundtrip() {
214 let d = GroupDigest::from_prefixes(&[
215 GuidPrefix::from_bytes([7; 12]),
216 GuidPrefix::from_bytes([8; 12]),
217 ]);
218 let bytes = d.to_bytes();
219 assert_eq!(GroupDigest::from_bytes(bytes), d);
220 assert_eq!(GroupDigest::read_from(&bytes).unwrap(), d);
221 }
222
223 #[test]
224 fn group_digest_read_from_truncated_rejects() {
225 let r = GroupDigest::read_from(&[0u8; 8]);
226 assert!(matches!(r, Err(WireError::UnexpectedEof { .. })));
227 }
228}