diff --git a/src/main/java/cpw/mods/modlauncher/ClassTransformer.java b/src/main/java/cpw/mods/modlauncher/ClassTransformer.java index 88376c4..ca3d41c 100644 --- a/src/main/java/cpw/mods/modlauncher/ClassTransformer.java +++ b/src/main/java/cpw/mods/modlauncher/ClassTransformer.java @@ -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> launchPluginTransformerSet = pluginHandler.computeLaunchPluginTransformerSet(classDesc, inputClass.length == 0, reason, this.auditTrail); + final EnumMap> launchPluginTransformerSet = pluginHandler.computeLaunchPluginTransformerSet(classDesc, inputClass, reason, this.auditTrail); final boolean needsTransforming = transformers.needsTransforming(internalName); if (!needsTransforming && launchPluginTransformerSet.isEmpty()) { diff --git a/src/main/java/cpw/mods/modlauncher/LaunchPluginHandler.java b/src/main/java/cpw/mods/modlauncher/LaunchPluginHandler.java index c406977..66a6b36 100644 --- a/src/main/java/cpw/mods/modlauncher/LaunchPluginHandler.java +++ b/src/main/java/cpw/mods/modlauncher/LaunchPluginHandler.java @@ -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; @@ -62,11 +63,21 @@ public Optional get(final String name) { return Optional.ofNullable(plugins.get(name)); } - public EnumMap> computeLaunchPluginTransformerSet(final Type className, final boolean isEmpty, final String reason, final TransformerAuditTrail auditTrail) { + public EnumMap> computeLaunchPluginTransformerSet(final Type className, final byte[] inputClass, final String reason, final TransformerAuditTrail auditTrail) { Set uniqueValues = new HashSet<>(); final EnumMap> 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)) { + 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)); diff --git a/src/main/java/cpw/mods/modlauncher/serviceapi/ILaunchPluginService.java b/src/main/java/cpw/mods/modlauncher/serviceapi/ILaunchPluginService.java index 64c9900..117afe2 100644 --- a/src/main/java/cpw/mods/modlauncher/serviceapi/ILaunchPluginService.java +++ b/src/main/java/cpw/mods/modlauncher/serviceapi/ILaunchPluginService.java @@ -193,6 +193,20 @@ default void initializeLaunch(ITransformerLoader transformerLoader, NamedPath[] default void customAuditConsumer(String className, Consumer 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. + *

+ * 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; } diff --git a/src/main/java/cpw/mods/modlauncher/util/ClassConstantPoolParser.java b/src/main/java/cpw/mods/modlauncher/util/ClassConstantPoolParser.java new file mode 100644 index 0000000..b8ecb9f --- /dev/null +++ b/src/main/java/cpw/mods/modlauncher/util/ClassConstantPoolParser.java @@ -0,0 +1,110 @@ +/*** + * This Class is derived from the ASM ClassReader + *

+ * ASM: a very small and fast Java bytecode manipulation framework Copyright (c) 2000-2011 INRIA, France Telecom All + * rights reserved. + *

+ * 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. + *

+ * 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: + 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); + } + +} diff --git a/src/test/java/cpw/mods/modlauncher/test/MockLaunchPluginService.java b/src/test/java/cpw/mods/modlauncher/test/MockLaunchPluginService.java new file mode 100644 index 0000000..f2435fc --- /dev/null +++ b/src/test/java/cpw/mods/modlauncher/test/MockLaunchPluginService.java @@ -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 YAY = EnumSet.of(Phase.BEFORE); + + @Override + public EnumSet 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; + } +} diff --git a/src/test/java/cpw/mods/modlauncher/test/TransformingClassLoaderTests.java b/src/test/java/cpw/mods/modlauncher/test/TransformingClassLoaderTests.java index 7d1e08a..056a4e7 100644 --- a/src/test/java/cpw/mods/modlauncher/test/TransformingClassLoaderTests.java +++ b/src/test/java/cpw/mods/modlauncher/test/TransformingClassLoaderTests.java @@ -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; @@ -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; @@ -58,6 +60,9 @@ public List> transformers() { TransformStore transformStore = new TransformStore(); ModuleLayerHandler layerHandler = Whitebox.invokeConstructor(ModuleLayerHandler.class); LaunchPluginHandler lph = new LaunchPluginHandler(layerHandler); + MockLaunchPluginService mockLaunchPluginService = new MockLaunchPluginService(); + // Inject it + ((Map)Whitebox.getField(LaunchPluginHandler.class, "plugins").get(lph)).put(mockLaunchPluginService.name(), mockLaunchPluginService); TransformationServiceDecorator sd = Whitebox.invokeConstructor(TransformationServiceDecorator.class, mockTransformerService); sd.gatherTransformers(transformStore); @@ -72,6 +77,8 @@ public List> 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");