Skip to main content

osdp/command/
biometric.rs

1//! `osdp_BIOREAD` (`0x73`) and `osdp_BIOMATCH` (`0x74`).
2//!
3//! # Spec: §6.14, §6.15, Tables 24–25
4
5use crate::error::Error;
6use crate::payload_util::{require_at_least, require_exact_len};
7use alloc::vec::Vec;
8
9/// Biometric type code (Table 24).
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11#[repr(u8)]
12#[allow(missing_docs)]
13pub enum BioType {
14    NotSpecified = 0x00,
15    RightThumb = 0x01,
16    RightIndex = 0x02,
17    RightMiddle = 0x03,
18    RightRing = 0x04,
19    RightLittle = 0x05,
20    LeftThumb = 0x06,
21    LeftIndex = 0x07,
22    LeftMiddle = 0x08,
23    LeftRing = 0x09,
24    LeftLittle = 0x0A,
25    RightIris = 0x0B,
26    RightRetina = 0x0C,
27    LeftIris = 0x0D,
28    LeftRetina = 0x0E,
29    Face = 0x0F,
30    RightHandGeometry = 0x10,
31    LeftHandGeometry = 0x11,
32}
33
34impl BioType {
35    /// Parse from byte; preserves "NotSpecified" for unknowns to avoid losing
36    /// data that the receiver ought to NAK with `0x07`.
37    pub const fn from_byte(b: u8) -> Self {
38        match b {
39            0x00 => Self::NotSpecified,
40            0x01 => Self::RightThumb,
41            0x02 => Self::RightIndex,
42            0x03 => Self::RightMiddle,
43            0x04 => Self::RightRing,
44            0x05 => Self::RightLittle,
45            0x06 => Self::LeftThumb,
46            0x07 => Self::LeftIndex,
47            0x08 => Self::LeftMiddle,
48            0x09 => Self::LeftRing,
49            0x0A => Self::LeftLittle,
50            0x0B => Self::RightIris,
51            0x0C => Self::RightRetina,
52            0x0D => Self::LeftIris,
53            0x0E => Self::LeftRetina,
54            0x0F => Self::Face,
55            0x10 => Self::RightHandGeometry,
56            0x11 => Self::LeftHandGeometry,
57            _ => Self::NotSpecified,
58        }
59    }
60
61    /// Raw byte.
62    pub const fn as_byte(self) -> u8 {
63        self as u8
64    }
65}
66
67/// Biometric data format (Table 25).
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
69#[repr(u8)]
70#[allow(missing_docs)]
71pub enum BioFormat {
72    NotSpecified = 0x00,
73    FingerprintRawPgm = 0x01,
74    FingerprintAnsi378 = 0x02,
75}
76
77impl BioFormat {
78    /// Parse.
79    pub const fn from_byte(b: u8) -> Self {
80        match b {
81            0x01 => Self::FingerprintRawPgm,
82            0x02 => Self::FingerprintAnsi378,
83            _ => Self::NotSpecified,
84        }
85    }
86
87    /// Raw byte.
88    pub const fn as_byte(self) -> u8 {
89        self as u8
90    }
91}
92
93/// `osdp_BIOREAD` body.
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub struct BioRead {
96    /// Reader number.
97    pub reader: u8,
98    /// Biometric type to capture.
99    pub bio_type: BioType,
100    /// Format the PD should respond with.
101    pub bio_format: BioFormat,
102    /// Capture quality (0..=100).
103    pub quality: u8,
104}
105
106impl BioRead {
107    /// Encode.
108    pub fn encode(&self) -> Result<Vec<u8>, Error> {
109        Ok(alloc::vec![
110            self.reader,
111            self.bio_type.as_byte(),
112            self.bio_format.as_byte(),
113            self.quality,
114        ])
115    }
116
117    /// Decode.
118    pub fn decode(data: &[u8]) -> Result<Self, Error> {
119        require_exact_len(data, 4, 0x73)?;
120        Ok(Self {
121            reader: data[0],
122            bio_type: BioType::from_byte(data[1]),
123            bio_format: BioFormat::from_byte(data[2]),
124            quality: data[3],
125        })
126    }
127}
128
129/// `osdp_BIOMATCH` body. Carries a template to match against.
130#[derive(Debug, Clone, PartialEq, Eq)]
131pub struct BioMatch {
132    /// Reader number.
133    pub reader: u8,
134    /// Biometric type to capture.
135    pub bio_type: BioType,
136    /// Format of the template.
137    pub bio_format: BioFormat,
138    /// Quality threshold for the match.
139    pub quality: u8,
140    /// Template payload.
141    pub template: Vec<u8>,
142}
143
144impl BioMatch {
145    /// Encode.
146    pub fn encode(&self) -> Result<Vec<u8>, Error> {
147        if self.template.len() > u16::MAX as usize {
148            return Err(Error::MalformedPayload {
149                code: 0x74,
150                reason: "BIOMATCH template > 65535 bytes",
151            });
152        }
153        let mut out = Vec::with_capacity(6 + self.template.len());
154        out.push(self.reader);
155        out.push(self.bio_type.as_byte());
156        out.push(self.bio_format.as_byte());
157        out.push(self.quality);
158        out.extend_from_slice(&(self.template.len() as u16).to_le_bytes());
159        out.extend_from_slice(&self.template);
160        Ok(out)
161    }
162
163    /// Decode.
164    pub fn decode(data: &[u8]) -> Result<Self, Error> {
165        require_at_least(data, 6, 0x74)?;
166        let length = u16::from_le_bytes([data[4], data[5]]) as usize;
167        if data.len() != 6 + length {
168            return Err(Error::MalformedPayload {
169                code: 0x74,
170                reason: "BIOMATCH length disagrees with payload",
171            });
172        }
173        Ok(Self {
174            reader: data[0],
175            bio_type: BioType::from_byte(data[1]),
176            bio_format: BioFormat::from_byte(data[2]),
177            quality: data[3],
178            template: data[6..6 + length].to_vec(),
179        })
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186
187    #[test]
188    fn bioread_roundtrip() {
189        let body = BioRead {
190            reader: 0x02,
191            bio_type: BioType::LeftThumb,
192            bio_format: BioFormat::FingerprintAnsi378,
193            quality: 80,
194        };
195        let bytes = body.encode().unwrap();
196        assert_eq!(bytes, [0x02, 0x06, 0x02, 80]);
197        assert_eq!(BioRead::decode(&bytes).unwrap(), body);
198    }
199
200    #[test]
201    fn bioread_rejects_wrong_length() {
202        assert!(matches!(
203            BioRead::decode(&[0; 5]),
204            Err(Error::PayloadLength { code: 0x73, .. })
205        ));
206    }
207
208    #[test]
209    fn biotype_unknown_decodes_to_not_specified() {
210        assert_eq!(BioType::from_byte(0xFF), BioType::NotSpecified);
211    }
212
213    #[test]
214    fn biomatch_roundtrip() {
215        let body = BioMatch {
216            reader: 0x01,
217            bio_type: BioType::RightIndex,
218            bio_format: BioFormat::FingerprintRawPgm,
219            quality: 75,
220            template: alloc::vec![0xDE, 0xAD, 0xBE, 0xEF],
221        };
222        let bytes = body.encode().unwrap();
223        // 4 fixed bytes + 2 length bytes (LE 4 = [0x04, 0x00]) + 4 template bytes.
224        assert_eq!(
225            bytes,
226            [0x01, 0x02, 0x01, 75, 0x04, 0x00, 0xDE, 0xAD, 0xBE, 0xEF]
227        );
228        assert_eq!(BioMatch::decode(&bytes).unwrap(), body);
229    }
230
231    #[test]
232    fn biomatch_rejects_length_disagreement() {
233        // Header claims 5-byte template but payload only has 3 bytes after header.
234        let bad = [0x01, 0x02, 0x01, 75, 0x05, 0x00, 0xAA, 0xBB, 0xCC];
235        assert!(matches!(
236            BioMatch::decode(&bad),
237            Err(Error::MalformedPayload { code: 0x74, .. })
238        ));
239    }
240}