mirror of
https://github.com/NinjaCheetah/libWiiPy.git
synced 2026-03-05 00:25:29 -05:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
6b8eda7823
|
|||
|
af6977a23c
|
|||
|
96c975728b
|
|||
|
2fbb2b04f3
|
|||
| 195a005752 | |||
|
736a9e5c0c
|
|||
|
5743ee2695
|
|||
|
b30017460b
|
|||
| 2755364472 | |||
|
1d77868cb1
|
|||
|
ade4b68394
|
|||
|
bc9224e40b
|
|||
|
cbaafca0d1
|
|||
| ede33dc503 | |||
| 6a81722ec5 | |||
| ecc68d9e57 | |||
| c42dc66209 | |||
| 045613216a | |||
| 98666285db | |||
| ba320a29de | |||
| 9890a6dbac | |||
|
c92a8096ea
|
|||
|
99a55a3de5
|
|||
| 4a3e9f8e7f | |||
| 8eeebd1d75 | |||
|
3b7a2d09b0
|
|||
|
a85beac602
|
|||
| 338446efcb | |||
|
ccbc2e262b
|
|||
|
|
17a894dc0d | ||
| 60918f1a39 | |||
|
fa6c9eb740
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -161,9 +161,10 @@ cython_debug/
|
|||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
# Allows me to keep TMD files in my repository folder for testing without accidentally publishing them
|
# Relevant files that are used for testing libWiiPy's features.
|
||||||
*.tmd
|
*.tmd
|
||||||
*.wad
|
*.wad
|
||||||
|
*.arc
|
||||||
out_prod/
|
out_prod/
|
||||||
remakewad.pl
|
remakewad.pl
|
||||||
|
|
||||||
|
|||||||
21
README.md
21
README.md
@@ -1,15 +1,16 @@
|
|||||||

|

|
||||||
# libWiiPy
|
# libWiiPy
|
||||||
libWiiPy is a modern Python 3 library for interacting with and editing files from the Wii. It aims to be simple to use, well maintained, and offer as many features as reasonably possible in one library, so that a newly-written Python program could reasonably do 100% of its Wii-related work with just one library. It also aims to be fully cross-platform, so that any tools written with it can also be cross-platform.
|
libWiiPy is a modern Python 3 library for handling the various files and formats found on the Wii. It aims to be simple to use, well maintained, and offer as many features as reasonably possible in one library, so that a newly-written Python program could reasonably do 100% of its Wii-related work with just one library. It also aims to be fully cross-platform, so that any tools written with it can also be cross-platform.
|
||||||
|
|
||||||
libWiiPy is inspired by [libWiiSharp](https://github.com/TheShadowEevee/libWiiSharp), originally created by `Leathl`, now maintained by [@TheShadowEevee](https://github.com/TheShadowEevee). libWiiSharp is absolutely the way to go if you need a C# library for Wii files.
|
libWiiPy is inspired by [libWiiSharp](https://github.com/TheShadowEevee/libWiiSharp), which was originally created by `Leathl` and is now maintained by [@TheShadowEevee](https://github.com/TheShadowEevee). If you're looking for a Wii library that isn't in Python, then go check it out!
|
||||||
|
|
||||||
**Note:** While libWiiPy is directly inspired by libWiiSharp and aims to have feature parity with it, no code from either libWiiSharp or Wii.py was used in the making of this library. All code is original and is written by [@NinjaCheetah](https://github.com/NinjaCheetah), [@rvtr](https://github.com/rvtr), and any other GitHub contributors.
|
|
||||||
|
|
||||||
# Features
|
# Features
|
||||||
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
|
||||||
|
- Packing and unpacking U8 archives (.app, .arc, .carc, .szs)
|
||||||
|
|
||||||
# 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 to People Behind Related 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
|
||||||
@@ -58,3 +59,5 @@ Thank you to all of the contributors to the documentation on the Wiibrew pages t
|
|||||||
### One additional special thanks to [@DamiDoop](https://github.com/DamiDoop)!
|
### One additional special thanks to [@DamiDoop](https://github.com/DamiDoop)!
|
||||||
She made the very cool banner you can see at the top of this README, and has also helped greatly with my sanity throughout debugging this library.
|
She made the very cool banner you can see at the top of this README, and has also helped greatly with my sanity throughout debugging this library.
|
||||||
|
|
||||||
|
**Note:** While libWiiPy is directly inspired by libWiiSharp and aims to have feature parity with it, no code from either libWiiSharp or Wii.py was used in the making of this library. All code is original and is written by [@NinjaCheetah](https://github.com/NinjaCheetah), [@rvtr](https://github.com/rvtr), and any other GitHub contributors.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "libWiiPy"
|
name = "libWiiPy"
|
||||||
version = "0.2.0"
|
version = "0.3.0"
|
||||||
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,11 +15,12 @@ classifiers = [
|
|||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pycryptodome",
|
"pycryptodome",
|
||||||
|
"requests"
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
Homepage = "https://github.com/NinjaCheetah/libWiiPy"
|
Homepage = "https://github.com/NinjaCheetah/libWiiPy"
|
||||||
Issues = "https://github.com/NinjaCheetah/libWiipy/issues"
|
Issues = "https://github.com/NinjaCheetah/libWiiPy/issues"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["setuptools>=61.0"]
|
requires = ["setuptools>=61.0"]
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
build
|
build
|
||||||
pycryptodome
|
pycryptodome
|
||||||
|
requests
|
||||||
|
|||||||
0
src/__init__.py
Normal file
0
src/__init__.py
Normal file
@@ -1,11 +1,9 @@
|
|||||||
# "__init__.py" from libWiiPy by NinjaCheetah & Contributors
|
# "__init__.py" from libWiiPy by NinjaCheetah & Contributors
|
||||||
# https://github.com/NinjaCheetah/libWiiPy
|
# https://github.com/NinjaCheetah/libWiiPy
|
||||||
#
|
#
|
||||||
# These are the essential modules from libWiiPy that you'd probably want imported by default.
|
# These are the essential submodules from libWiiPy that you'd probably want imported by default.
|
||||||
|
|
||||||
from .commonkeys import *
|
__all__ = ["archive", "title"]
|
||||||
from .content import *
|
|
||||||
from .ticket import *
|
from . import archive
|
||||||
from .title import *
|
from . import title
|
||||||
from .tmd import *
|
|
||||||
from .wad import *
|
|
||||||
|
|||||||
4
src/libWiiPy/archive/__init__.py
Normal file
4
src/libWiiPy/archive/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# "archive/__init__.py" from libWiiPy by NinjaCheetah & Contributors
|
||||||
|
# https://github.com/NinjaCheetah/libWiiPy
|
||||||
|
|
||||||
|
from .u8 import *
|
||||||
305
src/libWiiPy/archive/u8.py
Normal file
305
src/libWiiPy/archive/u8.py
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
# "archive/u8.py" from libWiiPy by NinjaCheetah & Contributors
|
||||||
|
# https://github.com/NinjaCheetah/libWiiPy
|
||||||
|
#
|
||||||
|
# See https://wiibrew.org/wiki/U8_archive for details about the U8 archive format.
|
||||||
|
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import List
|
||||||
|
from ..shared import align_value
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
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
|
||||||
|
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.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
type : int
|
||||||
|
Whether this node refers to a file or a directory. Either 0x0000 for files, or 0x0100 for directories.
|
||||||
|
name_offset : int
|
||||||
|
The offset of the name of the file/directory this node refers to.
|
||||||
|
data_offset : int
|
||||||
|
The offset of the data for the file/directory this node refers to.
|
||||||
|
size : int
|
||||||
|
The size of the data for this node.
|
||||||
|
"""
|
||||||
|
type: int
|
||||||
|
name_offset: int
|
||||||
|
data_offset: int
|
||||||
|
size: int
|
||||||
|
|
||||||
|
|
||||||
|
class U8Archive:
|
||||||
|
def __init__(self):
|
||||||
|
"""
|
||||||
|
A U8 object that allows for managing the contents of a U8 archive.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
"""
|
||||||
|
self.u8_magic = b''
|
||||||
|
self.u8_node_list: List[U8Node] = [] # All the nodes in the header of a U8 file.
|
||||||
|
self.file_name_list: List[str] = []
|
||||||
|
self.file_data_list: List[bytes] = []
|
||||||
|
self.u8_file_structure = dict
|
||||||
|
|
||||||
|
def load(self, u8_data: bytes) -> None:
|
||||||
|
"""
|
||||||
|
Loads raw U8 data into a new U8 object. This allows for extracting the file and updating its contents.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
u8_data : bytes
|
||||||
|
The data for the U8 file to load.
|
||||||
|
"""
|
||||||
|
with io.BytesIO(u8_data) as u8_data:
|
||||||
|
# Read the first 4 bytes of the file to ensure that it's a U8 archive.
|
||||||
|
u8_data.seek(0x0)
|
||||||
|
self.u8_magic = u8_data.read(4)
|
||||||
|
if self.u8_magic != b'\x55\xAA\x38\x2D':
|
||||||
|
raise TypeError("This is not a valid U8 archive!")
|
||||||
|
# The following code is all skipped because these values really don't matter for extraction. They honestly
|
||||||
|
# really only matter to my code when they get calculated and used for packing.
|
||||||
|
|
||||||
|
# Offset of the root node, which will always be 0x20.
|
||||||
|
# root_node_offset = int(binascii.hexlify(u8_data.read(4)), 16)
|
||||||
|
# The size of the U8 header.
|
||||||
|
# header_size = int(binascii.hexlify(u8_data.read(4)), 16)
|
||||||
|
# The offset of the data, which is root_node_offset + header_size, aligned to 0x10.
|
||||||
|
# data_offset = int(binascii.hexlify(u8_data.read(4)), 16)
|
||||||
|
|
||||||
|
# Seek ahead to the size defined in the root node, because it's the total number of nodes in the file. The
|
||||||
|
# rest of the data in the root node (not that it really matters) will get read when we read the whole list.
|
||||||
|
u8_data.seek(u8_data.tell() + 36)
|
||||||
|
root_node_size = int.from_bytes(u8_data.read(4))
|
||||||
|
# Seek back before the root node so that it gets read with all the rest.
|
||||||
|
u8_data.seek(u8_data.tell() - 12)
|
||||||
|
# Iterate over the number of nodes that the root node lists.
|
||||||
|
for node in range(root_node_size):
|
||||||
|
node_type = 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_size = int.from_bytes(u8_data.read(4))
|
||||||
|
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.
|
||||||
|
name_base_offset = u8_data.tell()
|
||||||
|
for node in self.u8_node_list:
|
||||||
|
u8_data.seek(name_base_offset + node.name_offset)
|
||||||
|
name_bin = b''
|
||||||
|
while name_bin[-1:] != b'\x00':
|
||||||
|
name_bin += u8_data.read(1)
|
||||||
|
name_bin = name_bin[:-1]
|
||||||
|
name = str(name_bin.decode())
|
||||||
|
self.file_name_list.append(name)
|
||||||
|
if node.type == 0:
|
||||||
|
u8_data.seek(node.data_offset)
|
||||||
|
self.file_data_list.append(u8_data.read(node.size))
|
||||||
|
else:
|
||||||
|
self.file_data_list.append(b'')
|
||||||
|
|
||||||
|
def dump(self) -> bytes:
|
||||||
|
"""
|
||||||
|
Dumps the U8Archive object into the raw data of a U8 archive.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bytes
|
||||||
|
The full U8 archive as bytes.
|
||||||
|
"""
|
||||||
|
# This is 0 because the header size DOES NOT include the initial 32 bytes describing the file.
|
||||||
|
header_size = 0
|
||||||
|
# Add 12 bytes for each node, since that's how many bytes each one is made up of.
|
||||||
|
for node in range(len(self.u8_node_list)):
|
||||||
|
header_size += 12
|
||||||
|
# Add the number of bytes used for each file/folder name in the string table.
|
||||||
|
for file_name in self.file_name_list:
|
||||||
|
header_size += len(file_name) + 1
|
||||||
|
# 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)
|
||||||
|
# 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
|
||||||
|
for node in range(len(self.u8_node_list)):
|
||||||
|
if self.u8_node_list[node].type == 0:
|
||||||
|
self.u8_node_list[node].data_offset = current_data_offset
|
||||||
|
current_data_offset += self.u8_node_list[node].size
|
||||||
|
# Begin joining all the U8 archive data into one variable.
|
||||||
|
u8_data = b''
|
||||||
|
# Magic number.
|
||||||
|
u8_data += b'\x55\xAA\x38\x2D'
|
||||||
|
# Root node offset (this is always 0x20).
|
||||||
|
u8_data += int.to_bytes(0x20, 4)
|
||||||
|
# Size of the file header (excluding the first 32 bytes).
|
||||||
|
u8_data += int.to_bytes(header_size, 4)
|
||||||
|
# Offset of the beginning of the data region of the U8 archive.
|
||||||
|
u8_data += int.to_bytes(data_offset, 4)
|
||||||
|
# 16 bytes of zeroes.
|
||||||
|
u8_data += (b'\x00' * 16)
|
||||||
|
# Iterate over all the U8 nodes and dump them.
|
||||||
|
for node in self.u8_node_list:
|
||||||
|
u8_data += int.to_bytes(node.type, 2)
|
||||||
|
u8_data += int.to_bytes(node.name_offset, 2)
|
||||||
|
u8_data += int.to_bytes(node.data_offset, 4)
|
||||||
|
u8_data += int.to_bytes(node.size, 4)
|
||||||
|
# Iterate over all file names and dump them. All file names are suffixed by a \x00 byte.
|
||||||
|
for file_name in self.file_name_list:
|
||||||
|
u8_data += str.encode(file_name) + b'\x00'
|
||||||
|
# Apply the extra padding we calculated earlier by padding to where the data offset begins.
|
||||||
|
while len(u8_data) < data_offset:
|
||||||
|
u8_data += b'\x00'
|
||||||
|
# Iterate all file data and dump it.
|
||||||
|
for file in self.file_data_list:
|
||||||
|
u8_data += file
|
||||||
|
# Return the U8 archive.
|
||||||
|
return u8_data
|
||||||
|
|
||||||
|
|
||||||
|
def extract_u8(u8_data, output_folder) -> None:
|
||||||
|
"""
|
||||||
|
Extracts the provided U8 archive file data into the provided output folder path. Note that the folder must not
|
||||||
|
already exist to ensure that the output can correctly represent the file structure of the original U8 archive.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
u8_data : bytes
|
||||||
|
The data for the U8 file to extract.
|
||||||
|
output_folder : str
|
||||||
|
The path to a new folder to extract the archive to.
|
||||||
|
"""
|
||||||
|
output_folder = pathlib.Path(output_folder)
|
||||||
|
if pathlib.Path.is_dir(output_folder):
|
||||||
|
raise ValueError("Output folder already exists!")
|
||||||
|
os.mkdir(output_folder)
|
||||||
|
# Create a new U8Archive object and load the provided U8 file data into it.
|
||||||
|
u8_archive = U8Archive()
|
||||||
|
u8_archive.load(u8_data)
|
||||||
|
# This variable stores the path of the directory we're currently processing.
|
||||||
|
current_dir = output_folder
|
||||||
|
# This variable stores the final nodes for every directory we've entered, and is used to handle the recursion of
|
||||||
|
# those directories to ensure that everything gets where it belongs.
|
||||||
|
directory_recursion = [0]
|
||||||
|
# Iterate over every node and extract the files and folders.
|
||||||
|
for node in range(len(u8_archive.u8_node_list)):
|
||||||
|
# Code for a directory node. Second check just ensures we ignore the root node.
|
||||||
|
if u8_archive.u8_node_list[node].type == 256 and u8_archive.u8_node_list[node].name_offset != 0:
|
||||||
|
# The size value for a directory node is the position of the last node in this directory, with the root node
|
||||||
|
# counting as node 1.
|
||||||
|
# If the current node is below the end of the current directory, create this directory inside the previous
|
||||||
|
# current directory and make the current.
|
||||||
|
if node + 1 < directory_recursion[-1]:
|
||||||
|
current_dir = current_dir.joinpath(u8_archive.file_name_list[node])
|
||||||
|
os.mkdir(current_dir)
|
||||||
|
# If the current node is beyond the end of the current directory, we've followed that path all the way down,
|
||||||
|
# so reset back to the root directory and put our new directory there.
|
||||||
|
elif node + 1 > directory_recursion[-1]:
|
||||||
|
current_dir = output_folder.joinpath(u8_archive.file_name_list[node])
|
||||||
|
os.mkdir(current_dir)
|
||||||
|
# This check is here just in case a directory ever ends with an empty directory and not a file.
|
||||||
|
elif node + 1 == directory_recursion[-1]:
|
||||||
|
current_dir = current_dir.parent
|
||||||
|
directory_recursion.pop()
|
||||||
|
# If the last node for the directory we just processed is new (which is always should be), add it to the
|
||||||
|
# recursion array.
|
||||||
|
if u8_archive.u8_node_list[node].size not in directory_recursion:
|
||||||
|
directory_recursion.append(u8_archive.u8_node_list[node].size)
|
||||||
|
# Code for a file node.
|
||||||
|
elif u8_archive.u8_node_list[node].type == 0:
|
||||||
|
# Write out the file to the current directory.
|
||||||
|
output_file = open(current_dir.joinpath(u8_archive.file_name_list[node]), "wb")
|
||||||
|
output_file.write(u8_archive.file_data_list[node])
|
||||||
|
output_file.close()
|
||||||
|
# If this file is the final node for the current directory, pop() the recursion array and set the current
|
||||||
|
# directory to the parent of the previous current.
|
||||||
|
if node + 1 in directory_recursion:
|
||||||
|
current_dir = current_dir.parent
|
||||||
|
directory_recursion.pop()
|
||||||
|
# Code for a totally unrecognized node type, which should not happen.
|
||||||
|
elif u8_archive.u8_node_list[node].type != 0 and u8_archive.u8_node_list[node].type != 256:
|
||||||
|
raise ValueError("A node with an invalid type (" + str(u8_archive.u8_node_list[node].type) + ") was"
|
||||||
|
"found!")
|
||||||
|
|
||||||
|
|
||||||
|
def _pack_u8_dir(u8_archive: U8Archive, current_path, node_count, name_offset):
|
||||||
|
# First, get the list of everything in current path.
|
||||||
|
root_list = os.listdir(current_path)
|
||||||
|
file_list = []
|
||||||
|
dir_list = []
|
||||||
|
# Create separate lists of the files and directories in the current directory so that we can handle the files first.
|
||||||
|
for path in root_list:
|
||||||
|
if os.path.isfile(current_path.joinpath(path)):
|
||||||
|
file_list.append(path)
|
||||||
|
elif os.path.isdir(current_path.joinpath(path)):
|
||||||
|
dir_list.append(path)
|
||||||
|
# For files, read their data into the file data list, add their name into the file name list, then calculate the
|
||||||
|
# offset for their file name and create a new U8Node() for them.
|
||||||
|
for file in file_list:
|
||||||
|
node_count += 1
|
||||||
|
u8_archive.file_name_list.append(file)
|
||||||
|
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])))
|
||||||
|
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
|
||||||
|
# wouldn't have any), find the total number of files and directories inside the directory to calculate the final
|
||||||
|
# node included in it, then recursively call this function again on that directory to process it.
|
||||||
|
for directory in dir_list:
|
||||||
|
node_count += 1
|
||||||
|
u8_archive.file_name_list.append(directory)
|
||||||
|
u8_archive.file_data_list.append(b'')
|
||||||
|
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))
|
||||||
|
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,
|
||||||
|
name_offset)
|
||||||
|
# Return the U8Archive object, the current node we're on, and the current name offset.
|
||||||
|
return u8_archive, node_count, name_offset
|
||||||
|
|
||||||
|
|
||||||
|
def pack_u8(input_path) -> bytes:
|
||||||
|
"""
|
||||||
|
Packs the provided file or folder into a new U8 archive, and returns the raw file data for it.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
input_path
|
||||||
|
The path to the input file or folder.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
u8_archive : bytes
|
||||||
|
The data for the packed U8 archive.
|
||||||
|
"""
|
||||||
|
input_path = pathlib.Path(input_path)
|
||||||
|
if os.path.isdir(input_path):
|
||||||
|
# Append empty entries at the start for the root node, and then create the root U8Node() object, using rglob()
|
||||||
|
# to read the total count of files and directories that will be packed so that we can add the total node count.
|
||||||
|
u8_archive = U8Archive()
|
||||||
|
u8_archive.file_name_list.append("")
|
||||||
|
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))
|
||||||
|
# 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
|
||||||
|
# really only necessary for the directory recursion.
|
||||||
|
u8_archive, _, _ = _pack_u8_dir(u8_archive, input_path, node_count=1, name_offset=1)
|
||||||
|
return u8_archive.dump()
|
||||||
|
elif os.path.isfile(input_path):
|
||||||
|
# Simple code to handle if a single file is provided as input. Not really sure *why* you'd do this, since the
|
||||||
|
# whole point of a U8 archive is to stitch files together, but it's here nonetheless.
|
||||||
|
with open(input_path, "rb") as f:
|
||||||
|
u8_archive = U8Archive()
|
||||||
|
file_name = input_path.name
|
||||||
|
file_data = f.read()
|
||||||
|
# Append blank file name for the root node.
|
||||||
|
u8_archive.file_name_list.append("")
|
||||||
|
u8_archive.file_name_list.append(file_name)
|
||||||
|
# Append blank data for the root node.
|
||||||
|
u8_archive.file_data_list.append(b'')
|
||||||
|
u8_archive.file_data_list.append(file_data)
|
||||||
|
# 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(0, 1, 0, len(file_data)))
|
||||||
|
return u8_archive.dump()
|
||||||
@@ -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
|
||||||
|
|||||||
10
src/libWiiPy/title/__init__.py
Normal file
10
src/libWiiPy/title/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# "title/__init__.py" from libWiiPy by NinjaCheetah & Contributors
|
||||||
|
# https://github.com/NinjaCheetah/libWiiPy
|
||||||
|
|
||||||
|
from .content import *
|
||||||
|
from .crypto import *
|
||||||
|
from .nus import *
|
||||||
|
from .ticket import *
|
||||||
|
from .title import *
|
||||||
|
from .tmd import *
|
||||||
|
from .wad import *
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# "commonkeys.py" from libWiiPy by NinjaCheetah & Contributors
|
# "title/commonkeys.py" from libWiiPy by NinjaCheetah & Contributors
|
||||||
# https://github.com/NinjaCheetah/libWiiPy
|
# https://github.com/NinjaCheetah/libWiiPy
|
||||||
|
|
||||||
import binascii
|
import binascii
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# "content.py" from libWiiPy by NinjaCheetah & Contributors
|
# "title/content.py" from libWiiPy by NinjaCheetah & Contributors
|
||||||
# https://github.com/NinjaCheetah/libWiiPy
|
# https://github.com/NinjaCheetah/libWiiPy
|
||||||
#
|
#
|
||||||
# See https://wiibrew.org/wiki/Title for details about how titles are formatted
|
# See https://wiibrew.org/wiki/Title for details about how titles are formatted
|
||||||
@@ -7,7 +7,7 @@ import io
|
|||||||
import sys
|
import sys
|
||||||
import hashlib
|
import hashlib
|
||||||
from typing import List
|
from typing import List
|
||||||
from .types import ContentRecord
|
from ..types import ContentRecord
|
||||||
from .crypto import decrypt_content, encrypt_content
|
from .crypto import decrypt_content, encrypt_content
|
||||||
|
|
||||||
|
|
||||||
@@ -79,21 +79,17 @@ 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)
|
return content_region_data
|
||||||
content_region_data.seek(0x0)
|
|
||||||
content_region_raw = content_region_data.read()
|
|
||||||
# Return the raw ContentRegion for the data contained in the object.
|
|
||||||
return content_region_raw
|
|
||||||
|
|
||||||
def get_enc_content_by_index(self, index: int) -> bytes:
|
def get_enc_content_by_index(self, index: int) -> bytes:
|
||||||
"""
|
"""
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
# "crypto.py" from libWiiPy by NinjaCheetah & Contributors
|
# "title/crypto.py" from libWiiPy by NinjaCheetah & Contributors
|
||||||
# https://github.com/NinjaCheetah/libWiiPy
|
# https://github.com/NinjaCheetah/libWiiPy
|
||||||
|
|
||||||
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/title/nus.py
Normal file
233
src/libWiiPy/title/nus.py
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
# "title/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
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# "ticket.py" from libWiiPy by NinjaCheetah & Contributors
|
# "title/ticket.py" from libWiiPy by NinjaCheetah & Contributors
|
||||||
# https://github.com/NinjaCheetah/libWiiPy
|
# https://github.com/NinjaCheetah/libWiiPy
|
||||||
#
|
#
|
||||||
# See https://wiibrew.org/wiki/Ticket for details about the ticket format
|
# See https://wiibrew.org/wiki/Ticket for details about the ticket format
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
import io
|
import io
|
||||||
import binascii
|
import binascii
|
||||||
from .crypto import decrypt_title_key
|
from .crypto import decrypt_title_key
|
||||||
from .types import TitleLimit
|
from ..types import TitleLimit
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
@@ -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,61 @@ 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)
|
return ticket_data
|
||||||
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 ticket_data_raw
|
|
||||||
|
|
||||||
def get_title_id(self) -> str:
|
def get_title_id(self) -> str:
|
||||||
"""
|
"""
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# "title.py" from libWiiPy by NinjaCheetah & Contributors
|
# "title/title.py" from libWiiPy by NinjaCheetah & Contributors
|
||||||
# https://github.com/NinjaCheetah/libWiiPy
|
# https://github.com/NinjaCheetah/libWiiPy
|
||||||
#
|
#
|
||||||
# See https://wiibrew.org/wiki/Title for details about how titles are formatted
|
# See https://wiibrew.org/wiki/Title for details about how titles are formatted
|
||||||
@@ -70,15 +70,16 @@ 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.
|
||||||
self.wad.set_ticket_data(self.ticket.dump())
|
self.wad.set_ticket_data(self.ticket.dump())
|
||||||
# Dump the ContentRegion and set it in the WAD.
|
# Dump the ContentRegion and set it in the WAD.
|
||||||
self.wad.set_content_data(self.content.dump())
|
self.wad.set_content_data(self.content.dump())
|
||||||
# Dump the WAD with the new regions back into raw data and return it.
|
return self.wad.dump()
|
||||||
wad_data = self.wad.dump()
|
|
||||||
return wad_data
|
|
||||||
|
|
||||||
def load_tmd(self, tmd: bytes) -> None:
|
def load_tmd(self, tmd: bytes) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# "tmd.py" from libWiiPy by NinjaCheetah & Contributors
|
# "title/tmd.py" from libWiiPy by NinjaCheetah & Contributors
|
||||||
# https://github.com/NinjaCheetah/libWiiPy
|
# https://github.com/NinjaCheetah/libWiiPy
|
||||||
#
|
#
|
||||||
# See https://wiibrew.org/wiki/Title_metadata for details about the TMD format
|
# See https://wiibrew.org/wiki/Title_metadata for details about the TMD format
|
||||||
@@ -7,7 +7,7 @@ import io
|
|||||||
import binascii
|
import binascii
|
||||||
import struct
|
import struct
|
||||||
from typing import List
|
from typing import List
|
||||||
from .types import ContentRecord
|
from ..types import ContentRecord
|
||||||
|
|
||||||
|
|
||||||
class TMD:
|
class TMD:
|
||||||
@@ -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,71 @@ 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)
|
return tmd_data
|
||||||
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 tmd_data_raw
|
|
||||||
|
|
||||||
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 +235,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 +258,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 +276,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':
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
# "wad.py" from libWiiPy by NinjaCheetah & Contributors
|
# "title/wad.py" from libWiiPy by NinjaCheetah & Contributors
|
||||||
# https://github.com/NinjaCheetah/libWiiPy
|
# https://github.com/NinjaCheetah/libWiiPy
|
||||||
#
|
#
|
||||||
# See https://wiibrew.org/wiki/WAD_files for details about the WAD format
|
# See https://wiibrew.org/wiki/WAD_files for details about the WAD format
|
||||||
|
|
||||||
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
|
||||||
@@ -49,7 +49,7 @@ class WAD:
|
|||||||
self.wad_content_data: bytes = b''
|
self.wad_content_data: bytes = b''
|
||||||
self.wad_meta_data: bytes = b''
|
self.wad_meta_data: bytes = b''
|
||||||
|
|
||||||
def load(self, wad_data) -> None:
|
def load(self, wad_data: bytes) -> None:
|
||||||
"""
|
"""
|
||||||
Loads raw WAD data and sets all attributes of the WAD object. This allows for manipulating an already
|
Loads raw WAD data and sets all attributes of the WAD object. This allows for manipulating an already
|
||||||
existing WAD file.
|
existing WAD file.
|
||||||
@@ -57,18 +57,17 @@ class WAD:
|
|||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
wad_data : bytes
|
wad_data : bytes
|
||||||
The data for the WAD you wish to load.
|
The data for the WAD file 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 is not 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,45 @@ 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)
|
return 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 wad_data_raw
|
|
||||||
|
|
||||||
def get_wad_type(self) -> str:
|
def get_wad_type(self) -> str:
|
||||||
"""
|
"""
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
# "types.py" from libWiiPy by NinjaCheetah & Contributors
|
# "types.py" from libWiiPy by NinjaCheetah & Contributors
|
||||||
# https://github.com/NinjaCheetah/libWiiPy
|
# https://github.com/NinjaCheetah/libWiiPy
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
@@ -14,21 +13,21 @@ class ContentRecord:
|
|||||||
Attributes
|
Attributes
|
||||||
----------
|
----------
|
||||||
content_id : int
|
content_id : int
|
||||||
ID of the content.
|
The unique ID of the content.
|
||||||
index : int
|
index : int
|
||||||
Index of the content in the list of contents.
|
The index of this content in the content records.
|
||||||
content_type : int
|
content_type : int
|
||||||
The type of the content.
|
The type of the content.
|
||||||
content_size : int
|
content_size : int
|
||||||
The size of the content.
|
The size of the content when decrypted.
|
||||||
content_hash
|
content_hash
|
||||||
The SHA-1 hash of the decrypted content.
|
The SHA-1 hash of the decrypted content.
|
||||||
"""
|
"""
|
||||||
content_id: int # The unique ID of the content.
|
content_id: int
|
||||||
index: int # The index of this content in the content record.
|
index: int
|
||||||
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 # Size of the content when decrypted.
|
content_size: int
|
||||||
content_hash: bytes # SHA-1 hash of the content when decrypted.
|
content_hash: bytes
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
11
tests/__init.py__.py
Normal file
11
tests/__init.py__.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# "__init__.py" from libWiiPy by NinjaCheetah & Contributors
|
||||||
|
# https://github.com/NinjaCheetah/libWiiPy
|
||||||
|
#
|
||||||
|
# Complete set of tests to be run.
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from test_commonkeys import TestCommonKeys
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
21
tests/test_commonkeys.py
Normal file
21
tests/test_commonkeys.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# "test_commonkeys.py" from libWiiPy by NinjaCheetah & Contributors
|
||||||
|
# https://github.com/NinjaCheetah/libWiiPy
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from libWiiPy import commonkeys
|
||||||
|
|
||||||
|
|
||||||
|
class TestCommonKeys(unittest.TestCase):
|
||||||
|
def test_common(self):
|
||||||
|
self.assertEqual(commonkeys.get_common_key(0), b'\xeb\xe4*"^\x85\x93\xe4H\xd9\xc5Es\x81\xaa\xf7')
|
||||||
|
|
||||||
|
def test_korean(self):
|
||||||
|
self.assertEqual(commonkeys.get_common_key(1), b'c\xb8+\xb4\xf4aN.\x13\xf2\xfe\xfb\xbaL\x9b~')
|
||||||
|
|
||||||
|
def test_vwii(self):
|
||||||
|
self.assertEqual(commonkeys.get_common_key(2), b'0\xbf\xc7n|\x19\xaf\xbb#\x1630\xce\xd7\xc2\x8d')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user