feat: migrate to dexlib

BREAKING CHANGE: Removed usage of ASM library
This commit is contained in:
oSumAtrIX 2022-03-30 15:10:18 +02:00
parent fa0412985c
commit be51f42710
No known key found for this signature in database
GPG key ID: A9B3094ACDB604B4
25 changed files with 590 additions and 541 deletions

View file

@ -12,10 +12,7 @@ repositories {
dependencies {
implementation(kotlin("stdlib"))
implementation("org.ow2.asm:asm:9.2")
implementation("org.ow2.asm:asm-util:9.2")
implementation("org.ow2.asm:asm-tree:9.2")
implementation("org.ow2.asm:asm-commons:9.2")
implementation("com.github.lanchon.dexpatcher:multidexlib2:2.3.4")
implementation("io.github.microutils:kotlin-logging:2.1.21")
testImplementation("ch.qos.logback:logback-classic:1.2.11") // use your own logger!
testImplementation(kotlin("test"))

View file

@ -3,49 +3,51 @@ package app.revanced.patcher
import app.revanced.patcher.cache.Cache
import app.revanced.patcher.patch.Patch
import app.revanced.patcher.resolver.MethodResolver
import app.revanced.patcher.signature.Signature
import app.revanced.patcher.util.Io
import org.objectweb.asm.tree.ClassNode
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import app.revanced.patcher.signature.MethodSignature
import lanchon.multidexlib2.BasicDexFileNamer
import lanchon.multidexlib2.MultiDexIO
import org.jf.dexlib2.Opcodes
import org.jf.dexlib2.iface.ClassDef
import org.jf.dexlib2.iface.DexFile
import java.io.File
/**
* The Patcher class.
* ***It is of utmost importance that the input and output streams are NEVER closed.***
*
* @param input the input stream to read from, must be a JAR
* @param output the output stream to write to
* @param signatures the signatures
* @sample app.revanced.patcher.PatcherTest
* @throws IOException if one of the streams are closed
*/
class Patcher(
private val input: InputStream,
private val output: OutputStream,
signatures: Array<Signature>,
) {
var cache: Cache
input: File,
private val output: File,
signatures: Array<MethodSignature>,
private var io: Io
private val patches = mutableListOf<Patch>()
) {
private val cache: Cache
private val patches = mutableSetOf<Patch>()
init {
val classes = mutableListOf<ClassNode>()
io = Io(input, output, classes)
io.readFromJar()
cache = Cache(classes, MethodResolver(classes, signatures).resolve())
// TODO: find a way to load all dex classes, the code below only loads the first .dex file
val dexFile = MultiDexIO.readDexFile(true, input, BasicDexFileNamer(), Opcodes.getDefault(), null)
cache = Cache(dexFile.classes, MethodResolver(dexFile.classes, signatures).resolve())
}
/**
* Saves the output to the output stream.
* Calling this method will close the input and output streams,
* meaning this method should NEVER be called after.
*
* @throws IOException if one of the streams are closed
*/
fun save() {
io.saveAsJar()
val newDexFile = object : DexFile {
override fun getClasses(): MutableSet<out ClassDef> {
// TODO: find a way to return a set with a custom iterator
// TODO: the iterator would return the proxied class matching the current index of the list
// TODO: instead of the original class
for (classProxy in cache.classProxy) {
if (!classProxy.proxyused) continue
// TODO: merge this class with cache.classes somehow in an iterator
classProxy.mutatedClass
}
return cache.classes.toMutableSet()
}
override fun getOpcodes(): Opcodes {
// TODO find a way to get the opcodes format
return Opcodes.getDefault()
}
}
// TODO: not sure about maxDexPoolSize & we should use the multithreading overload for writeDexFile
MultiDexIO.writeDexFile(true, output, BasicDexFileNamer(), newDexFile, 10, null)
}
fun addPatches(vararg patches: Patch) {
@ -67,4 +69,4 @@ class Patcher(
}
}
}
}
}

View file

@ -1,14 +1,30 @@
package app.revanced.patcher.cache
import org.objectweb.asm.tree.ClassNode
import app.revanced.patcher.cache.proxy.ClassProxy
import app.revanced.patcher.signature.MethodSignatureScanResult
import org.jf.dexlib2.iface.ClassDef
class Cache(
val classes: List<ClassNode>,
val methods: MethodMap
)
internal val classes: Set<ClassDef>,
val resolvedMethods: MethodMap
) {
internal val classProxy = mutableListOf<ClassProxy>()
class MethodMap : LinkedHashMap<String, PatchData>() {
override fun get(key: String): PatchData {
fun findClass(predicate: (ClassDef) -> Boolean): ClassProxy? {
// if a class has been found with the given predicate,
val foundClass = classes.singleOrNull(predicate) ?: return null
// create a class proxy with the index of the class in the classes list
// TODO: There might be a more elegant way to the comment above
val classProxy = ClassProxy(foundClass, classes.indexOf(foundClass))
// add it to the cache and
this.classProxy.add(classProxy)
// return the proxy class
return classProxy
}
}
class MethodMap : LinkedHashMap<String, MethodSignatureScanResult>() {
override fun get(key: String): MethodSignatureScanResult {
return super.get(key) ?: throw MethodNotFoundException("Method $key was not found in the method cache")
}
}

View file

@ -1,22 +0,0 @@
package app.revanced.patcher.cache
import app.revanced.patcher.resolver.MethodResolver
import app.revanced.patcher.signature.Signature
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.MethodNode
data class PatchData(
val declaringClass: ClassNode,
val method: MethodNode,
val scanData: PatternScanData
) {
@Suppress("Unused") // TODO(Sculas): remove this when we have coverage for this method.
fun findParentMethod(signature: Signature): PatchData? {
return MethodResolver.resolveMethod(declaringClass, signature)
}
}
data class PatternScanData(
val startIndex: Int,
val endIndex: Int
)

View file

@ -0,0 +1,21 @@
package app.revanced.patcher.cache.proxy
import app.revanced.patcher.cache.proxy.mutableTypes.MutableClass
import org.jf.dexlib2.iface.ClassDef
class ClassProxy(
val immutableClass: ClassDef,
val originalClassIndex: Int,
) {
internal var proxyused = false
internal lateinit var mutatedClass: MutableClass
fun resolve(): MutableClass {
if (!proxyused) {
proxyused = true
mutatedClass = MutableClass(immutableClass)
}
return mutatedClass
}
}

View file

@ -0,0 +1,29 @@
package app.revanced.patcher.cache.proxy.mutableTypes
import app.revanced.patcher.cache.proxy.mutableTypes.MutableAnnotationElement.Companion.toMutable
import org.jf.dexlib2.base.BaseAnnotation
import org.jf.dexlib2.iface.Annotation
class MutableAnnotation(annotation: Annotation) : BaseAnnotation() {
private val visibility = annotation.visibility
private val type = annotation.type
private val elements = annotation.elements.map { element -> element.toMutable() }.toMutableSet()
override fun getType(): String {
return type
}
override fun getElements(): MutableSet<MutableAnnotationElement> {
return elements
}
override fun getVisibility(): Int {
return visibility
}
companion object {
fun Annotation.toMutable(): MutableAnnotation {
return MutableAnnotation(this)
}
}
}

View file

@ -0,0 +1,33 @@
package app.revanced.patcher.cache.proxy.mutableTypes
import app.revanced.patcher.cache.proxy.mutableTypes.MutableEncodedValue.Companion.toMutable
import org.jf.dexlib2.base.BaseAnnotationElement
import org.jf.dexlib2.iface.AnnotationElement
import org.jf.dexlib2.iface.value.EncodedValue
class MutableAnnotationElement(annotationElement: AnnotationElement) : BaseAnnotationElement() {
private var name = annotationElement.name
private var value = annotationElement.value.toMutable()
fun setName(name: String) {
this.name = name
}
fun setValue(value: MutableEncodedValue) {
this.value = value
}
override fun getName(): String {
return name
}
override fun getValue(): EncodedValue {
return value
}
companion object {
fun AnnotationElement.toMutable(): MutableAnnotationElement {
return MutableAnnotationElement(this)
}
}
}

View file

@ -0,0 +1,94 @@
package app.revanced.patcher.cache.proxy.mutableTypes
import app.revanced.patcher.cache.proxy.mutableTypes.MutableAnnotation.Companion.toMutable
import app.revanced.patcher.cache.proxy.mutableTypes.MutableField.Companion.toMutable
import app.revanced.patcher.cache.proxy.mutableTypes.MutableMethod.Companion.toMutable
import org.jf.dexlib2.base.reference.BaseTypeReference
import org.jf.dexlib2.iface.ClassDef
class MutableClass(classDef: ClassDef) : ClassDef, BaseTypeReference() {
// Class
private var type = classDef.type
private var sourceFile = classDef.sourceFile
private var accessFlags = classDef.accessFlags
private var superclass = classDef.superclass
private val interfaces = classDef.interfaces.toMutableList()
private val annotations = classDef.annotations.map { annotation -> annotation.toMutable() }.toMutableSet()
// Methods
private val methods = classDef.methods.map { method -> method.toMutable() }.toMutableSet()
private val directMethods = classDef.directMethods.map { directMethod -> directMethod.toMutable() }.toMutableSet()
private val virtualMethods =
classDef.virtualMethods.map { virtualMethod -> virtualMethod.toMutable() }.toMutableSet()
// Fields
private val fields = classDef.fields.map { field -> field.toMutable() }.toMutableSet()
private val staticFields = classDef.staticFields.map { staticField -> staticField.toMutable() }.toMutableSet()
private val instanceFields =
classDef.instanceFields.map { instanceFields -> instanceFields.toMutable() }.toMutableSet()
fun setType(type: String) {
this.type = type
}
fun setSourceFile(sourceFile: String?) {
this.sourceFile = sourceFile
}
fun setAccessFlags(accessFlags: Int) {
this.accessFlags = accessFlags
}
fun setSuperClass(superclass: String?) {
this.superclass = superclass
}
override fun getType(): String {
return type
}
override fun getAccessFlags(): Int {
return accessFlags
}
override fun getSourceFile(): String? {
return sourceFile
}
override fun getSuperclass(): String? {
return superclass
}
override fun getInterfaces(): MutableList<String> {
return interfaces
}
override fun getAnnotations(): MutableSet<MutableAnnotation> {
return annotations
}
override fun getStaticFields(): MutableSet<MutableField> {
return staticFields
}
override fun getInstanceFields(): MutableSet<MutableField> {
return instanceFields
}
override fun getFields(): MutableSet<MutableField> {
return fields
}
override fun getDirectMethods(): MutableSet<MutableMethod> {
return directMethods
}
override fun getVirtualMethods(): MutableSet<MutableMethod> {
return virtualMethods
}
override fun getMethods(): MutableSet<MutableMethod> {
return methods
}
}

View file

@ -0,0 +1,26 @@
package app.revanced.patcher.cache.proxy.mutableTypes
import org.jf.dexlib2.iface.value.EncodedValue
class MutableEncodedValue(encodedValue: EncodedValue) : EncodedValue {
private var valueType = encodedValue.valueType
fun setValueType(valueType: Int) {
this.valueType = valueType
}
override fun compareTo(other: EncodedValue): Int {
return valueType - other.valueType
}
override fun getValueType(): Int {
return valueType
}
companion object {
fun EncodedValue.toMutable(): MutableEncodedValue {
return MutableEncodedValue(this)
}
}
}

View file

@ -0,0 +1,65 @@
package app.revanced.patcher.cache.proxy.mutableTypes
import app.revanced.patcher.cache.proxy.mutableTypes.MutableAnnotation.Companion.toMutable
import app.revanced.patcher.cache.proxy.mutableTypes.MutableEncodedValue.Companion.toMutable
import org.jf.dexlib2.base.reference.BaseFieldReference
import org.jf.dexlib2.iface.Field
class MutableField(field: Field) : Field, BaseFieldReference() {
private var definingClass = field.definingClass
private var name = field.name
private var type = field.type
private var accessFlags = field.accessFlags
private var initialValue = field.initialValue?.toMutable()
private val annotations = field.annotations.map { annotation -> annotation.toMutable() }.toMutableSet()
fun setDefiningClass(definingClass: String) {
this.definingClass
}
fun setName(name: String) {
this.name = name
}
fun setType(type: String) {
this.type = type
}
fun setAccessFlags(accessFlags: Int) {
this.accessFlags = accessFlags
}
fun setInitialValue(initialValue: MutableEncodedValue?) {
this.initialValue = initialValue
}
override fun getDefiningClass(): String {
return this.definingClass
}
override fun getName(): String {
return this.name
}
override fun getType(): String {
return this.type
}
override fun getAnnotations(): MutableSet<MutableAnnotation> {
return this.annotations
}
override fun getAccessFlags(): Int {
return this.accessFlags
}
override fun getInitialValue(): MutableEncodedValue? {
return this.initialValue
}
companion object {
fun Field.toMutable(): MutableField {
return MutableField(this)
}
}
}

View file

@ -0,0 +1,58 @@
package app.revanced.patcher.cache.proxy.mutableTypes
import app.revanced.patcher.cache.proxy.mutableTypes.MutableAnnotation.Companion.toMutable
import app.revanced.patcher.cache.proxy.mutableTypes.MutableMethodParameter.Companion.toMutable
import org.jf.dexlib2.base.reference.BaseMethodReference
import org.jf.dexlib2.builder.MutableMethodImplementation
import org.jf.dexlib2.iface.Method
class MutableMethod(method: Method) : Method, BaseMethodReference() {
private var definingClass = method.definingClass
private var name = method.name
private var accessFlags = method.accessFlags
private var returnType = method.returnType
// Create own mutable MethodImplementation (due to not being able to change members like register count)
private var implementation = method.implementation?.let { MutableMethodImplementation(it) }
private val annotations = method.annotations.map { annotation -> annotation.toMutable() }.toMutableSet()
private val parameters = method.parameters.map { parameter -> parameter.toMutable() }.toMutableList()
private val parameterTypes = method.parameterTypes.toMutableList()
override fun getDefiningClass(): String {
return this.definingClass
}
override fun getName(): String {
return name
}
override fun getParameterTypes(): MutableList<CharSequence> {
return parameterTypes
}
override fun getReturnType(): String {
return returnType
}
override fun getAnnotations(): MutableSet<MutableAnnotation> {
return annotations
}
override fun getAccessFlags(): Int {
return accessFlags
}
override fun getParameters(): MutableList<MutableMethodParameter> {
return parameters
}
override fun getImplementation(): MutableMethodImplementation? {
return implementation
}
companion object {
fun Method.toMutable(): MutableMethod {
return MutableMethod(this)
}
}
}

View file

@ -0,0 +1,35 @@
package app.revanced.patcher.cache.proxy.mutableTypes
import app.revanced.patcher.cache.proxy.mutableTypes.MutableAnnotation.Companion.toMutable
import org.jf.dexlib2.base.BaseMethodParameter
import org.jf.dexlib2.iface.MethodParameter
// TODO: finish overriding all members if necessary
class MutableMethodParameter(parameter: MethodParameter) : MethodParameter, BaseMethodParameter() {
private var type = parameter.type
private var name = parameter.name
private var signature = parameter.signature
private val annotations = parameter.annotations.map { annotation -> annotation.toMutable() }.toMutableSet()
override fun getType(): String {
return type
}
override fun getName(): String? {
return name
}
override fun getSignature(): String? {
return signature
}
override fun getAnnotations(): MutableSet<MutableAnnotation> {
return annotations
}
companion object {
fun MethodParameter.toMutable(): MutableMethodParameter {
return MutableMethodParameter(this)
}
}
}

View file

@ -4,4 +4,4 @@ import app.revanced.patcher.cache.Cache
abstract class Patch(val patchName: String) {
abstract fun execute(cache: Cache): PatchResult
}
}

View file

@ -1,36 +1,31 @@
package app.revanced.patcher.resolver
import app.revanced.patcher.cache.MethodMap
import app.revanced.patcher.cache.PatchData
import app.revanced.patcher.cache.PatternScanData
import app.revanced.patcher.signature.Signature
import app.revanced.patcher.util.ExtraTypes
import mu.KotlinLogging
import org.objectweb.asm.Type
import org.objectweb.asm.tree.*
import app.revanced.patcher.signature.MethodSignatureScanResult
import app.revanced.patcher.signature.PatternScanData
import app.revanced.patcher.signature.MethodSignature
import org.jf.dexlib2.Opcode
import org.jf.dexlib2.iface.ClassDef
import org.jf.dexlib2.iface.Method
private val logger = KotlinLogging.logger("MethodResolver")
internal class MethodResolver(private val classList: List<ClassNode>, private val signatures: Array<Signature>) {
// TODO: add logger
internal class MethodResolver(private val classes: Set<ClassDef>, private val signatures: Array<MethodSignature>) {
fun resolve(): MethodMap {
val methodMap = MethodMap()
for ((classNode, methods) in classList) {
for (method in methods) {
for (signature in signatures) {
if (methodMap.containsKey(signature.name)) { // method already found for this sig
logger.trace { "Sig ${signature.name} already found, skipping." }
for (classDef in classes) {
for (method in classDef.methods) {
for (methodSignature in signatures) {
if (methodMap.containsKey(methodSignature.name)) { // method already found for this sig
continue
}
logger.trace { "Resolving sig ${signature.name}: ${classNode.name} / ${method.name}" }
val (r, sr) = cmp(method, signature)
val (r, sr) = cmp(method, methodSignature)
if (!r || sr == null) {
logger.trace { "Compare result for sig ${signature.name} has failed!" }
continue
}
logger.trace { "Method for sig ${signature.name} found!" }
methodMap[signature.name] = PatchData(
classNode,
methodMap[methodSignature.name] = MethodSignatureScanResult(
method,
PatternScanData(
// sadly we cannot create contracts for a data class, so we must assert
@ -44,7 +39,6 @@ internal class MethodResolver(private val classList: List<ClassNode>, private va
for (signature in signatures) {
if (methodMap.containsKey(signature.name)) continue
logger.error { "Could not find method for sig ${signature.name}!" }
}
return methodMap
@ -52,12 +46,11 @@ internal class MethodResolver(private val classList: List<ClassNode>, private va
// These functions do not require the constructor values, so they can be static.
companion object {
fun resolveMethod(classNode: ClassNode, signature: Signature): PatchData? {
fun resolveMethod(classNode: ClassDef, signature: MethodSignature): MethodSignatureScanResult? {
for (method in classNode.methods) {
val (r, sr) = cmp(method, signature)
if (!r || sr == null) continue
return PatchData(
classNode,
return MethodSignatureScanResult(
method,
PatternScanData(0, 0) // opcode list is always ignored.
)
@ -65,92 +58,72 @@ internal class MethodResolver(private val classList: List<ClassNode>, private va
return null
}
private fun cmp(method: MethodNode, signature: Signature): Pair<Boolean, ScanResult?> {
signature.returns?.let { _ ->
val methodReturns = Type.getReturnType(method.desc).convertObject()
if (signature.returns != methodReturns) {
logger.trace {
"""
Comparing sig ${signature.name}: invalid return type:
expected ${signature.returns},
got $methodReturns
""".trimIndent()
}
private fun cmp(method: Method, signature: MethodSignature): Pair<Boolean, MethodResolverScanResult?> {
// TODO: compare as generic object if not primitive
signature.returnType?.let { _ ->
if (signature.returnType != method.returnType) {
return@cmp false to null
}
}
signature.accessors?.let { _ ->
if (signature.accessors != method.access) {
logger.trace {
"""
Comparing sig ${signature.name}: invalid accessors:
expected ${signature.accessors},
got ${method.access}
""".trimIndent()
}
signature.accessFlags?.let { _ ->
if (signature.accessFlags != method.accessFlags) {
return@cmp false to null
}
}
signature.parameters?.let { _ ->
val parameters = Type.getArgumentTypes(method.desc).convertObjects()
if (!signature.parameters.contentEquals(parameters)) {
logger.trace {
"""
Comparing sig ${signature.name}: invalid parameter types:
expected ${signature.parameters.joinToString()}},
got ${parameters.joinToString()}
""".trimIndent()
}
// TODO: compare as generic object if the parameter is not primitive
signature.methodParameters?.let { _ ->
if (signature.methodParameters != method.parameters) {
return@cmp false to null
}
}
signature.opcodes?.let { _ ->
val result = method.instructions.scanFor(signature.opcodes)
if (!result.found) {
logger.trace { "Comparing sig ${signature.name}: invalid opcode pattern" }
return@cmp false to null
}
return@cmp true to result
val result = method.implementation?.instructions?.scanFor(signature.opcodes)
return@cmp if (result != null && result.found) true to result else false to null
}
return true to ScanResult(true)
return true to MethodResolverScanResult(true)
}
}
}
private operator fun ClassNode.component1() = this
private operator fun ClassNode.component2() = this.methods
private operator fun ClassDef.component1() = this
private operator fun ClassDef.component2() = this.methods
private fun InsnList.scanFor(pattern: IntArray): ScanResult {
for (i in 0 until this.size()) {
private fun <T> MutableIterable<T>.scanFor(pattern: Array<Opcode>): MethodResolverScanResult {
// TODO: create var for count?
for (i in 0 until this.count()) {
var occurrence = 0
while (i + occurrence < this.size()) {
val n = this[i + occurrence]
if (!n.shouldSkip() && n.opcode != pattern[occurrence]) break
while (i + occurrence < this.count()) {
val n = this.elementAt(i + occurrence)
if (!n.shouldSkip() && n != pattern[occurrence]) break
if (++occurrence >= pattern.size) {
val current = i + occurrence
return ScanResult(true, current - pattern.size, current)
return MethodResolverScanResult(true, current - pattern.size, current)
}
}
}
return ScanResult(false)
return MethodResolverScanResult(false)
}
private fun Type.convertObject(): Type {
return when (this.sort) {
Type.OBJECT -> ExtraTypes.Any
Type.ARRAY -> ExtraTypes.ArrayAny
else -> this
}
// TODO: extend Opcode type, not T (requires a cast to Opcode)
private fun <T> T.shouldSkip(): Boolean {
return this == Opcode.GOTO // TODO: and: this == AbstractInsnNode.LINE
}
private fun Array<Type>.convertObjects(): Array<Type> {
return this.map { it.convertObject() }.toTypedArray()
}
// TODO: use this somehow to compare types as generic objects if not primitive
// private fun Type.convertObject(): Type {
// return when (this.sort) {
// Type.OBJECT -> ExtraTypes.Any
// Type.ARRAY -> ExtraTypes.ArrayAny
// else -> this
// }
// }
//
// private fun Array<Type>.convertObjects(): Array<Type> {
// return this.map { it.convertObject() }.toTypedArray()
// }
private fun AbstractInsnNode.shouldSkip() =
type == AbstractInsnNode.LABEL || type == AbstractInsnNode.LINE

View file

@ -1,7 +1,7 @@
package app.revanced.patcher.resolver
internal data class ScanResult(
internal data class MethodResolverScanResult(
val found: Boolean,
val startIndex: Int? = 0,
val endIndex: Int? = 0
)
)

View file

@ -0,0 +1,12 @@
package app.revanced.patcher.signature
import org.jf.dexlib2.Opcode
@Suppress("ArrayInDataClass")
data class MethodSignature(
val name: String,
val returnType: String?,
val accessFlags: Int?,
val methodParameters: Iterable<CharSequence>?,
val opcodes: Array<Opcode>?
)

View file

@ -0,0 +1,23 @@
package app.revanced.patcher.signature
import app.revanced.patcher.resolver.MethodResolver
import org.jf.dexlib2.iface.ClassDef
import org.jf.dexlib2.iface.Method
import org.jf.dexlib2.immutable.reference.ImmutableTypeReference
// TODO: IMPORTANT: we might have to use a class proxy as well here
data class MethodSignatureScanResult(
val method: Method,
val scanData: PatternScanData
) {
@Suppress("Unused") // TODO(Sculas): remove this when we have coverage for this method.
fun findParentMethod(signature: MethodSignature): MethodSignatureScanResult? {
// TODO: find a way to get the classNode out of method.definingClass
return MethodResolver.resolveMethod(ImmutableTypeReference(method.definingClass) as ClassDef, signature)
}
}
data class PatternScanData(
val startIndex: Int,
val endIndex: Int
)

View file

@ -1,27 +0,0 @@
package app.revanced.patcher.signature
import org.objectweb.asm.Type
/**
* An ASM signature list for the Patcher.
*
* @param name The name of the method.
* Do not use the actual method name, instead try to guess what the method name originally was.
* If you are unable to guess a method name, doing something like "patch-name-1" is fine too.
* For example: "override-codec-1".
* This method name will be mapped to the method matching the signature.
* Even though this is technically not needed for the `findParentMethod` method,
* it is still recommended giving the method a name, so it can be identified easily.
* @param returns The return type/signature of the method.
* @param accessors The accessors of the method.
* @param parameters The parameter types of the method.
* @param opcodes The opcode pattern of the method, used to find the method by pattern scanning.
*/
@Suppress("ArrayInDataClass")
data class Signature(
val name: String,
val returns: Type?,
val accessors: Int?,
val parameters: Array<Type>?,
val opcodes: IntArray?
)

View file

@ -1,12 +0,0 @@
package app.revanced.patcher.util
import org.objectweb.asm.Type
object ExtraTypes {
/**
* Any object type.
* Should be used instead of types such as: "Ljava/lang/String;"
*/
val Any: Type = Type.getType(Object::class.java)
val ArrayAny: Type = Type.getType(Array<Any>::class.java)
}

View file

@ -1,94 +0,0 @@
package app.revanced.patcher.util
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.tree.ClassNode
import java.io.BufferedInputStream
import java.io.InputStream
import java.io.OutputStream
import java.util.jar.JarEntry
import java.util.jar.JarInputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
internal class Io(
private val input: InputStream,
private val output: OutputStream,
private val classes: MutableList<ClassNode>
) {
private val bufferedInputStream = BufferedInputStream(input)
fun readFromJar() {
bufferedInputStream.mark(Integer.MAX_VALUE)
// create a BufferedInputStream in order to read the input stream again when calling saveAsJar(..)
val jis = JarInputStream(bufferedInputStream)
// read all entries from the input stream
// we use JarEntry because we only read .class files
lateinit var jarEntry: JarEntry
while (jis.nextJarEntry.also { if (it != null) jarEntry = it } != null) {
// if the current entry ends with .class (indicating a java class file), add it to our list of classes to return
if (jarEntry.name.endsWith(".class")) {
// create a new ClassNode
val classNode = ClassNode()
// read the bytes with a ClassReader into the ClassNode
ClassReader(jis.readBytes()).accept(classNode, ClassReader.EXPAND_FRAMES)
// add it to our list
classes.add(classNode)
}
// finally, close the entry
jis.closeEntry()
}
// at last reset the buffered input stream
bufferedInputStream.reset()
}
fun saveAsJar() {
val jis = ZipInputStream(bufferedInputStream)
val jos = ZipOutputStream(output)
val classReaders = mutableMapOf<String, ClassReader>()
// first write all non .class zip entries from the original input stream to the output stream
// we read it first to close the input stream as fast as possible
// TODO(oSumAtrIX): There is currently no way to remove non .class files.
lateinit var zipEntry: ZipEntry
while (jis.nextEntry.also { if (it != null) zipEntry = it } != null) {
if (zipEntry.name.endsWith(".class")) {
classReaders[zipEntry.name] = ClassReader(jis.readBytes())
continue
}
// create a new zipEntry and write the contents of the zipEntry to the output stream and close it
jos.putNextEntry(ZipEntry(zipEntry))
jos.write(jis.readBytes())
jos.closeEntry()
}
// finally, close the input stream
jis.close()
bufferedInputStream.close()
input.close()
// now write all the patched classes to the output stream
for (patchedClass in classes) {
// create a new entry of the patched class
val name = patchedClass.name + ".class"
jos.putNextEntry(JarEntry(name))
// parse the patched class to a byte array and write it to the output stream
val cw = ClassWriter(classReaders[name]!!, ClassWriter.COMPUTE_MAXS)
patchedClass.accept(cw)
jos.write(cw.toByteArray())
// close the newly created jar entry
jos.closeEntry()
}
// finally, close the rest of the streams
jos.close()
output.close()
}
}

View file

@ -1,21 +0,0 @@
package app.revanced.patcher.writer
import org.objectweb.asm.tree.AbstractInsnNode
import org.objectweb.asm.tree.InsnList
object ASMWriter {
fun InsnList.setAt(index: Int, node: AbstractInsnNode) {
this[this.get(index)] = node
}
fun InsnList.insertAt(index: Int = 0, vararg nodes: AbstractInsnNode) {
this.insert(this.get(index), nodes.toInsnList())
}
// TODO(Sculas): Should this be public?
private fun Array<out AbstractInsnNode>.toInsnList(): InsnList {
val list = InsnList()
this.forEach { list.add(it) }
return list
}
}

View file

@ -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
))
)
}
}
}

View file

@ -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
}
}

View file

@ -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 ", "
}
}

View 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()
}