mirror of
https://github.com/ReVanced/revanced-patcher.git
synced 2026-01-27 13:11:03 +00:00
feat: migrate to dexlib
BREAKING CHANGE: Removed usage of ASM library
This commit is contained in:
@@ -1,177 +0,0 @@
|
||||
package app.revanced.patcher
|
||||
|
||||
import app.revanced.patcher.cache.Cache
|
||||
import app.revanced.patcher.patch.Patch
|
||||
import app.revanced.patcher.patch.PatchResult
|
||||
import app.revanced.patcher.patch.PatchResultSuccess
|
||||
import app.revanced.patcher.signature.Signature
|
||||
import app.revanced.patcher.util.ExtraTypes
|
||||
import app.revanced.patcher.util.TestUtil
|
||||
import app.revanced.patcher.writer.ASMWriter.insertAt
|
||||
import app.revanced.patcher.writer.ASMWriter.setAt
|
||||
import org.junit.jupiter.api.assertDoesNotThrow
|
||||
import org.objectweb.asm.Opcodes.*
|
||||
import org.objectweb.asm.Type
|
||||
import org.objectweb.asm.tree.FieldInsnNode
|
||||
import org.objectweb.asm.tree.LdcInsnNode
|
||||
import org.objectweb.asm.tree.MethodInsnNode
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.PrintStream
|
||||
import kotlin.test.Test
|
||||
|
||||
internal class PatcherTest {
|
||||
companion object {
|
||||
val testSignatures: Array<Signature> = arrayOf(
|
||||
// Java:
|
||||
// public static void main(String[] args) {
|
||||
// System.out.println("Hello, world!");
|
||||
// }
|
||||
// Bytecode:
|
||||
// public static main(java.lang.String[] arg0) { // Method signature: ([Ljava/lang/String;)V
|
||||
// getstatic java/lang/System.out:java.io.PrintStream
|
||||
// ldc "Hello, world!" (java.lang.String)
|
||||
// invokevirtual java/io/PrintStream.println(Ljava/lang/String;)V
|
||||
// return
|
||||
// }
|
||||
Signature(
|
||||
"mainMethod",
|
||||
Type.VOID_TYPE,
|
||||
ACC_PUBLIC or ACC_STATIC,
|
||||
arrayOf(ExtraTypes.ArrayAny),
|
||||
intArrayOf(
|
||||
GETSTATIC,
|
||||
LDC,
|
||||
INVOKEVIRTUAL,
|
||||
RETURN
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPatcher() {
|
||||
val patcher = Patcher(
|
||||
PatcherTest::class.java.getResourceAsStream("/test1.jar")!!,
|
||||
ByteArrayOutputStream(),
|
||||
testSignatures
|
||||
)
|
||||
|
||||
patcher.addPatches(
|
||||
object : Patch("TestPatch") {
|
||||
override fun execute(cache: Cache): PatchResult {
|
||||
// Get the method from the resolver cache
|
||||
val mainMethod = patcher.cache.methods["mainMethod"]
|
||||
// Get the instruction list
|
||||
val instructions = mainMethod.method.instructions!!
|
||||
|
||||
// Let's modify it, so it prints "Hello, ReVanced! Editing bytecode."
|
||||
// Get the start index of our opcode pattern.
|
||||
// This will be the index of the LDC instruction.
|
||||
val startIndex = mainMethod.scanData.startIndex
|
||||
|
||||
// Ignore this, just testing if the method resolver works :)
|
||||
TestUtil.assertNodeEqual(
|
||||
FieldInsnNode(
|
||||
GETSTATIC,
|
||||
Type.getInternalName(System::class.java),
|
||||
"out",
|
||||
// for whatever reason, it adds an "L" and ";" to the node string
|
||||
"L${Type.getInternalName(PrintStream::class.java)};"
|
||||
),
|
||||
instructions[startIndex]!!
|
||||
)
|
||||
|
||||
// Create a new LDC node and replace the LDC instruction.
|
||||
val stringNode = LdcInsnNode("Hello, ReVanced! Editing bytecode.")
|
||||
instructions.setAt(startIndex, stringNode)
|
||||
|
||||
// Now lets print our string twice!
|
||||
// Insert our instructions after the second instruction by our pattern.
|
||||
// This will place our instructions after the original INVOKEVIRTUAL call.
|
||||
// You could also copy the instructions from the list and then modify the LDC instruction again,
|
||||
// but this is to show a more advanced example of writing bytecode using the patcher and ASM.
|
||||
instructions.insertAt(
|
||||
startIndex + 1,
|
||||
FieldInsnNode(
|
||||
GETSTATIC,
|
||||
Type.getInternalName(System::class.java), // "java/lang/System"
|
||||
"out",
|
||||
Type.getInternalName(PrintStream::class.java) // "java/io/PrintStream"
|
||||
),
|
||||
LdcInsnNode("Hello, ReVanced! Adding bytecode."),
|
||||
MethodInsnNode(
|
||||
INVOKEVIRTUAL,
|
||||
Type.getInternalName(PrintStream::class.java), // "java/io/PrintStream"
|
||||
"println",
|
||||
Type.getMethodDescriptor(
|
||||
Type.VOID_TYPE,
|
||||
Type.getType(String::class.java)
|
||||
) // "(Ljava/lang/String;)V"
|
||||
)
|
||||
)
|
||||
|
||||
// Our code now looks like this:
|
||||
// public static main(java.lang.String[] arg0) { // Method signature: ([Ljava/lang/String;)V
|
||||
// getstatic java/lang/System.out:java.io.PrintStream
|
||||
// ldc "Hello, ReVanced! Editing bytecode." (java.lang.String) // We overwrote this instruction.
|
||||
// invokevirtual java/io/PrintStream.println(Ljava/lang/String;)V
|
||||
// getstatic java/lang/System.out:java.io.PrintStream // This instruction and the 2 instructions below are written manually.
|
||||
// ldc "Hello, ReVanced! Adding bytecode." (java.lang.String)
|
||||
// invokevirtual java/io/PrintStream.println(Ljava/lang/String;)V
|
||||
// return
|
||||
// }
|
||||
|
||||
// Finally, tell the patcher that this patch was a success.
|
||||
// You can also return PatchResultError with a message.
|
||||
// If an exception is thrown inside this function,
|
||||
// a PatchResultError will be returned with the error message.
|
||||
return PatchResultSuccess()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Apply all patches loaded in the patcher
|
||||
val patchResult = patcher.applyPatches()
|
||||
// You can check if an error occurred
|
||||
for ((patchName, result) in patchResult) {
|
||||
if (result.isFailure) {
|
||||
throw Exception("Patch $patchName failed", result.exceptionOrNull()!!)
|
||||
}
|
||||
}
|
||||
|
||||
patcher.save()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test patcher with no changes`() {
|
||||
val testData = PatcherTest::class.java.getResourceAsStream("/test1.jar")!!
|
||||
// val available = testData.available()
|
||||
val out = ByteArrayOutputStream()
|
||||
Patcher(testData, out, testSignatures).save()
|
||||
// FIXME(Sculas): There seems to be a 1-byte difference, not sure what it is.
|
||||
// assertEquals(available, out.size())
|
||||
out.close()
|
||||
}
|
||||
|
||||
@Test()
|
||||
fun `should not raise an exception if any signature member except the name is missing`() {
|
||||
val sigName = "testMethod"
|
||||
|
||||
assertDoesNotThrow(
|
||||
"Should not raise an exception if any signature member except the name is missing"
|
||||
) {
|
||||
Patcher(
|
||||
PatcherTest::class.java.getResourceAsStream("/test1.jar")!!,
|
||||
ByteArrayOutputStream(),
|
||||
arrayOf(
|
||||
Signature(
|
||||
sigName,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package app.revanced.patcher
|
||||
|
||||
import java.io.ByteArrayOutputStream
|
||||
import kotlin.test.Test
|
||||
|
||||
internal class ReaderTest {
|
||||
@Test
|
||||
fun `read jar containing multiple classes`() {
|
||||
val testData = PatcherTest::class.java.getResourceAsStream("/test2.jar")!!
|
||||
Patcher(testData, ByteArrayOutputStream(), PatcherTest.testSignatures).save() // reusing test sigs from PatcherTest
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package app.revanced.patcher.util
|
||||
|
||||
import org.objectweb.asm.tree.AbstractInsnNode
|
||||
import org.objectweb.asm.tree.FieldInsnNode
|
||||
import org.objectweb.asm.tree.LdcInsnNode
|
||||
import kotlin.test.fail
|
||||
|
||||
object TestUtil {
|
||||
fun <T: AbstractInsnNode> assertNodeEqual(expected: T, actual: T) {
|
||||
val a = expected.nodeString()
|
||||
val b = actual.nodeString()
|
||||
if (a != b) {
|
||||
fail("expected: $a,\nactual: $b\n")
|
||||
}
|
||||
}
|
||||
|
||||
private fun AbstractInsnNode.nodeString(): String {
|
||||
val sb = NodeStringBuilder()
|
||||
when (this) {
|
||||
// TODO(Sculas): Add more types
|
||||
is LdcInsnNode -> sb
|
||||
.addType("cst", cst)
|
||||
is FieldInsnNode -> sb
|
||||
.addType("owner", owner)
|
||||
.addType("name", name)
|
||||
.addType("desc", desc)
|
||||
}
|
||||
return "(${this::class.simpleName}): (type = $type, opcode = $opcode, $sb)"
|
||||
}
|
||||
}
|
||||
|
||||
private class NodeStringBuilder {
|
||||
private val sb = StringBuilder()
|
||||
|
||||
fun addType(name: String, value: Any): NodeStringBuilder {
|
||||
sb.append("$name = \"$value\", ")
|
||||
return this
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
if (sb.isEmpty()) return ""
|
||||
val s = sb.toString()
|
||||
return s.substring(0 .. (s.length - 2).coerceAtLeast(0)) // remove the last ", "
|
||||
}
|
||||
}
|
||||
75
src/test/kotlin/patcher/PatcherTest.kt
Normal file
75
src/test/kotlin/patcher/PatcherTest.kt
Normal file
@@ -0,0 +1,75 @@
|
||||
package app.revanced.patcher
|
||||
|
||||
import app.revanced.patcher.cache.Cache
|
||||
import app.revanced.patcher.patch.Patch
|
||||
import app.revanced.patcher.patch.PatchResult
|
||||
import app.revanced.patcher.patch.PatchResultError
|
||||
import app.revanced.patcher.patch.PatchResultSuccess
|
||||
import app.revanced.patcher.signature.MethodSignature
|
||||
import org.jf.dexlib2.AccessFlags
|
||||
import org.jf.dexlib2.Opcode
|
||||
import org.jf.dexlib2.builder.instruction.BuilderInstruction21c
|
||||
import org.jf.dexlib2.iface.instruction.formats.Instruction21c
|
||||
import org.jf.dexlib2.iface.reference.FieldReference
|
||||
import org.jf.dexlib2.immutable.reference.ImmutableMethodReference
|
||||
import java.io.File
|
||||
|
||||
|
||||
fun main() {
|
||||
val signatures = arrayOf(
|
||||
MethodSignature(
|
||||
"main-method",
|
||||
"V",
|
||||
AccessFlags.STATIC.value or AccessFlags.PUBLIC.value,
|
||||
listOf("[O"),
|
||||
arrayOf(
|
||||
Opcode.SGET_OBJECT,
|
||||
Opcode.CONST_STRING,
|
||||
Opcode.INVOKE_VIRTUAL,
|
||||
Opcode.RETURN_VOID
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val patcher = Patcher(
|
||||
File("black.apk"),
|
||||
File("folder/"),
|
||||
signatures
|
||||
)
|
||||
|
||||
val mainMethodPatchViaClassProxy = object : Patch("main-method-patch-via-proxy") {
|
||||
override fun execute(cache: Cache): PatchResult {
|
||||
val proxy = cache.findClass { classDef ->
|
||||
classDef.methods.any { method ->
|
||||
method.name == "main"
|
||||
}
|
||||
} ?: return PatchResultError("Class with method 'mainMethod' could not be found")
|
||||
|
||||
val mainMethodClass = proxy.resolve()
|
||||
val mainMethod = mainMethodClass.methods.single { method -> method.name == "main" }
|
||||
|
||||
val hideReelMethodRef = ImmutableMethodReference(
|
||||
"Lfi/razerman/youtube/XAdRemover;",
|
||||
"HideReel",
|
||||
listOf("Landroid/view/View;"),
|
||||
"V"
|
||||
)
|
||||
|
||||
val mainMethodInstructions = mainMethod.implementation!!.instructions
|
||||
val printStreamFieldRef = (mainMethodInstructions.first() as Instruction21c).reference as FieldReference
|
||||
// TODO: not sure how to use the registers yet, find a way
|
||||
mainMethodInstructions.add(BuilderInstruction21c(Opcode.SGET_OBJECT, 0, printStreamFieldRef))
|
||||
return PatchResultSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
val mainMethodPatchViaSignature = object : Patch("main-method-patch-via-signature") {
|
||||
override fun execute(cache: Cache): PatchResult {
|
||||
cache.resolvedMethods["main-method"].method
|
||||
return PatchResultSuccess()
|
||||
}
|
||||
}
|
||||
patcher.addPatches(mainMethodPatchViaClassProxy, mainMethodPatchViaSignature)
|
||||
patcher.applyPatches()
|
||||
patcher.save()
|
||||
}
|
||||
Reference in New Issue
Block a user