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 — only `from_received` and
227 /// `from_wire` can construct a `Sack`, both of which guarantee at least one
228 /// range), and the arithmetic cannot overflow because all fields fit in
229 /// `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` (which requires a non-empty input)
252 // and `from_wire` (which rejects range_count == 0). This index cannot
253 // panic.
254 let (first_low, first_high) = self.ranges[0];
255 // PANIC-SAFETY: `from_received` guarantees low ≤ high because ranges
256 // are built from sorted sequences; `from_wire` validates the same
257 // invariant before storing. The subtraction cannot underflow.
258 let first_len: u32 = first_high - first_low;
259 buf.extend_from_slice(&first_len.to_be_bytes());
260
261 // Continuation ranges: (gap, len) pairs.
262 let mut prev_low = first_low;
263 for &(low, high) in &self.ranges[1..] {
264 // gap = number of unACKed sequences between prev range and this one.
265 // prev_low - 1 is the sequence just below the previous range.
266 // high is the top of the current range.
267 // gap = (prev_low - 1) - high (both sides of the unACKed gap)
268 // Since ranges are strictly descending with at least one gap seq,
269 // prev_low >= high + 2 must hold (invariant checked on decode;
270 // maintained by from_received which merges adjacent ranges).
271 let gap: u32 = prev_low.saturating_sub(1).saturating_sub(high);
272 buf.extend_from_slice(&gap.to_be_bytes());
273
274 let len: u32 = high - low;
275 buf.extend_from_slice(&len.to_be_bytes());
276
277 prev_low = low;
278 }
279
280 buf
281 }
282
283 /// Decode a [`Sack`] from wire bytes produced by [`Sack::to_wire`].
284 ///
285 /// # Errors
286 ///
287 /// See [`SackError`] for the full error table.
288 pub fn from_wire(buf: &[u8]) -> Result<Sack, SackError> {
289 // Need at least the fixed header + first_len field.
290 if buf.len() < MIN_WIRE_LEN {
291 return Err(SackError::Truncated);
292 }
293
294 let largest_acked = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]);
295 let ack_delay_us = u32::from_be_bytes([buf[4], buf[5], buf[6], buf[7]]);
296 let range_count = u16::from_be_bytes([buf[8], buf[9]]) as usize;
297
298 // Anti-DoS check BEFORE any allocation.
299 if range_count == 0 {
300 return Err(SackError::Malformed);
301 }
302 if range_count > MAX_SACK_RANGES {
303 return Err(SackError::TooManyRanges);
304 }
305
306 // Check that the buffer is long enough for all declared ranges.
307 // Wire size = 10 (fixed) + 4 (first_len) + 8*(range_count-1) (continuations)
308 let needed = FIXED_HDR_LEN
309 .checked_add(4)
310 .and_then(|n| n.checked_add(CONTINUATION_BYTES * (range_count - 1)))
311 .ok_or(SackError::Truncated)?;
312 if buf.len() < needed {
313 return Err(SackError::Truncated);
314 }
315
316 // Decode the first range.
317 let first_len = u32::from_be_bytes([buf[10], buf[11], buf[12], buf[13]]);
318 let first_high = largest_acked;
319 let first_low = largest_acked
320 .checked_sub(first_len)
321 .ok_or(SackError::Malformed)?;
322
323 let mut ranges: Vec<(u32, u32)> = Vec::with_capacity(range_count);
324 ranges.push((first_low, first_high));
325
326 // Decode continuation ranges.
327 let mut prev_low = first_low;
328 let mut pos = 14usize; // bytes consumed so far
329 for _ in 1..range_count {
330 let gap = u32::from_be_bytes([buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3]]);
331 let len = u32::from_be_bytes([buf[pos + 4], buf[pos + 5], buf[pos + 6], buf[pos + 7]]);
332 pos += 8;
333
334 // Fix 2: adjacent ranges (gap == 0) are invalid — the sender must
335 // coalesce them. Reject rather than silently accepting malformed
336 // input.
337 if gap == 0 {
338 return Err(SackError::Malformed);
339 }
340
341 // high_i = prev_low - 1 - gap
342 // prev_low must be ≥ gap + 1 so we don't wrap below 0.
343 let below_prev = prev_low.checked_sub(1).ok_or(SackError::Malformed)?;
344 let high = below_prev.checked_sub(gap).ok_or(SackError::Malformed)?;
345 let low = high.checked_sub(len).ok_or(SackError::Malformed)?;
346
347 // Ranges must be strictly descending and non-adjacent; `high` must
348 // be strictly less than `prev_low - 1` (gap ≥ 1 enforced above).
349 // The arithmetic already ensures high < prev_low. We additionally
350 // verify that low ≤ high (no inverted range).
351 if low > high {
352 return Err(SackError::Malformed);
353 }
354
355 ranges.push((low, high));
356 prev_low = low;
357 }
358
359 Ok(Sack {
360 largest_acked,
361 ack_delay_us,
362 ranges,
363 })
364 }
365
366 /// Returns `true` if `seq` is covered by any range in this SACK.
367 pub fn acks(&self, seq: u32) -> bool {
368 for &(low, high) in &self.ranges {
369 if seq >= low && seq <= high {
370 return true;
371 }
372 }
373 false
374 }
375}
376
377#[cfg(test)]
378#[allow(clippy::unwrap_used)]
379mod tests {
380 use super::*;
381
382 // ── from_received coalescing ──────────────────────────────────────────
383
384 #[test]
385 fn coalesces_to_descending_ranges() {
386 // Input: [5,6,7,10,11,3] → ranges [(10,11),(5,7),(3,3)], largest 11
387 let sack = Sack::from_received(&[5, 6, 7, 10, 11, 3], 0).unwrap();
388 assert_eq!(sack.largest_acked, 11);
389 assert_eq!(sack.ranges(), &[(10, 11), (5, 7), (3, 3)]);
390 }
391
392 #[test]
393 fn from_received_empty_returns_none() {
394 assert!(Sack::from_received(&[], 42).is_none());
395 }
396
397 #[test]
398 fn from_received_single_seq() {
399 let sack = Sack::from_received(&[7], 100).unwrap();
400 assert_eq!(sack.largest_acked, 7);
401 assert_eq!(sack.ranges(), &[(7, 7)]);
402 }
403
404 #[test]
405 fn from_received_contiguous() {
406 // 0..=9 should produce a single range (0, 9)
407 let input: Vec<u32> = (0..=9).collect();
408 let sack = Sack::from_received(&input, 0).unwrap();
409 assert_eq!(sack.largest_acked, 9);
410 assert_eq!(sack.ranges(), &[(0, 9)]);
411 }
412
413 #[test]
414 fn from_received_duplicate_seqs_coalesced() {
415 let sack = Sack::from_received(&[3, 3, 3, 5, 5], 0).unwrap();
416 assert_eq!(sack.ranges(), &[(5, 5), (3, 3)]);
417 }
418
419 // ── F1: generation must cap to MAX_SACK_RANGES so the peer can decode ──
420
421 #[test]
422 fn from_received_caps_ranges_to_max_and_stays_decodable() {
423 // 40 disjoint singleton islands (even sequences 0,2,..,78) → 40 ranges,
424 // which a peer would reject as TooManyRanges. Generation must cap to 32,
425 // keep the HIGHEST ranges (nearest largest_acked), and remain decodable.
426 let seqs: Vec<u32> = (0u32..40).map(|i| i * 2).collect();
427 let sack = Sack::from_received(&seqs, 0).expect("non-empty");
428 assert!(
429 sack.ranges().len() <= MAX_SACK_RANGES,
430 "generated SACK must be capped to MAX_SACK_RANGES, got {}",
431 sack.ranges().len()
432 );
433 // Kept the highest 32 ranges: (78,78) down to (16,16); largest unchanged.
434 assert_eq!(sack.largest_acked, 78);
435 assert_eq!(sack.ranges().len(), MAX_SACK_RANGES);
436 assert_eq!(sack.ranges()[0], (78, 78));
437 assert_eq!(sack.ranges()[MAX_SACK_RANGES - 1], (16, 16));
438 let wire = sack.to_wire();
439 let decoded = Sack::from_wire(&wire).expect("a capped SACK must decode at the peer");
440 assert_eq!(decoded, sack);
441 }
442
443 #[test]
444 fn from_inclusive_ranges_caps_and_keeps_highest() {
445 // Ascending (low,high) input with 40 islands; keep the highest 32.
446 let asc: Vec<(u32, u32)> = (0u32..40).map(|i| (i * 2, i * 2)).collect();
447 let sack = Sack::from_inclusive_ranges(asc, 7).expect("non-empty");
448 assert_eq!(sack.ack_delay_us, 7);
449 assert_eq!(sack.ranges().len(), MAX_SACK_RANGES);
450 assert_eq!(sack.largest_acked, 78);
451 assert_eq!(sack.ranges()[0], (78, 78));
452 // Decodes at the peer.
453 assert_eq!(Sack::from_wire(&sack.to_wire()).expect("decode"), sack);
454 }
455
456 #[test]
457 fn from_inclusive_ranges_coalesces_and_orders() {
458 // Unsorted, with an adjacent pair (4,5)+(6,7) that must coalesce to (4,7).
459 let sack = Sack::from_inclusive_ranges(vec![(6, 7), (0, 1), (4, 5)], 0).expect("non-empty");
460 assert_eq!(sack.ranges(), &[(4, 7), (0, 1)]);
461 assert_eq!(sack.largest_acked, 7);
462 assert_eq!(Sack::from_wire(&sack.to_wire()).expect("decode"), sack);
463 }
464
465 #[test]
466 fn from_inclusive_ranges_empty_is_none() {
467 assert!(Sack::from_inclusive_ranges(Vec::new(), 0).is_none());
468 }
469
470 // ── round-trip tests ─────────────────────────────────────────────────
471
472 #[test]
473 fn roundtrip_multi_gap() {
474 // Three disjoint ranges: (20,25), (10,13), (1,3)
475 let sack =
476 Sack::from_received(&[20, 21, 22, 23, 24, 25, 10, 11, 12, 13, 1, 2, 3], 1234).unwrap();
477 assert_eq!(sack.ranges(), &[(20, 25), (10, 13), (1, 3)]);
478 let wire = sack.to_wire();
479 let decoded = Sack::from_wire(&wire).expect("should decode");
480 assert_eq!(decoded, sack);
481 }
482
483 #[test]
484 fn roundtrip_single_contiguous() {
485 let sack = Sack::from_received(&(0u32..=9).collect::<Vec<_>>(), 0).unwrap();
486 assert_eq!(sack.ranges(), &[(0, 9)]);
487 let wire = sack.to_wire();
488 let decoded = Sack::from_wire(&wire).expect("roundtrip single");
489 assert_eq!(decoded, sack);
490 }
491
492 #[test]
493 fn roundtrip_degenerate_single_seq() {
494 let sack = Sack::from_received(&[42], 999).unwrap();
495 assert_eq!(sack.largest_acked, 42);
496 let wire = sack.to_wire();
497 let decoded = Sack::from_wire(&wire).expect("roundtrip single seq");
498 assert_eq!(decoded, sack);
499 }
500
501 #[test]
502 fn roundtrip_from_received_coalesced() {
503 let input = [5u32, 6, 7, 10, 11, 3];
504 let sack = Sack::from_received(&input, 500).unwrap();
505 let wire = sack.to_wire();
506 let decoded = Sack::from_wire(&wire).expect("from_received roundtrip");
507 assert_eq!(decoded, sack);
508 }
509
510 #[test]
511 fn roundtrip_preserves_ack_delay() {
512 let sack = Sack::from_received(&[1, 2, 3], 0xDEAD_BEEF).unwrap();
513 let wire = sack.to_wire();
514 let decoded = Sack::from_wire(&wire).expect("roundtrip ack_delay");
515 assert_eq!(decoded.ack_delay_us, 0xDEAD_BEEF);
516 }
517
518 // ── Fix 1: large span round-trip (was silently truncated with u16) ────
519
520 #[test]
521 fn roundtrip_large_span_exceeds_u16() {
522 // first_len = 100_000 − 0 = 100_000, which overflows u16::MAX (65535).
523 // This is the exact case that was silently corrupted before the fix.
524 let seqs: Vec<u32> = (0u32..=100_000).collect();
525 let sack = Sack::from_received(&seqs, 0).unwrap();
526 assert_eq!(sack.largest_acked, 100_000);
527 assert_eq!(sack.ranges(), &[(0, 100_000)]);
528 let wire = sack.to_wire();
529 let decoded = Sack::from_wire(&wire).expect("large span must round-trip");
530 assert_eq!(decoded, sack);
531 // Verify first_len on wire (bytes 10..14) is 100_000 in big-endian u32.
532 let first_len_wire = u32::from_be_bytes([wire[10], wire[11], wire[12], wire[13]]);
533 assert_eq!(first_len_wire, 100_000u32);
534 }
535
536 #[test]
537 fn roundtrip_large_gap_exceeds_u16() {
538 // Two ranges: (200_000, 200_000) and (0, 0).
539 // gap = 200_000 - 1 - 0 = 199_999, which overflows u16::MAX.
540 let sack = Sack::from_received(&[200_000u32, 0], 0).unwrap();
541 assert_eq!(sack.ranges(), &[(200_000, 200_000), (0, 0)]);
542 let wire = sack.to_wire();
543 let decoded = Sack::from_wire(&wire).expect("large gap must round-trip");
544 assert_eq!(decoded, sack);
545 // Verify gap on wire (bytes 14..18) is 199_999 in big-endian u32.
546 // Layout: largest_acked(4) + ack_delay_us(4) + range_count(2) + first_len(4) = 14
547 // then gap(4) starting at byte 14.
548 let gap_wire = u32::from_be_bytes([wire[14], wire[15], wire[16], wire[17]]);
549 assert_eq!(gap_wire, 199_999u32);
550 }
551
552 // ── Fix 2: gap == 0 must be rejected as Malformed ────────────────────
553
554 #[test]
555 fn decode_gap_zero_is_malformed() {
556 // Craft a 2-range SACK where gap == 0 (adjacent ranges).
557 // Wire layout (u32 fields):
558 // largest_acked=10, ack_delay=0, range_count=2,
559 // first_len=0 (range (10,10)),
560 // gap=0, len=0 (adjacent range (9,9) — invalid)
561 let mut buf = vec![0u8; 22]; // 10 + 4 + 8 = 22 bytes for 2 ranges
562 buf[0..4].copy_from_slice(&10u32.to_be_bytes()); // largest_acked
563 buf[4..8].copy_from_slice(&0u32.to_be_bytes()); // ack_delay_us
564 buf[8..10].copy_from_slice(&2u16.to_be_bytes()); // range_count = 2
565 buf[10..14].copy_from_slice(&0u32.to_be_bytes()); // first_len = 0
566 buf[14..18].copy_from_slice(&0u32.to_be_bytes()); // gap = 0 ← invalid
567 buf[18..22].copy_from_slice(&0u32.to_be_bytes()); // len = 0
568 assert!(
569 matches!(Sack::from_wire(&buf), Err(SackError::Malformed)),
570 "gap == 0 must be rejected as Malformed"
571 );
572 }
573
574 // ── acks() helper ────────────────────────────────────────────────────
575
576 #[test]
577 fn acks_correctness_across_gaps() {
578 // Ranges: (10,11), (5,7), (3,3)
579 let sack = Sack::from_received(&[5, 6, 7, 10, 11, 3], 0).unwrap();
580
581 // Covered sequences
582 for seq in [3u32, 5, 6, 7, 10, 11] {
583 assert!(sack.acks(seq), "expected seq {seq} to be ACKed");
584 }
585 // Gaps
586 for seq in [0u32, 1, 2, 4, 8, 9, 12, 100] {
587 assert!(!sack.acks(seq), "expected seq {seq} to NOT be ACKed");
588 }
589 }
590
591 #[test]
592 fn acks_single_seq() {
593 let sack = Sack::from_received(&[99], 0).unwrap();
594 assert!(sack.acks(99));
595 assert!(!sack.acks(98));
596 assert!(!sack.acks(100));
597 }
598
599 // ── decode error cases ───────────────────────────────────────────────
600
601 #[test]
602 fn decode_truncated_too_short() {
603 // Need at least 14 bytes; supply 13
604 let buf = [0u8; 13];
605 assert!(
606 matches!(Sack::from_wire(&buf), Err(SackError::Truncated)),
607 "expected Truncated for 13-byte buffer"
608 );
609 }
610
611 #[test]
612 fn decode_truncated_claimed_ranges_exceed_buffer() {
613 // Header claims 2 ranges but buffer only has the 1-range minimum (14 bytes).
614 // 2 ranges need 14 + 8 = 22 bytes.
615 let mut buf = vec![0u8; 14];
616 // largest_acked = 10
617 buf[0..4].copy_from_slice(&10u32.to_be_bytes());
618 // ack_delay_us = 0
619 buf[4..8].copy_from_slice(&0u32.to_be_bytes());
620 // range_count = 2
621 buf[8..10].copy_from_slice(&2u16.to_be_bytes());
622 // first_len = 0
623 buf[10..14].copy_from_slice(&0u32.to_be_bytes());
624 // Buffer is only 14 bytes but 2 ranges need 22 bytes → Truncated
625 assert!(
626 matches!(Sack::from_wire(&buf), Err(SackError::Truncated)),
627 "expected Truncated when buffer too small for claimed ranges"
628 );
629 }
630
631 #[test]
632 fn decode_too_many_ranges() {
633 // Craft a header with range_count = MAX_SACK_RANGES + 1
634 let bad_count = (MAX_SACK_RANGES + 1) as u16;
635 let needed = FIXED_HDR_LEN + 4 + CONTINUATION_BYTES * MAX_SACK_RANGES; // one more than max
636 let mut buf = vec![0u8; needed + CONTINUATION_BYTES]; // enough bytes to not be Truncated
637 // largest_acked = 0xFFFF_FFFF so ranges have room to descend
638 buf[0..4].copy_from_slice(&u32::MAX.to_be_bytes());
639 buf[4..8].copy_from_slice(&0u32.to_be_bytes());
640 buf[8..10].copy_from_slice(&bad_count.to_be_bytes());
641 // first_len and continuations don't matter — check happens before decode
642 assert!(
643 matches!(Sack::from_wire(&buf), Err(SackError::TooManyRanges)),
644 "expected TooManyRanges for range_count = MAX+1"
645 );
646 }
647
648 #[test]
649 fn decode_zero_range_count_is_malformed() {
650 let mut buf = vec![0u8; 14];
651 buf[8..10].copy_from_slice(&0u16.to_be_bytes()); // range_count = 0
652 assert!(
653 matches!(Sack::from_wire(&buf), Err(SackError::Malformed)),
654 "expected Malformed for range_count == 0"
655 );
656 }
657
658 #[test]
659 fn decode_first_len_underflow_is_malformed() {
660 // largest_acked = 3, first_len = 5 → low would be 3 - 5 (underflow)
661 let mut buf = vec![0u8; 14];
662 buf[0..4].copy_from_slice(&3u32.to_be_bytes()); // largest_acked = 3
663 buf[4..8].copy_from_slice(&0u32.to_be_bytes());
664 buf[8..10].copy_from_slice(&1u16.to_be_bytes()); // range_count = 1
665 buf[10..14].copy_from_slice(&5u32.to_be_bytes()); // first_len = 5 (> 3)
666 assert!(
667 matches!(Sack::from_wire(&buf), Err(SackError::Malformed)),
668 "expected Malformed when first_len underflows"
669 );
670 }
671
672 #[test]
673 fn decode_continuation_gap_underflow_is_malformed() {
674 // First range: largest_acked=5, first_len=0 → range (5,5)
675 // Continuation gap=10, len=0 → high = 5 - 1 - 10 = underflow
676 let mut buf = vec![0u8; 22]; // 14 + 8 for one continuation
677 buf[0..4].copy_from_slice(&5u32.to_be_bytes()); // largest_acked = 5
678 buf[4..8].copy_from_slice(&0u32.to_be_bytes());
679 buf[8..10].copy_from_slice(&2u16.to_be_bytes()); // range_count = 2
680 buf[10..14].copy_from_slice(&0u32.to_be_bytes()); // first_len = 0
681 buf[14..18].copy_from_slice(&10u32.to_be_bytes()); // gap = 10 (underflows)
682 buf[18..22].copy_from_slice(&0u32.to_be_bytes()); // len = 0
683 assert!(
684 matches!(Sack::from_wire(&buf), Err(SackError::Malformed)),
685 "expected Malformed when continuation gap underflows"
686 );
687 }
688
689 // ── wire size sanity ─────────────────────────────────────────────────
690
691 #[test]
692 fn wire_size_1_range() {
693 let sack = Sack::from_received(&[5], 0).unwrap();
694 assert_eq!(sack.to_wire().len(), 14);
695 }
696
697 #[test]
698 fn wire_size_2_ranges() {
699 let sack = Sack::from_received(&[8, 9, 10, 3, 4, 5], 0).unwrap();
700 assert_eq!(sack.ranges(), &[(8, 10), (3, 5)]);
701 assert_eq!(sack.to_wire().len(), 22);
702 }
703
704 #[test]
705 fn wire_size_n_ranges() {
706 // Build a 5-range SACK with gaps >0 between each range.
707 // Ranges (descending): (80,89), (60,69), (40,49), (20,29), (0,9)
708 let mut seqs: Vec<u32> = Vec::new();
709 for i in 0u32..5 {
710 for s in (i * 20)..(i * 20 + 10) {
711 seqs.push(s);
712 }
713 }
714 let sack = Sack::from_received(&seqs, 0).unwrap();
715 let n = sack.ranges().len();
716 assert_eq!(n, 5);
717 assert_eq!(sack.to_wire().len(), 10 + 4 + 8 * (n - 1));
718 }
719
720 // ── Display / Error trait ────────────────────────────────────────────
721
722 #[test]
723 fn sack_error_display_is_non_empty() {
724 for e in [
725 SackError::Truncated,
726 SackError::TooManyRanges,
727 SackError::Malformed,
728 ] {
729 let s = e.to_string();
730 assert!(!s.is_empty(), "Display must not be empty for {e:?}");
731 }
732 }
733
734 #[test]
735 fn sack_error_is_std_error() {
736 // Verify the trait bound compiles and the source chain is None.
737 let e: &dyn std::error::Error = &SackError::Truncated;
738 assert!(e.source().is_none());
739 }
740}