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