| @@ -0,0 +1,19 @@ | |||
| # top-most EditorConfig file | |||
| root = true | |||
| # Set our default format parameters. | |||
| [*] | |||
| charset = utf-8 | |||
| indent_size = 4 | |||
| end_of_line = lf | |||
| insert_final_newline = true | |||
| trim_trailing_whitespace = true | |||
| max_line_length = 120 | |||
| # Use python standard indentation for python files. | |||
| [*.py] | |||
| indent_style = space | |||
| # Use tabs for our C files, to match coding conventions of our submodues. | |||
| [*.{c,h}] | |||
| indent_style = tab | |||
| @@ -0,0 +1,133 @@ | |||
| .DS_Store | |||
| # Byte-compiled / optimized / DLL files | |||
| __pycache__/ | |||
| *.py[cod] | |||
| *$py.class | |||
| # C extensions | |||
| *.so | |||
| # Distribution / packaging | |||
| .Python | |||
| build/ | |||
| develop-eggs/ | |||
| dist/ | |||
| downloads/ | |||
| eggs/ | |||
| .eggs/ | |||
| lib/ | |||
| lib64/ | |||
| parts/ | |||
| sdist/ | |||
| var/ | |||
| wheels/ | |||
| pip-wheel-metadata/ | |||
| share/python-wheels/ | |||
| *.egg-info/ | |||
| .installed.cfg | |||
| *.egg | |||
| MANIFEST | |||
| # PyInstaller | |||
| # Usually these files are written by a python script from a template | |||
| # before PyInstaller builds the exe, so as to inject date/other infos into it. | |||
| *.manifest | |||
| *.spec | |||
| # Installer logs | |||
| pip-log.txt | |||
| pip-delete-this-directory.txt | |||
| # Unit test / coverage reports | |||
| htmlcov/ | |||
| .tox/ | |||
| .nox/ | |||
| .coverage | |||
| .coverage.* | |||
| .cache | |||
| nosetests.xml | |||
| coverage.xml | |||
| *.cover | |||
| *.py,cover | |||
| .hypothesis/ | |||
| .pytest_cache/ | |||
| # Translations | |||
| *.mo | |||
| *.pot | |||
| # Django stuff: | |||
| *.log | |||
| local_settings.py | |||
| db.sqlite3 | |||
| db.sqlite3-journal | |||
| # Flask stuff: | |||
| instance/ | |||
| .webassets-cache | |||
| # Scrapy stuff: | |||
| .scrapy | |||
| # Sphinx documentation | |||
| docs/_build/ | |||
| # PyBuilder | |||
| target/ | |||
| # Jupyter Notebook | |||
| .ipynb_checkpoints | |||
| # IPython | |||
| profile_default/ | |||
| ipython_config.py | |||
| # pyenv | |||
| .python-version | |||
| # pipenv | |||
| # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. | |||
| # However, in case of collaboration, if having platform-specific dependencies or dependencies | |||
| # having no cross-platform support, pipenv may install dependencies that don't work, or not | |||
| # install all needed dependencies. | |||
| #Pipfile.lock | |||
| # PEP 582; used by e.g. github.com/David-OConnor/pyflow | |||
| __pypackages__/ | |||
| # Celery stuff | |||
| celerybeat-schedule | |||
| celerybeat.pid | |||
| # SageMath parsed files | |||
| *.sage.py | |||
| # Environments | |||
| .env | |||
| .venv | |||
| env/ | |||
| venv/ | |||
| ENV/ | |||
| env.bak/ | |||
| venv.bak/ | |||
| # Spyder project settings | |||
| .spyderproject | |||
| .spyproject | |||
| # Rope project settings | |||
| .ropeproject | |||
| # mkdocs documentation | |||
| /site | |||
| # mypy | |||
| .mypy_cache/ | |||
| .dmypy.json | |||
| dmypy.json | |||
| # Pyre type checker | |||
| .pyre/ | |||
| # Editor / IDE files | |||
| .vscode | |||
| @@ -0,0 +1,29 @@ | |||
| BSD 3-Clause License | |||
| Copyright (c) 2020, Great Scott Gadgets <info@greatscottgadgets.com> | |||
| Copyright (c) 2020, Katherine J. Temkin <ktemkin@greatscottgadgets.com> | |||
| Redistribution and use in source and binary forms, with or without | |||
| modification, are permitted provided that the following conditions are met: | |||
| * Redistributions of source code must retain the above copyright notice, this | |||
| list of conditions and the following disclaimer. | |||
| * Redistributions in binary form must reproduce the above copyright notice, | |||
| this list of conditions and the following disclaimer in the documentation | |||
| and/or other materials provided with the distribution. | |||
| * Neither the name of the copyright holder nor the names of its | |||
| contributors may be used to endorse or promote products derived from | |||
| this software without specific prior written permission. | |||
| THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |||
| AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |||
| IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |||
| DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | |||
| FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | |||
| DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | |||
| SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | |||
| CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | |||
| OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |||
| OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |||
| @@ -0,0 +1,10 @@ | |||
| # USB Protocol Library for Python | |||
| `usb-protocol` is a library that collects common data-processing code for USB tasks; | |||
| and is meant to support a variety of projects, including USB stacks, analyzers, and | |||
| other tools that work with USB data. A primary intention is to unify common code from | |||
| LUNA, FaceDancer, and ViewSB. | |||
| The library is currently an early work-in-progress; this documentation will be updated | |||
| when the project is more mature. | |||
| @@ -0,0 +1,43 @@ | |||
| from setuptools import setup, find_packages | |||
| setup( | |||
| # Vitals | |||
| name='usb-protocol', | |||
| license='BSD', | |||
| url='https://github.com/usb-tool/luna', | |||
| author='Katherine J. Temkin', | |||
| author_email='ktemkin@greatscottgadgets.com', | |||
| description='python library providing utilities, data structures, constants, parsers, and tools for working with USB data', | |||
| use_scm_version= { | |||
| "root": '..', | |||
| "relative_to": __file__, | |||
| "version_scheme": "guess-next-dev", | |||
| "local_scheme": lambda version : version.format_choice("+{node}", "+{node}.dirty"), | |||
| "fallback_version": "0.0" | |||
| }, | |||
| # Imports / exports / requirements. | |||
| platforms='any', | |||
| packages=find_packages(), | |||
| include_package_data=True, | |||
| python_requires="~=3.7", | |||
| install_requires=['construct'], | |||
| setup_requires=['setuptools', 'setuptools_scm'], | |||
| # Metadata | |||
| classifiers = [ | |||
| 'Programming Language :: Python', | |||
| 'Development Status :: 1 - Planning', | |||
| 'Natural Language :: English', | |||
| 'Environment :: Console', | |||
| 'Environment :: Plugins', | |||
| 'Intended Audience :: Developers', | |||
| 'Intended Audience :: Science/Research', | |||
| 'License :: OSI Approved :: BSD License', | |||
| 'Operating System :: OS Independent', | |||
| 'Topic :: Scientific/Engineering', | |||
| 'Topic :: Security', | |||
| ], | |||
| ) | |||
| @@ -0,0 +1,416 @@ | |||
| # | |||
| # This file is part of usb-protocol. | |||
| # | |||
| """ USB types -- defines enumerations that describe standard USB types """ | |||
| from enum import Enum, IntFlag, IntEnum | |||
| class USBDirection(IntEnum): | |||
| """ Class representing USB directions. """ | |||
| OUT = 0 | |||
| IN = 1 | |||
| def is_in(self): | |||
| return self is self.IN | |||
| def is_out(self): | |||
| return self is self.OUT | |||
| @classmethod | |||
| def parse(cls, value): | |||
| """ Helper that converts a numeric field into a direction. """ | |||
| return cls(value) | |||
| @classmethod | |||
| def from_request_type(cls, request_type_int): | |||
| """ Helper method that extracts the direction from a request_type integer. """ | |||
| return cls(request_type_int >> 7) | |||
| @classmethod | |||
| def from_endpoint_address(cls, address): | |||
| """ Helper method that extracts the direction from an endpoint address. """ | |||
| return cls(address >> 7) | |||
| def token(self): | |||
| """ Generates the token corresponding to the given direction. """ | |||
| return USBPacketID.IN if (self is self.IN) else USBPacketID.OUT | |||
| def reverse(self): | |||
| """ Returns the reverse of the given direction. """ | |||
| return self.OUT if (self is self.IN) else self.IN | |||
| def to_endpoint_address(self, endpoint_number): | |||
| """ Helper method that converts and endpoint_number to an address, given direction. """ | |||
| if self.is_in(): | |||
| return endpoint_number | (1 << 7) | |||
| else: | |||
| return endpoint_number | |||
| class USBPIDCategory(IntFlag): | |||
| """ Category constants for each of the groups that PIDs can fall under. """ | |||
| SPECIAL = 0b00 | |||
| TOKEN = 0b01 | |||
| HANDSHAKE = 0b10 | |||
| DATA = 0b11 | |||
| MASK = 0b11 | |||
| class USBPacketID(IntFlag): | |||
| """ Enumeration specifying all of the valid USB PIDs we can handle. """ | |||
| # Token group (lsbs = 0b01). | |||
| OUT = 0b0001 | |||
| IN = 0b1001 | |||
| SOF = 0b0101 | |||
| SETUP = 0b1101 | |||
| # Data group (lsbs = 0b11). | |||
| DATA0 = 0b0011 | |||
| DATA1 = 0b1011 | |||
| DATA2 = 0b0111 | |||
| MDATA = 0b1111 | |||
| # Handshake group (lsbs = 0b10) | |||
| ACK = 0b0010 | |||
| NAK = 0b1010 | |||
| STALL = 0b1110 | |||
| NYET = 0b0110 | |||
| # Special group. | |||
| PRE = 0b1100 | |||
| ERR = 0b1100 | |||
| SPLIT = 0b1000 | |||
| PING = 0b0100 | |||
| # Flag representing that the PID seems invalid. | |||
| PID_INVALID = 0b10000 | |||
| PID_CORE_MASK = 0b01111 | |||
| @classmethod | |||
| def from_byte(cls, byte, skip_checks=False): | |||
| """ Creates a PID object from a byte. """ | |||
| # Convert the raw PID to an integer. | |||
| pid_as_int = int.from_bytes(byte, byteorder='little') | |||
| return cls.from_int(pid_as_int, skip_checks=skip_checks) | |||
| @classmethod | |||
| def from_int(cls, value, skip_checks=True): | |||
| """ Create a PID object from an integer. """ | |||
| PID_MASK = 0b1111 | |||
| INVERTED_PID_SHIFT = 4 | |||
| # Pull out the PID and its inverse from the byte. | |||
| pid = cls(value & PID_MASK) | |||
| inverted_pid = value >> INVERTED_PID_SHIFT | |||
| # If we're not skipping checks, | |||
| if not skip_checks: | |||
| if (pid ^ inverted_pid) != PID_MASK: | |||
| pid |= cls.PID_INVALID | |||
| return cls(pid) | |||
| @classmethod | |||
| def from_name(cls, name): | |||
| """ Create a PID object from a string representation of its name. """ | |||
| return cls[name] | |||
| @classmethod | |||
| def parse(cls, value): | |||
| """ Attempt to create a PID object from a number, byte, or string. """ | |||
| if isinstance(value, bytes): | |||
| return cls.from_byte(value) | |||
| if isinstance(value, str): | |||
| return cls.from_name(value) | |||
| if isinstance(value, int): | |||
| return cls.from_int(value) | |||
| return cls(value) | |||
| def category(self): | |||
| """ Returns the USBPIDCategory that each given PID belongs to. """ | |||
| return USBPIDCategory(self & USBPIDCategory.MASK) | |||
| def is_data(self): | |||
| """ Returns true iff the given PID represents a DATA packet. """ | |||
| return self.category() is USBPIDCategory.DATA | |||
| def is_token(self): | |||
| """ Returns true iff the given PID represents a token packet. """ | |||
| return self.category() is USBPIDCategory.TOKEN | |||
| def is_handshake(self): | |||
| """ Returns true iff the given PID represents a handshake packet. """ | |||
| return self.category() is USBPIDCategory.HANDSHAKE | |||
| def is_invalid(self): | |||
| """ Returns true if this object is an attempt to encapsulate an invalid PID. """ | |||
| return (self & self.PID_INVALID) | |||
| def direction(self): | |||
| """ Get a USB direction from a PacketID. """ | |||
| if self is self.SOF: | |||
| return None | |||
| if self is self.SETUP or self is self.OUT: | |||
| return USBDirection.OUT | |||
| if self is self.IN: | |||
| return USBDirection.IN | |||
| raise ValueError("cannot determine the direction of a non-token PID") | |||
| def summarize(self): | |||
| """ Return a summary of the given packet. """ | |||
| # By default, get the raw name. | |||
| core_pid = self & self.PID_CORE_MASK | |||
| name = core_pid.name | |||
| if self.is_invalid(): | |||
| return "{} (check-nibble invalid)".format(name) | |||
| else: | |||
| return name | |||
| def byte(self): | |||
| """ Return the PID's value with its upper nibble. """ | |||
| inverted_pid = self ^ 0b1111 | |||
| full_pid = (inverted_pid << 4) | self | |||
| return full_pid | |||
| class USBRequestRecipient(IntEnum): | |||
| """ Enumeration that describes each 'recipient' of a USB request field. """ | |||
| DEVICE = 0 | |||
| INTERFACE = 1 | |||
| ENDPOINT = 2 | |||
| OTHER = 3 | |||
| RESERVED = 4 | |||
| @classmethod | |||
| def from_integer(cls, value): | |||
| """ Special factory that correctly handles reserved values. """ | |||
| # If we have one of the reserved values; indicate so. | |||
| if 4 <= value < 16: | |||
| return cls.RESERVED | |||
| # Otherwise, translate the raw value. | |||
| return cls(value) | |||
| @classmethod | |||
| def from_request_type(cls, request_type_int): | |||
| """ Helper method that extracts the type from a request_type integer. """ | |||
| MASK = 0b11111 | |||
| return cls(request_type_int & MASK) | |||
| class USBRequestType(IntEnum): | |||
| """ Enumeration that describes each possible Type field for a USB request. """ | |||
| STANDARD = 0 | |||
| CLASS = 1 | |||
| VENDOR = 2 | |||
| RESERVED = 3 | |||
| @classmethod | |||
| def from_request_type(cls, request_type_int): | |||
| """ Helper method that extracts the type from a request_type integer. """ | |||
| SHIFT = 5 | |||
| MASK = 0b11 | |||
| return cls((request_type_int >> SHIFT) & MASK) | |||
| class USBTransferType(IntEnum): | |||
| CONTROL = 0 | |||
| ISOCHRONOUS = 1 | |||
| BULK = 2 | |||
| INTERRUPT = 3 | |||
| def endpoint_number_from_address(number): | |||
| return number & 0x7F | |||
| LANGUAGE_NAMES = { | |||
| 0x0436: "Afrikaans", | |||
| 0x041c: "Albanian", | |||
| 0x0401: "Arabic (Saudi Arabia)", | |||
| 0x0801: "Arabic (Iraq)", | |||
| 0x0c01: "Arabic (Egypt)", | |||
| 0x1001: "Arabic (Libya)", | |||
| 0x1401: "Arabic (Algeria)", | |||
| 0x1801: "Arabic (Morocco)", | |||
| 0x1c01: "Arabic (Tunisia)", | |||
| 0x2001: "Arabic (Oman)", | |||
| 0x2401: "Arabic (Yemen)", | |||
| 0x2801: "Arabic (Syria)", | |||
| 0x2c01: "Arabic (Jordan)", | |||
| 0x3001: "Arabic (Lebanon)", | |||
| 0x3401: "Arabic (Kuwait)", | |||
| 0x3801: "Arabic (U.A.E.)", | |||
| 0x3c01: "Arabic (Bahrain)", | |||
| 0x4001: "Arabic (Qatar)", | |||
| 0x042b: "Armenian", | |||
| 0x044d: "Assamese", | |||
| 0x042c: "Azeri (Latin)", | |||
| 0x082c: "Azeri (Cyrillic)", | |||
| 0x042d: "Basque", | |||
| 0x0423: "Belarussian", | |||
| 0x0445: "Bengali", | |||
| 0x0402: "Bulgarian", | |||
| 0x0455: "Burmese", | |||
| 0x0403: "Catalan", | |||
| 0x0404: "Chinese (Taiwan)", | |||
| 0x0804: "Chinese (PRC)", | |||
| 0x0c04: "Chinese (Hong Kong SAR, PRC)", | |||
| 0x1004: "Chinese (Singapore)", | |||
| 0x1404: "Chinese (Macau SAR)", | |||
| 0x041a: "Croatian", | |||
| 0x0405: "Czech", | |||
| 0x0406: "Danish", | |||
| 0x0413: "Dutch (Netherlands)", | |||
| 0x0813: "Dutch (Belgium)", | |||
| 0x0409: "English (US)", | |||
| 0x0809: "English (United Kingdom)", | |||
| 0x0c09: "English (Australian)", | |||
| 0x1009: "English (Canadian)", | |||
| 0x1409: "English (New Zealand)", | |||
| 0x1809: "English (Ireland)", | |||
| 0x1c09: "English (South Africa)", | |||
| 0x2009: "English (Jamaica)", | |||
| 0x2409: "English (Caribbean)", | |||
| 0x2809: "English (Belize)", | |||
| 0x2c09: "English (Trinidad)", | |||
| 0x3009: "English (Zimbabwe)", | |||
| 0x3409: "English (Philippines)", | |||
| 0x0425: "Estonian", | |||
| 0x0438: "Faeroese", | |||
| 0x0429: "Farsi", | |||
| 0x040b: "Finnish", | |||
| 0x040c: "French (Standard)", | |||
| 0x080c: "French (Belgian)", | |||
| 0x0c0c: "French (Canadian)", | |||
| 0x100c: "French (Switzerland)", | |||
| 0x140c: "French (Luxembourg)", | |||
| 0x180c: "French (Monaco)", | |||
| 0x0437: "Georgian", | |||
| 0x0407: "German (Standard)", | |||
| 0x0807: "German (Switzerland)", | |||
| 0x0c07: "German (Austria)", | |||
| 0x1007: "German (Luxembourg)", | |||
| 0x1407: "German (Liechtenstein)", | |||
| 0x0408: "Greek", | |||
| 0x0447: "Gujarati", | |||
| 0x040d: "Hebrew", | |||
| 0x0439: "Hindi", | |||
| 0x040e: "Hungarian", | |||
| 0x040f: "Icelandic", | |||
| 0x0421: "Indonesian", | |||
| 0x0410: "Italian (Standard)", | |||
| 0x0810: "Italian (Switzerland)", | |||
| 0x0411: "Japanese", | |||
| 0x044b: "Kannada", | |||
| 0x0860: "Kashmiri (India)", | |||
| 0x043f: "Kazakh", | |||
| 0x0457: "Konkani", | |||
| 0x0412: "Korean", | |||
| 0x0812: "Korean (Johab)", | |||
| 0x0426: "Latvian", | |||
| 0x0427: "Lithuanian", | |||
| 0x0827: "Lithuanian (Classic)", | |||
| 0x042f: "Macedonian", | |||
| 0x043e: "Malay (Malaysian)", | |||
| 0x083e: "Malay (Brunei Darussalam)", | |||
| 0x044c: "Malayalam", | |||
| 0x0458: "Manipuri", | |||
| 0x044e: "Marathi", | |||
| 0x0861: "Nepali (India)", | |||
| 0x0414: "Norwegian (Bokmal)", | |||
| 0x0814: "Norwegian (Nynorsk)", | |||
| 0x0448: "Oriya", | |||
| 0x0415: "Polish", | |||
| 0x0416: "Portuguese (Brazil)", | |||
| 0x0816: "Portuguese (Standard)", | |||
| 0x0446: "Punjabi", | |||
| 0x0418: "Romanian", | |||
| 0x0419: "Russian", | |||
| 0x044f: "Sanskrit", | |||
| 0x0c1a: "Serbian (Cyrillic)", | |||
| 0x081a: "Serbian (Latin)", | |||
| 0x0459: "Sindhi", | |||
| 0x041b: "Slovak", | |||
| 0x0424: "Slovenian", | |||
| 0x040a: "Spanish (Traditional Sort)", | |||
| 0x080a: "Spanish (Mexican)", | |||
| 0x0c0a: "Spanish (Modern Sort)", | |||
| 0x100a: "Spanish (Guatemala)", | |||
| 0x140a: "Spanish (Costa Rica)", | |||
| 0x180a: "Spanish (Panama)", | |||
| 0x1c0a: "Spanish (Dominican Republic)", | |||
| 0x200a: "Spanish (Venezuela)", | |||
| 0x240a: "Spanish (Colombia)", | |||
| 0x280a: "Spanish (Peru)", | |||
| 0x2c0a: "Spanish (Argentina)", | |||
| 0x300a: "Spanish (Ecuador)", | |||
| 0x340a: "Spanish (Chile)", | |||
| 0x380a: "Spanish (Uruguay)", | |||
| 0x3c0a: "Spanish (Paraguay)", | |||
| 0x400a: "Spanish (Bolivia)", | |||
| 0x440a: "Spanish (El Salvador)", | |||
| 0x480a: "Spanish (Honduras)", | |||
| 0x4c0a: "Spanish (Nicaragua)", | |||
| 0x500a: "Spanish (Puerto Rico)", | |||
| 0x0430: "Sutu", | |||
| 0x0441: "Swahili (Kenya)", | |||
| 0x041d: "Swedish", | |||
| 0x081d: "Swedish (Finland)", | |||
| 0x0449: "Tamil", | |||
| 0x0444: "Tatar (Tatarstan)", | |||
| 0x044a: "Telugu", | |||
| 0x041e: "Thai", | |||
| 0x041f: "Turkish", | |||
| 0x0422: "Ukrainian", | |||
| 0x0420: "Urdu (Pakistan)", | |||
| 0x0820: "Urdu (India)", | |||
| 0x0443: "Uzbek (Latin)", | |||
| 0x0843: "Uzbek (Cyrillic)", | |||
| 0x042a: "Vietnamese", | |||
| 0x04ff: "HID (Usage Data Descriptor)", | |||
| 0xf0ff: "HID (Vendor Defined 1)", | |||
| 0xf4ff: "HID (Vendor Defined 2)", | |||
| 0xf8ff: "HID (Vendor Defined 3)", | |||
| 0xfcff: "HID (Vendor Defined 4)", | |||
| } | |||
| @@ -0,0 +1,139 @@ | |||
| # | |||
| # This file is part of usb-protocol. | |||
| # | |||
| """ Type elements for defining USB descriptors. """ | |||
| import construct | |||
| class DescriptorFormat(construct.Struct): | |||
| @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 | |||
| 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 | |||
| 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] | |||
| def get_descriptor_number(self): | |||
| """ Returns this constant's associated descriptor number.""" | |||
| return self.number | |||
| 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.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), | |||
| } | |||
| @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=""): | |||
| self.description = description | |||
| 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 | |||
| @@ -0,0 +1,163 @@ | |||
| # | |||
| # 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() | |||