Report descriptor parser prototype
This commit is contained in:
8
report_descriptor/Cargo.toml
Normal file
8
report_descriptor/Cargo.toml
Normal file
@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "report_descriptor"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
359
report_descriptor/src/lib.rs
Normal file
359
report_descriptor/src/lib.rs
Normal file
@ -0,0 +1,359 @@
|
||||
//! ## References
|
||||
//!
|
||||
//! https://www.usb.org/sites/default/files/hid1_11.pdf
|
||||
|
||||
#![cfg_attr(not(test), no_std)]
|
||||
|
||||
pub mod usage;
|
||||
|
||||
pub use usage::{Usage, UsagePage};
|
||||
|
||||
use core::fmt;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[non_exhaustive]
|
||||
pub enum Item<'a> {
|
||||
Input(MainFlags),
|
||||
Output(MainFlags),
|
||||
Collection(Collection),
|
||||
Feature(MainFlags),
|
||||
EndCollection,
|
||||
|
||||
UsagePage(UsagePage),
|
||||
LogicalMin(i32),
|
||||
LogicalMax(i32),
|
||||
PhysicalMin(i32),
|
||||
PhysicalMax(i32),
|
||||
UnitExponential(u32),
|
||||
Unit(u32),
|
||||
ReportSize(u32),
|
||||
ReportId(u8),
|
||||
ReportCount(u32),
|
||||
Push,
|
||||
Pop,
|
||||
|
||||
Usage(Usage),
|
||||
UsageMin(u32),
|
||||
UsageMax(u32),
|
||||
DesignatorIndex(u32),
|
||||
DesignatorMin(u32),
|
||||
DesignatorMax(u32),
|
||||
StringIndex(u32),
|
||||
StringMin(u32),
|
||||
StringMax(u32),
|
||||
Delimiter(bool),
|
||||
|
||||
Unknown { tag: u8, data: &'a [u8] },
|
||||
}
|
||||
|
||||
impl<'a> Item<'a> {
|
||||
// Main (6.2.2.4)
|
||||
const INPUT: u8 = 0x80;
|
||||
const OUTPUT: u8 = 0x90;
|
||||
const COLLECTION: u8 = 0xa0;
|
||||
const FEATURE: u8 = 0xb0;
|
||||
const END_COLLECTION: u8 = 0xc0;
|
||||
|
||||
// Global (6.2.2.7)
|
||||
const USAGE_PAGE: u8 = 0x04;
|
||||
const LOGI_MIN: u8 = 0x14;
|
||||
const LOGI_MAX: u8 = 0x24;
|
||||
const PHYS_MIN: u8 = 0x34;
|
||||
const PHYS_MAX: u8 = 0x44;
|
||||
const UNIT_EXP: u8 = 0x54;
|
||||
const UNIT: u8 = 0x64;
|
||||
const REPORT_SIZE: u8 = 0x74;
|
||||
const REPORT_ID: u8 = 0x84;
|
||||
const REPORT_COUNT: u8 = 0x94;
|
||||
const PUSH: u8 = 0xa4;
|
||||
const POP: u8 = 0xb4;
|
||||
|
||||
// Local (6.2.2.8)
|
||||
const USAGE: u8 = 0x08;
|
||||
const USAGE_MIN: u8 = 0x18;
|
||||
const USAGE_MAX: u8 = 0x28;
|
||||
const DESIGNATOR_INDEX: u8 = 0x38;
|
||||
const DESIGNATOR_MIN: u8 = 0x48;
|
||||
const DESIGNATOR_MAX: u8 = 0x58;
|
||||
const STRING_INDEX: u8 = 0x78;
|
||||
const STRING_MIN: u8 = 0x88;
|
||||
const STRING_MAX: u8 = 0x98;
|
||||
const DELIMITER: u8 = 0xa8;
|
||||
|
||||
fn parse(data: &'a [u8], usage_page: u16) -> Result<(Self, &'a [u8]), ParseError> {
|
||||
use ParseError::*;
|
||||
let prefix = *data.get(0).ok_or(Truncated)?;
|
||||
let (size, tag);
|
||||
if prefix == 0b1111_11_10 {
|
||||
// Long item (6.2.2.3)
|
||||
size = usize::from(*data.get(1).ok_or(Truncated)?);
|
||||
tag = *data.get(2).ok_or(Truncated)?;
|
||||
} else {
|
||||
// Short item (6.2.2.2)
|
||||
size = (1 << (prefix & 0b11)) >> 1;
|
||||
tag = prefix & !0b11;
|
||||
}
|
||||
let d = data.get(1..1 + size).ok_or(Truncated)?;
|
||||
let d8 = || {
|
||||
d.try_into()
|
||||
.map_err(|_| UnexpectedData)
|
||||
.map(u8::from_le_bytes)
|
||||
};
|
||||
let d16u = || {
|
||||
Ok(u16::from_le_bytes(match d {
|
||||
&[] => [0, 0],
|
||||
&[a] => [a, 0],
|
||||
&[a, b] => [a, b],
|
||||
_ => return Err(UnexpectedData),
|
||||
}))
|
||||
};
|
||||
let d32u = || {
|
||||
Ok(u32::from_le_bytes(match d {
|
||||
&[] => [0, 0, 0, 0],
|
||||
&[a] => [a, 0, 0, 0],
|
||||
&[a, b] => [a, b, 0, 0],
|
||||
&[a, b, c] => [a, b, c, 0],
|
||||
&[a, b, c, d] => [a, b, c, d],
|
||||
_ => return Err(UnexpectedData),
|
||||
}))
|
||||
};
|
||||
let d32i = || {
|
||||
Ok(match d {
|
||||
&[] => 0,
|
||||
&[a] => i8::from_le_bytes([a]) as _,
|
||||
&[a, b] => i16::from_le_bytes([a, b]) as _,
|
||||
&[a, b, c] => i32::from_le_bytes([a, b, c, (c as i8 >> 7) as _]),
|
||||
&[a, b, c, d] => i32::from_le_bytes([a, b, c, d]),
|
||||
_ => return Err(UnexpectedData),
|
||||
})
|
||||
};
|
||||
let d_empty = |e| d.is_empty().then(|| e).ok_or(UnexpectedData);
|
||||
|
||||
let item = match tag {
|
||||
Self::INPUT => Self::Input(MainFlags(d32u()?)),
|
||||
Self::OUTPUT => Self::Output(MainFlags(d32u()?)),
|
||||
Self::COLLECTION => Self::Collection(Collection::from_raw(d8()?)),
|
||||
Self::FEATURE => Self::Feature(MainFlags(d32u()?)),
|
||||
Self::END_COLLECTION => d_empty(Self::EndCollection)?,
|
||||
|
||||
Self::USAGE_PAGE => Self::UsagePage(UsagePage::from_raw(d16u()?)),
|
||||
Self::LOGI_MIN => Self::LogicalMin(d32i()?),
|
||||
Self::LOGI_MAX => Self::LogicalMax(d32i()?),
|
||||
Self::PHYS_MIN => Self::PhysicalMin(d32i()?),
|
||||
Self::PHYS_MAX => Self::PhysicalMax(d32i()?),
|
||||
Self::UNIT_EXP => Self::UnitExponential(d32u()?),
|
||||
Self::UNIT => Self::Unit(d32u()?),
|
||||
Self::REPORT_SIZE => Self::ReportSize(d32u()?),
|
||||
Self::REPORT_ID => Self::ReportId(d8()?),
|
||||
Self::REPORT_COUNT => Self::ReportCount(d32u()?),
|
||||
Self::PUSH => Self::Push,
|
||||
Self::POP => Self::Pop,
|
||||
|
||||
Self::USAGE => Self::Usage(match d {
|
||||
&[] => Usage::from_raw(usage_page, 0),
|
||||
&[a] => Usage::from_raw(usage_page, u16::from_le_bytes([a, 0])),
|
||||
&[a, b] => Usage::from_raw(usage_page, u16::from_le_bytes([a, b])),
|
||||
&[a, b, c] => {
|
||||
Usage::from_raw(u16::from_le_bytes([c, 0]), u16::from_le_bytes([a, b]))
|
||||
}
|
||||
&[a, b, c, d] => {
|
||||
Usage::from_raw(u16::from_le_bytes([c, d]), u16::from_le_bytes([a, b]))
|
||||
}
|
||||
_ => return Err(UnexpectedData),
|
||||
}),
|
||||
Self::USAGE_MIN => Self::UsageMin(d32u()?),
|
||||
Self::USAGE_MAX => Self::UsageMax(d32u()?),
|
||||
Self::DESIGNATOR_INDEX => Self::DesignatorIndex(d32u()?),
|
||||
Self::DESIGNATOR_MIN => Self::DesignatorMin(d32u()?),
|
||||
Self::DESIGNATOR_MAX => Self::DesignatorMax(d32u()?),
|
||||
Self::STRING_INDEX => Self::StringIndex(d32u()?),
|
||||
Self::STRING_MIN => Self::StringMin(d32u()?),
|
||||
Self::STRING_MAX => Self::StringMax(d32u()?),
|
||||
Self::DELIMITER => Self::Delimiter(match d {
|
||||
&[0] => true,
|
||||
&[1] => false,
|
||||
_ => return Err(UnexpectedData),
|
||||
}),
|
||||
|
||||
_ => Self::Unknown { tag, data: d },
|
||||
};
|
||||
|
||||
Ok((item, &data[1 + size..]))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct MainFlags(pub u32);
|
||||
|
||||
macro_rules! flags {
|
||||
{ $($flag:ident $bit:literal)* } => {
|
||||
impl MainFlags {
|
||||
$(
|
||||
pub fn $flag(&self) -> bool {
|
||||
self.0 & 1 << $bit != 0
|
||||
}
|
||||
)*
|
||||
}
|
||||
|
||||
impl fmt::Debug for MainFlags {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct(stringify!(MainFlags))
|
||||
$(.field(stringify!($flag), &self.$flag()))*
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
flags! {
|
||||
constant 0
|
||||
variable 1
|
||||
relative 2
|
||||
wrap 3
|
||||
non_linear 4
|
||||
no_preferred 5
|
||||
null_state 6
|
||||
volatile 7
|
||||
buffered_bytes 8
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[non_exhaustive]
|
||||
pub enum Collection {
|
||||
Physical,
|
||||
Application,
|
||||
Logical,
|
||||
Report,
|
||||
NamedArray,
|
||||
UsageSwitch,
|
||||
UsageModifier,
|
||||
Unknown(u8),
|
||||
}
|
||||
|
||||
impl Collection {
|
||||
fn from_raw(raw: u8) -> Self {
|
||||
match raw {
|
||||
0x00 => Self::Physical,
|
||||
0x01 => Self::Application,
|
||||
0x02 => Self::Logical,
|
||||
0x03 => Self::Report,
|
||||
0x04 => Self::NamedArray,
|
||||
0x05 => Self::UsageSwitch,
|
||||
0x06 => Self::UsageModifier,
|
||||
r => Self::Unknown(r),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ParseError {
|
||||
Truncated,
|
||||
UnexpectedData,
|
||||
StackOverflow,
|
||||
}
|
||||
|
||||
pub struct Parser<'a> {
|
||||
data: &'a [u8],
|
||||
usage_page: [u16; 16],
|
||||
stack_index: usize,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for Parser<'a> {
|
||||
type Item = Result<Item<'a>, ParseError>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
(!self.data.is_empty()).then(|| {
|
||||
use ParseError::*;
|
||||
let e;
|
||||
let page = self
|
||||
.usage_page
|
||||
.get_mut(self.stack_index)
|
||||
.ok_or(StackOverflow)?;
|
||||
// TODO update usage page
|
||||
(e, self.data) = Item::parse(self.data, *page)?;
|
||||
match &e {
|
||||
Item::Collection(_) => {
|
||||
self.stack_index += 1;
|
||||
*self.usage_page.get_mut(self.stack_index).ok_or(StackOverflow)? = *page;
|
||||
}
|
||||
Item::EndCollection => {
|
||||
self.stack_index -= 1;
|
||||
}
|
||||
Item::UsagePage(p) => *page = p.as_raw(),
|
||||
_ => {}
|
||||
}
|
||||
Ok(e)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(data: &[u8]) -> Parser<'_> {
|
||||
Parser {
|
||||
data,
|
||||
usage_page: [0xffff; 16],
|
||||
stack_index: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// usb/dev-hid.c
|
||||
const QEMU_USB_TABLET: &[u8] = &[
|
||||
0x05, 0x01, 0x09, 0x02, 0xa1, 0x01, 0x09, 0x01, 0xa1, 0x00, 0x05, 0x09, 0x19, 0x01, 0x29,
|
||||
0x03, 0x15, 0x00, 0x25, 0x01, 0x95, 0x03, 0x75, 0x01, 0x81, 0x02, 0x95, 0x01, 0x75, 0x05,
|
||||
0x81, 0x01, 0x05, 0x01, 0x09, 0x30, 0x09, 0x31, 0x15, 0x00, 0x26, 0xff, 0x7f, 0x35, 0x00,
|
||||
0x46, 0xff, 0x7f, 0x75, 0x10, 0x95, 0x02, 0x81, 0x02, 0x05, 0x01, 0x09, 0x38, 0x15, 0x81,
|
||||
0x25, 0x7f, 0x35, 0x00, 0x45, 0x00, 0x75, 0x08, 0x95, 0x01, 0x81, 0x06, 0xc0, 0xc0,
|
||||
];
|
||||
|
||||
#[track_caller]
|
||||
fn tk(it: &mut Parser<'_>, item: Item<'_>) {
|
||||
assert_eq!(it.next().map(Result::unwrap), Some(item));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn qemu_usb_tablet() {
|
||||
let mut it = parse(QEMU_USB_TABLET);
|
||||
let it = &mut it;
|
||||
tk(it, Item::UsagePage(UsagePage::GenericDesktop));
|
||||
tk(it, Item::Usage(Usage::GenericDesktop(usage::generic_desktop::Usage::Mouse)));
|
||||
tk(it, Item::Collection(Collection::Application));
|
||||
tk(it, Item::Usage(Usage::GenericDesktop(usage::generic_desktop::Usage::Pointer)));
|
||||
tk(it, Item::Collection(Collection::Physical));
|
||||
tk(it, Item::UsagePage(UsagePage::Button));
|
||||
tk(it, Item::UsageMin(1));
|
||||
tk(it, Item::UsageMax(3));
|
||||
tk(it, Item::LogicalMin(0));
|
||||
tk(it, Item::LogicalMax(1));
|
||||
tk(it, Item::ReportCount(3));
|
||||
tk(it, Item::ReportSize(1));
|
||||
tk(it, Item::Input(MainFlags(0b010))); // absolute, variable, data
|
||||
tk(it, Item::ReportCount(1));
|
||||
tk(it, Item::ReportSize(5));
|
||||
tk(it, Item::Input(MainFlags(0b1))); // constant
|
||||
tk(it, Item::UsagePage(UsagePage::GenericDesktop));
|
||||
tk(it, Item::Usage(Usage::GenericDesktop(usage::generic_desktop::Usage::X)));
|
||||
tk(it, Item::Usage(Usage::GenericDesktop(usage::generic_desktop::Usage::Y)));
|
||||
tk(it, Item::LogicalMin(0));
|
||||
tk(it, Item::LogicalMax(0x7fff));
|
||||
tk(it, Item::PhysicalMin(0));
|
||||
tk(it, Item::PhysicalMax(0x7fff));
|
||||
tk(it, Item::ReportSize(16));
|
||||
tk(it, Item::ReportCount(2));
|
||||
tk(it, Item::Input(MainFlags(0b010))); // absolute, variable, data
|
||||
tk(it, Item::UsagePage(UsagePage::GenericDesktop));
|
||||
tk(it, Item::Usage(Usage::GenericDesktop(usage::generic_desktop::Usage::Wheel)));
|
||||
tk(it, Item::LogicalMin(-0x7f));
|
||||
tk(it, Item::LogicalMax(0x7f));
|
||||
tk(it, Item::PhysicalMin(0));
|
||||
tk(it, Item::PhysicalMax(0));
|
||||
tk(it, Item::ReportSize(8));
|
||||
tk(it, Item::ReportCount(1));
|
||||
tk(it, Item::Input(MainFlags(0b110))); // relative, variable, data
|
||||
tk(it, Item::EndCollection);
|
||||
tk(it, Item::EndCollection);
|
||||
assert!(it.next().is_none());
|
||||
}
|
||||
}
|
3
report_descriptor/src/usage/button.rs
Normal file
3
report_descriptor/src/usage/button.rs
Normal file
@ -0,0 +1,3 @@
|
||||
usage! {
|
||||
[0x09]
|
||||
}
|
52
report_descriptor/src/usage/generic_desktop.rs
Normal file
52
report_descriptor/src/usage/generic_desktop.rs
Normal file
@ -0,0 +1,52 @@
|
||||
usage! {
|
||||
[0x01]
|
||||
0x01 Pointer
|
||||
0x02 Mouse
|
||||
|
||||
0x04 Joystick
|
||||
0x05 Gamepad
|
||||
0x06 Keyboard
|
||||
0x07 Keypad
|
||||
0x08 MultiAxisController
|
||||
0x09 TabletPcSystemControls
|
||||
0x0a WaterCoolingDevice
|
||||
0x0b ComputerChassisDevice
|
||||
0x0c WirelessRadioControls
|
||||
0x0d PortableDeviceControl
|
||||
0x0e SystemMultiAxisController
|
||||
0x0f SpatialController
|
||||
0x10 AssistiveControl
|
||||
0x11 DeviceDock
|
||||
0x12 DockableDevice
|
||||
0x13 CallStateManagementControl
|
||||
|
||||
0x30 X
|
||||
0x31 Y
|
||||
0x32 Z
|
||||
0x33 Rx
|
||||
0x34 Ry
|
||||
0x35 Rz
|
||||
0x36 Slider
|
||||
0x37 Dial
|
||||
0x38 Wheel
|
||||
0x39 HatSwitch
|
||||
0x3a CountedBuffer
|
||||
0x3b ByteCount
|
||||
0x3c MotionWakeup
|
||||
0x3d Start
|
||||
0x3e Select
|
||||
|
||||
0x40 Vx
|
||||
0x41 Vy
|
||||
0x42 Vz
|
||||
0x43 Vbrx
|
||||
0x44 Vbry
|
||||
0x45 Vbrz
|
||||
0x46 Vno
|
||||
0x47 FeatureNotification
|
||||
0x48 ResolutionMultiplier
|
||||
0x49 Qx
|
||||
0x4a Qy
|
||||
0x4b Qz
|
||||
0x4c Qw
|
||||
}
|
75
report_descriptor/src/usage/mod.rs
Normal file
75
report_descriptor/src/usage/mod.rs
Normal file
@ -0,0 +1,75 @@
|
||||
//! # References
|
||||
//!
|
||||
//! * https://www.usb.org/sites/default/files/hut1_3_0.pdf
|
||||
|
||||
macro_rules! usage {
|
||||
{ [$page:literal] $($i:literal $v:ident)* } => {
|
||||
pub const PAGE: u16 = $page;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[non_exhaustive]
|
||||
pub enum Usage {
|
||||
$($v,)*
|
||||
Unknown(u16),
|
||||
}
|
||||
|
||||
pub(crate) fn from_raw(raw: u16) -> Usage {
|
||||
use Usage::*;
|
||||
match raw {
|
||||
$($i => $v,)*
|
||||
r => Unknown(r),
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub mod generic_desktop;
|
||||
pub mod button;
|
||||
|
||||
macro_rules! page {
|
||||
{ $($v:ident $m:ident)* } => {
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[non_exhaustive]
|
||||
pub enum UsagePage {
|
||||
$($v,)*
|
||||
Unknown(u16),
|
||||
}
|
||||
|
||||
impl UsagePage {
|
||||
pub(crate) fn from_raw(raw: u16) -> Self {
|
||||
match raw {
|
||||
$($m::PAGE => Self::$v,)*
|
||||
r => Self::Unknown(r),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn as_raw(&self) -> u16 {
|
||||
match self {
|
||||
$(Self::$v => $m::PAGE,)*
|
||||
Self::Unknown(n) => *n,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[non_exhaustive]
|
||||
pub enum Usage {
|
||||
$($v($m::Usage),)*
|
||||
Unknown(u16, u16),
|
||||
}
|
||||
|
||||
impl Usage {
|
||||
pub(crate) fn from_raw(page: u16, id: u16) -> Self {
|
||||
match page {
|
||||
$($m::PAGE => Self::$v($m::from_raw(id)),)*
|
||||
_ => Self::Unknown(page, id),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
page! {
|
||||
GenericDesktop generic_desktop
|
||||
Button button
|
||||
}
|
Reference in New Issue
Block a user