471 lines
16 KiB
Python
471 lines
16 KiB
Python
|
from .core import Adapter, AdaptationError, Pass
|
||
|
from .lib import int_to_bin, bin_to_int, swap_bytes
|
||
|
from .lib import FlagsContainer, HexString
|
||
|
from .lib.py3compat import BytesIO, decodebytes
|
||
|
|
||
|
|
||
|
#===============================================================================
|
||
|
# exceptions
|
||
|
#===============================================================================
|
||
|
class BitIntegerError(AdaptationError):
|
||
|
__slots__ = []
|
||
|
class MappingError(AdaptationError):
|
||
|
__slots__ = []
|
||
|
class ConstError(AdaptationError):
|
||
|
__slots__ = []
|
||
|
class ValidationError(AdaptationError):
|
||
|
__slots__ = []
|
||
|
class PaddingError(AdaptationError):
|
||
|
__slots__ = []
|
||
|
|
||
|
#===============================================================================
|
||
|
# adapters
|
||
|
#===============================================================================
|
||
|
class BitIntegerAdapter(Adapter):
|
||
|
"""
|
||
|
Adapter for bit-integers (converts bitstrings to integers, and vice versa).
|
||
|
See BitField.
|
||
|
|
||
|
Parameters:
|
||
|
* subcon - the subcon to adapt
|
||
|
* width - the size of the subcon, in bits
|
||
|
* swapped - whether to swap byte order (little endian/big endian).
|
||
|
default is False (big endian)
|
||
|
* signed - whether the value is signed (two's complement). the default
|
||
|
is False (unsigned)
|
||
|
* bytesize - number of bits per byte, used for byte-swapping (if swapped).
|
||
|
default is 8.
|
||
|
"""
|
||
|
__slots__ = ["width", "swapped", "signed", "bytesize"]
|
||
|
def __init__(self, subcon, width, swapped = False, signed = False,
|
||
|
bytesize = 8):
|
||
|
Adapter.__init__(self, subcon)
|
||
|
self.width = width
|
||
|
self.swapped = swapped
|
||
|
self.signed = signed
|
||
|
self.bytesize = bytesize
|
||
|
def _encode(self, obj, context):
|
||
|
if obj < 0 and not self.signed:
|
||
|
raise BitIntegerError("object is negative, but field is not signed",
|
||
|
obj)
|
||
|
obj2 = int_to_bin(obj, width = self.width)
|
||
|
if self.swapped:
|
||
|
obj2 = swap_bytes(obj2, bytesize = self.bytesize)
|
||
|
return obj2
|
||
|
def _decode(self, obj, context):
|
||
|
if self.swapped:
|
||
|
obj = swap_bytes(obj, bytesize = self.bytesize)
|
||
|
return bin_to_int(obj, signed = self.signed)
|
||
|
|
||
|
class MappingAdapter(Adapter):
|
||
|
"""
|
||
|
Adapter that maps objects to other objects.
|
||
|
See SymmetricMapping and Enum.
|
||
|
|
||
|
Parameters:
|
||
|
* subcon - the subcon to map
|
||
|
* decoding - the decoding (parsing) mapping (a dict)
|
||
|
* encoding - the encoding (building) mapping (a dict)
|
||
|
* decdefault - the default return value when the object is not found
|
||
|
in the decoding mapping. if no object is given, an exception is raised.
|
||
|
if `Pass` is used, the unmapped object will be passed as-is
|
||
|
* encdefault - the default return value when the object is not found
|
||
|
in the encoding mapping. if no object is given, an exception is raised.
|
||
|
if `Pass` is used, the unmapped object will be passed as-is
|
||
|
"""
|
||
|
__slots__ = ["encoding", "decoding", "encdefault", "decdefault"]
|
||
|
def __init__(self, subcon, decoding, encoding,
|
||
|
decdefault = NotImplemented, encdefault = NotImplemented):
|
||
|
Adapter.__init__(self, subcon)
|
||
|
self.decoding = decoding
|
||
|
self.encoding = encoding
|
||
|
self.decdefault = decdefault
|
||
|
self.encdefault = encdefault
|
||
|
def _encode(self, obj, context):
|
||
|
try:
|
||
|
return self.encoding[obj]
|
||
|
except (KeyError, TypeError):
|
||
|
if self.encdefault is NotImplemented:
|
||
|
raise MappingError("no encoding mapping for %r [%s]" % (
|
||
|
obj, self.subcon.name))
|
||
|
if self.encdefault is Pass:
|
||
|
return obj
|
||
|
return self.encdefault
|
||
|
def _decode(self, obj, context):
|
||
|
try:
|
||
|
return self.decoding[obj]
|
||
|
except (KeyError, TypeError):
|
||
|
if self.decdefault is NotImplemented:
|
||
|
raise MappingError("no decoding mapping for %r [%s]" % (
|
||
|
obj, self.subcon.name))
|
||
|
if self.decdefault is Pass:
|
||
|
return obj
|
||
|
return self.decdefault
|
||
|
|
||
|
class FlagsAdapter(Adapter):
|
||
|
"""
|
||
|
Adapter for flag fields. Each flag is extracted from the number, resulting
|
||
|
in a FlagsContainer object. Not intended for direct usage.
|
||
|
See FlagsEnum.
|
||
|
|
||
|
Parameters
|
||
|
* subcon - the subcon to extract
|
||
|
* flags - a dictionary mapping flag-names to their value
|
||
|
"""
|
||
|
__slots__ = ["flags"]
|
||
|
def __init__(self, subcon, flags):
|
||
|
Adapter.__init__(self, subcon)
|
||
|
self.flags = flags
|
||
|
def _encode(self, obj, context):
|
||
|
flags = 0
|
||
|
for name, value in self.flags.items():
|
||
|
if getattr(obj, name, False):
|
||
|
flags |= value
|
||
|
return flags
|
||
|
def _decode(self, obj, context):
|
||
|
obj2 = FlagsContainer()
|
||
|
for name, value in self.flags.items():
|
||
|
setattr(obj2, name, bool(obj & value))
|
||
|
return obj2
|
||
|
|
||
|
class StringAdapter(Adapter):
|
||
|
"""
|
||
|
Adapter for strings. Converts a sequence of characters into a python
|
||
|
string, and optionally handles character encoding.
|
||
|
See String.
|
||
|
|
||
|
Parameters:
|
||
|
* subcon - the subcon to convert
|
||
|
* encoding - the character encoding name (e.g., "utf8"), or None to
|
||
|
return raw bytes (usually 8-bit ASCII).
|
||
|
"""
|
||
|
__slots__ = ["encoding"]
|
||
|
def __init__(self, subcon, encoding = None):
|
||
|
Adapter.__init__(self, subcon)
|
||
|
self.encoding = encoding
|
||
|
def _encode(self, obj, context):
|
||
|
if self.encoding:
|
||
|
obj = obj.encode(self.encoding)
|
||
|
return obj
|
||
|
def _decode(self, obj, context):
|
||
|
if self.encoding:
|
||
|
obj = obj.decode(self.encoding)
|
||
|
return obj
|
||
|
|
||
|
class PaddedStringAdapter(Adapter):
|
||
|
r"""
|
||
|
Adapter for padded strings.
|
||
|
See String.
|
||
|
|
||
|
Parameters:
|
||
|
* subcon - the subcon to adapt
|
||
|
* padchar - the padding character. default is b"\x00".
|
||
|
* paddir - the direction where padding is placed ("right", "left", or
|
||
|
"center"). the default is "right".
|
||
|
* trimdir - the direction where trimming will take place ("right" or
|
||
|
"left"). the default is "right". trimming is only meaningful for
|
||
|
building, when the given string is too long.
|
||
|
"""
|
||
|
__slots__ = ["padchar", "paddir", "trimdir"]
|
||
|
def __init__(self, subcon, padchar = b"\x00", paddir = "right",
|
||
|
trimdir = "right"):
|
||
|
if paddir not in ("right", "left", "center"):
|
||
|
raise ValueError("paddir must be 'right', 'left' or 'center'",
|
||
|
paddir)
|
||
|
if trimdir not in ("right", "left"):
|
||
|
raise ValueError("trimdir must be 'right' or 'left'", trimdir)
|
||
|
Adapter.__init__(self, subcon)
|
||
|
self.padchar = padchar
|
||
|
self.paddir = paddir
|
||
|
self.trimdir = trimdir
|
||
|
def _decode(self, obj, context):
|
||
|
if self.paddir == "right":
|
||
|
obj = obj.rstrip(self.padchar)
|
||
|
elif self.paddir == "left":
|
||
|
obj = obj.lstrip(self.padchar)
|
||
|
else:
|
||
|
obj = obj.strip(self.padchar)
|
||
|
return obj
|
||
|
def _encode(self, obj, context):
|
||
|
size = self._sizeof(context)
|
||
|
if self.paddir == "right":
|
||
|
obj = obj.ljust(size, self.padchar)
|
||
|
elif self.paddir == "left":
|
||
|
obj = obj.rjust(size, self.padchar)
|
||
|
else:
|
||
|
obj = obj.center(size, self.padchar)
|
||
|
if len(obj) > size:
|
||
|
if self.trimdir == "right":
|
||
|
obj = obj[:size]
|
||
|
else:
|
||
|
obj = obj[-size:]
|
||
|
return obj
|
||
|
|
||
|
class LengthValueAdapter(Adapter):
|
||
|
"""
|
||
|
Adapter for length-value pairs. It extracts only the value from the
|
||
|
pair, and calculates the length based on the value.
|
||
|
See PrefixedArray and PascalString.
|
||
|
|
||
|
Parameters:
|
||
|
* subcon - the subcon returning a length-value pair
|
||
|
"""
|
||
|
__slots__ = []
|
||
|
def _encode(self, obj, context):
|
||
|
return (len(obj), obj)
|
||
|
def _decode(self, obj, context):
|
||
|
return obj[1]
|
||
|
|
||
|
class CStringAdapter(StringAdapter):
|
||
|
r"""
|
||
|
Adapter for C-style strings (strings terminated by a terminator char).
|
||
|
|
||
|
Parameters:
|
||
|
* subcon - the subcon to convert
|
||
|
* terminators - a sequence of terminator chars. default is b"\x00".
|
||
|
* encoding - the character encoding to use (e.g., "utf8"), or None to
|
||
|
return raw-bytes. the terminator characters are not affected by the
|
||
|
encoding.
|
||
|
"""
|
||
|
__slots__ = ["terminators"]
|
||
|
def __init__(self, subcon, terminators = b"\x00", encoding = None):
|
||
|
StringAdapter.__init__(self, subcon, encoding = encoding)
|
||
|
self.terminators = terminators
|
||
|
def _encode(self, obj, context):
|
||
|
return StringAdapter._encode(self, obj, context) + self.terminators[0:1]
|
||
|
def _decode(self, obj, context):
|
||
|
return StringAdapter._decode(self, b''.join(obj[:-1]), context)
|
||
|
|
||
|
class TunnelAdapter(Adapter):
|
||
|
"""
|
||
|
Adapter for tunneling (as in protocol tunneling). A tunnel is construct
|
||
|
nested upon another (layering). For parsing, the lower layer first parses
|
||
|
the data (note: it must return a string!), then the upper layer is called
|
||
|
to parse that data (bottom-up). For building it works in a top-down manner;
|
||
|
first the upper layer builds the data, then the lower layer takes it and
|
||
|
writes it to the stream.
|
||
|
|
||
|
Parameters:
|
||
|
* subcon - the lower layer subcon
|
||
|
* inner_subcon - the upper layer (tunneled/nested) subcon
|
||
|
|
||
|
Example:
|
||
|
# a pascal string containing compressed data (zlib encoding), so first
|
||
|
# the string is read, decompressed, and finally re-parsed as an array
|
||
|
# of UBInt16
|
||
|
TunnelAdapter(
|
||
|
PascalString("data", encoding = "zlib"),
|
||
|
GreedyRange(UBInt16("elements"))
|
||
|
)
|
||
|
"""
|
||
|
__slots__ = ["inner_subcon"]
|
||
|
def __init__(self, subcon, inner_subcon):
|
||
|
Adapter.__init__(self, subcon)
|
||
|
self.inner_subcon = inner_subcon
|
||
|
def _decode(self, obj, context):
|
||
|
return self.inner_subcon._parse(BytesIO(obj), context)
|
||
|
def _encode(self, obj, context):
|
||
|
stream = BytesIO()
|
||
|
self.inner_subcon._build(obj, stream, context)
|
||
|
return stream.getvalue()
|
||
|
|
||
|
class ExprAdapter(Adapter):
|
||
|
"""
|
||
|
A generic adapter that accepts 'encoder' and 'decoder' as parameters. You
|
||
|
can use ExprAdapter instead of writing a full-blown class when only a
|
||
|
simple expression is needed.
|
||
|
|
||
|
Parameters:
|
||
|
* subcon - the subcon to adapt
|
||
|
* encoder - a function that takes (obj, context) and returns an encoded
|
||
|
version of obj
|
||
|
* decoder - a function that takes (obj, context) and returns a decoded
|
||
|
version of obj
|
||
|
|
||
|
Example:
|
||
|
ExprAdapter(UBInt8("foo"),
|
||
|
encoder = lambda obj, ctx: obj / 4,
|
||
|
decoder = lambda obj, ctx: obj * 4,
|
||
|
)
|
||
|
"""
|
||
|
__slots__ = ["_encode", "_decode"]
|
||
|
def __init__(self, subcon, encoder, decoder):
|
||
|
Adapter.__init__(self, subcon)
|
||
|
self._encode = encoder
|
||
|
self._decode = decoder
|
||
|
|
||
|
class HexDumpAdapter(Adapter):
|
||
|
"""
|
||
|
Adapter for hex-dumping strings. It returns a HexString, which is a string
|
||
|
"""
|
||
|
__slots__ = ["linesize"]
|
||
|
def __init__(self, subcon, linesize = 16):
|
||
|
Adapter.__init__(self, subcon)
|
||
|
self.linesize = linesize
|
||
|
def _encode(self, obj, context):
|
||
|
return obj
|
||
|
def _decode(self, obj, context):
|
||
|
return HexString(obj, linesize = self.linesize)
|
||
|
|
||
|
class ConstAdapter(Adapter):
|
||
|
"""
|
||
|
Adapter for enforcing a constant value ("magic numbers"). When decoding,
|
||
|
the return value is checked; when building, the value is substituted in.
|
||
|
|
||
|
Parameters:
|
||
|
* subcon - the subcon to validate
|
||
|
* value - the expected value
|
||
|
|
||
|
Example:
|
||
|
Const(Field("signature", 2), "MZ")
|
||
|
"""
|
||
|
__slots__ = ["value"]
|
||
|
def __init__(self, subcon, value):
|
||
|
Adapter.__init__(self, subcon)
|
||
|
self.value = value
|
||
|
def _encode(self, obj, context):
|
||
|
if obj is None or obj == self.value:
|
||
|
return self.value
|
||
|
else:
|
||
|
raise ConstError("expected %r, found %r" % (self.value, obj))
|
||
|
def _decode(self, obj, context):
|
||
|
if obj != self.value:
|
||
|
raise ConstError("expected %r, found %r" % (self.value, obj))
|
||
|
return obj
|
||
|
|
||
|
class SlicingAdapter(Adapter):
|
||
|
"""
|
||
|
Adapter for slicing a list (getting a slice from that list)
|
||
|
|
||
|
Parameters:
|
||
|
* subcon - the subcon to slice
|
||
|
* start - start index
|
||
|
* stop - stop index (or None for up-to-end)
|
||
|
* step - step (or None for every element)
|
||
|
"""
|
||
|
__slots__ = ["start", "stop", "step"]
|
||
|
def __init__(self, subcon, start, stop = None):
|
||
|
Adapter.__init__(self, subcon)
|
||
|
self.start = start
|
||
|
self.stop = stop
|
||
|
def _encode(self, obj, context):
|
||
|
if self.start is None:
|
||
|
return obj
|
||
|
return [None] * self.start + obj
|
||
|
def _decode(self, obj, context):
|
||
|
return obj[self.start:self.stop]
|
||
|
|
||
|
class IndexingAdapter(Adapter):
|
||
|
"""
|
||
|
Adapter for indexing a list (getting a single item from that list)
|
||
|
|
||
|
Parameters:
|
||
|
* subcon - the subcon to index
|
||
|
* index - the index of the list to get
|
||
|
"""
|
||
|
__slots__ = ["index"]
|
||
|
def __init__(self, subcon, index):
|
||
|
Adapter.__init__(self, subcon)
|
||
|
if type(index) is not int:
|
||
|
raise TypeError("index must be an integer", type(index))
|
||
|
self.index = index
|
||
|
def _encode(self, obj, context):
|
||
|
return [None] * self.index + [obj]
|
||
|
def _decode(self, obj, context):
|
||
|
return obj[self.index]
|
||
|
|
||
|
class PaddingAdapter(Adapter):
|
||
|
r"""
|
||
|
Adapter for padding.
|
||
|
|
||
|
Parameters:
|
||
|
* subcon - the subcon to pad
|
||
|
* pattern - the padding pattern (character as byte). default is b"\x00"
|
||
|
* strict - whether or not to verify, during parsing, that the given
|
||
|
padding matches the padding pattern. default is False (unstrict)
|
||
|
"""
|
||
|
__slots__ = ["pattern", "strict"]
|
||
|
def __init__(self, subcon, pattern = b"\x00", strict = False):
|
||
|
Adapter.__init__(self, subcon)
|
||
|
self.pattern = pattern
|
||
|
self.strict = strict
|
||
|
def _encode(self, obj, context):
|
||
|
return self._sizeof(context) * self.pattern
|
||
|
def _decode(self, obj, context):
|
||
|
if self.strict:
|
||
|
expected = self._sizeof(context) * self.pattern
|
||
|
if obj != expected:
|
||
|
raise PaddingError("expected %r, found %r" % (expected, obj))
|
||
|
return obj
|
||
|
|
||
|
|
||
|
#===============================================================================
|
||
|
# validators
|
||
|
#===============================================================================
|
||
|
class Validator(Adapter):
|
||
|
"""
|
||
|
Abstract class: validates a condition on the encoded/decoded object.
|
||
|
Override _validate(obj, context) in deriving classes.
|
||
|
|
||
|
Parameters:
|
||
|
* subcon - the subcon to validate
|
||
|
"""
|
||
|
__slots__ = []
|
||
|
def _decode(self, obj, context):
|
||
|
if not self._validate(obj, context):
|
||
|
raise ValidationError("invalid object", obj)
|
||
|
return obj
|
||
|
def _encode(self, obj, context):
|
||
|
return self._decode(obj, context)
|
||
|
def _validate(self, obj, context):
|
||
|
raise NotImplementedError()
|
||
|
|
||
|
class OneOf(Validator):
|
||
|
"""
|
||
|
Validates that the object is one of the listed values.
|
||
|
|
||
|
:param ``Construct`` subcon: object to validate
|
||
|
:param iterable valids: a set of valid values
|
||
|
|
||
|
>>> OneOf(UBInt8("foo"), [4,5,6,7]).parse("\\x05")
|
||
|
5
|
||
|
>>> OneOf(UBInt8("foo"), [4,5,6,7]).parse("\\x08")
|
||
|
Traceback (most recent call last):
|
||
|
...
|
||
|
construct.core.ValidationError: ('invalid object', 8)
|
||
|
>>>
|
||
|
>>> OneOf(UBInt8("foo"), [4,5,6,7]).build(5)
|
||
|
'\\x05'
|
||
|
>>> OneOf(UBInt8("foo"), [4,5,6,7]).build(9)
|
||
|
Traceback (most recent call last):
|
||
|
...
|
||
|
construct.core.ValidationError: ('invalid object', 9)
|
||
|
"""
|
||
|
__slots__ = ["valids"]
|
||
|
def __init__(self, subcon, valids):
|
||
|
Validator.__init__(self, subcon)
|
||
|
self.valids = valids
|
||
|
def _validate(self, obj, context):
|
||
|
return obj in self.valids
|
||
|
|
||
|
class NoneOf(Validator):
|
||
|
"""
|
||
|
Validates that the object is none of the listed values.
|
||
|
|
||
|
:param ``Construct`` subcon: object to validate
|
||
|
:param iterable invalids: a set of invalid values
|
||
|
|
||
|
>>> NoneOf(UBInt8("foo"), [4,5,6,7]).parse("\\x08")
|
||
|
8
|
||
|
>>> NoneOf(UBInt8("foo"), [4,5,6,7]).parse("\\x06")
|
||
|
Traceback (most recent call last):
|
||
|
...
|
||
|
construct.core.ValidationError: ('invalid object', 6)
|
||
|
"""
|
||
|
__slots__ = ["invalids"]
|
||
|
def __init__(self, subcon, invalids):
|
||
|
Validator.__init__(self, subcon)
|
||
|
self.invalids = invalids
|
||
|
def _validate(self, obj, context):
|
||
|
return obj not in self.invalids
|