Skip to main content

ipfrs_network/
packet_fragmentation_assembler.rs

1//! Packet Fragmentation and Reassembly Engine
2//!
3//! Provides production-quality splitting of large messages into MTU-sized fragments
4//! with FNV-1a checksums, duplicate detection, stale-entry expiration, and rich
5//! statistics – all without panics (`unwrap`-free).
6
7use std::collections::{HashMap, VecDeque};
8
9// ──────────────────────────────────────────────────────────────────────────────
10// Type aliases
11// ──────────────────────────────────────────────────────────────────────────────
12
13/// Opaque message identifier.
14pub type PfaMessageId = u64;
15
16/// Re-export the main struct under a longer alias for doc-clarity.
17pub type PfaPacketFragmentationAssembler = PacketFragmentationAssembler;
18
19// ──────────────────────────────────────────────────────────────────────────────
20// Inline helpers (no external deps)
21// ──────────────────────────────────────────────────────────────────────────────
22
23/// FNV-1a 64-bit hash.
24#[inline]
25pub fn fnv1a_64(data: &[u8]) -> u64 {
26    let mut h: u64 = 14_695_981_039_346_656_037;
27    for &b in data {
28        h ^= b as u64;
29        h = h.wrapping_mul(1_099_511_628_211);
30    }
31    h
32}
33
34/// Xorshift-64 PRNG.
35#[inline]
36pub fn xorshift64(state: &mut u64) -> u64 {
37    let mut x = *state;
38    x ^= x << 13;
39    x ^= x >> 7;
40    x ^= x << 17;
41    *state = x;
42    x
43}
44
45/// FNV-1a truncated to 32 bits (used as per-fragment checksum).
46#[inline]
47fn fnv1a_32(data: &[u8]) -> u32 {
48    (fnv1a_64(data) & 0xFFFF_FFFF) as u32
49}
50
51// ──────────────────────────────────────────────────────────────────────────────
52// Configuration
53// ──────────────────────────────────────────────────────────────────────────────
54
55/// Configuration for [`PacketFragmentationAssembler`].
56#[derive(Debug, Clone)]
57pub struct PfaAssemblerConfig {
58    /// Maximum transmission unit (payload bytes per fragment).
59    pub mtu: usize,
60    /// Upper limit on fragment count for a single message.
61    pub max_fragments: u32,
62    /// Seconds before a partially-received message is discarded.
63    pub reassembly_timeout_secs: u64,
64    /// Whether to verify FNV-1a checksums on received fragments.
65    pub checksum_enabled: bool,
66}
67
68impl Default for PfaAssemblerConfig {
69    fn default() -> Self {
70        Self {
71            mtu: 1_400,
72            max_fragments: 4_096,
73            reassembly_timeout_secs: 30,
74            checksum_enabled: true,
75        }
76    }
77}
78
79// ──────────────────────────────────────────────────────────────────────────────
80// Data types
81// ──────────────────────────────────────────────────────────────────────────────
82
83/// A single fragment of a fragmented message.
84#[derive(Debug, Clone)]
85pub struct PfaFragment {
86    /// Message this fragment belongs to.
87    pub msg_id: PfaMessageId,
88    /// Zero-based index of this fragment within the message.
89    pub fragment_index: u32,
90    /// Total number of fragments that constitute the full message.
91    pub total_fragments: u32,
92    /// Byte offset of this fragment within the original message.
93    pub offset: usize,
94    /// Payload bytes.
95    pub data: Vec<u8>,
96    /// FNV-1a low-32 checksum of `data`.
97    pub checksum: u32,
98}
99
100/// Internal buffer accumulating received fragments for one message.
101#[derive(Debug)]
102pub struct PfaReassemblyBuffer {
103    /// Message identifier.
104    pub msg_id: PfaMessageId,
105    /// Slots for each expected fragment; `None` means not yet received.
106    pub received: Vec<Option<PfaFragment>>,
107    /// How many fragments are expected in total.
108    pub total_fragments: u32,
109    /// Unix-epoch seconds when the buffer was first created.
110    pub created_at: u64,
111    /// Unix-epoch seconds of the most recently received fragment.
112    pub last_updated: u64,
113    /// How many non-duplicate fragments have been received so far.
114    received_count: u32,
115}
116
117impl PfaReassemblyBuffer {
118    fn new(msg_id: PfaMessageId, total_fragments: u32, now_ts: u64) -> Self {
119        Self {
120            msg_id,
121            received: vec![None; total_fragments as usize],
122            total_fragments,
123            created_at: now_ts,
124            last_updated: now_ts,
125            received_count: 0,
126        }
127    }
128
129    /// Returns how many non-duplicate fragments have been stored.
130    pub fn received_count(&self) -> u32 {
131        self.received_count
132    }
133
134    /// Returns `true` when all fragments have been received.
135    pub fn is_complete(&self) -> bool {
136        self.received_count == self.total_fragments
137    }
138}
139
140/// Audit record for the fragment log.
141#[derive(Debug, Clone)]
142pub struct PfaFragmentRecord {
143    /// Timestamp of the event (Unix-epoch seconds).
144    pub ts: u64,
145    /// Message the fragment belongs to.
146    pub msg_id: PfaMessageId,
147    /// Fragment index.
148    pub fragment_index: u32,
149    /// Whether this was a duplicate fragment.
150    pub is_dup: bool,
151    /// Whether this fragment completed the message.
152    pub assembled: bool,
153}
154
155/// Result returned by [`PacketFragmentationAssembler::receive_fragment`].
156#[derive(Debug)]
157pub enum PfaReceiveResult {
158    /// Fragment stored; message is not yet complete.
159    Buffered,
160    /// Fragment was already received (duplicate).
161    Duplicate,
162    /// All fragments are now available; the reassembled payload is included.
163    Assembled(Vec<u8>),
164    /// An error occurred (checksum mismatch, out-of-range index, etc.).
165    Error(String),
166}
167
168/// Snapshot of assembler-level counters.
169#[derive(Debug, Clone, Default)]
170pub struct PfaAssemblerStats {
171    /// Number of messages that have been fragmented outbound.
172    pub total_fragmented: u64,
173    /// Number of messages that have been fully reassembled.
174    pub total_assembled: u64,
175    /// Fragments dropped due to checksum failure, range errors, or expiry.
176    pub total_dropped: u64,
177    /// Average fragment count across all fragmented messages.
178    pub avg_fragments: f64,
179}
180
181// ──────────────────────────────────────────────────────────────────────────────
182// Core engine
183// ──────────────────────────────────────────────────────────────────────────────
184
185/// Packet fragmentation and reassembly engine.
186///
187/// # Thread safety
188///
189/// This struct is `Send` but not `Sync`; wrap in `Arc<Mutex<...>>` for shared use.
190pub struct PacketFragmentationAssembler {
191    config: PfaAssemblerConfig,
192    /// Reassembly state keyed by message-id.
193    buffers: HashMap<PfaMessageId, PfaReassemblyBuffer>,
194    /// Rolling audit log (bounded to 1 000 entries).
195    fragment_log: VecDeque<PfaFragmentRecord>,
196    /// PRNG state used for unique message IDs.
197    rng_state: u64,
198    // Statistics counters
199    total_fragmented: u64,
200    total_assembled: u64,
201    total_dropped: u64,
202    /// Running sum of fragment-counts across all fragmented messages (for avg).
203    fragment_count_sum: u64,
204}
205
206impl PacketFragmentationAssembler {
207    // ── Construction ──────────────────────────────────────────────────────────
208
209    /// Create a new assembler with the given configuration.
210    pub fn new(config: PfaAssemblerConfig) -> Self {
211        // Seed derived from config to give each instance a distinct PRNG stream.
212        let seed = fnv1a_64(&config.mtu.to_le_bytes())
213            ^ fnv1a_64(&config.reassembly_timeout_secs.to_le_bytes())
214            ^ 0xDEAD_BEEF_CAFE_BABE;
215        Self {
216            config,
217            buffers: HashMap::new(),
218            fragment_log: VecDeque::with_capacity(1_000),
219            rng_state: seed,
220            total_fragmented: 0,
221            total_assembled: 0,
222            total_dropped: 0,
223            fragment_count_sum: 0,
224        }
225    }
226
227    /// Convenience constructor using default configuration.
228    pub fn with_defaults() -> Self {
229        Self::new(PfaAssemblerConfig::default())
230    }
231
232    // ── PRNG helpers ──────────────────────────────────────────────────────────
233
234    /// Generate the next pseudo-random u64.
235    fn next_rand(&mut self) -> u64 {
236        xorshift64(&mut self.rng_state)
237    }
238
239    /// Generate a new message ID that is not currently in use.
240    pub fn generate_msg_id(&mut self) -> PfaMessageId {
241        loop {
242            let id = self.next_rand();
243            if id != 0 && !self.buffers.contains_key(&id) {
244                return id;
245            }
246        }
247    }
248
249    // ── Fragmentation ─────────────────────────────────────────────────────────
250
251    /// Split `data` into MTU-sized fragments tagged with `msg_id`.
252    ///
253    /// Returns an empty `Vec` if `data` is empty.
254    ///
255    /// # Errors
256    ///
257    /// Returns `Err` if the payload would require more than
258    /// `config.max_fragments` fragments.
259    pub fn fragment(
260        &mut self,
261        msg_id: PfaMessageId,
262        data: &[u8],
263    ) -> Result<Vec<PfaFragment>, String> {
264        if data.is_empty() {
265            return Ok(Vec::new());
266        }
267
268        let mtu = self.config.mtu.max(1);
269        let total_fragments = data.len().div_ceil(mtu) as u32;
270
271        if total_fragments > self.config.max_fragments {
272            return Err(format!(
273                "message requires {} fragments but max_fragments is {}",
274                total_fragments, self.config.max_fragments
275            ));
276        }
277
278        let mut fragments = Vec::with_capacity(total_fragments as usize);
279        let mut offset = 0usize;
280
281        for idx in 0..total_fragments {
282            let end = (offset + mtu).min(data.len());
283            let payload = data[offset..end].to_vec();
284            let checksum = if self.config.checksum_enabled {
285                fnv1a_32(&payload)
286            } else {
287                0
288            };
289
290            fragments.push(PfaFragment {
291                msg_id,
292                fragment_index: idx,
293                total_fragments,
294                offset,
295                data: payload,
296                checksum,
297            });
298
299            offset = end;
300        }
301
302        self.total_fragmented += 1;
303        self.fragment_count_sum += total_fragments as u64;
304
305        Ok(fragments)
306    }
307
308    // ── Reception ─────────────────────────────────────────────────────────────
309
310    /// Process a received fragment.
311    ///
312    /// `now_ts` is the current Unix-epoch timestamp in seconds and is used for
313    /// the fragment log and buffer creation timestamps.
314    pub fn receive_fragment(&mut self, fragment: PfaFragment, now_ts: u64) -> PfaReceiveResult {
315        // ── Validate checksum ─────────────────────────────────────────────────
316        if self.config.checksum_enabled && !Self::verify_checksum_static(&fragment) {
317            self.total_dropped += 1;
318            self.push_log(PfaFragmentRecord {
319                ts: now_ts,
320                msg_id: fragment.msg_id,
321                fragment_index: fragment.fragment_index,
322                is_dup: false,
323                assembled: false,
324            });
325            return PfaReceiveResult::Error(format!(
326                "checksum mismatch for msg_id={} fragment_index={}",
327                fragment.msg_id, fragment.fragment_index
328            ));
329        }
330
331        // ── Basic sanity checks ───────────────────────────────────────────────
332        if fragment.total_fragments == 0 {
333            self.total_dropped += 1;
334            return PfaReceiveResult::Error("total_fragments must be > 0".into());
335        }
336        if fragment.fragment_index >= fragment.total_fragments {
337            self.total_dropped += 1;
338            return PfaReceiveResult::Error(format!(
339                "fragment_index {} out of range (total={})",
340                fragment.fragment_index, fragment.total_fragments
341            ));
342        }
343        if fragment.total_fragments > self.config.max_fragments {
344            self.total_dropped += 1;
345            return PfaReceiveResult::Error(format!(
346                "total_fragments {} exceeds max_fragments {}",
347                fragment.total_fragments, self.config.max_fragments
348            ));
349        }
350
351        let msg_id = fragment.msg_id;
352        let frag_idx = fragment.fragment_index;
353        let total = fragment.total_fragments;
354
355        // ── Ensure reassembly buffer exists ───────────────────────────────────
356        let buf = self
357            .buffers
358            .entry(msg_id)
359            .or_insert_with(|| PfaReassemblyBuffer::new(msg_id, total, now_ts));
360
361        // Guard: total_fragments must match the buffer's expectation.
362        if buf.total_fragments != total {
363            self.total_dropped += 1;
364            return PfaReceiveResult::Error(format!(
365                "total_fragments mismatch: buffer expects {} but fragment says {}",
366                buf.total_fragments, total
367            ));
368        }
369
370        // Bounds check on the slot vec (should be redundant, but be safe).
371        if frag_idx as usize >= buf.received.len() {
372            self.total_dropped += 1;
373            return PfaReceiveResult::Error(format!(
374                "fragment_index {} out of buffer range {}",
375                frag_idx,
376                buf.received.len()
377            ));
378        }
379
380        // ── Duplicate detection ───────────────────────────────────────────────
381        if buf.received[frag_idx as usize].is_some() {
382            self.push_log(PfaFragmentRecord {
383                ts: now_ts,
384                msg_id,
385                fragment_index: frag_idx,
386                is_dup: true,
387                assembled: false,
388            });
389            return PfaReceiveResult::Duplicate;
390        }
391
392        // ── Store fragment ────────────────────────────────────────────────────
393        buf.received[frag_idx as usize] = Some(fragment);
394        buf.received_count += 1;
395        buf.last_updated = now_ts;
396        let complete = buf.is_complete();
397
398        // ── Attempt reassembly if complete ────────────────────────────────────
399        if complete {
400            // Remove buffer and reassemble.
401            if let Some(completed_buf) = self.buffers.remove(&msg_id) {
402                match Self::assemble_buffer(completed_buf) {
403                    Ok(data) => {
404                        self.total_assembled += 1;
405                        self.push_log(PfaFragmentRecord {
406                            ts: now_ts,
407                            msg_id,
408                            fragment_index: frag_idx,
409                            is_dup: false,
410                            assembled: true,
411                        });
412                        return PfaReceiveResult::Assembled(data);
413                    }
414                    Err(e) => {
415                        self.total_dropped += 1;
416                        self.push_log(PfaFragmentRecord {
417                            ts: now_ts,
418                            msg_id,
419                            fragment_index: frag_idx,
420                            is_dup: false,
421                            assembled: false,
422                        });
423                        return PfaReceiveResult::Error(e);
424                    }
425                }
426            }
427        }
428
429        self.push_log(PfaFragmentRecord {
430            ts: now_ts,
431            msg_id,
432            fragment_index: frag_idx,
433            is_dup: false,
434            assembled: false,
435        });
436        PfaReceiveResult::Buffered
437    }
438
439    // ── Reassembly ────────────────────────────────────────────────────────────
440
441    /// Attempt to reassemble message `msg_id` if all fragments are present.
442    ///
443    /// Returns `None` if the buffer does not exist or is incomplete.
444    pub fn reassemble(&mut self, msg_id: PfaMessageId) -> Option<Vec<u8>> {
445        // Peek whether complete before removing.
446        let complete = self
447            .buffers
448            .get(&msg_id)
449            .map(|b| b.is_complete())
450            .unwrap_or(false);
451
452        if !complete {
453            return None;
454        }
455
456        let buf = self.buffers.remove(&msg_id)?;
457        match Self::assemble_buffer(buf) {
458            Ok(data) => {
459                self.total_assembled += 1;
460                Some(data)
461            }
462            Err(_) => {
463                self.total_dropped += 1;
464                None
465            }
466        }
467    }
468
469    /// Internal: consume a complete buffer and produce the reassembled byte vector.
470    fn assemble_buffer(buf: PfaReassemblyBuffer) -> Result<Vec<u8>, String> {
471        // Sort slots by fragment index to reconstruct in-order.
472        let mut indexed: Vec<(u32, Vec<u8>)> = Vec::with_capacity(buf.total_fragments as usize);
473        for (idx, slot) in buf.received.into_iter().enumerate() {
474            match slot {
475                Some(frag) => indexed.push((frag.fragment_index, frag.data)),
476                None => {
477                    return Err(format!("missing fragment at slot {}", idx));
478                }
479            }
480        }
481        indexed.sort_unstable_by_key(|(i, _)| *i);
482
483        let total_len: usize = indexed.iter().map(|(_, d)| d.len()).sum();
484        let mut out = Vec::with_capacity(total_len);
485        for (_, data) in indexed {
486            out.extend_from_slice(&data);
487        }
488        Ok(out)
489    }
490
491    // ── Expiration ────────────────────────────────────────────────────────────
492
493    /// Remove all buffers whose `created_at` is older than the configured timeout.
494    pub fn expire_stale(&mut self, now_ts: u64) {
495        let timeout = self.config.reassembly_timeout_secs;
496        let before = self.buffers.len();
497        self.buffers
498            .retain(|_, buf| now_ts.saturating_sub(buf.created_at) < timeout);
499        let dropped_count = before - self.buffers.len();
500        self.total_dropped = self.total_dropped.saturating_add(dropped_count as u64);
501    }
502
503    // ── Queries ───────────────────────────────────────────────────────────────
504
505    /// List pending messages: `(msg_id, received_count, total_fragments)`.
506    pub fn pending_messages(&self) -> Vec<(PfaMessageId, u32, u32)> {
507        self.buffers
508            .values()
509            .map(|b| (b.msg_id, b.received_count(), b.total_fragments))
510            .collect()
511    }
512
513    /// Verify the checksum of a fragment (instance method forwarding to static).
514    pub fn verify_checksum(&self, fragment: &PfaFragment) -> bool {
515        if !self.config.checksum_enabled {
516            return true;
517        }
518        Self::verify_checksum_static(fragment)
519    }
520
521    /// Static checksum verifier (does not need a `&self` reference).
522    fn verify_checksum_static(fragment: &PfaFragment) -> bool {
523        fnv1a_32(&fragment.data) == fragment.checksum
524    }
525
526    /// Return a snapshot of assembler-level statistics.
527    pub fn assembler_stats(&self) -> PfaAssemblerStats {
528        let avg_fragments = if self.total_fragmented == 0 {
529            0.0
530        } else {
531            self.fragment_count_sum as f64 / self.total_fragmented as f64
532        };
533        PfaAssemblerStats {
534            total_fragmented: self.total_fragmented,
535            total_assembled: self.total_assembled,
536            total_dropped: self.total_dropped,
537            avg_fragments,
538        }
539    }
540
541    // ── Accessors ─────────────────────────────────────────────────────────────
542
543    /// Reference to the configuration.
544    pub fn config(&self) -> &PfaAssemblerConfig {
545        &self.config
546    }
547
548    /// Number of messages currently awaiting full reassembly.
549    pub fn pending_count(&self) -> usize {
550        self.buffers.len()
551    }
552
553    /// A slice of the most recent fragment log entries.
554    pub fn fragment_log(&self) -> &VecDeque<PfaFragmentRecord> {
555        &self.fragment_log
556    }
557
558    /// True if the engine holds any in-flight reassembly buffers.
559    pub fn has_pending(&self) -> bool {
560        !self.buffers.is_empty()
561    }
562
563    /// Return a reference to the reassembly buffer for `msg_id`, if any.
564    pub fn get_buffer(&self, msg_id: PfaMessageId) -> Option<&PfaReassemblyBuffer> {
565        self.buffers.get(&msg_id)
566    }
567
568    /// Drain and return all fragment log entries.
569    pub fn drain_log(&mut self) -> Vec<PfaFragmentRecord> {
570        self.fragment_log.drain(..).collect()
571    }
572
573    /// Reset all state while keeping configuration.
574    pub fn reset(&mut self) {
575        self.buffers.clear();
576        self.fragment_log.clear();
577        self.total_fragmented = 0;
578        self.total_assembled = 0;
579        self.total_dropped = 0;
580        self.fragment_count_sum = 0;
581    }
582
583    // ── Private helpers ───────────────────────────────────────────────────────
584
585    /// Append an entry to the rolling fragment log, evicting the oldest if full.
586    fn push_log(&mut self, record: PfaFragmentRecord) {
587        if self.fragment_log.len() >= 1_000 {
588            self.fragment_log.pop_front();
589        }
590        self.fragment_log.push_back(record);
591    }
592}
593
594// ──────────────────────────────────────────────────────────────────────────────
595// Tests
596// ──────────────────────────────────────────────────────────────────────────────
597
598#[cfg(test)]
599mod tests {
600    use super::*;
601
602    // ── Helpers ───────────────────────────────────────────────────────────────
603
604    fn make_assembler() -> PacketFragmentationAssembler {
605        PacketFragmentationAssembler::with_defaults()
606    }
607
608    fn make_assembler_cfg(
609        mtu: usize,
610        timeout: u64,
611        checksum: bool,
612    ) -> PacketFragmentationAssembler {
613        PacketFragmentationAssembler::new(PfaAssemblerConfig {
614            mtu,
615            max_fragments: 4_096,
616            reassembly_timeout_secs: timeout,
617            checksum_enabled: checksum,
618        })
619    }
620
621    fn receive_all(
622        asm: &mut PacketFragmentationAssembler,
623        frags: Vec<PfaFragment>,
624        ts: u64,
625    ) -> PfaReceiveResult {
626        let len = frags.len();
627        let mut last = PfaReceiveResult::Buffered;
628        for (i, f) in frags.into_iter().enumerate() {
629            last = asm.receive_fragment(f, ts);
630            if i == len - 1 {
631                return last;
632            }
633        }
634        last
635    }
636
637    // ── fnv1a_64 ──────────────────────────────────────────────────────────────
638
639    #[test]
640    fn test_fnv1a_empty() {
641        assert_eq!(fnv1a_64(&[]), 14_695_981_039_346_656_037);
642    }
643
644    #[test]
645    fn test_fnv1a_known_value() {
646        // "abc" → well-known FNV-1a-64 result
647        let h = fnv1a_64(b"abc");
648        assert_ne!(h, 0);
649        assert_ne!(h, 14_695_981_039_346_656_037);
650    }
651
652    #[test]
653    fn test_fnv1a_deterministic() {
654        let a = fnv1a_64(b"hello world");
655        let b = fnv1a_64(b"hello world");
656        assert_eq!(a, b);
657    }
658
659    #[test]
660    fn test_fnv1a_different_inputs() {
661        assert_ne!(fnv1a_64(b"foo"), fnv1a_64(b"bar"));
662    }
663
664    // ── xorshift64 ────────────────────────────────────────────────────────────
665
666    #[test]
667    fn test_xorshift_nonzero() {
668        let mut s = 12345u64;
669        let v = xorshift64(&mut s);
670        assert_ne!(v, 0);
671    }
672
673    #[test]
674    fn test_xorshift_changes_state() {
675        let mut s = 99999u64;
676        let a = xorshift64(&mut s);
677        let b = xorshift64(&mut s);
678        assert_ne!(a, b);
679    }
680
681    #[test]
682    fn test_xorshift_reproducible() {
683        let mut s1 = 42u64;
684        let mut s2 = 42u64;
685        assert_eq!(xorshift64(&mut s1), xorshift64(&mut s2));
686        assert_eq!(xorshift64(&mut s1), xorshift64(&mut s2));
687    }
688
689    // ── Config defaults ───────────────────────────────────────────────────────
690
691    #[test]
692    fn test_default_config() {
693        let cfg = PfaAssemblerConfig::default();
694        assert_eq!(cfg.mtu, 1_400);
695        assert_eq!(cfg.max_fragments, 4_096);
696        assert_eq!(cfg.reassembly_timeout_secs, 30);
697        assert!(cfg.checksum_enabled);
698    }
699
700    // ── Construction ──────────────────────────────────────────────────────────
701
702    #[test]
703    fn test_new_empty_state() {
704        let asm = make_assembler();
705        assert_eq!(asm.pending_count(), 0);
706        assert!(!asm.has_pending());
707        let stats = asm.assembler_stats();
708        assert_eq!(stats.total_fragmented, 0);
709        assert_eq!(stats.total_assembled, 0);
710        assert_eq!(stats.total_dropped, 0);
711    }
712
713    // ── generate_msg_id ───────────────────────────────────────────────────────
714
715    #[test]
716    fn test_generate_msg_id_nonzero() {
717        let mut asm = make_assembler();
718        let id = asm.generate_msg_id();
719        assert_ne!(id, 0);
720    }
721
722    #[test]
723    fn test_generate_msg_id_unique() {
724        let mut asm = make_assembler();
725        let ids: Vec<u64> = (0..100).map(|_| asm.generate_msg_id()).collect();
726        let unique: std::collections::HashSet<_> = ids.iter().cloned().collect();
727        assert_eq!(unique.len(), 100);
728    }
729
730    // ── fragment – basic ──────────────────────────────────────────────────────
731
732    #[test]
733    fn test_fragment_empty_data() {
734        let mut asm = make_assembler();
735        let frags = asm
736            .fragment(1, &[])
737            .expect("test: fragment empty data should succeed");
738        assert!(frags.is_empty());
739    }
740
741    #[test]
742    fn test_fragment_single_chunk() {
743        let mut asm = make_assembler_cfg(1_400, 30, true);
744        let data: Vec<u8> = (0..100).collect();
745        let frags = asm
746            .fragment(1, &data)
747            .expect("test: fragment single chunk should succeed");
748        assert_eq!(frags.len(), 1);
749        assert_eq!(frags[0].fragment_index, 0);
750        assert_eq!(frags[0].total_fragments, 1);
751        assert_eq!(frags[0].offset, 0);
752        assert_eq!(frags[0].data, data);
753    }
754
755    #[test]
756    fn test_fragment_exact_mtu() {
757        let mut asm = make_assembler_cfg(10, 30, true);
758        let data = vec![0xABu8; 10];
759        let frags = asm
760            .fragment(42, &data)
761            .expect("test: fragment exact mtu should succeed");
762        assert_eq!(frags.len(), 1);
763        assert_eq!(frags[0].data.len(), 10);
764    }
765
766    #[test]
767    fn test_fragment_two_chunks() {
768        let mut asm = make_assembler_cfg(10, 30, true);
769        let data = vec![0u8; 15];
770        let frags = asm
771            .fragment(1, &data)
772            .expect("test: fragment two chunks should succeed");
773        assert_eq!(frags.len(), 2);
774        assert_eq!(frags[0].data.len(), 10);
775        assert_eq!(frags[1].data.len(), 5);
776    }
777
778    #[test]
779    fn test_fragment_many_chunks() {
780        let mut asm = make_assembler_cfg(8, 30, true);
781        let data = vec![0u8; 100];
782        let frags = asm
783            .fragment(1, &data)
784            .expect("test: fragment many chunks should succeed");
785        assert_eq!(frags.len(), 13); // ceil(100 / 8)
786    }
787
788    #[test]
789    fn test_fragment_offsets_contiguous() {
790        let mut asm = make_assembler_cfg(10, 30, true);
791        let data: Vec<u8> = (0..35).collect();
792        let frags = asm
793            .fragment(1, &data)
794            .expect("test: fragment offsets contiguous should succeed");
795        let mut expected_offset = 0usize;
796        for f in &frags {
797            assert_eq!(f.offset, expected_offset);
798            expected_offset += f.data.len();
799        }
800        assert_eq!(expected_offset, 35);
801    }
802
803    #[test]
804    fn test_fragment_checksum_set() {
805        let mut asm = make_assembler_cfg(10, 30, true);
806        let data = vec![1u8, 2, 3];
807        let frags = asm
808            .fragment(1, &data)
809            .expect("test: fragment checksum set should succeed");
810        assert_ne!(frags[0].checksum, 0);
811    }
812
813    #[test]
814    fn test_fragment_no_checksum() {
815        let mut asm = make_assembler_cfg(10, 30, false);
816        let data = vec![1u8, 2, 3];
817        let frags = asm
818            .fragment(1, &data)
819            .expect("test: fragment no checksum should succeed");
820        assert_eq!(frags[0].checksum, 0);
821    }
822
823    #[test]
824    fn test_fragment_too_many() {
825        let mut asm = PacketFragmentationAssembler::new(PfaAssemblerConfig {
826            mtu: 1,
827            max_fragments: 4,
828            reassembly_timeout_secs: 30,
829            checksum_enabled: true,
830        });
831        let data = vec![0u8; 10];
832        assert!(asm.fragment(1, &data).is_err());
833    }
834
835    #[test]
836    fn test_fragment_increments_stats() {
837        let mut asm = make_assembler();
838        let data = vec![0u8; 2_800];
839        asm.fragment(1, &data)
840            .expect("test: fragment increments stats should succeed");
841        let stats = asm.assembler_stats();
842        assert_eq!(stats.total_fragmented, 1);
843        assert!(stats.avg_fragments >= 2.0);
844    }
845
846    // ── reassemble round-trip ─────────────────────────────────────────────────
847
848    #[test]
849    fn test_roundtrip_small() {
850        let mut asm = make_assembler_cfg(10, 30, true);
851        let data: Vec<u8> = (0..25u8).collect();
852        let frags = asm
853            .fragment(1, &data)
854            .expect("test: roundtrip small fragment should succeed");
855        let result = receive_all(&mut asm, frags, 0);
856        if let PfaReceiveResult::Assembled(reassembled) = result {
857            assert_eq!(reassembled, data);
858        } else {
859            panic!("expected Assembled, got {:?}", result);
860        }
861    }
862
863    #[test]
864    fn test_roundtrip_exact_mtu() {
865        let mut asm = make_assembler_cfg(8, 30, true);
866        let data = vec![0xFFu8; 8];
867        let frags = asm
868            .fragment(1, &data)
869            .expect("test: roundtrip exact mtu fragment should succeed");
870        let result = receive_all(&mut asm, frags, 0);
871        assert!(matches!(result, PfaReceiveResult::Assembled(d) if d == data));
872    }
873
874    #[test]
875    fn test_roundtrip_single_byte() {
876        let mut asm = make_assembler_cfg(1_400, 30, true);
877        let data = vec![42u8];
878        let frags = asm
879            .fragment(99, &data)
880            .expect("test: roundtrip single byte fragment should succeed");
881        let result = receive_all(&mut asm, frags, 0);
882        assert!(matches!(result, PfaReceiveResult::Assembled(d) if d == data));
883    }
884
885    #[test]
886    fn test_roundtrip_large_payload() {
887        let mut asm = make_assembler_cfg(500, 60, true);
888        let data: Vec<u8> = (0..u8::MAX).cycle().take(10_000).collect();
889        let frags = asm
890            .fragment(7, &data)
891            .expect("test: roundtrip large payload fragment should succeed");
892        let result = receive_all(&mut asm, frags, 0);
893        assert!(matches!(result, PfaReceiveResult::Assembled(d) if d == data));
894    }
895
896    #[test]
897    fn test_roundtrip_no_checksum() {
898        let mut asm = make_assembler_cfg(16, 30, false);
899        let data: Vec<u8> = (0..48).collect();
900        let frags = asm
901            .fragment(5, &data)
902            .expect("test: roundtrip no checksum fragment should succeed");
903        let result = receive_all(&mut asm, frags, 0);
904        assert!(matches!(result, PfaReceiveResult::Assembled(d) if d == data));
905    }
906
907    #[test]
908    fn test_roundtrip_reverse_order() {
909        let mut asm = make_assembler_cfg(10, 60, true);
910        let data: Vec<u8> = (0..30u8).collect();
911        let mut frags = asm
912            .fragment(3, &data)
913            .expect("test: roundtrip reverse order fragment should succeed");
914        frags.reverse();
915        let result = receive_all(&mut asm, frags, 0);
916        assert!(matches!(result, PfaReceiveResult::Assembled(d) if d == data));
917    }
918
919    // ── duplicate detection ───────────────────────────────────────────────────
920
921    #[test]
922    fn test_duplicate_fragment() {
923        let mut asm = make_assembler_cfg(10, 30, true);
924        let data = vec![0u8; 20];
925        let frags = asm
926            .fragment(1, &data)
927            .expect("test: duplicate fragment setup should succeed");
928        let dup = frags[0].clone();
929        asm.receive_fragment(frags[0].clone(), 0);
930        let result = asm.receive_fragment(dup, 0);
931        assert!(matches!(result, PfaReceiveResult::Duplicate));
932    }
933
934    #[test]
935    fn test_duplicate_last_fragment() {
936        // With 3 fragments, duplicate the second (non-last) after it is received
937        // but before the message is assembled.
938        let mut asm = make_assembler_cfg(10, 30, true);
939        let data = vec![0u8; 30];
940        let frags = asm
941            .fragment(1, &data)
942            .expect("test: duplicate last fragment setup should succeed");
943        assert_eq!(frags.len(), 3);
944        let dup = frags[1].clone();
945        // Receive fragments 0 and 1 – not yet complete.
946        asm.receive_fragment(frags[0].clone(), 0);
947        asm.receive_fragment(frags[1].clone(), 0);
948        // Send fragment 1 again before the message is assembled.
949        let result = asm.receive_fragment(dup, 0);
950        assert!(matches!(result, PfaReceiveResult::Duplicate));
951    }
952
953    // ── checksum errors ───────────────────────────────────────────────────────
954
955    #[test]
956    fn test_checksum_mismatch_is_error() {
957        let mut asm = make_assembler_cfg(100, 30, true);
958        let data = vec![1u8; 50];
959        let mut frags = asm
960            .fragment(1, &data)
961            .expect("test: checksum mismatch setup should succeed");
962        frags[0].checksum ^= 0xFFFF_FFFF; // corrupt
963        let result = asm.receive_fragment(frags.remove(0), 0);
964        assert!(matches!(result, PfaReceiveResult::Error(_)));
965    }
966
967    #[test]
968    fn test_checksum_disabled_ignores_bad_checksum() {
969        let mut asm = make_assembler_cfg(100, 30, false);
970        let data = vec![1u8; 50];
971        let mut frags = asm
972            .fragment(1, &data)
973            .expect("test: checksum disabled setup should succeed");
974        frags[0].checksum = 0xDEAD_BEEF; // won't be checked
975        let result = asm.receive_fragment(frags.remove(0), 0);
976        // Only one fragment total → assembled
977        assert!(matches!(result, PfaReceiveResult::Assembled(_)));
978    }
979
980    // ── out-of-range errors ───────────────────────────────────────────────────
981
982    #[test]
983    fn test_zero_total_fragments_error() {
984        let mut asm = make_assembler();
985        let bad = PfaFragment {
986            msg_id: 1,
987            fragment_index: 0,
988            total_fragments: 0,
989            offset: 0,
990            data: vec![],
991            checksum: 0,
992        };
993        let result = asm.receive_fragment(bad, 0);
994        assert!(matches!(result, PfaReceiveResult::Error(_)));
995    }
996
997    #[test]
998    fn test_index_gte_total_is_error() {
999        let mut asm = make_assembler();
1000        let bad = PfaFragment {
1001            msg_id: 2,
1002            fragment_index: 5,
1003            total_fragments: 3,
1004            offset: 0,
1005            data: vec![],
1006            checksum: 0,
1007        };
1008        let result = asm.receive_fragment(bad, 0);
1009        assert!(matches!(result, PfaReceiveResult::Error(_)));
1010    }
1011
1012    #[test]
1013    fn test_max_fragments_exceeded_error() {
1014        let mut asm = PacketFragmentationAssembler::new(PfaAssemblerConfig {
1015            mtu: 1,
1016            max_fragments: 2,
1017            reassembly_timeout_secs: 30,
1018            checksum_enabled: false,
1019        });
1020        let bad = PfaFragment {
1021            msg_id: 3,
1022            fragment_index: 0,
1023            total_fragments: 10,
1024            offset: 0,
1025            data: vec![0],
1026            checksum: 0,
1027        };
1028        let result = asm.receive_fragment(bad, 0);
1029        assert!(matches!(result, PfaReceiveResult::Error(_)));
1030    }
1031
1032    // ── reassemble() explicit call ────────────────────────────────────────────
1033
1034    #[test]
1035    fn test_reassemble_incomplete_returns_none() {
1036        let mut asm = make_assembler_cfg(10, 30, true);
1037        let data = vec![0u8; 20];
1038        let frags = asm
1039            .fragment(1, &data)
1040            .expect("test: reassemble incomplete setup should succeed");
1041        asm.receive_fragment(frags[0].clone(), 0);
1042        // Only one of two received.
1043        assert!(asm.reassemble(1).is_none());
1044    }
1045
1046    #[test]
1047    fn test_reassemble_unknown_msg_id_returns_none() {
1048        let mut asm = make_assembler();
1049        assert!(asm.reassemble(999_999).is_none());
1050    }
1051
1052    #[test]
1053    fn test_reassemble_after_all_received() {
1054        let mut asm = make_assembler_cfg(10, 30, true);
1055        let data: Vec<u8> = (0..30).collect();
1056        let frags = asm
1057            .fragment(1, &data)
1058            .expect("test: reassemble after all received setup should succeed");
1059        for f in &frags {
1060            asm.receive_fragment(f.clone(), 0);
1061        }
1062        // receive_fragment already consumed; but let's check reassemble on its own
1063        // (it triggers Assembled internally, so buffer is removed).
1064        // Test a fresh scenario instead.
1065        let mut asm2 = make_assembler_cfg(10, 30, true);
1066        let frags2 = asm2
1067            .fragment(2, &data)
1068            .expect("test: reassemble asm2 fragment should succeed");
1069        let last_idx = frags2.len() - 1;
1070        for (i, f) in frags2.into_iter().enumerate() {
1071            if i < last_idx {
1072                asm2.receive_fragment(f, 0);
1073            } else {
1074                // Don't receive last; call reassemble first
1075                assert!(asm2.reassemble(2).is_none());
1076                asm2.receive_fragment(f, 0);
1077            }
1078        }
1079        // After the last receive_fragment, reassemble is triggered internally.
1080        assert!(asm2.reassemble(2).is_none()); // already consumed
1081    }
1082
1083    // ── expire_stale ──────────────────────────────────────────────────────────
1084
1085    #[test]
1086    fn test_expire_stale_removes_old_buffers() {
1087        let mut asm = make_assembler_cfg(10, 10, true);
1088        let data = vec![0u8; 20];
1089        let frags = asm
1090            .fragment(1, &data)
1091            .expect("test: expire stale removes old buffers setup should succeed");
1092        asm.receive_fragment(frags[0].clone(), 0);
1093        assert_eq!(asm.pending_count(), 1);
1094        asm.expire_stale(100); // 100 >> 10 timeout
1095        assert_eq!(asm.pending_count(), 0);
1096    }
1097
1098    #[test]
1099    fn test_expire_stale_keeps_fresh_buffers() {
1100        let mut asm = make_assembler_cfg(10, 60, true);
1101        let data = vec![0u8; 20];
1102        let frags = asm
1103            .fragment(1, &data)
1104            .expect("test: expire stale keeps fresh buffers setup should succeed");
1105        asm.receive_fragment(frags[0].clone(), 50);
1106        asm.expire_stale(55); // only 5 secs old; timeout = 60
1107        assert_eq!(asm.pending_count(), 1);
1108    }
1109
1110    #[test]
1111    fn test_expire_stale_increments_dropped() {
1112        let mut asm = make_assembler_cfg(10, 5, true);
1113        let data = vec![0u8; 20];
1114        let frags = asm
1115            .fragment(1, &data)
1116            .expect("test: expire stale increments dropped setup should succeed");
1117        asm.receive_fragment(frags[0].clone(), 0);
1118        let before = asm.assembler_stats().total_dropped;
1119        asm.expire_stale(100);
1120        assert!(asm.assembler_stats().total_dropped > before);
1121    }
1122
1123    #[test]
1124    fn test_expire_stale_no_op_on_empty() {
1125        let mut asm = make_assembler();
1126        asm.expire_stale(999_999); // should not panic
1127        assert_eq!(asm.pending_count(), 0);
1128    }
1129
1130    // ── pending_messages ──────────────────────────────────────────────────────
1131
1132    #[test]
1133    fn test_pending_messages_empty() {
1134        let asm = make_assembler();
1135        assert!(asm.pending_messages().is_empty());
1136    }
1137
1138    #[test]
1139    fn test_pending_messages_one_entry() {
1140        let mut asm = make_assembler_cfg(10, 30, true);
1141        let data = vec![0u8; 25];
1142        let frags = asm
1143            .fragment(7, &data)
1144            .expect("test: pending messages one entry setup should succeed");
1145        asm.receive_fragment(frags[0].clone(), 0);
1146        let pending = asm.pending_messages();
1147        assert_eq!(pending.len(), 1);
1148        let (mid, recv, total) = pending[0];
1149        assert_eq!(mid, 7);
1150        assert_eq!(recv, 1);
1151        assert_eq!(total, 3);
1152    }
1153
1154    #[test]
1155    fn test_pending_messages_multiple() {
1156        let mut asm = make_assembler_cfg(10, 30, true);
1157        let data1 = vec![0u8; 20];
1158        let data2 = vec![1u8; 30];
1159        let frags1 = asm
1160            .fragment(10, &data1)
1161            .expect("test: pending messages multiple frags1 should succeed");
1162        let frags2 = asm
1163            .fragment(20, &data2)
1164            .expect("test: pending messages multiple frags2 should succeed");
1165        asm.receive_fragment(frags1[0].clone(), 0);
1166        asm.receive_fragment(frags2[0].clone(), 0);
1167        let pending = asm.pending_messages();
1168        assert_eq!(pending.len(), 2);
1169    }
1170
1171    // ── verify_checksum ───────────────────────────────────────────────────────
1172
1173    #[test]
1174    fn test_verify_checksum_valid() {
1175        let asm = make_assembler();
1176        let data = vec![1u8, 2, 3, 4];
1177        let frag = PfaFragment {
1178            msg_id: 1,
1179            fragment_index: 0,
1180            total_fragments: 1,
1181            offset: 0,
1182            checksum: fnv1a_32(&data),
1183            data,
1184        };
1185        assert!(asm.verify_checksum(&frag));
1186    }
1187
1188    #[test]
1189    fn test_verify_checksum_invalid() {
1190        let asm = make_assembler();
1191        let data = vec![1u8, 2, 3, 4];
1192        let frag = PfaFragment {
1193            msg_id: 1,
1194            fragment_index: 0,
1195            total_fragments: 1,
1196            offset: 0,
1197            checksum: 0xDEAD_BEEF,
1198            data,
1199        };
1200        assert!(!asm.verify_checksum(&frag));
1201    }
1202
1203    #[test]
1204    fn test_verify_checksum_disabled() {
1205        let asm = make_assembler_cfg(100, 30, false);
1206        let frag = PfaFragment {
1207            msg_id: 1,
1208            fragment_index: 0,
1209            total_fragments: 1,
1210            offset: 0,
1211            checksum: 0xDEAD_BEEF, // wrong; should be ignored
1212            data: vec![42u8],
1213        };
1214        assert!(asm.verify_checksum(&frag));
1215    }
1216
1217    // ── assembler_stats ───────────────────────────────────────────────────────
1218
1219    #[test]
1220    fn test_stats_after_full_roundtrip() {
1221        let mut asm = make_assembler_cfg(10, 30, true);
1222        let data = vec![0u8; 30];
1223        let frags = asm
1224            .fragment(1, &data)
1225            .expect("test: stats after full roundtrip setup should succeed");
1226        receive_all(&mut asm, frags, 0);
1227        let stats = asm.assembler_stats();
1228        assert_eq!(stats.total_fragmented, 1);
1229        assert_eq!(stats.total_assembled, 1);
1230        assert_eq!(stats.total_dropped, 0);
1231        assert!((stats.avg_fragments - 3.0).abs() < f64::EPSILON);
1232    }
1233
1234    #[test]
1235    fn test_stats_dropped_on_checksum_fail() {
1236        let mut asm = make_assembler_cfg(100, 30, true);
1237        let data = vec![1u8; 50];
1238        let mut frags = asm
1239            .fragment(1, &data)
1240            .expect("test: stats dropped on checksum fail setup should succeed");
1241        frags[0].checksum ^= 0xFF;
1242        asm.receive_fragment(frags.remove(0), 0);
1243        let stats = asm.assembler_stats();
1244        assert_eq!(stats.total_dropped, 1);
1245    }
1246
1247    #[test]
1248    fn test_stats_avg_across_multiple() {
1249        let mut asm = make_assembler_cfg(10, 30, true);
1250        asm.fragment(1, &[0u8; 10])
1251            .expect("test: stats avg fragment 1 should succeed"); // 1 fragment
1252        asm.fragment(2, &[0u8; 20])
1253            .expect("test: stats avg fragment 2 should succeed"); // 2 fragments
1254        asm.fragment(3, &[0u8; 30])
1255            .expect("test: stats avg fragment 3 should succeed"); // 3 fragments
1256        let stats = asm.assembler_stats();
1257        assert_eq!(stats.total_fragmented, 3);
1258        // avg = (1+2+3)/3 = 2.0
1259        assert!((stats.avg_fragments - 2.0).abs() < f64::EPSILON);
1260    }
1261
1262    // ── fragment log ─────────────────────────────────────────────────────────
1263
1264    #[test]
1265    fn test_log_grows_on_receive() {
1266        let mut asm = make_assembler_cfg(10, 30, true);
1267        let data = vec![0u8; 30];
1268        let frags = asm
1269            .fragment(1, &data)
1270            .expect("test: log grows on receive setup should succeed");
1271        for f in frags {
1272            asm.receive_fragment(f, 0);
1273        }
1274        assert!(!asm.fragment_log().is_empty());
1275    }
1276
1277    #[test]
1278    fn test_log_capped_at_1000() {
1279        let mut asm = make_assembler_cfg(1, 30, false);
1280        // Send 1_100 single-byte single-fragment messages.
1281        for id in 1u64..=1_100 {
1282            let frags = asm
1283                .fragment(id, &[0u8])
1284                .expect("test: log capped at 1000 fragment should succeed");
1285            asm.receive_fragment(
1286                frags
1287                    .into_iter()
1288                    .next()
1289                    .expect("test: log capped at 1000 first fragment should exist"),
1290                0,
1291            );
1292        }
1293        assert_eq!(asm.fragment_log().len(), 1_000);
1294    }
1295
1296    #[test]
1297    fn test_log_drain_empties() {
1298        let mut asm = make_assembler_cfg(1, 30, false);
1299        let frags = asm
1300            .fragment(1, &[42u8])
1301            .expect("test: log drain empties fragment should succeed");
1302        asm.receive_fragment(
1303            frags
1304                .into_iter()
1305                .next()
1306                .expect("test: log drain empties first fragment should exist"),
1307            0,
1308        );
1309        let drained = asm.drain_log();
1310        assert!(!drained.is_empty());
1311        assert!(asm.fragment_log().is_empty());
1312    }
1313
1314    #[test]
1315    fn test_log_assembled_flag() {
1316        let mut asm = make_assembler_cfg(100, 30, true);
1317        let data = vec![0u8; 50];
1318        let frags = asm
1319            .fragment(1, &data)
1320            .expect("test: log assembled flag setup should succeed");
1321        receive_all(&mut asm, frags, 0);
1322        let log: Vec<_> = asm.fragment_log().iter().cloned().collect();
1323        let assembled_entry = log.iter().find(|r| r.assembled);
1324        assert!(assembled_entry.is_some());
1325    }
1326
1327    #[test]
1328    fn test_log_dup_flag() {
1329        // Use a 3-fragment message so we can duplicate fragment 0 before assembly.
1330        let mut asm = make_assembler_cfg(10, 30, true);
1331        let data = vec![0u8; 30];
1332        let frags = asm
1333            .fragment(1, &data)
1334            .expect("test: log dup flag setup should succeed");
1335        assert_eq!(frags.len(), 3);
1336        let dup = frags[0].clone();
1337        // Receive fragments 0 and 1 (not yet assembled).
1338        asm.receive_fragment(frags[0].clone(), 0);
1339        asm.receive_fragment(frags[1].clone(), 0);
1340        // Duplicate fragment 0.
1341        asm.receive_fragment(dup, 0);
1342        let log: Vec<_> = asm.fragment_log().iter().cloned().collect();
1343        let dup_entry = log.iter().find(|r| r.is_dup);
1344        assert!(dup_entry.is_some());
1345    }
1346
1347    // ── reset ─────────────────────────────────────────────────────────────────
1348
1349    #[test]
1350    fn test_reset_clears_all() {
1351        let mut asm = make_assembler_cfg(10, 30, true);
1352        let data = vec![0u8; 20];
1353        let frags = asm
1354            .fragment(1, &data)
1355            .expect("test: reset clears all setup should succeed");
1356        asm.receive_fragment(frags[0].clone(), 0);
1357        asm.reset();
1358        assert_eq!(asm.pending_count(), 0);
1359        assert!(asm.fragment_log().is_empty());
1360        let stats = asm.assembler_stats();
1361        assert_eq!(stats.total_fragmented, 0);
1362        assert_eq!(stats.total_assembled, 0);
1363    }
1364
1365    // ── get_buffer ────────────────────────────────────────────────────────────
1366
1367    #[test]
1368    fn test_get_buffer_present() {
1369        let mut asm = make_assembler_cfg(10, 30, true);
1370        let data = vec![0u8; 20];
1371        let frags = asm
1372            .fragment(1, &data)
1373            .expect("test: get buffer present setup should succeed");
1374        asm.receive_fragment(frags[0].clone(), 5);
1375        let buf = asm
1376            .get_buffer(1)
1377            .expect("test: get buffer present should return buffer");
1378        assert_eq!(buf.msg_id, 1);
1379        assert_eq!(buf.created_at, 5);
1380        assert_eq!(buf.received_count(), 1);
1381    }
1382
1383    #[test]
1384    fn test_get_buffer_absent() {
1385        let asm = make_assembler();
1386        assert!(asm.get_buffer(42).is_none());
1387    }
1388
1389    // ── PfaReassemblyBuffer helpers ───────────────────────────────────────────
1390
1391    #[test]
1392    fn test_reassembly_buffer_not_complete_initially() {
1393        let buf = PfaReassemblyBuffer::new(1, 3, 0);
1394        assert!(!buf.is_complete());
1395        assert_eq!(buf.received_count(), 0);
1396    }
1397
1398    #[test]
1399    fn test_total_fragments_mismatch_error() {
1400        let mut asm = make_assembler_cfg(10, 30, false);
1401        // First fragment claims total=2
1402        let f1 = PfaFragment {
1403            msg_id: 99,
1404            fragment_index: 0,
1405            total_fragments: 2,
1406            offset: 0,
1407            data: vec![0u8; 5],
1408            checksum: 0,
1409        };
1410        // Second fragment claims total=3 (mismatch)
1411        let f2 = PfaFragment {
1412            msg_id: 99,
1413            fragment_index: 1,
1414            total_fragments: 3,
1415            offset: 5,
1416            data: vec![0u8; 5],
1417            checksum: 0,
1418        };
1419        asm.receive_fragment(f1, 0);
1420        let result = asm.receive_fragment(f2, 0);
1421        assert!(matches!(result, PfaReceiveResult::Error(_)));
1422    }
1423
1424    // ── Multi-message concurrency (sequential) ────────────────────────────────
1425
1426    #[test]
1427    fn test_multiple_messages_in_flight() {
1428        let mut asm = make_assembler_cfg(8, 60, true);
1429        let payload_a: Vec<u8> = (0..24u8).collect();
1430        let payload_b: Vec<u8> = (0..16u8).map(|x| x * 2).collect();
1431
1432        let frags_a = asm
1433            .fragment(100, &payload_a)
1434            .expect("test: multiple messages in flight frags_a should succeed"); // 3 frags
1435        let frags_b = asm
1436            .fragment(200, &payload_b)
1437            .expect("test: multiple messages in flight frags_b should succeed"); // 2 frags
1438
1439        // Interleave receipt
1440        asm.receive_fragment(frags_a[0].clone(), 0);
1441        asm.receive_fragment(frags_b[0].clone(), 0);
1442        asm.receive_fragment(frags_a[1].clone(), 0);
1443        asm.receive_fragment(frags_b[1].clone(), 0);
1444        let result_a = asm.receive_fragment(frags_a[2].clone(), 0);
1445
1446        assert_eq!(asm.pending_count(), 0); // both assembled
1447        assert!(matches!(result_a, PfaReceiveResult::Assembled(d) if d == payload_a));
1448    }
1449
1450    #[test]
1451    fn test_stats_type_alias() {
1452        let stats: PfaAssemblerStats = PfaAssemblerStats::default();
1453        assert_eq!(stats.total_fragmented, 0);
1454    }
1455
1456    #[test]
1457    fn test_type_alias_packet_fragmentation_assembler() {
1458        let _asm: PfaPacketFragmentationAssembler = PacketFragmentationAssembler::with_defaults();
1459    }
1460}