refactor: move stuff around and improve memory profile/performance

This commit is contained in:
oSumAtrIX
2023-07-16 05:25:39 +02:00
parent 242d805c6c
commit 740911a2a3
11 changed files with 220 additions and 198 deletions

View File

@@ -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.")
}

View File

@@ -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.")
}

View File

@@ -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)
}
}

View File

@@ -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<String, String>) : 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<String, String>) : 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))

View File

@@ -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<String, Resource>, 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.
*/

View File

@@ -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)
}

View File

@@ -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<ResourceContainer>) {
private val packageName = base.packageBlock!!.name
@@ -31,7 +31,7 @@ class ResourceTable(base: ResourceContainer, all: Sequence<ResourceContainer>) {
}
/**
* 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<ResourceContainer>) {
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<ResourceContainer>) {
fun resolve(reference: String) = try {
encodeMaterials.resolveReference(reference)
} catch (e: EncodeException) {
throw ApkException.InvalidReference(reference, e)
throw ApkResourceException.InvalidReference(reference, e)
}
/**

View File

@@ -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()