diff options
Diffstat (limited to 'dep/CascLib/src/overwatch/cmf-update.py')
-rw-r--r-- | dep/CascLib/src/overwatch/cmf-update.py | 482 |
1 files changed, 482 insertions, 0 deletions
diff --git a/dep/CascLib/src/overwatch/cmf-update.py b/dep/CascLib/src/overwatch/cmf-update.py new file mode 100644 index 00000000000..6c3c2896e83 --- /dev/null +++ b/dep/CascLib/src/overwatch/cmf-update.py @@ -0,0 +1,482 @@ +#!/usr/bin/env python3 + +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): + + # Create the list of available build numbers + build_number_list = [] + + # Enumerate files in that folder and scan all files with names like "ProCMF_######.cs" + for plain_name in os.listdir(folder_name): + + # Retrieve the build number from that file name + build_number = get_file_build_number(plain_name) + if not build_number: + continue + + # Append the build number to the list + build_number_list.append(build_number) + + # Make it sorted + 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): + + # Setup the indent line + indent_line = CPP_BLOCK_INDENT * nest_level + + # If there is some remaining items in the single line, flush it to the stream + if len(line_buffer) != 0: + cmf_cpp.append(indent_line + line_buffer) + line_buffer = "" + + # Opening a new block? + if nest_increment == +1: + cmf_cpp.append(indent_line + closing_token) + return "", nest_level + 1 + + # Closing a block? + if nest_increment == -1: + indent_line = CPP_BLOCK_INDENT * (nest_level - 1) + cmf_cpp.append(indent_line + closing_token) + return "", nest_level - 1 + + # Move the nest level + return "", nest_level + + +def append_hex_array(cmf_cpp, header_line, key_table_tokens, nest_level): + + # Initialization + inside_block = False + indent_line = CPP_BLOCK_INDENT * nest_level + line_buffer = "" + hexa_values = 0 + + # Append the heading line + cmf_cpp.append(indent_line + header_line) + + # Parse the tokens + for token in key_table_tokens: + + # Skip spaces + if token == "": + continue + + # Block opening + 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 + + # Block closing + if token == "};" and inside_block == True: + line_buffer, nest_level = flush_line(cmf_cpp, line_buffer, "};", nest_level, -1) + inside_block = False + break + + # A hexa value + 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 + + # An unexpected token + print("[x] Unexpected token: " + token) + + # The line should be empty here + 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): + + # Initialization + 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 = "" + + # Append the heading line + cmf_cpp.append(indent_line + header_line) + + # Parse the tokens + for token in key_table_tokens: + + # Skip spaces + if token == "": + continue + + # Skip the function definition + if skipping_definition: + if token.endswith(")"): + skipping_definition = False + continue + + # Opening brace + if token == "{": + line_buffer, nest_level = flush_line(cmf_cpp, line_buffer, "{", nest_level, +1) + continue + + # Closing brace + if token == "}": + line_buffer, nest_level = flush_line(cmf_cpp, line_buffer, "}", nest_level, -1) + if nest_level == save_nest_level: + break + continue + + # Skipping the buffer declaration + if skipping_buffer_allocation: + + # Append the token to the line + 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 + + # Anything else - append to the line buffer + line_buffer = append_token(line_buffer, token) + + # Handle the begin and end of the "case" label + 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 + + # Handle the begin and end of the "for" header + 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 + + # End of line + 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 = [] + + # Build the header + 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") + + # Append the begin of the namespace + cmf_cpp.append("namespace KeyCMF_%06u" % build_number) + cmf_cpp.append("{") + + # Append the key table + 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("") + + # Append the key generation function + 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("") + + # Append the IV generation function + 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): + + # Load the content of the file to memory + try: + file_content = None + with open(source_file, "rt") as f: + file_content_cs = f.read() + except Exception as e: + return False + + # Locate the KeyTable, Key() and IV() procedures + 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 + + # Locate the function for the key generation + 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 + + # Locate the function for the IV generation + 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 + + # Generate the content of the C++ file + 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 + + # Write the content of the file + 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): + + # Create backup of the file + if not create_file_backup(file_name): + return False + + # Create the file + try: + file = open(file_name, "wt") + except Exception as e: + return False + + # Write the initial comment + 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: + # Show what we're doing + print("[*] Downloading TACTLib ...") + + # Run git clone + process = subprocess.Popen(["git", "clone", TACTLIB_GITHUB_REPOSITORY + ".git"], stderr=subprocess.PIPE, stdout=None) + process_output = process.communicate()[1].decode("ascii") + + # Check for success + if process_output.startswith("Cloning into "): + return True + + # Check for existing directory + 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: + # Show what we're doing + print("[*] Updating git repository ...") + + # Run git clone + 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(): + + # Initialization + print("[*] Gathering build numbers ...") + folder_name = os.path.abspath(TACTLIB_CMF_SUBDIRECTORY) + build_number_list = load_build_number_list(folder_name) + + # Create the new cmf-key.cpp + print("[*] Writing the source of providers ...") + target_file = create_cmf_key_cpp("cmf-key.cpp") + if target_file is None: + return False + + # Write warning supression + 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") + + # Convert every file in the directory + for build_number in build_number_list: + + # Show the processed file name + plain_name = "ProCMF_%u.cs" % build_number + source_file = os.path.join(folder_name, plain_name) + print("[*] %s ... " % source_file, end="") + + # Convert the content of the file into the cmf-key.cpp + if convert_cs_to_cpp_cmf(source_file, target_file, build_number) == 0: + break + print("(OK)") + + # Write the table that contains the list of providers + 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") + + # Write the entries + 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") + + # Write the end of warnings + target_file.write("#ifdef _MSC_VER\n") + target_file.write("#pragma warning(pop)\n") + target_file.write("#endif // _MSC_VER\n") + + # Write the tail + target_file.close() + return True + + +def perform_TACTLib_update(): + + # Download the content of the "TACTLib package" + if not download_TACTLib_repository(): + print("[x] Failed to download the TACTLib library") + return + + # Did we download something? + if not check_TACTLib_repository(TACTLIB_CMF_SUBDIRECTORY): + print("[x] It seems that the download failed") + return + + # Create list of the supported game builds + 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") + |