mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-04 10:19:23 +02:00
427 lines
13 KiB
Java
427 lines
13 KiB
Java
/* ###
|
|
* 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.
|
|
* <p>
|
|
* Note: Help links must not be absolute. They can be relative, including <code>. and ..</code>
|
|
* syntax. Further, they can use the special help system syntax, which is:
|
|
* <ul>
|
|
* <li><code><b>help/topics/</b>topicName/Filename.html</code> for referencing help topic files
|
|
* <li><code><b>help/</b>shared/image.png</code> for referencing image files at paths rooted under
|
|
* the module's root help dir
|
|
* </ul>
|
|
*/
|
|
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<File> dependencyHelpPaths = new LinkedHashSet<>();
|
|
private Collection<File> 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<File> 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<InvalidLink> invalidLinks = validator.validate(linkDatabase);
|
|
Collection<DuplicateAnchorCollection> 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(" <output directory>\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;
|
|
}
|
|
}
|
|
}
|