GP-89 - Scripting - added JSON support via GSON

Closes #1982
This commit is contained in:
dragonmacher 2020-09-17 14:17:24 -04:00
parent 3ffbf09e52
commit 8216440278
11 changed files with 311 additions and 526 deletions

View File

@ -13,56 +13,51 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// List function names and entry point addresses to a file
// List function names and entry point addresses to a file in JSON format
//@category Functions
import ghidra.app.plugin.core.script.Ingredient;
import ghidra.app.plugin.core.script.IngredientDescription;
import ghidra.app.script.GatherParamPanel;
import java.io.File;
import java.io.FileWriter;
import com.google.gson.*;
import com.google.gson.stream.JsonWriter;
import ghidra.app.script.GhidraScript;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.*;
import java.io.*;
public class ExportFunctionInfoScript extends GhidraScript {
public class ExportFunctionInfoScript extends GhidraScript implements Ingredient {
private static final String NAME = "name";
private static final String ENTRY = "entry";
@Override
public void run() throws Exception {
IngredientDescription[] ingredients = getIngredientDescriptions();
for (int i = 0; i < ingredients.length; i++) {
state.addParameter(ingredients[i].getID(), ingredients[i].getLabel(),
ingredients[i].getType(), ingredients[i].getDefaultValue());
}
if (!state.displayParameterGatherer("Script Options")) {
return;
}
File outputNameFile = (File) state.getEnvironmentVar("FunctionNameOutputFile");
PrintWriter pWriter = new PrintWriter(new FileOutputStream(outputNameFile));
Gson gson = new GsonBuilder().setPrettyPrinting().create();
File outputFile = askFile("Please Select Output File", "Choose");
JsonWriter jsonWriter = new JsonWriter(new FileWriter(outputFile));
jsonWriter.beginArray();
Listing listing = currentProgram.getListing();
FunctionIterator iter = listing.getFunctions(true);
while (iter.hasNext() && !monitor.isCancelled()) {
Function f = iter.next();
String fName = f.getName();
String name = f.getName();
Address entry = f.getEntryPoint();
if (entry == null) {
pWriter.println("/* FUNCTION_NAME_ " + fName + " FUNCTION_ADDR_ " +
"NO_ENTRY_POINT" + " */");
println("WARNING: no entry point for " + fName);
}
else {
pWriter.println("/* FUNCTION_NAME_ " + fName + " FUNCTION_ADDR_ " + entry + " */");
}
JsonObject json = new JsonObject();
json.addProperty(NAME, name);
json.addProperty(ENTRY, entry.toString());
gson.toJson(json, jsonWriter);
}
pWriter.close();
}
@Override
public IngredientDescription[] getIngredientDescriptions() {
IngredientDescription[] retVal =
new IngredientDescription[] { new IngredientDescription("FunctionNameOutputFile",
"Output Function Name File", GatherParamPanel.FILE, "") };
return retVal;
}
jsonWriter.endArray();
jsonWriter.close();
println("Wrote functions to " + outputFile);
}
}

View File

@ -1,153 +0,0 @@
/* ###
* 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.
*/
//searches for pre-defined patterns and free space in code images
import java.util.List;
import java.util.stream.Collectors;
import ghidra.app.cmd.label.AddLabelCmd;
import ghidra.app.plugin.core.script.Ingredient;
import ghidra.app.plugin.core.script.IngredientDescription;
import ghidra.app.plugin.core.searchmem.RegExSearchData;
import ghidra.app.script.GatherParamPanel;
import ghidra.app.script.GhidraScript;
import ghidra.program.model.address.*;
import ghidra.program.model.listing.CodeUnit;
import ghidra.program.model.symbol.SourceType;
import ghidra.util.datastruct.ListAccumulator;
import ghidra.util.search.memory.*;
public class FindEmptySpaceScript extends GhidraScript implements Ingredient {
@Override
public void run() throws Exception {
IngredientDescription[] ingredients = getIngredientDescriptions();
for (IngredientDescription ingredient : ingredients) {
state.addParameter(ingredient.getID(), ingredient.getLabel(), ingredient.getType(),
ingredient.getDefaultValue());
}
if (!state.displayParameterGatherer("Empty Area Finder Options")) {
return;
}
String emptyArea = (String) state.getEnvironmentVar("EmptyAreaData");
Integer threshold = (Integer) state.getEnvironmentVar("Threshold");
Integer align = (Integer) state.getEnvironmentVar("Alignment");
String stem = (String) state.getEnvironmentVar("NameStem");
findEmptyAreas(emptyArea, threshold, align, stem);
}
protected void findEmptyAreas(String emptyArea, Integer threshold, Integer align, String stem)
throws Exception {
String emptyAreaPlusThreshold = emptyArea + "{" + threshold + ",}";
if (align < currentProgram.getLanguage().getInstructionAlignment()) {
align = currentProgram.getLanguage().getInstructionAlignment();
println(
" Adjusting alignment to minimum instruction alignment for this processor; new alignment is " +
align + " bytes");
}
else if ((align % currentProgram.getLanguage().getInstructionAlignment()) != 0) {
align = align + (align % currentProgram.getLanguage().getInstructionAlignment());
println(
" Adjusting alignment to match processor instruction alignment; new alignment is " +
align + " bytes");
}
println(" Searching initialized memory for " + emptyAreaPlusThreshold +
"; minimum size = " + threshold + " bytes ; alignment = " + align +
" bytes; search limited to first 1000 matches");
AddressSetView addrs = currentProgram.getMemory().getLoadedAndInitializedAddressSet();
SearchInfo searchInfo = new SearchInfo(new RegExSearchData(emptyAreaPlusThreshold), 1000,
false, true, align, true, null);
RegExMemSearcherAlgorithm searcher =
new RegExMemSearcherAlgorithm(searchInfo, addrs, currentProgram, true);
ListAccumulator<MemSearchResult> accumulator = new ListAccumulator<>();
searcher.search(accumulator, monitor);
List<MemSearchResult> results = accumulator.asList();
List<Address> addresses =
results.stream().map(r -> r.getAddress()).collect(Collectors.toList());
int numMatches = 0;
long maxLen = 0;
if (results.isEmpty()) {
println(" FAILURE: Could not find any empty areas with regexp = " +
emptyAreaPlusThreshold + "and alignment = " + align + " bytes");
return;
}
//put matches into an address set, thereby coalescing ranges
AddressSet addrSet = new AddressSet();
for (MemSearchResult result : results) {
Address match = result.getAddress();
int len = result.getLength();
addrSet.addRange(match, match.addNoWrap(len));
}
//iterate over the set items that matched
for (AddressRange range : addrSet) {
long len = range.getLength();
addLabelAndExportSym(range.getMinAddress(), len, stem, "emptyArea", "size = " + len +
" bytes (alignment = " + align + " bytes; min size = " + threshold + " bytes)");
numMatches++;
if (len > maxLen) {
maxLen = len;
}
}
println(" Found " + numMatches +
" empty areas meeting size and alignment requirements; maximum length found = " +
maxLen + " bytes");
}
protected void addLabelAndExportSym(Address matchAddr, long len, String stem, String tag,
String optComment) {
String label = stem + "_" + matchAddr + "_" + len;
label = label.replaceAll(":", "_");
String comment = "{@exportsym " + tag + " " + optComment + "}";
CodeUnit cd = currentProgram.getListing().getCodeUnitAt(matchAddr);
if (cd == null) {
return;
}
AddLabelCmd lcmd = new AddLabelCmd(matchAddr, label, false, SourceType.USER_DEFINED);
lcmd.applyTo(currentProgram);
String commentThere = cd.getComment(CodeUnit.EOL_COMMENT);
if (commentThere != null) {
comment = commentThere + "\n" + comment;
}
cd.setComment(CodeUnit.EOL_COMMENT, comment);
}
@Override
public IngredientDescription[] getIngredientDescriptions() {
IngredientDescription[] retVal = new IngredientDescription[] {
new IngredientDescription("EmptyAreaData", "Regular Expression Data Pattern",
GatherParamPanel.STRING, "\\xff"),
new IngredientDescription("Threshold", "Minimum Size (decimal bytes)",
GatherParamPanel.INTEGER, ""),
new IngredientDescription("Alignment", "Alignment (decimal bytes)",
GatherParamPanel.INTEGER, ""),
new IngredientDescription("NameStem", "Optional Label Stem", GatherParamPanel.STRING,
"EMPTY") };
return retVal;
}
}

View File

@ -1,97 +0,0 @@
/* ###
* 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.
*/
//Places header structure on overlay segments
import java.io.File;
import ghidra.app.plugin.core.script.Ingredient;
import ghidra.app.plugin.core.script.IngredientDescription;
import ghidra.app.script.GatherParamPanel;
import ghidra.app.script.GhidraScript;
import ghidra.program.model.address.*;
import ghidra.program.model.data.DataType;
import ghidra.program.model.data.FileDataTypeManager;
import ghidra.program.model.util.CodeUnitInsertionException;
public class OverlayHeadersScript extends GhidraScript implements Ingredient {
@Override
public void run() throws Exception {
// Get our configuration info and save for other scripts to use
IngredientDescription[] ingredients = getIngredientDescriptions();
for (IngredientDescription ingredient : ingredients) {
state.addParameter(ingredient.getID(), ingredient.getLabel(), ingredient.getType(),
ingredient.getDefaultValue());
}
if (!state.displayParameterGatherer("Script Options")) {
return;
}
// Get our parameters for use here
String overlayName = (String) state.getEnvironmentVar("OverlayName");
File dataTypeArchive = (File) state.getEnvironmentVar("OverlayHeaderArchive");
String dataTypeName = (String) state.getEnvironmentVar("OverlayHeaderName"); // must include datatype category
// Create our history logger
Address histAddr = currentProgram.getMemory().getMinAddress();
String tmpString = "\nScript: OverlayHeaders()\n";
tmpString = tmpString + " Add " + dataTypeName + " structure\n from " +
dataTypeArchive.toString();
// Get the datatype that we want to place on the overlays
FileDataTypeManager dataTypeFileManager = openDataTypeArchive(dataTypeArchive, true);
DataType dataType = dataTypeFileManager.getDataType(dataTypeName);
dataTypeFileManager.close();
if (dataType == null) {
println("Can't find data type " + dataTypeName + " in " + dataTypeArchive.toString());
throw new Exception(
"Can't find data type " + dataTypeName + "\n in " + dataTypeArchive.toString());
}
// Now iterate over overlays the lay down structure
AddressSetView searchSet = currentProgram.getMemory();
AddressRangeIterator addressRanges = searchSet.getAddressRanges(true);
monitor.initialize(searchSet.getNumAddresses());
int progressCount = 0;
while (addressRanges.hasNext() && !monitor.isCancelled()) {
AddressRange range = addressRanges.next();
Address startAddr = range.getMinAddress();
String rangeName = startAddr.toString();
if (rangeName.startsWith(overlayName)) {
try {
createData(startAddr, dataType);
}
catch (CodeUnitInsertionException ex) {
println("Error creating data type: " + ex);
}
}
progressCount += range.getLength();
monitor.setProgress(progressCount);
}
}
@Override
public IngredientDescription[] getIngredientDescriptions() {
IngredientDescription[] retVal = new IngredientDescription[] {
new IngredientDescription("OverlayName", "Overlay Name", GatherParamPanel.STRING, "ov"),
new IngredientDescription("OverlayHeaderArchive", "Overlay Header Archive",
GatherParamPanel.FILE, ""),
new IngredientDescription("OverlayHeaderName", "Overlay Header Name",
GatherParamPanel.STRING, "/overlay_header") };
return retVal;
}
}

View File

@ -1,20 +0,0 @@
/* ###
* 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.
*/
package ghidra.app.plugin.core.script;
public interface Ingredient {
IngredientDescription [] getIngredientDescriptions();
}

View File

@ -1,54 +0,0 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* 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 ghidra.app.plugin.core.script;
public class IngredientDescription {
private boolean visited;
private String id;
private String label;
private int type;
private Object defaultValue;
public IngredientDescription(String id, String label, int type, Object defaultValue) {
this.id = id;
this.label = label;
this.type = type;
this.defaultValue = defaultValue;
visited = false;
}
public boolean wasVisited() {
return visited;
}
public String getLabel() {
return label;
}
public String getID() {
return id;
}
public int getType() {
return type;
}
public Object getDefaultValue() {
return defaultValue;
}
}

View File

@ -0,0 +1,156 @@
/* ###
* 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.
*/
package ghidra.app.plugin.core.script;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import com.google.gson.Gson;
import com.google.gson.stream.JsonReader;
import docking.widgets.filechooser.GhidraFileChooser;
import generic.json.Json;
import ghidra.framework.Application;
import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.Program;
import ghidra.test.*;
/**
* Tests the {@code ExportFunctionInfoScript}, which writes Ghidra function object info in JSON
* form for the entire program
*/
public class ExportFunctionInfoScriptTest extends AbstractGhidraHeadedIntegrationTest {
private TestEnv env;
private File script;
private Program program;
private Function f1;
private Function f2;
@Before
public void setUp() throws Exception {
program = buildProgram();
env = new TestEnv();
env.launchDefaultTool(program);
String scriptPath = "ghidra_scripts/ExportFunctionInfoScript.java";
script = Application.getModuleFile("Base", scriptPath).getFile(true);
}
private Program buildProgram() throws Exception {
ToyProgramBuilder builder = new ToyProgramBuilder("Test", true, this);
builder.createMemory(".text", "0x1001000", 0x40);
f1 = builder.createFunction("0x1001000");
f2 = builder.createFunction("0x1001020");
return builder.getProgram();
}
@Test
public void testScript() throws Exception {
File outputFile = createTempFileForTest();
ScriptTaskListener listener = env.runScript(script);
chooseFile(outputFile);
waitForScriptCompletion(listener, 20000);
assertFunctionsInFile(outputFile, f1, f2);
}
private void assertFunctionsInFile(File file, Function... functions)
throws Exception {
List<Function> testFunctions = new ArrayList<>(List.of(f1, f2));
List<TestJsonFunction> jsons = readFromJson(file);
jsons.forEach(jsonFunction -> assertFunction(jsonFunction, testFunctions));
assertThat("Not all program functions written to json file",
testFunctions, is(empty()));
}
private List<TestJsonFunction> readFromJson(File file) throws Exception {
List<TestJsonFunction> results = new ArrayList<>();
Gson gson = new Gson();
BufferedReader br = new BufferedReader(new FileReader(file));
JsonReader reader = new JsonReader(br);
// the file is an array of objects
reader.beginArray();
while (reader.hasNext()) {
TestJsonFunction function = gson.fromJson(reader, TestJsonFunction.class);
results.add(function);
}
reader.endArray();
reader.close();
return results;
}
private void assertFunction(TestJsonFunction function, List<Function> testFunctions) {
Function match = null;
for (Function expected : testFunctions) {
if (function.matches(expected)) {
match = expected;
break;
}
}
assertNotNull("Unexpected function written to file", match);
testFunctions.remove(match);
}
private void chooseFile(File file) throws Exception {
GhidraFileChooser chooser = waitForDialogComponent(GhidraFileChooser.class);
runSwing(() -> chooser.setSelectedFile(file));
waitForUpdateOnChooser(chooser);
pressButtonByText(chooser.getComponent(), "Choose");
waitForSwing();
}
private class TestJsonFunction {
private String name;
private String entry;
boolean matches(Function expected) {
return name.equals(expected.getName()) &&
entry.equals(expected.getEntryPoint().toString());
}
@Override
public String toString() {
// this is only for debug; not required
return Json.toString(this);
}
}
}

View File

@ -1,57 +0,0 @@
/* ###
* 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.
*/
//Decompile an entire program
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import ghidra.app.plugin.core.script.Ingredient;
import ghidra.app.plugin.core.script.IngredientDescription;
import ghidra.app.script.GatherParamPanel;
import ghidra.app.script.GhidraScript;
import ghidra.app.util.Option;
import ghidra.app.util.exporter.CppExporter;
public class Decompile extends GhidraScript implements Ingredient {
@Override
public void run() throws Exception {
IngredientDescription[] ingredients = getIngredientDescriptions();
for (IngredientDescription ingredient : ingredients) {
state.addParameter(ingredient.getID(), ingredient.getLabel(), ingredient.getType(),
ingredient.getDefaultValue());
}
if (!state.displayParameterGatherer("Script Options")) {
return;
}
File outputFile = (File) state.getEnvironmentVar("COutputFile");
CppExporter cppExporter = new CppExporter();
List<Option> options = new ArrayList<Option>();
options.add(new Option(CppExporter.CREATE_HEADER_FILE, new Boolean(false)));
cppExporter.setOptions(options);
cppExporter.setExporterServiceProvider(state.getTool());
cppExporter.export(outputFile, currentProgram, null, monitor);
}
@Override
public IngredientDescription[] getIngredientDescriptions() {
IngredientDescription[] retVal = new IngredientDescription[] {
new IngredientDescription("COutputFile", "Output C File", GatherParamPanel.FILE, "") };
return retVal;
}
}

View File

@ -18,8 +18,8 @@ dependencies {
compile "org.apache.commons:commons-collections4:4.1"
compile "org.apache.commons:commons-lang3:3.9"
compile "org.apache.commons:commons-text:1.6"
compile "commons-io:commons-io:2.6"
compile "commons-io:commons-io:2.6"
compile "com.google.code.gson:gson:2.8.6"
compileOnly "junit:junit:4.12"
}

View File

@ -67,7 +67,6 @@ public class ExtensionUtils {
public static String PROPERTIES_FILE_NAME = "extension.properties";
public static String PROPERTIES_FILE_NAME_UNINSTALLED = "extension.properties.uninstalled";
/**
* Returns a set of all extensions known to Ghidra, represented by
* {@link ExtensionDetails} objects. This will include all installed
@ -76,7 +75,7 @@ public class ExtensionUtils {
* Note that this method will only look in the known extension folder locations:
* <ul>
* <li>{@link ApplicationLayout#getExtensionArchiveDir}</li>
* <li>{@link ApplicationLayout#getExtensionInstallationDir}</li>
* <li>{@link ApplicationLayout#getExtensionInstallationDirs}</li>
* </ul>
* If users install extensions from other locations, the installed version of
* the extension will be known, but the source archive location will not be retained.
@ -123,13 +122,14 @@ public class ExtensionUtils {
/**
* Returns all installed extensions. These are all the extensions found in
* {@link ApplicationLayout#getExtensionInstallationDir}.
* {@link ApplicationLayout#getExtensionInstallationDirs}.
*
* @param includeUninstalled if true, include extensions that have been marked for removal
* @return set of installed extensions
* @throws ExtensionException if the extension details cannot be retrieved
*/
public static Set<ExtensionDetails> getInstalledExtensions(boolean includeUninstalled) throws ExtensionException {
public static Set<ExtensionDetails> getInstalledExtensions(boolean includeUninstalled)
throws ExtensionException {
ApplicationLayout layout = Application.getApplicationLayout();
@ -165,7 +165,7 @@ public class ExtensionUtils {
return extensions;
}
/**
* Returns all archived extensions. These are all the extensions found in
* {@link ApplicationLayout#getExtensionArchiveDir}.
@ -215,7 +215,6 @@ public class ExtensionUtils {
}
}
}
return extensions;
}
@ -262,7 +261,7 @@ public class ExtensionUtils {
e);
return false;
}
return runInstallTask(rFile.getFile(false));
}
@ -306,7 +305,7 @@ public class ExtensionUtils {
}
ResourceFile file = new ResourceFile(extension.getArchivePath());
// We need to handle a special case: If the user selects an extension to uninstall using
// the GUI then tries to reinstall it without restarting Ghidra, the extension hasn't actually
// been removed yet; just the manifest file has been renamed. In this case we don't need to go through
@ -315,7 +314,7 @@ public class ExtensionUtils {
if (installDir.exists()) {
return restoreStateFiles(installDir);
}
if (install(file)) {
extension.setInstallPath(installDir + File.separator + extension.getName());
return true;
@ -421,7 +420,7 @@ public class ExtensionUtils {
return false;
}
/**
* Returns true if the given file is a valid .zip archive.
*
@ -453,7 +452,7 @@ public class ExtensionUtils {
throw new ExtensionException(e.getMessage(), ExtensionExceptionType.ZIP_ERROR);
}
}
/**
* Returns a list of files representing all the <code>extension.properties</code> files found
* under a given directory. This will ONLY search the given directory and its immediate children.
@ -517,8 +516,10 @@ public class ExtensionUtils {
List<ResourceFile> tempFiles = Arrays.asList(rfiles);
Optional<ResourceFile> file =
tempFiles.stream().filter(f -> f.getName().equals(PROPERTIES_FILE_NAME) ||
f.getName().equals(PROPERTIES_FILE_NAME_UNINSTALLED)).findFirst();
tempFiles.stream()
.filter(f -> f.getName().equals(PROPERTIES_FILE_NAME) ||
f.getName().equals(PROPERTIES_FILE_NAME_UNINSTALLED))
.findFirst();
if (file.isPresent()) {
return file.get();
}
@ -547,14 +548,14 @@ public class ExtensionUtils {
}
else {
copyToInstallationFolder(file, monitor);
}
}
installed.set(true);
}
catch (ExtensionException e) {
// If there's a problem copying files, check to see if there's already an extension
// with this name in the install location that was slated for removal. If so, just
// restore the extension properties and manifest files.
if (e.getExceptionType() == ExtensionExceptionType.COPY_ERROR ||
if (e.getExceptionType() == ExtensionExceptionType.COPY_ERROR ||
e.getExceptionType() == ExtensionExceptionType.DUPLICATE_FILE_ERROR) {
File errorFile = e.getErrorFile();
@ -563,14 +564,15 @@ public class ExtensionUtils {
ResourceFile installDir = Application.getApplicationLayout()
.getExtensionInstallationDirs()
.get(0);
// Get the root directory of the extension (strip off the install folder location and
// grab the first part of the remaining path).
//
// eg: If errorFile is "/Users/johnG/Ghidra/Extensions/MyExtensionName/subdir1/problemFile"
// And installDir is "/Users/johnG/Ghidra/Extensions"
// We need to get "MyExtensionName"
String extPath = errorFile.getAbsolutePath().substring(installDir.getAbsolutePath().length()+1);
String extPath = errorFile.getAbsolutePath()
.substring(installDir.getAbsolutePath().length() + 1);
int slashIndex = extPath.indexOf(File.separator);
String extName;
if (slashIndex == -1) {
@ -580,12 +582,13 @@ public class ExtensionUtils {
extName = extPath.substring(0, extPath.indexOf(File.separator));
}
boolean success = restoreStateFiles(new File(installDir.getAbsolutePath() + File.separator + extName));
boolean success = restoreStateFiles(
new File(installDir.getAbsolutePath() + File.separator + extName));
installed.set(success);
}
}
if (installed.get() == false) {
Msg.showError(null, null, "Installation Error", "Error installing extension [" +
file.getName() + "]." + " " + e.getExceptionType());
@ -601,7 +604,7 @@ public class ExtensionUtils {
return installed.get();
}
/**
* Recursively searches a given directory for any module manifest and extension
* properties files that are in an installed state and converts them to an uninstalled
@ -617,7 +620,7 @@ public class ExtensionUtils {
* @return false if any renames fail
*/
public static boolean removeStateFiles(ExtensionDetails extension) {
// Sanity check
if (extension == null || extension.getInstallPath() == null ||
extension.getInstallPath().isEmpty()) {
@ -625,23 +628,29 @@ public class ExtensionUtils {
}
boolean success = true;
List<File> manifestFiles = new ArrayList<>();
ExtensionUtils.findFilesWithName(new File(extension.getInstallPath()), ModuleUtilities.MANIFEST_FILE_NAME, manifestFiles);
ExtensionUtils.findFilesWithName(new File(extension.getInstallPath()),
ModuleUtilities.MANIFEST_FILE_NAME, manifestFiles);
for (File f : manifestFiles) {
if (f.exists()) {
File newFile = new File(f.getAbsolutePath().replace(ModuleUtilities.MANIFEST_FILE_NAME, ModuleUtilities.MANIFEST_FILE_NAME_UNINSTALLED) );
File newFile = new File(f.getAbsolutePath()
.replace(ModuleUtilities.MANIFEST_FILE_NAME,
ModuleUtilities.MANIFEST_FILE_NAME_UNINSTALLED));
if (!f.renameTo(newFile)) {
success = false;
}
}
}
List<File> propFiles = new ArrayList<>();
ExtensionUtils.findFilesWithName(new File(extension.getInstallPath()), ExtensionUtils.PROPERTIES_FILE_NAME, propFiles);
ExtensionUtils.findFilesWithName(new File(extension.getInstallPath()),
ExtensionUtils.PROPERTIES_FILE_NAME, propFiles);
for (File f : propFiles) {
if (f.exists()) {
File newFile = new File(f.getAbsolutePath().replace(ExtensionUtils.PROPERTIES_FILE_NAME, ExtensionUtils.PROPERTIES_FILE_NAME_UNINSTALLED) );
File newFile = new File(f.getAbsolutePath()
.replace(ExtensionUtils.PROPERTIES_FILE_NAME,
ExtensionUtils.PROPERTIES_FILE_NAME_UNINSTALLED));
if (!f.renameTo(newFile)) {
success = false;
}
@ -665,34 +674,37 @@ public class ExtensionUtils {
* @return false if any renames fail
*/
public static boolean restoreStateFiles(File rootDir) {
boolean success = true;
List<File> manifestFiles = new ArrayList<>();
findFilesWithName(rootDir, ModuleUtilities.MANIFEST_FILE_NAME_UNINSTALLED, manifestFiles);
for (File f : manifestFiles) {
if (f.exists()) {
File newFile = new File(f.getAbsolutePath().replace(ModuleUtilities.MANIFEST_FILE_NAME_UNINSTALLED, ModuleUtilities.MANIFEST_FILE_NAME) );
File newFile = new File(f.getAbsolutePath()
.replace(ModuleUtilities.MANIFEST_FILE_NAME_UNINSTALLED,
ModuleUtilities.MANIFEST_FILE_NAME));
if (!f.renameTo(newFile)) {
success = false;
}
}
}
List<File> propFiles = new ArrayList<>();
findFilesWithName(rootDir, PROPERTIES_FILE_NAME_UNINSTALLED, propFiles);
for (File f : propFiles) {
if (f.exists()) {
File newFile = new File(f.getAbsolutePath().replace(PROPERTIES_FILE_NAME_UNINSTALLED, PROPERTIES_FILE_NAME) );
File newFile = new File(f.getAbsolutePath()
.replace(PROPERTIES_FILE_NAME_UNINSTALLED, PROPERTIES_FILE_NAME));
if (!f.renameTo(newFile)) {
success = false;
}
}
}
return success;
}
/**
*
* @param root the starting directory to search recursively
@ -700,22 +712,22 @@ public class ExtensionUtils {
* @param foundFiles list of all matching files
*/
public static void findFilesWithName(File root, String fileName, List<File> foundFiles) {
if (root == null || foundFiles == null) {
return;
return;
}
if (root.isDirectory()) {
File[] files = root.listFiles();
if (files != null) {
for (File file : files) {
findFilesWithName(file, fileName, foundFiles);
}
}
}
else if (root.isFile() && root.getName().equals(fileName)) {
foundFiles.add(root);
}
if (root.isDirectory()) {
File[] files = root.listFiles();
if (files != null) {
for (File file : files) {
findFilesWithName(file, fileName, foundFiles);
}
}
}
else if (root.isFile() && root.getName().equals(fileName)) {
foundFiles.add(root);
}
}
/**
@ -761,7 +773,7 @@ public class ExtensionUtils {
throws ExtensionException, CancelledException {
File newDir = null;
try {
try {
newDir =
new File(Application.getApplicationLayout().getExtensionInstallationDirs().get(0) +
File.separator + extension.getName());
@ -774,11 +786,11 @@ public class ExtensionUtils {
}
/**
* Unpacks a given zip file to {@link ApplicationLayout#getExtensionInstallationDir}. The
* Unpacks a given zip file to {@link ApplicationLayout#getExtensionInstallationDirs}. The
* file permissions in the original zip will be retained.
* <p>
* Note: This method uses the Apache zip files since they keep track of permissions info;
* the built-in java objects (ZipEntry et al.) do not.
* the built-in java objects (e.g., ZipEntry) do not.
*
* @param zipFile the zip file to unpack
* @param monitor the task monitor
@ -993,7 +1005,7 @@ public class ExtensionUtils {
String author = props.getProperty("author");
String date = props.getProperty("createdOn");
String version = props.getProperty("version");
return new ExtensionDetails(name, desc, author, date, version);
}

View File

@ -4,9 +4,7 @@ eclipse.project.name = '_JsonDoclet'
apply plugin: 'java'
dependencies {
compile('com.googlecode.json-simple:json-simple:1.1.1') {
exclude group: 'junit', module: 'junit'
}
compile "com.google.code.gson:gson:2.8.6"
}
rootProject.createJsondocs.dependsOn jar

View File

@ -24,9 +24,7 @@ import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.tools.Diagnostic.Kind;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import com.google.gson.*;
import com.sun.source.doctree.*;
import com.sun.source.util.DocTrees;
@ -35,8 +33,9 @@ import jdk.javadoc.doclet.*;
/**
* Doclet that outputs javadoc in JSON format (instead of HTML). Things like Python can then
* read in the JSON and easily access all of the javadoc elements.
*
* To run: gradle zipJavadocs
*/
@SuppressWarnings("unchecked")
public class JsonDoclet implements Doclet {
private Reporter log;
@ -45,6 +44,11 @@ public class JsonDoclet implements Doclet {
private DocletEnvironment docEnv;
private DocTrees docTrees;
private Gson gson = new GsonBuilder()
.setPrettyPrinting()
.serializeNulls()
.create();
@Override
public void init(Locale locale, Reporter reporter) {
this.log = reporter;
@ -128,13 +132,13 @@ public class JsonDoclet implements Doclet {
}
/**
* Converts a class {@link TypeElement} to a {@link JSONObject}.
* Converts a class {@link TypeElement} to a {@link JsonObject}.
*
* @param classElement the class {@link TypeElement} to convert
* @return A json object that represents the class.
*/
private JSONObject classToJson(TypeElement classElement) {
JSONObject classObj = new JSONObject();
private JsonObject classToJson(TypeElement classElement) {
JsonObject classObj = new JsonObject();
processClassAttributes(classElement, classObj);
processFieldAndMethodAttributes(classElement, classObj);
return classObj;
@ -146,11 +150,11 @@ public class JsonDoclet implements Doclet {
* @param classElement the class element to parse
* @param classObj the json object to populate
*/
private void processClassAttributes(TypeElement classElement, JSONObject classObj) {
classObj.put("name", classElement.getSimpleName().toString());
classObj.put("comment", getComment(docTrees.getDocCommentTree(classElement)));
classObj.put("javadoc", getJavadoc(docTrees.getDocCommentTree(classElement)));
classObj.put("static", classElement.getModifiers().contains(Modifier.STATIC));
private void processClassAttributes(TypeElement classElement, JsonObject classObj) {
classObj.addProperty("name", classElement.getSimpleName().toString());
classObj.addProperty("comment", getComment(docTrees.getDocCommentTree(classElement)));
classObj.addProperty("javadoc", getJavadoc(docTrees.getDocCommentTree(classElement)));
classObj.addProperty("static", classElement.getModifiers().contains(Modifier.STATIC));
addInterfaces(classElement, classObj);
addSuperClass(classElement, classObj);
}
@ -162,8 +166,8 @@ public class JsonDoclet implements Doclet {
* @param typeElement the {@link TypeElement} to parse
* @param obj the json object to populate
*/
private void addInterfaces(TypeElement typeElement, JSONObject obj) {
JSONArray interfaceArray = new JSONArray();
private void addInterfaces(TypeElement typeElement, JsonObject obj) {
JsonArray interfaceArray = new JsonArray();
//@formatter:off
typeElement.getInterfaces()
@ -176,7 +180,7 @@ public class JsonDoclet implements Doclet {
.forEach(ifaceTypeElement -> interfaceArray.add(ifaceTypeElement.getQualifiedName().toString()));
//@formatter:on
obj.put("implements", interfaceArray);
obj.add("implements", interfaceArray);
}
/**
@ -186,12 +190,12 @@ public class JsonDoclet implements Doclet {
* @param typeElement the {@link TypeElement} to parse
* @param obj the json object to populate
*/
private void addSuperClass(TypeElement typeElement, JSONObject obj) {
private void addSuperClass(TypeElement typeElement, JsonObject obj) {
if (typeElement.getSuperclass() instanceof DeclaredType) {
DeclaredType declaredType = (DeclaredType) typeElement.getSuperclass();
if (declaredType.asElement() instanceof TypeElement) {
TypeElement typeEl = (TypeElement) declaredType.asElement();
obj.put("extends", typeEl.getQualifiedName().toString());
obj.addProperty("extends", typeEl.getQualifiedName().toString());
}
}
}
@ -202,29 +206,29 @@ public class JsonDoclet implements Doclet {
* @param classElement the class to parse
* @param classObj the json object to populate
*/
private void processFieldAndMethodAttributes(TypeElement classElement, JSONObject classObj) {
private void processFieldAndMethodAttributes(TypeElement classElement, JsonObject classObj) {
JSONArray fieldArray = new JSONArray();
JSONArray methodArray = new JSONArray();
JsonArray fieldArray = new JsonArray();
JsonArray methodArray = new JsonArray();
for (Element el : classElement.getEnclosedElements()) {
JSONObject obj = new JSONObject();
obj.put("name", el.getSimpleName().toString());
obj.put("comment", getComment(docTrees.getDocCommentTree(el)));
obj.put("javadoc", getJavadoc(docTrees.getDocCommentTree(el)));
obj.put("static", el.getModifiers().contains(Modifier.STATIC));
JsonObject obj = new JsonObject();
obj.addProperty("name", el.getSimpleName().toString());
obj.addProperty("comment", getComment(docTrees.getDocCommentTree(el)));
obj.addProperty("javadoc", getJavadoc(docTrees.getDocCommentTree(el)));
obj.addProperty("static", el.getModifiers().contains(Modifier.STATIC));
switch (el.getKind()) {
case FIELD:
VariableElement varElement = (VariableElement) el;
obj.put("type_long", getTypeLong(el.asType()));
obj.put("type_short", getTypeShort(el.asType()));
obj.addProperty("type_long", getTypeLong(el.asType()));
obj.addProperty("type_short", getTypeShort(el.asType()));
Object constantValue = varElement.getConstantValue();
if (constantValue instanceof String) {
constantValue = "\"" + constantValue + "\"";
}
obj.put("constant_value", Objects.toString(constantValue, null)); // only applies to 'final'
obj.addProperty("constant_value", Objects.toString(constantValue, null)); // only applies to 'final'
fieldArray.add(obj);
break;
case CONSTRUCTOR:
@ -255,8 +259,8 @@ public class JsonDoclet implements Doclet {
}
}
classObj.put("fields", fieldArray);
classObj.put("methods", methodArray);
classObj.add("fields", fieldArray);
classObj.add("methods", methodArray);
}
/**
@ -266,14 +270,14 @@ public class JsonDoclet implements Doclet {
* @param execElement the element to parse
* @param obj the json object
*/
private void addParams(ExecutableElement execElement, JSONObject obj) {
private void addParams(ExecutableElement execElement, JsonObject obj) {
JSONArray paramsArray = new JSONArray();
JsonArray paramsArray = new JsonArray();
for (VariableElement varElement : execElement.getParameters()) {
JSONObject paramObj = new JSONObject();
paramObj.put("name", varElement.getSimpleName().toString());
paramObj.put("type_long", getTypeLong(varElement.asType()));
paramObj.put("type_short", getTypeShort(varElement.asType()));
JsonObject paramObj = new JsonObject();
paramObj.addProperty("name", varElement.getSimpleName().toString());
paramObj.addProperty("type_long", getTypeLong(varElement.asType()));
paramObj.addProperty("type_short", getTypeShort(varElement.asType()));
String comment = "";
DocCommentTree commentTree = docTrees.getDocCommentTree(execElement);
if (commentTree != null) {
@ -287,10 +291,10 @@ public class JsonDoclet implements Doclet {
}
}
}
paramObj.put("comment", comment);
paramObj.addProperty("comment", comment);
paramsArray.add(paramObj);
}
obj.put("params", paramsArray);
obj.add("params", paramsArray);
}
/**
@ -300,11 +304,11 @@ public class JsonDoclet implements Doclet {
* @param execElement the element to parse
* @param obj the json object
*/
private void addReturn(ExecutableElement execElement, JSONObject obj) {
private void addReturn(ExecutableElement execElement, JsonObject obj) {
TypeMirror returnType = execElement.getReturnType();
JSONObject returnObj = new JSONObject();
returnObj.put("type_long", getTypeLong(returnType));
returnObj.put("type_short", getTypeShort(returnType));
JsonObject returnObj = new JsonObject();
returnObj.addProperty("type_long", getTypeLong(returnType));
returnObj.addProperty("type_short", getTypeShort(returnType));
String comment = "";
DocCommentTree commentTree = docTrees.getDocCommentTree(execElement);
if (commentTree != null) {
@ -314,8 +318,8 @@ public class JsonDoclet implements Doclet {
}
}
}
returnObj.put("comment", comment);
obj.put("return", returnObj);
returnObj.addProperty("comment", comment);
obj.add("return", returnObj);
}
/**
@ -325,14 +329,14 @@ public class JsonDoclet implements Doclet {
* @param execElement the element to parse
* @param obj the json object
*/
private void addExceptions(ExecutableElement execElement, JSONObject obj) {
JSONArray throwsArray = new JSONArray();
private void addExceptions(ExecutableElement execElement, JsonObject obj) {
JsonArray throwsArray = new JsonArray();
for (TypeMirror thrownType : execElement.getThrownTypes()) {
JSONObject throwObj = new JSONObject();
JsonObject throwObj = new JsonObject();
String typeLong = getTypeLong(thrownType);
String typeShort = getTypeShort(thrownType);
throwObj.put("type_long", typeLong);
throwObj.put("type_short", typeShort);
throwObj.addProperty("type_long", typeLong);
throwObj.addProperty("type_short", typeShort);
String comment = "";
DocCommentTree commentTree = docTrees.getDocCommentTree(execElement);
if (commentTree != null) {
@ -346,10 +350,10 @@ public class JsonDoclet implements Doclet {
}
}
}
throwObj.put("comment", comment);
throwObj.addProperty("comment", comment);
throwsArray.add(throwObj);
}
obj.put("throws", throwsArray);
obj.add("throws", throwsArray);
}
/**
@ -446,11 +450,12 @@ public class JsonDoclet implements Doclet {
* @param qualifiedName The qualified class name. This name will get converted into a directory
* structure.
*/
private void writeJsonToFile(JSONObject json, Name qualifiedName) {
private void writeJsonToFile(JsonObject json, Name qualifiedName) {
File jsonFile = new File(destDir, qualifiedName.toString().replace('.', '/') + ".json");
jsonFile.getParentFile().mkdirs();
try (PrintWriter writer = new PrintWriter(new FileWriter(jsonFile))) {
writer.println(json.toJSONString());
writer.println(gson.toJson(json));
}
catch (IOException e) {
e.printStackTrace();