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