Skip to content

Commit

Permalink
Merge pull request #147 from pontem-network/aptos-decompiler
Browse files Browse the repository at this point in the history
Aptos decompiler
  • Loading branch information
mkurnikov authored May 24, 2024
2 parents d41cc8c + 604a327 commit 3827a30
Show file tree
Hide file tree
Showing 44 changed files with 1,354 additions and 356 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
tests:
strategy:
matrix:
gradle-properties-version: [ 232, 233, 241 ]
gradle-properties-version: [ 233, 241 ]

runs-on: ubuntu-latest
env:
Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ val javaVersion = JavaVersion.VERSION_17
val pluginJarName = "intellij-move-$pluginVersion"

val kotlinReflectVersion = "1.8.10"
val aptosVersion = "3.1.0"
val aptosVersion = "3.3.0"

val remoteRobotVersion = "0.11.22"

Expand Down
13 changes: 0 additions & 13 deletions gradle-232.properties

This file was deleted.

3 changes: 1 addition & 2 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,5 @@ jbrVersion=17.0.7b1000.6

propertiesPluginEnvironmentNameProperty=shortPlatformVersion
# properties files
# supported versions are 231, 232, 233, default is 231
# pass ORG_GRADLE_PROJECT_shortPlatformVersion environment variable to overwrite
shortPlatformVersion=232
shortPlatformVersion=233
13 changes: 13 additions & 0 deletions src/main/kotlin/org/move/bytecode/AptosBytecodeFileType.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.move.bytecode

import com.intellij.openapi.fileTypes.FileType
import org.move.ide.MoveIcons

object AptosBytecodeFileType: FileType {
override fun getIcon() = MoveIcons.MV_LOGO
override fun getName() = "APTOS_BYTECODE"
override fun getDefaultExtension() = "mv"
override fun getDescription() = "Aptos Move bytecode"
override fun getDisplayName() = "Aptos Move bytecode"
override fun isBinary(): Boolean = true
}
117 changes: 117 additions & 0 deletions src/main/kotlin/org/move/bytecode/AptosBytecodeNotificationProvider.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package org.move.bytecode

import com.intellij.ide.util.PropertiesComponent
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.showDebugBalloon
import org.move.ide.notifications.updateAllNotifications
import org.move.openapiext.openFile
import org.move.openapiext.pathAsPath
import org.move.stdext.RsResult
import org.move.stdext.unwrapOrElse
import java.util.function.Function
import javax.swing.JComponent

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 decompilationFailedKey = DECOMPILATION_FAILED + "." + file.path

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

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
}

// decompiledFile does not exist
val decompilationFailed = properties.getBoolean(decompilationFailedKey, false)
if (decompilationFailed) {
text = "Decompilation command failed"
createActionLabel("Try again") {
val virtualFile = decompilationTask.runWithProgress()
.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 = decompilationTask.runWithProgress()
.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)
}
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)
}

fun runWithProgress(): RsResult<VirtualFile, String> = ProgressManager.getInstance().run(this)
}

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

}
}
126 changes: 126 additions & 0 deletions src/main/kotlin/org/move/bytecode/AptosDecompiler.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
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.util.text.StringUtil
import com.intellij.openapi.vfs.*
import com.intellij.openapi.vfs.newvfs.BulkFileListener
import com.intellij.openapi.vfs.newvfs.events.VFileEvent
import org.move.cli.settings.getAptosCli
import org.move.openapiext.pathAsPath
import org.move.openapiext.rootDisposable
import org.move.openapiext.rootPath
import org.move.stdext.RsResult
import org.move.stdext.unwrapOrElse
import java.io.File
import java.nio.file.Path
import kotlin.io.path.exists
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 {
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 }
// ?: return file.readText()
// val targetFileDir = getDecompilerTargetFileDirOnTemp(project, file) ?: return file.readText()
// val targetFile = decompileFile(project, file, targetFileDir) ?: return file.readText()
// 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")

if (!targetFileDir.exists()) {
targetFileDir.toFile().mkdirs()
}

aptos.decompileFile(file.path, outputDir = targetFileDir.toString())
.unwrapOrElse {
return RsResult.Err("`aptos move decompile` failed")
}
val decompiledName = sourceFileName(file)
val decompiledFile =
VirtualFileManager.getInstance().findFileByNioPath(targetFileDir.resolve(decompiledName)) ?: run {
// something went wrong, no output file
return RsResult.Err("Cannot find decompiled file in the target directory")
}

val decompiledNioFile = decompiledFile.toNioPathOrNull()?.toFile()
?: return RsResult.Err("Cannot convert VirtualFile to File")
FileUtil.rename(decompiledNioFile, hashedSourceFileName(file))

return RsResult.Ok(decompiledFile)
}

fun getDecompilerTargetFileDirOnTemp(project: Project, file: VirtualFile): Path? {
val rootDecompilerDir = getArtifactsDir()
val projectDecompilerDir = rootDecompilerDir.resolve(project.name)
val root = project.rootPath ?: return null
val relativeFilePath = file.parent.pathAsPath.relativeTo(root)
val targetFileDir = projectDecompilerDir.toPath().resolve(relativeFilePath)
return targetFileDir
}

fun getArtifactsDir(): File {
return File(FileUtil.getTempDirectory(), "intellij-move-decompiled-artifacts")
}

fun sourceFileName(file: VirtualFile): String {
val fileName = file.name
return "$fileName.move"
}

fun hashedSourceFileName(file: VirtualFile): String {
val fileName = file.name
return "$fileName#decompiled.move"
}
}

fun Project.createDisposableOnFileChange(file: VirtualFile): Disposable {
val filePath = file.path
val disposable = Disposer.newDisposable("Dispose on any change to the ${file.name} file")
messageBus.connect(disposable).subscribe(
VirtualFileManager.VFS_CHANGES,
object: BulkFileListener {
override fun after(events: MutableList<out VFileEvent>) {
if (events.any { it.path == filePath }) {
Disposer.dispose(disposable)
}
}
}
)
Disposer.register(this.rootDisposable, disposable)
return disposable
}
37 changes: 37 additions & 0 deletions src/main/kotlin/org/move/bytecode/DecompileAptosMvFileAction.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
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.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 decompiledFile = DecompilationModalTask(project, file).runWithProgress()
.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()
}

}
Loading

0 comments on commit 3827a30

Please sign in to comment.