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.executablePatches.putIfAbsent(patch::class, patch) ?: run {
context.allPatches[patch::class] = patch 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,11 +86,14 @@ 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.
@ -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,17 +22,17 @@ 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) :
Context<Set<PatcherResult.PatchedDexFile>> {
private val logger = Logger.getLogger(BytecodeContext::class.java.name) private val logger = Logger.getLogger(BytecodeContext::class.java.name)
/** /**
* [Opcodes] of the supplied [PatcherOptions.inputFile]. * [Opcodes] of the supplied [PatcherConfig.apkFile].
*/ */
internal lateinit var opcodes: Opcodes internal lateinit var opcodes: Opcodes
@ -42,7 +43,7 @@ class BytecodeContext internal constructor(private val options: PatcherOptions)
ProxyClassList( ProxyClassList(
MultiDexIO.readDexFile( MultiDexIO.readDexFile(
true, true,
options.inputFile, config.apkFile,
BasicDexFileNamer(), BasicDexFileNamer(),
null, null,
null, null,
@ -100,17 +101,18 @@ class BytecodeContext internal constructor(private val options: PatcherOptions)
* *
* @return The compiled bytecode. * @return The compiled bytecode.
*/ */
override fun get(): List<PatcherResult.PatchedDexFile> { @InternalApi
override fun get(): Set<PatcherResult.PatchedDexFile> {
logger.info("Compiling patched dex files") logger.info("Compiling patched dex files")
val patchedDexFileResults = val patchedDexFileResults =
options.resourceCachePath.resolve("dex").also { config.patchedFiles.resolve("dex").also {
it.deleteRecursively() // Make sure the directory is empty. it.deleteRecursively() // Make sure the directory is empty.
it.mkdirs() it.mkdirs()
}.apply { }.apply {
MultiDexIO.writeDexFile( MultiDexIO.writeDexFile(
true, true,
if (options.multithreadingDexFileWriter) -1 else 1, if (config.multithreadingDexFileWriter) -1 else 1,
this, this,
BasicDexFileNamer(), BasicDexFileNamer(),
object : DexFile { object : DexFile {
@ -120,7 +122,9 @@ class BytecodeContext internal constructor(private val options: PatcherOptions)
}, },
DexIO.DEFAULT_MAX_DEX_POOL_SIZE, DexIO.DEFAULT_MAX_DEX_POOL_SIZE,
) { _, entryName, _ -> logger.info("Compiled $entryName") } ) { _, entryName, _ -> logger.info("Compiled $entryName") }
}.listFiles(FileFilter { it.isFile })!!.map { PatcherResult.PatchedDexFile(it.name, it.inputStream()) } }.listFiles(FileFilter { it.isFile })!!.map {
PatcherResult.PatchedDexFile(it.name, it.inputStream())
}.toSet()
System.gc() System.gc()

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,49 +22,57 @@ 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) {
ResourceDecodingMode.FULL -> {
val outDir = options.recreateResourceCacheDirectory()
if (mode == ResourceMode.FULL) {
logger.info("Decoding resources") logger.info("Decoding resources")
resourcesDecoder.decodeResources(outDir) resourcesDecoder.decodeResources(config.apkFiles)
resourcesDecoder.decodeManifest(outDir) resourcesDecoder.decodeManifest(config.apkFiles)
// Needed to record uncompressed files. // Needed to record uncompressed files.
val apkDecoder = ApkDecoder(options.resourceConfig, this) val apkDecoder = ApkDecoder(config.resourceConfig, this)
apkDecoder.recordUncompressedFiles(resourcesDecoder.resFileMapping) apkDecoder.recordUncompressedFiles(resourcesDecoder.resFileMapping)
usesFramework = usesFramework =
UsesFramework().apply { UsesFramework().apply {
ids = resourcesDecoder.resTable.listFramePackages().map { it.id } ids = resourcesDecoder.resTable.listFramePackages().map { it.id }
} }
} } else {
ResourceDecodingMode.MANIFEST_ONLY -> {
logger.info("Decoding app manifest") logger.info("Decoding app manifest")
// Decode manually instead of using resourceDecoder.decodeManifest // Decode manually instead of using resourceDecoder.decodeManifest
@ -73,14 +84,14 @@ class ResourceContext internal constructor(
apkFile.directory.getFileInput("AndroidManifest.xml"), apkFile.directory.getFileInput("AndroidManifest.xml"),
// Older Android versions do not support OutputStream.nullOutputStream() // Older Android versions do not support OutputStream.nullOutputStream()
object : OutputStream() { object : OutputStream() {
override fun write(b: Int) { // do nothing override fun write(b: Int) { // Do nothing.
} }
}, },
) )
// Get the package name and version from the manifest using the XmlPullStreamDecoder. // Get the package name and version from the manifest using the XmlPullStreamDecoder.
// XmlPullStreamDecoder.decodeManifest() sets metadata.apkInfo. // XmlPullStreamDecoder.decodeManifest() sets metadata.apkInfo.
context.packageMetadata.let { metadata -> packageMetadata.let { metadata ->
metadata.packageName = resourcesDecoder.resTable.packageRenamed metadata.packageName = resourcesDecoder.resTable.packageRenamed
versionInfo.let { versionInfo.let {
metadata.packageVersion = it.versionName ?: it.versionCode metadata.packageVersion = it.versionName ?: it.versionCode
@ -98,70 +109,143 @@ class ResourceContext internal constructor(
} }
} }
} }
}
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 =
if (config.resourceMode == ResourceMode.FULL) {
resources.resolve("resources.apk").apply {
// Compile the resources.apk file.
AaptInvoker( AaptInvoker(
options.resourceConfig, config.resourceConfig,
context.packageMetadata.apkInfo, packageMetadata.apkInfo,
).invokeAapt( ).invokeAapt(
aaptFile, resources.resolve("resources.apk"),
cacheDirectory.resolve("AndroidManifest.xml").also { config.apkFiles.resolve("AndroidManifest.xml").also {
ResXmlPatcher.fixingPublicAttrsInProviderAttributes(it) ResXmlPatcher.fixingPublicAttrsInProviderAttributes(it)
}, },
cacheDirectory.resolve("res"), config.apkFiles.resolve("res"),
null, null,
null, null,
context.packageMetadata.apkInfo.usesFramework.let { usesFramework -> packageMetadata.apkInfo.usesFramework.let { usesFramework ->
usesFramework.ids.map { id -> usesFramework.ids.map { id ->
Framework(options.resourceConfig).getFrameworkApk(id, usesFramework.tag) Framework(config.resourceConfig).getFrameworkApk(id, usesFramework.tag)
}.toTypedArray() }.toTypedArray()
}, },
) )
} finally {
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)
}
}
}
}
/** /**
* Decode all resources. * 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 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",
)
} }
} }
} }