/** * Android ProGuard / R8 mapping.txt 解析器 * * 标准 ProGuard 格式: * com.example.MyClass -> a.b.c: * void myMethod(int) -> a * * R8 内联格式(行号范围 + 原始类名): * 13:22:java.lang.Object com.example.Foo.bar():141:141 -> invokeSuspend */ export interface ProguardMapping { obfuscatedClass: string originalClass: string methods: MethodMapping[] lines: LineMapping[] } export interface MethodMapping { obfuscatedMethod: string originalMethod: string obfuscatedLine: number originalLine: number } export interface LineMapping { obfuscatedLineStart: number obfuscatedLineEnd: number originalLine: number originalFile: string // R8 内联:原始类名和方法名(与 obfuscatedClass 可能不同) originalClass?: string originalMethod?: string } /** * 解析 ProGuard / R8 mapping.txt 文件 */ export function parseProguardMapping(content: string): Map { const mappings = new Map() let currentClass: ProguardMapping | null = null let currentOriginalClass = '' let currentObfuscatedClass = '' const lines = content.split('\n') for (const line of lines) { // 跳过空行和注释(含 R8 的 # {...} 行) if (!line.trim() || line.startsWith('#')) continue // 类映射行:com.example.MyClass -> a.b.c: const classMatch = line.match(/^(\S+)\s+->\s+(\S+):$/) if (classMatch) { currentOriginalClass = classMatch[1] currentObfuscatedClass = classMatch[2].replace(/:$/, '') currentClass = { obfuscatedClass: currentObfuscatedClass, originalClass: currentOriginalClass, methods: [], lines: [], } mappings.set(currentObfuscatedClass, currentClass) continue } if (!currentClass) continue // R8 内联行:startLine:endLine:returnType OrigClass.method(args):origStart:origEnd -> obfuscatedMethod // 示例: 13:22:java.lang.Object com.szyx.app.ywx.Foo$Bar.invokeSuspend(java.lang.Object):141:141 -> invokeSuspend const r8InlineMatch = line.match( /^\s+(\d+):(\d+):(\S+)\s+([\w$.]+)\.([\w$]+)\(([^)]*)\):(\d+):(\d+)\s+->\s+(\S+)$/ ) if (r8InlineMatch) { const obfStart = parseInt(r8InlineMatch[1], 10) const obfEnd = parseInt(r8InlineMatch[2], 10) const originalClass = r8InlineMatch[4] const originalMethod = r8InlineMatch[5] const origLine = parseInt(r8InlineMatch[7], 10) currentClass.lines.push({ obfuscatedLineStart: obfStart, obfuscatedLineEnd: obfEnd, originalLine: origLine, originalFile: currentOriginalClass, originalClass, originalMethod, }) continue } // ProGuard 行号映射(无原始类信息):startLine:endLine:desc:origStart:origEnd -> obfuscatedMethod const lineMatch = line.match( /^\s+(\d+):(\d+):([^:]+):(\d+):(\d+)\s+->\s+(\S+)$/ ) if (lineMatch) { currentClass.lines.push({ obfuscatedLineStart: parseInt(lineMatch[1], 10), obfuscatedLineEnd: parseInt(lineMatch[2], 10), originalLine: parseInt(lineMatch[4], 10), originalFile: currentOriginalClass, }) continue } // 方法映射行:[startLine:endLine:]returnType methodName(args) -> obfuscatedName const methodMatch = line.match( /^\s+(?:(\d+):(\d+):)?(\S+)\s+(\S+)\(([^)]*)\)\s+->\s+(\S+)$/ ) if (methodMatch) { currentClass.methods.push({ obfuscatedMethod: methodMatch[6], originalMethod: methodMatch[4], obfuscatedLine: methodMatch[1] ? parseInt(methodMatch[1], 10) : 0, originalLine: methodMatch[2] ? parseInt(methodMatch[2], 10) : 0, }) } } return mappings } /** * 使用 mapping 反混淆堆栈帧 */ export function deobfuscateFrame( mappings: Map, obfuscatedClass: string, obfuscatedMethod: string, obfuscatedLine: number ): { class: string; method: string; line: number; file: string } | null { const mapping = mappings.get(obfuscatedClass) if (!mapping) return null // 优先:在行号范围内查找 R8 内联条目 const r8Entry = mapping.lines.find( l => l.originalClass !== undefined && l.originalMethod === obfuscatedMethod && obfuscatedLine >= l.obfuscatedLineStart && obfuscatedLine <= l.obfuscatedLineEnd ) if (r8Entry && r8Entry.originalClass) { const cls = r8Entry.originalClass return { class: cls, method: r8Entry.originalMethod!, line: r8Entry.originalLine, file: `${cls.split('.').pop()}.kt`, } } // 次选:行号范围条目(无内联类信息) const lineEntry = mapping.lines.find( l => obfuscatedLine >= l.obfuscatedLineStart && obfuscatedLine <= l.obfuscatedLineEnd ) if (lineEntry) { const methodEntry = mapping.methods.find(m => m.obfuscatedMethod === obfuscatedMethod) const cls = mapping.originalClass return { class: cls, method: methodEntry?.originalMethod || obfuscatedMethod, line: lineEntry.originalLine, file: `${cls.split('.').pop()}.kt`, } } // 兜底:仅匹配方法名 const methodMapping = mapping.methods.find(m => m.obfuscatedMethod === obfuscatedMethod) const cls = mapping.originalClass return { class: cls, method: methodMapping?.originalMethod || obfuscatedMethod, line: methodMapping?.originalLine || obfuscatedLine, file: `${cls.split('.').pop()}.kt`, } } /** * 反混淆整个堆栈字符串 */ export function deobfuscateStack( mappings: Map, stacktrace: string ): string { const lines = stacktrace.split('\n') const result: string[] = [] for (const line of lines) { // 匹配 "at ClassName.method(File:line)" 格式。 // File 部分允许连字符,以兼容 R8 的 r8-map-id-xxx 格式。 const match = line.match( /at\s+([\w.$]+)\.([\w$]+)\(([^:)]+):(\d+)\)/ ) if (match) { const obfuscatedClass = match[1] const obfuscatedMethod = match[2] const obfuscatedLine = parseInt(match[4], 10) const deobfuscated = deobfuscateFrame( mappings, obfuscatedClass, obfuscatedMethod, obfuscatedLine ) if (deobfuscated) { result.push( `\tat ${deobfuscated.class}.${deobfuscated.method}(${deobfuscated.file}:${deobfuscated.line})` ) continue } } // 无法反混淆,保留原始行 result.push(line) } return result.join('\n') }