mirror of
https://github.com/lcdr/utils.git
synced 2024-08-30 17:32:16 +00:00
189 lines
6.9 KiB
Python
189 lines
6.9 KiB
Python
"""
|
|
Module for parsing binary data into structs.
|
|
"""
|
|
import argparse
|
|
import re
|
|
from collections import namedtuple
|
|
|
|
from pyraknet.bitstream import BitStream, c_bit, c_float, c_double, c_int8, c_uint8, c_int16, c_uint16, c_int32, c_uint32, c_int64, c_uint64
|
|
|
|
VAR_CHARS = r"[^ \t\[\]]+"
|
|
BITSTREAM_TYPES = {"bit": c_bit, "float": c_float, "double": c_double, "s8": c_int8, "u8": c_uint8, "s16": c_int16, "u16": c_uint16, "s32": c_int32, "u32": c_uint32, "s64": c_int64, "u64": c_uint64}
|
|
TYPES_RE = "("+"|".join(BITSTREAM_TYPES.keys())+")"
|
|
|
|
DEFINITION_SYNTAX = re.compile(r"""
|
|
^(?P<indent>\t*) # Indentation
|
|
(if\ (?P<if_condition>.+):
|
|
|
|
|
while\ (?P<while_condition>.+):
|
|
|
|
|
(?P<break>break)
|
|
|
|
|
((?P<var_assign>"""+VAR_CHARS+r""")=)? # Assign this struct a variable so the value can be back-referenced later
|
|
\[
|
|
(?P<type>"""+TYPES_RE+r""") # Struct type
|
|
\]
|
|
\ -\ (?P<description>.*?) # Description for the struct
|
|
(,\ expect\ (?P<expect>(.+?)))? # Expect the value to be like this expression. Struct attribute 'unexpected' will be None if no expects, True if any expects are False, or False if all expects are True.
|
|
(,\ assert\ (?P<assert>(.+?)))? # Assert the value to be like this expression, will raise AssertionError if not True.
|
|
)$
|
|
""", re.VERBOSE)
|
|
|
|
IfStatement = namedtuple("IfStatement", ("condition",))
|
|
WhileStatement = namedtuple("WhileStatement", ("condition",))
|
|
BreakStatement = namedtuple("BreakStatement", ())
|
|
StructDefinition = namedtuple("struct_token", ("var_assign", "type", "description", "expects", "asserts"))
|
|
|
|
Structure = namedtuple("Structure", ("level", "description", "value", "unexpected"))
|
|
|
|
class StructParser:
|
|
def __init__(self, struct_defs):
|
|
"""
|
|
Set up the parser with the structure definitions.
|
|
Arguments:
|
|
struct_defs: A string of structure definitions in my custom format (currently unnamed), see the documentation of that for details.
|
|
"""
|
|
self._variables = {}
|
|
struct_defs = struct_defs.splitlines()
|
|
struct_defs = [re.search(DEFINITION_SYNTAX, struct).groupdict() for struct in struct_defs if re.search(DEFINITION_SYNTAX, struct) is not None] # Filter out lines not matching the syntax
|
|
|
|
self.defs = self._to_tree(iter(struct_defs))[0]
|
|
|
|
def parse(self, data, variables=None):
|
|
"""
|
|
Parse the binary data, yielding structure objects.
|
|
|
|
Arguments:
|
|
data: The binary data to parse.
|
|
variables: A dict of variables to be used in checks as defined by the structure definition, such as expects or asserts.
|
|
Yields:
|
|
Named structure tuples,
|
|
attributes:
|
|
level: The indentation level from the structure definition.
|
|
description: The description from the structure definition.
|
|
value: Parsed value of this structure occurrence in the binary data. The type of this is specified by the type specified in the structure definition.
|
|
unexpected: None if no expects defined, True if any expects are False, False if all expects are True.
|
|
Raises:
|
|
AssertionError if any assert is False.
|
|
"""
|
|
if variables is None:
|
|
variables = {}
|
|
self._variables = variables
|
|
if isinstance(data, BitStream):
|
|
stream = data
|
|
else:
|
|
stream = BitStream(data)
|
|
yield from self._parse_struct_occurrences(stream, self.defs)
|
|
|
|
def _to_tree(self, def_iter, stack_level=0, start_def=None):
|
|
current_level = []
|
|
try:
|
|
if start_def is not None:
|
|
def_ = start_def
|
|
else:
|
|
def_ = next(def_iter)
|
|
|
|
while True:
|
|
if len(def_["indent"]) == stack_level:
|
|
def_tuple = self._to_def_tuple(def_)
|
|
current_level.append((def_tuple, ()))
|
|
def_ = next(def_iter)
|
|
elif len(def_["indent"]) == stack_level+1:
|
|
# found a child of the previous
|
|
children, next_struct = self._to_tree(def_iter, stack_level+1, def_)
|
|
current_level[-1] = current_level[-1][0], children
|
|
if next_struct is None:
|
|
raise StopIteration
|
|
def_ = next_struct
|
|
elif len(def_["indent"]) < stack_level:
|
|
# we're at ancestor level again, done with the children
|
|
return current_level, def_
|
|
except StopIteration:
|
|
return current_level, None
|
|
|
|
@staticmethod
|
|
def _to_def_tuple(def_):
|
|
if def_["if_condition"] is not None:
|
|
condition = compile(def_["if_condition"], "<if_condition>", "eval")
|
|
return IfStatement(condition)
|
|
if def_["while_condition"] is not None:
|
|
condition = compile(def_["while_condition"], "<while_condition>", "eval")
|
|
return WhileStatement(condition)
|
|
if def_["break"] is not None:
|
|
return BreakStatement()
|
|
|
|
type_ = BITSTREAM_TYPES[def_["type"]]
|
|
|
|
if def_["expect"] is not None:
|
|
expects = [compile("value "+i, "<expect>", "eval") for i in def_["expect"].split(" and ")]
|
|
else:
|
|
expects = ()
|
|
if def_["assert"] is not None:
|
|
asserts = [compile("value "+i, "<assert>", "eval") for i in def_["assert"].split(" and ")]
|
|
else:
|
|
asserts = ()
|
|
|
|
return StructDefinition(def_["var_assign"], type_, def_["description"], expects, asserts)
|
|
|
|
def _parse_struct_occurrences(self, stream, defs, stack_level=0, repeat_times=1):
|
|
for _ in range(repeat_times):
|
|
for def_, children in defs:
|
|
if isinstance(def_, IfStatement):
|
|
if children and self._eval(def_.condition):
|
|
break_ = yield from self._parse_struct_occurrences(stream, children, stack_level+1)
|
|
if break_:
|
|
return True
|
|
elif isinstance(def_, WhileStatement):
|
|
if children:
|
|
while self._eval(def_.condition):
|
|
break_ = yield from self._parse_struct_occurrences(stream, children, stack_level+1)
|
|
if break_:
|
|
break
|
|
elif isinstance(def_, BreakStatement):
|
|
return True
|
|
else:
|
|
value = stream.read(def_.type)
|
|
|
|
if def_.expects:
|
|
for expression in def_.expects:
|
|
if not self._eval(expression, value):
|
|
unexpected = True
|
|
break
|
|
else:
|
|
unexpected = False
|
|
else:
|
|
unexpected = None
|
|
|
|
for expression in def_.asserts:
|
|
assert self._eval(expression, value), (value, expression, def_)
|
|
|
|
if def_.var_assign is not None:
|
|
self._variables[def_.var_assign] = value
|
|
yield Structure(stack_level, def_.description, value, unexpected)
|
|
|
|
if children and value:
|
|
break_ = yield from self._parse_struct_occurrences(stream, children, stack_level+1, value)
|
|
if break_:
|
|
return True
|
|
|
|
def _eval(self, expression, value=None):
|
|
globals_ = {"__builtins__": {}, "value": value}
|
|
globals_.update(self._variables)
|
|
return eval(expression, globals_) # definitely not safe, fwiw
|
|
|
|
|
|
if __name__ == "__main__":
|
|
argparser = argparse.ArgumentParser(description=__doc__)
|
|
argparser.add_argument("filepath", help="path of binary file")
|
|
argparser.add_argument("definition", help="struct definition file path to parse with")
|
|
args = argparser.parse_args()
|
|
|
|
with open(args.definition) as file:
|
|
defs = file.read()
|
|
|
|
parser = StructParser(defs)
|
|
|
|
with open(args.filepath, "rb") as file:
|
|
for structure in parser.parse(file.read()):
|
|
print(structure)
|