From 2d5f53e051afd691527cbc046082c03271b06e3f Mon Sep 17 00:00:00 2001 From: dragonmacher <48328597+dragonmacher@users.noreply.github.com> Date: Wed, 13 Apr 2022 12:56:21 -0400 Subject: [PATCH] GP-1816 - OSGi Bundles - minor refactoring of OSGi bundle code during exploration. --- .../app/plugin/core/osgi/BuildError.java | 16 +- .../app/plugin/core/osgi/BundleHost.java | 353 +++++------ .../app/plugin/core/osgi/BundleMap.java | 4 +- .../osgi/BundleStatusComponentProvider.java | 22 +- .../core/osgi/BundleStatusTableModel.java | 16 +- .../app/plugin/core/osgi/GhidraBundle.java | 78 ++- .../app/plugin/core/osgi/GhidraJarBundle.java | 2 +- .../plugin/core/osgi/GhidraSourceBundle.java | 571 +++++++++--------- .../app/plugin/core/osgi/OSGiUtils.java | 1 - .../script/GhidraScriptComponentProvider.java | 6 +- .../ghidra/app/script/GhidraScriptUtil.java | 4 +- .../ghidra/app/script/JavaScriptProvider.java | 2 +- .../plugin/core/script/BundleHostTest.java | 2 +- .../core/script/BundleStatusManagerTest.java | 4 +- .../script/GhidraScriptMgrPlugin2Test.java | 14 +- .../test/java/ghidra/test/JavaCompiler.java | 58 +- .../main/java/generic/io/NullPrintWriter.java | 4 + .../main/java/generic/util/FileLocker.java | 36 +- .../ghidra/framework/project/ProjectLock.java | 7 +- .../java/utilities/util/FileUtilities.java | 12 +- 20 files changed, 629 insertions(+), 583 deletions(-) diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/BuildError.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/BuildError.java index 8db1a5dcb7..1bf9c348f2 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/BuildError.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/BuildError.java @@ -18,16 +18,15 @@ package ghidra.app.plugin.core.osgi; import generic.jar.ResourceFile; /** - * An error produced during {@link GhidraBundle#build()} with a time stamp + * An error produced during {@link GhidraBundle#build()} with a timestamp. */ public class BuildError { // the lastModified time of the source causing this error private final long lastModified; - private final StringBuilder message = new StringBuilder(); /** - * Construct an object to record error message produced for {@code sourceFile} + * Construct an object to record error message produced for {@code sourceFile}. * @param sourceFile the file causing this error */ public BuildError(ResourceFile sourceFile) { @@ -35,15 +34,15 @@ public class BuildError { } /** - * Append {@code str} to the current error message - * - * @param str the string to append + * Append the given string to the current error message. + * @param s the string to append */ - public void append(String str) { - message.append(str); + public void append(String s) { + message.append(s); } /** + * The error message. * @return the error message */ String getMessage() { @@ -51,6 +50,7 @@ public class BuildError { } /** + * The last modified time of the source for this build error. * @return the last modified time of the source for this build error */ public long getLastModified() { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/BundleHost.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/BundleHost.java index 56a3141eb5..3fa33c5d6c 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/BundleHost.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/BundleHost.java @@ -15,7 +15,8 @@ */ package ghidra.app.plugin.core.osgi; -import java.io.*; +import java.io.IOException; +import java.io.PrintWriter; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; @@ -46,19 +47,19 @@ import ghidra.util.task.TaskMonitor; * Hosts the embedded OSGi framework and manages {@link GhidraBundle}s. * *

- * note: {@link GhidraBundle}, its implementations, and this class constitute - * a bridge between OSGi's {@link Bundle} and Ghidra. + * Note: {@link GhidraBundle}, its implementations, and this class constitute a bridge between + * OSGi's {@link Bundle} and Ghidra. * */ public class BundleHost { public static final String ACTIVATING_BUNDLE_ERROR_MSG = "activating bundle"; - protected static final boolean STDERR_DEBUGGING = false; + private static final boolean STDERR_DEBUGGING = false; private static final String SAVE_STATE_TAG_FILE = "BundleHost_FILE"; private static final String SAVE_STATE_TAG_ENABLE = "BundleHost_ENABLE"; private static final String SAVE_STATE_TAG_ACTIVE = "BundleHost_ACTIVE"; @@ -66,22 +67,10 @@ public class BundleHost { private final BundleMap bundleMap = new BundleMap(); - BundleContext frameworkBundleContext; - Framework felixFramework; + private BundleContext frameworkBundleContext; + private Framework felixFramework; - List listeners = new CopyOnWriteArrayList<>(); - - /** constructor */ - public BundleHost() { - // - } - - /** - * stop the framework. - */ - public void dispose() { - stopFramework(); - } + private List listeners = new CopyOnWriteArrayList<>(); /** * If a {@link GhidraBundle} hasn't already been added for {@bundleFile}, add it now as a @@ -132,8 +121,8 @@ public class BundleHost { } /** - * Assuming there is currently a bundle managed with file {@code bundleFile}, - * return its {@link GhidraBundle}, otherwise show an error dialog and return {@code null}. + * Assuming there is currently a bundle managed with file {@code bundleFile}, return its + * {@link GhidraBundle}, otherwise show an error dialog and return {@code null}. * * @param bundleFile the bundleFile of the sought bundle * @return a {@link GhidraBundle} or {@code null} @@ -149,8 +138,8 @@ public class BundleHost { } /** - * If there is currently a bundle managed with file {@code bundleFile}, - * return its {@link GhidraBundle}, otherwise return {@code null}. + * If there is currently a bundle managed with file {@code bundleFile}, return its + * {@link GhidraBundle}, otherwise return {@code null}. * * @param bundleFile the bundleFile of the sought bundle * @return a {@link GhidraBundle} or {@code null} @@ -180,7 +169,7 @@ public class BundleHost { } /** - * Create a new GhidraBundle and add to the list of managed bundles + * Create a new GhidraBundle and add to the list of managed bundles. * * @param bundleFile the bundle file * @param enabled if the new bundle should be enabled @@ -282,7 +271,7 @@ public class BundleHost { } } - Bundle installFromLoc(String bundleLocation) throws GhidraBundleException { + private Bundle installFromLoc(String bundleLocation) throws GhidraBundleException { try { return frameworkBundleContext.installBundle(bundleLocation); } @@ -291,17 +280,8 @@ public class BundleHost { } } - Bundle installAsLoc(String bundleLocation, InputStream contents) throws GhidraBundleException { - try { - return frameworkBundleContext.installBundle(bundleLocation, contents); - } - catch (BundleException e) { - throw new GhidraBundleException(bundleLocation, "installing as bundle location", e); - } - } - /** - * return all of the currently managed bundles + * Return all of the currently managed bundles. * * @return all the bundles */ @@ -310,7 +290,7 @@ public class BundleHost { } /** - * return the list of currently managed bundle files + * Return the list of currently managed bundle files. * * @return all the bundle files */ @@ -318,14 +298,6 @@ public class BundleHost { return bundleMap.getBundleFiles(); } - void dumpLoadedBundles() { - System.err.printf("=== Bundles ===\n"); - for (Bundle bundle : frameworkBundleContext.getBundles()) { - System.err.printf("%s: %s: %s: %s\n", bundle.getBundleId(), bundle.getSymbolicName(), - bundle.getState(), bundle.getVersion()); - } - } - /** * Attempt to resolve a list of BundleRequirements with active Bundle capabilities. * @@ -370,7 +342,7 @@ public class BundleHost { return tmpRequirements.isEmpty(); } - protected String buildExtraSystemPackages() { + private String buildExtraSystemPackages() { Set packages = new HashSet<>(); OSGiUtils.getPackagesFromClasspath(packages); return packages.stream().collect(Collectors.joining(",")); @@ -387,11 +359,11 @@ public class BundleHost { } /** - * A directory for use by the OSGi framework as a cache + * A directory for use by the OSGi framework as a cache. * * @return the directory */ - protected static Path getCacheDir() { + private static Path getCacheDir() { return BundleHost.getOsgiDir().resolve("felixcache"); } @@ -401,16 +373,19 @@ public class BundleHost { return cacheDir.toAbsolutePath().toString(); } - protected void createAndConfigureFramework() throws IOException { + private void createAndConfigureFramework() throws IOException { Properties config = new Properties(); // allow multiple bundles w/ the same symbolic name -- location can distinguish config.setProperty(Constants.FRAMEWORK_BSNVERSION, Constants.FRAMEWORK_BSNVERSION_MULTIPLE); - // use the default, inferred from environment - // config.setProperty(Constants.FRAMEWORK_SYSTEMCAPABILITIES,"osgi.ee; osgi.ee=\"JavaSE\";version:List=\"...\""); - // compute and add everything in the class path. extra packages have lower precedence than imports, - // so an Import-Package / @importpackage will override the "living off the land" default + // use the default, inferred from environment + // config.setProperty(Constants.FRAMEWORK_SYSTEMCAPABILITIES, + // "osgi.ee; osgi.ee=\"JavaSE\";version:List=\"...\""); + + // compute and add everything in the class path. extra packages have lower precedence than + // imports, so an Import-Package / @importpackage will override the "living off the land" + // default config.setProperty(Constants.FRAMEWORK_SYSTEMPACKAGES_EXTRA, buildExtraSystemPackages()); // only clean on first startup, o/w keep our storage around @@ -423,41 +398,43 @@ public class BundleHost { config.put(FelixConstants.LOG_LEVEL_PROP, "1"); if (STDERR_DEBUGGING) { config.put(FelixConstants.LOG_LEVEL_PROP, "999"); - // config.put(FelixConstants.LOG_LOGGER_PROP, new org.apache.felix.framework.Logger() {...}); + // config.put(FelixConstants.LOG_LOGGER_PROP, + // new org.apache.felix.framework.Logger() {...}); } FrameworkFactory factory = new FrameworkFactory(); felixFramework = factory.newFramework(config); } - protected void addDebuggingListeners() { - frameworkBundleContext.addFrameworkListener(new FrameworkListener() { - @Override - public void frameworkEvent(FrameworkEvent event) { - System.err.printf("%s %s\n", event.getBundle(), event); + private void addDebuggingListeners() { + if (!STDERR_DEBUGGING) { + return; + } + + frameworkBundleContext.addFrameworkListener( + event -> { + String msg = String.format("AA: %s %s\n", event.getBundle(), event); + Msg.debug(this, msg); + }); + + frameworkBundleContext.addServiceListener(event -> { + + String type = "?"; + if (event.getType() == ServiceEvent.REGISTERED) { + type = "registered"; } - }); - frameworkBundleContext.addServiceListener(new ServiceListener() { - @Override - public void serviceChanged(ServiceEvent event) { - - String type = "?"; - if (event.getType() == ServiceEvent.REGISTERED) { - type = "registered"; - } - else if (event.getType() == ServiceEvent.UNREGISTERING) { - type = "unregistering"; - } - - System.err.printf("%s %s from %s\n", event.getSource(), type, - event.getServiceReference().getBundle().getLocation()); - + else if (event.getType() == ServiceEvent.UNREGISTERING) { + type = "unregistering"; } + + String msg = String.format("BB: %s %s from %s\n", event.getSource(), type, + event.getServiceReference().getBundle().getLocation()); + Msg.debug(this, msg); }); } /** - * start the framework + * Start the framework. * * @throws OSGiException framework failures * @throws IOException filesystem setup @@ -469,48 +446,53 @@ public class BundleHost { felixFramework.init(); } catch (BundleException e) { - throw new OSGiException("initializing felix OSGi framework", e); + throw new OSGiException("Exception initializing felix OSGi framework", e); } + frameworkBundleContext = felixFramework.getBundleContext(); - if (STDERR_DEBUGGING) { - addDebuggingListeners(); - } + addDebuggingListeners(); - frameworkBundleContext - .addBundleListener(new MyBundleListener(frameworkBundleContext.getBundle())); + Bundle bundle = frameworkBundleContext.getBundle(); + frameworkBundleContext.addBundleListener(new MyBundleListener(bundle)); try { felixFramework.start(); } catch (BundleException e) { - throw new OSGiException("starting felix OSGi framework", e); + throw new OSGiException("Exception starting felix OSGi framework", e); } } /** - * stop the OSGi framework synchronously + * Stop the OSGi framework. + * + *

This may wait for up to 5 seconds for the framework to fully stop. If that timeout + * passes an error will be logged. */ - protected void stopFramework() { - if (felixFramework != null) { - try { - felixFramework.stop(); - // any bundles that linger after a few seconds might be the source - // of subtle problems, so wait for them to stop and report any problems. - FrameworkEvent event = felixFramework.waitForStop(5000); - if (event.getType() == FrameworkEvent.WAIT_TIMEDOUT) { - Msg.error(this, "Stopping OSGi framework timed out after 5 seconds."); - } - felixFramework = null; - } - catch (BundleException | InterruptedException e) { - Msg.error(this, "Failed to stop OSGi framework."); - e.printStackTrace(); + public void stopFramework() { + if (felixFramework == null) { + return; + } + + try { + felixFramework.stop(); + + // any bundles that linger after a few seconds might be the source of subtle problems, + // so wait for them to stop and report any problems. + FrameworkEvent event = felixFramework.waitForStop(5000); + if (event.getType() == FrameworkEvent.WAIT_TIMEDOUT) { + Msg.error(this, "Stopping OSGi framework timed-out after 5 seconds."); } + felixFramework = null; + } + catch (BundleException | InterruptedException e) { + Msg.error(this, "Failed to stop OSGi framework.", e); } } /** + * Gets the host framework. * @return the OSGi framework */ Framework getHostFramework() { @@ -565,7 +547,8 @@ public class BundleHost { } /** - * Deactivate a bundle. Either an exception is thrown or the bundle will be in "UNINSTALLED" state. + * Deactivate a bundle. Either an exception is thrown or the bundle will be in "UNINSTALLED" + * state. * * @param bundle the bundle * @throws GhidraBundleException if there's a problem activating @@ -575,7 +558,7 @@ public class BundleHost { return; } FrameworkWiring frameworkWiring = felixFramework.adapt(FrameworkWiring.class); - LinkedList dependentBundles = new LinkedList( + LinkedList dependentBundles = new LinkedList<>( frameworkWiring.getDependencyClosure(Collections.singleton(bundle))); while (!dependentBundles.isEmpty()) { Bundle dependentBundle = dependentBundles.pop(); @@ -596,7 +579,8 @@ public class BundleHost { } /** - * Deactivate a bundle. Either an exception is thrown or the bundle will be in "UNINSTALLED" state. + * Deactivate a bundle. Either an exception is thrown or the bundle will be in "UNINSTALLED" + * state. * * @param bundleLocation the bundle location identifier * @throws InterruptedException if the wait is interrupted @@ -611,8 +595,8 @@ public class BundleHost { } /** - * Refreshes the specified bundles. This forces the update (replacement) - * or removal of packages exported by the specified bundles. + * Refreshes the specified bundles. This forces the update (replacement) or removal of packages + * exported by the specified bundles. * * @param bundles the bundles to refresh * @see FrameworkWiring#refreshBundles @@ -620,22 +604,19 @@ public class BundleHost { protected void refreshBundlesSynchronously(Collection bundles) { FrameworkWiring frameworkWiring = felixFramework.adapt(FrameworkWiring.class); final CountDownLatch latch = new CountDownLatch(1); - frameworkWiring.refreshBundles(bundles, new FrameworkListener() { - @Override - public void frameworkEvent(FrameworkEvent event) { - if (event.getType() == FrameworkEvent.ERROR) { - Bundle bundle = event.getBundle(); - Msg.error(BundleHost.this, - String.format("OSGi error refreshing bundle: %s", bundle)); - } - latch.countDown(); + frameworkWiring.refreshBundles(bundles, event -> { + if (event.getType() == FrameworkEvent.ERROR) { + Bundle bundle = event.getBundle(); + Msg.error(BundleHost.this, + String.format("OSGi error refreshing bundle: %s", bundle)); } + latch.countDown(); }); try { latch.await(); } catch (InterruptedException e) { - e.printStackTrace(); + Msg.error(this, "Exception waiting for bundles to refresh", e); } } @@ -649,8 +630,11 @@ public class BundleHost { } /** - * Activate a set of bundles and any dependencies in topological order. This method doesn't rely on the - * framework, and so will add non-active dependencies. + * Activate a set of bundles and any dependencies in topological order. This method doesn't + * rely on the framework, and so will add non-active dependencies. + * + *

To load bundles without loading inactive dependencies, call + * {@link #activateInStages(Collection, TaskMonitor, PrintWriter)}. * * @param bundles bundles to activate * @param monitor a task monitor @@ -671,9 +655,13 @@ public class BundleHost { activateSynchronously(bundle.getLocationIdentifier()); } catch (GhidraBundleException e) { + // TODO should we report failing bundles to the console as well so they get logged + // in headless mode? fireBundleException(e); } catch (Exception e) { + // write the error to the console or log file + console.println("Unexpected error activating bundles: " + bundles); e.printStackTrace(console); } monitor.incrementProgress(1); @@ -682,7 +670,10 @@ public class BundleHost { /** * Activate a set of bundles in dependency topological order by resolving against currently - * active bundles in stages. No bundles outside those requested will be activated. + * active bundles in stages. No bundles outside those requested will be activated. + * + *

To have inactive dependencies loaded, call + * {@link #activateAll(Collection, TaskMonitor, PrintWriter)}. * * @param bundles bundles to activate * @param monitor a task monitor @@ -702,11 +693,13 @@ public class BundleHost { requirementMap.put(bundle, requirements); } catch (GhidraBundleException e) { + // TODO should we report failing bundles to the console as well so they get logged + // in headless mode? fireBundleException(e); } } - List bundlesRemaining = new ArrayList<>(requirementMap.keySet()); + List bundlesRemaining = new ArrayList<>(requirementMap.keySet()); monitor.setMaximum(bundlesRemaining.size()); while (!bundlesRemaining.isEmpty() && !monitor.isCancelled()) { List resolvableBundles = bundlesRemaining.stream() @@ -731,9 +724,13 @@ public class BundleHost { activateSynchronously(bundle.getLocationIdentifier()); } catch (GhidraBundleException e) { + // TODO should we report failing bundles to the console as well so they get logged + // in headless mode? fireBundleException(e); } catch (Exception e) { + // write the error to the console or log file + console.println("Unexpected error activating bundles: " + bundles); e.printStackTrace(console); } monitor.incrementProgress(1); @@ -813,7 +810,7 @@ public class BundleHost { * *

Bundles that had been active are reactivated. * - *

note: This is done once on startup after system bundles have been added. + *

Note: This is done once on startup after system bundles have been added. * * @param saveState the state object * @param tool the tool @@ -863,7 +860,7 @@ public class BundleHost { } if (!bundlesToActivate.isEmpty()) { - TaskLauncher.launchNonModal("restoring bundle state", + TaskLauncher.launchNonModal("Restoring bundle state", (monitor) -> activateInStages(bundlesToActivate, monitor, new NullPrintWriter())); } } @@ -896,35 +893,43 @@ public class BundleHost { saveState.putBooleans(SAVE_STATE_TAG_SYSTEM, bundleIsSystem); } - private static class Dependency { - // exists only to be distinguished by id +//================================================================================================= +// Inner Classes +//================================================================================================= + + private static class BundleEdge { + // edge type for dependency graph } /** - * Utility class to build a dependency graph from bundles where capabilities map to requirements. + * Utility class to build a dependency graph from bundles where capabilities map to + * requirements. */ - private class BundleDependencyGraph extends DirectedMultigraph { - final Map> capabilityMap = new HashMap<>(); - final List availableBundles; - final TaskMonitor monitor; + private class BundleDependencyGraph extends DirectedMultigraph { - BundleDependencyGraph(Collection startingBundles, TaskMonitor monitor) { + private final List availableBundles = new ArrayList<>(); + private final Map> capabilityMap = new HashMap<>(); + private final TaskMonitor monitor; + + BundleDependencyGraph(Collection activatingBundles, TaskMonitor monitor) { super(null, null, false); this.monitor = monitor; - // maintain a list of bundles available for resolution, starting with all of the enabled bundles - this.availableBundles = new ArrayList<>(); + // maintain a list of bundles available for resolution, starting with all of the + // enabled bundles for (GhidraBundle bundle : getGhidraBundles()) { if (bundle.isEnabled()) { addToAvailable(bundle); } } + // An edge A->B indicates that the capabilities of A resolve some requirement(s) of B - // "front" accumulates bundles and links to bundles already in the graph that they provide capabilities for. - // e.g. if front[A]=[B,...] then A->B, B is already in the graph, and we will add A next iteration. + // "front" accumulates bundles and links to bundles already in the graph that they + // provide capabilities for. e.g., if front[A]=[B,...] then A->B, B is already in the + // graph, and we will add A next iteration. Map> front = new HashMap<>(); - for (GhidraBundle bundle : startingBundles) { + for (GhidraBundle bundle : activatingBundles) { front.put(bundle, null); } @@ -939,46 +944,45 @@ public class BundleHost { handleBackEdges(newFront); front = newFront; } - } Iterable inTopologicalOrder() { return () -> new TopologicalOrderIterator<>(this); } - void handleBackEdges(Map> newFront) { - Iterator>> newFrontIter = + private void handleBackEdges(Map> newFront) { + Iterator>> it = newFront.entrySet().iterator(); - while (newFrontIter.hasNext() && !monitor.isCancelled()) { - Entry> entry = newFrontIter.next(); + while (it.hasNext() && !monitor.isCancelled()) { + Entry> entry = it.next(); GhidraBundle source = entry.getKey(); if (containsVertex(source)) { for (GhidraBundle destination : entry.getValue()) { if (source != destination) { - addEdge(source, destination, new Dependency()); + addEdge(source, destination, new BundleEdge()); } } - newFrontIter.remove(); + it.remove(); } } } - void addFront(Map> front) { - for (Entry> e : front.entrySet()) { - GhidraBundle source = e.getKey(); + private void addFront(Map> front) { + for (Entry> entry : front.entrySet()) { + GhidraBundle source = entry.getKey(); if (addToAvailable(source)) { addVertex(source); - Set destinations = e.getValue(); - if (destinations != null) { - for (GhidraBundle destination : destinations) { - addEdge(source, destination, new Dependency()); + Set dependents = entry.getValue(); + if (dependents != null) { + for (GhidraBundle destination : dependents) { + addEdge(source, destination, new BundleEdge()); } } } } } - boolean addToAvailable(GhidraBundle bundle) { + private boolean addToAvailable(GhidraBundle bundle) { try { capabilityMap.put(bundle, bundle.getAllCapabilities()); availableBundles.add(bundle); @@ -990,33 +994,38 @@ public class BundleHost { } } - // populate newFront with edges depBundle -> bundle, - // where depBundle has a capability that resolves a requirement of bundle - void resolve(GhidraBundle bundle, Map> newFront) { + // Populate newFront with edges supplierBundle -> dependentBundle, where supplierBundle has + // a capability that resolves a requirement of dependentBundle. Items added to newFront are + // already in the graph. These items will be added if the given bundle to resolve + // becomes a dependent on this added supplier. + private void resolve(GhidraBundle toResolve, + Map> newFront) { List requirements; try { - requirements = new ArrayList<>(bundle.getAllRequirements()); - if (requirements.isEmpty()) { - return; + List bundleRequirements = toResolve.getAllRequirements(); + if (bundleRequirements.isEmpty()) { + return; // no dependencies to resolve } + requirements = new ArrayList<>(bundleRequirements); } catch (GhidraBundleException e) { fireBundleException(e); - removeVertex(bundle); + removeVertex(toResolve); return; } - for (GhidraBundle depBundle : availableBundles) { - for (BundleCapability capability : capabilityMap.get(depBundle)) { + for (GhidraBundle supplierBundle : availableBundles) { + for (BundleCapability capability : capabilityMap.get(supplierBundle)) { if (monitor.isCancelled()) { return; } - Iterator reqIter = requirements.iterator(); - while (reqIter.hasNext()) { - BundleRequirement req = reqIter.next(); + Iterator it = requirements.iterator(); + while (it.hasNext()) { + BundleRequirement req = it.next(); if (req.matches(capability)) { - newFront.computeIfAbsent(depBundle, b -> new HashSet<>()).add(bundle); - reqIter.remove(); + newFront.computeIfAbsent(supplierBundle, b -> new HashSet<>()) + .add(toResolve); + it.remove(); } } if (requirements.isEmpty()) { @@ -1024,13 +1033,15 @@ public class BundleHost { } } } - // if requirements remain, some will be resolved by system - // and others will generate helpful errors for the user during activation + + // If requirements remain, some will be resolved by system and others will generate + // helpful errors for the user during activation } } /** - * The {@code BundleListener} that notifies {@link BundleHostListener}s of bundle activation changes + * The {@code BundleListener} that notifies {@link BundleHostListener}s of bundle activation + * changes. */ private class MyBundleListener implements BundleListener { private final Bundle systemBundle; @@ -1050,8 +1061,10 @@ public class BundleHost { if (STDERR_DEBUGGING) { String symbolicName = osgiBundle.getSymbolicName(); String locationIdentifier = osgiBundle.getLocation(); - System.err.printf("%s %s from %s\n", OSGiUtils.getEventTypeString(event), - symbolicName, locationIdentifier); + String message = + String.format("CC: %s %s from %s\n", OSGiUtils.getEventTypeString(event), + symbolicName, locationIdentifier); + Msg.debug(this, message); } GhidraBundle bundle; switch (event.getType()) { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/BundleMap.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/BundleMap.java index cee4bf6519..283f9b4f87 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/BundleMap.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/BundleMap.java @@ -45,7 +45,7 @@ public class BundleMap { bundlesByLocation.put(bundle.getLocationIdentifier(), bundle); } finally { - lock.writeLock().unlock(); + writeLock.unlock(); } } @@ -197,6 +197,7 @@ public class BundleMap { } /** + * Returns the currently mapped bundles. * @return the currently mapped bundles */ public Collection getGhidraBundles() { @@ -210,6 +211,7 @@ public class BundleMap { } /** + * Returns the currently mapped bundle files. * @return the currently mapped bundle files */ public Collection getBundleFiles() { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/BundleStatusComponentProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/BundleStatusComponentProvider.java index 5f4239ac34..0933ec6814 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/BundleStatusComponentProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/BundleStatusComponentProvider.java @@ -45,7 +45,7 @@ import resources.Icons; import resources.ResourceManager; /** - * component for managing OSGi bundle status + * Component for managing OSGi bundle status */ public class BundleStatusComponentProvider extends ComponentProviderAdapter { @@ -289,7 +289,7 @@ public class BundleStatusComponentProvider extends ComponentProviderAdapter { files.stream().map(ResourceFile::new).collect(Collectors.toUnmodifiableList()); Collection bundles = bundleHost.add(resourceFiles, true, false); - TaskLauncher.launchNonModal("activating new bundles", (monitor) -> { + TaskLauncher.launchNonModal("Activating new bundles", (monitor) -> { bundleHost.activateAll(bundles, monitor, getTool().getService(ConsoleService.class).getStdErr()); }); @@ -339,9 +339,6 @@ public class BundleStatusComponentProvider extends ComponentProviderAdapter { return panel; } - /** - * cleanup this component - */ public void dispose() { filterPanel.dispose(); } @@ -351,9 +348,10 @@ public class BundleStatusComponentProvider extends ComponentProviderAdapter { } /** - * This is for testing only! during normal execution, statuses are only added through BundleHostListener bundle(s) added events. + * This is for testing only! during normal execution, statuses are only added through + * BundleHostListener bundle(s) added events. * - *

each new bundle will be enabled and writable + *

Each new bundle will be enabled and writable * * @param bundleFiles the files to use */ @@ -363,6 +361,10 @@ public class BundleStatusComponentProvider extends ComponentProviderAdapter { .collect(Collectors.toList())); } +//================================================================================================= +// Inner Classes +//================================================================================================= + private final class RemoveBundlesTask extends Task { private final DeactivateAndDisableBundlesTask deactivateBundlesTask; private final List statuses; @@ -378,7 +380,7 @@ public class BundleStatusComponentProvider extends ComponentProviderAdapter { public void run(TaskMonitor monitor) throws CancelledException { deactivateBundlesTask.run(monitor); monitor.checkCanceled(); - // partition bundles into system (bundles.get(true)) and non-system (bundles.get(false)). + // partition bundles into system (bundles.get(true)) / non-system (bundles.get(false)) Map> bundles = statuses.stream() .map(bs -> bundleHost.getExistingGhidraBundle(bs.getFile())) .collect(Collectors.partitioningBy(GhidraBundle::isSystemBundle)); @@ -483,8 +485,8 @@ public class BundleStatusComponentProvider extends ComponentProviderAdapter { } /* - * Activating/deactivating a single bundle doesn't require resolving dependents, - * so this task is slightly different from the others. + * Activating/deactivating a single bundle doesn't require resolving dependents, so this task + * is slightly different from the others. */ private class ActivateDeactivateBundleTask extends Task { private final BundleStatus status; diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/BundleStatusTableModel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/BundleStatusTableModel.java index 7f6221992c..7f7a957baa 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/BundleStatusTableModel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/BundleStatusTableModel.java @@ -22,7 +22,7 @@ import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; -import javax.swing.*; +import javax.swing.JLabel; import javax.swing.event.TableModelEvent; import org.osgi.framework.Bundle; @@ -32,8 +32,10 @@ import generic.jar.ResourceFile; import generic.util.Path; import ghidra.docking.settings.Settings; import ghidra.framework.plugintool.ServiceProvider; -import ghidra.util.*; -import ghidra.util.table.column.*; +import ghidra.util.Swing; +import ghidra.util.SystemUtilities; +import ghidra.util.table.column.AbstractGColumnRenderer; +import ghidra.util.table.column.GColumnRenderer; /** * Model for {@link BundleStatus} objects. @@ -73,10 +75,10 @@ public class BundleStatusTableModel } private BundleStatus getStatus(GhidraBundle bundle) { - return getStatusFromLoc(bundle.getLocationIdentifier()); + return getStatusFromLocation(bundle.getLocationIdentifier()); } - private BundleStatus getStatusFromLoc(String bundleLoc) { + private BundleStatus getStatusFromLocation(String bundleLoc) { return bundleLocToStatusMap.get(bundleLoc); } @@ -254,7 +256,7 @@ public class BundleStatusTableModel // wrap the assigned comparator to detect if the order changes AtomicBoolean changed = new AtomicBoolean(false); - Comparator wrapper = new Comparator() { + Comparator wrapper = new Comparator<>() { Comparator comparator = sortingContext.getComparator(); @Override @@ -367,7 +369,7 @@ public class BundleStatusTableModel @Override public void bundleException(GhidraBundleException exception) { Swing.runLater(() -> { - BundleStatus status = getStatusFromLoc(exception.getBundleLocation()); + BundleStatus status = getStatusFromLocation(exception.getBundleLocation()); if (status != null) { status.setSummary(exception.getMessage()); int rowIndex = getRowIndex(status); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/GhidraBundle.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/GhidraBundle.java index 5043ae9c03..492ec60b9a 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/GhidraBundle.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/GhidraBundle.java @@ -30,8 +30,20 @@ import generic.jar.ResourceFile; */ public abstract class GhidraBundle { - protected final ResourceFile file; + /** + * A {@link GhidraBundle} can be + *

    + *
  • a Bndtools .bnd script
  • + *
  • an OSGi bundle .jar file
  • + *
  • a directory of Java source
  • + *
+ * + */ + enum Type { + BND_SCRIPT, JAR, SOURCE_DIR, INVALID + } + protected final ResourceFile bundleFile; // can be a dir or a jar file protected final BundleHost bundleHost; protected boolean enabled; protected boolean systemBundle; @@ -39,29 +51,29 @@ public abstract class GhidraBundle { GhidraBundle(BundleHost bundleHost, ResourceFile bundleFile, boolean enabled, boolean systemBundle) { this.bundleHost = bundleHost; - this.file = bundleFile; + this.bundleFile = bundleFile; this.enabled = enabled; this.systemBundle = systemBundle; } /** - * clean build artifacts generated during build of this bundle + * Clean build artifacts generated during build of this bundle. * * @return true if anything was done */ abstract boolean clean(); /** - * build OSGi bundle if possible + * Build OSGi bundle if needed and if possible. * * @param writer console for build messages to user - * @return true if build happened, false if already built + * @return true if build happened, false if already built or could not build * @throws Exception if the build cannot complete */ public abstract boolean build(PrintWriter writer) throws Exception; /** - * same as {@link #build(PrintWriter)} with writer = {@link System#err}. + * Same as {@link #build(PrintWriter)} with writer = {@link System#err}. * * @return true if build happened, false if already built * @throws Exception if the build cannot complete @@ -74,28 +86,43 @@ public abstract class GhidraBundle { * Return the location identifier of the bundle that this GhidraBundle represents. * *

The location identifier is used by the framework, e.g. it is passed to - * {@link org.osgi.framework.BundleContext#installBundle} when the bundle is - * first installed. + * {@link org.osgi.framework.BundleContext#installBundle} when the bundle is first installed. * - *

Although the bundle location is a URI, outside of interactions with the framework, - * the bundle location should remain opaque. + *

Although the bundle location is a URI, outside of interactions with the framework, the + * bundle location should remain opaque. * - * @return location identifier of this bundle + * @return location identifier of this bundle */ public abstract String getLocationIdentifier(); + /** + * Returns all bundle requirements. + * + * @return the requirements + * @throws GhidraBundleException if there is an exception parsing / loading bundle requirements + */ public abstract List getAllRequirements() throws GhidraBundleException; + /** + * Returns all bundle capabilities. + * + * @return the capabilities + * @throws GhidraBundleException if there is an exception parsing / loading bundle capabilities + */ public abstract List getAllCapabilities() throws GhidraBundleException; /** - * @return the file where this bundle is loaded from + * The file where this bundle is loaded from. + * + * @return the file from where this bundle is loaded */ public ResourceFile getFile() { - return file; + return bundleFile; } /** + * True if this bundle is enabled. + * * @return true if this bundle is enabled */ public boolean isEnabled() { @@ -103,7 +130,7 @@ public abstract class GhidraBundle { } /** - * set the enablement flag for this bundle. + * Set the enablement flag for this bundle. * *

If a bundle is enabled its contents will be scanned, e.g. for scripts. * @@ -123,7 +150,7 @@ public abstract class GhidraBundle { } /** - * Get the type of a GhidraBundle from its file. + * Get the type of {@link GhidraBundle} from its file. * * @param file a bundle file * @return the type @@ -163,8 +190,8 @@ public abstract class GhidraBundle { } /** - * Get the OSGi bundle represented by this GhidraBundle or null if it isn't in - * the "installed" state. + * Get the OSGi bundle represented by this GhidraBundle or null if it isn't in the "installed" + * state. * * @return a Bundle or null */ @@ -173,6 +200,8 @@ public abstract class GhidraBundle { } /** + * True if this bundle is active. + * * @return true if this bundle is active */ public boolean isActive() { @@ -180,17 +209,8 @@ public abstract class GhidraBundle { return (bundle != null) && bundle.getState() == Bundle.ACTIVE; } - /** - * A GhidraBundle can be - *

    - *
  • a Bndtools .bnd script
  • - *
  • an OSGi bundle .jar file
  • - *
  • a directory of Java source
  • - *
- * - */ - enum Type { - BND_SCRIPT, JAR, SOURCE_DIR, INVALID + @Override + public String toString() { + return getOSGiBundle().getSymbolicName(); } - } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/GhidraJarBundle.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/GhidraJarBundle.java index cfe4965002..1d26c6f9b7 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/GhidraJarBundle.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/GhidraJarBundle.java @@ -67,7 +67,7 @@ public class GhidraJarBundle extends GhidraBundle { } protected ManifestParser createManifestParser() throws GhidraBundleException { - try (Jar jar = new Jar(file.getFile(true))) { + try (Jar jar = new Jar(bundleFile.getFile(true))) { Manifest manifest = jar.getManifest(); if (manifest == null) { throw new GhidraBundleException(bundleLocation, "jar bundle with no manifest"); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/GhidraSourceBundle.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/GhidraSourceBundle.java index 68f8c51ac7..e108d13817 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/GhidraSourceBundle.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/GhidraSourceBundle.java @@ -35,8 +35,7 @@ import javax.tools.*; import javax.tools.JavaFileObject.Kind; import org.apache.felix.framework.util.manifestparser.ManifestParser; -import org.osgi.framework.Bundle; -import org.osgi.framework.BundleException; +import org.osgi.framework.*; import org.osgi.framework.Constants; import org.osgi.framework.wiring.*; import org.phidias.compile.BundleJavaManager; @@ -47,17 +46,16 @@ import generic.io.NullPrintWriter; import generic.jar.ResourceFile; import ghidra.app.script.*; import ghidra.util.Msg; +import util.CollectionUtils; import utilities.util.FileUtilities; /** - * {@link GhidraSourceBundle} represents a Java source directory that is compiled on build to an OSGi bundle. + * Represents a Java source directory that is compiled on build to an OSGi bundle. * *

A manifest and {@link BundleActivator} are generated if not already present. */ -/** - * - */ public class GhidraSourceBundle extends GhidraBundle { + private static final String INSTRCTION_ACTIVATOR = "org.osgi.framework.BundleActivator"; private static final String GENERATED_ACTIVATOR_CLASSNAME = "GeneratedActivator"; private static final String GENERATED_VERSION = "1.0"; @@ -70,43 +68,53 @@ public class GhidraSourceBundle extends GhidraBundle { private static final Predicate IS_CLASS_FILE = Pattern.compile("(\\$.*)?\\.class", Pattern.CASE_INSENSITIVE).asMatchPredicate(); - protected interface DiscrepencyCallback { + /** + * Used to report source and class file deviation + */ + protected interface DiscrepancyCallback { /** - * Invoked when there is a discrepancy between {@code sourceFile} and its - * corresponding class file(s), {@code classFiles} + * Invoked when there is a discrepancy between {@code sourceFile} and its corresponding + * class file(s), {@code classFiles} * - * @param sourceFile the source file or null to indicate the class files have no corresponding source + * @param sourceFile the source file or null to indicate the class files have no + * corresponding source * @param classFiles corresponding class file(s) * @throws Throwable an exception */ void found(ResourceFile sourceFile, Collection classFiles) throws Throwable; } + // This is a hash; it is used to create a unique directory name for this bundle private final String symbolicName; private final Path binaryDir; - private final String bundleLoc; + /** + * The bundle location id + * @see #getLocationIdentifier() + */ + private final String bundleLocationId; + + // These 2 lists are updated to track source file to class file changes; newSources are those + // that need to be compiled; oldBinaries are those that no longer have source files (such as + // when a source file is deleted) private final List newSources = new ArrayList<>(); private final List oldBinaries = new ArrayList<>(); private JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); - //// information indexed by source file + // information indexed by source file private final Map buildErrors = new HashMap<>(); private final Map> sourceFileToRequirements = new HashMap<>(); private final Map> requirementToSourceFileMap = new HashMap<>(); + private final Set missedRequirements = new HashSet<>(); private final Set importPackageValues = new HashSet<>(); - private Set missedRequirements = new HashSet<>(); - - private long lastCompileAttempt; - /** * Create a new GhidraSourceBundle. * - * @param bundleHost the {@link BundleHost} instance this bundle will belong to + * @param bundleHost the instance this bundle will belong to * @param sourceDirectory the source bundle directory * @param enabled true to start enabled * @param systemBundle true if this is a Ghidra system bundle @@ -115,18 +123,18 @@ public class GhidraSourceBundle extends GhidraBundle { boolean systemBundle) { super(bundleHost, sourceDirectory, enabled, systemBundle); - this.symbolicName = GhidraSourceBundle.sourceDirHash(getSourceDirectory()); - this.binaryDir = GhidraSourceBundle.getCompiledBundlesDir().resolve(symbolicName); - - this.bundleLoc = "reference:file://" + binaryDir.toAbsolutePath().normalize().toString(); + this.symbolicName = sourceDirHash(getSourceDirectory()); + this.binaryDir = getCompiledBundlesDir().resolve(symbolicName); + this.bundleLocationId = + "reference:file://" + binaryDir.toAbsolutePath().normalize().toString(); } /** * (alias of {@link #getFile} for readability) * @return the source directory this bundle represents */ - protected final ResourceFile getSourceDirectory() { - return file; + private final ResourceFile getSourceDirectory() { + return bundleFile; } /** @@ -142,8 +150,8 @@ public class GhidraSourceBundle extends GhidraBundle { } /** - * When a source bundle doesn't have a manifest, Ghidra computes the bundle's - * symbolic name as a hash of the source directory path. + * When a source bundle doesn't have a manifest, Ghidra computes the bundle's symbolic name as + * a hash of the source directory path. * *

This hash is also used as the final path component of the compile destination: *
 {@code $USERHOME/.ghidra/.ghidra_/osgi/compiled-bundles/ } @@ -157,18 +165,6 @@ public class GhidraSourceBundle extends GhidraBundle { return Integer.toHexString(sourceDir.getAbsolutePath().hashCode()); } - /** - * for testing only!!! - * - * @param sourceFile a ghidra script file - * @return the directory its class is compiled to - */ - public static Path getBindirFromScriptFile(ResourceFile sourceFile) { - ResourceFile tmpSourceDir = sourceFile.getParentFile(); - String tmpSymbolicName = GhidraSourceBundle.sourceDirHash(tmpSourceDir); - return GhidraSourceBundle.getCompiledBundlesDir().resolve(tmpSymbolicName); - } - /** * Return the class name corresponding to a script in this source bundle. * @@ -188,16 +184,16 @@ public class GhidraSourceBundle extends GhidraBundle { return relativePath.replace(File.separatorChar, '.'); } - void clearBuildErrors(ResourceFile sourceFile) { + private void clearBuildErrors(ResourceFile sourceFile) { buildErrors.remove(sourceFile); } /** - * append build error + * Append the given build error. * @param sourceFile the file w/ errors * @param err an error string */ - protected void buildError(ResourceFile sourceFile, String err) { + private void buildError(ResourceFile sourceFile, String err) { BuildError error = buildErrors.computeIfAbsent(sourceFile, BuildError::new); error.append(err); } @@ -206,7 +202,7 @@ public class GhidraSourceBundle extends GhidraBundle { * Get any errors associated with building the given source file. * * @param sourceFile the source file - * @return a {@link BuildError} object + * @return the build error or null if no errors */ public BuildError getErrors(ResourceFile sourceFile) { return buildErrors.get(sourceFile); @@ -233,9 +229,10 @@ public class GhidraSourceBundle extends GhidraBundle { } /** - * update buildReqs based on \@importpackage tag in java files in the default(unnamed) package + * Update build requirements based on {@code @importpackage} tag in java files in the + * default (unnamed) package. * - * @throws GhidraBundleException on failure to parse the \@importpackage tag + * @throws GhidraBundleException on failure to parse the {@code @importpackage} tag */ private void updateRequirementsFromMetadata() throws GhidraBundleException { sourceFileToRequirements.clear(); @@ -243,38 +240,42 @@ public class GhidraSourceBundle extends GhidraBundle { importPackageValues.clear(); for (ResourceFile rootSourceFile : getSourceDirectory().listFiles()) { - if (rootSourceFile.getName().endsWith(".java")) { - // without GhidraScriptComponentProvider.updateAvailableScriptFilesForDirectory, - // or GhidraScriptComponentProvider.newScript this might be the earliest need for - // ScriptInfo, so allow construction. + if (!rootSourceFile.getName().endsWith(".java")) { + continue; + } - // NB: ScriptInfo will update field values if lastModified has changed since last time they were computed - String importPackage = parseImportPackageMetadata(rootSourceFile); - if (importPackage != null && !importPackage.isEmpty()) { - importPackageValues.addAll( - ManifestParser.parseDelimitedString(importPackage.strip(), ",")); + // Without GhidraScriptComponentProvider.updateAvailableScriptFilesForDirectory, or + // GhidraScriptComponentProvider.newScript this might be the earliest need for + // ScriptInfo, so allow construction. - List requirements; - try { - requirements = OSGiUtils.parseImportPackage(importPackage); - } - catch (BundleException e) { - throw new GhidraBundleException(getLocationIdentifier(), - "@importpackage error", e); - } - sourceFileToRequirements.put(rootSourceFile, requirements); - for (BundleRequirement requirement : requirements) { - requirementToSourceFileMap - .computeIfAbsent(requirement.toString(), x -> new ArrayList<>()) - .add(rootSourceFile); - } - } + // NB: ScriptInfo will update field values if lastModified has changed since last time + // they were computed + String importPackage = parseImportPackageMetadata(rootSourceFile); + if (importPackage == null || importPackage.isEmpty()) { + continue; + } + + List parts = ManifestParser.parseDelimitedString(importPackage.strip(), ","); + importPackageValues.addAll(parts); + List requirements; + try { + requirements = OSGiUtils.parseImportPackage(importPackage); + } + catch (BundleException e) { + throw new GhidraBundleException(getLocationIdentifier(), + "@importpackage error", e); + } + sourceFileToRequirements.put(rootSourceFile, requirements); + for (BundleRequirement requirement : requirements) { + requirementToSourceFileMap + .computeIfAbsent(requirement.toString(), x -> new ArrayList<>()) + .add(rootSourceFile); } } } /** - * assumes that {@link #updateRequirementsFromMetadata()} has been called recently + * Assumes that {@link #updateRequirementsFromMetadata()} has been called recently * * @return deduped requirements */ @@ -288,30 +289,31 @@ public class GhidraSourceBundle extends GhidraBundle { return dedupedReqs; } - protected ManifestParser createSourceManifestParser() { + private ManifestParser createSourceManifestParser() { ResourceFile manifestFile = getSourceManifestFile(); - if (manifestFile.exists()) { - try (InputStream manifestInputStream = manifestFile.getInputStream()) { - Manifest manifest = new Manifest(manifestInputStream); - Attributes mainAttributes = manifest.getMainAttributes(); - Map headerMap = mainAttributes.entrySet() - .stream() - .collect(Collectors.toMap(e -> e.getKey().toString(), - e -> e.getValue().toString())); - return new ManifestParser(null, null, null, headerMap); - } - catch (IOException | BundleException e) { - throw new RuntimeException(e); - } + if (!manifestFile.exists()) { + return null; + } + + try (InputStream is = manifestFile.getInputStream()) { + Manifest manifest = new Manifest(is); + Attributes mainAttributes = manifest.getMainAttributes(); + Map headerMap = mainAttributes.entrySet() + .stream() + .collect(Collectors.toMap(e -> e.getKey().toString(), + e -> e.getValue().toString())); + return new ManifestParser(null, null, null, headerMap); + } + catch (IOException | BundleException e) { + throw new RuntimeException(e); } - return null; } @Override public List getAllRequirements() throws GhidraBundleException { - ManifestParser manifestParser = createSourceManifestParser(); - if (manifestParser != null) { - return manifestParser.getRequirements(); + ManifestParser parser = createSourceManifestParser(); + if (parser != null) { + return parser.getRequirements(); } updateRequirementsFromMetadata(); @@ -319,17 +321,17 @@ public class GhidraSourceBundle extends GhidraBundle { return new ArrayList<>(reqs.values()); } - protected static void findPackageDirs(List packages, ResourceFile directory) { - for (ResourceFile file : directory - .listFiles(f -> f.isDirectory() || f.getName().endsWith(".java"))) { + private static void findPackageDirs(List packages, ResourceFile dir) { + boolean added = false; + ResourceFile[] files = dir.listFiles(f -> f.isDirectory() || f.getName().endsWith(".java")); + for (ResourceFile file : files) { if (!file.getName().matches("internal|private")) { - boolean added = false; if (file.isDirectory()) { findPackageDirs(packages, file); } else if (!added) { added = true; - packages.add(directory.getAbsolutePath()); + packages.add(dir.getAbsolutePath()); } } } @@ -363,21 +365,23 @@ public class GhidraSourceBundle extends GhidraBundle { return OSGiUtils.parseExportPackage(sb.substring(1)); } catch (BundleException e) { - throw new GhidraBundleException(getLocationIdentifier(), "exports error", e); + throw new GhidraBundleException(getLocationIdentifier(), "Exports error", e); } } /** - * look for new sources, metadata, manifest file. + * Look for new sources, metadata, manifest file. This will find files that need to be + * compiled and files that need to be removed. * - *

if files had errors last time, haven't changed, and no new requirements are available, remove them. + *

If files had errors last time, haven't changed, and no new requirements are available, + * remove them. * * @param writer for reporting status to user * @throws IOException while accessing manifest file * @throws OSGiException while parsing imports */ - void updateFromFilesystem(PrintWriter writer) throws IOException, OSGiException { - // look for new source files + private void updateFromFilesystem(PrintWriter writer) throws IOException, OSGiException { + newSources.clear(); oldBinaries.clear(); @@ -395,13 +399,13 @@ public class GhidraSourceBundle extends GhidraBundle { } }); - // we don't want to rebuild source files that had errors last time and haven't changed, - // so remove them from newSources. Also remove old error messages. - Iterator newSourceIterator = newSources.iterator(); - while (newSourceIterator.hasNext()) { - ResourceFile newSourceFile = newSourceIterator.next(); + // we don't want to rebuild source files that had errors last time and haven't changed, so + // remove them from newSources. Also remove old error messages. + Iterator it = newSources.iterator(); + while (it.hasNext()) { + ResourceFile newSourceFile = it.next(); if (stillHasErrors(newSourceFile)) { - newSourceIterator.remove(); + it.remove(); } else { // any errors are old, so remove them @@ -421,46 +425,40 @@ public class GhidraSourceBundle extends GhidraBundle { } private void deleteOldBinaries() throws IOException { - // dedupe and omit files that don't exist + // dedup and omit files that don't exist oldBinaries.sort(null); - Iterable paths = - () -> oldBinaries.stream().distinct().filter(Files::exists).iterator(); - for (Path path : paths) { + Iterator toDelete = + oldBinaries.stream().distinct().filter(Files::exists).sorted().iterator(); + for (Path path : CollectionUtils.asIterable(toDelete)) { Files.delete(path); } + + oldBinaries.clear(); } - int getBuildErrorCount() { + private int getBuildErrorCount() { return buildErrors.size(); } - int getNewSourcesCount() { + private int getNewSourcesCount() { return newSources.size(); } /** - * used just after {@link #build} to get the newly compiled source files + * Used just after {@link #build} to get the newly compiled source files. * @return new source files */ public List getNewSources() { - return newSources; - } - - void compileAttempted() { - lastCompileAttempt = System.currentTimeMillis(); - } - - long getLastCompileAttempt() { - return lastCompileAttempt; + return Collections.unmodifiableList(newSources); } @Override public String getLocationIdentifier() { - return bundleLoc; + return bundleLocationId; } - ResourceFile getSourceManifestFile() { + private ResourceFile getSourceManifestFile() { return new ResourceFile(getSourceDirectory(), "META-INF" + File.separator + "MANIFEST.MF"); } @@ -468,19 +466,20 @@ public class GhidraSourceBundle extends GhidraBundle { return binaryDir.resolve("META-INF").resolve("MANIFEST.MF"); } - boolean hasSourceManifest() { + private boolean hasSourceManifest() { return getSourceManifestFile().exists(); } - boolean hasNewManifest() { + private boolean hasNewManifest() { ResourceFile sourceManifest = getSourceManifestFile(); Path binaryManifest = getBinaryManifestPath(); - return sourceManifest.exists() && (Files.notExists(binaryManifest) || - sourceManifest.lastModified() > binaryManifest.toFile().lastModified()); + boolean oldOrMissingBinaryManifest = Files.notExists(binaryManifest) || + sourceManifest.lastModified() > binaryManifest.toFile().lastModified(); + return sourceManifest.exists() && oldOrMissingBinaryManifest; } - protected static boolean wipeContents(Path path) throws IOException { + private static boolean wipeContents(Path path) throws IOException { if (Files.exists(path)) { boolean anythingDeleted = false; try (Stream walk = Files.walk(path)) { @@ -498,12 +497,12 @@ public class GhidraSourceBundle extends GhidraBundle { } /** - * if source with a previous requirement error now resolves, add it to newSources. + * If source with a previous requirement error now resolves, add it to newSources. * - *

The reason for the previous build error isn't necessarily a missing requirement, - * but this shouldn't be too expensive. + *

The reason for the previous build error isn't necessarily a missing requirement, but this + * shouldn't be too expensive. */ - private void addSourcesIfResolutionWillPass() { + private void addSourcesThatNoLongerHaveMissingRequirements() { for (ResourceFile sourceFile : buildErrors.keySet()) { List requirements = sourceFileToRequirements.get(sourceFile); if (requirements != null && !requirements.isEmpty() && @@ -519,10 +518,10 @@ public class GhidraSourceBundle extends GhidraBundle { } /** - * if a file that previously built without errors is now missing some requirements, - * rebuild it to capture errors (if any). + * If a file that previously built without errors is now missing some requirements, rebuild it + * to capture errors (if any). */ - void addSourcesIfResolutionWillFail() { + private void addSourcesThatNowHaveMissingRequirements() { // if previous successes no longer resolve, (cleanup) and try again for (Entry> e : sourceFileToRequirements.entrySet()) { ResourceFile sourceFile = e.getKey(); @@ -542,39 +541,42 @@ public class GhidraSourceBundle extends GhidraBundle { @Override public boolean build(PrintWriter writer) throws Exception { - if (writer == null) { - writer = new NullPrintWriter(); - } + writer = NullPrintWriter.dummyIfNull(writer); boolean needsCompile = false; if (hasSourceManifest()) { sourceFileToRequirements.clear(); requirementToSourceFileMap.clear(); - ArrayList reqs = new ArrayList<>(getAllRequirements()); + List reqs = new ArrayList<>(getAllRequirements()); bundleHost.resolve(reqs); - HashSet newMissedRequirements = new HashSet<>(); + Set newMissedRequirements = new HashSet<>(); for (BundleRequirement req : reqs) { newMissedRequirements.add(req.toString()); } if (hasNewManifest() || !newMissedRequirements.equals(missedRequirements)) { - missedRequirements = newMissedRequirements; + missedRequirements.clear(); + missedRequirements.addAll(newMissedRequirements); wipeBinDir(); buildErrors.clear(); } updateFromFilesystem(writer); } else { - updateFromFilesystem(writer); + // this gets all source files from the file system that we know need to be compiled + // based upon missing or outdated class files + updateFromFilesystem(writer); updateRequirementsFromMetadata(); - addSourcesIfResolutionWillPass(); - addSourcesIfResolutionWillFail(); + + // these 2 calls handle source files that need to be compiled based upon changes in + // added or removed bundles / requirements + addSourcesThatNoLongerHaveMissingRequirements(); + addSourcesThatNowHaveMissingRequirements(); } int buildErrorsLastTime = getBuildErrorCount(); int newSourceCount = getNewSourcesCount(); - if (newSourceCount == 0) { if (buildErrorsLastTime > 0) { writer.printf("%s hasn't changed, with %d file%s failing in previous build(s):\n", @@ -600,7 +602,7 @@ public class GhidraSourceBundle extends GhidraBundle { bundleHost.deactivateSynchronously(osgiBundle); } - // once we've committed to recompile and regenerate generated classes, delete the old stuff + // once we've committed to recompile and regenerate classes, delete the old stuff deleteOldBinaries(); String summary = compileToExplodedBundle(writer); @@ -635,16 +637,19 @@ public class GhidraSourceBundle extends GhidraBundle { private ResourceFile[] correspondingBinaries(ResourceFile source) { String parentPath = source.getParentFile().getAbsolutePath(); - String relPath = parentPath.substring(getSourceDirectory().getAbsolutePath().length()); - if (relPath.startsWith(File.separator)) { - relPath = relPath.substring(1); + int sourceDirLength = getSourceDirectory().getAbsolutePath().length(); + String relativePath = parentPath.substring(sourceDirLength); + if (relativePath.startsWith(File.separator)) { + relativePath = relativePath.substring(1); } - String className0 = source.getName(); - String className = className0.substring(0, className0.length() - 5);// drop ".java" - ResourceFile binarySubdir = new ResourceFile(binaryDir.resolve(relPath).toFile()); + + String javaFileName = source.getName(); + String className = javaFileName.substring(0, javaFileName.length() - 5); // drop ".java" + ResourceFile binarySubdir = new ResourceFile(binaryDir.resolve(relativePath).toFile()); if (!binarySubdir.exists() || !binarySubdir.isDirectory()) { return new ResourceFile[] {}; } + return binarySubdir.listFiles(f -> { String fileName = f.getName(); return fileName.startsWith(className) && @@ -674,23 +679,23 @@ public class GhidraSourceBundle extends GhidraBundle { * binary_root/com/blah/Blah$Inner$Innerer.class * ... * - * @param callback callback + * @param discrepancy the discrepancy callback */ - protected void visitDiscrepancies(DiscrepencyCallback callback) { + protected void visitDiscrepancies(DiscrepancyCallback discrepancy) { try { - Deque stack = new ArrayDeque<>(); - // start in the source directory root - stack.add(getSourceDirectory()); + Deque stack = new ArrayDeque<>(); + ResourceFile sourceDir = getSourceDirectory(); + stack.add(sourceDir); // start in the source directory root while (!stack.isEmpty()) { ResourceFile sourceSubdir = stack.pop(); String relPath = sourceSubdir.getAbsolutePath() - .substring(getSourceDirectory().getAbsolutePath().length()); + .substring(sourceDir.getAbsolutePath().length()); if (relPath.startsWith(File.separator)) { relPath = relPath.substring(1); } - Path binarySubdir = binaryDir.resolve(relPath); + Path binarySubdir = binaryDir.resolve(relPath); ClassMapper mapper = new ClassMapper(binarySubdir); // for each source file, lookup class files by class name @@ -701,13 +706,13 @@ public class GhidraSourceBundle extends GhidraBundle { else { List classFiles = mapper.findAndRemove(sourceFile); if (classFiles != null) { - callback.found(sourceFile, classFiles); + discrepancy.found(sourceFile, classFiles); } } } // any remaining .class files are missing .java files if (mapper.hasExtraClassFiles()) { - callback.found(null, mapper.extraClassFiles()); + discrepancy.found(null, mapper.extraClassFiles()); } } } @@ -716,12 +721,15 @@ public class GhidraSourceBundle extends GhidraBundle { } } - /* requirements that resolve internally are never "missing", but will only resolve _after_ build/install */ + /* + * Requirements that resolve internally are never "missing", but resolve _after_ build/install + */ private boolean resolveInternally(List requirements) throws GhidraBundleException { if (requirements.isEmpty()) { return true; } + List capabilities = getAllCapabilities(); Iterator requirementIterator = requirements.iterator(); boolean anyMissing = false; @@ -738,73 +746,79 @@ public class GhidraSourceBundle extends GhidraBundle { } /* - * when calling the java compiler programmatically, we map import requests to files with - * a custom {@link JavaFileManager}. We wrap the system JavaFileManager with one that - * handles ResourceFiles then wrap that with Phidia, which handles imports based on - * bundle requirements. + * When calling the java compiler programmatically, we map import requests to files with a + * custom {@link JavaFileManager}. We wrap the system JavaFileManager with one that handles + * ResourceFiles then wrap that with phidias, which handles imports based on bundle + * requirements. */ private BundleJavaManager createBundleJavaManager(PrintWriter writer, Summary summary, List options) throws IOException, GhidraBundleException { - final ResourceFileJavaFileManager resourceFileJavaManager = new ResourceFileJavaFileManager( - Collections.singletonList(getSourceDirectory()), buildErrors.keySet()); + ResourceFileJavaFileManager resourceFileJavaManager = new ResourceFileJavaFileManager( + Collections.singletonList(getSourceDirectory()), buildErrors.keySet()); BundleJavaManager bundleJavaManager = new MyBundleJavaManager(bundleHost.getHostFramework(), resourceFileJavaManager, options); // The phidias BundleJavaManager is for compiling from within a bundle -- it makes the - // bundle dependencies available to the compiler classpath. Here, we are compiling in an as-yet - // non-existing bundle, so we forge the wiring based on @importpackage metadata. + // bundle dependencies available to the compiler classpath. Here, we are compiling in an + // as-yet non-existing bundle, so we forge the wiring based on @importpackage metadata. // get wires for currently active bundles to satisfy all requirements List requirements = getAllRequirements(); List bundleWirings = bundleHost.resolve(requirements); - if (!resolveInternally(requirements)) { - writer.printf("%d import requirement%s remain%s unresolved:\n", requirements.size(), - requirements.size() > 1 ? "s" : "", requirements.size() > 1 ? "" : "s"); - for (BundleRequirement requirement : requirements) { - List requiringFiles = - requirementToSourceFileMap.get(requirement.toString()); - if (requiringFiles != null && requiringFiles.size() > 0) { - writer.printf(" %s, from %s\n", requirement.toString(), - requiringFiles.stream() - .map(generic.util.Path::toPathString) - .collect(Collectors.joining(","))); - for (ResourceFile sourceFile : requiringFiles) { - buildError(sourceFile, - generic.util.Path.toPathString(sourceFile) + " : failed import " + - OSGiUtils.extractPackageNamesFromFailedResolution( - requirement.toString())); - } - } - else { - writer.printf(" %s\n", requirement.toString()); - } - } - - summary.printf("%d missing package import%s:%s", requirements.size(), - requirements.size() > 1 ? "s" : "", - requirements.stream() - .flatMap( - r -> OSGiUtils.extractPackageNamesFromFailedResolution(r.toString()) - .stream()) - .distinct() - .collect(Collectors.joining(","))); + writeErrorUnresolved(writer, summary, requirements); } + // send the capabilities to phidias bundleWirings.forEach(bundleJavaManager::addBundleWiring); return bundleJavaManager; } + private void writeErrorUnresolved(PrintWriter writer, Summary summary, + List requirements) { + + writer.printf("%d import requirement%s remain%s unresolved:\n", requirements.size(), + requirements.size() > 1 ? "s" : "", requirements.size() > 1 ? "" : "s"); + for (BundleRequirement requirement : requirements) { + List requiringFiles = + requirementToSourceFileMap.get(requirement.toString()); + if (requiringFiles != null && requiringFiles.size() > 0) { + writer.printf(" %s, from %s\n", requirement.toString(), + requiringFiles.stream() + .map(generic.util.Path::toPathString) + .collect(Collectors.joining(","))); + + for (ResourceFile sourceFile : requiringFiles) { + buildError(sourceFile, + generic.util.Path.toPathString(sourceFile) + " : failed import " + + OSGiUtils.extractPackageNamesFromFailedResolution( + requirement.toString())); + } + } + else { + writer.printf(" %s\n", requirement.toString()); + } + } + + String singularity = requirements.size() > 1 ? "s" : ""; + String missing = requirements.stream() + .flatMap( + r -> OSGiUtils.extractPackageNamesFromFailedResolution(r.toString()).stream()) + .distinct() + .collect(Collectors.joining(",")); + summary.printf("%d missing package import%s:%s", requirements.size(), singularity, missing); + } + /* - * Try building sourcefiles.. on success return true. + * Try building source files. On success return true. * * If build fails, collect errors, remove files that caused * errors from source files, and return false. */ private boolean tryBuild(PrintWriter writer, BundleJavaManager bundleJavaManager, List sourceFiles, List options) throws IOException { - DiagnosticCollector diagnostics = new DiagnosticCollector(); + DiagnosticCollector diagnostics = new DiagnosticCollector<>(); JavaCompiler.CompilationTask task = compiler.getTask(writer, bundleJavaManager, diagnostics, options, null, sourceFiles); @@ -812,6 +826,7 @@ public class GhidraSourceBundle extends GhidraBundle { if (successfulCompilation) { return true; } + Set filesWithErrors = new HashSet<>(); for (Diagnostic diagnostic : diagnostics.getDiagnostics()) { String error = diagnostic.toString() + "\n"; @@ -836,9 +851,9 @@ public class GhidraSourceBundle extends GhidraBundle { } /** - * generate a manifest (and an activator) + * Generate a manifest (and an activator) * - * assumes that {@link #updateRequirementsFromMetadata()} has been called recently + *

Assumes that {@link #updateRequirementsFromMetadata()} has been called recently */ private String generateManifest(PrintWriter writer, Summary summary, Path binaryManifest) throws OSGiException, IOException { @@ -849,14 +864,14 @@ public class GhidraSourceBundle extends GhidraBundle { GhidraSourceBundle.sourceDirHash(getSourceDirectory())); analyzer.setProperty("Bundle-Version", GENERATED_VERSION); - if (!importPackageValues.isEmpty()) { + if (importPackageValues.isEmpty()) { + analyzer.setProperty("Import-Package", "*"); + } + else { // constrain analyzed imports according to what's declared in @importpackage tags analyzer.setProperty("Import-Package", importPackageValues.stream().collect(Collectors.joining(",")) + ",*"); } - else { - analyzer.setProperty("Import-Package", "*"); - } analyzer.setProperty("Export-Package", "!*.private.*,!*.internal.*,*"); @@ -866,44 +881,13 @@ public class GhidraSourceBundle extends GhidraBundle { manifest = analyzer.calcManifest(); } catch (Exception e) { - summary.print("bad manifest"); - throw new OSGiException("failed to calculate manifest by analyzing code", e); + summary.print("Bad manifest"); + throw new OSGiException("Failed to calculate manifest by analyzing code", e); } - String activatorClassName = null; - try { - for (Clazz clazz : analyzer.getClassspace().values()) { - if (clazz.is(QUERY.IMPLEMENTS, - new Instruction("org.osgi.framework.BundleActivator"), analyzer)) { - System.err.printf("found BundleActivator class %s\n", clazz); - activatorClassName = clazz.toString(); - } - } + if (!addActivatorClass(writer, analyzer, manifest, summary)) { + return summary.getValue(); } - catch (Exception e) { - summary.print("failed bnd analysis"); - throw new OSGiException("failed to query classes while searching for activator", e); - } - - Attributes manifestAttributes = manifest.getMainAttributes(); - if (activatorClassName == null) { - activatorClassName = GENERATED_ACTIVATOR_CLASSNAME; - if (!buildDefaultActivator(binaryDir, activatorClassName, writer)) { - summary.print("failed to build generated activator"); - return summary.getValue(); - } - // since we add the activator after bndtools built the imports, we should add its imports too - String imps = manifestAttributes.getValue(Constants.IMPORT_PACKAGE); - if (imps == null) { - manifestAttributes.putValue(Constants.IMPORT_PACKAGE, - GhidraBundleActivator.class.getPackageName()); - } - else { - manifestAttributes.putValue(Constants.IMPORT_PACKAGE, - imps + "," + GhidraBundleActivator.class.getPackageName()); - } - } - manifestAttributes.putValue(Constants.BUNDLE_ACTIVATOR, activatorClassName); // write the manifest Files.createDirectories(binaryManifest.getParent()); @@ -917,6 +901,50 @@ public class GhidraSourceBundle extends GhidraBundle { return summary.getValue(); } + private boolean addActivatorClass(PrintWriter writer, Analyzer analyzer, Manifest manifest, + Summary summary) throws OSGiException, IOException { + + String activatorClassName = null; + try { + for (Clazz clazz : analyzer.getClassspace().values()) { + if (clazz.is(QUERY.IMPLEMENTS, + new Instruction(INSTRCTION_ACTIVATOR), analyzer)) { + Msg.trace(this, "Found BundleActivator class " + clazz); + activatorClassName = clazz.toString(); + } + } + } + catch (Exception e) { + summary.print("Failed bnd analysis"); + throw new OSGiException("Failed to query classes while searching for activator", e); + } + + Attributes manifestAttributes = manifest.getMainAttributes(); + if (activatorClassName != null) { + manifestAttributes.putValue(Constants.BUNDLE_ACTIVATOR, activatorClassName); + return true; + } + + activatorClassName = GENERATED_ACTIVATOR_CLASSNAME; + if (!buildDefaultActivator(binaryDir, activatorClassName, writer)) { + summary.print("Failed to build generated activator"); + return false; + } + + // since we add the activator after bndtools built the imports, add its imports too + String imports = manifestAttributes.getValue(Constants.IMPORT_PACKAGE); + String activatorPackageName = GhidraBundleActivator.class.getPackageName(); + if (imports == null) { + manifestAttributes.putValue(Constants.IMPORT_PACKAGE, activatorPackageName); + } + else { + manifestAttributes.putValue(Constants.IMPORT_PACKAGE, imports + "," + + activatorPackageName); + } + manifestAttributes.putValue(Constants.BUNDLE_ACTIVATOR, activatorClassName); + return true; + } + /** * create and compile a default bundle activator * @@ -928,8 +956,8 @@ public class GhidraSourceBundle extends GhidraBundle { */ private boolean buildDefaultActivator(Path bindir, String activatorClassName, Writer writer) throws IOException { - Path activatorSourceFileName = bindir.resolve(activatorClassName + ".java"); + Path activatorSourceFileName = bindir.resolve(activatorClassName + ".java"); try (PrintWriter activatorWriter = new PrintWriter( Files.newBufferedWriter(activatorSourceFileName, Charset.forName("UTF-8")))) { activatorWriter.println("import " + GhidraBundleActivator.class.getName() + ";"); @@ -959,35 +987,36 @@ public class GhidraSourceBundle extends GhidraBundle { try (StandardJavaFileManager javaFileManager = compiler.getStandardFileManager(null, null, null); - BundleJavaManager bundleJavaManager = new MyBundleJavaManager( + BundleJavaManager bundleManager = new MyBundleJavaManager( bundleHost.getHostFramework(), javaFileManager, options);) { Iterable sourceFiles = javaFileManager.getJavaFileObjectsFromPaths(List.of(activatorSourceFileName)); DiagnosticCollector diagnostics = - new DiagnosticCollector(); - JavaCompiler.CompilationTask task = compiler.getTask(writer, bundleJavaManager, + new DiagnosticCollector<>(); + JavaCompiler.CompilationTask task = compiler.getTask(writer, bundleManager, diagnostics, options, null, sourceFiles); - if (!task.call()) { - for (Diagnostic diagnostic : diagnostics - .getDiagnostics()) { - writer.write(diagnostic.getSource().toString() + ": " + - diagnostic.getMessage(null) + "\n"); - } - return false; + if (task.call()) { + return true; } - return true; + + for (Diagnostic diagnostic : diagnostics + .getDiagnostics()) { + writer.write(diagnostic.getSource().toString() + ": " + + diagnostic.getMessage(null) + "\n"); + } + return false; } } /** - * compile a source directory to an exploded bundle + * Compile a source directory to an exploded bundle. * * @param writer for updating the user during compilation + * @return a summary of the work performed * @throws IOException for source/manifest file reading/generation and binary deletion/creation * @throws OSGiException if generation of bundle metadata fails */ private String compileToExplodedBundle(PrintWriter writer) throws IOException, OSGiException { - compileAttempted(); Files.createDirectories(binaryDir); @@ -1047,8 +1076,12 @@ public class GhidraSourceBundle extends GhidraBundle { } } +//================================================================================================= +// Inner Classes +//================================================================================================= + private static class MyBundleJavaManager extends BundleJavaManager { - static URL[] EMPTY_URL_ARRAY = new URL[0]; + private static URL[] EMPTY_URL_ARRAY = new URL[0]; MyBundleJavaManager(Bundle bundle, JavaFileManager javaFileManager, List options) throws IOException { @@ -1056,20 +1089,19 @@ public class GhidraSourceBundle extends GhidraBundle { } /** - * since the JavaCompiler tasks can close the class loader returned by this - * method, make sure we're returning a copy. + * Since the JavaCompiler tasks can close the class loader returned by this method, make + * sure we're returning a copy. */ @Override public ClassLoader getClassLoader() { return new URLClassLoader(EMPTY_URL_ARRAY, super.getClassLoader()); } - } private static class Summary { - static String SEPERATOR = ", "; - final StringWriter stringWriter = new StringWriter(); - final PrintWriter printWriter = new PrintWriter(stringWriter, true); + private static final String SEPERATOR = ", "; + private final StringWriter stringWriter = new StringWriter(); + private final PrintWriter printWriter = new PrintWriter(stringWriter, true); void printf(String format, Object... args) { if (stringWriter.getBuffer().length() > 0) { @@ -1096,12 +1128,12 @@ public class GhidraSourceBundle extends GhidraBundle { * "B" -> [directory/B.class, directory/B$inner.class] * * - *

A list of classes are then processed one at a time with {@link ClassMapper#findAndRemove}. + *

A list of classes are then processed with {@link ClassMapper#findAndRemove}. * *

After processing, "extras" are handled with {@link ClassMapper#extraClassFiles}. */ private static class ClassMapper { - final Map> classToClassFilesMap; + private final Map> classToClassFilesMap; /** * Map classes in {@code directory} with {@link ClassMapper}. @@ -1129,8 +1161,7 @@ public class GhidraSourceBundle extends GhidraBundle { if (money >= 0) { return fileName.substring(0, money); } - // drop ".class" - return fileName.substring(0, fileName.length() - 6); + return fileName.substring(0, fileName.length() - 6); // drop ".class" } List findAndRemove(ResourceFile sourceFile) { @@ -1139,7 +1170,7 @@ public class GhidraSourceBundle extends GhidraBundle { return null; } - className = className.substring(0, className.length() - 5); + className = className.substring(0, className.length() - 5); // drop ".java" long lastModifiedSource = sourceFile.lastModified(); List classFiles = classToClassFilesMap.remove(className); if (classFiles == null) { @@ -1158,17 +1189,15 @@ public class GhidraSourceBundle extends GhidraBundle { return null; } - public boolean hasExtraClassFiles() { + boolean hasExtraClassFiles() { return !classToClassFilesMap.isEmpty(); } - public Collection extraClassFiles() { + Collection extraClassFiles() { return classToClassFilesMap.values() .stream() .flatMap(l -> l.stream()) .collect(Collectors.toList()); } - } - } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/OSGiUtils.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/OSGiUtils.java index 50e6ced1c5..9b5104bdcd 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/OSGiUtils.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/OSGiUtils.java @@ -281,5 +281,4 @@ public class OSGiUtils { Msg.error(OSGiUtils.class, "Error while collecting packages from jar", e); } } - } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptComponentProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptComponentProvider.java index e3f3b08ab8..ebfe9e71da 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptComponentProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptComponentProvider.java @@ -1166,7 +1166,7 @@ public class GhidraScriptComponentProvider extends ComponentProviderAdapter { } } - class RefreshingBundleHostListener implements BundleHostListener { + private class RefreshingBundleHostListener implements BundleHostListener { @Override public void bundleBuilt(GhidraBundle bundle, String summary) { @@ -1179,8 +1179,8 @@ public class GhidraScriptComponentProvider extends ComponentProviderAdapter { GhidraSourceBundle sourceBundle = (GhidraSourceBundle) bundle; ResourceFile sourceDirectory = sourceBundle.getFile(); if (summary == null) { - // a null summary means the build didn't change anything, - // so use any errors from the last build + // a null summary means the build didn't change anything, so use any errors from + // the last build for (ResourceFile sourceFile : sourceBundle.getAllErrors().keySet()) { if (sourceFile.getParentFile().equals(sourceDirectory)) { ScriptInfo scriptInfo = infoManager.getScriptInfo(sourceFile); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/script/GhidraScriptUtil.java b/Ghidra/Features/Base/src/main/java/ghidra/app/script/GhidraScriptUtil.java index 6099eceeef..382b7c67aa 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/script/GhidraScriptUtil.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/script/GhidraScriptUtil.java @@ -83,7 +83,7 @@ public class GhidraScriptUtil { } /** - * initialize state of GhidraScriptUtil with user, system paths, and optional extra system paths. + * Initialize state of GhidraScriptUtil with user, system, and optional extra system paths. * * @param aBundleHost the host to use * @param extraSystemPaths additional system paths for this run, can be null @@ -106,7 +106,7 @@ public class GhidraScriptUtil { */ public static void dispose() { if (bundleHost != null) { - bundleHost.dispose(); + bundleHost.stopFramework(); bundleHost = null; } providers = null; diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/script/JavaScriptProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/script/JavaScriptProvider.java index 5c1e34d5c2..a958cd6c8b 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/script/JavaScriptProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/script/JavaScriptProvider.java @@ -121,8 +121,8 @@ public class JavaScriptProvider extends GhidraScriptProvider { throw new ClassNotFoundException( "Failed to find source bundle containing script: " + sourceFile.toString()); } - bundleHost.activateAll(Collections.singletonList(bundle), TaskMonitor.DUMMY, writer); + bundleHost.activateAll(Collections.singletonList(bundle), TaskMonitor.DUMMY, writer); String classname = bundle.classNameForScript(sourceFile); Class clazz = bundle.getOSGiBundle().loadClass(classname); // throws ClassNotFoundException return clazz; diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/script/BundleHostTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/script/BundleHostTest.java index 349ef1ab6f..9e7a070568 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/script/BundleHostTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/script/BundleHostTest.java @@ -87,7 +87,7 @@ public class BundleHostTest extends AbstractGhidraHeadlessIntegrationTest { @After public void tearDown() { - bundleHost.dispose(); + bundleHost.stopFramework(); capturingBundleHostListener = null; bundleHost = null; diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/script/BundleStatusManagerTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/script/BundleStatusManagerTest.java index 29b9c33716..6a7a075dfe 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/script/BundleStatusManagerTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/script/BundleStatusManagerTest.java @@ -435,11 +435,11 @@ public class BundleStatusManagerTest extends AbstractGhidraScriptMgrPluginTest { } void awaitActivation() throws InterruptedException { - assertTrue(activationLatch.await(5000, TimeUnit.MILLISECONDS)); + assertTrue(activationLatch.await(30000, TimeUnit.MILLISECONDS)); } void awaitDisablement() throws InterruptedException { - assertTrue(disablementLatch.await(5000, TimeUnit.MILLISECONDS)); + assertTrue(disablementLatch.await(30000, TimeUnit.MILLISECONDS)); } } diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/script/GhidraScriptMgrPlugin2Test.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/script/GhidraScriptMgrPlugin2Test.java index 2941477638..7a9f5b9f90 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/script/GhidraScriptMgrPlugin2Test.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/script/GhidraScriptMgrPlugin2Test.java @@ -18,6 +18,7 @@ package ghidra.app.plugin.core.script; import static org.junit.Assert.*; import java.io.*; +import java.nio.file.Path; import org.apache.logging.log4j.*; import org.apache.logging.log4j.core.config.Configurator; @@ -150,7 +151,7 @@ public class GhidraScriptMgrPlugin2Test extends AbstractGhidraScriptMgrPluginTes // remove all class files from the user script bin dir File userScriptsBinDir = - GhidraSourceBundle.getBindirFromScriptFile(new ResourceFile(newScriptFile)).toFile(); + getBinDirFromScriptFile(new ResourceFile(newScriptFile)).toFile(); File[] userScriptBinDirFiles; if (userScriptsBinDir.exists()) { userScriptBinDirFiles = userScriptsBinDir.listFiles(classFileFilter); @@ -195,7 +196,7 @@ public class GhidraScriptMgrPlugin2Test extends AbstractGhidraScriptMgrPluginTes // verify that the generated class file is placed in the default scripting home/bin File userScriptsBinDir = - GhidraSourceBundle.getBindirFromScriptFile(systemScriptFile).toFile(); + getBinDirFromScriptFile(systemScriptFile).toFile(); String className = scriptName.replace(".java", ".class"); File expectedClassFile = new File(userScriptsBinDir, className); @@ -232,7 +233,7 @@ public class GhidraScriptMgrPlugin2Test extends AbstractGhidraScriptMgrPluginTes waitForScriptCompletion(scriptID, 20000); // verify a bin dir was created and that the class file is in it - File binDir = GhidraSourceBundle.getBindirFromScriptFile(newScriptFile).toFile(); + File binDir = getBinDirFromScriptFile(newScriptFile).toFile(); assertTrue("bin output dir not created", binDir.exists()); File scriptClassFile = new File(binDir, rawScriptName + ".class"); @@ -493,4 +494,11 @@ public class GhidraScriptMgrPlugin2Test extends AbstractGhidraScriptMgrPluginTes assertContainsText("The field of the script still has state--the script was not recreated", "*2*", output); } + + private Path getBinDirFromScriptFile(ResourceFile sourceFile) { + ResourceFile tmpSourceDir = sourceFile.getParentFile(); + String tmpSymbolicName = GhidraSourceBundle.sourceDirHash(tmpSourceDir); + return GhidraSourceBundle.getCompiledBundlesDir().resolve(tmpSymbolicName); + } + } diff --git a/Ghidra/Features/Base/src/test/java/ghidra/test/JavaCompiler.java b/Ghidra/Features/Base/src/test/java/ghidra/test/JavaCompiler.java index 9ded6b8a91..82b2f7b82b 100644 --- a/Ghidra/Features/Base/src/test/java/ghidra/test/JavaCompiler.java +++ b/Ghidra/Features/Base/src/test/java/ghidra/test/JavaCompiler.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. @@ -24,69 +23,62 @@ import java.io.*; * * */ +@Deprecated(forRemoval = true, since = "10.2") // This is not used public class JavaCompiler { private IOThread cmdOut; private IOThread cmdErr; - /** - * Compile a java file. - */ - public void compile(File javaFile) { + public void compile(File javaFile) { String name = javaFile.getName(); String className = name.substring(0, name.indexOf(".")) + ".class"; - + File parent = javaFile.getParentFile(); String parentPath = parent.getAbsolutePath(); int pos = parentPath.lastIndexOf("ghidra"); - String destPath = parentPath.substring(0, pos-1); - + String destPath = parentPath.substring(0, pos - 1); + javaFile.deleteOnExit(); - + File classFile = new File(parent, className); classFile.deleteOnExit(); - + String classpath = System.getProperty("java.class.path"); String javaLoc = System.getProperty("java.home"); if (javaLoc.endsWith("jre")) { - javaLoc = javaLoc.substring(0, javaLoc.indexOf("jre")-1); + javaLoc = javaLoc.substring(0, javaLoc.indexOf("jre") - 1); } String argV[] = new String[6]; - argV[0] = javaLoc + File.separator + "bin" + File.separator +"javac"; + argV[0] = javaLoc + File.separator + "bin" + File.separator + "javac"; argV[1] = "-classpath"; argV[2] = classpath; argV[3] = "-d"; argV[4] = destPath; - argV[5] = javaFile.getAbsolutePath(); + argV[5] = javaFile.getAbsolutePath(); try { Process p = Runtime.getRuntime().exec(argV); - for (int i=0; iFor example, given, in this order, two files with these paths * /a/b and /a/b/c, this method will return 'c'. * * @param f1 the parent resource file * @param f2 the child resource file - * @return the relative path of {@code f2} in {@code f1} + * @return the relative path of {@code f2} in {@code f1}; null if f1 is not a parent of f2 */ public static String relativizePath(ResourceFile f1, ResourceFile f2) { StringBuilder sb = new StringBuilder(f2.getName());