[loader] Fix an issue when using lombok as a module while using ObjectWeb ASM as dep.

Evidently jigsaw's damage to java extends to our way of shading ObjectWeb ASM dep - it's a split package violation even though lombok loads its stuff in a separate loader. We fix it by using some on-the-fly extremely fast constant pool rewriting.
This commit is contained in:
Reinier Zwitserloot committed 2023-08-03 23:58:17 +02:00
1 parent 9e5e0c3803
commit 67d155985a
4 files changed
+620 -2

No files matched your search

@@ -22,11 +22,15 @@
package lombok.bytecode;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* Utility to read the constant pool, header, and inheritance information of any class file.
*
* THIS IS A COPY OF lombok.launch.ClassFileMetaData - it is used by the shadowclassloader which can't rely on anything from outside the lombok.launch package, but everything in the launch package has to be package private.
* If there is a bug or an update is needed here - make sure to also update it in the lombok.bytecode package.
*/
public class ClassFileMetaData {
private static final byte UTF8 = 1;
@@ -54,7 +58,7 @@ public class ClassFileMetaData {
private final byte[] byteCode;
private final int maxPoolSize;
private final int maxPoolSize;
private final int[] offsets;
private final byte[] types;
private final String[] utf8s;
@@ -138,6 +142,18 @@ public class ClassFileMetaData {
return new String(result, 0, length);
}
public int[] getOffsets(byte type) {
int[] out = new int[types.length];
int ptr = 0;
for (int i = 0; i < types.length; i++) {
if (types[i] != type) continue;
out[ptr++] = offsets[i];
}
return Arrays.copyOf(out, ptr);
}
/**
* Checks if the constant pool contains the provided 'raw' string. These are used as source material for further JVM types, such as string constants, type references, etcetera.
*/
@@ -457,6 +473,9 @@ public class ClassFileMetaData {
return NOT_FOUND;
}
/**
* Reads a 16-bit value at {@code position}
*/
private int readValue(int position) {
return ((byteCode[position] & 0xFF) << 8) | (byteCode[position + 1] & 0xFF);
}
@@ -0,0 +1,482 @@
/*
* Copyright (C) 2010-2019 The Project Lombok Authors.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package lombok.launch;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* Utility to read the constant pool, header, and inheritance information of any class file.
*
* THIS IS A COPY OF lombok.bytecode.ClassFileMetaData - it is used by the shadowclassloader which can't rely on anything from outside the lombok.launch package, but everything in the launch package has to be package private.
* If there is a bug or an update is needed here - make sure to also update it in the lombok.bytecode package.
*/
class ClassFileMetaData {
private static final byte UTF8 = 1;
private static final byte INTEGER = 3;
private static final byte FLOAT = 4;
private static final byte LONG = 5;
private static final byte DOUBLE = 6;
private static final byte CLASS = 7;
private static final byte STRING = 8;
private static final byte FIELD = 9;
private static final byte METHOD = 10;
private static final byte INTERFACE_METHOD = 11;
private static final byte NAME_TYPE = 12;
// New in java7: support for methodhandles and invokedynamic
private static final byte METHOD_HANDLE = 15;
private static final byte METHOD_TYPE = 16;
private static final byte DYNAMIC = 17;
private static final byte INVOKE_DYNAMIC = 18;
// New in java9: support for modules
private static final byte MODULE = 19;
private static final byte PACKAGE = 20;
private static final int NOT_FOUND = -1;
private static final int START_OF_CONSTANT_POOL = 8;
private final byte[] byteCode;
private final int maxPoolSize;
private final int[] offsets;
private final byte[] types;
private final String[] utf8s;
private final int endOfPool;
public ClassFileMetaData(byte[] byteCode) {
this.byteCode = byteCode;
maxPoolSize = readValue(START_OF_CONSTANT_POOL);
offsets = new int[maxPoolSize];
types = new byte[maxPoolSize];
utf8s = new String[maxPoolSize];
int position = 10;
for (int i = 1; i < maxPoolSize; i++) {
byte type = byteCode[position];
types[i] = type;
position++;
offsets[i] = position;
switch (type) {
case UTF8:
int length = readValue(position);
position += 2;
utf8s[i] = decodeString(position, length);
position += length;
break;
case CLASS:
case STRING:
case METHOD_TYPE:
case MODULE:
case PACKAGE:
position += 2;
break;
case METHOD_HANDLE:
position += 3;
break;
case INTEGER:
case FLOAT:
case FIELD:
case METHOD:
case INTERFACE_METHOD:
case NAME_TYPE:
case INVOKE_DYNAMIC:
case DYNAMIC:
position += 4;
break;
case LONG:
case DOUBLE:
position += 8;
i++;
break;
case 0:
break;
default:
throw new AssertionError("Unknown constant pool type " + type);
}
}
endOfPool = position;
}
private String decodeString(int pos, int size) {
int end = pos + size;
// the resulting string might be smaller
char[] result = new char[size];
int length = 0;
while (pos < end) {
int first = (byteCode[pos++] & 0xFF);
if (first < 0x80) {
result[length++] = (char)first;
} else if ((first & 0xE0) == 0xC0) {
int x = (first & 0x1F) << 6;
int y = (byteCode[pos++] & 0x3F);
result[length++] = (char)(x | y);
} else {
int x = (first & 0x0F) << 12;
int y = (byteCode[pos++] & 0x3F) << 6;
int z = (byteCode[pos++] & 0x3F);
result[length++] = (char)(x | y | z);
}
}
return new String(result, 0, length);
}
public int[] getOffsets(byte type) {
int[] out = new int[types.length];
int ptr = 0;
for (int i = 0; i < types.length; i++) {
if (types[i] != type) continue;
out[ptr++] = offsets[i];
}
return Arrays.copyOf(out, ptr);
}
/**
* Checks if the constant pool contains the provided 'raw' string. These are used as source material for further JVM types, such as string constants, type references, etcetera.
*/
public boolean containsUtf8(String value) {
return findUtf8(value) != NOT_FOUND;
}
/**
* Checks if the constant pool contains a reference to the provided class.
*
* NB: Most uses of a type do <em>NOT</em> show up as a class in the constant pool.
* For example, the parameter types and return type of any method you invoke or declare, are stored as signatures and not as type references,
* but the type to which any method you invoke belongs, is. Read the JVM Specification for more information.
*
* @param className must be provided JVM-style, such as {@code java/lang/String}
*/
public boolean usesClass(String className) {
return findClass(className) != NOT_FOUND;
}
/**
* Checks if the constant pool contains a reference to a given field, either for writing or reading.
*
* @param className must be provided JVM-style, such as {@code java/lang/String}
*/
public boolean usesField(String className, String fieldName) {
int classIndex = findClass(className);
if (classIndex == NOT_FOUND) return false;
int fieldNameIndex = findUtf8(fieldName);
if (fieldNameIndex == NOT_FOUND) return false;
for (int i = 1; i < maxPoolSize; i++) {
if (types[i] == FIELD && readValue(offsets[i]) == classIndex) {
int nameAndTypeIndex = readValue(offsets[i] + 2);
if (readValue(offsets[nameAndTypeIndex]) == fieldNameIndex) return true;
}
}
return false;
}
/**
* Checks if the constant pool contains a reference to a given method, with any signature (return type and parameter types).
*
* @param className must be provided JVM-style, such as {@code java/lang/String}
*/
public boolean usesMethod(String className, String methodName) {
int classIndex = findClass(className);
if (classIndex == NOT_FOUND) return false;
int methodNameIndex = findUtf8(methodName);
if (methodNameIndex == NOT_FOUND) return false;
for (int i = 1; i < maxPoolSize; i++) {
if (isMethod(i) && readValue(offsets[i]) == classIndex) {
int nameAndTypeIndex = readValue(offsets[i] + 2);
if (readValue(offsets[nameAndTypeIndex]) == methodNameIndex) return true;
}
}
return false;
}
/**
* Checks if the constant pool contains a reference to a given method.
*
* @param className must be provided JVM-style, such as {@code java/lang/String}
* @param descriptor must be provided JVM-style, such as {@code (IZ)Ljava/lang/String;}
*/
public boolean usesMethod(String className, String methodName, String descriptor) {
int classIndex = findClass(className);
if (classIndex == NOT_FOUND) return false;
int nameAndTypeIndex = findNameAndType(methodName, descriptor);
if (nameAndTypeIndex == NOT_FOUND) return false;
for (int i = 1; i < maxPoolSize; i++) {
if (isMethod(i) &&
readValue(offsets[i]) == classIndex &&
readValue(offsets[i] + 2) == nameAndTypeIndex) return true;
}
return false;
}
/**
* Checks if the constant pool contains the provided string constant, which implies the constant is used somewhere in the code.
*
* NB: String literals get concatenated by the compiler.
* NB2: This method does NOT do any kind of normalization.
*/
public boolean containsStringConstant(String value) {
int index = findUtf8(value);
if (index == NOT_FOUND) return false;
for (int i = 1; i < maxPoolSize; i++) {
if (types[i] == STRING && readValue(offsets[i]) == index) return true;
}
return false;
}
/**
* Checks if the constant pool contains the provided long constant, which implies the constant is used somewhere in the code.
*
* NB: compile-time constant expressions are evaluated at compile time.
*/
public boolean containsLong(long value) {
for (int i = 1; i < maxPoolSize; i++) {
if (types[i] == LONG && readLong(i) == value) return true;
}
return false;
}
/**
* Checks if the constant pool contains the provided double constant, which implies the constant is used somewhere in the code.
*
* NB: compile-time constant expressions are evaluated at compile time.
*/
public boolean containsDouble(double value) {
boolean isNan = Double.isNaN(value);
for (int i = 1; i < maxPoolSize; i++) {
if (types[i] == DOUBLE) {
double d = readDouble(i);
if (d == value || (isNan && Double.isNaN(d))) return true;
}
}
return false;
}
/**
* Checks if the constant pool contains the provided int constant, which implies the constant is used somewhere in the code.
*
* NB: compile-time constant expressions are evaluated at compile time.
*/
public boolean containsInteger(int value) {
for (int i = 1; i < maxPoolSize; i++) {
if (types[i] == INTEGER && readInteger(i) == value) return true;
}
return false;
}
/**
* Checks if the constant pool contains the provided float constant, which implies the constant is used somewhere in the code.
*
* NB: compile-time constant expressions are evaluated at compile time.
*/
public boolean containsFloat(float value) {
boolean isNan = Float.isNaN(value);
for (int i = 1; i < maxPoolSize; i++) {
if (types[i] == FLOAT) {
float f = readFloat(i);
if (f == value || (isNan && Float.isNaN(f))) return true;
}
}
return false;
}
private long readLong(int index) {
int pos = offsets[index];
return ((long)read32(pos)) << 32 | (read32(pos + 4) & 0x00000000FFFFFFFFL);
}
private double readDouble(int index) {
return Double.longBitsToDouble(readLong(index));
}
private int readInteger(int index) {
return read32(offsets[index]);
}
private float readFloat(int index) {
return Float.intBitsToFloat(readInteger(index));
}
private int read32(int pos) {
return (byteCode[pos] & 0xFF) << 24 | (byteCode[pos + 1] & 0xFF) << 16 | (byteCode[pos + 2] & 0xFF) << 8 | (byteCode[pos + 3] & 0xFF);
}
/**
* Returns the name of the class in JVM format, such as {@code java/lang/String}
*/
public String getClassName() {
return getClassName(readValue(endOfPool + 2));
}
/**
* Returns the name of the superclass in JVM format, such as {@code java/lang/Object}
*
* NB: If you try this on Object itself, you'll get {@code null}.<br />
* NB2: For interfaces and annotation interfaces, you'll always get {@code java/lang/Object}
*/
public String getSuperClassName() {
return getClassName(readValue(endOfPool + 4));
}
/**
* Returns the name of all implemented interfaces.
*/
public List<String> getInterfaces() {
int size = readValue(endOfPool + 6);
if (size == 0) return Collections.emptyList();
List<String> result = new ArrayList<String>();
for (int i = 0; i < size; i++) {
result.add(getClassName(readValue(endOfPool + 8 + (i * 2))));
}
return result;
}
/**
* A {@code toString()} like utility to dump all contents of the constant pool into a string.
*
* NB: No guarantees are made about the exact layout of this string. It is for informational purposes only, don't try to parse it.<br />
* NB2: After a double or long, there's a JVM spec-mandated gap, which is listed as {@code (cont.)} in the returned string.
*/
public String poolContent() {
StringBuilder result = new StringBuilder();
for (int i = 1; i < maxPoolSize; i++) {
result.append(String.format("#%02x: ", i));
int pos = offsets[i];
switch(types[i]) {
case UTF8:
result.append("Utf8 ").append(utf8s[i]);
break;
case CLASS:
result.append("Class ").append(getClassName(i));
break;
case STRING:
result.append("String \"").append(utf8s[readValue(pos)]).append("\"");
break;
case INTEGER:
result.append("int ").append(readInteger(i));
break;
case FLOAT:
result.append("float ").append(readFloat(i));
break;
case FIELD:
appendAccess(result.append("Field "), i);
break;
case METHOD:
case INTERFACE_METHOD:
appendAccess(result.append("Method "), i);
break;
case NAME_TYPE:
appendNameAndType(result.append("Name&Type "), i);
break;
case LONG:
result.append("long ").append(readLong(i));
break;
case DOUBLE:
result.append("double ").append(readDouble(i));
break;
case METHOD_HANDLE:
result.append("MethodHandle...");
break;
case METHOD_TYPE:
result.append("MethodType...");
break;
case DYNAMIC:
result.append("Dynamic...");
break;
case INVOKE_DYNAMIC:
result.append("InvokeDynamic...");
break;
case 0:
result.append("(cont.)");
break;
}
result.append("\n");
}
return result.toString();
}
private void appendAccess(StringBuilder result, int index) {
int pos = offsets[index];
result.append(getClassName(readValue(pos))).append(".");
appendNameAndType(result, readValue(pos + 2));
}
private void appendNameAndType(StringBuilder result, int index) {
int pos = offsets[index];
result.append(utf8s[readValue(pos)]).append(":").append(utf8s[readValue(pos + 2)]);
}
private String getClassName(int classIndex) {
if (classIndex < 1) return null;
return utf8s[readValue(offsets[classIndex])];
}
private boolean isMethod(int i) {
byte type = types[i];
return type == METHOD || type == INTERFACE_METHOD;
}
private int findNameAndType(String name, String descriptor) {
int nameIndex = findUtf8(name);
if (nameIndex == NOT_FOUND) return NOT_FOUND;
int descriptorIndex = findUtf8(descriptor);
if (descriptorIndex == NOT_FOUND) return NOT_FOUND;
for (int i = 1; i < maxPoolSize; i++) {
if (types[i] == NAME_TYPE &&
readValue(offsets[i]) == nameIndex &&
readValue(offsets[i] + 2) == descriptorIndex) return i;
}
return NOT_FOUND;
}
private int findUtf8(String value) {
for (int i = 1; i < maxPoolSize; i++) {
if (value.equals(utf8s[i])) {
return i;
}
}
return NOT_FOUND;
}
private int findClass(String className) {
int index = findUtf8(className);
if (index == -1) return NOT_FOUND;
for (int i = 1; i < maxPoolSize; i++) {
if (types[i] == CLASS && readValue(offsets[i]) == index) return i;
}
return NOT_FOUND;
}
/**
* Reads a 16-bit value at {@code position}
*/
private int readValue(int position) {
return ((byteCode[position] & 0xFF) << 8) | (byteCode[position + 1] & 0xFF);
}
}
+109
View File
@@ -0,0 +1,109 @@
package lombok.launch;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
/**
* Low-level shader tool, that inline 'rewrites' bytecode so that a given package name acts like it has a different name.
*
* This is useful particularly in light of the java module system which disallows 'split packages' - different loader systems / class file sources
* that both have the same package. Generally you'd take one of your dependency libraries (such as lombok's dependency on ObjectWeb's ASM bytecode parser) and
* rename that dependency to avoid conflicts with other code that also wants to use ASM.
*
* This shader doesn't use ASM or any other bytecode manip library; it just scans constant pools.
*
* It's limited in its capabilities: A shading op (rename 'this' package to 'that') requires same-length strings so that the replacement can be done without moving things around.
*/
class PackageShader {
private final byte[][] froms; // contains package prefixes that need shading.
private final byte[][] tos; // contains the post-shading package prefixes. froms.length == tos.length.
/**
* @param shadeOps provide pairs of strings; each pair is a from,to tuple listing binary package prefixes, e.g. "Lorg/objectweb/asm", "Lorg/lombokweb/asm".
*/
public PackageShader(String... shadeOps) {
if (shadeOps.length % 2 != 0) throw new IllegalArgumentException("Provide pairs: real package name to shaded package name (you provided an odd number of strings; even number required)");
Charset ascii = Charset.forName("US-ASCII");
int len = shadeOps.length / 2;
froms = new byte[len][];
tos = new byte[len][];
for (int i = 0; i < len; i ++) {
String in = shadeOps[i << 1];
String out = shadeOps[(i << 1) | 1];
if (in.contains(".") || out.contains(".")) throw new IllegalArgumentException("Binary name prefixes are required (use slashes and dollars instead of dots to separate type name elements); they look like e.g. 'java/util/'. Violating entry: " + in + " -> " + out);
froms[i] = in.getBytes(ascii);
tos[i] = out.getBytes(ascii);
if (froms[i].length != tos[i].length) throw new IllegalArgumentException("Pair [" + in + " -> " + out + "] is invalid: Both strings must be the same length");
}
}
private static final byte CONSTANTPOOLTYPE_UTF8 = 1;
/**
* Fuxxes the constant pool of the provided bytecode, rewriting any constant pool entries that represent types that start with any of the registered package shade requests to the shaded variant.
*
* The byte array is modified in-line.
*
* @param b Bytecode
* @return {@code true} if at least one shading modification has been applied.
*/
public boolean apply(byte[] b) {
ClassFileMetaData md = new ClassFileMetaData(b);
boolean changes = false;
// We want to replace the package name but it can occur in the middle of a 'signature' string, e.g. ([I[Ljava/util/List;)Ljava/util/List; - so replace from start and _after_ every capital-L.
// 'startPoints' tracks every location inside the constant pool UTF8 value that _could_ contain a fully qualified type name: Position 0 plus after _every_ capital-L in the UTF-8.
int[] startPoints = new int[260];
int maxStartPoints;
for (int offset : md.getOffsets(CONSTANTPOOLTYPE_UTF8)) {
int len = readValue(b, offset);
offset += 2;
startPoints[0] = offset;
maxStartPoints = 1;
for (int i = offset; i < offset + len; i++) {
if (b[i] == 'L') startPoints[maxStartPoints++] = offset + 1;
}
outer: for (int i = 0; i < froms.length; i++) {
for (int startPointIdx = 0; startPointIdx < maxStartPoints; startPointIdx++) {
int indexIntoSignature = startPoints[startPointIdx];
if (len - indexIntoSignature < froms[i].length) continue;
for (int p = 0; p < froms[i].length; p++) {
if (b[offset + indexIntoSignature + p] != froms[i][p]) continue outer;
}
System.arraycopy(tos[i], 0, b, offset + indexIntoSignature, tos[i].length);
changes = true;
}
}
}
return changes;
}
/**
* Reads a 16-bit value at {@code position}
*/
private static int readValue(byte[] b, int position) {
return ((b[position] & 0xFF) << 8) | (b[position + 1] & 0xFF);
}
public String reverseResourceName(String name) {
outer: for (int i = 0; i < tos.length; i++) {
int len = tos[i].length;
if (name.length() < len) continue;
for (int p = 0; p < len; p++) {
if (name.charAt(p) != tos[i][p]) continue outer;
}
try {
String out = new String(froms[i], 0, froms[i].length, "US-ASCII") + name.substring(len);
return out;
} catch (UnsupportedEncodingException e) {
// Can't happen - US-ASCII is guaranteed by the JVM spec
return name;
}
}
return name;
}
}
@@ -34,6 +34,7 @@ import java.net.URI;
import java.net.URL;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
@@ -105,6 +106,9 @@ class ShadowClassLoader extends ClassLoader {
private final Set<ClassLoader> prependedParentLoaders = Collections.newSetFromMap(new IdentityHashMap<ClassLoader, Boolean>());
private final PackageShader shader = new PackageShader(
"org/objectweb/asm/", "org/lombokweb/asm/");
public void prependParent(ClassLoader loader) {
if (loader == null) return;
if (loader == getParent()) return;
@@ -470,7 +474,9 @@ class ShadowClassLoader extends ClassLoader {
private URL getResource_(String name, boolean noSuper) {
String altName = null;
name = shader.reverseResourceName(name);
if (name.endsWith(".class")) altName = name.substring(0, name.length() - 6) + ".SCL." + sclSuffix;
for (File ce : override) {
URL url = getResourceFromLocation(name, altName, ce);
if (url != null) return url;
@@ -542,7 +548,7 @@ class ShadowClassLoader extends ClassLoader {
if (res == null) {
if (!exclusionListMatch(fileNameOfClass)) {
try {
// First search in the prepended classloaders, the class might be their already
// First search in the prepended classloaders, the class might be there already
for (ClassLoader pre : prependedParentLoaders) {
try {
Class<?> loadClass = pre.loadClass(name);
@@ -589,6 +595,8 @@ class ShadowClassLoader extends ClassLoader {
throw new ClassNotFoundException("I/O exception reading class " + name, e);
}
shader.apply(b);
Class<?> c;
try {
c = defineClass(name, b, 0, p);