mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-05 02:39:44 +02:00

These were found using the command below searching for duplicated words, and manually going through the results to remove the false positives and reword the true positives. Sometimes I removed the doubled word and sometimes I replaced the duplicated word. The grep command: grep -nIEr '\b([a-zA-Z]+)[[:space:]*]+\1\b' ./Ghidra
330 lines
10 KiB
Java
330 lines
10 KiB
Java
/* ###
|
|
* IP: GHIDRA
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
package help;
|
|
|
|
import java.io.*;
|
|
import java.nio.file.Files;
|
|
import java.nio.file.Path;
|
|
import java.util.*;
|
|
|
|
import help.validator.LinkDatabase;
|
|
import help.validator.model.*;
|
|
|
|
/**
|
|
* 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.getExternalTocItemsById().values()) {
|
|
addExternalTOCItem(external);
|
|
}
|
|
|
|
for (TOCItemDefinition definition : tocItemProvider.getTocDefinitionsByID().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;
|
|
}
|
|
|
|
OverlayNode newRootNode = new OverlayNode(null, rootItem);
|
|
buildChildren(newRootNode);
|
|
|
|
//
|
|
// The parent to children map is cleared as nodes are created. The map is populated by
|
|
// adding any references to the 'parent' key as they are loaded from the help files.
|
|
// As we build nodes, starting at the root, we will create child nodes for those that
|
|
// reference the 'parent' key. If the map is empty, then it means we never built a
|
|
// node for the 'parent' key, which means we never found a help file containing the
|
|
// definition for that key.
|
|
//
|
|
if (!parentToChildrenMap.isEmpty()) {
|
|
throw new RuntimeException("Unresolved definitions in tree! - " + parentToChildrenMap);
|
|
}
|
|
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()) {
|
|
|
|
validateChildrenSortGroups();
|
|
|
|
for (OverlayNode node : children) {
|
|
node.print(sourceFileID, writer, indentLevel + 1);
|
|
}
|
|
writer.println(item.generateEndTag(indentLevel));
|
|
}
|
|
}
|
|
|
|
// Note: this method will validate all TOC files for a given module, including its
|
|
// dependencies. If module A has a dependent B, A and B will be checked for re-used sort
|
|
// groups. This will not get sibling TOC issues that are found at runtime. In this case,
|
|
// considering module A with dependents B1 and B2, in separate unrelated modules, then if
|
|
// B1 and B2 share a sort group, this method will not detected that. This is because when
|
|
// building either B1 or B2, the other module is not available, since it is not a dependent
|
|
// module.
|
|
private void validateChildrenSortGroups() {
|
|
Map<String, OverlayNode> sortPreferences = new HashMap<>();
|
|
for (OverlayNode child : children) {
|
|
String sortPreference = child.item.getSortPreference();
|
|
OverlayNode existingNode = sortPreferences.get(sortPreference);
|
|
if (existingNode != null) {
|
|
|
|
String message = """
|
|
Found multiple child nodes with the same 'sortgroup' value.
|
|
Sort values must be unique. Duplicated value: '%s'
|
|
Parent: %s
|
|
First child: %s
|
|
Second child: %s
|
|
""".formatted(sortPreference, toString(), existingNode.toString(),
|
|
child.toString());
|
|
throw new RuntimeException(message);
|
|
}
|
|
|
|
sortPreferences.put(sortPreference, child);
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
int result = text1.compareTo(text2);
|
|
if (result != 0) {
|
|
return result;
|
|
}
|
|
|
|
// At this point we have 2 nodes that have the same text attribute as children of
|
|
// a <TOCDEF> tag. This is OK, as we use text only for sorting, but not for the
|
|
// display text. Use the ID as a tie-breaker for sorting, which should provide
|
|
// sorting consistency.
|
|
return o1.getIDAttribute().compareTo(o2.getIDAttribute()); // ID should not be null
|
|
}
|
|
};
|
|
}
|