refactor: do initial refactoring attempt

This commit is contained in:
oSumAtrIX 2023-07-30 00:07:41 +02:00
parent c52f0b80f2
commit c6fdf97794
No known key found for this signature in database
GPG Key ID: A9B3094ACDB604B4
9 changed files with 183 additions and 261 deletions

View File

@ -7,6 +7,29 @@ package app.revanced.arsc
* @param throwable The corresponding [Throwable].
*/
sealed class ApkResourceException(message: String, throwable: Throwable? = null) : Exception(message, throwable) {
/**
* An exception when locking resources.
*
* @param message The exception message.
* @param throwable The corresponding [Throwable].
*/
class Locked(message: String, throwable: Throwable? = null) : ApkResourceException(message, throwable)
/**
* An exception when writing resources.
*
* @param message The exception message.
* @param throwable The corresponding [Throwable].
*/
class Write(message: String, throwable: Throwable? = null) : ApkResourceException(message, throwable)
/**
* An exception when reading resources.
*
* @param message The exception message.
* @param throwable The corresponding [Throwable].
*/
class Read(message: String, throwable: Throwable? = null) : ApkResourceException(message, throwable)
/**
* An exception when decoding resources.
*
@ -45,5 +68,5 @@ sealed class ApkResourceException(message: String, throwable: Throwable? = null)
/**
* 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.")
class MissingResourceTable : ApkResourceException("Apk does not have a resource table.")
}

View File

@ -2,163 +2,27 @@
package app.revanced.arsc.archive
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.arsc.chunk.xml.AndroidManifestBlock
import com.reandroid.arsc.chunk.xml.ResXmlDocument
import com.reandroid.xml.XMLDocument
import java.io.Closeable
import com.reandroid.apk.DexFileInputSource
import com.reandroid.archive.InputSource
import java.io.File
import java.io.Flushable
/**
* 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
class Archive(internal val module: ApkModule) : Flushable {
val mainPackageResources = ResourceContainer(this, module.tableBlock)
/**
* The zip archive for the [ApkModule] this [Archive] is operating on.
*/
private val moduleArchive = module.apkArchive
private val lockedFiles = mutableMapOf<String, ResourceFile>()
/**
* 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 ApkResourceException.Decode(
"${file.handle.virtualPath} is currently being used. Close it before opening it again."
)
}
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()
}
moduleArchive.listInputSources().filterIsInstance<LazyXMLInputSource>()
.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 [ArchiveResource] containing the contents of the entry.
*/
fun read(path: String) = moduleArchive.getInputSource(path)?.let { inputSource ->
when {
inputSource is LazyXMLInputSource -> ArchiveResource.XmlResource(inputSource.document)
ResXmlDocument.isResXmlBlock(inputSource.openStream()) -> ArchiveResource.XmlResource(
module
.loadResXmlDocument(inputSource)
.decodeToXml(resources.resourceTable.entryStore, resources.packageBlock?.id ?: 0)
)
else -> ArchiveResource.RawResource(inputSource.openStream().use { it.readAllBytes() })
}
}
/**
* Reads the manifest from the archive as an [AndroidManifestBlock].
*
* @return The [AndroidManifestBlock] contained in this archive.
*/
fun readManifest(): AndroidManifestBlock =
moduleArchive.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() }
/**
* 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) =
moduleArchive.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) = moduleArchive.add(
LazyXMLInputSource(
path,
document,
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)
fun save(output: File) {
flush()
module.writeApk(output)
}
fun readDexFiles(): MutableList<DexFileInputSource> = module.listDexFiles()
fun write(inputSource: InputSource) = module.apkArchive.add(inputSource) // Overwrites existing files.
fun read(name: String): InputSource? = module.apkArchive.getInputSource(name)
override fun flush() = mainPackageResources.flush()
}

View File

@ -20,7 +20,7 @@ sealed class Resource {
internal abstract fun write(entry: Entry, resources: ResourceContainer)
}
internal val Resource.complex get() = when (this) {
internal val Resource.isComplex get() = when (this) {
is Scalar -> false
is Complex -> true
}

View File

@ -3,54 +3,102 @@ package app.revanced.arsc.resource
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.chunk.xml.ResXmlDocument
import com.reandroid.arsc.value.Entry
import com.reandroid.arsc.value.ResConfig
import java.io.Closeable
import java.io.File
import java.io.Flushable
/**
* A high-level API for modifying the resources contained in an APK file.
*
* @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.
class ResourceContainer(private val archive: Archive, internal val tableBlock: TableBlock) : Flushable {
private val packageBlock = tableBlock.pickOne() // Pick the main package block.
internal lateinit var resourceTable: ResourceTable // TODO: Set this.
internal lateinit var resourceTable: ResourceTable
private val lockedResourceFileNames = mutableSetOf<String>()
init {
archive.resources = this
private fun lock(resourceFile: ResourceFile) {
if (resourceFile.name in lockedResourceFileNames) {
throw ApkResourceException.Locked("Resource file ${resourceFile.name} is already locked.")
}
lockedResourceFileNames.add(resourceFile.name)
}
private fun unlock(resourceFile: ResourceFile) {
lockedResourceFileNames.remove(resourceFile.name)
}
fun <T : ResourceFile> openResource(name: String): ResourceFileEditor<T> {
val inputSource = archive.read(name)
?: throw ApkResourceException.Read("Resource file $name not found.")
val resourceFile = when {
ResXmlDocument.isResXmlBlock(inputSource.openStream()) -> {
val xmlDocument = archive.module
.loadResXmlDocument(inputSource)
.decodeToXml(resourceTable.entryStore, packageBlock.id)
ResourceFile.XmlResourceFile(name, xmlDocument)
}
else -> {
val bytes = inputSource.openStream().use { it.readAllBytes() }
ResourceFile.BinaryResourceFile(name, bytes)
}
}
try {
@Suppress("UNCHECKED_CAST")
return ResourceFileEditor(resourceFile as T).also {
lockedResourceFileNames.add(name)
}
} catch (e: ClassCastException) {
throw ApkResourceException.Decode("Resource file $name is not ${resourceFile::class}.", e)
}
}
inner class ResourceFileEditor<T : ResourceFile> internal constructor(
private val resourceFile: T,
) : Closeable {
fun use(block: (T) -> Unit) = block(resourceFile)
override fun close() {
lockedResourceFileNames.remove(resourceFile.name)
}
}
override fun flush() {
TODO("Not yet implemented")
}
/**
* Open a resource file, creating it if the file does not exist.
*
* @param path The resource file path.
* @return The corresponding [ResourceFile],
* @return The corresponding [ResourceFiles],
*/
fun openFile(path: String) = ResourceFile(createHandle(path), archive)
fun openFile(path: String) = ResourceFiles(createHandle(path), archive)
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.
*
* @param value The new value.
*/
private fun Entry.setTo(value: Resource) {
val savedRef = specReference
ensureComplex(value.complex)
if (savedRef != 0) {
private fun Entry.set(resource: Resource) {
val existingEntryNameReference = specReference
// Sets this.specReference if the entry is not yet initialized.
// Sets this.specReference to 0 if the resource type of the existing entry changes.
ensureComplex(resource.isComplex)
if (existingEntryNameReference != 0) {
// Preserve the entry name by restoring the previous spec block reference (if present).
specReference = savedRef
specReference = existingEntryNameReference
}
value.write(this, this@ResourceContainer)
resource.write(this, this@ResourceContainer)
resourceTable.registerChanged(this)
}
@ -73,12 +121,12 @@ class ResourceContainer(private val archive: Archive, internal val tableBlock: T
}
/**
* Create a [ResourceFile.Handle] that can be used to open a [ResourceFile].
* Create a [ResourceFiles.Handle] that can be used to open a [ResourceFiles].
* This may involve looking it up in the resource table to find the actual location in the archive.
*
* @param path The path of the resource.
*/
private fun createHandle(path: String): ResourceFile.Handle {
private fun createHandle(path: String): ResourceFiles.Handle {
if (path.startsWith("res/values")) throw ApkResourceException.Decode("Decoding the resource table as a file is not supported")
var onClose = {}
@ -97,42 +145,23 @@ class ResourceContainer(private val archive: Archive, internal val tableBlock: T
archivePath = it
} ?: run {
// An entry for this specific resource file was not found in the resource table, so we have to register it after we save.
onClose = { getOrCreateResource(type, name, StringResource(archivePath), qualifiers) }
onClose = { setResource(type, name, StringResource(archivePath), qualifiers) }
}
}
return ResourceFile.Handle(path, archivePath, onClose)
return ResourceFiles.Handle(path, archivePath, onClose)
}
/**
* Create or update a resource.
*
* @param type The resource type.
* @param name The name of the resource.
* @param resource The resource data.
* @param qualifiers The resource configuration.
* @return The resource ID for the resource.
*/
fun getOrCreateResource(type: String, name: String, resource: Resource, qualifiers: String? = null) =
getPackageBlock().getOrCreate(qualifiers, type, name).also { it.setTo(resource) }.resourceId
fun setResource(type: String, entryName: String, resource: Resource, qualifiers: String? = null) =
getPackageBlock().getOrCreate(qualifiers, type, entryName).also { it.set(resource) }.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<String, Resource>, configuration: String? = null) {
fun setResources(type: String, resources: Map<String, Resource>, configuration: String? = null) {
getPackageBlock().getOrCreateSpecTypePair(type).getOrCreateTypeBlock(configuration).apply {
map.forEach { (name, value) -> getOrCreateEntry(name).setTo(value) }
resources.forEach { (entryName, resource) -> getOrCreateEntry(entryName).set(resource) }
}
}
/**
* Update the [PackageBlock] name to match the manifest.
*/
fun refreshPackageName() {
packageBlock?.name = archive.readManifest().packageName
override fun flush() {
packageBlock?.name = archive
}
}

View File

@ -2,31 +2,25 @@ package app.revanced.arsc.resource
import app.revanced.arsc.ApkResourceException
import app.revanced.arsc.archive.Archive
import app.revanced.arsc.resource.ResourceFile.Handle
import com.reandroid.archive.InputSource
import com.reandroid.xml.XMLDocument
import com.reandroid.xml.XMLException
import java.io.*
/**
* 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.ArchiveResource?
) : Closeable {
private var pendingWrite = false
private val isXmlResource = readResult is Archive.ArchiveResource.XmlResource
init {
archive.lock(this)
}
abstract class ResourceFile(val name: String) {
internal var realName: String? = null
class XmlResourceFile(name: String, val document: XMLDocument) : ResourceFile(name)
class BinaryResourceFile(name: String, var bytes: ByteArray) : ResourceFile(name)
}
class ResourceFiles private constructor(
) : Closeable {
/**
* Instantiate a [ResourceFile].
* Instantiate a [ResourceFiles].
*
* @param handle The [Handle] associated with this file.
* @param archive The [Archive] that the file resides in.
@ -43,6 +37,10 @@ class ResourceFile private constructor(
}
)
companion object {
const val DEFAULT_BUFFER_SIZE = 1024
}
var contents = readResult?.data ?: ByteArray(0)
set(value) {
pendingWrite = true
@ -76,11 +74,18 @@ class ResourceFile private constructor(
fun inputStream(): InputStream = ByteArrayInputStream(contents)
fun outputStream(bufferSize: Int = DEFAULT_BUFFER_SIZE): OutputStream =
object : ByteArrayOutputStream(bufferSize) {
override fun close() {
this@ResourceFiles.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

@ -11,7 +11,7 @@ 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<ResourceContainer>) {
private val packageName = base.packageBlock!!.name
private val packageName = base.tableBlock!!.name
/**
* A [TableEntryStore] used to decode XML.
@ -87,7 +87,7 @@ class ResourceTable(base: ResourceContainer, all: Sequence<ResourceContainer>) {
}
base.also {
encodeMaterials.currentPackage = it.packageBlock
encodeMaterials.currentPackage = it.tableBlock
it.tableBlock!!.frameWorks.forEach { fw ->
if (fw is FrameworkTable) {

View File

@ -1,6 +1,5 @@
package app.revanced.arsc.xml
import app.revanced.arsc.ApkResourceException
import app.revanced.arsc.resource.ResourceContainer
import app.revanced.arsc.resource.boolean
import com.reandroid.apk.xmlencoder.EncodeException
@ -17,45 +16,39 @@ import com.reandroid.xml.source.XMLDocumentSource
* @param document The [XMLDocument] to encode.
* @param resources The [ResourceContainer] to use for encoding.
*/
internal class LazyXMLInputSource(
internal class LazyXMLEncodeSource(
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.getOrCreateResource("id", name, boolean(false))
attr.value = "@id/$name"
}
}
listChildElements().forEach { it.registerIds() }
}
private var encoded = false
override fun getResXmlBlock(): ResXmlDocument {
if (!ready) {
throw ApkResourceException.Encode("$name has not been encoded yet")
if (encoded) return super.getResXmlBlock()
XMLEncodeSource(resources.resourceTable.encodeMaterials, XMLDocumentSource(name, document))
fun XMLElement.registerIds() {
listAttributes().forEach { attr ->
if (!attr.value.startsWith("@+id/")) return@forEach
val name = attr.value.split('/').last()
resources.setResource("id", name, boolean(false))
attr.value = "@id/$name"
}
listChildElements().forEach { it.registerIds() }
}
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
encoded = true
// This will call XMLEncodeSource.getResXmlBlock(), which will encode the document if it has not already been encoded.
// This will call XMLEncodeSource.getResXmlBlock(),
// which will encode the document if it has not already been encoded.
try {
resXmlBlock
return super.getResXmlBlock()
} catch (e: EncodeException) {
throw EncodeException("Failed to encode $name", e)
}

View File

@ -3,7 +3,7 @@ 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.arsc.resource.ResourceFiles
import app.revanced.patcher.util.method.MethodWalker
import org.jf.dexlib2.iface.Method
import org.w3c.dom.Document
@ -85,7 +85,7 @@ class DomFileEditor internal constructor(
val file: Document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputStream)
.also(Document::normalize)
internal constructor(file: ResourceFile) : this(
internal constructor(file: ResourceFiles) : this(
file.inputStream(),
{
file.contents = it.toByteArray()

View File

@ -4,13 +4,13 @@ package app.revanced.patcher.apk
import app.revanced.arsc.ApkResourceException
import app.revanced.arsc.archive.Archive
import app.revanced.arsc.resource.ResourceContainer
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.archive.InputSource
import com.reandroid.arsc.chunk.xml.AndroidManifestBlock
import com.reandroid.arsc.value.ResConfig
import lanchon.multidexlib2.*
@ -37,8 +37,6 @@ sealed class Apk private constructor(module: ApkModule) {
*/
val packageMetadata = PackageMetadata(module.androidManifestBlock)
val resources = ResourceContainer(archive, module.tableBlock)
/**
* Refresh updated resources and close any open files.
*
@ -51,7 +49,7 @@ sealed class Apk private constructor(module: ApkModule) {
throw ApkResourceException.Encode(e.message!!, e)
}
resources.refreshPackageName()
archive.mainPackageResources.refreshPackageName()
}
/**
@ -114,8 +112,14 @@ sealed class Apk private constructor(module: ApkModule) {
init {
MultiDexContainerBackedDexFile(object : MultiDexContainer<DexBackedDexFile> {
// 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)) }
private val entries = archive.readDexFiles().associateBy { it.name }
.mapValues { (name, inputSource) ->
BasicDexEntry(
this,
name,
RawDexIO.readRawDexFile(inputSource.openStream(), inputSource.length, null)
)
}
override fun getDexEntryNames() = entries.keys.toList()
override fun getEntry(entryName: String) = entries[entryName]
@ -143,7 +147,11 @@ sealed class Apk private constructor(module: ApkModule) {
it, Patcher.dexFileNamer, newDexFile, DexIO.DEFAULT_MAX_DEX_POOL_SIZE, null
)
}.forEach { (name, store) ->
archive.writeRaw(name, store.data)
val dexFileInputSource = object : InputSource(name) {
override fun openStream() = store.readAt(0)
}
archive.write(dexFileInputSource)
}
}
}