Skip to main content

bacnet_encoding/
npdu.rs

1//! NPDU encoding and decoding per ASHRAE 135-2020 Clause 6.
2//!
3//! The Network Protocol Data Unit carries either an application-layer APDU
4//! or a network-layer message, with optional source/destination routing
5//! information for multi-hop BACnet internetworks.
6//!
7//! Both directions hold DADR and SADR to [`NpduAddress::MAX_MAC_LEN`] octets
8//! (#1141), so the router, the non-router network layer and everything above
9//! them only ever see an address that some data link could carry.
10
11use bacnet_types::constructed::BACnetAddress;
12use bacnet_types::enums::{NetworkPriority, RejectMessageReason};
13use bacnet_types::error::Error;
14use bacnet_types::MacAddr;
15use bytes::{BufMut, Bytes, BytesMut};
16
17mod decode_error;
18pub use decode_error::{NpduAddressField, NpduDecodeError};
19
20/// BACnet protocol version (always 1).
21pub const BACNET_PROTOCOL_VERSION: u8 = 1;
22
23// ---------------------------------------------------------------------------
24// Address used in NPDU routing fields
25// ---------------------------------------------------------------------------
26
27/// Network-layer address: network number + MAC address.
28///
29/// Used for source/destination fields in routed NPDUs.
30#[derive(Debug, Clone, PartialEq, Eq, Hash)]
31pub struct NpduAddress {
32    /// Network number (1-65534, or 0xFFFF for global broadcast destination).
33    pub network: u16,
34    /// MAC-layer address (variable length, empty for broadcast).
35    pub mac_address: MacAddr,
36}
37
38impl NpduAddress {
39    /// The longest DADR or SADR, in octets, the codec encodes or decodes
40    /// (#1141). It is the same limit as [`BACnetAddress::MAX_MAC_LEN`].
41    ///
42    /// A length octet could announce up to 255 octets, yet no standard data
43    /// link in Table 6-2 (Clause 6.2.2.2) needs more than 7, and the longest
44    /// MAC this stack serves is 6 octets (a B/IPv6 node's is its 3-octet
45    /// VMAC). The bound is the defensive 18 of [`BACnetAddress::MAX_MAC_LEN`].
46    /// A longer DLEN or SLEN names no node on a standard data link or on any
47    /// data link this stack serves.
48    pub const MAX_MAC_LEN: usize = BACnetAddress::MAX_MAC_LEN;
49}
50
51// ---------------------------------------------------------------------------
52// NPDU struct
53// ---------------------------------------------------------------------------
54
55/// Decoded Network Protocol Data Unit (Clause 6.2).
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct Npdu {
58    /// Whether this is a network-layer message (vs application-layer APDU).
59    pub is_network_message: bool,
60    /// Whether the sender expects a reply.
61    pub expecting_reply: bool,
62    /// Message priority.
63    pub priority: NetworkPriority,
64    /// Remote destination address, if routed.
65    pub destination: Option<NpduAddress>,
66    /// Originating source address (populated by routers).
67    pub source: Option<NpduAddress>,
68    /// Remaining hop count for routed messages (0-255).
69    pub hop_count: u8,
70    /// Network message type (when `is_network_message` is true).
71    pub message_type: Option<u8>,
72    /// Vendor ID for proprietary network messages (message_type >= 0x80).
73    pub vendor_id: Option<u16>,
74    /// Payload: either APDU bytes or network message data.
75    pub payload: Bytes,
76}
77
78/// Decoded payload of a Reject-Message-To-Network control message.
79///
80/// The enclosing NPDU identifies the network-message type; this type models
81/// only the fixed reason/DNET payload used by that message.
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub struct RejectMessageToNetwork {
84    /// Why the router rejected the message.
85    pub reason: RejectMessageReason,
86    /// Destination network named by the rejection.
87    pub dnet: u16,
88}
89
90impl Default for Npdu {
91    fn default() -> Self {
92        Self {
93            is_network_message: false,
94            expecting_reply: false,
95            priority: NetworkPriority::NORMAL,
96            destination: None,
97            source: None,
98            hop_count: 255,
99            message_type: None,
100            vendor_id: None,
101            payload: Bytes::new(),
102        }
103    }
104}
105
106// ---------------------------------------------------------------------------
107// Encoding
108// ---------------------------------------------------------------------------
109
110/// Encode an NPDU to wire format.
111pub fn encode_npdu(buf: &mut BytesMut, npdu: &Npdu) -> Result<(), Error> {
112    buf.put_u8(BACNET_PROTOCOL_VERSION);
113
114    let mut control: u8 = npdu.priority.to_raw() & 0x03;
115    if npdu.is_network_message {
116        control |= 0x80;
117    }
118    if npdu.destination.is_some() {
119        control |= 0x20;
120    }
121    if npdu.source.is_some() {
122        control |= 0x08;
123    }
124    if npdu.expecting_reply {
125        control |= 0x04;
126    }
127    buf.put_u8(control);
128
129    if let Some(dest) = &npdu.destination {
130        if dest.network == 0 {
131            return Err(Error::Encoding("NPDU DNET must not be 0".into()));
132        }
133        buf.put_u16(dest.network);
134        check_encoded_len(NpduAddressField::Destination, &dest.mac_address)?;
135        buf.put_u8(dest.mac_address.len() as u8);
136        buf.put_slice(&dest.mac_address);
137    }
138
139    if let Some(src) = &npdu.source {
140        if src.network == 0 || src.network == 0xFFFF {
141            return Err(Error::Encoding(format!(
142                "NPDU SNET must be 1..65534, got {}",
143                src.network
144            )));
145        }
146        if src.mac_address.is_empty() {
147            return Err(Error::Encoding("NPDU SLEN must not be 0".into()));
148        }
149        buf.put_u16(src.network);
150        check_encoded_len(NpduAddressField::Source, &src.mac_address)?;
151        buf.put_u8(src.mac_address.len() as u8);
152        buf.put_slice(&src.mac_address);
153    }
154
155    if npdu.destination.is_some() {
156        buf.put_u8(npdu.hop_count);
157    }
158
159    if npdu.is_network_message {
160        if let Some(msg_type) = npdu.message_type {
161            buf.put_u8(msg_type);
162            if msg_type >= 0x80 {
163                buf.put_u16(npdu.vendor_id.unwrap_or(0));
164            }
165        }
166    }
167
168    buf.put_slice(&npdu.payload);
169
170    Ok(())
171}
172
173/// Refuse to encode a DADR or SADR that the decoder would refuse (#1141).
174fn check_encoded_len(field: NpduAddressField, mac: &[u8]) -> Result<(), Error> {
175    if mac.len() > NpduAddress::MAX_MAC_LEN {
176        return Err(Error::Encoding(format!(
177            "NPDU {field} address of {} octets exceeds the {}-octet limit",
178            mac.len(),
179            NpduAddress::MAX_MAC_LEN
180        )));
181    }
182    Ok(())
183}
184
185// ---------------------------------------------------------------------------
186// Decoding
187// ---------------------------------------------------------------------------
188
189/// Decode an NPDU from raw bytes.
190///
191/// Returns the decoded [`Npdu`]. The `payload` field contains either the
192/// APDU bytes or network message data.
193///
194/// A DLEN or SLEN past [`NpduAddress::MAX_MAC_LEN`] fails with
195/// [`NpduDecodeError::AddressTooLong`], raised without reading the address
196/// octets, so a caller can tell it apart from every other malformation
197/// ([`NpduDecodeError::Malformed`]). For an over-long DADR the error still
198/// carries the SNET/SADR behind it, when the frame holds a valid one, so a
199/// router can address its reject (#1158).
200pub fn decode_npdu(data: Bytes) -> Result<Npdu, NpduDecodeError> {
201    if data.len() < 2 {
202        return Err(Error::buffer_too_short(2, data.len()).into());
203    }
204
205    let version = data[0];
206    if version != BACNET_PROTOCOL_VERSION {
207        return Err(
208            Error::decoding(0, format!("unsupported BACnet protocol version: {version}")).into(),
209        );
210    }
211
212    let control = data[1];
213    let is_network_message = control & 0x80 != 0;
214    let has_destination = control & 0x20 != 0;
215    let has_source = control & 0x08 != 0;
216    let expecting_reply = control & 0x04 != 0;
217    let priority = NetworkPriority::from_raw(control & 0x03);
218
219    // Only hosted builds log; tracing is a std-only dependency.
220    #[cfg(feature = "std")]
221    if control & 0x50 != 0 {
222        tracing::warn!(
223            control_byte = control,
224            "NPDU control byte has reserved bits set (bits 4 or 6)"
225        );
226    }
227
228    let mut offset = 2;
229    let mut destination = None;
230    let mut source = None;
231    let mut hop_count: u8 = 255;
232
233    if has_destination {
234        if offset + 3 > data.len() {
235            return Err(Error::decoding(offset, "NPDU too short for destination fields").into());
236        }
237        let dnet = u16::from_be_bytes([data[offset], data[offset + 1]]);
238        if dnet == 0 {
239            return Err(Error::decoding(offset, "NPDU destination network 0 is invalid").into());
240        }
241        offset += 2;
242        let dlen = data[offset];
243        offset += 1;
244        let dadr = match decode_address(&data, offset, NpduAddressField::Destination, dlen, dnet) {
245            Ok(dadr) => dadr,
246            Err(mut refused) => {
247                if let NpduDecodeError::AddressTooLong { source, .. } = &mut refused {
248                    *source = has_source
249                        .then(|| source_behind(&data, offset + usize::from(dlen)))
250                        .flatten();
251                }
252                return Err(refused);
253            }
254        };
255        offset += dadr.len();
256
257        destination = Some(NpduAddress {
258            network: dnet,
259            mac_address: dadr,
260        });
261    }
262
263    if has_source {
264        if offset + 3 > data.len() {
265            return Err(Error::decoding(offset, "NPDU too short for source fields").into());
266        }
267        let snet = u16::from_be_bytes([data[offset], data[offset + 1]]);
268        if snet == 0 {
269            return Err(Error::decoding(offset, "NPDU source network 0 is invalid").into());
270        }
271        if snet == 0xFFFF {
272            return Err(Error::decoding(offset, "NPDU source network 0xFFFF is invalid").into());
273        }
274        offset += 2;
275        let slen = data[offset];
276        offset += 1;
277
278        if slen == 0 {
279            return Err(Error::decoding(offset - 1, "NPDU source SLEN=0 is invalid").into());
280        }
281        let dnet = destination.as_ref().map(|d| d.network);
282        let sadr = decode_address(&data, offset, NpduAddressField::Source, slen, dnet)?;
283        offset += sadr.len();
284
285        source = Some(NpduAddress {
286            network: snet,
287            mac_address: sadr,
288        });
289    }
290
291    if has_destination {
292        if offset >= data.len() {
293            return Err(Error::decoding(offset, "NPDU too short for hop count").into());
294        }
295        hop_count = data[offset];
296        offset += 1;
297    }
298
299    let mut message_type = None;
300    let mut vendor_id = None;
301
302    if is_network_message {
303        if offset >= data.len() {
304            return Err(Error::decoding(offset, "NPDU too short for network message type").into());
305        }
306        let msg_type = data[offset];
307        offset += 1;
308        message_type = Some(msg_type);
309
310        if msg_type >= 0x80 {
311            if offset + 2 > data.len() {
312                return Err(
313                    Error::decoding(offset, "NPDU too short for proprietary vendor ID").into(),
314                );
315            }
316            vendor_id = Some(u16::from_be_bytes([data[offset], data[offset + 1]]));
317            offset += 2;
318        }
319    }
320
321    let payload = data.slice(offset..);
322
323    Ok(Npdu {
324        is_network_message,
325        expecting_reply,
326        priority,
327        destination,
328        source,
329        hop_count,
330        message_type,
331        vendor_id,
332        payload,
333    })
334}
335
336/// Read the `length`-octet DADR or SADR at `offset`. A length past
337/// [`NpduAddress::MAX_MAC_LEN`] is refused before any address octet is read,
338/// whether or not the frame holds that many (#1141).
339fn decode_address(
340    data: &[u8],
341    offset: usize,
342    field: NpduAddressField,
343    length: u8,
344    dnet: impl Into<Option<u16>>,
345) -> Result<MacAddr, NpduDecodeError> {
346    if usize::from(length) > NpduAddress::MAX_MAC_LEN {
347        return Err(NpduDecodeError::AddressTooLong {
348            field,
349            length,
350            dnet: dnet.into(),
351            source: None,
352        });
353    }
354    let end = offset + usize::from(length);
355    if end > data.len() {
356        return Err(Error::decoding(
357            offset,
358            format!(
359                "NPDU {field} address truncated: {}={length}",
360                field.length_octet()
361            ),
362        )
363        .into());
364    }
365    Ok(MacAddr::from_slice(&data[offset..end]))
366}
367
368/// The SNET/SADR that starts at `offset`, behind a DADR too long to decode.
369///
370/// A router answers such an NPDU with a reject addressed to its original
371/// source (#1158), so the source is read past the DLEN octets the frame
372/// announces. Only a source the decoder would accept counts: a usable SNET, an
373/// SLEN within the bound and every SADR octet present.
374fn source_behind(data: &[u8], offset: usize) -> Option<NpduAddress> {
375    let header = data.get(offset..offset + 3)?;
376    let network = u16::from_be_bytes([header[0], header[1]]);
377    let length = usize::from(header[2]);
378    if network == 0 || network == 0xFFFF || length == 0 || length > NpduAddress::MAX_MAC_LEN {
379        return None;
380    }
381    let sadr = data.get(offset + 3..offset + 3 + length)?;
382    Some(NpduAddress {
383        network,
384        mac_address: MacAddr::from_slice(sadr),
385    })
386}
387
388/// Decode the fixed three-octet Reject-Message-To-Network payload.
389///
390/// The payload is rejected unless it contains exactly one reason octet and
391/// one two-octet destination network. Callers remain responsible for checking
392/// that the enclosing NPDU is the standard Reject-Message-To-Network type.
393pub fn decode_reject_message_to_network(payload: &[u8]) -> Result<RejectMessageToNetwork, Error> {
394    if payload.len() != 3 {
395        return Err(Error::decoding(
396            0,
397            format!(
398                "Reject-Message-To-Network payload must be exactly 3 octets, got {}",
399                payload.len()
400            ),
401        ));
402    }
403    Ok(RejectMessageToNetwork {
404        reason: RejectMessageReason::from_raw(payload[0]),
405        dnet: u16::from_be_bytes([payload[1], payload[2]]),
406    })
407}
408
409// ---------------------------------------------------------------------------
410// Tests
411// ---------------------------------------------------------------------------
412
413#[cfg(test)]
414mod address_bound_tests;
415
416#[cfg(test)]
417mod tests {
418    use super::*;
419
420    fn encode_to_vec(npdu: &Npdu) -> Vec<u8> {
421        let mut buf = BytesMut::with_capacity(64);
422        encode_npdu(&mut buf, npdu).unwrap();
423        buf.to_vec()
424    }
425
426    #[test]
427    fn minimal_local_apdu() {
428        // Simplest case: local, no routing, with APDU payload
429        let npdu = Npdu {
430            payload: Bytes::from_static(&[0x10, 0x08]), // UnconfirmedRequest WhoIs
431            ..Default::default()
432        };
433        let encoded = encode_to_vec(&npdu);
434        // version=1, control=0x00 (normal priority, no flags), payload
435        assert_eq!(encoded, vec![0x01, 0x00, 0x10, 0x08]);
436
437        let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
438        assert_eq!(decoded, npdu);
439    }
440
441    #[test]
442    fn expecting_reply_flag() {
443        let npdu = Npdu {
444            expecting_reply: true,
445            payload: Bytes::from_static(&[0xAA]),
446            ..Default::default()
447        };
448        let encoded = encode_to_vec(&npdu);
449        assert_eq!(encoded[1], 0x04); // control: expecting_reply bit
450        let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
451        assert!(decoded.expecting_reply);
452    }
453
454    #[test]
455    fn priority_encoding() {
456        for (prio, expected_bits) in [
457            (NetworkPriority::NORMAL, 0x00),
458            (NetworkPriority::URGENT, 0x01),
459            (NetworkPriority::CRITICAL_EQUIPMENT, 0x02),
460            (NetworkPriority::LIFE_SAFETY, 0x03),
461        ] {
462            let npdu = Npdu {
463                priority: prio,
464                payload: Bytes::new(),
465                ..Default::default()
466            };
467            let encoded = encode_to_vec(&npdu);
468            assert_eq!(encoded[1] & 0x03, expected_bits);
469            let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
470            assert_eq!(decoded.priority, prio);
471        }
472    }
473
474    #[test]
475    fn destination_only_round_trip() {
476        let npdu = Npdu {
477            destination: Some(NpduAddress {
478                network: 1000,
479                mac_address: MacAddr::from_slice(&[0x0A, 0x00, 0x01, 0x01, 0xBA, 0xC0]),
480            }),
481            hop_count: 254,
482            payload: Bytes::from_static(&[0x10, 0x08]),
483            ..Default::default()
484        };
485        let encoded = encode_to_vec(&npdu);
486        // control: has_destination = 0x20
487        assert_eq!(encoded[1] & 0x20, 0x20);
488        let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
489        assert_eq!(decoded, npdu);
490    }
491
492    #[test]
493    fn destination_broadcast() {
494        // Global broadcast: DNET=0xFFFF, DLEN=0 (no DADR)
495        let npdu = Npdu {
496            destination: Some(NpduAddress {
497                network: 0xFFFF,
498                mac_address: MacAddr::new(),
499            }),
500            hop_count: 255,
501            payload: Bytes::from_static(&[0x10, 0x08]),
502            ..Default::default()
503        };
504        let encoded = encode_to_vec(&npdu);
505        // version(1) + control(1) + DNET(2) + DLEN(1) + hop_count(1) + payload(2)
506        assert_eq!(encoded.len(), 8);
507        // DNET = 0xFFFF
508        assert_eq!(&encoded[2..4], &[0xFF, 0xFF]);
509        // DLEN = 0
510        assert_eq!(encoded[4], 0);
511
512        let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
513        assert_eq!(decoded, npdu);
514    }
515
516    #[test]
517    fn source_only_round_trip() {
518        let npdu = Npdu {
519            source: Some(NpduAddress {
520                network: 500,
521                mac_address: MacAddr::from_slice(&[0x01]),
522            }),
523            payload: Bytes::from_static(&[0x30, 0x01, 0x0C]),
524            ..Default::default()
525        };
526        let encoded = encode_to_vec(&npdu);
527        // control: has_source = 0x08
528        assert_eq!(encoded[1] & 0x08, 0x08);
529        // No hop count (no destination)
530        let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
531        assert_eq!(decoded, npdu);
532    }
533
534    #[test]
535    fn source_and_destination_round_trip() {
536        let npdu = Npdu {
537            expecting_reply: true,
538            destination: Some(NpduAddress {
539                network: 2000,
540                mac_address: MacAddr::from_slice(&[0x0A, 0x00, 0x02, 0x01, 0xBA, 0xC0]),
541            }),
542            source: Some(NpduAddress {
543                network: 1000,
544                mac_address: MacAddr::from_slice(&[0x0A, 0x00, 0x01, 0x01, 0xBA, 0xC0]),
545            }),
546            hop_count: 250,
547            payload: Bytes::from_static(&[0x00, 0x05, 0x01, 0x0C]),
548            ..Default::default()
549        };
550        let encoded = encode_to_vec(&npdu);
551        // control: destination(0x20) | source(0x08) | expecting_reply(0x04) = 0x2C
552        assert_eq!(encoded[1], 0x2C);
553        let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
554        assert_eq!(decoded, npdu);
555    }
556
557    #[test]
558    fn network_message_round_trip() {
559        let npdu = Npdu {
560            is_network_message: true,
561            message_type: Some(0x01), // I-Am-Router-To-Network
562            payload: Bytes::from_static(&[0x03, 0xE8]), // network 1000
563            ..Default::default()
564        };
565        let encoded = encode_to_vec(&npdu);
566        // control: network_message = 0x80
567        assert_eq!(encoded[1] & 0x80, 0x80);
568        let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
569        assert_eq!(decoded, npdu);
570    }
571
572    #[test]
573    fn reject_message_to_network_payload_decodes_typed_reason_and_dnet() {
574        let decoded = decode_reject_message_to_network(&[
575            RejectMessageReason::MESSAGE_TOO_LONG.to_raw(),
576            0x12,
577            0x34,
578        ])
579        .unwrap();
580        assert_eq!(decoded.reason, RejectMessageReason::MESSAGE_TOO_LONG);
581        assert_eq!(decoded.dnet, 0x1234);
582    }
583
584    #[test]
585    fn reject_message_to_network_payload_requires_exact_length() {
586        for payload in [&[][..], &[4, 0][..], &[4, 0, 1, 2][..]] {
587            assert!(decode_reject_message_to_network(payload).is_err());
588        }
589    }
590
591    #[test]
592    fn proprietary_network_message_round_trip() {
593        let npdu = Npdu {
594            is_network_message: true,
595            message_type: Some(0x80), // Proprietary range
596            vendor_id: Some(999),
597            payload: Bytes::from_static(&[0xDE, 0xAD]),
598            ..Default::default()
599        };
600        let encoded = encode_to_vec(&npdu);
601        let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
602        assert_eq!(decoded, npdu);
603    }
604
605    #[test]
606    fn wire_format_who_is_broadcast() {
607        // Real-world WhoIs global broadcast:
608        // Version=1, Control=0x20 (dest present), DNET=0xFFFF, DLEN=0,
609        // HopCount=255, APDU=[0x10, 0x08]
610        let wire = [0x01, 0x20, 0xFF, 0xFF, 0x00, 0xFF, 0x10, 0x08];
611        let decoded = decode_npdu(Bytes::copy_from_slice(&wire)).unwrap();
612        assert!(!decoded.is_network_message);
613        assert!(!decoded.expecting_reply);
614        assert_eq!(decoded.priority, NetworkPriority::NORMAL);
615        assert_eq!(
616            decoded.destination,
617            Some(NpduAddress {
618                network: 0xFFFF,
619                mac_address: MacAddr::new(),
620            })
621        );
622        assert!(decoded.source.is_none());
623        assert_eq!(decoded.hop_count, 255);
624        assert_eq!(decoded.payload, vec![0x10, 0x08]);
625
626        // Re-encode should match
627        let reencoded = encode_to_vec(&decoded);
628        assert_eq!(reencoded, wire);
629    }
630
631    #[test]
632    fn decode_too_short() {
633        assert!(decode_npdu(Bytes::new()).is_err());
634        assert!(decode_npdu(Bytes::from_static(&[0x01])).is_err());
635    }
636
637    #[test]
638    fn decode_wrong_version() {
639        assert!(decode_npdu(Bytes::from_static(&[0x02, 0x00])).is_err());
640    }
641
642    #[test]
643    fn decode_truncated_destination() {
644        // Has destination flag but not enough bytes
645        assert!(decode_npdu(Bytes::from_static(&[0x01, 0x20, 0xFF])).is_err());
646    }
647
648    #[test]
649    fn decode_truncated_source() {
650        // Has source flag but not enough bytes after destination
651        assert!(decode_npdu(Bytes::from_static(&[0x01, 0x08, 0x00])).is_err());
652    }
653
654    // --- NPDU edge case tests ---
655
656    #[test]
657    fn npdu_network_zero() {
658        // DNET=0 is invalid per Clause 6.2.2 — rejected at encode time
659        let npdu = Npdu {
660            destination: Some(NpduAddress {
661                network: 0,
662                mac_address: MacAddr::from_slice(&[0x01]),
663            }),
664            hop_count: 255,
665            payload: Bytes::from_static(&[0x10, 0x08]),
666            ..Default::default()
667        };
668        let mut buf = BytesMut::new();
669        let result = encode_npdu(&mut buf, &npdu);
670        assert!(result.is_err());
671        let err = format!("{}", result.unwrap_err());
672        assert!(err.contains("DNET"), "got: {err}");
673    }
674
675    #[test]
676    fn npdu_network_fffe() {
677        // 0xFFFE is the max non-broadcast network number
678        // (0xFFFF is broadcast, 0xFFFE is the largest valid unicast network)
679        let npdu = Npdu {
680            destination: Some(NpduAddress {
681                network: 0xFFFE,
682                mac_address: MacAddr::from_slice(&[0x01, 0x02]),
683            }),
684            hop_count: 200,
685            payload: Bytes::from_static(&[0xAA]),
686            ..Default::default()
687        };
688        let encoded = encode_to_vec(&npdu);
689        let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
690        assert_eq!(decoded.destination.as_ref().unwrap().network, 0xFFFE);
691        assert_eq!(decoded.hop_count, 200);
692    }
693
694    #[test]
695    fn npdu_hop_count_zero() {
696        // Hop count 0 is valid (means don't forward further)
697        let npdu = Npdu {
698            destination: Some(NpduAddress {
699                network: 1000,
700                mac_address: MacAddr::new(),
701            }),
702            hop_count: 0,
703            payload: Bytes::from_static(&[0x10, 0x08]),
704            ..Default::default()
705        };
706        let encoded = encode_to_vec(&npdu);
707        let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
708        assert_eq!(decoded.hop_count, 0);
709    }
710
711    #[test]
712    fn npdu_source_with_empty_mac() {
713        // SLEN=0 is invalid for source per Clause 6.2.2 — rejected at encode time
714        let npdu = Npdu {
715            source: Some(NpduAddress {
716                network: 500,
717                mac_address: MacAddr::new(),
718            }),
719            payload: Bytes::from_static(&[0xBB]),
720            ..Default::default()
721        };
722        let mut buf = BytesMut::new();
723        let result = encode_npdu(&mut buf, &npdu);
724        assert!(result.is_err());
725        let err = format!("{}", result.unwrap_err());
726        assert!(err.contains("SLEN"), "got: {err}");
727    }
728
729    #[test]
730    fn npdu_destination_dlen_zero_broadcast_accepted() {
731        // DLEN=0 is valid for destination (broadcast) per Clause 6.2.2
732        let npdu = Npdu {
733            destination: Some(NpduAddress {
734                network: 0xFFFF,
735                mac_address: MacAddr::new(),
736            }),
737            hop_count: 255,
738            payload: Bytes::from_static(&[0x10, 0x08]),
739            ..Default::default()
740        };
741        let encoded = encode_to_vec(&npdu);
742        let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
743        assert_eq!(decoded.destination.as_ref().unwrap().network, 0xFFFF);
744        assert!(decoded.destination.as_ref().unwrap().mac_address.is_empty());
745    }
746
747    #[test]
748    fn npdu_destination_truncated_mac() {
749        // DNET + DLEN present but MAC bytes are short
750        // Version=1, Control=0x20 (dest present), DNET=1000, DLEN=6, only 2 MAC bytes
751        let data = [0x01, 0x20, 0x03, 0xE8, 0x06, 0x01, 0x02];
752        assert!(decode_npdu(Bytes::copy_from_slice(&data)).is_err());
753    }
754
755    #[test]
756    fn npdu_source_truncated_mac() {
757        // Source present but MAC bytes truncated
758        let data = [0x01, 0x08, 0x01, 0xF4, 0x04, 0x01]; // SNET=500, SLEN=4, only 1 byte
759        assert!(decode_npdu(Bytes::copy_from_slice(&data)).is_err());
760    }
761
762    #[test]
763    fn npdu_missing_hop_count() {
764        // Destination present but data ends before hop count
765        // Version=1, Control=0x20, DNET=0xFFFF, DLEN=0
766        let data = [0x01, 0x20, 0xFF, 0xFF, 0x00];
767        assert!(decode_npdu(Bytes::copy_from_slice(&data)).is_err());
768    }
769
770    #[test]
771    fn npdu_network_message_truncated_type() {
772        // Network message flag set but no message type byte
773        let data = [0x01, 0x80]; // is_network_message = true, but no type byte
774        assert!(decode_npdu(Bytes::copy_from_slice(&data)).is_err());
775    }
776
777    #[test]
778    fn npdu_proprietary_message_truncated_vendor() {
779        // Proprietary message type (>=0x80) but vendor ID missing
780        let data = [0x01, 0x80, 0x80]; // msg_type=0x80, need 2 more bytes for vendor
781        assert!(decode_npdu(Bytes::copy_from_slice(&data)).is_err());
782    }
783
784    #[test]
785    fn npdu_all_flags_round_trip() {
786        // Maximum complexity: all flags set
787        let npdu = Npdu {
788            is_network_message: false,
789            expecting_reply: true,
790            priority: NetworkPriority::LIFE_SAFETY,
791            destination: Some(NpduAddress {
792                network: 2000,
793                mac_address: MacAddr::from_slice(&[0x0A, 0x00, 0x02, 0x01, 0xBA, 0xC0]),
794            }),
795            source: Some(NpduAddress {
796                network: 1000,
797                mac_address: MacAddr::from_slice(&[0x0A, 0x00, 0x01, 0x01, 0xBA, 0xC0]),
798            }),
799            hop_count: 127,
800            payload: Bytes::from_static(&[0xDE, 0xAD, 0xBE, 0xEF]),
801            ..Default::default()
802        };
803        let encoded = encode_to_vec(&npdu);
804        let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
805        assert_eq!(decoded, npdu);
806    }
807
808    #[test]
809    fn npdu_empty_payload() {
810        // No payload at all
811        let npdu = Npdu {
812            payload: Bytes::new(),
813            ..Default::default()
814        };
815        let encoded = encode_to_vec(&npdu);
816        let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
817        assert!(decoded.payload.is_empty());
818    }
819
820    #[test]
821    fn reject_snet_zero() {
822        // Source network 0 is invalid per Clause 6.2.2 — rejected at encode time
823        let npdu = Npdu {
824            source: Some(NpduAddress {
825                network: 0,
826                mac_address: MacAddr::from_slice(&[0x01]),
827            }),
828            payload: Bytes::from_static(&[0x10, 0x08]),
829            ..Default::default()
830        };
831        let mut buf = BytesMut::new();
832        let result = encode_npdu(&mut buf, &npdu);
833        assert!(result.is_err());
834        let err = format!("{}", result.unwrap_err());
835        assert!(err.contains("SNET"), "got: {err}");
836    }
837
838    #[test]
839    fn reserved_bits_warning_still_decodes() {
840        // Reserved bits set in control byte should NOT cause decode failure
841        // (warning only). Construct wire bytes manually with reserved bit 6 set.
842        let mut data = vec![0x01, 0x40]; // version=1, control with reserved bit 6
843        data.extend_from_slice(&[0x10, 0x08]);
844
845        // Should decode successfully (warning only, not error)
846        let result = decode_npdu(Bytes::copy_from_slice(&data));
847        assert!(
848            result.is_ok(),
849            "reserved bits should not cause decode failure"
850        );
851    }
852}