Skip to main content

bacnet_rs/object/
file.rs

1//! File Object Implementation
2//!
3//! This module implements the File object type as defined in ASHRAE 135.
4//! File objects represent files that can be accessed using the AtomicReadFile
5//! and AtomicWriteFile services.
6
7use crate::object::{
8    BacnetObject, ObjectError, ObjectIdentifier, ObjectType, PropertyIdentifier, PropertyValue,
9    Result,
10};
11
12#[cfg(not(feature = "std"))]
13use alloc::{string::String, vec::Vec};
14
15/// File access method enumeration
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17#[repr(u32)]
18pub enum FileAccessMethod {
19    RecordAccess = 0,
20    StreamAccess = 1,
21}
22
23/// File object implementation
24#[derive(Debug, Clone)]
25pub struct File {
26    /// Object identifier
27    pub identifier: ObjectIdentifier,
28    /// Object name
29    pub object_name: String,
30    /// File type (MIME type or file extension)
31    pub file_type: String,
32    /// File size in octets
33    pub file_size: u32,
34    /// Modification date (BACnet Date format)
35    pub modification_date: crate::object::Date,
36    /// Archive flag
37    pub archive: bool,
38    /// Read only flag
39    pub read_only: bool,
40    /// File access method
41    pub file_access_method: FileAccessMethod,
42    /// Record count (for record access method)
43    pub record_count: Option<u32>,
44    /// Description
45    pub description: String,
46    /// File contents (in-memory storage for this implementation)
47    pub file_data: Vec<u8>,
48}
49
50impl File {
51    /// Create a new File object
52    pub fn new(instance: u32, object_name: String, file_type: String) -> Self {
53        Self {
54            identifier: ObjectIdentifier::new(ObjectType::File, instance),
55            object_name,
56            file_type,
57            file_size: 0,
58            modification_date: crate::object::Date {
59                year: 2024,
60                month: 1,
61                day: 1,
62                weekday: 1,
63            },
64            archive: false,
65            read_only: false,
66            file_access_method: FileAccessMethod::StreamAccess,
67            record_count: None,
68            description: String::new(),
69            file_data: Vec::new(),
70        }
71    }
72
73    /// Set file contents
74    pub fn set_file_data(&mut self, data: Vec<u8>) {
75        self.file_data = data;
76        self.file_size = self.file_data.len() as u32;
77        // Update modification date to current (simplified)
78        // In a real implementation, this would use actual system time
79    }
80
81    /// Get file contents
82    pub fn get_file_data(&self) -> &[u8] {
83        &self.file_data
84    }
85
86    /// Read data from file at specified position
87    pub fn read_data(&self, start_position: u32, requested_count: u32) -> Result<Vec<u8>> {
88        let start = start_position as usize;
89        let end = (start_position + requested_count) as usize;
90
91        if start >= self.file_data.len() {
92            return Ok(Vec::new()); // EOF
93        }
94
95        let actual_end = end.min(self.file_data.len());
96        Ok(self.file_data[start..actual_end].to_vec())
97    }
98
99    /// Write data to file at specified position
100    pub fn write_data(&mut self, start_position: u32, data: &[u8]) -> Result<()> {
101        if self.read_only {
102            return Err(ObjectError::WriteAccessDenied);
103        }
104
105        let start = start_position as usize;
106        let data_len = data.len();
107        let required_len = start + data_len;
108
109        // Extend file if necessary
110        if required_len > self.file_data.len() {
111            self.file_data.resize(required_len, 0);
112        }
113
114        // Write the data (overwrite existing data at this position)
115        self.file_data[start..start + data_len].copy_from_slice(data);
116        self.file_size = self.file_data.len() as u32;
117
118        Ok(())
119    }
120
121    /// Read records from file (for record access method)
122    pub fn read_records(&self, start_record: u32, record_count: u32) -> Result<Vec<Vec<u8>>> {
123        if self.file_access_method != FileAccessMethod::RecordAccess {
124            return Err(ObjectError::InvalidValue(
125                "File is not configured for record access".to_string(),
126            ));
127        }
128
129        // This is a simplified implementation
130        // In practice, records would have defined structure and separators
131        let mut records = Vec::new();
132
133        // For demonstration, treat each line as a record
134        let file_str = String::from_utf8_lossy(&self.file_data);
135        let lines: Vec<&str> = file_str.lines().collect();
136
137        let start_idx = start_record as usize;
138        let end_idx = (start_record + record_count) as usize;
139
140        for line in lines.iter().take(end_idx.min(lines.len())).skip(start_idx) {
141            records.push(line.as_bytes().to_vec());
142        }
143
144        Ok(records)
145    }
146
147    /// Write records to file (for record access method)
148    pub fn write_records(&mut self, start_record: u32, records: &[Vec<u8>]) -> Result<()> {
149        if self.read_only {
150            return Err(ObjectError::WriteAccessDenied);
151        }
152
153        if self.file_access_method != FileAccessMethod::RecordAccess {
154            return Err(ObjectError::InvalidValue(
155                "File is not configured for record access".to_string(),
156            ));
157        }
158
159        // This is a simplified implementation
160        // Convert current data to lines
161        let file_str = String::from_utf8_lossy(&self.file_data);
162        let mut lines: Vec<String> = file_str.lines().map(|s| s.to_string()).collect();
163
164        let start_idx = start_record as usize;
165
166        // Extend lines vector if necessary
167        while lines.len() < start_idx + records.len() {
168            lines.push(String::new());
169        }
170
171        // Replace records
172        for (i, record) in records.iter().enumerate() {
173            let record_str = String::from_utf8_lossy(record);
174            lines[start_idx + i] = record_str.to_string();
175        }
176
177        // Convert back to file data
178        let new_data = lines.join("\n");
179        self.file_data = new_data.into_bytes();
180        self.file_size = self.file_data.len() as u32;
181        self.record_count = Some(lines.len() as u32);
182
183        Ok(())
184    }
185}
186
187impl BacnetObject for File {
188    fn identifier(&self) -> ObjectIdentifier {
189        self.identifier
190    }
191
192    fn get_property(&self, property: PropertyIdentifier) -> Result<PropertyValue> {
193        match property {
194            PropertyIdentifier::ObjectIdentifier => {
195                Ok(PropertyValue::ObjectIdentifier(self.identifier))
196            }
197            PropertyIdentifier::ObjectName => {
198                Ok(PropertyValue::CharacterString(self.object_name.clone()))
199            }
200            PropertyIdentifier::ObjectType => {
201                Ok(PropertyValue::Enumerated(u32::from(ObjectType::File)))
202            }
203            PropertyIdentifier::Archive => Ok(PropertyValue::Boolean(self.archive)),
204            _ => Err(ObjectError::UnknownProperty),
205        }
206    }
207
208    fn set_property(&mut self, property: PropertyIdentifier, value: PropertyValue) -> Result<()> {
209        match property {
210            PropertyIdentifier::ObjectName => {
211                if let PropertyValue::CharacterString(name) = value {
212                    self.object_name = name;
213                    Ok(())
214                } else {
215                    Err(ObjectError::InvalidPropertyType)
216                }
217            }
218            PropertyIdentifier::Archive => {
219                if let PropertyValue::Boolean(archive) = value {
220                    self.archive = archive;
221                    Ok(())
222                } else {
223                    Err(ObjectError::InvalidPropertyType)
224                }
225            }
226            _ => Err(ObjectError::PropertyNotWritable),
227        }
228    }
229
230    fn is_property_writable(&self, property: PropertyIdentifier) -> bool {
231        matches!(
232            property,
233            PropertyIdentifier::ObjectName | PropertyIdentifier::Archive
234        )
235    }
236
237    fn property_list(&self) -> Vec<PropertyIdentifier> {
238        vec![
239            PropertyIdentifier::ObjectIdentifier,
240            PropertyIdentifier::ObjectName,
241            PropertyIdentifier::ObjectType,
242            PropertyIdentifier::Archive,
243        ]
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250
251    #[test]
252    fn test_file_creation() {
253        let file = File::new(1, "config.txt".to_string(), "text/plain".to_string());
254        assert_eq!(file.identifier.instance, 1);
255        assert_eq!(file.object_name, "config.txt");
256        assert_eq!(file.file_type, "text/plain");
257        assert_eq!(file.file_size, 0);
258    }
259
260    #[test]
261    fn test_file_data_operations() {
262        let mut file = File::new(
263            1,
264            "test.dat".to_string(),
265            "application/octet-stream".to_string(),
266        );
267
268        // Set initial data
269        let data = b"Hello, BACnet File!".to_vec();
270        file.set_file_data(data.clone());
271        assert_eq!(file.file_size, data.len() as u32);
272        assert_eq!(file.get_file_data(), data.as_slice());
273
274        // Test reading data
275        let read_data = file.read_data(0, 5).unwrap();
276        assert_eq!(read_data, b"Hello");
277
278        let read_data = file.read_data(7, 6).unwrap();
279        assert_eq!(read_data, b"BACnet");
280
281        // Test writing data (overwrite "BACnet" with "Rust  ")
282        file.write_data(7, b"Rust  ").unwrap();
283        let expected = b"Hello, Rust   File!";
284        assert_eq!(file.get_file_data(), expected);
285    }
286
287    #[test]
288    fn test_file_record_operations() {
289        let mut file = File::new(1, "records.txt".to_string(), "text/plain".to_string());
290        file.file_access_method = FileAccessMethod::RecordAccess;
291
292        // Set initial records as line-separated data
293        let initial_data = "Line 1\nLine 2\nLine 3\nLine 4".as_bytes().to_vec();
294        file.set_file_data(initial_data);
295
296        // Read records
297        let records = file.read_records(1, 2).unwrap();
298        assert_eq!(records.len(), 2);
299        assert_eq!(records[0], b"Line 2");
300        assert_eq!(records[1], b"Line 3");
301
302        // Write records
303        let new_records = vec![b"New Line 2".to_vec(), b"New Line 3".to_vec()];
304        file.write_records(1, &new_records).unwrap();
305
306        let updated_records = file.read_records(0, 4).unwrap();
307        assert_eq!(updated_records[0], b"Line 1");
308        assert_eq!(updated_records[1], b"New Line 2");
309        assert_eq!(updated_records[2], b"New Line 3");
310        assert_eq!(updated_records[3], b"Line 4");
311    }
312
313    #[test]
314    fn test_file_properties() {
315        let mut file = File::new(1, "test.txt".to_string(), "text/plain".to_string());
316
317        // Test property access
318        let name = file.get_property(PropertyIdentifier::ObjectName).unwrap();
319        if let PropertyValue::CharacterString(n) = name {
320            assert_eq!(n, "test.txt");
321        } else {
322            panic!("Expected CharacterString");
323        }
324
325        // Test property modification
326        file.set_property(PropertyIdentifier::Archive, PropertyValue::Boolean(true))
327            .unwrap();
328        assert!(file.archive);
329    }
330
331    #[test]
332    fn test_read_only_protection() {
333        let mut file = File::new(1, "readonly.txt".to_string(), "text/plain".to_string());
334        file.read_only = true;
335
336        // Should fail to write data
337        assert!(file.write_data(0, b"test").is_err());
338
339        // Should fail to write records
340        file.file_access_method = FileAccessMethod::RecordAccess;
341        let records = vec![b"test".to_vec()];
342        assert!(file.write_records(0, &records).is_err());
343    }
344}