Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add decoding of HID report descriptors #207

Merged
merged 1 commit into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
/tests/*/output.txt
/tests/*/devices-output.txt
/tests/ui/*/output.txt
/tests/hid/*/output.txt
/vcpkg
/wix/LICENSE-dynamic-libraries.txt
/wix/LICENSE-packetry.txt
Expand Down
20 changes: 20 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ anyhow = { version = "1.0.79", features = ["backtrace"] }
crc = "3.2.1"
usb-ids = "1.2024.4"
dark-light = "1.1.1"
hidreport = "0.4.1"
hut = "0.2.1"

[dev-dependencies]
serde = { version = "1.0.196", features = ["derive"] }
Expand Down
4 changes: 2 additions & 2 deletions src/capture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1750,9 +1750,9 @@ impl ItemSource<TrafficItem, TrafficViewMode> for CaptureReader {
"End of SOF groups"),
(Request(transfer), true) if detail => write!(s,
"Control transfer on device {addr}\n{}",
transfer.summary()),
transfer.summary(true)),
(Request(transfer), true) => write!(s,
"{}", transfer.summary()),
"{}", transfer.summary(false)),
(IncompleteRequest, true) => write!(s,
"Incomplete control transfer on device {addr}"),
(Request(_) | IncompleteRequest, false) => write!(s,
Expand Down
199 changes: 198 additions & 1 deletion src/usb.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
use std::collections::BTreeMap;
use std::fmt::Formatter;
use std::mem::size_of;
use std::ops::Range;

use bytemuck_derive::{Pod, Zeroable};
use bytemuck::pod_read_unaligned;
use crc::{Crc, CRC_16_USB};
use hidreport::{
Field,
LogicalMaximum,
LogicalMinimum,
ParserError,
Report,
ReportCount,
ReportDescriptor,
};
use itertools::{Itertools, Position};
use num_enum::{IntoPrimitive, FromPrimitive};
use derive_more::{From, Into, Display};
use usb_ids::FromId;
Expand Down Expand Up @@ -1160,7 +1171,7 @@ pub struct ControlTransfer {
}

impl ControlTransfer {
pub fn summary(&self) -> String {
pub fn summary(&self, detail: bool) -> String {
let request_type = self.fields.type_fields.request_type();
let direction = self.fields.type_fields.direction();
let request = self.fields.request;
Expand Down Expand Up @@ -1221,6 +1232,14 @@ impl ControlTransfer {
parts.push(
format!(": {}", UTF16Bytes(&self.data[2..size])));
},
(RequestType::Standard,
StandardRequest::GetDescriptor,
DescriptorType::Class(0x22))
if detail && self.recipient_class == Some(ClassId::HID) =>
{
parts.push(
format!("\n{}", HidReportDescriptor::from(&self.data)));
}
(..) => {}
};
let summary = parts.concat();
Expand Down Expand Up @@ -1281,9 +1300,152 @@ impl std::fmt::Display for UTF16ByteVec {
}
}

struct HidReportDescriptor(Result<ReportDescriptor, ParserError>);

impl HidReportDescriptor {
fn from(data: &[u8]) -> Self {
Self(ReportDescriptor::try_from(data))
}
}

impl std::fmt::Display for HidReportDescriptor{
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
match &self.0 {
Ok(desc) => {
for report in desc.input_reports() {
write_report(f, "Input", report)?;
}
for report in desc.output_reports() {
write_report(f, "Output", report)?;
}
},
Err(parse_err) => write!(f,
"\nFailed to parse report descriptor: {parse_err}")?,
}
Ok(())
}
}

fn write_report(f: &mut Formatter<'_>, kind: &str, report: &impl Report)
-> Result<(), std::fmt::Error>
{
use Field::*;
use Position::*;
write!(f, "\n○ {kind} report ")?;
match (report.report_id(), report.size_in_bytes()) {
(Some(id), 1) => writeln!(f, "#{id} (1 byte):")?,
(Some(id), n) => writeln!(f, "#{id} ({n} bytes):")?,
(None, 1) => writeln!(f, "(1 byte):")?,
(None, n) => writeln!(f, "({n} bytes):")?,
}
for (position, field) in report.fields().iter().with_position() {
match position {
First | Middle => write!(f, "├── ")?,
Last | Only => write!(f, "└── ")?,
};
match &field {
Array(array) => {
write!(f, "Array of {} {}: ",
array.report_count,
if array.report_count == ReportCount::from(1) {
"button"
} else {
"buttons"
}
)?;
write_bits(f, &array.bits)?;
let usage_range = array.usage_range();
write!(f, " [")?;
write_usage(f, &usage_range.minimum())?;
write!(f, " — ")?;
write_usage(f, &usage_range.maximum())?;
write!(f, "]")?;
}
Variable(var) => {
write_usage(f, &var.usage)?;
write!(f, ": ")?;
write_bits(f, &var.bits)?;
let bit_count = var.bits.end - var.bits.start;
if bit_count > 1 {
let max = (1 << bit_count) - 1;
if var.logical_minimum != LogicalMinimum::from(0) ||
var.logical_maximum != LogicalMaximum::from(max)
{
write!(f, " (values {} to {})",
var.logical_minimum,
var.logical_maximum)?;
}
}
},
Constant(constant) => {
write!(f, "Padding: ")?;
write_bits(f, &constant.bits)?;
},
};
writeln!(f)?;
}
Ok(())
}

fn write_usage<T>(f: &mut Formatter, usage: T)
-> Result<(), std::fmt::Error>
where u32: From<T>
{
let usage_code: u32 = usage.into();
match hut::Usage::try_from(usage_code) {
Ok(usage) => write!(f, "{usage}")?,
Err(_) => {
let page: u16 = (usage_code >> 16) as u16;
let id: u16 = usage_code as u16;
match hut::UsagePage::try_from(page) {
Ok(page) => write!(f,
"{} usage 0x{id:02X}", page.name())?,
Err(_) => write!(f,
"Unknown page 0x{page:02X} usage 0x{id:02X}")?,
}
}
};
Ok(())
}

fn write_bits(f: &mut Formatter, bit_range: &Range<usize>)
-> Result<(), std::fmt::Error>
{
let bit_count = bit_range.end - bit_range.start;
let byte_range = (bit_range.start / 8)..((bit_range.end - 1)/ 8);
let byte_count = byte_range.end - byte_range.start;
match (byte_count, bit_count) {
(_, 1) => write!(f,
"byte {} bit {}",
byte_range.start,
bit_range.start % 8)?,
(0, n) if n == 8 && bit_range.start % 8 == 0 => write!(f,
"byte {}",
byte_range.start)?,
(0, _) => write!(f,
"byte {} bits {}-{}",
byte_range.start,
bit_range.start % 8, (bit_range.end - 1) % 8)?,
(_, n) if n % 8 == 0 && bit_range.start % 8 == 0 => write!(f,
"bytes {}-{}",
byte_range.start,
byte_range.end)?,
(_, _) => write!(f,
"byte {} bit {} — byte {} bit {}",
byte_range.start,
bit_range.start % 8,
byte_range.end,
(bit_range.end - 1) % 8)?,
}
Ok(())
}

#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use std::io::{BufRead, BufReader, BufWriter, Write};
use std::path::PathBuf;

#[test]
fn test_parse_sof() {
Expand Down Expand Up @@ -1339,6 +1501,41 @@ mod tests {
panic!("Expected Data but got {:?}", p);
}
}

#[test]
fn test_parse_hid() {
let test_dir = PathBuf::from("./tests/hid/");
let mut list_path = test_dir.clone();
list_path.push("tests.txt");
let list_file = File::open(list_path).unwrap();
for test_name in BufReader::new(list_file).lines() {
let mut test_path = test_dir.clone();
test_path.push(test_name.unwrap());
let mut desc_path = test_path.clone();
let mut ref_path = test_path.clone();
let mut out_path = test_path.clone();
desc_path.push("descriptor.bin");
ref_path.push("reference.txt");
out_path.push("output.txt");
{
let data = std::fs::read(desc_path).unwrap();
let descriptor = HidReportDescriptor::from(&data);
let out_file = File::create(out_path.clone()).unwrap();
let mut writer = BufWriter::new(out_file);
write!(writer, "{descriptor}").unwrap();
}
let ref_file = File::open(ref_path).unwrap();
let out_file = File::open(out_path.clone()).unwrap();
let ref_reader = BufReader::new(ref_file);
let out_reader = BufReader::new(out_file);
let mut out_lines = out_reader.lines();
for line in ref_reader.lines() {
let expected = line.unwrap();
let actual = out_lines.next().unwrap().unwrap();
assert_eq!(actual, expected);
}
}
}
}

pub mod prelude {
Expand Down
Binary file added tests/hid/amp/descriptor.bin
Binary file not shown.
67 changes: 67 additions & 0 deletions tests/hid/amp/reference.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@

○ Input report #3 (2 bytes):
├── Volume Increment: byte 1 bit 0
├── Volume Decrement: byte 1 bit 1
├── Play/Pause: byte 1 bit 2
├── Voice Command: byte 1 bit 3
├── Scan Previous Track: byte 1 bit 4
├── Scan Next Track: byte 1 bit 5
└── Padding: byte 1 bits 6-7

○ Input report #5 (17 bytes):
├── Consumer usage 0x00: byte 1 (values 0 to 1)
├── Consumer usage 0x00: byte 2 (values 0 to 1)
├── Consumer usage 0x00: byte 3 (values 0 to 1)
├── Consumer usage 0x00: byte 4 (values 0 to 1)
├── Consumer usage 0x00: byte 5 (values 0 to 1)
├── Consumer usage 0x00: byte 6 (values 0 to 1)
├── Consumer usage 0x00: byte 7 (values 0 to 1)
├── Consumer usage 0x00: byte 8 (values 0 to 1)
├── Consumer usage 0x00: byte 9 (values 0 to 1)
├── Consumer usage 0x00: byte 10 (values 0 to 1)
├── Consumer usage 0x00: byte 11 (values 0 to 1)
├── Consumer usage 0x00: byte 12 (values 0 to 1)
├── Consumer usage 0x00: byte 13 (values 0 to 1)
├── Consumer usage 0x00: byte 14 (values 0 to 1)
├── Consumer usage 0x00: byte 15 (values 0 to 1)
└── Consumer usage 0x00: byte 16 (values 0 to 1)

○ Output report #4 (39 bytes):
├── Consumer usage 0x00: byte 1 (values 0 to 1)
├── Consumer usage 0x00: byte 2 (values 0 to 1)
├── Consumer usage 0x00: byte 3 (values 0 to 1)
├── Consumer usage 0x00: byte 4 (values 0 to 1)
├── Consumer usage 0x00: byte 5 (values 0 to 1)
├── Consumer usage 0x00: byte 6 (values 0 to 1)
├── Consumer usage 0x00: byte 7 (values 0 to 1)
├── Consumer usage 0x00: byte 8 (values 0 to 1)
├── Consumer usage 0x00: byte 9 (values 0 to 1)
├── Consumer usage 0x00: byte 10 (values 0 to 1)
├── Consumer usage 0x00: byte 11 (values 0 to 1)
├── Consumer usage 0x00: byte 12 (values 0 to 1)
├── Consumer usage 0x00: byte 13 (values 0 to 1)
├── Consumer usage 0x00: byte 14 (values 0 to 1)
├── Consumer usage 0x00: byte 15 (values 0 to 1)
├── Consumer usage 0x00: byte 16 (values 0 to 1)
├── Consumer usage 0x00: byte 17 (values 0 to 1)
├── Consumer usage 0x00: byte 18 (values 0 to 1)
├── Consumer usage 0x00: byte 19 (values 0 to 1)
├── Consumer usage 0x00: byte 20 (values 0 to 1)
├── Consumer usage 0x00: byte 21 (values 0 to 1)
├── Consumer usage 0x00: byte 22 (values 0 to 1)
├── Consumer usage 0x00: byte 23 (values 0 to 1)
├── Consumer usage 0x00: byte 24 (values 0 to 1)
├── Consumer usage 0x00: byte 25 (values 0 to 1)
├── Consumer usage 0x00: byte 26 (values 0 to 1)
├── Consumer usage 0x00: byte 27 (values 0 to 1)
├── Consumer usage 0x00: byte 28 (values 0 to 1)
├── Consumer usage 0x00: byte 29 (values 0 to 1)
├── Consumer usage 0x00: byte 30 (values 0 to 1)
├── Consumer usage 0x00: byte 31 (values 0 to 1)
├── Consumer usage 0x00: byte 32 (values 0 to 1)
├── Consumer usage 0x00: byte 33 (values 0 to 1)
├── Consumer usage 0x00: byte 34 (values 0 to 1)
├── Consumer usage 0x00: byte 35 (values 0 to 1)
├── Consumer usage 0x00: byte 36 (values 0 to 1)
├── Consumer usage 0x00: byte 37 (values 0 to 1)
└── Consumer usage 0x00: byte 38 (values 0 to 1)
Binary file added tests/hid/headset/descriptor.bin
Binary file not shown.
19 changes: 19 additions & 0 deletions tests/hid/headset/reference.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@

○ Input report (4 bytes):
├── Volume Increment: byte 0 bit 0
├── Volume Decrement: byte 0 bit 1
├── Mute: byte 0 bit 2
├── Consumer usage 0x00: byte 0 bit 3
├── Hook Switch: byte 0 bit 4
├── Consumer usage 0x00: byte 0 bit 5
├── Consumer usage 0x00: byte 0 bit 6
├── Consumer usage 0x00: byte 0 bit 7
├── Consumer usage 0x00: byte 1
├── Consumer usage 0x00: byte 2
└── Consumer usage 0x00: byte 3

○ Output report (4 bytes):
├── Consumer usage 0x00: byte 0
├── Consumer usage 0x00: byte 1
├── Consumer usage 0x00: byte 2
└── Consumer usage 0x00: byte 3
Binary file added tests/hid/hub/descriptor.bin
Binary file not shown.
Loading
Loading