diff --git a/Ghidra/Framework/Docking/src/main/java/docking/help/GHelpHTMLEditorKit.java b/Ghidra/Framework/Docking/src/main/java/docking/help/GHelpHTMLEditorKit.java index 96c7df66a4..8f0484c207 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/help/GHelpHTMLEditorKit.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/help/GHelpHTMLEditorKit.java @@ -15,7 +15,6 @@ */ package docking.help; -import java.awt.Image; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.*; @@ -25,7 +24,6 @@ import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; -import javax.swing.ImageIcon; import javax.swing.JEditorPane; import javax.swing.event.HyperlinkEvent; import javax.swing.event.HyperlinkListener; @@ -380,20 +378,10 @@ public class GHelpHTMLEditorKit extends HTMLEditorKit { */ private class GHelpImageView extends ImageView { - private Image myImage; - public GHelpImageView(Element elem) { super(elem); } - @Override - public Image getImage() { - if (myImage != null) { - return myImage; - } - return super.getImage(); - } - @Override public URL getImageURL() { @@ -419,10 +407,7 @@ public class GHelpHTMLEditorKit extends HTMLEditorKit { return null; } - ImageIcon icon = iconProvider.getIcon(); - myImage = icon.getImage(); - - URL url = iconProvider.getUrl(); + URL url = iconProvider.getOrCreateUrl(); return url; } diff --git a/Ghidra/Framework/Generic/src/main/java/resources/IconProvider.java b/Ghidra/Framework/Generic/src/main/java/resources/IconProvider.java index 4db7a5492a..8198341755 100644 --- a/Ghidra/Framework/Generic/src/main/java/resources/IconProvider.java +++ b/Ghidra/Framework/Generic/src/main/java/resources/IconProvider.java @@ -15,17 +15,30 @@ */ package resources; +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; import java.net.URL; import javax.swing.ImageIcon; +import generic.Images; +import generic.util.image.ImageUtils; +import ghidra.util.Msg; + /** - * A class that knows how to provide an icon and the URL for that icon + * A class that knows how to provide an icon and the URL for that icon. If {@link #getUrl()} + * returns a non-null value, then that is the URL used to originally load the icon in this class. + * + *

If {@link #getUrl()} returns null, then {@link #getOrCreateUrl()} can be used to create a + * value URL by writing out the image for this class's icon. */ public class IconProvider { private ImageIcon icon; private URL url; + private URL tempUrl; + private boolean tempFileFailed; public IconProvider(ImageIcon icon, URL url) { this.icon = icon; @@ -36,11 +49,76 @@ public class IconProvider { return icon; } + public boolean isInvalid() { + return icon == null; // as long as we have an icon, we are valid, url or not + } + public URL getUrl() { return url; } - public boolean isInvalid() { - return icon == null || url == null; + /** + * Returns the value of {@link #getUrl()} if it is non-null. Otherwise, this class will + * attempt to create a temporary file containing the image of this class in order to return + * a URL for that temp file. If a temporary file could not be created, then the URL + * returned from this class will point to the + * {@link ResourceManager#getDefaultIcon() default icon}. + * + * @return the URL + */ + public URL getOrCreateUrl() { + if (url != null) { + return url; + } + + createTempUrlAsNeeded(); + return tempUrl; + } + + private void createTempUrlAsNeeded() { + if (testUrl(tempUrl)) { + return; + } + + tempUrl = createTempUrl(); + if (tempUrl == null) { + tempUrl = getDefaultUrl(); + } + } + + private URL createTempUrl() { + if (tempFileFailed) { + return null; // don't repeatedly attempt to create a temp file + } + + try { + File imageFile = File.createTempFile("temp.help.icon", null); + imageFile.deleteOnExit(); // don't let this linger + ImageUtils.writeFile(icon.getImage(), imageFile); + return imageFile.toURI().toURL(); + } + catch (IOException e) { + tempFileFailed = true; + Msg.error(this, "Unable to write temp image to display in help for " + + ResourceManager.getIconName(icon)); + } + return null; + } + + private boolean testUrl(URL testUrl) { + if (testUrl == null) { + return false; + } + + try { + return new File(testUrl.toURI()).exists(); + } + catch (URISyntaxException e) { + return false; + } + } + + private URL getDefaultUrl() { + return ResourceManager.getResource(Images.BOMB); } } diff --git a/Ghidra/Framework/Generic/src/main/java/resources/Icons.java b/Ghidra/Framework/Generic/src/main/java/resources/Icons.java index 4b45221f5f..4557609210 100644 --- a/Ghidra/Framework/Generic/src/main/java/resources/Icons.java +++ b/Ghidra/Framework/Generic/src/main/java/resources/Icons.java @@ -91,7 +91,7 @@ public class Icons { ResourceManager.loadImage("images/dialog-cancel.png", 10, 10), 6, 6))); public static final ImageIcon APPLY_BLOCKED_MATCH_ICON = ResourceManager.getImageIcon( new MultiIcon(ResourceManager.loadImage("images/kgpg.png"), new TranslateIcon( - ResourceManager.loadImage("images/checkmark_green.png", 12, 12), 4, 0))); + ResourceManager.loadImage("images/checkmark_green.gif", 12, 12), 4, 0))); /** * Returns true if the given string is a Java code snippet that references this class @@ -126,24 +126,6 @@ public class Icons { return new IconProvider(icon, url); } - /** - * Returns a URL for the given code snippet if it is a field reference on this class - * - * @param snippet the snippet of Java code that references a field of this class - * @return the URL; null if the snippet does not refer to a field of this class - */ - public static URL getUrlForIconsReference(String snippet) { - - String fieldName = getIconName(snippet); - if (fieldName == null) { - return null; - } - - ImageIcon icon = getIconByFieldName(fieldName); - URL url = getUrlFromIcon(icon); - return url; - } - private static String getIconName(String snippet) { if (!isIconsReference(snippet)) { return null; @@ -185,7 +167,7 @@ public class Icons { return url; } catch (MalformedURLException e) { - Msg.debug(Icons.class, "Unable to get URL for icon: " + description, e); + Msg.trace(Icons.class, "Unable to get URL for icon: " + description); return null; } diff --git a/Ghidra/Framework/Generic/src/main/java/resources/MultiIcon.java b/Ghidra/Framework/Generic/src/main/java/resources/MultiIcon.java index d9e89b47bd..706aaf7e0e 100644 --- a/Ghidra/Framework/Generic/src/main/java/resources/MultiIcon.java +++ b/Ghidra/Framework/Generic/src/main/java/resources/MultiIcon.java @@ -160,19 +160,18 @@ public class MultiIcon implements Icon { @Override public String toString() { - // return getClass().getSimpleName() + "[" + getIconNames() + "]"; - return getDescription(); + return getClass().getSimpleName() + "[" + getIconNames() + "]"; } -// private String getIconNames() { -// StringBuffer buffy = new StringBuffer(); -// for (Icon icon : iconList) { -// if (buffy.length() > 0) { -// buffy.append(", "); -// } -// buffy.append(ResourceManager.getIconName(icon)); -// } -// -// return buffy.toString(); -// } + private String getIconNames() { + StringBuffer buffy = new StringBuffer(); + for (Icon icon : iconList) { + if (buffy.length() > 0) { + buffy.append(", "); + } + buffy.append(ResourceManager.getIconName(icon)); + } + + return buffy.toString(); + } } diff --git a/Ghidra/Framework/Generic/src/main/java/resources/ResourceManager.java b/Ghidra/Framework/Generic/src/main/java/resources/ResourceManager.java index 910b2e65dd..8e3ddd0579 100644 --- a/Ghidra/Framework/Generic/src/main/java/resources/ResourceManager.java +++ b/Ghidra/Framework/Generic/src/main/java/resources/ResourceManager.java @@ -400,14 +400,18 @@ public class ResourceManager { } /** - * Get the name of this icon. If icon is an ImageIcon, its getDescription() is called to - * get the name + * Get the name of this icon. The value is usually going to be the URL from which the icon + * was loaded * * @param icon the icon for which the name is desired - * @return the name + * @return the name */ public static String getIconName(Icon icon) { String iconName = icon.toString(); + + if (icon instanceof FileBasedIcon) { + return ((FileBasedIcon) icon).getFilename(); + } if (icon instanceof ImageIcon) { iconName = ((ImageIcon) icon).getDescription(); } diff --git a/Ghidra/Framework/Help/src/main/java/help/HelpBuildUtils.java b/Ghidra/Framework/Help/src/main/java/help/HelpBuildUtils.java index 5370879310..a69d6c6128 100644 --- a/Ghidra/Framework/Help/src/main/java/help/HelpBuildUtils.java +++ b/Ghidra/Framework/Help/src/main/java/help/HelpBuildUtils.java @@ -25,6 +25,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import help.validator.location.*; +import resources.IconProvider; import resources.Icons; public class HelpBuildUtils { @@ -536,6 +537,7 @@ public class HelpBuildUtils { * locate files based upon relative references, specialized help system references (i.e., * help/topics/...), and absolute URLs. * + * @param sourceFile the source file path of the image reference * @param ref the reference text * @return an absolute path; null if the URI is remote * @throws URISyntaxException @@ -544,15 +546,21 @@ public class HelpBuildUtils { throws URISyntaxException { if (Icons.isIconsReference(ref)) { + // help system syntax: - URL url = Icons.getUrlForIconsReference(ref); - if (url == null) { + IconProvider iconProvider = Icons.getIconForIconsReference(ref); + if (iconProvider == null || iconProvider.isInvalid()) { // bad icon name return ImageLocation.createInvalidRuntimeLocation(sourceFile, ref); } - URI resolved = url.toURI(); - Path path = toPath(resolved); + URL url = iconProvider.getUrl(); + URI resolved = null; + Path path = null; + if (url != null) { // we may have an icon with an invalid URL (e.g., a MultiIcon) + resolved = url.toURI(); + path = toPath(resolved); + } return ImageLocation.createRuntimeLocation(sourceFile, ref, resolved, path); } diff --git a/Ghidra/Framework/Help/src/main/java/help/ImageLocation.java b/Ghidra/Framework/Help/src/main/java/help/ImageLocation.java index fd3ffd0145..53d1a48e49 100644 --- a/Ghidra/Framework/Help/src/main/java/help/ImageLocation.java +++ b/Ghidra/Framework/Help/src/main/java/help/ImageLocation.java @@ -21,6 +21,9 @@ 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. + * + *

Some images are represented by 'in memory' or 'runtime' values that do not have a valid + * url. */ public class ImageLocation { @@ -30,8 +33,13 @@ public class ImageLocation { private Path resolvedPath; private URI resolvedUri; private boolean isRemote; + + /** An image that is taken from an image loaded by a Java class (e.g., Icons.XYZ_ICON) */ private boolean isRuntime; + /** A 'runtime' image that could not be located */ + private boolean invalidRuntimeImage; + public static ImageLocation createLocalLocation(Path sourceFile, String imageSrc, URI resolvedUri, Path resolvedPath) { @@ -61,6 +69,7 @@ public class ImageLocation { l.resolvedPath = null; l.isRemote = false; l.isRuntime = true; + l.invalidRuntimeImage = true; return l; } @@ -84,48 +93,28 @@ public class ImageLocation { 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; + public boolean isInvalidRuntimeImage() { + return invalidRuntimeImage; } @Override diff --git a/Ghidra/Framework/Help/src/main/java/help/validator/JavaHelpValidator.java b/Ghidra/Framework/Help/src/main/java/help/validator/JavaHelpValidator.java index 09c5e392b5..fc1aa68b1f 100644 --- a/Ghidra/Framework/Help/src/main/java/help/validator/JavaHelpValidator.java +++ b/Ghidra/Framework/Help/src/main/java/help/validator/JavaHelpValidator.java @@ -120,14 +120,22 @@ public class JavaHelpValidator { return; // don't even try to verify a remote URL } - Path imagePath = img.getImageFile(); - if (imagePath == null) { - unresolvedLinks.add(new NonExistentIMGFileInvalidLink(img)); + if (img.isRuntime()) { + + // + // The tool will load this image at runtime--don't perform normal validation + // (runtime means an icon to be loaded from a Java file) + // + if (img.isInvalid()) { + unresolvedLinks.add(new InvalidRuntimeIMGFileInvalidLink(img)); + return; + } return; } - if (img.isRuntime()) { - // the tool will load this image at runtime--don't perform normal validate + Path imagePath = img.getImageFile(); + if (imagePath == null) { + unresolvedLinks.add(new NonExistentIMGFileInvalidLink(img)); return; } diff --git a/Ghidra/Framework/Help/src/main/java/help/validator/UnusedHelpImageFileFinder.java b/Ghidra/Framework/Help/src/main/java/help/validator/UnusedHelpImageFileFinder.java index 59f7ffbb5e..483dfb6316 100644 --- a/Ghidra/Framework/Help/src/main/java/help/validator/UnusedHelpImageFileFinder.java +++ b/Ghidra/Framework/Help/src/main/java/help/validator/UnusedHelpImageFileFinder.java @@ -1,6 +1,5 @@ /* ### * 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. @@ -16,22 +15,25 @@ */ 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.*; +import org.apache.commons.lang3.StringUtils; + +import help.HelpBuildUtils; +import help.validator.location.HelpModuleLocation; +import help.validator.model.IMG; +import util.CollectionUtils; + public class UnusedHelpImageFileFinder { + private static final String HELP_PATHS_OPTION = "-hp"; // taken from GHelpBuilder private static final String DEBUG_SWITCH = "-debug"; - private static List moduleHelpPaths; + private static List moduleHelpPaths = new ArrayList<>(); private static boolean debugEnabled = false; private SortedSet unusedFiles; @@ -78,24 +80,20 @@ public class UnusedHelpImageFileFinder { } public SortedSet getUnusedImages() { - return new TreeSet(unusedFiles); + return new TreeSet<>(unusedFiles); } private static SortedSet getUnusedFiles(Collection referencedIMGs, Collection imageFiles) { - Map fileToIMGMap = new HashMap(); + Map fileToIMGMap = new HashMap<>(); for (IMG img : referencedIMGs) { fileToIMGMap.put(img.getImageFile(), img); } - SortedSet set = new TreeSet(new Comparator() { - @Override - public int compare(Path f1, Path f2) { - return f1.toUri().toString().toLowerCase().compareTo( - f2.toUri().toString().toLowerCase()); - } - }); + SortedSet set = + new TreeSet<>((f1, f2) -> f1.toUri().toString().toLowerCase().compareTo( + f2.toUri().toString().toLowerCase())); for (Path file : imageFiles) { IMG img = fileToIMGMap.get(file); if (img == null && !isExcludedImageFile(file)) { @@ -111,8 +109,9 @@ public class UnusedHelpImageFileFinder { return absolutePath.indexOf("help/shared/") != -1; } - private static Collection getReferencedIMGs(Collection helpCollections) { - Set set = new HashSet(); + private static Collection getReferencedIMGs( + Collection helpCollections) { + Set set = new HashSet<>(); for (HelpModuleLocation help : helpCollections) { Collection IMGs = help.getAllIMGs(); set.addAll(IMGs); @@ -122,7 +121,7 @@ public class UnusedHelpImageFileFinder { private static Collection getAllImagesOnDisk( Collection helpDirectories) { - List files = new ArrayList(); + List files = new ArrayList<>(); for (HelpModuleLocation help : helpDirectories) { Path helpDir = help.getHelpLocation(); gatherImageFiles(helpDir, files); @@ -155,10 +154,10 @@ public class UnusedHelpImageFileFinder { private static List collectHelp() { debug("Parsing help dirs..."); - List helpCollections = - new ArrayList(moduleHelpPaths.size()); + List helpCollections = new ArrayList<>(moduleHelpPaths.size()); for (String helpDirName : moduleHelpPaths) { - // 1) Make sure the help directory exists + + // Make sure the help directory exists File helpDirectoryFile = null; try { helpDirectoryFile = new File(helpDirName).getCanonicalFile(); @@ -172,14 +171,8 @@ public class UnusedHelpImageFileFinder { 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 + // Create the help directory helpCollections.add(HelpBuildUtils.toLocation(helpDirectoryFile)); } @@ -188,8 +181,8 @@ public class UnusedHelpImageFileFinder { private static void debug(String string) { if (debugEnabled) { - System.out.println("[" + UnusedHelpImageFileFinder.class.getSimpleName() + "] " + - string); + System.out.println( + "[" + UnusedHelpImageFileFinder.class.getSimpleName() + "] " + string); } } @@ -197,7 +190,7 @@ public class UnusedHelpImageFileFinder { StringBuilder buffy = new StringBuilder(); errorMessage("Usage:\n"); - buffy.append(" [-debug]"); + buffy.append("-hp path1[-hp path2 -hp path3 ...]> [-debug]"); errorMessage(buffy.toString()); } @@ -209,23 +202,53 @@ public class UnusedHelpImageFileFinder { System.exit(1); } - List argList = Arrays.asList(args); - - // get module directory paths - String modulePathsString = argList.get(args.length - 1); - moduleHelpPaths = new ArrayList(); - StringTokenizer tokenizer = new StringTokenizer(modulePathsString, File.pathSeparator); - while (tokenizer.hasMoreTokens()) { - moduleHelpPaths.add(tokenizer.nextToken()); + List argList = CollectionUtils.asList(args); + int debugIndex = argList.indexOf(DEBUG_SWITCH); + if (debugIndex > -1) { + debugEnabled = true; + argList.remove(debugIndex); } + + Map mapped = new TreeMap<>(); + for (int i = 0; i < argList.size(); i++) { + mapped.put(i, argList.get(i)); + } + + for (int i = 0; i < argList.size(); i++) { + String opt = argList.get(i); + if (opt.equals(HELP_PATHS_OPTION)) { + + if (i >= argList.size()) { + errorMessage(HELP_PATHS_OPTION + " requires an argument"); + printUsage(); + System.exit(1); + } + + mapped.remove(i); + String paths = mapped.remove(++i); + if (StringUtils.isBlank(paths)) { + errorMessage(HELP_PATHS_OPTION + " requires an argument"); + printUsage(); + System.exit(1); + } + + // each entry should be just one value, but handle multiple paths anyway + for (String p : paths.split(File.pathSeparator)) { + moduleHelpPaths.add(p); + } + } + } + if (moduleHelpPaths.size() == 0) { - errorMessage("Missing molule help path(s) argument - it must be last in the arg list"); + errorMessage( + "Missing molule help path(s) arguments - actual arguments:\n\t'" + argList + "'"); printUsage(); System.exit(1); } - int debugIndex = argList.indexOf(DEBUG_SWITCH); - debugEnabled = debugIndex != -1; + if (!mapped.isEmpty()) { + errorMessage("Ignoring unknown arguments: " + mapped.values()); + } } private static void errorMessage(String message) { @@ -233,7 +256,7 @@ public class UnusedHelpImageFileFinder { } private static void errorMessage(String message, Throwable t) { - System.err.println("[" + GHelpBuilder.class.getSimpleName() + "] " + message); + System.err.println("[" + UnusedHelpImageFileFinder.class.getSimpleName() + "] " + message); if (t != null) { t.printStackTrace(); } diff --git a/Ghidra/Framework/Help/src/main/java/help/validator/links/InvalidRuntimeIMGFileInvalidLink.java b/Ghidra/Framework/Help/src/main/java/help/validator/links/InvalidRuntimeIMGFileInvalidLink.java new file mode 100644 index 0000000000..e6b8c11f5e --- /dev/null +++ b/Ghidra/Framework/Help/src/main/java/help/validator/links/InvalidRuntimeIMGFileInvalidLink.java @@ -0,0 +1,32 @@ +/* ### + * 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; + +/** + * A link that represents the case where the HTML tried to reference a runtime Java image, but + * that value is not found + */ +public class InvalidRuntimeIMGFileInvalidLink extends InvalidIMGLink { + + private static final String MESSAGE = + "Runtime image not found (e.g., Icons.XYZ_ICON not found)"; + + public InvalidRuntimeIMGFileInvalidLink(IMG img) { + super(img, MESSAGE); + } +} diff --git a/Ghidra/Framework/Help/src/main/java/help/validator/model/IMG.java b/Ghidra/Framework/Help/src/main/java/help/validator/model/IMG.java index 96a697ac26..3e275f9b15 100644 --- a/Ghidra/Framework/Help/src/main/java/help/validator/model/IMG.java +++ b/Ghidra/Framework/Help/src/main/java/help/validator/model/IMG.java @@ -79,6 +79,10 @@ public class IMG implements Comparable { return imageLocation.isRuntime(); } + public boolean isInvalid() { + return imageLocation.isInvalidRuntimeImage(); + } + public Path getImageFile() { return imgFile; } diff --git a/gradle/helpProject.gradle b/gradle/helpProject.gradle index f77f7e7391..49cf074f63 100644 --- a/gradle/helpProject.gradle +++ b/gradle/helpProject.gradle @@ -141,10 +141,42 @@ task buildHelp(type: JavaExec, dependsOn: indexHelp) { } + +// Task for finding unused images that are not referenced from Ghidra help files +task findUnusedHelp(type: JavaExec) { + group rootProject.GHIDRA_GROUP + description " Finds unused help images for this module. [gradle/helpProject.gradle]\n" + + File helpRootDir = file('src/main/help/help') + File outputDir = file('build/help/main/help') + + dependsOn configurations.helpPath + + inputs.dir helpRootDir + + classpath = sourceSets.helpIndex.runtimeClasspath + + main = 'help.validator.UnusedHelpImageFileFinder' + + args '-debug' // print debug info + + doFirst { + // this modules runtime classpath (contains jhall.jar) + classpath project(':Help').sourceSets.main.runtimeClasspath + + // the current help dir to process + args "-hp" + args "${helpRootDir.absolutePath}" + } + +} + + + // include the help into the module's jar jar { from "build/help/main" // include the generated help index files - from "src/main/help" // include the help source files + from "src/main/help" // include the help source files } // build the help whenever this module's jar file is built