Skip to main content

hdm_am/
wire.rs

1//! Wire framing: magic, operation codes, request/response envelopes.
2
3use crate::error::Error;
4use std::io::{Read, Write};
5
6/// 6-byte magic prefix: UTF-8 encoding of "ՀԴՄ" (HDM).
7pub const MAGIC: [u8; 6] = [0xD5, 0x80, 0xD4, 0xB4, 0xD5, 0x84];
8
9/// Wire framing version advertised in the request header (big-endian).
10///
11/// This is the *framing* version, not the document version ([`crate::SPEC_VERSION`]): it went
12/// `03→04→05` across early manuals and has been frozen at `05` since spec v0.5 (2017), through
13/// v0.7.3. Spec v0.7.3 still documents `05`, so this is correct, not stale.
14///
15/// A device reports its own protocol version in the *response* header, which may differ from what
16/// we send — documented behaviour since v0.3. [`crate::identify`] therefore matches on the major
17/// version only.
18pub const PROTOCOL_VERSION: [u8; 2] = [0x00, 0x05];
19
20/// Length of the response header preceding the encrypted payload.
21pub const RESPONSE_HEADER_LEN: usize = 2 + 3 + 2 + 2 + 2;
22
23/// Successful response code (200) per spec §4.10.
24pub const RESPONSE_CODE_OK: u16 = 200;
25
26/// HDM protocol operation codes. Codes 1-10 are v0.5; 11-16 were added through v0.7.3.
27#[repr(u8)]
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum OperationCode {
30    /// List active operators and departments.
31    ListOpsAndDeps = 1,
32    /// Operator login. Returns the session key used for all subsequent ops.
33    OperatorLogin = 2,
34    /// Operator logout.
35    OperatorLogout = 3,
36    /// Print a fiscal receipt.
37    PrintReceipt = 4,
38    /// Reprint a copy of the last receipt.
39    PrintLastReceipt = 5,
40    /// Print a return / refund receipt (full, by-amount, or per-item). Registers the return.
41    PrintReturnReceipt = 6,
42    /// Configure receipt header and footer text lines.
43    SetupHeaderFooter = 7,
44    /// Upload the receipt header logo image (Base64 BMP, colour depth ≤4 bits).
45    SetupHeaderLogo = 8,
46    /// Print a fiscal report (1 = X-report, 2 = Z-report).
47    PrintFiscalReport = 9,
48    /// Get (look up) the contents of a receipt you intend to return. Read-only.
49    GetReturnableReceipt = 10,
50    /// Cash drawer in/out adjustment.
51    CashInOut = 11,
52    /// Query device date and time.
53    DateTime = 12,
54    /// Receipt sample (test print).
55    ReceiptSample = 13,
56    /// Synchronise HDM with tax authority.
57    HdmTimeSync = 14,
58    /// List available payment systems (returns codes 1-18, see spec §4.8).
59    PaymentSystemsList = 15,
60    /// Submit a single eMark traceability code.
61    SingleEmark = 16,
62}
63
64/// Encoded request to be written to the wire.
65#[derive(Debug)]
66pub struct Request {
67    /// Operation code.
68    pub op: OperationCode,
69    /// Encrypted payload (ciphertext only — the header is added on encode).
70    pub payload: Vec<u8>,
71}
72
73impl Request {
74    /// Encode into the on-the-wire byte stream.
75    ///
76    /// # Errors
77    /// Returns [`Error::Transport`] if `payload` exceeds `u16::MAX` bytes (HDM frames have a 2-byte length field).
78    pub fn encode(&self, writer: &mut impl Write) -> Result<(), Error> {
79        let len = u16::try_from(self.payload.len()).map_err(|_| {
80            Error::Transport(std::io::Error::new(
81                std::io::ErrorKind::InvalidInput,
82                "payload exceeds u16::MAX",
83            ))
84        })?;
85        writer.write_all(&MAGIC)?;
86        writer.write_all(&PROTOCOL_VERSION)?;
87        writer.write_all(&[self.op as u8, 0])?;
88        writer.write_all(&len.to_be_bytes())?;
89        writer.write_all(&self.payload)?;
90        Ok(())
91    }
92}
93
94/// Decoded response header. The encrypted payload follows immediately on the wire.
95#[derive(Debug, Clone, Copy)]
96pub struct ResponseHeader {
97    /// HDM protocol version reported by the device (major, minor).
98    pub protocol_version: (u8, u8),
99    /// HDM firmware version (major, minor, patch).
100    pub software_version: (u8, u8, u8),
101    /// Response code per spec §4.10. 200 = success.
102    pub code: u16,
103    /// Length of the encrypted payload that follows.
104    pub payload_len: u16,
105}
106
107impl ResponseHeader {
108    /// Read and parse the 11-byte response header from a transport.
109    ///
110    /// # Errors
111    /// Returns [`Error::Transport`] on read failure.
112    pub fn read(reader: &mut impl Read) -> Result<Self, Error> {
113        let mut buf = [0u8; RESPONSE_HEADER_LEN];
114        reader.read_exact(&mut buf)?;
115        let [
116            pv_major,
117            pv_minor,
118            sw_major,
119            sw_minor,
120            sw_patch,
121            code_hi,
122            code_lo,
123            len_hi,
124            len_lo,
125            _r0,
126            _r1,
127        ] = buf;
128        Ok(Self {
129            protocol_version: (pv_major, pv_minor),
130            software_version: (sw_major, sw_minor, sw_patch),
131            code: u16::from_be_bytes([code_hi, code_lo]),
132            payload_len: u16::from_be_bytes([len_hi, len_lo]),
133        })
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140    use std::io::Cursor;
141
142    #[test]
143    fn magic_is_hdm_in_utf8() {
144        assert_eq!(std::str::from_utf8(&MAGIC).unwrap(), "ՀԴՄ");
145    }
146
147    #[test]
148    fn request_encode_round_trips_through_header() {
149        let req = Request {
150            op: OperationCode::OperatorLogin,
151            payload: vec![0xAA; 32],
152        };
153        let mut buf = Vec::new();
154        req.encode(&mut buf).unwrap();
155
156        let [
157            m0,
158            m1,
159            m2,
160            m3,
161            m4,
162            m5,
163            pv0,
164            pv1,
165            op,
166            reserved,
167            len_hi,
168            len_lo,
169            ..,
170        ] = buf.as_slice()
171        else {
172            panic!("encoded buffer too short");
173        };
174        assert_eq!([*m0, *m1, *m2, *m3, *m4, *m5], MAGIC);
175        assert_eq!([*pv0, *pv1], PROTOCOL_VERSION);
176        assert_eq!(*op, OperationCode::OperatorLogin as u8);
177        assert_eq!(*reserved, 0);
178        assert_eq!(u16::from_be_bytes([*len_hi, *len_lo]), 32);
179    }
180
181    #[test]
182    fn response_header_decodes() {
183        let raw = [
184            0x00, 0x05, 0x02, 0x02, 0x10, 0x00, 0xC8, 0x00, 0x10, 0x00, 0x00,
185        ];
186        let mut cursor = Cursor::new(raw);
187        let hdr = ResponseHeader::read(&mut cursor).unwrap();
188        assert_eq!(hdr.protocol_version, (0, 5));
189        assert_eq!(hdr.software_version, (2, 2, 16));
190        assert_eq!(hdr.code, RESPONSE_CODE_OK);
191        assert_eq!(hdr.payload_len, 16);
192    }
193}