Skip to main content

zerodds_rtps/
reliable_reader.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Reliable RTPS reader (1:N writer proxies) — DDSI-RTPS 2.5 §8.4.10.
4//!
5//! Corresponds to the [`StatefulReader`] role with 1..N matched writers.
6//! Fragmentation (§8.4.14) is supported. Multi-writer since WP 1.4
7//! T4.5: a separate [`WriterProxyState`] per remote writer with its own
8//! `received_cache`, `delivered_up_to` and `FragmentAssembler`.
9//!
10//! # Why per-proxy state?
11//!
12//! SequenceNumbers are writer-local (Spec §8.3.5.4). Two writers with
13//! overlapping SN spaces would collide in a global cache
14//! — hence separate buffers per proxy.
15//!
16//! # API-Form
17//!
18//! ```text
19//!   let mut r = ReliableReader::new(...);
20//!   r.add_writer_proxy(proxy_for_remote_A);
21//!   loop {
22//!       match transport.recv_submessage() {
23//!           Data(d)      => for s in r.handle_data(&d) { deliver(s) },
24//!           DataFrag(df) => for s in r.handle_data_frag(&df, uptime()) { deliver(s) },
25//!           Heartbeat(h) => r.handle_heartbeat(&h, uptime()),
26//!           Gap(g)       => for s in r.handle_gap(&g) { deliver(s) },
27//!       }
28//!       for dg in r.tick(uptime())? { transport.send(dg) }
29//!   }
30//! ```
31//!
32//! [`StatefulReader`]: https://www.omg.org/spec/DDSI-RTPS/2.5/
33
34use core::time::Duration;
35
36extern crate alloc;
37use alloc::vec::Vec;
38
39use alloc::rc::Rc;
40
41use crate::error::WireError;
42use crate::fragment_assembler::{AssemblerCaps, FragmentAssembler};
43use crate::header::RtpsHeader;
44use crate::history_cache::{CacheChange, ChangeKind, HistoryCache};
45use crate::message_builder::OutboundDatagram;
46use crate::submessage_header::{FLAG_E_LITTLE_ENDIAN, SubmessageHeader, SubmessageId};
47use crate::submessages::{
48    AckNackSubmessage, DataFragSubmessage, DataSubmessage, GapSubmessage, HeartbeatSubmessage,
49    NackFragSubmessage, SequenceNumberSet,
50};
51use crate::wire_types::{Guid, GuidPrefix, SequenceNumber, VendorId};
52use crate::writer_proxy::WriterProxy;
53
54/// Default heartbeat response delay.
55///
56/// RTPS 2.5 §8.4.15.7 allows the reader a configurable delay
57/// between HEARTBEAT receipt and ACKNACK emit, to
58/// batch multiple HBs. The spec specifies no fixed default — the previously
59/// used 200 ms are a pre-1.0 implementation detail.
60///
61/// **0 ms** = synchronous ACK response. The Cyclone DDS default is also
62/// 0 (`HeartbeatResponseDelay` XML default). Makes ACKNACK event-driven
63/// instead of deferred-batched. No loss of correctness for reliable
64/// loopback / low-loss networks; for lossy networks the value can be
65/// raised via `ReliableReaderConfig::heartbeat_response_delay`.
66///
67/// Pre-D.5e: 200 ms — that was an implicit latency floor of 200 ms
68/// per roundtrip ACK cycle.
69pub const DEFAULT_HEARTBEAT_RESPONSE_DELAY: Duration = Duration::from_millis(0);
70
71/// Per-writer state: the proxy + separate receive state.
72///
73/// Every remote writer has its own SN space (§8.3.5.4), so also
74/// its own `received_cache`, `delivered_up_to` and
75/// `FragmentAssembler`. This way two writers with colliding SNs
76/// (e.g. both starting at 1) can be received in parallel without issue.
77#[derive(Debug, Clone)]
78pub struct WriterProxyState {
79    /// Writer-proxy protocol state.
80    pub proxy: WriterProxy,
81    /// Receive cache for this writer.
82    pub received_cache: HistoryCache,
83    /// Highest SN delivered to the app.
84    pub delivered_up_to: SequenceNumber,
85    /// Fragment reassembly for this writer.
86    pub assembler: FragmentAssembler,
87    /// Time since which an ACKNACK/NACK_FRAG to this writer is
88    /// pending. `None` = nothing pending.
89    pub pending_acknack_since: Option<Duration>,
90}
91
92impl WriterProxyState {
93    fn new(proxy: WriterProxy, max_samples: usize, caps: AssemblerCaps) -> Self {
94        Self {
95            proxy,
96            received_cache: HistoryCache::new(max_samples),
97            delivered_up_to: SequenceNumber(0),
98            assembler: FragmentAssembler::new(caps),
99            pending_acknack_since: None,
100        }
101    }
102}
103
104/// A reliable reader with 0..N writer proxies.
105#[derive(Debug, Clone)]
106pub struct ReliableReader {
107    guid: Guid,
108    vendor_id: VendorId,
109    writer_proxies: Vec<WriterProxyState>,
110    heartbeat_response_delay: Duration,
111    acknack_count: i32,
112    nackfrag_count: i32,
113    duplicate_frag_count: u64,
114    /// Template for new proxies.
115    max_samples_per_proxy: usize,
116    assembler_caps: AssemblerCaps,
117    /// Counter for submessages whose `writer_id` has no proxy.
118    unknown_src_count: u64,
119    /// `true` ⇒ this reader's RELIABILITY QoS is BEST_EFFORT. A best-effort
120    /// reader (RTPS §8.4.12.1) never blocks delivery on a missing earlier
121    /// sequence number — it has no in-order-without-loss guarantee and (unlike a
122    /// reliable reader) does not NACK to repair the gap. Defaults to `false`
123    /// (reliable, in-order); the DCPS layer flips it via [`Self::set_best_effort`]
124    /// from the reader QoS. Without this, a leading gap (e.g. SN 1 lost in
125    /// transit, or a late-join against a writer whose history still starts at 1)
126    /// deadlocks the best-effort reader forever.
127    best_effort: bool,
128}
129
130/// Configuration at creation.
131#[derive(Debug, Clone)]
132pub struct ReliableReaderConfig {
133    /// GUID of the reader endpoint.
134    pub guid: Guid,
135    /// VendorId for the RTPS header of the ACKNACKs.
136    pub vendor_id: VendorId,
137    /// Initial writer proxies. More via `add_writer_proxy`.
138    pub writer_proxies: Vec<WriterProxy>,
139    /// Capacity of the receive cache per proxy (not global).
140    pub max_samples_per_proxy: usize,
141    /// Heartbeat response delay (default: 200 ms).
142    pub heartbeat_response_delay: Duration,
143    /// Caps for the fragment assembler (per proxy).
144    pub assembler_caps: AssemblerCaps,
145}
146
147/// A sample delivered to the application.
148#[derive(Debug, Clone, PartialEq, Eq)]
149pub struct DeliveredSample {
150    /// GUID of the writer the sample comes from. Makes multi-writer
151    /// deduplication possible in the caller.
152    pub writer_guid: Guid,
153    /// Sequence number in the writer.
154    pub sequence_number: SequenceNumber,
155    /// Serialized payload (zero-copy via `Arc::clone` from the cache).
156    /// Payload.
157    pub payload: alloc::sync::Arc<[u8]>,
158    /// Spec §8.2.1.2 ChangeKind — `Alive` for normal samples,
159    /// `NotAliveDisposed` / `NotAliveUnregistered` /
160    /// `NotAliveDisposedUnregistered` for lifecycle markers that the
161    /// writer sent via `dispose`/`unregister_instance`.
162    /// Spec §9.6.3.9 PID_STATUS_INFO in the inline QoS.
163    pub kind: ChangeKind,
164    /// `PID_KEY_HASH` from the inline QoS (Spec §9.6.4.8). For
165    /// lifecycle markers this is the identity of the disposed/
166    /// unregistered instance; for keyed-topic ALIVE samples optional
167    /// (some vendors send an inline hash, some don't). `None`
168    /// if the writer does not supply a hash inline (typical for
169    /// keyless topics).
170    pub key_hash: Option<[u8; 16]>,
171    /// Source timestamp from the preceding INFO_TS submessage (DDSI-RTPS
172    /// §8.7.3), if the writer sent one. Feeds `SampleInfo.source_timestamp` and
173    /// `DESTINATION_ORDER = BY_SOURCE_TIMESTAMP`. `None` ⇒ the reader uses
174    /// reception order.
175    pub source_timestamp: Option<crate::header_extension::HeTimestamp>,
176}
177
178impl ReliableReader {
179    /// Creates a fresh reader.
180    ///
181    /// # Panics
182    /// If `cfg.assembler_caps.max_pending_sns == 0`.
183    #[must_use]
184    pub fn new(cfg: ReliableReaderConfig) -> Self {
185        assert!(
186            cfg.assembler_caps.max_pending_sns > 0,
187            "assembler_caps.max_pending_sns must be > 0; use a Best-Effort reader \
188             or increase the cap to actually accept fragmented samples"
189        );
190        let proxies = cfg
191            .writer_proxies
192            .into_iter()
193            .map(|p| WriterProxyState::new(p, cfg.max_samples_per_proxy, cfg.assembler_caps))
194            .collect();
195        Self {
196            guid: cfg.guid,
197            vendor_id: cfg.vendor_id,
198            writer_proxies: proxies,
199            heartbeat_response_delay: cfg.heartbeat_response_delay,
200            acknack_count: 0,
201            nackfrag_count: 0,
202            duplicate_frag_count: 0,
203            max_samples_per_proxy: cfg.max_samples_per_proxy,
204            assembler_caps: cfg.assembler_caps,
205            unknown_src_count: 0,
206            best_effort: false,
207        }
208    }
209
210    /// Marks this reader as BEST_EFFORT (`true`) or RELIABLE (`false`, the
211    /// default). Best-effort readers skip leading gaps instead of waiting (see
212    /// the `best_effort` field).
213    pub fn set_best_effort(&mut self, best_effort: bool) {
214        self.best_effort = best_effort;
215    }
216
217    /// GUID.
218    #[must_use]
219    pub fn guid(&self) -> Guid {
220        self.guid
221    }
222
223    /// Read-only slice of the writer-proxy states.
224    #[must_use]
225    pub fn writer_proxies(&self) -> &[WriterProxyState] {
226        &self.writer_proxies
227    }
228
229    /// Number of registered writer proxies.
230    #[must_use]
231    pub fn writer_proxy_count(&self) -> usize {
232        self.writer_proxies.len()
233    }
234
235    /// Counter of sent ACKNACKs.
236    #[must_use]
237    pub fn acknack_count(&self) -> i32 {
238        self.acknack_count
239    }
240
241    /// Counter of sent NACK_FRAGs.
242    #[must_use]
243    pub fn nackfrag_count(&self) -> i32 {
244        self.nackfrag_count
245    }
246
247    /// Sum of the active (incomplete) fragment buffers across all
248    /// proxies.
249    #[must_use]
250    pub fn pending_fragment_count(&self) -> usize {
251        self.writer_proxies.iter().map(|s| s.assembler.len()).sum()
252    }
253
254    /// Sum of the dropped fragments across all proxies
255    /// (DoS / inconsistency diagnosis).
256    #[must_use]
257    pub fn dropped_fragment_count(&self) -> u64 {
258        self.writer_proxies
259            .iter()
260            .map(|s| s.assembler.drop_count())
261            .sum()
262    }
263
264    /// Number of DATA_FRAGs that arrived for already-known SNs
265    /// (duplicate fragments, re-sends).
266    #[must_use]
267    pub fn duplicate_fragment_count(&self) -> u64 {
268        self.duplicate_frag_count
269    }
270
271    /// Number of submessages whose `writer_id` could not be assigned to a registered
272    /// proxy (misrouting / spoofing diagnosis).
273    #[must_use]
274    pub fn unknown_src_count(&self) -> u64 {
275        self.unknown_src_count
276    }
277
278    /// Adds a writer proxy. Idempotent: for a known GUID the
279    /// reliability state (SN bounds, received cache, delivered pointer)
280    /// is **preserved** — only the locators are refreshed.
281    ///
282    /// Sets a preemptive ACKNACK as pending, so the writer gets a
283    /// "hello, I'm here" ACKNACK on the next tick. Cyclone DDS
284    /// responds with a HEARTBEAT and starts DATA resends —
285    /// without this impulse the writer waits passively.
286    ///
287    /// Important: a renewed SPDP/SEDP announce of the same writer (Cyclone
288    /// re-announces periodically) must NOT discard the reader progress.
289    /// A reset would, after an already-processed HEARTBEAT, produce an empty
290    /// ACKNACK ("nothing missing") → the reliable writer never delivers the
291    /// DATA (cross-vendor secure-SEDP deadlock).
292    pub fn add_writer_proxy(&mut self, proxy: WriterProxy) {
293        let guid = proxy.remote_writer_guid;
294        if let Some(idx) = self
295            .writer_proxies
296            .iter()
297            .position(|s| s.proxy.remote_writer_guid == guid)
298        {
299            // Known: preserve the state, only refresh locators + re-arm the
300            // ACKNACK (if none is pending yet).
301            self.writer_proxies[idx]
302                .proxy
303                .refresh_locators(proxy.unicast_locators, proxy.multicast_locators);
304            self.writer_proxies[idx]
305                .pending_acknack_since
306                .get_or_insert(Duration::ZERO);
307        } else {
308            let mut state =
309                WriterProxyState::new(proxy, self.max_samples_per_proxy, self.assembler_caps);
310            // Duration::ZERO triggers an ACKNACK emit immediately on the next tick()
311            // (now - ZERO >= heartbeat_response_delay).
312            state.pending_acknack_since = Some(Duration::ZERO);
313            self.writer_proxies.push(state);
314        }
315    }
316
317    /// Removes a writer proxy.
318    pub fn remove_writer_proxy(&mut self, guid: Guid) -> Option<WriterProxy> {
319        let idx = self
320            .writer_proxies
321            .iter()
322            .position(|s| s.proxy.remote_writer_guid == guid)?;
323        Some(self.writer_proxies.remove(idx).proxy)
324    }
325
326    /// Zeroes all diagnostic counters. Touches no state machine.
327    pub fn reset_diagnostics(&mut self) {
328        self.acknack_count = 0;
329        self.nackfrag_count = 0;
330        self.duplicate_frag_count = 0;
331        self.unknown_src_count = 0;
332        for s in &mut self.writer_proxies {
333            s.assembler.reset_diagnostics();
334        }
335    }
336
337    // ---------- Incoming Submessages ----------
338
339    /// Process a DATA. Dispatch by `writer_id` to the matching
340    /// proxy. Returns the reassembled samples of this proxy.
341    ///
342    /// Spec §9.6.3.9 PID_STATUS_INFO: with `key_flag=true` + inline QoS
343    /// with STATUS_INFO set, the CacheChange is marked
344    /// NotAliveDisposed / NotAliveUnregistered / NotAliveDisposedUnregistered
345    /// instead of Alive.
346    pub fn handle_data(
347        &mut self,
348        source_prefix: GuidPrefix,
349        data: &DataSubmessage,
350        source_timestamp: Option<crate::header_extension::HeTimestamp>,
351    ) -> Vec<DeliveredSample> {
352        let Some(idx) = self.proxy_index_by_writer(Guid::new(source_prefix, data.writer_id)) else {
353            self.unknown_src_count = self.unknown_src_count.saturating_add(1);
354            return Vec::new();
355        };
356        let state = &mut self.writer_proxies[idx];
357        let sn = data.writer_sn;
358        if state.proxy.is_known(sn) || sn <= state.delivered_up_to {
359            return Vec::new();
360        }
361        state.proxy.received_change_set(sn);
362        let kind = Self::classify_change_kind(data);
363        // Key-only ALIVE sample (K-flag set, D-flag clear): the payload holds
364        // only the @key fields — an instance registration (e.g. OpenDDS
365        // `register_instance`), NOT a full sample. Full-decoding its key-only
366        // payload fails with a spurious cross-vendor decode error (OpenDDS sends
367        // one key-only DATA per data DATA). Mark the SN received so reliability
368        // advances past it (collect_in_order_for skips a known-but-uncached SN),
369        // but do not deliver it: the actual data arrives in the D-flag samples.
370        // Dispose/unregister key-only markers (kind != Alive) DO carry lifecycle
371        // semantics and fall through to be delivered below.
372        if data.key_flag && kind == ChangeKind::Alive {
373            // Treat like a GAP for this one SN: mark it irrelevant and, if it is
374            // the next in-order SN, advance the delivery pointer so the reliable
375            // reader does not stall waiting to "fill" a registration that will
376            // never be delivered. The subsequent D-flag data SN then flows.
377            state.proxy.irrelevant_change_set(sn);
378            if sn.0 == state.delivered_up_to.0 + 1 {
379                state.delivered_up_to = sn;
380            }
381            return Self::collect_in_order_for(state, self.best_effort);
382        }
383        let key_hash = data
384            .inline_qos
385            .as_ref()
386            .and_then(crate::inline_qos::find_key_hash);
387        // Arc::clone instead of Vec::clone on the payload — the
388        // refcount block is shared between DataSubmessage, cache and
389        // DeliveredSample.
390        let _ = state.received_cache.insert(CacheChange {
391            sequence_number: sn,
392            payload: alloc::sync::Arc::clone(&data.serialized_payload),
393            kind,
394            key_hash,
395            // From the preceding INFO_TS submessage (DDSI-RTPS §8.7.3).
396            source_timestamp,
397        });
398        Self::collect_in_order_for(state, self.best_effort)
399    }
400
401    /// Classifies an incoming DATA as Alive vs lifecycle marker.
402    /// `key_flag=true` indicates a key-only payload; STATUS_INFO in the
403    /// inline QoS says whether disposed/unregistered/both.
404    fn classify_change_kind(data: &DataSubmessage) -> ChangeKind {
405        if !data.key_flag {
406            return ChangeKind::Alive;
407        }
408        let Some(pl) = data.inline_qos.as_ref() else {
409            return ChangeKind::Alive;
410        };
411        let Some(bits) = crate::inline_qos::find_status_info(pl) else {
412            return ChangeKind::Alive;
413        };
414        let disposed = bits & crate::inline_qos::status_info::DISPOSED != 0;
415        let unregistered = bits & crate::inline_qos::status_info::UNREGISTERED != 0;
416        match (disposed, unregistered) {
417            (true, true) => ChangeKind::NotAliveDisposedUnregistered,
418            (true, false) => ChangeKind::NotAliveDisposed,
419            (false, true) => ChangeKind::NotAliveUnregistered,
420            (false, false) => ChangeKind::Alive,
421        }
422    }
423
424    /// Process a DATA_FRAG. `now` triggers NACK_FRAG scheduling
425    /// directly, without waiting for a HEARTBEAT.
426    pub fn handle_data_frag(
427        &mut self,
428        source_prefix: GuidPrefix,
429        df: &DataFragSubmessage,
430        now: Duration,
431        source_timestamp: Option<crate::header_extension::HeTimestamp>,
432    ) -> Vec<DeliveredSample> {
433        let Some(idx) = self.proxy_index_by_writer(Guid::new(source_prefix, df.writer_id)) else {
434            self.unknown_src_count = self.unknown_src_count.saturating_add(1);
435            return Vec::new();
436        };
437        let state = &mut self.writer_proxies[idx];
438        let sn = df.writer_sn;
439        if state.proxy.is_known(sn) || sn <= state.delivered_up_to {
440            self.duplicate_frag_count = self.duplicate_frag_count.saturating_add(1);
441            return Vec::new();
442        }
443        let result = if let Some(completed) = state.assembler.insert(df) {
444            state.proxy.received_change_set(sn);
445            let _ = state.received_cache.insert(
446                CacheChange::alive(sn, completed.payload).with_source_timestamp(source_timestamp),
447            );
448            Self::collect_in_order_for(state, self.best_effort)
449        } else {
450            Vec::new()
451        };
452        if state.assembler.has_gaps() {
453            state.pending_acknack_since.get_or_insert(now);
454        }
455        result
456    }
457
458    /// Process a HEARTBEAT. Dispatch by `writer_id`.
459    pub fn handle_heartbeat(
460        &mut self,
461        source_prefix: GuidPrefix,
462        hb: &HeartbeatSubmessage,
463        now: Duration,
464    ) -> Vec<DeliveredSample> {
465        let Some(idx) = self.proxy_index_by_writer(Guid::new(source_prefix, hb.writer_id)) else {
466            self.unknown_src_count = self.unknown_src_count.saturating_add(1);
467            return Vec::new();
468        };
469        let state = &mut self.writer_proxies[idx];
470        if hb.liveliness_flag {
471            return Vec::new();
472        }
473        state.proxy.update_from_heartbeat(hb.first_sn, hb.last_sn);
474        let has_missing = state.proxy.has_missing_changes();
475        let has_frag_gaps = state.assembler.has_gaps();
476        if !hb.final_flag || has_missing || has_frag_gaps {
477            state.pending_acknack_since.get_or_insert(now);
478        }
479        // An HB with first_sn > delivered_up_to+1 means that samples
480        // before first_sn are "lost". `collect_in_order_for` then advances
481        // `delivered_up_to` to first_sn-1 and delivers samples from
482        // the received_cache that were waiting on the hole fill (e.g. a
483        // volatile direct send with SN > delivered_up_to+1).
484        Self::collect_in_order_for(state, self.best_effort)
485    }
486
487    /// Process a GAP. Dispatch by `writer_id`.
488    pub fn handle_gap(
489        &mut self,
490        source_prefix: GuidPrefix,
491        gap: &GapSubmessage,
492    ) -> Vec<DeliveredSample> {
493        let Some(idx) = self.proxy_index_by_writer(Guid::new(source_prefix, gap.writer_id)) else {
494            self.unknown_src_count = self.unknown_src_count.saturating_add(1);
495            return Vec::new();
496        };
497        let state = &mut self.writer_proxies[idx];
498        let mut sn = gap.gap_start;
499        while sn < gap.gap_list.bitmap_base {
500            state.proxy.irrelevant_change_set(sn);
501            state.assembler.discard(sn);
502            sn = SequenceNumber(sn.0 + 1);
503        }
504        for sn in gap.gap_list.iter_set() {
505            state.proxy.irrelevant_change_set(sn);
506            state.assembler.discard(sn);
507        }
508        Self::collect_in_order_for(state, self.best_effort)
509    }
510
511    /// Tick: returns due ACKNACK/NACK_FRAG datagrams **across all
512    /// proxies**. Per proxy its own ACKNACK/NACK_FRAG, because
513    /// SN spaces are per writer.
514    ///
515    /// # Errors
516    /// Wire encode error.
517    pub fn tick(&mut self, now: Duration) -> Result<Vec<Vec<u8>>, WireError> {
518        Ok(self
519            .tick_outbound(now)?
520            .into_iter()
521            .map(|d| d.bytes)
522            .collect())
523    }
524
525    /// Like [`Self::tick`], but with target locators for each datagram.
526    /// Preferred for transport integration, because each AckNack must go to
527    /// the concrete writer-proxy unicast locator.
528    ///
529    /// # Errors
530    /// `WireError::ValueOutOfRange` for an overlong submessage body.
531    pub fn tick_outbound(&mut self, now: Duration) -> Result<Vec<OutboundDatagram>, WireError> {
532        let mut out = Vec::new();
533        for idx in 0..self.writer_proxies.len() {
534            let Some(since) = self.writer_proxies[idx].pending_acknack_since else {
535                continue;
536            };
537            if now.saturating_sub(since) < self.heartbeat_response_delay {
538                continue;
539            }
540            self.writer_proxies[idx].pending_acknack_since = None;
541            let targets = Rc::new(self.writer_proxies[idx].proxy.unicast_locators.clone());
542
543            let incomplete_sns: Vec<SequenceNumber> = self.writer_proxies[idx]
544                .assembler
545                .incomplete_sns()
546                .collect();
547            for sn in incomplete_sns {
548                let bytes = self.build_nackfrag_datagram(idx, sn)?;
549                out.push(OutboundDatagram {
550                    bytes,
551                    targets: Rc::clone(&targets),
552                });
553            }
554            let bytes = self.build_acknack_datagram(idx)?;
555            out.push(OutboundDatagram { bytes, targets });
556        }
557        Ok(out)
558    }
559
560    // ---------- Internal ----------
561
562    /// Finds the writer proxy by the **full writer GUID** (source
563    /// `guid_prefix` from the RTPS header + `writerId` of the submessage).
564    /// DDSI-RTPS 2.5 §8.3.4: the effective source of a writer submessage is
565    /// `Receiver.sourceGuidPrefix` + `writerId`; the EntityId alone
566    /// does NOT uniquely identify a remote writer (multiple participants
567    /// share the same per-participant base-assigned EntityId), otherwise
568    /// DATA/HB/GAP of two writers end up in the same proxy state.
569    fn proxy_index_by_writer(&self, guid: Guid) -> Option<usize> {
570        self.writer_proxies
571            .iter()
572            .position(|s| s.proxy.remote_writer_guid == guid)
573    }
574
575    fn collect_in_order_for(
576        state: &mut WriterProxyState,
577        best_effort: bool,
578    ) -> Vec<DeliveredSample> {
579        // Typically 1 sample per recv in steady-state, occasionally a burst.
580        // Pre-alloc with cap=2 eliminates the Vec::grow reallocs without
581        // over-allocating on the single-sample path.
582        let mut out = Vec::with_capacity(2);
583        loop {
584            let next = SequenceNumber(state.delivered_up_to.0 + 1);
585            if let Some(change) = state.received_cache.get(next) {
586                out.push(DeliveredSample {
587                    writer_guid: state.proxy.remote_writer_guid,
588                    sequence_number: change.sequence_number,
589                    payload: change.payload.clone(),
590                    kind: change.kind,
591                    key_hash: change.key_hash,
592                    source_timestamp: change.source_timestamp,
593                });
594                state.delivered_up_to = next;
595                state.received_cache.remove_up_to(next);
596            } else if state.proxy.is_known(next) && state.proxy.last_available_sn() >= next {
597                state.delivered_up_to = next;
598            } else if next < state.proxy.first_available_sn() {
599                // Writer announced first_sn > next via HEARTBEAT
600                // → samples before first_available are "lost" (volatile
601                // skip, historic eviction). Advance the delivery pointer
602                // so that subsequent SNs in the received_cache can finally
603                // be delivered. Spec §8.4.12.4.
604                state.delivered_up_to = next;
605            } else if best_effort {
606                // BEST_EFFORT reader (RTPS §8.4.12.1): never block on a missing
607                // `next`. A best-effort reader makes no in-order-without-loss
608                // guarantee and does not NACK to repair the gap, so waiting for
609                // `next` (a sample lost in transit, or one the writer still lists
610                // in its HEARTBEAT but we never received) would deadlock delivery
611                // forever. Skip ahead to the lowest cached SN and deliver from
612                // there. Cross-vendor: an OpenDDS/RTI writer whose first sample to
613                // us is mid-stream lands here.
614                match state.received_cache.min_sn() {
615                    Some(low) if low.0 > next.0 => {
616                        state.delivered_up_to = SequenceNumber(low.0 - 1);
617                    }
618                    _ => break,
619                }
620            } else {
621                break;
622            }
623        }
624        out
625    }
626
627    fn build_nackfrag_datagram(
628        &mut self,
629        proxy_idx: usize,
630        sn: SequenceNumber,
631    ) -> Result<Vec<u8>, WireError> {
632        let missing = self.writer_proxies[proxy_idx]
633            .assembler
634            .missing_fragments(sn);
635        self.nackfrag_count = self.nackfrag_count.wrapping_add(1);
636        let writer_guid = self.writer_proxies[proxy_idx].proxy.remote_writer_guid;
637        let nf = NackFragSubmessage {
638            reader_id: self.guid.entity_id,
639            writer_id: writer_guid.entity_id,
640            writer_sn: sn,
641            fragment_number_state: missing,
642            count: self.nackfrag_count,
643        };
644        let (body, mut flags) = nf.write_body(true);
645        flags |= FLAG_E_LITTLE_ENDIAN;
646        self.wrap_to_writer(writer_guid.prefix, SubmessageId::NackFrag, flags, &body)
647    }
648
649    fn build_acknack_datagram(&mut self, proxy_idx: usize) -> Result<Vec<u8>, WireError> {
650        let state = &self.writer_proxies[proxy_idx];
651        let base = state.proxy.acknack_base();
652        let missing = state.proxy.missing_changes(256);
653        let snset = SequenceNumberSet::from_missing(base, &missing);
654        self.acknack_count = self.acknack_count.wrapping_add(1);
655        // final_flag=true only if we really have everything up to base-1
656        // and no further writer action is needed. For the preemptive
657        // AckNack (base=1, empty bitmap, proxy has seen nothing yet)
658        // final must be false, otherwise the writer reads it as "reader is
659        // up-to-date" and sends no durability resends (Cyclone DDS
660        // then shows only HEARTBEATs, no DATA).
661        let final_flag = missing.is_empty() && state.proxy.last_available_sn().0 >= 1;
662        let writer_guid = state.proxy.remote_writer_guid;
663        let ack = AckNackSubmessage {
664            reader_id: self.guid.entity_id,
665            writer_id: writer_guid.entity_id,
666            reader_sn_state: snset,
667            count: self.acknack_count,
668            final_flag,
669        };
670        let (body, mut flags) = ack.write_body(true);
671        flags |= FLAG_E_LITTLE_ENDIAN;
672        self.wrap_to_writer(writer_guid.prefix, SubmessageId::AckNack, flags, &body)
673    }
674
675    /// Packs `Header + INFO_DST(writer_prefix) + Submessage` into a
676    /// datagram. INFO_DST is mandatory: without it the effective
677    /// destination prefix = UNKNOWN, and receivers (e.g. Cyclone DDS)
678    /// discard the submessage as "not a connection" (RTPS 2.5 §8.3.7.6).
679    fn wrap_to_writer(
680        &self,
681        writer_prefix: crate::wire_types::GuidPrefix,
682        id: SubmessageId,
683        flags: u8,
684        body: &[u8],
685    ) -> Result<Vec<u8>, WireError> {
686        let header = RtpsHeader::new(self.vendor_id, self.guid.prefix);
687        let mut out = Vec::new();
688        out.extend_from_slice(&header.to_bytes());
689
690        // INFO_DST: target writer's GuidPrefix (12 byte body).
691        let info_dst_header = SubmessageHeader {
692            submessage_id: SubmessageId::InfoDst,
693            flags: FLAG_E_LITTLE_ENDIAN,
694            octets_to_next_header: 12,
695        };
696        out.extend_from_slice(&info_dst_header.to_bytes());
697        out.extend_from_slice(&writer_prefix.to_bytes());
698
699        // Eigentliche Submessage (ACKNACK / NACK_FRAG).
700        let body_len = u16::try_from(body.len()).map_err(|_| WireError::ValueOutOfRange {
701            message: "submessage body exceeds u16::MAX",
702        })?;
703        let sh = SubmessageHeader {
704            submessage_id: id,
705            flags,
706            octets_to_next_header: body_len,
707        };
708        out.extend_from_slice(&sh.to_bytes());
709        out.extend_from_slice(body);
710        Ok(out)
711    }
712}
713
714#[cfg(test)]
715#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
716mod tests {
717    use super::*;
718    use crate::datagram::{ParsedSubmessage, decode_datagram};
719    use crate::wire_types::{EntityId, GuidPrefix, Locator};
720
721    fn single_writer_guid() -> Guid {
722        Guid::new(
723            GuidPrefix::from_bytes([1; 12]),
724            EntityId::user_writer_with_key([0x10, 0x20, 0x30]),
725        )
726    }
727
728    fn make_reader(max_samples: usize) -> ReliableReader {
729        let reader_guid = Guid::new(
730            GuidPrefix::from_bytes([2; 12]),
731            EntityId::user_reader_with_key([0xA0, 0xB0, 0xC0]),
732        );
733        let writer_proxy = WriterProxy::new(
734            single_writer_guid(),
735            alloc::vec![Locator::udp_v4([127, 0, 0, 1], 7420)],
736            alloc::vec![],
737            true,
738        );
739        ReliableReader::new(ReliableReaderConfig {
740            guid: reader_guid,
741            vendor_id: VendorId::ZERODDS,
742            writer_proxies: alloc::vec![writer_proxy],
743            max_samples_per_proxy: max_samples,
744            heartbeat_response_delay: Duration::from_millis(200),
745            assembler_caps: AssemblerCaps::default(),
746        })
747    }
748
749    fn sn(n: i64) -> SequenceNumber {
750        SequenceNumber(n)
751    }
752
753    /// source-`guid_prefix` des Default-Writer-Proxys (single_writer_guid).
754    fn p1() -> GuidPrefix {
755        single_writer_guid().prefix
756    }
757
758    /// source-`guid_prefix` des zweiten Writer-Proxys (second_writer_guid).
759    fn p2() -> GuidPrefix {
760        second_writer_guid().prefix
761    }
762
763    fn data(wid: EntityId, rid: EntityId, n: i64, byte: u8) -> DataSubmessage {
764        DataSubmessage {
765            extra_flags: 0,
766            reader_id: rid,
767            writer_id: wid,
768            writer_sn: sn(n),
769            inline_qos: None,
770            key_flag: false,
771            non_standard_flag: false,
772            serialized_payload: alloc::sync::Arc::from(alloc::vec![byte]),
773        }
774    }
775
776    fn heartbeat(
777        wid: EntityId,
778        rid: EntityId,
779        first: i64,
780        last: i64,
781        count: i32,
782        final_flag: bool,
783    ) -> HeartbeatSubmessage {
784        HeartbeatSubmessage {
785            reader_id: rid,
786            writer_id: wid,
787            first_sn: sn(first),
788            last_sn: sn(last),
789            count,
790            final_flag,
791            liveliness_flag: false,
792            group_info: None,
793        }
794    }
795
796    fn first_state(r: &ReliableReader) -> &WriterProxyState {
797        &r.writer_proxies()[0]
798    }
799
800    #[test]
801    fn re_adding_known_writer_preserves_reliability_state() {
802        // RTPS: a renewed SPDP/SEDP announce of the same writer must NOT
803        // discard the reader reliability state. Otherwise, after a
804        // HEARTBEAT(first=1,last=1), the reader falsely reports "nothing missing"
805        // (empty ACKNACK) and the reliable writer never delivers the DATA —
806        // exactly the cross-vendor secure-SEDP deadlock against Cyclone DDS.
807        let mut r = make_reader(10);
808        let w_eid = single_writer_guid().entity_id;
809        let r_eid = r.guid().entity_id;
810        // Writer announces seq 1 (not yet delivered).
811        r.handle_heartbeat(
812            p1(),
813            &heartbeat(w_eid, r_eid, 1, 1, 1, false),
814            Duration::ZERO,
815        );
816        assert!(first_state(&r).proxy.has_missing_changes());
817        assert_eq!(first_state(&r).proxy.last_available_sn(), sn(1));
818        // Re-discovery: the same writer proxy is added again.
819        r.add_writer_proxy(WriterProxy::new(
820            single_writer_guid(),
821            alloc::vec![Locator::udp_v4([127, 0, 0, 1], 7420)],
822            alloc::vec![],
823            true,
824        ));
825        // seq 1 must still be known as missing.
826        assert!(
827            first_state(&r).proxy.has_missing_changes(),
828            "re-add must not reset last_available/missing"
829        );
830        assert_eq!(first_state(&r).proxy.last_available_sn(), sn(1));
831    }
832
833    #[test]
834    fn in_order_data_delivered_immediately() {
835        let mut r = make_reader(10);
836        let w_eid = single_writer_guid().entity_id;
837        let r_eid = r.guid().entity_id;
838        let delivered = r.handle_data(p1(), &data(w_eid, r_eid, 1, 0xAA), None);
839        assert_eq!(delivered.len(), 1);
840        assert_eq!(delivered[0].payload.as_ref(), &[0xAA][..]);
841        assert_eq!(delivered[0].writer_guid, single_writer_guid());
842        assert_eq!(first_state(&r).delivered_up_to, sn(1));
843    }
844
845    #[test]
846    fn out_of_order_data_buffered_until_gap_filled() {
847        let mut r = make_reader(10);
848        let w = single_writer_guid().entity_id;
849        let rd = r.guid().entity_id;
850        assert!(r.handle_data(p1(), &data(w, rd, 2, 0x22), None).is_empty());
851        assert!(r.handle_data(p1(), &data(w, rd, 3, 0x33), None).is_empty());
852        let out = r.handle_data(p1(), &data(w, rd, 1, 0x11), None);
853        assert_eq!(
854            out.iter().map(|s| s.sequence_number).collect::<Vec<_>>(),
855            alloc::vec![sn(1), sn(2), sn(3)]
856        );
857        assert_eq!(first_state(&r).delivered_up_to, sn(3));
858    }
859
860    /// A BEST_EFFORT reader (RTPS §8.4.12.1) must NOT block on a leading gap —
861    /// it does not NACK to repair it, so waiting would deadlock forever. It
862    /// delivers from the lowest received SN. Regression for the cross-vendor
863    /// OpenDDS/RTI case where the writer's first sample to us is mid-stream.
864    #[test]
865    fn best_effort_reader_skips_leading_gap() {
866        let mut r = make_reader(10);
867        r.set_best_effort(true);
868        let w = single_writer_guid().entity_id;
869        let rd = r.guid().entity_id;
870        // SN 1 never arrives. A reliable reader would buffer SN 2/3 forever
871        // (see `out_of_order_data_buffered_until_gap_filled`); a best-effort
872        // reader delivers SN 2 immediately, skipping the missing SN 1.
873        let out = r.handle_data(p1(), &data(w, rd, 2, 0x22), None);
874        assert_eq!(
875            out.iter().map(|s| s.sequence_number).collect::<Vec<_>>(),
876            alloc::vec![sn(2)],
877            "best-effort reader must deliver SN 2 despite the missing SN 1"
878        );
879        assert_eq!(first_state(&r).delivered_up_to, sn(2));
880        // Subsequent in-order samples keep flowing.
881        let out3 = r.handle_data(p1(), &data(w, rd, 3, 0x33), None);
882        assert_eq!(out3.len(), 1);
883        assert_eq!(out3[0].sequence_number, sn(3));
884    }
885
886    #[test]
887    fn duplicate_data_is_rejected() {
888        let mut r = make_reader(10);
889        let w = single_writer_guid().entity_id;
890        let rd = r.guid().entity_id;
891        r.handle_data(p1(), &data(w, rd, 1, 0xAA), None);
892        let second = r.handle_data(p1(), &data(w, rd, 1, 0xAA), None);
893        assert!(second.is_empty());
894    }
895
896    #[test]
897    fn mismatched_writer_id_is_counted() {
898        let mut r = make_reader(10);
899        let rd = r.guid().entity_id;
900        let foreign = EntityId::user_writer_with_key([0xFF, 0xFF, 0xFF]);
901        assert!(
902            r.handle_data(p1(), &data(foreign, rd, 1, 0xAA), None)
903                .is_empty()
904        );
905        assert_eq!(r.unknown_src_count(), 1);
906    }
907
908    // ---------- Wire-Side Lifecycle (T8) ----------
909
910    #[test]
911    fn alive_data_yields_alive_changekind() {
912        let mut r = make_reader(10);
913        let w = single_writer_guid().entity_id;
914        let rd = r.guid().entity_id;
915        let delivered = r.handle_data(p1(), &data(w, rd, 1, 0xAA), None);
916        assert_eq!(delivered.len(), 1);
917        assert_eq!(delivered[0].kind, ChangeKind::Alive);
918    }
919
920    fn lifecycle_data(
921        wid: EntityId,
922        rid: EntityId,
923        n: i64,
924        key_hash: [u8; 16],
925        status_bits: u32,
926    ) -> DataSubmessage {
927        DataSubmessage {
928            extra_flags: 0,
929            reader_id: rid,
930            writer_id: wid,
931            writer_sn: sn(n),
932            inline_qos: Some(crate::inline_qos::lifecycle_inline_qos(
933                key_hash,
934                status_bits,
935            )),
936            key_flag: true,
937            non_standard_flag: false,
938            serialized_payload: alloc::sync::Arc::from(alloc::vec![0u8; 0]),
939        }
940    }
941
942    #[test]
943    fn dispose_data_yields_not_alive_disposed() {
944        let mut r = make_reader(10);
945        let w = single_writer_guid().entity_id;
946        let rd = r.guid().entity_id;
947        let delivered = r.handle_data(
948            p1(),
949            &lifecycle_data(
950                w,
951                rd,
952                1,
953                [0xAB; 16],
954                crate::inline_qos::status_info::DISPOSED,
955            ),
956            None,
957        );
958        assert_eq!(delivered.len(), 1);
959        assert_eq!(delivered[0].kind, ChangeKind::NotAliveDisposed);
960    }
961
962    #[test]
963    fn unregister_data_yields_not_alive_unregistered() {
964        let mut r = make_reader(10);
965        let w = single_writer_guid().entity_id;
966        let rd = r.guid().entity_id;
967        let delivered = r.handle_data(
968            p1(),
969            &lifecycle_data(
970                w,
971                rd,
972                1,
973                [0xCD; 16],
974                crate::inline_qos::status_info::UNREGISTERED,
975            ),
976            None,
977        );
978        assert_eq!(delivered.len(), 1);
979        assert_eq!(delivered[0].kind, ChangeKind::NotAliveUnregistered);
980    }
981
982    #[test]
983    fn dispose_and_unregister_combined() {
984        let mut r = make_reader(10);
985        let w = single_writer_guid().entity_id;
986        let rd = r.guid().entity_id;
987        let bits =
988            crate::inline_qos::status_info::DISPOSED | crate::inline_qos::status_info::UNREGISTERED;
989        let delivered = r.handle_data(p1(), &lifecycle_data(w, rd, 1, [0xEF; 16], bits), None);
990        assert_eq!(delivered.len(), 1);
991        assert_eq!(delivered[0].kind, ChangeKind::NotAliveDisposedUnregistered);
992    }
993
994    #[test]
995    fn key_only_alive_registration_is_acked_but_not_delivered() {
996        // A key_flag=true DATA without a DISPOSED/UNREGISTERED status is a
997        // key-only *instance registration* (e.g. OpenDDS register_instance):
998        // the payload holds only the @key fields, not a full sample. It must
999        // NOT be delivered to the application — full-decoding the key-only
1000        // payload would raise a spurious cross-vendor decode error. The SN is
1001        // still acknowledged so the reliable protocol advances; the actual data
1002        // arrives in the D-flag samples.
1003        let mut r = make_reader(10);
1004        let w = single_writer_guid().entity_id;
1005        let rd = r.guid().entity_id;
1006        let mut d = data(w, rd, 1, 0xAA);
1007        d.key_flag = true;
1008        let delivered = r.handle_data(p1(), &d, None);
1009        assert!(
1010            delivered.is_empty(),
1011            "key-only ALIVE registration must not be delivered"
1012        );
1013        // A subsequent full (D-flag) sample at SN 2 is delivered: the reader
1014        // advanced past the registration without stalling.
1015        let d2 = data(w, rd, 2, 0xBB);
1016        let delivered2 = r.handle_data(p1(), &d2, None);
1017        assert_eq!(delivered2.len(), 1);
1018        assert_eq!(delivered2[0].sequence_number, SequenceNumber(2));
1019    }
1020
1021    #[test]
1022    fn heartbeat_with_missing_triggers_acknack_after_delay() {
1023        let mut r = make_reader(10);
1024        let w = single_writer_guid().entity_id;
1025        let rd = r.guid().entity_id;
1026        r.handle_heartbeat(p1(), &heartbeat(w, rd, 1, 3, 1, false), Duration::ZERO);
1027        assert!(r.tick(Duration::from_millis(100)).unwrap().is_empty());
1028        let out = r.tick(Duration::from_millis(250)).unwrap();
1029        assert_eq!(out.len(), 1);
1030    }
1031
1032    #[test]
1033    fn heartbeat_without_missing_and_final_schedules_no_acknack() {
1034        let mut r = make_reader(10);
1035        let w = single_writer_guid().entity_id;
1036        let rd = r.guid().entity_id;
1037        r.handle_data(p1(), &data(w, rd, 1, 0xAA), None);
1038        r.handle_heartbeat(p1(), &heartbeat(w, rd, 1, 1, 1, true), Duration::ZERO);
1039        assert!(r.tick(Duration::from_secs(10)).unwrap().is_empty());
1040    }
1041
1042    // ---------- Multi-writer (T4.5) ----------
1043
1044    fn second_writer_guid() -> Guid {
1045        Guid::new(
1046            GuidPrefix::from_bytes([3; 12]),
1047            EntityId::user_writer_with_key([0x40, 0x50, 0x60]),
1048        )
1049    }
1050
1051    fn add_second_writer(r: &mut ReliableReader) {
1052        r.add_writer_proxy(WriterProxy::new(
1053            second_writer_guid(),
1054            alloc::vec![Locator::udp_v4([127, 0, 0, 2], 7420)],
1055            alloc::vec![],
1056            true,
1057        ));
1058    }
1059
1060    #[test]
1061    fn add_writer_proxy_increases_count() {
1062        let mut r = make_reader(10);
1063        add_second_writer(&mut r);
1064        assert_eq!(r.writer_proxy_count(), 2);
1065    }
1066
1067    #[test]
1068    fn two_writers_with_overlapping_sn_spaces_both_delivered() {
1069        // Core regression: both writers use SN 1. Without per-proxy
1070        // state the second `handle_data` would be rejected as a duplicate.
1071        let mut r = make_reader(10);
1072        add_second_writer(&mut r);
1073        let w1 = single_writer_guid().entity_id;
1074        let w2 = second_writer_guid().entity_id;
1075        let rd = r.guid().entity_id;
1076
1077        let d1 = r.handle_data(p1(), &data(w1, rd, 1, 0xAA), None);
1078        let d2 = r.handle_data(p2(), &data(w2, rd, 1, 0xBB), None);
1079
1080        assert_eq!(d1.len(), 1);
1081        assert_eq!(d1[0].payload.as_ref(), &[0xAA][..]);
1082        assert_eq!(d1[0].writer_guid, single_writer_guid());
1083        assert_eq!(d2.len(), 1);
1084        assert_eq!(d2[0].payload.as_ref(), &[0xBB][..]);
1085        assert_eq!(d2[0].writer_guid, second_writer_guid());
1086
1087        assert_eq!(r.writer_proxies()[0].delivered_up_to, sn(1));
1088        assert_eq!(r.writer_proxies()[1].delivered_up_to, sn(1));
1089    }
1090
1091    #[test]
1092    fn same_entity_id_different_prefix_not_confused() {
1093        // Regression H-1/H-2: two remote writers with the SAME entity_id but
1094        // different guid_prefix (the normal case for fan-in — entity keys are
1095        // assigned per-participant from a low base, the first user writer
1096        // of each participant shares the same entity_id). A sample from writer B
1097        // must NOT be attributed to writer A (first proxy).
1098        let mut r = make_reader(10);
1099        let eid = single_writer_guid().entity_id; // identical to proxy 0
1100        let prefix_b = GuidPrefix::from_bytes([9; 12]);
1101        let guid_b = Guid::new(prefix_b, eid);
1102        r.add_writer_proxy(WriterProxy::new(
1103            guid_b,
1104            alloc::vec![Locator::udp_v4([127, 0, 0, 9], 7420)],
1105            alloc::vec![],
1106            true,
1107        ));
1108        assert_eq!(r.writer_proxy_count(), 2);
1109        let rd = r.guid().entity_id;
1110        // Sample from B: must be assigned to the B proxy, not A.
1111        let d = r.handle_data(prefix_b, &data(eid, rd, 1, 0xBB), None);
1112        assert_eq!(d.len(), 1);
1113        assert_eq!(d[0].writer_guid, guid_b, "sample misattributed");
1114        // The A proxy (proxy 0) must NOT have advanced.
1115        assert_eq!(r.writer_proxies()[0].delivered_up_to, sn(0));
1116        assert_eq!(r.writer_proxies()[1].delivered_up_to, sn(1));
1117    }
1118
1119    #[test]
1120    fn remove_writer_proxy_drops_its_state() {
1121        let mut r = make_reader(10);
1122        add_second_writer(&mut r);
1123        let removed = r.remove_writer_proxy(single_writer_guid());
1124        assert!(removed.is_some());
1125        assert_eq!(r.writer_proxy_count(), 1);
1126        assert_eq!(
1127            r.writer_proxies()[0].proxy.remote_writer_guid,
1128            second_writer_guid()
1129        );
1130    }
1131
1132    #[test]
1133    fn tick_emits_one_acknack_per_writer_with_missing() {
1134        let mut r = make_reader(10);
1135        add_second_writer(&mut r);
1136        let rd = r.guid().entity_id;
1137        // Both writers send an HB with a missing SN
1138        r.handle_heartbeat(
1139            p1(),
1140            &heartbeat(single_writer_guid().entity_id, rd, 1, 3, 1, false),
1141            Duration::ZERO,
1142        );
1143        r.handle_heartbeat(
1144            p2(),
1145            &heartbeat(second_writer_guid().entity_id, rd, 1, 5, 1, false),
1146            Duration::ZERO,
1147        );
1148        let out = r.tick(Duration::from_millis(250)).unwrap();
1149        // 2 ACKNACKs (one per writer)
1150        assert_eq!(out.len(), 2);
1151    }
1152
1153    // ---------- WP 1.E stage B: pre-emptive ACKNACK ----------
1154
1155    /// §8.4.2.3.4: when matching a new writer proxy the reader sends
1156    /// **proactively** an ACKNACK with `bitmap_base=1, num_bits=0,
1157    /// final_flag=false` — this speeds up the first data flow by
1158    /// exactly one HB period (typ. 1 s).
1159    #[test]
1160    fn pre_emptive_acknack_emitted_after_add_writer_proxy() {
1161        let reader_guid = Guid::new(
1162            GuidPrefix::from_bytes([2; 12]),
1163            EntityId::user_reader_with_key([0xA0, 0xB0, 0xC0]),
1164        );
1165        let mut r = ReliableReader::new(ReliableReaderConfig {
1166            guid: reader_guid,
1167            vendor_id: VendorId::ZERODDS,
1168            writer_proxies: alloc::vec![],
1169            max_samples_per_proxy: 10,
1170            heartbeat_response_delay: Duration::from_millis(200),
1171            assembler_caps: AssemblerCaps::default(),
1172        });
1173        r.add_writer_proxy(WriterProxy::new(
1174            single_writer_guid(),
1175            alloc::vec![Locator::udp_v4([127, 0, 0, 1], 7420)],
1176            alloc::vec![],
1177            true,
1178        ));
1179        // Values from add_writer_proxy: pending_acknack_since=Duration::ZERO
1180        // → tick(>=delay) yields the pre-emptive AckNack.
1181        let out = r.tick(Duration::from_millis(250)).unwrap();
1182        assert_eq!(out.len(), 1, "exactly one Pre-Emptive ACKNACK expected");
1183        let parsed = decode_datagram(&out[0]).unwrap();
1184        let ack = parsed
1185            .submessages
1186            .iter()
1187            .find_map(|s| {
1188                if let ParsedSubmessage::AckNack(a) = s {
1189                    Some(a)
1190                } else {
1191                    None
1192                }
1193            })
1194            .expect("ACKNACK in datagram");
1195        assert_eq!(ack.reader_sn_state.bitmap_base, sn(1));
1196        assert_eq!(ack.reader_sn_state.num_bits, 0);
1197        assert!(
1198            !ack.final_flag,
1199            "Pre-Emptive ACKNACK must be non-final (force HB-response)"
1200        );
1201    }
1202
1203    /// Pre-emptive ACKNACK does NOT happen if `add_writer_proxy` was never
1204    /// called (defensive sanity check for the default reader).
1205    #[test]
1206    fn no_pre_emptive_acknack_without_proxy() {
1207        let reader_guid = Guid::new(
1208            GuidPrefix::from_bytes([2; 12]),
1209            EntityId::user_reader_with_key([0xA0, 0xB0, 0xC0]),
1210        );
1211        let mut r = ReliableReader::new(ReliableReaderConfig {
1212            guid: reader_guid,
1213            vendor_id: VendorId::ZERODDS,
1214            writer_proxies: alloc::vec![],
1215            max_samples_per_proxy: 10,
1216            heartbeat_response_delay: Duration::from_millis(200),
1217            assembler_caps: AssemblerCaps::default(),
1218        });
1219        // No proxies → no ACKNACK
1220        assert!(r.tick(Duration::from_secs(10)).unwrap().is_empty());
1221    }
1222
1223    /// Initial proxies from `ReliableReaderConfig.writer_proxies` get
1224    /// **no** automatic pre-emptive — only via `add_writer_proxy`.
1225    /// This is consistent with the DCPS integration: the discovery layer calls
1226    /// `add_writer_proxy` as soon as the SEDP match is established.
1227    #[test]
1228    fn initial_proxy_from_config_does_not_send_pre_emptive() {
1229        // make_reader() uses config.writer_proxies, not add_writer_proxy
1230        let mut r = make_reader(10);
1231        // Before add: no pre-emptive even after a long tick
1232        assert!(
1233            r.tick(Duration::from_secs(10)).unwrap().is_empty(),
1234            "initial proxy from config must not emit Pre-Emptive"
1235        );
1236    }
1237
1238    #[test]
1239    fn pre_emptive_acknack_carries_info_dst() {
1240        // The pre-emptive ACKNACK MUST be wrapped in INFO_DST(writer_prefix),
1241        // otherwise Cyclone/Fast-DDS discard the submessage as
1242        // "not for me" (Spec §8.3.7.6 / §8.3.8.7).
1243        let reader_guid = Guid::new(
1244            GuidPrefix::from_bytes([2; 12]),
1245            EntityId::user_reader_with_key([0xA0, 0xB0, 0xC0]),
1246        );
1247        let mut r = ReliableReader::new(ReliableReaderConfig {
1248            guid: reader_guid,
1249            vendor_id: VendorId::ZERODDS,
1250            writer_proxies: alloc::vec![],
1251            max_samples_per_proxy: 10,
1252            heartbeat_response_delay: Duration::from_millis(200),
1253            assembler_caps: AssemblerCaps::default(),
1254        });
1255        r.add_writer_proxy(WriterProxy::new(
1256            single_writer_guid(),
1257            alloc::vec![Locator::udp_v4([127, 0, 0, 1], 7420)],
1258            alloc::vec![],
1259            true,
1260        ));
1261        let out = r.tick(Duration::from_millis(250)).unwrap();
1262        assert_eq!(out.len(), 1);
1263        let parsed = decode_datagram(&out[0]).unwrap();
1264        // submessages[0] = INFO_DST (Unknown in the decoder, because InfoDst
1265        // is not unpacked by the decoder), [1] = ACKNACK
1266        assert!(parsed.submessages.len() >= 2, "INFO_DST + ACKNACK");
1267        match &parsed.submessages[0] {
1268            ParsedSubmessage::Unknown { id, .. } => assert_eq!(*id, 0x0E),
1269            other => panic!("expected INFO_DST first, got {other:?}"),
1270        }
1271    }
1272
1273    #[test]
1274    fn unknown_writer_id_in_heartbeat_counts_not_crashes() {
1275        let mut r = make_reader(10);
1276        let rd = r.guid().entity_id;
1277        let foreign = EntityId::user_writer_with_key([0xFF, 0xFF, 0xFF]);
1278        r.handle_heartbeat(
1279            p1(),
1280            &heartbeat(foreign, rd, 1, 3, 1, false),
1281            Duration::ZERO,
1282        );
1283        assert_eq!(r.unknown_src_count(), 1);
1284        assert!(r.tick(Duration::from_secs(1)).unwrap().is_empty());
1285    }
1286}