Merge remote-tracking branch 'origin/GP-0-dragonmacher-test-fixes-11-24-23'

This commit is contained in:
Ryan Kurtz 2023-11-24 14:14:21 -05:00
commit c225fac124
4 changed files with 81 additions and 81 deletions

View file

@ -41,8 +41,8 @@ import utility.application.ApplicationLayout;
*/ */
public class ExtensionUtils { 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. * file is a zip rather than just checking the extension.
*/ */
private static final int ZIPFILE = 0x504b0304; 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. * Returns true if the given file or directory is a valid ghidra extension.
* <p> * <p>
* Note: This means that the zip or directory contains an extension.properties file. * Note: This means that the zip or directory contains an extension.properties file.
* *
* @param file the zip or directory to inspect * @param file the zip or directory to inspect
* @return true if the given file represents a valid extension * @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) { public static boolean install(ExtensionDetails extension, File file, TaskMonitor monitor) {
boolean success = false;
try { try {
if (file.isFile()) { 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) { catch (CancelledException e) {
log.info("Extension installation cancelled by user"); log.info("Extension installation cancelled by user");
@ -110,7 +111,12 @@ public class ExtensionUtils {
Msg.showError(ExtensionUtils.class, null, "Error Installing Extension", Msg.showError(ExtensionUtils.class, null, "Error Installing Extension",
"Unexpected error installing extension", e); "Unexpected error installing extension", e);
} }
return false;
if (success) {
extensions.add(extension);
}
return success;
} }
public static Set<ExtensionDetails> getActiveInstalledExtensions() { public static Set<ExtensionDetails> getActiveInstalledExtensions() {
@ -120,7 +126,7 @@ public class ExtensionUtils {
/** /**
* Returns all installed extensions. These are all the extensions found in * Returns all installed extensions. These are all the extensions found in
* {@link ApplicationLayout#getExtensionInstallationDirs}. * {@link ApplicationLayout#getExtensionInstallationDirs}.
* *
* @return set of installed extensions * @return set of installed extensions
*/ */
public static Set<ExtensionDetails> getInstalledExtensions() { public static Set<ExtensionDetails> getInstalledExtensions() {
@ -197,17 +203,24 @@ public class ExtensionUtils {
*/ */
public static void reload() { public static void reload() {
log.trace("Clearing extensions cache"); log.trace("Clearing extensions cache");
extensions = null; clearCache();
getAllInstalledExtensions(); getAllInstalledExtensions();
} }
/**
* Clears any cached extensions.
*/
public static void clearCache() {
extensions = null;
}
/** /**
* Returns all archive extensions. These are all the extensions found in * Returns all archive extensions. These are all the extensions found in
* {@link ApplicationLayout#getExtensionArchiveDir}. This are added to an installation as * {@link ApplicationLayout#getExtensionArchiveDir}. This are added to an installation as
* part of the build processes. * part of the build processes.
* <p> * <p>
* Archived extensions may be zip files and directories. * Archived extensions may be zip files and directories.
* *
* @return set of archive extensions * @return set of archive extensions
*/ */
public static Set<ExtensionDetails> getArchiveExtensions() { public static Set<ExtensionDetails> getArchiveExtensions() {
@ -260,8 +273,7 @@ public class ExtensionUtils {
} }
} }
catch (IOException e) { catch (IOException e) {
log.error( log.error("Unable to read zip file to get extension properties: " + file, e);
"Unable to read zip file to get extension properties: " + file, e);
} }
return null; return null;
} }
@ -276,10 +288,9 @@ public class ExtensionUtils {
} }
if (results.contains(extension)) { if (results.contains(extension)) {
log.error( log.error("Skipping extension \"" + extension.getName() + "\" found at " +
"Skipping extension \"" + extension.getName() + "\" found at " + extension.getInstallPath() +
extension.getInstallPath() + ".\nArchived extension by that name already found.");
".\nArchived extension by that name already found.");
} }
results.add(extension); results.add(extension);
} }
@ -299,9 +310,8 @@ public class ExtensionUtils {
extension.setArchivePath(extDir.getAbsolutePath()); extension.setArchivePath(extDir.getAbsolutePath());
if (results.contains(extension)) { if (results.contains(extension)) {
log.error( log.error("Skipping duplicate extension \"" + extension.getName() + "\" found at " +
"Skipping duplicate extension \"" + extension.getName() + "\" found at " + extension.getInstallPath());
extension.getInstallPath());
} }
results.add(extension); results.add(extension);
} }
@ -368,8 +378,7 @@ public class ExtensionUtils {
Properties nextProperties = getProperties(zipFile, entry); Properties nextProperties = getProperties(zipFile, entry);
if (nextProperties != null) { if (nextProperties != null) {
if (props != null) { if (props != null) {
throw new IOException( throw new IOException("Zip file contains multiple extension properties files");
"Zip file contains multiple extension properties files");
} }
props = nextProperties; props = nextProperties;
} }
@ -403,7 +412,7 @@ public class ExtensionUtils {
* <p> * <p>
* Searching the child directories of a directory allows clients to pick an extension parent * Searching the child directories of a directory allows clients to pick an extension parent
* directory that contains multiple extension directories. * directory that contains multiple extension directories.
* *
* @param installDir the directory that contains extension subdirectories * @param installDir the directory that contains extension subdirectories
* @return list of extension.properties files * @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 * Returns an extension.properties or extension.properties.uninstalled file if the given
* directory contains one. * directory contains one.
* *
* @param dir the directory to search * @param dir the directory to search
* @return the file, or null if doesn't exist * @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. * Returns true if the given file is a valid .zip archive.
* *
* @param file the file to test * @param file the file to test
* @return true if file is a valid zip * @return true if file is a valid zip
*/ */
@ -483,28 +492,30 @@ public class ExtensionUtils {
* location will be deleted. * location will be deleted.
* <p> * <p>
* Note: Any existing folder with the same name will be overwritten. * Note: Any existing folder with the same name will be overwritten.
* *
* @param extension the extension
* @param sourceFolder the extension folder * @param sourceFolder the extension folder
* @param monitor the task monitor * @param monitor the task monitor
* @return true if successful * @return true if successful
* @throws IOException if the delete or copy fails * @throws IOException if the delete or copy fails
* @throws CancelledException if the user cancels the copy * @throws CancelledException if the user cancels the copy
*/ */
private static boolean copyToInstallationFolder(File sourceFolder, TaskMonitor monitor) private static boolean copyToInstallationFolder(ExtensionDetails extension, File sourceFolder,
throws IOException, CancelledException { TaskMonitor monitor) throws IOException, CancelledException {
log.trace("Copying extension from " + sourceFolder); log.trace("Copying extension from " + sourceFolder);
ApplicationLayout layout = Application.getApplicationLayout(); ApplicationLayout layout = Application.getApplicationLayout();
ResourceFile installDir = layout.getExtensionInstallationDirs().get(0); ResourceFile installDir = layout.getExtensionInstallationDirs().get(0);
File installDirRoot = installDir.getFile(false); File installDirRoot = installDir.getFile(false);
File newDir = new File(installDirRoot, sourceFolder.getName()); File destinationFolder = new File(installDirRoot, sourceFolder.getName());
if (hasExistingExtension(newDir, monitor)) { if (hasExistingExtension(destinationFolder, monitor)) {
return false; return false;
} }
log.trace("Copying extension to " + newDir); log.trace("Copying extension to " + destinationFolder);
FileUtilities.copyDir(sourceFolder, newDir, monitor); FileUtilities.copyDir(sourceFolder, destinationFolder, monitor);
extension.setInstallDir(destinationFolder);
return true; return true;
} }
@ -514,7 +525,7 @@ public class ExtensionUtils {
* <p> * <p>
* Note: This method uses the Apache zip files since they keep track of permissions info; * 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. * the built-in java objects (e.g., ZipEntry) do not.
* *
* @param extension the extension * @param extension the extension
* @param file the zip file to unpack * @param file the zip file to unpack
* @param monitor the task monitor * @param monitor the task monitor
@ -524,8 +535,7 @@ public class ExtensionUtils {
* @throws IOException if error unzipping zip file * @throws IOException if error unzipping zip file
*/ */
private static boolean unzipToInstallationFolder(ExtensionDetails extension, File file, private static boolean unzipToInstallationFolder(ExtensionDetails extension, File file,
TaskMonitor monitor) TaskMonitor monitor) throws CancelledException, IOException {
throws CancelledException, IOException {
log.trace("Unzipping extension from " + file); log.trace("Unzipping extension from " + file);
@ -554,6 +564,8 @@ public class ExtensionUtils {
} }
} }
} }
extension.setInstallDir(destinationFolder);
return true; return true;
} }
@ -600,7 +612,7 @@ public class ExtensionUtils {
/** /**
* Converts Unix permissions to a set of {@link PosixFilePermission}s. * Converts Unix permissions to a set of {@link PosixFilePermission}s.
* *
* @param unixMode integer representation of file permissions * @param unixMode integer representation of file permissions
* @return set of POSIX file permissions * @return set of POSIX file permissions
*/ */

View file

@ -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 * @param e the extension details to match
* @return all matching extensions * @return all matching extensions
*/ */
public List<ExtensionDetails> getMatchingExtensions(ExtensionDetails e) { public List<ExtensionDetails> 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 * Removes any extensions that have already been marked for removal. This should be called
* before any class loading has occurred. * before any class loading has occurred.
*/ */
void cleanupExtensionsMarkedForRemoval() { void cleanupExtensionsMarkedForRemoval() {
@ -187,8 +187,7 @@ public class Extensions {
for (int i = 1; i < extensions.size(); i++) { for (int i = 1; i < extensions.size(); i++) {
ExtensionDetails duplicate = extensions.get(i); ExtensionDetails duplicate = extensions.get(i);
log.info("Duplicate extension found '" + name + "'. Keeping extension from " + log.info("Duplicate extension found '" + name + "'. Keeping extension from " +
loadedInstallDir + ". Skipping extension found at " + loadedInstallDir + ". Skipping extension found at " + duplicate.getInstallDir());
duplicate.getInstallDir());
} }
} }

View file

@ -43,7 +43,7 @@ import utility.application.ApplicationLayout;
* <li>createdOn (format: MM/dd/yyyy)</li> * <li>createdOn (format: MM/dd/yyyy)</li>
* <li>version</li> * <li>version</li>
* </ul> * </ul>
* *
* <p> * <p>
* Extensions may be installed/uninstalled by users at runtime, using the * Extensions may be installed/uninstalled by users at runtime, using the
* {@link ExtensionTableProvider}. Installation consists of unzipping the extension archive to an * {@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 * Installs the given extension file. This can be either an archive (zip) or a directory that
* contains an extension.properties file. * contains an extension.properties file.
* *
* @param file the extension to install * @param file the extension to install
* @return true if the extension was successfully installed * @return true if the extension was successfully installed
*/ */
@ -121,8 +121,7 @@ public class ExtensionInstaller {
String archivePath = extension.getArchivePath(); String archivePath = extension.getArchivePath();
if (archivePath == null) { if (archivePath == null) {
log.error( log.error("Cannot install from archive; extension is missing archive path");
"Cannot install from archive; extension is missing archive path");
return false; return false;
} }
@ -143,7 +142,7 @@ public class ExtensionInstaller {
* Compares the given extension version to the current Ghidra version. If they are different, * 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 * 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. * if the versions match or the user has chosen to install anyway.
* *
* @param extension the extension * @param extension the extension
* @return true if the versions match or the user has chosen to install anyway * @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() + String message = "Extension version mismatch.\nName: " + extension.getName() +
"Extension version: " + extVersion + ".\nGhidra version: " + appVersion + "."; "Extension version: " + extVersion + ".\nGhidra version: " + appVersion + ".";
int choice = OptionDialog.showOptionDialogWithCancelAsDefaultButton(null, int choice = OptionDialog.showOptionDialogWithCancelAsDefaultButton(null,
"Extension Version Mismatch", "Extension Version Mismatch", message, "Install Anyway");
message,
"Install Anyway");
if (choice != OptionDialog.OPTION_ONE) { if (choice != OptionDialog.OPTION_ONE) {
log.info(removeNewlines(message + " Did not install")); log.info(removeNewlines(message + " Did not install"));
return false; return false;
@ -201,16 +198,12 @@ public class ExtensionInstaller {
"Installed extension version: " + installedExtension.getVersion() + ".\n\n" + "Installed extension version: " + installedExtension.getVersion() + ".\n\n" +
"To install, click 'Remove Existing', restart Ghidra, then install again."; "To install, click 'Remove Existing', restart Ghidra, then install again.";
int choice = OptionDialog.showOptionDialogWithCancelAsDefaultButton(null, int choice = OptionDialog.showOptionDialogWithCancelAsDefaultButton(null,
"Duplicate Extension", "Duplicate Extension", message, "Remove Existing");
message,
"Remove Existing");
String installPath = installedExtension.getInstallPath(); String installPath = installedExtension.getInstallPath();
if (choice != OptionDialog.OPTION_ONE) { if (choice != OptionDialog.OPTION_ONE) {
log.info( log.info(removeNewlines(message +
removeNewlines( " Skipping installation. Original extension still installed: " + installPath));
message + " Skipping installation. Original extension still installed: " +
installPath));
return true; return true;
} }
@ -218,10 +211,9 @@ public class ExtensionInstaller {
// At this point the user would like to replace the existing extension. We cannot delete // 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. // the existing extension, as it may be in use; mark it for removal.
// //
log.info( log.info(removeNewlines(
removeNewlines( message + " Installing new extension. Existing extension will be removed after " +
message + " Installing new extension. Existing extension will be removed after " + "restart: " + installPath));
"restart: " + installPath));
installedExtension.markForUninstall(); installedExtension.markForUninstall();
return true; return true;
} }

View file

@ -60,6 +60,9 @@ public class ExtensionInstallerTest extends AbstractDockingTest {
setErrorGUIEnabled(false); setErrorGUIEnabled(false);
// clear static caching of extensions
ExtensionUtils.clearCache();
appLayout = Application.getApplicationLayout(); appLayout = Application.getApplicationLayout();
FileUtilities.deleteDir(appLayout.getExtensionArchiveDir().getFile(false)); FileUtilities.deleteDir(appLayout.getExtensionArchiveDir().getFile(false));
@ -290,7 +293,7 @@ public class ExtensionInstallerTest extends AbstractDockingTest {
//================================================================================================= //=================================================================================================
// Edge Cases // Edge Cases
//================================================================================================= //=================================================================================================
@Test @Test
public void testInstallingNewExtension_SameName_NewVersion() throws Exception { public void testInstallingNewExtension_SameName_NewVersion() throws Exception {
@ -317,8 +320,7 @@ public class ExtensionInstallerTest extends AbstractDockingTest {
didInstall.set(ExtensionInstaller.install(extensionFolder2)); didInstall.set(ExtensionInstaller.install(extensionFolder2));
}); });
DialogComponentProvider confirmDialog = DialogComponentProvider confirmDialog = waitForDialogComponent("Duplicate Extension");
waitForDialogComponent("Duplicate Extension");
pressButtonByText(confirmDialog, "Remove Existing"); pressButtonByText(confirmDialog, "Remove Existing");
waitForSwing(); waitForSwing();
@ -333,7 +335,7 @@ public class ExtensionInstallerTest extends AbstractDockingTest {
didInstall.set(ExtensionInstaller.install(extensionFolder2)); 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"); confirmDialog = waitForDialogComponent("Extension Version Mismatch");
pressButtonByText(confirmDialog, "Install Anyway"); pressButtonByText(confirmDialog, "Install Anyway");
@ -363,8 +365,7 @@ public class ExtensionInstallerTest extends AbstractDockingTest {
didInstall.set(ExtensionInstaller.install(extensionFolder2)); didInstall.set(ExtensionInstaller.install(extensionFolder2));
}); });
DialogComponentProvider confirmDialog = DialogComponentProvider confirmDialog = waitForDialogComponent("Duplicate Extension");
waitForDialogComponent("Duplicate Extension");
pressButtonByText(confirmDialog, "Cancel"); pressButtonByText(confirmDialog, "Cancel");
waitForSwing(); waitForSwing();
@ -397,8 +398,7 @@ public class ExtensionInstallerTest extends AbstractDockingTest {
didInstall.set(ExtensionInstaller.install(extensionFolder2)); didInstall.set(ExtensionInstaller.install(extensionFolder2));
}); });
DialogComponentProvider confirmDialog = DialogComponentProvider confirmDialog = waitForDialogComponent("Duplicate Extension");
waitForDialogComponent("Duplicate Extension");
pressButtonByText(confirmDialog, "Remove Existing"); pressButtonByText(confirmDialog, "Remove Existing");
waitForSwing(); waitForSwing();
@ -426,14 +426,14 @@ public class ExtensionInstallerTest extends AbstractDockingTest {
/* /*
Create a zip file that looks something like this: Create a zip file that looks something like this:
/ /
{Extension Name 1}/ {Extension Name 1}/
extension.properties extension.properties
{Extension Name 2}/ {Extension Name 2}/
extension.properties extension.properties
*/ */
errorsExpected(() -> { errorsExpected(() -> {
@ -446,9 +446,9 @@ public class ExtensionInstallerTest extends AbstractDockingTest {
public void testInstallThenUninstallThenReinstallWhenExtensionNameDoesntMatchFolder() public void testInstallThenUninstallThenReinstallWhenExtensionNameDoesntMatchFolder()
throws Exception { 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 // 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. // condition.
String nameProperty = "ExtensionNamedFoo"; String nameProperty = "ExtensionNamedFoo";
@ -493,10 +493,9 @@ public class ExtensionInstallerTest extends AbstractDockingTest {
private void assertExtensionNotInstalled(String name, String version) { private void assertExtensionNotInstalled(String name, String version) {
Set<ExtensionDetails> extensions = ExtensionUtils.getInstalledExtensions(); Set<ExtensionDetails> extensions = ExtensionUtils.getInstalledExtensions();
Optional<ExtensionDetails> match = Optional<ExtensionDetails> match = extensions.stream()
extensions.stream() .filter(e -> e.getName().equals(name) && e.getVersion().equals(version))
.filter(e -> e.getName().equals(name) && e.getVersion().equals(version)) .findFirst();
.findFirst();
assertFalse("Extension should not be installed: '" + name + "'", match.isPresent()); 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, private File doCreateExternalExtensionInFolder(File externalFolder, String extensionName,
String version) String version) throws Exception {
throws Exception {
return doCreateExternalExtensionInFolder(externalFolder, extensionName, extensionName, return doCreateExternalExtensionInFolder(externalFolder, extensionName, extensionName,
version); version);
} }
@ -609,9 +607,8 @@ public class ExtensionInstallerTest extends AbstractDockingTest {
version); version);
} }
private File doCreateExternalExtensionInFolder(File externalFolder, private File doCreateExternalExtensionInFolder(File externalFolder, String extensionName,
String extensionName, String nameProperty, String version) String nameProperty, String version) throws Exception {
throws Exception {
ResourceFile root = new ResourceFile(new ResourceFile(externalFolder), extensionName); ResourceFile root = new ResourceFile(new ResourceFile(externalFolder), extensionName);
root.mkdir(); root.mkdir();