mirror of
https://github.com/NinjaCheetah/libWiiPy.git
synced 2026-03-05 08:35:28 -05:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6a81722ec5 | |||
| ecc68d9e57 | |||
| c42dc66209 | |||
| 045613216a | |||
| 98666285db | |||
| ba320a29de | |||
| 9890a6dbac | |||
|
c92a8096ea
|
|||
|
99a55a3de5
|
|||
| 4a3e9f8e7f | |||
| 8eeebd1d75 | |||
|
3b7a2d09b0
|
|||
|
a85beac602
|
|||
| 338446efcb | |||
|
ccbc2e262b
|
|||
|
|
17a894dc0d | ||
| 60918f1a39 | |||
|
fa6c9eb740
|
11
README.md
11
README.md
@@ -10,6 +10,7 @@ libWiiPy is inspired by [libWiiSharp](https://github.com/TheShadowEevee/libWiiSh
|
|||||||
This list will expand as libWiiPy is developed, but these features are currently available:
|
This list will expand as libWiiPy is developed, but these features are currently available:
|
||||||
- TMD, ticket, and WAD parsing
|
- TMD, ticket, and WAD parsing
|
||||||
- WAD content extraction, decryption, re-encryption, and packing
|
- WAD content extraction, decryption, re-encryption, and packing
|
||||||
|
- Downloading titles from the NUS
|
||||||
|
|
||||||
# Usage
|
# Usage
|
||||||
A wiki, and in the future a potential documenation site, is being worked on, and can be accessed [here](https://github.com/NinjaCheetah/libWiiPy/wiki). It is currently fairly barebones, but it will be improved in the future.
|
A wiki, and in the future a potential documenation site, is being worked on, and can be accessed [here](https://github.com/NinjaCheetah/libWiiPy/wiki). It is currently fairly barebones, but it will be improved in the future.
|
||||||
@@ -30,27 +31,27 @@ Please be aware that because libWiiPy is in a very early state right now, many f
|
|||||||
To build this package locally, the steps are quite simple, and should apply to all platforms. Make sure you've set up your `venv` first!
|
To build this package locally, the steps are quite simple, and should apply to all platforms. Make sure you've set up your `venv` first!
|
||||||
|
|
||||||
First, install the dependencies from `requirements.txt`:
|
First, install the dependencies from `requirements.txt`:
|
||||||
```py
|
```sh
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, build the package using the Python `build` module:
|
Then, build the package using the Python `build` module:
|
||||||
```py
|
```sh
|
||||||
python -m build
|
python -m build
|
||||||
```
|
```
|
||||||
|
|
||||||
And that's all! You'll find your compiled pip package in `dist/`.
|
And that's all! You'll find your compiled pip package in `dist/`.
|
||||||
|
|
||||||
# Special Thanks
|
# Special Thanks
|
||||||
This project wouldn't be possible without the amazing people behind its predecessors and all of the people who have contributed to the documentation of the Wii's inner workings over at [Wiibrew](https://wiibrew.org).
|
This project wouldn't be possible without the amazing people behind its predecessors and all of the people who have contributed to the documentation of the Wii's inner workings over at [WiiBrew](https://wiibrew.org).
|
||||||
|
|
||||||
## Special Thanks for the Inspiration and Previous Projects
|
## Special Thanks for the Inspiration and Previous Projects
|
||||||
- Xuzz, SquidMan, megazig, Matt_P, Omega and The Lemon Man for creating Wii.py
|
- Xuzz, SquidMan, megazig, Matt_P, Omega and The Lemon Man for creating Wii.py
|
||||||
- Leathl for creating libWiiSharp
|
- Leathl for creating libWiiSharp
|
||||||
- TheShadowEevee for maintaining libWiiSharp
|
- TheShadowEevee for maintaining libWiiSharp
|
||||||
|
|
||||||
## Special Thanks to Wiibrew Contributors
|
## Special Thanks to WiiBrew Contributors
|
||||||
Thank you to all of the contributors to the documentation on the Wiibrew pages that make this all understandable! Some of the key articles referenced are as follows:
|
Thank you to all of the contributors to the documentation on the WiiBrew pages that make this all understandable! Some of the key articles referenced are as follows:
|
||||||
- [Title metadata](https://wiibrew.org/wiki/Title_metadata), for the documentation on how a TMD is structured
|
- [Title metadata](https://wiibrew.org/wiki/Title_metadata), for the documentation on how a TMD is structured
|
||||||
- [WAD files](https://wiibrew.org/wiki/WAD_files), for the documentation on how a WAD is structured
|
- [WAD files](https://wiibrew.org/wiki/WAD_files), for the documentation on how a WAD is structured
|
||||||
- [IOS history](https://wiibrew.org/wiki/IOS_history), for the documentation on IOS TIDs and how IOS is versioned
|
- [IOS history](https://wiibrew.org/wiki/IOS_history), for the documentation on IOS TIDs and how IOS is versioned
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "libWiiPy"
|
name = "libWiiPy"
|
||||||
version = "0.2.0"
|
version = "0.2.3"
|
||||||
authors = [
|
authors = [
|
||||||
{ name="NinjaCheetah", email="ninjacheetah@ncxprogramming.com" },
|
{ name="NinjaCheetah", email="ninjacheetah@ncxprogramming.com" },
|
||||||
{ name="Lillian Skinner", email="lillian@randommeaninglesscharacters.com" }
|
{ name="Lillian Skinner", email="lillian@randommeaninglesscharacters.com" }
|
||||||
@@ -15,6 +15,7 @@ classifiers = [
|
|||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pycryptodome",
|
"pycryptodome",
|
||||||
|
"requests"
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
build
|
build
|
||||||
pycryptodome
|
pycryptodome
|
||||||
|
requests
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
from .commonkeys import *
|
from .commonkeys import *
|
||||||
from .content import *
|
from .content import *
|
||||||
from .ticket import *
|
from .ticket import *
|
||||||
|
from .crypto import *
|
||||||
from .title import *
|
from .title import *
|
||||||
from .tmd import *
|
from .tmd import *
|
||||||
from .wad import *
|
from .wad import *
|
||||||
|
from .nus import *
|
||||||
|
|||||||
@@ -79,21 +79,18 @@ class ContentRegion:
|
|||||||
bytes
|
bytes
|
||||||
The full WAD file as bytes.
|
The full WAD file as bytes.
|
||||||
"""
|
"""
|
||||||
# Open the stream and begin writing data to it.
|
content_region_data = b''
|
||||||
with io.BytesIO() as content_region_data:
|
for content in self.content_list:
|
||||||
for content in self.content_list:
|
# Calculate padding after this content before the next one.
|
||||||
# Calculate padding after this content before the next one.
|
padding_bytes = 0
|
||||||
padding_bytes = 0
|
if (len(content) % 64) != 0:
|
||||||
if (len(content) % 64) != 0:
|
padding_bytes = 64 - (len(content) % 64)
|
||||||
padding_bytes = 64 - (len(content) % 64)
|
# Write content data, then the padding afterward if necessary.
|
||||||
# Write content data, then the padding afterward if necessary.
|
content_region_data += content
|
||||||
content_region_data.write(content)
|
if padding_bytes > 0:
|
||||||
if padding_bytes > 0:
|
content_region_data += b'\x00' * padding_bytes
|
||||||
content_region_data.write(b'\x00' * padding_bytes)
|
|
||||||
content_region_data.seek(0x0)
|
|
||||||
content_region_raw = content_region_data.read()
|
|
||||||
# Return the raw ContentRegion for the data contained in the object.
|
# Return the raw ContentRegion for the data contained in the object.
|
||||||
return content_region_raw
|
return content_region_data
|
||||||
|
|
||||||
def get_enc_content_by_index(self, index: int) -> bytes:
|
def get_enc_content_by_index(self, index: int) -> bytes:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -3,10 +3,12 @@
|
|||||||
|
|
||||||
import struct
|
import struct
|
||||||
from .commonkeys import get_common_key
|
from .commonkeys import get_common_key
|
||||||
|
from .shared import convert_tid_to_iv
|
||||||
|
|
||||||
from Crypto.Cipher import AES
|
from Crypto.Cipher import AES
|
||||||
|
|
||||||
|
|
||||||
def decrypt_title_key(title_key_enc, common_key_index, title_id) -> bytes:
|
def decrypt_title_key(title_key_enc: bytes, common_key_index: int, title_id: bytes | str) -> bytes:
|
||||||
"""
|
"""
|
||||||
Gets the decrypted version of the encrypted Title Key provided.
|
Gets the decrypted version of the encrypted Title Key provided.
|
||||||
|
|
||||||
@@ -17,9 +19,9 @@ def decrypt_title_key(title_key_enc, common_key_index, title_id) -> bytes:
|
|||||||
title_key_enc : bytes
|
title_key_enc : bytes
|
||||||
The encrypted Title Key.
|
The encrypted Title Key.
|
||||||
common_key_index : int
|
common_key_index : int
|
||||||
The index of the common key to be returned.
|
The index of the common key used to encrypt the Title Key.
|
||||||
title_id : bytes
|
title_id : bytes, str
|
||||||
The title ID of the title that the key is for.
|
The Title ID of the title that the key is for.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
@@ -28,8 +30,10 @@ def decrypt_title_key(title_key_enc, common_key_index, title_id) -> bytes:
|
|||||||
"""
|
"""
|
||||||
# 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)
|
||||||
# Calculate the IV by adding 8 bytes to the end of the Title ID.
|
# Convert the IV into the correct format based on the type provided.
|
||||||
title_key_iv = title_id + (b'\x00' * 8)
|
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.
|
||||||
|
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.
|
||||||
@@ -37,6 +41,39 @@ def decrypt_title_key(title_key_enc, common_key_index, title_id) -> bytes:
|
|||||||
return title_key
|
return title_key
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt_title_key(title_key_dec: bytes, common_key_index: int, title_id: bytes | str) -> bytes:
|
||||||
|
"""
|
||||||
|
Encrypts the provided Title Key with the selected common key.
|
||||||
|
|
||||||
|
Requires the index of the common key to use, and the Title ID of the title that the Title Key is for.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
title_key_dec : bytes
|
||||||
|
The decrypted Title Key.
|
||||||
|
common_key_index : int
|
||||||
|
The index of the common key used to encrypt the Title Key.
|
||||||
|
title_id : bytes, str
|
||||||
|
The Title ID of the title that the key is for.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bytes
|
||||||
|
An encrypted Title Key.
|
||||||
|
"""
|
||||||
|
# Load the correct common key for the title.
|
||||||
|
common_key = get_common_key(common_key_index)
|
||||||
|
# Convert the IV into the correct format based on the type provided.
|
||||||
|
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.
|
||||||
|
title_key_iv = title_key_iv + (b'\x00' * 8)
|
||||||
|
# Create a new AES object with the values provided.
|
||||||
|
aes = AES.new(common_key, AES.MODE_CBC, title_key_iv)
|
||||||
|
# Encrypt Title Key using the AES object.
|
||||||
|
title_key = aes.encrypt(title_key_dec)
|
||||||
|
return title_key
|
||||||
|
|
||||||
|
|
||||||
def decrypt_content(content_enc, title_key, content_index, content_length) -> bytes:
|
def decrypt_content(content_enc, title_key, content_index, content_length) -> bytes:
|
||||||
"""
|
"""
|
||||||
Gets the decrypted version of the encrypted content.
|
Gets the decrypted version of the encrypted content.
|
||||||
@@ -72,8 +109,7 @@ def decrypt_content(content_enc, title_key, content_index, content_length) -> by
|
|||||||
# 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.
|
||||||
while len(content_dec) > content_length:
|
content_dec = content_dec[:content_length]
|
||||||
content_dec = content_dec[:-1]
|
|
||||||
return content_dec
|
return content_dec
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
233
src/libWiiPy/nus.py
Normal file
233
src/libWiiPy/nus.py
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
# "nus.py" from libWiiPy by NinjaCheetah & Contributors
|
||||||
|
# https://github.com/NinjaCheetah/libWiiPy
|
||||||
|
#
|
||||||
|
# See https://wiibrew.org/wiki/NUS for details about the NUS
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import hashlib
|
||||||
|
from typing import List
|
||||||
|
from .title import Title
|
||||||
|
from .tmd import TMD
|
||||||
|
from .ticket import Ticket
|
||||||
|
|
||||||
|
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:
|
||||||
|
"""
|
||||||
|
Download an entire title and all of its contents, then load the downloaded components into a Title object for
|
||||||
|
further use. This method is NOT recommended for general use, as it has absolutely no verbosity. It is instead
|
||||||
|
recommended to call the individual download methods instead to provide more flexibility and output.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
title_id : str
|
||||||
|
The Title ID of the title to download.
|
||||||
|
title_version : int, option
|
||||||
|
The version of the title to download. Defaults to latest if not set.
|
||||||
|
wiiu_endpoint : bool, option
|
||||||
|
Whether the Wii U endpoint for the NUS should be used or not. This increases download speeds. Defaults to False.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Title
|
||||||
|
A Title object containing all the data from the downloaded title.
|
||||||
|
"""
|
||||||
|
# First, create the new title.
|
||||||
|
title = Title()
|
||||||
|
# Download and load the TMD, Ticket, and certs.
|
||||||
|
title.load_tmd(download_tmd(title_id, title_version, wiiu_endpoint))
|
||||||
|
title.load_ticket(download_ticket(title_id, wiiu_endpoint))
|
||||||
|
title.wad.set_cert_data(download_cert(wiiu_endpoint))
|
||||||
|
# Download all contents
|
||||||
|
title.load_content_records()
|
||||||
|
title.content.content_list = download_contents(title_id, title.tmd, wiiu_endpoint)
|
||||||
|
# Return the completed title.
|
||||||
|
return title
|
||||||
|
|
||||||
|
|
||||||
|
def download_tmd(title_id: str, title_version: int = None, wiiu_endpoint: bool = False) -> bytes:
|
||||||
|
"""
|
||||||
|
Downloads the TMD of the Title specified in the object. Will download the latest version by default, or another
|
||||||
|
version if it was manually specified in the object.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
title_id : str
|
||||||
|
The Title ID of the title to download the TMD for.
|
||||||
|
title_version : int, option
|
||||||
|
The version of the TMD to download. Defaults to latest if not set.
|
||||||
|
wiiu_endpoint : bool, option
|
||||||
|
Whether the Wii U endpoint for the NUS should be used or not. This increases download speeds. Defaults to False.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bytes
|
||||||
|
The TMD file from the NUS.
|
||||||
|
"""
|
||||||
|
# Build the download URL. The structure is download/<TID>/tmd for latest and download/<TID>/tmd.<version> for
|
||||||
|
# when a specific version is requested.
|
||||||
|
if wiiu_endpoint is False:
|
||||||
|
tmd_url = nus_endpoint[0] + title_id + "/tmd"
|
||||||
|
else:
|
||||||
|
tmd_url = nus_endpoint[1] + title_id + "/tmd"
|
||||||
|
# Add the version to the URL if one was specified.
|
||||||
|
if title_version is not None:
|
||||||
|
tmd_url += "." + str(title_version)
|
||||||
|
# Make the request.
|
||||||
|
tmd_request = requests.get(url=tmd_url, headers={'User-Agent': 'wii libnup/1.0'}, stream=True)
|
||||||
|
# Handle a 404 if the TID/version doesn't exist.
|
||||||
|
if tmd_request.status_code != 200:
|
||||||
|
raise ValueError("The requested Title ID or TMD version does not exist. Please check the Title ID and Title"
|
||||||
|
" version and then try again.")
|
||||||
|
# Save the raw TMD.
|
||||||
|
raw_tmd = tmd_request.content
|
||||||
|
# Use a TMD object to load the data and then return only the actual TMD.
|
||||||
|
tmd_temp = TMD()
|
||||||
|
tmd_temp.load(raw_tmd)
|
||||||
|
tmd = tmd_temp.dump()
|
||||||
|
return tmd
|
||||||
|
|
||||||
|
|
||||||
|
def download_ticket(title_id: str, wiiu_endpoint: bool = False) -> bytes:
|
||||||
|
"""
|
||||||
|
Downloads the Ticket of the Title specified in the object. This will only work if the Title ID specified is for
|
||||||
|
a free title.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
title_id : str
|
||||||
|
The Title ID of the title to download the Ticket for.
|
||||||
|
wiiu_endpoint : bool, option
|
||||||
|
Whether the Wii U endpoint for the NUS should be used or not. This increases download speeds. Defaults to False.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bytes
|
||||||
|
The Ticket file from the NUS.
|
||||||
|
"""
|
||||||
|
# Build the download URL. The structure is download/<TID>/cetk, and cetk will only exist if this is a free
|
||||||
|
# title.
|
||||||
|
if wiiu_endpoint is False:
|
||||||
|
ticket_url = nus_endpoint[0] + title_id + "/cetk"
|
||||||
|
else:
|
||||||
|
ticket_url = nus_endpoint[1] + title_id + "/cetk"
|
||||||
|
# Make the request.
|
||||||
|
ticket_request = requests.get(url=ticket_url, headers={'User-Agent': 'wii libnup/1.0'}, stream=True)
|
||||||
|
if ticket_request.status_code != 200:
|
||||||
|
raise ValueError("The requested Title ID does not exist, or refers to a non-free title. Tickets can only"
|
||||||
|
" be downloaded for titles that are free on the NUS.")
|
||||||
|
# Save the raw cetk file.
|
||||||
|
cetk = ticket_request.content
|
||||||
|
# Use a Ticket object to load only the Ticket data from cetk and return it.
|
||||||
|
ticket_temp = Ticket()
|
||||||
|
ticket_temp.load(cetk)
|
||||||
|
ticket = ticket_temp.dump()
|
||||||
|
return ticket
|
||||||
|
|
||||||
|
|
||||||
|
def download_cert(wiiu_endpoint: bool = False) -> bytes:
|
||||||
|
"""
|
||||||
|
Downloads the signing certificate used by all WADs. This uses System Menu 4.3U as the source.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
wiiu_endpoint : bool, option
|
||||||
|
Whether the Wii U endpoint for the NUS should be used or not. This increases download speeds. Defaults to False.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bytes
|
||||||
|
The cert file.
|
||||||
|
"""
|
||||||
|
# Download the TMD and cetk for the System Menu 4.3U.
|
||||||
|
if wiiu_endpoint is False:
|
||||||
|
tmd_url = nus_endpoint[0] + "0000000100000002/tmd.513"
|
||||||
|
cetk_url = nus_endpoint[0] + "0000000100000002/cetk"
|
||||||
|
else:
|
||||||
|
tmd_url = nus_endpoint[1] + "0000000100000002/tmd.513"
|
||||||
|
cetk_url = nus_endpoint[1] + "0000000100000002/cetk"
|
||||||
|
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
|
||||||
|
# Assemble the certificate.
|
||||||
|
cert = b''
|
||||||
|
# Certificate Authority data.
|
||||||
|
cert += cetk[0x2A4 + 768:]
|
||||||
|
# Certificate Policy data.
|
||||||
|
cert += tmd[0x328:0x328 + 768]
|
||||||
|
# XS data.
|
||||||
|
cert += cetk[0x2A4:0x2A4 + 768]
|
||||||
|
# Since the cert is always the same, check the hash to make sure nothing went wildly wrong.
|
||||||
|
if hashlib.sha1(cert).hexdigest() != "ace0f15d2a851c383fe4657afc3840d6ffe30ad0":
|
||||||
|
raise Exception("An unknown error has occurred downloading and creating the certificate.")
|
||||||
|
return cert
|
||||||
|
|
||||||
|
|
||||||
|
def download_content(title_id: str, content_id: int, wiiu_endpoint: bool = False) -> bytes:
|
||||||
|
"""
|
||||||
|
Downloads a specified content for the title specified in the object.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
title_id : str
|
||||||
|
The Title ID of the title to download content from.
|
||||||
|
content_id : int
|
||||||
|
The Content ID of the content you wish to download.
|
||||||
|
wiiu_endpoint : bool, option
|
||||||
|
Whether the Wii U endpoint for the NUS should be used or not. This increases download speeds. Defaults to False.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bytes
|
||||||
|
The downloaded content.
|
||||||
|
"""
|
||||||
|
# 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 wiiu_endpoint is False:
|
||||||
|
content_url = nus_endpoint[0] + title_id + "/000000" + content_id_hex
|
||||||
|
else:
|
||||||
|
content_url = nus_endpoint[1] + title_id + "/000000" + content_id_hex
|
||||||
|
# Make the request.
|
||||||
|
content_request = requests.get(url=content_url, headers={'User-Agent': 'wii libnup/1.0'}, stream=True)
|
||||||
|
if content_request.status_code != 200:
|
||||||
|
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)
|
||||||
|
content_data = content_request.content
|
||||||
|
return content_data
|
||||||
|
|
||||||
|
|
||||||
|
def download_contents(title_id: str, tmd: TMD, wiiu_endpoint: bool = False) -> List[bytes]:
|
||||||
|
"""
|
||||||
|
Downloads all the contents for the title specified in the object. This requires a TMD to already be available
|
||||||
|
so that the content records can be accessed.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
title_id : str
|
||||||
|
The Title ID of the title to download content from.
|
||||||
|
tmd : TMD
|
||||||
|
The TMD that matches the title that the contents being downloaded are from.
|
||||||
|
wiiu_endpoint : bool, option
|
||||||
|
Whether the Wii U endpoint for the NUS should be used or not. This increases download speeds. Defaults to False.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
List[bytes]
|
||||||
|
A list of all the downloaded contents.
|
||||||
|
"""
|
||||||
|
# Retrieve the content records from the TMD.
|
||||||
|
content_records = tmd.content_records
|
||||||
|
# Create a list of Content IDs to download.
|
||||||
|
content_ids = []
|
||||||
|
for content_record in content_records:
|
||||||
|
content_ids.append(content_record.content_id)
|
||||||
|
# Iterate over that list and download each content in it, then add it to the array of contents.
|
||||||
|
content_list = []
|
||||||
|
for content_id in content_ids:
|
||||||
|
# Call self.download_content() for each Content ID.
|
||||||
|
content = download_content(title_id, content_id, wiiu_endpoint)
|
||||||
|
content_list.append(content)
|
||||||
|
return content_list
|
||||||
@@ -4,6 +4,9 @@
|
|||||||
# 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).
|
||||||
@@ -26,22 +29,43 @@ def align_value(value, alignment=64) -> int:
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
def pad_bytes_stream(data, alignment=64) -> bytes:
|
def pad_bytes(data, alignment=64) -> bytes:
|
||||||
"""
|
"""
|
||||||
Pads the provided bytes stream to the provided alignment (defaults to 64).
|
Pads the provided bytes object to the provided alignment (defaults to 64).
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
data : BytesIO
|
data : bytes
|
||||||
The data to align.
|
The data to align.
|
||||||
alignment : int
|
alignment : int
|
||||||
The number to align to. Defaults to 64.
|
The number to align to. Defaults to 64.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
BytesIO
|
bytes
|
||||||
The aligned data.
|
The aligned data.
|
||||||
"""
|
"""
|
||||||
while (data.getbuffer().nbytes % alignment) != 0:
|
while (len(data) % alignment) != 0:
|
||||||
data.write(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
|
||||||
|
|||||||
@@ -99,10 +99,9 @@ class Ticket:
|
|||||||
self.console_id = int.from_bytes(ticket_data.read(4))
|
self.console_id = int.from_bytes(ticket_data.read(4))
|
||||||
# Title ID.
|
# Title ID.
|
||||||
ticket_data.seek(0x1DC)
|
ticket_data.seek(0x1DC)
|
||||||
self.title_id = ticket_data.read(8)
|
self.title_id = binascii.hexlify(ticket_data.read(8))
|
||||||
# Title ID (as a string).
|
# Title ID (as a string).
|
||||||
title_id_hex = binascii.hexlify(self.title_id)
|
self.title_id_str = str(self.title_id.decode())
|
||||||
self.title_id_str = str(title_id_hex.decode())
|
|
||||||
# Unknown data 1.
|
# Unknown data 1.
|
||||||
ticket_data.seek(0x1E4)
|
ticket_data.seek(0x1E4)
|
||||||
self.unknown1 = ticket_data.read(2)
|
self.unknown1 = ticket_data.read(2)
|
||||||
@@ -147,68 +146,62 @@ class Ticket:
|
|||||||
bytes
|
bytes
|
||||||
The full Ticket file as bytes.
|
The full Ticket file as bytes.
|
||||||
"""
|
"""
|
||||||
# Open the stream and begin writing to it.
|
ticket_data = b''
|
||||||
with io.BytesIO() as ticket_data:
|
# Signature type.
|
||||||
# Signature type.
|
ticket_data += self.signature_type
|
||||||
ticket_data.write(self.signature_type)
|
# Signature data.
|
||||||
# Signature data.
|
ticket_data += self.signature
|
||||||
ticket_data.write(self.signature)
|
# Padding to 64 bytes.
|
||||||
# Padding to 64 bytes.
|
ticket_data += b'\x00' * 60
|
||||||
ticket_data.write(b'\x00' * 60)
|
# Signature issuer.
|
||||||
# Signature issuer.
|
ticket_data += str.encode(self.signature_issuer)
|
||||||
ticket_data.write(str.encode(self.signature_issuer))
|
# ECDH data.
|
||||||
# ECDH data.
|
ticket_data += self.ecdh_data
|
||||||
ticket_data.write(self.ecdh_data)
|
# Ticket version.
|
||||||
# Ticket version.
|
ticket_data += int.to_bytes(self.ticket_version, 1)
|
||||||
ticket_data.write(int.to_bytes(self.ticket_version, 1))
|
# Reserved (all \0x00).
|
||||||
# Reserved (all \0x00).
|
ticket_data += b'\x00\x00'
|
||||||
ticket_data.write(b'\x00\x00')
|
# Title Key.
|
||||||
# Title Key.
|
ticket_data += self.title_key_enc
|
||||||
ticket_data.write(self.title_key_enc)
|
# Unknown (write \0x00).
|
||||||
# Unknown (write \0x00).
|
ticket_data += b'\x00'
|
||||||
ticket_data.write(b'\x00')
|
# Ticket ID.
|
||||||
# Ticket ID.
|
ticket_data += self.ticket_id
|
||||||
ticket_data.write(self.ticket_id)
|
# Console ID.
|
||||||
# Console ID.
|
ticket_data += int.to_bytes(self.console_id, 4)
|
||||||
ticket_data.write(int.to_bytes(self.console_id, 4))
|
# Title ID.
|
||||||
# Title ID.
|
ticket_data += binascii.unhexlify(self.title_id)
|
||||||
ticket_data.write(self.title_id)
|
# Unknown data 1.
|
||||||
# Unknown data 1.
|
ticket_data += self.unknown1
|
||||||
ticket_data.write(self.unknown1)
|
# Title version.
|
||||||
# Title version.
|
title_version_high = round(self.title_version / 256)
|
||||||
title_version_high = round(self.title_version / 256)
|
ticket_data += int.to_bytes(title_version_high, 1)
|
||||||
ticket_data.write(int.to_bytes(title_version_high, 1))
|
title_version_low = self.title_version % 256
|
||||||
title_version_low = self.title_version % 256
|
ticket_data += int.to_bytes(title_version_low, 1)
|
||||||
ticket_data.write(int.to_bytes(title_version_low, 1))
|
# Permitted titles mask.
|
||||||
# Permitted titles mask.
|
ticket_data += self.permitted_titles
|
||||||
ticket_data.write(self.permitted_titles)
|
# Permit mask.
|
||||||
# Permit mask.
|
ticket_data += self.permit_mask
|
||||||
ticket_data.write(self.permit_mask)
|
# Title Export allowed.
|
||||||
# Title Export allowed.
|
ticket_data += int.to_bytes(self.title_export_allowed, 1)
|
||||||
ticket_data.write(int.to_bytes(self.title_export_allowed, 1))
|
# Common Key index.
|
||||||
# Common Key index.
|
ticket_data += int.to_bytes(self.common_key_index, 1)
|
||||||
ticket_data.write(int.to_bytes(self.common_key_index, 1))
|
# Unknown data 2.
|
||||||
# Unknown data 2.
|
ticket_data += self.unknown2
|
||||||
ticket_data.write(self.unknown2)
|
# Content access permissions.
|
||||||
# Content access permissions.
|
ticket_data += self.content_access_permissions
|
||||||
ticket_data.write(self.content_access_permissions)
|
# Padding (always \x00).
|
||||||
# Padding (always \x00).
|
ticket_data += b'\x00\x00'
|
||||||
ticket_data.write(b'\x00\x00')
|
# Iterate over Title Limit objects, write them back into raw data, then add them to the Ticket.
|
||||||
# Iterate over Title Limit objects, write them back into raw data, then add them to the Ticket.
|
for title_limit in range(len(self.title_limits_list)):
|
||||||
for title_limit in range(len(self.title_limits_list)):
|
title_limit_data = b''
|
||||||
title_limit_data = io.BytesIO()
|
# Write all fields from the title limit entry.
|
||||||
# Write all fields from the title limit entry.
|
title_limit_data += int.to_bytes(self.title_limits_list[title_limit].limit_type, 4)
|
||||||
title_limit_data.write(int.to_bytes(self.title_limits_list[title_limit].limit_type, 4))
|
title_limit_data += int.to_bytes(self.title_limits_list[title_limit].maximum_usage, 4)
|
||||||
title_limit_data.write(int.to_bytes(self.title_limits_list[title_limit].maximum_usage, 4))
|
# Write the entry to the ticket.
|
||||||
# Seek to the start and write the entry to the Ticket.
|
ticket_data += title_limit_data
|
||||||
title_limit_data.seek(0x0)
|
|
||||||
ticket_data.write(title_limit_data.read())
|
|
||||||
title_limit_data.close()
|
|
||||||
# Set the Ticket attribute of the object to the new raw Ticket.
|
|
||||||
ticket_data.seek(0x0)
|
|
||||||
ticket_data_raw = ticket_data.read()
|
|
||||||
# Return the raw TMD for the data contained in the object.
|
# Return the raw TMD for the data contained in the object.
|
||||||
return ticket_data_raw
|
return ticket_data
|
||||||
|
|
||||||
def get_title_id(self) -> str:
|
def get_title_id(self) -> str:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -70,6 +70,9 @@ class Title:
|
|||||||
wad_data : bytes
|
wad_data : bytes
|
||||||
The raw data of the WAD.
|
The raw data of the WAD.
|
||||||
"""
|
"""
|
||||||
|
# Set WAD type to ib if the title being packed is boot2.
|
||||||
|
if self.tmd.title_id == "0000000100000001":
|
||||||
|
self.wad.wad_type = "ib"
|
||||||
# Dump the TMD and set it in the WAD.
|
# Dump the TMD and set it in the WAD.
|
||||||
self.wad.set_tmd_data(self.tmd.dump())
|
self.wad.set_tmd_data(self.tmd.dump())
|
||||||
# Dump the Ticket and set it in the WAD.
|
# Dump the Ticket and set it in the WAD.
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ class TMD:
|
|||||||
self.sig: bytes = b''
|
self.sig: bytes = b''
|
||||||
self.issuer: bytes = b'' # Follows the format "Root-CA%08x-CP%08x"
|
self.issuer: bytes = b'' # Follows the format "Root-CA%08x-CP%08x"
|
||||||
self.tmd_version: int = 0 # This seems to always be 0 no matter what?
|
self.tmd_version: int = 0 # This seems to always be 0 no matter what?
|
||||||
self.ca_crl_version: int = 0
|
self.ca_crl_version: int = 0 # Certificate Authority Certificate Revocation List version
|
||||||
self.signer_crl_version: int = 0
|
self.signer_crl_version: int = 0 # Certificate Policy Certificate Revocation List version
|
||||||
self.vwii: int = 0 # Whether the title is for the vWii. 0 = No, 1 = Yes
|
self.vwii: int = 0 # Whether the title is for the vWii. 0 = No, 1 = Yes
|
||||||
self.ios_tid: str = "" # The Title ID of the IOS version the associated title runs on.
|
self.ios_tid: str = "" # The Title ID of the IOS version the associated title runs on.
|
||||||
self.ios_version: int = 0 # The IOS version the associated title runs on.
|
self.ios_version: int = 0 # The IOS version the associated title runs on.
|
||||||
@@ -44,17 +44,19 @@ class TMD:
|
|||||||
self.content_type: str = "" # The type of content contained within the associated title.
|
self.content_type: str = "" # The type of content contained within the associated title.
|
||||||
self.group_id: int = 0 # The ID of the publisher of the associated title.
|
self.group_id: int = 0 # The ID of the publisher of the associated title.
|
||||||
self.region: int = 0 # The ID of the region of the associated title.
|
self.region: int = 0 # The ID of the region of the associated title.
|
||||||
self.ratings: bytes = b''
|
self.ratings: bytes = b'' # The parental controls rating of the associated title.
|
||||||
|
self.reserved1: bytes = b'' # Unknown data labeled "Reserved" on WiiBrew.
|
||||||
self.ipc_mask: bytes = b''
|
self.ipc_mask: bytes = b''
|
||||||
|
self.reserved2: bytes = b'' # Other "Reserved" data from WiiBrew.
|
||||||
self.access_rights: bytes = b''
|
self.access_rights: bytes = b''
|
||||||
self.title_version: int = 0 # The version of the associated title.
|
self.title_version: int = 0 # The version of the associated title.
|
||||||
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
|
self.boot_index: int = 0 # The content index that contains the bootable executable.
|
||||||
self.content_records: List[ContentRecord] = []
|
self.content_records: List[ContentRecord] = []
|
||||||
|
|
||||||
def load(self, tmd: bytes) -> None:
|
def load(self, tmd: bytes) -> None:
|
||||||
"""
|
"""
|
||||||
Loads raw TMD data and sets all attributes of the WAD object. This allows for manipulating an already
|
Loads raw TMD data and sets all attributes of the TMD object. This allows for manipulating an already
|
||||||
existing TMD.
|
existing TMD.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@@ -74,10 +76,10 @@ class TMD:
|
|||||||
# TMD version, seems to usually be 0, but I've seen references to other numbers.
|
# TMD version, seems to usually be 0, but I've seen references to other numbers.
|
||||||
tmd_data.seek(0x180)
|
tmd_data.seek(0x180)
|
||||||
self.tmd_version = int.from_bytes(tmd_data.read(1))
|
self.tmd_version = int.from_bytes(tmd_data.read(1))
|
||||||
# Root certificate crl version.
|
# Certificate Authority CRL version.
|
||||||
tmd_data.seek(0x181)
|
tmd_data.seek(0x181)
|
||||||
self.ca_crl_version = int.from_bytes(tmd_data.read(1))
|
self.ca_crl_version = int.from_bytes(tmd_data.read(1))
|
||||||
# Signer crl version.
|
# Certificate Policy CRL version.
|
||||||
tmd_data.seek(0x182)
|
tmd_data.seek(0x182)
|
||||||
self.signer_crl_version = int.from_bytes(tmd_data.read(1))
|
self.signer_crl_version = int.from_bytes(tmd_data.read(1))
|
||||||
# If this is a vWii title or not.
|
# If this is a vWii title or not.
|
||||||
@@ -103,16 +105,22 @@ class TMD:
|
|||||||
# Publisher of the title.
|
# Publisher of the title.
|
||||||
tmd_data.seek(0x198)
|
tmd_data.seek(0x198)
|
||||||
self.group_id = int.from_bytes(tmd_data.read(2))
|
self.group_id = int.from_bytes(tmd_data.read(2))
|
||||||
# Region of the title, 0 = JAP, 1 = USA, 2 = EUR, 3 = NONE, 4 = KOR.
|
# Region of the title, 0 = JAP, 1 = USA, 2 = EUR, 3 = WORLD, 4 = KOR.
|
||||||
tmd_data.seek(0x19C)
|
tmd_data.seek(0x19C)
|
||||||
region_hex = tmd_data.read(2)
|
region_hex = tmd_data.read(2)
|
||||||
self.region = int.from_bytes(region_hex)
|
self.region = int.from_bytes(region_hex)
|
||||||
# Likely the localized content rating for the title. (ESRB, CERO, PEGI, etc.)
|
# Content rating of the title for parental controls. Likely based on ESRB, CERO, PEGI, etc. rating.
|
||||||
tmd_data.seek(0x19E)
|
tmd_data.seek(0x19E)
|
||||||
self.ratings = tmd_data.read(16)
|
self.ratings = tmd_data.read(16)
|
||||||
|
# "Reserved" data 1.
|
||||||
|
tmd_data.seek(0x1AE)
|
||||||
|
self.reserved1 = tmd_data.read(12)
|
||||||
# IPC mask.
|
# IPC mask.
|
||||||
tmd_data.seek(0x1BA)
|
tmd_data.seek(0x1BA)
|
||||||
self.ipc_mask = tmd_data.read(12)
|
self.ipc_mask = tmd_data.read(12)
|
||||||
|
# "Reserved" data 2.
|
||||||
|
tmd_data.seek(0x1C6)
|
||||||
|
self.reserved2 = tmd_data.read(18)
|
||||||
# Access rights of the title; DVD-video access and AHBPROT.
|
# Access rights of the title; DVD-video access and AHBPROT.
|
||||||
tmd_data.seek(0x1D8)
|
tmd_data.seek(0x1D8)
|
||||||
self.access_rights = tmd_data.read(4)
|
self.access_rights = tmd_data.read(4)
|
||||||
@@ -125,7 +133,7 @@ class TMD:
|
|||||||
# The number of contents listed in the TMD.
|
# The number of contents listed in the TMD.
|
||||||
tmd_data.seek(0x1DE)
|
tmd_data.seek(0x1DE)
|
||||||
self.num_contents = int.from_bytes(tmd_data.read(2))
|
self.num_contents = int.from_bytes(tmd_data.read(2))
|
||||||
# Content index in content list that contains the boot file.
|
# The content index that contains the bootable executable.
|
||||||
tmd_data.seek(0x1E0)
|
tmd_data.seek(0x1E0)
|
||||||
self.boot_index = int.from_bytes(tmd_data.read(2))
|
self.boot_index = int.from_bytes(tmd_data.read(2))
|
||||||
# Get content records for the number of contents in num_contents.
|
# Get content records for the number of contents in num_contents.
|
||||||
@@ -148,78 +156,72 @@ class TMD:
|
|||||||
bytes
|
bytes
|
||||||
The full TMD file as bytes.
|
The full TMD file as bytes.
|
||||||
"""
|
"""
|
||||||
# Open the stream and begin writing to it.
|
tmd_data = b''
|
||||||
with io.BytesIO() as tmd_data:
|
# Signed blob header.
|
||||||
# Signed blob header.
|
tmd_data += self.blob_header
|
||||||
tmd_data.write(self.blob_header)
|
# Signing certificate issuer.
|
||||||
# Signing certificate issuer.
|
tmd_data += self.issuer
|
||||||
tmd_data.write(self.issuer)
|
# TMD version.
|
||||||
# TMD version.
|
tmd_data += int.to_bytes(self.tmd_version, 1)
|
||||||
tmd_data.write(int.to_bytes(self.tmd_version, 1))
|
# Certificate Authority CRL version.
|
||||||
# Root certificate crl version.
|
tmd_data += int.to_bytes(self.ca_crl_version, 1)
|
||||||
tmd_data.write(int.to_bytes(self.ca_crl_version, 1))
|
# Certificate Policy CRL version.
|
||||||
# Signer crl version.
|
tmd_data += int.to_bytes(self.signer_crl_version, 1)
|
||||||
tmd_data.write(int.to_bytes(self.signer_crl_version, 1))
|
# If this is a vWii title or not.
|
||||||
# If this is a vWii title or not.
|
tmd_data += int.to_bytes(self.vwii, 1)
|
||||||
tmd_data.write(int.to_bytes(self.vwii, 1))
|
# IOS Title ID.
|
||||||
# IOS Title ID.
|
tmd_data += binascii.unhexlify(self.ios_tid)
|
||||||
tmd_data.write(binascii.unhexlify(self.ios_tid))
|
# Title's Title ID.
|
||||||
# Title's Title ID.
|
tmd_data += binascii.unhexlify(self.title_id)
|
||||||
tmd_data.write(binascii.unhexlify(self.title_id))
|
# Content type.
|
||||||
# Content type.
|
tmd_data += binascii.unhexlify(self.content_type)
|
||||||
tmd_data.write(binascii.unhexlify(self.content_type))
|
# Group ID.
|
||||||
# Group ID.
|
tmd_data += int.to_bytes(self.group_id, 2)
|
||||||
tmd_data.write(int.to_bytes(self.group_id, 2))
|
# 2 bytes of zero for reasons.
|
||||||
# 2 bytes of zero for reasons.
|
tmd_data += b'\x00\x00'
|
||||||
tmd_data.write(b'\x00\x00')
|
# Region.
|
||||||
# Region.
|
tmd_data += int.to_bytes(self.region, 2)
|
||||||
tmd_data.write(int.to_bytes(self.region, 2))
|
# Parental Controls Ratings.
|
||||||
# Ratings.
|
tmd_data += self.ratings
|
||||||
tmd_data.write(self.ratings)
|
# "Reserved" 1.
|
||||||
# Reserved (all \x00).
|
tmd_data += self.reserved1
|
||||||
tmd_data.write(b'\x00' * 12)
|
# IPC mask.
|
||||||
# IPC mask.
|
tmd_data += self.ipc_mask
|
||||||
tmd_data.write(self.ipc_mask)
|
# "Reserved" 2.
|
||||||
# Reserved (all \x00).
|
tmd_data += self.reserved2
|
||||||
tmd_data.write(b'\x00' * 18)
|
# Access rights.
|
||||||
# Access rights.
|
tmd_data += self.access_rights
|
||||||
tmd_data.write(self.access_rights)
|
# Title version.
|
||||||
# Title version.
|
title_version_high = round(self.title_version / 256)
|
||||||
title_version_high = round(self.title_version / 256)
|
tmd_data += int.to_bytes(title_version_high, 1)
|
||||||
tmd_data.write(int.to_bytes(title_version_high, 1))
|
title_version_low = self.title_version % 256
|
||||||
title_version_low = self.title_version % 256
|
tmd_data += int.to_bytes(title_version_low, 1)
|
||||||
tmd_data.write(int.to_bytes(title_version_low, 1))
|
# Number of contents.
|
||||||
# Number of contents.
|
tmd_data += int.to_bytes(self.num_contents, 2)
|
||||||
tmd_data.write(int.to_bytes(self.num_contents, 2))
|
# Boot index.
|
||||||
# Boot index.
|
tmd_data += int.to_bytes(self.boot_index, 2)
|
||||||
tmd_data.write(int.to_bytes(self.boot_index, 2))
|
# Minor version. Unused so write \x00.
|
||||||
# Minor version. Unused so write \x00.
|
tmd_data += b'\x00\x00'
|
||||||
tmd_data.write(b'\x00\x00')
|
# Iterate over content records, write them back into raw data, then add them to the TMD.
|
||||||
# Iterate over content records, write them back into raw data, then add them to the TMD.
|
for content_record in range(self.num_contents):
|
||||||
for content_record in range(self.num_contents):
|
content_data = b''
|
||||||
content_data = io.BytesIO()
|
# Write all fields from the content record.
|
||||||
# Write all fields from the content record.
|
content_data += int.to_bytes(self.content_records[content_record].content_id, 4)
|
||||||
content_data.write(int.to_bytes(self.content_records[content_record].content_id, 4))
|
content_data += int.to_bytes(self.content_records[content_record].index, 2)
|
||||||
content_data.write(int.to_bytes(self.content_records[content_record].index, 2))
|
content_data += int.to_bytes(self.content_records[content_record].content_type, 2)
|
||||||
content_data.write(int.to_bytes(self.content_records[content_record].content_type, 2))
|
content_data += int.to_bytes(self.content_records[content_record].content_size, 8)
|
||||||
content_data.write(int.to_bytes(self.content_records[content_record].content_size, 8))
|
content_data += binascii.unhexlify(self.content_records[content_record].content_hash)
|
||||||
content_data.write(binascii.unhexlify(self.content_records[content_record].content_hash))
|
# Write the record to the TMD.
|
||||||
# Seek to the start and write the record to the TMD.
|
tmd_data += content_data
|
||||||
content_data.seek(0x0)
|
|
||||||
tmd_data.write(content_data.read())
|
|
||||||
content_data.close()
|
|
||||||
# Set the TMD attribute of the object to the new raw TMD.
|
|
||||||
tmd_data.seek(0x0)
|
|
||||||
tmd_data_raw = tmd_data.read()
|
|
||||||
# Return the raw TMD for the data contained in the object.
|
# Return the raw TMD for the data contained in the object.
|
||||||
return tmd_data_raw
|
return tmd_data
|
||||||
|
|
||||||
def get_title_region(self) -> str:
|
def get_title_region(self) -> str:
|
||||||
"""
|
"""
|
||||||
Gets the region of the TMD's associated title.
|
Gets the region of the TMD's associated title.
|
||||||
|
|
||||||
Can be one of several possible values:
|
Can be one of several possible values:
|
||||||
'JAP', 'USA', 'EUR', 'NONE', or 'KOR'.
|
'JAP', 'USA', 'EUR', 'WORLD', or 'KOR'.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
@@ -234,7 +236,7 @@ class TMD:
|
|||||||
case 2:
|
case 2:
|
||||||
return "EUR"
|
return "EUR"
|
||||||
case 3:
|
case 3:
|
||||||
return "NONE"
|
return "WORLD"
|
||||||
case 4:
|
case 4:
|
||||||
return "KOR"
|
return "KOR"
|
||||||
|
|
||||||
@@ -257,7 +259,7 @@ class TMD:
|
|||||||
Gets the type of the TMD's associated title.
|
Gets the type of the TMD's associated title.
|
||||||
|
|
||||||
Can be one of several possible values:
|
Can be one of several possible values:
|
||||||
'System', 'Game', 'Channel', 'SystemChannel', 'GameWithChannel', or 'HiddenChannel'
|
'System', 'Game', 'Channel', 'SystemChannel', 'GameChannel', or 'HiddenChannel'
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
@@ -275,7 +277,7 @@ class TMD:
|
|||||||
case '00010002':
|
case '00010002':
|
||||||
return "SystemChannel"
|
return "SystemChannel"
|
||||||
case '00010004':
|
case '00010004':
|
||||||
return "GameWithChannel"
|
return "GameChannel"
|
||||||
case '00010005':
|
case '00010005':
|
||||||
return "DLC"
|
return "DLC"
|
||||||
case '00010008':
|
case '00010008':
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import io
|
import io
|
||||||
import binascii
|
import binascii
|
||||||
from .shared import align_value, pad_bytes_stream
|
from .shared import align_value, pad_bytes
|
||||||
|
|
||||||
|
|
||||||
class WAD:
|
class WAD:
|
||||||
@@ -15,7 +15,7 @@ class WAD:
|
|||||||
Attributes
|
Attributes
|
||||||
----------
|
----------
|
||||||
wad_type : str
|
wad_type : str
|
||||||
The type of WAD, either ib for boot2 or Is for normal installable WADs. libWiiPy only supports Is currently.
|
The type of WAD, either ib for boot2 or Is for normal installable WADs.
|
||||||
wad_cert_size : int
|
wad_cert_size : int
|
||||||
The size of the WAD's certificate.
|
The size of the WAD's certificate.
|
||||||
wad_crl_size : int
|
wad_crl_size : int
|
||||||
@@ -60,15 +60,14 @@ class WAD:
|
|||||||
The data for the WAD you wish to load.
|
The data for the WAD you wish to load.
|
||||||
"""
|
"""
|
||||||
with io.BytesIO(wad_data) as wad_data:
|
with io.BytesIO(wad_data) as wad_data:
|
||||||
# Read the first 8 bytes of the file to ensure that it's a WAD. This will currently reject boot2 WADs, but
|
# Read the first 8 bytes of the file to ensure that it's a WAD. Has two possible valid values for the two
|
||||||
# this tool cannot handle them correctly right now anyway.
|
# different types of WADs that might be encountered.
|
||||||
wad_data.seek(0x0)
|
wad_data.seek(0x0)
|
||||||
wad_magic_bin = wad_data.read(8)
|
wad_magic_bin = wad_data.read(8)
|
||||||
wad_magic_hex = binascii.hexlify(wad_magic_bin)
|
wad_magic_hex = binascii.hexlify(wad_magic_bin)
|
||||||
wad_magic = str(wad_magic_hex.decode())
|
wad_magic = str(wad_magic_hex.decode())
|
||||||
if wad_magic != "0000002049730000":
|
if wad_magic != "0000002049730000" and wad_magic != "0000002069620000":
|
||||||
raise TypeError("This does not appear to be a valid WAD file, or is a boot2 WAD, which is not currently"
|
raise TypeError("This does not appear to be a valid WAD file.")
|
||||||
" supported by this library.")
|
|
||||||
# ====================================================================================
|
# ====================================================================================
|
||||||
# Get the sizes of each data region contained within the WAD.
|
# Get the sizes of each data region contained within the WAD.
|
||||||
# ====================================================================================
|
# ====================================================================================
|
||||||
@@ -141,50 +140,46 @@ class WAD:
|
|||||||
bytes
|
bytes
|
||||||
The full WAD file as bytes.
|
The full WAD file as bytes.
|
||||||
"""
|
"""
|
||||||
# Open the stream and begin writing data to it.
|
wad_data = b''
|
||||||
with io.BytesIO() as wad_data:
|
# Lead-in data.
|
||||||
# Lead-in data.
|
wad_data += b'\x00\x00\x00\x20'
|
||||||
wad_data.write(b'\x00\x00\x00\x20')
|
# WAD type.
|
||||||
# WAD type.
|
wad_data += str.encode(self.wad_type)
|
||||||
wad_data.write(str.encode(self.wad_type))
|
# WAD version.
|
||||||
# WAD version.
|
wad_data += self.wad_version
|
||||||
wad_data.write(self.wad_version)
|
# WAD cert size.
|
||||||
# WAD cert size.
|
wad_data += int.to_bytes(self.wad_cert_size, 4)
|
||||||
wad_data.write(int.to_bytes(self.wad_cert_size, 4))
|
# WAD crl size.
|
||||||
# WAD crl size.
|
wad_data += int.to_bytes(self.wad_crl_size, 4)
|
||||||
wad_data.write(int.to_bytes(self.wad_crl_size, 4))
|
# WAD ticket size.
|
||||||
# WAD ticket size.
|
wad_data += int.to_bytes(self.wad_tik_size, 4)
|
||||||
wad_data.write(int.to_bytes(self.wad_tik_size, 4))
|
# WAD TMD size.
|
||||||
# WAD TMD size.
|
wad_data += int.to_bytes(self.wad_tmd_size, 4)
|
||||||
wad_data.write(int.to_bytes(self.wad_tmd_size, 4))
|
# WAD content size.
|
||||||
# WAD content size.
|
wad_data += int.to_bytes(self.wad_content_size, 4)
|
||||||
wad_data.write(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.write(int.to_bytes(self.wad_meta_size, 4))
|
wad_data = pad_bytes(wad_data)
|
||||||
wad_data = pad_bytes_stream(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.write(self.get_cert_data())
|
wad_data = pad_bytes(wad_data)
|
||||||
wad_data = pad_bytes_stream(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.write(self.get_crl_data())
|
wad_data = pad_bytes(wad_data)
|
||||||
wad_data = pad_bytes_stream(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.write(self.get_ticket_data())
|
wad_data = pad_bytes(wad_data)
|
||||||
wad_data = pad_bytes_stream(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.write(self.get_tmd_data())
|
wad_data = pad_bytes(wad_data)
|
||||||
wad_data = pad_bytes_stream(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.write(self.get_meta_data())
|
wad_data = pad_bytes(wad_data)
|
||||||
wad_data = pad_bytes_stream(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.write(self.get_content_data())
|
wad_data = pad_bytes(wad_data)
|
||||||
wad_data = pad_bytes_stream(wad_data)
|
|
||||||
# Seek to the beginning and save this as the WAD data for the object.
|
|
||||||
wad_data.seek(0x0)
|
|
||||||
wad_data_raw = wad_data.read()
|
|
||||||
# Return the raw WAD file for the data contained in the object.
|
# Return the raw WAD file for the data contained in the object.
|
||||||
return wad_data_raw
|
return wad_data
|
||||||
|
|
||||||
def get_wad_type(self) -> str:
|
def get_wad_type(self) -> str:
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user