mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-04 18:29:37 +02:00
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:
parent
da94eb86bd
commit
829a837a44
7 changed files with 464 additions and 15 deletions
|
@ -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 "#".
|
||||
|
|
|
@ -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 "//";
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue