diff --git a/arsclib-utils/.gitignore b/arsclib-utils/.gitignore new file mode 100644 index 0000000..b63da45 --- /dev/null +++ b/arsclib-utils/.gitignore @@ -0,0 +1,42 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/arsclib-utils/build.gradle.kts b/arsclib-utils/build.gradle.kts new file mode 100644 index 0000000..1fb02d2 --- /dev/null +++ b/arsclib-utils/build.gradle.kts @@ -0,0 +1,39 @@ +plugins { + kotlin("jvm") + `maven-publish` +} + +group = "app.revanced" + +dependencies { + implementation("io.github.reandroid:ARSCLib:1.1.7") +} + +java { + withSourcesJar() +} + +kotlin { + jvmToolchain(11) +} + +publishing { + repositories { + if (System.getenv("GITHUB_ACTOR") != null) + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/revanced/revanced-patcher") + credentials { + username = System.getenv("GITHUB_ACTOR") + password = System.getenv("GITHUB_TOKEN") + } + } + else + mavenLocal() + } + publications { + register("gpr") { + from(components["java"]) + } + } +} \ No newline at end of file diff --git a/arsclib-utils/src/main/kotlin/app/revanced/arsc/ApkException.kt b/arsclib-utils/src/main/kotlin/app/revanced/arsc/ApkException.kt new file mode 100644 index 0000000..7c56bfd --- /dev/null +++ b/arsclib-utils/src/main/kotlin/app/revanced/arsc/ApkException.kt @@ -0,0 +1,42 @@ +package app.revanced.arsc + +/** + * An exception thrown when working with [Apk]s. + * + * @param message The exception message. + * @param throwable The corresponding [Throwable]. + */ +// TODO: this probably needs a better name but idk what to call it. +sealed class ApkException(message: String, throwable: Throwable? = null) : Exception(message, throwable) { + /** + * An exception when decoding resources. + * + * @param message The exception message. + * @param throwable The corresponding [Throwable]. + */ + class Decode(message: String, throwable: Throwable? = null) : ApkException(message, throwable) + + /** + * An exception when encoding resources. + * + * @param message The exception message. + * @param throwable The corresponding [Throwable]. + */ + class Encode(message: String, throwable: Throwable? = null) : ApkException(message, throwable) + + /** + * An exception thrown when a reference could not be resolved. + * + * @param ref The invalid reference. + * @param throwable The corresponding [Throwable]. + */ + class InvalidReference(ref: String, throwable: Throwable? = null) : + ApkException("Failed to resolve: $ref", throwable) { + constructor(type: String, name: String, throwable: Throwable? = null) : this("@$type/$name", throwable) + } + + /** + * An exception thrown when the [Apk] does not have a resource table, but was expected to have one. + */ + object MissingResourceTable : ApkException("Apk does not have a resource table.") +} \ No newline at end of file diff --git a/arsclib-utils/src/main/kotlin/app/revanced/arsc/archive/Archive.kt b/arsclib-utils/src/main/kotlin/app/revanced/arsc/archive/Archive.kt new file mode 100644 index 0000000..10db972 --- /dev/null +++ b/arsclib-utils/src/main/kotlin/app/revanced/arsc/archive/Archive.kt @@ -0,0 +1,140 @@ +package app.revanced.arsc.archive + +import app.revanced.arsc.ApkException +import app.revanced.arsc.logging.Logger +import app.revanced.arsc.resource.ResourceContainer +import app.revanced.arsc.resource.ResourceFile +import app.revanced.arsc.xml.LazyXMLInputSource +import com.reandroid.apk.ApkModule +import com.reandroid.archive.ByteInputSource +import com.reandroid.archive.InputSource +import com.reandroid.arsc.chunk.xml.AndroidManifestBlock +import com.reandroid.arsc.chunk.xml.ResXmlDocument +import com.reandroid.xml.XMLDocument +import java.io.File + +private fun isResXml(inputSource: InputSource) = inputSource.openStream().use { ResXmlDocument.isResXmlBlock(it) } + +/** + * A class for reading/writing files in an [ApkModule]. + * + * @param module The [ApkModule] to operate on. + */ +class Archive(private val module: ApkModule) { + lateinit var resources: ResourceContainer + + /** + * The result of a [read] operation. + * + * @param xml Whether the contents were decoded from a [ResXmlDocument]. + * @param data The contents of the file. + */ + class ReadResult(val xml: Boolean, val data: ByteArray) + + /** + * The zip archive. + */ + private val archive = module.apkArchive + + private val lockedFiles = mutableMapOf() + + /** + * Lock the [ResourceFile], preventing it from being opened again until it is unlocked. + */ + fun lock(file: ResourceFile) { + val path = file.handle.archivePath + if (lockedFiles.contains(path)) { + throw ApkException.Decode("${file.handle.virtualPath} is locked. If you are a patch developer, make sure you always close files.") + } + lockedFiles[path] = file + } + + /** + * Unlock the [ResourceFile], allowing patches to open it again. + */ + fun unlock(file: ResourceFile) { + lockedFiles.remove(file.handle.archivePath) + } + + /** + * Closes all open files and encodes all XML files to binary XML. + * + * @param logger The [Logger] of the [app.revanced.patcher.Patcher]. + */ + fun cleanup(logger: Logger?) { + lockedFiles.values.toList().forEach { + logger?.warn("${it.handle.virtualPath} was never closed!") + it.close() + } + + archive.listInputSources().filterIsInstance() + .forEach(LazyXMLInputSource::encode) + } + + /** + * Save the archive to disk. + * + * @param output The file to write the updated archive to. + */ + fun save(output: File) = module.writeApk(output) + + /** + * Read an entry from the archive. + * + * @param path The archive path to read from. + * @return A [ReadResult] containing the contents of the entry. + */ + fun read(path: String) = + archive.getInputSource(path)?.let { inputSource -> + val xml = when { + inputSource is LazyXMLInputSource -> inputSource.document + isResXml(inputSource) -> module.loadResXmlDocument( + inputSource + ).decodeToXml(resources.resourceTable.entryStore, resources.packageBlock?.id ?: 0) + + else -> null + } + + ReadResult( + xml != null, + xml?.toText()?.toByteArray() ?: inputSource.openStream().use { it.readAllBytes() }) + } + + /** + * Reads the manifest from the archive as an [AndroidManifestBlock]. + * + * @return The [AndroidManifestBlock] contained in this archive. + */ + fun readManifest(): AndroidManifestBlock = + archive.getInputSource(AndroidManifestBlock.FILE_NAME).openStream().use { AndroidManifestBlock.load(it) } + + /** + * Reads all dex files from the archive. + * + * @return A [Map] containing all the dex files. + */ + fun readDexFiles() = module.listDexFiles().associate { file -> file.name to file.openStream().use { it.readAllBytes() } } + + /** + * Write the byte array to the archive entry. + * + * @param path The archive path to read from. + * @param content The content of the file. + */ + fun writeRaw(path: String, content: ByteArray) = + archive.add(ByteInputSource(content, path)) + + /** + * Write the XML to the entry associated. + * + * @param path The archive path to read from. + * @param document The XML document to encode. + */ + fun writeXml(path: String, document: XMLDocument) = archive.add( + LazyXMLInputSource( + path, + document, + resources, + ) + ) +} \ No newline at end of file diff --git a/arsclib-utils/src/main/kotlin/app/revanced/arsc/logging/Logger.kt b/arsclib-utils/src/main/kotlin/app/revanced/arsc/logging/Logger.kt new file mode 100644 index 0000000..855a392 --- /dev/null +++ b/arsclib-utils/src/main/kotlin/app/revanced/arsc/logging/Logger.kt @@ -0,0 +1,7 @@ +package app.revanced.arsc.logging +interface Logger { + fun error(msg: String) + fun warn(msg: String) + fun info(msg: String) + fun trace(msg: String) +} \ No newline at end of file diff --git a/arsclib-utils/src/main/kotlin/app/revanced/arsc/resource/Resource.kt b/arsclib-utils/src/main/kotlin/app/revanced/arsc/resource/Resource.kt new file mode 100644 index 0000000..f33dda4 --- /dev/null +++ b/arsclib-utils/src/main/kotlin/app/revanced/arsc/resource/Resource.kt @@ -0,0 +1,166 @@ +package app.revanced.arsc.resource + +import app.revanced.arsc.ApkException +import com.reandroid.arsc.coder.ValueDecoder +import com.reandroid.arsc.coder.EncodeResult +import com.reandroid.arsc.value.Entry +import com.reandroid.arsc.value.ValueType +import com.reandroid.arsc.value.array.ArrayBag +import com.reandroid.arsc.value.array.ArrayBagItem +import com.reandroid.arsc.value.plurals.PluralsBag +import com.reandroid.arsc.value.plurals.PluralsBagItem +import com.reandroid.arsc.value.plurals.PluralsQuantity +import com.reandroid.arsc.value.style.StyleBag +import com.reandroid.arsc.value.style.StyleBagItem + +/** + * A resource value. + */ +sealed class Resource { + internal abstract fun write(entry: Entry, resources: ResourceContainer) +} + +internal val Resource.complex get() = when (this) { + is Scalar -> false + is Complex -> true +} + +/** + * A simple resource. + */ +open class Scalar internal constructor(private val valueType: ValueType, private val value: Int) : Resource() { + protected open fun data(resources: ResourceContainer) = value + + override fun write(entry: Entry, resources: ResourceContainer) { + entry.setValueAsRaw(valueType, data(resources)) + } + + internal open fun toArrayItem(resources: ResourceContainer) = ArrayBagItem.create(valueType, data(resources)) + internal open fun toStyleItem(resources: ResourceContainer) = StyleBagItem.create(valueType, data(resources)) +} + +/** + * A marker class for complex resources. + */ +sealed class Complex : Resource() + +private fun encoded(encodeResult: EncodeResult?) = encodeResult?.let { Scalar(it.valueType, it.value) } + ?: throw ApkException.Encode("Failed to encode value") + +/** + * Encode a color. + * + * @param hex The hex value of the color. + * @return The encoded [Resource]. + */ +fun color(hex: String) = encoded(ValueDecoder.encodeColor(hex)) + +/** + * Encode a dimension or fraction. + * + * @param value The dimension value such as 24dp. + * @return The encoded [Resource]. + */ +fun dimension(value: String) = encoded(ValueDecoder.encodeDimensionOrFraction(value)) + +/** + * Encode a boolean resource. + * + * @param value The boolean. + * @return The encoded [Resource]. + */ +fun boolean(value: Boolean) = Scalar(ValueType.INT_BOOLEAN, if (value) -Int.MAX_VALUE else 0) + +/** + * Encode a float. + * + * @param n The number to encode. + * @return The encoded [Resource]. + */ +fun float(n: Float) = Scalar(ValueType.FLOAT, n.toBits()) + +/** + * Create an integer [Resource]. + * + * @param n The number to encode. + * @return The integer [Resource]. + */ +fun integer(n: Int) = Scalar(ValueType.INT_DEC, n) + +/** + * Create a reference [Resource]. + * + * @param resourceId The target resource. + * @return The reference resource. + */ +fun reference(resourceId: Int) = Scalar(ValueType.REFERENCE, resourceId) + +/** + * Resolve and create a reference [Resource]. + * + * @see reference + * @param ref The reference string to resolve. + * @param resourceTable The resource table to resolve the reference with. + * @return The reference resource. + */ +fun reference(resourceTable: ResourceTable, ref: String) = reference(resourceTable.resolve(ref)) + +/** + * An array [Resource]. + * + * @param elements The elements of the array. + */ +class Array(private val elements: Collection) : Complex() { + override fun write(entry: Entry, resources: ResourceContainer) { + ArrayBag.create(entry).addAll(elements.map { it.toArrayItem(resources) }) + } +} + +/** + * A style resource. + * + * @param elements The attributes to override. + * @param parent A reference to the parent style. + */ +class Style(private val elements: Map, private val parent: String? = null) : Complex() { + override fun write(entry: Entry, resources: ResourceContainer) { + val resTable = resources.resourceTable + val style = StyleBag.create(entry) + parent?.let { + style.parentId = resTable.resolve(parent) + } + + style.putAll( + elements.asIterable().associate { + StyleBag.resolve(resTable.encodeMaterials, it.key) to it.value.toStyleItem(resources) + }) + } +} + +/** + * A quantity string [Resource]. + * + * @param elements A map of the quantity to the corresponding string. + */ +class Plurals(private val elements: Map) : Complex() { + override fun write(entry: Entry, resources: ResourceContainer) { + val plurals = PluralsBag.create(entry) + + plurals.putAll(elements.asIterable().associate { (k, v) -> + PluralsQuantity.value(k) to PluralsBagItem.string(resources.getOrCreateTableString(v)) + }) + } +} + +/** + * A string [Resource]. + * + * @param value The string value. + */ +class StringResource(val value: String) : Scalar(ValueType.STRING, 0) { + private fun tableString(resources: ResourceContainer) = resources.getOrCreateTableString(value) + + override fun data(resources: ResourceContainer) = tableString(resources).index + override fun toArrayItem(resources: ResourceContainer) = ArrayBagItem.string(tableString(resources)) + override fun toStyleItem(resources: ResourceContainer) = StyleBagItem.string(tableString(resources)) +} \ No newline at end of file diff --git a/arsclib-utils/src/main/kotlin/app/revanced/arsc/resource/ResourceContainer.kt b/arsclib-utils/src/main/kotlin/app/revanced/arsc/resource/ResourceContainer.kt new file mode 100644 index 0000000..ab5aa8d --- /dev/null +++ b/arsclib-utils/src/main/kotlin/app/revanced/arsc/resource/ResourceContainer.kt @@ -0,0 +1,139 @@ +package app.revanced.arsc.resource + +import app.revanced.arsc.ApkException +import app.revanced.arsc.archive.Archive +import com.reandroid.apk.xmlencoder.EncodeUtil +import com.reandroid.arsc.chunk.TableBlock +import com.reandroid.arsc.value.Entry +import com.reandroid.arsc.value.ResConfig +import java.io.File + +/** + * A high-level API for modifying the resources contained in an Apk. + * + * @param tableBlock The resources.arsc file of this Apk. + */ +class ResourceContainer(private val archive: Archive, internal val tableBlock: TableBlock?) { + internal val packageBlock = tableBlock?.pickOne() // Pick the main PackageBlock. + + internal lateinit var resourceTable: ResourceTable + + init { + archive.resources = this + } + + private fun expectPackageBlock() = packageBlock ?: throw ApkException.MissingResourceTable + + internal fun getOrCreateTableString(value: String) = + tableBlock?.stringPool?.getOrCreate(value) ?: throw ApkException.MissingResourceTable + + /** + * Set the value of the [Entry] to the one specified. + * + * @param value The new value. + */ + private fun Entry.setTo(value: Resource) { + val savedRef = specReference + ensureComplex(value.complex) + if (savedRef != 0) { + // Preserve the entry name by restoring the previous spec block reference (if present). + specReference = savedRef + } + + value.write(this, this@ResourceContainer) + resourceTable.registerChanged(this) + } + + /** + * Retrieve an [Entry] from the resource table. + * + * @param type The resource type. + * @param name The resource name. + * @param qualifiers The variant to use. + */ + private fun getEntry(type: String, name: String, qualifiers: String?): Entry? { + val resourceId = try { + resourceTable.resolve("@$type/$name") + } catch (_: ApkException.InvalidReference) { + return null + } + + val config = ResConfig.parse(qualifiers) + return tableBlock?.resolveReference(resourceId)?.singleOrNull { it.resConfig == config } + } + + /** + * Create a [ResourceFile.Handle] that can be used to open a [ResourceFile]. + * This may involve looking it up in the resource table to find the actual location in the archive. + * + * @param resPath The path of the resource. + */ + private fun createHandle(resPath: String): ResourceFile.Handle { + if (resPath.startsWith("res/values")) throw ApkException.Decode("Decoding the resource table as a file is not supported") + + var callback = {} + var archivePath = resPath + + if (tableBlock != null && resPath.startsWith("res/") && resPath.count { it == '/' } == 2) { + val file = File(resPath) + + val qualifiers = EncodeUtil.getQualifiersFromResFile(file) + val type = EncodeUtil.getTypeNameFromResFile(file) + val name = file.nameWithoutExtension + + // The resource file names that the app developers used may have been minified, so we have to resolve it with the resource table. + // Example: res/drawable-hdpi/icon.png -> res/4a.png + val resolvedPath = getEntry(type, name, qualifiers)?.resValue?.valueAsString + + if (resolvedPath != null) { + archivePath = resolvedPath + } else { + // An entry for this specific resource file was not found in the resource table, so we have to register it after we save. + callback = { set(type, name, StringResource(archivePath), qualifiers) } + } + } + + return ResourceFile.Handle(resPath, archivePath, callback) + } + + /** + * Create or update an Android resource. + * + * @param type The resource type. + * @param name The name of the resource. + * @param value The resource data. + * @param configuration The resource configuration. + */ + fun set(type: String, name: String, value: Resource, configuration: String? = null) = + expectPackageBlock().getOrCreate(configuration, type, name).also { it.setTo(value) }.resourceId + + /** + * Create or update multiple resources in an ARSC type block. + * + * @param type The resource type. + * @param map A map of resource names to the corresponding value. + * @param configuration The resource configuration. + */ + fun setGroup(type: String, map: Map, configuration: String? = null) { + expectPackageBlock().getOrCreateSpecTypePair(type).getOrCreateTypeBlock(configuration).apply { + map.forEach { (name, value) -> getOrCreateEntry(name).setTo(value) } + } + } + + /** + * Open a resource file, creating it if the file does not exist. + * + * @param path The resource file path. + * @return The corresponding [ResourceFile], + */ + fun openFile(path: String) = ResourceFile( + createHandle(path), archive + ) + + /** + * Update the [PackageBlock] name to match the manifest. + */ + fun refreshPackageName() { + packageBlock?.name = archive.readManifest().packageName + } +} \ No newline at end of file diff --git a/arsclib-utils/src/main/kotlin/app/revanced/arsc/resource/ResourceFile.kt b/arsclib-utils/src/main/kotlin/app/revanced/arsc/resource/ResourceFile.kt new file mode 100644 index 0000000..becaa1c --- /dev/null +++ b/arsclib-utils/src/main/kotlin/app/revanced/arsc/resource/ResourceFile.kt @@ -0,0 +1,92 @@ +package app.revanced.arsc.resource + +import app.revanced.arsc.ApkException +import app.revanced.arsc.archive.Archive +import com.reandroid.xml.XMLDocument +import com.reandroid.xml.XMLException +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.Closeable +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream + +/** + * A resource file inside an [Apk]. + */ +class ResourceFile private constructor( + internal val handle: Handle, + private val archive: Archive, + readResult: Archive.ReadResult? +) : + Closeable { + + /** + * @param virtualPath The resource file path (res/drawable-hdpi/icon.png) + * @param archivePath The actual file path in the archive (res/4a.png) + * @param close An action to perform when the file associated with this handle is closed + */ + internal data class Handle(val virtualPath: String, val archivePath: String, val close: () -> Unit) + + private var changed = false + private val xml = readResult?.xml ?: handle.virtualPath.endsWith(".xml") + + /** + * @param handle The [Handle] associated with this file + * @param archive The [Archive] that the file resides in + */ + internal constructor(handle: Handle, archive: Archive) : this( + handle, + archive, + try { + archive.read(handle.archivePath) + } catch (e: XMLException) { + throw ApkException.Decode("Failed to decode XML while reading ${handle.virtualPath}", e) + } catch (e: IOException) { + throw ApkException.Decode("Could not read ${handle.virtualPath}", e) + } + ) + + var contents = readResult?.data ?: ByteArray(0) + set(value) { + changed = true + field = value + } + + val exists = readResult != null + + override fun toString() = handle.virtualPath + + init { + archive.lock(this) + } + + override fun close() { + if (changed) { + val path = handle.archivePath + if (xml) archive.writeXml( + path, + try { + XMLDocument.load(String(contents)) + } catch (e: XMLException) { + throw ApkException.Encode("Failed to parse XML while writing ${handle.virtualPath}", e) + } + ) else archive.writeRaw(path, contents) + } + handle.close() + archive.unlock(this) + } + + companion object { + const val DEFAULT_BUFFER_SIZE = 4096 + } + + fun inputStream(): InputStream = ByteArrayInputStream(contents) + fun outputStream(bufferSize: Int = DEFAULT_BUFFER_SIZE): OutputStream = + object : ByteArrayOutputStream(bufferSize) { + override fun close() { + this@ResourceFile.contents = if (buf.size > count) buf.copyOf(count) else buf + super.close() + } + } +} \ No newline at end of file diff --git a/arsclib-utils/src/main/kotlin/app/revanced/arsc/resource/ResourceTable.kt b/arsclib-utils/src/main/kotlin/app/revanced/arsc/resource/ResourceTable.kt new file mode 100644 index 0000000..de320bc --- /dev/null +++ b/arsclib-utils/src/main/kotlin/app/revanced/arsc/resource/ResourceTable.kt @@ -0,0 +1,100 @@ +package app.revanced.arsc.resource + +import app.revanced.arsc.ApkException +import com.reandroid.apk.xmlencoder.EncodeException +import com.reandroid.apk.xmlencoder.EncodeMaterials +import com.reandroid.arsc.util.FrameworkTable +import com.reandroid.arsc.value.Entry +import com.reandroid.common.TableEntryStore + +/** + * A high-level API for resolving resources in the resource table, which spans the entire [ApkBundle]. + */ +class ResourceTable(base: ResourceContainer, all: Sequence) { + private val packageName = base.packageBlock!!.name + + /** + * A [TableEntryStore] used to decode XML. + */ + internal val entryStore = TableEntryStore() + + /** + * The [EncodeMaterials] to use for resolving resources and encoding XML. + */ + internal val encodeMaterials: EncodeMaterials = object : EncodeMaterials() { + /* + Our implementation is more efficient because it does not have to loop through every single entry group + when the resource id cannot be found in the TableIdentifier, which does not update when you create a new resource. + It also looks at the entire table instead of just the current package. + */ + override fun resolveLocalResourceId(type: String, name: String) = resolveLocal(type, name) + } + + /** + * The resource mappings which are generated when the [ApkBundle] is created. + */ + private val tableIdentifier = encodeMaterials.tableIdentifier + + /** + * A table of all the resources that have been changed or added. + */ + private val modifiedResources = HashMap>() + + + /** + * Resolve a resource id for the specified resource. + * Cannot resolve resources from the android framework. + * + * @param type The type of the resource. + * @param name The name of the resource. + * @return The id of the resource. + */ + fun resolveLocal(type: String, name: String) = + modifiedResources[type]?.get(name) + ?: tableIdentifier.get(packageName, type, name)?.resourceId + ?: throw ApkException.InvalidReference( + type, + name + ) + + /** + * Resolve a resource id for the specified resource. + * + * @param reference The resource reference string. + * @return The id of the resource. + */ + fun resolve(reference: String) = try { + encodeMaterials.resolveReference(reference) + } catch (e: EncodeException) { + throw ApkException.InvalidReference(reference, e) + } + + /** + * Notify the [ResourceTable] that an [Entry] has been created or modified. + */ + internal fun registerChanged(entry: Entry) { + modifiedResources.getOrPut(entry.typeName, ::HashMap)[entry.name] = entry.resourceId + } + + init { + all.forEach { + it.tableBlock?.let { table -> + entryStore.add(table) + tableIdentifier.load(table) + } + + it.resourceTable = this + } + + base.also { + encodeMaterials.currentPackage = it.packageBlock + + it.tableBlock!!.frameWorks.forEach { fw -> + if (fw is FrameworkTable) { + entryStore.add(fw) + encodeMaterials.addFramework(fw) + } + } + } + } +} \ No newline at end of file diff --git a/arsclib-utils/src/main/kotlin/app/revanced/arsc/xml/LazyXMLInputSource.kt b/arsclib-utils/src/main/kotlin/app/revanced/arsc/xml/LazyXMLInputSource.kt new file mode 100644 index 0000000..78f2705 --- /dev/null +++ b/arsclib-utils/src/main/kotlin/app/revanced/arsc/xml/LazyXMLInputSource.kt @@ -0,0 +1,63 @@ +package app.revanced.arsc.xml + +import app.revanced.arsc.ApkException +import app.revanced.arsc.resource.ResourceContainer +import app.revanced.arsc.resource.boolean +import com.reandroid.apk.xmlencoder.EncodeException +import com.reandroid.apk.xmlencoder.XMLEncodeSource +import com.reandroid.arsc.chunk.xml.ResXmlDocument +import com.reandroid.xml.XMLDocument +import com.reandroid.xml.XMLElement +import com.reandroid.xml.source.XMLDocumentSource + +/** + * Archive input source that lazily encodes the [XMLDocument] when you read from it. + * + * @param name The file name of this input source. + * @param document The [XMLDocument] to encode. + * @param resources The [ResourceContainer] to use for encoding. + */ +internal class LazyXMLInputSource( + name: String, + val document: XMLDocument, + private val resources: ResourceContainer +) : XMLEncodeSource(resources.resourceTable.encodeMaterials, XMLDocumentSource(name, document)) { + private var ready = false + + private fun XMLElement.registerIds() { + listAttributes().forEach { attr -> + if (attr.value.startsWith("@+id/")) { + val name = attr.value.split('/').last() + resources.set("id", name, boolean(false)) + attr.value = "@id/$name" + } + } + + listChildElements().forEach { it.registerIds() } + } + + override fun getResXmlBlock(): ResXmlDocument { + if (!ready) { + throw ApkException.Encode("$name has not been encoded yet") + } + + return super.getResXmlBlock() + } + + /** + * Encode the [XMLDocument] associated with this input source. + */ + fun encode() { + // Handle all @+id/id_name references in the document. + document.documentElement.registerIds() + + ready = true + + // This will call XMLEncodeSource.getResXmlBlock(), which will encode the document if it has not already been encoded. + try { + resXmlBlock + } catch (e: EncodeException) { + throw EncodeException("Failed to encode $name", e) + } + } +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index cb8c52f..a2117c4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,71 +1,3 @@ plugins { - kotlin("jvm") version "1.8.20" - `maven-publish` -} - -group = "app.revanced" - -val githubUsername: String = project.findProperty("gpr.user") as? String ?: System.getenv("GITHUB_ACTOR") -val githubPassword: String = project.findProperty("gpr.key") as? String ?: System.getenv("GITHUB_TOKEN") - -repositories { - mavenCentral() - maven { - url = uri("https://maven.pkg.github.com/revanced/multidexlib2") - credentials { - username = githubUsername - password = githubPassword - } - } -} - -dependencies { - implementation("xpp3:xpp3:1.1.4c") - implementation("app.revanced:smali:2.5.3-a3836654") - implementation("app.revanced:multidexlib2:2.5.3-a3836654") - implementation("app.revanced:apktool-lib:2.7.0") - - implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.20-RC") - testImplementation("org.jetbrains.kotlin:kotlin-test:1.8.20-RC") -} - -tasks { - test { - useJUnitPlatform() - testLogging { - events("PASSED", "SKIPPED", "FAILED") - } - } - processResources { - expand("projectVersion" to project.version) - } -} - -java { - withSourcesJar() -} - -kotlin { - jvmToolchain(11) -} - -publishing { - repositories { - if (System.getenv("GITHUB_ACTOR") != null) - maven { - name = "GitHubPackages" - url = uri("https://maven.pkg.github.com/revanced/revanced-patcher") - credentials { - username = System.getenv("GITHUB_ACTOR") - password = System.getenv("GITHUB_TOKEN") - } - } - else - mavenLocal() - } - publications { - register("gpr") { - from(components["java"]) - } - } + kotlin("jvm") version "1.8.20" apply false } diff --git a/revanced-patcher/build.gradle.kts b/revanced-patcher/build.gradle.kts new file mode 100644 index 0000000..8498ca5 --- /dev/null +++ b/revanced-patcher/build.gradle.kts @@ -0,0 +1,61 @@ +plugins { + kotlin("jvm") + `maven-publish` +} + +group = "app.revanced" + +dependencies { + implementation("xpp3:xpp3:1.1.4c") + implementation("app.revanced:smali:2.5.3-a3836654") + implementation("app.revanced:multidexlib2:2.5.3-a3836654") + implementation("io.github.reandroid:ARSCLib:1.1.7") + implementation(project(":arsclib-utils")) + + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1") + implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.20-RC") + testImplementation("org.jetbrains.kotlin:kotlin-test:1.8.20-RC") + + compileOnly("com.google.android:android:4.1.1.4") +} + +tasks { + test { + useJUnitPlatform() + testLogging { + events("PASSED", "SKIPPED", "FAILED") + } + } + processResources { + expand("projectVersion" to project.version) + } +} + +java { + withSourcesJar() +} + +kotlin { + jvmToolchain(11) +} + +publishing { + repositories { + if (System.getenv("GITHUB_ACTOR") != null) + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/revanced/revanced-patcher") + credentials { + username = System.getenv("GITHUB_ACTOR") + password = System.getenv("GITHUB_TOKEN") + } + } + else + mavenLocal() + } + publications { + register("gpr") { + from(components["java"]) + } + } +} diff --git a/revanced-patcher/settings.gradle.kts b/revanced-patcher/settings.gradle.kts new file mode 100644 index 0000000..b4d1a12 --- /dev/null +++ b/revanced-patcher/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "revanced-patcher" diff --git a/revanced-patcher/src/main/kotlin/app/revanced/patcher/Context.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/Context.kt new file mode 100644 index 0000000..909bee9 --- /dev/null +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/Context.kt @@ -0,0 +1,113 @@ +package app.revanced.patcher + +import app.revanced.arsc.resource.ResourceContainer +import app.revanced.patcher.apk.Apk +import app.revanced.patcher.apk.ApkBundle +import app.revanced.arsc.resource.ResourceFile +import app.revanced.patcher.util.method.MethodWalker +import org.jf.dexlib2.iface.Method +import org.w3c.dom.Document +import java.io.Closeable +import java.io.InputStream +import java.io.StringWriter +import javax.xml.parsers.DocumentBuilderFactory +import javax.xml.transform.TransformerFactory +import javax.xml.transform.dom.DOMSource +import javax.xml.transform.stream.StreamResult + +/** + * A common class to constrain [Context] to [BytecodeContext] and [ResourceContext]. + * @param apkBundle The [ApkBundle] for this context. + */ +sealed class Context(val apkBundle: ApkBundle) + +/** + * A context for the bytecode of an [Apk.Base] file. + * + * @param apkBundle The [ApkBundle] for this context. + */ +class BytecodeContext internal constructor(apkBundle: ApkBundle) : Context(apkBundle) { + /** + * The list of classes. + */ + val classes = apkBundle.base.bytecodeData.classes + + /** + * Create a [MethodWalker] instance for the current [BytecodeContext]. + * + * @param startMethod The method to start at. + * @return A [MethodWalker] instance. + */ + fun traceMethodCalls(startMethod: Method) = MethodWalker(this, startMethod) +} + +/** + * A context for [Apk] file resources. + * + * @param apkBundle the [ApkBundle] for this context. + */ +class ResourceContext internal constructor(apkBundle: ApkBundle) : Context(apkBundle) { + + /** + * Open an [DomFileEditor] for a given DOM file. + * + * @param inputStream The input stream to read the DOM file from. + * @return A [DomFileEditor] instance. + */ + fun openXmlFile(inputStream: InputStream) = DomFileEditor(inputStream) +} + + +/** + * Open a [DomFileEditor] for a resource file in the archive. + * + * @see [ResourceContainer.openFile] + * @param path The resource file path. + * @return A [DomFileEditor]. + */ +fun ResourceContainer.openXmlFile(path: String) = DomFileEditor(openFile(path)) + +/** + * Wrapper for a file that can be edited as a dom document. + * + * @param inputStream the input stream to read the xml file from. + * @param onSave A callback that will be called when the editor is closed to save the file. + */ +class DomFileEditor internal constructor( + private val inputStream: InputStream, + private val onSave: ((String) -> Unit)? = null +) : Closeable { + private var closed: Boolean = false + + /** + * The document of the xml file. + */ + val file: Document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputStream) + .also(Document::normalize) + + internal constructor(file: ResourceFile) : this( + file.inputStream(), + { + file.contents = it.toByteArray() + file.close() + } + ) + + /** + * Closes the editor and writes back to the file. + */ + override fun close() { + if (closed) return + + inputStream.close() + + onSave?.let { callback -> + // Save the updated file. + val writer = StringWriter() + TransformerFactory.newInstance().newTransformer().transform(DOMSource(file), StreamResult(writer)) + callback(writer.toString()) + } + + closed = true + } +} \ No newline at end of file diff --git a/revanced-patcher/src/main/kotlin/app/revanced/patcher/Patcher.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/Patcher.kt new file mode 100644 index 0000000..f8da01e --- /dev/null +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/Patcher.kt @@ -0,0 +1,220 @@ +package app.revanced.patcher + +import app.revanced.patcher.apk.Apk +import app.revanced.patcher.extensions.PatchExtensions.dependencies +import app.revanced.patcher.extensions.PatchExtensions.patchName +import app.revanced.patcher.extensions.PatchExtensions.requiresIntegrations +import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint +import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint.Companion.resolveUsingLookupMap +import app.revanced.patcher.patch.* +import app.revanced.patcher.util.VersionReader +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import lanchon.multidexlib2.BasicDexFileNamer +import java.io.Closeable +import java.io.File +import java.util.function.Function + +typealias ExecutedPatchResults = Flow> + +/** + * The ReVanced Patcher. + * @param options The options for the patcher. + * @param patches The patches to use. + * @param integrations The integrations to merge if necessary. Must be dex files or dex file container such as ZIP, APK or DEX files. + */ +class Patcher(private val options: PatcherOptions, patches: Iterable, integrations: Iterable) : + Function { + private val context = PatcherContext(options, patches.toList(), integrations) + private val logger = options.logger + + companion object { + /** + * The version of the ReVanced Patcher. + */ + @JvmStatic + val version = VersionReader.read() + + @Suppress("SpellCheckingInspection") + internal val dexFileNamer = BasicDexFileNamer() + } + + init { + /** + * Returns true if at least one patches or its dependencies matches the given predicate. + */ + fun PatchClass.anyRecursively(predicate: (PatchClass) -> Boolean): Boolean = + predicate(this) || dependencies?.any { it.java.anyRecursively(predicate) } == true + + // Determine if merging integrations is required. + for (patch in context.patches) { + if (patch.anyRecursively { it.requiresIntegrations }) { + context.integrations.merge = true + break + } + } + } + + /** + * Execute the patcher. + * + * @param stopOnError If true, the patches will stop on the first error. + * @return A pair of the name of the [Patch] and a [PatchException] if it failed. + */ + override fun apply(stopOnError: Boolean) = flow { + /** + * Execute a [Patch] and its dependencies recursively. + * + * @param patchClass The [Patch] to execute. + * @param executedPatches A map of [Patch]es paired to a boolean indicating their success, to prevent infinite recursion. + */ + suspend fun executePatch( + patchClass: PatchClass, + executedPatches: HashMap + ) { + val patchName = patchClass.patchName + + // If the patch has already executed silently skip it. + if (executedPatches.contains(patchName)) { + if (!executedPatches[patchName]!!.success) + throw PatchException("'$patchName' did not succeed previously") + + logger.trace("Skipping '$patchName' because it has already been executed") + + return + } + + // Recursively execute all dependency patches. + patchClass.dependencies?.forEach { dependencyClass -> + val dependency = dependencyClass.java + + try { + executePatch(dependency, executedPatches) + } catch (throwable: Throwable) { + throw PatchException( + "'$patchName' depends on '${dependency.patchName}' " + + "but the following exception was raised: ${throwable.cause?.stackTraceToString() ?: throwable.message}", + throwable + ) + } + } + + val isResourcePatch = ResourcePatch::class.java.isAssignableFrom(patchClass) + val patchInstance = patchClass.getDeclaredConstructor().newInstance() + + // TODO: implement this in a more polymorphic way. + val patchContext = if (isResourcePatch) { + context.resourceContext + } else { + context.bytecodeContext.apply { + val bytecodePatch = patchInstance as BytecodePatch + bytecodePatch.fingerprints?.resolveUsingLookupMap(context.bytecodeContext) + } + } + + logger.trace("Executing '$patchName' of type: ${if (isResourcePatch) "resource" else "bytecode"}") + + var success = false + try { + patchInstance.execute(patchContext) + + success = true + } catch (patchException: PatchException) { + throw patchException + } catch (throwable: Throwable) { + throw PatchException("Unhandled patch exception: ${throwable.message}", throwable) + } finally { + executedPatches[patchName] = ExecutedPatch(patchInstance, success) + } + } + + if (context.integrations.merge) context.integrations.merge(logger) + + logger.trace("Initialize lookup maps for method MethodFingerprint resolution") + + MethodFingerprint.initializeFingerprintResolutionLookupMaps(context.bytecodeContext) + + logger.info("Executing patches") + + // Key is patch name. + LinkedHashMap().apply { + context.patches.forEach { patch -> + var exception: PatchException? = null + + try { + executePatch(patch, this) + } catch (patchException: PatchException) { + exception = patchException + } + + emit(patch.patchName to exception) + + if (stopOnError && exception != null) return@flow + } + }.let { + it.values + .filter(ExecutedPatch::success) + .map(ExecutedPatch::patchInstance) + .filterIsInstance(Closeable::class.java) + .asReversed().forEach { patch -> + try { + patch.close() + } catch (throwable: Throwable) { + val patchException = + if (throwable is PatchException) throwable + else PatchException(throwable) + + val patchName = (patch as Patch).javaClass.patchName + + logger.error("Failed to close '$patchName': ${patchException.stackTraceToString()}") + + emit(patchName to patchException) + + // This is not failsafe. If a patch throws an exception while closing, + // the other patches that depend on it may fail. + if (stopOnError) return@flow + } + } + } + + MethodFingerprint.clearFingerprintResolutionLookupMaps() + } + + /** + * Finish patching all [Apk]s. + * + * @return The [PatcherResult] of the [Patcher]. + */ + fun finish(): PatcherResult { + val patchResults = buildList { + logger.info("Processing patched apks") + options.apkBundle.cleanup(options).forEach { result -> + if (result.exception != null) { + logger.error("Got exception while processing ${result.apk}: ${result.exception.stackTraceToString()}") + return@forEach + } + + val patch = result.let { + when (it.apk) { + is Apk.Base -> PatcherResult.Patch.Base(it.apk) + is Apk.Split -> PatcherResult.Patch.Split(it.apk) + } + } + + add(patch) + + logger.info("Patched ${result.apk}") + } + } + + return PatcherResult(patchResults) + } +} + +/** + * A result of executing a [Patch]. + * + * @param patchInstance The instance of the [Patch] that was executed. + * @param success The result of the [Patch]. + */ +internal data class ExecutedPatch(val patchInstance: Patch, val success: Boolean) \ No newline at end of file diff --git a/revanced-patcher/src/main/kotlin/app/revanced/patcher/PatcherContext.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/PatcherContext.kt new file mode 100644 index 0000000..befd8c4 --- /dev/null +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/PatcherContext.kt @@ -0,0 +1,55 @@ +package app.revanced.patcher + +import app.revanced.patcher.logging.Logger +import app.revanced.patcher.patch.PatchClass +import app.revanced.patcher.util.ClassMerger.merge +import lanchon.multidexlib2.MultiDexIO +import java.io.File + +class PatcherContext( + options: PatcherOptions, + internal val patches: List, + integrations: Iterable +) { + internal val integrations = Integrations(this, integrations) + internal val bytecodeContext = BytecodeContext(options.apkBundle) + internal val resourceContext = ResourceContext(options.apkBundle) + + internal class Integrations(val context: PatcherContext, private val dexContainers: Iterable) { + var merge = false + + /** + * Merge integrations. + * @param logger A logger. + */ + fun merge(logger: Logger) { + context.bytecodeContext.classes.apply { + for (integrations in dexContainers) { + logger.info("Merging $integrations") + + for (classDef in MultiDexIO.readDexFile(true, integrations, Patcher.dexFileNamer, null, null).classes) { + val type = classDef.type + + val existingClassIndex = this.indexOfFirst { it.type == type } + if (existingClassIndex == -1) { + logger.trace("Merging type $type") + add(classDef) + continue + } + + + logger.trace("Type $type exists. Adding missing methods and fields.") + + get(existingClassIndex).apply { + merge(classDef, context.bytecodeContext, logger).let { mergedClass -> + if (mergedClass !== this) // referential equality check + set(existingClassIndex, mergedClass) + } + } + + } + } + } + } + } +} \ No newline at end of file diff --git a/revanced-patcher/src/main/kotlin/app/revanced/patcher/PatcherOptions.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/PatcherOptions.kt new file mode 100644 index 0000000..482f543 --- /dev/null +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/PatcherOptions.kt @@ -0,0 +1,14 @@ +package app.revanced.patcher + +import app.revanced.patcher.apk.ApkBundle +import app.revanced.patcher.logging.Logger + +/** + * Options for the [Patcher]. + * @param apkBundle The [ApkBundle]. + * @param logger Custom logger implementation for the [Patcher]. + */ +class PatcherOptions( + internal val apkBundle: ApkBundle, + internal val logger: Logger = Logger.Nop +) \ No newline at end of file diff --git a/revanced-patcher/src/main/kotlin/app/revanced/patcher/PatcherResult.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/PatcherResult.kt new file mode 100644 index 0000000..4d02618 --- /dev/null +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/PatcherResult.kt @@ -0,0 +1,33 @@ +package app.revanced.patcher + +import app.revanced.patcher.apk.Apk +import java.io.File + +/** + * The result of a patcher. + * @param apkFiles The patched [Apk] files. + */ +data class PatcherResult(val apkFiles: List) { + + /** + * The result of a patch. + * + * @param apk The patched [Apk] file. + */ + sealed class Patch(val apk: Apk) { + + /** + * The result of a patch of an [Apk.Split] file. + * + * @param apk The patched [Apk.Split] file. + */ + class Split(apk: Apk.Split) : Patch(apk) + + /** + * The result of a patch of an [Apk.Split] file. + * + * @param apk The patched [Apk.Base] file. + */ + class Base(apk: Apk.Base) : Patch(apk) + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/annotation/CompatibilityAnnotation.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/annotation/CompatibilityAnnotation.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/annotation/CompatibilityAnnotation.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/annotation/CompatibilityAnnotation.kt diff --git a/src/main/kotlin/app/revanced/patcher/annotation/MetadataAnnotation.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/annotation/MetadataAnnotation.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/annotation/MetadataAnnotation.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/annotation/MetadataAnnotation.kt diff --git a/revanced-patcher/src/main/kotlin/app/revanced/patcher/apk/Apk.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/apk/Apk.kt new file mode 100644 index 0000000..6b03a25 --- /dev/null +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/apk/Apk.kt @@ -0,0 +1,270 @@ +@file:Suppress("MemberVisibilityCanBePrivate") + +package app.revanced.patcher.apk + +import app.revanced.arsc.ApkException +import app.revanced.arsc.archive.Archive +import app.revanced.arsc.resource.* +import app.revanced.patcher.Patcher +import app.revanced.patcher.PatcherOptions +import app.revanced.patcher.logging.asArscLogger +import app.revanced.patcher.util.ProxyBackedClassList +import com.reandroid.apk.ApkModule +import com.reandroid.apk.xmlencoder.EncodeException +import com.reandroid.arsc.chunk.xml.AndroidManifestBlock +import com.reandroid.arsc.value.ResConfig +import lanchon.multidexlib2.* +import org.jf.dexlib2.dexbacked.DexBackedDexFile +import org.jf.dexlib2.iface.DexFile +import org.jf.dexlib2.iface.MultiDexContainer +import org.jf.dexlib2.writer.io.MemoryDataStore +import java.io.File + +/** + * An [Apk] file. + */ +sealed class Apk private constructor(module: ApkModule) { + /** + * A wrapper around the zip archive of this [Apk]. + * + * @see Archive + */ + private val archive = Archive(module) + + /** + * The metadata of the [Apk]. + */ + val packageMetadata = PackageMetadata(module.androidManifestBlock) + + val resources = ResourceContainer(archive, module.tableBlock) + + /** + * Refresh updated resources and close any open files. + * + * @param options The [PatcherOptions] of the [Patcher]. + */ + internal open fun cleanup(options: PatcherOptions) { + try { + archive.cleanup(options.logger.asArscLogger()) + } catch (e: EncodeException) { + throw ApkException.Encode(e.message!!, e) + } + + resources.refreshPackageName() + } + + /** + * Write the [Apk] to a file. + * + * @param output The target file. + */ + fun write(output: File) = archive.save(output) + + companion object { + const val manifest = "AndroidManifest.xml" + + /** + * Determine the [Module] and [Type] of an [ApkModule]. + * + * @return A [Pair] containing the [Module] and [Type] of the [ApkModule]. + */ + fun ApkModule.identify(): Pair { + val manifestElement = androidManifestBlock.manifestElement + return when { + isBaseModule -> Module.Main to Type.Base + // The module is a base apk for a dynamic feature module if the "isFeatureModule" attribute is set to true. + manifestElement.searchAttributeByName("isFeatureModule")?.valueAsBoolean == true -> Module.DynamicFeature( + split + ) to Type.Base + + else -> { + val module = manifestElement.searchAttributeByName("configForSplit")?.let { Module.DynamicFeature(it.valueAsString) } ?: Module.Main + // Examples: + // config.xhdpi + // df_my_feature.config.en + val config = this.split.split(".").last() + + val type = when { + // Language splits have a two-letter country code. + config.length == 2 -> Type.Language(config) + // Library splits use the target CPU architecture. + Split.Library.architectures.contains(config) -> Type.Library(config) + // Asset splits use the density. + ResConfig.Density.valueOf(config) != null -> Type.Asset(config) + else -> throw IllegalArgumentException("Invalid split config: $config") + } + + module to type + } + } + } + } + + internal inner class BytecodeData { + private val dexFile = MultiDexContainerBackedDexFile(object : MultiDexContainer { + // Load all dex files from the apk module and create a dex entry for each of them. + private val entries = archive.readDexFiles() + .mapValues { (name, data) -> BasicDexEntry(this, name, RawDexIO.readRawDexFile(data, 0, null)) } + + override fun getDexEntryNames() = entries.keys.toList() + override fun getEntry(entryName: String) = entries[entryName] + }) + private val opcodes = dexFile.opcodes + + /** + * The classes and proxied classes of the [Base] apk file. + */ + val classes = ProxyBackedClassList(dexFile.classes) + + /** + * Write [classes] to the archive. + */ + internal fun writeDexFiles() { + // Make sure to replace all classes with their proxy. + val classes = classes.also(ProxyBackedClassList::applyProxies) + val opcodes = opcodes + + // Create patched dex files. + mutableMapOf().also { + val newDexFile = object : DexFile { + override fun getClasses() = classes.toSet() + override fun getOpcodes() = opcodes + } + + // Write modified dex files. + MultiDexIO.writeDexFile( + true, -1, // Core count. + it, Patcher.dexFileNamer, newDexFile, DexIO.DEFAULT_MAX_DEX_POOL_SIZE, null + ) + }.forEach { (name, store) -> + archive.writeRaw(name, store.data) + } + } + } + + /** + * Metadata about an [Apk] file. + * + * @param packageName The package name of the [Apk] file. + * @param packageVersion The package version of the [Apk] file. + */ + data class PackageMetadata(val packageName: String, val packageVersion: String?) { + internal constructor(manifestBlock: AndroidManifestBlock) : this( + manifestBlock.packageName ?: "unnamed split apk file", manifestBlock.versionName + ) + } + + /** + * An [Apk] of type [Split]. + * + * @param config The device configuration associated with this [Split], such as arm64_v8a, en or xhdpi. + * @see Apk + */ + sealed class Split(val config: String, module: ApkModule) : Apk(module) { + override fun toString() = "split_config.$config.apk" + + /** + * The split apk file which contains libraries. + * + * @see Split + */ + class Library internal constructor(config: String, module: ApkModule) : Split(config, module) { + companion object { + /** + * A set of all architectures supported by android. + */ + val architectures = setOf("armeabi_v7a", "arm64_v8a", "x86", "x86_64") + } + } + + /** + * The split apk file which contains language strings. + * + * @see Split + */ + class Language internal constructor(config: String, module: ApkModule) : Split(config, module) + + /** + * The split apk file which contains assets. + * + * @see Split + */ + class Asset internal constructor(config: String, module: ApkModule) : Split(config, module) + } + + /** + * The base apk file that is to be patched. + * + * @see Apk + */ + class Base internal constructor(module: ApkModule) : Apk(module) { + /** + * Data of the [Base] apk file. + */ + internal val bytecodeData = BytecodeData() + + override fun toString() = "base.apk" + + override fun cleanup(options: PatcherOptions) { + super.cleanup(options) + + options.logger.info("Writing patched dex files") + bytecodeData.writeDexFiles() + } + } + + /** + * The module that the [ApkModule] belongs to. + */ + sealed class Module { + /** + * The default [Module] that is always installed by software repositories. + */ + object Main : Module() + + /** + * A [Module] that can be installed later by software repositories when requested by the application. + * + * @param name The name of the feature. + */ + data class DynamicFeature(val name: String) : Module() + } + + /** + * The type of the [ApkModule]. + */ + sealed class Type { + /** + * The main Apk of a [Module]. + */ + object Base : Type() + + /** + * A superclass for all split configuration types. + * + * @param target The target device configuration. + */ + sealed class SplitConfig(val target: String) : Type() + + /** + * The [Type] of an apk containing native libraries. + * + * @param architecture The target CPU architecture. + */ + data class Library(val architecture: String) : SplitConfig(architecture) + + /** + * The [Type] for an Apk containing language resources. + * + * @param language The target language code. + */ + data class Language(val language: String) : SplitConfig(language) + + /** + * The [Type] for an Apk containing assets. + * + * @param pixelDensity The target screen density. + */ + data class Asset(val pixelDensity: String) : SplitConfig(pixelDensity) + } +} diff --git a/revanced-patcher/src/main/kotlin/app/revanced/patcher/apk/ApkBundle.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/apk/ApkBundle.kt new file mode 100644 index 0000000..6e46440 --- /dev/null +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/apk/ApkBundle.kt @@ -0,0 +1,113 @@ +package app.revanced.patcher.apk + +import app.revanced.arsc.ApkException +import app.revanced.arsc.resource.ResourceTable +import app.revanced.patcher.Patcher +import app.revanced.patcher.PatcherOptions +import app.revanced.patcher.apk.Apk.Companion.identify +import com.reandroid.apk.ApkModule +import java.io.File + +/** + * An [Apk] file of type [Apk.Split]. + * + * @param files A list of apk files to load. + */ +class ApkBundle(files: List) { + /** + * The [Apk.Base] of this [ApkBundle]. + */ + val base: Apk.Base + + /** + * A map containing all the [Apk.Split]s in this bundle associated by their configuration. + */ + val splits: Map? + + init { + var baseApk: Apk.Base? = null + + splits = buildMap { + files.forEach { + val apk = ApkModule.loadApkFile(it) + val (module, type) = apk.identify() + if (module is Apk.Module.DynamicFeature) { + return@forEach // Dynamic feature modules are not supported yet. + } + + when (type) { + Apk.Type.Base -> { + if (baseApk != null) { + throw IllegalArgumentException("Cannot have more than one base apk") + } + baseApk = Apk.Base(apk) + } + + is Apk.Type.SplitConfig -> { + val target = type.target + if (this.contains(target)) { + throw IllegalArgumentException("Duplicate split: $target") + } + + val constructor = when (type) { + is Apk.Type.Asset -> Apk.Split::Asset + is Apk.Type.Library -> Apk.Split::Library + is Apk.Type.Language -> Apk.Split::Language + } + + this[target] = constructor(target, apk) + } + } + } + }.takeIf { it.isNotEmpty() } + + base = baseApk ?: throw IllegalArgumentException("Base apk not found") + } + + /** + * A [Sequence] yielding all [Apk]s in this [ApkBundle]. + */ + val all = sequence { + yield(base) + splits?.values?.let { + yieldAll(it) + } + } + + /** + * Get the [app.revanced.arsc.resource.ResourceContainer] for the specified configuration. + * + * @param config The config to search for. + */ + fun query(config: String) = splits?.get(config)?.resources ?: base.resources + + /** + * Refresh all updated resources in an [ApkBundle]. + * + * @param options The [PatcherOptions] of the [Patcher]. + * @return A sequence of the [Apk] files which are being refreshed. + */ + internal fun cleanup(options: PatcherOptions) = all.map { + var exception: ApkException? = null + try { + it.cleanup(options) + } catch (e: ApkException) { + exception = e + } + + SplitApkResult(it, exception) + } + + /** + * The [ResourceTable] of this [ApkBundle]. + */ + val resources = ResourceTable(base.resources, all.map { it.resources }) + + /** + * The result of writing an [Apk] file. + * + * @param apk The corresponding [Apk] file. + * @param exception The optional [ApkException] when an exception occurred. + */ + data class SplitApkResult(val apk: Apk, val exception: ApkException? = null) +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/extensions/AnnotationExtensions.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/extensions/AnnotationExtensions.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/extensions/AnnotationExtensions.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/extensions/AnnotationExtensions.kt diff --git a/src/main/kotlin/app/revanced/patcher/extensions/Extensions.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/extensions/Extensions.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/extensions/Extensions.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/extensions/Extensions.kt diff --git a/src/main/kotlin/app/revanced/patcher/extensions/InstructionExtensions.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/extensions/InstructionExtensions.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/extensions/InstructionExtensions.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/extensions/InstructionExtensions.kt diff --git a/src/main/kotlin/app/revanced/patcher/extensions/MethodFingerprintExtensions.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/extensions/MethodFingerprintExtensions.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/extensions/MethodFingerprintExtensions.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/extensions/MethodFingerprintExtensions.kt diff --git a/src/main/kotlin/app/revanced/patcher/extensions/PatchExtensions.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/extensions/PatchExtensions.kt similarity index 81% rename from src/main/kotlin/app/revanced/patcher/extensions/PatchExtensions.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/extensions/PatchExtensions.kt index 06f2224..bcbefe5 100644 --- a/src/main/kotlin/app/revanced/patcher/extensions/PatchExtensions.kt +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/extensions/PatchExtensions.kt @@ -4,10 +4,10 @@ import app.revanced.patcher.annotation.Compatibility import app.revanced.patcher.annotation.Description import app.revanced.patcher.annotation.Name import app.revanced.patcher.annotation.Version -import app.revanced.patcher.data.Context import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively import app.revanced.patcher.patch.OptionsContainer import app.revanced.patcher.patch.Patch +import app.revanced.patcher.patch.PatchClass import app.revanced.patcher.patch.PatchOptions import app.revanced.patcher.patch.annotations.DependsOn import app.revanced.patcher.patch.annotations.RequiresIntegrations @@ -19,49 +19,49 @@ object PatchExtensions { /** * The name of a [Patch]. */ - val Class>.patchName: String + val PatchClass.patchName: String get() = findAnnotationRecursively(Name::class)?.name ?: this.simpleName /** * The version of a [Patch]. */ - val Class>.version + val PatchClass.version get() = findAnnotationRecursively(Version::class)?.version /** * Weather or not a [Patch] should be included. */ - val Class>.include + val PatchClass.include get() = findAnnotationRecursively(app.revanced.patcher.patch.annotations.Patch::class)!!.include /** * The description of a [Patch]. */ - val Class>.description + val PatchClass.description get() = findAnnotationRecursively(Description::class)?.description /** * The dependencies of a [Patch]. */ - val Class>.dependencies + val PatchClass.dependencies get() = findAnnotationRecursively(DependsOn::class)?.dependencies /** * The packages a [Patch] is compatible with. */ - val Class>.compatiblePackages + val PatchClass.compatiblePackages get() = findAnnotationRecursively(Compatibility::class)?.compatiblePackages /** * Weather or not a [Patch] requires integrations. */ - internal val Class>.requiresIntegrations + internal val PatchClass.requiresIntegrations get() = findAnnotationRecursively(RequiresIntegrations::class) != null /** * The options of a [Patch]. */ - val Class>.options: PatchOptions? + val PatchClass.options: PatchOptions? get() = kotlin.companionObject?.let { cl -> if (cl.visibility != KVisibility.PUBLIC) return null kotlin.companionObjectInstance?.let { diff --git a/src/main/kotlin/app/revanced/patcher/fingerprint/Fingerprint.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/fingerprint/Fingerprint.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/fingerprint/Fingerprint.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/fingerprint/Fingerprint.kt diff --git a/src/main/kotlin/app/revanced/patcher/fingerprint/method/annotation/MethodFingerprintMetadata.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/fingerprint/method/annotation/MethodFingerprintMetadata.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/fingerprint/method/annotation/MethodFingerprintMetadata.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/fingerprint/method/annotation/MethodFingerprintMetadata.kt diff --git a/src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/MethodFingerprint.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/MethodFingerprint.kt similarity index 97% rename from src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/MethodFingerprint.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/MethodFingerprint.kt index 3921827..96c503c 100644 --- a/src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/MethodFingerprint.kt +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/MethodFingerprint.kt @@ -1,10 +1,10 @@ package app.revanced.patcher.fingerprint.method.impl -import app.revanced.patcher.data.BytecodeContext +import app.revanced.patcher.BytecodeContext import app.revanced.patcher.extensions.MethodFingerprintExtensions.fuzzyPatternScanMethod import app.revanced.patcher.fingerprint.Fingerprint import app.revanced.patcher.fingerprint.method.annotation.FuzzyPatternScanMethod -import app.revanced.patcher.patch.PatchResultError +import app.revanced.patcher.patch.PatchException import app.revanced.patcher.util.proxy.ClassProxy import org.jf.dexlib2.AccessFlags import org.jf.dexlib2.Opcode @@ -99,9 +99,9 @@ abstract class MethodFingerprint( methodClassPairs!!.add(methodClassPair) } - if (methods.isNotEmpty()) throw PatchResultError("Map already initialized") + if (methods.isNotEmpty()) throw PatchException("Map already initialized") - context.classes.classes.forEach { classDef -> + context.classes.forEach { classDef -> classDef.methods.forEach { method -> val methodClassPair = method to classDef @@ -160,7 +160,7 @@ abstract class MethodFingerprint( * - Fastest: Specify [strings], with at least one string being an exact (non-partial) match. */ internal fun Iterable.resolveUsingLookupMap(context: BytecodeContext) { - if (methods.isEmpty()) throw PatchResultError("lookup map not initialized") + if (methods.isEmpty()) throw PatchException("lookup map not initialized") for (fingerprint in this) { fingerprint.resolveUsingLookupMap(context) @@ -362,18 +362,18 @@ abstract class MethodFingerprint( 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 + // 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 + // If the entire pattern has not been scanned yet + // continue the scan. patternIndex++ continue } - // the pattern is valid, generate warnings if fuzzyPatternScanMethod is FuzzyPatternScanMethod + // The pattern is valid, generate warnings if fuzzyPatternScanMethod is FuzzyPatternScanMethod val result = MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult( index, @@ -448,7 +448,7 @@ data class MethodFingerprintResult( * Use [classDef] where possible. */ @Suppress("MemberVisibilityCanBePrivate") - val mutableClass by lazy { context.proxy(classDef).mutableClass } + val mutableClass by lazy { context.classes.proxy(classDef).mutableClass } /** * Returns a mutable clone of [method] diff --git a/revanced-patcher/src/main/kotlin/app/revanced/patcher/logging/Logger.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/logging/Logger.kt new file mode 100644 index 0000000..daaf551 --- /dev/null +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/logging/Logger.kt @@ -0,0 +1,20 @@ +package app.revanced.patcher.logging + +interface Logger { + fun error(msg: String) {} + fun warn(msg: String) {} + fun info(msg: String) {} + fun trace(msg: String) {} + + object Nop : Logger +} + +/** + * Turn a Patcher [Logger] into an [app.revanced.arsc.logging.Logger]. + */ +internal fun Logger.asArscLogger() = object : app.revanced.arsc.logging.Logger { + override fun error(msg: String) = this@asArscLogger.error(msg) + override fun warn(msg: String) = this@asArscLogger.warn(msg) + override fun info(msg: String) = this@asArscLogger.info(msg) + override fun trace(msg: String) = this@asArscLogger.error(msg) +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/patch/OptionsContainer.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/OptionsContainer.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/patch/OptionsContainer.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/OptionsContainer.kt diff --git a/src/main/kotlin/app/revanced/patcher/patch/Patch.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/Patch.kt similarity index 69% rename from src/main/kotlin/app/revanced/patcher/patch/Patch.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/Patch.kt index 3f982b8..3dab072 100644 --- a/src/main/kotlin/app/revanced/patcher/patch/Patch.kt +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/Patch.kt @@ -1,8 +1,8 @@ package app.revanced.patcher.patch -import app.revanced.patcher.data.BytecodeContext -import app.revanced.patcher.data.Context -import app.revanced.patcher.data.ResourceContext +import app.revanced.patcher.BytecodeContext +import app.revanced.patcher.Context +import app.revanced.patcher.ResourceContext import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint import java.io.Closeable @@ -17,9 +17,8 @@ sealed interface Patch { * The main function of the [Patch] which the patcher will call. * * @param context The [Context] the patch will work on. - * @return The result of executing the patch. */ - fun execute(context: @UnsafeVariance T): PatchResult + suspend fun execute(context: @UnsafeVariance T) } /** @@ -34,4 +33,10 @@ interface ResourcePatch : Patch */ abstract class BytecodePatch( internal val fingerprints: Iterable? = null -) : Patch \ No newline at end of file +) : Patch + +// TODO: populate this everywhere where the alias is not used yet +/** + * The class type of [Patch]. + */ +typealias PatchClass = Class> diff --git a/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/PatchException.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/PatchException.kt new file mode 100644 index 0000000..962e276 --- /dev/null +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/PatchException.kt @@ -0,0 +1,12 @@ +package app.revanced.patcher.patch + +/** + * An exception thrown when patching. + * + * @param errorMessage The exception message. + * @param cause The corresponding [Throwable]. + */ +class PatchException(errorMessage: String?, cause: Throwable?) : Exception(errorMessage, cause) { + constructor(errorMessage: String) : this(errorMessage, null) + constructor(cause: Throwable) : this(cause.message, cause) +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/patch/PatchOption.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/PatchOption.kt similarity index 99% rename from src/main/kotlin/app/revanced/patcher/patch/PatchOption.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/PatchOption.kt index cfb0c1c..0573dee 100644 --- a/src/main/kotlin/app/revanced/patcher/patch/PatchOption.kt +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/PatchOption.kt @@ -2,8 +2,6 @@ package app.revanced.patcher.patch -import java.nio.file.Path -import kotlin.io.path.pathString import kotlin.reflect.KProperty class NoSuchOptionException(val option: String) : Exception("No such option: $option") diff --git a/src/main/kotlin/app/revanced/patcher/patch/annotations/PatchAnnotation.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/annotations/PatchAnnotation.kt similarity index 68% rename from src/main/kotlin/app/revanced/patcher/patch/annotations/PatchAnnotation.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/annotations/PatchAnnotation.kt index 0ab81be..30d3e52 100644 --- a/src/main/kotlin/app/revanced/patcher/patch/annotations/PatchAnnotation.kt +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/annotations/PatchAnnotation.kt @@ -1,6 +1,6 @@ package app.revanced.patcher.patch.annotations -import app.revanced.patcher.data.Context +import app.revanced.patcher.Context import app.revanced.patcher.patch.Patch import kotlin.reflect.KClass @@ -16,7 +16,7 @@ annotation class Patch(val include: Boolean = true) */ @Target(AnnotationTarget.CLASS) annotation class DependsOn( - val dependencies: Array>> = [] + val dependencies: Array>> = [] // TODO: This should be a list of PatchClass instead ) @@ -24,4 +24,4 @@ annotation class DependsOn( * Annotation to mark [Patch]es which depend on integrations. */ @Target(AnnotationTarget.CLASS) -annotation class RequiresIntegrations // required because integrations are decoupled from patches \ No newline at end of file +annotation class RequiresIntegrations // TODO: Remove this annotation and replace it with a proper system \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/util/ClassMerger.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/ClassMerger.kt similarity index 96% rename from src/main/kotlin/app/revanced/patcher/util/ClassMerger.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/ClassMerger.kt index abac334..44a7479 100644 --- a/src/main/kotlin/app/revanced/patcher/util/ClassMerger.kt +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/ClassMerger.kt @@ -1,6 +1,6 @@ package app.revanced.patcher.util -import app.revanced.patcher.PatcherContext +import app.revanced.patcher.BytecodeContext import app.revanced.patcher.extensions.or import app.revanced.patcher.logging.Logger import app.revanced.patcher.util.ClassMerger.Utils.asMutableClass @@ -32,7 +32,7 @@ internal object ClassMerger { * @param context The context to traverse the class hierarchy in. * @param logger A logger. */ - fun ClassDef.merge(otherClass: ClassDef, context: PatcherContext, logger: Logger? = null) = this + fun ClassDef.merge(otherClass: ClassDef, context: BytecodeContext, logger: Logger? = null) = this //.fixFieldAccess(otherClass, logger) //.fixMethodAccess(otherClass, logger) .addMissingFields(otherClass, logger) @@ -89,10 +89,10 @@ internal object ClassMerger { * @param context The context to traverse the class hierarchy in. * @param logger A logger. */ - private fun ClassDef.publicize(reference: ClassDef, context: PatcherContext, logger: Logger? = null) = + private fun ClassDef.publicize(reference: ClassDef, context: BytecodeContext, logger: Logger? = null) = if (reference.accessFlags.isPublic() && !accessFlags.isPublic()) this.asMutableClass().apply { - context.bytecodeContext.traverseClassHierarchy(this) { + context.traverseClassHierarchy(this) { if (accessFlags.isPublic()) return@traverseClassHierarchy logger?.trace("Publicizing ${this.type}") diff --git a/src/main/kotlin/app/revanced/patcher/util/ListBackedSet.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/ListBackedSet.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/ListBackedSet.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/ListBackedSet.kt diff --git a/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/ProxyBackedClassList.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/ProxyBackedClassList.kt new file mode 100644 index 0000000..078cde2 --- /dev/null +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/ProxyBackedClassList.kt @@ -0,0 +1,89 @@ +package app.revanced.patcher.util + +import app.revanced.patcher.util.proxy.ClassProxy +import org.jf.dexlib2.iface.ClassDef + +/** + * A class that represents a list of classes and proxies. + * + * @param classes The classes to be backed by proxies. + */ +class ProxyBackedClassList(classes: Set) : Iterable { + // A list for pending proxied classes to be added to the current ProxyBackedClassList instance. + private val proxiedClasses = mutableListOf() + private val mutableClasses = classes.toMutableList() + + /** + * Replace the [mutableClasses]es with their proxies. + */ + internal fun applyProxies() { + proxiedClasses.removeIf { proxy -> + // If the proxy is unused, keep it in the proxiedClasses list. + if (!proxy.resolved) return@removeIf false + + with(mutableClasses) { + remove(proxy.immutableClass) + add(proxy.mutableClass) + } + + return@removeIf true + } + } + + /** + * Replace a [ClassDef] at a given [index]. + * + * @param index The index of the class to be replaced. + * @param classDef The new class to replace the old one. + */ + operator fun set(index: Int, classDef: ClassDef) { + mutableClasses[index] = classDef + } + + /** + * Get a [ClassDef] at a given [index]. + * + * @param index The index of the class. + */ + operator fun get(index: Int) = mutableClasses[index] + + /** + * Iterator for the classes in [ProxyBackedClassList]. + * + * @return The iterator for the classes. + */ + override fun iterator() = mutableClasses.iterator() + + /** + * Proxy a [ClassDef]. + * + * Note: This creates a [ClassProxy] of the [ClassDef], if not already present. + * + * @return A proxy for the given class. + */ + fun proxy(classDef: ClassDef) = proxiedClasses + .find { it.immutableClass.type == classDef.type } ?: ClassProxy(classDef).also(proxiedClasses::add) + + /** + * Add a [ClassDef]. + */ + fun add(classDef: ClassDef) = mutableClasses.add(classDef) + + /** + * 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 findClassProxied(className: String) = findClassProxied { 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 findClassProxied(predicate: (ClassDef) -> Boolean) = this.find(predicate)?.let(::proxy) + + val size get() = mutableClasses.size +} \ No newline at end of file diff --git a/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/TypeUtil.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/TypeUtil.kt new file mode 100644 index 0000000..254c7bc --- /dev/null +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/TypeUtil.kt @@ -0,0 +1,19 @@ +package app.revanced.patcher.util + +import app.revanced.patcher.BytecodeContext +import app.revanced.patcher.util.proxy.mutableTypes.MutableClass + +object TypeUtil { + /** + * Traverse the class hierarchy starting from the given root class. + * + * @param targetClass The class to start traversing the class hierarchy from. + * @param callback The function that is called for every class in the hierarchy. + */ + fun BytecodeContext.traverseClassHierarchy(targetClass: MutableClass, callback: MutableClass.() -> Unit) { + callback(targetClass) + this.classes.findClassProxied(targetClass.superclass ?: return)?.mutableClass?.let { + traverseClassHierarchy(it, callback) + } + } +} diff --git a/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/VersionReader.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/VersionReader.kt new file mode 100644 index 0000000..2361608 --- /dev/null +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/VersionReader.kt @@ -0,0 +1,19 @@ +package app.revanced.patcher.util + +import java.util.* + +@Deprecated("This class serves no purpose anymore") +internal object VersionReader { + @JvmStatic + private val properties = Properties().apply { + load( + VersionReader::class.java.getResourceAsStream("/app/revanced/patcher/version.properties") + ?: throw IllegalStateException("Could not load version.properties") + ) + } + + @JvmStatic + fun read(): String { + return properties.getProperty("version") ?: throw IllegalStateException("Version not found") + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/util/method/MethodWalker.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/method/MethodWalker.kt similarity index 94% rename from src/main/kotlin/app/revanced/patcher/util/method/MethodWalker.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/method/MethodWalker.kt index 70efa01..678561b 100644 --- a/src/main/kotlin/app/revanced/patcher/util/method/MethodWalker.kt +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/method/MethodWalker.kt @@ -1,6 +1,6 @@ package app.revanced.patcher.util.method -import app.revanced.patcher.data.BytecodeContext +import app.revanced.patcher.BytecodeContext import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod import org.jf.dexlib2.iface.Method import org.jf.dexlib2.iface.instruction.ReferenceInstruction @@ -41,7 +41,7 @@ class MethodWalker internal constructor( val instruction = instructions.elementAt(offset) val newMethod = (instruction as ReferenceInstruction).reference as MethodReference - val proxy = bytecodeContext.findClass(newMethod.definingClass)!! + val proxy = bytecodeContext.classes.findClassProxied(newMethod.definingClass)!! val methods = if (walkMutable) proxy.mutableClass.methods else proxy.immutableClass.methods currentMethod = methods.first { diff --git a/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/patch/PatchBundle.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/patch/PatchBundle.kt new file mode 100644 index 0000000..9218099 --- /dev/null +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/patch/PatchBundle.kt @@ -0,0 +1,59 @@ +@file:Suppress("unused") + +package app.revanced.patcher.util.patch + +import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively +import app.revanced.patcher.patch.Patch +import app.revanced.patcher.patch.PatchClass +import dalvik.system.PathClassLoader +import org.jf.dexlib2.DexFileFactory +import java.io.File +import java.net.URLClassLoader +import java.util.jar.JarFile +import kotlin.streams.toList + +/** + * A patch bundle. + * + * @param fromClasses The classes to get [Patch]es from. + */ +sealed class PatchBundle private constructor(fromClasses: Iterable>) : Iterable { + private val patches = fromClasses.filter { + if (it.isAnnotation) return@filter false + + it.findAnnotationRecursively(app.revanced.patcher.patch.annotations.Patch::class) != null + }.map { + @Suppress("UNCHECKED_CAST") + it as PatchClass + } + + override fun iterator() = patches.iterator() + + /** + * A patch bundle of type [Jar]. + * + * @param patchBundlePath The path to a patch bundle. + */ + class Jar(private val patchBundlePath: File) : PatchBundle( + with(URLClassLoader(arrayOf(patchBundlePath.toURI().toURL()), PatchBundle::class.java.classLoader)) { + JarFile(patchBundlePath).stream().filter { it.name.endsWith(".class") }.map { + loadClass( + it.realName.replace('/', '.').replace(".class", "") + ) + }.toList() + } + ) + + /** + * A patch bundle of type [Dex] format. + * + * @param patchBundlePath The path to a patch bundle of dex format. + */ + class Dex(private val patchBundlePath: File) : PatchBundle( + with(PathClassLoader(patchBundlePath.absolutePath, null, PatchBundle::class.java.classLoader)) { + DexFileFactory.loadDexFile(patchBundlePath, null).classes.map { classDef -> + classDef.type.substring(1, classDef.length - 1).replace('/', '.') + }.map { loadClass(it) } + } + ) +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/ClassProxy.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/ClassProxy.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/ClassProxy.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/ClassProxy.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableAnnotation.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableAnnotation.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableAnnotation.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableAnnotation.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableAnnotationElement.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableAnnotationElement.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableAnnotationElement.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableAnnotationElement.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableClass.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableClass.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableClass.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableClass.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableField.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableField.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableField.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableField.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableMethod.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableMethod.kt similarity index 95% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableMethod.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableMethod.kt index 9792b23..98188f6 100644 --- a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableMethod.kt +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableMethod.kt @@ -13,7 +13,7 @@ class MutableMethod(method: Method) : Method, BaseMethodReference() { private var accessFlags = method.accessFlags private var returnType = method.returnType - // Create own mutable MethodImplementation (due to not being able to change members like register count) + // TODO: Create own mutable MethodImplementation (due to not being able to change members like register count). private val _implementation by lazy { method.implementation?.let { MutableMethodImplementation(it) } } private val _annotations by lazy { method.annotations.map { annotation -> annotation.toMutable() }.toMutableSet() } private val _parameters by lazy { method.parameters.map { parameter -> parameter.toMutable() }.toMutableList() } diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableMethodParameter.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableMethodParameter.kt similarity index 95% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableMethodParameter.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableMethodParameter.kt index a9e8391..c880be3 100644 --- a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableMethodParameter.kt +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableMethodParameter.kt @@ -4,7 +4,7 @@ import app.revanced.patcher.util.proxy.mutableTypes.MutableAnnotation.Companion. import org.jf.dexlib2.base.BaseMethodParameter import org.jf.dexlib2.iface.MethodParameter -// TODO: finish overriding all members if necessary +// TODO: Finish overriding all members if necessary. class MutableMethodParameter(parameter: MethodParameter) : MethodParameter, BaseMethodParameter() { private var type = parameter.type private var name = parameter.name diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableAnnotationEncodedValue.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableAnnotationEncodedValue.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableAnnotationEncodedValue.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableAnnotationEncodedValue.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableArrayEncodedValue.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableArrayEncodedValue.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableArrayEncodedValue.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableArrayEncodedValue.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableBooleanEncodedValue.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableBooleanEncodedValue.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableBooleanEncodedValue.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableBooleanEncodedValue.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableByteEncodedValue.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableByteEncodedValue.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableByteEncodedValue.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableByteEncodedValue.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableCharEncodedValue.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableCharEncodedValue.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableCharEncodedValue.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableCharEncodedValue.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableDoubleEncodedValue.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableDoubleEncodedValue.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableDoubleEncodedValue.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableDoubleEncodedValue.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableEncodedValue.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableEncodedValue.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableEncodedValue.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableEncodedValue.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableEnumEncodedValue.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableEnumEncodedValue.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableEnumEncodedValue.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableEnumEncodedValue.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableFieldEncodedValue.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableFieldEncodedValue.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableFieldEncodedValue.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableFieldEncodedValue.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableFloatEncodedValue.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableFloatEncodedValue.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableFloatEncodedValue.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableFloatEncodedValue.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableIntEncodedValue.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableIntEncodedValue.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableIntEncodedValue.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableIntEncodedValue.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableLongEncodedValue.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableLongEncodedValue.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableLongEncodedValue.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableLongEncodedValue.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableMethodEncodedValue.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableMethodEncodedValue.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableMethodEncodedValue.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableMethodEncodedValue.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableMethodHandleEncodedValue.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableMethodHandleEncodedValue.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableMethodHandleEncodedValue.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableMethodHandleEncodedValue.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableMethodTypeEncodedValue.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableMethodTypeEncodedValue.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableMethodTypeEncodedValue.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableMethodTypeEncodedValue.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableNullEncodedValue.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableNullEncodedValue.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableNullEncodedValue.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableNullEncodedValue.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableShortEncodedValue.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableShortEncodedValue.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableShortEncodedValue.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableShortEncodedValue.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableStringEncodedValue.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableStringEncodedValue.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableStringEncodedValue.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableStringEncodedValue.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableTypeEncodedValue.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableTypeEncodedValue.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableTypeEncodedValue.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableTypeEncodedValue.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/smali/ExternalLabel.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/smali/ExternalLabel.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/smali/ExternalLabel.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/smali/ExternalLabel.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/smali/InlineSmaliCompiler.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/smali/InlineSmaliCompiler.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/smali/InlineSmaliCompiler.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/smali/InlineSmaliCompiler.kt diff --git a/src/main/resources/revanced-patcher/version.properties b/revanced-patcher/src/main/resources/app/revanced/patcher/version.properties similarity index 100% rename from src/main/resources/revanced-patcher/version.properties rename to revanced-patcher/src/main/resources/app/revanced/patcher/version.properties diff --git a/src/test/kotlin/app/revanced/patcher/issues/Issue98.kt b/revanced-patcher/src/test/kotlin/app/revanced/patcher/issues/Issue98.kt similarity index 100% rename from src/test/kotlin/app/revanced/patcher/issues/Issue98.kt rename to revanced-patcher/src/test/kotlin/app/revanced/patcher/issues/Issue98.kt diff --git a/src/test/kotlin/app/revanced/patcher/patch/PatchOptionsTest.kt b/revanced-patcher/src/test/kotlin/app/revanced/patcher/patch/PatchOptionsTest.kt similarity index 94% rename from src/test/kotlin/app/revanced/patcher/patch/PatchOptionsTest.kt rename to revanced-patcher/src/test/kotlin/app/revanced/patcher/patch/PatchOptionsTest.kt index d83e9d8..125b59b 100644 --- a/src/test/kotlin/app/revanced/patcher/patch/PatchOptionsTest.kt +++ b/revanced-patcher/src/test/kotlin/app/revanced/patcher/patch/PatchOptionsTest.kt @@ -3,8 +3,6 @@ package app.revanced.patcher.patch import app.revanced.patcher.usage.bytecode.ExampleBytecodePatch import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows -import kotlin.io.path.Path -import kotlin.io.path.pathString import kotlin.test.assertNotEquals internal class PatchOptionsTest { @@ -35,10 +33,6 @@ internal class PatchOptionsTest { println(choice) } } - - is PatchOption.PathOption -> { - option.value = Path("test.txt").pathString - } } } val option = options.get("key1") diff --git a/src/test/kotlin/app/revanced/patcher/usage/bytecode/ExampleBytecodeCompatibility.kt b/revanced-patcher/src/test/kotlin/app/revanced/patcher/usage/bytecode/ExampleBytecodeCompatibility.kt similarity index 100% rename from src/test/kotlin/app/revanced/patcher/usage/bytecode/ExampleBytecodeCompatibility.kt rename to revanced-patcher/src/test/kotlin/app/revanced/patcher/usage/bytecode/ExampleBytecodeCompatibility.kt diff --git a/src/test/kotlin/app/revanced/patcher/usage/bytecode/ExampleBytecodePatch.kt b/revanced-patcher/src/test/kotlin/app/revanced/patcher/usage/bytecode/ExampleBytecodePatch.kt similarity index 91% rename from src/test/kotlin/app/revanced/patcher/usage/bytecode/ExampleBytecodePatch.kt rename to revanced-patcher/src/test/kotlin/app/revanced/patcher/usage/bytecode/ExampleBytecodePatch.kt index 0add464..430333e 100644 --- a/src/test/kotlin/app/revanced/patcher/usage/bytecode/ExampleBytecodePatch.kt +++ b/revanced-patcher/src/test/kotlin/app/revanced/patcher/usage/bytecode/ExampleBytecodePatch.kt @@ -1,13 +1,15 @@ package app.revanced.patcher.usage.bytecode +import app.revanced.patcher.BytecodeContext import app.revanced.patcher.annotation.Description import app.revanced.patcher.annotation.Name import app.revanced.patcher.annotation.Version -import app.revanced.patcher.data.BytecodeContext import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels -import app.revanced.patcher.extensions.or import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction -import app.revanced.patcher.patch.* +import app.revanced.patcher.extensions.or +import app.revanced.patcher.patch.BytecodePatch +import app.revanced.patcher.patch.OptionsContainer +import app.revanced.patcher.patch.PatchOption import app.revanced.patcher.patch.annotations.DependsOn import app.revanced.patcher.patch.annotations.Patch import app.revanced.patcher.usage.resource.annotation.ExampleResourceCompatibility @@ -29,7 +31,6 @@ import org.jf.dexlib2.immutable.reference.ImmutableFieldReference import org.jf.dexlib2.immutable.reference.ImmutableStringReference import org.jf.dexlib2.immutable.value.ImmutableFieldEncodedValue import org.jf.dexlib2.util.Preconditions -import kotlin.io.path.Path @Patch @Name("example-bytecode-patch") @@ -40,7 +41,7 @@ import kotlin.io.path.Path class ExampleBytecodePatch : BytecodePatch(listOf(ExampleFingerprint)) { // This function will be executed by the patcher. // You can treat it as a constructor - override fun execute(context: BytecodeContext): PatchResult { + override suspend fun execute(context: BytecodeContext) { // Get the resolved method by its fingerprint from the resolver cache val result = ExampleFingerprint.result!! @@ -60,7 +61,7 @@ class ExampleBytecodePatch : BytecodePatch(listOf(ExampleFingerprint)) { implementation.replaceStringAt(startIndex, "Hello, ReVanced! Editing bytecode.") // Get the class in which the method matching our fingerprint is defined in. - val mainClass = context.findClass { + val mainClass = context.classes.findClassProxied { it.type == result.classDef.type }!!.mutableClass @@ -127,12 +128,6 @@ class ExampleBytecodePatch : BytecodePatch(listOf(ExampleFingerprint)) { invoke-virtual { v0, v1 }, Ljava/io/PrintStream;->println(Ljava/lang/String;)V """ ) - - // Finally, tell the patcher that this patch was a success. - // You can also return PatchResultError with a message. - // If an exception is thrown inside this function, - // a PatchResultError will be returned with the error message. - return PatchResultSuccess() } /** @@ -193,10 +188,5 @@ class ExampleBytecodePatch : BytecodePatch(listOf(ExampleFingerprint)) { "key5", null, "title", "description", true ) ) - private var key6 by option( - PatchOption.PathOption( - "key6", Path("test.txt"), "title", "description", true - ) - ) } } diff --git a/src/test/kotlin/app/revanced/patcher/usage/bytecode/ExampleFingerprint.kt b/revanced-patcher/src/test/kotlin/app/revanced/patcher/usage/bytecode/ExampleFingerprint.kt similarity index 100% rename from src/test/kotlin/app/revanced/patcher/usage/bytecode/ExampleFingerprint.kt rename to revanced-patcher/src/test/kotlin/app/revanced/patcher/usage/bytecode/ExampleFingerprint.kt diff --git a/src/test/kotlin/app/revanced/patcher/usage/resource/annotation/ExampleResourceCompatibility.kt b/revanced-patcher/src/test/kotlin/app/revanced/patcher/usage/resource/annotation/ExampleResourceCompatibility.kt similarity index 100% rename from src/test/kotlin/app/revanced/patcher/usage/resource/annotation/ExampleResourceCompatibility.kt rename to revanced-patcher/src/test/kotlin/app/revanced/patcher/usage/resource/annotation/ExampleResourceCompatibility.kt diff --git a/src/test/kotlin/app/revanced/patcher/usage/resource/patch/ExampleResourcePatch.kt b/revanced-patcher/src/test/kotlin/app/revanced/patcher/usage/resource/patch/ExampleResourcePatch.kt similarity index 74% rename from src/test/kotlin/app/revanced/patcher/usage/resource/patch/ExampleResourcePatch.kt rename to revanced-patcher/src/test/kotlin/app/revanced/patcher/usage/resource/patch/ExampleResourcePatch.kt index 95a4431..b1aadb5 100644 --- a/src/test/kotlin/app/revanced/patcher/usage/resource/patch/ExampleResourcePatch.kt +++ b/revanced-patcher/src/test/kotlin/app/revanced/patcher/usage/resource/patch/ExampleResourcePatch.kt @@ -1,11 +1,11 @@ package app.revanced.patcher.usage.resource.patch +import app.revanced.patcher.ResourceContext import app.revanced.patcher.annotation.Description import app.revanced.patcher.annotation.Name import app.revanced.patcher.annotation.Version -import app.revanced.patcher.data.ResourceContext -import app.revanced.patcher.patch.PatchResult -import app.revanced.patcher.patch.PatchResultSuccess +import app.revanced.patcher.apk.Apk +import app.revanced.patcher.openXmlFile import app.revanced.patcher.patch.ResourcePatch import app.revanced.patcher.patch.annotations.Patch import app.revanced.patcher.usage.resource.annotation.ExampleResourceCompatibility @@ -17,8 +17,8 @@ import org.w3c.dom.Element @ExampleResourceCompatibility @Version("0.0.1") class ExampleResourcePatch : ResourcePatch { - override fun execute(context: ResourceContext): PatchResult { - context.xmlEditor["AndroidManifest.xml"].use { editor -> + override suspend fun execute(context: ResourceContext) { + context.apkBundle.base.resources.openXmlFile(Apk.manifest).use { editor -> val element = editor // regular DomFileEditor .file .getElementsByTagName("application") @@ -29,7 +29,5 @@ class ExampleResourcePatch : ResourcePatch { "exampleValue" ) } - - return PatchResultSuccess() } } \ No newline at end of file diff --git a/src/test/kotlin/app/revanced/patcher/util/smali/InlineSmaliCompilerTest.kt b/revanced-patcher/src/test/kotlin/app/revanced/patcher/util/smali/InlineSmaliCompilerTest.kt similarity index 100% rename from src/test/kotlin/app/revanced/patcher/util/smali/InlineSmaliCompilerTest.kt rename to revanced-patcher/src/test/kotlin/app/revanced/patcher/util/smali/InlineSmaliCompilerTest.kt diff --git a/settings.gradle.kts b/settings.gradle.kts index b4d1a12..027fe23 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1 +1,29 @@ -rootProject.name = "revanced-patcher" +rootProject.name = "ReVanced Patcher" + +pluginManagement { + repositories { + mavenCentral() + maven { + url = uri("https://maven.pkg.github.com/revanced/multidexlib2") + credentials { + username = providers.gradleProperty("gpr.user").orNull ?: System.getenv("GITHUB_ACTOR") + password = providers.gradleProperty("gpr.key").orNull ?: System.getenv("GITHUB_TOKEN") + } + } + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + mavenCentral() + maven { + url = uri("https://maven.pkg.github.com/revanced/multidexlib2") + credentials { + username = providers.gradleProperty("gpr.user").orNull ?: System.getenv("GITHUB_ACTOR") + password = providers.gradleProperty("gpr.key").orNull ?: System.getenv("GITHUB_TOKEN") + } + } + } +} +include("revanced-patcher", "arsclib-utils") diff --git a/src/main/kotlin/app/revanced/patcher/Patcher.kt b/src/main/kotlin/app/revanced/patcher/Patcher.kt deleted file mode 100644 index 0865c62..0000000 --- a/src/main/kotlin/app/revanced/patcher/Patcher.kt +++ /dev/null @@ -1,419 +0,0 @@ -package app.revanced.patcher - -import app.revanced.patcher.data.Context -import app.revanced.patcher.extensions.PatchExtensions.dependencies -import app.revanced.patcher.extensions.PatchExtensions.patchName -import app.revanced.patcher.extensions.PatchExtensions.requiresIntegrations -import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint -import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint.Companion.resolveUsingLookupMap -import app.revanced.patcher.patch.* -import app.revanced.patcher.util.VersionReader -import brut.androlib.Androlib -import brut.androlib.meta.UsesFramework -import brut.androlib.options.BuildOptions -import brut.androlib.res.AndrolibResources -import brut.androlib.res.data.ResPackage -import brut.androlib.res.decoder.AXmlResourceParser -import brut.androlib.res.decoder.ResAttrDecoder -import brut.androlib.res.decoder.XmlPullStreamDecoder -import brut.androlib.res.xml.ResXmlPatcher -import brut.directory.ExtFile -import lanchon.multidexlib2.BasicDexFileNamer -import lanchon.multidexlib2.DexIO -import lanchon.multidexlib2.MultiDexIO -import org.jf.dexlib2.Opcodes -import org.jf.dexlib2.iface.DexFile -import org.jf.dexlib2.writer.io.MemoryDataStore -import java.io.Closeable -import java.io.File -import java.io.OutputStream -import java.nio.file.Files - -internal val NAMER = BasicDexFileNamer() - -/** - * The ReVanced Patcher. - * @param options The options for the patcher. - */ -class Patcher(private val options: PatcherOptions) { - private val logger = options.logger - private val opcodes: Opcodes - private var resourceDecodingMode = ResourceDecodingMode.MANIFEST_ONLY - private var mergeIntegrations = false - val context: PatcherContext - - companion object { - @JvmStatic - val version = VersionReader.read() - private fun BuildOptions.setBuildOptions(options: PatcherOptions) { - this.aaptPath = options.aaptPath - this.useAapt2 = true - this.frameworkFolderLocation = options.frameworkFolderLocation - } - } - - init { - logger.info("Reading dex files") - // read dex files - val dexFile = MultiDexIO.readDexFile(true, options.inputFile, NAMER, null, null) - // get the opcodes - opcodes = dexFile.opcodes - // finally create patcher context - context = PatcherContext(dexFile.classes.toMutableList(), File(options.resourceCacheDirectory)) - - // decode manifest file - decodeResources(ResourceDecodingMode.MANIFEST_ONLY) - } - - /** - * Add integrations to be merged by the patcher. - * The integrations will only be merged, if necessary. - * - * @param integrations The integrations, must be dex files or dex file container such as ZIP, APK or DEX files. - * @param callback The callback for [integrations] which are being added. - */ - fun addIntegrations( - integrations: List, - callback: (File) -> Unit - ) { - context.integrations.apply integrations@{ - add(integrations) - this@integrations.callback = callback - } - } - - /** - * Save the patched dex file. - */ - fun save(): PatcherResult { - val packageMetadata = context.packageMetadata - val metaInfo = packageMetadata.metaInfo - var resourceFile: File? = null - - when (resourceDecodingMode) { - ResourceDecodingMode.FULL -> { - val cacheDirectory = ExtFile(options.resourceCacheDirectory) - try { - val androlibResources = AndrolibResources().also { resources -> - resources.buildOptions = BuildOptions().also { buildOptions -> - buildOptions.setBuildOptions(options) - buildOptions.isFramework = metaInfo.isFrameworkApk - buildOptions.resourcesAreCompressed = metaInfo.compressionType - buildOptions.doNotCompress = metaInfo.doNotCompress - } - - resources.setSdkInfo(metaInfo.sdkInfo) - resources.setVersionInfo(metaInfo.versionInfo) - resources.setSharedLibrary(metaInfo.sharedLibrary) - resources.setSparseResources(metaInfo.sparseResources) - } - - val manifestFile = cacheDirectory.resolve("AndroidManifest.xml") - - ResXmlPatcher.fixingPublicAttrsInProviderAttributes(manifestFile) - - val aaptFile = cacheDirectory.resolve("aapt_temp_file") - - // delete if it exists - Files.deleteIfExists(aaptFile.toPath()) - - val resDirectory = cacheDirectory.resolve("res") - val includedFiles = metaInfo.usesFramework.ids.map { id -> - androlibResources.getFrameworkApk( - id, metaInfo.usesFramework.tag - ) - }.toTypedArray() - - logger.info("Compiling resources") - androlibResources.aaptPackage( - aaptFile, manifestFile, resDirectory, null, null, includedFiles - ) - - resourceFile = aaptFile - } finally { - cacheDirectory.close() - } - } - - else -> logger.info("Not compiling resources because resource patching is not required") - } - - logger.trace("Creating new dex file") - val newDexFile = object : DexFile { - override fun getClasses() = context.bytecodeContext.classes.also { it.replaceClasses() } - override fun getOpcodes() = this@Patcher.opcodes - } - - // write modified dex files - logger.info("Writing modified dex files") - val dexFiles = mutableMapOf() - MultiDexIO.writeDexFile( - true, -1, // core count - dexFiles, NAMER, newDexFile, DexIO.DEFAULT_MAX_DEX_POOL_SIZE, null - ) - - return PatcherResult( - dexFiles.map { - app.revanced.patcher.util.dex.DexFile(it.key, it.value.readAt(0)) - }, - metaInfo.doNotCompress?.toList(), - resourceFile - ) - } - - /** - * Add [Patch]es to the patcher. - * @param patches [Patch]es The patches to add. - */ - fun addPatches(patches: Iterable>>) { - /** - * Returns true if at least one patches or its dependencies matches the given predicate. - */ - fun Class>.anyRecursively(predicate: (Class>) -> Boolean): Boolean = - predicate(this) || dependencies?.any { it.java.anyRecursively(predicate) } == true - - - // Determine if resource patching is required. - for (patch in patches) { - if (patch.anyRecursively { ResourcePatch::class.java.isAssignableFrom(it) }) { - resourceDecodingMode = ResourceDecodingMode.FULL - break - } - } - - // Determine if merging integrations is required. - for (patch in patches) { - if (patch.anyRecursively { it.requiresIntegrations }) { - mergeIntegrations = true - break - } - } - - context.patches.addAll(patches) - } - - /** - * Decode resources for the patcher. - * - * @param mode The [ResourceDecodingMode] to use when decoding. - */ - private fun decodeResources(mode: ResourceDecodingMode) { - val extInputFile = ExtFile(options.inputFile) - try { - val androlib = Androlib(BuildOptions().also { it.setBuildOptions(options) }) - val resourceTable = androlib.getResTable(extInputFile, true) - when (mode) { - ResourceDecodingMode.FULL -> { - val outDir = File(options.resourceCacheDirectory) - if (outDir.exists()) { - logger.info("Deleting existing resource cache directory") - if (!outDir.deleteRecursively()) { - logger.error("Failed to delete existing resource cache directory") - } - } - outDir.mkdirs() - - logger.info("Decoding resources") - - // decode resources to cache directory - androlib.decodeManifestWithResources(extInputFile, outDir, resourceTable) - androlib.decodeResourcesFull(extInputFile, outDir, resourceTable) - - // read additional metadata from the resource table - context.packageMetadata.let { metadata -> - metadata.metaInfo.usesFramework = UsesFramework().also { framework -> - framework.ids = resourceTable.listFramePackages().map { it.id }.sorted() - } - - // read files to not compress - metadata.metaInfo.doNotCompress = buildList { - androlib.recordUncompressedFiles(extInputFile, this) - } - } - - } - - ResourceDecodingMode.MANIFEST_ONLY -> { - logger.info("Decoding AndroidManifest.xml only, because resources are not needed") - - // create decoder for the resource table - val decoder = ResAttrDecoder() - decoder.currentPackage = ResPackage(resourceTable, 0, null) - - // create xml parser with the decoder - val axmlParser = AXmlResourceParser() - axmlParser.attrDecoder = decoder - - // parse package information with the decoder and parser which will set required values in the resource table - // instead of decodeManifest another more low level solution can be created to make it faster/better - XmlPullStreamDecoder( - axmlParser, AndrolibResources().resXmlSerializer - ).decodeManifest( - extInputFile.directory.getFileInput("AndroidManifest.xml"), - // Older Android versions do not support OutputStream.nullOutputStream() - object : OutputStream() { - override fun write(b: Int) { - // do nothing - } - } - ) - } - } - - // read of the resourceTable which is created by reading the manifest file - context.packageMetadata.let { metadata -> - metadata.packageName = resourceTable.currentResPackage.name - metadata.packageVersion = resourceTable.versionInfo.versionName ?: resourceTable.versionInfo.versionCode - metadata.metaInfo.versionInfo = resourceTable.versionInfo - metadata.metaInfo.sdkInfo = resourceTable.sdkInfo - } - } finally { - extInputFile.close() - } - } - - /** - * Execute patches added the patcher. - * - * @param stopOnError If true, the patches will stop on the first error. - * @return A pair of the name of the [Patch] and its [PatchResult]. - */ - fun executePatches(stopOnError: Boolean = false): Sequence>> { - /** - * Execute a [Patch] and its dependencies recursively. - * - * @param patchClass The [Patch] to execute. - * @param executedPatches A map of [Patch]es paired to a boolean indicating their success, to prevent infinite recursion. - * @return The result of executing the [Patch]. - */ - fun executePatch( - patchClass: Class>, - executedPatches: LinkedHashMap - ): PatchResult { - val patchName = patchClass.patchName - - // if the patch has already applied silently skip it - if (executedPatches.contains(patchName)) { - if (!executedPatches[patchName]!!.success) - return PatchResultError("'$patchName' did not succeed previously") - - logger.trace("Skipping '$patchName' because it has already been applied") - - return PatchResultSuccess() - } - - // recursively execute all dependency patches - patchClass.dependencies?.forEach { dependencyClass -> - val dependency = dependencyClass.java - - val result = executePatch(dependency, executedPatches) - if (result.isSuccess()) return@forEach - - return PatchResultError( - "'$patchName' depends on '${dependency.patchName}' but the following error was raised: " + - result.error()!!.let { it.cause?.stackTraceToString() ?: it.message } - ) - } - - val isResourcePatch = ResourcePatch::class.java.isAssignableFrom(patchClass) - val patchInstance = patchClass.getDeclaredConstructor().newInstance() - - // TODO: implement this in a more polymorphic way - val patchContext = if (isResourcePatch) { - context.resourceContext - } else { - context.bytecodeContext.also { context -> - (patchInstance as BytecodePatch).fingerprints?.resolveUsingLookupMap(context) - } - } - - logger.trace("Executing '$patchName' of type: ${if (isResourcePatch) "resource" else "bytecode"}") - - return try { - patchInstance.execute(patchContext).also { - executedPatches[patchName] = ExecutedPatch(patchInstance, it.isSuccess()) - } - } catch (e: Exception) { - PatchResultError(e).also { - executedPatches[patchName] = ExecutedPatch(patchInstance, false) - } - } - } - - return sequence { - if (mergeIntegrations) context.integrations.merge(logger) - - logger.trace("Initialize lookup maps for method MethodFingerprint resolution") - - MethodFingerprint.initializeFingerprintResolutionLookupMaps(context.bytecodeContext) - - // prevent from decoding the manifest twice if it is not needed - if (resourceDecodingMode == ResourceDecodingMode.FULL) decodeResources(ResourceDecodingMode.FULL) - - logger.info("Executing patches") - - val executedPatches = LinkedHashMap() // first is name - - context.patches.forEach { patch -> - val patchResult = executePatch(patch, executedPatches) - - val result = if (patchResult.isSuccess()) { - Result.success(patchResult.success()!!) - } else { - Result.failure(patchResult.error()!!) - } - - // TODO: This prints before the patch really finishes in case it is a Closeable - // because the Closeable is closed after all patches are executed. - yield(patch.patchName to result) - - if (stopOnError && patchResult.isError()) return@sequence - } - - executedPatches.values - .filter(ExecutedPatch::success) - .map(ExecutedPatch::patchInstance) - .filterIsInstance(Closeable::class.java) - .asReversed().forEach { - try { - it.close() - } catch (exception: Exception) { - val patchName = (it as Patch).javaClass.patchName - - logger.error("Failed to close '$patchName': ${exception.stackTraceToString()}") - - yield(patchName to Result.failure(exception)) - - // This is not failsafe. If a patch throws an exception while closing, - // the other patches that depend on it may fail. - if (stopOnError) return@sequence - } - } - - MethodFingerprint.clearFingerprintResolutionLookupMaps() - } - } - - /** - * The type of decoding the resources. - */ - private enum class ResourceDecodingMode { - /** - * Decode all resources. - */ - FULL, - - /** - * Decode the manifest file only. - */ - MANIFEST_ONLY, - } -} - -/** - * A result of executing a [Patch]. - * - * @param patchInstance The instance of the [Patch] that was applied. - * @param success The result of the [Patch]. - */ -internal data class ExecutedPatch(val patchInstance: Patch, val success: Boolean) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/PatcherContext.kt b/src/main/kotlin/app/revanced/patcher/PatcherContext.kt deleted file mode 100644 index 941607d..0000000 --- a/src/main/kotlin/app/revanced/patcher/PatcherContext.kt +++ /dev/null @@ -1,64 +0,0 @@ -package app.revanced.patcher - -import app.revanced.patcher.data.* -import app.revanced.patcher.logging.Logger -import app.revanced.patcher.patch.Patch -import app.revanced.patcher.util.ClassMerger.merge -import org.jf.dexlib2.iface.ClassDef -import java.io.File - -data class PatcherContext( - val classes: MutableList, - val resourceCacheDirectory: File, -) { - val packageMetadata = PackageMetadata() - internal val patches = mutableListOf>>() - internal val integrations = Integrations(this) - internal val bytecodeContext = BytecodeContext(classes) - internal val resourceContext = ResourceContext(resourceCacheDirectory) - - internal class Integrations(val context: PatcherContext) { - var callback: ((File) -> Unit)? = null - private val integrations: MutableList = mutableListOf() - - fun add(integrations: List) = this@Integrations.integrations.addAll(integrations) - - /** - * Merge integrations. - * @param logger A logger. - */ - fun merge(logger: Logger) { - with(context.bytecodeContext.classes) { - for (integrations in integrations) { - callback?.let { it(integrations) } - - for (classDef in lanchon.multidexlib2.MultiDexIO.readDexFile( - true, - integrations, - NAMER, - null, - null - ).classes) { - val type = classDef.type - - val result = classes.findIndexed { it.type == type } - if (result == null) { - logger.trace("Merging type $type") - classes.add(classDef) - continue - } - - val (existingClass, existingClassIndex) = result - - logger.trace("Type $type exists. Adding missing methods and fields.") - - existingClass.merge(classDef, context, logger).let { mergedClass -> - if (mergedClass !== existingClass) // referential equality check - classes[existingClassIndex] = mergedClass - } - } - } - } - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/PatcherOptions.kt b/src/main/kotlin/app/revanced/patcher/PatcherOptions.kt deleted file mode 100644 index 4e39733..0000000 --- a/src/main/kotlin/app/revanced/patcher/PatcherOptions.kt +++ /dev/null @@ -1,21 +0,0 @@ -package app.revanced.patcher - -import app.revanced.patcher.logging.Logger -import app.revanced.patcher.logging.impl.NopLogger -import java.io.File - -/** - * Options for the [Patcher]. - * @param inputFile The input file (usually an apk file). - * @param resourceCacheDirectory Directory to cache resources. - * @param aaptPath Optional path to a custom aapt binary. - * @param frameworkFolderLocation Optional path to a custom framework folder. - * @param logger Custom logger implementation for the [Patcher]. - */ -data class PatcherOptions( - internal val inputFile: File, - internal val resourceCacheDirectory: String, - internal val aaptPath: String? = null, - internal val frameworkFolderLocation: String? = null, - internal val logger: Logger = NopLogger -) diff --git a/src/main/kotlin/app/revanced/patcher/PatcherResult.kt b/src/main/kotlin/app/revanced/patcher/PatcherResult.kt deleted file mode 100644 index b7007d6..0000000 --- a/src/main/kotlin/app/revanced/patcher/PatcherResult.kt +++ /dev/null @@ -1,16 +0,0 @@ -package app.revanced.patcher - -import app.revanced.patcher.util.dex.DexFile -import java.io.File - -/** - * The result of a patcher. - * @param dexFiles The patched dex files. - * @param doNotCompress List of relative paths to files to exclude from compressing. - * @param resourceFile File containing resources that need to be extracted into the APK. - */ -data class PatcherResult( - val dexFiles: List, - val doNotCompress: List? = null, - val resourceFile: File? -) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/data/Context.kt b/src/main/kotlin/app/revanced/patcher/data/Context.kt deleted file mode 100644 index 5083a42..0000000 --- a/src/main/kotlin/app/revanced/patcher/data/Context.kt +++ /dev/null @@ -1,170 +0,0 @@ -package app.revanced.patcher.data - -import app.revanced.patcher.util.ProxyBackedClassList -import app.revanced.patcher.util.method.MethodWalker -import org.jf.dexlib2.iface.ClassDef -import org.jf.dexlib2.iface.Method -import org.w3c.dom.Document -import java.io.Closeable -import java.io.File -import java.io.InputStream -import java.io.OutputStream -import javax.xml.parsers.DocumentBuilderFactory -import javax.xml.transform.TransformerFactory -import javax.xml.transform.dom.DOMSource -import javax.xml.transform.stream.StreamResult - -/** - * A common interface to constrain [Context] to [BytecodeContext] and [ResourceContext]. - */ - -sealed interface Context - -class BytecodeContext internal constructor(classes: MutableList) : Context { - /** - * The list of classes. - */ - val classes = ProxyBackedClassList(classes) - - /** - * 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) } - - fun proxy(classDef: ClassDef): app.revanced.patcher.util.proxy.ClassProxy { - var proxy = this.classes.proxies.find { it.immutableClass.type == classDef.type } - if (proxy == null) { - proxy = app.revanced.patcher.util.proxy.ClassProxy(classDef) - this.classes.add(proxy) - } - return proxy - } -} - -/** - * Create a [MethodWalker] instance for the current [BytecodeContext]. - * - * @param startMethod The method to start at. - * @return A [MethodWalker] instance. - */ -fun BytecodeContext.toMethodWalker(startMethod: Method): MethodWalker { - return MethodWalker(this, startMethod) -} - -internal inline fun Iterable.findIndexed(predicate: (T) -> Boolean): Pair? { - for ((index, element) in this.withIndex()) { - if (predicate(element)) { - return element to index - } - } - return null -} - -class ResourceContext internal constructor(private val resourceCacheDirectory: File) : Context, Iterable { - val xmlEditor = XmlFileHolder() - - operator fun get(path: String) = resourceCacheDirectory.resolve(path) - - override fun iterator() = resourceCacheDirectory.walkTopDown().iterator() - - inner class XmlFileHolder { - operator fun get(inputStream: InputStream) = - DomFileEditor(inputStream) - - operator fun get(path: String): DomFileEditor { - return DomFileEditor(this@ResourceContext[path]) - } - - } -} - -/** - * Wrapper for a file that can be edited as a dom document. - * - * This constructor does not check for locks to the file when writing. - * Use the secondary constructor. - * - * @param inputStream the input stream to read the xml file from. - * @param outputStream the output stream to write the xml file to. If null, the file will be read only. - * - */ -class DomFileEditor internal constructor( - private val inputStream: InputStream, - private val outputStream: Lazy? = null, -) : Closeable { - // path to the xml file to unlock the resource when closing the editor - private var filePath: String? = null - private var closed: Boolean = false - - /** - * The document of the xml file - */ - val file: Document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputStream) - .also(Document::normalize) - - - // 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 - // the workaround is to lazily create the output stream. This way it would be used after the input stream is closed, which happens in the constructor - constructor(file: File) : this(file.inputStream(), lazy { file.outputStream() }) { - // increase the lock - locks.merge(file.path, 1, Integer::sum) - filePath = file.path - } - - /** - * Closes the editor. Write backs and decreases the lock count. - * - * Will not write back to the file if the file is still locked. - */ - override fun close() { - if (closed) return - - inputStream.close() - - // if the output stream is not null, do not close it - outputStream?.let { - // prevent writing to same file, if it is being locked - // isLocked will be false if the editor was created through a stream - val isLocked = filePath?.let { path -> - val isLocked = locks[path]!! > 1 - // decrease the lock count if the editor was opened for a file - locks.merge(path, -1, Integer::sum) - isLocked - } ?: false - - // if unlocked, write back to the file - if (!isLocked) { - it.value.use { stream -> - val result = StreamResult(stream) - TransformerFactory.newInstance().newTransformer().transform(DOMSource(file), result) - } - - it.value.close() - return - } - } - - closed = true - } - - private companion object { - // map of concurrent open files - val locks = mutableMapOf() - } -} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/data/PackageMetadata.kt b/src/main/kotlin/app/revanced/patcher/data/PackageMetadata.kt deleted file mode 100644 index eaa1601..0000000 --- a/src/main/kotlin/app/revanced/patcher/data/PackageMetadata.kt +++ /dev/null @@ -1,13 +0,0 @@ -package app.revanced.patcher.data - -import brut.androlib.meta.MetaInfo - -/** - * Metadata about a package. - */ -class PackageMetadata { - lateinit var packageName: String - lateinit var packageVersion: String - - internal val metaInfo: MetaInfo = MetaInfo() -} diff --git a/src/main/kotlin/app/revanced/patcher/logging/Logger.kt b/src/main/kotlin/app/revanced/patcher/logging/Logger.kt deleted file mode 100644 index 63295e6..0000000 --- a/src/main/kotlin/app/revanced/patcher/logging/Logger.kt +++ /dev/null @@ -1,8 +0,0 @@ -package app.revanced.patcher.logging - -interface Logger { - fun error(msg: String) {} - fun warn(msg: String) {} - fun info(msg: String) {} - fun trace(msg: String) {} -} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/logging/impl/NopLogger.kt b/src/main/kotlin/app/revanced/patcher/logging/impl/NopLogger.kt deleted file mode 100644 index af88ab9..0000000 --- a/src/main/kotlin/app/revanced/patcher/logging/impl/NopLogger.kt +++ /dev/null @@ -1,5 +0,0 @@ -package app.revanced.patcher.logging.impl - -import app.revanced.patcher.logging.Logger - -object NopLogger : Logger \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/patch/PatchResult.kt b/src/main/kotlin/app/revanced/patcher/patch/PatchResult.kt deleted file mode 100644 index b177170..0000000 --- a/src/main/kotlin/app/revanced/patcher/patch/PatchResult.kt +++ /dev/null @@ -1,35 +0,0 @@ -package app.revanced.patcher.patch - -interface PatchResult { - fun error(): PatchResultError? { - if (this is PatchResultError) { - return this - } - return null - } - - fun success(): PatchResultSuccess? { - if (this is PatchResultSuccess) { - return this - } - return null - } - - fun isError(): Boolean { - return this is PatchResultError - } - - fun isSuccess(): Boolean { - return this is PatchResultSuccess - } -} - -class PatchResultError( - errorMessage: String?, cause: Exception? -) : Exception(errorMessage, cause), PatchResult { - constructor(errorMessage: String) : this(errorMessage, null) - constructor(cause: Exception) : this(cause.message, cause) - -} - -class PatchResultSuccess : PatchResult \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/util/ProxyBackedClassList.kt b/src/main/kotlin/app/revanced/patcher/util/ProxyBackedClassList.kt deleted file mode 100644 index ab9f540..0000000 --- a/src/main/kotlin/app/revanced/patcher/util/ProxyBackedClassList.kt +++ /dev/null @@ -1,46 +0,0 @@ -package app.revanced.patcher.util - -import app.revanced.patcher.util.proxy.ClassProxy -import org.jf.dexlib2.iface.ClassDef - -/** - * A class that represents a set of classes and proxies. - * - * @param classes The classes to be backed by proxies. - */ -class ProxyBackedClassList(internal val classes: MutableList) : Set { - internal val proxies = mutableListOf() - - /** - * Add a [ClassDef]. - */ - fun add(classDef: ClassDef) = classes.add(classDef) - - /** - * Add a [ClassProxy]. - */ - fun add(classProxy: ClassProxy) = proxies.add(classProxy) - - /** - * Replace all classes with their mutated versions. - */ - internal fun replaceClasses() = - proxies.removeIf { proxy -> - // if the proxy is unused, keep it in the list - if (!proxy.resolved) return@removeIf false - - // if it has been used, replace the original class with the new class - val index = classes.indexOfFirst { it.type == proxy.immutableClass.type } - classes[index] = proxy.mutableClass - - // return true to remove it from the proxies list - return@removeIf true - } - - - override val size get() = classes.size - override fun contains(element: ClassDef) = classes.contains(element) - override fun containsAll(elements: Collection) = classes.containsAll(elements) - override fun isEmpty() = classes.isEmpty() - override fun iterator() = classes.iterator() -} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/util/TypeUtil.kt b/src/main/kotlin/app/revanced/patcher/util/TypeUtil.kt deleted file mode 100644 index 87b5edc..0000000 --- a/src/main/kotlin/app/revanced/patcher/util/TypeUtil.kt +++ /dev/null @@ -1,19 +0,0 @@ -package app.revanced.patcher.util - -import app.revanced.patcher.data.BytecodeContext -import app.revanced.patcher.util.proxy.mutableTypes.MutableClass - -object TypeUtil { - /** - * traverse the class hierarchy starting from the given root class - * - * @param targetClass the class to start traversing the class hierarchy from - * @param callback function that is called for every class in the hierarchy - */ - fun BytecodeContext.traverseClassHierarchy(targetClass: MutableClass, callback: MutableClass.() -> Unit) { - callback(targetClass) - this.findClass(targetClass.superclass ?: return)?.mutableClass?.let { - traverseClassHierarchy(it, callback) - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/util/VersionReader.kt b/src/main/kotlin/app/revanced/patcher/util/VersionReader.kt deleted file mode 100644 index 6fc15a5..0000000 --- a/src/main/kotlin/app/revanced/patcher/util/VersionReader.kt +++ /dev/null @@ -1,18 +0,0 @@ -package app.revanced.patcher.util - -import java.util.* - -internal object VersionReader { - @JvmStatic - private val props = Properties().apply { - load( - VersionReader::class.java.getResourceAsStream("/revanced-patcher/version.properties") - ?: throw IllegalStateException("Could not load version.properties") - ) - } - - @JvmStatic - fun read(): String { - return props.getProperty("version") ?: throw IllegalStateException("Version not found") - } -} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/util/dex/DexFile.kt b/src/main/kotlin/app/revanced/patcher/util/dex/DexFile.kt deleted file mode 100644 index ec8ef00..0000000 --- a/src/main/kotlin/app/revanced/patcher/util/dex/DexFile.kt +++ /dev/null @@ -1,10 +0,0 @@ -package app.revanced.patcher.util.dex - -import java.io.InputStream - -/** - * Wrapper for dex files. - * @param name The original name of the dex file. - * @param stream The dex file as [InputStream]. - */ -data class DexFile(val name: String, val stream: InputStream) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/util/patch/PatchBundle.kt b/src/main/kotlin/app/revanced/patcher/util/patch/PatchBundle.kt deleted file mode 100644 index f334486..0000000 --- a/src/main/kotlin/app/revanced/patcher/util/patch/PatchBundle.kt +++ /dev/null @@ -1,76 +0,0 @@ -@file:Suppress("unused") - -package app.revanced.patcher.util.patch - -import app.revanced.patcher.data.Context -import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively -import app.revanced.patcher.extensions.PatchExtensions.patchName -import app.revanced.patcher.patch.Patch -import org.jf.dexlib2.DexFileFactory -import java.io.File -import java.net.URLClassLoader -import java.util.jar.JarFile - -/** - * A patch bundle. - - * @param path The path to the patch bundle. - */ -sealed class PatchBundle(path: String) : File(path) { - internal fun loadPatches(classLoader: ClassLoader, classNames: Iterator) = buildList { - classNames.forEach { className -> - val clazz = classLoader.loadClass(className) - - // Annotations can not Patch. - if (clazz.isAnnotation) return@forEach - - clazz.findAnnotationRecursively(app.revanced.patcher.patch.annotations.Patch::class) - ?: return@forEach - - @Suppress("UNCHECKED_CAST") this.add(clazz as Class>) - } - }.sortedBy { it.patchName } - - /** - * A patch bundle of type [Jar]. - * - * @param patchBundlePath The path to the patch bundle. - */ - class Jar(patchBundlePath: String) : PatchBundle(patchBundlePath) { - - /** - * Load patches from the patch bundle. - * - * Patches will be loaded with a new [URLClassLoader]. - */ - fun loadPatches() = loadPatches( - URLClassLoader( - arrayOf(this.toURI().toURL()), - Thread.currentThread().contextClassLoader // TODO: find out why this is required - ), - JarFile(this) - .stream() - .filter { it.name.endsWith(".class") && !it.name.contains("$") } - .map { it.realName.replace('/', '.').replace(".class", "") }.iterator() - ) - } - - /** - * A patch bundle of type [Dex] format. - * - * @param patchBundlePath The path to a patch bundle of dex format. - * @param dexClassLoader The dex class loader. - */ - class Dex(patchBundlePath: String, private val dexClassLoader: ClassLoader) : PatchBundle(patchBundlePath) { - /** - * Load patches from the patch bundle. - * - * Patches will be loaded to the provided [dexClassLoader]. - */ - fun loadPatches() = loadPatches(dexClassLoader, - DexFileFactory.loadDexFile(path, null).classes.asSequence().map { classDef -> - classDef.type.substring(1, classDef.length - 1).replace('/', '.') - }.iterator() - ) - } -} \ No newline at end of file diff --git a/src/test/kotlin/app/revanced/patcher/extensions/InstructionExtensionsTest.kt b/src/test/kotlin/app/revanced/patcher/extensions/InstructionExtensionsTest.kt deleted file mode 100644 index be77fcc..0000000 --- a/src/test/kotlin/app/revanced/patcher/extensions/InstructionExtensionsTest.kt +++ /dev/null @@ -1,233 +0,0 @@ -package app.revanced.patcher.extensions - -import app.revanced.patcher.extensions.InstructionExtensions.addInstruction -import app.revanced.patcher.extensions.InstructionExtensions.addInstructions -import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels -import app.revanced.patcher.extensions.InstructionExtensions.getInstruction -import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction -import app.revanced.patcher.extensions.InstructionExtensions.removeInstructions -import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction -import app.revanced.patcher.extensions.InstructionExtensions.replaceInstructions -import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod -import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable -import app.revanced.patcher.util.smali.ExternalLabel -import org.jf.dexlib2.AccessFlags -import org.jf.dexlib2.Opcode -import org.jf.dexlib2.builder.BuilderOffsetInstruction -import org.jf.dexlib2.builder.MutableMethodImplementation -import org.jf.dexlib2.builder.instruction.BuilderInstruction21s -import org.jf.dexlib2.iface.instruction.OneRegisterInstruction -import org.jf.dexlib2.immutable.ImmutableMethod -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import kotlin.test.assertEquals - -private object InstructionExtensionsTest { - private lateinit var testMethod: MutableMethod - private lateinit var testMethodImplementation: MutableMethodImplementation - - @BeforeEach - fun createTestMethod() = ImmutableMethod( - "TestClass;", - "testMethod", - null, - "V", - AccessFlags.PUBLIC.value, - null, - null, - MutableMethodImplementation(16).also { testMethodImplementation = it }.apply { - repeat(10) { i -> this.addInstruction(TestInstruction(i)) } - }, - ).let { testMethod = it.toMutable() } - - @Test - fun addInstructionsToImplementationIndexed() = applyToImplementation { - addInstructions(5, getTestInstructions(5..6)).also { - assertRegisterIs(5, 5) - assertRegisterIs(6, 6) - - assertRegisterIs(5, 7) - } - } - - @Test - fun addInstructionsToImplementation() = applyToImplementation { - addInstructions(getTestInstructions(10..11)).also { - assertRegisterIs(10, 10) - assertRegisterIs(11, 11) - } - } - - @Test - fun removeInstructionsFromImplementationIndexed() = applyToImplementation { - removeInstructions(5, 5).also { assertRegisterIs(4, 4) } - } - - @Test - fun removeInstructionsFromImplementation() = applyToImplementation { - removeInstructions(0).also { assertRegisterIs(9, 9) } - removeInstructions(1).also { assertRegisterIs(1, 0) } - removeInstructions(2).also { assertRegisterIs(3, 0) } - } - - @Test - fun replaceInstructionsInImplementationIndexed() = applyToImplementation { - replaceInstructions(5, getTestInstructions(0..1)).also { - assertRegisterIs(0, 5) - assertRegisterIs(1, 6) - assertRegisterIs(7, 7) - } - } - - @Test - fun addInstructionToMethodIndexed() = applyToMethod { - addInstruction(5, TestInstruction(0)).also { assertRegisterIs(0, 5) } - } - - @Test - fun addInstructionToMethod() = applyToMethod { - addInstruction(TestInstruction(0)).also { assertRegisterIs(0, 10) } - } - - @Test - fun addSmaliInstructionToMethodIndexed() = applyToMethod { - addInstruction(5, getTestSmaliInstruction(0)).also { assertRegisterIs(0, 5) } - } - - @Test - fun addSmaliInstructionToMethod() = applyToMethod { - addInstruction(getTestSmaliInstruction(0)).also { assertRegisterIs(0, 10) } - } - - @Test - fun addInstructionsToMethodIndexed() = applyToMethod { - addInstructions(5, getTestInstructions(0..1)).also { - assertRegisterIs(0, 5) - assertRegisterIs(1, 6) - - assertRegisterIs(5, 7) - } - } - - @Test - fun addInstructionsToMethod() = applyToMethod { - addInstructions(getTestInstructions(0..1)).also { - assertRegisterIs(0, 10) - assertRegisterIs(1, 11) - - assertRegisterIs(9, 9) - } - } - - @Test - fun addSmaliInstructionsToMethodIndexed() = applyToMethod { - addInstructionsWithLabels(5, getTestSmaliInstructions(0..1)).also { - assertRegisterIs(0, 5) - assertRegisterIs(1, 6) - - assertRegisterIs(5, 7) - } - } - - @Test - fun addSmaliInstructionsToMethod() = applyToMethod { - addInstructions(getTestSmaliInstructions(0..1)).also { - assertRegisterIs(0, 10) - assertRegisterIs(1, 11) - - assertRegisterIs(9, 9) - } - } - - @Test - fun addSmaliInstructionsWithExternalLabelToMethodIndexed() = applyToMethod { - val label = ExternalLabel("testLabel", getInstruction(5)) - - addInstructionsWithLabels( - 5, - getTestSmaliInstructions(0..1).plus("\n").plus("goto :${label.name}"), - label - ).also { - assertRegisterIs(0, 5) - assertRegisterIs(1, 6) - assertRegisterIs(5, 8) - - val gotoTarget = getInstruction(7) - .target.location.instruction as OneRegisterInstruction - - assertEquals(5, gotoTarget.registerA) - } - } - - @Test - fun removeInstructionFromMethodIndexed() = applyToMethod { - removeInstruction(5).also { - assertRegisterIs(4, 4) - assertRegisterIs(6, 5) - } - } - - @Test - fun removeInstructionsFromMethodIndexed() = applyToMethod { - removeInstructions(5, 5).also { assertRegisterIs(4, 4) } - } - - @Test - fun removeInstructionsFromMethod() = applyToMethod { - removeInstructions(0).also { assertRegisterIs(9, 9) } - removeInstructions(1).also { assertRegisterIs(1, 0) } - removeInstructions(2).also { assertRegisterIs(3, 0) } - } - - @Test - fun replaceInstructionInMethodIndexed() = applyToMethod { - replaceInstruction(5, TestInstruction(0)).also { assertRegisterIs(0, 5) } - } - - @Test - fun replaceInstructionsInMethodIndexed() = applyToMethod { - replaceInstructions(5, getTestInstructions(0..1)).also { - assertRegisterIs(0, 5) - assertRegisterIs(1, 6) - assertRegisterIs(7, 7) - } - } - - @Test - fun replaceSmaliInstructionsInMethodIndexed() = applyToMethod { - replaceInstructions(5, getTestSmaliInstructions(0..1)).also { - assertRegisterIs(0, 5) - assertRegisterIs(1, 6) - assertRegisterIs(7, 7) - } - } - - // region Helper methods - - private fun applyToImplementation(block: MutableMethodImplementation.() -> Unit) { - testMethodImplementation.apply(block) - } - - private fun applyToMethod(block: MutableMethod.() -> Unit) { - testMethod.apply(block) - } - - private fun MutableMethodImplementation.assertRegisterIs(register: Int, atIndex: Int) = assertEquals( - register, getInstruction(atIndex).registerA - ) - - private fun MutableMethod.assertRegisterIs(register: Int, atIndex: Int) = - implementation!!.assertRegisterIs(register, atIndex) - - private fun getTestInstructions(range: IntRange) = range.map { TestInstruction(it) } - - private fun getTestSmaliInstruction(register: Int) = "const/16 v$register, 0" - - private fun getTestSmaliInstructions(range: IntRange) = range.joinToString("\n") { - getTestSmaliInstruction(it) - } - - // endregion - - private class TestInstruction(register: Int) : BuilderInstruction21s(Opcode.CONST_16, register, 0) -} \ No newline at end of file