Skip to content

Commit

Permalink
Improve Ghidra configuration strategy and facet UI
Browse files Browse the repository at this point in the history
  • Loading branch information
garyttierney committed Apr 14, 2024
1 parent d457bd7 commit a4378e5
Show file tree
Hide file tree
Showing 12 changed files with 255 additions and 154 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.codingmates.ghidra.intellij.ide

import com.intellij.DynamicBundle
import org.jetbrains.annotations.Nls
import org.jetbrains.annotations.PropertyKey
import java.util.function.Supplier

object GhidraBundle : DynamicBundle("messages.GhidraBundle") {
fun message(key: @PropertyKey(resourceBundle = "messages.GhidraBundle") String, vararg params: Any): @Nls String {
return getMessage(key, *params)
}

fun messagePointer(

Check warning on line 13 in src/main/kotlin/com/codingmates/ghidra/intellij/ide/GhidraBundle.kt

View workflow job for this annotation

GitHub Actions / Qodana

Unused symbol

Function "messagePointer" is never used
key: @PropertyKey(resourceBundle = "messages.GhidraBundle") String,
vararg params: Any
): Supplier<String> {
return getLazyMessage(key, *params)
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
package com.codingmates.ghidra.intellij.ide.facet


import com.intellij.ProjectTopics
import com.codingmates.ghidra.intellij.ide.facet.model.GhidraProperties
import com.intellij.facet.*
import com.intellij.notification.NotificationGroupManager
import com.intellij.notification.NotificationType
import com.intellij.openapi.application.invokeLater
import com.intellij.openapi.application.runWriteAction
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.*
import com.intellij.util.messages.MessageBusConnection
import java.nio.file.Paths


class GhidraFacet(
Expand All @@ -21,6 +24,7 @@ class GhidraFacet(
) : Facet<GhidraFacetConfiguration>(facetType, module, name, configuration, underlyingFacet) {
private val connection: MessageBusConnection = module.project.messageBus.connect()

var installationProperties: GhidraProperties? = null

Check notice on line 27 in src/main/kotlin/com/codingmates/ghidra/intellij/ide/facet/GhidraFacet.kt

View workflow job for this annotation

GitHub Actions / Qodana

Class member can have 'private' visibility

Property 'installationProperties' could be private
val installationPath
get() = configuration.ghidraState.installationPath

Expand All @@ -30,35 +34,29 @@ class GhidraFacet(
removeLibrary()
}

override fun beforeFacetAdded(facet: Facet<*>) {
}

override fun beforeFacetRenamed(facet: Facet<*>) {
}

override fun facetAdded(facet: Facet<*>) {
}

override fun facetRemoved(facet: Facet<*>) {
}

override fun facetRenamed(facet: Facet<*>, oldName: String) {
}

override fun facetConfigurationChanged(facet: Facet<*>) {
updateLibrary()
}
})

connection.subscribe(ProjectTopics.PROJECT_ROOTS, object : ModuleRootListener {
connection.subscribe(ModuleRootListener.TOPIC, object : ModuleRootListener {
override fun rootsChanged(event: ModuleRootEvent) {
invokeLater { updateLibrary() }
}
})
}

override fun initFacet() {
invokeLater { updateLibrary() }
invokeLater {
try {
installationProperties = GhidraProperties.discoverFromRuntime(Paths.get(installationPath))
} catch (e: Exception) {
NotificationGroupManager.getInstance()
.getNotificationGroup("IDE-errors")
.createNotification(e.message.toString(), NotificationType.ERROR)
.notify(module.project)
}
}
}

fun removeLibrary() = runWriteAction {
Expand Down Expand Up @@ -88,16 +86,13 @@ class GhidraFacet(
val model = rootManager.modifiableModel

try {
val installation = configuration.loadGhidraInstallation()
val libraries = ModifiableModelsProvider.getInstance().libraryTableModifiableModel

var library = libraries.getLibraryByName(GHIDRA_LIBRARY_NAME)
if (library == null) {
library = libraries.createLibrary(GHIDRA_LIBRARY_NAME)

val libraryModel = library.modifiableModel
installation.binaries.forEach { libraryModel.addRoot(it, OrderRootType.CLASSES) }
installation.sources.forEach { libraryModel.addRoot(it, OrderRootType.SOURCES) }

libraryModel.commit()
libraries.commit()
Expand All @@ -118,6 +113,8 @@ class GhidraFacet(
if (!hasLibrary) {
model.addLibraryEntry(library)
}
} catch (err: Exception) {
// TODO: log this
} finally {
if (model.isChanged) {
model.commit()
Expand All @@ -131,8 +128,6 @@ class GhidraFacet(
const val GHIDRA_LIBRARY_NAME = "Ghidra"

fun findAnyInProject(project: Project) = ModuleManager.getInstance(project)
.modules
.mapNotNull { FacetManager.getInstance(it).getFacetByType(GhidraFacetType.FACET_TYPE_ID) }
.first()
.modules.firstNotNullOf { FacetManager.getInstance(it).getFacetByType(FACET_TYPE_ID) }
}
}
Original file line number Diff line number Diff line change
@@ -1,71 +1,14 @@
package com.codingmates.ghidra.intellij.ide.facet

import com.codingmates.ghidra.intellij.ide.facet.model.GhidraInstallation
import com.codingmates.ghidra.intellij.ide.facet.model.GhidraVersion
import com.intellij.facet.FacetConfiguration
import com.intellij.facet.ui.FacetEditorContext
import com.intellij.facet.ui.FacetEditorTab
import com.intellij.facet.ui.FacetValidatorsManager
import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.util.io.inputStream
import java.nio.file.Path
import java.nio.file.Paths
import java.util.*

class GhidraFacetConfiguration : FacetConfiguration, PersistentStateComponent<GhidraFacetState> {
class GhidraFacetConfiguration : FacetConfiguration, PersistentStateComponent<GhidraFacetSettings> {

var ghidraState = GhidraFacetState("")

private fun searchGhidraRoots(root: Path, filter: (VirtualFile) -> Boolean): MutableList<VirtualFile> {
val roots = mutableListOf<VirtualFile>()
val vfs = VirtualFileManager.getInstance()

vfs.findFileByNioPath(root)?.let { vfsRoot ->
VfsUtil.iterateChildrenRecursively(vfsRoot, { !it.path.contains("GhidraServer/data") }) {
if (filter(it)) {
val uri = VfsUtil.getUrlForLibraryRoot(it.toNioPath().toFile())
val archiveFile = vfs.findFileByUrl(uri) ?: return@iterateChildrenRecursively true
roots.add(archiveFile)
}

true
}
}

return roots
}

fun loadGhidraInstallation(): GhidraInstallation {
val propertyFile = Paths.get(state.installationPath, "Ghidra", "application.properties")
val properties = propertyFile.inputStream().use {
val properties = Properties()
properties.load(it)

properties
}

val version = GhidraVersion(
properties.getProperty("application.name"),
properties.getProperty("application.release.name"),
properties.getProperty("application.version")
)

val userDataKey = ".${version.name.lowercase(Locale.getDefault())}_${version.version}_${version.releaseName}"
val extensionRoot = Paths.get(System.getProperty("user.home"), ".ghidra", userDataKey, "Extensions")
val installationRoot = Paths.get(state.installationPath)

fun isBinaries(vf: VirtualFile) = vf.extension.equals("jar")
fun isSources(vf: VirtualFile) = vf.extension.equals("zip") && vf.path.contains(Regex("src|sources"))

val sources = searchGhidraRoots(installationRoot, ::isSources) + searchGhidraRoots(extensionRoot, ::isSources)
val binaries =
searchGhidraRoots(installationRoot, ::isBinaries) + searchGhidraRoots(extensionRoot, ::isBinaries)

return GhidraInstallation(state.installationPath, version, sources, binaries)
}
var ghidraState = GhidraFacetSettings()

override fun createEditorTabs(
editorContext: FacetEditorContext,
Expand All @@ -74,11 +17,9 @@ class GhidraFacetConfiguration : FacetConfiguration, PersistentStateComponent<Gh
return arrayOf(GhidraFacetConfigurationEditor(ghidraState, editorContext, validatorsManager))
}

override fun getState(): GhidraFacetSettings = ghidraState

override fun getState(): GhidraFacetState = ghidraState

override fun loadState(state: GhidraFacetState) {
override fun loadState(state: GhidraFacetSettings) {
ghidraState = state
}

}
Original file line number Diff line number Diff line change
@@ -1,70 +1,104 @@
package com.codingmates.ghidra.intellij.ide.facet

import com.intellij.facet.ui.FacetEditorContext
import com.intellij.facet.ui.FacetEditorTab
import com.intellij.facet.ui.FacetValidatorsManager
import com.intellij.openapi.fileChooser.FileChooser
import com.codingmates.ghidra.intellij.ide.GhidraBundle
import com.codingmates.ghidra.intellij.ide.facet.model.isGhidraInstallationPath
import com.codingmates.ghidra.intellij.ide.facet.model.isGhidraSourcesPath
import com.intellij.facet.ui.*
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
import com.intellij.openapi.observable.properties.PropertyGraph
import com.intellij.openapi.observable.util.toUiPathProperty
import com.intellij.openapi.options.ConfigurationException
import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.ui.BrowseFolderDescriptor.Companion.withPathToTextConvertor
import com.intellij.openapi.ui.BrowseFolderDescriptor.Companion.withTextToPathConvertor
import com.intellij.openapi.ui.getCanonicalPath
import com.intellij.openapi.ui.getPresentablePath
import com.intellij.openapi.ui.setEmptyState
import com.intellij.ui.dsl.builder.AlignX
import com.intellij.ui.dsl.builder.bindText
import com.intellij.ui.dsl.builder.panel
import org.jetbrains.annotations.Nls
import java.awt.BorderLayout
import javax.swing.*


class GhidraFacetConfigurationEditor(
private val state: GhidraFacetState,
private val state: GhidraFacetSettings,
private val context: FacetEditorContext,
private val _validator: FacetValidatorsManager
private val validator: FacetValidatorsManager
) : FacetEditorTab() {

private val installationPathEditor = JTextField(state.installationPath)

override fun createComponent(): JComponent {
val top = JPanel(BorderLayout())
top.add(JLabel("Path to Ghidra installation: "), BorderLayout.WEST)
top.add(installationPathEditor)
val chooserButton = JButton("...")
top.add(chooserButton, BorderLayout.EAST)
val facetPanel = JPanel(BorderLayout())
facetPanel.add(top, BorderLayout.NORTH)
chooserButton.addActionListener {
FileChooser.chooseFile(
FileChooserDescriptorFactory.createSingleFolderDescriptor(),
context.project, null
) { virtualFile ->
virtualFile.canonicalPath?.let { path ->
installationPathEditor.text = path
}
private val propertyGraph = PropertyGraph()
private val installationDir = propertyGraph.property(state.installationPath)
private val settingsDir = propertyGraph.property(state.settingsPath?.toString() ?: "")

Check warning on line 29 in src/main/kotlin/com/codingmates/ghidra/intellij/ide/facet/GhidraFacetConfigurationEditor.kt

View workflow job for this annotation

GitHub Actions / Qodana

Redundant call of conversion method

Remove redundant calls of the conversion method
private val version = propertyGraph.property(state.version ?: "")
private val applied = propertyGraph.property(false)

init {
validator.registerValidator(GhidraInstallationValidator())
propertyGraph.afterPropagation { validator.validate() }
}

override fun createComponent() = panel {
group("Ghidra Settings") {
row(GhidraBundle.message("ghidra.facet.editor.installation")) {
val title = GhidraBundle.message("ghidra.facet.editor.installation.dialog.title")
val fileChooserDescriptor = FileChooserDescriptorFactory.createSingleFolderDescriptor()
.withPathToTextConvertor(::getPresentablePath).withTextToPathConvertor(::getCanonicalPath)

textFieldWithBrowseButton(title, context.project, fileChooserDescriptor)
.bindText(installationDir.toUiPathProperty())
.applyToComponent { setEmptyState(GhidraBundle.message("ghidra.facet.editor.installation.empty")) }
.align(AlignX.FILL)
}
}
return facetPanel
}

/**
* @return the name of this facet for display in this editor tab.
*/
@Nls(capitalization = Nls.Capitalization.Title)
override fun getDisplayName(): String {
return GhidraFacetType.FACET_NAME
group("Ghidra Installation Details") {
row("Version") {
textField()
.bindText(version)
.enabled(false)
.align(AlignX.FILL)
}

row("Settings") {
textField()
.bindText(settingsDir)
.enabled(false)
.align(AlignX.FILL)

}
}.visibleIf(applied)
}

inner class GhidraInstallationValidator : FacetEditorValidator(), SlowFacetEditorValidator {
override fun check(): ValidationResult {
val ghidraInstallation = installationDir.get()

override fun isModified(): Boolean {
return !StringUtil.equals(state.installationPath, installationPathEditor.text.trim())
if (!isGhidraInstallationPath(ghidraInstallation)) {
return ValidationResult(GhidraBundle.message("ghidra.facet.editor.installation.error.no-properties"))
}

if (isGhidraSourcesPath(ghidraInstallation)) {
return ValidationResult(GhidraBundle.message("ghidra.facet.editor.installation.error.sources"))
}

return ValidationResult.OK
}
}

@Nls(capitalization = Nls.Capitalization.Title)
override fun getDisplayName() = GhidraFacetType.FACET_NAME

override fun isModified() = state.installationPath != installationDir.get()

@Throws(ConfigurationException::class)
override fun apply() {
// Not much to go wrong here, but fulfill the contract
try {
val newTextContent: String = installationPathEditor.text
state.installationPath = newTextContent
} catch (e: Exception) {
throw ConfigurationException(e.toString())
}
}
state.installationPath = installationDir.get()
state.resolve()

override fun reset() {
installationPathEditor.text = state.installationPath
settingsDir.set(state.settingsPath.toString())
version.set(state.version!!)
applied.set(true)
} catch (e: ConfigurationException) {
throw ConfigurationException(e.localizedMessage)
}
}
}
Loading

0 comments on commit a4378e5

Please sign in to comment.