1use zerodds_cdr::{BufferWriter, EncodeError, Endianness};
24use zerodds_foundation::md5;
25
26use crate::type_identifier::kinds::{EK_COMPLETE, EK_MINIMAL, EQUIVALENCE_HASH_LEN};
27use crate::type_identifier::{EquivalenceHash, TypeIdentifier};
28use crate::type_object::{CompleteTypeObject, MinimalTypeObject, TypeObject};
29
30pub fn compute_hash(to: &TypeObject) -> Result<EquivalenceHash, EncodeError> {
38 match to {
43 TypeObject::Minimal(m) => compute_minimal_hash(m),
44 TypeObject::Complete(c) => compute_complete_hash(c),
45 }
46}
47
48pub fn compute_minimal_hash(t: &MinimalTypeObject) -> Result<EquivalenceHash, EncodeError> {
55 Ok(hash_bytes(&xcdr2_typeobject_stream(EK_MINIMAL, |w| {
56 t.encode_into(w)
57 })?))
58}
59
60pub fn compute_complete_hash(t: &CompleteTypeObject) -> Result<EquivalenceHash, EncodeError> {
65 Ok(hash_bytes(&xcdr2_typeobject_stream(EK_COMPLETE, |w| {
66 t.encode_into(w)
67 })?))
68}
69
70pub fn minimal_hash_input(t: &MinimalTypeObject) -> Result<alloc::vec::Vec<u8>, EncodeError> {
77 xcdr2_typeobject_stream(EK_MINIMAL, |w| t.encode_into(w))
78}
79
80pub fn complete_hash_input(t: &CompleteTypeObject) -> Result<alloc::vec::Vec<u8>, EncodeError> {
85 xcdr2_typeobject_stream(EK_COMPLETE, |w| t.encode_into(w))
86}
87
88fn xcdr2_typeobject_stream(
95 ek: u8,
96 body: impl FnOnce(&mut BufferWriter) -> Result<(), EncodeError>,
97) -> Result<alloc::vec::Vec<u8>, EncodeError> {
98 let mut inner = BufferWriter::new(Endianness::Little).xcdr2();
99 inner.write_u8(ek)?;
100 body(&mut inner)?;
101 let body_bytes = inner.into_bytes();
102 let dheader = u32::try_from(body_bytes.len()).map_err(|_| EncodeError::ValueOutOfRange {
103 message: "TypeObject body exceeds u32::MAX",
104 })?;
105 let mut out = BufferWriter::new(Endianness::Little).xcdr2();
106 out.write_u32(dheader)?;
107 out.write_bytes(&body_bytes)?;
108 Ok(out.into_bytes())
109}
110
111#[must_use]
117pub fn hash_bytes(data: &[u8]) -> EquivalenceHash {
118 let digest = md5(data);
119 let mut out = [0u8; EQUIVALENCE_HASH_LEN];
120 out.copy_from_slice(&digest[..EQUIVALENCE_HASH_LEN]);
121 EquivalenceHash(out)
122}
123
124pub fn to_hashed_type_identifier(to: &TypeObject) -> Result<TypeIdentifier, EncodeError> {
131 let h = compute_hash(to)?;
132 Ok(match to {
133 TypeObject::Minimal(_) => TypeIdentifier::EquivalenceHashMinimal(h),
134 TypeObject::Complete(_) => TypeIdentifier::EquivalenceHashComplete(h),
135 })
136}
137
138#[cfg(test)]
143#[allow(clippy::unwrap_used)]
144mod tests {
145 use super::*;
146
147 use crate::type_identifier::PrimitiveKind;
148 use crate::type_object::common::{CommonStructMember, NameHash};
149 use crate::type_object::flags::{StructMemberFlag, StructTypeFlag};
150 use crate::type_object::minimal::{
151 MinimalStructHeader, MinimalStructMember, MinimalStructType,
152 };
153
154 fn sample_minimal_struct(field_count: u32) -> MinimalTypeObject {
155 MinimalTypeObject::Struct(MinimalStructType {
156 struct_flags: StructTypeFlag(StructTypeFlag::IS_APPENDABLE),
157 header: MinimalStructHeader {
158 base_type: TypeIdentifier::None,
159 },
160 member_seq: (0..field_count)
161 .map(|i| MinimalStructMember {
162 common: CommonStructMember {
163 member_id: i + 1,
164 member_flags: StructMemberFlag::default(),
165 member_type_id: TypeIdentifier::Primitive(PrimitiveKind::Int64),
166 },
167 detail: NameHash([i as u8; 4]),
168 })
169 .collect(),
170 })
171 }
172
173 #[test]
174 fn hash_is_14_bytes_and_deterministic() {
175 let t = sample_minimal_struct(3);
176 let h1 = compute_minimal_hash(&t).unwrap();
177 let h2 = compute_minimal_hash(&t).unwrap();
178 assert_eq!(h1, h2);
179 assert_eq!(h1.0.len(), 14);
180 }
181
182 #[test]
183 fn different_type_objects_have_different_hashes() {
184 let t1 = sample_minimal_struct(3);
185 let t2 = sample_minimal_struct(4);
186 let h1 = compute_minimal_hash(&t1).unwrap();
187 let h2 = compute_minimal_hash(&t2).unwrap();
188 assert_ne!(h1, h2);
189 }
190
191 #[test]
192 fn minimal_and_complete_same_semantic_differ_in_hash() {
193 use crate::type_object::common::{
197 AppliedBuiltinMemberAnnotations, AppliedBuiltinTypeAnnotations, CompleteMemberDetail,
198 CompleteTypeDetail, OptionalAppliedAnnotationSeq,
199 };
200 use crate::type_object::complete::{
201 CompleteStructHeader, CompleteStructMember, CompleteStructType,
202 };
203
204 let minimal = sample_minimal_struct(1);
205 let complete = CompleteStructType {
206 struct_flags: StructTypeFlag(StructTypeFlag::IS_APPENDABLE),
207 header: CompleteStructHeader {
208 base_type: TypeIdentifier::None,
209 detail: CompleteTypeDetail {
210 ann_builtin: AppliedBuiltinTypeAnnotations::default(),
211 ann_custom: OptionalAppliedAnnotationSeq::default(),
212 type_name: alloc::string::String::from("::Sample"),
213 },
214 },
215 member_seq: alloc::vec![CompleteStructMember {
216 common: CommonStructMember {
217 member_id: 1,
218 member_flags: StructMemberFlag::default(),
219 member_type_id: TypeIdentifier::Primitive(PrimitiveKind::Int64),
220 },
221 detail: CompleteMemberDetail {
222 name: alloc::string::String::from("x"),
223 ann_builtin: AppliedBuiltinMemberAnnotations::default(),
224 ann_custom: OptionalAppliedAnnotationSeq::default(),
225 },
226 }],
227 };
228
229 let hm = compute_minimal_hash(&minimal).unwrap();
230 let complete_wrapped = CompleteTypeObject::Struct(complete);
231 let hc = compute_complete_hash(&complete_wrapped).unwrap();
232 assert_ne!(hm, hc, "minimal and complete must hash to different values");
233 }
234
235 #[test]
236 fn to_hashed_type_identifier_picks_correct_kind() {
237 let minimal = sample_minimal_struct(2);
238 let ti = to_hashed_type_identifier(&TypeObject::Minimal(minimal.clone())).unwrap();
239 assert!(matches!(ti, TypeIdentifier::EquivalenceHashMinimal(_)));
240
241 use crate::type_object::common::{
245 AppliedBuiltinMemberAnnotations, AppliedBuiltinTypeAnnotations, CompleteTypeDetail,
246 OptionalAppliedAnnotationSeq,
247 };
248 use crate::type_object::complete::{CompleteCollectionElement, CompleteSequenceType};
249 use crate::type_object::flags::{CollectionElementFlag, CollectionTypeFlag};
250 use crate::type_object::minimal::CommonCollectionElement;
251
252 let complete_seq = CompleteSequenceType {
253 collection_flag: CollectionTypeFlag::default(),
254 bound: 10,
255 detail: CompleteTypeDetail {
256 ann_builtin: AppliedBuiltinTypeAnnotations::default(),
257 ann_custom: OptionalAppliedAnnotationSeq::default(),
258 type_name: alloc::string::String::from("::Seq"),
259 },
260 element: CompleteCollectionElement {
261 common: CommonCollectionElement {
262 element_flags: CollectionElementFlag::default(),
263 type_id: TypeIdentifier::Primitive(PrimitiveKind::Int32),
264 },
265 ann_builtin: AppliedBuiltinMemberAnnotations::default(),
266 ann_custom: OptionalAppliedAnnotationSeq::default(),
267 },
268 };
269 let ti2 = to_hashed_type_identifier(&TypeObject::Complete(CompleteTypeObject::Sequence(
270 complete_seq,
271 )))
272 .unwrap();
273 assert!(matches!(ti2, TypeIdentifier::EquivalenceHashComplete(_)));
274 }
275
276 #[test]
277 fn hash_bytes_matches_md5_truncated_reference() {
278 let h = hash_bytes(b"");
281 let expected: [u8; 14] = [
282 0xd4, 0x1d, 0x8c, 0xd9, 0x8f, 0x00, 0xb2, 0x04, 0xe9, 0x80, 0x09, 0x98, 0xec, 0xf8,
283 ];
284 assert_eq!(h.0, expected);
285 }
286
287 #[test]
288 fn hash_bytes_deterministic_for_known_input() {
289 let h1 = hash_bytes(b"ZeroDDS");
290 let h2 = hash_bytes(b"ZeroDDS");
291 assert_eq!(h1, h2);
292 let h3 = hash_bytes(b"ZeroDDs"); assert_ne!(h1, h3);
294 }
295}