Skip to main content

hsm_usb_protocol/
frame.rs

1// Copyright (c) 2026 Tuloup Simon
2//
3// This program is free software: you can redistribute it and/or modify
4// it under the terms of the GNU General Public License as published by
5// the Free Software Foundation, either version 3 of the License, or
6// any later version.
7//
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11// GNU General Public License for more details.
12//
13// You should have received a copy of the GNU General Public License
14// along with this program. If not, see <https://www.gnu.org/licenses/>.
15
16//! Byte-level encoding and decoding of HID frames.
17//!
18//! [`Frame`] is opcode-agnostic: it holds a `u8` opcode and a payload slice.
19//! Higher-level interpretation of the payload is the caller's job
20//! (firmware-side or host-side).
21
22use crate::{HEADER_SIZE, HID_REPORT_SIZE, MAX_PAYLOAD_SIZE};
23
24/// One parsed HID frame.
25///
26/// Holds a borrow into the underlying buffer. Lifetimes are tied to
27/// the buffer that was parsed, so a `Frame` is cheap to pass around without
28/// copies.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub struct Frame<'a>
31{
32    /// First byte of the report: a `CommandOpcode` from the host or a
33    /// `ResponseStatus` from the token, depending on direction.
34    pub opcode: u8,
35
36    /// Payload bytes (`len` bytes from the parsed report).
37    pub payload: &'a [u8],
38}
39
40/// Errors returned when parsing a HID report.
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42#[cfg_attr(feature = "defmt", derive(defmt::Format))]
43pub enum FrameParseError
44{
45    /// The buffer is not exactly [`HID_REPORT_SIZE`] bytes.
46    WrongReportSize
47    {
48        /// Actual buffer size.
49        actual: usize
50    },
51
52    /// The declared `len` exceeds [`MAX_PAYLOAD_SIZE`].
53    PayloadLenTooLarge
54    {
55        /// Declared payload size in the header.
56        declared: usize
57    },
58}
59
60impl<'a> Frame<'a>
61{
62    /// Parse a HID report into a [`Frame`].
63    ///
64    /// Returns a frame borrowing into `buf`. The payload slice has the
65    /// length declared in the header (`len`), not the report size. Padding
66    /// bytes after the payload are not validated: they are ignored
67    /// according to the protocol contract.
68    ///
69    /// # Errors
70    /// See [`FrameParseError`].
71    pub fn parse(buf: &'a [u8]) -> Result<Self, FrameParseError>
72    {
73        if buf.len() != HID_REPORT_SIZE
74        {
75            return Err(FrameParseError::WrongReportSize { actual: buf.len() });
76        }
77
78        let opcode = buf[0];
79        let len = u16::from_le_bytes([buf[1], buf[2]]) as usize;
80
81        if len > MAX_PAYLOAD_SIZE
82        {
83            return Err(FrameParseError::PayloadLenTooLarge { declared: len });
84        }
85
86        Ok(Self
87        {
88            opcode,
89            payload: &buf[HEADER_SIZE..HEADER_SIZE + len],
90        })
91    }
92
93    /// Build a HID report from an opcode and a payload.
94    ///
95    /// Writes into `out` (must be exactly [`HID_REPORT_SIZE`] long). All
96    /// bytes past the payload are zeroed.
97    ///
98    /// # Errors
99    /// Returns [`FrameBuildError::WrongOutputSize`] if `out` is not
100    /// [`HID_REPORT_SIZE`] bytes, or
101    /// [`FrameBuildError::PayloadTooLarge`] if `payload.len()` exceeds
102    /// [`MAX_PAYLOAD_SIZE`].
103    pub fn write
104    (
105        opcode: u8,
106        payload: &[u8],
107        out: &mut [u8],
108    ) -> Result<(), FrameBuildError>
109    {
110        if out.len() != HID_REPORT_SIZE
111        {
112            return Err(FrameBuildError::WrongOutputSize { actual: out.len() });
113        }
114        if payload.len() > MAX_PAYLOAD_SIZE
115        {
116            return Err(FrameBuildError::PayloadTooLarge { len: payload.len() });
117        }
118
119        // Zero the whole buffer first so the padding bytes are explicitly
120        // zero (the protocol requires it for the wire, even if receivers
121        // are supposed to ignore them).
122        for byte in out.iter_mut()
123        {
124            *byte = 0;
125        }
126
127        out[0] = opcode;
128        // The length should fits in u16: payload.len() <= MAX_PAYLOAD_SIZE < 256.
129        let len = u16::try_from(payload.len())
130            .map_err(|_| FrameBuildError::PayloadTooLarge {len: payload.len()})?;
131        let [lo, hi] = len.to_le_bytes();
132        out[1] = lo;
133        out[2] = hi;
134        out[HEADER_SIZE..HEADER_SIZE + payload.len()].copy_from_slice(payload);
135        Ok(())
136    }
137
138    /// Convenience wrapper around [`Frame::write`] that returns the report
139    /// by value on the stack. Useful for callers that just want a buffer
140    /// to hand to `embassy-usb` or to the OS HID layer.
141    ///
142    /// # Errors
143    /// See [`Frame::write`].
144    pub fn to_report(opcode: u8, payload: &[u8]) -> Result<[u8; HID_REPORT_SIZE], FrameBuildError>
145    {
146        let mut report = [0u8; HID_REPORT_SIZE];
147        Self::write(opcode, payload, &mut report)?;
148        Ok(report)
149    }
150}
151
152/// Errors returned when building a HID report.
153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
154#[cfg_attr(feature = "defmt", derive(defmt::Format))]
155pub enum FrameBuildError
156{
157    /// `out` was not exactly [`HID_REPORT_SIZE`] bytes long.
158    WrongOutputSize
159    {
160        /// Actual size of the provided output buffer.
161        actual: usize
162    },
163
164    /// `payload.len()` exceeds [`MAX_PAYLOAD_SIZE`].
165    PayloadTooLarge
166    {
167        /// The would-be payload size.
168        len: usize
169    },
170}
171
172#[cfg(test)]
173mod tests
174{
175    use super::*;
176
177    #[test]
178    fn write_then_parse_round_trip_with_empty_payload()
179    {
180        let report = Frame::to_report(0x01, &[]).unwrap();
181        let frame = Frame::parse(&report).unwrap();
182        assert_eq!(frame.opcode, 0x01);
183        assert_eq!(frame.payload.len(), 0);
184    }
185
186    #[test]
187    fn write_then_parse_round_trip_with_typical_payload()
188    {
189        let payload = [0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE];
190        let report = Frame::to_report(0x03, &payload).unwrap();
191        let frame = Frame::parse(&report).unwrap();
192        assert_eq!(frame.opcode, 0x03);
193        assert_eq!(frame.payload, &payload);
194    }
195
196    #[test]
197    fn write_pads_with_zeros()
198    {
199        let report = Frame::to_report(0xAA, &[0x11, 0x22, 0x33]).unwrap();
200        assert_eq!(report[0], 0xAA);
201        assert_eq!(report[1], 3); // len_lo
202        assert_eq!(report[2], 0); // len_hi
203        assert_eq!(&report[3..6], &[0x11, 0x22, 0x33]);
204        assert!(report[6..].iter().all(|b| *b == 0));
205    }
206
207    #[test]
208    fn write_at_max_payload_succeeds()
209    {
210        let payload = [0x42u8; MAX_PAYLOAD_SIZE];
211        let report = Frame::to_report(0x10, &payload).unwrap();
212        let frame = Frame::parse(&report).unwrap();
213        assert_eq!(frame.payload.len(), MAX_PAYLOAD_SIZE);
214        assert_eq!(frame.payload, &payload);
215    }
216
217    #[test]
218    fn write_rejects_oversized_payload()
219    {
220        let oversized = [0u8; MAX_PAYLOAD_SIZE + 1];
221        let mut out = [0u8; HID_REPORT_SIZE];
222        let err = Frame::write(0x00, &oversized, &mut out).unwrap_err();
223        assert_eq!(err, FrameBuildError::PayloadTooLarge { len: MAX_PAYLOAD_SIZE + 1 });
224    }
225
226    #[test]
227    fn write_rejects_wrong_output_size()
228    {
229        let mut out = [0u8; 63];
230        let err = Frame::write(0x00, &[], &mut out).unwrap_err();
231        assert_eq!(err, FrameBuildError::WrongOutputSize { actual: 63 });
232    }
233
234    #[test]
235    fn parse_rejects_wrong_report_size()
236    {
237        let short = [0u8; 32];
238        let err = Frame::parse(&short).unwrap_err();
239        assert_eq!(err, FrameParseError::WrongReportSize { actual: 32 });
240    }
241
242    #[test]
243    fn parse_rejects_oversized_payload_len()
244    {
245        let mut report = [0u8; HID_REPORT_SIZE];
246        report[0] = 0x42;
247        // Claim a payload of MAX_PAYLOAD_SIZE + 1 bytes. The value fits in u16
248        // by construction (MAX_PAYLOAD_SIZE < 256), so the try_from is infallible
249        // and we unwrap in the test.
250        let oversized = u16::try_from(MAX_PAYLOAD_SIZE + 1).unwrap();
251        let [lo, hi] = oversized.to_le_bytes();
252        report[1] = lo;
253        report[2] = hi;
254        let err = Frame::parse(&report).unwrap_err();
255        assert_eq!(
256            err,
257            FrameParseError::PayloadLenTooLarge { declared: MAX_PAYLOAD_SIZE + 1 }
258        );
259    }
260
261    #[test]
262    fn parse_ignores_padding_bytes()
263    {
264        let mut report = [0u8; HID_REPORT_SIZE];
265        report[0] = 0x01;
266        report[1] = 2;
267        report[2] = 0;
268        report[3] = 0xAB;
269        report[4] = 0xCD;
270        // Pollute padding bytes.
271        for byte in report.iter_mut().skip(5)
272        {
273            *byte = 0xFF;
274        }
275        let frame = Frame::parse(&report).unwrap();
276        assert_eq!(frame.opcode, 0x01);
277        assert_eq!(frame.payload, &[0xAB, 0xCD]);
278    }
279
280    #[test]
281    fn len_uses_little_endian_encoding()
282    {
283        // Encode a payload of 256 bytes worth. If MAX_PAYLOAD_SIZE were
284        // larger it would fit, but here we just verify the byte order in
285        // the header. We can't actually create such a payload, so we go
286        // the other way: hand-craft a report with len = 0x0102 (258) which
287        // is over MAX, and verify that the parser sees it correctly when
288        // it complains.
289        let mut report = [0u8; HID_REPORT_SIZE];
290        report[0] = 0x00;
291        report[1] = 0x02; // lo
292        report[2] = 0x01; // hi -> declared = 0x0102 = 258
293        let err = Frame::parse(&report).unwrap_err();
294        assert_eq!(err, FrameParseError::PayloadLenTooLarge { declared: 258 });
295    }
296}