Skip to main content

phantom_protocol/transport/
sack.rs

1//! Selective-acknowledgement (SACK) range codec.
2//!
3//! # Purpose
4//!
5//! Replaces the legacy 4-byte single-sequence ACK payload inside the
6//! `ENCRYPTED | ACK` [`crate::transport::PhantomPacket`] control frame with a
7//! compact multi-range encoding that lets the sender retire many segments in a
8//! single ACK and detect gaps for fast-retransmit.
9//!
10//! This is the **AEAD plaintext** of that control frame — it is NOT the outer
11//! frozen `PhantomPacket` container. Changing this format does NOT require a
12//! `WIRE_VERSION` or `PROTOCOL_VERSION` bump and does NOT invalidate
13//! `core/tests/wire_vectors`.
14//!
15//! # Wire format
16//!
17//! All integers are big-endian (network byte order), matching the rest of the
18//! Phantom Protocol codec (`PacketHeader::to_wire`, etc.).
19//!
20//! ```text
21//! off  0  largest_acked  : u32 be   — the highest sequence number ACKed
22//! off  4  ack_delay_us   : u32 be   — sender-measured ACK delay in µs
23//! off  8  range_count    : u16 be   — number of SACK ranges; ≥ 1, ≤ MAX_SACK_RANGES
24//! off 10  first_len      : u32 be   — length-minus-one of the first (highest) range
25//! off 14  [gap : u32 be, len : u32 be] × (range_count − 1)
26//! ```
27//!
28//! ## Range encoding
29//!
30//! Ranges are stored **descending** (highest first).  The first range is:
31//!
32//! ```text
33//! high = largest_acked
34//! low  = largest_acked − first_len          (inclusive; first_len is length − 1)
35//! ```
36//!
37//! Each subsequent `(gap, len)` pair descends below the previous range's low:
38//!
39//! ```text
40//! high_i = low_{i-1} − 1 − gap             (skip `gap` unACKed sequences)
41//! low_i  = high_i − len                    (len is length − 1)
42//! ```
43//!
44//! The "length − 1" convention means `len == 0` encodes a single-sequence range
45//! (one ACKed packet).  `gap` = number of unACKed sequences between a range's
46//! low and the next (lower) range's high; `gap` is always ≥ 1 (adjacent ranges
47//! are coalesced by the sender and rejected as `Malformed` on decode).
48//!
49//! ## Minimum wire size
50//!
51//! | ranges | bytes |
52//! |--------|-------|
53//! | 1      | 14    |
54//! | 2      | 22    |
55//! | N      | 10 + 4 + 8×(N−1) |
56//!
57//! ## Decode error conditions
58//!
59//! | Condition                        | Error            |
60//! |----------------------------------|------------------|
61//! | Buffer shorter than declared     | `Truncated`      |
62//! | `range_count > MAX_SACK_RANGES`  | `TooManyRanges`  |
63//! | `range_count == 0`               | `Malformed`      |
64//! | `gap == 0` (adjacent ranges)     | `Malformed`      |
65//! | gap/len underflows below 0       | `Malformed`      |
66//! | resulting range overlaps / is adjacent to previous | `Malformed` |
67
68use std::fmt;
69
70/// Maximum number of SACK ranges accepted during decode — anti-DoS bound.
71/// A 32-range SACK fits in 262 bytes, well within any AEAD budget.
72pub const MAX_SACK_RANGES: usize = 32;
73
74/// Minimum wire size for a 1-range SACK (10 fixed + 4 first_len).
75const MIN_WIRE_LEN: usize = 14;
76
77/// Fixed header before the variable-length range array.
78const FIXED_HDR_LEN: usize = 10; // largest_acked(4) + ack_delay_us(4) + range_count(2)
79
80/// Per-range continuation bytes after the first range.
81const CONTINUATION_BYTES: usize = 8; // gap(4) + len(4)
82
83/// A selective acknowledgement over a per-stream `u32` sequence space.
84///
85/// `ranges` are **inclusive `(low, high)`** runs of acknowledged sequences,
86/// sorted **descending** (highest first), non-overlapping and non-adjacent.
87/// `ranges[0].1 == largest_acked`.  An empty `ranges` slice is invalid — a
88/// [`Sack`] always ACKs at least one sequence.
89#[derive(Clone, Debug, PartialEq, Eq)]
90pub struct Sack {
91    /// The highest sequence number covered by this ACK.
92    pub largest_acked: u32,
93    /// Sender-measured delay between receiving the data packet and generating
94    /// this ACK, in microseconds.  Used by the BBR / RTT estimator.
95    pub ack_delay_us: u32,
96    /// Inclusive `(low, high)` ranges, sorted descending (highest first),
97    /// non-overlapping and non-adjacent.  Always non-empty.
98    ranges: Vec<(u32, u32)>,
99}
100
101/// Errors returned by [`Sack::from_wire`].
102#[derive(Clone, Copy, PartialEq, Eq, Debug)]
103pub enum SackError {
104    /// The buffer is shorter than the structure it declares.
105    Truncated,
106    /// `range_count` exceeds [`MAX_SACK_RANGES`].  Checked before any
107    /// allocation so an adversarial count cannot trigger OOM.
108    TooManyRanges,
109    /// The encoding is structurally invalid: `range_count == 0`, `gap == 0`
110    /// (adjacent ranges), a gap/len pair would underflow the sequence space,
111    /// or ranges are not strictly descending and non-adjacent.
112    Malformed,
113}
114
115impl fmt::Display for SackError {
116    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117        match self {
118            Self::Truncated => write!(f, "SACK payload too short for the declared range count"),
119            Self::TooManyRanges => write!(
120                f,
121                "SACK range_count exceeds the anti-DoS limit of {MAX_SACK_RANGES}"
122            ),
123            Self::Malformed => write!(
124                f,
125                "SACK encoding is malformed (zero ranges, adjacent ranges, underflow, or overlapping ranges)"
126            ),
127        }
128    }
129}
130
131impl std::error::Error for SackError {}
132
133impl Sack {
134    /// Build a [`Sack`] from an unordered slice of received sequence numbers.
135    ///
136    /// Coalesces adjacent or overlapping sequences into the minimal set of
137    /// inclusive `(low, high)` ranges sorted descending (highest first).
138    ///
139    /// Returns `None` if `received` is empty (there is nothing to ACK).
140    pub fn from_received(received: &[u32], ack_delay_us: u32) -> Option<Sack> {
141        if received.is_empty() {
142            return None;
143        }
144
145        // Sort ascending so we can coalesce in a single pass.
146        let mut seqs: Vec<u32> = received.to_vec();
147        seqs.sort_unstable();
148
149        // Coalesce into ascending (low, high) ranges.
150        let mut asc_ranges: Vec<(u32, u32)> = Vec::new();
151        for seq in seqs {
152            match asc_ranges.last_mut() {
153                Some(last) if seq <= last.1.saturating_add(1) => {
154                    // Extend or merge: handles both adjacent (seq == last.1+1)
155                    // and duplicate (seq <= last.1) entries.
156                    if seq > last.1 {
157                        last.1 = seq;
158                    }
159                }
160                _ => asc_ranges.push((seq, seq)),
161            }
162        }
163
164        Self::from_ascending_coalesced(asc_ranges, ack_delay_us)
165    }
166
167    /// Build a [`Sack`] from explicit inclusive `(low, high)` ranges in any order
168    /// (each must have `low <= high`). Ranges are sorted ascending, coalesced
169    /// (adjacent/overlapping merged), reversed to descending, and **capped to the
170    /// highest [`MAX_SACK_RANGES`]** so the encoded SACK always decodes at the peer
171    /// (`from_wire` rejects `range_count > MAX_SACK_RANGES`). Dropping the lowest
172    /// ranges is safe: those sequences are recovered by cumulative re-ACK as holes
173    /// fill, or by RTO. Returns `None` if `ranges` is empty.
174    pub fn from_inclusive_ranges(mut ranges: Vec<(u32, u32)>, ack_delay_us: u32) -> Option<Sack> {
175        if ranges.is_empty() {
176            return None;
177        }
178        ranges.sort_unstable_by_key(|&(lo, _)| lo);
179        let mut asc: Vec<(u32, u32)> = Vec::with_capacity(ranges.len());
180        for (lo, hi) in ranges {
181            match asc.last_mut() {
182                // Adjacent or overlapping with the previous (ascending) range → merge.
183                Some(last) if lo <= last.1.saturating_add(1) => {
184                    if hi > last.1 {
185                        last.1 = hi;
186                    }
187                }
188                _ => asc.push((lo, hi)),
189            }
190        }
191        Self::from_ascending_coalesced(asc, ack_delay_us)
192    }
193
194    /// Shared tail for [`from_received`] / [`from_inclusive_ranges`]: take ascending,
195    /// already-coalesced ranges, reverse to descending, **cap to the highest
196    /// [`MAX_SACK_RANGES`]** (drop the lowest, oldest ranges so the wire form always
197    /// decodes at the peer), set `largest_acked`, and construct. `None` if empty.
198    fn from_ascending_coalesced(
199        mut asc_ranges: Vec<(u32, u32)>,
200        ack_delay_us: u32,
201    ) -> Option<Sack> {
202        if asc_ranges.is_empty() {
203            return None;
204        }
205        // Reverse to descending order (highest first), then keep the highest ranges.
206        asc_ranges.reverse();
207        asc_ranges.truncate(MAX_SACK_RANGES);
208        let largest_acked = asc_ranges[0].1;
209
210        Some(Sack {
211            largest_acked,
212            ack_delay_us,
213            ranges: asc_ranges,
214        })
215    }
216
217    /// Returns the inclusive `(low, high)` ranges, sorted descending (highest
218    /// first), non-overlapping and non-adjacent.  Always non-empty.
219    pub fn ranges(&self) -> &[(u32, u32)] {
220        &self.ranges
221    }
222
223    /// Serialise to wire bytes.
224    ///
225    /// Panics are structurally impossible: `ranges` is always non-empty (the
226    /// invariant is enforced by the private field — every constructor
227    /// (`from_received`, `from_inclusive_ranges`, `from_wire`) guarantees at
228    /// least one range), and the arithmetic cannot overflow because all fields
229    /// fit in `u32`.
230    pub fn to_wire(&self) -> Vec<u8> {
231        let range_count = self.ranges.len();
232        // 10 bytes fixed header + 4 bytes first_len + 8 bytes per continuation.
233        let capacity = FIXED_HDR_LEN + 4 + CONTINUATION_BYTES * range_count.saturating_sub(1);
234        let mut buf = Vec::with_capacity(capacity);
235
236        buf.extend_from_slice(&self.largest_acked.to_be_bytes());
237        buf.extend_from_slice(&self.ack_delay_us.to_be_bytes());
238        // PANIC-SAFETY: range_count is bounded by the caller; values from
239        // `from_received` are bounded by `received.len()` which is a `usize`
240        // but in practice never close to u16::MAX.  Values from a decoded and
241        // re-encoded Sack are already ≤ MAX_SACK_RANGES (32). We cast
242        // defensively with `min` so an edge-case huge Vec produces a saturated
243        // count rather than truncation silently dropping ranges.
244        #[allow(clippy::cast_possible_truncation)]
245        let range_count_wire = (range_count.min(u16::MAX as usize)) as u16;
246        buf.extend_from_slice(&range_count_wire.to_be_bytes());
247
248        // First range: [largest_acked - first_len, largest_acked].
249        // `first_len` = high - low = range_width - 1.
250        // PANIC-SAFETY: `ranges` is always non-empty — the field is private and
251        // only populated by `from_received` / `from_inclusive_ranges` (both
252        // require a non-empty result) and `from_wire` (which rejects
253        // range_count == 0).  This index cannot panic.
254        let (first_low, first_high) = self.ranges[0];
255        // PANIC-SAFETY: `from_received` / `from_inclusive_ranges` guarantee
256        // low ≤ high because ranges are built from sorted, coalesced sequences;
257        // `from_wire` validates the same invariant before storing.  The
258        // subtraction cannot underflow.
259        let first_len: u32 = first_high - first_low;
260        buf.extend_from_slice(&first_len.to_be_bytes());
261
262        // Continuation ranges: (gap, len) pairs.
263        let mut prev_low = first_low;
264        for &(low, high) in &self.ranges[1..] {
265            // gap = number of unACKed sequences between prev range and this one.
266            // prev_low - 1 is the sequence just below the previous range.
267            // high      is the top of the current range.
268            // gap = (prev_low - 1) - high   (both sides of the unACKed gap)
269            // Since ranges are strictly descending with at least one gap seq,
270            // prev_low >= high + 2 must hold (invariant checked on decode;
271            // maintained by from_received which merges adjacent ranges).
272            let gap: u32 = prev_low.saturating_sub(1).saturating_sub(high);
273            buf.extend_from_slice(&gap.to_be_bytes());
274
275            let len: u32 = high - low;
276            buf.extend_from_slice(&len.to_be_bytes());
277
278            prev_low = low;
279        }
280
281        buf
282    }
283
284    /// Decode a [`Sack`] from wire bytes produced by [`Sack::to_wire`].
285    ///
286    /// # Errors
287    ///
288    /// See [`SackError`] for the full error table.
289    pub fn from_wire(buf: &[u8]) -> Result<Sack, SackError> {
290        // Need at least the fixed header + first_len field.
291        if buf.len() < MIN_WIRE_LEN {
292            return Err(SackError::Truncated);
293        }
294
295        let largest_acked = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]);
296        let ack_delay_us = u32::from_be_bytes([buf[4], buf[5], buf[6], buf[7]]);
297        let range_count = u16::from_be_bytes([buf[8], buf[9]]) as usize;
298
299        // Anti-DoS check BEFORE any allocation.
300        if range_count == 0 {
301            return Err(SackError::Malformed);
302        }
303        if range_count > MAX_SACK_RANGES {
304            return Err(SackError::TooManyRanges);
305        }
306
307        // Check that the buffer is long enough for all declared ranges.
308        // Wire size = 10 (fixed) + 4 (first_len) + 8*(range_count-1) (continuations)
309        let needed = FIXED_HDR_LEN
310            .checked_add(4)
311            .and_then(|n| n.checked_add(CONTINUATION_BYTES * (range_count - 1)))
312            .ok_or(SackError::Truncated)?;
313        if buf.len() < needed {
314            return Err(SackError::Truncated);
315        }
316
317        // Decode the first range.
318        let first_len = u32::from_be_bytes([buf[10], buf[11], buf[12], buf[13]]);
319        let first_high = largest_acked;
320        let first_low = largest_acked
321            .checked_sub(first_len)
322            .ok_or(SackError::Malformed)?;
323
324        let mut ranges: Vec<(u32, u32)> = Vec::with_capacity(range_count);
325        ranges.push((first_low, first_high));
326
327        // Decode continuation ranges.
328        let mut prev_low = first_low;
329        let mut pos = 14usize; // bytes consumed so far
330        for _ in 1..range_count {
331            let gap = u32::from_be_bytes([buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3]]);
332            let len = u32::from_be_bytes([buf[pos + 4], buf[pos + 5], buf[pos + 6], buf[pos + 7]]);
333            pos += 8;
334
335            // Fix 2: adjacent ranges (gap == 0) are invalid — the sender must
336            // coalesce them.  Reject rather than silently accepting malformed
337            // input.
338            if gap == 0 {
339                return Err(SackError::Malformed);
340            }
341
342            // high_i = prev_low - 1 - gap
343            // prev_low must be ≥ gap + 1 so we don't wrap below 0.
344            let below_prev = prev_low.checked_sub(1).ok_or(SackError::Malformed)?;
345            let high = below_prev.checked_sub(gap).ok_or(SackError::Malformed)?;
346            let low = high.checked_sub(len).ok_or(SackError::Malformed)?;
347
348            // Ranges must be strictly descending and non-adjacent; `high` must
349            // be strictly less than `prev_low - 1` (gap ≥ 1 enforced above).
350            // The arithmetic already ensures high < prev_low.  We additionally
351            // verify that low ≤ high (no inverted range).
352            if low > high {
353                return Err(SackError::Malformed);
354            }
355
356            ranges.push((low, high));
357            prev_low = low;
358        }
359
360        Ok(Sack {
361            largest_acked,
362            ack_delay_us,
363            ranges,
364        })
365    }
366
367    /// Returns `true` if `seq` is covered by any range in this SACK.
368    pub fn acks(&self, seq: u32) -> bool {
369        for &(low, high) in &self.ranges {
370            if seq >= low && seq <= high {
371                return true;
372            }
373        }
374        false
375    }
376}
377
378#[cfg(test)]
379#[allow(clippy::unwrap_used)]
380mod tests {
381    use super::*;
382
383    // ── from_received coalescing ──────────────────────────────────────────
384
385    #[test]
386    fn coalesces_to_descending_ranges() {
387        // Input: [5,6,7,10,11,3] → ranges [(10,11),(5,7),(3,3)], largest 11
388        let sack = Sack::from_received(&[5, 6, 7, 10, 11, 3], 0).unwrap();
389        assert_eq!(sack.largest_acked, 11);
390        assert_eq!(sack.ranges(), &[(10, 11), (5, 7), (3, 3)]);
391    }
392
393    #[test]
394    fn from_received_empty_returns_none() {
395        assert!(Sack::from_received(&[], 42).is_none());
396    }
397
398    #[test]
399    fn from_received_single_seq() {
400        let sack = Sack::from_received(&[7], 100).unwrap();
401        assert_eq!(sack.largest_acked, 7);
402        assert_eq!(sack.ranges(), &[(7, 7)]);
403    }
404
405    #[test]
406    fn from_received_contiguous() {
407        // 0..=9 should produce a single range (0, 9)
408        let input: Vec<u32> = (0..=9).collect();
409        let sack = Sack::from_received(&input, 0).unwrap();
410        assert_eq!(sack.largest_acked, 9);
411        assert_eq!(sack.ranges(), &[(0, 9)]);
412    }
413
414    #[test]
415    fn from_received_duplicate_seqs_coalesced() {
416        let sack = Sack::from_received(&[3, 3, 3, 5, 5], 0).unwrap();
417        assert_eq!(sack.ranges(), &[(5, 5), (3, 3)]);
418    }
419
420    // ── F1: generation must cap to MAX_SACK_RANGES so the peer can decode ──
421
422    #[test]
423    fn from_received_caps_ranges_to_max_and_stays_decodable() {
424        // 40 disjoint singleton islands (even sequences 0,2,..,78) → 40 ranges,
425        // which a peer would reject as TooManyRanges. Generation must cap to 32,
426        // keep the HIGHEST ranges (nearest largest_acked), and remain decodable.
427        let seqs: Vec<u32> = (0u32..40).map(|i| i * 2).collect();
428        let sack = Sack::from_received(&seqs, 0).expect("non-empty");
429        assert!(
430            sack.ranges().len() <= MAX_SACK_RANGES,
431            "generated SACK must be capped to MAX_SACK_RANGES, got {}",
432            sack.ranges().len()
433        );
434        // Kept the highest 32 ranges: (78,78) down to (16,16); largest unchanged.
435        assert_eq!(sack.largest_acked, 78);
436        assert_eq!(sack.ranges().len(), MAX_SACK_RANGES);
437        assert_eq!(sack.ranges()[0], (78, 78));
438        assert_eq!(sack.ranges()[MAX_SACK_RANGES - 1], (16, 16));
439        let wire = sack.to_wire();
440        let decoded = Sack::from_wire(&wire).expect("a capped SACK must decode at the peer");
441        assert_eq!(decoded, sack);
442    }
443
444    #[test]
445    fn from_inclusive_ranges_caps_and_keeps_highest() {
446        // Ascending (low,high) input with 40 islands; keep the highest 32.
447        let asc: Vec<(u32, u32)> = (0u32..40).map(|i| (i * 2, i * 2)).collect();
448        let sack = Sack::from_inclusive_ranges(asc, 7).expect("non-empty");
449        assert_eq!(sack.ack_delay_us, 7);
450        assert_eq!(sack.ranges().len(), MAX_SACK_RANGES);
451        assert_eq!(sack.largest_acked, 78);
452        assert_eq!(sack.ranges()[0], (78, 78));
453        // Decodes at the peer.
454        assert_eq!(Sack::from_wire(&sack.to_wire()).expect("decode"), sack);
455    }
456
457    #[test]
458    fn from_inclusive_ranges_coalesces_and_orders() {
459        // Unsorted, with an adjacent pair (4,5)+(6,7) that must coalesce to (4,7).
460        let sack = Sack::from_inclusive_ranges(vec![(6, 7), (0, 1), (4, 5)], 0).expect("non-empty");
461        assert_eq!(sack.ranges(), &[(4, 7), (0, 1)]);
462        assert_eq!(sack.largest_acked, 7);
463        assert_eq!(Sack::from_wire(&sack.to_wire()).expect("decode"), sack);
464    }
465
466    #[test]
467    fn from_inclusive_ranges_empty_is_none() {
468        assert!(Sack::from_inclusive_ranges(Vec::new(), 0).is_none());
469    }
470
471    // ── round-trip tests ─────────────────────────────────────────────────
472
473    #[test]
474    fn roundtrip_multi_gap() {
475        // Three disjoint ranges: (20,25), (10,13), (1,3)
476        let sack =
477            Sack::from_received(&[20, 21, 22, 23, 24, 25, 10, 11, 12, 13, 1, 2, 3], 1234).unwrap();
478        assert_eq!(sack.ranges(), &[(20, 25), (10, 13), (1, 3)]);
479        let wire = sack.to_wire();
480        let decoded = Sack::from_wire(&wire).expect("should decode");
481        assert_eq!(decoded, sack);
482    }
483
484    #[test]
485    fn roundtrip_single_contiguous() {
486        let sack = Sack::from_received(&(0u32..=9).collect::<Vec<_>>(), 0).unwrap();
487        assert_eq!(sack.ranges(), &[(0, 9)]);
488        let wire = sack.to_wire();
489        let decoded = Sack::from_wire(&wire).expect("roundtrip single");
490        assert_eq!(decoded, sack);
491    }
492
493    #[test]
494    fn roundtrip_degenerate_single_seq() {
495        let sack = Sack::from_received(&[42], 999).unwrap();
496        assert_eq!(sack.largest_acked, 42);
497        let wire = sack.to_wire();
498        let decoded = Sack::from_wire(&wire).expect("roundtrip single seq");
499        assert_eq!(decoded, sack);
500    }
501
502    #[test]
503    fn roundtrip_from_received_coalesced() {
504        let input = [5u32, 6, 7, 10, 11, 3];
505        let sack = Sack::from_received(&input, 500).unwrap();
506        let wire = sack.to_wire();
507        let decoded = Sack::from_wire(&wire).expect("from_received roundtrip");
508        assert_eq!(decoded, sack);
509    }
510
511    #[test]
512    fn roundtrip_preserves_ack_delay() {
513        let sack = Sack::from_received(&[1, 2, 3], 0xDEAD_BEEF).unwrap();
514        let wire = sack.to_wire();
515        let decoded = Sack::from_wire(&wire).expect("roundtrip ack_delay");
516        assert_eq!(decoded.ack_delay_us, 0xDEAD_BEEF);
517    }
518
519    // ── Fix 1: large span round-trip (was silently truncated with u16) ────
520
521    #[test]
522    fn roundtrip_large_span_exceeds_u16() {
523        // first_len = 100_000 − 0 = 100_000, which overflows u16::MAX (65535).
524        // This is the exact case that was silently corrupted before the fix.
525        let seqs: Vec<u32> = (0u32..=100_000).collect();
526        let sack = Sack::from_received(&seqs, 0).unwrap();
527        assert_eq!(sack.largest_acked, 100_000);
528        assert_eq!(sack.ranges(), &[(0, 100_000)]);
529        let wire = sack.to_wire();
530        let decoded = Sack::from_wire(&wire).expect("large span must round-trip");
531        assert_eq!(decoded, sack);
532        // Verify first_len on wire (bytes 10..14) is 100_000 in big-endian u32.
533        let first_len_wire = u32::from_be_bytes([wire[10], wire[11], wire[12], wire[13]]);
534        assert_eq!(first_len_wire, 100_000u32);
535    }
536
537    #[test]
538    fn roundtrip_large_gap_exceeds_u16() {
539        // Two ranges: (200_000, 200_000) and (0, 0).
540        // gap = 200_000 - 1 - 0 = 199_999, which overflows u16::MAX.
541        let sack = Sack::from_received(&[200_000u32, 0], 0).unwrap();
542        assert_eq!(sack.ranges(), &[(200_000, 200_000), (0, 0)]);
543        let wire = sack.to_wire();
544        let decoded = Sack::from_wire(&wire).expect("large gap must round-trip");
545        assert_eq!(decoded, sack);
546        // Verify gap on wire (bytes 14..18) is 199_999 in big-endian u32.
547        // Layout: largest_acked(4) + ack_delay_us(4) + range_count(2) + first_len(4) = 14
548        // then gap(4) starting at byte 14.
549        let gap_wire = u32::from_be_bytes([wire[14], wire[15], wire[16], wire[17]]);
550        assert_eq!(gap_wire, 199_999u32);
551    }
552
553    // ── Fix 2: gap == 0 must be rejected as Malformed ────────────────────
554
555    #[test]
556    fn decode_gap_zero_is_malformed() {
557        // Craft a 2-range SACK where gap == 0 (adjacent ranges).
558        // Wire layout (u32 fields):
559        //   largest_acked=10, ack_delay=0, range_count=2,
560        //   first_len=0  (range (10,10)),
561        //   gap=0, len=0 (adjacent range (9,9) — invalid)
562        let mut buf = vec![0u8; 22]; // 10 + 4 + 8 = 22 bytes for 2 ranges
563        buf[0..4].copy_from_slice(&10u32.to_be_bytes()); // largest_acked
564        buf[4..8].copy_from_slice(&0u32.to_be_bytes()); // ack_delay_us
565        buf[8..10].copy_from_slice(&2u16.to_be_bytes()); // range_count = 2
566        buf[10..14].copy_from_slice(&0u32.to_be_bytes()); // first_len = 0
567        buf[14..18].copy_from_slice(&0u32.to_be_bytes()); // gap = 0  ← invalid
568        buf[18..22].copy_from_slice(&0u32.to_be_bytes()); // len = 0
569        assert!(
570            matches!(Sack::from_wire(&buf), Err(SackError::Malformed)),
571            "gap == 0 must be rejected as Malformed"
572        );
573    }
574
575    // ── acks() helper ────────────────────────────────────────────────────
576
577    #[test]
578    fn acks_correctness_across_gaps() {
579        // Ranges: (10,11), (5,7), (3,3)
580        let sack = Sack::from_received(&[5, 6, 7, 10, 11, 3], 0).unwrap();
581
582        // Covered sequences
583        for seq in [3u32, 5, 6, 7, 10, 11] {
584            assert!(sack.acks(seq), "expected seq {seq} to be ACKed");
585        }
586        // Gaps
587        for seq in [0u32, 1, 2, 4, 8, 9, 12, 100] {
588            assert!(!sack.acks(seq), "expected seq {seq} to NOT be ACKed");
589        }
590    }
591
592    #[test]
593    fn acks_single_seq() {
594        let sack = Sack::from_received(&[99], 0).unwrap();
595        assert!(sack.acks(99));
596        assert!(!sack.acks(98));
597        assert!(!sack.acks(100));
598    }
599
600    // ── decode error cases ───────────────────────────────────────────────
601
602    #[test]
603    fn decode_truncated_too_short() {
604        // Need at least 14 bytes; supply 13
605        let buf = [0u8; 13];
606        assert!(
607            matches!(Sack::from_wire(&buf), Err(SackError::Truncated)),
608            "expected Truncated for 13-byte buffer"
609        );
610    }
611
612    #[test]
613    fn decode_truncated_claimed_ranges_exceed_buffer() {
614        // Header claims 2 ranges but buffer only has the 1-range minimum (14 bytes).
615        // 2 ranges need 14 + 8 = 22 bytes.
616        let mut buf = vec![0u8; 14];
617        // largest_acked = 10
618        buf[0..4].copy_from_slice(&10u32.to_be_bytes());
619        // ack_delay_us = 0
620        buf[4..8].copy_from_slice(&0u32.to_be_bytes());
621        // range_count = 2
622        buf[8..10].copy_from_slice(&2u16.to_be_bytes());
623        // first_len = 0
624        buf[10..14].copy_from_slice(&0u32.to_be_bytes());
625        // Buffer is only 14 bytes but 2 ranges need 22 bytes → Truncated
626        assert!(
627            matches!(Sack::from_wire(&buf), Err(SackError::Truncated)),
628            "expected Truncated when buffer too small for claimed ranges"
629        );
630    }
631
632    #[test]
633    fn decode_too_many_ranges() {
634        // Craft a header with range_count = MAX_SACK_RANGES + 1
635        let bad_count = (MAX_SACK_RANGES + 1) as u16;
636        let needed = FIXED_HDR_LEN + 4 + CONTINUATION_BYTES * MAX_SACK_RANGES; // one more than max
637        let mut buf = vec![0u8; needed + CONTINUATION_BYTES]; // enough bytes to not be Truncated
638                                                              // largest_acked = 0xFFFF_FFFF so ranges have room to descend
639        buf[0..4].copy_from_slice(&u32::MAX.to_be_bytes());
640        buf[4..8].copy_from_slice(&0u32.to_be_bytes());
641        buf[8..10].copy_from_slice(&bad_count.to_be_bytes());
642        // first_len and continuations don't matter — check happens before decode
643        assert!(
644            matches!(Sack::from_wire(&buf), Err(SackError::TooManyRanges)),
645            "expected TooManyRanges for range_count = MAX+1"
646        );
647    }
648
649    #[test]
650    fn decode_zero_range_count_is_malformed() {
651        let mut buf = vec![0u8; 14];
652        buf[8..10].copy_from_slice(&0u16.to_be_bytes()); // range_count = 0
653        assert!(
654            matches!(Sack::from_wire(&buf), Err(SackError::Malformed)),
655            "expected Malformed for range_count == 0"
656        );
657    }
658
659    #[test]
660    fn decode_first_len_underflow_is_malformed() {
661        // largest_acked = 3, first_len = 5 → low would be 3 - 5 (underflow)
662        let mut buf = vec![0u8; 14];
663        buf[0..4].copy_from_slice(&3u32.to_be_bytes()); // largest_acked = 3
664        buf[4..8].copy_from_slice(&0u32.to_be_bytes());
665        buf[8..10].copy_from_slice(&1u16.to_be_bytes()); // range_count = 1
666        buf[10..14].copy_from_slice(&5u32.to_be_bytes()); // first_len = 5 (> 3)
667        assert!(
668            matches!(Sack::from_wire(&buf), Err(SackError::Malformed)),
669            "expected Malformed when first_len underflows"
670        );
671    }
672
673    #[test]
674    fn decode_continuation_gap_underflow_is_malformed() {
675        // First range: largest_acked=5, first_len=0 → range (5,5)
676        // Continuation gap=10, len=0 → high = 5 - 1 - 10 = underflow
677        let mut buf = vec![0u8; 22]; // 14 + 8 for one continuation
678        buf[0..4].copy_from_slice(&5u32.to_be_bytes()); // largest_acked = 5
679        buf[4..8].copy_from_slice(&0u32.to_be_bytes());
680        buf[8..10].copy_from_slice(&2u16.to_be_bytes()); // range_count = 2
681        buf[10..14].copy_from_slice(&0u32.to_be_bytes()); // first_len = 0
682        buf[14..18].copy_from_slice(&10u32.to_be_bytes()); // gap = 10 (underflows)
683        buf[18..22].copy_from_slice(&0u32.to_be_bytes()); // len = 0
684        assert!(
685            matches!(Sack::from_wire(&buf), Err(SackError::Malformed)),
686            "expected Malformed when continuation gap underflows"
687        );
688    }
689
690    // ── wire size sanity ─────────────────────────────────────────────────
691
692    #[test]
693    fn wire_size_1_range() {
694        let sack = Sack::from_received(&[5], 0).unwrap();
695        assert_eq!(sack.to_wire().len(), 14);
696    }
697
698    #[test]
699    fn wire_size_2_ranges() {
700        let sack = Sack::from_received(&[8, 9, 10, 3, 4, 5], 0).unwrap();
701        assert_eq!(sack.ranges(), &[(8, 10), (3, 5)]);
702        assert_eq!(sack.to_wire().len(), 22);
703    }
704
705    #[test]
706    fn wire_size_n_ranges() {
707        // Build a 5-range SACK with gaps >0 between each range.
708        // Ranges (descending): (80,89), (60,69), (40,49), (20,29), (0,9)
709        let mut seqs: Vec<u32> = Vec::new();
710        for i in 0u32..5 {
711            for s in (i * 20)..(i * 20 + 10) {
712                seqs.push(s);
713            }
714        }
715        let sack = Sack::from_received(&seqs, 0).unwrap();
716        let n = sack.ranges().len();
717        assert_eq!(n, 5);
718        assert_eq!(sack.to_wire().len(), 10 + 4 + 8 * (n - 1));
719    }
720
721    // ── Display / Error trait ────────────────────────────────────────────
722
723    #[test]
724    fn sack_error_display_is_non_empty() {
725        for e in [
726            SackError::Truncated,
727            SackError::TooManyRanges,
728            SackError::Malformed,
729        ] {
730            let s = e.to_string();
731            assert!(!s.is_empty(), "Display must not be empty for {e:?}");
732        }
733    }
734
735    #[test]
736    fn sack_error_is_std_error() {
737        // Verify the trait bound compiles and the source chain is None.
738        let e: &dyn std::error::Error = &SackError::Truncated;
739        assert!(e.source().is_none());
740    }
741}