Skip to main content

osdp/packet/
codec.rs

1//! Packet codec โ€” encode/decode the OSDP wire framing.
2//!
3//! # Spec: ยง5.9 Tables 1โ€“3
4//!
5//! Layout:
6//!
7//! ```text
8//! +------+------+---------+---------+------+========+======+----+======+
9//! | SOM  | ADDR | LEN_LSB | LEN_MSB | CTRL |  SCB?  | CMND | DAT| MAC? |  cksum/CRC
10//! +------+------+---------+---------+------+========+======+----+======+
11//! ```
12
13use crate::SOM;
14use crate::error::Error;
15use crate::packet::checksum::checksum8;
16use crate::packet::crc::crc16;
17use crate::packet::header::{Address, ControlByte};
18use crate::packet::scb::ScbView;
19use crate::packet::trailer::Trailer;
20
21/// Length of the fixed OSDP header (SOM, ADDR, LEN_LSB, LEN_MSB, CTRL).
22pub const HEADER_LEN: usize = 5;
23
24/// MAC trailer length on the wire (the first 4 bytes of the computed MAC).
25pub const MAC_LEN: usize = 4;
26
27/// View of a parsed OSDP packet, borrowed from the input slice.
28///
29/// The codec is *zero-copy*: payload, SCB data, and MAC slices all point into
30/// the input buffer.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub struct ParsedPacket<'a> {
33    /// Source/destination address (with reply flag if from PD).
34    pub addr: Address,
35    /// Decoded CTRL byte.
36    pub ctrl: ControlByte,
37    /// Security Control Block, if `CTRL.HAS_SCB`.
38    pub scb: Option<ScbView<'a>>,
39    /// Command or reply code byte.
40    pub code: u8,
41    /// Application data following the code byte (excluding any MAC trailer
42    /// and the CRC/checksum).
43    pub data: &'a [u8],
44    /// 4-byte MAC, if SCS_15..18.
45    pub mac: Option<[u8; MAC_LEN]>,
46    /// Trailer kind.
47    pub trailer: Trailer,
48}
49
50impl<'a> ParsedPacket<'a> {
51    /// Parse a complete packet from `buf`.
52    ///
53    /// Returns the parsed packet view and the number of bytes consumed
54    /// (which equals the value of the LEN field).
55    pub fn parse(buf: &'a [u8]) -> Result<(Self, usize), Error> {
56        if buf.len() < HEADER_LEN {
57            return Err(Error::Truncated {
58                have: buf.len(),
59                need: HEADER_LEN,
60            });
61        }
62        if buf[0] != SOM {
63            return Err(Error::BadSom(buf[0]));
64        }
65
66        let addr = Address::from_raw(buf[1]);
67        let len = u16::from_le_bytes([buf[2], buf[3]]) as usize;
68        let ctrl = ControlByte::decode(buf[4])?;
69
70        if buf.len() < len {
71            return Err(Error::Truncated {
72                have: buf.len(),
73                need: len,
74            });
75        }
76        let frame = &buf[..len];
77
78        let trailer_len = if ctrl.use_crc() { 2 } else { 1 };
79        if frame.len() < HEADER_LEN + 1 + trailer_len {
80            return Err(Error::BadLength {
81                declared: len,
82                actual: frame.len(),
83            });
84        }
85
86        let mut cursor = HEADER_LEN;
87
88        let scb = if ctrl.has_scb() {
89            let view = ScbView::parse(&frame[cursor..])?;
90            cursor += view.wire_len();
91            Some(view)
92        } else {
93            None
94        };
95
96        if cursor + 1 + trailer_len > frame.len() {
97            return Err(Error::BadLength {
98                declared: len,
99                actual: frame.len(),
100            });
101        }
102        let code = frame[cursor];
103        cursor += 1;
104
105        let mac_present = scb.map(|s| s.ty.has_mac()).unwrap_or(false);
106        let mac_room = if mac_present { MAC_LEN } else { 0 };
107
108        let payload_end =
109            frame
110                .len()
111                .checked_sub(trailer_len + mac_room)
112                .ok_or(Error::BadLength {
113                    declared: len,
114                    actual: frame.len(),
115                })?;
116        if payload_end < cursor {
117            return Err(Error::BadLength {
118                declared: len,
119                actual: frame.len(),
120            });
121        }
122        let data = &frame[cursor..payload_end];
123
124        let mac = if mac_present {
125            let m = &frame[payload_end..payload_end + MAC_LEN];
126            let mut arr = [0u8; MAC_LEN];
127            arr.copy_from_slice(m);
128            Some(arr)
129        } else {
130            None
131        };
132
133        let trailer = if ctrl.use_crc() {
134            let got = u16::from_le_bytes([frame[frame.len() - 2], frame[frame.len() - 1]]);
135            let want = crc16(&frame[..frame.len() - 2]);
136            if got != want {
137                return Err(Error::BadCrc { got, want });
138            }
139            Trailer::Crc(got)
140        } else {
141            let got = frame[frame.len() - 1];
142            let want = checksum8(&frame[..frame.len() - 1]);
143            if got != want {
144                return Err(Error::BadChecksum { got, want });
145            }
146            Trailer::Checksum(got)
147        };
148
149        Ok((
150            Self {
151                addr,
152                ctrl,
153                scb,
154                code,
155                data,
156                mac,
157                trailer,
158            },
159            len,
160        ))
161    }
162}
163
164#[cfg(feature = "alloc")]
165mod alloc_impls {
166    use super::*;
167    use crate::packet::scb::Scb;
168    use alloc::vec::Vec;
169
170    /// Builder for a single OSDP packet.
171    ///
172    /// Caller fills in the high-level fields and calls
173    /// [`PacketBuilder::encode`] (for non-secure packets) or
174    /// [`PacketBuilder::encode_with_mac`] (for `SCS_15..=SCS_18`) to produce
175    /// the bytes ready for transmission.
176    #[derive(Debug, Clone)]
177    pub struct PacketBuilder {
178        /// Address byte (with reply flag if PD โ†’ ACU).
179        pub addr: Address,
180        /// CTRL byte (SQN + flags).
181        pub ctrl: ControlByte,
182        /// Security Control Block (None if `CTRL.HAS_SCB` is clear).
183        pub scb: Option<Scb>,
184        /// Command or reply code byte.
185        pub code: u8,
186        /// DATA payload (already encrypted, if SCS_17/18).
187        pub data: Vec<u8>,
188    }
189
190    impl PacketBuilder {
191        /// Build a packet with neither SCB nor MAC.
192        pub fn plain(addr: Address, ctrl: ControlByte, code: u8, data: Vec<u8>) -> Self {
193            Self {
194                addr,
195                ctrl,
196                scb: None,
197                code,
198                data,
199            }
200        }
201
202        /// Encode the packet without any MAC.
203        ///
204        /// If the CTRL byte has [`super::CtrlFlags::HAS_SCB`] set but the
205        /// SCB type *would* require a MAC, the caller must use
206        /// [`Self::encode_with_mac`] instead.
207        pub fn encode(&self) -> Result<Vec<u8>, Error> {
208            self.encode_inner(None::<fn(&[u8]) -> [u8; super::MAC_LEN]>)
209        }
210
211        /// Encode the packet, computing the MAC trailer with `mac_fn`.
212        ///
213        /// `mac_fn` receives the bytes from SOM through end-of-DATA (i.e.
214        /// what the wire MAC is computed over) and returns the first 4 bytes
215        /// of the resulting MAC.
216        pub fn encode_with_mac<F>(&self, mac_fn: F) -> Result<Vec<u8>, Error>
217        where
218            F: FnOnce(&[u8]) -> [u8; super::MAC_LEN],
219        {
220            self.encode_inner(Some(mac_fn))
221        }
222
223        fn encode_inner<F>(&self, mac_fn: Option<F>) -> Result<Vec<u8>, Error>
224        where
225            F: FnOnce(&[u8]) -> [u8; super::MAC_LEN],
226        {
227            // Validate SCB <-> CTRL coherence.
228            let scb_present = self.ctrl.has_scb();
229            if scb_present != self.scb.is_some() {
230                return Err(Error::BadControlByte(self.ctrl.encode()));
231            }
232            let mac_required = self.scb.as_ref().map(|s| s.ty.has_mac()).unwrap_or(false);
233            if mac_required && mac_fn.is_none() {
234                return Err(Error::BadMac);
235            }
236
237            let trailer_len = if self.ctrl.use_crc() { 2 } else { 1 };
238            let mac_len = if mac_required { super::MAC_LEN } else { 0 };
239            let scb_len = self.scb.as_ref().map(|s| s.data.len() + 2).unwrap_or(0);
240            let total = HEADER_LEN + scb_len + 1 + self.data.len() + mac_len + trailer_len;
241
242            if total > u16::MAX as usize {
243                return Err(Error::BufferOverflow {
244                    need: total,
245                    have: u16::MAX as usize,
246                });
247            }
248
249            let mut out = Vec::with_capacity(total);
250            out.push(SOM);
251            out.push(self.addr.as_byte());
252            let len_bytes = (total as u16).to_le_bytes();
253            out.push(len_bytes[0]);
254            out.push(len_bytes[1]);
255            out.push(self.ctrl.encode());
256
257            if let Some(scb) = &self.scb {
258                scb.encode(&mut out);
259            }
260
261            out.push(self.code);
262            out.extend_from_slice(&self.data);
263
264            if let Some(f) = mac_fn {
265                let mac = f(&out);
266                out.extend_from_slice(&mac);
267            }
268
269            if self.ctrl.use_crc() {
270                let crc = crc16(&out);
271                out.extend_from_slice(&crc.to_le_bytes());
272            } else {
273                out.push(checksum8(&out));
274            }
275
276            debug_assert_eq!(out.len(), total);
277            Ok(out)
278        }
279    }
280}
281
282#[cfg(feature = "alloc")]
283pub use alloc_impls::PacketBuilder;
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288    use crate::packet::header::{CtrlFlags, Sqn};
289    use crate::packet::scb::{Scb, ScsType};
290    use alloc::vec::Vec;
291
292    fn make_poll(addr: u8, sqn: u8) -> Vec<u8> {
293        PacketBuilder::plain(
294            Address::pd(addr).unwrap(),
295            ControlByte::new(Sqn::new(sqn).unwrap(), CtrlFlags::USE_CRC),
296            0x60, // POLL
297            Vec::new(),
298        )
299        .encode()
300        .unwrap()
301    }
302
303    #[test]
304    fn roundtrip_poll() {
305        let bytes = make_poll(0x05, 1);
306        let (parsed, used) = ParsedPacket::parse(&bytes).unwrap();
307        assert_eq!(used, bytes.len());
308        assert_eq!(parsed.addr.pd_addr(), 0x05);
309        assert_eq!(parsed.ctrl.sqn.value(), 1);
310        assert!(parsed.ctrl.use_crc());
311        assert_eq!(parsed.code, 0x60);
312        assert!(parsed.data.is_empty());
313        assert!(parsed.scb.is_none());
314        assert!(parsed.mac.is_none());
315    }
316
317    #[test]
318    fn roundtrip_with_data_checksum() {
319        let bytes = PacketBuilder::plain(
320            Address::pd(0x01).unwrap(),
321            ControlByte::new(Sqn::new(2).unwrap(), CtrlFlags::empty()),
322            0x6E, // COMSET
323            alloc::vec![0x00, 0x80, 0x25, 0x00, 0x00],
324        )
325        .encode()
326        .unwrap();
327
328        let (parsed, used) = ParsedPacket::parse(&bytes).unwrap();
329        assert_eq!(used, bytes.len());
330        assert_eq!(parsed.code, 0x6E);
331        assert_eq!(parsed.data, &[0x00, 0x80, 0x25, 0x00, 0x00]);
332        assert!(matches!(parsed.trailer, Trailer::Checksum(_)));
333    }
334
335    #[test]
336    fn roundtrip_with_scb_no_mac() {
337        let scb = Scb::new(ScsType::Scs11, [0x00, 1, 2, 3, 4, 5, 6, 7, 8]);
338        let bytes = PacketBuilder {
339            addr: Address::pd(0x01).unwrap(),
340            ctrl: ControlByte::new(
341                Sqn::new(0).unwrap(),
342                CtrlFlags::USE_CRC | CtrlFlags::HAS_SCB,
343            ),
344            scb: Some(scb.clone()),
345            code: 0x76,
346            data: Vec::new(),
347        }
348        .encode()
349        .unwrap();
350
351        let (parsed, _) = ParsedPacket::parse(&bytes).unwrap();
352        let view = parsed.scb.unwrap();
353        assert_eq!(view.ty, ScsType::Scs11);
354        assert_eq!(view.data, scb.data.as_slice());
355        assert!(parsed.mac.is_none());
356    }
357
358    #[test]
359    fn roundtrip_with_scb_and_mac() {
360        let mac_value = [0xDE, 0xAD, 0xBE, 0xEF];
361        let scb = Scb::new(ScsType::Scs15, []);
362        let bytes = PacketBuilder {
363            addr: Address::pd(0x02).unwrap(),
364            ctrl: ControlByte::new(
365                Sqn::new(2).unwrap(),
366                CtrlFlags::USE_CRC | CtrlFlags::HAS_SCB,
367            ),
368            scb: Some(scb),
369            code: 0x60,
370            data: alloc::vec![0xAA, 0xBB],
371        }
372        .encode_with_mac(|_| mac_value)
373        .unwrap();
374
375        let (parsed, _) = ParsedPacket::parse(&bytes).unwrap();
376        assert_eq!(parsed.mac.unwrap(), mac_value);
377        assert_eq!(parsed.data, &[0xAA, 0xBB]);
378    }
379
380    #[test]
381    fn rejects_bad_som() {
382        let mut bytes = make_poll(1, 1);
383        bytes[0] = 0x52;
384        assert!(matches!(
385            ParsedPacket::parse(&bytes),
386            Err(Error::BadSom(0x52))
387        ));
388    }
389
390    #[test]
391    fn rejects_truncated() {
392        let bytes = [0x53u8, 0x01];
393        assert!(matches!(
394            ParsedPacket::parse(&bytes),
395            Err(Error::Truncated { .. })
396        ));
397    }
398
399    #[test]
400    fn rejects_bad_crc() {
401        let mut bytes = make_poll(1, 1);
402        let n = bytes.len();
403        bytes[n - 1] ^= 0xFF;
404        assert!(matches!(
405            ParsedPacket::parse(&bytes),
406            Err(Error::BadCrc { .. })
407        ));
408    }
409
410    #[test]
411    fn rejects_bad_checksum() {
412        let mut bytes = PacketBuilder::plain(
413            Address::pd(0x01).unwrap(),
414            ControlByte::new(Sqn::new(1).unwrap(), CtrlFlags::empty()),
415            0x60,
416            Vec::new(),
417        )
418        .encode()
419        .unwrap();
420        let n = bytes.len();
421        bytes[n - 1] ^= 0xFF;
422        assert!(matches!(
423            ParsedPacket::parse(&bytes),
424            Err(Error::BadChecksum { .. })
425        ));
426    }
427
428    #[test]
429    fn parser_total_on_random_garbage() {
430        // Phase 1 invariant: parser is total. Should never panic.
431        for seed in 0u64..256 {
432            let mut x = seed.wrapping_mul(0x9E37_79B9_7F4A_7C15);
433            let mut buf = [0u8; 64];
434            for b in buf.iter_mut() {
435                x = x
436                    .wrapping_mul(6364136223846793005)
437                    .wrapping_add(1442695040888963407);
438                *b = (x >> 33) as u8;
439            }
440            let _ = ParsedPacket::parse(&buf);
441        }
442    }
443}