mirror of
https://github.com/ReVanced/revanced-patcher.git
synced 2024-11-10 01:02:22 +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
|
||||
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 _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) {
|
||||
this.type = type
|
||||
|
|
|
@ -31,8 +31,8 @@ internal class SignatureResolver(
|
|||
val classProxy = ClassProxy(classDef, index)
|
||||
methodMap[signature.name] = SignatureResolverResult(
|
||||
classProxy,
|
||||
patternScanData,
|
||||
method.name,
|
||||
patternScanData
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -53,8 +53,8 @@ internal class SignatureResolver(
|
|||
val result = compareSignatureToMethod(signature, method) ?: continue
|
||||
return SignatureResolverResult(
|
||||
classProxy,
|
||||
result,
|
||||
method.name,
|
||||
result
|
||||
)
|
||||
}
|
||||
return null
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package app.revanced.patcher.signature
|
||||
|
||||
import app.revanced.patcher.proxy.ClassProxy
|
||||
import app.revanced.patcher.proxy.mutableTypes.MutableMethod
|
||||
import app.revanced.patcher.resolver.SignatureResolver
|
||||
|
||||
/**
|
||||
|
@ -11,9 +12,14 @@ import app.revanced.patcher.resolver.SignatureResolver
|
|||
*/
|
||||
data class SignatureResolverResult(
|
||||
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.
|
||||
fun findParentMethod(signature: MethodSignature): SignatureResolverResult? {
|
||||
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