diff --git a/.gitignore b/.gitignore index aef39b9..8522187 100644 --- a/.gitignore +++ b/.gitignore @@ -161,9 +161,10 @@ cython_debug/ # option (not recommended) you can uncomment the following to ignore the entire idea folder. .idea/ -# Allows me to keep TMD files in my repository folder for testing without accidentally publishing them +# Relevant files that are used for testing libWiiPy's features. *.tmd *.wad +*.arc out_prod/ remakewad.pl diff --git a/pyproject.toml b/pyproject.toml index 5205905..08b02c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "libWiiPy" -version = "0.2.3" +version = "0.3.0" authors = [ { name="NinjaCheetah", email="ninjacheetah@ncxprogramming.com" }, { name="Lillian Skinner", email="lillian@randommeaninglesscharacters.com" } diff --git a/src/libWiiPy/__init__.py b/src/libWiiPy/__init__.py index 2df7705..f381d5b 100644 --- a/src/libWiiPy/__init__.py +++ b/src/libWiiPy/__init__.py @@ -11,3 +11,4 @@ from .title import * from .tmd import * from .wad import * from .nus import * +from .u8 import * diff --git a/src/libWiiPy/types.py b/src/libWiiPy/types.py index 6080dbc..48d8d91 100644 --- a/src/libWiiPy/types.py +++ b/src/libWiiPy/types.py @@ -1,6 +1,6 @@ # "types.py" from libWiiPy by NinjaCheetah & Contributors # https://github.com/NinjaCheetah/libWiiPy - +from builtins import type from dataclasses import dataclass @@ -14,21 +14,21 @@ class ContentRecord: Attributes ---------- content_id : int - ID of the content. + The unique ID of the content. index : int - Index of the content in the list of contents. + The index of this content in the content records. content_type : int The type of the content. content_size : int - The size of the content. + The size of the content when decrypted. content_hash The SHA-1 hash of the decrypted content. """ - content_id: int # The unique ID of the content. - index: int # The index of this content in the content record. + content_id: int + index: int content_type: int # Type of content, possible values of: 0x0001: Normal, 0x4001: DLC, 0x8001: Shared. - content_size: int # Size of the content when decrypted. - content_hash: bytes # SHA-1 hash of the content when decrypted. + content_size: int + content_hash: bytes @dataclass @@ -50,3 +50,27 @@ class TitleLimit: limit_type: int # The maximum value of the limit applied. maximum_usage: int + + +@dataclass +class U8Node: + """ + A U8Node object that contains the data of a single node in a U8 file header. Each node keeps track of whether this + node is for a file or directory, the offset of the name of the file/directory, the offset of the data for the file/ + directory, and the size of the data. + + Attributes + ---------- + type : int + Whether this node refers to a file or a directory. Either 0x0000 for files, or 0x0100 for directories. + name_offset : int + The offset of the name of the file/directory this node refers to. + data_offset : int + The offset of the data for the file/directory this node refers to. + size : int + The size of the data for this node. + """ + type: int + name_offset: int + data_offset: int + size: int diff --git a/src/libWiiPy/u8.py b/src/libWiiPy/u8.py new file mode 100644 index 0000000..ef16df7 --- /dev/null +++ b/src/libWiiPy/u8.py @@ -0,0 +1,83 @@ +# "u8.py" from libWiiPy by NinjaCheetah & Contributors +# https://github.com/NinjaCheetah/libWiiPy +# +# See https://wiibrew.org/wiki/U8_archive for details about the U8 archive format + +import io +import binascii +from typing import List +from .types import U8Node + + +class U8Archive: + def __init__(self): + """ + A U8 object that allows for extracting and packing U8 archives. + + Attributes + ---------- + """ + self.u8_magic = b'' + self.root_node_offset = 0 # Offset of the root node, which will always be 0x20. + self.header_size = 0 # The size of the U8 header. + self.data_offset = 0 # The offset of the data, which is root_node_offset + header_size, aligned to 0x40. + self.header_padding = b'' + self.root_node = U8Node + self.u8_node_list: List[U8Node] = [] # All the nodes in the header of a U8 file. + self.file_name_list: List[str] = [] + self.u8_file_data_list: List[bytes] = [] + self.u8_file_structure = dict + + def load(self, u8_data: bytes) -> None: + """ + Loads raw U8 data into a new U8 object. This allows for extracting the file and updating its contents. + + Parameters + ---------- + u8_data : bytes + The data for the U8 file to load. + """ + with io.BytesIO(u8_data) as u8_data: + # Read the first 4 bytes of the file to ensure that it's a U8 archive. + 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!") + self.root_node_offset = int(binascii.hexlify(u8_data.read(4)), 16) + self.header_size = int(binascii.hexlify(u8_data.read(4)), 16) + self.data_offset = int(binascii.hexlify(u8_data.read(4)), 16) + self.header_padding = u8_data.read(16) + root_node_type = int.from_bytes(u8_data.read(2)) + root_node_name_offset = int.from_bytes(u8_data.read(2)) + root_node_data_offset = int.from_bytes(u8_data.read(4)) + root_node_size = int.from_bytes(u8_data.read(4)) + self.root_node = U8Node(root_node_type, root_node_name_offset, root_node_data_offset, root_node_size) + self.u8_node_list.append(self.root_node) + # Iterate over the number of nodes that the root node lists, minus one since the count includes itself. + for node in range(self.root_node.size - 1): + node_type = int.from_bytes(u8_data.read(2)) + node_name_offset = int.from_bytes(u8_data.read(2)) + node_data_offset = int.from_bytes(u8_data.read(4)) + node_size = int.from_bytes(u8_data.read(4)) + self.u8_node_list.append(U8Node(node_type, node_name_offset, node_data_offset, node_size)) + # Iterate over all loaded nodes and create a list of file names. + name_base_offset = u8_data.tell() + for node in self.u8_node_list: + u8_data.seek(name_base_offset + node.name_offset) + name_bin = b'' + while name_bin[-1:] != b'\x00': + name_bin += u8_data.read(1) + name_bin = name_bin[:-1] + name = str(name_bin.decode()) + self.file_name_list.append(name) + if node.type == 0: + u8_data.seek(node.data_offset) + self.u8_file_data_list.append(u8_data.read(node.size)) + else: + self.u8_file_data_list.append(b'') + # This does nothing for now. + next_dir = 0 + for node in range(len(self.u8_node_list)): + if self.u8_node_list[node].type == 256 and node != 0: + next_dir = self.u8_node_list[node].size + diff --git a/src/libWiiPy/wad.py b/src/libWiiPy/wad.py index 5421784..19d1700 100644 --- a/src/libWiiPy/wad.py +++ b/src/libWiiPy/wad.py @@ -49,7 +49,7 @@ class WAD: self.wad_content_data: bytes = b'' self.wad_meta_data: bytes = b'' - def load(self, wad_data) -> None: + def load(self, wad_data: bytes) -> None: """ Loads raw WAD data and sets all attributes of the WAD object. This allows for manipulating an already existing WAD file. @@ -57,7 +57,7 @@ class WAD: Parameters ---------- wad_data : bytes - The data for the WAD you wish to load. + The data for the WAD file to load. """ with io.BytesIO(wad_data) as wad_data: # Read the first 8 bytes of the file to ensure that it's a WAD. Has two possible valid values for the two @@ -67,7 +67,7 @@ class WAD: wad_magic_hex = binascii.hexlify(wad_magic_bin) wad_magic = str(wad_magic_hex.decode()) if wad_magic != "0000002049730000" and wad_magic != "0000002069620000": - raise TypeError("This does not appear to be a valid WAD file.") + raise TypeError("This is not a valid WAD file!") # ==================================================================================== # Get the sizes of each data region contained within the WAD. # ====================================================================================