Skip to main content

bacnet_encoding/
segmentation.rs

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