mirror of
https://github.com/ReVanced/revanced-patcher.git
synced 2024-11-09 16:52:09 +01:00
feat: Read and write arbitrary files in APK files
This commit allows reading and writing arbitrary files in an APK file. Additionally it allows deleting files from APK files. A `RawResourcePatch` class has been added which has access to `ResourceContext` but ReVanced Patcher will not decode APK resources. A regular `ResourcePatch` can read and write arbitrary files from an APK file, unless they are decoded to `PatcherConfig.apkFiles`. On attempt to get a file from `PatcherConfig.apkFiles` if the second parameter is true, it will read and write the raw resource file from the original APK to `PatcherConfig.apkFiles` if it does not exist. With this commit, many APIs have been deprecated as well, such as `DomFileEditor` and instead a `Document` has been added.
This commit is contained in:
parent
64dd1526cd
commit
f1d7217495
23 changed files with 828 additions and 447 deletions
3
.editorconfig
Normal file
3
.editorconfig
Normal file
|
@ -0,0 +1,3 @@
|
|||
[*.{kt,kts}]
|
||||
ktlint_code_style = intellij_idea
|
||||
ktlint_standard_no-wildcard-imports = disabled
|
|
@ -1,5 +1,9 @@
|
|||
public abstract interface class app/revanced/patcher/IntegrationsConsumer {
|
||||
public abstract fun acceptIntegrations (Ljava/util/List;)V
|
||||
public abstract fun acceptIntegrations (Ljava/util/Set;)V
|
||||
}
|
||||
|
||||
public abstract interface annotation class app/revanced/patcher/InternalApi : java/lang/annotation/Annotation {
|
||||
}
|
||||
|
||||
public final class app/revanced/patcher/PackageMetadata {
|
||||
|
@ -41,8 +45,10 @@ public abstract interface class app/revanced/patcher/PatchExecutorFunction : jav
|
|||
}
|
||||
|
||||
public final class app/revanced/patcher/Patcher : app/revanced/patcher/IntegrationsConsumer, app/revanced/patcher/PatchExecutorFunction, app/revanced/patcher/PatchesConsumer, java/io/Closeable, java/util/function/Supplier {
|
||||
public fun <init> (Lapp/revanced/patcher/PatcherConfig;)V
|
||||
public fun <init> (Lapp/revanced/patcher/PatcherOptions;)V
|
||||
public fun acceptIntegrations (Ljava/util/List;)V
|
||||
public fun acceptIntegrations (Ljava/util/Set;)V
|
||||
public fun acceptPatches (Ljava/util/List;)V
|
||||
public fun acceptPatches (Ljava/util/Set;)V
|
||||
public synthetic fun apply (Ljava/lang/Object;)Ljava/lang/Object;
|
||||
|
@ -53,6 +59,11 @@ public final class app/revanced/patcher/Patcher : app/revanced/patcher/Integrati
|
|||
public final fun getContext ()Lapp/revanced/patcher/PatcherContext;
|
||||
}
|
||||
|
||||
public final class app/revanced/patcher/PatcherConfig {
|
||||
public fun <init> (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;Z)V
|
||||
public synthetic fun <init> (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||
}
|
||||
|
||||
public final class app/revanced/patcher/PatcherContext {
|
||||
public final fun getPackageMetadata ()Lapp/revanced/patcher/PackageMetadata;
|
||||
}
|
||||
|
@ -86,8 +97,10 @@ public final class app/revanced/patcher/PatcherResult {
|
|||
public static synthetic fun copy$default (Lapp/revanced/patcher/PatcherResult;Ljava/util/List;Ljava/io/File;Ljava/util/List;ILjava/lang/Object;)Lapp/revanced/patcher/PatcherResult;
|
||||
public fun equals (Ljava/lang/Object;)Z
|
||||
public final fun getDexFiles ()Ljava/util/List;
|
||||
public final fun getDexFiles ()Ljava/util/Set;
|
||||
public final fun getDoNotCompress ()Ljava/util/List;
|
||||
public final fun getResourceFile ()Ljava/io/File;
|
||||
public final fun getResources ()Lapp/revanced/patcher/PatcherResult$PatchedResources;
|
||||
public fun hashCode ()I
|
||||
public fun toString ()Ljava/lang/String;
|
||||
}
|
||||
|
@ -98,6 +111,13 @@ public final class app/revanced/patcher/PatcherResult$PatchedDexFile {
|
|||
public final fun getStream ()Ljava/io/InputStream;
|
||||
}
|
||||
|
||||
public final class app/revanced/patcher/PatcherResult$PatchedResources {
|
||||
public final fun getDeleteResources ()Ljava/util/Set;
|
||||
public final fun getDoNotCompress ()Ljava/util/Set;
|
||||
public final fun getOtherResources ()Ljava/io/File;
|
||||
public final fun getResourcesApk ()Ljava/io/File;
|
||||
}
|
||||
|
||||
public abstract interface class app/revanced/patcher/PatchesConsumer {
|
||||
public abstract fun acceptPatches (Ljava/util/List;)V
|
||||
public abstract fun acceptPatches (Ljava/util/Set;)V
|
||||
|
@ -111,7 +131,7 @@ public final class app/revanced/patcher/data/BytecodeContext : app/revanced/patc
|
|||
public final fun findClass (Ljava/lang/String;)Lapp/revanced/patcher/util/proxy/ClassProxy;
|
||||
public final fun findClass (Lkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/util/proxy/ClassProxy;
|
||||
public synthetic fun get ()Ljava/lang/Object;
|
||||
public fun get ()Ljava/util/List;
|
||||
public fun get ()Ljava/util/Set;
|
||||
public final fun getClasses ()Lapp/revanced/patcher/util/ProxyClassList;
|
||||
public final fun proxy (Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Lapp/revanced/patcher/util/proxy/ClassProxy;
|
||||
public final fun toMethodWalker (Lcom/android/tools/smali/dexlib2/iface/Method;)Lapp/revanced/patcher/util/method/MethodWalker;
|
||||
|
@ -121,11 +141,21 @@ public abstract interface class app/revanced/patcher/data/Context : java/util/fu
|
|||
}
|
||||
|
||||
public final class app/revanced/patcher/data/ResourceContext : app/revanced/patcher/data/Context, java/lang/Iterable, kotlin/jvm/internal/markers/KMappedMarker {
|
||||
public fun get ()Ljava/io/File;
|
||||
public fun get ()Lapp/revanced/patcher/PatcherResult$PatchedResources;
|
||||
public synthetic fun get ()Ljava/lang/Object;
|
||||
public final fun get (Ljava/lang/String;)Ljava/io/File;
|
||||
public final fun get (Ljava/lang/String;Z)Ljava/io/File;
|
||||
public static synthetic fun get$default (Lapp/revanced/patcher/data/ResourceContext;Ljava/lang/String;ZILjava/lang/Object;)Ljava/io/File;
|
||||
public final fun getDocument ()Lapp/revanced/patcher/data/ResourceContext$DocumentOperatable;
|
||||
public final fun getXmlEditor ()Lapp/revanced/patcher/data/ResourceContext$XmlFileHolder;
|
||||
public fun iterator ()Ljava/util/Iterator;
|
||||
public final fun stageDelete (Lkotlin/jvm/functions/Function1;)Z
|
||||
}
|
||||
|
||||
public final class app/revanced/patcher/data/ResourceContext$DocumentOperatable {
|
||||
public fun <init> (Lapp/revanced/patcher/data/ResourceContext;)V
|
||||
public final fun get (Ljava/io/InputStream;)Lapp/revanced/patcher/util/Document;
|
||||
public final fun get (Ljava/lang/String;)Lapp/revanced/patcher/util/Document;
|
||||
}
|
||||
|
||||
public final class app/revanced/patcher/data/ResourceContext$XmlFileHolder {
|
||||
|
@ -279,6 +309,12 @@ public final class app/revanced/patcher/patch/PatchResult {
|
|||
public final fun getPatch ()Lapp/revanced/patcher/patch/Patch;
|
||||
}
|
||||
|
||||
public abstract class app/revanced/patcher/patch/RawResourcePatch : app/revanced/patcher/patch/Patch {
|
||||
public fun <init> ()V
|
||||
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;ZZ)V
|
||||
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;ZZILkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||
}
|
||||
|
||||
public abstract class app/revanced/patcher/patch/ResourcePatch : app/revanced/patcher/patch/Patch {
|
||||
public fun <init> ()V
|
||||
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;ZZ)V
|
||||
|
@ -392,6 +428,78 @@ public final class app/revanced/patcher/patch/options/PatchOptions : java/util/M
|
|||
public final fun values ()Ljava/util/Collection;
|
||||
}
|
||||
|
||||
public final class app/revanced/patcher/util/Document : java/io/Closeable, org/w3c/dom/Document {
|
||||
public fun adoptNode (Lorg/w3c/dom/Node;)Lorg/w3c/dom/Node;
|
||||
public fun appendChild (Lorg/w3c/dom/Node;)Lorg/w3c/dom/Node;
|
||||
public fun cloneNode (Z)Lorg/w3c/dom/Node;
|
||||
public fun close ()V
|
||||
public fun compareDocumentPosition (Lorg/w3c/dom/Node;)S
|
||||
public fun createAttribute (Ljava/lang/String;)Lorg/w3c/dom/Attr;
|
||||
public fun createAttributeNS (Ljava/lang/String;Ljava/lang/String;)Lorg/w3c/dom/Attr;
|
||||
public fun createCDATASection (Ljava/lang/String;)Lorg/w3c/dom/CDATASection;
|
||||
public fun createComment (Ljava/lang/String;)Lorg/w3c/dom/Comment;
|
||||
public fun createDocumentFragment ()Lorg/w3c/dom/DocumentFragment;
|
||||
public fun createElement (Ljava/lang/String;)Lorg/w3c/dom/Element;
|
||||
public fun createElementNS (Ljava/lang/String;Ljava/lang/String;)Lorg/w3c/dom/Element;
|
||||
public fun createEntityReference (Ljava/lang/String;)Lorg/w3c/dom/EntityReference;
|
||||
public fun createProcessingInstruction (Ljava/lang/String;Ljava/lang/String;)Lorg/w3c/dom/ProcessingInstruction;
|
||||
public fun createTextNode (Ljava/lang/String;)Lorg/w3c/dom/Text;
|
||||
public fun getAttributes ()Lorg/w3c/dom/NamedNodeMap;
|
||||
public fun getBaseURI ()Ljava/lang/String;
|
||||
public fun getChildNodes ()Lorg/w3c/dom/NodeList;
|
||||
public fun getDoctype ()Lorg/w3c/dom/DocumentType;
|
||||
public fun getDocumentElement ()Lorg/w3c/dom/Element;
|
||||
public fun getDocumentURI ()Ljava/lang/String;
|
||||
public fun getDomConfig ()Lorg/w3c/dom/DOMConfiguration;
|
||||
public fun getElementById (Ljava/lang/String;)Lorg/w3c/dom/Element;
|
||||
public fun getElementsByTagName (Ljava/lang/String;)Lorg/w3c/dom/NodeList;
|
||||
public fun getElementsByTagNameNS (Ljava/lang/String;Ljava/lang/String;)Lorg/w3c/dom/NodeList;
|
||||
public fun getFeature (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Object;
|
||||
public fun getFirstChild ()Lorg/w3c/dom/Node;
|
||||
public fun getImplementation ()Lorg/w3c/dom/DOMImplementation;
|
||||
public fun getInputEncoding ()Ljava/lang/String;
|
||||
public fun getLastChild ()Lorg/w3c/dom/Node;
|
||||
public fun getLocalName ()Ljava/lang/String;
|
||||
public fun getNamespaceURI ()Ljava/lang/String;
|
||||
public fun getNextSibling ()Lorg/w3c/dom/Node;
|
||||
public fun getNodeName ()Ljava/lang/String;
|
||||
public fun getNodeType ()S
|
||||
public fun getNodeValue ()Ljava/lang/String;
|
||||
public fun getOwnerDocument ()Lorg/w3c/dom/Document;
|
||||
public fun getParentNode ()Lorg/w3c/dom/Node;
|
||||
public fun getPrefix ()Ljava/lang/String;
|
||||
public fun getPreviousSibling ()Lorg/w3c/dom/Node;
|
||||
public fun getStrictErrorChecking ()Z
|
||||
public fun getTextContent ()Ljava/lang/String;
|
||||
public fun getUserData (Ljava/lang/String;)Ljava/lang/Object;
|
||||
public fun getXmlEncoding ()Ljava/lang/String;
|
||||
public fun getXmlStandalone ()Z
|
||||
public fun getXmlVersion ()Ljava/lang/String;
|
||||
public fun hasAttributes ()Z
|
||||
public fun hasChildNodes ()Z
|
||||
public fun importNode (Lorg/w3c/dom/Node;Z)Lorg/w3c/dom/Node;
|
||||
public fun insertBefore (Lorg/w3c/dom/Node;Lorg/w3c/dom/Node;)Lorg/w3c/dom/Node;
|
||||
public fun isDefaultNamespace (Ljava/lang/String;)Z
|
||||
public fun isEqualNode (Lorg/w3c/dom/Node;)Z
|
||||
public fun isSameNode (Lorg/w3c/dom/Node;)Z
|
||||
public fun isSupported (Ljava/lang/String;Ljava/lang/String;)Z
|
||||
public fun lookupNamespaceURI (Ljava/lang/String;)Ljava/lang/String;
|
||||
public fun lookupPrefix (Ljava/lang/String;)Ljava/lang/String;
|
||||
public fun normalize ()V
|
||||
public fun normalizeDocument ()V
|
||||
public fun removeChild (Lorg/w3c/dom/Node;)Lorg/w3c/dom/Node;
|
||||
public fun renameNode (Lorg/w3c/dom/Node;Ljava/lang/String;Ljava/lang/String;)Lorg/w3c/dom/Node;
|
||||
public fun replaceChild (Lorg/w3c/dom/Node;Lorg/w3c/dom/Node;)Lorg/w3c/dom/Node;
|
||||
public fun setDocumentURI (Ljava/lang/String;)V
|
||||
public fun setNodeValue (Ljava/lang/String;)V
|
||||
public fun setPrefix (Ljava/lang/String;)V
|
||||
public fun setStrictErrorChecking (Z)V
|
||||
public fun setTextContent (Ljava/lang/String;)V
|
||||
public fun setUserData (Ljava/lang/String;Ljava/lang/Object;Lorg/w3c/dom/UserDataHandler;)Ljava/lang/Object;
|
||||
public fun setXmlStandalone (Z)V
|
||||
public fun setXmlVersion (Ljava/lang/String;)V
|
||||
}
|
||||
|
||||
public final class app/revanced/patcher/util/DomFileEditor : java/io/Closeable {
|
||||
public fun <init> (Ljava/io/File;)V
|
||||
public fun close ()V
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
plugins {
|
||||
kotlin("jvm") version "1.9.10"
|
||||
alias(libs.plugins.kotlin)
|
||||
alias(libs.plugins.binary.compatibility.validator)
|
||||
`maven-publish`
|
||||
signing
|
||||
|
@ -36,7 +36,11 @@ dependencies {
|
|||
implementation(libs.apktool.lib)
|
||||
implementation(libs.kotlin.reflect)
|
||||
|
||||
compileOnly(libs.android)
|
||||
// TODO: Convert project to KMP.
|
||||
compileOnly(libs.android) {
|
||||
// Exclude, otherwise the org.w3c.dom API breaks.
|
||||
exclude(group = "xerces", module = "xmlParserAPIs")
|
||||
}
|
||||
|
||||
testImplementation(libs.kotlin.test)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
org.gradle.parallel = true
|
||||
org.gradle.caching = true
|
||||
kotlin.code.style = official
|
||||
version = 19.2.1-dev.1
|
||||
|
|
|
@ -1,19 +1,18 @@
|
|||
[versions]
|
||||
android = "4.1.1.4"
|
||||
kotlin-reflect = "1.9.10"
|
||||
apktool-lib = "2.9.1"
|
||||
kotlin-test = "1.9.20"
|
||||
kotlin = "1.9.22"
|
||||
kotlinx-coroutines-core = "1.7.3"
|
||||
multidexlib2 = "3.0.3.r3"
|
||||
smali = "3.0.3"
|
||||
xpp3 = "1.1.4c"
|
||||
smali = "3.0.4"
|
||||
binary-compatibility-validator = "0.13.2"
|
||||
xpp3 = "1.1.4c"
|
||||
|
||||
[libraries]
|
||||
android = { module = "com.google.android:android", version.ref = "android" }
|
||||
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin-reflect" }
|
||||
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
|
||||
apktool-lib = { module = "app.revanced:apktool", version.ref = "apktool-lib" }
|
||||
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin-test" }
|
||||
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
|
||||
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines-core" }
|
||||
multidexlib2 = { module = "app.revanced:multidexlib2", version.ref = "multidexlib2" }
|
||||
smali = { module = "com.android.tools.smali:smali", version.ref = "smali" }
|
||||
|
@ -21,3 +20,4 @@ xpp3 = { module = "xpp3:xpp3", version.ref = "xpp3" }
|
|||
|
||||
[plugins]
|
||||
binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" }
|
||||
kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
|
@ -4,5 +4,8 @@ import java.io.File
|
|||
|
||||
@FunctionalInterface
|
||||
interface IntegrationsConsumer {
|
||||
fun acceptIntegrations(integrations: Set<File>)
|
||||
|
||||
@Deprecated("Use acceptIntegrations(Set<File>) instead.")
|
||||
fun acceptIntegrations(integrations: List<File>)
|
||||
}
|
||||
|
|
7
src/main/kotlin/app/revanced/patcher/InternalApi.kt
Normal file
7
src/main/kotlin/app/revanced/patcher/InternalApi.kt
Normal file
|
@ -0,0 +1,7 @@
|
|||
package app.revanced.patcher
|
||||
|
||||
@RequiresOptIn(
|
||||
level = RequiresOptIn.Level.ERROR,
|
||||
message = "This is an internal API, don't rely on it.",
|
||||
)
|
||||
annotation class InternalApi
|
|
@ -4,6 +4,8 @@ import brut.androlib.apk.ApkInfo
|
|||
|
||||
/**
|
||||
* Metadata about a package.
|
||||
*
|
||||
* @param apkInfo The [ApkInfo] of the apk file.
|
||||
*/
|
||||
class PackageMetadata internal constructor(internal val apkInfo: ApkInfo) {
|
||||
lateinit var packageName: String
|
||||
|
|
|
@ -12,34 +12,38 @@ import java.util.function.Supplier
|
|||
import java.util.logging.Logger
|
||||
|
||||
/**
|
||||
* ReVanced Patcher.
|
||||
* A Patcher.
|
||||
*
|
||||
* @param options The options for the patcher.
|
||||
* @param config The configuration to use for the patcher.
|
||||
*/
|
||||
class Patcher(
|
||||
private val options: PatcherOptions,
|
||||
private val config: PatcherConfig,
|
||||
) : PatchExecutorFunction, PatchesConsumer, IntegrationsConsumer, Supplier<PatcherResult>, Closeable {
|
||||
private val logger = Logger.getLogger(Patcher::class.java.name)
|
||||
|
||||
/**
|
||||
* The context of ReVanced [Patcher].
|
||||
* This holds the current state of the patcher.
|
||||
* A context for the patcher containing the current state of the patcher.
|
||||
*/
|
||||
val context = PatcherContext(options)
|
||||
val context = PatcherContext(config)
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated("Use Patcher(PatcherConfig) instead.")
|
||||
constructor(
|
||||
patcherOptions: PatcherOptions,
|
||||
) : this(
|
||||
PatcherConfig(
|
||||
patcherOptions.inputFile,
|
||||
patcherOptions.resourceCachePath,
|
||||
patcherOptions.aaptBinaryPath,
|
||||
patcherOptions.frameworkFileDirectory,
|
||||
patcherOptions.multithreadingDexFileWriter,
|
||||
),
|
||||
)
|
||||
|
||||
init {
|
||||
context.resourceContext.decodeResources(ResourceContext.ResourceDecodingMode.MANIFEST_ONLY)
|
||||
context.resourceContext.decodeResources(ResourceContext.ResourceMode.NONE)
|
||||
}
|
||||
|
||||
// TODO: Fix circular dependency detection.
|
||||
// /**
|
||||
// * Add [Patch]es to ReVanced [Patcher].
|
||||
// * It is not guaranteed that all supplied [Patch]es will be accepted, if an exception is thrown.
|
||||
// *
|
||||
// * @param patches The [Patch]es to add.
|
||||
// * @throws PatcherException.CircularDependencyException If a circular dependency is detected.
|
||||
// */
|
||||
|
||||
/**
|
||||
* Add [Patch]es to ReVanced [Patcher].
|
||||
*
|
||||
|
@ -61,29 +65,15 @@ class Patcher(
|
|||
}
|
||||
|
||||
// Add all patches and their dependencies to the context.
|
||||
for (patch in patches) context.executablePatches.putIfAbsent(patch::class, patch) ?: run {
|
||||
context.allPatches[patch::class] = patch
|
||||
patches.forEach { patch ->
|
||||
context.executablePatches.putIfAbsent(patch::class, patch) ?: run {
|
||||
context.allPatches[patch::class] = patch
|
||||
|
||||
patch.dependencies?.forEach { it.putDependenciesRecursively() }
|
||||
}
|
||||
|
||||
/* TODO: Fix circular dependency detection.
|
||||
val graph = mutableMapOf<PatchClass, MutableList<PatchClass>>()
|
||||
fun PatchClass.visit() {
|
||||
if (this in graph) return
|
||||
|
||||
val group = graph.getOrPut(this) { mutableListOf(this) }
|
||||
|
||||
val dependencies = context.allPatches[this]!!.manifest.dependencies ?: return
|
||||
dependencies.forEach { dependency ->
|
||||
if (group == graph[dependency])
|
||||
throw PatcherException.CircularDependencyException(context.allPatches[this]!!.manifest.name)
|
||||
|
||||
graph[dependency] = group.apply { add(dependency) }
|
||||
dependency.visit()
|
||||
patch.dependencies?.forEach { it.putDependenciesRecursively() }
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// TODO: Detect circular dependencies.
|
||||
|
||||
/**
|
||||
* Returns true if at least one patch or its dependencies matches the given predicate.
|
||||
|
@ -96,12 +86,15 @@ class Patcher(
|
|||
} ?: false
|
||||
|
||||
context.allPatches.values.let { patches ->
|
||||
// Determine, if resource patching is required.
|
||||
for (patch in patches)
|
||||
if (patch.anyRecursively { patch is ResourcePatch }) {
|
||||
options.resourceDecodingMode = ResourceContext.ResourceDecodingMode.FULL
|
||||
break
|
||||
}
|
||||
// Determine the resource mode.
|
||||
|
||||
config.resourceMode = if (patches.any { patch -> patch.anyRecursively { it is ResourcePatch } }) {
|
||||
ResourceContext.ResourceMode.FULL
|
||||
} else if (patches.any { patch -> patch.anyRecursively { it is RawResourcePatch } }) {
|
||||
ResourceContext.ResourceMode.RAW_ONLY
|
||||
} else {
|
||||
ResourceContext.ResourceMode.NONE
|
||||
}
|
||||
|
||||
// Determine, if merging integrations is required.
|
||||
for (patch in patches)
|
||||
|
@ -117,10 +110,16 @@ class Patcher(
|
|||
*
|
||||
* @param integrations The integrations to add. Must be a DEX file or container of DEX files.
|
||||
*/
|
||||
override fun acceptIntegrations(integrations: List<File>) {
|
||||
override fun acceptIntegrations(integrations: Set<File>) {
|
||||
context.bytecodeContext.integrations.addAll(integrations)
|
||||
}
|
||||
|
||||
@Deprecated(
|
||||
"Use acceptIntegrations(Set<File>) instead.",
|
||||
ReplaceWith("acceptIntegrations(integrations.toSet())"),
|
||||
)
|
||||
override fun acceptIntegrations(integrations: List<File>) = acceptIntegrations(integrations.toSet())
|
||||
|
||||
/**
|
||||
* Execute [Patch]es that were added to ReVanced [Patcher].
|
||||
*
|
||||
|
@ -173,6 +172,9 @@ class Patcher(
|
|||
patch.fingerprints.resolveUsingLookupMap(context.bytecodeContext)
|
||||
patch.execute(context.bytecodeContext)
|
||||
}
|
||||
is RawResourcePatch -> {
|
||||
patch.execute(context.resourceContext)
|
||||
}
|
||||
is ResourcePatch -> {
|
||||
patch.execute(context.resourceContext)
|
||||
}
|
||||
|
@ -191,8 +193,8 @@ class Patcher(
|
|||
LookupMap.initializeLookupMaps(context.bytecodeContext)
|
||||
|
||||
// Prevent from decoding the app manifest twice if it is not needed.
|
||||
if (options.resourceDecodingMode == ResourceContext.ResourceDecodingMode.FULL) {
|
||||
context.resourceContext.decodeResources(ResourceContext.ResourceDecodingMode.FULL)
|
||||
if (config.resourceMode != ResourceContext.ResourceMode.NONE) {
|
||||
context.resourceContext.decodeResources(config.resourceMode)
|
||||
}
|
||||
|
||||
logger.info("Executing patches")
|
||||
|
@ -259,10 +261,10 @@ class Patcher(
|
|||
*
|
||||
* @return The [PatcherResult] containing the patched input files.
|
||||
*/
|
||||
@OptIn(InternalApi::class)
|
||||
override fun get() =
|
||||
PatcherResult(
|
||||
context.bytecodeContext.get(),
|
||||
context.resourceContext.get(),
|
||||
context.packageMetadata.apkInfo.doNotCompress?.toList(),
|
||||
)
|
||||
}
|
||||
|
|
72
src/main/kotlin/app/revanced/patcher/PatcherConfig.kt
Normal file
72
src/main/kotlin/app/revanced/patcher/PatcherConfig.kt
Normal file
|
@ -0,0 +1,72 @@
|
|||
package app.revanced.patcher
|
||||
|
||||
import app.revanced.patcher.data.ResourceContext
|
||||
import brut.androlib.Config
|
||||
import java.io.File
|
||||
import java.util.logging.Logger
|
||||
|
||||
/**
|
||||
* The configuration for the patcher.
|
||||
*
|
||||
* @param apkFile The apk file to patch.
|
||||
* @param temporaryFilesPath A path to a folder to store temporary files in.
|
||||
* @param aaptBinaryPath A path to a custom aapt binary.
|
||||
* @param frameworkFileDirectory A path to the directory to cache the framework file in.
|
||||
* @param multithreadingDexFileWriter Whether to use multiple threads for writing dex files.
|
||||
* This has impact on memory usage and performance.
|
||||
*/
|
||||
class PatcherConfig(
|
||||
internal val apkFile: File,
|
||||
private val temporaryFilesPath: File = File("revanced-temporary-files"),
|
||||
aaptBinaryPath: String? = null,
|
||||
frameworkFileDirectory: String? = null,
|
||||
internal val multithreadingDexFileWriter: Boolean = false,
|
||||
) {
|
||||
private val logger = Logger.getLogger(PatcherConfig::class.java.name)
|
||||
|
||||
/**
|
||||
* The mode to use for resource decoding and compiling.
|
||||
*
|
||||
* @see ResourceContext.ResourceMode
|
||||
*/
|
||||
internal var resourceMode = ResourceContext.ResourceMode.NONE
|
||||
|
||||
/**
|
||||
* The configuration for decoding and compiling resources.
|
||||
*/
|
||||
internal val resourceConfig =
|
||||
Config.getDefaultConfig().apply {
|
||||
useAapt2 = true
|
||||
aaptPath = aaptBinaryPath ?: ""
|
||||
frameworkDirectory = frameworkFileDirectory
|
||||
}
|
||||
|
||||
/**
|
||||
* The path to the temporary apk files directory.
|
||||
*/
|
||||
internal val apkFiles = temporaryFilesPath.resolve("apk")
|
||||
|
||||
/**
|
||||
* The path to the temporary patched files directory.
|
||||
*/
|
||||
internal val patchedFiles = temporaryFilesPath.resolve("patched")
|
||||
|
||||
/**
|
||||
* Initialize the temporary files' directories.
|
||||
* This will delete the existing temporary files directory if it exists.
|
||||
*/
|
||||
internal fun initializeTemporaryFilesDirectories() {
|
||||
temporaryFilesPath.apply {
|
||||
if (exists()) {
|
||||
logger.info("Deleting existing temporary files directory")
|
||||
|
||||
if (!deleteRecursively()) {
|
||||
logger.severe("Failed to delete existing temporary files directory")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
apkFiles.mkdirs()
|
||||
patchedFiles.mkdirs()
|
||||
}
|
||||
}
|
|
@ -7,15 +7,16 @@ import brut.androlib.apk.ApkInfo
|
|||
import brut.directory.ExtFile
|
||||
|
||||
/**
|
||||
* A context for ReVanced [Patcher].
|
||||
* A context for the patcher containing the current state of the patcher.
|
||||
*
|
||||
* @param options The [PatcherOptions] used to create this context.
|
||||
* @param config The configuration for the patcher.
|
||||
*/
|
||||
class PatcherContext internal constructor(options: PatcherOptions) {
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
class PatcherContext internal constructor(config: PatcherConfig) {
|
||||
/**
|
||||
* [PackageMetadata] of the supplied [PatcherOptions.inputFile].
|
||||
* [PackageMetadata] of the supplied [PatcherConfig.apkFile].
|
||||
*/
|
||||
val packageMetadata = PackageMetadata(ApkInfo(ExtFile(options.inputFile)))
|
||||
val packageMetadata = PackageMetadata(ApkInfo(ExtFile(config.apkFile)))
|
||||
|
||||
/**
|
||||
* The map of [Patch]es associated by their [PatchClass].
|
||||
|
@ -28,14 +29,12 @@ class PatcherContext internal constructor(options: PatcherOptions) {
|
|||
internal val allPatches = mutableMapOf<PatchClass, Patch<*>>()
|
||||
|
||||
/**
|
||||
* The [ResourceContext] of this [PatcherContext].
|
||||
* This holds the current state of the resources.
|
||||
* A context for the patcher containing the current state of the resources.
|
||||
*/
|
||||
internal val resourceContext = ResourceContext(this, options)
|
||||
internal val resourceContext = ResourceContext(packageMetadata, config)
|
||||
|
||||
/**
|
||||
* The [BytecodeContext] of this [PatcherContext].
|
||||
* This holds the current state of the bytecode.
|
||||
* A context for the patcher containing the current state of the bytecode.
|
||||
*/
|
||||
internal val bytecodeContext = BytecodeContext(options)
|
||||
internal val bytecodeContext = BytecodeContext(config)
|
||||
}
|
||||
|
|
|
@ -1,19 +1,8 @@
|
|||
package app.revanced.patcher
|
||||
|
||||
import app.revanced.patcher.data.ResourceContext
|
||||
import brut.androlib.Config
|
||||
import java.io.File
|
||||
import java.util.logging.Logger
|
||||
|
||||
/**
|
||||
* Options for ReVanced [Patcher].
|
||||
* @param inputFile The input file to patch.
|
||||
* @param resourceCachePath The path to the directory to use for caching resources.
|
||||
* @param aaptBinaryPath The path to a custom aapt binary.
|
||||
* @param frameworkFileDirectory The path to the directory to cache the framework file in.
|
||||
* @param multithreadingDexFileWriter Whether to use multiple threads for writing dex files.
|
||||
* This can impact memory usage.
|
||||
*/
|
||||
@Deprecated("Use PatcherConfig instead.")
|
||||
data class PatcherOptions(
|
||||
internal val inputFile: File,
|
||||
internal val resourceCachePath: File = File("revanced-resource-cache"),
|
||||
|
@ -21,34 +10,16 @@ data class PatcherOptions(
|
|||
internal val frameworkFileDirectory: String? = null,
|
||||
internal val multithreadingDexFileWriter: Boolean = false,
|
||||
) {
|
||||
private val logger = Logger.getLogger(PatcherOptions::class.java.name)
|
||||
@Deprecated("This method will be removed in the future.")
|
||||
fun recreateResourceCacheDirectory(): File {
|
||||
PatcherConfig(
|
||||
inputFile,
|
||||
resourceCachePath,
|
||||
aaptBinaryPath,
|
||||
frameworkFileDirectory,
|
||||
multithreadingDexFileWriter,
|
||||
).initializeTemporaryFilesDirectories()
|
||||
|
||||
/**
|
||||
* The mode to use for resource decoding.
|
||||
* @see ResourceContext.ResourceDecodingMode
|
||||
*/
|
||||
internal var resourceDecodingMode = ResourceContext.ResourceDecodingMode.MANIFEST_ONLY
|
||||
|
||||
/**
|
||||
* The configuration to use for resource decoding and compiling.
|
||||
*/
|
||||
internal val resourceConfig =
|
||||
Config.getDefaultConfig().apply {
|
||||
useAapt2 = true
|
||||
aaptPath = aaptBinaryPath ?: ""
|
||||
frameworkDirectory = frameworkFileDirectory
|
||||
}
|
||||
|
||||
fun recreateResourceCacheDirectory() =
|
||||
resourceCachePath.also {
|
||||
if (it.exists()) {
|
||||
logger.info("Deleting existing resource cache directory")
|
||||
|
||||
if (!it.deleteRecursively()) {
|
||||
logger.severe("Failed to delete existing resource cache directory")
|
||||
}
|
||||
}
|
||||
|
||||
it.mkdirs()
|
||||
}
|
||||
return resourceCachePath
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,22 +2,121 @@ package app.revanced.patcher
|
|||
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import kotlin.jvm.internal.Intrinsics
|
||||
|
||||
/**
|
||||
* The result of a patcher.
|
||||
*
|
||||
* @param dexFiles The patched dex files.
|
||||
* @param resourceFile File containing resources that need to be extracted into the APK.
|
||||
* @param doNotCompress List of relative paths of files to exclude from compressing.
|
||||
* @param resources The patched resources.
|
||||
*/
|
||||
data class PatcherResult(
|
||||
val dexFiles: List<PatchedDexFile>,
|
||||
val resourceFile: File?,
|
||||
val doNotCompress: List<String>? = null,
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
class PatcherResult internal constructor(
|
||||
val dexFiles: Set<PatchedDexFile>,
|
||||
val resources: PatchedResources?,
|
||||
) {
|
||||
@Deprecated("This method is not used anymore")
|
||||
constructor(
|
||||
dexFiles: List<PatchedDexFile>,
|
||||
resourceFile: File?,
|
||||
doNotCompress: List<String>? = null,
|
||||
) : this(dexFiles.toSet(), PatchedResources(resourceFile, null, doNotCompress?.toSet() ?: emptySet(), emptySet()))
|
||||
|
||||
@Deprecated("This method is not used anymore")
|
||||
fun component1(): List<PatchedDexFile> {
|
||||
return dexFiles.toList()
|
||||
}
|
||||
|
||||
@Deprecated("This method is not used anymore")
|
||||
fun component2(): File? {
|
||||
return resources?.resourcesApk
|
||||
}
|
||||
|
||||
@Deprecated("This method is not used anymore")
|
||||
fun component3(): List<String>? {
|
||||
return resources?.doNotCompress?.toList()
|
||||
}
|
||||
|
||||
@Deprecated("This method is not used anymore")
|
||||
fun copy(
|
||||
dexFiles: List<PatchedDexFile>,
|
||||
resourceFile: File?,
|
||||
doNotCompress: List<String>? = null,
|
||||
): PatcherResult {
|
||||
return PatcherResult(
|
||||
dexFiles.toSet(),
|
||||
PatchedResources(
|
||||
resourceFile,
|
||||
null,
|
||||
doNotCompress?.toSet() ?: emptySet(),
|
||||
emptySet(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Deprecated("This method is not used anymore")
|
||||
override fun toString(): String {
|
||||
return (("PatcherResult(dexFiles=" + this.dexFiles + ", resourceFile=" + this.resources?.resourcesApk) + ", doNotCompress=" + this.resources?.doNotCompress) + ")"
|
||||
}
|
||||
|
||||
@Deprecated("This method is not used anymore")
|
||||
override fun hashCode(): Int {
|
||||
val result = dexFiles.hashCode()
|
||||
return (
|
||||
(
|
||||
(result * 31) +
|
||||
(if (this.resources?.resourcesApk == null) 0 else this.resources?.resourcesApk.hashCode())
|
||||
) * 31
|
||||
) +
|
||||
(if (this.resources?.doNotCompress == null) 0 else this.resources?.doNotCompress.hashCode())
|
||||
}
|
||||
|
||||
@Deprecated("This method is not used anymore")
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) {
|
||||
return true
|
||||
}
|
||||
if (other is PatcherResult) {
|
||||
return Intrinsics.areEqual(this.dexFiles, other.dexFiles) && Intrinsics.areEqual(
|
||||
this.resources?.resourcesApk,
|
||||
other.resources?.resourcesApk,
|
||||
) && Intrinsics.areEqual(this.resources?.doNotCompress, other.resources?.doNotCompress)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@Deprecated("This method is not used anymore")
|
||||
fun getDexFiles() = component1()
|
||||
|
||||
@Deprecated("This method is not used anymore")
|
||||
fun getResourceFile() = component2()
|
||||
|
||||
@Deprecated("This method is not used anymore")
|
||||
fun getDoNotCompress() = component3()
|
||||
|
||||
/**
|
||||
* Wrapper for dex files.
|
||||
* A dex file.
|
||||
*
|
||||
* @param name The original name of the dex file.
|
||||
* @param stream The dex file as [InputStream].
|
||||
*/
|
||||
class PatchedDexFile(val name: String, val stream: InputStream)
|
||||
class PatchedDexFile
|
||||
// TODO: Add internal modifier.
|
||||
@Deprecated("This constructor will be removed in the future.")
|
||||
constructor(val name: String, val stream: InputStream)
|
||||
|
||||
/**
|
||||
* The resources of a patched apk.
|
||||
*
|
||||
* @param resourcesApk The compiled resources.apk file.
|
||||
* @param otherResources The directory containing other resources files.
|
||||
* @param doNotCompress List of files that should not be compressed.
|
||||
* @param deleteResources List of predicates about resources that should be deleted.
|
||||
*/
|
||||
class PatchedResources internal constructor(
|
||||
val resourcesApk: File?,
|
||||
val otherResources: File?,
|
||||
val doNotCompress: Set<String>,
|
||||
val deleteResources: Set<(String) -> Boolean>,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
package app.revanced.patcher.data
|
||||
|
||||
import app.revanced.patcher.InternalApi
|
||||
import app.revanced.patcher.PatcherConfig
|
||||
import app.revanced.patcher.PatcherContext
|
||||
import app.revanced.patcher.PatcherOptions
|
||||
import app.revanced.patcher.PatcherResult
|
||||
import app.revanced.patcher.patch.Patch
|
||||
import app.revanced.patcher.util.ClassMerger.merge
|
||||
|
@ -21,160 +22,163 @@ import java.io.Flushable
|
|||
import java.util.logging.Logger
|
||||
|
||||
/**
|
||||
* A context for bytecode.
|
||||
* This holds the current state of the bytecode.
|
||||
* A context for the patcher containing the current state of the bytecode.
|
||||
*
|
||||
* @param options The [PatcherOptions] used to create this context.
|
||||
* @param config The [PatcherConfig] used to create this context.
|
||||
*/
|
||||
class BytecodeContext internal constructor(private val options: PatcherOptions) :
|
||||
Context<List<PatcherResult.PatchedDexFile>> {
|
||||
private val logger = Logger.getLogger(BytecodeContext::class.java.name)
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
class BytecodeContext internal constructor(private val config: PatcherConfig) :
|
||||
Context<Set<PatcherResult.PatchedDexFile>> {
|
||||
private val logger = Logger.getLogger(BytecodeContext::class.java.name)
|
||||
|
||||
/**
|
||||
* [Opcodes] of the supplied [PatcherConfig.apkFile].
|
||||
*/
|
||||
internal lateinit var opcodes: Opcodes
|
||||
|
||||
/**
|
||||
* The list of classes.
|
||||
*/
|
||||
val classes by lazy {
|
||||
ProxyClassList(
|
||||
MultiDexIO.readDexFile(
|
||||
true,
|
||||
config.apkFile,
|
||||
BasicDexFileNamer(),
|
||||
null,
|
||||
null,
|
||||
).also { opcodes = it.opcodes }.classes.toMutableSet(),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The [Integrations] of this [PatcherContext].
|
||||
*/
|
||||
internal val integrations = Integrations()
|
||||
|
||||
/**
|
||||
* Find a class by a given class name.
|
||||
*
|
||||
* @param className The name of the class.
|
||||
* @return A proxy for the first class that matches the class name.
|
||||
*/
|
||||
fun findClass(className: String) = findClass { it.type.contains(className) }
|
||||
|
||||
/**
|
||||
* Find a class by a given predicate.
|
||||
*
|
||||
* @param predicate A predicate to match the class.
|
||||
* @return A proxy for the first class that matches the predicate.
|
||||
*/
|
||||
fun findClass(predicate: (ClassDef) -> Boolean) =
|
||||
// if we already proxied the class matching the predicate...
|
||||
classes.proxies.firstOrNull { predicate(it.immutableClass) }
|
||||
?: // else resolve the class to a proxy and return it, if the predicate is matching a class
|
||||
classes.find(predicate)?.let { proxy(it) }
|
||||
|
||||
/**
|
||||
* Proxy a class.
|
||||
* This will allow the class to be modified.
|
||||
*
|
||||
* @param classDef The class to proxy.
|
||||
* @return A proxy for the class.
|
||||
*/
|
||||
fun proxy(classDef: ClassDef) =
|
||||
this.classes.proxies.find { it.immutableClass.type == classDef.type } ?: let {
|
||||
ClassProxy(classDef).also { this.classes.add(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a [MethodWalker] instance for the current [BytecodeContext].
|
||||
*
|
||||
* @param startMethod The method to start at.
|
||||
* @return A [MethodWalker] instance.
|
||||
*/
|
||||
fun toMethodWalker(startMethod: Method) = MethodWalker(this, startMethod)
|
||||
|
||||
/**
|
||||
* Compile bytecode from the [BytecodeContext].
|
||||
*
|
||||
* @return The compiled bytecode.
|
||||
*/
|
||||
@InternalApi
|
||||
override fun get(): Set<PatcherResult.PatchedDexFile> {
|
||||
logger.info("Compiling patched dex files")
|
||||
|
||||
val patchedDexFileResults =
|
||||
config.patchedFiles.resolve("dex").also {
|
||||
it.deleteRecursively() // Make sure the directory is empty.
|
||||
it.mkdirs()
|
||||
}.apply {
|
||||
MultiDexIO.writeDexFile(
|
||||
true,
|
||||
if (config.multithreadingDexFileWriter) -1 else 1,
|
||||
this,
|
||||
BasicDexFileNamer(),
|
||||
object : DexFile {
|
||||
override fun getClasses() = this@BytecodeContext.classes.also(ProxyClassList::replaceClasses)
|
||||
|
||||
override fun getOpcodes() = this@BytecodeContext.opcodes
|
||||
},
|
||||
DexIO.DEFAULT_MAX_DEX_POOL_SIZE,
|
||||
) { _, entryName, _ -> logger.info("Compiled $entryName") }
|
||||
}.listFiles(FileFilter { it.isFile })!!.map {
|
||||
PatcherResult.PatchedDexFile(it.name, it.inputStream())
|
||||
}.toSet()
|
||||
|
||||
System.gc()
|
||||
|
||||
return patchedDexFileResults
|
||||
}
|
||||
|
||||
/**
|
||||
* The integrations of a [PatcherContext].
|
||||
*/
|
||||
internal inner class Integrations : MutableList<File> by mutableListOf(), Flushable {
|
||||
/**
|
||||
* Whether to merge integrations.
|
||||
* Set to true, if the field requiresIntegrations of any supplied [Patch] is true.
|
||||
*/
|
||||
var merge = false
|
||||
|
||||
/**
|
||||
* [Opcodes] of the supplied [PatcherOptions.inputFile].
|
||||
* Merge integrations into the [BytecodeContext] and flush all [Integrations].
|
||||
*/
|
||||
internal lateinit var opcodes: Opcodes
|
||||
override fun flush() {
|
||||
if (!merge) return
|
||||
|
||||
/**
|
||||
* The list of classes.
|
||||
*/
|
||||
val classes by lazy {
|
||||
ProxyClassList(
|
||||
logger.info("Merging integrations")
|
||||
|
||||
val classMap = classes.associateBy { it.type }
|
||||
|
||||
this@Integrations.forEach { integrations ->
|
||||
MultiDexIO.readDexFile(
|
||||
true,
|
||||
options.inputFile,
|
||||
integrations,
|
||||
BasicDexFileNamer(),
|
||||
null,
|
||||
null,
|
||||
).also { opcodes = it.opcodes }.classes.toMutableSet(),
|
||||
)
|
||||
}
|
||||
).classes.forEach classDef@{ classDef ->
|
||||
val existingClass =
|
||||
classMap[classDef.type] ?: run {
|
||||
logger.fine("Adding $classDef")
|
||||
classes.add(classDef)
|
||||
return@classDef
|
||||
}
|
||||
|
||||
/**
|
||||
* The [Integrations] of this [PatcherContext].
|
||||
*/
|
||||
internal val integrations = Integrations()
|
||||
logger.fine("$classDef exists. Adding missing methods and fields.")
|
||||
|
||||
/**
|
||||
* Find a class by a given class name.
|
||||
*
|
||||
* @param className The name of the class.
|
||||
* @return A proxy for the first class that matches the class name.
|
||||
*/
|
||||
fun findClass(className: String) = findClass { it.type.contains(className) }
|
||||
|
||||
/**
|
||||
* Find a class by a given predicate.
|
||||
*
|
||||
* @param predicate A predicate to match the class.
|
||||
* @return A proxy for the first class that matches the predicate.
|
||||
*/
|
||||
fun findClass(predicate: (ClassDef) -> Boolean) =
|
||||
// if we already proxied the class matching the predicate...
|
||||
classes.proxies.firstOrNull { predicate(it.immutableClass) }
|
||||
?: // else resolve the class to a proxy and return it, if the predicate is matching a class
|
||||
classes.find(predicate)?.let { proxy(it) }
|
||||
|
||||
/**
|
||||
* Proxy a class.
|
||||
* This will allow the class to be modified.
|
||||
*
|
||||
* @param classDef The class to proxy.
|
||||
* @return A proxy for the class.
|
||||
*/
|
||||
fun proxy(classDef: ClassDef) =
|
||||
this.classes.proxies.find { it.immutableClass.type == classDef.type } ?: let {
|
||||
ClassProxy(classDef).also { this.classes.add(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a [MethodWalker] instance for the current [BytecodeContext].
|
||||
*
|
||||
* @param startMethod The method to start at.
|
||||
* @return A [MethodWalker] instance.
|
||||
*/
|
||||
fun toMethodWalker(startMethod: Method) = MethodWalker(this, startMethod)
|
||||
|
||||
/**
|
||||
* Compile bytecode from the [BytecodeContext].
|
||||
*
|
||||
* @return The compiled bytecode.
|
||||
*/
|
||||
override fun get(): List<PatcherResult.PatchedDexFile> {
|
||||
logger.info("Compiling patched dex files")
|
||||
|
||||
val patchedDexFileResults =
|
||||
options.resourceCachePath.resolve("dex").also {
|
||||
it.deleteRecursively() // Make sure the directory is empty.
|
||||
it.mkdirs()
|
||||
}.apply {
|
||||
MultiDexIO.writeDexFile(
|
||||
true,
|
||||
if (options.multithreadingDexFileWriter) -1 else 1,
|
||||
this,
|
||||
BasicDexFileNamer(),
|
||||
object : DexFile {
|
||||
override fun getClasses() = this@BytecodeContext.classes.also(ProxyClassList::replaceClasses)
|
||||
|
||||
override fun getOpcodes() = this@BytecodeContext.opcodes
|
||||
},
|
||||
DexIO.DEFAULT_MAX_DEX_POOL_SIZE,
|
||||
) { _, entryName, _ -> logger.info("Compiled $entryName") }
|
||||
}.listFiles(FileFilter { it.isFile })!!.map { PatcherResult.PatchedDexFile(it.name, it.inputStream()) }
|
||||
|
||||
System.gc()
|
||||
|
||||
return patchedDexFileResults
|
||||
}
|
||||
|
||||
/**
|
||||
* The integrations of a [PatcherContext].
|
||||
*/
|
||||
internal inner class Integrations : MutableList<File> by mutableListOf(), Flushable {
|
||||
/**
|
||||
* Whether to merge integrations.
|
||||
* Set to true, if the field requiresIntegrations of any supplied [Patch] is true.
|
||||
*/
|
||||
var merge = false
|
||||
|
||||
/**
|
||||
* Merge integrations into the [BytecodeContext] and flush all [Integrations].
|
||||
*/
|
||||
override fun flush() {
|
||||
if (!merge) return
|
||||
|
||||
logger.info("Merging integrations")
|
||||
|
||||
val classMap = classes.associateBy { it.type }
|
||||
|
||||
this@Integrations.forEach { integrations ->
|
||||
MultiDexIO.readDexFile(
|
||||
true,
|
||||
integrations,
|
||||
BasicDexFileNamer(),
|
||||
null,
|
||||
null,
|
||||
).classes.forEach classDef@{ classDef ->
|
||||
val existingClass =
|
||||
classMap[classDef.type] ?: run {
|
||||
logger.fine("Adding $classDef")
|
||||
classes.add(classDef)
|
||||
return@classDef
|
||||
}
|
||||
|
||||
logger.fine("$classDef exists. Adding missing methods and fields.")
|
||||
|
||||
existingClass.merge(classDef, this@BytecodeContext).let { mergedClass ->
|
||||
// If the class was merged, replace the original class with the merged class.
|
||||
if (mergedClass === existingClass) return@let
|
||||
classes.apply {
|
||||
remove(existingClass)
|
||||
add(mergedClass)
|
||||
}
|
||||
existingClass.merge(classDef, this@BytecodeContext).let { mergedClass ->
|
||||
// If the class was merged, replace the original class with the merged class.
|
||||
if (mergedClass === existingClass) return@let
|
||||
classes.apply {
|
||||
remove(existingClass)
|
||||
add(mergedClass)
|
||||
}
|
||||
}
|
||||
}
|
||||
clear()
|
||||
}
|
||||
clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
package app.revanced.patcher.data
|
||||
|
||||
import app.revanced.patcher.PatcherContext
|
||||
import app.revanced.patcher.PatcherOptions
|
||||
import app.revanced.patcher.InternalApi
|
||||
import app.revanced.patcher.PackageMetadata
|
||||
import app.revanced.patcher.PatcherConfig
|
||||
import app.revanced.patcher.PatcherResult
|
||||
import app.revanced.patcher.util.Document
|
||||
import app.revanced.patcher.util.DomFileEditor
|
||||
import brut.androlib.AaptInvoker
|
||||
import brut.androlib.ApkDecoder
|
||||
|
@ -19,72 +22,80 @@ import java.nio.file.Files
|
|||
import java.util.logging.Logger
|
||||
|
||||
/**
|
||||
* A context for resources.
|
||||
* This holds the current state of the resources.
|
||||
* A context for the patcher containing the current state of the resources.
|
||||
*
|
||||
* @param context The [PatcherContext] to create the context for.
|
||||
* @param packageMetadata The [PackageMetadata] of the apk file.
|
||||
* @param config The [PatcherConfig] used to create this context.
|
||||
*/
|
||||
class ResourceContext internal constructor(
|
||||
private val context: PatcherContext,
|
||||
private val options: PatcherOptions,
|
||||
) : Context<File?>, Iterable<File> {
|
||||
private val packageMetadata: PackageMetadata,
|
||||
private val config: PatcherConfig,
|
||||
) : Context<PatcherResult.PatchedResources?>, Iterable<File> {
|
||||
private val logger = Logger.getLogger(ResourceContext::class.java.name)
|
||||
|
||||
/**
|
||||
* Read and write documents in the [PatcherConfig.apkFiles].
|
||||
*/
|
||||
val document = DocumentOperatable()
|
||||
|
||||
@Deprecated("Use document instead.")
|
||||
val xmlEditor = XmlFileHolder()
|
||||
|
||||
/**
|
||||
* Decode resources for the patcher.
|
||||
*
|
||||
* @param mode The [ResourceDecodingMode] to use when decoding.
|
||||
* Predicate to delete resources from [PatcherConfig.apkFiles].
|
||||
*/
|
||||
internal fun decodeResources(mode: ResourceDecodingMode) =
|
||||
with(context.packageMetadata.apkInfo) {
|
||||
private val deleteResources = mutableSetOf<(String) -> Boolean>()
|
||||
|
||||
/**
|
||||
* Decode resources of [PatcherConfig.apkFile].
|
||||
*
|
||||
* @param mode The [ResourceMode] to use.
|
||||
*/
|
||||
internal fun decodeResources(mode: ResourceMode) =
|
||||
with(packageMetadata.apkInfo) {
|
||||
config.initializeTemporaryFilesDirectories()
|
||||
|
||||
// Needed to decode resources.
|
||||
val resourcesDecoder = ResourcesDecoder(options.resourceConfig, this)
|
||||
val resourcesDecoder = ResourcesDecoder(config.resourceConfig, this)
|
||||
|
||||
when (mode) {
|
||||
ResourceDecodingMode.FULL -> {
|
||||
val outDir = options.recreateResourceCacheDirectory()
|
||||
if (mode == ResourceMode.FULL) {
|
||||
logger.info("Decoding resources")
|
||||
|
||||
logger.info("Decoding resources")
|
||||
resourcesDecoder.decodeResources(config.apkFiles)
|
||||
resourcesDecoder.decodeManifest(config.apkFiles)
|
||||
|
||||
resourcesDecoder.decodeResources(outDir)
|
||||
resourcesDecoder.decodeManifest(outDir)
|
||||
// Needed to record uncompressed files.
|
||||
val apkDecoder = ApkDecoder(config.resourceConfig, this)
|
||||
apkDecoder.recordUncompressedFiles(resourcesDecoder.resFileMapping)
|
||||
|
||||
// Needed to record uncompressed files.
|
||||
val apkDecoder = ApkDecoder(options.resourceConfig, this)
|
||||
apkDecoder.recordUncompressedFiles(resourcesDecoder.resFileMapping)
|
||||
usesFramework =
|
||||
UsesFramework().apply {
|
||||
ids = resourcesDecoder.resTable.listFramePackages().map { it.id }
|
||||
}
|
||||
} else {
|
||||
logger.info("Decoding app manifest")
|
||||
|
||||
usesFramework =
|
||||
UsesFramework().apply {
|
||||
ids = resourcesDecoder.resTable.listFramePackages().map { it.id }
|
||||
// Decode manually instead of using resourceDecoder.decodeManifest
|
||||
// because it does not support decoding to an OutputStream.
|
||||
XmlPullStreamDecoder(
|
||||
AndroidManifestResourceParser(resourcesDecoder.resTable),
|
||||
resourcesDecoder.resXmlSerializer,
|
||||
).decodeManifest(
|
||||
apkFile.directory.getFileInput("AndroidManifest.xml"),
|
||||
// Older Android versions do not support OutputStream.nullOutputStream()
|
||||
object : OutputStream() {
|
||||
override fun write(b: Int) { // Do nothing.
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
ResourceDecodingMode.MANIFEST_ONLY -> {
|
||||
logger.info("Decoding app manifest")
|
||||
|
||||
// Decode manually instead of using resourceDecoder.decodeManifest
|
||||
// because it does not support decoding to an OutputStream.
|
||||
XmlPullStreamDecoder(
|
||||
AndroidManifestResourceParser(resourcesDecoder.resTable),
|
||||
resourcesDecoder.resXmlSerializer,
|
||||
).decodeManifest(
|
||||
apkFile.directory.getFileInput("AndroidManifest.xml"),
|
||||
// Older Android versions do not support OutputStream.nullOutputStream()
|
||||
object : OutputStream() {
|
||||
override fun write(b: Int) { // do nothing
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// Get the package name and version from the manifest using the XmlPullStreamDecoder.
|
||||
// XmlPullStreamDecoder.decodeManifest() sets metadata.apkInfo.
|
||||
context.packageMetadata.let { metadata ->
|
||||
metadata.packageName = resourcesDecoder.resTable.packageRenamed
|
||||
versionInfo.let {
|
||||
metadata.packageVersion = it.versionName ?: it.versionCode
|
||||
}
|
||||
// Get the package name and version from the manifest using the XmlPullStreamDecoder.
|
||||
// XmlPullStreamDecoder.decodeManifest() sets metadata.apkInfo.
|
||||
packageMetadata.let { metadata ->
|
||||
metadata.packageName = resourcesDecoder.resTable.packageRenamed
|
||||
versionInfo.let {
|
||||
metadata.packageVersion = it.versionName ?: it.versionCode
|
||||
}
|
||||
|
||||
/*
|
||||
The ResTable if flagged as sparse if the main package is not loaded, which is the case here,
|
||||
|
@ -94,74 +105,147 @@ class ResourceContext internal constructor(
|
|||
|
||||
Set this to false again to prevent the ResTable from being flagged as sparse falsely.
|
||||
*/
|
||||
metadata.apkInfo.sparseResources = false
|
||||
}
|
||||
metadata.apkInfo.sparseResources = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
operator fun get(path: String) = options.resourceCachePath.resolve(path)
|
||||
|
||||
override fun iterator() = options.resourceCachePath.walkTopDown().iterator()
|
||||
|
||||
/**
|
||||
* Compile resources from the [ResourceContext].
|
||||
* Compile resources in [PatcherConfig.apkFiles].
|
||||
*
|
||||
* @return The compiled resources.
|
||||
* @return The [PatcherResult.PatchedResources].
|
||||
*/
|
||||
override fun get(): File? {
|
||||
var resourceFile: File? = null
|
||||
@InternalApi
|
||||
override fun get(): PatcherResult.PatchedResources? {
|
||||
if (config.resourceMode == ResourceMode.NONE) return null
|
||||
|
||||
if (options.resourceDecodingMode == ResourceDecodingMode.FULL) {
|
||||
logger.info("Compiling modified resources")
|
||||
logger.info("Compiling modified resources")
|
||||
|
||||
val cacheDirectory = ExtFile(options.resourceCachePath)
|
||||
val aaptFile =
|
||||
cacheDirectory.resolve("aapt_temp_file").also {
|
||||
Files.deleteIfExists(it.toPath())
|
||||
}.also { resourceFile = it }
|
||||
val resources = config.patchedFiles.resolve("resources").also { it.mkdirs() }
|
||||
|
||||
try {
|
||||
AaptInvoker(
|
||||
options.resourceConfig,
|
||||
context.packageMetadata.apkInfo,
|
||||
).invokeAapt(
|
||||
aaptFile,
|
||||
cacheDirectory.resolve("AndroidManifest.xml").also {
|
||||
ResXmlPatcher.fixingPublicAttrsInProviderAttributes(it)
|
||||
},
|
||||
cacheDirectory.resolve("res"),
|
||||
null,
|
||||
null,
|
||||
context.packageMetadata.apkInfo.usesFramework.let { usesFramework ->
|
||||
usesFramework.ids.map { id ->
|
||||
Framework(options.resourceConfig).getFrameworkApk(id, usesFramework.tag)
|
||||
}.toTypedArray()
|
||||
},
|
||||
)
|
||||
} finally {
|
||||
cacheDirectory.close()
|
||||
val resourcesApkFile =
|
||||
if (config.resourceMode == ResourceMode.FULL) {
|
||||
resources.resolve("resources.apk").apply {
|
||||
// Compile the resources.apk file.
|
||||
AaptInvoker(
|
||||
config.resourceConfig,
|
||||
packageMetadata.apkInfo,
|
||||
).invokeAapt(
|
||||
resources.resolve("resources.apk"),
|
||||
config.apkFiles.resolve("AndroidManifest.xml").also {
|
||||
ResXmlPatcher.fixingPublicAttrsInProviderAttributes(it)
|
||||
},
|
||||
config.apkFiles.resolve("res"),
|
||||
null,
|
||||
null,
|
||||
packageMetadata.apkInfo.usesFramework.let { usesFramework ->
|
||||
usesFramework.ids.map { id ->
|
||||
Framework(config.resourceConfig).getFrameworkApk(id, usesFramework.tag)
|
||||
}.toTypedArray()
|
||||
},
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
return resourceFile
|
||||
val otherFiles =
|
||||
config.apkFiles.listFiles()!!.filter {
|
||||
// Excluded because present in resources.other.
|
||||
// TODO: We are reusing config.apkFiles as a temporarily directory for extracting resources.
|
||||
// This is not ideal as it could conflict with files such as the ones that we filter here.
|
||||
// The problem is that ResourceContext#get returns a File relative to config.apkFiles,
|
||||
// and we need to extract files to that directory.
|
||||
// A solution would be to use config.apkFiles as the working directory for the patching process.
|
||||
// Once all patches have been executed, we can move the decoded resources to a new directory.
|
||||
// The filters wouldn't be needed anymore.
|
||||
// For now, we assume that the files we filter here are not needed for the patching process.
|
||||
it.name != "AndroidManifest.xml" &&
|
||||
it.name != "res" &&
|
||||
// Generated by Androlib.
|
||||
it.name != "build"
|
||||
}
|
||||
|
||||
val otherResourceFiles =
|
||||
if (otherFiles.isNotEmpty()) {
|
||||
// Move the other resources files.
|
||||
resources.resolve("other").also { it.mkdirs() }.apply {
|
||||
otherFiles.forEach { file ->
|
||||
Files.move(file.toPath(), resolve(file.name).toPath())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
return PatcherResult.PatchedResources(
|
||||
resourcesApkFile,
|
||||
otherResourceFiles,
|
||||
packageMetadata.apkInfo.doNotCompress?.toSet() ?: emptySet(),
|
||||
deleteResources,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of decoding the resources.
|
||||
* Get a file from [PatcherConfig.apkFiles].
|
||||
*
|
||||
* @param path The path of the file.
|
||||
* @param copy Whether to copy the file from [PatcherConfig.apkFile] if it does not exist yet in [PatcherConfig.apkFiles].
|
||||
*/
|
||||
internal enum class ResourceDecodingMode {
|
||||
operator fun get(
|
||||
path: String,
|
||||
copy: Boolean = true,
|
||||
) = config.apkFiles.resolve(path).apply {
|
||||
if (copy && !exists()) {
|
||||
with(ExtFile(config.apkFile).directory) {
|
||||
if (containsFile(path) || containsDir(path)) {
|
||||
copyToDir(config.apkFiles, path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stage a file to be deleted from [PatcherConfig.apkFile].
|
||||
*
|
||||
* @param shouldDelete The predicate to stage the file for deletion given its name.
|
||||
*/
|
||||
fun stageDelete(shouldDelete: (String) -> Boolean) = deleteResources.add(shouldDelete)
|
||||
|
||||
@Deprecated("Use get(String, Boolean) instead.", ReplaceWith("get(path, false)"))
|
||||
operator fun get(path: String) = get(path, false)
|
||||
|
||||
@Deprecated("Use get(String, Boolean) instead.")
|
||||
override fun iterator(): Iterator<File> = config.apkFiles.listFiles()!!.iterator()
|
||||
|
||||
/**
|
||||
* How to handle resources decoding and compiling.
|
||||
*/
|
||||
internal enum class ResourceMode {
|
||||
/**
|
||||
* Decode all resources.
|
||||
* Decode and compile all resources.
|
||||
*/
|
||||
FULL,
|
||||
|
||||
/**
|
||||
* Decode the manifest file only.
|
||||
* Only extract resources from the APK.
|
||||
* The AndroidManifest.xml and resources inside /res are not decoded or compiled.
|
||||
*/
|
||||
MANIFEST_ONLY,
|
||||
RAW_ONLY,
|
||||
|
||||
/**
|
||||
* Do not decode or compile any resources.
|
||||
*/
|
||||
NONE,
|
||||
}
|
||||
|
||||
inner class DocumentOperatable {
|
||||
operator fun get(inputStream: InputStream) = Document(inputStream)
|
||||
|
||||
operator fun get(path: String) = Document(this@ResourceContext[path])
|
||||
}
|
||||
|
||||
@Deprecated("Use DocumentOperatable instead.")
|
||||
inner class XmlFileHolder {
|
||||
operator fun get(inputStream: InputStream) = DomFileEditor(inputStream)
|
||||
|
||||
|
|
|
@ -7,10 +7,10 @@ import app.revanced.patcher.fingerprint.MethodFingerprint
|
|||
import java.io.Closeable
|
||||
|
||||
/**
|
||||
* A ReVanced [Patch] that accesses a [BytecodeContext].
|
||||
* A [Patch] that accesses a [BytecodeContext].
|
||||
*
|
||||
* If an implementation of [Patch] also implements [Closeable]
|
||||
* it will be closed in reverse execution order of patches executed by ReVanced [Patcher].
|
||||
* it will be closed in reverse execution order of patches executed by [Patcher].
|
||||
*/
|
||||
@Suppress("unused")
|
||||
abstract class BytecodePatch : Patch<BytecodeContext> {
|
||||
|
|
|
@ -10,10 +10,10 @@ import app.revanced.patcher.patch.options.PatchOptions
|
|||
import java.io.Closeable
|
||||
|
||||
/**
|
||||
* A ReVanced patch.
|
||||
* A patch.
|
||||
*
|
||||
* If an implementation of [Patch] also implements [Closeable]
|
||||
* it will be closed in reverse execution order of patches executed by ReVanced [Patcher].
|
||||
* it will be closed in reverse execution order of patches executed by [Patcher].
|
||||
*
|
||||
* @param T The [Context] type this patch will work on.
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
package app.revanced.patcher.patch
|
||||
|
||||
import app.revanced.patcher.PatchClass
|
||||
import app.revanced.patcher.Patcher
|
||||
import app.revanced.patcher.data.ResourceContext
|
||||
import java.io.Closeable
|
||||
|
||||
/**
|
||||
* A [Patch] that accesses a [ResourceContext].
|
||||
*
|
||||
* If an implementation of [Patch] also implements [Closeable]
|
||||
* it will be closed in reverse execution order of patches executed by [Patcher].
|
||||
*
|
||||
* This type of patch that does not have access to decoded resources.
|
||||
* Instead, you can read and write arbitrary files in an APK file.
|
||||
*
|
||||
* If you want to access decoded resources, use [ResourcePatch] instead.
|
||||
*/
|
||||
abstract class RawResourcePatch : Patch<ResourceContext> {
|
||||
/**
|
||||
* Create a new [RawResourcePatch].
|
||||
*/
|
||||
constructor()
|
||||
|
||||
/**
|
||||
* Create a new [RawResourcePatch].
|
||||
*
|
||||
* @param name The name of the patch.
|
||||
* @param description The description of the patch.
|
||||
* @param compatiblePackages The packages the patch is compatible with.
|
||||
* @param dependencies Other patches this patch depends on.
|
||||
* @param use Weather or not the patch should be used.
|
||||
* @param requiresIntegrations Weather or not the patch requires integrations.
|
||||
*/
|
||||
constructor(
|
||||
name: String? = null,
|
||||
description: String? = null,
|
||||
compatiblePackages: Set<CompatiblePackage>? = null,
|
||||
dependencies: Set<PatchClass>? = null,
|
||||
use: Boolean = true,
|
||||
requiresIntegrations: Boolean = false,
|
||||
) : super(name, description, compatiblePackages, dependencies, use, requiresIntegrations)
|
||||
}
|
|
@ -6,10 +6,15 @@ import app.revanced.patcher.data.ResourceContext
|
|||
import java.io.Closeable
|
||||
|
||||
/**
|
||||
* A ReVanced [Patch] that accesses a [ResourceContext].
|
||||
* A [Patch] that accesses a [ResourceContext].
|
||||
*
|
||||
* If an implementation of [Patch] also implements [Closeable]
|
||||
* it will be closed in reverse execution order of patches executed by ReVanced [Patcher].
|
||||
* it will be closed in reverse execution order of patches executed by [Patcher].
|
||||
*
|
||||
* This type of patch has access to decoded resources.
|
||||
* Additionally, you can read and write arbitrary files in an APK file.
|
||||
*
|
||||
* If you do not need access to decoded resources, use [RawResourcePatch] instead.
|
||||
*/
|
||||
abstract class ResourcePatch : Patch<ResourceContext> {
|
||||
/**
|
||||
|
|
48
src/main/kotlin/app/revanced/patcher/util/Document.kt
Normal file
48
src/main/kotlin/app/revanced/patcher/util/Document.kt
Normal file
|
@ -0,0 +1,48 @@
|
|||
package app.revanced.patcher.util
|
||||
|
||||
import org.w3c.dom.Document
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
import javax.xml.transform.TransformerFactory
|
||||
import javax.xml.transform.dom.DOMSource
|
||||
import javax.xml.transform.stream.StreamResult
|
||||
|
||||
class Document internal constructor(
|
||||
inputStream: InputStream,
|
||||
) : Document by DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputStream), Closeable {
|
||||
private var file: File? = null
|
||||
|
||||
init {
|
||||
normalize()
|
||||
}
|
||||
|
||||
internal constructor(file: File) : this(file.inputStream()) {
|
||||
this.file = file
|
||||
readerCount.merge(file, 1, Int::plus)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
file?.let {
|
||||
if (readerCount[it]!! > 1) {
|
||||
throw IllegalStateException(
|
||||
"Two or more instances are currently reading $it." +
|
||||
"To be able to close this instance, no other instances may be reading $it at the same time.",
|
||||
)
|
||||
} else {
|
||||
readerCount.remove(it)
|
||||
}
|
||||
|
||||
it.outputStream().use { stream ->
|
||||
TransformerFactory.newInstance()
|
||||
.newTransformer()
|
||||
.transform(DOMSource(this), StreamResult(stream))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private val readerCount = mutableMapOf<File, Int>()
|
||||
}
|
||||
}
|
|
@ -4,85 +4,22 @@ import org.w3c.dom.Document
|
|||
import java.io.Closeable
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
import javax.xml.transform.TransformerFactory
|
||||
import javax.xml.transform.dom.DOMSource
|
||||
import javax.xml.transform.stream.StreamResult
|
||||
|
||||
/**
|
||||
* Wrapper for a file that can be edited as a dom document.
|
||||
*
|
||||
* This constructor does not check for locks to the file when writing.
|
||||
* Use the secondary constructor.
|
||||
*
|
||||
* @param inputStream the input stream to read the xml file from.
|
||||
* @param outputStream the output stream to write the xml file to. If null, the file will be read only.
|
||||
*
|
||||
*/
|
||||
class DomFileEditor internal constructor(
|
||||
private val inputStream: InputStream,
|
||||
private val outputStream: Lazy<OutputStream>? = null,
|
||||
) : Closeable {
|
||||
// path to the xml file to unlock the resource when closing the editor
|
||||
private var filePath: String? = null
|
||||
private var closed: Boolean = false
|
||||
|
||||
/**
|
||||
* The document of the xml file
|
||||
*/
|
||||
val file: Document =
|
||||
DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputStream)
|
||||
.also(Document::normalize)
|
||||
|
||||
// lazily open an output stream
|
||||
// this is required because when constructing a DomFileEditor the output stream is created along with the input stream, which is not allowed
|
||||
// the workaround is to lazily create the output stream. This way it would be used after the input stream is closed, which happens in the constructor
|
||||
constructor(file: File) : this(file.inputStream(), lazy { file.outputStream() }) {
|
||||
// increase the lock
|
||||
locks.merge(file.path, 1, Integer::sum)
|
||||
filePath = file.path
|
||||
@Deprecated("Use Document instead.")
|
||||
class DomFileEditor : Closeable {
|
||||
val file: Document
|
||||
internal constructor(
|
||||
inputStream: InputStream,
|
||||
) {
|
||||
file = Document(inputStream)
|
||||
}
|
||||
|
||||
constructor(file: File) {
|
||||
this.file = Document(file)
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the editor. Write backs and decreases the lock count.
|
||||
*
|
||||
* Will not write back to the file if the file is still locked.
|
||||
*/
|
||||
override fun close() {
|
||||
if (closed) return
|
||||
|
||||
inputStream.close()
|
||||
|
||||
// if the output stream is not null, do not close it
|
||||
outputStream?.let {
|
||||
// prevent writing to same file, if it is being locked
|
||||
// isLocked will be false if the editor was created through a stream
|
||||
val isLocked =
|
||||
filePath?.let { path ->
|
||||
val isLocked = locks[path]!! > 1
|
||||
// decrease the lock count if the editor was opened for a file
|
||||
locks.merge(path, -1, Integer::sum)
|
||||
isLocked
|
||||
} ?: false
|
||||
|
||||
// if unlocked, write back to the file
|
||||
if (!isLocked) {
|
||||
it.value.use { stream ->
|
||||
val result = StreamResult(stream)
|
||||
TransformerFactory.newInstance().newTransformer().transform(DOMSource(file), result)
|
||||
}
|
||||
|
||||
it.value.close()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
closed = true
|
||||
}
|
||||
|
||||
private companion object {
|
||||
// map of concurrent open files
|
||||
val locks = mutableMapOf<String, Int>()
|
||||
file as app.revanced.patcher.util.Document
|
||||
file.close()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package app.revanced.patcher.patch
|
||||
|
||||
import app.revanced.patcher.data.ResourceContext
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import kotlin.test.Test
|
||||
import app.revanced.patcher.patch.annotation.Patch as PatchAnnotation
|
||||
|
||||
|
@ -9,7 +8,7 @@ object PatchInitializationTest {
|
|||
@Test
|
||||
fun `initialize using constructor`() {
|
||||
val patch =
|
||||
object : ResourcePatch(name = "Resource patch test") {
|
||||
object : RawResourcePatch(name = "Resource patch test") {
|
||||
override fun execute(context: ResourceContext) {}
|
||||
}
|
||||
|
||||
|
@ -20,7 +19,7 @@ object PatchInitializationTest {
|
|||
fun `initialize using annotation`() {
|
||||
val patch =
|
||||
@PatchAnnotation("Resource patch test")
|
||||
object : ResourcePatch() {
|
||||
object : RawResourcePatch() {
|
||||
override fun execute(context: ResourceContext) {}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,17 +6,9 @@ import org.w3c.dom.Element
|
|||
|
||||
class ExampleResourcePatch : ResourcePatch() {
|
||||
override fun execute(context: ResourceContext) {
|
||||
context.xmlEditor["AndroidManifest.xml"].use { editor ->
|
||||
val element =
|
||||
editor // regular DomFileEditor
|
||||
.file
|
||||
.getElementsByTagName("application")
|
||||
.item(0) as Element
|
||||
element
|
||||
.setAttribute(
|
||||
"exampleAttribute",
|
||||
"exampleValue",
|
||||
)
|
||||
context.document["AndroidManifest.xml"].use { document ->
|
||||
val element = document.getElementsByTagName("application").item(0) as Element
|
||||
element.setAttribute("exampleAttribute", "exampleValue")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue