From c77700a9ea100a7fa363faa0cdf57e9fdcd073af Mon Sep 17 00:00:00 2001 From: David Hoppenbrouwers Date: Thu, 8 Sep 2022 01:40:40 +0200 Subject: [PATCH] Report descriptor parser prototype --- .gitignore | 2 + Cargo.toml | 4 + report_descriptor/Cargo.toml | 8 + report_descriptor/src/lib.rs | 359 ++++++++++++++++++ report_descriptor/src/usage/button.rs | 3 + .../src/usage/generic_desktop.rs | 52 +++ report_descriptor/src/usage/mod.rs | 75 ++++ 7 files changed, 503 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 report_descriptor/Cargo.toml create mode 100644 report_descriptor/src/lib.rs create mode 100644 report_descriptor/src/usage/button.rs create mode 100644 report_descriptor/src/usage/generic_desktop.rs create mode 100644 report_descriptor/src/usage/mod.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4fffb2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..885028a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,4 @@ +[workspace] +members = [ + "report_descriptor", +] diff --git a/report_descriptor/Cargo.toml b/report_descriptor/Cargo.toml new file mode 100644 index 0000000..b16bfa4 --- /dev/null +++ b/report_descriptor/Cargo.toml @@ -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] diff --git a/report_descriptor/src/lib.rs b/report_descriptor/src/lib.rs new file mode 100644 index 0000000..4800daf --- /dev/null +++ b/report_descriptor/src/lib.rs @@ -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, ParseError>; + + fn next(&mut self) -> Option { + (!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()); + } +} diff --git a/report_descriptor/src/usage/button.rs b/report_descriptor/src/usage/button.rs new file mode 100644 index 0000000..08a2301 --- /dev/null +++ b/report_descriptor/src/usage/button.rs @@ -0,0 +1,3 @@ +usage! { + [0x09] +} diff --git a/report_descriptor/src/usage/generic_desktop.rs b/report_descriptor/src/usage/generic_desktop.rs new file mode 100644 index 0000000..0abb52f --- /dev/null +++ b/report_descriptor/src/usage/generic_desktop.rs @@ -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 +} diff --git a/report_descriptor/src/usage/mod.rs b/report_descriptor/src/usage/mod.rs new file mode 100644 index 0000000..b8ff191 --- /dev/null +++ b/report_descriptor/src/usage/mod.rs @@ -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 +}