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, encode_app_object_id, encode_app_signed, encode_app_unsigned},
    reader::Reader,
    tag::{AppTag, Tag},
    writer::Writer,
};
use crate::types::ObjectId;
use crate::{DecodeError, EncodeError};

pub const SERVICE_ATOMIC_WRITE_FILE: u8 = 0x07;

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AtomicWriteFileAccessMethod<'a> {
    Stream {
        file_start_position: i32,
        file_data: &'a [u8],
    },
    Record {
        file_start_record: i32,
        file_record_data: &'a [&'a [u8]],
    },
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AtomicWriteFileRequest<'a> {
    pub file_object_id: ObjectId,
    pub access_method: AtomicWriteFileAccessMethod<'a>,
    pub invoke_id: u8,
}

impl<'a> AtomicWriteFileRequest<'a> {
    pub fn stream(
        file_object_id: ObjectId,
        file_start_position: i32,
        file_data: &'a [u8],
        invoke_id: u8,
    ) -> Self {
        Self {
            file_object_id,
            access_method: AtomicWriteFileAccessMethod::Stream {
                file_start_position,
                file_data,
            },
            invoke_id,
        }
    }

    pub fn record(
        file_object_id: ObjectId,
        file_start_record: i32,
        file_record_data: &'a [&'a [u8]],
        invoke_id: u8,
    ) -> Self {
        Self {
            file_object_id,
            access_method: AtomicWriteFileAccessMethod::Record {
                file_start_record,
                file_record_data,
            },
            invoke_id,
        }
    }

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

        encode_app_object_id(w, self.file_object_id.raw())?;
        match self.access_method {
            AtomicWriteFileAccessMethod::Stream {
                file_start_position,
                file_data,
            } => {
                Tag::Opening { tag_num: 0 }.encode(w)?;
                encode_app_signed(w, file_start_position)?;
                Tag::Application {
                    tag: AppTag::OctetString,
                    len: file_data.len() as u32,
                }
                .encode(w)?;
                w.write_all(file_data)?;
                Tag::Closing { tag_num: 0 }.encode(w)?;
            }
            AtomicWriteFileAccessMethod::Record {
                file_start_record,
                file_record_data,
            } => {
                Tag::Opening { tag_num: 1 }.encode(w)?;
                encode_app_signed(w, file_start_record)?;
                encode_app_unsigned(w, file_record_data.len() as u32)?;
                for record in file_record_data {
                    Tag::Application {
                        tag: AppTag::OctetString,
                        len: record.len() as u32,
                    }
                    .encode(w)?;
                    w.write_all(record)?;
                }
                Tag::Closing { tag_num: 1 }.encode(w)?;
            }
        }
        Ok(())
    }
}

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

impl AtomicWriteFileAck {
    pub fn decode_after_header(r: &mut Reader<'_>) -> Result<Self, DecodeError> {
        match Tag::decode(r)? {
            Tag::Context { tag_num: 0, len } => Ok(Self::Stream {
                file_start_position: decode_signed(r, len as usize)?,
            }),
            Tag::Context { tag_num: 1, len } => Ok(Self::Record {
                file_start_record: decode_signed(r, len as usize)?,
            }),
            Tag::Opening { tag_num: 0 } => {
                let start = decode_required_ctx_signed(r, 0)?;
                match Tag::decode(r)? {
                    Tag::Closing { tag_num: 0 } => Ok(Self::Stream {
                        file_start_position: start,
                    }),
                    _ => Err(DecodeError::InvalidTag),
                }
            }
            Tag::Opening { tag_num: 1 } => {
                let start = decode_required_ctx_signed(r, 0)?;
                match Tag::decode(r)? {
                    Tag::Closing { tag_num: 1 } => Ok(Self::Record {
                        file_start_record: start,
                    }),
                    _ => Err(DecodeError::InvalidTag),
                }
            }
            _ => Err(DecodeError::InvalidTag),
        }
    }
}

fn decode_required_ctx_signed(
    r: &mut Reader<'_>,
    expected_tag_num: u8,
) -> Result<i32, DecodeError> {
    match Tag::decode(r)? {
        Tag::Context { tag_num, len } if tag_num == expected_tag_num => {
            decode_signed(r, len as usize)
        }
        _ => Err(DecodeError::InvalidTag),
    }
}

#[cfg(test)]
mod tests {
    use super::{AtomicWriteFileAck, AtomicWriteFileRequest, SERVICE_ATOMIC_WRITE_FILE};
    use crate::apdu::{ComplexAckHeader, ConfirmedRequestHeader};
    use crate::encoding::{reader::Reader, tag::Tag, writer::Writer};
    use crate::types::{ObjectId, ObjectType};

    #[test]
    fn encode_atomic_write_file_stream_request() {
        let req = AtomicWriteFileRequest::stream(
            ObjectId::new(ObjectType::File, 3),
            128,
            &[0xAA, 0xBB, 0xCC],
            5,
        );
        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_WRITE_FILE);
    }

    #[test]
    fn decode_atomic_write_file_ack_stream() {
        let mut buf = [0u8; 32];
        let mut w = Writer::new(&mut buf);
        ComplexAckHeader {
            segmented: false,
            more_follows: false,
            invoke_id: 2,
            sequence_number: None,
            proposed_window_size: None,
            service_choice: SERVICE_ATOMIC_WRITE_FILE,
        }
        .encode(&mut w)
        .unwrap();
        Tag::Context { tag_num: 0, len: 2 }.encode(&mut w).unwrap();
        w.write_all(&0x0080u16.to_be_bytes()).unwrap();

        let mut r = Reader::new(w.as_written());
        let _ack = ComplexAckHeader::decode(&mut r).unwrap();
        let parsed = AtomicWriteFileAck::decode_after_header(&mut r).unwrap();
        assert_eq!(
            parsed,
            AtomicWriteFileAck::Stream {
                file_start_position: 128
            }
        );
    }
}