umsh_core/
options.rs

1use crate::{EncodeError, ParseError};
2
3/// Incremental encoder for CoAP-style delta/length option blocks.
4///
5/// The encoder writes directly into a caller-supplied buffer and tracks the last
6/// emitted option number so the on-wire delta encoding remains canonical.
7#[derive(Debug)]
8pub struct OptionEncoder<'a> {
9    buf: &'a mut [u8],
10    pos: usize,
11    last_number: u16,
12    wrote_any: bool,
13}
14
15impl<'a> OptionEncoder<'a> {
16    /// Create an encoder starting at option number `0`.
17    pub fn new(buf: &'a mut [u8]) -> Self {
18        Self {
19            buf,
20            pos: 0,
21            last_number: 0,
22            wrote_any: false,
23        }
24    }
25
26    /// Create an encoder that continues from an already-emitted option number.
27    pub fn with_last_number(buf: &'a mut [u8], last_number: u16) -> Self {
28        Self {
29            buf,
30            pos: 0,
31            last_number,
32            wrote_any: true,
33        }
34    }
35
36    /// Encode one option value.
37    pub fn put(&mut self, number: u16, value: &[u8]) -> Result<(), EncodeError> {
38        if self.wrote_any && number < self.last_number {
39            return Err(EncodeError::OptionOutOfOrder);
40        }
41        let delta = if self.wrote_any {
42            number - self.last_number
43        } else {
44            number
45        };
46        let delta_len = encoded_len(delta);
47        let value_len = encoded_len(value.len() as u16);
48        let required = 1 + delta_len + value_len + value.len();
49        if self.pos + required > self.buf.len() {
50            return Err(EncodeError::BufferTooSmall);
51        }
52
53        let header_pos = self.pos;
54        self.pos += 1;
55        let delta_nibble = write_extended(&mut self.buf[self.pos..], delta)?;
56        self.pos += delta_len;
57        let len_nibble = write_extended(&mut self.buf[self.pos..], value.len() as u16)?;
58        self.pos += value_len;
59        self.buf[header_pos] = (delta_nibble << 4) | len_nibble;
60        self.buf[self.pos..self.pos + value.len()].copy_from_slice(value);
61        self.pos += value.len();
62        self.last_number = number;
63        self.wrote_any = true;
64        Ok(())
65    }
66
67    /// Append the `0xFF` end marker for an option block.
68    pub fn end_marker(&mut self) -> Result<(), EncodeError> {
69        if self.pos >= self.buf.len() {
70            return Err(EncodeError::BufferTooSmall);
71        }
72        self.buf[self.pos] = 0xFF;
73        self.pos += 1;
74        Ok(())
75    }
76
77    /// Encode a `u32` in minimal big-endian form (leading zero bytes stripped).
78    pub fn put_u32(&mut self, number: u16, value: u32) -> Result<(), EncodeError> {
79        let (bytes, len) = minimal_u32(value);
80        self.put(number, &bytes[4 - len..])
81    }
82
83    /// Encode an `i32` in minimal big-endian form (leading sign-extension bytes stripped).
84    pub fn put_i32(&mut self, number: u16, value: i32) -> Result<(), EncodeError> {
85        let (bytes, len) = minimal_i32(value);
86        self.put(number, &bytes[4 - len..])
87    }
88
89    /// Finish the encoder and return the number of bytes written.
90    pub fn finish(self) -> usize {
91        self.pos
92    }
93}
94
95/// One trace-signal entry: negative RSSI in dBm, then SNR in centibels
96/// (packet-options.md § Trace Signal).
97///
98/// Every hop that prepends a router hint to a trace route prepends one of
99/// these to the trace signal, so entry `N` is the quality at which the hop
100/// named by hint `N` heard the packet. That pairing is the option's whole
101/// value, which is why the encoding lives here rather than in whichever
102/// forwarder happens to need it: a repeater and a bridge that disagree
103/// about a byte produce a list nothing downstream can read.
104///
105/// Both fields saturate rather than wrap, so a reading past the end of the
106/// range reports the nearest representable value instead of a
107/// plausible-looking wrong one.
108#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
109pub struct TraceSignalEntry([u8; 2]);
110
111impl TraceSignalEntry {
112    /// What a hop writes when its radio reported no measurement at all.
113    ///
114    /// An RSSI byte of zero is 0 dBm at the receiver, which no link this
115    /// protocol runs over produces, so it cannot be mistaken for a
116    /// reading. Writing it keeps the entry-for-entry pairing with the
117    /// trace route intact, which a hop that measures nothing is still
118    /// obliged to maintain.
119    pub const UNMEASURED: Self = Self([0, 0]);
120
121    /// The entry for a hop that measured both fields.
122    pub const fn new(rssi_dbm: i16, snr_centibels: i16) -> Self {
123        Self([saturate_rssi(rssi_dbm), saturate_snr(snr_centibels)])
124    }
125
126    /// The entry for a hop whose radio may not report either field.
127    ///
128    /// The two measurements are independent, so a radio that reports one
129    /// and not the other still contributes the one it has.
130    pub const fn from_measurements(rssi_dbm: Option<i16>, snr_centibels: Option<i16>) -> Self {
131        Self([
132            match rssi_dbm {
133                Some(rssi) => saturate_rssi(rssi),
134                None => Self::UNMEASURED.0[0],
135            },
136            match snr_centibels {
137                Some(snr) => saturate_snr(snr),
138                None => Self::UNMEASURED.0[1],
139            },
140        ])
141    }
142
143    /// The two bytes, ready to prepend to a trace-signal option value.
144    pub const fn as_bytes(self) -> [u8; 2] {
145        self.0
146    }
147}
148
149const fn saturate_rssi(rssi_dbm: i16) -> u8 {
150    if rssi_dbm >= 0 {
151        0
152    } else if rssi_dbm < -255 {
153        255
154    } else {
155        (-rssi_dbm) as u8
156    }
157}
158
159const fn saturate_snr(snr_centibels: i16) -> u8 {
160    if snr_centibels > 127 {
161        127u8
162    } else if snr_centibels < -128 {
163        0x80
164    } else {
165        (snr_centibels as i8) as u8
166    }
167}
168
169/// Parse a minimal big-endian unsigned integer (leading zero bytes stripped).
170///
171/// Returns `ParseError::MalformedOption` if `bytes.len() > 4`.
172pub fn parse_be_u32(bytes: &[u8]) -> Result<u32, ParseError> {
173    if bytes.len() > 4 {
174        return Err(ParseError::MalformedOption);
175    }
176    let mut arr = [0u8; 4];
177    arr[4 - bytes.len()..].copy_from_slice(bytes);
178    Ok(u32::from_be_bytes(arr))
179}
180
181/// Parse a minimal big-endian signed integer (leading sign-extension bytes stripped).
182///
183/// An empty slice decodes as `0`. Returns `ParseError::MalformedOption` if `bytes.len() > 4`.
184pub fn parse_be_i32(bytes: &[u8]) -> Result<i32, ParseError> {
185    if bytes.is_empty() {
186        return Ok(0);
187    }
188    if bytes.len() > 4 {
189        return Err(ParseError::MalformedOption);
190    }
191    let sign = if bytes[0] & 0x80 != 0 { 0xFF } else { 0x00 };
192    let mut arr = [sign; 4];
193    arr[4 - bytes.len()..].copy_from_slice(bytes);
194    Ok(i32::from_be_bytes(arr))
195}
196
197fn minimal_u32(v: u32) -> ([u8; 4], usize) {
198    let bytes = v.to_be_bytes();
199    let skip = bytes.iter().position(|&b| b != 0).unwrap_or(4);
200    (bytes, 4 - skip)
201}
202
203fn minimal_i32(v: i32) -> ([u8; 4], usize) {
204    if v == 0 {
205        return ([0u8; 4], 0);
206    }
207    let bytes = v.to_be_bytes();
208    let mut skip = 0;
209    if v > 0 {
210        while skip < 3 && bytes[skip] == 0x00 && (bytes[skip + 1] & 0x80 == 0) {
211            skip += 1;
212        }
213    } else {
214        while skip < 3 && bytes[skip] == 0xFF && (bytes[skip + 1] & 0x80 != 0) {
215            skip += 1;
216        }
217    }
218    (bytes, 4 - skip)
219}
220
221/// Incremental decoder for CoAP-style delta/length option blocks.
222///
223/// This iterator yields absolute option numbers together with borrowed value
224/// slices from the original buffer.
225#[derive(Clone, Debug)]
226pub struct OptionDecoder<'a> {
227    data: &'a [u8],
228    pos: usize,
229    last_number: u16,
230    finished: bool,
231    errored: bool,
232}
233
234impl<'a> OptionDecoder<'a> {
235    /// Create a decoder over a complete encoded option block.
236    pub fn new(data: &'a [u8]) -> Self {
237        Self {
238            data,
239            pos: 0,
240            last_number: 0,
241            finished: false,
242            errored: false,
243        }
244    }
245
246    /// Byte offset of the next undecoded byte in the underlying buffer.
247    ///
248    /// Sampled before a call to `next`, this is the offset of that option's
249    /// header, allowing higher-level codecs to retain sub-ranges of an option
250    /// block verbatim.
251    pub fn position(&self) -> usize {
252        self.pos
253    }
254
255    /// The absolute option number in effect for delta decoding.
256    pub fn last_number(&self) -> u16 {
257        self.last_number
258    }
259
260    /// Return the trailing bytes after a consumed end marker.
261    ///
262    /// This is typically used by higher-level codecs whose options are followed
263    /// by payload bytes.
264    pub fn remainder(&self) -> &'a [u8] {
265        if self.finished {
266            &self.data[self.pos..]
267        } else {
268            &[]
269        }
270    }
271}
272
273impl<'a> Iterator for OptionDecoder<'a> {
274    type Item = Result<(u16, &'a [u8]), ParseError>;
275
276    fn next(&mut self) -> Option<Self::Item> {
277        if self.finished || self.errored {
278            return None;
279        }
280        if self.pos >= self.data.len() {
281            self.finished = true;
282            return None;
283        }
284
285        let first = self.data[self.pos];
286        if first == 0xFF {
287            self.pos += 1;
288            self.finished = true;
289            return None;
290        }
291
292        self.pos += 1;
293        let delta_nibble = first >> 4;
294        let len_nibble = first & 0x0F;
295        let (delta, delta_len) = match read_extended(&self.data[self.pos..], delta_nibble) {
296            Ok(value) => value,
297            Err(err) => {
298                self.errored = true;
299                return Some(Err(err));
300            }
301        };
302        self.pos += delta_len;
303        let (len, len_len) = match read_extended(&self.data[self.pos..], len_nibble) {
304            Ok(value) => value,
305            Err(err) => {
306                self.errored = true;
307                return Some(Err(err));
308            }
309        };
310        self.pos += len_len;
311
312        if self.pos + len as usize > self.data.len() {
313            self.errored = true;
314            return Some(Err(ParseError::Truncated));
315        }
316
317        let number = self
318            .last_number
319            .checked_add(delta)
320            .ok_or(ParseError::MalformedOption);
321        let number = match number {
322            Ok(value) => value,
323            Err(err) => {
324                self.errored = true;
325                return Some(Err(err));
326            }
327        };
328        let value = &self.data[self.pos..self.pos + len as usize];
329        self.pos += len as usize;
330        self.last_number = number;
331        Some(Ok((number, value)))
332    }
333}
334
335fn encoded_len(value: u16) -> usize {
336    match value {
337        0..=12 => 0,
338        13..=268 => 1,
339        _ => 2,
340    }
341}
342
343fn write_extended(buf: &mut [u8], value: u16) -> Result<u8, EncodeError> {
344    match value {
345        0..=12 => Ok(value as u8),
346        13..=268 => {
347            if buf.is_empty() {
348                return Err(EncodeError::BufferTooSmall);
349            }
350            buf[0] = (value - 13) as u8;
351            Ok(13)
352        }
353        _ => {
354            if buf.len() < 2 {
355                return Err(EncodeError::BufferTooSmall);
356            }
357            let extended = value - 269;
358            buf[..2].copy_from_slice(&extended.to_be_bytes());
359            Ok(14)
360        }
361    }
362}
363
364fn read_extended(data: &[u8], nibble: u8) -> Result<(u16, usize), ParseError> {
365    match nibble {
366        0..=12 => Ok((nibble as u16, 0)),
367        13 => {
368            if data.is_empty() {
369                return Err(ParseError::Truncated);
370            }
371            Ok((data[0] as u16 + 13, 1))
372        }
373        14 => {
374            if data.len() < 2 {
375                return Err(ParseError::Truncated);
376            }
377            let value = u16::from_be_bytes([data[0], data[1]])
378                .checked_add(269)
379                .ok_or(ParseError::InvalidOptionNibble)?;
380            Ok((value, 2))
381        }
382        _ => Err(ParseError::InvalidOptionNibble),
383    }
384}
385
386#[cfg(test)]
387mod tests {
388    use super::*;
389
390    // ── TraceSignalEntry ──────────────────────────────────────────────────────
391
392    #[test]
393    fn a_trace_signal_entry_is_negated_rssi_then_signed_centibels() {
394        assert_eq!(TraceSignalEntry::new(-91, 42).as_bytes(), [91, 42]);
395        assert_eq!(TraceSignalEntry::new(-120, -75).as_bytes(), [120, 0xB5]);
396    }
397
398    #[test]
399    fn a_reading_past_the_end_of_the_range_saturates() {
400        // Better the nearest representable value than a wrapped one that
401        // reads as a plausible measurement.
402        assert_eq!(TraceSignalEntry::new(-400, 0).as_bytes()[0], 255);
403        assert_eq!(TraceSignalEntry::new(-90, 500).as_bytes()[1], 127);
404        assert_eq!(TraceSignalEntry::new(-90, -500).as_bytes()[1], 0x80);
405    }
406
407    #[test]
408    fn a_radio_that_reports_one_field_still_contributes_it() {
409        let entry = TraceSignalEntry::from_measurements(Some(-91), None);
410        assert_eq!(entry.as_bytes()[0], 91);
411        assert_eq!(
412            entry.as_bytes()[1],
413            TraceSignalEntry::UNMEASURED.as_bytes()[1]
414        );
415
416        assert_eq!(
417            TraceSignalEntry::from_measurements(None, None),
418            TraceSignalEntry::UNMEASURED
419        );
420    }
421
422    // ── parse_be_u32 ──────────────────────────────────────────────────────────
423
424    #[test]
425    fn parse_be_u32_values() {
426        assert_eq!(parse_be_u32(&[]).unwrap(), 0);
427        assert_eq!(parse_be_u32(&[1]).unwrap(), 1);
428        assert_eq!(parse_be_u32(&[1, 0]).unwrap(), 256);
429        assert_eq!(parse_be_u32(&[1, 0, 0]).unwrap(), 65536);
430        assert_eq!(parse_be_u32(&[0, 1, 0]).unwrap(), 256); // non-minimal: still parses
431        assert_eq!(parse_be_u32(&[0xFF, 0xFF, 0xFF, 0xFF]).unwrap(), u32::MAX);
432        assert!(parse_be_u32(&[0; 5]).is_err());
433    }
434
435    // ── parse_be_i32 ──────────────────────────────────────────────────────────
436
437    #[test]
438    fn parse_be_i32_values() {
439        assert_eq!(parse_be_i32(&[]).unwrap(), 0);
440        assert_eq!(parse_be_i32(&[0x7F]).unwrap(), 127);
441        assert_eq!(parse_be_i32(&[0x00, 0x80]).unwrap(), 128);
442        assert_eq!(parse_be_i32(&[0xFF]).unwrap(), -1);
443        assert_eq!(parse_be_i32(&[0x80]).unwrap(), -128);
444        assert_eq!(parse_be_i32(&[0xFF, 0x7F]).unwrap(), -129);
445        assert_eq!(parse_be_i32(&[0x80, 0x00, 0x00, 0x00]).unwrap(), i32::MIN);
446        assert_eq!(parse_be_i32(&[0x7F, 0xFF, 0xFF, 0xFF]).unwrap(), i32::MAX);
447        assert!(parse_be_i32(&[0; 5]).is_err());
448    }
449
450    // ── put_u32 / put_i32 round-trips ─────────────────────────────────────────
451
452    #[test]
453    fn put_u32_round_trips() {
454        let cases: &[u32] = &[0, 1, 127, 128, 255, 256, u32::MAX];
455        for &v in cases {
456            let mut buf = [0u8; 16];
457            let mut enc = OptionEncoder::new(&mut buf);
458            enc.put_u32(1, v).unwrap();
459            let len = enc.finish();
460            let (_, value) = OptionDecoder::new(&buf[..len]).next().unwrap().unwrap();
461            assert_eq!(parse_be_u32(value).unwrap(), v, "failed for u32 {v}");
462        }
463    }
464
465    #[test]
466    fn put_i32_round_trips() {
467        let cases: &[i32] = &[0, 1, 127, 128, -1, -128, -129, i32::MIN, i32::MAX];
468        for &v in cases {
469            let mut buf = [0u8; 16];
470            let mut enc = OptionEncoder::new(&mut buf);
471            enc.put_i32(1, v).unwrap();
472            let len = enc.finish();
473            let (_, value) = OptionDecoder::new(&buf[..len]).next().unwrap().unwrap();
474            assert_eq!(parse_be_i32(value).unwrap(), v, "failed for i32 {v}");
475        }
476    }
477
478    // ── wire format ───────────────────────────────────────────────────────────
479
480    // Option 5, value [0xAB]: delta=5 (nibble), len=1 (nibble) → header=0x51, body=[0xAB].
481    #[test]
482    fn wire_inline_delta_and_length() {
483        let mut buf = [0u8; 8];
484        let mut enc = OptionEncoder::new(&mut buf);
485        enc.put(5, &[0xAB]).unwrap();
486        assert_eq!(enc.finish(), 2);
487        assert_eq!(&buf[..2], &[0x51, 0xAB]);
488    }
489
490    // Option 0, empty value: header=0x00.
491    #[test]
492    fn wire_option_zero_empty_value() {
493        let mut buf = [0u8; 4];
494        let mut enc = OptionEncoder::new(&mut buf);
495        enc.put(0, &[]).unwrap();
496        assert_eq!(enc.finish(), 1);
497        assert_eq!(buf[0], 0x00);
498    }
499
500    // Delta=13 encodes as nibble 13 + 1 extended byte (delta - 13 = 0).
501    #[test]
502    fn wire_extended_delta_1byte_boundary() {
503        let mut buf = [0u8; 8];
504        let mut enc = OptionEncoder::new(&mut buf);
505        enc.put(13, &[]).unwrap();
506        assert_eq!(enc.finish(), 2);
507        // nibble=0xD (13), len_nibble=0 → header=0xD0; ext_delta=13-13=0x00
508        assert_eq!(&buf[..2], &[0xD0, 0x00]);
509    }
510
511    // Delta=268 is the largest that fits in a 1-byte extended field (268-13=255).
512    #[test]
513    fn wire_extended_delta_1byte_max() {
514        let mut buf = [0u8; 8];
515        let mut enc = OptionEncoder::new(&mut buf);
516        enc.put(268, &[]).unwrap();
517        assert_eq!(enc.finish(), 2);
518        assert_eq!(&buf[..2], &[0xD0, 0xFF]);
519    }
520
521    // Delta=269 requires a 2-byte extended field (nibble=14, value-269=0 → [0x00,0x00]).
522    #[test]
523    fn wire_extended_delta_2byte_boundary() {
524        let mut buf = [0u8; 8];
525        let mut enc = OptionEncoder::new(&mut buf);
526        enc.put(269, &[]).unwrap();
527        assert_eq!(enc.finish(), 3);
528        assert_eq!(&buf[..3], &[0xE0, 0x00, 0x00]);
529    }
530
531    // Value length=13 encodes in 1 extended length byte.
532    #[test]
533    fn wire_extended_length_1byte() {
534        let mut buf = [0u8; 32];
535        let value = [0u8; 13];
536        {
537            let mut enc = OptionEncoder::new(&mut buf);
538            enc.put(0, &value).unwrap();
539            assert_eq!(enc.finish(), 15);
540        }
541        // header: delta_nibble=0, len_nibble=13(0xD) → 0x0D; ext_len=13-13=0x00
542        assert_eq!(buf[0], 0x0D);
543        assert_eq!(buf[1], 0x00);
544        assert_eq!(&buf[2..15], &value);
545    }
546
547    // end_marker writes 0xFF.
548    #[test]
549    fn wire_end_marker() {
550        let mut buf = [0u8; 4];
551        let mut enc = OptionEncoder::new(&mut buf);
552        enc.put(1, &[0x01]).unwrap();
553        enc.end_marker().unwrap();
554        let len = enc.finish();
555        assert_eq!(buf[len - 1], 0xFF);
556    }
557
558    // ── OptionEncoder: multiple options and delta accumulation ────────────────
559
560    #[test]
561    fn encoder_multiple_options_sequential() {
562        let mut buf = [0u8; 16];
563        let mut enc = OptionEncoder::new(&mut buf);
564        enc.put(1, &[0x01]).unwrap();
565        enc.put(3, &[0x02]).unwrap(); // delta from 1 → 3 is 2
566        enc.put(3, &[0x03]).unwrap(); // delta 0 — same option again
567        let len = enc.finish();
568
569        let items: Vec<_> = OptionDecoder::new(&buf[..len])
570            .collect::<Result<_, _>>()
571            .unwrap();
572        assert_eq!(
573            items,
574            vec![(1, &[0x01u8][..]), (3, &[0x02][..]), (3, &[0x03][..])]
575        );
576    }
577
578    #[test]
579    fn encoder_with_last_number_continues_delta() {
580        // Pretend option 10 was already written; encode option 12 (delta=2).
581        let mut buf = [0u8; 8];
582        let mut enc = OptionEncoder::with_last_number(&mut buf, 10);
583        enc.put(12, &[0xBB]).unwrap();
584        let len = enc.finish();
585        // delta=2, len=1 → header=0x21, body=0xBB
586        assert_eq!(&buf[..len], &[0x21, 0xBB]);
587    }
588
589    #[test]
590    fn encoder_large_option_number_round_trip() {
591        let mut buf = [0u8; 16];
592        let mut enc = OptionEncoder::new(&mut buf);
593        enc.put(u16::MAX, &[0xCC]).unwrap();
594        let len = enc.finish();
595        let (num, val) = OptionDecoder::new(&buf[..len]).next().unwrap().unwrap();
596        assert_eq!(num, u16::MAX);
597        assert_eq!(val, &[0xCC]);
598    }
599
600    // ── OptionEncoder: error paths ────────────────────────────────────────────
601
602    #[test]
603    fn encoder_out_of_order_returns_error() {
604        let mut buf = [0u8; 16];
605        let mut enc = OptionEncoder::new(&mut buf);
606        enc.put(5, &[]).unwrap();
607        assert_eq!(enc.put(3, &[]), Err(EncodeError::OptionOutOfOrder));
608    }
609
610    #[test]
611    fn encoder_buffer_too_small_returns_error() {
612        let mut buf = [0u8; 1]; // only 1 byte: enough for header but not body
613        let mut enc = OptionEncoder::new(&mut buf);
614        assert_eq!(enc.put(0, &[0x01]), Err(EncodeError::BufferTooSmall));
615    }
616
617    #[test]
618    fn encoder_end_marker_buffer_too_small() {
619        let mut buf = [0u8; 0];
620        let mut enc = OptionEncoder::new(&mut buf);
621        assert_eq!(enc.end_marker(), Err(EncodeError::BufferTooSmall));
622    }
623
624    // ── OptionDecoder: basic iteration ────────────────────────────────────────
625
626    #[test]
627    fn decoder_empty_input_yields_nothing() {
628        assert!(OptionDecoder::new(&[]).next().is_none());
629    }
630
631    #[test]
632    fn decoder_end_marker_only_yields_nothing() {
633        let mut dec = OptionDecoder::new(&[0xFF]);
634        assert!(dec.next().is_none());
635        assert_eq!(dec.remainder(), &[] as &[u8]);
636    }
637
638    #[test]
639    fn decoder_remainder_after_end_marker() {
640        // Encode one option, write end marker, append trailing bytes manually.
641        let mut buf = [0u8; 16];
642        let mut enc = OptionEncoder::new(&mut buf);
643        enc.put(1, &[0xAA]).unwrap();
644        enc.end_marker().unwrap();
645        let opt_len = enc.finish();
646        buf[opt_len] = 0xDE;
647        buf[opt_len + 1] = 0xAD;
648
649        let mut dec = OptionDecoder::new(&buf[..opt_len + 2]);
650        let _ = dec.next().unwrap().unwrap(); // consume the option
651        assert!(dec.next().is_none()); // end marker stops iteration
652        assert_eq!(dec.remainder(), &[0xDE, 0xAD]);
653    }
654
655    #[test]
656    fn decoder_remainder_empty_without_end_marker() {
657        let mut buf = [0u8; 8];
658        let mut enc = OptionEncoder::new(&mut buf);
659        enc.put(1, &[0x01]).unwrap();
660        let len = enc.finish();
661
662        let mut dec = OptionDecoder::new(&buf[..len]);
663        let _ = dec.next().unwrap().unwrap();
664        assert!(dec.next().is_none());
665        // No end marker was written, so remainder is empty even after exhaustion.
666        assert_eq!(dec.remainder(), &[] as &[u8]);
667    }
668
669    #[test]
670    fn decoder_remainder_empty_before_exhausted() {
671        // remainder() returns empty while the decoder hasn't finished yet.
672        let mut buf = [0u8; 8];
673        let mut enc = OptionEncoder::new(&mut buf);
674        enc.put(1, &[0x01]).unwrap();
675        enc.end_marker().unwrap();
676        let len = enc.finish();
677
678        let dec = OptionDecoder::new(&buf[..len]);
679        // Haven't called next() yet — not finished.
680        assert_eq!(dec.remainder(), &[] as &[u8]);
681    }
682
683    // ── OptionDecoder: error paths ────────────────────────────────────────────
684
685    #[test]
686    fn decoder_truncated_value_returns_error() {
687        // Header says value is 3 bytes long, but only 1 byte follows.
688        let data = [0x03, 0xAB]; // delta=0, len=3, but only 1 value byte
689        let mut dec = OptionDecoder::new(&data);
690        assert!(matches!(dec.next(), Some(Err(ParseError::Truncated))));
691        assert!(dec.next().is_none()); // error stops further iteration
692    }
693
694    #[test]
695    fn decoder_truncated_extended_delta_returns_error() {
696        // Nibble 13 means 1 extended delta byte follows, but the buffer is empty after header.
697        let data = [0xD0]; // delta_nibble=13 but no ext byte
698        let mut dec = OptionDecoder::new(&data);
699        assert!(matches!(dec.next(), Some(Err(ParseError::Truncated))));
700        assert!(dec.next().is_none());
701    }
702
703    #[test]
704    fn decoder_invalid_nibble_returns_error() {
705        // Nibble value 15 (0xF) is reserved/invalid.
706        let data = [0xF0]; // delta_nibble=15
707        let mut dec = OptionDecoder::new(&data);
708        assert!(matches!(
709            dec.next(),
710            Some(Err(ParseError::InvalidOptionNibble))
711        ));
712        assert!(dec.next().is_none());
713    }
714
715    #[test]
716    fn decoder_option_number_overflow_returns_error() {
717        // Two options: first at u16::MAX, second with delta=1 → overflow.
718        let mut buf = [0u8; 16];
719        let mut enc = OptionEncoder::new(&mut buf);
720        enc.put(u16::MAX, &[]).unwrap();
721        let len = enc.finish();
722
723        // Manually append another option with delta=1 encoded inline.
724        buf[len] = 0x10; // delta_nibble=1, len_nibble=0
725        let mut dec = OptionDecoder::new(&buf[..len + 1]);
726        let _ = dec.next().unwrap().unwrap(); // u16::MAX decoded OK
727        assert!(matches!(dec.next(), Some(Err(ParseError::MalformedOption))));
728        assert!(dec.next().is_none());
729    }
730}