///|
pub(all) enum ReportKind {
Input
Output
Feature
} derive(Debug, Eq)
///|
pub extend ReportKind with @debug.Debug::{to_repr}
///|
pub extend ReportKind with Eq::{equal, not_equal}
///|
pub(all) struct Usage {
page : Int
id : Int
} derive(Debug, Eq)
///|
pub extend Usage with @debug.Debug::{to_repr}
///|
pub extend Usage with Eq::{equal, not_equal}
///|
/// Inclusive Usage IDs on one page; a single Usage has min=max.
pub(all) struct UsageSpan {
page : Int
min : Int
max : Int
} derive(Debug, Eq)
///|
pub extend UsageSpan with @debug.Debug::{to_repr}
///|
pub extend UsageSpan with Eq::{equal, not_equal}
///|
/// Inclusive unsigned short-item indices for String or Designator metadata.
pub(all) struct IndexSpan {
min : Int64
max : Int64
} derive(Debug, Eq)
///|
pub extend IndexSpan with @debug.Debug::{to_repr}
///|
pub extend IndexSpan with Eq::{equal, not_equal}
///|
fn valid_index_spans(spans : Array[IndexSpan]) -> Bool {
spans.length() <= 1024 &&
spans.all(s => s.min >= 0L && s.min <= s.max && s.max <= 4294967295L)
}
///|
fn valid_alternate_usages(
primary : Array[UsageSpan],
alternatives : Array[Array[UsageSpan]],
) -> Bool {
alternatives.length() <= 1024 &&
alternatives.all(valid_usage_spans) &&
alternatives.fold(init=primary.length(), (n, group) => n + group.length()) <=
1024
}
///|
fn valid_usage_spans(spans : Array[UsageSpan]) -> Bool {
spans.length() <= 1024 &&
spans.all(s => {
s.page >= 0 &&
s.page <= 65535 &&
s.min >= 0 &&
s.min <= s.max &&
s.max <= 65535
})
}
///|
fn usage_count(spans : Array[UsageSpan]) -> Int {
spans.fold(init=0, (n, s) => n + s.max - s.min + 1)
}
///|
fn usage_at(
spans : Array[UsageSpan],
index : Int,
repeat_last : Bool,
) -> Usage? {
if index < 0 {
return None
}
let mut remaining = index
for span in spans {
if span.min < 0 || span.max > 65535 || span.min > span.max {
return None
}
let count = span.max - span.min + 1
if remaining < count {
return Some({ page: span.page, id: span.min + remaining, })
}
remaining = remaining - count
}
if repeat_last {
match spans.last() {
Some(span) => Some({ page: span.page, id: span.max, })
None => None
}
} else {
None
}
}
///|
/// Variable indices repeat the last Usage; Array selector indices never repeat.
/// For Arrays, pass value-logical_min after checking the logical range.
pub fn field_usage(field : Field, index : Int) -> Usage? {
if (field.flags & 1) != 0 {
return None
}
usage_at(field.usage_spans, index, (field.flags & 2) != 0)
}
///|
/// Collections are stored in descriptor order. Parent indices form a tree.
pub(all) struct Collection {
descriptor_offset : Int
end_offset : Int
collection_type : Int
usage : Usage?
string_spans : Array[IndexSpan]
designator_spans : Array[IndexSpan]
alternate_usages : Array[Array[UsageSpan]]
parent_index : Int?
} derive(Debug, Eq)
///|
pub extend Collection with @debug.Debug::{to_repr}
///|
pub extend Collection with Eq::{equal, not_equal}
///|
/// bit_offset excludes an optional Report ID prefix byte. Padding is retained.
pub(all) struct Field {
descriptor_offset : Int
kind : ReportKind
report_id : Int
bit_offset : Int
bit_size : Int
count : Int
flags : Int
logical_min : Int64
logical_max : Int64
physical_min : Int64?
physical_max : Int64?
unit : Int64?
unit_exponent : Int?
collection_index : Int?
usage_spans : Array[UsageSpan]
string_spans : Array[IndexSpan]
designator_spans : Array[IndexSpan]
alternate_usages : Array[Array[UsageSpan]]
} derive(Debug, Eq)
///|
pub extend Field with @debug.Debug::{to_repr}
///|
pub extend Field with Eq::{equal, not_equal}
///|
pub(all) struct Layout {
fields : Array[Field]
collections : Array[Collection]
has_report_ids : Bool
} derive(Debug, Eq)
///|
pub extend Layout with @debug.Debug::{to_repr}
///|
pub extend Layout with Eq::{equal, not_equal}
///|
priv struct GlobalState {
page : Int
logical_min : Int64
logical_max_unsigned : Int64
logical_max_signed : Int64
physical_min : Int64?
physical_max_unsigned : Int64?
physical_max_signed : Int64?
unit : Int64?
unit_exponent : Int?
size : Int
count : Int
report_id : Int
}
///|
fn usage_value(item : Item, page : Int) -> Usage {
let value = item_unsigned(item)
if item.data.length() == 4 {
{ page: (value >> 16).to_int(), id: (value & 65535L).to_int(), }
} else {
{ page, id: value.to_int(), }
}
}
///|
/// Compiles the supported HID subset. Unknown semantics are rejected explicitly.
/// Limits: integer Array values are 1..32 bits; reports are at most 65536 bits.
pub fn compile_descriptor(bytes : Bytes) -> Result[Layout, Diagnostic] {
let items = match parse_items(bytes) {
Ok(items) => items
Err(error) => return Err(error)
}
let fields : Array[Field] = []
let collections : Array[Collection] = []
let collection_stack : Array[Int] = []
let offsets : Map[Int, Int] = Map([])
let mut global : GlobalState = {
page: 0,
logical_min: 0,
logical_max_unsigned: 0,
logical_max_signed: 0,
physical_min: None,
physical_max_unsigned: None,
physical_max_signed: None,
unit: None,
unit_exponent: None,
size: 0,
count: 0,
report_id: 0,
}
let locals : Array[UsageSpan] = []
let mut range_min : Usage? = None
let alternate_usages : Array[Array[UsageSpan]] = []
let mut delimiter_open = false
let mut delimiter_branch = 0
let strings : Array[IndexSpan] = []
let designators : Array[IndexSpan] = []
let mut string_min : Int64? = None
let mut designator_min : Int64? = None
let mut has_report_ids = false
let stack : Array[GlobalState] = []
for item in items {
if item.is_long || item.type_code == 3 {
return Err(
diagnostic(
item.offset,
"unsupported_item",
"Long and reserved items are not compiled",
),
)
}
let value = item_unsigned(item)
if item.type_code == 1 {
match item.tag {
0 => {
if value > 65535 {
return Err(
diagnostic(
item.offset,
"usage_page",
"Usage Page exceeds 16 bits",
),
)
}
global = { ..global, page: value.to_int(), }
}
1 => global = { ..global, logical_min: item_signed(item), }
2 =>
global = {
..global,
logical_max_unsigned: value,
logical_max_signed: item_signed(item),
}
3 => global = { ..global, physical_min: Some(item_signed(item)), }
4 =>
global = {
..global,
physical_max_unsigned: Some(value),
physical_max_signed: Some(item_signed(item)),
}
5 => {
let exponent = if value <= 15 {
signed_nibble(value.to_int())
} else {
let signed = item_signed(item)
if signed < -8 || signed > 7 {
return Err(
diagnostic(
item.offset,
"unit_exponent",
"Unit Exponent must encode -8..7",
),
)
}
signed.to_int()
}
global = { ..global, unit_exponent: Some(exponent), }
}
6 => global = { ..global, unit: Some(value), }
7 => {
if value > 65536 {
return Err(
diagnostic(
item.offset,
"report_size",
"Report Size must be 0..65536; zero cannot define a report",
),
)
}
global = { ..global, size: value.to_int(), }
}
8 => {
if item.data.length() != 1 || value < 1 || value > 255 {
return Err(
diagnostic(
item.offset,
"report_id",
"Report ID must be a single byte in 1..255",
),
)
}
has_report_ids = true
global = { ..global, report_id: value.to_int(), }
}
9 => {
if value < 1 || value > 65536 {
return Err(
diagnostic(
item.offset,
"report_count",
"Report Count must be 1..65536",
),
)
}
global = { ..global, count: value.to_int(), }
}
10 => {
if !item.data.is_empty() || stack.length() >= 64 {
return Err(
diagnostic(
item.offset,
"global_stack",
"Push payload or global stack depth exceeds 64",
),
)
}
stack.push(global)
}
11 => {
if !item.data.is_empty() {
return Err(
diagnostic(
item.offset,
"global_stack",
"Pop cannot have a payload",
),
)
}
global = match stack.pop() {
Some(saved) => saved
None =>
return Err(
diagnostic(
item.offset,
"global_stack",
"Global stack underflow",
),
)
}
}
_ =>
return Err(
diagnostic(
item.offset,
"unsupported_global",
"Global item is not supported",
),
)
}
} else if item.type_code == 2 {
let usage = usage_value(item, global.page)
let usage_locals = if delimiter_branch > 1 {
alternate_usages[delimiter_branch - 2]
} else {
locals
}
match item.tag {
0 =>
usage_locals.push({ page: usage.page, min: usage.id, max: usage.id, })
1 => {
if range_min is Some(_) {
return Err(
diagnostic(item.offset, "usage_range", "Nested Usage Minimum"),
)
}
range_min = Some(usage)
}
2 => {
let minimum = match range_min {
Some(minimum) => minimum
None =>
return Err(
diagnostic(
item.offset,
"usage_range",
"Usage Maximum has no minimum",
),
)
}
if minimum.page != usage.page || minimum.id > usage.id {
return Err(
diagnostic(item.offset, "usage_range", "Invalid usage range"),
)
}
usage_locals.push({
page: usage.page,
min: minimum.id,
max: usage.id,
})
range_min = None
}
3 => designators.push({ min: value, max: value, })
4 => {
if designator_min is Some(_) {
return Err(
diagnostic(
item.offset,
"local_range",
"Nested Designator Minimum",
),
)
}
designator_min = Some(value)
}
5 => {
let minimum = match designator_min {
Some(n) => n
None =>
return Err(
diagnostic(
item.offset,
"local_range",
"Designator Maximum has no minimum",
),
)
}
if minimum > value {
return Err(
diagnostic(
item.offset,
"local_range",
"Reversed Designator range",
),
)
}
designators.push({ min: minimum, max: value, })
designator_min = None
}
7 => strings.push({ min: value, max: value, })
8 => {
if string_min is Some(_) {
return Err(
diagnostic(item.offset, "local_range", "Nested String Minimum"),
)
}
string_min = Some(value)
}
9 => {
let minimum = match string_min {
Some(n) => n
None =>
return Err(
diagnostic(
item.offset,
"local_range",
"String Maximum has no minimum",
),
)
}
if minimum > value {
return Err(
diagnostic(item.offset, "local_range", "Reversed String range"),
)
}
strings.push({ min: minimum, max: value, })
string_min = None
}
10 => {
if range_min is Some(_) {
return Err(
diagnostic(
item.offset,
"usage_range",
"Usage range crosses a Delimiter",
),
)
}
if value != 0L {
if delimiter_open || delimiter_branch >= 1024 {
return Err(
diagnostic(
item.offset,
"delimiter",
"Nested Delimiter or more than 1024 sets",
),
)
}
delimiter_open = true
delimiter_branch = delimiter_branch + 1
if delimiter_branch > 1 {
alternate_usages.push([])
}
} else {
if !delimiter_open {
return Err(
diagnostic(
item.offset,
"delimiter",
"Delimiter close has no opening",
),
)
}
delimiter_open = false
}
}
_ =>
return Err(
diagnostic(
item.offset,
"unsupported_local",
"Local item semantics are not supported",
),
)
}
if strings.length() > 1024 || designators.length() > 1024 {
return Err(
diagnostic(
item.offset,
"local_limit",
"More than 1024 String or Designator spans",
),
)
}
if !valid_alternate_usages(locals, alternate_usages) {
return Err(
diagnostic(item.offset, "usage_limit", "More than 1024 Usage spans"),
)
}
} else {
if delimiter_open {
return Err(
diagnostic(
item.offset,
"delimiter",
"Delimiter is not closed before a Main item",
),
)
}
if range_min is Some(_) {
return Err(
diagnostic(item.offset, "usage_range", "Unclosed usage range"),
)
}
if string_min is Some(_) || designator_min is Some(_) {
return Err(
diagnostic(
item.offset,
"local_range",
"Unclosed String or Designator range",
),
)
}
match item.tag {
10 => {
if item.data.length() > 1 ||
collection_stack.length() >= 64 ||
collections.length() >= 4096 {
return Err(
diagnostic(
item.offset,
"collection",
"Invalid collection, depth exceeds 64 or more than 4096 collections",
),
)
}
let index = collections.length()
collections.push({
descriptor_offset: item.offset,
end_offset: -1,
collection_type: value.to_int(),
usage: usage_at(locals, 0, false),
string_spans: strings.copy(),
designator_spans: designators.copy(),
alternate_usages: alternate_usages.map(group => group.copy()),
parent_index: collection_stack.last(),
})
collection_stack.push(index)
}
12 => {
if !item.data.is_empty() || collection_stack.is_empty() {
return Err(
diagnostic(item.offset, "collection", "Unmatched End Collection"),
)
}
match collection_stack.pop() {
Some(index) =>
collections[index] = {
..collections[index],
end_offset: item.offset,
}
None => () // The empty stack is checked above.
}
}
8 | 9 | 11 => {
if (value & 256L) != 0 {
return Err(
diagnostic(
item.offset,
"buffered_bytes",
"Buffered Bytes are not supported",
),
)
}
if value > 255L {
return Err(
diagnostic(
item.offset,
"main_flags",
"Reserved Main flags are not supported",
),
)
}
if global.size == 0 || global.count == 0 {
return Err(
diagnostic(
item.offset,
"missing_dimensions",
"Main item requires Report Size and Count",
),
)
}
let kind = if item.tag == 8 {
Input
} else if item.tag == 9 {
Output
} else {
Feature
}
if fields.length() >= 4096 {
return Err(
diagnostic(
item.offset,
"field_limit",
"Descriptor exceeds 4096 Main report items",
),
)
}
let cursor = item.tag * 256 + global.report_id
let offset = offsets.get_or_default(cursor, 0)
if global.size > (65536 - offset) / global.count {
return Err(
diagnostic(
item.offset,
"report_limit",
"Report exceeds 65536 bits",
),
)
}
let maximum = if global.logical_min < 0 {
global.logical_max_signed
} else {
global.logical_max_unsigned
}
if (value & 3L) == 0 && global.size > 32 {
return Err(
diagnostic(
item.offset,
"report_size",
"Data Array fields require widths 1..32",
),
)
}
if (value & 1L) == 0 {
if maximum < global.logical_min {
return Err(
diagnostic(
item.offset,
"logical_range",
"Logical Maximum is below Logical Minimum",
),
)
}
if global.size <= 32 {
let representable_min = if global.logical_min < 0 {
-(1L << (global.size - 1))
} else {
0L
}
let representable_max = if global.logical_min < 0 {
(1L << (global.size - 1)) - 1
} else {
(1L << global.size) - 1
}
if global.logical_min < representable_min ||
maximum > representable_max {
return Err(
diagnostic(
item.offset,
"logical_range",
"Logical range exceeds Report Size",
),
)
}
}
}
fields.push({
descriptor_offset: item.offset,
kind,
report_id: global.report_id,
bit_offset: offset,
bit_size: global.size,
count: global.count,
flags: value.to_int(),
logical_min: global.logical_min,
logical_max: maximum,
physical_min: global.physical_min,
physical_max: if global.physical_min is Some(minimum) && minimum < 0 {
global.physical_max_signed
} else {
global.physical_max_unsigned
},
unit: global.unit,
unit_exponent: global.unit_exponent,
collection_index: collection_stack.last(),
usage_spans: locals.copy(),
string_spans: strings.copy(),
designator_spans: designators.copy(),
alternate_usages: alternate_usages.map(group => group.copy()),
})
offsets[cursor] = offset + global.size * global.count
}
_ =>
return Err(
diagnostic(item.offset, "unsupported_main", "Unknown Main item"),
)
}
locals.clear() // HID local state applies only to the next Main item.
strings.clear()
designators.clear()
alternate_usages.clear()
delimiter_branch = 0
}
}
if delimiter_open {
return Err(
diagnostic(bytes.length(), "delimiter", "Delimiter is not closed"),
)
}
if !collection_stack.is_empty() {
return Err(
diagnostic(bytes.length(), "collection", "Collection is not closed"),
)
}
if !stack.is_empty() {
return Err(
diagnostic(
bytes.length(),
"global_stack",
"Global Push is not balanced by Pop",
),
)
}
if range_min is Some(_) ||
!locals.is_empty() ||
string_min is Some(_) ||
designator_min is Some(_) ||
!strings.is_empty() ||
!designators.is_empty() ||
delimiter_branch > 0 {
return Err(
diagnostic(
bytes.length(),
"dangling_local",
"Local state has no following Main item",
),
)
}
if has_report_ids && fields.any(field => field.report_id == 0) {
return Err(
diagnostic(
0, "mixed_report_ids", "Unnumbered fields cannot be mixed with numbered reports",
),
)
}
if fields.is_empty() {
Err(diagnostic(0, "empty_layout", "Descriptor contains no report fields"))
} else {
Ok({ fields, collections, has_report_ids, })
}
}