osgi: ignore optional requirements and include versions in extra packages

This commit is contained in:
Jason P. Leasure 2021-05-17 16:27:55 -04:00
parent fbc8a6fb05
commit fc00b774d5
3 changed files with 122 additions and 8 deletions

View file

@ -26,6 +26,7 @@ import java.util.stream.Collectors;
import org.apache.felix.framework.FrameworkFactory;
import org.apache.felix.framework.util.FelixConstants;
import org.apache.felix.framework.wiring.BundleRequirementImpl;
import org.jgrapht.graph.DirectedMultigraph;
import org.jgrapht.traverse.TopologicalOrderIterator;
import org.osgi.framework.*;
@ -691,7 +692,13 @@ public class BundleHost {
Map<GhidraBundle, List<BundleRequirement>> requirementMap = new HashMap<>();
for (GhidraBundle bundle : bundles) {
try {
requirementMap.put(bundle, bundle.getAllRequirements());
List<BundleRequirement> requirements = bundle.getAllRequirements();
// remove optional requirements
requirements.removeIf(r -> {
BundleRequirementImpl rimpl = (BundleRequirementImpl) r;
return rimpl.isOptional();
});
requirementMap.put(bundle, requirements);
}
catch (GhidraBundleException e) {
fireBundleException(e);

View file

@ -129,6 +129,10 @@ public class OSGiUtils {
static List<BundleRequirement> parseImportPackage(String importPackageString)
throws BundleException {
Map<String, Object> headerMap = new HashMap<>();
// assume version 2 for a more robust parse
headerMap.put(Constants.BUNDLE_MANIFESTVERSION, "2");
// symbolic name is required for version 2 bundle manifest
headerMap.put(Constants.BUNDLE_SYMBOLICNAME, Constants.SYSTEM_BUNDLE_SYMBOLICNAME);
headerMap.put(Constants.IMPORT_PACKAGE, importPackageString);
ManifestParser manifestParser = new ManifestParser(null, null, null, headerMap);
return manifestParser.getRequirements();
@ -144,6 +148,10 @@ public class OSGiUtils {
static List<BundleCapability> parseExportPackage(String exportPackageString)
throws BundleException {
Map<String, Object> headerMap = new HashMap<>();
// assume version 2 for a more robust parse
headerMap.put(Constants.BUNDLE_MANIFESTVERSION, "2");
// symbolic name is required for version 2 bundle manifest
headerMap.put(Constants.BUNDLE_SYMBOLICNAME, Constants.SYSTEM_BUNDLE_SYMBOLICNAME);
headerMap.put(Constants.EXPORT_PACKAGE, exportPackageString);
ManifestParser manifestParser = new ManifestParser(null, null, null, headerMap);
return manifestParser.getCapabilities();
@ -205,15 +213,64 @@ public class OSGiUtils {
}
}
static private boolean hasEvenQuoteCount(String s) {
return s.chars().filter(c -> c == '"').count() % 2 == 0;
}
static void collectPackagesFromJar(Path jarPath, Set<String> packages) {
try {
try (JarFile j = new JarFile(jarPath.toFile())) {
j.stream().filter(entry -> entry.getName().endsWith(".class")).forEach(jarEntry -> {
String entryName = jarEntry.getName();
int lastSlash = entryName.lastIndexOf('/');
packages.add(
lastSlash > 0 ? entryName.substring(0, lastSlash).replace('/', '.') : "");
});
try (JarFile jarFile = new JarFile(jarPath.toFile())) {
// if this jar is an OSGi bundle, use its declared exports
String exportPackageString =
jarFile.getManifest().getMainAttributes().getValue(Constants.EXPORT_PACKAGE);
if (exportPackageString != null) {
String saved = null;
/*
* split on commas not contained in quotes.
*
* e.g.
* org.foo,org.bar;uses="org.baz,org.qux"
* ^- should split here ^- not here
*
* We first split on all commas. The first entry,
* org.foo
* has an even number of quotes, so it's added as is to packages.
* The second entry,
* org.bar;uses="org.baz
* has an odd number of quotes, so we save
* org.bar;uses="org.baz,
* Then the third entry,
* org.qux"
* is appended, and the result has an even number of quotes, so is added.
*/
for (String packageName : exportPackageString.split(",")) {
boolean evenQuoteCount = hasEvenQuoteCount(packageName);
if (saved != null) {
packageName = saved + packageName;
evenQuoteCount = !evenQuoteCount;
saved = null;
}
if (evenQuoteCount) {
packages.add(packageName);
}
else {
saved = packageName + ',';
}
}
}
else {
jarFile.stream()
.filter(entry -> entry.getName().endsWith(".class"))
.forEach(jarEntry -> {
String entryName = jarEntry.getName();
int lastSlash = entryName.lastIndexOf('/');
if (lastSlash > 0) {
packages.add(
entryName.substring(0, lastSlash).replace('/', '.'));
}
});
}
}
}
catch (IOException e) {

View file

@ -34,6 +34,10 @@ import utilities.util.FileUtilities;
public class BundleHostTest extends AbstractGhidraHeadlessIntegrationTest {
private static final String TEMP_NAME_PREFIX = "sourcebundle";
// the version of Guava Ghidra is currently using.
private static final int GUAVA_MAJOR_VERSION = 19;
private BundleHost bundleHost;
private CapturingBundleHostListener capturingBundleHostListener;
@ -333,6 +337,52 @@ public class BundleHostTest extends AbstractGhidraHeadlessIntegrationTest {
// @formatter:on
}
@Test
public void testImportFromExtraSystemPackagesWithVersionConstraint() throws Exception {
// @formatter:off
String goodRange = String.format("[%d,%d)", GUAVA_MAJOR_VERSION, GUAVA_MAJOR_VERSION+1);
addClass(
"//@importpackage com.google.common.io;version=\""+goodRange+"\"\n",
"import com.google.common.io.BaseEncoding;",
"AClass",
"@Override\n" +
"public String toString() {\n" +
" return BaseEncoding.base16().encode(new byte[] {0x42});\n" +
"}\n"
);
buildAndActivate();
assertEquals("wrong response from instantiated class", "42",
getInstance("AClass").toString());
// @formatter:on
}
@Test
public void testImportFromExtraSystemPackagesWithBadVersionConstraint() throws Exception {
// @formatter:off
String badRange = String.format("[%d,%d)", GUAVA_MAJOR_VERSION+1, GUAVA_MAJOR_VERSION+2);
addClass(
"//@importpackage com.google.common.io;version=\""+badRange+"\"\n",
"import com.google.common.io.BaseEncoding;",
"AClass",
"@Override\n" +
"public String toString() {\n" +
" return BaseEncoding.base16().encode(new byte[] {0x42});\n" +
"}\n"
);
buildWithExpectations(
"1 import requirement remains unresolved:\n" +
" [null] osgi.wiring.package; (&(osgi.wiring.package=com.google.common.io)" +
"(version>="+(GUAVA_MAJOR_VERSION+1)+".0.0)" +
"(!(version>="+(GUAVA_MAJOR_VERSION+2)+".0.0))), " +
"from /tmp/ghidra.dev2tmp/sourcebundle000/AClass.java\n",
"1 missing package import:com.google.common.io (version>="+(GUAVA_MAJOR_VERSION+1)+".0.0)" +
", 1 source file with errors"
);
// @formatter:on
}
@Test
public void testLoadLibraryFromOtherBundleWithManifest() throws Exception {
// @formatter:off