mirror of
https://github.com/ReVanced/revanced-patcher.git
synced 2026-01-10 21:36:16 +00:00
Refactor matching API into separate module, simplify & refactor matching code, convert patcher to functions/DSL, refactor & greatly simplify internal code, refactor & simplify patch api internal code, update deps, fix workflow, add callback for patches files failed to load to be able to be able to load the rest of the patches
This commit is contained in:
13
.github/workflows/release.yml
vendored
13
.github/workflows/release.yml
vendored
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
@@ -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<ClassDef>.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<out ExceptionHandler>.() -> Boolean) = tryBlocks.any(predicate)
|
||||
|
||||
fun MethodImplementation.anyDebugItem(predicate: Any.() -> Boolean) = debugItems.any(predicate)
|
||||
|
||||
fun Iterable<Instruction>.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<T> internal constructor(
|
||||
private val block: BytecodePatchContext.(KProperty<*>) -> T
|
||||
) : ReadOnlyProperty<BytecodePatchContext, T> {
|
||||
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 <T> indexedMatcher() = IndexedMatcher<T>()
|
||||
|
||||
fun <T> indexedMatcher(build: IndexedMatcher<T>.() -> Unit) =
|
||||
IndexedMatcher<T>().apply(build)
|
||||
|
||||
fun <T> Iterable<T>.matchIndexed(build: IndexedMatcher<T>.() -> Unit) =
|
||||
indexedMatcher(build)(this)
|
||||
|
||||
context(_: PredicateContext)
|
||||
fun <T> Iterable<T>.rememberedMatchIndexed(key: Any, build: IndexedMatcher<T>.() -> Unit) =
|
||||
indexedMatcher<T>()(key, this, build)
|
||||
|
||||
context(matcher: IndexedMatcher<T>)
|
||||
fun <T> head(
|
||||
predicate: T.(lastMatchedIndex: Int, currentIndex: Int) -> Boolean
|
||||
): T.(Int, Int) -> Boolean = { lastMatchedIndex, currentIndex ->
|
||||
currentIndex == 0 && predicate(lastMatchedIndex, currentIndex)
|
||||
}
|
||||
|
||||
context(matcher: IndexedMatcher<T>)
|
||||
fun <T> head(predicate: T.() -> Boolean): T.(Int, Int) -> Boolean =
|
||||
head { _, _ -> predicate() }
|
||||
|
||||
context(matcher: IndexedMatcher<T>)
|
||||
fun <T> 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<T>)
|
||||
fun <T> after(range: IntRange = 1..1, predicate: T.() -> Boolean) =
|
||||
after(range) { _, _ -> predicate() }
|
||||
|
||||
context(matcher: IndexedMatcher<T>)
|
||||
operator fun <T> (T.(Int, Int) -> Boolean).unaryPlus() = matcher.add(this)
|
||||
|
||||
class IndexedMatcher<T> : Matcher<T, T.(lastMatchedIndex: Int, currentIndex: Int) -> Boolean>() {
|
||||
private val _indices: MutableList<Int> = mutableListOf()
|
||||
val indices: List<Int> = _indices
|
||||
|
||||
private var lastMatchedIndex = -1
|
||||
private var currentIndex = -1
|
||||
var nextIndex: Int? = null
|
||||
|
||||
override fun invoke(haystack: Iterable<T>): Boolean {
|
||||
// Normalize to list
|
||||
val hay = haystack as? List<T> ?: 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<Frame>()
|
||||
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 <T, U, reified M : Matcher<T, U>> M.invoke(key: Any, iterable: Iterable<T>, builder: M.() -> Unit) =
|
||||
remembered(key) { apply(builder) }(iterable)
|
||||
|
||||
context(_: PredicateContext)
|
||||
inline operator fun <T, U, reified M : Matcher<T, U>> M.invoke(
|
||||
iterable: Iterable<T>,
|
||||
builder: M.() -> Unit
|
||||
) = invoke(this@invoke.hashCode(), iterable, builder)
|
||||
|
||||
abstract class Matcher<T, U> : MutableList<U> by mutableListOf() {
|
||||
var matchIndex = -1
|
||||
protected set
|
||||
|
||||
abstract operator fun invoke(haystack: Iterable<T>): Boolean
|
||||
}
|
||||
|
||||
// endregion Matcher
|
||||
|
||||
class PredicateContext internal constructor() : MutableMap<Any, Any> by mutableMapOf()
|
||||
|
||||
context(context: PredicateContext)
|
||||
inline fun <reified V : Any> remembered(key: Any, defaultValue: () -> V) =
|
||||
context[key] as? V ?: defaultValue().also { context[key] = it }
|
||||
|
||||
|
||||
fun <T> T.declarativePredicate(build: DeclarativePredicateBuilder<T>.() -> Unit) =
|
||||
DeclarativePredicateBuilder<T>().apply(build).all(this)
|
||||
|
||||
context(_: PredicateContext)
|
||||
fun <T> T.rememberedDeclarativePredicate(key: Any, block: DeclarativePredicateBuilder<T>.() -> Unit): Boolean =
|
||||
remembered(key) { DeclarativePredicateBuilder<T>().apply(block) }.all(this)
|
||||
|
||||
context(_: PredicateContext)
|
||||
private fun <T> T.rememberedDeclarativePredicate(predicate: context(PredicateContext, T) DeclarativePredicateBuilder<T>.() -> Unit) =
|
||||
rememberedDeclarativePredicate("declarative predicate build") { predicate() }
|
||||
|
||||
fun BytecodePatchContext.firstClassDefByDeclarativePredicateOrNull(
|
||||
predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
|
||||
) = firstClassDefOrNull { rememberedDeclarativePredicate(predicate) }
|
||||
|
||||
fun BytecodePatchContext.firstClassDefByDeclarativePredicate(
|
||||
predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
|
||||
) = requireNotNull(firstClassDefByDeclarativePredicateOrNull(predicate))
|
||||
|
||||
fun BytecodePatchContext.firstClassDefMutableByDeclarativePredicateOrNull(
|
||||
predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
|
||||
) = firstClassDefMutableOrNull { rememberedDeclarativePredicate(predicate) }
|
||||
|
||||
fun BytecodePatchContext.firstClassDefMutableByDeclarativePredicate(
|
||||
predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
|
||||
) = requireNotNull(firstClassDefMutableByDeclarativePredicateOrNull(predicate))
|
||||
|
||||
fun BytecodePatchContext.firstClassDefByDeclarativePredicateOrNull(
|
||||
type: String,
|
||||
predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
|
||||
) = firstClassDefOrNull(type) { rememberedDeclarativePredicate(predicate) }
|
||||
|
||||
fun BytecodePatchContext.firstClassDefByDeclarativePredicate(
|
||||
type: String,
|
||||
predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
|
||||
) = requireNotNull(firstClassDefByDeclarativePredicateOrNull(type, predicate))
|
||||
|
||||
fun BytecodePatchContext.firstClassDefMutableByDeclarativePredicateOrNull(
|
||||
type: String,
|
||||
predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
|
||||
) = firstClassDefMutableOrNull(type) { rememberedDeclarativePredicate(predicate) }
|
||||
|
||||
fun BytecodePatchContext.firstClassDefMutableByDeclarativePredicate(
|
||||
type: String,
|
||||
predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
|
||||
) = requireNotNull(firstClassDefMutableByDeclarativePredicateOrNull(type, predicate))
|
||||
|
||||
fun BytecodePatchContext.firstMethodByDeclarativePredicateOrNull(
|
||||
predicate: context(PredicateContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
|
||||
) = firstMethodOrNull { rememberedDeclarativePredicate(predicate) }
|
||||
|
||||
fun BytecodePatchContext.firstMethodByDeclarativePredicate(
|
||||
predicate: context(PredicateContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
|
||||
) = requireNotNull(firstMethodByDeclarativePredicateOrNull(predicate))
|
||||
|
||||
fun BytecodePatchContext.firstMethodMutableByDeclarativePredicateOrNull(
|
||||
predicate: context(PredicateContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
|
||||
) = firstMethodMutableOrNull { rememberedDeclarativePredicate(predicate) }
|
||||
|
||||
fun BytecodePatchContext.firstMethodMutableByDeclarativePredicate(
|
||||
predicate: context(PredicateContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
|
||||
) = requireNotNull(firstMethodMutableByDeclarativePredicateOrNull(predicate))
|
||||
|
||||
fun BytecodePatchContext.firstMethodByDeclarativePredicateOrNull(
|
||||
vararg strings: String,
|
||||
predicate: context(PredicateContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
|
||||
) = firstMethodOrNull(*strings) { rememberedDeclarativePredicate(predicate) }
|
||||
|
||||
fun BytecodePatchContext.firstMethodByDeclarativePredicate(
|
||||
vararg strings: String,
|
||||
predicate: context(PredicateContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
|
||||
) = requireNotNull(firstMethodByDeclarativePredicateOrNull(*strings, predicate = predicate))
|
||||
|
||||
fun BytecodePatchContext.firstMethodMutableByDeclarativePredicateOrNull(
|
||||
vararg strings: String,
|
||||
predicate: context(PredicateContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
|
||||
) = firstMethodMutableOrNull(*strings) { rememberedDeclarativePredicate(predicate) }
|
||||
|
||||
fun BytecodePatchContext.firstMethodMutableByDeclarativePredicate(
|
||||
vararg strings: String,
|
||||
predicate: context(PredicateContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
|
||||
) = requireNotNull(firstMethodMutableByDeclarativePredicateOrNull(*strings, predicate = predicate))
|
||||
|
||||
fun gettingFirstClassDefByDeclarativePredicateOrNull(
|
||||
type: String,
|
||||
predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
|
||||
) = gettingFirstClassDefOrNull(type) { rememberedDeclarativePredicate(predicate) }
|
||||
|
||||
fun gettingFirstClassDefByDeclarativePredicate(
|
||||
type: String,
|
||||
predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
|
||||
) = CachedReadOnlyProperty { firstClassDefByDeclarativePredicate(type, predicate) }
|
||||
|
||||
fun gettingFirstClassDefMutableByDeclarativePredicateOrNull(
|
||||
type: String,
|
||||
predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
|
||||
) = gettingFirstClassDefMutableOrNull(type) { rememberedDeclarativePredicate(predicate) }
|
||||
|
||||
fun gettingFirstClassDefMutableByDeclarativePredicate(
|
||||
type: String,
|
||||
predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
|
||||
) = CachedReadOnlyProperty { firstClassDefMutableByDeclarativePredicate(type, predicate) }
|
||||
|
||||
fun gettingFirstClassDefByDeclarativePredicateOrNull(
|
||||
predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
|
||||
) = gettingFirstClassDefOrNull { rememberedDeclarativePredicate(predicate) }
|
||||
|
||||
fun gettingFirstClassDefByDeclarativePredicate(
|
||||
predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
|
||||
) = CachedReadOnlyProperty { firstClassDefByDeclarativePredicate(predicate) }
|
||||
|
||||
fun gettingFirstClassDefMutableByDeclarativePredicateOrNull(
|
||||
predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
|
||||
) = gettingFirstClassDefMutableOrNull { rememberedDeclarativePredicate(predicate) }
|
||||
|
||||
fun gettingFirstClassDefMutableByDeclarativePredicate(
|
||||
predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
|
||||
) = CachedReadOnlyProperty { firstClassDefMutableByDeclarativePredicate(predicate) }
|
||||
|
||||
fun gettingFirstMethodByDeclarativePredicateOrNull(
|
||||
predicate: context(PredicateContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
|
||||
) = gettingFirstMethodOrNull { rememberedDeclarativePredicate(predicate) }
|
||||
|
||||
fun gettingFirstMethodByDeclarativePredicate(
|
||||
predicate: context(PredicateContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
|
||||
) = CachedReadOnlyProperty { firstMethodByDeclarativePredicate(predicate = predicate) }
|
||||
|
||||
fun gettingFirstMethodMutableByDeclarativePredicateOrNull(
|
||||
predicate: context(PredicateContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
|
||||
) = gettingFirstMethodMutableOrNull { rememberedDeclarativePredicate(predicate) }
|
||||
|
||||
fun gettingFirstMethodMutableByDeclarativePredicate(
|
||||
predicate: context(PredicateContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
|
||||
) = CachedReadOnlyProperty { firstMethodMutableByDeclarativePredicate(predicate = predicate) }
|
||||
|
||||
fun gettingFirstMethodByDeclarativePredicateOrNull(
|
||||
vararg strings: String,
|
||||
predicate: context(PredicateContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
|
||||
) = gettingFirstMethodOrNull(*strings) { rememberedDeclarativePredicate(predicate) }
|
||||
|
||||
fun gettingFirstMethodByDeclarativePredicate(
|
||||
vararg strings: String,
|
||||
predicate: context(PredicateContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
|
||||
) = CachedReadOnlyProperty { firstMethodByDeclarativePredicate(*strings, predicate = predicate) }
|
||||
|
||||
fun gettingFirstMethodMutableByDeclarativePredicateOrNull(
|
||||
vararg strings: String,
|
||||
predicate: context(PredicateContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
|
||||
) = gettingFirstMethodMutableOrNull(*strings) { rememberedDeclarativePredicate(predicate) }
|
||||
|
||||
fun gettingFirstMethodMutableByDeclarativePredicate(
|
||||
vararg strings: String,
|
||||
predicate: context(PredicateContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
|
||||
) = CachedReadOnlyProperty { firstMethodMutableByDeclarativePredicate(*strings, predicate = predicate) }
|
||||
|
||||
|
||||
class DeclarativePredicateBuilder<T> internal constructor() {
|
||||
private val children = mutableListOf<T.() -> Boolean>()
|
||||
|
||||
fun anyOf(block: DeclarativePredicateBuilder<T>.() -> Unit) {
|
||||
val child = DeclarativePredicateBuilder<T>().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<Instruction>, MutableList<String>) DeclarativePredicateBuilder<Method>.() -> Unit
|
||||
) = with(indexedMatcher<Instruction>()) matcher@{
|
||||
with(mutableListOf<String>()) 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`<Instruction22t>()),
|
||||
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`<Instruction22t>()
|
||||
+`is`<WideLiteralInstruction>()
|
||||
+allOf(`is`<ReferenceInstruction>(), string("test"))
|
||||
+`is`<ReferenceInstruction> { reference !is StringReference }
|
||||
+`is`<VariableRegisterInstruction> { 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 <reified T : Instruction> `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<String>, builder: DeclarativePredicateBuilder<Method>)
|
||||
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<String>)
|
||||
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<Method>.accessFlags(vararg flags: AccessFlags) =
|
||||
predicate { accessFlags(*flags) }
|
||||
|
||||
fun DeclarativePredicateBuilder<Method>.returnType(
|
||||
returnType: String,
|
||||
compare: String.(String) -> Boolean = String::startsWith
|
||||
) = predicate { this.returnType.compare(returnType) }
|
||||
|
||||
fun DeclarativePredicateBuilder<Method>.name(
|
||||
name: String,
|
||||
compare: String.(String) -> Boolean = String::equals
|
||||
) =
|
||||
predicate { this.name.compare(name) }
|
||||
|
||||
fun DeclarativePredicateBuilder<Method>.definingClass(
|
||||
definingClass: String,
|
||||
compare: String.(String) -> Boolean = String::equals
|
||||
) = predicate { this.definingClass.compare(definingClass) }
|
||||
|
||||
fun DeclarativePredicateBuilder<Method>.parameterTypes(vararg parameterTypePrefixes: String) = predicate {
|
||||
parameterTypes.size == parameterTypePrefixes.size && parameterTypes.zip(parameterTypePrefixes)
|
||||
.all { (a, b) -> a.startsWith(b) }
|
||||
}
|
||||
|
||||
context(matcher: IndexedMatcher<Instruction>)
|
||||
fun DeclarativePredicateBuilder<Method>.instructions(
|
||||
build: IndexedMatcher<Instruction>.() -> Unit
|
||||
) {
|
||||
matcher.apply(build)
|
||||
predicate { implementation { matcher(instructions) } }
|
||||
}
|
||||
|
||||
context(matcher: IndexedMatcher<Instruction>)
|
||||
fun DeclarativePredicateBuilder<Method>.instructions(
|
||||
vararg predicates: Instruction.(currentIndex: Int, lastMatchedIndex: Int) -> Boolean
|
||||
) = instructions { addAll(predicates) }
|
||||
|
||||
fun DeclarativePredicateBuilder<Method>.custom(block: Method.() -> Boolean) {
|
||||
predicate { block() }
|
||||
}
|
||||
|
||||
class Composition internal constructor(
|
||||
val indices: List<Int>,
|
||||
val strings: List<String>,
|
||||
private val predicate: context(PredicateContext, Method) DeclarativePredicateBuilder<Method>.() -> 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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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<Patch<*>>) {
|
||||
// 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<Patch<*>, 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<Patch<*>, 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())
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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<Patch<*>>()
|
||||
|
||||
/**
|
||||
* The set of all [Patch]es and their dependencies.
|
||||
*/
|
||||
internal val allPatches = mutableSetOf<Patch<*>>()
|
||||
|
||||
/**
|
||||
* 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()
|
||||
}
|
||||
@@ -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<PatchedDexFile>,
|
||||
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<String>,
|
||||
val deleteResources: Set<String>,
|
||||
)
|
||||
}
|
||||
@@ -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<PackageName, Set<VersionName>?>
|
||||
|
||||
|
||||
/**
|
||||
* A common interface for contexts such as [ResourcePatchContext] and [BytecodePatchContext].
|
||||
*/
|
||||
|
||||
sealed interface PatchContext<T> : Supplier<T>
|
||||
|
||||
/**
|
||||
* 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<C : PatchContext<*>>(
|
||||
val name: String?,
|
||||
val description: String?,
|
||||
val use: Boolean,
|
||||
val dependencies: Set<Patch<*>>,
|
||||
val compatiblePackages: Set<Package>?,
|
||||
options: Set<Option<*>>,
|
||||
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<Patch<*>> = 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<Patch<*>>.forEachRecursively(
|
||||
visited: MutableSet<Patch<*>> = 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<Package>?,
|
||||
dependencies: Set<Patch<*>>,
|
||||
options: Set<Option<*>>,
|
||||
internal val extensionInputStream: Supplier<InputStream>?,
|
||||
executeBlock: (BytecodePatchContext) -> Unit,
|
||||
finalizeBlock: ((BytecodePatchContext) -> Unit)?,
|
||||
) : Patch<BytecodePatchContext>(
|
||||
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<Package>?,
|
||||
dependencies: Set<Patch<*>>,
|
||||
options: Set<Option<*>>,
|
||||
executeBlock: (ResourcePatchContext) -> Unit,
|
||||
finalizeBlock: ((ResourcePatchContext) -> Unit)?,
|
||||
) : Patch<ResourcePatchContext>(
|
||||
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<Package>?,
|
||||
dependencies: Set<Patch<*>>,
|
||||
options: Set<Option<*>>,
|
||||
executeBlock: (ResourcePatchContext) -> Unit,
|
||||
finalizeBlock: ((ResourcePatchContext) -> Unit)?,
|
||||
) : Patch<ResourcePatchContext>(
|
||||
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<C : PatchContext<*>>(
|
||||
protected val name: String?,
|
||||
protected val description: String?,
|
||||
protected val use: Boolean,
|
||||
) {
|
||||
protected var compatiblePackages: MutableSet<Package>? = null
|
||||
protected var dependencies = mutableSetOf<Patch<*>>()
|
||||
protected val options = mutableSetOf<Option<*>>()
|
||||
|
||||
protected var executionBlock: ((C) -> Unit) = { }
|
||||
protected var finalizeBlock: ((C) -> Unit)? = null
|
||||
|
||||
/**
|
||||
* Add an option to the patch.
|
||||
*
|
||||
* @return The added option.
|
||||
*/
|
||||
operator fun <T> Option<T>.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<String>? = 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<C>
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 : PatchBuilder<*>> 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<BytecodePatchContext>(name, description, use) {
|
||||
internal var extensionInputStream: Supplier<InputStream>? = 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<Any?, BytecodePatch> { _, 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<ResourcePatchContext>(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<Any?, RawResourcePatch> { _, 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<ResourcePatchContext>(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<Any?, ResourcePatch> { _, 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<File, Set<Patch<*>>>) : Set<Patch<*>>
|
||||
by patchesByFile.values.flatten().toSet()
|
||||
|
||||
internal fun loadPatches(
|
||||
patchesFiles: Set<File>,
|
||||
getBinaryClassNames: (patchesFile: File) -> List<String>,
|
||||
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<File>): Patches
|
||||
|
||||
internal expect val Class<*>.isPatch: Boolean
|
||||
@@ -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<PatcherResult.PatchedResources?> {
|
||||
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<String>()
|
||||
|
||||
/**
|
||||
* 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,
|
||||
}
|
||||
}
|
||||
@@ -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<Patcher> {
|
||||
// Can't mock private fields, until https://github.com/mockk/mockk/issues/1244 is resolved.
|
||||
setPrivateField(
|
||||
"config",
|
||||
mockk<PatcherConfig> {
|
||||
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<PatcherContext> {
|
||||
every { bytecodeContext } returns mockk<BytecodePatchContext> context@{
|
||||
every { config } returns mockk<PatcherConfig> {
|
||||
every { apkFile } returns mockk()
|
||||
}
|
||||
|
||||
mockkStatic(MultiDexIO::readDexFile)
|
||||
every {
|
||||
MultiDexIO.readDexFile(
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any()
|
||||
)
|
||||
} returns mockk<DexFile> {
|
||||
every { classes } returns classDefs
|
||||
every { opcodes } returns Opcodes.getDefault()
|
||||
}
|
||||
every { this@context.classDefs } returns ClassDefs().apply { initializeCache() }
|
||||
every { mergeExtension(any<BytecodePatch>()) } just runs
|
||||
}
|
||||
}
|
||||
|
||||
every { patcher.context } returns patcherContext
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `executes patches in correct order`() {
|
||||
val executed = mutableListOf<String>()
|
||||
|
||||
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<String>()
|
||||
|
||||
infix fun Patch<*>.produces(equals: List<String>) {
|
||||
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<PatchException>("Expected an exception because the fingerprint can't match.") { patch() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `matcher finds indices correctly`() {
|
||||
val iterable = (1..10).toList()
|
||||
val matcher = indexedMatcher<Int>()
|
||||
|
||||
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`<TwoRegisterInstruction>(),
|
||||
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<Patch<*>>.invoke(): List<PatchResult> {
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<File>()),
|
||||
{ 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.",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
85
matching/build.gradle.kts
Normal file
85
matching/build.gradle.kts
Normal file
@@ -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<Test>("jvmTest") {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
}
|
||||
|
||||
mavenPublishing {
|
||||
publishing {
|
||||
repositories {
|
||||
maven {
|
||||
name = "githubPackages"
|
||||
url = uri("https://maven.pkg.github.com/revanced/revanced-patcher")
|
||||
credentials(PasswordCredentials::class)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
signAllPublications()
|
||||
extensions.getByType<SigningExtension>().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"
|
||||
}
|
||||
}
|
||||
}
|
||||
653
matching/src/commonMain/kotlin/app/revanced/patcher/Matching.kt
Normal file
653
matching/src/commonMain/kotlin/app/revanced/patcher/Matching.kt
Normal file
@@ -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<ClassDef>.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<out ExceptionHandler>.() -> Boolean) = tryBlocks.any(predicate)
|
||||
|
||||
fun MethodImplementation.anyDebugItem(predicate: Any.() -> Boolean) = debugItems.any(predicate)
|
||||
|
||||
fun Iterable<Instruction>.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<Any, Any> by mutableMapOf()
|
||||
|
||||
// region Matcher
|
||||
|
||||
// region IndexedMatcher
|
||||
|
||||
fun <T> indexedMatcher() = IndexedMatcher<T>()
|
||||
|
||||
fun <T> indexedMatcher(build: IndexedMatcher<T>.() -> Unit) =
|
||||
IndexedMatcher<T>().apply(build)
|
||||
|
||||
fun <T> Iterable<T>.matchIndexed(build: IndexedMatcher<T>.() -> Unit) =
|
||||
indexedMatcher(build)(this)
|
||||
|
||||
context(_: PredicateContext)
|
||||
fun <T> Iterable<T>.rememberedMatchIndexed(key: Any, build: IndexedMatcher<T>.() -> Unit) =
|
||||
indexedMatcher<T>()(key, this, build)
|
||||
|
||||
context(_: IndexedMatcher<T>)
|
||||
fun <T> head(
|
||||
predicate: T.(lastMatchedIndex: Int, currentIndex: Int) -> Boolean
|
||||
): T.(Int, Int) -> Boolean = { lastMatchedIndex, currentIndex ->
|
||||
currentIndex == 0 && predicate(lastMatchedIndex, currentIndex)
|
||||
}
|
||||
|
||||
context(_: IndexedMatcher<T>)
|
||||
fun <T> head(predicate: T.() -> Boolean): T.(Int, Int) -> Boolean =
|
||||
head { _, _ -> predicate() }
|
||||
|
||||
context(matcher: IndexedMatcher<T>)
|
||||
fun <T> 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<T>)
|
||||
fun <T> after(range: IntRange = 1..1, predicate: T.() -> Boolean) =
|
||||
after(range) { _, _ -> predicate() }
|
||||
|
||||
context(matcher: IndexedMatcher<T>)
|
||||
operator fun <T> (T.(Int, Int) -> Boolean).unaryPlus() = matcher.add(this)
|
||||
|
||||
class IndexedMatcher<T> : Matcher<T, T.(lastMatchedIndex: Int, currentIndex: Int) -> Boolean>() {
|
||||
val indices: List<Int>
|
||||
field = mutableListOf()
|
||||
|
||||
private var lastMatchedIndex = -1
|
||||
private var currentIndex = -1
|
||||
var nextIndex: Int? = null
|
||||
|
||||
override fun invoke(haystack: Iterable<T>): Boolean {
|
||||
// Normalize to list
|
||||
val hay = haystack as? List<T> ?: 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<Frame>()
|
||||
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 <T, U, reified M : Matcher<T, U>> M.invoke(
|
||||
key: Any,
|
||||
iterable: Iterable<T>,
|
||||
builder: M.() -> Unit
|
||||
) = remembered(key) { apply(builder) }(iterable)
|
||||
|
||||
context(_: PredicateContext)
|
||||
inline operator fun <T, U, reified M : Matcher<T, U>> M.invoke(
|
||||
iterable: Iterable<T>,
|
||||
builder: M.() -> Unit
|
||||
) = invoke(this@invoke.hashCode(), iterable, builder)
|
||||
|
||||
abstract class Matcher<T, U> : MutableList<U> by mutableListOf() {
|
||||
var matchIndex = -1
|
||||
protected set
|
||||
|
||||
abstract operator fun invoke(haystack: Iterable<T>): Boolean
|
||||
}
|
||||
|
||||
// endregion Matcher
|
||||
|
||||
context(context: PredicateContext)
|
||||
|
||||
inline fun <reified V : Any> remembered(key: Any, defaultValue: () -> V) =
|
||||
context[key] as? V ?: defaultValue().also { context[key] = it }
|
||||
|
||||
private fun <T> cachedReadOnlyProperty(block: BytecodePatchContext.(KProperty<*>) -> T) =
|
||||
object : ReadOnlyProperty<BytecodePatchContext, T> {
|
||||
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<ClassDef.() -> Boolean>) () -> Unit
|
||||
|
||||
private typealias DeclarativeMethodPredicate = context(PredicateContext, MutableList<Method.() -> Boolean>) () -> Unit
|
||||
|
||||
fun <T> T.declarativePredicate(build: context(MutableList<T.() -> Boolean>) () -> Unit) =
|
||||
context(mutableListOf<T.() -> Boolean>().apply(build)) {
|
||||
all(this)
|
||||
}
|
||||
|
||||
context(_: PredicateContext)
|
||||
fun <T> T.rememberedDeclarativePredicate(key: Any, block: context(MutableList<T.() -> Boolean>) () -> Unit) =
|
||||
context(remembered(key) { mutableListOf<T.() -> Boolean>().apply(block) }) {
|
||||
all(this)
|
||||
}
|
||||
|
||||
context(_: PredicateContext)
|
||||
private fun <T> T.rememberedDeclarativePredicate(
|
||||
predicate: context(PredicateContext, MutableList<T.() -> 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<T.() -> Boolean>)
|
||||
fun <T> allOf(block: MutableList<T.() -> Boolean>.() -> Unit) {
|
||||
val child = mutableListOf<T.() -> Boolean>().apply(block)
|
||||
list.add { child.all { it() } }
|
||||
}
|
||||
|
||||
context(list: MutableList<T.() -> Boolean>)
|
||||
fun <T> anyOf(block: MutableList<T.() -> Boolean>.() -> Unit) {
|
||||
val child = mutableListOf<T.() -> Boolean>().apply(block)
|
||||
list.add { child.any { it() } }
|
||||
}
|
||||
|
||||
context(list: MutableList<T.() -> Boolean>)
|
||||
fun <T> predicate(block: T.() -> Boolean) {
|
||||
list.add(block)
|
||||
}
|
||||
|
||||
context(list: MutableList<T.() -> Boolean>)
|
||||
fun <T> all(target: T): Boolean = list.all { target.it() }
|
||||
|
||||
context(list: MutableList<T.() -> Boolean>)
|
||||
fun <T> any(target: T): Boolean = list.any { target.it() }
|
||||
|
||||
fun firstMethodBuilder(
|
||||
vararg strings: String,
|
||||
builder:
|
||||
context(PredicateContext, MutableList<Method.() -> Boolean>, IndexedMatcher<Instruction>, MutableList<String>) () -> Unit
|
||||
) = with(mutableListOf<String>()) stringsList@{
|
||||
addAll(strings)
|
||||
|
||||
with(indexedMatcher<Instruction>()) {
|
||||
Match(indices = indices, strings = this@stringsList) { builder() }
|
||||
}
|
||||
}
|
||||
|
||||
context(_: MutableList<Method.() -> Boolean>)
|
||||
fun accessFlags(vararg flags: AccessFlags) =
|
||||
predicate { accessFlags(*flags) }
|
||||
|
||||
context(_: MutableList<Method.() -> Boolean>)
|
||||
fun returnType(
|
||||
returnType: String,
|
||||
compare: String.(String) -> Boolean = String::startsWith
|
||||
) = predicate { this.returnType.compare(returnType) }
|
||||
|
||||
context(_: MutableList<Method.() -> Boolean>)
|
||||
fun name(
|
||||
name: String,
|
||||
compare: String.(String) -> Boolean = String::equals
|
||||
) = predicate { this.name.compare(name) }
|
||||
|
||||
context(_: MutableList<Method.() -> Boolean>)
|
||||
fun definingClass(
|
||||
definingClass: String,
|
||||
compare: String.(String) -> Boolean = String::equals
|
||||
) = predicate { this.definingClass.compare(definingClass) }
|
||||
|
||||
context(_: MutableList<Method.() -> Boolean>)
|
||||
fun parameterTypes(vararg parameterTypePrefixes: String) = predicate {
|
||||
parameterTypes.size == parameterTypePrefixes.size && parameterTypes.zip(parameterTypePrefixes)
|
||||
.all { (a, b) -> a.startsWith(b) }
|
||||
}
|
||||
|
||||
context(_: MutableList<Method.() -> Boolean>, matcher: IndexedMatcher<Instruction>)
|
||||
fun instructions(
|
||||
build: context(IndexedMatcher<Instruction>) () -> Unit
|
||||
) {
|
||||
build()
|
||||
predicate { implementation { matcher(instructions) } }
|
||||
}
|
||||
|
||||
context(_: MutableList<Method.() -> Boolean>, matcher: IndexedMatcher<Instruction>)
|
||||
fun instructions(
|
||||
vararg predicates: Instruction.(currentIndex: Int, lastMatchedIndex: Int) -> Boolean
|
||||
) = instructions {
|
||||
predicates.forEach { +it }
|
||||
}
|
||||
|
||||
context(_: MutableList<Method.() -> Boolean>)
|
||||
fun custom(block: Method.() -> Boolean) {
|
||||
predicate { block() }
|
||||
}
|
||||
|
||||
context(_: IndexedMatcher<Instruction>)
|
||||
inline fun <reified T : Instruction> `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<String>)
|
||||
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<String>)
|
||||
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<Int>,
|
||||
val strings: List<String>,
|
||||
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)
|
||||
}
|
||||
@@ -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`<TwoRegisterInstruction>(),
|
||||
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<Int>()
|
||||
|
||||
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."
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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<ProcessResources>("jvmProcessResources") {
|
||||
expand("projectVersion" to project.version)
|
||||
}
|
||||
|
||||
named<Test>("jvmTest") {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
@@ -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<File>) = 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<File>) = loadPatches(
|
||||
patchesFiles.joinToString(File.pathSeparator) { it.absolutePath },
|
||||
null,
|
||||
null, null
|
||||
)
|
||||
),
|
||||
onFailedToLoad
|
||||
)
|
||||
@@ -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].
|
||||
163
patcher/src/commonMain/kotlin/app/revanced/patcher/Patching.kt
Normal file
163
patcher/src/commonMain/kotlin/app/revanced/patcher/Patching.kt
Normal file
@@ -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<Patch>,
|
||||
): (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<Patch>.execute(
|
||||
bytecodePatchContext: BytecodePatchContext,
|
||||
resourcePatchContext: ResourcePatchContext,
|
||||
emit: (PatchResult) -> Unit
|
||||
): PatchesResult {
|
||||
val executedPatches = LinkedHashMap<Patch, PatchResult>()
|
||||
|
||||
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<PatchedDexFile>,
|
||||
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<String>,
|
||||
val deleteResources: Set<String>,
|
||||
)
|
||||
}
|
||||
@@ -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<Set<PatcherResult.PatchedDexFile>>,
|
||||
Closeable {
|
||||
class BytecodePatchContext internal constructor(
|
||||
internal val apkFile: File,
|
||||
internal val patchedFilesPath: File,
|
||||
) : PatchContext<Set<PatchesResult.PatchedDexFile>> {
|
||||
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<PatcherResult.PatchedDexFile> {
|
||||
override fun get(): Set<PatchesResult.PatchedDexFile> {
|
||||
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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<T>
|
||||
@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<String, T?>? = 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>.(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<String, T?>? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
type: KType,
|
||||
validator: Option<T>.(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 <T : Any> set(key: String, value: T?) {
|
||||
val option = this[key]
|
||||
operator fun <T : Any> 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<String, String?>? = null,
|
||||
title: String? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<String>.(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<String, String?>? = null,
|
||||
title: String? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<String>.(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<String, Int?>? = null,
|
||||
title: String? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<Int>.(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<String, Int?>? = null,
|
||||
title: String? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<Int>.(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<String, Boolean?>? = null,
|
||||
title: String? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<Boolean>.(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<String, Boolean?>? = null,
|
||||
title: String? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<Boolean>.(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<String, Float?>? = null,
|
||||
title: String? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<Float>.(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<String, Float?>? = null,
|
||||
title: String? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<Float>.(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<String, Long?>? = null,
|
||||
title: String? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<Long>.(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<String, Long?>? = null,
|
||||
title: String? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<Long>.(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<String>? = null,
|
||||
values: Map<String, List<String>?>? = null,
|
||||
title: String? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<List<String>>.(List<String>?) -> 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<String>? = null,
|
||||
values: Map<String, List<String>?>? = null,
|
||||
title: String? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<List<String>>.(List<String>?) -> 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<Int>? = null,
|
||||
values: Map<String, List<Int>?>? = null,
|
||||
title: String? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<List<Int>>.(List<Int>?) -> 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<Int>? = null,
|
||||
values: Map<String, List<Int>?>? = null,
|
||||
title: String? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<List<Int>>.(List<Int>?) -> 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<Boolean>? = null,
|
||||
values: Map<String, List<Boolean>?>? = null,
|
||||
title: String? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<List<Boolean>>.(List<Boolean>?) -> 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<Boolean>? = null,
|
||||
values: Map<String, List<Boolean>?>? = null,
|
||||
title: String? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<List<Boolean>>.(List<Boolean>?) -> 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<Float>? = null,
|
||||
values: Map<String, List<Float>?>? = null,
|
||||
title: String? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<List<Float>>.(List<Float>?) -> 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<Long>? = null,
|
||||
values: Map<String, List<Long>?>? = null,
|
||||
title: String? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<List<Long>>.(List<Long>?) -> 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<Long>? = null,
|
||||
values: Map<String, List<Long>?>? = null,
|
||||
title: String? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<List<Long>>.(List<Long>?) -> 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 <reified T> option(
|
||||
key: String,
|
||||
name: String,
|
||||
default: T? = null,
|
||||
values: Map<String, T?>? = null,
|
||||
title: String? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
noinline validator: Option<T>.(T?) -> Boolean = { true },
|
||||
) = Option(
|
||||
key,
|
||||
name,
|
||||
default,
|
||||
values,
|
||||
title,
|
||||
description,
|
||||
required,
|
||||
typeOf<T>(),
|
||||
@@ -845,10 +749,9 @@ inline fun <reified T> 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 <reified T> option(
|
||||
* @see Option
|
||||
*/
|
||||
inline fun <reified T> PatchBuilder<*>.option(
|
||||
key: String,
|
||||
name: String,
|
||||
default: T? = null,
|
||||
values: Map<String, T?>? = null,
|
||||
title: String? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
noinline validator: Option<T>.(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")
|
||||
}
|
||||
@@ -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<PackageName, Set<VersionName>?>
|
||||
|
||||
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<Patch>,
|
||||
val compatiblePackages: Set<Package>?,
|
||||
options: Set<Option<*>>,
|
||||
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<C : PatchContext<*>>(
|
||||
private val type: PatchType,
|
||||
private val getPatchContext: context(BytecodePatchContext, ResourcePatchContext) () -> C
|
||||
) {
|
||||
private var compatiblePackages: MutableSet<Package>? = null
|
||||
private val dependencies = mutableSetOf<Patch>()
|
||||
private val options = mutableSetOf<Option<*>>()
|
||||
|
||||
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 <T> Option<T>.invoke() = apply {
|
||||
options += this
|
||||
}
|
||||
|
||||
operator fun String.invoke(vararg versions: VersionName) = invoke(versions.toSet())
|
||||
|
||||
private operator fun String.invoke(versions: Set<VersionName>? = 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<BytecodePatchContext>(
|
||||
PatchType.BYTECODE,
|
||||
{
|
||||
// Extend the context with the extension, before returning it to the patch for execution.
|
||||
contextOf<BytecodePatchContext>().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<ResourcePatchContext>(
|
||||
type,
|
||||
{ contextOf<ResourcePatchContext>() }
|
||||
) {
|
||||
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 <B : PatchBuilder<*>> creatingPatch(
|
||||
description: String? = null,
|
||||
use: Boolean = true,
|
||||
block: B.() -> Unit,
|
||||
patchSupplier: (String?, String?, Boolean, B.() -> Unit) -> Patch
|
||||
) = ReadOnlyProperty<Any?, Patch> { _, 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<T> : Supplier<T>
|
||||
|
||||
/**
|
||||
* 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<File, Set<Patch>>) : Set<Patch>
|
||||
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<String>, classLoader: ClassLoader): Set<Patch> {
|
||||
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<String>,
|
||||
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
|
||||
@@ -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<PatchesResult.PatchedResources?> {
|
||||
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<String>()
|
||||
|
||||
internal fun decodeManifest(): Pair<PackageName, VersionName> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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<File>) = 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
|
||||
)
|
||||
158
patcher/src/jvmTest/kotlin/app/revanced/patcher/PatcherTest.kt
Normal file
158
patcher/src/jvmTest/kotlin/app/revanced/patcher/PatcherTest.kt
Normal file
@@ -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<String>()
|
||||
|
||||
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<String>()
|
||||
|
||||
infix fun Patch.resultsIn(equals: List<String>) {
|
||||
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<PatchException>("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) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -21,4 +21,6 @@ dependencyResolutionManagement {
|
||||
}
|
||||
}
|
||||
|
||||
include(":core")
|
||||
include(":patcher")
|
||||
include(":matching")
|
||||
include(":tests")
|
||||
|
||||
24
tests/build.gradle.kts
Normal file
24
tests/build.gradle.kts
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ResourcePatchContext>(relaxed = true)
|
||||
bytecodePatchContext = mockk<BytecodePatchContext> bytecodePatchContext@{
|
||||
mockkStatic(MultiDexIO::readDexFile)
|
||||
every {
|
||||
MultiDexIO.readDexFile(
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any()
|
||||
)
|
||||
} returns mockk<DexFile> {
|
||||
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<File>()
|
||||
|
||||
every { this@bytecodePatchContext.classDefs } returns ClassDefs().apply {
|
||||
invokePrivateMethod($$"initializeCache$patcher")
|
||||
}
|
||||
|
||||
every { get() } returns emptySet()
|
||||
|
||||
justRun { this@bytecodePatchContext["extendWith"](any<InputStream>()) }
|
||||
}
|
||||
}
|
||||
|
||||
protected operator fun Set<Patch>.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)
|
||||
}
|
||||
Reference in New Issue
Block a user