2020-08-24 13:08:44 +09:00
|
|
|
from io import BytesIO
|
|
|
|
|
|
|
|
import tools
|
2020-09-26 11:33:17 +09:00
|
|
|
from fileutils import *
|
2020-08-24 13:08:44 +09:00
|
|
|
|
2020-09-26 11:33:17 +09:00
|
|
|
class DolFile(object):
|
2020-08-24 13:08:44 +09:00
|
|
|
|
2020-09-26 11:33:17 +09:00
|
|
|
def __init__(self, f: GC_File=None):
|
2020-08-24 13:08:44 +09:00
|
|
|
self.fileOffsetLoc = 0
|
|
|
|
self.fileAddressLoc = 0x48
|
|
|
|
self.fileSizeLoc = 0x90
|
|
|
|
self.fileBssInfoLoc = 0xD8
|
|
|
|
self.fileEntryLoc = 0xE0
|
|
|
|
|
|
|
|
self.textSections = []
|
|
|
|
self.dataSections = []
|
|
|
|
self.maxTextSections = 7
|
|
|
|
self.maxDataSections = 11
|
|
|
|
|
|
|
|
self.bssAddress = 0
|
|
|
|
self.bssSize = 0
|
|
|
|
self.entryPoint = 0x80003000
|
|
|
|
|
2020-08-24 19:10:23 +09:00
|
|
|
if f is None: return
|
2020-08-24 13:08:44 +09:00
|
|
|
|
|
|
|
# Read text and data section addresses and sizes
|
|
|
|
for i in range(self.maxTextSections + self.maxDataSections):
|
|
|
|
f.seek(self.fileOffsetLoc + (i << 2))
|
2020-09-26 11:33:17 +09:00
|
|
|
offset = read_uint32(f)
|
2020-08-24 13:08:44 +09:00
|
|
|
f.seek(self.fileAddressLoc + (i << 2))
|
2020-09-26 11:33:17 +09:00
|
|
|
address = read_uint32(f)
|
2020-08-24 13:08:44 +09:00
|
|
|
f.seek(self.fileSizeLoc + (i << 2))
|
2020-09-26 11:33:17 +09:00
|
|
|
size = read_uint32(f)
|
2020-08-24 13:08:44 +09:00
|
|
|
|
|
|
|
if offset >= 0x100:
|
|
|
|
f.seek(offset)
|
|
|
|
data = BytesIO(f.read(size))
|
|
|
|
if i < self.maxTextSections:
|
|
|
|
self.textSections.append((offset, address, size, data))
|
|
|
|
else:
|
|
|
|
self.dataSections.append((offset, address, size, data))
|
|
|
|
|
|
|
|
f.seek(self.fileBssInfoLoc)
|
2020-09-26 11:33:17 +09:00
|
|
|
self.bssAddress = read_uint32(f)
|
|
|
|
self.bssSize = read_uint32(f)
|
2020-08-24 13:08:44 +09:00
|
|
|
|
|
|
|
f.seek(self.fileEntryLoc)
|
2020-09-26 11:33:17 +09:00
|
|
|
self.entryPoint = read_uint32(f)
|
2020-08-24 13:08:44 +09:00
|
|
|
|
|
|
|
self._currLogicAddr = self.textSections[0][1]
|
|
|
|
self.seek(self._currLogicAddr)
|
|
|
|
f.seek(0)
|
|
|
|
|
|
|
|
# Internal function for
|
2020-09-26 11:33:17 +09:00
|
|
|
def resolve_address(self, gcAddr, raiseError=True) -> (None, tuple):
|
2020-08-24 13:08:44 +09:00
|
|
|
'''Returns the data of the section that houses the given address
|
2020-09-26 11:33:17 +09:00
|
|
|
If raiseError is True, a RuntimeError is raised when the address is unmapped,
|
|
|
|
otherwise it returns None'''
|
|
|
|
|
2020-08-24 13:08:44 +09:00
|
|
|
for offset, address, size, data in self.textSections:
|
|
|
|
if address <= gcAddr < address+size:
|
|
|
|
return offset, address, size, data
|
|
|
|
for offset, address, size, data in self.dataSections:
|
|
|
|
if address <= gcAddr < address+size:
|
|
|
|
return offset, address, size, data
|
|
|
|
|
|
|
|
if raiseError:
|
|
|
|
raise RuntimeError("Unmapped address: 0x{:X}".format(gcAddr))
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
2020-09-26 11:33:17 +09:00
|
|
|
def seek_nearest_unmapped(self, gcAddr, buffer=0) -> int:
|
2020-08-24 13:08:44 +09:00
|
|
|
'''Returns the nearest unmapped address (greater) if the given address is already taken by data'''
|
2020-09-26 11:33:17 +09:00
|
|
|
|
2020-08-24 13:08:44 +09:00
|
|
|
for _, address, size, _ in self.textSections:
|
|
|
|
if address > (gcAddr + buffer) or address+size < gcAddr:
|
|
|
|
continue
|
|
|
|
gcAddr = address + size
|
|
|
|
for _, address, size, _ in self.dataSections:
|
|
|
|
if address > (gcAddr + buffer) or address+size < gcAddr:
|
|
|
|
continue
|
|
|
|
gcAddr = address + size
|
|
|
|
return gcAddr
|
|
|
|
|
2020-09-26 11:33:17 +09:00
|
|
|
def get_final_section(self) -> tuple:
|
2020-08-24 13:08:44 +09:00
|
|
|
largestOffset = 0
|
|
|
|
indexToTarget = 0
|
|
|
|
targetType = 0
|
|
|
|
|
|
|
|
for i, sectionData in enumerate(self.textSections):
|
|
|
|
if sectionData[0] > largestOffset:
|
|
|
|
largestOffset = sectionData[0]
|
|
|
|
indexToTarget = i
|
|
|
|
targetType = 0
|
|
|
|
for i, sectionData in enumerate(self.dataSections):
|
|
|
|
if sectionData[0] > largestOffset:
|
|
|
|
largestOffset = sectionData[0]
|
|
|
|
indexToTarget = i
|
|
|
|
targetType = 1
|
|
|
|
|
|
|
|
if targetType == 0:
|
|
|
|
return self.textSections[indexToTarget]
|
|
|
|
else:
|
|
|
|
return self.dataSections[indexToTarget]
|
|
|
|
|
|
|
|
# Unsupported: Reading an entire dol file
|
|
|
|
# Assumption: A read should not go beyond the current section
|
2020-09-26 11:33:17 +09:00
|
|
|
def read(self, _size) -> bytes:
|
2020-08-24 13:08:44 +09:00
|
|
|
_, address, size, data = self.resolve_address(self._currLogicAddr)
|
2020-08-24 19:10:23 +09:00
|
|
|
if self._currLogicAddr + _size > address + size:
|
2020-08-24 13:08:44 +09:00
|
|
|
raise RuntimeError("Read goes over current section")
|
|
|
|
|
2020-08-24 19:10:23 +09:00
|
|
|
self._currLogicAddr += _size
|
|
|
|
return data.read(_size)
|
2020-08-24 13:08:44 +09:00
|
|
|
|
|
|
|
# Assumption: A write should not go beyond the current section
|
2020-08-24 19:10:23 +09:00
|
|
|
def write(self, _data):
|
2020-09-26 11:33:17 +09:00
|
|
|
_, address, size, data = self.resolve_address(self._currLogicAddr)
|
2020-08-24 19:10:23 +09:00
|
|
|
if self._currLogicAddr + len(_data) > address + size:
|
2020-08-24 13:08:44 +09:00
|
|
|
raise RuntimeError("Write goes over current section")
|
|
|
|
|
2020-08-24 19:10:23 +09:00
|
|
|
data.write(_data)
|
|
|
|
self._currLogicAddr += len(_data)
|
2020-08-24 13:08:44 +09:00
|
|
|
|
|
|
|
def seek(self, where, whence=0):
|
|
|
|
if whence == 0:
|
2020-08-24 19:10:23 +09:00
|
|
|
_, address, _, data = self.resolve_address(where)
|
2020-08-24 13:08:44 +09:00
|
|
|
data.seek(where - address)
|
|
|
|
|
|
|
|
self._currLogicAddr = where
|
|
|
|
elif whence == 1:
|
2020-08-24 19:10:23 +09:00
|
|
|
_, address, _, data = self.resolve_address(self._currLogicAddr + where)
|
2020-08-24 13:08:44 +09:00
|
|
|
data.seek((self._currLogicAddr + where) - address)
|
|
|
|
|
|
|
|
self._currLogicAddr += where
|
|
|
|
else:
|
|
|
|
raise RuntimeError("Unsupported whence type '{}'".format(whence))
|
|
|
|
|
2020-09-26 11:33:17 +09:00
|
|
|
def tell(self) -> int:
|
2020-08-24 13:08:44 +09:00
|
|
|
return self._currLogicAddr
|
|
|
|
|
2020-09-26 11:33:17 +09:00
|
|
|
def save(self, f: GC_File):
|
2020-08-24 13:08:44 +09:00
|
|
|
f.seek(0)
|
|
|
|
f.write(b"\x00" * 0x100)
|
|
|
|
|
|
|
|
for i in range(self.maxTextSections + self.maxDataSections):
|
|
|
|
if i < self.maxTextSections:
|
|
|
|
if i < len(self.textSections):
|
|
|
|
offset, address, size, data = self.textSections[i]
|
|
|
|
else:
|
|
|
|
continue
|
|
|
|
else:
|
|
|
|
if i - self.maxTextSections < len(self.dataSections):
|
|
|
|
offset, address, size, data = self.dataSections[i - self.maxTextSections]
|
|
|
|
else:
|
|
|
|
continue
|
|
|
|
|
|
|
|
f.seek(self.fileOffsetLoc + (i * 4))
|
2020-09-26 11:33:17 +09:00
|
|
|
f.write_uint32(offset) #offset in file
|
2020-08-24 13:08:44 +09:00
|
|
|
f.seek(self.fileAddressLoc + (i * 4))
|
2020-09-26 11:33:17 +09:00
|
|
|
f.write_uint32(address) #game address
|
2020-08-24 13:08:44 +09:00
|
|
|
f.seek(self.fileSizeLoc + (i * 4))
|
2020-09-26 11:33:17 +09:00
|
|
|
f.write_uint32(size) #size in file
|
2020-08-24 13:08:44 +09:00
|
|
|
|
2020-09-26 11:33:17 +09:00
|
|
|
if offset > f.get_size():
|
2020-08-24 13:08:44 +09:00
|
|
|
f.seek(0, 2)
|
2020-09-26 11:33:17 +09:00
|
|
|
f.write(b"\x00" * (offset - f.get_size()))
|
2020-08-24 13:08:44 +09:00
|
|
|
|
|
|
|
f.seek(offset)
|
|
|
|
f.write(data.getbuffer())
|
2020-09-26 11:33:17 +09:00
|
|
|
f.align_file(32)
|
2020-08-24 13:08:44 +09:00
|
|
|
|
|
|
|
f.seek(self.fileBssInfoLoc)
|
2020-09-26 11:33:17 +09:00
|
|
|
f.write_uint32(self.bssAddress)
|
|
|
|
f.write_uint32(self.bssSize)
|
2020-08-24 13:08:44 +09:00
|
|
|
|
|
|
|
f.seek(self.fileEntryLoc)
|
2020-09-26 11:33:17 +09:00
|
|
|
f.write_uint32(self.entryPoint)
|
|
|
|
f.align_file(256)
|
2020-08-24 13:08:44 +09:00
|
|
|
|
2020-09-26 11:33:17 +09:00
|
|
|
def get_full_size(self) -> int:
|
2020-08-24 13:08:44 +09:00
|
|
|
fullSize = 0x100
|
|
|
|
for section in self.textSections:
|
|
|
|
fullSize += section[2]
|
|
|
|
for section in self.dataSections:
|
|
|
|
fullSize += section[2]
|
|
|
|
return fullSize
|
|
|
|
|
2020-09-26 11:33:17 +09:00
|
|
|
def get_section_size(self, sectionsList: list, index: int) -> int:
|
2020-08-24 13:08:44 +09:00
|
|
|
return sectionsList[index][2]
|
|
|
|
|
2020-09-26 11:33:17 +09:00
|
|
|
def append_text_sections(self, sectionsList: list) -> bool:
|
|
|
|
""" Follows the list format: [tuple(<Bytes>Data, <Int>GameAddress or None), tuple(<Bytes>Data...
|
|
|
|
|
|
|
|
Returns True if the operation can be performed, otherwise it returns False """
|
2020-08-24 13:08:44 +09:00
|
|
|
|
|
|
|
'''Write offset/address/size to each section in DOL file header'''
|
|
|
|
for i, dataSet in enumerate(sectionsList):
|
|
|
|
if len(self.textSections) >= self.maxTextSections:
|
|
|
|
return False
|
|
|
|
|
|
|
|
fOffset, _, fSize, _ = self.get_final_section()
|
|
|
|
_, pAddress, pSize, _ = self.textSections[len(self.textSections) - 1]
|
|
|
|
data, address = dataSet
|
|
|
|
|
|
|
|
if not isinstance(data, BytesIO):
|
|
|
|
data = BytesIO(data)
|
|
|
|
|
|
|
|
offset = fOffset + fSize
|
|
|
|
|
|
|
|
if i < len(sectionsList) - 1:
|
|
|
|
size = (len(data.getbuffer()) + 31) & -32
|
|
|
|
else:
|
|
|
|
size = (len(data.getbuffer()) + 255) & -256
|
|
|
|
|
|
|
|
if address is None:
|
|
|
|
address = self.seek_nearest_unmapped(pAddress + pSize, size)
|
|
|
|
|
|
|
|
if address < 0x80000000 or address >= 0x81200000:
|
|
|
|
raise ValueError("Address '{:08X}' of text section {} is beyond scope (0x80000000 <-> 0x81200000)".format(address, i))
|
|
|
|
|
|
|
|
self.textSections.append((offset, address, size, data))
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
2020-09-26 11:33:17 +09:00
|
|
|
def append_data_sections(self, sectionsList: list) -> bool:
|
|
|
|
""" Follows the list format: [tuple(<Bytes>Data, <Int>GameAddress or None), tuple(<Bytes>Data...
|
|
|
|
|
|
|
|
Returns True if the operation can be performed, otherwise it returns False """
|
2020-08-24 13:08:44 +09:00
|
|
|
|
|
|
|
'''Write offset/address/size to each section in DOL file header'''
|
|
|
|
for i, dataSet in enumerate(sectionsList):
|
|
|
|
if len(self.dataSections) >= self.maxDataSections:
|
|
|
|
return False
|
|
|
|
|
|
|
|
fOffset, _, fSize, _ = self.get_final_section()
|
|
|
|
_, pAddress, pSize, _ = self.dataSections[len(self.dataSections) - 1]
|
|
|
|
data, address = dataSet
|
|
|
|
|
|
|
|
if not isinstance(data, BytesIO):
|
|
|
|
data = BytesIO(data)
|
|
|
|
|
|
|
|
offset = fOffset + fSize
|
|
|
|
|
|
|
|
if i < len(sectionsList) - 1:
|
|
|
|
size = (len(data.getbuffer()) + 31) & -32
|
|
|
|
else:
|
|
|
|
size = (len(data.getbuffer()) + 255) & -256
|
|
|
|
|
|
|
|
if address is None:
|
|
|
|
address = self.seek_nearest_unmapped(pAddress + pSize, size)
|
|
|
|
|
|
|
|
if address < 0x80000000 or address >= 0x81200000:
|
|
|
|
raise ValueError("Address '{:08X}' of data section {} is beyond scope (0x80000000 <-> 0x81200000)".format(address, i))
|
|
|
|
|
|
|
|
self.dataSections.append((offset, address, size, data))
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
2020-08-25 18:28:22 +09:00
|
|
|
def insert_branch(self, to: int, _from: int, lk=0):
|
|
|
|
self.seek(_from)
|
2020-09-26 11:33:17 +09:00
|
|
|
f.write_uint32(self, (to - _from) & 0x3FFFFFD | 0x48000000 | lk)
|
|
|
|
|
|
|
|
def extract_branch_addr(self, bAddr: int) -> tuple:
|
|
|
|
""" Returns the branch offset of the given instruction,
|
|
|
|
and if the branch is conditional """
|
2020-08-25 18:28:22 +09:00
|
|
|
|
|
|
|
self.seek(bAddr)
|
|
|
|
|
2020-09-26 11:33:17 +09:00
|
|
|
ppc = f.read_uint32(self)
|
|
|
|
conditional = False
|
|
|
|
|
|
|
|
if (ppc >> 24) & 0xFF < 0x48:
|
|
|
|
conditional = True
|
|
|
|
|
|
|
|
if conditional is True:
|
|
|
|
if (ppc & 0x8000):
|
|
|
|
offset = (ppc & 0xFFFD) - 0x10000
|
|
|
|
else:
|
|
|
|
offset = ppc & 0xFFFD
|
2020-08-25 18:28:22 +09:00
|
|
|
else:
|
2020-09-26 11:33:17 +09:00
|
|
|
if (ppc & 0x2000000):
|
|
|
|
offset = (ppc & 0x3FFFFFD) - 0x4000000
|
|
|
|
else:
|
|
|
|
offset = ppc & 0x3FFFFFD
|
2020-08-25 18:28:22 +09:00
|
|
|
|
2020-09-26 11:33:17 +09:00
|
|
|
return (bAddr + offset, conditional)
|
|
|
|
|
|
|
|
def read_string(self, addr: int = None, maxlen: int = 0, encoding: str = "utf-8") -> str:
|
|
|
|
""" Reads a null terminated string from the specified address """
|
|
|
|
|
|
|
|
if addr != None:
|
|
|
|
self.seek(addr)
|
2020-08-24 13:08:44 +09:00
|
|
|
|
|
|
|
length = 0
|
2020-09-26 11:33:17 +09:00
|
|
|
string = ""
|
|
|
|
while (char := self.read(1)) != b"\x00":
|
|
|
|
try:
|
|
|
|
string += char.decode(encoding)
|
|
|
|
except UnicodeDecodeError:
|
|
|
|
print(f"{char} at pos {length}, (address 0x{addr + length:08X}) is not a valid utf-8 character")
|
|
|
|
return ""
|
|
|
|
if length > maxlen and maxlen != 0:
|
2020-08-24 13:08:44 +09:00
|
|
|
break
|
|
|
|
|
2020-09-26 11:33:17 +09:00
|
|
|
return string
|
2020-08-24 13:08:44 +09:00
|
|
|
|
2020-09-26 11:33:17 +09:00
|
|
|
if __name__ == "__main__":
|
|
|
|
# Example usage (Reading global string "mario" from Super Mario Sunshine (NTSC-U))
|
2020-08-24 13:08:44 +09:00
|
|
|
|
2020-09-26 11:33:17 +09:00
|
|
|
with GC_File("Start.dol", "rb") as f:
|
|
|
|
dol = DolFile(f)
|
2020-08-24 13:08:44 +09:00
|
|
|
|
2020-09-26 11:33:17 +09:00
|
|
|
name = dol.read_string(addr=0x804165A0)
|
|
|
|
print(name)
|