#!/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")