Skip to main content

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}