@@ -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() |