Candidate release of source code.

This commit is contained in:
Dan 2019-03-26 13:45:32 -04:00
parent db81e6b3b0
commit 79d8f164f8
12449 changed files with 2800756 additions and 16 deletions

View 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;
}
}
}

View 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;
}
}
}

View 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
}
}

View file

@ -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) {
}
}
}
}

View 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);
}
}
}

View 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);
}
};
}

View 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();
}
}
}

View file

@ -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();
}

View file

@ -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("&nbsp;\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("&nbsp;\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("&nbsp;");
}
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 + "()";
}
}
}

View file

@ -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");
}
}

View file

@ -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);
}
}
}
}

View file

@ -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 {
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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
}
}
}

View file

@ -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;
}
}

View file

@ -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());
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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();
}
}
}

View file

@ -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() + "\"";
}
}

View file

@ -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() + "\"";
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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();
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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 );
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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 );
}
}

View file

@ -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);
}
}

View file

@ -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();
}
}
}

View file

@ -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();
}
}

View file

@ -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
}
}

View 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();
}
}

View file

@ -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();
}
}
}

View file

@ -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;
}
}

View file

@ -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");
}
}
}

View file

@ -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();
}
}

View file

@ -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>&lt;IMG SRC='Icons.REFRESH_ICON /'&gt;</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]";
}
}

View file

@ -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);
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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() + "\"/>";
}
}