archive_viewer.py 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. #-----------------------------------------------------------------------------
  2. # Copyright (c) 2013-2023, PyInstaller Development Team.
  3. #
  4. # Distributed under the terms of the GNU General Public License (version 2
  5. # or later) with exception for distributing the bootloader.
  6. #
  7. # The full license is in the file COPYING.txt, distributed with this software.
  8. #
  9. # SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
  10. #-----------------------------------------------------------------------------
  11. """
  12. Viewer for PyInstaller-generated archives.
  13. """
  14. import argparse
  15. import os
  16. import sys
  17. import PyInstaller.log
  18. from PyInstaller.archive.readers import CArchiveReader, ZlibArchiveReader
  19. try:
  20. from argcomplete import autocomplete
  21. except ImportError:
  22. def autocomplete(parser):
  23. return None
  24. class ArchiveViewer:
  25. def __init__(self, filename, interactive_mode, recursive_mode, brief_mode):
  26. self.filename = filename
  27. self.interactive_mode = interactive_mode
  28. self.recursive_mode = recursive_mode
  29. self.brief_mode = brief_mode
  30. self.stack = []
  31. # Recursive mode implies non-interactive mode
  32. if self.recursive_mode:
  33. self.interactive_mode = False
  34. def main(self):
  35. # Open top-level (initial) archive
  36. archive = self._open_toplevel_archive(self.filename)
  37. archive_name = os.path.basename(self.filename)
  38. self.stack.append((archive_name, archive))
  39. # Not-interactive mode
  40. if not self.interactive_mode:
  41. return self._non_interactive_processing()
  42. # Interactive mode; show top-level archive
  43. self._show_archive_contents(archive_name, archive)
  44. # Interactive command processing
  45. while True:
  46. # Read command
  47. try:
  48. tokens = input('? ').split(None, 1)
  49. except EOFError:
  50. # Ctrl-D
  51. print(file=sys.stderr) # Clear line.
  52. break
  53. # Print usage?
  54. if not tokens:
  55. self._print_usage()
  56. continue
  57. # Process
  58. command = tokens[0].upper()
  59. if command == 'Q':
  60. break
  61. elif command == 'U':
  62. self._move_up_the_stack()
  63. elif command == 'O':
  64. self._open_embedded_archive(*tokens[1:])
  65. elif command == 'X':
  66. self._extract_file(*tokens[1:])
  67. elif command == 'S':
  68. archive_name, archive = self.stack[-1]
  69. self._show_archive_contents(archive_name, archive)
  70. else:
  71. self._print_usage()
  72. def _non_interactive_processing(self):
  73. archive_count = 0
  74. while self.stack:
  75. archive_name, archive = self.stack.pop()
  76. archive_count += 1
  77. if archive_count > 1:
  78. print("")
  79. self._show_archive_contents(archive_name, archive)
  80. if not self.recursive_mode:
  81. continue
  82. # Scan for embedded archives
  83. if isinstance(archive, CArchiveReader):
  84. for name, (*_, typecode) in archive.toc.items():
  85. if typecode == 'z':
  86. try:
  87. embedded_archive = archive.open_embedded_archive(name)
  88. except Exception as e:
  89. print(f"Could not open embedded archive {name!r}: {e}", file=sys.stderr)
  90. self.stack.append((name, embedded_archive))
  91. def _print_usage(self):
  92. print("U: go up one level", file=sys.stderr)
  93. print("O <name>: open embedded archive with given name", file=sys.stderr)
  94. print("X <name>: extract file with given name", file=sys.stderr)
  95. print("S: list the contents of current archive again", file=sys.stderr)
  96. print("Q: quit", file=sys.stderr)
  97. def _move_up_the_stack(self):
  98. if len(self.stack) > 1:
  99. self.stack.pop()
  100. archive_name, archive = self.stack[-1]
  101. self._show_archive_contents(archive_name, archive)
  102. else:
  103. print("Already in the top archive!", file=sys.stderr)
  104. def _open_toplevel_archive(self, filename):
  105. if not os.path.isfile(filename):
  106. print(f"Archive {filename} does not exist!", file=sys.stderr)
  107. sys.exit(1)
  108. if filename[-4:].lower() == '.pyz':
  109. return ZlibArchiveReader(filename)
  110. return CArchiveReader(filename)
  111. def _open_embedded_archive(self, archive_name=None):
  112. # Ask for name if not provided
  113. if not archive_name:
  114. archive_name = input('Open name? ')
  115. archive_name = archive_name.strip()
  116. # No name given; abort
  117. if not archive_name:
  118. return
  119. # Open the embedded archive
  120. _, parent_archive = self.stack[-1]
  121. if not hasattr(parent_archive, 'open_embedded_archive'):
  122. print("Archive does not support embedded archives!", file=sys.stderr)
  123. return
  124. try:
  125. archive = parent_archive.open_embedded_archive(archive_name)
  126. except Exception as e:
  127. print(f"Could not open embedded archive {archive_name!r}: {e}", file=sys.stderr)
  128. return
  129. # Add to stack and display contents
  130. self.stack.append((archive_name, archive))
  131. self._show_archive_contents(archive_name, archive)
  132. def _extract_file(self, name=None):
  133. # Ask for name if not provided
  134. if not name:
  135. name = input('Extract name? ')
  136. name = name.strip()
  137. # Archive
  138. archive_name, archive = self.stack[-1]
  139. # Retrieve data
  140. try:
  141. if isinstance(archive, CArchiveReader):
  142. data = archive.extract(name)
  143. elif isinstance(archive, ZlibArchiveReader):
  144. data = archive.extract(name, raw=True)
  145. if data is None:
  146. raise ValueError("Entry has no associated data!")
  147. else:
  148. raise NotImplementedError(f"Extraction from archive type {type(archive)} not implemented!")
  149. except Exception as e:
  150. print(f"Failed to extract data for entry {name!r} from {archive_name!r}: {e}", file=sys.stderr)
  151. return
  152. # Write to file
  153. filename = input('Output filename? ')
  154. if not filename:
  155. print(repr(data))
  156. else:
  157. with open(filename, 'wb') as fp:
  158. fp.write(data)
  159. def _show_archive_contents(self, archive_name, archive):
  160. if isinstance(archive, CArchiveReader):
  161. if archive.options:
  162. print(f"Options in {archive_name!r} (PKG/CArchive):")
  163. for option in archive.options:
  164. print(f" {option}")
  165. print(f"Contents of {archive_name!r} (PKG/CArchive):")
  166. if self.brief_mode:
  167. for name in archive.toc.keys():
  168. print(f" {name}")
  169. else:
  170. print(" position, length, uncompressed_length, is_compressed, typecode, name")
  171. for name, (position, length, uncompressed_length, is_compressed, typecode) in archive.toc.items():
  172. print(f" {position}, {length}, {uncompressed_length}, {is_compressed}, {typecode!r}, {name!r}")
  173. elif isinstance(archive, ZlibArchiveReader):
  174. print(f"Contents of {archive_name!r} (PYZ):")
  175. if self.brief_mode:
  176. for name in archive.toc.keys():
  177. print(f" {name}")
  178. else:
  179. print(" typecode, position, length, name")
  180. for name, (typecode, position, length) in archive.toc.items():
  181. print(f" {typecode}, {position}, {length}, {name!r}")
  182. else:
  183. print(f"Contents of {name} (unknown)")
  184. print(f"FIXME: implement content listing for archive type {type(archive)}!")
  185. def run():
  186. parser = argparse.ArgumentParser()
  187. parser.add_argument(
  188. '-l',
  189. '--list',
  190. default=False,
  191. action='store_true',
  192. dest='listing_mode',
  193. help='List the archive contents and exit (default: %(default)s).',
  194. )
  195. parser.add_argument(
  196. '-r',
  197. '--recursive',
  198. default=False,
  199. action='store_true',
  200. dest='recursive',
  201. help='Recursively print an archive log (default: %(default)s). Implies --list.',
  202. )
  203. parser.add_argument(
  204. '-b',
  205. '--brief',
  206. default=False,
  207. action='store_true',
  208. dest='brief',
  209. help='When displaying archive contents, show only file names. (default: %(default)s).',
  210. )
  211. PyInstaller.log.__add_options(parser)
  212. parser.add_argument(
  213. 'filename',
  214. metavar='pyi_archive',
  215. help="PyInstaller archive to process.",
  216. )
  217. autocomplete(parser)
  218. args = parser.parse_args()
  219. PyInstaller.log.__process_options(parser, args)
  220. try:
  221. viewer = ArchiveViewer(
  222. filename=args.filename,
  223. interactive_mode=not args.listing_mode,
  224. recursive_mode=args.recursive,
  225. brief_mode=args.brief,
  226. )
  227. viewer.main()
  228. except KeyboardInterrupt:
  229. raise SystemExit("Aborted by user.")
  230. if __name__ == '__main__':
  231. run()