use bacnet_types::enums::{ObjectType, PropertyIdentifier};
use bacnet_types::error::Error;
use bacnet_types::primitives::{Date, ObjectIdentifier, PropertyValue, StatusFlags, Time};
use std::borrow::Cow;
use crate::common::{self, read_common_properties};
use crate::traits::BACnetObject;
pub struct FileObject {
oid: ObjectIdentifier,
name: String,
description: String,
file_type: String,
file_size: u64,
modification_date: (Date, Time),
archive: bool,
read_only: bool,
file_access_method: u32,
record_count: Option<u64>,
data: Vec<u8>,
records: Vec<Vec<u8>>,
status_flags: StatusFlags,
out_of_service: bool,
reliability: u32,
}
impl FileObject {
pub fn new(
instance: u32,
name: impl Into<String>,
file_type: impl Into<String>,
) -> Result<Self, Error> {
let oid = ObjectIdentifier::new(ObjectType::FILE, instance)?;
Ok(Self {
oid,
name: name.into(),
description: String::new(),
file_type: file_type.into(),
file_size: 0,
modification_date: (
Date {
year: 0xFF,
month: 0xFF,
day: 0xFF,
day_of_week: 0xFF,
},
Time {
hour: 0xFF,
minute: 0xFF,
second: 0xFF,
hundredths: 0xFF,
},
),
archive: false,
read_only: false,
file_access_method: 0,
record_count: None,
data: Vec::new(),
records: Vec::new(),
status_flags: StatusFlags::empty(),
out_of_service: false,
reliability: 0,
})
}
pub fn set_description(&mut self, desc: impl Into<String>) {
self.description = desc.into();
}
pub fn set_file_type(&mut self, ft: impl Into<String>) {
self.file_type = ft.into();
}
pub fn set_data(&mut self, data: Vec<u8>) {
self.file_size = data.len() as u64;
self.data = data;
}
pub fn data(&self) -> &[u8] {
&self.data
}
pub fn set_file_access_method(&mut self, method: u32) {
self.file_access_method = method;
if method == 1 {
self.record_count = Some(self.records.len() as u64);
} else {
self.record_count = None;
}
}
pub fn set_records(&mut self, records: Vec<Vec<u8>>) {
let total_size: u64 = records.iter().map(|r| r.len() as u64).sum();
self.file_size = total_size;
self.record_count = Some(records.len() as u64);
self.records = records;
}
pub fn records(&self) -> &[Vec<u8>] {
&self.records
}
pub fn set_modification_date(&mut self, date: Date, time: Time) {
self.modification_date = (date, time);
}
pub fn set_archive(&mut self, archive: bool) {
self.archive = archive;
}
pub fn set_read_only(&mut self, read_only: bool) {
self.read_only = read_only;
}
pub fn file_size(&self) -> u64 {
self.file_size
}
pub fn archive(&self) -> bool {
self.archive
}
pub fn read_only(&self) -> bool {
self.read_only
}
}
impl BACnetObject for FileObject {
fn object_identifier(&self) -> ObjectIdentifier {
self.oid
}
fn object_name(&self) -> &str {
&self.name
}
fn read_property(
&self,
property: PropertyIdentifier,
array_index: Option<u32>,
) -> Result<PropertyValue, Error> {
if let Some(result) = read_common_properties!(self, property, array_index) {
return result;
}
match property {
p if p == PropertyIdentifier::OBJECT_TYPE => {
Ok(PropertyValue::Enumerated(ObjectType::FILE.to_raw()))
}
p if p == PropertyIdentifier::FILE_TYPE => {
Ok(PropertyValue::CharacterString(self.file_type.clone()))
}
p if p == PropertyIdentifier::FILE_SIZE => Ok(PropertyValue::Unsigned(self.file_size)),
p if p == PropertyIdentifier::MODIFICATION_DATE => Ok(PropertyValue::List(vec![
PropertyValue::Date(self.modification_date.0),
PropertyValue::Time(self.modification_date.1),
])),
p if p == PropertyIdentifier::ARCHIVE => Ok(PropertyValue::Boolean(self.archive)),
p if p == PropertyIdentifier::READ_ONLY => Ok(PropertyValue::Boolean(self.read_only)),
p if p == PropertyIdentifier::FILE_ACCESS_METHOD => {
Ok(PropertyValue::Enumerated(self.file_access_method))
}
p if p == PropertyIdentifier::RECORD_COUNT => match self.record_count {
Some(count) => Ok(PropertyValue::Unsigned(count)),
None => Err(common::unknown_property_error()),
},
_ => Err(common::unknown_property_error()),
}
}
fn write_property(
&mut self,
property: PropertyIdentifier,
_array_index: Option<u32>,
value: PropertyValue,
_priority: Option<u8>,
) -> Result<(), Error> {
if let Some(result) = common::write_description(&mut self.description, property, &value) {
return result;
}
if let Some(result) =
common::write_out_of_service(&mut self.out_of_service, property, &value)
{
return result;
}
match property {
p if p == PropertyIdentifier::ARCHIVE => {
if let PropertyValue::Boolean(v) = value {
self.archive = v;
Ok(())
} else {
Err(common::invalid_data_type_error())
}
}
p if p == PropertyIdentifier::FILE_TYPE => {
if let PropertyValue::CharacterString(s) = value {
self.file_type = s;
Ok(())
} else {
Err(common::invalid_data_type_error())
}
}
p if p == PropertyIdentifier::READ_ONLY => {
Err(common::write_access_denied_error())
}
p if p == PropertyIdentifier::FILE_SIZE => Err(common::write_access_denied_error()),
p if p == PropertyIdentifier::FILE_ACCESS_METHOD => {
Err(common::write_access_denied_error())
}
p if p == PropertyIdentifier::MODIFICATION_DATE => {
Err(common::write_access_denied_error())
}
p if p == PropertyIdentifier::RECORD_COUNT => Err(common::write_access_denied_error()),
_ => Err(common::write_access_denied_error()),
}
}
fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
let mut props = vec![
PropertyIdentifier::OBJECT_IDENTIFIER,
PropertyIdentifier::OBJECT_NAME,
PropertyIdentifier::OBJECT_TYPE,
PropertyIdentifier::DESCRIPTION,
PropertyIdentifier::FILE_TYPE,
PropertyIdentifier::FILE_SIZE,
PropertyIdentifier::MODIFICATION_DATE,
PropertyIdentifier::ARCHIVE,
PropertyIdentifier::READ_ONLY,
PropertyIdentifier::FILE_ACCESS_METHOD,
PropertyIdentifier::STATUS_FLAGS,
PropertyIdentifier::OUT_OF_SERVICE,
PropertyIdentifier::RELIABILITY,
];
if self.record_count.is_some() {
props.push(PropertyIdentifier::RECORD_COUNT);
}
Cow::Owned(props)
}
}
#[cfg(test)]
mod tests {
use super::*;
use bacnet_types::enums::ErrorCode;
#[test]
fn file_object_creation() {
let file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
assert_eq!(file.object_name(), "FILE-1");
assert_eq!(file.object_identifier().instance_number(), 1);
}
#[test]
fn file_read_object_type() {
let file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
let val = file
.read_property(PropertyIdentifier::OBJECT_TYPE, None)
.unwrap();
assert_eq!(val, PropertyValue::Enumerated(ObjectType::FILE.to_raw()));
}
#[test]
fn file_read_object_identifier() {
let file = FileObject::new(42, "FILE-42", "application/octet-stream").unwrap();
let val = file
.read_property(PropertyIdentifier::OBJECT_IDENTIFIER, None)
.unwrap();
if let PropertyValue::ObjectIdentifier(oid) = val {
assert_eq!(oid.instance_number(), 42);
} else {
panic!("expected ObjectIdentifier");
}
}
#[test]
fn file_read_object_name() {
let file = FileObject::new(1, "MY-FILE", "text/plain").unwrap();
let val = file
.read_property(PropertyIdentifier::OBJECT_NAME, None)
.unwrap();
assert_eq!(val, PropertyValue::CharacterString("MY-FILE".into()));
}
#[test]
fn file_read_file_type() {
let file = FileObject::new(1, "FILE-1", "text/csv").unwrap();
let val = file
.read_property(PropertyIdentifier::FILE_TYPE, None)
.unwrap();
assert_eq!(val, PropertyValue::CharacterString("text/csv".into()));
}
#[test]
fn file_read_file_size_default_zero() {
let file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
let val = file
.read_property(PropertyIdentifier::FILE_SIZE, None)
.unwrap();
assert_eq!(val, PropertyValue::Unsigned(0));
}
#[test]
fn file_set_data_updates_file_size() {
let mut file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
file.set_data(vec![0x48, 0x65, 0x6C, 0x6C, 0x6F]); let val = file
.read_property(PropertyIdentifier::FILE_SIZE, None)
.unwrap();
assert_eq!(val, PropertyValue::Unsigned(5));
assert_eq!(file.data(), &[0x48, 0x65, 0x6C, 0x6C, 0x6F]);
}
#[test]
fn file_read_archive_default_false() {
let file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
let val = file
.read_property(PropertyIdentifier::ARCHIVE, None)
.unwrap();
assert_eq!(val, PropertyValue::Boolean(false));
}
#[test]
fn file_set_and_read_archive() {
let mut file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
file.set_archive(true);
assert!(file.archive());
let val = file
.read_property(PropertyIdentifier::ARCHIVE, None)
.unwrap();
assert_eq!(val, PropertyValue::Boolean(true));
}
#[test]
fn file_read_read_only_default_false() {
let file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
let val = file
.read_property(PropertyIdentifier::READ_ONLY, None)
.unwrap();
assert_eq!(val, PropertyValue::Boolean(false));
}
#[test]
fn file_set_and_read_read_only() {
let mut file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
file.set_read_only(true);
assert!(file.read_only());
let val = file
.read_property(PropertyIdentifier::READ_ONLY, None)
.unwrap();
assert_eq!(val, PropertyValue::Boolean(true));
}
#[test]
fn file_read_modification_date_default_unspecified() {
let file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
let val = file
.read_property(PropertyIdentifier::MODIFICATION_DATE, None)
.unwrap();
if let PropertyValue::List(items) = val {
assert_eq!(items.len(), 2);
let unspec_date = Date {
year: 0xFF,
month: 0xFF,
day: 0xFF,
day_of_week: 0xFF,
};
let unspec_time = Time {
hour: 0xFF,
minute: 0xFF,
second: 0xFF,
hundredths: 0xFF,
};
assert_eq!(items[0], PropertyValue::Date(unspec_date));
assert_eq!(items[1], PropertyValue::Time(unspec_time));
} else {
panic!("expected PropertyValue::List");
}
}
#[test]
fn file_set_and_read_modification_date() {
let mut file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
let d = Date {
year: 126,
month: 3,
day: 1,
day_of_week: 7,
};
let t = Time {
hour: 14,
minute: 30,
second: 0,
hundredths: 0,
};
file.set_modification_date(d, t);
let val = file
.read_property(PropertyIdentifier::MODIFICATION_DATE, None)
.unwrap();
if let PropertyValue::List(items) = val {
assert_eq!(items[0], PropertyValue::Date(d));
assert_eq!(items[1], PropertyValue::Time(t));
} else {
panic!("expected PropertyValue::List");
}
}
#[test]
fn file_read_file_access_method_default_stream() {
let file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
let val = file
.read_property(PropertyIdentifier::FILE_ACCESS_METHOD, None)
.unwrap();
assert_eq!(val, PropertyValue::Enumerated(0));
}
#[test]
fn file_record_count_unavailable_for_stream() {
let file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
let result = file.read_property(PropertyIdentifier::RECORD_COUNT, None);
assert!(result.is_err());
}
#[test]
fn file_set_records_updates_record_count_and_size() {
let mut file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
file.set_file_access_method(1);
file.set_records(vec![vec![0x01, 0x02], vec![0x03, 0x04, 0x05]]);
let count = file
.read_property(PropertyIdentifier::RECORD_COUNT, None)
.unwrap();
assert_eq!(count, PropertyValue::Unsigned(2));
let size = file
.read_property(PropertyIdentifier::FILE_SIZE, None)
.unwrap();
assert_eq!(size, PropertyValue::Unsigned(5)); assert_eq!(file.records().len(), 2);
}
#[test]
fn file_read_status_flags_default() {
let file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
let val = file
.read_property(PropertyIdentifier::STATUS_FLAGS, None)
.unwrap();
if let PropertyValue::BitString { unused_bits, data } = val {
assert_eq!(unused_bits, 4);
assert_eq!(data, vec![0x00]);
} else {
panic!("expected BitString");
}
}
#[test]
fn file_read_out_of_service_default_false() {
let file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
let val = file
.read_property(PropertyIdentifier::OUT_OF_SERVICE, None)
.unwrap();
assert_eq!(val, PropertyValue::Boolean(false));
}
#[test]
fn file_read_reliability_default() {
let file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
let val = file
.read_property(PropertyIdentifier::RELIABILITY, None)
.unwrap();
assert_eq!(val, PropertyValue::Enumerated(0));
}
#[test]
fn file_read_description_default_empty() {
let file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
let val = file
.read_property(PropertyIdentifier::DESCRIPTION, None)
.unwrap();
assert_eq!(val, PropertyValue::CharacterString(String::new()));
}
#[test]
fn file_write_description() {
let mut file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
file.write_property(
PropertyIdentifier::DESCRIPTION,
None,
PropertyValue::CharacterString("A test file".into()),
None,
)
.unwrap();
let val = file
.read_property(PropertyIdentifier::DESCRIPTION, None)
.unwrap();
assert_eq!(val, PropertyValue::CharacterString("A test file".into()));
}
#[test]
fn file_write_archive() {
let mut file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
file.write_property(
PropertyIdentifier::ARCHIVE,
None,
PropertyValue::Boolean(true),
None,
)
.unwrap();
let val = file
.read_property(PropertyIdentifier::ARCHIVE, None)
.unwrap();
assert_eq!(val, PropertyValue::Boolean(true));
}
#[test]
fn file_write_archive_invalid_type() {
let mut file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
let result = file.write_property(
PropertyIdentifier::ARCHIVE,
None,
PropertyValue::Unsigned(1),
None,
);
assert!(result.is_err());
}
#[test]
fn file_write_file_type() {
let mut file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
file.write_property(
PropertyIdentifier::FILE_TYPE,
None,
PropertyValue::CharacterString("application/json".into()),
None,
)
.unwrap();
let val = file
.read_property(PropertyIdentifier::FILE_TYPE, None)
.unwrap();
assert_eq!(
val,
PropertyValue::CharacterString("application/json".into())
);
}
#[test]
fn file_write_out_of_service() {
let mut file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
file.write_property(
PropertyIdentifier::OUT_OF_SERVICE,
None,
PropertyValue::Boolean(true),
None,
)
.unwrap();
let val = file
.read_property(PropertyIdentifier::OUT_OF_SERVICE, None)
.unwrap();
assert_eq!(val, PropertyValue::Boolean(true));
}
#[test]
fn file_write_read_only_denied() {
let mut file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
let result = file.write_property(
PropertyIdentifier::READ_ONLY,
None,
PropertyValue::Boolean(true),
None,
);
assert!(result.is_err());
}
#[test]
fn file_write_file_size_denied() {
let mut file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
let result = file.write_property(
PropertyIdentifier::FILE_SIZE,
None,
PropertyValue::Unsigned(100),
None,
);
assert!(result.is_err());
}
#[test]
fn file_property_list_stream() {
let file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
let props = file.property_list();
assert!(props.contains(&PropertyIdentifier::OBJECT_IDENTIFIER));
assert!(props.contains(&PropertyIdentifier::OBJECT_NAME));
assert!(props.contains(&PropertyIdentifier::OBJECT_TYPE));
assert!(props.contains(&PropertyIdentifier::FILE_TYPE));
assert!(props.contains(&PropertyIdentifier::FILE_SIZE));
assert!(props.contains(&PropertyIdentifier::MODIFICATION_DATE));
assert!(props.contains(&PropertyIdentifier::ARCHIVE));
assert!(props.contains(&PropertyIdentifier::READ_ONLY));
assert!(props.contains(&PropertyIdentifier::FILE_ACCESS_METHOD));
assert!(props.contains(&PropertyIdentifier::STATUS_FLAGS));
assert!(props.contains(&PropertyIdentifier::OUT_OF_SERVICE));
assert!(props.contains(&PropertyIdentifier::RELIABILITY));
assert!(!props.contains(&PropertyIdentifier::RECORD_COUNT));
}
#[test]
fn file_property_list_record_access() {
let mut file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
file.set_file_access_method(1);
let props = file.property_list();
assert!(props.contains(&PropertyIdentifier::RECORD_COUNT));
}
#[test]
fn file_unknown_property_error() {
let file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
let result = file.read_property(PropertyIdentifier::PRESENT_VALUE, None);
assert!(result.is_err());
if let Err(Error::Protocol { code, .. }) = result {
assert_eq!(code, ErrorCode::UNKNOWN_PROPERTY.to_raw() as u32);
} else {
panic!("expected Protocol error");
}
}
}