From 266d19ed275161468b13eb1bd2d5be326bcceb69 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 | 44 +++++-- .../romainguy/kotlin/explorer/Disassembly.kt | 89 +------------ .../kotlin/explorer/KotlinExplorer.kt | 29 ++++- .../dev/romainguy/kotlin/explorer/State.kt | 17 ++- .../kotlin/explorer/dex/DexDumpParser.kt | 121 ++++++++++++++++++ 6 files changed, 194 insertions(+), 107 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..0d4fd5a2 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 @@ -29,10 +29,11 @@ internal fun updateTextArea(textArea: RSyntaxTextArea, text: String) { textArea.caretPosition = minOf(position, textArea.document.length) } -class DexTextArea : RSyntaxTextArea() { +class DexTextArea(private val explorerState: ExplorerState) : RSyntaxTextArea() { private var displayJump = false private var jumpRange = 0 to 0 private var horizontalOffsets = 0 to 0 + private var fullText = "" init { addCaretListener { event -> @@ -49,9 +50,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/KotlinExplorer.kt b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/KotlinExplorer.kt index 2bcf0050..72234b0a 100644 --- a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/KotlinExplorer.kt +++ b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/KotlinExplorer.kt @@ -74,7 +74,7 @@ private fun FrameWindowScope.KotlinExplorer( ) { // TODO: Move all those remembers to an internal private state object var sourceTextArea by remember { mutableStateOf(null) } - var dexTextArea by remember { mutableStateOf(null) } + var dexTextArea by remember { mutableStateOf(null) } var oatTextArea by remember { mutableStateOf(null) } var activeTextArea by remember { mutableStateOf(null) } var status by remember { mutableStateOf("Ready") } @@ -121,7 +121,14 @@ private fun FrameWindowScope.KotlinExplorer( MainMenu( explorerState, sourceTextArea, - { dex -> updateTextArea(dexTextArea!!, dex) }, + { dex -> + if (dex != null) { + updateTextArea(dexTextArea!!, dex) + } else { + dexTextArea?.refreshText() + } + + }, { oat -> updateTextArea(oatTextArea!!, oat) }, { statusUpdate -> status = statusUpdate }, { findDialog.isVisible = true }, @@ -183,7 +190,7 @@ private fun FrameWindowScope.KotlinExplorer( SwingPanel( modifier = Modifier.fillMaxSize(), factory = { - dexTextArea = DexTextArea().apply { + dexTextArea = DexTextArea(explorerState).apply { configureSyntaxTextArea(SyntaxConstants.SYNTAX_STYLE_NONE) addFocusListener(focusTracker) } @@ -247,7 +254,7 @@ private fun FrameWindowScope.KotlinExplorer( private fun FrameWindowScope.MainMenu( explorerState: ExplorerState, sourceTextArea: RSyntaxTextArea?, - onDexUpdate: (String) -> Unit, + onDexUpdate: (String?) -> Unit, onOatUpdate: (String) -> Unit, onStatusUpdate: (String) -> Unit, onFindClicked: () -> Unit, @@ -296,6 +303,20 @@ private fun FrameWindowScope.MainMenu( ), onCheckedChange = { explorerState.presentationMode = it } ) + CheckboxItem( + "Show Line Numbers Mode", + explorerState.showLineNumbers, + shortcut = KeyShortcut( + key = Key.L, + ctrl = !isMac, + shift = true, + meta = isMac + ), + onCheckedChange = { + explorerState.showLineNumbers = it + onDexUpdate(null) + } + ) } Menu("Compilation") { CheckboxItem( diff --git a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/State.kt b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/State.kt index 8192cfca..ea32ff7f 100644 --- a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/State.kt +++ b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/State.kt @@ -26,16 +26,18 @@ import java.nio.file.Paths import kotlin.io.path.exists import kotlin.io.path.readLines -private const val OPTIMIZE = "OPTIMIZE" -private const val PRESENTATION = "PRESENTATION" +private const val Optimize = "OPTIMIZE" +private const val Presentation = "PRESENTATION" +private const val ShowLineNumbers = "SHOW_LINE_NUMBERS" @Stable class ExplorerState( val settings: Settings = Settings() ) { var toolPaths by mutableStateOf(createToolPaths(settings)) - var optimize by mutableStateOf(settings.getValue(OPTIMIZE, "true").toBoolean()) - var presentationMode by mutableStateOf(settings.getValue(PRESENTATION, "false").toBoolean()) + var optimize by mutableStateOf(settings.getValue(Optimize, "true").toBoolean()) + var presentationMode by mutableStateOf(settings.getValue(Presentation, "false").toBoolean()) + var showLineNumbers by mutableStateOf(settings.getValue(ShowLineNumbers, "true").toBoolean()) var sourceCode: String = readSourceCode(toolPaths) fun reloadToolPathsFromSettings() { @@ -80,9 +82,10 @@ fun writeState(state: ExplorerState) { state.toolPaths.sourceFile, state.sourceCode ) - state.settings.entries[OPTIMIZE] = state.optimize.toString() - state.settings.entries[PRESENTATION] = state.presentationMode.toString() - + state.settings.entries[Optimize] = state.optimize.toString() + state.settings.entries[Presentation] = state.presentationMode.toString() + state.settings.entries[ShowLineNumbers] = state.showLineNumbers.toString() + Files.writeString( state.settings.file, state.settings.entries 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('\'') +}