diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/extensions/ExtensionUtils.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/extensions/ExtensionUtils.java index cefae7610e..8ba9a32cd5 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/util/extensions/ExtensionUtils.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/util/extensions/ExtensionUtils.java @@ -41,8 +41,8 @@ import utility.application.ApplicationLayout; */ public class ExtensionUtils { - /** - * Magic number that identifies the first bytes of a ZIP archive. This is used to verify that a + /** + * Magic number that identifies the first bytes of a ZIP archive. This is used to verify that a * file is a zip rather than just checking the extension. */ private static final int ZIPFILE = 0x504b0304; @@ -87,7 +87,7 @@ public class ExtensionUtils { * Returns true if the given file or directory is a valid ghidra extension. *

* Note: This means that the zip or directory contains an extension.properties file. - * + * * @param file the zip or directory to inspect * @return true if the given file represents a valid extension */ @@ -96,12 +96,13 @@ public class ExtensionUtils { } public static boolean install(ExtensionDetails extension, File file, TaskMonitor monitor) { + boolean success = false; try { if (file.isFile()) { - return unzipToInstallationFolder(extension, file, monitor); + success = unzipToInstallationFolder(extension, file, monitor); } - return copyToInstallationFolder(file, monitor); + success = copyToInstallationFolder(extension, file, monitor); } catch (CancelledException e) { log.info("Extension installation cancelled by user"); @@ -110,7 +111,12 @@ public class ExtensionUtils { Msg.showError(ExtensionUtils.class, null, "Error Installing Extension", "Unexpected error installing extension", e); } - return false; + + if (success) { + extensions.add(extension); + } + + return success; } public static Set getActiveInstalledExtensions() { @@ -120,7 +126,7 @@ public class ExtensionUtils { /** * Returns all installed extensions. These are all the extensions found in * {@link ApplicationLayout#getExtensionInstallationDirs}. - * + * * @return set of installed extensions */ public static Set getInstalledExtensions() { @@ -197,17 +203,24 @@ public class ExtensionUtils { */ public static void reload() { log.trace("Clearing extensions cache"); - extensions = null; + clearCache(); getAllInstalledExtensions(); } + /** + * Clears any cached extensions. + */ + public static void clearCache() { + extensions = null; + } + /** * Returns all archive extensions. These are all the extensions found in * {@link ApplicationLayout#getExtensionArchiveDir}. This are added to an installation as * part of the build processes. *

* Archived extensions may be zip files and directories. - * + * * @return set of archive extensions */ public static Set getArchiveExtensions() { @@ -260,8 +273,7 @@ public class ExtensionUtils { } } catch (IOException e) { - log.error( - "Unable to read zip file to get extension properties: " + file, e); + log.error("Unable to read zip file to get extension properties: " + file, e); } return null; } @@ -276,10 +288,9 @@ public class ExtensionUtils { } if (results.contains(extension)) { - log.error( - "Skipping extension \"" + extension.getName() + "\" found at " + - extension.getInstallPath() + - ".\nArchived extension by that name already found."); + log.error("Skipping extension \"" + extension.getName() + "\" found at " + + extension.getInstallPath() + + ".\nArchived extension by that name already found."); } results.add(extension); } @@ -299,9 +310,8 @@ public class ExtensionUtils { extension.setArchivePath(extDir.getAbsolutePath()); if (results.contains(extension)) { - log.error( - "Skipping duplicate extension \"" + extension.getName() + "\" found at " + - extension.getInstallPath()); + log.error("Skipping duplicate extension \"" + extension.getName() + "\" found at " + + extension.getInstallPath()); } results.add(extension); } @@ -368,8 +378,7 @@ public class ExtensionUtils { Properties nextProperties = getProperties(zipFile, entry); if (nextProperties != null) { if (props != null) { - throw new IOException( - "Zip file contains multiple extension properties files"); + throw new IOException("Zip file contains multiple extension properties files"); } props = nextProperties; } @@ -403,7 +412,7 @@ public class ExtensionUtils { *

* Searching the child directories of a directory allows clients to pick an extension parent * directory that contains multiple extension directories. - * + * * @param installDir the directory that contains extension subdirectories * @return list of extension.properties files */ @@ -427,7 +436,7 @@ public class ExtensionUtils { /** * Returns an extension.properties or extension.properties.uninstalled file if the given * directory contains one. - * + * * @param dir the directory to search * @return the file, or null if doesn't exist */ @@ -448,7 +457,7 @@ public class ExtensionUtils { /** * Returns true if the given file is a valid .zip archive. - * + * * @param file the file to test * @return true if file is a valid zip */ @@ -483,28 +492,30 @@ public class ExtensionUtils { * location will be deleted. *

* Note: Any existing folder with the same name will be overwritten. - * + * + * @param extension the extension * @param sourceFolder the extension folder * @param monitor the task monitor * @return true if successful * @throws IOException if the delete or copy fails * @throws CancelledException if the user cancels the copy */ - private static boolean copyToInstallationFolder(File sourceFolder, TaskMonitor monitor) - throws IOException, CancelledException { + private static boolean copyToInstallationFolder(ExtensionDetails extension, File sourceFolder, + TaskMonitor monitor) throws IOException, CancelledException { log.trace("Copying extension from " + sourceFolder); ApplicationLayout layout = Application.getApplicationLayout(); ResourceFile installDir = layout.getExtensionInstallationDirs().get(0); File installDirRoot = installDir.getFile(false); - File newDir = new File(installDirRoot, sourceFolder.getName()); - if (hasExistingExtension(newDir, monitor)) { + File destinationFolder = new File(installDirRoot, sourceFolder.getName()); + if (hasExistingExtension(destinationFolder, monitor)) { return false; } - log.trace("Copying extension to " + newDir); - FileUtilities.copyDir(sourceFolder, newDir, monitor); + log.trace("Copying extension to " + destinationFolder); + FileUtilities.copyDir(sourceFolder, destinationFolder, monitor); + extension.setInstallDir(destinationFolder); return true; } @@ -514,7 +525,7 @@ public class ExtensionUtils { *

* Note: This method uses the Apache zip files since they keep track of permissions info; * the built-in java objects (e.g., ZipEntry) do not. - * + * * @param extension the extension * @param file the zip file to unpack * @param monitor the task monitor @@ -524,8 +535,7 @@ public class ExtensionUtils { * @throws IOException if error unzipping zip file */ private static boolean unzipToInstallationFolder(ExtensionDetails extension, File file, - TaskMonitor monitor) - throws CancelledException, IOException { + TaskMonitor monitor) throws CancelledException, IOException { log.trace("Unzipping extension from " + file); @@ -554,6 +564,8 @@ public class ExtensionUtils { } } } + + extension.setInstallDir(destinationFolder); return true; } @@ -600,7 +612,7 @@ public class ExtensionUtils { /** * Converts Unix permissions to a set of {@link PosixFilePermission}s. - * + * * @param unixMode integer representation of file permissions * @return set of POSIX file permissions */ diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/extensions/Extensions.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/extensions/Extensions.java index 0d553b6a51..b872cacc07 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/util/extensions/Extensions.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/util/extensions/Extensions.java @@ -38,12 +38,12 @@ public class Extensions { } /** - * Returns all extensions matching the given details + * Returns all extensions matching the given details * @param e the extension details to match * @return all matching extensions */ public List getMatchingExtensions(ExtensionDetails e) { - return extensionsByName.computeIfAbsent(e.getName(), name -> List.of()); + return extensionsByName.computeIfAbsent(e.getName(), name -> new ArrayList<>()); } /** @@ -68,8 +68,8 @@ public class Extensions { } /** - * Removes any extensions that have already been marked for removal. This should be called - * before any class loading has occurred. + * Removes any extensions that have already been marked for removal. This should be called + * before any class loading has occurred. */ void cleanupExtensionsMarkedForRemoval() { @@ -187,8 +187,7 @@ public class Extensions { for (int i = 1; i < extensions.size(); i++) { ExtensionDetails duplicate = extensions.get(i); log.info("Duplicate extension found '" + name + "'. Keeping extension from " + - loadedInstallDir + ". Skipping extension found at " + - duplicate.getInstallDir()); + loadedInstallDir + ". Skipping extension found at " + duplicate.getInstallDir()); } } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/extensions/ExtensionInstaller.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/extensions/ExtensionInstaller.java index 7490ebe015..400fdd3f56 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/extensions/ExtensionInstaller.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/extensions/ExtensionInstaller.java @@ -43,7 +43,7 @@ import utility.application.ApplicationLayout; *

  • createdOn (format: MM/dd/yyyy)
  • *
  • version
  • * - * + * *

    * Extensions may be installed/uninstalled by users at runtime, using the * {@link ExtensionTableProvider}. Installation consists of unzipping the extension archive to an @@ -57,7 +57,7 @@ public class ExtensionInstaller { /** * Installs the given extension file. This can be either an archive (zip) or a directory that * contains an extension.properties file. - * + * * @param file the extension to install * @return true if the extension was successfully installed */ @@ -121,8 +121,7 @@ public class ExtensionInstaller { String archivePath = extension.getArchivePath(); if (archivePath == null) { - log.error( - "Cannot install from archive; extension is missing archive path"); + log.error("Cannot install from archive; extension is missing archive path"); return false; } @@ -143,7 +142,7 @@ public class ExtensionInstaller { * Compares the given extension version to the current Ghidra version. If they are different, * then the user will be prompted to confirm the installation. This method will return true * if the versions match or the user has chosen to install anyway. - * + * * @param extension the extension * @return true if the versions match or the user has chosen to install anyway */ @@ -161,9 +160,7 @@ public class ExtensionInstaller { String message = "Extension version mismatch.\nName: " + extension.getName() + "Extension version: " + extVersion + ".\nGhidra version: " + appVersion + "."; int choice = OptionDialog.showOptionDialogWithCancelAsDefaultButton(null, - "Extension Version Mismatch", - message, - "Install Anyway"); + "Extension Version Mismatch", message, "Install Anyway"); if (choice != OptionDialog.OPTION_ONE) { log.info(removeNewlines(message + " Did not install")); return false; @@ -201,16 +198,12 @@ public class ExtensionInstaller { "Installed extension version: " + installedExtension.getVersion() + ".\n\n" + "To install, click 'Remove Existing', restart Ghidra, then install again."; int choice = OptionDialog.showOptionDialogWithCancelAsDefaultButton(null, - "Duplicate Extension", - message, - "Remove Existing"); + "Duplicate Extension", message, "Remove Existing"); String installPath = installedExtension.getInstallPath(); if (choice != OptionDialog.OPTION_ONE) { - log.info( - removeNewlines( - message + " Skipping installation. Original extension still installed: " + - installPath)); + log.info(removeNewlines(message + + " Skipping installation. Original extension still installed: " + installPath)); return true; } @@ -218,10 +211,9 @@ public class ExtensionInstaller { // At this point the user would like to replace the existing extension. We cannot delete // the existing extension, as it may be in use; mark it for removal. // - log.info( - removeNewlines( - message + " Installing new extension. Existing extension will be removed after " + - "restart: " + installPath)); + log.info(removeNewlines( + message + " Installing new extension. Existing extension will be removed after " + + "restart: " + installPath)); installedExtension.markForUninstall(); return true; } diff --git a/Ghidra/Framework/Project/src/test/java/ghidra/framework/project/extensions/ExtensionInstallerTest.java b/Ghidra/Framework/Project/src/test/java/ghidra/framework/project/extensions/ExtensionInstallerTest.java index 47a2858dd1..36e69b1ee6 100644 --- a/Ghidra/Framework/Project/src/test/java/ghidra/framework/project/extensions/ExtensionInstallerTest.java +++ b/Ghidra/Framework/Project/src/test/java/ghidra/framework/project/extensions/ExtensionInstallerTest.java @@ -60,6 +60,9 @@ public class ExtensionInstallerTest extends AbstractDockingTest { setErrorGUIEnabled(false); + // clear static caching of extensions + ExtensionUtils.clearCache(); + appLayout = Application.getApplicationLayout(); FileUtilities.deleteDir(appLayout.getExtensionArchiveDir().getFile(false)); @@ -290,7 +293,7 @@ public class ExtensionInstallerTest extends AbstractDockingTest { //================================================================================================= // Edge Cases -//================================================================================================= +//================================================================================================= @Test public void testInstallingNewExtension_SameName_NewVersion() throws Exception { @@ -317,8 +320,7 @@ public class ExtensionInstallerTest extends AbstractDockingTest { didInstall.set(ExtensionInstaller.install(extensionFolder2)); }); - DialogComponentProvider confirmDialog = - waitForDialogComponent("Duplicate Extension"); + DialogComponentProvider confirmDialog = waitForDialogComponent("Duplicate Extension"); pressButtonByText(confirmDialog, "Remove Existing"); waitForSwing(); @@ -333,7 +335,7 @@ public class ExtensionInstallerTest extends AbstractDockingTest { didInstall.set(ExtensionInstaller.install(extensionFolder2)); }); - // no longer an installed extension conflict; now we have a version mismatch + // no longer an installed extension conflict; now we have a version mismatch confirmDialog = waitForDialogComponent("Extension Version Mismatch"); pressButtonByText(confirmDialog, "Install Anyway"); @@ -363,8 +365,7 @@ public class ExtensionInstallerTest extends AbstractDockingTest { didInstall.set(ExtensionInstaller.install(extensionFolder2)); }); - DialogComponentProvider confirmDialog = - waitForDialogComponent("Duplicate Extension"); + DialogComponentProvider confirmDialog = waitForDialogComponent("Duplicate Extension"); pressButtonByText(confirmDialog, "Cancel"); waitForSwing(); @@ -397,8 +398,7 @@ public class ExtensionInstallerTest extends AbstractDockingTest { didInstall.set(ExtensionInstaller.install(extensionFolder2)); }); - DialogComponentProvider confirmDialog = - waitForDialogComponent("Duplicate Extension"); + DialogComponentProvider confirmDialog = waitForDialogComponent("Duplicate Extension"); pressButtonByText(confirmDialog, "Remove Existing"); waitForSwing(); @@ -426,14 +426,14 @@ public class ExtensionInstallerTest extends AbstractDockingTest { /* Create a zip file that looks something like this: - + / {Extension Name 1}/ extension.properties - + {Extension Name 2}/ extension.properties - + */ errorsExpected(() -> { @@ -446,9 +446,9 @@ public class ExtensionInstallerTest extends AbstractDockingTest { public void testInstallThenUninstallThenReinstallWhenExtensionNameDoesntMatchFolder() throws Exception { - // This tests a previous failure case where an extension could not be reinstalled if its + // This tests a previous failure case where an extension could not be reinstalled if its // name did not match the folder it was installed into. This could happen because the code - // that installed the extension did not match the code to clear the 'mark for uninstall' + // that installed the extension did not match the code to clear the 'mark for uninstall' // condition. String nameProperty = "ExtensionNamedFoo"; @@ -493,10 +493,9 @@ public class ExtensionInstallerTest extends AbstractDockingTest { private void assertExtensionNotInstalled(String name, String version) { Set extensions = ExtensionUtils.getInstalledExtensions(); - Optional match = - extensions.stream() - .filter(e -> e.getName().equals(name) && e.getVersion().equals(version)) - .findFirst(); + Optional match = extensions.stream() + .filter(e -> e.getName().equals(name) && e.getVersion().equals(version)) + .findFirst(); assertFalse("Extension should not be installed: '" + name + "'", match.isPresent()); } @@ -594,8 +593,7 @@ public class ExtensionInstallerTest extends AbstractDockingTest { } private File doCreateExternalExtensionInFolder(File externalFolder, String extensionName, - String version) - throws Exception { + String version) throws Exception { return doCreateExternalExtensionInFolder(externalFolder, extensionName, extensionName, version); } @@ -609,9 +607,8 @@ public class ExtensionInstallerTest extends AbstractDockingTest { version); } - private File doCreateExternalExtensionInFolder(File externalFolder, - String extensionName, String nameProperty, String version) - throws Exception { + private File doCreateExternalExtensionInFolder(File externalFolder, String extensionName, + String nameProperty, String version) throws Exception { ResourceFile root = new ResourceFile(new ResourceFile(externalFolder), extensionName); root.mkdir();