pyimod01_archive.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. #-----------------------------------------------------------------------------
  2. # Copyright (c) 2005-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. # **NOTE** This module is used during bootstrap.
  12. # Import *ONLY* builtin modules or modules that are collected into the base_library.zip archive.
  13. # List of built-in modules: sys.builtin_module_names
  14. # List of modules collected into base_library.zip: PyInstaller.compat.PY3_BASE_MODULES
  15. import os
  16. import struct
  17. import marshal
  18. import zlib
  19. # In Python3, the MAGIC_NUMBER value is available in the importlib module. However, in the bootstrap phase we cannot use
  20. # importlib directly, but rather its frozen variant.
  21. import _frozen_importlib
  22. PYTHON_MAGIC_NUMBER = _frozen_importlib._bootstrap_external.MAGIC_NUMBER
  23. # Type codes for PYZ PYZ entries
  24. PYZ_ITEM_MODULE = 0
  25. PYZ_ITEM_PKG = 1
  26. PYZ_ITEM_DATA = 2 # deprecated; PYZ does not contain any data entries anymore
  27. PYZ_ITEM_NSPKG = 3 # PEP-420 namespace package
  28. class ArchiveReadError(RuntimeError):
  29. pass
  30. class ZlibArchiveReader:
  31. """
  32. Reader for PyInstaller's PYZ (ZlibArchive) archive. The archive is used to store collected byte-compiled Python
  33. modules, as individually-compressed entries.
  34. """
  35. _PYZ_MAGIC_PATTERN = b'PYZ\0'
  36. def __init__(self, filename, start_offset=None, check_pymagic=False):
  37. self._filename = filename
  38. self._start_offset = start_offset
  39. self.toc = {}
  40. # If no offset is given, try inferring it from filename
  41. if start_offset is None:
  42. self._filename, self._start_offset = self._parse_offset_from_filename(filename)
  43. # Parse header and load TOC. Standard header contains 12 bytes: PYZ magic pattern, python bytecode magic
  44. # pattern, and offset to TOC (32-bit integer). It might be followed by additional fields, depending on
  45. # implementation version.
  46. with open(self._filename, "rb") as fp:
  47. # Read PYZ magic pattern, located at the start of the file
  48. fp.seek(self._start_offset, os.SEEK_SET)
  49. magic = fp.read(len(self._PYZ_MAGIC_PATTERN))
  50. if magic != self._PYZ_MAGIC_PATTERN:
  51. raise ArchiveReadError("PYZ magic pattern mismatch!")
  52. # Read python magic/version number
  53. pymagic = fp.read(len(PYTHON_MAGIC_NUMBER))
  54. if check_pymagic and pymagic != PYTHON_MAGIC_NUMBER:
  55. raise ArchiveReadError("Python magic pattern mismatch!")
  56. # Read TOC offset
  57. toc_offset, *_ = struct.unpack('!i', fp.read(4))
  58. # Load TOC
  59. fp.seek(self._start_offset + toc_offset, os.SEEK_SET)
  60. self.toc = dict(marshal.load(fp))
  61. @staticmethod
  62. def _parse_offset_from_filename(filename):
  63. """
  64. Parse the numeric offset from filename, stored as: `/path/to/file?offset`.
  65. """
  66. offset = 0
  67. idx = filename.rfind('?')
  68. if idx == -1:
  69. return filename, offset
  70. try:
  71. offset = int(filename[idx + 1:])
  72. filename = filename[:idx] # Remove the offset from filename
  73. except ValueError:
  74. # Ignore spurious "?" in the path (for example, like in Windows UNC \\?\<path>).
  75. pass
  76. return filename, offset
  77. def extract(self, name, raw=False):
  78. """
  79. Extract data from entry with the given name.
  80. If the entry belongs to a module or a package, the data is loaded (unmarshaled) into code object. To retrieve
  81. raw data, set `raw` flag to True.
  82. """
  83. # Look up entry
  84. entry = self.toc.get(name)
  85. if entry is None:
  86. raise KeyError(f"No entry named {name!r} found in the archive!")
  87. typecode, entry_offset, entry_length = entry
  88. # PEP-420 namespace package does not have a data blob.
  89. if typecode == PYZ_ITEM_NSPKG:
  90. return None
  91. # Read data blob
  92. try:
  93. with open(self._filename, "rb") as fp:
  94. fp.seek(self._start_offset + entry_offset)
  95. obj = fp.read(entry_length)
  96. except FileNotFoundError:
  97. # We open the archive file each time we need to read from it, to avoid locking the file by keeping it open.
  98. # This allows executable to be deleted or moved (renamed) while it is running, which is useful in certain
  99. # scenarios (e.g., automatic update that replaces the executable). The caveat is that once the executable is
  100. # renamed, we cannot read from its embedded PYZ archive anymore. In such case, exit with informative
  101. # message.
  102. raise SystemExit(
  103. f"ERROR: {self._filename} appears to have been moved or deleted since this application was launched. "
  104. "Continouation from this state is impossible. Exiting now."
  105. )
  106. try:
  107. obj = zlib.decompress(obj)
  108. if typecode in (PYZ_ITEM_MODULE, PYZ_ITEM_PKG) and not raw:
  109. obj = marshal.loads(obj)
  110. except EOFError as e:
  111. raise ImportError(f"Failed to unmarshal PYZ entry {name!r}!") from e
  112. return obj