Files
2025-03-13 16:17:53 +01:00

619 lines
20 KiB
Python

import sys
import ast
import inspect
import argparse
import os
p = argparse.ArgumentParser()
p.add_argument("-p", "--paths", type=str, required=True, help="Path(s) to the file(s) to parse")
p.add_argument("-c", "--dump-doc", default=False, action="store_true", help="Dump python docstrings")
args = p.parse_args()
class scope_t(object):
def __init__(self, node, scope):
self.node = node
self.parent = None
self.children = []
self.doc = None
def _new_scope(self, _type, node):
scope = _type(node, self)
self.children.append(scope)
scope.parent = self
return scope
def kind(self):
return self.__class__.__name__.replace("_t", "")
def new_class(self, node):
return self._new_scope(class_t, node)
def new_function(self, node):
return self._new_scope(function_t, node)
def new_variable(self, node, variable_name):
s = self._new_scope(variable_t, node)
s.variable_name = variable_name
return s
def get_name(self):
return self.node.name
def get_full_name_parts(self):
parts = []
s = self
while s is not None:
parts.append(s.get_name())
s = s.parent
return reversed(parts)
def get_full_name(self):
return ".".join(self.get_full_name_parts())
def set_doc(self, doc):
self.doc = doc
class module_t(scope_t):
pass
class class_t(scope_t):
pass
class function_t(scope_t):
pass
class variable_t(scope_t):
def get_name(self):
return self.variable_name
DF_DOC = 0x1
# https://gist.github.com/MineRobber9000/485e648da3a77d5894fa1fe9189b5728
def produce_function_prototype(node, name):
def get_value(node):
if type(node) == ast.Name:
return node.id
if type(node) == ast.Num:
return str(node.n)
if type(node) == ast.Subscript:
return get_value(node.value)+"["+get_value(node.slice)+"]"
if type(node) == ast.Index:
return get_value(node.value)
if type(node) == ast.Tuple:
return ", ".join(map(get_value,node.elts))
if type(node) == ast.Ellipsis:
return "..."
if type(node) == ast.NameConstant:
return str(node.value)
if type(node) == ast.Attribute:
return get_value(node.value)+"."+node.attr
if type(node) == ast.Constant:
value = str(node.value)
if isinstance(node.value, str):
value = f'"{value}"'
return value
if type(node) == ast.List:
pieces = map(get_value, node.elts)
return "[" + ", ".join(pieces) + "]"
if type(node) == ast.UnaryOp:
return get_value(node.op) + get_value(node.operand)
if type(node) == ast.BinOp:
return get_value(node.left) + get_value(node.op) + get_value(node.right)
if type(node) == ast.USub:
return "-"
if type(node) == ast.BitOr:
return "|"
print(type(node))
return "???"
out = [name, "("]
def put(what):
if what is not None:
out.append(what)
defs = len(node.args.args) - len(node.args.defaults)
for n, arg in enumerate(node.args.args):
put(arg.arg)
if arg.annotation:
put(": ")
put(get_value(arg.annotation))
if n >= defs:
put(" = ")
put(get_value(node.args.defaults[n-defs]))
put(", ")
if node.args.vararg:
put("*")
put(node.args.vararg.arg)
if node.args.vararg.annotation:
put(": ")
put(get_value(node.args.vararg.annotation))
put(", ")
if node.args.kwarg:
put("**")
put(node.args.kwarg.arg)
if node.args.kwarg.annotation:
put(": ")
put(get_value(node.args.kwarg.annotation))
put(", ")
if node.args.kwonlyargs:
if not node.args.vararg:
put("*, ")
for n, arg in enumerate(node.args.kwonlyargs):
put(arg.arg)
if arg.annotation:
put(": ")
put(get_value(arg.annotation))
if node.args.kw_defaults[n]:
put(" = ")
put(get_value(node.args.kw_defaults[n]))
put(", ")
out = "".join(out)
out = (out[:-2] if (node.args.args or node.args.vararg or node.args.kwarg or node.args.kwonlyargs) else out)+")"
if node.returns:
out += " -> "+get_value(node.returns)
return out
def dump(scope, flags=0, sort=True):
lines = []
def dump1(s):
name = s.get_full_name()
if (flags & DF_DOC) != 0 and isinstance(s, function_t):
line = [produce_function_prototype(s.node, name)]
else:
line = [name]
lines.append(" ".join(line))
if (flags & DF_DOC) != 0:
if s.doc is not None:
lines.extend(s.doc.split("\n"))
lines.append("")
children = s.children[:]
if sort:
children = sorted(children, key=lambda n: n.get_name())
for c in children:
dump1(c)
dump1(scope)
return "\n".join(lines)
class collector_t(ast.NodeVisitor):
class temp_scope_t(object):
def __init__(self, collector, scope):
self.collector = collector
self.scope = scope
def __enter__(self):
old_scope = self.collector.scope
self.collector.scope = self.scope
self.scope = old_scope # swoop in the old context
def __exit__(self, tp, value, traceback):
self.collector.scope = self.scope
if value:
raise
def __init__(self, module_name):
class module_node_t(object):
def __init__(self, name):
self.name = name
self.scope = module_t(module_node_t(module_name), None)
self.assign_last_line = -1
super(collector_t, self).__init__()
def in_function(self):
return isinstance(self.scope, function_t)
def in_class(self):
return isinstance(self.scope, class_t)
def accept(self, node):
return True
def visit_FunctionDef(self, node):
if self.accept(node):
s = self.scope.new_function(node)
with self.temp_scope_t(self, s):
self.generic_visit(node)
def visit_ClassDef(self, node):
if self.in_function():
return
if self.accept(node):
with self.temp_scope_t(self, self.scope.new_class(node)):
self.generic_visit(node)
def visit_AnnAssign(self, node):
return self.visit_Assign(node)
def visit_Assign(self, node):
if self.in_function():
return
if isinstance(node, ast.AnnAssign):
target = node.target
elif len(node.targets) != 1:
return
else:
target = node.targets[0]
if isinstance(target, ast.Name):
self.assign_variable = target.id
self.assign_last_line = self._highest_lineno(node)
def _highest_lineno(self, node):
if hasattr(node, "end_lineno"):
return node.end_lineno
highest = node.lineno
for child in ast.walk(node):
if hasattr(child, "lineno") and child.lineno > highest:
highest = child.lineno
return highest
def visit_Expr(self, node):
# print("%d: %s" % (node.lineno, ast.dump(node.value)))
if not isinstance(node.value, ast.Constant):
return
if hasattr(node, "end_lineno"):
line_before = node.lineno - 1 # in this case, lineno = start
else:
# hack until Python 3.8; if <3.8, lineno = end
line_before = node.lineno - len(node.value.s.split("\n"))
clean_doc = self._cleandoc(node.value.value)
if line_before == self.assign_last_line:
self.scope.new_variable(node, self.assign_variable).set_doc(clean_doc)
else:
self.scope.set_doc(clean_doc)
# self.variables.append(
# "\nDocumentation on variable %s in module %s:\n\n%s\n" \
# % (self.assign_variable,
# self.module_name,
# self._cleandoc(node.value.s)))
def _cleandoc(self, docstring):
if docstring:
docstring = inspect.cleandoc(docstring)
# 4-blanks indent
docstring = "\n".join(" " + line for line in docstring.split("\n"))
return docstring
class docfixing_collector_t(collector_t):
IGNORE = [
"ida_kernwin.__ask_form_callable",
"ida_kernwin.__call_form_callable",
"ida_kernwin.__open_form_callable",
"ida_kernwin.PluginForm.TWidgetToQtPythonWidget",
"ida_kernwin.PluginForm.TWidgetToPyQtWidget",
"ida_kernwin.PluginForm.TWidgetToPySideWidget",
"ida_kernwin.PluginForm.FormToPyQtWidget",
"*._SwigNonDynamicMeta",
"*._swig_add_metaclass",
"*._swig_add_metaclass.wrapper",
"*._swig_repr",
"*._swig_setattr_nondynamic_class_variable",
"*._swig_setattr_nondynamic_class_variable.set_class_attr",
"*._swig_setattr_nondynamic_instance_variable",
"*._swig_setattr_nondynamic_instance_variable.set_instance_attr",
"*.*.dump_state",
]
def accept(self, node):
full_name_parts = list(self.scope.get_full_name_parts()) + [node.name]
for ign in self.IGNORE:
ign_parts = ign.split(".")
if len(full_name_parts) == len(ign_parts):
match = True
for got, against in zip(full_name_parts, ign_parts):
if against != "*" and got != against:
match = False
break
if match:
return False
return True
TRANSLATIONS = {
"ida_hexrays.casm_t" : [
((
"uintvec_t",
"uint64vec_t"
), "eavec_t", True),
((
"unsigned int *",
"unsigned long long *"
), "unsigned-ea-like-numeric-type *", True),
((
"unsigned int &",
"unsigned long long &"
), "unsigned-ea-like-numeric-type &", True),
((
"unsigned int const &",
"unsigned long long const &"
), "unsigned-ea-like-numeric-type const &", True),
((
"qvector< unsigned int >::",
"qvector< unsigned long long >::"
), "qvector< unsigned-ea-like-numeric-type >::", True),
((
"qvector< unsigned int > &",
"qvector< unsigned long long > &"
), "qvector< unsigned-ea-like-numeric-type > &", True),
],
"ida_hexrays.ivl_t" : [
((
") -> 'unsigned int'",
") -> 'unsigned long long'", # py3
), ") -> 'unsigned-ea-like-numeric-type'", True),
((
") -> unsigned int",
") -> unsigned long long", # py3
), ") -> unsigned-ea-like-numeric-type", True),
],
"ida_hexrays.uval_ivl_t" : [
((
") -> 'unsigned int'",
") -> 'unsigned long long'", # py3
), ") -> 'unsigned-ea-like-numeric-type'", True),
((
") -> unsigned int",
") -> unsigned long long", # py3
), ") -> unsigned-ea-like-numeric-type", True),
((
"_off: unsigned int",
"_off: unsigned long long", # py3
), "_off: unsigned-ea-like-numeric-type", True),
((
"_size: unsigned int",
"_size: unsigned long long", # py3
), "_size: unsigned-ea-like-numeric-type", True),
],
"ida_hexrays.ivlset_t" : [
((
"ivlset_tpl< ivl_t,unsigned int >::",
"ivlset_tpl< ivl_t,unsigned long long >::", # py3
), "ivlset_tpl< ivl_t,unsigned-ea-like-numeric-type >::", True),
((
"v: unsigned int",
"v: unsigned long long"
), "v: unsigned-ea-like-numeric-type", True),
],
"ida_hexrays.uval_ivl_ivlset_t" : [
((
"ivlset_tpl< ivl_t,unsigned int >::",
"ivlset_tpl< ivl_t,unsigned long long >::",
), "ivlset_tpl< ivl_t,unsigned-ea-like-numeric-type >::", True),
((
"v: unsigned int",
"v: unsigned long long"
), "v: unsigned-ea-like-numeric-type", True),
],
"ida_segment.segment_defsr_array" : [
((
"unsigned int const &",
"unsigned long long const &",
), "unsigned-ea-like-numeric-type const &", True),
((
"data: unsigned int (&)",
"data: unsigned long long (&)",
), "data: unsigned-ea-like-numeric-type (&)", True),
],
"ida_nalt.strpath_ids_array" : [
# py3
((
"unsigned int const &",
"unsigned long long const &",
), "unsigned-ea-like-numeric-type const &", True),
# py3
((
"data: unsigned int (&)",
"data: unsigned long long (&)",
), "data: unsigned-ea-like-numeric-type (&)", True),
],
"idc.add_func" : [
(("add_func(start, end=4294967295)",
"add_func(start, end=4294967295L)",
"add_func(start, end=18446744073709551615)", # py3
"add_func(start, end=18446744073709551615L)",
), "add_func(start, end=BADADDR)", True),
],
"idc.next_head" : [
(("next_head(ea, maxea=4294967295)",
"next_head(ea, maxea=4294967295L)",
"next_head(ea, maxea=18446744073709551615)", # py3
"next_head(ea, maxea=18446744073709551615L)",
), "next_head(ea, maxea=BADADDR)", True),
],
"ida_xref.casevec_t" : [
((
"qvector< int >",
"qvector< long long >",
), "qvector< signed-ea-like-numeric-type >", True),
],
# all that follows is for py3
"ida_dbg.dbg_bin_search" : [
((
"'uint32 *, qstring *'",
"'uint64 *, qstring *'",
), "'unsigned-ea-like-numeric-type *, qstring *'", True),
],
"ida_dbg.get_ip_val" : [
((
"'uint32 *'",
"'uint64 *'",
), "'unsigned-ea-like-numeric-type *'", True),
],
"ida_dbg.get_sp_val" : [
((
"'uint32 *'",
"'uint64 *'",
), "'unsigned-ea-like-numeric-type *'", True),
],
"ida_funcs.dyn_ea_array" : [
((
"-> unsigned int const &",
"-> unsigned long long const &",
), "-> unsigned-ea-like-numeric-type const &", True),
((
"-> unsigned int *",
"-> unsigned long long *",
), "-> unsigned-ea-like-numeric-type *", True),
((
"_data: unsigned int *",
"_data: unsigned long long *",
), "_data: unsigned-ea-like-numeric-type *", True),
((
"v: unsigned int const &",
"v: unsigned long long const &",
), "v: unsigned-ea-like-numeric-type const &", True),
# Python3
((
"-> 'unsigned int const &'",
"-> 'unsigned long long const &'",
), "-> 'unsigned-ea-like-numeric-type const &'", True),
],
"ida_idp.ph_find_op_value" : [
((
"uint32",
"uint64",
), "unsigned-ea-like-numeric-type", True),
],
"ida_idp.ph_find_reg_value" : [
((
"uint32",
"uint64",
), "unsigned-ea-like-numeric-type", True),
],
"ida_regfinder.find_reg_value" : [
((
"'uint32 *'",
"'uint64 *'",
), "unsigned-ea-like-numeric-type", True),
],
"ida_regfinder.find_sp_value" : [
((
"'int32 *'",
"'int64 *'",
), "signed-ea-like-numeric-type", True),
],
"ida_hexrays.user_iflags_t" : [
((
"int([x]) -> integer",
), "int(x=0) -> integer", False),
],
"ida_hexrays.eamap_t" : [
((
"int([x]) -> integer",
), "int(x=0) -> integer", False),
((
"_Keyval: unsigned int const &",
"_Keyval: unsigned long long const &"
), "_Keyval: unsigned-ea-like-numeric-type const &", True),
],
"ida_hexrays.user_unions_t" : [
((
"_Keyval: unsigned int const &",
"_Keyval: unsigned long long const &"
), "_Keyval: unsigned-ea-like-numeric-type const &", True),
],
"ida_hexrays.DecompilationFailure" : [
((
"Helper for pickle.",
), "helper for pickle", False),
],
"idc.DeprecatedIDCError" : [
((
"Helper for pickle.",
), "helper for pickle", False),
],
"ida_idp._processor_t" : [
((
"'uint32 *'",
"'uint64 *'",
), "'unsigned-ea-like-numeric-type *'", True),
],
"ida_idp._processor_t_find_op_value" : [
((
"'uint32 *'",
"'uint64 *'",
), "'unsigned-ea-like-numeric-type *'", True),
],
"ida_idp._processor_t_find_reg_value" : [
((
"'uint32 *'",
"'uint64 *'",
), "'unsigned-ea-like-numeric-type *'", True),
],
"ida_kernwin.atoea" : [
((
"-> 'uint32 *'",
"-> 'uint64 *'",
), "'unsigned-ea-like-numeric-type *'", True),
],
"ida_kernwin.str2ea" : [
((
"-> 'uint32 *'",
"-> 'uint64 *'",
), "'unsigned-ea-like-numeric-type *'", True),
],
"ida_kernwin.str2ea_ex" : [
((
"-> 'uint32 *'",
"-> 'uint64 *'",
), "'unsigned-ea-like-numeric-type *'", True),
],
"ida_kernwin.PluginForm" : [
((
"module '__main__' from 'tools/dumpdoc.py'",
"module '__main__' (built-in)",
), "module 'main'", False),
],
}
def _cleandoc(self, docstring):
docstring = super(docfixing_collector_t, self)._cleandoc(docstring)
name = self.scope.get_full_name()
translations = None
while name:
translations = self.TRANSLATIONS.get(name, None)
if translations:
break
name_parts = name.split(".")
name = ".".join(name_parts[:-1])
if translations:
out = []
for l in docstring.split("\n"):
for all_frm, dst, _ in translations:
assert(isinstance(all_frm, tuple))
for frm in all_frm:
idx = l.find(frm)
if idx > -1:
# sys.stderr.write("SPOTTED '%s' in '%s', position %s\n" % (frm, l, idx))
l = l[0:idx] + dst + l[idx+len(frm):]
# sys.stderr.write("ADDING '%s'\n" % l)
out.append(l)
docstring = '\n'.join(out)
return docstring
toplevel_scopes = []
for path in args.paths.split(","):
with open(path, "r") as f:
tree = ast.parse(f.read())
_, fname = os.path.split(path)
module_name = fname[:fname.index(".")]
vc = docfixing_collector_t(module_name)
vc.visit(tree)
toplevel_scopes.append(vc.scope)
flags = (DF_DOC if args.dump_doc else 0)
for s in toplevel_scopes:
print(dump(s, flags=flags))