rustbac-core 0.4.0

Core BACnet protocol types, encoders, and service codecs for rustbac.
Documentation
use crate::apdu::UnconfirmedRequestHeader;
use crate::encoding::{
    primitives::{
        decode_ctx_character_string, decode_unsigned, encode_ctx_character_string,
        encode_ctx_object_id, encode_ctx_unsigned,
    },
    reader::Reader,
    tag::Tag,
    writer::Writer,
};
use crate::types::ObjectId;
use crate::{DecodeError, EncodeError};

pub const SERVICE_I_HAVE: u8 = 0x01;
pub const SERVICE_WHO_HAS: u8 = 0x07;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WhoHasObject<'a> {
    ObjectId(ObjectId),
    ObjectName(&'a str),
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WhoHasRequest<'a> {
    pub low_limit: Option<u32>,
    pub high_limit: Option<u32>,
    pub object: WhoHasObject<'a>,
}

impl<'a> WhoHasRequest<'a> {
    pub const fn for_object_id(object_id: ObjectId) -> Self {
        Self {
            low_limit: None,
            high_limit: None,
            object: WhoHasObject::ObjectId(object_id),
        }
    }

    pub const fn for_object_name(object_name: &'a str) -> Self {
        Self {
            low_limit: None,
            high_limit: None,
            object: WhoHasObject::ObjectName(object_name),
        }
    }

    pub fn encode(&self, w: &mut Writer<'_>) -> Result<(), EncodeError> {
        UnconfirmedRequestHeader {
            service_choice: SERVICE_WHO_HAS,
        }
        .encode(w)?;

        match (self.low_limit, self.high_limit) {
            (Some(low), Some(high)) => {
                encode_ctx_unsigned(w, 0, low)?;
                encode_ctx_unsigned(w, 1, high)?;
            }
            (None, None) => {}
            _ => {
                return Err(EncodeError::Message(
                    "low/high limits must be both set or absent",
                ))
            }
        }

        match self.object {
            WhoHasObject::ObjectId(object_id) => encode_ctx_object_id(w, 2, object_id.raw()),
            WhoHasObject::ObjectName(object_name) => encode_ctx_character_string(w, 3, object_name),
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct IHaveRequest<'a> {
    pub device_id: ObjectId,
    pub object_id: ObjectId,
    pub object_name: &'a str,
}

impl<'a> IHaveRequest<'a> {
    pub fn decode_after_header(r: &mut Reader<'a>) -> Result<Self, DecodeError> {
        let device_id = decode_required_ctx_object_id(r, 0)?;
        let object_id = decode_required_ctx_object_id(r, 1)?;
        let object_name = match Tag::decode(r)? {
            Tag::Context { tag_num: 2, len } => decode_ctx_character_string(r, len as usize)?,
            _ => return Err(DecodeError::InvalidTag),
        };
        Ok(Self {
            device_id,
            object_id,
            object_name,
        })
    }
}

fn decode_required_ctx_object_id(
    r: &mut Reader<'_>,
    expected_tag: u8,
) -> Result<ObjectId, DecodeError> {
    match Tag::decode(r)? {
        Tag::Context { tag_num, len } if tag_num == expected_tag => {
            if len != 4 {
                return Err(DecodeError::InvalidLength);
            }
            Ok(ObjectId::from_raw(decode_unsigned(r, len as usize)?))
        }
        _ => Err(DecodeError::InvalidTag),
    }
}

#[cfg(test)]
mod tests {
    use super::{IHaveRequest, WhoHasRequest, SERVICE_I_HAVE, SERVICE_WHO_HAS};
    use crate::apdu::UnconfirmedRequestHeader;
    use crate::encoding::{
        primitives::{encode_ctx_character_string, encode_ctx_object_id},
        reader::Reader,
        writer::Writer,
    };
    use crate::types::{ObjectId, ObjectType};

    #[test]
    fn encode_who_has_request_by_id() {
        let req = WhoHasRequest::for_object_id(ObjectId::new(ObjectType::AnalogInput, 2));
        let mut buf = [0u8; 64];
        let mut w = Writer::new(&mut buf);
        req.encode(&mut w).unwrap();
        let mut r = Reader::new(w.as_written());
        let hdr = UnconfirmedRequestHeader::decode(&mut r).unwrap();
        assert_eq!(hdr.service_choice, SERVICE_WHO_HAS);
    }

    #[test]
    fn encode_who_has_request_by_name_with_limits() {
        let req = WhoHasRequest {
            low_limit: Some(1),
            high_limit: Some(100),
            object: super::WhoHasObject::ObjectName("AHU-1"),
        };
        let mut buf = [0u8; 64];
        let mut w = Writer::new(&mut buf);
        req.encode(&mut w).unwrap();
        assert_eq!(w.as_written()[0], 0x10);
    }

    #[test]
    fn decode_i_have_after_header() {
        let mut buf = [0u8; 128];
        let mut w = Writer::new(&mut buf);
        UnconfirmedRequestHeader {
            service_choice: SERVICE_I_HAVE,
        }
        .encode(&mut w)
        .unwrap();
        encode_ctx_object_id(&mut w, 0, ObjectId::new(ObjectType::Device, 5).raw()).unwrap();
        encode_ctx_object_id(&mut w, 1, ObjectId::new(ObjectType::AnalogInput, 2).raw()).unwrap();
        encode_ctx_character_string(&mut w, 2, "Zone Temp").unwrap();
        let mut r = Reader::new(w.as_written());
        let hdr = UnconfirmedRequestHeader::decode(&mut r).unwrap();
        assert_eq!(hdr.service_choice, SERVICE_I_HAVE);
        let decoded = IHaveRequest::decode_after_header(&mut r).unwrap();
        assert_eq!(decoded.device_id, ObjectId::new(ObjectType::Device, 5));
        assert_eq!(decoded.object_id, ObjectId::new(ObjectType::AnalogInput, 2));
        assert_eq!(decoded.object_name, "Zone Temp");
    }

    #[test]
    fn encode_who_has_request_rejects_partial_limits() {
        let req = WhoHasRequest {
            low_limit: Some(1),
            high_limit: None,
            object: super::WhoHasObject::ObjectName("bad"),
        };
        let mut buf = [0u8; 32];
        let mut w = Writer::new(&mut buf);
        assert!(req.encode(&mut w).is_err());
    }
}