Skip to main content

zerodds_types/
hash.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! TypeIdentifier-Hash-Computation (XTypes 1.3 §7.3.1.2).
4//!
5//! Der `EquivalenceHash` (14 byte) wird aus den **ersten 14 Bytes** des
6//! **MD5**-Digests ueber die XCDR2-serialisierte TypeObject-Darstellung
7//! gebildet (XTypes 1.3 §7.3.1.2.1):
8//!
9//! ```text
10//! EquivalenceHash(T) := MD5(xcdr2_le_bytes(T))[0..14]
11//! ```
12//!
13//! Cyclone DDS und Fast-DDS verwenden MD5 — daher pflicht fuer Live-Interop.
14//!
15//! - Fuer `MinimalTypeObject T`: `EK_MINIMAL`-Hash
16//! - Fuer `CompleteTypeObject T`: `EK_COMPLETE`-Hash
17//!
18//! Aus dem Hash wird ein TypeIdentifier der Kind `EK_MINIMAL` bzw.
19//! `EK_COMPLETE` gebaut — dieser ist die "strongly-hashed" Form des
20//! TypeObjects, wie sie ueber SEDP (`PID_TYPE_INFORMATION`) und
21//! TypeLookup-Service zwischen Peers ausgetauscht wird.
22
23use 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
30/// Berechnet den 14-byte-EquivalenceHash eines TypeObjects.
31///
32/// Serialisiert das TypeObject nach XCDR2-LE-Bytes (inkl. Equivalence-
33/// Kind-Discriminator), hasht mit SHA-256 und schneidet auf 14 byte.
34///
35/// # Errors
36/// `EncodeError` wenn Serialisierung ueberlaeuft.
37pub fn compute_hash(to: &TypeObject) -> Result<EquivalenceHash, EncodeError> {
38    let bytes = to.to_bytes_le()?;
39    Ok(hash_bytes(&bytes))
40}
41
42/// Wie [`compute_hash`], aber direkt ueber `MinimalTypeObject` (ohne
43/// EquivalenceKind-Discriminator-Wrapper-Clone). Schreibt selbst den
44/// EK_MINIMAL-Discriminator vor den Body.
45///
46/// # Errors
47/// `EncodeError`.
48pub fn compute_minimal_hash(t: &MinimalTypeObject) -> Result<EquivalenceHash, EncodeError> {
49    let mut w = BufferWriter::new(Endianness::Little);
50    w.write_u8(EK_MINIMAL)?;
51    t.encode_into(&mut w)?;
52    Ok(hash_bytes(&w.into_bytes()))
53}
54
55/// Wie [`compute_minimal_hash`], nur fuer `CompleteTypeObject`.
56///
57/// # Errors
58/// `EncodeError`.
59pub fn compute_complete_hash(t: &CompleteTypeObject) -> Result<EquivalenceHash, EncodeError> {
60    let mut w = BufferWriter::new(Endianness::Little);
61    w.write_u8(EK_COMPLETE)?;
62    t.encode_into(&mut w)?;
63    Ok(hash_bytes(&w.into_bytes()))
64}
65
66/// Rohe Hash-Funktion: MD5 + Truncate auf 14 byte.
67///
68/// MD5 ist hier **spec-konform**, nicht kryptographisch. XTypes 1.3
69/// §7.3.1.2.1 verlangt MD5 fuer Wire-Kompatibilitaet mit anderen DDS-
70/// Implementierungen (Cyclone, Fast-DDS, RTI Connext).
71#[must_use]
72pub fn hash_bytes(data: &[u8]) -> EquivalenceHash {
73    let digest = md5(data);
74    let mut out = [0u8; EQUIVALENCE_HASH_LEN];
75    out.copy_from_slice(&digest[..EQUIVALENCE_HASH_LEN]);
76    EquivalenceHash(out)
77}
78
79/// Shortcut: baut einen strongly-hashed TypeIdentifier aus einem
80/// TypeObject. Wraps [`compute_hash`] in den passenden `EquivalenceHash*`-
81/// TypeIdentifier-Variant abhaengig von Minimal/Complete.
82///
83/// # Errors
84/// `EncodeError`.
85pub fn to_hashed_type_identifier(to: &TypeObject) -> Result<TypeIdentifier, EncodeError> {
86    let h = compute_hash(to)?;
87    Ok(match to {
88        TypeObject::Minimal(_) => TypeIdentifier::EquivalenceHashMinimal(h),
89        TypeObject::Complete(_) => TypeIdentifier::EquivalenceHashComplete(h),
90    })
91}
92
93// ============================================================================
94// Tests
95// ============================================================================
96
97#[cfg(test)]
98#[allow(clippy::unwrap_used)]
99mod tests {
100    use super::*;
101
102    use crate::type_identifier::PrimitiveKind;
103    use crate::type_object::common::{CommonStructMember, NameHash};
104    use crate::type_object::flags::{StructMemberFlag, StructTypeFlag};
105    use crate::type_object::minimal::{
106        MinimalStructHeader, MinimalStructMember, MinimalStructType,
107    };
108
109    fn sample_minimal_struct(field_count: u32) -> MinimalTypeObject {
110        MinimalTypeObject::Struct(MinimalStructType {
111            struct_flags: StructTypeFlag(StructTypeFlag::IS_APPENDABLE),
112            header: MinimalStructHeader {
113                base_type: TypeIdentifier::None,
114            },
115            member_seq: (0..field_count)
116                .map(|i| MinimalStructMember {
117                    common: CommonStructMember {
118                        member_id: i + 1,
119                        member_flags: StructMemberFlag::default(),
120                        member_type_id: TypeIdentifier::Primitive(PrimitiveKind::Int64),
121                    },
122                    detail: NameHash([i as u8; 4]),
123                })
124                .collect(),
125        })
126    }
127
128    #[test]
129    fn hash_is_14_bytes_and_deterministic() {
130        let t = sample_minimal_struct(3);
131        let h1 = compute_minimal_hash(&t).unwrap();
132        let h2 = compute_minimal_hash(&t).unwrap();
133        assert_eq!(h1, h2);
134        assert_eq!(h1.0.len(), 14);
135    }
136
137    #[test]
138    fn different_type_objects_have_different_hashes() {
139        let t1 = sample_minimal_struct(3);
140        let t2 = sample_minimal_struct(4);
141        let h1 = compute_minimal_hash(&t1).unwrap();
142        let h2 = compute_minimal_hash(&t2).unwrap();
143        assert_ne!(h1, h2);
144    }
145
146    #[test]
147    fn minimal_and_complete_same_semantic_differ_in_hash() {
148        // Selbst wenn Minimal und Complete denselben "Typ" darstellen,
149        // unterscheidet sie der Equivalence-Kind-Discriminator am Anfang
150        // der serialisierten Bytes → unterschiedliche Hashes.
151        use crate::type_object::common::{
152            AppliedBuiltinMemberAnnotations, AppliedBuiltinTypeAnnotations, CompleteMemberDetail,
153            CompleteTypeDetail, OptionalAppliedAnnotationSeq,
154        };
155        use crate::type_object::complete::{
156            CompleteStructHeader, CompleteStructMember, CompleteStructType,
157        };
158
159        let minimal = sample_minimal_struct(1);
160        let complete = CompleteStructType {
161            struct_flags: StructTypeFlag(StructTypeFlag::IS_APPENDABLE),
162            header: CompleteStructHeader {
163                base_type: TypeIdentifier::None,
164                detail: CompleteTypeDetail {
165                    ann_builtin: AppliedBuiltinTypeAnnotations::default(),
166                    ann_custom: OptionalAppliedAnnotationSeq::default(),
167                    type_name: alloc::string::String::from("::Sample"),
168                },
169            },
170            member_seq: alloc::vec![CompleteStructMember {
171                common: CommonStructMember {
172                    member_id: 1,
173                    member_flags: StructMemberFlag::default(),
174                    member_type_id: TypeIdentifier::Primitive(PrimitiveKind::Int64),
175                },
176                detail: CompleteMemberDetail {
177                    name: alloc::string::String::from("x"),
178                    ann_builtin: AppliedBuiltinMemberAnnotations::default(),
179                    ann_custom: OptionalAppliedAnnotationSeq::default(),
180                },
181            }],
182        };
183
184        let hm = compute_minimal_hash(&minimal).unwrap();
185        let complete_wrapped = CompleteTypeObject::Struct(complete);
186        let hc = compute_complete_hash(&complete_wrapped).unwrap();
187        assert_ne!(hm, hc, "minimal and complete must hash to different values");
188    }
189
190    #[test]
191    fn to_hashed_type_identifier_picks_correct_kind() {
192        let minimal = sample_minimal_struct(2);
193        let ti = to_hashed_type_identifier(&TypeObject::Minimal(minimal.clone())).unwrap();
194        assert!(matches!(ti, TypeIdentifier::EquivalenceHashMinimal(_)));
195
196        // Complete-Variant mit Minimal-Shape (wir simulieren nur den
197        // Dispatch) — hier nutzen wir eine Array-Fixture, damit kein
198        // Name verifiziert werden muss.
199        use crate::type_object::common::{
200            AppliedBuiltinMemberAnnotations, AppliedBuiltinTypeAnnotations, CompleteTypeDetail,
201            OptionalAppliedAnnotationSeq,
202        };
203        use crate::type_object::complete::{CompleteCollectionElement, CompleteSequenceType};
204        use crate::type_object::flags::{CollectionElementFlag, CollectionTypeFlag};
205        use crate::type_object::minimal::CommonCollectionElement;
206
207        let complete_seq = CompleteSequenceType {
208            collection_flag: CollectionTypeFlag::default(),
209            bound: 10,
210            detail: CompleteTypeDetail {
211                ann_builtin: AppliedBuiltinTypeAnnotations::default(),
212                ann_custom: OptionalAppliedAnnotationSeq::default(),
213                type_name: alloc::string::String::from("::Seq"),
214            },
215            element: CompleteCollectionElement {
216                common: CommonCollectionElement {
217                    element_flags: CollectionElementFlag::default(),
218                    type_id: TypeIdentifier::Primitive(PrimitiveKind::Int32),
219                },
220                ann_builtin: AppliedBuiltinMemberAnnotations::default(),
221                ann_custom: OptionalAppliedAnnotationSeq::default(),
222            },
223        };
224        let ti2 = to_hashed_type_identifier(&TypeObject::Complete(CompleteTypeObject::Sequence(
225            complete_seq,
226        )))
227        .unwrap();
228        assert!(matches!(ti2, TypeIdentifier::EquivalenceHashComplete(_)));
229    }
230
231    #[test]
232    fn hash_bytes_matches_md5_truncated_reference() {
233        // MD5("") = d41d8cd98f00b204e9800998ecf8427e
234        // Erste 14 bytes: d4 1d 8c d9 8f 00 b2 04 e9 80 09 98 ec f8
235        let h = hash_bytes(b"");
236        let expected: [u8; 14] = [
237            0xd4, 0x1d, 0x8c, 0xd9, 0x8f, 0x00, 0xb2, 0x04, 0xe9, 0x80, 0x09, 0x98, 0xec, 0xf8,
238        ];
239        assert_eq!(h.0, expected);
240    }
241
242    #[test]
243    fn hash_bytes_deterministic_for_known_input() {
244        let h1 = hash_bytes(b"ZeroDDS");
245        let h2 = hash_bytes(b"ZeroDDS");
246        assert_eq!(h1, h2);
247        let h3 = hash_bytes(b"ZeroDDs"); // case-sensitiv
248        assert_ne!(h1, h3);
249    }
250}