diff --git a/gcmtool.py b/gcmtool.py index a520c82..7489ada 100644 --- a/gcmtool.py +++ b/gcmtool.py @@ -3,7 +3,7 @@ from pathlib import Path import logging -__version__ = "0.0.4" +__version__ = "0.0.5" __author__ = "rigodron, algoflash, GGLinnk" __license__ = "MIT" __status__ = "developpement" @@ -12,9 +12,15 @@ __status__ = "developpement" DVD_MAGIC = b"\xC2\x33\x9F\x3D" FST_TYPE_FILE = 0 FST_TYPE_DIR = 1 -MIN_FILEOFFSET_ENDDOL = "ENDDOL" -MIN_FILEOFFSET_FSTOFFSET = "FSTOFFSET" +BOOTBIN_LEN = 0x440 +BOOTBIN_DOLOFFSET_OFFSET = 0x420 +BOOTBIN_FSTOFFSET_OFFSET = 0x424 +BOOTBIN_FSTLEN_OFFSET = 0x428 +BI2BIN_LEN = 0x2000 +DOL_HEADER_LEN = 0x100 +ISO_APPLOADER_OFFSET = 0x2440 +ISO_APPLOADERSIZE_OFFSET = 0x2454 ###################################################################### # Todo : add extension check ; add --disable-ignore # -> test it on random iso and check that it's the same than dolphin extract @@ -87,16 +93,14 @@ class FstTree: __fst_block = None __name_block = None __nameblock_length = None # Used to find min file_offset when fst is at the end of the iso beginning - __min_file_offset_type = None - def __init__(self, root_path:Path, min_file_offset:tuple, align:int = 4): + def __init__(self, root_path:Path, fst_offset:int, align:int = 4): self.__root_path_length = len(root_path.parts) self.__root_node = Folder(root_path.name, None) self.__align = align self.__name_block = b"" self.__fst_block = b"" - self.__min_file_offset_type = min_file_offset[0] self.__nameblock_length = 0 - self.__current_file_offset = min_file_offset[1] + self.__current_file_offset = fst_offset def __str__(self): return self.__to_str(self.__root_node) def __to_str(self, node:Node, depth=0): @@ -105,12 +109,13 @@ class FstTree: for child in node.childs(): result += self.__to_str(child, depth+1) return result + # Needed to know where we can begin to write files def __get_fst_length(self): self.__generate_nameblock_length() - min_fileoffset = self.count_childs(self.__root_node)*12 + 12 + self.__nameblock_length - if min_fileoffset % self.__align != 0: - min_fileoffset += self.__align - (min_fileoffset % self.__align) - return min_fileoffset + fst_len = self.__count_childs(self.__root_node)*12 + 12 + self.__nameblock_length + if fst_len % self.__align != 0: + fst_len += self.__align - (fst_len % self.__align) + return fst_len def __generate_nameblock_length(self, node:Node = None): if node == None: node = self.__root_node @@ -130,7 +135,7 @@ class FstTree: else: node = Folder(node_path.name, parent) parent.add_child(node) - def prepare(self, node:Node = None): + def __prepare(self, node:Node = None): offset_name = 0 if node == None: node = self.__root_node @@ -142,13 +147,13 @@ class FstTree: self.__current_index += 1 if node.type() == FST_TYPE_DIR: - node.set_next_dir(self.__current_index + self.count_childs(node)) + node.set_next_dir(self.__current_index + self.__count_childs(node)) if node == self.__root_node: self.__fst_block = b"\x01\x00\x00\x00\x00\x00\x00\x00" + node.next_dir().to_bytes(4, "big") else: self.__fst_block += node.format() for child in node.childs(): - self.prepare(child) + self.__prepare(child) else: node.set_offset(self.__current_file_offset) self.__fst_block += node.format() @@ -156,22 +161,21 @@ class FstTree: if self.__align != 0 and self.__current_file_offset % self.__align != 0: self.__current_file_offset += self.__align - (self.__current_file_offset % self.__align) def get_fst(self): - if self.__min_file_offset_type == MIN_FILEOFFSET_FSTOFFSET: - self.__current_file_offset += self.__get_fst_length() - self.prepare() + self.__current_file_offset += self.__get_fst_length() + self.__prepare() return self.__fst_block + self.__name_block - def count_childs(self, node:Folder): + def __count_childs(self, node:Folder): count = 0 for child in node.childs(): if child.type() == FST_TYPE_DIR: - count += self.count_childs(child) + count += self.__count_childs(child) return count + len(node.childs()) class Dol: - # Get total length using the sum of the 18 sections length and dol header length (0x100) - def getDolLen(self, dolheader_data:bytes): - dol_len = 0x100 + # Get total length using the sum of the 18 sections length and dol header length + def get_dol_len(self, dolheader_data:bytes): + dol_len = DOL_HEADER_LEN for i in range(18): dol_len += int.from_bytes(dolheader_data[0x90+i*4:0x90+(i+1)*4], "big", signed=False) return dol_len @@ -179,47 +183,36 @@ class Dol: # https://sudonull.com/post/68549-Gamecube-file-system-device class GCM: - def __get_min_file_offset(self, sys_path:Path): - bootbin_path = sys_path / "boot.bin" - with bootbin_path.open("rb") as bootbin_file: - # We find offset of fst.bin - bootbin_file.seek(0x420) - dol_end = int.from_bytes(bootbin_file.read(4),"big", signed=False) + (sys_path / "boot.dol").stat().st_size - fst_offset = int.from_bytes(bootbin_file.read(4), "big", signed=False) - if dol_end > fst_offset: - logging.warning("boot.dol is after fst.bin ! This mean that boot.bin has to be patched if fst erase the beginning of the dol.") - return (MIN_FILEOFFSET_ENDDOL, dol_end) - return (MIN_FILEOFFSET_FSTOFFSET, fst_offset) def unpack(self, iso_path:Path, folder_path:Path): with iso_path.open("rb") as iso_file: - bootbin_data = iso_file.read(0x440) + bootbin_data = iso_file.read(BOOTBIN_LEN) if bootbin_data[0x1c:0x20] != DVD_MAGIC: raise Exception("Invalid DVD format - this tool is for ISO/GCM files") - bi2bin_data = iso_file.read(0x2000) + bi2bin_data = iso_file.read(BI2BIN_LEN) # https://www.gc-forever.com/wiki/index.php?title=Apploader # -> Full apploader size is sum of size and trailerSize, rounded up to 32 bytes. - iso_file.seek(0x2454) + iso_file.seek(ISO_APPLOADERSIZE_OFFSET) size = int.from_bytes(iso_file.read(4), "big", signed=False) trailerSize = int.from_bytes(iso_file.read(4), "big", signed=False) # Dolphin Emulator add 32 Null bytes at the end of the extracted apploader.img apploader_size = size + trailerSize + 32 - iso_file.seek(0x2440) + iso_file.seek(ISO_APPLOADER_OFFSET) apploaderimg_data = iso_file.read(apploader_size) - fstbin_offset = int.from_bytes(bootbin_data[0x424:0x428],"big", signed=False) - fstbin_len = int.from_bytes(bootbin_data[0x428:0x42c],"big", signed=False) + fstbin_offset = int.from_bytes(bootbin_data[BOOTBIN_FSTOFFSET_OFFSET:BOOTBIN_FSTOFFSET_OFFSET+4],"big", signed=False) + fstbin_len = int.from_bytes(bootbin_data[BOOTBIN_FSTLEN_OFFSET:BOOTBIN_FSTLEN_OFFSET+4],"big", signed=False) iso_file.seek( fstbin_offset ) fstbin_data = iso_file.read( fstbin_len ) - dol_offset = int.from_bytes(bootbin_data[0x420:0x424],"big", signed=False) + dol_offset = int.from_bytes(bootbin_data[BOOTBIN_DOLOFFSET_OFFSET:BOOTBIN_DOLOFFSET_OFFSET+4],"big", signed=False) iso_file.seek( dol_offset ) dol = Dol() - dolheader_data = iso_file.read(0x100) - dol_len = dol.getDolLen( dolheader_data ) - bootdol_data = dolheader_data + iso_file.read( dol_len - 0x100 ) + dolheader_data = iso_file.read(DOL_HEADER_LEN) + dol_len = dol.get_dol_len( dolheader_data ) + bootdol_data = dolheader_data + iso_file.read( dol_len - DOL_HEADER_LEN ) if folder_path != Path("."): base_path = folder_path else: @@ -234,11 +227,11 @@ class GCM: (sys_path / "fst.bin").open("wb") as fstbin_file, \ (sys_path / "apploader.img").open("wb") as apploaderimg_file,\ (sys_path / "boot.dol").open("wb") as bootdol_file: - logging.debug(f"{iso_path}(0x0:0x440) -> {sys_path / 'boot.bin'}") + logging.debug(f"{iso_path}(0x0:0x{BOOTBIN_LEN:x}) -> {sys_path / 'boot.bin'}") bootbin_file.write(bootbin_data) - logging.debug(f"{iso_path}(0x440:0x2440) -> {sys_path / 'bi2.bin'}") + logging.debug(f"{iso_path}(0x440:0x{ISO_APPLOADER_OFFSET:x}) -> {sys_path / 'bi2.bin'}") bi2bin_file.write(bi2bin_data) - logging.debug(f"{iso_path}(0x2440:0x{0x2440 + apploader_size:x} -> {sys_path / 'apploader.img'}") + logging.debug(f"{iso_path}(0x{ISO_APPLOADER_OFFSET:x}:0x{ISO_APPLOADER_OFFSET + apploader_size:x} -> {sys_path / 'apploader.img'}") apploaderimg_file.write(apploaderimg_data) logging.debug(f"{iso_path}(0x{fstbin_offset:x}:0x{fstbin_offset + fstbin_len:x}) -> {sys_path / 'fst.bin'}") fstbin_file.write(fstbin_data) @@ -294,25 +287,25 @@ class GCM: (folder_path / "sys" / "apploader.img").open("rb") as apploaderimg_file,\ (folder_path / "sys" / "boot.dol").open("rb") as bootdol_file : - logging.debug(f"{folder_path / 'sys' / 'boot.bin'} -> {iso_path}(0x0:0x440)") - logging.debug(f"{folder_path / 'sys' / 'bi2.bin'} -> {iso_path}(0x440:0x2440)") - logging.debug(f"{folder_path / 'sys' / 'apploader.img'} -> {iso_path}(0x2440:0x{0x2440 + (folder_path / 'sys' / 'apploader.img').stat().st_size:x}") + logging.debug(f"{folder_path / 'sys' / 'boot.bin'} -> {iso_path}(0x0:0x{BOOTBIN_LEN:x})") + logging.debug(f"{folder_path / 'sys' / 'bi2.bin'} -> {iso_path}(0x{BOOTBIN_LEN:x}:0x{ISO_APPLOADER_OFFSET:x})") + logging.debug(f"{folder_path / 'sys' / 'apploader.img'} -> {iso_path}(0x{ISO_APPLOADER_OFFSET:x}:0x{ISO_APPLOADER_OFFSET + (folder_path / 'sys' / 'apploader.img').stat().st_size:x}") bootbin_data = bootbin_file.read() iso_file.write( bootbin_data ) iso_file.write(bi2bin_file.read()) iso_file.write(apploaderimg_file.read()) - fstbin_offset = int.from_bytes(bootbin_data[0x424:0x428],"big", signed=False) - fstbin_len = int.from_bytes(bootbin_data[0x428:0x42c],"big", signed=False) + fstbin_offset = int.from_bytes(bootbin_data[BOOTBIN_FSTOFFSET_OFFSET:BOOTBIN_FSTOFFSET_OFFSET+4],"big", signed=False) + fstbin_len = int.from_bytes(bootbin_data[BOOTBIN_FSTLEN_OFFSET:BOOTBIN_FSTLEN_OFFSET+4],"big", signed=False) if (folder_path / "sys" / "fst.bin").stat().st_size != fstbin_len: - raise Exception("Invalid fst.bin size in boot.bin offset 0x428:0x42c!") + raise Exception("Invalid fst.bin size in boot.bin offset 0x{BOOTBIN_FSTLEN_OFFSET:x}:0x{BOOTBIN_FSTLEN_OFFSET+4:x}!") logging.debug(f"{folder_path / 'sys' / 'fst.bin'} -> {iso_path}(0x{fstbin_offset:x}:0x{fstbin_offset + fstbin_len:x})") iso_file.seek( fstbin_offset ) fstbin_data = fstbin_file.read() iso_file.write( fstbin_data ) - dol_offset = int.from_bytes(bootbin_data[0x420:0x424],"big", signed=False) + dol_offset = int.from_bytes(bootbin_data[BOOTBIN_DOLOFFSET_OFFSET:BOOTBIN_DOLOFFSET_OFFSET+4],"big", signed=False) logging.debug(f"{folder_path / 'sys' / 'boot.dol'} -> {iso_path}(0x{dol_offset:x}:0x{dol_offset + (folder_path / 'sys' / 'boot.dol').stat().st_size:x})") iso_file.seek( dol_offset ) iso_file.write( bootdol_file.read() ) @@ -358,20 +351,35 @@ class GCM: def rebuild_fst(self, folder_path:Path, align:int): root_path = folder_path / "root" sys_path = folder_path / "sys" - fst_tree = FstTree(root_path, self.__get_min_file_offset(sys_path), align=align) + with (sys_path / "boot.bin").open("rb+") as bootbin_file: + dol_offset = ISO_APPLOADER_OFFSET + (sys_path / "apploader.img").stat().st_size + if dol_offset % align != 0: + dol_offset += align - (dol_offset % align) + logging.info(f"Patching sys/boot.bin offset 0x{BOOTBIN_DOLOFFSET_OFFSET:x} with new dol offset (0x{dol_offset:x})") + bootbin_file.seek(BOOTBIN_DOLOFFSET_OFFSET) + bootbin_file.write(dol_offset.to_bytes(4, "big")) + + fst_offset = dol_offset + (sys_path / "boot.dol").stat().st_size + if fst_offset % align != 0: + fst_offset += align - (fst_offset % align) + logging.info(f"Patching sys/boot.bin offset 0x{BOOTBIN_FSTOFFSET_OFFSET:x} with new fst offset (0x{fst_offset:x})") + bootbin_file.seek(BOOTBIN_FSTOFFSET_OFFSET) + bootbin_file.write(fst_offset.to_bytes(4, "big")) + + fst_tree = FstTree(root_path, fst_offset, align=align) - path_list = sorted([path for path in root_path.glob('**/*')], key=lambda s:Path(str(s).upper())) - for path in path_list: - fst_tree.add_node_by_path(path) - logging.debug(fst_tree) - fst_path = sys_path / "fst.bin" - with fst_path.open("wb") as fstbin_file: - logging.info("Writing fst in sys/fst.bin") - fstbin_file.write( fst_tree.get_fst() ) - with (folder_path / "sys" / "boot.bin").open("rb+") as bootbin_file: + # Sorting paths approach original fst sort but 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())) + for path in path_list: + fst_tree.add_node_by_path(path) + logging.debug(fst_tree) + fst_path = sys_path / "fst.bin" + with fst_path.open("wb") as fstbin_file: + logging.info("Writing fst in sys/fst.bin") + fstbin_file.write( fst_tree.get_fst() ) fst_size = fst_path.stat().st_size - logging.info(f"Patching sys/boot.bin offset 0x428 with new fst size ({fst_size})") - bootbin_file.seek(0x428) + logging.info(f"Patching sys/boot.bin offset 0x{BOOTBIN_FSTLEN_OFFSET:x} with new fst size (0x{fst_size:x})") + bootbin_file.seek(BOOTBIN_FSTLEN_OFFSET) bootbin_file.write(fst_size.to_bytes(4, "big"))