diff --git a/build.gradle.kts b/build.gradle.kts index 40f24195..fcd11d9e 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}") + implementation("com.google.guava:guava:${extra["guava"]}") runtimeOnly("org.jetbrains.skiko:skiko-awt-runtime-linux-x64:${extra["skiko.version"] as String}") } } diff --git a/gradle.properties b/gradle.properties index 9f07fdff..a9958021 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,3 +9,4 @@ jewel.version=0.17.3 jna.version=5.13.0 collections.version=1.4.0 lifecycle.version=2.8.0-alpha01 +guava=33.2.0-jre \ No newline at end of file diff --git a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Disassembly.kt b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Disassembly.kt index 10a91b7b..79ee0dba 100644 --- a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Disassembly.kt +++ b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Disassembly.kt @@ -16,18 +16,27 @@ package dev.romainguy.kotlin.explorer +import dev.romainguy.kotlin.explorer.code.CodeContent +import dev.romainguy.kotlin.explorer.code.CodeContent.Error import dev.romainguy.kotlin.explorer.dex.DexDumpParser -import kotlinx.coroutines.* +import dev.romainguy.kotlin.explorer.oat.OatDumpParser +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.launch import java.nio.file.Files import java.nio.file.Path import java.util.stream.Collectors import kotlin.io.path.extension +private val dexDumpParser = DexDumpParser() +private val oatDumpParser = OatDumpParser() + suspend fun disassemble( toolPaths: ToolPaths, source: String, - onDex: (String) -> Unit, - onOat: (String) -> Unit, + onDex: (CodeContent) -> Unit, + onOat: (CodeContent) -> Unit, onStatusUpdate: (String) -> Unit, optimize: Boolean ) = coroutineScope { @@ -49,7 +58,7 @@ suspend fun disassemble( if (kotlinc.exitCode != 0) { launch(ui) { - onDex(kotlinc.output.replace(path.parent.toString() + "/", "")) + onDex(Error(kotlinc.output.replace(path.parent.toString() + "/", ""))) onStatusUpdate("Ready") } return@launch @@ -72,7 +81,7 @@ suspend fun disassemble( if (r8.exitCode != 0) { launch(ui) { - onDex(r8.output) + onDex(Error(r8.output)) onStatusUpdate("Ready") } return@launch @@ -89,14 +98,14 @@ suspend fun disassemble( if (dexdump.exitCode != 0) { launch(ui) { - onDex(dexdump.output) + onDex(Error(dexdump.output)) onStatusUpdate("Ready") } return@launch } launch(ui) { - onDex(DexDumpParser(dexdump.output).parseDexDump()) + onDex(dexDumpParser.parse(dexdump.output)) onStatusUpdate("AOT compilation…") } @@ -110,7 +119,7 @@ suspend fun disassemble( if (push.exitCode != 0) { launch(ui) { - onOat(push.output) + onOat(Error(push.output)) onStatusUpdate("Ready") } return@launch @@ -127,7 +136,7 @@ suspend fun disassemble( if (dex2oat.exitCode != 0) { launch(ui) { - onOat(dex2oat.output) + onOat(Error(dex2oat.output)) onStatusUpdate("Ready") } return@launch @@ -143,7 +152,7 @@ suspend fun disassemble( directory = directory ) - launch(ui) { onOat(filterOat(oatdump.output)) } + launch(ui) { onOat(oatDumpParser.parse(oatdump.output)) } if (oatdump.exitCode != 0) { launch(ui) { onStatusUpdate("Ready") } @@ -251,73 +260,3 @@ private fun cleanupClasses(directory: Path) { 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+\\)") -private val OatCodePattern = Regex("^\\s+(0x[a-zA-Z0-9]+):\\s+[a-zA-Z0-9]+\\s+(.+)") - -private fun filterOat(oat: String) = buildString { - val indent = " " - val lines = oat.lineSequence().iterator() - - if (!lines.consumeUntil("OatDexFile:")) return@buildString - - var insideClass = false - var insideMethod = false - var firstMethod = false - var firstClass = true - - while (lines.hasNext()) { - val line = lines.next() - - var match: MatchResult? = null - - if (insideClass) { - if (insideMethod) { - match = OatCodePattern.matchEntire(line) - if (match != null && match.groupValues.isNotEmpty()) { - appendLine("$indent${match.groupValues[1]}: ${match.groupValues[2]}") - } - } - - if (match === null) { - match = OatMethodPattern.matchEntire(line) - if (match != null && match.groupValues.isNotEmpty()) { - val name = match.groupValues[1] - if (!firstMethod) appendLine() - firstMethod = false - - appendLine(" $name") - - if (!lines.consumeUntil("CODE: ")) break - insideMethod = true - } - } - } - - if (match === null) { - match = OatClassNamePattern.matchEntire(line) - if (match != null && match.groupValues.isNotEmpty()) { - val className = match.groupValues[1].replace('/', '.') - - val suppress = className.matches(BuiltInKotlinClass) - if (!suppress) { - if (!firstClass) appendLine() - appendLine("class $className") - } - - insideMethod = false - firstMethod = true - firstClass = false - insideClass = !suppress - } - } - } -} - -internal fun Iterator.consumeUntil(prefix: String): Boolean { - while (hasNext()) { - val line = next() - if (line.trim().startsWith(prefix)) return true - } - return false -} diff --git a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/KotlinExplorer.kt b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/KotlinExplorer.kt index b115f938..5cdd7c79 100644 --- a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/KotlinExplorer.kt +++ b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/KotlinExplorer.kt @@ -36,8 +36,10 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.window.* import dev.romainguy.kotlin.explorer.Shortcut.Ctrl import dev.romainguy.kotlin.explorer.Shortcut.CtrlShift -import dev.romainguy.kotlin.explorer.dex.DexTextArea -import dev.romainguy.kotlin.explorer.oat.OatTextArea +import dev.romainguy.kotlin.explorer.code.CodeBuilder.LineNumberMode.FixedWidth +import dev.romainguy.kotlin.explorer.code.CodeBuilder.LineNumberMode.None +import dev.romainguy.kotlin.explorer.code.CodeContent +import dev.romainguy.kotlin.explorer.code.CodeTextArea import kotlinx.coroutines.launch import org.fife.rsta.ui.search.FindDialog import org.fife.rsta.ui.search.SearchEvent @@ -55,7 +57,10 @@ import org.jetbrains.jewel.intui.window.decoratedWindow import org.jetbrains.jewel.intui.window.styling.dark import org.jetbrains.jewel.intui.window.styling.light import org.jetbrains.jewel.ui.ComponentStyling -import org.jetbrains.jewel.ui.component.* +import org.jetbrains.jewel.ui.component.DefaultButton +import org.jetbrains.jewel.ui.component.Icon +import org.jetbrains.jewel.ui.component.Text +import org.jetbrains.jewel.ui.component.TextField import org.jetbrains.jewel.window.DecoratedWindow import org.jetbrains.jewel.window.TitleBar import org.jetbrains.jewel.window.newFullscreenControls @@ -67,6 +72,8 @@ import javax.swing.SwingUtilities private const val FontSizeEditingMode = 12.0f private const val FontSizePresentationMode = 20.0f +private const val LINE_NUMBER_WIDTH = 4 +private const val CODE_INDENT = 4 @Composable private fun FrameWindowScope.KotlinExplorer( @@ -125,22 +132,23 @@ private fun FrameWindowScope.KotlinExplorer( val oatPanel: @Composable () -> Unit = { TextPanel("OAT", oatTextArea, explorerState) } var panels by remember { mutableStateOf(explorerState.getPanels(sourcePanel, dexPanel, oatPanel)) } + val updatePresentationMode: (Boolean) -> Unit = { + listOf(dexTextArea, oatTextArea).forEach { area -> area.presentationMode = it } + } + val updateShowLineNumbers: (Boolean) -> Unit = { dexTextArea.lineNumberMode = it.toLineNumberMode() } + MainMenu( explorerState, sourceTextArea, - { dex -> - if (dex != null) { - updateTextArea(dexTextArea, dex) - } else { - dexTextArea.refreshText() - } - }, - { oat -> updateTextArea(oatTextArea, oat) }, + { dex -> dexTextArea.content = dex }, + { oat -> oatTextArea.content = oat }, { statusUpdate -> status = statusUpdate }, { findDialog.isVisible = true }, { SearchEngine.find(activeTextArea, findDialog.searchContext) }, { showSettings = true }, { panels = explorerState.getPanels(sourcePanel, dexPanel, oatPanel) }, + updateShowLineNumbers, + updatePresentationMode, ) if (showSettings) { @@ -232,15 +240,18 @@ private fun sourceTextArea(focusTracker: FocusListener, explorerState: ExplorerS } } -private fun dexTextArea(explorerState: ExplorerState, focusTracker: FocusListener): DexTextArea { - return DexTextArea(explorerState).apply { - configureSyntaxTextArea(SyntaxConstants.SYNTAX_STYLE_NONE) - addFocusListener(focusTracker) - } -} +private fun dexTextArea(state: ExplorerState, focusTracker: FocusListener) = + codeTextArea(state, focusTracker) + +private fun oatTextArea(state: ExplorerState, focusTracker: FocusListener) = + codeTextArea(state, focusTracker, hasLineNumbers = false) -private fun oatTextArea(explorerState: ExplorerState, focusTracker: FocusListener): RSyntaxTextArea { - return OatTextArea(explorerState).apply { +private fun codeTextArea( + state: ExplorerState, + focusTracker: FocusListener, + hasLineNumbers: Boolean = true +): CodeTextArea { + return CodeTextArea(state.presentationMode, CODE_INDENT, hasLineNumbers.toLineNumberMode()).apply { configureSyntaxTextArea(SyntaxConstants.SYNTAX_STYLE_NONE) addFocusListener(focusTracker) } @@ -250,13 +261,15 @@ private fun oatTextArea(explorerState: ExplorerState, focusTracker: FocusListene private fun FrameWindowScope.MainMenu( explorerState: ExplorerState, sourceTextArea: RSyntaxTextArea, - onDexUpdate: (String?) -> Unit, - onOatUpdate: (String) -> Unit, + onDexUpdate: (CodeContent) -> Unit, + onOatUpdate: (CodeContent) -> Unit, onStatusUpdate: (String) -> Unit, onFindClicked: () -> Unit, onFindNextClicked: () -> Unit, onOpenSettings: () -> Unit, onPanelsUpdated: () -> Unit, + onShowLineNumberChanged: (Boolean) -> Unit, + onPresentationModeChanged: (Boolean) -> Unit, ) { val scope = rememberCoroutineScope() @@ -285,10 +298,12 @@ private fun FrameWindowScope.MainMenu( MenuCheckboxItem("Show DEX", Ctrl(D), explorerState::showDex, onShowPanelChanged) MenuCheckboxItem("Show OAT", Ctrl(O), explorerState::showOat, onShowPanelChanged) MenuCheckboxItem("Show Line Numbers", CtrlShift(L), explorerState::showLineNumbers) { - onDexUpdate(null) + onShowLineNumberChanged(it) } Separator() - MenuCheckboxItem("Presentation Mode", CtrlShift(P), explorerState::presentationMode) + MenuCheckboxItem("Presentation Mode", CtrlShift(P), explorerState::presentationMode) { + onPresentationModeChanged(it) + } } Menu("Compilation") { MenuCheckboxItem("Optimize with R8", CtrlShift(O), explorerState::optimize) @@ -406,11 +421,7 @@ private fun RSyntaxTextArea.updateStyle(explorerState: ExplorerState) { font = font.deriveFont(if (presentation) FontSizePresentationMode else FontSizeEditingMode) } -private fun updateTextArea(textArea: RSyntaxTextArea, text: String) { - val position = textArea.caretPosition - textArea.text = text - textArea.caretPosition = minOf(position, textArea.document.length) -} +private fun Boolean.toLineNumberMode() = if (this) FixedWidth(LINE_NUMBER_WIDTH) else None fun main() = application { val explorerState = remember { ExplorerState() } diff --git a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Regex.kt b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Regex.kt index 62d15896..eda5154c 100644 --- a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Regex.kt +++ b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Regex.kt @@ -16,6 +16,8 @@ package dev.romainguy.kotlin.explorer +const val HexDigit = "[0-9a-fA-F]" + fun MatchResult.getValue(group: String): String { return groups[group]?.value ?: throw IllegalStateException("Value of $group not found in $value") } diff --git a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/String.kt b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/String.kt new file mode 100644 index 00000000..bb8292db --- /dev/null +++ b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/String.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * 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 + +fun Iterator.consumeUntil(prefix: String): Boolean { + while (hasNext()) { + val line = next() + if (line.trim().startsWith(prefix)) return true + } + return false +} + +fun Iterator.consumeUntil(regex: Regex): MatchResult? { + while (hasNext()) { + val line = next() + val match = regex.matchEntire(line) + if (match != null) { + return match + } + } + return null +} diff --git a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/code/Code.kt b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/code/Code.kt new file mode 100644 index 00000000..38a6dcb2 --- /dev/null +++ b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/code/Code.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2024 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.code + +import dev.romainguy.kotlin.explorer.code.CodeBuilder.LineNumberMode + +/** + * A data model representing disassembled code + * + * Given a list [Class]'s constructs a mode that provides: + * * Disassembled text with optional line number annotations + * * Jump information for branch instructions + */ +class Code(private val classes: List, indent: Int, lineNumberMode: LineNumberMode) { + private val jumps: Map + + val text: String + + init { + val code = buildCode(indent, lineNumberMode) { + classes.forEach { clazz -> + startClass(clazz) + clazz.methods.forEach { method -> + startMethod(method) + method.instructions.forEach { instruction -> + writeInstruction(instruction) + } + endMethod() + } + } + } + text = code.toString() + jumps = code.getJumps() + } + + fun getJumpTargetOfLine(line: Int) = jumps[line] +} diff --git a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/code/CodeBuilder.kt b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/code/CodeBuilder.kt new file mode 100644 index 00000000..5a3058fc --- /dev/null +++ b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/code/CodeBuilder.kt @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2024 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.code + +import dev.romainguy.kotlin.explorer.code.CodeBuilder.LineNumberMode +import dev.romainguy.kotlin.explorer.code.CodeBuilder.LineNumberMode.FixedWidth + +fun buildCode(indent: Int, lineNumberMode: LineNumberMode, builderAction: CodeBuilder.() -> Unit): CodeBuilder { + return CodeBuilder(indent, lineNumberMode).apply(builderAction) +} + +/** + * Builds a [Code] model + * + * This class maintains a state allowing it to build the `jump` table `line-number` list + * that will be used by the UI to display jump markers and line number annotations. + */ +class CodeBuilder( + private val indent: Int, + private val lineNumberMode: LineNumberMode, +) { + private var line = 0 + private val sb = StringBuilder() + private val jumps = mutableMapOf() + + // These 2 fields collect method scope data. They are reset when a method is added + private val methodAddresses = mutableMapOf() + private val methodJumps = mutableListOf>() + + fun getJumps(): Map = jumps + + fun startClass(clazz: Class) { + writeLine(clazz.header) + } + + fun startMethod(method: Method) { + sb.append(" ".repeat(indent)) + writeLine(method.header) + } + + fun endMethod() { + methodJumps.forEach { (line, address) -> + val targetLine = methodAddresses[address] ?: return@forEach + jumps[line] = targetLine + } + methodAddresses.clear() + methodJumps.clear() + writeLine("") + } + + fun writeInstruction(instruction: Instruction) { + sb.append(" ".repeat(indent)) + methodAddresses[instruction.address] = line + if (instruction.jumpAddress != null) { + methodJumps.add(line to instruction.jumpAddress) + } + if (lineNumberMode is FixedWidth) { + val lineNumber = instruction.lineNumber + val prefix = if (lineNumber != null) "$lineNumber:" else " " + sb.append(prefix.padEnd(lineNumberMode.width + 2)) + } + writeLine(instruction.code) + } + + override fun toString() = sb.toString() + + private fun writeLine(text: String) { + sb.append(text) + sb.append('\n') + line++ + } + + sealed class LineNumberMode { + data object None : LineNumberMode() + class FixedWidth(val width: Int) : LineNumberMode() + // todo: Maybe add an AutoWidth mode that derives the width from the max line number. + } +} \ No newline at end of file diff --git a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/code/CodeResult.kt b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/code/CodeResult.kt new file mode 100644 index 00000000..77f302da --- /dev/null +++ b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/code/CodeResult.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2024 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.code + +import java.io.ByteArrayOutputStream +import java.io.PrintStream + +sealed class CodeContent { + class Success(val classes: List) : CodeContent() + class Error(val errorText: String) : CodeContent() { + constructor(e: Exception) : this(e.toFullString()) + } + + data object Empty : CodeContent() +} + +private fun Throwable.toFullString(): String { + return ByteArrayOutputStream().use { + printStackTrace(PrintStream(it)) + it.toString() + } +} \ No newline at end of file diff --git a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/CodeTextArea.kt b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/code/CodeTextArea.kt similarity index 52% rename from src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/CodeTextArea.kt rename to src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/code/CodeTextArea.kt index 9889cbf5..26be8f69 100644 --- a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/CodeTextArea.kt +++ b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/code/CodeTextArea.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Romain Guy + * Copyright (C) 2024 Romain Guy * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,50 +14,75 @@ * limitations under the License. */ -package dev.romainguy.kotlin.explorer +package dev.romainguy.kotlin.explorer.code -import dev.romainguy.kotlin.explorer.jump.JumpDetector +import dev.romainguy.kotlin.explorer.code.CodeBuilder.LineNumberMode +import dev.romainguy.kotlin.explorer.code.CodeBuilder.LineNumberMode.FixedWidth +import dev.romainguy.kotlin.explorer.code.CodeContent.* import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea import java.awt.BasicStroke import java.awt.Graphics import java.awt.Graphics2D -import java.awt.Polygon import java.awt.RenderingHints import java.awt.geom.GeneralPath import javax.swing.event.CaretEvent import javax.swing.event.CaretListener open class CodeTextArea( - private val explorerState: ExplorerState, - private val jumpDetector: JumpDetector, - private val lineNumberRegex: Regex?, + presentationMode: Boolean = false, + private val indent: Int = 4, + lineNumberMode: LineNumberMode = FixedWidth(4), ) : RSyntaxTextArea() { + private var code: Code? = null private var jumpOffsets: JumpOffsets? = null - private var fullText = "" + + var content: CodeContent = Empty + set(value) { + field = value + updateContent() + } + + var presentationMode = presentationMode + set(value) { + field = value + repaint() + } + + var lineNumberMode = lineNumberMode + set(value) { + field = value + val line = getLineOfOffset(caretPosition) + updateContent() + caretPosition = getLineStartOffset(line) + caretUpdate(line) + } init { addCaretListener(::caretUpdate) } - final override fun addCaretListener(listener: CaretListener?) { - super.addCaretListener(listener) - } - - override fun setText(text: String) { - fullText = text - refreshText() + private fun updateContent() { + code = null + when (val content = content) { + is Empty -> text = "" + is Error -> text = content.errorText + is Success -> code = Code(content.classes, indent, lineNumberMode).also { + val text = it.text + if (text != this.text) { + this.text = text + } + } + } } - fun refreshText() { - val saveCaret = caretPosition - super.setText(fullText.takeIf { explorerState.showLineNumbers } ?: fullText.removeLineLumbers()) - caretPosition = minOf(saveCaret, document.length) + final override fun addCaretListener(listener: CaretListener?) { + super.addCaretListener(listener) } override fun paintComponent(g: Graphics?) { super.paintComponent(g) jumpOffsets?.let { jump -> - val scale = if (explorerState.presentationMode) 2 else 1 + val scale = if (presentationMode) 2 else 1 val padding = 6 * scale val triangleSize = 8 * scale @@ -67,17 +92,10 @@ open class CodeTextArea( val x1 = bounds1.x.toInt() - padding val y1 = (bounds1.y + lineHeight / 2).toInt() - val delta = jump.dst - getLineStartOffset(getLineOfOffset(jump.dst)) - val endPadding = if (explorerState.showLineNumbers && delta < 4) 2 else padding - - val x2 = bounds2.x.toInt() - endPadding + val x2 = bounds2.x.toInt() - 2 val y2 = (bounds2.y + lineHeight / 2).toInt() - val x0 = if (explorerState.showLineNumbers) { - modelToView2D(minOf(0, jump.dst - 4)).x.toInt() + padding - } else { - modelToView2D(6).x.toInt() - } + val x0 = modelToView2D(minOf(0, jump.dst - 4)).x.toInt() + padding val g2 = g as Graphics2D g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) @@ -98,43 +116,31 @@ open class CodeTextArea( } private fun caretUpdate(event: CaretEvent) { - val oldJumpOffsets = jumpOffsets - jumpOffsets = null - val srcLine = getLineOfOffset(event.dot) - var start = getLineStartOffset(srcLine) - var end = getLineEndOffset(srcLine) - - val caretLine = document.getText(start, end - start).trimEnd() - val jump = jumpDetector.detectJump(caretLine) - if (jump != null) { - val srcOffset = start + caretLine.countPadding() - - var dstLine = srcLine + jump.direction - while (dstLine in 0..) + +data class Method(val header: String, val instructions: List) + +data class Instruction(val address: Int, val code: String, val jumpAddress: Int?, val lineNumber: Int? = null) diff --git a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/dex/DexDumpParser.kt b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/dex/DexDumpParser.kt index 73ba6298..dd994d93 100644 --- a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/dex/DexDumpParser.kt +++ b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/dex/DexDumpParser.kt @@ -17,91 +17,96 @@ package dev.romainguy.kotlin.explorer.dex import dev.romainguy.kotlin.explorer.BuiltInKotlinClass +import dev.romainguy.kotlin.explorer.HexDigit +import dev.romainguy.kotlin.explorer.code.Class +import dev.romainguy.kotlin.explorer.code.CodeContent +import dev.romainguy.kotlin.explorer.code.CodeContent.Error +import dev.romainguy.kotlin.explorer.code.CodeContent.Success +import dev.romainguy.kotlin.explorer.code.Instruction +import dev.romainguy.kotlin.explorer.code.Method import dev.romainguy.kotlin.explorer.consumeUntil import dev.romainguy.kotlin.explorer.getValue private val PositionRegex = Regex("^\\s*0x(?
[0-9a-f]+) line=(?\\d+)$") +private val JumpRegex = Regex("^$HexDigit{4}: .* (?
$HexDigit{4}) // [+-]$HexDigit{4}$") + 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()) +internal class DexDumpParser { + + fun parse(text: String): CodeContent { + return try { + val lines = text.lineSequence().iterator() + val classes = buildList { + while (lines.consumeUntil(ClassStart)) { + val clazz = lines.readClass() + if (clazz != null && clazz.methods.isNotEmpty()) { + add(clazz) + } + } } + Success(classes) + } catch (e: Exception) { + Error(e) } - return classes - .filter { it.methods.isNotEmpty() && !it.name.matches(BuiltInKotlinClass)} - .joinToString(separator = "\n") { it.toString() } } - private fun readClass(): DexClass { - val className = lines.next().getClassName() + private fun Iterator.readClass(): Class? { + val className = next().getClassName() + if (className.matches(BuiltInKotlinClass)) { + return null + } val methods = buildList { - while (lines.hasNext()) { - val line = lines.next().trim() + while (hasNext()) { + val line = next().trim() when { line.startsWith(ClassEnd) -> break line.startsWith(Instructions) -> add(readMethod(className)) } } } - return DexClass(className, methods) + return Class("class $className", methods) } - private fun readMethod(className: String): DexMethod { - val (name, type) = lines.next().substringAfterLast(".").split(':', limit = 2) - val instructions = readInstructions() - lines.consumeUntil(Positions) + private fun Iterator.readMethod(className: String): Method { + val (name, type) = next().substringAfterLast(".").split(':', limit = 2) + val instructionsWithoutLineNumbers = readInstructions() + 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) + val instruction = instructionsWithoutLineNumbers.map { it.copy(lineNumber = positions[it.address]) } + return Method("$name$type // $className.$name()", instruction) } - private fun readInstructions(): List { + private fun Iterator.readInstructions(): List { return buildList { - while (lines.hasNext()) { - val line = lines.next() + while (hasNext()) { + val line = next() if (line.startsWith(" ")) { break } - val (address, code) = line.substringAfter('|').split(": ", limit = 2) - add(DexInstruction(address, code)) + val code = line.substringAfter('|') + val address = code.substringBefore(": ") + val jumpAddress = JumpRegex.matchEntire(code)?.getValue("address") + add(Instruction(address.toInt(16), code, jumpAddress?.toInt(16))) } } } - private fun readPositions(): Map { + private fun Iterator.readPositions(): Map { return buildMap { - while (lines.hasNext()) { - val line = lines.next() + while (hasNext()) { + val line = next() val match = PositionRegex.matchEntire(line) ?: break - put(match.getValue("address"), match.getValue("line").toInt()) + put(match.getValue("address").toInt(16), match.getValue("line").toInt()) } } } - private class DexClass(val name: String, val methods: List) { - override fun toString() = "class $name\n${methods.joinToString("\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 String.getClassName() = diff --git a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/dex/DexTextArea.kt b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/dex/DexTextArea.kt deleted file mode 100644 index 817fb1ac..00000000 --- a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/dex/DexTextArea.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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.CodeTextArea -import dev.romainguy.kotlin.explorer.ExplorerState -import dev.romainguy.kotlin.explorer.jump.RegexJumpDetector - -// DEX Syntax: ' 0005: if-nez v0, 0008 // +0003' -private val DexJumpRegex = - Regex("^.{9}[0-9a-fA-F]{4}: .+(?
[0-9a-fA-F]{4}) // (?[+-])[0-9a-fA-F]{4}$") -private val DexAddressedRegex = Regex("^.{9}(?
[0-9a-fA-F]{4}): .+$") -private val DexLineNumberRegex = Regex("^(? +\\d+: )([0-9a-f]{4}: )", RegexOption.MULTILINE) - -class DexTextArea(explorerState: ExplorerState) : - CodeTextArea(explorerState, RegexJumpDetector(DexJumpRegex, DexAddressedRegex), DexLineNumberRegex) diff --git a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/oat/OatDumpParser.kt b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/oat/OatDumpParser.kt new file mode 100644 index 00000000..e7c53bf0 --- /dev/null +++ b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/oat/OatDumpParser.kt @@ -0,0 +1,96 @@ +package dev.romainguy.kotlin.explorer.oat + +import com.google.common.collect.Iterators +import com.google.common.collect.PeekingIterator +import dev.romainguy.kotlin.explorer.BuiltInKotlinClass +import dev.romainguy.kotlin.explorer.HexDigit +import dev.romainguy.kotlin.explorer.code.Class +import dev.romainguy.kotlin.explorer.code.CodeContent +import dev.romainguy.kotlin.explorer.code.CodeContent.Error +import dev.romainguy.kotlin.explorer.code.CodeContent.Success +import dev.romainguy.kotlin.explorer.code.Instruction +import dev.romainguy.kotlin.explorer.code.Method +import dev.romainguy.kotlin.explorer.consumeUntil +import dev.romainguy.kotlin.explorer.getValue + +private val ClassNameRegex = Regex("^\\d+: L(?[^;]+); \\(offset=0x$HexDigit+\\) \\(type_idx=\\d+\\).+") +private val MethodRegex = Regex("^\\s+\\d+:\\s+(?.+)\\s+\\(dex_method_idx=\\d+\\)") +private val CodeRegex = Regex("^\\s+0x(?
$HexDigit+):\\s+$HexDigit+\\s+(?.+)") +private val X86JumpRegex = Regex(".+ [+-]\\d+ \\(0x(?
$HexDigit{8})\\)\$") +private val Arm64JumpRegex = Regex(".+ #[+-]0x$HexDigit+ \\(addr 0x(?
$HexDigit+)\\)\$") + +internal class OatDumpParser { + fun parse(text: String): CodeContent { + return try { + val lines = Iterators.peekingIterator(text.lineSequence().iterator()) + val jumpRegex = when (val set = lines.readInstructionSet()) { + "X86_64" -> X86JumpRegex + "Arm64" -> Arm64JumpRegex + else -> throw IllegalStateException("Unknown instruction set: $set") + } + val classes = buildList { + while (lines.hasNext()) { + val match = lines.consumeUntil(ClassNameRegex) ?: break + val clazz = lines.readClass(match.getValue("class"), jumpRegex) + if (clazz != null && clazz.methods.isNotEmpty()) { + add(clazz) + } + } + } + Success(classes) + } catch (e: Exception) { + Error(e) + } + } + + private fun PeekingIterator.readInstructionSet(): String { + consumeUntil("INSTRUCTION SET:") + return next() + } + + private fun PeekingIterator.readClass(className: String, jumpRegex: Regex): Class? { + if (className.matches(BuiltInKotlinClass)) { + return null + } + val methods = buildList { + while (hasNext()) { + val line = peek() + when { + ClassNameRegex.matches(line) -> break + MethodRegex.matches(line) -> add(readMethod(jumpRegex)) + } + } + } + return Class("class $className", methods) + } + + private fun PeekingIterator.readMethod(jumpRegex: Regex): Method { + val match = MethodRegex.matchEntire(next()) ?: throw IllegalStateException("Should not happen") + val method = match.getValue("method") + consumeUntil("CODE:") + val instructions = readInstructions(jumpRegex) + return Method(method, instructions) + } + + private fun PeekingIterator.readInstructions(jumpRegex: Regex): List { + return buildList { + while (hasNext()) { + val line = peek() + when { + line.matches(MethodRegex) -> break + line.matches(ClassNameRegex) -> break + line.matches(CodeRegex) -> add(readInstruction(jumpRegex)) + else -> next() + } + } + } + } + + private fun PeekingIterator.readInstruction(jumpRegex: Regex): Instruction { + val match = CodeRegex.matchEntire(next()) ?: throw IllegalStateException("Should not happen") + val address = match.getValue("address") + val code = match.getValue("code") + val jumpAddress = jumpRegex.matchEntire(code)?.getValue("address")?.toInt(16) + return Instruction(address.toInt(16), "0x$address: $code", jumpAddress) + } +} diff --git a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/oat/OatTextArea.kt b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/oat/OatTextArea.kt deleted file mode 100644 index 454f1509..00000000 --- a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/oat/OatTextArea.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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.oat - -import dev.romainguy.kotlin.explorer.CodeTextArea -import dev.romainguy.kotlin.explorer.ExplorerState -import dev.romainguy.kotlin.explorer.jump.CompoundJumpDetector -import dev.romainguy.kotlin.explorer.jump.RegexJumpDetector - -// Arm64 syntax: ' 0x00001040: b.ge #+0x48 (addr 0x1088)' -private val Arm64JumpRegex = - Regex("^ +0x[0-9a-fA-F]{8}: .+ #(?[+-])0x[0-9a-fA-F]+ \\(addr 0x(?
[0-9a-fA-F]+)\\)$") - -// X86 syntax: ' 0x00001048: jnl/ge +103 (0x000010b5)' -private val X86JumpRegex = - Regex("^ +0x[0-9a-fA-F]{8}: .+ (?[+-])\\d+ \\(0x(?
[0-9a-fA-F]{8})\\)$") - -private val OatAddressedRegex = Regex("^ +0x(?
[0-9a-fA-F]{8}): .+$") - -class OatTextArea(explorerState: ExplorerState) : - CodeTextArea( - explorerState, - CompoundJumpDetector( - RegexJumpDetector(Arm64JumpRegex, OatAddressedRegex), - RegexJumpDetector(X86JumpRegex, OatAddressedRegex) - ), - lineNumberRegex = null - ) - -fun main() { - println(X86JumpRegex.matchEntire(" 0x00001048: jnl/ge +103 (0x000010b5)")) -} \ No newline at end of file