├── .gitignore ├── README.md ├── common.py ├── config.json ├── detector.py ├── lib ├── 7z-mac │ └── 7zz └── 7z-win │ ├── 7z.dll │ └── 7z.exe ├── main.py └── packages └── .gitkeep /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore thumbnails created by windows 2 | Thumbs.db 3 | 4 | # Ignore files build by Visual Studio 5 | *.obj 6 | *.exe 7 | *.pdb 8 | *.aps 9 | *.vspscc 10 | *_i.c 11 | *.i 12 | *.icf 13 | *_p.c 14 | *.ncb 15 | *.suo 16 | *.tlb 17 | *.tlh 18 | *.bak 19 | *.cache 20 | *.ilk 21 | *.log 22 | *.apk 23 | [Bb]in 24 | [Dd]ebug/ 25 | [Dd]ebug.win32/ 26 | *.sbr 27 | *.sdf 28 | obj/ 29 | [Rr]elease/ 30 | [Rr]elease.win32/ 31 | _ReSharper*/ 32 | [Tt]est[Rr]esult* 33 | ipch/ 34 | *.opensdf 35 | 36 | # Ignore files build by ndk and eclipse 37 | libs/ 38 | bin/ 39 | dist/ 40 | obj/ 41 | gen/ 42 | assets/ 43 | proguard/ 44 | local.properties 45 | .settings/ 46 | build.xml 47 | lint.xml 48 | *.class 49 | 50 | # Ignore python compiled files 51 | *.pyc 52 | 53 | # Ignore files build by airplay and marmalade 54 | build_*_xcode/ 55 | build_*_vc10/ 56 | 57 | # Ignore files build by xcode 58 | *.mode*v* 59 | *.pbxuser 60 | *.xcbkptlist 61 | *.xcworkspacedata 62 | *.xcuserstate 63 | *.xccheckout 64 | xcschememanagement.plist 65 | .DS_Store 66 | ._.* 67 | xcuserdata/ 68 | DerivedData/ 69 | 70 | # Ignore files built by AppCode 71 | .idea/ 72 | 73 | # Ignore files built by bada 74 | .Simulator-Debug/ 75 | .Target-Debug/ 76 | .Target-Release/ 77 | 78 | # Ignore files built by blackberry 79 | Simulator/ 80 | Device-Debug/ 81 | Device-Release/ 82 | 83 | # Ignore vim swaps 84 | *.swp 85 | *.swo 86 | 87 | # CTags 88 | tags 89 | 90 | # Cmake files 91 | CMakeCache.txt 92 | CMakeFiles 93 | Makefile 94 | cmake_install.cmake 95 | 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | game-engine-detector 2 | ==================== 3 | 4 | Automatically detect which game engine is an .apk or .ipa using. 5 | 6 | ## Usage 7 | 8 | ### For Mac users 9 | 10 | 1. Put all ipa and apk files to `packages` folder 11 | 2. open terminal and enter `game-engine-detector` folder 12 | 3. Execute `main.py` 13 | 14 | ``` 15 | $ python main.py 16 | ``` 17 | 18 | ### For Windows users 19 | 20 | 1. Install python2.7 from [python-2.7.6](https://www.python.org/ftp/python/2.7.6/python-2.7.6.msi) 21 | 22 | 2. Put all ipa and apk files to `packages` folder 23 | 3. Open windows commandline tool (cmd.exe), enter `game-engine-detector` folder 24 | 4. Execute `main.py` 25 | 26 | ``` 27 | > python main.py 28 | 29 | ``` -------------------------------------------------------------------------------- /common.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #coding=utf-8 3 | import json 4 | import re 5 | import os 6 | import subprocess 7 | import platform 8 | 9 | def to_unix_path(path): 10 | return path.replace('\\', '/') 11 | 12 | def normalize_utf8_path(path, index): 13 | dir = os.path.split(path)[0] 14 | ext = os.path.splitext(path)[1] 15 | new_path = os.path.join(dir, str(index) + ext) 16 | return new_path 17 | 18 | def unzip_package(file_path, out_dir, seven_zip_path=None): 19 | if not seven_zip_path: 20 | os_name = platform.system().lower() 21 | if os_name == "darwin": 22 | seven_zip_path = './lib/7z-mac/7zz' 23 | elif os_name == "windows": 24 | seven_zip_path = '.\\lib\\7z-win\\7z.exe' 25 | else: 26 | seven_zip_path = '7z' 27 | 28 | ret = subprocess.call([seven_zip_path, 'x', file_path, "-o" + out_dir], stdout=open(os.devnull, 'w')) 29 | return ret 30 | 31 | def to_absolute_path(basePath, relativePath): 32 | if relativePath and not os.path.isabs(relativePath): 33 | relativePath = os.path.join(basePath, relativePath) 34 | return relativePath 35 | 36 | 37 | def read_object_from_json_file(jsonFilePath): 38 | ret = None 39 | with open(jsonFilePath, 'r') as json_file: 40 | ret = json.load(json_file) 41 | return ret 42 | 43 | 44 | # Returns True of callback indicates to stop iteration 45 | def deep_iterate_dir(rootDir, callback, to_iter=True): 46 | for lists in os.listdir(rootDir): 47 | path = os.path.join(rootDir, lists) 48 | if os.path.isdir(path): 49 | if not to_iter: 50 | print("*** Skip sub directory: " + path) 51 | continue 52 | if callback(path, True): 53 | return True 54 | else: 55 | if deep_iterate_dir(path, callback, to_iter): 56 | return True 57 | elif os.path.isfile(path): 58 | if callback(path, False): 59 | return True 60 | return False 61 | 62 | def result_csv_output(result, output_path): 63 | import csv 64 | 65 | with open(output_path, "wb") as f: 66 | csv_writer = csv.writer(f, delimiter=",", quotechar='"', quoting=csv.QUOTE_MINIMAL) 67 | csv_writer.writerow(["File", 68 | "Engine", 69 | "EngineVersion", 70 | "Subtypes", 71 | "matched_content_file_name", 72 | "matched_content_keywords"]) 73 | for e in result: 74 | if len(e["error_info"]) > 0: 75 | engine = ", ".join(e["error_info"]) 76 | engine_version = "UNKNOWN" 77 | else: 78 | engine = e["engine"] 79 | engine_version = e["engine_version"] 80 | 81 | sub_types = ", ".join(e["sub_types"]) 82 | matched_content_keywords = ",".join(e["matched_content_keywords"]) 83 | 84 | csv_writer.writerow([e["file_name"].encode("utf-8"), 85 | engine.encode("utf-8"), 86 | engine_version.encode("utf-8"), 87 | sub_types.encode("utf-8"), 88 | e["matched_content_file_name"].encode("utf-8"), 89 | matched_content_keywords.encode("utf-8")]) 90 | f.flush() 91 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "package_dirs": 3 | [ 4 | "packages" 5 | ], 6 | 7 | "package_suffixes": 8 | [ 9 | ".apk", 10 | ".ipa", 11 | ".zip" 12 | ], 13 | 14 | "check_file_content_keywords": 15 | [ 16 | "[^/]+\\.so$", 17 | "[^/]+\\.app/.*" 18 | ], 19 | 20 | "no_need_to_check_file_content": 21 | [ 22 | "[^/]+\\.png$", 23 | "[^/]+\\.jpg$", 24 | "[^/]+\\.jpeg$", 25 | "[^/]+\\.plist$", 26 | "[^/]+\\.caf", 27 | "[^/]+\\.pvr", 28 | "[^/]+\\.swf", 29 | "[^/]+\\.json", 30 | "[^/]+\\.txt", 31 | "[^/]+\\.mp3", 32 | "[^/]+\\.ogg", 33 | "[^/]+\\.wav", 34 | "PkgInfo$", 35 | "iTunesArtwork$" 36 | ], 37 | 38 | "engines": 39 | [ 40 | { 41 | "name": "cocos2d", 42 | "file_name_keywords": ["cocos2d"], 43 | "file_content_keywords": ["CCFileUtils", "cocos2dVersion", "cocos2d"], 44 | "sub_types": 45 | { 46 | "lua": ["CCLuaEngine", "LuaEngine"], 47 | "js": ["ScriptingCore"], 48 | "CocosCreator": ["Scale9SpriteV2"] 49 | }, 50 | "engine_version_keyword": "\\0cocos2d-x[\\s-](\\d+\\..*?)\\0" 51 | }, 52 | 53 | { 54 | "name": "unity", 55 | "file_name_keywords": ["libunity\\.so", "unity3d", "unityengine", "unityscript"], 56 | "file_content_keywords": ["mono_unity", "unity3d", "unityengine", "unityscript"], 57 | "sub_types": {} 58 | }, 59 | 60 | { 61 | "name": "flash", 62 | "file_name_keywords": ["flash.*\\.so"], 63 | "file_content_keywords": [], 64 | "sub_types": 65 | { 66 | "Stage3D": ["AIRStage3D"], 67 | "ActionScript": ["ActionScript"], 68 | "FlashAir": ["flash air", "flash_air", "flashair"] 69 | } 70 | }, 71 | 72 | { 73 | "name": "Unreal", 74 | "file_name_keywords": ["libUE4.so", "UE4Game"], 75 | "file_content_keywords": ["UProjectileMovementComponent", "UPrimitiveComponent", "UE4Game"], 76 | "sub_types": {} 77 | }, 78 | 79 | { 80 | "name": "libgdx", 81 | "file_name_keywords": ["libgdx.so"], 82 | "file_content_keywords": ["com_badlogic_gdx"], 83 | "sub_types": {} 84 | }, 85 | 86 | { 87 | "name": "egret", 88 | "file_name_keywords": [], 89 | "file_content_keywords": ["egretframeworknative", "EGTTexture"], 90 | "sub_types": {} 91 | } 92 | ] 93 | } -------------------------------------------------------------------------------- /detector.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import shutil 4 | import common 5 | import re 6 | 7 | TAG = "GameEngineDetector: " 8 | 9 | class PackageScanner: 10 | def __init__(self, workspace, engines, file_name): 11 | self.result = None 12 | self.workspace = workspace 13 | self.engines = engines 14 | self.file_name = file_name 15 | self.prev_engine_name = None 16 | self._reset_result() 17 | 18 | 19 | def unzip_package(self, pkg_path, out_dir, seven_zip_path): 20 | 21 | ret = common.unzip_package(pkg_path, out_dir, seven_zip_path) 22 | 23 | if 0 != ret: 24 | print("==> ERROR: unzip package ( %s ) failed!" % self.file_name) 25 | self.result["error_info"].append("Unzip package failed") 26 | return ret 27 | 28 | def _set_engine_name(self, name): 29 | self.result["engine"] = name 30 | if self.prev_engine_name and self.prev_engine_name != name: 31 | self.result["error_info"].append("Previous check result is (%s), but now is (%s), please check config.json") 32 | 33 | def _remove_prefix(self, path): 34 | pos = path.find(self.workspace) 35 | if pos != -1: 36 | return path[len(self.workspace):] 37 | return path 38 | 39 | def _check_chunk(self, path, chunk, engine): 40 | "@return True if we could confirm the engine type" 41 | ret = False 42 | chunk = chunk.lower() 43 | 44 | for keyword in engine["file_content_keywords"]: 45 | keyword = keyword.lower() 46 | if re.search(keyword, chunk): 47 | #print("==> FOUND (engine: %s, keyword: %s)" % (engine["name"], keyword)) 48 | self._set_engine_name(engine["name"]) 49 | self.result["matched_content_file_name"] = self._remove_prefix(path) 50 | self.result["matched_content_keywords"].add(keyword) 51 | ret = True 52 | 53 | for (k, v) in engine["sub_types"].items(): 54 | for keyword in v: 55 | keyword = keyword.lower() 56 | if re.search(keyword, chunk): 57 | 58 | #print("==> FOUND sub type ( %s )" % v) 59 | self._set_engine_name(engine["name"]) 60 | self.result["matched_content_file_name"] = self._remove_prefix(path) 61 | self.result["sub_types"].add(k) 62 | self.result["matched_sub_type_keywords"].add(keyword) 63 | ret = True 64 | 65 | return ret 66 | 67 | 68 | def check_file_name(self, path): 69 | found = False 70 | path = common.to_unix_path(path) 71 | for engine in self.engines: 72 | #print("==> Checking whether the game is made by " + engine["name"]) 73 | 74 | for keyword in engine["file_name_keywords"]: 75 | if re.search(keyword, path): 76 | self._set_engine_name(engine["name"]) 77 | self.result["matched_file_name_keywords"].add(keyword) 78 | 79 | if self.result["engine"] != "unknown": 80 | found = True 81 | break 82 | 83 | return found 84 | 85 | def check_file_content(self, path, chunk_size=81920): 86 | #print("==> Checking executable file ( %s )" % path) 87 | 88 | found = False 89 | found_engine = None 90 | for engine in self.engines: 91 | #print("==> Checking whether the game is made by " + engine["name"]) 92 | 93 | with open(path, "rb") as f: 94 | while True: 95 | chunk = f.read(chunk_size) 96 | if chunk: 97 | ret = self._check_chunk(path, chunk, engine) 98 | if not found and ret: 99 | found = True 100 | found_engine = engine 101 | else: 102 | break 103 | 104 | if found: 105 | print("RESULT: " + str(self.result)) 106 | break 107 | 108 | # Check engine version 109 | if found: 110 | # Re-open so file 111 | if 'engine_version_keyword' in found_engine and path.endswith('.so'): 112 | engine_version_keyword = found_engine['engine_version_keyword'] 113 | with open(path, "rb") as f: 114 | while True: 115 | chunk = f.read(chunk_size) 116 | if chunk: 117 | matched = re.search(engine_version_keyword, chunk) 118 | if matched: 119 | self.result['engine_version'] = matched.group(1) 120 | else: 121 | break 122 | 123 | return found 124 | 125 | 126 | def _reset_result(self): 127 | self.result = { 128 | "file_name": self.file_name, 129 | "engine": "unknown", 130 | "engine_version": "unknown", 131 | "matched_file_name_keywords": set(), 132 | "matched_content_file_name": "", 133 | "matched_content_keywords": set(), 134 | "sub_types": set(), 135 | "matched_sub_type_keywords": set(), 136 | "error_info": [] 137 | } 138 | return 139 | 140 | 141 | class GameEngineDetector: 142 | def __init__(self, workspace, opts): 143 | "Constructor" 144 | 145 | self.workspace = workspace 146 | self.package_index = 0 147 | self.opts = opts 148 | self.all_results = [] 149 | 150 | print(TAG + str(opts)) 151 | 152 | self.temp_dir = os.path.join(self.workspace, "temp") 153 | 154 | self.engines = opts["engines"] 155 | self.package_dirs = opts["package_dirs"] 156 | self.package_suffixes = opts["package_suffixes"] 157 | self._normalize_package_dirs() 158 | self.check_file_content_keywords = opts["check_file_content_keywords"] 159 | self.no_need_to_check_file_content = opts["no_need_to_check_file_content"] 160 | 161 | 162 | def _normalize_package_dirs(self): 163 | for i in range(0, len(self.package_dirs)): 164 | self.package_dirs[i] = common.to_absolute_path(self.workspace, self.package_dirs[i]) 165 | print("package_dirs: " + str(self.package_dirs)) 166 | 167 | 168 | def _need_to_check_file_content(self, path): 169 | "Check whether the file is an executable file" 170 | path = common.to_unix_path(path) 171 | 172 | for k in self.no_need_to_check_file_content: 173 | m = re.search(k, path) 174 | if m: 175 | #print("==> Not need to check content: (%s)" % m.group(0)) 176 | return False 177 | 178 | for keyword in self.check_file_content_keywords: 179 | m = re.search(keyword, path) 180 | if m: 181 | #print("==> Found file to check content: (%s)" % m.group(0)) 182 | return True 183 | return False 184 | 185 | def _scan_package(self, pkg_path): 186 | 187 | file_name = os.path.split(pkg_path)[-1] 188 | 189 | print("==> Scanning package ( %s )" % file_name.encode('utf-8')) 190 | print("==> Unzip package ...") 191 | out_dir = os.path.join(self.temp_dir, file_name) 192 | 193 | scanner = PackageScanner(self.workspace, self.engines, file_name) 194 | 195 | # FIXME: rename the file path to avoid to use utf8 encoding string since 7z.exe on windows will complain. 196 | new_pkg_path = common.normalize_utf8_path(pkg_path, self.package_index) 197 | 198 | if new_pkg_path != pkg_path: 199 | os.rename(pkg_path, new_pkg_path) 200 | 201 | new_out_dir = common.normalize_utf8_path(out_dir, self.package_index) 202 | os.mkdir(new_out_dir) 203 | 204 | try: 205 | if 0 == scanner.unzip_package(new_pkg_path, new_out_dir, self.opts["7z_path"]): 206 | def callback(path, is_dir): 207 | if is_dir: 208 | return False 209 | 210 | scanner.check_file_name(path) 211 | 212 | if self._need_to_check_file_content(path): 213 | if scanner.check_file_content(path): 214 | return True 215 | 216 | return False 217 | 218 | common.deep_iterate_dir(new_out_dir, callback) 219 | 220 | self.all_results.append(scanner.result) 221 | if pkg_path != new_pkg_path: 222 | os.rename(new_pkg_path, pkg_path) 223 | 224 | except Exception as e: 225 | if pkg_path != new_pkg_path: 226 | os.rename(new_pkg_path, pkg_path) 227 | raise Exception(e) 228 | 229 | self.package_index += 1 230 | 231 | return 232 | 233 | 234 | def _iteration_callback(self, path, is_dir): 235 | for suffix in self.package_suffixes: 236 | if path.endswith(suffix): 237 | self._scan_package(path) 238 | 239 | return False 240 | 241 | def run(self): 242 | self.clean() 243 | # Re-create the temporary directory, it's an empty directory now 244 | os.mkdir(self.temp_dir) 245 | 246 | for d in self.package_dirs: 247 | common.deep_iterate_dir(d, self._iteration_callback, False) 248 | 249 | self.clean() 250 | print("==> DONE!") 251 | return 252 | 253 | def clean(self): 254 | print("==> Cleaning ...") 255 | # Remove temporary directory 256 | if os.path.exists(self.temp_dir): 257 | shutil.rmtree(self.temp_dir) 258 | 259 | return 260 | 261 | def get_all_results(self): 262 | return self.all_results 263 | 264 | 265 | 266 | 267 | 268 | -------------------------------------------------------------------------------- /lib/7z-mac/7zz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walzer/game-engine-detector/e7d201c35cb128ba35d0e7ae6e4582b95831c822/lib/7z-mac/7zz -------------------------------------------------------------------------------- /lib/7z-win/7z.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walzer/game-engine-detector/e7d201c35cb128ba35d0e7ae6e4582b95831c822/lib/7z-win/7z.dll -------------------------------------------------------------------------------- /lib/7z-win/7z.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walzer/game-engine-detector/e7d201c35cb128ba35d0e7ae6e4582b95831c822/lib/7z-win/7z.exe -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | import os 4 | import traceback 5 | 6 | import common 7 | import detector 8 | 9 | 10 | def main(): 11 | print("main entry!") 12 | 13 | workspace = os.getcwd() 14 | print("workspace: " + workspace) 15 | 16 | from optparse import OptionParser 17 | 18 | parser = OptionParser(usage="./main.py -o csv_path") 19 | 20 | parser.add_option("-c", "--configfile", 21 | action="store", type="string", dest="config_file", default=None, 22 | help="The config file path") 23 | 24 | parser.add_option("-z", "--zip", 25 | action="store", type="string", dest="seven_zip_path", default=None, 26 | help="7z path") 27 | 28 | parser.add_option("-p", "--pkg-dir", 29 | action="store", type="string", dest="pkg_dir", default=None, 30 | help="Directory that contains packages") 31 | 32 | parser.add_option("-o", "--out-file", 33 | action="store", type="string", dest="out_file", default=None, 34 | help="The result file") 35 | 36 | (opts, args) = parser.parse_args() 37 | 38 | if opts.config_file is None: 39 | opts.config_file = "config.json" 40 | 41 | cfg = common.read_object_from_json_file(opts.config_file) 42 | 43 | cfg["7z_path"]= opts.seven_zip_path 44 | 45 | if opts.pkg_dir is not None: 46 | cfg["package_dirs"]= [opts.pkg_dir.decode("utf-8")] 47 | 48 | d = detector.GameEngineDetector(workspace, cfg) 49 | d.run() 50 | r = d.get_all_results() 51 | 52 | out_file_name = os.path.join(workspace, "result.csv") 53 | 54 | if opts.out_file: 55 | out_file_name = opts.out_file 56 | 57 | common.result_csv_output(r, out_file_name) 58 | 59 | for e in r: 60 | str = "package: " + e["file_name"] + ", engine: " + e["engine"] + ", version: " + e["engine_version"] + ", " 61 | if e["sub_types"]: 62 | for sub_type in e["sub_types"]: 63 | str += "subtype: " + sub_type + ", " 64 | 65 | if len(e["error_info"]) > 0: 66 | for err in e["error_info"]: 67 | str += ", error info: " + err + ", " 68 | 69 | str += "matched:" + e["matched_content_file_name"] 70 | 71 | print(str.encode('utf-8')) 72 | 73 | if __name__ == '__main__': 74 | try: 75 | main() 76 | except Exception as e: 77 | traceback.print_exc() 78 | sys.exit(1) 79 | -------------------------------------------------------------------------------- /packages/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walzer/game-engine-detector/e7d201c35cb128ba35d0e7ae6e4582b95831c822/packages/.gitkeep --------------------------------------------------------------------------------