Beginning libWiiPy refactors

No major functional changes have been made so far other than a couple of things being shifted between files, but a lot of bad code has been rewritten to hopefully make libWiiPy maintainable into the future.
This commit is contained in:
Campbell 2025-12-16 23:14:33 -05:00
parent ce5d118de1
commit 374358711b
Signed by: NinjaCheetah
GPG Key ID: B547958AF96ED344
19 changed files with 262 additions and 203 deletions

View File

@ -16,8 +16,9 @@ The `libWiiPy.title` package contains modules for interacting with Wii titles. T
| [libWiiPy.title.ticket](/title/ticket) | Provides support for parsing and editing Tickets used for content decryption | | [libWiiPy.title.ticket](/title/ticket) | Provides support for parsing and editing Tickets used for content decryption |
| [libWiiPy.title.title](/title/title.title) | Provides high-level support for parsing and editing an entire title with the context of each component | | [libWiiPy.title.title](/title/title.title) | Provides high-level support for parsing and editing an entire title with the context of each component |
| [libWiiPy.title.tmd](/title/tmd) | Provides support for parsing and editing TMDs (Title Metadata) | | [libWiiPy.title.tmd](/title/tmd) | Provides support for parsing and editing TMDs (Title Metadata) |
| [libWiiPy.title.util](/title/util) | Provides some simple utility functions relating to titles |
| [libWiiPy.title.wad](/title/wad) | Provides support for parsing and editing WAD files, allowing you to load each component into the other available classes | | [libWiiPy.title.wad](/title/wad) | Provides support for parsing and editing WAD files, allowing you to load each component into the other available classes |
| [libWiiPy.title.types](/title/types) | Provides shared types used across the title module. |
| [libWiiPy.title.versions](/title/versions) | Provides utility functions for converting the format that a title's version is in. |
## Full Package Contents ## Full Package Contents
@ -33,6 +34,7 @@ The `libWiiPy.title` package contains modules for interacting with Wii titles. T
/title/ticket /title/ticket
/title/title.title /title/title.title
/title/tmd /title/tmd
/title/util
/title/wad /title/wad
/title/types
/title/versions
``` ```

View File

@ -0,0 +1,14 @@
# libWiiPy.title.types Module
## Description
The `libWiiPy.title.types` module provides shared types used across the title module.
## Module Contents
```{eval-rst}
.. automodule:: libWiiPy.title.types
:members:
:undoc-members:
:show-inheritance:
```

View File

@ -1,14 +0,0 @@
# libWiiPy.title.util Module
## Description
The `libWiiPy.title.util` module provides common utility functions internally. It is not designed to be used directly.
## Module Contents
```{eval-rst}
.. automodule:: libWiiPy.title.util
:members:
:undoc-members:
:show-inheritance:
```

View File

@ -0,0 +1,14 @@
# libWiiPy.title.versions Module
## Description
The `libWiiPy.title.versions` module provides functions for converting the format that a title's version is in.
## Module Contents
```{eval-rst}
.. automodule:: libWiiPy.title.versions
:members:
:undoc-members:
:show-inheritance:
```

65
src/libWiiPy/constants.py Normal file
View File

@ -0,0 +1,65 @@
# "constants.py" from libWiiPy by NinjaCheetah & Contributors
# https://github.com/NinjaCheetah/libWiiPy
#
# This file defines constant values referenced across the library.
_WII_MENU_VERSIONS = {
"Prelaunch": [0, 1, 2],
"1.0J": 64,
"1.0U": 33,
"1.0E": 34,
"2.0J": 128,
"2.0U": 97,
"2.0E": 130,
"2.1E": 162,
"2.2J": 192,
"2.2U": 193,
"2.2E": 194,
"3.0J": 224,
"3.0U": 225,
"3.0E": 226,
"3.1J": 256,
"3.1U": 257,
"3.1E": 258,
"3.2J": 288,
"3.2U": 289,
"3.2E": 290,
"3.3J": 352,
"3.3U": 353,
"3.3E": 354,
"3.3K": 326,
"3.4J": 384,
"3.4U": 385,
"3.4E": 386,
"3.5K": 390,
"4.0J": 416,
"4.0U": 417,
"4.0E": 418,
"4.1J": 448,
"4.1U": 449,
"4.1E": 450,
"4.1K": 454,
"4.2J": 480,
"4.2U": 481,
"4.2E": 482,
"4.2K": 486,
"4.3J": 512,
"4.3U": 513,
"4.3E": 514,
"4.3K": 518,
"4.3U-Mini": 4609,
"4.3E-Mini": 4610
}
_VWII_MENU_VERSIONS = {
"vWii-1.0.0J": 512,
"vWii-1.0.0U": 513,
"vWii-1.0.0E": 514,
"vWii-4.0.0J": 544,
"vWii-4.0.0U": 545,
"vWii-4.0.0E": 546,
"vWii-5.2.0J": 608,
"vWii-5.2.0U": 609,
"vWii-5.2.0E": 610,
}

View File

@ -8,6 +8,7 @@ import pathlib
import shutil import shutil
from dataclasses import dataclass as _dataclass from dataclasses import dataclass as _dataclass
from typing import Callable, List from typing import Callable, List
from ..title.ticket import Ticket from ..title.ticket import Ticket
from ..title.title import Title from ..title.title import Title
from ..title.tmd import TMD from ..title.tmd import TMD

View File

@ -8,7 +8,7 @@ from typing import List
from ..shared import _pad_bytes from ..shared import _pad_bytes
_key = 0x73B5DBFA _KEY = 0x73B5DBFA
class SettingTxt: class SettingTxt:
""" """
@ -53,11 +53,11 @@ class SettingTxt:
The data of an encrypted setting.txt file. The data of an encrypted setting.txt file.
""" """
with io.BytesIO(setting_txt) as setting_data: with io.BytesIO(setting_txt) as setting_data:
global _key # I still don't actually know what *kind* of encryption this is. global _KEY # I still don't actually know what *kind* of encryption this is.
setting_txt_dec: List[int] = [] setting_txt_dec: List[int] = []
for i in range(0, 256): for i in range(0, 256):
setting_txt_dec.append(int.from_bytes(setting_data.read(1)) ^ (_key & 0xff)) setting_txt_dec.append(int.from_bytes(setting_data.read(1)) ^ (_KEY & 0xff))
_key = (_key << 1) | (_key >> 31) _KEY = (_KEY << 1) | (_KEY >> 31)
setting_txt_bytes = bytes(setting_txt_dec) setting_txt_bytes = bytes(setting_txt_dec)
try: try:
setting_str = setting_txt_bytes.decode('utf-8') setting_str = setting_txt_bytes.decode('utf-8')
@ -103,13 +103,13 @@ class SettingTxt:
""" """
setting_str = self.dump_decrypted() setting_str = self.dump_decrypted()
setting_txt_dec = setting_str.encode() setting_txt_dec = setting_str.encode()
global _key global _KEY
# This could probably be made more efficient somehow. # This could probably be made more efficient somehow.
setting_txt_enc: List[int] = [] setting_txt_enc: List[int] = []
with io.BytesIO(setting_txt_dec) as setting_data: with io.BytesIO(setting_txt_dec) as setting_data:
for i in range(0, len(setting_txt_dec)): for i in range(0, len(setting_txt_dec)):
setting_txt_enc.append(int.from_bytes(setting_data.read(1)) ^ (_key & 0xff)) setting_txt_enc.append(int.from_bytes(setting_data.read(1)) ^ (_KEY & 0xff))
_key = (_key << 1) | (_key >> 31) _KEY = (_KEY << 1) | (_KEY >> 31)
setting_txt_bytes = _pad_bytes(bytes(setting_txt_enc), 256) setting_txt_bytes = _pad_bytes(bytes(setting_txt_enc), 256)
return setting_txt_bytes return setting_txt_bytes

View File

@ -47,69 +47,3 @@ def _pad_bytes(data, alignment=64) -> bytes:
while (len(data) % alignment) != 0: while (len(data) % alignment) != 0:
data += b'\x00' data += b'\x00'
return data return data
def _bitmask(x: int) -> int:
return 1 << x
_wii_menu_versions = {
"Prelaunch": [0, 1, 2],
"1.0J": 64,
"1.0U": 33,
"1.0E": 34,
"2.0J": 128,
"2.0U": 97,
"2.0E": 130,
"2.1E": 162,
"2.2J": 192,
"2.2U": 193,
"2.2E": 194,
"3.0J": 224,
"3.0U": 225,
"3.0E": 226,
"3.1J": 256,
"3.1U": 257,
"3.1E": 258,
"3.2J": 288,
"3.2U": 289,
"3.2E": 290,
"3.3J": 352,
"3.3U": 353,
"3.3E": 354,
"3.3K": 326,
"3.4J": 384,
"3.4U": 385,
"3.4E": 386,
"3.5K": 390,
"4.0J": 416,
"4.0U": 417,
"4.0E": 418,
"4.1J": 448,
"4.1U": 449,
"4.1E": 450,
"4.1K": 454,
"4.2J": 480,
"4.2U": 481,
"4.2E": 482,
"4.2K": 486,
"4.3J": 512,
"4.3U": 513,
"4.3E": 514,
"4.3K": 518,
"4.3U-Mini": 4609,
"4.3E-Mini": 4610
}
_vwii_menu_versions = {
"vWii-1.0.0J": 512,
"vWii-1.0.0U": 513,
"vWii-1.0.0E": 514,
"vWii-4.0.0J": 544,
"vWii-4.0.0U": 545,
"vWii-4.0.0E": 546,
"vWii-5.2.0J": 608,
"vWii-5.2.0U": 609,
"vWii-5.2.0E": 610,
}

View File

@ -9,5 +9,6 @@ from .nus import *
from .ticket import * from .ticket import *
from .title import * from .title import *
from .tmd import * from .tmd import *
from .util import * from .types import *
from .versions import *
from .wad import * from .wad import *

View File

@ -5,33 +5,47 @@
import io import io
from enum import IntEnum as _IntEnum from enum import IntEnum as _IntEnum
from ..shared import _align_value, _pad_bytes
from .ticket import Ticket
from .tmd import TMD
from Crypto.Hash import SHA1 from Crypto.Hash import SHA1
from Crypto.PublicKey import RSA from Crypto.PublicKey import RSA
from Crypto.Signature import pkcs1_15 from Crypto.Signature import pkcs1_15
from ..shared import _align_value, _pad_bytes
from .ticket import Ticket
from .tmd import TMD
class CertificateType(_IntEnum): class CertificateType(_IntEnum):
"""
The type of a certificate.
"""
RSA_4096 = 0x00010000 RSA_4096 = 0x00010000
RSA_2048 = 0x00010001 RSA_2048 = 0x00010001
ECC = 0x00010002 ECC = 0x00010002
class CertificateSignatureLength(_IntEnum): class CertificateSignatureLength(_IntEnum):
"""
The length of a certificate's signature.
"""
RSA_4096 = 0x200 RSA_4096 = 0x200
RSA_2048 = 0x100 RSA_2048 = 0x100
ECC = 0x3C ECC = 0x3C
class CertificateKeyType(_IntEnum): class CertificateKeyType(_IntEnum):
"""
The type of key contained in a certificate.
"""
RSA_4096 = 0x00000000 RSA_4096 = 0x00000000
RSA_2048 = 0x00000001 RSA_2048 = 0x00000001
ECC = 0x00000002 ECC = 0x00000002
class CertificateKeyLength(_IntEnum): class CertificateKeyLength(_IntEnum):
"""
The length of the key contained in a certificate.
"""
RSA_4096 = 0x200 RSA_4096 = 0x200
RSA_2048 = 0x100 RSA_2048 = 0x100
ECC = 0x3C ECC = 0x3C

View File

@ -33,13 +33,12 @@ def get_common_key(common_key_index, dev=False) -> bytes:
match common_key_index: match common_key_index:
case 0: case 0:
if dev: if dev:
common_key_bin = binascii.unhexlify(development_key) return binascii.unhexlify(development_key)
else: else:
common_key_bin = binascii.unhexlify(common_key) return binascii.unhexlify(common_key)
case 1: case 1:
common_key_bin = binascii.unhexlify(korean_key) return binascii.unhexlify(korean_key)
case 2: case 2:
common_key_bin = binascii.unhexlify(vwii_key) return binascii.unhexlify(vwii_key)
case _: case _:
common_key_bin = binascii.unhexlify(common_key) return binascii.unhexlify(common_key)
return common_key_bin

View File

@ -9,7 +9,7 @@ import hashlib
from typing import List from typing import List
from dataclasses import dataclass as _dataclass from dataclasses import dataclass as _dataclass
from enum import IntEnum as _IntEnum from enum import IntEnum as _IntEnum
from ..types import _ContentRecord from .types import ContentRecord
from ..shared import _pad_bytes, _align_value from ..shared import _pad_bytes, _align_value
from .crypto import decrypt_content, encrypt_content from .crypto import decrypt_content, encrypt_content
@ -28,20 +28,20 @@ class ContentRegion:
Attributes Attributes
---------- ----------
content_records : List[_ContentRecord] content_records : List[ContentRecord]
The content records for the content stored in the region. The content records for the content stored in the region.
num_contents : int num_contents : int
The total number of contents stored in the region. The total number of contents stored in the region.
""" """
def __init__(self) -> None: def __init__(self) -> None:
self.content_records: List[_ContentRecord] = [] self.content_records: List[ContentRecord] = []
self.content_region_size: int = 0 # Size of the content region. self.content_region_size: int = 0 # Size of the content region.
self.num_contents: int = 0 # Number of contents in the content region. self.num_contents: int = 0 # Number of contents in the content region.
self.content_start_offsets: List[int] = [0] # The start offsets of each content in the content region. self.content_start_offsets: List[int] = [0] # The start offsets of each content in the content region.
self.content_list: List[bytes] = [] self.content_list: List[bytes] = []
def load(self, content_region: bytes, content_records: List[_ContentRecord]) -> None: def load(self, content_region: bytes, content_records: List[ContentRecord]) -> None:
""" """
Loads the raw content region and builds a list of all the contents. Loads the raw content region and builds a list of all the contents.
@ -49,7 +49,7 @@ class ContentRegion:
---------- ----------
content_region : bytes content_region : bytes
The raw data for the content region being loaded. The raw data for the content region being loaded.
content_records : list[_ContentRecord] content_records : list[ContentRecord]
A list of ContentRecord objects detailing all contents contained in the region. A list of ContentRecord objects detailing all contents contained in the region.
""" """
self.content_records = content_records self.content_records = content_records
@ -303,7 +303,7 @@ class ContentRegion:
raise ValueError("Content with an index of " + str(index) + " already exists!") raise ValueError("Content with an index of " + str(index) + " already exists!")
# If we're good, then append all the data and create a new ContentRecord(). # If we're good, then append all the data and create a new ContentRecord().
self.content_list.append(enc_content) self.content_list.append(enc_content)
self.content_records.append(_ContentRecord(cid, index, content_type, content_size, content_hash)) self.content_records.append(ContentRecord(cid, index, content_type, content_size, content_hash))
self.num_contents += 1 self.num_contents += 1
def add_content(self, dec_content: bytes, cid: int, content_type: int, title_key: bytes) -> None: def add_content(self, dec_content: bytes, cid: int, content_type: int, title_key: bytes) -> None:

View File

@ -10,7 +10,6 @@ from Crypto.Cipher import AES as _AES
def _convert_tid_to_iv(title_id: str | bytes) -> bytes: def _convert_tid_to_iv(title_id: str | bytes) -> bytes:
# Converts a Title ID in various formats into the format required to act as an IV. Private function used by other # Converts a Title ID in various formats into the format required to act as an IV. Private function used by other
# crypto functions. # crypto functions.
title_key_iv = b''
if type(title_id) is bytes: if type(title_id) is bytes:
# This catches the format b'0000000100000002' # This catches the format b'0000000100000002'
if len(title_id) == 16: if len(title_id) == 16:

View File

@ -302,9 +302,6 @@ def download_content(title_id: str, content_id: int, wiiu_endpoint: bool = False
libWiiPy.title.nus.DownloadCallback libWiiPy.title.nus.DownloadCallback
""" """
# Build the download URL. The structure is download/<TID>/<Content ID>. # Build the download URL. The structure is download/<TID>/<Content ID>.
content_id_hex = hex(content_id)[2:]
if len(content_id_hex) < 2:
content_id_hex = "0" + content_id_hex
if endpoint_override is not None: if endpoint_override is not None:
endpoint_url = _validate_endpoint(endpoint_override) endpoint_url = _validate_endpoint(endpoint_override)
else: else:
@ -312,7 +309,7 @@ def download_content(title_id: str, content_id: int, wiiu_endpoint: bool = False
endpoint_url = _nus_endpoint[1] endpoint_url = _nus_endpoint[1]
else: else:
endpoint_url = _nus_endpoint[0] endpoint_url = _nus_endpoint[0]
content_url = endpoint_url + title_id + "/000000" + content_id_hex content_url = f"{endpoint_url}{title_id}/{content_id:08X}"
# Make the request. # Make the request.
try: try:
response = requests.get(url=content_url, headers={'User-Agent': 'wii libnup/1.0'}, stream=True) response = requests.get(url=content_url, headers={'User-Agent': 'wii libnup/1.0'}, stream=True)
@ -323,9 +320,8 @@ def download_content(title_id: str, content_id: int, wiiu_endpoint: bool = False
else: else:
raise Exception("A connection could not be made to the NUS endpoint. The NUS may be unavailable.") raise Exception("A connection could not be made to the NUS endpoint. The NUS may be unavailable.")
if response.status_code == 404: if response.status_code == 404:
raise ValueError("The requested Title ID does not exist, or an invalid Content ID is present in the" raise ValueError(f"The requested Title ID does not exist, or an invalid Content ID is present in the"
" content records provided.\n Failed while downloading Content ID: 000000" + f" content records provided.\n Failed while downloading Content ID: {content_id:08X}")
content_id_hex)
elif response.status_code != 200: elif response.status_code != 200:
raise Exception(f"An unknown error occurred while downloading the content. " raise Exception(f"An unknown error occurred while downloading the content. "
f"Got HTTP status code: {response.status_code}") f"Got HTTP status code: {response.status_code}")

View File

@ -9,7 +9,7 @@ import hashlib
from dataclasses import dataclass as _dataclass from dataclasses import dataclass as _dataclass
from .crypto import decrypt_title_key from .crypto import decrypt_title_key
from typing import List from typing import List
from .util import title_ver_standard_to_dec from .versions import title_ver_standard_to_dec
@_dataclass @_dataclass
@ -281,8 +281,7 @@ class Ticket:
""" """
if self.signature != b'\x00' * 256: if self.signature != b'\x00' * 256:
return False return False
test_hash = hashlib.sha1(self.dump()[320:]).hexdigest() if hashlib.sha1(self.dump()[320:]).hexdigest()[:2] != '00':
if test_hash[:2] != '00':
return False return False
return True return True
@ -295,8 +294,7 @@ class Ticket:
str str
The Title ID of the title. The Title ID of the title.
""" """
title_id_str = str(self.title_id.decode()) return str(self.title_id.decode())
return title_id_str
def get_common_key_type(self) -> str: def get_common_key_type(self) -> str:
""" """
@ -370,8 +368,8 @@ class Ticket:
version_converted = title_ver_standard_to_dec(new_version, str(self.title_id.decode())) version_converted = title_ver_standard_to_dec(new_version, str(self.title_id.decode()))
self.title_version = version_converted self.title_version = version_converted
elif type(new_version) is int: elif type(new_version) is int:
# Validate that the version isn't higher than v65280. If the check passes, set that as the title version. # Validate that the version isn't higher than 0xFFFF (v65535).
if new_version > 65535: if new_version > 0xFFFF:
raise ValueError("Title version is not valid! Integer version number cannot exceed v65535.") raise ValueError("Title version is not valid! Integer version number cannot exceed v65535.")
self.title_version = new_version self.title_version = new_version
else: else:

View File

@ -4,14 +4,16 @@
# See https://wiibrew.org/wiki/Title for details about how titles are formatted # See https://wiibrew.org/wiki/Title for details about how titles are formatted
import math import math
from .cert import (CertificateChain as _CertificateChain, from .cert import (CertificateChain as _CertificateChain,
verify_ca_cert as _verify_ca_cert, verify_cert_sig as _verify_cert_sig, verify_ca_cert as _verify_ca_cert, verify_cert_sig as _verify_cert_sig,
verify_tmd_sig as _verify_tmd_sig, verify_ticket_sig as _verify_ticket_sig) verify_tmd_sig as _verify_tmd_sig, verify_ticket_sig as _verify_ticket_sig)
from .content import ContentRegion as _ContentRegion from .content import ContentRegion as _ContentRegion
from .crypto import encrypt_title_key
from .ticket import Ticket as _Ticket from .ticket import Ticket as _Ticket
from .tmd import TMD as _TMD from .tmd import TMD as _TMD
from .types import ContentType
from .wad import WAD as _WAD from .wad import WAD as _WAD
from .crypto import encrypt_title_key
class Title: class Title:
@ -243,7 +245,7 @@ class Title:
# For contents, get their sizes from the content records, because they store the intended sizes of the decrypted # For contents, get their sizes from the content records, because they store the intended sizes of the decrypted
# contents, which are usually different from the encrypted sizes. # contents, which are usually different from the encrypted sizes.
for record in self.content.content_records: for record in self.content.content_records:
if record.content_type == 32769: if record.content_type == ContentType.SHARED:
if absolute: if absolute:
title_size += record.content_size title_size += record.content_size
else: else:
@ -443,14 +445,14 @@ class Title:
-------- --------
libWiiPy.title.cert libWiiPy.title.cert
""" """
# The entire chain needs to be verified, so start with the CA cert and work our way down. If anything fails # I did not understand short-circuiting when I originally wrote this code, and it was 5 nested if statements
# along the way, future steps don't matter so exit the descending if's and return False. # which looked silly. I now understand that this is functionally identical!
try: try:
if _verify_ca_cert(self.cert_chain.ca_cert) is True: if _verify_ca_cert(self.cert_chain.ca_cert) and \
if _verify_cert_sig(self.cert_chain.ca_cert, self.cert_chain.tmd_cert) is True: _verify_cert_sig(self.cert_chain.ca_cert, self.cert_chain.tmd_cert) and \
if _verify_tmd_sig(self.cert_chain.tmd_cert, self.tmd) is True: _verify_tmd_sig(self.cert_chain.tmd_cert, self.tmd) and \
if _verify_cert_sig(self.cert_chain.ca_cert, self.cert_chain.ticket_cert) is True: _verify_cert_sig(self.cert_chain.ca_cert, self.cert_chain.ticket_cert) and \
if _verify_ticket_sig(self.cert_chain.ticket_cert, self.ticket) is True: _verify_ticket_sig(self.cert_chain.ticket_cert, self.ticket):
return True return True
except ValueError: except ValueError:
raise ValueError("This title's certificate chain is not valid, or does not match the signature type of " raise ValueError("This title's certificate chain is not valid, or does not match the signature type of "

View File

@ -10,9 +10,9 @@ import math
import struct import struct
from typing import List from typing import List
from enum import IntEnum as _IntEnum from enum import IntEnum as _IntEnum
from ..types import _ContentRecord
from ..shared import _bitmask from .types import ContentRecord, ContentType, TitleType, Region
from .util import title_ver_standard_to_dec from .versions import title_ver_standard_to_dec
class TMD: class TMD:
@ -58,7 +58,7 @@ class TMD:
self.num_contents: int = 0 # The number of contents contained in the associated title. self.num_contents: int = 0 # The number of contents contained in the associated title.
self.boot_index: int = 0 # The content index that contains the bootable executable. self.boot_index: int = 0 # The content index that contains the bootable executable.
self.minor_version: int = 0 # Minor version (unused typically). self.minor_version: int = 0 # Minor version (unused typically).
self.content_records: List[_ContentRecord] = [] self.content_records: List[ContentRecord] = []
def load(self, tmd: bytes) -> None: def load(self, tmd: bytes) -> None:
""" """
@ -151,7 +151,7 @@ class TMD:
tmd_data.seek(0x1E4 + (36 * content)) tmd_data.seek(0x1E4 + (36 * content))
content_record_hdr = struct.unpack(">LHH4x4s20s", tmd_data.read(36)) content_record_hdr = struct.unpack(">LHH4x4s20s", tmd_data.read(36))
self.content_records.append( self.content_records.append(
_ContentRecord(int(content_record_hdr[0]), int(content_record_hdr[1]), ContentRecord(int(content_record_hdr[0]), int(content_record_hdr[1]),
int(content_record_hdr[2]), int.from_bytes(content_record_hdr[3]), int(content_record_hdr[2]), int.from_bytes(content_record_hdr[3]),
binascii.hexlify(content_record_hdr[4]))) binascii.hexlify(content_record_hdr[4])))
@ -251,7 +251,8 @@ class TMD:
self.minor_version = current_int self.minor_version = current_int
# Trim off the first 320 bytes, because we're only looking for the hash of the TMD's body. # Trim off the first 320 bytes, because we're only looking for the hash of the TMD's body.
# This is a try-except because an OverflowError will be thrown if the number being used to brute-force the # This is a try-except because an OverflowError will be thrown if the number being used to brute-force the
# hash gets too big, as it is only a 16-bit integer. If that happens, then fakesigning has failed. # hash gets too big, as it is only a 16-bit integer. If that happens, then fakesigning has failed. This
# shouldn't ever realistically happen, though.
try: try:
test_hash = hashlib.sha1(self.dump()[320:]).hexdigest() test_hash = hashlib.sha1(self.dump()[320:]).hexdigest()
except OverflowError: except OverflowError:
@ -273,8 +274,7 @@ class TMD:
""" """
if self.signature != b'\x00' * 256: if self.signature != b'\x00' * 256:
return False return False
test_hash = hashlib.sha1(self.dump()[320:]).hexdigest() if hashlib.sha1(self.dump()[320:]).hexdigest()[:2] != '00':
if test_hash[:2] != '00':
return False return False
return True return True
@ -292,15 +292,15 @@ class TMD:
The region of the title. The region of the title.
""" """
match self.region: match self.region:
case 0: case Region.JPN:
return "JPN" return "JPN"
case 1: case Region.USA:
return "USA" return "USA"
case 2: case Region.EUR:
return "EUR" return "EUR"
case 3: case Region.WORLD:
return "None" return "World"
case 4: case Region.KOR:
return "KOR" return "KOR"
case _: case _:
raise ValueError(f"Title contains unknown region \"{self.region}\".") raise ValueError(f"Title contains unknown region \"{self.region}\".")
@ -318,19 +318,19 @@ class TMD:
The type of the title. The type of the title.
""" """
match self.title_id[:8]: match self.title_id[:8]:
case '00000001': case TitleType.SYSTEM:
return "System" return "System"
case '00010000': case TitleType.GAME:
return "Game" return "Game"
case '00010001': case TitleType.CHANNEL:
return "Channel" return "Channel"
case '00010002': case TitleType.SYSTEM_CHANNEL:
return "SystemChannel" return "SystemChannel"
case '00010004': case TitleType.GAME_CHANNEL:
return "GameChannel" return "GameChannel"
case '00010005': case TitleType.DLC:
return "DLC" return "DLC"
case '00010008': case TitleType.HIDDEN_CHANNEL:
return "HiddenChannel" return "HiddenChannel"
case _: case _:
return "Unknown" return "Unknown"
@ -360,20 +360,20 @@ class TMD:
# This is the literal index in the list of content that we're going to get. # This is the literal index in the list of content that we're going to get.
target_index = current_indices.index(content_index) target_index = current_indices.index(content_index)
match self.content_records[target_index].content_type: match self.content_records[target_index].content_type:
case 1: case ContentType.NORMAL:
return "Normal" return "Normal"
case 2: case ContentType.DEVELOPMENT:
return "Development/Unknown" return "Development/Unknown"
case 3: case ContentType.HASH_TREE:
return "Hash Tree" return "Hash Tree"
case 16385: case ContentType.DLC:
return "DLC" return "DLC"
case 32769: case ContentType.SHARED:
return "Shared" return "Shared"
case _: case _:
return "Unknown" return "Unknown"
def get_content_record(self, record) -> _ContentRecord: def get_content_record(self, record) -> ContentRecord:
""" """
Gets the content record at the specified index. Gets the content record at the specified index.
@ -390,8 +390,8 @@ class TMD:
if record < self.num_contents: if record < self.num_contents:
return self.content_records[record] return self.content_records[record]
else: else:
raise IndexError("Invalid content record! TMD lists '" + str(self.num_contents - 1) + raise IndexError(f"Invalid content record! TMD lists \"{self.num_contents - 1}\" contents "
"' contents but index was '" + str(record) + "'!") f"but index was \"{record}\"!")
def get_content_size(self, absolute=False, dlc=False) -> int: def get_content_size(self, absolute=False, dlc=False) -> int:
""" """
@ -415,13 +415,13 @@ class TMD:
""" """
title_size = 0 title_size = 0
for record in self.content_records: for record in self.content_records:
if record.content_type == 0x8001: if record.content_type == ContentType.SHARED:
if absolute: if absolute:
title_size += record.content_size title_size += record.content_size
elif record.content_type == 0x4001: elif record.content_type == ContentType.DLC:
if dlc: if dlc:
title_size += record.content_size title_size += record.content_size
elif record.content_type != 3: elif record.content_type != ContentType.DEVELOPMENT:
title_size += record.content_size title_size += record.content_size
return title_size return title_size
@ -450,10 +450,6 @@ class TMD:
blocks = math.ceil(title_size_bytes / 131072) blocks = math.ceil(title_size_bytes / 131072)
return blocks return blocks
class AccessFlags(_IntEnum):
AHB = 0
DVD_VIDEO = 1
def get_access_right(self, flag: int) -> bool: def get_access_right(self, flag: int) -> bool:
""" """
Gets whether the specified access rights flag is enabled or not. This is done by checking the specified bit. Gets whether the specified access rights flag is enabled or not. This is done by checking the specified bit.
@ -473,7 +469,7 @@ class TMD:
-------- --------
libWiiPy.title.tmd.TMD.AccessFlags libWiiPy.title.tmd.TMD.AccessFlags
""" """
return bool(self.access_rights & _bitmask(flag)) return bool(self.access_rights & (1 << flag))
def set_title_id(self, title_id) -> None: def set_title_id(self, title_id) -> None:
""" """
@ -512,10 +508,17 @@ class TMD:
version_converted: int = title_ver_standard_to_dec(new_version, self.title_id) version_converted: int = title_ver_standard_to_dec(new_version, self.title_id)
self.title_version = version_converted self.title_version = version_converted
elif type(new_version) is int: elif type(new_version) is int:
# Validate that the version isn't higher than v65280. If the check passes, set that as the title version, # Validate that the version isn't higher than 0xFFFF (v65535).
# then convert to standard form and set that as well. if new_version > 0xFFFF:
if new_version > 65535:
raise ValueError("Title version is not valid! Integer version number cannot exceed v65535.") raise ValueError("Title version is not valid! Integer version number cannot exceed v65535.")
self.title_version = new_version self.title_version = new_version
else: else:
raise TypeError("Title version type is not valid! Type must be either integer or string.") raise TypeError("Title version type is not valid! Type must be either integer or string.")
class AccessFlags(_IntEnum):
"""
Flags set in a TMD's access rights field used to enable specific feature access.
"""
AHB = 0
DVD_VIDEO = 1

View File

@ -1,11 +1,14 @@
# "types.py" from libWiiPy by NinjaCheetah & Contributors # "title/types.py" from libWiiPy by NinjaCheetah & Contributors
# https://github.com/NinjaCheetah/libWiiPy # https://github.com/NinjaCheetah/libWiiPy
#
# Shared types used across the title module.
from dataclasses import dataclass from dataclasses import dataclass as _dataclass
from enum import IntEnum as _IntEnum, StrEnum as _StrEnum
@dataclass @_dataclass
class _ContentRecord: class ContentRecord:
""" """
A content record object that contains the details of a content contained in a title. This information must match A content record object that contains the details of a content contained in a title. This information must match
the content stored at the index in the record, or else the content will not decrypt properly, as the hash of the the content stored at the index in the record, or else the content will not decrypt properly, as the hash of the
@ -29,3 +32,38 @@ class _ContentRecord:
content_type: int # Type of content, possible values of: 0x0001: Normal, 0x4001: DLC, 0x8001: Shared. content_type: int # Type of content, possible values of: 0x0001: Normal, 0x4001: DLC, 0x8001: Shared.
content_size: int content_size: int
content_hash: bytes content_hash: bytes
class ContentType(_IntEnum):
"""
The type of an individual piece of content.
"""
NORMAL = 0x0001
DEVELOPMENT = 0x0002
HASH_TREE = 0x0003
DLC = 0x4001
SHARED = 0x8001
class TitleType(_StrEnum):
"""
The type of a title.
"""
SYSTEM = "00000001"
GAME = "00010000"
CHANNEL = "00010001"
SYSTEM_CHANNEL = "00010002"
GAME_CHANNEL = "00010004"
DLC = "00010005"
HIDDEN_CHANNEL = "00010008"
class Region(_IntEnum):
"""
The region of a title.
"""
JPN = 0
USA = 1
EUR = 2
WORLD = 3
KOR = 4

View File

@ -1,10 +1,9 @@
# "title/util.py" from libWiiPy by NinjaCheetah & Contributors # "title/versions.py" from libWiiPy by NinjaCheetah & Contributors
# https://github.com/NinjaCheetah/libWiiPy # https://github.com/NinjaCheetah/libWiiPy
# #
# General title-related utilities that don't fit within a specific module. # Functions for converting the format that a title's version is in.
import math from ..constants import _WII_MENU_VERSIONS, _VWII_MENU_VERSIONS
from ..shared import _wii_menu_versions, _vwii_menu_versions
def title_ver_dec_to_standard(version: int, title_id: str, vwii: bool = False) -> str: def title_ver_dec_to_standard(version: int, title_id: str, vwii: bool = False) -> str:
@ -27,26 +26,18 @@ def title_ver_dec_to_standard(version: int, title_id: str, vwii: bool = False) -
str str
The version of the title, in standard form. The version of the title, in standard form.
""" """
version_out = ""
if title_id == "0000000100000002": if title_id == "0000000100000002":
try:
if vwii: if vwii:
try: return list(_VWII_MENU_VERSIONS.keys())[list(_VWII_MENU_VERSIONS.values()).index(version)]
version_out = list(_vwii_menu_versions.keys())[list(_vwii_menu_versions.values()).index(version)]
except ValueError:
version_out = ""
else: else:
try: return list(_WII_MENU_VERSIONS.keys())[list(_WII_MENU_VERSIONS.values()).index(version)]
version_out = list(_wii_menu_versions.keys())[list(_wii_menu_versions.values()).index(version)]
except ValueError: except ValueError:
version_out = "" raise ValueError(f"Unrecognized System Menu version \"{version}\".")
else: else:
# For most channels, we need to get the floored value of version / 256 for the major version, and the version % # Typical titles use a two-byte version format where the upper byte is the major version, and the lower byte is
# 256 as the minor version. Minor versions > 9 are intended, as Nintendo themselves frequently used them. # the minor version.
version_upper = math.floor(version / 256) return f"{version >> 8}.{version & 0xFF}"
version_lower = version % 256
version_out = f"{version_upper}.{version_lower}"
return version_out
def title_ver_standard_to_dec(version: str, title_id: str) -> int: def title_ver_standard_to_dec(version: str, title_id: str) -> int:
@ -68,13 +59,15 @@ def title_ver_standard_to_dec(version: str, title_id: str) -> int:
int int
The version of the title, in decimal form. The version of the title, in decimal form.
""" """
version_out = 0
if title_id == "0000000100000002": if title_id == "0000000100000002":
raise ValueError("The System Menu's version cannot currently be converted.") for key in _WII_MENU_VERSIONS.keys():
if version.casefold() == key.casefold():
return _WII_MENU_VERSIONS[key]
for key in _VWII_MENU_VERSIONS.keys():
if version.casefold() == key.casefold():
return _VWII_MENU_VERSIONS[key]
raise ValueError(f"Unrecognized System Menu version \"{version}\".")
else: else:
version_str_split = version.split(".") version_str_split = version.split(".")
version_upper = int(version_str_split[0]) * 256 version_out = (int(version_str_split[0]) << 8) + int(version_str_split[1])
version_lower = int(version_str_split[1])
version_out = version_upper + version_lower
return version_out return version_out