mirror of
https://github.com/Virtual-World-RE/NeoGF.git
synced 2024-11-15 11:45:33 +01:00
300 lines
12 KiB
Python
300 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
from afstool import AfsInvalidFileLenError, Afs, FilenameResolver
|
|
from math import floor, ceil
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
from time import time
|
|
|
|
__version__ = "0.0.3"
|
|
__author__ = "rigodron, algoflash, GGLinnk"
|
|
__license__ = "MIT"
|
|
__status__ = "developpement"
|
|
|
|
|
|
##################################################
|
|
# Set afss_path with your AFSs folder
|
|
# Set afspacker_path with the path of AFSPacker.exe
|
|
##################################################
|
|
afss_path = Path("afs")
|
|
afspacker_path = Path("../_autres/_soft/AFSPacker.exe")
|
|
|
|
afspacker_unpack_path = Path("afspacker_unpack")
|
|
# Created tmp paths
|
|
unpack_path = Path("unpack")
|
|
unpack2_path = Path("unpack2")
|
|
repack_path = Path("repack")
|
|
|
|
|
|
class AfsTest(Afs):
|
|
# return a list of tuples with (offset, resolved filename)
|
|
def get_range(self, folder_path:Path):
|
|
sys_path = folder_path / "sys"
|
|
self._Afs__loadsys_from_folder(sys_path)
|
|
resolver = FilenameResolver(sys_path)
|
|
|
|
offsets_names_map = [(0, "SYS TOC")]
|
|
for i in range(0, self._Afs__file_count):
|
|
filename = resolver.resolve_from_index(i, self._Afs__get_file_name(i)) if self._Afs__filenamedirectory else f"{i:08}"
|
|
offsets_names_map.append( (self._Afs__get_file_offset(i), filename) )
|
|
if self._Afs__filenamedirectory:
|
|
offsets_names_map.append( (self._Afs__get_filenamedirectory_offset(), "SYS FD") )
|
|
return offsets_names_map
|
|
|
|
|
|
def print_paths_differences(folder1_paths:list, folder2_paths:list):
|
|
backup_paths = folder2_paths.copy()
|
|
for path in folder1_paths:
|
|
if path in folder2_paths:
|
|
folder2_paths.remove(path)
|
|
for path in backup_paths:
|
|
if path in folder1_paths:
|
|
folder1_paths.remove(path)
|
|
print("folder1 diff :")
|
|
for path in folder1_paths:
|
|
print(path)
|
|
print("folder2 diff :")
|
|
for path in folder2_paths:
|
|
print(path)
|
|
|
|
|
|
# compare two files
|
|
def compare_files(file1_path:Path, file2_path:Path):
|
|
CLUSTER_LEN = 131072
|
|
with file1_path.open("rb") as file1, file2_path.open("rb") as file2:
|
|
# Init
|
|
bytes1 = file1.read(CLUSTER_LEN)
|
|
bytes2 = file2.read(CLUSTER_LEN)
|
|
while bytes1 or bytes2: # continue if bytes1 and bytes2 have a len > 0
|
|
if bytes1 != bytes2:
|
|
return False
|
|
bytes1 = file1.read(CLUSTER_LEN)
|
|
bytes2 = file2.read(CLUSTER_LEN)
|
|
return True
|
|
|
|
|
|
# compare two folder
|
|
# -> raise an exception if there is a difference in paths or in file content
|
|
def compare_folders(folder1: Path, folder2: Path, compare_mtime=False):
|
|
folder1_tmp_paths = list(folder1.glob("*"))
|
|
folder1_file_count = len(folder1_tmp_paths)
|
|
print(f"compare \"{folder1}\" - \"{folder2}\" ({folder1_file_count} files)")
|
|
if folder1_file_count == 0:
|
|
raise Exception(f"ERROR - EMPTY FOLDER: {folder1}")
|
|
|
|
len1 = len(folder1.parts)
|
|
len2 = len(folder2.parts)
|
|
# 1. Compare names in filesystems
|
|
folder1_paths = [Path(*path.parts[len1:]) for path in folder1_tmp_paths]
|
|
folder2_paths = [Path(*path.parts[len2:]) for path in folder2.glob("*")]
|
|
if folder1_paths != folder2_paths:
|
|
print_paths_differences(folder1_paths, folder2_paths)
|
|
raise Exception(f"Folders \"{folder1}\" and \"{folder2}\" are different (not the same folders or files names).")
|
|
# 2. Compare files content
|
|
for path1 in folder1_tmp_paths:
|
|
path2 = folder2 / Path(*path1.parts[len1:])
|
|
if compare_mtime:
|
|
# When using FD Date it floor to second
|
|
if round(path1.stat().st_mtime) != round(path2.stat().st_mtime):
|
|
raise Exception(f"\"{path1}\" and \"{path2}\" mtime (update time) are different:\n {round(path1.stat().st_mtime)}-{round(path2.stat().st_mtime)}")
|
|
if not compare_files(path1, path2):
|
|
raise Exception(f"\"{path1}\" and \"{path2}\" are different.")
|
|
|
|
|
|
# compare two AFS
|
|
# -> raise an exception if there is a difference in:
|
|
# -paths
|
|
# -files content
|
|
# -mtime if there is a filename directory
|
|
def compare_unpacked_AFS(folder1: Path, folder2: Path):
|
|
compare_mtime = False
|
|
if (folder1 / "sys" / "filenamedirectory.bin").is_file():
|
|
compare_mtime = True
|
|
compare_folders(folder1 / "root", folder2 / "root", compare_mtime)
|
|
|
|
|
|
def patch_all_bytes(file_path:Path, max_len:int = None):
|
|
file_data = bytearray(file_path.read_bytes())
|
|
if max_len == None:
|
|
max_len = len(file_data)
|
|
elif max_len < len(file_data):
|
|
file_data = file_data[:max_len]
|
|
for i in range(0, len(file_data)):
|
|
file_data[i] = (file_data[i] + 1) % 255
|
|
if max_len > len(file_data):
|
|
file_data.extend(b"\x01"*(max_len - len(file_data)))
|
|
file_path.write_bytes(file_data)
|
|
|
|
|
|
# if not bool_len: patch all files with max len
|
|
# if bool_len: patch first file found with max len + 1
|
|
def patch_unpackedfiles_in_folder(folder_path:Path, bool_len:bool = False):
|
|
for afsfolder_path in folder_path.glob("*"):
|
|
print(f"Patching {afsfolder_path}...")
|
|
afs_test = AfsTest()
|
|
offsets_names_map = afs_test.get_range(afsfolder_path)
|
|
|
|
for file_path in afsfolder_path.glob("root/*"):
|
|
max_len = None
|
|
# Search by resolved name and get begin offset of next file / SYS File
|
|
for i in range(0, len(offsets_names_map)):
|
|
if offsets_names_map[i][1] == file_path.name:
|
|
if i+1 < len(offsets_names_map):
|
|
max_len = offsets_names_map[i+1][0] - offsets_names_map[i][0]
|
|
if bool_len:
|
|
max_len += 1
|
|
# else there is no limit because last file
|
|
else:
|
|
max_len = file_path.stat().st_size + Afs.ALIGN
|
|
break
|
|
patch_all_bytes(file_path, max_len)
|
|
if bool_len:
|
|
break
|
|
|
|
|
|
##################################################
|
|
# afstool.py commands wrappers
|
|
##################################################
|
|
def afspacker_extract(afs_path:Path, folder_path:Path):
|
|
print(f"AFSPacker.exe : Extracting \"{afs_path}\" in \"{folder_path}\"")
|
|
if os.system(f"{afspacker_path} -e \"{afs_path}\" \"{folder_path}\" > NUL") != 0:
|
|
raise Exception("Error while unpacking with AFSPacker.exe")
|
|
def afstool_pack(folder_path:Path, afs_path:Path):
|
|
if os.system(f"python afstool.py -p \"{folder_path}\" \"{afs_path}\"") != 0:
|
|
raise Exception("Error while (re)packing.")
|
|
def afstool_unpack(afs_path:Path, folder_path:Path):
|
|
if os.system(f"python afstool.py -u \"{afs_path}\" \"{folder_path}\"") != 0:
|
|
raise Exception("Error while unpacking.")
|
|
|
|
|
|
def repack_unpack2_compare():
|
|
repack_path.mkdir()
|
|
unpack2_path.mkdir()
|
|
unpack_paths = list(unpack_path.glob("*"))
|
|
|
|
# repack unpack_path repack_path
|
|
for folder_path in unpack_paths:
|
|
afstool_pack(folder_path, repack_path / Path(folder_path.stem).with_suffix('.afs'))
|
|
|
|
# unpack repack_path unpack2_path
|
|
for afs_path in repack_path.glob("*"):
|
|
afstool_unpack(afs_path, unpack2_path / afs_path.stem)
|
|
|
|
shutil.rmtree(repack_path)
|
|
|
|
# compare unpack_path unpack2_path
|
|
for folder_path in unpack_paths:
|
|
compare_unpacked_AFS(folder_path, unpack2_path / folder_path.name)
|
|
|
|
shutil.rmtree(unpack2_path)
|
|
|
|
|
|
TEST_COUNT = 6
|
|
|
|
start = time()
|
|
print("###############################################################################")
|
|
print("# Checking tests folder")
|
|
print("###############################################################################")
|
|
# Check if tests folders exist
|
|
if unpack_path.is_dir() or unpack2_path.is_dir() or repack_path.is_dir():
|
|
raise Exception(f"Error - Please remove:\n-{unpack_path}\n-{unpack2_path}\n-{repack_path}")
|
|
|
|
print("###############################################################################")
|
|
print(f"# TEST 1/{TEST_COUNT}")
|
|
print("# Comparing afss_path->unpack->[unpack_path] AFS with [afspacker_unpack_path].")
|
|
print("###############################################################################")
|
|
|
|
unpack_path.mkdir()
|
|
afss_paths = list(afss_path.glob("*"))
|
|
|
|
if not afspacker_unpack_path.is_dir():
|
|
afspacker_unpack_path.mkdir()
|
|
# AFSPacker.exe unpack afss_path in afspacker_unpack_path
|
|
for afs_path in afss_paths:
|
|
afspacker_extract(afs_path, afspacker_unpack_path / afs_path.stem)
|
|
|
|
# unpack afss_path unpack_path
|
|
for afs_path in afss_paths:
|
|
afstool_unpack(afs_path, unpack_path / afs_path.stem)
|
|
|
|
# compare unpack_path afspacker_unpack_path
|
|
# AFSPacker don't store the date present in the filename directory in the file metadatas and update every dates when packing
|
|
for folder_path in unpack_path.glob("*"):
|
|
compare_folders(folder_path / "root", afspacker_unpack_path / folder_path.stem)
|
|
|
|
print("###############################################################################")
|
|
print(f"# TEST 2/{TEST_COUNT}")
|
|
print("# Comparing unpack_path->pack->[repack_path] AFS with [afss_path].")
|
|
print("###############################################################################")
|
|
repack_path.mkdir()
|
|
# repack unpack_path repack_path
|
|
for folder_path in unpack_path.glob("*"):
|
|
afstool_pack(folder_path, repack_path / Path(folder_path.stem).with_suffix('.afs'))
|
|
|
|
# compare repack_path afss_path
|
|
compare_folders(afss_path, repack_path)
|
|
|
|
shutil.rmtree(repack_path)
|
|
|
|
print("###############################################################################")
|
|
print(f"# TEST 3/{TEST_COUNT}")
|
|
print("# Comparing [unpack_path]->patch->pack->unpack->[unpack2_path].")
|
|
print("###############################################################################")
|
|
# Patch unpack files whithout changing their len
|
|
for folder_path in unpack_path.glob("*"):
|
|
print(f"Patching {folder_path}...")
|
|
for file_path in folder_path.glob("root/*"):
|
|
patch_all_bytes(file_path)
|
|
|
|
repack_unpack2_compare()
|
|
|
|
print("###############################################################################")
|
|
print(f"# TEST 4/{TEST_COUNT}")
|
|
print("# Comparing [unpack_path]->patch(max_size)->pack->unpack->[unpack2_path].")
|
|
print("###############################################################################")
|
|
# Patch unpack files changing len to max
|
|
patch_unpackedfiles_in_folder(unpack_path)
|
|
|
|
repack_unpack2_compare()
|
|
|
|
print("###############################################################################")
|
|
print(f"# TEST 5/{TEST_COUNT}")
|
|
print("# Testing exception unpack_path->patch(max_size+1)->[pack]->repack_path.")
|
|
print("###############################################################################")
|
|
# Patch unpack files with 1 byte in a new used block in the first file
|
|
patch_unpackedfiles_in_folder(unpack_path, True)
|
|
|
|
repack_path.mkdir()
|
|
|
|
# repack unpack_path repack_path
|
|
for folder_path in unpack_path.glob("*"):
|
|
try:
|
|
afs = Afs()
|
|
afs.pack(folder_path, repack_path / Path(folder_path.stem).with_suffix('.afs'))
|
|
raise Exception(f"Error - Invalid file len check. Must raise an exception.")
|
|
except AfsInvalidFileLenError:
|
|
print(f"Correct AfsInvalidFileLenError - {folder_path}")
|
|
|
|
shutil.rmtree(repack_path)
|
|
|
|
print("###############################################################################")
|
|
print(f"# TEST 6/{TEST_COUNT}")
|
|
print("# Comparing [unpack_path]->patch(blocks - 1)->pack->unpack->[unpack2_path].")
|
|
print("###############################################################################")
|
|
# Patch unpack files with 1 block less
|
|
for file_path in unpack_path.glob("*/root/*"):
|
|
patch_all_bytes(file_path, file_path.stat().st_size - Afs.ALIGN)
|
|
|
|
repack_unpack2_compare()
|
|
|
|
print("###############################################################################")
|
|
print("# Cleaning test folders.")
|
|
print("###############################################################################")
|
|
# Remove tests folders
|
|
shutil.rmtree(unpack_path)
|
|
|
|
end = time()
|
|
print("###############################################################################")
|
|
print(f"# All tests are OK - elapsed time : {end - start}")
|
|
print("###############################################################################")
|