system.conf with collision detection improvments

This commit is contained in:
tmpz23 2022-08-10 19:25:27 +02:00 committed by GitHub
parent bc5057dd24
commit 5882ddd464
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -1,9 +1,11 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from pathlib import Path from configparser import ConfigParser
import logging import logging
from pathlib import Path
import re
__version__ = "0.1.3" __version__ = "0.1.4"
__author__ = "rigodron, algoflash, GGLinnk" __author__ = "rigodron, algoflash, GGLinnk"
__license__ = "MIT" __license__ = "MIT"
__status__ = "developpement" __status__ = "developpement"
@ -19,6 +21,8 @@ class InvalidPackIsoError(Exception): pass
class InvalidFSTSizeError(Exception): pass class InvalidFSTSizeError(Exception): pass
# raised during pack when boot.dol size overflow on first file or on FST # raised during pack when boot.dol size overflow on first file or on FST
class DolSizeOverflowError(Exception): pass class DolSizeOverflowError(Exception): pass
# raised during pack when fst.bin size overflow on first file or on dol
class FstSizeOverflowError(Exception): pass
# raised during pack when FST folder entry has an invalid nextdir id value; this happen when file and folder has been added/removed # raised during pack when FST folder entry has an invalid nextdir id value; this happen when file and folder has been added/removed
class InvalidRootFileFolderCountError(Exception): pass class InvalidRootFileFolderCountError(Exception): pass
# raised during pack when FST file entry has a different value than the file being packed; this happen when a file has been edited changing it's size # raised during pack when FST file entry has a different value than the file being packed; this happen when a file has been edited changing it's size
@ -29,6 +33,10 @@ class FSTDirNotFoundError(Exception): pass
class FSTFileNotFoundError(Exception): pass class FSTFileNotFoundError(Exception): pass
# raised when using an invalid align # raised when using an invalid align
class BadAlignError(Exception): pass class BadAlignError(Exception): pass
# raised when a system conf entry has an invalid format
class InvalidConfValueError(Exception): pass
# raised when apploader overflow on dol or fst
class ApploaderOverflowError(Exception): pass
def align_top(address:int, align:int): def align_top(address:int, align:int):
@ -156,7 +164,7 @@ class FstTree(Fst):
__name_block = None __name_block = None
# Used to find min file_offset when fst is at the end of the iso beginning (otherweise we can't know the first available offset) # Used to find min file_offset when fst is at the end of the iso beginning (otherweise we can't know the first available offset)
__nameblock_length = None __nameblock_length = None
def __init__(self, root_path:Path, fst_offset:int, align:int = 4): def __init__(self, root_path:Path, fstbin_offset:int, align:int = 4):
# as said before we don't want to add parents folder that don't are used in the folder we are packing. # as said before we don't want to add parents folder that don't are used in the folder we are packing.
self.__root_path_length = len(root_path.parts) self.__root_path_length = len(root_path.parts)
self.__root_node = Folder(root_path.name, None) self.__root_node = Folder(root_path.name, None)
@ -164,7 +172,7 @@ class FstTree(Fst):
self.__name_block = b"" self.__name_block = b""
self.__fst_block = b"" self.__fst_block = b""
self.__nameblock_length = 0 self.__nameblock_length = 0
self.__current_file_offset = fst_offset self.__current_file_offset = fstbin_offset
def __str__(self): def __str__(self):
return self.__to_str(self.__root_node) return self.__to_str(self.__root_node)
def __to_str(self, node:Node, depth=0): def __to_str(self, node:Node, depth=0):
@ -245,12 +253,11 @@ class FstTree(Fst):
""" """
Add a path with each folder as Folder class and the File as a leaf. Add a path with each folder as Folder class and the File as a leaf.
We take care to set parent and childs for folder and retrieve necessary We take care to set parent and childs for folder and retrieve necessary
informations: informations from the node_path input:
* name * name
* size * size
* parent id & parent->child * parent id & parent->child
input: input: path = Path (folder / file)
* path = Path (folder / file)
""" """
parent = self.__root_node parent = self.__root_node
node = None node = None
@ -266,7 +273,7 @@ class FstTree(Fst):
""" """
Generate the FST. Generate the FST.
The hard part Here is that we have to know the result before The hard part Here is that we have to know the result before
knowing where we can begin to add files knowing where we can begin to add files.
""" """
self.__current_file_offset += self.__get_fst_length() self.__current_file_offset += self.__get_fst_length()
self.__prepare() self.__prepare()
@ -275,41 +282,118 @@ class FstTree(Fst):
class BootBin: class BootBin:
""" """
BootBin group all operations related to the boot.bin system file BootBin describe the Disk Header "boot.bin" file at the beginning of
using this class avoid errors and it's easier to use it elsewhere the GCM/iso. It groups all operations related to the boot.bin system
this groupment add meaning to hex values but we can also patch it. file extracted in sys/boot.bin. Using this class avoid errors on offsets
and makes it easier to get or set values.
Constructor: Constructor:
* datas = bytes or bytearray if edit is needed of the boot.bin * datas = bytes or bytearray if edit of the boot.bin is needed.
""" """
LEN = 0x440 LEN = 0x440
DOLOFFSET_OFFSET = 0x420 DOLOFFSET_OFFSET = 0x420
FSTOFFSET_OFFSET = 0x424 FSTOFFSET_OFFSET = 0x424
FSTLEN_OFFSET = 0x428 FSTLEN_OFFSET = 0x428
MAXFSTLEN_OFFSET = 0x42c FSTMAXLEN_OFFSET = 0x42c
__data = None __data = None
def __init__(self, data:bytes): def __init__(self, data:bytes): self.__data = data
self.__data = bytearray(data)
def data(self): return self.__data def data(self): return self.__data
def dvd_magic(self): def make_mut(self): self.__data = bytearray(self.__data)
return self.__data[0x1c:0x20] def game_code(self): return self.__data[:4].decode("ascii")
def fstbin_offset(self): def maker_code(self): return self.__data[4:6].decode("ascii")
return int.from_bytes(self.__data[BootBin.FSTOFFSET_OFFSET:BootBin.FSTOFFSET_OFFSET+4],"big", signed=False) def disk_number(self): return int.from_bytes(self.__data[6:7], 'big')
def fstbin_len(self): def game_version(self): return int.from_bytes(self.__data[7:8], 'big')
return int.from_bytes(self.__data[BootBin.FSTLEN_OFFSET:BootBin.FSTLEN_OFFSET+4],"big", signed=False) def audio_streaming(self): return int.from_bytes(self.__data[8:9], 'big')
def dol_offset(self): def stream_buffer_size(self): return int.from_bytes(self.__data[9:0xa], 'big')
return int.from_bytes(self.__data[BootBin.DOLOFFSET_OFFSET:BootBin.DOLOFFSET_OFFSET+4],"big", signed=False) def dvd_magic(self): return self.__data[0x1c:0x20]
def game_code(self): def game_name(self): return self.__data[0x20:0x60].split(b"\x00")[0].decode("utf-8")
return self.__data[:4].decode('utf-8') def dol_offset(self): return int.from_bytes(self.__data[BootBin.DOLOFFSET_OFFSET:BootBin.DOLOFFSET_OFFSET+4],"big")
def disc_number(self): def fst_offset(self): return int.from_bytes(self.__data[BootBin.FSTOFFSET_OFFSET:BootBin.FSTOFFSET_OFFSET+4],"big")
return int.from_bytes(self.__data[6:7], 'big', signed=False) def fst_len(self): return int.from_bytes(self.__data[BootBin.FSTLEN_OFFSET:BootBin.FSTLEN_OFFSET+4],"big")
def fst_max_len(self): return int.from_bytes(self.__data[BootBin.FSTMAXLEN_OFFSET:BootBin.FSTMAXLEN_OFFSET+4],"big")
def set_game_code(self, game_code:str):
self.__data[:4] = bytes(game_code, "ascii")
def set_maker_code(self, maker_code:str):
self.__data[4:6] = bytes(maker_code, "ascii")
def set_disk_number(self, disk_number:int):
self.__data[6:7] = disk_number.to_bytes(1, "big")
def set_game_version(self, game_version:int):
self.__data[7:8] = game_version.to_bytes(1, "big")
def set_audio_streaming(self, audio_streaming:int):
self.__data[8:9] = audio_streaming.to_bytes(1, "big")
def set_stream_buffer_size(self, stream_buffer_size:int):
self.__data[9:0xa] = stream_buffer_size.to_bytes(1, "big")
def set_dvd_magic(self, dvd_magic:int):
self.__data[0x1c:0x20] = dvd_magic.to_bytes(4, "big")
def set_game_name(self, game_name:int):
self.__data[0x20:0x60] = bytes(game_name, "utf-8").ljust(0x40, b"\x00")
def set_dol_offset(self, offset:int): def set_dol_offset(self, offset:int):
self.__data[BootBin.DOLOFFSET_OFFSET:BootBin.DOLOFFSET_OFFSET+4] = offset.to_bytes(4, "big") self.__data[BootBin.DOLOFFSET_OFFSET:BootBin.DOLOFFSET_OFFSET+4] = offset.to_bytes(4, "big")
def set_fst_offset(self, offset:int): def set_fst_offset(self, offset:int):
self.__data[BootBin.FSTOFFSET_OFFSET:BootBin.FSTOFFSET_OFFSET+4] = offset.to_bytes(4, "big") self.__data[BootBin.FSTOFFSET_OFFSET:BootBin.FSTOFFSET_OFFSET+4] = offset.to_bytes(4, "big")
def set_fst_len(self, size:int): def set_fst_len(self, size:int):
self.__data[BootBin.FSTLEN_OFFSET:BootBin.FSTLEN_OFFSET+4] = size.to_bytes(4, "big") self.__data[BootBin.FSTLEN_OFFSET:BootBin.FSTLEN_OFFSET+4] = size.to_bytes(4, "big")
def set_max_fst_len(self, size:int): def set_fst_max_len(self, size:int):
self.__data[BootBin.MAXFSTLEN_OFFSET:BootBin.MAXFSTLEN_OFFSET+4] = size.to_bytes(4, "big") self.__data[BootBin.FSTMAXLEN_OFFSET:BootBin.FSTMAXLEN_OFFSET+4] = size.to_bytes(4, "big")
class Bi2Bin:
"""
Bi2Bin describe the Disk Header Information "bi2.bin" file at the
beginning of the GCM/iso after boot.bin. It groups all operations
related to the bi2.bin system file extracted in sys/bi2.bin. Using
this class avoid errors on offsets and makes it easier to get or set
values.
Constructor:
* datas = bytes or bytearray if edit of the bi2.bin is needed.
"""
LEN = 0x2000
__data = None
def __init__(self, data:bytes): self.__data = data
def data(self): return self.__data
def make_mut(self): self.__data = bytearray(self.__data)
def debug_monitor_size(self): return int.from_bytes(self.__data[:4], "big")
def simulated_memory_size(self): return int.from_bytes(self.__data[4:8], "big")
def argument_offset(self): return int.from_bytes(self.__data[8:12], "big")
def debug_flag(self): return int.from_bytes(self.__data[12:16], "big")
def track_location(self): return int.from_bytes(self.__data[16:20], "big")
def track_size(self): return int.from_bytes(self.__data[20:24], "big")
def country_code(self): return int.from_bytes(self.__data[24:28], "big")
def total_disk(self): return int.from_bytes(self.__data[28:32], "big")
def long_file_name_support(self): return int.from_bytes(self.__data[32:36], "big")
def set_debug_monitor_size(self, debug_monitor_size:int):
self.__data[:4] = debug_monitor_size.to_bytes(4, "big")
def set_simulated_memory_size(self, simulated_memory_size:int):
self.__data[4:8] = simulated_memory_size.to_bytes(4, "big")
def set_argument_offset(self, argument_offset:int):
self.__data[8:12] = argument_offset.to_bytes(4, "big")
def set_debug_flag(self, debug_flag:int):
self.__data[12:16] = debug_flag.to_bytes(4, "big")
def set_track_location(self, track_location:int):
self.__data[16:20] = track_location.to_bytes(4, "big")
def set_track_size(self, track_size:int):
self.__data[20:24] = track_size.to_bytes(4, "big")
def set_country_code(self, country_code:int):
self.__data[24:28] = country_code.to_bytes(4, "big")
def set_total_disk(self, total_disk:int):
self.__data[28:32] = total_disk.to_bytes(4, "big")
def set_long_file_name_support(self, long_file_name_support:int):
self.__data[32:36] = long_file_name_support.to_bytes(4, "big")
class ApploaderImg:
__data = None
def __init__(self, data:bytes): self.__data = data
def data(self): return self.__data
def len(self): return len(self.__data)
def make_mut(self): self.__data = bytearray(self.__data)
def version(self): return self.__data[:0x10].split(b"\x00")[0].decode("ascii")
def entry_point(self): return int.from_bytes(self.__data[0x10:0x14], "big")
def size(self): return int.from_bytes(self.__data[0x14:0x18], "big")
def trailer_size(self): return int.from_bytes(self.__data[0x18:0x1c], "big")
def set_version(self, version:int): self.__data[:0x10] = bytes(version, "ascii").ljust(0x10, b"\x00")
def set_entry_point(self, entry_point:int): self.__data[0x10:0x14] = entry_point.to_bytes(4, "big")
def set_size(self, size:int): self.__data[0x14:0x18] = size.to_bytes(4, "big")
def set_trailer_size(self, trailer_size:int): self.__data[0x18:0x1c] = trailer_size.to_bytes(4, "big")
class Dol: class Dol:
@ -324,7 +408,7 @@ class Dol:
""" """
dol_len = Dol.HEADER_LEN dol_len = Dol.HEADER_LEN
for i in range(18): for i in range(18):
dol_len += int.from_bytes(dolheader_data[Dol.HEADER_SECTIONLENTABLE_OFFSET+i*4:Dol.HEADER_SECTIONLENTABLE_OFFSET+(i+1)*4], "big", signed=False) dol_len += int.from_bytes(dolheader_data[Dol.HEADER_SECTIONLENTABLE_OFFSET+i*4:Dol.HEADER_SECTIONLENTABLE_OFFSET+(i+1)*4], "big")
return dol_len return dol_len
@ -333,20 +417,244 @@ class Gcm:
Gcm handle all operations needed by the command parser. Gcm handle all operations needed by the command parser.
File format informations: https://sudonull.com/post/68549-Gamecube-file-system-device File format informations: https://sudonull.com/post/68549-Gamecube-file-system-device
""" """
BI2BIN_LEN = 0x2000
APPLOADER_HEADER_LEN = 0x20 APPLOADER_HEADER_LEN = 0x20
APPLOADER_OFFSET = 0x2440 APPLOADER_OFFSET = 0x2440
APPLOADERSIZE_OFFSET = 0x2454 APPLOADERLEN_OFFSET = 0x2454
DVD_MAGIC = b"\xC2\x33\x9F\x3D" DVD_MAGIC = b"\xC2\x33\x9F\x3D"
__bootbin = None # Disk header
__bi2bin = None # Disk header Information
__apploaderimg = None
__hex_pattern = re.compile("^0x[0-9a-fA-F]+$")
def __save_conf(self, sys_path:Path):
"Read boot.bin and bi2.bin and save theirs conf in sys/system.conf."
config = ConfigParser(allow_no_value=True) # allow_no_value to allow adding comments
config.optionxform = str # makes options case sensitive
config.add_section("Default")
config.set("Default", "# Documentation available here: https://github.com/Virtual-World-RE/NeoGF/tree/main/gcmtool#system_conf")
config.set("Default", "boot.bin_section", "disabled")
config.set("Default", "bi2.bin_section", "disabled")
config.set("Default", "apploader.img_section", "disabled")
config.add_section("boot.bin")
config.set("boot.bin", "GameCode", self.__bootbin.game_code()) # 4 bytes ASCII
config.set("boot.bin", "MakerCode", self.__bootbin.maker_code()) # 2 bytes ASCII
config.set("boot.bin", "DiskNumber", str(self.__bootbin.disk_number())) # 0-98
config.set("boot.bin", "GameVersion", str(self.__bootbin.game_version())) # 0-99
config.set("boot.bin", "AudioStreaming", str(self.__bootbin.audio_streaming())) # 0 or 1 flag
config.set("boot.bin", "StreamBufferSize", str(self.__bootbin.stream_buffer_size())) # 0-15
config.set("boot.bin", "DVDMagic", "0x" + self.__bootbin.dvd_magic().hex())
config.set("boot.bin", "GameName", self.__bootbin.game_name()) # 64 bytes
config.set("boot.bin", "DolOffset", f"auto")
config.set("boot.bin", "FstOffset", f"auto")
config.set("boot.bin", "FstLen", f"auto")
config.set("boot.bin", "FstMaxLen", f"auto")
config.add_section("bi2.bin")
config.set("bi2.bin", "DebugMonitorSize", f"0x{self.__bi2bin.debug_monitor_size():x}")
config.set("bi2.bin", "SimulatedMemorySize", f"0x{self.__bi2bin.simulated_memory_size():x}")
config.set("bi2.bin", "ArgumentOffset", f"0x{self.__bi2bin.argument_offset():x}")
config.set("bi2.bin", "DebugFlag", str(self.__bi2bin.debug_flag()))
config.set("bi2.bin", "TrackLocation", f"0x{self.__bi2bin.track_location():x}")
config.set("bi2.bin", "TrackSize", f"0x{self.__bi2bin.track_size():x}")
config.set("bi2.bin", "CountryCode", str(self.__bi2bin.country_code())) # 0, 1, 2, 4
config.set("bi2.bin", "TotalDisk", str(self.__bi2bin.total_disk())) # 1-99
config.set("bi2.bin", "LongFileNameSupport", str(self.__bi2bin.long_file_name_support())) # 0, 1
config.add_section("apploader.img")
config.set("apploader.img", "Version", self.__apploaderimg.version())
config.set("apploader.img", "EntryPoint", f"0x{self.__apploaderimg.entry_point():x}")
config.set("apploader.img", "Size", f"0x{self.__apploaderimg.size():x}")
config.set("apploader.img", "TrailerSize", f"0x{self.__apploaderimg.trailer_size():x}")
with (sys_path / "system.conf").open("w") as conf_file:
config.write(conf_file)
def __load_conf(self, sys_path:Path):
"Patch boot.bin, bi2.bin and apploader.img with the conf in sys/system.conf if Default section status is enabled."
config = ConfigParser(allow_no_value=True) # allow_no_value to allow adding comments
config.optionxform = str # makes options case sensitive
config.read(sys_path / "system.conf")
if config["Default"]["boot.bin_section"].lower() not in ["enabled", "disabled"]:
raise InvalidConfValueError("Error - Invalid [Default][boot.bin_section]: must be enabled or disabled.")
if config["Default"]["bi2.bin_section"].lower() not in ["enabled", "disabled"]:
raise InvalidConfValueError("Error - Invalid [Default][bi2.bin_section]: must be enabled or disabled.")
if config["Default"]["apploader.img_section"].lower() not in ["enabled", "disabled"]:
raise InvalidConfValueError("Error - Invalid [Default][apploader.img_section]: must be enabled or disabled.")
def check_numeric_format(config:ConfigParser, conf_list:list):
for conf in conf_list:
if not config[conf[0]][conf[1]].isnumeric():
raise InvalidConfValueError(f"Error - Invalid [{conf[0]}][{conf[1]}]: must be numeric - 1234.")
def check_hex_format(config:ConfigParser, conf_list:list):
for conf in conf_list:
if conf[2] and config[conf[0]][conf[1]] == "auto":
continue
if not self.__hex_pattern.fullmatch(config[conf[0]][conf[1]]):
raise InvalidConfValueError(f"Error - Invalid [{conf[0]}][{conf[1]}]: must be hex - 0xabcdef.")
check_numeric_format(config, [
("boot.bin", "DiskNumber"),
("boot.bin", "GameVersion"),
("boot.bin", "StreamBufferSize"),
("bi2.bin", "DebugFlag"),
("bi2.bin", "TotalDisk")])
check_hex_format(config, [
("boot.bin", "DVDMagic", False),
("boot.bin", "DolOffset", True),
("boot.bin", "FstOffset", True),
("boot.bin", "FstLen", True),
("boot.bin", "FstMaxLen", True),
("bi2.bin", "DebugMonitorSize", False),
("bi2.bin", "SimulatedMemorySize", False),
("bi2.bin", "ArgumentOffset", False),
("bi2.bin", "TrackLocation", False),
("bi2.bin", "TrackSize", False),
("apploader.img", "EntryPoint", False),
("apploader.img", "Size", False),
("apploader.img", "TrailerSize", False)])
self.__bootbin.make_mut()
self.__bi2bin.make_mut()
self.__apploaderimg.make_mut()
if config["Default"]["boot.bin_section"].lower() == "enabled":
if len(config["boot.bin"]["GameCode"]) != 4:
raise InvalidConfValueError("Error - Invalid [boot.bin][GameCode]: must be str with length = 4.")
self.__bootbin.set_game_code( config["boot.bin"]["GameCode"] )
if len(config["boot.bin"]["MakerCode"]) != 2:
raise InvalidConfValueError("Error - Invalid [boot.bin][MakerCode]: must be str with length = 2.")
self.__bootbin.set_maker_code( config["boot.bin"]["MakerCode"] )
disk_number = int(config["boot.bin"]["DiskNumber"])
if disk_number > 98:
raise InvalidConfValueError("Error - Invalid [boot.bin][DiskNumber]: must be int with value < 99.")
self.__bootbin.set_disk_number( disk_number )
game_version = int(config["boot.bin"]["GameVersion"])
if game_version > 99:
raise InvalidConfValueError("Error - Invalid [boot.bin][GameVersion]: must be int with value < 100.")
self.__bootbin.set_game_version( game_version )
if config["boot.bin"]["AudioStreaming"] not in ["0", "1"]:
raise InvalidConfValueError("Error - Invalid [boot.bin][AudioStreaming]: this flag must be 0 or 1.")
self.__bootbin.set_audio_streaming( int(config["boot.bin"]["AudioStreaming"]) )
stream_buffer_size = int(config["boot.bin"]["StreamBufferSize"])
if stream_buffer_size > 15:
raise InvalidConfValueError("Error - Invalid [boot.bin][StreamBufferSize]: must be int with value between 0 and 15.")
self.__bootbin.set_stream_buffer_size( stream_buffer_size )
if len(config["boot.bin"]["DVDMagic"]) != 10:
raise InvalidConfValueError("Error - Invalid [boot.bin][DVDMagic]: must be 8 hex digits begining with 0x.")
self.__bootbin.set_dvd_magic( int(config["boot.bin"]["DVDMagic"], 16) )
if len(config["boot.bin"]["GameName"]) > 64:
raise InvalidConfValueError("Error - Invalid [boot.bin][GameName]: must be str with length < 64.")
self.__bootbin.set_game_name( config["boot.bin"]["GameName"] )
if config["boot.bin"]["DolOffset"] != "auto":
dol_offset = int(config["boot.bin"]["DolOffset"], 16)
if dol_offset > 0xffffffff:
raise InvalidConfValueError("Error - Invalid [boot.bin][DolOffset]: must be auto or unsigned hex value with length < 5 bytes.")
self.__bootbin.set_dol_offset( dol_offset )
if config["boot.bin"]["FstOffset"] != "auto":
fst_offset = int(config["boot.bin"]["FstOffset"], 16)
if fst_offset > 0xffffffff:
raise InvalidConfValueError("Error - Invalid [boot.bin][FstOffset]: must be auto or unsigned hex value with length < 5 bytes.")
self.__bootbin.set_fst_offset( fst_offset )
if config["boot.bin"]["FstLen"] != "auto":
fst_len = int(config["boot.bin"]["FstLen"], 16)
if fst_len > 0xffffffff:
raise InvalidConfValueError("Error - Invalid [boot.bin][FstLen]: must be auto or unsigned hex value with length < 5 bytes.")
self.__bootbin.set_fst_len( fst_len )
if config["boot.bin"]["FstMaxLen"] != "auto":
fst_max_len = int(config["boot.bin"]["FstMaxLen"], 16)
if fst_max_len > 0xffffffff:
raise InvalidConfValueError("Error - Invalid [boot.bin][FstMaxLen]: must be auto or unsigned hex value with length < 5 bytes.")
self.__bootbin.set_fst_max_len( fst_max_len )
if config["Default"]["bi2.bin_section"].lower() == "enabled":
debug_monitor_size = int(config["bi2.bin"]["DebugMonitorSize"], 16)
if debug_monitor_size > 0xffffffff or debug_monitor_size & 31:
raise InvalidConfValueError("Error - Invalid [bi2.bin][DebugMonitorSize]: must be hex value with length < 5 bytes and aligned to 32.")
self.__bi2bin.set_debug_monitor_size( debug_monitor_size )
simulated_memory_size = int(config["bi2.bin"]["SimulatedMemorySize"], 16)
if simulated_memory_size > 0xffffffff or simulated_memory_size & 31:
raise InvalidConfValueError("Error - Invalid [bi2.bin][SimulatedMemorySize]: must be hex value with length < 5 bytes and aligned to 32.")
self.__bi2bin.set_simulated_memory_size( simulated_memory_size )
argument_offset = int(config["bi2.bin"]["ArgumentOffset"], 16)
if argument_offset > 0xffffffff:
raise InvalidConfValueError("Error - Invalid [bi2.bin][ArgumentOffset]: must be hex value with length < 5 bytes.")
self.__bi2bin.set_argument_offset( argument_offset )
debug_flag = int(config["bi2.bin"]["DebugFlag"])
if debug_flag > 0xffffffff:
raise InvalidConfValueError("Error - Invalid [bi2.bin][DebugFlag]: must be hex value with length < 5 bytes.")
self.__bi2bin.set_debug_flag( debug_flag )
track_location = int(config["bi2.bin"]["TrackLocation"], 16)
if track_location > 0xffffffff:
raise InvalidConfValueError("Error - Invalid [bi2.bin][TrackLocation]: must be hex value with length < 5 bytes.")
self.__bi2bin.set_track_location( track_location )
track_size = int(config["bi2.bin"]["TrackSize"], 16)
if track_size > 0xffffffff:
raise InvalidConfValueError("Error - Invalid [bi2.bin][TrackSize]: must be hex value with length < 5 bytes.")
self.__bi2bin.set_track_size( track_size )
if config["bi2.bin"]["CountryCode"] not in ["0", "1", "2", "4"]:
raise InvalidConfValueError("Error - Invalid [bi2.bin][CountryCode]: must have 0, 1, 2 or 4 value.")
self.__bi2bin.set_country_code( int(config["bi2.bin"]["CountryCode"]) )
if int(config["bi2.bin"]["TotalDisk"]) > 99:
raise InvalidConfValueError("Error - Invalid [bi2.bin][TotalDisk]: must between 1 and 99.")
self.__bi2bin.set_total_disk( int(config["bi2.bin"]["TotalDisk"], 16) )
if config["bi2.bin"]["LongFileNameSupport"] not in ["0", "1"]:
raise InvalidConfValueError("Error - Invalid [bi2.bin][LongFileNameSupport]: must be 0 or 1.")
self.__bi2bin.set_long_file_name_support( int(config["bi2.bin"]["LongFileNameSupport"]) )
if config["Default"]["apploader.img_section"].lower() == "enabled":
version = config["apploader.img"]["Version"]
if len(version) > 10:
raise InvalidConfValueError("Error - Invalid [apploader.img][Version]: must be 16 byte ascii string.")
self.__apploaderimg.set_version( version )
entry_point = int(config["apploader.img"]["EntryPoint"], 16)
if entry_point > 0xffffffff:
raise InvalidConfValueError("Error - Invalid [apploader.img][EntryPoint]: must be hex value with length < 5 bytes.")
self.__apploaderimg.set_entry_point( entry_point )
size = int(config["apploader.img"]["Size"], 16)
if size > 0xffffffff:
raise InvalidConfValueError("Error - Invalid [apploader.img][Size]: must be hex value with length < 5 bytes.")
self.__apploaderimg.set_size( size )
trailer_size = int(config["apploader.img"]["TrailerSize"], 16)
if trailer_size > 0xffffffff:
raise InvalidConfValueError("Error - Invalid [apploader.img][TrailerSize]: must be hex value with length < 5 bytes.")
self.__apploaderimg.set_trailer_size( trailer_size )
(sys_path / "boot.bin").write_bytes(self.__bootbin.data())
(sys_path / "bi2.bin").write_bytes(self.__bi2bin.data())
(sys_path / "apploader.img").write_bytes(self.__apploaderimg.data())
def __get_min_file_offset(self, fstbin_data:bytes): def __get_min_file_offset(self, fstbin_data:bytes):
"Get the min file offset to check if there is an overflow." "Get the min file offset to check if there is an overflow."
min_offset = None min_offset = None
for i in range(2, int.from_bytes(fstbin_data[8:12], "big", signed=False)): for i in range(2, int.from_bytes(fstbin_data[8:12], "big")):
if int.from_bytes(fstbin_data[i*12:i*12+1], "big", signed=False) == FstTree.TYPE_FILE: if int.from_bytes(fstbin_data[i*12:i*12+1], "big") == FstTree.TYPE_FILE:
if min_offset is None: if min_offset is None:
min_offset = int.from_bytes(fstbin_data[i*12+4:i*12+8], "big", signed=False) min_offset = int.from_bytes(fstbin_data[i*12+4:i*12+8], "big")
continue continue
min_offset = min(min_offset, int.from_bytes(fstbin_data[i*12+4:i*12+8], "big", signed=False)) min_offset = min(min_offset, int.from_bytes(fstbin_data[i*12+4:i*12+8], "big"))
return min_offset return min_offset
def unpack(self, iso_path:Path, folder_path:Path): def unpack(self, iso_path:Path, folder_path:Path):
""" """
@ -355,27 +663,27 @@ class Gcm:
input: folder_path = Path input: folder_path = Path
""" """
with iso_path.open("rb") as iso_file: with iso_path.open("rb") as iso_file:
bootbin = BootBin(iso_file.read(BootBin.LEN)) self.__bootbin = BootBin(iso_file.read(BootBin.LEN))
if bootbin.dvd_magic() != Gcm.DVD_MAGIC: if self.__bootbin.dvd_magic() != Gcm.DVD_MAGIC:
raise InvalidDVDMagicError("Error - Invalid DVD format - this tool is for ISO/GCM files") raise InvalidDVDMagicError("Error - Invalid DVD format - this tool is for ISO/GCM files.")
bi2bin_data = iso_file.read(Gcm.BI2BIN_LEN) self.__bi2bin = Bi2Bin(iso_file.read(Bi2Bin.LEN))
iso_file.seek(Gcm.APPLOADERSIZE_OFFSET) iso_file.seek(Gcm.APPLOADERLEN_OFFSET)
size = int.from_bytes(iso_file.read(4), "big", signed=False) size = int.from_bytes(iso_file.read(4), "big")
trailerSize = int.from_bytes(iso_file.read(4), "big", signed=False) trailerSize = int.from_bytes(iso_file.read(4), "big")
apploader_size = Gcm.APPLOADER_HEADER_LEN + size + trailerSize apploader_size = Gcm.APPLOADER_HEADER_LEN + size + trailerSize
iso_file.seek(Gcm.APPLOADER_OFFSET) iso_file.seek(Gcm.APPLOADER_OFFSET)
apploaderimg_data = iso_file.read(apploader_size) self.__apploaderimg = ApploaderImg(iso_file.read(apploader_size))
fstbin_offset = bootbin.fstbin_offset() fstbin_offset = self.__bootbin.fst_offset()
fstbin_len = bootbin.fstbin_len() fstbin_len = self.__bootbin.fst_len()
iso_file.seek( fstbin_offset ) iso_file.seek( fstbin_offset )
fstbin_data = iso_file.read( fstbin_len ) fstbin_data = iso_file.read( fstbin_len )
dol_offset = bootbin.dol_offset() dol_offset = self.__bootbin.dol_offset()
iso_file.seek( dol_offset ) iso_file.seek( dol_offset )
dol = Dol() dol = Dol()
dolheader_data = iso_file.read(Dol.HEADER_LEN) dolheader_data = iso_file.read(Dol.HEADER_LEN)
@ -383,7 +691,7 @@ class Gcm:
bootdol_data = dolheader_data + iso_file.read( dol_len - Dol.HEADER_LEN ) bootdol_data = dolheader_data + iso_file.read( dol_len - Dol.HEADER_LEN )
if folder_path == Path("."): if folder_path == Path("."):
folder_path = Path(f"{bootbin.game_code()}-{bootbin.disc_number():02}") folder_path = Path(f"{self.__bootbin.game_code()}-{self.__bootbin.disk_number():02}")
if folder_path.is_dir(): if folder_path.is_dir():
raise InvalidUnpackFolderError(f"Error - \"{folder_path}\" already exist. Remove this folder or use another name for the unpack folder.") raise InvalidUnpackFolderError(f"Error - \"{folder_path}\" already exist. Remove this folder or use another name for the unpack folder.")
@ -392,16 +700,19 @@ class Gcm:
sys_path.mkdir(parents=True) sys_path.mkdir(parents=True)
logging.debug(f"{iso_path}(0x0:0x{BootBin.LEN:x}) -> {sys_path / 'boot.bin'}") logging.debug(f"{iso_path}(0x0:0x{BootBin.LEN:x}) -> {sys_path / 'boot.bin'}")
(sys_path / "boot.bin").write_bytes(bootbin.data()) (sys_path / "boot.bin").write_bytes(self.__bootbin.data())
logging.debug(f"{iso_path}(0x440:0x{Gcm.APPLOADER_OFFSET:x}) -> {sys_path / 'bi2.bin'}") logging.debug(f"{iso_path}(0x440:0x{Gcm.APPLOADER_OFFSET:x}) -> {sys_path / 'bi2.bin'}")
(sys_path / "bi2.bin" ).write_bytes(bi2bin_data) (sys_path / "bi2.bin" ).write_bytes(self.__bi2bin.data())
logging.debug(f"{iso_path}(0x{Gcm.APPLOADER_OFFSET:x}:0x{Gcm.APPLOADER_OFFSET + apploader_size:x} -> {sys_path / 'apploader.img'}") logging.debug(f"{iso_path}(0x{Gcm.APPLOADER_OFFSET:x}:0x{Gcm.APPLOADER_OFFSET + apploader_size:x} -> {sys_path / 'apploader.img'}")
(sys_path / "apploader.img").write_bytes(apploaderimg_data) (sys_path / "apploader.img").write_bytes(self.__apploaderimg.data())
logging.debug(f"{iso_path}(0x{fstbin_offset:x}:0x{fstbin_offset + fstbin_len:x}) -> {sys_path / 'fst.bin'}") logging.debug(f"{iso_path}(0x{fstbin_offset:x}:0x{fstbin_offset + fstbin_len:x}) -> {sys_path / 'fst.bin'}")
(sys_path / "fst.bin").write_bytes(fstbin_data) (sys_path / "fst.bin").write_bytes(fstbin_data)
logging.debug(f"{iso_path}(0x{dol_offset:x}:0x{dol_offset + dol_len:x}) -> {sys_path / 'boot.dol'}") logging.debug(f"{iso_path}(0x{dol_offset:x}:0x{dol_offset + dol_len:x}) -> {sys_path / 'boot.dol'}")
(sys_path / "boot.dol").write_bytes(bootdol_data) (sys_path / "boot.dol").write_bytes(bootdol_data)
# Generate conf from sys files
self.__save_conf(sys_path)
root_path = folder_path / "root" root_path = folder_path / "root"
root_path.mkdir() root_path.mkdir()
@ -410,7 +721,7 @@ class Gcm:
currentdir_path = root_path currentdir_path = root_path
# root: id=0 so nextdir is the end # root: id=0 so nextdir is the end
nextdir = int.from_bytes(fstbin_data[8:12], "big", signed=False) nextdir = int.from_bytes(fstbin_data[8:12], "big")
# offset of filenames block # offset of filenames block
base_names = nextdir * 12 base_names = nextdir * 12
# go to parent when id reach next dir # go to parent when id reach next dir
@ -418,24 +729,24 @@ class Gcm:
for id in range(1, base_names // 12): for id in range(1, base_names // 12):
i = id * 12 i = id * 12
file_type = int.from_bytes(fstbin_data[i:i+1], "big", signed=False) file_type = int.from_bytes(fstbin_data[i:i+1], "big")
name = fstbin_data[base_names + int.from_bytes(fstbin_data[i+1:i+4], "big", signed=False):].split(b"\x00")[0].decode("utf-8") name = fstbin_data[base_names + int.from_bytes(fstbin_data[i+1:i+4], "big"):].split(b"\x00")[0].decode("utf-8")
while id == nextdir_arr[-1]: while id == nextdir_arr[-1]:
currentdir_path = currentdir_path.parent currentdir_path = currentdir_path.parent
nextdir_arr.pop() nextdir_arr.pop()
if file_type == FstTree.TYPE_DIR: if file_type == FstTree.TYPE_DIR:
nextdir = int.from_bytes(fstbin_data[i+8:i+12], "big", signed=False) nextdir = int.from_bytes(fstbin_data[i+8:i+12], "big")
parentdir = int.from_bytes(fstbin_data[i+4:i+8], "big", signed=False) parentdir = int.from_bytes(fstbin_data[i+4:i+8], "big")
nextdir_arr.append( nextdir ) nextdir_arr.append( nextdir )
currentdir_path = dir_id_path[parentdir] / name currentdir_path = dir_id_path[parentdir] / name
dir_id_path[id] = currentdir_path dir_id_path[id] = currentdir_path
currentdir_path.mkdir(exist_ok=True) currentdir_path.mkdir(exist_ok=True)
else: else:
fileoffset = int.from_bytes(fstbin_data[i+4:i+8], "big", signed=False) fileoffset = int.from_bytes(fstbin_data[i+4:i+8], "big")
filesize = int.from_bytes(fstbin_data[i+8:i+12], "big", signed=False) filesize = int.from_bytes(fstbin_data[i+8:i+12], "big")
iso_file.seek(fileoffset) iso_file.seek(fileoffset)
(currentdir_path / name).write_bytes( iso_file.read(filesize) ) (currentdir_path / name).write_bytes( iso_file.read(filesize) )
@ -456,28 +767,52 @@ class Gcm:
with iso_path.open("wb") as iso_file: with iso_path.open("wb") as iso_file:
sys_path = folder_path / "sys" sys_path = folder_path / "sys"
logging.debug(f"{sys_path / 'boot.bin'} -> {iso_path}(0x0:0x{BootBin.LEN:x})") self.__bootbin = BootBin((sys_path / "boot.bin").read_bytes())
bootbin = BootBin((sys_path / "boot.bin").read_bytes()) self.__bi2bin = Bi2Bin((sys_path / "bi2.bin").read_bytes())
iso_file.write(bootbin.data()) self.__apploaderimg = ApploaderImg((sys_path / "apploader.img").read_bytes())
logging.debug(f"{sys_path / 'bi2.bin'} -> {iso_path}(0x{BootBin.LEN:x}:0x{Gcm.APPLOADER_OFFSET:x})")
iso_file.write((sys_path / "bi2.bin").read_bytes())
logging.debug(f"{sys_path / 'apploader.img'} -> {iso_path}(0x{Gcm.APPLOADER_OFFSET:x}:0x{Gcm.APPLOADER_OFFSET + (sys_path / 'apploader.img').stat().st_size:x}")
iso_file.write((sys_path / "apploader.img").read_bytes())
fstbin_offset = bootbin.fstbin_offset() # Patch boot.bin and bi2.bin if system.conf is enabled
fstbin_len = bootbin.fstbin_len() self.__load_conf(sys_path)
logging.debug(f"{sys_path / 'boot.bin'} -> {iso_path}(0x0:0x{BootBin.LEN:x})")
iso_file.write(self.__bootbin.data())
logging.debug(f"{sys_path / 'bi2.bin'} -> {iso_path}(0x{BootBin.LEN:x}:0x{Gcm.APPLOADER_OFFSET:x})")
iso_file.write(self.__bi2bin.data())
apploader_end_offset = Gcm.APPLOADER_OFFSET + self.__apploaderimg.len()
logging.debug(f"{sys_path / 'apploader.img'} -> {iso_path}(0x{Gcm.APPLOADER_OFFSET:x}:0x{apploader_end_offset:x}")
iso_file.write(self.__apploaderimg.data())
fstbin_offset = self.__bootbin.fst_offset()
fstbin_len = self.__bootbin.fst_len()
fstbin_end_offset = fstbin_offset + fstbin_len
if (sys_path / "fst.bin").stat().st_size != fstbin_len: if (sys_path / "fst.bin").stat().st_size != fstbin_len:
raise InvalidFSTSizeError(f"Error - Invalid fst.bin size in boot.bin offset 0x{BootBin.FSTLEN_OFFSET:x}:0x{BootBin.FSTLEN_OFFSET+4:x}!") raise InvalidFSTSizeError(f"Error - Invalid fst.bin size in boot.bin offset 0x{BootBin.FSTLEN_OFFSET:x}:0x{BootBin.FSTLEN_OFFSET+4:x}.")
logging.debug(f"{sys_path / 'fst.bin'} -> {iso_path}(0x{fstbin_offset:x}:0x{fstbin_offset + fstbin_len:x})") logging.debug(f"{sys_path / 'fst.bin'} -> {iso_path}(0x{fstbin_offset:x}:0x{fstbin_offset + fstbin_len:x})")
iso_file.seek( fstbin_offset ) iso_file.seek( fstbin_offset )
fstbin_data = (sys_path / "fst.bin").read_bytes() fstbin_data = (sys_path / "fst.bin").read_bytes()
iso_file.write( fstbin_data ) iso_file.write( fstbin_data )
dol_offset = bootbin.dol_offset() dol_offset = self.__bootbin.dol_offset()
dol_end_offset = dol_offset + (sys_path / 'boot.dol').stat().st_size dol_end_offset = dol_offset + (sys_path / 'boot.dol').stat().st_size
min_file_offset = self.__get_min_file_offset(fstbin_data)
# FST can be before the dol or after # FST can be before the dol or after
if not disable_ignore and (dol_offset < fstbin_offset < dol_end_offset or (fstbin_offset < dol_offset and dol_end_offset > self.__get_min_file_offset(fstbin_data))): # We control values to avoid Overflows
raise DolSizeOverflowError("Error - The dol size has been increased and overflow on next file or on FST. To solve this use --rebuild-fst.") if not disable_ignore:
if not Gcm.APPLOADER_OFFSET < dol_offset < dol_end_offset <= fstbin_offset and not \
fstbin_offset < dol_offset < dol_end_offset <= min_file_offset:
raise DolSizeOverflowError("Error - The dol size has been increased and overflow on next file or on FST. To solve this check the sys/system.conf file if used or use --rebuild-fst.")
if not Gcm.APPLOADER_OFFSET < fstbin_offset < fstbin_end_offset <= dol_offset and not \
dol_end_offset <= fstbin_offset < fstbin_end_offset <= min_file_offset:
raise FstSizeOverflowError("Error - The FST size has been increased and overflow on next file or on FST. To solve this check the sys/system.conf file if used or use --rebuild-fst.")
if Gcm.APPLOADER_OFFSET < dol_offset < apploader_end_offset or \
Gcm.APPLOADER_OFFSET < fstbin_offset < apploader_end_offset:
raise ApploaderOverflowError("Error - The apploader size has been increased and overflow on dol or on FST. To solve this check the sys/system.conf file if used or use --rebuild-fst.")
logging.debug(f"{sys_path / 'boot.dol'} -> {iso_path}(0x{dol_offset:x}:0x{dol_end_offset:x})") logging.debug(f"{sys_path / 'boot.dol'} -> {iso_path}(0x{dol_offset:x}:0x{dol_end_offset:x})")
iso_file.seek( dol_offset ) iso_file.seek( dol_offset )
iso_file.write( (sys_path / "boot.dol").read_bytes() ) iso_file.write( (sys_path / "boot.dol").read_bytes() )
@ -487,7 +822,7 @@ class Gcm:
currentdir_path = folder_path / "root" currentdir_path = folder_path / "root"
# root: id=0 so nextdir is the end # root: id=0 so nextdir is the end
nextdir = int.from_bytes(fstbin_data[8:12], "big", signed=False) nextdir = int.from_bytes(fstbin_data[8:12], "big")
# offset of filenames block # offset of filenames block
base_names = nextdir * 12 base_names = nextdir * 12
# go to parent when id reach next dir # go to parent when id reach next dir
@ -499,16 +834,16 @@ class Gcm:
for id in range(1, base_names // 12): for id in range(1, base_names // 12):
i = id * 12 i = id * 12
file_type = int.from_bytes(fstbin_data[i:i+1], "big", signed=False) file_type = int.from_bytes(fstbin_data[i:i+1], "big")
name = fstbin_data[base_names + int.from_bytes(fstbin_data[i+1:i+4], "big", signed=False):].split(b"\x00")[0].decode("utf-8") name = fstbin_data[base_names + int.from_bytes(fstbin_data[i+1:i+4], "big"):].split(b"\x00")[0].decode("utf-8")
while id == nextdir_arr[-1]: while id == nextdir_arr[-1]:
currentdir_path = currentdir_path.parent currentdir_path = currentdir_path.parent
nextdir_arr.pop() nextdir_arr.pop()
if file_type == FstTree.TYPE_DIR: if file_type == FstTree.TYPE_DIR:
nextdir = int.from_bytes(fstbin_data[i+8:i+12], "big", signed=False) nextdir = int.from_bytes(fstbin_data[i+8:i+12], "big")
parentdir = int.from_bytes(fstbin_data[i+4:i+8], "big", signed=False) parentdir = int.from_bytes(fstbin_data[i+4:i+8], "big")
nextdir_arr.append( nextdir ) nextdir_arr.append( nextdir )
currentdir_path = dir_id_path[parentdir] / name currentdir_path = dir_id_path[parentdir] / name
@ -523,22 +858,24 @@ class Gcm:
"The file has been removed or renamed. Use --rebuild-fst to update the FST and avoid this error." "The file has been removed or renamed. Use --rebuild-fst to update the FST and avoid this error."
"Warning: DVD SDK use filenames to load files from the GCM/iso.") "Warning: DVD SDK use filenames to load files from the GCM/iso.")
file_offset = int.from_bytes(fstbin_data[i+4:i+8], "big", signed=False) file_offset = int.from_bytes(fstbin_data[i+4:i+8], "big")
file_len = int.from_bytes(fstbin_data[i+8:i+12], "big", signed=False) file_len = int.from_bytes(fstbin_data[i+8:i+12], "big")
if (currentdir_path / name).stat().st_size != file_len: if (currentdir_path / name).stat().st_size != file_len:
raise InvalidFSTFileSizeError(f"Error - Invalid file size: {currentdir_path / name} - use --rebuild-fst before packing files in the iso.") raise InvalidFSTFileSizeError(f"Error - Invalid file size: {currentdir_path / name} - use --rebuild-fst before packing files in the iso.")
logging.debug(f"{currentdir_path / name} -> {iso_path}(0x{file_offset:x}:0x{file_offset + file_len:x})") logging.debug(f"{currentdir_path / name} -> {iso_path}(0x{file_offset:x}:0x{file_offset + file_len:x})")
iso_file.seek(file_offset) iso_file.seek(file_offset)
iso_file.write( (currentdir_path / name).read_bytes() ) iso_file.write( (currentdir_path / name).read_bytes() )
except (InvalidFSTSizeError, DolSizeOverflowError, InvalidRootFileFolderCountError, InvalidFSTFileSizeError, FSTDirNotFoundError, FSTFileNotFoundError): except (InvalidFSTSizeError, DolSizeOverflowError, InvalidRootFileFolderCountError, InvalidFSTFileSizeError, \
FSTDirNotFoundError, FSTFileNotFoundError, InvalidConfValueError, FstSizeOverflowError, ApploaderOverflowError):
iso_path.unlink() iso_path.unlink()
raise raise
def rebuild_fst(self, folder_path:Path, align:int): def rebuild_fst(self, folder_path:Path, align:int):
""" """
Rebuild FST generate a new file system by using all files in the root folder Rebuild FST generate a new file system by using all files in the root folder
it also patch boot.bin caracteristics and apploader.img or also file system changes. it patch boot.bin caracteristics, apploader.img and also file system changes.
Game dol use filenames to find files so be carrefull when changing the root filesystem. Game dol use FST filenames to find files so be carrefull when changing the
root filesystem. Align is 0x8000 for APDCM.
input: folder_path = Path input: folder_path = Path
input: align = int input: align = int
""" """
@ -547,14 +884,14 @@ class Gcm:
dol_offset = align_top(Gcm.APPLOADER_OFFSET + (sys_path / "apploader.img").stat().st_size, align) dol_offset = align_top(Gcm.APPLOADER_OFFSET + (sys_path / "apploader.img").stat().st_size, align)
logging.info(f"Patching {Path('sys/boot.bin')} offset 0x{BootBin.DOLOFFSET_OFFSET:x} with new dol offset (0x{dol_offset:x})") logging.info(f"Patching {Path('sys/boot.bin')} offset 0x{BootBin.DOLOFFSET_OFFSET:x} with new dol offset (0x{dol_offset:x})")
bootbin = BootBin((sys_path / "boot.bin").read_bytes()) self.__bootbin = BootBin(bytearray((sys_path / "boot.bin").read_bytes()))
bootbin.set_dol_offset(dol_offset) self.__bootbin.set_dol_offset(dol_offset)
fst_offset = align_top(dol_offset + (sys_path / "boot.dol").stat().st_size, align) fstbin_offset = align_top(dol_offset + (sys_path / "boot.dol").stat().st_size, align)
logging.info(f"Patching {Path('sys/boot.bin')} offset 0x{BootBin.FSTOFFSET_OFFSET:x} with new FST offset (0x{fst_offset:x})") logging.info(f"Patching {Path('sys/boot.bin')} offset 0x{BootBin.FSTOFFSET_OFFSET:x} with new FST offset (0x{fstbin_offset:x})")
bootbin.set_fst_offset(fst_offset) self.__bootbin.set_fst_offset(fstbin_offset)
fst_tree = FstTree(root_path, fst_offset, align=align) fst_tree = FstTree(root_path, fstbin_offset, align=align)
# Sorting paths approach original fst sort, but in original fst specials chars are after and not before chars # Sorting paths approach original fst sort, but in original fst specials chars are after and not before chars
path_list = sorted([path for path in root_path.glob('**/*')], key=lambda s:Path(str(s).upper())) path_list = sorted([path for path in root_path.glob('**/*')], key=lambda s:Path(str(s).upper()))
@ -567,46 +904,52 @@ class Gcm:
logging.info(f"Writing fst in {Path('sys/fst.bin')}") logging.info(f"Writing fst in {Path('sys/fst.bin')}")
fst_path.write_bytes( fst_tree.generate_fst() ) fst_path.write_bytes( fst_tree.generate_fst() )
fst_size = fst_path.stat().st_size fstbin_len = fst_path.stat().st_size
logging.info(f"Patching {Path('sys/boot.bin')} offset 0x{BootBin.FSTLEN_OFFSET:x} with new FST size (0x{fst_size:x})") logging.info(f"Patching {Path('sys/boot.bin')} offset 0x{BootBin.FSTLEN_OFFSET:x} with new FST size (0x{fstbin_len:x})")
bootbin.set_fst_len(fst_size) self.__bootbin.set_fst_len(fstbin_len)
logging.info(f"Patching {Path('sys/boot.bin')} offset 0x{BootBin.MAXFSTLEN_OFFSET:x} with new FST max size (0x{fst_size:x})") logging.info(f"Patching {Path('sys/boot.bin')} offset 0x{BootBin.FSTMAXLEN_OFFSET:x} with new FST max size (0x{fstbin_len:x})")
bootbin.set_max_fst_len(fst_size) self.__bootbin.set_fst_max_len(fstbin_len)
(sys_path / "boot.bin").write_bytes(bootbin.data()) (sys_path / "boot.bin").write_bytes(self.__bootbin.data())
def __get_sys_from_folder(self, folder_path:Path): def __get_sys_from_folder(self, folder_path:Path):
""" """
Load system files from an unpacked GCM/iso folder and returns informations for the stats command. Load system files from an unpacked GCM/iso folder and returns informations for the stats command.
input: folder_path = Path input: folder_path = Path
return (BootBin, apploader_size:int, dol_len:int, fstbin_data:bytes) return (dol_len:int, fstbin_data:bytes)
load __bootbin, __bi2bin, __apploaderimg
""" """
sys_path = folder_path / "sys" sys_path = folder_path / "sys"
bootbin = BootBin((sys_path / "boot.bin").read_bytes()) self.__bootbin = BootBin((sys_path / "boot.bin").read_bytes())
apploader_size = (sys_path / "apploader.img").stat().st_size self.__bi2bin = Bi2Bin((sys_path / "bi2.bin").read_bytes())
self.__apploaderimg = ApploaderImg((sys_path / "apploader.img").read_bytes())
dol_len = (sys_path / "boot.dol").stat().st_size dol_len = (sys_path / "boot.dol").stat().st_size
fstbin_data = (sys_path / "fst.bin").read_bytes() fstbin_data = (sys_path / "fst.bin").read_bytes()
return (bootbin, apploader_size, dol_len, fstbin_data) return (dol_len, fstbin_data)
def __get_sys_from_file(self, file_path:Path): def __get_sys_from_file(self, file_path:Path):
""" """
Load system files from a GCM/iso file and returns informations for the stats command. Load system files from a GCM/iso file and returns informations for the stats command.
input: folder_path = Path input: folder_path = Path
return (BootBin, apploader_size:int, dol_len:int, fstbin_data:bytes) return (dol_len:int, fstbin_data:bytes)
load __bootbin, __bi2bin, __apploaderimg
""" """
bootbin = None
apploader_size = None
dol_len = None dol_len = None
fstbin_data = None fstbin_data = None
with file_path.open("rb") as iso_file: with file_path.open("rb") as iso_file:
bootbin = BootBin(iso_file.read(BootBin.LEN)) self.__bootbin = BootBin(iso_file.read(BootBin.LEN))
iso_file.seek(Gcm.APPLOADERSIZE_OFFSET) self.__bi2bin = Bi2Bin(iso_file.read(Bi2Bin.LEN))
apploader_size = Gcm.APPLOADER_HEADER_LEN + int.from_bytes(iso_file.read(4), "big", signed=False) + int.from_bytes(iso_file.read(4), "big", signed=False)
iso_file.seek(Gcm.APPLOADERLEN_OFFSET)
apploader_size = Gcm.APPLOADER_HEADER_LEN + int.from_bytes(iso_file.read(4), "big") + int.from_bytes(iso_file.read(4), "big")
iso_file.seek(Gcm.APPLOADER_OFFSET)
self.__apploaderimg = ApploaderImg(iso_file.read(apploader_size))
dol = Dol() dol = Dol()
iso_file.seek( bootbin.dol_offset() ) iso_file.seek( self.__bootbin.dol_offset() )
dol_len = dol.get_dol_len( iso_file.read(Dol.HEADER_LEN) ) dol_len = dol.get_dol_len( iso_file.read(Dol.HEADER_LEN) )
iso_file.seek( bootbin.fstbin_offset() ) iso_file.seek( self.__bootbin.fst_offset() )
fstbin_data = iso_file.read(bootbin.fstbin_len()) fstbin_data = iso_file.read(self.__bootbin.fst_len())
return (bootbin, apploader_size, dol_len, fstbin_data) return (dol_len, fstbin_data)
def stats(self, path:Path, align:int = 4): def stats(self, path:Path, align:int = 4):
""" """
Print SYS files informations, global memory mapping, empty spaces inside the GCM/iso Print SYS files informations, global memory mapping, empty spaces inside the GCM/iso
@ -614,7 +957,39 @@ class Gcm:
* path = Path (folder or iso/GCM file) * path = Path (folder or iso/GCM file)
* align = int * align = int
""" """
(bootbin, apploader_size, dol_len, fstbin_data) = self.__get_sys_from_folder(path) if path.is_dir() else self.__get_sys_from_file(path) (dol_len, fstbin_data) = self.__get_sys_from_folder(path) if path.is_dir() else self.__get_sys_from_file(path)
global_stats = f"# Stats for \"{path}\":\n\n" + \
"[boot.bin]\n" + \
f"GameCode = {self.__bootbin.game_code()}\n" + \
f"MakerCode = {self.__bootbin.maker_code()}\n" + \
f"DiskNumber = {self.__bootbin.disk_number()}\n" + \
f"GameVersion = {self.__bootbin.game_version()}\n" + \
f"AudioStreaming = {self.__bootbin.audio_streaming()}\n" + \
f"StreamBufferSize = {self.__bootbin.stream_buffer_size()}\n" + \
f"DVDMagic = 0x{self.__bootbin.dvd_magic().hex()}\n" + \
f"GameName = {self.__bootbin.game_name()}\n" + \
f"DolOffset = 0x{self.__bootbin.dol_offset():x}\n" + \
f"FstOffset = 0x{self.__bootbin.fst_offset():x}\n" + \
f"FstLen = 0x{self.__bootbin.fst_len():x}\n" + \
f"FstMaxLen = 0x{self.__bootbin.fst_max_len():x}\n\n" + \
"[bi2.bin]\n" + \
f"DebugMonitorSize = 0x{self.__bi2bin.debug_monitor_size():x}\n" + \
f"SimulatedMemorySize = 0x{self.__bi2bin.simulated_memory_size():x}\n" + \
f"ArgumentOffset = 0x{self.__bi2bin.argument_offset():x}\n" + \
f"DebugFlag = {self.__bi2bin.debug_flag()}\n" + \
f"TrackLocation = 0x{self.__bi2bin.track_location():x}\n" + \
f"TrackSize = 0x{self.__bi2bin.track_size():x}\n" + \
f"CountryCode = {self.__bi2bin.country_code()}\n" + \
f"TotalDisk = {self.__bi2bin.total_disk()}\n" + \
f"LongFileNameSupport = {self.__bi2bin.long_file_name_support()}\n\n" + \
"[apploader.img]\n" + \
f"Version = {self.__apploaderimg.version()}\n" + \
f"EntryPoint = 0x{self.__apploaderimg.entry_point():x}\n" + \
f"Size = 0x{self.__apploaderimg.size():x}\n" + \
f"TrailerSize = 0x{self.__apploaderimg.trailer_size():x}\n"
print(global_stats)
class MemoryObject: class MemoryObject:
def __init__(self, name:str, beg_offset:int, length:int): def __init__(self, name:str, beg_offset:int, length:int):
@ -627,16 +1002,16 @@ class Gcm:
mem_obj_list = [ mem_obj_list = [
MemoryObject("boot.bin", 0, BootBin.LEN), MemoryObject("boot.bin", 0, BootBin.LEN),
MemoryObject("bi2.bin", 0x440, Gcm.BI2BIN_LEN), MemoryObject("bi2.bin", 0x440, Bi2Bin.LEN),
MemoryObject("apploader.img", Gcm.APPLOADER_OFFSET, apploader_size), MemoryObject("apploader.img", Gcm.APPLOADER_OFFSET, self.__apploaderimg.len()),
MemoryObject("fst.bin", bootbin.fstbin_offset(), bootbin.fstbin_len()), MemoryObject("fst.bin", self.__bootbin.fst_offset(), self.__bootbin.fst_len()),
MemoryObject("boot.dol", bootbin.dol_offset(), dol_len)] MemoryObject("boot.dol", self.__bootbin.dol_offset(), dol_len)]
dir_id_path = {0: Path(".")} dir_id_path = {0: Path(".")}
currentdir_path = Path(".") currentdir_path = Path(".")
# root: id=0 so nextdir is the end # root: id=0 so nextdir is the end
nextdir = int.from_bytes(fstbin_data[8:12], "big", signed=False) nextdir = int.from_bytes(fstbin_data[8:12], "big")
# offset of filenames block # offset of filenames block
base_names = nextdir * 12 base_names = nextdir * 12
# go to parent when id reach next dir # go to parent when id reach next dir
@ -644,23 +1019,23 @@ class Gcm:
for id in range(1, base_names // 12): for id in range(1, base_names // 12):
i = id * 12 i = id * 12
file_type = int.from_bytes(fstbin_data[i:i+1], "big", signed=False) file_type = int.from_bytes(fstbin_data[i:i+1], "big")
name = fstbin_data[base_names + int.from_bytes(fstbin_data[i+1:i+4], "big", signed=False):].split(b"\x00")[0].decode("utf-8") name = fstbin_data[base_names + int.from_bytes(fstbin_data[i+1:i+4], "big"):].split(b"\x00")[0].decode("utf-8")
while id == nextdir_arr[-1]: while id == nextdir_arr[-1]:
currentdir_path = currentdir_path.parent currentdir_path = currentdir_path.parent
nextdir_arr.pop() nextdir_arr.pop()
if file_type == FstTree.TYPE_DIR: if file_type == FstTree.TYPE_DIR:
nextdir = int.from_bytes(fstbin_data[i+8:i+12], "big", signed=False) nextdir = int.from_bytes(fstbin_data[i+8:i+12], "big")
parentdir = int.from_bytes(fstbin_data[i+4:i+8], "big", signed=False) parentdir = int.from_bytes(fstbin_data[i+4:i+8], "big")
nextdir_arr.append( nextdir ) nextdir_arr.append( nextdir )
currentdir_path = dir_id_path[parentdir] / name currentdir_path = dir_id_path[parentdir] / name
dir_id_path[id] = currentdir_path dir_id_path[id] = currentdir_path
else: else:
fileoffset = int.from_bytes(fstbin_data[i+4:i+8], "big", signed=False) fileoffset = int.from_bytes(fstbin_data[i+4:i+8], "big")
filesize = int.from_bytes(fstbin_data[i+8:i+12], "big", signed=False) filesize = int.from_bytes(fstbin_data[i+8:i+12], "big")
mem_obj_list.append( MemoryObject(str(currentdir_path / name), fileoffset, filesize) ) mem_obj_list.append( MemoryObject(str(currentdir_path / name), fileoffset, filesize) )
mem_obj_list.sort(key=lambda x: x.beg_offset) mem_obj_list.sort(key=lambda x: x.beg_offset)
@ -675,7 +1050,7 @@ class Gcm:
elif last_aligned > mem_obj.beg_offset: elif last_aligned > mem_obj.beg_offset:
collision_list += [last_mem_obj, mem_obj] collision_list += [last_mem_obj, mem_obj]
last_mem_obj = mem_obj last_mem_obj = mem_obj
print(f"# Stats for \"{path}\":")
self.__print("Global memory mapping:", mem_obj_list) self.__print("Global memory mapping:", mem_obj_list)
if empty_space_list: if empty_space_list:
self.__print(f"Empty spaces (align={align}):", empty_space_list) self.__print(f"Empty spaces (align={align}):", empty_space_list)
@ -725,10 +1100,10 @@ def get_argparser():
group = parser.add_mutually_exclusive_group(required=True) group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('-p', '--pack', action='store_true', help="-p source_folder (dest_file.iso): Pack source_folder in new file source_folder.iso or dest_file.iso if specified.") group.add_argument('-p', '--pack', action='store_true', help="-p source_folder (dest_file.iso): Pack source_folder in new file source_folder.iso or dest_file.iso if specified.")
group.add_argument('-u', '--unpack', action='store_true', help="-u source_iso.iso (dest_folder): Unpack the GCM/ISO in new folder source_iso or dest_folder if specified.") group.add_argument('-u', '--unpack', action='store_true', help="-u source_iso.iso (dest_folder): Unpack the GCM/ISO in new folder source_iso or dest_folder if specified.")
group.add_argument('-s', '--stats', action='store_true', help="-s source_iso.iso or source_folder: Get stats about GCM, FST, memory, lengths and offsets.") group.add_argument('-s', '--stats', action='store_true', help="-s source_iso.iso or source_folder (-a 4): Get stats about GCM, FST, memory, lengths and offsets.")
group.add_argument('-r', '--rebuild-fst', action='store_true', help="-r game_folder: Rebuild the game_folder/sys/fst.bin using files in game_folder/root.") group.add_argument('-r', '--rebuild-fst', action='store_true', help="-r game_folder (-a 4): Rebuild the game_folder/sys/fst.bin using files in game_folder/root. For ADPCM (...) use 0x8000 align.")
group.add_argument('-ur', '--unpack-rebuild-fst', action='store_true', help="-ur source_iso.iso (dest_folder): Unpack and rebuild the FST.") group.add_argument('-ur', '--unpack-rebuild-fst', action='store_true', help="-ur source_iso.iso (dest_folder) (-a 4): Unpack and rebuild the FST.")
group.add_argument('-rp', '--rebuild-fst-pack', action='store_true', help="-rp source_folder (dest_file.iso): Rebuild the FST and pack.") group.add_argument('-rp', '--rebuild-fst-pack', action='store_true', help="-rp source_folder (dest_file.iso) (-a 4): Rebuild the FST and pack.")
return parser return parser