Remove hardcoded libpython binaries and add debug step
All checks were successful
build / build-linux (push) Successful in 16s

This commit is contained in:
kdusek
2025-12-07 23:15:18 +01:00
parent 308ce7768e
commit 6a1fe63684
1807 changed files with 172293 additions and 1 deletions

View File

@@ -0,0 +1 @@
__author__ = 'martin'

View File

@@ -0,0 +1,251 @@
#-----------------------------------------------------------------------------
# Copyright (c) 2013-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------
"""
The code in this module supports the --icon parameter on Windows.
(For --icon support under macOS, see building/osx.py.)
The only entry point, called from api.py, is CopyIcons(), below. All the elaborate structure of classes that follows
is used to support the operation of CopyIcons_FromIco(). None of these classes and globals are referenced outside
this module.
"""
import os
import os.path
import struct
import PyInstaller.log as logging
from PyInstaller import config
from PyInstaller.compat import pywintypes, win32api
from PyInstaller.building.icon import normalize_icon_type
logger = logging.getLogger(__name__)
RT_ICON = 3
RT_GROUP_ICON = 14
LOAD_LIBRARY_AS_DATAFILE = 2
class Structure:
def __init__(self):
size = self._sizeInBytes = struct.calcsize(self._format_)
self._fields_ = list(struct.unpack(self._format_, b'\000' * size))
indexes = self._indexes_ = {}
for i, nm in enumerate(self._names_):
indexes[nm] = i
def dump(self):
logger.info("DUMP of %s", self)
for name in self._names_:
if not name.startswith('_'):
logger.info("%20s = %s", name, getattr(self, name))
logger.info("")
def __getattr__(self, name):
if name in self._names_:
index = self._indexes_[name]
return self._fields_[index]
try:
return self.__dict__[name]
except KeyError as e:
raise AttributeError(name) from e
def __setattr__(self, name, value):
if name in self._names_:
index = self._indexes_[name]
self._fields_[index] = value
else:
self.__dict__[name] = value
def tostring(self):
return struct.pack(self._format_, *self._fields_)
def fromfile(self, file):
data = file.read(self._sizeInBytes)
self._fields_ = list(struct.unpack(self._format_, data))
class ICONDIRHEADER(Structure):
_names_ = "idReserved", "idType", "idCount"
_format_ = "hhh"
class ICONDIRENTRY(Structure):
_names_ = ("bWidth", "bHeight", "bColorCount", "bReserved", "wPlanes", "wBitCount", "dwBytesInRes", "dwImageOffset")
_format_ = "bbbbhhii"
class GRPICONDIR(Structure):
_names_ = "idReserved", "idType", "idCount"
_format_ = "hhh"
class GRPICONDIRENTRY(Structure):
_names_ = ("bWidth", "bHeight", "bColorCount", "bReserved", "wPlanes", "wBitCount", "dwBytesInRes", "nID")
_format_ = "bbbbhhih"
# An IconFile instance is created for each .ico file given.
class IconFile:
def __init__(self, path):
self.path = path
try:
# The path is from the user parameter, don't trust it.
file = open(self.path, "rb")
except OSError:
# The icon file can't be opened for some reason. Stop the
# program with an informative message.
raise SystemExit(f'ERROR: Unable to open icon file {self.path}!')
with file:
self.entries = []
self.images = []
header = self.header = ICONDIRHEADER()
header.fromfile(file)
for i in range(header.idCount):
entry = ICONDIRENTRY()
entry.fromfile(file)
self.entries.append(entry)
for e in self.entries:
file.seek(e.dwImageOffset, 0)
self.images.append(file.read(e.dwBytesInRes))
def grp_icon_dir(self):
return self.header.tostring()
def grp_icondir_entries(self, id=1):
data = b''
for entry in self.entries:
e = GRPICONDIRENTRY()
for n in e._names_[:-1]:
setattr(e, n, getattr(entry, n))
e.nID = id
id = id + 1
data = data + e.tostring()
return data
def CopyIcons_FromIco(dstpath, srcpath, id=1):
"""
Use the Win API UpdateResource facility to apply the icon resource(s) to the .exe file.
:param str dstpath: absolute path of the .exe file being built.
:param str srcpath: list of 1 or more .ico file paths
"""
icons = map(IconFile, srcpath)
logger.debug("Copying icons from %s", srcpath)
hdst = win32api.BeginUpdateResource(dstpath, 0)
iconid = 1
# Each step in the following enumerate() will instantiate an IconFile object, as a result of deferred execution
# of the map() above.
for i, f in enumerate(icons):
data = f.grp_icon_dir()
data = data + f.grp_icondir_entries(iconid)
win32api.UpdateResource(hdst, RT_GROUP_ICON, i + 1, data)
logger.debug("Writing RT_GROUP_ICON %d resource with %d bytes", i + 1, len(data))
for data in f.images:
win32api.UpdateResource(hdst, RT_ICON, iconid, data)
logger.debug("Writing RT_ICON %d resource with %d bytes", iconid, len(data))
iconid = iconid + 1
win32api.EndUpdateResource(hdst, 0)
def CopyIcons(dstpath, srcpath):
"""
Called from building/api.py to handle icons. If the input was by --icon on the command line, srcpath is a single
string. However, it is possible to modify the spec file adding icon=['foo.ico','bar.ico'] to the EXE() statement.
In that case, srcpath is a list of strings.
The string format is either path-to-.ico or path-to-.exe,n for n an integer resource index in the .exe. In either
case, the path can be relative or absolute.
"""
if isinstance(srcpath, (str, os.PathLike)):
# Just a single string, make it a one-element list.
srcpath = [srcpath]
# Convert possible PathLike elements to strings to allow the splitter function to work.
srcpath = [str(path) for path in srcpath]
def splitter(s):
"""
Convert "pathname" to tuple ("pathname", None)
Convert "pathname,n" to tuple ("pathname", n)
"""
try:
srcpath, index = s.split(',')
return srcpath.strip(), int(index)
except ValueError:
return s, None
# split all the items in the list into tuples as above.
srcpath = list(map(splitter, srcpath))
if len(srcpath) > 1:
# More than one icon source given. We currently handle multiple icons by calling CopyIcons_FromIco(), which only
# allows .ico, but will convert to that format if needed.
#
# Note that a ",index" on a .ico is just ignored in the single or multiple case.
srcs = []
for s in srcpath:
srcs.append(normalize_icon_type(s[0], ("ico",), "ico", config.CONF["workpath"]))
return CopyIcons_FromIco(dstpath, srcs)
# Just one source given.
srcpath, index = srcpath[0]
# Makes sure the icon exists and attempts to convert to the proper format if applicable
srcpath = normalize_icon_type(srcpath, ("exe", "ico"), "ico", config.CONF["workpath"])
srcext = os.path.splitext(srcpath)[1]
# Handle the simple case of foo.ico, ignoring any index.
if srcext.lower() == '.ico':
return CopyIcons_FromIco(dstpath, [srcpath])
# Single source is not .ico, presumably it is .exe (and if not, some error will occur).
if index is not None:
logger.debug("Copying icon from %s, %d", srcpath, index)
else:
logger.debug("Copying icons from %s", srcpath)
try:
# Attempt to load the .ico or .exe containing the icon into memory using the same mechanism as if it were a DLL.
# If this fails for any reason (for example if the file does not exist or is not a .ico/.exe) then LoadLibraryEx
# returns a null handle and win32api raises a unique exception with a win error code and a string.
hsrc = win32api.LoadLibraryEx(srcpath, 0, LOAD_LIBRARY_AS_DATAFILE)
except pywintypes.error as W32E:
# We could continue with no icon (i.e., just return), but it seems best to terminate the build with a message.
raise SystemExit(
"ERROR: Unable to load icon file {}\n {} (Error code {})".format(srcpath, W32E.strerror, W32E.winerror)
)
hdst = win32api.BeginUpdateResource(dstpath, 0)
if index is None:
grpname = win32api.EnumResourceNames(hsrc, RT_GROUP_ICON)[0]
elif index >= 0:
grpname = win32api.EnumResourceNames(hsrc, RT_GROUP_ICON)[index]
else:
grpname = -index
data = win32api.LoadResource(hsrc, RT_GROUP_ICON, grpname)
win32api.UpdateResource(hdst, RT_GROUP_ICON, grpname, data)
for iconname in win32api.EnumResourceNames(hsrc, RT_ICON):
data = win32api.LoadResource(hsrc, RT_ICON, iconname)
win32api.UpdateResource(hdst, RT_ICON, iconname, data)
win32api.FreeLibrary(hsrc)
win32api.EndUpdateResource(hdst, 0)
if __name__ == "__main__":
import sys
dstpath = sys.argv[1]
srcpath = sys.argv[2:]
CopyIcons(dstpath, srcpath)

View File

@@ -0,0 +1,604 @@
#-----------------------------------------------------------------------------
# Copyright (c) 2013-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------
import struct
import pefile
from PyInstaller.compat import win32api
def pefile_check_control_flow_guard(filename):
"""
Checks if the specified PE file has CFG (Control Flow Guard) enabled.
Parameters
----------
filename : str
Path to the PE file to inspect.
Returns
----------
bool
True if file is a PE file with CFG enabled. False if CFG is not enabled or if file could not be processed using
the pefile library.
"""
try:
pe = pefile.PE(filename, fast_load=True)
# https://docs.microsoft.com/en-us/windows/win32/debug/pe-format
# IMAGE_DLLCHARACTERISTICS_GUARD_CF = 0x4000
return bool(pe.OPTIONAL_HEADER.DllCharacteristics & 0x4000)
except Exception:
return False
# Ensures no code from the executable is executed.
LOAD_LIBRARY_AS_DATAFILE = 2
def getRaw(text):
"""
Encodes text as UTF-16LE (Microsoft 'Unicode') for use in structs.
"""
return text.encode('UTF-16LE')
def read_version_info_from_executable(exe_filename):
"""
Read the version information structure from the given executable's resources, and return it as an instance of
`VSVersionInfo` structure.
"""
h = win32api.LoadLibraryEx(exe_filename, 0, LOAD_LIBRARY_AS_DATAFILE)
res = win32api.EnumResourceNames(h, pefile.RESOURCE_TYPE['RT_VERSION'])
if not len(res):
return None
data = win32api.LoadResource(h, pefile.RESOURCE_TYPE['RT_VERSION'], res[0])
info = VSVersionInfo()
info.fromRaw(data)
win32api.FreeLibrary(h)
return info
def nextDWord(offset):
"""
Align `offset` to the next 4-byte boundary.
"""
return ((offset + 3) >> 2) << 2
class VSVersionInfo:
"""
WORD wLength; // length of the VS_VERSION_INFO structure
WORD wValueLength; // length of the Value member
WORD wType; // 1 means text, 0 means binary
WCHAR szKey[]; // Contains the Unicode string "VS_VERSION_INFO".
WORD Padding1[];
VS_FIXEDFILEINFO Value;
WORD Padding2[];
WORD Children[]; // zero or more StringFileInfo or VarFileInfo
// structures (or both) that are children of the
// current version structure.
"""
def __init__(self, ffi=None, kids=None):
self.ffi = ffi
self.kids = kids or []
def fromRaw(self, data):
i, (sublen, vallen, wType, nm) = parseCommon(data)
#vallen is length of the ffi, typ is 0, nm is 'VS_VERSION_INFO'.
i = nextDWord(i)
# Now a VS_FIXEDFILEINFO
self.ffi = FixedFileInfo()
j = self.ffi.fromRaw(data, i)
i = j
while i < sublen:
j = i
i, (csublen, cvallen, ctyp, nm) = parseCommon(data, i)
if nm.strip() == 'StringFileInfo':
sfi = StringFileInfo()
k = sfi.fromRaw(csublen, cvallen, nm, data, i, j + csublen)
self.kids.append(sfi)
i = k
else:
vfi = VarFileInfo()
k = vfi.fromRaw(csublen, cvallen, nm, data, i, j + csublen)
self.kids.append(vfi)
i = k
i = j + csublen
i = nextDWord(i)
return i
def toRaw(self):
raw_name = getRaw('VS_VERSION_INFO')
rawffi = self.ffi.toRaw()
vallen = len(rawffi)
typ = 0
sublen = 6 + len(raw_name) + 2
pad = b''
if sublen % 4:
pad = b'\000\000'
sublen = sublen + len(pad) + vallen
pad2 = b''
if sublen % 4:
pad2 = b'\000\000'
tmp = b''.join([kid.toRaw() for kid in self.kids])
sublen = sublen + len(pad2) + len(tmp)
return struct.pack('HHH', sublen, vallen, typ) + raw_name + b'\000\000' + pad + rawffi + pad2 + tmp
def __eq__(self, other):
return self.toRaw() == other
def __str__(self, indent=''):
indent = indent + ' '
tmp = [kid.__str__(indent + ' ') for kid in self.kids]
tmp = ', \n'.join(tmp)
return '\n'.join([
"# UTF-8",
"#",
"# For more details about fixed file info 'ffi' see:",
"# http://msdn.microsoft.com/en-us/library/ms646997.aspx",
"VSVersionInfo(",
indent + f"ffi={self.ffi.__str__(indent)},",
indent + "kids=[",
tmp,
indent + "]",
")",
])
def __repr__(self):
return "versioninfo.VSVersionInfo(ffi=%r, kids=%r)" % (self.ffi, self.kids)
def parseCommon(data, start=0):
i = start + 6
(wLength, wValueLength, wType) = struct.unpack('3H', data[start:i])
i, text = parseUString(data, i, i + wLength)
return i, (wLength, wValueLength, wType, text)
def parseUString(data, start, limit):
i = start
while i < limit:
if data[i:i + 2] == b'\000\000':
break
i += 2
text = data[start:i].decode('UTF-16LE')
i += 2
return i, text
class FixedFileInfo:
"""
DWORD dwSignature; //Contains the value 0xFEEFO4BD
DWORD dwStrucVersion; // binary version number of this structure.
// The high-order word of this member contains
// the major version number, and the low-order
// word contains the minor version number.
DWORD dwFileVersionMS; // most significant 32 bits of the file's binary
// version number
DWORD dwFileVersionLS; //
DWORD dwProductVersionMS; // most significant 32 bits of the binary version
// number of the product with which this file was
// distributed
DWORD dwProductVersionLS; //
DWORD dwFileFlagsMask; // bitmask that specifies the valid bits in
// dwFileFlags. A bit is valid only if it was
// defined when the file was created.
DWORD dwFileFlags; // VS_FF_DEBUG, VS_FF_PATCHED etc.
DWORD dwFileOS; // VOS_NT, VOS_WINDOWS32 etc.
DWORD dwFileType; // VFT_APP etc.
DWORD dwFileSubtype; // 0 unless VFT_DRV or VFT_FONT or VFT_VXD
DWORD dwFileDateMS;
DWORD dwFileDateLS;
"""
def __init__(
self,
filevers=(0, 0, 0, 0),
prodvers=(0, 0, 0, 0),
mask=0x3f,
flags=0x0,
OS=0x40004,
fileType=0x1,
subtype=0x0,
date=(0, 0)
):
self.sig = 0xfeef04bd
self.strucVersion = 0x10000
self.fileVersionMS = (filevers[0] << 16) | (filevers[1] & 0xffff)
self.fileVersionLS = (filevers[2] << 16) | (filevers[3] & 0xffff)
self.productVersionMS = (prodvers[0] << 16) | (prodvers[1] & 0xffff)
self.productVersionLS = (prodvers[2] << 16) | (prodvers[3] & 0xffff)
self.fileFlagsMask = mask
self.fileFlags = flags
self.fileOS = OS
self.fileType = fileType
self.fileSubtype = subtype
self.fileDateMS = date[0]
self.fileDateLS = date[1]
def fromRaw(self, data, i):
(
self.sig,
self.strucVersion,
self.fileVersionMS,
self.fileVersionLS,
self.productVersionMS,
self.productVersionLS,
self.fileFlagsMask,
self.fileFlags,
self.fileOS,
self.fileType,
self.fileSubtype,
self.fileDateMS,
self.fileDateLS,
) = struct.unpack('13L', data[i:i + 52])
return i + 52
def toRaw(self):
return struct.pack(
'13L',
self.sig,
self.strucVersion,
self.fileVersionMS,
self.fileVersionLS,
self.productVersionMS,
self.productVersionLS,
self.fileFlagsMask,
self.fileFlags,
self.fileOS,
self.fileType,
self.fileSubtype,
self.fileDateMS,
self.fileDateLS,
)
def __eq__(self, other):
return self.toRaw() == other
def __str__(self, indent=''):
fv = (
self.fileVersionMS >> 16, self.fileVersionMS & 0xffff,
self.fileVersionLS >> 16, self.fileVersionLS & 0xffff,
) # yapf: disable
pv = (
self.productVersionMS >> 16, self.productVersionMS & 0xffff,
self.productVersionLS >> 16, self.productVersionLS & 0xffff,
) # yapf: disable
fd = (self.fileDateMS, self.fileDateLS)
tmp = [
'FixedFileInfo(',
'# filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4)',
'# Set not needed items to zero 0.',
'filevers=%s,' % (fv,),
'prodvers=%s,' % (pv,),
"# Contains a bitmask that specifies the valid bits 'flags'r",
'mask=%s,' % hex(self.fileFlagsMask),
'# Contains a bitmask that specifies the Boolean attributes of the file.',
'flags=%s,' % hex(self.fileFlags),
'# The operating system for which this file was designed.',
'# 0x4 - NT and there is no need to change it.',
'OS=%s,' % hex(self.fileOS),
'# The general type of file.',
'# 0x1 - the file is an application.',
'fileType=%s,' % hex(self.fileType),
'# The function of the file.',
'# 0x0 - the function is not defined for this fileType',
'subtype=%s,' % hex(self.fileSubtype),
'# Creation date and time stamp.',
'date=%s' % (fd,),
')',
]
return f'\n{indent} '.join(tmp)
def __repr__(self):
fv = (
self.fileVersionMS >> 16, self.fileVersionMS & 0xffff,
self.fileVersionLS >> 16, self.fileVersionLS & 0xffff,
) # yapf: disable
pv = (
self.productVersionMS >> 16, self.productVersionMS & 0xffff,
self.productVersionLS >> 16, self.productVersionLS & 0xffff,
) # yapf: disable
fd = (self.fileDateMS, self.fileDateLS)
return (
'versioninfo.FixedFileInfo(filevers=%r, prodvers=%r, '
'mask=0x%x, flags=0x%x, OS=0x%x, '
'fileType=%r, subtype=0x%x, date=%r)' %
(fv, pv, self.fileFlagsMask, self.fileFlags, self.fileOS, self.fileType, self.fileSubtype, fd)
)
class StringFileInfo:
"""
WORD wLength; // length of the version resource
WORD wValueLength; // length of the Value member in the current
// VS_VERSION_INFO structure
WORD wType; // 1 means text, 0 means binary
WCHAR szKey[]; // Contains the Unicode string 'StringFileInfo'.
WORD Padding[];
StringTable Children[]; // list of zero or more String structures
"""
def __init__(self, kids=None):
self.name = 'StringFileInfo'
self.kids = kids or []
def fromRaw(self, sublen, vallen, name, data, i, limit):
self.name = name
while i < limit:
st = StringTable()
j = st.fromRaw(data, i, limit)
self.kids.append(st)
i = j
return i
def toRaw(self):
raw_name = getRaw(self.name)
vallen = 0
typ = 1
sublen = 6 + len(raw_name) + 2
pad = b''
if sublen % 4:
pad = b'\000\000'
tmp = b''.join([kid.toRaw() for kid in self.kids])
sublen = sublen + len(pad) + len(tmp)
return struct.pack('HHH', sublen, vallen, typ) + raw_name + b'\000\000' + pad + tmp
def __eq__(self, other):
return self.toRaw() == other
def __str__(self, indent=''):
new_indent = indent + ' '
tmp = ', \n'.join(kid.__str__(new_indent) for kid in self.kids)
return f'{indent}StringFileInfo(\n{new_indent}[\n{tmp}\n{new_indent}])'
def __repr__(self):
return 'versioninfo.StringFileInfo(%r)' % self.kids
class StringTable:
"""
WORD wLength;
WORD wValueLength;
WORD wType;
WCHAR szKey[];
String Children[]; // list of zero or more String structures.
"""
def __init__(self, name=None, kids=None):
self.name = name or ''
self.kids = kids or []
def fromRaw(self, data, i, limit):
i, (cpsublen, cpwValueLength, cpwType, self.name) = parseCodePage(data, i, limit) # should be code page junk
i = nextDWord(i)
while i < limit:
ss = StringStruct()
j = ss.fromRaw(data, i, limit)
i = j
self.kids.append(ss)
i = nextDWord(i)
return i
def toRaw(self):
raw_name = getRaw(self.name)
vallen = 0
typ = 1
sublen = 6 + len(raw_name) + 2
tmp = []
for kid in self.kids:
raw = kid.toRaw()
if len(raw) % 4:
raw = raw + b'\000\000'
tmp.append(raw)
tmp = b''.join(tmp)
sublen += len(tmp)
return struct.pack('HHH', sublen, vallen, typ) + raw_name + b'\000\000' + tmp
def __eq__(self, other):
return self.toRaw() == other
def __str__(self, indent=''):
new_indent = indent + ' '
tmp = (',\n' + new_indent).join(str(kid) for kid in self.kids)
return f"{indent}StringTable(\n{new_indent}'{self.name}',\n{new_indent}[{tmp}])"
def __repr__(self):
return 'versioninfo.StringTable(%r, %r)' % (self.name, self.kids)
class StringStruct:
"""
WORD wLength;
WORD wValueLength;
WORD wType;
WCHAR szKey[];
WORD Padding[];
String Value[];
"""
def __init__(self, name=None, val=None):
self.name = name or ''
self.val = val or ''
def fromRaw(self, data, i, limit):
i, (sublen, vallen, typ, self.name) = parseCommon(data, i)
limit = i + sublen
i = nextDWord(i)
i, self.val = parseUString(data, i, limit)
return i
def toRaw(self):
raw_name = getRaw(self.name)
raw_val = getRaw(self.val)
# TODO: document the size of vallen and sublen.
vallen = len(self.val) + 1 # Number of (wide-)characters, not bytes!
typ = 1
sublen = 6 + len(raw_name) + 2
pad = b''
if sublen % 4:
pad = b'\000\000'
sublen = sublen + len(pad) + (vallen * 2)
return struct.pack('HHH', sublen, vallen, typ) + raw_name + b'\000\000' + pad + raw_val + b'\000\000'
def __eq__(self, other):
return self.toRaw() == other
def __str__(self, indent=''):
return "StringStruct(%r, %r)" % (self.name, self.val)
def __repr__(self):
return 'versioninfo.StringStruct(%r, %r)' % (self.name, self.val)
def parseCodePage(data, i, limit):
i, (sublen, wValueLength, wType, nm) = parseCommon(data, i)
return i, (sublen, wValueLength, wType, nm)
class VarFileInfo:
"""
WORD wLength; // length of the version resource
WORD wValueLength; // length of the Value member in the current
// VS_VERSION_INFO structure
WORD wType; // 1 means text, 0 means binary
WCHAR szKey[]; // Contains the Unicode string 'VarFileInfo'.
WORD Padding[];
Var Children[]; // list of zero or more Var structures
"""
def __init__(self, kids=None):
self.kids = kids or []
def fromRaw(self, sublen, vallen, name, data, i, limit):
self.sublen = sublen
self.vallen = vallen
self.name = name
i = nextDWord(i)
while i < limit:
vs = VarStruct()
j = vs.fromRaw(data, i, limit)
self.kids.append(vs)
i = j
return i
def toRaw(self):
self.vallen = 0
self.wType = 1
self.name = 'VarFileInfo'
raw_name = getRaw(self.name)
sublen = 6 + len(raw_name) + 2
pad = b''
if sublen % 4:
pad = b'\000\000'
tmp = b''.join([kid.toRaw() for kid in self.kids])
self.sublen = sublen + len(pad) + len(tmp)
return struct.pack('HHH', self.sublen, self.vallen, self.wType) + raw_name + b'\000\000' + pad + tmp
def __eq__(self, other):
return self.toRaw() == other
def __str__(self, indent=''):
return indent + "VarFileInfo([%s])" % ', '.join(str(kid) for kid in self.kids)
def __repr__(self):
return 'versioninfo.VarFileInfo(%r)' % self.kids
class VarStruct:
"""
WORD wLength; // length of the version resource
WORD wValueLength; // length of the Value member in the current
// VS_VERSION_INFO structure
WORD wType; // 1 means text, 0 means binary
WCHAR szKey[]; // Contains the Unicode string 'Translation'
// or a user-defined key string value
WORD Padding[]; //
WORD Value[]; // list of one or more values that are language
// and code-page identifiers
"""
def __init__(self, name=None, kids=None):
self.name = name or ''
self.kids = kids or []
def fromRaw(self, data, i, limit):
i, (self.sublen, self.wValueLength, self.wType, self.name) = parseCommon(data, i)
i = nextDWord(i)
for j in range(0, self.wValueLength, 2):
kid = struct.unpack('H', data[i:i + 2])[0]
self.kids.append(kid)
i += 2
return i
def toRaw(self):
self.wValueLength = len(self.kids) * 2
self.wType = 0
raw_name = getRaw(self.name)
sublen = 6 + len(raw_name) + 2
pad = b''
if sublen % 4:
pad = b'\000\000'
self.sublen = sublen + len(pad) + self.wValueLength
tmp = b''.join([struct.pack('H', kid) for kid in self.kids])
return struct.pack('HHH', self.sublen, self.wValueLength, self.wType) + raw_name + b'\000\000' + pad + tmp
def __eq__(self, other):
return self.toRaw() == other
def __str__(self, indent=''):
return "VarStruct('%s', %r)" % (self.name, self.kids)
def __repr__(self):
return 'versioninfo.VarStruct(%r, %r)' % (self.name, self.kids)
def load_version_info_from_text_file(filename):
"""
Load the `VSVersionInfo` structure from its string-based (`VSVersionInfo.__str__`) serialization by reading the
text from the file and running it through `eval()`.
"""
# Read and parse the version file. It may have a byte order marker or encoding cookie - respect it if it does.
import PyInstaller.utils.misc as miscutils
with open(filename, 'rb') as fp:
text = miscutils.decode(fp.read())
# Deserialize via eval()
try:
info = eval(text)
except Exception as e:
raise ValueError("Failed to deserialize VSVersionInfo from text-based representation!") from e
# Sanity check
assert isinstance(info, VSVersionInfo), \
f"Loaded incompatible structure type! Expected VSVersionInfo, got: {type(info)!r}"
return info
def write_version_info_to_executable(exe_filename, info):
assert isinstance(info, VSVersionInfo)
# Remember overlay
pe = pefile.PE(exe_filename, fast_load=True)
overlay_before = pe.get_overlay()
pe.close()
hdst = win32api.BeginUpdateResource(exe_filename, 0)
win32api.UpdateResource(hdst, pefile.RESOURCE_TYPE['RT_VERSION'], 1, info.toRaw())
win32api.EndUpdateResource(hdst, 0)
if overlay_before:
# Check if the overlay is still present
pe = pefile.PE(exe_filename, fast_load=True)
overlay_after = pe.get_overlay()
pe.close()
# If the update removed the overlay data, re-append it
if not overlay_after:
with open(exe_filename, 'ab') as exef:
exef.write(overlay_before)

View File

@@ -0,0 +1,244 @@
#-----------------------------------------------------------------------------
# Copyright (c) 2013-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------
import xml.dom
import xml.dom.minidom
#- Relevant constants from Windows headers
# Manifest resource code
RT_MANIFEST = 24
# Resource IDs (names) for manifest.
# See: https://www.gamedev.net/blogs/entry/2154553-manifest-embedding-and-activation
CREATEPROCESS_MANIFEST_RESOURCE_ID = 1
ISOLATIONAWARE_MANIFEST_RESOURCE_ID = 2
LANG_NEUTRAL = 0
#- Default application manifest template, based on the one found in python executable.
_DEFAULT_MANIFEST_XML = \
b"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker" uiAccess="false"></requestedExecutionLevel>
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"></supportedOS>
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"></supportedOS>
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"></supportedOS>
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"></supportedOS>
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"></supportedOS>
</application>
</compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
</windowsSettings>
</application>
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"></assemblyIdentity>
</dependentAssembly>
</dependency>
</assembly>
""" # noqa: E122,E501
#- DOM navigation helpers
def _find_elements_by_tag(root, tag):
"""
Find all elements with given tag under the given root element.
"""
return [node for node in root.childNodes if node.nodeType == xml.dom.Node.ELEMENT_NODE and node.tagName == tag]
def _find_element_by_tag(root, tag):
"""
Attempt to find a single element with given tag under the given root element, and return None if no such element
is found. Raises an error if multiple elements are found.
"""
elements = _find_elements_by_tag(root, tag)
if len(elements) > 1:
raise ValueError(f"Expected a single {tag!r} element, found {len(elements)} element(s)!")
if not elements:
return None
return elements[0]
#- Application manifest modification helpers
def _set_execution_level(manifest_dom, root_element, uac_admin=False, uac_uiaccess=False):
"""
Find <security> -> <requestedPrivileges> -> <requestedExecutionLevel> element, and set its `level` and `uiAccess`
attributes based on supplied arguments. Create the XML elements if necessary, as they are optional.
"""
# <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
trust_info_element = _find_element_by_tag(root_element, "trustInfo")
if not trust_info_element:
trust_info_element = manifest_dom.createElement("trustInfo")
trust_info_element.setAttribute("xmlns", "urn:schemas-microsoft-com:asm.v3")
root_element.appendChild(trust_info_element)
# <security>
security_element = _find_element_by_tag(trust_info_element, "security")
if not security_element:
security_element = manifest_dom.createElement("security")
trust_info_element.appendChild(security_element)
# <requestedPrivileges>
requested_privileges_element = _find_element_by_tag(security_element, "requestedPrivileges")
if not requested_privileges_element:
requested_privileges_element = manifest_dom.createElement("requestedPrivileges")
security_element.appendChild(requested_privileges_element)
# <requestedExecutionLevel>
requested_execution_level_element = _find_element_by_tag(requested_privileges_element, "requestedExecutionLevel")
if not requested_execution_level_element:
requested_execution_level_element = manifest_dom.createElement("requestedExecutionLevel")
requested_privileges_element.appendChild(requested_execution_level_element)
requested_execution_level_element.setAttribute("level", "requireAdministrator" if uac_admin else "asInvoker")
requested_execution_level_element.setAttribute("uiAccess", "true" if uac_uiaccess else "false")
def _ensure_common_controls_dependency(manifest_dom, root_element):
"""
Scan <dependency> elements for the one whose <<dependentAssembly> -> <assemblyIdentity> corresponds to the
`Microsoft.Windows.Common-Controls`. If found, overwrite its properties. If not, create new <dependency>
element with corresponding sub-elements and attributes.
"""
# <dependency>
dependency_elements = _find_elements_by_tag(root_element, "dependency")
for dependency_element in dependency_elements:
# <dependentAssembly>
dependent_assembly_element = _find_element_by_tag(dependency_element, "dependentAssembly")
# <assemblyIdentity>
assembly_identity_element = _find_element_by_tag(dependent_assembly_element, "assemblyIdentity")
# Check the name attribute
if assembly_identity_element.attributes["name"].value == "Microsoft.Windows.Common-Controls":
common_controls_element = assembly_identity_element
break
else:
# Create <dependency>
dependency_element = manifest_dom.createElement("dependency")
root_element.appendChild(dependency_element)
# Create <dependentAssembly>
dependent_assembly_element = manifest_dom.createElement("dependentAssembly")
dependency_element.appendChild(dependent_assembly_element)
# Create <assemblyIdentity>
common_controls_element = manifest_dom.createElement("assemblyIdentity")
dependent_assembly_element.appendChild(common_controls_element)
common_controls_element.setAttribute("type", "win32")
common_controls_element.setAttribute("name", "Microsoft.Windows.Common-Controls")
common_controls_element.setAttribute("version", "6.0.0.0")
common_controls_element.setAttribute("processorArchitecture", "*")
common_controls_element.setAttribute("publicKeyToken", "6595b64144ccf1df")
common_controls_element.setAttribute("language", "*")
def create_application_manifest(manifest_xml=None, uac_admin=False, uac_uiaccess=False):
"""
Create application manifest, from built-in or custom manifest XML template. If provided, `manifest_xml` must be
a string or byte string containing XML source. The returned manifest is a byte string, encoded in UTF-8.
This function sets the attributes of `requestedExecutionLevel` based on provided `uac_admin` and `auc_uiacces`
arguments (creating the parent elements in the XML, if necessary). It also scans `dependency` elements for the
entry corresponding to `Microsoft.Windows.Common-Controls` and creates or modifies it as necessary.
"""
if manifest_xml is None:
manifest_xml = _DEFAULT_MANIFEST_XML
with xml.dom.minidom.parseString(manifest_xml) as manifest_dom:
root_element = manifest_dom.documentElement
# Validate root element - must be <assembly>
assert root_element.tagName == "assembly"
assert root_element.namespaceURI == "urn:schemas-microsoft-com:asm.v1"
assert root_element.attributes["manifestVersion"].value == "1.0"
# Modify the manifest
_set_execution_level(manifest_dom, root_element, uac_admin, uac_uiaccess)
_ensure_common_controls_dependency(manifest_dom, root_element)
# Create output XML
output = manifest_dom.toprettyxml(indent=" ", encoding="UTF-8")
# Strip extra newlines
output = [line for line in output.splitlines() if line.strip()]
# Replace: `<?xml version="1.0" encoding="UTF-8"?>` with `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`.
# Support for `standalone` was added to `toprettyxml` in python 3.9, so do a manual work around.
output[0] = b"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>"""
output = b"\n".join(output)
return output
def write_manifest_to_executable(filename, manifest_xml):
"""
Write the given manifest XML to the given executable's RT_MANIFEST resource.
"""
from PyInstaller.utils.win32 import winresource
# CREATEPROCESS_MANIFEST_RESOURCE_ID is used for manifest resource in executables.
# ISOLATIONAWARE_MANIFEST_RESOURCE_ID is used for manifest resources in DLLs.
names = [CREATEPROCESS_MANIFEST_RESOURCE_ID]
# Ensure LANG_NEUTRAL is updated, and also update any other present languages.
languages = [LANG_NEUTRAL, "*"]
winresource.add_or_update_resource(filename, manifest_xml, RT_MANIFEST, names, languages)
def read_manifest_from_executable(filename):
"""
Read manifest from the given executable."
"""
from PyInstaller.utils.win32 import winresource
resources = winresource.get_resources(filename, [RT_MANIFEST])
# `resources` is a three-level dictionary:
# - level 1: resource type (RT_MANIFEST)
# - level 2: resource name (CREATEPROCESS_MANIFEST_RESOURCE_ID)
# - level 3: resource language (LANG_NEUTRAL)
# Level 1
if RT_MANIFEST not in resources:
raise ValueError(f"No RT_MANIFEST resources found in {filename!r}.")
resources = resources[RT_MANIFEST]
# Level 2
if CREATEPROCESS_MANIFEST_RESOURCE_ID not in resources:
raise ValueError(f"No RT_MANIFEST resource named CREATEPROCESS_MANIFEST_RESOURCE_ID found in {filename!r}.")
resources = resources[CREATEPROCESS_MANIFEST_RESOURCE_ID]
# Level 3
# We prefer LANG_NEUTRAL, but allow fall back to the first available entry.
if LANG_NEUTRAL in resources:
resources = resources[LANG_NEUTRAL]
else:
resources = next(iter(resources.items()))
manifest_xml = resources
return manifest_xml

View File

@@ -0,0 +1,189 @@
#-----------------------------------------------------------------------------
# Copyright (c) 2013-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------
"""
Read and write resources from/to Win32 PE files.
"""
import PyInstaller.log as logging
from PyInstaller.compat import pywintypes, win32api
logger = logging.getLogger(__name__)
LOAD_LIBRARY_AS_DATAFILE = 2
ERROR_BAD_EXE_FORMAT = 193
ERROR_RESOURCE_DATA_NOT_FOUND = 1812
ERROR_RESOURCE_TYPE_NOT_FOUND = 1813
ERROR_RESOURCE_NAME_NOT_FOUND = 1814
ERROR_RESOURCE_LANG_NOT_FOUND = 1815
def get_resources(filename, types=None, names=None, languages=None):
"""
Retrieve resources from the given PE file.
filename: path to the PE file.
types: a list of resource types (integers or strings) to search for (None = all).
names: a list of resource names (integers or strings) to search for (None = all).
languages: a list of resource languages (integers) to search for (None = all).
Returns a dictionary of the form {type: {name: {language: data}}}, which might also be empty if no matching
resources were found.
"""
types = set(types) if types is not None else {"*"}
names = set(names) if names is not None else {"*"}
languages = set(languages) if languages is not None else {"*"}
output = {}
# Errors codes for which we swallow exceptions
_IGNORE_EXCEPTIONS = {
ERROR_RESOURCE_DATA_NOT_FOUND,
ERROR_RESOURCE_TYPE_NOT_FOUND,
ERROR_RESOURCE_NAME_NOT_FOUND,
ERROR_RESOURCE_LANG_NOT_FOUND,
}
# Open file
module_handle = win32api.LoadLibraryEx(filename, 0, LOAD_LIBRARY_AS_DATAFILE)
# Enumerate available resource types
try:
available_types = win32api.EnumResourceTypes(module_handle)
except pywintypes.error as e:
if e.args[0] not in _IGNORE_EXCEPTIONS:
raise
available_types = []
if "*" not in types:
available_types = [res_type for res_type in available_types if res_type in types]
for res_type in available_types:
# Enumerate available names for the resource type.
try:
available_names = win32api.EnumResourceNames(module_handle, res_type)
except pywintypes.error as e:
if e.args[0] not in _IGNORE_EXCEPTIONS:
raise
continue
if "*" not in names:
available_names = [res_name for res_name in available_names if res_name in names]
for res_name in available_names:
# Enumerate available languages for the resource type and name combination.
try:
available_languages = win32api.EnumResourceLanguages(module_handle, res_type, res_name)
except pywintypes.error as e:
if e.args[0] not in _IGNORE_EXCEPTIONS:
raise
continue
if "*" not in languages:
available_languages = [res_lang for res_lang in available_languages if res_lang in languages]
for res_lang in available_languages:
# Read data
try:
data = win32api.LoadResource(module_handle, res_type, res_name, res_lang)
except pywintypes.error as e:
if e.args[0] not in _IGNORE_EXCEPTIONS:
raise
continue
if res_type not in output:
output[res_type] = {}
if res_name not in output[res_type]:
output[res_type][res_name] = {}
output[res_type][res_name][res_lang] = data
# Close file
win32api.FreeLibrary(module_handle)
return output
def add_or_update_resource(filename, data, res_type, names=None, languages=None):
"""
Update or add a single resource in the PE file with the given binary data.
filename: path to the PE file.
data: binary data to write to the resource.
res_type: resource type to add/update (integer or string).
names: a list of resource names (integers or strings) to update (None = all).
languages: a list of resource languages (integers) to update (None = all).
"""
if res_type == "*":
raise ValueError("res_type cannot be a wildcard (*)!")
names = set(names) if names is not None else {"*"}
languages = set(languages) if languages is not None else {"*"}
# Retrieve existing resources, filtered by the given resource type and given resource names and languages.
resources = get_resources(filename, [res_type], names, languages)
# Add res_type, name, language combinations that are not already present
resources = resources.get(res_type, {}) # This is now a {name: {language: data}} dictionary
for res_name in names:
if res_name == "*":
continue
if res_name not in resources:
resources[res_name] = {}
for res_lang in languages:
if res_lang == "*":
continue
if res_lang not in resources[res_name]:
resources[res_name][res_lang] = None # Just an indicator
# Add resource to the target file, overwriting the existing resources with same type, name, language combinations.
module_handle = win32api.BeginUpdateResource(filename, 0)
for res_name in resources.keys():
for res_lang in resources[res_name].keys():
win32api.UpdateResource(module_handle, res_type, res_name, data, res_lang)
win32api.EndUpdateResource(module_handle, 0)
def copy_resources_from_pe_file(filename, src_filename, types=None, names=None, languages=None):
"""
Update or add resources in the given PE file by copying them over from the specified source PE file.
filename: path to the PE file.
src_filename: path to the source PE file.
types: a list of resource types (integers or strings) to add/update via copy for (None = all).
names: a list of resource names (integers or strings) to add/update via copy (None = all).
languages: a list of resource languages (integers) to add/update via copy (None = all).
"""
types = set(types) if types is not None else {"*"}
names = set(names) if names is not None else {"*"}
languages = set(languages) if languages is not None else {"*"}
# Retrieve existing resources, filtered by the given resource type and given resource names and languages.
resources = get_resources(src_filename, types, names, languages)
for res_type, resources_for_type in resources.items():
if "*" not in types and res_type not in types:
continue
for res_name, resources_for_type_name in resources_for_type.items():
if "*" not in names and res_name not in names:
continue
for res_lang, data in resources_for_type_name.items():
if "*" not in languages and res_lang not in languages:
continue
add_or_update_resource(filename, data, res_type, [res_name], [res_lang])
def remove_all_resources(filename):
"""
Remove all resources from the given PE file:
"""
module_handle = win32api.BeginUpdateResource(filename, True) # bDeleteExistingResources=True
win32api.EndUpdateResource(module_handle, False)

View File

@@ -0,0 +1,257 @@
#-----------------------------------------------------------------------------
# Copyright (c) 2013-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------
"""
Utilities for Windows platform.
"""
from PyInstaller import compat
def get_windows_dir():
"""
Return the Windows directory, e.g., C:\\Windows.
"""
windir = compat.win32api.GetWindowsDirectory()
if not windir:
raise SystemExit("ERROR: Cannot determine Windows directory!")
return windir
def get_system_path():
"""
Return the required Windows system paths.
"""
sys_dir = compat.win32api.GetSystemDirectory()
# Ensure C:\Windows\system32 and C:\Windows directories are always present in PATH variable.
# C:\Windows\system32 is valid even for 64-bit Windows. Access do DLLs are transparently redirected to
# C:\Windows\syswow64 for 64bit applactions.
# See http://msdn.microsoft.com/en-us/library/aa384187(v=vs.85).aspx
return [sys_dir, get_windows_dir()]
def get_pe_file_machine_type(filename):
"""
Return the machine type code from the header of the given PE file.
"""
import pefile
with pefile.PE(filename, fast_load=True) as pe:
return pe.FILE_HEADER.Machine
def set_exe_build_timestamp(exe_path, timestamp):
"""
Modifies the executable's build timestamp by updating values in the corresponding PE headers.
"""
import pefile
with pefile.PE(exe_path, fast_load=True) as pe:
# Manually perform a full load. We need it to load all headers, but specifying it in the constructor triggers
# byte statistics gathering that takes forever with large files. So we try to go around that...
pe.full_load()
# Set build timestamp.
# See: https://0xc0decafe.com/malware-analyst-guide-to-pe-timestamps
timestamp = int(timestamp)
# Set timestamp field in FILE_HEADER
pe.FILE_HEADER.TimeDateStamp = timestamp
# MSVC-compiled executables contain (at least?) one DIRECTORY_ENTRY_DEBUG entry that also contains timestamp
# with same value as set in FILE_HEADER. So modify that as well, as long as it is set.
debug_entries = getattr(pe, 'DIRECTORY_ENTRY_DEBUG', [])
for debug_entry in debug_entries:
if debug_entry.struct.TimeDateStamp:
debug_entry.struct.TimeDateStamp = timestamp
# Generate updated EXE data
data = pe.write()
# Rewrite the exe
with open(exe_path, 'wb') as fp:
fp.write(data)
def update_exe_pe_checksum(exe_path):
"""
Compute the executable's PE checksum, and write it to PE headers.
This optional checksum is supposed to protect the executable against corruption but some anti-viral software have
taken to flagging anything without it set correctly as malware. See issue #5579.
"""
import pefile
# Compute checksum using our equivalent of the MapFileAndCheckSumW - for large files, it is significantly faster
# than pure-pyton pefile.PE.generate_checksum(). However, it requires the file to be on disk (i.e., cannot operate
# on a memory buffer).
try:
checksum = compute_exe_pe_checksum(exe_path)
except Exception as e:
raise RuntimeError("Failed to compute PE checksum!") from e
# Update the checksum
with pefile.PE(exe_path, fast_load=True) as pe:
pe.OPTIONAL_HEADER.CheckSum = checksum
# Generate updated EXE data
data = pe.write()
# Rewrite the exe
with open(exe_path, 'wb') as fp:
fp.write(data)
def compute_exe_pe_checksum(exe_path):
"""
This is a replacement for the MapFileAndCheckSumW function. As noted in MSDN documentation, the Microsoft's
implementation of MapFileAndCheckSumW internally calls its ASCII variant (MapFileAndCheckSumA), and therefore
cannot handle paths that contain characters that are not representable in the current code page.
See: https://docs.microsoft.com/en-us/windows/win32/api/imagehlp/nf-imagehlp-mapfileandchecksumw
This function is based on Wine's implementation of MapFileAndCheckSumW, and due to being based entirely on
the pure widechar-API functions, it is not limited by the current code page.
"""
# ctypes bindings for relevant win32 API functions
import ctypes
from ctypes import windll, wintypes
INVALID_HANDLE = wintypes.HANDLE(-1).value
GetLastError = ctypes.windll.kernel32.GetLastError
GetLastError.argtypes = ()
GetLastError.restype = wintypes.DWORD
CloseHandle = windll.kernel32.CloseHandle
CloseHandle.argtypes = (
wintypes.HANDLE, # hObject
)
CloseHandle.restype = wintypes.BOOL
CreateFileW = windll.kernel32.CreateFileW
CreateFileW.argtypes = (
wintypes.LPCWSTR, # lpFileName
wintypes.DWORD, # dwDesiredAccess
wintypes.DWORD, # dwShareMode
wintypes.LPVOID, # lpSecurityAttributes
wintypes.DWORD, # dwCreationDisposition
wintypes.DWORD, # dwFlagsAndAttributes
wintypes.HANDLE, # hTemplateFile
)
CreateFileW.restype = wintypes.HANDLE
CreateFileMappingW = windll.kernel32.CreateFileMappingW
CreateFileMappingW.argtypes = (
wintypes.HANDLE, # hFile
wintypes.LPVOID, # lpSecurityAttributes
wintypes.DWORD, # flProtect
wintypes.DWORD, # dwMaximumSizeHigh
wintypes.DWORD, # dwMaximumSizeLow
wintypes.LPCWSTR, # lpName
)
CreateFileMappingW.restype = wintypes.HANDLE
MapViewOfFile = windll.kernel32.MapViewOfFile
MapViewOfFile.argtypes = (
wintypes.HANDLE, # hFileMappingObject
wintypes.DWORD, # dwDesiredAccess
wintypes.DWORD, # dwFileOffsetHigh
wintypes.DWORD, # dwFileOffsetLow
wintypes.DWORD, # dwNumberOfBytesToMap
)
MapViewOfFile.restype = wintypes.LPVOID
UnmapViewOfFile = windll.kernel32.UnmapViewOfFile
UnmapViewOfFile.argtypes = (
wintypes.LPCVOID, # lpBaseAddress
)
UnmapViewOfFile.restype = wintypes.BOOL
GetFileSizeEx = windll.kernel32.GetFileSizeEx
GetFileSizeEx.argtypes = (
wintypes.HANDLE, # hFile
wintypes.PLARGE_INTEGER, # lpFileSize
)
CheckSumMappedFile = windll.imagehlp.CheckSumMappedFile
CheckSumMappedFile.argtypes = (
wintypes.LPVOID, # BaseAddress
wintypes.DWORD, # FileLength
wintypes.PDWORD, # HeaderSum
wintypes.PDWORD, # CheckSum
)
CheckSumMappedFile.restype = wintypes.LPVOID
# Open file
hFile = CreateFileW(
ctypes.c_wchar_p(exe_path),
0x80000000, # dwDesiredAccess = GENERIC_READ
0x00000001 | 0x00000002, # dwShareMode = FILE_SHARE_READ | FILE_SHARE_WRITE,
None, # lpSecurityAttributes = NULL
3, # dwCreationDisposition = OPEN_EXISTING
0x80, # dwFlagsAndAttributes = FILE_ATTRIBUTE_NORMAL
None # hTemplateFile = NULL
)
if hFile == INVALID_HANDLE:
err = GetLastError()
raise RuntimeError(f"Failed to open file {exe_path}! Error code: {err}")
# Query file size
fileLength = wintypes.LARGE_INTEGER(0)
if GetFileSizeEx(hFile, fileLength) == 0:
err = GetLastError()
CloseHandle(hFile)
raise RuntimeError(f"Failed to query file size file! Error code: {err}")
fileLength = fileLength.value
if fileLength > (2**32 - 1):
raise RuntimeError("Executable size exceeds maximum allowed executable size on Windows (4 GiB)!")
# Map the file
hMapping = CreateFileMappingW(
hFile,
None, # lpFileMappingAttributes = NULL
0x02, # flProtect = PAGE_READONLY
0, # dwMaximumSizeHigh = 0
0, # dwMaximumSizeLow = 0
None # lpName = NULL
)
if not hMapping:
err = GetLastError()
CloseHandle(hFile)
raise RuntimeError(f"Failed to map file! Error code: {err}")
# Create map view
baseAddress = MapViewOfFile(
hMapping,
4, # dwDesiredAccess = FILE_MAP_READ
0, # dwFileOffsetHigh = 0
0, # dwFileOffsetLow = 0
0 # dwNumberOfBytesToMap = 0
)
if baseAddress == 0:
err = GetLastError()
CloseHandle(hMapping)
CloseHandle(hFile)
raise RuntimeError(f"Failed to create map view! Error code: {err}")
# Finally, compute the checksum
headerSum = wintypes.DWORD(0)
checkSum = wintypes.DWORD(0)
ret = CheckSumMappedFile(baseAddress, fileLength, ctypes.byref(headerSum), ctypes.byref(checkSum))
if ret is None:
err = GetLastError()
# Cleanup
UnmapViewOfFile(baseAddress)
CloseHandle(hMapping)
CloseHandle(hFile)
if ret is None:
raise RuntimeError(f"CheckSumMappedFile failed! Error code: {err}")
return checkSum.value