fix github 1484 to allow python block comment docstrings

Delegate the definition of block comment start and end patterns to
ScriptProvider classes instead of ScriptInfo.

Move the functionality to handle Java block comments out of the base
`GhidraScriptProvider` class into the `JavaScriptProvider` subclass.
Default behavior is now to not support block comments and rely on
extensions to implement these themselves.

Add a number of tests for Java and Python `ScriptInfo` generation
to ensure that scripts with no block comments, single-line block
comments, and multi-line block comments are all handled appropriately.

Add ScriptInfo tests for detailed Java and Python scripts including
multiline descriptions and additional metadata flags.
This commit is contained in:
Joel Anderson 2021-03-14 17:31:15 -04:00 committed by dragonmacher
parent da94eb86bd
commit 829a837a44
7 changed files with 464 additions and 15 deletions

View file

@ -17,6 +17,7 @@ package ghidra.app.script;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.regex.Pattern;
import generic.jar.ResourceFile;
import ghidra.util.classfinder.ExtensionPoint;
@ -93,6 +94,24 @@ public abstract class GhidraScriptProvider
public abstract void createNewScript(ResourceFile newScript, String category)
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.
* For example, "//" or "#".

View file

@ -17,6 +17,7 @@ package ghidra.app.script;
import java.io.*;
import java.util.Collections;
import java.util.regex.Pattern;
import org.osgi.framework.Bundle;
@ -26,6 +27,9 @@ import ghidra.util.Msg;
import ghidra.util.task.TaskMonitor;
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;
/**
@ -159,6 +163,26 @@ public class JavaScriptProvider extends GhidraScriptProvider {
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
public String getCommentCharacter() {
return "//";

View file

@ -20,6 +20,8 @@ import static ghidra.util.HTMLUtilities.*;
import java.io.*;
import java.util.List;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.ImageIcon;
import javax.swing.KeyStroke;
@ -228,6 +230,21 @@ public class ScriptInfo {
}
}
Pattern blockStart = provider.getBlockCommentStart();
Pattern blockEnd = provider.getBlockCommentEnd();
if (blockStart != null && blockEnd != null) {
Matcher startMatcher = blockStart.matcher(line);
if (startMatcher.find()) {
int last_offset = startMatcher.end();
while (line != null && !blockEnd.matcher(line).find(last_offset)) {
line = reader.readLine();
last_offset = 0;
}
continue;
}
}
if (line.startsWith(commentPrefix)) {
allowCertifyHeader = false;

View file

@ -528,6 +528,18 @@ public abstract class AbstractGhidraScriptMgrPluginTest
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() {
assertNotNull("Editor not opened and initialized", editorTextArea);
assertNotNull("Editor not opened and initialized", buffer);

View file

@ -0,0 +1,170 @@
/* ###
* 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.assertEquals;
import static org.junit.Assert.fail;
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 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;
import java.io.*;
import java.util.regex.Pattern;
import generic.jar.ResourceFile;
import ghidra.app.script.GhidraScript;
@ -23,6 +24,8 @@ import ghidra.app.script.GhidraScriptProvider;
public class PythonScriptProvider extends GhidraScriptProvider {
private static final Pattern BLOCK_COMMENT = Pattern.compile("'''");
@Override
public void createNewScript(ResourceFile newScript, String category) throws IOException {
PrintWriter writer = new PrintWriter(new FileWriter(newScript.getFile(false)));
@ -33,26 +36,31 @@ public class PythonScriptProvider extends GhidraScriptProvider {
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
public String getCommentCharacter() {
return "#";
}
@Override
protected String getCertifyHeaderStart() {
return "## ###";
}
@Override
protected String getCertifyHeaderEnd() {
return "##";
}
@Override
protected String getCertificationBodyPrefix() {
return "#";
}
@Override
public String getDescription() {
return "Python";

View file

@ -0,0 +1,199 @@
/* ###
* 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.assertEquals;
import static org.junit.Assert.fail;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import javax.swing.KeyStroke;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
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 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;
}
}