decompile.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. #!/usr/bin/env python
  2. # Mode: -*- python -*-
  3. #
  4. # Copyright (c) 2015-2017, 2019-2024 by Rocky Bernstein
  5. # Copyright (c) 2000-2002 by hartmut Goebel <h.goebel@crazy-compilers.com>
  6. #
  7. import os
  8. import sys
  9. from typing import List
  10. import click
  11. from xdis.version_info import version_tuple_to_str
  12. from decompyle3.main import main, status_msg
  13. from decompyle3.version import __version__
  14. case_sensitive = {"case_sensitive": False}
  15. program = "decompyle3"
  16. def usage():
  17. print(__doc__)
  18. sys.exit(1)
  19. @click.command()
  20. @click.option(
  21. "--asm++/--no-asm++",
  22. "-A",
  23. "asm_plus",
  24. default=False,
  25. help="show xdis assembler and tokenized assembler",
  26. )
  27. @click.option("--asm/--no-asm", "-a", default=False)
  28. @click.option("--grammar/--no-grammar", "-g", "show_grammar", default=False)
  29. @click.option("--tree/--no-tree", "-t", default=False)
  30. @click.option(
  31. "--tree++/--no-tree++",
  32. "-T",
  33. "tree_plus",
  34. default=False,
  35. help="show parse tree and Abstract Syntax Tree",
  36. )
  37. @click.option(
  38. "--linemaps/--no-linemaps",
  39. default=False,
  40. help="show line number correspondencies between byte-code "
  41. "and generated source output",
  42. )
  43. @click.option(
  44. "--verify",
  45. type=click.Choice(["run", "syntax"]),
  46. default=None,
  47. )
  48. @click.option(
  49. "--recurse/--no-recurse",
  50. "-r",
  51. "recurse_dirs",
  52. default=False,
  53. )
  54. @click.option(
  55. "--output",
  56. "-o",
  57. "outfile",
  58. type=click.Path(
  59. exists=True, file_okay=True, dir_okay=True, writable=True, resolve_path=True
  60. ),
  61. required=False,
  62. )
  63. @click.version_option(version=__version__)
  64. @click.option(
  65. "--start-offset",
  66. "start_offset",
  67. default=0,
  68. help="start decomplation at offset; default is 0 or the starting offset.",
  69. )
  70. @click.version_option(version=__version__)
  71. @click.option(
  72. "--stop-offset",
  73. "stop_offset",
  74. default=-1,
  75. help="stop decomplation when seeing an offset greater or equal to this; default is "
  76. "-1 which indicates no stopping point.",
  77. )
  78. @click.argument("files", nargs=-1, type=click.Path(readable=True), required=True)
  79. def main_bin(
  80. asm_plus: bool,
  81. asm: bool,
  82. show_grammar,
  83. tree: bool,
  84. tree_plus: bool,
  85. linemaps: bool,
  86. verify,
  87. recurse_dirs: bool,
  88. outfile,
  89. start_offset: int,
  90. stop_offset: int,
  91. files: List[str],
  92. ):
  93. """
  94. Cross Python bytecode decompiler for Python 3.7-3.8 bytecode
  95. """
  96. version_tuple = sys.version_info[0:2]
  97. if version_tuple < (3, 7):
  98. print(
  99. f"Error: {program} runs from Python 3.7 or greater."
  100. f""" \n\tYou have version: {version_tuple_to_str()}."""
  101. )
  102. sys.exit(-1)
  103. out_base = None
  104. source_paths: List[str] = []
  105. # timestamp = False
  106. # timestampfmt = "# %Y.%m.%d %H:%M:%S %Z"
  107. pyc_paths = files
  108. # Expand directory if "recurse" was specified.
  109. if recurse_dirs:
  110. expanded_files = []
  111. for f in pyc_paths:
  112. if os.path.isdir(f):
  113. for root, _, dir_files in os.walk(f):
  114. for df in dir_files:
  115. if df.endswith(".pyc") or df.endswith(".pyo"):
  116. expanded_files.append(os.path.join(root, df))
  117. pyc_paths = expanded_files
  118. # argl, commonprefix works on strings, not on path parts,
  119. # thus we must handle the case with files in 'some/classes'
  120. # and 'some/cmds'
  121. src_base = os.path.commonprefix(pyc_paths)
  122. if src_base[-1:] != os.sep:
  123. src_base = os.path.dirname(src_base)
  124. if src_base:
  125. sb_len = len(os.path.join(src_base, ""))
  126. pyc_paths = [f[sb_len:] for f in pyc_paths]
  127. if not pyc_paths and not source_paths:
  128. print("No input files given to decompile", file=sys.stderr)
  129. usage()
  130. if outfile == "-":
  131. outfile = None # use stdout
  132. elif outfile and os.path.isdir(outfile):
  133. out_base = outfile
  134. outfile = None
  135. elif outfile and len(pyc_paths) > 1:
  136. out_base = outfile
  137. outfile = None
  138. # A second -a turns show_asm="after" into show_asm="before"
  139. if asm_plus or asm:
  140. asm_opt = "both" if asm_plus else "after"
  141. else:
  142. asm_opt = None
  143. # if timestamp:
  144. # print(time.strftime(timestampfmt))
  145. show_grammar = {
  146. "rules": False,
  147. "transition": False,
  148. "reduce": show_grammar,
  149. "errorstack": "full",
  150. "context": True,
  151. "dups": False,
  152. }
  153. numproc = 1
  154. if numproc <= 1:
  155. show_ast = {"before": tree or tree_plus, "after": tree_plus}
  156. try:
  157. result = main(
  158. src_base,
  159. out_base,
  160. pyc_paths,
  161. source_paths,
  162. outfile,
  163. showasm=asm_opt,
  164. showgrammar=show_grammar,
  165. showast=show_ast,
  166. do_verify=verify,
  167. do_linemaps=linemaps,
  168. start_offset=start_offset,
  169. stop_offset=stop_offset,
  170. )
  171. if len(pyc_paths) > 1:
  172. mess = status_msg(verify, *result)
  173. print("# " + mess)
  174. pass
  175. except ImportError as e:
  176. print(str(e))
  177. sys.exit(2)
  178. except KeyboardInterrupt:
  179. pass
  180. else:
  181. from multiprocessing import Process, Queue
  182. from queue import Empty
  183. fqueue = Queue(len(pyc_paths) + numproc)
  184. for f in pyc_paths:
  185. fqueue.put(f)
  186. for i in range(numproc):
  187. fqueue.put(None)
  188. rqueue = Queue(numproc)
  189. def process_func():
  190. (tot_files, okay_files, failed_files, verify_failed_files) = (
  191. 0,
  192. 0,
  193. 0,
  194. 0,
  195. )
  196. try:
  197. while True:
  198. f = fqueue.get()
  199. if f is None:
  200. break
  201. (t, o, f, v) = main(src_base, out_base, [f], [], outfile)
  202. tot_files += t
  203. okay_files += o
  204. failed_files += f
  205. verify_failed_files += v
  206. except (Empty, KeyboardInterrupt):
  207. pass
  208. rqueue.put((tot_files, okay_files, failed_files, verify_failed_files))
  209. rqueue.close()
  210. try:
  211. procs = [Process(target=process_func) for _ in range(numproc)]
  212. for p in procs:
  213. p.start()
  214. for p in procs:
  215. p.join()
  216. (tot_files, okay_files, failed_files, verify_failed_files) = (
  217. 0,
  218. 0,
  219. 0,
  220. 0,
  221. )
  222. try:
  223. while True:
  224. (t, o, f, v) = rqueue.get(False)
  225. tot_files += t
  226. okay_files += o
  227. failed_files += f
  228. verify_failed_files += v
  229. except Empty:
  230. pass
  231. print(
  232. "# decompiled %i files: %i okay, %i failed, %i verify failed"
  233. % (tot_files, okay_files, failed_files, verify_failed_files)
  234. )
  235. except (KeyboardInterrupt, OSError):
  236. pass
  237. # if timestamp:
  238. # print(time.strftime(timestampfmt))
  239. return
  240. if __name__ == "__main__":
  241. main_bin()