From 7d0370db486cb57b17bd72cbeda8d0ef16f7944e Mon Sep 17 00:00:00 2001 From: Alon Albert Date: Tue, 30 Apr 2024 18:18:03 -0700 Subject: [PATCH] Add Parser --- build.gradle.kts | 1 + .../romainguy/kotlin/explorer/DexTextArea.kt | 23 ++-- .../romainguy/kotlin/explorer/Disassembly.kt | 89 +------------ .../kotlin/explorer/dex/DexDumpParser.kt | 121 ++++++++++++++++++ 4 files changed, 139 insertions(+), 95 deletions(-) create mode 100644 src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/dex/DexDumpParser.kt diff --git a/build.gradle.kts b/build.gradle.kts index dc75d320..40f24195 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -37,6 +37,7 @@ kotlin { implementation("net.java.dev.jna:jna:${extra["jna.version"] as String}") implementation("androidx.collection:collection:${extra["collections.version"] as String}") implementation("org.jetbrains.androidx.lifecycle:lifecycle-runtime:${extra["lifecycle.version"] as String}") + runtimeOnly("org.jetbrains.skiko:skiko-awt-runtime-linux-x64:${extra["skiko.version"] as String}") } } } diff --git a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/DexTextArea.kt b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/DexTextArea.kt index 0460f4b8..2d6bb50d 100644 --- a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/DexTextArea.kt +++ b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/DexTextArea.kt @@ -20,8 +20,8 @@ import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea import java.awt.Graphics import java.awt.Graphics2D -private val JumpPattern = Regex("(\\s+)[0-9a-fA-F]{4}: .+([0-9a-fA-F]{4}) // ([+-])[0-9a-fA-F]{4}[\\n\\r]*") -private val AddressedPattern = Regex("(\\s+)([0-9a-fA-F]{4}): .+[\\n\\r]*") +private val JumpPattern = Regex(".{9}[0-9a-fA-F]{4}: .+([0-9a-fA-F]{4}) // ([+-])[0-9a-fA-F]{4}[\\n\\r]*") +private val AddressedPattern = Regex(".{9}([0-9a-fA-F]{4}): .+[\\n\\r]*") internal fun updateTextArea(textArea: RSyntaxTextArea, text: String) { val position = textArea.caretPosition @@ -49,9 +49,9 @@ class DexTextArea : RSyntaxTextArea() { var line = document.getText(start, end - start) var result = JumpPattern.matchEntire(line) if (result != null) { - val srcHorizontalOffset = start + result.groupValues[1].length - val targetAddress = result.groupValues[2] - val direction = if (result.groupValues[3] == "+") 1 else -1 + val srcHorizontalOffset = start + line.countPadding() + val targetAddress = result.groupValues[1] + val direction = if (result.groupValues[2] == "+") 1 else -1 var dstLine = srcLine + direction while (dstLine in 0.. path.toFile().delete() } } -private val BuiltInKotlinClass = Regex("^(kotlin|kotlinx|java|javax|org\\.(intellij|jetbrains))\\..+") - -private val DexCodePattern = Regex("^[0-9a-fA-F]+:[^|]+\\|([0-9a-fA-F]+: .+)") -private val DexMethodStartPattern = Regex("^\\s+#[0-9]+\\s+:\\s+\\(in L[^;]+;\\)") -private val DexMethodNamePattern = Regex("^\\s+name\\s+:\\s+'(.+)'") -private val DexMethodTypePattern = Regex("^\\s+type\\s+:\\s+'(.+)'") -private val DexClassNamePattern = Regex("^\\s+Class descriptor\\s+:\\s+'L(.+);'") +internal val BuiltInKotlinClass = Regex("^(kotlin|kotlinx|java|javax|org\\.(intellij|jetbrains))\\..+") private val OatClassNamePattern = Regex("^\\d+: L([^;]+); \\(offset=[0-9a-zA-Zx]+\\) \\(type_idx=\\d+\\).+") private val OatMethodPattern = Regex("^\\s+\\d+:\\s+(.+)\\s+\\(dex_method_idx=\\d+\\)") @@ -319,83 +314,7 @@ private fun filterOat(oat: String) = buildString { } } -private fun filterDex(dex: String) = buildString { - val indent = " " - val lines = dex.lineSequence().iterator() - - var insideClass = false - var insideMethod = false - var firstMethod = false - var firstClass = true - var className = "" - - while (lines.hasNext()) { - var line = lines.next() - - var match: MatchResult? = null - - if (insideClass) { - if (insideMethod) { - match = DexCodePattern.matchEntire(line) - if (match != null && match.groupValues.isNotEmpty()) { - appendLine("$indent${match.groupValues[1]}") - } - } - - if (match === null) { - match = DexMethodStartPattern.matchEntire(line) - if (match != null) { - if (!lines.hasNext()) return@buildString - line = lines.next() - - match = DexMethodNamePattern.matchEntire(line) - if (match != null && match.groupValues.isNotEmpty()) { - val name = match.groupValues[1] - if (!firstMethod) appendLine() - firstMethod = false - - if (!lines.hasNext()) return@buildString - line = lines.next() - - match = DexMethodTypePattern.matchEntire(line) - if (match != null && match.groupValues.isNotEmpty()) { - val type = match.groupValues[1] - appendLine(" $name$type // $className.$name()") - insideMethod = true - } - } - } - } - } - - if (match === null) { - if (line.trim().startsWith("Class #")) { - if (!lines.hasNext()) return@buildString - line = lines.next() - - match = DexClassNamePattern.matchEntire(line) - if (match != null && match.groupValues.isNotEmpty()) { - className = match.groupValues[1].replace('/', '.') - - val suppress = className.matches(BuiltInKotlinClass) - if (!suppress) { - if (!firstClass) appendLine() - appendLine("class $className") - } - - if (!lines.consumeUntil("Direct methods")) break - - insideMethod = false - firstMethod = true - firstClass = false - insideClass = !suppress - } - } - } - } -} - -private fun Iterator.consumeUntil(prefix: String): Boolean { +internal fun Iterator.consumeUntil(prefix: String): Boolean { while (hasNext()) { val line = next() if (line.trim().startsWith(prefix)) return true diff --git a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/dex/DexDumpParser.kt b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/dex/DexDumpParser.kt new file mode 100644 index 00000000..0d8d4673 --- /dev/null +++ b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/dex/DexDumpParser.kt @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2023 Romain Guy + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.romainguy.kotlin.explorer.dex + +import dev.romainguy.kotlin.explorer.BuiltInKotlinClass +import dev.romainguy.kotlin.explorer.consumeUntil + +private val PositionRegex = Regex("^\\s*0x(?
[0-9a-f]+) line=(?\\d+)$") + +private const val ClassStart = "Class #" +private const val ClassEnd = "source_file_idx" +private const val ClassName = "Class descriptor" +private const val Instructions = "insns size" +private const val Positions = "positions" + +internal class DexDumpParser(text: String) { + private val lines = text.lineSequence().iterator() + fun parseDexDump(): String { + val classes = buildList { + while (lines.consumeUntil(ClassStart)) { + add(readClass()) + } + } + return classes + .filter { it.methods.isNotEmpty() && !it.name.matches(BuiltInKotlinClass)} + .joinToString(separator = "\n") { it.toString() } + } + + private fun readClass(): DexClass { + val className = lines.next().getClassName() + val methods = buildList { + while (lines.hasNext()) { + val line = lines.next().trim() + when { + line.startsWith(ClassEnd) -> break + line.startsWith(Instructions) -> add(readMethod(className)) + } + } + } + return DexClass(className, methods) + } + + private fun readMethod(className: String): DexMethod { + val (name, type) = lines.next().substringAfterLast(".").split(':', limit = 2) + val instructions = readInstructions() + lines.consumeUntil(Positions) + val positions = readPositions() + val code = buildString { + instructions.forEach { + val lineNumber = positions[it.address] + val prefix = if (lineNumber != null) "%3s: ".format(lineNumber) else " " + append(" $prefix${it.address}: ${it.code}\n") + } + } + return DexMethod(className, name, type, code) + } + + private fun readInstructions(): List { + return buildList { + while (lines.hasNext()) { + val line = lines.next() + if (line.startsWith(" ")) { + break + } + val (address, code) = line.substringAfter('|').split(": ", limit = 2) + add(DexInstruction(address, code)) + } + } + } + + private fun readPositions(): Map { + return buildMap { + while (lines.hasNext()) { + val line = lines.next() + val match = PositionRegex.matchEntire(line) ?: break + put(match.getValue("address"), match.getValue("line").toInt()) + } + } + } + + private class DexClass(val name: String, val methods: List) { + override fun toString() = "class $name\n${methods.joinToString("\n\n") { it.toString() }}" + } + + private class DexMethod(val className: String, val name: String, val type: String, val code: String) { + override fun toString() = " $name$type // $className.$name()\n$code" + } + + private class DexInstruction(val address: String, val code: String) +} + +private fun MatchResult.getValue(group: String): String { + return groups[group]?.value ?: throw IllegalStateException("Value of $group not found in $value") +} + +private fun String.getClassName() = + getValue(ClassName) + .removePrefix("L") + .removeSuffix(";") + .replace('/', '.') + +private fun String.getValue(name: String): String { + if (!trim().startsWith(name)) { + throw IllegalStateException("Expected '$name'") + } + return substringAfter('\'').substringBefore('\'') +}