From 9a13903a2f7d3a65fdf15a65fb59cccd622e2066 Mon Sep 17 00:00:00 2001 From: Livio Recchia Date: Mon, 10 Feb 2020 23:06:34 +0100 Subject: Initial commit --- external/construct/macros.py | 676 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 676 insertions(+) create mode 100644 external/construct/macros.py (limited to 'external/construct/macros.py') diff --git a/external/construct/macros.py b/external/construct/macros.py new file mode 100644 index 0000000..bbcbd33 --- /dev/null +++ b/external/construct/macros.py @@ -0,0 +1,676 @@ +import six +from construct.lib.py3compat import int2byte +from construct.lib import (BitStreamReader, BitStreamWriter, encode_bin, decode_bin) +from construct.core import (Struct, MetaField, StaticField, FormatField, + OnDemand, Pointer, Switch, Value, RepeatUntil, MetaArray, Sequence, Range, + Select, Pass, SizeofError, Buffered, Restream, Reconfig) +from construct.adapters import (BitIntegerAdapter, PaddingAdapter, + ConstAdapter, CStringAdapter, LengthValueAdapter, IndexingAdapter, + PaddedStringAdapter, FlagsAdapter, StringAdapter, MappingAdapter) +try: + from sys import maxsize +except ImportError: + from sys import maxint as maxsize + + +#=============================================================================== +# fields +#=============================================================================== +def Field(name, length): + """ + A field consisting of a specified number of bytes. + + :param name: the name of the field + :param length: the length of the field. the length can be either an integer + (StaticField), or a function that takes the context as an argument and + returns the length (MetaField) + """ + if callable(length): + return MetaField(name, length) + else: + return StaticField(name, length) + +def BitField(name, length, swapped = False, signed = False, bytesize = 8): + r""" + BitFields, as the name suggests, are fields that operate on raw, unaligned + bits, and therefore must be enclosed in a BitStruct. Using them is very + similar to all normal fields: they take a name and a length (in bits). + + :param name: name of the field + :param length: number of bits in the field, or a function that takes + the context as its argument and returns the length + :param swapped: whether the value is byte-swapped + :param signed: whether the value is signed + :param bytesize: number of bits per byte, for byte-swapping + + Example:: + + >>> foo = BitStruct("foo", + ... BitField("a", 3), + ... Flag("b"), + ... Padding(3), + ... Nibble("c"), + ... BitField("d", 5), + ... ) + >>> foo.parse("\xe1\x1f") + Container(a = 7, b = False, c = 8, d = 31) + >>> foo = BitStruct("foo", + ... BitField("a", 3), + ... Flag("b"), + ... Padding(3), + ... Nibble("c"), + ... Struct("bar", + ... Nibble("d"), + ... Bit("e"), + ... ) + ... ) + >>> foo.parse("\xe1\x1f") + Container(a = 7, b = False, bar = Container(d = 15, e = 1), c = 8) + """ + + return BitIntegerAdapter(Field(name, length), + length, + swapped=swapped, + signed=signed, + bytesize=bytesize + ) + +def Padding(length, pattern = six.b("\x00"), strict = False): + r"""A padding field (value is discarded) + + :param length: the length of the field. the length can be either an integer, + or a function that takes the context as an argument and returns the length + :param pattern: the padding pattern (character) to use. default is "\x00" + :param strict: whether or not to raise an exception is the actual padding + pattern mismatches the desired pattern. default is False. + """ + return PaddingAdapter(Field(None, length), + pattern = pattern, + strict = strict, + ) + +def Flag(name, truth = 1, falsehood = 0, default = False): + """ + A flag. + + Flags are usually used to signify a Boolean value, and this construct + maps values onto the ``bool`` type. + + .. note:: This construct works with both bit and byte contexts. + + .. warning:: Flags default to False, not True. This is different from the + C and Python way of thinking about truth, and may be subject to change + in the future. + + :param name: field name + :param truth: value of truth (default 1) + :param falsehood: value of falsehood (default 0) + :param default: default value (default False) + """ + + return SymmetricMapping(Field(name, 1), + {True : int2byte(truth), False : int2byte(falsehood)}, + default = default, + ) + +#=============================================================================== +# field shortcuts +#=============================================================================== +def Bit(name): + """A 1-bit BitField; must be enclosed in a BitStruct""" + return BitField(name, 1) +def Nibble(name): + """A 4-bit BitField; must be enclosed in a BitStruct""" + return BitField(name, 4) +def Octet(name): + """An 8-bit BitField; must be enclosed in a BitStruct""" + return BitField(name, 8) + +def UBInt8(name): + """Unsigned, big endian 8-bit integer""" + return FormatField(name, ">", "B") +def UBInt16(name): + """Unsigned, big endian 16-bit integer""" + return FormatField(name, ">", "H") +def UBInt32(name): + """Unsigned, big endian 32-bit integer""" + return FormatField(name, ">", "L") +def UBInt64(name): + """Unsigned, big endian 64-bit integer""" + return FormatField(name, ">", "Q") + +def SBInt8(name): + """Signed, big endian 8-bit integer""" + return FormatField(name, ">", "b") +def SBInt16(name): + """Signed, big endian 16-bit integer""" + return FormatField(name, ">", "h") +def SBInt32(name): + """Signed, big endian 32-bit integer""" + return FormatField(name, ">", "l") +def SBInt64(name): + """Signed, big endian 64-bit integer""" + return FormatField(name, ">", "q") + +def ULInt8(name): + """Unsigned, little endian 8-bit integer""" + return FormatField(name, "<", "B") +def ULInt16(name): + """Unsigned, little endian 16-bit integer""" + return FormatField(name, "<", "H") +def ULInt32(name): + """Unsigned, little endian 32-bit integer""" + return FormatField(name, "<", "L") +def ULInt64(name): + """Unsigned, little endian 64-bit integer""" + return FormatField(name, "<", "Q") + +def SLInt8(name): + """Signed, little endian 8-bit integer""" + return FormatField(name, "<", "b") +def SLInt16(name): + """Signed, little endian 16-bit integer""" + return FormatField(name, "<", "h") +def SLInt32(name): + """Signed, little endian 32-bit integer""" + return FormatField(name, "<", "l") +def SLInt64(name): + """Signed, little endian 64-bit integer""" + return FormatField(name, "<", "q") + +def UNInt8(name): + """Unsigned, native endianity 8-bit integer""" + return FormatField(name, "=", "B") +def UNInt16(name): + """Unsigned, native endianity 16-bit integer""" + return FormatField(name, "=", "H") +def UNInt32(name): + """Unsigned, native endianity 32-bit integer""" + return FormatField(name, "=", "L") +def UNInt64(name): + """Unsigned, native endianity 64-bit integer""" + return FormatField(name, "=", "Q") + +def SNInt8(name): + """Signed, native endianity 8-bit integer""" + return FormatField(name, "=", "b") +def SNInt16(name): + """Signed, native endianity 16-bit integer""" + return FormatField(name, "=", "h") +def SNInt32(name): + """Signed, native endianity 32-bit integer""" + return FormatField(name, "=", "l") +def SNInt64(name): + """Signed, native endianity 64-bit integer""" + return FormatField(name, "=", "q") + +def BFloat32(name): + """Big endian, 32-bit IEEE floating point number""" + return FormatField(name, ">", "f") +def LFloat32(name): + """Little endian, 32-bit IEEE floating point number""" + return FormatField(name, "<", "f") +def NFloat32(name): + """Native endianity, 32-bit IEEE floating point number""" + return FormatField(name, "=", "f") + +def BFloat64(name): + """Big endian, 64-bit IEEE floating point number""" + return FormatField(name, ">", "d") +def LFloat64(name): + """Little endian, 64-bit IEEE floating point number""" + return FormatField(name, "<", "d") +def NFloat64(name): + """Native endianity, 64-bit IEEE floating point number""" + return FormatField(name, "=", "d") + + +#=============================================================================== +# arrays +#=============================================================================== +def Array(count, subcon): + r""" + Repeats the given unit a fixed number of times. + + :param count: number of times to repeat + :param subcon: construct to repeat + + Example:: + + >>> c = Array(4, UBInt8("foo")) + >>> c.parse("\x01\x02\x03\x04") + [1, 2, 3, 4] + >>> c.parse("\x01\x02\x03\x04\x05\x06") + [1, 2, 3, 4] + >>> c.build([5,6,7,8]) + '\x05\x06\x07\x08' + >>> c.build([5,6,7,8,9]) + Traceback (most recent call last): + ... + construct.core.RangeError: expected 4..4, found 5 + """ + + if callable(count): + con = MetaArray(count, subcon) + else: + con = MetaArray(lambda ctx: count, subcon) + con._clear_flag(con.FLAG_DYNAMIC) + return con + +def PrefixedArray(subcon, length_field = UBInt8("length")): + """An array prefixed by a length field. + + :param subcon: the subcon to be repeated + :param length_field: a construct returning an integer + """ + def _length(ctx): + if issubclass(ctx.__class__, (list, tuple)): + return len(ctx) + return ctx[length_field.name] + + return LengthValueAdapter( + Sequence(subcon.name, + length_field, + Array(_length, subcon), + nested = False + ) + ) + +def OpenRange(mincount, subcon): + return Range(mincount, maxsize, subcon) + +def GreedyRange(subcon): + r""" + Repeats the given unit one or more times. + + :param subcon: construct to repeat + + Example:: + + >>> from construct import GreedyRange, UBInt8 + >>> c = GreedyRange(UBInt8("foo")) + >>> c.parse("\x01") + [1] + >>> c.parse("\x01\x02\x03") + [1, 2, 3] + >>> c.parse("\x01\x02\x03\x04\x05\x06") + [1, 2, 3, 4, 5, 6] + >>> c.parse("") + Traceback (most recent call last): + ... + construct.core.RangeError: expected 1..2147483647, found 0 + >>> c.build([1,2]) + '\x01\x02' + >>> c.build([]) + Traceback (most recent call last): + ... + construct.core.RangeError: expected 1..2147483647, found 0 + """ + return OpenRange(1, subcon) + +def OptionalGreedyRange(subcon): + r""" + Repeats the given unit zero or more times. This repeater can't + fail, as it accepts lists of any length. + + :param subcon: construct to repeat + + Example:: + + >>> from construct import OptionalGreedyRange, UBInt8 + >>> c = OptionalGreedyRange(UBInt8("foo")) + >>> c.parse("") + [] + >>> c.parse("\x01\x02") + [1, 2] + >>> c.build([]) + '' + >>> c.build([1,2]) + '\x01\x02' + """ + return OpenRange(0, subcon) + + +#=============================================================================== +# subconstructs +#=============================================================================== +def Optional(subcon): + """An optional construct. if parsing fails, returns None. + + :param subcon: the subcon to optionally parse or build + """ + return Select(subcon.name, subcon, Pass) + +def Bitwise(subcon): + """Converts the stream to bits, and passes the bitstream to subcon + + :param subcon: a bitwise construct (usually BitField) + """ + # subcons larger than MAX_BUFFER will be wrapped by Restream instead + # of Buffered. implementation details, don't stick your nose in :) + MAX_BUFFER = 1024 * 8 + def resizer(length): + if length & 7: + raise SizeofError("size must be a multiple of 8", length) + return length >> 3 + if not subcon._is_flag(subcon.FLAG_DYNAMIC) and subcon.sizeof() < MAX_BUFFER: + con = Buffered(subcon, + encoder = decode_bin, + decoder = encode_bin, + resizer = resizer + ) + else: + con = Restream(subcon, + stream_reader = BitStreamReader, + stream_writer = BitStreamWriter, + resizer = resizer) + return con + +def Aligned(subcon, modulus = 4, pattern = six.b("\x00")): + r"""Aligns subcon to modulus boundary using padding pattern + + :param subcon: the subcon to align + :param modulus: the modulus boundary (default is 4) + :param pattern: the padding pattern (default is \x00) + """ + if modulus < 2: + raise ValueError("modulus must be >= 2", modulus) + def padlength(ctx): + return (modulus - (subcon._sizeof(ctx) % modulus)) % modulus + return SeqOfOne(subcon.name, + subcon, + # ?????? + # ?????? + # ?????? + # ?????? + Padding(padlength, pattern = pattern), + nested = False, + ) + +def SeqOfOne(name, *args, **kw): + r"""A sequence of one element. only the first element is meaningful, the + rest are discarded + + :param name: the name of the sequence + :param \*args: subconstructs + :param \*\*kw: any keyword arguments to Sequence + """ + return IndexingAdapter(Sequence(name, *args, **kw), index = 0) + +def Embedded(subcon): + """Embeds a struct into the enclosing struct. + + :param subcon: the struct to embed + """ + return Reconfig(subcon.name, subcon, subcon.FLAG_EMBED) + +def Rename(newname, subcon): + """Renames an existing construct + + :param newname: the new name + :param subcon: the subcon to rename + """ + return Reconfig(newname, subcon) + +def Alias(newname, oldname): + """Creates an alias for an existing element in a struct + + :param newname: the new name + :param oldname: the name of an existing element + """ + return Value(newname, lambda ctx: ctx[oldname]) + + +#=============================================================================== +# mapping +#=============================================================================== +def SymmetricMapping(subcon, mapping, default = NotImplemented): + """Defines a symmetrical mapping: a->b, b->a. + + :param subcon: the subcon to map + :param mapping: the encoding mapping (a dict); the decoding mapping is + achieved by reversing this mapping + :param default: the default value to use when no mapping is found. if no + default value is given, and exception is raised. setting to Pass would + return the value "as is" (unmapped) + """ + reversed_mapping = dict((v, k) for k, v in mapping.items()) + return MappingAdapter(subcon, + encoding = mapping, + decoding = reversed_mapping, + encdefault = default, + decdefault = default, + ) + +def Enum(subcon, **kw): + r"""A set of named values mapping. + + :param subcon: the subcon to map + :param \*\*kw: keyword arguments which serve as the encoding mapping + :param _default_: an optional, keyword-only argument that specifies the + default value to use when the mapping is undefined. if not given, + and exception is raised when the mapping is undefined. use `Pass` to + pass the unmapped value as-is + """ + return SymmetricMapping(subcon, kw, kw.pop("_default_", NotImplemented)) + +def FlagsEnum(subcon, **kw): + r"""A set of flag values mapping. + + :param subcon: the subcon to map + :param \*\*kw: keyword arguments which serve as the encoding mapping + """ + return FlagsAdapter(subcon, kw) + + +#=============================================================================== +# structs +#=============================================================================== +def AlignedStruct(name, *subcons, **kw): + r"""A struct of aligned fields + + :param name: the name of the struct + :param \*subcons: the subcons that make up this structure + :param \*\*kw: keyword arguments to pass to Aligned: 'modulus' and 'pattern' + """ + return Struct(name, *(Aligned(sc, **kw) for sc in subcons)) + +def BitStruct(name, *subcons): + r"""A struct of bitwise fields + + :param name: the name of the struct + :param \*subcons: the subcons that make up this structure + """ + return Bitwise(Struct(name, *subcons)) + +def EmbeddedBitStruct(*subcons): + r"""An embedded BitStruct. no name is necessary. + + :param \*subcons: the subcons that make up this structure + """ + return Bitwise(Embedded(Struct(None, *subcons))) + +#=============================================================================== +# strings +#=============================================================================== +def String(name, length, encoding=None, padchar=None, paddir="right", + trimdir="right"): + r""" + A configurable, fixed-length string field. + + The padding character must be specified for padding and trimming to work. + + :param name: name + :param length: length, in bytes + :param encoding: encoding (e.g. "utf8") or None for no encoding + :param padchar: optional character to pad out strings + :param paddir: direction to pad out strings; one of "right", "left", or "both" + :param str trim: direction to trim strings; one of "right", "left" + + Example:: + + >>> from construct import String + >>> String("foo", 5).parse("hello") + 'hello' + >>> + >>> String("foo", 12, encoding = "utf8").parse("hello joh\xd4\x83n") + u'hello joh\u0503n' + >>> + >>> foo = String("foo", 10, padchar = "X", paddir = "right") + >>> foo.parse("helloXXXXX") + 'hello' + >>> foo.build("hello") + 'helloXXXXX' + """ + con = StringAdapter(Field(name, length), encoding=encoding) + if padchar is not None: + con = PaddedStringAdapter(con, padchar=padchar, paddir=paddir, + trimdir=trimdir) + return con + +def PascalString(name, length_field=UBInt8("length"), encoding=None): + r""" + A length-prefixed string. + + ``PascalString`` is named after the string types of Pascal, which are + length-prefixed. Lisp strings also follow this convention. + + The length field will appear in the same ``Container`` as the + ``PascalString``, with the given name. + + :param name: name + :param length_field: a field which will store the length of the string + :param encoding: encoding (e.g. "utf8") or None for no encoding + + Example:: + + >>> foo = PascalString("foo") + >>> foo.parse("\x05hello") + 'hello' + >>> foo.build("hello world") + '\x0bhello world' + >>> + >>> foo = PascalString("foo", length_field = UBInt16("length")) + >>> foo.parse("\x00\x05hello") + 'hello' + >>> foo.build("hello") + '\x00\x05hello' + """ + + return StringAdapter( + LengthValueAdapter( + Sequence(name, + length_field, + Field("data", lambda ctx: ctx[length_field.name]), + ) + ), + encoding=encoding, + ) + +def CString(name, terminators=six.b("\x00"), encoding=None, + char_field=Field(None, 1)): + r""" + A string ending in a terminator. + + ``CString`` is similar to the strings of C, C++, and other related + programming languages. + + By default, the terminator is the NULL byte (b``0x00``). + + :param name: name + :param terminators: sequence of valid terminators, in order of preference + :param encoding: encoding (e.g. "utf8") or None for no encoding + :param char_field: construct representing a single character + + Example:: + + >>> foo = CString("foo") + >>> foo.parse(b"hello\x00") + b'hello' + >>> foo.build(b"hello") + b'hello\x00' + >>> foo = CString("foo", terminators = b"XYZ") + >>> foo.parse(b"helloX") + b'hello' + >>> foo.parse(b"helloY") + b'hello' + >>> foo.parse(b"helloZ") + b'hello' + >>> foo.build(b"hello") + b'helloX' + """ + + return Rename(name, + CStringAdapter( + RepeatUntil(lambda obj, ctx: obj in terminators, char_field), + terminators=terminators, + encoding=encoding, + ) + ) + + +#=============================================================================== +# conditional +#=============================================================================== +def IfThenElse(name, predicate, then_subcon, else_subcon): + """An if-then-else conditional construct: if the predicate indicates True, + `then_subcon` will be used; otherwise `else_subcon` + + :param name: the name of the construct + :param predicate: a function taking the context as an argument and returning True or False + :param then_subcon: the subcon that will be used if the predicate returns True + :param else_subcon: the subcon that will be used if the predicate returns False + """ + return Switch(name, lambda ctx: bool(predicate(ctx)), + { + True : then_subcon, + False : else_subcon, + } + ) + +def If(predicate, subcon, elsevalue = None): + """An if-then conditional construct: if the predicate indicates True, + subcon will be used; otherwise, `elsevalue` will be returned instead. + + :param predicate: a function taking the context as an argument and returning True or False + :param subcon: the subcon that will be used if the predicate returns True + :param elsevalue: the value that will be used should the predicate return False. + by default this value is None. + """ + return IfThenElse(subcon.name, + predicate, + subcon, + Value("elsevalue", lambda ctx: elsevalue) + ) + + +#=============================================================================== +# misc +#=============================================================================== +def OnDemandPointer(offsetfunc, subcon, force_build = True): + """An on-demand pointer. + + :param offsetfunc: a function taking the context as an argument and returning + the absolute stream position + :param subcon: the subcon that will be parsed from the `offsetfunc()` stream position on demand + :param force_build: see OnDemand. by default True. + """ + return OnDemand(Pointer(offsetfunc, subcon), + advance_stream = False, + force_build = force_build + ) + +def Magic(data): + """A 'magic number' construct. it is used for file signatures, etc., to validate + that the given pattern exists. + + Example:: + + elf_header = Struct("elf_header", + Magic("\x7fELF"), + # ... + ) + """ + return ConstAdapter(Field(None, len(data)), data) + + -- cgit v1.2.3-70-g09d2