Create cert.sys during EmuNAND title installation if not found

This commit is contained in:
Campbell 2025-07-12 13:03:42 -04:00
parent 6d38df9133
commit ce5d118de1
Signed by: NinjaCheetah
GPG Key ID: 39C2500E1778B156
2 changed files with 39 additions and 4 deletions

View File

@ -34,7 +34,7 @@ class EmuNAND:
"""
def __init__(self, emunand_root: str | pathlib.Path, callback: Callable | None = None):
self.emunand_root = pathlib.Path(emunand_root)
self.log = callback if callback is not None else None
self.log = callback if callback is not None else lambda x: None
self.import_dir = self.emunand_root.joinpath("import")
self.meta_dir = self.emunand_root.joinpath("meta")
@ -70,12 +70,14 @@ class EmuNAND:
skip_hash : bool, optional
Skip the hash check and install the title regardless of its hashes. Defaults to false.
"""
self.log(f"[PROGRESS] Starting install of title with Title ID {title.tmd.title_id}...")
# Save the upper and lower portions of the Title ID, because these are used as target install directories.
tid_upper = title.tmd.title_id[:8]
tid_lower = title.tmd.title_id[8:]
# Tickets are installed as <tid_lower>.tik in /ticket/<tid_upper>/
ticket_dir = self.ticket_dir.joinpath(tid_upper)
self.log(f"[PROGRESS] Installing ticket to \"{ticket_dir}\"...")
ticket_dir.mkdir(exist_ok=True)
ticket_dir.joinpath(f"{tid_lower}.tik").write_bytes(title.ticket.dump())
@ -86,19 +88,25 @@ class EmuNAND:
title_dir = title_dir.joinpath(tid_lower)
title_dir.mkdir(exist_ok=True)
content_dir = title_dir.joinpath("content")
self.log(f"[PROGRESS] Installing TMD to \"{content_dir}\"...")
if content_dir.exists():
shutil.rmtree(content_dir) # Clear the content directory so old contents aren't left behind.
content_dir.mkdir(exist_ok=True)
content_dir.joinpath("title.tmd").write_bytes(title.tmd.dump())
self.log(f"[PROGRESS] Installing content to \"{content_dir}\"...")
if skip_hash:
self.log("[WARN] Not checking content hashes! Content validity will not be verified.")
for content_file in range(0, title.tmd.num_contents):
if title.tmd.content_records[content_file].content_type == 1:
content_file_name = f"{title.tmd.content_records[content_file].content_id:08X}".lower()
self.log(f"[PROGRESS] Installing content \"{content_file_name}.app\" to \"{content_dir}\"... ")
content_dir.joinpath(f"{content_file_name}.app").write_bytes(
title.get_content_by_index(content_file, skip_hash=skip_hash))
title_dir.joinpath("data").mkdir(exist_ok=True) # Empty directory used for save data for the title.
# Shared contents need to be installed to /shared1/, with incremental names determined by /shared1/content.map.
content_map_path = self.shared1_dir.joinpath("content.map")
self.log(f"[PROGRESS] Installing shared content to \"{self.shared1_dir}\"...")
content_map = _SharedContentMap()
existing_hashes = []
if content_map_path.exists():
@ -108,7 +116,10 @@ class EmuNAND:
for content_file in range(0, title.tmd.num_contents):
if title.tmd.content_records[content_file].content_type == 32769:
if title.tmd.content_records[content_file].content_hash not in existing_hashes:
self.log(f"[PROGRESS] Adding shared content hash to content.map...")
content_file_name = content_map.add_content(title.tmd.content_records[content_file].content_hash)
self.log(f"[PROGRESS] Installing shared content \"{content_file_name}.app\" to "
f"\"{self.shared1_dir}\"...")
self.shared1_dir.joinpath(f"{content_file_name}.app").write_bytes(
title.get_content_by_index(content_file, skip_hash=skip_hash))
self.shared1_dir.joinpath("content.map").write_bytes(content_map.dump())
@ -120,6 +131,7 @@ class EmuNAND:
meta_dir = self.meta_dir.joinpath(tid_upper)
meta_dir.mkdir(exist_ok=True)
meta_dir = meta_dir.joinpath(tid_lower)
self.log(f"[PROGRESS] Installing meta data to \"{meta_dir}\"...")
meta_dir.mkdir(exist_ok=True)
meta_dir.joinpath("title.met").write_bytes(title.wad.get_meta_data())
@ -127,12 +139,26 @@ class EmuNAND:
uid_sys_path = self.sys_dir.joinpath("uid.sys")
uid_sys = _UidSys()
if not uid_sys_path.exists():
self.log("[WARN] uid.sys does not exist! Creating it with the default entry.")
uid_sys.create()
else:
uid_sys.load(uid_sys_path.read_bytes())
self.log("[PROGRESS] Adding title to uid.sys and assigning a new UID...")
uid_sys.add(title.tmd.title_id)
uid_sys_path.write_bytes(uid_sys.dump())
# Check for a cert.sys and initialize it using the certs in the WAD if it doesn't exist.
cert_sys_path = self.sys_dir.joinpath("cert.sys")
if not cert_sys_path.exists():
self.log("[WARN] cert.sys does not exist! Creating it using certs from the installed title...")
cert_sys_data = b''
cert_sys_data += title.cert_chain.ticket_cert.dump()
cert_sys_data += title.cert_chain.ca_cert.dump()
cert_sys_data += title.cert_chain.tmd_cert.dump()
cert_sys_path.write_bytes(cert_sys_data)
self.log("[PROGRESS] Completed title installation.")
def uninstall_title(self, tid: str) -> None:
"""
Uninstall the Title with the specified Title ID from the EmuNAND. This will leave shared contents unmodified.

View File

@ -134,9 +134,12 @@ def download_tmd(title_id: str, title_version: int | None = None, wiiu_endpoint:
else:
raise Exception("A connection could not be made to the NUS endpoint. The NUS may be unavailable.")
# Handle a 404 if the TID/version doesn't exist.
if response.status_code != 200:
if response.status_code == 404:
raise ValueError("The requested Title ID or TMD version does not exist. Please check the Title ID and Title"
" version and then try again.")
elif response.status_code != 200:
raise Exception(f"An unknown error occurred while downloading the TMD. "
f"Got HTTP status code: {response.status_code}")
total_size = int(response.headers["Content-Length"])
progress(0, total_size)
# Stream the TMD's data in chunks so that we can post updates to the callback function (assuming one was supplied).
@ -198,9 +201,12 @@ def download_ticket(title_id: str, wiiu_endpoint: bool = False, endpoint_overrid
"override is valid.")
else:
raise Exception("A connection could not be made to the NUS endpoint. The NUS may be unavailable.")
if response.status_code != 200:
if response.status_code == 404:
raise ValueError("The requested Title ID does not exist, or refers to a non-free title. Tickets can only"
" be downloaded for titles that are free on the NUS.")
elif response.status_code != 200:
raise Exception(f"An unknown error occurred while downloading the Ticket. "
f"Got HTTP status code: {response.status_code}")
total_size = int(response.headers["Content-Length"])
progress(0, total_size)
# Stream the Ticket's data just like with the TMD.
@ -316,10 +322,13 @@ def download_content(title_id: str, content_id: int, wiiu_endpoint: bool = False
"override is valid.")
else:
raise Exception("A connection could not be made to the NUS endpoint. The NUS may be unavailable.")
if response.status_code != 200:
if response.status_code == 404:
raise ValueError("The requested Title ID does not exist, or an invalid Content ID is present in the"
" content records provided.\n Failed while downloading Content ID: 000000" +
content_id_hex)
elif response.status_code != 200:
raise Exception(f"An unknown error occurred while downloading the content. "
f"Got HTTP status code: {response.status_code}")
total_size = int(response.headers["Content-Length"])
progress(0, total_size)
# Stream the content just like the TMD/Ticket.