SickGear/lib/hachoir/parser/program/nds.py
2023-02-09 13:41:15 +00:00

384 lines
14 KiB
Python

"""
Nintendo DS .nds game file parser
File format references:
- http://www.bottledlight.com/ds/index.php/FileFormats/NDSFormat
- http://imrannazar.com/The-Smallest-NDS-File
- http://darkfader.net/ds/files/ndstool.cpp
- http://crackerscrap.com/docs/dsromstructure.html
- http://nocash.emubase.de/gbatek.htm
"""
from hachoir.parser import Parser
from hachoir.field import (UInt8, UInt16, UInt32, UInt64, String, RawBytes, SubFile, FieldSet, NullBits, Bit, Bits, Bytes,
SeekableFieldSet, RootSeekableFieldSet)
from hachoir.core.text_handler import textHandler, hexadecimal
from hachoir.core.endian import LITTLE_ENDIAN
"""
CRC16 Calculation
Modified from:
http://www.mail-archive.com/python-list@python.org/msg47844.html
Original License:
crc16.py by Bryan G. Olson, 2005
This module is free software and may be used and
distributed under the same terms as Python itself.
"""
class CRC16:
_table = None
def _initTable(self):
from array import array
# CRC-16 poly: p(x) = x**16 + x**15 + x**2 + 1
# top bit implicit, reflected
poly = 0xa001
CRC16._table = array('H')
for byte in range(256):
crc = 0
for bit in range(8):
if (byte ^ crc) & 1:
crc = (crc >> 1) ^ poly
else:
crc >>= 1
byte >>= 1
CRC16._table.append(crc)
def checksum(self, string, value):
if CRC16._table is None:
self._initTable()
for ch in string:
value = self._table[ord(ch) ^ (value & 0xff)] ^ (value >> 8)
return value
class Crc16(UInt16):
"16 bit field for calculating and comparing CRC-16 of specified string"
def __init__(self, parent, name, targetBytes):
UInt16.__init__(self, parent, name)
self.targetBytes = targetBytes
def createDescription(self):
crc = CRC16().checksum(self.targetBytes, 0xffff)
if crc == self.value:
return "matches CRC of %d bytes" % len(self.targetBytes)
else:
return "mismatch (calculated CRC %d for %d bytes)" % (crc, len(self.targetBytes))
class FileNameDirTable(FieldSet):
static_size = (4 + 2 + 2) * 8
def createFields(self):
yield UInt32(self, "entry_start")
yield UInt16(self, "entry_file_id")
yield UInt16(self, "parent_id")
def createDescription(self):
return "first file id: %d; parent directory id: %d (%d)" % (self["entry_file_id"].value, self["parent_id"].value, self["parent_id"].value & 0xFFF)
class FileNameEntry(FieldSet):
def createFields(self):
yield Bits(self, "name_len", 7)
yield Bit(self, "is_directory")
yield String(self, "name", self["name_len"].value)
if self["is_directory"].value:
yield UInt16(self, "dir_id")
def createDescription(self):
s = ""
if self["is_directory"].value:
s = "[D] "
return s + self["name"].value
class Directory(FieldSet):
def createFields(self):
while True:
fne = FileNameEntry(self, "entry[]")
if fne["name_len"].value == 0:
yield UInt8(self, "end_marker")
break
yield fne
class FileNameTable(SeekableFieldSet):
def createFields(self):
self.startOffset = self.absolute_address // 8
# parent_id of first FileNameDirTable contains number of directories:
dt = FileNameDirTable(self, "dir_table[]")
numDirs = dt["parent_id"].value
yield dt
for i in range(1, numDirs):
yield FileNameDirTable(self, "dir_table[]")
for i in range(0, numDirs):
dt = self["dir_table[%d]" % i]
offset = self.startOffset + dt["entry_start"].value
self.seekByte(offset, relative=False)
yield Directory(self, "directory[]")
class FATFileEntry(FieldSet):
static_size = 2 * 4 * 8
def createFields(self):
yield UInt32(self, "start")
yield UInt32(self, "end")
def createDescription(self):
return "start: %d; size: %d" % (self["start"].value, self["end"].value - self["start"].value)
class FATContent(FieldSet):
def createFields(self):
num_entries = self.parent["header"]["fat_size"].value // 8
for i in range(0, num_entries):
yield FATFileEntry(self, "entry[]")
class BannerTile(FieldSet):
static_size = 32 * 8
def createFields(self):
for y in range(8):
for x in range(8):
yield Bits(self, "pixel[%d,%d]" % (x, y), 4)
class BannerIcon(FieldSet):
static_size = 16 * 32 * 8
def createFields(self):
for y in range(4):
for x in range(4):
yield BannerTile(self, "tile[%d,%d]" % (x, y))
class NdsColor(FieldSet):
static_size = 16
def createFields(self):
yield Bits(self, "red", 5)
yield Bits(self, "green", 5)
yield Bits(self, "blue", 5)
yield NullBits(self, "pad", 1)
def createDescription(self):
return "#%02x%02x%02x" % (self["red"].value << 3, self["green"].value << 3, self["blue"].value << 3)
class Banner(FieldSet):
static_size = 2112 * 8
def createFields(self):
yield UInt16(self, "version")
# CRC of this structure, excluding first 32 bytes:
yield Crc16(self, "crc", self.stream.readBytes(self.absolute_address + (32 * 8), (2112 - 32)))
yield RawBytes(self, "reserved", 28)
yield BannerIcon(self, "icon_data")
for i in range(0, 16):
yield NdsColor(self, "palette_color[]")
yield String(self, "title_jp", 256, charset="UTF-16-LE", truncate="\0")
yield String(self, "title_en", 256, charset="UTF-16-LE", truncate="\0")
yield String(self, "title_fr", 256, charset="UTF-16-LE", truncate="\0")
yield String(self, "title_de", 256, charset="UTF-16-LE", truncate="\0")
yield String(self, "title_it", 256, charset="UTF-16-LE", truncate="\0")
yield String(self, "title_es", 256, charset="UTF-16-LE", truncate="\0")
class Overlay(FieldSet):
static_size = 8 * 4 * 8
def createFields(self):
yield UInt32(self, "id")
yield textHandler(UInt32(self, "ram_address"), hexadecimal)
yield UInt32(self, "ram_size")
yield UInt32(self, "bss_size")
yield textHandler(UInt32(self, "init_start_address"), hexadecimal)
yield textHandler(UInt32(self, "init_end_address"), hexadecimal)
yield UInt32(self, "file_id")
yield RawBytes(self, "reserved[]", 4)
def createDescription(self):
return "file #%d, %d (+%d) bytes to 0x%08x" % (
self["file_id"].value, self["ram_size"].value, self["bss_size"].value, self["ram_address"].value)
class SecureArea(FieldSet):
static_size = 2048 * 8
def createFields(self):
yield textHandler(UInt64(self, "id"), hexadecimal)
if self["id"].value == 0xe7ffdeffe7ffdeff: # indicates that secure area is decrypted
yield Bytes(self, "fixed[]", 6) # always \xff\xde\xff\xe7\xff\xde
yield Crc16(self, "header_crc16", self.stream.readBytes(self.absolute_address + (16 * 8), 2048 - 16))
yield RawBytes(self, "unknown[]", 2048 - 16 - 2)
yield Bytes(self, "fixed[]", 2) # always \0\0
else:
yield RawBytes(self, "encrypted[]", 2048 - 8)
class DeviceSize(UInt8):
def createDescription(self):
return "%d Mbit" % ((2**(20 + self.value)) // (1024 * 1024))
class Header(FieldSet):
def createFields(self):
yield String(self, "game_title", 12, truncate="\0")
yield String(self, "game_code", 4)
yield String(self, "maker_code", 2)
yield UInt8(self, "unit_code")
yield UInt8(self, "device_code")
yield DeviceSize(self, "card_size")
yield String(self, "card_info", 9)
yield UInt8(self, "rom_version")
yield Bits(self, "unknown_flags[]", 2)
yield Bit(self, "autostart_flag")
yield Bits(self, "unknown_flags[]", 5)
yield UInt32(self, "arm9_source", "ARM9 ROM offset")
yield textHandler(UInt32(self, "arm9_execute_addr", "ARM9 entry address"), hexadecimal)
yield textHandler(UInt32(self, "arm9_copy_to_addr", "ARM9 RAM address"), hexadecimal)
yield UInt32(self, "arm9_bin_size", "ARM9 code size")
yield UInt32(self, "arm7_source", "ARM7 ROM offset")
yield textHandler(UInt32(self, "arm7_execute_addr", "ARM7 entry address"), hexadecimal)
yield textHandler(UInt32(self, "arm7_copy_to_addr", "ARM7 RAM address"), hexadecimal)
yield UInt32(self, "arm7_bin_size", "ARM7 code size")
yield UInt32(self, "filename_table_offset")
yield UInt32(self, "filename_table_size")
yield UInt32(self, "fat_offset")
yield UInt32(self, "fat_size")
yield UInt32(self, "arm9_overlay_src")
yield UInt32(self, "arm9_overlay_size")
yield UInt32(self, "arm7_overlay_src")
yield UInt32(self, "arm7_overlay_size")
yield textHandler(UInt32(self, "ctl_read_flags"), hexadecimal)
yield textHandler(UInt32(self, "ctl_init_flags"), hexadecimal)
yield UInt32(self, "banner_offset")
yield Crc16(self, "secure_crc16", self.stream.readBytes(0x4000 * 8, 0x4000))
yield UInt16(self, "rom_timeout")
yield UInt32(self, "arm9_unk_addr")
yield UInt32(self, "arm7_unk_addr")
yield UInt64(self, "unenc_mode_magic")
yield UInt32(self, "rom_size")
yield UInt32(self, "header_size")
yield RawBytes(self, "unknown[]", 36)
yield String(self, "passme_autoboot_detect", 4)
yield RawBytes(self, "unknown[]", 16)
yield RawBytes(self, "gba_logo", 156)
yield Crc16(self, "logo_crc16", self.stream.readBytes(0xc0 * 8, 156))
yield Crc16(self, "header_crc16", self.stream.readBytes(0, 350))
yield UInt32(self, "debug_rom_offset")
yield UInt32(self, "debug_size")
yield textHandler(UInt32(self, "debug_ram_address"), hexadecimal)
class NdsFile(Parser, RootSeekableFieldSet):
PARSER_TAGS = {
"id": "nds_file",
"category": "program",
"file_ext": ("nds",),
"mime": ("application/octet-stream",),
"min_size": 352 * 8, # just a minimal header
"description": "Nintendo DS game file",
}
endian = LITTLE_ENDIAN
def validate(self):
try:
header = self["header"]
except Exception:
return False
return (self.stream.readBytes(0, 1) != b"\0"
and (header["device_code"].value & 7) == 0
and header["header_size"].value >= 352
and header["card_size"].value < 15 # arbitrary limit at 32Gbit
and header["arm9_bin_size"].value > 0 and header["arm9_bin_size"].value <= 0x3bfe00
and header["arm7_bin_size"].value > 0 and header["arm7_bin_size"].value <= 0x3bfe00
and header["arm9_source"].value + header["arm9_bin_size"].value < self._size
and header["arm7_source"].value + header["arm7_bin_size"].value < self._size
and header["arm9_execute_addr"].value >= 0x02000000 and header["arm9_execute_addr"].value <= 0x023bfe00
and header["arm9_copy_to_addr"].value >= 0x02000000 and header["arm9_copy_to_addr"].value <= 0x023bfe00
and header["arm7_execute_addr"].value >= 0x02000000 and header["arm7_execute_addr"].value <= 0x03807e00
and header["arm7_copy_to_addr"].value >= 0x02000000 and header["arm7_copy_to_addr"].value <= 0x03807e00
)
def createFields(self):
# Header
yield Header(self, "header")
# Secure Area
if self["header"]["arm9_source"].value >= 0x4000 and self["header"]["arm9_source"].value < 0x8000:
secStart = self["header"]["arm9_source"].value & 0xfffff000
self.seekByte(secStart, relative=False)
yield SecureArea(self, "secure_area", size=0x8000 - secStart)
# ARM9 binary
self.seekByte(self["header"]["arm9_source"].value, relative=False)
yield RawBytes(self, "arm9_bin", self["header"]["arm9_bin_size"].value)
# ARM7 binary
self.seekByte(self["header"]["arm7_source"].value, relative=False)
yield RawBytes(self, "arm7_bin", self["header"]["arm7_bin_size"].value)
# File Name Table
if self["header"]["filename_table_size"].value > 0:
self.seekByte(
self["header"]["filename_table_offset"].value, relative=False)
yield FileNameTable(self, "filename_table", size=self["header"]["filename_table_size"].value * 8)
# FAT
if self["header"]["fat_size"].value > 0:
self.seekByte(self["header"]["fat_offset"].value, relative=False)
yield FATContent(self, "fat_content", size=self["header"]["fat_size"].value * 8)
# banner
if self["header"]["banner_offset"].value > 0:
self.seekByte(
self["header"]["banner_offset"].value, relative=False)
yield Banner(self, "banner")
# ARM9 overlays
if self["header"]["arm9_overlay_src"].value > 0:
self.seekByte(
self["header"]["arm9_overlay_src"].value, relative=False)
numOvls = self["header"]["arm9_overlay_size"].value // (8 * 4)
for i in range(numOvls):
yield Overlay(self, "arm9_overlay[]")
# files
if self["header"]["fat_size"].value > 0:
for field in self["fat_content"]:
if field["end"].value > field["start"].value:
self.seekByte(field["start"].value, relative=False)
yield SubFile(self, "file[]", field["end"].value - field["start"].value)