Skip to main content

dlep_core/
codec.rs

1//! Byte-level encoding and decoding primitives.
2//!
3//! These helpers live in `dlep-core` so that any crate can serialize or parse
4//! DLEP wire bytes without dragging in tokio. The async-friendly
5//! `tokio_util::codec::{Decoder, Encoder}` wrappers live in `dlep-net`.
6//!
7//! All multi-byte integers are big-endian (network byte order). UTF-8 strings
8//! inside Data Items have no length prefix and no NUL terminator; their length
9//! is governed by the surrounding TLV header. Insertion order of Data Items is
10//! preserved on encode and is decode-stable.
11
12use std::net::{Ipv4Addr, Ipv6Addr};
13use std::time::Duration;
14
15use bytes::{Buf, BufMut, Bytes, BytesMut};
16use ipnet::{Ipv4Net, Ipv6Net};
17
18use crate::data_item::{ConnectionPointFlags, DataItem, PeerFlags, RawDataItem};
19use crate::error::{CodecError, ExpectedLen};
20use crate::ids::{DataItemType, ExtensionId, MessageType, SignalType};
21use crate::mac::MacAddress;
22use crate::message::Message;
23use crate::signal::Signal;
24use crate::status::StatusCode;
25use crate::{MIN_HEARTBEAT_INTERVAL_MS, SIGNAL_PREFIX};
26
27/// Length of the fixed-size signal header: `"DLEP" (4) || type (2) || length (2)`.
28pub const SIGNAL_HEADER_LEN: usize = 8;
29
30/// Length of the fixed-size message header: `type (2) || length (2)`.
31pub const MESSAGE_HEADER_LEN: usize = 4;
32
33/// Field-name labels used in `CodecError::OutOfRange`. Keeps the literals in
34/// one place so a rename touches one site, not ten, and a typo at a call site
35/// becomes a compile error rather than a silent diagnostic regression.
36mod field {
37    pub const DATA_ITEM_FLAGS: &str = "data_item_flags";
38    pub const HEARTBEAT_INTERVAL_MS: &str = "heartbeat_interval_ms";
39    pub const LATENCY_US: &str = "latency_us";
40    pub const RESOURCES: &str = "resources";
41    pub const RLQ_RECEIVE: &str = "relative_link_quality_receive";
42    pub const RLQ_TRANSMIT: &str = "relative_link_quality_transmit";
43    pub const IPV4_ATTACHED_SUBNET_PREFIX: &str = "ipv4_attached_subnet_prefix";
44    pub const IPV6_ATTACHED_SUBNET_PREFIX: &str = "ipv6_attached_subnet_prefix";
45    pub const DATA_ITEM_VALUE_LENGTH: &str = "data_item_value_length";
46    pub const SIGNAL_BODY_LENGTH: &str = "signal_body_length";
47    pub const MESSAGE_BODY_LENGTH: &str = "message_body_length";
48}
49
50impl RawDataItem {
51    /// Encode this raw TLV onto `out`. Validates the value length upfront —
52    /// extension/forward-compat callers using `DataItem::Unknown(raw)` must
53    /// not silently truncate when `value.len() > u16::MAX`.
54    pub fn encode(&self, out: &mut BytesMut) -> Result<(), CodecError> {
55        let len = u16_length(field::DATA_ITEM_VALUE_LENGTH, self.value.len())?;
56        out.put_u16(self.type_id.0);
57        out.put_u16(len);
58        out.put_slice(&self.value);
59        Ok(())
60    }
61
62    pub fn decode(src: &mut Bytes) -> Result<Self, CodecError> {
63        ensure_len(src.remaining(), 4)?;
64        let type_id = DataItemType(src.get_u16());
65        let len = src.get_u16() as usize;
66        ensure_len(src.remaining(), len)?;
67        let value = src.split_to(len);
68        Ok(RawDataItem { type_id, value })
69    }
70}
71
72impl DataItem {
73    /// Encode this typed Data Item — including its 4-byte TLV header —
74    /// onto `out`. Writes directly into the caller's buffer; on error `out`
75    /// is restored to its starting length so the caller can recover.
76    ///
77    /// Returns an error for values that cannot be expressed on the wire:
78    ///
79    /// * `Resources` / `RelativeLinkQuality{Receive,Transmit}` outside `0..=100`
80    ///   (per RFC 8175 §13.17–§13.19)
81    /// * `HeartbeatInterval` whose milliseconds exceed `u32::MAX`
82    /// * `Latency` whose microseconds exceed `u64::MAX`
83    ///
84    /// `Ipv4AttachedSubnet` / `Ipv6AttachedSubnet` always emit the **network
85    /// address** (host bits truncated to the prefix), per RFC 8175 §13.10–§13.11.
86    /// `decode` symmetrically truncates non-canonical input, so a peer that
87    /// puts host bits on the wire is normalized in memory rather than silently
88    /// re-canonicalized on the next encode.
89    pub fn encode(&self, out: &mut BytesMut) -> Result<(), CodecError> {
90        let restore_to = out.len();
91        match self.encode_into(out) {
92            Ok(()) => Ok(()),
93            Err(e) => {
94                out.truncate(restore_to);
95                Err(e)
96            }
97        }
98    }
99
100    fn encode_into(&self, out: &mut BytesMut) -> Result<(), CodecError> {
101        if let DataItem::Unknown(raw) = self {
102            return raw.encode(out);
103        }
104
105        // For variable-length variants, project the wire length up-front so
106        // an oversized payload errors immediately instead of allocating
107        // multi-MB into `out` only to roll back via `truncate` on the
108        // post-write `u16_length` check.
109        if let Some(projected) = self.projected_value_len() {
110            u16_length(field::DATA_ITEM_VALUE_LENGTH, projected)?;
111        }
112
113        // Avoids a temp `BytesMut` per item: write the type, leave the length
114        // as a placeholder, append the value bytes, then patch the placeholder.
115        out.put_u16(self.type_id().0);
116        let len_pos = out.len();
117        out.put_u16(0);
118        let value_start = out.len();
119
120        match self {
121            DataItem::Status { code, text } => {
122                out.put_u8(code.0);
123                out.put_slice(text.as_bytes());
124            }
125            DataItem::Ipv4ConnectionPoint { flags, addr, port } => {
126                out.put_u8(encode_cp_flags(*flags));
127                out.put_slice(&addr.octets());
128                if let Some(p) = port {
129                    out.put_u16(*p);
130                }
131            }
132            DataItem::Ipv6ConnectionPoint { flags, addr, port } => {
133                out.put_u8(encode_cp_flags(*flags));
134                out.put_slice(&addr.octets());
135                if let Some(p) = port {
136                    out.put_u16(*p);
137                }
138            }
139            DataItem::PeerType { flags, description } => {
140                out.put_u8(encode_peer_flags(*flags));
141                out.put_slice(description.as_bytes());
142            }
143            DataItem::HeartbeatInterval(d) => {
144                let ms = d.as_millis();
145                if ms < MIN_HEARTBEAT_INTERVAL_MS as u128 {
146                    return Err(CodecError::OutOfRange {
147                        field: field::HEARTBEAT_INTERVAL_MS,
148                        value: u64::try_from(ms).unwrap_or(u64::MAX),
149                    });
150                }
151                if ms > u32::MAX as u128 {
152                    return Err(CodecError::OutOfRange {
153                        field: field::HEARTBEAT_INTERVAL_MS,
154                        value: u64::try_from(ms).unwrap_or(u64::MAX),
155                    });
156                }
157                out.put_u32(ms as u32);
158            }
159            DataItem::ExtensionsSupported(ids) => {
160                for id in ids {
161                    out.put_u16(id.0);
162                }
163            }
164            DataItem::MacAddress(mac) => {
165                // RFC 8175 §13.7: 6 octets for EUI-48, 8 for EUI-64.
166                out.put_slice(mac.as_bytes());
167            }
168            DataItem::Ipv4Address { add, addr } => {
169                out.put_u8(u8::from(*add));
170                out.put_slice(&addr.octets());
171            }
172            DataItem::Ipv6Address { add, addr } => {
173                out.put_u8(u8::from(*add));
174                out.put_slice(&addr.octets());
175            }
176            DataItem::Ipv4AttachedSubnet { add, subnet } => {
177                out.put_u8(u8::from(*add));
178                out.put_slice(&subnet.network().octets());
179                out.put_u8(subnet.prefix_len());
180            }
181            DataItem::Ipv6AttachedSubnet { add, subnet } => {
182                out.put_u8(u8::from(*add));
183                out.put_slice(&subnet.network().octets());
184                out.put_u8(subnet.prefix_len());
185            }
186            DataItem::MaxDataRateReceive(bps)
187            | DataItem::MaxDataRateTransmit(bps)
188            | DataItem::CurrentDataRateReceive(bps)
189            | DataItem::CurrentDataRateTransmit(bps) => {
190                out.put_u64(*bps);
191            }
192            DataItem::Latency(d) => {
193                let us = d.as_micros();
194                if us > u64::MAX as u128 {
195                    // Threshold equals u64::MAX, so any overflow is by
196                    // definition unrepresentable as u64 — there is no
197                    // informative actual value to surface (unlike
198                    // `HeartbeatInterval`, whose threshold is u32::MAX so
199                    // the actual ms can still fit in the report).
200                    return Err(CodecError::OutOfRange {
201                        field: field::LATENCY_US,
202                        value: u64::MAX,
203                    });
204                }
205                out.put_u64(us as u64);
206            }
207            DataItem::Resources(pct) => {
208                check_percent(field::RESOURCES, *pct)?;
209                out.put_u8(*pct);
210            }
211            DataItem::RelativeLinkQualityReceive(pct) => {
212                check_percent(field::RLQ_RECEIVE, *pct)?;
213                out.put_u8(*pct);
214            }
215            DataItem::RelativeLinkQualityTransmit(pct) => {
216                check_percent(field::RLQ_TRANSMIT, *pct)?;
217                out.put_u8(*pct);
218            }
219            DataItem::Mtu(mtu) => {
220                out.put_u16(*mtu);
221            }
222            DataItem::Unknown(_) => unreachable!("Unknown handled by early return"),
223        }
224
225        let value_len = out.len() - value_start;
226        let len_u16 = u16_length(field::DATA_ITEM_VALUE_LENGTH, value_len)?;
227        out[len_pos..len_pos + 2].copy_from_slice(&len_u16.to_be_bytes());
228        Ok(())
229    }
230
231    /// Projected wire length for variants whose value bytes are dominated by
232    /// caller-controlled data: text strings or `Vec`s. Returns `None` for
233    /// fixed- or small-bounded-length variants where the post-write
234    /// `u16_length` check is already O(1) cheap. `saturating_mul` guards
235    /// against pathological `usize` overflow on 32-bit targets — the
236    /// `u16_length` check then catches the saturated value as out-of-range.
237    fn projected_value_len(&self) -> Option<usize> {
238        match self {
239            DataItem::Status { text, .. } => Some(1usize.saturating_add(text.len())),
240            DataItem::PeerType { description, .. } => {
241                Some(1usize.saturating_add(description.len()))
242            }
243            DataItem::ExtensionsSupported(ids) => Some(ids.len().saturating_mul(2)),
244            _ => None,
245        }
246    }
247
248    /// Lift a parsed `RawDataItem` into a typed `DataItem`. Unknown type ids
249    /// fall through to `DataItem::Unknown` for negotiated extension dispatch.
250    /// The session layer rejects unclaimed items under RFC 8175 §12.1.
251    /// Length errors and value-range errors are reported here.
252    ///
253    /// `Ipv4AttachedSubnet` / `Ipv6AttachedSubnet` are normalized to network
254    /// form here — host bits in non-canonical wire input are zeroed so the
255    /// in-memory representation matches what `encode` would produce, and
256    /// equality comparisons across peer-sent subnets are stable.
257    pub fn decode(raw: RawDataItem) -> Result<Self, CodecError> {
258        let kind = raw.type_id;
259        let len = raw.value.len();
260        let v = &raw.value[..];
261
262        match kind {
263            DataItemType::STATUS => {
264                if len < 1 {
265                    return Err(CodecError::InvalidDataItemLength {
266                        kind,
267                        expected: ExpectedLen::AtLeast(1),
268                        got: len,
269                    });
270                }
271                let code = StatusCode(v[0]);
272                let text = String::from_utf8(v[1..].to_vec())?;
273                Ok(DataItem::Status { code, text })
274            }
275            DataItemType::IPV4_CONNECTION_POINT => {
276                if len != 5 && len != 7 {
277                    return Err(CodecError::InvalidDataItemLength {
278                        kind,
279                        expected: ExpectedLen::OneOf(&[5, 7]),
280                        got: len,
281                    });
282                }
283                validate_flags(v[0])?;
284                let flags = decode_cp_flags(v[0]);
285                let addr = Ipv4Addr::new(v[1], v[2], v[3], v[4]);
286                let port = (len == 7).then(|| u16::from_be_bytes([v[5], v[6]]));
287                Ok(DataItem::Ipv4ConnectionPoint { flags, addr, port })
288            }
289            DataItemType::IPV6_CONNECTION_POINT => {
290                if len != 17 && len != 19 {
291                    return Err(CodecError::InvalidDataItemLength {
292                        kind,
293                        expected: ExpectedLen::OneOf(&[17, 19]),
294                        got: len,
295                    });
296                }
297                validate_flags(v[0])?;
298                let flags = decode_cp_flags(v[0]);
299                let mut octets = [0u8; 16];
300                octets.copy_from_slice(&v[1..17]);
301                let addr = Ipv6Addr::from(octets);
302                let port = (len == 19).then(|| u16::from_be_bytes([v[17], v[18]]));
303                Ok(DataItem::Ipv6ConnectionPoint { flags, addr, port })
304            }
305            DataItemType::PEER_TYPE => {
306                if len < 1 {
307                    return Err(CodecError::InvalidDataItemLength {
308                        kind,
309                        expected: ExpectedLen::AtLeast(1),
310                        got: len,
311                    });
312                }
313                validate_flags(v[0])?;
314                let flags = decode_peer_flags(v[0]);
315                let description = String::from_utf8(v[1..].to_vec())?;
316                Ok(DataItem::PeerType { flags, description })
317            }
318            DataItemType::HEARTBEAT_INTERVAL => {
319                expect_exact(kind, len, 4)?;
320                let ms = u32::from_be_bytes([v[0], v[1], v[2], v[3]]);
321                if ms < MIN_HEARTBEAT_INTERVAL_MS {
322                    return Err(CodecError::OutOfRange {
323                        field: field::HEARTBEAT_INTERVAL_MS,
324                        value: ms.into(),
325                    });
326                }
327                Ok(DataItem::HeartbeatInterval(Duration::from_millis(
328                    ms.into(),
329                )))
330            }
331            DataItemType::EXTENSIONS_SUPPORTED => {
332                if len % 2 != 0 {
333                    return Err(CodecError::InvalidDataItemLength {
334                        kind,
335                        expected: ExpectedLen::Multiple(2),
336                        got: len,
337                    });
338                }
339                let mut ids = Vec::with_capacity(len / 2);
340                let mut i = 0;
341                while i < len {
342                    ids.push(ExtensionId(u16::from_be_bytes([v[i], v[i + 1]])));
343                    i += 2;
344                }
345                Ok(DataItem::ExtensionsSupported(ids))
346            }
347            DataItemType::MAC_ADDRESS => {
348                // RFC 8175 §13.7: "Length: 6 for EUI-48 format or 8 for
349                // EUI-64 format." All destination MACs in a single session
350                // MUST share one format (consistency check belongs at the
351                // session layer, not the codec).
352                let mac = match len {
353                    6 => {
354                        let mut octets = [0u8; 6];
355                        octets.copy_from_slice(&v[..6]);
356                        MacAddress::Eui48(octets)
357                    }
358                    8 => {
359                        let mut octets = [0u8; 8];
360                        octets.copy_from_slice(&v[..8]);
361                        MacAddress::Eui64(octets)
362                    }
363                    _ => {
364                        return Err(CodecError::InvalidDataItemLength {
365                            kind,
366                            expected: ExpectedLen::OneOf(&[6, 8]),
367                            got: len,
368                        });
369                    }
370                };
371                Ok(DataItem::MacAddress(mac))
372            }
373            DataItemType::IPV4_ADDRESS => {
374                expect_exact(kind, len, 5)?;
375                validate_flags(v[0])?;
376                let add = (v[0] & 0x01) != 0;
377                let addr = Ipv4Addr::new(v[1], v[2], v[3], v[4]);
378                Ok(DataItem::Ipv4Address { add, addr })
379            }
380            DataItemType::IPV6_ADDRESS => {
381                expect_exact(kind, len, 17)?;
382                validate_flags(v[0])?;
383                let add = (v[0] & 0x01) != 0;
384                let mut octets = [0u8; 16];
385                octets.copy_from_slice(&v[1..17]);
386                Ok(DataItem::Ipv6Address {
387                    add,
388                    addr: Ipv6Addr::from(octets),
389                })
390            }
391            DataItemType::IPV4_ATTACHED_SUBNET => {
392                expect_exact(kind, len, 6)?;
393                validate_flags(v[0])?;
394                let add = (v[0] & 0x01) != 0;
395                let addr = Ipv4Addr::new(v[1], v[2], v[3], v[4]);
396                let prefix = v[5];
397                // Normalize host bits to zero so encode/decode are symmetric:
398                // a peer that sends `10.0.0.5/24` and one that sends
399                // `10.0.0.0/24` produce identical in-memory subnets, and
400                // re-encoding never silently changes the value.
401                let subnet = Ipv4Net::new(addr, prefix)
402                    .map_err(|_| CodecError::OutOfRange {
403                        field: field::IPV4_ATTACHED_SUBNET_PREFIX,
404                        value: prefix as u64,
405                    })?
406                    .trunc();
407                Ok(DataItem::Ipv4AttachedSubnet { add, subnet })
408            }
409            DataItemType::IPV6_ATTACHED_SUBNET => {
410                expect_exact(kind, len, 18)?;
411                validate_flags(v[0])?;
412                let add = (v[0] & 0x01) != 0;
413                let mut octets = [0u8; 16];
414                octets.copy_from_slice(&v[1..17]);
415                let prefix = v[17];
416                let subnet = Ipv6Net::new(Ipv6Addr::from(octets), prefix)
417                    .map_err(|_| CodecError::OutOfRange {
418                        field: field::IPV6_ATTACHED_SUBNET_PREFIX,
419                        value: prefix as u64,
420                    })?
421                    .trunc();
422                Ok(DataItem::Ipv6AttachedSubnet { add, subnet })
423            }
424            DataItemType::MAXIMUM_DATA_RATE_RECEIVE => {
425                expect_exact(kind, len, 8)?;
426                Ok(DataItem::MaxDataRateReceive(read_u64_be(v)))
427            }
428            DataItemType::MAXIMUM_DATA_RATE_TRANSMIT => {
429                expect_exact(kind, len, 8)?;
430                Ok(DataItem::MaxDataRateTransmit(read_u64_be(v)))
431            }
432            DataItemType::CURRENT_DATA_RATE_RECEIVE => {
433                expect_exact(kind, len, 8)?;
434                Ok(DataItem::CurrentDataRateReceive(read_u64_be(v)))
435            }
436            DataItemType::CURRENT_DATA_RATE_TRANSMIT => {
437                expect_exact(kind, len, 8)?;
438                Ok(DataItem::CurrentDataRateTransmit(read_u64_be(v)))
439            }
440            DataItemType::LATENCY => {
441                expect_exact(kind, len, 8)?;
442                Ok(DataItem::Latency(Duration::from_micros(read_u64_be(v))))
443            }
444            DataItemType::RESOURCES => Ok(DataItem::Resources(decode_percent(
445                kind,
446                v,
447                field::RESOURCES,
448            )?)),
449            DataItemType::RELATIVE_LINK_QUALITY_RECEIVE => Ok(
450                DataItem::RelativeLinkQualityReceive(decode_percent(kind, v, field::RLQ_RECEIVE)?),
451            ),
452            DataItemType::RELATIVE_LINK_QUALITY_TRANSMIT => {
453                Ok(DataItem::RelativeLinkQualityTransmit(decode_percent(
454                    kind,
455                    v,
456                    field::RLQ_TRANSMIT,
457                )?))
458            }
459            DataItemType::MTU => {
460                expect_exact(kind, len, 2)?;
461                Ok(DataItem::Mtu(u16::from_be_bytes([v[0], v[1]])))
462            }
463            _ => Ok(DataItem::Unknown(raw)),
464        }
465    }
466}
467
468impl Signal {
469    /// Encode this signal into a fresh `BytesMut`. Returns an error if any
470    /// contained `DataItem` cannot be expressed on the wire (see
471    /// [`DataItem::encode`] for which values can fail).
472    pub fn encode(&self) -> Result<BytesMut, CodecError> {
473        let mut body = BytesMut::new();
474        for item in &self.data_items {
475            item.encode(&mut body)?;
476        }
477        let body_len = u16_length(field::SIGNAL_BODY_LENGTH, body.len())?;
478        let mut out = BytesMut::with_capacity(SIGNAL_HEADER_LEN + body.len());
479        out.put_slice(SIGNAL_PREFIX);
480        out.put_u16(self.signal_type.0);
481        out.put_u16(body_len);
482        out.put(body);
483        Ok(out)
484    }
485
486    pub fn decode(mut src: Bytes) -> Result<Self, CodecError> {
487        ensure_len(src.remaining(), SIGNAL_HEADER_LEN)?;
488        let mut prefix = [0u8; 4];
489        src.copy_to_slice(&mut prefix);
490        if &prefix != SIGNAL_PREFIX {
491            return Err(CodecError::MissingSignalPrefix);
492        }
493        let signal_type = SignalType(src.get_u16());
494        let declared = src.get_u16() as usize;
495        if src.remaining() != declared {
496            return Err(CodecError::LengthMismatch {
497                declared,
498                remaining: src.remaining(),
499            });
500        }
501        let mut body = src.split_to(declared);
502        let mut data_items = Vec::new();
503        while body.has_remaining() {
504            let raw = RawDataItem::decode(&mut body)?;
505            data_items.push(DataItem::decode(raw)?);
506        }
507        Ok(Signal {
508            signal_type,
509            data_items,
510        })
511    }
512}
513
514impl Message {
515    /// Encode this message into a fresh `BytesMut`. Returns an error if any
516    /// contained `DataItem` cannot be expressed on the wire (see
517    /// [`DataItem::encode`] for which values can fail).
518    pub fn encode(&self) -> Result<BytesMut, CodecError> {
519        let mut body = BytesMut::new();
520        for item in &self.data_items {
521            item.encode(&mut body)?;
522        }
523        let body_len = u16_length(field::MESSAGE_BODY_LENGTH, body.len())?;
524        let mut out = BytesMut::with_capacity(MESSAGE_HEADER_LEN + body.len());
525        out.put_u16(self.message_type.0);
526        out.put_u16(body_len);
527        out.put(body);
528        Ok(out)
529    }
530
531    pub fn decode(mut src: Bytes) -> Result<Self, CodecError> {
532        ensure_len(src.remaining(), MESSAGE_HEADER_LEN)?;
533        let message_type = MessageType(src.get_u16());
534        let declared = src.get_u16() as usize;
535        if src.remaining() < declared {
536            return Err(CodecError::LengthMismatch {
537                declared,
538                remaining: src.remaining(),
539            });
540        }
541        let mut body = src.split_to(declared);
542        let mut data_items = Vec::new();
543        while body.has_remaining() {
544            let raw = RawDataItem::decode(&mut body)?;
545            data_items.push(DataItem::decode(raw)?);
546        }
547        Ok(Message {
548            message_type,
549            data_items,
550        })
551    }
552}
553
554fn ensure_len(have: usize, needed: usize) -> Result<(), CodecError> {
555    if have < needed {
556        Err(CodecError::Truncated { needed, have })
557    } else {
558        Ok(())
559    }
560}
561
562fn expect_exact(kind: DataItemType, got: usize, expected: usize) -> Result<(), CodecError> {
563    if got == expected {
564        Ok(())
565    } else {
566        Err(CodecError::InvalidDataItemLength {
567            kind,
568            expected: ExpectedLen::Exact(expected),
569            got,
570        })
571    }
572}
573
574fn read_u64_be(value: &[u8]) -> u64 {
575    let mut buf = [0u8; 8];
576    buf.copy_from_slice(&value[..8]);
577    u64::from_be_bytes(buf)
578}
579
580// All single-bit flag fields in RFC 8175 §13 (T in §13.2/§13.3, S in §13.4,
581// A in §13.8/§13.9/§13.10/§13.11) sit at the **rightmost** column of the
582// 8-bit IETF wire diagram — i.e. bit 7 in IETF numbering, the LSB of the
583// octet, mask `0x01`. The leftmost seven bits are Reserved (MUST be zero).
584// Encoding the named flag at any other position would set a Reserved bit and
585// place the flag where peers expect zero.
586
587fn validate_flags(byte: u8) -> Result<(), CodecError> {
588    if byte & 0xfe != 0 {
589        return Err(CodecError::OutOfRange {
590            field: field::DATA_ITEM_FLAGS,
591            value: byte.into(),
592        });
593    }
594    Ok(())
595}
596
597fn encode_cp_flags(flags: ConnectionPointFlags) -> u8 {
598    u8::from(flags.use_tls)
599}
600
601fn decode_cp_flags(byte: u8) -> ConnectionPointFlags {
602    ConnectionPointFlags {
603        use_tls: (byte & 0x01) != 0,
604    }
605}
606
607fn encode_peer_flags(flags: PeerFlags) -> u8 {
608    u8::from(flags.smi)
609}
610
611fn decode_peer_flags(byte: u8) -> PeerFlags {
612    PeerFlags {
613        smi: (byte & 0x01) != 0,
614    }
615}
616
617fn check_percent(field: &'static str, pct: u8) -> Result<(), CodecError> {
618    if pct > 100 {
619        Err(CodecError::OutOfRange {
620            field,
621            value: pct as u64,
622        })
623    } else {
624        Ok(())
625    }
626}
627
628/// Decode the body of a percentage Data Item — Resources, Relative Link
629/// Quality Receive/Transmit. All three share the same wire shape (single
630/// `u8` in `0..=100`) and only differ in the field name surfaced on error.
631fn decode_percent(kind: DataItemType, value: &[u8], field: &'static str) -> Result<u8, CodecError> {
632    expect_exact(kind, value.len(), 1)?;
633    let pct = value[0];
634    check_percent(field, pct)?;
635    Ok(pct)
636}
637
638/// Convert a buffer length to the `u16` width used by every DLEP TLV
639/// length field. Returns `OutOfRange` rather than silently truncating, which
640/// would produce a corrupt frame.
641fn u16_length(field: &'static str, len: usize) -> Result<u16, CodecError> {
642    u16::try_from(len).map_err(|_| CodecError::OutOfRange {
643        field,
644        value: len as u64,
645    })
646}
647
648#[cfg(test)]
649mod tests {
650    use super::*;
651
652    fn encode_one(item: DataItem) -> Vec<u8> {
653        let mut buf = BytesMut::new();
654        item.encode(&mut buf).expect("encode should not fail");
655        buf.to_vec()
656    }
657
658    fn decode_one(bytes: &[u8]) -> DataItem {
659        let mut b = Bytes::copy_from_slice(bytes);
660        let raw = RawDataItem::decode(&mut b).unwrap();
661        DataItem::decode(raw).unwrap()
662    }
663
664    #[test]
665    fn empty_message_roundtrips() {
666        let m = Message::new(MessageType::HEARTBEAT);
667        let bytes = m.encode().unwrap().freeze();
668        let decoded = Message::decode(bytes).unwrap();
669        assert_eq!(decoded.message_type, MessageType::HEARTBEAT);
670        assert!(decoded.data_items.is_empty());
671    }
672
673    #[test]
674    fn empty_signal_roundtrips() {
675        let s = Signal::new(SignalType::PEER_DISCOVERY);
676        let bytes = s.encode().unwrap().freeze();
677        let decoded = Signal::decode(bytes).unwrap();
678        assert_eq!(decoded.signal_type, SignalType::PEER_DISCOVERY);
679    }
680
681    #[test]
682    fn signal_rejects_bad_prefix() {
683        let mut bad = BytesMut::from(&b"XLEP"[..]);
684        bad.put_u16(1);
685        bad.put_u16(0);
686        let err = Signal::decode(bad.freeze()).unwrap_err();
687        assert!(matches!(err, CodecError::MissingSignalPrefix));
688    }
689
690    // --- Per-variant byte-vector tests ---
691
692    #[test]
693    fn status_encodes_with_text() {
694        let item = DataItem::Status {
695            code: StatusCode::SUCCESS,
696            text: "ok".into(),
697        };
698        // type=1, length=3, value=00 'o' 'k'
699        assert_eq!(
700            encode_one(item),
701            vec![0x00, 0x01, 0x00, 0x03, 0x00, b'o', b'k']
702        );
703    }
704
705    #[test]
706    fn status_roundtrips_terminate_code() {
707        let item = DataItem::Status {
708            code: StatusCode::TIMED_OUT,
709            text: "deadline".into(),
710        };
711        let bytes = encode_one(item);
712        let DataItem::Status { code, text } = decode_one(&bytes) else {
713            panic!("wrong variant")
714        };
715        assert_eq!(code, StatusCode::TIMED_OUT);
716        assert_eq!(text, "deadline");
717    }
718
719    #[test]
720    fn status_with_empty_text_roundtrips() {
721        let item = DataItem::Status {
722            code: StatusCode::SUCCESS,
723            text: String::new(),
724        };
725        let bytes = encode_one(item);
726        // type=1, length=1, value=00
727        assert_eq!(bytes, vec![0x00, 0x01, 0x00, 0x01, 0x00]);
728        let DataItem::Status { code, text } = decode_one(&bytes) else {
729            panic!()
730        };
731        assert_eq!(code, StatusCode::SUCCESS);
732        assert!(text.is_empty());
733    }
734
735    #[test]
736    fn status_with_zero_length_value_rejected() {
737        let raw = RawDataItem {
738            type_id: DataItemType::STATUS,
739            value: Bytes::new(),
740        };
741        let err = DataItem::decode(raw).unwrap_err();
742        assert!(matches!(
743            err,
744            CodecError::InvalidDataItemLength {
745                kind: DataItemType::STATUS,
746                ..
747            }
748        ));
749    }
750
751    #[test]
752    fn ipv4_connection_point_with_port_encodes() {
753        let item = DataItem::Ipv4ConnectionPoint {
754            flags: ConnectionPointFlags { use_tls: true },
755            addr: Ipv4Addr::new(10, 0, 0, 1),
756            port: Some(854),
757        };
758        // type=2, length=7, flags=01, addr=10.0.0.1, port=854 (0x0356)
759        assert_eq!(
760            encode_one(item),
761            vec![0x00, 0x02, 0x00, 0x07, 0x01, 10, 0, 0, 1, 0x03, 0x56]
762        );
763    }
764
765    #[test]
766    fn ipv4_connection_point_length_between_valid_forms_rejected() {
767        // Valid lengths are 5 (no port) or 7 (with port). Length 6 is illegal
768        // — but a strict "expected: 5" message would be misleading. Confirm
769        // we report `OneOf([5, 7])`.
770        let raw = RawDataItem {
771            type_id: DataItemType::IPV4_CONNECTION_POINT,
772            value: Bytes::from_static(&[0, 1, 2, 3, 4, 5]),
773        };
774        match DataItem::decode(raw).unwrap_err() {
775            CodecError::InvalidDataItemLength {
776                kind: DataItemType::IPV4_CONNECTION_POINT,
777                expected: ExpectedLen::OneOf(&[5, 7]),
778                got: 6,
779            } => {}
780            other => panic!("unexpected error: {other:?}"),
781        }
782    }
783
784    #[test]
785    fn ipv6_connection_point_length_between_valid_forms_rejected() {
786        let raw = RawDataItem {
787            type_id: DataItemType::IPV6_CONNECTION_POINT,
788            value: Bytes::from_static(&[0u8; 18]),
789        };
790        match DataItem::decode(raw).unwrap_err() {
791            CodecError::InvalidDataItemLength {
792                kind: DataItemType::IPV6_CONNECTION_POINT,
793                expected: ExpectedLen::OneOf(&[17, 19]),
794                got: 18,
795            } => {}
796            other => panic!("unexpected error: {other:?}"),
797        }
798    }
799
800    #[test]
801    fn status_at_least_one_byte_error_carries_atleast() {
802        let raw = RawDataItem {
803            type_id: DataItemType::STATUS,
804            value: Bytes::new(),
805        };
806        match DataItem::decode(raw).unwrap_err() {
807            CodecError::InvalidDataItemLength {
808                kind: DataItemType::STATUS,
809                expected: ExpectedLen::AtLeast(1),
810                got: 0,
811            } => {}
812            other => panic!("unexpected error: {other:?}"),
813        }
814    }
815
816    #[test]
817    fn extensions_supported_odd_length_error_carries_multiple() {
818        let raw = RawDataItem {
819            type_id: DataItemType::EXTENSIONS_SUPPORTED,
820            value: Bytes::from_static(&[0x00, 0x01, 0x00]),
821        };
822        match DataItem::decode(raw).unwrap_err() {
823            CodecError::InvalidDataItemLength {
824                kind: DataItemType::EXTENSIONS_SUPPORTED,
825                expected: ExpectedLen::Multiple(2),
826                got: 3,
827            } => {}
828            other => panic!("unexpected error: {other:?}"),
829        }
830    }
831
832    #[test]
833    fn ipv4_connection_point_without_port_roundtrips() {
834        let item = DataItem::Ipv4ConnectionPoint {
835            flags: ConnectionPointFlags::default(),
836            addr: Ipv4Addr::new(192, 168, 1, 1),
837            port: None,
838        };
839        let bytes = encode_one(item);
840        assert_eq!(bytes.len(), 4 + 5);
841        let DataItem::Ipv4ConnectionPoint { flags, addr, port } = decode_one(&bytes) else {
842            panic!()
843        };
844        assert!(!flags.use_tls);
845        assert_eq!(addr, Ipv4Addr::new(192, 168, 1, 1));
846        assert_eq!(port, None);
847    }
848
849    #[test]
850    fn ipv6_connection_point_with_port_roundtrips() {
851        let item = DataItem::Ipv6ConnectionPoint {
852            flags: ConnectionPointFlags { use_tls: true },
853            addr: "fe80::1".parse().unwrap(),
854            port: Some(854),
855        };
856        let bytes = encode_one(item);
857        assert_eq!(bytes.len(), 4 + 19);
858        let DataItem::Ipv6ConnectionPoint { flags, addr, port } = decode_one(&bytes) else {
859            panic!()
860        };
861        assert!(flags.use_tls);
862        assert_eq!(addr, "fe80::1".parse::<Ipv6Addr>().unwrap());
863        assert_eq!(port, Some(854));
864    }
865
866    #[test]
867    fn ipv6_connection_point_without_port_roundtrips() {
868        let item = DataItem::Ipv6ConnectionPoint {
869            flags: ConnectionPointFlags::default(),
870            addr: Ipv6Addr::LOCALHOST,
871            port: None,
872        };
873        let bytes = encode_one(item);
874        assert_eq!(bytes.len(), 4 + 17);
875        let DataItem::Ipv6ConnectionPoint { port, .. } = decode_one(&bytes) else {
876            panic!()
877        };
878        assert_eq!(port, None);
879    }
880
881    #[test]
882    fn peer_type_encodes() {
883        let item = DataItem::PeerType {
884            flags: PeerFlags { smi: true },
885            description: "modem".into(),
886        };
887        // type=4, length=6, flags=01, "modem"
888        assert_eq!(
889            encode_one(item),
890            vec![0x00, 0x04, 0x00, 0x06, 0x01, b'm', b'o', b'd', b'e', b'm']
891        );
892    }
893
894    #[test]
895    fn peer_type_with_empty_description_roundtrips() {
896        let item = DataItem::PeerType {
897            flags: PeerFlags::default(),
898            description: String::new(),
899        };
900        let bytes = encode_one(item);
901        let DataItem::PeerType { flags, description } = decode_one(&bytes) else {
902            panic!()
903        };
904        assert!(!flags.smi);
905        assert!(description.is_empty());
906    }
907
908    #[test]
909    fn heartbeat_interval_encodes() {
910        let item = DataItem::HeartbeatInterval(Duration::from_millis(60_000));
911        // type=5, length=4, value=60000_u32_be (0x0000EA60)
912        assert_eq!(
913            encode_one(item),
914            vec![0x00, 0x05, 0x00, 0x04, 0x00, 0x00, 0xEA, 0x60]
915        );
916    }
917
918    #[test]
919    fn heartbeat_interval_below_rfc_minimum_rejected_on_encode() {
920        for ms in [0, MIN_HEARTBEAT_INTERVAL_MS - 1] {
921            let item = DataItem::HeartbeatInterval(Duration::from_millis(ms.into()));
922            let mut buf = BytesMut::new();
923            let err = item.encode(&mut buf).unwrap_err();
924            assert!(matches!(
925                err,
926                CodecError::OutOfRange {
927                    field: field::HEARTBEAT_INTERVAL_MS,
928                    ..
929                }
930            ));
931            assert!(buf.is_empty());
932        }
933    }
934
935    #[test]
936    fn heartbeat_interval_below_rfc_minimum_rejected_on_decode() {
937        for ms in [0, MIN_HEARTBEAT_INTERVAL_MS - 1] {
938            let raw = RawDataItem {
939                type_id: DataItemType::HEARTBEAT_INTERVAL,
940                value: Bytes::copy_from_slice(&ms.to_be_bytes()),
941            };
942            let err = DataItem::decode(raw).unwrap_err();
943            assert!(matches!(
944                err,
945                CodecError::OutOfRange {
946                    field: field::HEARTBEAT_INTERVAL_MS,
947                    ..
948                }
949            ));
950        }
951    }
952
953    #[test]
954    fn heartbeat_interval_overflow_rejected_on_encode() {
955        let item = DataItem::HeartbeatInterval(Duration::from_secs(u64::MAX / 1000));
956        let mut buf = BytesMut::new();
957        let err = item.encode(&mut buf).unwrap_err();
958        assert!(matches!(err, CodecError::OutOfRange { .. }));
959        // On encode failure the buffer is restored to its starting length.
960        assert!(buf.is_empty());
961    }
962
963    #[test]
964    fn heartbeat_interval_at_u32_max_encodes() {
965        // u32::MAX milliseconds is the largest value the wire format can carry.
966        let item = DataItem::HeartbeatInterval(Duration::from_millis(u32::MAX.into()));
967        let bytes = encode_one(item);
968        let DataItem::HeartbeatInterval(d) = decode_one(&bytes) else {
969            panic!()
970        };
971        assert_eq!(d, Duration::from_millis(u32::MAX.into()));
972    }
973
974    #[test]
975    fn extensions_supported_encodes_three_ids() {
976        let item = DataItem::ExtensionsSupported(vec![
977            ExtensionId(1),
978            ExtensionId(2),
979            ExtensionId(0xFFFF),
980        ]);
981        // type=6, length=6, value=0001 0002 FFFF
982        assert_eq!(
983            encode_one(item),
984            vec![0x00, 0x06, 0x00, 0x06, 0x00, 0x01, 0x00, 0x02, 0xFF, 0xFF]
985        );
986    }
987
988    #[test]
989    fn extensions_supported_empty_roundtrips() {
990        let item = DataItem::ExtensionsSupported(Vec::new());
991        let bytes = encode_one(item);
992        // type=6, length=0
993        assert_eq!(bytes, vec![0x00, 0x06, 0x00, 0x00]);
994        let DataItem::ExtensionsSupported(ids) = decode_one(&bytes) else {
995            panic!()
996        };
997        assert!(ids.is_empty());
998    }
999
1000    #[test]
1001    fn extensions_supported_odd_length_rejected() {
1002        let raw = RawDataItem {
1003            type_id: DataItemType::EXTENSIONS_SUPPORTED,
1004            value: Bytes::from_static(&[0x00, 0x01, 0x00]),
1005        };
1006        let err = DataItem::decode(raw).unwrap_err();
1007        assert!(matches!(
1008            err,
1009            CodecError::InvalidDataItemLength {
1010                kind: DataItemType::EXTENSIONS_SUPPORTED,
1011                ..
1012            }
1013        ));
1014    }
1015
1016    #[test]
1017    fn mac_address_eui48_encodes() {
1018        let item = DataItem::MacAddress(MacAddress::Eui48([0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x01]));
1019        // type=7, length=6
1020        assert_eq!(
1021            encode_one(item),
1022            vec![0x00, 0x07, 0x00, 0x06, 0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x01]
1023        );
1024    }
1025
1026    #[test]
1027    fn mac_address_eui64_encodes() {
1028        let item = DataItem::MacAddress(MacAddress::Eui64([
1029            0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE,
1030        ]));
1031        // type=7, length=8
1032        assert_eq!(
1033            encode_one(item),
1034            vec![
1035                0x00, 0x07, 0x00, 0x08, 0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE,
1036            ]
1037        );
1038    }
1039
1040    #[test]
1041    fn mac_address_eui48_roundtrips() {
1042        let bytes = encode_one(DataItem::MacAddress(MacAddress::Eui48([
1043            0x02, 0x00, 0x00, 0x00, 0x00, 0x01,
1044        ])));
1045        let DataItem::MacAddress(mac) = decode_one(&bytes) else {
1046            panic!("expected MacAddress variant")
1047        };
1048        assert!(mac.is_eui48());
1049        assert_eq!(mac.as_bytes(), &[0x02, 0x00, 0x00, 0x00, 0x00, 0x01]);
1050    }
1051
1052    #[test]
1053    fn mac_address_eui64_roundtrips() {
1054        let bytes = encode_one(DataItem::MacAddress(MacAddress::Eui64([
1055            0x02, 0x00, 0x00, 0xFF, 0xFE, 0x00, 0x00, 0x01,
1056        ])));
1057        let DataItem::MacAddress(mac) = decode_one(&bytes) else {
1058            panic!("expected MacAddress variant")
1059        };
1060        assert!(mac.is_eui64());
1061        assert_eq!(
1062            mac.as_bytes(),
1063            &[0x02, 0x00, 0x00, 0xFF, 0xFE, 0x00, 0x00, 0x01]
1064        );
1065    }
1066
1067    /// RFC 8175 §13.7 explicitly allows lengths 6 *and* 8 — every other
1068    /// length must be rejected with `OneOf([6, 8])`. The samples below
1069    /// straddle each accepted length (5/7 around 6, 7/9 around 8) plus
1070    /// extremes (0, 1, 16) to guard against overly permissive bounds.
1071    #[test]
1072    fn mac_address_wrong_length_rejected() {
1073        for bad_len in [0usize, 1, 5, 7, 9, 16] {
1074            let raw = RawDataItem {
1075                type_id: DataItemType::MAC_ADDRESS,
1076                value: Bytes::from(vec![0u8; bad_len]),
1077            };
1078            match DataItem::decode(raw).unwrap_err() {
1079                CodecError::InvalidDataItemLength {
1080                    kind: DataItemType::MAC_ADDRESS,
1081                    expected: ExpectedLen::OneOf(&[6, 8]),
1082                    got,
1083                } => assert_eq!(got, bad_len),
1084                other => panic!("len={bad_len} got unexpected error {other:?}"),
1085            }
1086        }
1087    }
1088
1089    #[test]
1090    fn ipv4_address_add_drop_flag_roundtrips() {
1091        let add = DataItem::Ipv4Address {
1092            add: true,
1093            addr: Ipv4Addr::new(1, 2, 3, 4),
1094        };
1095        let bytes = encode_one(add);
1096        assert_eq!(bytes, vec![0x00, 0x08, 0x00, 0x05, 0x01, 1, 2, 3, 4]);
1097        let DataItem::Ipv4Address { add, addr } = decode_one(&bytes) else {
1098            panic!()
1099        };
1100        assert!(add);
1101        assert_eq!(addr, Ipv4Addr::new(1, 2, 3, 4));
1102
1103        let drop = DataItem::Ipv4Address {
1104            add: false,
1105            addr: Ipv4Addr::new(1, 2, 3, 4),
1106        };
1107        let bytes = encode_one(drop);
1108        assert_eq!(bytes[4], 0x00);
1109        let DataItem::Ipv4Address { add, .. } = decode_one(&bytes) else {
1110            panic!()
1111        };
1112        assert!(!add);
1113    }
1114
1115    #[test]
1116    fn ipv6_address_roundtrips() {
1117        let item = DataItem::Ipv6Address {
1118            add: true,
1119            addr: "2001:db8::1".parse().unwrap(),
1120        };
1121        let bytes = encode_one(item);
1122        assert_eq!(bytes.len(), 4 + 17);
1123        let DataItem::Ipv6Address { add, addr } = decode_one(&bytes) else {
1124            panic!()
1125        };
1126        assert!(add);
1127        assert_eq!(addr, "2001:db8::1".parse::<Ipv6Addr>().unwrap());
1128    }
1129
1130    #[test]
1131    fn ipv4_attached_subnet_encodes() {
1132        let item = DataItem::Ipv4AttachedSubnet {
1133            add: true,
1134            subnet: "10.0.0.0/24".parse().unwrap(),
1135        };
1136        // type=10, length=6, flags=01, addr=10.0.0.0, prefix=24
1137        assert_eq!(
1138            encode_one(item),
1139            vec![0x00, 0x0A, 0x00, 0x06, 0x01, 10, 0, 0, 0, 24]
1140        );
1141    }
1142
1143    #[test]
1144    fn ipv4_attached_subnet_bad_prefix_rejected() {
1145        let raw = RawDataItem {
1146            type_id: DataItemType::IPV4_ATTACHED_SUBNET,
1147            // 33 is out of range for IPv4
1148            value: Bytes::from_static(&[0x01, 10, 0, 0, 0, 33]),
1149        };
1150        let err = DataItem::decode(raw).unwrap_err();
1151        assert!(matches!(err, CodecError::OutOfRange { .. }));
1152    }
1153
1154    #[test]
1155    fn ipv6_attached_subnet_roundtrips() {
1156        let item = DataItem::Ipv6AttachedSubnet {
1157            add: true,
1158            subnet: "2001:db8::/32".parse().unwrap(),
1159        };
1160        let bytes = encode_one(item);
1161        assert_eq!(bytes.len(), 4 + 18);
1162        let DataItem::Ipv6AttachedSubnet { add, subnet } = decode_one(&bytes) else {
1163            panic!()
1164        };
1165        assert!(add);
1166        assert_eq!(subnet, "2001:db8::/32".parse::<Ipv6Net>().unwrap());
1167    }
1168
1169    #[test]
1170    fn data_rate_variants_encode_as_u64() {
1171        let pairs = [
1172            (
1173                DataItem::MaxDataRateReceive(1_000_000_000),
1174                DataItemType::MAXIMUM_DATA_RATE_RECEIVE,
1175            ),
1176            (
1177                DataItem::MaxDataRateTransmit(1_000_000_000),
1178                DataItemType::MAXIMUM_DATA_RATE_TRANSMIT,
1179            ),
1180            (
1181                DataItem::CurrentDataRateReceive(500_000_000),
1182                DataItemType::CURRENT_DATA_RATE_RECEIVE,
1183            ),
1184            (
1185                DataItem::CurrentDataRateTransmit(500_000_000),
1186                DataItemType::CURRENT_DATA_RATE_TRANSMIT,
1187            ),
1188        ];
1189        for (item, ty) in pairs {
1190            let bytes = encode_one(item);
1191            assert_eq!(bytes.len(), 4 + 8);
1192            assert_eq!(u16::from_be_bytes([bytes[0], bytes[1]]), ty.0);
1193            assert_eq!(u16::from_be_bytes([bytes[2], bytes[3]]), 8);
1194        }
1195    }
1196
1197    #[test]
1198    fn latency_encodes_microseconds() {
1199        let item = DataItem::Latency(Duration::from_micros(0xDEAD_BEEF));
1200        let bytes = encode_one(item);
1201        // type=16, length=8
1202        assert_eq!(&bytes[..4], &[0x00, 0x10, 0x00, 0x08]);
1203        let DataItem::Latency(d) = decode_one(&bytes) else {
1204            panic!()
1205        };
1206        assert_eq!(d, Duration::from_micros(0xDEAD_BEEF));
1207    }
1208
1209    #[test]
1210    fn resources_encodes_single_byte() {
1211        let item = DataItem::Resources(75);
1212        // type=17, length=1, value=0x4B
1213        assert_eq!(encode_one(item), vec![0x00, 0x11, 0x00, 0x01, 0x4B]);
1214    }
1215
1216    #[test]
1217    fn resources_above_100_rejected_on_decode() {
1218        let raw = RawDataItem {
1219            type_id: DataItemType::RESOURCES,
1220            value: Bytes::from_static(&[150]),
1221        };
1222        let err = DataItem::decode(raw).unwrap_err();
1223        assert!(matches!(err, CodecError::OutOfRange { .. }));
1224    }
1225
1226    #[test]
1227    fn resources_above_100_rejected_on_encode() {
1228        let mut buf = BytesMut::new();
1229        let err = DataItem::Resources(150).encode(&mut buf).unwrap_err();
1230        assert!(matches!(err, CodecError::OutOfRange { .. }));
1231        // Buffer restored on error.
1232        assert!(buf.is_empty());
1233    }
1234
1235    #[test]
1236    fn rlq_above_100_rejected_on_encode() {
1237        for item in [
1238            DataItem::RelativeLinkQualityReceive(101),
1239            DataItem::RelativeLinkQualityTransmit(255),
1240        ] {
1241            let mut buf = BytesMut::new();
1242            let err = item.encode(&mut buf).unwrap_err();
1243            assert!(matches!(err, CodecError::OutOfRange { .. }));
1244            assert!(buf.is_empty());
1245        }
1246    }
1247
1248    #[test]
1249    fn latency_overflow_rejected_on_encode() {
1250        // Duration::MAX has more microseconds than fit in u64 — encode must
1251        // refuse rather than silently truncate. Buffer is restored on error,
1252        // including any pre-existing prefix.
1253        let mut buf = BytesMut::from(&b"prefix"[..]);
1254        let snapshot = buf.clone();
1255        let err = DataItem::Latency(Duration::MAX)
1256            .encode(&mut buf)
1257            .unwrap_err();
1258        assert!(matches!(err, CodecError::OutOfRange { .. }));
1259        assert_eq!(buf, snapshot);
1260    }
1261
1262    #[test]
1263    fn heartbeat_interval_overflow_restores_nonempty_buffer() {
1264        // The HeartbeatInterval validation runs *after* the 4-byte TLV header
1265        // is written into `out` — confirm the wrapping `encode` undoes that
1266        // partial write even when `out` was non-empty going in.
1267        let mut buf = BytesMut::from(&b"existing"[..]);
1268        let snapshot = buf.clone();
1269        let item = DataItem::HeartbeatInterval(Duration::from_secs(u64::MAX / 1000));
1270        let err = item.encode(&mut buf).unwrap_err();
1271        assert!(matches!(err, CodecError::OutOfRange { .. }));
1272        assert_eq!(buf, snapshot);
1273    }
1274
1275    #[test]
1276    fn oversized_data_item_value_rejected() {
1277        // A Status with > 64 KiB of text overflows the 16-bit TLV length
1278        // field. Must surface as OutOfRange, not as a silently-truncated frame.
1279        let item = DataItem::Status {
1280            code: StatusCode::SUCCESS,
1281            text: "x".repeat(70_000),
1282        };
1283        let mut buf = BytesMut::new();
1284        let err = item.encode(&mut buf).unwrap_err();
1285        match err {
1286            CodecError::OutOfRange { field, value } => {
1287                assert_eq!(field, "data_item_value_length");
1288                assert!(value > u16::MAX as u64);
1289            }
1290            other => panic!("unexpected error: {other:?}"),
1291        }
1292        assert!(buf.is_empty());
1293    }
1294
1295    #[test]
1296    fn oversized_signal_body_rejected() {
1297        // Two large items together push the signal body past u16::MAX even
1298        // though each individual item fits. The frame-level length check must
1299        // catch this.
1300        let s = Signal::new(SignalType::PEER_OFFER)
1301            .with_item(DataItem::Status {
1302                code: StatusCode::SUCCESS,
1303                text: "x".repeat(40_000),
1304            })
1305            .with_item(DataItem::Status {
1306                code: StatusCode::SUCCESS,
1307                text: "y".repeat(40_000),
1308            });
1309        match s.encode().unwrap_err() {
1310            CodecError::OutOfRange { field, .. } => {
1311                assert_eq!(field, "signal_body_length");
1312            }
1313            other => panic!("unexpected error: {other:?}"),
1314        }
1315    }
1316
1317    #[test]
1318    fn oversized_message_body_rejected() {
1319        let m = Message::new(MessageType::SESSION_UPDATE)
1320            .with_item(DataItem::Status {
1321                code: StatusCode::SUCCESS,
1322                text: "x".repeat(40_000),
1323            })
1324            .with_item(DataItem::Status {
1325                code: StatusCode::SUCCESS,
1326                text: "y".repeat(40_000),
1327            });
1328        match m.encode().unwrap_err() {
1329            CodecError::OutOfRange { field, .. } => {
1330                assert_eq!(field, "message_body_length");
1331            }
1332            other => panic!("unexpected error: {other:?}"),
1333        }
1334    }
1335
1336    #[test]
1337    fn mtu_wrong_length_carries_exact() {
1338        // Round out the ExpectedLen variants: the three non-Exact forms have
1339        // direct assertions; this test pins the Exact(N) form too.
1340        let raw = RawDataItem {
1341            type_id: DataItemType::MTU,
1342            value: Bytes::from_static(&[0x05]),
1343        };
1344        match DataItem::decode(raw).unwrap_err() {
1345            CodecError::InvalidDataItemLength {
1346                kind: DataItemType::MTU,
1347                expected: ExpectedLen::Exact(2),
1348                got: 1,
1349            } => {}
1350            other => panic!("unexpected error: {other:?}"),
1351        }
1352    }
1353
1354    #[test]
1355    fn ipv4_attached_subnet_decode_normalizes_host_bits() {
1356        // Wire form carries `10.0.0.5/24` (host bits set). After decode, the
1357        // in-memory subnet must match the canonical `10.0.0.0/24`, so encode
1358        // and decode are symmetric. The `add` flag must survive truncation.
1359        let raw = RawDataItem {
1360            type_id: DataItemType::IPV4_ATTACHED_SUBNET,
1361            value: Bytes::from_static(&[0x01, 10, 0, 0, 5, 24]),
1362        };
1363        let DataItem::Ipv4AttachedSubnet { add, subnet } = DataItem::decode(raw).unwrap() else {
1364            panic!()
1365        };
1366        assert!(add);
1367        assert_eq!(subnet, "10.0.0.0/24".parse::<Ipv4Net>().unwrap());
1368    }
1369
1370    #[test]
1371    fn ipv6_attached_subnet_decode_normalizes_host_bits() {
1372        // 2001:db8::1234/32 → 2001:db8::/32 after decode.
1373        let mut wire = vec![0x01u8];
1374        wire.extend_from_slice(&"2001:db8::1234".parse::<Ipv6Addr>().unwrap().octets());
1375        wire.push(32);
1376        let raw = RawDataItem {
1377            type_id: DataItemType::IPV6_ATTACHED_SUBNET,
1378            value: Bytes::from(wire),
1379        };
1380        let DataItem::Ipv6AttachedSubnet { add, subnet } = DataItem::decode(raw).unwrap() else {
1381            panic!()
1382        };
1383        assert!(add);
1384        assert_eq!(subnet, "2001:db8::/32".parse::<Ipv6Net>().unwrap());
1385    }
1386
1387    #[test]
1388    fn non_canonical_subnet_wire_bytes_round_trip_to_canonical() {
1389        // End-to-end contract: peer sends non-canonical subnet wire bytes,
1390        // we decode → re-encode, and the second wire form is canonical
1391        // (host bits zeroed). Locks in the symmetry property at the
1392        // boundary, not just inside decode and encode in isolation.
1393        let non_canonical = [0x01u8, 10, 0, 0, 5, 24];
1394        let canonical = [0x01u8, 10, 0, 0, 0, 24];
1395
1396        let raw = RawDataItem {
1397            type_id: DataItemType::IPV4_ATTACHED_SUBNET,
1398            value: Bytes::copy_from_slice(&non_canonical),
1399        };
1400        let item = DataItem::decode(raw).unwrap();
1401
1402        let mut out = BytesMut::new();
1403        item.encode(&mut out).unwrap();
1404        // Skip the 4-byte TLV header; compare just the value bytes.
1405        assert_eq!(&out[4..], &canonical[..]);
1406    }
1407
1408    #[test]
1409    fn oversized_peer_type_value_rejected() {
1410        let item = DataItem::PeerType {
1411            flags: PeerFlags::default(),
1412            description: "x".repeat(70_000),
1413        };
1414        let mut buf = BytesMut::new();
1415        let err = item.encode(&mut buf).unwrap_err();
1416        match err {
1417            CodecError::OutOfRange { field, value } => {
1418                assert_eq!(field, "data_item_value_length");
1419                assert!(value > u16::MAX as u64);
1420            }
1421            other => panic!("unexpected error: {other:?}"),
1422        }
1423        // Up-front check fires before any header is written.
1424        assert!(buf.is_empty());
1425    }
1426
1427    #[test]
1428    fn oversized_unknown_data_item_value_rejected() {
1429        // The Unknown path delegates to RawDataItem::encode, which historically
1430        // cast `len() as u16` and silently truncated. Confirm it now refuses
1431        // values that don't fit the 16-bit TLV length field.
1432        let item = DataItem::Unknown(RawDataItem {
1433            type_id: DataItemType(4242),
1434            value: Bytes::from(vec![0u8; 70_000]),
1435        });
1436        let mut buf = BytesMut::new();
1437        let err = item.encode(&mut buf).unwrap_err();
1438        match err {
1439            CodecError::OutOfRange { field, value } => {
1440                assert_eq!(field, "data_item_value_length");
1441                assert_eq!(value, 70_000);
1442            }
1443            other => panic!("unexpected error: {other:?}"),
1444        }
1445        // Buffer untouched on rejection.
1446        assert!(buf.is_empty());
1447    }
1448
1449    #[test]
1450    fn oversized_extensions_supported_value_rejected() {
1451        // > 32_768 ids overflows the 16-bit length field (each id is 2 bytes).
1452        let item = DataItem::ExtensionsSupported(vec![ExtensionId(0); 33_000]);
1453        let mut buf = BytesMut::new();
1454        let err = item.encode(&mut buf).unwrap_err();
1455        match err {
1456            CodecError::OutOfRange { field, value } => {
1457                assert_eq!(field, "data_item_value_length");
1458                assert!(value > u16::MAX as u64);
1459            }
1460            other => panic!("unexpected error: {other:?}"),
1461        }
1462        assert!(buf.is_empty());
1463    }
1464
1465    #[test]
1466    fn encode_failure_does_not_corrupt_existing_buffer() {
1467        // Pre-populate the buffer; encode failure must restore it byte-for-byte.
1468        let mut buf = BytesMut::from(&b"prefix"[..]);
1469        let snapshot = buf.clone();
1470        let err = DataItem::Resources(200).encode(&mut buf).unwrap_err();
1471        assert!(matches!(err, CodecError::OutOfRange { .. }));
1472        assert_eq!(buf, snapshot);
1473    }
1474
1475    #[test]
1476    fn signal_encode_propagates_data_item_error() {
1477        let s = Signal::new(SignalType::PEER_OFFER).with_item(DataItem::Resources(200));
1478        let err = s.encode().unwrap_err();
1479        assert!(matches!(err, CodecError::OutOfRange { .. }));
1480    }
1481
1482    #[test]
1483    fn message_encode_propagates_data_item_error() {
1484        let m = Message::new(MessageType::SESSION_UPDATE)
1485            .with_item(DataItem::HeartbeatInterval(Duration::from_millis(1000)))
1486            .with_item(DataItem::RelativeLinkQualityReceive(120));
1487        let err = m.encode().unwrap_err();
1488        assert!(matches!(err, CodecError::OutOfRange { .. }));
1489    }
1490
1491    #[test]
1492    fn relative_link_quality_variants_roundtrip() {
1493        for v in [0u8, 50, 100] {
1494            let rx = DataItem::RelativeLinkQualityReceive(v);
1495            let DataItem::RelativeLinkQualityReceive(out) = decode_one(&encode_one(rx)) else {
1496                panic!()
1497            };
1498            assert_eq!(out, v);
1499
1500            let tx = DataItem::RelativeLinkQualityTransmit(v);
1501            let DataItem::RelativeLinkQualityTransmit(out) = decode_one(&encode_one(tx)) else {
1502                panic!()
1503            };
1504            assert_eq!(out, v);
1505        }
1506    }
1507
1508    #[test]
1509    fn mtu_encodes() {
1510        let item = DataItem::Mtu(1500);
1511        // type=20, length=2, value=0x05DC
1512        assert_eq!(encode_one(item), vec![0x00, 0x14, 0x00, 0x02, 0x05, 0xDC]);
1513    }
1514
1515    #[test]
1516    fn unknown_data_item_passes_through() {
1517        // Unknown type id (4242) should round-trip verbatim through Unknown.
1518        let raw_in = RawDataItem {
1519            type_id: DataItemType(4242),
1520            value: Bytes::from_static(&[0xAA, 0xBB, 0xCC]),
1521        };
1522        let item = DataItem::decode(raw_in.clone()).unwrap();
1523        match &item {
1524            DataItem::Unknown(r) => {
1525                assert_eq!(r.type_id, DataItemType(4242));
1526                assert_eq!(&r.value[..], &[0xAA, 0xBB, 0xCC]);
1527            }
1528            _ => panic!("expected Unknown"),
1529        }
1530        // Encoded form preserves bytes exactly.
1531        let mut buf = BytesMut::new();
1532        item.encode(&mut buf).unwrap();
1533        assert_eq!(&buf[..], &[0x10, 0x92, 0x00, 0x03, 0xAA, 0xBB, 0xCC]);
1534    }
1535
1536    #[test]
1537    fn decoder_skips_unknown_data_items_inside_message() {
1538        // A message with one known (HeartbeatInterval) and one unknown item.
1539        let m = Message::new(MessageType::SESSION_INITIALIZATION)
1540            .with_item(DataItem::HeartbeatInterval(Duration::from_millis(1000)))
1541            .with_item(DataItem::Unknown(RawDataItem {
1542                type_id: DataItemType(9999),
1543                value: Bytes::from_static(&[0x42]),
1544            }));
1545        let decoded = Message::decode(m.encode().unwrap().freeze()).unwrap();
1546        assert_eq!(decoded.message_type, MessageType::SESSION_INITIALIZATION);
1547        assert_eq!(decoded.data_items.len(), 2);
1548        assert!(matches!(
1549            decoded.data_items[0],
1550            DataItem::HeartbeatInterval(_)
1551        ));
1552        assert!(matches!(decoded.data_items[1], DataItem::Unknown(_)));
1553    }
1554
1555    #[test]
1556    fn signal_with_multiple_unknown_items_round_trips() {
1557        // Mirrors the Message-side test: forward-compat must work on Signals
1558        // (Peer Offer / Peer Discovery) too.
1559        let s = Signal::new(SignalType::PEER_OFFER)
1560            .with_item(DataItem::PeerType {
1561                flags: PeerFlags::default(),
1562                description: "router".into(),
1563            })
1564            .with_item(DataItem::Unknown(RawDataItem {
1565                type_id: DataItemType(7777),
1566                value: Bytes::from_static(&[1, 2, 3]),
1567            }))
1568            .with_item(DataItem::Unknown(RawDataItem {
1569                type_id: DataItemType(8888),
1570                value: Bytes::new(),
1571            }));
1572        let decoded = Signal::decode(s.encode().unwrap().freeze()).unwrap();
1573        assert_eq!(decoded.data_items.len(), 3);
1574        assert!(matches!(decoded.data_items[0], DataItem::PeerType { .. }));
1575        match &decoded.data_items[1] {
1576            DataItem::Unknown(r) => {
1577                assert_eq!(r.type_id, DataItemType(7777));
1578                assert_eq!(&r.value[..], &[1, 2, 3]);
1579            }
1580            _ => panic!(),
1581        }
1582        match &decoded.data_items[2] {
1583            DataItem::Unknown(r) => {
1584                assert_eq!(r.type_id, DataItemType(8888));
1585                assert!(r.value.is_empty());
1586            }
1587            _ => panic!(),
1588        }
1589    }
1590
1591    #[test]
1592    fn signal_with_multiple_items_preserves_order() {
1593        let s = Signal::new(SignalType::PEER_OFFER)
1594            .with_item(DataItem::PeerType {
1595                flags: PeerFlags::default(),
1596                description: "router".into(),
1597            })
1598            .with_item(DataItem::Ipv4ConnectionPoint {
1599                flags: ConnectionPointFlags { use_tls: false },
1600                addr: Ipv4Addr::new(127, 0, 0, 1),
1601                port: Some(854),
1602            });
1603        let decoded = Signal::decode(s.encode().unwrap().freeze()).unwrap();
1604        assert_eq!(decoded.signal_type, SignalType::PEER_OFFER);
1605        assert_eq!(decoded.data_items.len(), 2);
1606        assert!(matches!(decoded.data_items[0], DataItem::PeerType { .. }));
1607        assert!(matches!(
1608            decoded.data_items[1],
1609            DataItem::Ipv4ConnectionPoint { .. }
1610        ));
1611    }
1612
1613    #[test]
1614    fn truncated_message_buffer_rejected() {
1615        // declared length 4 but only 2 bytes follow
1616        let mut buf = BytesMut::new();
1617        buf.put_u16(MessageType::HEARTBEAT.0);
1618        buf.put_u16(4);
1619        buf.put_u8(0xAB);
1620        buf.put_u8(0xCD);
1621        let err = Message::decode(buf.freeze()).unwrap_err();
1622        assert!(matches!(err, CodecError::LengthMismatch { .. }));
1623    }
1624
1625    #[test]
1626    fn truncated_signal_header_rejected() {
1627        let buf = Bytes::from_static(b"DLE");
1628        let err = Signal::decode(buf).unwrap_err();
1629        assert!(matches!(err, CodecError::Truncated { .. }));
1630    }
1631
1632    #[test]
1633    fn invalid_utf8_in_status_text_rejected() {
1634        // Status code 0 followed by an invalid UTF-8 sequence (0xFF is never valid).
1635        let raw = RawDataItem {
1636            type_id: DataItemType::STATUS,
1637            value: Bytes::from_static(&[0x00, 0xFF, 0xFE]),
1638        };
1639        let err = DataItem::decode(raw).unwrap_err();
1640        assert!(matches!(err, CodecError::InvalidUtf8(_)));
1641    }
1642}