Skip to content

Commit

Permalink
decompiler action with context menu and notifications
Browse files Browse the repository at this point in the history
  • Loading branch information
mkurnikov committed May 21, 2024
1 parent e844189 commit 85f2992
Show file tree
Hide file tree
Showing 9 changed files with 329 additions and 46 deletions.
107 changes: 76 additions & 31 deletions src/main/kotlin/org/move/bytecode/AptosBytecodeNotificationProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,67 +5,112 @@ import com.intellij.notification.NotificationType.ERROR
import com.intellij.openapi.fileEditor.FileEditor
import com.intellij.openapi.fileTypes.FileTypeRegistry
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.Task
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.openapi.vfs.newvfs.BulkFileListener
import com.intellij.openapi.vfs.newvfs.events.VFileEvent
import com.intellij.ui.EditorNotificationPanel
import com.intellij.ui.EditorNotificationProvider
import org.move.ide.notifications.showBalloon
import org.move.ide.notifications.showDebugBalloon
import org.move.ide.notifications.updateAllNotifications
import org.move.openapiext.openFile
import org.move.openapiext.toVirtualFile
import org.move.openapiext.pathAsPath
import org.move.stdext.RsResult
import org.move.stdext.unwrapOrElse
import java.util.function.Function
import javax.swing.JComponent
import kotlin.io.path.exists

class AptosBytecodeNotificationProvider: EditorNotificationProvider {
class AptosBytecodeNotificationProvider(project: Project): EditorNotificationProvider {

init {
project.messageBus.connect().subscribe(VirtualFileManager.VFS_CHANGES, object: BulkFileListener {
override fun after(events: MutableList<out VFileEvent>) {
updateAllNotifications(project)
}
})
}

override fun collectNotificationData(
project: Project,
file: VirtualFile
): Function<in FileEditor, out JComponent?>? {
if (!FileTypeRegistry.getInstance().isFileOfType(file, AptosBytecodeFileType)) {
return null
}
val properties = PropertiesComponent.getInstance(project)
val progressManager = ProgressManager.getInstance()
val decompilationFailedKey = DECOMPILATION_FAILED + "." + file.path

val aptosDecompiler = AptosBytecodeDecompiler()
val targetFileDir = aptosDecompiler.getDecompilerTargetFileDirOnTemp(project, file)!!
val decompiledFilePath = file.parent.pathAsPath.resolve(aptosDecompiler.sourceFileName(file))
val decompilationTask = DecompilationModalTask(project, file)

val expectedTargetFile = targetFileDir.resolve(aptosDecompiler.hashedSourceFileName(file))
val properties = PropertiesComponent.getInstance(project)

val triedKey = DECOMPILATION_TRIED + "-" + file.path + "-" + aptosDecompiler.hashedSourceFileName(file)
if (!expectedTargetFile.exists()) {
if (properties.getBoolean(triedKey, false)) {
return Function {
EditorNotificationPanel(it).apply {
text = "Error with decompilation occurred"
return Function {
EditorNotificationPanel(it).apply {
val existingDecompiledFile =
VirtualFileManager.getInstance().refreshAndFindFileByNioPath(decompiledFilePath)
if (existingDecompiledFile != null) {
// file exists
text = "Decompiled source file exists"
createActionLabel("Open source file") {
project.openFile(existingDecompiledFile)
}
return@apply
}
}
object : Task.Backgroundable(project, "Decompiling ${file.name}...", true) {
override fun run(indicator: ProgressIndicator) {
aptosDecompiler.decompileFile(project, file, targetFileDir)
.unwrapOrElse {
project.showBalloon("Error with decompilation process", it, ERROR)

// decompiledFile does not exist
val decompilationFailed = properties.getBoolean(decompilationFailedKey, false)
if (decompilationFailed) {
text = "Decompilation command failed"
createActionLabel("Try again") {
val virtualFile = progressManager.run(decompilationTask)
.unwrapOrElse {
// something went wrong with the decompilation command again
project.showDebugBalloon("Error with decompilation process", it, ERROR)
return@createActionLabel
}

properties.setValue(decompilationFailedKey, false)
project.openFile(virtualFile)
updateAllNotifications(project)
}
} else {
createActionLabel("Decompile into source code") {
val decompiledFile = progressManager.run(decompilationTask)
.unwrapOrElse {
project.showDebugBalloon("Error with decompilation process", it, ERROR)
return@unwrapOrElse null
}
if (decompiledFile == null) {
// something went wrong with the decompilation command
properties.setValue(decompilationFailedKey, true)
} else {
project.openFile(decompiledFile)
}
properties.setValue(triedKey, true)
updateAllNotifications(project)
}
}.queue()
return null
} else {
return Function {
EditorNotificationPanel(it).apply {
createActionLabel("Show decompiled source code") {
project.openFile(expectedTargetFile.toVirtualFile()!!)
updateAllNotifications(project)
}
}
}
}
}

class DecompilationModalTask(project: Project, val file: VirtualFile):
Task.WithResult<RsResult<VirtualFile, String>, Exception>(
project,
"Decompiling ${file.name}...",
true
) {
override fun compute(indicator: ProgressIndicator): RsResult<VirtualFile, String> {
val aptosDecompiler = AptosBytecodeDecompiler()
return aptosDecompiler.decompileFileToTheSameDir(project, file)
}
}

companion object {
private const val DECOMPILATION_TRIED = "org.move.aptosDecompilerNotificationKey"
private const val DECOMPILATION_FAILED = "org.move.aptosDecompilerNotificationKey"

}
}
33 changes: 28 additions & 5 deletions src/main/kotlin/org/move/bytecode/AptosDecompiler.kt
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
package org.move.bytecode

import com.intellij.openapi.Disposable
import com.intellij.openapi.fileEditor.impl.LoadTextUtil
import com.intellij.openapi.fileTypes.BinaryFileDecompiler
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.vfs.*
import com.intellij.openapi.vfs.newvfs.BulkFileListener
import com.intellij.openapi.vfs.newvfs.events.VFileEvent
import com.intellij.openapi.vfs.readText
import com.intellij.openapi.vfs.toNioPathOrNull
import org.move.cli.settings.getAptosCli
import org.move.openapiext.pathAsPath
import org.move.openapiext.rootDisposable
Expand All @@ -25,7 +24,14 @@ import kotlin.io.path.relativeTo
// todo: this is disabled for now, it's a process under ReadAction, and needs to be run in the indexing phase
class AptosBytecodeDecompiler: BinaryFileDecompiler {
override fun decompile(file: VirtualFile): CharSequence {
return file.readText()
val fileText = file.readText()
try {
StringUtil.assertValidSeparators(fileText)
return fileText
} catch (e: AssertionError) {
val bytes = file.readBytes()
return LoadTextUtil.getTextByBinaryPresentation(bytes, file)
}
// val project =
// ProjectLocator.getInstance().getProjectsForFile(file).firstOrNull { it?.isAptosConfigured == true }
// ?: ProjectManager.getInstance().defaultProject.takeIf { it.isAptosConfigured }
Expand All @@ -35,6 +41,23 @@ class AptosBytecodeDecompiler: BinaryFileDecompiler {
// return LoadTextUtil.loadText(targetFile)
}

fun decompileFileToTheSameDir(project: Project, file: VirtualFile): RsResult<VirtualFile, String> {
val disposable = project.createDisposableOnFileChange(file)
val aptos = project.getAptosCli(disposable) ?: return RsResult.Err("No Aptos CLI configured")

aptos.decompileFile(file.path, outputDir = null)
.unwrapOrElse {
return RsResult.Err("`aptos move decompile` failed")
}
val decompiledFilePath = file.parent.pathAsPath.resolve(sourceFileName(file))
val decompiledFile = VirtualFileManager.getInstance().refreshAndFindFileByNioPath(decompiledFilePath)
?: run {
// something went wrong, no output file
return RsResult.Err("Expected decompiled file $decompiledFilePath does not exist")
}
return RsResult.Ok(decompiledFile)
}

fun decompileFile(project: Project, file: VirtualFile, targetFileDir: Path): RsResult<VirtualFile, String> {
val disposable = project.createDisposableOnFileChange(file)
val aptos = project.getAptosCli(disposable) ?: return RsResult.Err("No Aptos CLI configured")
Expand Down
39 changes: 39 additions & 0 deletions src/main/kotlin/org/move/bytecode/DecompileAptosMvFileAction.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.move.bytecode

import com.intellij.notification.NotificationType.ERROR
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.project.DumbAwareAction
import org.move.bytecode.AptosBytecodeNotificationProvider.DecompilationModalTask
import org.move.cli.settings.getAptosCli
import org.move.ide.MoveIcons
import org.move.ide.notifications.showBalloon
import org.move.openapiext.openFile
import org.move.stdext.unwrapOrElse
import java.util.*

class DecompileAptosMvFileAction: DumbAwareAction("Decompile .mv File", null, MoveIcons.APTOS_LOGO) {
override fun actionPerformed(e: AnActionEvent) {
val project = e.project ?: return
val file = e.getData(CommonDataKeys.PSI_FILE)?.virtualFile ?: return
val decompilationTask = DecompilationModalTask(project, file)
val decompiledFile = ProgressManager.getInstance().run(decompilationTask)
.unwrapOrElse {
project.showBalloon("Error with decompilation process", it, ERROR)
return
}
project.openFile(decompiledFile)
}

override fun update(e: AnActionEvent) {
val file = e.getData(CommonDataKeys.PSI_FILE)
val presentation = e.presentation
val enabled =
(file != null
&& Objects.nonNull(file.virtualFile) && !(file.virtualFile.fileSystem.isReadOnly)
&& file.fileType == AptosBytecodeFileType
&& e.getData(CommonDataKeys.PROJECT)?.getAptosCli() != null)
presentation.isEnabledAndVisible = enabled
}
}
14 changes: 14 additions & 0 deletions src/main/kotlin/org/move/bytecode/FetchAptosPackageAction.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.move.bytecode

import com.intellij.openapi.actionSystem.AnActionEvent
import org.move.cli.runConfigurations.aptos.RunAptosCommandActionBase

class FetchAptosPackageAction: RunAptosCommandActionBase("Fetch on-chain package") {
override fun actionPerformed(e: AnActionEvent) {
val project = e.project ?: return

val parametersDialog = FetchAptosPackageDialog(project)
parametersDialog.show()
}

}
113 changes: 113 additions & 0 deletions src/main/kotlin/org/move/bytecode/FetchAptosPackageDialog.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package org.move.bytecode

import com.intellij.execution.process.ProcessOutput
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.Task
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.ui.components.JBCheckBox
import com.intellij.ui.components.JBTextField
import com.intellij.ui.dsl.builder.AlignX
import com.intellij.ui.dsl.builder.panel
import org.move.cli.settings.getAptosCli
import org.move.openapiext.RsProcessResult
import org.move.openapiext.pathField
import org.move.stdext.unwrapOrElse
import javax.swing.JComponent
import kotlin.io.path.Path

class FetchAptosPackageDialog(val project: Project): DialogWrapper(project, true) {

val addressTextField = JBTextField()
val packageTextField = JBTextField()
val outputDirField = pathField(
FileChooserDescriptorFactory.createSingleFolderDescriptor(),
this.disposable,
"Output Directory"
)
val decompileCheckbox = JBCheckBox("Decompile afterwards")

val profileField = JBTextField("default")
val nodeApiKey = JBTextField()
val connectionTimeout = JBTextField()

init {
title = "Aptos Decompiler"
setSize(600, 400)

outputDirField.text = project.basePath.orEmpty()
decompileCheckbox.isSelected = true
init()
}

override fun createCenterPanel(): JComponent {
return panel {
row("Address:") { cell(addressTextField).align(AlignX.FILL) }
row("Package:") { cell(packageTextField).align(AlignX.FILL) }
row("Output directory:") { cell(outputDirField).align(AlignX.FILL) }
row { cell(decompileCheckbox) }

val parametersGroup = collapsibleGroup("Connection Parameters") {
row("Profile:") { cell(profileField) }
row("Node API Key:") { cell(nodeApiKey) }
row("Connection timeout:") { cell(connectionTimeout) }
}
parametersGroup.expanded = false

}
}

override fun doOKAction() {
val accountAddress = this.addressTextField.text
val packageName = this.packageTextField.text
// val profile = this.profileField.text
val outputDir = this.outputDirField.text
val decompile = this.decompileCheckbox.isSelected
val aptos = project.getAptosCli(this.disposable) ?: return

val downloadTask = object: Task.WithResult<RsProcessResult<ProcessOutput>, Exception>(
project,
"Downloading $accountAddress::$packageName...",
true
) {
override fun compute(indicator: ProgressIndicator): RsProcessResult<ProcessOutput> {
return aptos.downloadPackage(project, accountAddress, packageName, outputDir,
runner = { runProcessWithProgressIndicator(indicator) })
}
}
ProgressManager.getInstance().run(downloadTask)
.unwrapOrElse {
this.setErrorText(it.message)
return
}

if (decompile) {
val decompileTask = object: Task.WithResult<RsProcessResult<ProcessOutput>, Exception>(
project,
"Decompiling $accountAddress::$packageName...",
true
) {
override fun compute(indicator: ProgressIndicator): RsProcessResult<ProcessOutput> {
val downloadedPath = Path(outputDir).resolve(packageName)
return aptos.decompileDownloadedPackage(downloadedPath)
}
}
ProgressManager.getInstance().run(decompileTask)
.unwrapOrElse {
this.setErrorText(it.message)
return
}
}

super.doOKAction()
}

override fun getPreferredFocusedComponent(): JComponent = addressTextField

override fun doValidate(): ValidationInfo? {
return super.doValidate()
}
}
Loading

0 comments on commit 85f2992

Please sign in to comment.