mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-04 02:09:44 +02:00
Candidate release of source code.
This commit is contained in:
parent
db81e6b3b0
commit
79d8f164f8
12449 changed files with 2800756 additions and 16 deletions
416
Ghidra/Framework/Help/src/main/java/help/GHelpBuilder.java
Normal file
416
Ghidra/Framework/Help/src/main/java/help/GHelpBuilder.java
Normal file
|
@ -0,0 +1,416 @@
|
|||
/* ###
|
||||
* 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 ghidra.GhidraApplicationLayout;
|
||||
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 <tt>. and ..</tt>
|
||||
* syntax. Further, they can use the special help system syntax, which is:
|
||||
* <ul>
|
||||
* <li><tt><b>help/topics/</b>topicName/Filename.html</tt> for referencing help topic files
|
||||
* <li><tt><b>help/</b>shared/image.png</tt> 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<File>();
|
||||
private Collection<File> helpInputDirectories = new LinkedHashSet<File>();
|
||||
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();
|
||||
Application.initializeApplication(new GhidraApplicationLayout(), 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<File>(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;
|
||||
}
|
||||
}
|
||||
}
|
641
Ghidra/Framework/Help/src/main/java/help/HelpBuildUtils.java
Normal file
641
Ghidra/Framework/Help/src/main/java/help/HelpBuildUtils.java
Normal file
|
@ -0,0 +1,641 @@
|
|||
/* ###
|
||||
* 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.*;
|
||||
import java.net.*;
|
||||
import java.nio.file.*;
|
||||
import java.text.MessageFormat;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import help.validator.location.*;
|
||||
import resources.Icons;
|
||||
|
||||
public class HelpBuildUtils {
|
||||
|
||||
private static final String HELP_TOPICS_ROOT_PATH = "help/topics";
|
||||
|
||||
// Great. You've just summoned Cthulu.
|
||||
private static final Pattern HREF_PATTERN =
|
||||
Pattern.compile("\"(\\.\\./[^/.]+/[^/.]+\\.html*(#[^\"]+)*)\"", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private static final Pattern STYLE_SHEET_PATTERN = Pattern.compile(
|
||||
"<link\\s+rel.+stylesheet.+href=\"*(.+(Frontpage.css))\"*.+>", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private static final Pattern STYLE_CLASS_PATTERN =
|
||||
Pattern.compile("class\\s*=\\s*\"(\\w+)\"", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private static final String STYLE_SHEET_FORMAT_STRING =
|
||||
"<link rel=\"stylesheet\" type=\"text/css\" href=\"{0}{1}{2}\">";
|
||||
private static final String SHARED_DIRECTORY = "shared/";
|
||||
|
||||
public static boolean debug = true;
|
||||
|
||||
private HelpBuildUtils() {
|
||||
// utils class; can't create
|
||||
}
|
||||
|
||||
public static HelpModuleLocation toLocation(File file) {
|
||||
if (file.isDirectory()) {
|
||||
return new DirectoryHelpModuleLocation(file);
|
||||
}
|
||||
else if (file.isFile()) {
|
||||
return new JarHelpModuleLocation(file);
|
||||
}
|
||||
throw new IllegalArgumentException(
|
||||
"Don't know how to create a help module location for file: " + file);
|
||||
}
|
||||
|
||||
public static Path getRoot(Collection<Path> roots, Path file) {
|
||||
for (Path dir : roots) {
|
||||
if (file.startsWith(dir)) {
|
||||
return dir;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a file object that is the help topic directory for the given file.
|
||||
* This method is useful for finding the help topic directory when the given
|
||||
* file doesn't live directly under a help topic.
|
||||
*/
|
||||
public static Path getHelpTopicDir(Path file) {
|
||||
Path helpTopics = file.getFileSystem().getPath("help", "topics");
|
||||
int last = file.getNameCount();
|
||||
for (int i = 0; i < last; i++) {
|
||||
Path sp = file.subpath(i, last);
|
||||
if (sp.startsWith(helpTopics)) {
|
||||
return file.subpath(0, i + 3); // 2 for help/topics, 1 for the actual topic
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Path getFile(Path srcFile, String relativePath) {
|
||||
if (relativePath == null || relativePath.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (relativePath.startsWith("/")) {
|
||||
return null; // absolute path
|
||||
}
|
||||
|
||||
if (relativePath.contains(":")) {
|
||||
return null; // real URL
|
||||
}
|
||||
|
||||
if (relativePath.contains("\\")) {
|
||||
return null; // not sure why this is here
|
||||
}
|
||||
|
||||
Path parent = srcFile.getParent();
|
||||
return parent.resolve(relativePath);
|
||||
}
|
||||
|
||||
public static Path relativizeWithHelpTopics(Path p) {
|
||||
if (p == null) {
|
||||
return null;
|
||||
}
|
||||
Path helpTopics = p.getFileSystem().getPath("help", "topics");
|
||||
return relativize(helpTopics, p);
|
||||
}
|
||||
|
||||
public static Path relativize(Path parent, Path child) {
|
||||
if (child == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int last = child.getNameCount();
|
||||
for (int i = 0; i < last; i++) {
|
||||
Path sp = child.subpath(i, last);
|
||||
if (sp.startsWith(parent)) {
|
||||
return sp;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
//==================================================================================================
|
||||
// Cleanup Methods
|
||||
//==================================================================================================
|
||||
|
||||
public static void cleanupHelpFileLinks(Path helpFile) throws IOException {
|
||||
String fixupHelpProperty = System.getProperty("fix.help.links");
|
||||
boolean cleanupHelpFiles = Boolean.parseBoolean(fixupHelpProperty);
|
||||
if (!cleanupHelpFiles) {
|
||||
return;
|
||||
}
|
||||
|
||||
String fileContents = readFile(helpFile);
|
||||
String newContents = null; // this will be set if changes take place
|
||||
|
||||
String linkFixupContents = fixLinksInFile(helpFile, fileContents);
|
||||
if (linkFixupContents != null) {
|
||||
newContents = linkFixupContents;
|
||||
|
||||
// replace the input to future processing so we don't lose changes
|
||||
fileContents = newContents;
|
||||
}
|
||||
|
||||
String styleSheetFixupContents = fixStyleSheetLinkInFile(helpFile, fileContents);
|
||||
if (styleSheetFixupContents != null) {
|
||||
// a fixup has taken place
|
||||
newContents = styleSheetFixupContents;
|
||||
|
||||
// replace the input to future processing so we don't lose changes
|
||||
fileContents = newContents;
|
||||
}
|
||||
|
||||
String styleSheetClassFixupContents = fixStyleSheetClassNames(helpFile, fileContents);
|
||||
if (styleSheetClassFixupContents != null) {
|
||||
newContents = styleSheetClassFixupContents;
|
||||
}
|
||||
|
||||
if (newContents == null) {
|
||||
return; // nothing to write; no changes
|
||||
}
|
||||
|
||||
writeFileContents(helpFile, newContents);
|
||||
}
|
||||
|
||||
private static String fixStyleSheetLinkInFile(Path helpFile, String fileContents) {
|
||||
|
||||
int currentPosition = 0;
|
||||
StringBuffer newContents = new StringBuffer();
|
||||
Matcher matcher = STYLE_SHEET_PATTERN.matcher(fileContents);
|
||||
|
||||
boolean hasMatches = matcher.find();
|
||||
if (!hasMatches) {
|
||||
return null; // no work to do
|
||||
}
|
||||
|
||||
// only care about the first hit, if there are multiple matches
|
||||
// Groups:
|
||||
// 0 - full match
|
||||
// 1 - href text with relative notation "../.."
|
||||
// 2 - href text without relative prefix
|
||||
|
||||
int matchStart = matcher.start();
|
||||
String fullMatch = matcher.group(0);
|
||||
|
||||
String beforeMatchString = fileContents.substring(currentPosition, matchStart);
|
||||
newContents.append(beforeMatchString);
|
||||
currentPosition = matchStart + fullMatch.length();
|
||||
|
||||
String fullHREFText = matcher.group(1);
|
||||
if (fullHREFText.indexOf(SHARED_DIRECTORY) != -1) {
|
||||
return null; // already fixed; nothing to do
|
||||
}
|
||||
|
||||
debug("Found stylesheet reference text: " + fullHREFText + " in file: " +
|
||||
helpFile.getFileName());
|
||||
|
||||
// pull off the relative path structure
|
||||
String filenameOnlyHREFText = matcher.group(2);
|
||||
int filenameStart = fullHREFText.indexOf(filenameOnlyHREFText);
|
||||
String reltativePrefix = fullHREFText.substring(0, filenameStart);
|
||||
|
||||
String updatedStyleSheetTag = MessageFormat.format(STYLE_SHEET_FORMAT_STRING,
|
||||
reltativePrefix, SHARED_DIRECTORY, filenameOnlyHREFText);
|
||||
debug("\tnew link tag: " + updatedStyleSheetTag);
|
||||
newContents.append(updatedStyleSheetTag);
|
||||
|
||||
// grab the remaining content
|
||||
if (currentPosition < fileContents.length()) {
|
||||
newContents.append(fileContents.substring(currentPosition));
|
||||
}
|
||||
|
||||
return newContents.toString();
|
||||
}
|
||||
|
||||
private static String fixStyleSheetClassNames(Path helpFile, String fileContents) {
|
||||
|
||||
int currentPosition = 0;
|
||||
StringBuffer newContents = new StringBuffer();
|
||||
Matcher matcher = STYLE_CLASS_PATTERN.matcher(fileContents);
|
||||
|
||||
boolean hasMatches = matcher.find();
|
||||
if (!hasMatches) {
|
||||
return null; // no work to do
|
||||
}
|
||||
|
||||
// only care about the first hit, if there are multiple matches
|
||||
// Groups:
|
||||
// 0 - full match
|
||||
// 1 - class name between quotes
|
||||
|
||||
while (hasMatches) {
|
||||
|
||||
int matchStart = matcher.start();
|
||||
String fullMatch = matcher.group(0);
|
||||
|
||||
String beforeMatchString = fileContents.substring(currentPosition, matchStart);
|
||||
newContents.append(beforeMatchString);
|
||||
currentPosition = matchStart + fullMatch.length();
|
||||
|
||||
String classNameText = matcher.group(1);
|
||||
if (!containsUpperCase(classNameText)) {
|
||||
// nothing to fixup; put the original contents back
|
||||
newContents.append(fullMatch);
|
||||
}
|
||||
else {
|
||||
debug("Found stylesheet class name text: " + classNameText + " in file: " +
|
||||
helpFile.getFileName());
|
||||
|
||||
// pull off the relative path structure
|
||||
String updatedText = "class=\"" + classNameText.toLowerCase() + "\"";
|
||||
debug("\tnew link tag: " + updatedText);
|
||||
newContents.append(updatedText);
|
||||
}
|
||||
|
||||
hasMatches = matcher.find();
|
||||
}
|
||||
|
||||
// grab the remaining content
|
||||
if (currentPosition < fileContents.length()) {
|
||||
newContents.append(fileContents.substring(currentPosition));
|
||||
}
|
||||
|
||||
return newContents.toString();
|
||||
}
|
||||
|
||||
private static String fixLinksInFile(Path helpFile, String fileContents) {
|
||||
String updatedContents = fixRelativeLink(HREF_PATTERN, helpFile, fileContents);
|
||||
|
||||
// not sure if more types to come
|
||||
return updatedContents;
|
||||
}
|
||||
|
||||
private static String fixRelativeLink(Pattern pattern, Path helpFile, String fileContents) {
|
||||
int currentPosition = 0;
|
||||
StringBuffer newContents = new StringBuffer();
|
||||
Matcher matcher = pattern.matcher(fileContents);
|
||||
|
||||
boolean hasMatches = matcher.find();
|
||||
if (!hasMatches) {
|
||||
return null; // no work to do
|
||||
}
|
||||
|
||||
while (hasMatches) {
|
||||
int matchStart = matcher.start();
|
||||
String fullMatch = matcher.group(0);
|
||||
|
||||
String beforeMatchString = fileContents.substring(currentPosition, matchStart);
|
||||
newContents.append(beforeMatchString);
|
||||
currentPosition = matchStart + fullMatch.length();
|
||||
|
||||
String HREFText = matcher.group(1);
|
||||
debug("Found HREF text: " + HREFText + " in file: " + helpFile.getFileName());
|
||||
String updatedHREFText = resolveLink(HREFText);
|
||||
debug("\tnew link text: " + updatedHREFText);
|
||||
newContents.append('"').append(updatedHREFText).append('"');
|
||||
|
||||
hasMatches = matcher.find();
|
||||
}
|
||||
|
||||
// grab the remaining content
|
||||
if (currentPosition < fileContents.length()) {
|
||||
newContents.append(fileContents.substring(currentPosition));
|
||||
}
|
||||
|
||||
return newContents.toString();
|
||||
}
|
||||
|
||||
private static String resolveLink(String linkTextReference) {
|
||||
String helpTopicsPrefix = HELP_TOPICS_ROOT_PATH;
|
||||
if (linkTextReference.startsWith(helpTopicsPrefix)) {
|
||||
// this is what we prefer
|
||||
return linkTextReference;
|
||||
}
|
||||
|
||||
String[] referenceParts = linkTextReference.split("/");
|
||||
if (referenceParts.length != 3) {
|
||||
return linkTextReference;
|
||||
}
|
||||
|
||||
if (!referenceParts[0].equals("..")) {
|
||||
return linkTextReference;
|
||||
}
|
||||
|
||||
return HELP_TOPICS_ROOT_PATH + "/" + referenceParts[1] + "/" + referenceParts[2];
|
||||
}
|
||||
|
||||
private static String readFile(Path helpFile) throws IOException {
|
||||
InputStreamReader isr = new InputStreamReader(Files.newInputStream(helpFile));
|
||||
BufferedReader reader = new BufferedReader(isr);
|
||||
try {
|
||||
StringBuffer buffy = new StringBuffer();
|
||||
String line = null;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
buffy.append(line).append('\n');
|
||||
}
|
||||
|
||||
return buffy.toString();
|
||||
}
|
||||
finally {
|
||||
reader.close();
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeFileContents(Path helpFile, String updatedContents)
|
||||
throws IOException {
|
||||
OutputStreamWriter osw = new OutputStreamWriter(Files.newOutputStream(helpFile));
|
||||
BufferedWriter writer = new BufferedWriter(osw);
|
||||
try {
|
||||
writer.write(updatedContents);
|
||||
}
|
||||
finally {
|
||||
writer.close();
|
||||
}
|
||||
}
|
||||
|
||||
public static void debug(String text) {
|
||||
if (debug) {
|
||||
System.err.println("[" + HelpBuildUtils.class.getSimpleName() + "] " + text);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean containsUpperCase(String string) {
|
||||
for (int i = 0; i < string.length(); i++) {
|
||||
char charAt = string.charAt(i);
|
||||
if (Character.isUpperCase(charAt)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static final Path DEFAULT_FS_ROOT;
|
||||
static {
|
||||
try {
|
||||
DEFAULT_FS_ROOT = Paths.get(".").toRealPath().getRoot();
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new RuntimeException(
|
||||
"Unexpected error finding root directory of local filesystem", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Path toFSGivenRoot(Path root, Path path) {
|
||||
if (path.getNameCount() == 0) {
|
||||
if (path.isAbsolute()) {
|
||||
return root;
|
||||
}
|
||||
return root.relativize(root);
|
||||
}
|
||||
|
||||
String first = path.getName(0).toString();
|
||||
String[] names = new String[path.getNameCount() - 1];
|
||||
for (int i = 0; i < names.length; i++) {
|
||||
names[i] = path.getName(i + 1).toString();
|
||||
}
|
||||
Path temp = root.getFileSystem().getPath(first, names);
|
||||
if (path.isAbsolute()) {
|
||||
return root.resolve(temp);
|
||||
}
|
||||
return temp;
|
||||
}
|
||||
|
||||
public static Path toDefaultFS(Path path) {
|
||||
return toFSGivenRoot(DEFAULT_FS_ROOT, path);
|
||||
}
|
||||
|
||||
public static Path toFS(Path targetFS, Path path) {
|
||||
return toFSGivenRoot(targetFS.toAbsolutePath().getRoot(), path);
|
||||
}
|
||||
|
||||
public static Path createReferencePath(URI fileURI) {
|
||||
Path res;
|
||||
if (fileURI.getScheme() != null) {
|
||||
res = Paths.get(fileURI);
|
||||
}
|
||||
else {
|
||||
// res = new File(fileURI.getPath()).toPath();
|
||||
res = Paths.get(fileURI.getPath());
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given String represents a remote resource
|
||||
*
|
||||
* @param uriString the URI to test
|
||||
* @return true if the given String represents a remote resource
|
||||
*/
|
||||
public static boolean isRemote(String uriString) {
|
||||
try {
|
||||
URI uri = new URI(uriString);
|
||||
return isRemote(uri);
|
||||
}
|
||||
catch (URISyntaxException e) {
|
||||
debug("Invalid URI: " + uriString);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given Path represents a remote resource
|
||||
*
|
||||
* @param path the path
|
||||
* @return true if the given Path represents a remote resource
|
||||
*/
|
||||
public static boolean isRemote(Path path) {
|
||||
if (path == null) {
|
||||
return false;
|
||||
}
|
||||
URI uri = path.toUri();
|
||||
return isRemote(uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given URI represents a remote resource
|
||||
*
|
||||
* @param uri the URI
|
||||
* @return true if the given URI represents a remote resource
|
||||
*/
|
||||
public static boolean isRemote(URI uri) {
|
||||
|
||||
if (isFilesystemPath(uri)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String scheme = uri.getScheme();
|
||||
if (scheme == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (scheme) {
|
||||
case "file":
|
||||
return false;
|
||||
case "jar":
|
||||
return false;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean isFilesystemPath(URI uri) {
|
||||
String scheme = uri.getScheme();
|
||||
if (scheme == null) {
|
||||
return true;
|
||||
}
|
||||
return scheme.equals("file");
|
||||
}
|
||||
|
||||
private static URI resolve(Path sourceFile, String ref) throws URISyntaxException {
|
||||
URI resolved;
|
||||
if (ref.startsWith("help/topics")) {
|
||||
resolved = new URI(ref); // help system syntax
|
||||
}
|
||||
else if (ref.startsWith("help/")) {
|
||||
resolved = new URI(ref); // help system syntax
|
||||
}
|
||||
else {
|
||||
resolved = sourceFile.toUri().resolve(ref); // real link
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private static Path toPath(URI uri) {
|
||||
try {
|
||||
return Paths.get(uri);
|
||||
}
|
||||
catch (FileSystemNotFoundException e) {
|
||||
try {
|
||||
FileSystems.newFileSystem(uri, Collections.emptyMap());
|
||||
}
|
||||
catch (IOException e1) {
|
||||
debug("Exception loading filesystem for uri: " + uri + "\n\t" + e1.getMessage());
|
||||
}
|
||||
}
|
||||
return Paths.get(uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn an HTML IMG reference into a location object that has resolved path info. This will
|
||||
* locate files based upon relative references, specialized help system references (i.e.,
|
||||
* help/topics/...), and absolute URLs.
|
||||
*
|
||||
* @param ref the reference text
|
||||
* @return an absolute path; null if the URI is remote
|
||||
* @throws URISyntaxException
|
||||
*/
|
||||
public static ImageLocation locateImageReference(Path sourceFile, String ref)
|
||||
throws URISyntaxException {
|
||||
|
||||
if (Icons.isIconsReference(ref)) {
|
||||
// help system syntax: <img src="Icons.ERROR_ICON" />
|
||||
URL url = Icons.getUrlForIconsReference(ref);
|
||||
if (url == null) {
|
||||
// bad icon name
|
||||
return ImageLocation.createInvalidRuntimeLocation(sourceFile, ref);
|
||||
}
|
||||
|
||||
URI resolved = url.toURI();
|
||||
Path path = toPath(resolved);
|
||||
return ImageLocation.createRuntimeLocation(sourceFile, ref, resolved, path);
|
||||
}
|
||||
|
||||
URI resolved = resolve(sourceFile, ref);
|
||||
if (isRemote(resolved)) {
|
||||
return ImageLocation.createRemoteLocation(sourceFile, ref, resolved);
|
||||
}
|
||||
|
||||
Path path = createPathFromURI(sourceFile, resolved);
|
||||
return ImageLocation.createLocalLocation(sourceFile, ref, resolved, path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn an HTML HREF reference into an absolute path. This will
|
||||
* locate files based upon relative references, specialized help system references (i.e.,
|
||||
* help/topics/...), and absolute URLs.
|
||||
*
|
||||
* @param ref the reference text
|
||||
* @return an absolute path; null if the URI is remote
|
||||
* @throws URISyntaxException
|
||||
*/
|
||||
public static Path locateReference(Path sourceFile, String ref) throws URISyntaxException {
|
||||
|
||||
URI resolved = resolve(sourceFile, ref);
|
||||
if (isRemote(resolved)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// non-remote/local path
|
||||
Path refPath = createPathFromURI(sourceFile, resolved);
|
||||
return refPath;
|
||||
}
|
||||
|
||||
private static Path createPathFromURI(Path sourceFile, URI resolved) throws URISyntaxException {
|
||||
String scheme = resolved.getScheme();
|
||||
if (scheme == null) {
|
||||
// res = new File(fileURI.getPath()).toPath();
|
||||
return Paths.get(resolved.getRawPath());
|
||||
}
|
||||
|
||||
if (scheme.startsWith("file")) {
|
||||
// bug?...we are sometimes handed a URI of the form 'file:/some/path', where the
|
||||
// single '/' is not a valid file URI
|
||||
URI uri = new URI("file://" + resolved.getRawPath());
|
||||
return Paths.get(uri);
|
||||
}
|
||||
|
||||
return Paths.get(resolved); // for now, allow non-local paths through to be handled later
|
||||
// throw new AssertException("Don't know how to handle path URI: " + sourceFile);
|
||||
}
|
||||
|
||||
//==================================================================================================
|
||||
// Inner Classes
|
||||
//==================================================================================================
|
||||
|
||||
public static interface Stringizer<T> {
|
||||
public String stringize(T obj);
|
||||
}
|
||||
|
||||
public static class HelpFilesFilter implements FileFilter {
|
||||
|
||||
private final String[] fileExtensions;
|
||||
|
||||
public HelpFilesFilter(String... extensions) {
|
||||
this.fileExtensions = extensions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean accept(File file) {
|
||||
String name = file.getName();
|
||||
if (file.isDirectory()) {
|
||||
if (".svn".equals(name) || "bin".equals(name) || "api".equals(name)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
for (String extension : fileExtensions) {
|
||||
if (name.endsWith(extension)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
144
Ghidra/Framework/Help/src/main/java/help/ImageLocation.java
Normal file
144
Ghidra/Framework/Help/src/main/java/help/ImageLocation.java
Normal file
|
@ -0,0 +1,144 @@
|
|||
/* ###
|
||||
* 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.net.URI;
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* A class that represents the original location of an IMG tag along with its location
|
||||
* resolution within the help system.
|
||||
*/
|
||||
public class ImageLocation {
|
||||
|
||||
private Path sourceFile;
|
||||
private String imageSrc;
|
||||
|
||||
private Path resolvedPath;
|
||||
private URI resolvedUri;
|
||||
private boolean isRemote;
|
||||
private boolean isRuntime;
|
||||
|
||||
public static ImageLocation createLocalLocation(Path sourceFile, String imageSrc,
|
||||
URI resolvedUri, Path resolvedPath) {
|
||||
|
||||
ImageLocation l = new ImageLocation(sourceFile, imageSrc);
|
||||
l.resolvedUri = resolvedUri;
|
||||
l.resolvedPath = resolvedPath;
|
||||
l.isRemote = false;
|
||||
l.isRuntime = false;
|
||||
return l;
|
||||
}
|
||||
|
||||
public static ImageLocation createRuntimeLocation(Path sourceFile, String imageSrc,
|
||||
URI resolvedUri, Path resolvedPath) {
|
||||
|
||||
ImageLocation l = new ImageLocation(sourceFile, imageSrc);
|
||||
l.resolvedUri = resolvedUri;
|
||||
l.resolvedPath = resolvedPath;
|
||||
l.isRemote = false;
|
||||
l.isRuntime = true;
|
||||
return l;
|
||||
}
|
||||
|
||||
public static ImageLocation createInvalidRuntimeLocation(Path sourceFile, String imageSrc) {
|
||||
|
||||
ImageLocation l = new ImageLocation(sourceFile, imageSrc);
|
||||
l.resolvedUri = null;
|
||||
l.resolvedPath = null;
|
||||
l.isRemote = false;
|
||||
l.isRuntime = true;
|
||||
return l;
|
||||
}
|
||||
|
||||
public static ImageLocation createRemoteLocation(Path sourceFile, String imageSrc,
|
||||
URI resolvedUri) {
|
||||
|
||||
ImageLocation l = new ImageLocation(sourceFile, imageSrc);
|
||||
l.resolvedUri = resolvedUri;
|
||||
l.resolvedPath = null;
|
||||
l.isRemote = true;
|
||||
l.isRuntime = false;
|
||||
return l;
|
||||
}
|
||||
|
||||
private ImageLocation(Path sourceFile, String imageSrc) {
|
||||
this.sourceFile = sourceFile;
|
||||
this.imageSrc = imageSrc;
|
||||
}
|
||||
|
||||
public Path getSourceFile() {
|
||||
return sourceFile;
|
||||
}
|
||||
|
||||
public void setSourceFile(Path sourceFile) {
|
||||
this.sourceFile = sourceFile;
|
||||
}
|
||||
|
||||
public String getImageSrc() {
|
||||
return imageSrc;
|
||||
}
|
||||
|
||||
public void setImageSrc(String imageSrc) {
|
||||
this.imageSrc = imageSrc;
|
||||
}
|
||||
|
||||
public Path getResolvedPath() {
|
||||
return resolvedPath;
|
||||
}
|
||||
|
||||
public void setResolvedPath(Path resolvedPath) {
|
||||
this.resolvedPath = resolvedPath;
|
||||
}
|
||||
|
||||
public URI getResolvedUri() {
|
||||
return resolvedUri;
|
||||
}
|
||||
|
||||
public void setResolvedUri(URI resolvedUri) {
|
||||
this.resolvedUri = resolvedUri;
|
||||
}
|
||||
|
||||
public boolean isRemote() {
|
||||
return isRemote;
|
||||
}
|
||||
|
||||
public void setRemote(boolean isRemote) {
|
||||
this.isRemote = isRemote;
|
||||
}
|
||||
|
||||
public boolean isRuntime() {
|
||||
return isRuntime;
|
||||
}
|
||||
|
||||
public void setRuntime(boolean isRuntime) {
|
||||
this.isRuntime = isRuntime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
//@formatter:off
|
||||
return "{\n" +
|
||||
"\tsource file: " + sourceFile + ",\n" +
|
||||
"\tsrc: " + imageSrc + ",\n" +
|
||||
"\turi: " + resolvedUri + ",\n" +
|
||||
"\tpath: " + resolvedPath + ",\n" +
|
||||
"\tis runtime: " + isRuntime + ",\n" +
|
||||
"\tis remote: " + isRemote + "\n" +
|
||||
"}";
|
||||
//@formatter:on
|
||||
}
|
||||
}
|
|
@ -0,0 +1,225 @@
|
|||
/* ###
|
||||
* 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 ghidra.util.exception.AssertException;
|
||||
import help.validator.LinkDatabase;
|
||||
import help.validator.location.HelpModuleCollection;
|
||||
import help.validator.model.AnchorDefinition;
|
||||
import help.validator.model.GhidraTOCFile;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* This class:
|
||||
* <ul>
|
||||
* <li>Creates a XXX_map.xml file (topic IDs to help files)</li>
|
||||
* <li>Creates a XXX_TOC.xml file from a source toc.xml file</li>
|
||||
* <li>Finds unused images</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class JavaHelpFilesBuilder {
|
||||
private static final String MAP_OUTPUT_FILENAME_SUFFIX = "_map.xml";
|
||||
private static final String TOC_OUTPUT_FILENAME_SUFFIX = "_TOC.xml";
|
||||
|
||||
private static final String LOG_FILENAME = "help.log";
|
||||
|
||||
private final String moduleName;
|
||||
private Path outputDir;
|
||||
private String mapOutputFilename;
|
||||
private String tocOutputFilename;
|
||||
private LinkDatabase linkDatabase;
|
||||
|
||||
public JavaHelpFilesBuilder(Path outputDir, String moduleName, LinkDatabase linkDatabase) {
|
||||
this.moduleName = moduleName;
|
||||
this.linkDatabase = linkDatabase;
|
||||
this.outputDir = initializeOutputDirectory(outputDir);
|
||||
|
||||
mapOutputFilename = moduleName + MAP_OUTPUT_FILENAME_SUFFIX;
|
||||
tocOutputFilename = moduleName + TOC_OUTPUT_FILENAME_SUFFIX;
|
||||
}
|
||||
|
||||
private Path initializeOutputDirectory(Path outputDirectory) {
|
||||
if (!Files.exists(outputDirectory)) {
|
||||
try {
|
||||
return Files.createDirectories(outputDirectory);
|
||||
}
|
||||
catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return outputDirectory;
|
||||
}
|
||||
|
||||
public void generateHelpFiles(HelpModuleCollection help) throws Exception {
|
||||
message("Generating Help Files for: " + help);
|
||||
|
||||
LogFileWriter errorLog = createLogFile();
|
||||
|
||||
boolean hasErrors = false;
|
||||
StringBuffer shortErrorDescription = new StringBuffer();
|
||||
try {
|
||||
generateMapFile(help);
|
||||
}
|
||||
catch (IOException e) {
|
||||
hasErrors = true;
|
||||
shortErrorDescription.append("Unexpected error generating map file!\n");
|
||||
errorLog.append("Failed to generate " + mapOutputFilename + ": ");
|
||||
errorLog.append(e.getMessage());
|
||||
errorLog.println();
|
||||
}
|
||||
|
||||
try {
|
||||
generateTOCFile(linkDatabase, help);
|
||||
}
|
||||
catch (IOException e) {
|
||||
hasErrors = true;
|
||||
shortErrorDescription.append("Unexpected error writing TOC file!\n");
|
||||
errorLog.append("Failed to generate " + tocOutputFilename + ": ");
|
||||
errorLog.append(e.getMessage());
|
||||
errorLog.println();
|
||||
}
|
||||
|
||||
if (hasErrors) {
|
||||
errorLog.close();
|
||||
throw new RuntimeException("Errors Creating Help Files - " + shortErrorDescription +
|
||||
"\n\tsee help log for details: " + errorLog.getFile());
|
||||
}
|
||||
|
||||
errorLog.close();
|
||||
if (errorLog.isEmpty()) {
|
||||
errorLog.delete();
|
||||
}
|
||||
|
||||
message("Done generating help files for module: " + moduleName);
|
||||
}
|
||||
|
||||
private LogFileWriter createLogFile() throws IOException {
|
||||
String logFilename = moduleName + "." + LOG_FILENAME;
|
||||
Path logFile = outputDir.resolve(logFilename);
|
||||
return new LogFileWriter(logFile);
|
||||
}
|
||||
|
||||
private static void message(String message) {
|
||||
System.out.println("[" + JavaHelpFilesBuilder.class.getSimpleName() + "] " + message);
|
||||
System.out.flush();
|
||||
}
|
||||
|
||||
private void generateMapFile(HelpModuleCollection help) throws IOException {
|
||||
Path mapFile = outputDir.resolve(mapOutputFilename);
|
||||
message("Generating map file: " + mapFile.toUri() + "...");
|
||||
if (Files.exists(mapFile)) {
|
||||
Files.delete(mapFile);
|
||||
}
|
||||
PrintWriter out = new LogFileWriter(mapFile);
|
||||
try {
|
||||
out.println("<?xml version='1.0' encoding='ISO-8859-1' ?>");
|
||||
out.println("<!doctype MAP public \"-//Sun Microsystems Inc.//DTD JavaHelp Map Version 1.0//EN\">");
|
||||
out.println("<!-- Auto-generated on " + (new Date()).toString() + " : Do Not Edit -->");
|
||||
out.println("<map version=\"1.0\">");
|
||||
|
||||
Collection<AnchorDefinition> anchors = help.getAllAnchorDefinitions();
|
||||
Iterator<AnchorDefinition> iterator = anchors.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
AnchorDefinition a = iterator.next();
|
||||
String anchorTarget = a.getHelpPath();
|
||||
|
||||
//
|
||||
// JavaHelp Note: the JavaHelp system will resolve relative map entries by using
|
||||
// this map file that we are generating as the base. So, whatever
|
||||
// directory this file lives under is the root directory for the
|
||||
// relative path that we are writing here for the 'mapID' entry.
|
||||
// Thus, make sure that the relative entry is relative to the
|
||||
// directory of this map file.
|
||||
//
|
||||
|
||||
String updatedPath = relativize(outputDir, anchorTarget);
|
||||
out.println(" <mapID target=\"" + a.getId() + "\" url=\"" + updatedPath + "\"/>");
|
||||
}
|
||||
out.println("</map>");
|
||||
message("\tfinished generating map file");
|
||||
}
|
||||
finally {
|
||||
out.close();
|
||||
}
|
||||
}
|
||||
|
||||
private String relativize(Path parent, String anchorTarget) {
|
||||
Path anchorPath = Paths.get(anchorTarget);
|
||||
if (anchorPath.isAbsolute()) {
|
||||
return anchorTarget; // not a relative path; nothing to do
|
||||
}
|
||||
|
||||
if (!parent.endsWith("help")) {
|
||||
throw new AssertException("Map file expected in a directory name 'help'. "
|
||||
+ "Update the map file generation code.");
|
||||
}
|
||||
|
||||
if (!anchorTarget.startsWith("help")) {
|
||||
throw new AssertException("Relative anchor path does not start with 'help'");
|
||||
}
|
||||
|
||||
Path relative = anchorPath.subpath(1, anchorPath.getNameCount());
|
||||
String relativePath = relative.toString();
|
||||
String normalized = relativePath.replaceAll("\\\\", "/");
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private void generateTOCFile(LinkDatabase database, HelpModuleCollection help)
|
||||
throws IOException {
|
||||
message("Generating TOC file: " + tocOutputFilename + "...");
|
||||
GhidraTOCFile sourceTOCFile = help.getSourceTOCFile();
|
||||
Path outputFile = outputDir.resolve(tocOutputFilename);
|
||||
database.generateTOCOutputFile(outputFile, sourceTOCFile);
|
||||
message("\tfinished generating TOC file");
|
||||
}
|
||||
|
||||
//==================================================================================================
|
||||
// Inner Classes
|
||||
//==================================================================================================
|
||||
|
||||
private class LogFileWriter extends PrintWriter {
|
||||
private final Path file;
|
||||
|
||||
LogFileWriter(Path logFile) throws IOException {
|
||||
super(new BufferedWriter(new OutputStreamWriter(Files.newOutputStream(logFile))));
|
||||
this.file = logFile;
|
||||
}
|
||||
|
||||
Path getFile() {
|
||||
return file;
|
||||
}
|
||||
|
||||
boolean isEmpty() {
|
||||
try {
|
||||
return Files.size(file) == 0;
|
||||
}
|
||||
catch (IOException e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void delete() {
|
||||
try {
|
||||
Files.delete(file);
|
||||
}
|
||||
catch (IOException e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
195
Ghidra/Framework/Help/src/main/java/help/JavaHelpSetBuilder.java
Normal file
195
Ghidra/Framework/Help/src/main/java/help/JavaHelpSetBuilder.java
Normal file
|
@ -0,0 +1,195 @@
|
|||
/* ###
|
||||
* 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.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package help;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class JavaHelpSetBuilder {
|
||||
private static final String TAB = "\t";
|
||||
private static final Set<String> searchFileNames =
|
||||
new HashSet<String>(Arrays.asList(new String[] { "DOCS", "DOCS.TAB", "OFFSETS",
|
||||
"POSITIONS", "SCHEMA", "TMAP" }));
|
||||
private static int indentionLevel;
|
||||
|
||||
private final String moduleName;
|
||||
private final Path mapFile;
|
||||
private final Path tocFile;
|
||||
private final Path searchDirectory;
|
||||
private final Path helpSetFile;
|
||||
|
||||
public JavaHelpSetBuilder(String moduleName, Path helpMapFile, Path helpTOCFile,
|
||||
Path indexerOutputDirectory, Path helpSetFile2) {
|
||||
this.moduleName = moduleName;
|
||||
this.mapFile = helpMapFile;
|
||||
this.tocFile = helpTOCFile;
|
||||
this.searchDirectory = indexerOutputDirectory;
|
||||
this.helpSetFile = helpSetFile2;
|
||||
}
|
||||
|
||||
public void writeHelpSetFile() throws IOException {
|
||||
BufferedWriter writer = null;
|
||||
try {
|
||||
OutputStreamWriter osw = new OutputStreamWriter(Files.newOutputStream(helpSetFile));
|
||||
writer = new BufferedWriter(osw);
|
||||
|
||||
generateFileHeader(writer, moduleName);
|
||||
|
||||
writeMapEntry(mapFile, writer);
|
||||
|
||||
writeTOCEntry(tocFile, writer);
|
||||
|
||||
writeSearchEntry(searchDirectory, writer);
|
||||
|
||||
writeFavoritesEntry(writer);
|
||||
|
||||
generateFileFooter(writer);
|
||||
}
|
||||
finally {
|
||||
if (writer != null) {
|
||||
try {
|
||||
writer.close();
|
||||
}
|
||||
catch (IOException e) {
|
||||
// we tried
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeMapEntry(Path helpSetMapFile, BufferedWriter writer)
|
||||
throws IOException {
|
||||
writeLine("<maps>", writer);
|
||||
|
||||
indentionLevel++;
|
||||
writeLine("<mapref location=\"" + helpSetMapFile.getFileName() + "\" />", writer);
|
||||
indentionLevel--;
|
||||
|
||||
writeLine("</maps>", writer);
|
||||
}
|
||||
|
||||
private static void writeTOCEntry(Path helpSetTOCFile, BufferedWriter writer)
|
||||
throws IOException {
|
||||
writeLine("<view mergetype=\"javax.help.UniteAppendMerge\">", writer);
|
||||
|
||||
indentionLevel++;
|
||||
writeLine("<name>TOC</name>", writer);
|
||||
writeLine("<label>Ghidra Table of Contents</label>", writer);
|
||||
writeLine("<type>docking.help.CustomTOCView</type>", writer);
|
||||
writeLine("<data>" + helpSetTOCFile.getFileName() + "</data>", writer);
|
||||
indentionLevel--;
|
||||
|
||||
writeLine("</view>", writer);
|
||||
}
|
||||
|
||||
private static void writeSearchEntry(Path helpSearchDirectory, BufferedWriter writer)
|
||||
throws IOException {
|
||||
if (!Files.exists(helpSearchDirectory)) {
|
||||
return; // some help dirs don't have content, like GhidraHelp
|
||||
}
|
||||
|
||||
writeLine("<view>", writer);
|
||||
|
||||
indentionLevel++;
|
||||
writeLine("<name>Search</name>", writer);
|
||||
writeLine("<label>Search for Keywords</label>", writer);
|
||||
// writeLine("<type>javax.help.SearchView</type>", writer);
|
||||
writeLine("<type>docking.help.CustomSearchView</type>", writer);
|
||||
|
||||
if (hasIndexerFiles(helpSearchDirectory)) {
|
||||
writeLine("<data engine=\"com.sun.java.help.search.DefaultSearchEngine\">" +
|
||||
helpSearchDirectory.getFileName() + "</data>", writer);
|
||||
}
|
||||
indentionLevel--;
|
||||
|
||||
writeLine("</view>", writer);
|
||||
}
|
||||
|
||||
private static boolean hasIndexerFiles(Path helpSearchDirectory) {
|
||||
Set<String> found = new HashSet<String>();
|
||||
try (DirectoryStream<Path> ds = Files.newDirectoryStream(helpSearchDirectory);) {
|
||||
for (Path file : ds) {
|
||||
found.add(file.getFileName().toString());
|
||||
}
|
||||
}
|
||||
catch (IOException e) {
|
||||
// It doesn't exist, so it's "empty".
|
||||
}
|
||||
return searchFileNames.equals(found);
|
||||
}
|
||||
|
||||
private static void writeFavoritesEntry(BufferedWriter writer) throws IOException {
|
||||
writeLine("<view>", writer);
|
||||
|
||||
indentionLevel++;
|
||||
writeLine("<name>Favorites</name>", writer);
|
||||
// writeLine( "<label>Favorites</label>", writer );
|
||||
// writeLine( "<type>javax.help.FavoritesView</type>", writer );
|
||||
writeLine("<label>Ghidra Favorites</label>", writer);
|
||||
writeLine("<type>docking.help.CustomFavoritesView</type>", writer);
|
||||
indentionLevel--;
|
||||
|
||||
writeLine("</view>", writer);
|
||||
}
|
||||
|
||||
private static void generateFileHeader(BufferedWriter writer, String moduleName)
|
||||
throws IOException {
|
||||
writer.write("<?xml version='1.0' encoding='ISO-8859-1' ?>");
|
||||
writer.newLine();
|
||||
writer.write("<!DOCTYPE helpset PUBLIC \"-//Sun Microsystems Inc.//DTD JavaHelp "
|
||||
+ "HelpSet Version 2.0//EN\" \"http://java.sun.com/products/javahelp/helpset_2_0.dtd\">");
|
||||
writer.newLine();
|
||||
writer.newLine();
|
||||
|
||||
writer.write("<!-- HelpSet auto-generated on " + (new Date()).toString() + " -->");
|
||||
writer.newLine();
|
||||
|
||||
writer.write("<helpset version=\"2.0\">");
|
||||
writer.newLine();
|
||||
|
||||
indentionLevel++;
|
||||
writeIndentation(writer);
|
||||
writer.write("<title>" + moduleName + " HelpSet</title>");
|
||||
writer.newLine();
|
||||
}
|
||||
|
||||
private static void generateFileFooter(BufferedWriter writer) throws IOException {
|
||||
indentionLevel--;
|
||||
writer.write("</helpset>");
|
||||
writer.newLine();
|
||||
}
|
||||
|
||||
private static void writeLine(String text, BufferedWriter writer) throws IOException {
|
||||
writeIndentation(writer);
|
||||
writer.write(text);
|
||||
writer.newLine();
|
||||
}
|
||||
|
||||
private static void writeIndentation(BufferedWriter writer) throws IOException {
|
||||
for (int i = 0; i < indentionLevel; i++) {
|
||||
writer.write(TAB);
|
||||
}
|
||||
}
|
||||
}
|
289
Ghidra/Framework/Help/src/main/java/help/OverlayHelpTree.java
Normal file
289
Ghidra/Framework/Help/src/main/java/help/OverlayHelpTree.java
Normal file
|
@ -0,0 +1,289 @@
|
|||
/* ###
|
||||
* 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.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package help;
|
||||
|
||||
import help.validator.LinkDatabase;
|
||||
import help.validator.model.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* A class that will take in a group of help directories and create a tree of
|
||||
* help Table of Contents (TOC) items. Ideally, this tree can be used to create a single
|
||||
* TOC document, or individual TOC documents, one for each help directory (this allows
|
||||
* for better modularity).
|
||||
* <p>
|
||||
* We call this class an <b>overlay</b> tree to drive home the idea that each
|
||||
* help directory's TOC data is put into the tree, with any duplicate paths overlayed
|
||||
* on top of those from other help directories.
|
||||
*
|
||||
*/
|
||||
public class OverlayHelpTree {
|
||||
|
||||
private Map<String, Set<TOCItem>> parentToChildrenMap = new HashMap<String, Set<TOCItem>>();
|
||||
private TOCItem rootItem;
|
||||
private OverlayNode rootNode;
|
||||
private final LinkDatabase linkDatabase;
|
||||
|
||||
public OverlayHelpTree(TOCItemProvider tocItemProvider, LinkDatabase linkDatabase) {
|
||||
this.linkDatabase = linkDatabase;
|
||||
for (TOCItemExternal external : tocItemProvider.getTOCItemExternalsByDisplayMapping().values()) {
|
||||
addExternalTOCItem(external);
|
||||
}
|
||||
for (TOCItemDefinition definition : tocItemProvider.getTOCItemDefinitionsByIDMapping().values()) {
|
||||
addSourceTOCItem(definition);
|
||||
}
|
||||
}
|
||||
|
||||
private void addExternalTOCItem(TOCItem item) {
|
||||
TOCItem parent = item.getParent();
|
||||
String parentID = parent == null ? null : parent.getIDAttribute();
|
||||
if (parentID == null) {
|
||||
// must be the root, since the root has no parent
|
||||
if (rootItem != null) {
|
||||
|
||||
//
|
||||
// We will have equivalent items in the generated TOC files, as that is how we
|
||||
// enable merging of TOC files in the JavaHelp system. So, multiple roots are
|
||||
// OK.
|
||||
//
|
||||
|
||||
if (!item.isEquivalent(rootItem)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Cannot define more than one root node:\n\t" + item +
|
||||
", but there already exists\n\t" + rootItem);
|
||||
}
|
||||
}
|
||||
else {
|
||||
rootItem = item;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
doAddTOCIItem(item);
|
||||
}
|
||||
|
||||
private void addSourceTOCItem(TOCItem item) {
|
||||
TOCItem parent = item.getParent();
|
||||
String parentID = parent == null ? null : parent.getIDAttribute();
|
||||
if (parentID == null) {
|
||||
// must be the root, since the root has no parent
|
||||
if (rootItem != null) {
|
||||
// when loading source items, it is only an error when there is more than one
|
||||
// root item defined *in the same file*
|
||||
if (rootItem.getSourceFile().equals(item.getSourceFile())) {
|
||||
throw new IllegalArgumentException(
|
||||
"Cannot define more than one root node in the same file:\n\t" + item +
|
||||
",\nbut there already exists\n\t" + rootItem);
|
||||
}
|
||||
}
|
||||
else {
|
||||
rootItem = item;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
doAddTOCIItem(item);
|
||||
}
|
||||
|
||||
private void doAddTOCIItem(TOCItem item) {
|
||||
TOCItem parent = item.getParent();
|
||||
String parentID = parent == null ? null : parent.getIDAttribute();
|
||||
Set<TOCItem> set = parentToChildrenMap.get(parentID);
|
||||
if (set == null) {
|
||||
set = new LinkedHashSet<TOCItem>();
|
||||
parentToChildrenMap.put(parentID, set);
|
||||
}
|
||||
|
||||
set.add(item);
|
||||
}
|
||||
|
||||
public void printTreeForID(Path outputFile, String sourceFileID) throws IOException {
|
||||
|
||||
if (Files.exists(outputFile)) {
|
||||
Files.delete(outputFile);
|
||||
}
|
||||
|
||||
OutputStreamWriter osw = new OutputStreamWriter(Files.newOutputStream(outputFile));
|
||||
PrintWriter writer = new PrintWriter(new BufferedWriter(osw));
|
||||
printTreeForID(writer, sourceFileID);
|
||||
|
||||
// debug
|
||||
// writer = new PrintWriter(System.err);
|
||||
// printTreeForID(writer, sourceFileID);
|
||||
}
|
||||
|
||||
void printTreeForID(PrintWriter writer, String sourceFileID) {
|
||||
initializeTree();
|
||||
|
||||
try {
|
||||
writer.println("<?xml version='1.0' encoding='ISO-8859-1' ?>");
|
||||
writer.println("<!-- Auto-generated on " + (new Date()).toString() + " -->");
|
||||
writer.println();
|
||||
writer.println("<toc version=\"2.0\">");
|
||||
|
||||
printContents(sourceFileID, writer);
|
||||
|
||||
writer.println("</toc>");
|
||||
}
|
||||
finally {
|
||||
writer.close();
|
||||
}
|
||||
}
|
||||
|
||||
private void printContents(String sourceFileID, PrintWriter writer) {
|
||||
if (rootNode == null) {
|
||||
// assume not TOC contents; empty TOC file
|
||||
return;
|
||||
}
|
||||
|
||||
rootNode.print(sourceFileID, writer, 0);
|
||||
}
|
||||
|
||||
private boolean initializeTree() {
|
||||
if (rootNode != null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (rootItem == null) {
|
||||
// no content in the TOC file; help module does not appear in TOC view
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO delete this debug
|
||||
// Set<Entry<String, Set<TOCItem>>> entrySet = parentToChildrenMap.entrySet();
|
||||
// for (Entry<String, Set<TOCItem>> entry : entrySet) {
|
||||
// System.out.println(entry.getKey() + " -> " + entry.getValue());
|
||||
// }
|
||||
|
||||
OverlayNode newRootNode = new OverlayNode(null, rootItem);
|
||||
buildChildren(newRootNode);
|
||||
|
||||
if (!parentToChildrenMap.isEmpty()) {
|
||||
throw new RuntimeException("Unresolved definitions in tree!");
|
||||
}
|
||||
rootNode = newRootNode;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void buildChildren(OverlayNode node) {
|
||||
String definitionID = node.getDefinitionID();
|
||||
Set<TOCItem> children = parentToChildrenMap.remove(definitionID);
|
||||
if (children == null) {
|
||||
return; // childless
|
||||
}
|
||||
|
||||
for (TOCItem child : children) {
|
||||
OverlayNode childNode = new OverlayNode(node, child);
|
||||
node.addChild(childNode);
|
||||
buildChildren(childNode);
|
||||
}
|
||||
}
|
||||
|
||||
//==================================================================================================
|
||||
// Inner Classes
|
||||
//==================================================================================================
|
||||
|
||||
private class OverlayNode {
|
||||
private final TOCItem item;
|
||||
private final OverlayNode parentNode;
|
||||
private Set<String> fileIDs = new HashSet<String>();
|
||||
private Set<OverlayNode> children = new TreeSet<OverlayNode>(CHILD_SORT_COMPARATOR);
|
||||
|
||||
public OverlayNode(OverlayNode parentNode, TOCItem rootItem) {
|
||||
this.parentNode = parentNode;
|
||||
this.item = rootItem;
|
||||
Path sourceFile = rootItem.getSourceFile();
|
||||
String fileID = sourceFile.toUri().toString();
|
||||
addFileIDToTreePath(fileID);
|
||||
}
|
||||
|
||||
void print(String sourceFileID, PrintWriter writer, int indentLevel) {
|
||||
if (!fileIDs.contains(sourceFileID)) {
|
||||
return;
|
||||
}
|
||||
|
||||
writer.println(item.generateTOCItemTag(linkDatabase, children.isEmpty(), indentLevel));
|
||||
if (!children.isEmpty()) {
|
||||
|
||||
for (OverlayNode node : children) {
|
||||
node.print(sourceFileID, writer, indentLevel + 1);
|
||||
}
|
||||
writer.println(item.generateEndTag(indentLevel));
|
||||
}
|
||||
}
|
||||
|
||||
void addChild(OverlayNode overlayNode) {
|
||||
children.add(overlayNode);
|
||||
}
|
||||
|
||||
String getDefinitionID() {
|
||||
return item.getIDAttribute();
|
||||
}
|
||||
|
||||
private void addFileIDToTreePath(String fileID) {
|
||||
fileIDs.add(fileID);
|
||||
if (parentNode != null) {
|
||||
parentNode.addFileIDToTreePath(fileID);
|
||||
}
|
||||
}
|
||||
|
||||
TOCItem getTOCItemDefinition() {
|
||||
return item;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return item.toString();
|
||||
}
|
||||
}
|
||||
|
||||
private static final Comparator<OverlayNode> CHILD_SORT_COMPARATOR =
|
||||
new Comparator<OverlayNode>() {
|
||||
@Override
|
||||
public int compare(OverlayNode ov1, OverlayNode ov2) {
|
||||
TOCItem o1 = ov1.getTOCItemDefinition();
|
||||
TOCItem o2 = ov2.getTOCItemDefinition();
|
||||
|
||||
if (!o1.getSortPreference().equals(o2.getSortPreference())) {
|
||||
return o1.getSortPreference().compareTo(o2.getSortPreference());
|
||||
}
|
||||
|
||||
// if sort preference is the same, then sort alphabetically by display name
|
||||
String text1 = o1.getTextAttribute();
|
||||
String text2 = o2.getTextAttribute();
|
||||
|
||||
// null values can happen for reference items
|
||||
if (text1 == null && text2 == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// push any null values to the bottom
|
||||
if (text1 == null) {
|
||||
return 1;
|
||||
}
|
||||
else if (text2 == null) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return text1.compareTo(text2);
|
||||
}
|
||||
};
|
||||
}
|
276
Ghidra/Framework/Help/src/main/java/help/TOCConverter.java
Normal file
276
Ghidra/Framework/Help/src/main/java/help/TOCConverter.java
Normal file
|
@ -0,0 +1,276 @@
|
|||
/* ###
|
||||
* 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.*;
|
||||
import java.util.*;
|
||||
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import javax.xml.parsers.SAXParserFactory;
|
||||
|
||||
import org.xml.sax.*;
|
||||
import org.xml.sax.helpers.DefaultHandler;
|
||||
import org.xml.sax.helpers.ParserAdapter;
|
||||
|
||||
import ghidra.util.xml.XmlUtilities;
|
||||
|
||||
/**
|
||||
* Converts the Ghidra "source" TOC file to a JavaHelp TOC file. The Ghidra
|
||||
* source TOC file contains the table of context index name and its
|
||||
* corresponding url. However, JavaHelp expects the target value to be map ID in
|
||||
* the map file.
|
||||
*
|
||||
*/
|
||||
public class TOCConverter {
|
||||
|
||||
private String sourceFilename;
|
||||
private String outFilename;
|
||||
|
||||
private final static String TOC_VERSION = "<toc version";
|
||||
private final static String TOCITEM = "<tocitem";
|
||||
private final static String TEXT = "text";
|
||||
private final static String TARGET = " target";
|
||||
|
||||
private Map<String, String> urlMap; // map TOC target tag to its corresponding URL
|
||||
private List<String> tocList; // list of TOC entry names values
|
||||
|
||||
TOCConverter(String sourceTOCfilename, String outFilename)
|
||||
throws IOException, SAXException, ParserConfigurationException {
|
||||
|
||||
sourceFilename = sourceTOCfilename;
|
||||
this.outFilename = outFilename;
|
||||
urlMap = new HashMap<String, String>();
|
||||
tocList = new ArrayList<String>();
|
||||
readSourceTOC();
|
||||
writeJavaHelpTOC();
|
||||
System.out.println(" TOC conversion is done!");
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the section of the map file for the table of contents.
|
||||
*
|
||||
* @param out output for the map file that maps a help ID to a url.
|
||||
* @throws IOException
|
||||
*/
|
||||
void writeTOCMapFile(PrintWriter out) {
|
||||
out.println(" <!-- Table of Contents help IDs -->");
|
||||
for (int i = 0; i < tocList.size(); i++) {
|
||||
String target = tocList.get(i);
|
||||
String url = urlMap.get(target);
|
||||
|
||||
String line = " <mapID target=\"" + target + "\" url=\"" + url + "\" />";
|
||||
out.println(line);
|
||||
}
|
||||
out.println(" <!-- End of Table of Contents help IDs -->");
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the source table of contents file and build up hash maps to maintain
|
||||
* TOC entry names to urls and map IDs.
|
||||
*/
|
||||
private void readSourceTOC() throws IOException, SAXException, ParserConfigurationException {
|
||||
SAXParserFactory factory = XmlUtilities.createSecureSAXParserFactory(false);
|
||||
XMLReader parser = new ParserAdapter(factory.newSAXParser().getParser());
|
||||
File file = createTempTOCFile();
|
||||
String fileURL = file.toURI().toURL().toString();
|
||||
TOCHandler handler = new TOCHandler();
|
||||
parser.setContentHandler(handler);
|
||||
parser.setErrorHandler(handler);
|
||||
parser.setFeature("http://xml.org/sax/features/namespaces", true);
|
||||
System.out.println(" Parsing input file " + sourceFilename);
|
||||
parser.parse(fileURL);
|
||||
file.deleteOnExit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the JavaHelp table of contents file.
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
private void writeJavaHelpTOC() throws IOException {
|
||||
System.out.println(" Writing JavaHelp TOC file " + outFilename);
|
||||
PrintWriter out = new PrintWriter(new FileOutputStream(outFilename));
|
||||
BufferedReader reader = new BufferedReader(new FileReader(sourceFilename));
|
||||
|
||||
String line = null;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (line.indexOf(TOCITEM) > 0) {
|
||||
TOCItem item = parseLine(line);
|
||||
if (item == null) {
|
||||
continue;
|
||||
}
|
||||
String endline = " >";
|
||||
if (line.endsWith("/>")) {
|
||||
endline = " />";
|
||||
}
|
||||
line = getPadString(line) + TOCITEM + " " + TEXT + "=\"" + item.getText() + "\"";
|
||||
|
||||
if (item.getTarget().length() > 0) {
|
||||
line = line + TARGET + "=\"" + item.getTarget() + "\"";
|
||||
}
|
||||
line = line + endline;
|
||||
}
|
||||
else if (line.indexOf(TOC_VERSION) == 0) {
|
||||
out.println("<!-- This is the JavaHelp Table of Contents file -->");
|
||||
out.println("<!-- Auto generated on " + new Date() + ": Do not edit! -->");
|
||||
}
|
||||
if (!line.startsWith("<!-- Source")) {
|
||||
out.println(line);
|
||||
}
|
||||
}
|
||||
|
||||
out.close();
|
||||
reader.close();
|
||||
}
|
||||
|
||||
private String getPadString(String line) {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
for (int i = 0; i < line.length(); i++) {
|
||||
if (line.charAt(i) == ' ') {
|
||||
sb.append(' ');
|
||||
}
|
||||
else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private TOCItem parseLine(String line) {
|
||||
int pos = line.indexOf(TOCITEM);
|
||||
line = line.substring(pos + TOCITEM.length());
|
||||
StringTokenizer st = new StringTokenizer(line, "=\"");
|
||||
st.nextToken();
|
||||
String text = st.nextToken();
|
||||
if (st.hasMoreTokens()) {
|
||||
st.nextToken();
|
||||
}
|
||||
if (!st.hasMoreTokens()) {
|
||||
return new TOCItem(text, "");
|
||||
}
|
||||
String target = st.nextToken();
|
||||
return new TOCItem(text, target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a temporary TOC file that does not have the <!DOCTYPE line in it
|
||||
* which causes the SAX parser to blow up; it does not like the bad url in
|
||||
* it.
|
||||
*
|
||||
* @return
|
||||
* @throws IOException
|
||||
*/
|
||||
private File createTempTOCFile() throws IOException {
|
||||
File tempFile = File.createTempFile("toc", ".xml");
|
||||
|
||||
PrintWriter out = new PrintWriter(new FileOutputStream(tempFile));
|
||||
BufferedReader reader = new BufferedReader(new FileReader(sourceFilename));
|
||||
boolean endLineFound = true;
|
||||
String line = null;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (line.startsWith("<!DOCTYPE")) {
|
||||
if (line.endsWith(">")) {
|
||||
continue;
|
||||
}
|
||||
endLineFound = false;
|
||||
}
|
||||
if (!endLineFound) {
|
||||
if (line.endsWith(">")) {
|
||||
endLineFound = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
out.println(line);
|
||||
}
|
||||
out.close();
|
||||
reader.close();
|
||||
return tempFile;
|
||||
}
|
||||
|
||||
private class TOCItem {
|
||||
private String text;
|
||||
private String target;
|
||||
|
||||
TOCItem(String text, String url) {
|
||||
this.text = text;
|
||||
target = url.replace('.', '_');
|
||||
target = target.replace('#', '_');
|
||||
target = target.replace('-', '_');
|
||||
}
|
||||
|
||||
String getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
String getTarget() {
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
private class TOCHandler extends DefaultHandler {
|
||||
|
||||
/**
|
||||
* @see org.xml.sax.ContentHandler#startElement(java.lang.String,
|
||||
* java.lang.String, java.lang.String, org.xml.sax.Attributes)
|
||||
*/
|
||||
@Override
|
||||
public void startElement(String namespaceURI, String localName, String qName,
|
||||
Attributes atts) throws SAXException {
|
||||
|
||||
if (atts != null) {
|
||||
if (!atts.getQName(0).equals(TEXT)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String url = atts.getValue(1);
|
||||
String target = url;
|
||||
if (url != null && url.length() > 0) {
|
||||
target = target.replace('.', '_');
|
||||
target = target.replace('#', '_');
|
||||
target = target.replace('-', '_');
|
||||
|
||||
urlMap.put(target, url);
|
||||
if (!tocList.contains(target)) {
|
||||
tocList.add(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final static void main(String[] args) {
|
||||
if (args.length < 2) {
|
||||
System.out.println("Usage: TOCConverter [source TOC filename] [out filename]");
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
try {
|
||||
TOCConverter conv = new TOCConverter(args[0], args[1]);
|
||||
File file = new File(args[1]);
|
||||
String name = file.getName();
|
||||
name = "map_" + name;
|
||||
|
||||
PrintWriter out =
|
||||
new PrintWriter(new FileOutputStream(new File(file.getParentFile(), name)));
|
||||
conv.writeTOCMapFile(out);
|
||||
out.close();
|
||||
}
|
||||
catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/* ###
|
||||
* 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.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package help;
|
||||
|
||||
import help.validator.model.TOCItemDefinition;
|
||||
import help.validator.model.TOCItemExternal;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* An interface that allows us to perform dependency injection in the testing
|
||||
* environment.
|
||||
*/
|
||||
public interface TOCItemProvider {
|
||||
|
||||
public Map<String, TOCItemExternal> getTOCItemExternalsByDisplayMapping();
|
||||
|
||||
public Map<String, TOCItemDefinition> getTOCItemDefinitionsByIDMapping();
|
||||
}
|
|
@ -0,0 +1,759 @@
|
|||
/* ###
|
||||
* 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.screenshot;
|
||||
|
||||
import help.GHelpBuilder;
|
||||
import help.HelpBuildUtils;
|
||||
import help.validator.UnusedHelpImageFileFinder;
|
||||
import help.validator.location.DirectoryHelpModuleLocation;
|
||||
import help.validator.location.HelpModuleLocation;
|
||||
import help.validator.model.HelpTopic;
|
||||
import help.validator.model.IMG;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.*;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
public class HelpMissingScreenShotReportGenerator {
|
||||
|
||||
private static boolean debugEnabled;
|
||||
private static final String DEBUG_OPTION = "-debug";
|
||||
private static final String SCREEN_SHOTS = "ScreenShots";
|
||||
|
||||
private static final String PNG_EXT = ".png";
|
||||
private static final String JAVA_DIR = "java";
|
||||
private static final String TEST = "test";
|
||||
private static final String CAPTURE = "Capture";
|
||||
private static final String CUSTOM_NAME = "Custom";
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
|
||||
if (args.length < 3) {
|
||||
throw new Exception(
|
||||
"Expecting at least 3 args: <output file path> <help modules> <screen shot tests> [" +
|
||||
DEBUG_OPTION + "]");
|
||||
}
|
||||
|
||||
for (String arg : args) {
|
||||
if (arg.equalsIgnoreCase(DEBUG_OPTION)) {
|
||||
debugEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
HelpMissingScreenShotReportGenerator generator =
|
||||
new HelpMissingScreenShotReportGenerator(args[0], args[1], args[2]);
|
||||
|
||||
generator.generateReport();
|
||||
}
|
||||
|
||||
private Set<HelpModuleLocation> helpDirectories = new HashSet<HelpModuleLocation>();
|
||||
private Map<String, HelpTopic> topicNameToTopic = new HashMap<String, HelpTopic>();
|
||||
private Set<HelpTestCase> testCases = new HashSet<HelpTestCase>();
|
||||
private Map<String, HelpTestCase> imageNameToTestCase = new HashMap<String, HelpTestCase>();
|
||||
|
||||
private SortedSet<String> badlyNamedTestFiles = new TreeSet<String>();
|
||||
private SortedSet<HelpTestCase> badlyNamedTestCases = new TreeSet<HelpTestCase>();
|
||||
// private Map<HelpDirectory, IMG> untestedImages = new TreeMap<HelpDirectory, IMG>();
|
||||
private Map<HelpTopic, Set<IMG>> untestedImages = new TreeMap<HelpTopic, Set<IMG>>();
|
||||
|
||||
private Set<Path> examinedImageFiles = new HashSet<Path>();
|
||||
|
||||
private File outputFile;
|
||||
|
||||
HelpMissingScreenShotReportGenerator(String outputFilePath, String helpModulePaths,
|
||||
String screenShotPaths) {
|
||||
this.outputFile = new File(outputFilePath);
|
||||
|
||||
parseHelpDirectories(helpModulePaths);
|
||||
|
||||
parseScreenShots(screenShotPaths);
|
||||
|
||||
validateScreenShotTests();
|
||||
|
||||
validateHelpImages();
|
||||
}
|
||||
|
||||
void generateReport() {
|
||||
outputFile.getParentFile().mkdirs(); // make sure the folder exists
|
||||
|
||||
BufferedWriter writer = null;
|
||||
try {
|
||||
writer = new BufferedWriter(new FileWriter(outputFile));
|
||||
doGenerateReport(writer);
|
||||
System.out.println("Report written to " + outputFile);
|
||||
}
|
||||
catch (Exception e) {
|
||||
errorMessage("FAILED!", e);
|
||||
}
|
||||
finally {
|
||||
if (writer != null) {
|
||||
try {
|
||||
writer.close();
|
||||
}
|
||||
catch (IOException e1) {
|
||||
// don't care
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void doGenerateReport(BufferedWriter writer) throws IOException {
|
||||
writeHeader(writer);
|
||||
|
||||
writer.write("<P>\n");
|
||||
|
||||
//
|
||||
// Total Image File Count
|
||||
//
|
||||
int untestedCount = 0;
|
||||
Collection<Set<IMG>> values = untestedImages.values();
|
||||
for (Set<IMG> set : values) {
|
||||
untestedCount += set.size();
|
||||
}
|
||||
|
||||
int totalImageCount = imageNameToTestCase.size() + untestedCount;
|
||||
|
||||
writer.write("<H3>\n");
|
||||
writer.write("Total Image Count: " + totalImageCount + "\n");
|
||||
writer.write("</H3>\n");
|
||||
|
||||
//
|
||||
// All Tested Images
|
||||
//
|
||||
writer.write("<H3>\n");
|
||||
writer.write("Total Tested Images: " + imageNameToTestCase.size() + "\n");
|
||||
writer.write("</H3>\n");
|
||||
|
||||
//
|
||||
// Badly Named Test Files
|
||||
//
|
||||
|
||||
writer.write("<H3>\n");
|
||||
writer.write("Improperly Named Test Files: " + badlyNamedTestFiles.size() + "\n");
|
||||
writer.write("</H3>\n");
|
||||
|
||||
writer.write("<P>\n");
|
||||
writer.write("<TABLE BORDER=\"1\">\n");
|
||||
|
||||
for (String filename : badlyNamedTestFiles) {
|
||||
|
||||
writer.write(" <TR>\n");
|
||||
|
||||
writer.write(" <TD>\n");
|
||||
writer.write(" ");
|
||||
writer.write(filename);
|
||||
writer.write('\n');
|
||||
writer.write(" </TD>\n");
|
||||
writer.write(" </TR>\n");
|
||||
}
|
||||
|
||||
writer.write("</TABLE>\n");
|
||||
writer.write("</P>\n");
|
||||
|
||||
//
|
||||
// Badly Named Test Cases
|
||||
//
|
||||
|
||||
writer.write("<H3>\n");
|
||||
writer.write("Improperly Named Test Cases: " + badlyNamedTestCases.size() + "\n");
|
||||
writer.write("</H3>\n");
|
||||
|
||||
writer.write("<P>\n");
|
||||
writer.write("<TABLE BORDER=\"1\">\n");
|
||||
|
||||
writer.write(" <TH>\n");
|
||||
writer.write(" Test Case\n");
|
||||
writer.write(" </TH>\n");
|
||||
writer.write(" <TH>\n");
|
||||
writer.write(" Image Name\n");
|
||||
writer.write(" </TH>\n");
|
||||
|
||||
String lastTopicName = null;
|
||||
for (HelpTestCase testCase : badlyNamedTestCases) {
|
||||
|
||||
writer.write(" <TR>\n");
|
||||
|
||||
writer.write(" <TD>\n");
|
||||
writer.write(" ");
|
||||
String topicName = testCase.getHelpTopic().getName();
|
||||
if (!topicName.equals(lastTopicName)) {
|
||||
lastTopicName = topicName;
|
||||
writer.write(topicName);
|
||||
writer.write('\n');
|
||||
}
|
||||
else {
|
||||
writer.write(" \n");
|
||||
}
|
||||
|
||||
writer.write(" </TD>\n");
|
||||
|
||||
writer.write(" <TD>\n");
|
||||
writer.write(" ");
|
||||
writer.write(testCase.getTestName());
|
||||
writer.write("()\n");
|
||||
writer.write(" </TD>\n");
|
||||
writer.write(" </TR>\n");
|
||||
}
|
||||
|
||||
writer.write("</TABLE>\n");
|
||||
writer.write("</P>\n");
|
||||
|
||||
//
|
||||
// All Untested Images
|
||||
//
|
||||
|
||||
File untestedOutputFile = new File(outputFile.getParentFile(), "_untested.images.html");
|
||||
|
||||
writer.write("<H3>\n");
|
||||
writer.write("<A HREF=\"" + untestedOutputFile.getName() + "\">Total Untested Images: " +
|
||||
untestedCount + "</A>\n");
|
||||
writer.write("</H3>\n");
|
||||
|
||||
generateUntestedImagesFile(untestedOutputFile);
|
||||
|
||||
//
|
||||
// All Unused Images
|
||||
//
|
||||
|
||||
Set<Path> unusedImages = getUnusedImages();
|
||||
|
||||
writer.write("<H3>\n");
|
||||
writer.write("Total Unused Images: " + unusedImages.size() + "\n");
|
||||
writer.write("</H3>\n");
|
||||
|
||||
writer.write("<P>\n");
|
||||
writer.write("<TABLE BORDER=\"1\">\n");
|
||||
|
||||
writer.write(" <TH>\n");
|
||||
writer.write(" Help Topic\n");
|
||||
writer.write(" </TH>\n");
|
||||
writer.write(" <TH>\n");
|
||||
writer.write(" Image Name\n");
|
||||
writer.write(" </TH>\n");
|
||||
|
||||
for (Path imageFile : unusedImages) {
|
||||
Path helpTopicDir = HelpBuildUtils.getHelpTopicDir(imageFile);
|
||||
|
||||
writer.write(" <TR>\n");
|
||||
writer.write(" <TD>\n");
|
||||
writer.write(" ");
|
||||
|
||||
String topicName = helpTopicDir.getFileName().toString();
|
||||
if (!topicName.equals(lastTopicName)) {
|
||||
lastTopicName = topicName;
|
||||
writer.write(topicName);
|
||||
writer.write('\n');
|
||||
}
|
||||
else {
|
||||
writer.write(" \n");
|
||||
}
|
||||
|
||||
writer.write(" </TD>\n");
|
||||
|
||||
writer.write(" <TD>\n");
|
||||
writer.write(" ");
|
||||
writer.write(imageFile.getParent() + File.separator);
|
||||
writer.write("<font size=\"5\">");
|
||||
writer.write(imageFile.getFileName().toString());
|
||||
writer.write("</font>");
|
||||
writer.write('\n');
|
||||
writer.write(" </TD>\n");
|
||||
writer.write(" </TR>\n");
|
||||
}
|
||||
|
||||
writer.write("</TABLE>\n");
|
||||
writer.write("</P>");
|
||||
|
||||
writeFooter(writer);
|
||||
}
|
||||
|
||||
private void generateUntestedImagesFile(File untestedOutputFile) {
|
||||
BufferedWriter writer = null;
|
||||
try {
|
||||
writer = new BufferedWriter(new FileWriter(untestedOutputFile));
|
||||
doGenerateUntestedImagesFile(writer);
|
||||
}
|
||||
catch (Exception e) {
|
||||
errorMessage("FAILED! writing untested images file", e);
|
||||
}
|
||||
finally {
|
||||
if (writer != null) {
|
||||
try {
|
||||
writer.close();
|
||||
}
|
||||
catch (IOException e1) {
|
||||
// don't care
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void doGenerateUntestedImagesFile(BufferedWriter writer) throws IOException {
|
||||
writeHeader(writer);
|
||||
|
||||
writer.write("<P>\n");
|
||||
writer.write("<TABLE BORDER=\"1\">\n");
|
||||
|
||||
writer.write(" <TH>\n");
|
||||
writer.write(" Help Topic\n");
|
||||
writer.write(" </TH>\n");
|
||||
writer.write(" <TH>\n");
|
||||
writer.write(" Image Name\n");
|
||||
writer.write(" </TH>\n");
|
||||
|
||||
Set<Entry<HelpTopic, Set<IMG>>> entrySet = untestedImages.entrySet();
|
||||
for (Entry<HelpTopic, Set<IMG>> entry : entrySet) {
|
||||
|
||||
boolean printTopic = true;
|
||||
Set<IMG> set = entry.getValue();
|
||||
for (IMG img : set) {
|
||||
|
||||
writer.write(" <TR>\n");
|
||||
writer.write(" <TD>\n");
|
||||
writer.write(" ");
|
||||
|
||||
if (printTopic) {
|
||||
printTopic = false;
|
||||
writer.write(entry.getKey().getName());
|
||||
}
|
||||
else {
|
||||
writer.write(" ");
|
||||
}
|
||||
|
||||
writer.write('\n');
|
||||
writer.write(" </TD>\n");
|
||||
|
||||
writer.write(" <TD>\n");
|
||||
writer.write(" ");
|
||||
|
||||
Path imageFile = img.getImageFile();
|
||||
writer.write(imageFile.getParent() + File.separator);
|
||||
writer.write("<font size=\"5\">");
|
||||
writer.write(imageFile.getFileName().toString());
|
||||
writer.write("</font>");
|
||||
writer.write('\n');
|
||||
writer.write(" </TD>\n");
|
||||
writer.write(" </TR>\n");
|
||||
}
|
||||
}
|
||||
|
||||
writer.write("</TABLE>\n");
|
||||
writer.write("</P>\n");
|
||||
|
||||
writeFooter(writer);
|
||||
}
|
||||
|
||||
private Set<Path> getUnusedImages() {
|
||||
UnusedHelpImageFileFinder finder =
|
||||
new UnusedHelpImageFileFinder(helpDirectories, debugEnabled);
|
||||
return finder.getUnusedImages();
|
||||
}
|
||||
|
||||
private void validateHelpImages() {
|
||||
debug("validating help images...");
|
||||
|
||||
for (HelpModuleLocation helpDir : helpDirectories) {
|
||||
Collection<HelpTopic> topics = helpDir.getHelpTopics();
|
||||
for (HelpTopic topic : topics) {
|
||||
Collection<IMG> IMGs = topic.getAllIMGs();
|
||||
for (IMG img : IMGs) {
|
||||
Path imageFile = img.getImageFile();
|
||||
String imageName = imageFile.getFileName().toString();
|
||||
HelpTestCase testCase = imageNameToTestCase.get(imageName);
|
||||
if (testCase == null) {
|
||||
filterUntestedImage(topic, img);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debug("Total untested images: " + untestedImages.size());
|
||||
}
|
||||
|
||||
private void filterUntestedImage(HelpTopic topic, IMG img) {
|
||||
Path imageFile = img.getImageFile();
|
||||
if (examinedImageFiles.contains(imageFile)) {
|
||||
return; // already checked this image
|
||||
}
|
||||
examinedImageFiles.add(imageFile);
|
||||
|
||||
// we don't wish to track small icons
|
||||
URL url;
|
||||
try {
|
||||
url = imageFile.toUri().toURL();
|
||||
|
||||
// debug("Reading image: " + url);
|
||||
BufferedImage bufferedImage = ImageIO.read(url);
|
||||
int width = bufferedImage.getWidth();
|
||||
if (width <= 32) {
|
||||
return;
|
||||
}
|
||||
|
||||
int height = bufferedImage.getHeight();
|
||||
if (height <= 32) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if the size is not enough, then we may just have to hard-code a 'known image list'
|
||||
}
|
||||
catch (MalformedURLException e) {
|
||||
errorMessage("Unable to read image: " + img, e);
|
||||
}
|
||||
catch (IndexOutOfBoundsException ioobe) {
|
||||
// happens for some of our invalid images
|
||||
errorMessage("Problem reading image (bad data?): " + img, ioobe);
|
||||
return;
|
||||
}
|
||||
catch (IOException e) {
|
||||
errorMessage("Unable to load image: " + img, e);
|
||||
}
|
||||
|
||||
Set<IMG> set = untestedImages.get(topic);
|
||||
if (set == null) {
|
||||
set = new TreeSet<IMG>();
|
||||
untestedImages.put(topic, set);
|
||||
}
|
||||
|
||||
set.add(img);
|
||||
}
|
||||
|
||||
private void validateScreenShotTests() {
|
||||
debug("validating screen shots...");
|
||||
|
||||
for (HelpTestCase testCase : testCases) {
|
||||
String imageName = testCase.getImageName();
|
||||
HelpTopic helpTopic = testCase.getHelpTopic();
|
||||
Collection<IMG> imgs = helpTopic.getAllIMGs();
|
||||
|
||||
boolean foundImage = false;
|
||||
for (IMG img : imgs) {
|
||||
|
||||
if (img.getImageFile().toAbsolutePath().toString().contains("shared")) {
|
||||
continue; // skip images in the shared/images dir, they are not screen shots
|
||||
}
|
||||
|
||||
Path imageFile = img.getImageFile();
|
||||
if (imageFile == null) {
|
||||
errorMessage("\n\nNo image file found for IMG tag: " + img + " in topic: " +
|
||||
helpTopic, null);
|
||||
}
|
||||
|
||||
String imgName = imageFile.getFileName().toString();
|
||||
if (testCase.matches(imgName)) {
|
||||
foundImage = true;
|
||||
|
||||
// there may be case issues in the test vs. the filename--prefer the filename
|
||||
imageName = imgName;
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (!foundImage) {
|
||||
badlyNamedTestCases.add(testCase);
|
||||
}
|
||||
else {
|
||||
imageNameToTestCase.put(imageName, testCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void parseScreenShots(String screenShotPaths) {
|
||||
debug("parsing help screenshots...");
|
||||
|
||||
StringTokenizer tokenizer = new StringTokenizer(screenShotPaths, File.pathSeparator);
|
||||
while (tokenizer.hasMoreTokens()) {
|
||||
String path = tokenizer.nextToken();
|
||||
debug("\tparsing path entry: " + path);
|
||||
|
||||
Path screenshotFile = Paths.get(path);
|
||||
String testName = screenshotFile.getFileName().toString();
|
||||
HelpTopic helpTopic = getHelpTopic(testName, screenshotFile);
|
||||
if (helpTopic == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
HelpModuleLocation helpDir = helpTopic.getHelpDirectory();
|
||||
HelpTestFile testFile = new HelpTestFile(helpDir, helpTopic, screenshotFile, testName);
|
||||
Class<?> clazz = loadClass(screenshotFile);
|
||||
parseScreenShotTests(testFile, clazz);
|
||||
}
|
||||
|
||||
debug("\tscreenshot test count: " + testCases.size());
|
||||
}
|
||||
|
||||
private HelpTopic getHelpTopic(String testName, Path screenshotFile) {
|
||||
|
||||
int index = testName.indexOf(SCREEN_SHOTS);
|
||||
String topicName = testName.substring(0, index);
|
||||
|
||||
HelpTopic helpTopic = topicNameToTopic.get(topicName);
|
||||
if (helpTopic != null) {
|
||||
return helpTopic;
|
||||
}
|
||||
|
||||
debug("Found file without a proper help topic name: " + topicName);
|
||||
|
||||
// we make an exception for 'custom' test files
|
||||
int custom = testName.indexOf(CUSTOM_NAME);
|
||||
if (custom < 0) {
|
||||
// not custom
|
||||
badlyNamedTestFiles.add(testName);
|
||||
return null;
|
||||
}
|
||||
|
||||
// note: the format for a custom screenshot name is FooCustomScreenShots, where Foo is the
|
||||
// topic name
|
||||
topicName = testName.substring(0, custom);
|
||||
helpTopic = topicNameToTopic.get(topicName);
|
||||
if (helpTopic != null) {
|
||||
debug("\tit IS a custom screenshot; it is valid");
|
||||
return helpTopic;
|
||||
}
|
||||
|
||||
// nope, it's bad
|
||||
badlyNamedTestFiles.add(testName);
|
||||
return null;
|
||||
}
|
||||
|
||||
private void parseScreenShotTests(HelpTestFile testFile, Class<?> clazz) {
|
||||
Method[] methods = clazz.getDeclaredMethods();
|
||||
for (Method method : methods) {
|
||||
int modifiers = method.getModifiers();
|
||||
boolean isPublic = (Modifier.PUBLIC & modifiers) == Modifier.PUBLIC;
|
||||
if (!isPublic) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String name = method.getName();
|
||||
if (!name.startsWith(TEST)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
debug("\tfound test method: " + name);
|
||||
HelpTestCase helpTestCase = new HelpTestCase(testFile, name);
|
||||
testCases.add(helpTestCase);
|
||||
}
|
||||
}
|
||||
|
||||
private Class<?> loadClass(Path testFile) {
|
||||
Path parent = testFile.getParent();
|
||||
String pathString = parent.toAbsolutePath().toString();
|
||||
int javaIndex = pathString.lastIndexOf(JAVA_DIR);
|
||||
|
||||
String absolutePath = testFile.toAbsolutePath().toString();
|
||||
String packageAndClassName = absolutePath.substring(javaIndex + JAVA_DIR.length() + 1); // +1 for slash
|
||||
packageAndClassName = packageAndClassName.replaceAll("\\", ".");
|
||||
packageAndClassName = packageAndClassName.replaceAll("/", ".");
|
||||
String className = packageAndClassName.replace(".java", "");
|
||||
|
||||
debug("Loading class: " + className);
|
||||
try {
|
||||
return Class.forName(className);
|
||||
}
|
||||
catch (ClassNotFoundException e) {
|
||||
errorMessage("Couldn't load class: " + className, e);
|
||||
e.printStackTrace();
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void parseHelpDirectories(String helpModulePaths) {
|
||||
debug("parsing help directories...");
|
||||
|
||||
StringTokenizer tokenizer = new StringTokenizer(helpModulePaths, File.pathSeparator);
|
||||
while (tokenizer.hasMoreTokens()) {
|
||||
String helpFilePath = tokenizer.nextToken();
|
||||
File directoryFile = new File(helpFilePath);
|
||||
if (!directoryFile.exists()) {
|
||||
debug("Help directory does not exist: " + directoryFile);
|
||||
continue;
|
||||
}
|
||||
|
||||
HelpModuleLocation helpDir = new DirectoryHelpModuleLocation(directoryFile);
|
||||
helpDirectories.add(helpDir);
|
||||
|
||||
Collection<HelpTopic> topics = helpDir.getHelpTopics();
|
||||
for (HelpTopic topic : topics) {
|
||||
topicNameToTopic.put(topic.getName(), topic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void debug(String string) {
|
||||
if (debugEnabled) {
|
||||
System.err.println("[" + HelpMissingScreenShotReportGenerator.class.getSimpleName() +
|
||||
"] " + string);
|
||||
}
|
||||
}
|
||||
|
||||
private static void errorMessage(String message, Throwable t) {
|
||||
System.err.println("[" + GHelpBuilder.class.getSimpleName() + "] " + message);
|
||||
if (t != null) {
|
||||
t.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private void writeHeader(BufferedWriter writer) throws IOException {
|
||||
writer.write("<HTML>\n");
|
||||
writer.write("<HEAD>\n");
|
||||
createStyleSheet(writer);
|
||||
writer.write("</HEAD>\n");
|
||||
writer.write("<BODY>\n");
|
||||
writer.write("<H1>\n");
|
||||
writer.write("Ghidra Help Screen Shots Report");
|
||||
writer.write("</H1>\n");
|
||||
}
|
||||
|
||||
private void writeFooter(BufferedWriter writer) throws IOException {
|
||||
|
||||
writer.write("<BR>\n");
|
||||
writer.write("<BR>\n");
|
||||
|
||||
writer.write("</BODY>\n");
|
||||
writer.write("</HTML>\n");
|
||||
}
|
||||
|
||||
private void createStyleSheet(BufferedWriter writer) throws IOException {
|
||||
writer.write("<style>\n");
|
||||
writer.write("<!--\n");
|
||||
|
||||
writer.write("body { font-family:arial; font-size:22pt }\n");
|
||||
writer.write("h1 { color:#000080; font-family:times new roman; font-size:28pt; font-weight:bold; text-align:center; }\n");
|
||||
writer.write("h2 { color:#984c4c; font-family:times new roman; font-size:28pt; font-weight:bold; }\n");
|
||||
writer.write("h2.title { color:#000080; font-family:times new roman; font-size:14pt; font-weight:bold; text-align:center;}\n");
|
||||
writer.write("h3 { color:#0000ff; font-family:times new roman; font-size:14pt; font-weight:bold; margin-left:.5in }\n");
|
||||
writer.write("table { margin-left:1in; margin-right:1in; min-width:20em; width:90%; background-color:#EEEEFF }\n");
|
||||
writer.write("th { text-align:center; }\n");
|
||||
writer.write("td { text-align:left; padding: 20px }\n");
|
||||
|
||||
writer.write("-->\n");
|
||||
writer.write("</style>\n");
|
||||
}
|
||||
|
||||
//==================================================================================================
|
||||
// Inner Classes
|
||||
//==================================================================================================
|
||||
|
||||
private class HelpTestFile implements Comparable<HelpTestFile> {
|
||||
private String filename;
|
||||
private HelpModuleLocation helpDir;
|
||||
private Path filePath;
|
||||
private HelpTopic helpTopic;
|
||||
|
||||
HelpTestFile(HelpModuleLocation helpDir, HelpTopic helpTopic, Path filePath, String filename) {
|
||||
this.helpDir = helpDir;
|
||||
this.helpTopic = helpTopic;
|
||||
this.filePath = filePath;
|
||||
this.filename = filename;
|
||||
}
|
||||
|
||||
HelpTopic getHelpTopic() {
|
||||
return helpTopic;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(HelpTestFile o) {
|
||||
int result = helpDir.getHelpLocation().compareTo(o.helpDir.getHelpLocation());
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
return filename.compareTo(o.filename);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return helpDir.getHelpLocation().getFileName() + " -> " + filename;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class HelpTestCase implements Comparable<HelpTestCase> {
|
||||
|
||||
private HelpTestFile file;
|
||||
private String name;
|
||||
private String testMethodName;
|
||||
private String imageName;
|
||||
|
||||
HelpTestCase(HelpTestFile file, String name) {
|
||||
this.file = file;
|
||||
this.name = name;
|
||||
|
||||
if (!name.startsWith(TEST)) {
|
||||
throw new RuntimeException("Expecting test method name");
|
||||
}
|
||||
|
||||
testMethodName = name;
|
||||
|
||||
imageName = name.substring(TEST.length());
|
||||
|
||||
if (imageName.startsWith(CAPTURE)) {
|
||||
imageName = imageName.substring(CAPTURE.length());
|
||||
}
|
||||
|
||||
// TODO for now, we expect the case to match; should we change all images to start with an upper case?
|
||||
// imageName = name.substring(TEST.length() + 1);
|
||||
// imageName = Character.toLowerCase(name.charAt(TEST.length() + 1)) + imageName;
|
||||
imageName = imageName + PNG_EXT;
|
||||
}
|
||||
|
||||
boolean matches(String imgName) {
|
||||
if (imageName.equals(imgName)) {
|
||||
return true; // direct match!
|
||||
}
|
||||
|
||||
return imageName.toLowerCase().equals(imgName.toLowerCase());
|
||||
}
|
||||
|
||||
String getImageName() {
|
||||
return imageName;
|
||||
}
|
||||
|
||||
String getTestName() {
|
||||
return testMethodName;
|
||||
}
|
||||
|
||||
HelpTopic getHelpTopic() {
|
||||
return file.getHelpTopic();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(HelpTestCase o) {
|
||||
int result = file.filename.compareTo(o.file.filename);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
return name.compareTo(o.name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return file + " " + name + "()";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,198 @@
|
|||
/* ###
|
||||
* 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.screenshot;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
|
||||
public class HelpScreenShotReportGenerator {
|
||||
|
||||
//
|
||||
// TODO sort and group the output by module
|
||||
//
|
||||
|
||||
private static final int ITEMS_PER_PAGE = 25;
|
||||
private static final String PNG_EXT = ".png";
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
|
||||
if (args.length != 2) {
|
||||
throw new Exception(
|
||||
"Expecting 2 args: <output file path> <image filepath[,image filepath,...]>");
|
||||
}
|
||||
|
||||
String filePath = args[0];
|
||||
System.out.println("Using file path: " + filePath);
|
||||
|
||||
String images = args[1];
|
||||
if (images.trim().isEmpty()) {
|
||||
throw new Exception("No image files provided!");
|
||||
}
|
||||
|
||||
System.out.println("Processing image files: " + images);
|
||||
|
||||
StringTokenizer tokenizer = new StringTokenizer(images, ",");
|
||||
List<String> list = new ArrayList<String>();
|
||||
while (tokenizer.hasMoreTokens()) {
|
||||
list.add(tokenizer.nextToken());
|
||||
}
|
||||
|
||||
HelpScreenShotReportGenerator generator = new HelpScreenShotReportGenerator();
|
||||
generator.generateReport(filePath, list);
|
||||
}
|
||||
|
||||
private void generateReport(String filePath, List<String> list) throws Exception {
|
||||
|
||||
int filenameStartIndex = filePath.lastIndexOf(File.separator) + 1;
|
||||
String parentPath = filePath.substring(0, filenameStartIndex);
|
||||
new File(parentPath).mkdirs(); // make sure the folder exists
|
||||
|
||||
String baseFilename = filePath.substring(filenameStartIndex);
|
||||
|
||||
//
|
||||
// Make a report that is a series of pages with a table of side-by-side images
|
||||
//
|
||||
int n = list.size();
|
||||
int pageCount = n / ITEMS_PER_PAGE;
|
||||
if (n % ITEMS_PER_PAGE != 0) {
|
||||
pageCount++;
|
||||
}
|
||||
|
||||
String filenameNoExtension = baseFilename.substring(0, baseFilename.indexOf('.'));
|
||||
|
||||
for (int i = 0; i < pageCount; i++) {
|
||||
|
||||
BufferedWriter writer = null;
|
||||
try {
|
||||
String prefix = (i == 0) ? filenameNoExtension : filenameNoExtension + i;
|
||||
File file = new File(parentPath, prefix + ".html");
|
||||
System.out.println("Creating output file: " + file);
|
||||
|
||||
writer = new BufferedWriter(new FileWriter(file));
|
||||
writeFile(filenameNoExtension, writer, i, pageCount, list);
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw e;
|
||||
}
|
||||
finally {
|
||||
if (writer != null) {
|
||||
try {
|
||||
writer.close();
|
||||
}
|
||||
catch (IOException e) {
|
||||
// don't care
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void writeFile(String filenameNoExtension, BufferedWriter writer, int pageNumber,
|
||||
int pageCount, List<String> list) throws Exception {
|
||||
|
||||
writeHeader(writer);
|
||||
writer.write("<P>\n");
|
||||
writer.write("<TABLE BORDER=\"1\">\n");
|
||||
|
||||
int start = pageNumber * ITEMS_PER_PAGE;
|
||||
if (start > 0) {
|
||||
// 25 * 0 = 0
|
||||
// 25 * 1 = 25 => 26
|
||||
start++; // each page should start on the next item after the last end
|
||||
}
|
||||
|
||||
int n = Math.min(ITEMS_PER_PAGE, list.size() - start);
|
||||
int end = start + n;
|
||||
for (int i = start; i < end; i++) {
|
||||
|
||||
String newFilePath = list.get(i);
|
||||
int originalExtention = newFilePath.indexOf(PNG_EXT);
|
||||
int length = originalExtention + PNG_EXT.length();
|
||||
String oldFilePath = newFilePath.substring(0, length);
|
||||
|
||||
//@formatter:off
|
||||
writer.write(" <TR>\n");
|
||||
writer.write(" <TD>\n");
|
||||
writer.write(" <IMG SRC=\"" + oldFilePath + "\" ALT=\"" + oldFilePath + ".html\"><BR>\n");
|
||||
writer.write(" <CENTER><FONT COLOR=\"GRAY\">"+oldFilePath+"</FONT></CENTER>\n");
|
||||
writer.write(" </TD>\n");
|
||||
writer.write(" <TD>\n");
|
||||
writer.write(" <IMG SRC=\"" + newFilePath + "\" ALT=\"" + newFilePath + ".html\"><BR>\n");
|
||||
writer.write(" <CENTER><FONT COLOR=\"GRAY\">"+newFilePath+"</FONT></CENTER>\n");
|
||||
writer.write(" </TD>\n");
|
||||
writer.write(" </TR>\n");
|
||||
//@formatter:on
|
||||
}
|
||||
|
||||
writer.write("</TABLE>\n");
|
||||
writer.write("</P>");
|
||||
|
||||
writeFooter(filenameNoExtension, writer, pageCount);
|
||||
}
|
||||
|
||||
private void writeHeader(BufferedWriter writer) throws IOException {
|
||||
writer.write("<HTML>\n");
|
||||
writer.write("<HEAD>\n");
|
||||
createStyleSheet(writer);
|
||||
writer.write("</HEAD>\n");
|
||||
writer.write("<BODY>\n");
|
||||
writer.write("<H1>\n");
|
||||
writer.write("Ghidra Help Screen Shots");
|
||||
writer.write("</H1>\n");
|
||||
}
|
||||
|
||||
private void writeFooter(String filenameNoExtension, BufferedWriter writer, int pageCount)
|
||||
throws IOException {
|
||||
|
||||
writer.write("<BR>\n");
|
||||
writer.write("<BR>\n");
|
||||
writer.write("<P>\n");
|
||||
writer.write("<CENTER>\n");
|
||||
|
||||
for (int i = 0; i < pageCount; i++) {
|
||||
if (i == 0) {
|
||||
writer.write("<A HREF=\"" + filenameNoExtension + ".html\">" + (i + 1) + "</A>\n");
|
||||
}
|
||||
else {
|
||||
writer.write("<A HREF=\"" + filenameNoExtension + i + ".html\">" + (i + 1) +
|
||||
"</A>\n");
|
||||
}
|
||||
}
|
||||
|
||||
writer.write("</CENTER>\n");
|
||||
writer.write("</P>\n");
|
||||
|
||||
writer.write("</BODY>\n");
|
||||
writer.write("</HTML>\n");
|
||||
}
|
||||
|
||||
private void createStyleSheet(BufferedWriter writer) throws IOException {
|
||||
writer.write("<style>\n");
|
||||
writer.write("<!--\n");
|
||||
|
||||
writer.write("body { font-family:arial; font-size:22pt }\n");
|
||||
writer.write("h1 { color:#000080; font-family:times new roman; font-size:28pt; font-weight:bold; text-align:center; }\n");
|
||||
writer.write("h2 { color:#984c4c; font-family:times new roman; font-size:28pt; font-weight:bold; }\n");
|
||||
writer.write("h2.title { color:#000080; font-family:times new roman; font-size:14pt; font-weight:bold; text-align:center;}\n");
|
||||
writer.write("h3 { color:#0000ff; font-family:times new roman; font-size:14pt; font-weight:bold; margin-left:.5in }\n");
|
||||
writer.write("table { margin-left:1in; min-width:20em; width:95%; background-color:#EEEEFF }\n");
|
||||
writer.write("th { text-align:center; }\n");
|
||||
writer.write("td { text-align:center; padding: 20px }\n");
|
||||
|
||||
writer.write("-->\n");
|
||||
writer.write("</style>\n");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
/* ###
|
||||
* 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.validator;
|
||||
|
||||
import help.validator.model.*;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
|
||||
public class AnchorManager {
|
||||
|
||||
private Map<String, AnchorDefinition> anchorsByHelpPath =
|
||||
new HashMap<String, AnchorDefinition>();
|
||||
private Map<String, AnchorDefinition> anchorsById = new HashMap<String, AnchorDefinition>();
|
||||
private Map<String, AnchorDefinition> anchorsByName = new HashMap<String, AnchorDefinition>();
|
||||
private Map<String, List<AnchorDefinition>> duplicateAnchorsById =
|
||||
new HashMap<String, List<AnchorDefinition>>();
|
||||
|
||||
private List<HREF> anchorRefs = new ArrayList<HREF>();
|
||||
private List<IMG> imgRefs = new ArrayList<IMG>();
|
||||
|
||||
public AnchorManager() {
|
||||
}
|
||||
|
||||
public void addAnchor(Path file, String anchorName, int srcLineNo) {
|
||||
AnchorDefinition anchor = new AnchorDefinition(file, anchorName, srcLineNo);
|
||||
|
||||
String id = anchor.getId();
|
||||
if (anchorsById.containsKey(id)) {
|
||||
addDuplicateAnchor(anchorName, anchor, id);
|
||||
return;
|
||||
}
|
||||
|
||||
anchorsById.put(id, anchor);
|
||||
anchorsByHelpPath.put(anchor.getHelpPath(), anchor);
|
||||
|
||||
if (anchorName != null) {
|
||||
anchorsByName.put(anchorName, anchor);
|
||||
}
|
||||
}
|
||||
|
||||
private void addDuplicateAnchor(String anchorName, AnchorDefinition anchor, String id) {
|
||||
List<AnchorDefinition> list = duplicateAnchorsById.get(id);
|
||||
if (list == null) {
|
||||
list = new ArrayList<AnchorDefinition>();
|
||||
list.add(anchorsById.get(id)); // put in the original definition
|
||||
duplicateAnchorsById.put(id, list);
|
||||
}
|
||||
|
||||
list.add(anchor); // add the newly found definition
|
||||
|
||||
//
|
||||
// special code: make sure at least one of these duplicates makes it into the map
|
||||
//
|
||||
if (anchorName == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!anchorsByName.containsKey(anchorName)) {
|
||||
anchorsByName.put(anchorName, anchor);
|
||||
anchorsByHelpPath.put(anchor.getHelpPath(), anchor);
|
||||
}
|
||||
}
|
||||
|
||||
public Map<String, AnchorDefinition> getAnchorsByHelpPath() {
|
||||
return anchorsByHelpPath;
|
||||
}
|
||||
|
||||
public AnchorDefinition getAnchorForHelpPath(String path) {
|
||||
if (path == null) {
|
||||
return null;
|
||||
}
|
||||
return anchorsByHelpPath.get(path);
|
||||
}
|
||||
|
||||
public void addAnchorRef(HREF href) {
|
||||
anchorRefs.add(href);
|
||||
}
|
||||
|
||||
public void addImageRef(IMG ref) {
|
||||
imgRefs.add(ref);
|
||||
}
|
||||
|
||||
public List<HREF> getAnchorRefs() {
|
||||
return anchorRefs;
|
||||
}
|
||||
|
||||
public List<IMG> getImageRefs() {
|
||||
return imgRefs;
|
||||
}
|
||||
|
||||
public AnchorDefinition getAnchorForName(String anchorName) {
|
||||
return anchorsByName.get(anchorName);
|
||||
}
|
||||
|
||||
public Map<String, List<AnchorDefinition>> getDuplicateAnchorsByID() {
|
||||
cleanupDuplicateAnchors();
|
||||
return duplicateAnchorsById;
|
||||
}
|
||||
|
||||
private void cleanupDuplicateAnchors() {
|
||||
Set<String> keySet = duplicateAnchorsById.keySet();
|
||||
for (String id : keySet) {
|
||||
List<AnchorDefinition> list = duplicateAnchorsById.get(id);
|
||||
for (Iterator<AnchorDefinition> iterator = list.iterator(); iterator.hasNext();) {
|
||||
AnchorDefinition anchorDefinition = iterator.next();
|
||||
if (anchorDefinition.getLineNumber() < 0) {
|
||||
// a line number of < 0 indicates an AnchorDefinition, which is not found in a file
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// if there is only one item left in the list after removing the definitions, then
|
||||
// there are not really any duplicate definitions, so cleanup the list
|
||||
if (list.size() == 1) {
|
||||
list.clear();
|
||||
duplicateAnchorsById.remove(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
/* ###
|
||||
* 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.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package help.validator;
|
||||
|
||||
public interface DuplicateAnchorCollection {
|
||||
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
/* ###
|
||||
* 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.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package help.validator;
|
||||
|
||||
import help.validator.model.AnchorDefinition;
|
||||
import help.validator.model.HelpFile;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
public class DuplicateAnchorCollectionByHelpFile implements DuplicateAnchorCollection,
|
||||
Comparable<DuplicateAnchorCollectionByHelpFile> {
|
||||
|
||||
private final HelpFile helpFile;
|
||||
private final Map<String, List<AnchorDefinition>> duplicateAnchors;
|
||||
|
||||
DuplicateAnchorCollectionByHelpFile(HelpFile helpFile,
|
||||
Map<String, List<AnchorDefinition>> duplicateAnchors) {
|
||||
this.helpFile = helpFile;
|
||||
this.duplicateAnchors = duplicateAnchors;
|
||||
}
|
||||
|
||||
public HelpFile getHelpFile() {
|
||||
return helpFile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Duplicate anchors for file\n\tfile: " + helpFile + "\n\tanchor: " +
|
||||
getAnchorsAsString();
|
||||
}
|
||||
|
||||
private String getAnchorsAsString() {
|
||||
StringBuilder buildy = new StringBuilder();
|
||||
for (Entry<String, List<AnchorDefinition>> entry : duplicateAnchors.entrySet()) {
|
||||
buildy.append("Generated ID: ").append(entry.getKey()).append('\n');
|
||||
List<AnchorDefinition> list = entry.getValue();
|
||||
for (AnchorDefinition anchorDefinition : list) {
|
||||
buildy.append('\t').append('\t').append(anchorDefinition).append('\n');
|
||||
}
|
||||
}
|
||||
return buildy.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(DuplicateAnchorCollectionByHelpFile o) {
|
||||
HelpFile helpFile1 = getHelpFile();
|
||||
HelpFile helpFile2 = o.getHelpFile();
|
||||
Path file1 = helpFile1.getFile();
|
||||
Path file2 = helpFile2.getFile();
|
||||
return file1.compareTo(file2);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
/* ###
|
||||
* 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.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package help.validator;
|
||||
|
||||
import help.validator.model.AnchorDefinition;
|
||||
import help.validator.model.HelpTopic;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
public class DuplicateAnchorCollectionByHelpTopic implements DuplicateAnchorCollection,
|
||||
Comparable<DuplicateAnchorCollectionByHelpTopic> {
|
||||
|
||||
private final HelpTopic topic;
|
||||
private final List<AnchorDefinition> definitions;
|
||||
|
||||
DuplicateAnchorCollectionByHelpTopic(HelpTopic topic, List<AnchorDefinition> definitions) {
|
||||
this.topic = topic;
|
||||
this.definitions = definitions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Duplicate anchors for topic\n\ttopic file: " + topic.getTopicFile() + "\n" +
|
||||
getAnchorsAsString();
|
||||
}
|
||||
|
||||
private String getAnchorsAsString() {
|
||||
StringBuilder buildy = new StringBuilder();
|
||||
for (AnchorDefinition definition : definitions) {
|
||||
buildy.append('\t').append('\t').append(definition).append('\n');
|
||||
}
|
||||
return buildy.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(DuplicateAnchorCollectionByHelpTopic o) {
|
||||
Path topicFile1 = topic.getTopicFile();
|
||||
Path topicFile2 = o.topic.getTopicFile();
|
||||
return topicFile1.compareTo(topicFile2);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,377 @@
|
|||
/* ###
|
||||
* 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.validator;
|
||||
|
||||
import static help.validator.TagProcessor.TagProcessingState.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
|
||||
import help.validator.TagProcessor.TagProcessingState;
|
||||
|
||||
public class HTMLFileParser {
|
||||
|
||||
private static final String COMMENT_END_TAG = "-->";
|
||||
private static final String COMMENT_START_TAG = "!--";
|
||||
|
||||
public static void scanHtmlFile(Path file, TagProcessor tagProcessor) throws IOException {
|
||||
InputStreamReader isr = new InputStreamReader(Files.newInputStream(file));
|
||||
try (LineNumberReader rdr = new LineNumberReader(isr)) {
|
||||
|
||||
tagProcessor.startOfFile(file);
|
||||
String text;
|
||||
while ((text = rdr.readLine()) != null) {
|
||||
Line line = new Line(file, text, rdr.getLineNumber());
|
||||
processLine(line, rdr, file, tagProcessor);
|
||||
}
|
||||
tagProcessor.endOfFile();
|
||||
}
|
||||
}
|
||||
|
||||
private static void processLine(Line line, LineNumberReader rdr, Path file,
|
||||
TagProcessor tagProcessor) throws IOException {
|
||||
|
||||
if (line == null) {
|
||||
// this can happen if we call ourselves recursively
|
||||
return;
|
||||
}
|
||||
|
||||
int tagStartIndex = -1;
|
||||
int tagNameEndIndex = -1;
|
||||
String tagType = null;
|
||||
for (int i = 0; i < line.length(); i++) {
|
||||
char c = line.charAt(i);
|
||||
if (c == '<') {
|
||||
|
||||
boolean isComment = line.regionMatches(i + 1, COMMENT_START_TAG, 0, 3);
|
||||
if (isComment) {
|
||||
int start = i + COMMENT_START_TAG.length() + 1;
|
||||
TagBlock commentBlock = skipPastCommentEnd(rdr, line, start);
|
||||
Line next = commentBlock.remainingText;
|
||||
|
||||
//System.out.println("comment: " + commentBlock.tagContent + "\n\t" +
|
||||
// "from file: " + file.getFileName());
|
||||
|
||||
// finish any remaining text on the line
|
||||
processLine(next, rdr, file, tagProcessor);
|
||||
return;
|
||||
}
|
||||
|
||||
tagStartIndex = i;
|
||||
ScanResult result = getTagName(line, i + 1);
|
||||
tagType = result.text;
|
||||
if (tagProcessor.isTagSupported(tagType)) {
|
||||
tagNameEndIndex = i + tagType.length() + 1;
|
||||
break;
|
||||
}
|
||||
|
||||
tagStartIndex = -1; // reset
|
||||
i = result.lastPosition; // keep looking
|
||||
}
|
||||
}
|
||||
|
||||
// now, finish processing the text on the line, either: the rest of the tag we found,
|
||||
// or the rest of the line when no tag
|
||||
if (tagStartIndex < 0) {
|
||||
// no tag found
|
||||
tagProcessor.processText(line.text);
|
||||
return;
|
||||
}
|
||||
|
||||
Line precedingText = line.substring(0, tagStartIndex);
|
||||
Line remainingText = line.substring(tagNameEndIndex);
|
||||
tagProcessor.processText(precedingText.text);
|
||||
|
||||
TagBlock tagBlock = getTagBody(rdr, remainingText);
|
||||
|
||||
String tagBody = tagBlock.tagContent;
|
||||
Line postTagText = tagBlock.remainingText;
|
||||
int lineNum = rdr.getLineNumber();
|
||||
processTag(tagType, tagBody, file, lineNum, tagProcessor);
|
||||
|
||||
processLine(postTagText, rdr, file, tagProcessor);
|
||||
}
|
||||
|
||||
private static TagBlock getTagBody(LineNumberReader rdr, Line line) throws IOException {
|
||||
|
||||
String tagBody = "";
|
||||
int tagEnd = -1;
|
||||
while ((tagEnd = line.indexOf('>')) < 0) {
|
||||
tagBody += line.text + " ";
|
||||
String nextLineText = rdr.readLine();
|
||||
if (nextLineText == null) {
|
||||
line = null;
|
||||
break;
|
||||
}
|
||||
|
||||
line = new Line(line.file, nextLineText, line.lineNumber);
|
||||
}
|
||||
|
||||
if (line != null) {
|
||||
tagBody += line.substring(0, tagEnd).text;
|
||||
line = line.substring(tagEnd + 1);
|
||||
}
|
||||
|
||||
TagBlock tag = new TagBlock(line, tagBody);
|
||||
return tag;
|
||||
}
|
||||
|
||||
private static TagBlock skipPastCommentEnd(LineNumberReader rdr, Line line, int start)
|
||||
throws IOException {
|
||||
|
||||
line = line.substring(start);
|
||||
|
||||
String comment = "";
|
||||
while (!line.contains(COMMENT_END_TAG)) {
|
||||
comment += line.text + '\n';
|
||||
String text = rdr.readLine();
|
||||
line = new Line(line.file, text, rdr.getLineNumber());
|
||||
}
|
||||
|
||||
int index = line.indexOf(COMMENT_END_TAG, 0);
|
||||
if (index >= 0) {
|
||||
// update the line to move past the comment closing tag
|
||||
comment += line.substring(0, index).text;
|
||||
line = line.substring(index + COMMENT_END_TAG.length());
|
||||
}
|
||||
|
||||
TagBlock tag = new TagBlock(line, comment);
|
||||
return tag;
|
||||
}
|
||||
|
||||
private static ScanResult getTagName(Line line, int index) throws IOException {
|
||||
|
||||
int end = index;
|
||||
for (int i = index; i < line.length(); i++, end++) {
|
||||
char c = line.charAt(i);
|
||||
if (c == '<') {
|
||||
throw new IOException("Bad tag on line " + line.lineNumber + ": " + line.file);
|
||||
}
|
||||
if (c == ' ' || c == '\t' || c == '>') {
|
||||
return new ScanResult(line.text.substring(index, i), i);
|
||||
}
|
||||
}
|
||||
|
||||
if (end > index) {
|
||||
return new ScanResult(line.text.substring(index, end), end);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String processTag(String tagType, String tagBody, Path file, int lineNum,
|
||||
TagProcessor tagProcessor) throws IOException {
|
||||
|
||||
if (tagBody.indexOf('<') >= 0 || tagBody.indexOf('>') >= 0) {
|
||||
throw new IOException("Bad Tag at line " + lineNum);
|
||||
}
|
||||
|
||||
LinkedHashMap<String, String> map = new LinkedHashMap<>();
|
||||
StringBuffer buf = new StringBuffer();
|
||||
String attr = null;
|
||||
TagProcessingState mode = LOOKING_FOR_NEXT_ATTR;
|
||||
char term = 0;
|
||||
|
||||
int end = tagBody.length();
|
||||
for (int ix = 0; ix < end; ix++) {
|
||||
char c = tagBody.charAt(ix);
|
||||
|
||||
switch (mode) {
|
||||
|
||||
case READING_ATTR:
|
||||
if (c == '=') {
|
||||
attr = buf.toString().toLowerCase();
|
||||
mode = LOOKING_FOR_VALUE;
|
||||
break;
|
||||
}
|
||||
if (c == ' ' || c == '\t') {
|
||||
attr = buf.toString().toLowerCase();
|
||||
map.put(attr, null);
|
||||
mode = LOOKING_FOR_NEXT_ATTR;
|
||||
break;
|
||||
}
|
||||
buf.append(c);
|
||||
break;
|
||||
|
||||
case LOOKING_FOR_VALUE:
|
||||
if (c == ' ' || c == '\t') {
|
||||
// we now allow spaces after the '=', but before the '"' starts, as our
|
||||
// tidy tool breaks on the '=' sometimes
|
||||
//map.put(attr, null);
|
||||
//mode = LOOKING_FOR_NEXT_ATTR;
|
||||
break;
|
||||
}
|
||||
if (c == '"' || c == '\'') {
|
||||
buf = new StringBuffer();
|
||||
mode = READING_VALUE;
|
||||
term = c;
|
||||
break;
|
||||
}
|
||||
buf = new StringBuffer();
|
||||
buf.append(c);
|
||||
mode = READING_VALUE;
|
||||
term = 0;
|
||||
break;
|
||||
|
||||
case READING_VALUE:
|
||||
if (c == term || (term == 0 && (c == ' ' || c == '\t'))) {
|
||||
map.put(attr, buf.toString());
|
||||
mode = LOOKING_FOR_NEXT_ATTR;
|
||||
break;
|
||||
}
|
||||
buf.append(c);
|
||||
break;
|
||||
|
||||
default:
|
||||
if (c == ' ' || c == '\t') {
|
||||
continue;
|
||||
}
|
||||
buf = new StringBuffer();
|
||||
buf.append(c);
|
||||
mode = READING_ATTR;
|
||||
}
|
||||
}
|
||||
|
||||
if (mode == READING_ATTR) {
|
||||
map.put(buf.toString().toLowerCase(), null);
|
||||
}
|
||||
else if (mode == LOOKING_FOR_VALUE) {
|
||||
map.put(attr, null);
|
||||
}
|
||||
else if (mode == READING_VALUE) {
|
||||
map.put(attr, buf.toString());
|
||||
}
|
||||
|
||||
tagProcessor.processTag(tagType, map, file, lineNum);
|
||||
|
||||
buf = new StringBuffer();
|
||||
buf.append('<');
|
||||
buf.append(tagType);
|
||||
Iterator<String> iter = map.keySet().iterator();
|
||||
while (iter.hasNext()) {
|
||||
attr = iter.next();
|
||||
String value = map.get(attr);
|
||||
buf.append(' ');
|
||||
buf.append(attr);
|
||||
if (value != null) {
|
||||
buf.append("=\"");
|
||||
buf.append(value);
|
||||
buf.append("\"");
|
||||
}
|
||||
}
|
||||
buf.append('>');
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
private static class Line {
|
||||
private Path file;
|
||||
private String text;
|
||||
private int lineNumber;
|
||||
|
||||
Line(Path file, String text, int lineNumber) {
|
||||
this.file = file;
|
||||
this.text = text;
|
||||
this.lineNumber = lineNumber;
|
||||
}
|
||||
|
||||
int indexOf(char c) {
|
||||
return text.indexOf(c);
|
||||
}
|
||||
|
||||
boolean contains(String s) {
|
||||
return text.contains(s);
|
||||
}
|
||||
|
||||
Line substring(int from) {
|
||||
return new Line(file, text.substring(from), lineNumber);
|
||||
}
|
||||
|
||||
Line substring(int from, int exclusiveEnd) {
|
||||
return new Line(file, text.substring(from, exclusiveEnd), lineNumber);
|
||||
}
|
||||
|
||||
int indexOf(String s, int from) {
|
||||
return text.indexOf(s, from);
|
||||
}
|
||||
|
||||
boolean regionMatches(int from, String s, int ooffset, int len) {
|
||||
return text.regionMatches(from, s, ooffset, len);
|
||||
}
|
||||
|
||||
char charAt(int i) {
|
||||
return text.charAt(i);
|
||||
}
|
||||
|
||||
int length() {
|
||||
return text.length();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
//@formatter:off
|
||||
return "{\n" +
|
||||
"\tfile: " + file.getFileName() + ",\n" +
|
||||
"\tline_number: " + lineNumber + ",\n" +
|
||||
"\ttext: " + text + "\n" +
|
||||
"}";
|
||||
//@formatter:on
|
||||
}
|
||||
}
|
||||
|
||||
// container to hold the result text of a search, as well as the last index checked
|
||||
private static class ScanResult {
|
||||
private String text;
|
||||
private int lastPosition;
|
||||
|
||||
ScanResult(String text, int lastPosition) {
|
||||
this.text = text;
|
||||
this.lastPosition = lastPosition;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
//@formatter:off
|
||||
return "{\n" +
|
||||
"\ttext: " + text + ",\n" +
|
||||
"\tlast_position: " + lastPosition + "\n" +
|
||||
"}";
|
||||
//@formatter:on
|
||||
}
|
||||
}
|
||||
|
||||
private static class TagBlock {
|
||||
|
||||
private Line remainingText;
|
||||
private String tagContent;
|
||||
|
||||
TagBlock(Line remainingText, String tagContent) {
|
||||
this.remainingText = remainingText;
|
||||
this.tagContent = tagContent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
//@formatter:off
|
||||
return "{\n" +
|
||||
"\tcontent: " + tagContent + ",\n" +
|
||||
"\tpost_text: " + remainingText + "\n" +
|
||||
"}";
|
||||
//@formatter:on
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,409 @@
|
|||
/* ###
|
||||
* 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.validator;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import generic.jar.ResourceFile;
|
||||
import ghidra.framework.Application;
|
||||
import help.HelpBuildUtils;
|
||||
import help.validator.links.*;
|
||||
import help.validator.location.HelpModuleCollection;
|
||||
import help.validator.model.*;
|
||||
|
||||
public class JavaHelpValidator {
|
||||
private static boolean debug;
|
||||
|
||||
/** Files that are generated and may not exist at validation time */
|
||||
private static Set<String> EXCLUDED_FILE_NAMES = createExcludedFileSet();
|
||||
|
||||
private static Set<String> createExcludedFileSet() {
|
||||
Set<String> set = new HashSet<>();
|
||||
|
||||
// The expected format is the help path, without an extension (this helps catch multiple
|
||||
// references with anchors)
|
||||
set.add("help/topics/Misc/Tips");
|
||||
set.add("docs/WhatsNew");
|
||||
set.add("docs/README_PDB");
|
||||
|
||||
return set;
|
||||
}
|
||||
|
||||
private String moduleName;
|
||||
private HelpModuleCollection help;
|
||||
|
||||
public JavaHelpValidator(String moduleName, HelpModuleCollection help) {
|
||||
this.moduleName = moduleName;
|
||||
this.help = help;
|
||||
}
|
||||
|
||||
private void validateInternalFileLinks(LinkDatabase linkDatabase) {
|
||||
validateHelpDirectoryInternalLinks(help, linkDatabase);
|
||||
}
|
||||
|
||||
private void validateHelpDirectoryInternalLinks(HelpModuleCollection helpCollection,
|
||||
LinkDatabase linkDatabase) {
|
||||
|
||||
debug("validating internal help links for module: " + helpCollection);
|
||||
|
||||
// resolve all links that can be found (any unresolved links are either external or bad)
|
||||
// Link resolution issues:
|
||||
// -Can't find file
|
||||
// -Found file, can't find internal anchor
|
||||
List<InvalidLink> unresolvedLinks = new ArrayList<>();
|
||||
Collection<HREF> helpDirHREFs = helpCollection.getAllHREFs();
|
||||
debug("\tHREF count: " + helpDirHREFs.size());
|
||||
for (HREF href : helpDirHREFs) {
|
||||
if (href.isRemote()) {
|
||||
continue; // don't try to validate remote refs--let them go through as-is
|
||||
}
|
||||
Path referenceFileHelpPath = href.getReferenceFileHelpPath();
|
||||
HelpFile helpFile = helpCollection.getHelpFile(referenceFileHelpPath);
|
||||
validateHREFHelpFile(href, helpFile, unresolvedLinks);
|
||||
}
|
||||
|
||||
//
|
||||
// now resolve all image links
|
||||
//
|
||||
Collection<IMG> helpDirIMGs = helpCollection.getAllIMGs();
|
||||
debug("\tIMG count: " + helpDirIMGs.size());
|
||||
for (IMG img : helpDirIMGs) {
|
||||
validateIMGFile(img, unresolvedLinks);
|
||||
}
|
||||
|
||||
linkDatabase.addUnresolvedLinks(unresolvedLinks);
|
||||
|
||||
//
|
||||
// check for duplicate anchor references
|
||||
//
|
||||
Map<HelpFile, Map<String, List<AnchorDefinition>>> duplicateAnchors =
|
||||
helpCollection.getDuplicateAnchorsByFile();
|
||||
debug("\tHelp files with duplicate anchors: " + duplicateAnchors.size());
|
||||
for (Entry<HelpFile, Map<String, List<AnchorDefinition>>> entry : duplicateAnchors.entrySet()) {
|
||||
HelpFile helpFile = entry.getKey();
|
||||
Map<String, List<AnchorDefinition>> list = entry.getValue();
|
||||
linkDatabase.addDuplicateAnchors(
|
||||
new DuplicateAnchorCollectionByHelpFile(helpFile, list));
|
||||
}
|
||||
|
||||
Map<HelpTopic, List<AnchorDefinition>> duplicateAnchorsByTopic =
|
||||
helpCollection.getDuplicateAnchorsByTopic();
|
||||
debug("\tHelp topics with duplicate anchors: " + duplicateAnchorsByTopic.size());
|
||||
Set<Entry<HelpTopic, List<AnchorDefinition>>> entrySet = duplicateAnchorsByTopic.entrySet();
|
||||
for (Entry<HelpTopic, List<AnchorDefinition>> entry : entrySet) {
|
||||
linkDatabase.addDuplicateAnchors(
|
||||
new DuplicateAnchorCollectionByHelpTopic(entry.getKey(), entry.getValue()));
|
||||
}
|
||||
}
|
||||
|
||||
private void validateIMGFile(IMG img, List<InvalidLink> unresolvedLinks) {
|
||||
//
|
||||
// Try to resolve the given image link
|
||||
//
|
||||
if (img.isRemote()) {
|
||||
return; // don't even try to verify a remote URL
|
||||
}
|
||||
|
||||
Path imagePath = img.getImageFile();
|
||||
if (imagePath == null) {
|
||||
unresolvedLinks.add(new NonExistentIMGFileInvalidLink(img));
|
||||
return;
|
||||
}
|
||||
|
||||
if (img.isRuntime()) {
|
||||
// the tool will load this image at runtime--don't perform normal validate
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Look first in the help system, then in the modules' resources
|
||||
//
|
||||
Path testPath = findPathInHelp(img);
|
||||
if (testPath == null) {
|
||||
// not in a help dir; perhaps the image lives in module's resource dir?
|
||||
testPath = findPathInModules(img);
|
||||
}
|
||||
|
||||
if (testPath == null) {
|
||||
unresolvedLinks.add(new NonExistentIMGFileInvalidLink(img));
|
||||
return;
|
||||
}
|
||||
|
||||
// O.K., file exists, but is the case correct
|
||||
if (!caseMatches(img, testPath)) {
|
||||
unresolvedLinks.add(new IncorrectIMGFilenameCaseInvalidLink(img));
|
||||
}
|
||||
}
|
||||
|
||||
private Path findPathInHelp(IMG img) {
|
||||
|
||||
Path imagePath = img.getImageFile();
|
||||
for (Path helpDir : help.getHelpRoots()) {
|
||||
Path toCheck = makePath(helpDir, imagePath);
|
||||
if (toCheck != null) {
|
||||
return toCheck;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Path findPathInModules(IMG img) {
|
||||
|
||||
String rawSrc = img.getSrcAttribute();
|
||||
Collection<ResourceFile> moduleRoots = Application.getModuleRootDirectories();
|
||||
for (ResourceFile root : moduleRoots) {
|
||||
ResourceFile resourceDir = new ResourceFile(root, "src/main/resources");
|
||||
Path toCheck = makePath(resourceDir, rawSrc);
|
||||
if (toCheck != null) {
|
||||
return toCheck;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Path makePath(ResourceFile dir, String imgSrc) {
|
||||
|
||||
if (!dir.exists()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Path dirPath = Paths.get(dir.getAbsolutePath());
|
||||
Path imagePath = Paths.get(imgSrc);
|
||||
|
||||
Path imageFileFS = HelpBuildUtils.toFS(dirPath, imagePath);
|
||||
Path toCheck = dirPath.resolve(imageFileFS);
|
||||
if (Files.exists(toCheck)) {
|
||||
return toCheck;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Path makePath(Path helpDir, Path imagePath) {
|
||||
|
||||
Path imageFileFS = HelpBuildUtils.toFS(helpDir, imagePath);
|
||||
imageFileFS = removeRedundantHelp(helpDir, imageFileFS);
|
||||
Path toCheck = helpDir.resolve(imageFileFS);
|
||||
if (Files.exists(toCheck)) {
|
||||
return toCheck;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean caseMatches(IMG img, Path path) {
|
||||
|
||||
// validate case (some platforms are case-sensitive)
|
||||
Path realPath;
|
||||
try {
|
||||
realPath = path.toRealPath(); // gets the actual filesystem name
|
||||
}
|
||||
catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String realFilename = realPath.getFileName().toString();
|
||||
Path imagePath = img.getImageFile();
|
||||
String imageFilename = imagePath.getFileName().toString();
|
||||
|
||||
if (realFilename.equals(imageFilename)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private Path removeRedundantHelp(Path root, Path p) {
|
||||
if (p.startsWith("help")) {
|
||||
// this is the 'help system syntax'; may need to chop off 'help'
|
||||
if (root.endsWith("help")) {
|
||||
p = p.subpath(1, p.getNameCount());
|
||||
}
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
private void validateHREFHelpFile(HREF href, HelpFile helpFile,
|
||||
List<InvalidLink> unresolvedLinks) {
|
||||
|
||||
if (helpFile == null) {
|
||||
if (isExcludedHREF(href)) {
|
||||
return; // ignore calls made to the the API as being invalid
|
||||
}
|
||||
unresolvedLinks.add(new MissingFileInvalidLink(href));
|
||||
return;
|
||||
}
|
||||
|
||||
// we have found a help file, make sure the anchor is there
|
||||
String anchorName = href.getAnchorName();
|
||||
if (anchorName == null) {
|
||||
return; // no anchor to validate
|
||||
}
|
||||
if (!helpFile.containsAnchor(anchorName)) {
|
||||
unresolvedLinks.add(new MissingAnchorInvalidLink(href));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isExcludedHREF(HREF href) {
|
||||
|
||||
String path = href.getRefString();
|
||||
return isExcludedPath(path);
|
||||
}
|
||||
|
||||
private boolean isExcludedPath(String path) {
|
||||
if (path.indexOf("/docs/api/") != -1) {
|
||||
// exclude all api files
|
||||
return true;
|
||||
}
|
||||
|
||||
// strip off the extension
|
||||
int index = path.lastIndexOf(".");
|
||||
if (index != -1) {
|
||||
path = path.substring(0, index);
|
||||
}
|
||||
|
||||
return EXCLUDED_FILE_NAMES.contains(path);
|
||||
}
|
||||
|
||||
private void validateExternalFileLinks(LinkDatabase linkDatabase) {
|
||||
|
||||
Collection<InvalidLink> unresolvedLinks = linkDatabase.getUnresolvedLinks();
|
||||
debug("validating " + unresolvedLinks.size() + " unresolved external links");
|
||||
|
||||
// Link resolution issues:
|
||||
// -Can't find file
|
||||
// -Found file, can't find internal anchor
|
||||
// -Found file (and anchor if present), but module is an illegal dependency
|
||||
|
||||
Set<InvalidLink> remainingInvalidLinks = new TreeSet<>();
|
||||
for (Iterator<InvalidLink> iterator = unresolvedLinks.iterator(); iterator.hasNext();) {
|
||||
InvalidLink link = iterator.next();
|
||||
if (!(link instanceof InvalidHREFLink)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
InvalidHREFLink invalidHREFLink = (InvalidHREFLink) link;
|
||||
if (invalidHREFLink instanceof MissingAnchorInvalidLink) {
|
||||
remainingInvalidLinks.add(link);
|
||||
continue;
|
||||
}
|
||||
|
||||
HelpFile referencedHelpFile = linkDatabase.resolveLink(link);
|
||||
if (referencedHelpFile != null) {
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
|
||||
linkDatabase.addUnresolvedLinks(remainingInvalidLinks);
|
||||
}
|
||||
|
||||
private void validateExternalImageFileLinks(LinkDatabase linkDatabase) {
|
||||
Collection<InvalidLink> unresolvedLinks = linkDatabase.getUnresolvedLinks();
|
||||
debug("validating " + unresolvedLinks.size() + " unresolved external image links");
|
||||
|
||||
// Link resolution issues:
|
||||
// -Can't find file
|
||||
// -Found file, but module is an illegal dependency
|
||||
Set<InvalidLink> remainingInvalidLinks = new TreeSet<>();
|
||||
for (InvalidLink link : unresolvedLinks) {
|
||||
if (link instanceof NonExistentIMGFileInvalidLink) {
|
||||
remainingInvalidLinks.add(link);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
linkDatabase.addUnresolvedLinks(remainingInvalidLinks);
|
||||
}
|
||||
|
||||
private void validateTOCItemIDs(LinkDatabase linkDatabase) {
|
||||
debug("Validating TOC item IDs...");
|
||||
List<InvalidLink> unresolvedLinks = new ArrayList<>();
|
||||
|
||||
Collection<TOCItem> items = help.getInputTOCItems();
|
||||
|
||||
debug("\tvalidating " + items.size() + " TOC item references for module: " + moduleName);
|
||||
for (TOCItem item : items) {
|
||||
if (!item.validate(linkDatabase)) {
|
||||
if (item instanceof TOCItemReference) {
|
||||
TOCItemReference reference = (TOCItemReference) item;
|
||||
unresolvedLinks.add(new MissingTOCDefinitionInvalidLink(help, reference));
|
||||
}
|
||||
else {
|
||||
String targetPath = item.getTargetAttribute();
|
||||
if (!isExcludedPath(targetPath)) {
|
||||
unresolvedLinks.add(new MissingTOCTargetIDInvalidLink(help, item));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Note: we have to validate the target links of the TOC file here, *after* we
|
||||
// validate the links, as until then, references aren't resolved
|
||||
//
|
||||
Collection<HREF> TOC_HREFs = help.getTOC_HREFs();
|
||||
debug("\tvalidating TOC links: " + TOC_HREFs.size());
|
||||
for (HREF href : TOC_HREFs) {
|
||||
Path referenceFileHelpPath = href.getReferenceFileHelpPath();
|
||||
HelpFile helpFile = linkDatabase.resolveFile(referenceFileHelpPath);
|
||||
validateHREFHelpFile(href, helpFile, unresolvedLinks);
|
||||
}
|
||||
|
||||
linkDatabase.addUnresolvedLinks(unresolvedLinks);
|
||||
|
||||
debug("\tfinished validating TOC item IDs...");
|
||||
}
|
||||
|
||||
public Collection<InvalidLink> validate(LinkDatabase linkDatabase) {
|
||||
// validate internal links for each help file
|
||||
validateInternalFileLinks(linkDatabase);
|
||||
|
||||
// validate external links
|
||||
validateExternalFileLinks(linkDatabase);
|
||||
validateExternalImageFileLinks(linkDatabase);
|
||||
|
||||
validateTOCItemIDs(linkDatabase);
|
||||
|
||||
return linkDatabase.getUnresolvedLinks();
|
||||
}
|
||||
|
||||
//==================================================================================================
|
||||
// Static Methods
|
||||
//==================================================================================================
|
||||
|
||||
private static void debug(String message) {
|
||||
if (debug) {
|
||||
flush();
|
||||
System.out.println("[" + JavaHelpValidator.class.getSimpleName() + "] " + message);
|
||||
}
|
||||
}
|
||||
|
||||
private static void flush() {
|
||||
System.out.flush();
|
||||
System.out.println();
|
||||
System.out.flush();
|
||||
System.err.flush();
|
||||
System.err.println();
|
||||
System.err.flush();
|
||||
}
|
||||
|
||||
public void setDebugEnabled(boolean debug) {
|
||||
JavaHelpValidator.debug = debug;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,202 @@
|
|||
/* ###
|
||||
* 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.validator;
|
||||
|
||||
import help.OverlayHelpTree;
|
||||
import help.TOCItemProvider;
|
||||
import help.validator.links.InvalidHREFLink;
|
||||
import help.validator.links.InvalidLink;
|
||||
import help.validator.location.HelpModuleCollection;
|
||||
import help.validator.model.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
public class LinkDatabase {
|
||||
|
||||
/** Sorted for later presentation */
|
||||
private Set<InvalidLink> allUnresolvedLinks = new TreeSet<InvalidLink>(
|
||||
new Comparator<InvalidLink>() {
|
||||
@Override
|
||||
public int compare(InvalidLink o1, InvalidLink o2) {
|
||||
// same module...no subgroup by error type
|
||||
String name1 = o1.getClass().getSimpleName();
|
||||
String name2 = o2.getClass().getSimpleName();
|
||||
if (!name1.equals(name2)) {
|
||||
return name1.compareTo(name2);
|
||||
}
|
||||
|
||||
// ...also same error type, now subgroup by file
|
||||
Path file1 = o1.getSourceFile();
|
||||
Path file2 = o2.getSourceFile();
|
||||
if (!file1.equals(file2)) {
|
||||
return file1.toUri().compareTo(file2.toUri());
|
||||
}
|
||||
|
||||
// ...same file too...compare by line number
|
||||
int lineNumber1 = o1.getLineNumber();
|
||||
int lineNumber2 = o2.getLineNumber();
|
||||
if (lineNumber1 != lineNumber2) {
|
||||
return lineNumber1 - lineNumber2;
|
||||
}
|
||||
|
||||
// ...wow...on the same line too?...just use identity, since we
|
||||
// create as we parse, which is how we read, from left to right
|
||||
|
||||
return o1.identityHashCode() - o2.identityHashCode();
|
||||
}
|
||||
});
|
||||
|
||||
private final Set<DuplicateAnchorCollection> duplicateAnchors =
|
||||
new TreeSet<DuplicateAnchorCollection>(new Comparator<DuplicateAnchorCollection>() {
|
||||
@Override
|
||||
public int compare(DuplicateAnchorCollection o1, DuplicateAnchorCollection o2) {
|
||||
if (o1.getClass().equals(o2.getClass())) {
|
||||
if (o1 instanceof DuplicateAnchorCollectionByHelpTopic) {
|
||||
DuplicateAnchorCollectionByHelpTopic d1 =
|
||||
(DuplicateAnchorCollectionByHelpTopic) o1;
|
||||
DuplicateAnchorCollectionByHelpTopic d2 =
|
||||
(DuplicateAnchorCollectionByHelpTopic) o2;
|
||||
return d1.compareTo(d2);
|
||||
}
|
||||
else if (o1 instanceof DuplicateAnchorCollectionByHelpFile) {
|
||||
DuplicateAnchorCollectionByHelpFile d1 =
|
||||
(DuplicateAnchorCollectionByHelpFile) o1;
|
||||
DuplicateAnchorCollectionByHelpFile d2 =
|
||||
(DuplicateAnchorCollectionByHelpFile) o2;
|
||||
return d1.compareTo(d2);
|
||||
}
|
||||
throw new RuntimeException(
|
||||
"New type of DuplicateAnchorCollection not handled by this comparator");
|
||||
}
|
||||
|
||||
return o1.getClass().getSimpleName().compareTo(o2.getClass().getSimpleName());
|
||||
}
|
||||
});
|
||||
|
||||
private final HelpModuleCollection helpCollection;
|
||||
private final Map<String, TOCItemDefinition> mapOfIDsToTOCDefinitions =
|
||||
new HashMap<String, TOCItemDefinition>();
|
||||
private final Map<String, TOCItemExternal> mapOfIDsToTOCExternals =
|
||||
new HashMap<String, TOCItemExternal>();
|
||||
|
||||
private OverlayHelpTree printableTree;
|
||||
|
||||
public LinkDatabase(HelpModuleCollection helpCollection) {
|
||||
this.helpCollection = helpCollection;
|
||||
collectTOCItemDefinitions(helpCollection);
|
||||
collectTOCItemExternals(helpCollection);
|
||||
|
||||
// a tree of help TOC nodes that allows us to print the branches for a given TOC source file
|
||||
printableTree = new OverlayHelpTree(helpCollection, this);
|
||||
}
|
||||
|
||||
private void collectTOCItemDefinitions(TOCItemProvider tocProvider) {
|
||||
Map<String, TOCItemDefinition> map = tocProvider.getTOCItemDefinitionsByIDMapping();
|
||||
Set<Entry<String, TOCItemDefinition>> entrySet = map.entrySet();
|
||||
for (Entry<String, TOCItemDefinition> entry : entrySet) {
|
||||
String key = entry.getKey();
|
||||
TOCItemDefinition value = entry.getValue();
|
||||
if (mapOfIDsToTOCDefinitions.containsKey(key)) {
|
||||
throw new IllegalArgumentException("Cannot define the same TOC definition " +
|
||||
"more than once! Original definition: " + mapOfIDsToTOCDefinitions.get(key) +
|
||||
"\nSecond definition: " + value);
|
||||
}
|
||||
|
||||
mapOfIDsToTOCDefinitions.put(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
private void collectTOCItemExternals(TOCItemProvider tocProvider) {
|
||||
Map<String, TOCItemExternal> map = tocProvider.getTOCItemExternalsByDisplayMapping();
|
||||
for (TOCItemExternal tocItem : map.values()) {
|
||||
mapOfIDsToTOCExternals.put(tocItem.getIDAttribute(), tocItem);
|
||||
}
|
||||
}
|
||||
|
||||
public TOCItemDefinition getTOCDefinition(TOCItemReference referenceTOC) {
|
||||
return mapOfIDsToTOCDefinitions.get(referenceTOC.getIDAttribute());
|
||||
}
|
||||
|
||||
public TOCItemExternal getTOCExternal(TOCItemReference referenceTOC) {
|
||||
return mapOfIDsToTOCExternals.get(referenceTOC.getIDAttribute());
|
||||
}
|
||||
|
||||
HelpFile resolveLink(InvalidLink link) {
|
||||
if (!(link instanceof InvalidHREFLink)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
InvalidHREFLink hrefLink = (InvalidHREFLink) link;
|
||||
HREF href = hrefLink.getHREF();
|
||||
Path helpPath = href.getReferenceFileHelpPath();
|
||||
return findHelpFileForPath(helpPath);
|
||||
}
|
||||
|
||||
HelpFile resolveFile(Path referenceFileHelpPath) {
|
||||
return findHelpFileForPath(referenceFileHelpPath);
|
||||
}
|
||||
|
||||
private HelpFile findHelpFileForPath(Path helpPath) {
|
||||
HelpFile helpFile = helpCollection.getHelpFile(helpPath);
|
||||
if (helpFile != null) {
|
||||
return helpFile;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Collection<InvalidLink> getUnresolvedLinks() {
|
||||
return allUnresolvedLinks;
|
||||
}
|
||||
|
||||
public Collection<DuplicateAnchorCollection> getDuplicateAnchors() {
|
||||
return duplicateAnchors;
|
||||
}
|
||||
|
||||
void addUnresolvedLinks(Collection<InvalidLink> unresolvedLinks) {
|
||||
allUnresolvedLinks.addAll(unresolvedLinks);
|
||||
}
|
||||
|
||||
void addDuplicateAnchors(DuplicateAnchorCollection collection) {
|
||||
duplicateAnchors.add(collection);
|
||||
}
|
||||
|
||||
public String getIDForLink(String target) {
|
||||
Path path = Paths.get(target);
|
||||
Path file = Paths.get(target.split("#")[0]);
|
||||
|
||||
// TODO: Revisit how this is populated. Would like to include path back to /topics/
|
||||
// This currently requires every .htm[l] file to have a unique name, regardless of directory.
|
||||
HelpFile helpFile = findHelpFileForPath(file);
|
||||
|
||||
if (helpFile == null) {
|
||||
return null; // shouldn't happen under non-buggy conditions
|
||||
}
|
||||
|
||||
AnchorDefinition definition = helpFile.getAnchorDefinition(path);
|
||||
if (definition == null) {
|
||||
return null; // shouldn't happen under non-buggy conditions
|
||||
}
|
||||
return definition.getId();
|
||||
}
|
||||
|
||||
public void generateTOCOutputFile(Path outputFile, GhidraTOCFile file) throws IOException {
|
||||
printableTree.printTreeForID(outputFile, file.getFile().toUri().toString());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,206 @@
|
|||
/* ###
|
||||
* 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.validator;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
|
||||
import ghidra.util.exception.AssertException;
|
||||
import help.HelpBuildUtils;
|
||||
import help.validator.location.HelpModuleLocation;
|
||||
import help.validator.model.HREF;
|
||||
import help.validator.model.IMG;
|
||||
|
||||
public class ReferenceTagProcessor extends TagProcessor {
|
||||
|
||||
private static final String EOL = System.getProperty("line.separator");
|
||||
private static final String STYLESHEET_FILENAME = "Frontpage.css";
|
||||
private static final String STYLESHEET_PATHNAME = "shared/" + STYLESHEET_FILENAME;
|
||||
|
||||
private Path htmlFile;
|
||||
private Set<Path> styleSheets = new HashSet<>();
|
||||
private String title;
|
||||
private boolean readingTitle = false;
|
||||
|
||||
private final StringBuffer errors = new StringBuffer();
|
||||
private final Path defaultStyleSheet;
|
||||
private final AnchorManager anchorManager;
|
||||
private final HelpModuleLocation help;
|
||||
private int errorCount;
|
||||
|
||||
public ReferenceTagProcessor(HelpModuleLocation help, AnchorManager anchorManager) {
|
||||
this.help = help;
|
||||
this.anchorManager = anchorManager;
|
||||
|
||||
//
|
||||
// Note: currently all help being built has the required stylesheet living under
|
||||
// <help dir>/shared/<stylesheet name>
|
||||
//
|
||||
// If we ever need a more robust styling mechanism, then this code would need to be
|
||||
// updated to know how to search for the referenced stylesheet
|
||||
Path helpPath = help.getHelpLocation();
|
||||
FileSystem fs = helpPath.getFileSystem();
|
||||
Path relativeSSPath = fs.getPath(STYLESHEET_PATHNAME);
|
||||
defaultStyleSheet = helpPath.resolve(relativeSSPath);
|
||||
if (Files.notExists(helpPath)) {
|
||||
throw new AssertException("Cannot find expected stylesheet: " + defaultStyleSheet);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTagSupported(String tagType) {
|
||||
if (tagType == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
tagType = tagType.toLowerCase();
|
||||
return "a".equals(tagType) || "img".equals(tagType) || "title".equals(tagType) ||
|
||||
"/title".equals(tagType) || "link".equals(tagType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processTag(String tagType, LinkedHashMap<String, String> tagAttributes, Path file,
|
||||
int lineNum) throws IOException {
|
||||
|
||||
tagType = tagType.toLowerCase();
|
||||
if ("a".equals(tagType)) {
|
||||
if (tagAttributes.containsKey("href")) {
|
||||
try {
|
||||
anchorManager.addAnchorRef(
|
||||
new HREF(help, file, tagAttributes.get("href"), lineNum));
|
||||
}
|
||||
catch (URISyntaxException e) {
|
||||
errorCount++;
|
||||
errors.append(
|
||||
"Malformed Anchor Tag at (line " + lineNum + "): " + htmlFile + EOL);
|
||||
}
|
||||
}
|
||||
else if (tagAttributes.containsKey("name")) {
|
||||
anchorManager.addAnchor(file, tagAttributes.get("name"), lineNum);
|
||||
}
|
||||
else {
|
||||
errorCount++;
|
||||
errors.append("Bad Anchor Tag - unexpected attribtute (line " + lineNum + "): " +
|
||||
htmlFile + EOL);
|
||||
}
|
||||
}
|
||||
else if ("img".equals(tagType)) {
|
||||
if (tagAttributes.containsKey("src")) {
|
||||
try {
|
||||
anchorManager.addImageRef(
|
||||
new IMG(help, file, tagAttributes.get("src"), lineNum));
|
||||
}
|
||||
catch (URISyntaxException e) {
|
||||
errorCount++;
|
||||
errors.append("Malformed IMG Tag at (line " + lineNum + "): " + htmlFile + EOL);
|
||||
}
|
||||
}
|
||||
else {
|
||||
errorCount++;
|
||||
errors.append("Bad IMG Tag - unexpected attribtute (line " + lineNum + "): " +
|
||||
htmlFile + EOL);
|
||||
}
|
||||
}
|
||||
else if ("link".equals(tagType)) {
|
||||
String rel = tagAttributes.get("rel");
|
||||
if (rel != null && "stylesheet".equals(rel.toLowerCase())) {
|
||||
// TODO there is at least one help module that has multiple style sheets. I see no reason to
|
||||
// enforce this constraint:
|
||||
// if (hasStyleSheet) {
|
||||
// errorCount++;
|
||||
// errors.append("Multiple Stylesheets specified: " + htmlFile + EOL);
|
||||
// }
|
||||
// else {
|
||||
|
||||
String href = tagAttributes.get("href");
|
||||
if (href != null) {
|
||||
Path css = HelpBuildUtils.getFile(htmlFile, href);
|
||||
css = css.normalize();
|
||||
styleSheets.add(css); // validated later
|
||||
}
|
||||
// }
|
||||
}
|
||||
}
|
||||
else if ("title".equals(tagType)) {
|
||||
readingTitle = true;
|
||||
}
|
||||
else if ("/title".equals(tagType)) {
|
||||
readingTitle = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String processText(String text) {
|
||||
if (readingTitle) {
|
||||
text = text.trim();
|
||||
if (text.length() != 0) {
|
||||
if (title == null) {
|
||||
title = text;
|
||||
}
|
||||
else {
|
||||
title = title + " " + text;
|
||||
}
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startOfFile(Path localFile) {
|
||||
this.htmlFile = localFile;
|
||||
title = null;
|
||||
styleSheets.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endOfFile() {
|
||||
|
||||
if (title == null) {
|
||||
errorCount++;
|
||||
errors.append("Missing TITLE in: " + htmlFile + EOL);
|
||||
}
|
||||
|
||||
if (styleSheets.isEmpty()) {
|
||||
errorCount++;
|
||||
errors.append("Missing Stylesheet in: " + htmlFile + EOL);
|
||||
}
|
||||
|
||||
boolean hasDefaultStyleSheet = false;
|
||||
for (Path ss : styleSheets) {
|
||||
if (defaultStyleSheet.equals(ss)) {
|
||||
hasDefaultStyleSheet = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasDefaultStyleSheet) {
|
||||
errorCount++;
|
||||
errors.append("Incorrect stylesheet defined - none match " + defaultStyleSheet +
|
||||
" in file " + htmlFile + EOL);
|
||||
}
|
||||
}
|
||||
|
||||
public String getErrorText() {
|
||||
return errors.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getErrorCount() {
|
||||
return errorCount;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
/* ###
|
||||
* 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.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package help.validator;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.LinkedHashMap;
|
||||
|
||||
public abstract class TagProcessor {
|
||||
|
||||
enum TagProcessingState {
|
||||
LOOKING_FOR_NEXT_ATTR, READING_ATTR, LOOKING_FOR_VALUE, READING_VALUE;
|
||||
}
|
||||
|
||||
TagProcessor() {
|
||||
}
|
||||
|
||||
abstract void processTag(String tagType, LinkedHashMap<String, String> tagAttributes,
|
||||
Path file, int lineNum) throws IOException;
|
||||
|
||||
public void startOfFile(Path htmlFile) {
|
||||
// stub
|
||||
}
|
||||
|
||||
public void endOfFile() {
|
||||
// stub
|
||||
}
|
||||
|
||||
public String processText(String text) {
|
||||
return text;
|
||||
}
|
||||
|
||||
public boolean isTagSupported(String tagType) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public int getErrorCount() {
|
||||
return 0;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,241 @@
|
|||
/* ###
|
||||
* 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.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package help.validator;
|
||||
|
||||
import help.GHelpBuilder;
|
||||
import help.HelpBuildUtils;
|
||||
import help.validator.location.HelpModuleLocation;
|
||||
import help.validator.model.IMG;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.*;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.*;
|
||||
|
||||
public class UnusedHelpImageFileFinder {
|
||||
|
||||
private static final String DEBUG_SWITCH = "-debug";
|
||||
|
||||
private static List<String> moduleHelpPaths;
|
||||
private static boolean debugEnabled = false;
|
||||
|
||||
private SortedSet<Path> unusedFiles;
|
||||
|
||||
public static void main(String[] args) {
|
||||
parseArguments(args);
|
||||
|
||||
List<HelpModuleLocation> helpCollections = collectHelp();
|
||||
|
||||
Collection<IMG> referencedIMGs = getReferencedIMGs(helpCollections);
|
||||
debug("Found " + referencedIMGs.size() + " image referenes from help files");
|
||||
|
||||
Collection<Path> allImagesOnDisk = getAllImagesOnDisk(helpCollections);
|
||||
debug("Found " + allImagesOnDisk.size() + " image files in help directories");
|
||||
|
||||
Collection<Path> unusedFiles = getUnusedFiles(referencedIMGs, allImagesOnDisk);
|
||||
if (unusedFiles.size() == 0) {
|
||||
System.out.println("No unused image files found!");
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
System.err.println("Found the following " + unusedFiles.size() + " unused images: ");
|
||||
for (Path file : unusedFiles) {
|
||||
System.err.println(file.toUri());
|
||||
}
|
||||
}
|
||||
|
||||
public UnusedHelpImageFileFinder(Collection<HelpModuleLocation> helpCollections) {
|
||||
this(helpCollections, debugEnabled);
|
||||
}
|
||||
|
||||
public UnusedHelpImageFileFinder(Collection<HelpModuleLocation> helpCollections,
|
||||
boolean debugEnabled) {
|
||||
UnusedHelpImageFileFinder.debugEnabled = debugEnabled;
|
||||
|
||||
Collection<IMG> referencedIMGs = getReferencedIMGs(helpCollections);
|
||||
debug("Found " + referencedIMGs.size() + " image referenes from help files");
|
||||
|
||||
Collection<Path> allImagesOnDisk = getAllImagesOnDisk(helpCollections);
|
||||
debug("Found " + allImagesOnDisk.size() + " image files in help directories");
|
||||
|
||||
unusedFiles = getUnusedFiles(referencedIMGs, allImagesOnDisk);
|
||||
debug("Found " + unusedFiles.size() + " unused images");
|
||||
}
|
||||
|
||||
public SortedSet<Path> getUnusedImages() {
|
||||
return new TreeSet<Path>(unusedFiles);
|
||||
}
|
||||
|
||||
private static SortedSet<Path> getUnusedFiles(Collection<IMG> referencedIMGs,
|
||||
Collection<Path> imageFiles) {
|
||||
|
||||
Map<Path, IMG> fileToIMGMap = new HashMap<Path, IMG>();
|
||||
for (IMG img : referencedIMGs) {
|
||||
fileToIMGMap.put(img.getImageFile(), img);
|
||||
}
|
||||
|
||||
SortedSet<Path> set = new TreeSet<Path>(new Comparator<Path>() {
|
||||
@Override
|
||||
public int compare(Path f1, Path f2) {
|
||||
return f1.toUri().toString().toLowerCase().compareTo(
|
||||
f2.toUri().toString().toLowerCase());
|
||||
}
|
||||
});
|
||||
for (Path file : imageFiles) {
|
||||
IMG img = fileToIMGMap.get(file);
|
||||
if (img == null && !isExcludedImageFile(file)) {
|
||||
set.add(file);
|
||||
}
|
||||
}
|
||||
return set;
|
||||
}
|
||||
|
||||
private static boolean isExcludedImageFile(Path file) {
|
||||
String absolutePath = file.toUri().toString().toLowerCase();
|
||||
// Could be done by subpath examination
|
||||
return absolutePath.indexOf("help/shared/") != -1;
|
||||
}
|
||||
|
||||
private static Collection<IMG> getReferencedIMGs(Collection<HelpModuleLocation> helpCollections) {
|
||||
Set<IMG> set = new HashSet<IMG>();
|
||||
for (HelpModuleLocation help : helpCollections) {
|
||||
Collection<IMG> IMGs = help.getAllIMGs();
|
||||
set.addAll(IMGs);
|
||||
}
|
||||
return set;
|
||||
}
|
||||
|
||||
private static Collection<Path> getAllImagesOnDisk(
|
||||
Collection<HelpModuleLocation> helpDirectories) {
|
||||
List<Path> files = new ArrayList<Path>();
|
||||
for (HelpModuleLocation help : helpDirectories) {
|
||||
Path helpDir = help.getHelpLocation();
|
||||
gatherImageFiles(helpDir, files);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
private static void gatherImageFiles(Path file, final List<Path> files) {
|
||||
try {
|
||||
Files.walkFileTree(file, new SimpleFileVisitor<Path>() {
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path path, BasicFileAttributes attrs)
|
||||
throws IOException {
|
||||
if (isImageFile(path)) {
|
||||
files.add(path);
|
||||
}
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (IOException e) {
|
||||
// Must not exist
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isImageFile(Path file) {
|
||||
String filename = file.getFileName().toString().toLowerCase();
|
||||
return filename.endsWith(".png") || filename.endsWith(".gif") || filename.endsWith(".jpg");
|
||||
}
|
||||
|
||||
private static List<HelpModuleLocation> collectHelp() {
|
||||
debug("Parsing help dirs...");
|
||||
List<HelpModuleLocation> helpCollections =
|
||||
new ArrayList<HelpModuleLocation>(moduleHelpPaths.size());
|
||||
for (String helpDirName : moduleHelpPaths) {
|
||||
// 1) Make sure the help directory exists
|
||||
File helpDirectoryFile = null;
|
||||
try {
|
||||
helpDirectoryFile = new File(helpDirName).getCanonicalFile();
|
||||
debug("\tadding help dir: " + helpDirectoryFile);
|
||||
}
|
||||
catch (IOException e) {
|
||||
// handled below
|
||||
}
|
||||
|
||||
if (helpDirectoryFile == null || !helpDirectoryFile.isDirectory()) {
|
||||
errorMessage("Help directory not found - skipping: " + helpDirName);
|
||||
continue;
|
||||
}
|
||||
File moduleDir = helpDirectoryFile.getParentFile();
|
||||
File manifestFile = new File(moduleDir, "Module.manifest");
|
||||
if (!manifestFile.exists()) {
|
||||
errorMessage("Help directory not inside valid module: " + helpDirName);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3) Create the help directory
|
||||
helpCollections.add(HelpBuildUtils.toLocation(helpDirectoryFile));
|
||||
}
|
||||
|
||||
return helpCollections;
|
||||
}
|
||||
|
||||
private static void debug(String string) {
|
||||
if (debugEnabled) {
|
||||
System.out.println("[" + UnusedHelpImageFileFinder.class.getSimpleName() + "] " +
|
||||
string);
|
||||
}
|
||||
}
|
||||
|
||||
private static void printUsage() {
|
||||
StringBuilder buffy = new StringBuilder();
|
||||
|
||||
errorMessage("Usage:\n");
|
||||
buffy.append("<module help path1[;module help path2;module help path3;...]> [-debug]");
|
||||
|
||||
errorMessage(buffy.toString());
|
||||
}
|
||||
|
||||
private static void parseArguments(String[] args) {
|
||||
if (args.length == 0) {
|
||||
errorMessage("Missing required arguments - must supply at least one module help path");
|
||||
printUsage();
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
List<String> argList = Arrays.asList(args);
|
||||
|
||||
// get module directory paths
|
||||
String modulePathsString = argList.get(args.length - 1);
|
||||
moduleHelpPaths = new ArrayList<String>();
|
||||
StringTokenizer tokenizer = new StringTokenizer(modulePathsString, File.pathSeparator);
|
||||
while (tokenizer.hasMoreTokens()) {
|
||||
moduleHelpPaths.add(tokenizer.nextToken());
|
||||
}
|
||||
if (moduleHelpPaths.size() == 0) {
|
||||
errorMessage("Missing molule help path(s) argument - it must be last in the arg list");
|
||||
printUsage();
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
int debugIndex = argList.indexOf(DEBUG_SWITCH);
|
||||
debugEnabled = debugIndex != -1;
|
||||
}
|
||||
|
||||
private static void errorMessage(String message) {
|
||||
errorMessage(message, null);
|
||||
}
|
||||
|
||||
private static void errorMessage(String message, Throwable t) {
|
||||
System.err.println("[" + GHelpBuilder.class.getSimpleName() + "] " + message);
|
||||
if (t != null) {
|
||||
t.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
/* ###
|
||||
* 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.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package help.validator.links;
|
||||
|
||||
import help.validator.model.HREF;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
class IllegalHModuleAssociationHREFInvalidLink extends InvalidHREFLink {
|
||||
|
||||
private static final String MESSAGE = "Illegal module association";
|
||||
private final File sourceModule;
|
||||
private final File destinationModule;
|
||||
|
||||
IllegalHModuleAssociationHREFInvalidLink(HREF href, File sourceModule, File destinationModule) {
|
||||
super(href, MESSAGE);
|
||||
this.sourceModule = sourceModule;
|
||||
this.destinationModule = destinationModule;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return message + " - link: " + href + " from file: " + href.getSourceFile().toUri() +
|
||||
" (line:" + href.getLineNumber() + ") " + "\"" + sourceModule.getName() + "\"->" +
|
||||
"\"" + destinationModule.getName() + "\"";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
/* ###
|
||||
* 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.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package help.validator.links;
|
||||
|
||||
import help.validator.model.IMG;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class IllegalHModuleAssociationIMGInvalidLink extends InvalidIMGLink {
|
||||
|
||||
private static final String MESSAGE = "Illegal module association";
|
||||
private final File sourceModule;
|
||||
private final File destinationModule;
|
||||
|
||||
IllegalHModuleAssociationIMGInvalidLink(IMG img, File sourceModule, File destinationModule) {
|
||||
super(img, MESSAGE);
|
||||
this.sourceModule = sourceModule;
|
||||
this.destinationModule = destinationModule;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return message + " - link: " + img + " from file: " + img.getSourceFile().toUri() +
|
||||
" (line:" + img.getLineNumber() + ") " + "\"" + sourceModule.getName() + "\"->" + "\"" +
|
||||
destinationModule.getName() + "\"";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
/* ###
|
||||
* 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.validator.links;
|
||||
|
||||
import help.validator.model.IMG;
|
||||
|
||||
public class IncorrectIMGFilenameCaseInvalidLink extends InvalidIMGLink {
|
||||
private static final String MESSAGE = "Image filename has incorrect case";
|
||||
|
||||
public IncorrectIMGFilenameCaseInvalidLink(IMG img) {
|
||||
super(img, MESSAGE);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,137 @@
|
|||
/* ###
|
||||
* 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.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package help.validator.links;
|
||||
|
||||
import help.validator.model.HREF;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
public abstract class InvalidHREFLink implements InvalidLink {
|
||||
|
||||
protected final HREF href;
|
||||
protected final String message;
|
||||
|
||||
InvalidHREFLink(HREF href, String message) {
|
||||
this.href = href;
|
||||
this.message = message;
|
||||
if (Boolean.parseBoolean(System.getProperty("ghidra.help.failfast"))) {
|
||||
throw new RuntimeException(message + ": " + href);
|
||||
}
|
||||
}
|
||||
|
||||
public HREF getHREF() {
|
||||
return href;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int identityHashCode() {
|
||||
return System.identityHashCode(href);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path getSourceFile() {
|
||||
return href.getSourceFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLineNumber() {
|
||||
return href.getLineNumber();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(InvalidLink other) {
|
||||
if (other == null) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!(other instanceof InvalidHREFLink)) {
|
||||
return 1; // always put us below other types of Invalid Links
|
||||
}
|
||||
InvalidHREFLink otherLink = (InvalidHREFLink) other;
|
||||
|
||||
// Artificial sorting priority based upon the type of invalid link. When I wrote this, it
|
||||
// turns out that reverse alphabetical order is what I want, which is something like
|
||||
// missing files first, missing anchors in files second followed by illegal associations
|
||||
String className = getClass().getSimpleName();
|
||||
String otherClassName = other.getClass().getSimpleName();
|
||||
int result = className.compareTo(otherClassName);
|
||||
if (result != 0) {
|
||||
return -result;
|
||||
}
|
||||
|
||||
return href.compareTo(otherLink.href);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
// String sourceFileInfo = getSourceFileInfo();
|
||||
return message + "\n\tlink: " + href;// + "\n\tfrom file: " + sourceFileInfo;
|
||||
}
|
||||
|
||||
//
|
||||
// private String getSourceFileInfo() {
|
||||
// int lineNumber = href.getLineNumber();
|
||||
// if (lineNumber < 0) {
|
||||
// // shouldn't happen
|
||||
// return href.getSourceFile().toUri().toString();
|
||||
// }
|
||||
//
|
||||
// return href.getSourceFile().toUri() + " (line:" + lineNumber + ")";
|
||||
// }
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ((href == null) ? 0 : href.hashCode());
|
||||
result = prime * result + ((message == null) ? 0 : message.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
InvalidHREFLink other = (InvalidHREFLink) obj;
|
||||
if (href == null) {
|
||||
if (other.href != null) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (!href.equals(other.href)) {
|
||||
return false;
|
||||
}
|
||||
if (message == null) {
|
||||
if (other.message != null) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (!message.equals(other.message)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
/* ###
|
||||
* 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.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package help.validator.links;
|
||||
|
||||
import help.validator.model.IMG;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
public class InvalidIMGLink implements InvalidLink {
|
||||
|
||||
protected final IMG img;
|
||||
protected final String message;
|
||||
|
||||
protected InvalidIMGLink(IMG img, String message) {
|
||||
this.img = img;
|
||||
this.message = message;
|
||||
if (Boolean.parseBoolean(System.getProperty("ghidra.help.failfast"))) {
|
||||
throw new RuntimeException(message + ": " + img);
|
||||
}
|
||||
}
|
||||
|
||||
public IMG getIMG() {
|
||||
return img;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int identityHashCode() {
|
||||
return System.identityHashCode(img);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLineNumber() {
|
||||
return img.getLineNumber();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path getSourceFile() {
|
||||
return img.getSourceFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(InvalidLink other) {
|
||||
if (other == null) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!(other instanceof InvalidIMGLink)) {
|
||||
return 1;
|
||||
}
|
||||
InvalidIMGLink otherLink = (InvalidIMGLink) other;
|
||||
|
||||
// Artificial sorting priority based upon the type of invalid link. When I wrote this, it
|
||||
// turns out that reverse alphabetical order is what I want, which is something like
|
||||
// missing files first, missing anchors in files second followed by illegal associations
|
||||
String className = getClass().getSimpleName();
|
||||
String otherClassName = other.getClass().getSimpleName();
|
||||
int result = className.compareTo(otherClassName);
|
||||
if (result != 0) {
|
||||
return -result;
|
||||
}
|
||||
|
||||
return img.compareTo(otherLink.img);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return message + " -\n\tlink: " + img + "\n\tfrom file: " + getSourceFileInfo();
|
||||
}
|
||||
|
||||
private String getSourceFileInfo() {
|
||||
int lineNumber = img.getLineNumber();
|
||||
return img.getSourceFile().toUri() + " (line:" + lineNumber + ")";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ((img == null) ? 0 : img.hashCode());
|
||||
result = prime * result + ((message == null) ? 0 : message.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (obj == null)
|
||||
return false;
|
||||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
InvalidIMGLink other = (InvalidIMGLink) obj;
|
||||
if (img == null) {
|
||||
if (other.img != null)
|
||||
return false;
|
||||
}
|
||||
else if (!img.equals(other.img))
|
||||
return false;
|
||||
if (message == null) {
|
||||
if (other.message != null)
|
||||
return false;
|
||||
}
|
||||
else if (!message.equals(other.message))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
/* ###
|
||||
* 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.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package help.validator.links;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
public interface InvalidLink extends Comparable<InvalidLink> {
|
||||
|
||||
@Override
|
||||
public int compareTo(InvalidLink other);
|
||||
|
||||
@Override
|
||||
public String toString();
|
||||
|
||||
@Override
|
||||
public int hashCode();
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj);
|
||||
|
||||
public Path getSourceFile();
|
||||
|
||||
public int getLineNumber();
|
||||
|
||||
public int identityHashCode();
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/* ###
|
||||
* 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.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package help.validator.links;
|
||||
|
||||
import help.validator.model.HREF;
|
||||
|
||||
public class MissingAnchorInvalidLink extends InvalidHREFLink {
|
||||
|
||||
private static final String MESSAGE = "Unable to locate anchor in reference file";
|
||||
|
||||
public MissingAnchorInvalidLink(HREF href) {
|
||||
super(href, MESSAGE);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/* ###
|
||||
* 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.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package help.validator.links;
|
||||
|
||||
import help.validator.model.HREF;
|
||||
|
||||
public class MissingFileInvalidLink extends InvalidHREFLink {
|
||||
|
||||
private static final String MESSAGE = "Unable to locate reference file";
|
||||
|
||||
public MissingFileInvalidLink(HREF href) {
|
||||
super(href, MESSAGE);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/* ###
|
||||
* 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.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package help.validator.links;
|
||||
|
||||
import help.validator.model.IMG;
|
||||
|
||||
|
||||
public class MissingIMGFileInvalidLink extends InvalidIMGLink {
|
||||
|
||||
private static final String MESSAGE = "Image file not in help module";
|
||||
|
||||
public MissingIMGFileInvalidLink( IMG img ) {
|
||||
super( img, MESSAGE );
|
||||
}
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
/* ###
|
||||
* 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.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package help.validator.links;
|
||||
|
||||
import help.validator.location.HelpModuleCollection;
|
||||
import help.validator.model.TOCItemReference;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
public class MissingTOCDefinitionInvalidLink implements InvalidLink {
|
||||
|
||||
private final TOCItemReference reference;
|
||||
private final HelpModuleCollection help;
|
||||
|
||||
public MissingTOCDefinitionInvalidLink(HelpModuleCollection help, TOCItemReference reference) {
|
||||
this.help = help;
|
||||
this.reference = reference;
|
||||
if (Boolean.parseBoolean(System.getProperty("ghidra.help.failfast"))) {
|
||||
throw new RuntimeException(toString());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int identityHashCode() {
|
||||
return System.identityHashCode(reference);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path getSourceFile() {
|
||||
return reference.getSourceFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLineNumber() {
|
||||
return reference.getLineNumber();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(InvalidLink other) {
|
||||
if (other == null) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!(other instanceof MissingTOCDefinitionInvalidLink)) {
|
||||
return -1; // always put us above other types of Invalid Links
|
||||
}
|
||||
|
||||
MissingTOCDefinitionInvalidLink otherLink = (MissingTOCDefinitionInvalidLink) other;
|
||||
return reference.compareTo(otherLink.reference);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Missing TOC definition (<tocdef>) for reference (<tocref>):\n\t" + reference;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ((reference == null) ? 0 : reference.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
MissingTOCDefinitionInvalidLink other = (MissingTOCDefinitionInvalidLink) obj;
|
||||
if (reference == null) {
|
||||
if (other.reference != null) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (!reference.equals(other.reference)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
/* ###
|
||||
* 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.validator.links;
|
||||
|
||||
import help.validator.location.HelpModuleCollection;
|
||||
import help.validator.model.TOCItem;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
public class MissingTOCTargetIDInvalidLink implements InvalidLink {
|
||||
|
||||
private final TOCItem item;
|
||||
private final HelpModuleCollection help;
|
||||
|
||||
public MissingTOCTargetIDInvalidLink(HelpModuleCollection help, TOCItem item) {
|
||||
this.help = help;
|
||||
this.item = item;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int identityHashCode() {
|
||||
return System.identityHashCode(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path getSourceFile() {
|
||||
return item.getSourceFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLineNumber() {
|
||||
return item.getLineNumber();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(InvalidLink other) {
|
||||
if (other == null) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!(other instanceof MissingTOCTargetIDInvalidLink)) {
|
||||
return -1; // always put us above other types of Invalid Links
|
||||
}
|
||||
|
||||
MissingTOCTargetIDInvalidLink otherLink = (MissingTOCTargetIDInvalidLink) other;
|
||||
Path sourceFile = item.getSourceFile();
|
||||
Path otherSourceFile = otherLink.item.getSourceFile();
|
||||
int result = sourceFile.compareTo(otherSourceFile);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
return item.getIDAttribute().compareTo(otherLink.item.getIDAttribute());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Missing TOC target ID for definition (<tocdef>):\n\t" + item;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ((help == null) ? 0 : help.hashCode());
|
||||
result = prime * result + ((item == null) ? 0 : item.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
MissingTOCTargetIDInvalidLink other = (MissingTOCTargetIDInvalidLink) obj;
|
||||
if (help == null) {
|
||||
if (other.help != null) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (!help.equals(other.help)) {
|
||||
return false;
|
||||
}
|
||||
if (item == null) {
|
||||
if (other.item != null) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (!item.equals(other.item)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/* ###
|
||||
* 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.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package help.validator.links;
|
||||
|
||||
import help.validator.model.IMG;
|
||||
|
||||
|
||||
public class NonExistentIMGFileInvalidLink extends InvalidIMGLink {
|
||||
|
||||
private static final String MESSAGE = "Unable to locate image file";
|
||||
|
||||
public NonExistentIMGFileInvalidLink( IMG img ) {
|
||||
super( img, MESSAGE );
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
/* ###
|
||||
* 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.validator.location;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.*;
|
||||
|
||||
import javax.help.HelpSet;
|
||||
|
||||
import ghidra.util.exception.AssertException;
|
||||
import help.validator.model.GhidraTOCFile;
|
||||
|
||||
public class DirectoryHelpModuleLocation extends HelpModuleLocation {
|
||||
|
||||
public DirectoryHelpModuleLocation(File file) {
|
||||
super(file.toPath());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isHelpInputSource() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HelpSet loadHelpSet() {
|
||||
// help sets are generated from a directory module structure, thus one does not exist here
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GhidraTOCFile loadSourceTOCFile() {
|
||||
try (DirectoryStream<Path> ds = Files.newDirectoryStream(helpDir, "TOC_Source*.xml")) {
|
||||
for (Path file : ds) {
|
||||
try {
|
||||
return GhidraTOCFile.createGhidraTOCFile(file);
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new AssertException("Unexpected error loading source TOC file!: " + file,
|
||||
e);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new AssertException("Error reading help path: " + helpDir);
|
||||
}
|
||||
|
||||
throw new AssertException("Help module has no TOC_Source.xml file: " + helpDir);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,388 @@
|
|||
/* ###
|
||||
* 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.validator.location;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.*;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
|
||||
import javax.help.HelpSet;
|
||||
import javax.help.Map.ID;
|
||||
import javax.help.TOCView;
|
||||
import javax.swing.tree.DefaultMutableTreeNode;
|
||||
|
||||
import docking.help.CustomTOCView.CustomTreeItemDecorator;
|
||||
import help.HelpBuildUtils;
|
||||
import help.TOCItemProvider;
|
||||
import help.validator.model.*;
|
||||
|
||||
/**
|
||||
* A class that is meant to hold a single help <b>input</b> directory and 0 or more
|
||||
* <b>external, pre-built</b> help sources (i.e., jar file or directory).
|
||||
* <p>
|
||||
* <pre>
|
||||
* Note
|
||||
* Note
|
||||
* Note
|
||||
*
|
||||
* This class is a bit conceptually muddled. Our build system is reflected in this class in that
|
||||
* we currently build one help module at a time. Thus, any dependencies of that module being
|
||||
* built can be passed into this "collection" at build time. We used to build multiple help
|
||||
* modules at once, resolving dependencies for all of the input modules after we built each
|
||||
* module. This class will need to be tweaked in order to go back to a build system with
|
||||
* multiple input builds.
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
public class HelpModuleCollection implements TOCItemProvider {
|
||||
|
||||
private Collection<HelpModuleLocation> helpLocations;
|
||||
|
||||
/** The help we are building */
|
||||
private HelpModuleLocation inputHelp;
|
||||
|
||||
private List<HelpSet> externalHelpSets;
|
||||
private Map<PathKey, HelpFile> pathToHelpFileMap;
|
||||
|
||||
/**
|
||||
* Creates a help module collection that contains only a singe help module from a help
|
||||
* directory, not a pre-built help jar.
|
||||
*/
|
||||
public static HelpModuleCollection fromHelpDirectory(File dir) {
|
||||
return new HelpModuleCollection(toHelpLocations(Collections.singleton(dir)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a help module collection that assumes zero or more pre-built help jar files and
|
||||
* one help directory that is an input into the help building process.
|
||||
*/
|
||||
public static HelpModuleCollection fromFiles(Collection<File> files) {
|
||||
return new HelpModuleCollection(toHelpLocations(files));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a help module collection that assumes zero or more pre-built help jar files and
|
||||
* one help directory that is an input into the help building process.
|
||||
*/
|
||||
public static HelpModuleCollection fromHelpLocations(Collection<HelpModuleLocation> locations) {
|
||||
return new HelpModuleCollection(locations);
|
||||
}
|
||||
|
||||
private static Set<HelpModuleLocation> toHelpLocations(Collection<File> files) {
|
||||
Set<HelpModuleLocation> set = new HashSet<>();
|
||||
for (File file : files) {
|
||||
set.add(HelpBuildUtils.toLocation(file));
|
||||
}
|
||||
return set;
|
||||
}
|
||||
|
||||
private HelpModuleCollection(Collection<HelpModuleLocation> locations) {
|
||||
helpLocations = new LinkedHashSet<>(locations);
|
||||
|
||||
loadTOCs();
|
||||
|
||||
loadHelpSets();
|
||||
|
||||
if (inputHelp == null && externalHelpSets.size() == 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"Required TOC file does not exist. " + "You must create a TOC_Source.xml file, " +
|
||||
"even if it is an empty template, or provide a pre-built TOC. " +
|
||||
"Help directories: " + locations.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public GhidraTOCFile getSourceTOCFile() {
|
||||
return inputHelp.getSourceTOCFile();
|
||||
}
|
||||
|
||||
private void loadTOCs() {
|
||||
|
||||
for (HelpModuleLocation location : helpLocations) {
|
||||
if (!location.isHelpInputSource()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inputHelp != null) {
|
||||
throw new IllegalArgumentException("Cannot have more than one source input " +
|
||||
"help module. Found a second input module: " + location);
|
||||
}
|
||||
|
||||
inputHelp = location;
|
||||
}
|
||||
}
|
||||
|
||||
private void loadHelpSets() {
|
||||
|
||||
externalHelpSets = new ArrayList<>();
|
||||
for (HelpModuleLocation location : helpLocations) {
|
||||
if (location.isHelpInputSource()) {
|
||||
continue; // help sets only exist in pre-built help
|
||||
}
|
||||
|
||||
HelpSet helpSet = location.getHelpSet();
|
||||
externalHelpSets.add(helpSet);
|
||||
}
|
||||
|
||||
if (externalHelpSets.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean containsHelpFiles() {
|
||||
for (HelpModuleLocation location : helpLocations) {
|
||||
if (location.containsHelp()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public Collection<Path> getHelpRoots() {
|
||||
List<Path> result = new ArrayList<>();
|
||||
for (HelpModuleLocation location : helpLocations) {
|
||||
result.add(location.getHelpLocation());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public Map<HelpFile, Map<String, List<AnchorDefinition>>> getDuplicateAnchorsByFile() {
|
||||
|
||||
Map<HelpFile, Map<String, List<AnchorDefinition>>> result = new HashMap<>();
|
||||
for (HelpModuleLocation location : helpLocations) {
|
||||
Map<HelpFile, Map<String, List<AnchorDefinition>>> anchors =
|
||||
location.getDuplicateAnchorsByFile();
|
||||
result.putAll(anchors);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public Map<HelpTopic, List<AnchorDefinition>> getDuplicateAnchorsByTopic() {
|
||||
Map<HelpTopic, List<AnchorDefinition>> result = new HashMap<>();
|
||||
for (HelpModuleLocation location : helpLocations) {
|
||||
Map<HelpTopic, List<AnchorDefinition>> anchors = location.getDuplicateAnchorsByTopic();
|
||||
result.putAll(anchors);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public Collection<HREF> getAllHREFs() {
|
||||
|
||||
List<HREF> result = new ArrayList<>();
|
||||
for (HelpModuleLocation location : helpLocations) {
|
||||
result.addAll(location.getAllHREFs());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public Collection<IMG> getAllIMGs() {
|
||||
List<IMG> result = new ArrayList<>();
|
||||
for (HelpModuleLocation location : helpLocations) {
|
||||
result.addAll(location.getAllIMGs());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public Collection<AnchorDefinition> getAllAnchorDefinitions() {
|
||||
List<AnchorDefinition> result = new ArrayList<>();
|
||||
for (HelpModuleLocation location : helpLocations) {
|
||||
result.addAll(location.getAllAnchorDefinitions());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public AnchorDefinition getAnchorDefinition(Path target) {
|
||||
Map<PathKey, HelpFile> map = getPathHelpFileMap();
|
||||
HelpFile helpFile = map.get(new PathKey(target));
|
||||
if (helpFile == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
AnchorDefinition definition = helpFile.getAnchorDefinition(target);
|
||||
return definition;
|
||||
}
|
||||
|
||||
public HelpFile getHelpFile(Path helpPath) {
|
||||
if (helpPath == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<PathKey, HelpFile> map = getPathHelpFileMap();
|
||||
return map.get(new PathKey(helpPath));
|
||||
}
|
||||
|
||||
private Map<PathKey, HelpFile> getPathHelpFileMap() {
|
||||
if (pathToHelpFileMap == null) {
|
||||
pathToHelpFileMap = new HashMap<>();
|
||||
for (HelpModuleLocation location : helpLocations) {
|
||||
Collection<HelpFile> helpFiles = location.getHelpFiles();
|
||||
for (HelpFile helpFile : helpFiles) {
|
||||
PathKey entry = new PathKey(helpFile.getRelativePath());
|
||||
pathToHelpFileMap.put(entry, helpFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
return pathToHelpFileMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, TOCItemDefinition> getTOCItemDefinitionsByIDMapping() {
|
||||
Map<String, TOCItemDefinition> map = new HashMap<>();
|
||||
GhidraTOCFile TOC = inputHelp.getSourceTOCFile();
|
||||
map.putAll(TOC.getTOCDefinitionByIDMapping());
|
||||
return map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, TOCItemExternal> getTOCItemExternalsByDisplayMapping() {
|
||||
Map<String, TOCItemExternal> map = new HashMap<>();
|
||||
|
||||
if (externalHelpSets.isEmpty()) {
|
||||
return map;
|
||||
}
|
||||
|
||||
for (HelpSet helpSet : externalHelpSets) {
|
||||
TOCView view = (TOCView) helpSet.getNavigatorView("TOC");
|
||||
DefaultMutableTreeNode node = view.getDataAsTree();
|
||||
URL url = helpSet.getHelpSetURL();
|
||||
try {
|
||||
URL dataURL = new URL(url, (String) view.getParameters().get("data"));
|
||||
Path path = Paths.get(dataURL.toURI());
|
||||
addPrebuiltItem(node, path, map);
|
||||
}
|
||||
catch (MalformedURLException | URISyntaxException e) {
|
||||
throw new RuntimeException("Internal error", e);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private void addPrebuiltItem(DefaultMutableTreeNode tn, Path tocPath,
|
||||
Map<String, TOCItemExternal> mapByDisplay) {
|
||||
|
||||
Object userObject = tn.getUserObject();
|
||||
CustomTreeItemDecorator item = (CustomTreeItemDecorator) userObject;
|
||||
if (item != null) {
|
||||
DefaultMutableTreeNode parent = (DefaultMutableTreeNode) tn.getParent();
|
||||
TOCItemExternal parentItem = null;
|
||||
if (parent != null) {
|
||||
CustomTreeItemDecorator dec = (CustomTreeItemDecorator) parent.getUserObject();
|
||||
if (dec != null) {
|
||||
parentItem = mapByDisplay.get(dec.getDisplayText());
|
||||
}
|
||||
}
|
||||
|
||||
ID targetID = item.getID();
|
||||
|
||||
String displayText = item.getDisplayText();
|
||||
String tocID = item.getTocID();
|
||||
String target = targetID == null ? null : targetID.getIDString();
|
||||
TOCItemExternal external = new TOCItemExternal(parentItem, tocPath, tocID, displayText,
|
||||
target, item.getName(), -1);
|
||||
mapByDisplay.put(displayText, external);
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
Enumeration children = tn.children();
|
||||
while (children.hasMoreElements()) {
|
||||
DefaultMutableTreeNode child = (DefaultMutableTreeNode) children.nextElement();
|
||||
addPrebuiltItem(child, tocPath, mapByDisplay);
|
||||
}
|
||||
}
|
||||
|
||||
/** Input TOC items are those that we are building for the input help module of this collection */
|
||||
public Collection<TOCItem> getInputTOCItems() {
|
||||
Collection<TOCItem> items = new ArrayList<>();
|
||||
GhidraTOCFile TOC = inputHelp.getSourceTOCFile();
|
||||
items.addAll(TOC.getAllTOCItems());
|
||||
return items;
|
||||
}
|
||||
|
||||
public Collection<HREF> getTOC_HREFs() {
|
||||
Collection<HREF> definitions = new ArrayList<>();
|
||||
GhidraTOCFile TOC = inputHelp.getSourceTOCFile();
|
||||
definitions.addAll(getTOC_HREFs(TOC));
|
||||
return definitions;
|
||||
}
|
||||
|
||||
private Collection<HREF> getTOC_HREFs(GhidraTOCFile file) {
|
||||
Collection<TOCItemDefinition> definitions = file.getTOCDefinitions();
|
||||
Collection<HREF> hrefs = new HashSet<>();
|
||||
for (TOCItemDefinition definition : definitions) {
|
||||
if (definition.getTargetAttribute() == null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
hrefs.add(new HREF(inputHelp, file.getFile(), definition.getTargetAttribute(),
|
||||
definition.getLineNumber()));
|
||||
}
|
||||
catch (URISyntaxException e) {
|
||||
throw new RuntimeException("Malformed reference: ", e);
|
||||
}
|
||||
}
|
||||
return hrefs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return helpLocations.toString();
|
||||
}
|
||||
|
||||
//==================================================================================================
|
||||
// Inner Classes
|
||||
//==================================================================================================
|
||||
|
||||
/** A class that wraps a Path and allows map lookup for paths from different file systems */
|
||||
private class PathKey {
|
||||
private String path;
|
||||
|
||||
PathKey(Path p) {
|
||||
if (p == null) {
|
||||
throw new IllegalArgumentException("Path cannot be null");
|
||||
}
|
||||
this.path = p.toString().replace('\\', '/');
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return path.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
PathKey other = (PathKey) obj;
|
||||
|
||||
boolean result = path.equals(other.path);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return path.toString();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,218 @@
|
|||
/* ###
|
||||
* 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.validator.location;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
|
||||
import javax.help.HelpSet;
|
||||
|
||||
import ghidra.util.exception.AssertException;
|
||||
import help.validator.model.*;
|
||||
|
||||
public abstract class HelpModuleLocation {
|
||||
|
||||
/** The help dir parent of the help topics and such */
|
||||
protected Path helpDir;
|
||||
|
||||
private List<HelpTopic> helpTopics = new ArrayList<>();
|
||||
|
||||
/** this is the TOC_Source.xml file, not the generated file */
|
||||
protected GhidraTOCFile sourceTOCFile;
|
||||
private HelpSet helpSet;
|
||||
|
||||
HelpModuleLocation(Path source) {
|
||||
this.helpDir = source;
|
||||
|
||||
loadHelpTopics();
|
||||
sourceTOCFile = loadSourceTOCFile();
|
||||
helpSet = loadHelpSet();
|
||||
}
|
||||
|
||||
public abstract GhidraTOCFile loadSourceTOCFile();
|
||||
|
||||
public abstract HelpSet loadHelpSet();
|
||||
|
||||
/** Returns true if this help location represents a source of input files to generate help output */
|
||||
public abstract boolean isHelpInputSource();
|
||||
|
||||
protected void loadHelpTopics() {
|
||||
Path helpTopicsDir = helpDir.resolve("topics");
|
||||
if (!Files.exists(helpTopicsDir)) {
|
||||
throw new AssertException("No topics found in help dir: " + helpDir);
|
||||
}
|
||||
|
||||
try (DirectoryStream<Path> ds = Files.newDirectoryStream(helpTopicsDir);) {
|
||||
for (Path file : ds) {
|
||||
if (Files.isDirectory(file)) {
|
||||
helpTopics.add(new HelpTopic(this, file));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (IOException e) {
|
||||
// I suppose there aren't any
|
||||
throw new AssertException("No topics found in help dir: " + helpDir);
|
||||
}
|
||||
}
|
||||
|
||||
public Path getHelpLocation() {
|
||||
return helpDir;
|
||||
}
|
||||
|
||||
public Path getHelpModuleLocation() {
|
||||
// format: <module>/src/main/help/help/topics/<topic name>/<topic file>
|
||||
// help dir: <module>/src/main/help/help/
|
||||
|
||||
Path srcMainHelp = helpDir.getParent();
|
||||
Path srcMain = srcMainHelp.getParent();
|
||||
Path src = srcMain.getParent();
|
||||
Path module = src.getParent();
|
||||
return module;
|
||||
}
|
||||
|
||||
public Path getModuleRepoRoot() {
|
||||
|
||||
// module path format: <git dir>/<repo root>/<repo>/Features/Foo
|
||||
Path module = getHelpModuleLocation();
|
||||
Path category = module.getParent();
|
||||
Path repo = category.getParent();
|
||||
Path repoRoot = repo.getParent();
|
||||
return repoRoot;
|
||||
}
|
||||
|
||||
GhidraTOCFile getSourceTOCFile() {
|
||||
return sourceTOCFile;
|
||||
}
|
||||
|
||||
HelpSet getHelpSet() {
|
||||
return helpSet;
|
||||
}
|
||||
|
||||
public Collection<HelpTopic> getHelpTopics() {
|
||||
return new ArrayList<>(helpTopics);
|
||||
}
|
||||
|
||||
public Collection<HREF> getAllHREFs() {
|
||||
List<HREF> list = new ArrayList<>();
|
||||
for (HelpTopic helpTopic : helpTopics) {
|
||||
list.addAll(helpTopic.getAllHREFs());
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public Collection<IMG> getAllIMGs() {
|
||||
List<IMG> list = new ArrayList<>();
|
||||
for (HelpTopic helpTopic : helpTopics) {
|
||||
list.addAll(helpTopic.getAllIMGs());
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
Collection<AnchorDefinition> getAllAnchorDefinitions() {
|
||||
List<AnchorDefinition> list = new ArrayList<>();
|
||||
for (HelpTopic helpTopic : helpTopics) {
|
||||
list.addAll(helpTopic.getAllAnchorDefinitions());
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
Collection<HelpFile> getHelpFiles() {
|
||||
List<HelpFile> result = new ArrayList<>();
|
||||
for (HelpTopic topic : helpTopics) {
|
||||
result.addAll(topic.getHelpFiles());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
boolean containsHelp() {
|
||||
if (helpTopics.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (HelpTopic topic : helpTopics) {
|
||||
Collection<HelpFile> helpFiles = topic.getHelpFiles();
|
||||
if (!helpFiles.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Map<HelpFile, Map<String, List<AnchorDefinition>>> getDuplicateAnchorsByFile() {
|
||||
Map<HelpFile, Map<String, List<AnchorDefinition>>> map = new HashMap<>();
|
||||
for (HelpTopic helpTopic : helpTopics) {
|
||||
Collection<HelpFile> helpFiles = helpTopic.getHelpFiles();
|
||||
for (HelpFile helpFile : helpFiles) {
|
||||
Map<String, List<AnchorDefinition>> anchors = helpFile.getDuplicateAnchorsByID();
|
||||
if (anchors.size() > 0) {
|
||||
map.put(helpFile, anchors);
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
Map<HelpTopic, List<AnchorDefinition>> getDuplicateAnchorsByTopic() {
|
||||
Map<HelpTopic, List<AnchorDefinition>> map = new HashMap<>();
|
||||
for (HelpTopic helpTopic : helpTopics) {
|
||||
List<AnchorDefinition> duplicateDefinitions =
|
||||
getDuplicateTopicAnchorDefinitions(helpTopic);
|
||||
if (duplicateDefinitions.size() > 0) {
|
||||
map.put(helpTopic, duplicateDefinitions);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private List<AnchorDefinition> getDuplicateTopicAnchorDefinitions(HelpTopic helpTopic) {
|
||||
Map<String, List<AnchorDefinition>> map = new HashMap<>();
|
||||
Collection<HelpFile> helpFiles = helpTopic.getHelpFiles();
|
||||
|
||||
// collect all the anchor definitions by name
|
||||
for (HelpFile helpFile : helpFiles) {
|
||||
Collection<AnchorDefinition> definitions = helpFile.getAllAnchorDefinitions();
|
||||
for (AnchorDefinition anchorDefinition : definitions) {
|
||||
String name = anchorDefinition.getAnchorName();
|
||||
if (name == null) {
|
||||
continue; // ignore anchor definitions, as they don't exist in the source code
|
||||
}
|
||||
List<AnchorDefinition> list = map.get(name);
|
||||
if (list == null) {
|
||||
list = new ArrayList<AnchorDefinition>();
|
||||
map.put(name, list);
|
||||
}
|
||||
list.add(anchorDefinition);
|
||||
}
|
||||
}
|
||||
|
||||
// add the contents of all the lists with more than one item
|
||||
List<AnchorDefinition> list = new ArrayList<>();
|
||||
Collection<List<AnchorDefinition>> values = map.values();
|
||||
for (List<AnchorDefinition> definitions : values) {
|
||||
if (definitions.size() > 1) {
|
||||
list.addAll(definitions);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return helpDir.toUri().toString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
/* ###
|
||||
* 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.validator.location;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.*;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.help.HelpSet;
|
||||
import javax.help.HelpSetException;
|
||||
|
||||
import docking.help.GHelpSet;
|
||||
import ghidra.util.exception.AssertException;
|
||||
import help.validator.model.GhidraTOCFile;
|
||||
|
||||
public class JarHelpModuleLocation extends HelpModuleLocation {
|
||||
|
||||
private static Map<String, String> env = new HashMap<String, String>();
|
||||
static {
|
||||
env.put("create", "false");
|
||||
}
|
||||
|
||||
private static FileSystem getOrCreateJarFS(File jar) {
|
||||
URI jarURI;
|
||||
try {
|
||||
jarURI = new URI("jar:file://" + jar.toURI().getRawPath());
|
||||
}
|
||||
catch (URISyntaxException e) {
|
||||
throw new RuntimeException("Internal error", e);
|
||||
}
|
||||
try {
|
||||
return FileSystems.getFileSystem(jarURI);
|
||||
}
|
||||
catch (FileSystemNotFoundException e) {
|
||||
try {
|
||||
return FileSystems.newFileSystem(jarURI, env);
|
||||
}
|
||||
catch (IOException e1) {
|
||||
throw new RuntimeException("Unexpected error building help", e1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public JarHelpModuleLocation(File file) {
|
||||
super(getOrCreateJarFS(file).getPath("/help"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isHelpInputSource() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HelpSet loadHelpSet() {
|
||||
try (DirectoryStream<Path> ds = Files.newDirectoryStream(helpDir, "*_HelpSet.hs");) {
|
||||
for (Path path : ds) {
|
||||
return new GHelpSet(null, path.toUri().toURL());
|
||||
}
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new AssertException("No _HelpSet.hs file found for help directory: " + helpDir);
|
||||
}
|
||||
catch (HelpSetException e) {
|
||||
throw new AssertException("Error loading help set for " + helpDir);
|
||||
}
|
||||
|
||||
throw new AssertException("Pre-built help jar file is missing it's help set: " + helpDir);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GhidraTOCFile loadSourceTOCFile() {
|
||||
return null; // jar files have only generated content, not the source TOC file
|
||||
}
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
/* ###
|
||||
* 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.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package help.validator.model;
|
||||
|
||||
import ghidra.util.exception.AssertException;
|
||||
import help.HelpBuildUtils;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* A representation of a help location, which can be a file or a file with an anchor inside of
|
||||
* that file.
|
||||
* <p>
|
||||
* This class is used to generate target information for TOC files and to generate link information
|
||||
* for the help map files.
|
||||
* <p>
|
||||
* <br>
|
||||
* <p>
|
||||
* <b>Warning: </b> The ID generated by this class is specific to the JavaHelp system. It is of
|
||||
* the format:
|
||||
* <p>
|
||||
* <tt>TopicName_anchorName</tt>
|
||||
* <p>
|
||||
* or
|
||||
* <p>
|
||||
* <tt>TopicName_Filename</tt>
|
||||
*/
|
||||
public class AnchorDefinition {
|
||||
|
||||
private final Path sourceFile;
|
||||
private final Path helpRelativePath;
|
||||
private final String anchorName;
|
||||
private final int lineNum;
|
||||
private final String ID;
|
||||
|
||||
public AnchorDefinition(Path file, String anchorName, int lineNum) {
|
||||
this.sourceFile = file;
|
||||
this.anchorName = anchorName;
|
||||
this.lineNum = lineNum;
|
||||
|
||||
String prefix = getAnchorDefinitionPrefix(sourceFile);
|
||||
|
||||
String anchor = anchorName;
|
||||
if (anchor == null) {
|
||||
anchor = getDefaultAnchor(sourceFile);
|
||||
}
|
||||
|
||||
String rawID = prefix + "_" + anchor;
|
||||
ID = rawID.replace(' ', '_').replace('-', '_').replace('.', '_');
|
||||
|
||||
this.helpRelativePath = HelpBuildUtils.relativizeWithHelpTopics(file);
|
||||
}
|
||||
|
||||
private String getAnchorDefinitionPrefix(Path anchorSourceFile) {
|
||||
Path topicDir = HelpBuildUtils.getHelpTopicDir(anchorSourceFile);
|
||||
if (topicDir == null) {
|
||||
throw new AssertException(
|
||||
"Anchor defined in a file that does not live inside of a help topic");
|
||||
}
|
||||
|
||||
return topicDir.getFileName().toString();
|
||||
}
|
||||
|
||||
private String getDefaultAnchor(Path file) {
|
||||
String filename = file.getFileName().toString();
|
||||
int extension = filename.toLowerCase().indexOf(".htm");
|
||||
if (extension != -1) {
|
||||
return filename.substring(0, extension);
|
||||
}
|
||||
throw new AssertException("Cannot have HTML file without an .html extension");
|
||||
}
|
||||
|
||||
public String getAnchorName() {
|
||||
return anchorName;
|
||||
}
|
||||
|
||||
public Path getSrcFile() {
|
||||
return sourceFile;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return ID;
|
||||
}
|
||||
|
||||
public int getLineNumber() {
|
||||
return lineNum;
|
||||
}
|
||||
|
||||
private String getSource() {
|
||||
if (lineNum >= 0) {
|
||||
return "(line " + lineNum + ") in " + sourceFile.getFileName();
|
||||
}
|
||||
return "(File ID) in " + sourceFile.getFileName();
|
||||
}
|
||||
|
||||
public String getHelpPath() {
|
||||
if (anchorName == null) {
|
||||
return helpRelativePath.toString();
|
||||
}
|
||||
return helpRelativePath.toString() + '#' + anchorName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
if (lineNum < 0) {
|
||||
return "Anchor Definition: " + ID + getSource();
|
||||
}
|
||||
return "<a name=\"" + anchorName + "\"> " + getSource();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,210 @@
|
|||
/* ###
|
||||
* 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.validator.model;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
|
||||
import org.xml.sax.*;
|
||||
|
||||
import ghidra.xml.*;
|
||||
import help.validator.LinkDatabase;
|
||||
|
||||
public class GhidraTOCFile {
|
||||
|
||||
private static final String TOC_ITEM_ID = "id";
|
||||
private static final String TOC_ITEM_TEXT = "text";
|
||||
private static final String TOC_ITEM_TARGET = "target";
|
||||
private static final String TOC_ITEM_SORT_PREFERENCE = "sortgroup";
|
||||
|
||||
public static final String TOC_ITEM_REFERENCE = "tocref";
|
||||
public static final String TOC_ITEM_DEFINITION = "tocdef";
|
||||
private static final String ROOT_ATTRIBUTE_NAME = "tocroot";
|
||||
|
||||
private Map<String, TOCItemDefinition> mapOfIDsToTOCDefinitions = new HashMap<>();
|
||||
private List<TOCItemReference> listOfTOCReferences = new ArrayList<>();
|
||||
|
||||
public static GhidraTOCFile createGhidraTOCFile(Path sourceTOCFile)
|
||||
throws IOException, SAXException {
|
||||
GhidraTOCFile ghidraTOCFile = parseTOCFile(sourceTOCFile);
|
||||
ghidraTOCFile.sourceTOCFile = sourceTOCFile;
|
||||
return ghidraTOCFile;
|
||||
}
|
||||
|
||||
private Path sourceTOCFile;
|
||||
private DummyRootTOCItem rootItem;
|
||||
|
||||
GhidraTOCFile(Path sourceFile) {
|
||||
sourceTOCFile = sourceFile;
|
||||
}
|
||||
|
||||
private static GhidraTOCFile parseTOCFile(Path sourceTOCFile) throws SAXException, IOException {
|
||||
ErrorHandler handler = new ErrorHandler() {
|
||||
@Override
|
||||
public void warning(SAXParseException exception) throws SAXException {
|
||||
throw exception;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(SAXParseException exception) throws SAXException {
|
||||
throw exception;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fatalError(SAXParseException exception) throws SAXException {
|
||||
throw exception;
|
||||
}
|
||||
};
|
||||
XmlPullParser parser = new NonThreadedXmlPullParserImpl(Files.newInputStream(sourceTOCFile),
|
||||
sourceTOCFile.toUri().toString(), handler, false);
|
||||
|
||||
XmlElement root = parser.start();
|
||||
|
||||
if (!ROOT_ATTRIBUTE_NAME.equals(root.getName())) {
|
||||
throw new IOException("TOC source file does not start with a root tag named \"" +
|
||||
ROOT_ATTRIBUTE_NAME + "\"");
|
||||
}
|
||||
|
||||
GhidraTOCFile file = new GhidraTOCFile(sourceTOCFile);
|
||||
DummyRootTOCItem rootItem = new DummyRootTOCItem(sourceTOCFile);
|
||||
|
||||
buildRootNodes(parser, file, rootItem);
|
||||
file.rootItem = rootItem;
|
||||
return file;
|
||||
}
|
||||
|
||||
private static List<TOCItem> buildRootNodes(XmlPullParser parser, GhidraTOCFile file,
|
||||
TOCItem parent) {
|
||||
|
||||
List<TOCItem> list = new ArrayList<>();
|
||||
|
||||
while (parser.peek().isStart()) {
|
||||
XmlElement element = parser.next();
|
||||
TOCItem item = createTOCItem(element, parent, file);
|
||||
|
||||
list.addAll(buildRootNodes(parser, file, item));
|
||||
|
||||
parser.end(element);
|
||||
list.add(item);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private static TOCItem createTOCItem(XmlElement element, TOCItem parentItem,
|
||||
GhidraTOCFile file) {
|
||||
String typeOfTOCItem = element.getName();
|
||||
String ID = element.getAttribute(TOC_ITEM_ID);
|
||||
String text = element.getAttribute(TOC_ITEM_TEXT);
|
||||
String target = element.getAttribute(TOC_ITEM_TARGET);
|
||||
|
||||
if (ID == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"TOC \"" + typeOfTOCItem + "\" attribute \"" + TOC_ITEM_ID + "\" cannot be null!");
|
||||
}
|
||||
|
||||
int lineNumber = element.getLineNumber();
|
||||
|
||||
if (TOC_ITEM_REFERENCE.equals(typeOfTOCItem)) {
|
||||
return file.addTOCItemReference(
|
||||
new TOCItemReference(parentItem, file.sourceTOCFile, ID, lineNumber));
|
||||
}
|
||||
else if (TOC_ITEM_DEFINITION.equals(typeOfTOCItem)) {
|
||||
String sortPreference = element.getAttribute(TOC_ITEM_SORT_PREFERENCE);
|
||||
return file.addTOCItemDefinition(new TOCItemDefinition(parentItem, file.sourceTOCFile,
|
||||
ID, text, target, sortPreference, lineNumber));
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Unknown TOC type: " + typeOfTOCItem);
|
||||
}
|
||||
|
||||
private TOCItemDefinition addTOCItemDefinition(TOCItemDefinition definition) {
|
||||
TOCItemDefinition previous =
|
||||
mapOfIDsToTOCDefinitions.put(definition.getIDAttribute(), definition);
|
||||
if (previous != null) {
|
||||
throw new IllegalArgumentException(
|
||||
"Cannot define the same TOC definition more than once!\n\tOld value:\n\t" +
|
||||
previous + "\n\tNew value:\n\t" + definition + "\n\n");
|
||||
}
|
||||
return definition;
|
||||
}
|
||||
|
||||
private TOCItemReference addTOCItemReference(TOCItemReference reference) {
|
||||
listOfTOCReferences.add(reference);
|
||||
return reference;
|
||||
}
|
||||
|
||||
public Map<String, TOCItemDefinition> getTOCDefinitionByIDMapping() {
|
||||
return new HashMap<>(mapOfIDsToTOCDefinitions);
|
||||
}
|
||||
|
||||
Collection<TOCItemReference> getTOCReferences() {
|
||||
return new ArrayList<>(listOfTOCReferences);
|
||||
}
|
||||
|
||||
public Collection<TOCItemDefinition> getTOCDefinitions() {
|
||||
return new ArrayList<>(mapOfIDsToTOCDefinitions.values());
|
||||
}
|
||||
|
||||
public Collection<TOCItem> getAllTOCItems() {
|
||||
ArrayList<TOCItem> list = new ArrayList<>(listOfTOCReferences);
|
||||
list.addAll(mapOfIDsToTOCDefinitions.values());
|
||||
return list;
|
||||
}
|
||||
|
||||
public Path getFile() {
|
||||
return sourceTOCFile;
|
||||
}
|
||||
|
||||
//==================================================================================================
|
||||
// Inner Classes
|
||||
//==================================================================================================
|
||||
|
||||
static class DummyRootTOCItem extends TOCItem {
|
||||
|
||||
DummyRootTOCItem(Path sourceFile) {
|
||||
super(null, sourceFile, "Dummy Root Item", "Dummy Root Item", null, null, -1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getIDAttribute() {
|
||||
return null;
|
||||
}
|
||||
|
||||
void setChildren(List<TOCItem> rootChildren) {
|
||||
for (TOCItem item : rootChildren) {
|
||||
addChild(item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addChild(TOCItem child) {
|
||||
super.addChild(child);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean validate(LinkDatabase linkDatabase) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Dummy Root:\n" + printChildren();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,190 @@
|
|||
/* ###
|
||||
* 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.validator.model;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import help.HelpBuildUtils;
|
||||
import help.validator.location.HelpModuleLocation;
|
||||
|
||||
public class HREF implements Comparable<HREF> {
|
||||
|
||||
private HelpModuleLocation help;
|
||||
private final Path sourceFile;
|
||||
|
||||
private Path refFile;
|
||||
private String anchorName;
|
||||
private final int lineNumber;
|
||||
private final String href;
|
||||
private final boolean isRemote;
|
||||
private boolean isLocalAnchor;
|
||||
private Path relativePath;
|
||||
|
||||
public HREF(HelpModuleLocation help, Path sourceFile, String href, int lineNum)
|
||||
throws URISyntaxException {
|
||||
this.help = help;
|
||||
this.sourceFile = sourceFile;
|
||||
this.href = href;
|
||||
this.lineNumber = lineNum;
|
||||
|
||||
URI resolved;
|
||||
if (href.startsWith("help/topics")) {
|
||||
resolved = new URI(href);
|
||||
}
|
||||
else {
|
||||
URI URI = sourceFile.toUri();
|
||||
resolved = URI.resolve(href);
|
||||
}
|
||||
|
||||
isRemote = HelpBuildUtils.isRemote(resolved);
|
||||
if (!isRemote) {
|
||||
if (resolved.getFragment() == null) {
|
||||
this.refFile = HelpBuildUtils.locateReference(sourceFile, href);
|
||||
this.anchorName = null;
|
||||
}
|
||||
else if (resolved.getPath() == null) {
|
||||
// HREF to local anchor
|
||||
this.refFile = sourceFile;
|
||||
this.anchorName = resolved.getFragment();
|
||||
this.isLocalAnchor = true;
|
||||
}
|
||||
else {
|
||||
// HREF to other file
|
||||
this.refFile = HelpBuildUtils.locateReference(sourceFile, href);
|
||||
this.anchorName = resolved.getFragment();
|
||||
}
|
||||
}
|
||||
|
||||
this.relativePath = HelpBuildUtils.relativizeWithHelpTopics(refFile);
|
||||
}
|
||||
|
||||
public boolean isURL() {
|
||||
return isRemote;
|
||||
}
|
||||
|
||||
public boolean isLocalAnchor() {
|
||||
return isLocalAnchor;
|
||||
}
|
||||
|
||||
public Path getSourceFile() {
|
||||
return sourceFile;
|
||||
}
|
||||
|
||||
public boolean isRemote() {
|
||||
return isRemote;
|
||||
}
|
||||
|
||||
public String getAnchorName() {
|
||||
return anchorName;
|
||||
}
|
||||
|
||||
public String getRefString() {
|
||||
return href;
|
||||
}
|
||||
|
||||
/** The relative help path to the destination of this HREF */
|
||||
public Path getReferenceFileHelpPath() {
|
||||
return relativePath;
|
||||
}
|
||||
|
||||
public String getHelpPath() {
|
||||
Path referenceFileHelpPath = getReferenceFileHelpPath();
|
||||
if (referenceFileHelpPath == null) {
|
||||
return null;
|
||||
}
|
||||
if (anchorName == null) {
|
||||
return referenceFileHelpPath.toString();
|
||||
}
|
||||
|
||||
return referenceFileHelpPath.toString() + '#' + anchorName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(HREF other) {
|
||||
if (this.equals(other)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// group all HREFs in the same directory first
|
||||
HelpModuleLocation otherHelp = other.help;
|
||||
Path otherHelpLoc = otherHelp.getHelpLocation();
|
||||
Path myHelpLoc = help.getHelpLocation();
|
||||
if (!myHelpLoc.equals(otherHelpLoc)) {
|
||||
return myHelpLoc.compareTo(otherHelpLoc);
|
||||
}
|
||||
|
||||
// check file
|
||||
Path otherSourceFile = other.getSourceFile();
|
||||
if (!sourceFile.equals(otherSourceFile)) {
|
||||
return sourceFile.compareTo(otherSourceFile);
|
||||
}
|
||||
|
||||
// same source file, check line number
|
||||
if (lineNumber != other.lineNumber) {
|
||||
return lineNumber - other.lineNumber;
|
||||
}
|
||||
|
||||
String helpPath = getHelpPath();
|
||||
String otherHelpPath = other.getHelpPath();
|
||||
if (helpPath != null && otherHelpPath != null) {
|
||||
int result = helpPath.compareTo(otherHelpPath);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (helpPath == null && otherHelpPath != null) {
|
||||
return -1; // our path is null and 'other's is not; we go before
|
||||
}
|
||||
else if (helpPath != null && otherHelpPath == null) {
|
||||
return 1; // we have a non-null path, but 'other' doesn't; we go after
|
||||
}
|
||||
}
|
||||
|
||||
// highly unlikely case that we have to HREFs from the same file, pointing to the same
|
||||
// place, on the same HTML line. In this case, just use the object that was created first,
|
||||
// as it was probably parsed first from the file
|
||||
int identityHashCode = System.identityHashCode(this);
|
||||
int otherIdentityHashCode = System.identityHashCode(other);
|
||||
return identityHashCode - otherIdentityHashCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
String source = null;
|
||||
Path sourcePath = HelpBuildUtils.relativizeWithHelpTopics(sourceFile);
|
||||
if (sourcePath == null) {
|
||||
// not in 'help/topics'; relativize to the repo name
|
||||
Path repoRoot = help.getModuleRepoRoot();
|
||||
Path name = repoRoot.getFileName();
|
||||
sourcePath = HelpBuildUtils.relativize(name, sourceFile);
|
||||
}
|
||||
|
||||
source = sourcePath.toString();
|
||||
//@formatter:off
|
||||
return "<a href=\"" + href + "\">\n\t\t\t" +
|
||||
"From: " + source + " (line:" + lineNumber + "),\n\t\t\t" +
|
||||
"Resolved to: " + refFile;
|
||||
//@formatter:on
|
||||
}
|
||||
|
||||
public int getLineNumber() {
|
||||
return lineNumber;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
/* ###
|
||||
* 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.validator.model;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
|
||||
import ghidra.util.exception.AssertException;
|
||||
import help.HelpBuildUtils;
|
||||
import help.validator.*;
|
||||
import help.validator.location.HelpModuleLocation;
|
||||
|
||||
public class HelpFile {
|
||||
|
||||
private final Path helpFile;
|
||||
private final HelpModuleLocation help;
|
||||
private final Path relativePath;
|
||||
|
||||
private AnchorManager anchorManager;
|
||||
|
||||
HelpFile(HelpModuleLocation help, Path file) {
|
||||
this.help = help;
|
||||
this.helpFile = file;
|
||||
this.anchorManager = new AnchorManager();
|
||||
this.relativePath = HelpBuildUtils.relativizeWithHelpTopics(file);
|
||||
|
||||
cleanupHelpFile();
|
||||
|
||||
parseLinks();
|
||||
}
|
||||
|
||||
private void cleanupHelpFile() {
|
||||
try {
|
||||
HelpBuildUtils.cleanupHelpFileLinks(helpFile);
|
||||
}
|
||||
catch (IOException e) {
|
||||
System.err.println("Unexpected exception fixing help file links: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
Collection<HREF> getAllHREFs() {
|
||||
return anchorManager.getAnchorRefs();
|
||||
}
|
||||
|
||||
Collection<IMG> getAllIMGs() {
|
||||
return anchorManager.getImageRefs();
|
||||
}
|
||||
|
||||
public Path getRelativePath() {
|
||||
return relativePath;
|
||||
}
|
||||
|
||||
public boolean containsAnchor(String anchorName) {
|
||||
AnchorDefinition anchor = anchorManager.getAnchorForName(anchorName);
|
||||
return anchor != null;
|
||||
}
|
||||
|
||||
public Map<String, List<AnchorDefinition>> getDuplicateAnchorsByID() {
|
||||
return anchorManager.getDuplicateAnchorsByID();
|
||||
}
|
||||
|
||||
public AnchorDefinition getAnchorDefinition(Path helpPath) {
|
||||
Map<String, AnchorDefinition> anchorsByHelpPath = anchorManager.getAnchorsByHelpPath();
|
||||
return anchorsByHelpPath.get(helpPath.toString());
|
||||
}
|
||||
|
||||
public Collection<AnchorDefinition> getAllAnchorDefinitions() {
|
||||
return anchorManager.getAnchorsByHelpPath().values();
|
||||
}
|
||||
|
||||
public Path getFile() {
|
||||
return helpFile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return helpFile.toUri().toString();
|
||||
}
|
||||
|
||||
//==================================================================================================
|
||||
// Parsing Methods
|
||||
//==================================================================================================
|
||||
|
||||
private void parseLinks() {
|
||||
ReferenceTagProcessor tagProcessor = new ReferenceTagProcessor(help, anchorManager);
|
||||
processHelpFile(helpFile, anchorManager, tagProcessor);
|
||||
|
||||
if (tagProcessor.getErrorCount() > 0) {
|
||||
String errorText = tagProcessor.getErrorText();
|
||||
throw new AssertException(
|
||||
"Errors parsing HTML file: " + helpFile.getFileName() + "\n" + errorText);
|
||||
}
|
||||
}
|
||||
|
||||
private static void processHelpFile(Path file, AnchorManager anchorManager,
|
||||
TagProcessor tagProcessor) {
|
||||
|
||||
String fname = file.getFileName().toString().toLowerCase();
|
||||
if (fname.endsWith(".htm") || fname.endsWith(".html")) {
|
||||
try {
|
||||
anchorManager.addAnchor(file, null, -1);
|
||||
HTMLFileParser.scanHtmlFile(file, tagProcessor);
|
||||
}
|
||||
catch (IOException e) {
|
||||
System.err.println("Exception parsing file: " + file.toUri() + "\n");
|
||||
System.err.println(e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
else {
|
||||
// We've already filtered for .htm, .html, no?
|
||||
throw new RuntimeException("Internal error");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,160 @@
|
|||
/* ###
|
||||
* 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.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package help.validator.model;
|
||||
|
||||
import help.HelpBuildUtils;
|
||||
import help.validator.location.DirectoryHelpModuleLocation;
|
||||
import help.validator.location.HelpModuleLocation;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.*;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.*;
|
||||
|
||||
public class HelpTopic implements Comparable<HelpTopic> {
|
||||
private final HelpModuleLocation help;
|
||||
private final Path topicFile;
|
||||
private final Path relativePath;
|
||||
|
||||
private Map<Path, HelpFile> helpFiles = new LinkedHashMap<Path, HelpFile>();
|
||||
|
||||
public static HelpTopic fromHTMLFile(Path topicFile) {
|
||||
|
||||
// format: <module>/src/main/help/help/topics/<topic name>/<topic file>
|
||||
|
||||
Path topic = topicFile.getParent();
|
||||
Path topicsDir = topic.getParent();
|
||||
Path helpDir = topicsDir.getParent();
|
||||
|
||||
DirectoryHelpModuleLocation loc = new DirectoryHelpModuleLocation(helpDir.toFile());
|
||||
HelpTopic helpTopic = new HelpTopic(loc, topicFile);
|
||||
return helpTopic;
|
||||
}
|
||||
|
||||
public HelpTopic(HelpModuleLocation help, Path topicFile) {
|
||||
this.help = help;
|
||||
this.topicFile = topicFile;
|
||||
|
||||
Path helpDir = help.getHelpLocation();
|
||||
|
||||
Path unknowFSRelativePath = helpDir.relativize(topicFile); // may or may not be jar paths
|
||||
this.relativePath = HelpBuildUtils.toDefaultFS(unknowFSRelativePath);
|
||||
|
||||
loadHelpFiles(topicFile);
|
||||
}
|
||||
|
||||
public Path getTopicFile() {
|
||||
return topicFile;
|
||||
}
|
||||
|
||||
private void loadHelpFiles(final Path dir) {
|
||||
final PathMatcher matcher =
|
||||
dir.getFileSystem().getPathMatcher("glob:**/*.{[Hh][Tt][Mm],[Hh][Tt][Mm][Ll]}");
|
||||
|
||||
// Ex:
|
||||
// jar: /help/topics/FooPlugin
|
||||
final Path dirDefaultFS = HelpBuildUtils.toDefaultFS(dir);
|
||||
try {
|
||||
Files.walkFileTree(dir, new SimpleFileVisitor<Path>() {
|
||||
@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));
|
||||
}
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (IOException e) {
|
||||
System.err.println("Error loading help files: " + dir.toUri());
|
||||
e.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
|
||||
void addHelpFile(Path relPath, HelpFile helpFile) {
|
||||
helpFiles.put(relPath, helpFile);
|
||||
}
|
||||
|
||||
public Collection<HREF> getAllHREFs() {
|
||||
// Don't need to validate hrefs already in a .jar
|
||||
if (topicFile.getFileSystem() != FileSystems.getDefault()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<HREF> list = new ArrayList<HREF>();
|
||||
for (HelpFile helpFile : helpFiles.values()) {
|
||||
list.addAll(helpFile.getAllHREFs());
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public Collection<IMG> getAllIMGs() {
|
||||
// Don't need to validate imgs already in a .jar
|
||||
if (topicFile.getFileSystem() != FileSystems.getDefault()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<IMG> list = new ArrayList<IMG>();
|
||||
for (HelpFile helpFile : helpFiles.values()) {
|
||||
list.addAll(helpFile.getAllIMGs());
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public Collection<AnchorDefinition> getAllAnchorDefinitions() {
|
||||
// The current module may refer to anchors in pre-built modules.
|
||||
List<AnchorDefinition> list = new ArrayList<AnchorDefinition>();
|
||||
for (HelpFile helpFile : helpFiles.values()) {
|
||||
list.addAll(helpFile.getAllAnchorDefinitions());
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public Collection<HelpFile> getHelpFiles() {
|
||||
return helpFiles.values();
|
||||
}
|
||||
|
||||
Path getRelativePath() {
|
||||
return relativePath;
|
||||
}
|
||||
|
||||
public HelpModuleLocation getHelpDirectory() {
|
||||
return help;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return topicFile.getFileName().toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(HelpTopic o) {
|
||||
return topicFile.compareTo(o.topicFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return topicFile.toString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
/* ###
|
||||
* 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.validator.model;
|
||||
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import help.HelpBuildUtils;
|
||||
import help.ImageLocation;
|
||||
import help.validator.location.HelpModuleLocation;
|
||||
|
||||
public class IMG implements Comparable<IMG> {
|
||||
|
||||
private HelpModuleLocation help;
|
||||
private final Path sourceFile;
|
||||
/** Relative--starting with help/topics */
|
||||
private final Path relativePath;
|
||||
private final String imgSrc;
|
||||
|
||||
/**
|
||||
* The file on this filesystem; null if the file does not exists or of the image src
|
||||
* points to a remote URL or a runtime url.
|
||||
* <P>
|
||||
* An example remote URL is one that points to a web server, like <code>http://...</code>
|
||||
* <BR>An example runtime URL is one that the help system knows how to resolve at
|
||||
* runtime, like <code><IMG SRC='Icons.REFRESH_ICON /'></code>
|
||||
*/
|
||||
private final Path imgFile;
|
||||
private final ImageLocation imageLocation;
|
||||
private final int lineNumber;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param help the help module containing the file containing this IMG reference
|
||||
* @param sourceFile the source file containing this IMG reference
|
||||
* @param imgSrc the IMG SRC attribute pulled from the HTML file
|
||||
* @param lineNumber the line number of the IMG tag
|
||||
* @throws URISyntaxException
|
||||
*/
|
||||
public IMG(HelpModuleLocation help, Path sourceFile, String imgSrc, int lineNumber)
|
||||
throws URISyntaxException {
|
||||
this.help = help;
|
||||
this.sourceFile = sourceFile;
|
||||
this.relativePath = HelpBuildUtils.relativizeWithHelpTopics(sourceFile);
|
||||
this.imgSrc = imgSrc;
|
||||
this.lineNumber = lineNumber;
|
||||
|
||||
this.imageLocation = HelpBuildUtils.locateImageReference(sourceFile, imgSrc);
|
||||
this.imgFile = imageLocation.getResolvedPath();
|
||||
}
|
||||
|
||||
public Path getSourceFile() {
|
||||
return sourceFile;
|
||||
}
|
||||
|
||||
public String getSrcAttribute() {
|
||||
return imgSrc;
|
||||
}
|
||||
|
||||
public boolean isRemote() {
|
||||
return imageLocation.isRemote();
|
||||
}
|
||||
|
||||
public boolean isRuntime() {
|
||||
return imageLocation.isRuntime();
|
||||
}
|
||||
|
||||
public Path getImageFile() {
|
||||
return imgFile;
|
||||
}
|
||||
|
||||
public Path getHelpPath() {
|
||||
return imgFile;
|
||||
}
|
||||
|
||||
public int getLineNumber() {
|
||||
return lineNumber;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(IMG other) {
|
||||
|
||||
// group all HREFs in the same directory first
|
||||
HelpModuleLocation otherHelp = other.help;
|
||||
Path otherHelpLoc = otherHelp.getHelpLocation();
|
||||
Path myHelpLoc = help.getHelpLocation();
|
||||
if (!myHelpLoc.equals(otherHelpLoc)) {
|
||||
return myHelpLoc.compareTo(otherHelpLoc);
|
||||
}
|
||||
|
||||
// check file
|
||||
Path otherSourceFile = other.getSourceFile();
|
||||
if (!sourceFile.equals(otherSourceFile)) {
|
||||
return sourceFile.toUri().compareTo(otherSourceFile.toUri());
|
||||
}
|
||||
|
||||
// same source file, check line number
|
||||
if (lineNumber != other.lineNumber) {
|
||||
return lineNumber - other.lineNumber;
|
||||
}
|
||||
|
||||
Path myHelpPath = getHelpPath();
|
||||
Path otherHelpPath = other.getHelpPath();
|
||||
if (myHelpPath != null && otherHelpPath != null) {
|
||||
int result = myHelpPath.compareTo(otherHelpPath);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (myHelpPath == null && otherHelpPath != null) {
|
||||
return -1; // our path is null and 'other's is not; we go before
|
||||
}
|
||||
else if (myHelpPath != null && otherHelpPath == null) {
|
||||
return 1; // we have a non-null path, but 'other' doesn't; we go after
|
||||
}
|
||||
}
|
||||
|
||||
// highly unlikely case that we have to HREFs from the same file, pointing to the same
|
||||
// place, on the same HTML line. In this case, just use the object that was created first,
|
||||
// as it was probably parsed first from the file
|
||||
int identityHashCode = System.identityHashCode(this);
|
||||
int otherIdentityHashCode = System.identityHashCode(other);
|
||||
return identityHashCode - otherIdentityHashCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "<img src=\"" + imgSrc + "\"> [\n\t\tFrom: " + relativePath + ",\n\t\tResolved: " +
|
||||
imgFile + "\n\t]";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,305 @@
|
|||
/* ###
|
||||
* 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.validator.model;
|
||||
|
||||
import help.validator.LinkDatabase;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* A Table of Contents entry, which is represented in the help output as an xml tag.
|
||||
*/
|
||||
public abstract class TOCItem {
|
||||
|
||||
//@formatter:off
|
||||
protected static final String[] INDENTS = {
|
||||
"",
|
||||
"\t",
|
||||
"\t\t",
|
||||
"\t\t\t",
|
||||
"\t\t\t\t",
|
||||
"\t\t\t\t\t",
|
||||
"\t\t\t\t\t\t",
|
||||
"\t\t\t\t\t\t\t",
|
||||
"\t\t\t\t\t\t\t\t"
|
||||
};
|
||||
//@formatter:on
|
||||
|
||||
private static final String TOC_TAG_NAME = "tocitem";
|
||||
private static final String TEXT = "text";
|
||||
private static final String TARGET = "target";
|
||||
private static final String MERGE_TYPE_ATTRIBUTE = "mergetype=\"javax.help.SortMerge\"";
|
||||
protected static final String TOC_ITEM_CLOSE_TAG = "</tocitem>";
|
||||
|
||||
private String sortPreference;
|
||||
private final String IDAttribute;
|
||||
protected String textAttribute;
|
||||
protected String targetAttribute;
|
||||
private final Path sourceFile;
|
||||
protected TOCItem parentItem;
|
||||
private Set<TOCItem> children = new HashSet<TOCItem>();
|
||||
private int lineNumber;
|
||||
|
||||
public TOCItem(TOCItem parentItem, Path sourceFile, String ID, int lineNumber) {
|
||||
this(parentItem, sourceFile, ID, null, null, null, lineNumber);
|
||||
}
|
||||
|
||||
TOCItem(TOCItem parentItem, Path sourceFile, String ID, String text, String target,
|
||||
String sortPreference, int lineNumber) {
|
||||
this.parentItem = parentItem;
|
||||
this.sourceFile = sourceFile;
|
||||
this.IDAttribute = ID;
|
||||
this.textAttribute = text;
|
||||
|
||||
this.targetAttribute = target;
|
||||
if (sortPreference != null) {
|
||||
this.sortPreference = sortPreference.toLowerCase();
|
||||
}
|
||||
else {
|
||||
this.sortPreference = (textAttribute == null) ? "" : textAttribute.toLowerCase();
|
||||
}
|
||||
this.lineNumber = lineNumber;
|
||||
|
||||
if (parentItem != null) {
|
||||
parentItem.addChild(this);
|
||||
}
|
||||
}
|
||||
|
||||
public abstract boolean validate(LinkDatabase linkDatabase);
|
||||
|
||||
protected int childCount() {
|
||||
return children.size();
|
||||
}
|
||||
|
||||
protected void addChild(TOCItem child) {
|
||||
if (this == child) {
|
||||
throw new IllegalArgumentException("TOCItem cannot be added to itself");
|
||||
}
|
||||
|
||||
children.add(child);
|
||||
}
|
||||
|
||||
protected void removeChild(TOCItem child) {
|
||||
children.remove(child);
|
||||
}
|
||||
|
||||
protected Collection<TOCItem> getChildren() {
|
||||
return Collections.unmodifiableCollection(children);
|
||||
}
|
||||
|
||||
public String getSortPreference() {
|
||||
return sortPreference;
|
||||
}
|
||||
|
||||
public int getLineNumber() {
|
||||
return lineNumber;
|
||||
}
|
||||
|
||||
public TOCItem getParent() {
|
||||
return parentItem;
|
||||
}
|
||||
|
||||
public Path getSourceFile() {
|
||||
return sourceFile;
|
||||
}
|
||||
|
||||
public String getIDAttribute() {
|
||||
return IDAttribute;
|
||||
}
|
||||
|
||||
public String getTextAttribute() {
|
||||
return textAttribute;
|
||||
}
|
||||
|
||||
public String getTargetAttribute() {
|
||||
return targetAttribute;
|
||||
}
|
||||
|
||||
protected String printChildren() {
|
||||
return printChildren(1);
|
||||
}
|
||||
|
||||
protected String printChildren(int tabCount) {
|
||||
StringBuilder buildy = new StringBuilder();
|
||||
for (TOCItem item : children) {
|
||||
buildy.append(INDENTS[tabCount]).append(item.toString());
|
||||
buildy.append('\n').append(item.printChildren(tabCount + 1));
|
||||
}
|
||||
return buildy.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ((IDAttribute == null) ? 0 : IDAttribute.hashCode());
|
||||
result = prime * result + ((sortPreference == null) ? 0 : sortPreference.hashCode());
|
||||
result = prime * result + ((sourceFile == null) ? 0 : sourceFile.hashCode());
|
||||
result = prime * result + ((targetAttribute == null) ? 0 : targetAttribute.hashCode());
|
||||
result = prime * result + ((textAttribute == null) ? 0 : textAttribute.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (obj == null)
|
||||
return false;
|
||||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
TOCItem other = (TOCItem) obj;
|
||||
if (IDAttribute == null) {
|
||||
if (other.IDAttribute != null)
|
||||
return false;
|
||||
}
|
||||
else if (!IDAttribute.equals(other.IDAttribute))
|
||||
return false;
|
||||
if (sortPreference == null) {
|
||||
if (other.sortPreference != null)
|
||||
return false;
|
||||
}
|
||||
else if (!sortPreference.equals(other.sortPreference))
|
||||
return false;
|
||||
if (sourceFile == null) {
|
||||
if (other.sourceFile != null)
|
||||
return false;
|
||||
}
|
||||
else if (!sourceFile.equals(other.sourceFile))
|
||||
return false;
|
||||
if (targetAttribute == null) {
|
||||
if (other.targetAttribute != null)
|
||||
return false;
|
||||
}
|
||||
else if (!targetAttribute.equals(other.targetAttribute))
|
||||
return false;
|
||||
if (textAttribute == null) {
|
||||
if (other.textAttribute != null)
|
||||
return false;
|
||||
}
|
||||
else if (!textAttribute.equals(other.textAttribute))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if the two items are the same, except that they come from a different source file.
|
||||
*/
|
||||
public boolean isEquivalent(TOCItem other) {
|
||||
if (this == other)
|
||||
return true;
|
||||
if (other == null)
|
||||
return false;
|
||||
if (getClass() != other.getClass())
|
||||
return false;
|
||||
|
||||
if (IDAttribute == null) {
|
||||
if (other.IDAttribute != null)
|
||||
return false;
|
||||
}
|
||||
else if (!IDAttribute.equals(other.IDAttribute))
|
||||
return false;
|
||||
if (sortPreference == null) {
|
||||
if (other.sortPreference != null)
|
||||
return false;
|
||||
}
|
||||
else if (!sortPreference.equals(other.sortPreference))
|
||||
return false;
|
||||
|
||||
if (targetAttribute == null) {
|
||||
if (other.targetAttribute != null)
|
||||
return false;
|
||||
}
|
||||
else if (!targetAttribute.equals(other.targetAttribute))
|
||||
return false;
|
||||
if (textAttribute == null) {
|
||||
if (other.textAttribute != null)
|
||||
return false;
|
||||
}
|
||||
else if (!textAttribute.equals(other.textAttribute))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void writeContents(LinkDatabase linkDatabase, PrintWriter writer, int indentLevel) {
|
||||
// if I have no children, then just write out a simple tag
|
||||
if (children.size() == 0) {
|
||||
writer.println(generateTOCItemTag(linkDatabase, true, indentLevel));
|
||||
}
|
||||
|
||||
// otherwise, write out my opening tag, my children's data and then my closing tag
|
||||
else {
|
||||
writer.println(generateTOCItemTag(linkDatabase, false, indentLevel));
|
||||
int nextIndentLevel = indentLevel + 1;
|
||||
for (TOCItem item : children) {
|
||||
item.writeContents(linkDatabase, writer, nextIndentLevel);
|
||||
}
|
||||
writer.println(INDENTS[indentLevel] + TOC_ITEM_CLOSE_TAG);
|
||||
}
|
||||
}
|
||||
|
||||
public String generateTOCItemTag(LinkDatabase linkDatabase, boolean isInlineTag, int indentLevel) {
|
||||
StringBuilder buildy = new StringBuilder();
|
||||
buildy.append(INDENTS[indentLevel]);
|
||||
buildy.append('<').append(TOC_TAG_NAME).append(' ');
|
||||
|
||||
// text attribute
|
||||
// NOTE: we do not put our display text in this attribute. This is because JavaHelp uses
|
||||
// this attribute for sorting. We want to separate sorting from display, so we
|
||||
// manipulate the JavaHelp software by setting this attribute the desired sort value.
|
||||
// We have overridden JavaHelp to use a custom renderer that will paint the display
|
||||
// text with the attribute we set below.
|
||||
buildy.append(TEXT).append("=\"").append(sortPreference).append("\" ");
|
||||
|
||||
// target attribute
|
||||
if (targetAttribute != null) {
|
||||
// this can be null if no html file is specified for a TOC item (like a parent folder)
|
||||
String ID = linkDatabase.getIDForLink(targetAttribute);
|
||||
if (ID == null) {
|
||||
ID = targetAttribute; // this can happen for things we do not map, like raw URLs
|
||||
}
|
||||
buildy.append(TARGET).append("=\"").append(ID).append("\" ");
|
||||
}
|
||||
|
||||
// mergetype attribute
|
||||
buildy.append(MERGE_TYPE_ATTRIBUTE);
|
||||
|
||||
// our custom display text attribute
|
||||
buildy.append(' ').append("display").append("=\"").append(textAttribute).append("\"");
|
||||
|
||||
// our custom toc id attribute
|
||||
buildy.append(' ').append("toc_id").append("=\"").append(IDAttribute).append("\"");
|
||||
|
||||
if (isInlineTag) {
|
||||
buildy.append(" />");
|
||||
}
|
||||
else {
|
||||
buildy.append(">");
|
||||
}
|
||||
|
||||
return buildy.toString();
|
||||
}
|
||||
|
||||
public String generateEndTag(int indentLevel) {
|
||||
return INDENTS[indentLevel] + TOC_ITEM_CLOSE_TAG;
|
||||
}
|
||||
|
||||
public void writeContents(LinkDatabase linkDatabase, PrintWriter writer) {
|
||||
writeContents(linkDatabase, writer, 0);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
/* ###
|
||||
* 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.validator.model;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
import help.validator.LinkDatabase;
|
||||
|
||||
/**
|
||||
* A representation of the <tocdef> tag, which is a way to define a TOC item entry in
|
||||
* a TOC_Source.xml file.
|
||||
*/
|
||||
public class TOCItemDefinition extends TOCItem {
|
||||
|
||||
public TOCItemDefinition(TOCItem parentItem, Path sourceTOCFile, String ID, String text,
|
||||
String target, String sortPreference, int lineNumber) {
|
||||
super(parentItem, sourceTOCFile, ID, text, target, sortPreference, lineNumber);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean validate(LinkDatabase linkDatabase) {
|
||||
if (getTargetAttribute() == null) {
|
||||
return true; // no target path to validate
|
||||
}
|
||||
|
||||
String ID = linkDatabase.getIDForLink(getTargetAttribute());
|
||||
if (ID != null) {
|
||||
return true; // valid help ID found
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generateTOCItemTag(LinkDatabase linkDatabase, boolean isInlineTag,
|
||||
int indentLevel) {
|
||||
|
||||
return super.generateTOCItemTag(linkDatabase, isInlineTag, indentLevel);
|
||||
|
||||
// This code allows us to comment out definitions with unresolved paths. We are not using it
|
||||
// now because the build will fail if there are unresolved links and *they are not excluded*
|
||||
/*
|
||||
if ( getTargetAttribute() == null ) {
|
||||
return super.generateTOCItemTag( linkDatabase, isInlineTag, indentLevel );
|
||||
}
|
||||
|
||||
String ID = linkDatabase.getIDForLink( getTargetAttribute() );
|
||||
if( ID != null ) {
|
||||
return super.generateTOCItemTag( linkDatabase, isInlineTag, indentLevel );
|
||||
}
|
||||
|
||||
String indent = INDENTS[indentLevel];
|
||||
|
||||
StringBuilder buildy = new StringBuilder();
|
||||
buildy.append( indent ).append( "<!-- WARNING: Unresolved definition target (cannot find ID for target)\n" );
|
||||
buildy.append( indent ).append( '\t' ).append( generateXMLString() ).append( "\n" );
|
||||
buildy.append( indent ).append( "-->");
|
||||
return buildy.toString();*/
|
||||
}
|
||||
|
||||
//
|
||||
// private String generateXMLString() {
|
||||
// if (getTargetAttribute() == null) {
|
||||
// return "<" + GhidraTOCFile.TOC_ITEM_DEFINITION + " id=\"" + getIDAttribute() +
|
||||
// "\" text=\"" + getTextAttribute() + "\"/>";
|
||||
// }
|
||||
// return "<" + GhidraTOCFile.TOC_ITEM_DEFINITION + " id=\"" + getIDAttribute() +
|
||||
// "\" text=\"" + getTextAttribute() + "\" target=\"" + getTargetAttribute() + "\"/>";
|
||||
// }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
//@formatter:off
|
||||
return "<"+GhidraTOCFile.TOC_ITEM_DEFINITION +
|
||||
" id=\"" + getIDAttribute() + "\" text=\"" + getTextAttribute() + "\" " +
|
||||
"\n\t\ttarget=\"" + getTargetAttribute() + "\" />" +
|
||||
"\n\t\t[source file=\"" + getSourceFile() + "\" (line:" + getLineNumber() + ")]";
|
||||
//@formatter:on
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
/* ###
|
||||
* 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.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package help.validator.model;
|
||||
|
||||
import help.validator.LinkDatabase;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
public class TOCItemExternal extends TOCItem {
|
||||
|
||||
public TOCItemExternal(TOCItem parentItem, Path tocFile, String ID, String text, String target,
|
||||
String sortPreference, int lineNumber) {
|
||||
super(parentItem, tocFile, ID, text, target, sortPreference, lineNumber);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean validate(LinkDatabase linkDatabase) {
|
||||
if (getTargetAttribute() == null) {
|
||||
return true; // no target path to validate
|
||||
}
|
||||
|
||||
String ID = linkDatabase.getIDForLink(getTargetAttribute());
|
||||
if (ID != null) {
|
||||
return true; // valid help ID found
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generateTOCItemTag(LinkDatabase linkDatabase, boolean isInlineTag, int indentLevel) {
|
||||
return super.generateTOCItemTag(linkDatabase, isInlineTag, indentLevel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
//@formatter:off
|
||||
return "<tocitem id=\"" + getIDAttribute() + "\"\n\t\t" +
|
||||
"text=\"" + getTextAttribute() + "\"\n\t\t" +
|
||||
"target=\"" + getTargetAttribute() + "\"" +
|
||||
"/>\n\t" +
|
||||
"\tTOC file=\"" + getSourceFile() +"\n";
|
||||
//@formatter:on
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
/* ###
|
||||
* 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.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package help.validator.model;
|
||||
|
||||
import help.validator.LinkDatabase;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* A representation of the <tocref> tag, which is a way to reference a TOC item entry in
|
||||
* a TOC_Source.xml file other than the one in which the reference lives.
|
||||
*/
|
||||
public class TOCItemReference extends TOCItem implements Comparable<TOCItemReference> {
|
||||
|
||||
public TOCItemReference(TOCItem parentItem, Path sourceTOCFile, String ID, int lineNumber) {
|
||||
super(parentItem, sourceTOCFile, ID, lineNumber);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean validate(LinkDatabase linkDatabase) {
|
||||
TOCItemDefinition definition = linkDatabase.getTOCDefinition(this);
|
||||
if (definition != null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
TOCItemExternal external = linkDatabase.getTOCExternal(this);
|
||||
if (external != null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Overridden, as references cannot have targets, only their definitions */
|
||||
@Override
|
||||
public String getTargetAttribute() {
|
||||
throw new IllegalStateException("TOC reference item has not been validated!: " + this);
|
||||
}
|
||||
|
||||
/** Overridden, as if we get called, then something is in an invalid state, so generate special output */
|
||||
@Override
|
||||
public String generateTOCItemTag(LinkDatabase linkDatabase, boolean isInlineTag, int indentLevel) {
|
||||
String indent = INDENTS[indentLevel];
|
||||
|
||||
StringBuilder buildy = new StringBuilder();
|
||||
buildy.append(indent).append("<!-- WARNING: Unresolved reference ID\n");
|
||||
buildy.append(indent).append('\t').append(generateXMLString()).append("\n");
|
||||
buildy.append(indent).append("-->");
|
||||
return buildy.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(TOCItemReference other) {
|
||||
if (other == null) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
int fileComparison = getSourceFile().compareTo(other.getSourceFile());
|
||||
if (fileComparison != 0) {
|
||||
return fileComparison;
|
||||
}
|
||||
return getIDAttribute().compareTo(other.getIDAttribute());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return generateXMLString() + "\n\t[source file=\"" + getSourceFile() + "\" (line:" +
|
||||
getLineNumber() + ")]";
|
||||
}
|
||||
|
||||
private String generateXMLString() {
|
||||
return "<" + GhidraTOCFile.TOC_ITEM_REFERENCE + " id=\"" + getIDAttribute() + "\"/>";
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue