renpy/launcher/game/change_icon.py

429 lines
13 KiB
Python
Raw Normal View History

2023-01-18 22:13:55 +00:00
# Copyright 2004-2022 Tom Rothamel <pytom@bishoujo.us>
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
# http://www.csn.ul.ie/~caolan/publink/winresdump/winresdump/doc/pefile.html
# Contains a reasonable description of the format.
from __future__ import print_function
from renpy.compat import bchr, chr, str, PY2
import struct
import sys
import array
import pefile
# This class performs various operations on memory-loaded binary files,
# including modifications.
class BinFile(object):
def set_u32(self, addr, value):
self.a[addr + 0] = (value >> 0) & 0xff
self.a[addr + 1] = (value >> 8) & 0xff
self.a[addr + 2] = (value >> 16) & 0xff
self.a[addr + 3] = (value >> 24) & 0xff
def u32(self):
addr = self.addr
rv = self.a[addr]
rv |= self.a[addr + 1] << 8
rv |= self.a[addr + 2] << 16
rv |= self.a[addr + 3] << 24
self.addr += 4
return rv
def u16(self):
addr = self.addr
rv = self.a[addr]
rv |= self.a[addr + 1] << 8
self.addr += 2
return rv
def u8(self):
rv = self.a[self.addr]
self.addr += 1
return rv
def name(self):
c = self.u16()
rv = u""
for _i in range(c):
rv += chr(self.u16())
return rv
def seek(self, addr):
self.addr = addr
def tostring(self):
if PY2:
return self.a.tostring() # type: ignore
else:
return self.a.tobytes()
def substring(self, start, len): # @ReservedAssignment
if PY2:
return self.a[start:start + len].tostring() # type: ignore
else:
return self.a[start:start + len].tobytes()
def __init__(self, data):
self.a = array.array('B')
if PY2:
self.a.fromstring(data) # type: ignore
else:
self.a.frombytes(data)
##############################################################################
# These functions parse data out of the file. In these functions, offset is
# relative to the start of the file.
# The virtual address of the resource segment.
resource_virtual = 0
# This parses a data block out of the resources.
def parse_data(bf, offset):
bf.seek(offset)
data_offset = bf.u32()
data_len = bf.u32()
code_page = bf.u32()
bf.u32()
l = [ ]
bf.seek(data_offset - resource_virtual)
for _i in range(data_len):
l.append(bchr(bf.u8()))
return (code_page, b"".join(l))
# This parses a resource directory.
def parse_directory(bf, offset):
bf.seek(offset)
char = bf.u32() # @UnusedVariable
timedate = bf.u32() # @UnusedVariable
major = bf.u16() # @UnusedVariable
minor = bf.u16() # @UnusedVariable
n_named = bf.u16()
n_id = bf.u16()
entries = [ ]
for _i in range(n_named + n_id):
entries.append((bf.u32(), bf.u32()))
rv = { }
for name, value in entries:
if name & 0x80000000:
bf.seek((name & 0x7fffffff))
name = bf.name()
if value & 0x80000000:
value = parse_directory(bf, value & 0x7fffffff)
else:
value = parse_data(bf, value)
rv[name] = value
return rv
##############################################################################
# This utility function displays the tree of resources that have been loaded.
def show_resources(d, prefix):
if not isinstance(d, dict):
print(prefix, "Codepage", d[0], "length", len(d[1]))
return
for k in d:
print(prefix, k)
show_resources(d[k], prefix + " ")
##############################################################################
# These functions repack the resources into a new resource segment. Here,
# the offset is relative to the start of the resource segment.
class Packer(object):
def pack(self, d):
self.data = b""
self.data_offset = 0
self.entries = b""
self.entries_offset = 0
head = self.pack_dict(d, 0)
self.data = b""
self.data_offset = len(head) + len(self.entries)
self.entries = b""
self.entries_offset = len(head)
return self.pack_dict(d, 0) + self.entries + self.data
def pack_name(self, s):
rv = self.data_offset + len(self.data)
l = len(s)
s = s.encode("utf-16le")
self.data += struct.pack("<H", l) + s + b"\0\0"
return rv
def pack_tuple(self, t):
codepage, data = t
rv = len(self.entries) + self.entries_offset
if len(self.data) % 2:
self.data += b"P"
daddr = len(self.data) + self.data_offset
self.entries += struct.pack("<IIII", daddr + resource_virtual, len(data), codepage, 0)
self.data += data
# if len(self.data) % 1 == 1:
# self.data += 'P'
return rv
def pack_dict(self, d, offset):
name_entries = sorted((a, b) for a, b in d.items() if isinstance(a, str))
id_entries = sorted((a, b) for a, b in d.items() if isinstance(a, int))
rv = struct.pack("<IIHHHH", 0, 0, 4, 0, len(name_entries), len(id_entries))
offset += len(rv) + (len(name_entries) + len(id_entries)) * 8
rest = b""
for (name, value) in name_entries + id_entries:
if isinstance(name, str):
name = 0x80000000 | self.pack_name(name)
if isinstance(value, dict):
addr = offset | 0x80000000
packed = self.pack_dict(value, offset)
offset += len(packed)
rest += packed
else:
addr = self.pack_tuple(value)
rv += struct.pack("<II", name, addr)
return rv + rest
##############################################################################
# This loads in an icon file, and returns a dictionary that is suitable for
# use in the resources of an exe file.
def load_icon(fn):
f = BinFile(open(fn, "rb").read())
f.seek(0)
f.u16()
f.u16()
count = f.u16()
rv = { }
rv[3] = { }
group = struct.pack("HHH", 0, 1, count)
for i in range(count):
width = f.u8()
height = f.u8()
colors = f.u8()
reserved = f.u8()
planes = f.u16()
bpp = f.u16()
size = f.u32()
offset = f.u32()
addr = f.addr
f.seek(offset + 16)
if not f.u32():
f.set_u32(offset + 20, 0)
#rv[3][i + 1] = { 0 : (1252, f.substring(offset, size)) }
#I do not have justification for the 0x0409 entry ID, other than copying what other files do. Same for codepage of 0
rv[3][i + 1] = { 1033 : (0, f.substring(offset, size)) }
group += struct.pack("BBBBHHIH", width, height, colors, reserved,
planes, bpp, size, i + 1)
f.seek(addr)
#rv[14] = { 1 : { 0 : (1252, group) } }
rv[14] = { 1 : { 1033 : (0, group) } }
return rv
##############################################################################
# This is the main function that should be called externally, that copies over
# the icons.
def change_icons(oldexe, icofn):
global resource_virtual
pe = pefile.PE(oldexe)
for s in pe.sections:
if s.Name == b".rsrc\0\0\0":
rsrc_section = s
break
else:
raise Exception("Couldn't find resource section.")
base = rsrc_section.PointerToRawData
resource_virtual = rsrc_section.VirtualAddress
physize = rsrc_section.SizeOfRawData
virsize = rsrc_section.Misc_VirtualSize
f = open(oldexe, "rb")
basedata = f.read(base)
data = f.read(physize)
#Symbol table, I could not understand so well
# Some analysis and the little I could find on how the symbol tables were layed out show that the PE table is in two sections. One is
# a list of NumberOfSymbols*(18 bytes) symbol entries, followed immediately by a string table who's length is specified in the 4 bytes following the symbol structure list
#Again, the information *seems* to indicate that this will always follow the entire block of all sections (so be at the end of the file)
######
# As a related note, apparently this flag has been deprecated, but appears to be set (I think ..) in the renpy.exe that is used to build from..
# no idea of a consequence, just an observation
# https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#characteristics , see "IMAGE_FILE_LINE_NUMS_STRIPPED" :
# COFF line numbers have been removed. This flag is deprecated and should be zero.
# In renpy.exe, this 2-byte flag is @ 0x096 and 0x097 in little-endian
######
f.close()
bf = BinFile(data)
resources = parse_directory(bf, 0)
# show_resources(resources, "")
resources.update(load_icon(icofn))
# show_resources(resources, "")
rsrc = Packer().pack(resources)
newExactSize = len(rsrc)
#From note about flags above regarding image flags. These two should be 0
pe.FILE_HEADER.Characteristics = pe.FILE_HEADER.Characteristics & (~pefile.IMAGE_CHARACTERISTICS["IMAGE_FILE_LINE_NUMS_STRIPPED"])
pe.FILE_HEADER.Characteristics = pe.FILE_HEADER.Characteristics & (~pefile.IMAGE_CHARACTERISTICS["IMAGE_FILE_LOCAL_SYMS_STRIPPED"])
#Per docs, symbol table can just be removed
#https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#coff-file-header-object-and-image see "PointerToSymbolTable" and "NumberOfSymbols "
pe.FILE_HEADER.PointerToSymbolTable = 0
pe.FILE_HEADER.NumberOfSymbols = 0
#####
# Alignment requirements as I understand them
#
# SectionAlignment (misnomer.. 'memory' alignment) = 4096
# FileAlignment = 512
#
# Section, SizeOfRawData = multiple of FileAlignment (On Disk size)
# Scetion, VirtualSize is NOT aligned to any specific number, and rwdata may be > than this due to its alignment requirement (In memory size)
#
# the resource section should be padded out to meet the alignment requirement of SizeOfRawData = multiple of FileAlignment (So, in the file, since on disk)
#
#####
# RVA table virt address and size = that in the section header
#
#####
# SizeOfImage must be aligned to SectionAlignment (memory alignment)
# There is NO padding needed, the image size number simply needs to be aligned to >= the
memoryalignment = pe.OPTIONAL_HEADER.SectionAlignment
filealignment = pe.OPTIONAL_HEADER.FileAlignment
rsrc_section.Misc_VirtualSize = newExactSize
rsrc_section.Misc_PhysicalAddress = newExactSize
rsrc_section.Misc = newExactSize
#Size of resource section (and pointer, but we're not changing anything 'above' resource) is the same as virtual address in the section table
# https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#optional-header-data-directories-image-only
pe.OPTIONAL_HEADER.DATA_DIRECTORY[2].Size = rsrc_section.Misc_VirtualSize
#Alignment for raw data
if len(rsrc) % filealignment:
pad = filealignment - (len(rsrc) % filealignment)
padding = b"\0" * (pad)
padding = padding[:pad]
rsrc += padding
rsrc_section.SizeOfRawData = len(rsrc)
#Image size is the memory size of all sections + all headers, padded to memory alignment.
#There's alread a header size, and this isn't changing headers, so just add.
#imageSize = pe.OPTIONAL_HEADER.ImageBase + pe.OPTIONAL_HEADER.SizeOfHeaders
imageSize = pe.OPTIONAL_HEADER.SizeOfHeaders
for s in pe.sections:
sectionSize = s.Misc_VirtualSize
#Align every section, so add padding to size
pad = 0
if sectionSize % memoryalignment:
pad = memoryalignment - (sectionSize % memoryalignment)
imageSize += (sectionSize + pad)
pad = 0
if imageSize % memoryalignment:
pad = memoryalignment - (imageSize % memoryalignment)
pe.OPTIONAL_HEADER.SizeOfImage = imageSize + pad
#The symbol table is simply left off. Its size DOES factor into ImageSize, but we didn't calculate it above, so fine
#Correctly checksum the file. The entire file is involved in the calculation, so a new PE object must be generated for the calculation to work against
newFile = pe.write()[:base] + rsrc
newpe = pefile.PE(data=bytes(newFile))
newpe.OPTIONAL_HEADER.CheckSum = newpe.generate_checksum()
return bytes(newpe.write())
if __name__ == "__main__":
f = open(sys.argv[3], "wb")
f.write(change_icons(sys.argv[1], sys.argv[2]))
f.close()