rustbac-core 0.4.0

Core BACnet protocol types, encoders, and service codecs for rustbac.
Documentation
use crate::encoding::{reader::Reader, writer::Writer};
use crate::{DecodeError, EncodeError};

/// BACnet network layer protocol version (always `0x01`).
pub const NPDU_VERSION: u8 = 0x01;

/// A network-layer address consisting of a network number and a MAC address.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct NpduAddress {
    /// The DNET/SNET network number.
    pub network: u16,
    /// MAC address bytes (up to 18 — supports IPv6 virtual MACs: 16-byte addr + 2-byte port).
    pub mac: [u8; 18],
    /// Number of valid bytes in `mac`.
    pub mac_len: u8,
}

/// BACnet Network Protocol Data Unit (NPDU) header.
///
/// Handles encoding and decoding of the NPDU including optional source/
/// destination addresses, hop count, and network-layer message fields.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Npdu {
    pub control: u8,
    pub destination: Option<NpduAddress>,
    pub source: Option<NpduAddress>,
    pub hop_count: Option<u8>,
    pub message_type: Option<u8>,
    pub vendor_id: Option<u16>,
}

impl Npdu {
    pub const fn new(control: u8) -> Self {
        Self {
            control,
            destination: None,
            source: None,
            hop_count: None,
            message_type: None,
            vendor_id: None,
        }
    }

    pub fn encode(&self, w: &mut Writer<'_>) -> Result<(), EncodeError> {
        // Derive control bits from optional fields so the header is always
        // consistent, regardless of what the caller set in `self.control`.
        let mut control = self.control;
        if self.destination.is_some() {
            control |= 0x20;
        } else {
            control &= !0x20;
        }
        if self.source.is_some() {
            control |= 0x08;
        } else {
            control &= !0x08;
        }
        if self.message_type.is_some() {
            control |= 0x80;
        } else {
            control &= !0x80;
        }

        w.write_u8(NPDU_VERSION)?;
        w.write_u8(control)?;

        if let Some(dest) = self.destination {
            encode_addr(w, dest)?;
        }
        if let Some(src) = self.source {
            encode_addr(w, src)?;
        }
        if self.destination.is_some() {
            w.write_u8(self.hop_count.unwrap_or(255))?;
        }
        if let Some(mt) = self.message_type {
            w.write_u8(mt)?;
            if mt >= 0x80 {
                w.write_be_u16(self.vendor_id.unwrap_or(0))?;
            }
        }
        Ok(())
    }

    pub fn decode(r: &mut Reader<'_>) -> Result<Self, DecodeError> {
        let version = r.read_u8()?;
        if version != NPDU_VERSION {
            return Err(DecodeError::InvalidValue);
        }

        let control = r.read_u8()?;
        let has_dest = (control & 0x20) != 0;
        let has_src = (control & 0x08) != 0;
        let is_network_msg = (control & 0x80) != 0;

        let destination = if has_dest {
            Some(decode_addr(r)?)
        } else {
            None
        };
        let source = if has_src { Some(decode_addr(r)?) } else { None };
        let hop_count = if has_dest { Some(r.read_u8()?) } else { None };

        let (message_type, vendor_id) = if is_network_msg {
            let mt = r.read_u8()?;
            let vid = if mt >= 0x80 {
                Some(r.read_be_u16()?)
            } else {
                None
            };
            (Some(mt), vid)
        } else {
            (None, None)
        };

        Ok(Self {
            control,
            destination,
            source,
            hop_count,
            message_type,
            vendor_id,
        })
    }
}

fn encode_addr(w: &mut Writer<'_>, addr: NpduAddress) -> Result<(), EncodeError> {
    if addr.mac_len as usize > addr.mac.len() {
        return Err(EncodeError::InvalidLength);
    }
    w.write_be_u16(addr.network)?;
    w.write_u8(addr.mac_len)?;
    w.write_all(&addr.mac[..addr.mac_len as usize])
}

fn decode_addr(r: &mut Reader<'_>) -> Result<NpduAddress, DecodeError> {
    let network = r.read_be_u16()?;
    let mac_len = r.read_u8()?;
    if mac_len as usize > 18 {
        return Err(DecodeError::InvalidLength);
    }
    let mut mac = [0u8; 18];
    let src = r.read_exact(mac_len as usize)?;
    mac[..mac_len as usize].copy_from_slice(src);
    Ok(NpduAddress {
        network,
        mac,
        mac_len,
    })
}

#[cfg(test)]
mod tests {
    use super::{Npdu, NpduAddress};
    use crate::encoding::{reader::Reader, writer::Writer};

    #[test]
    fn npdu_roundtrip() {
        let mut p = Npdu::new(0x20);
        p.destination = Some(NpduAddress {
            network: 1,
            mac: {
                let mut m = [0u8; 18];
                m[..6].copy_from_slice(&[192, 168, 1, 2, 0xBA, 0xC0]);
                m
            },
            mac_len: 6,
        });
        p.hop_count = Some(255);

        let mut buf = [0u8; 32];
        let mut w = Writer::new(&mut buf);
        p.encode(&mut w).unwrap();

        let mut r = Reader::new(w.as_written());
        let dec = Npdu::decode(&mut r).unwrap();
        assert_eq!(dec.control, p.control);
        assert_eq!(dec.destination.unwrap().network, 1);
    }

    #[test]
    fn npdu_ipv6_mac_roundtrip() {
        let mut p = Npdu::new(0x20);
        // 18-byte virtual MAC: 16 bytes IPv6 + 2 bytes port
        let mut mac = [0u8; 18];
        mac[..16].copy_from_slice(&[
            0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0x02, 0x0c, 0x29, 0xff, 0xfe, 0x5a, 0x1c, 0x3d,
        ]);
        mac[16..].copy_from_slice(&0xBAC0u16.to_be_bytes());
        p.destination = Some(NpduAddress {
            network: 2,
            mac,
            mac_len: 18,
        });
        p.hop_count = Some(255);

        let mut buf = [0u8; 48];
        let mut w = Writer::new(&mut buf);
        p.encode(&mut w).unwrap();

        let mut r = Reader::new(w.as_written());
        let dec = Npdu::decode(&mut r).unwrap();
        let dest = dec.destination.unwrap();
        assert_eq!(dest.network, 2);
        assert_eq!(dest.mac_len, 18);
        assert_eq!(&dest.mac[..18], &mac[..]);
    }

    #[test]
    fn network_message_vendor_id_only_for_vendor_types() {
        let mut p = Npdu::new(0x80);
        p.message_type = Some(0x80);
        p.vendor_id = Some(260);

        let mut buf = [0u8; 16];
        let mut w = Writer::new(&mut buf);
        p.encode(&mut w).unwrap();

        let mut r = Reader::new(w.as_written());
        let dec = Npdu::decode(&mut r).unwrap();
        assert_eq!(dec.message_type, Some(0x80));
        assert_eq!(dec.vendor_id, Some(260));
    }
}