Skip to main content

osdp/reply/
nak.rs

1//! `osdp_NAK` (`0x41`).
2//!
3//! # Spec: ยง7.3, Table 47
4//!
5//! Body is `error_code (1 byte)` followed by an *optional* per-record
6//! completion-code array (only when error code is `0x09`).
7
8use crate::error::Error;
9use crate::payload_util::require_at_least;
10use alloc::vec::Vec;
11
12/// NAK error code (Table 47).
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14#[repr(u8)]
15#[allow(missing_docs)]
16pub enum NakErrorCode {
17    NoError = 0x00,
18    BadCrcOrChecksum = 0x01,
19    CommandLengthError = 0x02,
20    UnknownCommandCode = 0x03,
21    UnexpectedSequenceNumber = 0x04,
22    SecurityBlockTypeNotSupported = 0x05,
23    EncryptedCommunicationRequired = 0x06,
24    BioTypeNotSupported = 0x07,
25    BioFormatNotSupported = 0x08,
26    UnableToProcessCommandRecord = 0x09,
27}
28
29impl NakErrorCode {
30    /// Parse from byte.
31    pub const fn from_byte(b: u8) -> Result<Self, Error> {
32        Ok(match b {
33            0x00 => Self::NoError,
34            0x01 => Self::BadCrcOrChecksum,
35            0x02 => Self::CommandLengthError,
36            0x03 => Self::UnknownCommandCode,
37            0x04 => Self::UnexpectedSequenceNumber,
38            0x05 => Self::SecurityBlockTypeNotSupported,
39            0x06 => Self::EncryptedCommunicationRequired,
40            0x07 => Self::BioTypeNotSupported,
41            0x08 => Self::BioFormatNotSupported,
42            0x09 => Self::UnableToProcessCommandRecord,
43            _ => {
44                return Err(Error::MalformedPayload {
45                    code: 0x41,
46                    reason: "unknown NAK error code",
47                });
48            }
49        })
50    }
51
52    /// Raw byte.
53    pub const fn as_byte(self) -> u8 {
54        self as u8
55    }
56}
57
58/// `osdp_NAK` body.
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct Nak {
61    /// Top-level error code.
62    pub error: NakErrorCode,
63    /// Per-record completion codes (only when `error == UnableToProcessCommandRecord`).
64    pub completion_codes: Vec<u8>,
65}
66
67impl Nak {
68    /// Build a simple NAK with no completion array.
69    pub fn simple(err: NakErrorCode) -> Self {
70        Self {
71            error: err,
72            completion_codes: Vec::new(),
73        }
74    }
75
76    /// Encode.
77    pub fn encode(&self) -> Result<Vec<u8>, Error> {
78        let mut out = Vec::with_capacity(1 + self.completion_codes.len());
79        out.push(self.error.as_byte());
80        out.extend_from_slice(&self.completion_codes);
81        Ok(out)
82    }
83
84    /// Decode. Treats unknown error codes as a parse failure rather than
85    /// silently dropping the byte (NAK arriving with an unknown code is
86    /// usually a sign of bus chaos worth surfacing).
87    pub fn decode(data: &[u8]) -> Result<Self, Error> {
88        require_at_least(data, 1, 0x41)?;
89        let error = NakErrorCode::from_byte(data[0])?;
90        let completion_codes = if error == NakErrorCode::UnableToProcessCommandRecord {
91            data[1..].to_vec()
92        } else {
93            Vec::new()
94        };
95        Ok(Self {
96            error,
97            completion_codes,
98        })
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn simple_roundtrip() {
108        let n = Nak::simple(NakErrorCode::BadCrcOrChecksum);
109        let bytes = n.encode().unwrap();
110        let parsed = Nak::decode(&bytes).unwrap();
111        assert_eq!(parsed, n);
112    }
113
114    #[test]
115    fn record_completion_codes() {
116        let n = Nak {
117            error: NakErrorCode::UnableToProcessCommandRecord,
118            completion_codes: alloc::vec![0xAA, 0xBB, 0xCC],
119        };
120        let bytes = n.encode().unwrap();
121        let parsed = Nak::decode(&bytes).unwrap();
122        assert_eq!(parsed, n);
123    }
124
125    #[test]
126    fn rejects_unknown_code() {
127        assert!(Nak::decode(&[0xFE]).is_err());
128    }
129}