Created
April 28, 2015 14:30
-
-
Save yxy/5aa98c3e42507fe3ace8 to your computer and use it in GitHub Desktop.
This recipe provides a higher level wrapper around the struct module.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# encoding: utf-8 | |
""" | |
This recipe provides a higher level wrapper around the struct module. | |
It provides a more convenient syntax for defining and using structs, | |
and adds additional features such as: | |
- Allows embedding structures within other structures | |
- Allows defining arrays of items (or other structures) | |
- Class based syntax, allowing access and updates by field name, not position | |
- Extension of structures by inheritance | |
recipe from: http://code.activestate.com/recipes/498149-a-higher-level-struct-module/ | |
""" | |
import struct | |
from threading import Lock | |
_lock = Lock() | |
class Format(object): | |
"""Endianness and size format for structures.""" | |
Native = "@" # native format, native size | |
StandardNative = "=" # Native format, standard size | |
LittleEndian = "<" # Standard size | |
BigEndian = ">" # Standard size | |
class Element(object): | |
"""A single element in a struct. | |
:arg string typecode: | |
.. attribute:: id | |
Use for identification and sorting | |
.. attribute:: typecode | |
The typecode list in struct | |
.. attribute: size | |
Size of struct | |
""" | |
_id = 0 | |
def __init__(self, typecode): | |
with _lock as l: | |
Element._id += 1 | |
self.id = Element._id | |
self.typecode = typecode | |
self.size = struct.calcsize(typecode) | |
def __len__(self): | |
return self.size | |
def decode(self, fmt, s): | |
return s | |
def encode(self, fmt, val): | |
return val | |
def __str__(self): | |
return self.typecode | |
def __call__(self, num): | |
# Special case - strings already handled as one blob. | |
if self.typecode in 'sp': | |
# Strings handled specially - one one item | |
return Element("%ds" % num) | |
else: | |
return ArrayElement(self, num) | |
def __getitem__(self, num): | |
"""for convertion x[n] syntax""" | |
return self(num) | |
class ArrayElement(Element): | |
""" | |
.. attribute:: basic_element | |
Basic element | |
.. attribute:: num | |
Number of elements | |
""" | |
def __init__(self, basic_element, num): | |
super(ArrayElement, self).__init__("%ds" % (len(basic_element) * num)) | |
self.num = num | |
self.basic_element = basic_element | |
def decode(self, fmt, s): | |
# We use typecode * size, not %s%s' % (size, typecode), | |
# so we deal with typecodes that already have numbers, | |
# ie 2*'4s' != '24s' | |
return [self.basic_element.decode(fmt, x) for x in | |
struct.unpack('%s%s' % (fmt, | |
self.num * self.basic_element.typecode), | |
s)] | |
def encode(self, fmt, vals): | |
_fmt = fmt + (self.basic_element.typecode * self.num) | |
return struct.pack(_fmt, *[self.basic_element.encode(fmt,v) | |
for v in vals]) | |
class EmbeddedStructElement(Element): | |
def __init__(self, structure): | |
super(EmbeddedStructElement, self).__init__('%ds' % structure._struct_size) | |
self.struct = structure | |
# Note: Structs use their own endianness format, not their parent's | |
def decode(self, fmt, s): | |
return self.struct(s) | |
def encode(self, fmt, s): | |
return self.struct._pack(s) | |
name_to_code = { | |
'Char' : 'c', | |
'Byte' : 'b', | |
'UnsignedByte' : 'B', | |
'Int' : 'i', | |
'UnsignedInt' : 'I', | |
'Short' : 'h', | |
'UnsignedShort' : 'H', | |
'Long' : 'l', | |
'UnsignedLong' : 'L', | |
'String' : 's', | |
'PascalString' : 'p', | |
'Pointer' : 'P', | |
'Float' : 'f', | |
'Double' : 'd', | |
'LongLong' : 'q', | |
'UnsignedLongLong' : 'Q', | |
} | |
class _Type(object): | |
def __getattr__(self, name): | |
if not name in name_to_code: | |
raise ValueError('Type not support %s' % (name)) | |
return Element(name_to_code[name]) | |
def Struct(self, struct): | |
return EmbeddedStructElement(struct) | |
Type = _Type() | |
class MetaStruct(type): | |
""" | |
.. attribute:: _struct_data | |
All elements's typecode, order by it's id | |
.. attribute:: _struct_info | |
[(field_1, element_1), (field_2, element_2) ...] | |
.. attribute:: _struct_size | |
size of struct | |
""" | |
def __init__(cls, name, bases, d): | |
type.__init__(cls, name, bases, d) | |
if hasattr(cls, '_struct_data'): # Allow extending | |
cls._struct_info = list(cls._struct_info) # TODO: use deepcopy | |
else: | |
cls._struct_data = '' | |
cls._struct_info = [] # name / element pairs | |
# Get each Element field, sorted by id. | |
elems = sorted( | |
((k, v) for (k, v) in d.iteritems() if isinstance(v, Element)), | |
key=lambda x: x[1].id | |
) | |
cls._struct_data += ''.join(v.typecode for (k, v) in elems) | |
cls._struct_info += elems | |
cls._struct_size = struct.calcsize(cls._format + cls._struct_data) | |
class Struct(object): | |
"""Represent a binary structure.""" | |
__metaclass__ = MetaStruct | |
_format = Format.Native # Default to native format, native size | |
def __init__(self, _data=None, **kwargs): | |
if _data is None: | |
# zero-ify | |
_data = '\0' * self._struct_size | |
fieldvals = zip(self._struct_info, | |
struct.unpack(self._format + self._struct_data, _data)) | |
for (name, elem), val in fieldvals: | |
setattr(self, name, elem.decode(self._format, val)) | |
for (k, v) in kwargs.iteritems(): | |
setattr(self, k, v) | |
def _pack(self): | |
return struct.pack(self._format + self._struct_data, | |
*[elem.encode(self._format, getattr(self, name)) | |
for (name, elem) in self._struct_info]) | |
def __str__(self): | |
return self._pack() | |
def __repr__(self): | |
return "%s(%r)" % (self.__class__.__name__, self._pack()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment