Skip to content

Instantly share code, notes, and snippets.

@yxy
Created April 28, 2015 14:30
Show Gist options
  • Save yxy/5aa98c3e42507fe3ace8 to your computer and use it in GitHub Desktop.
Save yxy/5aa98c3e42507fe3ace8 to your computer and use it in GitHub Desktop.
This recipe provides a higher level wrapper around the struct module.
# 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