diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 364cdf7..46acd7e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,18 +17,14 @@ jobs: steps: - name: Checkout uses: actions/checkout@v5 - with: - # Make sure the release step uses its own credentials: - # https://github.com/cycjimmy/semantic-release-action#private-packages - persist-credentials: false - fetch-depth: 0 - name: Cache Gradle uses: burrunan/gradle-cache-action@v3 - name: Build env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ env.GITHUB_ACTOR }} + ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }} run: ./gradlew build clean - name: Setup Node.js @@ -48,6 +44,7 @@ jobs: fingerprint: ${{ vars.GPG_FINGERPRINT }} - name: Release + uses: cycjimmy/semantic-release-action@v4 env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: npm exec semantic-release + ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ env.GITHUB_ACTOR }} + ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }} diff --git a/core/src/commonMain/kotlin/app/revanced/patcher/InternalApi.kt b/core/src/commonMain/kotlin/app/revanced/patcher/InternalApi.kt deleted file mode 100644 index 71b5647..0000000 --- a/core/src/commonMain/kotlin/app/revanced/patcher/InternalApi.kt +++ /dev/null @@ -1,7 +0,0 @@ -package app.revanced.patcher - -@RequiresOptIn( - level = RequiresOptIn.Level.ERROR, - message = "This is an internal API, don't rely on it.", -) -annotation class InternalApi diff --git a/core/src/commonMain/kotlin/app/revanced/patcher/Matching.kt b/core/src/commonMain/kotlin/app/revanced/patcher/Matching.kt deleted file mode 100644 index 69decf5..0000000 --- a/core/src/commonMain/kotlin/app/revanced/patcher/Matching.kt +++ /dev/null @@ -1,814 +0,0 @@ -@file:Suppress("unused", "MemberVisibilityCanBePrivate", "CONTEXT_RECEIVERS_DEPRECATED") - -package app.revanced.patcher - -import app.revanced.patcher.extensions.* -import app.revanced.patcher.patch.BytecodePatchContext -import com.android.tools.smali.dexlib2.AccessFlags -import com.android.tools.smali.dexlib2.HiddenApiRestriction -import com.android.tools.smali.dexlib2.Opcode -import com.android.tools.smali.dexlib2.iface.* -import com.android.tools.smali.dexlib2.iface.Annotation -import com.android.tools.smali.dexlib2.iface.instruction.* -import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction22t -import com.android.tools.smali.dexlib2.util.MethodUtil -import com.sun.jdi.StringReference -import kotlin.properties.ReadOnlyProperty -import kotlin.reflect.KProperty - -fun Iterable.anyClassDef(predicate: ClassDef.() -> Boolean) = any(predicate) - -fun ClassDef.anyMethod(predicate: Method.() -> Boolean) = methods.any(predicate) - -fun ClassDef.anyDirectMethod(predicate: Method.() -> Boolean) = directMethods.any(predicate) - -fun ClassDef.anyVirtualMethod(predicate: Method.() -> Boolean) = virtualMethods.any(predicate) - -fun ClassDef.anyField(predicate: Field.() -> Boolean) = fields.any(predicate) - -fun ClassDef.anyInstanceField(predicate: Field.() -> Boolean) = instanceFields.any(predicate) - -fun ClassDef.anyStaticField(predicate: Field.() -> Boolean) = staticFields.any(predicate) - -fun ClassDef.anyInterface(predicate: String.() -> Boolean) = interfaces.any(predicate) - -fun ClassDef.anyAnnotation(predicate: Annotation.() -> Boolean) = annotations.any(predicate) - -fun Method.implementation(predicate: MethodImplementation.() -> Boolean) = implementation?.predicate() ?: false - -fun Method.anyParameter(predicate: MethodParameter.() -> Boolean) = parameters.any(predicate) - -fun Method.anyParameterType(predicate: CharSequence.() -> Boolean) = parameterTypes.any(predicate) - -fun Method.anyAnnotation(predicate: Annotation.() -> Boolean) = annotations.any(predicate) - -fun Method.anyHiddenApiRestriction(predicate: HiddenApiRestriction.() -> Boolean) = hiddenApiRestrictions.any(predicate) - -fun MethodImplementation.anyInstruction(predicate: Instruction.() -> Boolean) = instructions.any(predicate) - -fun MethodImplementation.anyTryBlock(predicate: TryBlock.() -> Boolean) = tryBlocks.any(predicate) - -fun MethodImplementation.anyDebugItem(predicate: Any.() -> Boolean) = debugItems.any(predicate) - -fun Iterable.anyInstruction(predicate: Instruction.() -> Boolean) = any(predicate) - -fun BytecodePatchContext.firstClassDefOrNull(predicate: context(PredicateContext) ClassDef.() -> Boolean) = - with(PredicateContext()) { classDefs.firstOrNull { it.predicate() } } - -fun BytecodePatchContext.firstClassDef(predicate: context(PredicateContext) ClassDef.() -> Boolean) = - requireNotNull(firstClassDefOrNull(predicate)) - -fun BytecodePatchContext.firstClassDefMutableOrNull(predicate: context(PredicateContext) ClassDef.() -> Boolean) = - firstClassDefOrNull(predicate)?.let { classDefs.getOrReplaceMutable(it) } - -fun BytecodePatchContext.firstClassDefMutable(predicate: context(PredicateContext) ClassDef.() -> Boolean) = - requireNotNull(firstClassDefMutableOrNull(predicate)) - -fun BytecodePatchContext.firstClassDefOrNull( - type: String, predicate: (context(PredicateContext) ClassDef.() -> Boolean)? = null -) = classDefs[type]?.takeIf { - predicate == null || with(PredicateContext()) { it.predicate() } -} - -fun BytecodePatchContext.firstClassDef( - type: String, - predicate: (context(PredicateContext) ClassDef.() -> Boolean)? = null -) = requireNotNull(firstClassDefOrNull(type, predicate)) - -fun BytecodePatchContext.firstClassDefMutableOrNull( - type: String, - predicate: (context(PredicateContext) ClassDef.() -> Boolean)? = null -) = firstClassDefOrNull(type, predicate)?.let { classDefs.getOrReplaceMutable(it) } - -fun BytecodePatchContext.firstClassDefMutable( - type: String, - predicate: (context(PredicateContext) ClassDef.() -> Boolean)? = null -) = requireNotNull(firstClassDefMutableOrNull(type, predicate)) - -fun BytecodePatchContext.firstMethodOrNull(predicate: context(PredicateContext) Method.() -> Boolean): Method? { - val methods = classDefs.asSequence().flatMap { it.methods.asSequence() } - with(PredicateContext()) { - return methods.firstOrNull { it.predicate() } - } -} - -fun BytecodePatchContext.firstMethod(predicate: context(PredicateContext) Method.() -> Boolean) = - requireNotNull(firstMethodOrNull(predicate)) - -fun BytecodePatchContext.firstMethodMutableOrNull(predicate: context(PredicateContext) Method.() -> Boolean) = - firstMethodOrNull(predicate)?.let { method -> - firstClassDefMutable(method.definingClass).methods.first { - MethodUtil.methodSignaturesMatch(method, it) - } - } - -fun BytecodePatchContext.firstMethodMutable(predicate: context(PredicateContext) Method.() -> Boolean) = - requireNotNull(firstMethodMutableOrNull(predicate)) - -fun BytecodePatchContext.firstMethodOrNull( - vararg strings: String, - predicate: context(PredicateContext) Method.() -> Boolean = { true }, -) = with(PredicateContext()) { - val methodsWithStrings = strings.mapNotNull { classDefs.methodsByString[it] } - if (methodsWithStrings.size != strings.size) return null - - methodsWithStrings.minBy { it.size }.firstOrNull { method -> - val containsAllOtherStrings = methodsWithStrings.all { method in it } - containsAllOtherStrings && method.predicate() - } -} - -fun BytecodePatchContext.firstMethod( - vararg strings: String, - predicate: context(PredicateContext) Method.() -> Boolean = { true }, -) = requireNotNull(firstMethodOrNull(*strings, predicate = predicate)) - -fun BytecodePatchContext.firstMethodMutableOrNull( - vararg strings: String, - predicate: context(PredicateContext) Method.() -> Boolean = { true }, -) = firstMethodOrNull(*strings, predicate = predicate)?.let { method -> - firstClassDefMutable(method.definingClass).methods.first { - MethodUtil.methodSignaturesMatch( - method, it - ) - } -} - -fun BytecodePatchContext.firstMethodMutable( - vararg strings: String, predicate: context(PredicateContext) Method.() -> Boolean = { true } -) = requireNotNull(firstMethodMutableOrNull(*strings, predicate = predicate)) - -class CachedReadOnlyProperty internal constructor( - private val block: BytecodePatchContext.(KProperty<*>) -> T -) : ReadOnlyProperty { - private var value: T? = null - private var cached = false - - override fun getValue(thisRef: BytecodePatchContext, property: KProperty<*>): T { - if (!cached) { - value = thisRef.block(property) - cached = true - } - - return value!! - } -} - -fun gettingFirstClassDefOrNull(predicate: context(PredicateContext) ClassDef.() -> Boolean) = - CachedReadOnlyProperty { firstClassDefOrNull(predicate) } - -fun gettingFirstClassDef(predicate: context(PredicateContext) ClassDef.() -> Boolean) = - CachedReadOnlyProperty { firstClassDef(predicate) } - -fun gettingFirstClassDefMutableOrNull(predicate: context(PredicateContext) ClassDef.() -> Boolean) = - CachedReadOnlyProperty { firstClassDefMutableOrNull(predicate) } - -fun gettingFirstClassDefMutable(predicate: context(PredicateContext) ClassDef.() -> Boolean) = - CachedReadOnlyProperty { firstClassDefMutable(predicate) } - -fun gettingFirstClassDefOrNull( - type: String, predicate: (context(PredicateContext) ClassDef.() -> Boolean)? = null -) = CachedReadOnlyProperty { firstClassDefOrNull(type, predicate) } - -fun gettingFirstClassDef( - type: String, predicate: (context(PredicateContext) ClassDef.() -> Boolean)? = null -) = CachedReadOnlyProperty { firstClassDef(type, predicate) } - -fun gettingFirstClassDefMutableOrNull( - type: String, predicate: (context(PredicateContext) ClassDef.() -> Boolean)? = null -) = CachedReadOnlyProperty { firstClassDefMutableOrNull(type, predicate) } - -fun gettingFirstClassDefMutable( - type: String, predicate: (context(PredicateContext) ClassDef.() -> Boolean)? = null -) = CachedReadOnlyProperty { firstClassDefMutable(type, predicate) } - -fun gettingFirstMethodOrNull(predicate: context(PredicateContext) Method.() -> Boolean) = - CachedReadOnlyProperty { firstMethodOrNull(predicate) } - -fun gettingFirstMethod(predicate: context(PredicateContext) Method.() -> Boolean) = - CachedReadOnlyProperty { firstMethod(predicate) } - -fun gettingFirstMethodMutableOrNull(predicate: context(PredicateContext) Method.() -> Boolean) = - CachedReadOnlyProperty { firstMethodMutableOrNull(predicate) } - -fun gettingFirstMethodMutable(predicate: context(PredicateContext) Method.() -> Boolean) = - CachedReadOnlyProperty { firstMethodMutable(predicate) } - -fun gettingFirstMethodOrNull( - vararg strings: String, - predicate: context(PredicateContext) Method.() -> Boolean = { true }, -) = CachedReadOnlyProperty { firstMethodOrNull(*strings, predicate = predicate) } - -fun gettingFirstMethod( - vararg strings: String, - predicate: context(PredicateContext) Method.() -> Boolean = { true }, -) = CachedReadOnlyProperty { firstMethod(*strings, predicate = predicate) } - -fun gettingFirstMethodMutableOrNull( - vararg strings: String, - predicate: context(PredicateContext) Method.() -> Boolean = { true }, -) = CachedReadOnlyProperty { firstMethodMutableOrNull(*strings, predicate = predicate) } - -fun gettingFirstMethodMutable( - vararg strings: String, - predicate: context(PredicateContext) Method.() -> Boolean = { true }, -) = CachedReadOnlyProperty { firstMethodMutable(*strings, predicate = predicate) } - -// region Matcher - -// region IndexedMatcher - -fun indexedMatcher() = IndexedMatcher() - -fun indexedMatcher(build: IndexedMatcher.() -> Unit) = - IndexedMatcher().apply(build) - -fun Iterable.matchIndexed(build: IndexedMatcher.() -> Unit) = - indexedMatcher(build)(this) - -context(_: PredicateContext) -fun Iterable.rememberedMatchIndexed(key: Any, build: IndexedMatcher.() -> Unit) = - indexedMatcher()(key, this, build) - -context(matcher: IndexedMatcher) -fun head( - predicate: T.(lastMatchedIndex: Int, currentIndex: Int) -> Boolean -): T.(Int, Int) -> Boolean = { lastMatchedIndex, currentIndex -> - currentIndex == 0 && predicate(lastMatchedIndex, currentIndex) -} - -context(matcher: IndexedMatcher) -fun head(predicate: T.() -> Boolean): T.(Int, Int) -> Boolean = - head { _, _ -> predicate() } - -context(matcher: IndexedMatcher) -fun after( - range: IntRange = 1..1, - predicate: T.(lastMatchedIndex: Int, currentIndex: Int) -> Boolean -): T.(Int, Int) -> Boolean = predicate@{ lastMatchedIndex, currentIndex -> - val distance = currentIndex - lastMatchedIndex - - matcher.nextIndex = when { - distance < range.first -> lastMatchedIndex + range.first - distance > range.last -> -1 - else -> return@predicate predicate(lastMatchedIndex, currentIndex) - } - - false -} - -context(matcher: IndexedMatcher) -fun after(range: IntRange = 1..1, predicate: T.() -> Boolean) = - after(range) { _, _ -> predicate() } - -context(matcher: IndexedMatcher) -operator fun (T.(Int, Int) -> Boolean).unaryPlus() = matcher.add(this) - -class IndexedMatcher : Matcher Boolean>() { - private val _indices: MutableList = mutableListOf() - val indices: List = _indices - - private var lastMatchedIndex = -1 - private var currentIndex = -1 - var nextIndex: Int? = null - - override fun invoke(haystack: Iterable): Boolean { - // Normalize to list - val hay = haystack as? List ?: haystack.toList() - - _indices.clear() - this@IndexedMatcher.lastMatchedIndex = -1 - currentIndex = -1 - - data class Frame( - val patternIndex: Int, - val lastMatchedIndex: Int, - val previousFrame: Frame?, - var nextHayIndex: Int, - val matchedIndex: Int - ) - - val stack = ArrayDeque() - stack.add( - Frame( - patternIndex = 0, - lastMatchedIndex = -1, - previousFrame = null, - nextHayIndex = 0, - matchedIndex = -1 - ) - ) - - while (stack.isNotEmpty()) { - val frame = stack.last() - - if (frame.nextHayIndex >= hay.size || nextIndex == -1) { - stack.removeLast() - nextIndex = null - continue - } - - val i = frame.nextHayIndex - currentIndex = i - lastMatchedIndex = frame.lastMatchedIndex - nextIndex = null - - if (this[frame.patternIndex](hay[i], lastMatchedIndex, currentIndex)) { - Frame( - patternIndex = frame.patternIndex + 1, - lastMatchedIndex = i, - previousFrame = frame, - nextHayIndex = i + 1, - matchedIndex = i - ).also { - if (it.patternIndex == size) { - _indices += buildList(size) { - var f: Frame? = it - while (f != null && f.matchedIndex != -1) { - add(f.matchedIndex) - f = f.previousFrame - } - }.asReversed() - - return true - } - }.let(stack::add) - } - - frame.nextHayIndex = when (val nextIndex = nextIndex) { - null -> frame.nextHayIndex + 1 - -1 -> 0 // Frame will be removed next loop. - else -> nextIndex - } - } - - return false - } -} - -// endregion - -context(_: PredicateContext) -inline operator fun > M.invoke(key: Any, iterable: Iterable, builder: M.() -> Unit) = - remembered(key) { apply(builder) }(iterable) - -context(_: PredicateContext) -inline operator fun > M.invoke( - iterable: Iterable, - builder: M.() -> Unit -) = invoke(this@invoke.hashCode(), iterable, builder) - -abstract class Matcher : MutableList by mutableListOf() { - var matchIndex = -1 - protected set - - abstract operator fun invoke(haystack: Iterable): Boolean -} - -// endregion Matcher - -class PredicateContext internal constructor() : MutableMap by mutableMapOf() - -context(context: PredicateContext) -inline fun remembered(key: Any, defaultValue: () -> V) = - context[key] as? V ?: defaultValue().also { context[key] = it } - - -fun T.declarativePredicate(build: DeclarativePredicateBuilder.() -> Unit) = - DeclarativePredicateBuilder().apply(build).all(this) - -context(_: PredicateContext) -fun T.rememberedDeclarativePredicate(key: Any, block: DeclarativePredicateBuilder.() -> Unit): Boolean = - remembered(key) { DeclarativePredicateBuilder().apply(block) }.all(this) - -context(_: PredicateContext) -private fun T.rememberedDeclarativePredicate(predicate: context(PredicateContext, T) DeclarativePredicateBuilder.() -> Unit) = - rememberedDeclarativePredicate("declarative predicate build") { predicate() } - -fun BytecodePatchContext.firstClassDefByDeclarativePredicateOrNull( - predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder.() -> Unit -) = firstClassDefOrNull { rememberedDeclarativePredicate(predicate) } - -fun BytecodePatchContext.firstClassDefByDeclarativePredicate( - predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder.() -> Unit -) = requireNotNull(firstClassDefByDeclarativePredicateOrNull(predicate)) - -fun BytecodePatchContext.firstClassDefMutableByDeclarativePredicateOrNull( - predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder.() -> Unit -) = firstClassDefMutableOrNull { rememberedDeclarativePredicate(predicate) } - -fun BytecodePatchContext.firstClassDefMutableByDeclarativePredicate( - predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder.() -> Unit -) = requireNotNull(firstClassDefMutableByDeclarativePredicateOrNull(predicate)) - -fun BytecodePatchContext.firstClassDefByDeclarativePredicateOrNull( - type: String, - predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder.() -> Unit -) = firstClassDefOrNull(type) { rememberedDeclarativePredicate(predicate) } - -fun BytecodePatchContext.firstClassDefByDeclarativePredicate( - type: String, - predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder.() -> Unit -) = requireNotNull(firstClassDefByDeclarativePredicateOrNull(type, predicate)) - -fun BytecodePatchContext.firstClassDefMutableByDeclarativePredicateOrNull( - type: String, - predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder.() -> Unit -) = firstClassDefMutableOrNull(type) { rememberedDeclarativePredicate(predicate) } - -fun BytecodePatchContext.firstClassDefMutableByDeclarativePredicate( - type: String, - predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder.() -> Unit -) = requireNotNull(firstClassDefMutableByDeclarativePredicateOrNull(type, predicate)) - -fun BytecodePatchContext.firstMethodByDeclarativePredicateOrNull( - predicate: context(PredicateContext, Method) DeclarativePredicateBuilder.() -> Unit -) = firstMethodOrNull { rememberedDeclarativePredicate(predicate) } - -fun BytecodePatchContext.firstMethodByDeclarativePredicate( - predicate: context(PredicateContext, Method) DeclarativePredicateBuilder.() -> Unit -) = requireNotNull(firstMethodByDeclarativePredicateOrNull(predicate)) - -fun BytecodePatchContext.firstMethodMutableByDeclarativePredicateOrNull( - predicate: context(PredicateContext, Method) DeclarativePredicateBuilder.() -> Unit -) = firstMethodMutableOrNull { rememberedDeclarativePredicate(predicate) } - -fun BytecodePatchContext.firstMethodMutableByDeclarativePredicate( - predicate: context(PredicateContext, Method) DeclarativePredicateBuilder.() -> Unit -) = requireNotNull(firstMethodMutableByDeclarativePredicateOrNull(predicate)) - -fun BytecodePatchContext.firstMethodByDeclarativePredicateOrNull( - vararg strings: String, - predicate: context(PredicateContext, Method) DeclarativePredicateBuilder.() -> Unit -) = firstMethodOrNull(*strings) { rememberedDeclarativePredicate(predicate) } - -fun BytecodePatchContext.firstMethodByDeclarativePredicate( - vararg strings: String, - predicate: context(PredicateContext, Method) DeclarativePredicateBuilder.() -> Unit -) = requireNotNull(firstMethodByDeclarativePredicateOrNull(*strings, predicate = predicate)) - -fun BytecodePatchContext.firstMethodMutableByDeclarativePredicateOrNull( - vararg strings: String, - predicate: context(PredicateContext, Method) DeclarativePredicateBuilder.() -> Unit -) = firstMethodMutableOrNull(*strings) { rememberedDeclarativePredicate(predicate) } - -fun BytecodePatchContext.firstMethodMutableByDeclarativePredicate( - vararg strings: String, - predicate: context(PredicateContext, Method) DeclarativePredicateBuilder.() -> Unit -) = requireNotNull(firstMethodMutableByDeclarativePredicateOrNull(*strings, predicate = predicate)) - -fun gettingFirstClassDefByDeclarativePredicateOrNull( - type: String, - predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder.() -> Unit -) = gettingFirstClassDefOrNull(type) { rememberedDeclarativePredicate(predicate) } - -fun gettingFirstClassDefByDeclarativePredicate( - type: String, - predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder.() -> Unit -) = CachedReadOnlyProperty { firstClassDefByDeclarativePredicate(type, predicate) } - -fun gettingFirstClassDefMutableByDeclarativePredicateOrNull( - type: String, - predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder.() -> Unit -) = gettingFirstClassDefMutableOrNull(type) { rememberedDeclarativePredicate(predicate) } - -fun gettingFirstClassDefMutableByDeclarativePredicate( - type: String, - predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder.() -> Unit -) = CachedReadOnlyProperty { firstClassDefMutableByDeclarativePredicate(type, predicate) } - -fun gettingFirstClassDefByDeclarativePredicateOrNull( - predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder.() -> Unit -) = gettingFirstClassDefOrNull { rememberedDeclarativePredicate(predicate) } - -fun gettingFirstClassDefByDeclarativePredicate( - predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder.() -> Unit -) = CachedReadOnlyProperty { firstClassDefByDeclarativePredicate(predicate) } - -fun gettingFirstClassDefMutableByDeclarativePredicateOrNull( - predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder.() -> Unit -) = gettingFirstClassDefMutableOrNull { rememberedDeclarativePredicate(predicate) } - -fun gettingFirstClassDefMutableByDeclarativePredicate( - predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder.() -> Unit -) = CachedReadOnlyProperty { firstClassDefMutableByDeclarativePredicate(predicate) } - -fun gettingFirstMethodByDeclarativePredicateOrNull( - predicate: context(PredicateContext, Method) DeclarativePredicateBuilder.() -> Unit -) = gettingFirstMethodOrNull { rememberedDeclarativePredicate(predicate) } - -fun gettingFirstMethodByDeclarativePredicate( - predicate: context(PredicateContext, Method) DeclarativePredicateBuilder.() -> Unit -) = CachedReadOnlyProperty { firstMethodByDeclarativePredicate(predicate = predicate) } - -fun gettingFirstMethodMutableByDeclarativePredicateOrNull( - predicate: context(PredicateContext, Method) DeclarativePredicateBuilder.() -> Unit -) = gettingFirstMethodMutableOrNull { rememberedDeclarativePredicate(predicate) } - -fun gettingFirstMethodMutableByDeclarativePredicate( - predicate: context(PredicateContext, Method) DeclarativePredicateBuilder.() -> Unit -) = CachedReadOnlyProperty { firstMethodMutableByDeclarativePredicate(predicate = predicate) } - -fun gettingFirstMethodByDeclarativePredicateOrNull( - vararg strings: String, - predicate: context(PredicateContext, Method) DeclarativePredicateBuilder.() -> Unit -) = gettingFirstMethodOrNull(*strings) { rememberedDeclarativePredicate(predicate) } - -fun gettingFirstMethodByDeclarativePredicate( - vararg strings: String, - predicate: context(PredicateContext, Method) DeclarativePredicateBuilder.() -> Unit -) = CachedReadOnlyProperty { firstMethodByDeclarativePredicate(*strings, predicate = predicate) } - -fun gettingFirstMethodMutableByDeclarativePredicateOrNull( - vararg strings: String, - predicate: context(PredicateContext, Method) DeclarativePredicateBuilder.() -> Unit -) = gettingFirstMethodMutableOrNull(*strings) { rememberedDeclarativePredicate(predicate) } - -fun gettingFirstMethodMutableByDeclarativePredicate( - vararg strings: String, - predicate: context(PredicateContext, Method) DeclarativePredicateBuilder.() -> Unit -) = CachedReadOnlyProperty { firstMethodMutableByDeclarativePredicate(*strings, predicate = predicate) } - - -class DeclarativePredicateBuilder internal constructor() { - private val children = mutableListOf Boolean>() - - fun anyOf(block: DeclarativePredicateBuilder.() -> Unit) { - val child = DeclarativePredicateBuilder().apply(block) - children += { child.children.any { it() } } - } - - fun predicate(block: T.() -> Boolean) { - children += block - } - - fun all(target: T): Boolean = children.all { target.it() } - fun any(target: T): Boolean = children.all { target.it() } -} - -fun firstMethodComposite( - vararg strings: String, - builder: - context(PredicateContext, Method, IndexedMatcher, MutableList) DeclarativePredicateBuilder.() -> Unit -) = with(indexedMatcher()) matcher@{ - with(mutableListOf()) strings@{ - addAll(strings) - - Composition( - indices = this@matcher.indices, - strings = this@strings - ) { builder() } - } -} - -val m = firstMethodComposite("lookup") { - instructions( - head { string == "str" }, - anyOf(), - anyOf(after(1..2, string("also lookup"))), - string("s", String::startsWith), - string(), - literal(), - after(1..4, anyOf()), - noneOf(`is`()), - instruction { opcode == Opcode.CONST_STRING }, - { _, _ -> opcode == Opcode.CONST_STRING }, - ) - instructions { - +head(literal()) - - if (true) - +after(1..2, string("lookup")) - else - +instruction { opcode == Opcode.CONST_STRING } - - add { currentIndex, lastMatchedIndex -> - currentIndex == 2 && opcode == Opcode.CONST_STRING - } - } - - instructions { - +anyOf(after(1..2, string("also lookup")), Opcode.IF_EQ()) - +head(anyOf(string("s"), "s"(), Opcode.IF_EQ())) - +head(allOf(Opcode.CONST_STRING_JUMBO(), "str"())) - add(instruction { this.opcode == Opcode.CONST_STRING || this.string == "lookup" }) - add(instruction { string == "lookup" }) - +after(1..2, anyOf(string("s"), Opcode.IF_EQ())) - +string("also lookup") - +"equals"() - +"prefix" { startsWith(it) } - +string { startsWith("prefix") } - +"suffix"(String::endsWith) - +literal(12) { it >= this } - +literal(1232) - +literal { this >= 1123 } - +literal() - +string() - +string { startsWith("s") } - +method() - +reference() - +field() - +`is`() - +`is`() - +allOf(`is`(), string("test")) - +`is` { reference !is StringReference } - +`is` { registerCount > 2 } - +registers(0, 1, 1, 2) - +noneOf(registers({ size > 3 }), reference { contains("SomeClass") }) - +type() - +Opcode.CONST_STRING() - +after(1..2, Opcode.RETURN_VOID()) - +reference { startsWith("some") } - +field("s") - +allOf() // Wildcard - +anyOf(noneOf(string(), literal(123)), allOf(Opcode.CONST_STRING(), string("tet"))) - +method("abc") { startsWith(it) } - +after(1..2, string("also lookup") { startsWith(it) }) - +after(1..2, anyOf(string("a", String::endsWith), Opcode.CONST_4())) - +reference { contains("some") } - +method("name") - +field("name") - +after(1..2, reference("com/example", String::contains)) - +after(1..2, reference("lookup()V", String::endsWith)) - +after(1..2, reference("Lcom/example;->method()V", String::startsWith)) - +after(1..2, reference("Lcom/example;->method()V")) - +after(1..2, reference("Lcom/example;->field:Ljava/lang/String;") { endsWith(it) }) - } -} - -inline fun `is`( - crossinline predicate: T.() -> Boolean = { true } -): Instruction.(Int, Int) -> Boolean = { _, _ -> (this as? T)?.predicate() == true } - -fun instruction(predicate: Instruction.() -> Boolean): Instruction.(Int, Int) -> Boolean = { _, _ -> predicate() } - -fun registers(predicate: IntArray.() -> Boolean = { true }): Instruction.(Int, Int) -> Boolean = { _, _ -> - when (this) { - is RegisterRangeInstruction -> - IntArray(registerCount) { startRegister + it }.predicate() - - is FiveRegisterInstruction -> - intArrayOf(registerC, registerD, registerE, registerF, registerG).predicate() - - is ThreeRegisterInstruction -> - intArrayOf(registerA, registerB, registerC).predicate() - - is TwoRegisterInstruction -> - intArrayOf(registerA, registerB).predicate() - - is OneRegisterInstruction -> - intArrayOf(registerA).predicate() - - else -> false - } -} - -fun registers( - vararg registers: Int, - compare: IntArray.(registers: IntArray) -> Boolean = { registers -> - this.size >= registers.size && registers.indices.all { this[it] == registers[it] } - } -) = registers({ compare(registers) }) - -fun literal(predicate: Long.() -> Boolean = { true }): Instruction.(Int, Int) -> Boolean = - { _, _ -> wideLiteral?.predicate() == true } - -fun literal(literal: Long, compare: Long.(Long) -> Boolean = Long::equals) = - literal { compare(literal) } - -fun reference(predicate: String.() -> Boolean = { true }): Instruction.(Int, Int) -> Boolean = - predicate@{ _, _ -> this.reference?.toString()?.predicate() == true } - -fun reference(reference: String, compare: String.(String) -> Boolean = String::equals) = - reference { compare(reference) } - -fun field(predicate: String.() -> Boolean = { true }): Instruction.(Int, Int) -> Boolean = { _, _ -> - fieldReference?.name?.predicate() == true -} - -fun field(name: String, compare: String.(String) -> Boolean = String::equals) = - field { compare(name) } - - -fun type(predicate: String.() -> Boolean = { true }): Instruction.(Int, Int) -> Boolean = - { _, _ -> type?.predicate() == true } - -fun type(type: String, compare: String.(String) -> Boolean = String::equals) = - type { compare(type) } - -fun method(predicate: String.() -> Boolean = { true }): Instruction.(Int, Int) -> Boolean = { _, _ -> - methodReference?.name?.predicate() == true -} - -fun method(name: String, compare: String.(String) -> Boolean = String::equals) = - method { compare(name) } - -fun string(compare: String.() -> Boolean = { true }): Instruction.(Int, Int) -> Boolean = predicate@{ _, _ -> - this@predicate.string?.compare() == true -} - -context(stringsList: MutableList, builder: DeclarativePredicateBuilder) -fun string( - string: String, - compare: String.(String) -> Boolean = String::equals -): Instruction.(Int, Int) -> Boolean { - if (compare == String::equals) stringsList += string - - return string { compare(string) } -} - -context(stringsList: MutableList) -operator fun String.invoke(compare: String.(String) -> Boolean = String::equals): Instruction.(Int, Int) -> Boolean { - if (compare == String::equals) stringsList += this - - return { _, _ -> string?.compare(this@invoke) == true } -} - -operator fun Opcode.invoke(): Instruction.(currentIndex: Int, lastMatchedIndex: Int) -> Boolean = - { _, _ -> opcode == this@invoke } - -fun anyOf( - vararg predicates: Instruction.(currentIndex: Int, lastMatchedIndex: Int) -> Boolean -): Instruction.(Int, Int) -> Boolean = { currentIndex, lastMatchedIndex -> - predicates.any { predicate -> predicate(currentIndex, lastMatchedIndex) } -} - -fun allOf( - vararg predicates: Instruction.(currentIndex: Int, lastMatchedIndex: Int) -> Boolean -): Instruction.(Int, Int) -> Boolean = { currentIndex, lastMatchedIndex -> - predicates.all { predicate -> predicate(currentIndex, lastMatchedIndex) } -} - -fun noneOf( - vararg predicates: Instruction.(currentIndex: Int, lastMatchedIndex: Int) -> Boolean -): Instruction.(Int, Int) -> Boolean = { currentIndex, lastMatchedIndex -> - predicates.none { predicate -> predicate(currentIndex, lastMatchedIndex) } -} - -fun DeclarativePredicateBuilder.accessFlags(vararg flags: AccessFlags) = - predicate { accessFlags(*flags) } - -fun DeclarativePredicateBuilder.returnType( - returnType: String, - compare: String.(String) -> Boolean = String::startsWith -) = predicate { this.returnType.compare(returnType) } - -fun DeclarativePredicateBuilder.name( - name: String, - compare: String.(String) -> Boolean = String::equals -) = - predicate { this.name.compare(name) } - -fun DeclarativePredicateBuilder.definingClass( - definingClass: String, - compare: String.(String) -> Boolean = String::equals -) = predicate { this.definingClass.compare(definingClass) } - -fun DeclarativePredicateBuilder.parameterTypes(vararg parameterTypePrefixes: String) = predicate { - parameterTypes.size == parameterTypePrefixes.size && parameterTypes.zip(parameterTypePrefixes) - .all { (a, b) -> a.startsWith(b) } -} - -context(matcher: IndexedMatcher) -fun DeclarativePredicateBuilder.instructions( - build: IndexedMatcher.() -> Unit -) { - matcher.apply(build) - predicate { implementation { matcher(instructions) } } -} - -context(matcher: IndexedMatcher) -fun DeclarativePredicateBuilder.instructions( - vararg predicates: Instruction.(currentIndex: Int, lastMatchedIndex: Int) -> Boolean -) = instructions { addAll(predicates) } - -fun DeclarativePredicateBuilder.custom(block: Method.() -> Boolean) { - predicate { block() } -} - -class Composition internal constructor( - val indices: List, - val strings: List, - private val predicate: context(PredicateContext, Method) DeclarativePredicateBuilder.() -> Unit -) { - private var _methodOrNull: com.android.tools.smali.dexlib2.mutable.MutableMethod? = null - - context(context: BytecodePatchContext) - val methodOrNull: com.android.tools.smali.dexlib2.mutable.MutableMethod? - get() { - if (_methodOrNull == null) { - _methodOrNull = if (strings.isEmpty()) - context.firstMethodMutableByDeclarativePredicateOrNull(predicate) - else - context.firstMethodMutableByDeclarativePredicateOrNull( - strings = strings.toTypedArray(), - predicate - ) - } - - return _methodOrNull - } - - context(_: BytecodePatchContext) - val method get() = requireNotNull(methodOrNull) -} diff --git a/core/src/commonMain/kotlin/app/revanced/patcher/PackageMetadata.kt b/core/src/commonMain/kotlin/app/revanced/patcher/PackageMetadata.kt deleted file mode 100644 index 4c7b9d6..0000000 --- a/core/src/commonMain/kotlin/app/revanced/patcher/PackageMetadata.kt +++ /dev/null @@ -1,16 +0,0 @@ -package app.revanced.patcher - -import brut.androlib.apk.ApkInfo - -/** - * Metadata about a package. - * - * @param apkInfo The [ApkInfo] of the apk file. - */ -class PackageMetadata internal constructor(internal val apkInfo: ApkInfo) { - lateinit var packageName: String - internal set - - lateinit var packageVersion: String - internal set -} diff --git a/core/src/commonMain/kotlin/app/revanced/patcher/Patcher.kt b/core/src/commonMain/kotlin/app/revanced/patcher/Patcher.kt deleted file mode 100644 index d8cdec8..0000000 --- a/core/src/commonMain/kotlin/app/revanced/patcher/Patcher.kt +++ /dev/null @@ -1,161 +0,0 @@ -package app.revanced.patcher - -import app.revanced.patcher.patch.* -import com.google.common.annotations.VisibleForTesting -import kotlinx.coroutines.flow.flow -import java.io.Closeable -import java.util.logging.Logger -import kotlin.reflect.jvm.jvmName - -/** - * A Patcher. - * - * @param config The configuration to use for the patcher. - */ -class Patcher(private val config: PatcherConfig) : Closeable { - private val logger = Logger.getLogger(this::class.jvmName) - - /** - * The context containing the current state of the patcher. - */ - val context = PatcherContext(config) - - init { - context.resourceContext.decodeResources(ResourcePatchContext.ResourceMode.NONE) - } - - /** - * Add patches. - * - * @param patches The patches to add. - */ - operator fun plusAssign(patches: Set>) { - // Add all patches to the executablePatches set. - context.executablePatches += patches - - // Add all patches and their dependencies to the allPatches set. - patches.forEach { patch -> - fun Patch<*>.addRecursively() = - also(context.allPatches::add).dependencies.forEach(Patch<*>::addRecursively) - - patch.addRecursively() - } - - context.allPatches.let { allPatches -> - // Check, if what kind of resource mode is required. - config.resourceMode = if (allPatches.any { patch -> patch.anyRecursively { it is ResourcePatch } }) { - ResourcePatchContext.ResourceMode.FULL - } else if (allPatches.any { patch -> patch.anyRecursively { it is RawResourcePatch } }) { - ResourcePatchContext.ResourceMode.RAW_ONLY - } else { - ResourcePatchContext.ResourceMode.NONE - } - } - } - - /** - * Execute added patches. - * - * @return A flow of [PatchResult]s. - */ - operator fun invoke() = flow { - fun Patch<*>.execute( - executedPatches: LinkedHashMap, PatchResult>, - ): PatchResult { - // If the patch was executed before or failed, return it's the result. - executedPatches[this]?.let { patchResult -> - patchResult.exception ?: return patchResult - - return PatchResult(this, PatchException("The patch '$this' failed previously")) - } - - // Recursively execute all dependency patches. - dependencies.forEach { dependency -> - dependency.execute(executedPatches).exception?.let { - return PatchResult( - this, - PatchException( - "The patch \"$this\" depends on \"$dependency\", which raised an exception:\n${it.stackTraceToString()}", - ), - ) - } - } - - // Execute the patch. - return try { - execute(context) - - PatchResult(this) - } catch (exception: PatchException) { - PatchResult(this, exception) - } catch (exception: Exception) { - PatchResult(this, PatchException(exception)) - }.also { executedPatches[this] = it } - } - - // Prevent decoding the app manifest twice if it is not needed. - if (config.resourceMode != ResourcePatchContext.ResourceMode.NONE) { - context.resourceContext.decodeResources(config.resourceMode) - } - - logger.info("Initializing cache") - - context.bytecodeContext.classDefs.initializeCache() - - logger.info("Executing patches") - - val executedPatches = LinkedHashMap, PatchResult>() - - context.executablePatches.sortedBy { it.name }.forEach { patch -> - val patchResult = patch.execute(executedPatches) - - // If an exception occurred or the patch has no finalize block, emit the result. - if (patchResult.exception != null || patch.finalizeBlock == null) { - emit(patchResult) - } - } - - val succeededPatchesWithFinalizeBlock = executedPatches.values.filter { - it.exception == null && it.patch.finalizeBlock != null - } - - succeededPatchesWithFinalizeBlock.asReversed().forEach { executionResult -> - val patch = executionResult.patch - - val result = - try { - patch.finalize(context) - - executionResult - } catch (exception: PatchException) { - PatchResult(patch, exception) - } catch (exception: Exception) { - PatchResult(patch, PatchException(exception)) - } - - if (result.exception != null) { - emit( - PatchResult( - patch, - PatchException( - "The patch \"$patch\" raised an exception: ${result.exception.stackTraceToString()}", - result.exception, - ), - ), - ) - } else if (patch in context.executablePatches) { - emit(result) - } - } - } - - override fun close() = context.close() - - /** - * Compile and save patched APK files. - * - * @return The [PatcherResult] containing the patched APK files. - */ - @OptIn(InternalApi::class) - fun get() = PatcherResult(context.bytecodeContext.get(), context.resourceContext.get()) -} diff --git a/core/src/commonMain/kotlin/app/revanced/patcher/PatcherConfig.kt b/core/src/commonMain/kotlin/app/revanced/patcher/PatcherConfig.kt deleted file mode 100644 index 5ee117d..0000000 --- a/core/src/commonMain/kotlin/app/revanced/patcher/PatcherConfig.kt +++ /dev/null @@ -1,90 +0,0 @@ -package app.revanced.patcher - -import app.revanced.patcher.patch.ResourcePatchContext -import brut.androlib.Config -import java.io.File -import java.io.deleteRecursively -import java.io.resolve -import java.util.logging.Logger -import kotlin.reflect.jvm.jvmName - -/** - * The configuration for the patcher. - * - * @param apkFile The apk file to patch. - * @param temporaryFilesPath A path to a folder to store temporary files in. - * @param aaptBinaryPath A path to a custom aapt binary. - * @param frameworkFileDirectory A path to the directory to cache the framework file in. - */ -class PatcherConfig( - internal val apkFile: File, - private val temporaryFilesPath: File = File("revanced-temporary-files"), - aaptBinaryPath: File? = null, - frameworkFileDirectory: String? = null, -) { - /** - * The configuration for the patcher. - * - * @param apkFile The apk file to patch. - * @param temporaryFilesPath A path to a folder to store temporary files in. - * @param aaptBinaryPath A path to a custom aapt binary. - * @param frameworkFileDirectory A path to the directory to cache the framework file in. - */ - @Deprecated( - "Use the constructor with a File for aaptBinaryPath instead.", - ReplaceWith("PatcherConfig(apkFile, temporaryFilesPath, aaptBinaryPath?.let { File(it) }, frameworkFileDirectory)"), - ) - constructor( - apkFile: File, - temporaryFilesPath: File = File("revanced-temporary-files"), - aaptBinaryPath: String? = null, - frameworkFileDirectory: String? = null, - ) : this(apkFile, temporaryFilesPath, aaptBinaryPath?.let { File(it) }, frameworkFileDirectory) - - private val logger = Logger.getLogger(PatcherConfig::class.jvmName) - - /** - * The mode to use for resource decoding and compiling. - * - * @see ResourcePatchContext.ResourceMode - */ - internal var resourceMode = ResourcePatchContext.ResourceMode.NONE - - /** - * The configuration for decoding and compiling resources. - */ - internal val resourceConfig = - Config.getDefaultConfig().apply { - aaptBinary = aaptBinaryPath - frameworkDirectory = frameworkFileDirectory - } - - /** - * The path to the temporary apk files directory. - */ - internal val apkFiles = temporaryFilesPath.resolve("apk") - - /** - * The path to the temporary patched files directory. - */ - internal val patchedFiles = temporaryFilesPath.resolve("patched") - - /** - * Initialize the temporary files' directories. - * This will delete the existing temporary files directory if it exists. - */ - internal fun initializeTemporaryFilesDirectories() { - temporaryFilesPath.apply { - if (exists()) { - logger.info("Deleting existing temporary files directory") - - if (!deleteRecursively()) { - logger.severe("Failed to delete existing temporary files directory") - } - } - } - - apkFiles.mkdirs() - patchedFiles.mkdirs() - } -} diff --git a/core/src/commonMain/kotlin/app/revanced/patcher/PatcherContext.kt b/core/src/commonMain/kotlin/app/revanced/patcher/PatcherContext.kt deleted file mode 100644 index e09ea0e..0000000 --- a/core/src/commonMain/kotlin/app/revanced/patcher/PatcherContext.kt +++ /dev/null @@ -1,43 +0,0 @@ -package app.revanced.patcher - -import app.revanced.patcher.patch.BytecodePatchContext -import app.revanced.patcher.patch.Patch -import app.revanced.patcher.patch.ResourcePatchContext -import brut.androlib.apk.ApkInfo -import brut.directory.ExtFile -import java.io.Closeable - -/** - * A context for the patcher containing the current state of the patcher. - * - * @param config The configuration for the patcher. - */ -@Suppress("MemberVisibilityCanBePrivate") -class PatcherContext internal constructor(config: PatcherConfig): Closeable { - /** - * [PackageMetadata] of the supplied [PatcherConfig.apkFile]. - */ - val packageMetadata = PackageMetadata(ApkInfo(ExtFile(config.apkFile))) - - /** - * The set of [Patch]es. - */ - internal val executablePatches = mutableSetOf>() - - /** - * The set of all [Patch]es and their dependencies. - */ - internal val allPatches = mutableSetOf>() - - /** - * The context for patches containing the current state of the resources. - */ - internal val resourceContext = ResourcePatchContext(packageMetadata, config) - - /** - * The context for patches containing the current state of the bytecode. - */ - internal val bytecodeContext = BytecodePatchContext(config) - - override fun close() = bytecodeContext.close() -} diff --git a/core/src/commonMain/kotlin/app/revanced/patcher/PatcherResult.kt b/core/src/commonMain/kotlin/app/revanced/patcher/PatcherResult.kt deleted file mode 100644 index 8236cef..0000000 --- a/core/src/commonMain/kotlin/app/revanced/patcher/PatcherResult.kt +++ /dev/null @@ -1,40 +0,0 @@ -package app.revanced.patcher - -import java.io.File -import java.io.InputStream - -/** - * The result of a patcher. - * - * @param dexFiles The patched dex files. - * @param resources The patched resources. - */ -@Suppress("MemberVisibilityCanBePrivate") -class PatcherResult internal constructor( - val dexFiles: Set, - val resources: PatchedResources?, -) { - - /** - * A dex file. - * - * @param name The original name of the dex file. - * @param stream The dex file as [InputStream]. - */ - class PatchedDexFile internal constructor(val name: String, val stream: InputStream) - - /** - * The resources of a patched apk. - * - * @param resourcesApk The compiled resources.apk file. - * @param otherResources The directory containing other resources files. - * @param doNotCompress List of files that should not be compressed. - * @param deleteResources List of resources that should be deleted. - */ - class PatchedResources internal constructor( - val resourcesApk: File?, - val otherResources: File?, - val doNotCompress: Set, - val deleteResources: Set, - ) -} diff --git a/core/src/commonMain/kotlin/app/revanced/patcher/patch/Patch.kt b/core/src/commonMain/kotlin/app/revanced/patcher/patch/Patch.kt deleted file mode 100644 index a918721..0000000 --- a/core/src/commonMain/kotlin/app/revanced/patcher/patch/Patch.kt +++ /dev/null @@ -1,634 +0,0 @@ -@file:Suppress("MemberVisibilityCanBePrivate", "unused") - -package app.revanced.patcher.patch - -import app.revanced.patcher.Patcher -import app.revanced.patcher.PatcherContext -import java.io.File -import java.io.InputStream -import java.lang.reflect.Member -import java.lang.reflect.Method -import java.lang.reflect.Modifier -import java.util.function.Supplier -import kotlin.properties.ReadOnlyProperty - -typealias PackageName = String -typealias VersionName = String -typealias Package = Pair?> - - -/** - * A common interface for contexts such as [ResourcePatchContext] and [BytecodePatchContext]. - */ - -sealed interface PatchContext : Supplier - -/** - * A patch. - * - * @param C The [PatchContext] to execute and finalize the patch with. - * @param name The name of the patch. - * If null, the patch is named "Patch" and will not be loaded by [loadPatches]. - * @param description The description of the patch. - * @param use Weather or not the patch should be used. - * @param dependencies Other patches this patch depends on. - * @param compatiblePackages The packages the patch is compatible with. - * If null, the patch is compatible with all packages. - * @param options The options of the patch. - * @param executeBlock The execution block of the patch. - * @param finalizeBlock The finalizing block of the patch. Called after all patches have been executed, - * in reverse order of execution. - * - * @constructor Create a new patch. - */ -sealed class Patch>( - val name: String?, - val description: String?, - val use: Boolean, - val dependencies: Set>, - val compatiblePackages: Set?, - options: Set>, - private val executeBlock: (C) -> Unit, - // Must be internal and nullable, so that Patcher.invoke can check, - // if a patch has a finalizing block in order to not emit it twice. - internal var finalizeBlock: ((C) -> Unit)?, -) { - /** - * The options of the patch. - */ - val options = Options(options) - - /** - * Calls the execution block of the patch. - * This function is called by [Patcher.invoke]. - * - * @param context The [PatcherContext] to get the [PatchContext] from to execute the patch with. - */ - internal abstract fun execute(context: PatcherContext) - - /** - * Calls the execution block of the patch. - * - * @param context The [PatchContext] to execute the patch with. - */ - fun execute(context: C) = executeBlock(context) - - /** - * Calls the finalizing block of the patch. - * This function is called by [Patcher.invoke]. - * - * @param context The [PatcherContext] to get the [PatchContext] from to finalize the patch with. - */ - internal abstract fun finalize(context: PatcherContext) - - /** - * Calls the finalizing block of the patch. - * - * @param context The [PatchContext] to finalize the patch with. - */ - fun finalize(context: C) { - finalizeBlock?.invoke(context) - } - - override fun toString() = name ?: "Patch@${System.identityHashCode(this)}" -} - -internal fun Patch<*>.anyRecursively( - visited: MutableSet> = mutableSetOf(), - predicate: (Patch<*>) -> Boolean, -): Boolean { - if (this in visited) return false - - if (predicate(this)) return true - - visited += this - - return dependencies.any { it.anyRecursively(visited, predicate) } -} - -internal fun Iterable>.forEachRecursively( - visited: MutableSet> = mutableSetOf(), - action: (Patch<*>) -> Unit, -): Unit = forEach { - if (it in visited) return@forEach - - visited += it - action(it) - - it.dependencies.forEachRecursively(visited, action) -} - -/** - * A bytecode patch. - * - * @param name The name of the patch. - * If null, the patch is named "Patch" and will not be loaded by [loadPatches]. - * @param description The description of the patch. - * @param use Weather or not the patch should be used. - * @param compatiblePackages The packages the patch is compatible with. - * If null, the patch is compatible with all packages. - * @param dependencies Other patches this patch depends on. - * @param options The options of the patch. - * @property extensionInputStream Getter for the extension input stream of the patch. - * An extension is a precompiled DEX file that is merged into the patched app before this patch is executed. - * @param executeBlock The execution block of the patch. - * @param finalizeBlock The finalizing block of the patch. Called after all patches have been executed, - * in reverse order of execution. - * - * @constructor Create a new bytecode patch. - */ -class BytecodePatch internal constructor( - name: String?, - description: String?, - use: Boolean, - compatiblePackages: Set?, - dependencies: Set>, - options: Set>, - internal val extensionInputStream: Supplier?, - executeBlock: (BytecodePatchContext) -> Unit, - finalizeBlock: ((BytecodePatchContext) -> Unit)?, -) : Patch( - name, - description, - use, - dependencies, - compatiblePackages, - options, - executeBlock, - finalizeBlock, -) { - override fun execute(context: PatcherContext) = with(context.bytecodeContext) { - mergeExtension(this@BytecodePatch) - execute(this) - } - - override fun finalize(context: PatcherContext) = finalize(context.bytecodeContext) - - override fun toString() = name ?: "Bytecode${super.toString()}" -} - -/** - * A raw resource patch. - * - * @param name The name of the patch. - * If null, the patch is named "Patch" and will not be loaded by [loadPatches]. - * @param description The description of the patch. - * @param use Weather or not the patch should be used. - * @param compatiblePackages The packages the patch is compatible with. - * If null, the patch is compatible with all packages. - * @param dependencies Other patches this patch depends on. - * @param options The options of the patch. - * @param executeBlock The execution block of the patch. - * @param finalizeBlock The finalizing block of the patch. Called after all patches have been executed, - * in reverse order of execution. - * - * @constructor Create a new raw resource patch. - */ -class RawResourcePatch internal constructor( - name: String?, - description: String?, - use: Boolean, - compatiblePackages: Set?, - dependencies: Set>, - options: Set>, - executeBlock: (ResourcePatchContext) -> Unit, - finalizeBlock: ((ResourcePatchContext) -> Unit)?, -) : Patch( - name, - description, - use, - dependencies, - compatiblePackages, - options, - executeBlock, - finalizeBlock, -) { - override fun execute(context: PatcherContext) = execute(context.resourceContext) - - override fun finalize(context: PatcherContext) = finalize(context.resourceContext) - - override fun toString() = name ?: "RawResource${super.toString()}" -} - -/** - * A resource patch. - * - * @param name The name of the patch. - * If null, the patch is named "Patch" and will not be loaded by [loadPatches]. - * @param description The description of the patch. - * @param use Weather or not the patch should be used. - * @param compatiblePackages The packages the patch is compatible with. - * If null, the patch is compatible with all packages. - * @param dependencies Other patches this patch depends on. - * @param options The options of the patch. - * @param executeBlock The execution block of the patch. - * @param finalizeBlock The finalizing block of the patch. Called after all patches have been executed, - * in reverse order of execution. - * - * @constructor Create a new resource patch. - */ -class ResourcePatch internal constructor( - name: String?, - description: String?, - use: Boolean, - compatiblePackages: Set?, - dependencies: Set>, - options: Set>, - executeBlock: (ResourcePatchContext) -> Unit, - finalizeBlock: ((ResourcePatchContext) -> Unit)?, -) : Patch( - name, - description, - use, - dependencies, - compatiblePackages, - options, - executeBlock, - finalizeBlock, -) { - override fun execute(context: PatcherContext) = execute(context.resourceContext) - - override fun finalize(context: PatcherContext) = finalize(context.resourceContext) - - override fun toString() = name ?: "Resource${super.toString()}" -} - -/** - * A [Patch] builder. - * - * @param C The [PatchContext] to execute and finalize the patch with. - * @param name The name of the patch. - * If null, the patch is named "Patch" and will not be loaded by [loadPatches]. - * @param description The description of the patch. - * @param use Weather or not the patch should be used. - * @property compatiblePackages The packages the patch is compatible with. - * If null, the patch is compatible with all packages. - * @property dependencies Other patches this patch depends on. - * @property options The options of the patch. - * @property executionBlock The execution block of the patch. - * @property finalizeBlock The finalizing block of the patch. Called after all patches have been executed, - * in reverse order of execution. - * - * @constructor Create a new [Patch] builder. - */ -sealed class PatchBuilder>( - protected val name: String?, - protected val description: String?, - protected val use: Boolean, -) { - protected var compatiblePackages: MutableSet? = null - protected var dependencies = mutableSetOf>() - protected val options = mutableSetOf>() - - protected var executionBlock: ((C) -> Unit) = { } - protected var finalizeBlock: ((C) -> Unit)? = null - - /** - * Add an option to the patch. - * - * @return The added option. - */ - operator fun Option.invoke() = apply { - options += this - } - - /** - * Create a package a patch is compatible with. - * - * @param versions The versions of the package. - */ - operator fun String.invoke(vararg versions: String) = invoke(versions.toSet()) - - /** - * Create a package a patch is compatible with. - * - * @param versions The versions of the package. - */ - private operator fun String.invoke(versions: Set? = null) = this to versions - - /** - * Add packages the patch is compatible with. - * - * @param packages The packages the patch is compatible with. - */ - fun compatibleWith(vararg packages: Package) { - if (compatiblePackages == null) { - compatiblePackages = mutableSetOf() - } - - compatiblePackages!! += packages - } - - /** - * Set the compatible packages of the patch. - * - * @param packages The packages the patch is compatible with. - */ - fun compatibleWith(vararg packages: String) = compatibleWith(*packages.map { it() }.toTypedArray()) - - /** - * Add dependencies to the patch. - * - * @param patches The patches the patch depends on. - */ - fun dependsOn(vararg patches: Patch<*>) { - dependencies += patches - } - - /** - * Set the execution block of the patch. - * - * @param block The execution block of the patch. - */ - fun execute(block: C.() -> Unit) { - executionBlock = block - } - - /** - * Set the finalizing block of the patch. - * - * @param block The finalizing block of the patch. - */ - fun finalize(block: C.() -> Unit) { - finalizeBlock = block - } - - /** - * Build the patch. - * - * @return The built patch. - */ - internal abstract fun build(): Patch -} - -/** - * Builds a [Patch]. - * - * @param B The [PatchBuilder] to build the patch with. - * @param block The block to build the patch. - * - * @return The built [Patch]. - */ -private fun > B.buildPatch(block: B.() -> Unit = {}) = apply(block).build() - -/** - * A [BytecodePatchBuilder] builder. - * - * @param name The name of the patch. - * If null, the patch is named "Patch" and will not be loaded by [loadPatches]. - * @param description The description of the patch. - * @param use Weather or not the patch should be used. - * @property extensionInputStream Getter for the extension input stream of the patch. - * An extension is a precompiled DEX file that is merged into the patched app before this patch is executed. - * - * @constructor Create a new [BytecodePatchBuilder] builder. - */ -class BytecodePatchBuilder internal constructor( - name: String?, - description: String?, - use: Boolean, -) : PatchBuilder(name, description, use) { - internal var extensionInputStream: Supplier? = null - - /** - * Set the extension of the patch. - * - * @param extension The name of the extension resource. - */ - fun extendWith(extension: String) = apply { - // Should be the classloader which loaded the patch class. - val classLoader = Class.forName(Thread.currentThread().stackTrace[2].className).classLoader!! - - extensionInputStream = Supplier { - classLoader.getResourceAsStream(extension) - ?: throw PatchException("Extension \"$extension\" not found") - } - } - - override fun build() = BytecodePatch( - name, - description, - use, - compatiblePackages, - dependencies, - options, - extensionInputStream, - executionBlock, - finalizeBlock, - ) -} - -/** - * Create a new [BytecodePatch]. - * - * @param name The name of the patch. - * If null, the patch is named "Patch" and will not be loaded by [loadPatches]. - * @param description The description of the patch. - * @param use Weather or not the patch should be used. - * @param block The block to build the patch. - * - * @return The created [BytecodePatch]. - */ -fun bytecodePatch( - name: String? = null, - description: String? = null, - use: Boolean = true, - block: BytecodePatchBuilder.() -> Unit = {}, -) = BytecodePatchBuilder(name, description, use).buildPatch(block) as BytecodePatch - -/** - * Create a [ReadOnlyProperty] that creates a new [BytecodePatch] with the name of the property. - * - * @param description The description of the patch. - * @param use Weather or not the patch should be used. - * @param block The block to build the patch. - * - * @return The created [ReadOnlyProperty] that creates a new [BytecodePatch]. - */ -fun gettingBytecodePatch( - description: String? = null, - use: Boolean = true, - block: BytecodePatchBuilder.() -> Unit = {}, -) = ReadOnlyProperty { _, property -> bytecodePatch(property.name, description, use, block) } - -/** - * A [RawResourcePatch] builder. - * - * @param name The name of the patch. - * If null, the patch is named "Patch" and will not be loaded by [loadPatches]. - * @param description The description of the patch. - * @param use Weather or not the patch should be used. - * - * @constructor Create a new [RawResourcePatch] builder. - */ -class RawResourcePatchBuilder internal constructor( - name: String?, - description: String?, - use: Boolean, -) : PatchBuilder(name, description, use) { - override fun build() = RawResourcePatch( - name, - description, - use, - compatiblePackages, - dependencies, - options, - executionBlock, - finalizeBlock, - ) -} - -/** - * Create a new [RawResourcePatch]. - * - * @param name The name of the patch. - * If null, the patch is named "Patch" and will not be loaded by [loadPatches]. - * @param description The description of the patch. - * @param use Weather or not the patch should be used. - * @param block The block to build the patch. - * @return The created [RawResourcePatch]. - */ -fun rawResourcePatch( - name: String? = null, - description: String? = null, - use: Boolean = true, - block: RawResourcePatchBuilder.() -> Unit = {}, -) = RawResourcePatchBuilder(name, description, use).buildPatch(block) as RawResourcePatch - -/** - * Create a [ReadOnlyProperty] that creates a new [RawResourcePatch] with the name of the property. - * - * @param description The description of the patch. - * @param use Weather or not the patch should be used. - * @param block The block to build the patch. - * - * @return The created [ReadOnlyProperty] that creates a new [RawResourcePatch]. - */ -fun gettingRawResourcePatch( - description: String? = null, - use: Boolean = true, - block: RawResourcePatchBuilder.() -> Unit = {}, -) = ReadOnlyProperty { _, property -> rawResourcePatch(property.name, description, use, block) } - -/** - * A [ResourcePatch] builder. - * - * @param name The name of the patch. - * If null, the patch is named "Patch" and will not be loaded by [loadPatches]. - * @param description The description of the patch. - * @param use Weather or not the patch should be used. - * - * @constructor Create a new [ResourcePatch] builder. - */ -class ResourcePatchBuilder internal constructor( - name: String?, - description: String?, - use: Boolean, -) : PatchBuilder(name, description, use) { - override fun build() = ResourcePatch( - name, - description, - use, - compatiblePackages, - dependencies, - options, - executionBlock, - finalizeBlock, - ) -} - -/** - * Create a new [ResourcePatch]. - * - * @param name The name of the patch. - * If null, the patch is named "Patch" and will not be loaded by [loadPatches]. - * @param description The description of the patch. - * @param use Weather or not the patch should be used. - * @param block The block to build the patch. - * - * @return The created [ResourcePatch]. - */ -fun resourcePatch( - name: String? = null, - description: String? = null, - use: Boolean = true, - block: ResourcePatchBuilder.() -> Unit = {}, -) = ResourcePatchBuilder(name, description, use).buildPatch(block) as ResourcePatch - -/** - * Create a [ReadOnlyProperty] that creates a new [ResourcePatch] with the name of the property. - * - * @param description The description of the patch. - * @param use Weather or not the patch should be used. - * @param block The block to build the patch. - * - * @return The created [ReadOnlyProperty] that creates a new [ResourcePatch]. - */ -fun gettingResourcePatch( - description: String? = null, - use: Boolean = true, - block: ResourcePatchBuilder.() -> Unit = {}, -) = ReadOnlyProperty { _, property -> resourcePatch(property.name, description, use, block) } - -/** - * An exception thrown when patching. - * - * @param errorMessage The exception message. - * @param cause The corresponding [Throwable]. - */ -class PatchException(errorMessage: String?, cause: Throwable?) : Exception(errorMessage, cause) { - constructor(errorMessage: String) : this(errorMessage, null) - constructor(cause: Throwable) : this(cause.message, cause) -} - -/** - * A result of executing a [Patch]. - * - * @param patch The [Patch] that was executed. - * @param exception The [PatchException] thrown, if any. - */ -class PatchResult internal constructor(val patch: Patch<*>, val exception: PatchException? = null) - -/** - * A collection of patches loaded from patches files. - * - * @property patchesByFile The patches mapped by their patches file. - */ -class Patches internal constructor(val patchesByFile: Map>>) : Set> -by patchesByFile.values.flatten().toSet() - -internal fun loadPatches( - patchesFiles: Set, - getBinaryClassNames: (patchesFile: File) -> List, - classLoader: ClassLoader, -): Patches { - fun Member.isUsable(): Boolean { - if (this is Method && parameterCount != 0) return false - - return Modifier.isStatic(modifiers) && Modifier.isPublic(modifiers) - } - - fun Class<*>.getPatchFields() = fields.filter { field -> - field.type.isPatch && field.isUsable() - }.map { field -> - field.get(null) as Patch<*> - } - - fun Class<*>.getPatchMethods() = methods.filter { method -> - method.returnType.isPatch && method.parameterCount == 0 && method.isUsable() - }.map { method -> - method.invoke(null) as Patch<*> - } - - return Patches(patchesFiles.associateWith { file -> - getBinaryClassNames(file).map { - classLoader.loadClass(it) - }.flatMap { clazz -> - clazz.getPatchMethods() + clazz.getPatchFields() - }.filter { it.name != null }.toSet() - }) -} - -expect fun loadPatches(patchesFiles: Set): Patches - -internal expect val Class<*>.isPatch: Boolean diff --git a/core/src/commonMain/kotlin/app/revanced/patcher/patch/ResourcePatchContext.kt b/core/src/commonMain/kotlin/app/revanced/patcher/patch/ResourcePatchContext.kt deleted file mode 100644 index 69fbc6e..0000000 --- a/core/src/commonMain/kotlin/app/revanced/patcher/patch/ResourcePatchContext.kt +++ /dev/null @@ -1,235 +0,0 @@ -package app.revanced.patcher.patch - -import app.revanced.patcher.InternalApi -import app.revanced.patcher.PackageMetadata -import app.revanced.patcher.PatcherConfig -import app.revanced.patcher.PatcherResult -import app.revanced.patcher.util.Document -import brut.androlib.AaptInvoker -import brut.androlib.ApkDecoder -import brut.androlib.apk.UsesFramework -import brut.androlib.res.Framework -import brut.androlib.res.ResourcesDecoder -import brut.androlib.res.decoder.AndroidManifestPullStreamDecoder -import brut.androlib.res.decoder.AndroidManifestResourceParser -import brut.androlib.res.xml.ResXmlUtils -import brut.directory.ExtFile -import java.io.InputStream -import java.io.OutputStream -import java.io.resolve -import java.nio.file.Files -import java.util.logging.Logger -import kotlin.reflect.jvm.jvmName - -/** - * A context for patches containing the current state of resources. - * - * @param packageMetadata The [PackageMetadata] of the apk file. - * @param config The [PatcherConfig] used to create this context. - */ -class ResourcePatchContext internal constructor( - private val packageMetadata: PackageMetadata, - private val config: PatcherConfig, -) : PatchContext { - private val logger = Logger.getLogger(ResourcePatchContext::class.jvmName) - - /** - * Read a document from an [InputStream]. - */ - fun document(inputStream: InputStream) = Document(inputStream) - - /** - * Read and write documents in the [PatcherConfig.apkFiles]. - */ - fun document(path: String) = Document(get(path)) - - /** - * Set of resources from [PatcherConfig.apkFiles] to delete. - */ - private val deleteResources = mutableSetOf() - - /** - * Decode resources of [PatcherConfig.apkFile]. - * - * @param mode The [ResourceMode] to use. - */ - internal fun decodeResources(mode: ResourceMode) = with(packageMetadata.apkInfo) { - config.initializeTemporaryFilesDirectories() - - // Needed to decode resources. - val resourcesDecoder = ResourcesDecoder(config.resourceConfig, this) - - if (mode == ResourceMode.FULL) { - logger.info("Decoding resources") - - resourcesDecoder.decodeResources(config.apkFiles) - resourcesDecoder.decodeManifest(config.apkFiles) - - // Needed to record uncompressed files. - ApkDecoder(this, config.resourceConfig).recordUncompressedFiles(resourcesDecoder.resFileMapping) - - usesFramework = - UsesFramework().apply { - ids = resourcesDecoder.resTable.listFramePackages().map { it.id } - } - } else { - logger.info("Decoding app manifest") - - // Decode manually instead of using resourceDecoder.decodeManifest - // because it does not support decoding to an OutputStream. - AndroidManifestPullStreamDecoder( - AndroidManifestResourceParser(resourcesDecoder.resTable), - resourcesDecoder.newXmlSerializer(), - ).decode( - apkFile.directory.getFileInput("AndroidManifest.xml"), - // Older Android versions do not support OutputStream.nullOutputStream() - object : OutputStream() { - override fun write(b: Int) { // Do nothing. - } - }, - ) - - // Get the package name and version from the manifest using the XmlPullStreamDecoder. - // AndroidManifestPullStreamDecoder.decode() sets metadata.apkInfo. - packageMetadata.let { metadata -> - metadata.packageName = resourcesDecoder.resTable.packageRenamed - versionInfo.let { - metadata.packageVersion = it.versionName ?: it.versionCode - } - - /* - The ResTable if flagged as sparse if the main package is not loaded, which is the case here, - because ResourcesDecoder.decodeResources loads the main package - and not AndroidManifestPullStreamDecoder.decode. - See ARSCDecoder.readTableType for more info. - - Set this to false again to prevent the ResTable from being flagged as sparse falsely. - */ - metadata.apkInfo.sparseResources = false - } - } - } - - /** - * Compile resources in [PatcherConfig.apkFiles]. - * - * @return The [PatcherResult.PatchedResources]. - */ - @InternalApi - override fun get(): PatcherResult.PatchedResources? { - if (config.resourceMode == ResourceMode.NONE) return null - - logger.info("Compiling modified resources") - - val resources = config.patchedFiles.resolve("resources").also { it.mkdirs() } - - val resourcesApkFile = - if (config.resourceMode == ResourceMode.FULL) { - resources.resolve("resources.apk").apply { - // Compile the resources.apk file. - AaptInvoker( - config.resourceConfig, - packageMetadata.apkInfo, - ).invoke( - resources.resolve("resources.apk"), - config.apkFiles.resolve("AndroidManifest.xml").also { - ResXmlUtils.fixingPublicAttrsInProviderAttributes(it) - }, - config.apkFiles.resolve("res"), - null, - null, - packageMetadata.apkInfo.usesFramework.let { usesFramework -> - usesFramework.ids.map { id -> - Framework(config.resourceConfig).getFrameworkApk(id, usesFramework.tag) - }.toTypedArray() - }, - ) - } - } else { - null - } - - val otherFiles = - config.apkFiles.listFiles()!!.filter { - // Excluded because present in resources.other. - // TODO: We are reusing config.apkFiles as a temporarily directory for extracting resources. - // This is not ideal as it could conflict with files such as the ones that we filter here. - // The problem is that ResourcePatchContext#get returns a File relative to config.apkFiles, - // and we need to extract files to that directory. - // A solution would be to use config.apkFiles as the working directory for the patching process. - // Once all patches have been executed, we can move the decoded resources to a new directory. - // The filters wouldn't be needed anymore. - // For now, we assume that the files we filter here are not needed for the patching process. - it.name != "AndroidManifest.xml" && - it.name != "res" && - // Generated by Androlib. - it.name != "build" - } - - val otherResourceFiles = - if (otherFiles.isNotEmpty()) { - // Move the other resources files. - resources.resolve("other").also { it.mkdirs() }.apply { - otherFiles.forEach { file -> - Files.move(file.toPath(), resolve(file.name).toPath()) - } - } - } else { - null - } - - return PatcherResult.PatchedResources( - resourcesApkFile, - otherResourceFiles, - packageMetadata.apkInfo.doNotCompress?.toSet() ?: emptySet(), - deleteResources, - ) - } - - /** - * Get a file from [PatcherConfig.apkFiles]. - * - * @param path The path of the file. - * @param copy Whether to copy the file from [PatcherConfig.apkFile] if it does not exist yet in [PatcherConfig.apkFiles]. - */ - operator fun get( - path: String, - copy: Boolean = true, - ) = config.apkFiles.resolve(path).apply { - if (copy && !exists()) { - with(ExtFile(config.apkFile).directory) { - if (containsFile(path) || containsDir(path)) { - copyToDir(config.apkFiles, path) - } - } - } - } - - /** - * Mark a file for deletion when the APK is rebuilt. - * - * @param name The name of the file to delete. - */ - fun delete(name: String) = deleteResources.add(name) - - /** - * How to handle resources decoding and compiling. - */ - internal enum class ResourceMode { - /** - * Decode and compile all resources. - */ - FULL, - - /** - * Only extract resources from the APK. - * The AndroidManifest.xml and resources inside /res are not decoded or compiled. - */ - RAW_ONLY, - - /** - * Do not decode or compile any resources. - */ - NONE, - } -} diff --git a/core/src/jvmTest/kotlin/app/revanced/patcher/PatcherTest.kt b/core/src/jvmTest/kotlin/app/revanced/patcher/PatcherTest.kt deleted file mode 100644 index c64d761..0000000 --- a/core/src/jvmTest/kotlin/app/revanced/patcher/PatcherTest.kt +++ /dev/null @@ -1,401 +0,0 @@ -package app.revanced.patcher - -import app.revanced.patcher.extensions.toInstructions -import app.revanced.patcher.patch.* -import com.android.tools.smali.dexlib2.Opcode -import com.android.tools.smali.dexlib2.Opcodes -import com.android.tools.smali.dexlib2.iface.DexFile -import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction -import com.android.tools.smali.dexlib2.immutable.ImmutableClassDef -import com.android.tools.smali.dexlib2.immutable.ImmutableMethod -import com.android.tools.smali.dexlib2.immutable.ImmutableMethodImplementation -import io.mockk.every -import io.mockk.just -import io.mockk.mockk -import io.mockk.mockkStatic -import io.mockk.runs -import io.mockk.spyk -import kotlinx.coroutines.flow.toList -import kotlinx.coroutines.runBlocking -import lanchon.multidexlib2.MultiDexIO -import org.junit.jupiter.api.Assertions.assertNull -import org.junit.jupiter.api.BeforeAll -import org.junit.jupiter.api.assertAll -import org.junit.jupiter.api.assertThrows -import java.util.logging.Logger -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNotNull -import kotlin.test.assertNull - -internal object PatcherTest { - private lateinit var patcher: Patcher - private lateinit var patcherContext: PatcherContext - - @JvmStatic - @BeforeAll - fun setUp() { - patcher = mockk { - // Can't mock private fields, until https://github.com/mockk/mockk/issues/1244 is resolved. - setPrivateField( - "config", - mockk { - every { resourceMode } returns ResourcePatchContext.ResourceMode.NONE - }, - ) - setPrivateField( - "logger", - Logger.getAnonymousLogger(), - ) - - every { this@mockk() } answers { callOriginal() } - } - - val classDefs = mutableSetOf( - ImmutableClassDef( - "class", - 0, - null, - null, - null, - null, - null, - listOf( - ImmutableMethod( - "class", - "method", - emptyList(), - "V", - 0, - null, - null, - ImmutableMethodImplementation( - 2, - """ - const-string v0, "Hello, World!" - iput-object v0, p0, Ljava/lang/System;->out:Ljava/io/PrintStream; - iget-object v0, p0, Ljava/lang/System;->out:Ljava/io/PrintStream; - return-void - const-string v0, "This is a test." - return-object v0 - invoke-virtual { p0, v0 }, Ljava/io/PrintStream;->println(Ljava/lang/String;)V - invoke-static { p0 }, Ljava/lang/System;->currentTimeMillis()J - check-cast p0, Ljava/io/PrintStream; - """.toInstructions(), - null, - null - ), - ), - ), - ) - ) - - patcherContext = mockk { - every { bytecodeContext } returns mockk context@{ - every { config } returns mockk { - every { apkFile } returns mockk() - } - - mockkStatic(MultiDexIO::readDexFile) - every { - MultiDexIO.readDexFile( - any(), - any(), - any(), - any(), - any() - ) - } returns mockk { - every { classes } returns classDefs - every { opcodes } returns Opcodes.getDefault() - } - every { this@context.classDefs } returns ClassDefs().apply { initializeCache() } - every { mergeExtension(any()) } just runs - } - } - - every { patcher.context } returns patcherContext - } - - @Test - fun `executes patches in correct order`() { - val executed = mutableListOf() - - val patches = setOf( - bytecodePatch { execute { executed += "1" } }, - bytecodePatch { - dependsOn( - bytecodePatch { - execute { executed += "2" } - finalize { executed += "-2" } - }, - bytecodePatch { execute { executed += "3" } }, - ) - - execute { executed += "4" } - finalize { executed += "-1" } - }, - ) - - assert(executed.isEmpty()) - - patches() - - assertEquals( - listOf("1", "2", "3", "4", "-1", "-2"), - executed, - "Expected patches to be executed in correct order.", - ) - } - - @Test - fun `handles execution of patches correctly when exceptions occur`() { - val executed = mutableListOf() - - infix fun Patch<*>.produces(equals: List) { - val patches = setOf(this) - - try { - patches() - } catch (_: PatchException) { - // Swallow expected exceptions for testing purposes. - } - - assertEquals(equals, executed, "Expected patches to be executed in correct order.") - - executed.clear() - } - - // No patches execute successfully, - // because the dependency patch throws an exception inside the execute block. - bytecodePatch { - dependsOn( - bytecodePatch { - execute { throw PatchException("1") } - finalize { executed += "-2" } - }, - ) - - execute { executed += "2" } - finalize { executed += "-1" } - } produces emptyList() - - // The dependency patch is executed successfully, - // because only the dependant patch throws an exception inside the finalize block. - // Patches that depend on a failed patch should not be executed, - // but patches that are depended on by a failed patch should be executed. - bytecodePatch { - dependsOn( - bytecodePatch { - execute { executed += "1" } - finalize { executed += "-2" } - }, - ) - - execute { throw PatchException("2") } - finalize { executed += "-1" } - } produces listOf("1", "-2") - - // Because the finalize block of the dependency patch is executed after the finalize block of the dependant patch, - // the dependant patch executes successfully, but the dependency patch raises an exception in the finalize block. - bytecodePatch { - dependsOn( - bytecodePatch { - execute { executed += "1" } - finalize { throw PatchException("-2") } - }, - ) - - execute { executed += "2" } - finalize { executed += "-1" } - } produces listOf("1", "2", "-1") - - // The dependency patch is executed successfully, - // because the dependant patch raises an exception in the finalize block. - // Patches that depend on a failed patch should not be executed, - // but patches that are depended on by a failed patch should be executed. - bytecodePatch { - dependsOn( - bytecodePatch { - execute { executed += "1" } - finalize { executed += "-2" } - }, - ) - - execute { executed += "2" } - finalize { throw PatchException("-1") } - } produces listOf("1", "2", "-2") - } - - @Test - fun `throws if unmatched fingerprint match is used`() { - val patch = bytecodePatch { - execute { - // Fingerprint can never match. - val fingerprint = fingerprint { - strings("doesnt exist") - } - - // Throws, because the fingerprint can't be matched. - fingerprint.patternMatch - } - } - - assertThrows("Expected an exception because the fingerprint can't match.") { patch() } - } - - @Test - fun `matcher finds indices correctly`() { - val iterable = (1..10).toList() - val matcher = indexedMatcher() - - matcher.apply { - +head { this > 5 } - } - assertFalse( - matcher(iterable), - "Should not match at any other index than first" - ) - matcher.clear() - - matcher.apply { +head { this == 1 } }(iterable) - assertEquals( - listOf(0), - matcher.indices, - "Should match at first index." - ) - matcher.clear() - - matcher.apply { add { _, _ -> this > 0 } }(iterable) - assertEquals(1, matcher.indices.size, "Should only match once.") - matcher.clear() - - matcher.apply { add { _, _ -> this == 2 } }(iterable) - assertEquals( - listOf(1), - matcher.indices, - "Should find the index correctly." - ) - matcher.clear() - - matcher.apply { - +head { this == 1 } - add { _, _ -> this == 2 } - add { _, _ -> this == 4 } - }(iterable) - assertEquals( - listOf(0, 1, 3), - matcher.indices, - "Should match 1, 2 and 4 at indices 0, 1 and 3." - ) - matcher.clear() - - matcher.apply { - +after { this == 1 } - }(iterable) - assertEquals( - listOf(0), - matcher.indices, - "Should match index 0 after nothing" - ) - matcher.clear() - - matcher.apply { - +after(2..Int.MAX_VALUE) { this == 1 } - } - assertFalse( - matcher(iterable), - "Should not match, because 1 is out of range" - ) - matcher.clear() - - matcher.apply { - +after(1..1) { this == 2 } - } - assertFalse( - matcher(iterable), - "Should not match, because 2 is at index 1" - ) - matcher.clear() - - matcher.apply { - +head { this == 1 } - +after(2..5) { this == 4 } - add { _, _ -> this == 8 } - add { _, _ -> this == 9 } - }(iterable) - assertEquals( - listOf(0, 3, 7, 8), - matcher.indices, - "Should match indices correctly." - ) - } - - @Test - fun `matches via composite`() { - fun composite(fail: Boolean = false) = firstMethodComposite { - name("method") - definingClass("class") - - if (fail) returnType("doesnt exist") - - instructions( - head(Opcode.CONST_STRING()), - `is`(), - noneOf(registers()), - string("test", String::contains), - after(1..3, allOf(Opcode.INVOKE_VIRTUAL(), registers(1, 0))), - allOf(), - type("PrintStream;", String::endsWith) - ) - } - - with(patcher.context.bytecodeContext) { - assertNotNull(composite().methodOrNull) { - "Expected to find a method matching the composite fingerprint." - } - assertNull(composite(fail = true).methodOrNull) { - "Expected to not find a method matching the composite fingerprint." - } - } - } - - @Test - fun `matches fingerprint`() { - val fingerprint = fingerprint { returns("V") } - val fingerprint2 = fingerprint { returns("V") } - val fingerprint3 = fingerprint { returns("V") } - - with(patcher.context.bytecodeContext) { - assertAll( - "Expected fingerprints to match.", - { assertNotNull(fingerprint.matchOrNull(this.classDefs.first().methods.first())) }, - { assertNotNull(fingerprint2.matchOrNull(this.classDefs.first())) }, - { assertNotNull(fingerprint3.originalClassDefOrNull) }, - ) - } - } - - private operator fun Set>.invoke(): List { - // TODO: Ideally most of this mocking could be moved to setUp, - // but by doing so the mocking breaks and it is not clear why. - - every { patcherContext.executablePatches } returns toMutableSet() - - return runBlocking { - patcher().toList().also { results -> - results.firstOrNull { result -> result.exception != null }?.let { result -> throw result.exception!! } - } - } - } - - private operator fun Patch<*>.invoke() = setOf(this)().first() - - private fun Any.setPrivateField(field: String, value: Any) { - this::class.java.getDeclaredField(field).apply { - this.isAccessible = true - set(this@setPrivateField, value) - } - } -} diff --git a/core/src/jvmTest/kotlin/app/revanced/patcher/patch/PatchLoaderTest.kt b/core/src/jvmTest/kotlin/app/revanced/patcher/patch/PatchLoaderTest.kt deleted file mode 100644 index fd79b00..0000000 --- a/core/src/jvmTest/kotlin/app/revanced/patcher/patch/PatchLoaderTest.kt +++ /dev/null @@ -1,70 +0,0 @@ -@file:Suppress("unused") - -package app.revanced.patcher.patch - -import io.mockk.mockk -import org.junit.jupiter.api.Test -import java.io.File -import kotlin.reflect.KFunction -import kotlin.reflect.full.companionObject -import kotlin.reflect.full.declaredFunctions -import kotlin.reflect.jvm.isAccessible -import kotlin.reflect.jvm.javaField -import kotlin.test.assertEquals - -// region Test patches. - -// Not loaded, because it's unnamed. -val publicUnnamedPatch = bytecodePatch { -} - -// Loaded, because it's named. -val publicPatch = bytecodePatch("Public") { -} - -// Not loaded, because it's private. -private val privateUnnamedPatch = bytecodePatch { -} - -// Not loaded, because it's private. -private val privatePatch = bytecodePatch("Private") { -} - -// Not loaded, because it's unnamed. -fun publicUnnamedPatchFunction() = publicUnnamedPatch - -// Loaded, because it's named. -fun publicNamedPatchFunction() = bytecodePatch("Public") { } - -// Not loaded, because it's parameterized. -fun parameterizedFunction(@Suppress("UNUSED_PARAMETER") param: Any) = publicNamedPatchFunction() - -// Not loaded, because it's private. -private fun privateUnnamedPatchFunction() = privateUnnamedPatch - -// Not loaded, because it's private. -private fun privateNamedPatchFunction() = privatePatch - -// endregion - -internal object PatchLoaderTest { - private val TEST_PATCHES_CLASS = ::publicPatch.javaField!!.declaringClass.name - private val TEST_PATCHES_CLASS_LOADER = ::publicPatch.javaClass.classLoader - - @Test - fun `loads patches correctly`() { - val patches = loadPatches( - setOf(mockk()), - { listOf(TEST_PATCHES_CLASS) }, - TEST_PATCHES_CLASS_LOADER - ) - - assertEquals( - 2, - patches.size, - "Expected 2 patches to be loaded, " + - "because there's only two named patches declared as public static fields " + - "or returned by public static and non-parametrized methods.", - ) - } -} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4f6dfaf..bc0e087 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,10 +3,10 @@ agp = "8.12.3" android-compileSdk = "36" android-minSdk = "26" -kotlin = "2.2.21" +kotlin = "2.3.0" apktool-lib = "2.10.1.1" kotlinx-coroutines-core = "1.10.2" -mockk = "1.14.6" +mockk = "1.14.7" multidexlib2 = "3.0.3.r3" # Tracking https://github.com/google/smali/issues/64. #noinspection GradleDependency diff --git a/matching/build.gradle.kts b/matching/build.gradle.kts new file mode 100644 index 0000000..eae807a --- /dev/null +++ b/matching/build.gradle.kts @@ -0,0 +1,85 @@ +import org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation + +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.vanniktech.mavenPublish) +} + +group = "app.revanced" + +kotlin { + @OptIn(ExperimentalAbiValidation::class) + abiValidation { + enabled = true + } + + jvm() + + sourceSets { + commonMain.dependencies { + implementation(libs.multidexlib2) + implementation(libs.smali) + implementation(project(":patcher")) + } + jvmTest.dependencies { + implementation(libs.mockk) + implementation(libs.kotlin.test) + implementation(project(":tests")) + } + } + + compilerOptions { + freeCompilerArgs.addAll( + "-Xexplicit-backing-fields", + "-Xcontext-parameters" + ) + } +} + +tasks { + named("jvmTest") { + useJUnitPlatform() + } +} + +mavenPublishing { + publishing { + repositories { + maven { + name = "githubPackages" + url = uri("https://maven.pkg.github.com/revanced/revanced-patcher") + credentials(PasswordCredentials::class) + } + } + } + + signAllPublications() + extensions.getByType().useGpgCmd() + + coordinates(group.toString(), project.name, version.toString()) + + pom { + name = "ReVanced Patcher Matching API" + description = "Matching API used by ReVanced." + inceptionYear = "2022" + url = "https://revanced.app" + licenses { + license { + name = "GNU General Public License v3.0" + url = "https://www.gnu.org/licenses/gpl-3.0.en.html" + } + } + developers { + developer { + id = "ReVanced" + name = "ReVanced" + email = "contact@revanced.app" + } + } + scm { + connection = "scm:git:git://github.com/revanced/revanced-patcher.git" + developerConnection = "scm:git:git@github.com:revanced/revanced-patcher.git" + url = "https://github.com/revanced/revanced-patcher" + } + } +} \ No newline at end of file diff --git a/matching/src/commonMain/kotlin/app/revanced/patcher/Matching.kt b/matching/src/commonMain/kotlin/app/revanced/patcher/Matching.kt new file mode 100644 index 0000000..1dc5b53 --- /dev/null +++ b/matching/src/commonMain/kotlin/app/revanced/patcher/Matching.kt @@ -0,0 +1,653 @@ +@file:Suppress("unused") + +package app.revanced.patcher + +import app.revanced.patcher.extensions.* +import app.revanced.patcher.patch.BytecodePatchContext +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.HiddenApiRestriction +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.* +import com.android.tools.smali.dexlib2.iface.Annotation +import com.android.tools.smali.dexlib2.iface.instruction.* +import com.android.tools.smali.dexlib2.mutable.MutableMethod +import com.android.tools.smali.dexlib2.util.MethodUtil +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KProperty + +fun Iterable.anyClassDef(predicate: ClassDef.() -> Boolean) = any(predicate) + +fun ClassDef.anyMethod(predicate: Method.() -> Boolean) = methods.any(predicate) + +fun ClassDef.anyDirectMethod(predicate: Method.() -> Boolean) = directMethods.any(predicate) + +fun ClassDef.anyVirtualMethod(predicate: Method.() -> Boolean) = virtualMethods.any(predicate) + +fun ClassDef.anyField(predicate: Field.() -> Boolean) = fields.any(predicate) + +fun ClassDef.anyInstanceField(predicate: Field.() -> Boolean) = instanceFields.any(predicate) + +fun ClassDef.anyStaticField(predicate: Field.() -> Boolean) = staticFields.any(predicate) + +fun ClassDef.anyInterface(predicate: String.() -> Boolean) = interfaces.any(predicate) + +fun ClassDef.anyAnnotation(predicate: Annotation.() -> Boolean) = + annotations.any(predicate) + +fun Method.implementation(predicate: MethodImplementation.() -> Boolean) = implementation?.predicate() ?: false + +fun Method.anyParameter(predicate: MethodParameter.() -> Boolean) = parameters.any(predicate) + +fun Method.anyParameterType(predicate: CharSequence.() -> Boolean) = parameterTypes.any(predicate) + +fun Method.anyAnnotation(predicate: Annotation.() -> Boolean) = annotations.any(predicate) + +fun Method.anyHiddenApiRestriction(predicate: HiddenApiRestriction.() -> Boolean) = hiddenApiRestrictions.any(predicate) + +fun MethodImplementation.anyInstruction(predicate: Instruction.() -> Boolean) = instructions.any(predicate) + +fun MethodImplementation.anyTryBlock(predicate: TryBlock.() -> Boolean) = tryBlocks.any(predicate) + +fun MethodImplementation.anyDebugItem(predicate: Any.() -> Boolean) = debugItems.any(predicate) + +fun Iterable.anyInstruction(predicate: Instruction.() -> Boolean) = any(predicate) + +private typealias ClassDefPredicate = context(PredicateContext) ClassDef.() -> Boolean + +private typealias MethodPredicate = context(PredicateContext) Method.() -> Boolean + +fun BytecodePatchContext.firstClassDefOrNull( + type: String? = null, predicate: ClassDefPredicate = { true } +) = with(PredicateContext()) { + if (type == null) classDefs.firstOrNull { it.predicate() } + else classDefs[type]?.takeIf { it.predicate() } +} + +fun BytecodePatchContext.firstClassDef( + type: String? = null, + predicate: ClassDefPredicate = { true } +) = requireNotNull(firstClassDefOrNull(type, predicate)) + +fun BytecodePatchContext.firstClassDefMutableOrNull( + type: String? = null, + predicate: ClassDefPredicate = { true } +) = firstClassDefOrNull(type, predicate)?.let { classDefs.getOrReplaceMutable(it) } + +fun BytecodePatchContext.firstClassDefMutable( + type: String? = null, + predicate: ClassDefPredicate = { true } +) = requireNotNull(firstClassDefMutableOrNull(type, predicate)) + +fun BytecodePatchContext.firstMethodOrNull( + vararg strings: String, + predicate: MethodPredicate = { true }, +): Method? = with(PredicateContext()) { + if (strings.isEmpty()) + return classDefs.asSequence().flatMap { it.methods.asSequence() }.firstOrNull { it.predicate() } + + val methodsWithStrings = strings.mapNotNull { classDefs.methodsByString[it] } + if (methodsWithStrings.size != strings.size) return null + + return methodsWithStrings.minBy { it.size }.firstOrNull { method -> + val containsAllOtherStrings = methodsWithStrings.all { method in it } + containsAllOtherStrings && method.predicate() + } +} + +fun BytecodePatchContext.firstMethod( + vararg strings: String, + predicate: MethodPredicate = { true }, +) = requireNotNull(firstMethodOrNull(*strings, predicate = predicate)) + +fun BytecodePatchContext.firstMethodMutableOrNull( + vararg strings: String, + predicate: MethodPredicate = { true }, +) = firstMethodOrNull(*strings, predicate = predicate)?.let { method -> + firstClassDefMutable(method.definingClass).methods.first { + MethodUtil.methodSignaturesMatch(method, it) + } +} + +fun BytecodePatchContext.firstMethodMutable( + vararg strings: String, predicate: MethodPredicate = { true } +) = requireNotNull(firstMethodMutableOrNull(*strings, predicate = predicate)) + +fun gettingFirstClassDefOrNull( + type: String? = null, predicate: ClassDefPredicate = { true } +) = cachedReadOnlyProperty { firstClassDefOrNull(type, predicate) } + +fun gettingFirstClassDef( + type: String? = null, predicate: ClassDefPredicate = { true } +) = cachedReadOnlyProperty { firstClassDef(type, predicate) } + +fun gettingFirstClassDefMutableOrNull( + type: String? = null, predicate: ClassDefPredicate = { true } +) = cachedReadOnlyProperty { firstClassDefMutableOrNull(type, predicate) } + +fun gettingFirstClassDefMutable( + type: String? = null, predicate: ClassDefPredicate = { true } +) = cachedReadOnlyProperty { firstClassDefMutable(type, predicate) } + +fun gettingFirstMethodOrNull( + vararg strings: String, + predicate: MethodPredicate = { true }, +) = cachedReadOnlyProperty { firstMethodOrNull(*strings, predicate = predicate) } + +fun gettingFirstMethod( + vararg strings: String, + predicate: MethodPredicate = { true }, +) = cachedReadOnlyProperty { firstMethod(*strings, predicate = predicate) } + +fun gettingFirstMethodMutableOrNull( + vararg strings: String, + predicate: MethodPredicate = { true }, +) = cachedReadOnlyProperty { firstMethodMutableOrNull(*strings, predicate = predicate) } + +fun gettingFirstMethodMutable( + vararg strings: String, + predicate: MethodPredicate = { true }, +) = cachedReadOnlyProperty { firstMethodMutable(*strings, predicate = predicate) } + +class PredicateContext internal constructor() : MutableMap by mutableMapOf() + +// region Matcher + +// region IndexedMatcher + +fun indexedMatcher() = IndexedMatcher() + +fun indexedMatcher(build: IndexedMatcher.() -> Unit) = + IndexedMatcher().apply(build) + +fun Iterable.matchIndexed(build: IndexedMatcher.() -> Unit) = + indexedMatcher(build)(this) + +context(_: PredicateContext) +fun Iterable.rememberedMatchIndexed(key: Any, build: IndexedMatcher.() -> Unit) = + indexedMatcher()(key, this, build) + +context(_: IndexedMatcher) +fun head( + predicate: T.(lastMatchedIndex: Int, currentIndex: Int) -> Boolean +): T.(Int, Int) -> Boolean = { lastMatchedIndex, currentIndex -> + currentIndex == 0 && predicate(lastMatchedIndex, currentIndex) +} + +context(_: IndexedMatcher) +fun head(predicate: T.() -> Boolean): T.(Int, Int) -> Boolean = + head { _, _ -> predicate() } + +context(matcher: IndexedMatcher) +fun after( + range: IntRange = 1..1, + predicate: T.(lastMatchedIndex: Int, currentIndex: Int) -> Boolean +): T.(Int, Int) -> Boolean = predicate@{ lastMatchedIndex, currentIndex -> + val distance = currentIndex - lastMatchedIndex + + matcher.nextIndex = when { + distance < range.first -> lastMatchedIndex + range.first + distance > range.last -> -1 + else -> return@predicate predicate(lastMatchedIndex, currentIndex) + } + + false +} + +context(_: IndexedMatcher) +fun after(range: IntRange = 1..1, predicate: T.() -> Boolean) = + after(range) { _, _ -> predicate() } + +context(matcher: IndexedMatcher) +operator fun (T.(Int, Int) -> Boolean).unaryPlus() = matcher.add(this) + +class IndexedMatcher : Matcher Boolean>() { + val indices: List + field = mutableListOf() + + private var lastMatchedIndex = -1 + private var currentIndex = -1 + var nextIndex: Int? = null + + override fun invoke(haystack: Iterable): Boolean { + // Normalize to list + val hay = haystack as? List ?: haystack.toList() + + indices.clear() + this@IndexedMatcher.lastMatchedIndex = -1 + currentIndex = -1 + + data class Frame( + val patternIndex: Int, + val lastMatchedIndex: Int, + val previousFrame: Frame?, + var nextHayIndex: Int, + val matchedIndex: Int + ) + + val stack = ArrayDeque() + stack.add( + Frame( + patternIndex = 0, + lastMatchedIndex = -1, + previousFrame = null, + nextHayIndex = 0, + matchedIndex = -1 + ) + ) + + while (stack.isNotEmpty()) { + val frame = stack.last() + + if (frame.nextHayIndex >= hay.size || nextIndex == -1) { + stack.removeLast() + nextIndex = null + continue + } + + val i = frame.nextHayIndex + currentIndex = i + lastMatchedIndex = frame.lastMatchedIndex + nextIndex = null + + if (this[frame.patternIndex](hay[i], lastMatchedIndex, currentIndex)) { + Frame( + patternIndex = frame.patternIndex + 1, + lastMatchedIndex = i, + previousFrame = frame, + nextHayIndex = i + 1, + matchedIndex = i + ).also { + if (it.patternIndex == size) { + indices += buildList(size) { + var frame: Frame? = it + while (frame != null && frame.matchedIndex != -1) { + add(frame.matchedIndex) + frame = frame.previousFrame + } + }.asReversed() + + return true + } + }.let(stack::add) + } + + frame.nextHayIndex = when (val nextIndex = nextIndex) { + null -> frame.nextHayIndex + 1 + -1 -> 0 // Frame will be removed next loop. + else -> nextIndex + } + } + + return false + } +} + +// endregion + +context(_: PredicateContext) +inline operator fun > M.invoke( + key: Any, + iterable: Iterable, + builder: M.() -> Unit +) = remembered(key) { apply(builder) }(iterable) + +context(_: PredicateContext) +inline operator fun > M.invoke( + iterable: Iterable, + builder: M.() -> Unit +) = invoke(this@invoke.hashCode(), iterable, builder) + +abstract class Matcher : MutableList by mutableListOf() { + var matchIndex = -1 + protected set + + abstract operator fun invoke(haystack: Iterable): Boolean +} + +// endregion Matcher + +context(context: PredicateContext) + +inline fun remembered(key: Any, defaultValue: () -> V) = + context[key] as? V ?: defaultValue().also { context[key] = it } + +private fun cachedReadOnlyProperty(block: BytecodePatchContext.(KProperty<*>) -> T) = + object : ReadOnlyProperty { + private var value: T? = null + private var cached = false + + override fun getValue(thisRef: BytecodePatchContext, property: KProperty<*>): T { + if (!cached) { + value = thisRef.block(property) + cached = true + } + + return value!! + } + } + +private typealias DeclarativeClassDefPredicate = context(PredicateContext, MutableList Boolean>) () -> Unit + +private typealias DeclarativeMethodPredicate = context(PredicateContext, MutableList Boolean>) () -> Unit + +fun T.declarativePredicate(build: context(MutableList Boolean>) () -> Unit) = + context(mutableListOf Boolean>().apply(build)) { + all(this) + } + +context(_: PredicateContext) +fun T.rememberedDeclarativePredicate(key: Any, block: context(MutableList Boolean>) () -> Unit) = + context(remembered(key) { mutableListOf Boolean>().apply(block) }) { + all(this) + } + +context(_: PredicateContext) +private fun T.rememberedDeclarativePredicate( + predicate: context(PredicateContext, MutableList Boolean>) () -> Unit +) = rememberedDeclarativePredicate("declarativePredicate") { predicate() } + +fun BytecodePatchContext.firstClassDefByDeclarativePredicateOrNull( + predicate: DeclarativeClassDefPredicate +) = firstClassDefOrNull { rememberedDeclarativePredicate(predicate) } + +fun BytecodePatchContext.firstClassDefByDeclarativePredicateOrNull( + type: String? = null, + predicate: DeclarativeClassDefPredicate = { } +) = firstClassDefOrNull(type) { rememberedDeclarativePredicate(predicate) } + +fun BytecodePatchContext.firstClassDefByDeclarativePredicate( + type: String? = null, + predicate: DeclarativeClassDefPredicate = { } +) = requireNotNull(firstClassDefByDeclarativePredicateOrNull(type, predicate)) + +fun BytecodePatchContext.firstClassDefMutableByDeclarativePredicateOrNull( + type: String? = null, + predicate: DeclarativeClassDefPredicate = { } +) = firstClassDefMutableOrNull(type) { rememberedDeclarativePredicate(predicate) } + +fun BytecodePatchContext.firstClassDefMutableByDeclarativePredicate( + type: String? = null, + predicate: DeclarativeClassDefPredicate = { } +) = requireNotNull(firstClassDefMutableByDeclarativePredicateOrNull(type, predicate)) + +fun BytecodePatchContext.firstMethodByDeclarativePredicateOrNull( + vararg strings: String, + predicate: DeclarativeMethodPredicate = { } +) = firstMethodOrNull(*strings) { rememberedDeclarativePredicate(predicate) } + +fun BytecodePatchContext.firstMethodByDeclarativePredicate( + vararg strings: String, + predicate: DeclarativeMethodPredicate = { } +) = requireNotNull(firstMethodByDeclarativePredicateOrNull(*strings, predicate = predicate)) + +fun BytecodePatchContext.firstMethodMutableByDeclarativePredicateOrNull( + vararg strings: String, + predicate: DeclarativeMethodPredicate = { } +) = firstMethodMutableOrNull(*strings) { rememberedDeclarativePredicate(predicate) } + +fun BytecodePatchContext.firstMethodMutableByDeclarativePredicate( + vararg strings: String, + predicate: DeclarativeMethodPredicate = { } +) = requireNotNull(firstMethodMutableByDeclarativePredicateOrNull(*strings, predicate = predicate)) + +fun gettingFirstClassDefByDeclarativePredicateOrNull( + type: String? = null, + predicate: DeclarativeClassDefPredicate = { } +) = gettingFirstClassDefOrNull(type) { rememberedDeclarativePredicate(predicate) } + +fun gettingFirstClassDefByDeclarativePredicate( + type: String? = null, + predicate: DeclarativeClassDefPredicate = { } +) = cachedReadOnlyProperty { firstClassDefByDeclarativePredicate(type, predicate) } + +fun gettingFirstClassDefMutableByDeclarativePredicateOrNull( + type: String? = null, + predicate: DeclarativeClassDefPredicate = { } +) = gettingFirstClassDefMutableOrNull(type) { rememberedDeclarativePredicate(predicate) } + +fun gettingFirstClassDefMutableByDeclarativePredicate( + type: String? = null, + predicate: DeclarativeClassDefPredicate = { } +) = cachedReadOnlyProperty { firstClassDefMutableByDeclarativePredicate(type, predicate) } + +fun gettingFirstMethodByDeclarativePredicateOrNull( + vararg strings: String, + predicate: DeclarativeMethodPredicate = { } +) = gettingFirstMethodOrNull(*strings) { rememberedDeclarativePredicate(predicate) } + +fun gettingFirstMethodByDeclarativePredicate( + vararg strings: String, + predicate: DeclarativeMethodPredicate = { } +) = cachedReadOnlyProperty { firstMethodByDeclarativePredicate(*strings, predicate = predicate) } + +fun gettingFirstMethodMutableByDeclarativePredicateOrNull( + vararg strings: String, + predicate: DeclarativeMethodPredicate = { } +) = gettingFirstMethodMutableOrNull(*strings) { rememberedDeclarativePredicate(predicate) } + +fun gettingFirstMethodMutableByDeclarativePredicate( + vararg strings: String, + predicate: DeclarativeMethodPredicate = { } +) = cachedReadOnlyProperty { firstMethodMutableByDeclarativePredicate(*strings, predicate = predicate) } + +context(list: MutableList Boolean>) +fun allOf(block: MutableList Boolean>.() -> Unit) { + val child = mutableListOf Boolean>().apply(block) + list.add { child.all { it() } } +} + +context(list: MutableList Boolean>) +fun anyOf(block: MutableList Boolean>.() -> Unit) { + val child = mutableListOf Boolean>().apply(block) + list.add { child.any { it() } } +} + +context(list: MutableList Boolean>) +fun predicate(block: T.() -> Boolean) { + list.add(block) +} + +context(list: MutableList Boolean>) +fun all(target: T): Boolean = list.all { target.it() } + +context(list: MutableList Boolean>) +fun any(target: T): Boolean = list.any { target.it() } + +fun firstMethodBuilder( + vararg strings: String, + builder: + context(PredicateContext, MutableList Boolean>, IndexedMatcher, MutableList) () -> Unit +) = with(mutableListOf()) stringsList@{ + addAll(strings) + + with(indexedMatcher()) { + Match(indices = indices, strings = this@stringsList) { builder() } + } +} + +context(_: MutableList Boolean>) +fun accessFlags(vararg flags: AccessFlags) = + predicate { accessFlags(*flags) } + +context(_: MutableList Boolean>) +fun returnType( + returnType: String, + compare: String.(String) -> Boolean = String::startsWith +) = predicate { this.returnType.compare(returnType) } + +context(_: MutableList Boolean>) +fun name( + name: String, + compare: String.(String) -> Boolean = String::equals +) = predicate { this.name.compare(name) } + +context(_: MutableList Boolean>) +fun definingClass( + definingClass: String, + compare: String.(String) -> Boolean = String::equals +) = predicate { this.definingClass.compare(definingClass) } + +context(_: MutableList Boolean>) +fun parameterTypes(vararg parameterTypePrefixes: String) = predicate { + parameterTypes.size == parameterTypePrefixes.size && parameterTypes.zip(parameterTypePrefixes) + .all { (a, b) -> a.startsWith(b) } +} + +context(_: MutableList Boolean>, matcher: IndexedMatcher) +fun instructions( + build: context(IndexedMatcher) () -> Unit +) { + build() + predicate { implementation { matcher(instructions) } } +} + +context(_: MutableList Boolean>, matcher: IndexedMatcher) +fun instructions( + vararg predicates: Instruction.(currentIndex: Int, lastMatchedIndex: Int) -> Boolean +) = instructions { + predicates.forEach { +it } +} + +context(_: MutableList Boolean>) +fun custom(block: Method.() -> Boolean) { + predicate { block() } +} + +context(_: IndexedMatcher) +inline fun `is`( + crossinline predicate: T.() -> Boolean = { true } +): Instruction.(Int, Int) -> Boolean = { _, _ -> (this as? T)?.predicate() == true } + +fun instruction(predicate: Instruction.() -> Boolean): Instruction.(Int, Int) -> Boolean = { _, _ -> predicate() } + +fun registers(predicate: IntArray.() -> Boolean = { true }): Instruction.(Int, Int) -> Boolean = { _, _ -> + when (this) { + is RegisterRangeInstruction -> + IntArray(registerCount) { startRegister + it }.predicate() + + is FiveRegisterInstruction -> + intArrayOf(registerC, registerD, registerE, registerF, registerG).predicate() + + is ThreeRegisterInstruction -> + intArrayOf(registerA, registerB, registerC).predicate() + + is TwoRegisterInstruction -> + intArrayOf(registerA, registerB).predicate() + + is OneRegisterInstruction -> + intArrayOf(registerA).predicate() + + else -> false + } +} + +fun registers( + vararg registers: Int, + compare: IntArray.(registers: IntArray) -> Boolean = { registers -> + this.size >= registers.size && registers.indices.all { this[it] == registers[it] } + } +) = registers({ compare(registers) }) + +fun literal(predicate: Long.() -> Boolean = { true }): Instruction.(Int, Int) -> Boolean = + { _, _ -> wideLiteral?.predicate() == true } + +fun literal(literal: Long, compare: Long.(Long) -> Boolean = Long::equals) = + literal { compare(literal) } + +fun reference(predicate: String.() -> Boolean = { true }): Instruction.(Int, Int) -> Boolean = + predicate@{ _, _ -> this.reference?.toString()?.predicate() == true } + +fun reference(reference: String, compare: String.(String) -> Boolean = String::equals) = + reference { compare(reference) } + +fun field(predicate: String.() -> Boolean = { true }): Instruction.(Int, Int) -> Boolean = { _, _ -> + fieldReference?.name?.predicate() == true +} + +fun field(name: String, compare: String.(String) -> Boolean = String::equals) = + field { compare(name) } + +fun type(predicate: String.() -> Boolean = { true }): Instruction.(Int, Int) -> Boolean = + { _, _ -> type?.predicate() == true } + +fun type(type: String, compare: String.(String) -> Boolean = String::equals) = + type { compare(type) } + +fun method(predicate: String.() -> Boolean = { true }): Instruction.(Int, Int) -> Boolean = { _, _ -> + methodReference?.name?.predicate() == true +} + +fun method(name: String, compare: String.(String) -> Boolean = String::equals) = + method { compare(name) } + +fun string(compare: String.() -> Boolean = { true }): Instruction.(Int, Int) -> Boolean = predicate@{ _, _ -> + this@predicate.string?.compare() == true +} + +context(stringsList: MutableList) +fun string( + string: String, + compare: String.(String) -> Boolean = String::equals +): Instruction.(Int, Int) -> Boolean { + if (compare == String::equals) stringsList += string + + return string { compare(string) } +} + +fun string(string: String, compare: String.(String) -> Boolean = String::equals) = string { compare(string) } + +context(stringsList: MutableList) +operator fun String.invoke(compare: String.(String) -> Boolean = String::equals): Instruction.(Int, Int) -> Boolean { + if (compare == String::equals) stringsList += this + + return { _, _ -> string?.compare(this@invoke) == true } +} + +operator fun Opcode.invoke(): Instruction.(currentIndex: Int, lastMatchedIndex: Int) -> Boolean = + { _, _ -> opcode == this@invoke } + +fun anyOf( + vararg predicates: Instruction.(currentIndex: Int, lastMatchedIndex: Int) -> Boolean +): Instruction.(Int, Int) -> Boolean = { currentIndex, lastMatchedIndex -> + predicates.any { predicate -> predicate(currentIndex, lastMatchedIndex) } +} + +fun allOf( + vararg predicates: Instruction.(currentIndex: Int, lastMatchedIndex: Int) -> Boolean +): Instruction.(Int, Int) -> Boolean = { currentIndex, lastMatchedIndex -> + predicates.all { predicate -> predicate(currentIndex, lastMatchedIndex) } +} + +fun noneOf( + vararg predicates: Instruction.(currentIndex: Int, lastMatchedIndex: Int) -> Boolean +): Instruction.(Int, Int) -> Boolean = { currentIndex, lastMatchedIndex -> + predicates.none { predicate -> predicate(currentIndex, lastMatchedIndex) } +} + +class Match internal constructor( + val indices: List, + val strings: List, + private val predicate: DeclarativeMethodPredicate +) { + private var _methodOrNull: MutableMethod? = null + + context(context: BytecodePatchContext) + val methodOrNull: MutableMethod? + get() = _methodOrNull ?: run { + _methodOrNull = if (strings.isEmpty()) + context.firstMethodMutableByDeclarativePredicateOrNull(predicate = predicate) + else + context.firstMethodMutableByDeclarativePredicateOrNull(strings = strings.toTypedArray(), predicate) + + _methodOrNull + } + + context(_: BytecodePatchContext) + val method get() = requireNotNull(methodOrNull) + + context(context: BytecodePatchContext) + val classDefOrNull get() = methodOrNull?.definingClass?.let(context::firstClassDefOrNull) + + context(_: BytecodePatchContext) + val classDef get() = requireNotNull(classDefOrNull) +} diff --git a/matching/src/commonTest/kotlin/app/revanced/patcher/MatchingTest.kt b/matching/src/commonTest/kotlin/app/revanced/patcher/MatchingTest.kt new file mode 100644 index 0000000..bc0e9ad --- /dev/null +++ b/matching/src/commonTest/kotlin/app/revanced/patcher/MatchingTest.kt @@ -0,0 +1,161 @@ +package app.revanced.patcher + +import app.revanced.patcher.patch.bytecodePatch +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.assertDoesNotThrow +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +object MatchingTest : PatcherTestBase() { + @BeforeAll + fun setUp() = setUpMock() + + @Test + fun `matches via builder api`() { + fun firstMethodBuilder(fail: Boolean = false) = firstMethodBuilder { + name("method") + definingClass("class") + + if (fail) returnType("doesnt exist") + + instructions( + head(Opcode.CONST_STRING()), + `is`(), + noneOf(registers()), + string("test", String::contains), + after(1..3, allOf(Opcode.INVOKE_VIRTUAL(), registers(1, 0))), + allOf(), + type("PrintStream;", String::endsWith) + ) + } + + bytecodePatch { + execute { + assertNotNull(firstMethodBuilder().methodOrNull) { "Expected to find a method" } + Assertions.assertNull(firstMethodBuilder(fail = true).methodOrNull) { "Expected to not find a method" } + } + }() + } + + @Test + fun `matches via declarative api`() { + bytecodePatch { + execute { + val method = firstMethodByDeclarativePredicateOrNull { + anyOf { + predicate { name == "method" } + add { false } + } + allOf { + predicate { returnType == "V" } + } + predicate { definingClass == "class" } + } + assertNotNull(method) { "Expected to find a method" } + } + }() + } + + @Test + fun `predicate matcher works correctly`() { + bytecodePatch { + execute { + assertDoesNotThrow("Should find method") { firstMethod { name == "method" } } + } + } + } + + @Test + fun `matcher finds indices correctly`() { + val iterable = (1..10).toList() + val matcher = indexedMatcher() + + matcher.apply { + +head { this > 5 } + } + assertFalse( + matcher(iterable), + "Should not match at any other index than first" + ) + matcher.clear() + + matcher.apply { +head { this == 1 } }(iterable) + assertEquals( + listOf(0), + matcher.indices, + "Should match at first index." + ) + matcher.clear() + + matcher.apply { add { _, _ -> this > 0 } }(iterable) + assertEquals(1, matcher.indices.size, "Should only match once.") + matcher.clear() + + matcher.apply { add { _, _ -> this == 2 } }(iterable) + assertEquals( + listOf(1), + matcher.indices, + "Should find the index correctly." + ) + matcher.clear() + + matcher.apply { + +head { this == 1 } + add { _, _ -> this == 2 } + add { _, _ -> this == 4 } + }(iterable) + assertEquals( + listOf(0, 1, 3), + matcher.indices, + "Should match 1, 2 and 4 at indices 0, 1 and 3." + ) + matcher.clear() + + matcher.apply { + +after { this == 1 } + }(iterable) + assertEquals( + listOf(0), + matcher.indices, + "Should match index 0 after nothing" + ) + matcher.clear() + + matcher.apply { + +after(2..Int.MAX_VALUE) { this == 1 } + } + assertFalse( + matcher(iterable), + "Should not match, because 1 is out of range" + ) + matcher.clear() + + matcher.apply { + +after(1..1) { this == 2 } + } + assertFalse( + matcher(iterable), + "Should not match, because 2 is at index 1" + ) + matcher.clear() + + matcher.apply { + +head { this == 1 } + +after(2..5) { this == 4 } + add { _, _ -> this == 8 } + add { _, _ -> this == 9 } + }(iterable) + assertEquals( + listOf(0, 3, 7, 8), + matcher.indices, + "Should match indices correctly." + ) + } +} \ No newline at end of file diff --git a/core/api/android/core.api b/patcher/api/android/core.api similarity index 100% rename from core/api/android/core.api rename to patcher/api/android/core.api diff --git a/core/api/jvm/core.api b/patcher/api/jvm/core.api similarity index 100% rename from core/api/jvm/core.api rename to patcher/api/jvm/core.api diff --git a/core/build.gradle.kts b/patcher/build.gradle.kts similarity index 96% rename from core/build.gradle.kts rename to patcher/build.gradle.kts index 1186b90..f61f566 100644 --- a/core/build.gradle.kts +++ b/patcher/build.gradle.kts @@ -17,6 +17,7 @@ kotlin { } jvm() + androidLibrary { namespace = "app.revanced.patcher" compileSdk = libs.versions.android.compileSdk.get().toInt() @@ -35,6 +36,7 @@ kotlin { } } } + sourceSets { commonMain.dependencies { implementation(libs.apktool.lib) @@ -48,18 +50,16 @@ kotlin { jvmTest.dependencies { implementation(libs.mockk) implementation(libs.kotlin.test) + implementation(project(":tests")) } } + compilerOptions { freeCompilerArgs = listOf("-Xcontext-parameters") } } tasks { - named("jvmProcessResources") { - expand("projectVersion" to project.version) - } - named("jvmTest") { useJUnitPlatform() } diff --git a/core/src/androidMain/kotlin/app/revanced/patcher/patch/Patch.android.kt b/patcher/src/androidMain/kotlin/app/revanced/patcher/patch/Patch.android.kt similarity index 63% rename from core/src/androidMain/kotlin/app/revanced/patcher/patch/Patch.android.kt rename to patcher/src/androidMain/kotlin/app/revanced/patcher/patch/Patch.android.kt index f3ee873..698316d 100644 --- a/core/src/androidMain/kotlin/app/revanced/patcher/patch/Patch.android.kt +++ b/patcher/src/androidMain/kotlin/app/revanced/patcher/patch/Patch.android.kt @@ -10,14 +10,20 @@ actual val Class<*>.isPatch get() = Patch::class.java.isAssignableFrom(this) /** * Loads patches from DEX files declared as public static fields * or returned by public static and non-parametrized methods. - * Patches with no name are not loaded. + * Patches with no name are not loaded. If a patches file fails to load, + * the [onFailedToLoad] callback is invoked with the file and the throwable + * and the loading continues for the other files. * * @param patchesFiles The DEX files to load the patches from. + * @param onFailedToLoad A callback invoked when a patches file fails to load. * * @return The loaded patches. */ -actual fun loadPatches(patchesFiles: Set) = loadPatches( - patchesFiles, +actual fun loadPatches( + vararg patchesFiles: File, + onFailedToLoad: (patchesFile: File, throwable: Throwable) -> Unit, +) = loadPatches( + patchesFiles = patchesFiles, { patchBundle -> MultiDexIO.readDexFile(true, patchBundle, BasicDexFileNamer(), null, null).classes .map { classDef -> @@ -28,5 +34,6 @@ actual fun loadPatches(patchesFiles: Set) = loadPatches( patchesFiles.joinToString(File.pathSeparator) { it.absolutePath }, null, null, null - ) + ), + onFailedToLoad ) diff --git a/core/src/androidMain/kotlin/collections/MutableMap.android.kt b/patcher/src/androidMain/kotlin/collections/MutableMap.android.kt similarity index 100% rename from core/src/androidMain/kotlin/collections/MutableMap.android.kt rename to patcher/src/androidMain/kotlin/collections/MutableMap.android.kt diff --git a/core/src/androidMain/kotlin/java/io/File.android.kt b/patcher/src/androidMain/kotlin/java/io/File.android.kt similarity index 100% rename from core/src/androidMain/kotlin/java/io/File.android.kt rename to patcher/src/androidMain/kotlin/java/io/File.android.kt diff --git a/core/src/commonMain/kotlin/app/revanced/patcher/Fingerprint.kt b/patcher/src/commonMain/kotlin/app/revanced/patcher/Fingerprint.kt similarity index 99% rename from core/src/commonMain/kotlin/app/revanced/patcher/Fingerprint.kt rename to patcher/src/commonMain/kotlin/app/revanced/patcher/Fingerprint.kt index ad46187..59afabb 100644 --- a/core/src/commonMain/kotlin/app/revanced/patcher/Fingerprint.kt +++ b/patcher/src/commonMain/kotlin/app/revanced/patcher/Fingerprint.kt @@ -430,7 +430,7 @@ class Match internal constructor( * Accessing this property allocates a new mutable instance. * Use [originalClassDef] if mutable access is not required. */ - val classDef by lazy { context.firstClassDefMutable(originalClassDef.type) } + val classDef by lazy { context.classDefs[originalClassDef.type]!! } /** * The mutable version of [originalMethod]. diff --git a/patcher/src/commonMain/kotlin/app/revanced/patcher/Patching.kt b/patcher/src/commonMain/kotlin/app/revanced/patcher/Patching.kt new file mode 100644 index 0000000..18f3914 --- /dev/null +++ b/patcher/src/commonMain/kotlin/app/revanced/patcher/Patching.kt @@ -0,0 +1,163 @@ +package app.revanced.patcher + +import app.revanced.patcher.patch.* +import java.io.File +import java.io.InputStream +import java.io.deleteRecursively +import java.io.resolve +import java.util.logging.Logger + +fun patcher( + apkFile: File, + temporaryFilesPath: File = File("revanced-patcher-temporary-files"), + aaptBinaryPath: File? = null, + frameworkFileDirectory: String? = null, + getPatches: (packageName: String, versionName: String) -> Set, +): (emit: (PatchResult) -> Unit) -> PatchesResult { + val logger = Logger.getLogger("Patcher") + + if (temporaryFilesPath.exists()) { + logger.info("Deleting existing temporary files directory") + + if (!temporaryFilesPath.deleteRecursively()) + logger.severe("Failed to delete existing temporary files directory") + } + + val apkFilesPath = temporaryFilesPath.resolve("apk").also { it.mkdirs() } + val patchedFilesPath = temporaryFilesPath.resolve("patched").also { it.mkdirs() } + + val resourcePatchContext = ResourcePatchContext( + apkFile, + apkFilesPath, + patchedFilesPath, + aaptBinaryPath, + frameworkFileDirectory + ) + + val (packageName, versionName) = resourcePatchContext.decodeManifest() + val patches = getPatches(packageName, versionName) + + return { emit: (PatchResult) -> Unit -> + if (patches.any { patch -> + fun Patch.check(): Boolean = type == PatchType.RESOURCE || dependencies.any { it.check() } + patch.check() + } + ) resourcePatchContext.decodeResources() + + // After initializing the resource context, to keep memory usage time low. + val bytecodePatchContext = BytecodePatchContext( + apkFile, + patchedFilesPath + ) + + logger.info("Warming up the cache") + + bytecodePatchContext.classDefs.initializeCache() + + logger.info("Executing patches") + + patches.execute(bytecodePatchContext, resourcePatchContext, emit) + } +} + +// Public for testing. +fun Set.execute( + bytecodePatchContext: BytecodePatchContext, + resourcePatchContext: ResourcePatchContext, + emit: (PatchResult) -> Unit +): PatchesResult { + val executedPatches = LinkedHashMap() + + sortedBy { it.name }.forEach { patch -> + fun Patch.execute(): PatchResult { + val result = executedPatches[this] + if (result != null) { + if (result.exception == null) return result + return patchResult(PatchException("The patch '$this' has failed previously")) + } + + val failedDependency = dependencies.asSequence().map { it.execute() }.firstOrNull { it.exception != null } + if (failedDependency != null) { + return patchResult( + PatchException( + "The dependant patch \"$failedDependency\" of the patch \"$this\"" + + " raised an exception:\n${failedDependency.exception!!.stackTraceToString()}", + ), + ) + } + + val exception = runCatching { + execute(bytecodePatchContext, resourcePatchContext) + }.exceptionOrNull() as? Exception + + return patchResult(exception).also { executedPatches[this] = it } + } + + val patchResult = patch.execute() + + // If an exception occurred or the patch has no finalize block, emit the result. + if (patchResult.exception != null || patch.finalize == null) { + emit(patchResult) + } + } + + val succeededPatchesWithFinalizeBlock = executedPatches.values.filter { + it.exception == null && it.patch.finalize != null + } + + succeededPatchesWithFinalizeBlock.asReversed().forEach { executionResult -> + val patch = executionResult.patch + runCatching { patch.finalize!!.invoke(bytecodePatchContext, resourcePatchContext) } + .fold( + { emit(executionResult) }, + { + emit( + PatchResult( + patch, + PatchException( + "The patch \"$patch\" raised an exception:\n${it.stackTraceToString()}", + it, + ), + ) + ) + } + ) + } + + return PatchesResult(bytecodePatchContext.get(), resourcePatchContext.get()) +} + +/** + * The result of applying patches. + * + * @param dexFiles The patched dex files. + * @param resources The patched resources. + */ +class PatchesResult internal constructor( + val dexFiles: Set, + val resources: PatchedResources?, +) { + + /** + * A dex file. + * + * @param name The original name of the dex file. + * @param stream The dex file as [InputStream]. + */ + class PatchedDexFile internal constructor(val name: String, val stream: InputStream) + + /** + * The resources of a patched apk. + * + * @param resourcesApk The compiled resources.apk file. + * @param otherResources The directory containing other resources files. + * @param doNotCompress List of files that should not be compressed. + * @param deleteResources List of resources that should be deleted. + */ + class PatchedResources internal constructor( + val resourcesApk: File?, + val otherResources: File?, + val doNotCompress: Set, + val deleteResources: Set, + ) +} diff --git a/core/src/commonMain/kotlin/app/revanced/patcher/extensions/Instruction.kt b/patcher/src/commonMain/kotlin/app/revanced/patcher/extensions/Instruction.kt similarity index 100% rename from core/src/commonMain/kotlin/app/revanced/patcher/extensions/Instruction.kt rename to patcher/src/commonMain/kotlin/app/revanced/patcher/extensions/Instruction.kt diff --git a/core/src/commonMain/kotlin/app/revanced/patcher/extensions/Method.kt b/patcher/src/commonMain/kotlin/app/revanced/patcher/extensions/Method.kt similarity index 100% rename from core/src/commonMain/kotlin/app/revanced/patcher/extensions/Method.kt rename to patcher/src/commonMain/kotlin/app/revanced/patcher/extensions/Method.kt diff --git a/core/src/commonMain/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt b/patcher/src/commonMain/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt similarity index 86% rename from core/src/commonMain/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt rename to patcher/src/commonMain/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt index 137179e..842b61f 100644 --- a/core/src/commonMain/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt +++ b/patcher/src/commonMain/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt @@ -1,10 +1,6 @@ package app.revanced.patcher.patch -import app.revanced.patcher.InternalApi -import app.revanced.patcher.PatcherConfig -import app.revanced.patcher.PatcherResult -import com.android.tools.smali.dexlib2.mutable.MutableClassDef -import com.android.tools.smali.dexlib2.mutable.MutableClassDef.Companion.toMutable +import app.revanced.patcher.PatchesResult import app.revanced.patcher.extensions.instructionsOrNull import app.revanced.patcher.util.ClassMerger.merge import app.revanced.patcher.util.MethodNavigator @@ -14,27 +10,27 @@ import com.android.tools.smali.dexlib2.iface.Method import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction import com.android.tools.smali.dexlib2.iface.reference.MethodReference import com.android.tools.smali.dexlib2.iface.reference.StringReference +import com.android.tools.smali.dexlib2.mutable.MutableClassDef +import com.android.tools.smali.dexlib2.mutable.MutableClassDef.Companion.toMutable import lanchon.multidexlib2.BasicDexFileNamer import lanchon.multidexlib2.DexIO import lanchon.multidexlib2.MultiDexIO import lanchon.multidexlib2.RawDexIO -import java.io.Closeable -import java.io.IOException -import java.io.deleteRecursively -import java.io.inputStream -import java.io.resolve +import java.io.* import java.util.logging.Logger import kotlin.reflect.jvm.jvmName /** * A context for patches containing the current state of the bytecode. * - * @param config The [PatcherConfig] used to create this context. + * @param apkFile The apk [File] to patch. + * @param patchedFilesPath The path to the temporary apk files directory. */ @Suppress("MemberVisibilityCanBePrivate") -class BytecodePatchContext internal constructor(internal val config: PatcherConfig) : - PatchContext>, - Closeable { +class BytecodePatchContext internal constructor( + internal val apkFile: File, + internal val patchedFilesPath: File, +) : PatchContext> { private val logger = Logger.getLogger(this::class.jvmName) inner class ClassDefs private constructor( @@ -60,7 +56,7 @@ class BytecodePatchContext internal constructor(internal val config: PatcherConf constructor() : this( MultiDexIO.readDexFile( true, - config.apkFile, + apkFile, BasicDexFileNamer(), null, null @@ -189,17 +185,13 @@ class BytecodePatchContext internal constructor(internal val config: PatcherConf val classDefs = ClassDefs() /** - * Merge the extension of [bytecodePatch] into the [BytecodePatchContext]. - * If no extension is present, the function will return early. + * Extend this [BytecodePatchContext] with [extensionInputStream]. * - * @param bytecodePatch The [BytecodePatch] to merge the extension of. + * @param extensionInputStream The input stream for an extension dex file. */ - internal fun mergeExtension(bytecodePatch: BytecodePatch) { - val extensionStream = bytecodePatch.extensionInputStream?.get() - ?: return logger.fine("Extension not found") - + internal fun extendWith(extensionInputStream: InputStream) { RawDexIO.readRawDexFile( - extensionStream, 0, null + extensionInputStream, 0, null ).classes.forEach { classDef -> val existingClass = classDefs[classDef.type] ?: run { logger.fine { "Adding class \"$classDef\"" } @@ -222,7 +214,7 @@ class BytecodePatchContext internal constructor(internal val config: PatcherConf } } - extensionStream.close() + extensionInputStream.close() } /** @@ -239,15 +231,14 @@ class BytecodePatchContext internal constructor(internal val config: PatcherConf * * @return The compiled bytecode. */ - @InternalApi - override fun get(): Set { + override fun get(): Set { logger.info("Compiling patched dex files") classDefs.clearCache() System.gc() val patchedDexFileResults = - config.patchedFiles.resolve("dex").also { + patchedFilesPath.resolve("dex").also { it.deleteRecursively() // Make sure the directory is empty. it.mkdirs() }.apply { @@ -271,17 +262,9 @@ class BytecodePatchContext internal constructor(internal val config: PatcherConf DexIO.DEFAULT_MAX_DEX_POOL_SIZE, ) { _, entryName, _ -> logger.info { "Compiled $entryName" } } }.listFiles { it.isFile }!!.map { - PatcherResult.PatchedDexFile(it.name, it.inputStream()) + PatchesResult.PatchedDexFile(it.name, it.inputStream()) }.toSet() return patchedDexFileResults } - - override fun close() { - try { - classDefs.clear() - } catch (e: IOException) { - logger.warning("Failed to clear BytecodePatchContext: ${e.message}") - } - } } diff --git a/core/src/commonMain/kotlin/app/revanced/patcher/patch/Option.kt b/patcher/src/commonMain/kotlin/app/revanced/patcher/patch/Option.kt similarity index 82% rename from core/src/commonMain/kotlin/app/revanced/patcher/patch/Option.kt rename to patcher/src/commonMain/kotlin/app/revanced/patcher/patch/Option.kt index 2999d2b..24398b0 100644 --- a/core/src/commonMain/kotlin/app/revanced/patcher/patch/Option.kt +++ b/patcher/src/commonMain/kotlin/app/revanced/patcher/patch/Option.kt @@ -11,10 +11,9 @@ import kotlin.reflect.typeOf * An option. * * @param T The value type of the option. - * @param key The key. + * @param name The name. * @param default The default value. * @param values Eligible option values mapped to a human-readable name. - * @param title The title. * @param description A description. * @param required Whether the option is required. * @param type The type of the option value (to handle type erasure). @@ -25,49 +24,15 @@ import kotlin.reflect.typeOf @Suppress("MemberVisibilityCanBePrivate", "unused") class Option @PublishedApi -@Deprecated("Use the constructor with the name instead of a key instead.") internal constructor( - @Deprecated("Use the name property instead.") - val key: String, + val name: String, val default: T? = null, val values: Map? = null, - @Deprecated("Use the name property instead.") - val title: String? = null, val description: String? = null, val required: Boolean = false, val type: KType, val validator: Option.(T?) -> Boolean = { true }, ) { - /** - * The name. - */ - val name = key - - /** - * An option. - * - * @param T The value type of the option. - * @param name The name. - * @param default The default value. - * @param values Eligible option values mapped to a human-readable name. - * @param description A description. - * @param required Whether the option is required. - * @param type The type of the option value (to handle type erasure). - * @param validator The function to validate the option value. - * - * @constructor Create a new [Option]. - */ - @PublishedApi - internal constructor( - name: String, - default: T? = null, - values: Map? = null, - description: String? = null, - required: Boolean = false, - type: KType, - validator: Option.(T?) -> Boolean = { true }, - ) : this(name, default, values, name, description, required, type, validator) - /** * The value of the [Option]. */ @@ -86,7 +51,6 @@ internal constructor( uncheckedValue = value } - /** * Get the value of the [Option]. * @@ -152,13 +116,13 @@ class Options internal constructor( /** * Set an option's value. * - * @param key The key. + * @param name The name. * @param value The value. * * @throws OptionException.OptionNotFoundException If the option does not exist. */ - operator fun set(key: String, value: T?) { - val option = this[key] + operator fun set(name: String, value: T?) { + val option = this[name] try { @Suppress("UNCHECKED_CAST") @@ -174,7 +138,7 @@ class Options internal constructor( /** * Get an option. * - * @param key The key. + * @param key The name. * * @return The option. */ @@ -184,10 +148,9 @@ class Options internal constructor( /** * Create a new [Option] with a string value. * - * @param key The key. + * @param name The name. * @param default The default value. * @param values Eligible option values mapped to a human-readable name. - * @param title The title. * @param description A description. * @param required Whether the option is required. * @param validator The function to validate the option value. @@ -197,18 +160,16 @@ class Options internal constructor( * @see Option */ fun stringOption( - key: String, + name: String, default: String? = null, values: Map? = null, - title: String? = null, description: String? = null, required: Boolean = false, validator: Option.(String?) -> Boolean = { true }, ) = option( - key, + name, default, values, - title, description, required, validator, @@ -217,10 +178,9 @@ fun stringOption( /** * Create a new [Option] with a string value and add it to the current [PatchBuilder]. * - * @param key The key. + * @param name The name. * @param default The default value. * @param values Eligible option values mapped to a human-readable name. - * @param title The title. * @param description A description. * @param required Whether the option is required. * @param validator The function to validate the option value. @@ -230,18 +190,16 @@ fun stringOption( * @see Option */ fun PatchBuilder<*>.stringOption( - key: String, + name: String, default: String? = null, values: Map? = null, - title: String? = null, description: String? = null, required: Boolean = false, validator: Option.(String?) -> Boolean = { true }, ) = option( - key, + name, default, values, - title, description, required, validator, @@ -250,10 +208,9 @@ fun PatchBuilder<*>.stringOption( /** * Create a new [Option] with an integer value. * - * @param key The key. + * @param name The name. * @param default The default value. * @param values Eligible option values mapped to a human-readable name. - * @param title The title. * @param description A description. * @param required Whether the option is required. * @param validator The function to validate the option value. @@ -263,18 +220,16 @@ fun PatchBuilder<*>.stringOption( * @see Option */ fun intOption( - key: String, + name: String, default: Int? = null, values: Map? = null, - title: String? = null, description: String? = null, required: Boolean = false, validator: Option.(Int?) -> Boolean = { true }, ) = option( - key, + name, default, values, - title, description, required, validator, @@ -283,10 +238,9 @@ fun intOption( /** * Create a new [Option] with an integer value and add it to the current [PatchBuilder]. * - * @param key The key. + * @param name The name. * @param default The default value. * @param values Eligible option values mapped to a human-readable name. - * @param title The title. * @param description A description. * @param required Whether the option is required. * @param validator The function to validate the option value. @@ -296,18 +250,16 @@ fun intOption( * @see Option */ fun PatchBuilder<*>.intOption( - key: String, + name: String, default: Int? = null, values: Map? = null, - title: String? = null, description: String? = null, required: Boolean = false, validator: Option.(Int?) -> Boolean = { true }, ) = option( - key, + name, default, values, - title, description, required, validator, @@ -316,10 +268,9 @@ fun PatchBuilder<*>.intOption( /** * Create a new [Option] with a boolean value. * - * @param key The key. + * @param name The name. * @param default The default value. * @param values Eligible option values mapped to a human-readable name. - * @param title The title. * @param description A description. * @param required Whether the option is required. * @param validator The function to validate the option value. @@ -329,18 +280,16 @@ fun PatchBuilder<*>.intOption( * @see Option */ fun booleanOption( - key: String, + name: String, default: Boolean? = null, values: Map? = null, - title: String? = null, description: String? = null, required: Boolean = false, validator: Option.(Boolean?) -> Boolean = { true }, ) = option( - key, + name, default, values, - title, description, required, validator, @@ -349,10 +298,9 @@ fun booleanOption( /** * Create a new [Option] with a boolean value and add it to the current [PatchBuilder]. * - * @param key The key. + * @param name The name. * @param default The default value. * @param values Eligible option values mapped to a human-readable name. - * @param title The title. * @param description A description. * @param required Whether the option is required. * @param validator The function to validate the option value. @@ -362,18 +310,16 @@ fun booleanOption( * @see Option */ fun PatchBuilder<*>.booleanOption( - key: String, + name: String, default: Boolean? = null, values: Map? = null, - title: String? = null, description: String? = null, required: Boolean = false, validator: Option.(Boolean?) -> Boolean = { true }, ) = option( - key, + name, default, values, - title, description, required, validator, @@ -382,10 +328,9 @@ fun PatchBuilder<*>.booleanOption( /** * Create a new [Option] with a float value. * - * @param key The key. + * @param name The name. * @param default The default value. * @param values Eligible option values mapped to a human-readable name. - * @param title The title. * @param description A description. * @param required Whether the option is required. * @param validator The function to validate the option value. @@ -395,18 +340,16 @@ fun PatchBuilder<*>.booleanOption( * @see Option */ fun floatOption( - key: String, + name: String, default: Float? = null, values: Map? = null, - title: String? = null, description: String? = null, required: Boolean = false, validator: Option.(Float?) -> Boolean = { true }, ) = option( - key, + name, default, values, - title, description, required, validator, @@ -415,10 +358,9 @@ fun floatOption( /** * Create a new [Option] with a float value and add it to the current [PatchBuilder]. * - * @param key The key. + * @param name The name. * @param default The default value. * @param values Eligible option values mapped to a human-readable name. - * @param title The title. * @param description A description. * @param required Whether the option is required. * @param validator The function to validate the option value. @@ -428,18 +370,16 @@ fun floatOption( * @see Option */ fun PatchBuilder<*>.floatOption( - key: String, + name: String, default: Float? = null, values: Map? = null, - title: String? = null, description: String? = null, required: Boolean = false, validator: Option.(Float?) -> Boolean = { true }, ) = option( - key, + name, default, values, - title, description, required, validator, @@ -448,10 +388,9 @@ fun PatchBuilder<*>.floatOption( /** * Create a new [Option] with a long value. * - * @param key The key. + * @param name The name. * @param default The default value. * @param values Eligible option values mapped to a human-readable name. - * @param title The title. * @param description A description. * @param required Whether the option is required. * @param validator The function to validate the option value. @@ -461,18 +400,16 @@ fun PatchBuilder<*>.floatOption( * @see Option */ fun longOption( - key: String, + name: String, default: Long? = null, values: Map? = null, - title: String? = null, description: String? = null, required: Boolean = false, validator: Option.(Long?) -> Boolean = { true }, ) = option( - key, + name, default, values, - title, description, required, validator, @@ -481,10 +418,9 @@ fun longOption( /** * Create a new [Option] with a long value and add it to the current [PatchBuilder]. * - * @param key The key. + * @param name The name. * @param default The default value. * @param values Eligible option values mapped to a human-readable name. - * @param title The title. * @param description A description. * @param required Whether the option is required. * @param validator The function to validate the option value. @@ -494,18 +430,16 @@ fun longOption( * @see Option */ fun PatchBuilder<*>.longOption( - key: String, + name: String, default: Long? = null, values: Map? = null, - title: String? = null, description: String? = null, required: Boolean = false, validator: Option.(Long?) -> Boolean = { true }, ) = option( - key, + name, default, values, - title, description, required, validator, @@ -514,10 +448,9 @@ fun PatchBuilder<*>.longOption( /** * Create a new [Option] with a string list value. * - * @param key The key. + * @param name The name. * @param default The default value. * @param values Eligible option values mapped to a human-readable name. - * @param title The title. * @param description A description. * @param required Whether the option is required. * @param validator The function to validate the option value. @@ -527,18 +460,16 @@ fun PatchBuilder<*>.longOption( * @see Option */ fun stringsOption( - key: String, + name: String, default: List? = null, values: Map?>? = null, - title: String? = null, description: String? = null, required: Boolean = false, validator: Option>.(List?) -> Boolean = { true }, ) = option( - key, + name, default, values, - title, description, required, validator, @@ -547,10 +478,9 @@ fun stringsOption( /** * Create a new [Option] with a string list value and add it to the current [PatchBuilder]. * - * @param key The key. + * @param name The name. * @param default The default value. * @param values Eligible option values mapped to a human-readable name. - * @param title The title. * @param description A description. * @param required Whether the option is required. * @param validator The function to validate the option value. @@ -560,18 +490,16 @@ fun stringsOption( * @see Option */ fun PatchBuilder<*>.stringsOption( - key: String, + name: String, default: List? = null, values: Map?>? = null, - title: String? = null, description: String? = null, required: Boolean = false, validator: Option>.(List?) -> Boolean = { true }, ) = option( - key, + name, default, values, - title, description, required, validator, @@ -580,10 +508,9 @@ fun PatchBuilder<*>.stringsOption( /** * Create a new [Option] with an integer list value. * - * @param key The key. + * @param name The name. * @param default The default value. * @param values Eligible option values mapped to a human-readable name. - * @param title The title. * @param description A description. * @param required Whether the option is required. * @param validator The function to validate the option value. @@ -593,18 +520,16 @@ fun PatchBuilder<*>.stringsOption( * @see Option */ fun intsOption( - key: String, + name: String, default: List? = null, values: Map?>? = null, - title: String? = null, description: String? = null, required: Boolean = false, validator: Option>.(List?) -> Boolean = { true }, ) = option( - key, + name, default, values, - title, description, required, validator, @@ -613,10 +538,9 @@ fun intsOption( /** * Create a new [Option] with an integer list value and add it to the current [PatchBuilder]. * - * @param key The key. + * @param name The name. * @param default The default value. * @param values Eligible option values mapped to a human-readable name. - * @param title The title. * @param description A description. * @param required Whether the option is required. * @param validator The function to validate the option value. @@ -626,18 +550,16 @@ fun intsOption( * @see Option */ fun PatchBuilder<*>.intsOption( - key: String, + name: String, default: List? = null, values: Map?>? = null, - title: String? = null, description: String? = null, required: Boolean = false, validator: Option>.(List?) -> Boolean = { true }, ) = option( - key, + name, default, values, - title, description, required, validator, @@ -646,10 +568,9 @@ fun PatchBuilder<*>.intsOption( /** * Create a new [Option] with a boolean list value. * - * @param key The key. + * @param name The name. * @param default The default value. * @param values Eligible option values mapped to a human-readable name. - * @param title The title. * @param description A description. * @param required Whether the option is required. * @param validator The function to validate the option value. @@ -659,18 +580,16 @@ fun PatchBuilder<*>.intsOption( * @see Option */ fun booleansOption( - key: String, + name: String, default: List? = null, values: Map?>? = null, - title: String? = null, description: String? = null, required: Boolean = false, validator: Option>.(List?) -> Boolean = { true }, ) = option( - key, + name, default, values, - title, description, required, validator, @@ -679,10 +598,9 @@ fun booleansOption( /** * Create a new [Option] with a boolean list value and add it to the current [PatchBuilder]. * - * @param key The key. + * @param name The name. * @param default The default value. * @param values Eligible option values mapped to a human-readable name. - * @param title The title. * @param description A description. * @param required Whether the option is required. * @param validator The function to validate the option value. @@ -692,18 +610,16 @@ fun booleansOption( * @see Option */ fun PatchBuilder<*>.booleansOption( - key: String, + name: String, default: List? = null, values: Map?>? = null, - title: String? = null, description: String? = null, required: Boolean = false, validator: Option>.(List?) -> Boolean = { true }, ) = option( - key, + name, default, values, - title, description, required, validator, @@ -712,10 +628,9 @@ fun PatchBuilder<*>.booleansOption( /** * Create a new [Option] with a float list value and add it to the current [PatchBuilder]. * - * @param key The key. + * @param name The name. * @param default The default value. * @param values Eligible option values mapped to a human-readable name. - * @param title The title. * @param description A description. * @param required Whether the option is required. * @param validator The function to validate the option value. @@ -725,18 +640,16 @@ fun PatchBuilder<*>.booleansOption( * @see Option */ fun PatchBuilder<*>.floatsOption( - key: String, + name: String, default: List? = null, values: Map?>? = null, - title: String? = null, description: String? = null, required: Boolean = false, validator: Option>.(List?) -> Boolean = { true }, ) = option( - key, + name, default, values, - title, description, required, validator, @@ -745,10 +658,9 @@ fun PatchBuilder<*>.floatsOption( /** * Create a new [Option] with a long list value. * - * @param key The key. + * @param name The name. * @param default The default value. * @param values Eligible option values mapped to a human-readable name. - * @param title The title. * @param description A description. * @param required Whether the option is required. * @param validator The function to validate the option value. @@ -758,18 +670,16 @@ fun PatchBuilder<*>.floatsOption( * @see Option */ fun longsOption( - key: String, + name: String, default: List? = null, values: Map?>? = null, - title: String? = null, description: String? = null, required: Boolean = false, validator: Option>.(List?) -> Boolean = { true }, ) = option( - key, + name, default, values, - title, description, required, validator, @@ -778,10 +688,9 @@ fun longsOption( /** * Create a new [Option] with a long list value and add it to the current [PatchBuilder]. * - * @param key The key. + * @param name The name. * @param default The default value. * @param values Eligible option values mapped to a human-readable name. - * @param title The title. * @param description A description. * @param required Whether the option is required. * @param validator The function to validate the option value. @@ -791,18 +700,16 @@ fun longsOption( * @see Option */ fun PatchBuilder<*>.longsOption( - key: String, + name: String, default: List? = null, values: Map?>? = null, - title: String? = null, description: String? = null, required: Boolean = false, validator: Option>.(List?) -> Boolean = { true }, ) = option( - key, + name, default, values, - title, description, required, validator, @@ -811,10 +718,9 @@ fun PatchBuilder<*>.longsOption( /** * Create a new [Option]. * - * @param key The key. + * @param name The name. * @param default The default value. * @param values Eligible option values mapped to a human-readable name. - * @param title The title. * @param description A description. * @param required Whether the option is required. * @param validator The function to validate the option value. @@ -824,18 +730,16 @@ fun PatchBuilder<*>.longsOption( * @see Option */ inline fun option( - key: String, + name: String, default: T? = null, values: Map? = null, - title: String? = null, description: String? = null, required: Boolean = false, noinline validator: Option.(T?) -> Boolean = { true }, ) = Option( - key, + name, default, values, - title, description, required, typeOf(), @@ -845,10 +749,9 @@ inline fun option( /** * Create a new [Option] and add it to the current [PatchBuilder]. * - * @param key The key. + * @param name The name. * @param default The default value. * @param values Eligible option values mapped to a human-readable name. - * @param title The title. * @param description A description. * @param required Whether the option is required. * @param validator The function to validate the option value. @@ -858,18 +761,16 @@ inline fun option( * @see Option */ inline fun PatchBuilder<*>.option( - key: String, + name: String, default: T? = null, values: Map? = null, - title: String? = null, description: String? = null, required: Boolean = false, noinline validator: Option.(T?) -> Boolean = { true }, ) = app.revanced.patcher.patch.option( - key, + name, default, values, - title, description, required, validator, @@ -887,26 +788,29 @@ sealed class OptionException(errorMessage: String) : Exception(errorMessage, nul * @param invalidType The type of the value that was passed. * @param expectedType The type of the value that was expected. */ - class InvalidValueTypeException(invalidType: String, expectedType: String) : OptionException("Type $expectedType was expected but received type $invalidType") + class InvalidValueTypeException(invalidType: String, expectedType: String) : + OptionException("Type $expectedType was expected but received type $invalidType") /** * An exception thrown when a value did not satisfy the value conditions specified by the [Option]. * * @param value The value that failed validation. */ - class ValueValidationException(value: Any?, option: Option<*>) : OptionException("The option value \"$value\" failed validation for ${option.name}") + class ValueValidationException(value: Any?, option: Option<*>) : + OptionException("The option value \"$value\" failed validation for ${option.name}") /** * An exception thrown when a value is required but null was passed. * * @param option The [Option] that requires a value. */ - class ValueRequiredException(option: Option<*>) : OptionException("The option ${option.name} requires a value, but the value was null") + class ValueRequiredException(option: Option<*>) : + OptionException("The option ${option.name} requires a value, but the value was null") /** * An exception thrown when a [Option] is not found. * - * @param key The key of the [Option]. + * @param name The name of the [Option]. */ - class OptionNotFoundException(key: String) : OptionException("No option with key $key") + class OptionNotFoundException(name: String) : OptionException("No option with name $name") } diff --git a/patcher/src/commonMain/kotlin/app/revanced/patcher/patch/Patch.kt b/patcher/src/commonMain/kotlin/app/revanced/patcher/patch/Patch.kt new file mode 100644 index 0000000..4e40910 --- /dev/null +++ b/patcher/src/commonMain/kotlin/app/revanced/patcher/patch/Patch.kt @@ -0,0 +1,263 @@ +@file:Suppress("MemberVisibilityCanBePrivate", "unused") + +package app.revanced.patcher.patch + +import java.io.File +import java.io.InputStream +import java.lang.reflect.Member +import java.lang.reflect.Method +import java.lang.reflect.Modifier +import java.util.function.Supplier +import kotlin.properties.ReadOnlyProperty + +typealias PackageName = String +typealias VersionName = String +typealias Package = Pair?> + +enum class PatchType(internal val prefix: String) { + BYTECODE("Bytecode"), + RAW_RESOURCE("RawResource"), + RESOURCE("Resource") +} + +open class Patch internal constructor( + val name: String?, + val description: String?, + val use: Boolean, + val dependencies: Set, + val compatiblePackages: Set?, + options: Set>, + internal val execute: context(BytecodePatchContext, ResourcePatchContext) () -> Unit, + // Must be nullable, so that Patcher.invoke can check, + // if a patch has "finalize" in order to not emit it twice. + internal var finalize: (context(BytecodePatchContext, ResourcePatchContext) () -> Unit)?, + internal val type: PatchType, +) { + val options = Options(options) + + override fun toString() = name ?: "${type.prefix}Patch@${System.identityHashCode(this)}" +} + +sealed class PatchBuilder>( + private val type: PatchType, + private val getPatchContext: context(BytecodePatchContext, ResourcePatchContext) () -> C +) { + private var compatiblePackages: MutableSet? = null + private val dependencies = mutableSetOf() + private val options = mutableSetOf>() + + internal var execute: context(BytecodePatchContext, ResourcePatchContext) () -> Unit = { } + internal var finalize: (context(BytecodePatchContext, ResourcePatchContext) () -> Unit)? = null + + context(_: BytecodePatchContext, _: ResourcePatchContext) + private val patchContext get() = getPatchContext() + + fun execute(block: C.() -> Unit) { + execute = { block(patchContext) } + } + + fun finalize(block: C.() -> Unit) { + finalize = { block(patchContext) } + } + + operator fun Option.invoke() = apply { + options += this + } + + operator fun String.invoke(vararg versions: VersionName) = invoke(versions.toSet()) + + private operator fun String.invoke(versions: Set? = null): Package = this to versions + + fun compatibleWith(vararg packages: Package) { + if (compatiblePackages == null) { + compatiblePackages = mutableSetOf() + } + + compatiblePackages!! += packages + } + + fun compatibleWith(vararg packages: String) = compatibleWith(*packages.map { it() }.toTypedArray()) + + fun dependsOn(vararg patches: Patch) { + dependencies += patches + } + + + fun build(name: String?, description: String?, use: Boolean) = Patch( + name, + description, + use, + dependencies, + compatiblePackages, + options, + execute, + finalize, + type, + ) +} + +class BytecodePatchBuilder private constructor( + private var extensionInputStream: InputStream? = null +) : PatchBuilder( + PatchType.BYTECODE, + { + // Extend the context with the extension, before returning it to the patch for execution. + contextOf().apply { + if (extensionInputStream != null) extendWith(extensionInputStream) + } + } +) { + internal constructor() : this(null) + + fun extendWith(extension: String) = apply { + // Should be the classloader which loaded the patch class. + val classLoader = Class.forName(Thread.currentThread().stackTrace[2].className).classLoader!! + + extensionInputStream = classLoader.getResourceAsStream(extension) + ?: throw PatchException("Extension \"$extension\" not found") + } +} + +open class ResourcePatchBuilder internal constructor(type: PatchType) : PatchBuilder( + type, + { contextOf() } +) { + internal constructor() : this(PatchType.RESOURCE) +} + +class RawResourcePatchBuilder internal constructor() : ResourcePatchBuilder() + +fun bytecodePatch( + name: String? = null, + description: String? = null, + use: Boolean = true, + block: BytecodePatchBuilder.() -> Unit +) = BytecodePatchBuilder().apply(block).build(name, description, use) + +fun resourcePatch( + name: String? = null, + description: String? = null, + use: Boolean = true, + block: ResourcePatchBuilder.() -> Unit +) = ResourcePatchBuilder().apply(block).build(name, description, use) + +fun rawResourcePatch( + name: String? = null, + description: String? = null, + use: Boolean = true, + block: RawResourcePatchBuilder.() -> Unit +) = RawResourcePatchBuilder().apply(block).build(name, description, use) + +private fun > creatingPatch( + description: String? = null, + use: Boolean = true, + block: B.() -> Unit, + patchSupplier: (String?, String?, Boolean, B.() -> Unit) -> Patch +) = ReadOnlyProperty { _, property -> patchSupplier(property.name, description, use, block) } + +fun creatingBytecodePatch( + description: String? = null, + use: Boolean = true, + block: BytecodePatchBuilder.() -> Unit, +) = creatingPatch(description, use, block) { name, description, use, block -> + bytecodePatch(name, description, use, block) +} + +fun creatingResourcePatch( + description: String? = null, + use: Boolean = true, + block: ResourcePatchBuilder.() -> Unit, +) = creatingPatch(description, use, block) { name, description, use, block -> + resourcePatch(name, description, use, block) +} + +fun creatingRawResourcePatch( + description: String? = null, + use: Boolean = true, + block: RawResourcePatchBuilder.() -> Unit, +) = creatingPatch(description, use, block) { name, description, use, block -> + rawResourcePatch(name, description, use, block) +} + + +/** + * A common interface for contexts such as [ResourcePatchContext] and [BytecodePatchContext]. + */ + +sealed interface PatchContext : Supplier + +/** + * An exception thrown when patching. + * + * @param errorMessage The exception message. + * @param cause The corresponding [Throwable]. + */ +class PatchException(errorMessage: String?, cause: Throwable?) : Exception(errorMessage, cause) { + constructor(errorMessage: String) : this(errorMessage, null) + constructor(cause: Throwable) : this(cause.message, cause) +} + +/** + * A result of executing a [Patch]. + * + * @param patch The [Patch] that was executed. + * @param exception The [PatchException] thrown, if any. + */ +class PatchResult internal constructor(val patch: Patch, val exception: PatchException? = null) + +/** + * Creates a [PatchResult] for this [Patch]. + * + * @param exception The [PatchException] thrown, if any. + * @return The created [PatchResult]. + */ +internal fun Patch.patchResult(exception: Exception? = null) = PatchResult(this, exception?.toPatchException()) +private fun Exception.toPatchException() = this as? PatchException ?: PatchException(this) + +/** + * A collection of patches loaded from patches files. + * + * @property patchesByFile The patches mapped by their patches file. + */ +class Patches internal constructor(val patchesByFile: Map>) : Set +by patchesByFile.values.flatten().toSet() + +// Must be internal and a separate function for testing. +@Suppress("MISSING_DEPENDENCY_IN_INFERRED_TYPE_ANNOTATION_WARNING") +internal fun getPatches(classNames: List, classLoader: ClassLoader): Set { + fun Member.isUsable() = + Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers) && (this !is Method || parameterCount != 0) + + fun Class<*>.getPatchFields() = fields + .filter { it.type.isPatch && it.isUsable() } + .map { it.get(null) as Patch } + + fun Class<*>.getPatchMethods() = methods + .filter { it.returnType.isPatch && it.parameterCount == 0 && it.isUsable() } + .map { it.invoke(null) as Patch } + + return classNames + .map { classLoader.loadClass(it) } + .flatMap { it.getPatchMethods() + it.getPatchFields() } + .filter { it.name != null } + .toSet() +} + +internal fun loadPatches( + vararg patchesFiles: File, + getBinaryClassNames: (patchesFile: File) -> List, + classLoader: ClassLoader, + onFailedToLoad: (File, Throwable) -> Unit +) = Patches(patchesFiles.map { file -> + file to getBinaryClassNames(file) +}.mapNotNull { (file, classNames) -> + runCatching { file to getPatches(classNames, classLoader) } + .onFailure { onFailedToLoad(file, it) }.getOrNull() +}.toMap()) + +expect fun loadPatches( + vararg patchesFiles: File, + onFailedToLoad: (patchesFile: File, throwable: Throwable) -> Unit = { _, _ -> }, +): Patches + +internal expect val Class<*>.isPatch: Boolean diff --git a/patcher/src/commonMain/kotlin/app/revanced/patcher/patch/ResourcePatchContext.kt b/patcher/src/commonMain/kotlin/app/revanced/patcher/patch/ResourcePatchContext.kt new file mode 100644 index 0000000..a39101f --- /dev/null +++ b/patcher/src/commonMain/kotlin/app/revanced/patcher/patch/ResourcePatchContext.kt @@ -0,0 +1,229 @@ +package app.revanced.patcher.patch + +import app.revanced.patcher.PatchesResult +import app.revanced.patcher.util.Document +import brut.androlib.AaptInvoker +import brut.androlib.ApkDecoder +import brut.androlib.Config +import brut.androlib.apk.ApkInfo +import brut.androlib.apk.UsesFramework +import brut.androlib.res.Framework +import brut.androlib.res.ResourcesDecoder +import brut.androlib.res.decoder.AndroidManifestPullStreamDecoder +import brut.androlib.res.decoder.AndroidManifestResourceParser +import brut.androlib.res.xml.ResXmlUtils +import brut.directory.ExtFile +import java.io.File +import java.io.InputStream +import java.io.OutputStream +import java.io.resolve +import java.nio.file.Files +import java.util.logging.Logger +import kotlin.reflect.jvm.jvmName + +/** + * A context for patches containing the current state of resources. + * + * @param apkFile The apk file to patch. + * @param apkFilesPath The path to the temporary apk files directory. + * @param patchedFilesPath The path to the temporary patched files directory. + * @param aaptBinaryPath The path to a custom aapt binary. + * @param frameworkFileDirectory The path to the directory to cache the framework file in. + */ +class ResourcePatchContext internal constructor( + private val apkFile: File, + private val apkFilesPath: File, + private val patchedFilesPath: File, + aaptBinaryPath: File? = null, + frameworkFileDirectory: String? = null, +) : PatchContext { + private val apkInfo = ApkInfo(ExtFile(apkFile)) + + private val logger = Logger.getLogger(ResourcePatchContext::class.jvmName) + + private val resourceConfig = Config.getDefaultConfig().apply { + aaptBinary = aaptBinaryPath + frameworkDirectory = frameworkFileDirectory + } + + internal var decodingMode = ResourceDecodingMode.MANIFEST + + /** + * Read a document from an [InputStream]. + */ + fun document(inputStream: InputStream) = Document(inputStream) + + /** + * Read and write documents in the [apkFile]. + */ + fun document(path: String) = Document(get(path)) + + /** + * Set of resources from [apkFile] to delete. + */ + private val deleteResources = mutableSetOf() + + internal fun decodeManifest(): Pair { + logger.info("Decoding manifest") + + val resourcesDecoder = ResourcesDecoder(resourceConfig, apkInfo) + + // Decode manually instead of using resourceDecoder.decodeManifest + // because it does not support decoding to an OutputStream. + AndroidManifestPullStreamDecoder( + AndroidManifestResourceParser(resourcesDecoder.resTable), + resourcesDecoder.newXmlSerializer(), + ).decode( + apkInfo.apkFile.directory.getFileInput("AndroidManifest.xml"), + // Older Android versions do not support OutputStream.nullOutputStream() + object : OutputStream() { + override fun write(b: Int) { // Do nothing. + } + }, + ) + + // Get the package name and version from the manifest using the XmlPullStreamDecoder. + // The call to AndroidManifestPullStreamDecoder.decode() above sets apkInfo. + val packageName = resourcesDecoder.resTable.packageRenamed + val packageVersion = + apkInfo.versionInfo.versionName ?: apkInfo.versionInfo.versionCode + + /* + When the main resource package is not loaded, the ResTable is flagged as sparse. + Because ResourcesDecoder.decodeResources loads the main package and is not called here, + set sparseResources to false again to prevent the ResTable from being flagged as sparse falsely, + in case ResourcesDecoder.decodeResources is not later used in the patching process + to set sparseResources correctly. + + See ARSCDecoder.readTableType for more info. + */ + apkInfo.sparseResources = false + + return packageName to packageVersion + } + + internal fun decodeResources() { + logger.info("Decoding resources") + + val resourcesDecoder = ResourcesDecoder(resourceConfig, apkInfo).also { + it.decodeResources(apkFilesPath) + it.decodeManifest(apkFilesPath) + } + + // Record uncompressed files to preserve their state when recompiling. + ApkDecoder(apkInfo, resourceConfig).recordUncompressedFiles(resourcesDecoder.resFileMapping) + + // Get the ids of the used framework packages to include them for reference when recompiling. + apkInfo.usesFramework = UsesFramework().apply { + ids = resourcesDecoder.resTable.listFramePackages().map { it.id } + } + } + + /** + * Compile resources in [apkFilesPath]. + * + * @return The [PatchesResult.PatchedResources]. + */ + override fun get(): PatchesResult.PatchedResources { + logger.info("Compiling patched resources") + + val resourcesPath = patchedFilesPath.resolve("resources").also { it.mkdirs() } + + val resourcesApkFile = if (decodingMode == ResourceDecodingMode.ALL) { + val resourcesApkFile = resourcesPath.resolve("resources.apk").also { it.createNewFile() } + + val manifestFile = apkFilesPath.resolve("AndroidManifest.xml").also { + ResXmlUtils.fixingPublicAttrsInProviderAttributes(it) + } + val resPath = apkFilesPath.resolve("res") + val frameworkApkFiles = with(Framework(resourceConfig)) { + apkInfo.usesFramework.ids.map { id -> getFrameworkApk(id, null) } + }.toTypedArray() + + AaptInvoker( + resourceConfig, + apkInfo + ).invoke(resourcesApkFile, manifestFile, resPath, null, null, frameworkApkFiles) + + resourcesApkFile + } else null + + + val otherFiles = apkFilesPath.listFiles()!!.filter { + // Excluded because present in resources.other. + // TODO: We are reusing apkFiles as a temporarily directory for extracting resources. + // This is not ideal as it could conflict with files such as the ones that are filtered here. + // The problem is that ResourcePatchContext#get returns a File relative to apkFiles, + // and we need to extract files to that directory. + // A solution would be to use apkFiles as the working directory for the patching process. + // Once all patches have been executed, we can move the decoded resources to a new directory. + // The filters wouldn't be needed anymore. + // For now, we assume that the files we filter here are not needed for the patching process. + it.name != "AndroidManifest.xml" && + it.name != "res" && + // Generated by Androlib. + it.name != "build" + } + val otherResourceFiles = if (otherFiles.isNotEmpty()) { + // Move the other resources files. + resourcesPath.resolve("other").also { it.mkdirs() }.apply { + otherFiles.forEach { file -> + Files.move(file.toPath(), resolve(file.name).toPath()) + } + } + } else null + + return PatchesResult.PatchedResources( + resourcesApkFile, + otherResourceFiles, + apkInfo.doNotCompress?.toSet() ?: emptySet(), + deleteResources, + ) + } + + /** + * Get a file from [apkFilesPath]. + * + * @param path The path of the file. + * @param copy Whether to copy the file from [apkFile] if it does not exist yet in [apkFilesPath]. + */ + operator fun get( + path: String, + copy: Boolean = true, + ) = apkFilesPath.resolve(path).apply { + if (copy && !exists()) { + with(ExtFile(apkFile).directory) { + if (containsFile(path) || containsDir(path)) { + copyToDir(apkFilesPath, path) + } + } + } + } + + /** + * Mark a file for deletion when the APK is rebuilt. + * + * @param name The name of the file to delete. + */ + fun delete(name: String) = deleteResources.add(name) + + /** + * How to handle resources decoding and compiling. + */ + internal enum class ResourceDecodingMode { + /** + * Decode and compile all resources. + */ + ALL, + + /** + * Do not decode or compile any resources. + */ + NONE, + + /** + * Do not decode or compile any resources. + */ + MANIFEST, + } +} diff --git a/core/src/commonMain/kotlin/app/revanced/patcher/util/ClassMerger.kt b/patcher/src/commonMain/kotlin/app/revanced/patcher/util/ClassMerger.kt similarity index 97% rename from core/src/commonMain/kotlin/app/revanced/patcher/util/ClassMerger.kt rename to patcher/src/commonMain/kotlin/app/revanced/patcher/util/ClassMerger.kt index b2fcb49..ade3f88 100644 --- a/core/src/commonMain/kotlin/app/revanced/patcher/util/ClassMerger.kt +++ b/patcher/src/commonMain/kotlin/app/revanced/patcher/util/ClassMerger.kt @@ -6,7 +6,6 @@ import com.android.tools.smali.dexlib2.mutable.MutableField import com.android.tools.smali.dexlib2.mutable.MutableField.Companion.toMutable import com.android.tools.smali.dexlib2.mutable.MutableMethod import com.android.tools.smali.dexlib2.mutable.MutableMethod.Companion.toMutable -import app.revanced.patcher.firstClassDefOrNull import app.revanced.patcher.patch.BytecodePatchContext import app.revanced.patcher.util.ClassMerger.Utils.asMutableClass import app.revanced.patcher.util.ClassMerger.Utils.filterAny @@ -182,9 +181,7 @@ internal object ClassMerger { ) { callback(targetClass) - targetClass.superclass ?: return - - firstClassDefOrNull { type == targetClass.superclass }?.let { classDef -> + classDefs[targetClass.superclass ?: return]?.let { classDef -> traverseClassHierarchy(classDefs.getOrReplaceMutable(classDef), callback) } } diff --git a/core/src/commonMain/kotlin/app/revanced/patcher/util/Document.kt b/patcher/src/commonMain/kotlin/app/revanced/patcher/util/Document.kt similarity index 100% rename from core/src/commonMain/kotlin/app/revanced/patcher/util/Document.kt rename to patcher/src/commonMain/kotlin/app/revanced/patcher/util/Document.kt diff --git a/core/src/commonMain/kotlin/app/revanced/patcher/util/MethodNavigator.kt b/patcher/src/commonMain/kotlin/app/revanced/patcher/util/MethodNavigator.kt similarity index 92% rename from core/src/commonMain/kotlin/app/revanced/patcher/util/MethodNavigator.kt rename to patcher/src/commonMain/kotlin/app/revanced/patcher/util/MethodNavigator.kt index d58f872..dbdd647 100644 --- a/core/src/commonMain/kotlin/app/revanced/patcher/util/MethodNavigator.kt +++ b/patcher/src/commonMain/kotlin/app/revanced/patcher/util/MethodNavigator.kt @@ -4,8 +4,6 @@ package app.revanced.patcher.util import com.android.tools.smali.dexlib2.mutable.MutableMethod import app.revanced.patcher.extensions.instructionsOrNull -import app.revanced.patcher.firstClassDef -import app.revanced.patcher.firstClassDefMutable import app.revanced.patcher.patch.BytecodePatchContext import com.android.tools.smali.dexlib2.iface.ClassDef import com.android.tools.smali.dexlib2.iface.Method @@ -84,7 +82,7 @@ class MethodNavigator internal constructor( * @return The last navigated method mutably. */ context(context: BytecodePatchContext) - fun stop() = context.firstClassDefMutable(lastNavigatedMethodReference.definingClass) + fun stop() = context.classDefs[lastNavigatedMethodReference.definingClass]!! .firstMethodBySignature as MutableMethod @@ -102,7 +100,7 @@ class MethodNavigator internal constructor( * @return The last navigated method immutably. */ context(context: BytecodePatchContext) - fun original(): Method = context.firstClassDef(lastNavigatedMethodReference.definingClass).firstMethodBySignature + fun original(): Method = context.classDefs[lastNavigatedMethodReference.definingClass]!!.firstMethodBySignature /** * Find the first [lastNavigatedMethodReference] in the class. diff --git a/core/src/commonMain/kotlin/collections/MutableMap.kt b/patcher/src/commonMain/kotlin/collections/MutableMap.kt similarity index 100% rename from core/src/commonMain/kotlin/collections/MutableMap.kt rename to patcher/src/commonMain/kotlin/collections/MutableMap.kt diff --git a/core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableAnnotationEncodedValue.kt b/patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableAnnotationEncodedValue.kt similarity index 100% rename from core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableAnnotationEncodedValue.kt rename to patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableAnnotationEncodedValue.kt diff --git a/core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableArrayEncodedValue.kt b/patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableArrayEncodedValue.kt similarity index 100% rename from core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableArrayEncodedValue.kt rename to patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableArrayEncodedValue.kt diff --git a/core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableBooleanEncodedValue.kt b/patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableBooleanEncodedValue.kt similarity index 100% rename from core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableBooleanEncodedValue.kt rename to patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableBooleanEncodedValue.kt diff --git a/core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableByteEncodedValue.kt b/patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableByteEncodedValue.kt similarity index 100% rename from core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableByteEncodedValue.kt rename to patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableByteEncodedValue.kt diff --git a/core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableCharEncodedValue.kt b/patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableCharEncodedValue.kt similarity index 100% rename from core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableCharEncodedValue.kt rename to patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableCharEncodedValue.kt diff --git a/core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableDoubleEncodedValue.kt b/patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableDoubleEncodedValue.kt similarity index 100% rename from core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableDoubleEncodedValue.kt rename to patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableDoubleEncodedValue.kt diff --git a/core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableEncodedValue.kt b/patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableEncodedValue.kt similarity index 100% rename from core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableEncodedValue.kt rename to patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableEncodedValue.kt diff --git a/core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableEnumEncodedValue.kt b/patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableEnumEncodedValue.kt similarity index 100% rename from core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableEnumEncodedValue.kt rename to patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableEnumEncodedValue.kt diff --git a/core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableFieldEncodedValue.kt b/patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableFieldEncodedValue.kt similarity index 100% rename from core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableFieldEncodedValue.kt rename to patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableFieldEncodedValue.kt diff --git a/core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableFloatEncodedValue.kt b/patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableFloatEncodedValue.kt similarity index 100% rename from core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableFloatEncodedValue.kt rename to patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableFloatEncodedValue.kt diff --git a/core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableIntEncodedValue.kt b/patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableIntEncodedValue.kt similarity index 100% rename from core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableIntEncodedValue.kt rename to patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableIntEncodedValue.kt diff --git a/core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableLongEncodedValue.kt b/patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableLongEncodedValue.kt similarity index 100% rename from core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableLongEncodedValue.kt rename to patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableLongEncodedValue.kt diff --git a/core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableMethodEncodedValue.kt b/patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableMethodEncodedValue.kt similarity index 100% rename from core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableMethodEncodedValue.kt rename to patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableMethodEncodedValue.kt diff --git a/core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableMethodHandleEncodedValue.kt b/patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableMethodHandleEncodedValue.kt similarity index 100% rename from core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableMethodHandleEncodedValue.kt rename to patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableMethodHandleEncodedValue.kt diff --git a/core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableMethodTypeEncodedValue.kt b/patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableMethodTypeEncodedValue.kt similarity index 100% rename from core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableMethodTypeEncodedValue.kt rename to patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableMethodTypeEncodedValue.kt diff --git a/core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableNullEncodedValue.kt b/patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableNullEncodedValue.kt similarity index 100% rename from core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableNullEncodedValue.kt rename to patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableNullEncodedValue.kt diff --git a/core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableShortEncodedValue.kt b/patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableShortEncodedValue.kt similarity index 100% rename from core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableShortEncodedValue.kt rename to patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableShortEncodedValue.kt diff --git a/core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableStringEncodedValue.kt b/patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableStringEncodedValue.kt similarity index 100% rename from core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableStringEncodedValue.kt rename to patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableStringEncodedValue.kt diff --git a/core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableTypeEncodedValue.kt b/patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableTypeEncodedValue.kt similarity index 100% rename from core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableTypeEncodedValue.kt rename to patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/iface/value/MutableTypeEncodedValue.kt diff --git a/core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/mutable/MutableAnnotation.kt b/patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/mutable/MutableAnnotation.kt similarity index 100% rename from core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/mutable/MutableAnnotation.kt rename to patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/mutable/MutableAnnotation.kt diff --git a/core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/mutable/MutableAnnotationElement.kt b/patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/mutable/MutableAnnotationElement.kt similarity index 100% rename from core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/mutable/MutableAnnotationElement.kt rename to patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/mutable/MutableAnnotationElement.kt diff --git a/core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/mutable/MutableClassDef.kt b/patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/mutable/MutableClassDef.kt similarity index 100% rename from core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/mutable/MutableClassDef.kt rename to patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/mutable/MutableClassDef.kt diff --git a/core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/mutable/MutableField.kt b/patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/mutable/MutableField.kt similarity index 100% rename from core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/mutable/MutableField.kt rename to patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/mutable/MutableField.kt diff --git a/core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/mutable/MutableMethod.kt b/patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/mutable/MutableMethod.kt similarity index 100% rename from core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/mutable/MutableMethod.kt rename to patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/mutable/MutableMethod.kt diff --git a/core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/mutable/MutableMethodParameter.kt b/patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/mutable/MutableMethodParameter.kt similarity index 100% rename from core/src/commonMain/kotlin/com/android/tools/smali/dexlib2/mutable/MutableMethodParameter.kt rename to patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/mutable/MutableMethodParameter.kt diff --git a/core/src/commonMain/kotlin/java/io/File.kt b/patcher/src/commonMain/kotlin/java/io/File.kt similarity index 100% rename from core/src/commonMain/kotlin/java/io/File.kt rename to patcher/src/commonMain/kotlin/java/io/File.kt diff --git a/core/src/jvmMain/kotlin/app/revanced/patcher/patch/Patch.jvm.kt b/patcher/src/jvmMain/kotlin/app/revanced/patcher/patch/Patch.jvm.kt similarity index 58% rename from core/src/jvmMain/kotlin/app/revanced/patcher/patch/Patch.jvm.kt rename to patcher/src/jvmMain/kotlin/app/revanced/patcher/patch/Patch.jvm.kt index 0c7ffe8..947eb91 100644 --- a/core/src/jvmMain/kotlin/app/revanced/patcher/patch/Patch.jvm.kt +++ b/patcher/src/jvmMain/kotlin/app/revanced/patcher/patch/Patch.jvm.kt @@ -2,7 +2,6 @@ package app.revanced.patcher.patch import java.io.File import java.net.URLClassLoader -import java.util.function.Predicate import java.util.jar.JarFile actual val Class<*>.isPatch get() = Patch::class.java.isAssignableFrom(this) @@ -10,17 +9,24 @@ actual val Class<*>.isPatch get() = Patch::class.java.isAssignableFrom(this) /** * Loads patches from JAR files declared as public static fields * or returned by public static and non-parametrized methods. - * Patches with no name are not loaded. + * Patches with no name are not loaded. If a patches file fails to load, + * the [onFailedToLoad] callback is invoked with the file and the throwable + * and the loading continues for the other files. * * @param patchesFiles The JAR files to load the patches from. + * @param onFailedToLoad A callback invoked when a patches file fails to load. * * @return The loaded patches. */ -actual fun loadPatches(patchesFiles: Set) = loadPatches( - patchesFiles, +actual fun loadPatches( + vararg patchesFiles: File, + onFailedToLoad: (patchesFile: File, throwable: Throwable) -> Unit, +) = loadPatches( + patchesFiles = patchesFiles, { file -> JarFile(file).entries().toList().filter { it.name.endsWith(".class") } .map { it.name.substringBeforeLast('.').replace('/', '.') } }, URLClassLoader(patchesFiles.map { it.toURI().toURL() }.toTypedArray()), + onFailedToLoad = onFailedToLoad ) diff --git a/core/src/jvmMain/kotlin/collections/MutableMap.jvm.kt b/patcher/src/jvmMain/kotlin/collections/MutableMap.jvm.kt similarity index 100% rename from core/src/jvmMain/kotlin/collections/MutableMap.jvm.kt rename to patcher/src/jvmMain/kotlin/collections/MutableMap.jvm.kt diff --git a/core/src/jvmMain/kotlin/java/io/File.jvm.kt b/patcher/src/jvmMain/kotlin/java/io/File.jvm.kt similarity index 100% rename from core/src/jvmMain/kotlin/java/io/File.jvm.kt rename to patcher/src/jvmMain/kotlin/java/io/File.jvm.kt diff --git a/core/src/jvmTest/kotlin/app/revanced/patcher/ExtensionsTest.kt b/patcher/src/jvmTest/kotlin/app/revanced/patcher/ExtensionsTest.kt similarity index 100% rename from core/src/jvmTest/kotlin/app/revanced/patcher/ExtensionsTest.kt rename to patcher/src/jvmTest/kotlin/app/revanced/patcher/ExtensionsTest.kt diff --git a/patcher/src/jvmTest/kotlin/app/revanced/patcher/PatcherTest.kt b/patcher/src/jvmTest/kotlin/app/revanced/patcher/PatcherTest.kt new file mode 100644 index 0000000..3bb5227 --- /dev/null +++ b/patcher/src/jvmTest/kotlin/app/revanced/patcher/PatcherTest.kt @@ -0,0 +1,158 @@ +package app.revanced.patcher + +import app.revanced.patcher.patch.Patch +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.bytecodePatch +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.assertAll +import org.junit.jupiter.api.assertThrows +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class PatcherTest : PatcherTestBase() { + @BeforeAll + fun setUp() = setUpMock() + + @Test + fun `executes patches in correct order`() { + val executed = mutableListOf() + + val patches = setOf( + bytecodePatch { execute { executed += "1" } }, + bytecodePatch { + dependsOn( + bytecodePatch { + execute { executed += "2" } + finalize { executed += "-2" } + }, + bytecodePatch { execute { executed += "3" } }, + ) + + execute { executed += "4" } + finalize { executed += "-1" } + }, + ) + + assert(executed.isEmpty()) + + patches() + + assertEquals( + listOf("1", "2", "3", "4", "-1", "-2"), + executed, + "Expected patches to be executed in correct order.", + ) + } + + @Test + fun `handles execution of patches correctly when exceptions occur`() { + val executed = mutableListOf() + + infix fun Patch.resultsIn(equals: List) { + val patches = setOf(this) + + try { + patches() + } catch (_: PatchException) { + // Swallow expected exceptions for testing purposes. + } + + assertEquals(equals, executed, "Expected patches to be executed in correct order.") + + executed.clear() + } + + // No patches execute successfully, + // because the dependency patch throws an exception inside the execute block. + bytecodePatch { + dependsOn( + bytecodePatch { + execute { throw PatchException("1") } + finalize { executed += "-2" } + }, + ) + + execute { executed += "2" } + finalize { executed += "-1" } + } resultsIn emptyList() + + // The dependency patch is executed successfully, + // because only the dependant patch throws an exception inside the execute block. + // Patches that depend on a failed patch should not be executed, + // but patches that are dependant by a failed patch should be finalized. + bytecodePatch { + dependsOn( + bytecodePatch { + execute { executed += "1" } + finalize { executed += "-2" } + }, + ) + + execute { throw PatchException("2") } + finalize { executed += "-1" } + } resultsIn listOf("1", "-2") + + // Because the finalize block of the dependency patch is executed after the finalize block of the dependant patch, + // the dependant patch executes successfully, but the dependency patch raises an exception in the finalize block. + bytecodePatch { + dependsOn( + bytecodePatch { + execute { executed += "1" } + finalize { throw PatchException("-2") } + }, + ) + + execute { executed += "2" } + finalize { executed += "-1" } + } resultsIn listOf("1", "2", "-1") + + // The dependency patch is executed successfully, + // because the dependant patch raises an exception in the finalize block. + // Patches that depend on a failed patch should not be executed, + // but patches that are depended on by a failed patch should be executed. + bytecodePatch { + dependsOn( + bytecodePatch { + execute { executed += "1" } + finalize { executed += "-2" } + }, + ) + + execute { executed += "2" } + finalize { throw PatchException("-1") } + } resultsIn listOf("1", "2", "-2") + } + + @Test + fun `throws if unmatched fingerprint match is used`() { + with(bytecodePatchContext) { + val fingerprint = fingerprint { + strings("doesnt exist") + } + + assertThrows("Expected an exception because the fingerprint can't match.") { + fingerprint.patternMatch + } + } + } + + + @Test + fun `matches fingerprint`() { + val fingerprint = fingerprint { returns("V") } + val fingerprint2 = fingerprint { returns("V") } + val fingerprint3 = fingerprint { returns("V") } + + with(bytecodePatchContext) { + assertAll( + "Expected fingerprints to match.", + { assertNotNull(fingerprint.matchOrNull(this.classDefs.first().methods.first())) }, + { assertNotNull(fingerprint2.matchOrNull(this.classDefs.first())) }, + { assertNotNull(fingerprint3.originalClassDefOrNull) }, + ) + } + } +} diff --git a/patcher/src/jvmTest/kotlin/app/revanced/patcher/patch/PatchLoaderTest.kt b/patcher/src/jvmTest/kotlin/app/revanced/patcher/patch/PatchLoaderTest.kt new file mode 100644 index 0000000..b31eb0f --- /dev/null +++ b/patcher/src/jvmTest/kotlin/app/revanced/patcher/patch/PatchLoaderTest.kt @@ -0,0 +1,44 @@ +@file:Suppress("unused") + +package app.revanced.patcher.patch + +import org.junit.jupiter.api.Test +import kotlin.reflect.jvm.javaField +import kotlin.test.assertEquals + +internal object PatchLoaderTest { + @Test + fun `loads patches correctly`() { + val patchesClass = ::Public.javaField!!.declaringClass.name + val classLoader = ::Public.javaClass.classLoader + + val patches = getPatches(listOf(patchesClass), classLoader) + + assertEquals( + 2, + patches.size, + "Expected 2 patches to be loaded, " + + "because there's only two named patches declared as public static fields " + + "or returned by public static and non-parametrized methods.", + ) + } +} + +val publicUnnamedPatch = bytecodePatch {} // Not loaded, because it's unnamed. + +val Public by creatingBytecodePatch {} // Loaded, because it's named. + +private val privateUnnamedPatch = bytecodePatch {} // Not loaded, because it's private. + +private val Private by creatingBytecodePatch {} // Not loaded, because it's private. + +fun publicUnnamedPatchFunction() = publicUnnamedPatch // Not loaded, because it's unnamed. + +fun publicNamedPatchFunction() = bytecodePatch("Public") { } // Loaded, because it's named. + +fun parameterizedFunction(@Suppress("UNUSED_PARAMETER") param: Any) = + publicNamedPatchFunction() // Not loaded, because it's parameterized. + +private fun privateUnnamedPatchFunction() = privateUnnamedPatch // Not loaded, because it's private. + +private fun privateNamedPatchFunction() = Private // Not loaded, because it's private. diff --git a/core/src/jvmTest/kotlin/app/revanced/patcher/patch/PatchTest.kt b/patcher/src/jvmTest/kotlin/app/revanced/patcher/patch/PatchTest.kt similarity index 100% rename from core/src/jvmTest/kotlin/app/revanced/patcher/patch/PatchTest.kt rename to patcher/src/jvmTest/kotlin/app/revanced/patcher/patch/PatchTest.kt diff --git a/core/src/jvmTest/kotlin/app/revanced/patcher/patch/options/OptionsTest.kt b/patcher/src/jvmTest/kotlin/app/revanced/patcher/patch/options/OptionsTest.kt similarity index 100% rename from core/src/jvmTest/kotlin/app/revanced/patcher/patch/options/OptionsTest.kt rename to patcher/src/jvmTest/kotlin/app/revanced/patcher/patch/options/OptionsTest.kt diff --git a/core/src/jvmTest/kotlin/app/revanced/patcher/util/SmaliTest.kt b/patcher/src/jvmTest/kotlin/app/revanced/patcher/util/SmaliTest.kt similarity index 100% rename from core/src/jvmTest/kotlin/app/revanced/patcher/util/SmaliTest.kt rename to patcher/src/jvmTest/kotlin/app/revanced/patcher/util/SmaliTest.kt diff --git a/settings.gradle.kts b/settings.gradle.kts index 9fe0d61..289f08b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -21,4 +21,6 @@ dependencyResolutionManagement { } } -include(":core") +include(":patcher") +include(":matching") +include(":tests") diff --git a/tests/build.gradle.kts b/tests/build.gradle.kts new file mode 100644 index 0000000..4d60e85 --- /dev/null +++ b/tests/build.gradle.kts @@ -0,0 +1,24 @@ +import com.android.build.api.dsl.androidLibrary +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation + +plugins { + alias(libs.plugins.kotlinMultiplatform) +} + +group = "app.revanced" + +kotlin { + jvm() + + sourceSets { + commonMain.dependencies { + implementation(libs.kotlinx.coroutines.core) + implementation(libs.multidexlib2) + implementation(libs.smali) + implementation(project(":patcher")) + implementation(libs.mockk) + implementation(libs.kotlin.test) + } + } +} diff --git a/tests/src/commonMain/kotlin/app/revanced/patcher/PatcherTestBase.kt b/tests/src/commonMain/kotlin/app/revanced/patcher/PatcherTestBase.kt new file mode 100644 index 0000000..94d1cdb --- /dev/null +++ b/tests/src/commonMain/kotlin/app/revanced/patcher/PatcherTestBase.kt @@ -0,0 +1,115 @@ +package app.revanced.patcher + +import app.revanced.patcher.extensions.toInstructions +import app.revanced.patcher.patch.BytecodePatchContext +import app.revanced.patcher.patch.Patch +import app.revanced.patcher.patch.ResourcePatchContext +import com.android.tools.smali.dexlib2.Opcodes +import com.android.tools.smali.dexlib2.iface.DexFile +import com.android.tools.smali.dexlib2.immutable.ImmutableClassDef +import com.android.tools.smali.dexlib2.immutable.ImmutableMethod +import com.android.tools.smali.dexlib2.immutable.ImmutableMethodImplementation +import io.mockk.every +import io.mockk.justRun +import io.mockk.mockk +import io.mockk.mockkStatic +import lanchon.multidexlib2.MultiDexIO +import java.io.File +import java.io.InputStream + +abstract class PatcherTestBase { + protected lateinit var bytecodePatchContext: BytecodePatchContext + protected lateinit var resourcePatchContext: ResourcePatchContext + + protected fun setUpMock( + method: ImmutableMethod = ImmutableMethod( + "class", + "method", + emptyList(), + "V", + 0, + null, + null, + ImmutableMethodImplementation( + 2, + """ + const-string v0, "Hello, World!" + iput-object v0, p0, Ljava/lang/System;->out:Ljava/io/PrintStream; + iget-object v0, p0, Ljava/lang/System;->out:Ljava/io/PrintStream; + return-void + const-string v0, "This is a test." + return-object v0 + invoke-virtual { p0, v0 }, Ljava/io/PrintStream;->println(Ljava/lang/String;)V + invoke-static { p0 }, Ljava/lang/System;->currentTimeMillis()J + check-cast p0, Ljava/io/PrintStream; + """.toInstructions(), + null, + null + ), + ), + ) { + resourcePatchContext = mockk(relaxed = true) + bytecodePatchContext = mockk bytecodePatchContext@{ + mockkStatic(MultiDexIO::readDexFile) + every { + MultiDexIO.readDexFile( + any(), + any(), + any(), + any(), + any() + ) + } returns mockk { + every { classes } returns mutableSetOf( + ImmutableClassDef( + "class", + 0, + null, + null, + null, + null, + null, + listOf(method), + ) + ) + every { opcodes } returns Opcodes.getDefault() + } + + every { this@bytecodePatchContext.getProperty("apkFile") } returns mockk() + + every { this@bytecodePatchContext.classDefs } returns ClassDefs().apply { + invokePrivateMethod($$"initializeCache$patcher") + } + + every { get() } returns emptySet() + + justRun { this@bytecodePatchContext["extendWith"](any()) } + } + } + + protected operator fun Set.invoke() { + runCatching { + execute( + bytecodePatchContext, + resourcePatchContext + ) { } + }.fold( + { it.dexFiles }, + { it.printStackTrace() } + ) + } + + protected operator fun Patch.invoke() = setOf(this)() + + private fun Any.setPrivateField(field: String, value: Any) { + this::class.java.getDeclaredField(field).apply { + this.isAccessible = true + set(this@setPrivateField, value) + } + } + + private fun Any.invokePrivateMethod(method: String) = + javaClass.getDeclaredMethod(method).apply { + isAccessible = true + }.invoke(this) +} \ No newline at end of file