diff --git a/readme.md b/readme.md index d0ca270..76cd27d 100644 --- a/readme.md +++ b/readme.md @@ -102,7 +102,11 @@ the front and `;` in the end of the class name, so if you want to match `java.la Not Supported Yet ### What is lazy -Lazy in regex is `?`, for example, `.*?`, it means that it will match as few as possible +Lazy in regex is `?`, for example, `.*?`, it means that it will match as few as possible. + +### Is it thread-safe? +About the matcher, no, not at all. Please create multiple matcher instances to match instructions in a different +thread, even if they are the same. ## Development / Code Quality ### About Testing diff --git a/src/main/kotlin/me/fan87/regbex/Regbex.kt b/src/main/kotlin/me/fan87/regbex/Regbex.kt index ff06822..5ef3a28 100644 --- a/src/main/kotlin/me/fan87/regbex/Regbex.kt +++ b/src/main/kotlin/me/fan87/regbex/Regbex.kt @@ -41,81 +41,145 @@ class Regbex { ////////// Basic Functions (Requires Implementation) ////////// + /** + * A named capture group. Everything inside this block will be captured, and will be accessible via [RegbexMatcher.group(String)][RegbexMatcher.group] + */ fun thenGroup(name: String, regbex: RegbexBuilder) { elements.add(GroupBegin(name)) elements.addAll(regbex.getRegbex().elements) elements.add(GroupEnd()) } + /** + * A custom check to check if an instruction matches or not + */ fun thenCustomCheck(check: (instruction: AbstractInsnNode) -> Boolean) { elements.add(CustomCheck(check)) } + /** + * Amount of [regbex]. Will match as few as possible (lazy) + * Equivalent to `{x,y}?` in regular expression + */ fun thenLazyAmountOf(range: IntProgression, regbex: RegbexBuilder) { elements.add(LazyAmountOfBegin(range)) elements.addAll(regbex.getRegbex().elements) elements.add(LazyAmountOfEnd()) } + /** + * Expect an already captured named group + */ fun thenCapturedGroup(name: String) { elements.add(CapturedGroup(name)) } + /** + * Amount of [regbex]. Will match as much as possible (greedy) + * Equivalent to `{x,y}` in regular expression + */ fun thenAmountOf(range: IntProgression, regbex: RegbexBuilder) { elements.add(GreedyAmountOfBegin(range)) elements.addAll(regbex.getRegbex().elements) elements.add(GreedyAmountOfEnd()) } + /** + * Check without increasing the index of current match. Could be captured, but since it won't increase the index, + * the matched group will always be the same. Being used to do [thenAnd] + */ fun thenCheckWithoutMovingPointer(regbex: RegbexBuilder) { elements.add(CheckWithoutMovingPointerBegin()) elements.addAll(regbex.getRegbex().elements) elements.add(CheckWithoutMovingPointerEnd()) } + /** + * Assert that it's the start of instructions (Index = 0). With [thenEndOfInstructions] at the end, and this at the front, + * it will check if the entire instructions list matches. + * @see thenEndOfInstructions + */ fun thenStartOfInstructions() { elements.add(StartOfInstructions()) } + + /** + * Assert that it's the end of instructions (index = size - 1) + * @see thenStartOfInstructions + */ fun thenEndOfInstructions() { elements.add(EndOfInstructions()) } - ////////// Debug Friendly Aliases ////////// + ////////// Aliases ////////// - fun thenAny() { + /** + * Expect any instruction + */ + fun thenAny() { // . thenCustomCheck { true } } - ////////// Aliases ////////// - - - fun thenLazyAmountOf(amount: Int, regbex: RegbexBuilder) { + /** + * Fixed amount of [regbex] (without a range, but only 1 number), will match as few as possible (lazy) + * Equivalent to `{x}?` in regular expression + * @see thenLazyAmountOf + */ + fun thenLazyAmountOf(amount: Int, regbex: RegbexBuilder) { // {x}? thenLazyAmountOf(amount..amount, regbex) } - fun thenAmountOf(amount: Int, regbex: RegbexBuilder) { + /** + * Fixed amount of [regbex] (without a range, but only 1 number). Will match as much as possible (greedy) + * Equivalent to `{x}` in regular expression + * @see thenAmountOf + */ + fun thenAmountOf(amount: Int, regbex: RegbexBuilder) { // {x} thenAmountOf(amount..amount, regbex) } - // Operators + // Greedy Operators + /** + * Then any amount of matches, will match as much as possible (greedy). + * Equivalent to `*` in regular expression + */ fun thenAnyAmountOf(regbex: RegbexBuilder) { // * thenAmountOf(0..Int.MAX_VALUE, regbex) } + /** + * Then at least one match, will match as much as possible (greedy). + * Equivalent to `+` in regular expression + */ fun thenAtLeastOneOf(regbex: RegbexBuilder) { // + thenAmountOf(1..Int.MAX_VALUE, regbex) } + /** + * Then optional (0-1), will match if possible (greedy) + * Equivalent to `?` in regular expression + */ fun thenOptional(regbex: RegbexBuilder) { // ? thenAmountOf(0..1, regbex) } + // Lazy + /** + * Then any amount of matches, will match as few as possible (lazy) + * Equivalent to `*?` in regular expression + */ fun thenLazyAnyAmountOf(regbex: RegbexBuilder) { // *? thenLazyAmountOf(0..Int.MAX_VALUE, regbex) } + /** + * Then at least one match, will match as few as possible (lazy) + * Equivalent to `+?` in regular expression + */ fun thenLazyAtLeastOneOf(regbex: RegbexBuilder) { // +? thenLazyAmountOf(1..Int.MAX_VALUE, regbex) } - + /** + * And check the same thing. The index pointer will be moved to where the last condition is ended + */ fun thenAnd(vararg regbexs: RegbexBuilder) { val toList = regbexs.toList() for (regbex in toList.dropLast(1)) { @@ -128,19 +192,51 @@ class Regbex { ////////// Advanced Matching ////////// - + /** + * Expect an instruction with [opcode] + */ fun thenOpcodeCheck(opcode: Int) { thenCustomCheck { it.opcode == opcode } } - fun thenTypeCheck(type: Class) { + /** + * Expect an instruction with type [type] (Equal, not assignable from) + */ + fun thenTypeCheckEqual(type: Class) { thenCustomCheck { it.javaClass == type } } + /** + * Expect an instruction with type [T] (Equal, not assignable from) + */ + inline fun thenTypeCheckEqual() { + thenCustomCheck { it.javaClass == T::class.java } + } + + /** + * Expect an instruction with type [type] (Assignable From) + */ + fun thenTypeCheckAssignableFrom(type: Class) { + thenCustomCheck { type.isAssignableFrom(it.javaClass) } + } + + /** + * Expect an instruction with type [T] (Assignable From) + */ + inline fun thenTypeCheckAssignableFrom() { + thenCustomCheck { it is T } + } + + /** + * Expect an exact same instruction + */ fun thenEqual(instruction: AbstractInsnNode) { thenCustomCheck { InstructionEqualChecker.checkEquals(instruction, it) } } + /** + * Expect exact same list of instructions + */ fun thenEqual(list: Iterable) { for (abstractInsnNode in list) { thenEqual(abstractInsnNode) @@ -148,21 +244,36 @@ class Regbex { } // + /** + * Expect a [VarInsnNode] with var number + */ fun thenVarNode(varNumber: Int) { thenCustomCheck { it is VarInsnNode && it.`var` == varNumber } } + + /** + * Expect a [VarInsnNode] with var number and opcode + */ fun thenVarNode(varNumber: Int, opcode: Int) { thenCustomCheck { it.opcode == opcode && it is VarInsnNode && it.`var` == varNumber } } + + /** + * Expect a storing [VarInsnNode] with var number + */ fun thenVarStoreNode(varNumber: Int) { thenCustomCheck { it.opcode in 54..58 && it is VarInsnNode && it.`var` == varNumber } } + + /** + * Expect a loading [VarInsnNode] with var number + */ fun thenVarLoadNode(varNumber: Int) { thenCustomCheck { it.opcode in 21..25 && it is VarInsnNode && it.`var` == varNumber @@ -170,16 +281,25 @@ class Regbex { } // // + /** + * Expect a [LdcInsnNode] with string as [LdcInsnNode.cst]'s type + */ fun thenLdcString() { thenCustomCheck { it is LdcInsnNode && it.cst is String } } + /** + * Expect a [LdcInsnNode] with string + */ fun thenLdcStringEqual(string: String) { thenCustomCheck { it is LdcInsnNode && it.cst is String && it.cst == string } } + /** + * Expect a [LdcInsnNode] with string that matches [regex] + */ fun thenLdcStringMatches(regex: Regex) { thenCustomCheck { it is LdcInsnNode && it.cst is String && (it.cst as String).matches(regex) @@ -187,6 +307,9 @@ class Regbex { } // // + /** + * Then expect a static method call of specified method + */ fun thenMethodCall(ownerType: TypeExp, methodNamePattern: Regex, returnType: TypeExp, vararg argsTypes: TypeExp) { thenCustomCheck { if (it is MethodInsnNode) { @@ -203,13 +326,22 @@ class Regbex { } } + /** + * Then expect a static method call of specified method + */ fun thenStaticMethodCall(ownerType: TypeExp, methodNamePattern: Regex, returnType: TypeExp, vararg argsTypes: TypeExp) { thenAnd({thenCustomCheck { it.opcode == Opcodes.INVOKESTATIC }}, {thenMethodCall(ownerType, methodNamePattern, returnType, *argsTypes)}) } + /** + * Then expect a virtual (non-static) method call of specified method + */ fun thenVirtualMethodCall(ownerType: TypeExp, methodNamePattern: Regex, returnType: TypeExp, vararg argsTypes: TypeExp) { thenAnd({thenCustomCheck { it.opcode == Opcodes.INVOKEVIRTUAL }}, {thenMethodCall(ownerType, methodNamePattern, returnType, *argsTypes)}) } + /** + * Then expect a static method call of specified method without checking its argument types + */ fun thenMethodCallIgnoreArgs(ownerType: TypeExp, methodNamePattern: Regex, returnType: TypeExp) { thenCustomCheck { if (it is MethodInsnNode) { @@ -221,50 +353,73 @@ class Regbex { } } } - + /** + * Then expect a static method call of specified method without checking its argument types + */ fun thenStaticMethodCallIgnoreArgs(ownerType: TypeExp, methodNamePattern: Regex, returnType: TypeExp) { thenAnd({thenCustomCheck { it.opcode == Opcodes.INVOKESTATIC }}, {thenMethodCallIgnoreArgs(ownerType, methodNamePattern, returnType)}) } - + /** + * Then expect a virtual (non-static) method call of specified method without checking its argument types + */ fun thenVirtualMethodCallIgnoreArgs(ownerType: TypeExp, methodNamePattern: Regex, returnType: TypeExp) { thenAnd({thenCustomCheck { it.opcode == Opcodes.INVOKEVIRTUAL }}, {thenMethodCallIgnoreArgs(ownerType, methodNamePattern, returnType)}) } // // + /** + * Then expect a [FieldInsnNode] with specified field info + */ fun thenField(ownerType: TypeExp, fieldNamePattern: Regex, fieldType: TypeExp) { thenCustomCheck { it is FieldInsnNode && ownerType.matches(it.owner) && it.name.matches(fieldNamePattern) && fieldType.matches(it.desc) } } - + /** + * Then expect a static [FieldInsnNode] with specified field info + */ fun thenStaticField(ownerType: TypeExp, fieldNamePattern: Regex, fieldType: TypeExp) { thenAnd({thenCustomCheck { it.opcode == Opcodes.GETSTATIC || it.opcode == Opcodes.PUTSTATIC }}, {thenField(ownerType, fieldNamePattern, fieldType)}) } - + /** + * Then expect a virtual (non-static) [FieldInsnNode] with specified field info + */ fun thenVirtualField(ownerType: TypeExp, fieldNamePattern: Regex, fieldType: TypeExp) { thenAnd({thenCustomCheck { it.opcode == Opcodes.GETFIELD || it.opcode == Opcodes.PUTFIELD }}, {thenField(ownerType, fieldNamePattern, fieldType)}) } - + /** + * Then expect a get [FieldInsnNode] with specified field info + */ fun thenGetField(ownerType: TypeExp, fieldNamePattern: Regex, fieldType: TypeExp) { thenAnd({thenCustomCheck { it.opcode == Opcodes.GETSTATIC || it.opcode == Opcodes.GETFIELD }}, {thenField(ownerType, fieldNamePattern, fieldType)}) } - + /** + * Then expect a get static [FieldInsnNode] with specified field info + */ fun thenGetStaticField(ownerType: TypeExp, fieldNamePattern: Regex, fieldType: TypeExp) { thenAnd({thenCustomCheck { it.opcode == Opcodes.GETSTATIC }}, {thenField(ownerType, fieldNamePattern, fieldType)}) } - + /** + * Then expect a get virtual (non-static) [FieldInsnNode] with specified field info + */ fun thenGetVirtualField(ownerType: TypeExp, fieldNamePattern: Regex, fieldType: TypeExp) { thenAnd({thenCustomCheck { it.opcode == Opcodes.GETFIELD }}, {thenField(ownerType, fieldNamePattern, fieldType)}) } - + /** + * Then expect a put [FieldInsnNode] with specified field info + */ fun thenPutField(ownerType: TypeExp, fieldNamePattern: Regex, fieldType: TypeExp) { thenAnd({thenCustomCheck { it.opcode == Opcodes.PUTSTATIC || it.opcode == Opcodes.PUTFIELD }}, {thenField(ownerType, fieldNamePattern, fieldType)}) } - + /** + * Then expect a put static [FieldInsnNode] with specified field info + */ fun thenPutStaticField(ownerType: TypeExp, fieldNamePattern: Regex, fieldType: TypeExp) { thenAnd({thenCustomCheck { it.opcode == Opcodes.PUTSTATIC }}, {thenField(ownerType, fieldNamePattern, fieldType)}) } - + /** + * Then expect a put virtual (non-static) [FieldInsnNode] with specified field info + */ fun thenPutVirtualField(ownerType: TypeExp, fieldNamePattern: Regex, fieldType: TypeExp) { thenAnd({thenCustomCheck { it.opcode == Opcodes.PUTFIELD }}, {thenField(ownerType, fieldNamePattern, fieldType)}) } diff --git a/src/main/kotlin/me/fan87/regbex/RegbexMatcher.kt b/src/main/kotlin/me/fan87/regbex/RegbexMatcher.kt index b09488a..1db910b 100644 --- a/src/main/kotlin/me/fan87/regbex/RegbexMatcher.kt +++ b/src/main/kotlin/me/fan87/regbex/RegbexMatcher.kt @@ -4,9 +4,14 @@ import me.fan87.regbex.utils.InstructionEqualChecker import org.objectweb.asm.tree.AbstractInsnNode import java.util.Stack -class RegbexMatcher internal constructor(instructions: Iterable, private val pattern: RegbexPattern) { +/** + * A matcher with same concept as Java's [Regular Expression Matcher API][java.util.regex.Matcher]. + * You should only be obtaining this object via [RegbexPattern.matcher]. + */ +class RegbexMatcher internal constructor(instructions: Iterable, private val pattern: RegbexPattern): RegbexResultable { - val instructions = ArrayList() + + private val instructions = ArrayList() init { for (instruction in instructions) { @@ -16,23 +21,17 @@ class RegbexMatcher internal constructor(instructions: Iterable): ArrayList { + override fun replaceGroup(groupName: String, replaceTo: Iterable): ArrayList { checkMatched() if (group(groupName) == null) { return ArrayList(instructions) @@ -54,7 +53,7 @@ class RegbexMatcher internal constructor(instructions: Iterable): ArrayList { + override fun replace(replaceTo: Iterable): ArrayList { checkMatched() val newInstructions = ArrayList() @@ -76,7 +75,7 @@ class RegbexMatcher internal constructor(instructions: Iterable { + override fun matchedSize(): Int { + checkMatched() + return matched!!.size() + } + + override fun group(): List { checkMatched() return matched!!.getRegion() } - fun group(name: String): List? { + override fun group(name: String): List? { checkMatched() val regbexRegion = capturedNamed[name] return regbexRegion?.getRegion() @@ -104,7 +108,7 @@ class RegbexMatcher internal constructor(instructions: Iterable // State: private var elements = pattern.regbex.elements private var target: List = ArrayList() @@ -207,7 +219,7 @@ class RegbexMatcher internal constructor(instructions: Iterable } diff --git a/src/main/kotlin/me/fan87/regbex/RegbexPattern.kt b/src/main/kotlin/me/fan87/regbex/RegbexPattern.kt index db7b492..64b34a0 100644 --- a/src/main/kotlin/me/fan87/regbex/RegbexPattern.kt +++ b/src/main/kotlin/me/fan87/regbex/RegbexPattern.kt @@ -1,15 +1,28 @@ package me.fan87.regbex import org.objectweb.asm.tree.AbstractInsnNode +import org.objectweb.asm.tree.MethodNode class RegbexPattern(val regbex: Regbex) { - constructor(regbexBuilder: RegbexBuilder) : this(Regbex().apply(regbexBuilder)) + /** + * New matcher instance of an instructions list. You may assume the instruction list provided will be cloned, + * so you won't have to put it into another list to avoid changing, which also means that you could provide + * an immutable list, and it should work fine. + */ fun matcher(insnList: Iterable): RegbexMatcher { return RegbexMatcher(insnList, this) } + /** + * New matcher instance of a method's instructions + */ + fun matcher(method: MethodNode): RegbexMatcher { + return RegbexMatcher(method.instructions, this) + } + + } \ No newline at end of file diff --git a/src/main/kotlin/me/fan87/regbex/RegbexResultable.kt b/src/main/kotlin/me/fan87/regbex/RegbexResultable.kt new file mode 100644 index 0000000..a551411 --- /dev/null +++ b/src/main/kotlin/me/fan87/regbex/RegbexResultable.kt @@ -0,0 +1,64 @@ +package me.fan87.regbex + +import org.objectweb.asm.tree.AbstractInsnNode + +interface RegbexResultable { + + /** + * End index of the match (Exclusive) + */ + fun endIndex(): Int + + /** + * Start index of the match (Inclusive) + */ + fun startIndex(): Int + + /** + * Number of instructions that were matched + */ + fun matchedSize(): Int + + /** + * Get matched instructions + */ + fun group(): List + + /** + * Get content of captured named group. Returns null if the group is not found or not captured + */ + fun group(name: String): List? + + /** + * Get the pattern of the result + */ + fun pattern(): RegbexPattern + + /** + * Get the start of a group (Inclusive) + */ + fun groupStart(groupName: String): Int? + + /** + * Get the end of a group (Exclusive) + */ + fun groupEnd(groupName: String): Int? + + /** + * Replace the matched result with [replaceTo], and return the replaced list of instructions + */ + fun replace(replaceTo: Iterable): ArrayList + + /** + * Replace captured named group with name [groupName], and return the replaced list of instructions + */ + fun replaceGroup(groupName: String, replaceTo: Iterable): ArrayList + + /** + * Get matched instructions + */ + fun matched(): List { + return group() + } + +} \ No newline at end of file diff --git a/src/test/kotlin/AndTest.kt b/src/test/kotlin/AndTest.kt index ca31d07..37141fd 100644 --- a/src/test/kotlin/AndTest.kt +++ b/src/test/kotlin/AndTest.kt @@ -9,7 +9,6 @@ import org.objectweb.asm.tree.LdcInsnNode import org.objectweb.asm.tree.MethodInsnNode import java.io.PrintStream import kotlin.test.assertEquals -import kotlin.test.assertFalse import kotlin.test.assertTrue class AndTest { @@ -32,7 +31,7 @@ class AndTest { thenVirtualMethodCallIgnoreArgs(TypeExp(PrintStream::class.java), Regex.fromLiteral("println"), TypeExp(PrimitiveType.VOID)) }.matcher(instructions) - assertTrue(matcher.nextOnlyOne(0)) + assertTrue(matcher.next(0)) assertEquals("Hello, World!", (matcher.group("printedMessage")!![0] as LdcInsnNode).cst) assertEquals(matcher.group().size, 3) } diff --git a/src/test/kotlin/StartEndTest.kt b/src/test/kotlin/StartEndTest.kt index ef6292d..f81266e 100644 --- a/src/test/kotlin/StartEndTest.kt +++ b/src/test/kotlin/StartEndTest.kt @@ -32,7 +32,7 @@ class StartEndTest { thenEndOfInstructions() }.matcher(instructions) - assertTrue(matcher.nextOnlyOne(0)) + assertTrue(matcher.next(0)) assertEquals("Hello, World!", (matcher.group("printedMessage")!![0] as LdcInsnNode).cst) assertEquals(matcher.group().size, 3) }