# Copyright 2015 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import struct
from collections import namedtuple
from autotest_lib.client.cros.cellular.mbim_compliance import mbim_errors
# All the MBIM_ONLY_* maps are filters for MBIM only function. These maps
# specify the values of the fields which should be matched in the target
# interface.
MBIM_ONLY_COMMUNICATION_INTERFACE = {'bAlternateSetting': 0,
'bNumEndpoints': 1,
'bInterfaceClass': 0x02,
'bInterfaceSubClass': 0x0E,
'bInterfaceProtocol': 0x00}
MBIM_ONLY_DATA_INTERFACE_NO_DATA = {'bAlternateSetting': 0,
'bNumEndpoints': 0,
'bInterfaceClass': 0x0A,
'bInterfaceSubClass': 0x00,
'bInterfaceProtocol': 0x02}
MBIM_ONLY_DATA_INTERFACE_MBIM = {'bAlternateSetting': 1,
'bNumEndpoints': 2,
'bInterfaceClass': 0x0A,
'bInterfaceSubClass': 0x00,
'bInterfaceProtocol': 0x02}
# All the NCM_MBIM_* maps are filters for NCM/MBIM function. These maps
# specify the values of the fields which should be matched in the target
# interface.
NCM_MBIM_COMMUNICATION_INTERFACE_NCM = {'bAlternateSetting': 0,
'bNumEndpoints': 1,
'bInterfaceClass': 0x02,
'bInterfaceSubClass': 0x0D}
NCM_MBIM_COMMUNICATION_INTERFACE_MBIM = {'bAlternateSetting': 1,
'bNumEndpoints': 1,
'bInterfaceClass': 0x02,
'bInterfaceSubClass': 0x0E,
'bInterfaceProtocol': 0x00}
NCM_MBIM_DATA_INTERFACE_NO_DATA = {'bAlternateSetting': 0,
'bNumEndpoints': 0,
'bInterfaceClass': 0x0A,
'bInterfaceSubClass': 0x00,
'bInterfaceProtocol': 0x01}
NCM_MBIM_DATA_INTERFACE_NCM = {'bAlternateSetting': 1,
'bNumEndpoints': 2,
'bInterfaceClass': 0x0A,
'bInterfaceSubClass': 0x00,
'bInterfaceProtocol': 0x01}
NCM_MBIM_DATA_INTERFACE_MBIM = {'bAlternateSetting': 2,
'bNumEndpoints': 2,
'bInterfaceClass': 0x0A,
'bInterfaceSubClass': 0x00,
'bInterfaceProtocol': 0x02}
class DescriptorMeta(type):
"""
Metaclass for creating a USB descriptor class.
A derived descriptor class takes raw descriptor data as an array of unsigned
bytes via its constructor and parses the data into individual fields stored
as instance attributes. A derived class of |Descriptor| should specify the
following class attributes as part of the class definition:
DESCRIPTOR_TYPE: An unsigned 8-bit number specifying the descriptor
type. Except for |UnknownDescriptor|, all derived classes should specify
this attribute. This attribute can be inherited from a parent class.
DESCRIPTOR_SUBTYPE: An unsigned 8-bit number specifying the descriptor
subtype. Only descriptors have a bDescriptorSubtype field should specify
this attribute.
_FIELDS: A list of field definitions specified as a nested tuple. The
field definitions are ordered in the same way as the fields are present
in the USB descriptor. Each inner tuple is a field definition and
contains two elements. The first element specifies the format
character(s), which instructs |struct.unpack_from| how to extract the
field from the raw descriptor data. The second element specifies the
field name, which is also the attribute name used by an instance of the
derived descriptor class for storing the field. Each derived descriptor
class must define its own _FIELDS attribute, which must have
('B', 'bLength'), ('B', 'bDescriptorType') as the first two entries.
"""
descriptor_classes = []
def __new__(mcs, name, bases, attrs):
# The Descriptor base class, which inherits from 'object', is merely
# used to establish the class hierarchy and is never constructed from
# raw descriptor data.
if object in bases:
return super(DescriptorMeta, mcs).__new__(mcs, name, bases, attrs)
if '_FIELDS' not in attrs:
raise mbim_errors.MBIMComplianceFrameworkError(
'%s must define a _FIELDS attribute' % name)
field_formats, field_names = zip(*attrs['_FIELDS'])
# USB descriptor data are in the little-endian format.
data_format = '<' + ''.join(field_formats)
unpack_length = struct.calcsize(data_format)
def descriptor_class_new(cls, data):
"""
Creates a descriptor instance with the given descriptor data.
@param cls: The descriptor class of the instance to be created.
@param data: The raw descriptor data as an array of unsigned bytes.
@returns The descriptor instance.
"""
data_length = len(data)
if unpack_length > data_length:
raise mbim_errors.MBIMComplianceFrameworkError(
'Expected %d or more bytes of descriptor data, got %d' %
(unpack_length, data_length))
obj = super(cls, cls).__new__(cls, *struct.unpack_from(data_format,
data))
setattr(obj, 'data', data)
descriptor_type = attrs.get('DESCRIPTOR_TYPE')
if (descriptor_type is not None and
descriptor_type != obj.bDescriptorType):
raise mbim_errors.MBIMComplianceFrameworkError(
'Expected descriptor type 0x%02X, got 0x%02X' %
(descriptor_type, obj.bDescriptorType))
descriptor_subtype = attrs.get('DESCRIPTOR_SUBTYPE')
if (descriptor_subtype is not None and
descriptor_subtype != obj.bDescriptorSubtype):
raise mbim_errors.MBIMComplianceFrameworkError(
'Expected descriptor subtype 0x%02X, got 0x%02X' %
(descriptor_subtype, obj.bDescriptorSubtype))
if data_length != obj.bLength:
raise mbim_errors.MBIMComplianceFrameworkError(
'Expected descriptor length %d, got %d' %
(data_length, obj.bLength))
# TODO(benchan): We don't currently handle the case where
# |data_length| > |unpack_length|, which happens if the descriptor
# contains a variable length field (e.g. StringDescriptor).
return obj
attrs['__new__'] = descriptor_class_new
descriptor_class = namedtuple(name, field_names)
# Prepend the class created via namedtuple to |bases| in order to
# correctly resolve the __new__ method while preserving the class
# hierarchy.
cls = super(DescriptorMeta, mcs).__new__(mcs, name,
(descriptor_class,) + bases,
attrs)
# As Descriptor.__subclasses__() only reports its direct subclasses,
# we keep track of all subclasses of Descriptor using the
# |DescriptorMeta.descriptor_classes| attribute.
mcs.descriptor_classes.append(cls)
return cls
class Descriptor(object):
"""
USB Descriptor base class.
This class should not be instantiated or used directly.
"""
__metaclass__ = DescriptorMeta
class UnknownDescriptor(Descriptor):
"""
Unknown USB Descriptor.
This class is a catch-all descriptor for unsupported or unknown descriptor
types.
"""
_FIELDS = (('B', 'bLength'),
('B', 'bDescriptorType'))
class DeviceDescriptor(Descriptor):
""" Device Descriptor. """
DESCRIPTOR_TYPE = 0x01
_FIELDS = (('B', 'bLength'),
('B', 'bDescriptorType'),
('H', 'bcdUSB'),
('B', 'bDeviceClass'),
('B', 'bDeviceSubClass'),
('B', 'bDeviceProtocol'),
('B', 'bMaxPacketSize0'),
('H', 'idVendor'),
('H', 'idProduct'),
('H', 'bcdDevice'),
('B', 'iManufacturer'),
('B', 'iProduct'),
('B', 'iSerialNumber'),
('B', 'bNumConfigurations'))
class ConfigurationDescriptor(Descriptor):
""" Configuration Descriptor. """
DESCRIPTOR_TYPE = 0x02
_FIELDS = (('B', 'bLength'),
('B', 'bDescriptorType'),
('H', 'wTotalLength'),
('B', 'bNumInterfaces'),
('B', 'bConfigurationValue'),
('B', 'iConfiguration'),
('B', 'bmAttributes'),
('B', 'bMaxPower'))
class InterfaceDescriptor(Descriptor):
""" Interface Descriptor. """
DESCRIPTOR_TYPE = 0x04
_FIELDS = (('B', 'bLength'),
('B', 'bDescriptorType'),
('B', 'bInterfaceNumber'),
('B', 'bAlternateSetting'),
('B', 'bNumEndpoints'),
('B', 'bInterfaceClass'),
('B', 'bInterfaceSubClass'),
('B', 'bInterfaceProtocol'),
('B', 'iInterface'))
class EndpointDescriptor(Descriptor):
""" Endpoint Descriptor. """
DESCRIPTOR_TYPE = 0x05
_FIELDS = (('B', 'bLength'),
('B', 'bDescriptorType'),
('B', 'bEndpointAddress'),
('B', 'bmAttributes'),
('H', 'wMaxPacketSize'),
('B', 'bInterval'))
class InterfaceAssociationDescriptor(Descriptor):
""" Interface Asscociation Descriptor. """
DESCRIPTOR_TYPE = 0x0B
_FIELDS = (('B', 'bLength'),
('B', 'bDescriptorType'),
('B', 'bFirstInterface'),
('B', 'bInterfaceCount'),
('B', 'bFunctionClass'),
('B', 'bFunctionSubClass'),
('B', 'bFunctionProtocol'),
('B', 'iFunction'))
class FunctionalDescriptor(Descriptor):
""" Functional Descriptor. """
DESCRIPTOR_TYPE = 0x24
_FIELDS = (('B', 'bLength'),
('B', 'bDescriptorType'),
('B', 'bDescriptorSubtype'))
class HeaderFunctionalDescriptor(FunctionalDescriptor):
""" Header Functional Descriptor. """
DESCRIPTOR_SUBTYPE = 0x00
_FIELDS = (('B', 'bLength'),
('B', 'bDescriptorType'),
('B', 'bDescriptorSubtype'),
('H', 'bcdCDC'))
class UnionFunctionalDescriptor(FunctionalDescriptor):
""" Union Functional Descriptor. """
DESCRIPTOR_SUBTYPE = 0x06
_FIELDS = (('B', 'bLength'),
('B', 'bDescriptorType'),
('B', 'bDescriptorSubtype'),
('B', 'bControlInterface'),
('B', 'bSubordinateInterface0'))
class MBIMFunctionalDescriptor(FunctionalDescriptor):
""" MBIM Functional Descriptor. """
DESCRIPTOR_SUBTYPE = 0x1B
_FIELDS = (('B', 'bLength'),
('B', 'bDescriptorType'),
('B', 'bDescriptorSubtype'),
('H', 'bcdMBIMVersion'),
('H', 'wMaxControlMessage'),
('B', 'bNumberFilters'),
('B', 'bMaxFilterSize'),
('H', 'wMaxSegmentSize'),
('B', 'bmNetworkCapabilities'))
class MBIMExtendedFunctionalDescriptor(FunctionalDescriptor):
""" MBIM Extended Functional Descriptor. """
DESCRIPTOR_SUBTYPE = 0x1C
_FIELDS = (('B', 'bLength'),
('B', 'bDescriptorType'),
('B', 'bDescriptorSubtype'),
('H', 'bcdMBIMExtendedVersion'),
('B', 'bMaxOutstandingCommandMessages'),
('H', 'wMTU'))
class SuperSpeedEndpointCompanionDescriptor(Descriptor):
""" SuperSpeed Endpoint Companion Descriptor. """
DESCRIPTOR_TYPE = 0x30
_FIELDS = (('B', 'bLength'),
('B', 'bDescriptorType'),
('B', 'bMaxBurst'),
('B', 'bmAttributes'),
('H', 'wBytesPerInterval'))
class DescriptorParser(object):
"""
A class for extracting USB descriptors from raw descriptor data.
This class takes raw descriptor data as an array of unsigned bytes via its
constructor and provides an iterator interface to return individual USB
descriptors via instances derived from a subclass of Descriptor.
"""
_DESCRIPTOR_CLASS_MAP = {
(cls.DESCRIPTOR_TYPE, getattr(cls, 'DESCRIPTOR_SUBTYPE', None)): cls
for cls in DescriptorMeta.descriptor_classes
if hasattr(cls, 'DESCRIPTOR_TYPE')
}
def __init__(self, data):
self._data = data
self._data_length = len(data)
self._index = 0
# The position of each descriptor in the list.
self._descriptor_index = 0
def __iter__(self):
return self
def next(self):
"""
Returns the next descriptor found in the descriptor data.
@returns An instance of a subclass of Descriptor.
@raises StopIteration if no more descriptor is found,
"""
if self._index >= self._data_length:
raise StopIteration
# Identify the descriptor class based on bDescriptorType, and if
# available, bDescriptorSubtype. The descriptor data has a standard
# layout as follows:
# self._data[self._index]: bLength
# self._data[self._index + 1]: bDescriptorType
# self._data[self._index + 2]: bDescriptorSubtype for some descriptors
descriptor_type, descriptor_subtype = None, None
if self._index + 1 < self._data_length:
descriptor_type = self._data[self._index + 1]
if self._index + 2 < self._data_length:
descriptor_subtype = self._data[self._index + 2]
descriptor_class = self._DESCRIPTOR_CLASS_MAP.get(
(descriptor_type, descriptor_subtype), None)
if descriptor_class is None:
descriptor_class = self._DESCRIPTOR_CLASS_MAP.get(
(descriptor_type, None), UnknownDescriptor)
next_index = self._index + self._data[self._index]
descriptor = descriptor_class(self._data[self._index:next_index])
self._index = next_index
descriptor.index = self._descriptor_index
self._descriptor_index += 1
return descriptor
def filter_descriptors(descriptor_type, descriptors):
"""
Filter a list of descriptors based on the target |descriptor_type|.
@param descriptor_type: The target descriptor type.
@param descriptors: The list of functional descriptors.
Type: Array of |Descriptor| objects.
@returns A list of target descriptors.
"""
if not descriptors:
return []
return filter(lambda descriptor: isinstance(descriptor, descriptor_type),
descriptors)
def has_distinct_descriptors(descriptors):
"""
Check if there are distinct descriptors in the given list.
@param descriptors: The list of descriptors.
Type: Array of |Descriptor| objects.
@returns True if distinct descriptor are found, False otherwise.
"""
return not all(descriptor == descriptors[0] for descriptor in descriptors)
def get_descriptor_bundle(descriptors, descriptor):
"""
Get the bundle for the |descriptor|. For example, if |descriptor| is of
inferface type, this bundle should include functional descriptors and
endpoint descriptors.
@param descriptors: A list of all descriptors.
Type: Array of |Descriptor| objects.
@param descriptor: The starting point of the bundle.
@returns The bundle for |descriptor|.
"""
index = descriptor.index + 1
while (index < len(descriptors) and
type(descriptor) != type(descriptors[index])):
index += 1
return descriptors[descriptor.index: index]
def filter_interface_descriptors(descriptors, interface_type):
"""
Filter interface descriptors based on the values in fields.
@param descriptors: A list of interface descriptors.
Type: Array of |Descriptor| objects.
@param interface_type: A dictionary composed of pairs(field: value) to
match the target interface.
@returns A list of target interfaces.
"""
def _match_all_fields(interface):
"""
Match fields for a given interface descriptor.
The descriptor is matched based on the fields provided in
|interface_type|.
@param interface: An interface descriptor.
Type: |Descriptor| object.
@returns True if all fields match, False otherwise.
"""
for key, value in interface_type.iteritems():
if (not hasattr(interface, key) or
getattr(interface, key) != value):
return False
return True
return filter(lambda descriptor: _match_all_fields(descriptor),
descriptors)
def has_bulk_in_and_bulk_out(endpoints):
"""
Check if there are one bulk-in endpoint and one bulk-out endpoint.
@param endpoints: A list of endpoint descriptors.
Type: Array of |Descriptor| objects.
@returns True if there are one bulk-in and one bulk-out endpoint, False
otherwise.
"""
bulk_in, bulk_out = False, False
for endpoint in endpoints:
if (endpoint.bLength == 7 and
endpoint.bEndpointAddress < 0x80 and
endpoint.bmAttributes == 0x02):
bulk_out = True
elif (endpoint.bLength == 7 and
endpoint.bEndpointAddress >= 0x80 and
endpoint.bmAttributes == 0x02):
bulk_in = True
return bulk_in and bulk_out