Add highly experimental U8 handling module

This commit is contained in:
Campbell 2024-05-16 21:24:42 -04:00
parent 6a81722ec5
commit ede33dc503
No known key found for this signature in database
GPG Key ID: E543751376940756
6 changed files with 122 additions and 13 deletions

3
.gitignore vendored
View File

@ -161,9 +161,10 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder. # option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/ .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 *.tmd
*.wad *.wad
*.arc
out_prod/ out_prod/
remakewad.pl remakewad.pl

View File

@ -1,6 +1,6 @@
[project] [project]
name = "libWiiPy" name = "libWiiPy"
version = "0.2.3" version = "0.3.0"
authors = [ authors = [
{ name="NinjaCheetah", email="ninjacheetah@ncxprogramming.com" }, { name="NinjaCheetah", email="ninjacheetah@ncxprogramming.com" },
{ name="Lillian Skinner", email="lillian@randommeaninglesscharacters.com" } { name="Lillian Skinner", email="lillian@randommeaninglesscharacters.com" }

View File

@ -11,3 +11,4 @@ from .title import *
from .tmd import * from .tmd import *
from .wad import * from .wad import *
from .nus import * from .nus import *
from .u8 import *

View File

@ -1,6 +1,6 @@
# "types.py" from libWiiPy by NinjaCheetah & Contributors # "types.py" from libWiiPy by NinjaCheetah & Contributors
# https://github.com/NinjaCheetah/libWiiPy # https://github.com/NinjaCheetah/libWiiPy
from builtins import type
from dataclasses import dataclass from dataclasses import dataclass
@ -14,21 +14,21 @@ class ContentRecord:
Attributes Attributes
---------- ----------
content_id : int content_id : int
ID of the content. The unique ID of the content.
index : int index : int
Index of the content in the list of contents. The index of this content in the content records.
content_type : int content_type : int
The type of the content. The type of the content.
content_size : int content_size : int
The size of the content. The size of the content when decrypted.
content_hash content_hash
The SHA-1 hash of the decrypted content. The SHA-1 hash of the decrypted content.
""" """
content_id: int # The unique ID of the content. content_id: int
index: int # The index of this content in the content record. index: int
content_type: int # Type of content, possible values of: 0x0001: Normal, 0x4001: DLC, 0x8001: Shared. 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_size: int
content_hash: bytes # SHA-1 hash of the content when decrypted. content_hash: bytes
@dataclass @dataclass
@ -50,3 +50,27 @@ class TitleLimit:
limit_type: int limit_type: int
# The maximum value of the limit applied. # The maximum value of the limit applied.
maximum_usage: int 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

83
src/libWiiPy/u8.py Normal file
View File

@ -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

View File

@ -49,7 +49,7 @@ class WAD:
self.wad_content_data: bytes = b'' self.wad_content_data: bytes = b''
self.wad_meta_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 Loads raw WAD data and sets all attributes of the WAD object. This allows for manipulating an already
existing WAD file. existing WAD file.
@ -57,7 +57,7 @@ class WAD:
Parameters Parameters
---------- ----------
wad_data : bytes 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: 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 # 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_hex = binascii.hexlify(wad_magic_bin)
wad_magic = str(wad_magic_hex.decode()) wad_magic = str(wad_magic_hex.decode())
if wad_magic != "0000002049730000" and wad_magic != "0000002069620000": 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. # Get the sizes of each data region contained within the WAD.
# ==================================================================================== # ====================================================================================