/* ### * 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 java.io.File; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.*; import generic.application.GenericApplicationLayout; import generic.theme.ApplicationThemeManager; import ghidra.framework.Application; import ghidra.framework.ApplicationConfiguration; import help.validator.*; import help.validator.links.InvalidLink; import help.validator.location.HelpModuleCollection; /** * A class to build help for an entire 'G' application. This class will take in a list of * module paths and build the help for each module. To build single modules, call this class * with only one module path. *

* Note: Help links must not be absolute. They can be relative, including . and .. * syntax. Further, they can use the special help system syntax, which is: *

*/ public class GHelpBuilder { private static final String TOC_OUTPUT_FILE_APPENDIX = "_TOC.xml"; private static final String MAP_OUTPUT_FILE_APPENDIX = "_map.xml"; private static final String HELP_SET_OUTPUT_FILE_APPENDIX = "_HelpSet.hs"; private static final String HELP_SEARCH_DIRECTORY_APPENDIX = "_JavaHelpSearch"; private static final String OUTPUT_DIRECTORY_OPTION = "-o"; private static final String MODULE_NAME_OPTION = "-n"; private static final String HELP_PATHS_OPTION = "-hp"; private static final String DEBUG_SWITCH = "-debug"; private static final String IGNORE_INVALID_SWITCH = "-ignoreinvalid"; private String outputDirectoryName; private String moduleName; private Collection dependencyHelpPaths = new LinkedHashSet<>(); private Collection helpInputDirectories = new LinkedHashSet<>(); private static boolean debugEnabled = false; private boolean ignoreInvalid = false; // TODO: Do actual validation here boolean exitOnError = false; boolean failed = false; public static void main(String[] args) throws Exception { GHelpBuilder builder = new GHelpBuilder(); builder.exitOnError = true; ApplicationConfiguration config = new ApplicationConfiguration() { @Override protected void initializeApplication() { ApplicationThemeManager.initialize(); } @Override public boolean isHeadless() { return false; } }; Application.initializeApplication(new GenericApplicationLayout("Help Builder", "0.1"), config); builder.build(args); } void build(String[] args) { parseArguments(args); HelpModuleCollection allHelp = collectAllHelp(); LinkDatabase linkDatabase = new LinkDatabase(allHelp); debug("Validating help directories..."); Results results = validateHelpDirectories(allHelp, linkDatabase); if (results.failed()) { String message = "Found invalid help:\n" + results.getMessage(); if (ignoreInvalid) { printErrorMessage(message); } else { exitWithError(message, null); } } debug("\tfinished validating help directories"); debug("Building JavaHelp output files..."); buildJavaHelpFiles(linkDatabase); debug("\tfinished building output files"); } private HelpModuleCollection collectAllHelp() { List allHelp = new ArrayList<>(helpInputDirectories); for (File file : dependencyHelpPaths) { allHelp.add(file); } return HelpModuleCollection.fromFiles(allHelp); } private Results validateHelpDirectories(HelpModuleCollection help, LinkDatabase linkDatabase) { JavaHelpValidator validator = new JavaHelpValidator(moduleName, help); validator.setDebugEnabled(debugEnabled); Collection invalidLinks = validator.validate(linkDatabase); Collection duplicateAnchors = linkDatabase.getDuplicateAnchors(); // report the results if (invalidLinks.size() == 0 && duplicateAnchors.size() == 0) { // everything is valid! return new Results("Finished validating help files--all valid!", false); } // flush the output stream so our error reporting is not mixed with the previous output flush(); StringBuilder buildy = new StringBuilder(); if (invalidLinks.size() > 0) { //@formatter:off buildy.append('[').append(JavaHelpValidator.class.getSimpleName()).append(']'); buildy.append(" - Found the following ").append(invalidLinks.size()).append(" invalid links:\n"); for (InvalidLink invalidLink : invalidLinks) { buildy.append("Module ").append(moduleName).append(" - ").append(invalidLink); buildy.append('\n').append("\n"); } //@formatter:on } if (duplicateAnchors.size() > 0) { //@formatter:off buildy.append('[').append(JavaHelpValidator.class.getSimpleName()).append(']'); buildy.append(" - Found the following ").append(duplicateAnchors.size()).append(" topic(s) with duplicate anchor definitions:\n"); for (DuplicateAnchorCollection collection : duplicateAnchors) { buildy.append(collection).append('\n').append("\n"); } //@formatter:on } return new Results(buildy.toString(), true); } private void buildJavaHelpFiles(LinkDatabase linkDatabase) { Path outputDirectory = Paths.get(outputDirectoryName); JavaHelpFilesBuilder fileBuilder = new JavaHelpFilesBuilder(outputDirectory, moduleName, linkDatabase); HelpModuleCollection help = HelpModuleCollection.fromFiles(helpInputDirectories); // 1) Generate JavaHelp files for the module (e.g., TOC file, map file) try { fileBuilder.generateHelpFiles(help); } catch (Exception e) { exitWithError("Unexpected error building help module files:\n", e); } // 2) Generate the help set file for the module Path helpSetFile = outputDirectory.resolve(moduleName + HELP_SET_OUTPUT_FILE_APPENDIX); Path helpMapFile = outputDirectory.resolve(moduleName + MAP_OUTPUT_FILE_APPENDIX); Path helpTOCFile = outputDirectory.resolve(moduleName + TOC_OUTPUT_FILE_APPENDIX); Path indexerOutputDirectory = outputDirectory.resolve(moduleName + HELP_SEARCH_DIRECTORY_APPENDIX); JavaHelpSetBuilder helpSetBuilder = new JavaHelpSetBuilder(moduleName, helpMapFile, helpTOCFile, indexerOutputDirectory, helpSetFile); try { helpSetBuilder.writeHelpSetFile(); } catch (IOException e) { exitWithError("\tError building helpset for module: " + moduleName + "\n", e); } } private void exitWithError(String message, Throwable t) { failed = true; // this prevents error messages getting interspursed with output messages flush(); if (!exitOnError) { // the test environment does not want to exit, so just print the error, even though // it may appear in the incorrect order with the builder's output messages System.err.println(message); if (t != null) { t.printStackTrace(System.err); } return; } // Unusual Code Alert!: If we print the error right away, sometimes the System.out // data has not yet been flushed. Using a thread, with a sleep seems to work. PrintErrorRunnable runnable = new PrintErrorRunnable(message, t); Thread thread = new Thread(runnable); thread.setDaemon(false); thread.start(); try { thread.join(2000); } catch (InterruptedException e) { // just exit } System.exit(1); } private static class PrintErrorRunnable implements Runnable { private String message; private Throwable t; PrintErrorRunnable(String message, Throwable t) { this.message = message; this.t = t; } @Override public void run() { try { Thread.sleep(250); } catch (InterruptedException e) { // don't care } System.err.println(message); if (t != null) { t.printStackTrace(System.err); } } } 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(); System.out.println("[" + GHelpBuilder.class.getSimpleName() + "] " + string); } } private void parseArguments(String[] args) { for (int i = 0; i < args.length; i++) { String opt = args[i]; if (opt.equals(OUTPUT_DIRECTORY_OPTION)) { i++; if (i >= args.length) { errorMessage(OUTPUT_DIRECTORY_OPTION + " requires an argument"); printUsage(); System.exit(1); } outputDirectoryName = args[i]; } else if (opt.equals(MODULE_NAME_OPTION)) { i++; if (i >= args.length) { errorMessage(MODULE_NAME_OPTION + " requires an argument"); printUsage(); System.exit(1); } moduleName = args[i]; } else if (opt.equals(HELP_PATHS_OPTION)) { i++; if (i >= args.length) { errorMessage(HELP_PATHS_OPTION + " requires an argument"); printUsage(); System.exit(1); } String hp = args[i]; if (hp.length() > 0) { for (String p : hp.split(File.pathSeparator)) { dependencyHelpPaths.add(new File(p)); } } } else if (opt.equals(DEBUG_SWITCH)) { debugEnabled = true; } else if (opt.equals(IGNORE_INVALID_SWITCH)) { ignoreInvalid = true; } else if (opt.startsWith("-")) { errorMessage("Unknown option " + opt); printUsage(); System.exit(1); } else { // It must just be an input helpInputDirectories.add(new File(opt)); } } HelpBuildUtils.debug = debugEnabled; if (helpInputDirectories.size() == 0) { errorMessage("Must specify at least one input directory"); printUsage(); System.exit(1); } if (outputDirectoryName == null) { errorMessage("Missing output directory: " + OUTPUT_DIRECTORY_OPTION + " [output]"); printUsage(); System.exit(1); } if (moduleName == null) { errorMessage("Missing module name: " + MODULE_NAME_OPTION + " [name]"); printUsage(); System.exit(1); } } private static void printUsage() { StringBuilder buffy = new StringBuilder(); // TODO: Complete this once the options are stable buffy.append("Usage: "); buffy.append(GHelpBuilder.class.getName()).append(" [-options] [inputs...]\n"); buffy.append(" (to build help for a Ghidra module)\n"); buffy.append("where options include:\n"); buffy.append(" ").append(OUTPUT_DIRECTORY_OPTION).append(" \n"); buffy.append( " REQUIRED to specify the output location of the built help\n"); buffy.append(" ").append(DEBUG_SWITCH).append(" to enable debugging output\n"); buffy.append(" ").append(IGNORE_INVALID_SWITCH).append("\n"); buffy.append(" to continue despite broken links and anchors\n"); errorMessage(buffy.toString()); } private static void warningMessage(String... message) { StringBuilder buffy = new StringBuilder(); buffy.append("\n"); buffy.append(" !!!!! WARNING !!!!!\n"); for (String string : message) { buffy.append('\t').append('\t').append(string).append('\n'); } buffy.append("\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(); } //================================================================================================== // Inner Classes //================================================================================================== private static class Results { private final String message; private final boolean failed; Results(String message, boolean failed) { this.message = message; this.failed = failed; } String getMessage() { return message; } @Override public String toString() { return getMessage(); } boolean failed() { return failed; } } }