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