From 0a088c2f3afabaf5e2a81fcb804b44f680d332bb Mon Sep 17 00:00:00 2001 From: danyadev Date: Wed, 20 May 2026 07:17:03 +0300 Subject: [PATCH 1/2] use interfaces from metadata instead of ClassNode in MixinPluginTransformer --- gradlew | 0 .../transformers/MixinPluginTransformer.java | 13 ++----- .../lib/turboasm/ClassHeaderMetadata.java | 37 ++++++++++++++++++- .../lib/turboasm/FastClassAccessor.java | 31 +++++++++++++++- 4 files changed, 70 insertions(+), 11 deletions(-) mode change 100644 => 100755 gradlew diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/src/main/java/com/falsepattern/lib/internal/asm/transformers/MixinPluginTransformer.java b/src/main/java/com/falsepattern/lib/internal/asm/transformers/MixinPluginTransformer.java index efe179f..c27f2bc 100644 --- a/src/main/java/com/falsepattern/lib/internal/asm/transformers/MixinPluginTransformer.java +++ b/src/main/java/com/falsepattern/lib/internal/asm/transformers/MixinPluginTransformer.java @@ -23,6 +23,7 @@ package com.falsepattern.lib.internal.asm.transformers; import com.falsepattern.lib.internal.Tags; +import com.falsepattern.lib.turboasm.ClassHeaderMetadata; import com.falsepattern.lib.turboasm.ClassNodeHandle; import com.falsepattern.lib.turboasm.TurboClassTransformer; import lombok.val; @@ -75,17 +76,11 @@ public boolean shouldTransformClass(@NotNull String className, @NotNull ClassNod if (!classNode.isPresent()) return false; - if (classNode.isOriginal()) { - val meta = classNode.getOriginalMetadata(); - if (meta != null && meta.interfacesCount == 0) - return false; - } - - val cn = classNode.getNode(); - if (cn == null) + ClassHeaderMetadata metadata = classNode.getOriginalMetadata(); + if (metadata == null) return false; - for (String i : cn.interfaces) { + for (String i : metadata.binaryInterfaceNames()) { if (IMIXINPLUGIN_INTERNAL.equals(i) || IMIXINCONFIGPLUGIN_INTERNAL.equals(i)) { return true; } diff --git a/src/main/java/com/falsepattern/lib/turboasm/ClassHeaderMetadata.java b/src/main/java/com/falsepattern/lib/turboasm/ClassHeaderMetadata.java index 103bc6b..44319aa 100644 --- a/src/main/java/com/falsepattern/lib/turboasm/ClassHeaderMetadata.java +++ b/src/main/java/com/falsepattern/lib/turboasm/ClassHeaderMetadata.java @@ -30,6 +30,9 @@ import java.io.ByteArrayInputStream; import java.io.DataInputStream; import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; /** * Utilities for quickly processing class files without fully parsing them. @@ -49,8 +52,11 @@ public final class ClassHeaderMetadata implements FastClassAccessor { public final int thisClassIndex; public final int superClassIndex; public final int interfacesCount; + public final int @NotNull [] interfaceIndices; public final @NotNull String binaryThisName; public final @Nullable String binarySuperName; + /** List is unmodifiable */ + public final @NotNull List<@NotNull String> binaryInterfaceNames; /** * Attempts to parse a class header. @@ -88,6 +94,9 @@ public ClassHeaderMetadata(byte @NotNull [] bytes) { this.thisClassIndex = u16(bytes, cpOff + Offsets.pastCpThisClassU16); this.superClassIndex = u16(bytes, cpOff + Offsets.pastCpSuperClassU16); this.interfacesCount = u16(bytes, cpOff + Offsets.pastCpInterfacesCountU16); + this.interfaceIndices = new int[this.interfacesCount]; + List interfaceNames = new ArrayList<>(this.interfacesCount); + // Parse this&super names if (constantPoolEntryTypes[thisClassIndex - 1] != ConstantPoolEntryTypes.Class) { throw new IllegalArgumentException("This class index is not a class ref"); @@ -110,6 +119,25 @@ public ClassHeaderMetadata(byte @NotNull [] bytes) { } this.binarySuperName = modifiedUtf8(bytes, constantPoolEntryOffsets[superNameIndex - 1] + 1); } + + // Parse interface names + for (int i = 0; i < this.interfacesCount; i++) { + final int interfaceOffset = cpOff + Offsets.pastCpInterfacesList + i * 2; + final int interfaceIndex = u16(bytes, interfaceOffset); + if (constantPoolEntryTypes[interfaceIndex - 1] != ConstantPoolEntryTypes.Class) { + throw new IllegalArgumentException("Interface " + i + " index is not a class ref"); + } + final int interfaceNameIndex = u16(bytes, constantPoolEntryOffsets[interfaceIndex - 1] + 1); + if (constantPoolEntryTypes[interfaceNameIndex - 1] != ConstantPoolEntryTypes.Utf8) { + throw new IllegalArgumentException("Interface " + i + " index does not point to a UTF8 entry"); + } + final String binaryInterfaceName = + modifiedUtf8(bytes, constantPoolEntryOffsets[interfaceNameIndex - 1] + 1); + + this.interfaceIndices[i] = interfaceIndex; + interfaceNames.add(binaryInterfaceName); + } + this.binaryInterfaceNames = Collections.unmodifiableList(interfaceNames); } /** Helpers to read big-endian values from class files. */ @@ -189,6 +217,8 @@ private Offsets() {} public static final int pastCpSuperClassU16 = pastCpThisClassU16 + 2; /** The value of the interfaces_count item gives the number of direct superinterfaces of this class or interface type */ public static final int pastCpInterfacesCountU16 = pastCpSuperClassU16 + 2; + + public static final int pastCpInterfacesList = pastCpInterfacesCountU16 + 2; } public enum ConstantPoolEntryTypes { @@ -354,4 +384,9 @@ public boolean isEnum() { public @Nullable String binarySuperName() { return binarySuperName; } -} \ No newline at end of file + + @Override + public @NotNull List<@NotNull String> binaryInterfaceNames() { + return binaryInterfaceNames; + } +} diff --git a/src/main/java/com/falsepattern/lib/turboasm/FastClassAccessor.java b/src/main/java/com/falsepattern/lib/turboasm/FastClassAccessor.java index 49ca1f7..e79cceb 100644 --- a/src/main/java/com/falsepattern/lib/turboasm/FastClassAccessor.java +++ b/src/main/java/com/falsepattern/lib/turboasm/FastClassAccessor.java @@ -28,6 +28,9 @@ import org.objectweb.asm.tree.ClassNode; import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; /** An accessor to metadata about a class that is quickly accessible without fully parsing one. */ public interface FastClassAccessor { @@ -51,6 +54,9 @@ public interface FastClassAccessor { /** Binary (slash-separated packages) name of the super-class, null for the Object class */ @Nullable String binarySuperName(); + /** Binary (slash-separated packages) names of the implemented interfaces, list is unmodifiable */ + @NotNull + List<@NotNull String> binaryInterfaceNames(); static OfLoaded ofLoaded(Class loadedClass) { return new OfLoaded(loadedClass); @@ -111,10 +117,18 @@ public boolean isEnum() { public @Nullable String binarySuperName() { return handle.superName; } + + @Override + public @NotNull List<@NotNull String> binaryInterfaceNames() { + return handle.interfaces == null + ? Collections.emptyList() + : Collections.unmodifiableList(handle.interfaces); + } } final class OfLoaded implements FastClassAccessor { public final Class handle; + private @Nullable List<@NotNull String> interfacesCache = null; private OfLoaded(Class handle) { this.handle = handle; @@ -165,5 +179,20 @@ public boolean isEnum() { final Class superclass = handle.getSuperclass(); return superclass == null ? null : superclass.getName().replace('.', '/'); } + + @Override + public @NotNull List<@NotNull String> binaryInterfaceNames() { + if (interfacesCache != null) { + return interfacesCache; + } + + Class[] interfaces = handle.getInterfaces(); + List binaryInterfaceNames = new ArrayList<>(interfaces.length); + for (Class iface : interfaces) { + binaryInterfaceNames.add(iface.getName().replace('.', '/')); + } + interfacesCache = Collections.unmodifiableList(binaryInterfaceNames); + return interfacesCache; + } } -} \ No newline at end of file +} From 17619eedcc483f81a9eb8295b3f1c670b548bfbc Mon Sep 17 00:00:00 2001 From: danyadev Date: Wed, 20 May 2026 07:39:42 +0300 Subject: [PATCH 2/2] use byte matcher to find Config annotation descriptor instead of ClassNode --- .../transformers/ConfigOrderTransformer.java | 21 +-- .../lib/turboasm/BytePatternMatcher.java | 137 ++++++++++++++++++ .../lib/turboasm/ClassHeaderMetadata.java | 52 +++++-- 3 files changed, 186 insertions(+), 24 deletions(-) create mode 100644 src/main/java/com/falsepattern/lib/turboasm/BytePatternMatcher.java diff --git a/src/main/java/com/falsepattern/lib/internal/asm/transformers/ConfigOrderTransformer.java b/src/main/java/com/falsepattern/lib/internal/asm/transformers/ConfigOrderTransformer.java index 728aaf3..9007bcd 100644 --- a/src/main/java/com/falsepattern/lib/internal/asm/transformers/ConfigOrderTransformer.java +++ b/src/main/java/com/falsepattern/lib/internal/asm/transformers/ConfigOrderTransformer.java @@ -25,6 +25,8 @@ import com.falsepattern.lib.config.Config; import com.falsepattern.lib.internal.Tags; import com.falsepattern.lib.internal.impl.config.DeclOrderInternal; +import com.falsepattern.lib.turboasm.BytePatternMatcher; +import com.falsepattern.lib.turboasm.ClassHeaderMetadata; import com.falsepattern.lib.turboasm.ClassNodeHandle; import com.falsepattern.lib.turboasm.TurboClassTransformer; import lombok.val; @@ -37,6 +39,8 @@ public class ConfigOrderTransformer implements TurboClassTransformer { private static final String DESC_CONFIG_IGNORE = Type.getDescriptor(Config.Ignore.class); private static final String DESC_ORDER = Type.getDescriptor(DeclOrderInternal.class); + final BytePatternMatcher configAnnotationMatcher = new BytePatternMatcher(DESC_CONFIG, BytePatternMatcher.Mode.Equals); + @Override public String name() { return "ConfigOrderTransformer"; @@ -49,17 +53,12 @@ public String owner() { @Override public boolean shouldTransformClass(@NotNull String className, @NotNull ClassNodeHandle classNode) { - val cn = classNode.getNode(); - if (cn == null) + final ClassHeaderMetadata metadata = classNode.getOriginalMetadata(); + if (metadata == null) { return false; - if (cn.visibleAnnotations != null) { - for (val ann : cn.visibleAnnotations) { - if (DESC_CONFIG.equals(ann.desc)) { - return true; - } - } } - return false; + + return metadata.matchesBytes(configAnnotationMatcher); } @Override @@ -75,7 +74,9 @@ public boolean transformClass(@NotNull String className, @NotNull ClassNodeHandl || (field.access & Opcodes.ACC_STATIC) == 0 || (field.access & Opcodes.ACC_FINAL) != 0) { continue; - } else if (field.visibleAnnotations != null) { + } + + if (field.visibleAnnotations != null) { for (val ann : field.visibleAnnotations) { if (DESC_CONFIG_IGNORE.equals(ann.desc)) { continue outer; diff --git a/src/main/java/com/falsepattern/lib/turboasm/BytePatternMatcher.java b/src/main/java/com/falsepattern/lib/turboasm/BytePatternMatcher.java new file mode 100644 index 0000000..db2ae26 --- /dev/null +++ b/src/main/java/com/falsepattern/lib/turboasm/BytePatternMatcher.java @@ -0,0 +1,137 @@ +package com.falsepattern.lib.turboasm; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +public class BytePatternMatcher { + final Mode mode; + // first byte -> matched patterns + final byte[][][] byFirst = new byte[256][][]; + int minPatternLen = Integer.MAX_VALUE; + + public enum Mode { + /** Checks if the constant pool entry contains a pattern */ + Contains, + /** Checks if the whole constant pool entry equals a pattern */ + Equals, + /** Checks if the constant pool entry starts with a pattern */ + StartsWith + } + + public BytePatternMatcher(String strPattern, Mode mode) { + this(new String[] {strPattern}, mode); + } + + public BytePatternMatcher(String[] strPatterns, Mode mode) { + this.mode = mode; + + final byte[][] patterns = new byte[strPatterns.length][]; + final int[] bucketSizes = new int[256]; + final int[] bucketIndices = new int[Math.min(strPatterns.length, 256)]; + int bucketsCount = 0; + + for (int i = 0; i < strPatterns.length; i++) { + final byte[] pattern = strPatterns[i].getBytes(StandardCharsets.UTF_8); + patterns[i] = pattern; + + if (pattern.length < minPatternLen) { + minPatternLen = pattern.length; + } + + final int bucketIndex = pattern[0] & 0xFF; + if (bucketSizes[bucketIndex]++ == 0) { + bucketIndices[bucketsCount++] = bucketIndex; + } + } + + // Ascending sorting by length + Arrays.sort(patterns, (a, b) -> Integer.compare(a.length, b.length)); + + for (int i = 0; i < bucketsCount; i++) { + final int bucketIndex = bucketIndices[i]; + byFirst[bucketIndex] = new byte[bucketSizes[bucketIndex]][]; + bucketSizes[bucketIndex] = 0; // reuse as write index + } + + for (final byte[] pattern : patterns) { + final int bucketIndex = pattern[0] & 0xFF; + byFirst[bucketIndex][bucketSizes[bucketIndex]++] = pattern; + } + } + + public boolean matches(byte[] bytes, int start, int len) { + if (len < minPatternLen) { + return false; + } + + return switch (mode) { + case Contains -> contains(bytes, start, len); + case Equals -> equals(bytes, start, len); + case StartsWith -> startsWith(bytes, start, len); + }; + } + + private boolean contains(byte[] bytes, int start, int len) { + final int end = start + len; + + for (int pos = start; pos <= end - minPatternLen; pos++) { + final byte[][] patterns = byFirst[bytes[pos] & 0xFF]; + if (patterns == null) { + continue; + } + + for (final byte[] pattern : patterns) { + if (pattern.length > end - pos) { + break; + } + + int k = pattern.length - 1; + while (k > 0 && bytes[pos + k] == pattern[k]) k--; + if (k == 0) return true; + } + } + + return false; + } + + private boolean equals(byte[] bytes, int start, int len) { + final byte[][] patterns = byFirst[bytes[start] & 0xFF]; + if (patterns == null) { + return false; + } + + for (final byte[] pattern : patterns) { + if (pattern.length < len) { + continue; + } + if (pattern.length > len) { + break; + } + + int k = pattern.length - 1; + while (k > 0 && bytes[start + k] == pattern[k]) k--; + if (k == 0) return true; + } + + return false; + } + + private boolean startsWith(byte[] bytes, int start, int len) { + final byte[][] patterns = byFirst[bytes[start] & 0xFF]; + if (patterns == null) { + return false; + } + + for (final byte[] pattern : patterns) { + if (pattern.length > len) { + break; + } + + int k = pattern.length - 1; + while (k > 0 && bytes[start + k] == pattern[k]) k--; + if (k == 0) return true; + } + + return false; + } +} diff --git a/src/main/java/com/falsepattern/lib/turboasm/ClassHeaderMetadata.java b/src/main/java/com/falsepattern/lib/turboasm/ClassHeaderMetadata.java index 44319aa..5699062 100644 --- a/src/main/java/com/falsepattern/lib/turboasm/ClassHeaderMetadata.java +++ b/src/main/java/com/falsepattern/lib/turboasm/ClassHeaderMetadata.java @@ -31,6 +31,7 @@ import java.io.DataInputStream; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -39,6 +40,7 @@ */ public final class ClassHeaderMetadata implements FastClassAccessor { + public final byte[] classBytes; public final int minorVersion; public final int majorVersion; public final int constantPoolEntryCount; @@ -47,6 +49,9 @@ public final class ClassHeaderMetadata implements FastClassAccessor { /** Type of each parsed constant pool entry, zero-indexed! */ public final ConstantPoolEntryTypes @NotNull [] constantPoolEntryTypes; + /** Approximately only half of the entries are utf-8, so this array can be used to iterate over them more quickly */ + public final int @NotNull [] constantPoolUtf8EntryOffsets; + public final int constantPoolEndOffset; public final int accessFlags; public final int thisClassIndex; @@ -66,15 +71,17 @@ public ClassHeaderMetadata(byte @NotNull [] bytes) { if (!isValidClass(bytes)) { throw new IllegalArgumentException("Invalid class detected"); } + this.classBytes = bytes; this.minorVersion = u16(bytes, Offsets.minorVersionU16); this.majorVersion = u16(bytes, Offsets.majorVersionU16); this.constantPoolEntryCount = u16(bytes, Offsets.constantPoolCountU16); this.constantPoolEntryOffsets = new int[constantPoolEntryCount]; this.constantPoolEntryTypes = new ConstantPoolEntryTypes[constantPoolEntryCount]; // scan through CP entries - final int cpOff; { int off = Offsets.constantPoolStart; + final int[] utf8EntryOffsets = new int[constantPoolEntryCount]; + int utf8Entries = 0; for (int entry = 0; entry < constantPoolEntryCount - 1; entry++) { constantPoolEntryOffsets[entry] = off; ConstantPoolEntryTypes type = ConstantPoolEntryTypes.parse(bytes, off); @@ -84,18 +91,18 @@ public ClassHeaderMetadata(byte @NotNull [] bytes) { entry++; constantPoolEntryOffsets[entry] = off; constantPoolEntryTypes[entry] = type; + } else if (type == ConstantPoolEntryTypes.Utf8) { + utf8EntryOffsets[utf8Entries++] = off; } off += type.byteLength(bytes, off); } - cpOff = off; - this.constantPoolEndOffset = cpOff; + this.constantPoolEndOffset = off; + this.constantPoolUtf8EntryOffsets = Arrays.copyOf(utf8EntryOffsets, utf8Entries); } - this.accessFlags = u16(bytes, cpOff + Offsets.pastCpAccessFlagsU16); - this.thisClassIndex = u16(bytes, cpOff + Offsets.pastCpThisClassU16); - this.superClassIndex = u16(bytes, cpOff + Offsets.pastCpSuperClassU16); - this.interfacesCount = u16(bytes, cpOff + Offsets.pastCpInterfacesCountU16); - this.interfaceIndices = new int[this.interfacesCount]; - List interfaceNames = new ArrayList<>(this.interfacesCount); + this.accessFlags = u16(bytes, this.constantPoolEndOffset + Offsets.pastCpAccessFlagsU16); + this.thisClassIndex = u16(bytes, this.constantPoolEndOffset + Offsets.pastCpThisClassU16); + this.superClassIndex = u16(bytes, this.constantPoolEndOffset + Offsets.pastCpSuperClassU16); + this.interfacesCount = u16(bytes, this.constantPoolEndOffset + Offsets.pastCpInterfacesCountU16); // Parse this&super names if (constantPoolEntryTypes[thisClassIndex - 1] != ConstantPoolEntryTypes.Class) { @@ -121,8 +128,10 @@ public ClassHeaderMetadata(byte @NotNull [] bytes) { } // Parse interface names + List interfaceNames = new ArrayList<>(this.interfacesCount); + this.interfaceIndices = new int[this.interfacesCount]; for (int i = 0; i < this.interfacesCount; i++) { - final int interfaceOffset = cpOff + Offsets.pastCpInterfacesList + i * 2; + final int interfaceOffset = this.constantPoolEndOffset + Offsets.pastCpInterfacesList + i * 2; final int interfaceIndex = u16(bytes, interfaceOffset); if (constantPoolEntryTypes[interfaceIndex - 1] != ConstantPoolEntryTypes.Class) { throw new IllegalArgumentException("Interface " + i + " index is not a class ref"); @@ -314,11 +323,26 @@ public static int majorVersion(byte @NotNull [] classBytes) { } /** - * Searches for a sub"string" (byte array) in a longer byte array. Not efficient for long search strings. - * @param classBytes The long byte string to search in. - * @param substring The short substring to search for. - * @return If the substring was found somewhere in the long string. + * Searches for byte patterns in the constant pool. + * @param matcher A configured byte matcher with patterns to search for. + * @return {@code true} if there is a match for at least one constant pool entry. */ + public boolean matchesBytes(final BytePatternMatcher matcher) { + for (final int offset : constantPoolUtf8EntryOffsets) { + // first byte is entry type, second and third bytes are length + final int length = u16(classBytes, offset + 1); + final int start = offset + 3; + + if (matcher.matches(classBytes, start, length)) { + return true; + } + } + + return false; + } + + /** @deprecated This method is very slow, use {@link #matchesBytes} instead */ + @Deprecated public static boolean hasSubstring(final byte @Nullable [] classBytes, final byte @NotNull [] substring) { if (classBytes == null) { return false;