diff --git a/src/libWiiPy/nand/emunand.py b/src/libWiiPy/nand/emunand.py index 5fcaf55..e5e83d3 100644 --- a/src/libWiiPy/nand/emunand.py +++ b/src/libWiiPy/nand/emunand.py @@ -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 .tik in /ticket// 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. diff --git a/src/libWiiPy/title/nus.py b/src/libWiiPy/title/nus.py index 44b76b8..9a15841 100644 --- a/src/libWiiPy/title/nus.py +++ b/src/libWiiPy/title/nus.py @@ -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.