diff --git a/Ghidra/Features/Base/build.gradle b/Ghidra/Features/Base/build.gradle index 3361951924..eb9a2c3486 100644 --- a/Ghidra/Features/Base/build.gradle +++ b/Ghidra/Features/Base/build.gradle @@ -123,7 +123,7 @@ task buildJavacc { description = " Compiles the JavaCC files\n" } -// Note: this must happen before the standard buildHelp for Base +// Note: this must happen before the standard buildModuleHelp for Base tasks.register('generateExtraHelpFiles') { group = 'private' @@ -193,7 +193,7 @@ def createTipsHelpFile(input, output) { compileJava.dependsOn buildJavacc rootProject.prepDev.dependsOn buildJavacc -// 'indexHelp' is defined in the buildHelp.gradle 'script plugin' +// 'indexHelp' is defined in the helpProject.gradle 'script plugin' indexHelp.dependsOn generateExtraHelpFiles zipSourceSubproject.dependsOn buildCPPParser diff --git a/Ghidra/Features/Jython/src/main/help/help/TOC_Source.xml b/Ghidra/Features/Jython/src/main/help/help/TOC_Source.xml index cf46dcb1b7..d161e9b355 100644 --- a/Ghidra/Features/Jython/src/main/help/help/TOC_Source.xml +++ b/Ghidra/Features/Jython/src/main/help/help/TOC_Source.xml @@ -3,7 +3,10 @@ - + + + diff --git a/Ghidra/Features/PDB/src/main/help/help/TOC_Source.xml b/Ghidra/Features/PDB/src/main/help/help/TOC_Source.xml index cd29ebf273..7167596387 100644 --- a/Ghidra/Features/PDB/src/main/help/help/TOC_Source.xml +++ b/Ghidra/Features/PDB/src/main/help/help/TOC_Source.xml @@ -50,7 +50,10 @@ - + + + diff --git a/Ghidra/Features/PyGhidra/src/main/help/help/TOC_Source.xml b/Ghidra/Features/PyGhidra/src/main/help/help/TOC_Source.xml index 28de8c3882..27be9a551a 100644 --- a/Ghidra/Features/PyGhidra/src/main/help/help/TOC_Source.xml +++ b/Ghidra/Features/PyGhidra/src/main/help/help/TOC_Source.xml @@ -3,7 +3,10 @@ - + + + diff --git a/Ghidra/Features/Sarif/src/main/help/help/TOC_Source.xml b/Ghidra/Features/Sarif/src/main/help/help/TOC_Source.xml index d88893ccd2..ae1ea05db5 100644 --- a/Ghidra/Features/Sarif/src/main/help/help/TOC_Source.xml +++ b/Ghidra/Features/Sarif/src/main/help/help/TOC_Source.xml @@ -50,7 +50,10 @@ - + + + diff --git a/Ghidra/Framework/Help/build.gradle b/Ghidra/Framework/Help/build.gradle index ba6960c375..de3410c49c 100644 --- a/Ghidra/Framework/Help/build.gradle +++ b/Ghidra/Framework/Help/build.gradle @@ -4,9 +4,9 @@ * 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. @@ -31,3 +31,85 @@ dependencies { //api name:'jh2.with.debug' api 'javax.help:javahelp:2.0.05' } + +/* + * Get all help tasks from all projects + */ +def getAllHelpTasks() { + + List list = new ArrayList() + rootProject.allprojects { + tasks.each { + if (it.name == 'buildModuleHelp') { + list.add(it) + } + } + } + return list +} + +/* + * A task at the Ghidra tool top-level build that will trigger all 'buildModuleHelp' tasks to run, + * this building help. Additionally, this task will perform any needed validation after all help + * modules are built. + */ +tasks.register('buildHelp') { + + description 'Build all Ghidra help' + + dependsOn { getAllHelpTasks() } // all 'buildHelpModule' tasks + dependsOn 'validateHelpTocFiles' // the validate TOC task to run after all help is built +} + + + +/* + * Gathers all generated help TOC files and validates them. + */ +tasks.register('validateHelpTocFiles', JavaExec) { + + group = "private" + + dependsOn { getAllHelpTasks() } // all 'buildHelpModule' tasks to provide TOC inputs + + // we need our Help java files compiled + dependsOn jar + + mainClass = 'help.GHelpTocValidator' + + // to allow remote debugging of the help build jvm + // jvmArgs '-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=13001' + + // print debug info + // args '-debug' + + doFirst { + + // get the Help module jar file, since it has classes we use + classpath += jar.outputs.files + + // get the classpath items used by the Help module, since we use those classes + classpath += configurations.runtimeClasspath + + // add each Module-help.jar file created by 'buildModuleHelp' to the application inputs + rootProject.allprojects.forEach { + + // /build/libs/Module-help.jar + def helpLibDir = it.file('build/libs') + if (helpLibDir.isDirectory()) { + + helpLibDir.listFiles().each { + String name = it.getName() + if (name.endsWith('-help.jar')) { + args "${it.absolutePath}" + } + } + } + + } + + // Sigal that any System.out messages from this Java process should be logged at INFO level. + // To see this output, run gradle with the '-i' option to show INFO messages. + logging.captureStandardOutput LogLevel.INFO + } +} diff --git a/Ghidra/Framework/Help/src/main/java/help/GHelpBuilder.java b/Ghidra/Framework/Help/src/main/java/help/GHelpBuilder.java index b15242ba77..521460283f 100644 --- a/Ghidra/Framework/Help/src/main/java/help/GHelpBuilder.java +++ b/Ghidra/Framework/Help/src/main/java/help/GHelpBuilder.java @@ -15,6 +15,8 @@ */ package help; +import static help.GHelpMsg.*; + import java.io.File; import java.io.IOException; import java.nio.file.Path; @@ -96,7 +98,7 @@ public class GHelpBuilder { if (results.failed()) { String message = "Found invalid help:\n" + results.getMessage(); if (ignoreInvalid) { - printErrorMessage(message); + error(message); } else { exitWithError(message, null); @@ -267,15 +269,6 @@ public class GHelpBuilder { } } - private static void flush() { - System.out.flush(); - System.out.println(); - System.out.flush(); - System.err.flush(); - System.err.println(); - System.err.flush(); - } - private static void debug(String string) { if (debugEnabled) { flush(); @@ -290,7 +283,7 @@ public class GHelpBuilder { if (opt.equals(OUTPUT_DIRECTORY_OPTION)) { i++; if (i >= args.length) { - errorMessage(OUTPUT_DIRECTORY_OPTION + " requires an argument"); + error(OUTPUT_DIRECTORY_OPTION + " requires an argument"); printUsage(); System.exit(1); } @@ -299,7 +292,7 @@ public class GHelpBuilder { else if (opt.equals(MODULE_NAME_OPTION)) { i++; if (i >= args.length) { - errorMessage(MODULE_NAME_OPTION + " requires an argument"); + error(MODULE_NAME_OPTION + " requires an argument"); printUsage(); System.exit(1); } @@ -308,7 +301,7 @@ public class GHelpBuilder { else if (opt.equals(HELP_PATHS_OPTION)) { i++; if (i >= args.length) { - errorMessage(HELP_PATHS_OPTION + " requires an argument"); + error(HELP_PATHS_OPTION + " requires an argument"); printUsage(); System.exit(1); } @@ -322,7 +315,7 @@ public class GHelpBuilder { else if (opt.equals(HELP_PATHS_GENERATED_OPTION)) { i++; if (i >= args.length) { - errorMessage(HELP_PATHS_GENERATED_OPTION + " requires an argument"); + error(HELP_PATHS_GENERATED_OPTION + " requires an argument"); printUsage(); System.exit(1); } @@ -340,7 +333,7 @@ public class GHelpBuilder { ignoreInvalid = true; } else if (opt.startsWith("-")) { - errorMessage("Unknown option " + opt); + error("Unknown option " + opt); printUsage(); System.exit(1); } @@ -353,17 +346,17 @@ public class GHelpBuilder { HelpBuildUtils.debug = debugEnabled; if (helpInputDirectories.size() == 0) { - errorMessage("Must specify at least one input directory"); + error("Must specify at least one input directory"); printUsage(); System.exit(1); } if (outputDirectoryName == null) { - errorMessage("Missing output directory: " + OUTPUT_DIRECTORY_OPTION + " [output]"); + error("Missing output directory: " + OUTPUT_DIRECTORY_OPTION + " [output]"); printUsage(); System.exit(1); } if (moduleName == null) { - errorMessage("Missing module name: " + MODULE_NAME_OPTION + " [name]"); + error("Missing module name: " + MODULE_NAME_OPTION + " [name]"); printUsage(); System.exit(1); } @@ -385,35 +378,7 @@ public class GHelpBuilder { buffy.append(" ").append(IGNORE_INVALID_SWITCH).append("\n"); buffy.append(" to continue despite broken links and anchors\n"); - errorMessage(buffy.toString()); - } - - private static void printErrorMessage(String message) { - // this prevents error messages getting interspersed with output messages - flush(); - errorMessage(message); - } - - private static void errorMessage(String message) { - errorMessage(message, null); - } - - private static void errorMessage(String message, Throwable t) { - try { - // give the output thread a chance to finish it's output (this is a workaround for - // the Eclipse editor, and its use of two threads in its console). - Thread.sleep(250); - } - catch (InterruptedException e) { - // don't care; we tried - } - - System.err.println("[" + GHelpBuilder.class.getSimpleName() + "] " + message); - if (t != null) { - t.printStackTrace(); - } - - flush(); + error(buffy.toString()); } //================================================================================================== diff --git a/Ghidra/Framework/Help/src/main/java/help/GHelpMsg.java b/Ghidra/Framework/Help/src/main/java/help/GHelpMsg.java new file mode 100644 index 0000000000..f254488e60 --- /dev/null +++ b/Ghidra/Framework/Help/src/main/java/help/GHelpMsg.java @@ -0,0 +1,62 @@ +/* ### + * 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 help; + +import utilities.util.reflection.ReflectionUtilities; + +/** + * Contains helpful methods for emitting messages to the console. + */ +public class GHelpMsg { + + public static void error(String message) { + error(message, null); + } + + public static void error(String message, Throwable t) { + + flush(); + + try { + // give the output thread a chance to finish it's output (this is a workaround for + // the Eclipse editor, and its use of two threads in its console). + Thread.sleep(250); + } + catch (InterruptedException e) { + // don't care; we tried + } + + String caller = ReflectionUtilities.getClassNameOlderThan(GHelpMsg.class); + int index = caller.lastIndexOf('.'); + caller = caller.substring(index + 1); + + System.err.println("[" + caller + "] " + message); + if (t != null) { + t.printStackTrace(); + } + + flush(); + } + + public static void flush() { + System.out.flush(); + System.out.println(); + System.out.flush(); + System.err.flush(); + System.err.println(); + System.err.flush(); + } +} diff --git a/Ghidra/Framework/Help/src/main/java/help/GHelpTocValidator.java b/Ghidra/Framework/Help/src/main/java/help/GHelpTocValidator.java new file mode 100644 index 0000000000..d9877d359c --- /dev/null +++ b/Ghidra/Framework/Help/src/main/java/help/GHelpTocValidator.java @@ -0,0 +1,85 @@ +/* ### + * 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 help; + +import static help.GHelpMsg.*; + +import java.io.File; +import java.util.*; + +import generic.application.GenericApplicationLayout; +import ghidra.framework.Application; +import ghidra.framework.ApplicationConfiguration; +import help.validator.LinkDatabase; +import help.validator.location.HelpModuleCollection; + +/** + * Checks for errors in source TOC files, such as conflicting sort groups. This validator is meant + * to be used when all system TOC files have been built. Individual module TOC files are validated + * for correctness with their dependencies when they are built. This class is needed to validate + * all TOC files, including for leaf modules that don't have each other as dependencies. + */ +public class GHelpTocValidator { + + private static final String DEBUG_SWITCH = "-debug"; + + private Collection helpInputDirectories = new LinkedHashSet<>(); + + public static void main(String[] args) throws Exception { + GHelpTocValidator validator = new GHelpTocValidator(); + + ApplicationConfiguration config = new ApplicationConfiguration(); + Application.initializeApplication(new GenericApplicationLayout("Help TOC Validator", "0.1"), + config); + + validator.validate(args); + } + + private void validate(String[] args) { + + parseArguments(args); + + List allHelp = new ArrayList<>(helpInputDirectories); + HelpModuleCollection help = HelpModuleCollection.fromFiles(allHelp); + LinkDatabase linkDatabase = new LinkDatabase(help); + linkDatabase.validateAllTOCs(); + } + + private void parseArguments(String[] args) { + + boolean debugEnabled = false; + for (String opt : args) { + if (opt.equals(DEBUG_SWITCH)) { + debugEnabled = true; + } + else if (opt.startsWith("-")) { + error("Unknown option " + opt); + System.exit(1); + } + else { + // It must just be an input + helpInputDirectories.add(new File(opt)); + } + } + + HelpBuildUtils.debug = debugEnabled; + + if (helpInputDirectories.size() == 0) { + error("Must specify at least one help jar file"); + System.exit(1); + } + } +} diff --git a/Ghidra/Framework/Help/src/main/java/help/HelpBuildUtils.java b/Ghidra/Framework/Help/src/main/java/help/HelpBuildUtils.java index b58b1f29f3..51792843b1 100644 --- a/Ghidra/Framework/Help/src/main/java/help/HelpBuildUtils.java +++ b/Ghidra/Framework/Help/src/main/java/help/HelpBuildUtils.java @@ -428,10 +428,10 @@ public class HelpBuildUtils { return false; } - private static final Path DEFAULT_FS_ROOT; + private static final Path DEFAULT_ROOT_DIR; static { try { - DEFAULT_FS_ROOT = Paths.get(".").toRealPath().getRoot(); + DEFAULT_ROOT_DIR = Paths.get(".").toRealPath().getRoot(); } catch (IOException e) { throw new RuntimeException( @@ -439,7 +439,7 @@ public class HelpBuildUtils { } } - private static Path toFSGivenRoot(Path root, Path path) { + private static Path relativeToRoot(Path root, Path path) { if (path.getNameCount() == 0) { if (path.isAbsolute()) { return root; @@ -459,12 +459,12 @@ public class HelpBuildUtils { return temp; } - public static Path toDefaultFS(Path path) { - return toFSGivenRoot(DEFAULT_FS_ROOT, path); + public static Path relativeToWorkingDir(Path path) { + return relativeToRoot(DEFAULT_ROOT_DIR, path); } - public static Path toFS(Path targetFS, Path path) { - return toFSGivenRoot(targetFS.toAbsolutePath().getRoot(), path); + public static Path relativeTo(Path targetFS, Path path) { + return relativeToRoot(targetFS.toAbsolutePath().getRoot(), path); } public static Path createReferencePath(URI fileURI) { diff --git a/Ghidra/Framework/Help/src/main/java/help/JavaHelpFilesBuilder.java b/Ghidra/Framework/Help/src/main/java/help/JavaHelpFilesBuilder.java index 425f1115cf..89ebcf0b77 100644 --- a/Ghidra/Framework/Help/src/main/java/help/JavaHelpFilesBuilder.java +++ b/Ghidra/Framework/Help/src/main/java/help/JavaHelpFilesBuilder.java @@ -17,7 +17,8 @@ package help; import java.io.*; import java.nio.file.*; -import java.util.*; +import java.util.Collection; +import java.util.Date; import ghidra.util.exception.AssertException; import help.validator.LinkDatabase; @@ -135,9 +136,7 @@ public class JavaHelpFilesBuilder { out.println(""); Collection anchors = help.getAllAnchorDefinitions(); - Iterator iterator = anchors.iterator(); - while (iterator.hasNext()) { - AnchorDefinition a = iterator.next(); + for (AnchorDefinition a : anchors) { String anchorTarget = a.getHelpPath(); // diff --git a/Ghidra/Framework/Help/src/main/java/help/OverlayHelpTree.java b/Ghidra/Framework/Help/src/main/java/help/OverlayHelpTree.java index 8bb51c1cea..b18a2f0707 100644 --- a/Ghidra/Framework/Help/src/main/java/help/OverlayHelpTree.java +++ b/Ghidra/Framework/Help/src/main/java/help/OverlayHelpTree.java @@ -52,6 +52,7 @@ public class OverlayHelpTree { } private void addExternalTOCItem(TOCItem item) { + TOCItem parent = item.getParent(); String parentID = parent == null ? null : parent.getIDAttribute(); if (parentID == null) { @@ -129,6 +130,20 @@ public class OverlayHelpTree { // printTreeForID(writer, sourceFileID); } + public void validateAllTOCs() { + + initializeTree(); + doValidateAllTOCs(rootNode); + } + + private void doValidateAllTOCs(OverlayNode node) { + node.validateChildrenSortGroups(); + Set children = node.children; + for (OverlayNode child : children) { + doValidateAllTOCs(child); + } + } + void printTreeForID(PrintWriter writer, String sourceFileID) { initializeTree(); @@ -149,7 +164,7 @@ public class OverlayHelpTree { private void printContents(String sourceFileID, PrintWriter writer) { if (rootNode == null) { - // assume not TOC contents; empty TOC file + // assume no TOC contents; empty TOC file return; } @@ -185,6 +200,7 @@ public class OverlayHelpTree { } private void buildChildren(OverlayNode node) { + String definitionID = node.getDefinitionID(); Set children = parentToChildrenMap.remove(definitionID); if (children == null) { diff --git a/Ghidra/Framework/Help/src/main/java/help/validator/DuplicateAnchorCollectionByHelpTopic.java b/Ghidra/Framework/Help/src/main/java/help/validator/DuplicateAnchorCollectionByHelpTopic.java index 0db4b938ac..c5f327e478 100644 --- a/Ghidra/Framework/Help/src/main/java/help/validator/DuplicateAnchorCollectionByHelpTopic.java +++ b/Ghidra/Framework/Help/src/main/java/help/validator/DuplicateAnchorCollectionByHelpTopic.java @@ -1,13 +1,12 @@ /* ### * 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. @@ -16,12 +15,12 @@ */ package help.validator; -import help.validator.model.AnchorDefinition; -import help.validator.model.HelpTopic; - import java.nio.file.Path; import java.util.List; +import help.validator.model.AnchorDefinition; +import help.validator.model.HelpTopic; + public class DuplicateAnchorCollectionByHelpTopic implements DuplicateAnchorCollection, Comparable { @@ -35,7 +34,7 @@ public class DuplicateAnchorCollectionByHelpTopic implements DuplicateAnchorColl @Override public String toString() { - return "Duplicate anchors for topic\n\ttopic file: " + topic.getTopicFile() + "\n" + + return "Duplicate anchors for topic\n\ttopic dir: " + topic.getTopicDir() + "\n" + getAnchorsAsString(); } @@ -49,8 +48,8 @@ public class DuplicateAnchorCollectionByHelpTopic implements DuplicateAnchorColl @Override public int compareTo(DuplicateAnchorCollectionByHelpTopic o) { - Path topicFile1 = topic.getTopicFile(); - Path topicFile2 = o.topic.getTopicFile(); - return topicFile1.compareTo(topicFile2); + Path topicDir1 = topic.getTopicDir(); + Path topicDir2 = o.topic.getTopicDir(); + return topicDir1.compareTo(topicDir2); } } diff --git a/Ghidra/Framework/Help/src/main/java/help/validator/JavaHelpValidator.java b/Ghidra/Framework/Help/src/main/java/help/validator/JavaHelpValidator.java index 12013c9638..4e051d0afc 100644 --- a/Ghidra/Framework/Help/src/main/java/help/validator/JavaHelpValidator.java +++ b/Ghidra/Framework/Help/src/main/java/help/validator/JavaHelpValidator.java @@ -204,7 +204,7 @@ public class JavaHelpValidator { Path dirPath = Paths.get(dir.getAbsolutePath()); Path imagePath = Paths.get(imgSrc); - Path imageFileFS = HelpBuildUtils.toFS(dirPath, imagePath); + Path imageFileFS = HelpBuildUtils.relativeTo(dirPath, imagePath); Path toCheck = dirPath.resolve(imageFileFS); if (Files.exists(toCheck)) { return toCheck; @@ -214,7 +214,7 @@ public class JavaHelpValidator { private Path makePath(Path helpDir, Path imagePath) { - Path imageFileFS = HelpBuildUtils.toFS(helpDir, imagePath); + Path imageFileFS = HelpBuildUtils.relativeTo(helpDir, imagePath); imageFileFS = removeRedundantHelp(helpDir, imageFileFS); Path toCheck = helpDir.resolve(imageFileFS); if (Files.exists(toCheck)) { diff --git a/Ghidra/Framework/Help/src/main/java/help/validator/LinkDatabase.java b/Ghidra/Framework/Help/src/main/java/help/validator/LinkDatabase.java index e40e2f6522..da2b1e61d9 100644 --- a/Ghidra/Framework/Help/src/main/java/help/validator/LinkDatabase.java +++ b/Ghidra/Framework/Help/src/main/java/help/validator/LinkDatabase.java @@ -204,4 +204,8 @@ public class LinkDatabase { public void generateTOCOutputFile(Path outputFile, GhidraTOCFile file) throws IOException { printableTree.printTreeForID(outputFile, file.getFile().toUri().toString()); } + + public void validateAllTOCs() { + printableTree.validateAllTOCs(); + } } diff --git a/Ghidra/Framework/Help/src/main/java/help/validator/location/HelpModuleCollection.java b/Ghidra/Framework/Help/src/main/java/help/validator/location/HelpModuleCollection.java index 97f1369f3c..178c2d4b0c 100644 --- a/Ghidra/Framework/Help/src/main/java/help/validator/location/HelpModuleCollection.java +++ b/Ghidra/Framework/Help/src/main/java/help/validator/location/HelpModuleCollection.java @@ -124,6 +124,10 @@ public class HelpModuleCollection implements TOCItemProvider { } public GhidraTOCFile getSourceTOCFile() { + if (inputHelp == null) { + // this collection of help modules is only external inputs (e.g., jar files) + return null; + } return inputHelp.getSourceTOCFile(); } @@ -258,6 +262,11 @@ public class HelpModuleCollection implements TOCItemProvider { @Override public Map getTocDefinitionsByID() { + if (inputHelp == null) { + // this collection of help modules is only external inputs (e.g., jar files) + return Map.of(); + } + Map map = new HashMap<>(); GhidraTOCFile TOC = inputHelp.getSourceTOCFile(); map.putAll(TOC.getTOCDefinitionByIDMapping()); @@ -267,7 +276,6 @@ public class HelpModuleCollection implements TOCItemProvider { @Override public Map getExternalTocItemsById() { Map map = new HashMap<>(); - if (externalHelpSets.isEmpty()) { return map; } @@ -325,6 +333,11 @@ public class HelpModuleCollection implements TOCItemProvider { * @return the items */ public Collection getInputTOCItems() { + if (inputHelp == null) { + // this collection of help modules is only external inputs (e.g., jar files) + return List.of(); + } + Collection items = new ArrayList<>(); GhidraTOCFile TOC = inputHelp.getSourceTOCFile(); items.addAll(TOC.getAllTOCItems()); @@ -332,6 +345,11 @@ public class HelpModuleCollection implements TOCItemProvider { } public Collection getTOC_HREFs() { + if (inputHelp == null) { + // this collection of help modules is only external inputs (e.g., jar files) + return List.of(); + } + Collection definitions = new ArrayList<>(); GhidraTOCFile TOC = inputHelp.getSourceTOCFile(); definitions.addAll(getTOC_HREFs(TOC)); diff --git a/Ghidra/Framework/Help/src/main/java/help/validator/model/HelpFile.java b/Ghidra/Framework/Help/src/main/java/help/validator/model/HelpFile.java index e3fbc280ea..486451ff13 100644 --- a/Ghidra/Framework/Help/src/main/java/help/validator/model/HelpFile.java +++ b/Ghidra/Framework/Help/src/main/java/help/validator/model/HelpFile.java @@ -4,9 +4,9 @@ * 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. @@ -17,17 +17,11 @@ package help.validator.model; import java.io.IOException; import java.nio.file.Path; -import java.util.Collection; -import java.util.List; -import java.util.Map; +import java.util.*; import ghidra.util.exception.AssertException; -import help.HelpBuildUtils; -import help.PathKey; -import help.validator.AnchorManager; -import help.validator.HTMLFileParser; -import help.validator.ReferenceTagProcessor; -import help.validator.TagProcessor; +import help.*; +import help.validator.*; import help.validator.location.HelpModuleLocation; public class HelpFile { @@ -80,7 +74,7 @@ public class HelpFile { return anchorManager.getDuplicateAnchorsByID(); } - public AnchorDefinition getAnchorDefinition(Path helpPath) { + public AnchorDefinition getAnchorDefinition(Path helpPath) { Map anchorsByHelpPath = anchorManager.getAnchorsByHelpPath(); AnchorDefinition def = anchorsByHelpPath.get(new PathKey(helpPath)); return def; @@ -124,9 +118,9 @@ public class HelpFile { HTMLFileParser.scanHtmlFile(file, tagProcessor); } catch (IOException e) { - System.err.println("Exception parsing file: " + file.toUri() + "\n"); - System.err.println(e.getMessage()); - e.printStackTrace(); + String msg = + "Exception parsing file: %s\n%s".formatted(file.toUri(), e.getMessage()); + GHelpMsg.error(msg, e); } } else { diff --git a/Ghidra/Framework/Help/src/main/java/help/validator/model/HelpTopic.java b/Ghidra/Framework/Help/src/main/java/help/validator/model/HelpTopic.java index 4c474a2c7a..57d1bd5ea3 100644 --- a/Ghidra/Framework/Help/src/main/java/help/validator/model/HelpTopic.java +++ b/Ghidra/Framework/Help/src/main/java/help/validator/model/HelpTopic.java @@ -4,9 +4,9 @@ * 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. @@ -20,16 +20,19 @@ import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; import java.util.*; +import help.GHelpMsg; import help.HelpBuildUtils; import help.validator.location.DirectoryHelpModuleLocation; import help.validator.location.HelpModuleLocation; public class HelpTopic implements Comparable { private final HelpModuleLocation help; - private final Path topicFile; + private final Path topicDir; + + // topics/TopicName private final Path relativePath; - private Map helpFiles = new LinkedHashMap<>(); + private Map helpFiles; public static HelpTopic fromHTMLFile(Path topicFile) { @@ -44,65 +47,80 @@ public class HelpTopic implements Comparable { return helpTopic; } - public HelpTopic(HelpModuleLocation help, Path topicFile) { + public HelpTopic(HelpModuleLocation help, Path topicDir) { this.help = help; - this.topicFile = topicFile; + this.topicDir = topicDir; Path helpDir = help.getHelpLocation(); - Path unknownFSRelativePath = helpDir.relativize(topicFile); // may or may not be jar paths - this.relativePath = HelpBuildUtils.toDefaultFS(unknownFSRelativePath); - - loadHelpFiles(topicFile); + // topic file: /help/topics/TopicName + // relative: topics/TopicName + Path relativeTopicPath = helpDir.relativize(topicDir); // may or may not be jar paths + this.relativePath = HelpBuildUtils.relativeToWorkingDir(relativeTopicPath); } - public Path getTopicFile() { - return topicFile; + public Path getTopicDir() { + return topicDir; } - private void loadHelpFiles(final Path dir) { - final PathMatcher matcher = - dir.getFileSystem().getPathMatcher("glob:**/*.{[Hh][Tt][Mm],[Hh][Tt][Mm][Ll]}"); + private void lazyLoad() { + if (helpFiles != null) { + return; + } - // Ex: - // jar: /help/topics/FooPlugin - final Path dirDefaultFS = HelpBuildUtils.toDefaultFS(dir); + helpFiles = new LinkedHashMap<>(); + loadHelpFiles(topicDir); + } + + private void loadHelpFiles(Path dir) { + FileSystem fs = dir.getFileSystem(); + PathMatcher matcher = + fs.getPathMatcher("glob:**/*.{[Hh][Tt][Mm],[Hh][Tt][Mm][Ll]}"); + + // Ex: /help/topics/FooPlugin + Path dirDefaultFS = HelpBuildUtils.relativeToWorkingDir(dir); try { Files.walkFileTree(dir, new SimpleFileVisitor() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { if (matcher.matches(file)) { - // Ex: - // jar: /help/topics/FooPlugin/Foo.html - Path fileDefaultFS = HelpBuildUtils.toDefaultFS(file); - - // Ex: jar: Foo.html - Path relFilePath = dirDefaultFS.relativize(fileDefaultFS); - - // Ex: jar: topics/FooPlugin/Foo.html - relFilePath = relativePath.resolve(relFilePath); - helpFiles.put(relFilePath, new HelpFile(help, file)); + mapHelpFile(dirDefaultFS, file); } return FileVisitResult.CONTINUE; } }); } catch (IOException e) { - System.err.println("Error loading help files: " + dir.toUri()); - e.printStackTrace(System.err); + GHelpMsg.error("Error loading help files: " + dir.toUri(), e); } } + private void mapHelpFile(Path dirDefaultFS, Path file) { + + // Ex: /help/topics/FooPlugin/Foo.html + Path fileDefaultFS = HelpBuildUtils.relativeToWorkingDir(file); + + // Ex: Foo.html + Path relFilePath = dirDefaultFS.relativize(fileDefaultFS); + + // Ex: topics/FooPlugin/Foo.html + relFilePath = relativePath.resolve(relFilePath); + helpFiles.put(relFilePath, new HelpFile(help, file)); + } + void addHelpFile(Path relPath, HelpFile helpFile) { + lazyLoad(); helpFiles.put(relPath, helpFile); } public Collection getAllHREFs() { // Don't need to validate hrefs already in a .jar - if (topicFile.getFileSystem() != FileSystems.getDefault()) { + if (topicDir.getFileSystem() != FileSystems.getDefault()) { return Collections.emptyList(); } + + lazyLoad(); List list = new ArrayList<>(); for (HelpFile helpFile : helpFiles.values()) { list.addAll(helpFile.getAllHREFs()); @@ -112,9 +130,11 @@ public class HelpTopic implements Comparable { public Collection getAllIMGs() { // Don't need to validate imgs already in a .jar - if (topicFile.getFileSystem() != FileSystems.getDefault()) { + if (topicDir.getFileSystem() != FileSystems.getDefault()) { return Collections.emptyList(); } + + lazyLoad(); List list = new ArrayList<>(); for (HelpFile helpFile : helpFiles.values()) { list.addAll(helpFile.getAllIMGs()); @@ -124,6 +144,7 @@ public class HelpTopic implements Comparable { public Collection getAllAnchorDefinitions() { // The current module may refer to anchors in pre-built modules. + lazyLoad(); List list = new ArrayList<>(); for (HelpFile helpFile : helpFiles.values()) { list.addAll(helpFile.getAllAnchorDefinitions()); @@ -132,9 +153,14 @@ public class HelpTopic implements Comparable { } public Collection getHelpFiles() { + lazyLoad(); return helpFiles.values(); } + /** + * Returns the relative path, which is {@code topics/TopicName} + * @return the path + */ Path getRelativePath() { return relativePath; } @@ -144,16 +170,16 @@ public class HelpTopic implements Comparable { } public String getName() { - return topicFile.getFileName().toString(); + return topicDir.getFileName().toString(); } @Override public int compareTo(HelpTopic o) { - return topicFile.compareTo(o.topicFile); + return topicDir.compareTo(o.topicDir); } @Override public String toString() { - return topicFile.toString(); + return topicDir.toString(); } } diff --git a/Ghidra/Framework/Help/src/main/java/help/validator/model/TOCItemDefinition.java b/Ghidra/Framework/Help/src/main/java/help/validator/model/TOCItemDefinition.java index c6e247c023..7144422a63 100644 --- a/Ghidra/Framework/Help/src/main/java/help/validator/model/TOCItemDefinition.java +++ b/Ghidra/Framework/Help/src/main/java/help/validator/model/TOCItemDefinition.java @@ -4,9 +4,9 @@ * 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. @@ -85,6 +85,7 @@ return buildy.toString();*/ //@formatter:off return "<"+GhidraTOCFile.TOC_ITEM_DEFINITION + " id=\"" + getIDAttribute() + "\" text=\"" + getTextAttribute() + "\" " + + "\n\t\tsortgroup=\"" + getSortPreference() + "\"" + "\n\t\ttarget=\"" + getTargetAttribute() + "\" />" + "\n\t\t[source file=\"" + getSourceFile() + "\" (line:" + getLineNumber() + ")]"; //@formatter:on diff --git a/Ghidra/Framework/Help/src/main/java/help/validator/model/TOCItemExternal.java b/Ghidra/Framework/Help/src/main/java/help/validator/model/TOCItemExternal.java index b9b3a8e142..3dc93c9c70 100644 --- a/Ghidra/Framework/Help/src/main/java/help/validator/model/TOCItemExternal.java +++ b/Ghidra/Framework/Help/src/main/java/help/validator/model/TOCItemExternal.java @@ -1,13 +1,12 @@ /* ### * 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. @@ -16,10 +15,10 @@ */ package help.validator.model; -import help.validator.LinkDatabase; - import java.nio.file.Path; +import help.validator.LinkDatabase; + public class TOCItemExternal extends TOCItem { public TOCItemExternal(TOCItem parentItem, Path tocFile, String ID, String text, String target, @@ -41,7 +40,8 @@ public class TOCItemExternal extends TOCItem { } @Override - public String generateTOCItemTag(LinkDatabase linkDatabase, boolean isInlineTag, int indentLevel) { + public String generateTOCItemTag(LinkDatabase linkDatabase, boolean isInlineTag, + int indentLevel) { return super.generateTOCItemTag(linkDatabase, isInlineTag, indentLevel); } @@ -49,6 +49,7 @@ public class TOCItemExternal extends TOCItem { public String toString() { //@formatter:off return "\n\t" + diff --git a/Ghidra/RuntimeScripts/Common/support/buildExtension.gradle b/Ghidra/RuntimeScripts/Common/support/buildExtension.gradle index 927ffdfb43..a01ac955b5 100644 --- a/Ghidra/RuntimeScripts/Common/support/buildExtension.gradle +++ b/Ghidra/RuntimeScripts/Common/support/buildExtension.gradle @@ -363,7 +363,7 @@ tasks.register('indexHelp', JavaExec) { // - validates help // - the files generated will be placed in a diretory usable during development mode and will // eventually be placed in the .jar file -tasks.register('buildHelp', JavaExec) { +tasks.register('buildModuleHelp', JavaExec) { group = "Ghidra Private" dependsOn 'indexHelp' @@ -406,7 +406,7 @@ tasks.register('buildHelp', JavaExec) { // // The classpath needs to include: - // 1) the jar of each dependent Module that has already been built + // 1) the jar of each depended upon Module that has already been built // 2) 'src/main/resources' // @@ -434,6 +434,12 @@ tasks.register('buildHelp', JavaExec) { } } +// a simple task to alias the old 'buildHelp' task to 'buildModuleHelp' so users that uses of the +// old command will still work for end users +tasks.register('buildHelp', JavaExec) { + group = "Ghidra Private" + dependsOn 'buildModuleHelp' +} // include the help into the module's jar @@ -445,7 +451,7 @@ jar { } // build the help whenever this module's jar file is built -jar.dependsOn 'buildHelp' +jar.dependsOn 'buildModuleHelp' /********************************************************************************* diff --git a/gradle/helpProject.gradle b/gradle/helpProject.gradle index 5fd5576032..fc74917c21 100644 --- a/gradle/helpProject.gradle +++ b/gradle/helpProject.gradle @@ -197,14 +197,14 @@ def getModuleResourcesDirs(Collection fullClasspath) { .findAll(dir -> dir.exists()) } -// Locatates 'buildHelp' tasks in projects that this project depends on. The output of the tasks -// is the module's help jar, which is only used to build help and not in the final release. The -// jar file names follow this format: -help.jar. +// Locates 'buildModuleHelp' tasks in projects that this project depends on. The output of the +// tasks is the module's help jar, which is only used to build help and not in the final release. +// The jar file names follow this format: -help.jar. def getDependentProjectHelpTasks(Collection fullClasspath) { def myModules = getMyModules(fullClasspath) def myProjects = filterProjectsBy(myModules) - return myProjects.collect(p -> p.tasks.findByPath('buildHelp')) + return myProjects.collect(p -> p.tasks.findByPath('buildModuleHelp')) .findAll(t -> t != null) } @@ -313,6 +313,8 @@ tasks.register('buildGlobalMarkdown') { } } + + // Task for building Ghidra help files // - depends on the output from the help indexer // - validates help @@ -376,7 +378,7 @@ tasks.register('buildHelpFiles', JavaExec) { // // The classpath needs to include items used by internal Java code to validate help // resources: - // 1) The jar path of each dependent Module. The jar file will be on the 'main' runtime + // 1) The jar path of each depended upon Module. The jar file will be on the 'main' runtime // classpath, but may not yet exist. Regardless, the Java code will use the path to // locate the module for that path. // 2) Each module's 'src/main/resources' dir (this is needed when the jar files from 1 @@ -395,7 +397,7 @@ tasks.register('buildHelpFiles', JavaExec) { // To build help, the validator needs any other help content that this module may reference. // Add each of these dependencies as an argument to the validator. - // The dependency file is the -help.jar file from the 'buildHelp' tasks upon which + // The dependency file is -help.jar from the 'buildModuleHelp' tasks upon which // we depend. def buildHelpTasks = getDependentProjectHelpTasks(sourceSets.main.runtimeClasspath.files) buildHelpTasks.each { @@ -423,7 +425,7 @@ tasks.register('buildHelpFiles', JavaExec) { * this jar is -help.jar. This is in contrast to each module's jar which itself contains * all help needed in production. The module's jar filename is .jar. */ -tasks.register('buildHelp', Jar) { +tasks.register('buildModuleHelp', Jar) { group = rootProject.GHIDRA_GROUP description = " Builds the help for this module. [gradle/helpProject.gradle]\n" @@ -473,11 +475,11 @@ jar { } // build the help whenever this module's jar file is built -processResources.dependsOn buildHelp -jar.dependsOn buildHelp +processResources.dependsOn buildModuleHelp +jar.dependsOn buildModuleHelp // make sure generated help directories exist during prepdev so that the directories are created and // eclipse doesn't complain about missing src directories. -rootProject.prepDev.dependsOn buildHelp +rootProject.prepDev.dependsOn buildModuleHelp diff --git a/gradle/root/usage.gradle b/gradle/root/usage.gradle index b82158a7dd..b933b6473a 100644 --- a/gradle/root/usage.gradle +++ b/gradle/root/usage.gradle @@ -4,9 +4,9 @@ * 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. @@ -27,8 +27,8 @@ GHIDRA GRADLE be run against a specific module. For example: from root project, "gradle buildHelp" builds help for all modules. - from root project, "gradle :Base:buildHelp" builds help for the "Base" module - from the Base project dir, "gradle buildHelp" builds help for the "Base" module + from root project, "gradle :Base:buildModuleHelp" builds help for the "Base" module + from the Base project dir, "gradle buildModuleHelp" builds help for the "Base" module Primary gradle tasks for Ghidra