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
5use bacnet_encoding::{primitives, tags};
6use bacnet_types::enums::PropertyIdentifier;
7use bacnet_types::error::Error;
8use bacnet_types::primitives::{Date, ObjectIdentifier, Time};
9use bytes::BytesMut;
10
11/// Decode a tag and validate the resulting slice bounds.
12fn checked_slice<'a>(
13    content: &'a [u8],
14    offset: usize,
15    context: &str,
16) -> Result<(&'a [u8], usize), Error> {
17    let (t, p) = tags::decode_tag(content, offset)?;
18    let end = p + t.length as usize;
19    if end > content.len() {
20        return Err(Error::decoding(p, format!("{context} truncated")));
21    }
22    Ok((&content[p..end], end))
23}
24
25/// ReadRange-Request service parameters.
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct ReadRangeRequest {
28    pub object_identifier: ObjectIdentifier,
29    pub property_identifier: PropertyIdentifier,
30    pub property_array_index: Option<u32>,
31    /// Range specification: by-position, by-sequence-number, or by-time.
32    pub range: Option<RangeSpec>,
33}
34
35/// Range specification for ReadRange.
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub enum RangeSpec {
38    /// By position: reference_index, count.
39    ByPosition { reference_index: u32, count: i32 },
40    /// By sequence number: reference_seq, count.
41    BySequenceNumber { reference_seq: u32, count: i32 },
42    /// By time: reference_time (Date, Time), count.
43    ByTime {
44        reference_time: (Date, Time),
45        count: i32,
46    },
47}
48
49impl ReadRangeRequest {
50    pub fn encode(&self, buf: &mut BytesMut) {
51        // [0] objectIdentifier
52        primitives::encode_ctx_object_id(buf, 0, &self.object_identifier);
53        // [1] propertyIdentifier
54        primitives::encode_ctx_enumerated(buf, 1, self.property_identifier.to_raw());
55        // [2] propertyArrayIndex (optional)
56        if let Some(idx) = self.property_array_index {
57            primitives::encode_ctx_unsigned(buf, 2, idx as u64);
58        }
59        // Range specification
60        if let Some(ref range) = self.range {
61            match range {
62                RangeSpec::ByPosition {
63                    reference_index,
64                    count,
65                } => {
66                    tags::encode_opening_tag(buf, 3);
67                    primitives::encode_app_unsigned(buf, *reference_index as u64);
68                    primitives::encode_app_signed(buf, *count);
69                    tags::encode_closing_tag(buf, 3);
70                }
71                RangeSpec::BySequenceNumber {
72                    reference_seq,
73                    count,
74                } => {
75                    tags::encode_opening_tag(buf, 6);
76                    primitives::encode_app_unsigned(buf, *reference_seq as u64);
77                    primitives::encode_app_signed(buf, *count);
78                    tags::encode_closing_tag(buf, 6);
79                }
80                RangeSpec::ByTime {
81                    reference_time,
82                    count,
83                } => {
84                    tags::encode_opening_tag(buf, 7);
85                    primitives::encode_app_date(buf, &reference_time.0);
86                    primitives::encode_app_time(buf, &reference_time.1);
87                    primitives::encode_app_signed(buf, *count);
88                    tags::encode_closing_tag(buf, 7);
89                }
90            }
91        }
92    }
93
94    pub fn decode(data: &[u8]) -> Result<Self, Error> {
95        let mut offset = 0;
96
97        // [0] objectIdentifier
98        let (tag, pos) = tags::decode_tag(data, offset)?;
99        let end = pos + tag.length as usize;
100        if end > data.len() {
101            return Err(Error::decoding(
102                pos,
103                "ReadRange request truncated at object-id",
104            ));
105        }
106        let object_identifier = ObjectIdentifier::decode(&data[pos..end])?;
107        offset = end;
108
109        // [1] propertyIdentifier
110        let (tag, pos) = tags::decode_tag(data, offset)?;
111        let end = pos + tag.length as usize;
112        if end > data.len() {
113            return Err(Error::decoding(
114                pos,
115                "ReadRange request truncated at property-id",
116            ));
117        }
118        let property_identifier =
119            PropertyIdentifier::from_raw(primitives::decode_unsigned(&data[pos..end])? as u32);
120        offset = end;
121
122        // [2] propertyArrayIndex (optional)
123        let mut property_array_index = None;
124        if offset < data.len() {
125            let (opt_data, new_offset) = tags::decode_optional_context(data, offset, 2)?;
126            if let Some(content) = opt_data {
127                property_array_index = Some(primitives::decode_unsigned(content)? as u32);
128                offset = new_offset;
129            }
130        }
131
132        // Range specification (optional)
133        let mut range = None;
134        if offset < data.len() {
135            let (tag, tag_end) = tags::decode_tag(data, offset)?;
136            if tag.is_opening_tag(3) {
137                // byPosition
138                let (content, new_offset) = tags::extract_context_value(data, tag_end, 3)?;
139                let (slice, inner_offset) =
140                    checked_slice(content, 0, "ReadRange byPosition reference-index")?;
141                let reference_index = primitives::decode_unsigned(slice)? as u32;
142                let (slice, _) =
143                    checked_slice(content, inner_offset, "ReadRange byPosition count")?;
144                let count = primitives::decode_signed(slice)?;
145                range = Some(RangeSpec::ByPosition {
146                    reference_index,
147                    count,
148                });
149                offset = new_offset;
150            } else if tag.is_opening_tag(6) {
151                // bySequenceNumber
152                let (content, new_offset) = tags::extract_context_value(data, tag_end, 6)?;
153                let (slice, inner_offset) =
154                    checked_slice(content, 0, "ReadRange bySequenceNumber reference-seq")?;
155                let reference_seq = primitives::decode_unsigned(slice)? as u32;
156                let (slice, _) =
157                    checked_slice(content, inner_offset, "ReadRange bySequenceNumber count")?;
158                let count = primitives::decode_signed(slice)?;
159                range = Some(RangeSpec::BySequenceNumber {
160                    reference_seq,
161                    count,
162                });
163                offset = new_offset;
164            } else if tag.is_opening_tag(7) {
165                // byTime
166                let (content, new_offset) = tags::extract_context_value(data, tag_end, 7)?;
167                let (slice, inner_offset) = checked_slice(content, 0, "ReadRange byTime date")?;
168                let date = Date::decode(slice)?;
169                let (slice, inner_offset) =
170                    checked_slice(content, inner_offset, "ReadRange byTime time")?;
171                let time = Time::decode(slice)?;
172                let (slice, _) = checked_slice(content, inner_offset, "ReadRange byTime count")?;
173                let count = primitives::decode_signed(slice)?;
174                range = Some(RangeSpec::ByTime {
175                    reference_time: (date, time),
176                    count,
177                });
178                offset = new_offset;
179            }
180        }
181        let _ = offset;
182
183        if let Some(ref r) = range {
184            let count = match r {
185                RangeSpec::ByPosition { count, .. } => *count,
186                RangeSpec::BySequenceNumber { count, .. } => *count,
187                RangeSpec::ByTime { count, .. } => *count,
188            };
189            if count == 0 {
190                return Err(Error::Encoding("ReadRange count may not be zero".into()));
191            }
192        }
193
194        Ok(Self {
195            object_identifier,
196            property_identifier,
197            property_array_index,
198            range,
199        })
200    }
201}
202
203/// ReadRange-ACK service parameters.
204#[derive(Debug, Clone)]
205pub struct ReadRangeAck {
206    pub object_identifier: ObjectIdentifier,
207    pub property_identifier: PropertyIdentifier,
208    pub property_array_index: Option<u32>,
209    /// Result flags: first_item, last_item, more_items.
210    pub result_flags: (bool, bool, bool),
211    pub item_count: u32,
212    /// Raw item data (application-layer interprets content).
213    pub item_data: Vec<u8>,
214    /// Optional first sequence number (context tag [6]).
215    pub first_sequence_number: Option<u32>,
216}
217
218impl ReadRangeAck {
219    pub fn encode(&self, buf: &mut BytesMut) {
220        // [0] objectIdentifier
221        primitives::encode_ctx_object_id(buf, 0, &self.object_identifier);
222        // [1] propertyIdentifier
223        primitives::encode_ctx_enumerated(buf, 1, self.property_identifier.to_raw());
224        // [2] propertyArrayIndex (optional)
225        if let Some(idx) = self.property_array_index {
226            primitives::encode_ctx_unsigned(buf, 2, idx as u64);
227        }
228        // [3] resultFlags — 3-bit bitstring
229        let mut flags: u8 = 0;
230        if self.result_flags.0 {
231            flags |= 0x80;
232        }
233        if self.result_flags.1 {
234            flags |= 0x40;
235        }
236        if self.result_flags.2 {
237            flags |= 0x20;
238        }
239        primitives::encode_ctx_bit_string(buf, 3, 5, &[flags]);
240        // [4] itemCount
241        primitives::encode_ctx_unsigned(buf, 4, self.item_count as u64);
242        // [5] itemData
243        tags::encode_opening_tag(buf, 5);
244        buf.extend_from_slice(&self.item_data);
245        tags::encode_closing_tag(buf, 5);
246        // [6] firstSequenceNumber (optional)
247        if let Some(seq) = self.first_sequence_number {
248            primitives::encode_ctx_unsigned(buf, 6, seq as u64);
249        }
250    }
251
252    pub fn decode(data: &[u8]) -> Result<Self, Error> {
253        let mut offset = 0;
254
255        // [0] objectIdentifier
256        let (tag, pos) = tags::decode_tag(data, offset)?;
257        let end = pos + tag.length as usize;
258        if end > data.len() {
259            return Err(Error::decoding(pos, "ReadRange ACK truncated at object-id"));
260        }
261        let object_identifier = ObjectIdentifier::decode(&data[pos..end])?;
262        offset = end;
263
264        // [1] propertyIdentifier
265        let (tag, pos) = tags::decode_tag(data, offset)?;
266        let end = pos + tag.length as usize;
267        if end > data.len() {
268            return Err(Error::decoding(
269                pos,
270                "ReadRange ACK truncated at property-id",
271            ));
272        }
273        let property_identifier =
274            PropertyIdentifier::from_raw(primitives::decode_unsigned(&data[pos..end])? as u32);
275        offset = end;
276
277        // [2] propertyArrayIndex (optional)
278        let mut property_array_index = None;
279        let (opt_data, new_offset) = tags::decode_optional_context(data, offset, 2)?;
280        if let Some(content) = opt_data {
281            property_array_index = Some(primitives::decode_unsigned(content)? as u32);
282            offset = new_offset;
283        }
284
285        // [3] resultFlags
286        let (tag, pos) = tags::decode_tag(data, offset)?;
287        let end = pos + tag.length as usize;
288        if end > data.len() {
289            return Err(Error::decoding(
290                pos,
291                "ReadRange ACK truncated at result-flags",
292            ));
293        }
294        let (_, bits) = primitives::decode_bit_string(&data[pos..end])?;
295        let b = bits.first().copied().unwrap_or(0);
296        let result_flags = (b & 0x80 != 0, b & 0x40 != 0, b & 0x20 != 0);
297        offset = end;
298
299        // [4] itemCount
300        let (tag, pos) = tags::decode_tag(data, offset)?;
301        let end = pos + tag.length as usize;
302        if end > data.len() {
303            return Err(Error::decoding(
304                pos,
305                "ReadRange ACK truncated at item-count",
306            ));
307        }
308        let item_count = primitives::decode_unsigned(&data[pos..end])? as u32;
309        offset = end;
310
311        // [5] itemData
312        let (_tag, tag_end) = tags::decode_tag(data, offset)?;
313        let (content, _new_offset) = tags::extract_context_value(data, tag_end, 5)?;
314        let item_data = content.to_vec();
315        let mut new_offset = _new_offset;
316
317        // [6] firstSequenceNumber (optional)
318        let mut first_sequence_number = None;
319        if new_offset < data.len() {
320            let (opt_data, after) = tags::decode_optional_context(data, new_offset, 6)?;
321            if let Some(content) = opt_data {
322                first_sequence_number = Some(primitives::decode_unsigned(content)? as u32);
323                new_offset = after;
324            }
325        }
326        let _ = new_offset;
327
328        Ok(Self {
329            object_identifier,
330            property_identifier,
331            property_array_index,
332            result_flags,
333            item_count,
334            item_data,
335            first_sequence_number,
336        })
337    }
338}
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343    use bacnet_types::enums::ObjectType;
344    use bacnet_types::primitives::{Date, Time};
345
346    fn make_oid() -> ObjectIdentifier {
347        ObjectIdentifier::new(ObjectType::TREND_LOG, 1).unwrap()
348    }
349
350    #[test]
351    fn request_round_trip() {
352        let req = ReadRangeRequest {
353            object_identifier: make_oid(),
354            property_identifier: PropertyIdentifier::LOG_BUFFER,
355            property_array_index: None,
356            range: Some(RangeSpec::ByPosition {
357                reference_index: 1,
358                count: 10,
359            }),
360        };
361        let mut buf = BytesMut::new();
362        req.encode(&mut buf);
363        let decoded = ReadRangeRequest::decode(&buf).unwrap();
364        assert_eq!(decoded.object_identifier, req.object_identifier);
365        assert_eq!(decoded.property_identifier, req.property_identifier);
366        assert_eq!(decoded.range, req.range);
367    }
368
369    #[test]
370    fn request_no_range() {
371        let req = ReadRangeRequest {
372            object_identifier: make_oid(),
373            property_identifier: PropertyIdentifier::LOG_BUFFER,
374            property_array_index: None,
375            range: None,
376        };
377        let mut buf = BytesMut::new();
378        req.encode(&mut buf);
379        let decoded = ReadRangeRequest::decode(&buf).unwrap();
380        assert!(decoded.range.is_none());
381    }
382
383    #[test]
384    fn request_by_sequence_number() {
385        let req = ReadRangeRequest {
386            object_identifier: make_oid(),
387            property_identifier: PropertyIdentifier::LOG_BUFFER,
388            property_array_index: None,
389            range: Some(RangeSpec::BySequenceNumber {
390                reference_seq: 100,
391                count: -5,
392            }),
393        };
394        let mut buf = BytesMut::new();
395        req.encode(&mut buf);
396        let decoded = ReadRangeRequest::decode(&buf).unwrap();
397        assert_eq!(decoded.range, req.range);
398    }
399
400    #[test]
401    fn ack_round_trip() {
402        let ack = ReadRangeAck {
403            object_identifier: make_oid(),
404            property_identifier: PropertyIdentifier::LOG_BUFFER,
405            property_array_index: None,
406            result_flags: (true, false, true),
407            item_count: 2,
408            item_data: vec![0xAA, 0xBB, 0xCC],
409            first_sequence_number: None,
410        };
411        let mut buf = BytesMut::new();
412        ack.encode(&mut buf);
413        let decoded = ReadRangeAck::decode(&buf).unwrap();
414        assert_eq!(decoded.object_identifier, ack.object_identifier);
415        assert_eq!(decoded.result_flags, (true, false, true));
416        assert_eq!(decoded.item_count, 2);
417        assert_eq!(decoded.item_data, vec![0xAA, 0xBB, 0xCC]);
418        assert_eq!(decoded.first_sequence_number, None);
419    }
420
421    #[test]
422    fn ack_round_trip_with_first_sequence_number() {
423        let ack = ReadRangeAck {
424            object_identifier: make_oid(),
425            property_identifier: PropertyIdentifier::LOG_BUFFER,
426            property_array_index: None,
427            result_flags: (true, true, false),
428            item_count: 5,
429            item_data: vec![0x01, 0x02],
430            first_sequence_number: Some(42),
431        };
432        let mut buf = BytesMut::new();
433        ack.encode(&mut buf);
434        let decoded = ReadRangeAck::decode(&buf).unwrap();
435        assert_eq!(decoded.object_identifier, ack.object_identifier);
436        assert_eq!(decoded.result_flags, (true, true, false));
437        assert_eq!(decoded.item_count, 5);
438        assert_eq!(decoded.item_data, vec![0x01, 0x02]);
439        assert_eq!(decoded.first_sequence_number, Some(42));
440    }
441
442    #[test]
443    fn request_by_time() {
444        let req = ReadRangeRequest {
445            object_identifier: make_oid(),
446            property_identifier: PropertyIdentifier::LOG_BUFFER,
447            property_array_index: None,
448            range: Some(RangeSpec::ByTime {
449                reference_time: (
450                    Date {
451                        year: 126, // 2026
452                        month: 3,
453                        day: 1,
454                        day_of_week: 7, // Sunday
455                    },
456                    Time {
457                        hour: 14,
458                        minute: 30,
459                        second: 0,
460                        hundredths: 0,
461                    },
462                ),
463                count: -10,
464            }),
465        };
466        let mut buf = BytesMut::new();
467        req.encode(&mut buf);
468        let decoded = ReadRangeRequest::decode(&buf).unwrap();
469        assert_eq!(decoded.range, req.range);
470    }
471
472    // -----------------------------------------------------------------------
473    // Malformed-input decode error tests
474    // -----------------------------------------------------------------------
475
476    #[test]
477    fn test_decode_read_range_request_empty_input() {
478        assert!(ReadRangeRequest::decode(&[]).is_err());
479    }
480
481    #[test]
482    fn test_decode_read_range_request_truncated_1_byte() {
483        let req = ReadRangeRequest {
484            object_identifier: make_oid(),
485            property_identifier: PropertyIdentifier::LOG_BUFFER,
486            property_array_index: None,
487            range: Some(RangeSpec::ByPosition {
488                reference_index: 1,
489                count: 10,
490            }),
491        };
492        let mut buf = BytesMut::new();
493        req.encode(&mut buf);
494        assert!(ReadRangeRequest::decode(&buf[..1]).is_err());
495    }
496
497    #[test]
498    fn test_decode_read_range_request_truncated_3_bytes() {
499        let req = ReadRangeRequest {
500            object_identifier: make_oid(),
501            property_identifier: PropertyIdentifier::LOG_BUFFER,
502            property_array_index: None,
503            range: Some(RangeSpec::ByPosition {
504                reference_index: 1,
505                count: 10,
506            }),
507        };
508        let mut buf = BytesMut::new();
509        req.encode(&mut buf);
510        assert!(ReadRangeRequest::decode(&buf[..3]).is_err());
511    }
512
513    #[test]
514    fn test_decode_read_range_request_invalid_tag() {
515        assert!(ReadRangeRequest::decode(&[0xFF, 0xFF, 0xFF]).is_err());
516    }
517
518    #[test]
519    fn test_decode_read_range_ack_empty_input() {
520        assert!(ReadRangeAck::decode(&[]).is_err());
521    }
522
523    #[test]
524    fn test_decode_read_range_ack_truncated_1_byte() {
525        let ack = ReadRangeAck {
526            object_identifier: make_oid(),
527            property_identifier: PropertyIdentifier::LOG_BUFFER,
528            property_array_index: None,
529            result_flags: (true, false, true),
530            item_count: 2,
531            item_data: vec![0xAA, 0xBB, 0xCC],
532            first_sequence_number: None,
533        };
534        let mut buf = BytesMut::new();
535        ack.encode(&mut buf);
536        assert!(ReadRangeAck::decode(&buf[..1]).is_err());
537    }
538
539    #[test]
540    fn test_decode_read_range_ack_truncated_3_bytes() {
541        let ack = ReadRangeAck {
542            object_identifier: make_oid(),
543            property_identifier: PropertyIdentifier::LOG_BUFFER,
544            property_array_index: None,
545            result_flags: (true, false, true),
546            item_count: 2,
547            item_data: vec![0xAA, 0xBB, 0xCC],
548            first_sequence_number: None,
549        };
550        let mut buf = BytesMut::new();
551        ack.encode(&mut buf);
552        assert!(ReadRangeAck::decode(&buf[..3]).is_err());
553    }
554
555    #[test]
556    fn test_decode_read_range_ack_truncated_half() {
557        let ack = ReadRangeAck {
558            object_identifier: make_oid(),
559            property_identifier: PropertyIdentifier::LOG_BUFFER,
560            property_array_index: None,
561            result_flags: (true, false, true),
562            item_count: 2,
563            item_data: vec![0xAA, 0xBB, 0xCC],
564            first_sequence_number: None,
565        };
566        let mut buf = BytesMut::new();
567        ack.encode(&mut buf);
568        let half = buf.len() / 2;
569        assert!(ReadRangeAck::decode(&buf[..half]).is_err());
570    }
571
572    #[test]
573    fn test_decode_read_range_ack_invalid_tag() {
574        assert!(ReadRangeAck::decode(&[0xFF, 0xFF, 0xFF]).is_err());
575    }
576
577    #[test]
578    fn read_range_request_truncated_inner_tag() {
579        // Craft a ReadRangeRequest with truncated inner content in byPosition
580        let data = [
581            0x0C, 0x05, 0x00, 0x00, 0x01, // [0] object id (TrendLog:1)
582            0x19, 0x83, // [1] property id (LOG_BUFFER=131)
583            // Opening tag [3] byPosition
584            0x3E, // Inner tag claiming 50 bytes but only 1 byte present
585            0x21, 50, 0x01, // Closing tag [3]
586            0x3F,
587        ];
588        assert!(ReadRangeRequest::decode(&data).is_err());
589    }
590}