mirror of
https://github.com/ReVanced/revanced-patcher.git
synced 2024-09-19 20:51:10 +02:00
refactor: do initial refactoring attempt
This commit is contained in:
parent
c52f0b80f2
commit
c6fdf97794
9 changed files with 183 additions and 261 deletions
|
@ -7,6 +7,29 @@ package app.revanced.arsc
|
||||||
* @param throwable The corresponding [Throwable].
|
* @param throwable The corresponding [Throwable].
|
||||||
*/
|
*/
|
||||||
sealed class ApkResourceException(message: String, throwable: Throwable? = null) : Exception(message, 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.
|
* 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.
|
* 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.")
|
||||||
}
|
}
|
|
@ -2,163 +2,27 @@
|
||||||
|
|
||||||
package app.revanced.arsc.archive
|
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.ResourceContainer
|
||||||
import app.revanced.arsc.resource.ResourceFile
|
|
||||||
import app.revanced.arsc.xml.LazyXMLInputSource
|
|
||||||
import com.reandroid.apk.ApkModule
|
import com.reandroid.apk.ApkModule
|
||||||
import com.reandroid.archive.ByteInputSource
|
import com.reandroid.apk.DexFileInputSource
|
||||||
import com.reandroid.arsc.chunk.xml.AndroidManifestBlock
|
import com.reandroid.archive.InputSource
|
||||||
import com.reandroid.arsc.chunk.xml.ResXmlDocument
|
|
||||||
import com.reandroid.xml.XMLDocument
|
|
||||||
import java.io.Closeable
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.Flushable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A class for reading/writing files in an [ApkModule].
|
* A class for reading/writing files in an [ApkModule].
|
||||||
*
|
*
|
||||||
* @param module The [ApkModule] to operate on.
|
* @param module The [ApkModule] to operate on.
|
||||||
*/
|
*/
|
||||||
class Archive(private val module: ApkModule) {
|
class Archive(internal val module: ApkModule) : Flushable {
|
||||||
lateinit var resources: ResourceContainer
|
val mainPackageResources = ResourceContainer(this, module.tableBlock)
|
||||||
|
|
||||||
/**
|
fun save(output: File) {
|
||||||
* The zip archive for the [ApkModule] this [Archive] is operating on.
|
flush()
|
||||||
*/
|
module.writeApk(output)
|
||||||
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 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()
|
||||||
}
|
}
|
|
@ -20,7 +20,7 @@ sealed class Resource {
|
||||||
internal abstract fun write(entry: Entry, resources: ResourceContainer)
|
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 Scalar -> false
|
||||||
is Complex -> true
|
is Complex -> true
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,54 +3,102 @@ package app.revanced.arsc.resource
|
||||||
import app.revanced.arsc.ApkResourceException
|
import app.revanced.arsc.ApkResourceException
|
||||||
import app.revanced.arsc.archive.Archive
|
import app.revanced.arsc.archive.Archive
|
||||||
import com.reandroid.apk.xmlencoder.EncodeUtil
|
import com.reandroid.apk.xmlencoder.EncodeUtil
|
||||||
import com.reandroid.arsc.chunk.PackageBlock
|
|
||||||
import com.reandroid.arsc.chunk.TableBlock
|
import com.reandroid.arsc.chunk.TableBlock
|
||||||
|
import com.reandroid.arsc.chunk.xml.ResXmlDocument
|
||||||
import com.reandroid.arsc.value.Entry
|
import com.reandroid.arsc.value.Entry
|
||||||
import com.reandroid.arsc.value.ResConfig
|
import com.reandroid.arsc.value.ResConfig
|
||||||
|
import java.io.Closeable
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.Flushable
|
||||||
|
|
||||||
/**
|
class ResourceContainer(private val archive: Archive, internal val tableBlock: TableBlock) : Flushable {
|
||||||
* A high-level API for modifying the resources contained in an APK file.
|
private val packageBlock = tableBlock.pickOne() // Pick the main package block.
|
||||||
*
|
internal lateinit var resourceTable: ResourceTable // TODO: Set this.
|
||||||
* @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.
|
|
||||||
|
|
||||||
internal lateinit var resourceTable: ResourceTable
|
private val lockedResourceFileNames = mutableSetOf<String>()
|
||||||
|
|
||||||
init {
|
private fun lock(resourceFile: ResourceFile) {
|
||||||
archive.resources = this
|
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.
|
* Open a resource file, creating it if the file does not exist.
|
||||||
*
|
*
|
||||||
* @param path The resource file path.
|
* @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
|
private fun getPackageBlock() = packageBlock ?: throw ApkResourceException.MissingResourceTable
|
||||||
|
|
||||||
internal fun getOrCreateString(value: String) =
|
internal fun getOrCreateString(value: String) =
|
||||||
tableBlock?.stringPool?.getOrCreate(value) ?: throw ApkResourceException.MissingResourceTable
|
tableBlock?.stringPool?.getOrCreate(value) ?: throw ApkResourceException.MissingResourceTable
|
||||||
|
|
||||||
/**
|
private fun Entry.set(resource: Resource) {
|
||||||
* Set the value of the [Entry] to the one specified.
|
val existingEntryNameReference = specReference
|
||||||
*
|
|
||||||
* @param value The new value.
|
// Sets this.specReference if the entry is not yet initialized.
|
||||||
*/
|
// Sets this.specReference to 0 if the resource type of the existing entry changes.
|
||||||
private fun Entry.setTo(value: Resource) {
|
ensureComplex(resource.isComplex)
|
||||||
val savedRef = specReference
|
|
||||||
ensureComplex(value.complex)
|
if (existingEntryNameReference != 0) {
|
||||||
if (savedRef != 0) {
|
|
||||||
// Preserve the entry name by restoring the previous spec block reference (if present).
|
// 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)
|
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.
|
* 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.
|
* @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")
|
if (path.startsWith("res/values")) throw ApkResourceException.Decode("Decoding the resource table as a file is not supported")
|
||||||
|
|
||||||
var onClose = {}
|
var onClose = {}
|
||||||
|
@ -97,42 +145,23 @@ class ResourceContainer(private val archive: Archive, internal val tableBlock: T
|
||||||
archivePath = it
|
archivePath = it
|
||||||
} ?: run {
|
} ?: run {
|
||||||
// An entry for this specific resource file was not found in the resource table, so we have to register it after we save.
|
// 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
fun setResource(type: String, entryName: String, resource: Resource, qualifiers: String? = null) =
|
||||||
* Create or update a resource.
|
getPackageBlock().getOrCreate(qualifiers, type, entryName).also { it.set(resource) }.resourceId
|
||||||
*
|
|
||||||
* @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 setResources(type: String, resources: Map<String, Resource>, configuration: String? = null) {
|
||||||
* 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) {
|
|
||||||
getPackageBlock().getOrCreateSpecTypePair(type).getOrCreateTypeBlock(configuration).apply {
|
getPackageBlock().getOrCreateSpecTypePair(type).getOrCreateTypeBlock(configuration).apply {
|
||||||
map.forEach { (name, value) -> getOrCreateEntry(name).setTo(value) }
|
resources.forEach { (entryName, resource) -> getOrCreateEntry(entryName).set(resource) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
override fun flush() {
|
||||||
* Update the [PackageBlock] name to match the manifest.
|
packageBlock?.name = archive
|
||||||
*/
|
|
||||||
fun refreshPackageName() {
|
|
||||||
packageBlock?.name = archive.readManifest().packageName
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -2,31 +2,25 @@ package app.revanced.arsc.resource
|
||||||
|
|
||||||
import app.revanced.arsc.ApkResourceException
|
import app.revanced.arsc.ApkResourceException
|
||||||
import app.revanced.arsc.archive.Archive
|
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.XMLDocument
|
||||||
import com.reandroid.xml.XMLException
|
import com.reandroid.xml.XMLException
|
||||||
import java.io.*
|
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 {
|
abstract class ResourceFile(val name: String) {
|
||||||
archive.lock(this)
|
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 handle The [Handle] associated with this file.
|
||||||
* @param archive The [Archive] that the file resides in.
|
* @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)
|
var contents = readResult?.data ?: ByteArray(0)
|
||||||
set(value) {
|
set(value) {
|
||||||
pendingWrite = true
|
pendingWrite = true
|
||||||
|
@ -76,11 +74,18 @@ class ResourceFile private constructor(
|
||||||
|
|
||||||
fun inputStream(): InputStream = ByteArrayInputStream(contents)
|
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 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 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
|
* @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)
|
internal data class Handle(val virtualPath: String, val archivePath: String, val onClose: () -> Unit)
|
||||||
|
|
||||||
}
|
}
|
|
@ -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.
|
* A high-level API for resolving resources in the resource table, which spans the entire ApkBundle.
|
||||||
*/
|
*/
|
||||||
class ResourceTable(base: ResourceContainer, all: Sequence<ResourceContainer>) {
|
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.
|
* A [TableEntryStore] used to decode XML.
|
||||||
|
@ -87,7 +87,7 @@ class ResourceTable(base: ResourceContainer, all: Sequence<ResourceContainer>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
base.also {
|
base.also {
|
||||||
encodeMaterials.currentPackage = it.packageBlock
|
encodeMaterials.currentPackage = it.tableBlock
|
||||||
|
|
||||||
it.tableBlock!!.frameWorks.forEach { fw ->
|
it.tableBlock!!.frameWorks.forEach { fw ->
|
||||||
if (fw is FrameworkTable) {
|
if (fw is FrameworkTable) {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package app.revanced.arsc.xml
|
package app.revanced.arsc.xml
|
||||||
|
|
||||||
import app.revanced.arsc.ApkResourceException
|
|
||||||
import app.revanced.arsc.resource.ResourceContainer
|
import app.revanced.arsc.resource.ResourceContainer
|
||||||
import app.revanced.arsc.resource.boolean
|
import app.revanced.arsc.resource.boolean
|
||||||
import com.reandroid.apk.xmlencoder.EncodeException
|
import com.reandroid.apk.xmlencoder.EncodeException
|
||||||
|
@ -17,45 +16,39 @@ import com.reandroid.xml.source.XMLDocumentSource
|
||||||
* @param document The [XMLDocument] to encode.
|
* @param document The [XMLDocument] to encode.
|
||||||
* @param resources The [ResourceContainer] to use for encoding.
|
* @param resources The [ResourceContainer] to use for encoding.
|
||||||
*/
|
*/
|
||||||
internal class LazyXMLInputSource(
|
internal class LazyXMLEncodeSource(
|
||||||
name: String,
|
name: String,
|
||||||
val document: XMLDocument,
|
val document: XMLDocument,
|
||||||
private val resources: ResourceContainer
|
private val resources: ResourceContainer
|
||||||
) : XMLEncodeSource(resources.resourceTable.encodeMaterials, XMLDocumentSource(name, document)) {
|
) : XMLEncodeSource(resources.resourceTable.encodeMaterials, XMLDocumentSource(name, document)) {
|
||||||
private var ready = false
|
private var encoded = false
|
||||||
|
|
||||||
private fun XMLElement.registerIds() {
|
override fun getResXmlBlock(): ResXmlDocument {
|
||||||
|
if (encoded) return super.getResXmlBlock()
|
||||||
|
|
||||||
|
XMLEncodeSource(resources.resourceTable.encodeMaterials, XMLDocumentSource(name, document))
|
||||||
|
|
||||||
|
fun XMLElement.registerIds() {
|
||||||
listAttributes().forEach { attr ->
|
listAttributes().forEach { attr ->
|
||||||
if (attr.value.startsWith("@+id/")) {
|
if (!attr.value.startsWith("@+id/")) return@forEach
|
||||||
|
|
||||||
val name = attr.value.split('/').last()
|
val name = attr.value.split('/').last()
|
||||||
resources.getOrCreateResource("id", name, boolean(false))
|
resources.setResource("id", name, boolean(false))
|
||||||
attr.value = "@id/$name"
|
attr.value = "@id/$name"
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
listChildElements().forEach { it.registerIds() }
|
listChildElements().forEach { it.registerIds() }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getResXmlBlock(): ResXmlDocument {
|
|
||||||
if (!ready) {
|
|
||||||
throw ApkResourceException.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.
|
// Handle all @+id/id_name references in the document.
|
||||||
document.documentElement.registerIds()
|
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 {
|
try {
|
||||||
resXmlBlock
|
return super.getResXmlBlock()
|
||||||
} catch (e: EncodeException) {
|
} catch (e: EncodeException) {
|
||||||
throw EncodeException("Failed to encode $name", e)
|
throw EncodeException("Failed to encode $name", e)
|
||||||
}
|
}
|
|
@ -3,7 +3,7 @@ package app.revanced.patcher
|
||||||
import app.revanced.arsc.resource.ResourceContainer
|
import app.revanced.arsc.resource.ResourceContainer
|
||||||
import app.revanced.patcher.apk.Apk
|
import app.revanced.patcher.apk.Apk
|
||||||
import app.revanced.patcher.apk.ApkBundle
|
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 app.revanced.patcher.util.method.MethodWalker
|
||||||
import org.jf.dexlib2.iface.Method
|
import org.jf.dexlib2.iface.Method
|
||||||
import org.w3c.dom.Document
|
import org.w3c.dom.Document
|
||||||
|
@ -85,7 +85,7 @@ class DomFileEditor internal constructor(
|
||||||
val file: Document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputStream)
|
val file: Document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputStream)
|
||||||
.also(Document::normalize)
|
.also(Document::normalize)
|
||||||
|
|
||||||
internal constructor(file: ResourceFile) : this(
|
internal constructor(file: ResourceFiles) : this(
|
||||||
file.inputStream(),
|
file.inputStream(),
|
||||||
{
|
{
|
||||||
file.contents = it.toByteArray()
|
file.contents = it.toByteArray()
|
||||||
|
|
|
@ -4,13 +4,13 @@ package app.revanced.patcher.apk
|
||||||
|
|
||||||
import app.revanced.arsc.ApkResourceException
|
import app.revanced.arsc.ApkResourceException
|
||||||
import app.revanced.arsc.archive.Archive
|
import app.revanced.arsc.archive.Archive
|
||||||
import app.revanced.arsc.resource.ResourceContainer
|
|
||||||
import app.revanced.patcher.Patcher
|
import app.revanced.patcher.Patcher
|
||||||
import app.revanced.patcher.PatcherOptions
|
import app.revanced.patcher.PatcherOptions
|
||||||
import app.revanced.patcher.logging.asArscLogger
|
import app.revanced.patcher.logging.asArscLogger
|
||||||
import app.revanced.patcher.util.ProxyBackedClassList
|
import app.revanced.patcher.util.ProxyBackedClassList
|
||||||
import com.reandroid.apk.ApkModule
|
import com.reandroid.apk.ApkModule
|
||||||
import com.reandroid.apk.xmlencoder.EncodeException
|
import com.reandroid.apk.xmlencoder.EncodeException
|
||||||
|
import com.reandroid.archive.InputSource
|
||||||
import com.reandroid.arsc.chunk.xml.AndroidManifestBlock
|
import com.reandroid.arsc.chunk.xml.AndroidManifestBlock
|
||||||
import com.reandroid.arsc.value.ResConfig
|
import com.reandroid.arsc.value.ResConfig
|
||||||
import lanchon.multidexlib2.*
|
import lanchon.multidexlib2.*
|
||||||
|
@ -37,8 +37,6 @@ sealed class Apk private constructor(module: ApkModule) {
|
||||||
*/
|
*/
|
||||||
val packageMetadata = PackageMetadata(module.androidManifestBlock)
|
val packageMetadata = PackageMetadata(module.androidManifestBlock)
|
||||||
|
|
||||||
val resources = ResourceContainer(archive, module.tableBlock)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Refresh updated resources and close any open files.
|
* 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)
|
throw ApkResourceException.Encode(e.message!!, e)
|
||||||
}
|
}
|
||||||
|
|
||||||
resources.refreshPackageName()
|
archive.mainPackageResources.refreshPackageName()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -114,8 +112,14 @@ sealed class Apk private constructor(module: ApkModule) {
|
||||||
init {
|
init {
|
||||||
MultiDexContainerBackedDexFile(object : MultiDexContainer<DexBackedDexFile> {
|
MultiDexContainerBackedDexFile(object : MultiDexContainer<DexBackedDexFile> {
|
||||||
// Load all dex files from the apk module and create a dex entry for each of them.
|
// Load all dex files from the apk module and create a dex entry for each of them.
|
||||||
private val entries = archive.readDexFiles()
|
private val entries = archive.readDexFiles().associateBy { it.name }
|
||||||
.mapValues { (name, data) -> BasicDexEntry(this, name, RawDexIO.readRawDexFile(data, 0, null)) }
|
.mapValues { (name, inputSource) ->
|
||||||
|
BasicDexEntry(
|
||||||
|
this,
|
||||||
|
name,
|
||||||
|
RawDexIO.readRawDexFile(inputSource.openStream(), inputSource.length, null)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getDexEntryNames() = entries.keys.toList()
|
override fun getDexEntryNames() = entries.keys.toList()
|
||||||
override fun getEntry(entryName: String) = entries[entryName]
|
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
|
it, Patcher.dexFileNamer, newDexFile, DexIO.DEFAULT_MAX_DEX_POOL_SIZE, null
|
||||||
)
|
)
|
||||||
}.forEach { (name, store) ->
|
}.forEach { (name, store) ->
|
||||||
archive.writeRaw(name, store.data)
|
val dexFileInputSource = object : InputSource(name) {
|
||||||
|
override fun openStream() = store.readAt(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
archive.write(dexFileInputSource)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue