Made wad.py able to export all data contained in a WAD

Also adjusted wad.py and tmd.py to both use byte streams so that the method of loading the WAD or TMD data is handled outside of the library.
Comments are also more robust overall.
This commit is contained in:
Campbell 2024-02-28 23:38:45 -05:00
parent 66e83ff3f8
commit d7e45bb2a4
3 changed files with 135 additions and 105 deletions

View File

@ -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' default_key = 'ebe42a225e8593e448d9c5457381aaf7'
korean_key = '63b82bb4f4614e2e13f2fefbba4c9b7e' korean_key = '63b82bb4f4614e2e13f2fefbba4c9b7e'

View File

@ -1,7 +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 # See https://wiibrew.org/wiki/Title_metadata for details about the TMD format
import io
import binascii import binascii
from dataclasses import dataclass from dataclasses import dataclass
from typing import List from typing import List
@ -18,7 +20,7 @@ class ContentRecord:
class TMD: 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): def __init__(self, tmd):
self.tmd = tmd self.tmd = tmd
self.sig_type: int self.sig_type: int
@ -41,64 +43,64 @@ class TMD:
self.boot_index: int self.boot_index: int
self.content_record: List[ContentRecord] self.content_record: List[ContentRecord]
# Load data from TMD file # Load data from TMD file
with open(tmd, "rb") as tmdfile: with io.BytesIO(tmd) as tmddata:
# Signing certificate issuer # Signing certificate issuer
tmdfile.seek(0x140) tmddata.seek(0x140)
self.issuer = tmdfile.read(64) self.issuer = tmddata.read(64)
# TMD version, seems to usually be 0, but I've seen references to other numbers # TMD version, seems to usually be 0, but I've seen references to other numbers
tmdfile.seek(0x180) tmddata.seek(0x180)
self.version = int.from_bytes(tmdfile.read(1)) self.version = int.from_bytes(tmddata.read(1))
# TODO: label # TODO: label
tmdfile.seek(0x181) tmddata.seek(0x181)
self.ca_crl_version = tmdfile.read(1) self.ca_crl_version = tmddata.read(1)
# TODO: label # TODO: label
tmdfile.seek(0x182) tmddata.seek(0x182)
self.signer_crl_version = tmdfile.read(1) self.signer_crl_version = tmddata.read(1)
# If this is a vWii title or not # If this is a vWii title or not
tmdfile.seek(0x183) tmddata.seek(0x183)
self.vwii = int.from_bytes(tmdfile.read(1)) 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 # 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) tmddata.seek(0x184)
ios_version_bin = tmdfile.read(8) ios_version_bin = tmddata.read(8)
ios_version_hex = binascii.hexlify(ios_version_bin) ios_version_hex = binascii.hexlify(ios_version_bin)
self.ios_tid = str(ios_version_hex.decode()) self.ios_tid = str(ios_version_hex.decode())
# Get IOS version based on TID # Get IOS version based on TID
self.ios_version = int(self.ios_tid[-2:], 16) self.ios_version = int(self.ios_tid[-2:], 16)
# Title ID of the title # Title ID of the title
tmdfile.seek(0x18C) tmddata.seek(0x18C)
title_id_bin = tmdfile.read(8) title_id_bin = tmddata.read(8)
title_id_hex = binascii.hexlify(title_id_bin) title_id_hex = binascii.hexlify(title_id_bin)
self.title_id = str(title_id_hex.decode()) self.title_id = str(title_id_hex.decode())
# Type of content # Type of content
tmdfile.seek(0x194) tmddata.seek(0x194)
content_type_bin = tmdfile.read(4) content_type_bin = tmddata.read(4)
content_type_hex = binascii.hexlify(content_type_bin) content_type_hex = binascii.hexlify(content_type_bin)
self.content_type = str(content_type_hex.decode()) self.content_type = str(content_type_hex.decode())
# Publisher of the title # Publisher of the title
tmdfile.seek(0x198) tmddata.seek(0x198)
self.group_id = tmdfile.read(2) self.group_id = tmddata.read(2)
# Region of the title, 0 = JAP, 1 = USA, 2 = EUR, 3 = NONE, 4 = KOR # Region of the title, 0 = JAP, 1 = USA, 2 = EUR, 3 = NONE, 4 = KOR
tmdfile.seek(0x19C) tmddata.seek(0x19C)
region_hex = tmdfile.read(2) region_hex = tmddata.read(2)
self.region = int.from_bytes(region_hex) self.region = int.from_bytes(region_hex)
# TODO: figure this one out # TODO: figure this one out
tmdfile.seek(0x19E) tmddata.seek(0x19E)
self.ratings = tmdfile.read(16) self.ratings = tmddata.read(16)
# Access rights of the title; DVD-video access and AHBPROT # Access rights of the title; DVD-video access and AHBPROT
tmdfile.seek(0x1D8) tmddata.seek(0x1D8)
self.access_rights = tmdfile.read(4) self.access_rights = tmddata.read(4)
# Calculate the version number by multiplying 0x1DC by 256 and adding 0x1DD # Calculate the version number by multiplying 0x1DC by 256 and adding 0x1DD
tmdfile.seek(0x1DC) tmddata.seek(0x1DC)
title_version_high = int.from_bytes(tmdfile.read(1)) * 256 title_version_high = int.from_bytes(tmddata.read(1)) * 256
tmdfile.seek(0x1DD) tmddata.seek(0x1DD)
title_version_low = int.from_bytes(tmdfile.read(1)) title_version_low = int.from_bytes(tmddata.read(1))
self.title_version = title_version_high + title_version_low self.title_version = title_version_high + title_version_low
# The number of contents listed in the TMD # The number of contents listed in the TMD
tmdfile.seek(0x1DE) tmddata.seek(0x1DE)
self.num_contents = int.from_bytes(tmdfile.read(2)) self.num_contents = int.from_bytes(tmddata.read(2))
# Content index in content list that contains the boot file # Content index in content list that contains the boot file
tmdfile.seek(0x1E0) tmddata.seek(0x1E0)
self.boot_index = tmdfile.read(2) self.boot_index = tmddata.read(2)
def get_title_id(self): def get_title_id(self):
"""Returns the TID of the TMD's associated title.""" """Returns the TID of the TMD's associated title."""

View File

@ -1,105 +1,132 @@
# Project: libWiiPy by NinjaCheetah # "wad.py" from libWiiPy by NinjaCheetah & Contributors
# File: wad.py by rmc # https://github.com/NinjaCheetah/libWiiPy
# #
# See https://wiibrew.org/wiki/WAD_files for details about the WAD format # See https://wiibrew.org/wiki/WAD_files for details about the WAD format
import io
import binascii import binascii
from dataclasses import dataclass
from typing import List
@dataclass class WAD:
class ContentRecord: """Creates a WAD object to parse the header of a WAD file and retrieve the data contained in it."""
"""Creates a content record object that contains the details of a content contained in a title."""
cid: int # Content ID
index: int # Index in the list of contents
content_type: int # normal: 0x0001; dlc: 0x4001; shared: 0x8001
content_size: int
content_hash: bytearray # SHA1 hash content
class wadHeader:
"""Break down 32 byte WAD header."""
def __init__(self, wad): def __init__(self, wad):
self.wad = wad self.wad = wad
self.wad_hdr_size: int self.wad_hdr_size: int
self.wad_type: str self.wad_type: str
self.wad_version: int self.wad_version: int
# Sizes # === Sizes ===
self.wad_cert_size: int self.wad_cert_size: int
self.wad_crl_size: int self.wad_crl_size: int
self.wad_tik_size: int self.wad_tik_size: int
self.wad_tmd_size: int self.wad_tmd_size: int
self.wad_app_size: int # Note that this is the size of the app region. This is each individual app file bundled together. # This is the size of the content region, which contains all app files combined.
self.wad_content_size: int
self.wad_meta_size: int self.wad_meta_size: int
# Offsets # === Offsets ===
self.wad_cert_offset: int self.wad_cert_offset: int
self.wad_crl_offset: int self.wad_crl_offset: int
self.wad_tik_offset: int self.wad_tik_offset: int
self.wad_tmd_offset: int self.wad_tmd_offset: int
self.wad_app_offset: int self.wad_content_offset: int
self.wad_meta_offset: int self.wad_meta_offset: int
#self.content_record: List[ContentRecord] # Load header data from WAD stream
# Load header data from WAD file with io.BytesIO(wad) as waddata:
with open(wad, "rb") as wadfile: # ====================================================================================
#==================================================================================== # Get the sizes of each data region contained within the WAD.
# Get the sizes of each data region contained within the WAD. Sorry for mid code! # ====================================================================================
#==================================================================================== # Header length, which will always be 64 bytes, as it is padded out if it is shorter.
# Header length. Always seems to be 32 so we'll ignore it for now. self.wad_hdr_size = 64
wadfile.seek(0x0) # WAD type, denoting whether this WAD contains boot2 ("ib"), or anything else ("Is").
self.wad_hdr_size = wadfile.read(4) waddata.seek(0x04)
# WAD type self.wad_type = str(waddata.read(2).decode())
wadfile.seek(0x04) # WAD version, this is always 0.
self.wad_type = str(wadfile.read(2).decode()) waddata.seek(0x06)
# WAD version self.wad_version = waddata.read(2)
wadfile.seek(0x06) # WAD cert size.
self.wad_version = wadfile.read(2) waddata.seek(0x08)
# WAD cert size self.wad_cert_size = int(binascii.hexlify(waddata.read(4)), 16)
wadfile.seek(0x08) # WAD crl size.
self.wad_cert_size = wadfile.read(4) waddata.seek(0x0c)
# WAD crl size self.wad_crl_size = int(binascii.hexlify(waddata.read(4)), 16)
wadfile.seek(0x0c) # WAD ticket size.
self.wad_crl_size = wadfile.read(4) waddata.seek(0x10)
# WAD ticket size self.wad_tik_size = int(binascii.hexlify(waddata.read(4)), 16)
wadfile.seek(0x10) # WAD TMD size.
self.wad_tik_size = wadfile.read(4) waddata.seek(0x14)
# WAD TMD size self.wad_tmd_size = int(binascii.hexlify(waddata.read(4)), 16)
wadfile.seek(0x14) # WAD content size.
self.wad_tmd_size = wadfile.read(4) waddata.seek(0x18)
# WAD app size self.wad_content_size = int(binascii.hexlify(waddata.read(4)), 16)
wadfile.seek(0x18) # Publisher of the title contained in the WAD.
self.wad_app_size = wadfile.read(4) waddata.seek(0x1c)
# Publisher of the title self.wad_meta_size = int(binascii.hexlify(waddata.read(4)), 16)
wadfile.seek(0x1c) # ====================================================================================
self.wad_meta_size = wadfile.read(4) # Calculate file offsets from sizes. Every section of the WAD is padded out to a multiple of 0x40.
#==================================================================================== # ====================================================================================
# Calculate file offsets from sizes
#====================================================================================
self.wad_cert_offset = self.wad_hdr_size self.wad_cert_offset = self.wad_hdr_size
# I've never seen crl used (don't even know what it's for) but still calculating in case... # 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 = self.wad_cert_offset + self.wad_cert_size self.wad_crl_offset = int(64 * round((self.wad_cert_offset + self.wad_cert_size) / 64))
self.wad_tik_offset = self.wad_crl_offset + self.wad_crl_size self.wad_tik_offset = int(64 * round((self.wad_crl_offset + self.wad_crl_size) / 64))
self.wad_tmd_offset = self.wad_tik_offset + self.wad_tik_size self.wad_tmd_offset = int(64 * round((self.wad_tik_offset + self.wad_tik_size) / 64))
self.wad_app_offset = self.wad_tmd_offset + self.wad_tmd_size self.wad_content_offset = int(64 * round((self.wad_tmd_offset + self.wad_tmd_size) / 64))
# Same with meta. If private Nintendo tools calculate these then maaaaaybe we should too. # meta is also never used, but Nintendo's tools calculate it so we should too.
self.wad_meta_offset = self.wad_app_offset + self.wad_app_size self.wad_meta_offset = int(64 * round((self.wad_content_offset + self.wad_content_size) / 64))
def get_cert_region(self): def get_cert_region(self):
"""Returns the offset and size for the cert.""" """Returns the offset and size for the cert data."""
return self.wad_cert_offset, self.wad_cert_size 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): def get_ticket_region(self):
"""Returns the offset and size for the ticket.""" """Returns the offset and size for the ticket data."""
return self.wad_tik_offset, self.wad_tik_size return self.wad_tik_offset, self.wad_tik_size
def get_tmd_region(self): def get_tmd_region(self):
"""Returns the offset and size for the TMD.""" """Returns the offset and size for the TMD data."""
return self.wad_tmd_offset, self.wad_tmd_size return self.wad_tmd_offset, self.wad_tmd_size
def get_app_region(self): def get_content_region(self):
"""Returns the offset and size for the app.""" """Returns the offset and size for the content of the WAD."""
return self.wad_app_offset, self.wad_tmd_size return self.wad_content_offset, self.wad_tmd_size
def get_wad_type(self): def get_wad_type(self):
"""Returns the type of the WAD. This is 'Is' unless the WAD contains boot2 where it is 'ib'.""" """Returns the type of the WAD. This is 'Is' unless the WAD contains boot2 where it is 'ib'."""
return self.wad_type 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