diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/script/GhidraScriptProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/script/GhidraScriptProvider.java index 82da554bf4..6bef0f076a 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/script/GhidraScriptProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/script/GhidraScriptProvider.java @@ -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 "#". diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/script/JavaScriptProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/script/JavaScriptProvider.java index 8f3539576b..1a433dd2cc 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/script/JavaScriptProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/script/JavaScriptProvider.java @@ -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 "//"; diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/script/ScriptInfo.java b/Ghidra/Features/Base/src/main/java/ghidra/app/script/ScriptInfo.java index f92c30647f..a4686aa309 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/script/ScriptInfo.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/script/ScriptInfo.java @@ -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; diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/script/AbstractGhidraScriptMgrPluginTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/script/AbstractGhidraScriptMgrPluginTest.java index f787212819..6f5dd34f81 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/script/AbstractGhidraScriptMgrPluginTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/script/AbstractGhidraScriptMgrPluginTest.java @@ -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); diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/script/JavaScriptInfoTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/script/JavaScriptInfoTest.java new file mode 100644 index 0000000000..e62263d254 --- /dev/null +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/script/JavaScriptInfoTest.java @@ -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]); + } +} diff --git a/Ghidra/Features/Python/src/main/java/ghidra/python/PythonScriptProvider.java b/Ghidra/Features/Python/src/main/java/ghidra/python/PythonScriptProvider.java index 7ccace6923..fe49b67279 100644 --- a/Ghidra/Features/Python/src/main/java/ghidra/python/PythonScriptProvider.java +++ b/Ghidra/Features/Python/src/main/java/ghidra/python/PythonScriptProvider.java @@ -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"; diff --git a/Ghidra/Features/Python/src/test.slow/java/ghidra/python/PythonScriptInfoTest.java b/Ghidra/Features/Python/src/test.slow/java/ghidra/python/PythonScriptInfoTest.java new file mode 100644 index 0000000000..b3650454ea --- /dev/null +++ b/Ghidra/Features/Python/src/test.slow/java/ghidra/python/PythonScriptInfoTest.java @@ -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; + } +}