Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/main/java/cpw/mods/modlauncher/ClassTransformer.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ byte[] transform(byte[] inputClass, String className, final String reason) {
final String internalName = className.replace('.', '/');
final Type classDesc = Type.getObjectType(internalName);

final EnumMap<ILaunchPluginService.Phase, List<ILaunchPluginService>> launchPluginTransformerSet = pluginHandler.computeLaunchPluginTransformerSet(classDesc, inputClass.length == 0, reason, this.auditTrail);
final EnumMap<ILaunchPluginService.Phase, List<ILaunchPluginService>> launchPluginTransformerSet = pluginHandler.computeLaunchPluginTransformerSet(classDesc, inputClass, reason, this.auditTrail);

final boolean needsTransforming = transformers.needsTransforming(internalName);
if (!needsTransforming && launchPluginTransformerSet.isEmpty()) {
Expand Down
15 changes: 13 additions & 2 deletions src/main/java/cpw/mods/modlauncher/LaunchPluginHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import cpw.mods.modlauncher.api.IEnvironment;
import cpw.mods.modlauncher.api.IModuleLayerManager;
import cpw.mods.modlauncher.api.NamedPath;
import cpw.mods.modlauncher.util.ClassConstantPoolParser;
import cpw.mods.modlauncher.util.ServiceLoaderUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
Expand Down Expand Up @@ -62,11 +63,21 @@ public Optional<ILaunchPluginService> get(final String name) {
return Optional.ofNullable(plugins.get(name));
}

public EnumMap<ILaunchPluginService.Phase, List<ILaunchPluginService>> computeLaunchPluginTransformerSet(final Type className, final boolean isEmpty, final String reason, final TransformerAuditTrail auditTrail) {
public EnumMap<ILaunchPluginService.Phase, List<ILaunchPluginService>> computeLaunchPluginTransformerSet(final Type className, final byte[] inputClass, final String reason, final TransformerAuditTrail auditTrail) {
Set<ILaunchPluginService> uniqueValues = new HashSet<>();
final EnumMap<ILaunchPluginService.Phase, List<ILaunchPluginService>> phaseObjectEnumMap = new EnumMap<>(ILaunchPluginService.Phase.class);
for (ILaunchPluginService plugin : plugins.values()) {
for (ILaunchPluginService.Phase ph : plugin.handlesClass(className, isEmpty, reason)) {
// Check if the plugin handles classes of this name at all
var phaseSet = plugin.handlesClass(className, inputClass.length == 0, reason);
if (phaseSet.isEmpty()) {
continue;
}
// Filter out classes that don't match the constants filter
if (!ClassConstantPoolParser.constantPoolMatches(plugin.constantsFilter(className, reason), inputClass)) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need a way to opt-out of this behavior in case it causes issues (for example due to forgetting to update the constant pool parser). Probably via an env variable or ML arg.

continue;
}
// The plugin will transform this class, add it to the list
for (ILaunchPluginService.Phase ph : phaseSet) {
phaseObjectEnumMap.computeIfAbsent(ph, e -> new ArrayList<>()).add(plugin);
if (uniqueValues.add(plugin)) {
plugin.customAuditConsumer(className.getClassName(), strings -> auditTrail.addPluginCustomAuditTrail(className.getClassName(), plugin, strings));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,20 @@ default void initializeLaunch(ITransformerLoader transformerLoader, NamedPath[]
default void customAuditConsumer(String className, Consumer<String[]> auditDataAcceptor) {
}

/**
* If this transformer should only run when a class' constant pool contains a given byte sequence,
* return it here. Multiple byte sequences will be treated as an OR relationship, and an empty
* array indicates that no filtering should be performed.
* <p></p>
* The return value of this method should be cached as it will be called frequently.
* @param classType class type being transformed
* @param reason the reason for the class being loaded/transformed
* @return an array of sequences of bytes to search for in the class' constant pool
*/
default byte[][] constantsFilter(Type classType, String reason) {
return new byte[0][0];
}

interface ITransformerLoader {
byte[] buildTransformedClassNodeFor(final String className) throws ClassNotFoundException;
}
Expand Down
110 changes: 110 additions & 0 deletions src/main/java/cpw/mods/modlauncher/util/ClassConstantPoolParser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/***
* This Class is derived from the ASM ClassReader
* <p>
* ASM: a very small and fast Java bytecode manipulation framework Copyright (c) 2000-2011 INRIA, France Telecom All
* rights reserved.
* <p>
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
* following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation and/or other materials provided with the
* distribution. 3. Neither the name of the copyright holders nor the names of its contributors may be used to endorse
* or promote products derived from this software without specific prior written permission.
* <p>
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

package cpw.mods.modlauncher.util;

/**
* Using this class to search for a (single) String reference is > 40 times faster than parsing a class with a ClassReader +
* ClassNode while using way less RAM
*/
public class ClassConstantPoolParser {

private static final int UTF8 = 1;
private static final int INT = 3;
private static final int FLOAT = 4;
private static final int LONG = 5;
private static final int DOUBLE = 6;
private static final int FIELD = 9;
private static final int METH = 10;
private static final int IMETH = 11;
private static final int NAME_TYPE = 12;
private static final int HANDLE = 15;
private static final int INDY = 18;

/**
* Returns true if the constant pool of the class represented by this byte array contains one of the Strings we are looking
* for
*/
public static boolean constantPoolMatches(byte[][] stringsToSearch, byte[] basicClass) {
if (stringsToSearch.length == 0) {
return true; // empty list
}
if (basicClass == null || basicClass.length == 0) {
return true; // assume empty classes match
}

// parses the constant pool
int n = readUnsignedShort(8, basicClass);
int index = 10;
for (int i = 1; i < n; ++i) {
int size;
switch (basicClass[index]) {
case FIELD:
case METH:
case IMETH:
case INT:
case FLOAT:
case NAME_TYPE:
case INDY:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You forgot CONDY.

size = 5;
break;
case LONG:
case DOUBLE:
size = 9;
++i;
break;
case UTF8:
final int strLen = readUnsignedShort(index + 1, basicClass);
size = 3 + strLen;
label:
for (byte[] bytes : stringsToSearch) {
if (strLen == bytes.length) {
for (int j = index + 3; j < index + 3 + strLen; j++) {
if (basicClass[j] != bytes[j - (index + 3)]) {
break label;
}
}
return true;
}
}
break;
case HANDLE:
size = 4;
break;
default:
size = 3;
break;
}
index += size;
}
return false;
}

private static short readShort(final int index, byte[] basicClass) {
return (short) (((basicClass[index] & 0xFF) << 8) | (basicClass[index + 1] & 0xFF));
}

private static int readUnsignedShort(final int index, byte[] basicClass) {
return ((basicClass[index] & 0xFF) << 8) | (basicClass[index + 1] & 0xFF);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package cpw.mods.modlauncher.test;

import cpw.mods.modlauncher.serviceapi.ILaunchPluginService;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldNode;

import java.nio.charset.StandardCharsets;
import java.util.EnumSet;

public class MockLaunchPluginService implements ILaunchPluginService {
@Override
public String name() {
return "testlaunchplugin";
}

private static final EnumSet<Phase> YAY = EnumSet.of(Phase.BEFORE);

@Override
public EnumSet<Phase> handlesClass(Type classType, boolean isEmpty) {
return YAY;
}

@Override
public boolean processClass(Phase phase, ClassNode classNode, Type classType) {
FieldNode fn = new FieldNode(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "testfield2", "Ljava/lang/String;", null, "BUTTER!");
classNode.fields.add(fn);
return true;
}

// We'll test that filtering for the Ljava/lang/String; constant pool entry used by 'testfield' (which is injected by
// the other mock transformer) works
// Note: This assumes we run after that transformer

private static final byte[][] FILTER = new byte[][] {
"Ljava/lang/String;".getBytes(StandardCharsets.UTF_8)
};

@Override
public byte[][] constantsFilter(Type classType, String reason) {
return FILTER;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import cpw.mods.modlauncher.api.IEnvironment;
import cpw.mods.modlauncher.api.ITransformer;
import cpw.mods.modlauncher.api.TypesafeMap;
import cpw.mods.modlauncher.serviceapi.ILaunchPluginService;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Test;
import org.powermock.reflect.Whitebox;
Expand All @@ -33,6 +34,7 @@
import java.lang.reflect.Constructor;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
Expand All @@ -58,6 +60,9 @@ public List<? extends ITransformer<?>> transformers() {
TransformStore transformStore = new TransformStore();
ModuleLayerHandler layerHandler = Whitebox.invokeConstructor(ModuleLayerHandler.class);
LaunchPluginHandler lph = new LaunchPluginHandler(layerHandler);
MockLaunchPluginService mockLaunchPluginService = new MockLaunchPluginService();
// Inject it
((Map<String, ILaunchPluginService>)Whitebox.getField(LaunchPluginHandler.class, "plugins").get(lph)).put(mockLaunchPluginService.name(), mockLaunchPluginService);
TransformationServiceDecorator sd = Whitebox.invokeConstructor(TransformationServiceDecorator.class, mockTransformerService);
sd.gatherTransformers(transformStore);

Expand All @@ -72,6 +77,8 @@ public List<? extends ITransformer<?>> transformers() {
final Class<?> aClass = Class.forName(TARGET_CLASS, true, tcl);
assertEquals(Whitebox.getField(aClass, "testfield").getType(), String.class);
assertEquals(Whitebox.getField(aClass, "testfield").get(null), "CHEESE!");
// Check that the field injected by our MockLaunchPluginService that uses a filter works
assertEquals(Whitebox.getField(aClass, "testfield2").get(null), "BUTTER!");

final Class<?> newClass = tcl.loadClass(TARGET_CLASS);
assertEquals(aClass, newClass, "Class instance is the same from Class.forName and tcl.loadClass");
Expand Down