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:
oSumAtrIX 2024-02-11 21:09:38 +01:00
parent 64dd1526cd
commit f1d7217495
No known key found for this signature in database
GPG key ID: A9B3094ACDB604B4
23 changed files with 828 additions and 447 deletions

3
.editorconfig Normal file
View file

@ -0,0 +1,3 @@
[*.{kt,kts}]
ktlint_code_style = intellij_idea
ktlint_standard_no-wildcard-imports = disabled

View file

@ -1,5 +1,9 @@
public abstract interface class app/revanced/patcher/IntegrationsConsumer { public abstract interface class app/revanced/patcher/IntegrationsConsumer {
public abstract fun acceptIntegrations (Ljava/util/List;)V 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 { 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 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 <init> (Lapp/revanced/patcher/PatcherOptions;)V
public fun acceptIntegrations (Ljava/util/List;)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/List;)V
public fun acceptPatches (Ljava/util/Set;)V public fun acceptPatches (Ljava/util/Set;)V
public synthetic fun apply (Ljava/lang/Object;)Ljava/lang/Object; 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 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 class app/revanced/patcher/PatcherContext {
public final fun getPackageMetadata ()Lapp/revanced/patcher/PackageMetadata; 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 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 fun equals (Ljava/lang/Object;)Z
public final fun getDexFiles ()Ljava/util/List; public final fun getDexFiles ()Ljava/util/List;
public final fun getDexFiles ()Ljava/util/Set;
public final fun getDoNotCompress ()Ljava/util/List; public final fun getDoNotCompress ()Ljava/util/List;
public final fun getResourceFile ()Ljava/io/File; public final fun getResourceFile ()Ljava/io/File;
public final fun getResources ()Lapp/revanced/patcher/PatcherResult$PatchedResources;
public fun hashCode ()I public fun hashCode ()I
public fun toString ()Ljava/lang/String; 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 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 interface class app/revanced/patcher/PatchesConsumer {
public abstract fun acceptPatches (Ljava/util/List;)V public abstract fun acceptPatches (Ljava/util/List;)V
public abstract fun acceptPatches (Ljava/util/Set;)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 (Ljava/lang/String;)Lapp/revanced/patcher/util/proxy/ClassProxy;
public final fun findClass (Lkotlin/jvm/functions/Function1;)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 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 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 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; 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 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 synthetic fun get ()Ljava/lang/Object;
public final fun get (Ljava/lang/String;)Ljava/io/File; 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 final fun getXmlEditor ()Lapp/revanced/patcher/data/ResourceContext$XmlFileHolder;
public fun iterator ()Ljava/util/Iterator; 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 { 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 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 abstract class app/revanced/patcher/patch/ResourcePatch : app/revanced/patcher/patch/Patch {
public fun <init> ()V public fun <init> ()V
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;ZZ)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 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 final class app/revanced/patcher/util/DomFileEditor : java/io/Closeable {
public fun <init> (Ljava/io/File;)V public fun <init> (Ljava/io/File;)V
public fun close ()V public fun close ()V

View file

@ -1,5 +1,5 @@
plugins { plugins {
kotlin("jvm") version "1.9.10" alias(libs.plugins.kotlin)
alias(libs.plugins.binary.compatibility.validator) alias(libs.plugins.binary.compatibility.validator)
`maven-publish` `maven-publish`
signing signing
@ -36,7 +36,11 @@ dependencies {
implementation(libs.apktool.lib) implementation(libs.apktool.lib)
implementation(libs.kotlin.reflect) 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) testImplementation(libs.kotlin.test)
} }

View file

@ -1,4 +1,3 @@
org.gradle.parallel = true org.gradle.parallel = true
org.gradle.caching = true org.gradle.caching = true
kotlin.code.style = official
version = 19.2.1-dev.1 version = 19.2.1-dev.1

View file

@ -1,19 +1,18 @@
[versions] [versions]
android = "4.1.1.4" android = "4.1.1.4"
kotlin-reflect = "1.9.10"
apktool-lib = "2.9.1" apktool-lib = "2.9.1"
kotlin-test = "1.9.20" kotlin = "1.9.22"
kotlinx-coroutines-core = "1.7.3" kotlinx-coroutines-core = "1.7.3"
multidexlib2 = "3.0.3.r3" multidexlib2 = "3.0.3.r3"
smali = "3.0.3" smali = "3.0.4"
xpp3 = "1.1.4c"
binary-compatibility-validator = "0.13.2" binary-compatibility-validator = "0.13.2"
xpp3 = "1.1.4c"
[libraries] [libraries]
android = { module = "com.google.android:android", version.ref = "android" } 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" } 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" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines-core" }
multidexlib2 = { module = "app.revanced:multidexlib2", version.ref = "multidexlib2" } multidexlib2 = { module = "app.revanced:multidexlib2", version.ref = "multidexlib2" }
smali = { module = "com.android.tools.smali:smali", version.ref = "smali" } smali = { module = "com.android.tools.smali:smali", version.ref = "smali" }
@ -21,3 +20,4 @@ xpp3 = { module = "xpp3:xpp3", version.ref = "xpp3" }
[plugins] [plugins]
binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" } binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" }
kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }

View file

@ -4,5 +4,8 @@ import java.io.File
@FunctionalInterface @FunctionalInterface
interface IntegrationsConsumer { interface IntegrationsConsumer {
fun acceptIntegrations(integrations: Set<File>)
@Deprecated("Use acceptIntegrations(Set<File>) instead.")
fun acceptIntegrations(integrations: List<File>) fun acceptIntegrations(integrations: List<File>)
} }

View 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

View file

@ -4,6 +4,8 @@ import brut.androlib.apk.ApkInfo
/** /**
* Metadata about a package. * Metadata about a package.
*
* @param apkInfo The [ApkInfo] of the apk file.
*/ */
class PackageMetadata internal constructor(internal val apkInfo: ApkInfo) { class PackageMetadata internal constructor(internal val apkInfo: ApkInfo) {
lateinit var packageName: String lateinit var packageName: String

View file

@ -12,34 +12,38 @@ import java.util.function.Supplier
import java.util.logging.Logger 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( class Patcher(
private val options: PatcherOptions, private val config: PatcherConfig,
) : PatchExecutorFunction, PatchesConsumer, IntegrationsConsumer, Supplier<PatcherResult>, Closeable { ) : PatchExecutorFunction, PatchesConsumer, IntegrationsConsumer, Supplier<PatcherResult>, Closeable {
private val logger = Logger.getLogger(Patcher::class.java.name) private val logger = Logger.getLogger(Patcher::class.java.name)
/** /**
* The context of ReVanced [Patcher]. * A context for the patcher containing the current state of the patcher.
* This holds 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 { 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]. * Add [Patch]es to ReVanced [Patcher].
* *
@ -61,29 +65,15 @@ class Patcher(
} }
// Add all patches and their dependencies to the context. // Add all patches and their dependencies to the context.
for (patch in patches) context.executablePatches.putIfAbsent(patch::class, patch) ?: run { patches.forEach { patch ->
context.allPatches[patch::class] = patch context.executablePatches.putIfAbsent(patch::class, patch) ?: run {
context.allPatches[patch::class] = patch
patch.dependencies?.forEach { it.putDependenciesRecursively() } 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()
} }
} }
*/
// TODO: Detect circular dependencies.
/** /**
* Returns true if at least one patch or its dependencies matches the given predicate. * Returns true if at least one patch or its dependencies matches the given predicate.
@ -96,12 +86,15 @@ class Patcher(
} ?: false } ?: false
context.allPatches.values.let { patches -> context.allPatches.values.let { patches ->
// Determine, if resource patching is required. // Determine the resource mode.
for (patch in patches)
if (patch.anyRecursively { patch is ResourcePatch }) { config.resourceMode = if (patches.any { patch -> patch.anyRecursively { it is ResourcePatch } }) {
options.resourceDecodingMode = ResourceContext.ResourceDecodingMode.FULL ResourceContext.ResourceMode.FULL
break } else if (patches.any { patch -> patch.anyRecursively { it is RawResourcePatch } }) {
} ResourceContext.ResourceMode.RAW_ONLY
} else {
ResourceContext.ResourceMode.NONE
}
// Determine, if merging integrations is required. // Determine, if merging integrations is required.
for (patch in patches) 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. * @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) 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]. * Execute [Patch]es that were added to ReVanced [Patcher].
* *
@ -173,6 +172,9 @@ class Patcher(
patch.fingerprints.resolveUsingLookupMap(context.bytecodeContext) patch.fingerprints.resolveUsingLookupMap(context.bytecodeContext)
patch.execute(context.bytecodeContext) patch.execute(context.bytecodeContext)
} }
is RawResourcePatch -> {
patch.execute(context.resourceContext)
}
is ResourcePatch -> { is ResourcePatch -> {
patch.execute(context.resourceContext) patch.execute(context.resourceContext)
} }
@ -191,8 +193,8 @@ class Patcher(
LookupMap.initializeLookupMaps(context.bytecodeContext) LookupMap.initializeLookupMaps(context.bytecodeContext)
// Prevent from decoding the app manifest twice if it is not needed. // Prevent from decoding the app manifest twice if it is not needed.
if (options.resourceDecodingMode == ResourceContext.ResourceDecodingMode.FULL) { if (config.resourceMode != ResourceContext.ResourceMode.NONE) {
context.resourceContext.decodeResources(ResourceContext.ResourceDecodingMode.FULL) context.resourceContext.decodeResources(config.resourceMode)
} }
logger.info("Executing patches") logger.info("Executing patches")
@ -259,10 +261,10 @@ class Patcher(
* *
* @return The [PatcherResult] containing the patched input files. * @return The [PatcherResult] containing the patched input files.
*/ */
@OptIn(InternalApi::class)
override fun get() = override fun get() =
PatcherResult( PatcherResult(
context.bytecodeContext.get(), context.bytecodeContext.get(),
context.resourceContext.get(), context.resourceContext.get(),
context.packageMetadata.apkInfo.doNotCompress?.toList(),
) )
} }

View 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()
}
}

View file

@ -7,15 +7,16 @@ import brut.androlib.apk.ApkInfo
import brut.directory.ExtFile 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]. * 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<*>>() internal val allPatches = mutableMapOf<PatchClass, Patch<*>>()
/** /**
* The [ResourceContext] of this [PatcherContext]. * A context for the patcher containing the current state of the resources.
* This holds the current state of the resources.
*/ */
internal val resourceContext = ResourceContext(this, options) internal val resourceContext = ResourceContext(packageMetadata, config)
/** /**
* The [BytecodeContext] of this [PatcherContext]. * A context for the patcher containing the current state of the bytecode.
* This holds the current state of the bytecode.
*/ */
internal val bytecodeContext = BytecodeContext(options) internal val bytecodeContext = BytecodeContext(config)
} }

View file

@ -1,19 +1,8 @@
package app.revanced.patcher package app.revanced.patcher
import app.revanced.patcher.data.ResourceContext
import brut.androlib.Config
import java.io.File import java.io.File
import java.util.logging.Logger
/** @Deprecated("Use PatcherConfig instead.")
* 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.
*/
data class PatcherOptions( data class PatcherOptions(
internal val inputFile: File, internal val inputFile: File,
internal val resourceCachePath: File = File("revanced-resource-cache"), internal val resourceCachePath: File = File("revanced-resource-cache"),
@ -21,34 +10,16 @@ data class PatcherOptions(
internal val frameworkFileDirectory: String? = null, internal val frameworkFileDirectory: String? = null,
internal val multithreadingDexFileWriter: Boolean = false, 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()
/** return resourceCachePath
* 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()
}
} }

View file

@ -2,22 +2,121 @@ package app.revanced.patcher
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
import kotlin.jvm.internal.Intrinsics
/** /**
* The result of a patcher. * The result of a patcher.
*
* @param dexFiles The patched dex files. * @param dexFiles The patched dex files.
* @param resourceFile File containing resources that need to be extracted into the APK. * @param resources The patched resources.
* @param doNotCompress List of relative paths of files to exclude from compressing.
*/ */
data class PatcherResult( @Suppress("MemberVisibilityCanBePrivate")
val dexFiles: List<PatchedDexFile>, class PatcherResult internal constructor(
val resourceFile: File?, val dexFiles: Set<PatchedDexFile>,
val doNotCompress: List<String>? = null, 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 name The original name of the dex file.
* @param stream The dex file as [InputStream]. * @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>,
)
} }

View file

@ -1,7 +1,8 @@
package app.revanced.patcher.data package app.revanced.patcher.data
import app.revanced.patcher.InternalApi
import app.revanced.patcher.PatcherConfig
import app.revanced.patcher.PatcherContext import app.revanced.patcher.PatcherContext
import app.revanced.patcher.PatcherOptions
import app.revanced.patcher.PatcherResult import app.revanced.patcher.PatcherResult
import app.revanced.patcher.patch.Patch import app.revanced.patcher.patch.Patch
import app.revanced.patcher.util.ClassMerger.merge import app.revanced.patcher.util.ClassMerger.merge
@ -21,160 +22,163 @@ import java.io.Flushable
import java.util.logging.Logger import java.util.logging.Logger
/** /**
* A context for bytecode. * A context for the patcher containing the current state of the bytecode.
* This holds 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) : @Suppress("MemberVisibilityCanBePrivate")
Context<List<PatcherResult.PatchedDexFile>> { class BytecodeContext internal constructor(private val config: PatcherConfig) :
private val logger = Logger.getLogger(BytecodeContext::class.java.name) 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
/** logger.info("Merging integrations")
* The list of classes.
*/ val classMap = classes.associateBy { it.type }
val classes by lazy {
ProxyClassList( this@Integrations.forEach { integrations ->
MultiDexIO.readDexFile( MultiDexIO.readDexFile(
true, true,
options.inputFile, integrations,
BasicDexFileNamer(), BasicDexFileNamer(),
null, null,
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
}
/** logger.fine("$classDef exists. Adding missing methods and fields.")
* The [Integrations] of this [PatcherContext].
*/
internal val integrations = Integrations()
/** existingClass.merge(classDef, this@BytecodeContext).let { mergedClass ->
* Find a class by a given class name. // If the class was merged, replace the original class with the merged class.
* if (mergedClass === existingClass) return@let
* @param className The name of the class. classes.apply {
* @return A proxy for the first class that matches the class name. remove(existingClass)
*/ add(mergedClass)
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)
}
} }
} }
} }
clear()
} }
clear()
} }
} }
}

View file

@ -1,7 +1,10 @@
package app.revanced.patcher.data package app.revanced.patcher.data
import app.revanced.patcher.PatcherContext import app.revanced.patcher.InternalApi
import app.revanced.patcher.PatcherOptions 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 app.revanced.patcher.util.DomFileEditor
import brut.androlib.AaptInvoker import brut.androlib.AaptInvoker
import brut.androlib.ApkDecoder import brut.androlib.ApkDecoder
@ -19,72 +22,80 @@ import java.nio.file.Files
import java.util.logging.Logger import java.util.logging.Logger
/** /**
* A context for resources. * A context for the patcher containing the current state of the resources.
* This holds 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( class ResourceContext internal constructor(
private val context: PatcherContext, private val packageMetadata: PackageMetadata,
private val options: PatcherOptions, private val config: PatcherConfig,
) : Context<File?>, Iterable<File> { ) : Context<PatcherResult.PatchedResources?>, Iterable<File> {
private val logger = Logger.getLogger(ResourceContext::class.java.name) 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() val xmlEditor = XmlFileHolder()
/** /**
* Decode resources for the patcher. * Predicate to delete resources from [PatcherConfig.apkFiles].
*
* @param mode The [ResourceDecodingMode] to use when decoding.
*/ */
internal fun decodeResources(mode: ResourceDecodingMode) = private val deleteResources = mutableSetOf<(String) -> Boolean>()
with(context.packageMetadata.apkInfo) {
/**
* 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. // Needed to decode resources.
val resourcesDecoder = ResourcesDecoder(options.resourceConfig, this) val resourcesDecoder = ResourcesDecoder(config.resourceConfig, this)
when (mode) { if (mode == ResourceMode.FULL) {
ResourceDecodingMode.FULL -> { logger.info("Decoding resources")
val outDir = options.recreateResourceCacheDirectory()
logger.info("Decoding resources") resourcesDecoder.decodeResources(config.apkFiles)
resourcesDecoder.decodeManifest(config.apkFiles)
resourcesDecoder.decodeResources(outDir) // Needed to record uncompressed files.
resourcesDecoder.decodeManifest(outDir) val apkDecoder = ApkDecoder(config.resourceConfig, this)
apkDecoder.recordUncompressedFiles(resourcesDecoder.resFileMapping)
// Needed to record uncompressed files. usesFramework =
val apkDecoder = ApkDecoder(options.resourceConfig, this) UsesFramework().apply {
apkDecoder.recordUncompressedFiles(resourcesDecoder.resFileMapping) ids = resourcesDecoder.resTable.listFramePackages().map { it.id }
}
} else {
logger.info("Decoding app manifest")
usesFramework = // Decode manually instead of using resourceDecoder.decodeManifest
UsesFramework().apply { // because it does not support decoding to an OutputStream.
ids = resourcesDecoder.resTable.listFramePackages().map { it.id } 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 -> { // Get the package name and version from the manifest using the XmlPullStreamDecoder.
logger.info("Decoding app manifest") // XmlPullStreamDecoder.decodeManifest() sets metadata.apkInfo.
packageMetadata.let { metadata ->
// Decode manually instead of using resourceDecoder.decodeManifest metadata.packageName = resourcesDecoder.resTable.packageRenamed
// because it does not support decoding to an OutputStream. versionInfo.let {
XmlPullStreamDecoder( metadata.packageVersion = it.versionName ?: it.versionCode
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
}
/* /*
The ResTable if flagged as sparse if the main package is not loaded, which is the case here, 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. 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? { @InternalApi
var resourceFile: File? = null 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 resources = config.patchedFiles.resolve("resources").also { it.mkdirs() }
val aaptFile =
cacheDirectory.resolve("aapt_temp_file").also {
Files.deleteIfExists(it.toPath())
}.also { resourceFile = it }
try { val resourcesApkFile =
AaptInvoker( if (config.resourceMode == ResourceMode.FULL) {
options.resourceConfig, resources.resolve("resources.apk").apply {
context.packageMetadata.apkInfo, // Compile the resources.apk file.
).invokeAapt( AaptInvoker(
aaptFile, config.resourceConfig,
cacheDirectory.resolve("AndroidManifest.xml").also { packageMetadata.apkInfo,
ResXmlPatcher.fixingPublicAttrsInProviderAttributes(it) ).invokeAapt(
}, resources.resolve("resources.apk"),
cacheDirectory.resolve("res"), config.apkFiles.resolve("AndroidManifest.xml").also {
null, ResXmlPatcher.fixingPublicAttrsInProviderAttributes(it)
null, },
context.packageMetadata.apkInfo.usesFramework.let { usesFramework -> config.apkFiles.resolve("res"),
usesFramework.ids.map { id -> null,
Framework(options.resourceConfig).getFrameworkApk(id, usesFramework.tag) null,
}.toTypedArray() packageMetadata.apkInfo.usesFramework.let { usesFramework ->
}, usesFramework.ids.map { id ->
) Framework(config.resourceConfig).getFrameworkApk(id, usesFramework.tag)
} finally { }.toTypedArray()
cacheDirectory.close() },
)
}
} 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, 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 { inner class XmlFileHolder {
operator fun get(inputStream: InputStream) = DomFileEditor(inputStream) operator fun get(inputStream: InputStream) = DomFileEditor(inputStream)

View file

@ -7,10 +7,10 @@ import app.revanced.patcher.fingerprint.MethodFingerprint
import java.io.Closeable 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] * 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") @Suppress("unused")
abstract class BytecodePatch : Patch<BytecodeContext> { abstract class BytecodePatch : Patch<BytecodeContext> {

View file

@ -10,10 +10,10 @@ import app.revanced.patcher.patch.options.PatchOptions
import java.io.Closeable import java.io.Closeable
/** /**
* A ReVanced patch. * A patch.
* *
* If an implementation of [Patch] also implements [Closeable] * 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. * @param T The [Context] type this patch will work on.
*/ */

View file

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

View file

@ -6,10 +6,15 @@ import app.revanced.patcher.data.ResourceContext
import java.io.Closeable 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] * 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> { abstract class ResourcePatch : Patch<ResourceContext> {
/** /**

View 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>()
}
}

View file

@ -4,85 +4,22 @@ import org.w3c.dom.Document
import java.io.Closeable import java.io.Closeable
import java.io.File import java.io.File
import java.io.InputStream 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
/** @Deprecated("Use Document instead.")
* Wrapper for a file that can be edited as a dom document. class DomFileEditor : Closeable {
* val file: Document
* This constructor does not check for locks to the file when writing. internal constructor(
* Use the secondary constructor. inputStream: InputStream,
* ) {
* @param inputStream the input stream to read the xml file from. file = Document(inputStream)
* @param outputStream the output stream to write the xml file to. If null, the file will be read only. }
*
*/ constructor(file: File) {
class DomFileEditor internal constructor( this.file = Document(file)
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
} }
/**
* 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() { override fun close() {
if (closed) return file as app.revanced.patcher.util.Document
file.close()
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>()
} }
} }

View file

@ -1,7 +1,6 @@
package app.revanced.patcher.patch package app.revanced.patcher.patch
import app.revanced.patcher.data.ResourceContext import app.revanced.patcher.data.ResourceContext
import org.junit.jupiter.api.assertThrows
import kotlin.test.Test import kotlin.test.Test
import app.revanced.patcher.patch.annotation.Patch as PatchAnnotation import app.revanced.patcher.patch.annotation.Patch as PatchAnnotation
@ -9,7 +8,7 @@ object PatchInitializationTest {
@Test @Test
fun `initialize using constructor`() { fun `initialize using constructor`() {
val patch = val patch =
object : ResourcePatch(name = "Resource patch test") { object : RawResourcePatch(name = "Resource patch test") {
override fun execute(context: ResourceContext) {} override fun execute(context: ResourceContext) {}
} }
@ -20,7 +19,7 @@ object PatchInitializationTest {
fun `initialize using annotation`() { fun `initialize using annotation`() {
val patch = val patch =
@PatchAnnotation("Resource patch test") @PatchAnnotation("Resource patch test")
object : ResourcePatch() { object : RawResourcePatch() {
override fun execute(context: ResourceContext) {} override fun execute(context: ResourceContext) {}
} }

View file

@ -6,17 +6,9 @@ import org.w3c.dom.Element
class ExampleResourcePatch : ResourcePatch() { class ExampleResourcePatch : ResourcePatch() {
override fun execute(context: ResourceContext) { override fun execute(context: ResourceContext) {
context.xmlEditor["AndroidManifest.xml"].use { editor -> context.document["AndroidManifest.xml"].use { document ->
val element = val element = document.getElementsByTagName("application").item(0) as Element
editor // regular DomFileEditor element.setAttribute("exampleAttribute", "exampleValue")
.file
.getElementsByTagName("application")
.item(0) as Element
element
.setAttribute(
"exampleAttribute",
"exampleValue",
)
} }
} }
} }