Skip to main content

osdp/command/
mod.rs

1//! Typed OSDP commands (ACU → PD).
2//!
3//! # Spec: §6 / Annex A.1
4//!
5//! Each command is a strongly-typed Rust value; encoding to wire bytes goes
6//! through [`Command::encode_data`] and decoding through [`Command::decode`].
7//! [`CommandCode`] is the byte enum from Annex A.1.
8
9#[cfg(feature = "alloc")]
10use alloc::vec::Vec;
11
12use crate::error::Error;
13
14/// All `CMND` byte values from Annex A.1 of the spec.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16#[repr(u8)]
17#[allow(missing_docs)]
18pub enum CommandCode {
19    Poll = 0x60,
20    Id = 0x61,
21    Cap = 0x62,
22    LStat = 0x64,
23    IStat = 0x65,
24    OStat = 0x66,
25    RStat = 0x67,
26    Out = 0x68,
27    Led = 0x69,
28    Buz = 0x6A,
29    Text = 0x6B,
30    ComSet = 0x6E,
31    BioRead = 0x73,
32    BioMatch = 0x74,
33    KeySet = 0x75,
34    Chlng = 0x76,
35    SCrypt = 0x77,
36    AcuRxSize = 0x7B,
37    FileTransfer = 0x7C,
38    Mfg = 0x80,
39    XWrite = 0xA1,
40    Abort = 0xA2,
41    PivData = 0xA3,
42    GenAuth = 0xA4,
43    CrAuth = 0xA5,
44    KeepActive = 0xA7,
45}
46
47impl CommandCode {
48    /// Parse from raw byte.
49    pub const fn from_byte(b: u8) -> Result<Self, Error> {
50        Ok(match b {
51            0x60 => Self::Poll,
52            0x61 => Self::Id,
53            0x62 => Self::Cap,
54            0x64 => Self::LStat,
55            0x65 => Self::IStat,
56            0x66 => Self::OStat,
57            0x67 => Self::RStat,
58            0x68 => Self::Out,
59            0x69 => Self::Led,
60            0x6A => Self::Buz,
61            0x6B => Self::Text,
62            0x6E => Self::ComSet,
63            0x73 => Self::BioRead,
64            0x74 => Self::BioMatch,
65            0x75 => Self::KeySet,
66            0x76 => Self::Chlng,
67            0x77 => Self::SCrypt,
68            0x7B => Self::AcuRxSize,
69            0x7C => Self::FileTransfer,
70            0x80 => Self::Mfg,
71            0xA1 => Self::XWrite,
72            0xA2 => Self::Abort,
73            0xA3 => Self::PivData,
74            0xA4 => Self::GenAuth,
75            0xA5 => Self::CrAuth,
76            0xA7 => Self::KeepActive,
77            other => return Err(Error::UnknownCommand(other)),
78        })
79    }
80
81    /// Raw byte.
82    pub const fn as_byte(self) -> u8 {
83        self as u8
84    }
85}
86
87pub mod abort;
88pub mod acu_rx_size;
89pub mod biometric;
90pub mod buzzer;
91pub mod cap;
92pub mod chlng;
93pub mod comset;
94pub mod file_transfer;
95pub mod id;
96pub mod keep_active;
97pub mod keyset;
98pub mod led;
99pub mod local_status;
100pub mod mfg;
101pub mod output;
102pub mod piv;
103pub mod poll;
104pub mod scrypt;
105pub mod text;
106pub mod xwrite;
107
108pub use abort::Abort;
109pub use acu_rx_size::AcuRxSize;
110pub use biometric::{BioFormat, BioMatch, BioRead, BioType};
111pub use buzzer::{BuzzerControl, BuzzerTone};
112pub use cap::Cap;
113pub use chlng::Chlng;
114pub use comset::ComSet;
115pub use file_transfer::FileTransfer;
116pub use id::Id;
117pub use keep_active::KeepActive;
118pub use keyset::KeySet;
119pub use led::{LedColor, LedControl, LedPermanent, LedTemporary};
120pub use local_status::{InputStatus, LocalStatus, OutputStatus, ReaderStatus};
121pub use mfg::Mfg;
122pub use output::{OutputControl, OutputControlCode};
123pub use piv::{CrAuth, GenAuth, PivData};
124pub use poll::Poll;
125pub use scrypt::SCrypt;
126pub use text::{Text, TextCommand};
127pub use xwrite::{XWrite, XwrMode00, XwrMode01};
128
129/// Typed dispatch over every command supported by the crate.
130///
131/// Variants whose parser is not yet implemented carry the raw payload bytes
132/// so that round-tripping unknown-but-recognized commands still works.
133#[cfg(feature = "alloc")]
134#[derive(Debug, Clone, PartialEq, Eq)]
135#[non_exhaustive]
136#[allow(missing_docs)]
137pub enum Command {
138    Poll(Poll),
139    Id(Id),
140    Cap(Cap),
141    LocalStatus(LocalStatus),
142    InputStatus(InputStatus),
143    OutputStatus(OutputStatus),
144    ReaderStatus(ReaderStatus),
145    Output(OutputControl),
146    Led(LedControl),
147    Buzzer(BuzzerControl),
148    Text(Text),
149    ComSet(ComSet),
150    BioRead(BioRead),
151    BioMatch(BioMatch),
152    KeySet(KeySet),
153    Chlng(Chlng),
154    SCrypt(SCrypt),
155    AcuRxSize(AcuRxSize),
156    FileTransfer(FileTransfer),
157    Mfg(Mfg),
158    XWrite(XWrite),
159    Abort(Abort),
160    PivData(PivData),
161    GenAuth(GenAuth),
162    CrAuth(CrAuth),
163    KeepActive(KeepActive),
164}
165
166#[cfg(feature = "alloc")]
167impl Command {
168    /// Code byte for this command.
169    pub fn code(&self) -> CommandCode {
170        match self {
171            Command::Poll(_) => CommandCode::Poll,
172            Command::Id(_) => CommandCode::Id,
173            Command::Cap(_) => CommandCode::Cap,
174            Command::LocalStatus(_) => CommandCode::LStat,
175            Command::InputStatus(_) => CommandCode::IStat,
176            Command::OutputStatus(_) => CommandCode::OStat,
177            Command::ReaderStatus(_) => CommandCode::RStat,
178            Command::Output(_) => CommandCode::Out,
179            Command::Led(_) => CommandCode::Led,
180            Command::Buzzer(_) => CommandCode::Buz,
181            Command::Text(_) => CommandCode::Text,
182            Command::ComSet(_) => CommandCode::ComSet,
183            Command::BioRead(_) => CommandCode::BioRead,
184            Command::BioMatch(_) => CommandCode::BioMatch,
185            Command::KeySet(_) => CommandCode::KeySet,
186            Command::Chlng(_) => CommandCode::Chlng,
187            Command::SCrypt(_) => CommandCode::SCrypt,
188            Command::AcuRxSize(_) => CommandCode::AcuRxSize,
189            Command::FileTransfer(_) => CommandCode::FileTransfer,
190            Command::Mfg(_) => CommandCode::Mfg,
191            Command::XWrite(_) => CommandCode::XWrite,
192            Command::Abort(_) => CommandCode::Abort,
193            Command::PivData(_) => CommandCode::PivData,
194            Command::GenAuth(_) => CommandCode::GenAuth,
195            Command::CrAuth(_) => CommandCode::CrAuth,
196            Command::KeepActive(_) => CommandCode::KeepActive,
197        }
198    }
199
200    /// Encode the DATA payload for this command (does not include the code byte).
201    pub fn encode_data(&self) -> Result<Vec<u8>, Error> {
202        match self {
203            Command::Poll(c) => c.encode(),
204            Command::Id(c) => c.encode(),
205            Command::Cap(c) => c.encode(),
206            Command::LocalStatus(c) => c.encode(),
207            Command::InputStatus(c) => c.encode(),
208            Command::OutputStatus(c) => c.encode(),
209            Command::ReaderStatus(c) => c.encode(),
210            Command::Output(c) => c.encode(),
211            Command::Led(c) => c.encode(),
212            Command::Buzzer(c) => c.encode(),
213            Command::Text(c) => c.encode(),
214            Command::ComSet(c) => c.encode(),
215            Command::BioRead(c) => c.encode(),
216            Command::BioMatch(c) => c.encode(),
217            Command::KeySet(c) => c.encode(),
218            Command::Chlng(c) => c.encode(),
219            Command::SCrypt(c) => c.encode(),
220            Command::AcuRxSize(c) => c.encode(),
221            Command::FileTransfer(c) => c.encode(),
222            Command::Mfg(c) => c.encode(),
223            Command::XWrite(c) => c.encode(),
224            Command::Abort(c) => c.encode(),
225            Command::PivData(c) => c.encode(),
226            Command::GenAuth(c) => c.encode(),
227            Command::CrAuth(c) => c.encode(),
228            Command::KeepActive(c) => c.encode(),
229        }
230    }
231
232    /// Decode the typed payload, given the code byte and DATA bytes.
233    pub fn decode(code: CommandCode, data: &[u8]) -> Result<Self, Error> {
234        Ok(match code {
235            CommandCode::Poll => Command::Poll(Poll::decode(data)?),
236            CommandCode::Id => Command::Id(Id::decode(data)?),
237            CommandCode::Cap => Command::Cap(Cap::decode(data)?),
238            CommandCode::LStat => Command::LocalStatus(LocalStatus::decode(data)?),
239            CommandCode::IStat => Command::InputStatus(InputStatus::decode(data)?),
240            CommandCode::OStat => Command::OutputStatus(OutputStatus::decode(data)?),
241            CommandCode::RStat => Command::ReaderStatus(ReaderStatus::decode(data)?),
242            CommandCode::Out => Command::Output(OutputControl::decode(data)?),
243            CommandCode::Led => Command::Led(LedControl::decode(data)?),
244            CommandCode::Buz => Command::Buzzer(BuzzerControl::decode(data)?),
245            CommandCode::Text => Command::Text(Text::decode(data)?),
246            CommandCode::ComSet => Command::ComSet(ComSet::decode(data)?),
247            CommandCode::BioRead => Command::BioRead(BioRead::decode(data)?),
248            CommandCode::BioMatch => Command::BioMatch(BioMatch::decode(data)?),
249            CommandCode::KeySet => Command::KeySet(KeySet::decode(data)?),
250            CommandCode::Chlng => Command::Chlng(Chlng::decode(data)?),
251            CommandCode::SCrypt => Command::SCrypt(SCrypt::decode(data)?),
252            CommandCode::AcuRxSize => Command::AcuRxSize(AcuRxSize::decode(data)?),
253            CommandCode::FileTransfer => Command::FileTransfer(FileTransfer::decode(data)?),
254            CommandCode::Mfg => Command::Mfg(Mfg::decode(data)?),
255            CommandCode::XWrite => Command::XWrite(XWrite::decode(data)?),
256            CommandCode::Abort => Command::Abort(Abort::decode(data)?),
257            CommandCode::PivData => Command::PivData(PivData::decode(data)?),
258            CommandCode::GenAuth => Command::GenAuth(GenAuth::decode(data)?),
259            CommandCode::CrAuth => Command::CrAuth(CrAuth::decode(data)?),
260            CommandCode::KeepActive => Command::KeepActive(KeepActive::decode(data)?),
261        })
262    }
263}
264
265#[cfg(test)]
266mod tests {
267    use super::*;
268
269    #[test]
270    fn code_roundtrip() {
271        for byte in [
272            0x60u8, 0x61, 0x62, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6E, 0x73, 0x74,
273            0x75, 0x76, 0x77, 0x7B, 0x7C, 0x80, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA7,
274        ] {
275            assert_eq!(CommandCode::from_byte(byte).unwrap().as_byte(), byte);
276        }
277    }
278
279    #[test]
280    fn unknown_byte_errors() {
281        assert!(matches!(
282            CommandCode::from_byte(0xFF),
283            Err(Error::UnknownCommand(0xFF))
284        ));
285    }
286}