diff --git a/.env.example b/.env.example index 19d9ef8..02e90e6 100644 --- a/.env.example +++ b/.env.example @@ -1,23 +1,22 @@ # === iOS 自动打包服务配置 === # 复制此文件为 .env 并修改为实际值 -# ---- 项目路径 ---- -# iOS 源码项目根目录(也用于 AutoPacking 脚本路径) -PROJECT_ROOT=/Users/shen/Work/Code/Readoor +# ---- 目录配置 ---- +# 分支源码根目录,每个分支一个子目录(如 ReadoorBranches/main、ReadoorBranches/dev) +GIT_SOURCE_BASE=./ReadoorBranches # 打包输出基础目录(每次打包会在此目录下创建子目录) -BUILD_BASE_DIR=/Users/shen/Documents +BUILD_BASE_DIR=./build # ---- 分支源码管理 ---- -# 分支源码根目录,每个分支一个子目录(如 ReadoorBranches/main、ReadoorBranches/dev) -GIT_SOURCE_BASE=/Users/shen/Work/Code/ReadoorBranches - # Git 远程仓库地址,分支目录不存在时自动 clone -GIT_REMOTE_URL=git@github.com:your-org/readoor.git +GIT_REMOTE_URL=https://pineapple.readoor.cn:9080/trn/triapp.git +GIT_USERNAME=shenlei +GIT_PASSWORD='&s9U5fbreA' # ---- 服务端口 ---- -BACKEND_PORT=8000 -FRONTEND_PORT=3000 +BACKEND_PORT=5002 +FRONTEND_PORT=5999 # ---- 管理员账号 ---- ADMIN_USERNAME=admin diff --git a/.gitignore b/.gitignore index e41d212..f718dd9 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,9 @@ __pycache__/ # 运行时数据 backend/data/*.bak +config.json +ReadoorBranches/ +build/ # 日志 logs/ diff --git a/README.md b/README.md index 9788511..c9a420f 100644 --- a/README.md +++ b/README.md @@ -44,14 +44,17 @@ cp .env.example .env vim .env ``` -**必须修改的配置项:** +**建议优先检查的配置项:** ```bash -# iOS 源码项目路径(包含 readoor.xcworkspace 的目录) -PROJECT_ROOT=/path/to/your/Readoor +# 分支源码根目录,默认在当前项目下 +GIT_SOURCE_BASE=/path/to/iOSBuildServer/ReadoorBranches -# 打包输出目录 -BUILD_BASE_DIR=/path/to/build/output +# 打包输出目录,默认在当前项目下 +BUILD_BASE_DIR=/path/to/iOSBuildServer/build + +# Git 远程仓库地址(配置后可自动拉取分支) +GIT_REMOTE_URL=git@github.com:org/repo.git # 管理员密码(建议修改) ADMIN_PASSWORD=your_secure_password @@ -71,17 +74,19 @@ ADMIN_PASSWORD=your_secure_password ### 4.(可选)初始化分支源码目录 -如果需要按分支打包,先准备分支源码目录: +系统现在默认直接从分支源码目录打包,每个分支对应 `GIT_SOURCE_BASE` 下的一个子目录。 + +如果已配置 `GIT_REMOTE_URL`,首次打包某个分支时会自动 clone;如果未配置,则需要先手动准备分支目录: ```bash # 创建分支源码根目录 mkdir -p /path/to/ReadoorBranches -# 手动 clone 主分支(后续自动 pull 更新) +# 手动 clone 主分支(后续可继续手动维护) git clone -b main git@github.com:org/repo.git /path/to/ReadoorBranches/main ``` -其他分支无需手动 clone,首次打包时会自动从远程仓库 clone。 +如果已配置 `GIT_REMOTE_URL`,其他分支无需手动 clone,首次打包时会自动拉取。 ### 5.(可选)启用健康监测 @@ -101,13 +106,12 @@ git clone -b main git@github.com:org/repo.git /path/to/ReadoorBranches/main 所有配置通过项目根目录的 `.env` 文件管理,修改后重启服务生效。 -### 项目路径 +### 目录配置 | 变量 | 示例 | 说明 | |------|------|------| -| `PROJECT_ROOT` | `/Users/shen/Work/Code/Readoor` | iOS 源码项目根目录(AutoPacking 脚本路径) | -| `BUILD_BASE_DIR` | `/Users/shen/Documents` | 打包产物输出基础目录 | -| `GIT_SOURCE_BASE` | `/path/to/ReadoorBranches` | 分支源码根目录,每个分支一个子目录 | +| `GIT_SOURCE_BASE` | `/Users/shen/Work/Code/iOSBuildServer/ReadoorBranches` | 分支源码根目录,每个分支一个子目录,仅提供 iOS 工程源码 | +| `BUILD_BASE_DIR` | `/Users/shen/Work/Code/iOSBuildServer/build` | 打包产物输出基础目录 | | `GIT_REMOTE_URL` | `git@github.com:org/repo.git` | 远程仓库地址,分支目录不存在时自动 clone | ### 服务配置 @@ -131,14 +135,20 @@ git clone -b main git@github.com:org/repo.git /path/to/ReadoorBranches/main 以下配置通过管理后台 Web 界面管理(存储在 `config.json`): +首次部署时,如果项目根目录下存在 `config.json`,系统会自动导入其中的 `apps` 和 `schemes` 作为初始配置;导入后即可继续在后台增删改。 + | 配置项 | 说明 | |--------|------| | App 管理 | 应用名称、Scheme、服务器环境、upload_key 等 | | Scheme 管理 | 打包 Scheme 配置 | | 服务器环境 | 测试/正式/自定义环境的 API 地址、Universal Link 等 | | 分支管理 | 可打包的代码分支列表 | -| 版本号 | App_Ver 和 Build_Ver(写入 AutoPacking 脚本) | -| 上传配置 | OSS / WebDAV 文件上传设置 | +| 版本号 | 服务端统一管理 App_Ver 和 Build_Ver,不修改分支源码 | +| 上传配置 | 服务端内置 OSS / WebDAV 分发,生成 IPA、manifest、下载页和二维码 | + +### 服务端自动化资源 + +项目内的 `backend/automation/` 是 Web 打包服务唯一使用的自动化资源:默认皮肤和混淆工具都在此处维护。分支源码中的 `AutoPacking/` 不会被复制、执行或修改,仍可供开发者手动打包使用。 ### Watchdog 配置 @@ -496,7 +506,7 @@ tail -50 logs/server.log ``` 常见原因: -- `PROJECT_ROOT` 路径不存在 +- `GIT_SOURCE_BASE` 下对应分支目录不存在,且未配置 `GIT_REMOTE_URL` - 磁盘空间不足 - Python 依赖缺失(重新执行 `./deploy.sh build`) diff --git a/backend/automation/obfuscation/generate_image_whitelist.py b/backend/automation/obfuscation/generate_image_whitelist.py new file mode 100644 index 0000000..ce28695 --- /dev/null +++ b/backend/automation/obfuscation/generate_image_whitelist.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python3 +""" +构建期 dylib 白名单生成脚本 + +优先扫描最终 .app Bundle 内实际嵌入并会被 dyld 加载的镜像,生成: +1. RDKnownImages.plist — 白名单 + HMAC-SHA256 摘要 +2. Generated/RDWhitelistSecret.swift — 仅在缺失时补生成 + +在 Xcode Build Phase 中调用时,会复用现有密钥并更新最终 app bundle 内的 plist; +手动执行时若无法定位 app bundle,则退回扫描仓库中的 framework/dylib 来源。 +""" + +import os +import sys +import hmac +import hashlib +import plistlib +import secrets +import re +from pathlib import Path + +# 所有 framework 来源目录(相对于项目根目录) +FRAMEWORK_SEARCH_PATHS = [ + 'Pods', + 'readoor/BookShelf/Classes/Factorys', + 'readoor/3.0/Factorys', + 'readoor/3.0/Features/ThirdPartyService', +] + +# 输出路径 +PLIST_OUTPUT = 'readoor/3.0/Base/Security/RDKnownImages.plist' +SECRET_OUTPUT = 'readoor/3.0/Base/Security/Generated/RDWhitelistSecret.swift' +def scan_source_frameworks(search_paths, app_name): + """扫描仓库中的 framework 和 dylib 来源,作为无法定位 app bundle 时的兜底方案。""" + known = [] + seen = set() + for search_dir in search_paths: + if not os.path.isdir(search_dir): + continue + for root, dirs, files in os.walk(search_dir): + # .framework 在 macOS 上是目录,出现在 dirs 中 + for d in dirs: + if d.endswith('.framework'): + basename = d.replace('.framework', '') + rel_path = os.path.relpath(os.path.join(root, d), '.') + key = (basename, rel_path) + if key not in seen: + seen.add(key) + known.append({'name': basename, 'path': rel_path}) + for f in files: + if f.endswith('.dylib'): + basename = f.replace('.dylib', '') + rel_path = os.path.relpath(os.path.join(root, f), '.') + key = (basename, rel_path) + if key not in seen: + seen.add(key) + known.append({'name': basename, 'path': rel_path}) + + # App 自身 + app_key = (app_name, app_name) + if app_key not in seen: + known.append({'name': app_name, 'path': app_name}) + + # 按 name 排序保证稳定性 + known.sort(key=lambda x: x['name']) + return known + + +def scan_bundled_images(app_bundle_path, executable_path): + """扫描最终 app bundle 中会实际被加载的镜像,路径使用 bundle 相对路径。""" + known = [] + seen = set() + + executable_name = Path(executable_path).name if executable_path else Path(app_bundle_path).stem + main_key = (executable_name, executable_path or executable_name) + seen.add(main_key) + known.append({'name': executable_name, 'path': executable_path or executable_name}) + + for root, dirs, files in os.walk(app_bundle_path): + dirs.sort() + files.sort() + + for d in dirs: + if not d.endswith('.framework'): + continue + framework_dir = os.path.join(root, d) + basename = d.replace('.framework', '') + + # if basename == 'MJExtension': # 故意排除 + # continue + + binary_path = os.path.join(framework_dir, basename) + if not os.path.isfile(binary_path): + continue + rel_path = os.path.relpath(binary_path, app_bundle_path) + key = (basename, rel_path) + if key not in seen: + seen.add(key) + known.append({'name': basename, 'path': rel_path}) + + for f in files: + if not f.endswith('.dylib'): + continue + dylib_path = os.path.join(root, f) + basename = f.replace('.dylib', '') + rel_path = os.path.relpath(dylib_path, app_bundle_path) + key = (basename, rel_path) + if key not in seen: + seen.add(key) + known.append({'name': basename, 'path': rel_path}) + + known.sort(key=lambda x: (x['name'], x['path'])) + return known + + +def compute_digest(entries, secret): + """对排序后的白名单内容计算 HMAC-SHA256 摘要""" + parts = [f"{e['name']}|{e['path']}" for e in entries] + message = '\n'.join(parts).encode('utf-8') + return hmac.new(secret, message, hashlib.sha256).hexdigest() + + +def generate_swift_secret(secret_bytes): + """将构建期密钥写入 Swift 源码""" + hex_bytes = ', '.join(f'0x{b:02x}' for b in secret_bytes) + source = f"""// Auto-generated by generate_image_whitelist.py — do not edit manually +import Foundation + +enum RDWhitelistSecret {{ + static let key: [UInt8] = [{hex_bytes}] +}} +""" + os.makedirs(os.path.dirname(SECRET_OUTPUT), exist_ok=True) + with open(SECRET_OUTPUT, 'w') as f: + f.write(source) + + +def load_existing_secret(): + if not os.path.isfile(SECRET_OUTPUT): + return None + with open(SECRET_OUTPUT, 'r', encoding='utf-8') as f: + content = f.read() + hex_values = re.findall(r'0x([0-9a-fA-F]{2})', content) + if not hex_values: + return None + return bytes(int(value, 16) for value in hex_values) + + +def resolve_app_bundle_path(): + target_build_dir = os.environ.get('TARGET_BUILD_DIR') + wrapper_name = os.environ.get('WRAPPER_NAME') + if target_build_dir and wrapper_name: + app_bundle_path = os.path.join(target_build_dir, wrapper_name) + if os.path.isdir(app_bundle_path): + return app_bundle_path + return None + + +def main(): + root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + os.chdir(root) + + build_secret = load_existing_secret() + if build_secret is None: + build_secret = secrets.token_bytes(32) + generate_swift_secret(build_secret) + print(f'[whitelist] Generated {SECRET_OUTPUT}') + + app_bundle_path = resolve_app_bundle_path() + executable_path = os.environ.get('EXECUTABLE_PATH', 'readoor') + if app_bundle_path: + frameworks = scan_bundled_images(app_bundle_path, executable_path) + print(f'[whitelist] Scanned app bundle images from {app_bundle_path}') + else: + frameworks = scan_source_frameworks(FRAMEWORK_SEARCH_PATHS, 'readoor') + print('[whitelist] App bundle not found, fallback to source framework scan') + + digest = compute_digest(frameworks, build_secret) + + output = { + 'known_images': frameworks, + 'digest': digest + } + + if app_bundle_path: + destination = os.path.join(app_bundle_path, os.path.basename(PLIST_OUTPUT)) + os.makedirs(os.path.dirname(destination), exist_ok=True) + with open(destination, 'wb') as f: + plistlib.dump(output, f) + print(f'[whitelist] Generated app bundle plist {destination} with {len(frameworks)} dynamic entries') + else: + os.makedirs(os.path.dirname(PLIST_OUTPUT), exist_ok=True) + with open(PLIST_OUTPUT, 'wb') as f: + plistlib.dump(output, f) + print(f'[whitelist] Generated {PLIST_OUTPUT} with {len(frameworks)} dynamic entries') + + print(f'[whitelist] digest={digest[:16]}...') + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/backend/automation/obfuscation/obfuscate_symbols.py b/backend/automation/obfuscation/obfuscate_symbols.py new file mode 100644 index 0000000..7c04da4 --- /dev/null +++ b/backend/automation/obfuscation/obfuscate_symbols.py @@ -0,0 +1,4293 @@ +#!/usr/bin/env python3 +""" +obfuscate_symbols.py — Swift 类型名符号混淆工具 +用法: + python3 AutoPacking/obfuscation/obfuscate_symbols.py --scan-dir readoor/3.0 # 仅预览,不修改 + python3 AutoPacking/obfuscation/obfuscate_symbols.py --scan-dir readoor/3.0 --apply # 真正替换 + python3 AutoPacking/obfuscation/obfuscate_symbols.py --deobfuscate crash.log # 还原 crash 符号 +""" + +import os +import re +import json +import hashlib +import argparse +import sys +import secrets +import time +import shutil +import tempfile +import multiprocessing +import subprocess +from concurrent.futures import ThreadPoolExecutor, as_completed +from pathlib import Path + +from obfuscation_runtime.canonical import ( + build_canonical_symbol_tables, + build_method_mapping_from_canonical_table, + build_type_mappings_from_canonical_table, +) +from obfuscation_runtime.discovery import discover_files, prefilter_files_with_rg +from obfuscation_runtime.json_cache import IncrementalAnalysisCache, load_json_file, save_json_file + +# ── 配置 ────────────────────────────────────────────────────────────────────── + +# 扫描这些目录(相对于脚本所在目录)。默认改为项目根以覆盖整个仓库, +# 也可通过命令行的 --scan-dir 覆盖为更精确的子目录。 +DEFAULT_SCAN_DIRS = ["."] + +# 排除目录(含这些路径片段的文件跳过) +EXCLUDE_PATH_FRAGMENTS = [ + ".build/", + ".git/", + "Pods/", + "Carthage/", + "Tests/", + "readoorTests/", + "DerivedData/", + "RDObfuscateMacro/", # 我们自己的宏包,不混淆 + ".framework/", + ".xcframework/", +] +THIRD_PARTY_PROTOCOL_EXCLUDE_PATH_FRAGMENTS = [ + frag for frag in EXCLUDE_PATH_FRAGMENTS + if frag not in {"Pods/", ".framework/", ".xcframework/"} +] + +# 目录名白名单:完全匹配这些目录名则在提取阶段忽略(例如第三方/工具目录) +EXCLUDE_DIR_NAMES = ("Factorys",) +METHOD_REPLACEMENT_SKIP_DIR_NAMES = ("Pods",) +# 始终保留(白名单)的符号,完全匹配 +SYMBOL_WHITELIST = { + # AppKit / UIKit 入口 + "AppDelegate", "SceneDelegate", + # 常见系统协议实现类型 + "Error", "Result", "Event", + # Swift 内建类型 + "String", "Int", "Bool", "Double", "Float", "Data", "URL", "Date", + "Array", "Dictionary", "Set", "Optional", + # JSON 编解码保留(必须与 JSON key 名一致,混淆会导致解码失败) + "CodingKeys", + # StoreKit / 内购 相关类型(保留,避免混淆第三方/系统 IAP 协议/模型) + "SKProduct", "SKProductsRequest", "SKProductsResponse", "SKPayment", "SKPaymentQueue", + "SKPaymentTransaction", "SKPaymentTransactionObserver", "SKProductsRequestDelegate", + "SKReceiptRefreshRequest", "SKStoreProductViewController", "SKMutablePayment", + # StoreKit 2 及 App Store 相关通用类型 + "Product", "Transaction", "StoreKit", "AppStore", +} + +# 前缀白名单:含这些前缀的符号跳过(第三方/系统) +SYMBOL_PREFIX_WHITELIST = ("NS", "UI", "AV", "CL", "MK", "SK", "CG", "CA", "WK") + +# 一些语言/运行时保留标识符,绝对不能被替换(例如 Objective-C/Swift 的 `self`/`super` 等) +RESERVED_IDENTIFIERS = { + # Obj-C instance/reference keywords + "self", "super", + # Obj-C literals / common macros + "nil", "NULL", "YES", "NO", + # Swift / JSON booleans + "true", "false", +} + +# 仅混淆这些 Swift 声明关键字(保守策略,先不动 func/var) +DECL_KEYWORDS = ("class", "struct", "enum", "protocol") + +# 混淆后名称前缀(让 crash 还原时还能快速识别来源) +# 类型名使用 YMH;方法名使用 ymh,避免 Swift 导入 Obj-C 时出现 +# “'YMHxxxx()' has been renamed to 'ymhxxxx()'” 提示。 +TYPE_OBFUSCATED_PREFIX = "YMH" +METHOD_OBFUSCATED_PREFIX = "ymh" +OBFUSCATED_PREFIX = TYPE_OBFUSCATED_PREFIX +# 混淆哈希长度(hex 字符数)。增大此值可显著降低不同运行间重复概率。 +OBFUSCATED_HASH_LEN = 10 +LEGACY_OBFUSCATED_HASH_LENGTHS = (6, 8, 10) +ALREADY_OBFUSCATED_PREFIXES = (TYPE_OBFUSCATED_PREFIX, METHOD_OBFUSCATED_PREFIX) + +# 仅对这些文件名前缀进行混淆(基于文件名,不含扩展) +# 例如 Swift 文件 `RDWereadOAuthWebViewController.swift` 会匹配 'RD' +SWIFT_FILENAME_PREFIXES = ("RD", "RB") +# 对 Obj-C 文件匹配的前缀(例如 `STS... .m/.h`) +OBJC_FILENAME_PREFIXES = ("STS", "RB") + +# 仅混淆这些符号名前缀(避免在 RD 文件中误混淆第三方或通用类) +SWIFT_SYMBOL_PREFIXES = ("RD", "RB") +OBJC_SYMBOL_PREFIXES = ("STS", "RB") + +# 映射表保存路径 +MAP_OUTPUT_PATH = "obfuscation_map.json" +APPLY_STATE_PATH = ".obfuscate_apply_state.json" +SCRIPT_DIR = Path(__file__).resolve().parent +PROJECT_METHOD_IGNORE_FILE_PATH = str(SCRIPT_DIR / "system_methods_ignore.txt") +AUTO_DETECTED_METHOD_IGNORE_BEGIN = "# BEGIN AUTO-DETECTED-OBJC-DOT-SYNTAX" +AUTO_DETECTED_METHOD_IGNORE_END = "# END AUTO-DETECTED-OBJC-DOT-SYNTAX" +SOURCE_FILE_EXTENSIONS = (".swift", ".m", ".mm", ".h", ".pch") +# 方法映射输出(dry-run) +METHOD_MAP_OUTPUT_PATH = "tmp_method_map.json" +DEFAULT_WORKERS = max(1, min(8, os.cpu_count() or 4)) +IOS_SYSTEM_SYMBOL_CACHE_PATH = ".ios_system_symbols_cache.json" +IOS_SYSTEM_SYMBOL_CACHE_VERSION = 2 +EXTRACTION_CACHE_PATH = ".obfuscate_extraction_cache.json" +EXTRACTION_CACHE_VERSION = 3 +IOS_SYSTEM_SYMBOL_SCAN_DIRS = ( + "System/Library/Frameworks", + "usr/include", +) +THIRD_PARTY_PROTOCOL_ROOT_NAMES = ("Pods", "Factorys") +METHOD_CANDIDATE_PREFIXES = ("STS", "RD", "RB") +METHOD_CANDIDATE_EXTENSIONS = (".swift", ".m", ".mm", ".h") +RG_DISCOVERY_EXTENSIONS = SOURCE_FILE_EXTENSIONS +RG_CHUNK_SIZE = 256 +PROJECT_SOURCE_ANALYSIS_KIND = "project_source" +THIRD_PARTY_MEMBER_ANALYSIS_KIND = "third_party_member" +_EXTRACTION_ANALYSIS_CACHE = IncrementalAnalysisCache(EXTRACTION_CACHE_PATH, EXTRACTION_CACHE_VERSION) + +TYPE_DECLARATION_RG_PATTERN = r"\b(?:class|struct|enum|protocol)\s+[A-Za-z_]|@(?:interface|implementation|protocol)\s+[A-Za-z_]" +METHOD_DECLARATION_RG_PATTERN = r"\bfunc\s+[A-Za-z_]|^[[:space:]]*[+-][[:space:]]*\(" +PROPERTY_DECLARATION_RG_PATTERN = r"@property\b|\b(?:var|let)\s+[A-Za-z_]" +SELECTOR_LITERAL_RG_PATTERN = r'@"[A-Za-z_][A-Za-z0-9_:@]*"' +OBJC_DOT_SYNTAX_MEMBER_RG_PATTERN = r"\.\s*[A-Za-z_]" + +# 常见系统成员名兜底(即使 SDK 扫描失败,也不要混淆这些高风险名字) +COMMON_SYSTEM_MEMBER_NAMES = { + # 来源: Apple Developer Documentation (常用 UIKit / Delegate / DataSource / WebKit / UserNotifications API) + # - https://developer.apple.com/documentation/uikit/uiviewcontroller + # - https://developer.apple.com/documentation/uikit/uiview + # - https://developer.apple.com/documentation/uikit/uislider + # - https://developer.apple.com/documentation/uikit/uiscrollviewdelegate + # - https://developer.apple.com/documentation/uikit/uitableviewdelegate + # - https://developer.apple.com/documentation/uikit/uitableviewdatasource + # - https://developer.apple.com/documentation/uikit/uicollectionviewdelegate + # - https://developer.apple.com/documentation/uikit/uicollectionviewdatasource + # - https://developer.apple.com/documentation/uikit/uiapplicationdelegate + # - https://developer.apple.com/documentation/uikit/uiscenedelegate + # - https://developer.apple.com/documentation/usernotifications/unusernotificationcenterdelegate + # - https://developer.apple.com/documentation/uikit/uigesturerecognizerdelegate + # - https://developer.apple.com/documentation/webkit/wkwebview + # - https://developer.apple.com/documentation/webkit/wknavigationdelegate + + # 基本 Obj-C/Swift setter/getter 名 + "set", + "get", + + # UIViewController lifecycle / layout + "viewDidLoad", "viewWillAppear", "viewDidAppear", "viewWillDisappear", "viewDidDisappear", + "viewWillLayoutSubviews", "viewDidLayoutSubviews", "viewSafeAreaInsetsDidChange", + "viewWillLayoutSubviews", "viewDidLayoutSubviews", "viewWillLayoutSubviews", + "didReceiveMemoryWarning", "traitCollectionDidChange", "awakeFromNib", "deinit", + "prepareForSegue", "loadView", "loadViewIfNeeded", "setNeedsUpdateProperties", + + # UIView / view hierarchy + "layoutSubviews", "layoutIfNeeded", "setNeedsLayout", "setNeedsDisplay", + "addSubview", "removeFromSuperview", "willMoveToSuperview", "didMoveToSuperview", + "willMoveToWindow", "didMoveToWindow", "didAddSubview", "willRemoveSubview", + "draw", "drawRect", "setNeedsDisplay", "tintColorDidChange", + "systemLayoutSizeFittingSize", "systemLayoutSizeFittingSize:", + "systemLayoutSizeFitting", "systemLayoutSizeFitting(_:withHorizontalFittingPriority:verticalFittingPriority:)", + + # UIResponder / touch handling + "touchesBegan", "touchesMoved", "touchesEnded", "touchesCancelled", + + # Gesture recognizer + "gestureRecognizerShouldBegin", "gestureRecognizer", "gestureRecognizer:shouldReceiveTouch:", + "gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:)", + + # Scroll / Zoom delegate + "scrollViewDidScroll", "scrollViewWillBeginDragging", "scrollViewWillEndDragging", + "scrollViewDidEndDragging", "scrollViewDidEndDecelerating", "scrollViewDidEndScrollingAnimation", + "scrollViewDidZoom", "scrollViewWillBeginZooming", "viewForZooming(in:)", + "scrollViewDidChangeAdjustedContentInset", + + # TableView delegate / datasource (常见签名关键词) + "tableView", "didSelectRowAt", "didDeselectRowAt", "willSelectRowAt", "willDeselectRowAt", + "cellForRowAt", "numberOfRowsInSection", "numberOfSections", "heightForRowAt", "estimatedHeightForRowAt", + "willDisplayCell", "didEndDisplayingCell", "commit", "canEditRowAt", "moveRowAt", + "accessoryButtonTappedForRowWith", "leadingSwipeActionsConfigurationForRowAt", "trailingSwipeActionsConfigurationForRowAt", + + # CollectionView delegate / datasource + "collectionView", "cellForItemAt", "numberOfItemsInSection", "didSelectItemAt", "didDeselectItemAt", + "willDisplayCellForItemAt", "didEndDisplayingCellForItemAt", "targetContentOffsetForProposedContentOffset", + + # App / Scene lifecycle + "application:didFinishLaunchingWithOptions:", "application:open:options:", + "applicationDidBecomeActive", "applicationWillResignActive", "applicationDidEnterBackground", + "applicationWillEnterForeground", "applicationWillTerminate", + "application:didRegisterForRemoteNotificationsWithDeviceToken:", + "application:didFailToRegisterForRemoteNotificationsWithError:", + "application:didReceiveRemoteNotification:fetchCompletionHandler:", + + # Scene delegate + "scene(_:willConnectTo:options:)", "sceneDidDisconnect", "sceneWillEnterForeground", + "sceneDidBecomeActive", "sceneWillResignActive", "sceneDidEnterBackground", + + # Notifications / UNUserNotificationCenterDelegate + "userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:", + "userNotificationCenter:willPresentNotification:withCompletionHandler:", + "userNotificationCenter:openSettingsFor:", + + # WebKit / WKNavigationDelegate + "webView(_:decidePolicyFor:decisionHandler:)", "webView(_:didStartProvisionalNavigation:)", + "webView(_:didFinish:)", "webView(_:didFail:withError:)", + + # WKWebView documented instance methods (avoid obfuscating WebKit entry points used directly in app code) + "loadHTMLString", "loadFileURL", "loadFileRequest", "loadSimulatedRequest", + "reloadFromOrigin", "stopLoading", "goBack", "goForward", "go(to:)", + "evaluateJavaScript", "callAsyncJavaScript", "takeSnapshot", "createPDF", + "createWebArchiveData", "startDownload", "resumeDownload", "setMagnification", + "pauseAllMediaPlayback", "requestMediaPlaybackState", "setAllMediaPlaybackSuspended", + "closeAllMediaPresentations", "setCameraCaptureState", "setMicrophoneCaptureState", + "find", "setMinimumViewportInset", "fetchData", "restoreData", + + # UISlider documented methods and subclass override points + "setValue", "minimumTrackImage", "setMinimumTrackImage", + "maximumTrackImage", "setMaximumTrackImage", "thumbImage", "setThumbImage", + "minimumValueImageRect", "maximumValueImageRect", "trackRect", "thumbRect", + + # StoreKit / In-App Purchase callbacks (Obj-C selectors / Swift method names) + "paymentQueue:updatedTransactions:", "paymentQueueRestoreCompletedTransactionsFinished", + "paymentQueue:restoreCompletedTransactionsFailedWithError:", "productsRequest:didReceiveResponse:", + "request:didFailWithError:", "paymentQueue:removedTransactions:", "paymentQueue:updatedDownloads:", + + # Text input delegates + "textFieldShouldBeginEditing", "textFieldDidBeginEditing", "textFieldShouldReturn", + "textField(_:shouldChangeCharactersIn:replacementString:)", + "textViewDidChange", "textViewDidBeginEditing", "textViewDidEndEditing", + + # Common focus / state restoration hooks + "encodeRestorableState", "decodeRestorableState", "applicationFinishedRestoringState", + "archivedValue", + "responds", "forwardingTarget", "respondsToSelector", "forwardingTargetForSelector:", + + # SwiftUI lightweight hooks (common closure names) + "onAppear", "onDisappear", + # Project-specific API exemptions + "consigneeCreate", "consigneeUpdate", + "addRightHandleClick", + "topModel", + "topPageVc", + "modelList", + "bookTypeToItemType", + "vocabularyArray", + "letterArray", +} + +# UINavigationController 常用方法/属性(来源:Apple Developer Documentation) +COMMON_SYSTEM_MEMBER_NAMES.update({ + # Properties + "topViewController", "visibleViewController", "viewControllers", + "navigationBar", "toolbar", "isNavigationBarHidden", "isToolbarHidden", + "interactivePopGestureRecognizer", "interactiveContentPopGestureRecognizer", + "hidesBarsOnTap", "hidesBarsOnSwipe", "hidesBarsWhenVerticallyCompact", + "hidesBarsWhenKeyboardAppears", "barHideOnTapGestureRecognizer", "barHideOnSwipeGestureRecognizer", + "hideShowBarDuration", + + # Swift-style method bases + "pushViewController", "popViewController", "popToViewController", "popToRootViewController", + "setViewControllers", "setNavigationBarHidden", "setToolbarHidden", "show", "showDetailViewController", + + # Obj-C selectors / Obj-C-style names (保守兼容) + "pushViewController:animated:", "popViewControllerAnimated:", "popToViewController:animated:", + "popToRootViewControllerAnimated:", "setViewControllers:animated:", + "setNavigationBarHidden:animated:", "setToolbarHidden:animated:", +}) + +IOS_SYSTEM_MEMBER_NAMES = set() +IOS_SYSTEM_SYMBOLS_LOADED = False +# If enabled, allow rewriting `#import "X.h"` lines and rename corresponding header files. +# This is risky: filenames may diverge from disk unless headers are renamed accordingly. +REWRITE_IMPORTS_ALLOWED = False +# Pods / Factorys 中声明过的第三方成员名。保守处理:这些名字一律不参与方法混淆, +# 以避免继承第三方类、实现第三方 Delegate/DataSource 时把回调或重写方法改坏。 +THIRD_PARTY_PROTOCOL_METHOD_NAMES = set() +THIRD_PARTY_PROTOCOL_METHODS_LOADED = False +PROJECT_PROPERTY_MEMBER_NAMES = set() +PROJECT_PROPERTY_MEMBERS_LOADED = False +PROJECT_METHOD_IGNORE_NAMES = set() +PROJECT_METHOD_IGNORE_PATTERNS = [] + +OBJC_PROPERTY_DECL_RE = re.compile(r"@property\b(?P.*?);", re.S) +OBJC_PROPERTY_GETTER_ATTR_RE = re.compile(r"\bgetter\s*=\s*([A-Za-z_][A-Za-z0-9_]*)") +OBJC_PROPERTY_SETTER_ATTR_RE = re.compile(r"\bsetter\s*=\s*([A-Za-z_][A-Za-z0-9_]*:)") +OBJC_SYNTHESIZE_IVAR_RE = re.compile(r"@synthesize\s+[A-Za-z_][A-Za-z0-9_]*\s*=\s*_([A-Za-z_][A-Za-z0-9_]*)\s*;") +SWIFT_INTERFACE_FUNC_RE = re.compile(r"\bfunc\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(") +SWIFT_INTERFACE_PROPERTY_RE = re.compile(r"\b(?:var|let)\s+([A-Za-z_][A-Za-z0-9_]*)\s*:") +SWIFT_PROPERTY_DECL_RE = re.compile( + r"^\s*(?:@[A-Za-z_][A-Za-z0-9_]*(?:\([^\n]*?\))?\s*)*" + r"(?:(?:public|private|fileprivate|internal|open|final|weak|unowned|lazy|static|class|override|required|optional|dynamic|mutating|nonmutating|convenience|indirect)\s+)*" + r"(?:var|let)\s+([A-Za-z_][A-Za-z0-9_]*)\b", + re.M, +) +THIRD_PARTY_PROTOCOL_NAME_RE = re.compile(r"(?:Delegate|DataSource)\b", re.I) +SWIFT_PROTOCOL_DECL_RE = re.compile(r"\bprotocol\s+([A-Za-z_][A-Za-z0-9_]*)\b[^\{]*\{") + +# ── 工具函数 ────────────────────────────────────────────────────────────────── + +def short_hash(name: str, salt: str) -> str: + """生成可复现的伪随机 ID,长度由 OBFUSCATED_HASH_LEN 控制。""" + h = hashlib.sha256(f"{salt}{name}".encode()).hexdigest()[:OBFUSCATED_HASH_LEN] + return h + +def obfuscated_name(original: str, salt: str, nonce: int = 0, prefix: str = TYPE_OBFUSCATED_PREFIX) -> str: + # nonce 用于极端哈希冲突时重试,保证本次映射唯一 + payload = original if nonce == 0 else f"{original}:{nonce}" + return f"{prefix}{short_hash(payload, salt)}" + +def build_unique_mapping(symbols: list, salt: str, prefix: str = TYPE_OBFUSCATED_PREFIX) -> dict: + mapping = {} + used_obf = set() + for name in symbols: + nonce = 0 + while True: + obf = obfuscated_name(name, salt, nonce, prefix=prefix) + if obf not in used_obf: + mapping[name] = obf + used_obf.add(obf) + break + nonce += 1 + return mapping + +def should_exclude_path(path: str) -> bool: + return any(frag in path for frag in EXCLUDE_PATH_FRAGMENTS) + +def collect_swift_files(scan_dirs: list) -> list: + return discover_files(scan_dirs, (".swift",), EXCLUDE_PATH_FRAGMENTS, skip_dir_names=EXCLUDE_DIR_NAMES) + +def collect_objc_files(scan_dirs: list) -> list: + return discover_files(scan_dirs, (".m", ".mm", ".h"), EXCLUDE_PATH_FRAGMENTS, skip_dir_names=EXCLUDE_DIR_NAMES) + +def collect_source_files(scan_dirs: list) -> list: + """收集所有可恢复源码文件,不受 RD/STS 文件名前缀限制。""" + return discover_files(scan_dirs, SOURCE_FILE_EXTENSIONS, EXCLUDE_PATH_FRAGMENTS) + +def collect_method_candidate_files(scan_dirs: list) -> list: + """仅收集用于方法提取的候选文件:文件名以 STS 或 RD 开头,忽略 Factorys 等目录。""" + files = discover_files( + scan_dirs, + METHOD_CANDIDATE_EXTENSIONS, + EXCLUDE_PATH_FRAGMENTS, + skip_dir_names=EXCLUDE_DIR_NAMES, + basename_prefixes=METHOD_CANDIDATE_PREFIXES, + ) + return prefilter_files_with_rg(files, METHOD_DECLARATION_RG_PATTERN, chunk_size=RG_CHUNK_SIZE) + +def collect_apply_target_files(scan_dirs: list, enable_objc_symbols: bool) -> list: + """收集实际执行替换的源码文件。 + + 定义提取仍可只看 RD/STS 前缀文件,但符号引用可能出现在任意业务源码文件中, + 因此替换阶段需要覆盖全部源码文件,否则会遗漏 String+xxx 之类的引用文件。 + """ + return discover_files(scan_dirs, SOURCE_FILE_EXTENSIONS, EXCLUDE_PATH_FRAGMENTS) + +def collect_resource_strings(scan_dirs: list) -> set: + """扫描 storyboard/xib 等资源,收集 class/outlet/selector 等字符串作为白名单。""" + strs = set() + pattern_attr = re.compile(r'(?:customClass|customModule|placeholder|storyboardIdentifier|reuseIdentifier|userLabel|selector)\s*=\s*"([A-Za-z_][A-Za-z0-9_:@]*)"') + pattern_tag = re.compile(r'<[^>]+\bclass\s*=\s*"([A-Za-z_][A-Za-z0-9_]*)"') + resource_files = discover_files(scan_dirs, ('.storyboard', '.xib', '.xml'), EXCLUDE_PATH_FRAGMENTS) + for full in resource_files: + try: + text = Path(full).read_text(encoding='utf-8') + except Exception: + continue + for m in pattern_attr.finditer(text): + strs.add(m.group(1)) + for m in pattern_tag.finditer(text): + strs.add(m.group(1)) + return strs + + +def _discover_third_party_protocol_roots(scan_dirs: list) -> list: + roots = set() + excluded_dir_names = {"Pods", ".git", ".build", "Carthage", "Tests", "readoorTests", "DerivedData", "RDObfuscateMacro"} + + search_roots = {str(Path(os.getcwd()).resolve())} + for scan_dir in scan_dirs: + try: + resolved_scan_dir = Path(scan_dir).resolve() + except Exception: + continue + if resolved_scan_dir.exists(): + search_roots.add(str(resolved_scan_dir)) + + for search_root in sorted(search_roots): + base_path = Path(search_root) + if not base_path.exists(): + continue + if base_path.name in THIRD_PARTY_PROTOCOL_ROOT_NAMES: + roots.add(str(base_path.resolve())) + for root, dirs, _ in os.walk(base_path): + current_root = Path(root) + for directory in list(dirs): + if directory in THIRD_PARTY_PROTOCOL_ROOT_NAMES: + roots.add(str((current_root / directory).resolve())) + dirs[:] = [ + directory for directory in dirs + if directory not in excluded_dir_names and directory not in THIRD_PARTY_PROTOCOL_ROOT_NAMES + ] + + return sorted(roots) + + +def _iter_protocol_source_files(root_dirs: list): + seen = set() + candidates = discover_files(root_dirs, (".h", ".swift"), THIRD_PARTY_PROTOCOL_EXCLUDE_PATH_FRAGMENTS) + prefetched = prefilter_files_with_rg(candidates, PROPERTY_DECLARATION_RG_PATTERN + "|" + METHOD_DECLARATION_RG_PATTERN) + for file_path in prefetched: + full_path = os.path.realpath(file_path) + if full_path in seen: + continue + seen.add(full_path) + yield file_path + + +def _extract_swift_property_members(text: str) -> set: + return {match.group(1) for match in SWIFT_PROPERTY_DECL_RE.finditer(text)} + + +def _empty_project_source_analysis() -> dict: + return { + "swift_symbols": [], + "objc_symbols": [], + "raw_direct_names": [], + "raw_objc_selectors": [], + "raw_swift_signatures": {}, + "property_members": [], + "selector_literals": [], + } + + +def _empty_third_party_member_analysis() -> dict: + return { + "member_names": [], + } + + +def load_extraction_cache() -> dict: + return _EXTRACTION_ANALYSIS_CACHE.load_state() + + +def save_extraction_cache(): + _EXTRACTION_ANALYSIS_CACHE.save_if_dirty() + + +def _build_analysis_cache_key(analysis_kind: str, path: str) -> str: + return IncrementalAnalysisCache.build_cache_key(analysis_kind, path) + + +def _read_text_with_hash(path: str): + return IncrementalAnalysisCache._read_text_with_hash(path) + + +def _get_cached_file_analysis(path: str, analysis_kind: str, analyzer): + return _EXTRACTION_ANALYSIS_CACHE.get_or_compute(path, analysis_kind, analyzer) + + +def _analyze_project_source_text(text: str, path: str) -> dict: + analysis = _empty_project_source_analysis() + if not text: + return analysis + + if path.endswith(".swift"): + analysis["swift_symbols"] = sorted(_extract_swift_symbols_from_text(text)) + direct_names, swift_signatures = _extract_swift_methods_from_text(text) + analysis["raw_direct_names"] = sorted(direct_names) + analysis["raw_swift_signatures"] = swift_signatures + analysis["property_members"] = sorted(_extract_swift_property_members(text)) + return analysis + + if path.endswith((".h", ".m", ".mm", ".pch")): + direct_names, objc_selectors = _extract_objc_methods_from_text(text) + analysis["objc_symbols"] = sorted(_extract_objc_symbols_from_text(text)) + analysis["raw_direct_names"] = sorted(direct_names) + analysis["raw_objc_selectors"] = sorted(objc_selectors) + analysis["property_members"] = sorted(_extract_objc_property_members(text)) + analysis["selector_literals"] = sorted(_collect_selector_literals_from_text(text)) + + return analysis + + +def _analyze_third_party_member_text(text: str, path: str) -> dict: + analysis = _empty_third_party_member_analysis() + if not text: + return analysis + + if path.endswith(".swift"): + analysis["member_names"] = sorted(_extract_delegate_protocol_methods_from_swift_text(text)) + else: + analysis["member_names"] = sorted(_extract_delegate_protocol_methods_from_objc_text(text)) + return analysis + + +def ensure_project_property_members_loaded(scan_dirs: list) -> set: + global PROJECT_PROPERTY_MEMBERS_LOADED + + if PROJECT_PROPERTY_MEMBERS_LOADED: + return PROJECT_PROPERTY_MEMBER_NAMES + + property_members = set() + source_files = collect_apply_target_files(scan_dirs, enable_objc_symbols=True) + property_files = prefilter_files_with_rg(source_files, PROPERTY_DECLARATION_RG_PATTERN) + for file_path in property_files: + analysis = _get_cached_file_analysis(file_path, PROJECT_SOURCE_ANALYSIS_KIND, _analyze_project_source_text) + property_members.update(analysis.get("property_members", [])) + + PROJECT_PROPERTY_MEMBER_NAMES.clear() + PROJECT_PROPERTY_MEMBER_NAMES.update(property_members) + PROJECT_PROPERTY_MEMBERS_LOADED = True + save_extraction_cache() + return PROJECT_PROPERTY_MEMBER_NAMES + + +def _protocol_name_is_delegate_like(name: str) -> bool: + return bool(THIRD_PARTY_PROTOCOL_NAME_RE.search(name or "")) + + +def _extract_delegate_protocol_methods_from_objc_text(text: str) -> set: + protected = set() + protected.update(_extract_objc_property_members(text)) + + for method_match in OBJC_METHOD_DECL_RE.finditer(text): + declaration = method_match.group(1).strip() + selector_parts = re.findall(r'([A-Za-z_][A-Za-z0-9_]*:)', declaration) + if selector_parts: + # Obj-C 带参数方法在 Swift 中实现时通常会以首段 selector 作为 base name, + # 例如 `tencentDidNotLogin:` -> `func tencentDidNotLogin(_:)`。 + # 这里同时保护完整 selector 和 Swift 可见的 base name, + # 避免实现第三方 Delegate/DataSource 或重写第三方类方法时被误混淆。 + protected.add("".join(selector_parts)) + protected.add(selector_parts[0][:-1]) + continue + + simple_match = re.match(r'([A-Za-z_][A-Za-z0-9_]*)', declaration) + if simple_match: + protected.add(simple_match.group(1)) + + return protected + + +def _extract_delegate_protocol_methods_from_swift_text(text: str) -> set: + protected = set(_extract_swift_property_members(text)) + for method_match in SWIFT_METHOD_DECL_RE.finditer(text): + protected.add(method_match.group(1)) + + return protected + + +def ensure_third_party_protocol_methods_loaded(scan_dirs: list) -> set: + global THIRD_PARTY_PROTOCOL_METHODS_LOADED + + if THIRD_PARTY_PROTOCOL_METHODS_LOADED: + return THIRD_PARTY_PROTOCOL_METHOD_NAMES + + # 保守策略:Pods / Factorys 里声明过的成员名全部视为受保护, + # 这样第三方基类重写、第三方 Delegate / DataSource 实现都不会进入方法映射。 + protected = set() + protocol_roots = _discover_third_party_protocol_roots(scan_dirs) + for file_path in _iter_protocol_source_files(protocol_roots): + analysis = _get_cached_file_analysis(file_path, THIRD_PARTY_MEMBER_ANALYSIS_KIND, _analyze_third_party_member_text) + protected.update(analysis.get("member_names", [])) + + THIRD_PARTY_PROTOCOL_METHOD_NAMES.clear() + THIRD_PARTY_PROTOCOL_METHOD_NAMES.update(protected) + THIRD_PARTY_PROTOCOL_METHODS_LOADED = True + save_extraction_cache() + return THIRD_PARTY_PROTOCOL_METHOD_NAMES + + +def _path_contains_dir_name(path: str, dir_names: tuple) -> bool: + wanted = {name.lower() for name in dir_names} + return any(part.lower() in wanted for part in Path(path).parts) + + +def should_skip_method_replacements(path: str) -> bool: + return _path_contains_dir_name(path, METHOD_REPLACEMENT_SKIP_DIR_NAMES) + + +def _run_command_capture_stdout(args: list) -> str: + try: + completed = subprocess.run( + args, + capture_output=True, + text=True, + check=True, + ) + except Exception: + return "" + return (completed.stdout or "").strip() + + +def _discover_ios_sdk_paths() -> list: + sdk_paths = [] + + env_sdk_root = os.environ.get("SDKROOT") + if env_sdk_root and os.path.exists(env_sdk_root): + sdk_paths.append(os.path.realpath(env_sdk_root)) + + for sdk_name in ("iphoneos", "iphonesimulator"): + sdk_path = _run_command_capture_stdout(["xcrun", "--sdk", sdk_name, "--show-sdk-path"]) + if sdk_path and os.path.exists(sdk_path): + sdk_paths.append(os.path.realpath(sdk_path)) + + unique_paths = [] + seen = set() + for sdk_path in sdk_paths: + if sdk_path in seen: + continue + seen.add(sdk_path) + unique_paths.append(sdk_path) + return unique_paths + + +def _load_cached_ios_system_symbols(sdk_paths: list): + payload = load_json_file(IOS_SYSTEM_SYMBOL_CACHE_PATH, {}) + if not isinstance(payload, dict): + return None + + cache_version = payload.get("cache_version") + cached_sdk_paths = payload.get("sdk_paths") + cached_symbols = payload.get("symbols") + if cache_version != IOS_SYSTEM_SYMBOL_CACHE_VERSION or cached_sdk_paths != sdk_paths or not isinstance(cached_symbols, list): + return None + + return set(symbol for symbol in cached_symbols if isinstance(symbol, str) and symbol) + + +def _save_ios_system_symbols_cache(sdk_paths: list, symbols: set): + payload = { + "cache_version": IOS_SYSTEM_SYMBOL_CACHE_VERSION, + "sdk_paths": sdk_paths, + "symbols": sorted(symbols), + } + try: + save_json_file(IOS_SYSTEM_SYMBOL_CACHE_PATH, payload) + except Exception: + pass + + +def _iter_ios_sdk_symbol_files(sdk_path: str): + seen = set() + sdk_root = Path(sdk_path) + for relative_dir in IOS_SYSTEM_SYMBOL_SCAN_DIRS: + base_dir = sdk_root / relative_dir + if not base_dir.exists(): + continue + for root, dirs, files in os.walk(base_dir): + dirs[:] = [directory for directory in dirs if not directory.endswith(".docc")] + for filename in files: + if not filename.endswith((".h", ".swiftinterface")): + continue + full_path = os.path.realpath(os.path.join(root, filename)) + if full_path in seen: + continue + seen.add(full_path) + yield full_path + + +def _default_objc_setter_name(property_name: str) -> str: + if property_name.startswith("is") and len(property_name) > 2 and property_name[2].isupper(): + setter_base = property_name[2:] + else: + setter_base = property_name[:1].upper() + property_name[1:] + return f"set{setter_base}:" + + +def _extract_objc_property_members(text: str) -> set: + members = set() + for match in OBJC_PROPERTY_DECL_RE.finditer(text): + body = " ".join(match.group("body").split()) + if not body: + continue + + name_match = re.search(r"([A-Za-z_][A-Za-z0-9_]*)\s*$", body) + if not name_match: + continue + + property_name = name_match.group(1) + members.add(property_name) + + getter_match = OBJC_PROPERTY_GETTER_ATTR_RE.search(body) + if getter_match: + members.add(getter_match.group(1)) + else: + members.add(property_name) + + if "readonly" in body: + continue + + setter_match = OBJC_PROPERTY_SETTER_ATTR_RE.search(body) + if setter_match: + members.add(setter_match.group(1)) + else: + members.add(_default_objc_setter_name(property_name)) + + for match in OBJC_SYNTHESIZE_IVAR_RE.finditer(text): + property_name = match.group(1) + members.add(property_name) + members.add(_default_objc_setter_name(property_name)) + + return members + + +def _extract_objc_sdk_members(text: str) -> set: + members = set() + members.update(_extract_objc_property_members(text)) + + for match in OBJC_METHOD_DECL_RE.finditer(text): + declaration = match.group(1).strip() + selector_parts = re.findall(r'([A-Za-z_][A-Za-z0-9_]*:)', declaration) + if selector_parts: + members.add("".join(selector_parts)) + continue + + simple_match = re.match(r'([A-Za-z_][A-Za-z0-9_]*)', declaration) + if simple_match: + members.add(simple_match.group(1)) + + return members + + +def _extract_swift_sdk_members(text: str) -> set: + members = set() + for match in SWIFT_INTERFACE_FUNC_RE.finditer(text): + members.add(match.group(1)) + for match in SWIFT_INTERFACE_PROPERTY_RE.finditer(text): + members.add(match.group(1)) + return members + + +def _extract_ios_system_symbols_from_sdk(sdk_path: str) -> set: + members = set(COMMON_SYSTEM_MEMBER_NAMES) + for file_path in _iter_ios_sdk_symbol_files(sdk_path): + try: + text = Path(file_path).read_text(encoding="utf-8", errors="ignore") + except Exception: + continue + + if file_path.endswith(".swiftinterface"): + members.update(_extract_swift_sdk_members(text)) + else: + members.update(_extract_objc_sdk_members(text)) + + return members + + +def ensure_ios_system_symbols_loaded(verbose: bool = True) -> set: + global IOS_SYSTEM_SYMBOLS_LOADED + + if IOS_SYSTEM_SYMBOLS_LOADED: + return IOS_SYSTEM_MEMBER_NAMES + + sdk_paths = _discover_ios_sdk_paths() + symbols = _load_cached_ios_system_symbols(sdk_paths) + if symbols is None: + symbols = set(COMMON_SYSTEM_MEMBER_NAMES) + for sdk_path in sdk_paths: + symbols.update(_extract_ios_system_symbols_from_sdk(sdk_path)) + if sdk_paths: + _save_ios_system_symbols_cache(sdk_paths, symbols) + + IOS_SYSTEM_MEMBER_NAMES.clear() + IOS_SYSTEM_MEMBER_NAMES.update(symbols) + IOS_SYSTEM_SYMBOLS_LOADED = True + + if verbose: + if sdk_paths: + print(f"🛡️ 已加载 {len(IOS_SYSTEM_MEMBER_NAMES)} 个 iOS SDK 系统方法/属性名") + else: + print("⚠️ 未找到 iOS SDK,当前仅使用内置系统方法保护列表") + + return IOS_SYSTEM_MEMBER_NAMES + +# ── 提取阶段 ───────────────────────────────────────────────────────────────── + +# 匹配声明前修饰符(注解行或同行),用于过滤不应混淆的符号 +UNSAFE_MODIFIERS_RE = re.compile( + r"@objc\b|@objcMembers\b|@IBAction\b|@IBOutlet\b|@IBInspectable\b" + r"|@NSManaged\b|@NSApplicationMain\b|@UIApplicationMain\b" + r"|\bopen\b|\bpublic\b" +) + +# 捕获类型声明: [class|struct|enum|protocol] TypeName +DECL_RE = re.compile( + r"(? re.Pattern: + lengths = sorted(set(LEGACY_OBFUSCATED_HASH_LENGTHS)) + suffix = "|".join(f"[a-f0-9]{{{length}}}" for length in lengths) + prefixes = "|".join(re.escape(prefix) for prefix in sorted(set(ALREADY_OBFUSCATED_PREFIXES), key=len, reverse=True)) + return re.compile(rf"^(?:{prefixes})(?:{suffix})$") + + +ALREADY_OBFUSCATED_RE = build_already_obfuscated_re() + +# 方法抽取正则(Swift / Obj-C) +SWIFT_FUNC_RE = re.compile(r"^\s*(?:@[^\n]+\s*)*(?:public|private|fileprivate|internal|open|static|class|override|final|mutating|nonmutating|async|throws|rethrows|lazy|weak|unowned|convenience|required|optional|override|protocol|extension|extension\s+|actor|struct|enum|actor\s+)?\s*func\s+([a-zA-Z_][a-zA-Z0-9_]+)") +SWIFT_METHOD_DECL_RE = re.compile(r"\bfunc\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(") +SWIFT_PARAM_LABEL_RE = re.compile( + r"^\s*(?:@[A-Za-z_][A-Za-z0-9_.]*(?:\([^)]*\))?\s+)*" + r"(?:(?:inout|borrowing|consuming|isolated|__shared|__owned|sending)\s+)*" + r"(?P