Browse Source

initial commit: import descriptor definitions and basic types from ViewSB

main
Kate Temkin 4 years ago
commit
667118879e
10 changed files with 952 additions and 0 deletions
  1. +19
    -0
      .editorconfig
  2. +133
    -0
      .gitignore
  3. +29
    -0
      LICENSE.txt
  4. +10
    -0
      README.md
  5. +43
    -0
      setup.py
  6. +0
    -0
      usb-protocol/__init__.py
  7. +416
    -0
      usb-protocol/types/__init__.py
  8. +139
    -0
      usb-protocol/types/descriptor.py
  9. +0
    -0
      usb-protocol/types/descriptors/__init__.py
  10. +163
    -0
      usb-protocol/types/descriptors/standard.py

+ 19
- 0
.editorconfig View File

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

+ 133
- 0
.gitignore View File

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

+ 29
- 0
LICENSE.txt View File

@@ -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.

+ 10
- 0
README.md View File

@@ -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.

+ 43
- 0
setup.py View File

@@ -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
usb-protocol/__init__.py View File


+ 416
- 0
usb-protocol/types/__init__.py View File

@@ -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)",
}

+ 139
- 0
usb-protocol/types/descriptor.py View File

@@ -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
usb-protocol/types/descriptors/__init__.py View File


+ 163
- 0
usb-protocol/types/descriptors/standard.py View File

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

Loading…
Cancel
Save