use crate::object::{
BacnetObject, ObjectError, ObjectIdentifier, ObjectType, PropertyIdentifier, PropertyValue,
Result,
};
#[cfg(not(feature = "std"))]
use alloc::{string::String, vec::Vec};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum FileAccessMethod {
RecordAccess = 0,
StreamAccess = 1,
}
#[derive(Debug, Clone)]
pub struct File {
pub identifier: ObjectIdentifier,
pub object_name: String,
pub file_type: String,
pub file_size: u32,
pub modification_date: crate::object::Date,
pub archive: bool,
pub read_only: bool,
pub file_access_method: FileAccessMethod,
pub record_count: Option<u32>,
pub description: String,
pub file_data: Vec<u8>,
}
impl File {
pub fn new(instance: u32, object_name: String, file_type: String) -> Self {
Self {
identifier: ObjectIdentifier::new(ObjectType::File, instance),
object_name,
file_type,
file_size: 0,
modification_date: crate::object::Date {
year: 2024,
month: 1,
day: 1,
weekday: 1,
},
archive: false,
read_only: false,
file_access_method: FileAccessMethod::StreamAccess,
record_count: None,
description: String::new(),
file_data: Vec::new(),
}
}
pub fn set_file_data(&mut self, data: Vec<u8>) {
self.file_data = data;
self.file_size = self.file_data.len() as u32;
}
pub fn get_file_data(&self) -> &[u8] {
&self.file_data
}
pub fn read_data(&self, start_position: u32, requested_count: u32) -> Result<Vec<u8>> {
let start = start_position as usize;
let end = (start_position + requested_count) as usize;
if start >= self.file_data.len() {
return Ok(Vec::new()); }
let actual_end = end.min(self.file_data.len());
Ok(self.file_data[start..actual_end].to_vec())
}
pub fn write_data(&mut self, start_position: u32, data: &[u8]) -> Result<()> {
if self.read_only {
return Err(ObjectError::WriteAccessDenied);
}
let start = start_position as usize;
let data_len = data.len();
let required_len = start + data_len;
if required_len > self.file_data.len() {
self.file_data.resize(required_len, 0);
}
self.file_data[start..start + data_len].copy_from_slice(data);
self.file_size = self.file_data.len() as u32;
Ok(())
}
pub fn read_records(&self, start_record: u32, record_count: u32) -> Result<Vec<Vec<u8>>> {
if self.file_access_method != FileAccessMethod::RecordAccess {
return Err(ObjectError::InvalidValue(
"File is not configured for record access".to_string(),
));
}
let mut records = Vec::new();
let file_str = String::from_utf8_lossy(&self.file_data);
let lines: Vec<&str> = file_str.lines().collect();
let start_idx = start_record as usize;
let end_idx = (start_record + record_count) as usize;
for line in lines.iter().take(end_idx.min(lines.len())).skip(start_idx) {
records.push(line.as_bytes().to_vec());
}
Ok(records)
}
pub fn write_records(&mut self, start_record: u32, records: &[Vec<u8>]) -> Result<()> {
if self.read_only {
return Err(ObjectError::WriteAccessDenied);
}
if self.file_access_method != FileAccessMethod::RecordAccess {
return Err(ObjectError::InvalidValue(
"File is not configured for record access".to_string(),
));
}
let file_str = String::from_utf8_lossy(&self.file_data);
let mut lines: Vec<String> = file_str.lines().map(|s| s.to_string()).collect();
let start_idx = start_record as usize;
while lines.len() < start_idx + records.len() {
lines.push(String::new());
}
for (i, record) in records.iter().enumerate() {
let record_str = String::from_utf8_lossy(record);
lines[start_idx + i] = record_str.to_string();
}
let new_data = lines.join("\n");
self.file_data = new_data.into_bytes();
self.file_size = self.file_data.len() as u32;
self.record_count = Some(lines.len() as u32);
Ok(())
}
}
impl BacnetObject for File {
fn identifier(&self) -> ObjectIdentifier {
self.identifier
}
fn get_property(&self, property: PropertyIdentifier) -> Result<PropertyValue> {
match property {
PropertyIdentifier::ObjectIdentifier => {
Ok(PropertyValue::ObjectIdentifier(self.identifier))
}
PropertyIdentifier::ObjectName => {
Ok(PropertyValue::CharacterString(self.object_name.clone()))
}
PropertyIdentifier::ObjectType => {
Ok(PropertyValue::Enumerated(u32::from(ObjectType::File)))
}
PropertyIdentifier::Archive => Ok(PropertyValue::Boolean(self.archive)),
_ => Err(ObjectError::UnknownProperty),
}
}
fn set_property(&mut self, property: PropertyIdentifier, value: PropertyValue) -> Result<()> {
match property {
PropertyIdentifier::ObjectName => {
if let PropertyValue::CharacterString(name) = value {
self.object_name = name;
Ok(())
} else {
Err(ObjectError::InvalidPropertyType)
}
}
PropertyIdentifier::Archive => {
if let PropertyValue::Boolean(archive) = value {
self.archive = archive;
Ok(())
} else {
Err(ObjectError::InvalidPropertyType)
}
}
_ => Err(ObjectError::PropertyNotWritable),
}
}
fn is_property_writable(&self, property: PropertyIdentifier) -> bool {
matches!(
property,
PropertyIdentifier::ObjectName | PropertyIdentifier::Archive
)
}
fn property_list(&self) -> Vec<PropertyIdentifier> {
vec![
PropertyIdentifier::ObjectIdentifier,
PropertyIdentifier::ObjectName,
PropertyIdentifier::ObjectType,
PropertyIdentifier::Archive,
]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_file_creation() {
let file = File::new(1, "config.txt".to_string(), "text/plain".to_string());
assert_eq!(file.identifier.instance, 1);
assert_eq!(file.object_name, "config.txt");
assert_eq!(file.file_type, "text/plain");
assert_eq!(file.file_size, 0);
}
#[test]
fn test_file_data_operations() {
let mut file = File::new(
1,
"test.dat".to_string(),
"application/octet-stream".to_string(),
);
let data = b"Hello, BACnet File!".to_vec();
file.set_file_data(data.clone());
assert_eq!(file.file_size, data.len() as u32);
assert_eq!(file.get_file_data(), data.as_slice());
let read_data = file.read_data(0, 5).unwrap();
assert_eq!(read_data, b"Hello");
let read_data = file.read_data(7, 6).unwrap();
assert_eq!(read_data, b"BACnet");
file.write_data(7, b"Rust ").unwrap();
let expected = b"Hello, Rust File!";
assert_eq!(file.get_file_data(), expected);
}
#[test]
fn test_file_record_operations() {
let mut file = File::new(1, "records.txt".to_string(), "text/plain".to_string());
file.file_access_method = FileAccessMethod::RecordAccess;
let initial_data = "Line 1\nLine 2\nLine 3\nLine 4".as_bytes().to_vec();
file.set_file_data(initial_data);
let records = file.read_records(1, 2).unwrap();
assert_eq!(records.len(), 2);
assert_eq!(records[0], b"Line 2");
assert_eq!(records[1], b"Line 3");
let new_records = vec![b"New Line 2".to_vec(), b"New Line 3".to_vec()];
file.write_records(1, &new_records).unwrap();
let updated_records = file.read_records(0, 4).unwrap();
assert_eq!(updated_records[0], b"Line 1");
assert_eq!(updated_records[1], b"New Line 2");
assert_eq!(updated_records[2], b"New Line 3");
assert_eq!(updated_records[3], b"Line 4");
}
#[test]
fn test_file_properties() {
let mut file = File::new(1, "test.txt".to_string(), "text/plain".to_string());
let name = file.get_property(PropertyIdentifier::ObjectName).unwrap();
if let PropertyValue::CharacterString(n) = name {
assert_eq!(n, "test.txt");
} else {
panic!("Expected CharacterString");
}
file.set_property(PropertyIdentifier::Archive, PropertyValue::Boolean(true))
.unwrap();
assert!(file.archive);
}
#[test]
fn test_read_only_protection() {
let mut file = File::new(1, "readonly.txt".to_string(), "text/plain".to_string());
file.read_only = true;
assert!(file.write_data(0, b"test").is_err());
file.file_access_method = FileAccessMethod::RecordAccess;
let records = vec![b"test".to_vec()];
assert!(file.write_records(0, &records).is_err());
}
}