mirror of
https://github.com/NinjaCheetah/WiiPy.git
synced 2026-02-17 02:25:39 -05:00
Restructured command files, updated U8 command syntax to match others
This commit is contained in:
143
commands/title/ciosbuild.py
Normal file
143
commands/title/ciosbuild.py
Normal file
@@ -0,0 +1,143 @@
|
||||
# "commands/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!")
|
||||
print(f"Building cIOS \"{args.cios_ver}\" from base IOS{target_base.get('ios')} v{base_version}...")
|
||||
|
||||
# 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.
|
||||
print("Patching existing commands...")
|
||||
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 commands.
|
||||
print("Adding required additional commands...")
|
||||
for content in target_base.findall("content"):
|
||||
target_module = content.get("module")
|
||||
if target_module is not None:
|
||||
target_index = int(content.get("tmdmoduleid"), 16)
|
||||
# The cIOS map supplies a Content ID to use for each additional module.
|
||||
cid = int(content.get("id"), 16)
|
||||
target_path = modules_path.joinpath(target_module + ".app")
|
||||
if not target_path.exists():
|
||||
raise Exception(f"A required module \"{target_module}.app\" could not be found!")
|
||||
# Check where this module belongs. If it's -1, add it to the end. If it's any other value, this module needs
|
||||
# to go at the index specified.
|
||||
new_module = target_path.read_bytes()
|
||||
if target_index == -1:
|
||||
title.add_content(new_module, cid, libWiiPy.title.ContentType.NORMAL)
|
||||
else:
|
||||
existing_module = title.get_content_by_index(target_index)
|
||||
existing_cid = title.content.content_records[target_index].content_id
|
||||
existing_type = title.content.content_records[target_index].content_type
|
||||
title.set_content(new_module, target_index, cid, libWiiPy.title.ContentType.NORMAL)
|
||||
title.add_content(existing_module, existing_cid, existing_type)
|
||||
|
||||
# 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!")
|
||||
print(f"Set cIOS slot to \"{slot}\" and cIOS version to \"{args.version}\"!")
|
||||
|
||||
# If this is a vWii cIOS, then we need to re-encrypt it with the Wii Common key so that it's installable from
|
||||
# within Wii mode.
|
||||
title_key_dec = title.ticket.get_title_key()
|
||||
title_key_common = libWiiPy.title.encrypt_title_key(title_key_dec, 0, title.tmd.title_id)
|
||||
title.ticket.title_key_enc = title_key_common
|
||||
title.ticket.common_key_index = 0
|
||||
|
||||
# Ensure the WAD is fakesigned.
|
||||
title.fakesign()
|
||||
|
||||
# Write the new cIOS to the specified output path.
|
||||
output_path.write_bytes(title.dump_wad())
|
||||
|
||||
print(f"Successfully built cIOS \"{args.cios_ver}\"!")
|
||||
37
commands/title/fakesign.py
Normal file
37
commands/title/fakesign.py
Normal file
@@ -0,0 +1,37 @@
|
||||
# "commands/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.")
|
||||
154
commands/title/info.py
Normal file
154
commands/title/info.py
Normal file
@@ -0,0 +1,154 @@
|
||||
# "commands/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" 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:
|
||||
if ticket.is_dev:
|
||||
key = "Common (Development)"
|
||||
else:
|
||||
key = "Common (Retail)"
|
||||
case 1:
|
||||
key = "Korean"
|
||||
case 2:
|
||||
key = "vWii"
|
||||
case _:
|
||||
key = "Unknown (Likely Common)"
|
||||
print(f" Decryption 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 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
commands/title/iospatcher.py
Normal file
129
commands/title/iospatcher.py
Normal file
@@ -0,0 +1,129 @@
|
||||
# "commands/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!")
|
||||
234
commands/title/nus.py
Normal file
234
commands/title/nus.py
Normal file
@@ -0,0 +1,234 @@
|
||||
# "commands/title/nus.py" from WiiPy by NinjaCheetah
|
||||
# https://github.com/NinjaCheetah/WiiPy
|
||||
|
||||
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_file():
|
||||
raise ValueError("A file already exists with the provided directory name!")
|
||||
else:
|
||||
output_dir.mkdir()
|
||||
|
||||
# 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:
|
||||
output_dir.joinpath("tmd." + str(title_version)).write_bytes(title.tmd.dump())
|
||||
|
||||
# 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:
|
||||
output_dir.joinpath("tik").write_bytes(title.ticket.dump())
|
||||
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:
|
||||
output_dir.joinpath(content_file_name).write_bytes(content_list[content])
|
||||
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"
|
||||
output_dir.joinpath(content_file_name).write_bytes(dec_content)
|
||||
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.
|
||||
pathlib.Path(wad_file).write_bytes(title.dump_wad())
|
||||
|
||||
print("Downloaded title with Title ID \"" + args.tid + "\"!")
|
||||
|
||||
|
||||
def handle_nus_content(args):
|
||||
tid = args.tid
|
||||
cid = args.cid
|
||||
version = args.version
|
||||
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:
|
||||
raise ValueError("Invalid Content ID! Content ID must be in format \"000000xx\"!")
|
||||
|
||||
# Use the supplied output path if one was specified, otherwise generate one using the Content ID.
|
||||
if args.output is None:
|
||||
content_file_name = f"{content_id:08X}".lower()
|
||||
output_path = pathlib.Path(content_file_name)
|
||||
else:
|
||||
output_path = pathlib.Path(args.output)
|
||||
|
||||
# Ensure that a version was supplied before downloading, because we need the matching TMD for decryption to work.
|
||||
if decrypt_content is True and version is None:
|
||||
print("You must specify the version that the requested content belongs to for decryption!")
|
||||
return
|
||||
|
||||
# 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:
|
||||
raise ValueError("The Title ID or Content ID you specified could not be found!")
|
||||
|
||||
if decrypt_content is True:
|
||||
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))
|
||||
output_path.write_bytes(content_dec)
|
||||
else:
|
||||
output_path.write_bytes(content_data)
|
||||
|
||||
print(f"Downloaded content with Content ID \"{cid}\"!")
|
||||
|
||||
|
||||
def handle_nus_tmd(args):
|
||||
tid = args.tid
|
||||
|
||||
# Check if --version was passed, because it'll be None if it wasn't.
|
||||
if args.version is not None:
|
||||
try:
|
||||
version = int(args.version)
|
||||
except ValueError:
|
||||
raise ValueError("Enter a valid integer for the TMD Version.")
|
||||
else:
|
||||
version = None
|
||||
|
||||
# Use the supplied output path if one was specified, otherwise generate one using the Title ID. If a version has
|
||||
# been specified, append the version to the end of the path as well.
|
||||
if args.output is None:
|
||||
if version is not None:
|
||||
output_path = pathlib.Path(f"{tid}.tmd.{version}")
|
||||
else:
|
||||
output_path = pathlib.Path(f"{tid}.tmd")
|
||||
else:
|
||||
output_path = pathlib.Path(args.output)
|
||||
|
||||
# Try to download the TMD, and catch the ValueError libWiiPy will throw if it can't be found.
|
||||
print(f"Downloading TMD for title {tid}...")
|
||||
try:
|
||||
tmd_data = libWiiPy.title.download_tmd(tid, version)
|
||||
except ValueError:
|
||||
raise ValueError("The Title ID or version you specified could not be found!")
|
||||
|
||||
output_path.write_bytes(tmd_data)
|
||||
|
||||
print(f"Downloaded TMD for title \"{tid}\"!")
|
||||
47
commands/title/tmd.py
Normal file
47
commands/title/tmd.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# "commands/title/tmd.py" from WiiPy by NinjaCheetah
|
||||
# https://github.com/NinjaCheetah/WiiPy
|
||||
|
||||
import pathlib
|
||||
import libWiiPy
|
||||
|
||||
|
||||
def handle_tmd_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)
|
||||
|
||||
tmd = libWiiPy.title.TMD()
|
||||
tmd.load(input_path.read_bytes())
|
||||
|
||||
if args.index is not None:
|
||||
# Make sure the target index exists, then remove it from the TMD.
|
||||
if args.index >= len(tmd.content_records):
|
||||
raise ValueError("The provided index could not be found in this TMD!")
|
||||
tmd.content_records.pop(args.index)
|
||||
tmd.num_contents -= 1
|
||||
# Auto fakesign because we've edited the TMD.
|
||||
tmd.fakesign()
|
||||
output_path.write_bytes(tmd.dump())
|
||||
print(f"Removed content record at 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 tmd.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 TMD!")
|
||||
tmd.content_records.pop(valid_ids.index(target_cid))
|
||||
tmd.num_contents -= 1
|
||||
# Auto fakesign because we've edited the TMD.
|
||||
tmd.fakesign()
|
||||
output_path.write_bytes(tmd.dump())
|
||||
print(f"Removed content record with Content ID \"{target_cid:08X}\"!")
|
||||
370
commands/title/wad.py
Normal file
370
commands/title/wad.py
Normal file
@@ -0,0 +1,370 @@
|
||||
# "commands/title/wad.py" from WiiPy by NinjaCheetah
|
||||
# https://github.com/NinjaCheetah/WiiPy
|
||||
|
||||
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)
|
||||
|
||||
title = libWiiPy.title.Title()
|
||||
title.load_wad(input_path.read_bytes())
|
||||
content_data = content_path.read_bytes()
|
||||
|
||||
# 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()
|
||||
output_path.write_bytes(title.dump_wad())
|
||||
|
||||
print(f"Successfully added new content with Content ID \"{target_cid:08X}\" and type \"{target_type.name}\"!")
|
||||
|
||||
|
||||
def handle_wad_convert(args):
|
||||
input_path = pathlib.Path(args.input)
|
||||
if args.dev:
|
||||
target = "development"
|
||||
elif args.retail:
|
||||
target = "retail"
|
||||
elif args.vwii:
|
||||
target = "vWii"
|
||||
else:
|
||||
raise ValueError("No valid target was provided!")
|
||||
|
||||
if args.output is None:
|
||||
match target:
|
||||
case "development":
|
||||
output_path = pathlib.Path(input_path.stem + "_dev" + input_path.suffix)
|
||||
case "retail":
|
||||
output_path = pathlib.Path(input_path.stem + "_retail" + input_path.suffix)
|
||||
case "vWii":
|
||||
output_path = pathlib.Path(input_path.stem + "_vWii" + input_path.suffix)
|
||||
case _:
|
||||
raise ValueError("No valid target was provided!")
|
||||
else:
|
||||
output_path = pathlib.Path(args.output)
|
||||
|
||||
if not input_path.exists():
|
||||
raise FileNotFoundError(input_path)
|
||||
|
||||
title = libWiiPy.title.Title()
|
||||
title.load_wad(input_path.read_bytes())
|
||||
# First, verify that this WAD isn't already the type we're trying to convert to.
|
||||
if title.ticket.is_dev and target == "development":
|
||||
raise ValueError("This is already a development WAD!")
|
||||
elif not title.ticket.is_dev and not title.tmd.vwii and target == "retail":
|
||||
raise ValueError("This is already a retail WAD!")
|
||||
elif not title.ticket.is_dev and title.tmd.vwii and target == "vWii":
|
||||
raise ValueError("This is already a vWii WAD!")
|
||||
# Get the current type to display later.
|
||||
if title.ticket.is_dev:
|
||||
source = "development"
|
||||
elif title.tmd.vwii:
|
||||
source = "vWii"
|
||||
else:
|
||||
source = "retail"
|
||||
# Extract the Title Key so it can be re-encrypted with the correct key later.
|
||||
title_key = title.ticket.get_title_key()
|
||||
title_key_new = None
|
||||
if target == "development":
|
||||
# Set the development signature info.
|
||||
title.ticket.signature_issuer = "Root-CA00000002-XS00000006" + title.ticket.signature_issuer[26:]
|
||||
title.tmd.signature_issuer = "Root-CA00000002-CP00000007" + title.tmd.signature_issuer[26:]
|
||||
# Re-encrypt the title key with the dev key, and set that in the Ticket.
|
||||
title_key_new = libWiiPy.title.encrypt_title_key(title_key, 0, title.ticket.title_id, True)
|
||||
title.ticket.common_key_index = 0
|
||||
else:
|
||||
# Set the retail signature info.
|
||||
title.ticket.signature_issuer = "Root-CA00000001-XS00000003" + title.ticket.signature_issuer[26:]
|
||||
title.tmd.signature_issuer = "Root-CA00000001-CP00000004" + title.tmd.signature_issuer[26:]
|
||||
if target == "retail":
|
||||
title_key_new = libWiiPy.title.encrypt_title_key(title_key, 0, title.ticket.title_id, False)
|
||||
title.ticket.common_key_index = 0
|
||||
elif target == "vWii":
|
||||
title_key_new = libWiiPy.title.encrypt_title_key(title_key, 2, title.ticket.title_id, False)
|
||||
title.ticket.common_key_index = 2
|
||||
title.ticket.title_key_enc = title_key_new
|
||||
title.fakesign()
|
||||
output_path.write_bytes(title.dump_wad())
|
||||
print(f"Successfully converted {source} WAD to {target} WAD \"{output_path.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.")
|
||||
tmd_file = pathlib.Path(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.")
|
||||
ticket_file = pathlib.Path(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.")
|
||||
cert_file = pathlib.Path(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.")
|
||||
|
||||
# Open the output file, and load all the component files that we've now verified we have into a libWiiPy Title()
|
||||
# object.
|
||||
title = libWiiPy.title.Title()
|
||||
title.load_tmd(tmd_file.read_bytes())
|
||||
title.load_ticket(ticket_file.read_bytes())
|
||||
title.wad.set_cert_data(cert_file.read_bytes())
|
||||
# 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 = pathlib.Path(list(input_path.glob("*.[fF][oO][oO][tT][eE][rR]"))[0])
|
||||
if footer_file.exists():
|
||||
title.wad.set_meta_data(footer_file.read_bytes())
|
||||
# Method to ensure that the title's content records match between the TMD() and ContentRegion() objects.
|
||||
title.load_content_records()
|
||||
# Sort the contents based on the records. May still be kinda hacky.
|
||||
content_indices = []
|
||||
for record in title.content.content_records:
|
||||
content_indices.append(record.index)
|
||||
content_files_ordered = []
|
||||
for _ in content_files:
|
||||
content_files_ordered.append(None)
|
||||
for index in range(len(content_files)):
|
||||
target_index = content_indices.index(int(content_files[index].stem, 16))
|
||||
content_files_ordered[target_index] = content_files[index]
|
||||
# Iterate over every file in the content_files list, and set them in the Title().
|
||||
for index in range(title.content.num_contents):
|
||||
dec_content = content_files_ordered[index].read_bytes()
|
||||
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_bytes(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)
|
||||
|
||||
title = libWiiPy.title.Title()
|
||||
title.load_wad(input_path.read_bytes())
|
||||
|
||||
# TODO: see if this implementation is problematic now
|
||||
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()
|
||||
output_path.write_bytes(title.dump_wad())
|
||||
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()
|
||||
output_path.write_bytes(title.dump_wad())
|
||||
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(input_path.read_bytes())
|
||||
content_data = content_path.read_bytes()
|
||||
|
||||
# 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()
|
||||
output_path.write_bytes(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()
|
||||
output_path.write_bytes(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_file():
|
||||
raise ValueError("A file already exists with the provided directory name!")
|
||||
else:
|
||||
output_path.mkdir()
|
||||
|
||||
# Step through each component of a WAD and dump it to a file.
|
||||
title = libWiiPy.title.Title()
|
||||
title.load_wad(input_path.read_bytes())
|
||||
|
||||
cert_name = title.tmd.title_id + ".cert"
|
||||
output_path.joinpath(cert_name).write_bytes(title.wad.get_cert_data())
|
||||
|
||||
tmd_name = title.tmd.title_id + ".tmd"
|
||||
output_path.joinpath(tmd_name).write_bytes(title.wad.get_tmd_data())
|
||||
|
||||
ticket_name = title.tmd.title_id + ".tik"
|
||||
output_path.joinpath(ticket_name).write_bytes(title.wad.get_ticket_data())
|
||||
|
||||
meta_name = title.tmd.title_id + ".footer"
|
||||
output_path.joinpath(meta_name).write_bytes(title.wad.get_meta_data())
|
||||
|
||||
# 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_index = title.content.content_records[content_file].index
|
||||
content_file_name = f"{content_index:08X}".lower() + ".app"
|
||||
output_path.joinpath(content_file_name).write_bytes(title.get_content_by_index(content_file, skip_hash))
|
||||
|
||||
print("WAD file unpacked!")
|
||||
Reference in New Issue
Block a user