Merge remote-tracking branch

'origin/GP-843_dragonmacher_PR-2846_goatshriek_python-header' (Closes
#2846)
This commit is contained in:
ghidra1 2021-04-16 16:05:56 -04:00
commit 76a73095df
7 changed files with 599 additions and 58 deletions

View file

@ -17,6 +17,7 @@ package ghidra.app.script;
import java.io.IOException; import java.io.IOException;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.util.regex.Pattern;
import generic.jar.ResourceFile; import generic.jar.ResourceFile;
import ghidra.util.classfinder.ExtensionPoint; import ghidra.util.classfinder.ExtensionPoint;
@ -93,6 +94,24 @@ public abstract class GhidraScriptProvider
public abstract void createNewScript(ResourceFile newScript, String category) public abstract void createNewScript(ResourceFile newScript, String category)
throws IOException; throws IOException;
/**
* Returns a Pattern that matches block comment openings.
* If block comments are not supported by this provider, then this returns null.
* @return the Pattern for block comment openings, null if block comments are not supported
*/
public Pattern getBlockCommentStart() {
return null;
}
/**
* Returns a Pattern that matches block comment closings.
* If block comments are not supported by this provider, then this returns null.
* @return the Pattern for block comment closings, null if block comments are not supported
*/
public Pattern getBlockCommentEnd() {
return null;
}
/** /**
* Returns the comment character. * Returns the comment character.
* For example, "//" or "#". * For example, "//" or "#".

View file

@ -17,6 +17,7 @@ package ghidra.app.script;
import java.io.*; import java.io.*;
import java.util.Collections; import java.util.Collections;
import java.util.regex.Pattern;
import org.osgi.framework.Bundle; import org.osgi.framework.Bundle;
@ -26,6 +27,9 @@ import ghidra.util.Msg;
import ghidra.util.task.TaskMonitor; import ghidra.util.task.TaskMonitor;
public class JavaScriptProvider extends GhidraScriptProvider { public class JavaScriptProvider extends GhidraScriptProvider {
private static final Pattern BLOCK_COMMENT_START = Pattern.compile("/\\*");
private static final Pattern BLOCK_COMMENT_END = Pattern.compile("\\*/");
private final BundleHost bundleHost; private final BundleHost bundleHost;
/** /**
@ -159,6 +163,26 @@ public class JavaScriptProvider extends GhidraScriptProvider {
writer.close(); writer.close();
} }
/**
* Returns a Pattern that matches block comment openings.
* For Java this is "/*".
* @return the Pattern for Java block comment openings
*/
@Override
public Pattern getBlockCommentStart() {
return BLOCK_COMMENT_START;
}
/**
* Returns a Pattern that matches block comment closings.
* In Java this is an asterisk followed by a forward slash.
* @return the Pattern for Java block comment closings
*/
@Override
public Pattern getBlockCommentEnd() {
return BLOCK_COMMENT_END;
}
@Override @Override
public String getCommentCharacter() { public String getCommentCharacter() {
return "//"; return "//";

View file

@ -20,6 +20,8 @@ import static ghidra.util.HTMLUtilities.*;
import java.io.*; import java.io.*;
import java.util.List; import java.util.List;
import java.util.StringTokenizer; import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.ImageIcon; import javax.swing.ImageIcon;
import javax.swing.KeyStroke; import javax.swing.KeyStroke;
@ -187,60 +189,39 @@ public class ScriptInfo {
// Note that skipping certification header presumes that the header // Note that skipping certification header presumes that the header
// is intact with an appropriate start and end // is intact with an appropriate start and end
String certifyHeaderStart = provider.getCertifyHeaderStart(); String certifyHeaderStart = provider.getCertifyHeaderStart();
String certifyHeaderEnd = provider.getCertifyHeaderEnd();
String certifyHeaderBodyPrefix = provider.getCertificationBodyPrefix();
boolean allowCertifyHeader = (certifyHeaderStart != null); boolean allowCertifyHeader = (certifyHeaderStart != null);
boolean skipCertifyHeader = false;
BufferedReader reader = null; try (BufferedReader reader =
try { new BufferedReader(new InputStreamReader(sourceFile.getInputStream()))) {
StringBuffer buffer = new StringBuffer(); StringBuilder buffer = new StringBuilder();
boolean hitAtSign = false; boolean hitAtSign = false;
reader = new BufferedReader(new InputStreamReader(sourceFile.getInputStream()));
while (true) { while (true) {
String line = reader.readLine(); String line = reader.readLine();
if (line == null) { if (line == null) {
break; break;
} }
if (allowCertifyHeader) { if (allowCertifyHeader && skipCertifyHeader(reader, line)) {
// Skip past certification header if found
if (skipCertifyHeader) {
String trimLine = line.trim();
if (trimLine.startsWith(certifyHeaderEnd)) {
allowCertifyHeader = false; allowCertifyHeader = false;
skipCertifyHeader = false;
continue; continue;
} }
if (certifyHeaderBodyPrefix == null ||
trimLine.startsWith(certifyHeaderBodyPrefix)) { if (parseBlockComment(reader, line)) {
continue; // skip certification header body
}
// broken certification header - unexpected line
Msg.error(this,
"Script contains invalid certification header: " + getName());
allowCertifyHeader = false; allowCertifyHeader = false;
skipCertifyHeader = false; continue; // read block comment; move to next line
}
else if (line.startsWith(certifyHeaderStart)) {
skipCertifyHeader = true;
continue;
}
} }
if (line.startsWith(commentPrefix)) { if (line.startsWith(commentPrefix)) {
allowCertifyHeader = false; allowCertifyHeader = false;
line = line.substring(commentPrefix.length()).trim(); line = line.substring(commentPrefix.length()).trim();
if (line.startsWith("@")) { if (line.startsWith("@")) {
hitAtSign = true; hitAtSign = true;
parseMetaDataLine(line); parseMetaDataLine(line);
} }
else if (!hitAtSign) { else if (!hitAtSign) {
buffer.append(line); // only consume line comments that come before metadata
buffer.append(' '); buffer.append(line).append(' ').append('\n');
buffer.append('\n');
} }
} }
else if (line.trim().isEmpty()) { else if (line.trim().isEmpty()) {
@ -257,16 +238,62 @@ public class ScriptInfo {
catch (IOException e) { catch (IOException e) {
Msg.debug(this, "Unexpected exception reading script: " + sourceFile, e); Msg.debug(this, "Unexpected exception reading script: " + sourceFile, e);
} }
finally {
if (reader != null) {
try {
reader.close();
} }
catch (IOException e) {
// don't care; we tried private boolean skipCertifyHeader(BufferedReader reader, String line) throws IOException {
// Note that skipping certification header presumes that the header
// is intact with an appropriate start and end
String certifyHeaderStart = provider.getCertifyHeaderStart();
if (certifyHeaderStart == null) {
return false;
} }
if (!line.startsWith(certifyHeaderStart)) {
return false;
} }
String certifyHeaderEnd = provider.getCertifyHeaderEnd();
String certifyHeaderBodyPrefix = provider.getCertificationBodyPrefix();
certifyHeaderBodyPrefix = certifyHeaderBodyPrefix == null ? "" : certifyHeaderBodyPrefix;
while ((line = reader.readLine()) != null) {
// Skip past certification header if found
String trimLine = line.trim();
if (trimLine.startsWith(certifyHeaderEnd)) {
return true;
} }
if (trimLine.startsWith(certifyHeaderBodyPrefix)) {
continue; // skip certification header body
}
// broken certification header - unexpected line
Msg.error(this,
"Script contains invalid certification header: " + getName());
}
return false;
}
private boolean parseBlockComment(BufferedReader reader, String line) throws IOException {
Pattern blockStart = provider.getBlockCommentStart();
Pattern blockEnd = provider.getBlockCommentEnd();
if (blockStart == null || blockEnd == null) {
return false;
}
Matcher startMatcher = blockStart.matcher(line);
if (startMatcher.find()) {
int lastOffset = startMatcher.end();
while (line != null && !blockEnd.matcher(line).find(lastOffset)) {
line = reader.readLine();
lastOffset = 0;
}
return true;
}
return false;
} }
private void parseMetaDataLine(String line) { private void parseMetaDataLine(String line) {

View file

@ -528,6 +528,18 @@ public abstract class AbstractGhidraScriptMgrPluginTest
return new ResourceFile(tempFile); return new ResourceFile(tempFile);
} }
protected ResourceFile createTempScriptFileWithLines(String... lines) throws IOException {
ResourceFile newScript = createTempScriptFile();
PrintWriter writer = new PrintWriter(newScript.getOutputStream());
for (String line : lines) {
writer.println(line);
}
writer.close();
return newScript;
}
protected String changeEditorContents() { protected String changeEditorContents() {
assertNotNull("Editor not opened and initialized", editorTextArea); assertNotNull("Editor not opened and initialized", editorTextArea);
assertNotNull("Editor not opened and initialized", buffer); assertNotNull("Editor not opened and initialized", buffer);

View file

@ -0,0 +1,214 @@
/* ###
* 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.junit.Assert.*;
import java.io.IOException;
import javax.swing.KeyStroke;
import org.junit.Test;
import generic.jar.ResourceFile;
import ghidra.app.script.GhidraScriptUtil;
import ghidra.app.script.ScriptInfo;
public class JavaScriptInfoTest extends AbstractGhidraScriptMgrPluginTest {
@Test
public void testDetailedJavaScript() {
String descLine1 = "This script exists to check that the info on";
String descLine2 = "a script that has extensive documentation is";
String descLine3 = "properly parsed and represented.";
String author = "Fake Name";
String categoryTop = "Test";
String categoryBottom = "ScriptInfo";
String keybinding = "ctrl shift COMMA";
String menupath = "File.Run.Detailed Script";
String importPackage = "detailStuff";
ResourceFile scriptFile = null;
try {
//@formatter:off
scriptFile = createTempScriptFileWithLines(
"/*",
" * This is a test block comment. It will be ignored.",
" * @category NotTheRealCategory",
" */",
"//" + descLine1,
"//" + descLine2,
"//" + descLine3,
"//@author " + author,
"//@category " + categoryTop + "." + categoryBottom,
"//@keybinding " + keybinding,
"//@menupath " + menupath,
"//@importpackage " + importPackage,
"class DetailedScript {",
" // for a blank class, it sure is well documented!",
"}");
//@formatter:on
}
catch (IOException e) {
fail("couldn't create a test script: " + e.getMessage());
}
ScriptInfo info = GhidraScriptUtil.newScriptInfo(scriptFile);
String expectedDescription = descLine1 + " \n" + descLine2 + " \n" + descLine3 + " \n";
assertEquals(expectedDescription, info.getDescription());
assertEquals(author, info.getAuthor());
assertEquals(KeyStroke.getKeyStroke(keybinding), info.getKeyBinding());
assertEquals(menupath.replace(".", "->"), info.getMenuPathAsString());
assertEquals(importPackage, info.getImportPackage());
String[] actualCategory = info.getCategory();
assertEquals(2, actualCategory.length);
assertEquals(categoryTop, actualCategory[0]);
assertEquals(categoryBottom, actualCategory[1]);
}
@Test
public void testJavaScriptWithBlockComment() {
String description = "Script with a block comment at the top.";
String category = "Test";
ResourceFile scriptFile = null;
try {
//@formatter:off
scriptFile = createTempScriptFileWithLines(
"/*",
" * This is a test block comment. It will be ignored.",
" * @category NotTheRealCategory",
" */",
"//" + description,
"//@category " + category,
"class BlockCommentScript {",
" // just a blank class, nothing to see here",
"}");
//@formatter:on
}
catch (IOException e) {
fail("couldn't create a test script: " + e.getMessage());
}
ScriptInfo info = GhidraScriptUtil.newScriptInfo(scriptFile);
assertEquals(description + " \n", info.getDescription());
String[] actualCategory = info.getCategory();
assertEquals(1, actualCategory.length);
assertEquals(category, actualCategory[0]);
}
@Test
public void testJavaScriptWithBlockCommentAndCertifyHeader() {
String description = "Script with a block comment at the top.";
String category = "Test";
ResourceFile scriptFile = null;
try {
//@formatter:off
scriptFile = createTempScriptFileWithLines(
"/* ###" +
" * IP: GHIDRA" +
" * " +
" * Some license text..." +
" * you may not use this file except in compliance with the License." +
" * " +
" * blah blah blah" +
" */" +
" " +
"/*",
" * This is a test block comment. It will be ignored.",
" * @category NotTheRealCategory",
" */",
"//" + description,
"//@category " + category,
"class BlockCommentScript {",
" // just a blank class, nothing to see here",
"}");
//@formatter:on
}
catch (IOException e) {
fail("couldn't create a test script: " + e.getMessage());
}
ScriptInfo info = GhidraScriptUtil.newScriptInfo(scriptFile);
assertEquals(description + " \n", info.getDescription());
String[] actualCategory = info.getCategory();
assertEquals(1, actualCategory.length);
assertEquals(category, actualCategory[0]);
}
@Test
public void testJavaScriptWithoutBlockComment() {
String description = "Script without a block comment at the top.";
String category = "Test";
ResourceFile scriptFile = null;
try {
//@formatter:off
scriptFile = createTempScriptFileWithLines(
"//" + description,
"//@category " + category,
"class NoBlockCommentScript {",
" // just a blank class, nothing to see here",
"}");
//@formatter:on
}
catch (IOException e) {
fail("couldn't create a test script: " + e.getMessage());
}
ScriptInfo info = GhidraScriptUtil.newScriptInfo(scriptFile);
assertEquals(description + " \n", info.getDescription());
String[] actualCategory = info.getCategory();
assertEquals(1, actualCategory.length);
assertEquals(category, actualCategory[0]);
}
@Test
public void testJavaScriptWithSingleLineBlockComment() {
String description = "Script with a block comment at the top.";
String category = "Test";
ResourceFile scriptFile = null;
try {
//@formatter:off
scriptFile = createTempScriptFileWithLines(
"/* This is a test block comment. It will be ignored. */",
"//" + description,
"//@category " + category,
"class SingleLineBlockCommentScript {",
" // just a blank class, nothing to see here",
"}");
//@formatter:on
}
catch (IOException e) {
fail("couldn't create a test script: " + e.getMessage());
}
ScriptInfo info = GhidraScriptUtil.newScriptInfo(scriptFile);
assertEquals(description + " \n", info.getDescription());
String[] actualCategory = info.getCategory();
assertEquals(1, actualCategory.length);
assertEquals(category, actualCategory[0]);
}
}

View file

@ -16,6 +16,7 @@
package ghidra.python; package ghidra.python;
import java.io.*; import java.io.*;
import java.util.regex.Pattern;
import generic.jar.ResourceFile; import generic.jar.ResourceFile;
import ghidra.app.script.GhidraScript; import ghidra.app.script.GhidraScript;
@ -23,6 +24,8 @@ import ghidra.app.script.GhidraScriptProvider;
public class PythonScriptProvider extends GhidraScriptProvider { public class PythonScriptProvider extends GhidraScriptProvider {
private static final Pattern BLOCK_COMMENT = Pattern.compile("'''");
@Override @Override
public void createNewScript(ResourceFile newScript, String category) throws IOException { public void createNewScript(ResourceFile newScript, String category) throws IOException {
PrintWriter writer = new PrintWriter(new FileWriter(newScript.getFile(false))); PrintWriter writer = new PrintWriter(new FileWriter(newScript.getFile(false)));
@ -33,26 +36,31 @@ public class PythonScriptProvider extends GhidraScriptProvider {
writer.close(); writer.close();
} }
/**
* Returns a Pattern that matches block comment openings.
* In Python this is a triple single quote sequence, "'''".
* @return the Pattern for Python block comment openings
*/
@Override
public Pattern getBlockCommentStart() {
return BLOCK_COMMENT;
}
/**
* Returns a Pattern that matches block comment closings.
* In Python this is a triple single quote sequence, "'''".
* @return the Pattern for Python block comment openings
*/
@Override
public Pattern getBlockCommentEnd() {
return BLOCK_COMMENT;
}
@Override @Override
public String getCommentCharacter() { public String getCommentCharacter() {
return "#"; return "#";
} }
@Override
protected String getCertifyHeaderStart() {
return "## ###";
}
@Override
protected String getCertifyHeaderEnd() {
return "##";
}
@Override
protected String getCertificationBodyPrefix() {
return "#";
}
@Override @Override
public String getDescription() { public String getDescription() {
return "Python"; return "Python";

View file

@ -0,0 +1,237 @@
/* ###
* 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.python;
import static org.junit.Assert.*;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import javax.swing.KeyStroke;
import org.junit.*;
import generic.jar.ResourceFile;
import ghidra.app.plugin.core.osgi.BundleHost;
import ghidra.app.script.GhidraScriptUtil;
import ghidra.app.script.ScriptInfo;
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
public class PythonScriptInfoTest extends AbstractGhidraHeadedIntegrationTest {
@Before
public void setUp() throws Exception {
GhidraScriptUtil.initialize(new BundleHost(), null);
Path userScriptDir = java.nio.file.Paths.get(GhidraScriptUtil.USER_SCRIPTS_DIR);
if (Files.notExists(userScriptDir)) {
Files.createDirectories(userScriptDir);
}
}
@After
public void tearDown() throws Exception {
GhidraScriptUtil.dispose();
}
@Test
public void testDetailedPythonScript() {
String descLine1 = "This script exists to check that the info on";
String descLine2 = "a script that has extensive documentation is";
String descLine3 = "properly parsed and represented.";
String author = "Fake Name";
String categoryTop = "Test";
String categoryBottom = "ScriptInfo";
String keybinding = "ctrl shift COMMA";
String menupath = "File.Run.Detailed Script";
String importPackage = "detailStuff";
ResourceFile scriptFile = null;
try {
//@formatter:off
scriptFile = createTempPyScriptFileWithLines(
"'''",
"This is a test block comment. It will be ignored.",
"@category NotTheRealCategory",
"'''",
"#" + descLine1,
"#" + descLine2,
"#" + descLine3,
"#@author " + author,
"#@category " + categoryTop + "." + categoryBottom,
"#@keybinding " + keybinding,
"#@menupath " + menupath,
"#@importpackage " + importPackage,
"print('for a blank class, it sure is well documented!')");
//@formatter:on
}
catch (IOException e) {
fail("couldn't create a test script: " + e.getMessage());
}
ScriptInfo info = GhidraScriptUtil.newScriptInfo(scriptFile);
String expectedDescription = descLine1 + " \n" + descLine2 + " \n" + descLine3 + " \n";
assertEquals(expectedDescription, info.getDescription());
assertEquals(author, info.getAuthor());
assertEquals(KeyStroke.getKeyStroke(keybinding), info.getKeyBinding());
assertEquals(menupath.replace(".", "->"), info.getMenuPathAsString());
assertEquals(importPackage, info.getImportPackage());
String[] actualCategory = info.getCategory();
assertEquals(2, actualCategory.length);
assertEquals(categoryTop, actualCategory[0]);
assertEquals(categoryBottom, actualCategory[1]);
}
@Test
public void testPythonScriptWithBlockComment() {
String description = "Script with a block comment at the top.";
String category = "Test";
ResourceFile scriptFile = null;
try {
//@formatter:off
scriptFile = createTempPyScriptFileWithLines(
"'''",
"This is a test block comment. It will be ignored.",
"@category NotTheRealCategory",
"'''",
"#" + description,
"#@category " + category,
"print 'hello!'");
//@formatter:on
}
catch (IOException e) {
fail("couldn't create a test script: " + e.getMessage());
}
ScriptInfo info = GhidraScriptUtil.newScriptInfo(scriptFile);
assertEquals(description + " \n", info.getDescription());
String[] actualCategory = info.getCategory();
assertEquals(1, actualCategory.length);
assertEquals(category, actualCategory[0]);
}
@Test
public void testPythonScriptWithBlockCommentAndCertifyHeader() {
String description = "Script with a block comment at the top.";
String category = "Test";
ResourceFile scriptFile = null;
try {
//@formatter:off
scriptFile = createTempPyScriptFileWithLines(
"## ###" +
"# IP: GHIDRA" +
"# " +
"# Some license text..." +
"# you may not use this file except in compliance with the License." +
"# " +
"# blah blah blah" +
"##" +
"" +
"'''",
"This is a test block comment. It will be ignored.",
"@category NotTheRealCategory",
"'''",
"#" + description,
"#@category " + category,
"print 'hello!'");
//@formatter:on
}
catch (IOException e) {
fail("couldn't create a test script: " + e.getMessage());
}
ScriptInfo info = GhidraScriptUtil.newScriptInfo(scriptFile);
assertEquals(description + " \n", info.getDescription());
String[] actualCategory = info.getCategory();
assertEquals(1, actualCategory.length);
assertEquals(category, actualCategory[0]);
}
@Test
public void testPythonScriptWithoutBlockComment() {
String description = "Script without a block comment at the top.";
String category = "Test";
ResourceFile scriptFile = null;
try {
//@formatter:off
scriptFile = createTempPyScriptFileWithLines(
"#" + description,
"#@category " + category,
"print 'hello!'");
//@formatter:on
}
catch (IOException e) {
fail("couldn't create a test script: " + e.getMessage());
}
ScriptInfo info = GhidraScriptUtil.newScriptInfo(scriptFile);
assertEquals(description + " \n", info.getDescription());
String[] actualCategory = info.getCategory();
assertEquals(1, actualCategory.length);
assertEquals(category, actualCategory[0]);
}
@Test
public void testPythonScriptWithSingleLineBlockComment() {
String description = "Script with a block comment at the top.";
String category = "Test";
ResourceFile scriptFile = null;
try {
//@formatter:off
scriptFile = createTempPyScriptFileWithLines(
"'''This is a test block comment. It will be ignored.'''",
"#" + description,
"#@category " + category,
"print 'hello!'");
//@formatter:on
}
catch (IOException e) {
fail("couldn't create a test script: " + e.getMessage());
}
ScriptInfo info = GhidraScriptUtil.newScriptInfo(scriptFile);
assertEquals(description + " \n", info.getDescription());
String[] actualCategory = info.getCategory();
assertEquals(1, actualCategory.length);
assertEquals(category, actualCategory[0]);
}
private ResourceFile createTempPyScriptFileWithLines(String... lines) throws IOException {
File scriptDir = new File(GhidraScriptUtil.USER_SCRIPTS_DIR);
File tempFile = File.createTempFile(testName.getMethodName(), ".py", scriptDir);
tempFile.deleteOnExit();
ResourceFile tempResourceFile = new ResourceFile(tempFile);
PrintWriter writer = new PrintWriter(tempResourceFile.getOutputStream());
for (String line : lines) {
writer.println(line);
}
writer.close();
return tempResourceFile;
}
}