Source code for usb_protocol.types.descriptor

# -*- coding: utf-8 -*-
#
# This file is part of usb-protocol.
#
""" Type elements for defining USB descriptors. """

import construct


[docs] class DescriptorFormat(construct.Struct): """ Creates a Construct structure for a USB descriptor, and a corresponding version that supports parsing incomplete binary as `DescriptorType.Partial`, e.g. `DeviceDescriptor.Partial`. """ def __init__(self, *subcons, _create_partial=True, **subconskw): if _create_partial: self.Partial = self._create_partial(*subcons, **subconskw) # pylint: disable=invalid-name super().__init__(*subcons, **subconskw) @classmethod def _get_subcon_field_type(cls, subcon): """ Gets the actual field type for a Subconstruct behind arbitrary levels of `Renamed`s.""" # DescriptorFields are usually `<Renamed <Renamed <FormatField>>>`. # The type behind the `Renamed`s is the one we're interested in, so let's recursively examine # the child Subconstruct until we get to it. if not isinstance(subcon, construct.Renamed): return subcon else: return cls._get_subcon_field_type(subcon.subcon) @classmethod def _create_partial(cls, *subcons, **subconskw): """ Creates a version of the descriptor format for parsing incomplete binary data as a descriptor. This essentially wraps every field after bLength and bDescriptorType in a `construct.Optional`. """ def _apply_optional(subcon): subcon_type = cls._get_subcon_field_type(subcon) # # If it's already Optional then we don't need to apply it again. # if isinstance(subcon_type, construct.Select): # construct uses a weird singleton to define Pass. `construct.core.Pass` would normally be # the type's name, but then they create a singleton of that same name, replacing that name and # making the type technically unnamable and only accessable via `type()`. if isinstance(subcon_type.subcons[1], type(construct.Pass)): return subcon return (subcon.name / construct.Optional(subcon_type)) * subcon.docs # First store the Subconstructs we don't want to modify: bLength and bDescriptorType, # as these are never optional. new_subcons = list(subcons[0:2]) # Then apply Optional to all of the rest of the Subconstructs. new_subcons.extend([_apply_optional(subcon) for subcon in subcons[2:]]) return DescriptorFormat(*new_subcons, _create_partial=False, **subconskw) @staticmethod def _to_detail_dictionary(descriptor, use_pretty_names=True): result = {} # Loop over every entry in our descriptor context, and try to get a # fancy name for it. for key, value in descriptor.items(): # Don't include any underscore-prefixed private members. if key.startswith('_'): continue # If there's no definition for the given key in our format, # skip it. if not hasattr(descriptor._format, key): continue # Try to apply any documentation on the given field rather than it's internal name. format_element = getattr(descriptor._format, key) detail_key = format_element.docs if (format_element.docs and use_pretty_names) else key # Finally, add the entry to our dict. result[detail_key] = value return result
[docs] def parse(self, data, **context_keywords): """ Hook on the parent parse() method which attaches a few methods. """ # Use construct to run the parse itself... result = super().parse(bytes(data), **context_keywords) # ... and then bind our static to_detail_dictionary to it. result._format = self result._to_detail_dictionary = self._to_detail_dictionary.__get__(result, type(result)) return result
[docs] class DescriptorNumber(construct.Const): """ Trivial wrapper class that denotes a particular Const as the descriptor number. """ def __init__(self, const): # If our descriptor number is an integer, instead of "raw", # convert it to a byte, first. if not isinstance(const, bytes): const = const.to_bytes(1, byteorder='little') # Grab the inner descriptor number represented by the constant. self.number = int.from_bytes(const, byteorder='little') # And pass this to the core constant class. super().__init__(const) # Finally, add a documentation string for the type. self.docs = "Descriptor type" def _parse(self, stream, context, path): const_bytes = super()._parse(stream, context, path) return const_bytes[0]
[docs] def get_descriptor_number(self): """ Returns this constant's associated descriptor number.""" return self.number
[docs] class BCDFieldAdapter(construct.Adapter): """ Construct adapter that dynamically parses BCD fields. """ def _decode(self, obj, context, path): hex_string = f"{obj:04x}" return float(f"{hex_string[0:2]}.{hex_string[2:]}") def _encode(self, obj, context, path): # Break the object down into its component parts... integer = int(obj) % 100 percent = int(round(obj * 100)) % 100 # ... make sure nothing is lost during conversion... if float(f"{integer:02}.{percent:02}") != obj: raise AssertionError("BCD fields must be in the format XX.YY") # ... and squish them into an integer. return int(f"{integer:02}{percent:02}", 16)
[docs] class DescriptorField(construct.Subconstruct): """ Construct field definition that automatically adds fields of the proper size to Descriptor definitions. """ # # The C++-wonk operator overloading is Construct, not me, I swear. # # FIXME: these are really primitive views of these types; # we should extend these to get implicit parsing wherever possible USB_TYPES = { 'b' : construct.Int8ul, 'bcd' : BCDFieldAdapter(construct.Int16ul), # TODO: Create a BCD parser for this 'i' : construct.Int8ul, 'id' : construct.Int16ul, 'bm' : construct.Int8ul, 'w' : construct.Int16ul, } LENGTH_TYPES = { 1: construct.Int8ul, 2: construct.Int16ul, 3: construct.Int24ul, 4: construct.Int32ul, 8: construct.Int64ul } @staticmethod def _get_prefix(name): """ Returns the lower-case prefix on a USB descriptor name. """ prefix = [] # Silly loop that continues until we find an uppercase letter. # You'd be aghast at how the 'pythonic' answers look. for c in name: # Ignore leading underscores. if c == '_': continue if c.isupper(): break prefix.append(c) return ''.join(prefix) @classmethod def _get_type_for_name(cls, name): """ Returns the type that's appropriate for a given descriptor field name. """ try: return cls.USB_TYPES[cls._get_prefix(name)] except KeyError: raise ValueError("field names must be formatted per the USB standard!") def __init__(self, description="", default=None, *, length=None): self.description = description self.default = default self.length = length def __rtruediv__(self, field_name): # If we have a length, use it to figure out the type. # Otherwise, extract the type from the prefix. (Using a length # is useful for e.g. USB3 bitfields; which can span several bytes.) if self.length is not None: field_type = self.LENGTH_TYPES[self.length] else: field_type = self._get_type_for_name(field_name) if self.default is not None: field_type = construct.Default(field_type, self.default) # Build our subconstruct. Construct makes this look super weird, # but this is actually "we have a field with <field_name> of type <field_type>". # In long form, we'll call it "description". return (field_name / field_type) * self.description
# Convenience type that gets a descriptor's own length. DescriptorLength = \ construct.Rebuild(construct.Int8ul, construct.len_(construct.this)) \ * "Descriptor Length"