mirror of
https://github.com/ReVanced/revanced-patcher.git
synced 2024-11-10 09:08:04 +01:00
feat: Add first tests
This commit is contained in:
parent
4543b36616
commit
6767c8fbc1
5 changed files with 139 additions and 126 deletions
|
@ -29,7 +29,7 @@ class MutableClass(classDef: ClassDef) : ClassDef, BaseTypeReference() {
|
||||||
// Fields
|
// Fields
|
||||||
private val _fields by lazy { classDef.fields.map { field -> field.toMutable() }.toMutableSet() }
|
private val _fields by lazy { classDef.fields.map { field -> field.toMutable() }.toMutableSet() }
|
||||||
private val _staticFields by lazy { Iterables.filter(_fields, FieldUtil.FIELD_IS_STATIC).toMutableSet() }
|
private val _staticFields by lazy { Iterables.filter(_fields, FieldUtil.FIELD_IS_STATIC).toMutableSet() }
|
||||||
private val _instanceFields by lazy {Iterables.filter(_fields, FieldUtil.FIELD_IS_INSTANCE) .toMutableSet() }
|
private val _instanceFields by lazy { Iterables.filter(_fields, FieldUtil.FIELD_IS_INSTANCE).toMutableSet() }
|
||||||
|
|
||||||
fun setType(type: String) {
|
fun setType(type: String) {
|
||||||
this.type = type
|
this.type = type
|
||||||
|
|
|
@ -31,8 +31,8 @@ internal class SignatureResolver(
|
||||||
val classProxy = ClassProxy(classDef, index)
|
val classProxy = ClassProxy(classDef, index)
|
||||||
methodMap[signature.name] = SignatureResolverResult(
|
methodMap[signature.name] = SignatureResolverResult(
|
||||||
classProxy,
|
classProxy,
|
||||||
|
patternScanData,
|
||||||
method.name,
|
method.name,
|
||||||
patternScanData
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,8 +53,8 @@ internal class SignatureResolver(
|
||||||
val result = compareSignatureToMethod(signature, method) ?: continue
|
val result = compareSignatureToMethod(signature, method) ?: continue
|
||||||
return SignatureResolverResult(
|
return SignatureResolverResult(
|
||||||
classProxy,
|
classProxy,
|
||||||
|
result,
|
||||||
method.name,
|
method.name,
|
||||||
result
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package app.revanced.patcher.signature
|
package app.revanced.patcher.signature
|
||||||
|
|
||||||
import app.revanced.patcher.proxy.ClassProxy
|
import app.revanced.patcher.proxy.ClassProxy
|
||||||
|
import app.revanced.patcher.proxy.mutableTypes.MutableMethod
|
||||||
import app.revanced.patcher.resolver.SignatureResolver
|
import app.revanced.patcher.resolver.SignatureResolver
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -11,9 +12,14 @@ import app.revanced.patcher.resolver.SignatureResolver
|
||||||
*/
|
*/
|
||||||
data class SignatureResolverResult(
|
data class SignatureResolverResult(
|
||||||
val definingClassProxy: ClassProxy,
|
val definingClassProxy: ClassProxy,
|
||||||
val resolvedMethodName: String,
|
val scanData: PatternScanResult,
|
||||||
val scanData: PatternScanResult?
|
private val resolvedMethodName: String,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
fun resolveAndGetMethod(): MutableMethod {
|
||||||
|
return definingClassProxy.resolve().methods.single { it.name == resolvedMethodName }
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("Unused") // TODO(Sculas): remove this when we have coverage for this method.
|
@Suppress("Unused") // TODO(Sculas): remove this when we have coverage for this method.
|
||||||
fun findParentMethod(signature: MethodSignature): SignatureResolverResult? {
|
fun findParentMethod(signature: MethodSignature): SignatureResolverResult? {
|
||||||
return SignatureResolver.resolveFromProxy(definingClassProxy, signature)
|
return SignatureResolver.resolveFromProxy(definingClassProxy, signature)
|
||||||
|
|
128
src/test/kotlin/app/revanced/patcher/PatcherTest.kt
Normal file
128
src/test/kotlin/app/revanced/patcher/PatcherTest.kt
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
package app.revanced.patcher
|
||||||
|
|
||||||
|
import app.revanced.patcher.cache.Cache
|
||||||
|
import app.revanced.patcher.extensions.or
|
||||||
|
import app.revanced.patcher.patch.Patch
|
||||||
|
import app.revanced.patcher.patch.PatchResult
|
||||||
|
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.builder.instruction.BuilderInstruction35c
|
||||||
|
import org.jf.dexlib2.immutable.reference.ImmutableMethodReference
|
||||||
|
import org.jf.dexlib2.immutable.reference.ImmutableStringReference
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
internal class PatcherTest {
|
||||||
|
companion object {
|
||||||
|
val testSignatures: Array<MethodSignature> = arrayOf(
|
||||||
|
MethodSignature(
|
||||||
|
"main-method",
|
||||||
|
"V",
|
||||||
|
AccessFlags.PUBLIC or AccessFlags.STATIC,
|
||||||
|
setOf("[L"),
|
||||||
|
arrayOf(
|
||||||
|
Opcode.CONST_STRING,
|
||||||
|
Opcode.INVOKE_VIRTUAL,
|
||||||
|
Opcode.RETURN_VOID
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testPatcher() {
|
||||||
|
val patcher = Patcher(
|
||||||
|
File(PatcherTest::class.java.getResource("/test1.apk")!!.toURI()),
|
||||||
|
File("/"),
|
||||||
|
testSignatures
|
||||||
|
)
|
||||||
|
|
||||||
|
patcher.addPatches(
|
||||||
|
object : Patch("TestPatch") {
|
||||||
|
override fun execute(cache: Cache): PatchResult {
|
||||||
|
// Get the result from the resolver cache
|
||||||
|
val result = cache.resolvedMethods["main-method"]
|
||||||
|
// Get the implementation for the resolved method
|
||||||
|
val implementation = result.resolveAndGetMethod().implementation!!
|
||||||
|
// 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 instruction with the opcode CONST_STRING.
|
||||||
|
val startIndex = result.scanData.startIndex
|
||||||
|
|
||||||
|
// the instruction format can be found via the docs at https://source.android.com/devices/tech/dalvik/dalvik-bytecode
|
||||||
|
// in our case we want an instruction with the opcode CONST_STRING and the string "Hello, ReVanced! Editing bytecode."
|
||||||
|
// the format is 21c, so we create a new BuilderInstruction21c
|
||||||
|
// with the opcode CONST_STRING and the string "Hello, ReVanced! Editing bytecode."
|
||||||
|
// This instruction will store the constant string reference in the register v1
|
||||||
|
// For that a reference to the string is needed. It can be created by creating a ImmutableStringReference
|
||||||
|
val stringInstruction1 = BuilderInstruction21c(
|
||||||
|
Opcode.CONST_STRING,
|
||||||
|
1,
|
||||||
|
ImmutableStringReference("Hello, ReVanced! Editing bytecode.")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Replace the instruction at index startIndex with a new instruction
|
||||||
|
// We make sure to use this method to handle references to it via labels in any case
|
||||||
|
// If we are sure that the instruction is not referenced by any label, we can use the index operator overload
|
||||||
|
// of the instruction list:
|
||||||
|
// implementation.instructions[startIndex] = instruction
|
||||||
|
implementation.replaceInstruction(startIndex, stringInstruction1)
|
||||||
|
|
||||||
|
// Now lets print our string twice!
|
||||||
|
|
||||||
|
// Create the necessary instructions (we could also clone the existing ones)
|
||||||
|
val stringInstruction2 = BuilderInstruction21c(
|
||||||
|
Opcode.CONST_STRING,
|
||||||
|
1,
|
||||||
|
ImmutableStringReference("Hello, ReVanced! Adding bytecode.")
|
||||||
|
)
|
||||||
|
val invokeInstruction = BuilderInstruction35c(
|
||||||
|
Opcode.INVOKE_VIRTUAL,
|
||||||
|
2, 0, 1, 0, 0, 0,
|
||||||
|
ImmutableMethodReference(
|
||||||
|
"Ljava.io.PrintStream;",
|
||||||
|
"println",
|
||||||
|
setOf("Ljava/lang/String;"),
|
||||||
|
"V"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Insert our instructions after the second instruction by our pattern.
|
||||||
|
implementation.addInstruction(startIndex + 1, stringInstruction2)
|
||||||
|
implementation.addInstruction(startIndex + 3, invokeInstruction)
|
||||||
|
|
||||||
|
// 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`() {
|
||||||
|
Patcher(
|
||||||
|
File(PatcherTest::class.java.getResource("/test1.apk")!!.toURI()),
|
||||||
|
File("/no-changes-test"),
|
||||||
|
testSignatures
|
||||||
|
).save()
|
||||||
|
// FIXME(Sculas): There seems to be a 1-byte difference, not sure what it is.
|
||||||
|
// assertEquals(available, out.size())
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,121 +0,0 @@
|
||||||
package patcher
|
|
||||||
|
|
||||||
import app.revanced.patcher.Patcher
|
|
||||||
import app.revanced.patcher.cache.Cache
|
|
||||||
import app.revanced.patcher.extensions.or
|
|
||||||
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 app.revanced.patcher.smali.asInstruction
|
|
||||||
import org.jf.dexlib2.AccessFlags
|
|
||||||
import org.jf.dexlib2.Opcode
|
|
||||||
import org.jf.dexlib2.builder.instruction.BuilderInstruction21t
|
|
||||||
import org.jf.dexlib2.builder.instruction.BuilderInstruction35c
|
|
||||||
import org.jf.dexlib2.iface.reference.MethodReference
|
|
||||||
import org.jf.dexlib2.immutable.reference.ImmutableMethodReference
|
|
||||||
import java.io.File
|
|
||||||
import kotlin.test.assertContentEquals
|
|
||||||
import kotlin.test.assertEquals
|
|
||||||
|
|
||||||
fun main() {
|
|
||||||
val signatures = arrayOf(
|
|
||||||
MethodSignature(
|
|
||||||
"main-method",
|
|
||||||
"V",
|
|
||||||
AccessFlags.STATIC or AccessFlags.PUBLIC,
|
|
||||||
listOf("[O"),
|
|
||||||
arrayOf(
|
|
||||||
Opcode.SGET_OBJECT,
|
|
||||||
Opcode.CONST_STRING,
|
|
||||||
Opcode.INVOKE_VIRTUAL,
|
|
||||||
Opcode.RETURN_VOID
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val patcher = Patcher(
|
|
||||||
File("black.apk"),
|
|
||||||
File("./"),
|
|
||||||
signatures
|
|
||||||
)
|
|
||||||
|
|
||||||
patcher.addPatches(
|
|
||||||
object : Patch("main-method-patch-via-proxy") {
|
|
||||||
override fun execute(cache: Cache): PatchResult {
|
|
||||||
val proxy = cache.findClass("XAdRemover")
|
|
||||||
?: return PatchResultError("Class 'XAdRemover' could not be found")
|
|
||||||
|
|
||||||
val xAdRemoverClass = proxy.resolve()
|
|
||||||
val hideReelMethod = xAdRemoverClass.methods.find {
|
|
||||||
it.name.contains("HideReel")
|
|
||||||
}!!
|
|
||||||
|
|
||||||
val implementation = hideReelMethod.implementation!!
|
|
||||||
|
|
||||||
val readSettingsInstructionCompiled =
|
|
||||||
"invoke-static { }, Lfi/razerman/youtube/XGlobals;->ReadSettings()V"
|
|
||||||
.asInstruction() as BuilderInstruction35c
|
|
||||||
val readSettingsInstructionAssembled = BuilderInstruction35c(
|
|
||||||
Opcode.INVOKE_STATIC,
|
|
||||||
0, 0, 0, 0, 0, 0,
|
|
||||||
ImmutableMethodReference(
|
|
||||||
"Lfi/razerman/youtube/XGlobals;",
|
|
||||||
"ReadSettings",
|
|
||||||
emptyList(),
|
|
||||||
"V"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
assertEquals(readSettingsInstructionAssembled.opcode, readSettingsInstructionCompiled.opcode)
|
|
||||||
assertEquals(
|
|
||||||
readSettingsInstructionAssembled.referenceType,
|
|
||||||
readSettingsInstructionCompiled.referenceType
|
|
||||||
)
|
|
||||||
assertEquals(
|
|
||||||
readSettingsInstructionAssembled.registerCount,
|
|
||||||
readSettingsInstructionCompiled.registerCount
|
|
||||||
)
|
|
||||||
assertEquals(readSettingsInstructionAssembled.registerC, readSettingsInstructionCompiled.registerC)
|
|
||||||
assertEquals(readSettingsInstructionAssembled.registerD, readSettingsInstructionCompiled.registerD)
|
|
||||||
assertEquals(readSettingsInstructionAssembled.registerE, readSettingsInstructionCompiled.registerE)
|
|
||||||
assertEquals(readSettingsInstructionAssembled.registerF, readSettingsInstructionCompiled.registerF)
|
|
||||||
assertEquals(readSettingsInstructionAssembled.registerG, readSettingsInstructionCompiled.registerG)
|
|
||||||
run {
|
|
||||||
val compiledRef = readSettingsInstructionCompiled.reference as MethodReference
|
|
||||||
val assembledRef = readSettingsInstructionAssembled.reference as MethodReference
|
|
||||||
|
|
||||||
assertEquals(assembledRef.name, compiledRef.name)
|
|
||||||
assertEquals(assembledRef.definingClass, compiledRef.definingClass)
|
|
||||||
assertEquals(assembledRef.returnType, compiledRef.returnType)
|
|
||||||
assertContentEquals(assembledRef.parameterTypes, compiledRef.parameterTypes)
|
|
||||||
}
|
|
||||||
|
|
||||||
implementation.addInstruction(
|
|
||||||
21,
|
|
||||||
readSettingsInstructionCompiled
|
|
||||||
)
|
|
||||||
|
|
||||||
// fix labels
|
|
||||||
// create a new label for the instruction we want to jump to
|
|
||||||
val newLabel = implementation.newLabelForIndex(21)
|
|
||||||
// replace all instances of the old label with the new one
|
|
||||||
implementation.replaceInstruction(4, BuilderInstruction21t(Opcode.IF_NEZ, 0, newLabel))
|
|
||||||
return PatchResultSuccess()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
object : Patch("main-method-patch-via-signature") {
|
|
||||||
override fun execute(cache: Cache): PatchResult {
|
|
||||||
val mainMethodMap = cache.resolvedMethods["main-method"]
|
|
||||||
mainMethodMap.definingClassProxy.immutableClass.methods.single { method ->
|
|
||||||
method.name == mainMethodMap.resolvedMethodName
|
|
||||||
}
|
|
||||||
return PatchResultSuccess()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
patcher.applyPatches()
|
|
||||||
patcher.save()
|
|
||||||
}
|
|
Loading…
Reference in a new issue