Skip to main content

bacnet_encoding/
segmentation.rs

1//! Segmentation: split and reassemble large APDU payloads.
2//!
3//! Per ASHRAE 135-2020 Clause 9, segmented messages use 8-bit sequence
4//! numbers with windowed flow control. This module provides the basic
5//! payload splitting and reassembly primitives.
6
7use bacnet_types::error::Error;
8use bytes::Bytes;
9use std::collections::HashMap;
10
11/// Return whether `sequence_number` is a duplicate in the current incomplete window.
12///
13/// This is the modulo-256 predicate from ANSI/ASHRAE 135-2020 Clause 5.4.2.2,
14/// as corrected by Addendum 135-2020ch. `initial_sequence_number` is the last
15/// sequence number in the previously completed window, and
16/// `last_sequence_number` is the last segment accepted in order. When those
17/// values are equal, no segment has been accepted in the current window and
18/// the predicate returns `false`.
19pub fn duplicate_in_window(
20    sequence_number: u8,
21    initial_sequence_number: u8,
22    last_sequence_number: u8,
23) -> bool {
24    let received_count = last_sequence_number.wrapping_sub(initial_sequence_number);
25    received_count != 0 && sequence_number.wrapping_sub(initial_sequence_number) <= received_count
26}
27
28/// PDU types that affect segmentation overhead calculation.
29///
30/// Named `SegmentedPduType` to avoid collision with `bacnet_types::enums::PduType`.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum SegmentedPduType {
33    /// ConfirmedRequest: 6-byte overhead (type + max-seg/apdu + invoke + seq + window + service).
34    ConfirmedRequest,
35    /// ComplexACK: 5-byte overhead (type + invoke + seq + window + service).
36    ComplexAck,
37}
38
39/// Compute the maximum service data payload per segment.
40///
41/// This is the max APDU length minus the PDU header overhead for segmented messages.
42pub fn max_segment_payload(max_apdu_length: u16, pdu_type: SegmentedPduType) -> usize {
43    let overhead = match pdu_type {
44        SegmentedPduType::ConfirmedRequest => 6,
45        SegmentedPduType::ComplexAck => 5,
46    };
47    (max_apdu_length as usize).saturating_sub(overhead)
48}
49
50/// Split a payload into segments of at most `max_segment_size` bytes.
51///
52/// Returns at least one segment (possibly empty) whenever `max_segment_size`
53/// leaves room for one. A zero segment size is an error even for an empty
54/// payload: a segment still costs its PDU header, so a peer whose maximum APDU
55/// cannot hold that header cannot receive a segment at all. Returning a single
56/// empty segment there produced a frame larger than the peer had advertised.
57pub fn split_payload(payload: &[u8], max_segment_size: usize) -> Result<Vec<Bytes>, Error> {
58    if max_segment_size == 0 {
59        return Err(Error::Segmentation(
60            "payload cannot be segmented with max segment size 0".into(),
61        ));
62    }
63    if payload.is_empty() {
64        return Ok(vec![Bytes::new()]);
65    }
66    let segments: Vec<Bytes> = payload
67        .chunks(max_segment_size)
68        .map(Bytes::copy_from_slice)
69        .collect();
70    if segments.len() > 256 {
71        return Err(Error::Segmentation(format!(
72            "payload requires {} segments, maximum is 256",
73            segments.len()
74        )));
75    }
76    Ok(segments)
77}
78
79/// Collects received segments for reassembly.
80///
81/// Segments can arrive out of order. Call [`reassemble`](SegmentReceiver::reassemble)
82/// once all segments have been received.
83pub struct SegmentReceiver {
84    segments: HashMap<u8, Bytes>,
85}
86
87impl Default for SegmentReceiver {
88    fn default() -> Self {
89        Self::new()
90    }
91}
92
93impl SegmentReceiver {
94    /// Create a new empty receiver.
95    pub fn new() -> Self {
96        Self {
97            segments: HashMap::new(),
98        }
99    }
100
101    /// Maximum BACnet APDU segment size (BACnet/IP over UDP).
102    const MAX_SEGMENT_SIZE: usize = 1476;
103
104    /// Store a received segment.
105    ///
106    /// Returns an error if the segment exceeds [`MAX_SEGMENT_SIZE`](Self::MAX_SEGMENT_SIZE).
107    pub fn receive(&mut self, sequence_number: u8, data: Bytes) -> Result<(), Error> {
108        if data.len() > Self::MAX_SEGMENT_SIZE {
109            return Err(Error::Segmentation(format!(
110                "segment size {} exceeds maximum {}",
111                data.len(),
112                Self::MAX_SEGMENT_SIZE
113            )));
114        }
115        self.segments.insert(sequence_number, data);
116        Ok(())
117    }
118
119    /// Check whether a specific segment has been received.
120    pub fn has_segment(&self, sequence_number: u8) -> bool {
121        self.segments.contains_key(&sequence_number)
122    }
123
124    /// Number of segments received so far.
125    pub fn received_count(&self) -> usize {
126        self.segments.len()
127    }
128
129    /// Reassemble all segments in order. `total_segments` is the expected count.
130    ///
131    /// Returns an error if any segment is missing or if `total_segments` exceeds
132    /// the BACnet 8-bit sequence number limit (256).
133    pub fn reassemble(&self, total_segments: usize) -> Result<Vec<u8>, Error> {
134        if total_segments > 256 {
135            return Err(Error::Segmentation(format!(
136                "total_segments {total_segments} exceeds maximum BACnet value (256)"
137            )));
138        }
139        let mut result = Vec::with_capacity(total_segments * 480);
140        for i in 0..total_segments {
141            let seq = i as u8;
142            match self.segments.get(&seq) {
143                Some(data) => result.extend_from_slice(data),
144                None => {
145                    return Err(Error::Segmentation(format!(
146                        "missing segment {} of {}",
147                        i, total_segments
148                    )));
149                }
150            }
151        }
152        Ok(result)
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    #[test]
161    fn max_segment_payload_confirmed_request() {
162        assert_eq!(
163            max_segment_payload(480, SegmentedPduType::ConfirmedRequest),
164            474
165        );
166        assert_eq!(
167            max_segment_payload(1476, SegmentedPduType::ConfirmedRequest),
168            1470
169        );
170    }
171
172    #[test]
173    fn duplicate_in_window_matches_addendum_examples() {
174        assert!(duplicate_in_window(0, 0, 1));
175        assert!(duplicate_in_window(1, 0, 1));
176        assert!(!duplicate_in_window(2, 0, 1));
177        assert!(!duplicate_in_window(3, 0, 1));
178    }
179
180    #[test]
181    fn duplicate_in_window_is_false_before_current_window_receives_a_segment() {
182        assert!(!duplicate_in_window(0, 0, 0));
183        assert!(!duplicate_in_window(255, 0, 0));
184    }
185
186    #[test]
187    fn duplicate_in_window_uses_modulo_256_arithmetic() {
188        assert!(duplicate_in_window(255, 254, 0));
189        assert!(duplicate_in_window(0, 254, 0));
190        assert!(!duplicate_in_window(1, 254, 0));
191    }
192
193    #[test]
194    fn duplicate_in_window_does_not_apply_ordinary_sequence_ordering() {
195        assert!(duplicate_in_window(255, 250, 2));
196        assert!(duplicate_in_window(1, 250, 2));
197        assert!(!duplicate_in_window(249, 250, 2));
198        assert!(!duplicate_in_window(3, 250, 2));
199    }
200
201    #[test]
202    fn max_segment_payload_complex_ack() {
203        assert_eq!(max_segment_payload(480, SegmentedPduType::ComplexAck), 475);
204        assert_eq!(
205            max_segment_payload(1476, SegmentedPduType::ComplexAck),
206            1471
207        );
208    }
209
210    #[test]
211    fn split_payload_fits_single_segment() {
212        let payload = vec![0u8; 100];
213        let segments = split_payload(&payload, 200).unwrap();
214        assert_eq!(segments.len(), 1);
215        assert_eq!(segments[0], payload);
216    }
217
218    #[test]
219    fn split_payload_exact_fit() {
220        let payload = vec![0u8; 200];
221        let segments = split_payload(&payload, 100).unwrap();
222        assert_eq!(segments.len(), 2);
223        assert_eq!(segments[0].len(), 100);
224        assert_eq!(segments[1].len(), 100);
225    }
226
227    #[test]
228    fn split_payload_remainder() {
229        let payload = vec![0u8; 250];
230        let segments = split_payload(&payload, 100).unwrap();
231        assert_eq!(segments.len(), 3);
232        assert_eq!(segments[0].len(), 100);
233        assert_eq!(segments[1].len(), 100);
234        assert_eq!(segments[2].len(), 50);
235    }
236
237    #[test]
238    fn split_empty_payload() {
239        let segments = split_payload(&[], 100).unwrap();
240        assert_eq!(segments.len(), 1);
241        assert!(segments[0].is_empty());
242    }
243
244    /// An empty payload is still a segment, and a segment still costs a PDU
245    /// header, so zero capacity cannot carry one. This used to return a single
246    /// empty segment because the empty-payload shortcut ran before the
247    /// zero-size check — which let the client emit a six-octet segmented
248    /// Confirmed-Request to a peer whose advertised maximum was smaller.
249    #[test]
250    fn split_empty_payload_zero_segment_size_errors() {
251        assert!(split_payload(&[], 0).is_err());
252    }
253
254    #[test]
255    fn reassemble_ordered_segments() {
256        let original = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
257        let segments = split_payload(&original, 3).unwrap();
258        assert_eq!(segments.len(), 4); // 3+3+3+1
259
260        let mut receiver = SegmentReceiver::new();
261        for (i, seg) in segments.iter().enumerate() {
262            receiver.receive(i as u8, seg.clone()).unwrap();
263        }
264        let reassembled = receiver.reassemble(segments.len()).unwrap();
265        assert_eq!(reassembled, original);
266    }
267
268    #[test]
269    fn reassemble_out_of_order() {
270        let mut receiver = SegmentReceiver::new();
271        receiver.receive(2, Bytes::from_static(&[5, 6])).unwrap();
272        receiver.receive(0, Bytes::from_static(&[1, 2])).unwrap();
273        receiver.receive(1, Bytes::from_static(&[3, 4])).unwrap();
274        let reassembled = receiver.reassemble(3).unwrap();
275        assert_eq!(reassembled, vec![1, 2, 3, 4, 5, 6]);
276    }
277
278    #[test]
279    fn reassemble_missing_segment_fails() {
280        let mut receiver = SegmentReceiver::new();
281        receiver.receive(0, Bytes::from_static(&[1, 2])).unwrap();
282        // Missing segment 1
283        receiver.receive(2, Bytes::from_static(&[5, 6])).unwrap();
284        assert!(receiver.reassemble(3).is_err());
285    }
286
287    #[test]
288    fn split_payload_zero_segment_size() {
289        assert!(split_payload(&[1, 2, 3], 0).is_err());
290    }
291
292    #[test]
293    fn split_payload_over_256_segments_errors() {
294        let payload = vec![0u8; 257];
295        assert!(split_payload(&payload, 1).is_err());
296    }
297}