From 855200bb98ff5a801017c810495e55f4fcb2fbf7 Mon Sep 17 00:00:00 2001 From: NinjaCheetah <58050615+NinjaCheetah@users.noreply.github.com> Date: Mon, 18 Nov 2024 16:59:46 -0500 Subject: [PATCH] Add preliminary support for parsing 00000000.app New module banner.py offers classes for IMD5 and IMET headers, U8 unpacker now supports U8 archives with IMET headers. --- pyproject.toml | 2 +- src/libWiiPy/archive/u8.py | 33 ++++++++++++++-- src/libWiiPy/title/banner.py | 73 ++++++++++++++++++++++++++++++++++++ src/libWiiPy/title/ticket.py | 3 +- 4 files changed, 105 insertions(+), 6 deletions(-) create mode 100644 src/libWiiPy/title/banner.py diff --git a/pyproject.toml b/pyproject.toml index aea1d4e..e69bee9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "libWiiPy" -version = "0.5.3" +version = "0.6.0" authors = [ { name="NinjaCheetah", email="ninjacheetah@ncxprogramming.com" }, { name="Lillian Skinner", email="lillian@randommeaninglesscharacters.com" } diff --git a/src/libWiiPy/archive/u8.py b/src/libWiiPy/archive/u8.py index c178593..24f12f4 100644 --- a/src/libWiiPy/archive/u8.py +++ b/src/libWiiPy/archive/u8.py @@ -66,7 +66,28 @@ class U8Archive: u8_data.seek(0x0) self.u8_magic = u8_data.read(4) if self.u8_magic != b'\x55\xAA\x38\x2D': - raise TypeError("This is not a valid U8 archive!") + # Check for an IMET header, if the file doesn't start with the proper magic number. The header magic + # may be at either 0x40 or 0x80 depending on whether this title has a build tag at the start or not. + u8_data.seek(0x40) + self.u8_magic = u8_data.read(4) + if self.u8_magic == b'\x49\x4D\x45\x54': + # IMET with no build tag means the U8 archive should start at 0x600. + u8_data.seek(0x600) + self.u8_magic = u8_data.read(4) + if self.u8_magic != b'\x55\xAA\x38\x2D': + raise TypeError("This is not a valid U8 archive!") + else: + # This check will pass if the IMET comes after a build tag. + u8_data.seek(0x80) + self.u8_magic = u8_data.read(4) + if self.u8_magic == b'\x49\x4D\x45\x54': + # IMET with a build tag means the U8 archive should start at 0x640. + u8_data.seek(0x640) + self.u8_magic = u8_data.read(4) + if self.u8_magic != b'\x55\xAA\x38\x2D': + raise TypeError("This is not a valid U8 archive!") + else: + raise TypeError("This is not a valid U8 archive!") # Offset of the root node, which will always be 0x20. self.root_node_offset = int.from_bytes(u8_data.read(4)) # The size of the U8 header. @@ -257,7 +278,7 @@ def _pack_u8_dir(u8_archive: U8Archive, current_path, node_count, parent_node): return u8_archive, node_count -def pack_u8(input_path) -> bytes: +def pack_u8(input_path, generate_imet=False, imet_titles:List[str]=None) -> bytes: """ Packs the provided file or folder into a new U8 archive, and returns the raw file data for it. @@ -265,6 +286,12 @@ def pack_u8(input_path) -> bytes: ---------- input_path The path to the input file or folder. + generate_imet : bool, optional + Whether an IMET header should be generated for this U8 archive or not. IMET headers are only used for channel + banners (00000000.app). Defaults to False. + imet_titles : List[str], optional + A list of the channel title in different languages for the IMET header. If only one item is provided, that + item will be used for all entries in the header. Defaults to None, and is only used when generate_imet is True. Returns ------- @@ -287,4 +314,4 @@ def pack_u8(input_path) -> bytes: elif input_path.is_file(): raise ValueError("This does not appear to be a directory.") else: - raise FileNotFoundError("Input directory: \"" + str(input_path) + "\" does not exist!") + raise FileNotFoundError(f"Input directory: \"{input_path}\" does not exist!") diff --git a/src/libWiiPy/title/banner.py b/src/libWiiPy/title/banner.py new file mode 100644 index 0000000..971e979 --- /dev/null +++ b/src/libWiiPy/title/banner.py @@ -0,0 +1,73 @@ +# "title/banner.py" from libWiiPy by NinjaCheetah & Contributors +# https://github.com/NinjaCheetah/libWiiPy +# +# See https://wiibrew.org/wiki/Opening.bnr for details about the Wii's banner format + +from dataclasses import dataclass as _dataclass +from typing import List + + +@_dataclass +class IMD5Header: + """ + An IMD5Header object that contains the properties of an IMD5 header. These headers precede the data of banner.bin + and icon.bin inside the banner (00000000.app) of a channel, and are used to verify the data of those files. + + An IMD5 header is always 32 bytes long. + + Attributes + ---------- + magic : str + Magic number for the header, should be "IMD5". + file_size : int + The size of the file this header precedes. + zeros : int + 8 bytes of zero padding. + md5_hash : bytes + The MD5 hash of the file this header precedes. + """ + magic: str # Should always be "IMD5" + file_size: int + zeros: int + md5_hash: bytes + + +@_dataclass +class IMETHeader: + """ + An IMETHeader object that contains the properties of an IMET header. These headers precede the data of a channel + banner (00000000.app), and are used to store metadata about the banner and verify its data. + + An IMET header is always 1,536 bytes long. + + Attributes + ---------- + zeros : int + 64 bytes of zero padding. + magic : str + Magic number for the header, should be "IMD5". + hash_size : int + Length of the MD5 hash. + imet_version : int + Version of the IMET header. Normally always 3. + sizes : List[int] + The file sizes of icon.bin, banner.bin, and sound.bin. + flag1 : int + Unknown. + channel_names : List[str] + The name of the channel this header is for in Japanese, English, German, French, Spanish, Italian, Dutch, + Simplified Chinese, Traditional Chinese, and Korean, in that order. + zeros2 : int + An additional 588 bytes of zero padding. + md5_hash : bytes + "MD5 of 0 to 'hashsize' in header. crypto should be all 0's when calculating final MD5" -WiiBrew + """ + zeros: int + magic: str # Should always be "IMET" + hash_size: int + imet_version: int # Always 3? + sizes: List[int] # Should only have 3 items + flag1: int # Unknown + channel_names: List[str] # Should have 10 items + zeros2: int + md5_hash: bytes diff --git a/src/libWiiPy/title/ticket.py b/src/libWiiPy/title/ticket.py index b1f4ed9..8485ab3 100644 --- a/src/libWiiPy/title/ticket.py +++ b/src/libWiiPy/title/ticket.py @@ -23,12 +23,11 @@ class _TitleLimit: Attributes ---------- limit_type : int - The type of play limit applied. + The type of play limit applied. 0 and 3 are none, 1 is a time limit, and 4 is a launch count limit. maximum_usage : int The maximum value for the type of play limit applied. """ # The type of play limit applied. - # 0 = None, 1 = Time Limit, 3 = None, 4 = Launch Count limit_type: int # The maximum value of the limit applied. maximum_usage: int