chore: Lint code

This commit is contained in:
oSumAtrIX 2023-11-26 05:57:41 +01:00
parent 287841d806
commit 80407b6102
No known key found for this signature in database
GPG Key ID: A9B3094ACDB604B4
57 changed files with 1060 additions and 840 deletions

View File

@ -37,7 +37,7 @@ sealed class PatchBundleLoader private constructor(
// This constructor parameter is unfortunately necessary, // This constructor parameter is unfortunately necessary,
// so that a reference to the mutable set is present in the constructor to be able to add patches to it. // so that a reference to the mutable set is present in the constructor to be able to add patches to it.
// because the instance itself is a PatchSet, which is immutable, that is delegated by the parameter. // because the instance itself is a PatchSet, which is immutable, that is delegated by the parameter.
private val patchSet: MutableSet<Patch<*>> = mutableSetOf() private val patchSet: MutableSet<Patch<*>> = mutableSetOf(),
) : PatchSet by patchSet { ) : PatchSet by patchSet {
private val logger = Logger.getLogger(PatchBundleLoader::class.java.name) private val logger = Logger.getLogger(PatchBundleLoader::class.java.name)
@ -63,22 +63,29 @@ sealed class PatchBundleLoader private constructor(
* @param silent Whether to suppress logging. * @param silent Whether to suppress logging.
* @return The instantiated [Patch] or `null` if the [Patch] could not be instantiated. * @return The instantiated [Patch] or `null` if the [Patch] could not be instantiated.
*/ */
internal fun Class<*>.getInstance(logger: Logger, silent: Boolean = false): Patch<*>? { internal fun Class<*>.getInstance(
logger: Logger,
silent: Boolean = false,
): Patch<*>? {
return try { return try {
getField("INSTANCE").get(null) getField("INSTANCE").get(null)
} catch (exception: NoSuchFieldException) { } catch (exception: NoSuchFieldException) {
if (!silent) logger.fine( if (!silent) {
"Patch class '${name}' has no INSTANCE field, therefor not a singleton. " + logger.fine(
"Will try to instantiate it." "Patch class '$name' has no INSTANCE field, therefor not a singleton. " +
) "Will try to instantiate it.",
)
}
try { try {
getDeclaredConstructor().newInstance() getDeclaredConstructor().newInstance()
} catch (exception: Exception) { } catch (exception: Exception) {
if (!silent) logger.severe( if (!silent) {
"Patch class '${name}' is not singleton and has no suitable constructor, " + logger.severe(
"therefor cannot be instantiated and will be ignored." "Patch class '$name' is not singleton and has no suitable constructor, " +
) "therefor cannot be instantiated and will be ignored.",
)
}
return null return null
} }
@ -97,7 +104,7 @@ sealed class PatchBundleLoader private constructor(
{ patchBundle -> { patchBundle ->
JarFile(patchBundle).entries().toList().filter { it.name.endsWith(".class") } JarFile(patchBundle).entries().toList().filter { it.name.endsWith(".class") }
.map { it.name.replace('/', '.').replace(".class", "") } .map { it.name.replace('/', '.').replace(".class", "") }
} },
) )
/** /**
@ -109,9 +116,10 @@ sealed class PatchBundleLoader private constructor(
*/ */
class Dex(vararg patchBundles: File, optimizedDexDirectory: File? = null) : PatchBundleLoader( class Dex(vararg patchBundles: File, optimizedDexDirectory: File? = null) : PatchBundleLoader(
DexClassLoader( DexClassLoader(
patchBundles.joinToString(File.pathSeparator) { it.absolutePath }, optimizedDexDirectory?.absolutePath, patchBundles.joinToString(File.pathSeparator) { it.absolutePath },
optimizedDexDirectory?.absolutePath,
null, null,
PatchBundleLoader::class.java.classLoader PatchBundleLoader::class.java.classLoader,
), ),
patchBundles, patchBundles,
{ patchBundle -> { patchBundle ->
@ -119,7 +127,7 @@ sealed class PatchBundleLoader private constructor(
.map { classDef -> .map { classDef ->
classDef.type.substring(1, classDef.length - 1) classDef.type.substring(1, classDef.length - 1)
} }
} },
) { ) {
@Deprecated("This constructor is deprecated. Use the constructor with the second parameter instead.") @Deprecated("This constructor is deprecated. Use the constructor with the second parameter instead.")
constructor(vararg patchBundles: File) : this(*patchBundles, optimizedDexDirectory = null) constructor(vararg patchBundles: File) : this(*patchBundles, optimizedDexDirectory = null)

View File

@ -17,7 +17,7 @@ import java.util.logging.Logger
* @param options The options for the patcher. * @param options The options for the patcher.
*/ */
class Patcher( class Patcher(
private val options: PatcherOptions private val options: PatcherOptions,
) : PatchExecutorFunction, PatchesConsumer, IntegrationsConsumer, Supplier<PatcherResult>, Closeable { ) : PatchExecutorFunction, PatchesConsumer, IntegrationsConsumer, Supplier<PatcherResult>, Closeable {
private val logger = Logger.getLogger(Patcher::class.java.name) private val logger = Logger.getLogger(Patcher::class.java.name)
@ -39,11 +39,12 @@ class Patcher(
// * @param patches The [Patch]es to add. // * @param patches The [Patch]es to add.
// * @throws PatcherException.CircularDependencyException If a circular dependency is detected. // * @throws PatcherException.CircularDependencyException If a circular dependency is detected.
// */ // */
/** /**
* Add [Patch]es to ReVanced [Patcher]. * Add [Patch]es to ReVanced [Patcher].
* *
* @param patches The [Patch]es to add. * @param patches The [Patch]es to add.
*/ */
@Suppress("NAME_SHADOWING") @Suppress("NAME_SHADOWING")
override fun acceptPatches(patches: List<Patch<*>>) { override fun acceptPatches(patches: List<Patch<*>>) {
/** /**
@ -82,7 +83,7 @@ class Patcher(
dependency.visit() dependency.visit()
} }
} }
*/ */
/** /**
* Returns true if at least one patch or its dependencies matches the given predicate. * Returns true if at least one patch or its dependencies matches the given predicate.
@ -126,128 +127,130 @@ class Patcher(
* @param returnOnError If true, ReVanced [Patcher] will return immediately if a [Patch] fails. * @param returnOnError If true, ReVanced [Patcher] will return immediately if a [Patch] fails.
* @return A pair of the name of the [Patch] and its [PatchResult]. * @return A pair of the name of the [Patch] and its [PatchResult].
*/ */
override fun apply(returnOnError: Boolean) = flow { override fun apply(returnOnError: Boolean) =
flow {
/**
* Execute a [Patch] and its dependencies recursively.
*
* @param patch The [Patch] to execute.
* @param executedPatches A map to prevent [Patch]es from being executed twice due to dependencies.
* @return The result of executing the [Patch].
*/
fun executePatch(
patch: Patch<*>,
executedPatches: LinkedHashMap<Patch<*>, PatchResult>,
): PatchResult {
val patchName = patch.name ?: patch.toString()
/** executedPatches[patch]?.let { patchResult ->
* Execute a [Patch] and its dependencies recursively. patchResult.exception ?: return patchResult
*
* @param patch The [Patch] to execute.
* @param executedPatches A map to prevent [Patch]es from being executed twice due to dependencies.
* @return The result of executing the [Patch].
*/
fun executePatch(
patch: Patch<*>,
executedPatches: LinkedHashMap<Patch<*>, PatchResult>
): PatchResult {
val patchName = patch.name ?: patch.toString()
executedPatches[patch]?.let { patchResult -> // Return a new result with an exception indicating that the patch was not executed previously,
patchResult.exception ?: return patchResult // because it is a dependency of another patch that failed.
return PatchResult(patch, PatchException("'$patchName' did not succeed previously"))
}
// Return a new result with an exception indicating that the patch was not executed previously, // Recursively execute all dependency patches.
// because it is a dependency of another patch that failed. patch.dependencies?.forEach { dependencyClass ->
return PatchResult(patch, PatchException("'$patchName' did not succeed previously")) val dependency = context.allPatches[dependencyClass]!!
} val result = executePatch(dependency, executedPatches)
// Recursively execute all dependency patches. result.exception?.let {
patch.dependencies?.forEach { dependencyClass -> return PatchResult(
val dependency = context.allPatches[dependencyClass]!! patch,
val result = executePatch(dependency, executedPatches) PatchException(
"'$patchName' depends on '${dependency.name ?: dependency}' " +
result.exception?.let { "that raised an exception:\n${it.stackTraceToString()}",
return PatchResult( ),
patch,
PatchException(
"'$patchName' depends on '${dependency.name ?: dependency}' " +
"that raised an exception:\n${it.stackTraceToString()}"
) )
)
}
}
return try {
// TODO: Implement this in a more polymorphic way.
when (patch) {
is BytecodePatch -> {
patch.fingerprints.resolveUsingLookupMap(context.bytecodeContext)
patch.execute(context.bytecodeContext)
}
is ResourcePatch -> {
patch.execute(context.resourceContext)
} }
} }
PatchResult(patch) return try {
} catch (exception: PatchException) { // TODO: Implement this in a more polymorphic way.
PatchResult(patch, exception) when (patch) {
} catch (exception: Exception) { is BytecodePatch -> {
PatchResult(patch, PatchException(exception)) patch.fingerprints.resolveUsingLookupMap(context.bytecodeContext)
}.also { executedPatches[patch] = it } patch.execute(context.bytecodeContext)
} }
is ResourcePatch -> {
patch.execute(context.resourceContext)
}
}
if (context.bytecodeContext.integrations.merge) context.bytecodeContext.integrations.flush() PatchResult(patch)
LookupMap.initializeLookupMaps(context.bytecodeContext)
// Prevent from decoding the app manifest twice if it is not needed.
if (options.resourceDecodingMode == ResourceContext.ResourceDecodingMode.FULL)
context.resourceContext.decodeResources(ResourceContext.ResourceDecodingMode.FULL)
logger.info("Executing patches")
val executedPatches = LinkedHashMap<Patch<*>, PatchResult>() // Key is name.
context.executablePatches.values.sortedBy { it.name }.forEach { patch ->
val patchResult = executePatch(patch, executedPatches)
// If the patch failed, emit the result, even if it is closeable.
// Results of executed patches that are closeable will be emitted later.
patchResult.exception?.let {
// Propagate exception to caller instead of wrapping it in a new exception.
emit(patchResult)
if (returnOnError) return@flow
} ?: run {
if (patch is Closeable) return@run
emit(patchResult)
}
}
executedPatches.values
.filter { it.exception == null }
.filter { it.patch is Closeable }.asReversed().forEach { executedPatch ->
val patch = executedPatch.patch
val result = try {
(patch as Closeable).close()
executedPatch
} catch (exception: PatchException) { } catch (exception: PatchException) {
PatchResult(patch, exception) PatchResult(patch, exception)
} catch (exception: Exception) { } catch (exception: Exception) {
PatchResult(patch, PatchException(exception)) PatchResult(patch, PatchException(exception))
} }.also { executedPatches[patch] = it }
}
result.exception?.let { if (context.bytecodeContext.integrations.merge) context.bytecodeContext.integrations.flush()
emit(
PatchResult( LookupMap.initializeLookupMaps(context.bytecodeContext)
patch,
PatchException( // Prevent from decoding the app manifest twice if it is not needed.
"'${patch.name}' raised an exception while being closed: ${it.stackTraceToString()}", if (options.resourceDecodingMode == ResourceContext.ResourceDecodingMode.FULL) {
result.exception context.resourceContext.decodeResources(ResourceContext.ResourceDecodingMode.FULL)
) }
)
) logger.info("Executing patches")
val executedPatches = LinkedHashMap<Patch<*>, PatchResult>() // Key is name.
context.executablePatches.values.sortedBy { it.name }.forEach { patch ->
val patchResult = executePatch(patch, executedPatches)
// If the patch failed, emit the result, even if it is closeable.
// Results of executed patches that are closeable will be emitted later.
patchResult.exception?.let {
// Propagate exception to caller instead of wrapping it in a new exception.
emit(patchResult)
if (returnOnError) return@flow if (returnOnError) return@flow
} ?: run { } ?: run {
patch.name ?: return@run if (patch is Closeable) return@run
emit(result) emit(patchResult)
} }
} }
}
executedPatches.values
.filter { it.exception == null }
.filter { it.patch is Closeable }.asReversed().forEach { executedPatch ->
val patch = executedPatch.patch
val result =
try {
(patch as Closeable).close()
executedPatch
} catch (exception: PatchException) {
PatchResult(patch, exception)
} catch (exception: Exception) {
PatchResult(patch, PatchException(exception))
}
result.exception?.let {
emit(
PatchResult(
patch,
PatchException(
"'${patch.name}' raised an exception while being closed: ${it.stackTraceToString()}",
result.exception,
),
),
)
if (returnOnError) return@flow
} ?: run {
patch.name ?: return@run
emit(result)
}
}
}
override fun close() = LookupMap.clearLookupMaps() override fun close() = LookupMap.clearLookupMaps()
@ -256,10 +259,10 @@ class Patcher(
* *
* @return The [PatcherResult] containing the patched input files. * @return The [PatcherResult] containing the patched input files.
*/ */
override fun get() = PatcherResult( override fun get() =
context.bytecodeContext.get(), PatcherResult(
context.resourceContext.get(), context.bytecodeContext.get(),
context.packageMetadata.apkInfo.doNotCompress?.toList() context.resourceContext.get(),
) context.packageMetadata.apkInfo.doNotCompress?.toList(),
)
} }

View File

@ -9,8 +9,7 @@ package app.revanced.patcher
sealed class PatcherException(errorMessage: String?, cause: Throwable?) : Exception(errorMessage, cause) { sealed class PatcherException(errorMessage: String?, cause: Throwable?) : Exception(errorMessage, cause) {
constructor(errorMessage: String) : this(errorMessage, null) constructor(errorMessage: String) : this(errorMessage, null)
class CircularDependencyException internal constructor(dependant: String) : PatcherException( class CircularDependencyException internal constructor(dependant: String) : PatcherException(
"Patch '$dependant' causes a circular dependency" "Patch '$dependant' causes a circular dependency",
) )
} }

View File

@ -32,20 +32,23 @@ data class PatcherOptions(
/** /**
* The configuration to use for resource decoding and compiling. * The configuration to use for resource decoding and compiling.
*/ */
internal val resourceConfig = Config.getDefaultConfig().apply { internal val resourceConfig =
useAapt2 = true Config.getDefaultConfig().apply {
aaptPath = aaptBinaryPath ?: "" useAapt2 = true
frameworkDirectory = frameworkFileDirectory aaptPath = aaptBinaryPath ?: ""
} frameworkDirectory = frameworkFileDirectory
fun recreateResourceCacheDirectory() = resourceCachePath.also {
if (it.exists()) {
logger.info("Deleting existing resource cache directory")
if (!it.deleteRecursively())
logger.severe("Failed to delete existing resource cache directory")
} }
it.mkdirs() fun recreateResourceCacheDirectory() =
} resourceCachePath.also {
if (it.exists()) {
logger.info("Deleting existing resource cache directory")
if (!it.deleteRecursively()) {
logger.severe("Failed to delete existing resource cache directory")
}
}
it.mkdirs()
}
} }

View File

@ -12,7 +12,7 @@ import java.io.InputStream
data class PatcherResult( data class PatcherResult(
val dexFiles: List<PatchedDexFile>, val dexFiles: List<PatchedDexFile>,
val resourceFile: File?, val resourceFile: File?,
val doNotCompress: List<String>? = null val doNotCompress: List<String>? = null,
) { ) {
/** /**
* Wrapper for dex files. * Wrapper for dex files.

View File

@ -28,141 +28,153 @@ import java.util.logging.Logger
*/ */
class BytecodeContext internal constructor(private val options: PatcherOptions) : class BytecodeContext internal constructor(private val options: PatcherOptions) :
Context<List<PatcherResult.PatchedDexFile>> { Context<List<PatcherResult.PatchedDexFile>> {
private val logger = Logger.getLogger(BytecodeContext::class.java.name) private val logger = Logger.getLogger(BytecodeContext::class.java.name)
/**
* [Opcodes] of the supplied [PatcherOptions.inputFile].
*/
internal lateinit var opcodes: Opcodes
/**
* The list of classes.
*/
val classes by lazy {
ProxyClassList(
MultiDexIO.readDexFile(
true, options.inputFile, BasicDexFileNamer(), null, null
).also { opcodes = it.opcodes }.classes.toMutableSet()
)
}
/**
* The [Integrations] of this [PatcherContext].
*/
internal val integrations = Integrations()
/**
* Find a class by a given class name.
*
* @param className The name of the class.
* @return A proxy for the first class that matches the class name.
*/
fun findClass(className: String) = findClass { it.type.contains(className) }
/**
* Find a class by a given predicate.
*
* @param predicate A predicate to match the class.
* @return A proxy for the first class that matches the predicate.
*/
fun findClass(predicate: (ClassDef) -> Boolean) =
// if we already proxied the class matching the predicate...
classes.proxies.firstOrNull { predicate(it.immutableClass) } ?:
// else resolve the class to a proxy and return it, if the predicate is matching a class
classes.find(predicate)?.let { proxy(it) }
/**
* Proxy a class.
* This will allow the class to be modified.
*
* @param classDef The class to proxy.
* @return A proxy for the class.
*/
fun proxy(classDef: ClassDef) = this.classes.proxies.find { it.immutableClass.type == classDef.type } ?: let {
ClassProxy(classDef).also { this.classes.add(it) }
}
/**
* Create a [MethodWalker] instance for the current [BytecodeContext].
*
* @param startMethod The method to start at.
* @return A [MethodWalker] instance.
*/
fun toMethodWalker(startMethod: Method) = MethodWalker(this, startMethod)
/**
* Compile bytecode from the [BytecodeContext].
*
* @return The compiled bytecode.
*/
override fun get(): List<PatcherResult.PatchedDexFile> {
logger.info("Compiling patched dex files")
val patchedDexFileResults = options.resourceCachePath.resolve("dex").also {
it.deleteRecursively() // Make sure the directory is empty.
it.mkdirs()
}.apply {
MultiDexIO.writeDexFile(
true,
if (options.multithreadingDexFileWriter) -1 else 1,
this,
BasicDexFileNamer(),
object : DexFile {
override fun getClasses() = this@BytecodeContext.classes.also(ProxyClassList::replaceClasses)
override fun getOpcodes() = this@BytecodeContext.opcodes
},
DexIO.DEFAULT_MAX_DEX_POOL_SIZE
) { _, entryName, _ -> logger.info("Compiled $entryName") }
}.listFiles(FileFilter { it.isFile })!!.map { PatcherResult.PatchedDexFile(it.name, it.inputStream()) }
System.gc()
return patchedDexFileResults
}
/**
* The integrations of a [PatcherContext].
*/
internal inner class Integrations : MutableList<File> by mutableListOf(), Flushable {
/**
* Whether to merge integrations.
* Set to true, if the field requiresIntegrations of any supplied [Patch] is true.
*/
var merge = false
/** /**
* Merge integrations into the [BytecodeContext] and flush all [Integrations]. * [Opcodes] of the supplied [PatcherOptions.inputFile].
*/ */
override fun flush() { internal lateinit var opcodes: Opcodes
if (!merge) return
logger.info("Merging integrations") /**
* The list of classes.
val classMap = classes.associateBy { it.type } */
val classes by lazy {
this@Integrations.forEach { integrations -> ProxyClassList(
MultiDexIO.readDexFile( MultiDexIO.readDexFile(
true, true,
integrations, BasicDexFileNamer(), options.inputFile,
BasicDexFileNamer(),
null, null,
null null,
).classes.forEach classDef@{ classDef -> ).also { opcodes = it.opcodes }.classes.toMutableSet(),
val existingClass = classMap[classDef.type] ?: run { )
logger.fine("Adding $classDef") }
classes.add(classDef)
return@classDef
}
logger.fine("$classDef exists. Adding missing methods and fields.") /**
* The [Integrations] of this [PatcherContext].
*/
internal val integrations = Integrations()
existingClass.merge(classDef, this@BytecodeContext).let { mergedClass -> /**
// If the class was merged, replace the original class with the merged class. * Find a class by a given class name.
if (mergedClass === existingClass) return@let *
classes.apply { remove(existingClass); add(mergedClass) } * @param className The name of the class.
* @return A proxy for the first class that matches the class name.
*/
fun findClass(className: String) = findClass { it.type.contains(className) }
/**
* Find a class by a given predicate.
*
* @param predicate A predicate to match the class.
* @return A proxy for the first class that matches the predicate.
*/
fun findClass(predicate: (ClassDef) -> Boolean) =
// if we already proxied the class matching the predicate...
classes.proxies.firstOrNull { predicate(it.immutableClass) }
?: // else resolve the class to a proxy and return it, if the predicate is matching a class
classes.find(predicate)?.let { proxy(it) }
/**
* Proxy a class.
* This will allow the class to be modified.
*
* @param classDef The class to proxy.
* @return A proxy for the class.
*/
fun proxy(classDef: ClassDef) =
this.classes.proxies.find { it.immutableClass.type == classDef.type } ?: let {
ClassProxy(classDef).also { this.classes.add(it) }
}
/**
* Create a [MethodWalker] instance for the current [BytecodeContext].
*
* @param startMethod The method to start at.
* @return A [MethodWalker] instance.
*/
fun toMethodWalker(startMethod: Method) = MethodWalker(this, startMethod)
/**
* Compile bytecode from the [BytecodeContext].
*
* @return The compiled bytecode.
*/
override fun get(): List<PatcherResult.PatchedDexFile> {
logger.info("Compiling patched dex files")
val patchedDexFileResults =
options.resourceCachePath.resolve("dex").also {
it.deleteRecursively() // Make sure the directory is empty.
it.mkdirs()
}.apply {
MultiDexIO.writeDexFile(
true,
if (options.multithreadingDexFileWriter) -1 else 1,
this,
BasicDexFileNamer(),
object : DexFile {
override fun getClasses() = this@BytecodeContext.classes.also(ProxyClassList::replaceClasses)
override fun getOpcodes() = this@BytecodeContext.opcodes
},
DexIO.DEFAULT_MAX_DEX_POOL_SIZE,
) { _, entryName, _ -> logger.info("Compiled $entryName") }
}.listFiles(FileFilter { it.isFile })!!.map { PatcherResult.PatchedDexFile(it.name, it.inputStream()) }
System.gc()
return patchedDexFileResults
}
/**
* The integrations of a [PatcherContext].
*/
internal inner class Integrations : MutableList<File> by mutableListOf(), Flushable {
/**
* Whether to merge integrations.
* Set to true, if the field requiresIntegrations of any supplied [Patch] is true.
*/
var merge = false
/**
* Merge integrations into the [BytecodeContext] and flush all [Integrations].
*/
override fun flush() {
if (!merge) return
logger.info("Merging integrations")
val classMap = classes.associateBy { it.type }
this@Integrations.forEach { integrations ->
MultiDexIO.readDexFile(
true,
integrations,
BasicDexFileNamer(),
null,
null,
).classes.forEach classDef@{ classDef ->
val existingClass =
classMap[classDef.type] ?: run {
logger.fine("Adding $classDef")
classes.add(classDef)
return@classDef
}
logger.fine("$classDef exists. Adding missing methods and fields.")
existingClass.merge(classDef, this@BytecodeContext).let { mergedClass ->
// If the class was merged, replace the original class with the merged class.
if (mergedClass === existingClass) return@let
classes.apply {
remove(existingClass)
add(mergedClass)
}
}
} }
} }
clear()
} }
clear()
} }
} }
}

View File

@ -26,7 +26,7 @@ import java.util.logging.Logger
*/ */
class ResourceContext internal constructor( class ResourceContext internal constructor(
private val context: PatcherContext, private val context: PatcherContext,
private val options: PatcherOptions private val options: PatcherOptions,
) : Context<File?>, Iterable<File> { ) : Context<File?>, Iterable<File> {
private val logger = Logger.getLogger(ResourceContext::class.java.name) private val logger = Logger.getLogger(ResourceContext::class.java.name)
@ -37,52 +37,54 @@ class ResourceContext internal constructor(
* *
* @param mode The [ResourceDecodingMode] to use when decoding. * @param mode The [ResourceDecodingMode] to use when decoding.
*/ */
internal fun decodeResources(mode: ResourceDecodingMode) = with(context.packageMetadata.apkInfo) { internal fun decodeResources(mode: ResourceDecodingMode) =
// Needed to decode resources. with(context.packageMetadata.apkInfo) {
val resourcesDecoder = ResourcesDecoder(options.resourceConfig, this) // Needed to decode resources.
val resourcesDecoder = ResourcesDecoder(options.resourceConfig, this)
when (mode) { when (mode) {
ResourceDecodingMode.FULL -> { ResourceDecodingMode.FULL -> {
val outDir = options.recreateResourceCacheDirectory() val outDir = options.recreateResourceCacheDirectory()
logger.info("Decoding resources") logger.info("Decoding resources")
resourcesDecoder.decodeResources(outDir) resourcesDecoder.decodeResources(outDir)
resourcesDecoder.decodeManifest(outDir) resourcesDecoder.decodeManifest(outDir)
// Needed to record uncompressed files. // Needed to record uncompressed files.
val apkDecoder = ApkDecoder(options.resourceConfig, this) val apkDecoder = ApkDecoder(options.resourceConfig, this)
apkDecoder.recordUncompressedFiles(resourcesDecoder.resFileMapping) apkDecoder.recordUncompressedFiles(resourcesDecoder.resFileMapping)
usesFramework = UsesFramework().apply { usesFramework =
ids = resourcesDecoder.resTable.listFramePackages().map { it.id } UsesFramework().apply {
} ids = resourcesDecoder.resTable.listFramePackages().map { it.id }
}
ResourceDecodingMode.MANIFEST_ONLY -> {
logger.info("Decoding app manifest")
// Decode manually instead of using resourceDecoder.decodeManifest
// because it does not support decoding to an OutputStream.
XmlPullStreamDecoder(
AndroidManifestResourceParser(resourcesDecoder.resTable),
resourcesDecoder.resXmlSerializer
).decodeManifest(
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. ResourceDecodingMode.MANIFEST_ONLY -> {
// XmlPullStreamDecoder.decodeManifest() sets metadata.apkInfo. logger.info("Decoding app manifest")
context.packageMetadata.let { metadata ->
metadata.packageName = resourcesDecoder.resTable.packageRenamed // Decode manually instead of using resourceDecoder.decodeManifest
versionInfo.let { // because it does not support decoding to an OutputStream.
metadata.packageVersion = it.versionName ?: it.versionCode XmlPullStreamDecoder(
} AndroidManifestResourceParser(resourcesDecoder.resTable),
resourcesDecoder.resXmlSerializer,
).decodeManifest(
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.
// XmlPullStreamDecoder.decodeManifest() sets metadata.apkInfo.
context.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, The ResTable if flagged as sparse if the main package is not loaded, which is the case here,
@ -92,18 +94,16 @@ class ResourceContext internal constructor(
Set this to false again to prevent the ResTable from being flagged as sparse falsely. Set this to false again to prevent the ResTable from being flagged as sparse falsely.
*/ */
metadata.apkInfo.sparseResources = false metadata.apkInfo.sparseResources = false
}
} }
} }
} }
}
operator fun get(path: String) = options.resourceCachePath.resolve(path) operator fun get(path: String) = options.resourceCachePath.resolve(path)
override fun iterator() = options.resourceCachePath.walkTopDown().iterator() override fun iterator() = options.resourceCachePath.walkTopDown().iterator()
/** /**
* Compile resources from the [ResourceContext]. * Compile resources from the [ResourceContext].
* *
@ -116,14 +116,17 @@ class ResourceContext internal constructor(
logger.info("Compiling modified resources") logger.info("Compiling modified resources")
val cacheDirectory = ExtFile(options.resourceCachePath) val cacheDirectory = ExtFile(options.resourceCachePath)
val aaptFile = cacheDirectory.resolve("aapt_temp_file").also { val aaptFile =
Files.deleteIfExists(it.toPath()) cacheDirectory.resolve("aapt_temp_file").also {
}.also { resourceFile = it } Files.deleteIfExists(it.toPath())
}.also { resourceFile = it }
try { try {
AaptInvoker( AaptInvoker(
options.resourceConfig, context.packageMetadata.apkInfo options.resourceConfig,
).invokeAapt(aaptFile, context.packageMetadata.apkInfo,
).invokeAapt(
aaptFile,
cacheDirectory.resolve("AndroidManifest.xml").also { cacheDirectory.resolve("AndroidManifest.xml").also {
ResXmlPatcher.fixingPublicAttrsInProviderAttributes(it) ResXmlPatcher.fixingPublicAttrsInProviderAttributes(it)
}, },
@ -134,7 +137,8 @@ class ResourceContext internal constructor(
usesFramework.ids.map { id -> usesFramework.ids.map { id ->
Framework(options.resourceConfig).getFrameworkApk(id, usesFramework.tag) Framework(options.resourceConfig).getFrameworkApk(id, usesFramework.tag)
}.toTypedArray() }.toTypedArray()
}) },
)
} finally { } finally {
cacheDirectory.close() cacheDirectory.close()
} }
@ -159,12 +163,10 @@ class ResourceContext internal constructor(
} }
inner class XmlFileHolder { inner class XmlFileHolder {
operator fun get(inputStream: InputStream) = operator fun get(inputStream: InputStream) = DomFileEditor(inputStream)
DomFileEditor(inputStream)
operator fun get(path: String): DomFileEditor { operator fun get(path: String): DomFileEditor {
return DomFileEditor(this@ResourceContext[path]) return DomFileEditor(this@ResourceContext[path])
} }
} }
} }

View File

@ -11,11 +11,13 @@ internal object AnnotationExtensions {
*/ */
fun <T : Annotation> Class<*>.findAnnotationRecursively(targetAnnotation: KClass<T>): T? { fun <T : Annotation> Class<*>.findAnnotationRecursively(targetAnnotation: KClass<T>): T? {
fun <T : Annotation> Class<*>.findAnnotationRecursively( fun <T : Annotation> Class<*>.findAnnotationRecursively(
targetAnnotation: Class<T>, traversed: MutableSet<Annotation> targetAnnotation: Class<T>,
traversed: MutableSet<Annotation>,
): T? { ): T? {
val found = this.annotations.firstOrNull { it.annotationClass.java.name == targetAnnotation.name } val found = this.annotations.firstOrNull { it.annotationClass.java.name == targetAnnotation.name }
@Suppress("UNCHECKED_CAST") if (found != null) return found as T @Suppress("UNCHECKED_CAST")
if (found != null) return found as T
for (annotation in this.annotations) { for (annotation in this.annotations) {
if (traversed.contains(annotation)) continue if (traversed.contains(annotation)) continue

View File

@ -12,7 +12,6 @@ import com.android.tools.smali.dexlib2.builder.instruction.*
import com.android.tools.smali.dexlib2.iface.instruction.Instruction import com.android.tools.smali.dexlib2.iface.instruction.Instruction
object InstructionExtensions { object InstructionExtensions {
/** /**
* Add instructions to a method at the given index. * Add instructions to a method at the given index.
* *
@ -21,7 +20,7 @@ object InstructionExtensions {
*/ */
fun MutableMethodImplementation.addInstructions( fun MutableMethodImplementation.addInstructions(
index: Int, index: Int,
instructions: List<BuilderInstruction> instructions: List<BuilderInstruction>,
) = instructions.asReversed().forEach { addInstruction(index, it) } ) = instructions.asReversed().forEach { addInstruction(index, it) }
/** /**
@ -39,7 +38,10 @@ object InstructionExtensions {
* @param index The index to remove the instructions at. * @param index The index to remove the instructions at.
* @param count The amount of instructions to remove. * @param count The amount of instructions to remove.
*/ */
fun MutableMethodImplementation.removeInstructions(index: Int, count: Int) = repeat(count) { fun MutableMethodImplementation.removeInstructions(
index: Int,
count: Int,
) = repeat(count) {
removeInstruction(index) removeInstruction(index)
} }
@ -57,7 +59,10 @@ object InstructionExtensions {
* @param index The index to replace the instructions at. * @param index The index to replace the instructions at.
* @param instructions The instructions to replace the instructions with. * @param instructions The instructions to replace the instructions with.
*/ */
fun MutableMethodImplementation.replaceInstructions(index: Int, instructions: List<BuilderInstruction>) { fun MutableMethodImplementation.replaceInstructions(
index: Int,
instructions: List<BuilderInstruction>,
) {
// Remove the instructions at the given index. // Remove the instructions at the given index.
removeInstructions(index, instructions.size) removeInstructions(index, instructions.size)
@ -71,16 +76,17 @@ object InstructionExtensions {
* @param index The index to add the instruction at. * @param index The index to add the instruction at.
* @param instruction The instruction to add. * @param instruction The instruction to add.
*/ */
fun MutableMethod.addInstruction(index: Int, instruction: BuilderInstruction) = fun MutableMethod.addInstruction(
implementation!!.addInstruction(index, instruction) index: Int,
instruction: BuilderInstruction,
) = implementation!!.addInstruction(index, instruction)
/** /**
* Add an instruction to a method. * Add an instruction to a method.
* *
* @param instruction The instructions to add. * @param instruction The instructions to add.
*/ */
fun MutableMethod.addInstruction(instruction: BuilderInstruction) = fun MutableMethod.addInstruction(instruction: BuilderInstruction) = implementation!!.addInstruction(instruction)
implementation!!.addInstruction(instruction)
/** /**
* Add an instruction to a method at the given index. * Add an instruction to a method at the given index.
@ -88,17 +94,17 @@ object InstructionExtensions {
* @param index The index to add the instruction at. * @param index The index to add the instruction at.
* @param smaliInstructions The instruction to add. * @param smaliInstructions The instruction to add.
*/ */
fun MutableMethod.addInstruction(index: Int, smaliInstructions: String) = fun MutableMethod.addInstruction(
implementation!!.addInstruction(index, smaliInstructions.toInstruction(this)) index: Int,
smaliInstructions: String,
) = implementation!!.addInstruction(index, smaliInstructions.toInstruction(this))
/** /**
* Add an instruction to a method. * Add an instruction to a method.
* *
* @param smaliInstructions The instruction to add. * @param smaliInstructions The instruction to add.
*/ */
fun MutableMethod.addInstruction(smaliInstructions: String) = fun MutableMethod.addInstruction(smaliInstructions: String) = implementation!!.addInstruction(smaliInstructions.toInstruction(this))
implementation!!.addInstruction(smaliInstructions.toInstruction(this))
/** /**
* Add instructions to a method at the given index. * Add instructions to a method at the given index.
@ -106,32 +112,34 @@ object InstructionExtensions {
* @param index The index to add the instructions at. * @param index The index to add the instructions at.
* @param instructions The instructions to add. * @param instructions The instructions to add.
*/ */
fun MutableMethod.addInstructions(index: Int, instructions: List<BuilderInstruction>) = fun MutableMethod.addInstructions(
implementation!!.addInstructions(index, instructions) index: Int,
instructions: List<BuilderInstruction>,
) = implementation!!.addInstructions(index, instructions)
/** /**
* Add instructions to a method. * Add instructions to a method.
* *
* @param instructions The instructions to add. * @param instructions The instructions to add.
*/ */
fun MutableMethod.addInstructions(instructions: List<BuilderInstruction>) = fun MutableMethod.addInstructions(instructions: List<BuilderInstruction>) = implementation!!.addInstructions(instructions)
implementation!!.addInstructions(instructions)
/** /**
* Add instructions to a method. * Add instructions to a method.
* *
* @param smaliInstructions The instructions to add. * @param smaliInstructions The instructions to add.
*/ */
fun MutableMethod.addInstructions(index: Int, smaliInstructions: String) = fun MutableMethod.addInstructions(
implementation!!.addInstructions(index, smaliInstructions.toInstructions(this)) index: Int,
smaliInstructions: String,
) = implementation!!.addInstructions(index, smaliInstructions.toInstructions(this))
/** /**
* Add instructions to a method. * Add instructions to a method.
* *
* @param smaliInstructions The instructions to add. * @param smaliInstructions The instructions to add.
*/ */
fun MutableMethod.addInstructions(smaliInstructions: String) = fun MutableMethod.addInstructions(smaliInstructions: String) = implementation!!.addInstructions(smaliInstructions.toInstructions(this))
implementation!!.addInstructions(smaliInstructions.toInstructions(this))
/** /**
* Add instructions to a method at the given index. * Add instructions to a method at the given index.
@ -144,14 +152,15 @@ object InstructionExtensions {
fun MutableMethod.addInstructionsWithLabels( fun MutableMethod.addInstructionsWithLabels(
index: Int, index: Int,
smaliInstructions: String, smaliInstructions: String,
vararg externalLabels: ExternalLabel vararg externalLabels: ExternalLabel,
) { ) {
// Create reference dummy instructions for the instructions. // Create reference dummy instructions for the instructions.
val nopSmali = StringBuilder(smaliInstructions).also { builder -> val nopSmali =
externalLabels.forEach { (name, _) -> StringBuilder(smaliInstructions).also { builder ->
builder.append("\n:$name\nnop") externalLabels.forEach { (name, _) ->
} builder.append("\n:$name\nnop")
}.toString() }
}.toString()
// Compile the instructions with the dummy labels // Compile the instructions with the dummy labels
val compiledInstructions = nopSmali.toInstructions(this) val compiledInstructions = nopSmali.toInstructions(this)
@ -159,7 +168,7 @@ object InstructionExtensions {
// Add the compiled list of instructions to the method. // Add the compiled list of instructions to the method.
addInstructions( addInstructions(
index, index,
compiledInstructions.subList(0, compiledInstructions.size - externalLabels.size) compiledInstructions.subList(0, compiledInstructions.size - externalLabels.size),
) )
implementation!!.apply { implementation!!.apply {
@ -174,22 +183,24 @@ object InstructionExtensions {
*/ */
fun Instruction.makeNewLabel() { fun Instruction.makeNewLabel() {
fun replaceOffset( fun replaceOffset(
i: BuilderOffsetInstruction, label: Label i: BuilderOffsetInstruction,
label: Label,
): BuilderOffsetInstruction { ): BuilderOffsetInstruction {
return when (i) { return when (i) {
is BuilderInstruction10t -> BuilderInstruction10t(i.opcode, label) is BuilderInstruction10t -> BuilderInstruction10t(i.opcode, label)
is BuilderInstruction20t -> BuilderInstruction20t(i.opcode, label) is BuilderInstruction20t -> BuilderInstruction20t(i.opcode, label)
is BuilderInstruction21t -> BuilderInstruction21t(i.opcode, i.registerA, label) is BuilderInstruction21t -> BuilderInstruction21t(i.opcode, i.registerA, label)
is BuilderInstruction22t -> BuilderInstruction22t( is BuilderInstruction22t ->
i.opcode, BuilderInstruction22t(
i.registerA, i.opcode,
i.registerB, i.registerA,
label i.registerB,
) label,
)
is BuilderInstruction30t -> BuilderInstruction30t(i.opcode, label) is BuilderInstruction30t -> BuilderInstruction30t(i.opcode, label)
is BuilderInstruction31t -> BuilderInstruction31t(i.opcode, i.registerA, label) is BuilderInstruction31t -> BuilderInstruction31t(i.opcode, i.registerA, label)
else -> throw IllegalStateException( else -> throw IllegalStateException(
"A non-offset instruction was given, this should never happen!" "A non-offset instruction was given, this should never happen!",
) )
} }
} }
@ -198,9 +209,11 @@ object InstructionExtensions {
val label = newLabelForIndex(this@apply.instructions.indexOf(this)) val label = newLabelForIndex(this@apply.instructions.indexOf(this))
// Create the final instruction with the new label. // Create the final instruction with the new label.
val newInstruction = replaceOffset( val newInstruction =
compiledInstruction, label replaceOffset(
) compiledInstruction,
label,
)
// Replace the instruction pointing to the dummy label // Replace the instruction pointing to the dummy label
// with the new instruction pointing to the real instruction. // with the new instruction pointing to the real instruction.
@ -233,8 +246,7 @@ object InstructionExtensions {
* *
* @param index The index to remove the instruction at. * @param index The index to remove the instruction at.
*/ */
fun MutableMethod.removeInstruction(index: Int) = fun MutableMethod.removeInstruction(index: Int) = implementation!!.removeInstruction(index)
implementation!!.removeInstruction(index)
/** /**
* Remove instructions at the given index. * Remove instructions at the given index.
@ -242,16 +254,17 @@ object InstructionExtensions {
* @param index The index to remove the instructions at. * @param index The index to remove the instructions at.
* @param count The amount of instructions to remove. * @param count The amount of instructions to remove.
*/ */
fun MutableMethod.removeInstructions(index: Int, count: Int) = fun MutableMethod.removeInstructions(
implementation!!.removeInstructions(index, count) index: Int,
count: Int,
) = implementation!!.removeInstructions(index, count)
/** /**
* Remove instructions at the given index. * Remove instructions at the given index.
* *
* @param count The amount of instructions to remove. * @param count The amount of instructions to remove.
*/ */
fun MutableMethod.removeInstructions(count: Int) = fun MutableMethod.removeInstructions(count: Int) = implementation!!.removeInstructions(count)
implementation!!.removeInstructions(count)
/** /**
* Replace an instruction at the given index. * Replace an instruction at the given index.
@ -259,8 +272,10 @@ object InstructionExtensions {
* @param index The index to replace the instruction at. * @param index The index to replace the instruction at.
* @param instruction The instruction to replace the instruction with. * @param instruction The instruction to replace the instruction with.
*/ */
fun MutableMethod.replaceInstruction(index: Int, instruction: BuilderInstruction) = fun MutableMethod.replaceInstruction(
implementation!!.replaceInstruction(index, instruction) index: Int,
instruction: BuilderInstruction,
) = implementation!!.replaceInstruction(index, instruction)
/** /**
* Replace an instruction at the given index. * Replace an instruction at the given index.
@ -268,8 +283,10 @@ object InstructionExtensions {
* @param index The index to replace the instruction at. * @param index The index to replace the instruction at.
* @param smaliInstruction The smali instruction to replace the instruction with. * @param smaliInstruction The smali instruction to replace the instruction with.
*/ */
fun MutableMethod.replaceInstruction(index: Int, smaliInstruction: String) = fun MutableMethod.replaceInstruction(
implementation!!.replaceInstruction(index, smaliInstruction.toInstruction(this)) index: Int,
smaliInstruction: String,
) = implementation!!.replaceInstruction(index, smaliInstruction.toInstruction(this))
/** /**
* Replace instructions at the given index. * Replace instructions at the given index.
@ -277,8 +294,10 @@ object InstructionExtensions {
* @param index The index to replace the instructions at. * @param index The index to replace the instructions at.
* @param instructions The instructions to replace the instructions with. * @param instructions The instructions to replace the instructions with.
*/ */
fun MutableMethod.replaceInstructions(index: Int, instructions: List<BuilderInstruction>) = fun MutableMethod.replaceInstructions(
implementation!!.replaceInstructions(index, instructions) index: Int,
instructions: List<BuilderInstruction>,
) = implementation!!.replaceInstructions(index, instructions)
/** /**
* Replace instructions at the given index. * Replace instructions at the given index.
@ -286,8 +305,10 @@ object InstructionExtensions {
* @param index The index to replace the instructions at. * @param index The index to replace the instructions at.
* @param smaliInstructions The smali instructions to replace the instructions with. * @param smaliInstructions The smali instructions to replace the instructions with.
*/ */
fun MutableMethod.replaceInstructions(index: Int, smaliInstructions: String) = fun MutableMethod.replaceInstructions(
implementation!!.replaceInstructions(index, smaliInstructions.toInstructions(this)) index: Int,
smaliInstructions: String,
) = implementation!!.replaceInstructions(index, smaliInstructions.toInstructions(this))
/** /**
* Get an instruction at the given index. * Get an instruction at the given index.

View File

@ -1,8 +1,8 @@
package app.revanced.patcher.extensions package app.revanced.patcher.extensions
import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively
import app.revanced.patcher.fingerprint.annotation.FuzzyPatternScanMethod
import app.revanced.patcher.fingerprint.MethodFingerprint import app.revanced.patcher.fingerprint.MethodFingerprint
import app.revanced.patcher.fingerprint.annotation.FuzzyPatternScanMethod
object MethodFingerprintExtensions { object MethodFingerprintExtensions {
// TODO: Make this a property. // TODO: Make this a property.

View File

@ -20,7 +20,7 @@ internal class LookupMap : MutableMap<String, LookupMap.MethodClassList> by muta
*/ */
fun add( fun add(
key: String, key: String,
methodClassPair: MethodClassPair methodClassPair: MethodClassPair,
) { ) {
getOrPut(key) { MethodClassList() }.add(methodClassPair) getOrPut(key) { MethodClassList() }.add(methodClassPair)
} }
@ -73,13 +73,14 @@ internal class LookupMap : MutableMap<String, LookupMap.MethodClassList> by muta
append(accessFlagsReturnKey) append(accessFlagsReturnKey)
appendParameters(method.parameterTypes) appendParameters(method.parameterTypes)
}, },
methodClassPair methodClassPair,
) )
// Add strings contained in the method as the key. // Add strings contained in the method as the key.
method.implementation?.instructions?.forEach instructions@{ instruction -> method.implementation?.instructions?.forEach instructions@{ instruction ->
if (instruction.opcode != Opcode.CONST_STRING && instruction.opcode != Opcode.CONST_STRING_JUMBO) if (instruction.opcode != Opcode.CONST_STRING && instruction.opcode != Opcode.CONST_STRING_JUMBO) {
return@instructions return@instructions
}
val string = ((instruction as ReferenceInstruction).reference as StringReference).string val string = ((instruction as ReferenceInstruction).reference as StringReference).string
@ -120,6 +121,5 @@ internal class LookupMap : MutableMap<String, LookupMap.MethodClassList> by muta
append(parameter.first()) append(parameter.first())
} }
} }
} }
} }

View File

@ -34,7 +34,7 @@ abstract class MethodFingerprint(
internal val parameters: Iterable<String>? = null, internal val parameters: Iterable<String>? = null,
internal val opcodes: Iterable<Opcode?>? = null, internal val opcodes: Iterable<Opcode?>? = null,
internal val strings: Iterable<String>? = null, internal val strings: Iterable<String>? = null,
internal val customFingerprint: ((methodDef: Method, classDef: ClassDef) -> Boolean)? = null internal val customFingerprint: ((methodDef: Method, classDef: ClassDef) -> Boolean)? = null,
) { ) {
/** /**
* The result of the [MethodFingerprint]. * The result of the [MethodFingerprint].
@ -86,11 +86,12 @@ abstract class MethodFingerprint(
} }
} }
val key = buildString { val key =
append(accessFlags) buildString {
append(returnTypeValue.first()) append(accessFlags)
if (parameters != null) appendParameters(parameters) append(returnTypeValue.first())
} if (parameters != null) appendParameters(parameters)
}
return methodSignatureLookupMap[key] ?: return LookupMap.MethodClassList() return methodSignatureLookupMap[key] ?: return LookupMap.MethodClassList()
} }
@ -107,7 +108,11 @@ abstract class MethodFingerprint(
} }
val methodsWithSameStrings = methodStringsLookup() val methodsWithSameStrings = methodStringsLookup()
if (methodsWithSameStrings != null) if (resolveUsingMethodClassPair(methodsWithSameStrings)) return true if (methodsWithSameStrings != null) {
if (resolveUsingMethodClassPair(methodsWithSameStrings)) {
return true
}
}
// No strings declared or none matched (partial matches are allowed). // No strings declared or none matched (partial matches are allowed).
// Use signature matching. // Use signature matching.
@ -121,10 +126,14 @@ abstract class MethodFingerprint(
* @param context The [BytecodeContext] to host proxies. * @param context The [BytecodeContext] to host proxies.
* @return True if the resolution was successful, false otherwise. * @return True if the resolution was successful, false otherwise.
*/ */
fun resolve(context: BytecodeContext, forClass: ClassDef): Boolean { fun resolve(
context: BytecodeContext,
forClass: ClassDef,
): Boolean {
for (method in forClass.methods) for (method in forClass.methods)
if (resolve(context, method, forClass)) if (resolve(context, method, forClass)) {
return true return true
}
return false return false
} }
@ -136,20 +145,26 @@ abstract class MethodFingerprint(
* @param context The [BytecodeContext] to host proxies. * @param context The [BytecodeContext] to host proxies.
* @return True if the resolution was successful or if the fingerprint is already resolved, false otherwise. * @return True if the resolution was successful or if the fingerprint is already resolved, false otherwise.
*/ */
fun resolve(context: BytecodeContext, method: Method, forClass: ClassDef): Boolean { fun resolve(
context: BytecodeContext,
method: Method,
forClass: ClassDef,
): Boolean {
val methodFingerprint = this val methodFingerprint = this
if (methodFingerprint.result != null) return true if (methodFingerprint.result != null) return true
if (methodFingerprint.returnType != null && !method.returnType.startsWith(methodFingerprint.returnType)) if (methodFingerprint.returnType != null && !method.returnType.startsWith(methodFingerprint.returnType)) {
return false return false
}
if (methodFingerprint.accessFlags != null && methodFingerprint.accessFlags != method.accessFlags) if (methodFingerprint.accessFlags != null && methodFingerprint.accessFlags != method.accessFlags) {
return false return false
}
fun parametersEqual( fun parametersEqual(
parameters1: Iterable<CharSequence>, parameters2: Iterable<CharSequence> parameters1: Iterable<CharSequence>,
parameters2: Iterable<CharSequence>,
): Boolean { ): Boolean {
if (parameters1.count() != parameters2.count()) return false if (parameters1.count() != parameters2.count()) return false
val iterator1 = parameters1.iterator() val iterator1 = parameters1.iterator()
@ -159,15 +174,19 @@ abstract class MethodFingerprint(
return true return true
} }
if (methodFingerprint.parameters != null && !parametersEqual( if (methodFingerprint.parameters != null &&
!parametersEqual(
methodFingerprint.parameters, // TODO: parseParameters() methodFingerprint.parameters, // TODO: parseParameters()
method.parameterTypes method.parameterTypes,
) )
) return false ) {
return false
}
@Suppress("UNNECESSARY_NOT_NULL_ASSERTION") @Suppress("UNNECESSARY_NOT_NULL_ASSERTION")
if (methodFingerprint.customFingerprint != null && !methodFingerprint.customFingerprint!!(method, forClass)) if (methodFingerprint.customFingerprint != null && !methodFingerprint.customFingerprint!!(method, forClass)) {
return false return false
}
val stringsScanResult: StringsScanResult? = val stringsScanResult: StringsScanResult? =
if (methodFingerprint.strings != null) { if (methodFingerprint.strings != null) {
@ -181,7 +200,9 @@ abstract class MethodFingerprint(
if ( if (
instruction.opcode != Opcode.CONST_STRING && instruction.opcode != Opcode.CONST_STRING &&
instruction.opcode != Opcode.CONST_STRING_JUMBO instruction.opcode != Opcode.CONST_STRING_JUMBO
) return@forEachIndexed ) {
return@forEachIndexed
}
val string = ((instruction as ReferenceInstruction).reference as StringReference).string val string = ((instruction as ReferenceInstruction).reference as StringReference).string
val index = stringsList.indexOfFirst(string::contains) val index = stringsList.indexOfFirst(string::contains)
@ -192,91 +213,98 @@ abstract class MethodFingerprint(
} }
if (stringsList.isNotEmpty()) return false if (stringsList.isNotEmpty()) return false
} },
) )
} else null } else {
null
val patternScanResult = if (methodFingerprint.opcodes != null) {
method.implementation?.instructions ?: return false
fun MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult.newWarnings(
pattern: Iterable<Opcode?>, instructions: Iterable<Instruction>
) = buildList {
for ((patternIndex, instructionIndex) in (this@newWarnings.startIndex until this@newWarnings.endIndex).withIndex()) {
val originalOpcode = instructions.elementAt(instructionIndex).opcode
val patternOpcode = pattern.elementAt(patternIndex)
if (patternOpcode == null || patternOpcode.ordinal == originalOpcode.ordinal) continue
this.add(
MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult.Warning(
originalOpcode,
patternOpcode,
instructionIndex,
patternIndex
)
)
}
} }
fun Method.patternScan( val patternScanResult =
fingerprint: MethodFingerprint if (methodFingerprint.opcodes != null) {
): MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult? { method.implementation?.instructions ?: return false
val instructions = this.implementation!!.instructions
val fingerprintFuzzyPatternScanThreshold = fingerprint.fuzzyPatternScanMethod?.threshold ?: 0
val pattern = fingerprint.opcodes!! fun MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult.newWarnings(
val instructionLength = instructions.count() pattern: Iterable<Opcode?>,
val patternLength = pattern.count() instructions: Iterable<Instruction>,
) = buildList {
for (index in 0 until instructionLength) { for ((patternIndex, instructionIndex) in (this@newWarnings.startIndex until this@newWarnings.endIndex).withIndex()) {
var patternIndex = 0 val originalOpcode = instructions.elementAt(instructionIndex).opcode
var threshold = fingerprintFuzzyPatternScanThreshold
while (index + patternIndex < instructionLength) {
val originalOpcode = instructions.elementAt(index + patternIndex).opcode
val patternOpcode = pattern.elementAt(patternIndex) val patternOpcode = pattern.elementAt(patternIndex)
if (patternOpcode != null && patternOpcode.ordinal != originalOpcode.ordinal) { if (patternOpcode == null || patternOpcode.ordinal == originalOpcode.ordinal) continue
// reaching maximum threshold (0) means,
// the pattern does not match to the current instructions
if (threshold-- == 0) break
}
if (patternIndex < patternLength - 1) { this.add(
// if the entire pattern has not been scanned yet MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult.Warning(
// continue the scan originalOpcode,
patternIndex++ patternOpcode,
continue instructionIndex,
} patternIndex,
// the pattern is valid, generate warnings if fuzzyPatternScanMethod is FuzzyPatternScanMethod ),
val result = )
MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult(
index,
index + patternIndex
)
if (fingerprint.fuzzyPatternScanMethod !is FuzzyPatternScanMethod) return result
result.warnings = result.newWarnings(pattern, instructions)
return result
} }
} }
return null fun Method.patternScan(
fingerprint: MethodFingerprint,
): MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult? {
val instructions = this.implementation!!.instructions
val fingerprintFuzzyPatternScanThreshold = fingerprint.fuzzyPatternScanMethod?.threshold ?: 0
val pattern = fingerprint.opcodes!!
val instructionLength = instructions.count()
val patternLength = pattern.count()
for (index in 0 until instructionLength) {
var patternIndex = 0
var threshold = fingerprintFuzzyPatternScanThreshold
while (index + patternIndex < instructionLength) {
val originalOpcode = instructions.elementAt(index + patternIndex).opcode
val patternOpcode = pattern.elementAt(patternIndex)
if (patternOpcode != null && patternOpcode.ordinal != originalOpcode.ordinal) {
// reaching maximum threshold (0) means,
// the pattern does not match to the current instructions
if (threshold-- == 0) break
}
if (patternIndex < patternLength - 1) {
// if the entire pattern has not been scanned yet
// continue the scan
patternIndex++
continue
}
// the pattern is valid, generate warnings if fuzzyPatternScanMethod is FuzzyPatternScanMethod
val result =
MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult(
index,
index + patternIndex,
)
if (fingerprint.fuzzyPatternScanMethod !is FuzzyPatternScanMethod) return result
result.warnings = result.newWarnings(pattern, instructions)
return result
}
}
return null
}
method.patternScan(methodFingerprint) ?: return false
} else {
null
} }
method.patternScan(methodFingerprint) ?: return false methodFingerprint.result =
} else null MethodFingerprintResult(
method,
methodFingerprint.result = MethodFingerprintResult( forClass,
method, MethodFingerprintResult.MethodFingerprintScanResult(
forClass, patternScanResult,
MethodFingerprintResult.MethodFingerprintScanResult( stringsScanResult,
patternScanResult, ),
stringsScanResult context,
), )
context
)
return true return true
} }
@ -309,12 +337,13 @@ abstract class MethodFingerprint(
* @param context The [BytecodeContext] to host proxies. * @param context The [BytecodeContext] to host proxies.
* @return True if the resolution was successful, false otherwise. * @return True if the resolution was successful, false otherwise.
*/ */
fun Iterable<MethodFingerprint>.resolve(context: BytecodeContext, classes: Iterable<ClassDef>) = fun Iterable<MethodFingerprint>.resolve(
forEach { fingerprint -> context: BytecodeContext,
for (classDef in classes) { classes: Iterable<ClassDef>,
if (fingerprint.resolve(context, classDef)) break ) = forEach { fingerprint ->
} for (classDef in classes) {
if (fingerprint.resolve(context, classDef)) break
} }
}
} }
} }

View File

@ -20,7 +20,7 @@ class MethodFingerprintResult(
val method: Method, val method: Method,
val classDef: ClassDef, val classDef: ClassDef,
val scanResult: MethodFingerprintScanResult, val scanResult: MethodFingerprintScanResult,
internal val context: BytecodeContext internal val context: BytecodeContext,
) { ) {
/** /**
* Returns a mutable clone of [classDef] * Returns a mutable clone of [classDef]
@ -50,7 +50,7 @@ class MethodFingerprintResult(
*/ */
class MethodFingerprintScanResult( class MethodFingerprintScanResult(
val patternScanResult: PatternScanResult?, val patternScanResult: PatternScanResult?,
val stringsScanResult: StringsScanResult? val stringsScanResult: StringsScanResult?,
) { ) {
/** /**
* The result of scanning strings on the [MethodFingerprint]. * The result of scanning strings on the [MethodFingerprint].
@ -74,7 +74,7 @@ class MethodFingerprintResult(
class PatternScanResult( class PatternScanResult(
val startIndex: Int, val startIndex: Int,
val endIndex: Int, val endIndex: Int,
var warnings: List<Warning>? = null var warnings: List<Warning>? = null,
) { ) {
/** /**
* Represents warnings of the pattern scan. * Represents warnings of the pattern scan.

View File

@ -8,5 +8,5 @@ import app.revanced.patcher.fingerprint.MethodFingerprint
*/ */
@Target(AnnotationTarget.CLASS) @Target(AnnotationTarget.CLASS)
annotation class FuzzyPatternScanMethod( annotation class FuzzyPatternScanMethod(
val threshold: Int = 1 val threshold: Int = 1,
) )

View File

@ -9,5 +9,5 @@ import app.revanced.patcher.fingerprint.MethodFingerprint
* @param fingerprints A list of [MethodFingerprint]s which will be resolved before the patch is executed. * @param fingerprints A list of [MethodFingerprint]s which will be resolved before the patch is executed.
*/ */
abstract class BytecodePatch( abstract class BytecodePatch(
internal val fingerprints : Set<MethodFingerprint> = emptySet(), internal val fingerprints: Set<MethodFingerprint> = emptySet(),
) : Patch<BytecodeContext>() ) : Patch<BytecodeContext>()

View File

@ -47,7 +47,6 @@ sealed class Patch<out T : Context<*>> {
var use = true var use = true
private set private set
// TODO: Remove this property, once integrations are coupled with patches. // TODO: Remove this property, once integrations are coupled with patches.
/** /**
* Weather or not the patch requires integrations. * Weather or not the patch requires integrations.
@ -64,9 +63,10 @@ sealed class Patch<out T : Context<*>> {
this::class.findAnnotation<app.revanced.patcher.patch.annotation.Patch>()?.let { annotation -> this::class.findAnnotation<app.revanced.patcher.patch.annotation.Patch>()?.let { annotation ->
name = annotation.name.ifEmpty { null } name = annotation.name.ifEmpty { null }
description = annotation.description.ifEmpty { null } description = annotation.description.ifEmpty { null }
compatiblePackages = annotation.compatiblePackages compatiblePackages =
.map { CompatiblePackage(it.name, it.versions.toSet().ifEmpty { null }) } annotation.compatiblePackages
.toSet().ifEmpty { null } .map { CompatiblePackage(it.name, it.versions.toSet().ifEmpty { null }) }
.toSet().ifEmpty { null }
dependencies = annotation.dependencies.toSet().ifEmpty { null } dependencies = annotation.dependencies.toSet().ifEmpty { null }
use = annotation.use use = annotation.use
requiresIntegrations = annotation.requiresIntegrations requiresIntegrations = annotation.requiresIntegrations

View File

@ -25,7 +25,7 @@ open class PatchOption<T>(
val description: String?, val description: String?,
val required: Boolean, val required: Boolean,
val valueType: String, val valueType: String,
val validator: PatchOption<T>.(T?) -> Boolean val validator: PatchOption<T>.(T?) -> Boolean,
) { ) {
/** /**
* The value of the [PatchOption]. * The value of the [PatchOption].
@ -45,6 +45,7 @@ open class PatchOption<T>(
uncheckedValue = value uncheckedValue = value
} }
/** /**
* Get the value of the [PatchOption]. * Get the value of the [PatchOption].
* *
@ -81,9 +82,16 @@ open class PatchOption<T>(
override fun toString() = value.toString() override fun toString() = value.toString()
operator fun getValue(thisRef: Any?, property: KProperty<*>) = value operator fun getValue(
thisRef: Any?,
property: KProperty<*>,
) = value
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) { operator fun setValue(
thisRef: Any?,
property: KProperty<*>,
value: T?,
) {
this.value = value this.value = value
} }
@ -111,9 +119,16 @@ open class PatchOption<T>(
title: String? = null, title: String? = null,
description: String? = null, description: String? = null,
required: Boolean = false, required: Boolean = false,
validator: PatchOption<String>.(String?) -> Boolean = { true } validator: PatchOption<String>.(String?) -> Boolean = { true },
) = PatchOption( ) = PatchOption(
key, default, values, title, description, required, "String", validator key,
default,
values,
title,
description,
required,
"String",
validator,
).also { registerOption(it) } ).also { registerOption(it) }
/** /**
@ -138,9 +153,16 @@ open class PatchOption<T>(
title: String? = null, title: String? = null,
description: String? = null, description: String? = null,
required: Boolean = false, required: Boolean = false,
validator: PatchOption<Int?>.(Int?) -> Boolean = { true } validator: PatchOption<Int?>.(Int?) -> Boolean = { true },
) = PatchOption( ) = PatchOption(
key, default, values, title, description, required, "Int", validator key,
default,
values,
title,
description,
required,
"Int",
validator,
).also { registerOption(it) } ).also { registerOption(it) }
/** /**
@ -165,7 +187,7 @@ open class PatchOption<T>(
title: String? = null, title: String? = null,
description: String? = null, description: String? = null,
required: Boolean = false, required: Boolean = false,
validator: PatchOption<Boolean?>.(Boolean?) -> Boolean = { true } validator: PatchOption<Boolean?>.(Boolean?) -> Boolean = { true },
) = PatchOption(key, default, values, title, description, required, "Boolean", validator).also { ) = PatchOption(key, default, values, title, description, required, "Boolean", validator).also {
registerOption(it) registerOption(it)
} }
@ -192,9 +214,16 @@ open class PatchOption<T>(
title: String? = null, title: String? = null,
description: String? = null, description: String? = null,
required: Boolean = false, required: Boolean = false,
validator: PatchOption<Float?>.(Float?) -> Boolean = { true } validator: PatchOption<Float?>.(Float?) -> Boolean = { true },
) = PatchOption( ) = PatchOption(
key, default, values, title, description, required, "Float", validator key,
default,
values,
title,
description,
required,
"Float",
validator,
).also { registerOption(it) } ).also { registerOption(it) }
/** /**
@ -219,9 +248,16 @@ open class PatchOption<T>(
title: String? = null, title: String? = null,
description: String? = null, description: String? = null,
required: Boolean = false, required: Boolean = false,
validator: PatchOption<Long?>.(Long?) -> Boolean = { true } validator: PatchOption<Long?>.(Long?) -> Boolean = { true },
) = PatchOption( ) = PatchOption(
key, default, values, title, description, required, "Long", validator key,
default,
values,
title,
description,
required,
"Long",
validator,
).also { registerOption(it) } ).also { registerOption(it) }
/** /**
@ -246,9 +282,16 @@ open class PatchOption<T>(
title: String? = null, title: String? = null,
description: String? = null, description: String? = null,
required: Boolean = false, required: Boolean = false,
validator: PatchOption<Array<String>?>.(Array<String>?) -> Boolean = { true } validator: PatchOption<Array<String>?>.(Array<String>?) -> Boolean = { true },
) = PatchOption( ) = PatchOption(
key, default, values, title, description, required, "StringArray", validator key,
default,
values,
title,
description,
required,
"StringArray",
validator,
).also { registerOption(it) } ).also { registerOption(it) }
/** /**
@ -273,9 +316,16 @@ open class PatchOption<T>(
title: String? = null, title: String? = null,
description: String? = null, description: String? = null,
required: Boolean = false, required: Boolean = false,
validator: PatchOption<Array<Int>?>.(Array<Int>?) -> Boolean = { true } validator: PatchOption<Array<Int>?>.(Array<Int>?) -> Boolean = { true },
) = PatchOption( ) = PatchOption(
key, default, values, title, description, required, "IntArray", validator key,
default,
values,
title,
description,
required,
"IntArray",
validator,
).also { registerOption(it) } ).also { registerOption(it) }
/** /**
@ -300,9 +350,16 @@ open class PatchOption<T>(
title: String? = null, title: String? = null,
description: String? = null, description: String? = null,
required: Boolean = false, required: Boolean = false,
validator: PatchOption<Array<Boolean>?>.(Array<Boolean>?) -> Boolean = { true } validator: PatchOption<Array<Boolean>?>.(Array<Boolean>?) -> Boolean = { true },
) = PatchOption( ) = PatchOption(
key, default, values, title, description, required, "BooleanArray", validator key,
default,
values,
title,
description,
required,
"BooleanArray",
validator,
).also { registerOption(it) } ).also { registerOption(it) }
/** /**
@ -327,9 +384,16 @@ open class PatchOption<T>(
title: String? = null, title: String? = null,
description: String? = null, description: String? = null,
required: Boolean = false, required: Boolean = false,
validator: PatchOption<Array<Float>?>.(Array<Float>?) -> Boolean = { true } validator: PatchOption<Array<Float>?>.(Array<Float>?) -> Boolean = { true },
) = PatchOption( ) = PatchOption(
key, default, values, title, description, required, "FloatArray", validator key,
default,
values,
title,
description,
required,
"FloatArray",
validator,
).also { registerOption(it) } ).also { registerOption(it) }
/** /**
@ -354,9 +418,16 @@ open class PatchOption<T>(
title: String? = null, title: String? = null,
description: String? = null, description: String? = null,
required: Boolean = false, required: Boolean = false,
validator: PatchOption<Array<Long>?>.(Array<Long>?) -> Boolean = { true } validator: PatchOption<Array<Long>?>.(Array<Long>?) -> Boolean = { true },
) = PatchOption( ) = PatchOption(
key, default, values, title, description, required, "LongArray", validator key,
default,
values,
title,
description,
required,
"LongArray",
validator,
).also { registerOption(it) } ).also { registerOption(it) }
private fun <P : Patch<*>> P.registerOption(option: PatchOption<*>) = option.also { options.register(it) } private fun <P : Patch<*>> P.registerOption(option: PatchOption<*>) = option.also { options.register(it) }

View File

@ -36,6 +36,6 @@ sealed class PatchOptionException(errorMessage: String) : Exception(errorMessage
* *
* @param key The key of the [PatchOption]. * @param key The key of the [PatchOption].
*/ */
class PatchOptionNotFoundException(key: String) class PatchOptionNotFoundException(key: String) :
: PatchOptionException("No option with key $key") PatchOptionException("No option with key $key")
} }

View File

@ -1,13 +1,12 @@
package app.revanced.patcher.patch.options package app.revanced.patcher.patch.options
/** /**
* A map of [PatchOption]s associated by their keys. * A map of [PatchOption]s associated by their keys.
* *
* @param options The [PatchOption]s to initialize with. * @param options The [PatchOption]s to initialize with.
*/ */
class PatchOptions internal constructor( class PatchOptions internal constructor(
private val options: MutableMap<String, PatchOption<*>> = mutableMapOf() private val options: MutableMap<String, PatchOption<*>> = mutableMapOf(),
) : MutableMap<String, PatchOption<*>> by options { ) : MutableMap<String, PatchOption<*>> by options {
/** /**
* Register a [PatchOption]. Acts like [MutableMap.put]. * Register a [PatchOption]. Acts like [MutableMap.put].
@ -23,7 +22,10 @@ class PatchOptions internal constructor(
* @param value The value. * @param value The value.
* @throws PatchOptionException.PatchOptionNotFoundException If the option does not exist. * @throws PatchOptionException.PatchOptionNotFoundException If the option does not exist.
*/ */
operator fun <T : Any> set(key: String, value: T?) { operator fun <T : Any> set(
key: String,
value: T?,
) {
val option = this[key] val option = this[key]
try { try {
@ -40,6 +42,5 @@ class PatchOptions internal constructor(
/** /**
* Get an option. * Get an option.
*/ */
override operator fun get(key: String) = override operator fun get(key: String) = options[key] ?: throw PatchOptionException.PatchOptionNotFoundException(key)
options[key] ?: throw PatchOptionException.PatchOptionNotFoundException(key)
} }

View File

@ -34,9 +34,12 @@ internal object ClassMerger {
* @param context The context to traverse the class hierarchy in. * @param context The context to traverse the class hierarchy in.
* @return The merged class or the original class if no merge was needed. * @return The merged class or the original class if no merge was needed.
*/ */
fun ClassDef.merge(otherClass: ClassDef, context: BytecodeContext) = this fun ClassDef.merge(
//.fixFieldAccess(otherClass) otherClass: ClassDef,
//.fixMethodAccess(otherClass) context: BytecodeContext,
) = this
// .fixFieldAccess(otherClass)
// .fixMethodAccess(otherClass)
.addMissingFields(otherClass) .addMissingFields(otherClass)
.addMissingMethods(otherClass) .addMissingMethods(otherClass)
.publicize(otherClass, context) .publicize(otherClass, context)
@ -47,13 +50,14 @@ internal object ClassMerger {
* @param fromClass The class to add missing methods from. * @param fromClass The class to add missing methods from.
*/ */
private fun ClassDef.addMissingMethods(fromClass: ClassDef): ClassDef { private fun ClassDef.addMissingMethods(fromClass: ClassDef): ClassDef {
val missingMethods = fromClass.methods.let { fromMethods -> val missingMethods =
methods.filterNot { method -> fromClass.methods.let { fromMethods ->
fromMethods.any { fromMethod -> methods.filterNot { method ->
MethodUtil.methodSignaturesMatch(fromMethod, method) fromMethods.any { fromMethod ->
MethodUtil.methodSignaturesMatch(fromMethod, method)
}
} }
} }
}
if (missingMethods.isEmpty()) return this if (missingMethods.isEmpty()) return this
@ -70,9 +74,10 @@ internal object ClassMerger {
* @param fromClass The class to add missing fields from. * @param fromClass The class to add missing fields from.
*/ */
private fun ClassDef.addMissingFields(fromClass: ClassDef): ClassDef { private fun ClassDef.addMissingFields(fromClass: ClassDef): ClassDef {
val missingFields = fields.filterNotAny(fromClass.fields) { field, fromField -> val missingFields =
fromField.name == field.name fields.filterNotAny(fromClass.fields) { field, fromField ->
} fromField.name == field.name
}
if (missingFields.isEmpty()) return this if (missingFields.isEmpty()) return this
@ -88,18 +93,22 @@ internal object ClassMerger {
* @param reference The class to check the [AccessFlags] of. * @param reference The class to check the [AccessFlags] of.
* @param context The context to traverse the class hierarchy in. * @param context The context to traverse the class hierarchy in.
*/ */
private fun ClassDef.publicize(reference: ClassDef, context: BytecodeContext) = private fun ClassDef.publicize(
if (reference.accessFlags.isPublic() && !accessFlags.isPublic()) reference: ClassDef,
this.asMutableClass().apply { context: BytecodeContext,
context.traverseClassHierarchy(this) { ) = if (reference.accessFlags.isPublic() && !accessFlags.isPublic()) {
if (accessFlags.isPublic()) return@traverseClassHierarchy this.asMutableClass().apply {
context.traverseClassHierarchy(this) {
if (accessFlags.isPublic()) return@traverseClassHierarchy
logger.fine("Publicizing ${this.type}") logger.fine("Publicizing ${this.type}")
accessFlags = accessFlags.toPublic() accessFlags = accessFlags.toPublic()
}
} }
else this }
} else {
this
}
/** /**
* Publicize fields if they are public in [reference]. * Publicize fields if they are public in [reference].
@ -107,11 +116,12 @@ internal object ClassMerger {
* @param reference The class to check the [AccessFlags] of the fields in. * @param reference The class to check the [AccessFlags] of the fields in.
*/ */
private fun ClassDef.fixFieldAccess(reference: ClassDef): ClassDef { private fun ClassDef.fixFieldAccess(reference: ClassDef): ClassDef {
val brokenFields = fields.filterAny(reference.fields) { field, referenceField -> val brokenFields =
if (field.name != referenceField.name) return@filterAny false fields.filterAny(reference.fields) { field, referenceField ->
if (field.name != referenceField.name) return@filterAny false
referenceField.accessFlags.isPublic() && !field.accessFlags.isPublic() referenceField.accessFlags.isPublic() && !field.accessFlags.isPublic()
} }
if (brokenFields.isEmpty()) return this if (brokenFields.isEmpty()) return this
@ -135,11 +145,12 @@ internal object ClassMerger {
* @param reference The class to check the [AccessFlags] of the methods in. * @param reference The class to check the [AccessFlags] of the methods in.
*/ */
private fun ClassDef.fixMethodAccess(reference: ClassDef): ClassDef { private fun ClassDef.fixMethodAccess(reference: ClassDef): ClassDef {
val brokenMethods = methods.filterAny(reference.methods) { method, referenceMethod -> val brokenMethods =
if (!MethodUtil.methodSignaturesMatch(method, referenceMethod)) return@filterAny false methods.filterAny(reference.methods) { method, referenceMethod ->
if (!MethodUtil.methodSignaturesMatch(method, referenceMethod)) return@filterAny false
referenceMethod.accessFlags.isPublic() && !method.accessFlags.isPublic() referenceMethod.accessFlags.isPublic() && !method.accessFlags.isPublic()
} }
if (brokenMethods.isEmpty()) return this if (brokenMethods.isEmpty()) return this
@ -164,7 +175,10 @@ internal object ClassMerger {
* @param targetClass the class to start traversing the class hierarchy from * @param targetClass the class to start traversing the class hierarchy from
* @param callback function that is called for every class in the hierarchy * @param callback function that is called for every class in the hierarchy
*/ */
fun BytecodeContext.traverseClassHierarchy(targetClass: MutableClass, callback: MutableClass.() -> Unit) { fun BytecodeContext.traverseClassHierarchy(
targetClass: MutableClass,
callback: MutableClass.() -> Unit,
) {
callback(targetClass) callback(targetClass)
this.findClass(targetClass.superclass ?: return)?.mutableClass?.let { this.findClass(targetClass.superclass ?: return)?.mutableClass?.let {
traverseClassHierarchy(it, callback) traverseClassHierarchy(it, callback)
@ -195,7 +209,8 @@ internal object ClassMerger {
* @return The [this] filtered on [needles] matching the given [predicate]. * @return The [this] filtered on [needles] matching the given [predicate].
*/ */
fun <HayType, NeedleType> Iterable<HayType>.filterAny( fun <HayType, NeedleType> Iterable<HayType>.filterAny(
needles: Iterable<NeedleType>, predicate: (HayType, NeedleType) -> Boolean needles: Iterable<NeedleType>,
predicate: (HayType, NeedleType) -> Boolean,
) = Iterable<HayType>::filter.any(this, needles, predicate) ) = Iterable<HayType>::filter.any(this, needles, predicate)
/** /**
@ -206,13 +221,14 @@ internal object ClassMerger {
* @return The [this] filtered on [needles] not matching the given [predicate]. * @return The [this] filtered on [needles] not matching the given [predicate].
*/ */
fun <HayType, NeedleType> Iterable<HayType>.filterNotAny( fun <HayType, NeedleType> Iterable<HayType>.filterNotAny(
needles: Iterable<NeedleType>, predicate: (HayType, NeedleType) -> Boolean needles: Iterable<NeedleType>,
predicate: (HayType, NeedleType) -> Boolean,
) = Iterable<HayType>::filterNot.any(this, needles, predicate) ) = Iterable<HayType>::filterNot.any(this, needles, predicate)
fun <HayType, NeedleType> KFunction2<Iterable<HayType>, (HayType) -> Boolean, List<HayType>>.any( fun <HayType, NeedleType> KFunction2<Iterable<HayType>, (HayType) -> Boolean, List<HayType>>.any(
haystack: Iterable<HayType>, haystack: Iterable<HayType>,
needles: Iterable<NeedleType>, needles: Iterable<NeedleType>,
predicate: (HayType, NeedleType) -> Boolean predicate: (HayType, NeedleType) -> Boolean,
) = this(haystack) { hay -> ) = this(haystack) { hay ->
needles.any { needle -> needles.any { needle ->
predicate(hay, needle) predicate(hay, needle)

View File

@ -31,9 +31,9 @@ class DomFileEditor internal constructor(
/** /**
* The document of the xml file * The document of the xml file
*/ */
val file: Document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputStream) val file: Document =
.also(Document::normalize) DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputStream)
.also(Document::normalize)
// lazily open an output stream // lazily open an output stream
// this is required because when constructing a DomFileEditor the output stream is created along with the input stream, which is not allowed // this is required because when constructing a DomFileEditor the output stream is created along with the input stream, which is not allowed
@ -58,12 +58,13 @@ class DomFileEditor internal constructor(
outputStream?.let { outputStream?.let {
// prevent writing to same file, if it is being locked // prevent writing to same file, if it is being locked
// isLocked will be false if the editor was created through a stream // isLocked will be false if the editor was created through a stream
val isLocked = filePath?.let { path -> val isLocked =
val isLocked = locks[path]!! > 1 filePath?.let { path ->
// decrease the lock count if the editor was opened for a file val isLocked = locks[path]!! > 1
locks.merge(path, -1, Integer::sum) // decrease the lock count if the editor was opened for a file
isLocked locks.merge(path, -1, Integer::sum)
} ?: false isLocked
} ?: false
// if unlocked, write back to the file // if unlocked, write back to the file
if (!isLocked) { if (!isLocked) {

View File

@ -19,15 +19,16 @@ class ProxyClassList internal constructor(classes: MutableSet<ClassDef>) : Mutab
/** /**
* Replace all classes with their mutated versions. * Replace all classes with their mutated versions.
*/ */
internal fun replaceClasses() = proxies.removeIf { proxy -> internal fun replaceClasses() =
// If the proxy is unused, return false to keep it in the proxies list. proxies.removeIf { proxy ->
// If the proxy is unused, return false to keep it in the proxies list.
if (!proxy.resolved) return@removeIf false if (!proxy.resolved) return@removeIf false
// If it has been used, replace the original class with the mutable class. // If it has been used, replace the original class with the mutable class.
remove(proxy.immutableClass) remove(proxy.immutableClass)
add(proxy.mutableClass) add(proxy.mutableClass)
// Return true to remove the proxy from the proxies list. // Return true to remove the proxy from the proxies list.
return@removeIf true return@removeIf true
} }
} }

View File

@ -14,7 +14,7 @@ import com.android.tools.smali.dexlib2.util.MethodUtil
*/ */
class MethodWalker internal constructor( class MethodWalker internal constructor(
private val bytecodeContext: BytecodeContext, private val bytecodeContext: BytecodeContext,
private var currentMethod: Method private var currentMethod: Method,
) { ) {
/** /**
* Get the method which was walked last. * Get the method which was walked last.
@ -36,7 +36,10 @@ class MethodWalker internal constructor(
* @param walkMutable If this is true, the class of the method will be resolved mutably. * @param walkMutable If this is true, the class of the method will be resolved mutably.
* @return The same [MethodWalker] instance with the method at [offset]. * @return The same [MethodWalker] instance with the method at [offset].
*/ */
fun nextMethod(offset: Int, walkMutable: Boolean = false): MethodWalker { fun nextMethod(
offset: Int,
walkMutable: Boolean = false,
): MethodWalker {
currentMethod.implementation?.instructions?.let { instructions -> currentMethod.implementation?.instructions?.let { instructions ->
val instruction = instructions.elementAt(offset) val instruction = instructions.elementAt(offset)
@ -44,9 +47,10 @@ class MethodWalker internal constructor(
val proxy = bytecodeContext.findClass(newMethod.definingClass)!! val proxy = bytecodeContext.findClass(newMethod.definingClass)!!
val methods = if (walkMutable) proxy.mutableClass.methods else proxy.immutableClass.methods val methods = if (walkMutable) proxy.mutableClass.methods else proxy.immutableClass.methods
currentMethod = methods.first { currentMethod =
return@first MethodUtil.methodSignaturesMatch(it, newMethod) methods.first {
} return@first MethodUtil.methodSignaturesMatch(it, newMethod)
}
return this return this
} }
throw MethodNotFoundException("This method can not be walked at offset $offset inside the method ${currentMethod.name}") throw MethodNotFoundException("This method can not be walked at offset $offset inside the method ${currentMethod.name}")

View File

@ -27,7 +27,8 @@ class ClassProxy internal constructor(
resolved = true resolved = true
if (immutableClass is MutableClass) { if (immutableClass is MutableClass) {
immutableClass immutableClass
} else } else {
MutableClass(immutableClass) MutableClass(immutableClass)
}
} }
} }

View File

@ -3,11 +3,11 @@ package app.revanced.patcher.util.proxy.mutableTypes
import app.revanced.patcher.util.proxy.mutableTypes.MutableAnnotation.Companion.toMutable import app.revanced.patcher.util.proxy.mutableTypes.MutableAnnotation.Companion.toMutable
import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
import com.google.common.collect.Iterables
import com.android.tools.smali.dexlib2.base.reference.BaseTypeReference import com.android.tools.smali.dexlib2.base.reference.BaseTypeReference
import com.android.tools.smali.dexlib2.iface.ClassDef import com.android.tools.smali.dexlib2.iface.ClassDef
import com.android.tools.smali.dexlib2.util.FieldUtil import com.android.tools.smali.dexlib2.util.FieldUtil
import com.android.tools.smali.dexlib2.util.MethodUtil import com.android.tools.smali.dexlib2.util.MethodUtil
import com.google.common.collect.Iterables
class MutableClass(classDef: ClassDef) : ClassDef, BaseTypeReference() { class MutableClass(classDef: ClassDef) : ClassDef, BaseTypeReference() {
// Class // Class

View File

@ -5,7 +5,8 @@ import com.android.tools.smali.dexlib2.base.value.BaseAnnotationEncodedValue
import com.android.tools.smali.dexlib2.iface.AnnotationElement import com.android.tools.smali.dexlib2.iface.AnnotationElement
import com.android.tools.smali.dexlib2.iface.value.AnnotationEncodedValue import com.android.tools.smali.dexlib2.iface.value.AnnotationEncodedValue
class MutableAnnotationEncodedValue(annotationEncodedValue: AnnotationEncodedValue) : BaseAnnotationEncodedValue(), class MutableAnnotationEncodedValue(annotationEncodedValue: AnnotationEncodedValue) :
BaseAnnotationEncodedValue(),
MutableEncodedValue { MutableEncodedValue {
private var type = annotationEncodedValue.type private var type = annotationEncodedValue.type

View File

@ -3,7 +3,8 @@ package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
import com.android.tools.smali.dexlib2.base.value.BaseBooleanEncodedValue import com.android.tools.smali.dexlib2.base.value.BaseBooleanEncodedValue
import com.android.tools.smali.dexlib2.iface.value.BooleanEncodedValue import com.android.tools.smali.dexlib2.iface.value.BooleanEncodedValue
class MutableBooleanEncodedValue(booleanEncodedValue: BooleanEncodedValue) : BaseBooleanEncodedValue(), class MutableBooleanEncodedValue(booleanEncodedValue: BooleanEncodedValue) :
BaseBooleanEncodedValue(),
MutableEncodedValue { MutableEncodedValue {
private var value = booleanEncodedValue.value private var value = booleanEncodedValue.value

View File

@ -3,7 +3,8 @@ package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
import com.android.tools.smali.dexlib2.base.value.BaseDoubleEncodedValue import com.android.tools.smali.dexlib2.base.value.BaseDoubleEncodedValue
import com.android.tools.smali.dexlib2.iface.value.DoubleEncodedValue import com.android.tools.smali.dexlib2.iface.value.DoubleEncodedValue
class MutableDoubleEncodedValue(doubleEncodedValue: DoubleEncodedValue) : BaseDoubleEncodedValue(), class MutableDoubleEncodedValue(doubleEncodedValue: DoubleEncodedValue) :
BaseDoubleEncodedValue(),
MutableEncodedValue { MutableEncodedValue {
private var value = doubleEncodedValue.value private var value = doubleEncodedValue.value

View File

@ -4,7 +4,8 @@ import com.android.tools.smali.dexlib2.base.value.BaseMethodEncodedValue
import com.android.tools.smali.dexlib2.iface.reference.MethodReference import com.android.tools.smali.dexlib2.iface.reference.MethodReference
import com.android.tools.smali.dexlib2.iface.value.MethodEncodedValue import com.android.tools.smali.dexlib2.iface.value.MethodEncodedValue
class MutableMethodEncodedValue(methodEncodedValue: MethodEncodedValue) : BaseMethodEncodedValue(), class MutableMethodEncodedValue(methodEncodedValue: MethodEncodedValue) :
BaseMethodEncodedValue(),
MutableEncodedValue { MutableEncodedValue {
private var value = methodEncodedValue.value private var value = methodEncodedValue.value

View File

@ -22,6 +22,4 @@ class MutableMethodHandleEncodedValue(methodHandleEncodedValue: MethodHandleEnco
return MutableMethodHandleEncodedValue(this) return MutableMethodHandleEncodedValue(this)
} }
} }
} }

View File

@ -4,7 +4,8 @@ import com.android.tools.smali.dexlib2.base.value.BaseMethodTypeEncodedValue
import com.android.tools.smali.dexlib2.iface.reference.MethodProtoReference import com.android.tools.smali.dexlib2.iface.reference.MethodProtoReference
import com.android.tools.smali.dexlib2.iface.value.MethodTypeEncodedValue import com.android.tools.smali.dexlib2.iface.value.MethodTypeEncodedValue
class MutableMethodTypeEncodedValue(methodTypeEncodedValue: MethodTypeEncodedValue) : BaseMethodTypeEncodedValue(), class MutableMethodTypeEncodedValue(methodTypeEncodedValue: MethodTypeEncodedValue) :
BaseMethodTypeEncodedValue(),
MutableEncodedValue { MutableEncodedValue {
private var value = methodTypeEncodedValue.value private var value = methodTypeEncodedValue.value
@ -21,6 +22,4 @@ class MutableMethodTypeEncodedValue(methodTypeEncodedValue: MethodTypeEncodedVal
return MutableMethodTypeEncodedValue(this) return MutableMethodTypeEncodedValue(this)
} }
} }
} }

View File

@ -4,7 +4,8 @@ import com.android.tools.smali.dexlib2.base.value.BaseStringEncodedValue
import com.android.tools.smali.dexlib2.iface.value.ByteEncodedValue import com.android.tools.smali.dexlib2.iface.value.ByteEncodedValue
import com.android.tools.smali.dexlib2.iface.value.StringEncodedValue import com.android.tools.smali.dexlib2.iface.value.StringEncodedValue
class MutableStringEncodedValue(stringEncodedValue: StringEncodedValue) : BaseStringEncodedValue(), class MutableStringEncodedValue(stringEncodedValue: StringEncodedValue) :
BaseStringEncodedValue(),
MutableEncodedValue { MutableEncodedValue {
private var value = stringEncodedValue.value private var value = stringEncodedValue.value

View File

@ -1,9 +1,6 @@
package app.revanced.patcher.util.smali package app.revanced.patcher.util.smali
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import org.antlr.runtime.CommonTokenStream
import org.antlr.runtime.TokenSource
import org.antlr.runtime.tree.CommonTreeNodeStream
import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcodes import com.android.tools.smali.dexlib2.Opcodes
import com.android.tools.smali.dexlib2.builder.BuilderInstruction import com.android.tools.smali.dexlib2.builder.BuilderInstruction
@ -12,6 +9,9 @@ import com.android.tools.smali.smali.LexerErrorInterface
import com.android.tools.smali.smali.smaliFlexLexer import com.android.tools.smali.smali.smaliFlexLexer
import com.android.tools.smali.smali.smaliParser import com.android.tools.smali.smali.smaliParser
import com.android.tools.smali.smali.smaliTreeWalker import com.android.tools.smali.smali.smaliTreeWalker
import org.antlr.runtime.CommonTokenStream
import org.antlr.runtime.TokenSource
import org.antlr.runtime.tree.CommonTreeNodeStream
import java.io.InputStreamReader import java.io.InputStreamReader
import java.util.Locale import java.util.Locale
@ -32,15 +32,23 @@ class InlineSmaliCompiler {
* if the parameters and registers of the method are passed. * if the parameters and registers of the method are passed.
*/ */
fun compile( fun compile(
instructions: String, parameters: String, registers: Int, forStaticMethod: Boolean instructions: String,
parameters: String,
registers: Int,
forStaticMethod: Boolean,
): List<BuilderInstruction> { ): List<BuilderInstruction> {
val input = METHOD_TEMPLATE.format(Locale.ENGLISH, val input =
if (forStaticMethod) { METHOD_TEMPLATE.format(
"static" Locale.ENGLISH,
} else { if (forStaticMethod) {
"" "static"
}, parameters, registers, instructions } else {
) ""
},
parameters,
registers,
instructions,
)
val reader = InputStreamReader(input.byteInputStream()) val reader = InputStreamReader(input.byteInputStream())
val lexer: LexerErrorInterface = smaliFlexLexer(reader, 15) val lexer: LexerErrorInterface = smaliFlexLexer(reader, 15)
val tokens = CommonTokenStream(lexer as TokenSource) val tokens = CommonTokenStream(lexer as TokenSource)
@ -48,7 +56,7 @@ class InlineSmaliCompiler {
val result = parser.smali_file() val result = parser.smali_file()
if (parser.numberOfSyntaxErrors > 0 || lexer.numberOfSyntaxErrors > 0) { if (parser.numberOfSyntaxErrors > 0 || lexer.numberOfSyntaxErrors > 0) {
throw IllegalStateException( throw IllegalStateException(
"Encountered ${parser.numberOfSyntaxErrors} parser syntax errors and ${lexer.numberOfSyntaxErrors} lexer syntax errors!" "Encountered ${parser.numberOfSyntaxErrors} parser syntax errors and ${lexer.numberOfSyntaxErrors} lexer syntax errors!",
) )
} }
val treeStream = CommonTreeNodeStream(result.tree) val treeStream = CommonTreeNodeStream(result.tree)
@ -70,10 +78,11 @@ class InlineSmaliCompiler {
* @returns A list of instructions. * @returns A list of instructions.
*/ */
fun String.toInstructions(method: MutableMethod? = null): List<BuilderInstruction> { fun String.toInstructions(method: MutableMethod? = null): List<BuilderInstruction> {
return InlineSmaliCompiler.compile(this, return InlineSmaliCompiler.compile(
this,
method?.parameters?.joinToString("") { it } ?: "", method?.parameters?.joinToString("") { it } ?: "",
method?.implementation?.registerCount ?: 1, method?.implementation?.registerCount ?: 1,
method?.let { AccessFlags.STATIC.isSet(it.accessFlags) } ?: true method?.let { AccessFlags.STATIC.isSet(it.accessFlags) } ?: true,
) )
} }

View File

@ -27,180 +27,202 @@ private object InstructionExtensionsTest {
private lateinit var testMethodImplementation: MutableMethodImplementation private lateinit var testMethodImplementation: MutableMethodImplementation
@BeforeEach @BeforeEach
fun createTestMethod() = ImmutableMethod( fun createTestMethod() =
"TestClass;", ImmutableMethod(
"testMethod", "TestClass;",
null, "testMethod",
"V", null,
AccessFlags.PUBLIC.value, "V",
null, AccessFlags.PUBLIC.value,
null, null,
MutableMethodImplementation(16).also { testMethodImplementation = it }.apply { null,
repeat(10) { i -> this.addInstruction(TestInstruction(i)) } MutableMethodImplementation(16).also { testMethodImplementation = it }.apply {
}, repeat(10) { i -> this.addInstruction(TestInstruction(i)) }
).let { testMethod = it.toMutable() } },
).let { testMethod = it.toMutable() }
@Test @Test
fun addInstructionsToImplementationIndexed() = applyToImplementation { fun addInstructionsToImplementationIndexed() =
addInstructions(5, getTestInstructions(5..6)).also { applyToImplementation {
assertRegisterIs(5, 5) addInstructions(5, getTestInstructions(5..6)).also {
assertRegisterIs(6, 6) assertRegisterIs(5, 5)
assertRegisterIs(6, 6)
assertRegisterIs(5, 7) assertRegisterIs(5, 7)
}
} }
}
@Test @Test
fun addInstructionsToImplementation() = applyToImplementation { fun addInstructionsToImplementation() =
addInstructions(getTestInstructions(10..11)).also { applyToImplementation {
assertRegisterIs(10, 10) addInstructions(getTestInstructions(10..11)).also {
assertRegisterIs(11, 11) assertRegisterIs(10, 10)
assertRegisterIs(11, 11)
}
} }
}
@Test @Test
fun removeInstructionsFromImplementationIndexed() = applyToImplementation { fun removeInstructionsFromImplementationIndexed() =
removeInstructions(5, 5).also { assertRegisterIs(4, 4) } applyToImplementation {
} removeInstructions(5, 5).also { assertRegisterIs(4, 4) }
@Test
fun removeInstructionsFromImplementation() = applyToImplementation {
removeInstructions(0).also { assertRegisterIs(9, 9) }
removeInstructions(1).also { assertRegisterIs(1, 0) }
removeInstructions(2).also { assertRegisterIs(3, 0) }
}
@Test
fun replaceInstructionsInImplementationIndexed() = applyToImplementation {
replaceInstructions(5, getTestInstructions(0..1)).also {
assertRegisterIs(0, 5)
assertRegisterIs(1, 6)
assertRegisterIs(7, 7)
} }
}
@Test @Test
fun addInstructionToMethodIndexed() = applyToMethod { fun removeInstructionsFromImplementation() =
addInstruction(5, TestInstruction(0)).also { assertRegisterIs(0, 5) } applyToImplementation {
} removeInstructions(0).also { assertRegisterIs(9, 9) }
removeInstructions(1).also { assertRegisterIs(1, 0) }
@Test removeInstructions(2).also { assertRegisterIs(3, 0) }
fun addInstructionToMethod() = applyToMethod {
addInstruction(TestInstruction(0)).also { assertRegisterIs(0, 10) }
}
@Test
fun addSmaliInstructionToMethodIndexed() = applyToMethod {
addInstruction(5, getTestSmaliInstruction(0)).also { assertRegisterIs(0, 5) }
}
@Test
fun addSmaliInstructionToMethod() = applyToMethod {
addInstruction(getTestSmaliInstruction(0)).also { assertRegisterIs(0, 10) }
}
@Test
fun addInstructionsToMethodIndexed() = applyToMethod {
addInstructions(5, getTestInstructions(0..1)).also {
assertRegisterIs(0, 5)
assertRegisterIs(1, 6)
assertRegisterIs(5, 7)
} }
}
@Test @Test
fun addInstructionsToMethod() = applyToMethod { fun replaceInstructionsInImplementationIndexed() =
addInstructions(getTestInstructions(0..1)).also { applyToImplementation {
assertRegisterIs(0, 10) replaceInstructions(5, getTestInstructions(0..1)).also {
assertRegisterIs(1, 11) assertRegisterIs(0, 5)
assertRegisterIs(1, 6)
assertRegisterIs(9, 9) assertRegisterIs(7, 7)
}
} }
}
@Test @Test
fun addSmaliInstructionsToMethodIndexed() = applyToMethod { fun addInstructionToMethodIndexed() =
addInstructionsWithLabels(5, getTestSmaliInstructions(0..1)).also { applyToMethod {
assertRegisterIs(0, 5) addInstruction(5, TestInstruction(0)).also { assertRegisterIs(0, 5) }
assertRegisterIs(1, 6)
assertRegisterIs(5, 7)
} }
}
@Test @Test
fun addSmaliInstructionsToMethod() = applyToMethod { fun addInstructionToMethod() =
addInstructions(getTestSmaliInstructions(0..1)).also { applyToMethod {
assertRegisterIs(0, 10) addInstruction(TestInstruction(0)).also { assertRegisterIs(0, 10) }
assertRegisterIs(1, 11)
assertRegisterIs(9, 9)
} }
}
@Test @Test
fun addSmaliInstructionsWithExternalLabelToMethodIndexed() = applyToMethod { fun addSmaliInstructionToMethodIndexed() =
val label = ExternalLabel("testLabel", getInstruction(5)) applyToMethod {
addInstruction(5, getTestSmaliInstruction(0)).also { assertRegisterIs(0, 5) }
addInstructionsWithLabels(
5,
getTestSmaliInstructions(0..1).plus("\n").plus("goto :${label.name}"),
label
).also {
assertRegisterIs(0, 5)
assertRegisterIs(1, 6)
assertRegisterIs(5, 8)
val gotoTarget = getInstruction<BuilderOffsetInstruction>(7)
.target.location.instruction as OneRegisterInstruction
assertEquals(5, gotoTarget.registerA)
} }
}
@Test @Test
fun removeInstructionFromMethodIndexed() = applyToMethod { fun addSmaliInstructionToMethod() =
removeInstruction(5).also { applyToMethod {
assertRegisterIs(4, 4) addInstruction(getTestSmaliInstruction(0)).also { assertRegisterIs(0, 10) }
assertRegisterIs(6, 5)
} }
}
@Test @Test
fun removeInstructionsFromMethodIndexed() = applyToMethod { fun addInstructionsToMethodIndexed() =
removeInstructions(5, 5).also { assertRegisterIs(4, 4) } applyToMethod {
} addInstructions(5, getTestInstructions(0..1)).also {
assertRegisterIs(0, 5)
assertRegisterIs(1, 6)
@Test assertRegisterIs(5, 7)
fun removeInstructionsFromMethod() = applyToMethod { }
removeInstructions(0).also { assertRegisterIs(9, 9) }
removeInstructions(1).also { assertRegisterIs(1, 0) }
removeInstructions(2).also { assertRegisterIs(3, 0) }
}
@Test
fun replaceInstructionInMethodIndexed() = applyToMethod {
replaceInstruction(5, TestInstruction(0)).also { assertRegisterIs(0, 5) }
}
@Test
fun replaceInstructionsInMethodIndexed() = applyToMethod {
replaceInstructions(5, getTestInstructions(0..1)).also {
assertRegisterIs(0, 5)
assertRegisterIs(1, 6)
assertRegisterIs(7, 7)
} }
}
@Test @Test
fun replaceSmaliInstructionsInMethodIndexed() = applyToMethod { fun addInstructionsToMethod() =
replaceInstructions(5, getTestSmaliInstructions(0..1)).also { applyToMethod {
assertRegisterIs(0, 5) addInstructions(getTestInstructions(0..1)).also {
assertRegisterIs(1, 6) assertRegisterIs(0, 10)
assertRegisterIs(7, 7) assertRegisterIs(1, 11)
assertRegisterIs(9, 9)
}
}
@Test
fun addSmaliInstructionsToMethodIndexed() =
applyToMethod {
addInstructionsWithLabels(5, getTestSmaliInstructions(0..1)).also {
assertRegisterIs(0, 5)
assertRegisterIs(1, 6)
assertRegisterIs(5, 7)
}
}
@Test
fun addSmaliInstructionsToMethod() =
applyToMethod {
addInstructions(getTestSmaliInstructions(0..1)).also {
assertRegisterIs(0, 10)
assertRegisterIs(1, 11)
assertRegisterIs(9, 9)
}
}
@Test
fun addSmaliInstructionsWithExternalLabelToMethodIndexed() =
applyToMethod {
val label = ExternalLabel("testLabel", getInstruction(5))
addInstructionsWithLabels(
5,
getTestSmaliInstructions(0..1).plus("\n").plus("goto :${label.name}"),
label,
).also {
assertRegisterIs(0, 5)
assertRegisterIs(1, 6)
assertRegisterIs(5, 8)
val gotoTarget =
getInstruction<BuilderOffsetInstruction>(7)
.target.location.instruction as OneRegisterInstruction
assertEquals(5, gotoTarget.registerA)
}
}
@Test
fun removeInstructionFromMethodIndexed() =
applyToMethod {
removeInstruction(5).also {
assertRegisterIs(4, 4)
assertRegisterIs(6, 5)
}
}
@Test
fun removeInstructionsFromMethodIndexed() =
applyToMethod {
removeInstructions(5, 5).also { assertRegisterIs(4, 4) }
}
@Test
fun removeInstructionsFromMethod() =
applyToMethod {
removeInstructions(0).also { assertRegisterIs(9, 9) }
removeInstructions(1).also { assertRegisterIs(1, 0) }
removeInstructions(2).also { assertRegisterIs(3, 0) }
}
@Test
fun replaceInstructionInMethodIndexed() =
applyToMethod {
replaceInstruction(5, TestInstruction(0)).also { assertRegisterIs(0, 5) }
}
@Test
fun replaceInstructionsInMethodIndexed() =
applyToMethod {
replaceInstructions(5, getTestInstructions(0..1)).also {
assertRegisterIs(0, 5)
assertRegisterIs(1, 6)
assertRegisterIs(7, 7)
}
}
@Test
fun replaceSmaliInstructionsInMethodIndexed() =
applyToMethod {
replaceInstructions(5, getTestSmaliInstructions(0..1)).also {
assertRegisterIs(0, 5)
assertRegisterIs(1, 6)
assertRegisterIs(7, 7)
}
} }
}
// region Helper methods // region Helper methods
@ -212,20 +234,27 @@ private object InstructionExtensionsTest {
testMethod.apply(block) testMethod.apply(block)
} }
private fun MutableMethodImplementation.assertRegisterIs(register: Int, atIndex: Int) = assertEquals( private fun MutableMethodImplementation.assertRegisterIs(
register, getInstruction<OneRegisterInstruction>(atIndex).registerA register: Int,
atIndex: Int,
) = assertEquals(
register,
getInstruction<OneRegisterInstruction>(atIndex).registerA,
) )
private fun MutableMethod.assertRegisterIs(register: Int, atIndex: Int) = private fun MutableMethod.assertRegisterIs(
implementation!!.assertRegisterIs(register, atIndex) register: Int,
atIndex: Int,
) = implementation!!.assertRegisterIs(register, atIndex)
private fun getTestInstructions(range: IntRange) = range.map { TestInstruction(it) } private fun getTestInstructions(range: IntRange) = range.map { TestInstruction(it) }
private fun getTestSmaliInstruction(register: Int) = "const/16 v$register, 0" private fun getTestSmaliInstruction(register: Int) = "const/16 v$register, 0"
private fun getTestSmaliInstructions(range: IntRange) = range.joinToString("\n") { private fun getTestSmaliInstructions(range: IntRange) =
getTestSmaliInstruction(it) range.joinToString("\n") {
} getTestSmaliInstruction(it)
}
// endregion // endregion

View File

@ -67,8 +67,7 @@ internal class PatchOptionsTest {
} }
@Test @Test
fun `should allow setting custom value`() = fun `should allow setting custom value`() = assertDoesNotThrow { OptionsTestPatch.stringOptionWithChoices = "unknown" }
assertDoesNotThrow { OptionsTestPatch.stringOptionWithChoices = "unknown" }
@Test @Test
fun `should allow resetting value`() = assertDoesNotThrow { OptionsTestPatch.stringOptionWithChoices = null } fun `should allow resetting value`() = assertDoesNotThrow { OptionsTestPatch.stringOptionWithChoices = null }
@ -86,41 +85,41 @@ internal class PatchOptionsTest {
} }
@Test @Test
fun `option types should be known`() = fun `option types should be known`() = assertTrue(OptionsTestPatch.options["array"].valueType == "StringArray")
assertTrue(OptionsTestPatch.options["array"].valueType == "StringArray")
@Test @Test
fun `getting default value should work`() = fun `getting default value should work`() = assertDoesNotThrow { assertNull(OptionsTestPatch.resettableOption.default) }
assertDoesNotThrow { assertNull(OptionsTestPatch.resettableOption.default) }
private object OptionsTestPatch : BytecodePatch() { private object OptionsTestPatch : BytecodePatch() {
var booleanOption by booleanPatchOption( var booleanOption by booleanPatchOption(
"bool", "bool",
true true,
) )
var requiredStringOption by stringPatchOption( var requiredStringOption by stringPatchOption(
"required", "required",
"default", "default",
required = true required = true,
)
var stringArrayOption = stringArrayPatchOption(
"array",
arrayOf("1", "2")
) )
var stringArrayOption =
stringArrayPatchOption(
"array",
arrayOf("1", "2"),
)
var stringOptionWithChoices by stringPatchOption( var stringOptionWithChoices by stringPatchOption(
"choices", "choices",
"value", "value",
values = mapOf("Valid option value" to "valid") values = mapOf("Valid option value" to "valid"),
) )
var validatedOption by stringPatchOption( var validatedOption by stringPatchOption(
"validated", "validated",
"default" "default",
) { it == "valid" } ) { it == "valid" }
var resettableOption = stringPatchOption( var resettableOption =
"resettable", null, stringPatchOption(
required = true "resettable",
) null,
required = true,
)
override fun execute(context: BytecodeContext) {} override fun execute(context: BytecodeContext) {}
} }

View File

@ -32,7 +32,7 @@ import com.google.common.collect.ImmutableList
name = "Example bytecode patch", name = "Example bytecode patch",
description = "Example demonstration of a bytecode patch.", description = "Example demonstration of a bytecode patch.",
dependencies = [ExampleResourcePatch::class], dependencies = [ExampleResourcePatch::class],
compatiblePackages = [CompatiblePackage("com.example.examplePackage", arrayOf("0.0.1", "0.0.2"))] compatiblePackages = [CompatiblePackage("com.example.examplePackage", arrayOf("0.0.1", "0.0.2"))],
) )
object ExampleBytecodePatch : BytecodePatch(setOf(ExampleFingerprint)) { object ExampleBytecodePatch : BytecodePatch(setOf(ExampleFingerprint)) {
// Entry point of a patch. Supplied fingerprints are resolved at this point. // Entry point of a patch. Supplied fingerprints are resolved at this point.
@ -60,7 +60,7 @@ object ExampleBytecodePatch : BytecodePatch(setOf(ExampleFingerprint)) {
invoke-static { }, LTestClass;->returnHello()Ljava/lang/String; invoke-static { }, LTestClass;->returnHello()Ljava/lang/String;
move-result-object v1 move-result-object v1
invoke-virtual { v0, v1 }, Ljava/io/PrintStream;->println(Ljava/lang/String;)V invoke-virtual { v0, v1 }, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
""" """,
) )
} }
@ -82,14 +82,14 @@ object ExampleBytecodePatch : BytecodePatch(setOf(ExampleFingerprint)) {
BuilderInstruction21c( BuilderInstruction21c(
Opcode.CONST_STRING, Opcode.CONST_STRING,
0, 0,
ImmutableStringReference("Hello, ReVanced! Adding bytecode.") ImmutableStringReference("Hello, ReVanced! Adding bytecode."),
), ),
BuilderInstruction11x(Opcode.RETURN_OBJECT, 0) BuilderInstruction11x(Opcode.RETURN_OBJECT, 0),
), ),
null, null,
null null,
) ),
).toMutable() ).toMutable(),
) )
// Add a field in the main class. // Add a field in the main class.
@ -105,12 +105,12 @@ object ExampleBytecodePatch : BytecodePatch(setOf(ExampleFingerprint)) {
ImmutableFieldReference( ImmutableFieldReference(
"Ljava/lang/System;", "Ljava/lang/System;",
"out", "out",
"Ljava/io/PrintStream;" "Ljava/io/PrintStream;",
) ),
), ),
null, null,
null null,
).toMutable() ).toMutable(),
) )
} }
} ?: throw PatchException("Fingerprint failed to resolve.") } ?: throw PatchException("Fingerprint failed to resolve.")
@ -121,7 +121,10 @@ object ExampleBytecodePatch : BytecodePatch(setOf(ExampleFingerprint)) {
* @param index The index of the instruction to replace. * @param index The index of the instruction to replace.
* @param string The replacement string. * @param string The replacement string.
*/ */
private fun MutableMethod.replaceStringAt(index: Int, string: String) { private fun MutableMethod.replaceStringAt(
index: Int,
string: String,
) {
val instruction = getInstruction(index) val instruction = getInstruction(index)
// Utility method of dexlib2. // Utility method of dexlib2.
@ -139,8 +142,7 @@ object ExampleBytecodePatch : BytecodePatch(setOf(ExampleFingerprint)) {
// At last, use the method replaceInstruction to replace it at the given index startIndex. // At last, use the method replaceInstruction to replace it at the given index startIndex.
replaceInstruction( replaceInstruction(
index, index,
"const-string ${strInstruction.registerA}, ${ImmutableStringReference(string)}" "const-string ${strInstruction.registerA}, ${ImmutableStringReference(string)}",
) )
} }
} }

View File

@ -1,7 +1,7 @@
package app.revanced.patcher.patch.usage package app.revanced.patcher.patch.usage
import app.revanced.patcher.extensions.or import app.revanced.patcher.extensions.or
import app.revanced.patcher.fingerprint.annotation.FuzzyPatternScanMethod
import app.revanced.patcher.fingerprint.MethodFingerprint import app.revanced.patcher.fingerprint.MethodFingerprint
import app.revanced.patcher.fingerprint.annotation.FuzzyPatternScanMethod
import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.Opcode
@ -12,9 +12,9 @@ object ExampleFingerprint : MethodFingerprint(
listOf("[L"), listOf("[L"),
listOf( listOf(
Opcode.SGET_OBJECT, Opcode.SGET_OBJECT,
null, // Matching unknown opcodes. null, // Matching unknown opcodes.
Opcode.INVOKE_STATIC, // This is intentionally wrong to test fuzzy matching. Opcode.INVOKE_STATIC, // This is intentionally wrong to test fuzzy matching.
Opcode.RETURN_VOID Opcode.RETURN_VOID,
), ),
null null,
) )

View File

@ -4,18 +4,18 @@ import app.revanced.patcher.data.ResourceContext
import app.revanced.patcher.patch.ResourcePatch import app.revanced.patcher.patch.ResourcePatch
import org.w3c.dom.Element import org.w3c.dom.Element
class ExampleResourcePatch : ResourcePatch() { class ExampleResourcePatch : ResourcePatch() {
override fun execute(context: ResourceContext) { override fun execute(context: ResourceContext) {
context.xmlEditor["AndroidManifest.xml"].use { editor -> context.xmlEditor["AndroidManifest.xml"].use { editor ->
val element = editor // regular DomFileEditor val element =
.file editor // regular DomFileEditor
.getElementsByTagName("application") .file
.item(0) as Element .getElementsByTagName("application")
.item(0) as Element
element element
.setAttribute( .setAttribute(
"exampleAttribute", "exampleAttribute",
"exampleValue" "exampleValue",
) )
} }
} }

View File

@ -33,16 +33,18 @@ internal class InlineSmaliCompilerTest {
val insnIndex = insnAmount - 2 val insnIndex = insnAmount - 2
val targetIndex = insnIndex - 1 val targetIndex = insnIndex - 1
method.addInstructions(arrayOfNulls<String>(insnAmount).also { method.addInstructions(
Arrays.fill(it, "const/4 v0, 0x0") arrayOfNulls<String>(insnAmount).also {
}.joinToString("\n")) Arrays.fill(it, "const/4 v0, 0x0")
}.joinToString("\n"),
)
method.addInstructionsWithLabels( method.addInstructionsWithLabels(
targetIndex, targetIndex,
""" """
:test :test
const/4 v0, 0x1 const/4 v0, 0x1
if-eqz v0, :test if-eqz v0, :test
""" """,
) )
val insn = method.getInstruction<BuilderInstruction21t>(insnIndex) val insn = method.getInstruction<BuilderInstruction21t>(insnIndex)
@ -59,7 +61,7 @@ internal class InlineSmaliCompilerTest {
""" """
const/4 v0, 0x1 const/4 v0, 0x1
const/4 v0, 0x0 const/4 v0, 0x0
""" """,
) )
assertEquals(labelIndex, method.newLabel(labelIndex).location.index) assertEquals(labelIndex, method.newLabel(labelIndex).location.index)
@ -71,7 +73,7 @@ internal class InlineSmaliCompilerTest {
if-eqz v0, :test if-eqz v0, :test
return-void return-void
""", """,
ExternalLabel("test", method.getInstruction(1)) ExternalLabel("test", method.getInstruction(1)),
) )
val insn = method.getInstruction<BuilderInstruction21t>(insnIndex) val insn = method.getInstruction<BuilderInstruction21t>(insnIndex)
@ -93,10 +95,13 @@ internal class InlineSmaliCompilerTest {
accessFlags, accessFlags,
emptySet(), emptySet(),
emptySet(), emptySet(),
MutableMethodImplementation(registerCount) MutableMethodImplementation(registerCount),
).toMutable() ).toMutable()
private fun instructionEquals(want: BuilderInstruction, have: BuilderInstruction) { private fun instructionEquals(
want: BuilderInstruction,
have: BuilderInstruction,
) {
assertEquals(want.opcode, have.opcode) assertEquals(want.opcode, have.opcode)
assertEquals(want.format, have.format) assertEquals(want.format, have.format)
assertEquals(want.codeUnits, have.codeUnits) assertEquals(want.codeUnits, have.codeUnits)