ghidra/Ghidra/Features/Base/ghidra_scripts/OpenSourceFileAtLineInVSCodeScript.java

245 lines
6.9 KiB
Java

/* ###
* IP: GHIDRA
*
* 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.
*/
// This script reads the source map information for the current address and uses it to open
// a source file in vs code at the appropriate line. If there are multiple source map entries
// at the current address, the script displays a table to allow the user to select which ones
// to send to vs code. The source file paths can be adjusted via
//
// Window -> Source Files and Transforms
//
// from the Code Browser. The path to the vs code executable can be set via
//
// Edit -> Tool Options -> Visual Studio Code Integration
//
// from the Ghidra Project Manager.
//@category Source Mapping
import java.io.File;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.List;
import ghidra.app.script.GhidraScript;
import ghidra.app.services.VSCodeIntegrationService;
import ghidra.app.tablechooser.*;
import ghidra.program.database.sourcemap.UserDataPathTransformer;
import ghidra.program.model.address.Address;
import ghidra.program.model.sourcemap.SourceMapEntry;
import ghidra.program.model.sourcemap.SourcePathTransformer;
import ghidra.util.task.MonitoredRunnable;
import ghidra.util.task.TaskBuilder;
public class OpenSourceFileAtLineInVSCodeScript extends GhidraScript {
private VSCodeIntegrationService vscodeService;
protected SourcePathTransformer pathTransformer;
protected File ideExecutableFile;
@Override
protected void run() throws Exception {
if (isRunningHeadless()) {
popup("This script cannot be run headlessly.");
return;
}
if (currentProgram == null) {
popup("This script requires an open program.");
return;
}
List<SourceMapEntry> entries =
currentProgram.getSourceFileManager().getSourceMapEntries(currentAddress);
if (entries.isEmpty()) {
popup("No source map entries found for " + currentAddress);
return;
}
if (!verifyAndSetIdeExe()) {
popup("Error acquiring IDE executable");
return;
}
pathTransformer = UserDataPathTransformer.getPathTransformer(currentProgram);
// if there is only one source map entry, send it to IDE
if (entries.size() == 1) {
SourceMapEntry entry = entries.get(0);
openInIde(pathTransformer.getTransformedPath(entry.getSourceFile(), true),
entry.getLineNumber());
}
// if there are multiple entries, pop up a table and let the user pick which ones
// to send to IDE
else {
TableChooserDialog tableDialog =
createTableChooserDialog("SourceMapEntries at " + currentAddress,
new OpenInIdeExecutor());
configureTableColumns(tableDialog);
for (SourceMapEntry entry : entries) {
tableDialog.add(new LocalPathRowObject(entry));
}
tableDialog.show();
}
}
/**
* Sets the field {@code ideExecutableField}
* @return false if the IDE executable field could not be acquired
*/
protected boolean verifyAndSetIdeExe() {
vscodeService = state.getTool().getService(VSCodeIntegrationService.class);
if (vscodeService == null) {
popup("VSCode service not configured for tool");
return false;
}
try {
ideExecutableFile = vscodeService.getVSCodeExecutableFile();
}
catch (FileNotFoundException e) {
printerr(e.getMessage());
return false;
}
return true;
}
/**
* Opens the source file at {@code transformedPath} at the given line number
* @param transformedPath source file path
* @param lineNumber line number
*/
protected void openInIde(String transformedPath, int lineNumber) {
// transformedPath is a file uri path so it uses forward slashes
// File constructor on windows can accept such paths
File localSourceFile = new File(transformedPath);
if (!localSourceFile.exists()) {
popup(transformedPath + " does not exist");
return;
}
MonitoredRunnable r = m -> {
try {
List<String> args = new ArrayList<>();
args.add(ideExecutableFile.getAbsolutePath());
args.add("--goto");
args.add(localSourceFile.getAbsolutePath() + ":" + lineNumber);
new ProcessBuilder(args).redirectErrorStream(true).start();
}
catch (Exception e) {
vscodeService.handleVSCodeError(
"Unexpected exception occurred while launching Visual Studio Code.", false,
null);
return;
}
};
new TaskBuilder("Opening File in VSCode", r)
.setHasProgress(false)
.setCanCancel(true)
.launchModal();
return;
}
////////////////table stuff //////////////////
private void configureTableColumns(TableChooserDialog tableDialog) {
StringColumnDisplay fileNameColumn = new StringColumnDisplay() {
@Override
public String getColumnName() {
return "Filename";
}
@Override
public String getColumnValue(AddressableRowObject rowObject) {
return ((LocalPathRowObject) rowObject).getFileName();
}
};
ColumnDisplay<Integer> lineNumberColumn = new AbstractComparableColumnDisplay<>() {
@Override
public Integer getColumnValue(AddressableRowObject rowObject) {
return ((LocalPathRowObject) rowObject).getLineNumber();
}
@Override
public String getColumnName() {
return "Line Number";
}
};
StringColumnDisplay localPathColumn = new StringColumnDisplay() {
@Override
public String getColumnValue(AddressableRowObject rowObject) {
return ((LocalPathRowObject) rowObject).getLocalPath();
}
@Override
public String getColumnName() {
return "Local Path";
}
};
tableDialog.addCustomColumn(fileNameColumn);
tableDialog.addCustomColumn(lineNumberColumn);
tableDialog.addCustomColumn(localPathColumn);
}
class LocalPathRowObject implements AddressableRowObject {
private Address baseAddress;
private String fileName;
private String localPath;
private int lineNumber;
LocalPathRowObject(SourceMapEntry entry) {
this.baseAddress = entry.getBaseAddress();
this.fileName = entry.getSourceFile().getFilename();
this.lineNumber = entry.getLineNumber();
this.localPath = pathTransformer.getTransformedPath(entry.getSourceFile(), true);
}
@Override
public Address getAddress() {
return baseAddress;
}
String getFileName() {
return fileName;
}
String getLocalPath() {
return localPath;
}
int getLineNumber() {
return lineNumber;
}
}
class OpenInIdeExecutor implements TableChooserExecutor {
@Override
public String getButtonName() {
return "Open Source File";
}
@Override
public boolean execute(AddressableRowObject rowObject) {
LocalPathRowObject row = (LocalPathRowObject) rowObject;
openInIde(row.getLocalPath(), row.getLineNumber());
return false;
}
}
}