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

View File

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

View File

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

View File

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

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_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.
# ====================================================================================