import re, os, shutil, stat, subprocess
TACTLIB_GITHUB_REPOSITORY = "https://github.com/overtools/TACTLib"
TACTLIB_ROOT_SUBDIRECTORY = "./TACTLib"
TACTLIB_CMF_SUBDIRECTORY = os.path.join(TACTLIB_ROOT_SUBDIRECTORY, "TACTLib/Core/Product/Tank/CMF")
CPP_BLOCK_INDENT = " "
def create_file_backup(file_name):
try:
file_name_backup = file_name + ".bak"
backup_index = 1
while os.path.isfile(file_name_backup):
file_name_backup = file_name + ".b%02u" % backup_index
backup_index += 1
shutil.copyfile(file_name, file_name_backup)
except Exception as e:
return False
return True
def delete_directory(folder_name):
try:
for fs_item in os.listdir(folder_name):
full_path = os.path.join(folder_name, fs_item)
if os.path.isdir(full_path):
delete_directory(full_path)
else:
os.chmod(full_path, stat.S_IWRITE)
os.remove(full_path)
os.rmdir(folder_name)
except Exception as e:
pass
def get_file_build_number(plain_name):
try:
string_index1 = plain_name.find("_")
if string_index1 == -1:
return 0
string_index1 += 1
string_index2 = plain_name.find(".")
if string_index2 < string_index1:
return 0
return int(plain_name[string_index1:string_index2])
except:
pass
return 0
def load_build_number_list(folder_name):
build_number_list = []
for plain_name in os.listdir(folder_name):
build_number = get_file_build_number(plain_name)
if not build_number:
continue
build_number_list.append(build_number)
build_number_list.sort()
return build_number_list
def append_token(line_buffer, token):
if len(line_buffer):
line_buffer = line_buffer + " "
return line_buffer + token
def flush_line(cmf_cpp, line_buffer, closing_token, nest_level, nest_increment):
indent_line = CPP_BLOCK_INDENT * nest_level
if len(line_buffer) != 0:
cmf_cpp.append(indent_line + line_buffer)
line_buffer = ""
if nest_increment == +1:
cmf_cpp.append(indent_line + closing_token)
return "", nest_level + 1
if nest_increment == -1:
indent_line = CPP_BLOCK_INDENT * (nest_level - 1)
cmf_cpp.append(indent_line + closing_token)
return "", nest_level - 1
return "", nest_level
def append_hex_array(cmf_cpp, header_line, key_table_tokens, nest_level):
inside_block = False
indent_line = CPP_BLOCK_INDENT * nest_level
line_buffer = ""
hexa_values = 0
cmf_cpp.append(indent_line + header_line)
for token in key_table_tokens:
if token == "":
continue
if token == "{" and inside_block == False:
line_buffer, nest_level = flush_line(cmf_cpp, line_buffer, "{", nest_level, +1)
inside_block = True
hexa_values = 0
continue
if token == "};" and inside_block == True:
line_buffer, nest_level = flush_line(cmf_cpp, line_buffer, "};", nest_level, -1)
inside_block = False
break
if token.startswith("0x") or token.startswith("0X"):
if hexa_values >= 16:
line_buffer, nest_level = flush_line(cmf_cpp, line_buffer, None, nest_level, 0)
hexa_values = 0
line_buffer = append_token(line_buffer, token)
hexa_values += 1
continue
print("[x] Unexpected token: " + token)
assert len(line_buffer) == 0, f"Unexpected remained in the single line: {line_buffer}"
assert nest_level == 1, f"Unexpected nest level: {nest_level}"
return
def append_cpp_function(cmf_cpp, header_line, key_table_tokens, nest_level):
indent_line = CPP_BLOCK_INDENT * nest_level
skipping_buffer_allocation = True
save_nest_level = nest_level
skipping_definition = True
inside_for_header = False
inside_case_label = False
line_buffer = ""
cmf_cpp.append(indent_line + header_line)
for token in key_table_tokens:
if token == "":
continue
if skipping_definition:
if token.endswith(")"):
skipping_definition = False
continue
if token == "{":
line_buffer, nest_level = flush_line(cmf_cpp, line_buffer, "{", nest_level, +1)
continue
if token == "}":
line_buffer, nest_level = flush_line(cmf_cpp, line_buffer, "}", nest_level, -1)
if nest_level == save_nest_level:
break
continue
if skipping_buffer_allocation:
if len(line_buffer) == 0:
line_buffer = append_token(line_buffer, "//")
line_buffer = append_token(line_buffer, token)
if token.endswith(";"):
line_buffer, nest_level = flush_line(cmf_cpp, line_buffer, None, nest_level, 0)
skipping_buffer_allocation = False
continue
line_buffer = append_token(line_buffer, token)
if token == "case":
inside_case_label = True
if inside_case_label and token.endswith(":"):
line_buffer, nest_level = flush_line(cmf_cpp, line_buffer, "", nest_level, +1)
continue
if inside_case_label and token == "break;":
line_buffer, nest_level = flush_line(cmf_cpp, line_buffer, "", nest_level, -1)
continue
if token == "for" or token.startswith("for("):
inside_for_header = True
cmf_cpp.append("")
if inside_for_header and token.endswith(")"):
line_buffer, nest_level = flush_line(cmf_cpp, line_buffer, None, nest_level, 0)
inside_for_header = False
continue
if token.endswith(";"):
if inside_for_header == False:
line_buffer, nest_level = flush_line(cmf_cpp, line_buffer, None, nest_level, 0)
continue
return
def build_cmf_cpp(file_content_cs, key_table, key_function, iv_function, build_number):
cmf_cpp = []
cmf_cpp.append("//")
cmf_cpp.append("// Key+IV provider for build %u. Created automatically, DO NOT EDIT." % build_number)
cmf_cpp.append("// Source: .\TACTLib\TACTLib\Core\Product\Tank\CMF\ProCMF_%s.cs" % build_number)
cmf_cpp.append("//\n")
cmf_cpp.append("namespace KeyCMF_%06u" % build_number)
cmf_cpp.append("{")
key_table_tokens = re.split(r"\s+", file_content_cs[key_table.end():])
append_hex_array(cmf_cpp, "static const BYTE Keytable[] =", key_table_tokens, 1)
cmf_cpp.append("")
key_table_tokens = re.split(r"\s+", file_content_cs[key_function.end():])
append_cpp_function(cmf_cpp, "LPBYTE Key(const CASC_CMF_HEADER & header, LPBYTE buffer, int length)", key_table_tokens, 1)
cmf_cpp.append("")
iv_table_tokens = re.split(r"\s+", file_content_cs[iv_function.end():])
append_cpp_function(cmf_cpp, "LPBYTE IV(const CASC_CMF_HEADER & header, LPBYTE digest, LPBYTE buffer, int length)", iv_table_tokens, 1)
cmf_cpp.append("}")
return cmf_cpp
def convert_cs_to_cpp_cmf(source_file, target_file, build_number):
try:
file_content = None
with open(source_file, "rt") as f:
file_content_cs = f.read()
except Exception as e:
return False
try:
search_regexp = r"private\s+static\s+readonly\s+byte\s*\[\] +Keytable +="
key_table = re.search(search_regexp, file_content_cs, re.I)
if key_table is None:
print("\n[x] Failed to find the key table")
return 0
except Exception as e:
return False
try:
search_regexp = r"public\s+byte\s*\[\]\s*Key\s*"
key_function = re.search(search_regexp, file_content_cs, re.I)
if key_function is None:
print("\n[x] Failed to find the Key() function")
return 0
except Exception as e:
return False
try:
search_regexp = r"public\s+byte\s*\[\]\s*IV\s*"
iv_function = re.search(search_regexp, file_content_cs, re.I)
if iv_function is None:
print("\n[x] Failed to find the IV() function")
return 0
except Exception as e:
return False
try:
file_content_cpp = build_cmf_cpp(file_content_cs, key_table, key_function, iv_function, build_number)
if file_content_cpp is None:
print("\n[x] Failed to build the CPP file")
return 0
except Exception as e:
return False
try:
target_file.writelines(single_line + "\n" for single_line in file_content_cpp)
target_file.write("\n")
except Exception as e:
return 0
return 1
def create_cmf_key_cpp(file_name):
if not create_file_backup(file_name):
return False
try:
file = open(file_name, "wt")
except Exception as e:
return False
file.write("//\n")
file.write("// This file was converted from the sources of TACTLib. DO NOT EDIT.\n")
file.write("// Source: https://github.com/overtools/TACTLib\n")
file.write("//\n\n")
return file
def download_TACTLib_repository():
try:
print("[*] Downloading TACTLib ...")
process = subprocess.Popen(["git", "clone", TACTLIB_GITHUB_REPOSITORY + ".git"], stderr=subprocess.PIPE, stdout=None)
process_output = process.communicate()[1].decode("ascii")
if process_output.startswith("Cloning into "):
return True
if process_output.endswith("already exists and is not an empty directory.\n"):
return True
except subprocess.CalledProcessError as e:
pass
return False
def update_CascLib_repository():
try:
print("[*] Updating git repository ...")
process = subprocess.Popen(["git", "add", ".\cmf"], stderr=subprocess.PIPE, stdout=None)
process_output = process.communicate()[1].decode("ascii")
return True
except subprocess.CalledProcessError as e:
pass
return False
def check_TACTLib_repository(folder_name):
try:
print("[*] Checking the downloaded folder ...")
source_file_list = os.listdir(folder_name)
if len(source_file_list) != 0:
return True
except Exception as e:
pass
return False
def process_TACTLib_repository():
print("[*] Gathering build numbers ...")
folder_name = os.path.abspath(TACTLIB_CMF_SUBDIRECTORY)
build_number_list = load_build_number_list(folder_name)
print("[*] Writing the source of providers ...")
target_file = create_cmf_key_cpp("cmf-key.cpp")
if target_file is None:
return False
target_file.write("// Supress warnings that may be raised by the converted C# code\n")
target_file.write("#ifdef _MSC_VER\n")
target_file.write("#pragma warning(push)\n")
target_file.write("#pragma warning(disable: 4100) // warning C4100: 'header': unreferenced formal parameter\n")
target_file.write("#pragma warning(disable: 4389) // warning C4389: '!=': signed/unsigned mismatch\n")
target_file.write("#endif // _MSC_VER\n\n")
for build_number in build_number_list:
plain_name = "ProCMF_%u.cs" % build_number
source_file = os.path.join(folder_name, plain_name)
print("[*] %s ... " % source_file, end="")
if convert_cs_to_cpp_cmf(source_file, target_file, build_number) == 0:
break
print("(OK)")
print("[*] Writing the table of providers ...")
build_number_list.sort()
target_file.write("// Sorted list of Key+IV providers. DO NOT EDIT.\n")
target_file.write("static const CASC_CMF_KEY_PROVIDER CmfKeyProviders[] =\n")
target_file.write("{\n")
for build_number in build_number_list:
target_file.write(" {%6u, KeyCMF_%06u::Key, KeyCMF_%06u::IV},\n" % (build_number, build_number, build_number))
target_file.write("};\n\n")
target_file.write("#ifdef _MSC_VER\n")
target_file.write("#pragma warning(pop)\n")
target_file.write("#endif // _MSC_VER\n")
target_file.close()
return True
def perform_TACTLib_update():
if not download_TACTLib_repository():
print("[x] Failed to download the TACTLib library")
return
if not check_TACTLib_repository(TACTLIB_CMF_SUBDIRECTORY):
print("[x] It seems that the download failed")
return
if not process_TACTLib_repository():
print("[x] Failed to update the key providers")
return
if not update_CascLib_repository():
print("[x] Failed to update the git repository")
return
def perform_TACTLib_cleanup():
print("[*] Cleaning up")
folder_name = os.path.abspath(TACTLIB_ROOT_SUBDIRECTORY)
delete_directory(folder_name)
if __name__ == '__main__':
perform_TACTLib_update()
perform_TACTLib_cleanup()
print("[*] Complete")