mirror of
https://github.com/NinjaCheetah/WiiPy.git
synced 2026-02-17 02:25:39 -05:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
4c700266cb
|
|||
|
fa6ba28dbe
|
|||
|
3062a739d6
|
|||
|
bc1b6623bb
|
|||
|
d9c5940307
|
|||
|
f09806e002
|
|||
|
a863da98c7
|
|||
|
1b7c9afb53
|
|||
| fc8bef61a7 | |||
|
26b49ddff6
|
|||
|
b183336d95
|
|||
|
bb3c2737e5
|
|||
|
304539b120
|
|||
|
6af035068c
|
|||
| 8ec2a0d186 | |||
|
15e99af267
|
|||
|
744d738a8c
|
|||
|
9db9e3ad6f
|
|||
|
4e2f7b14e7
|
|||
| 5d8b9e7c08 | |||
| b1de3aa9c1 | |||
|
6ddb97eb6c
|
|||
|
b82b6f3873
|
|||
|
3115105343
|
|||
|
b2de9eb7dc
|
|||
|
a5afdc6d6a
|
|||
|
1f82aa61c7
|
|||
|
125ba4ea69
|
|||
|
7c4906f0db
|
|||
|
2066f8b4a2
|
|||
|
4ba95d0472
|
|||
|
9abdf4af04
|
|||
|
183498025a
|
|||
|
8599c43c2d
|
|||
|
1b603e94fc
|
|||
|
09631d509e
|
|||
|
475f82aa18
|
10
.github/workflows/python-build.yaml
vendored
10
.github/workflows/python-build.yaml
vendored
@@ -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:
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -25,6 +25,7 @@ share/python-wheels/
|
|||||||
*.build/
|
*.build/
|
||||||
*.dist/
|
*.dist/
|
||||||
*.onefile-build/
|
*.onefile-build/
|
||||||
|
wiipy
|
||||||
.installed.cfg
|
.installed.cfg
|
||||||
*.egg
|
*.egg
|
||||||
MANIFEST
|
MANIFEST
|
||||||
@@ -165,6 +166,9 @@ cython_debug/
|
|||||||
|
|
||||||
# Allows me to keep TMD files in my repository folder for testing without accidentally publishing them
|
# Allows me to keep TMD files in my repository folder for testing without accidentally publishing them
|
||||||
*.tmd
|
*.tmd
|
||||||
|
*.tik
|
||||||
|
*.cert
|
||||||
|
*.footer
|
||||||
*.wad
|
*.wad
|
||||||
*.app
|
*.app
|
||||||
*.arc
|
*.arc
|
||||||
|
|||||||
13
Makefile
Normal file
13
Makefile
Normal 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
|
||||||
25
README.md
25
README.md
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -1,95 +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
|
|
||||||
tid = args.tid
|
|
||||||
if args.wii:
|
|
||||||
use_wiiu_servers = False
|
|
||||||
else:
|
|
||||||
use_wiiu_servers = True
|
|
||||||
if args.verbose:
|
|
||||||
verbose = True
|
|
||||||
else:
|
|
||||||
verbose = False
|
|
||||||
|
|
||||||
# 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")
|
|
||||||
|
|
||||||
# Download the title from the NUS. This is done "manually" (as opposed to using download_title()) so that we can
|
|
||||||
# provide verbose output if desired.
|
|
||||||
title = libWiiPy.title.Title()
|
|
||||||
|
|
||||||
# Announce the title being downloaded, and the version if applicable.
|
|
||||||
if verbose:
|
|
||||||
if title_version is not None:
|
|
||||||
print("Downloading title " + tid + " v" + str(title_version) + ", please wait...")
|
|
||||||
else:
|
|
||||||
print("Downloading title " + tid + " vLatest, please wait...")
|
|
||||||
|
|
||||||
# Download a specific TMD version if a version was specified, otherwise just download the latest TMD.
|
|
||||||
if verbose:
|
|
||||||
print(" - Downloading and parsing TMD...")
|
|
||||||
if title_version is not None:
|
|
||||||
title.load_tmd(libWiiPy.title.download_tmd(tid, title_version, wiiu_endpoint=use_wiiu_servers))
|
|
||||||
else:
|
|
||||||
title.load_tmd(libWiiPy.title.download_tmd(tid, wiiu_endpoint=use_wiiu_servers))
|
|
||||||
|
|
||||||
# Download and parse the Ticket.
|
|
||||||
if verbose:
|
|
||||||
print(" - Downloading and parsing Ticket...")
|
|
||||||
try:
|
|
||||||
title.load_ticket(libWiiPy.title.download_ticket(tid, wiiu_endpoint=use_wiiu_servers))
|
|
||||||
except ValueError:
|
|
||||||
# If libWiiPy returns an error, then no ticket is available, so we can't continue.
|
|
||||||
print("No Ticket is available for this title! Exiting...")
|
|
||||||
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)):
|
|
||||||
if verbose:
|
|
||||||
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=use_wiiu_servers))
|
|
||||||
if verbose:
|
|
||||||
print(" - Done!")
|
|
||||||
title.content.content_list = content_list
|
|
||||||
|
|
||||||
# Get the WAD certificate chain.
|
|
||||||
if verbose:
|
|
||||||
print(" - Building certificate...")
|
|
||||||
title.wad.set_cert_data(libWiiPy.title.download_cert(wiiu_endpoint=use_wiiu_servers))
|
|
||||||
|
|
||||||
# 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 + "\"!")
|
|
||||||
57
modules/title/emunand.py
Normal file
57
modules/title/emunand.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# "modules/title/emunand.py" from WiiPy by NinjaCheetah
|
||||||
|
# https://github.com/NinjaCheetah/WiiPy
|
||||||
|
|
||||||
|
import pathlib
|
||||||
|
import libWiiPy
|
||||||
|
|
||||||
|
|
||||||
|
def handle_emunand_title(args):
|
||||||
|
emunand = libWiiPy.title.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 = args.install
|
||||||
|
|
||||||
|
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!")
|
||||||
37
modules/title/fakesign.py
Normal file
37
modules/title/fakesign.py
Normal 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.")
|
||||||
119
modules/title/info.py
Normal file
119
modules/title/info.py
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
# "modules/title/info.py" from WiiPy by NinjaCheetah
|
||||||
|
# https://github.com/NinjaCheetah/WiiPy
|
||||||
|
|
||||||
|
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" IOS Version: N/A")
|
||||||
|
else:
|
||||||
|
print(f" Required IOS: IOS{int(tmd.ios_tid[-2:], 16)} ({tmd.ios_tid})")
|
||||||
|
if tmd.issuer.decode().find("CP00000004") != 1:
|
||||||
|
print(f" Certificate: CP00000004 (Retail)")
|
||||||
|
print(f" Certificate Issuer: Root-CA00000001")
|
||||||
|
elif tmd.issuer.decode().find("CP00000007") != 1:
|
||||||
|
print(f" Certificate: CP00000007 (Development)")
|
||||||
|
print(f" Certificate Issuer: Root-CA00000002")
|
||||||
|
elif tmd.issuer.decode().find("CP10000000") != 1:
|
||||||
|
print(f" Certificate: CP10000000 (Arcade)")
|
||||||
|
print(f" Certificate Issuer: Root-CA10000000")
|
||||||
|
print(f" Region: {tmd.get_title_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}")
|
||||||
|
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")
|
||||||
|
elif ticket.signature_issuer.find("XS00000006") != 1:
|
||||||
|
print(f" Certificate: XS00000006 (Development)")
|
||||||
|
print(f" Certificate Issuer: Root-CA00000002")
|
||||||
|
else:
|
||||||
|
print(f" Certificate Info: {ticket.signature_issuer}")
|
||||||
|
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})")
|
||||||
|
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 handle_info(args):
|
||||||
|
input_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())
|
||||||
|
_print_tmd_info(tmd)
|
||||||
|
elif input_path.suffix.lower() == ".tik":
|
||||||
|
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)
|
||||||
|
else:
|
||||||
|
raise TypeError("This does not appear to be a TMD, Ticket, or WAD! No info can be provided.")
|
||||||
129
modules/title/iospatcher.py
Normal file
129
modules/title/iospatcher.py
Normal 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
254
modules/title/nus.py
Normal 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 + "\"!")
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
# "wad.py" from WiiPy by NinjaCheetah
|
# "modules/title/wad.py" from WiiPy by NinjaCheetah
|
||||||
# https://github.com/NinjaCheetah/WiiPy
|
# https://github.com/NinjaCheetah/WiiPy
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
import binascii
|
|
||||||
import libWiiPy
|
import libWiiPy
|
||||||
|
|
||||||
|
|
||||||
@@ -22,7 +21,7 @@ def handle_wad(args):
|
|||||||
|
|
||||||
# Get a list of all files ending in .tmd, and then make sure that that list has *only* 1 entry. More than 1
|
# 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.
|
# 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'))
|
tmd_list = list(input_path.glob('*.[tT][mM][dD]'))
|
||||||
if len(tmd_list) > 1:
|
if len(tmd_list) > 1:
|
||||||
raise FileExistsError("More than one TMD file was found! Only one TMD can be packed into a WAD.")
|
raise FileExistsError("More than one TMD file was found! Only one TMD can be packed into a WAD.")
|
||||||
elif len(tmd_list) == 0:
|
elif len(tmd_list) == 0:
|
||||||
@@ -31,7 +30,7 @@ def handle_wad(args):
|
|||||||
tmd_file = tmd_list[0]
|
tmd_file = tmd_list[0]
|
||||||
|
|
||||||
# Repeat the same process as above for all .tik files.
|
# Repeat the same process as above for all .tik files.
|
||||||
ticket_list = list(input_path.glob('*.tik'))
|
ticket_list = list(input_path.glob('*.[tT][iI][kK]'))
|
||||||
if len(ticket_list) > 1:
|
if len(ticket_list) > 1:
|
||||||
raise FileExistsError("More than one Ticket file was found! Only one Ticket can be packed into a WAD.")
|
raise FileExistsError("More than one Ticket file was found! Only one Ticket can be packed into a WAD.")
|
||||||
elif len(ticket_list) == 0:
|
elif len(ticket_list) == 0:
|
||||||
@@ -40,7 +39,7 @@ def handle_wad(args):
|
|||||||
ticket_file = ticket_list[0]
|
ticket_file = ticket_list[0]
|
||||||
|
|
||||||
# And one more time for all .cert files.
|
# And one more time for all .cert files.
|
||||||
cert_list = list(input_path.glob('*.cert'))
|
cert_list = list(input_path.glob('*.[cC][eE][rR][tT]'))
|
||||||
if len(cert_list) > 1:
|
if len(cert_list) > 1:
|
||||||
raise FileExistsError("More than one certificate file was found! Only one certificate can be packed into a "
|
raise FileExistsError("More than one certificate file was found! Only one certificate can be packed into a "
|
||||||
"WAD.")
|
"WAD.")
|
||||||
@@ -50,10 +49,18 @@ def handle_wad(args):
|
|||||||
cert_file = cert_list[0]
|
cert_file = cert_list[0]
|
||||||
|
|
||||||
# Make sure that there's at least one content to pack.
|
# Make sure that there's at least one content to pack.
|
||||||
content_files = list(input_path.glob("*.app"))
|
content_files = list(input_path.glob("*.[aA][pP][pP]"))
|
||||||
if not content_files:
|
if not content_files:
|
||||||
raise FileNotFoundError("No contents found! Cannot pack WAD.")
|
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()
|
# Open the output file, and load all the component files that we've now verified we have into a libWiiPy Title()
|
||||||
# object.
|
# object.
|
||||||
with open(output_path, "wb") as output_path:
|
with open(output_path, "wb") as output_path:
|
||||||
@@ -64,28 +71,21 @@ def handle_wad(args):
|
|||||||
title.wad.set_cert_data(open(cert_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
|
# 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.
|
# the step where we'd pack it.
|
||||||
footer_file = list(input_path.glob("*.footer"))[0]
|
footer_file = list(input_path.glob("*.[fF][oO][oO][tT][eE][rR]"))[0]
|
||||||
if footer_file.exists():
|
if footer_file.exists():
|
||||||
title.wad.set_meta_data(open(footer_file, "rb").read())
|
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.
|
# Method to ensure that the title's content records match between the TMD() and ContentRegion() objects.
|
||||||
title.load_content_records()
|
title.load_content_records()
|
||||||
|
|
||||||
# Nullify TMD/Ticket signatures here if the argument was passed.
|
# Iterate over every file in the content_files list, and set them in the Title().
|
||||||
if args.null_sigs:
|
for record in title.content.content_records:
|
||||||
title.tmd.signature = b'\x00' * 256
|
index = title.content.content_records.index(record)
|
||||||
title.ticket.signature = b'\x00' * 256
|
dec_content = open(content_files_ordered[index], "rb").read()
|
||||||
|
title.set_content(dec_content, index)
|
||||||
|
|
||||||
# Iterate over every file in the content_files list, and attempt to load it into the Title().
|
# Fakesign the TMD and Ticket using the trucha bug, if enabled. This is built-in in libWiiPy v0.4.1+.
|
||||||
for index in range(len(title.content.content_records)):
|
if args.fakesign:
|
||||||
for content in range(len(content_files)):
|
title.fakesign()
|
||||||
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())
|
output_path.write(title.dump_wad())
|
||||||
|
|
||||||
@@ -97,10 +97,10 @@ def handle_wad(args):
|
|||||||
raise FileNotFoundError(input_path)
|
raise FileNotFoundError(input_path)
|
||||||
# Check if the output path already exists, and if it does, ensure that it is both a directory and empty.
|
# 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.exists():
|
||||||
if output_path.is_dir() and next(os.scandir(output_path), None):
|
# if output_path.is_dir() and next(os.scandir(output_path), None):
|
||||||
raise ValueError("Output folder is not empty!")
|
# raise ValueError("Output folder is not empty!")
|
||||||
elif output_path.is_file():
|
if output_path.is_file():
|
||||||
raise ValueError("A file already exists with the provided name!")
|
raise ValueError("A file already exists with the provided directory name!")
|
||||||
else:
|
else:
|
||||||
os.mkdir(output_path)
|
os.mkdir(output_path)
|
||||||
|
|
||||||
@@ -129,10 +129,16 @@ def handle_wad(args):
|
|||||||
meta_out.write(title.wad.get_meta_data())
|
meta_out.write(title.wad.get_meta_data())
|
||||||
meta_out.close()
|
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):
|
for content_file in range(0, title.tmd.num_contents):
|
||||||
content_file_name = "000000" + str(binascii.hexlify(content_file.to_bytes()).decode()) + ".app"
|
content_file_name = f"{content_file:08X}".lower() + ".app"
|
||||||
content_out = open(output_path.joinpath(content_file_name), "wb")
|
content_out = open(output_path.joinpath(content_file_name), "wb")
|
||||||
content_out.write(title.get_content_by_index(content_file))
|
content_out.write(title.get_content_by_index(content_file, skip_hash))
|
||||||
content_out.close()
|
content_out.close()
|
||||||
|
|
||||||
print("WAD file unpacked!")
|
print("WAD file unpacked!")
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
git+https://github.com/NinjaCheetah/libWiiPy
|
git+https://github.com/NinjaCheetah/libWiiPy
|
||||||
nuitka
|
nuitka==2.3.11
|
||||||
|
|||||||
27
scripts/content-checker.py
Normal file
27
scripts/content-checker.py
Normal 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")
|
||||||
28
scripts/nus-scraper.py
Normal file
28
scripts/nus-scraper.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
import libWiiPy
|
||||||
|
|
||||||
|
tid_high = ["00010000", "00010001", "00010005"]
|
||||||
|
types = ["43", "44", "45", "46", "47", "48", "4A", "4C", "4D", "4E", "50", "51", "52", "53", "57", "58"]
|
||||||
|
regions = ["45", "4A", "4B", "50"]
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
||||||
|
try:
|
||||||
|
tmd = libWiiPy.title.download_tmd(f"{tid}{ttype}{title:04X}{region}")
|
||||||
|
print(f"Found valid TID: {tid}{ttype}{title:04X}{region}")
|
||||||
|
log.write(f"{tid}{ttype}{title:02X}{region}")
|
||||||
|
except ValueError:
|
||||||
|
print(f"Invalid TID: {tid}{ttype}{title:04X}{region}")
|
||||||
|
pass
|
||||||
|
log.close()
|
||||||
174
wiipy.py
174
wiipy.py
@@ -4,53 +4,22 @@
|
|||||||
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.title.emunand import *
|
||||||
from modules.ash 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.1.0, based on libWiiPy v{version('libWiiPy')} (from branch \'main\')")
|
version=f"WiiPy v1.3.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")
|
|
||||||
wad_parser.add_argument("--null-sigs", help="nullify signatures in the TMD and Ticket (packing only)",
|
|
||||||
action="store_true")
|
|
||||||
|
|
||||||
# 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)")
|
|
||||||
nus_parser.add_argument("--verbose", help="output more information about the current download",
|
|
||||||
action="store_true")
|
|
||||||
nus_parser.add_argument("-w", "--wii", help="use original Wii NUS instead of the Wii U servers",
|
|
||||||
action="store_true")
|
|
||||||
|
|
||||||
# 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",
|
||||||
@@ -66,6 +35,127 @@ 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 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 TMD, Ticket, or WAD",
|
||||||
|
description="get information about a TMD, Ticket, or WAD")
|
||||||
|
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 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_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")
|
||||||
|
wad_pack_group = wad_parser.add_argument_group(title="packing options")
|
||||||
|
wad_pack_group.add_argument("-f", "--fakesign", help="fakesign the TMD and Ticket (trucha bug)",
|
||||||
|
action="store_true")
|
||||||
|
wad_unpack_group = wad_parser.add_argument_group(title="unpacking options")
|
||||||
|
wad_unpack_group.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)
|
||||||
|
|||||||
Reference in New Issue
Block a user