├── README.md └── convert_headers_to_extracted 2.py /README.md: -------------------------------------------------------------------------------- 1 | # Flex-Dump: A Header Conversion Tool for Flex 3 2 | 3 | Flex-Dump is a Python tool designed to convert class-dumped headers from iOS frameworks into a format suitable for Flex 3 patches. By automating the conversion process, it simplifies the creation and modification of Flex 3 patches for developers and enthusiasts. 4 | 5 | --- 6 | ## 🚀 How to Use the Script 7 | 8 | ### **Step 1: Dump Headers Using class-dumpc** 9 | 10 | 1. **Download class-dumpc** from this [release](https://github.com/lechium/classdumpios/releases/tag/4.2.0-RELEASE1). 11 | 2. **Open a terminal** and run the following command: 12 | 13 | ```sh 14 | ./class-dumpc -H -o 15 | ``` 16 | 17 | **Example:** 18 | ```sh 19 | ./class-dumpc -H /path/to/GoogleInteractiveMediaAds.framework/GoogleInteractiveMediaAds -o /path/to/output/GoogleInteractiveMediaAds.framework 20 | ``` 21 | 22 | ✅ Ensure the output directory has the same name as the framework, e.g., `GoogleInteractiveMediaAds.framework`. 23 | ✅ All headers should now be dumped into the specified directory. 24 | 25 | --- 26 | ### **Step 2: Run the Flex-Dump Script** 27 | 28 | 1. **Ensure you have Python 3 installed**, then navigate to the directory where the script is located and run: 29 | 30 | ```sh 31 | python3 convert_headers_to_extracted.py 32 | ``` 33 | 34 | 2. **When prompted, enter the name of the framework** (without the `.framework` extension). Example: 35 | 36 | ``` 37 | GoogleInteractiveMediaAds 38 | ``` 39 | 40 | 3. **Drag and drop the entire framework folder** (e.g., `GoogleInteractiveMediaAds.framework`, which contains all the headers) into the script when prompted. 41 | 4. The script will generate a `.extracted` file named after the framework. 42 | 43 | --- 44 | ### **Step 3: Copy the Extracted File to Flex 3 Directory** 45 | 46 | 1. **Move the generated `.extracted` file** to the following directory: 47 | 48 | ```sh 49 | /var/mobile/Documents/Flex 50 | ``` 51 | 52 | 2. **Open Flex 3**, and the new library should appear under the **“Embedded Libraries”** section. 53 | 54 | --- 55 | ## 📌 Current Status 56 | 57 | 🚧 **Early Development:** This tool is in its early stages and may contain bugs or incomplete functionality. Community contributions are highly encouraged to improve its reliability and feature set. 58 | 59 | --- 60 | ## ✨ Features 61 | 62 | ✅ **Header Conversion:** Converts headers dumped using `class-dumpc`. 63 | ✅ **Flex 3 Compatibility:** Simplifies header preparation for Flex 3 patches. 64 | ✅ **Minimal Dependencies:** Easy to run with a simple setup. 65 | 66 | --- 67 | ## 🤝 How You Can Help 68 | 69 | We welcome contributions to fix bugs, improve conversion accuracy, and expand functionality. Here’s how you can help: 70 | 71 | 1. **Submit Issues:** Report bugs or suggest improvements. 72 | 2. **Fork & Pull Requests:** Fix bugs or add features and submit pull requests. 73 | 3. **Testing:** Test the tool on different frameworks and share your feedback. 74 | 75 | Thank you for your support in making Flex-Dump better for everyone! 🚀 76 | 77 | -------------------------------------------------------------------------------- /convert_headers_to_extracted 2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import re 4 | from xml.etree import ElementTree as ET 5 | from xml.dom import minidom 6 | 7 | def calculate_type_encoding(return_type, selector, params=None): 8 | # Map _Bool to bool for consistency 9 | if return_type == '_Bool': 10 | return_type = 'bool' 11 | 12 | # Special method signatures with known encodings 13 | special_method_encodings = { 14 | # Memory management methods 15 | 'dealloc': 'v16@0:8', # -(void) dealloc 16 | 'release': 'v16@0:8', # -(void) release 17 | 'retain': '@16@0:8', # -(id) retain 18 | 'autorelease': '@16@0:8', # -(id) autorelease 19 | 20 | # Initialization methods 21 | 'init': '@16@0:8', # -(id) init 22 | 'new': '@16@0:8', # +(id) new 23 | 'alloc': '@16@0:8', # +(id) alloc 24 | 25 | # Comparison and equality methods 26 | 'isEqual:': 'B24@0:8@16', # -(BOOL) isEqual:(id) 27 | 'isLogFile:': 'B24@0:8@16', # -(BOOL) isLogFile:(id) 28 | 'isInSet:': 'B24@0:8@16', # -(BOOL) isInSet:(id) 29 | 'hash': 'Q16@0:8', # -(NSUInteger) hash 30 | 31 | # Description methods 32 | 'description': '@16@0:8', # -(NSString *) description 33 | 'debugDescription': '@16@0:8', # -(NSString *) debugDescription 34 | 35 | # Class/type methods 36 | 'class': '#16@0:8', # +(Class) class 37 | 'superclass': '#16@0:8', # +(Class) superclass 38 | 'conformsToProtocol:': 'B24@0:8^#16', # -(BOOL) conformsToProtocol:(Protocol *) 39 | 'isKindOfClass:': 'B24@0:8#16', # -(BOOL) isKindOfClass:(Class) 40 | 'respondsToSelector:': 'B24@0:8:16', # -(BOOL) respondsToSelector:(SEL) 41 | 42 | # Copying methods 43 | 'copyWithZone:': '@24@0:8^{_NSZone=}16', # -(id) copyWithZone:(NSZone *) 44 | 'mutableCopyWithZone:': '@24@0:8^{_NSZone=}16' # -(id) mutableCopyWithZone:(NSZone *) 45 | } 46 | 47 | if selector in special_method_encodings: 48 | return special_method_encodings[selector] 49 | 50 | # Handle BOOL methods with id parameter 51 | if return_type.strip() in ['BOOL', '_Bool'] and ':' in selector: 52 | param_type = selector.split(':')[1].strip() 53 | if param_type == '(id)': 54 | return 'B24@0:8@16' 55 | 56 | # Handle instancetype at the encoding level 57 | if return_type.strip() == 'instancetype': 58 | return_type = 'id' 59 | 60 | params_count = selector.count(':') 61 | base_size = 16 62 | param_size = 8 63 | total_size = base_size + (param_size * params_count) 64 | return_type = return_type.strip() 65 | 66 | # Handle completion handlers and block types 67 | if '^' in return_type or 'CDUnknownBlockType' in return_type: 68 | # Handle completion handler with BOOL parameter 69 | if '(void (^)(_Bool))' in return_type or '(void (^)(BOOL))' in return_type: 70 | return f"v32@0:8@?16@24" # void return, block with BOOL param 71 | # Handle completion handler with void parameter 72 | elif '(void (^)(void))' in return_type: 73 | return f"v24@0:8@?16" # void return, block with void param 74 | # Handle completion handler with id parameter 75 | elif '(void (^)(id))' in return_type: 76 | return f"v32@0:8@?16@24" # void return, block with id param 77 | # Handle block as parameter 78 | elif 'CDUnknownBlockType' in return_type: 79 | return f"v32@0:8@?16@24" # void return with block parameter 80 | # Handle block returning BOOL 81 | elif return_type.startswith('_Bool (^)') or return_type.startswith('BOOL (^)'): 82 | return f"B32@0:8@?16@24" # BOOL return with block 83 | else: 84 | return f"@32@0:8@?16@24" # generic block type 85 | 86 | # Handle struct types 87 | struct_types = { 88 | "CGRect": "{CGRect={CGPoint=dd}{CGSize=dd}}", 89 | "CGPoint": "{CGPoint=dd}", 90 | "CGSize": "{CGSize=dd}", 91 | "NSRange": "{_NSRange=QQ}", 92 | "UIEdgeInsets": "{UIEdgeInsets=dddd}", 93 | "CGAffineTransform": "{CGAffineTransform=dddddd}", 94 | "IMAAdPlaybackInfo": "^{?=iiiiddddd{?=iiiiiiiiiiiiii}}", 95 | "struct _NSZone *": "^{_NSZone=}", 96 | "_NSZone *": "^{_NSZone=}", 97 | "NSXMLParser *": "@", 98 | "Protocol *": "^#", 99 | "id": "@" 100 | } 101 | 102 | if return_type in struct_types: 103 | type_prefix = struct_types[return_type] 104 | return f"{type_prefix}{total_size}@0:8" 105 | 106 | # Basic type mapping 107 | type_map = { 108 | "void": "v", 109 | "BOOL": "B", "bool": "B", "_Bool": "B", "Bool": "B", "bool_": "B", 110 | "instancetype": "@", # Map instancetype to id encoding 111 | "double": "d", 112 | "long long": "q", "long": "q", 113 | "unsigned long long": "Q", 114 | "unsigned int": "I", 115 | "int": "i", 116 | "float": "f", 117 | "id": "@", 118 | "NSString *": "@", 119 | "NSArray *": "@", 120 | "NSDictionary *": "@", 121 | "NSObject *": "@", 122 | "Class": "#", 123 | "SEL": ":", 124 | "char *": "*", 125 | "const char *": "r*", 126 | "void *": "^v", 127 | "IMP": "^?", 128 | "id *": "^@", 129 | "NSError *": "@", 130 | "NSError **": "^@", 131 | "CDUnknownBlockType": "@?", 132 | "dispatch_block_t": "@?", 133 | "void(^)(void)": "@?" 134 | } 135 | 136 | # Handle pointer types more robustly 137 | if return_type.endswith('**'): 138 | base_type = return_type.rstrip('* ').strip() 139 | # Handle const for double pointers 140 | if 'const' in base_type: 141 | base_type = base_type.replace('const', '').strip() 142 | if base_type.startswith('NS') or base_type.startswith('UI'): 143 | type_prefix = "r^@" # const pointer to NS/UI object pointer 144 | else: 145 | type_prefix = "r^^" # const pointer to pointer 146 | else: 147 | if base_type.startswith('NS') or base_type.startswith('UI'): 148 | type_prefix = "^@" # pointer to NS/UI object pointer 149 | else: 150 | type_prefix = "^^" # pointer to pointer 151 | elif return_type.endswith('*'): 152 | base_type = return_type.rstrip('* ').strip() 153 | # Handle const for single pointers 154 | if 'const' in base_type: 155 | base_type = base_type.replace('const', '').strip() 156 | if base_type.startswith('NS') or base_type.startswith('UI'): 157 | type_prefix = "r@" # const NS/UI object pointer 158 | elif base_type == "char": 159 | type_prefix = "r*" # const char pointer 160 | else: 161 | type_prefix = "r^" # const pointer 162 | else: 163 | if base_type.startswith('NS') or base_type.startswith('UI'): 164 | type_prefix = "@" # NS/UI object pointer 165 | else: 166 | type_prefix = "^" # regular pointer 167 | else: 168 | type_prefix = type_map.get(return_type, '@') 169 | 170 | return f"{type_prefix}{total_size}@0:8" 171 | 172 | def process_header_file(file_path): 173 | methods = [] 174 | current_interface = None 175 | current_category = None 176 | superclass = None 177 | 178 | try: 179 | with open(file_path, 'r', encoding='utf-8') as f: 180 | content = f.read() 181 | 182 | # Remove comments and preprocessor directives 183 | content = re.sub(r'//.*?\n|/\*.*?\*/', '', content, flags=re.DOTALL) 184 | content = re.sub(r'#.*?\n', '', content) 185 | 186 | # Better multi-line method handling 187 | content = re.sub(r'(\n\s*)([+-])', r' \2', content) # Preserve method declarations 188 | 189 | # Improved interface/protocol/category detection 190 | interface_match = re.search(r'@interface\s+(\w+)\s*:\s*(\w+)', content) 191 | protocol_match = re.search(r'@protocol\s+(\w+)', content) 192 | category_match = re.search(r'@interface\s+(\w+)\s*\((\w*)\)', content) 193 | 194 | if interface_match: 195 | current_interface = interface_match.group(1) 196 | superclass = interface_match.group(2) 197 | elif protocol_match: 198 | current_interface = protocol_match.group(1) 199 | superclass = "NSObject" # Protocols inherit from NSObject 200 | elif category_match: 201 | current_interface = category_match.group(1) 202 | superclass = None # Categories don't define superclasses 203 | 204 | # Default to NSObject if no class found 205 | if not current_interface: 206 | current_interface = "NSObject" 207 | 208 | # Extract all method declarations 209 | method_pattern = r'([+-])\s*\(([\w\s*<>{}\[\]]+(?:\s*\*)?)\)\s*([^;]+);' 210 | method_matches = re.finditer(method_pattern, content) 211 | 212 | seen_methods = set() 213 | 214 | for match in method_matches: 215 | prefix, return_type, selector = match.groups() 216 | 217 | # Clean up selector (handle multi-line) 218 | selector = re.sub(r'\s+', ' ', selector.strip()) 219 | 220 | # Create unique key for method 221 | method_key = f"{prefix}{selector.strip()}" 222 | if method_key in seen_methods: 223 | continue 224 | seen_methods.add(method_key) 225 | 226 | method_info = parse_method_declaration(f"{prefix}({return_type}) {selector}", current_interface) 227 | if method_info: 228 | method_info['className'] = current_interface 229 | methods.append(method_info) 230 | 231 | return methods, current_interface, superclass 232 | except Exception as e: 233 | print(f"Error processing file {file_path}: {str(e)}") 234 | return [], None, None 235 | 236 | def parse_method_declaration(line, class_name): 237 | # Use Flex-Dump's improved pattern 238 | pattern = r'([+-])\s*\(([\w\s*<>{}\[\]]+(?:\s*\*)?)\)\s*([^;]+)' 239 | match = re.match(pattern, line.strip()) 240 | if not match: 241 | return None 242 | 243 | prefix, return_type, selector = match.groups() 244 | return_type = return_type.strip() 245 | 246 | # Special handling for .cxx_destruct 247 | if ".cxx_destruct" in selector: 248 | return { 249 | 'prefix': prefix, 250 | 'selector': ".cxx_destruct", 251 | 'typeEncoding': "v16@0:8", 252 | 'displayName': f"{prefix}(void) .cxx_destruct", 253 | 'className': class_name 254 | } 255 | 256 | # Clean up return type for display 257 | display_type = return_type 258 | if return_type.lower() in ["_bool", "bool", "bool_", "bool"]: 259 | display_type = "bool" 260 | elif 'Protocol' in return_type: 261 | display_type = 'Protocol *' 262 | 263 | selector = selector.strip() 264 | 265 | # Use Flex-Dump's parameter parsing 266 | if ':' not in selector: 267 | clean_selector = selector 268 | display_name = f"{prefix}({display_type}) {selector}" 269 | else: 270 | parts = re.split(r'\([^)]+\)\s*\w+', selector) 271 | types = re.findall(r'\(([^)]+)\)', selector) 272 | 273 | method_parts = [p.strip().rstrip(':') for p in parts if p.strip()] 274 | clean_selector = ':'.join(method_parts) + ':' 275 | 276 | display_parts = [] 277 | param_types = [] # Track parameter types for type encoding 278 | 279 | for i, part in enumerate(method_parts): 280 | param_type = types[i] if i < len(types) else 'id' 281 | param_type = param_type.strip() 282 | 283 | # Clean up parameter types 284 | if param_type.lower() in ["_bool", "bool", "bool_", "bool"]: 285 | param_type = "bool" 286 | param_types.append(('B', 16 + (i * 8))) 287 | elif param_type.lower() in ['int', 'nsinteger']: 288 | param_types.append(('i', 16 + (i * 8))) 289 | elif param_type.lower() in ['long', 'long long', 'nsuinteger']: 290 | param_types.append(('q', 16 + (i * 8))) 291 | elif param_type.lower() in ['float', 'cgfloat']: 292 | param_types.append(('f', 16 + (i * 8))) 293 | elif param_type.lower() == 'double': 294 | param_types.append(('d', 16 + (i * 8))) 295 | else: 296 | param_types.append(('@', 16 + (i * 8))) 297 | 298 | display_parts.append(f"{part}:({param_type})") 299 | 300 | display_name = f"{prefix}({display_type}) {' '.join(display_parts)}" 301 | 302 | # Build type encoding with proper offsets 303 | return_code = 'v' if return_type == 'void' else \ 304 | 'B' if display_type == 'bool' else \ 305 | 'i' if return_type == 'int' else \ 306 | 'q' if return_type == 'long' else \ 307 | 'f' if return_type == 'float' else \ 308 | 'd' if return_type == 'double' else '@' 309 | 310 | total_size = 16 + (len(param_types) * 8) 311 | type_encoding = f"{return_code}{total_size}@0:8" + ''.join(f"{code}{offset}" for code, offset in param_types) 312 | 313 | return { 314 | 'prefix': prefix, 315 | 'selector': clean_selector, 316 | 'typeEncoding': type_encoding, 317 | 'displayName': display_name, 318 | 'className': class_name 319 | } 320 | 321 | # For no-parameter methods 322 | type_encoding = calculate_type_encoding(return_type, clean_selector) 323 | return { 324 | 'prefix': prefix, 325 | 'selector': clean_selector, 326 | 'typeEncoding': type_encoding, 327 | 'displayName': display_name, 328 | 'className': class_name 329 | } 330 | 331 | def clean_method_name(name): 332 | """Thoroughly clean method names""" 333 | name = name.strip() 334 | name = re.sub(r'\s*arg\d+\s*', '', name) # Remove all argN 335 | name = re.sub(r'\s+', ' ', name) # Clean up spaces 336 | return name 337 | 338 | def clean_parameter_type(param_type): 339 | """Thoroughly clean parameter types""" 340 | if not param_type: 341 | return 'id' 342 | 343 | param_type = param_type.strip('() ') 344 | param_type = re.sub(r'_Bool\s*\)?', 'bool', param_type, flags=re.IGNORECASE) 345 | param_type = re.sub(r'BOOL\s*\)?', 'bool', param_type, flags=re.IGNORECASE) 346 | param_type = re.sub(r'\s*\)\s*$', '', param_type) 347 | param_type = re.sub(r'\s+', ' ', param_type) 348 | 349 | if param_type.lower() in ['id)', 'id']: 350 | return 'id' 351 | 352 | return param_type 353 | 354 | def parse_method_parts(selector): 355 | """Parse method parts with improved cleaning""" 356 | parts = [] 357 | current_name = [] 358 | current_type = [] 359 | in_parens = 0 360 | 361 | tokens = re.findall(r'[^:\s()]+|\(|\)|:', selector) 362 | 363 | for token in tokens: 364 | if token == '(': 365 | in_parens += 1 366 | if in_parens == 1: 367 | continue 368 | elif token == ')': 369 | in_parens -= 1 370 | if in_parens == 0: 371 | continue 372 | elif token == ':': 373 | if in_parens == 0: 374 | name = ''.join(current_name).strip() 375 | param_type = ''.join(current_type).strip() 376 | parts.append((name, param_type)) 377 | current_name = [] 378 | current_type = [] 379 | continue 380 | 381 | if in_parens > 0: 382 | current_type.append(token) 383 | else: 384 | current_name.append(token) 385 | 386 | return parts 387 | 388 | def format_parameter(param_type): 389 | # Add thorough parameter cleaning 390 | param_type = param_type.strip('() ') 391 | param_type = re.sub(r'_Bool\s*\)?', 'bool', param_type, flags=re.IGNORECASE) 392 | param_type = re.sub(r'BOOL\s*\)?', 'bool', param_type, flags=re.IGNORECASE) 393 | param_type = re.sub(r'\s*\)\s*$', '', param_type) # Remove trailing ) 394 | param_type = re.sub(r'\s+', ' ', param_type) # Clean up spaces 395 | return param_type 396 | 397 | def clean_type(type_str): 398 | """Clean up type declarations to match Flex 3 format exactly""" 399 | # Remove extra spaces and parentheses 400 | type_str = type_str.strip('() ') 401 | 402 | # Handle instancetype explicitly 403 | if type_str == 'instancetype': 404 | return 'id' 405 | 406 | # Handle boolean types 407 | if type_str.lower() in ['_bool', 'bool', 'bool_', 'bool', 'BOOL']: 408 | return 'bool' 409 | 410 | # Handle id types 411 | if type_str.lower() in ['id)', 'id']: 412 | return 'id' 413 | 414 | # Handle numeric types 415 | if type_str == 'double': 416 | return 'double' 417 | if type_str == 'float': 418 | return 'float' 419 | if type_str == 'int': 420 | return 'int' 421 | 422 | # Handle common Objective-C types 423 | type_map = { 424 | 'void': 'void', 425 | 'NSString *': 'NSString *', 426 | 'NSArray *': 'NSArray *', 427 | 'NSDictionary *': 'NSDictionary *', 428 | 'NSObject *': 'NSObject *', 429 | 'NSError *': 'NSError *', 430 | 'Class': 'Class', 431 | 'SEL': 'SEL', 432 | 'CGRect': 'struct CGRect', 433 | 'CGPoint': 'struct CGPoint', 434 | 'CGSize': 'struct CGSize' 435 | } 436 | 437 | # Need more aggressive cleaning 438 | type_str = re.sub(r'\s*\)$', '', type_str) # Remove trailing ) 439 | type_str = re.sub(r'_Bool\s*\)?', 'bool', type_str, flags=re.IGNORECASE) 440 | type_str = re.sub(r'BOOL\s*\)?', 'bool', type_str, flags=re.IGNORECASE) 441 | 442 | return type_map.get(type_str, type_str) 443 | 444 | def create_extracted_xml(framework_name, headers_dir): 445 | root = ET.Element("plist", version="1.0") 446 | dict_root = ET.SubElement(root, "dict") 447 | 448 | # Add classes key and array 449 | classes_key = ET.SubElement(dict_root, "key") 450 | classes_key.text = "objcClasses" 451 | array_classes = ET.SubElement(dict_root, "array") 452 | 453 | # Process headers and create class data 454 | classes_data = [] 455 | for filename in sorted(os.listdir(headers_dir)): 456 | if not filename.endswith('.h'): 457 | continue 458 | 459 | filepath = os.path.join(headers_dir, filename) 460 | methods, class_name, super_class = process_header_file(filepath) 461 | 462 | if methods: # Only add if we have valid methods 463 | class_info = { 464 | 'name': class_name, 465 | 'superClassName': super_class, 466 | 'methods': methods 467 | } 468 | classes_data.append(class_info) 469 | 470 | # Create XML structure 471 | for class_info in classes_data: 472 | dict_class = ET.SubElement(array_classes, "dict") 473 | 474 | # Add methods array 475 | methods_key = ET.SubElement(dict_class, "key") 476 | methods_key.text = "methods" 477 | methods_array = ET.SubElement(dict_class, "array") 478 | 479 | for method in class_info['methods']: 480 | method_dict = ET.SubElement(methods_array, "dict") 481 | 482 | # Clean method info to remove any remaining arg1, arg2 483 | clean_method = { 484 | 'className': method['className'], 485 | 'displayName': method['displayName'].replace('arg1', '').replace('arg2', '').replace('arg3', '').replace('arg4', ''), 486 | 'prefix': method['prefix'], 487 | 'selector': method['selector'].replace('arg1', '').replace('arg2', '').replace('arg3', '').replace('arg4', ''), 488 | 'typeEncoding': method['typeEncoding'] 489 | } 490 | 491 | # Add method info exactly matching Flex format 492 | for key in ["className", "displayName", "prefix", "selector", "typeEncoding"]: 493 | key_elem = ET.SubElement(method_dict, "key") 494 | key_elem.text = key 495 | string_elem = ET.SubElement(method_dict, "string") 496 | string_elem.text = clean_method[key] 497 | 498 | # Add class name 499 | ET.SubElement(dict_class, "key").text = "name" 500 | ET.SubElement(dict_class, "string").text = class_info['name'] 501 | 502 | # Only add superclass if it exists (not for categories) 503 | if class_info.get('superClassName'): 504 | ET.SubElement(dict_class, "key").text = "superClassName" 505 | ET.SubElement(dict_class, "string").text = class_info['superClassName'] 506 | 507 | ET.SubElement(dict_root, "key").text = "version" 508 | ET.SubElement(dict_root, "real").text = "1.2050000429153442" 509 | 510 | xmlstr = minidom.parseString(ET.tostring(root)).toprettyxml(indent="\t") 511 | xmlstr = '\n'.join(line for line in xmlstr.split('\n') if line.strip()) 512 | 513 | output_file = f"{framework_name}.extracted" 514 | with open(output_file, 'w', encoding='utf-8') as f: 515 | f.write('\n') 516 | f.write('\n') 517 | plist_content = xmlstr[xmlstr.find('