Skip to main content

bacnet_encoding/
npdu.rs

1//! The owned NPDU (ASHRAE 135-2020 Clause 6) and its codec.
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//! [`Npdu`] holds its addresses and payload in owned buffers. It decodes
8//! and encodes through `bacnet-npdu`'s borrowed view, the one NPDU codec of
9//! the stack, so the router core, the non-router network layer and every
10//! layer above them accept and refuse the same frames. Both directions hold
11//! DADR and SADR to [`MAX_MAC_LEN`] octets (#1141), so everything above the
12//! network layer only ever sees an address that some data link could carry.
13
14use bacnet_npdu::{Address, EncodeError, MAX_MAC_LEN, NetworkMessageType, NetworkPriority};
15use bacnet_types::MacAddr;
16use bacnet_types::error::{Error, RangeError};
17use bytes::{Bytes, BytesMut};
18
19mod decode_error;
20pub use decode_error::NpduDecodeError;
21
22/// `bacnet-npdu`, whose types this module's API speaks (the priority, the
23/// message type, the decode faults and the address bound), re-exported so
24/// its version can't drift apart from this crate's: the view this codec
25/// runs on is [`view::Npdu`].
26pub use bacnet_npdu as view;
27
28// ---------------------------------------------------------------------------
29// Address used in NPDU routing fields
30// ---------------------------------------------------------------------------
31
32/// Network-layer address: network number + MAC address.
33///
34/// Used for source/destination fields in routed NPDUs. The MAC holds at most
35/// [`MAX_MAC_LEN`] octets on the wire: the decoder refuses a longer one and
36/// the encoder refuses to write it.
37#[derive(Debug, Clone, PartialEq, Eq, Hash)]
38pub struct NpduAddress {
39    /// Network number (1-65534, or 0xFFFF for global broadcast destination).
40    pub network: u16,
41    /// MAC-layer address (variable length, empty for broadcast).
42    pub mac_address: MacAddr,
43}
44
45impl NpduAddress {
46    /// The borrowed form `bacnet-npdu`'s view uses.
47    fn view(&self) -> Address<'_> {
48        Address {
49            network: self.network,
50            mac: &self.mac_address,
51        }
52    }
53}
54
55/// An owned copy of a borrowed address.
56impl From<Address<'_>> for NpduAddress {
57    fn from(address: Address<'_>) -> Self {
58        Self {
59            network: address.network,
60            mac_address: MacAddr::from_slice(address.mac),
61        }
62    }
63}
64
65// ---------------------------------------------------------------------------
66// NPDU struct
67// ---------------------------------------------------------------------------
68
69/// Decoded Network Protocol Data Unit (Clause 6.2).
70///
71/// A network-layer message is one whose [`message_type`](Self::message_type)
72/// is set; without it the payload is an APDU.
73#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct Npdu {
75    /// Whether the sender expects a reply.
76    pub expecting_reply: bool,
77    /// Message priority.
78    pub priority: NetworkPriority,
79    /// Remote destination address, if routed.
80    pub destination: Option<NpduAddress>,
81    /// Originating source address (populated by routers).
82    pub source: Option<NpduAddress>,
83    /// Remaining hop count for routed messages (0-255). Only an NPDU with a
84    /// destination carries one: without it this is 255, and the encoder
85    /// leaves it out.
86    pub hop_count: u8,
87    /// The network-layer message type, or `None` for an APDU.
88    pub message_type: Option<NetworkMessageType>,
89    /// Vendor ID for a proprietary network message type
90    /// ([`NetworkMessageType::is_proprietary`]).
91    pub vendor_id: Option<u16>,
92    /// Payload: either APDU bytes or network message data.
93    pub payload: Bytes,
94}
95
96impl Npdu {
97    /// Whether it carries a network-layer message rather than an APDU.
98    pub fn is_network_message(&self) -> bool {
99        self.message_type.is_some()
100    }
101
102    /// An owned copy of `view`'s control information, with an empty
103    /// payload. The payload is the caller's to attach: the frame the view
104    /// was read from, advanced by [`payload_offset`](view::Npdu::payload_offset),
105    /// shares the frame's buffer instead of copying it.
106    pub fn header_from(view: &view::Npdu<'_>) -> Self {
107        Self {
108            expecting_reply: view.expecting_reply,
109            priority: view.priority,
110            destination: view.destination.map(NpduAddress::from),
111            source: view.source.map(NpduAddress::from),
112            hop_count: view.hop_count,
113            message_type: view.message_type,
114            vendor_id: view.vendor_id,
115            payload: Bytes::new(),
116        }
117    }
118
119    /// The borrowed form `bacnet-npdu`'s encoder writes.
120    fn view(&self) -> bacnet_npdu::Npdu<'_> {
121        bacnet_npdu::Npdu {
122            expecting_reply: self.expecting_reply,
123            priority: self.priority,
124            destination: self.destination.as_ref().map(NpduAddress::view),
125            source: self.source.as_ref().map(NpduAddress::view),
126            hop_count: self.hop_count,
127            message_type: self.message_type,
128            vendor_id: self.vendor_id,
129            payload: &self.payload,
130            payload_offset: 0,
131        }
132    }
133}
134
135impl Default for Npdu {
136    fn default() -> Self {
137        Self {
138            expecting_reply: false,
139            priority: NetworkPriority::Normal,
140            destination: None,
141            source: None,
142            hop_count: 255,
143            message_type: None,
144            vendor_id: None,
145            payload: Bytes::new(),
146        }
147    }
148}
149
150// ---------------------------------------------------------------------------
151// Encoding
152// ---------------------------------------------------------------------------
153
154/// Encode an NPDU to wire format, appending it to `buf`.
155///
156/// Nothing is appended when the NPDU is one the decoder would refuse: a
157/// DNET of 0, an SNET of 0 or 0xFFFF, an empty SADR, or a DADR or SADR past
158/// [`MAX_MAC_LEN`].
159pub fn encode_npdu(buf: &mut BytesMut, npdu: &Npdu) -> Result<(), Error> {
160    let view = npdu.view();
161    let start = buf.len();
162    buf.resize(start + view.encoded_len(), 0);
163    match view.encode_into(&mut buf[start..]) {
164        Ok(_) => Ok(()),
165        Err(refused) => {
166            buf.truncate(start);
167            Err(encode_error(npdu, refused))
168        }
169    }
170}
171
172/// An encoder refusal as a static [`Error::Range`] naming the field it
173/// concerns, formatted only when displayed (#1777).
174fn encode_error(npdu: &Npdu, refused: EncodeError) -> Error {
175    let destination = npdu.destination.as_ref();
176    let source = npdu.source.as_ref();
177    match refused {
178        EncodeError::ReservedNetwork if destination.is_some_and(|d| d.network == 0) => {
179            RangeError::new("NPDU DNET must not be 0")
180        }
181        EncodeError::ReservedNetwork => RangeError::new("NPDU SNET must be 1..65534, got {}")
182            .with_value(source.map_or(0, |s| s.network).into()),
183        EncodeError::EmptySource => RangeError::new("NPDU SLEN must not be 0"),
184        EncodeError::AddressTooLong => {
185            let (label, address) = match destination {
186                Some(d) if d.mac_address.len() > MAX_MAC_LEN => (
187                    "NPDU destination address of {} octets exceeds the {}-octet limit",
188                    d,
189                ),
190                _ => (
191                    "NPDU source address of {} octets exceeds the {}-octet limit",
192                    source.expect("an over-long address is one of the two"),
193                ),
194            };
195            RangeError::new(label).with_values(address.mac_address.len() as u64, MAX_MAC_LEN as u64)
196        }
197        // The buffer is sized to the NPDU, so nothing else is refused.
198        _ => RangeError::new("NPDU encoding refused"),
199    }
200    .into()
201}
202
203// ---------------------------------------------------------------------------
204// Decoding
205// ---------------------------------------------------------------------------
206
207/// Decode an NPDU from raw bytes.
208///
209/// Returns the decoded [`Npdu`]. The `payload` field contains either the
210/// APDU bytes or network message data, sharing `data`'s buffer.
211///
212/// A DLEN or SLEN past [`MAX_MAC_LEN`] fails with
213/// [`NpduDecodeError::AddressTooLong`], raised without reading the address
214/// octets, so a caller can tell it apart from every other malformation
215/// ([`NpduDecodeError::Malformed`]). For an over-long DADR the error still
216/// carries the SNET/SADR behind it, when the frame holds a valid one, so a
217/// router can address its reject (#1158).
218pub fn decode_npdu(data: Bytes) -> Result<Npdu, NpduDecodeError> {
219    // Only hosted builds log; tracing is a std-only dependency.
220    #[cfg(feature = "std")]
221    if let Some(&control) = data.get(1)
222        && control & 0x50 != 0
223    {
224        tracing::warn!(
225            control_byte = control,
226            "NPDU control byte has reserved bits set (bits 4 or 6)"
227        );
228    }
229    let view = match bacnet_npdu::Npdu::decode(&data) {
230        Ok(view) => view,
231        Err(bacnet_npdu::DecodeError::AddressTooLong {
232            field,
233            length,
234            dnet,
235            source,
236        }) => {
237            return Err(NpduDecodeError::AddressTooLong {
238                field,
239                length,
240                dnet,
241                source: source.map(NpduAddress::from),
242            });
243        }
244        Err(bacnet_npdu::DecodeError::Malformed(fault)) => {
245            return Err(NpduDecodeError::Malformed(fault));
246        }
247    };
248    let mut npdu = Npdu::header_from(&view);
249    // Advanced, not sliced: a slice is a clone, and the first clone of a
250    // frame received into a plain buffer allocates its shared header (#1713).
251    let offset = view.payload_offset;
252    let mut payload = data;
253    bytes::Buf::advance(&mut payload, offset);
254    npdu.payload = payload;
255    Ok(npdu)
256}
257
258// ---------------------------------------------------------------------------
259// Tests
260// ---------------------------------------------------------------------------
261
262#[cfg(test)]
263mod address_bound_tests;
264
265#[cfg(test)]
266mod tests {
267    use super::*;
268    use bacnet_npdu::Malformed;
269
270    fn encode_to_vec(npdu: &Npdu) -> Vec<u8> {
271        let mut buf = BytesMut::with_capacity(64);
272        encode_npdu(&mut buf, npdu).unwrap();
273        buf.to_vec()
274    }
275
276    #[test]
277    fn minimal_local_apdu() {
278        // Simplest case: local, no routing, with APDU payload
279        let npdu = Npdu {
280            payload: Bytes::from_static(&[0x10, 0x08]), // UnconfirmedRequest WhoIs
281            ..Default::default()
282        };
283        let encoded = encode_to_vec(&npdu);
284        // version=1, control=0x00 (normal priority, no flags), payload
285        assert_eq!(encoded, vec![0x01, 0x00, 0x10, 0x08]);
286
287        let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
288        assert_eq!(decoded, npdu);
289    }
290
291    #[test]
292    fn expecting_reply_flag() {
293        let npdu = Npdu {
294            expecting_reply: true,
295            payload: Bytes::from_static(&[0xAA]),
296            ..Default::default()
297        };
298        let encoded = encode_to_vec(&npdu);
299        assert_eq!(encoded[1], 0x04); // control: expecting_reply bit
300        let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
301        assert!(decoded.expecting_reply);
302    }
303
304    #[test]
305    fn priority_encoding() {
306        for (prio, expected_bits) in [
307            (NetworkPriority::Normal, 0x00),
308            (NetworkPriority::Urgent, 0x01),
309            (NetworkPriority::CriticalEquipment, 0x02),
310            (NetworkPriority::LifeSafety, 0x03),
311        ] {
312            let npdu = Npdu {
313                priority: prio,
314                payload: Bytes::new(),
315                ..Default::default()
316            };
317            let encoded = encode_to_vec(&npdu);
318            assert_eq!(encoded[1] & 0x03, expected_bits);
319            let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
320            assert_eq!(decoded.priority, prio);
321        }
322    }
323
324    #[test]
325    fn destination_only_round_trip() {
326        let npdu = Npdu {
327            destination: Some(NpduAddress {
328                network: 1000,
329                mac_address: MacAddr::from_slice(&[0x0A, 0x00, 0x01, 0x01, 0xBA, 0xC0]),
330            }),
331            hop_count: 254,
332            payload: Bytes::from_static(&[0x10, 0x08]),
333            ..Default::default()
334        };
335        let encoded = encode_to_vec(&npdu);
336        // control: has_destination = 0x20
337        assert_eq!(encoded[1] & 0x20, 0x20);
338        let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
339        assert_eq!(decoded, npdu);
340    }
341
342    #[test]
343    fn destination_broadcast() {
344        // Global broadcast: DNET=0xFFFF, DLEN=0 (no DADR)
345        let npdu = Npdu {
346            destination: Some(NpduAddress {
347                network: 0xFFFF,
348                mac_address: MacAddr::new(),
349            }),
350            hop_count: 255,
351            payload: Bytes::from_static(&[0x10, 0x08]),
352            ..Default::default()
353        };
354        let encoded = encode_to_vec(&npdu);
355        // version(1) + control(1) + DNET(2) + DLEN(1) + hop_count(1) + payload(2)
356        assert_eq!(encoded.len(), 8);
357        // DNET = 0xFFFF
358        assert_eq!(&encoded[2..4], &[0xFF, 0xFF]);
359        // DLEN = 0
360        assert_eq!(encoded[4], 0);
361
362        let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
363        assert_eq!(decoded, npdu);
364    }
365
366    #[test]
367    fn source_only_round_trip() {
368        let npdu = Npdu {
369            source: Some(NpduAddress {
370                network: 500,
371                mac_address: MacAddr::from_slice(&[0x01]),
372            }),
373            payload: Bytes::from_static(&[0x30, 0x01, 0x0C]),
374            ..Default::default()
375        };
376        let encoded = encode_to_vec(&npdu);
377        // control: has_source = 0x08
378        assert_eq!(encoded[1] & 0x08, 0x08);
379        // No hop count (no destination)
380        let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
381        assert_eq!(decoded, npdu);
382    }
383
384    #[test]
385    fn source_and_destination_round_trip() {
386        let npdu = Npdu {
387            expecting_reply: true,
388            destination: Some(NpduAddress {
389                network: 2000,
390                mac_address: MacAddr::from_slice(&[0x0A, 0x00, 0x02, 0x01, 0xBA, 0xC0]),
391            }),
392            source: Some(NpduAddress {
393                network: 1000,
394                mac_address: MacAddr::from_slice(&[0x0A, 0x00, 0x01, 0x01, 0xBA, 0xC0]),
395            }),
396            hop_count: 250,
397            payload: Bytes::from_static(&[0x00, 0x05, 0x01, 0x0C]),
398            ..Default::default()
399        };
400        let encoded = encode_to_vec(&npdu);
401        // control: destination(0x20) | source(0x08) | expecting_reply(0x04) = 0x2C
402        assert_eq!(encoded[1], 0x2C);
403        let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
404        assert_eq!(decoded, npdu);
405    }
406
407    #[test]
408    fn network_message_round_trip() {
409        let npdu = Npdu {
410            message_type: Some(NetworkMessageType::I_AM_ROUTER_TO_NETWORK),
411            payload: Bytes::from_static(&[0x03, 0xE8]), // network 1000
412            ..Default::default()
413        };
414        let encoded = encode_to_vec(&npdu);
415        // control: network_message = 0x80
416        assert_eq!(encoded[1] & 0x80, 0x80);
417        let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
418        assert_eq!(decoded, npdu);
419    }
420
421    #[test]
422    fn proprietary_network_message_round_trip() {
423        let npdu = Npdu {
424            message_type: Some(NetworkMessageType::from_raw(0x80)), // Proprietary range
425            vendor_id: Some(999),
426            payload: Bytes::from_static(&[0xDE, 0xAD]),
427            ..Default::default()
428        };
429        let encoded = encode_to_vec(&npdu);
430        let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
431        assert_eq!(decoded, npdu);
432    }
433
434    #[test]
435    fn wire_format_who_is_broadcast() {
436        // Real-world WhoIs global broadcast:
437        // Version=1, Control=0x20 (dest present), DNET=0xFFFF, DLEN=0,
438        // HopCount=255, APDU=[0x10, 0x08]
439        let wire = [0x01, 0x20, 0xFF, 0xFF, 0x00, 0xFF, 0x10, 0x08];
440        let decoded = decode_npdu(Bytes::copy_from_slice(&wire)).unwrap();
441        assert!(!decoded.is_network_message());
442        assert!(!decoded.expecting_reply);
443        assert_eq!(decoded.priority, NetworkPriority::Normal);
444        assert_eq!(
445            decoded.destination,
446            Some(NpduAddress {
447                network: 0xFFFF,
448                mac_address: MacAddr::new(),
449            })
450        );
451        assert!(decoded.source.is_none());
452        assert_eq!(decoded.hop_count, 255);
453        assert_eq!(decoded.payload, vec![0x10, 0x08]);
454
455        // Re-encode should match
456        let reencoded = encode_to_vec(&decoded);
457        assert_eq!(reencoded, wire);
458    }
459
460    #[test]
461    fn decode_too_short() {
462        assert_eq!(
463            decode_npdu(Bytes::new()),
464            Err(NpduDecodeError::Malformed(Malformed::Truncated))
465        );
466        assert_eq!(
467            decode_npdu(Bytes::from_static(&[0x01])),
468            Err(NpduDecodeError::Malformed(Malformed::Truncated))
469        );
470    }
471
472    #[test]
473    fn decode_wrong_version() {
474        assert_eq!(
475            decode_npdu(Bytes::from_static(&[0x02, 0x00])),
476            Err(NpduDecodeError::Malformed(Malformed::Version))
477        );
478    }
479
480    #[test]
481    fn decode_truncated_destination() {
482        // Has destination flag but not enough bytes
483        assert_eq!(
484            decode_npdu(Bytes::from_static(&[0x01, 0x20, 0xFF])),
485            Err(NpduDecodeError::Malformed(Malformed::Truncated))
486        );
487    }
488
489    #[test]
490    fn decode_truncated_source() {
491        // Has source flag but not enough bytes after destination
492        assert_eq!(
493            decode_npdu(Bytes::from_static(&[0x01, 0x08, 0x00])),
494            Err(NpduDecodeError::Malformed(Malformed::Truncated))
495        );
496    }
497
498    // --- NPDU edge case tests ---
499
500    #[test]
501    fn npdu_network_zero() {
502        // DNET=0 is invalid per Clause 6.2.2 — rejected at encode time
503        let npdu = Npdu {
504            destination: Some(NpduAddress {
505                network: 0,
506                mac_address: MacAddr::from_slice(&[0x01]),
507            }),
508            hop_count: 255,
509            payload: Bytes::from_static(&[0x10, 0x08]),
510            ..Default::default()
511        };
512        let mut buf = BytesMut::new();
513        let result = encode_npdu(&mut buf, &npdu);
514        assert!(result.is_err());
515        let err = format!("{}", result.unwrap_err());
516        assert!(err.contains("DNET"), "got: {err}");
517    }
518
519    #[test]
520    fn npdu_network_fffe() {
521        // 0xFFFE is the max non-broadcast network number
522        // (0xFFFF is broadcast, 0xFFFE is the largest valid unicast network)
523        let npdu = Npdu {
524            destination: Some(NpduAddress {
525                network: 0xFFFE,
526                mac_address: MacAddr::from_slice(&[0x01, 0x02]),
527            }),
528            hop_count: 200,
529            payload: Bytes::from_static(&[0xAA]),
530            ..Default::default()
531        };
532        let encoded = encode_to_vec(&npdu);
533        let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
534        assert_eq!(decoded.destination.as_ref().unwrap().network, 0xFFFE);
535        assert_eq!(decoded.hop_count, 200);
536    }
537
538    #[test]
539    fn npdu_hop_count_zero() {
540        // Hop count 0 is valid (means don't forward further)
541        let npdu = Npdu {
542            destination: Some(NpduAddress {
543                network: 1000,
544                mac_address: MacAddr::new(),
545            }),
546            hop_count: 0,
547            payload: Bytes::from_static(&[0x10, 0x08]),
548            ..Default::default()
549        };
550        let encoded = encode_to_vec(&npdu);
551        let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
552        assert_eq!(decoded.hop_count, 0);
553    }
554
555    #[test]
556    fn npdu_source_with_empty_mac() {
557        // SLEN=0 is invalid for source per Clause 6.2.2 — rejected at encode time
558        let npdu = Npdu {
559            source: Some(NpduAddress {
560                network: 500,
561                mac_address: MacAddr::new(),
562            }),
563            payload: Bytes::from_static(&[0xBB]),
564            ..Default::default()
565        };
566        let mut buf = BytesMut::new();
567        let result = encode_npdu(&mut buf, &npdu);
568        assert!(result.is_err());
569        let err = format!("{}", result.unwrap_err());
570        assert!(err.contains("SLEN"), "got: {err}");
571    }
572
573    #[test]
574    fn npdu_destination_dlen_zero_broadcast_accepted() {
575        // DLEN=0 is valid for destination (broadcast) per Clause 6.2.2
576        let npdu = Npdu {
577            destination: Some(NpduAddress {
578                network: 0xFFFF,
579                mac_address: MacAddr::new(),
580            }),
581            hop_count: 255,
582            payload: Bytes::from_static(&[0x10, 0x08]),
583            ..Default::default()
584        };
585        let encoded = encode_to_vec(&npdu);
586        let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
587        assert_eq!(decoded.destination.as_ref().unwrap().network, 0xFFFF);
588        assert!(decoded.destination.as_ref().unwrap().mac_address.is_empty());
589    }
590
591    #[test]
592    fn npdu_destination_truncated_mac() {
593        // DNET + DLEN present but MAC bytes are short
594        // Version=1, Control=0x20 (dest present), DNET=1000, DLEN=6, only 2 MAC bytes
595        let data = [0x01, 0x20, 0x03, 0xE8, 0x06, 0x01, 0x02];
596        assert_eq!(
597            decode_npdu(Bytes::copy_from_slice(&data)),
598            Err(NpduDecodeError::Malformed(Malformed::Truncated))
599        );
600    }
601
602    #[test]
603    fn npdu_source_truncated_mac() {
604        // Source present but MAC bytes truncated
605        let data = [0x01, 0x08, 0x01, 0xF4, 0x04, 0x01]; // SNET=500, SLEN=4, only 1 byte
606        assert_eq!(
607            decode_npdu(Bytes::copy_from_slice(&data)),
608            Err(NpduDecodeError::Malformed(Malformed::Truncated))
609        );
610    }
611
612    #[test]
613    fn npdu_missing_hop_count() {
614        // Destination present but data ends before hop count
615        // Version=1, Control=0x20, DNET=0xFFFF, DLEN=0
616        let data = [0x01, 0x20, 0xFF, 0xFF, 0x00];
617        assert_eq!(
618            decode_npdu(Bytes::copy_from_slice(&data)),
619            Err(NpduDecodeError::Malformed(Malformed::Truncated))
620        );
621    }
622
623    #[test]
624    fn npdu_network_message_truncated_type() {
625        // Network message flag set but no message type byte
626        let data = [0x01, 0x80]; // is_network_message = true, but no type byte
627        assert_eq!(
628            decode_npdu(Bytes::copy_from_slice(&data)),
629            Err(NpduDecodeError::Malformed(Malformed::Truncated))
630        );
631    }
632
633    #[test]
634    fn npdu_proprietary_message_truncated_vendor() {
635        // Proprietary message type (>=0x80) but vendor ID missing
636        let data = [0x01, 0x80, 0x80]; // msg_type=0x80, need 2 more bytes for vendor
637        assert_eq!(
638            decode_npdu(Bytes::copy_from_slice(&data)),
639            Err(NpduDecodeError::Malformed(Malformed::Truncated))
640        );
641    }
642
643    #[test]
644    fn npdu_all_flags_round_trip() {
645        // Maximum complexity: all flags set
646        let npdu = Npdu {
647            expecting_reply: true,
648            priority: NetworkPriority::LifeSafety,
649            destination: Some(NpduAddress {
650                network: 2000,
651                mac_address: MacAddr::from_slice(&[0x0A, 0x00, 0x02, 0x01, 0xBA, 0xC0]),
652            }),
653            source: Some(NpduAddress {
654                network: 1000,
655                mac_address: MacAddr::from_slice(&[0x0A, 0x00, 0x01, 0x01, 0xBA, 0xC0]),
656            }),
657            hop_count: 127,
658            payload: Bytes::from_static(&[0xDE, 0xAD, 0xBE, 0xEF]),
659            ..Default::default()
660        };
661        let encoded = encode_to_vec(&npdu);
662        let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
663        assert_eq!(decoded, npdu);
664    }
665
666    #[test]
667    fn npdu_empty_payload() {
668        // No payload at all
669        let npdu = Npdu {
670            payload: Bytes::new(),
671            ..Default::default()
672        };
673        let encoded = encode_to_vec(&npdu);
674        let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
675        assert!(decoded.payload.is_empty());
676    }
677
678    #[test]
679    fn reject_snet_zero() {
680        // Source network 0 is invalid per Clause 6.2.2 — rejected at encode time
681        let npdu = Npdu {
682            source: Some(NpduAddress {
683                network: 0,
684                mac_address: MacAddr::from_slice(&[0x01]),
685            }),
686            payload: Bytes::from_static(&[0x10, 0x08]),
687            ..Default::default()
688        };
689        let mut buf = BytesMut::new();
690        let result = encode_npdu(&mut buf, &npdu);
691        assert!(result.is_err());
692        let err = format!("{}", result.unwrap_err());
693        assert!(err.contains("SNET"), "got: {err}");
694    }
695
696    #[test]
697    fn reserved_bits_warning_still_decodes() {
698        // Reserved bits set in control byte should NOT cause decode failure
699        // (warning only). Construct wire bytes manually with reserved bit 6 set.
700        let mut data = vec![0x01, 0x40]; // version=1, control with reserved bit 6
701        data.extend_from_slice(&[0x10, 0x08]);
702
703        // Should decode successfully (warning only, not error)
704        let result = decode_npdu(Bytes::copy_from_slice(&data));
705        assert!(
706            result.is_ok(),
707            "reserved bits should not cause decode failure"
708        );
709    }
710}