readers.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  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. Python-based CArchive (PKG) reader implementation. Used only in the archive_viewer utility.
  13. """
  14. import os
  15. import struct
  16. from PyInstaller.loader.pyimod01_archive import ZlibArchiveReader, ArchiveReadError
  17. class NotAnArchiveError(TypeError):
  18. pass
  19. # Type codes for CArchive TOC entries
  20. PKG_ITEM_BINARY = 'b' # binary
  21. PKG_ITEM_DEPENDENCY = 'd' # runtime option
  22. PKG_ITEM_PYZ = 'z' # zlib (pyz) - frozen Python code
  23. PKG_ITEM_ZIPFILE = 'Z' # zlib (pyz) - frozen Python code
  24. PKG_ITEM_PYPACKAGE = 'M' # Python package (__init__.py)
  25. PKG_ITEM_PYMODULE = 'm' # Python module
  26. PKG_ITEM_PYSOURCE = 's' # Python script (v3)
  27. PKG_ITEM_DATA = 'x' # data
  28. PKG_ITEM_RUNTIME_OPTION = 'o' # runtime option
  29. PKG_ITEM_SPLASH = 'l' # splash resources
  30. class CArchiveReader:
  31. """
  32. Reader for PyInstaller's CArchive (PKG) archive.
  33. """
  34. # Cookie - holds some information for the bootloader. C struct format definition. '!' at the beginning means network
  35. # byte order. C struct looks like:
  36. #
  37. # typedef struct _archive_cookie
  38. # {
  39. # char magic[8];
  40. # uint32_t pkg_length;
  41. # uint32_t toc_offset;
  42. # uint32_t toc_length;
  43. # uint32_t python_version;
  44. # char python_libname[64];
  45. # } ARCHIVE_COOKIE;
  46. #
  47. _COOKIE_MAGIC_PATTERN = b'MEI\014\013\012\013\016'
  48. _COOKIE_FORMAT = '!8sIIII64s'
  49. _COOKIE_LENGTH = struct.calcsize(_COOKIE_FORMAT)
  50. # TOC entry:
  51. #
  52. # typedef struct _toc_entry
  53. # {
  54. # uint32_t entry_length;
  55. # uint32_t offset;
  56. # uint32_t length;
  57. # uint32_t uncompressed_length;
  58. # unsigned char compression_flag;
  59. # char typecode;
  60. # char name[1]; /* Variable-length name, padded to multiple of 16 */
  61. # } TOC_ENTRY;
  62. #
  63. _TOC_ENTRY_FORMAT = '!IIIIBc'
  64. _TOC_ENTRY_LENGTH = struct.calcsize(_TOC_ENTRY_FORMAT)
  65. def __init__(self, filename):
  66. self._filename = filename
  67. self._start_offset = 0
  68. self._end_offset = 0
  69. self._toc_offset = 0
  70. self._toc_length = 0
  71. self.toc = {}
  72. self.options = []
  73. # Load TOC
  74. with open(self._filename, "rb") as fp:
  75. # Find cookie MAGIC pattern
  76. cookie_start_offset = self._find_magic_pattern(fp, self._COOKIE_MAGIC_PATTERN)
  77. if cookie_start_offset == -1:
  78. raise ArchiveReadError("Could not find COOKIE magic pattern!")
  79. # Read the whole cookie
  80. fp.seek(cookie_start_offset, os.SEEK_SET)
  81. cookie_data = fp.read(self._COOKIE_LENGTH)
  82. magic, archive_length, toc_offset, toc_length, pyvers, pylib_name = \
  83. struct.unpack(self._COOKIE_FORMAT, cookie_data)
  84. # Compute start and end offset of the the archive
  85. self._end_offset = cookie_start_offset + self._COOKIE_LENGTH
  86. self._start_offset = self._end_offset - archive_length
  87. # Verify that Python shared library name is set
  88. if not pylib_name:
  89. raise ArchiveReadError("Python shared library name not set in the archive!")
  90. # Read whole toc
  91. fp.seek(self._start_offset + toc_offset)
  92. toc_data = fp.read(toc_length)
  93. self.toc, self.options = self._parse_toc(toc_data)
  94. @staticmethod
  95. def _find_magic_pattern(fp, magic_pattern):
  96. # Start at the end of file, and scan back-to-start
  97. fp.seek(0, os.SEEK_END)
  98. end_pos = fp.tell()
  99. # Scan from back
  100. SEARCH_CHUNK_SIZE = 8192
  101. magic_offset = -1
  102. while end_pos >= len(magic_pattern):
  103. start_pos = max(end_pos - SEARCH_CHUNK_SIZE, 0)
  104. chunk_size = end_pos - start_pos
  105. # Is the remaining chunk large enough to hold the pattern?
  106. if chunk_size < len(magic_pattern):
  107. break
  108. # Read and scan the chunk
  109. fp.seek(start_pos, os.SEEK_SET)
  110. buf = fp.read(chunk_size)
  111. pos = buf.rfind(magic_pattern)
  112. if pos != -1:
  113. magic_offset = start_pos + pos
  114. break
  115. # Adjust search location for next chunk; ensure proper overlap
  116. end_pos = start_pos + len(magic_pattern) - 1
  117. return magic_offset
  118. @classmethod
  119. def _parse_toc(cls, data):
  120. options = []
  121. toc = {}
  122. cur_pos = 0
  123. while cur_pos < len(data):
  124. # Read and parse the fixed-size TOC entry header
  125. entry_length, entry_offset, data_length, uncompressed_length, compression_flag, typecode = \
  126. struct.unpack(cls._TOC_ENTRY_FORMAT, data[cur_pos:(cur_pos + cls._TOC_ENTRY_LENGTH)])
  127. cur_pos += cls._TOC_ENTRY_LENGTH
  128. # Read variable-length name
  129. name_length = entry_length - cls._TOC_ENTRY_LENGTH
  130. name, *_ = struct.unpack(f'{name_length}s', data[cur_pos:(cur_pos + name_length)])
  131. cur_pos += name_length
  132. # Name string may contain up to 15 bytes of padding
  133. name = name.rstrip(b'\0').decode('utf-8')
  134. typecode = typecode.decode('ascii')
  135. # The TOC should not contain duplicates, except for OPTION entries. Therefore, keep those
  136. # in a separate list. With options, the rest of the entries do not make sense, anyway.
  137. if typecode == 'o':
  138. options.append(name)
  139. else:
  140. toc[name] = (entry_offset, data_length, uncompressed_length, compression_flag, typecode)
  141. return toc, options
  142. def extract(self, name):
  143. """
  144. Extract data for the given entry name.
  145. """
  146. entry = self.toc.get(name)
  147. if entry is None:
  148. raise KeyError(f"No entry named {name!r} found in the archive!")
  149. entry_offset, data_length, uncompressed_length, compression_flag, typecode = entry
  150. with open(self._filename, "rb") as fp:
  151. fp.seek(self._start_offset + entry_offset, os.SEEK_SET)
  152. data = fp.read(data_length)
  153. if compression_flag:
  154. import zlib
  155. data = zlib.decompress(data)
  156. return data
  157. def raw_pkg_data(self):
  158. """
  159. Extract complete PKG/CArchive archive from the parent file (executable).
  160. """
  161. total_length = self._end_offset - self._start_offset
  162. with open(self._filename, "rb") as fp:
  163. fp.seek(self._start_offset, os.SEEK_SET)
  164. return fp.read(total_length)
  165. def open_embedded_archive(self, name):
  166. """
  167. Open new archive reader for the embedded archive.
  168. """
  169. entry = self.toc.get(name)
  170. if entry is None:
  171. raise KeyError(f"No entry named {name!r} found in the archive!")
  172. entry_offset, data_length, uncompressed_length, compression_flag, typecode = entry
  173. if typecode == PKG_ITEM_PYZ:
  174. # Open as embedded archive, without extraction.
  175. return ZlibArchiveReader(self._filename, self._start_offset + entry_offset)
  176. elif typecode == PKG_ITEM_ZIPFILE:
  177. raise NotAnArchiveError("Zipfile archives not supported yet!")
  178. else:
  179. raise NotAnArchiveError(f"Entry {name!r} is not a supported embedded archive!")
  180. def pkg_archive_contents(filename, recursive=True):
  181. """
  182. List the contents of the PKG / CArchive. If `recursive` flag is set (the default), the contents of the embedded PYZ
  183. archive is included as well.
  184. Used by the tests.
  185. """
  186. contents = []
  187. pkg_archive = CArchiveReader(filename)
  188. for name, toc_entry in pkg_archive.toc.items():
  189. *_, typecode = toc_entry
  190. contents.append(name)
  191. if typecode == PKG_ITEM_PYZ and recursive:
  192. pyz_archive = pkg_archive.open_embedded_archive(name)
  193. for name in pyz_archive.toc.keys():
  194. contents.append(name)
  195. return contents