14 Commits

Author SHA1 Message Date
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
5 changed files with 253 additions and 35 deletions

View File

@@ -31,7 +31,7 @@ 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
- name: Prepare Package for Upload - name: Prepare Package for Upload
run: | run: |
mv wiipy.bin ~/wiipy mv wiipy.bin ~/wiipy
@@ -59,7 +59,7 @@ 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
- name: Prepare Package for Upload - name: Prepare Package for Upload
run: | run: |
mv wiipy.bin ~/wiipy mv wiipy.bin ~/wiipy
@@ -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:

View File

@@ -1,13 +1,23 @@
# "nus.py" from WiiPy by NinjaCheetah # "nus.py" from WiiPy by NinjaCheetah
# https://github.com/NinjaCheetah/WiiPy # https://github.com/NinjaCheetah/WiiPy
import os
import hashlib
import pathlib import pathlib
import binascii
import libWiiPy import libWiiPy
def handle_nus(args): def handle_nus_title(args):
title_version = None title_version = None
file_path = 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. # Check if --version was passed, because it'll be None if it wasn't.
if args.version is not None: if args.version is not None:
@@ -17,24 +27,199 @@ def handle_nus(args):
print("Enter a valid integer for the Title Version.") print("Enter a valid integer for the Title Version.")
return return
# If --output was passed, then save the file to the specified path (as long as it's valid). # 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: if args.output is not None:
file_path = pathlib.Path(args.output) output_dir = pathlib.Path(args.output)
if not file_path.parent.exists() or not file_path.parent.is_dir(): if output_dir.exists():
print("The specified output path does not exist!") 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 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. # Load the content records from the TMD, and begin iterating over the records.
title = libWiiPy.title.download_title(args.tid, title_version) 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
# If we haven't gotten a name yet, make one from the TID and version. # Try to decrypt the contents for this title if a ticket was available.
if file_path is None: if output_dir is not None:
file_path = pathlib.Path(args.tid + "-v" + str(title.tmd.title_version) + ".wad") 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 = hex(title.tmd.content_records[content].content_id)[2:]
while len(content_file_name) < 8:
content_file_name = "0" + content_file_name
content_file_name = content_file_name + ".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!")
wad_file = open(file_path, "wb") # If --wad was passed, pack a WAD and output that.
wad_file.write(title.dump_wad()) if wad_file is not None:
wad_file.close() # 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 + "\"!") 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 = hex(content_id)[2:]
while len(content_file_name) < 8:
content_file_name = "0" + content_file_name
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 + "\"!")

View File

@@ -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!")

View File

@@ -1,7 +1,9 @@
# "wad.py" from WiiPy by NinjaCheetah # "wad.py" from WiiPy by NinjaCheetah
# https://github.com/NinjaCheetah/WiiPy # https://github.com/NinjaCheetah/WiiPy
import os
import pathlib import pathlib
import hashlib
import binascii import binascii
import libWiiPy import libWiiPy
@@ -69,6 +71,10 @@ def handle_wad(args):
# 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()
# 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()
# Iterate over every file in the content_files list, and attempt to load it into the Title(). # 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 index in range(len(title.content.content_records)):
for content in range(len(content_files)): for content in range(len(content_files)):
@@ -89,8 +95,14 @@ def handle_wad(args):
elif args.unpack: elif args.unpack:
if not input_path.exists(): if not input_path.exists():
raise FileNotFoundError(input_path) raise FileNotFoundError(input_path)
if not output_path.is_dir(): # Check if the output path already exists, and if it does, ensure that it is both a directory and empty.
output_path.mkdir() if output_path.exists():
if output_path.is_dir() and next(os.scandir(output_path), None):
raise ValueError("Output folder is not empty!")
elif 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. # Step through each component of a WAD and dump it to a file.
with open(args.input, "rb") as wad_file: with open(args.input, "rb") as wad_file:

View File

@@ -14,7 +14,7 @@ if __name__ == "__main__":
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="WiiPy is a simple command line tool to manage file formats used by the Wii.") description="WiiPy is 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.2.1, based on libWiiPy v{version('libWiiPy')} (from branch \'main\')")
subparsers = parser.add_subparsers(dest="subcommand", required=True) subparsers = parser.add_subparsers(dest="subcommand", required=True)
# Argument parser for the WAD subcommand. # Argument parser for the WAD subcommand.
@@ -26,15 +26,40 @@ if __name__ == "__main__":
wad_group.add_argument("-u", "--unpack", help="unpack a WAD file to a directory", 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("input", metavar="IN", type=str, help="input file")
wad_parser.add_argument("output", metavar="OUT", type=str, help="output file") wad_parser.add_argument("output", metavar="OUT", type=str, help="output file")
wad_parser.add_argument("--fakesign", help="fakesign the TMD and Ticket (trucha bug)",
action="store_true")
# Argument parser for the NUS subcommand. # Argument parser for the NUS subcommand.
nus_parser = subparsers.add_parser("nus", help="download a title from the NUS", 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") description="download a title from the NUS")
nus_parser.set_defaults(func=handle_nus) nus_title_parser.set_defaults(func=handle_nus_title)
nus_parser.add_argument("tid", metavar="TID", type=str, help="Title ID to download") nus_title_parser.add_argument("tid", metavar="TID", type=str, help="Title ID to download")
nus_parser.add_argument("-v", "--version", metavar="VERSION", type=int, nus_title_parser.add_argument("-v", "--version", metavar="VERSION", type=int,
help="version to download (optional)") help="version to download (optional)")
nus_parser.add_argument("-o", "--output", metavar="OUT", type=str, help="output file (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")
# Argument parser for the U8 subcommand. # Argument parser for the U8 subcommand.
u8_parser = subparsers.add_parser("u8", help="pack/unpack a U8 archive", u8_parser = subparsers.add_parser("u8", help="pack/unpack a U8 archive",