""" This module is currently not finished, and the interface exposed by NdsHeader is likely to change quite a lot. The filename might change to 'header' if more rom header support is added (and gba rom header support *is* planned... """ import ndscodes, cksum, ndsencrypt import struct, binascii def _lookup(dic, key): try: return dic[key] except: return None #TODO: Internal CRC's #TODO: Extraction of the icon into something usable #TODO: filesystem extraction (how to do this to mem?) #TODO: writing over pieces of the header """ The _unpackstr documented!: 12 string, 4 string, 2 string, 3 byte, 9 string (bytes), 2 byte, 19 integer, 2 short, 20 integer, 156 string (icon), 2 short, 4 integer (more offsets), 144 string (zero?) """ _unpackstr = '=12s4s2s3B9s2B19I2H20I156s2H4I144s' # sanity check if struct.calcsize(_unpackstr) != 512: print "warning: size of header unpack string is the wrong size" _romtype = '=3I' _banner_ups = "=2H28s512s32s1536s" _HOMEBREW = 'homebrew' _MULTIBOOT = 'multiboot' _NDSDUMPED = 'decrypted' _MASKROM = 'mask ROM' _ENCRSECURE = 'encrypted' # a dictionary of tuples: # (struct position, offset in bytes, length, unpackstr, disp name) header_fields = { 'game_name' : (0, 0x000, 0x0C, '=12s', 'Game title' ), 'game_code' : (1, 0x00C, 0x04, '=4s', 'Game code' ), 'country_code' : (None, 0x00F, 0x01, '=B', '' ), 'maker_code' : (2, 0x010, 0x02, '=2s', 'Maker code' ), 'unit_code' : (3, 0x012, 0x01, '=B', 'Unit code' ), 'device_type' : (4, 0x013, 0x01, '=B', 'Device type' ), 'device_capacity' : (5, 0x014, 0x01, '=B', 'Device capacity'), 'reserved_1' : (6, 0x015, 0x09, '=9s', 'Reserved 1' ), 'reserved_2' : (8, 0x01F, 0x01, '=B', 'Reserved 2' ), 'rom_version' : (7, 0x01E, 0x01, '=B', 'ROM Version' ), 'arm9_rom_offset' : (9, 0x020, 0x04, '=I', 'ARM9 ROM offset'), # 19 integers... 'arm9_entry_address' : (10, 0x024, 0x04, '=I', 'ARM9 entry address'), 'arm9_ram_address' : (11, 0x028, 0x04, '=I', 'ARM9 RAM address'), 'arm9_size' : (12, 0x02C, 0x04, '=I', 'ARM9 code size' ), 'arm7_rom_offset' : (13, 0x030, 0x04, '=I', 'ARM7 ROM offset'), 'arm7_entry_address' : (14, 0x034, 0x04, '=I', 'ARM7 entry address'), 'arm7_ram_address' : (15, 0x038, 0x04, '=I', 'ARM7 RAM address'), 'arm7_size' : (16, 0x03C, 0x04, '=I', 'ARM7 code size' ), 'fnt_offset' : (17, 0x040, 0x04, '=I', 'File name table offset'), 'fnt_size' : (18, 0x044, 0x04, '=I', 'File name table size'), 'fat_offset' : (19, 0x048, 0x04, '=I', 'FAT offset' ), 'fat_size' : (20, 0x04C, 0x04, '=I', 'FAT size' ), 'arm9_overlay_offset' : (21, 0x050, 0x04, '=I', 'ARM9 overlay offset'), 'arm9_overlay_size' : (22, 0x054, 0x04, '=I', 'ARM9 overlay size'), 'arm7_overlay_offset' : (23, 0x058, 0x04, '=I', 'ARM7 overlay offset'), 'arm7_overlay_size' : (24, 0x05C, 0x04, '=I', 'ARM7 overlay size'), 'rom_control_info_1' : (25, 0x060, 0x04, '=I', 'ROM control info 1'), 'rom_control_info_2' : (26, 0x064, 0x04, '=I', 'ROM control info 2'), 'rom_control_info_3' : (29, 0x06E, 0x02, '=H', 'ROM control info 3'), 'banner_offset' : (27, 0x068, 0x04, '=I', 'Icon/Title offset'), 'secure_area_crc' : (28, 0x06C, 0x02, '=H', 'Secure area CRC'), 'arm9?' : (30, 0x070, 0x04, '=I', 'ARM9?' ), 'arm7?' : (31, 0x074, 0x04, '=I', 'ARM7?' ), 'magic_1' : (32, 0x078, 0x04, '=I', 'Magic 1' ), 'magic_2' : (33, 0x07C, 0x04, '=I', 'Magic 2' ), 'app_end_offset' : (34, 0x080, 0x04, '=I', 'Application end offset'), 'rom_header_size' : (35, 0x084, 0x04, '=I', 'ROM header size'), 'logo' : (50, 0x0C0, 0x9C, '=156s','' ), 'logo_crc' : (51, 0x15C, 0x02, '=H', 'Logo CRC' ), 'header_crc' : (52, 0x15E, 0x02, '=H', 'Header CRC' ), # get these via NdsHeader.staticmethods } # There is a disparity here between the comments in ndstool # and the code. offset 0x70, 0x74, 0x78, and 0x7C are commented # as 'magic1', 'magic2', and then 'homebrew codes' respectively, # but are printed out as 'arm9?', 'arm7?', 'magic1', 'magic2' # until I figure out why from someone, I'm keeping the output # and behavior the same as ndstool's STRUCTOFFSET, FILEOFFSET, DLENGTH, STRUCTFMT, DISPLAYNAME = 0, 1, 2, 3, 4 def _up1(fmt, string): """unpack 1 value from 'string' using 'fmt' format""" if fmt in ('=I', '=B', '=H'): # i was returning this as a hex string before, # which is stupid return struct.unpack(fmt, string)[0] return struct.unpack(fmt, string)[0] def _p1(fmt, value): """pack 1 value using 'fmt' format""" return struct.pack(fmt, value) def get_hval(fileobj, name): """get header value from header_fields name""" i = header_fields[name] fileobj.seek(i[FILEOFFSET]) return _up1(i[STRUCTFMT], fileobj.read(i[DLENGTH])) def set_hval(fileobj, name, value): """set header value from header_fields name""" i = header_fields[name] fileobj.seek(i[FILEOFFSET]) packed = _p1(i[STRUCTFMT], value) fileobj.write(packed) def _of_resolve(obj, fileobj, attr): """This procedure is used by NdsHeader's static methods to determine if they should be reading from the fileobj or using an object for the header entry values they need.""" if obj: return getattr(obj, attr) return get_hval(fileobj, attr) class NdsHeader: def __init__(self, fileobj): """A structure that represents an NDS Rom header. NdsHeader sports a number of static methods that may be invoked as a utility statically, but take an optional "self" keyword arg. These functions will use the values already within the NdsHeader object if they are made available by passing "self", and if not they will read from the passed fileobject using the header_field offsets to obtain necessary values. """ info = fileobj.read(struct.calcsize(_unpackstr)) fileobj.seek(0) parts = struct.unpack(_unpackstr, info) for key, tup in header_fields.items(): if tup[STRUCTOFFSET] is not None: setattr(self, key, parts[tup[STRUCTOFFSET]]) self._clean_attrs() self.rom_type = NdsHeader.getRomType(fileobj, self) # get CRCs self.logo_crc_check = cksum.crc16(self.logo) self.header_crc_check = cksum.crc16(info[:0x15E]) self.secure_area_crc_check = NdsHeader.getSecureAreaCRC(fileobj, self) if self.rom_type in (_NDSDUMPED, _ENCRSECURE, _MASKROM): self.security_data_crc = NdsHeader.getSecurityDataCRC(fileobj, self) self.segment3_crc = NdsHeader.getSegment3CRC(fileobj, self) self.banner_data = NdsHeader.getBannerData(fileobj, self) self.banner = NdsBanner(self.banner_data, self.banner_offset) self.arm9_footer_found = NdsHeader.findArm9Footer(fileobj, self) def _clean_attrs(self): self.game_name = self.game_name.strip('\x00') self.country_code = self.game_code[-1] self.country = _lookup(ndscodes.countries, self.country_code) self.maker = _lookup(ndscodes.makers, self.maker_code) @staticmethod def findArm9Footer(fileobj, self=None): """Find the ARM9 footer in a rom.""" a9off = _of_resolve(self, fileobj, 'arm9_rom_offset') a9size = _of_resolve(self, fileobj, 'arm9_size') fileobj.seek(a9off + a9size) nitro = _up1("=I", fileobj.read(4)) if nitro == 0xDEC00621: return True return False # CRC fixing functions @staticmethod def fixSecureAreaCRC(fileobj, self=False): """Fix the Security area CRC. Return True if it was changed, False if not.""" if 'w' not in fileobj.mode: raise IOError, "file %s not writable" % (fileobj) secure_area_crc = _of_resolve(self, fileobj, 'security_area_crc') secure_area_crc_check = NdsHeader.getSecureAreaCRC(fileobj, self) if secure_area_crc == secure_area_crc_check: # the CRC was good return False set_hval(fileobj, 'secure_area_crc', secure_area_crc_check) return True @staticmethod def fixLogoCRC(fileobj, self=False): if 'w' not in fileobj.mode: raise IOError, "file %s not writable" % (fileobj) logo = _of_resolve(self, fileobj, 'logo') logo_crc = _of_resolve(self, fileobj, 'logo_crc') if self: logo_crc_check = self.logo_crc_check else: logo_crc_check = cksum.crc16(logo) if logo_crc == logo_crc_check: return False set_hval(fileobj, 'logo_crc', logo_crc_check) return True @staticmethod def fixHeaderCRC(fileobj, self=False): """Fix the Header CRC. NOTE: to fix all CRCs in the header, call 'NdsHeader.fixHeaderCRCs' """ if 'w' not in fileobj.mode: raise IOError, "file %s not writable" % (fileobj) header_crc = _of_resolve(self, fileobj, 'header_crc') if self: header_crc_check = self.header_crc_check else: fileobj.seek(0) header_crc_check = cksum.crc16(fileobj.read(0x15E)) if header_crc == header_crc_check: return False set_hval(fileobj, 'header_crc', header_crc_check) return True @staticmethod def fixHeaderCRCs(fileobj, self=False): """Fix all CRCs in the Header. Return True if anything changed, False if it didn't.""" # technically, if we've changed anything in the previous functions, # the header CRC must almost definitely change. just in case, though.. ret = NdsHeader.fixLogoCRC(fileobj, self) or False ret = NdsHeader.fixSecureAreaCRC(fileobj, self) or ret ret = NdsHeader.fixHeaderCRC(fileobj, self) or ret return ret # CRC finding functions @staticmethod def getSecurityDataCRC(fileobj, self=False): """Get the Security Data CRC. Note that 'self' is not used.""" fileobj.seek(0x1001) data = fileobj.read(0x2000) return cksum.crc16(data) @staticmethod def getSecureAreaCRC(fileobj, self=False): """Get the Secure Area CRC.""" fileobj.seek(0x4000) info = fileobj.read(0x4000) game_code = _of_resolve(self, fileobj, 'game_code') if self: rom_type = self.rom_type else: rom_type = NdsHeader.getRomType(fileobj) if rom_type == _NDSDUMPED: info = ndsencrypt.encrypt_arm9(game_code, info) return cksum.crc16(info) @staticmethod def getSegment3CRC(fileobj, self=False): """This uses ccitt instead of crc; Not sure why yet.""" pass @staticmethod def getBannerData(fileobj, self=False): length = struct.calcsize(_banner_ups) banner_offset = _of_resolve(self, fileobj, 'banner_offset') fileobj.seek(int(banner_offset)) if not self: return fileobj.read(length), banner_offset return fileobj.read(length) # this is mostly the DetectRomType() function from ndstool @staticmethod def getRomType(fileobj, self=False): a9off = _of_resolve(self, fileobj, 'arm9_rom_offset') if a9off < 0x4000: return _HOMEBREW fileobj.seek(0x4000) info = fileobj.read(12) parts = struct.unpack(_romtype, info) if parts[0] == 0x00000000 and parts[1] == 0x00000000: return _MULTIBOOT if parts[0] == 0xe7ffdeff and parts[1] == 0xe7ffdeff: return _NDSDUMPED # there might be something wrong... fileobj.seek(0x200) info = fileobj.read(0x4000 - 0x200) if [i for i in info if ord(i) != 0]: return _MASKROM return _ENCRSECURE def ndstool_info(self): s = self.info().split('\n') for idx,val in enumerate(ndscodes.header_info_pos): s[idx] = ('0x%02X' % val).ljust(8) + s[idx] for idx,line in enumerate(s): if line.startswith('Banner CRC'): spl = line.split() title = "%s %s" % (spl[0], spl[1]) s[idx] = title.ljust(40) + ' '.join(spl[2:]) return 'Header Information:\n' + "\n".join(s) def _info_makeline(self, format, attr, *attrs, **kwargs): if kwargs.has_key('lj'): lj = kwargs['lj'] else: lj = 32 a = header_fields[attr] rattrs = [getattr(self, attr)] for item in attrs: rattrs.append(getattr(self, str(item), item)) try:return a[DISPLAYNAME].ljust(32) + format % tuple(rattrs) except: return a[DISPLAYNAME].ljust(32) + str(format) def info(self): s = '' lj = 32 _ = lambda x: x.ljust(lj) s += self._info_makeline('%s\n', 'game_name') s += self._info_makeline('%s (NTR-%s-%s)\n', 'game_code', 'game_code', 'country') s += self._info_makeline('%s (%s)\n', 'maker_code', 'maker') s += self._info_makeline('0x%02X\n', 'unit_code') s += self._info_makeline('0x%02X\n', 'device_type') s += self._info_makeline('0x%02X (%d Mbit)\n', 'device_capacity', 1< 0xff: s2 += hex(c) + ', ' if c == 0x00: s += '\n' break elif c == 0x0A: nextline = True else: if nextline: if line != 1: s += '\n' s += '%s banner text, line %d:' % (self.langs[lang], line) s += ' ' * (19 - len(self.langs[lang])) nextline = False line += 1 s += unichr(c) #if s2: print s2 return s def calcCRC(self): return cksum.crc16(self.data[32:]) if __name__ == "__main__": import sys if len(sys.argv) < 2: print 'usage: python ndsheader.py ' sys.exit(0) fn = sys.argv[1] ndsfile = open(fn, 'rb') ndshdr = NdsHeader(ndsfile) #print ndshdr.info() print ndshdr.ndstool_info()