rustbac-core 0.4.0

Core BACnet protocol types, encoders, and service codecs for rustbac.
Documentation
use crate::apdu::ConfirmedRequestHeader;
use crate::encoding::{
    primitives::{
        decode_signed, decode_unsigned, encode_app_object_id, encode_app_signed,
        encode_app_unsigned,
    },
    tag::Tag,
    writer::Writer,
};
use crate::types::ObjectId;
use crate::EncodeError;

#[cfg(feature = "alloc")]
use crate::encoding::reader::Reader;
#[cfg(feature = "alloc")]
use crate::encoding::tag::AppTag;
#[cfg(feature = "alloc")]
use crate::DecodeError;
#[cfg(feature = "alloc")]
use alloc::vec::Vec;

pub const SERVICE_ATOMIC_READ_FILE: u8 = 0x06;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AtomicReadFileAccessMethod {
    Stream {
        file_start_position: i32,
        requested_octet_count: u32,
    },
    Record {
        file_start_record: i32,
        requested_record_count: u32,
    },
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AtomicReadFileRequest {
    pub file_object_id: ObjectId,
    pub access_method: AtomicReadFileAccessMethod,
    pub invoke_id: u8,
}

impl AtomicReadFileRequest {
    pub fn stream(
        file_object_id: ObjectId,
        file_start_position: i32,
        requested_octet_count: u32,
        invoke_id: u8,
    ) -> Self {
        Self {
            file_object_id,
            access_method: AtomicReadFileAccessMethod::Stream {
                file_start_position,
                requested_octet_count,
            },
            invoke_id,
        }
    }

    pub fn record(
        file_object_id: ObjectId,
        file_start_record: i32,
        requested_record_count: u32,
        invoke_id: u8,
    ) -> Self {
        Self {
            file_object_id,
            access_method: AtomicReadFileAccessMethod::Record {
                file_start_record,
                requested_record_count,
            },
            invoke_id,
        }
    }

    pub fn encode(&self, w: &mut Writer<'_>) -> Result<(), EncodeError> {
        ConfirmedRequestHeader {
            segmented: false,
            more_follows: false,
            segmented_response_accepted: true,
            max_segments: 0,
            max_apdu: 5,
            invoke_id: self.invoke_id,
            sequence_number: None,
            proposed_window_size: None,
            service_choice: SERVICE_ATOMIC_READ_FILE,
        }
        .encode(w)?;

        encode_app_object_id(w, self.file_object_id.raw())?;
        match self.access_method {
            AtomicReadFileAccessMethod::Stream {
                file_start_position,
                requested_octet_count,
            } => {
                Tag::Opening { tag_num: 0 }.encode(w)?;
                encode_app_signed(w, file_start_position)?;
                encode_app_unsigned(w, requested_octet_count)?;
                Tag::Closing { tag_num: 0 }.encode(w)?;
            }
            AtomicReadFileAccessMethod::Record {
                file_start_record,
                requested_record_count,
            } => {
                Tag::Opening { tag_num: 1 }.encode(w)?;
                encode_app_signed(w, file_start_record)?;
                encode_app_unsigned(w, requested_record_count)?;
                Tag::Closing { tag_num: 1 }.encode(w)?;
            }
        }
        Ok(())
    }
}

#[cfg(feature = "alloc")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AtomicReadFileAckAccess<'a> {
    Stream {
        file_start_position: i32,
        file_data: &'a [u8],
    },
    Record {
        file_start_record: i32,
        returned_record_count: u32,
        file_record_data: Vec<&'a [u8]>,
    },
}

#[cfg(feature = "alloc")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AtomicReadFileAck<'a> {
    pub end_of_file: bool,
    pub access_method: AtomicReadFileAckAccess<'a>,
}

#[cfg(feature = "alloc")]
impl<'a> AtomicReadFileAck<'a> {
    pub fn decode_after_header(r: &mut Reader<'a>) -> Result<Self, DecodeError> {
        let end_of_file = match Tag::decode(r)? {
            Tag::Application {
                tag: AppTag::Boolean,
                len,
            } => len != 0,
            _ => return Err(DecodeError::InvalidTag),
        };

        let access_method = match Tag::decode(r)? {
            Tag::Opening { tag_num: 0 } => {
                let file_start_position = decode_required_app_signed(r)?;
                let file_data = decode_required_app_octets(r)?;
                match Tag::decode(r)? {
                    Tag::Closing { tag_num: 0 } => {}
                    _ => return Err(DecodeError::InvalidTag),
                }
                AtomicReadFileAckAccess::Stream {
                    file_start_position,
                    file_data,
                }
            }
            Tag::Opening { tag_num: 1 } => {
                let file_start_record = decode_required_app_signed(r)?;
                let returned_record_count = decode_required_app_unsigned(r)?;

                let mut file_record_data = Vec::new();
                loop {
                    let tag = Tag::decode(r)?;
                    if tag == (Tag::Closing { tag_num: 1 }) {
                        break;
                    }
                    match tag {
                        Tag::Application {
                            tag: AppTag::OctetString,
                            len,
                        } => file_record_data.push(r.read_exact(len as usize)?),
                        _ => return Err(DecodeError::InvalidTag),
                    }
                }
                AtomicReadFileAckAccess::Record {
                    file_start_record,
                    returned_record_count,
                    file_record_data,
                }
            }
            _ => return Err(DecodeError::InvalidTag),
        };

        Ok(Self {
            end_of_file,
            access_method,
        })
    }
}

#[cfg(feature = "alloc")]
fn decode_required_app_unsigned(r: &mut Reader<'_>) -> Result<u32, DecodeError> {
    match Tag::decode(r)? {
        Tag::Application {
            tag: AppTag::UnsignedInt,
            len,
        } => decode_unsigned(r, len as usize),
        _ => Err(DecodeError::InvalidTag),
    }
}

#[cfg(feature = "alloc")]
fn decode_required_app_signed(r: &mut Reader<'_>) -> Result<i32, DecodeError> {
    match Tag::decode(r)? {
        Tag::Application {
            tag: AppTag::SignedInt,
            len,
        } => decode_signed(r, len as usize),
        _ => Err(DecodeError::InvalidTag),
    }
}

#[cfg(feature = "alloc")]
fn decode_required_app_octets<'a>(r: &mut Reader<'a>) -> Result<&'a [u8], DecodeError> {
    match Tag::decode(r)? {
        Tag::Application {
            tag: AppTag::OctetString,
            len,
        } => r.read_exact(len as usize),
        _ => Err(DecodeError::InvalidTag),
    }
}

#[cfg(test)]
mod tests {
    #[cfg(feature = "alloc")]
    use super::{AtomicReadFileAck, AtomicReadFileAckAccess};
    use super::{AtomicReadFileRequest, SERVICE_ATOMIC_READ_FILE};
    #[cfg(feature = "alloc")]
    use crate::apdu::ComplexAckHeader;
    use crate::apdu::ConfirmedRequestHeader;
    #[cfg(feature = "alloc")]
    use crate::encoding::tag::{AppTag, Tag};
    use crate::encoding::{reader::Reader, writer::Writer};
    use crate::types::{ObjectId, ObjectType};

    #[test]
    fn encode_atomic_read_file_stream_request() {
        let req = AtomicReadFileRequest::stream(ObjectId::new(ObjectType::File, 7), 0, 512, 4);
        let mut buf = [0u8; 128];
        let mut w = Writer::new(&mut buf);
        req.encode(&mut w).unwrap();

        let mut r = Reader::new(w.as_written());
        let hdr = ConfirmedRequestHeader::decode(&mut r).unwrap();
        assert_eq!(hdr.service_choice, SERVICE_ATOMIC_READ_FILE);
    }

    #[cfg(feature = "alloc")]
    #[test]
    fn decode_atomic_read_file_ack_stream() {
        let mut buf = [0u8; 128];
        let mut w = Writer::new(&mut buf);
        ComplexAckHeader {
            segmented: false,
            more_follows: false,
            invoke_id: 9,
            sequence_number: None,
            proposed_window_size: None,
            service_choice: SERVICE_ATOMIC_READ_FILE,
        }
        .encode(&mut w)
        .unwrap();
        Tag::Application {
            tag: AppTag::Boolean,
            len: 1,
        }
        .encode(&mut w)
        .unwrap();
        Tag::Opening { tag_num: 0 }.encode(&mut w).unwrap();
        Tag::Application {
            tag: AppTag::SignedInt,
            len: 1,
        }
        .encode(&mut w)
        .unwrap();
        w.write_u8(0).unwrap();
        Tag::Application {
            tag: AppTag::OctetString,
            len: 4,
        }
        .encode(&mut w)
        .unwrap();
        w.write_all(&[1, 2, 3, 4]).unwrap();
        Tag::Closing { tag_num: 0 }.encode(&mut w).unwrap();

        let mut r = Reader::new(w.as_written());
        let _ack = ComplexAckHeader::decode(&mut r).unwrap();
        let parsed = AtomicReadFileAck::decode_after_header(&mut r).unwrap();
        assert!(parsed.end_of_file);
        match parsed.access_method {
            AtomicReadFileAckAccess::Stream {
                file_start_position,
                file_data,
            } => {
                assert_eq!(file_start_position, 0);
                assert_eq!(file_data, &[1, 2, 3, 4]);
            }
            other => panic!("unexpected ack access: {other:?}"),
        }
    }
}