uncompile.py 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. #!/usr/bin/env python
  2. # Mode: -*- python -*-
  3. #
  4. # Copyright (c) 2015-2017, 2019-2020, 2023-2024
  5. # by Rocky Bernstein
  6. # Copyright (c) 2000-2002 by hartmut Goebel <h.goebel@crazy-compilers.com>
  7. #
  8. from __future__ import print_function
  9. import os
  10. import sys
  11. import time
  12. from typing import List
  13. import click
  14. from xdis.version_info import version_tuple_to_str
  15. from uncompyle6.main import main, status_msg
  16. from uncompyle6.verify import VerifyCmpError
  17. from uncompyle6.version import __version__
  18. program = "uncompyle6"
  19. def usage():
  20. print(__doc__)
  21. sys.exit(1)
  22. # __doc__ = """
  23. # Usage:
  24. # %s [OPTIONS]... [ FILE | DIR]...
  25. # %s [--help | --version]
  26. # Examples:
  27. # %s foo.pyc bar.pyc # decompile foo.pyc, bar.pyc to stdout
  28. # %s -o . foo.pyc bar.pyc # decompile to ./foo.pyc_dis and ./bar.pyc_dis
  29. # %s -o /tmp /usr/lib/python1.5 # decompile whole library
  30. # Options:
  31. # -o <path> output decompiled files to this path:
  32. # if multiple input files are decompiled, the common prefix
  33. # is stripped from these names and the remainder appended to
  34. # <path>
  35. # uncompyle6 -o /tmp bla/fasel.pyc bla/foo.pyc
  36. # -> /tmp/fasel.pyc_dis, /tmp/foo.pyc_dis
  37. # uncompyle6 -o /tmp bla/fasel.pyc bar/foo.pyc
  38. # -> /tmp/bla/fasel.pyc_dis, /tmp/bar/foo.pyc_dis
  39. # uncompyle6 -o /tmp /usr/lib/python1.5
  40. # -> /tmp/smtplib.pyc_dis ... /tmp/lib-tk/FixTk.pyc_dis
  41. # --compile | -c <python-file>
  42. # attempts a decompilation after compiling <python-file>
  43. # -d print timestamps
  44. # -p <integer> use <integer> number of processes
  45. # -r recurse directories looking for .pyc and .pyo files
  46. # --fragments use fragments deparser
  47. # --verify compare generated source with input byte-code
  48. # --verify-run compile generated source, run it and check exit code
  49. # --syntax-verify compile generated source
  50. # --linemaps generated line number correspondencies between byte-code
  51. # and generated source output
  52. # --encoding <encoding>
  53. # use <encoding> in generated source according to pep-0263
  54. # --help show this message
  55. # Debugging Options:
  56. # --asm | -a include byte-code (disables --verify)
  57. # --grammar | -g show matching grammar
  58. # --tree={before|after}
  59. # -t {before|after} include syntax before (or after) tree transformation
  60. # (disables --verify)
  61. # --tree++ | -T add template rules to --tree=before when possible
  62. # Extensions of generated files:
  63. # '.pyc_dis' '.pyo_dis' successfully decompiled (and verified if --verify)
  64. # + '_unverified' successfully decompile but --verify failed
  65. # + '_failed' decompile failed (contact author for enhancement)
  66. # """ % (
  67. # (program,) * 5
  68. # )
  69. @click.command()
  70. @click.option(
  71. "--asm++/--no-asm++",
  72. "-A",
  73. "asm_plus",
  74. default=False,
  75. help="show xdis assembler and tokenized assembler",
  76. )
  77. @click.option("--asm/--no-asm", "-a", default=False)
  78. @click.option("--grammar/--no-grammar", "-g", "show_grammar", default=False)
  79. @click.option("--tree/--no-tree", "-t", default=False)
  80. @click.option(
  81. "--tree++/--no-tree++",
  82. "-T",
  83. "tree_plus",
  84. default=False,
  85. help="show parse tree and Abstract Syntax Tree",
  86. )
  87. @click.option(
  88. "--linemaps/--no-linemaps",
  89. default=False,
  90. help="show line number correspondencies between byte-code "
  91. "and generated source output",
  92. )
  93. @click.option(
  94. "--verify",
  95. type=click.Choice(["run", "syntax"]),
  96. default=None,
  97. )
  98. @click.option(
  99. "--recurse/--no-recurse",
  100. "-r",
  101. "recurse_dirs",
  102. default=False,
  103. )
  104. @click.option(
  105. "--output",
  106. "-o",
  107. "outfile",
  108. type=click.Path(
  109. exists=True, file_okay=True, dir_okay=True, writable=True, resolve_path=True
  110. ),
  111. required=False,
  112. )
  113. @click.version_option(version=__version__)
  114. @click.option(
  115. "--start-offset",
  116. "start_offset",
  117. default=0,
  118. help="start decomplation at offset; default is 0 or the starting offset.",
  119. )
  120. @click.option(
  121. "--stop-offset",
  122. "stop_offset",
  123. default=-1,
  124. help="stop decomplation when seeing an offset greater or equal to this; default is "
  125. "-1 which indicates no stopping point.",
  126. )
  127. @click.argument("files", nargs=-1, type=click.Path(readable=True), required=True)
  128. def main_bin(
  129. asm: bool,
  130. asm_plus: bool,
  131. show_grammar,
  132. tree: bool,
  133. tree_plus: bool,
  134. linemaps: bool,
  135. verify,
  136. recurse_dirs: bool,
  137. outfile,
  138. start_offset: int,
  139. stop_offset: int,
  140. files,
  141. ):
  142. """
  143. Cross Python bytecode decompiler for Python bytecode up to Python 3.8.
  144. """
  145. version_tuple = sys.version_info[0:2]
  146. if version_tuple < (3, 6):
  147. print(
  148. f"Error: This version of the {program} runs from Python 3.6 or greater."
  149. f"You need another branch of this code for Python before 3.6."
  150. f""" \n\tYou have version: {version_tuple_to_str()}."""
  151. )
  152. sys.exit(-1)
  153. numproc = 0
  154. out_base = None
  155. out_base = None
  156. source_paths: List[str] = []
  157. timestamp = False
  158. timestampfmt = "# %Y.%m.%d %H:%M:%S %Z"
  159. pyc_paths = files
  160. # Expand directory if "recurse" was specified.
  161. if recurse_dirs:
  162. expanded_files = []
  163. for f in pyc_paths:
  164. if os.path.isdir(f):
  165. for root, _, dir_files in os.walk(f):
  166. for df in dir_files:
  167. if df.endswith(".pyc") or df.endswith(".pyo"):
  168. expanded_files.append(os.path.join(root, df))
  169. pyc_paths = expanded_files
  170. # argl, commonprefix works on strings, not on path parts,
  171. # thus we must handle the case with files in 'some/classes'
  172. # and 'some/cmds'
  173. src_base = os.path.commonprefix(pyc_paths)
  174. if src_base[-1:] != os.sep:
  175. src_base = os.path.dirname(src_base)
  176. if src_base:
  177. sb_len = len(os.path.join(src_base, ""))
  178. pyc_paths = [f[sb_len:] for f in pyc_paths]
  179. if not pyc_paths and not source_paths:
  180. print("No input files given to decompile", file=sys.stderr)
  181. usage()
  182. if outfile == "-":
  183. outfile = None # use stdout
  184. elif outfile and os.path.isdir(outfile):
  185. out_base = outfile
  186. outfile = None
  187. elif outfile and len(pyc_paths) > 1:
  188. out_base = outfile
  189. outfile = None
  190. # A second -a turns show_asm="after" into show_asm="before"
  191. if asm_plus or asm:
  192. asm_opt = "both" if asm_plus else "after"
  193. else:
  194. asm_opt = None
  195. if timestamp:
  196. print(time.strftime(timestampfmt))
  197. if numproc <= 1:
  198. show_ast = {"before": tree or tree_plus, "after": tree_plus}
  199. try:
  200. result = main(
  201. src_base,
  202. out_base,
  203. pyc_paths,
  204. source_paths,
  205. outfile,
  206. showasm=asm_opt,
  207. showgrammar=show_grammar,
  208. showast=show_ast,
  209. do_verify=verify,
  210. do_linemaps=linemaps,
  211. start_offset=start_offset,
  212. stop_offset=stop_offset,
  213. )
  214. if len(pyc_paths) > 1:
  215. mess = status_msg(*result)
  216. print("# " + mess)
  217. pass
  218. except ImportError as e:
  219. print(str(e))
  220. sys.exit(2)
  221. except KeyboardInterrupt:
  222. pass
  223. except VerifyCmpError:
  224. raise
  225. else:
  226. from multiprocessing import Process, Queue
  227. try:
  228. from Queue import Empty
  229. except ImportError:
  230. from queue import Empty
  231. fqueue = Queue(len(pyc_paths) + numproc)
  232. for f in pyc_paths:
  233. fqueue.put(f)
  234. for i in range(numproc):
  235. fqueue.put(None)
  236. rqueue = Queue(numproc)
  237. tot_files = okay_files = failed_files = verify_failed_files = 0
  238. def process_func():
  239. (tot_files, okay_files, failed_files, verify_failed_files) = (
  240. 0,
  241. 0,
  242. 0,
  243. 0,
  244. )
  245. try:
  246. while 1:
  247. f = fqueue.get()
  248. if f is None:
  249. break
  250. (t, o, f, v) = main(src_base, out_base, [f], [], outfile)
  251. tot_files += t
  252. okay_files += o
  253. failed_files += f
  254. verify_failed_files += v
  255. except (Empty, KeyboardInterrupt):
  256. pass
  257. rqueue.put((tot_files, okay_files, failed_files, verify_failed_files))
  258. rqueue.close()
  259. try:
  260. procs = [Process(target=process_func) for i in range(numproc)]
  261. for p in procs:
  262. p.start()
  263. for p in procs:
  264. p.join()
  265. try:
  266. (tot_files, okay_files, failed_files, verify_failed_files) = (
  267. 0,
  268. 0,
  269. 0,
  270. 0,
  271. )
  272. while True:
  273. (t, o, f, v) = rqueue.get(False)
  274. tot_files += t
  275. okay_files += o
  276. failed_files += f
  277. verify_failed_files += v
  278. except Empty:
  279. pass
  280. print(
  281. "# decompiled %i files: %i okay, %i failed, %i verify failed"
  282. % (tot_files, okay_files, failed_files, verify_failed_files)
  283. )
  284. except (KeyboardInterrupt, OSError):
  285. pass
  286. if timestamp:
  287. print(time.strftime(timestampfmt))
  288. return
  289. if __name__ == "__main__":
  290. main_bin()