zerodds_ast_layout/keyhash.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Key-holder planning. DDSI-RTPS 2.5 §9.6.3.8, XTypes 1.3 §7.6.8.
4//!
5//! The key holder is the one place where endianness genuinely changes
6//! the plan: it is defined big-endian regardless of what the sample
7//! itself uses, because the hash has to match across peers that disagree
8//! about byte order.
9
10use crate::key::{Encoding, KeyHolderNesting, KeyHolderOrder, LayoutKey};
11use crate::plan::{Extent, LayoutError, TypeLayout};
12use crate::xcdr::{DepCollector, plan};
13use alloc::vec::Vec;
14use zerodds_ast::{Ast, TypeKind, TypeNode};
15use zerodds_ast_core::{Id, NodeId};
16
17/// How the KeyHash is derived from the key holder.
18/// DDSI-RTPS 2.5 §9.6.3.8.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub enum KeyHashForm {
21 /// The holder is at most sixteen bytes: it becomes the hash
22 /// directly, zero-padded on the right.
23 ZeroPadded,
24 /// The holder's declared maximum exceeds sixteen bytes or is unbounded:
25 /// the hash is its MD5, even when a particular value is shorter.
26 Md5,
27}
28
29/// The maximum key-holder size that still fits a KeyHash unhashed.
30pub const KEY_HASH_LEN: u32 = 16;
31
32/// Plan the key holder of a type.
33///
34/// Contains exactly the `@key` members, recursing into nested keyed
35/// aggregates, and always big-endian.
36///
37/// # Errors
38/// [`LayoutError::KeyMustBeBigEndian`] if a little-endian key is asked
39/// for — honouring it would produce a hash that no peer agrees with.
40/// Otherwise whatever [`plan`] returns.
41pub fn key_holder_plan(
42 ast: &Ast,
43 type_: Id<TypeNode>,
44 encoding: Encoding,
45 deps: &mut DepCollector,
46) -> Result<TypeLayout, LayoutError> {
47 plan(ast, type_, LayoutKey::key(encoding), deps)
48}
49
50/// Plan the key holder under a caller-supplied key.
51///
52/// [`key_holder_plan`] plans in the default dialect, which is what a
53/// caller who has not chosen one means. This one exists because the
54/// choice reaches the holder: whether an unannotated keyed aggregate
55/// carries a DHEADER depends on what unannotated means, and two peers
56/// that answer that differently compute different KeyHashes for the
57/// same sample. A probe that writes RTI's payload bytes and Cyclone's
58/// KeyHash would be reporting a divergence it created itself.
59///
60/// The purpose and the byte order are overwritten rather than trusted:
61/// §7.6.8 fixes both, and a caller who passed a data key would
62/// otherwise get every member planned instead of the keyed ones.
63///
64/// # Errors
65/// Whatever [`plan`] returns.
66pub fn key_holder_plan_with(
67 ast: &Ast,
68 type_: Id<TypeNode>,
69 key: LayoutKey,
70 deps: &mut DepCollector,
71) -> Result<TypeLayout, LayoutError> {
72 let key = LayoutKey {
73 purpose: crate::key::Purpose::Key,
74 endianness: crate::key::Endianness::Big,
75 ..key
76 };
77 plan(ast, type_, key, deps)
78}
79
80/// Legacy fixed-extent classification, not a maximum-size calculation.
81///
82/// `Extent::Unbounded` also represents bounded variable-length strings and
83/// collections. Use [`key_hash_form_for`] for generated or runtime KeyHashes.
84#[must_use]
85pub fn key_hash_form(layout: &TypeLayout) -> KeyHashForm {
86 match layout.extent {
87 Extent::Sized(size) if size <= KEY_HASH_LEN => KeyHashForm::ZeroPadded,
88 _ => KeyHashForm::Md5,
89 }
90}
91
92/// Select KeyHash storage from the holder's maximum, not its actual extent.
93/// The walk saturates at seventeen bytes because only the sixteen-byte
94/// threshold matters, preserving alignment, bounds and selected key members.
95///
96/// # Errors
97/// Invalid layout, unresolved bounds or unsupported maximum-size semantics.
98pub fn key_hash_form_for(
99 ast: &Ast,
100 type_: Id<TypeNode>,
101 key: LayoutKey,
102) -> Result<KeyHashForm, LayoutError> {
103 let key = LayoutKey {
104 purpose: crate::key::Purpose::Key,
105 endianness: crate::key::Endianness::Big,
106 ..key
107 };
108 key_holder_plan_with(ast, type_, key, &mut DepCollector::default())?;
109 Ok(
110 if crate::key_maximum::holder(ast, type_, key)? <= KEY_HASH_LEN {
111 KeyHashForm::ZeroPadded
112 } else {
113 KeyHashForm::Md5
114 },
115 )
116}
117
118/// Whether a key holder for `type_` keeps only the `@key` members.
119///
120/// False when the aggregate declares none. XTypes 1.3 §7.6.8 Step 1:
121///
122/// > If there are any key members, then remove the non-key members from
123/// > FooKeyHolder. Otherwise do not remove any members.
124///
125/// So a `@key` member whose type declares no key of its own contributes
126/// *all* of that type's members to the holder, not none. Reading the
127/// first half of the sentence alone gave `@key Middle middle;` an empty
128/// holder and every instance of such a type one instance handle.
129///
130/// A non-aggregate has no members to remove and answers false, which is
131/// why [`member_key`] asks about the kind before it asks this.
132#[must_use]
133pub fn holder_keeps_only_keys(ast: &Ast, type_: Id<TypeNode>) -> bool {
134 any_key_member(ast, &ast.all_members_of(type_))
135}
136
137/// Whether any of `members` carries `@key`.
138///
139/// `all_members_of`, not `members_of`, is what the caller must hand in:
140/// a `@key` member the aggregate inherited is part of its key, and a
141/// walk that misses it decides the aggregate is keyless and keeps
142/// everything. XTypes 1.3 §7.4.1.1.5.1.
143pub(crate) fn any_key_member(ast: &Ast, members: &[Id<zerodds_ast::MemberNode>]) -> bool {
144 members
145 .iter()
146 .any(|member| ast.member(*member).is_ok_and(|m| m.facets.is_key()))
147}
148
149/// The key the members of `type_` are planned under, given the key
150/// `type_` itself was planned under.
151///
152/// The same key, except under [`KeyHolderNesting::KeylessNestedWhole`]
153/// below a keyless aggregate, where the subtree stops being filtered at
154/// all and is therefore planned for data.
155///
156/// It is a function of the type and the key alone, with no accumulated
157/// depth, because the callers re-enter the plan at arbitrary points: the
158/// probe re-plans every nested aggregate it writes, so a rule that only
159/// the root's walk could apply would be lost on the way down.
160#[must_use]
161pub fn member_key(ast: &Ast, type_: Id<TypeNode>, key: LayoutKey) -> LayoutKey {
162 let aggregate = ast.type_(type_).is_ok_and(|node| {
163 matches!(
164 node.kind,
165 TypeKind::Structure { .. } | TypeKind::Valuetype { .. }
166 )
167 });
168 member_key_for(key, aggregate && !holder_keeps_only_keys(ast, type_))
169}
170
171/// [`member_key`] once the caller has established that the type is a
172/// keyless aggregate.
173///
174/// Split out so the planner, which has already walked the member list to
175/// decide that, does not walk it a second time — and so the rule itself
176/// exists once.
177pub(crate) const fn member_key_for(key: LayoutKey, keyless_aggregate: bool) -> LayoutKey {
178 if !keyless_aggregate
179 || !matches!(key.purpose, crate::key::Purpose::Key)
180 || !matches!(key.key_holder_nesting, KeyHolderNesting::KeylessNestedWhole)
181 {
182 return key;
183 }
184 LayoutKey {
185 purpose: crate::key::Purpose::Data,
186 ..key
187 }
188}
189
190/// The discriminator of a union that is keyed by it, or `None`.
191///
192/// DDS-XTypes 1.3 Table 21 puts `@key` on a union discriminator and
193/// §7.2.2.4.4.4.8's own example writes it, so a union can be a keyed
194/// topic type without owning a single `@key` **member** — and
195/// [`key_members`] walks members, so it answers the empty list for one.
196/// Every caller that asks "is this type keyed" has to ask both, which is
197/// why this is a function of its own and not folded into that walk.
198///
199/// §7.6.8's Step 1 over such a union removes everything but the
200/// discriminator: the branches are not keys, and which branch a sample
201/// carries is the discriminator's own answer. So the holder is the
202/// discriminator alone, which is what the planner builds and what
203/// `zerodds-wire-codec` writes.
204///
205/// A typedef in front of the union is transparent, as everywhere else.
206///
207/// Nothing answers `Some` unless a front end was asked to carry the
208/// annotation — `zerodds_idl_lower::KeyOnUnionDiscriminator::Carried` —
209/// so under the default this is `None` for every union in the corpus.
210///
211/// zerodds-lint: recursion-depth 64 — one frame per typedef hop, bounded
212/// by [`crate::plan::MAX_DEPTH`].
213#[must_use]
214pub fn union_key_discriminator(ast: &Ast, type_: Id<TypeNode>) -> Option<Id<TypeNode>> {
215 let mut current = type_;
216 for _ in 0..crate::plan::MAX_DEPTH {
217 let node = ast.type_(current).ok()?;
218 match &node.kind {
219 TypeKind::Alias { aliased } => current = aliased.resolved()?,
220 TypeKind::Union { discriminator, .. } => {
221 if !ast.discriminator_is_key(current) {
222 return None;
223 }
224 return discriminator.resolved();
225 }
226 _ => return None,
227 }
228 }
229 None
230}
231
232/// The members that make up a type's key, in the holder's order.
233///
234/// Member-id order by default, which is XTypes 1.3 §7.6.8's and what
235/// three of the four measured combinations write — Cyclone DDS in both
236/// encoding versions, RTI Connext under version 2 and Fast DDS in both.
237/// RTI under version 1 keeps declaration order, and two peers that
238/// disagree about it compute different hashes for the same sample, so
239/// the same instance arrives under two handles. [`KeyHolderOrder`]
240/// carries the measurement and the selection.
241///
242/// # A keyless topic type still reports no key members
243///
244/// Step 1's "otherwise do not remove any members" reaches nested
245/// aggregates, not the type the algorithm was invoked on: §7.6.8 defines
246/// the computation for "a keyed type", and every vendor decides a
247/// keyless topic has no KeyHash at all before any holder is built —
248/// measured in `tests/fixtures/vendor_bytes.md`. So the rule applies
249/// below the root and the root keeps answering with an empty list, which
250/// is what both callers test for before they emit anything.
251///
252/// # Errors
253/// [`LayoutError::Model`] when a handle in the type graph is stale.
254pub fn key_members(ast: &Ast, type_: Id<TypeNode>) -> Result<Vec<NodeId>, LayoutError> {
255 key_members_with(
256 ast,
257 type_,
258 KeyHolderNesting::default(),
259 KeyHolderOrder::default(),
260 )
261}
262
263/// [`key_members`] under a caller-supplied nesting and ordering rule.
264///
265/// Both are dialect settings because the implementations disagree about
266/// them; [`KeyHolderNesting`] and [`KeyHolderOrder`] carry the
267/// measurements.
268///
269/// # Errors
270/// [`LayoutError::Model`] when a handle in the type graph is stale.
271pub fn key_members_with(
272 ast: &Ast,
273 type_: Id<TypeNode>,
274 nesting: KeyHolderNesting,
275 order: KeyHolderOrder,
276) -> Result<Vec<NodeId>, LayoutError> {
277 let mut out = Vec::new();
278 collect_key_members(ast, type_, nesting, order, &mut out, 0)?;
279 Ok(out)
280}
281
282/// zerodds-lint: recursion-depth 64 (typedef chains and nested key
283/// holders; bounded by `plan::MAX_DEPTH`, which returns
284/// `LayoutError::TooDeep`)
285fn collect_key_members(
286 ast: &Ast,
287 type_: Id<TypeNode>,
288 nesting: KeyHolderNesting,
289 order: KeyHolderOrder,
290 out: &mut Vec<NodeId>,
291 depth: usize,
292) -> Result<(), LayoutError> {
293 let node = ast.type_(type_)?;
294 // Loud, like `plan_at`: a truncated key set is a KeyHash two peers
295 // compute differently, and returning `Ok` here would ship it.
296 if depth > crate::plan::MAX_DEPTH {
297 return Err(LayoutError::TooDeep { node: node.node_id });
298 }
299 // A typedef is transparent on the wire, so it is transparent to the
300 // key set too: the aliased type's keyed members are this member's.
301 if let TypeKind::Alias { aliased } = &node.kind {
302 let target = aliased
303 .resolved()
304 .ok_or(LayoutError::Unresolved { node: node.node_id })?;
305 return collect_key_members(ast, target, nesting, order, out, depth + 1);
306 }
307 if !matches!(
308 node.kind,
309 TypeKind::Structure { .. } | TypeKind::Valuetype { .. }
310 ) {
311 return Ok(());
312 }
313 // `all_members_of`, not the type's own list: a `@key` member the
314 // aggregate inherited is part of its key, and leaving it out gives
315 // two distinct instances one instance handle. The same walk
316 // `plan_at` does for the holder plan, which this list has to agree
317 // with. XTypes 1.3 §7.4.1.1.5.1, DDSI-RTPS 2.5 §9.6.3.8.
318 let members = ast.all_members_of(type_);
319
320 // Step 1 removes the non-key members only "if there are any key
321 // members"; a nested aggregate that declares none contributes all of
322 // them. The root is exempt — see this function's own documentation —
323 // because a keyless topic type has no KeyHash to build a holder for.
324 let only_keys = depth == 0 || any_key_member(ast, &members);
325
326 // Collect first, then order by effective member id.
327 let mut keyed: Vec<(u32, NodeId, Option<Id<TypeNode>>)> = Vec::new();
328 for member in &members {
329 let m = ast.member(*member)?;
330 if only_keys && !m.facets.is_key() {
331 continue;
332 }
333 // The same rule `plan_aggregate` applies: a member without a
334 // written id takes its declaration index, so both sides order
335 // the key the same way. Collapsing them all onto 0 would let
336 // the declaration order decide, which is not what §9.6.3.8 says.
337 let id = m
338 .facets
339 .effective_member_id()
340 .unwrap_or_else(|| position_of(&members, *member));
341 keyed.push((id, m.node_id, m.type_.resolved()));
342 }
343 // A stable sort, so that under `Declaration` — where every entry
344 // keeps the position it was collected in — the order is exactly the
345 // one the members were declared in.
346 if order == KeyHolderOrder::AscendingMemberId {
347 keyed.sort_by_key(|(id, _, _)| *id);
348 }
349
350 // Below a keyless aggregate RTI's XCDR1 holder stops filtering
351 // altogether and takes the subtree as the data has it, so nothing
352 // under it is a key member. Everyone else keeps applying Step 1.
353 let recurse = only_keys || nesting == KeyHolderNesting::Recursive;
354
355 for (_, node_id, member_type) in keyed {
356 out.push(node_id);
357 // A member of the holder that is itself an aggregate contributes
358 // its own holder's members. XTypes 1.3 §7.6.8 Step 3.
359 if let (true, Some(inner)) = (recurse, member_type) {
360 collect_key_members(ast, inner, nesting, order, out, depth + 1)?;
361 }
362 }
363 Ok(())
364}
365
366/// Where `member` sits in `members`, as its implicit member id.
367///
368/// `u32::MAX` for a member that is not in the list, which cannot happen
369/// for a list this function was handed — and if it ever does, the id
370/// sorts last rather than colliding with a real one at 0.
371fn position_of(
372 members: &[Id<zerodds_ast::MemberNode>],
373 member: Id<zerodds_ast::MemberNode>,
374) -> u32 {
375 members
376 .iter()
377 .position(|candidate| *candidate == member)
378 .and_then(|index| u32::try_from(index).ok())
379 .unwrap_or(u32::MAX)
380}