From ccde85ede5e6b78778f90b2ac36ee2cdef2c6957 Mon Sep 17 00:00:00 2001 From: Alon Albert Date: Sat, 4 May 2024 14:15:33 -0700 Subject: [PATCH] Add a Code Model This makes it easier to detect jumps and toggle show-line-number --- .../romainguy/kotlin/explorer/Disassembly.kt | 94 ++----------- .../kotlin/explorer/KotlinExplorer.kt | 66 +++++---- .../kotlin/explorer/PeekingIterator.kt | 36 +++++ .../dev/romainguy/kotlin/explorer/Regex.kt | 2 + .../dev/romainguy/kotlin/explorer/String.kt | 36 +++++ .../romainguy/kotlin/explorer/code/Code.kt | 51 +++++++ .../kotlin/explorer/code/CodeBuilder.kt | 92 +++++++++++++ .../kotlin/explorer/code/CodeResult.kt | 36 +++++ .../explorer/{ => code}/CodeTextArea.kt | 128 ++++++++++-------- .../kotlin/explorer/code/DataModels.kt | 23 ++++ .../kotlin/explorer/dex/DexDumpParser.kt | 93 +++++++------ .../kotlin/explorer/dex/DexTextArea.kt | 30 ---- .../kotlin/explorer/oat/OatDumpParser.kt | 108 +++++++++++++++ .../kotlin/explorer/oat/OatTextArea.kt | 46 ------- 14 files changed, 558 insertions(+), 283 deletions(-) create mode 100644 src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/PeekingIterator.kt create mode 100644 src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/String.kt create mode 100644 src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/code/Code.kt create mode 100644 src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/code/CodeBuilder.kt create mode 100644 src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/code/CodeResult.kt rename src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/{ => code}/CodeTextArea.kt (51%) create mode 100644 src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/code/DataModels.kt delete mode 100644 src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/dex/DexTextArea.kt create mode 100644 src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/oat/OatDumpParser.kt delete mode 100644 src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/oat/OatTextArea.kt diff --git a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Disassembly.kt b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Disassembly.kt index 1b09aaf9..98764d41 100644 --- a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Disassembly.kt +++ b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Disassembly.kt @@ -16,7 +16,10 @@ 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 dev.romainguy.kotlin.explorer.oat.OatDumpParser import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.currentCoroutineContext @@ -29,11 +32,14 @@ import kotlin.io.path.extension private const val TotalSteps = 5 private const val Done = 1f +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, Float) -> Unit, optimize: Boolean ) = coroutineScope { @@ -57,7 +63,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", Done) } return@launch @@ -77,7 +83,7 @@ suspend fun disassemble( if (r8.exitCode != 0) { launch(ui) { - onDex(r8.output) + onDex(Error(r8.output)) onStatusUpdate("Ready", Done) } return@launch @@ -94,14 +100,14 @@ suspend fun disassemble( if (dexdump.exitCode != 0) { launch(ui) { - onDex(dexdump.output) + onDex(Error(dexdump.output)) onStatusUpdate("Ready", Done) } return@launch } launch(ui) { - onDex(DexDumpParser(dexdump.output).parseDexDump()) + onDex(dexDumpParser.parse(dexdump.output)) onStatusUpdate("AOT compilation…", step++ / TotalSteps) } @@ -115,7 +121,7 @@ suspend fun disassemble( if (push.exitCode != 0) { launch(ui) { - onOat(push.output) + onOat(Error(push.output)) onStatusUpdate("Ready", Done) } return@launch @@ -132,7 +138,7 @@ suspend fun disassemble( if (dex2oat.exitCode != 0) { launch(ui) { - onOat(dex2oat.output) + onOat(Error(dex2oat.output)) onStatusUpdate("Ready", Done) } return@launch @@ -148,7 +154,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", Done) } @@ -256,73 +262,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 9a898731..75f85c58 100644 --- a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/KotlinExplorer.kt +++ b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/KotlinExplorer.kt @@ -39,8 +39,10 @@ import androidx.compose.ui.window.* import androidx.compose.ui.window.WindowPosition.Aligned 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 @@ -70,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( @@ -128,27 +132,29 @@ private fun FrameWindowScope.KotlinExplorer( val dexPanel: @Composable () -> Unit = { TextPanel("DEX", dexTextArea, explorerState) } 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() } + val onProgressUpdate: (String, Float) -> Unit = { newStatus: String, newProgress: Float -> status = newStatus progress = newProgress } - + 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 }, onProgressUpdate, { findDialog.isVisible = true }, { SearchEngine.find(activeTextArea, findDialog.searchContext) }, { showSettings = true }, { panels = explorerState.getPanels(sourcePanel, dexPanel, oatPanel) }, + updateShowLineNumbers, + updatePresentationMode, ) if (showSettings) { @@ -243,15 +249,19 @@ 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(explorerState: ExplorerState, focusTracker: FocusListener): RSyntaxTextArea { - return OatTextArea(explorerState).apply { +private fun oatTextArea(state: ExplorerState, focusTracker: FocusListener) = + codeTextArea(state, focusTracker, hasLineNumbers = false) + +private fun codeTextArea( + state: ExplorerState, + focusTracker: FocusListener, + hasLineNumbers: Boolean = true +): CodeTextArea { + val linNumberMode = (hasLineNumbers && state.showLineNumbers).toLineNumberMode() + return CodeTextArea(state.presentationMode, CODE_INDENT, linNumberMode).apply { configureSyntaxTextArea(SyntaxConstants.SYNTAX_STYLE_NONE) addFocusListener(focusTracker) } @@ -261,13 +271,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, Float) -> Unit, onFindClicked: () -> Unit, onFindNextClicked: () -> Unit, onOpenSettings: () -> Unit, onPanelsUpdated: () -> Unit, + onShowLineNumberChanged: (Boolean) -> Unit, + onPresentationModeChanged: (Boolean) -> Unit, ) { val scope = rememberCoroutineScope() @@ -296,10 +308,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) @@ -334,11 +348,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/PeekingIterator.kt b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/PeekingIterator.kt new file mode 100644 index 00000000..358e684b --- /dev/null +++ b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/PeekingIterator.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 + +/** Based on Guava PeekingIterator */ +class PeekingIterator(private val iterator: Iterator) : Iterator { + private var peekedElement: E? = null + + override fun hasNext(): Boolean { + return peekedElement != null || iterator.hasNext() + } + + override fun next(): E { + val element = peekedElement ?: return iterator.next() + peekedElement = null + return element + } + + fun peek(): E { + return peekedElement.takeIf { it != null } ?: iterator.next().also { peekedElement = it } + } +} \ No newline at end of file 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 51% 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..bfae9e54 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,76 @@ * 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.CodeBuilder.LineNumberMode.None +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 @@ -68,15 +94,17 @@ open class CodeTextArea( 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 showLineNumbers = lineNumberMode !is None + val endPadding = if (showLineNumbers && delta < 4) 2 else padding val x2 = bounds2.x.toInt() - endPadding val y2 = (bounds2.y + lineHeight / 2).toInt() - val x0 = if (explorerState.showLineNumbers) { - modelToView2D(minOf(0, jump.dst - 4)).x.toInt() + padding + val x0 = if (showLineNumbers) { + modelToView2D(minOf(4, jump.dst - 4)).x.toInt() + padding } else { - modelToView2D(6).x.toInt() + modelToView2D(4).x.toInt() + modelToView2D(4).x.toInt() } val g2 = g as Graphics2D @@ -98,43 +126,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..fcf008b6 --- /dev/null +++ b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/oat/OatDumpParser.kt @@ -0,0 +1,108 @@ +/* + * 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.oat + +import dev.romainguy.kotlin.explorer.* +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 + +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 = 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").replace('/', '.'), 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)) + else -> next() + } + } + } + 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