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 @@ package com.falsepattern.lib.internal.asm.transformers; 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 class ConfigOrderTransformer implements TurboClassTransformer { @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 class ConfigOrderTransformer implements TurboClassTransformer { || (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.ByteArrayInputStream; 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 @@ import java.util.List; */ 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 final class ClassHeaderMetadata implements FastClassAccessor { 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 final class ClassHeaderMetadata implements FastClassAccessor { 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 final class ClassHeaderMetadata implements FastClassAccessor { } // 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 final class ClassHeaderMetadata implements FastClassAccessor { } /** - * 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;