build: Bump ReVanced Patcher
BREAKING CHANGE: Various APIs have been changed or removed.
This commit is contained in:
@@ -0,0 +1,468 @@
|
||||
package app.revanced.util
|
||||
|
||||
import app.revanced.patcher.Fingerprint
|
||||
import app.revanced.patcher.FingerprintBuilder
|
||||
import app.revanced.patcher.Match
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.instructions
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction
|
||||
import app.revanced.patcher.patch.BytecodePatchContext
|
||||
import app.revanced.patcher.patch.PatchException
|
||||
import app.revanced.patcher.util.proxy.mutableTypes.MutableClass
|
||||
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
|
||||
import app.revanced.patches.shared.misc.mapping.get
|
||||
import app.revanced.patches.shared.misc.mapping.resourceMappingPatch
|
||||
import app.revanced.patches.shared.misc.mapping.resourceMappings
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
import com.android.tools.smali.dexlib2.iface.Method
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.Instruction
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.WideLiteralInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.reference.Reference
|
||||
import com.android.tools.smali.dexlib2.util.MethodUtil
|
||||
|
||||
val Fingerprint.matchOrThrow
|
||||
get() = match ?: throw exception
|
||||
|
||||
/**
|
||||
* The [PatchException] of failing to match a [Fingerprint].
|
||||
*
|
||||
* @return The [PatchException].
|
||||
*/
|
||||
val Fingerprint.exception
|
||||
get() = PatchException("Failed to match the fingerprint: $this")
|
||||
|
||||
/**
|
||||
* Find the [MutableMethod] from a given [Method] in a [MutableClass].
|
||||
*
|
||||
* @param method The [Method] to find.
|
||||
* @return The [MutableMethod].
|
||||
*/
|
||||
fun MutableClass.findMutableMethodOf(method: Method) = this.methods.first {
|
||||
MethodUtil.methodSignaturesMatch(it, method)
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a transform to all methods of the class.
|
||||
*
|
||||
* @param transform The transformation function. Accepts a [MutableMethod] and returns a transformed [MutableMethod].
|
||||
*/
|
||||
fun MutableClass.transformMethods(transform: MutableMethod.() -> MutableMethod) {
|
||||
val transformedMethods = methods.map { it.transform() }
|
||||
methods.clear()
|
||||
methods.addAll(transformedMethods)
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject a call to a method that hides a view.
|
||||
*
|
||||
* @param insertIndex The index to insert the call at.
|
||||
* @param viewRegister The register of the view to hide.
|
||||
* @param classDescriptor The descriptor of the class that contains the method.
|
||||
* @param targetMethod The name of the method to call.
|
||||
*/
|
||||
fun MutableMethod.injectHideViewCall(
|
||||
insertIndex: Int,
|
||||
viewRegister: Int,
|
||||
classDescriptor: String,
|
||||
targetMethod: String,
|
||||
) = addInstruction(
|
||||
insertIndex,
|
||||
"invoke-static { v$viewRegister }, $classDescriptor->$targetMethod(Landroid/view/View;)V",
|
||||
)
|
||||
|
||||
/**
|
||||
* Inserts instructions at a given index, using the existing control flow label at that index.
|
||||
* Inserted instructions can have it's own control flow labels as well.
|
||||
*
|
||||
* Effectively this changes the code from:
|
||||
* :label
|
||||
* (original code)
|
||||
*
|
||||
* Into:
|
||||
* :label
|
||||
* (patch code)
|
||||
* (original code)
|
||||
*/
|
||||
internal fun MutableMethod.addInstructionsAtControlFlowLabel(
|
||||
insertIndex: Int,
|
||||
instructions: String,
|
||||
) {
|
||||
// Duplicate original instruction and add to +1 index.
|
||||
addInstruction(insertIndex + 1, getInstruction(insertIndex))
|
||||
|
||||
// Add patch code at same index as duplicated instruction,
|
||||
// so it uses the original instruction control flow label.
|
||||
addInstructionsWithLabels(insertIndex + 1, instructions)
|
||||
|
||||
// Remove original non duplicated instruction.
|
||||
removeInstruction(insertIndex)
|
||||
|
||||
// Original instruction is now after the inserted patch instructions,
|
||||
// and the original control flow label is on the first instruction of the patch code.
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the index of the first instruction with the id of the given resource name.
|
||||
*
|
||||
* Requires [resourceMappingPatch] as a dependency.
|
||||
*
|
||||
* @param resourceName the name of the resource to find the id for.
|
||||
* @return the index of the first instruction with the id of the given resource name, or -1 if not found.
|
||||
* @throws PatchException if the resource cannot be found.
|
||||
* @see [indexOfIdResourceOrThrow], [indexOfFirstWideLiteralInstructionValueReversed]
|
||||
*/
|
||||
fun Method.indexOfIdResource(resourceName: String): Int {
|
||||
val resourceId = resourceMappings["id", resourceName]
|
||||
return indexOfFirstWideLiteralInstructionValue(resourceId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the index of the first instruction with the id of the given resource name or throw a [PatchException].
|
||||
*
|
||||
* Requires [resourceMappingPatch] as a dependency.
|
||||
*
|
||||
* @throws [PatchException] if the resource is not found, or the method does not contain the resource id literal value.
|
||||
* @see [indexOfIdResource], [indexOfFirstWideLiteralInstructionValueReversedOrThrow]
|
||||
*/
|
||||
fun Method.indexOfIdResourceOrThrow(resourceName: String): Int {
|
||||
val index = indexOfIdResource(resourceName)
|
||||
if (index < 0) {
|
||||
throw PatchException("Found resource id for: '$resourceName' but method does not contain the id: $this")
|
||||
}
|
||||
|
||||
return index
|
||||
}
|
||||
|
||||
// TODO Rename these from 'FirstWideLiteralInstruction' to 'FirstLiteralInstruction',
|
||||
// since NarrowLiteralInstruction is a subclass of WideLiteralInstruction.
|
||||
|
||||
/**
|
||||
* Find the index of the first wide literal instruction with the given value.
|
||||
*
|
||||
* @return the first literal instruction with the value, or -1 if not found.
|
||||
* @see indexOfFirstWideLiteralInstructionValueOrThrow
|
||||
*/
|
||||
fun Method.indexOfFirstWideLiteralInstructionValue(literal: Long) = implementation?.let {
|
||||
it.instructions.indexOfFirst { instruction ->
|
||||
(instruction as? WideLiteralInstruction)?.wideLiteral == literal
|
||||
}
|
||||
} ?: -1
|
||||
|
||||
/**
|
||||
* Find the index of the first wide literal instruction with the given value,
|
||||
* or throw an exception if not found.
|
||||
*
|
||||
* @return the first literal instruction with the value, or throws [PatchException] if not found.
|
||||
*/
|
||||
fun Method.indexOfFirstWideLiteralInstructionValueOrThrow(literal: Long): Int {
|
||||
val index = indexOfFirstWideLiteralInstructionValue(literal)
|
||||
if (index < 0) throw PatchException("Could not find literal value: $literal")
|
||||
return index
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the index of the last wide literal instruction with the given value.
|
||||
*
|
||||
* @return the last literal instruction with the value, or -1 if not found.
|
||||
* @see indexOfFirstWideLiteralInstructionValueOrThrow
|
||||
*/
|
||||
fun Method.indexOfFirstWideLiteralInstructionValueReversed(literal: Long) = implementation?.let {
|
||||
it.instructions.indexOfLast { instruction ->
|
||||
(instruction as? WideLiteralInstruction)?.wideLiteral == literal
|
||||
}
|
||||
} ?: -1
|
||||
|
||||
/**
|
||||
* Find the index of the last wide literal instruction with the given value,
|
||||
* or throw an exception if not found.
|
||||
*
|
||||
* @return the last literal instruction with the value, or throws [PatchException] if not found.
|
||||
*/
|
||||
fun Method.indexOfFirstWideLiteralInstructionValueReversedOrThrow(literal: Long): Int {
|
||||
val index = indexOfFirstWideLiteralInstructionValueReversed(literal)
|
||||
if (index < 0) throw PatchException("Could not find literal value: $literal")
|
||||
return index
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the method contains a literal with the given value.
|
||||
*
|
||||
* @return if the method contains a literal with the given value.
|
||||
*/
|
||||
fun Method.containsWideLiteralInstructionValue(literal: Long) =
|
||||
indexOfFirstWideLiteralInstructionValue(literal) >= 0
|
||||
|
||||
/**
|
||||
* Traverse the class hierarchy starting from the given root class.
|
||||
*
|
||||
* @param targetClass the class to start traversing the class hierarchy from.
|
||||
* @param callback function that is called for every class in the hierarchy.
|
||||
*/
|
||||
fun BytecodePatchContext.traverseClassHierarchy(targetClass: MutableClass, callback: MutableClass.() -> Unit) {
|
||||
callback(targetClass)
|
||||
|
||||
targetClass.superclass ?: return
|
||||
|
||||
classBy { targetClass.superclass == it.type }?.mutableClass?.let {
|
||||
traverseClassHierarchy(it, callback)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the [Reference] of an [Instruction] as [T].
|
||||
*
|
||||
* @param T The type of [Reference] to cast to.
|
||||
* @return The [Reference] as [T] or null
|
||||
* if the [Instruction] is not a [ReferenceInstruction] or the [Reference] is not of type [T].
|
||||
* @see ReferenceInstruction
|
||||
*/
|
||||
inline fun <reified T : Reference> Instruction.getReference() = (this as? ReferenceInstruction)?.reference as? T
|
||||
|
||||
/**
|
||||
* Get the index of the first [Instruction] that matches the predicate.
|
||||
*
|
||||
* @param predicate The predicate to match.
|
||||
* @return The index of the first [Instruction] that matches the predicate.
|
||||
*/
|
||||
// TODO: delete this on next major release, the overloaded method with an optional start index serves the same purposes.
|
||||
// Method is deprecated, but annotation is commented out otherwise during compilation usage of the replacement is
|
||||
// incorrectly flagged as deprecated.
|
||||
// @Deprecated("Use the overloaded method with an optional start index.", ReplaceWith("indexOfFirstInstruction(predicate)"))
|
||||
fun Method.indexOfFirstInstruction(predicate: Instruction.() -> Boolean) = indexOfFirstInstruction(0, predicate)
|
||||
|
||||
/**
|
||||
* @return The index of the first opcode specified, or -1 if not found.
|
||||
* @see indexOfFirstInstructionOrThrow
|
||||
*/
|
||||
fun Method.indexOfFirstInstruction(targetOpcode: Opcode): Int =
|
||||
indexOfFirstInstruction(0, targetOpcode)
|
||||
|
||||
/**
|
||||
* @param startIndex Optional starting index to start searching from.
|
||||
* @return The index of the first opcode specified, or -1 if not found.
|
||||
* @see indexOfFirstInstructionOrThrow
|
||||
*/
|
||||
fun Method.indexOfFirstInstruction(startIndex: Int = 0, targetOpcode: Opcode): Int =
|
||||
indexOfFirstInstruction(startIndex) {
|
||||
opcode == targetOpcode
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the index of the first [Instruction] that matches the predicate, starting from [startIndex].
|
||||
*
|
||||
* @param startIndex Optional starting index to start searching from.
|
||||
* @return -1 if the instruction is not found.
|
||||
* @see indexOfFirstInstructionOrThrow
|
||||
*/
|
||||
fun Method.indexOfFirstInstruction(startIndex: Int = 0, predicate: Instruction.() -> Boolean): Int {
|
||||
var instructions = this.implementation!!.instructions
|
||||
if (startIndex != 0) {
|
||||
instructions = instructions.drop(startIndex)
|
||||
}
|
||||
val index = instructions.indexOfFirst(predicate)
|
||||
|
||||
return if (index >= 0) {
|
||||
startIndex + index
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The index of the first opcode specified
|
||||
* @throws PatchException
|
||||
* @see indexOfFirstInstruction
|
||||
*/
|
||||
fun Method.indexOfFirstInstructionOrThrow(targetOpcode: Opcode): Int =
|
||||
indexOfFirstInstructionOrThrow(0, targetOpcode)
|
||||
|
||||
/**
|
||||
* @return The index of the first opcode specified, starting from the index specified.
|
||||
* @throws PatchException
|
||||
* @see indexOfFirstInstruction
|
||||
*/
|
||||
fun Method.indexOfFirstInstructionOrThrow(startIndex: Int = 0, targetOpcode: Opcode): Int =
|
||||
indexOfFirstInstructionOrThrow(startIndex) {
|
||||
opcode == targetOpcode
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the index of the first [Instruction] that matches the predicate, starting from [startIndex].
|
||||
*
|
||||
* @return the index of the instruction
|
||||
* @throws PatchException
|
||||
* @see indexOfFirstInstruction
|
||||
*/
|
||||
fun Method.indexOfFirstInstructionOrThrow(startIndex: Int = 0, predicate: Instruction.() -> Boolean): Int {
|
||||
val index = indexOfFirstInstruction(startIndex, predicate)
|
||||
if (index < 0) {
|
||||
throw PatchException("Could not find instruction index")
|
||||
}
|
||||
|
||||
return index
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the index of matching instruction,
|
||||
* starting from and [startIndex] and searching down.
|
||||
*
|
||||
* @param startIndex Optional starting index to search down from. Searching includes the start index.
|
||||
* @return -1 if the instruction is not found.
|
||||
* @see indexOfFirstInstructionReversedOrThrow
|
||||
*/
|
||||
fun Method.indexOfFirstInstructionReversed(startIndex: Int? = null, targetOpcode: Opcode): Int =
|
||||
indexOfFirstInstructionReversed(startIndex) {
|
||||
opcode == targetOpcode
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the index of matching instruction,
|
||||
* starting from and [startIndex] and searching down.
|
||||
*
|
||||
* @param startIndex Optional starting index to search down from. Searching includes the start index.
|
||||
* @return -1 if the instruction is not found.
|
||||
* @see indexOfFirstInstructionReversedOrThrow
|
||||
*/
|
||||
fun Method.indexOfFirstInstructionReversed(startIndex: Int? = null, predicate: Instruction.() -> Boolean): Int {
|
||||
var instructions = this.implementation!!.instructions
|
||||
if (startIndex != null) {
|
||||
instructions = instructions.take(startIndex + 1)
|
||||
}
|
||||
|
||||
return instructions.indexOfLast(predicate)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the index of matching instruction,
|
||||
* starting from and [startIndex] and searching down.
|
||||
*
|
||||
* @param startIndex Optional starting index to search down from. Searching includes the start index.
|
||||
* @return -1 if the instruction is not found.
|
||||
* @see indexOfFirstInstructionReversed
|
||||
*/
|
||||
fun Method.indexOfFirstInstructionReversedOrThrow(startIndex: Int? = null, targetOpcode: Opcode): Int =
|
||||
indexOfFirstInstructionReversedOrThrow(startIndex) {
|
||||
opcode == targetOpcode
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the index of matching instruction,
|
||||
* starting from and [startIndex] and searching down.
|
||||
*
|
||||
* @param startIndex Optional starting index to search down from. Searching includes the start index.
|
||||
* @return -1 if the instruction is not found.
|
||||
* @see indexOfFirstInstructionReversed
|
||||
*/
|
||||
fun Method.indexOfFirstInstructionReversedOrThrow(startIndex: Int? = null, predicate: Instruction.() -> Boolean): Int {
|
||||
val index = indexOfFirstInstructionReversed(startIndex, predicate)
|
||||
|
||||
if (index < 0) {
|
||||
throw PatchException("Could not find instruction index")
|
||||
}
|
||||
|
||||
return index
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The list of indices of the opcode in reverse order.
|
||||
*/
|
||||
fun Method.findOpcodeIndicesReversed(opcode: Opcode): List<Int> =
|
||||
findOpcodeIndicesReversed { this.opcode == opcode }
|
||||
|
||||
/**
|
||||
* @return The list of indices of the opcode in reverse order.
|
||||
*/
|
||||
fun Method.findOpcodeIndicesReversed(filter: Instruction.() -> Boolean): List<Int> {
|
||||
val indexes = instructions
|
||||
.withIndex()
|
||||
.filter { (_, instruction) -> filter(instruction) }
|
||||
.map { (index, _) -> index }
|
||||
.reversed() // TODO: Use asReversed here to avoid creating a new list.
|
||||
|
||||
if (indexes.isEmpty()) throw PatchException("No matching instructions found in: $this")
|
||||
|
||||
return indexes
|
||||
}
|
||||
|
||||
/**
|
||||
* Called for _all_ instructions with the given literal value.
|
||||
*/
|
||||
fun BytecodePatchContext.forEachLiteralValueInstruction(
|
||||
literal: Long,
|
||||
block: MutableMethod.(literalInstructionIndex: Int) -> Unit,
|
||||
) {
|
||||
classes.forEach { classDef ->
|
||||
classDef.methods.forEach { method ->
|
||||
method.implementation?.instructions?.forEachIndexed { index, instruction ->
|
||||
if (instruction.opcode == Opcode.CONST &&
|
||||
(instruction as WideLiteralInstruction).wideLiteral == literal
|
||||
) {
|
||||
val mutableMethod = proxy(classDef).mutableClass.findMutableMethodOf(method)
|
||||
block.invoke(mutableMethod, index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the matched method early.
|
||||
*/
|
||||
fun Fingerprint.returnEarly(bool: Boolean = false) {
|
||||
val const = if (bool) "0x1" else "0x0"
|
||||
match?.let { match ->
|
||||
val stringInstructions = when (match.method.returnType.first()) {
|
||||
'L' ->
|
||||
"""
|
||||
const/4 v0, $const
|
||||
return-object v0
|
||||
"""
|
||||
'V' -> "return-void"
|
||||
'I', 'Z' ->
|
||||
"""
|
||||
const/4 v0, $const
|
||||
return v0
|
||||
"""
|
||||
else -> throw Exception("This case should never happen.")
|
||||
}
|
||||
|
||||
match.mutableMethod.addInstructions(0, stringInstructions)
|
||||
} ?: throw exception
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the matched methods early.
|
||||
*/
|
||||
fun Iterable<Fingerprint>.returnEarly(bool: Boolean = false) = forEach { fingerprint ->
|
||||
fingerprint.returnEarly(bool)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the matched methods early.
|
||||
*/
|
||||
@Deprecated("Use the Iterable version")
|
||||
fun List<Fingerprint>.returnEarly(bool: Boolean = false) = forEach { fingerprint ->
|
||||
fingerprint.returnEarly(bool)
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches this fingerprint using the classDef of a parent fingerprint match.
|
||||
*/
|
||||
fun Fingerprint.applyMatch(context: BytecodePatchContext, parentMatch: Match) =
|
||||
apply { match(context, parentMatch.classDef) }.matchOrThrow
|
||||
|
||||
/**
|
||||
* Set the custom condition for this fingerprint to check for a literal value.
|
||||
*
|
||||
* @param literalSupplier The supplier for the literal value to check for.
|
||||
*/
|
||||
// TODO: add a way for subclasses to also use their own custom fingerprint.
|
||||
fun FingerprintBuilder.literal(literalSupplier: () -> Long) {
|
||||
custom { method, _ ->
|
||||
method.containsWideLiteralInstructionValue(literalSupplier())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package app.revanced.util
|
||||
|
||||
import app.revanced.patcher.patch.ResourcePatchContext
|
||||
import app.revanced.patcher.patch.PatchException
|
||||
import app.revanced.patcher.util.Document
|
||||
import app.revanced.util.resource.BaseResource
|
||||
import org.w3c.dom.Attr
|
||||
import org.w3c.dom.Element
|
||||
import org.w3c.dom.Node
|
||||
import org.w3c.dom.NodeList
|
||||
import java.io.InputStream
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.StandardCopyOption
|
||||
|
||||
private val classLoader = object {}.javaClass.classLoader
|
||||
|
||||
/**
|
||||
* Returns a sequence for all child nodes.
|
||||
*/
|
||||
fun NodeList.asSequence() = (0 until this.length).asSequence().map { this.item(it) }
|
||||
|
||||
/**
|
||||
* Returns a sequence for all child nodes.
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun Node.childElementsSequence() = this.childNodes.asSequence().filter { it.nodeType == Node.ELEMENT_NODE } as Sequence<Element>
|
||||
|
||||
/**
|
||||
* Performs the given [action] on each child element.
|
||||
*/
|
||||
inline fun Node.forEachChildElement(action: (Element) -> Unit) =
|
||||
childElementsSequence().forEach {
|
||||
action(it)
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively traverse the DOM tree starting from the given root node.
|
||||
*
|
||||
* @param action function that is called for every node in the tree.
|
||||
*/
|
||||
fun Node.doRecursively(action: (Node) -> Unit) {
|
||||
action(this)
|
||||
for (i in 0 until this.childNodes.length) this.childNodes.item(i).doRecursively(action)
|
||||
}
|
||||
|
||||
fun Node.insertFirst(node: Node) {
|
||||
if (hasChildNodes()) {
|
||||
insertBefore(node, firstChild)
|
||||
} else {
|
||||
appendChild(node)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy resources from the current class loader to the resource directory.
|
||||
*
|
||||
* @param sourceResourceDirectory The source resource directory name.
|
||||
* @param resources The resources to copy.
|
||||
*/
|
||||
fun ResourcePatchContext.copyResources(
|
||||
sourceResourceDirectory: String,
|
||||
vararg resources: ResourceGroup,
|
||||
) {
|
||||
val targetResourceDirectory = this["res", false]
|
||||
|
||||
for (resourceGroup in resources) {
|
||||
resourceGroup.resources.forEach { resource ->
|
||||
val resourceFile = "${resourceGroup.resourceDirectoryName}/$resource"
|
||||
Files.copy(
|
||||
inputStreamFromBundledResource(sourceResourceDirectory, resourceFile)!!,
|
||||
targetResourceDirectory.resolve(resourceFile).toPath(),
|
||||
StandardCopyOption.REPLACE_EXISTING,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun inputStreamFromBundledResource(
|
||||
sourceResourceDirectory: String,
|
||||
resourceFile: String,
|
||||
): InputStream? = classLoader.getResourceAsStream("$sourceResourceDirectory/$resourceFile")
|
||||
|
||||
/**
|
||||
* Resource names mapped to their corresponding resource data.
|
||||
* @param resourceDirectoryName The name of the directory of the resource.
|
||||
* @param resources A list of resource names.
|
||||
*/
|
||||
class ResourceGroup(val resourceDirectoryName: String, vararg val resources: String)
|
||||
|
||||
/**
|
||||
* Iterate through the children of a node by its tag.
|
||||
* @param resource The xml resource.
|
||||
* @param targetTag The target xml node.
|
||||
* @param callback The callback to call when iterating over the nodes.
|
||||
*/
|
||||
fun ResourcePatchContext.iterateXmlNodeChildren(
|
||||
resource: String,
|
||||
targetTag: String,
|
||||
callback: (node: Node) -> Unit,
|
||||
) = document[classLoader.getResourceAsStream(resource)!!].use { document ->
|
||||
val stringsNode = document.getElementsByTagName(targetTag).item(0).childNodes
|
||||
for (i in 1 until stringsNode.length - 1) callback(stringsNode.item(i))
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies the specified node of the source [Document] to the target [Document].
|
||||
* @param source the source [Document].
|
||||
* @param target the target [Document]-
|
||||
* @return AutoCloseable that closes the [Document]s.
|
||||
*/
|
||||
fun String.copyXmlNode(
|
||||
source: Document,
|
||||
target: Document,
|
||||
): AutoCloseable {
|
||||
val hostNodes = source.getElementsByTagName(this).item(0).childNodes
|
||||
|
||||
val destinationNode = target.getElementsByTagName(this).item(0)
|
||||
|
||||
for (index in 0 until hostNodes.length) {
|
||||
val node = hostNodes.item(index).cloneNode(true)
|
||||
target.adoptNode(node)
|
||||
destinationNode.appendChild(node)
|
||||
}
|
||||
|
||||
return AutoCloseable {
|
||||
source.close()
|
||||
target.close()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a resource node child.
|
||||
*
|
||||
* @param resource The resource to add.
|
||||
* @param resourceCallback Called when a resource has been processed.
|
||||
*/
|
||||
internal fun Node.addResource(
|
||||
resource: BaseResource,
|
||||
resourceCallback: (BaseResource) -> Unit = { },
|
||||
) {
|
||||
appendChild(resource.serialize(ownerDocument, resourceCallback))
|
||||
}
|
||||
|
||||
internal fun org.w3c.dom.Document.getNode(tagName: String) = this.getElementsByTagName(tagName).item(0)
|
||||
|
||||
internal fun NodeList.findElementByAttributeValue(attributeName: String, value: String): Element? {
|
||||
for (i in 0 until length) {
|
||||
val node = item(i)
|
||||
if (node.nodeType == Node.ELEMENT_NODE) {
|
||||
val element = node as Element
|
||||
|
||||
if (element.getAttribute(attributeName) == value) {
|
||||
return element
|
||||
}
|
||||
|
||||
// Recursively search.
|
||||
val found = element.childNodes.findElementByAttributeValue(attributeName, value)
|
||||
if (found != null) {
|
||||
return found
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
internal fun NodeList.findElementByAttributeValueOrThrow(attributeName: String, value: String): Element {
|
||||
return findElementByAttributeValue(attributeName, value) ?: throw PatchException("Could not find: $attributeName $value")
|
||||
}
|
||||
|
||||
internal fun Element.copyAttributesFrom(oldContainer: Element) {
|
||||
// Copy attributes from the old element to the new element
|
||||
val attributes = oldContainer.attributes
|
||||
for (i in 0 until attributes.length) {
|
||||
val attr = attributes.item(i) as Attr
|
||||
setAttribute(attr.name, attr.value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package app.revanced.util
|
||||
|
||||
internal object Utils {
|
||||
internal fun String.trimIndentMultiline() =
|
||||
this.split("\n")
|
||||
.joinToString("\n") { it.trimIndent() } // Remove the leading whitespace from each line.
|
||||
.trimIndent() // Remove the leading newline.
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package app.revanced.util.resource
|
||||
|
||||
import app.revanced.util.childElementsSequence
|
||||
import org.w3c.dom.Document
|
||||
import org.w3c.dom.Node
|
||||
|
||||
/**
|
||||
* An array resource.
|
||||
*
|
||||
* @param name The name of the array resource.
|
||||
* @param items The items of the array resource.
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
class ArrayResource(
|
||||
name: String,
|
||||
val items: List<String>,
|
||||
) : BaseResource(name, "string-array") {
|
||||
override fun serialize(ownerDocument: Document, resourceCallback: (BaseResource) -> Unit) =
|
||||
super.serialize(ownerDocument, resourceCallback).apply {
|
||||
setAttribute("name", name)
|
||||
|
||||
items.forEach { item ->
|
||||
appendChild(ownerDocument.createElement("item").also { itemNode ->
|
||||
itemNode.textContent = item
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromNode(node: Node): ArrayResource {
|
||||
val key = node.attributes.getNamedItem("name").textContent
|
||||
val items = node.childElementsSequence().map { it.textContent }.toList()
|
||||
|
||||
return ArrayResource(key, items)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package app.revanced.util.resource
|
||||
|
||||
import org.w3c.dom.Document
|
||||
import org.w3c.dom.Element
|
||||
|
||||
/**
|
||||
* Base resource class for all resources.
|
||||
*
|
||||
* @param name The name of the resource.
|
||||
* @param tag The tag of the resource.
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
abstract class BaseResource(
|
||||
val name: String,
|
||||
val tag: String
|
||||
) {
|
||||
/**
|
||||
* Serialize resource element to XML.
|
||||
* Overriding methods should invoke super and operate on its return value.
|
||||
* @param ownerDocument Target document to create elements from.
|
||||
* @param resourceCallback Called when a resource has been processed.
|
||||
*/
|
||||
open fun serialize(ownerDocument: Document, resourceCallback: (BaseResource) -> Unit = { }): Element {
|
||||
return ownerDocument.createElement(tag).apply {
|
||||
setAttribute("name", name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = name.hashCode()
|
||||
result = 31 * result + tag.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as BaseResource
|
||||
|
||||
return name == other.name
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package app.revanced.util.resource
|
||||
|
||||
import app.revanced.patcher.patch.PatchException
|
||||
import org.w3c.dom.Document
|
||||
import org.w3c.dom.Node
|
||||
|
||||
/**
|
||||
* A string value.
|
||||
* Represents a string in the strings.xml file.
|
||||
*
|
||||
* @param name The name of the string.
|
||||
* @param value The value of the string.
|
||||
* @param formatted If the string is formatted. Defaults to `true`.
|
||||
*/
|
||||
class StringResource(
|
||||
name: String,
|
||||
val value: String,
|
||||
val formatted: Boolean = true,
|
||||
) : BaseResource(name, "string") {
|
||||
override fun serialize(ownerDocument: Document, resourceCallback: (BaseResource) -> Unit) =
|
||||
super.serialize(ownerDocument, resourceCallback).apply {
|
||||
// if the string is un-formatted, explicitly add the formatted attribute
|
||||
if (!formatted) setAttribute("formatted", "false")
|
||||
|
||||
if (value.contains(Regex("(?<!\\\\)['\"]")))
|
||||
throw PatchException("String $name cannot contain unescaped quotes in value \"$value\".")
|
||||
|
||||
textContent = value
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromNode(node: Node): StringResource {
|
||||
val name = node.attributes.getNamedItem("name").textContent
|
||||
val value = node.textContent
|
||||
// TODO: Should this be true by default? It is too in the constructor of StringResource.
|
||||
val formatted = node.attributes.getNamedItem("formatted")?.textContent?.toBoolean() ?: true
|
||||
|
||||
return StringResource(name, value, formatted)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user