# -*- coding: utf-8 -*-
"""This module contains enough knowledge about `struct` format strings to
figure out size and position of fields.
"""
import re
import struct
from struct import Struct
from .utils import split_fields
[docs]class Field(object):
"""Representation of a field defined in a `struct` format string.
(should only be created from :class:`Layout`.
"""
def __init__(self, **kw):
# defaults
self.name = ""
self.index = 0
self.position = 0
self.layout = ""
self.count = 0
self.format = 'x'
self.type = ""
self.size = 0
# should padding bytes be stripped when pretty printing
self.strip = None
self.__dict__.update(kw)
if self.strip is None:
self.strip = self.format == 's'
[docs] def get_value(self, data):
"Get this field's value from `data`."
rec = struct.unpack_from(
'=' + self.layout,
str(data), self.position
)
if self.format == 's' and self.strip:
return rec[0].rstrip('\0')
return rec[0]
[docs] def set_value(self, data, value):
"Set this field to `value` in `data`."
struct.pack_into('=' + self.layout, data, self.position, value)
return value
def __repr__(self):
return '<Field:%s [%d] "%s" count=%d format=%r type="%s" size=%d>' % (
self.name,
self.index,
self.layout,
self.count,
self.format,
self.type,
self.size
)
def __str__(self):
return "#%-2d %14s %5s = %d" % (self.index, self.name, self.layout, self.size)
def __len__(self):
return self.size
[docs]class Layout(object):
"""Record layout.
Usage::
record_layout = Layout(
'=12x?3sQ16s16s68s128sHcc',
'pad',
'local',
'rectype',
'timestamp',
'salt',
'digest',
'key',
'data',
'chksum',
'cr',
'nl',
name="Record"
)
"""
#: types corresponding to struct character codes
struct_field_types = {
'x': "pad byte",
'c': "char",
'b': "signed char",
'B': "unsigned char",
'?': "_Bool",
'h': "short",
'H': "unsigned short",
'i': "int",
'I': "unsigned int",
'l': "long",
'L': "unsigned long",
'q': "long long",
'Q': "unsigned long long",
'n': "ssize_t",
'N': "size_t",
'f': "float",
'd': "double",
's': "char[]",
'p': "char[]",
'P': "void *",
}
#: byte sizes corresponding to struct character codes
struct_field_sizes = {
'x': 1,
'c': 1,
'b': 1,
'B': 1,
'?': 1,
'h': 2,
'H': 2,
'i': 4,
'I': 4,
'l': 4,
'L': 4,
'q': 8,
'Q': 8,
'n': 8,
'N': 4,
'f': 4,
'd': 8,
's': 1,
'p': 1,
'P': 4,
}
#: legal struct format string record prefixes
record_prefix = {
'@': "native aligned",
'=': "native",
'<': "little-endian",
'>': "big-endian",
'!': "network-endian",
}
#: regex matching one field in a struct format string
layoutre = re.compile(r'''
(?P<field>\d*[xcbB?hHiIlLqQnNfdspP])
''', re.VERBOSE)
def __init__(self, layout, *names, **kw):
self.name = kw.get('name')
self.names = names
self.layout = layout
self.struct = Struct(layout)
if not layout:
raise ValueError("illegal layout")
if layout[0] in self.record_prefix:
prefix = layout[0]
layout = layout[1:]
else:
prefix = '@'
fields = [] # get field by position
self._field = {} # get field by name
pos = 0
for i, field in enumerate(self.layoutre.findall(layout)):
count = int(field[:-1] or '0', 10)
fmtch = field[-1]
f = Field(
name=names[i] if names else "",
index=i,
layout=field,
count=count,
format=fmtch,
type=self.struct_field_types[fmtch],
size=max(1, count) * self.struct_field_sizes[fmtch],
position=pos
)
pos += len(f)
fields.append(f)
if f.name:
self._field[f.name] = f
self.prefix = prefix
self.fields = fields
def __getitem__(self, key):
"""Get field `key`, where key is either the position of the field
or its name.
"""
if isinstance(key, int):
return self.fields[key]
else:
return self._field[key]
def __contains__(self, fieldname):
"""Is there a field named `fieldname`?
"""
return fieldname in self._field
def __len__(self):
"""The length of the record is the sum of all fields.
.. todo: This is obviously not correct for any prefix other than '='
since it doesn't take padding into account.
"""
return sum(f.size for f in self.fields)
def __repr__(self):
return '{Layout(%d) "%s" %r}' % (len(self), self.prefix, self.fields)
def __str__(self):
# res = self.name + "[%d] (%s):\n" % (self.index, self.layout)
res = 'Layout for %r: (len=%d)\n' % (self.name, len(self))
for f in self.fields:
res += ' @%4d:' % f.position + str(f) + '\n'
return res
[docs] def split(self, data):
"""Split the byte string `data` into a list of substrings holding the
data for each field.
"""
llen = len(self)
dlen = len(data)
if dlen != llen:
msg = "Data (%d) doesn't match layout (%s) length (%d): %r" % (
dlen, self.layout, llen, data)
raise ValueError(msg)
return split_fields(data, [f.size for f in self.fields])