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}