Privatized many functions and classes that should be private

Also changed some imports to import as _name so that stuff like dataclass() doesn't appear as available under libWiiPy.title
This commit is contained in:
NinjaCheetah 2024-06-23 18:28:32 -04:00
parent 2d64f7961e
commit 0861c20100
No known key found for this signature in database
GPG Key ID: 670C282B3291D63D
10 changed files with 107 additions and 103 deletions

1
.gitignore vendored
View File

@ -165,6 +165,7 @@ cython_debug/
*.tmd *.tmd
*.wad *.wad
*.arc *.arc
*.ash
out_prod/ out_prod/
remakewad.pl remakewad.pl

View File

@ -8,10 +8,10 @@
# See <link pending> for details about the ASH archive format. # See <link pending> for details about the ASH archive format.
import io import io
from dataclasses import dataclass from dataclasses import dataclass as _dataclass
@dataclass @_dataclass
class _ASHBitReader: class _ASHBitReader:
""" """
An _ASHBitReader class used to parse individual words in an ASH file. Private class used by the ASH module. An _ASHBitReader class used to parse individual words in an ASH file. Private class used by the ASH module.

View File

@ -6,17 +6,17 @@
import io import io
import os import os
import pathlib import pathlib
from dataclasses import dataclass from dataclasses import dataclass as _dataclass
from typing import List from typing import List
from ..shared import align_value from ..shared import _align_value
@dataclass @_dataclass
class U8Node: class _U8Node:
""" """
A U8Node object that contains the data of a single node in a U8 file header. Each node keeps track of whether this A U8Node object that contains the data of a single node in a U8 file header. Each node keeps track of whether this
node is for a file or directory, the offset of the name of the file/directory, the offset of the data for the file/ node is for a file or directory, the offset of the name of the file/directory, the offset of the data for the file/
directory, and the size of the data. directory, and the size of the data. Private class used by functions and methods in the U8 module.
Attributes Attributes
---------- ----------
@ -44,7 +44,7 @@ class U8Archive:
---------- ----------
""" """
self.u8_magic = b'' self.u8_magic = b''
self.u8_node_list: List[U8Node] = [] # All the nodes in the header of a U8 file. self.u8_node_list: List[_U8Node] = [] # All the nodes in the header of a U8 file.
self.file_name_list: List[str] = [] self.file_name_list: List[str] = []
self.file_data_list: List[bytes] = [] self.file_data_list: List[bytes] = []
self.u8_file_structure = dict self.u8_file_structure = dict
@ -86,7 +86,7 @@ class U8Archive:
node_name_offset = int.from_bytes(u8_data.read(2)) node_name_offset = int.from_bytes(u8_data.read(2))
node_data_offset = int.from_bytes(u8_data.read(4)) node_data_offset = int.from_bytes(u8_data.read(4))
node_size = int.from_bytes(u8_data.read(4)) node_size = int.from_bytes(u8_data.read(4))
self.u8_node_list.append(U8Node(node_type, node_name_offset, node_data_offset, node_size)) self.u8_node_list.append(_U8Node(node_type, node_name_offset, node_data_offset, node_size))
# Iterate over all loaded nodes and create a list of file names and a list of file data. # Iterate over all loaded nodes and create a list of file names and a list of file data.
name_base_offset = u8_data.tell() name_base_offset = u8_data.tell()
for node in self.u8_node_list: for node in self.u8_node_list:
@ -121,7 +121,7 @@ class U8Archive:
for file_name in self.file_name_list: for file_name in self.file_name_list:
header_size += len(file_name) + 1 header_size += len(file_name) + 1
# The initial data offset is equal to the file header (32 bytes) + node data aligned to 16 bytes. # The initial data offset is equal to the file header (32 bytes) + node data aligned to 16 bytes.
data_offset = align_value(header_size + 32, 16) data_offset = _align_value(header_size + 32, 16)
# Adjust all nodes to place file data in the same order as the nodes. Why isn't it already like this? # Adjust all nodes to place file data in the same order as the nodes. Why isn't it already like this?
current_data_offset = data_offset current_data_offset = data_offset
for node in range(len(self.u8_node_list)): for node in range(len(self.u8_node_list)):
@ -241,7 +241,7 @@ def _pack_u8_dir(u8_archive: U8Archive, current_path, node_count, name_offset):
node_count += 1 node_count += 1
u8_archive.file_name_list.append(file) u8_archive.file_name_list.append(file)
u8_archive.file_data_list.append(open(current_path.joinpath(file), "rb").read()) u8_archive.file_data_list.append(open(current_path.joinpath(file), "rb").read())
u8_archive.u8_node_list.append(U8Node(0, name_offset, 0, len(u8_archive.file_data_list[-1]))) u8_archive.u8_node_list.append(_U8Node(0, name_offset, 0, len(u8_archive.file_data_list[-1])))
name_offset = name_offset + len(file) + 1 # Add 1 to accommodate the null byte at the end of the name. name_offset = name_offset + len(file) + 1 # Add 1 to accommodate the null byte at the end of the name.
# For directories, add their name to the file name list, add empty data to the file data list (since they obviously # For directories, add their name to the file name list, add empty data to the file data list (since they obviously
# wouldn't have any), find the total number of files and directories inside the directory to calculate the final # wouldn't have any), find the total number of files and directories inside the directory to calculate the final
@ -251,7 +251,7 @@ def _pack_u8_dir(u8_archive: U8Archive, current_path, node_count, name_offset):
u8_archive.file_name_list.append(directory) u8_archive.file_name_list.append(directory)
u8_archive.file_data_list.append(b'') u8_archive.file_data_list.append(b'')
max_node = node_count + sum(1 for _ in current_path.joinpath(directory).rglob('*')) max_node = node_count + sum(1 for _ in current_path.joinpath(directory).rglob('*'))
u8_archive.u8_node_list.append(U8Node(256, name_offset, 0, max_node)) u8_archive.u8_node_list.append(_U8Node(256, name_offset, 0, max_node))
name_offset = name_offset + len(directory) + 1 # Add 1 to accommodate the null byte at the end of the name. name_offset = name_offset + len(directory) + 1 # Add 1 to accommodate the null byte at the end of the name.
u8_archive, node_count, name_offset = _pack_u8_dir(u8_archive, current_path.joinpath(directory), node_count, u8_archive, node_count, name_offset = _pack_u8_dir(u8_archive, current_path.joinpath(directory), node_count,
name_offset) name_offset)
@ -280,7 +280,7 @@ def pack_u8(input_path) -> bytes:
u8_archive = U8Archive() u8_archive = U8Archive()
u8_archive.file_name_list.append("") u8_archive.file_name_list.append("")
u8_archive.file_data_list.append(b'') u8_archive.file_data_list.append(b'')
u8_archive.u8_node_list.append(U8Node(256, 0, 0, sum(1 for _ in input_path.rglob('*')) + 1)) u8_archive.u8_node_list.append(_U8Node(256, 0, 0, sum(1 for _ in input_path.rglob('*')) + 1))
# Call the private function _pack_u8_dir() on the root note, which will recursively call itself to pack every # Call the private function _pack_u8_dir() on the root note, which will recursively call itself to pack every
# subdirectory and file. Discard node_count and name_offset since we don't care about them here, as they're # subdirectory and file. Discard node_count and name_offset since we don't care about them here, as they're
# really only necessary for the directory recursion. # really only necessary for the directory recursion.
@ -300,8 +300,8 @@ def pack_u8(input_path) -> bytes:
u8_archive.file_data_list.append(b'') u8_archive.file_data_list.append(b'')
u8_archive.file_data_list.append(file_data) u8_archive.file_data_list.append(file_data)
# Append generic U8Node for the root, followed by the actual file's node. # Append generic U8Node for the root, followed by the actual file's node.
u8_archive.u8_node_list.append(U8Node(256, 0, 0, 2)) u8_archive.u8_node_list.append(_U8Node(256, 0, 0, 2))
u8_archive.u8_node_list.append(U8Node(0, 1, 0, len(file_data))) u8_archive.u8_node_list.append(_U8Node(0, 1, 0, len(file_data)))
return u8_archive.dump() return u8_archive.dump()
else: else:
raise FileNotFoundError("Input file/directory: \"" + str(input_path) + "\" does not exist!") raise FileNotFoundError("Input file/directory: \"" + str(input_path) + "\" does not exist!")

View File

@ -4,12 +4,10 @@
# This file defines general functions that may be useful in other modules of libWiiPy. Putting them here cuts down on # This file defines general functions that may be useful in other modules of libWiiPy. Putting them here cuts down on
# clutter in other files. # clutter in other files.
import binascii
def _align_value(value, alignment=64) -> int:
def align_value(value, alignment=64) -> int:
""" """
Aligns the provided value to the set alignment (defaults to 64). Aligns the provided value to the set alignment (defaults to 64). Private function used by other libWiiPy modules.
Parameters Parameters
---------- ----------
@ -29,9 +27,10 @@ def align_value(value, alignment=64) -> int:
return value return value
def pad_bytes(data, alignment=64) -> bytes: def _pad_bytes(data, alignment=64) -> bytes:
""" """
Pads the provided bytes object to the provided alignment (defaults to 64). Pads the provided bytes object to the provided alignment (defaults to 64). Private function used by other libWiiPy
modules.
Parameters Parameters
---------- ----------
@ -48,24 +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 convert_tid_to_iv(title_id: str) -> bytes:
title_key_iv = b''
if type(title_id) is bytes:
# This catches the format b'0000000100000002'
if len(title_id) == 16:
title_key_iv = binascii.unhexlify(title_id)
# This catches the format b'\x00\x00\x00\x01\x00\x00\x00\x02'
elif len(title_id) == 8:
pass
# If it isn't one of those lengths, it cannot possibly be valid, so reject it.
else:
raise ValueError("Title ID is not valid!")
# Allow for a string like "0000000100000002"
elif type(title_id) is str:
title_key_iv = binascii.unhexlify(title_id)
# If the Title ID isn't bytes or a string, it isn't valid and is rejected.
else:
raise TypeError("Title ID type is not valid! It must be either type str or bytes.")
return title_key_iv

View File

@ -2,10 +2,32 @@
# https://github.com/NinjaCheetah/libWiiPy # https://github.com/NinjaCheetah/libWiiPy
import struct import struct
import binascii
from .commonkeys import get_common_key from .commonkeys import get_common_key
from ..shared import convert_tid_to_iv from Crypto.Cipher import AES as _AES
from Crypto.Cipher import AES
def _convert_tid_to_iv(title_id: str) -> 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:
title_key_iv = binascii.unhexlify(title_id)
# This catches the format b'\x00\x00\x00\x01\x00\x00\x00\x02'
elif len(title_id) == 8:
pass
# If it isn't one of those lengths, it cannot possibly be valid, so reject it.
else:
raise ValueError("Title ID is not valid!")
# Allow for a string like "0000000100000002"
elif type(title_id) is str:
title_key_iv = binascii.unhexlify(title_id)
# If the Title ID isn't bytes or a string, it isn't valid and is rejected.
else:
raise TypeError("Title ID type is not valid! It must be either type str or bytes.")
return title_key_iv
def decrypt_title_key(title_key_enc: bytes, common_key_index: int, title_id: bytes | str) -> bytes: def decrypt_title_key(title_key_enc: bytes, common_key_index: int, title_id: bytes | str) -> bytes:
@ -31,11 +53,11 @@ def decrypt_title_key(title_key_enc: bytes, common_key_index: int, title_id: byt
# Load the correct common key for the title. # Load the correct common key for the title.
common_key = get_common_key(common_key_index) common_key = get_common_key(common_key_index)
# Convert the IV into the correct format based on the type provided. # Convert the IV into the correct format based on the type provided.
title_key_iv = convert_tid_to_iv(title_id) title_key_iv = _convert_tid_to_iv(title_id)
# The IV will always be in the same format by this point, so add the last 8 bytes. # The IV will always be in the same format by this point, so add the last 8 bytes.
title_key_iv = title_key_iv + (b'\x00' * 8) title_key_iv = title_key_iv + (b'\x00' * 8)
# Create a new AES object with the values provided. # Create a new AES object with the values provided.
aes = AES.new(common_key, AES.MODE_CBC, title_key_iv) aes = _AES.new(common_key, _AES.MODE_CBC, title_key_iv)
# Decrypt the Title Key using the AES object. # Decrypt the Title Key using the AES object.
title_key = aes.decrypt(title_key_enc) title_key = aes.decrypt(title_key_enc)
return title_key return title_key
@ -64,11 +86,11 @@ def encrypt_title_key(title_key_dec: bytes, common_key_index: int, title_id: byt
# Load the correct common key for the title. # Load the correct common key for the title.
common_key = get_common_key(common_key_index) common_key = get_common_key(common_key_index)
# Convert the IV into the correct format based on the type provided. # Convert the IV into the correct format based on the type provided.
title_key_iv = convert_tid_to_iv(title_id) title_key_iv = _convert_tid_to_iv(title_id)
# The IV will always be in the same format by this point, so add the last 8 bytes. # The IV will always be in the same format by this point, so add the last 8 bytes.
title_key_iv = title_key_iv + (b'\x00' * 8) title_key_iv = title_key_iv + (b'\x00' * 8)
# Create a new AES object with the values provided. # Create a new AES object with the values provided.
aes = AES.new(common_key, AES.MODE_CBC, title_key_iv) aes = _AES.new(common_key, _AES.MODE_CBC, title_key_iv)
# Encrypt Title Key using the AES object. # Encrypt Title Key using the AES object.
title_key = aes.encrypt(title_key_dec) title_key = aes.encrypt(title_key_dec)
return title_key return title_key
@ -105,7 +127,7 @@ def decrypt_content(content_enc, title_key, content_index, content_length) -> by
if (len(content_enc) % 16) != 0: if (len(content_enc) % 16) != 0:
content_enc = content_enc + (b'\x00' * (16 - (len(content_enc) % 16))) content_enc = content_enc + (b'\x00' * (16 - (len(content_enc) % 16)))
# Create a new AES object with the values provided, with the content's unique ID as the IV. # Create a new AES object with the values provided, with the content's unique ID as the IV.
aes = AES.new(title_key, AES.MODE_CBC, content_index_bin) aes = _AES.new(title_key, _AES.MODE_CBC, content_index_bin)
# Decrypt the content using the AES object. # Decrypt the content using the AES object.
content_dec = aes.decrypt(content_enc) content_dec = aes.decrypt(content_enc)
# Trim additional bytes that may have been added so the content is the correct size. # Trim additional bytes that may have been added so the content is the correct size.
@ -144,7 +166,7 @@ def encrypt_content(content_dec, title_key, content_index) -> bytes:
if (len(content_dec) % 16) != 0: if (len(content_dec) % 16) != 0:
content_dec = content_dec + (b'\x00' * (16 - (len(content_dec) % 16))) content_dec = content_dec + (b'\x00' * (16 - (len(content_dec) % 16)))
# Create a new AES object with the values provided, with the content's unique ID as the IV. # Create a new AES object with the values provided, with the content's unique ID as the IV.
aes = AES.new(title_key, AES.MODE_CBC, content_index_bin) aes = _AES.new(title_key, _AES.MODE_CBC, content_index_bin)
# Encrypt the content using the AES object. # Encrypt the content using the AES object.
content_enc = aes.encrypt(content_dec) content_enc = aes.encrypt(content_dec)
# Trim down the encrypted content. # Trim down the encrypted content.

View File

@ -10,7 +10,7 @@ from .title import Title
from .tmd import TMD from .tmd import TMD
from .ticket import Ticket from .ticket import Ticket
nus_endpoint = ["http://nus.cdn.shop.wii.com/ccs/download/", "http://ccs.cdn.wup.shop.nintendo.net/ccs/download/"] _nus_endpoint = ["http://nus.cdn.shop.wii.com/ccs/download/", "http://ccs.cdn.wup.shop.nintendo.net/ccs/download/"]
def download_title(title_id: str, title_version: int = None, wiiu_endpoint: bool = False) -> Title: def download_title(title_id: str, title_version: int = None, wiiu_endpoint: bool = False) -> Title:
@ -68,9 +68,9 @@ def download_tmd(title_id: str, title_version: int = None, wiiu_endpoint: bool =
# Build the download URL. The structure is download/<TID>/tmd for latest and download/<TID>/tmd.<version> for # Build the download URL. The structure is download/<TID>/tmd for latest and download/<TID>/tmd.<version> for
# when a specific version is requested. # when a specific version is requested.
if wiiu_endpoint is False: if wiiu_endpoint is False:
tmd_url = nus_endpoint[0] + title_id + "/tmd" tmd_url = _nus_endpoint[0] + title_id + "/tmd"
else: else:
tmd_url = nus_endpoint[1] + title_id + "/tmd" tmd_url = _nus_endpoint[1] + title_id + "/tmd"
# Add the version to the URL if one was specified. # Add the version to the URL if one was specified.
if title_version is not None: if title_version is not None:
tmd_url += "." + str(title_version) tmd_url += "." + str(title_version)
@ -109,9 +109,9 @@ def download_ticket(title_id: str, wiiu_endpoint: bool = False) -> bytes:
# Build the download URL. The structure is download/<TID>/cetk, and cetk will only exist if this is a free # Build the download URL. The structure is download/<TID>/cetk, and cetk will only exist if this is a free
# title. # title.
if wiiu_endpoint is False: if wiiu_endpoint is False:
ticket_url = nus_endpoint[0] + title_id + "/cetk" ticket_url = _nus_endpoint[0] + title_id + "/cetk"
else: else:
ticket_url = nus_endpoint[1] + title_id + "/cetk" ticket_url = _nus_endpoint[1] + title_id + "/cetk"
# Make the request. # Make the request.
ticket_request = requests.get(url=ticket_url, headers={'User-Agent': 'wii libnup/1.0'}, stream=True) ticket_request = requests.get(url=ticket_url, headers={'User-Agent': 'wii libnup/1.0'}, stream=True)
if ticket_request.status_code != 200: if ticket_request.status_code != 200:
@ -142,11 +142,11 @@ def download_cert(wiiu_endpoint: bool = False) -> bytes:
""" """
# Download the TMD and cetk for the System Menu 4.3U. # Download the TMD and cetk for the System Menu 4.3U.
if wiiu_endpoint is False: if wiiu_endpoint is False:
tmd_url = nus_endpoint[0] + "0000000100000002/tmd.513" tmd_url = _nus_endpoint[0] + "0000000100000002/tmd.513"
cetk_url = nus_endpoint[0] + "0000000100000002/cetk" cetk_url = _nus_endpoint[0] + "0000000100000002/cetk"
else: else:
tmd_url = nus_endpoint[1] + "0000000100000002/tmd.513" tmd_url = _nus_endpoint[1] + "0000000100000002/tmd.513"
cetk_url = nus_endpoint[1] + "0000000100000002/cetk" cetk_url = _nus_endpoint[1] + "0000000100000002/cetk"
tmd = requests.get(url=tmd_url, headers={'User-Agent': 'wii libnup/1.0'}, stream=True).content tmd = requests.get(url=tmd_url, headers={'User-Agent': 'wii libnup/1.0'}, stream=True).content
cetk = requests.get(url=cetk_url, headers={'User-Agent': 'wii libnup/1.0'}, stream=True).content cetk = requests.get(url=cetk_url, headers={'User-Agent': 'wii libnup/1.0'}, stream=True).content
# Assemble the certificate. # Assemble the certificate.
@ -186,9 +186,9 @@ def download_content(title_id: str, content_id: int, wiiu_endpoint: bool = False
if len(content_id_hex) < 2: if len(content_id_hex) < 2:
content_id_hex = "0" + content_id_hex content_id_hex = "0" + content_id_hex
if wiiu_endpoint is False: if wiiu_endpoint is False:
content_url = nus_endpoint[0] + title_id + "/000000" + content_id_hex content_url = _nus_endpoint[0] + title_id + "/000000" + content_id_hex
else: else:
content_url = nus_endpoint[1] + title_id + "/000000" + content_id_hex content_url = _nus_endpoint[1] + title_id + "/000000" + content_id_hex
# Make the request. # Make the request.
content_request = requests.get(url=content_url, headers={'User-Agent': 'wii libnup/1.0'}, stream=True) content_request = requests.get(url=content_url, headers={'User-Agent': 'wii libnup/1.0'}, stream=True)
if content_request.status_code != 200: if content_request.status_code != 200:

View File

@ -5,11 +5,33 @@
import io import io
import binascii import binascii
from dataclasses import dataclass as _dataclass
from .crypto import decrypt_title_key from .crypto import decrypt_title_key
from ..types import TitleLimit
from typing import List from typing import List
@_dataclass
class _TitleLimit:
"""
A TitleLimit object that contains the type of restriction and the limit. The limit type can be one of the following:
0 = None, 1 = Time Limit, 3 = None, or 4 = Launch Count. The maximum usage is then either the time in minutes the
title can be played or the maximum number of launches allowed for that title, based on the type of limit applied.
Private class used only by the Ticket class.
Attributes
----------
limit_type : int
The type of play limit applied.
maximum_usage : int
The maximum value for the type of play limit applied.
"""
# The type of play limit applied.
# 0 = None, 1 = Time Limit, 3 = None, 4 = Launch Count
limit_type: int
# The maximum value of the limit applied.
maximum_usage: int
class Ticket: class Ticket:
""" """
A Ticket object that allows for either loading and editing an existing Ticket or creating one manually if desired. A Ticket object that allows for either loading and editing an existing Ticket or creating one manually if desired.
@ -47,12 +69,14 @@ class Ticket:
self.unknown1: bytes = b'' # Some unknown data, not always the same so reading it just in case. self.unknown1: bytes = b'' # Some unknown data, not always the same so reading it just in case.
self.title_version: int = 0 # Version of the ticket's associated title. self.title_version: int = 0 # Version of the ticket's associated title.
self.permitted_titles: bytes = b'' # Permitted titles mask self.permitted_titles: bytes = b'' # Permitted titles mask
self.permit_mask: bytes = b'' # "Permit mask. The current disc title is ANDed with the inverse of this mask to see if the result matches the Permitted Titles Mask." # "Permit mask. The current disc title is ANDed with the inverse of this mask to see if the result matches the
# Permitted Titles Mask."
self.permit_mask: bytes = b''
self.title_export_allowed: int = 0 # Whether title export is allowed with a PRNG key or not. self.title_export_allowed: int = 0 # Whether title export is allowed with a PRNG key or not.
self.common_key_index: int = 0 # Which common key should be used. 0 = Common Key, 1 = Korean Key, 2 = vWii Key self.common_key_index: int = 0 # Which common key should be used. 0 = Common Key, 1 = Korean Key, 2 = vWii Key
self.unknown2: bytes = b'' # More unknown data. Varies for VC/non-VC titles so reading it to ensure it matches. self.unknown2: bytes = b'' # More unknown data. Varies for VC/non-VC titles so reading it to ensure it matches.
self.content_access_permissions: bytes = b'' # "Content access permissions (one bit for each content)" self.content_access_permissions: bytes = b'' # "Content access permissions (one bit for each content)"
self.title_limits_list: List[TitleLimit] = [] # List of play limits applied to the title. self.title_limits_list: List[_TitleLimit] = [] # List of play limits applied to the title.
# v1 ticket data # v1 ticket data
# TODO: Write in v1 ticket attributes here. This code can currently only handle v0 tickets, and will reject v1. # TODO: Write in v1 ticket attributes here. This code can currently only handle v0 tickets, and will reject v1.
@ -134,7 +158,7 @@ class Ticket:
for limit in range(0, 8): for limit in range(0, 8):
limit_type = int.from_bytes(ticket_data.read(4)) limit_type = int.from_bytes(ticket_data.read(4))
limit_value = int.from_bytes(ticket_data.read(4)) limit_value = int.from_bytes(ticket_data.read(4))
self.title_limits_list.append(TitleLimit(limit_type, limit_value)) self.title_limits_list.append(_TitleLimit(limit_type, limit_value))
def dump(self) -> bytes: def dump(self) -> bytes:
""" """

View File

@ -5,7 +5,7 @@
import io import io
import binascii import binascii
from ..shared import align_value, pad_bytes from ..shared import _align_value, _pad_bytes
class WAD: class WAD:
@ -102,12 +102,12 @@ class WAD:
# ==================================================================================== # ====================================================================================
wad_cert_offset = self.wad_hdr_size 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. # crl isn't ever used, however an entry for its size exists in the header, so its calculated just in case.
wad_crl_offset = align_value(wad_cert_offset + self.wad_cert_size) wad_crl_offset = _align_value(wad_cert_offset + self.wad_cert_size)
wad_tik_offset = align_value(wad_crl_offset + self.wad_crl_size) wad_tik_offset = _align_value(wad_crl_offset + self.wad_crl_size)
wad_tmd_offset = align_value(wad_tik_offset + self.wad_tik_size) wad_tmd_offset = _align_value(wad_tik_offset + self.wad_tik_size)
# meta isn't guaranteed to be used, but some older SDK titles use it, and not reading it breaks things. # meta isn't guaranteed to be used, but some older SDK titles use it, and not reading it breaks things.
wad_meta_offset = align_value(wad_tmd_offset + self.wad_tmd_size) wad_meta_offset = _align_value(wad_tmd_offset + self.wad_tmd_size)
wad_content_offset = align_value(wad_meta_offset + self.wad_meta_size) wad_content_offset = _align_value(wad_meta_offset + self.wad_meta_size)
# ==================================================================================== # ====================================================================================
# Load data for each WAD section based on the previously calculated offsets. # Load data for each WAD section based on the previously calculated offsets.
# ==================================================================================== # ====================================================================================
@ -159,25 +159,25 @@ class WAD:
wad_data += int.to_bytes(self.wad_content_size, 4) wad_data += int.to_bytes(self.wad_content_size, 4)
# WAD meta size. # WAD meta size.
wad_data += int.to_bytes(self.wad_meta_size, 4) wad_data += int.to_bytes(self.wad_meta_size, 4)
wad_data = pad_bytes(wad_data) wad_data = _pad_bytes(wad_data)
# Retrieve the cert data and write it out. # Retrieve the cert data and write it out.
wad_data += self.get_cert_data() wad_data += self.get_cert_data()
wad_data = pad_bytes(wad_data) wad_data = _pad_bytes(wad_data)
# Retrieve the crl data and write it out. # Retrieve the crl data and write it out.
wad_data += self.get_crl_data() wad_data += self.get_crl_data()
wad_data = pad_bytes(wad_data) wad_data = _pad_bytes(wad_data)
# Retrieve the ticket data and write it out. # Retrieve the ticket data and write it out.
wad_data += self.get_ticket_data() wad_data += self.get_ticket_data()
wad_data = pad_bytes(wad_data) wad_data = _pad_bytes(wad_data)
# Retrieve the TMD data and write it out. # Retrieve the TMD data and write it out.
wad_data += self.get_tmd_data() wad_data += self.get_tmd_data()
wad_data = pad_bytes(wad_data) wad_data = _pad_bytes(wad_data)
# Retrieve the meta/footer data and write it out. # Retrieve the meta/footer data and write it out.
wad_data += self.get_meta_data() wad_data += self.get_meta_data()
wad_data = pad_bytes(wad_data) wad_data = _pad_bytes(wad_data)
# Retrieve the content data and write it out. # Retrieve the content data and write it out.
wad_data += self.get_content_data() wad_data += self.get_content_data()
wad_data = pad_bytes(wad_data) wad_data = _pad_bytes(wad_data)
return wad_data return wad_data
def get_wad_type(self) -> str: def get_wad_type(self) -> str:

View File

@ -28,24 +28,3 @@ 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
@dataclass
class TitleLimit:
"""
A TitleLimit object that contains the type of restriction and the limit. The limit type can be one of the following:
0 = None, 1 = Time Limit, 3 = None, or 4 = Launch Count. The maximum usage is then either the time in minutes the
title can be played or the maximum number of launches allowed for that title, based on the type of limit applied.
Attributes
----------
limit_type : int
The type of play limit applied.
maximum_usage : int
The maximum value for the type of play limit applied.
"""
# The type of play limit applied.
# 0 = None, 1 = Time Limit, 3 = None, 4 = Launch Count
limit_type: int
# The maximum value of the limit applied.
maximum_usage: int

View File

@ -3,18 +3,18 @@
import unittest import unittest
from libWiiPy import commonkeys from libWiiPy import title
class TestCommonKeys(unittest.TestCase): class TestCommonKeys(unittest.TestCase):
def test_common(self): def test_common(self):
self.assertEqual(commonkeys.get_common_key(0), b'\xeb\xe4*"^\x85\x93\xe4H\xd9\xc5Es\x81\xaa\xf7') self.assertEqual(title.get_common_key(0), b'\xeb\xe4*"^\x85\x93\xe4H\xd9\xc5Es\x81\xaa\xf7')
def test_korean(self): def test_korean(self):
self.assertEqual(commonkeys.get_common_key(1), b'c\xb8+\xb4\xf4aN.\x13\xf2\xfe\xfb\xbaL\x9b~') self.assertEqual(title.get_common_key(1), b'c\xb8+\xb4\xf4aN.\x13\xf2\xfe\xfb\xbaL\x9b~')
def test_vwii(self): def test_vwii(self):
self.assertEqual(commonkeys.get_common_key(2), b'0\xbf\xc7n|\x19\xaf\xbb#\x1630\xce\xd7\xc2\x8d') self.assertEqual(title.get_common_key(2), b'0\xbf\xc7n|\x19\xaf\xbb#\x1630\xce\xd7\xc2\x8d')
if __name__ == '__main__': if __name__ == '__main__':