From 93558d3ffc55b02c6f9ee36cddc4f68bee33a269 Mon Sep 17 00:00:00 2001 From: Christoph Rueger Date: Mon, 6 Oct 2025 23:24:10 +0200 Subject: [PATCH] more lenient osgi.ee for unknown JDKs For a yet to bnd unknown JDK (e.g. JDK-10000) Instead of ``` Require-Capability: osgi.ee;filter:="(osgi.ee=UNKNOWN)" ``` we now create Require-Capability: osgi.ee;filter:="(&(osgi.ee=JavaSE)(version=9955))" for a .class file with u2 major_version 10000 (9955 == 10000 - 45) Signed-off-by: Christoph Rueger --- .../test/test/ClazzTest.java | 82 +++++++++++++++++++ .../src/aQute/bnd/osgi/Analyzer.java | 27 ++++-- .../src/aQute/bnd/osgi/Clazz.java | 29 ++++++- 3 files changed, 131 insertions(+), 7 deletions(-) diff --git a/biz.aQute.bndlib.tests/test/test/ClazzTest.java b/biz.aQute.bndlib.tests/test/test/ClazzTest.java index 7d99c30812..713ae44a92 100644 --- a/biz.aQute.bndlib.tests/test/test/ClazzTest.java +++ b/biz.aQute.bndlib.tests/test/test/ClazzTest.java @@ -6,16 +6,20 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.Serializable; import java.lang.annotation.ElementType; import java.lang.annotation.Target; +import java.nio.file.Files; import java.util.ArrayList; import java.util.EnumSet; import java.util.List; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; +import java.util.jar.Attributes; +import java.util.jar.Manifest; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledForJreRange; @@ -30,16 +34,21 @@ import aQute.bnd.osgi.ClassDataCollector; import aQute.bnd.osgi.Clazz; import aQute.bnd.osgi.Clazz.FieldDef; +import aQute.bnd.osgi.Clazz.JAVA; import aQute.bnd.osgi.Clazz.MethodDef; import aQute.bnd.osgi.Clazz.QUERY; +import aQute.bnd.osgi.Constants; import aQute.bnd.osgi.Descriptors; import aQute.bnd.osgi.Descriptors.PackageRef; +import aQute.bnd.osgi.EmbeddedResource; import aQute.bnd.osgi.FileResource; import aQute.bnd.osgi.Instruction; import aQute.bnd.osgi.Jar; import aQute.bnd.osgi.Macro; +import aQute.bnd.osgi.Resource; import aQute.bnd.xmlattribute.XMLAttributeFinder; import aQute.lib.io.IO; +import aQute.lib.manifest.ManifestUtil; public class ClazzTest { @@ -1111,4 +1120,77 @@ public void kotlin_LambdaClass() throws Exception { } } + + /** + * This test creates a fake java .class file with an imaginary large major + * version (10000) to test how our Analyzer handles class files of a yet + * unknown future JDK. + */ + @Test + void testUnknownJDKClass() throws Exception { + + + File file = IO.getFile("bin_test/test/ClazzTest$Inner.class"); + byte[] fakeClassBytes = Files.readAllBytes(file.toPath()); + + int newMajor = 10_000; + int expected = 9955; // newMajor - 45; + // Patch major version (u2 big-endian) + // https://docs.oracle.com/javase/specs/jvms/se21/html/jvms-4.html + fakeClassBytes[6] = (byte) ((newMajor >> 8) & 0xFF); + fakeClassBytes[7] = (byte) (newMajor & 0xFF); + + try (Jar jar = new Jar("future"); + Analyzer a = new Analyzer(jar)) { + + Resource r = new EmbeddedResource(fakeClassBytes, fakeClassBytes.length); + + Clazz c = new Clazz(a, "", r); + c.parseClassFile(new ByteArrayInputStream(fakeClassBytes), new ClassDataCollector() {}); + a.getClassspace() + .put(c.getClassName(), c); + + assertThat(c.getMajorVersion()).isEqualTo(newMajor); + + Manifest calcManifest = a.calcManifest(); + ManifestUtil.write(calcManifest, System.out); + Attributes mainAttributes = calcManifest.getMainAttributes(); + assertThat(mainAttributes.getValue(Constants.REQUIRE_CAPABILITY)) + .contains("(&(osgi.ee=JavaSE)(version=" + expected + "))"); + assertThat(a.getEEs()).containsExactly(JAVA.UNKNOWN); + + + } + catch (Exception e) { + e.printStackTrace(); + throw e; + } + + } + + @Test + void testBuildEEFilter_lenientKnown() { + // lenient = true but version within known range + String result = Clazz.JAVA.buildEEFilterLenient(55); // within range + assertEquals("(&(osgi.ee=JavaSE)(version=11))", result); + } + + + @Test + void testBuildEEFilter_lenientTooLow() { + // version < 0 -> UNKNOWN + String result = Clazz.JAVA.buildEEFilterLenient(40); + assertEquals("(&(osgi.ee=JavaSE)(version=UNKNOWN))", result); + } + + @Test + void testBuildEEFilter_lenientTooHigh() { + // version >= JAVA.values.length - 1 -> eeFilter(version) + int maxmajor = Clazz.JAVA.values()[Clazz.JAVA.values().length - 2].getMajor(); + int tooHigh = maxmajor + 10; + int expected = tooHigh - 45; + String result = Clazz.JAVA.buildEEFilterLenient(tooHigh); + // version = max. known version + 1 + assertEquals("(&(osgi.ee=JavaSE)(version=" + expected + "))", result); + } } diff --git a/biz.aQute.bndlib/src/aQute/bnd/osgi/Analyzer.java b/biz.aQute.bndlib/src/aQute/bnd/osgi/Analyzer.java index 64577f2e17..281b00662a 100644 --- a/biz.aQute.bndlib/src/aQute/bnd/osgi/Analyzer.java +++ b/biz.aQute.bndlib/src/aQute/bnd/osgi/Analyzer.java @@ -118,6 +118,7 @@ public class Analyzer extends Processor { private final static Logger logger = LoggerFactory.getLogger(Analyzer.class); private final static Version frameworkR7 = new Version("1.9"); private final SortedSet ees = new TreeSet<>(); + private int highestMajorJavaVersion = 0; // Bundle parameters private Jar dot; @@ -231,8 +232,14 @@ private void analyze0() throws Exception { classspace.values() .stream() .filter(c -> !c.isModule()) - .map(Clazz::getFormat) - .forEach(ees::add); + .forEach(c -> { + ees.add(c.getFormat()); + int majorVersion = c.getMajorVersion(); + if(majorVersion > highestMajorJavaVersion) { + highestMajorJavaVersion = majorVersion; + } + + }); try (ClassDataCollectors cds = new ClassDataCollectors(this)) { List parsers = getPlugins(ClassParser.class); @@ -1301,9 +1308,17 @@ public Jar findClasspathEntry(String bsn, String r) { * highest found profile is added. This only works for java packages. */ private String doEEProfiles(JAVA highest) throws IOException { + + String highestFilter; + if (highest == aQute.bnd.osgi.Clazz.JAVA.UNKNOWN) { + highestFilter = aQute.bnd.osgi.Clazz.JAVA.buildEEFilterLenient(highestMajorJavaVersion); + } else { + highestFilter = highest.getFilter(); + } + String ee = getProperty(EEPROFILE); if (ee == null) - return highest.getFilter(); + return highestFilter; ee = ee.trim(); @@ -1312,7 +1327,7 @@ private String doEEProfiles(JAVA highest) throws IOException { if (ee.equals(EEPROFILE_AUTO_ATTRIBUTE)) { profiles = highest.getProfiles(); if (profiles == null) - return highest.getFilter(); + return highestFilter; } else { profiles = OSGiHeader.parseProperties(ee) .stream() @@ -1349,11 +1364,11 @@ private String doEEProfiles(JAVA highest) throws IOException { // // Ouch, outside any profile // - return highest.getFilter(); + return highestFilter; } } - String filter = highest.getFilter(); + String filter = highestFilter; if (!found.isEmpty()) filter = filter.replaceAll("JavaSE", "JavaSE/" + found.last()); // TODO a more elegant way to build the filter, we now assume JavaSE diff --git a/biz.aQute.bndlib/src/aQute/bnd/osgi/Clazz.java b/biz.aQute.bndlib/src/aQute/bnd/osgi/Clazz.java index 585513a7f3..8d43f9052b 100644 --- a/biz.aQute.bndlib/src/aQute/bnd/osgi/Clazz.java +++ b/biz.aQute.bndlib/src/aQute/bnd/osgi/Clazz.java @@ -198,7 +198,7 @@ public Map> getProfiles() throws IOException { this.major = ordinal() + 45; String version = Integer.toString(ordinal() + 1); this.ee = "JavaSE-" + version; - this.filter = "(&(osgi.ee=JavaSE)(version=" + version + "))"; + this.filter = eeFilter(version); } JAVA(String ee, String filter) { @@ -248,6 +248,28 @@ public String getEE() { return ee; } + /** + * Returns an eeFilter String also supporting yet unknown JDKs (lenient) + */ + public static String buildEEFilterLenient(int major) { + + // lenient: We try to return a useful filter + // even for yet unknown JDKs + int version = major - 45; + if ((version < 0)) { + return eeFilter("UNKNOWN"); + } else if (version >= (JAVA.values.length - 1)) { + // yet UNKNOWN + return eeFilter(Integer.toString(version)); + } + + return format(major).getFilter(); + } + + private static String eeFilter(String version) { + return "(&(osgi.ee=JavaSE)(version=" + version + "))"; + } + public String getFilter() { return filter; } @@ -1936,6 +1958,11 @@ public JAVA getFormat() { } + public int getMajorVersion() { + return classFile.major_version; + + } + public static String objectDescriptorToFQN(String string) { if ((string.startsWith("L") || string.startsWith("T")) && string.endsWith(";")) return string.substring(1, string.length() - 1)