55 Commits

Author SHA1 Message Date
cef85b4951 WIP U8 archive info and improved support for matching other files 2024-10-11 19:55:19 -04:00
079c7f9109 Automatically fakesign WAD when using add/remove/set 2024-10-11 13:52:24 -04:00
0a9733a8d3 Changed setting generation syntax, added commands to encrypt/decrypt setting file 2024-10-10 14:42:12 -04:00
676dbab4f1 Added command to generate a setting.txt from a serial number and region 2024-10-09 20:40:55 -04:00
97bc77b337 Added wad set command to replace content in a WAD 2024-09-19 14:41:23 -04:00
0ae9ac5060 Added work in progress cIOS build command, use with caution 2024-09-13 22:19:53 -04:00
2316e938b2 Removed randomly inserted (and broken) import; thanks PyCharm 2024-09-11 11:18:51 -04:00
e047b12114 Added wad remove command to remove contents from a WAD by index or CID 2024-09-11 11:13:54 -04:00
a35ba2e4b6 Adjusted WAD command syntax, added subcommand to add content to an existing WAD 2024-09-08 13:16:37 -04:00
4730f3512b Info cmd: Improved region output, added boot content index, installed title size now displayed for WADs 2024-09-04 14:37:21 -04:00
e34c10c3fa Added missing regions to nus scraper 2024-08-22 23:18:30 -04:00
55c237f5f7 Merge remote-tracking branch 'origin/main' 2024-08-22 22:16:47 -04:00
c51244e8e7 Rewrote scraping script around requests instead of downloading the whole TMD every time 2024-08-22 22:16:35 -04:00
dc94ca09c1 Fix uninstalling titles from EmuNAND 2024-08-19 10:39:44 -07:00
02fa6d09ac Updated for libWiiPy v0.5.1 dev, fixed ticket/tmd cert info for info command 2024-08-14 01:34:18 -04:00
4c700266cb Minor README updates in preparation for WiiPy v1.3.0 2024-08-12 16:29:04 -04:00
fa6ba28dbe Rewrote EmuNAND module to use libWiiPy's new EmuNAND class 2024-08-08 13:24:56 -04:00
3062a739d6 Output when a WAD fails to install during bulk EmuNAND installs 2024-08-04 22:13:02 -04:00
bc1b6623bb Added verbose slot/version output to iospatch, allow skipping hashes for emunand 2024-08-04 18:05:46 -04:00
d9c5940307 Fix issue with EmuNAND uninstall args 2024-08-04 10:10:56 -04:00
f09806e002 EmuNAND: uninstall now accepts a WAD file that it'll get the TID from 2024-08-04 10:00:23 -04:00
a863da98c7 Major updates to info and emunand commands
Info command now displays more info for TMDs, as well as info for Tickets or entire WADs.
EmuNAND command now allows for bulk installing a folder of WADs, and will create/update uid.sys during installation.
2024-08-04 00:00:38 -04:00
1b7c9afb53 Added work in progress command for getting info about files, currently only supports TMDs 2024-08-03 14:06:33 -04:00
fc8bef61a7 Don't always disable shared content in iospatch module 2024-08-02 07:58:10 -04:00
26b49ddff6 Add argument to set content to non-shared when patching IOS
Also added TMD downloader to the NUS subcommand
2024-08-01 15:55:25 -04:00
b183336d95 Properly sort contents when packing WAD 2024-08-01 03:49:23 -04:00
bb3c2737e5 Add argument to apply experimental DI patch to IOS 2024-07-31 01:33:50 -04:00
304539b120 Allow fakesigning a file in place via fakesign command
WADs patched with iospatch are also now fakesigned by default when exported.
2024-07-29 17:18:24 -04:00
6af035068c Improve verbosity in iospatcher module 2024-07-29 16:47:52 -04:00
8ec2a0d186 Yet again revert to Nuitka 2.3.11 to avoid sys.argv issues on Windows 2024-07-28 15:57:41 -04:00
15e99af267 Added module to apply patches to IOS 2024-07-28 03:40:30 -04:00
744d738a8c Added missing uninstall successful print 2024-07-27 21:56:38 -04:00
9db9e3ad6f Add support for installing/uninstalling titles to/from an EmuNAND 2024-07-27 21:50:52 -04:00
4e2f7b14e7 Add option to skip hash checks when unpacking WAD 2024-07-25 21:16:08 -04:00
5d8b9e7c08 Unpin Nuitka because 2.4.2 hotfix fixed sys.argv on Windows 2024-07-24 10:18:28 -04:00
b1de3aa9c1 Revert Nutika back to 2.3.11 because of another sys.argv issue on Windows 2024-07-23 13:53:13 -04:00
6ddb97eb6c Minor help formatting changes 2024-07-22 20:20:06 -04:00
b82b6f3873 Add module for directly fakesigning a TMD, Ticket, or WAD 2024-07-22 20:11:33 -04:00
3115105343 Minor README changes 2024-07-22 02:56:12 -04:00
b2de9eb7dc Minor README changes 2024-07-22 02:56:04 -04:00
a5afdc6d6a Add Makefile for building/installing on Linux 2024-07-22 02:53:06 -04:00
1f82aa61c7 Unpin Nutika from v2.3.11, since the 2.4.1 hotfix fixed sys.argv 2024-07-21 13:40:29 -04:00
125ba4ea69 Fixed error message incorrectly being printed when downloading to a WAD 2024-07-20 17:48:00 -04:00
7c4906f0db Pin Nuitka to v2.3.11 as v2.4 breaks sys.argv on Windows 2024-07-20 17:05:43 -04:00
2066f8b4a2 Actions fix for the Nuitka executable no longer being nuitka3 2024-07-20 15:22:04 -04:00
4ba95d0472 Removed fakesigning code, as it is now part of libWiiPy instead 2024-07-17 20:51:17 -04:00
9abdf4af04 Fix fakesigning code in wad module 2024-07-11 21:35:41 +10:00
183498025a Add checks to nus content subcommand, and improve error handling 2024-07-11 21:11:58 +10:00
8599c43c2d Change content subcommand to use 000000xx formatting for CID 2024-07-10 21:59:02 +10:00
1b603e94fc Add WIP nus subcommand for downloading a specific content 2024-07-10 21:34:28 +10:00
09631d509e Change --null-sigs to --fakesign and add fakesigning code 2024-07-10 20:50:45 +10:00
475f82aa18 Began rewriting NUS subcommand to allow for more options
New command uses its own subcommands. Currently only offers "title", which supports downloading a WAD using --wad or downloading to a folder using --output. More subcommands will be added.
Verbose output is also the default now and --verbose has been removed.
2024-07-07 18:55:33 +10:00
436189659d Change --wiiu to --wii and made Wii U servers the default 2024-07-07 07:05:55 +10:00
5cff545921 Added --verbose and --wiiu to nus module
--verbose prints NUSGet-style logging to the terminal during the download, and --wiiu enables the Wii U servers for faster downloads.
2024-07-06 20:39:52 +10:00
dcafda4b71 Allow extracting WAD/U8 files to an existing empty directory 2024-07-06 07:46:46 +10:00
20 changed files with 1582 additions and 220 deletions

View File

@@ -31,10 +31,10 @@ jobs:
pip install -r requirements.txt pip install -r requirements.txt
- name: Build Application - name: Build Application
run: | run: |
nuitka3 --show-progress --assume-yes-for-downloads --onefile wiipy.py python -m nuitka --show-progress --assume-yes-for-downloads --onefile wiipy.py -o wiipy
- name: Prepare Package for Upload - name: Prepare Package for Upload
run: | run: |
mv wiipy.bin ~/wiipy mv wiipy ~/wiipy
cd ~ cd ~
tar cvf WiiPy.tar wiipy tar cvf WiiPy.tar wiipy
- name: Upload Application - name: Upload Application
@@ -59,10 +59,10 @@ jobs:
pip install -r requirements.txt pip install -r requirements.txt
- name: Build Application - name: Build Application
run: | run: |
nuitka3 --show-progress --assume-yes-for-downloads --onefile wiipy.py python -m nuitka --show-progress --assume-yes-for-downloads --onefile wiipy.py -o wiipy
- name: Prepare Package for Upload - name: Prepare Package for Upload
run: | run: |
mv wiipy.bin ~/wiipy mv wiipy ~/wiipy
cd ~ cd ~
tar cvf WiiPy.tar wiipy tar cvf WiiPy.tar wiipy
- name: Upload Application - name: Upload Application
@@ -89,7 +89,7 @@ jobs:
pip install -r requirements.txt pip install -r requirements.txt
- name: Build Application - name: Build Application
run: | run: |
nuitka --show-progress --assume-yes-for-downloads --onefile wiipy.py python -m nuitka --show-progress --assume-yes-for-downloads --onefile wiipy.py
- name: Upload Application - name: Upload Application
uses: actions/upload-artifact@v4.3.0 uses: actions/upload-artifact@v4.3.0
with: with:

12
.gitignore vendored
View File

@@ -25,6 +25,7 @@ share/python-wheels/
*.build/ *.build/
*.dist/ *.dist/
*.onefile-build/ *.onefile-build/
wiipy
.installed.cfg .installed.cfg
*.egg *.egg
MANIFEST MANIFEST
@@ -163,14 +164,23 @@ 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 # Allows me to keep Wii files in my repository folder for testing without accidentally publishing them
*.tmd *.tmd
*.tik
*.cert
*.footer
*.wad *.wad
*.app *.app
*.arc *.arc
*.ash *.ash
*.met
out_prod/ out_prod/
remakewad.pl remakewad.pl
content.map
uid.sys
SYSCONF
setting.txt
ciosmaps.xml
# Also awful macOS files # Also awful macOS files
*._* *._*

13
Makefile Normal file
View File

@@ -0,0 +1,13 @@
CC=python -m nuitka
linux:
$(CC) --show-progress --assume-yes-for-downloads --onefile wiipy.py -o wiipy
linux-install:
install wiipy /usr/bin/
clean:
rm wiipy
rm -rd wiipy.build
rm -rd wiipy.dist
rm -rd wiipy.onefile-build

View File

@@ -5,22 +5,25 @@ WiiPy is cross-platform, and supports macOS, Windows, and Linux, both because it
To see what features are supported, I would recommend checking out the list of features that libWiiPy offers, which can be found [here]("https://github.com/NinjaCheetah/libWiiPy?tab=readme-ov-file#features"). To see what features are supported, I would recommend checking out the list of features that libWiiPy offers, which can be found [here]("https://github.com/NinjaCheetah/libWiiPy?tab=readme-ov-file#features").
## Requirements ## Requirements
libWiiPy has been tested on both Python 3.11 and 3.12, and works as expected in both places. libWiiPy *should* support Python 3.10, however this is not verified. WiiPy only relies on libWiiPy, so it supports the same Python versions. libWiiPy has been tested on both Python 3.11 and 3.12, and works as expected in both places. WiiPy relies only on libWiiPy, so generally any version supported by libWiiPy should be supported by WiiPy.
To make sure you have libWiiPy and all of its dependencies, you can simply run: To make sure you have libWiiPy and all of its dependencies, you can simply run:
```shell ```shell
pip install -r requirements.txt pip install -r requirements.txt
``` ```
## Usage ## Usage
Basic usage for WiiPy is very simple. Basic usage for WiiPy is very simple.
```shell ```shell
python3 wiipy.py <subcommand> python3 wiipy.py <subcommand>
``` ```
You can use `--help` to see a list of all subcommands, or use `<subcommand> --help` to see usage instructions for that subcommand. You can use `--help` to see a list of all commands, or use `<command> --help` to see usage instructions for that command. This also applies to subcommands, with the syntax `<command> <subcommand> --help`.
Available subcommands will expand as support for more features are added into libWiiPy. WiiPy is designed around libWiiPy's `main` branch, so any features that have been merged into main are likely to be supported here within a short period of time. This also means that any updates to the library will be addressed here quickly, so breaking changes in libWiiPy shouldn't cause issues.
Available subcommands will expand as support for more features are added into libWiiPy. WiiPy is designed around libWiiPy's `main` branch, so any features that have been merged into main are likely to be supported here within a short period of time. This also means that any breaking changes to the library will be handled here quickly, so incompatibilities shouldn't happen.
## Compiling ## Compiling
If you'd like to compile WiiPy from Python into something a little more native, you can use one of the following commands. Make sure you've installed Nuitka first (it's included in `requirements.txt`). If you'd like to compile WiiPy from Python into something a little more native, you can use one of the following commands. Make sure you've installed Nuitka first (it's included in `requirements.txt`).
@@ -28,3 +31,19 @@ If you'd like to compile WiiPy from Python into something a little more native,
python -m nuitka --show-progress --assume-yes-for-downloads --onefile wiipy.py python -m nuitka --show-progress --assume-yes-for-downloads --onefile wiipy.py
``` ```
On macOS and Linux, this will give you a binary named `wiipy.bin` in the same directory as the Python file. On Windows, you'll get a binary named `wiipy.exe` instead. On macOS and Linux, this will give you a binary named `wiipy.bin` in the same directory as the Python file. On Windows, you'll get a binary named `wiipy.exe` instead.
### For Linux Users:
A Makefile has been included to both build and install WiiPy on Linux. This will install `wiipy` to `/usr/bin/`.
First, use make to build WiiPy (this automates the step above):
```shell
make linux
```
Then, run the install command with `sudo` (or your favorite alternative):
```shell
sudo make linux-install
```
### A Note About Scripts
WiiPy's source includes a directory named `scripts`, which is currently where miscellaneous libWiiPy-based scripts live. Note that they are not part of WiiPy releases, and are not tested the same way the WiiPy is. They are simply here for those who may find them useful.

View File

@@ -1,4 +1,4 @@
# "ash.py" from WiiPy by NinjaCheetah # "modules/archive/ash.py" from WiiPy by NinjaCheetah
# https://github.com/NinjaCheetah/WiiPy # https://github.com/NinjaCheetah/WiiPy
import pathlib import pathlib

View File

@@ -1,4 +1,4 @@
# "u8.py" from WiiPy by NinjaCheetah # "modules/archive/u8.py" from WiiPy by NinjaCheetah
# https://github.com/NinjaCheetah/WiiPy # https://github.com/NinjaCheetah/WiiPy
import pathlib import pathlib
@@ -30,12 +30,8 @@ def handle_u8(args):
u8_data = open(input_path, "rb").read() u8_data = open(input_path, "rb").read()
# Ensure the output directory doesn't already exist, because libWiiPy wants to create a new one to ensure that # Output path is deliberately not checked in any way because libWiiPy already has those checks, and it's easier
# the contents of the U8 archive are extracted correctly. # and cleaner to only have one component doing all the checks.
if output_path.exists():
print("Error: Specified output directory already exists!")
return
libWiiPy.archive.extract_u8(u8_data, str(output_path)) libWiiPy.archive.extract_u8(u8_data, str(output_path))
print("U8 archive unpacked!") print("U8 archive unpacked!")

57
modules/nand/emunand.py Normal file
View File

@@ -0,0 +1,57 @@
# "modules/nand/emunand.py" from WiiPy by NinjaCheetah
# https://github.com/NinjaCheetah/WiiPy
import pathlib
import libWiiPy
def handle_emunand_title(args):
emunand = libWiiPy.nand.EmuNAND(args.emunand)
if args.skip_hash:
skip_hash = True
else:
skip_hash = False
# Code for if the --install argument was passed.
if args.install:
input_path = pathlib.Path(args.install)
if not input_path.exists():
raise FileNotFoundError(input_path)
if input_path.is_dir():
wad_files = list(input_path.glob("*.[wW][aA][dD]"))
if not wad_files:
raise FileNotFoundError("No WAD files were found in the provided input directory!")
wad_count = 0
for wad in wad_files:
title = libWiiPy.title.Title()
title.load_wad(open(wad, "rb").read())
try:
emunand.install_title(title, skip_hash=skip_hash)
wad_count += 1
except ValueError:
print(f"WAD {wad} could not be installed!")
print(f"Successfully installed {wad_count} WAD(s) to EmuNAND!")
else:
title = libWiiPy.title.Title()
title.load_wad(open(input_path, "rb").read())
emunand.install_title(title, skip_hash=skip_hash)
print("Successfully installed WAD to EmuNAND!")
# Code for if the --uninstall argument was passed.
elif args.uninstall:
input_str = args.uninstall
if pathlib.Path(input_str).exists():
title = libWiiPy.title.Title()
title.load_wad(open(pathlib.Path(input_str), "rb").read())
target_tid = title.tmd.title_id
else:
target_tid = input_str
if len(target_tid) != 16:
raise ValueError("Invalid Title ID! Title IDs must be 16 characters long.")
emunand.uninstall_title(target_tid)
print("Title uninstalled from EmuNAND!")

100
modules/nand/setting.py Normal file
View File

@@ -0,0 +1,100 @@
# "modules/nand/setting.py" from WiiPy by NinjaCheetah
# https://github.com/NinjaCheetah/WiiPy
import pathlib
import libWiiPy
def handle_setting_decrypt(args):
input_path = pathlib.Path(args.input)
if args.output is not None:
output_path = pathlib.Path(args.output)
else:
output_path = pathlib.Path(input_path.stem + "_dec" + input_path.suffix)
if not input_path.exists():
raise FileNotFoundError(input_path)
# Load and decrypt the provided file.
setting = libWiiPy.nand.SettingTxt()
setting.load(open(input_path, "rb").read())
# Write out the decrypted data.
open(output_path, "w").write(setting.dump_decrypted())
print("Successfully decrypted setting.txt!")
def handle_setting_encrypt(args):
input_path = pathlib.Path(args.input)
if args.output is not None:
output_path = pathlib.Path(args.output)
else:
output_path = pathlib.Path("setting.txt")
if not input_path.exists():
raise FileNotFoundError(input_path)
# Load and encrypt the provided file.
setting = libWiiPy.nand.SettingTxt()
setting.load_decrypted(open(input_path, "r").read())
# Write out the encrypted data.
open(output_path, "wb").write(setting.dump())
print("Successfully encrypted setting.txt!")
def handle_setting_gen(args):
# Validate the provided SN. It should be 2 or 3 letters followed by 9 numbers.
if len(args.serno) != 11 and len(args.serno) != 12:
raise ValueError("The provided Serial Number is not valid!")
try:
int(args.serno[-9:])
except ValueError:
raise ValueError("The provided Serial Number is not valid!")
prefix = args.serno[:-9]
# Detect the console revision based on the SN.
match prefix[0].upper():
case "L":
revision = "RVL-001"
case "K":
revision = "RVL-101"
case "H":
revision = "RVL-201"
case _:
revision = "RVL-001"
# Validate the region, and then validate the SN based on the region. USA has a two-letter prefix for a total length
# of 11 characters, while other regions have a three-letter prefix for a total length of 12 characters.
valid_regions = ["USA", "EUR", "JPN", "KOR"]
if args.region not in valid_regions:
raise ValueError("The provided region is not valid!")
if len(prefix) == 2 and args.region != "USA":
raise ValueError("The provided region does not match the provided Serial Number!")
elif len(prefix) == 3 and args.region == "USA":
raise ValueError("The provided region does not match the provided Serial Number!")
# Get the values for VIDEO and GAME.
video = ""
game = ""
match args.region:
case "USA":
video = "NTSC"
game = "US"
case "EUR":
video = "PAL"
game = "EU"
case "JPN":
video = "NTSC"
game = "JP"
case "KOR":
video = "NTSC"
game = "KR"
# Create a new SettingTxt object and load the settings into it.
setting = libWiiPy.nand.SettingTxt()
setting.area = args.region
setting.model = f"{revision}({args.region})"
setting.dvd = 0
setting.mpch = "0x7FFE"
setting.code = prefix
setting.serial_number = args.serno[-9:]
setting.video = video
setting.game = game
# Write out the setting.txt file.
open("setting.txt", "wb").write(setting.dump())
print(f"Successfully created setting.txt for console with serial number {args.serno}!")

View File

@@ -1,40 +0,0 @@
# "nus.py" from WiiPy by NinjaCheetah
# https://github.com/NinjaCheetah/WiiPy
import pathlib
import libWiiPy
def handle_nus(args):
title_version = None
file_path = None
# Check if --version was passed, because it'll be None if it wasn't.
if args.version is not None:
try:
title_version = int(args.version)
except ValueError:
print("Enter a valid integer for the Title Version.")
return
# If --output was passed, then save the file to the specified path (as long as it's valid).
if args.output is not None:
file_path = pathlib.Path(args.output)
if not file_path.parent.exists() or not file_path.parent.is_dir():
print("The specified output path does not exist!")
return
if file_path.suffix != ".wad":
file_path = file_path.with_suffix(".wad")
# libWiiPy accepts a title version of "None" and will just use the latest available version if it gets it.
title = libWiiPy.title.download_title(args.tid, title_version)
# If we haven't gotten a name yet, make one from the TID and version.
if file_path is None:
file_path = pathlib.Path(args.tid + "-v" + str(title.tmd.title_version) + ".wad")
wad_file = open(file_path, "wb")
wad_file.write(title.dump_wad())
wad_file.close()
print("Downloaded title with Title ID \"" + args.tid + "\"!")

125
modules/title/ciosbuild.py Normal file
View File

@@ -0,0 +1,125 @@
# "modules/title/ciosbuild.py" from WiiPy by NinjaCheetah
# https://github.com/NinjaCheetah/WiiPy
import io
import os
import xml.etree.ElementTree as ET
import pathlib
import libWiiPy
def build_cios(args):
base_path = pathlib.Path(args.base)
map_path = pathlib.Path(args.map)
if args.modules:
modules_path = pathlib.Path(args.modules)
else:
modules_path = pathlib.Path(os.getcwd())
output_path = pathlib.Path(args.output)
if not base_path.exists():
raise FileNotFoundError(base_path)
if not map_path.exists():
raise FileNotFoundError(map_path)
if not modules_path.exists():
raise FileNotFoundError(modules_path)
title = libWiiPy.title.Title()
title.load_wad(open(base_path, 'rb').read())
cios_tree = ET.parse(map_path)
cios_root = cios_tree.getroot()
# Iterate over all <ciosgroup> tags to find the cIOS that was requested, and return an error if it doesn't match
# any of the groups in the provided map.
target_cios = None
for child in cios_root:
cios = child.get("name")
if args.cios_ver == cios:
target_cios = child
break
if target_cios is None:
raise ValueError("The target cIOS could not be found in the provided map!")
# Iterate over all bases in the target cIOS to find a base that matches the provided WAD. If one is found, ensure
# that the version of the base in the map matches the version of the IOS WAD.
target_base = None
provided_base = int(title.tmd.title_id[-2:], 16)
for child in target_cios:
base = int(child.get("ios"))
if base == provided_base:
target_base = child
break
if target_base is None:
raise ValueError("The provided base IOS doesn't match any bases found in the provided map!")
base_version = int(target_base.get("version"))
if title.tmd.title_version != base_version:
raise ValueError("The provided base IOS does not match the required version for this base!")
# We're ready to begin building the cIOS now. Find all the <content> tags that have <patch> tags, and then apply
# the patches listed in them to the content.
for content in target_base.findall("content"):
patches = content.findall("patch")
if patches:
cid = int(content.get("id"), 16)
dec_content = title.get_content_by_cid(cid)
content_index = title.content.get_index_from_cid(cid)
with io.BytesIO(dec_content) as content_data:
for patch in patches:
# Read patch info from the map. This requires some conversion since ciosmap files seem to use a
# comma-separated list of bytes.
offset = int(patch.get("offset"), 16)
original_data = b''
original_data_map = patch.get("originalbytes").split(",")
for byte in original_data_map:
original_data += bytes.fromhex(byte[2:])
new_data = b''
new_data_map = patch.get("newbytes").split(",")
for byte in new_data_map:
new_data += bytes.fromhex(byte[2:])
# Seek to the target offset and apply the patches. One last sanity check to ensure this
# original data exists.
if original_data in dec_content:
content_data.seek(offset)
content_data.write(new_data)
else:
raise Exception("An error occurred while patching! Please make sure your base IOS is valid.")
content_data.seek(0x0)
dec_content = content_data.read()
# Set the content in the title to the newly-patched content, and set the type to normal.
title.set_content(dec_content, content_index, content_type=libWiiPy.title.ContentType.NORMAL)
# Next phase of cIOS building is to add the required extra modules to the end.
for content in target_base.findall("content"):
target_module = content.get("module")
if target_module is not None:
# The cIOS map supplies a Content ID to use for each additional module.
cid = int(content.get("id")[-2:], 16)
target_path = modules_path.joinpath(target_module + ".app")
if target_path.exists():
new_module = open(target_path, "rb").read()
title.add_content(new_module, cid, libWiiPy.title.ContentType.NORMAL)
else:
raise Exception(f"A required module \"{target_module}.app\" could not be found!")
# Last cIOS building step, we need to set the slot and version.
slot = args.slot
if 3 <= slot <= 255:
tid = title.tmd.title_id[:-2] + f"{slot:02X}"
title.set_title_id(tid)
else:
raise ValueError(f"The provided slot \"{slot}\" is not valid!")
try:
title.set_title_version(args.version)
except ValueError:
raise ValueError(f"The provided version \"{args.version}\" is not valid!")
# Ensure the WAD is fakesigned.
title.fakesign()
# Write the new cIOS to the specified output path.
out_file = open(output_path, "wb")
out_file.write(title.dump_wad())
out_file.close()
print("success")

37
modules/title/fakesign.py Normal file
View File

@@ -0,0 +1,37 @@
# "modules/title/fakesign.py" from WiiPy by NinjaCheetah
# https://github.com/NinjaCheetah/WiiPy
import pathlib
import libWiiPy
def handle_fakesign(args):
input_path = pathlib.Path(args.input)
if args.output is not None:
output_path = pathlib.Path(args.output)
else:
output_path = pathlib.Path(args.input)
if not input_path.exists():
raise FileNotFoundError(input_path)
if input_path.suffix.lower() == ".tmd":
tmd = libWiiPy.title.TMD()
tmd.load(open(input_path, "rb").read())
tmd.fakesign()
open(output_path, "wb").write(tmd.dump())
print("TMD fakesigned successfully!")
elif input_path.suffix.lower() == ".tik":
tik = libWiiPy.title.Ticket()
tik.load(open(input_path, "rb").read())
tik.fakesign()
open(output_path, "wb").write(tik.dump())
print("Ticket fakesigned successfully!")
elif input_path.suffix.lower() == ".wad":
title = libWiiPy.title.Title()
title.load_wad(open(input_path, "rb").read())
title.fakesign()
open(output_path, "wb").write(title.dump_wad())
print("WAD fakesigned successfully!")
else:
raise TypeError("This does not appear to be a TMD, Ticket, or WAD! Cannot fakesign.")

200
modules/title/info.py Normal file
View File

@@ -0,0 +1,200 @@
# "modules/title/info.py" from WiiPy by NinjaCheetah
# https://github.com/NinjaCheetah/WiiPy
import re
import pathlib
import binascii
import libWiiPy
def _print_tmd_info(tmd: libWiiPy.title.TMD):
# Get all important keys from the TMD and print them out nicely.
print("Title Info")
print(f" Title ID: {tmd.title_id}")
# This type of version number really only applies to the System Menu and IOS.
if tmd.title_id[:8] == "00000001":
print(f" Title Version: {tmd.title_version} ({tmd.title_version_converted})")
else:
print(f" Title Version: {tmd.title_version}")
print(f" TMD Version: {tmd.tmd_version}")
# IOSes just have an all-zero TID, so don't bothering showing that.
if tmd.ios_tid == "0000000000000000":
print(f" Required IOS: N/A")
else:
print(f" Required IOS: IOS{int(tmd.ios_tid[-2:], 16)} ({tmd.ios_tid})")
if tmd.signature_issuer.find("CP00000004") != -1:
print(f" Certificate: CP00000004 (Retail)")
print(f" Certificate Issuer: Root-CA00000001 (Retail)")
elif tmd.signature_issuer.find("CP00000007") != -1:
print(f" Certificate: CP00000007 (Development)")
print(f" Certificate Issuer: Root-CA00000002 (Development)")
elif tmd.signature_issuer.find("CP10000000") != -1:
print(f" Certificate: CP10000000 (Arcade)")
print(f" Certificate Issuer: Root-CA10000000 (Arcade)")
else:
print(f" Certificate Info: {tmd.signature_issuer} (Unknown)")
if tmd.title_id == "0000000100000002":
match tmd.title_version_converted[-1:]:
case "U":
region = "USA"
case "E":
region = "EUR"
case "J":
region = "JPN"
case "K":
region = "KOR"
case _:
region = "None"
elif tmd.title_id[:8] == "00000001":
region = "None"
else:
region = tmd.get_title_region()
print(f" Region: {region}")
print(f" Title Type: {tmd.get_title_type()}")
print(f" vWii Title: {bool(tmd.vwii)}")
print(f" DVD Video Access: {tmd.get_access_right(tmd.AccessFlags.DVD_VIDEO)}")
print(f" AHB Access: {tmd.get_access_right(tmd.AccessFlags.AHB)}")
print(f" Fakesigned: {tmd.get_is_fakesigned()}")
# Iterate over the content and print their details.
print("\nContent Info")
print(f" Total Contents: {tmd.num_contents}")
print(f" Boot Content Index: {tmd.boot_index}")
print(" Content Records:")
for content in tmd.content_records:
print(f" Content Index: {content.index}")
print(f" Content ID: " + f"{content.content_id:08X}".lower())
print(f" Content Type: {tmd.get_content_type(content.index)}")
print(f" Content Size: {content.content_size} bytes")
print(f" Content Hash: {content.content_hash.decode()}")
def _print_ticket_info(ticket: libWiiPy.title.Ticket):
# Get all important keys from the TMD and print them out nicely.
print(f"Ticket Info")
print(f" Title ID: {ticket.title_id.decode()}")
# This type of version number really only applies to the System Menu and IOS.
if ticket.title_id.decode()[:8] == "00000001":
print(f" Title Version: {ticket.title_version} "
f"({libWiiPy.title.title_ver_dec_to_standard(ticket.title_version, ticket.title_id.decode())})")
else:
print(f" Title Version: {ticket.title_version}")
print(f" Ticket Version: {ticket.ticket_version}")
if ticket.signature_issuer.find("XS00000003") != -1:
print(f" Certificate: XS00000003 (Retail)")
print(f" Certificate Issuer: Root-CA00000001 (Retail)")
elif ticket.signature_issuer.find("XS00000006") != -1:
print(f" Certificate: XS00000006 (Development)")
print(f" Certificate Issuer: Root-CA00000002 (Development)")
else:
print(f" Certificate Info: {ticket.signature_issuer} (Unknown)")
match ticket.common_key_index:
case 0:
key = "Common"
case 1:
key = "Korean"
case 2:
key = "vWii"
case _:
key = "Unknown (Likely Common)"
print(f" Common Key: {key}")
print(f" Title Key (Encrypted): {binascii.hexlify(ticket.title_key_enc).decode()}")
print(f" Title Key (Decrypted): {binascii.hexlify(ticket.get_title_key()).decode()}")
def _print_wad_info(title: libWiiPy.title.Title):
print(f"WAD Info")
match title.wad.wad_type:
case "Is":
print(f" WAD Type: Standard Installable")
case "ib":
print(f" WAD Type: boot2")
case _:
print(f" WAD Type: Unknown ({title.wad.wad_type})")
min_size_blocks = title.get_title_size_blocks()
max_size_blocks = title.get_title_size_blocks(absolute=True)
if min_size_blocks == max_size_blocks:
print(f" Installed Size: {min_size_blocks} blocks")
else:
print(f" Installed Size: {min_size_blocks}-{max_size_blocks} blocks")
min_size = round(title.get_title_size() / 1048576, 2)
max_size = round(title.get_title_size(absolute=True) / 1048576, 2)
if min_size == max_size:
print(f" Installed Size (MB): {min_size} MB")
else:
print(f" Installed Size (MB): {min_size}-{max_size} MB")
print(f" Has Meta/Footer: {bool(title.wad.wad_meta_size)}")
print(f" Has CRL: {bool(title.wad.wad_crl_size)}")
print("")
_print_ticket_info(title.ticket)
print("")
_print_tmd_info(title.tmd)
def _print_u8_info(u8_archive: libWiiPy.archive.U8Archive):
# Build the file structure of the U8 archive and print it out.
print(f"U8 Info")
# This variable stores the path of the directory we're currently processing.
current_dir = pathlib.Path()
# This variable stores the order of directory nodes leading to the current working directory, to make sure that
# things get where they belong.
parent_dirs = [0]
for node in range(len(u8_archive.u8_node_list)):
# Code for a directory node (excluding the root node since that already exists).
if u8_archive.u8_node_list[node].type == 1 and u8_archive.u8_node_list[node].name_offset != 0:
if u8_archive.u8_node_list[node].data_offset == parent_dirs[-1]:
current_dir = current_dir.joinpath(u8_archive.file_name_list[node])
print(("" * (len(parent_dirs) - 1) + "├┬ ") + str(current_dir.name) + "/")
parent_dirs.append(node)
else:
# Go up until we're back at the correct level.
while u8_archive.u8_node_list[node].data_offset != parent_dirs[-1]:
parent_dirs.pop()
parent_dirs.append(node)
current_dir = pathlib.Path()
# Rebuild current working directory, and make sure all directories in the path exist.
for directory in parent_dirs:
current_dir = current_dir.joinpath(u8_archive.file_name_list[directory])
#print(("│" * (len(parent_dirs) - 1) + "┬ ") + str(current_dir.name))
# Code for a file node.
elif u8_archive.u8_node_list[node].type == 0:
print(("" * (len(parent_dirs) - 1) + "") + u8_archive.file_name_list[node])
def handle_info(args):
input_path = pathlib.Path(args.input)
if not input_path.exists():
raise FileNotFoundError(input_path)
if input_path.suffix.lower() == ".tmd" or input_path.name == "tmd.bin" or re.match("tmd.?[0-9]*", input_path.name):
tmd = libWiiPy.title.TMD()
tmd.load(open(input_path, "rb").read())
_print_tmd_info(tmd)
elif input_path.suffix.lower() == ".tik" or input_path.name == "ticket.bin" or input_path.name == "cetk":
tik = libWiiPy.title.Ticket()
tik.load(open(input_path, "rb").read())
_print_ticket_info(tik)
elif input_path.suffix.lower() == ".wad":
title = libWiiPy.title.Title()
title.load_wad(open(input_path, "rb").read())
_print_wad_info(title)
elif input_path.suffix.lower() == ".arc":
u8_archive = libWiiPy.archive.U8Archive()
u8_archive.load(open(input_path, "rb").read())
_print_u8_info(u8_archive)
else:
# Try file types that have a matchable magic number if we can't tell the easy way.
magic_number = open(input_path, "rb").read(8)
if magic_number == b'\x00\x00\x00\x20\x49\x73\x00\x00' or magic_number == b'\x00\x00\x00\x20\x69\x62\x00\x00':
title = libWiiPy.title.Title()
title.load_wad(open(input_path, "rb").read())
_print_wad_info(title)
return
# This is the length of a normal magic number, WADs just require a little more checking.
magic_number = open(input_path, "rb").read(4)
# U8 archives have an annoying number of possible extensions, so this is definitely necessary.
if magic_number == b'\x55\xAA\x38\x2D':
u8_archive = libWiiPy.archive.U8Archive()
u8_archive.load(open(input_path, "rb").read())
_print_u8_info(u8_archive)
return
raise TypeError("This does not appear to be a supported file type! No info can be provided.")

129
modules/title/iospatcher.py Normal file
View File

@@ -0,0 +1,129 @@
# "modules/title/iospatcher.py" from WiiPy by NinjaCheetah
# https://github.com/NinjaCheetah/WiiPy
import pathlib
import libWiiPy
def _patch_fakesigning(ios_patcher: libWiiPy.title.IOSPatcher) -> int:
print("Applying fakesigning patch... ", end="", flush=True)
count = ios_patcher.patch_fakesigning()
if count == 1:
print(f"{count} patch applied")
else:
print(f"{count} patches applied")
return count
def _patch_es_identify(ios_patcher: libWiiPy.title.IOSPatcher) -> int:
print("Applying ES_Identify access patch... ", end="", flush=True)
count = ios_patcher.patch_es_identify()
if count == 1:
print(f"{count} patch applied")
else:
print(f"{count} patches applied")
return count
def _patch_nand_access(ios_patcher: libWiiPy.title.IOSPatcher) -> int:
print("Applying /dev/flash access patch... ", end="", flush=True)
count = ios_patcher.patch_nand_access()
if count == 1:
print(f"{count} patch applied")
else:
print(f"{count} patches applied")
return count
def _patch_version_downgrading(ios_patcher: libWiiPy.title.IOSPatcher) -> int:
print("Applying version downgrading patch... ", end="", flush=True)
count = ios_patcher.patch_version_downgrading()
if count == 1:
print(f"{count} patch applied")
else:
print(f"{count} patches applied")
return count
def _patch_drive_inquiry(ios_patcher: libWiiPy.title.IOSPatcher) -> int:
print("\n/!\\ WARNING! /!\\\n"
"This drive inquiry patch is experimental, and may introduce unexpected side effects on some consoles.\n")
print("Applying drive inquiry patch... ", end="", flush=True)
count = ios_patcher.patch_drive_inquiry()
if count == 1:
print(f"{count} patch applied")
else:
print(f"{count} patches applied")
return count
def handle_iospatch(args):
input_path = pathlib.Path(args.input)
if not input_path.exists():
raise FileNotFoundError(input_path)
title = libWiiPy.title.Title()
title.load_wad(open(input_path, "rb").read())
tid = title.tmd.title_id
if tid[:8] != "00000001" or tid[8:] == "00000001" or tid[8:] == "00000002":
raise ValueError("This WAD does not appear to contain an IOS! Patching cannot continue.")
patch_count = 0
if args.version is not None:
title.set_title_version(args.version)
print(f"Title version set to {args.version}!")
if args.slot is not None:
slot = args.slot
if 3 <= slot <= 255:
tid = title.tmd.title_id[:-2] + f"{slot:02X}"
title.set_title_id(tid)
print(f"IOS slot set to {slot}!")
ios_patcher = libWiiPy.title.IOSPatcher()
ios_patcher.load(title)
if args.all is True:
patch_count += _patch_fakesigning(ios_patcher)
patch_count += _patch_es_identify(ios_patcher)
patch_count += _patch_nand_access(ios_patcher)
patch_count += _patch_version_downgrading(ios_patcher)
else:
if args.fakesigning is True:
patch_count += _patch_fakesigning(ios_patcher)
if args.es_identify is True:
patch_count += _patch_es_identify(ios_patcher)
if args.nand_access is True:
patch_count += _patch_nand_access(ios_patcher)
if args.version_downgrading is True:
patch_count += _patch_version_downgrading(ios_patcher)
if args.drive_inquiry is True:
patch_count += _patch_drive_inquiry(ios_patcher)
print(f"\nTotal patches applied: {patch_count}")
if patch_count == 0 and args.version is None and args.slot is None:
raise ValueError("No patches were applied! Please select patches to apply, and ensure that selected patches are"
" compatible with this IOS.")
if patch_count > 0 or args.version is not None or args.slot is not None:
# Set patched content to non-shared if that argument was passed.
if args.no_shared:
ios_patcher.title.content.content_records[ios_patcher.es_module_index].content_type = 1
if ios_patcher.dip_module_index != -1:
ios_patcher.title.content.content_records[ios_patcher.dip_module_index].content_type = 1
ios_patcher.title.fakesign() # Signature is broken anyway, so fakesign for maximum installation openings
if args.output is not None:
output_path = pathlib.Path(args.output)
output_file = open(output_path, "wb")
output_file.write(ios_patcher.title.dump_wad())
output_file.close()
else:
output_file = open(input_path, "wb")
output_file.write(ios_patcher.title.dump_wad())
output_file.close()
print("IOS successfully patched!")

254
modules/title/nus.py Normal file
View File

@@ -0,0 +1,254 @@
# "modules/title/nus.py" from WiiPy by NinjaCheetah
# https://github.com/NinjaCheetah/WiiPy
import os
import hashlib
import pathlib
import binascii
import libWiiPy
def handle_nus_title(args):
title_version = None
wad_file = None
output_dir = None
can_decrypt = False
tid = args.tid
if args.wii:
wiiu_nus_enabled = False
else:
wiiu_nus_enabled = True
# Check if --version was passed, because it'll be None if it wasn't.
if args.version is not None:
try:
title_version = int(args.version)
except ValueError:
print("Enter a valid integer for the Title Version.")
return
# If --wad was passed, check to make sure the path is okay.
if args.wad is not None:
wad_file = pathlib.Path(args.wad)
if wad_file.suffix != ".wad":
wad_file = wad_file.with_suffix(".wad")
# If --output was passed, make sure the directory either doesn't exist or is empty.
if args.output is not None:
output_dir = pathlib.Path(args.output)
if output_dir.exists():
if output_dir.is_dir() and next(os.scandir(output_dir), None):
raise ValueError("Output folder is not empty!")
elif output_dir.is_file():
raise ValueError("A file already exists with the provided directory name!")
else:
os.mkdir(output_dir)
# Download the title from the NUS. This is done "manually" (as opposed to using download_title()) so that we can
# provide verbose output.
title = libWiiPy.title.Title()
# Announce the title being downloaded, and the version if applicable.
if title_version is not None:
print("Downloading title " + tid + " v" + str(title_version) + ", please wait...")
else:
print("Downloading title " + tid + " vLatest, please wait...")
print(" - Downloading and parsing TMD...")
# Download a specific TMD version if a version was specified, otherwise just download the latest TMD.
if title_version is not None:
title.load_tmd(libWiiPy.title.download_tmd(tid, title_version, wiiu_endpoint=wiiu_nus_enabled))
else:
title.load_tmd(libWiiPy.title.download_tmd(tid, wiiu_endpoint=wiiu_nus_enabled))
title_version = title.tmd.title_version
# Write out the TMD to a file.
if output_dir is not None:
tmd_out = open(output_dir.joinpath("tmd." + str(title_version)), "wb")
tmd_out.write(title.tmd.dump())
tmd_out.close()
# Download the ticket, if we can.
print(" - Downloading and parsing Ticket...")
try:
title.load_ticket(libWiiPy.title.download_ticket(tid, wiiu_endpoint=wiiu_nus_enabled))
can_decrypt = True
if output_dir is not None:
ticket_out = open(output_dir.joinpath("tik"), "wb")
ticket_out.write(title.ticket.dump())
ticket_out.close()
except ValueError:
# If libWiiPy returns an error, then no ticket is available. Log this, and disable options requiring a
# ticket so that they aren't attempted later.
print(" - No Ticket is available!")
if wad_file is not None and output_dir is None:
print("--wad was passed, but this title cannot be packed into a WAD!")
return
# Load the content records from the TMD, and begin iterating over the records.
title.load_content_records()
content_list = []
for content in range(len(title.tmd.content_records)):
# Generate the content file name by converting the Content ID to hex and then removing the 0x.
content_file_name = hex(title.tmd.content_records[content].content_id)[2:]
while len(content_file_name) < 8:
content_file_name = "0" + content_file_name
print(" - Downloading content " + str(content + 1) + " of " +
str(len(title.tmd.content_records)) + " (Content ID: " +
str(title.tmd.content_records[content].content_id) + ", Size: " +
str(title.tmd.content_records[content].content_size) + " bytes)...")
content_list.append(libWiiPy.title.download_content(tid, title.tmd.content_records[content].content_id,
wiiu_endpoint=wiiu_nus_enabled))
print(" - Done!")
# If we're supposed to be outputting to a folder, then write these files out.
if output_dir is not None:
enc_content_out = open(output_dir.joinpath(content_file_name), "wb")
enc_content_out.write(content_list[content])
enc_content_out.close()
title.content.content_list = content_list
# Try to decrypt the contents for this title if a ticket was available.
if output_dir is not None:
if can_decrypt is True:
for content in range(len(title.tmd.content_records)):
print(" - Decrypting content " + str(content + 1) + " of " + str(len(title.tmd.content_records)) +
" (Content ID: " + str(title.tmd.content_records[content].content_id) + ")...")
dec_content = title.get_content_by_index(content)
content_file_name = f"{title.tmd.content_records[content].content_id:08X}".lower() + ".app"
dec_content_out = open(output_dir.joinpath(content_file_name), "wb")
dec_content_out.write(dec_content)
dec_content_out.close()
else:
print("Title has no Ticket, so content will not be decrypted!")
# If --wad was passed, pack a WAD and output that.
if wad_file is not None:
# Get the WAD certificate chain.
print(" - Building certificate...")
title.wad.set_cert_data(libWiiPy.title.download_cert(wiiu_endpoint=wiiu_nus_enabled))
# Ensure that the path ends in .wad, and add that if it doesn't.
print("Packing WAD...")
if wad_file.suffix != ".wad":
wad_file = wad_file.with_suffix(".wad")
# Have libWiiPy dump the WAD, and write that data out.
file = open(wad_file, "wb")
file.write(title.dump_wad())
file.close()
print("Downloaded title with Title ID \"" + args.tid + "\"!")
def handle_nus_content(args):
tid = args.tid
cid = args.cid
version = args.version
out = args.output
if args.decrypt:
decrypt_content = True
else:
decrypt_content = False
# Only accepting the 000000xx format because it's the one that would be most commonly known, rather than using the
# actual integer that the hex Content ID translates to.
try:
content_id = int.from_bytes(binascii.unhexlify(cid))
except binascii.Error:
print("Invalid Content ID! Content ID must be in format \"000000xx\"!")
return
# Use the supplied output path if one was specified, otherwise generate one using the Content ID.
if out is None:
content_file_name = f"{content_id:08X}".lower()
output_path = pathlib.Path(content_file_name)
else:
output_path = pathlib.Path(out)
# Try to download the content, and catch the ValueError libWiiPy will throw if it can't be found.
print("Downloading content with Content ID " + cid + "...")
try:
content_data = libWiiPy.title.download_content(tid, content_id)
except ValueError:
print("The Title ID or Content ID you specified could not be found!")
return
if decrypt_content is True:
# Ensure that a version was supplied, because we need the matching TMD for decryption to work.
if version is None:
print("You must specify the version that the requested content belongs to for decryption!")
return
output_path = output_path.with_suffix(".app")
tmd = libWiiPy.title.TMD()
tmd.load(libWiiPy.title.download_tmd(tid, version))
# Try to get a Ticket for the title, if a common one is available.
try:
ticket = libWiiPy.title.Ticket()
ticket.load(libWiiPy.title.download_ticket(tid, wiiu_endpoint=True))
except ValueError:
print("No Ticket is available! Content cannot be decrypted!")
return
content_hash = 'gggggggggggggggggggggggggggggggggggggggg'
content_size = 0
content_index = 0
for record in tmd.content_records:
if record.content_id == content_id:
content_hash = record.content_hash.decode()
content_size = record.content_size
content_index = record.index
# If the default hash never changed, then a content record matching the downloaded content couldn't be found,
# which most likely means that the wrong version was specified.
if content_hash == 'gggggggggggggggggggggggggggggggggggggggg':
print("Content was not found in the TMD from the specified version! Content cannot be decrypted!")
return
# Manually decrypt the content and verify its hash, which is what libWiiPy's get_content() methods do. We just
# can't really use that here because that require setting up a lot more of the title than is necessary.
content_dec = libWiiPy.title.decrypt_content(content_data, ticket.get_title_key(), content_index, content_size)
content_dec_hash = hashlib.sha1(content_dec).hexdigest()
if content_hash != content_dec_hash:
raise ValueError("The decrypted content provided does not match the record at the provided index. \n"
"Expected hash is: {}\n".format(content_hash) +
"Actual hash is: {}".format(content_dec_hash))
file = open(output_path, "wb")
file.write(content_dec)
file.close()
else:
file = open(output_path, "wb")
file.write(content_data)
file.close()
print("Downloaded content with Content ID \"" + cid + "\"!")
def handle_nus_tmd(args):
tid = args.tid
version = args.version
out = args.output
# Check if --version was passed, because it'll be None if it wasn't.
if args.version is not None:
try:
title_version = int(args.version)
except ValueError:
print("Enter a valid integer for the Title Version.")
return
# Use the supplied output path if one was specified, otherwise generate one using the Title ID.
if out is None:
output_path = pathlib.Path(tid + ".tmd")
else:
output_path = pathlib.Path(out)
# Try to download the TMD, and catch the ValueError libWiiPy will throw if it can't be found.
print("Downloading TMD for title " + tid + "...")
try:
tmd_data = libWiiPy.title.download_tmd(tid, version)
except ValueError:
print("The Title ID or version you specified could not be found!")
return
file = open(output_path, "wb")
file.write(tmd_data)
file.close()
print("Downloaded TMD for title \"" + tid + "\"!")

334
modules/title/wad.py Normal file
View File

@@ -0,0 +1,334 @@
# "modules/title/wad.py" from WiiPy by NinjaCheetah
# https://github.com/NinjaCheetah/WiiPy
import os
import pathlib
from random import randint
import libWiiPy
def handle_wad_add(args):
input_path = pathlib.Path(args.input)
content_path = pathlib.Path(args.content)
if args.output is not None:
output_path = pathlib.Path(args.output)
else:
output_path = pathlib.Path(args.input)
if not input_path.exists():
raise FileNotFoundError(input_path)
if not content_path.exists():
raise FileNotFoundError(content_path)
wad_file = open(input_path, 'rb')
title = libWiiPy.title.Title()
title.load_wad(wad_file.read())
wad_file.close()
content_file = open(content_path, 'rb')
content_data = content_file.read()
content_file.close()
# Prepare the CID so it's ready when we go to add this content to the WAD.
# We need to both validate that this is a real CID, and also that it isn't already taken by another content.
if args.cid is not None:
if len(args.cid) != 8:
raise ValueError("The provided Content ID is invalid!")
target_cid = int(args.cid, 16)
for record in title.content.content_records:
if target_cid == record.content_id:
raise ValueError("The provided Content ID is already being used by this title!")
print(f"Using provided Content ID \"{target_cid:08X}\".")
# If we weren't given a CID, then we need to randomly assign one, and ensure it isn't being used.
else:
used_cids = []
for record in title.content.content_records:
used_cids.append(record.content_id)
target_cid = randint(0, 0x000000FF)
while target_cid in used_cids:
target_cid = randint(0, 0x000000FF)
print(f"Using randomly assigned Content ID \"{target_cid:08X}\" since none were provided.")
# Get the type of the new content.
if args.type is not None:
match str.lower(args.type):
case "normal":
target_type = libWiiPy.title.ContentType.NORMAL
case "shared":
target_type = libWiiPy.title.ContentType.SHARED
case "dlc":
target_type = libWiiPy.title.ContentType.DLC
case _:
raise ValueError("The provided content type is invalid!")
else:
target_type = libWiiPy.title.ContentType.NORMAL
# Call add_content to add our new content with the set parameters.
title.add_content(content_data, target_cid, target_type)
# Auto fakesign because we've edited the title.
title.fakesign()
out_file = open(output_path, 'wb')
out_file.write(title.dump_wad())
out_file.close()
print(f"Successfully added new content with Content ID \"{target_cid:08X}\" and type \"{target_type.name}\"!")
def handle_wad_pack(args):
input_path = pathlib.Path(args.input)
output_path = pathlib.Path(args.output)
# Make sure input path both exists and is a directory. Separate checks because this provides more relevant
# errors than just a NotADirectoryError if the actual issue is that there's nothing at all.
if not input_path.exists():
raise FileNotFoundError(input_path)
if not input_path.is_dir():
raise NotADirectoryError(input_path)
# Get a list of all files ending in .tmd, and then make sure that that list has *only* 1 entry. More than 1
# means we can't pack a WAD because we couldn't really tell which TMD is intended for this WAD.
tmd_list = list(input_path.glob('*.[tT][mM][dD]'))
if len(tmd_list) > 1:
raise FileExistsError("More than one TMD file was found! Only one TMD can be packed into a WAD.")
elif len(tmd_list) == 0:
raise FileNotFoundError("No TMD file found! Cannot pack WAD.")
else:
tmd_file = tmd_list[0]
# Repeat the same process as above for all .tik files.
ticket_list = list(input_path.glob('*.[tT][iI][kK]'))
if len(ticket_list) > 1:
raise FileExistsError("More than one Ticket file was found! Only one Ticket can be packed into a WAD.")
elif len(ticket_list) == 0:
raise FileNotFoundError("No Ticket file found! Cannot pack WAD.")
else:
ticket_file = ticket_list[0]
# And one more time for all .cert files.
cert_list = list(input_path.glob('*.[cC][eE][rR][tT]'))
if len(cert_list) > 1:
raise FileExistsError("More than one certificate file was found! Only one certificate can be packed into a "
"WAD.")
elif len(cert_list) == 0:
raise FileNotFoundError("No certificate file found! Cannot pack WAD.")
else:
cert_file = cert_list[0]
# Make sure that there's at least one content to pack.
content_files = list(input_path.glob("*.[aA][pP][pP]"))
if not content_files:
raise FileNotFoundError("No contents found! Cannot pack WAD.")
# Semi-hacky sorting method, but it works. Should maybe be changed eventually.
content_files_ordered = []
for index in range(len(content_files)):
content_files_ordered.append(None)
for content_file in content_files:
content_index = int(content_file.stem, 16)
content_files_ordered[content_index] = content_file
# Open the output file, and load all the component files that we've now verified we have into a libWiiPy Title()
# object.
with open(output_path, "wb") as output_path:
title = libWiiPy.title.Title()
title.load_tmd(open(tmd_file, "rb").read())
title.load_ticket(open(ticket_file, "rb").read())
title.wad.set_cert_data(open(cert_file, "rb").read())
# Footers are not super common and are not required, so we don't care about one existing until we get to
# the step where we'd pack it.
footer_file = list(input_path.glob("*.[fF][oO][oO][tT][eE][rR]"))[0]
if footer_file.exists():
title.wad.set_meta_data(open(footer_file, "rb").read())
# Method to ensure that the title's content records match between the TMD() and ContentRegion() objects.
title.load_content_records()
# Iterate over every file in the content_files list, and set them in the Title().
for record in title.content.content_records:
index = title.content.content_records.index(record)
dec_content = open(content_files_ordered[index], "rb").read()
title.set_content(dec_content, index)
# Fakesign the TMD and Ticket using the trucha bug, if enabled. This is built-in in libWiiPy v0.4.1+.
if args.fakesign:
title.fakesign()
output_path.write(title.dump_wad())
print("WAD file packed!")
def handle_wad_remove(args):
input_path = pathlib.Path(args.input)
if args.output is not None:
output_path = pathlib.Path(args.output)
else:
output_path = pathlib.Path(args.input)
if not input_path.exists():
raise FileNotFoundError(input_path)
wad_file = open(input_path, 'rb')
title = libWiiPy.title.Title()
title.load_wad(wad_file.read())
wad_file.close()
if args.index is not None:
# List indices in the title, and ensure that the target content index exists.
valid_indices = []
for record in title.content.content_records:
valid_indices.append(record.index)
if args.index not in valid_indices:
raise ValueError("The provided content index could not be found in this title!")
title.content.remove_content_by_index(args.index)
# Auto fakesign because we've edited the title.
title.fakesign()
out_file = open(output_path, 'wb')
out_file.write(title.dump_wad())
out_file.close()
print(f"Removed content at content index {args.index}!")
elif args.cid is not None:
if len(args.cid) != 8:
raise ValueError("The provided Content ID is invalid!")
target_cid = int(args.cid, 16)
# List Contents IDs in the title, and ensure that the target Content ID exists.
valid_ids = []
for record in title.content.content_records:
valid_ids.append(record.content_id)
if target_cid not in valid_ids:
raise ValueError("The provided Content ID could not be found in this title!")
title.content.remove_content_by_cid(target_cid)
# Auto fakesign because we've edited the title.
title.fakesign()
out_file = open(output_path, 'wb')
out_file.write(title.dump_wad())
out_file.close()
print(f"Removed content with Content ID \"{target_cid:08X}\"!")
def handle_wad_set(args):
input_path = pathlib.Path(args.input)
content_path = pathlib.Path(args.content)
if args.output is not None:
output_path = pathlib.Path(args.output)
else:
output_path = pathlib.Path(args.input)
if not input_path.exists():
raise FileNotFoundError(input_path)
if not content_path.exists():
raise FileNotFoundError(content_path)
title = libWiiPy.title.Title()
title.load_wad(open(input_path, "rb").read())
content_data = open(content_path, "rb").read()
# Get the new type of the content, if one was specified.
if args.type is not None:
match str.lower(args.type):
case "normal":
target_type = libWiiPy.title.ContentType.NORMAL
case "shared":
target_type = libWiiPy.title.ContentType.SHARED
case "dlc":
target_type = libWiiPy.title.ContentType.DLC
case _:
raise ValueError("The provided content type is invalid!")
else:
target_type = None
if args.index is not None:
# If we're replacing based on the index, then make sure the specified index exists.
existing_indices = []
for record in title.content.content_records:
existing_indices.append(record.index)
if args.index not in existing_indices:
raise ValueError("The provided index could not be found in this title!")
if target_type:
title.set_content(content_data, args.index, content_type=target_type)
else:
title.set_content(content_data, args.index)
# Auto fakesign because we've edited the title.
title.fakesign()
open(output_path, "wb").write(title.dump_wad())
print(f"Replaced content at content index {args.index}!")
elif args.cid is not None:
# If we're replacing based on the CID, then make sure the specified CID is valid and exists.
if len(args.cid) != 8:
raise ValueError("The provided Content ID is invalid!")
target_cid = int(args.cid, 16)
existing_cids = []
for record in title.content.content_records:
existing_cids.append(record.content_id)
if target_cid not in existing_cids:
raise ValueError("The provided Content ID could not be found in this title!")
target_index = title.content.get_index_from_cid(target_cid)
if target_type:
title.set_content(content_data, target_index, content_type=target_type)
else:
title.set_content(content_data, target_index)
# Auto fakesign because we've edited the title.
title.fakesign()
open(output_path, "wb").write(title.dump_wad())
print(f"Replaced content with Content ID \"{target_cid:08X}\"!")
def handle_wad_unpack(args):
input_path = pathlib.Path(args.input)
output_path = pathlib.Path(args.output)
if not input_path.exists():
raise FileNotFoundError(input_path)
# Check if the output path already exists, and if it does, ensure that it is both a directory and empty.
if output_path.exists():
# if output_path.is_dir() and next(os.scandir(output_path), None):
# raise ValueError("Output folder is not empty!")
if output_path.is_file():
raise ValueError("A file already exists with the provided directory name!")
else:
os.mkdir(output_path)
# Step through each component of a WAD and dump it to a file.
with open(args.input, "rb") as wad_file:
title = libWiiPy.title.Title()
title.load_wad(wad_file.read())
cert_name = title.tmd.title_id + ".cert"
cert_out = open(output_path.joinpath(cert_name), "wb")
cert_out.write(title.wad.get_cert_data())
cert_out.close()
tmd_name = title.tmd.title_id + ".tmd"
tmd_out = open(output_path.joinpath(tmd_name), "wb")
tmd_out.write(title.wad.get_tmd_data())
tmd_out.close()
ticket_name = title.tmd.title_id + ".tik"
ticket_out = open(output_path.joinpath(ticket_name), "wb")
ticket_out.write(title.wad.get_ticket_data())
ticket_out.close()
meta_name = title.tmd.title_id + ".footer"
meta_out = open(output_path.joinpath(meta_name), "wb")
meta_out.write(title.wad.get_meta_data())
meta_out.close()
# Skip validating hashes if -s/--skip-hash was passed.
if args.skip_hash:
skip_hash = True
else:
skip_hash = False
for content_file in range(0, title.tmd.num_contents):
content_file_name = f"{content_file:08X}".lower() + ".app"
content_out = open(output_path.joinpath(content_file_name), "wb")
content_out.write(title.get_content_by_index(content_file, skip_hash))
content_out.close()
print("WAD file unpacked!")

View File

@@ -1,126 +0,0 @@
# "wad.py" from WiiPy by NinjaCheetah
# https://github.com/NinjaCheetah/WiiPy
import pathlib
import binascii
import libWiiPy
def handle_wad(args):
input_path = pathlib.Path(args.input)
output_path = pathlib.Path(args.output)
# Code for if the --pack argument was passed.
if args.pack:
# Make sure input path both exists and is a directory. Separate checks because this provides more relevant
# errors than just a NotADirectoryError if the actual issue is that there's nothing at all.
if not input_path.exists():
raise FileNotFoundError(input_path)
if not input_path.is_dir():
raise NotADirectoryError(input_path)
# Get a list of all files ending in .tmd, and then make sure that that list has *only* 1 entry. More than 1
# means we can't pack a WAD because we couldn't really tell which TMD is intended for this WAD.
tmd_list = list(input_path.glob('*.tmd'))
if len(tmd_list) > 1:
raise FileExistsError("More than one TMD file was found! Only one TMD can be packed into a WAD.")
elif len(tmd_list) == 0:
raise FileNotFoundError("No TMD file found! Cannot pack WAD.")
else:
tmd_file = tmd_list[0]
# Repeat the same process as above for all .tik files.
ticket_list = list(input_path.glob('*.tik'))
if len(ticket_list) > 1:
raise FileExistsError("More than one Ticket file was found! Only one Ticket can be packed into a WAD.")
elif len(ticket_list) == 0:
raise FileNotFoundError("No Ticket file found! Cannot pack WAD.")
else:
ticket_file = ticket_list[0]
# And one more time for all .cert files.
cert_list = list(input_path.glob('*.cert'))
if len(cert_list) > 1:
raise FileExistsError("More than one certificate file was found! Only one certificate can be packed into a "
"WAD.")
elif len(cert_list) == 0:
raise FileNotFoundError("No certificate file found! Cannot pack WAD.")
else:
cert_file = cert_list[0]
# Make sure that there's at least one content to pack.
content_files = list(input_path.glob("*.app"))
if not content_files:
raise FileNotFoundError("No contents found! Cannot pack WAD.")
# Open the output file, and load all the component files that we've now verified we have into a libWiiPy Title()
# object.
with open(output_path, "wb") as output_path:
title = libWiiPy.title.Title()
title.load_tmd(open(tmd_file, "rb").read())
title.load_ticket(open(ticket_file, "rb").read())
title.wad.set_cert_data(open(cert_file, "rb").read())
# Footers are not super common and are not required, so we don't care about one existing until we get to
# the step where we'd pack it.
footer_file = list(input_path.glob("*.footer"))[0]
if footer_file.exists():
title.wad.set_meta_data(open(footer_file, "rb").read())
# Method to ensure that the title's content records match between the TMD() and ContentRegion() objects.
title.load_content_records()
# Iterate over every file in the content_files list, and attempt to load it into the Title().
for index in range(len(title.content.content_records)):
for content in range(len(content_files)):
dec_content = open(content_files[content], "rb").read()
try:
# Attempt to load the content into the correct index.
title.load_content(dec_content, index)
break
except ValueError:
# Wasn't the right content, so try again.
pass
output_path.write(title.dump_wad())
print("WAD file packed!")
# Code for if the --unpack argument was passed.
elif args.unpack:
if not input_path.exists():
raise FileNotFoundError(input_path)
if not output_path.is_dir():
output_path.mkdir()
# Step through each component of a WAD and dump it to a file.
with open(args.input, "rb") as wad_file:
title = libWiiPy.title.Title()
title.load_wad(wad_file.read())
cert_name = title.tmd.title_id + ".cert"
cert_out = open(output_path.joinpath(cert_name), "wb")
cert_out.write(title.wad.get_cert_data())
cert_out.close()
tmd_name = title.tmd.title_id + ".tmd"
tmd_out = open(output_path.joinpath(tmd_name), "wb")
tmd_out.write(title.wad.get_tmd_data())
tmd_out.close()
ticket_name = title.tmd.title_id + ".tik"
ticket_out = open(output_path.joinpath(ticket_name), "wb")
ticket_out.write(title.wad.get_ticket_data())
ticket_out.close()
meta_name = title.tmd.title_id + ".footer"
meta_out = open(output_path.joinpath(meta_name), "wb")
meta_out.write(title.wad.get_meta_data())
meta_out.close()
for content_file in range(0, title.tmd.num_contents):
content_file_name = "000000" + str(binascii.hexlify(content_file.to_bytes()).decode()) + ".app"
content_out = open(output_path.joinpath(content_file_name), "wb")
content_out.write(title.get_content_by_index(content_file))
content_out.close()
print("WAD file unpacked!")

View File

@@ -1,2 +1,2 @@
git+https://github.com/NinjaCheetah/libWiiPy git+https://github.com/NinjaCheetah/libWiiPy
nuitka nuitka==2.3.11

View File

@@ -0,0 +1,27 @@
import pathlib
import sys
import libWiiPy
target_hash = sys.argv[1].lower().encode()
print(target_hash)
for content in range(3, 81):
try:
tmd = libWiiPy.title.download_tmd(f"00000007000000{content:02X}")
open(f"00000007000000{content:02X}.tmd", "wb").write(tmd)
except ValueError:
pass
workdir = pathlib.Path(".")
tmd_files = list(workdir.glob("*.tmd"))
for tmd in tmd_files:
new_tmd = libWiiPy.title.TMD()
new_tmd.load(open(tmd, "rb").read())
hash_list = []
for content in new_tmd.content_records:
hash_list.append(content.content_hash)
if target_hash in hash_list:
print(f"Found match in {tmd}\n")

30
scripts/nus-scraper.py Normal file
View File

@@ -0,0 +1,30 @@
import os
import requests
tid_high = ["00010000", "00010001", "00010005"]
types = ["43", "44", "45", "46", "47", "48", "4A", "4C", "4D", "4E", "50", "51", "52", "53", "57", "58"]
regions = ["41", "42", "43", "44", "45", "46", "49", "4A", "4B", "4C", "4D", "4E", "50", "51", "53", "54", "55", "57", "58"]
for tid in tid_high:
print(f"Starting scrape for TID high {tid}...")
if os.path.exists(f"{tid}.log"):
os.remove(f"{tid}.log")
log = open(f"{tid}.log", "a")
for ttype in types:
print(f"Scraping titles of type: {ttype}")
for title in range(0, 65536):
for region in regions:
request = requests.get(url=f"http://ccs.cdn.wup.shop.nintendo.net/ccs/download/{tid}{ttype}{title:04X}{region}/tmd", headers={'User-Agent': 'wii libnup/1.0'}, stream=True)
if request.status_code == 200:
print(f"Found valid TID: {tid}{ttype}{title:04X}{region}")
log.write(f"{tid}{ttype}{title:02X}{region}")
else:
print(f"Invalid TID: {tid}{ttype}{title:04X}{region}")
pass
request.close()
log.close()

269
wiipy.py
View File

@@ -4,47 +4,24 @@
import argparse import argparse
from importlib.metadata import version from importlib.metadata import version
from modules.wad import * from modules.archive.ash import *
from modules.nus import * from modules.archive.u8 import *
from modules.u8 import * from modules.nand.emunand import *
from modules.ash import * from modules.nand.setting import *
from modules.title.ciosbuild import *
from modules.title.fakesign import *
from modules.title.info import *
from modules.title.iospatcher import *
from modules.title.nus import *
from modules.title.wad import *
if __name__ == "__main__": if __name__ == "__main__":
# Main argument parser. # Main argument parser.
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="WiiPy is a simple command line tool to manage file formats used by the Wii.") description="A simple command line tool to manage file formats used by the Wii.")
parser.add_argument("--version", action="version", parser.add_argument("--version", action="version",
version=f"WiiPy v1.0.0, based on libWiiPy v{version('libWiiPy')} (from branch \'main\')") version=f"WiiPy v1.4.0, based on libWiiPy v{version('libWiiPy')} (from branch \'main\')")
subparsers = parser.add_subparsers(dest="subcommand", required=True) subparsers = parser.add_subparsers(title="subcommands", dest="subcommand", required=True)
# Argument parser for the WAD subcommand.
wad_parser = subparsers.add_parser("wad", help="pack/unpack a WAD file",
description="pack/unpack a WAD file")
wad_parser.set_defaults(func=handle_wad)
wad_group = wad_parser.add_mutually_exclusive_group(required=True)
wad_group.add_argument("-p", "--pack", help="pack a directory to a WAD file", action="store_true")
wad_group.add_argument("-u", "--unpack", help="unpack a WAD file to a directory", action="store_true")
wad_parser.add_argument("input", metavar="IN", type=str, help="input file")
wad_parser.add_argument("output", metavar="OUT", type=str, help="output file")
# Argument parser for the NUS subcommand.
nus_parser = subparsers.add_parser("nus", help="download a title from the NUS",
description="download a title from the NUS")
nus_parser.set_defaults(func=handle_nus)
nus_parser.add_argument("tid", metavar="TID", type=str, help="Title ID to download")
nus_parser.add_argument("-v", "--version", metavar="VERSION", type=int,
help="version to download (optional)")
nus_parser.add_argument("-o", "--output", metavar="OUT", type=str, help="output file (optional)")
# Argument parser for the U8 subcommand.
u8_parser = subparsers.add_parser("u8", help="pack/unpack a U8 archive",
description="pack/unpack a U8 archive")
u8_parser.set_defaults(func=handle_u8)
u8_group = u8_parser.add_mutually_exclusive_group(required=True)
u8_group.add_argument("-p", "--pack", help="pack a directory to a U8 archive", action="store_true")
u8_group.add_argument("-u", "--unpack", help="unpack a U8 archive to a directory", action="store_true")
u8_parser.add_argument("input", metavar="IN", type=str, help="input file")
u8_parser.add_argument("output", metavar="OUT", type=str, help="output file")
# Argument parser for the ASH subcommand. # Argument parser for the ASH subcommand.
ash_parser = subparsers.add_parser("ash", help="compress/decompress an ASH file", ash_parser = subparsers.add_parser("ash", help="compress/decompress an ASH file",
@@ -60,6 +37,226 @@ if __name__ == "__main__":
ash_parser.add_argument("--dist-bits", metavar="DIST_BITS", type=int, ash_parser.add_argument("--dist-bits", metavar="DIST_BITS", type=int,
help="number of bits in each distance tree leaf (default: 11)", default=11) help="number of bits in each distance tree leaf (default: 11)", default=11)
# Argument parser for the cIOS command
cios_parser = subparsers.add_parser("cios", help="build a cIOS from a base IOS and provided map",
description="build a cIOS from a base IOS and provided map")
cios_parser.set_defaults(func=build_cios)
cios_parser.add_argument("base", metavar="BASE", type=str, help="base IOS WAD")
cios_parser.add_argument("map", metavar="MAP", type=str, help="cIOS map file")
cios_parser.add_argument("output", metavar="OUT", type=str, help="file to output the cIOS to")
cios_parser.add_argument("-c", "--cios-ver", metavar="CIOS", type=str,
help="cIOS version from the map to build", required=True)
cios_parser.add_argument("-m", "--modules", metavar="MODULES", type=str,
help="directory to look for cIOS modules in (optional, defaults to current directory)")
cios_parser.add_argument("-s", "--slot", metavar="SLOT", type=int,
help="slot that this cIOS will install to (optional, defaults to 249)", default=249)
cios_parser.add_argument("-v", "--version", metavar="VERSION", type=int,
help="version that this cIOS will be (optional, defaults to 65535)", default=65535)
# Argument parser for the EmuNAND subcommand.
emunand_parser = subparsers.add_parser("emunand", help="manage Wii EmuNAND directories",
description="manage Wii EmuNAND directories")
emunand_subparsers = emunand_parser.add_subparsers(title="emunand", dest="emunand", required=True)
# Title EmuNAND subcommand.
emunand_title_parser = emunand_subparsers.add_parser("title", help="manage titles on an EmuNAND",
description="manage titles on an EmuNAND")
emunand_title_parser.set_defaults(func=handle_emunand_title)
emunand_title_parser.add_argument("emunand", metavar="EMUNAND", type=str,
help="path to the target EmuNAND directory")
emunand_title_install_group = emunand_title_parser.add_mutually_exclusive_group(required=True)
emunand_title_install_group.add_argument("--install", metavar="WAD", type=str,
help="install the target WAD(s) to an EmuNAND (can be a single file or a "
"folder of WADs)")
emunand_title_install_group.add_argument("--uninstall", metavar="TID", type=str,
help="uninstall a title with the provided Title ID from an EmuNAND (also"
"accepts a WAD file to read the TID from)")
emunand_title_parser.add_argument("-s", "--skip-hash", help="skips validating the hashes of decrypted "
"content (install only)", action="store_true")
# Argument parser for the fakesign subcommand.
fakesign_parser = subparsers.add_parser("fakesign", help="fakesign a TMD, Ticket, or WAD (trucha bug)",
description="fakesign a TMD, Ticket, or WAD (trucha bug); by default, this "
"will overwrite the input file if no output file is specified")
fakesign_parser.set_defaults(func=handle_fakesign)
fakesign_parser.add_argument("input", metavar="IN", type=str, help="input file")
fakesign_parser.add_argument("-o", "--output", metavar="OUT", type=str, help="output file (optional)")
# Argument parser for the info command.
info_parser = subparsers.add_parser("info", help="get information about a Wii file",
description="get information about a Wii file")
info_parser.set_defaults(func=handle_info)
info_parser.add_argument("input", metavar="IN", type=str, help="input file")
# Argument parser for the iospatch command.
iospatch_parser = subparsers.add_parser("iospatch", help="patch IOS WADs to re-enable exploits",
description="patch IOS WADs to re-enable exploits; by default, this will"
"overwrite the input file in place unless you use -o/--output")
iospatch_parser.set_defaults(func=handle_iospatch)
iospatch_parser.add_argument("input", metavar="IN", type=str, help="input file")
iospatch_parser.add_argument("-o", "--output", metavar="OUT", type=str, help="output file (optional)")
iospatch_parser.add_argument("-fs", "--fakesigning", action="store_true", help="patch in fakesigning support")
iospatch_parser.add_argument("-ei", "--es-identify", action="store_true", help="patch in ES_Identify access")
iospatch_parser.add_argument("-na", "--nand-access", action="store_true", help="patch in /dev/flash access")
iospatch_parser.add_argument("-vd", "--version-downgrading", action="store_true",
help="patch in version downgrading support")
iospatch_parser.add_argument("-di", "--drive-inquiry", action="store_true",
help="patches out the drive inquiry check")
iospatch_parser.add_argument("-v", "--version", metavar="VERSION", type=int, help="set the IOS version")
iospatch_parser.add_argument("-s", "--slot", metavar="SLOT", type=int,
help="set the slot that this IOS will install to")
iospatch_parser.add_argument("-a", "--all", action="store_true", help="apply all patches (overrides other options)")
iospatch_parser.add_argument("-ns", "--no-shared", action="store_true",
help="set all patched content to be non-shared")
# Argument parser for the NUS subcommand.
nus_parser = subparsers.add_parser("nus", help="download data from the NUS",
description="download from the NUS")
nus_subparsers = nus_parser.add_subparsers(dest="subcommand", required=True)
# Title NUS subcommand.
nus_title_parser = nus_subparsers.add_parser("title", help="download a title from the NUS",
description="download a title from the NUS")
nus_title_parser.set_defaults(func=handle_nus_title)
nus_title_parser.add_argument("tid", metavar="TID", type=str, help="Title ID to download")
nus_title_parser.add_argument("-v", "--version", metavar="VERSION", type=int,
help="version to download (optional)")
nus_title_out_group_label = nus_title_parser.add_argument_group(title="output types (required)")
nus_title_out_group = nus_title_out_group_label.add_mutually_exclusive_group(required=True)
nus_title_out_group.add_argument("-o", "--output", metavar="OUT", type=str,
help="download the title to a folder")
nus_title_out_group.add_argument("-w", "--wad", metavar="WAD", type=str,
help="pack a wad with the provided name")
nus_title_parser.add_argument("--wii", help="use original Wii NUS instead of the Wii U servers",
action="store_true")
# Content NUS subcommand.
nus_content_parser = nus_subparsers.add_parser("content", help="download a specific content from the NUS",
description="download a specific content from the NUS")
nus_content_parser.set_defaults(func=handle_nus_content)
nus_content_parser.add_argument("tid", metavar="TID", type=str, help="Title ID the content belongs to")
nus_content_parser.add_argument("cid", metavar="CID", type=str,
help="Content ID to download (in \"000000xx\" format)")
nus_content_parser.add_argument("-v", "--version", metavar="VERSION", type=int,
help="version this content belongs to (required for decryption)")
nus_content_parser.add_argument("-o", "--output", metavar="OUT", type=str,
help="path to download the content to (optional)")
nus_content_parser.add_argument("-d", "--decrypt", action="store_true", help="decrypt this content")
# TMD NUS subcommand.
nus_tmd_parser = nus_subparsers.add_parser("tmd", help="download a tmd from the NUS",
description="download a tmd from the NUS")
nus_tmd_parser.set_defaults(func=handle_nus_tmd)
nus_tmd_parser.add_argument("tid", metavar="TID", type=str, help="Title ID the TMD is for")
nus_tmd_parser.add_argument("-v", "--version", metavar="VERSION", type=int, help="version of the TMD to download")
nus_tmd_parser.add_argument("-o", "--output", metavar="OUT", type=str,
help="path to download the TMD to (optional)")
# Argument parser for the setting subcommand.
setting_parser = subparsers.add_parser("setting", help="manage setting.txt",
description="manage setting.txt")
setting_subparsers = setting_parser.add_subparsers(dest="subcommand", required=True)
# Decrypt setting.txt subcommand.
setting_dec_parser = setting_subparsers.add_parser("decrypt", help="decrypt setting.txt",
description="decrypt setting.txt; by default, this will output "
"to setting_dec.txt")
setting_dec_parser.set_defaults(func=handle_setting_decrypt)
setting_dec_parser.add_argument("input", metavar="IN", type=str, help="encrypted setting.txt file to decrypt")
setting_dec_parser.add_argument("-o", "--output", metavar="OUT", type=str,
help="path to output the decrypted file to (optional)")
# Encrypt setting.txt subcommand.
setting_enc_parser = setting_subparsers.add_parser("encrypt", help="encrypt setting.txt",
description="encrypt setting.txt; by default, this will output "
"to setting.txt")
setting_enc_parser.set_defaults(func=handle_setting_encrypt)
setting_enc_parser.add_argument("input", metavar="IN", type=str, help="decrypted setting.txt file to encrypt")
setting_enc_parser.add_argument("-o", "--output", metavar="OUT", type=str,
help="path to output the encrypted file to (optional)")
# Generate setting.txt subcommand.
setting_gen_parser = setting_subparsers.add_parser("gen",
help="generate a new setting.txt based on the provided values",
description="generate a new setting.txt based on the provided values")
setting_gen_parser.set_defaults(func=handle_setting_gen)
setting_gen_parser.add_argument("serno", metavar="SERNO", type=str,
help="serial number of the console these settings are for")
setting_gen_parser.add_argument("region", metavar="REGION", type=str,
help="region of the console these settings are for (USA, EUR, JPN, or KOR)")
# Argument parser for the U8 subcommand.
u8_parser = subparsers.add_parser("u8", help="pack/unpack a U8 archive",
description="pack/unpack a U8 archive")
u8_parser.set_defaults(func=handle_u8)
u8_group = u8_parser.add_mutually_exclusive_group(required=True)
u8_group.add_argument("-p", "--pack", help="pack a directory to a U8 archive", action="store_true")
u8_group.add_argument("-u", "--unpack", help="unpack a U8 archive to a directory", action="store_true")
u8_parser.add_argument("input", metavar="IN", type=str, help="input file")
u8_parser.add_argument("output", metavar="OUT", type=str, help="output file")
# Argument parser for the WAD subcommand.
wad_parser = subparsers.add_parser("wad", help="pack/unpack a WAD file",
description="pack/unpack a WAD file")
wad_subparsers = wad_parser.add_subparsers(dest="subcommand", required=True)
# Add WAD subcommand.
wad_add_parser = wad_subparsers.add_parser("add", help="add decrypted content to a WAD file",
description="add decrypted content to a WAD file; by default, this "
"will overwrite the input file unless an output is specified")
wad_add_parser.set_defaults(func=handle_wad_add)
wad_add_parser.add_argument("input", metavar="IN", type=str, help="WAD file to add to")
wad_add_parser.add_argument("content", metavar="CONTENT", type=str, help="decrypted content to add")
wad_add_parser.add_argument("-c", "--cid", metavar="CID", type=str,
help="Content ID to assign the new content (optional, will be randomly assigned if "
"not specified)")
wad_add_parser.add_argument("-t", "--type", metavar="TYPE", type=str,
help="the type of the new content, can be \"Normal\", \"Shared\", or \"DLC\" "
"(optional, will default to \"Normal\" if not specified)")
wad_add_parser.add_argument("-o", "--output", metavar="OUT", type=str,
help="file to output the updated WAD to (optional)")
# Pack WAD subcommand.
wad_pack_parser = wad_subparsers.add_parser("pack", help="pack a directory to a WAD file",
description="pack a directory to a WAD file")
wad_pack_parser.set_defaults(func=handle_wad_pack)
wad_pack_parser.add_argument("input", metavar="IN", type=str, help="input directory")
wad_pack_parser.add_argument("output", metavar="OUT", type=str, help="WAD file to pack")
wad_pack_parser.add_argument("-f", "--fakesign", help="fakesign the TMD and Ticket (trucha bug)",
action="store_true")
# Remove WAD subcommand.
wad_remove_parser = wad_subparsers.add_parser("remove", help="remove content from a WAD file",
description="remove content from a WAD file, either by its CID or"
"by its index; by default, this will overwrite the input "
"file unless an output is specified")
wad_remove_parser.set_defaults(func=handle_wad_remove)
wad_remove_parser.add_argument("input", metavar="IN", type=str, help="WAD file to remove content from")
wad_remove_targets = wad_remove_parser.add_mutually_exclusive_group(required=True)
wad_remove_targets.add_argument("-i", "--index", metavar="INDEX", type=int,
help="index of the content to remove")
wad_remove_targets.add_argument("-c", "--cid", metavar="CID", type=str,
help="Content ID of the content to remove")
wad_remove_parser.add_argument("-o", "--output", metavar="OUT", type=str,
help="file to output the updated WAD to (optional)")
# Set WAD subcommand.
wad_set_parser = wad_subparsers.add_parser("set", help="set content in a WAD file",
description="replace existing content in a WAD file with new decrypted "
"data; by default, this will overwrite the input file "
"unless an output is specified")
wad_set_parser.set_defaults(func=handle_wad_set)
wad_set_parser.add_argument("input", metavar="IN", type=str, help="WAD file to replace content in")
wad_set_parser.add_argument("content", metavar="CONTENT", type=str, help="new decrypted content")
wad_set_targets = wad_set_parser.add_mutually_exclusive_group(required=True)
wad_set_targets.add_argument("-i", "--index", metavar="INDEX", type=int,
help="index of the content to replace")
wad_set_targets.add_argument("-c", "--cid", metavar="CID", type=str,
help="Content ID of the content to replace")
wad_set_parser.add_argument("-o", "--output", metavar="OUT", type=str,
help="file to output the updated WAD to (optional)")
wad_set_parser.add_argument("-t", "--type", metavar="TYPE", type=str,
help="specifies a new type for the content, can be \"Normal\", \"Shared\", or \"DLC\" "
"(optional)")
# Unpack WAD subcommand.
wad_unpack_parser = wad_subparsers.add_parser("unpack", help="unpack a WAD file to a directory",
description="unpack a WAD file to a directory")
wad_unpack_parser.set_defaults(func=handle_wad_unpack)
wad_unpack_parser.add_argument("input", metavar="IN", type=str, help="WAD file to unpack")
wad_unpack_parser.add_argument("output", metavar="OUT", type=str, help="output directory")
wad_unpack_parser.add_argument("-s", "--skip-hash", help="skips validating the hashes of decrypted "
"content", action="store_true")
# Parse all the args, and call the appropriate function with all of those args if a valid subcommand was passed. # Parse all the args, and call the appropriate function with all of those args if a valid subcommand was passed.
args = parser.parse_args() args = parser.parse_args()
args.func(args) args.func(args)