diff --git a/setup.py b/setup.py index c3a695d..100e9ea 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from setuptools import setup, find_packages setup( # Vitals - name='usb-protocol', + name='usb_protocol', license='BSD', url='https://github.com/usb-tool/luna', author='Katherine J. Temkin', diff --git a/usb-protocol/types/descriptors/standard.py b/usb-protocol/types/descriptors/standard.py deleted file mode 100644 index 6ace2b9..0000000 --- a/usb-protocol/types/descriptors/standard.py +++ /dev/null @@ -1,163 +0,0 @@ -# -# This file is part of usb-protocol. -# -""" Structures describing standard USB descriptors. """ - -import unittest - -import construct -from construct import this - -from ..descriptor import DescriptorField, DescriptorNumber, DescriptorFormat - -DeviceDescriptor = DescriptorFormat( - "bLength" / DescriptorField("Length"), - "bDescriptorType" / DescriptorNumber(1), - "bcdUSB" / DescriptorField("USB Version"), - "bDeviceClass" / DescriptorField("Class"), - "bDeviceSubclass" / DescriptorField("Subclass"), - "bDeviceProtocol" / DescriptorField("Protocol"), - "bMaxPacketSize0" / DescriptorField("EP0 Max Pkt Size"), - "idVendor" / DescriptorField("Vendor ID"), - "idProduct" / DescriptorField("Product ID"), - "bcdDevice" / DescriptorField("Device Version"), - "iManufacturer" / DescriptorField("Manufacturer Str"), - "iProduct" / DescriptorField("Product Str"), - "iSerialNumber" / DescriptorField("Serial Number"), - "bNumConfigurations" / DescriptorField("Configuration Count"), -) - - -ConfigurationDescriptor = DescriptorFormat( - "bLength" / DescriptorField("Length"), - "bDescriptorType" / DescriptorNumber(2), - "wTotalLength" / DescriptorField("Length including subordinates"), - "bNumInterfaces" / DescriptorField("Interface count"), - "bConfigurationValue" / DescriptorField("Configuration number"), - "iConfiguration" / DescriptorField("Description string"), - "bmAttributes" / DescriptorField("Attributes"), - "bMaxPower" / DescriptorField("Max power consumption"), -) - - -StringDescriptor = DescriptorFormat( - "bLength" / DescriptorField("Length"), - "bDescriptorType" / DescriptorNumber(3), - "bString" / construct.PaddedString(this.bLength - 2, "utf_16_le") -) - - -InterfaceDescriptor = DescriptorFormat( - "bLength" / DescriptorField("Length"), - "bDescriptorType" / DescriptorNumber(4), - "bInterfaceNumber" / DescriptorField("Interface number"), - "bAlternateSetting" / DescriptorField("Alternate setting"), - "bNumEndpoints" / DescriptorField("Endpoints included"), - "bInterfaceClass" / DescriptorField("Class"), - "bInterfaceSubclass" / DescriptorField("Subclass"), - "bInterfaceProtocol" / DescriptorField("Protocol"), - "iInterface" / DescriptorField("String index"), -) - - -EndpointDescriptor = DescriptorFormat( - "bLength" / DescriptorField("Length"), - "bDescriptorType" / DescriptorNumber(5), - "bEndpointAddress" / DescriptorField("Endpoint Address"), - "bmAttributes" / DescriptorField("Attributes"), - "wMaxPacketSize" / DescriptorField("Maximum Packet Size"), - "bInterval" / DescriptorField("Polling interval"), -) - - -DeviceQualifierDescriptor = DescriptorFormat( - "bLength" / DescriptorField("Length"), - "bDescriptorType" / DescriptorNumber(6), - "bcdUSB" / DescriptorField("USB Version"), - "bDeviceClass" / DescriptorField("Class"), - "bDeviceSubclass" / DescriptorField("Subclass"), - "bDeviceProtocol" / DescriptorField("Protocol"), - "bMaxPacketSize0" / DescriptorField("EP0 Max Pkt Size"), - "bNumConfigurations" / DescriptorField("Configuration Count"), - "_bReserved" / construct.Optional(construct.Const(b"\0")) -) - - -class DescriptorParserCases(unittest.TestCase): - - def test_string_descriptor(self): - - string_descriptor = bytes([ - 40, # Length - 3, # Type - ord('G'), 0x00, - ord('r'), 0x00, - ord('e'), 0x00, - ord('a'), 0x00, - ord('t'), 0x00, - ord(' '), 0x00, - ord('S'), 0x00, - ord('c'), 0x00, - ord('o'), 0x00, - ord('t'), 0x00, - ord('t'), 0x00, - ord(' '), 0x00, - ord('G'), 0x00, - ord('a'), 0x00, - ord('d'), 0x00, - ord('g'), 0x00, - ord('e'), 0x00, - ord('t'), 0x00, - ord('s'), 0x00, - ]) - - # Parse the relevant string... - parsed = StringDescriptor.parse(string_descriptor) - - # ... and check the desriptor's fields. - self.assertEqual(parsed.bLength, 40) - self.assertEqual(parsed.bDescriptorType, 3) - self.assertEqual(parsed.bString, "Great Scott Gadgets") - - - def test_device_descriptor(self): - - device_descriptor = [ - 0x12, # Length - 0x01, # Type - 0x00, 0x02, # USB version - 0xFF, # class - 0xFF, # subclass - 0xFF, # protocol - 64, # ep0 max packet size - 0xd0, 0x16, # VID - 0x3b, 0x0f, # PID - 0x00, 0x00, # device rev - 0x01, # manufacturer string - 0x02, # product string - 0x03, # serial number - 0x01 # number of configurations - ] - - # Parse the relevant string... - parsed = DeviceDescriptor.parse(device_descriptor) - - # ... and check the desriptor's fields. - self.assertEqual(parsed.bLength, 18) - self.assertEqual(parsed.bDescriptorType, 1) - self.assertEqual(parsed.bcdUSB, 0x0200) - self.assertEqual(parsed.bDeviceClass, 0xFF) - self.assertEqual(parsed.bDeviceSubclass, 0xFF) - self.assertEqual(parsed.bDeviceProtocol, 0xFF) - self.assertEqual(parsed.bMaxPacketSize0, 64) - self.assertEqual(parsed.idVendor, 0x16d0) - self.assertEqual(parsed.idProduct, 0x0f3b) - self.assertEqual(parsed.bcdDevice, 0x0000) - self.assertEqual(parsed.iManufacturer, 1) - self.assertEqual(parsed.iProduct, 2) - self.assertEqual(parsed.iSerialNumber, 3) - self.assertEqual(parsed.bNumConfigurations, 1) - - -if __name__ == "__main__": - unittest.main() diff --git a/usb-protocol/__init__.py b/usb_protocol/__init__.py similarity index 100% rename from usb-protocol/__init__.py rename to usb_protocol/__init__.py diff --git a/usb_protocol/emitters/__init__.py b/usb_protocol/emitters/__init__.py new file mode 100644 index 0000000..2c27afc --- /dev/null +++ b/usb_protocol/emitters/__init__.py @@ -0,0 +1,94 @@ +# +# This file is part of usb-protocol. +# +""" Helpers for creating easy emitters. """ + +import unittest +import construct + +class ConstructEmitter: + """ Class that creates a simple emitter based on a construct struct. + + For example, if we have a construct format that looks like the following: + MyStruct = struct( + "a" / Int8 + "b" / Int8 + ) + + We could create emit an object like follows: + emitter = ConstructEmitter(MyStruct) + emitter.a = 0xab + emitter.b = 0xcd + my_bytes = emitter.emit() # "\xab\xcd" + """ + + def __init__(self, struct): + """ + Parmeters: + construct_format -- The format for which to create an emitter. + """ + self.__dict__['format'] = struct + self.__dict__['fields'] = {} + + + def _format_contains_field(self, field_name): + """ Returns True iff the given format has a field with the provided name. + + Parameters: + format_object -- The Construct format to work with. This includes e.g. most descriptor types. + field_name -- The field name to query. + """ + return any(f.name == field_name for f in self.format.subcons) + + + def __setattr__(self, name, value): + """ Hook that we used to set our fields. """ + + # If the field starts with a '_', don't handle it, as it's an internal field. + if name.startswith('_'): + super().__setattr__(name, value) + return + + if not self._format_contains_field(name): + raise AttributeError(f"emitter specification contains no field {name}") + + self.fields[name] = value + + + def emit(self): + """ Emits the stream of bytes associated with this object. """ + + try: + return self.format.build(self.fields) + except KeyError as e: + raise KeyError(f"missing necessary field: {e}") + + + +class ConstructEmitterTest(unittest.TestCase): + + def test_simple_emitter(self): + + test_struct = construct.Struct( + "a" / construct.Int8ul, + "b" / construct.Int8ul + ) + + emitter = ConstructEmitter(test_struct) + emitter.a = 0xab + emitter.b = 0xcd + + self.assertEqual(emitter.emit(), b"\xab\xcd") + + +def emitter_for_format(construct_format): + """ Creates a factory method for the relevant construct format. """ + + def _factory(): + return ConstructEmitter(construct_format) + + return _factory + + +if __name__ == "__main__": + unittest.main() diff --git a/usb_protocol/emitters/descriptor.py b/usb_protocol/emitters/descriptor.py new file mode 100644 index 0000000..0d74142 --- /dev/null +++ b/usb_protocol/emitters/descriptor.py @@ -0,0 +1,68 @@ +# +# This file is part of usb-protocol. +# + + +from . import ConstructEmitter +from collections import defaultdict + +class ComplexDescriptorEmitter(ConstructEmitter): + """ Base class for emitting complex descriptors, which contain nested subordinates. """ + + # Base classes should override this. + DESCRIPTOR_FORMAT = None + + def __init__(self): + + # Always create a basic ConstructEmitter from the given format. + super().__init__(self.DESCRIPTOR_FORMAT) + + # Store a list of subordinate descriptors, and a count of + # subordinate descriptor types. + self._subordinates = [] + self._type_counts = {} + + + def add_subordinate_descriptor(self, subordinate): + """ Adds a subordinate descriptor to the relevant descriptor. + + Parameter: + subordinate -- The subordinate descriptor to add; can be an emitter, + or a bytes-like object. + """ + + if hasattr(subordinate, 'emit'): + subordinate = subordinate.emit() + else: + subordinate = bytes(subordinate) + + # The second byte of a given descriptor is always its type number. + # Count this descriptor type... + subordinate_type = subordinate[1] + + try: + self._type_counts[subordinate_type] += 1 + except KeyError: + self._type_counts[subordinate_type] = 1 + + # ... and add the relevant bytes to our list of subordinates. + self._subordinates.append(subordinate) + + + def emit(self, include_subordinates=True): + """ Emit our descriptor. + + Parameters: + include_subordinates -- If true or not provided, any subordinate descriptors will be included. + """ + + result = bytearray() + + # Add our basic descriptor... + result.extend(super().emit()) + + # ... and if descired, add our subordinates... + for sub in self._subordinates: + result.extend(sub) + + return bytes(result) diff --git a/usb-protocol/types/descriptors/__init__.py b/usb_protocol/emitters/descriptors/__init__.py similarity index 100% rename from usb-protocol/types/descriptors/__init__.py rename to usb_protocol/emitters/descriptors/__init__.py diff --git a/usb_protocol/emitters/descriptors/standard.py b/usb_protocol/emitters/descriptors/standard.py new file mode 100644 index 0000000..8ad4b19 --- /dev/null +++ b/usb_protocol/emitters/descriptors/standard.py @@ -0,0 +1,171 @@ +# +# This file is part of usb_protocol. +# +""" Convenience emitters for simple, standard descriptors. """ + +import unittest +import functools + +from contextlib import contextmanager + +from .. import emitter_for_format +from ..descriptor import ComplexDescriptorEmitter + +from ...types.descriptors.standard import \ + DeviceDescriptor, StringDescriptor, EndpointDescriptor, DeviceQualifierDescriptor, \ + ConfigurationDescriptor, InterfaceDescriptor, StandardDescriptorNumbers + + +# Create our basic emitters... +DeviceDescriptorEmitter = emitter_for_format(DeviceDescriptor) +StringDescriptorEmitter = emitter_for_format(StringDescriptor) +EndpointDescriptorEmitter = emitter_for_format(EndpointDescriptor) +DeviceQualifierDescriptor = emitter_for_format(DeviceQualifierDescriptor) + +# ... convenience functions ... +def get_string_descriptor(string): + """ Generates a string descriptor for the relevant string. """ + + emitter = StringDescriptorEmitter() + emitter.bString = string + return emitter.emit() + +# ... and complex emitters. + +class InterfaceDescriptorEmitter(ComplexDescriptorEmitter): + """ Emitter that creates an InterfaceDescriptor. """ + + DESCRIPTOR_FORMAT = InterfaceDescriptor + + @contextmanager + def EndpointDescriptor(self): + """ Context manager that allows addition of a subordinate endpoint descriptor. + + It can be used with a `with` statement; and yields an EndpointDesriptorEmitter + that can be populated: + + with interface.EndpointDescriptor() as d: + d.bEndpointAddress = 0x01 + d.bmAttributes = 0x80 + d.wMaxPacketSize = 64 + d.bInterval = 0 + + This adds the relevant descriptor, automatically. + """ + + descriptor = EndpointDescriptorEmitter() + yield descriptor + + self.add_subordinate_descriptor(descriptor) + + + def emit(self, include_subordinates=True): + + # Count our endpoints, and then call our parent emitter. + self.bNumEndpoints = self._type_counts[StandardDescriptorNumbers.ENDPOINT] + return super().emit(include_subordinates=include_subordinates) + + + +class ConfigurationDescriptorEmitter(ComplexDescriptorEmitter): + """ Emitter that creates a configuration descriptor. """ + + DESCRIPTOR_FORMAT = ConfigurationDescriptor + + @contextmanager + def InterfaceDescriptor(self): + """ Context manager that allows addition of a subordinate interface descriptor. + + It can be used with a `with` statement; and yields an InterfaceDescriptorEmitter + that can be populated: + + with interface.InterfaceDescriptor() as d: + d.bInterfaceNumber = 0x01 + [snip] + + This adds the relevant descriptor, automatically. Note that populating derived + fields such as bNumEndpoints aren't necessary; they'll be populated automatically. + """ + descriptor = InterfaceDescriptorEmitter() + yield descriptor + + self.add_subordinate_descriptor(descriptor) + + + def emit(self, include_subordinates=True): + + # Count our interfaces... + self.bNumInterfaces = self._type_counts[StandardDescriptorNumbers.INTERFACE] + + # ... and figure out our total length. + subordinate_length = sum(len(sub) for sub in self._subordinates) + self.wTotalLength = subordinate_length + self.DESCRIPTOR_FORMAT.sizeof() + + # Finally, emit our whole descriptor. + return super().emit(include_subordinates=include_subordinates) + + +class EmitterTests(unittest.TestCase): + + def test_string_emitter(self): + emitter = StringDescriptorEmitter() + emitter.bString = "Hello" + + self.assertEqual(emitter.emit(), b"\x0C\x03H\0e\0l\0l\0o\0") + + + def test_string_emitter_function(self): + self.assertEqual(get_string_descriptor("Hello"), b"\x0C\x03H\0e\0l\0l\0o\0") + + + def test_configuration_emitter(self): + descriptor = bytes([ + + # config descriptor + 12, # length + 2, # type + 25, 00, # total length + 1, # num interfaces + 1, # configuration number + 0, # config string + 0x80, # attributes + 250, # max power + + # interface descriptor + 9, # length + 4, # type + 0, # number + 0, # alternate + 1, # num endpoints + 0xff, # class + 0xff, # subclass + 0xff, # protocol + 0, # string + + # endpoint descriptor + 7, # length + 5, # type + 0x01, # address + 2, # attributes + 64, 0, # max packet size + 255, # interval + ]) + + + # Create a trivial configuration descriptor... + emitter = ConfigurationDescriptorEmitter() + + with emitter.InterfaceDescriptor() as interface: + interface.bInterfaceNumber = 0 + + with interface.EndpointDescriptor() as endpoint: + endpoint.bEndpointAddress = 1 + + + # ... and validate that it maches our reference descriptor. + binary = emitter.emit() + self.assertEqual(len(binary), len(descriptor)) + + +if __name__ == "__main__": + unittest.main() diff --git a/usb-protocol/types/__init__.py b/usb_protocol/types/__init__.py similarity index 59% rename from usb-protocol/types/__init__.py rename to usb_protocol/types/__init__.py index e5be239..04548c7 100644 --- a/usb-protocol/types/__init__.py +++ b/usb_protocol/types/__init__.py @@ -414,3 +414,228 @@ LANGUAGE_NAMES = { 0xf8ff: "HID (Vendor Defined 3)", 0xfcff: "HID (Vendor Defined 4)", } + + +class LanguageIDs(IntEnum): + AFRIKAANS = 0X0436 + ALBANIAN = 0X041C + ARABIC_SAUDI_ARABIA = 0X0401 + ARABIC_IRAQ = 0X0801 + ARABIC_EGYPT = 0X0C01 + ARABIC_LIBYA = 0X1001 + ARABIC_ALGERIA = 0X1401 + ARABIC_MOROCCO = 0X1801 + ARABIC_TUNISIA = 0X1C01 + ARABIC_OMAN = 0X2001 + ARABIC_YEMEN = 0X2401 + ARABIC_SYRIA = 0X2801 + ARABIC_JORDAN = 0X2C01 + ARABIC_LEBANON = 0X3001 + ARABIC_KUWAIT = 0X3401 + ARABIC_UAE = 0X3801 + ARABIC_BAHRAIN = 0X3C01 + ARABIC_QATAR = 0X4001 + ARMENIAN = 0X042B + ASSAMESE = 0X044D + AZERI_LATIN = 0X042C + AZERI_CYRILLIC = 0X082C + BASQUE = 0X042D + BELARUSSIAN = 0X0423 + BENGALI = 0X0445 + BULGARIAN = 0X0402 + BURMESE = 0X0455 + CATALAN = 0X0403 + CHINESE_TAIWAN = 0X0404 + CHINESE_PRC = 0X0804 + CHINESE_HONG_KONG = 0X0C04 + CHINESE_SINGAPORE = 0X1004 + CHINESE_MACAU_SAR = 0X1404 + CROATIAN = 0X041A + CZECH = 0X0405 + DANISH = 0X0406 + DUTCH_NETHERLANDS = 0X0413 + DUTCH_BELGIUM = 0X0813 + ENGLISH_US = 0X0409 + ENGLISH_UNITED_KINGDOM = 0X0809 + ENGLISH_AUSTRALIAN = 0X0C09 + ENGLISH_CANADIAN = 0X1009 + ENGLISH_NEW_ZEALAND = 0X1409 + ENGLISH_IRELAND = 0X1809 + ENGLISH_SOUTH_AFRICA = 0X1C09 + ENGLISH_JAMAICA = 0X2009 + ENGLISH_CARIBBEAN = 0X2409 + ENGLISH_BELIZE = 0X2809 + ENGLISH_TRINIDAD = 0X2C09 + ENGLISH_ZIMBABWE = 0X3009 + ENGLISH_PHILIPPINES = 0X3409 + ESTONIAN = 0X0425 + FAEROESE = 0X0438 + FARSI = 0X0429 + FINNISH = 0X040B + FRENCH_STANDARD = 0X040C + FRENCH_BELGIAN = 0X080C + FRENCH_CANADIAN = 0X0C0C + FRENCH_SWITZERLAND = 0X100C + FRENCH_LUXEMBOURG = 0X140C + FRENCH_MONACO = 0X180C + GEORGIAN = 0X0437 + GERMAN_STANDARD = 0X0407 + GERMAN_SWITZERLAND = 0X0807 + GERMAN_AUSTRIA = 0X0C07 + GERMAN_LUXEMBOURG = 0X1007 + GERMAN_LIECHTENSTEIN = 0X1407 + GREEK = 0X0408 + GUJARATI = 0X0447 + HEBREW = 0X040D + HINDI = 0X0439 + HUNGARIAN = 0X040E + ICELANDIC = 0X040F + INDONESIAN = 0X0421 + ITALIAN_STANDARD = 0X0410 + ITALIAN_SWITZERLAND = 0X0810 + JAPANESE = 0X0411 + KANNADA = 0X044B + KASHMIRI_INDIA = 0X0860 + KAZAKH = 0X043F + KONKANI = 0X0457 + KOREAN = 0X0412 + KOREAN_JOHAB = 0X0812 + LATVIAN = 0X0426 + LITHUANIAN = 0X0427 + LITHUANIAN_CLASSIC = 0X0827 + MACEDONIAN = 0X042F + MALAY_MALAYSIAN = 0X043E + MALAY_BRUNEI_DARUSSALAM = 0X083E + MALAYALAM = 0X044C + MANIPURI = 0X0458 + MARATHI = 0X044E + NEPALI_INDIA = 0X0861 + NORWEGIAN_BOKMAL = 0X0414 + NORWEGIAN_NYNORSK = 0X0814 + ORIYA = 0X0448 + POLISH = 0X0415 + PORTUGUESE_BRAZIL = 0X0416 + PORTUGUESE_STANDARD = 0X0816 + PUNJABI = 0X0446 + ROMANIAN = 0X0418 + RUSSIAN = 0X0419 + SANSKRIT = 0X044F + SERBIAN_CYRILLIC = 0X0C1A + SERBIAN_LATIN = 0X081A + SINDHI = 0X0459 + SLOVAK = 0X041B + SLOVENIAN = 0X0424 + SPANISH_TRADITIONAL_SORT = 0X040A + SPANISH_MEXICAN = 0X080A + SPANISH_MODERN_SORT = 0X0C0A + SPANISH_GUATEMALA = 0X100A + SPANISH_COSTA_RICA = 0X140A + SPANISH_PANAMA = 0X180A + SPANISH_DOMINICAN_REPUBLIC = 0X1C0A + SPANISH_VENEZUELA = 0X200A + SPANISH_COLOMBIA = 0X240A + SPANISH_PERU = 0X280A + SPANISH_ARGENTINA = 0X2C0A + SPANISH_ECUADOR = 0X300A + SPANISH_CHILE = 0X340A + SPANISH_URUGUAY = 0X380A + SPANISH_PARAGUAY = 0X3C0A + SPANISH_BOLIVIA = 0X400A + SPANISH_EL_SALVADOR = 0X440A + SPANISH_HONDURAS = 0X480A + SPANISH_NICARAGUA = 0X4C0A + SPANISH_PUERTO_RICO = 0X500A + SUTU = 0X0430 + SWAHILI_KENYA = 0X0441 + SWEDISH = 0X041D + SWEDISH_FINLAND = 0X081D + TAMIL = 0X0449 + TATAR_TATARSTAN = 0X0444 + TELUGU = 0X044A + THAI = 0X041E + TURKISH = 0X041F + UKRAINIAN = 0X0422 + URDU_PAKISTAN = 0X0420 + URDU_INDIA = 0X0820 + UZBEK_LATIN = 0X0443 + UZBEK_CYRILLIC = 0X0843 + VIETNAMESE = 0X042A + HID_USAGE_DATA_DESCRIPTOR = 0X04FF + HID_VENDOR_DEFINED_1 = 0XF0FF + HID_VENDOR_DEFINED_2 = 0XF4FF + HID_VENDOR_DEFINED_3 = 0XF8FF + HID_VENDOR_DEFINED_4 = 0XFCFF + + +class DescriptorTypes(IntEnum): + DEVICE = 1 + CONFIGURATION = 2 + STRING = 3 + INTERFACE = 4 + ENDPOINT = 5 + DEVICE_QUALIFIER = 6 + OTHER_SPEED_CONFIGURATION = 7 + INTERFACE_POWER = 8 + HID = 33 + REPORT = 34 + + +class USBSynchronizationType(IntEnum): + NONE = 0x00 + ASYNC = 0x01 + ADAPTIVE = 0x02 + SYNCHRONOUS = 0x03 + + +class USBUsageType(IntEnum): + DATA = 0 + FEEDBACK = 1 + IMPLICIT_FEEDBACK = 2 + + +class USBStandardRequests(IntEnum): + GET_STATUS = 0 + CLEAR_FEATURE = 1 + SET_FEATURE = 3 + SET_ADDRESS = 5 + GET_DESCRIPTOR = 6 + SET_DESCRIPTOR = 7 + GET_CONFIGURATION = 8 + SET_CONFIGURATION = 9 + GET_INTERFACE = 10 + SET_INTERFACE = 11 + SYNCH_FRAME = 12 + + +class USBTransferType(IntEnum): + CONTROL = 0 + ISOCHRONOUS = 1 + BULK = 2 + INTERRUPT = 3 + + +class USBSynchronizationType(IntEnum): + NONE = 0x00 + ASYNC = 0x01 + ADAPTIVE = 0x02 + SYNCHRONOUS = 0x03 + + +class USBUsageType(IntEnum): + DATA = 0 + FEEDBACK = 1 + IMPLICIT_FEEDBACK = 2 + + +class USBStandardRequests(IntEnum): + GET_STATUS = 0 + CLEAR_FEATURE = 1 + SET_FEATURE = 3 + SET_ADDRESS = 5 + GET_DESCRIPTOR = 6 + SET_DESCRIPTOR = 7 + GET_CONFIGURATION = 8 + SET_CONFIGURATION = 9 + GET_INTERFACE = 10 + SET_INTERFACE = 11 + SYNCH_FRAME = 12 diff --git a/usb-protocol/types/descriptor.py b/usb_protocol/types/descriptor.py similarity index 70% rename from usb-protocol/types/descriptor.py rename to usb_protocol/types/descriptor.py index 2661bc1..e3057ca 100644 --- a/usb-protocol/types/descriptor.py +++ b/usb_protocol/types/descriptor.py @@ -3,6 +3,7 @@ # """ Type elements for defining USB descriptors. """ +import unittest import construct class DescriptorFormat(construct.Struct): @@ -76,6 +77,28 @@ class DescriptorNumber(construct.Const): return self.number +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): + + # Ensure the data is parseable. + if (obj * 100) % 1: + raise AssertionError("BCD fields must be in the format XX.YY") + + # Break the object down into its component parts... + integer = int(obj) + percent = int((obj * 100) % 100) + + # ... and squish them into an integer. + return int(f"{integer:02}{percent:02}", 16) + + class DescriptorField(construct.Subconstruct): """ @@ -90,12 +113,12 @@ class DescriptorField(construct.Subconstruct): # FIXME: these are really primitive views of these types; # we should extend these to get implicit parsing wherever possible USB_TYPES = { - 'b' : construct.Optional(construct.Int8ul), - 'bcd' : construct.Optional(construct.Int16ul), # TODO: Create a BCD parser for this - 'i' : construct.Optional(construct.Int8ul), - 'id' : construct.Optional(construct.Int16ul), - 'bm' : construct.Optional(construct.Int8ul), - 'w' : construct.Optional(construct.Int16ul), + '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, } @staticmethod @@ -128,12 +151,26 @@ class DescriptorField(construct.Subconstruct): raise ValueError("field names must be formatted per the USB standard!") - def __init__(self, description=""): + def __init__(self, description="", default=None): self.description = description + self.default = default def __rtruediv__(self, field_name): field_type = self._get_type_for_name(field_name) - # wew does construct make this look weird - return (field_name / field_type) * self.description + # Build our subconstruct. Construct makes this look super weird, + # but this is actually "we have a field with of type ". + # In long form, we'll call it "description". + subconstruct = (field_name / field_type) * self.description + + if self.default is not None: + return construct.Default(subconstruct, self.default) + else: + return subconstruct + + +# Convenience type that gets a descriptor's own length. +DescriptorLength = \ + construct.Rebuild(construct.Int8ul, construct.len_(construct.this)) \ + * "Descriptor Length" diff --git a/usb_protocol/types/descriptors/__init__.py b/usb_protocol/types/descriptors/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/usb_protocol/types/descriptors/standard.py b/usb_protocol/types/descriptors/standard.py new file mode 100644 index 0000000..ea11c27 --- /dev/null +++ b/usb_protocol/types/descriptors/standard.py @@ -0,0 +1,218 @@ +# +# This file is part of usb-protocol. +# +""" Structures describing standard USB descriptors. """ + +import unittest +from enum import IntEnum + +import construct +from construct import this, Default + +from .. import LanguageIDs +from ..descriptor import \ + DescriptorField, DescriptorNumber, DescriptorFormat, \ + BCDFieldAdapter, DescriptorLength + + +class StandardDescriptorNumbers(IntEnum): + """ Numbers of our standard descriptors. """ + + DEVICE = 1 + CONFIGURATION = 2 + STRING = 3 + INTERFACE = 4 + ENDPOINT = 5 + DEVICE_QUALIFIER = 6 + OTHER_SPEED_DESCRIPTOR = 7 + INTERFACE_POWER = 8 + + +DeviceDescriptor = DescriptorFormat( + "bLength" / DescriptorLength, + "bDescriptorType" / DescriptorNumber(StandardDescriptorNumbers.DEVICE), + "bcdUSB" / DescriptorField("USB Version", default=2.0), + "bDeviceClass" / DescriptorField("Class", default=0), + "bDeviceSubclass" / DescriptorField("Subclass", default=0), + "bDeviceProtocol" / DescriptorField("Protocol", default=0), + "bMaxPacketSize0" / DescriptorField("EP0 Max Pkt Size", default=64), + "idVendor" / DescriptorField("Vendor ID"), + "idProduct" / DescriptorField("Product ID"), + "bcdDevice" / DescriptorField("Device Version", default=0), + "iManufacturer" / DescriptorField("Manufacturer Str", default=0), + "iProduct" / DescriptorField("Product Str", default=0), + "iSerialNumber" / DescriptorField("Serial Number", default=0), + "bNumConfigurations" / DescriptorField("Configuration Count"), +) + + +ConfigurationDescriptor = DescriptorFormat( + "bLength" / DescriptorLength, + "bDescriptorType" / DescriptorNumber(StandardDescriptorNumbers.CONFIGURATION), + "wTotalLength" / DescriptorField("Length including subordinates"), + "bNumInterfaces" / DescriptorField("Interface count"), + "bConfigurationValue" / DescriptorField("Configuration number", default=1), + "iConfiguration" / DescriptorField("Description string", default=0), + "bmAttributes" / DescriptorField("Attributes", default=0x80), + "bMaxPower" / DescriptorField("Max power consumption", default=250), +) + +# Field that automatically reflects a string descriptor's length. +StringDescriptorLength = construct.Rebuild(construct.Int8ul, construct.len_(this.bString) * 2 + 2) + +StringDescriptor = DescriptorFormat( + "bLength" / StringDescriptorLength, + "bDescriptorType" / DescriptorNumber(StandardDescriptorNumbers.STRING), + "bString" / construct.GreedyString("utf_16_le") +) + + +StringLanguageDescriptorLength = \ + construct.Rebuild(construct.Int8ul, construct.len_(this.wLANGID) * 2 + 2) + +StringLanguageDescriptor = DescriptorFormat( + "bLength" / StringLanguageDescriptorLength, + "bDescriptorType" / DescriptorNumber(StandardDescriptorNumbers.STRING), + "wLANGID" / construct.GreedyRange(construct.Int16ul) +) + + +InterfaceDescriptor = DescriptorFormat( + "bLength" / construct.Const(9, construct.Int8ul), + "bDescriptorType" / DescriptorNumber(StandardDescriptorNumbers.INTERFACE), + "bInterfaceNumber" / DescriptorField("Interface number"), + "bAlternateSetting" / DescriptorField("Alternate setting", default=0), + "bNumEndpoints" / DescriptorField("Endpoints included"), + "bInterfaceClass" / DescriptorField("Class", default=0xff), + "bInterfaceSubclass" / DescriptorField("Subclass", default=0xff), + "bInterfaceProtocol" / DescriptorField("Protocol", default=0xff), + "iInterface" / DescriptorField("String index", default=0), +) + + +EndpointDescriptor = DescriptorFormat( + "bLength" / construct.Const(7, construct.Int8ul), + "bDescriptorType" / DescriptorNumber(StandardDescriptorNumbers.ENDPOINT), + "bEndpointAddress" / DescriptorField("Endpoint Address"), + "bmAttributes" / DescriptorField("Attributes", default=2), + "wMaxPacketSize" / DescriptorField("Maximum Packet Size", default=64), + "bInterval" / DescriptorField("Polling interval", default=255), +) + + +DeviceQualifierDescriptor = DescriptorFormat( + "bLength" / DescriptorLength, + "bDescriptorType" / DescriptorNumber(StandardDescriptorNumbers.DEVICE_QUALIFIER), + "bcdUSB" / DescriptorField("USB Version"), + "bDeviceClass" / DescriptorField("Class"), + "bDeviceSubclass" / DescriptorField("Subclass"), + "bDeviceProtocol" / DescriptorField("Protocol"), + "bMaxPacketSize0" / DescriptorField("EP0 Max Pkt Size"), + "bNumConfigurations" / DescriptorField("Configuration Count"), + "_bReserved" / construct.Optional(construct.Const(b"\0")) +) + + +class DescriptorParserCases(unittest.TestCase): + + STRING_DESCRIPTOR = bytes([ + 40, # Length + 3, # Type + ord('G'), 0x00, + ord('r'), 0x00, + ord('e'), 0x00, + ord('a'), 0x00, + ord('t'), 0x00, + ord(' '), 0x00, + ord('S'), 0x00, + ord('c'), 0x00, + ord('o'), 0x00, + ord('t'), 0x00, + ord('t'), 0x00, + ord(' '), 0x00, + ord('G'), 0x00, + ord('a'), 0x00, + ord('d'), 0x00, + ord('g'), 0x00, + ord('e'), 0x00, + ord('t'), 0x00, + ord('s'), 0x00, + ]) + + + def test_string_descriptor_parse(self): + + # Parse the relevant string... + parsed = StringDescriptor.parse(self.STRING_DESCRIPTOR) + + # ... and check the desriptor's fields. + self.assertEqual(parsed.bLength, 40) + self.assertEqual(parsed.bDescriptorType, 3) + self.assertEqual(parsed.bString, "Great Scott Gadgets") + + + def test_string_descriptor_build(self): + data = StringDescriptor.build({ + 'bString': "Great Scott Gadgets" + }) + + self.assertEqual(data, self.STRING_DESCRIPTOR) + + + def test_string_language_descriptor_build(self): + data = StringLanguageDescriptor.build({ + 'wLANGID': (LanguageIDs.ENGLISH_US,) + }) + + self.assertEqual(data, b"\x04\x03\x09\x04") + + + def test_device_descriptor(self): + + device_descriptor = [ + 0x12, # Length + 0x01, # Type + 0x00, 0x02, # USB version + 0xFF, # class + 0xFF, # subclass + 0xFF, # protocol + 64, # ep0 max packet size + 0xd0, 0x16, # VID + 0x3b, 0x0f, # PID + 0x00, 0x00, # device rev + 0x01, # manufacturer string + 0x02, # product string + 0x03, # serial number + 0x01 # number of configurations + ] + + # Parse the relevant string... + parsed = DeviceDescriptor.parse(device_descriptor) + + # ... and check the desriptor's fields. + self.assertEqual(parsed.bLength, 18) + self.assertEqual(parsed.bDescriptorType, 1) + self.assertEqual(parsed.bcdUSB, 2.0) + self.assertEqual(parsed.bDeviceClass, 0xFF) + self.assertEqual(parsed.bDeviceSubclass, 0xFF) + self.assertEqual(parsed.bDeviceProtocol, 0xFF) + self.assertEqual(parsed.bMaxPacketSize0, 64) + self.assertEqual(parsed.idVendor, 0x16d0) + self.assertEqual(parsed.idProduct, 0x0f3b) + self.assertEqual(parsed.bcdDevice, 0) + self.assertEqual(parsed.iManufacturer, 1) + self.assertEqual(parsed.iProduct, 2) + self.assertEqual(parsed.iSerialNumber, 3) + self.assertEqual(parsed.bNumConfigurations, 1) + + + def test_bcd_constructor(self): + + emitter = BCDFieldAdapter(construct.Int16ul) + result = emitter.build(1.4) + + self.assertEqual(result, b"\x40\x01") + + +if __name__ == "__main__": + unittest.main()