mirror of
https://github.com/NinjaCheetah/WiiPy.git
synced 2026-03-05 01:55:29 -05:00
Compare commits
11 Commits
a6388834e0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
ebd6702056
|
|||
|
4b7eae8e85
|
|||
|
935f5eb7df
|
|||
|
dfb527388c
|
|||
|
46784f126e
|
|||
|
8e7489ec57
|
|||
| b123005bf5 | |||
|
5f751acabd
|
|||
|
71c0726c4f
|
|||
| 1410dd6c36 | |||
|
9a89f80247
|
19
.github/workflows/python-build.yaml
vendored
19
.github/workflows/python-build.yaml
vendored
@@ -19,7 +19,9 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Install ccache for Nuitka
|
- name: Install ccache for Nuitka
|
||||||
run: sudo apt update && sudo apt install -y ccache libicu70
|
run: |
|
||||||
|
sudo apt update && \
|
||||||
|
sudo apt install -y ccache patchelf
|
||||||
- name: Set up Python 3.12
|
- name: Set up Python 3.12
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
@@ -36,7 +38,7 @@ jobs:
|
|||||||
cd ~
|
cd ~
|
||||||
tar cvf WiiPy.tar wiipy
|
tar cvf WiiPy.tar wiipy
|
||||||
- name: Upload Application
|
- name: Upload Application
|
||||||
uses: actions/upload-artifact@v4.3.0
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
path: ~/WiiPy.tar
|
path: ~/WiiPy.tar
|
||||||
name: WiiPy-Linux-bin
|
name: WiiPy-Linux-bin
|
||||||
@@ -63,7 +65,7 @@ jobs:
|
|||||||
cd ~
|
cd ~
|
||||||
tar cvf WiiPy.tar wiipy
|
tar cvf WiiPy.tar wiipy
|
||||||
- name: Upload Application
|
- name: Upload Application
|
||||||
uses: actions/upload-artifact@v4.3.0
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
path: ~/WiiPy.tar
|
path: ~/WiiPy.tar
|
||||||
name: WiiPy-macOS-x86_64-bin
|
name: WiiPy-macOS-x86_64-bin
|
||||||
@@ -90,7 +92,7 @@ jobs:
|
|||||||
cd ~
|
cd ~
|
||||||
tar cvf WiiPy.tar wiipy
|
tar cvf WiiPy.tar wiipy
|
||||||
- name: Upload Application
|
- name: Upload Application
|
||||||
uses: actions/upload-artifact@v4.3.0
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
path: ~/WiiPy.tar
|
path: ~/WiiPy.tar
|
||||||
name: WiiPy-macOS-arm64-bin
|
name: WiiPy-macOS-arm64-bin
|
||||||
@@ -114,7 +116,12 @@ jobs:
|
|||||||
- name: Build Application
|
- name: Build Application
|
||||||
run: .\Build.ps1
|
run: .\Build.ps1
|
||||||
- name: Upload Application
|
- name: Upload Application
|
||||||
uses: actions/upload-artifact@v4.3.0
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
path: D:\a\WiiPy\WiiPy\WiiPy.dist
|
||||||
|
name: WiiPy-Windows-bin
|
||||||
|
- name: Upload Onefile Application
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
path: D:\a\WiiPy\WiiPy\wiipy.exe
|
path: D:\a\WiiPy\WiiPy\wiipy.exe
|
||||||
name: WiiPy-Windows-bin
|
name: WiiPy-Windows-onefile-bin
|
||||||
|
|||||||
41
commands/archive/lz77.py
Normal file
41
commands/archive/lz77.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# "commands/archive/lz77.py" from WiiPy by NinjaCheetah
|
||||||
|
# https://github.com/NinjaCheetah/WiiPy
|
||||||
|
|
||||||
|
import pathlib
|
||||||
|
import libWiiPy
|
||||||
|
from modules.core import fatal_error
|
||||||
|
|
||||||
|
|
||||||
|
def handle_lz77_compress(args):
|
||||||
|
input_path = pathlib.Path(args.input)
|
||||||
|
if args.output is not None:
|
||||||
|
output_path = pathlib.Path(args.output)
|
||||||
|
else:
|
||||||
|
output_path = pathlib.Path(input_path.name + ".lz77")
|
||||||
|
|
||||||
|
if not input_path.exists():
|
||||||
|
fatal_error(f"The specified file \"{input_path}\" does not exist!")
|
||||||
|
|
||||||
|
lz77_data = input_path.read_bytes()
|
||||||
|
data = libWiiPy.archive.compress_lz77(lz77_data)
|
||||||
|
output_path.write_bytes(data)
|
||||||
|
|
||||||
|
print("LZ77 file compressed!")
|
||||||
|
|
||||||
|
|
||||||
|
def handle_lz77_decompress(args):
|
||||||
|
input_path = pathlib.Path(args.input)
|
||||||
|
if args.output is not None:
|
||||||
|
output_path = pathlib.Path(args.output)
|
||||||
|
else:
|
||||||
|
output_path = pathlib.Path(input_path.name + ".out")
|
||||||
|
|
||||||
|
if not input_path.exists():
|
||||||
|
fatal_error(f"The specified file \"{input_path}\" does not exist!")
|
||||||
|
|
||||||
|
lz77_data = input_path.read_bytes()
|
||||||
|
data = libWiiPy.archive.decompress_lz77(lz77_data)
|
||||||
|
output_path.write_bytes(data)
|
||||||
|
|
||||||
|
print("LZ77 file decompressed!")
|
||||||
|
|
||||||
@@ -26,8 +26,15 @@ def handle_u8_unpack(args):
|
|||||||
|
|
||||||
if not input_path.exists():
|
if not input_path.exists():
|
||||||
fatal_error(f"The specified input file \"{input_path}\" does not exist!")
|
fatal_error(f"The specified input file \"{input_path}\" does not exist!")
|
||||||
|
|
||||||
|
u8_data = input_path.read_bytes()
|
||||||
|
# U8 archives are sometimes compressed. In the event that the provided data is LZ77 data, assume it's a compressed
|
||||||
|
# U8 archive and decompress it before continuing. Standard checks will then catch it if it was something else.
|
||||||
|
if u8_data[0:4] == b'LZ77':
|
||||||
|
u8_data = libWiiPy.archive.decompress_lz77(u8_data)
|
||||||
|
|
||||||
# Output path is deliberately not checked in any way because libWiiPy already has those checks, and it's easier
|
# Output path is deliberately not checked in any way because libWiiPy already has those checks, and it's easier
|
||||||
# and cleaner to only have one component doing all the checks.
|
# and cleaner to only have one component doing all the checks.
|
||||||
libWiiPy.archive.extract_u8(input_path.read_bytes(), str(output_path))
|
libWiiPy.archive.extract_u8(u8_data, str(output_path))
|
||||||
|
|
||||||
print("U8 archive unpacked!")
|
print("U8 archive unpacked!")
|
||||||
|
|||||||
@@ -78,7 +78,8 @@ def handle_emunand_info(args):
|
|||||||
else:
|
else:
|
||||||
print(f" IOS{int(ios[-2:], 16)} ({ios.upper()})")
|
print(f" IOS{int(ios[-2:], 16)} ({ios.upper()})")
|
||||||
tmd = emunand.get_title_tmd(ios)
|
tmd = emunand.get_title_tmd(ios)
|
||||||
print(f" Version: {tmd.title_version} ({tmd.title_version_converted})")
|
print(f" Version: {tmd.title_version} "
|
||||||
|
f"({libWiiPy.title.title_ver_dec_to_standard(tmd.title_version, tmd.title_id, bool(tmd.vwii))})")
|
||||||
print("")
|
print("")
|
||||||
|
|
||||||
print(f"Installed Titles:")
|
print(f"Installed Titles:")
|
||||||
@@ -180,8 +181,13 @@ def handle_emunand_install_missing(args):
|
|||||||
print(f"\nAll missing IOSes have been installed!")
|
print(f"\nAll missing IOSes have been installed!")
|
||||||
|
|
||||||
|
|
||||||
|
def _emunand_logger(log):
|
||||||
|
print(log)
|
||||||
|
|
||||||
|
|
||||||
def handle_emunand_title(args):
|
def handle_emunand_title(args):
|
||||||
emunand = libWiiPy.nand.EmuNAND(args.emunand)
|
logger = _emunand_logger if args.verbose else lambda _: None
|
||||||
|
emunand = libWiiPy.nand.EmuNAND(args.emunand, logger)
|
||||||
if args.skip_hash:
|
if args.skip_hash:
|
||||||
skip_hash = True
|
skip_hash = True
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
import binascii
|
import binascii
|
||||||
import pathlib
|
import pathlib
|
||||||
import re
|
import re
|
||||||
|
|
||||||
import libWiiPy
|
import libWiiPy
|
||||||
|
|
||||||
from modules.core import fatal_error
|
from modules.core import fatal_error
|
||||||
|
|
||||||
|
|
||||||
@@ -22,11 +24,12 @@ def _print_tmd_info(tmd: libWiiPy.title.TMD, signing_cert=None):
|
|||||||
else:
|
else:
|
||||||
print(f" Title ID: {tmd.title_id.upper()}")
|
print(f" Title ID: {tmd.title_id.upper()}")
|
||||||
# This type of version number really only applies to the System Menu and IOS.
|
# This type of version number really only applies to the System Menu and IOS.
|
||||||
|
title_version_converted = libWiiPy.title.title_ver_dec_to_standard(tmd.title_version, tmd.title_id, bool(tmd.vwii))
|
||||||
if tmd.title_id.startswith("00000001"):
|
if tmd.title_id.startswith("00000001"):
|
||||||
if tmd.title_id == "0000000100000001":
|
if tmd.title_id == "0000000100000001":
|
||||||
print(f" Title Version: {tmd.title_version} (boot2v{tmd.title_version})")
|
print(f" Title Version: {tmd.title_version} (boot2v{tmd.title_version})")
|
||||||
else:
|
else:
|
||||||
print(f" Title Version: {tmd.title_version} ({tmd.title_version_converted})")
|
print(f" Title Version: {tmd.title_version} ({title_version_converted})")
|
||||||
else:
|
else:
|
||||||
print(f" Title Version: {tmd.title_version}")
|
print(f" Title Version: {tmd.title_version}")
|
||||||
print(f" TMD Version: {tmd.tmd_version}")
|
print(f" TMD Version: {tmd.tmd_version}")
|
||||||
@@ -43,13 +46,16 @@ def _print_tmd_info(tmd: libWiiPy.title.TMD, signing_cert=None):
|
|||||||
elif tmd.signature_issuer.find("CP00000007") != -1:
|
elif tmd.signature_issuer.find("CP00000007") != -1:
|
||||||
print(f" Certificate: CP00000007 (Development)")
|
print(f" Certificate: CP00000007 (Development)")
|
||||||
print(f" Certificate Issuer: Root-CA00000002 (Development)")
|
print(f" Certificate Issuer: Root-CA00000002 (Development)")
|
||||||
|
elif tmd.signature_issuer.find("CP00000005") != -1:
|
||||||
|
print(f" Certificate: CP00000005 (Development/Unknown)")
|
||||||
|
print(f" Certificate Issuer: Root-CA00000002 (Development)")
|
||||||
elif tmd.signature_issuer.find("CP10000000") != -1:
|
elif tmd.signature_issuer.find("CP10000000") != -1:
|
||||||
print(f" Certificate: CP10000000 (Arcade)")
|
print(f" Certificate: CP10000000 (Arcade)")
|
||||||
print(f" Certificate Issuer: Root-CA10000000 (Arcade)")
|
print(f" Certificate Issuer: Root-CA10000000 (Arcade)")
|
||||||
else:
|
else:
|
||||||
print(f" Certificate Info: {tmd.signature_issuer} (Unknown)")
|
print(f" Certificate Info: {tmd.signature_issuer} (Unknown)")
|
||||||
if tmd.title_id == "0000000100000002":
|
if tmd.title_id == "0000000100000002":
|
||||||
match tmd.title_version_converted[-1:]:
|
match title_version_converted[-1:]:
|
||||||
case "U":
|
case "U":
|
||||||
region = "USA"
|
region = "USA"
|
||||||
case "E":
|
case "E":
|
||||||
@@ -67,23 +73,22 @@ def _print_tmd_info(tmd: libWiiPy.title.TMD, signing_cert=None):
|
|||||||
print(f" Region: {region}")
|
print(f" Region: {region}")
|
||||||
print(f" Title Type: {tmd.get_title_type()}")
|
print(f" Title Type: {tmd.get_title_type()}")
|
||||||
print(f" vWii Title: {bool(tmd.vwii)}")
|
print(f" vWii Title: {bool(tmd.vwii)}")
|
||||||
print(f" DVD Video Access: {tmd.get_access_right(tmd.AccessFlags.DVD_VIDEO)}")
|
print(f" DVD Video Access: {tmd.get_access_right(libWiiPy.title.AccessFlags.DVD_VIDEO)}")
|
||||||
print(f" AHB Access: {tmd.get_access_right(tmd.AccessFlags.AHB)}")
|
print(f" AHB Access: {tmd.get_access_right(libWiiPy.title.AccessFlags.AHB)}")
|
||||||
if signing_cert is not None:
|
if signing_cert is not None:
|
||||||
try:
|
try:
|
||||||
signed = libWiiPy.title.verify_tmd_sig(signing_cert, tmd)
|
if libWiiPy.title.verify_tmd_sig(signing_cert, tmd):
|
||||||
if signed:
|
signing_str = "Valid (Unmodified TMD)"
|
||||||
signing_str = "Valid (Unmodified)"
|
|
||||||
elif tmd.get_is_fakesigned():
|
elif tmd.get_is_fakesigned():
|
||||||
signing_str = "Fakesigned"
|
signing_str = "Fakesigned"
|
||||||
else:
|
else:
|
||||||
signing_str = "Invalid (Modified)"
|
signing_str = "Invalid (Modified TMD)"
|
||||||
except ValueError:
|
except ValueError:
|
||||||
if tmd.get_is_fakesigned():
|
if tmd.get_is_fakesigned():
|
||||||
signing_str = "Fakesigned"
|
signing_str = "Fakesigned"
|
||||||
else:
|
else:
|
||||||
signing_str = "Invalid (Modified)"
|
signing_str = "Invalid (Modified TMD)"
|
||||||
print(f" Signing Status: {signing_str}")
|
print(f" Signature: {signing_str}")
|
||||||
else:
|
else:
|
||||||
print(f" Fakesigned: {tmd.get_is_fakesigned()}")
|
print(f" Fakesigned: {tmd.get_is_fakesigned()}")
|
||||||
# Iterate over the content and print their details.
|
# Iterate over the content and print their details.
|
||||||
@@ -103,22 +108,23 @@ def _print_ticket_info(ticket: libWiiPy.title.Ticket, signing_cert=None):
|
|||||||
# Get all important keys from the TMD and print them out nicely.
|
# Get all important keys from the TMD and print them out nicely.
|
||||||
print(f"Ticket Info")
|
print(f"Ticket Info")
|
||||||
ascii_tid = ""
|
ascii_tid = ""
|
||||||
|
decoded_tid = binascii.hexlify(ticket.title_id).decode()
|
||||||
try:
|
try:
|
||||||
ascii_tid = str(bytes.fromhex(ticket.title_id.decode()[8:].replace("00", "30")).decode("ascii"))
|
ascii_tid = str(bytes.fromhex(decoded_tid[8:].replace("00", "30")).decode("ascii"))
|
||||||
except UnicodeDecodeError or binascii.Error:
|
except UnicodeDecodeError:
|
||||||
pass
|
pass
|
||||||
pattern = r"^[a-z0-9!@#$%^&*]{4}$"
|
pattern = r"^[a-z0-9!@#$%^&*]{4}$"
|
||||||
if re.fullmatch(pattern, ascii_tid, re.IGNORECASE):
|
if re.fullmatch(pattern, ascii_tid, re.IGNORECASE):
|
||||||
print(f" Title ID: {ticket.title_id.decode().upper()} ({ascii_tid})")
|
print(f" Title ID: {decoded_tid.upper()} ({ascii_tid})")
|
||||||
else:
|
else:
|
||||||
print(f" Title ID: {ticket.title_id.decode().upper()}")
|
print(f" Title ID: {decoded_tid.upper()}")
|
||||||
# This type of version number really only applies to the System Menu and IOS.
|
# This type of version number really only applies to the System Menu and IOS.
|
||||||
if ticket.title_id.decode().startswith("00000001"):
|
if decoded_tid.startswith("00000001"):
|
||||||
if ticket.title_id.decode() == "0000000100000001":
|
if decoded_tid == "0000000100000001":
|
||||||
print(f" Title Version: {ticket.title_version} (boot2v{ticket.title_version})")
|
print(f" Title Version: {ticket.title_version} (boot2v{ticket.title_version})")
|
||||||
else:
|
else:
|
||||||
print(f" Title Version: {ticket.title_version} "
|
print(f" Title Version: {ticket.title_version} "
|
||||||
f"({libWiiPy.title.title_ver_dec_to_standard(ticket.title_version, ticket.title_id.decode())})")
|
f"({libWiiPy.title.title_ver_dec_to_standard(ticket.title_version, decoded_tid)})")
|
||||||
else:
|
else:
|
||||||
print(f" Title Version: {ticket.title_version}")
|
print(f" Title Version: {ticket.title_version}")
|
||||||
print(f" Ticket Version: {ticket.ticket_version}")
|
print(f" Ticket Version: {ticket.ticket_version}")
|
||||||
@@ -128,8 +134,12 @@ def _print_ticket_info(ticket: libWiiPy.title.Ticket, signing_cert=None):
|
|||||||
elif ticket.signature_issuer.find("XS00000006") != -1:
|
elif ticket.signature_issuer.find("XS00000006") != -1:
|
||||||
print(f" Certificate: XS00000006 (Development)")
|
print(f" Certificate: XS00000006 (Development)")
|
||||||
print(f" Certificate Issuer: Root-CA00000002 (Development)")
|
print(f" Certificate Issuer: Root-CA00000002 (Development)")
|
||||||
|
elif ticket.signature_issuer.find("XS00000004") != -1:
|
||||||
|
print(f" Certificate: XS00000004 (Development/Unknown)")
|
||||||
|
print(f" Certificate Issuer: Root-CA00000002 (Development)")
|
||||||
else:
|
else:
|
||||||
print(f" Certificate Info: {ticket.signature_issuer} (Unknown)")
|
print(f" Certificate Info: {ticket.signature_issuer} (Unknown)")
|
||||||
|
key = ""
|
||||||
match ticket.common_key_index:
|
match ticket.common_key_index:
|
||||||
case 0:
|
case 0:
|
||||||
if ticket.is_dev:
|
if ticket.is_dev:
|
||||||
@@ -147,19 +157,18 @@ def _print_ticket_info(ticket: libWiiPy.title.Ticket, signing_cert=None):
|
|||||||
print(f" Title Key (Decrypted): {binascii.hexlify(ticket.get_title_key()).decode()}")
|
print(f" Title Key (Decrypted): {binascii.hexlify(ticket.get_title_key()).decode()}")
|
||||||
if signing_cert is not None:
|
if signing_cert is not None:
|
||||||
try:
|
try:
|
||||||
signed = libWiiPy.title.verify_ticket_sig(signing_cert, ticket)
|
if libWiiPy.title.verify_ticket_sig(signing_cert, ticket):
|
||||||
if signed:
|
signing_str = "Valid (Unmodified Ticket)"
|
||||||
signing_str = "Valid (Unmodified)"
|
|
||||||
elif ticket.get_is_fakesigned():
|
elif ticket.get_is_fakesigned():
|
||||||
signing_str = "Fakesigned"
|
signing_str = "Fakesigned"
|
||||||
else:
|
else:
|
||||||
signing_str = "Invalid (Modified)"
|
signing_str = "Invalid (Modified Ticket)"
|
||||||
except ValueError:
|
except ValueError:
|
||||||
if ticket.get_is_fakesigned():
|
if ticket.get_is_fakesigned():
|
||||||
signing_str = "Fakesigned"
|
signing_str = "Fakesigned"
|
||||||
else:
|
else:
|
||||||
signing_str = "Invalid (Modified)"
|
signing_str = "Invalid (Modified Ticket)"
|
||||||
print(f" Signing Status: {signing_str}")
|
print(f" Signature: {signing_str}")
|
||||||
else:
|
else:
|
||||||
print(f" Fakesigned: {ticket.get_is_fakesigned()}")
|
print(f" Fakesigned: {ticket.get_is_fakesigned()}")
|
||||||
|
|
||||||
@@ -199,17 +208,22 @@ def _print_wad_info(title: libWiiPy.title.Title):
|
|||||||
tmd_cert = None
|
tmd_cert = None
|
||||||
ticket_cert = None
|
ticket_cert = None
|
||||||
try:
|
try:
|
||||||
signed = title.get_is_signed()
|
|
||||||
tmd_cert = title.cert_chain.tmd_cert
|
tmd_cert = title.cert_chain.tmd_cert
|
||||||
ticket_cert = title.cert_chain.ticket_cert
|
ticket_cert = title.cert_chain.ticket_cert
|
||||||
if signed:
|
if title.get_is_signed():
|
||||||
signing_str = "Valid (Unmodified)"
|
signing_str = "Legitimate (Unmodified TMD + Ticket)"
|
||||||
elif title.get_is_fakesigned():
|
elif title.get_is_fakesigned():
|
||||||
signing_str = "Fakesigned"
|
signing_str = "Fakesigned"
|
||||||
|
elif (libWiiPy.title.verify_tmd_sig(tmd_cert, title.tmd)
|
||||||
|
and not libWiiPy.title.verify_ticket_sig(ticket_cert, title.ticket)):
|
||||||
|
signing_str = "Piratelegit (Unmodified TMD, Modified Ticket)"
|
||||||
|
elif (not libWiiPy.title.verify_tmd_sig(tmd_cert, title.tmd)
|
||||||
|
and libWiiPy.title.verify_ticket_sig(ticket_cert, title.ticket)):
|
||||||
|
signing_str = "Edited (Modified TMD, Unmodified Ticket)"
|
||||||
else:
|
else:
|
||||||
signing_str = "Invalid (Modified)"
|
signing_str = "Illegitimate (Modified TMD + Ticket)"
|
||||||
except ValueError:
|
except ValueError:
|
||||||
signing_str = "Invalid (Modified)"
|
signing_str = "Illegitimate (Modified TMD + Ticket)"
|
||||||
print(f" Signing Status: {signing_str}")
|
print(f" Signing Status: {signing_str}")
|
||||||
print("")
|
print("")
|
||||||
_print_ticket_info(title.ticket, ticket_cert)
|
_print_ticket_info(title.ticket, ticket_cert)
|
||||||
|
|||||||
@@ -8,119 +8,6 @@ import libWiiPy
|
|||||||
from modules.core import fatal_error
|
from modules.core import fatal_error
|
||||||
|
|
||||||
|
|
||||||
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:
|
|
||||||
fatal_error("The specified Title Version must be a valid integer!")
|
|
||||||
|
|
||||||
# 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():
|
|
||||||
fatal_error("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(f"Downloading title {tid} v{title_version}, please wait...")
|
|
||||||
else:
|
|
||||||
print(f"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(f"tmd.{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:
|
|
||||||
fatal_error("--wad was passed, but this title has no common ticket and cannot be packed into a WAD!")
|
|
||||||
|
|
||||||
# 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(f" - Downloading content {content + 1} of {len(title.tmd.content_records)} "
|
|
||||||
f"(Content ID: {title.tmd.content_records[content].content_id}, "
|
|
||||||
f"Size: {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(f" - Decrypting content {content + 1} of {len(title.tmd.content_records)} "
|
|
||||||
f"(Content ID: {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.load_cert_chain(libWiiPy.title.download_cert_chain(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(f"Downloaded title with Title ID \"{args.tid}\"!")
|
|
||||||
|
|
||||||
|
|
||||||
def handle_nus_content(args):
|
def handle_nus_content(args):
|
||||||
tid = args.tid
|
tid = args.tid
|
||||||
cid = args.cid
|
cid = args.cid
|
||||||
@@ -157,7 +44,7 @@ def handle_nus_content(args):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
fatal_error("The specified Title ID or Content ID could not be found!")
|
fatal_error("The specified Title ID or Content ID could not be found!")
|
||||||
|
|
||||||
if decrypt_content is True:
|
if decrypt_content:
|
||||||
output_path = output_path.with_suffix(".app")
|
output_path = output_path.with_suffix(".app")
|
||||||
tmd = libWiiPy.title.TMD()
|
tmd = libWiiPy.title.TMD()
|
||||||
tmd.load(libWiiPy.title.download_tmd(tid, version))
|
tmd.load(libWiiPy.title.download_tmd(tid, version))
|
||||||
@@ -198,6 +85,122 @@ def handle_nus_content(args):
|
|||||||
print(f"Downloaded content with Content ID \"{cid}\"!")
|
print(f"Downloaded content with Content ID \"{cid}\"!")
|
||||||
|
|
||||||
|
|
||||||
|
def handle_nus_title(args):
|
||||||
|
title_version = None
|
||||||
|
wad_file = None
|
||||||
|
output_dir = None
|
||||||
|
can_decrypt = False
|
||||||
|
tid = args.tid
|
||||||
|
wiiu_nus_enabled = False if args.wii else True
|
||||||
|
endpoint_override = args.endpoint if args.endpoint else None
|
||||||
|
|
||||||
|
# Check if --version was passed, because it'll be None if it wasn't.
|
||||||
|
if args.version is not None:
|
||||||
|
try:
|
||||||
|
title_version = int(args.version)
|
||||||
|
except ValueError:
|
||||||
|
fatal_error("The specified Title Version must be a valid integer!")
|
||||||
|
|
||||||
|
# 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():
|
||||||
|
fatal_error("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(f"Downloading title {tid} v{title_version}, please wait...")
|
||||||
|
else:
|
||||||
|
print(f"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,
|
||||||
|
endpoint_override=endpoint_override))
|
||||||
|
else:
|
||||||
|
title.load_tmd(libWiiPy.title.download_tmd(tid, wiiu_endpoint=wiiu_nus_enabled,
|
||||||
|
endpoint_override=endpoint_override))
|
||||||
|
title_version = title.tmd.title_version
|
||||||
|
# Write out the TMD to a file.
|
||||||
|
if output_dir is not None:
|
||||||
|
output_dir.joinpath(f"tmd.{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,
|
||||||
|
endpoint_override=endpoint_override))
|
||||||
|
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:
|
||||||
|
fatal_error("--wad was passed, but this title has no common ticket and cannot be packed into a WAD!")
|
||||||
|
|
||||||
|
# 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(f" - Downloading content {content + 1} of {len(title.tmd.content_records)} "
|
||||||
|
f"(Content ID: {title.tmd.content_records[content].content_id}, "
|
||||||
|
f"Size: {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,
|
||||||
|
endpoint_override=endpoint_override))
|
||||||
|
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:
|
||||||
|
for content in range(len(title.tmd.content_records)):
|
||||||
|
print(f" - Decrypting content {content + 1} of {len(title.tmd.content_records)} "
|
||||||
|
f"(Content ID: {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.load_cert_chain(libWiiPy.title.download_cert_chain(wiiu_endpoint=wiiu_nus_enabled,
|
||||||
|
endpoint_override=endpoint_override))
|
||||||
|
# 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(f"Downloaded title with Title ID \"{args.tid}\"!")
|
||||||
|
|
||||||
|
|
||||||
def handle_nus_tmd(args):
|
def handle_nus_tmd(args):
|
||||||
tid = args.tid
|
tid = args.tid
|
||||||
|
|
||||||
|
|||||||
@@ -81,7 +81,6 @@ def handle_wad_convert(args):
|
|||||||
else:
|
else:
|
||||||
fatal_error("No valid encryption target was specified!")
|
fatal_error("No valid encryption target was specified!")
|
||||||
|
|
||||||
output_path = pathlib.Path(args.output)
|
|
||||||
if args.output is None:
|
if args.output is None:
|
||||||
match target:
|
match target:
|
||||||
case "development":
|
case "development":
|
||||||
@@ -92,6 +91,8 @@ def handle_wad_convert(args):
|
|||||||
output_path = pathlib.Path(input_path.stem + "_vWii" + input_path.suffix)
|
output_path = pathlib.Path(input_path.stem + "_vWii" + input_path.suffix)
|
||||||
case _:
|
case _:
|
||||||
fatal_error("No valid encryption target was specified!")
|
fatal_error("No valid encryption target was specified!")
|
||||||
|
else:
|
||||||
|
output_path = pathlib.Path(args.output)
|
||||||
|
|
||||||
if not input_path.exists():
|
if not input_path.exists():
|
||||||
fatal_error(f"The specified WAD file \"{input_path}\" does not exist!")
|
fatal_error(f"The specified WAD file \"{input_path}\" does not exist!")
|
||||||
|
|||||||
33
wiipy.py
33
wiipy.py
@@ -5,6 +5,7 @@ import argparse
|
|||||||
from importlib.metadata import version
|
from importlib.metadata import version
|
||||||
|
|
||||||
from commands.archive.ash import *
|
from commands.archive.ash import *
|
||||||
|
from commands.archive.lz77 import *
|
||||||
from commands.archive.theme import *
|
from commands.archive.theme import *
|
||||||
from commands.archive.u8 import *
|
from commands.archive.u8 import *
|
||||||
from commands.nand.emunand import *
|
from commands.nand.emunand import *
|
||||||
@@ -17,7 +18,7 @@ from commands.title.nus import *
|
|||||||
from commands.title.tmd import *
|
from commands.title.tmd import *
|
||||||
from commands.title.wad import *
|
from commands.title.wad import *
|
||||||
|
|
||||||
wiipy_ver = "1.5.0"
|
wiipy_ver = "1.6.0"
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# Main argument parser.
|
# Main argument parser.
|
||||||
@@ -30,7 +31,7 @@ if __name__ == "__main__":
|
|||||||
# 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",
|
||||||
description="compress/decompress an ASH file")
|
description="compress/decompress an ASH file")
|
||||||
ash_subparsers = ash_parser.add_subparsers(title="emunand", dest="emunand", required=True)
|
ash_subparsers = ash_parser.add_subparsers(title="ash", dest="ash", required=True)
|
||||||
# ASH compress parser.
|
# ASH compress parser.
|
||||||
ash_compress_parser = ash_subparsers.add_parser("compress", help="compress a file into an ASH file",
|
ash_compress_parser = ash_subparsers.add_parser("compress", help="compress a file into an ASH file",
|
||||||
description="compress a file into an ASH file; by default, this "
|
description="compress a file into an ASH file; by default, this "
|
||||||
@@ -110,6 +111,8 @@ if __name__ == "__main__":
|
|||||||
"accepts a WAD file to read the TID from)")
|
"accepts a WAD file to read the TID from)")
|
||||||
emunand_title_parser.add_argument("-s", "--skip-hash", help="skips validating the hashes of decrypted "
|
emunand_title_parser.add_argument("-s", "--skip-hash", help="skips validating the hashes of decrypted "
|
||||||
"content (install only)", action="store_true")
|
"content (install only)", action="store_true")
|
||||||
|
emunand_title_parser.add_argument("-v", "--verbose", action="store_true",
|
||||||
|
help="show verbose installation/uninstallation details")
|
||||||
|
|
||||||
# Argument parser for the fakesign subcommand.
|
# Argument parser for the fakesign subcommand.
|
||||||
fakesign_parser = subparsers.add_parser("fakesign", help="fakesign a TMD, Ticket, or WAD (trucha bug)",
|
fakesign_parser = subparsers.add_parser("fakesign", help="fakesign a TMD, Ticket, or WAD (trucha bug)",
|
||||||
@@ -146,6 +149,28 @@ if __name__ == "__main__":
|
|||||||
iospatch_parser.add_argument("-ns", "--no-shared", action="store_true",
|
iospatch_parser.add_argument("-ns", "--no-shared", action="store_true",
|
||||||
help="set all patched content to be non-shared")
|
help="set all patched content to be non-shared")
|
||||||
|
|
||||||
|
# Argument parser for the LZ77 subcommand.
|
||||||
|
lz77_parser = subparsers.add_parser("lz77", help="compress/decompress data using LZ77 compression",
|
||||||
|
description="compress/decompress data using LZ77 compression")
|
||||||
|
lz77_subparsers = lz77_parser.add_subparsers(title="lz77", dest="lz77", required=True)
|
||||||
|
# LZ77 compress parser.
|
||||||
|
lz77_compress_parser = lz77_subparsers.add_parser("compress", help="compress a file with LZ77 compression",
|
||||||
|
description="compress a file with LZ77 compression; by default, "
|
||||||
|
"this will output to <input file>.lz77")
|
||||||
|
lz77_compress_parser.set_defaults(func=handle_lz77_compress)
|
||||||
|
lz77_compress_parser.add_argument("input", metavar="IN", type=str, help="file to compress")
|
||||||
|
lz77_compress_parser.add_argument("-o", "--output", metavar="OUT", type=str,
|
||||||
|
help="file to output the compressed data to (optional)")
|
||||||
|
# LZ77 decompress parser.
|
||||||
|
lz77_decompress_parser = lz77_subparsers.add_parser("decompress", help="decompress an LZ77-compressed file",
|
||||||
|
description="decompress an LZ77-compressed file; by default, "
|
||||||
|
"this will output to <input file>.out")
|
||||||
|
lz77_decompress_parser.set_defaults(func=handle_lz77_decompress)
|
||||||
|
lz77_decompress_parser.add_argument("input", metavar="IN", type=str,
|
||||||
|
help="LZ77-compressed file to decompress")
|
||||||
|
lz77_decompress_parser.add_argument("-o", "--output", metavar="OUT", type=str,
|
||||||
|
help="file to output the decompressed data to (optional)")
|
||||||
|
|
||||||
# Argument parser for the NUS subcommand.
|
# Argument parser for the NUS subcommand.
|
||||||
nus_parser = subparsers.add_parser("nus", help="download data from the NUS",
|
nus_parser = subparsers.add_parser("nus", help="download data from the NUS",
|
||||||
description="download from the NUS")
|
description="download from the NUS")
|
||||||
@@ -163,8 +188,10 @@ if __name__ == "__main__":
|
|||||||
help="download the title to a folder")
|
help="download the title to a folder")
|
||||||
nus_title_out_group.add_argument("-w", "--wad", metavar="WAD", type=str,
|
nus_title_out_group.add_argument("-w", "--wad", metavar="WAD", type=str,
|
||||||
help="pack a wad with the provided name")
|
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",
|
nus_title_parser.add_argument("--wii", help="use the original Wii NUS endpoint instead of the Wii U endpoint",
|
||||||
action="store_true")
|
action="store_true")
|
||||||
|
nus_title_parser.add_argument("-e", "--endpoint", metavar="ENDPOINT", type=str,
|
||||||
|
help="use the specified NUS endpoint instead of the official one")
|
||||||
# Content NUS subcommand.
|
# Content NUS subcommand.
|
||||||
nus_content_parser = nus_subparsers.add_parser("content", help="download a specific content from the NUS",
|
nus_content_parser = nus_subparsers.add_parser("content", help="download a specific content from the NUS",
|
||||||
description="download a specific content from the NUS")
|
description="download a specific content from the NUS")
|
||||||
|
|||||||
Reference in New Issue
Block a user