From 740911a2a38be3595efde5064b65ca6edcdfd34d Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Sun, 16 Jul 2023 05:25:39 +0200 Subject: [PATCH] refactor: move stuff around and improve memory profile/performance --- .../kotlin/app/revanced/arsc/ApkException.kt | 42 --------- .../app/revanced/arsc/ApkResourceException.kt | 49 +++++++++++ .../app/revanced/arsc/archive/Archive.kt | 76 ++++++++++------ .../app/revanced/arsc/resource/Resource.kt | 10 +-- .../arsc/resource/ResourceContainer.kt | 46 +++++----- .../revanced/arsc/resource/ResourceFile.kt | 86 +++++++++---------- .../revanced/arsc/resource/ResourceTable.kt | 10 +-- .../revanced/arsc/xml/LazyXMLInputSource.kt | 6 +- .../kotlin/app/revanced/patcher/apk/Apk.kt | 55 ++++++------ .../app/revanced/patcher/apk/ApkBundle.kt | 36 +++----- .../resource/patch/ExampleResourcePatch.kt | 2 +- 11 files changed, 220 insertions(+), 198 deletions(-) delete mode 100644 arsclib-utils/src/main/kotlin/app/revanced/arsc/ApkException.kt create mode 100644 arsclib-utils/src/main/kotlin/app/revanced/arsc/ApkResourceException.kt diff --git a/arsclib-utils/src/main/kotlin/app/revanced/arsc/ApkException.kt b/arsclib-utils/src/main/kotlin/app/revanced/arsc/ApkException.kt deleted file mode 100644 index 7c56bfd..0000000 --- a/arsclib-utils/src/main/kotlin/app/revanced/arsc/ApkException.kt +++ /dev/null @@ -1,42 +0,0 @@ -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/ApkResourceException.kt b/arsclib-utils/src/main/kotlin/app/revanced/arsc/ApkResourceException.kt new file mode 100644 index 0000000..fc660c7 --- /dev/null +++ b/arsclib-utils/src/main/kotlin/app/revanced/arsc/ApkResourceException.kt @@ -0,0 +1,49 @@ +package app.revanced.arsc + +/** + * An exception thrown when there is an error with APK resources. + * + * @param message The exception message. + * @param throwable The corresponding [Throwable]. + */ +sealed class ApkResourceException(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) : ApkResourceException(message, throwable) + + /** + * An exception when encoding resources. + * + * @param message The exception message. + * @param throwable The corresponding [Throwable]. + */ + class Encode(message: String, throwable: Throwable? = null) : ApkResourceException(message, throwable) + + /** + * An exception thrown when a reference could not be resolved. + * + * @param reference The invalid reference. + * @param throwable The corresponding [Throwable]. + */ + class InvalidReference(reference: String, throwable: Throwable? = null) : + ApkResourceException("Failed to resolve: $reference", throwable) { + + /** + * An exception thrown when a reference could not be resolved. + * + * @param type The type of the reference. + * @param name The name of the reference. + * @param throwable The corresponding [Throwable]. + */ + constructor(type: String, name: String, throwable: Throwable? = null) : this("@$type/$name", throwable) + } + + /** + * An exception thrown when the Apk file not have a resource table, but was expected to have one. + */ + object MissingResourceTable : ApkResourceException("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 index 10db972..1d23c44 100644 --- a/arsclib-utils/src/main/kotlin/app/revanced/arsc/archive/Archive.kt +++ b/arsclib-utils/src/main/kotlin/app/revanced/arsc/archive/Archive.kt @@ -1,20 +1,20 @@ +@file:Suppress("MemberVisibilityCanBePrivate") + package app.revanced.arsc.archive -import app.revanced.arsc.ApkException +import app.revanced.arsc.ApkResourceException 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.Closeable import java.io.File -private fun isResXml(inputSource: InputSource) = inputSource.openStream().use { ResXmlDocument.isResXmlBlock(it) } - /** * A class for reading/writing files in an [ApkModule]. * @@ -23,14 +23,6 @@ private fun isResXml(inputSource: InputSource) = inputSource.openStream().use { 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. */ @@ -44,7 +36,7 @@ class Archive(private val module: ApkModule) { 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.") + throw ApkResourceException.Decode("${file.handle.virtualPath} is locked. If you are a patch developer, make sure you always close files.") } lockedFiles[path] = file } @@ -82,23 +74,21 @@ class Archive(private val module: ApkModule) { * Read an entry from the archive. * * @param path The archive path to read from. - * @return A [ReadResult] containing the contents of the entry. + * @return A [ArchiveResource] 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) + fun read(path: String) = archive.getInputSource(path)?.let { inputSource -> + when { + inputSource is LazyXMLInputSource -> ArchiveResource.XmlResource(inputSource.document) - else -> null - } + ResXmlDocument.isResXmlBlock(inputSource.openStream()) -> ArchiveResource.XmlResource( + module + .loadResXmlDocument(inputSource) + .decodeToXml(resources.resourceTable.entryStore, resources.packageBlock?.id ?: 0) + ) - ReadResult( - xml != null, - xml?.toText()?.toByteArray() ?: inputSource.openStream().use { it.readAllBytes() }) + else -> ArchiveResource.RawResource(inputSource.openStream().use { it.readAllBytes() }) } + } /** * Reads the manifest from the archive as an [AndroidManifestBlock]. @@ -113,7 +103,7 @@ class Archive(private val module: ApkModule) { * * @return A [Map] containing all the dex files. */ - fun readDexFiles() = module.listDexFiles().associate { file -> file.name to file.openStream().use { it.readAllBytes() } } + fun readDexFiles() = module.listDexFiles().associate { file -> file.name to file.openStream() } /** * Write the byte array to the archive entry. @@ -137,4 +127,36 @@ class Archive(private val module: ApkModule) { resources, ) ) + + /** + * A resource file of an [Archive]. + */ + abstract class ArchiveResource() : Closeable { + private var pendingWrite = false + + override fun close() { + TODO("Not yet implemented") + } + + /** + * An [ResXmlDocument] resource file. + * + * @param xmlResource The [XMLDocument] of the file. + */ + class XmlResource(val xmlResource: XMLDocument, archive: Archive) : ArchiveResource() + + /** + * A raw resource file. + * + * @param data The raw data of the file. + */ + class RawResource(val data: ByteArray, archive: Archive) : ArchiveResource() + + /** + * @param virtualPath The resource file path. Example: /res/drawable-hdpi/icon.png. + * @param archivePath The actual file path in the archive. Example: res/4a.png. + * @param onClose An action to perform when the file associated with this handle is closed + */ + data class Handle(val virtualPath: String, val archivePath: String, val onClose: () -> Unit) + } } \ 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 index f33dda4..e1a942a 100644 --- a/arsclib-utils/src/main/kotlin/app/revanced/arsc/resource/Resource.kt +++ b/arsclib-utils/src/main/kotlin/app/revanced/arsc/resource/Resource.kt @@ -1,8 +1,8 @@ package app.revanced.arsc.resource -import app.revanced.arsc.ApkException -import com.reandroid.arsc.coder.ValueDecoder +import app.revanced.arsc.ApkResourceException import com.reandroid.arsc.coder.EncodeResult +import com.reandroid.arsc.coder.ValueDecoder import com.reandroid.arsc.value.Entry import com.reandroid.arsc.value.ValueType import com.reandroid.arsc.value.array.ArrayBag @@ -45,7 +45,7 @@ open class Scalar internal constructor(private val valueType: ValueType, private sealed class Complex : Resource() private fun encoded(encodeResult: EncodeResult?) = encodeResult?.let { Scalar(it.valueType, it.value) } - ?: throw ApkException.Encode("Failed to encode value") + ?: throw ApkResourceException.Encode("Failed to encode value") /** * Encode a color. @@ -147,7 +147,7 @@ class Plurals(private val elements: Map) : Complex() { val plurals = PluralsBag.create(entry) plurals.putAll(elements.asIterable().associate { (k, v) -> - PluralsQuantity.value(k) to PluralsBagItem.string(resources.getOrCreateTableString(v)) + PluralsQuantity.value(k) to PluralsBagItem.string(resources.getOrCreateString(v)) }) } } @@ -158,7 +158,7 @@ class Plurals(private val elements: Map) : Complex() { * @param value The string value. */ class StringResource(val value: String) : Scalar(ValueType.STRING, 0) { - private fun tableString(resources: ResourceContainer) = resources.getOrCreateTableString(value) + private fun tableString(resources: ResourceContainer) = resources.getOrCreateString(value) override fun data(resources: ResourceContainer) = tableString(resources).index override fun toArrayItem(resources: ResourceContainer) = ArrayBagItem.string(tableString(resources)) 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 index ab5aa8d..62bdc80 100644 --- a/arsclib-utils/src/main/kotlin/app/revanced/arsc/resource/ResourceContainer.kt +++ b/arsclib-utils/src/main/kotlin/app/revanced/arsc/resource/ResourceContainer.kt @@ -1,17 +1,19 @@ package app.revanced.arsc.resource -import app.revanced.arsc.ApkException +import app.revanced.arsc.ApkResourceException import app.revanced.arsc.archive.Archive import com.reandroid.apk.xmlencoder.EncodeUtil +import com.reandroid.arsc.chunk.PackageBlock 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. + * A high-level API for modifying the resources contained in an APK file. * - * @param tableBlock The resources.arsc file of this Apk. + * @param archive The [Archive] containing this resource table. + * @param tableBlock The resources file of this APK file. Typically named "resources.arsc". */ class ResourceContainer(private val archive: Archive, internal val tableBlock: TableBlock?) { internal val packageBlock = tableBlock?.pickOne() // Pick the main PackageBlock. @@ -22,10 +24,18 @@ class ResourceContainer(private val archive: Archive, internal val tableBlock: T archive.resources = this } - private fun expectPackageBlock() = packageBlock ?: throw ApkException.MissingResourceTable + /** + * 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) - internal fun getOrCreateTableString(value: String) = - tableBlock?.stringPool?.getOrCreate(value) ?: throw ApkException.MissingResourceTable + private fun getPackageBlock() = packageBlock ?: throw ApkResourceException.MissingResourceTable + + internal fun getOrCreateString(value: String) = + tableBlock?.stringPool?.getOrCreate(value) ?: throw ApkResourceException.MissingResourceTable /** * Set the value of the [Entry] to the one specified. @@ -54,7 +64,7 @@ class ResourceContainer(private val archive: Archive, internal val tableBlock: T private fun getEntry(type: String, name: String, qualifiers: String?): Entry? { val resourceId = try { resourceTable.resolve("@$type/$name") - } catch (_: ApkException.InvalidReference) { + } catch (_: ApkResourceException.InvalidReference) { return null } @@ -69,9 +79,9 @@ class ResourceContainer(private val archive: Archive, internal val tableBlock: T * @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") + if (resPath.startsWith("res/values")) throw ApkResourceException.Decode("Decoding the resource table as a file is not supported") - var callback = {} + var onClose = {} var archivePath = resPath if (tableBlock != null && resPath.startsWith("res/") && resPath.count { it == '/' } == 2) { @@ -89,11 +99,11 @@ class ResourceContainer(private val archive: Archive, internal val tableBlock: T 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) } + onClose = { set(type, name, StringResource(archivePath), qualifiers) } } } - return ResourceFile.Handle(resPath, archivePath, callback) + return ResourceFile.Handle(resPath, archivePath, onClose) } /** @@ -105,7 +115,7 @@ class ResourceContainer(private val archive: Archive, internal val tableBlock: T * @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 + getPackageBlock().getOrCreate(configuration, type, name).also { it.setTo(value) }.resourceId /** * Create or update multiple resources in an ARSC type block. @@ -115,21 +125,11 @@ class ResourceContainer(private val archive: Archive, internal val tableBlock: T * @param configuration The resource configuration. */ fun setGroup(type: String, map: Map, configuration: String? = null) { - expectPackageBlock().getOrCreateSpecTypePair(type).getOrCreateTypeBlock(configuration).apply { + getPackageBlock().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. */ 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 index becaa1c..db7962f 100644 --- a/arsclib-utils/src/main/kotlin/app/revanced/arsc/resource/ResourceFile.kt +++ b/arsclib-utils/src/main/kotlin/app/revanced/arsc/resource/ResourceFile.kt @@ -1,39 +1,35 @@ package app.revanced.arsc.resource -import app.revanced.arsc.ApkException +import app.revanced.arsc.ApkResourceException import app.revanced.arsc.archive.Archive +import app.revanced.arsc.resource.ResourceFile.Handle 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 +import java.io.* /** - * A resource file inside an [Apk]. + * Instantiate a [ResourceFile] and lock the file which [handle] is associated with. + * + * @param handle The [Handle] associated with this file. + * @param archive The [Archive] that the file resides in. */ class ResourceFile private constructor( internal val handle: Handle, private val archive: Archive, - readResult: Archive.ReadResult? -) : - Closeable { + readResult: Archive.ArchiveResource? +) : Closeable { + private var pendingWrite = false + private val isXmlResource = readResult is Archive.ArchiveResource.XmlResource + + init { + archive.lock(this) + } /** - * @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 + * Instantiate a [ResourceFile]. + * + * @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, @@ -41,15 +37,15 @@ class ResourceFile private constructor( try { archive.read(handle.archivePath) } catch (e: XMLException) { - throw ApkException.Decode("Failed to decode XML while reading ${handle.virtualPath}", e) + throw ApkResourceException.Decode("Failed to decode XML while reading ${handle.virtualPath}", e) } catch (e: IOException) { - throw ApkException.Decode("Could not read ${handle.virtualPath}", e) + throw ApkResourceException.Decode("Could not read ${handle.virtualPath}", e) } ) var contents = readResult?.data ?: ByteArray(0) set(value) { - changed = true + pendingWrite = true field = value } @@ -57,36 +53,34 @@ class ResourceFile private constructor( override fun toString() = handle.virtualPath - init { - archive.lock(this) - } - override fun close() { - if (changed) { + if (pendingWrite) { val path = handle.archivePath - if (xml) archive.writeXml( + + if (isXmlResource) archive.writeXml( path, try { - XMLDocument.load(String(contents)) + XMLDocument.load(inputStream()) } catch (e: XMLException) { - throw ApkException.Encode("Failed to parse XML while writing ${handle.virtualPath}", e) + throw ApkResourceException.Encode("Failed to parse XML while writing ${handle.virtualPath}", e) } + ) else archive.writeRaw(path, contents) } - handle.close() + + handle.onClose() + + 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() - } - } + + /** + * @param virtualPath The resource file path. Example: /res/drawable-hdpi/icon.png. + * @param archivePath The actual file path in the archive. Example: res/4a.png. + * @param onClose An action to perform when the file associated with this handle is closed + */ + internal data class Handle(val virtualPath: String, val archivePath: String, val onClose: () -> Unit) + } \ 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 index de320bc..b2c5574 100644 --- a/arsclib-utils/src/main/kotlin/app/revanced/arsc/resource/ResourceTable.kt +++ b/arsclib-utils/src/main/kotlin/app/revanced/arsc/resource/ResourceTable.kt @@ -1,6 +1,6 @@ package app.revanced.arsc.resource -import app.revanced.arsc.ApkException +import app.revanced.arsc.ApkResourceException import com.reandroid.apk.xmlencoder.EncodeException import com.reandroid.apk.xmlencoder.EncodeMaterials import com.reandroid.arsc.util.FrameworkTable @@ -8,7 +8,7 @@ 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]. + * 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 @@ -31,7 +31,7 @@ class ResourceTable(base: ResourceContainer, all: Sequence) { } /** - * The resource mappings which are generated when the [ApkBundle] is created. + * The resource mappings which are generated when the ApkBundle is created. */ private val tableIdentifier = encodeMaterials.tableIdentifier @@ -52,7 +52,7 @@ class ResourceTable(base: ResourceContainer, all: Sequence) { fun resolveLocal(type: String, name: String) = modifiedResources[type]?.get(name) ?: tableIdentifier.get(packageName, type, name)?.resourceId - ?: throw ApkException.InvalidReference( + ?: throw ApkResourceException.InvalidReference( type, name ) @@ -66,7 +66,7 @@ class ResourceTable(base: ResourceContainer, all: Sequence) { fun resolve(reference: String) = try { encodeMaterials.resolveReference(reference) } catch (e: EncodeException) { - throw ApkException.InvalidReference(reference, e) + throw ApkResourceException.InvalidReference(reference, e) } /** 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 index 78f2705..19a228b 100644 --- a/arsclib-utils/src/main/kotlin/app/revanced/arsc/xml/LazyXMLInputSource.kt +++ b/arsclib-utils/src/main/kotlin/app/revanced/arsc/xml/LazyXMLInputSource.kt @@ -1,6 +1,6 @@ package app.revanced.arsc.xml -import app.revanced.arsc.ApkException +import app.revanced.arsc.ApkResourceException import app.revanced.arsc.resource.ResourceContainer import app.revanced.arsc.resource.boolean import com.reandroid.apk.xmlencoder.EncodeException @@ -11,7 +11,7 @@ import com.reandroid.xml.XMLElement import com.reandroid.xml.source.XMLDocumentSource /** - * Archive input source that lazily encodes the [XMLDocument] when you read from it. + * Archive input source to lazily encode an [XMLDocument] after it has been modified. * * @param name The file name of this input source. * @param document The [XMLDocument] to encode. @@ -38,7 +38,7 @@ internal class LazyXMLInputSource( override fun getResXmlBlock(): ResXmlDocument { if (!ready) { - throw ApkException.Encode("$name has not been encoded yet") + throw ApkResourceException.Encode("$name has not been encoded yet") } return super.getResXmlBlock() 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 index 6b03a25..f35fdab 100644 --- a/revanced-patcher/src/main/kotlin/app/revanced/patcher/apk/Apk.kt +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/apk/Apk.kt @@ -2,9 +2,9 @@ package app.revanced.patcher.apk -import app.revanced.arsc.ApkException +import app.revanced.arsc.ApkResourceException import app.revanced.arsc.archive.Archive -import app.revanced.arsc.resource.* +import app.revanced.arsc.resource.ResourceContainer import app.revanced.patcher.Patcher import app.revanced.patcher.PatcherOptions import app.revanced.patcher.logging.asArscLogger @@ -14,6 +14,7 @@ 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.Opcodes import org.jf.dexlib2.dexbacked.DexBackedDexFile import org.jf.dexlib2.iface.DexFile import org.jf.dexlib2.iface.MultiDexContainer @@ -47,7 +48,7 @@ sealed class Apk private constructor(module: ApkModule) { try { archive.cleanup(options.logger.asArscLogger()) } catch (e: EncodeException) { - throw ApkException.Encode(e.message!!, e) + throw ApkResourceException.Encode(e.message!!, e) } resources.refreshPackageName() @@ -61,7 +62,7 @@ sealed class Apk private constructor(module: ApkModule) { fun write(output: File) = archive.save(output) companion object { - const val manifest = "AndroidManifest.xml" + const val MANIFEST_FILE_NAME = "AndroidManifest.xml" /** * Determine the [Module] and [Type] of an [ApkModule]. @@ -78,7 +79,9 @@ sealed class Apk private constructor(module: ApkModule) { ) to Type.Base else -> { - val module = manifestElement.searchAttributeByName("configForSplit")?.let { Module.DynamicFeature(it.valueAsString) } ?: Module.Main + val module = manifestElement.searchAttributeByName("configForSplit") + ?.let { Module.DynamicFeature(it.valueAsString) } ?: Module.Main + // Examples: // config.xhdpi // df_my_feature.config.en @@ -101,34 +104,37 @@ sealed class Apk private constructor(module: ApkModule) { } 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 + private val opcodes: Opcodes /** * The classes and proxied classes of the [Base] apk file. */ - val classes = ProxyBackedClassList(dexFile.classes) + val classes: ProxyBackedClassList + + init { + 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] + }).let { + opcodes = it.opcodes + classes = ProxyBackedClassList(it.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 + override fun getClasses() = + this@BytecodeData.classes.also(ProxyBackedClassList::applyProxies).toSet() + override fun getOpcodes() = this@BytecodeData.opcodes } // Write modified dex files. @@ -148,9 +154,10 @@ sealed class Apk private constructor(module: ApkModule) { * @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?) { + data class PackageMetadata(val packageName: String?, val packageVersion: String?) { internal constructor(manifestBlock: AndroidManifestBlock) : this( - manifestBlock.packageName ?: "unnamed split apk file", manifestBlock.versionName + manifestBlock.packageName, + manifestBlock.versionName ) } @@ -193,7 +200,7 @@ sealed class Apk private constructor(module: ApkModule) { } /** - * The base apk file that is to be patched. + * The base [Apk] file.. * * @see Apk */ 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 index 6e46440..945a850 100644 --- a/revanced-patcher/src/main/kotlin/app/revanced/patcher/apk/ApkBundle.kt +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/apk/ApkBundle.kt @@ -1,6 +1,8 @@ +@file:Suppress("MemberVisibilityCanBePrivate") + package app.revanced.patcher.apk -import app.revanced.arsc.ApkException +import app.revanced.arsc.ApkResourceException import app.revanced.arsc.resource.ResourceTable import app.revanced.patcher.Patcher import app.revanced.patcher.PatcherOptions @@ -13,7 +15,7 @@ import java.io.File * * @param files A list of apk files to load. */ -class ApkBundle(files: List) { +class ApkBundle(files: List) : Sequence { /** * The [Apk.Base] of this [ApkBundle]. */ @@ -65,21 +67,16 @@ class ApkBundle(files: List) { } /** - * A [Sequence] yielding all [Apk]s in this [ApkBundle]. + * The [ResourceTable] of this [ApkBundle]. */ - val all = sequence { + val resources = ResourceTable(base.resources, map { it.resources }) + + override fun iterator() = 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 + }.iterator() /** * Refresh all updated resources in an [ApkBundle]. @@ -87,27 +84,22 @@ class ApkBundle(files: List) { * @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 + internal fun cleanup(options: PatcherOptions) = map { + var exception: ApkResourceException? = null try { it.cleanup(options) - } catch (e: ApkException) { + } catch (e: ApkResourceException) { 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. + * @param exception The optional [ApkResourceException] when an exception occurred. */ - data class SplitApkResult(val apk: Apk, val exception: ApkException? = null) + data class SplitApkResult(val apk: Apk, val exception: ApkResourceException? = null) } \ No newline at end of file diff --git a/revanced-patcher/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 index b1aadb5..a786fbf 100644 --- a/revanced-patcher/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 @@ -18,7 +18,7 @@ import org.w3c.dom.Element @Version("0.0.1") class ExampleResourcePatch : ResourcePatch { override suspend fun execute(context: ResourceContext) { - context.apkBundle.base.resources.openXmlFile(Apk.manifest).use { editor -> + context.apkBundle.base.resources.openXmlFile(Apk.MANIFEST_FILE_NAME).use { editor -> val element = editor // regular DomFileEditor .file .getElementsByTagName("application")