mirror of
https://github.com/ReVanced/revanced-patcher.git
synced 2024-11-10 01:02:22 +01:00
feat: Read and write arbitrary files in APK files
This commit allows reading and writing arbitrary files in an APK file. Additionally it allows deleting files from APK files. A `RawResourcePatch` class has been added which has access to `ResourceContext` but ReVanced Patcher will not decode APK resources. A regular `ResourcePatch` can read and write arbitrary files from an APK file, unless they are decoded to `PatcherConfig.apkFiles`. On attempt to get a file from `PatcherConfig.apkFiles` if the second parameter is true, it will read and write the raw resource file from the original APK to `PatcherConfig.apkFiles` if it does not exist. With this commit, many APIs have been deprecated as well, such as `DomFileEditor` and instead a `Document` has been added.
This commit is contained in:
parent
64dd1526cd
commit
f1d7217495
23 changed files with 828 additions and 447 deletions
3
.editorconfig
Normal file
3
.editorconfig
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[*.{kt,kts}]
|
||||||
|
ktlint_code_style = intellij_idea
|
||||||
|
ktlint_standard_no-wildcard-imports = disabled
|
|
@ -1,5 +1,9 @@
|
||||||
public abstract interface class app/revanced/patcher/IntegrationsConsumer {
|
public abstract 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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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" }
|
|
@ -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>)
|
||||||
}
|
}
|
||||||
|
|
7
src/main/kotlin/app/revanced/patcher/InternalApi.kt
Normal file
7
src/main/kotlin/app/revanced/patcher/InternalApi.kt
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
package app.revanced.patcher
|
||||||
|
|
||||||
|
@RequiresOptIn(
|
||||||
|
level = RequiresOptIn.Level.ERROR,
|
||||||
|
message = "This is an internal API, don't rely on it.",
|
||||||
|
)
|
||||||
|
annotation class InternalApi
|
|
@ -4,6 +4,8 @@ import brut.androlib.apk.ApkInfo
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Metadata about a package.
|
* 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
|
||||||
|
|
|
@ -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(),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
72
src/main/kotlin/app/revanced/patcher/PatcherConfig.kt
Normal file
72
src/main/kotlin/app/revanced/patcher/PatcherConfig.kt
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
package app.revanced.patcher
|
||||||
|
|
||||||
|
import app.revanced.patcher.data.ResourceContext
|
||||||
|
import brut.androlib.Config
|
||||||
|
import java.io.File
|
||||||
|
import java.util.logging.Logger
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The configuration for the patcher.
|
||||||
|
*
|
||||||
|
* @param apkFile The apk file to patch.
|
||||||
|
* @param temporaryFilesPath A path to a folder to store temporary files in.
|
||||||
|
* @param aaptBinaryPath A path to a custom aapt binary.
|
||||||
|
* @param frameworkFileDirectory A path to the directory to cache the framework file in.
|
||||||
|
* @param multithreadingDexFileWriter Whether to use multiple threads for writing dex files.
|
||||||
|
* This has impact on memory usage and performance.
|
||||||
|
*/
|
||||||
|
class PatcherConfig(
|
||||||
|
internal val apkFile: File,
|
||||||
|
private val temporaryFilesPath: File = File("revanced-temporary-files"),
|
||||||
|
aaptBinaryPath: String? = null,
|
||||||
|
frameworkFileDirectory: String? = null,
|
||||||
|
internal val multithreadingDexFileWriter: Boolean = false,
|
||||||
|
) {
|
||||||
|
private val logger = Logger.getLogger(PatcherConfig::class.java.name)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The mode to use for resource decoding and compiling.
|
||||||
|
*
|
||||||
|
* @see ResourceContext.ResourceMode
|
||||||
|
*/
|
||||||
|
internal var resourceMode = ResourceContext.ResourceMode.NONE
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The configuration for decoding and compiling resources.
|
||||||
|
*/
|
||||||
|
internal val resourceConfig =
|
||||||
|
Config.getDefaultConfig().apply {
|
||||||
|
useAapt2 = true
|
||||||
|
aaptPath = aaptBinaryPath ?: ""
|
||||||
|
frameworkDirectory = frameworkFileDirectory
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The path to the temporary apk files directory.
|
||||||
|
*/
|
||||||
|
internal val apkFiles = temporaryFilesPath.resolve("apk")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The path to the temporary patched files directory.
|
||||||
|
*/
|
||||||
|
internal val patchedFiles = temporaryFilesPath.resolve("patched")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the temporary files' directories.
|
||||||
|
* This will delete the existing temporary files directory if it exists.
|
||||||
|
*/
|
||||||
|
internal fun initializeTemporaryFilesDirectories() {
|
||||||
|
temporaryFilesPath.apply {
|
||||||
|
if (exists()) {
|
||||||
|
logger.info("Deleting existing temporary files directory")
|
||||||
|
|
||||||
|
if (!deleteRecursively()) {
|
||||||
|
logger.severe("Failed to delete existing temporary files directory")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apkFiles.mkdirs()
|
||||||
|
patchedFiles.mkdirs()
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,15 +7,16 @@ import brut.androlib.apk.ApkInfo
|
||||||
import brut.directory.ExtFile
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
package app.revanced.patcher.patch
|
||||||
|
|
||||||
|
import app.revanced.patcher.PatchClass
|
||||||
|
import app.revanced.patcher.Patcher
|
||||||
|
import app.revanced.patcher.data.ResourceContext
|
||||||
|
import java.io.Closeable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [Patch] that accesses a [ResourceContext].
|
||||||
|
*
|
||||||
|
* If an implementation of [Patch] also implements [Closeable]
|
||||||
|
* it will be closed in reverse execution order of patches executed by [Patcher].
|
||||||
|
*
|
||||||
|
* This type of patch that does not have access to decoded resources.
|
||||||
|
* Instead, you can read and write arbitrary files in an APK file.
|
||||||
|
*
|
||||||
|
* If you want to access decoded resources, use [ResourcePatch] instead.
|
||||||
|
*/
|
||||||
|
abstract class RawResourcePatch : Patch<ResourceContext> {
|
||||||
|
/**
|
||||||
|
* Create a new [RawResourcePatch].
|
||||||
|
*/
|
||||||
|
constructor()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new [RawResourcePatch].
|
||||||
|
*
|
||||||
|
* @param name The name of the patch.
|
||||||
|
* @param description The description of the patch.
|
||||||
|
* @param compatiblePackages The packages the patch is compatible with.
|
||||||
|
* @param dependencies Other patches this patch depends on.
|
||||||
|
* @param use Weather or not the patch should be used.
|
||||||
|
* @param requiresIntegrations Weather or not the patch requires integrations.
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
name: String? = null,
|
||||||
|
description: String? = null,
|
||||||
|
compatiblePackages: Set<CompatiblePackage>? = null,
|
||||||
|
dependencies: Set<PatchClass>? = null,
|
||||||
|
use: Boolean = true,
|
||||||
|
requiresIntegrations: Boolean = false,
|
||||||
|
) : super(name, description, compatiblePackages, dependencies, use, requiresIntegrations)
|
||||||
|
}
|
|
@ -6,10 +6,15 @@ import app.revanced.patcher.data.ResourceContext
|
||||||
import java.io.Closeable
|
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> {
|
||||||
/**
|
/**
|
||||||
|
|
48
src/main/kotlin/app/revanced/patcher/util/Document.kt
Normal file
48
src/main/kotlin/app/revanced/patcher/util/Document.kt
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
package app.revanced.patcher.util
|
||||||
|
|
||||||
|
import org.w3c.dom.Document
|
||||||
|
import java.io.Closeable
|
||||||
|
import java.io.File
|
||||||
|
import java.io.InputStream
|
||||||
|
import javax.xml.parsers.DocumentBuilderFactory
|
||||||
|
import javax.xml.transform.TransformerFactory
|
||||||
|
import javax.xml.transform.dom.DOMSource
|
||||||
|
import javax.xml.transform.stream.StreamResult
|
||||||
|
|
||||||
|
class Document internal constructor(
|
||||||
|
inputStream: InputStream,
|
||||||
|
) : Document by DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputStream), Closeable {
|
||||||
|
private var file: File? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
normalize()
|
||||||
|
}
|
||||||
|
|
||||||
|
internal constructor(file: File) : this(file.inputStream()) {
|
||||||
|
this.file = file
|
||||||
|
readerCount.merge(file, 1, Int::plus)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
file?.let {
|
||||||
|
if (readerCount[it]!! > 1) {
|
||||||
|
throw IllegalStateException(
|
||||||
|
"Two or more instances are currently reading $it." +
|
||||||
|
"To be able to close this instance, no other instances may be reading $it at the same time.",
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
readerCount.remove(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
it.outputStream().use { stream ->
|
||||||
|
TransformerFactory.newInstance()
|
||||||
|
.newTransformer()
|
||||||
|
.transform(DOMSource(this), StreamResult(stream))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
private val readerCount = mutableMapOf<File, Int>()
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,85 +4,22 @@ import org.w3c.dom.Document
|
||||||
import java.io.Closeable
|
import java.io.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>()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue