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