diff --git a/docs/source/title/title.md b/docs/source/title/title.md index 6764bf1..9092fb1 100644 --- a/docs/source/title/title.md +++ b/docs/source/title/title.md @@ -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.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.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.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 @@ -33,6 +34,7 @@ The `libWiiPy.title` package contains modules for interacting with Wii titles. T /title/ticket /title/title.title /title/tmd -/title/util /title/wad +/title/types +/title/versions ``` diff --git a/docs/source/title/types.md b/docs/source/title/types.md new file mode 100644 index 0000000..8e340f5 --- /dev/null +++ b/docs/source/title/types.md @@ -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: +``` diff --git a/docs/source/title/util.md b/docs/source/title/util.md deleted file mode 100644 index d5efec4..0000000 --- a/docs/source/title/util.md +++ /dev/null @@ -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: -``` diff --git a/docs/source/title/versions.md b/docs/source/title/versions.md new file mode 100644 index 0000000..68705d8 --- /dev/null +++ b/docs/source/title/versions.md @@ -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: +``` diff --git a/src/libWiiPy/constants.py b/src/libWiiPy/constants.py new file mode 100644 index 0000000..b3aab22 --- /dev/null +++ b/src/libWiiPy/constants.py @@ -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, +} diff --git a/src/libWiiPy/nand/emunand.py b/src/libWiiPy/nand/emunand.py index e5e83d3..77adf95 100644 --- a/src/libWiiPy/nand/emunand.py +++ b/src/libWiiPy/nand/emunand.py @@ -8,6 +8,7 @@ import pathlib import shutil from dataclasses import dataclass as _dataclass from typing import Callable, List + from ..title.ticket import Ticket from ..title.title import Title from ..title.tmd import TMD diff --git a/src/libWiiPy/nand/setting.py b/src/libWiiPy/nand/setting.py index ac0b345..f99c453 100644 --- a/src/libWiiPy/nand/setting.py +++ b/src/libWiiPy/nand/setting.py @@ -8,7 +8,7 @@ from typing import List from ..shared import _pad_bytes -_key = 0x73B5DBFA +_KEY = 0x73B5DBFA class SettingTxt: """ @@ -53,11 +53,11 @@ class SettingTxt: The data of an encrypted setting.txt file. """ 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] = [] for i in range(0, 256): - setting_txt_dec.append(int.from_bytes(setting_data.read(1)) ^ (_key & 0xff)) - _key = (_key << 1) | (_key >> 31) + setting_txt_dec.append(int.from_bytes(setting_data.read(1)) ^ (_KEY & 0xff)) + _KEY = (_KEY << 1) | (_KEY >> 31) setting_txt_bytes = bytes(setting_txt_dec) try: setting_str = setting_txt_bytes.decode('utf-8') @@ -103,13 +103,13 @@ class SettingTxt: """ setting_str = self.dump_decrypted() setting_txt_dec = setting_str.encode() - global _key + global _KEY # This could probably be made more efficient somehow. setting_txt_enc: List[int] = [] with io.BytesIO(setting_txt_dec) as setting_data: for i in range(0, len(setting_txt_dec)): - setting_txt_enc.append(int.from_bytes(setting_data.read(1)) ^ (_key & 0xff)) - _key = (_key << 1) | (_key >> 31) + setting_txt_enc.append(int.from_bytes(setting_data.read(1)) ^ (_KEY & 0xff)) + _KEY = (_KEY << 1) | (_KEY >> 31) setting_txt_bytes = _pad_bytes(bytes(setting_txt_enc), 256) return setting_txt_bytes diff --git a/src/libWiiPy/shared.py b/src/libWiiPy/shared.py index 706d0ed..3b697d8 100644 --- a/src/libWiiPy/shared.py +++ b/src/libWiiPy/shared.py @@ -47,69 +47,3 @@ def _pad_bytes(data, alignment=64) -> bytes: while (len(data) % alignment) != 0: data += b'\x00' 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, -} diff --git a/src/libWiiPy/title/__init__.py b/src/libWiiPy/title/__init__.py index f523857..2a29a33 100644 --- a/src/libWiiPy/title/__init__.py +++ b/src/libWiiPy/title/__init__.py @@ -9,5 +9,6 @@ from .nus import * from .ticket import * from .title import * from .tmd import * -from .util import * +from .types import * +from .versions import * from .wad import * diff --git a/src/libWiiPy/title/cert.py b/src/libWiiPy/title/cert.py index 184830f..0eb73c4 100644 --- a/src/libWiiPy/title/cert.py +++ b/src/libWiiPy/title/cert.py @@ -5,33 +5,47 @@ import io 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.PublicKey import RSA from Crypto.Signature import pkcs1_15 +from ..shared import _align_value, _pad_bytes +from .ticket import Ticket +from .tmd import TMD + class CertificateType(_IntEnum): + """ + The type of a certificate. + """ RSA_4096 = 0x00010000 RSA_2048 = 0x00010001 ECC = 0x00010002 class CertificateSignatureLength(_IntEnum): + """ + The length of a certificate's signature. + """ RSA_4096 = 0x200 RSA_2048 = 0x100 ECC = 0x3C class CertificateKeyType(_IntEnum): + """ + The type of key contained in a certificate. + """ RSA_4096 = 0x00000000 RSA_2048 = 0x00000001 ECC = 0x00000002 class CertificateKeyLength(_IntEnum): + """ + The length of the key contained in a certificate. + """ RSA_4096 = 0x200 RSA_2048 = 0x100 ECC = 0x3C diff --git a/src/libWiiPy/title/commonkeys.py b/src/libWiiPy/title/commonkeys.py index 10d4903..37008b9 100644 --- a/src/libWiiPy/title/commonkeys.py +++ b/src/libWiiPy/title/commonkeys.py @@ -33,13 +33,12 @@ def get_common_key(common_key_index, dev=False) -> bytes: match common_key_index: case 0: if dev: - common_key_bin = binascii.unhexlify(development_key) + return binascii.unhexlify(development_key) else: - common_key_bin = binascii.unhexlify(common_key) + return binascii.unhexlify(common_key) case 1: - common_key_bin = binascii.unhexlify(korean_key) + return binascii.unhexlify(korean_key) case 2: - common_key_bin = binascii.unhexlify(vwii_key) + return binascii.unhexlify(vwii_key) case _: - common_key_bin = binascii.unhexlify(common_key) - return common_key_bin + return binascii.unhexlify(common_key) diff --git a/src/libWiiPy/title/content.py b/src/libWiiPy/title/content.py index 78a4b17..3de531e 100644 --- a/src/libWiiPy/title/content.py +++ b/src/libWiiPy/title/content.py @@ -9,7 +9,7 @@ import hashlib from typing import List from dataclasses import dataclass as _dataclass from enum import IntEnum as _IntEnum -from ..types import _ContentRecord +from .types import ContentRecord from ..shared import _pad_bytes, _align_value from .crypto import decrypt_content, encrypt_content @@ -28,20 +28,20 @@ class ContentRegion: Attributes ---------- - content_records : List[_ContentRecord] + content_records : List[ContentRecord] The content records for the content stored in the region. num_contents : int The total number of contents stored in the region. """ 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.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_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. @@ -49,7 +49,7 @@ class ContentRegion: ---------- content_region : bytes 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. """ self.content_records = content_records @@ -303,7 +303,7 @@ class ContentRegion: 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(). 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 def add_content(self, dec_content: bytes, cid: int, content_type: int, title_key: bytes) -> None: diff --git a/src/libWiiPy/title/crypto.py b/src/libWiiPy/title/crypto.py index 5463eea..f9c8b91 100644 --- a/src/libWiiPy/title/crypto.py +++ b/src/libWiiPy/title/crypto.py @@ -10,7 +10,6 @@ from Crypto.Cipher import AES as _AES 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 # crypto functions. - title_key_iv = b'' if type(title_id) is bytes: # This catches the format b'0000000100000002' if len(title_id) == 16: diff --git a/src/libWiiPy/title/nus.py b/src/libWiiPy/title/nus.py index 9a15841..650c793 100644 --- a/src/libWiiPy/title/nus.py +++ b/src/libWiiPy/title/nus.py @@ -302,9 +302,6 @@ def download_content(title_id: str, content_id: int, wiiu_endpoint: bool = False libWiiPy.title.nus.DownloadCallback """ # Build the download URL. The structure is download//. - 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: endpoint_url = _validate_endpoint(endpoint_override) else: @@ -312,7 +309,7 @@ def download_content(title_id: str, content_id: int, wiiu_endpoint: bool = False endpoint_url = _nus_endpoint[1] else: 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. try: 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: raise Exception("A connection could not be made to the NUS endpoint. The NUS may be unavailable.") if response.status_code == 404: - raise ValueError("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" + - content_id_hex) + raise ValueError(f"The requested Title ID does not exist, or an invalid Content ID is present in the" + f" content records provided.\n Failed while downloading Content ID: {content_id:08X}") elif response.status_code != 200: raise Exception(f"An unknown error occurred while downloading the content. " f"Got HTTP status code: {response.status_code}") diff --git a/src/libWiiPy/title/ticket.py b/src/libWiiPy/title/ticket.py index 7abfc58..73e3481 100644 --- a/src/libWiiPy/title/ticket.py +++ b/src/libWiiPy/title/ticket.py @@ -9,7 +9,7 @@ import hashlib from dataclasses import dataclass as _dataclass from .crypto import decrypt_title_key from typing import List -from .util import title_ver_standard_to_dec +from .versions import title_ver_standard_to_dec @_dataclass @@ -281,8 +281,7 @@ class Ticket: """ if self.signature != b'\x00' * 256: return False - test_hash = hashlib.sha1(self.dump()[320:]).hexdigest() - if test_hash[:2] != '00': + if hashlib.sha1(self.dump()[320:]).hexdigest()[:2] != '00': return False return True @@ -295,8 +294,7 @@ class Ticket: str The Title ID of the title. """ - title_id_str = str(self.title_id.decode()) - return title_id_str + return str(self.title_id.decode()) 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())) self.title_version = version_converted 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. - if new_version > 65535: + # Validate that the version isn't higher than 0xFFFF (v65535). + if new_version > 0xFFFF: raise ValueError("Title version is not valid! Integer version number cannot exceed v65535.") self.title_version = new_version else: diff --git a/src/libWiiPy/title/title.py b/src/libWiiPy/title/title.py index 682866b..a20c89c 100644 --- a/src/libWiiPy/title/title.py +++ b/src/libWiiPy/title/title.py @@ -4,14 +4,16 @@ # See https://wiibrew.org/wiki/Title for details about how titles are formatted import math + from .cert import (CertificateChain as _CertificateChain, 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) from .content import ContentRegion as _ContentRegion +from .crypto import encrypt_title_key from .ticket import Ticket as _Ticket from .tmd import TMD as _TMD +from .types import ContentType from .wad import WAD as _WAD -from .crypto import encrypt_title_key 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 # contents, which are usually different from the encrypted sizes. for record in self.content.content_records: - if record.content_type == 32769: + if record.content_type == ContentType.SHARED: if absolute: title_size += record.content_size else: @@ -443,15 +445,15 @@ class Title: -------- libWiiPy.title.cert """ - # The entire chain needs to be verified, so start with the CA cert and work our way down. If anything fails - # along the way, future steps don't matter so exit the descending if's and return False. + # I did not understand short-circuiting when I originally wrote this code, and it was 5 nested if statements + # which looked silly. I now understand that this is functionally identical! try: - if _verify_ca_cert(self.cert_chain.ca_cert) is True: - if _verify_cert_sig(self.cert_chain.ca_cert, self.cert_chain.tmd_cert) is True: - if _verify_tmd_sig(self.cert_chain.tmd_cert, self.tmd) is True: - if _verify_cert_sig(self.cert_chain.ca_cert, self.cert_chain.ticket_cert) is True: - if _verify_ticket_sig(self.cert_chain.ticket_cert, self.ticket) is True: - return True + if _verify_ca_cert(self.cert_chain.ca_cert) and \ + _verify_cert_sig(self.cert_chain.ca_cert, self.cert_chain.tmd_cert) and \ + _verify_tmd_sig(self.cert_chain.tmd_cert, self.tmd) and \ + _verify_cert_sig(self.cert_chain.ca_cert, self.cert_chain.ticket_cert) and \ + _verify_ticket_sig(self.cert_chain.ticket_cert, self.ticket): + return True except ValueError: raise ValueError("This title's certificate chain is not valid, or does not match the signature type of " "the TMD/Ticket.") diff --git a/src/libWiiPy/title/tmd.py b/src/libWiiPy/title/tmd.py index e2473ba..04098ce 100644 --- a/src/libWiiPy/title/tmd.py +++ b/src/libWiiPy/title/tmd.py @@ -10,9 +10,9 @@ import math import struct from typing import List from enum import IntEnum as _IntEnum -from ..types import _ContentRecord -from ..shared import _bitmask -from .util import title_ver_standard_to_dec + +from .types import ContentRecord, ContentType, TitleType, Region +from .versions import title_ver_standard_to_dec class TMD: @@ -58,7 +58,7 @@ class TMD: 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.minor_version: int = 0 # Minor version (unused typically). - self.content_records: List[_ContentRecord] = [] + self.content_records: List[ContentRecord] = [] def load(self, tmd: bytes) -> None: """ @@ -151,7 +151,7 @@ class TMD: tmd_data.seek(0x1E4 + (36 * content)) content_record_hdr = struct.unpack(">LHH4x4s20s", tmd_data.read(36)) 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]), binascii.hexlify(content_record_hdr[4]))) @@ -251,7 +251,8 @@ class TMD: self.minor_version = current_int # 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 - # 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: test_hash = hashlib.sha1(self.dump()[320:]).hexdigest() except OverflowError: @@ -273,8 +274,7 @@ class TMD: """ if self.signature != b'\x00' * 256: return False - test_hash = hashlib.sha1(self.dump()[320:]).hexdigest() - if test_hash[:2] != '00': + if hashlib.sha1(self.dump()[320:]).hexdigest()[:2] != '00': return False return True @@ -292,15 +292,15 @@ class TMD: The region of the title. """ match self.region: - case 0: + case Region.JPN: return "JPN" - case 1: + case Region.USA: return "USA" - case 2: + case Region.EUR: return "EUR" - case 3: - return "None" - case 4: + case Region.WORLD: + return "World" + case Region.KOR: return "KOR" case _: raise ValueError(f"Title contains unknown region \"{self.region}\".") @@ -318,19 +318,19 @@ class TMD: The type of the title. """ match self.title_id[:8]: - case '00000001': + case TitleType.SYSTEM: return "System" - case '00010000': + case TitleType.GAME: return "Game" - case '00010001': + case TitleType.CHANNEL: return "Channel" - case '00010002': + case TitleType.SYSTEM_CHANNEL: return "SystemChannel" - case '00010004': + case TitleType.GAME_CHANNEL: return "GameChannel" - case '00010005': + case TitleType.DLC: return "DLC" - case '00010008': + case TitleType.HIDDEN_CHANNEL: return "HiddenChannel" case _: return "Unknown" @@ -360,20 +360,20 @@ class TMD: # This is the literal index in the list of content that we're going to get. target_index = current_indices.index(content_index) match self.content_records[target_index].content_type: - case 1: + case ContentType.NORMAL: return "Normal" - case 2: + case ContentType.DEVELOPMENT: return "Development/Unknown" - case 3: + case ContentType.HASH_TREE: return "Hash Tree" - case 16385: + case ContentType.DLC: return "DLC" - case 32769: + case ContentType.SHARED: return "Shared" case _: return "Unknown" - def get_content_record(self, record) -> _ContentRecord: + def get_content_record(self, record) -> ContentRecord: """ Gets the content record at the specified index. @@ -390,8 +390,8 @@ class TMD: if record < self.num_contents: return self.content_records[record] else: - raise IndexError("Invalid content record! TMD lists '" + str(self.num_contents - 1) + - "' contents but index was '" + str(record) + "'!") + raise IndexError(f"Invalid content record! TMD lists \"{self.num_contents - 1}\" contents " + f"but index was \"{record}\"!") def get_content_size(self, absolute=False, dlc=False) -> int: """ @@ -415,13 +415,13 @@ class TMD: """ title_size = 0 for record in self.content_records: - if record.content_type == 0x8001: + if record.content_type == ContentType.SHARED: if absolute: title_size += record.content_size - elif record.content_type == 0x4001: + elif record.content_type == ContentType.DLC: if dlc: title_size += record.content_size - elif record.content_type != 3: + elif record.content_type != ContentType.DEVELOPMENT: title_size += record.content_size return title_size @@ -450,10 +450,6 @@ class TMD: blocks = math.ceil(title_size_bytes / 131072) return blocks - class AccessFlags(_IntEnum): - AHB = 0 - DVD_VIDEO = 1 - 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. @@ -473,7 +469,7 @@ class TMD: -------- 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: """ @@ -512,10 +508,17 @@ class TMD: version_converted: int = title_ver_standard_to_dec(new_version, self.title_id) self.title_version = version_converted 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, - # then convert to standard form and set that as well. - if new_version > 65535: + # Validate that the version isn't higher than 0xFFFF (v65535). + if new_version > 0xFFFF: raise ValueError("Title version is not valid! Integer version number cannot exceed v65535.") self.title_version = new_version else: 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 diff --git a/src/libWiiPy/types.py b/src/libWiiPy/title/types.py similarity index 52% rename from src/libWiiPy/types.py rename to src/libWiiPy/title/types.py index fe0e983..0c5ec1d 100644 --- a/src/libWiiPy/types.py +++ b/src/libWiiPy/title/types.py @@ -1,11 +1,14 @@ -# "types.py" from libWiiPy by NinjaCheetah & Contributors +# "title/types.py" from libWiiPy by NinjaCheetah & Contributors # 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 -class _ContentRecord: +@_dataclass +class ContentRecord: """ 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 @@ -29,3 +32,38 @@ class _ContentRecord: content_type: int # Type of content, possible values of: 0x0001: Normal, 0x4001: DLC, 0x8001: Shared. content_size: int 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 diff --git a/src/libWiiPy/title/util.py b/src/libWiiPy/title/versions.py similarity index 56% rename from src/libWiiPy/title/util.py rename to src/libWiiPy/title/versions.py index 416b381..44eef51 100644 --- a/src/libWiiPy/title/util.py +++ b/src/libWiiPy/title/versions.py @@ -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 # -# 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 ..shared import _wii_menu_versions, _vwii_menu_versions +from ..constants import _WII_MENU_VERSIONS, _VWII_MENU_VERSIONS 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 The version of the title, in standard form. """ - version_out = "" if title_id == "0000000100000002": - if vwii: - try: - version_out = list(_vwii_menu_versions.keys())[list(_vwii_menu_versions.values()).index(version)] - except ValueError: - version_out = "" - else: - try: - version_out = list(_wii_menu_versions.keys())[list(_wii_menu_versions.values()).index(version)] - except ValueError: - version_out = "" + try: + if vwii: + return list(_VWII_MENU_VERSIONS.keys())[list(_VWII_MENU_VERSIONS.values()).index(version)] + else: + return list(_WII_MENU_VERSIONS.keys())[list(_WII_MENU_VERSIONS.values()).index(version)] + except ValueError: + raise ValueError(f"Unrecognized System Menu version \"{version}\".") else: - # For most channels, we need to get the floored value of version / 256 for the major version, and the version % - # 256 as the minor version. Minor versions > 9 are intended, as Nintendo themselves frequently used them. - version_upper = math.floor(version / 256) - version_lower = version % 256 - version_out = f"{version_upper}.{version_lower}" - - return version_out + # Typical titles use a two-byte version format where the upper byte is the major version, and the lower byte is + # the minor version. + return f"{version >> 8}.{version & 0xFF}" 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 The version of the title, in decimal form. """ - version_out = 0 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: version_str_split = version.split(".") - version_upper = int(version_str_split[0]) * 256 - version_lower = int(version_str_split[1]) - version_out = version_upper + version_lower - - return version_out + version_out = (int(version_str_split[0]) << 8) + int(version_str_split[1]) + return version_out