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