refactor: Move ReVanced Patcher to sub-project

This allows other sub-projects to exist.
This commit is contained in:
oSumAtrIX
2023-09-04 05:37:13 +02:00
parent 3b4db3ddb7
commit 4dd04975d9
75 changed files with 72 additions and 100 deletions

View File

@@ -1,233 +0,0 @@
package app.revanced.patcher.extensions
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction
import app.revanced.patcher.extensions.InstructionExtensions.removeInstructions
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstructions
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
import app.revanced.patcher.util.smali.ExternalLabel
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.builder.BuilderOffsetInstruction
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21s
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
private object InstructionExtensionsTest {
private lateinit var testMethod: MutableMethod
private lateinit var testMethodImplementation: MutableMethodImplementation
@BeforeEach
fun createTestMethod() = ImmutableMethod(
"TestClass;",
"testMethod",
null,
"V",
AccessFlags.PUBLIC.value,
null,
null,
MutableMethodImplementation(16).also { testMethodImplementation = it }.apply {
repeat(10) { i -> this.addInstruction(TestInstruction(i)) }
},
).let { testMethod = it.toMutable() }
@Test
fun addInstructionsToImplementationIndexed() = applyToImplementation {
addInstructions(5, getTestInstructions(5..6)).also {
assertRegisterIs(5, 5)
assertRegisterIs(6, 6)
assertRegisterIs(5, 7)
}
}
@Test
fun addInstructionsToImplementation() = applyToImplementation {
addInstructions(getTestInstructions(10..11)).also {
assertRegisterIs(10, 10)
assertRegisterIs(11, 11)
}
}
@Test
fun removeInstructionsFromImplementationIndexed() = applyToImplementation {
removeInstructions(5, 5).also { assertRegisterIs(4, 4) }
}
@Test
fun removeInstructionsFromImplementation() = applyToImplementation {
removeInstructions(0).also { assertRegisterIs(9, 9) }
removeInstructions(1).also { assertRegisterIs(1, 0) }
removeInstructions(2).also { assertRegisterIs(3, 0) }
}
@Test
fun replaceInstructionsInImplementationIndexed() = applyToImplementation {
replaceInstructions(5, getTestInstructions(0..1)).also {
assertRegisterIs(0, 5)
assertRegisterIs(1, 6)
assertRegisterIs(7, 7)
}
}
@Test
fun addInstructionToMethodIndexed() = applyToMethod {
addInstruction(5, TestInstruction(0)).also { assertRegisterIs(0, 5) }
}
@Test
fun addInstructionToMethod() = applyToMethod {
addInstruction(TestInstruction(0)).also { assertRegisterIs(0, 10) }
}
@Test
fun addSmaliInstructionToMethodIndexed() = applyToMethod {
addInstruction(5, getTestSmaliInstruction(0)).also { assertRegisterIs(0, 5) }
}
@Test
fun addSmaliInstructionToMethod() = applyToMethod {
addInstruction(getTestSmaliInstruction(0)).also { assertRegisterIs(0, 10) }
}
@Test
fun addInstructionsToMethodIndexed() = applyToMethod {
addInstructions(5, getTestInstructions(0..1)).also {
assertRegisterIs(0, 5)
assertRegisterIs(1, 6)
assertRegisterIs(5, 7)
}
}
@Test
fun addInstructionsToMethod() = applyToMethod {
addInstructions(getTestInstructions(0..1)).also {
assertRegisterIs(0, 10)
assertRegisterIs(1, 11)
assertRegisterIs(9, 9)
}
}
@Test
fun addSmaliInstructionsToMethodIndexed() = applyToMethod {
addInstructionsWithLabels(5, getTestSmaliInstructions(0..1)).also {
assertRegisterIs(0, 5)
assertRegisterIs(1, 6)
assertRegisterIs(5, 7)
}
}
@Test
fun addSmaliInstructionsToMethod() = applyToMethod {
addInstructions(getTestSmaliInstructions(0..1)).also {
assertRegisterIs(0, 10)
assertRegisterIs(1, 11)
assertRegisterIs(9, 9)
}
}
@Test
fun addSmaliInstructionsWithExternalLabelToMethodIndexed() = applyToMethod {
val label = ExternalLabel("testLabel", getInstruction(5))
addInstructionsWithLabels(
5,
getTestSmaliInstructions(0..1).plus("\n").plus("goto :${label.name}"),
label
).also {
assertRegisterIs(0, 5)
assertRegisterIs(1, 6)
assertRegisterIs(5, 8)
val gotoTarget = getInstruction<BuilderOffsetInstruction>(7)
.target.location.instruction as OneRegisterInstruction
assertEquals(5, gotoTarget.registerA)
}
}
@Test
fun removeInstructionFromMethodIndexed() = applyToMethod {
removeInstruction(5).also {
assertRegisterIs(4, 4)
assertRegisterIs(6, 5)
}
}
@Test
fun removeInstructionsFromMethodIndexed() = applyToMethod {
removeInstructions(5, 5).also { assertRegisterIs(4, 4) }
}
@Test
fun removeInstructionsFromMethod() = applyToMethod {
removeInstructions(0).also { assertRegisterIs(9, 9) }
removeInstructions(1).also { assertRegisterIs(1, 0) }
removeInstructions(2).also { assertRegisterIs(3, 0) }
}
@Test
fun replaceInstructionInMethodIndexed() = applyToMethod {
replaceInstruction(5, TestInstruction(0)).also { assertRegisterIs(0, 5) }
}
@Test
fun replaceInstructionsInMethodIndexed() = applyToMethod {
replaceInstructions(5, getTestInstructions(0..1)).also {
assertRegisterIs(0, 5)
assertRegisterIs(1, 6)
assertRegisterIs(7, 7)
}
}
@Test
fun replaceSmaliInstructionsInMethodIndexed() = applyToMethod {
replaceInstructions(5, getTestSmaliInstructions(0..1)).also {
assertRegisterIs(0, 5)
assertRegisterIs(1, 6)
assertRegisterIs(7, 7)
}
}
// region Helper methods
private fun applyToImplementation(block: MutableMethodImplementation.() -> Unit) {
testMethodImplementation.apply(block)
}
private fun applyToMethod(block: MutableMethod.() -> Unit) {
testMethod.apply(block)
}
private fun MutableMethodImplementation.assertRegisterIs(register: Int, atIndex: Int) = assertEquals(
register, getInstruction<OneRegisterInstruction>(atIndex).registerA
)
private fun MutableMethod.assertRegisterIs(register: Int, atIndex: Int) =
implementation!!.assertRegisterIs(register, atIndex)
private fun getTestInstructions(range: IntRange) = range.map { TestInstruction(it) }
private fun getTestSmaliInstruction(register: Int) = "const/16 v$register, 0"
private fun getTestSmaliInstructions(range: IntRange) = range.joinToString("\n") {
getTestSmaliInstruction(it)
}
// endregion
private class TestInstruction(register: Int) : BuilderInstruction21s(Opcode.CONST_16, register, 0)
}

View File

@@ -1,18 +0,0 @@
package app.revanced.patcher.issues
import app.revanced.patcher.patch.PatchOption
import org.junit.jupiter.api.Test
import kotlin.test.assertNull
internal class Issue98 {
companion object {
var key1: String? by PatchOption.StringOption(
"key1", null, "title", "description"
)
}
@Test
fun `should infer nullable type correctly`() {
assertNull(key1)
}
}

View File

@@ -1,109 +0,0 @@
package app.revanced.patcher.patch
import app.revanced.patcher.usage.bytecode.ExampleBytecodePatch
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals
import kotlin.test.assertNotNull
internal class PatchOptionsTest {
private val options = ExampleBytecodePatch.options
@Test
fun `should not throw an exception`() {
for (option in options) {
when (option) {
is PatchOption.StringOption -> {
option.value = "Hello World"
}
is PatchOption.BooleanOption -> {
option.value = false
}
is PatchOption.StringListOption -> {
option.value = option.options.first()
for (choice in option.options) {
assertNotNull(choice)
}
}
is PatchOption.IntListOption -> {
option.value = option.options.first()
for (choice in option.options) {
assertNotNull(choice)
}
}
}
}
val option = options.get<String>("key1")
// or: val option: String? by options["key1"]
// then you won't need `.value` every time
assertEquals("Hello World", option.value)
options["key1"] = "Hello, world!"
assertEquals("Hello, world!", option.value)
}
@Test
fun `should return a different value when changed`() {
var value: String? by options["key1"]
val current = value + "" // force a copy
value = "Hello, world!"
assertNotEquals(current, value)
}
@Test
fun `should be able to set value to null`() {
// Sadly, doing:
// > options["key2"] = null
// is not possible because Kotlin
// cannot reify the type "Nothing?".
// So we have to do this instead:
options["key2"] = null as Any?
// This is a cleaner replacement for the above:
options.nullify("key2")
}
@Test
fun `should fail because the option does not exist`() {
assertThrows<NoSuchOptionException> {
options["this option does not exist"] = 123
}
}
@Test
fun `should fail because of invalid value type when setting an option`() {
assertThrows<InvalidTypeException> {
options["key1"] = 123
}
}
@Test
fun `should fail because of invalid value type when getting an option`() {
assertThrows<InvalidTypeException> {
options.get<Int>("key1")
}
}
@Test
fun `should fail because of an illegal value`() {
assertThrows<IllegalValueException> {
options["key3"] = "this value is not an allowed option"
}
}
@Test
fun `should fail because the requirement is not met`() {
assertThrows<RequirementNotMetException> {
options.nullify("key1")
}
}
@Test
fun `should fail because getting a non-initialized option is illegal`() {
assertThrows<RequirementNotMetException> {
options["key5"].value
}
}
}

View File

@@ -1,13 +0,0 @@
package app.revanced.patcher.usage.bytecode
import app.revanced.patcher.annotation.Compatibility
import app.revanced.patcher.annotation.Package
@Compatibility(
[Package(
"com.example.examplePackage", arrayOf("0.0.1", "0.0.2")
)]
)
@Target(AnnotationTarget.CLASS)
internal annotation class ExampleBytecodeCompatibility

View File

@@ -1,190 +0,0 @@
package app.revanced.patcher.usage.bytecode
import app.revanced.patcher.annotation.Description
import app.revanced.patcher.annotation.Name
import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patcher.extensions.or
import app.revanced.patcher.patch.BytecodePatch
import app.revanced.patcher.patch.OptionsContainer
import app.revanced.patcher.patch.PatchOption
import app.revanced.patcher.patch.annotations.DependsOn
import app.revanced.patcher.patch.annotations.Patch
import app.revanced.patcher.usage.resource.annotation.ExampleResourceCompatibility
import app.revanced.patcher.usage.resource.patch.ExampleResourcePatch
import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Format
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction11x
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21c
import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction21c
import com.android.tools.smali.dexlib2.immutable.ImmutableField
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
import com.android.tools.smali.dexlib2.immutable.ImmutableMethodImplementation
import com.android.tools.smali.dexlib2.immutable.reference.ImmutableFieldReference
import com.android.tools.smali.dexlib2.immutable.reference.ImmutableStringReference
import com.android.tools.smali.dexlib2.immutable.value.ImmutableFieldEncodedValue
import com.android.tools.smali.dexlib2.util.Preconditions
import com.google.common.collect.ImmutableList
@Patch
@Name("example-bytecode-patch")
@Description("Example demonstration of a bytecode patch.")
@ExampleResourceCompatibility
@DependsOn([ExampleResourcePatch::class])
class ExampleBytecodePatch : BytecodePatch(listOf(ExampleFingerprint)) {
// This function will be executed by the patcher.
// You can treat it as a constructor
override fun execute(context: BytecodeContext) {
// Get the resolved method by its fingerprint from the resolver cache
val result = ExampleFingerprint.result!!
// Patch options
println(key1)
key2 = false
// Get the implementation for the resolved method
val method = result.mutableMethod
val implementation = method.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.scanResult.patternScanResult!!.startIndex
implementation.replaceStringAt(startIndex, "Hello, ReVanced! Editing bytecode.")
// Get the class in which the method matching our fingerprint is defined in.
val mainClass = context.findClass {
it.type == result.classDef.type
}!!.mutableClass
// Add a new method returning a string
mainClass.methods.add(
ImmutableMethod(
result.classDef.type,
"returnHello",
null,
"Ljava/lang/String;",
AccessFlags.PRIVATE or AccessFlags.STATIC,
null,
null,
ImmutableMethodImplementation(
1,
ImmutableList.of(
BuilderInstruction21c(
Opcode.CONST_STRING,
0,
ImmutableStringReference("Hello, ReVanced! Adding bytecode.")
),
BuilderInstruction11x(Opcode.RETURN_OBJECT, 0)
),
null,
null
)
).toMutable()
)
// Add a field in the main class
// We will use this field in our method below to call println on
// The field holds the Ljava/io/PrintStream->out; field
mainClass.fields.add(
ImmutableField(
mainClass.type,
"dummyField",
"Ljava/io/PrintStream;",
AccessFlags.PRIVATE or AccessFlags.STATIC,
ImmutableFieldEncodedValue(
ImmutableFieldReference(
"Ljava/lang/System;",
"out",
"Ljava/io/PrintStream;"
)
),
null,
null
).toMutable()
)
// store the fields initial value into the first virtual register
method.replaceInstruction(0, "sget-object v0, LTestClass;->dummyField:Ljava/io/PrintStream;")
// Now let's create a new call to our method and print the return value!
// You can also use the smali compiler to create instructions.
// For this sake of example I reuse the TestClass field dummyField inside the virtual register 0.
//
// Control flow instructions are not supported as of now.
method.addInstructionsWithLabels(
startIndex + 2,
"""
invoke-static { }, LTestClass;->returnHello()Ljava/lang/String;
move-result-object v1
invoke-virtual { v0, v1 }, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
"""
)
}
/**
* Replace the string for an instruction at the given index with a new one.
* @param index The index of the instruction to replace the string for
* @param string The replacing string
*/
private fun MutableMethodImplementation.replaceStringAt(index: Int, string: String) {
val instruction = this.instructions[index]
// Utility method of dexlib2
Preconditions.checkFormat(instruction.opcode, Format.Format21c)
// Cast this to an instruction of the format 21c
// The instruction format can be found in the docs at
// https://source.android.com/devices/tech/dalvik/dalvik-bytecode
val strInstruction = instruction as Instruction21c
// In our case we want an instruction with the opcode CONST_STRING
// The format is 21c, so we create a new BuilderInstruction21c
// This instruction will hold the string reference constant in the virtual register of the original instruction
// For that a reference to the string is needed. It can be created with an ImmutableStringReference.
// At last, use the method replaceInstruction to replace it at the given index startIndex.
this.replaceInstruction(
index,
BuilderInstruction21c(
Opcode.CONST_STRING,
strInstruction.registerA,
ImmutableStringReference(string)
)
)
}
@Suppress("unused")
companion object : OptionsContainer() {
private var key1 by option(
PatchOption.StringOption(
"key1", "default", "title", "description", true
)
)
private var key2 by option(
PatchOption.BooleanOption(
"key2", true, "title", "description" // required defaults to false
)
)
private var key3 by option(
PatchOption.StringListOption(
"key3", "TEST", listOf("TEST", "TEST1", "TEST2"), "title", "description"
)
)
private var key4 by option(
PatchOption.IntListOption(
"key4", 1, listOf(1, 2, 3), "title", "description"
)
)
private var key5 by option(
PatchOption.StringOption(
"key5", null, "title", "description", true
)
)
}
}

View File

@@ -1,20 +0,0 @@
package app.revanced.patcher.usage.bytecode
import app.revanced.patcher.extensions.or
import app.revanced.patcher.fingerprint.method.annotation.FuzzyPatternScanMethod
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
@FuzzyPatternScanMethod(2)
object ExampleFingerprint : MethodFingerprint(
"V",
AccessFlags.PUBLIC or AccessFlags.STATIC,
listOf("[L"),
listOf(
Opcode.SGET_OBJECT,
null, // Testing unknown opcodes.
Opcode.INVOKE_STATIC, // This is intentionally wrong to test the Fuzzy resolver.
Opcode.RETURN_VOID
),
null
)

View File

@@ -1,13 +0,0 @@
package app.revanced.patcher.usage.resource.annotation
import app.revanced.patcher.annotation.Compatibility
import app.revanced.patcher.annotation.Package
@Compatibility(
[Package(
"com.example.examplePackage", arrayOf("0.0.1", "0.0.2")
)]
)
@Target(AnnotationTarget.CLASS)
internal annotation class ExampleResourceCompatibility

View File

@@ -1,29 +0,0 @@
package app.revanced.patcher.usage.resource.patch
import app.revanced.patcher.annotation.Description
import app.revanced.patcher.annotation.Name
import app.revanced.patcher.data.ResourceContext
import app.revanced.patcher.patch.ResourcePatch
import app.revanced.patcher.patch.annotations.Patch
import app.revanced.patcher.usage.resource.annotation.ExampleResourceCompatibility
import org.w3c.dom.Element
@Patch
@Name("example-resource-patch")
@Description("Example demonstration of a resource patch.")
@ExampleResourceCompatibility
class ExampleResourcePatch : ResourcePatch {
override fun execute(context: ResourceContext) {
context.xmlEditor["AndroidManifest.xml"].use { editor ->
val element = editor // regular DomFileEditor
.file
.getElementsByTagName("application")
.item(0) as Element
element
.setAttribute(
"exampleAttribute",
"exampleValue"
)
}
}
}

View File

@@ -1,105 +0,0 @@
package app.revanced.patcher.util.smali
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.extensions.newLabel
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.builder.BuilderInstruction
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21c
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21t
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
import com.android.tools.smali.dexlib2.immutable.reference.ImmutableStringReference
import java.util.*
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
internal class InlineSmaliCompilerTest {
@Test
fun `compiler should output valid instruction`() {
val want = BuilderInstruction21c(Opcode.CONST_STRING, 0, ImmutableStringReference("Test")) as BuilderInstruction
val have = "const-string v0, \"Test\"".toInstruction()
instructionEquals(want, have)
}
@Test
fun `compiler should support branching with own branches`() {
val method = createMethod()
val insnAmount = 8
val insnIndex = insnAmount - 2
val targetIndex = insnIndex - 1
method.addInstructions(arrayOfNulls<String>(insnAmount).also {
Arrays.fill(it, "const/4 v0, 0x0")
}.joinToString("\n"))
method.addInstructionsWithLabels(
targetIndex,
"""
:test
const/4 v0, 0x1
if-eqz v0, :test
"""
)
val insn = method.getInstruction<BuilderInstruction21t>(insnIndex)
assertEquals(targetIndex, insn.target.location.index)
}
@Test
fun `compiler should support branching to outside branches`() {
val method = createMethod()
val insnIndex = 3
val labelIndex = 1
method.addInstructions(
"""
const/4 v0, 0x1
const/4 v0, 0x0
"""
)
assertEquals(labelIndex, method.newLabel(labelIndex).location.index)
method.addInstructionsWithLabels(
method.implementation!!.instructions.size,
"""
const/4 v0, 0x1
if-eqz v0, :test
return-void
""",
ExternalLabel("test", method.getInstruction(1))
)
val insn = method.getInstruction<BuilderInstruction21t>(insnIndex)
assertTrue(insn.target.isPlaced, "Label was not placed")
assertEquals(labelIndex, insn.target.location.index)
}
companion object {
private fun createMethod(
name: String = "dummy",
returnType: String = "V",
accessFlags: Int = AccessFlags.STATIC.value,
registerCount: Int = 1,
) = ImmutableMethod(
"Ldummy;",
name,
emptyList(), // parameters
returnType,
accessFlags,
emptySet(),
emptySet(),
MutableMethodImplementation(registerCount)
).toMutable()
private fun instructionEquals(want: BuilderInstruction, have: BuilderInstruction) {
assertEquals(want.opcode, have.opcode)
assertEquals(want.format, have.format)
assertEquals(want.codeUnits, have.codeUnits)
}
}
}