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