543 lines
19 KiB
Zig
543 lines
19 KiB
Zig
//! USB class-code decoding: turn the (class, subclass, protocol) triple a USB device or
|
|
//! interface reports in its descriptors into typed values. The device descriptor carries one
|
|
//! triple for the whole device, and each interface descriptor carries its own; a class code
|
|
//! of zero at the device level defers entirely to the interfaces. Subclass and protocol
|
|
//! codes are qualified by the class code — the same value means different things under
|
|
//! different classes — so there is no single SubClass or Protocol enum: each class with
|
|
//! spec-defined codes gets its own namespace below. Pure reference data (from the USB-IF
|
|
//! defined class codes; see https://www.usb.org/defined-class-codes) — no hardware access —
|
|
//! so it is shared by kernel discovery and any user-space tool (device naming, driver
|
|
//! matching).
|
|
|
|
// Base class codes (assigned by the USB-IF). The comment on each value notes where the code
|
|
// may legally appear: in the device descriptor, in interface descriptors, or both.
|
|
pub const Class = enum(u8) {
|
|
// Use class information in the interface descriptors (device descriptor only). Each
|
|
// interface within a configuration specifies its own class information and the various
|
|
// interfaces operate independently.
|
|
per_interface = 0x00,
|
|
// Audio: speakers, microphones, sound cards (interface)
|
|
audio = 0x01,
|
|
// Communications and CDC control: modems, network adapters (both)
|
|
communications = 0x02,
|
|
// Human Interface Device: keyboards, mice, game controllers (interface)
|
|
hid = 0x03,
|
|
// Physical: force-feedback devices (interface)
|
|
physical = 0x05,
|
|
// Image: still-imaging cameras, scanners (interface)
|
|
image = 0x06,
|
|
// Printer (interface)
|
|
printer = 0x07,
|
|
// Mass storage: flash drives, external disks, card readers (interface)
|
|
mass_storage = 0x08,
|
|
// Hub (device descriptor only)
|
|
hub = 0x09,
|
|
// CDC-Data: the data interfaces paired with a communications control interface
|
|
// (interface)
|
|
cdc_data = 0x0A,
|
|
// Smart card readers (interface)
|
|
smart_card = 0x0B,
|
|
// Content security (interface)
|
|
content_security = 0x0D,
|
|
// Video: webcams (interface)
|
|
video = 0x0E,
|
|
// Personal healthcare devices (interface)
|
|
personal_healthcare = 0x0F,
|
|
// Audio/Video devices (interface)
|
|
audio_video = 0x10,
|
|
// Billboard: describes alternate modes a USB Type-C device supports (device descriptor
|
|
// only)
|
|
billboard = 0x11,
|
|
// USB Type-C bridge (interface)
|
|
type_c_bridge = 0x12,
|
|
// USB Bulk Display Protocol devices (interface)
|
|
bulk_display = 0x13,
|
|
// MCTP over USB protocol endpoint devices (interface)
|
|
mctp = 0x14,
|
|
// I3C devices (interface)
|
|
i3c = 0x3C,
|
|
// Diagnostic devices (both)
|
|
diagnostic = 0xDC,
|
|
// Wireless controllers: Bluetooth adapters (interface)
|
|
wireless_controller = 0xE0,
|
|
// Miscellaneous (both)
|
|
miscellaneous = 0xEF,
|
|
// Application-specific: firmware upgrade, IrDA bridges, test and measurement
|
|
// (interface)
|
|
application_specific = 0xFE,
|
|
// Vendor-specific (both)
|
|
vendor_specific = 0xFF,
|
|
_,
|
|
};
|
|
|
|
/// A human-readable name for a device/interface class code, for logs. Unknown
|
|
/// codes fall through to "class 0xNN".
|
|
pub fn className(class: u8) []const u8 {
|
|
return switch (@as(Class, @enumFromInt(class))) {
|
|
.per_interface => "per-interface",
|
|
.audio => "Audio",
|
|
.communications => "Communications",
|
|
.hid => "HID",
|
|
.physical => "Physical",
|
|
.image => "Image",
|
|
.printer => "Printer",
|
|
.mass_storage => "Mass Storage",
|
|
.hub => "Hub",
|
|
.cdc_data => "CDC Data",
|
|
.smart_card => "Smart Card",
|
|
.content_security => "Content Security",
|
|
.video => "Video",
|
|
.personal_healthcare => "Personal Healthcare",
|
|
.audio_video => "Audio/Video",
|
|
.billboard => "Billboard",
|
|
.type_c_bridge => "Type-C Bridge",
|
|
.bulk_display => "Bulk Display",
|
|
.mctp => "MCTP",
|
|
.i3c => "I3C",
|
|
.diagnostic => "Diagnostic",
|
|
.wireless_controller => "Wireless Controller",
|
|
.miscellaneous => "Miscellaneous",
|
|
.application_specific => "Application-specific",
|
|
.vendor_specific => "Vendor-specific",
|
|
_ => "Unknown",
|
|
};
|
|
}
|
|
|
|
/// The USB speed class (as xHCI reports it in PORTSC/slot contexts) named.
|
|
pub fn speedName(speed: u32) []const u8 {
|
|
return switch (speed) {
|
|
1 => "Full-speed",
|
|
2 => "Low-speed",
|
|
3 => "High-speed",
|
|
4 => "SuperSpeed",
|
|
5 => "SuperSpeedPlus",
|
|
else => "unknown-speed",
|
|
};
|
|
}
|
|
|
|
/// A USB3 Port Link State (xHCI PORTSC PLS field) named.
|
|
pub fn linkStateName(pls: u32) []const u8 {
|
|
return switch (pls) {
|
|
0 => "U0",
|
|
1 => "U1",
|
|
2 => "U2",
|
|
3 => "U3-suspended",
|
|
4 => "Disabled",
|
|
5 => "RxDetect",
|
|
6 => "Inactive",
|
|
7 => "Polling",
|
|
8 => "Recovery",
|
|
9 => "HotReset",
|
|
10 => "Compliance",
|
|
11 => "Test",
|
|
15 => "Resume",
|
|
else => "reserved",
|
|
};
|
|
}
|
|
|
|
/// The most useful readable name for an interface's (class, subclass, protocol)
|
|
/// triple, decoding the well-known combinations recognizable in a log — e.g.
|
|
/// "HID boot keyboard", "Mass Storage SCSI Bulk-Only", "Bluetooth". Falls back
|
|
/// to the class name (and then "Unknown") for codes without a spelled-out combo.
|
|
pub fn interfaceName(class: u8, subclass: u8, protocol: u8) []const u8 {
|
|
return switch (@as(Class, @enumFromInt(class))) {
|
|
.hid => if (subclass == @intFromEnum(hid.SubClass.boot)) switch (@as(hid.Protocol, @enumFromInt(protocol))) {
|
|
.keyboard => "HID boot keyboard",
|
|
.mouse => "HID boot mouse",
|
|
else => "HID boot device",
|
|
} else "HID",
|
|
.mass_storage => switch (@as(mass_storage.Protocol, @enumFromInt(protocol))) {
|
|
.bulk_only => "Mass Storage (Bulk-Only)",
|
|
.uas => "Mass Storage (UAS)",
|
|
else => "Mass Storage",
|
|
},
|
|
.hub => switch (@as(hub.Protocol, @enumFromInt(protocol))) {
|
|
.super_speed => "Hub (SuperSpeed)",
|
|
.hi_speed_multi_tt => "Hub (Hi-Speed multi-TT)",
|
|
.hi_speed_single_tt => "Hub (Hi-Speed single-TT)",
|
|
else => "Hub",
|
|
},
|
|
.wireless_controller => if (subclass == @intFromEnum(wireless_controller.SubClass.radio_frequency))
|
|
wireless_controller.protocolName(protocol)
|
|
else
|
|
"Wireless Controller",
|
|
.communications => communications.subclassName(subclass),
|
|
.application_specific => application_specific.subclassName(subclass),
|
|
.miscellaneous => "Miscellaneous",
|
|
else => className(class),
|
|
};
|
|
}
|
|
|
|
// Subclass and protocol codes qualified by Class.hub. Hubs have no subclass codes; the
|
|
// protocol distinguishes the hub's transaction-translator arrangement.
|
|
pub const hub = struct {
|
|
pub const Protocol = enum(u8) {
|
|
// Full-speed hub
|
|
full_speed = 0x00,
|
|
// Hi-speed hub with a single transaction translator
|
|
hi_speed_single_tt = 0x01,
|
|
// Hi-speed hub with multiple transaction translators
|
|
hi_speed_multi_tt = 0x02,
|
|
// SuperSpeed hub (USB 3)
|
|
super_speed = 0x03,
|
|
_,
|
|
};
|
|
|
|
pub fn protocolName(protocol: u8) []const u8 {
|
|
return switch (@as(Protocol, @enumFromInt(protocol))) {
|
|
.full_speed => "full-speed",
|
|
.hi_speed_single_tt => "Hi-Speed single-TT",
|
|
.hi_speed_multi_tt => "Hi-Speed multi-TT",
|
|
.super_speed => "SuperSpeed",
|
|
_ => "unknown",
|
|
};
|
|
}
|
|
};
|
|
|
|
// Subclass and protocol codes qualified by Class.hid.
|
|
pub const hid = struct {
|
|
pub const SubClass = enum(u8) {
|
|
// No subclass
|
|
none = 0x00,
|
|
// Boot interface: the device also supports the simplified boot protocol, usable by
|
|
// firmware before a full HID report-descriptor parser is available
|
|
boot = 0x01,
|
|
_,
|
|
};
|
|
|
|
// Only meaningful when the subclass is boot
|
|
pub const Protocol = enum(u8) {
|
|
none = 0x00,
|
|
keyboard = 0x01,
|
|
mouse = 0x02,
|
|
_,
|
|
};
|
|
|
|
pub fn subclassName(subclass: u8) []const u8 {
|
|
return switch (@as(SubClass, @enumFromInt(subclass))) {
|
|
.none => "none",
|
|
.boot => "boot",
|
|
_ => "unknown",
|
|
};
|
|
}
|
|
|
|
pub fn protocolName(protocol: u8) []const u8 {
|
|
return switch (@as(Protocol, @enumFromInt(protocol))) {
|
|
.none => "none",
|
|
.keyboard => "keyboard",
|
|
.mouse => "mouse",
|
|
_ => "unknown",
|
|
};
|
|
}
|
|
};
|
|
|
|
// Subclass and protocol codes qualified by Class.mass_storage. The subclass identifies the
|
|
// command set the device understands; the protocol identifies the transport used to carry
|
|
// commands, data, and status over the bus.
|
|
pub const mass_storage = struct {
|
|
pub const SubClass = enum(u8) {
|
|
// SCSI command set not reported; de facto, treat as scsi
|
|
not_reported = 0x00,
|
|
// Reduced Block Commands: typically flash devices
|
|
rbc = 0x01,
|
|
// MMC-5 (ATAPI): CD and DVD drives
|
|
atapi = 0x02,
|
|
// QIC-157 tape drives (obsolete)
|
|
qic_157 = 0x03,
|
|
// UFI: floppy disk drives
|
|
ufi = 0x04,
|
|
// SFF-8070i (obsolete)
|
|
sff_8070i = 0x05,
|
|
// Transparent SCSI command set: the common case for flash drives and disks
|
|
scsi = 0x06,
|
|
// LSD FS: negotiated access to large storage devices
|
|
lsd_fs = 0x07,
|
|
// IEEE 1667
|
|
ieee_1667 = 0x08,
|
|
// Vendor-specific
|
|
vendor_specific = 0xFF,
|
|
_,
|
|
};
|
|
|
|
pub const Protocol = enum(u8) {
|
|
// Control/Bulk/Interrupt with command completion interrupt
|
|
cbi_completion_interrupt = 0x00,
|
|
// Control/Bulk/Interrupt without command completion interrupt
|
|
cbi = 0x01,
|
|
// Bulk-only transport: the common case for flash drives and disks
|
|
bulk_only = 0x50,
|
|
// USB attached SCSI
|
|
uas = 0x62,
|
|
// Vendor-specific
|
|
vendor_specific = 0xFF,
|
|
_,
|
|
};
|
|
|
|
pub fn subclassName(subclass: u8) []const u8 {
|
|
return switch (@as(SubClass, @enumFromInt(subclass))) {
|
|
.not_reported => "SCSI (not reported)",
|
|
.rbc => "RBC",
|
|
.atapi => "ATAPI",
|
|
.qic_157 => "QIC-157",
|
|
.ufi => "UFI",
|
|
.sff_8070i => "SFF-8070i",
|
|
.scsi => "SCSI",
|
|
.lsd_fs => "LSD FS",
|
|
.ieee_1667 => "IEEE 1667",
|
|
.vendor_specific => "vendor-specific",
|
|
_ => "unknown",
|
|
};
|
|
}
|
|
|
|
pub fn protocolName(protocol: u8) []const u8 {
|
|
return switch (@as(Protocol, @enumFromInt(protocol))) {
|
|
.cbi_completion_interrupt => "CBI",
|
|
.cbi => "CBI (no completion IRQ)",
|
|
.bulk_only => "Bulk-Only",
|
|
.uas => "UAS",
|
|
.vendor_specific => "vendor-specific",
|
|
_ => "unknown",
|
|
};
|
|
}
|
|
};
|
|
|
|
// Subclass and protocol codes qualified by Class.communications (CDC). The protocol codes
|
|
// are model-specific; the useful invariant is the subclass, which selects the control model
|
|
// the interface implements.
|
|
pub const communications = struct {
|
|
pub const SubClass = enum(u8) {
|
|
// Direct line control model
|
|
direct_line = 0x01,
|
|
// Abstract control model: USB modems and serial adapters
|
|
abstract_control = 0x02,
|
|
// Telephone control model
|
|
telephone = 0x03,
|
|
// Multi-channel control model
|
|
multi_channel = 0x04,
|
|
// CAPI control model
|
|
capi = 0x05,
|
|
// Ethernet networking control model
|
|
ethernet = 0x06,
|
|
// ATM networking control model
|
|
atm = 0x07,
|
|
// Wireless handset control model
|
|
wireless_handset = 0x08,
|
|
// Device management
|
|
device_management = 0x09,
|
|
// Mobile direct line model
|
|
mobile_direct_line = 0x0A,
|
|
// OBEX
|
|
obex = 0x0B,
|
|
// Ethernet emulation model
|
|
ethernet_emulation = 0x0C,
|
|
// Network control model
|
|
network_control = 0x0D,
|
|
_,
|
|
};
|
|
|
|
pub fn subclassName(subclass: u8) []const u8 {
|
|
return switch (@as(SubClass, @enumFromInt(subclass))) {
|
|
.direct_line => "Direct Line",
|
|
.abstract_control => "Abstract Control (modem/serial)",
|
|
.telephone => "Telephone",
|
|
.multi_channel => "Multi-Channel",
|
|
.capi => "CAPI",
|
|
.ethernet => "Ethernet",
|
|
.atm => "ATM",
|
|
.wireless_handset => "Wireless Handset",
|
|
.device_management => "Device Management",
|
|
.mobile_direct_line => "Mobile Direct Line",
|
|
.obex => "OBEX",
|
|
.ethernet_emulation => "Ethernet Emulation",
|
|
.network_control => "Network Control",
|
|
_ => "unknown",
|
|
};
|
|
}
|
|
};
|
|
|
|
// Subclass and protocol codes qualified by Class.wireless_controller.
|
|
pub const wireless_controller = struct {
|
|
pub const SubClass = enum(u8) {
|
|
// Radio frequency controllers
|
|
radio_frequency = 0x01,
|
|
_,
|
|
};
|
|
|
|
// Only meaningful when the subclass is radio_frequency
|
|
pub const Protocol = enum(u8) {
|
|
// Bluetooth programming interface
|
|
bluetooth = 0x01,
|
|
// Ultra-wideband radio control
|
|
ultra_wideband = 0x02,
|
|
// Remote NDIS
|
|
remote_ndis = 0x03,
|
|
// Bluetooth AMP controller
|
|
bluetooth_amp = 0x04,
|
|
_,
|
|
};
|
|
|
|
pub fn subclassName(subclass: u8) []const u8 {
|
|
return switch (@as(SubClass, @enumFromInt(subclass))) {
|
|
.radio_frequency => "RF",
|
|
_ => "unknown",
|
|
};
|
|
}
|
|
|
|
pub fn protocolName(protocol: u8) []const u8 {
|
|
return switch (@as(Protocol, @enumFromInt(protocol))) {
|
|
.bluetooth => "Bluetooth",
|
|
.ultra_wideband => "Ultra-Wideband",
|
|
.remote_ndis => "Remote NDIS",
|
|
.bluetooth_amp => "Bluetooth AMP",
|
|
_ => "unknown",
|
|
};
|
|
}
|
|
};
|
|
|
|
// Subclass and protocol codes qualified by Class.miscellaneous.
|
|
pub const miscellaneous = struct {
|
|
pub const SubClass = enum(u8) {
|
|
// Common class
|
|
common = 0x02,
|
|
_,
|
|
};
|
|
|
|
// Only meaningful when the subclass is common
|
|
pub const Protocol = enum(u8) {
|
|
// Interface association descriptor: at the device level, announces that the
|
|
// configuration groups interfaces into functions with IADs
|
|
interface_association = 0x01,
|
|
_,
|
|
};
|
|
|
|
pub fn subclassName(subclass: u8) []const u8 {
|
|
return switch (@as(SubClass, @enumFromInt(subclass))) {
|
|
.common => "common",
|
|
_ => "unknown",
|
|
};
|
|
}
|
|
|
|
pub fn protocolName(protocol: u8) []const u8 {
|
|
return switch (@as(Protocol, @enumFromInt(protocol))) {
|
|
.interface_association => "Interface Association",
|
|
_ => "unknown",
|
|
};
|
|
}
|
|
};
|
|
|
|
// Subclass and protocol codes qualified by Class.application_specific.
|
|
pub const application_specific = struct {
|
|
pub const SubClass = enum(u8) {
|
|
// Device firmware upgrade
|
|
firmware_upgrade = 0x01,
|
|
// IrDA bridge
|
|
irda_bridge = 0x02,
|
|
// Test and measurement
|
|
test_and_measurement = 0x03,
|
|
_,
|
|
};
|
|
|
|
pub fn subclassName(subclass: u8) []const u8 {
|
|
return switch (@as(SubClass, @enumFromInt(subclass))) {
|
|
.firmware_upgrade => "Device Firmware Upgrade",
|
|
.irda_bridge => "IrDA Bridge",
|
|
.test_and_measurement => "Test & Measurement",
|
|
_ => "unknown",
|
|
};
|
|
}
|
|
};
|
|
|
|
/// Pack a (class, subclass, protocol) triple into one 0xCCSSPP value — the
|
|
/// bus-native identity a USB bus driver reports in `ChildAdded.identity` and the
|
|
/// device manager matches on (the USB analog of a packed PCI class code). Mirrors
|
|
/// `pci_class.ClassCode.pack`, so both sides build/decode the identical u64.
|
|
pub fn packTriple(class: u8, subclass: u8, protocol: u8) u64 {
|
|
return (@as(u64, class) << 16) | (@as(u64, subclass) << 8) | protocol;
|
|
}
|
|
|
|
/// The inverse of `packTriple`.
|
|
pub fn unpackTriple(triple: u64) struct { class: u8, subclass: u8, protocol: u8 } {
|
|
return .{
|
|
.class = @truncate(triple >> 16),
|
|
.subclass = @truncate(triple >> 8),
|
|
.protocol = @truncate(triple),
|
|
};
|
|
}
|
|
|
|
test "class codes match the USB-IF assignments" {
|
|
const std = @import("std");
|
|
const expectEqual = std.testing.expectEqual;
|
|
|
|
try expectEqual(0x03, @intFromEnum(Class.hid));
|
|
try expectEqual(0x09, @intFromEnum(Class.hub));
|
|
try expectEqual(0xFF, @intFromEnum(Class.vendor_specific));
|
|
|
|
// A typical flash drive: mass storage, transparent SCSI, bulk-only transport.
|
|
try expectEqual(0x06, @intFromEnum(mass_storage.SubClass.scsi));
|
|
try expectEqual(0x50, @intFromEnum(mass_storage.Protocol.bulk_only));
|
|
|
|
// A boot keyboard: HID, boot subclass, keyboard protocol.
|
|
try expectEqual(0x01, @intFromEnum(hid.SubClass.boot));
|
|
try expectEqual(0x01, @intFromEnum(hid.Protocol.keyboard));
|
|
|
|
// Class codes are non-exhaustive: unlisted values pass through undamaged.
|
|
const unknown: Class = @enumFromInt(0x42);
|
|
try expectEqual(0x42, @intFromEnum(unknown));
|
|
|
|
_ = hub.Protocol.hi_speed_multi_tt;
|
|
_ = communications.SubClass.abstract_control;
|
|
_ = wireless_controller.Protocol.bluetooth;
|
|
_ = miscellaneous.Protocol.interface_association;
|
|
_ = application_specific.SubClass.firmware_upgrade;
|
|
}
|
|
|
|
test "readable names decode the well-known triples" {
|
|
const std = @import("std");
|
|
const eql = std.testing.expectEqualStrings;
|
|
|
|
try eql("Hub", className(0x09));
|
|
try eql("Unknown", className(0x42));
|
|
|
|
// interfaceName decodes the combos we log.
|
|
try eql("HID boot keyboard", interfaceName(0x03, 0x01, 0x01));
|
|
try eql("HID boot mouse", interfaceName(0x03, 0x01, 0x02));
|
|
try eql("Mass Storage (Bulk-Only)", interfaceName(0x08, 0x06, 0x50));
|
|
try eql("Hub (SuperSpeed)", interfaceName(0x09, 0x00, 0x03));
|
|
try eql("Bluetooth", interfaceName(0xE0, 0x01, 0x01));
|
|
|
|
// The per-enum name functions.
|
|
try eql("Bulk-Only", mass_storage.protocolName(0x50));
|
|
try eql("SCSI", mass_storage.subclassName(0x06));
|
|
try eql("Bluetooth", wireless_controller.protocolName(0x01));
|
|
try eql("SuperSpeed", hub.protocolName(0x03));
|
|
try eql("keyboard", hid.protocolName(0x01));
|
|
try eql("SuperSpeed", speedName(4));
|
|
try eql("Polling", linkStateName(7));
|
|
}
|
|
|
|
test "packTriple / unpackTriple round-trip the identity a bus driver reports" {
|
|
const std = @import("std");
|
|
const expectEqual = std.testing.expectEqual;
|
|
|
|
// A boot keyboard interface: HID / boot / keyboard.
|
|
const keyboard = packTriple(
|
|
@intFromEnum(Class.hid),
|
|
@intFromEnum(hid.SubClass.boot),
|
|
@intFromEnum(hid.Protocol.keyboard),
|
|
);
|
|
try expectEqual(@as(u64, 0x03_01_01), keyboard);
|
|
|
|
// A flash drive interface: mass storage / SCSI / bulk-only.
|
|
const storage = packTriple(
|
|
@intFromEnum(Class.mass_storage),
|
|
@intFromEnum(mass_storage.SubClass.scsi),
|
|
@intFromEnum(mass_storage.Protocol.bulk_only),
|
|
);
|
|
try expectEqual(@as(u64, 0x08_06_50), storage);
|
|
|
|
const parts = unpackTriple(storage);
|
|
try expectEqual(@as(u8, 0x08), parts.class);
|
|
try expectEqual(@as(u8, 0x06), parts.subclass);
|
|
try expectEqual(@as(u8, 0x50), parts.protocol);
|
|
}
|