papermario/tools/splat/util/gc/gcfst.py

182 lines
6.9 KiB
Python

import struct
from pathlib import Path
from typing import List, Optional
from segtypes.gc.segment import GCSegment
from util import options
from util.gc.gcutil import read_string_from_bytes
# Represents the info for either a directory or a file within a GameCube disc image's file system.
class GCFSTEntry:
def __init__(self, flags: bool, name_offset, offset, length):
self.flags = flags
self.name_offset = name_offset
self.offset = offset
self.length = length
self.name = ""
self.parent: Optional[GCFSTEntry] = None
self.children: List[GCFSTEntry] = []
def populate_children_recursive(
self, root_dir: "GCFSTEntry", current_node_offset, fst_bytes, string_table_bytes
):
self.parent = root_dir
self.name = read_string_from_bytes(self.name_offset, string_table_bytes)
# This node is a file, so we don't do anything but return that we read 1 node.
if self.flags == False:
return 1
nodes_read = 1
next_child_offset = current_node_offset + 0x0C
# Directory nodes contain the index of the next node that is NOT its child, meaning the index of their next sibling node.
# We can figure out when we're done reading child nodes by comparing the offset of the next node to read to the
# offset of the next sibling node. We stop reading when the next node offset is >= the offset of the next sibling node.
while next_child_offset < self.length * 0x0C:
new_entry = GCFSTEntry(
bool(fst_bytes[next_child_offset + 0x0000]),
struct.unpack_from(
">I", fst_bytes[next_child_offset : next_child_offset + 0x0004]
)[0]
& 0x00FFFFFF,
struct.unpack_from(">I", fst_bytes, next_child_offset + 0x0004)[0],
struct.unpack_from(">I", fst_bytes, next_child_offset + 0x0008)[0],
)
self.children.append(new_entry)
nodes_read += new_entry.populate_children_recursive(
self, next_child_offset, fst_bytes, string_table_bytes
)
next_child_offset = current_node_offset + nodes_read * 0x0C
return nodes_read
# Builds this entry's full path within the filesystem from its parents' names.
def get_full_name(self):
path_components: List[str] = []
entry = self
while entry.parent != None:
path_components.insert(0, entry.name)
if entry.parent is None:
break
entry = entry.parent
return Path(*path_components)
# Emits this entry to the filesystem.
def emit(self, filesystem_dir: Path, iso_bytes):
full_path = filesystem_dir / self.get_full_name()
# If this is a directory, we just need to make the directory on disk.
if self.flags == True:
full_path.mkdir(parents=True, exist_ok=True)
return
file_bytes = iso_bytes[self.offset : self.offset + self.length]
with open(full_path, "wb") as f:
f.write(file_bytes)
def emit_recursive(self, filesystem_dir: Path, iso_bytes):
# Don't emit if this is the root directory.
if self.parent != None:
self.emit(filesystem_dir, iso_bytes)
for e in self.children:
e.emit_recursive(filesystem_dir, iso_bytes)
# Splits the ISO into its component parts - header info, apploader, DOL, FST metadata, and the individual files in the filesystem.
def split_iso(iso_bytes):
split_sys_info(iso_bytes)
split_content(iso_bytes)
# Splits the header info, apploader, DOL, and FST metadata from the ISO.
def split_sys_info(iso_bytes):
assert options.opts.filesystem_path is not None
sys_path = options.opts.filesystem_path / "sys"
sys_path.mkdir(parents=True, exist_ok=True)
# Split boot.info. Always at 0x0000 and 0x0440 bytes long.
with open(sys_path / "boot.bin", "wb") as f:
f.write(iso_bytes[0x0000:0x0440])
# Split bi2.info. Always at 0x0440 and 0x2000 bytes long.
with open(sys_path / "bi2.bin", "wb") as f:
f.write(iso_bytes[0x0440:0x2440])
# Split apploader.img. Always at 0x2440 and size is listed at 0x0400.
apploader_size = struct.unpack_from(">I", iso_bytes, 0x0400)[0]
with open(sys_path / "apploader.img", "wb") as f:
f.write(iso_bytes[0x2440 : 0x2440 + apploader_size])
# Split main.dol. Offset specified explicitly at 0x0420, but size must be calculated.
dol_offset = struct.unpack_from(">I", iso_bytes, 0x0420)[0]
fst_offset = struct.unpack_from(">I", iso_bytes, 0x0424)[0]
dol_size = fst_offset - dol_offset
with open(sys_path / "main.dol", "wb") as f:
f.write(iso_bytes[dol_offset : dol_offset + dol_size])
# Split fst.bin. Offset specified at 0x0424 and size specified at 0x402C.
fst_size = struct.unpack_from(">I", iso_bytes, 0x0428)[0]
with open(sys_path / "fst.bin", "wb") as f:
f.write(iso_bytes[fst_offset : fst_offset + fst_size])
# Splits the ISO's filesystem into individual files.
def split_content(iso_bytes):
assert options.opts.filesystem_path is not None
fst_path = options.opts.filesystem_path / "sys" / "fst.bin"
assert fst_path.is_file()
fst_bytes = fst_path.read_bytes()
fst_root_entry = populate_filesystem(fst_bytes)
files_path = options.opts.filesystem_path / "files"
files_path.mkdir(parents=True, exist_ok=True)
fst_root_entry.emit_recursive(files_path, iso_bytes)
# Loads the FST data needed to split the filesystem.
def populate_filesystem(fst_bytes):
root_dir = GCFSTEntry(
bool(fst_bytes[0x0000]),
struct.unpack_from(">I", fst_bytes, 0x0000)[0] & 0x00FFFFFF,
struct.unpack_from(">I", fst_bytes, 0x0004)[0],
struct.unpack_from(">I", fst_bytes, 0x0008)[0],
)
string_table_bytes = fst_bytes[root_dir.length * 0x0C : len(fst_bytes)]
# Parsing the filesystem is a bit tricky. The root directory's length property is the total number of nodes in the FST.
# So, we initialize nodes_read to 1, since the root is included in the number of nodes.
# We will rely on each directory and file on the root directory to tell us how many nodes were read while parsing them.
# We can stop reading the FST when our total number of nodes read is >= the number of nodes in the FST.
nodes_read = 1
while nodes_read < root_dir.length:
current_offset = nodes_read * 0x0C
new_entry = GCFSTEntry(
bool(fst_bytes[current_offset + 0x0000]),
struct.unpack_from(">I", fst_bytes, current_offset)[0] & 0x00FFFFFF,
struct.unpack_from(">I", fst_bytes, current_offset + 0x0004)[0],
struct.unpack_from(">I", fst_bytes, current_offset + 0x0008)[0],
)
root_dir.children.append(new_entry)
nodes_read += new_entry.populate_children_recursive(
root_dir, current_offset, fst_bytes, string_table_bytes
)
return root_dir