Source code for ndspy.rom

# Copyright 2019 RoadrunnerWMC
#
# This file is part of ndspy.
#
# ndspy is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ndspy is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with ndspy.  If not, see <https://www.gnu.org/licenses/>.
"""
Support for ROMs.
"""


import math
import struct

from . import _common
from . import code
from . import fnt as fntLib


_ICON_BANNER_LENGTHS = {
    # version: length,
    0x0001: 0x840,
    0x0002: 0x940,
    0x0003: 0x1240,
    0x0103: 0x23C0,
}


[docs]class NintendoDSRom: """ A Nintendo DS ROM file (.nds). """ def __init__(self, data=None): super().__init__() if data is None: self._initAsNew() else: self._initFromData(data) def _initAsNew(self): """ Initialize this ROM with default values. """ self.name = b'' self.idCode = b'####' self.developerCode = b'\0\0' self.unitCode = 0 self.encryptionSeedSelect = 0 self.deviceCapacity = 9 self.pad015 = 0 self.pad016 = 0 self.pad017 = 0 self.pad018 = 0 self.pad019 = 0 self.pad01A = 0 self.pad01B = 0 self.pad01C = 0 self.region = 0 self.version = 0 self.autostart = 0 self.arm9EntryAddress = 0x2000800 self.arm9RamAddress = 0x2000000 self.arm7EntryAddress = 0x2380000 self.arm7RamAddress = 0x2380000 self.normalCardControlRegisterSettings = 0x0416657 self.secureCardControlRegisterSettings = 0x81808f8 self.secureAreaChecksum = 0x0000 self.secureTransferDelay = 0x0D7E self.arm9CodeSettingsPointerAddress = 0 self.arm7CodeSettingsPointerAddress = 0 self.secureAreaDisable = b'\0' * 8 self.pad088 = b'\0' * 0x38 self.nintendoLogo = (b'$\xff\xaeQi\x9a\xa2!=\x84\x82\n\x84\xe4\t\xad' b"\x11$\x8b\x98\xc0\x81\x7f!\xa3R\xbe\x19\x93\t\xce \x10FJJ\xf8'1" b'\xecX\xc7\xe83\x82\xe3\xce\xbf\x85\xf4\xdf\x94\xceK\t\xc1\x94V' b"\x8a\xc0\x13r\xa7\xfc\x9f\x84Ms\xa3\xca\x9aaX\x97\xa3'\xfc\3\x98" b'v#\x1d\xc7a\3\4\xaeV\xbf8\x84\0@\xa7\x0e\xfd\xffR\xfe\3o\x950' b'\xf1\x97\xfb\xc0\x85`\xd6\x80%\xa9c\xbe\3\1N8\xe2\xf9\xa24\xff' b'\xbb>\3Dx\0\x90\xcb\x88\x11:\x94e\xc0|c\x87\xf0<\xaf\xd6%\xe4' b'\x8b8\n\xacr!\xd4\xf8\7') self.debugRomAddress = 0 self.pad16C = b'\0' * 0x94 self.pad200 = b'\0' * 0x3E00 self.rsaSignature = b'' self.arm9 = b'' self.arm9PostData = b'' self.arm7 = b'' self.arm9OverlayTable = b'' self.arm7OverlayTable = b'' self.iconBanner = b'' self.debugRom = b'' self.filenames = fntLib.Folder() self.files = [] self.sortedFileIds = [] def _initFromData(self, data): """ Initialize this ROM from existing data. """ # I could read the header as one huge struct, # but... no. data = bytearray(data) if len(data) < 0x200: data.extend(b'\0' * (0x200 - len(data))) assert len(data) == 0x200, f'ROM data extension to length 0x200 failed (actual new length {hex(len(data))})' headerOffset = 0 def readRaw(length): nonlocal headerOffset retVal = data[headerOffset : headerOffset+length] headerOffset += length return retVal def read8(): nonlocal headerOffset retVal = data[headerOffset] headerOffset += 1 return retVal def read16(): nonlocal headerOffset retVal, = struct.unpack_from('<H', data, headerOffset) headerOffset += 2 return retVal def read32(): nonlocal headerOffset retVal, = struct.unpack_from('<I', data, headerOffset) headerOffset += 4 return retVal assert headerOffset == 0, f'(Load) Header offset check at 0x00: {hex(headerOffset)}' self.name = readRaw(12).rstrip(b'\0') self.idCode = readRaw(4) self.developerCode = readRaw(2) self.unitCode = read8() self.encryptionSeedSelect = read8() self.deviceCapacity = read8() assert headerOffset == 0x15, f'(Load) Header offset check at 0x15: {hex(headerOffset)}' self.pad015 = read8() self.pad016 = read8() self.pad017 = read8() self.pad018 = read8() self.pad019 = read8() self.pad01A = read8() self.pad01B = read8() self.pad01C = read8() self.region = read8() self.version = read8() self.autostart = read8() assert headerOffset == 0x20, f'(Load) Header offset check at 0x20: {hex(headerOffset)}' arm9Offset = read32() self.arm9EntryAddress = read32() self.arm9RamAddress = read32() arm9Len = read32() arm7Offset = read32() self.arm7EntryAddress = read32() self.arm7RamAddress = read32() arm7Len = read32() assert headerOffset == 0x40, f'(Load) Header offset check at 0x40: {hex(headerOffset)}' fntOffset = read32() fntLen = read32() fatOffset = read32() fatLen = read32() arm9OvTOffset = read32() arm9OvTLen = read32() arm7OvTOffset = read32() arm7OvTLen = read32() assert headerOffset == 0x60, f'(Load) Header offset check at 0x60: {hex(headerOffset)}' self.normalCardControlRegisterSettings = read32() self.secureCardControlRegisterSettings = read32() iconBannerOffset = read32() self.secureAreaChecksum = read16() # TODO: Actually recalculate # this upon saving. self.secureTransferDelay = read16() assert headerOffset == 0x70, f'(Load) Header offset check at 0x70: {hex(headerOffset)}' self.arm9CodeSettingsPointerAddress = read32() self.arm7CodeSettingsPointerAddress = read32() self.secureAreaDisable = readRaw(8) assert headerOffset == 0x80, f'(Load) Header offset check at 0x80: {hex(headerOffset)}' romSizeOrRsaSigOffset = read32() headerSize = read32() self.pad088 = readRaw(0x38) self.nintendoLogo = readRaw(0x9C) nintendoLogoChecksum = read16() headerChecksum = read16() assert headerOffset == 0x160, f'(Load) Header offset check at 0x160: {hex(headerOffset)}' debugRomOffset = read32() debugRomSize = read32() self.debugRomAddress = read32() self.pad16C = readRaw(0x94) assert headerOffset == 0x200, f'(Load) Header offset check at 0x200: {hex(headerOffset)}' self.pad200 = data[0x200 : min(arm9Offset, len(data))] # Read the RSA signature file realSigOffset = 0 if len(data) >= 0x1004: realSigOffset, = struct.unpack_from('<I', data, 0x1000) if not realSigOffset and len(data) > romSizeOrRsaSigOffset: realSigOffset = romSizeOrRsaSigOffset self.rsaSignature = b'' if realSigOffset: self.rsaSignature = data[realSigOffset : min(len(data), realSigOffset + 0x88)] # Read arm9, arm7, FNT, FAT, overlay tables, icon banner self.arm9 = data[arm9Offset : arm9Offset+arm9Len] self.arm7 = data[arm7Offset : arm7Offset+arm7Len] fnt = data[fntOffset : fntOffset+fntLen] fat = data[fatOffset : fatOffset+fatLen] self.arm9OverlayTable = data[ arm9OvTOffset : arm9OvTOffset + arm9OvTLen] self.arm7OverlayTable = data[ arm7OvTOffset : arm7OvTOffset + arm7OvTLen] if iconBannerOffset: version, = struct.unpack_from('<H', data, iconBannerOffset) iconBannerLen = _ICON_BANNER_LENGTHS.get(version, _ICON_BANNER_LENGTHS[1]) self.iconBanner = \ data[iconBannerOffset : iconBannerOffset + iconBannerLen] else: self.iconBanner = b'' if debugRomOffset: self.debugRom = \ data[debugRomOffset : debugRomOffset + debugRomSize] else: self.debugRom = b'' # Read the small amount of data immediately following arm9 # No idea what this is, though... # Probably related to the "code settings" stuff in code.py. arm9PostData = bytearray() arm9PostDataOffset = arm9Offset+arm9Len while (data[arm9PostDataOffset:arm9PostDataOffset+4] == b'\x21\x06\xC0\xDE'): arm9PostData.extend(data[arm9PostDataOffset:arm9PostDataOffset+12]) arm9PostDataOffset += 12 self.arm9PostData = arm9PostData # Read the filename table if fnt: self.filenames = fntLib.load(fnt) else: self.filenames = fntLib.Folder() # Read files self.files = [] self.sortedFileIds = [] if fat: offset2Id = {} for i in range(len(fat) // 8): startOffset, endOffset = struct.unpack_from('<II', fat, 8 * i) self.files.append(data[startOffset:endOffset]) offset2Id[startOffset] = i for off in sorted(offset2Id): self.sortedFileIds.append(offset2Id[off])
[docs] @classmethod def fromFile(cls, filePath): """ Load a ROM from a filesystem file. """ with open(filePath, 'rb') as f: return cls(f.read())
[docs] def save(self, *, updateDeviceCapacity=False): """ Generate file data representing this ROM. """ fileOffsets = {} # The header will be filled in at the end. data = bytearray(0x200) def align(alignment, fill=b'\0'): if len(data) % alignment: extra = len(data) % alignment needed = alignment - extra data.extend(fill * needed) # Add post-header padding data.extend(self.pad200) align(0x4000) # Pack arm9 arm9Offset = len(data) data.extend(self.arm9) data.extend(self.arm9PostData) align(0x200, b'\xFF') # Pack arm9 overlay table if self.arm9OverlayTable: arm9OvTOffset = len(data) data.extend(self.arm9OverlayTable) align(0x200, b'\xFF') else: arm9OvTOffset = 0 # Pack arm9 overlays for i in range(0, len(self.arm9OverlayTable), 32): fileId, = struct.unpack_from('<I', self.arm9OverlayTable, i + 0x18) fileOffsets[fileId] = len(data) data.extend(self.files[fileId]) align(0x200, b'\xFF') # Pack arm7 arm7Offset = len(data) data.extend(self.arm7) align(0x200, b'\xFF') # Pack arm7 overlay table if self.arm7OverlayTable: arm7OvTOffset = len(data) data.extend(self.arm7OverlayTable) align(0x200, b'\xFF') else: arm7OvTOffset = 0 # Pack arm7 overlays for i in range(0, len(self.arm7OverlayTable), 32): fileId, = struct.unpack_from('<I', self.arm7OverlayTable, i + 0x18) fileOffsets[fileId] = len(data) data.extend(self.files[fileId]) align(0x200, b'\xFF') # Pack the filename table fntOffset = len(data) fnt = fntLib.save(self.filenames) data.extend(fnt) align(0x200, b'\xFF') # Leave some empty space for the file allocation table -- we'll # fill in the real values later fatOffset = len(data) data.extend(b'\0' * 8 * len(self.files)) align(0x200, b'\xFF') # Pack the icon/banner if self.iconBanner: version, = struct.unpack_from('<H', self.iconBanner, 0) iconBannerLen = _ICON_BANNER_LENGTHS.get(version, _ICON_BANNER_LENGTHS[1]) assert len(self.iconBanner) == iconBannerLen, f'(Save) Icon banner length is wrong (version {hex(version)}, length {hex(len(self.iconBanner))})' iconBannerOffset = len(data) data.extend(self.iconBanner) align(0x200, b'\xFF') else: iconBannerOffset = 0 # Pack the debug rom # I don't know if this is really where it would go, but it seems # to be the logical place to put it... if self.debugRom: debugRomOffset = len(data) data.extend(self.debugRom) align(0x200, b'\xFF') else: debugRomOffset = 0 # Pack the rest of the files def iterFilenums(): for fileNum in self.sortedFileIds: if fileNum not in fileOffsets and fileNum < len(self.files): yield fileNum for fileNum in range(len(self.files)): if fileNum not in fileOffsets: yield fileNum for fileNum in iterFilenums(): # Align before instead of after, so that there's no extra # padding after the last file align(0x200, b'\xFF') fileOffsets[fileNum] = len(data) data.extend(self.files[fileNum]) # Pack the FAT for i, file in enumerate(self.files): assert i in fileOffsets, f'(Save) File {i} has no offset' startOffset = fileOffsets[i] endOffset = startOffset + len(file) struct.pack_into('<II', data, fatOffset + 8 * i, startOffset, endOffset) # Pack the RSA signature align(0x20) rsaSignatureOffset = len(data) data.extend(self.rsaSignature) # We need to do this for compatibility with NSMBe struct.pack_into('<I', data, 0x1000, rsaSignatureOffset) # Now that we know how large the ROM data is, we can update the # device capacity value if updateDeviceCapacity: self.deviceCapacity = math.ceil(math.log2(len(data))) - 17 # Now that all the offsets and stuff are determined, write the # header data headerOffset = 0 def writeRaw(value): nonlocal headerOffset data[headerOffset : headerOffset+len(value)] = value headerOffset += len(value) def write8(value): nonlocal headerOffset data[headerOffset] = value headerOffset += 1 def write16(value): nonlocal headerOffset struct.pack_into('<H', data, headerOffset, value) headerOffset += 2 def write32(value): nonlocal headerOffset struct.pack_into('<I', data, headerOffset, value) headerOffset += 4 assert headerOffset == 0, f'(Save) Header offset check at 0x00: {hex(headerOffset)}' writeRaw(self.name.ljust(12, b'\0')[:12]) assert len(self.idCode) == 4, f'(Save) Wrong ID code length: {len(self.idCode)}' writeRaw(self.idCode) assert len(self.developerCode) == 2, f'(Save) Wrong developer code length: {len(self.developerCode)}' writeRaw(self.developerCode) write8(self.unitCode) write8(self.encryptionSeedSelect) write8(self.deviceCapacity) assert headerOffset == 0x15, f'(Save) Header offset check at 0x15: {hex(headerOffset)}' write8(self.pad015) write8(self.pad016) write8(self.pad017) write8(self.pad018) write8(self.pad019) write8(self.pad01A) write8(self.pad01B) write8(self.pad01C) write8(self.region) write8(self.version) write8(self.autostart) assert headerOffset == 0x20, f'(Save) Header offset check at 0x20: {hex(headerOffset)}' write32(arm9Offset) write32(self.arm9EntryAddress) write32(self.arm9RamAddress) write32(len(self.arm9)) write32(arm7Offset) write32(self.arm7EntryAddress) write32(self.arm7RamAddress) write32(len(self.arm7)) assert headerOffset == 0x40, f'(Save) Header offset check at 0x40: {hex(headerOffset)}' write32(fntOffset) write32(len(fnt)) write32(fatOffset) write32(len(self.files) * 8) write32(arm9OvTOffset) write32(len(self.arm9OverlayTable)) write32(arm7OvTOffset) write32(len(self.arm7OverlayTable)) assert headerOffset == 0x60, f'(Save) Header offset check at 0x60: {hex(headerOffset)}' write32(self.normalCardControlRegisterSettings) write32(self.secureCardControlRegisterSettings) write32(iconBannerOffset) write16(self.secureAreaChecksum) write16(self.secureTransferDelay) assert headerOffset == 0x70, f'(Save) Header offset check at 0x70: {hex(headerOffset)}' write32(self.arm9CodeSettingsPointerAddress) write32(self.arm7CodeSettingsPointerAddress) writeRaw(self.secureAreaDisable.ljust(8, b'\0')[:8]) assert headerOffset == 0x80, f'(Save) Header offset check at 0x80: {hex(headerOffset)}' write32(rsaSignatureOffset) write32(0x4000) assert len(self.pad088) == 0x38, f'(Save) Wrong pad088 length: {hex(len(self.pad088))}' writeRaw(self.pad088) assert len(self.nintendoLogo) == 0x9C, f'(Save) Wrong Nintendo logo length: {hex(len(self.nintendoLogo))}' writeRaw(self.nintendoLogo) write16(_common.crc16(self.nintendoLogo)) write16(_common.crc16(data[0:0x15e])) assert headerOffset == 0x160, f'(Save) Header offset check at 0x160: {hex(headerOffset)}' write32(debugRomOffset) write32(len(self.debugRom)) write32(self.debugRomAddress) assert len(self.pad16C) == 0x94, f'(Save) Wrong pad16C length: {hex(len(self.pad16C))}' writeRaw(self.pad16C) assert headerOffset == 0x200, f'(Save) Header offset check at 0x200: {hex(headerOffset)}' return bytes(data)
[docs] def saveToFile(self, filePath, *, updateDeviceCapacity=False): """ Generate file data representing this ROM, and save it to a filesystem file. """ d = self.save(updateDeviceCapacity=updateDeviceCapacity) with open(filePath, 'wb') as f: f.write(d)
[docs] def loadArm9(self): """ Create a MainCodeFile object representing the main ARM9 code file in this ROM. """ return code.MainCodeFile(self.arm9, self.arm9RamAddress, self.arm9CodeSettingsPointerAddress)
[docs] def loadArm7(self): """ Create a MainCodeFile object representing the main ARM7 code file in this ROM. """ return code.MainCodeFile(self.arm7, self.arm7RamAddress, self.arm7CodeSettingsPointerAddress)
[docs] def loadArm9Overlays(self, idsToLoad=None): """ Create a dictionary of this ROM's ARM9 overlays. """ def callback(ovID, fileID): return self.files[fileID] return code.loadOverlayTable(self.arm9OverlayTable, callback, idsToLoad)
[docs] def loadArm7Overlays(self, idsToLoad=None): """ Create a dictionary of this ROM's ARM7 overlays. """ def callback(ovID, fileID): return self.files[fileID] return code.loadOverlayTable(self.arm7OverlayTable, callback, idsToLoad)
[docs] def getFileByName(self, filename): """ Return the data for the file with the given filename (path). This is a convenience function. """ fid = self.filenames.idOf(filename) if fid is None: raise ValueError(f'Cannot find file ID of "{filename}"') return self.files[fid]
[docs] def setFileByName(self, filename, data): """ Replace the data for the file with the given filename (path) with the given data. This is a convenience function. """ fid = self.filenames.idOf(filename) if fid is None: raise ValueError(f'Cannot find file ID of "{filename}"') self.files[fid] = data
def __str__(self): title = repr(bytes(self.name))[2:-1].rstrip(' ') code = repr(bytes(self.idCode))[2:-1] return f'<rom "{title}" ({code})>' def __repr__(self): try: data = _common.shortBytesRepr(self.save()) except Exception: data = '...' return f'{type(self).__name__}({data})'