Skip to main content

bacnet_services/
read_range.rs

1//! ReadRange service per ASHRAE 135-2020 Clause 15.8.
2//!
3//! Reads a range of items from a list or log-buffer property.
4
5#[cfg(not(feature = "std"))]
6use alloc::vec::Vec;
7
8use bacnet_encoding::{primitives, tags};
9use bacnet_types::enums::PropertyIdentifier;
10use bacnet_types::error::{DecodeError, DecodingKind, Error};
11use bacnet_types::primitives::{Date, ObjectIdentifier, Time};
12use bytes::BytesMut;
13
14use bacnet_encoding::constructed::tagged::{
15    decode_app_primitive, decode_app_unsigned, decode_ctx_object_id, decode_ctx_primitive,
16    decode_ctx_unsigned, expect_end, misplaced_tag, next_is_context,
17};
18
19fn decode_count(data: &[u8], offset: usize, field: &'static str) -> Result<(i32, usize), Error> {
20    let (content, end) = decode_app_primitive(data, offset, tags::app_tag::SIGNED, field)?;
21    let value = primitives::decode_signed(content)?;
22    let value = i16::try_from(value).map_err(|_| {
23        DecodeError::new(DecodingKind::OutOfRange, offset, "exceeds INTEGER16").within(field)
24    })?;
25    if value == 0 {
26        return Err(
27            DecodeError::new(DecodingKind::OutOfRange, offset, "may not be zero")
28                .within(field)
29                .into(),
30        );
31    }
32    Ok((i32::from(value), end))
33}
34
35fn specific_datetime(date: &Date, time: &Time) -> bool {
36    date.year != Date::UNSPECIFIED
37        && (1..=12).contains(&date.month)
38        && (1..=31).contains(&date.day)
39        && (1..=7).contains(&date.day_of_week)
40        && time.hour <= 23
41        && time.minute <= 59
42        && time.second <= 59
43        && time.hundredths <= 99
44}
45
46/// ReadRange-Request service parameters.
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct ReadRangeRequest {
49    /// Object holding the list property to read.
50    pub object_identifier: ObjectIdentifier,
51    /// List property to read; not ALL, REQUIRED or OPTIONAL.
52    pub property_identifier: PropertyIdentifier,
53    /// Array index selecting one list within an array-of-lists property; `None` otherwise. Zero is
54    /// invalid.
55    pub property_array_index: Option<u32>,
56    /// Range specification: by-position, by-sequence-number, or by-time.
57    pub range: Option<RangeSpec>,
58}
59
60/// Range specification for ReadRange.
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub enum RangeSpec {
63    /// By position: reference_index, count.
64    ByPosition {
65        /// One-based list index (first item is 1) of the item that anchors the range. Unsigned64
66        /// because this stack hosts Audit Log, whose Total_Record_Count is Unsigned64
67        /// (Clause 15.8.1.1.4.1.1).
68        reference_index: u64,
69        /// Signed INTEGER16 item count, never zero. Positive reads forward from the reference
70        /// and negative reads backward, ending at the reference.
71        count: i32,
72    },
73    /// By sequence number: reference_seq, count.
74    BySequenceNumber {
75        /// Sequence number of the item that anchors the range. Unsigned64 for the same reason as
76        /// `reference_index` (Clause 15.8.1.1.4.2.1).
77        reference_seq: u64,
78        /// Signed INTEGER16 item count, never zero. Positive reads forward from the reference
79        /// and negative reads backward, ending at the reference.
80        count: i32,
81    },
82    /// By time: reference_time (Date, Time), count.
83    ByTime {
84        /// Timestamp that anchors the range; must be fully specified.
85        reference_time: (Date, Time),
86        /// Signed INTEGER16 item count, never zero. The reference time itself is excluded:
87        /// positive reads forward from the first item newer than it, negative reads backward
88        /// ending at the newest item older than it (Clause 15.8.1.1.4.3).
89        count: i32,
90    },
91}
92
93impl ReadRangeRequest {
94    /// Validate typed arguments without output, leases or transport side effects.
95    pub fn validate(&self) -> Result<(), Error> {
96        if matches!(
97            self.property_identifier,
98            PropertyIdentifier::ALL | PropertyIdentifier::REQUIRED | PropertyIdentifier::OPTIONAL
99        ) {
100            return Err(Error::encode("ReadRange property may not be ALL, REQUIRED, or OPTIONAL"));
101        }
102        if self.property_array_index == Some(0) {
103            return Err(Error::encode("ReadRange array index may not be zero"));
104        }
105        if let Some(range) = &self.range {
106            let count = match range {
107                RangeSpec::ByPosition { count, .. } | RangeSpec::BySequenceNumber { count, .. } => {
108                    *count
109                }
110                RangeSpec::ByTime {
111                    reference_time: (date, time),
112                    count,
113                } => {
114                    if !specific_datetime(date, time) {
115                        return Err(Error::encode("ReadRange byTime requires a specific datetime"));
116                    }
117                    *count
118                }
119            };
120            if count == 0 || i16::try_from(count).is_err() {
121                return Err(Error::encode("ReadRange count must be a nonzero INTEGER16"));
122            }
123        }
124        Ok(())
125    }
126
127    /// Encode only valid requests; on error, existing output remains unchanged.
128    pub fn encode(&self, buf: &mut BytesMut) -> Result<(), Error> {
129        self.validate()?;
130        // [0] objectIdentifier
131        primitives::encode_ctx_object_id(buf, 0, &self.object_identifier);
132        // [1] propertyIdentifier
133        primitives::encode_ctx_enumerated(buf, 1, self.property_identifier.to_raw());
134        // [2] propertyArrayIndex (optional)
135        if let Some(idx) = self.property_array_index {
136            primitives::encode_ctx_unsigned(buf, 2, idx as u64);
137        }
138        // Range specification
139        if let Some(ref range) = self.range {
140            match range {
141                RangeSpec::ByPosition {
142                    reference_index,
143                    count,
144                } => {
145                    tags::encode_opening_tag(buf, 3);
146                    primitives::encode_app_unsigned(buf, *reference_index);
147                    primitives::encode_app_signed(buf, *count);
148                    tags::encode_closing_tag(buf, 3);
149                }
150                RangeSpec::BySequenceNumber {
151                    reference_seq,
152                    count,
153                } => {
154                    tags::encode_opening_tag(buf, 6);
155                    primitives::encode_app_unsigned(buf, *reference_seq);
156                    primitives::encode_app_signed(buf, *count);
157                    tags::encode_closing_tag(buf, 6);
158                }
159                RangeSpec::ByTime {
160                    reference_time,
161                    count,
162                } => {
163                    tags::encode_opening_tag(buf, 7);
164                    primitives::encode_app_date(buf, &reference_time.0);
165                    primitives::encode_app_time(buf, &reference_time.1);
166                    primitives::encode_app_signed(buf, *count);
167                    tags::encode_closing_tag(buf, 7);
168                }
169            }
170        }
171        Ok(())
172    }
173
174    /// Decode the request from service-request octets; fails on malformed or truncated input.
175    pub fn decode(data: &[u8]) -> Result<Self, Error> {
176        let mut offset = 0;
177
178        // [0] objectIdentifier
179        let (object_identifier, end) =
180            decode_ctx_object_id(data, offset, 0, "ReadRange request object-id")?;
181        offset = end;
182
183        // [1] propertyIdentifier
184        let (property_identifier, end) =
185            decode_ctx_unsigned::<u32>(data, offset, 1, "ReadRange request property-id")?;
186        let property_identifier = PropertyIdentifier::from_raw(property_identifier);
187        if matches!(
188            property_identifier,
189            PropertyIdentifier::ALL | PropertyIdentifier::REQUIRED | PropertyIdentifier::OPTIONAL
190        ) {
191            return Err(Error::decoding(
192                offset,
193                "ReadRange request property-id may not be ALL, REQUIRED, or OPTIONAL",
194            ));
195        }
196        offset = end;
197
198        // [2] propertyArrayIndex (optional)
199        let mut property_array_index = None;
200        if next_is_context(data, offset, 2)? {
201            let (index, end) =
202                decode_ctx_unsigned::<u32>(data, offset, 2, "ReadRange request array-index")?;
203            if index == 0 {
204                return Err(Error::out_of_range(
205                    offset,
206                    "ReadRange request array-index may not be zero",
207                ));
208            }
209            property_array_index = Some(index);
210            offset = end;
211        }
212
213        // Range specification (optional)
214        let mut range = None;
215        if offset < data.len() {
216            let (tag, tag_end) = tags::decode_tag(data, offset)?;
217            if tag.is_opening_tag(3) {
218                // byPosition
219                let (content, new_offset) = tags::extract_context_value(data, tag_end, 3)?;
220                let (reference_index, inner_offset) =
221                    decode_app_unsigned::<u64>(content, 0, "ReadRange byPosition reference-index")?;
222                let (count, inner_offset) =
223                    decode_count(content, inner_offset, "ReadRange byPosition count")?;
224                expect_end(content, inner_offset, inner_offset, "ReadRange byPosition")?;
225                range = Some(RangeSpec::ByPosition {
226                    reference_index,
227                    count,
228                });
229                offset = new_offset;
230            } else if tag.is_opening_tag(6) {
231                // bySequenceNumber
232                let (content, new_offset) = tags::extract_context_value(data, tag_end, 6)?;
233                let (reference_seq, inner_offset) = decode_app_unsigned::<u64>(
234                    content,
235                    0,
236                    "ReadRange bySequenceNumber reference-seq",
237                )?;
238                let (count, inner_offset) =
239                    decode_count(content, inner_offset, "ReadRange bySequenceNumber count")?;
240                expect_end(
241                    content,
242                    inner_offset,
243                    inner_offset,
244                    "ReadRange bySequenceNumber",
245                )?;
246                range = Some(RangeSpec::BySequenceNumber {
247                    reference_seq,
248                    count,
249                });
250                offset = new_offset;
251            } else if tag.is_opening_tag(7) {
252                // byTime
253                let (content, new_offset) = tags::extract_context_value(data, tag_end, 7)?;
254                let (date, inner_offset) =
255                    decode_app_primitive(content, 0, tags::app_tag::DATE, "ReadRange byTime date")?;
256                let date = Date::decode(date)?;
257                let (time, inner_offset) = decode_app_primitive(
258                    content,
259                    inner_offset,
260                    tags::app_tag::TIME,
261                    "ReadRange byTime time",
262                )?;
263                let time = Time::decode(time)?;
264                if !specific_datetime(&date, &time) {
265                    return Err(Error::decoding(
266                        inner_offset,
267                        "ReadRange byTime requires a specific datetime",
268                    ));
269                }
270                let (count, inner_offset) =
271                    decode_count(content, inner_offset, "ReadRange byTime count")?;
272                expect_end(content, inner_offset, inner_offset, "ReadRange byTime")?;
273                range = Some(RangeSpec::ByTime {
274                    reference_time: (date, time),
275                    count,
276                });
277                offset = new_offset;
278            } else {
279                return Err(misplaced_tag(
280                    data,
281                    &tag,
282                    None,
283                    offset,
284                    "ReadRange request has invalid range choice",
285                ));
286            }
287        }
288        expect_end(data, offset, offset, "ReadRange request")?;
289
290        Ok(Self {
291            object_identifier,
292            property_identifier,
293            property_array_index,
294            range,
295        })
296    }
297}
298
299/// ReadRange-ACK service parameters.
300#[derive(Debug, Clone)]
301pub struct ReadRangeAck {
302    /// Object that was read.
303    pub object_identifier: ObjectIdentifier,
304    /// Property that was read.
305    pub property_identifier: PropertyIdentifier,
306    /// Array index that was requested, echoed from the request; `None` when absent.
307    pub property_array_index: Option<u32>,
308    /// Result flags: first_item, last_item, more_items.
309    pub result_flags: (bool, bool, bool),
310    /// Number of items encoded in `item_data`.
311    pub item_count: u32,
312    /// Raw item data (application-layer interprets content).
313    pub item_data: Vec<u8>,
314    /// Optional first sequence number (context tag \[6\]). Unsigned64, since a device holding
315    /// an Audit Log must accept and report its 64-bit sequence numbers (Clause 15.8.1.2.7).
316    pub first_sequence_number: Option<u64>,
317}
318
319impl ReadRangeAck {
320    /// Encode the acknowledgment parameters into `buf`.
321    pub fn encode(&self, buf: &mut BytesMut) {
322        // [0] objectIdentifier
323        primitives::encode_ctx_object_id(buf, 0, &self.object_identifier);
324        // [1] propertyIdentifier
325        primitives::encode_ctx_enumerated(buf, 1, self.property_identifier.to_raw());
326        // [2] propertyArrayIndex (optional)
327        if let Some(idx) = self.property_array_index {
328            primitives::encode_ctx_unsigned(buf, 2, idx as u64);
329        }
330        // [3] resultFlags — 3-bit bitstring
331        let mut flags: u8 = 0;
332        if self.result_flags.0 {
333            flags |= 0x80;
334        }
335        if self.result_flags.1 {
336            flags |= 0x40;
337        }
338        if self.result_flags.2 {
339            flags |= 0x20;
340        }
341        primitives::encode_ctx_bit_string(buf, 3, 5, &[flags]);
342        // [4] itemCount
343        primitives::encode_ctx_unsigned(buf, 4, self.item_count as u64);
344        // [5] itemData
345        tags::encode_opening_tag(buf, 5);
346        buf.extend_from_slice(&self.item_data);
347        tags::encode_closing_tag(buf, 5);
348        // [6] firstSequenceNumber (optional)
349        if let Some(seq) = self.first_sequence_number {
350            primitives::encode_ctx_unsigned(buf, 6, seq);
351        }
352    }
353
354    /// Decode the acknowledgment from its service-ack octets; fails on malformed or truncated
355    /// input.
356    pub fn decode(data: &[u8]) -> Result<Self, Error> {
357        let mut offset = 0;
358
359        // [0] objectIdentifier
360        let (object_identifier, end) =
361            decode_ctx_object_id(data, offset, 0, "ReadRange ACK object-id")?;
362        offset = end;
363
364        // [1] propertyIdentifier
365        let (property_identifier, end) =
366            decode_ctx_unsigned::<u32>(data, offset, 1, "ReadRange ACK property-id")?;
367        let property_identifier = PropertyIdentifier::from_raw(property_identifier);
368        offset = end;
369
370        // [2] propertyArrayIndex (optional)
371        let mut property_array_index = None;
372        if next_is_context(data, offset, 2)? {
373            let (index, end) =
374                decode_ctx_unsigned::<u32>(data, offset, 2, "ReadRange ACK array-index")?;
375            property_array_index = Some(index);
376            offset = end;
377        }
378
379        // [3] resultFlags
380        let (content, end) = decode_ctx_primitive(data, offset, 3, "ReadRange ACK result-flags")?;
381        let (unused_bits, bits) = primitives::decode_bit_string(content)?;
382        if unused_bits != 5 || bits.len() != 1 || bits[0] & 0x1f != 0 {
383            return Err(Error::decoding(
384                offset,
385                "ReadRange ACK result-flags must contain three bits with zero padding",
386            ));
387        }
388        let b = bits[0];
389        let result_flags = (b & 0x80 != 0, b & 0x40 != 0, b & 0x20 != 0);
390        offset = end;
391
392        // [4] itemCount
393        let (item_count, end) =
394            decode_ctx_unsigned::<u32>(data, offset, 4, "ReadRange ACK item-count")?;
395        offset = end;
396
397        // [5] itemData
398        let (tag, tag_end) = tags::decode_tag(data, offset)?;
399        if !tag.is_opening_tag(5) {
400            return Err(misplaced_tag(
401                data,
402                &tag,
403                Some(5),
404                offset,
405                "ReadRange ACK item-data expected opening tag 5",
406            ));
407        }
408        let (content, new_offset) = tags::extract_context_value(data, tag_end, 5)?;
409        if (item_count == 0) != content.is_empty() {
410            return Err(Error::decoding(
411                offset,
412                "ReadRange ACK item-count contradicts empty item-data",
413            ));
414        }
415        let item_data = content.to_vec();
416        offset = new_offset;
417
418        // [6] firstSequenceNumber (optional)
419        let mut first_sequence_number = None;
420        if offset < data.len() {
421            let (tag, _) = tags::decode_tag(data, offset)?;
422            if !tag.is_context(6) {
423                return Err(misplaced_tag(
424                    data,
425                    &tag,
426                    Some(6),
427                    offset,
428                    "ReadRange ACK expected context tag 6 for first-sequence-number",
429                ));
430            }
431            if item_count == 0 {
432                return Err(Error::decoding(
433                    offset,
434                    "ReadRange ACK first-sequence-number requires a nonzero item-count",
435                ));
436            }
437            let (sequence_number, end) =
438                decode_ctx_unsigned::<u64>(data, offset, 6, "ReadRange ACK first-sequence-number")?;
439            first_sequence_number = Some(sequence_number);
440            offset = end;
441        }
442        expect_end(data, offset, offset, "ReadRange ACK")?;
443
444        Ok(Self {
445            object_identifier,
446            property_identifier,
447            property_array_index,
448            result_flags,
449            item_count,
450            item_data,
451            first_sequence_number,
452        })
453    }
454}
455
456#[path = "read_range_records.rs"]
457mod records;
458pub use records::{LogBufferRecord, LogRecords, LogRecordsError};
459#[path = "read_range_checks.rs"]
460mod checks;
461pub use bacnet_types::error::ReadRangeViolation;
462pub use checks::{ReadRangeReply, ReadRangeValidation};
463
464#[cfg(test)]
465#[path = "read_range_width_tests.rs"]
466mod width_tests;
467
468#[cfg(test)]
469#[path = "read_range_tests.rs"]
470mod tests;
471
472#[cfg(test)]
473#[path = "read_range_validation_tests.rs"]
474mod validation_tests;