Merge pull request #4 from NinjaCheetah/breakdown_wad

Add wad.py, which handles the data contained in a WAD file
This commit is contained in:
Campbell 2024-02-28 23:44:37 -05:00 committed by GitHub
commit 2bacc474f4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 174 additions and 37 deletions

1
.gitignore vendored
View File

@ -163,3 +163,4 @@ cython_debug/
# Allows me to keep TMD files in my repository folder for testing without accidentally publishing them # Allows me to keep TMD files in my repository folder for testing without accidentally publishing them
*.tmd *.tmd
*.wad

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,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 # 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
@ -17,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
@ -40,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."""

132
src/libWiiPy/wad.py Normal file
View File

@ -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