diff --git a/.gitignore b/.gitignore index 4c2ceec..62b9984 100644 --- a/.gitignore +++ b/.gitignore @@ -162,4 +162,5 @@ cython_debug/ .idea/ # Allows me to keep TMD files in my repository folder for testing without accidentally publishing them -*.tmd \ No newline at end of file +*.tmd +*.wad \ No newline at end of file diff --git a/src/libWiiPy/commonkeys.py b/src/libWiiPy/commonkeys.py index 6b8687e..f6eee50 100644 --- a/src/libWiiPy/commonkeys.py +++ b/src/libWiiPy/commonkeys.py @@ -1,4 +1,5 @@ -# "commonkeys.py" from libWiiPy by NinjaCheetah +# "commonkeys.py" from libWiiPy by NinjaCheetah & Contributors +# https://github.com/NinjaCheetah/libWiiPy default_key = 'ebe42a225e8593e448d9c5457381aaf7' korean_key = '63b82bb4f4614e2e13f2fefbba4c9b7e' diff --git a/src/libWiiPy/tmd.py b/src/libWiiPy/tmd.py index fc3e22c..31155b0 100644 --- a/src/libWiiPy/tmd.py +++ b/src/libWiiPy/tmd.py @@ -1,6 +1,9 @@ -# "tmd.py" from libWiiPy by NinjaCheetah +# "tmd.py" from libWiiPy by NinjaCheetah & Contributors +# https://github.com/NinjaCheetah/libWiiPy +# # See https://wiibrew.org/wiki/Title_metadata for details about the TMD format +import io import binascii from dataclasses import dataclass from typing import List @@ -17,7 +20,7 @@ class ContentRecord: class TMD: - """Creates a TMD object that can be used to read all the data contained in a TMD.""" + """Creates a TMD object to parse a TMD file to retrieve information about a title.""" def __init__(self, tmd): self.tmd = tmd self.sig_type: int @@ -40,64 +43,64 @@ class TMD: self.boot_index: int self.content_record: List[ContentRecord] # Load data from TMD file - with open(tmd, "rb") as tmdfile: + with io.BytesIO(tmd) as tmddata: # Signing certificate issuer - tmdfile.seek(0x140) - self.issuer = tmdfile.read(64) + tmddata.seek(0x140) + self.issuer = tmddata.read(64) # TMD version, seems to usually be 0, but I've seen references to other numbers - tmdfile.seek(0x180) - self.version = int.from_bytes(tmdfile.read(1)) + tmddata.seek(0x180) + self.version = int.from_bytes(tmddata.read(1)) # TODO: label - tmdfile.seek(0x181) - self.ca_crl_version = tmdfile.read(1) + tmddata.seek(0x181) + self.ca_crl_version = tmddata.read(1) # TODO: label - tmdfile.seek(0x182) - self.signer_crl_version = tmdfile.read(1) + tmddata.seek(0x182) + self.signer_crl_version = tmddata.read(1) # If this is a vWii title or not - tmdfile.seek(0x183) - self.vwii = int.from_bytes(tmdfile.read(1)) + tmddata.seek(0x183) + self.vwii = int.from_bytes(tmddata.read(1)) # TID of the IOS to use for the title, set to 0 if this title is the IOS, set to boot2 version if boot2 - tmdfile.seek(0x184) - ios_version_bin = tmdfile.read(8) + tmddata.seek(0x184) + ios_version_bin = tmddata.read(8) ios_version_hex = binascii.hexlify(ios_version_bin) self.ios_tid = str(ios_version_hex.decode()) # Get IOS version based on TID self.ios_version = int(self.ios_tid[-2:], 16) # Title ID of the title - tmdfile.seek(0x18C) - title_id_bin = tmdfile.read(8) + tmddata.seek(0x18C) + title_id_bin = tmddata.read(8) title_id_hex = binascii.hexlify(title_id_bin) self.title_id = str(title_id_hex.decode()) # Type of content - tmdfile.seek(0x194) - content_type_bin = tmdfile.read(4) + tmddata.seek(0x194) + content_type_bin = tmddata.read(4) content_type_hex = binascii.hexlify(content_type_bin) self.content_type = str(content_type_hex.decode()) # Publisher of the title - tmdfile.seek(0x198) - self.group_id = tmdfile.read(2) + tmddata.seek(0x198) + self.group_id = tmddata.read(2) # Region of the title, 0 = JAP, 1 = USA, 2 = EUR, 3 = NONE, 4 = KOR - tmdfile.seek(0x19C) - region_hex = tmdfile.read(2) + tmddata.seek(0x19C) + region_hex = tmddata.read(2) self.region = int.from_bytes(region_hex) # TODO: figure this one out - tmdfile.seek(0x19E) - self.ratings = tmdfile.read(16) + tmddata.seek(0x19E) + self.ratings = tmddata.read(16) # Access rights of the title; DVD-video access and AHBPROT - tmdfile.seek(0x1D8) - self.access_rights = tmdfile.read(4) + tmddata.seek(0x1D8) + self.access_rights = tmddata.read(4) # Calculate the version number by multiplying 0x1DC by 256 and adding 0x1DD - tmdfile.seek(0x1DC) - title_version_high = int.from_bytes(tmdfile.read(1)) * 256 - tmdfile.seek(0x1DD) - title_version_low = int.from_bytes(tmdfile.read(1)) + tmddata.seek(0x1DC) + title_version_high = int.from_bytes(tmddata.read(1)) * 256 + tmddata.seek(0x1DD) + title_version_low = int.from_bytes(tmddata.read(1)) self.title_version = title_version_high + title_version_low # The number of contents listed in the TMD - tmdfile.seek(0x1DE) - self.num_contents = int.from_bytes(tmdfile.read(2)) + tmddata.seek(0x1DE) + self.num_contents = int.from_bytes(tmddata.read(2)) # Content index in content list that contains the boot file - tmdfile.seek(0x1E0) - self.boot_index = tmdfile.read(2) + tmddata.seek(0x1E0) + self.boot_index = tmddata.read(2) def get_title_id(self): """Returns the TID of the TMD's associated title.""" diff --git a/src/libWiiPy/wad.py b/src/libWiiPy/wad.py new file mode 100644 index 0000000..19b52d8 --- /dev/null +++ b/src/libWiiPy/wad.py @@ -0,0 +1,132 @@ +# "wad.py" from libWiiPy by NinjaCheetah & Contributors +# https://github.com/NinjaCheetah/libWiiPy +# +# See https://wiibrew.org/wiki/WAD_files for details about the WAD format + +import io +import binascii + + +class WAD: + """Creates a WAD object to parse the header of a WAD file and retrieve the data contained in it.""" + def __init__(self, wad): + self.wad = wad + self.wad_hdr_size: int + self.wad_type: str + self.wad_version: int + # === Sizes === + self.wad_cert_size: int + self.wad_crl_size: int + self.wad_tik_size: int + self.wad_tmd_size: int + # This is the size of the content region, which contains all app files combined. + self.wad_content_size: int + self.wad_meta_size: int + # === Offsets === + self.wad_cert_offset: int + self.wad_crl_offset: int + self.wad_tik_offset: int + self.wad_tmd_offset: int + self.wad_content_offset: int + self.wad_meta_offset: int + # Load header data from WAD stream + with io.BytesIO(wad) as waddata: + # ==================================================================================== + # Get the sizes of each data region contained within the WAD. + # ==================================================================================== + # Header length, which will always be 64 bytes, as it is padded out if it is shorter. + self.wad_hdr_size = 64 + # WAD type, denoting whether this WAD contains boot2 ("ib"), or anything else ("Is"). + waddata.seek(0x04) + self.wad_type = str(waddata.read(2).decode()) + # WAD version, this is always 0. + waddata.seek(0x06) + self.wad_version = waddata.read(2) + # WAD cert size. + waddata.seek(0x08) + self.wad_cert_size = int(binascii.hexlify(waddata.read(4)), 16) + # WAD crl size. + waddata.seek(0x0c) + self.wad_crl_size = int(binascii.hexlify(waddata.read(4)), 16) + # WAD ticket size. + waddata.seek(0x10) + self.wad_tik_size = int(binascii.hexlify(waddata.read(4)), 16) + # WAD TMD size. + waddata.seek(0x14) + self.wad_tmd_size = int(binascii.hexlify(waddata.read(4)), 16) + # WAD content size. + waddata.seek(0x18) + self.wad_content_size = int(binascii.hexlify(waddata.read(4)), 16) + # Publisher of the title contained in the WAD. + waddata.seek(0x1c) + self.wad_meta_size = int(binascii.hexlify(waddata.read(4)), 16) + # ==================================================================================== + # Calculate file offsets from sizes. Every section of the WAD is padded out to a multiple of 0x40. + # ==================================================================================== + self.wad_cert_offset = self.wad_hdr_size + # crl isn't ever used, however an entry for its size exists in the header, so its calculated just in case. + self.wad_crl_offset = int(64 * round((self.wad_cert_offset + self.wad_cert_size) / 64)) + self.wad_tik_offset = int(64 * round((self.wad_crl_offset + self.wad_crl_size) / 64)) + self.wad_tmd_offset = int(64 * round((self.wad_tik_offset + self.wad_tik_size) / 64)) + self.wad_content_offset = int(64 * round((self.wad_tmd_offset + self.wad_tmd_size) / 64)) + # meta is also never used, but Nintendo's tools calculate it so we should too. + self.wad_meta_offset = int(64 * round((self.wad_content_offset + self.wad_content_size) / 64)) + + def get_cert_region(self): + """Returns the offset and size for the cert data.""" + return self.wad_cert_offset, self.wad_cert_size + + def get_crl_region(self): + """Returns the offset and size for the crl data.""" + return self.wad_crl_offset, self.wad_crl_size + + def get_ticket_region(self): + """Returns the offset and size for the ticket data.""" + return self.wad_tik_offset, self.wad_tik_size + + def get_tmd_region(self): + """Returns the offset and size for the TMD data.""" + return self.wad_tmd_offset, self.wad_tmd_size + + def get_content_region(self): + """Returns the offset and size for the content of the WAD.""" + return self.wad_content_offset, self.wad_tmd_size + + def get_wad_type(self): + """Returns the type of the WAD. This is 'Is' unless the WAD contains boot2 where it is 'ib'.""" + return self.wad_type + + def get_cert_data(self): + """Returns the certificate data from the WAD.""" + waddata = io.BytesIO(self.wad) + waddata.seek(self.wad_cert_offset) + cert_data = waddata.read(self.wad_cert_size) + return cert_data + + def get_crl_data(self): + """Returns the crl data from the WAD, if it exists.""" + waddata = io.BytesIO(self.wad) + waddata.seek(self.wad_crl_offset) + crl_data = waddata.read(self.wad_crl_size) + return crl_data + + def get_ticket_data(self): + """Returns the ticket data from the WAD.""" + waddata = io.BytesIO(self.wad) + waddata.seek(self.wad_tik_offset) + ticket_data = waddata.read(self.wad_tik_size) + return ticket_data + + def get_tmd_data(self): + """Returns the TMD data from the WAD.""" + waddata = io.BytesIO(self.wad) + waddata.seek(self.wad_tmd_offset) + tmd_data = waddata.read(self.wad_tmd_size) + return tmd_data + + def get_content_data(self): + """Returns the content of the WAD.""" + waddata = io.BytesIO(self.wad) + waddata.seek(self.wad_content_offset) + content_data = waddata.read(self.wad_content_size) + return content_data