reque
/**
* If the jdkHome is specified, its version it considered higher than the runtime java version.
- * In that case ASM must be used to read the module descriptor
+ * In that case the version-agnostic {@link ModuleInfoClassParser} must be used to read the module
+ * descriptor, since the runtime's own {@code java.lang.module.ModuleDescriptor} cannot be trusted
+ * to read a module-info.class produced by a newer JDK than the one it runs on.
*
* @param jdkHome
* @return
@@ -289,7 +291,7 @@ ModuleInfoParser getBinaryModuleInfoParser(final Path jdkHome) {
if (jdkHome == null) {
binaryParser = new BinaryModuleInfoParser();
} else {
- binaryParser = new AsmModuleInfoParser();
+ binaryParser = new ModuleInfoClassParser();
}
return binaryParser;
}
diff --git a/plexus-java/src/main/java/org/codehaus/plexus/languages/java/jpms/ModuleInfoClassParser.java b/plexus-java/src/main/java/org/codehaus/plexus/languages/java/jpms/ModuleInfoClassParser.java
new file mode 100644
index 0000000..f6f117d
--- /dev/null
+++ b/plexus-java/src/main/java/org/codehaus/plexus/languages/java/jpms/ModuleInfoClassParser.java
@@ -0,0 +1,302 @@
+package org.codehaus.plexus.languages.java.jpms;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Reads the {@code Module} attribute (JVMS 4.7.25) directly out of a {@code module-info.class} file,
+ * without depending on a bytecode library or on the class-file support of the running JDK.
+ *
+ * Only the class file's overall shape (constant pool, then fields, then methods, then attributes)
+ * needs to be understood to reach the {@code Module} attribute; every other section is skipped using
+ * its own declared length rather than parsed. The {@code Module} attribute format has been stable
+ * since it was introduced in Java 9, and this parser never looks at the class file's major/minor
+ * version, so it reads a {@code module-info.class} produced by any future JDK just as well as one
+ * from Java 9.
+ *
+ * @author Robert Scholte
+ * @since 1.7.0
+ */
+class ModuleInfoClassParser extends AbstractBinaryModuleInfoParser {
+
+ private static final int MAGIC = 0xCAFEBABE;
+
+ // Constant pool tags, JVMS 4.4, Table 4.4-A.
+ private static final int CONSTANT_UTF8 = 1;
+ private static final int CONSTANT_INTEGER = 3;
+ private static final int CONSTANT_FLOAT = 4;
+ private static final int CONSTANT_LONG = 5;
+ private static final int CONSTANT_DOUBLE = 6;
+ private static final int CONSTANT_CLASS = 7;
+ private static final int CONSTANT_STRING = 8;
+ private static final int CONSTANT_FIELDREF = 9;
+ private static final int CONSTANT_METHODREF = 10;
+ private static final int CONSTANT_INTERFACE_METHODREF = 11;
+ private static final int CONSTANT_NAME_AND_TYPE = 12;
+ private static final int CONSTANT_METHOD_HANDLE = 15;
+ private static final int CONSTANT_METHOD_TYPE = 16;
+ private static final int CONSTANT_DYNAMIC = 17;
+ private static final int CONSTANT_INVOKE_DYNAMIC = 18;
+ private static final int CONSTANT_MODULE = 19;
+ private static final int CONSTANT_PACKAGE = 20;
+
+ private static final String MODULE_ATTRIBUTE_NAME = "Module";
+
+ // Module_attribute requires_flags, JVMS 4.7.25.
+ private static final int ACC_TRANSITIVE = 0x0020;
+ private static final int ACC_STATIC_PHASE = 0x0040;
+
+ @Override
+ JavaModuleDescriptor parse(InputStream in) throws IOException {
+ DataInputStream dis = (in instanceof DataInputStream) ? (DataInputStream) in : new DataInputStream(in);
+
+ int magic = dis.readInt();
+ if (magic != MAGIC) {
+ throw new IOException("Not a class file, bad magic number: 0x" + Integer.toHexString(magic));
+ }
+
+ dis.readUnsignedShort(); // minor_version, unused
+ dis.readUnsignedShort(); // major_version, deliberately unchecked: the Module attribute below
+ // has the same layout no matter which major version produced the file.
+
+ ConstantPool pool = ConstantPool.read(dis);
+
+ dis.readUnsignedShort(); // access_flags
+ dis.readUnsignedShort(); // this_class
+ dis.readUnsignedShort(); // super_class
+
+ int interfacesCount = dis.readUnsignedShort();
+ skipFully(dis, 2L * interfacesCount);
+
+ skipMembers(dis); // fields
+ skipMembers(dis); // methods
+
+ int attributesCount = dis.readUnsignedShort();
+ for (int i = 0; i < attributesCount; i++) {
+ int attributeNameIndex = dis.readUnsignedShort();
+ long attributeLength = readU4(dis);
+ if (MODULE_ATTRIBUTE_NAME.equals(pool.utf8(attributeNameIndex))) {
+ return readModuleAttribute(dis, pool);
+ }
+ skipFully(dis, attributeLength);
+ }
+
+ throw new IOException("Not a module: no Module attribute found");
+ }
+
+ /**
+ * Skips over a {@code fields} or {@code methods} array: same {@code field_info}/{@code method_info}
+ * shape, each with its own list of attributes that are skipped by their declared length.
+ */
+ private static void skipMembers(DataInputStream dis) throws IOException {
+ int count = dis.readUnsignedShort();
+ for (int i = 0; i < count; i++) {
+ skipFully(dis, 6); // access_flags, name_index, descriptor_index
+ int attributesCount = dis.readUnsignedShort();
+ for (int j = 0; j < attributesCount; j++) {
+ skipFully(dis, 2); // attribute_name_index
+ long attributeLength = readU4(dis);
+ skipFully(dis, attributeLength);
+ }
+ }
+ }
+
+ /**
+ * Reads the body of the {@code Module} attribute (JVMS 4.7.25), mapping it onto a
+ * {@link JavaModuleDescriptor} the same way {@code java.lang.module.ModuleDescriptor} does.
+ */
+ private static JavaModuleDescriptor readModuleAttribute(DataInputStream dis, ConstantPool pool) throws IOException {
+ String moduleName = pool.moduleName(dis.readUnsignedShort());
+ JavaModuleDescriptor.Builder builder = JavaModuleDescriptor.newModule(moduleName);
+
+ dis.readUnsignedShort(); // module_flags
+ dis.readUnsignedShort(); // module_version_index
+
+ int requiresCount = dis.readUnsignedShort();
+ for (int i = 0; i < requiresCount; i++) {
+ int requiresIndex = dis.readUnsignedShort();
+ int requiresFlags = dis.readUnsignedShort();
+ dis.readUnsignedShort(); // requires_version_index
+
+ String requiredModule = pool.moduleName(requiresIndex);
+ if ((requiresFlags & (ACC_STATIC_PHASE | ACC_TRANSITIVE)) != 0) {
+ Set modifiers = new LinkedHashSet<>();
+ if ((requiresFlags & ACC_STATIC_PHASE) != 0) {
+ modifiers.add(JavaModuleDescriptor.JavaRequires.JavaModifier.STATIC);
+ }
+ if ((requiresFlags & ACC_TRANSITIVE) != 0) {
+ modifiers.add(JavaModuleDescriptor.JavaRequires.JavaModifier.TRANSITIVE);
+ }
+ builder.requires(modifiers, requiredModule);
+ } else {
+ builder.requires(requiredModule);
+ }
+ }
+
+ int exportsCount = dis.readUnsignedShort();
+ for (int i = 0; i < exportsCount; i++) {
+ int exportsIndex = dis.readUnsignedShort();
+ dis.readUnsignedShort(); // exports_flags
+ int exportsToCount = dis.readUnsignedShort();
+ String pkg = pool.packageName(exportsIndex);
+ if (exportsToCount == 0) {
+ builder.exports(pkg);
+ } else {
+ Set targets = new HashSet<>();
+ for (int j = 0; j < exportsToCount; j++) {
+ targets.add(pool.moduleName(dis.readUnsignedShort()));
+ }
+ builder.exports(pkg, targets);
+ }
+ }
+
+ int opensCount = dis.readUnsignedShort();
+ for (int i = 0; i < opensCount; i++) {
+ dis.readUnsignedShort(); // opens_index
+ dis.readUnsignedShort(); // opens_flags
+ int opensToCount = dis.readUnsignedShort();
+ skipFully(dis, 2L * opensToCount);
+ }
+
+ int usesCount = dis.readUnsignedShort();
+ for (int i = 0; i < usesCount; i++) {
+ builder.uses(pool.className(dis.readUnsignedShort()));
+ }
+
+ int providesCount = dis.readUnsignedShort();
+ for (int i = 0; i < providesCount; i++) {
+ String service = pool.className(dis.readUnsignedShort());
+ int withCount = dis.readUnsignedShort();
+ List providers = new ArrayList<>(withCount);
+ for (int j = 0; j < withCount; j++) {
+ providers.add(pool.className(dis.readUnsignedShort()));
+ }
+ builder.provides(service, providers);
+ }
+
+ return builder.build();
+ }
+
+ private static long readU4(DataInputStream dis) throws IOException {
+ return dis.readInt() & 0xFFFFFFFFL;
+ }
+
+ private static void skipFully(DataInputStream dis, long length) throws IOException {
+ long remaining = length;
+ while (remaining > 0) {
+ long skipped = dis.skip(remaining);
+ if (skipped <= 0) {
+ // skip() may legitimately return 0 near the end of the stream; fall back to read()
+ if (dis.read() < 0) {
+ throw new IOException("Unexpected end of class file");
+ }
+ skipped = 1;
+ }
+ remaining -= skipped;
+ }
+ }
+
+ /**
+ * The subset of the constant pool this parser needs: the raw UTF-8 entries, and, for the
+ * Class/Module/Package entries, which UTF-8 entry holds their name. All three of those tags
+ * have the same one-field shape (JVMS 4.4.1, 4.4.8, 4.4.9): {@code u1 tag; u2 name_index;}.
+ */
+ private static final class ConstantPool {
+ private final String[] utf8;
+ private final int[] classLikeNameIndex;
+
+ private ConstantPool(String[] utf8, int[] classLikeNameIndex) {
+ this.utf8 = utf8;
+ this.classLikeNameIndex = classLikeNameIndex;
+ }
+
+ static ConstantPool read(DataInputStream dis) throws IOException {
+ int constantPoolCount = dis.readUnsignedShort();
+ String[] utf8 = new String[constantPoolCount];
+ int[] classLikeNameIndex = new int[constantPoolCount];
+
+ for (int i = 1; i < constantPoolCount; i++) {
+ int tag = dis.readUnsignedByte();
+ switch (tag) {
+ case CONSTANT_UTF8:
+ utf8[i] = dis.readUTF();
+ break;
+ case CONSTANT_CLASS:
+ case CONSTANT_MODULE:
+ case CONSTANT_PACKAGE:
+ classLikeNameIndex[i] = dis.readUnsignedShort();
+ break;
+ case CONSTANT_STRING:
+ skipFully(dis, 2);
+ break;
+ case CONSTANT_INTEGER:
+ case CONSTANT_FLOAT:
+ case CONSTANT_FIELDREF:
+ case CONSTANT_METHODREF:
+ case CONSTANT_INTERFACE_METHODREF:
+ case CONSTANT_NAME_AND_TYPE:
+ case CONSTANT_DYNAMIC:
+ case CONSTANT_INVOKE_DYNAMIC:
+ skipFully(dis, 4);
+ break;
+ case CONSTANT_LONG:
+ case CONSTANT_DOUBLE:
+ skipFully(dis, 8);
+ // long/double take up two consecutive constant pool entries; the second is unused
+ i++;
+ break;
+ case CONSTANT_METHOD_HANDLE:
+ skipFully(dis, 3);
+ break;
+ case CONSTANT_METHOD_TYPE:
+ skipFully(dis, 2);
+ break;
+ default:
+ throw new IOException("Unsupported constant pool tag " + tag + " at index " + i);
+ }
+ }
+ return new ConstantPool(utf8, classLikeNameIndex);
+ }
+
+ String utf8(int index) {
+ return utf8[index];
+ }
+
+ String moduleName(int index) {
+ return utf8[classLikeNameIndex[index]];
+ }
+
+ String packageName(int index) {
+ return utf8[classLikeNameIndex[index]].replace('/', '.');
+ }
+
+ String className(int index) {
+ return utf8[classLikeNameIndex[index]].replace('/', '.');
+ }
+ }
+}
diff --git a/plexus-java/src/main/java9/module-info.java b/plexus-java/src/main/java9/module-info.java
index b9ce123..1a68b2a 100644
--- a/plexus-java/src/main/java9/module-info.java
+++ b/plexus-java/src/main/java9/module-info.java
@@ -18,7 +18,6 @@
*/
module org.codehaus.plexus.languages.java {
requires com.github.javaparser.core;
- requires org.objectweb.asm;
exports org.codehaus.plexus.languages.java.jpms;
exports org.codehaus.plexus.languages.java.version;
diff --git a/plexus-java/src/test/java/org/codehaus/plexus/languages/java/jpms/AsmModuleInfoParserTest.java b/plexus-java/src/test/java/org/codehaus/plexus/languages/java/jpms/ModuleInfoClassParserTest.java
similarity index 56%
rename from plexus-java/src/test/java/org/codehaus/plexus/languages/java/jpms/AsmModuleInfoParserTest.java
rename to plexus-java/src/test/java/org/codehaus/plexus/languages/java/jpms/ModuleInfoClassParserTest.java
index efc865a..6d3883e 100644
--- a/plexus-java/src/test/java/org/codehaus/plexus/languages/java/jpms/AsmModuleInfoParserTest.java
+++ b/plexus-java/src/test/java/org/codehaus/plexus/languages/java/jpms/ModuleInfoClassParserTest.java
@@ -29,29 +29,32 @@
import org.codehaus.plexus.languages.java.jpms.JavaModuleDescriptor.JavaRequires;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
-class AsmModuleInfoParserTest {
- private final AsmModuleInfoParser parser = new AsmModuleInfoParser();
+class ModuleInfoClassParserTest {
+ private final ModuleInfoClassParser parser = new ModuleInfoClassParser();
/**
- * A class file major version newer than what the bundled ASM release supports (e.g. major
- * version 72 emitted by JDK 28, see codehaus-plexus/plexus-languages#165) used to make
- * ClassReader throw IllegalArgumentException. Patch a known-good module-info.class to claim
- * that unsupported major version and verify it still parses, with the same descriptor as the
- * unpatched file (see {@code BinaryModuleInfoParserTest#requires()}).
+ * This parser never looks at the class file's major/minor version, so a module-info.class compiled
+ * by a JDK newer than any released today (e.g. major version 72 for JDK 28, see
+ * codehaus-plexus/plexus-languages#165, or an arbitrarily higher one) must still parse, with the same
+ * descriptor as the unpatched file (see {@code BinaryModuleInfoParserTest#requires()}).
*/
- @Test
- void parsesModuleInfoWithMajorVersionNewerThanAsmSupports() throws Exception {
+ @ParameterizedTest
+ @ValueSource(ints = {72, 99})
+ void parsesModuleInfoWithMajorVersionNewerThanAnyKnownJdk(int majorVersion) throws Exception {
byte[] classBytes =
Files.readAllBytes(Paths.get("src/test/test-data/dir.descriptor.requires/out/module-info.class"));
- // bytes 6-7 are the big-endian major version; 72 is one past ASM 9.10.1's newest (71/V27)
- classBytes[6] = 0x00;
- classBytes[7] = 0x48;
+ // bytes 6-7 are the big-endian major version
+ classBytes[6] = (byte) (majorVersion >>> 8);
+ classBytes[7] = (byte) majorVersion;
JavaModuleDescriptor descriptor;
try (ByteArrayInputStream is = new ByteArrayInputStream(classBytes)) {
@@ -75,4 +78,26 @@ void parsesModuleInfoWithMajorVersionNewerThanAsmSupports() throws Exception {
assertEquals(expectedRequires, descriptor.requires());
}
+
+ @Test
+ void rejectsBadMagic() throws Exception {
+ byte[] classBytes =
+ Files.readAllBytes(Paths.get("src/test/test-data/dir.descriptor.requires/out/module-info.class"));
+ classBytes[0] = 0x00;
+
+ try (ByteArrayInputStream is = new ByteArrayInputStream(classBytes)) {
+ assertThrows(java.io.IOException.class, () -> parser.parse(is));
+ }
+ }
+
+ @Test
+ void rejectsClassFileWithoutModuleAttribute() throws Exception {
+ // an ordinary compiled class, i.e. one without a Module attribute, must be rejected rather than
+ // silently producing a bogus descriptor
+ byte[] classBytes = Files.readAllBytes(Paths.get("src/test/test-data/classfile.version/helloworld-17.class"));
+
+ try (ByteArrayInputStream is = new ByteArrayInputStream(classBytes)) {
+ assertThrows(java.io.IOException.class, () -> parser.parse(is));
+ }
+ }
}
diff --git a/pom.xml b/pom.xml
index c0621f3..72e477c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -120,5 +120,16 @@
+
+
+ jdk27-skip-spotless
+
+ [27,)
+
+
+ true
+ true
+
+