bacnet-server 0.15.0

BACnet server: APDU dispatch and service handlers
Documentation
use super::*;
use bacnet_objects::file::{
    FileObject, FileRecordRead, FileStorage, FileStreamRead, FileWriteStart,
};

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum Kind {
    Range,
    Stream,
    Record,
}

impl Kind {
    pub(super) fn service(self) -> ConfirmedServiceChoice {
        if self == Self::Range {
            ConfirmedServiceChoice::READ_RANGE
        } else {
            ConfirmedServiceChoice::ATOMIC_READ_FILE
        }
    }

    pub(super) fn target(self) -> ObjectIdentifier {
        oid(
            if self == Self::Range {
                ObjectType::ANALOG_VALUE
            } else {
                ObjectType::FILE
            },
            9,
        )
    }

    pub(super) fn request(self, start: i32, count: u32) -> Bytes {
        let mut out = BytesMut::new();
        if self == Self::Range {
            range_request(
                self.target(),
                PropertyIdentifier::LOG_BUFFER,
                None,
                Some(RangeSpec::ByPosition {
                    reference_index: u64::try_from(start).unwrap(),
                    count: count as i32,
                }),
            )
            .encode(&mut out)
            .unwrap();
        } else {
            AtomicReadFileRequest {
                file_identifier: self.target(),
                access: if self == Self::Stream {
                    FileAccessMethod::Stream {
                        file_start_position: start,
                        requested_octet_count: count,
                    }
                } else {
                    FileAccessMethod::Record {
                        file_start_record: start,
                        requested_record_count: count,
                    }
                },
            }
            .encode(&mut out);
        }
        out.freeze()
    }

    pub(super) fn expected(
        self,
        invoke: u8,
        result: Option<(ErrorClass, ErrorCode)>,
    ) -> BACnetAuditNotification {
        let mut record = expected(
            self.target(),
            PropertyIdentifier::LOG_BUFFER,
            None,
            invoke,
            0,
            result,
        );
        if self != Self::Range {
            record.target_property = None;
        }
        record
    }
}

pub(super) fn range_request(
    target: ObjectIdentifier,
    property: PropertyIdentifier,
    index: Option<u32>,
    range: Option<RangeSpec>,
) -> ReadRangeRequest {
    ReadRangeRequest {
        object_identifier: target,
        property_identifier: property,
        property_array_index: index,
        range,
    }
}

struct Probe {
    kind: Kind,
    file: FileObject,
    reads: Arc<AtomicUsize>,
    failure: Option<fn() -> Error>,
    large: bool,
}

impl BACnetObject for Probe {
    fn object_identifier(&self) -> ObjectIdentifier {
        self.kind.target()
    }
    fn object_name(&self) -> &str {
        "read target"
    }
    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
        Cow::Borrowed(&[])
    }
    fn is_array_property(&self, property: PropertyIdentifier) -> bool {
        property == PropertyIdentifier::WEEKLY_SCHEDULE
    }
    // Present_Value is a vendor list here, so a ReadRange can reach a
    // property outside the configuration audit level.
    fn is_list_property(&self, property: PropertyIdentifier) -> bool {
        matches!(
            property,
            PropertyIdentifier::LOG_BUFFER | PropertyIdentifier::PRESENT_VALUE
        )
    }
    fn read_property(
        &self,
        property: PropertyIdentifier,
        index: Option<u32>,
    ) -> Result<PropertyValue, Error> {
        if self.kind != Kind::Range {
            assert_eq!(
                property,
                PropertyIdentifier::FILE_ACCESS_METHOD,
                "no extra file metadata reads"
            );
            return self.file.read_property(property, index);
        }
        self.reads.fetch_add(1, Ordering::AcqRel);
        if let Some(failure) = self.failure {
            return Err(failure());
        }
        match property {
            PropertyIdentifier::LOG_BUFFER
            | PropertyIdentifier::WEEKLY_SCHEDULE
            | PropertyIdentifier::PRESENT_VALUE => Ok(PropertyValue::List(if self.large {
                vec![PropertyValue::OctetString(vec![42; 2000])]
            } else {
                vec![PropertyValue::Unsigned(11), PropertyValue::Unsigned(22)]
            })),
            PropertyIdentifier::OBJECT_NAME => {
                Ok(PropertyValue::CharacterString("not a list".into()))
            }
            _ => Err(Error::protocol(
                ErrorClass::PROPERTY,
                ErrorCode::UNKNOWN_PROPERTY,
            )),
        }
    }
    fn write_property(
        &mut self,
        _: PropertyIdentifier,
        _: Option<u32>,
        _: PropertyValue,
        _: Option<u8>,
    ) -> Result<(), Error> {
        panic!("read-only fixture")
    }
    fn file_storage_internal(&self) -> Option<&dyn FileStorage> {
        Some(self)
    }
}

impl FileStorage for Probe {
    fn read_stream(&self, start: u64, count: u64) -> Result<FileStreamRead, Error> {
        self.reads.fetch_add(1, Ordering::AcqRel);
        if let Some(failure) = self.failure {
            return Err(failure());
        }
        self.file.read_stream(start, count)
    }
    fn read_records(&self, start: u64, count: u64) -> Result<FileRecordRead, Error> {
        self.reads.fetch_add(1, Ordering::AcqRel);
        if let Some(failure) = self.failure {
            return Err(failure());
        }
        self.file.read_records(start, count)
    }
    fn write_stream(&mut self, _: FileWriteStart, _: &[u8]) -> Result<u64, Error> {
        panic!("no writes")
    }
    fn write_records(&mut self, _: FileWriteStart, _: &[Vec<u8>]) -> Result<u64, Error> {
        panic!("no writes")
    }
}

pub(super) async fn add_target(
    fixture: &Fixture,
    kind: Kind,
    failure: Option<fn() -> Error>,
    large: bool,
) -> Arc<AtomicUsize> {
    let mut file = FileObject::new(9, "read file", "binary").unwrap();
    if kind == Kind::Record {
        file.set_file_access_method(bacnet_types::enums::FileAccessMethod::RECORD_ACCESS.to_raw());
        file.set_records(vec![vec![], vec![1, 2, 3, 4, 5]]);
    } else {
        file.set_data(if large {
            vec![42; 2000]
        } else {
            vec![1, 2, 3, 4, 5]
        });
    }
    let reads = Arc::new(AtomicUsize::new(0));
    fixture
        .server
        .db
        .write()
        .await
        .add(Box::new(Probe {
            kind,
            file,
            reads: reads.clone(),
            failure,
            large,
        }))
        .unwrap();
    reads
}

pub(super) fn assert_idle(fixture: &Fixture) {
    assert_eq!(
        fixture.server.notification_transactions.audit_resources(),
        (false, 0, 64)
    );
    assert_eq!(fixture.server.notification_transactions.active_count(), 0);
}