Compare commits

...

24 Commits

Author SHA1 Message Date
oSumAtrIX
91cb464a27 fix nullability 2026-01-11 03:09:49 +01:00
oSumAtrIX
dc8565e8a6 use context params for apis 2026-01-09 19:06:50 +01:00
oSumAtrIX
1a052b9787 contextualize delegate variants and composite api 2026-01-09 18:53:35 +01:00
oSumAtrIX
ed56bf49ad make typealiases public 2026-01-09 16:02:46 +01:00
oSumAtrIX
56fd65d6ce formatting 2026-01-08 00:51:56 +01:00
oSumAtrIX
5674c1f2a2 more completeness 2026-01-08 00:51:17 +01:00
oSumAtrIX
d2461f92aa more or less finish composite api 2026-01-06 04:06:37 +01:00
oSumAtrIX
18570656cc merge matching and tests module with patcher, make builder api context aware and refactor 2025-12-30 00:02:16 +01:00
oSumAtrIX
005c91bc08 Modernize patch api names, deprecate fingerprints, simplify patching code even more, add mutablemethod implementation setter, refactor tests and improve for better coverage 2025-12-29 07:41:07 +01:00
oSumAtrIX
f17fbd8c40 Refactor matching API into separate module, simplify & refactor matching code, convert patcher to functions/DSL, refactor & greatly simplify internal code, refactor & simplify patch api internal code, update deps, fix workflow, add callback for patches files failed to load to be able to be able to load the rest of the patches 2025-12-29 03:41:10 +01:00
oSumAtrIX
2c97de2894 Use Kotlin Multiplatform, update to new binary compatibility plugin, more or less finish composite api, modularize project for separation of APIs, update Gradle wrapper, add new handy declarative predicate APIs, use new publishing plugin, simplify/modernize build system, standardize gitignore, and optimize build properties for performance 2025-12-14 03:17:33 +01:00
oSumAtrIX
2509997432 feat: Fix property delegates & add cache 2025-11-30 19:21:35 +01:00
oSumAtrIX
58ff464192 feat: Simplify composite API 2025-11-28 17:43:51 +01:00
oSumAtrIX
4106ce4070 feat: Composition API 2025-11-28 16:10:58 +01:00
oSumAtrIX
e6eaf6cb73 feat: Infer cache key from unique instance hashcode 2025-11-28 14:25:35 +01:00
oSumAtrIX
c56ac7a81f fix: Everything works now 2025-11-27 00:36:36 +01:00
oSumAtrIX
cdc480acea fix: More bugfixes, patches now compile 2025-11-26 23:12:46 +01:00
oSumAtrIX
14f2eb69e4 fix: Couple more matching fixes 2025-11-26 10:25:19 +01:00
oSumAtrIX
fcdaf324fe fix: Do not create a new matcher instance if provided with one 2025-11-23 16:02:26 +01:00
oSumAtrIX
8653d8304b feat: Appy upstream 2025-11-22 23:20:51 +01:00
oSumAtrIX
e2c781f12c feat: Builds now 2025-11-22 19:23:48 +01:00
oSumAtrIX
0b5e8b791d feat: More progress towards compatibility 2025-11-20 20:08:22 +01:00
oSumAtrIX
cf57726bbb feat: More APIs/adjustments 2025-11-19 20:08:49 +01:00
oSumAtrIX
79d3640186 feat: Modernize APIs 2025-11-19 00:20:41 +01:00
97 changed files with 8776 additions and 4907 deletions

View File

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

View File

@@ -17,18 +17,14 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v5
with:
# Make sure the release step uses its own credentials:
# https://github.com/cycjimmy/semantic-release-action#private-packages
persist-credentials: false
fetch-depth: 0
- name: Cache Gradle
uses: burrunan/gradle-cache-action@v3
- name: Build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ env.GITHUB_ACTOR }}
ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }}
run: ./gradlew build clean
- name: Setup Node.js
@@ -48,6 +44,7 @@ jobs:
fingerprint: ${{ vars.GPG_FINGERPRINT }}
- name: Release
uses: cycjimmy/semantic-release-action@v4
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npm exec semantic-release
ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ env.GITHUB_ACTOR }}
ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }}

133
.gitignore vendored
View File

@@ -1,124 +1,19 @@
### Java template
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
.idea/artifacts
.idea/compiler.xml
.idea/jarRepositories.xml
.idea/modules.xml
.idea/*.iml
.idea/modules
*.iml
*.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
.idea/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### Gradle template
.kotlin
.gradle
**/build/
xcuserdata
!src/**/build/
# Ignore Gradle GUI config
gradle-app.setting
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar
# Cache of project
.gradletasknamecache
# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
# gradle/wrapper/gradle-wrapper.properties
# Avoid ignoring test resources
!src/test/resources/*
# Dependency directories
local.properties
.idea
.DS_Store
captures
.externalNativeBuild
.cxx
*.xcodeproj/*
!*.xcodeproj/project.pbxproj
!*.xcodeproj/xcshareddata/
!*.xcodeproj/project.xcworkspace/
!*.xcworkspace/contents.xcworkspacedata
**/xcshareddata/WorkspaceSettings.xcsettings
node_modules/
# Gradle props, to avoid sharing the gpr key
gradle.properties

View File

@@ -1,889 +0,0 @@
public final class app/revanced/patcher/Fingerprint {
public final fun getClassDef (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;
public final fun getClassDefOrNull (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;
public final fun getMethod (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;
public final fun getMethodOrNull (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;
public final fun getOriginalClassDef (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lcom/android/tools/smali/dexlib2/iface/ClassDef;
public final fun getOriginalClassDefOrNull (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lcom/android/tools/smali/dexlib2/iface/ClassDef;
public final fun getOriginalMethod (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lcom/android/tools/smali/dexlib2/iface/Method;
public final fun getOriginalMethodOrNull (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lcom/android/tools/smali/dexlib2/iface/Method;
public final fun getPatternMatch (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lapp/revanced/patcher/Match$PatternMatch;
public final fun getPatternMatchOrNull (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lapp/revanced/patcher/Match$PatternMatch;
public final fun getStringMatches (Lapp/revanced/patcher/patch/BytecodePatchContext;)Ljava/util/List;
public final fun getStringMatchesOrNull (Lapp/revanced/patcher/patch/BytecodePatchContext;)Ljava/util/List;
public final fun match (Lapp/revanced/patcher/patch/BytecodePatchContext;Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Lapp/revanced/patcher/Match;
public final fun match (Lapp/revanced/patcher/patch/BytecodePatchContext;Lcom/android/tools/smali/dexlib2/iface/Method;)Lapp/revanced/patcher/Match;
public final fun match (Lapp/revanced/patcher/patch/BytecodePatchContext;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Lapp/revanced/patcher/Match;
public final fun matchOrNull (Lapp/revanced/patcher/patch/BytecodePatchContext;Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Lapp/revanced/patcher/Match;
public final fun matchOrNull (Lapp/revanced/patcher/patch/BytecodePatchContext;Lcom/android/tools/smali/dexlib2/iface/Method;)Lapp/revanced/patcher/Match;
public final fun matchOrNull (Lapp/revanced/patcher/patch/BytecodePatchContext;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Lapp/revanced/patcher/Match;
}
public final class app/revanced/patcher/FingerprintBuilder {
public fun <init> ()V
public final fun accessFlags (I)V
public final fun accessFlags ([Lcom/android/tools/smali/dexlib2/AccessFlags;)V
public final fun custom (Lkotlin/jvm/functions/Function2;)V
public final fun opcodes (Ljava/lang/String;)V
public final fun opcodes ([Lcom/android/tools/smali/dexlib2/Opcode;)V
public final fun parameters ([Ljava/lang/String;)V
public final fun returns (Ljava/lang/String;)V
public final fun strings ([Ljava/lang/String;)V
}
public final class app/revanced/patcher/FingerprintKt {
public static final fun fingerprint (ILkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/Fingerprint;
public static synthetic fun fingerprint$default (ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/Fingerprint;
}
public abstract interface annotation class app/revanced/patcher/InternalApi : java/lang/annotation/Annotation {
}
public final class app/revanced/patcher/Match {
public final fun getClassDef ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;
public final fun getMethod ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;
public final fun getOriginalClassDef ()Lcom/android/tools/smali/dexlib2/iface/ClassDef;
public final fun getOriginalMethod ()Lcom/android/tools/smali/dexlib2/iface/Method;
public final fun getPatternMatch ()Lapp/revanced/patcher/Match$PatternMatch;
public final fun getStringMatches ()Ljava/util/List;
}
public final class app/revanced/patcher/Match$PatternMatch {
public final fun getEndIndex ()I
public final fun getStartIndex ()I
}
public final class app/revanced/patcher/Match$StringMatch {
public final fun getIndex ()I
public final fun getString ()Ljava/lang/String;
}
public final class app/revanced/patcher/PackageMetadata {
public final fun getPackageName ()Ljava/lang/String;
public final fun getPackageVersion ()Ljava/lang/String;
}
public final class app/revanced/patcher/Patcher : java/io/Closeable {
public fun <init> (Lapp/revanced/patcher/PatcherConfig;)V
public fun close ()V
public final fun get ()Lapp/revanced/patcher/PatcherResult;
public final fun getContext ()Lapp/revanced/patcher/PatcherContext;
public final fun invoke ()Lkotlinx/coroutines/flow/Flow;
public final fun plusAssign (Ljava/util/Set;)V
}
public final class app/revanced/patcher/PatcherConfig {
public fun <init> (Ljava/io/File;Ljava/io/File;Ljava/io/File;Ljava/lang/String;)V
public synthetic fun <init> (Ljava/io/File;Ljava/io/File;Ljava/io/File;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;)V
public synthetic fun <init> (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
}
public final class app/revanced/patcher/PatcherContext : java/io/Closeable {
public fun close ()V
public final fun getPackageMetadata ()Lapp/revanced/patcher/PackageMetadata;
}
public final class app/revanced/patcher/PatcherResult {
public final fun getDexFiles ()Ljava/util/Set;
public final fun getResources ()Lapp/revanced/patcher/PatcherResult$PatchedResources;
}
public final class app/revanced/patcher/PatcherResult$PatchedDexFile {
public final fun getName ()Ljava/lang/String;
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 final class app/revanced/patcher/extensions/ExtensionsKt {
public static final fun newLabel (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;I)Lcom/android/tools/smali/dexlib2/builder/Label;
}
public final class app/revanced/patcher/extensions/InstructionExtensions {
public static final field INSTANCE Lapp/revanced/patcher/extensions/InstructionExtensions;
public final fun addInstruction (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;ILcom/android/tools/smali/dexlib2/builder/BuilderInstruction;)V
public final fun addInstruction (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;ILjava/lang/String;)V
public final fun addInstruction (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Lcom/android/tools/smali/dexlib2/builder/BuilderInstruction;)V
public final fun addInstruction (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Ljava/lang/String;)V
public final fun addInstructions (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;ILjava/lang/String;)V
public final fun addInstructions (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;ILjava/util/List;)V
public final fun addInstructions (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Ljava/lang/String;)V
public final fun addInstructions (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Ljava/util/List;)V
public final fun addInstructions (Lcom/android/tools/smali/dexlib2/builder/MutableMethodImplementation;ILjava/util/List;)V
public final fun addInstructions (Lcom/android/tools/smali/dexlib2/builder/MutableMethodImplementation;Ljava/util/List;)V
public final fun addInstructionsWithLabels (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;ILjava/lang/String;[Lapp/revanced/patcher/util/smali/ExternalLabel;)V
public final fun getInstruction (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;I)Lcom/android/tools/smali/dexlib2/builder/BuilderInstruction;
public final fun getInstruction (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;I)Ljava/lang/Object;
public final fun getInstruction (Lcom/android/tools/smali/dexlib2/builder/MutableMethodImplementation;I)Lcom/android/tools/smali/dexlib2/builder/BuilderInstruction;
public final fun getInstruction (Lcom/android/tools/smali/dexlib2/builder/MutableMethodImplementation;I)Ljava/lang/Object;
public final fun getInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;I)Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;
public final fun getInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;I)Ljava/lang/Object;
public final fun getInstruction (Lcom/android/tools/smali/dexlib2/iface/MethodImplementation;I)Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;
public final fun getInstruction (Lcom/android/tools/smali/dexlib2/iface/MethodImplementation;I)Ljava/lang/Object;
public final fun getInstructionOrNull (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;I)Lcom/android/tools/smali/dexlib2/builder/BuilderInstruction;
public final fun getInstructionOrNull (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;I)Ljava/lang/Object;
public final fun getInstructionOrNull (Lcom/android/tools/smali/dexlib2/iface/Method;I)Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;
public final fun getInstructionOrNull (Lcom/android/tools/smali/dexlib2/iface/Method;I)Ljava/lang/Object;
public final fun getInstructions (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;)Ljava/util/List;
public final fun getInstructions (Lcom/android/tools/smali/dexlib2/iface/Method;)Ljava/lang/Iterable;
public final fun getInstructionsOrNull (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;)Ljava/util/List;
public final fun getInstructionsOrNull (Lcom/android/tools/smali/dexlib2/iface/Method;)Ljava/lang/Iterable;
public final fun removeInstruction (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;I)V
public final fun removeInstructions (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;I)V
public final fun removeInstructions (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;II)V
public final fun removeInstructions (Lcom/android/tools/smali/dexlib2/builder/MutableMethodImplementation;I)V
public final fun removeInstructions (Lcom/android/tools/smali/dexlib2/builder/MutableMethodImplementation;II)V
public final fun replaceInstruction (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;ILcom/android/tools/smali/dexlib2/builder/BuilderInstruction;)V
public final fun replaceInstruction (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;ILjava/lang/String;)V
public final fun replaceInstructions (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;ILjava/lang/String;)V
public final fun replaceInstructions (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;ILjava/util/List;)V
public final fun replaceInstructions (Lcom/android/tools/smali/dexlib2/builder/MutableMethodImplementation;ILjava/util/List;)V
}
public final class app/revanced/patcher/patch/BytecodePatch : app/revanced/patcher/patch/Patch {
public final fun getExtensionInputStream ()Ljava/util/function/Supplier;
public fun toString ()Ljava/lang/String;
}
public final class app/revanced/patcher/patch/BytecodePatchBuilder : app/revanced/patcher/patch/PatchBuilder {
public synthetic fun build$revanced_patcher ()Lapp/revanced/patcher/patch/Patch;
public final fun extendWith (Ljava/lang/String;)Lapp/revanced/patcher/patch/BytecodePatchBuilder;
public final fun getExtensionInputStream ()Ljava/util/function/Supplier;
public final fun setExtensionInputStream (Ljava/util/function/Supplier;)V
}
public final class app/revanced/patcher/patch/BytecodePatchContext : app/revanced/patcher/patch/PatchContext, java/io/Closeable {
public final fun classBy (Lkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/util/proxy/ClassProxy;
public fun close ()V
public synthetic fun get ()Ljava/lang/Object;
public fun get ()Ljava/util/Set;
public final fun getClasses ()Lapp/revanced/patcher/util/ProxyClassList;
public final fun navigate (Lcom/android/tools/smali/dexlib2/iface/reference/MethodReference;)Lapp/revanced/patcher/util/MethodNavigator;
public final fun proxy (Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Lapp/revanced/patcher/util/proxy/ClassProxy;
}
public final class app/revanced/patcher/patch/Option {
public fun <init> (Ljava/lang/String;Ljava/lang/Object;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/reflect/KType;Lkotlin/jvm/functions/Function2;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/Object;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/reflect/KType;Lkotlin/jvm/functions/Function2;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Ljava/lang/String;Ljava/lang/Object;Ljava/util/Map;Ljava/lang/String;ZLkotlin/reflect/KType;Lkotlin/jvm/functions/Function2;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/Object;Ljava/util/Map;Ljava/lang/String;ZLkotlin/reflect/KType;Lkotlin/jvm/functions/Function2;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getDefault ()Ljava/lang/Object;
public final fun getDescription ()Ljava/lang/String;
public final fun getKey ()Ljava/lang/String;
public final fun getName ()Ljava/lang/String;
public final fun getRequired ()Z
public final fun getTitle ()Ljava/lang/String;
public final fun getType ()Lkotlin/reflect/KType;
public final fun getValidator ()Lkotlin/jvm/functions/Function2;
public final fun getValue ()Ljava/lang/Object;
public final fun getValue (Ljava/lang/Object;Lkotlin/reflect/KProperty;)Ljava/lang/Object;
public final fun getValues ()Ljava/util/Map;
public final fun reset ()V
public final fun setValue (Ljava/lang/Object;)V
public final fun setValue (Ljava/lang/Object;Lkotlin/reflect/KProperty;Ljava/lang/Object;)V
public fun toString ()Ljava/lang/String;
}
public abstract class app/revanced/patcher/patch/OptionException : java/lang/Exception {
public synthetic fun <init> (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
}
public final class app/revanced/patcher/patch/OptionException$InvalidValueTypeException : app/revanced/patcher/patch/OptionException {
public fun <init> (Ljava/lang/String;Ljava/lang/String;)V
}
public final class app/revanced/patcher/patch/OptionException$OptionNotFoundException : app/revanced/patcher/patch/OptionException {
public fun <init> (Ljava/lang/String;)V
}
public final class app/revanced/patcher/patch/OptionException$ValueRequiredException : app/revanced/patcher/patch/OptionException {
public fun <init> (Lapp/revanced/patcher/patch/Option;)V
}
public final class app/revanced/patcher/patch/OptionException$ValueValidationException : app/revanced/patcher/patch/OptionException {
public fun <init> (Ljava/lang/Object;Lapp/revanced/patcher/patch/Option;)V
}
public final class app/revanced/patcher/patch/OptionKt {
public static final fun booleanOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/Boolean;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public static final fun booleanOption (Ljava/lang/String;Ljava/lang/Boolean;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public static synthetic fun booleanOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/Boolean;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public static synthetic fun booleanOption$default (Ljava/lang/String;Ljava/lang/Boolean;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public static final fun booleansOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public static final fun booleansOption (Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public static synthetic fun booleansOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public static synthetic fun booleansOption$default (Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public static final fun floatOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/Float;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public static final fun floatOption (Ljava/lang/String;Ljava/lang/Float;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public static synthetic fun floatOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/Float;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public static synthetic fun floatOption$default (Ljava/lang/String;Ljava/lang/Float;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public static final fun floatsOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public static synthetic fun floatsOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public static final fun intOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/Integer;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public static final fun intOption (Ljava/lang/String;Ljava/lang/Integer;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public static synthetic fun intOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/Integer;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public static synthetic fun intOption$default (Ljava/lang/String;Ljava/lang/Integer;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public static final fun intsOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public static final fun intsOption (Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public static synthetic fun intsOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public static synthetic fun intsOption$default (Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public static final fun longOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/Long;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public static final fun longOption (Ljava/lang/String;Ljava/lang/Long;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public static synthetic fun longOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/Long;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public static synthetic fun longOption$default (Ljava/lang/String;Ljava/lang/Long;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public static final fun longsOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public static final fun longsOption (Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public static synthetic fun longsOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public static synthetic fun longsOption$default (Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public static final fun stringOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public static final fun stringOption (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public static synthetic fun stringOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public static synthetic fun stringOption$default (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public static final fun stringsOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public static final fun stringsOption (Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public static synthetic fun stringsOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public static synthetic fun stringsOption$default (Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
}
public final class app/revanced/patcher/patch/Options : java/util/Map, kotlin/jvm/internal/markers/KMappedMarker {
public fun clear ()V
public synthetic fun compute (Ljava/lang/Object;Ljava/util/function/BiFunction;)Ljava/lang/Object;
public fun compute (Ljava/lang/String;Ljava/util/function/BiFunction;)Lapp/revanced/patcher/patch/Option;
public synthetic fun computeIfAbsent (Ljava/lang/Object;Ljava/util/function/Function;)Ljava/lang/Object;
public fun computeIfAbsent (Ljava/lang/String;Ljava/util/function/Function;)Lapp/revanced/patcher/patch/Option;
public synthetic fun computeIfPresent (Ljava/lang/Object;Ljava/util/function/BiFunction;)Ljava/lang/Object;
public fun computeIfPresent (Ljava/lang/String;Ljava/util/function/BiFunction;)Lapp/revanced/patcher/patch/Option;
public final fun containsKey (Ljava/lang/Object;)Z
public fun containsKey (Ljava/lang/String;)Z
public fun containsValue (Lapp/revanced/patcher/patch/Option;)Z
public final fun containsValue (Ljava/lang/Object;)Z
public final fun entrySet ()Ljava/util/Set;
public final fun get (Ljava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public final synthetic fun get (Ljava/lang/Object;)Ljava/lang/Object;
public fun get (Ljava/lang/String;)Lapp/revanced/patcher/patch/Option;
public fun getEntries ()Ljava/util/Set;
public fun getKeys ()Ljava/util/Set;
public fun getSize ()I
public fun getValues ()Ljava/util/Collection;
public fun isEmpty ()Z
public final fun keySet ()Ljava/util/Set;
public synthetic fun merge (Ljava/lang/Object;Ljava/lang/Object;Ljava/util/function/BiFunction;)Ljava/lang/Object;
public fun merge (Ljava/lang/String;Lapp/revanced/patcher/patch/Option;Ljava/util/function/BiFunction;)Lapp/revanced/patcher/patch/Option;
public synthetic fun put (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
public fun put (Ljava/lang/String;Lapp/revanced/patcher/patch/Option;)Lapp/revanced/patcher/patch/Option;
public fun putAll (Ljava/util/Map;)V
public synthetic fun putIfAbsent (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
public fun putIfAbsent (Ljava/lang/String;Lapp/revanced/patcher/patch/Option;)Lapp/revanced/patcher/patch/Option;
public fun remove (Ljava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public synthetic fun remove (Ljava/lang/Object;)Ljava/lang/Object;
public fun remove (Ljava/lang/Object;Ljava/lang/Object;)Z
public synthetic fun replace (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
public synthetic fun replace (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Z
public fun replace (Ljava/lang/String;Lapp/revanced/patcher/patch/Option;)Lapp/revanced/patcher/patch/Option;
public fun replace (Ljava/lang/String;Lapp/revanced/patcher/patch/Option;Lapp/revanced/patcher/patch/Option;)Z
public fun replaceAll (Ljava/util/function/BiFunction;)V
public final fun set (Ljava/lang/String;Ljava/lang/Object;)V
public final fun size ()I
public final fun values ()Ljava/util/Collection;
}
public abstract class app/revanced/patcher/patch/Patch {
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;ZLjava/util/Set;Ljava/util/Set;Ljava/util/Set;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun execute (Lapp/revanced/patcher/patch/PatchContext;)V
public final fun finalize (Lapp/revanced/patcher/patch/PatchContext;)V
public final fun getCompatiblePackages ()Ljava/util/Set;
public final fun getDependencies ()Ljava/util/Set;
public final fun getDescription ()Ljava/lang/String;
public final fun getName ()Ljava/lang/String;
public final fun getOptions ()Lapp/revanced/patcher/patch/Options;
public final fun getUse ()Z
public fun toString ()Ljava/lang/String;
}
public abstract class app/revanced/patcher/patch/PatchBuilder {
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun compatibleWith ([Ljava/lang/String;)V
public final fun compatibleWith ([Lkotlin/Pair;)V
public final fun dependsOn ([Lapp/revanced/patcher/patch/Patch;)V
public final fun execute (Lkotlin/jvm/functions/Function1;)V
public final fun finalize (Lkotlin/jvm/functions/Function1;)V
protected final fun getCompatiblePackages ()Ljava/util/Set;
protected final fun getDependencies ()Ljava/util/Set;
protected final fun getDescription ()Ljava/lang/String;
protected final fun getExecutionBlock ()Lkotlin/jvm/functions/Function1;
protected final fun getFinalizeBlock ()Lkotlin/jvm/functions/Function1;
protected final fun getName ()Ljava/lang/String;
protected final fun getOptions ()Ljava/util/Set;
protected final fun getUse ()Z
public final fun invoke (Lapp/revanced/patcher/patch/Option;)Lapp/revanced/patcher/patch/Option;
public final fun invoke (Ljava/lang/String;[Ljava/lang/String;)Lkotlin/Pair;
protected final fun setCompatiblePackages (Ljava/util/Set;)V
protected final fun setDependencies (Ljava/util/Set;)V
protected final fun setExecutionBlock (Lkotlin/jvm/functions/Function1;)V
protected final fun setFinalizeBlock (Lkotlin/jvm/functions/Function1;)V
}
public abstract interface class app/revanced/patcher/patch/PatchContext : java/util/function/Supplier {
}
public final class app/revanced/patcher/patch/PatchException : java/lang/Exception {
public fun <init> (Ljava/lang/String;)V
public fun <init> (Ljava/lang/String;Ljava/lang/Throwable;)V
public fun <init> (Ljava/lang/Throwable;)V
}
public final class app/revanced/patcher/patch/PatchKt {
public static final fun bytecodePatch (Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/BytecodePatch;
public static synthetic fun bytecodePatch$default (Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/BytecodePatch;
public static final fun loadPatchesFromDex (Ljava/util/Set;Ljava/io/File;)Lapp/revanced/patcher/patch/PatchLoader$Dex;
public static synthetic fun loadPatchesFromDex$default (Ljava/util/Set;Ljava/io/File;ILjava/lang/Object;)Lapp/revanced/patcher/patch/PatchLoader$Dex;
public static final fun loadPatchesFromJar (Ljava/util/Set;)Lapp/revanced/patcher/patch/PatchLoader$Jar;
public static final fun rawResourcePatch (Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/RawResourcePatch;
public static synthetic fun rawResourcePatch$default (Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/RawResourcePatch;
public static final fun resourcePatch (Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/ResourcePatch;
public static synthetic fun resourcePatch$default (Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/ResourcePatch;
}
public abstract class app/revanced/patcher/patch/PatchLoader : java/util/Set, kotlin/jvm/internal/markers/KMappedMarker {
public synthetic fun <init> (Ljava/util/Map;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (Ljava/util/Set;Lkotlin/jvm/functions/Function1;Ljava/lang/ClassLoader;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun add (Lapp/revanced/patcher/patch/Patch;)Z
public synthetic fun add (Ljava/lang/Object;)Z
public fun addAll (Ljava/util/Collection;)Z
public fun clear ()V
public fun contains (Lapp/revanced/patcher/patch/Patch;)Z
public final fun contains (Ljava/lang/Object;)Z
public fun containsAll (Ljava/util/Collection;)Z
public final fun getByPatchesFile ()Ljava/util/Map;
public fun getSize ()I
public fun isEmpty ()Z
public fun iterator ()Ljava/util/Iterator;
public fun remove (Ljava/lang/Object;)Z
public fun removeAll (Ljava/util/Collection;)Z
public fun retainAll (Ljava/util/Collection;)Z
public final fun size ()I
public fun toArray ()[Ljava/lang/Object;
public fun toArray ([Ljava/lang/Object;)[Ljava/lang/Object;
}
public final class app/revanced/patcher/patch/PatchLoader$Dex : app/revanced/patcher/patch/PatchLoader {
public fun <init> (Ljava/util/Set;Ljava/io/File;)V
public synthetic fun <init> (Ljava/util/Set;Ljava/io/File;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
}
public final class app/revanced/patcher/patch/PatchLoader$Jar : app/revanced/patcher/patch/PatchLoader {
public fun <init> (Ljava/util/Set;)V
}
public final class app/revanced/patcher/patch/PatchResult {
public final fun getException ()Lapp/revanced/patcher/patch/PatchException;
public final fun getPatch ()Lapp/revanced/patcher/patch/Patch;
}
public final class app/revanced/patcher/patch/RawResourcePatch : app/revanced/patcher/patch/Patch {
public fun toString ()Ljava/lang/String;
}
public final class app/revanced/patcher/patch/RawResourcePatchBuilder : app/revanced/patcher/patch/PatchBuilder {
public synthetic fun build$revanced_patcher ()Lapp/revanced/patcher/patch/Patch;
}
public final class app/revanced/patcher/patch/ResourcePatch : app/revanced/patcher/patch/Patch {
public fun toString ()Ljava/lang/String;
}
public final class app/revanced/patcher/patch/ResourcePatchBuilder : app/revanced/patcher/patch/PatchBuilder {
public synthetic fun build$revanced_patcher ()Lapp/revanced/patcher/patch/Patch;
}
public final class app/revanced/patcher/patch/ResourcePatchContext : app/revanced/patcher/patch/PatchContext {
public final fun delete (Ljava/lang/String;)Z
public final fun document (Ljava/io/InputStream;)Lapp/revanced/patcher/util/Document;
public final fun document (Ljava/lang/String;)Lapp/revanced/patcher/util/Document;
public fun get ()Lapp/revanced/patcher/PatcherResult$PatchedResources;
public synthetic fun get ()Ljava/lang/Object;
public final fun get (Ljava/lang/String;Z)Ljava/io/File;
public static synthetic fun get$default (Lapp/revanced/patcher/patch/ResourcePatchContext;Ljava/lang/String;ZILjava/lang/Object;)Ljava/io/File;
}
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/MethodNavigator {
public final fun getValue (Ljava/lang/Void;Lkotlin/reflect/KProperty;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;
public final fun original ()Lcom/android/tools/smali/dexlib2/iface/Method;
public final fun stop ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;
public final fun to (ILkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/util/MethodNavigator;
public final fun to ([I)Lapp/revanced/patcher/util/MethodNavigator;
public static synthetic fun to$default (Lapp/revanced/patcher/util/MethodNavigator;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/util/MethodNavigator;
}
public final class app/revanced/patcher/util/ProxyClassList : java/util/List, kotlin/jvm/internal/markers/KMutableList {
public fun add (ILcom/android/tools/smali/dexlib2/iface/ClassDef;)V
public synthetic fun add (ILjava/lang/Object;)V
public fun add (Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Z
public synthetic fun add (Ljava/lang/Object;)Z
public fun addAll (ILjava/util/Collection;)Z
public fun addAll (Ljava/util/Collection;)Z
public fun clear ()V
public fun contains (Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Z
public final fun contains (Ljava/lang/Object;)Z
public fun containsAll (Ljava/util/Collection;)Z
public fun get (I)Lcom/android/tools/smali/dexlib2/iface/ClassDef;
public synthetic fun get (I)Ljava/lang/Object;
public fun getSize ()I
public fun indexOf (Lcom/android/tools/smali/dexlib2/iface/ClassDef;)I
public final fun indexOf (Ljava/lang/Object;)I
public fun isEmpty ()Z
public fun iterator ()Ljava/util/Iterator;
public fun lastIndexOf (Lcom/android/tools/smali/dexlib2/iface/ClassDef;)I
public final fun lastIndexOf (Ljava/lang/Object;)I
public fun listIterator ()Ljava/util/ListIterator;
public fun listIterator (I)Ljava/util/ListIterator;
public final fun remove (I)Lcom/android/tools/smali/dexlib2/iface/ClassDef;
public synthetic fun remove (I)Ljava/lang/Object;
public fun remove (Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Z
public final fun remove (Ljava/lang/Object;)Z
public fun removeAll (Ljava/util/Collection;)Z
public fun removeAt (I)Lcom/android/tools/smali/dexlib2/iface/ClassDef;
public fun retainAll (Ljava/util/Collection;)Z
public fun set (ILcom/android/tools/smali/dexlib2/iface/ClassDef;)Lcom/android/tools/smali/dexlib2/iface/ClassDef;
public synthetic fun set (ILjava/lang/Object;)Ljava/lang/Object;
public final fun size ()I
public fun subList (II)Ljava/util/List;
public fun toArray ()[Ljava/lang/Object;
public fun toArray ([Ljava/lang/Object;)[Ljava/lang/Object;
}
public final class app/revanced/patcher/util/proxy/ClassProxy {
public final fun getImmutableClass ()Lcom/android/tools/smali/dexlib2/iface/ClassDef;
public final fun getMutableClass ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;
}
public final class app/revanced/patcher/util/proxy/mutableTypes/MutableAnnotation : com/android/tools/smali/dexlib2/base/BaseAnnotation {
public static final field Companion Lapp/revanced/patcher/util/proxy/mutableTypes/MutableAnnotation$Companion;
public fun <init> (Lcom/android/tools/smali/dexlib2/iface/Annotation;)V
public fun getElements ()Ljava/util/Set;
public fun getType ()Ljava/lang/String;
public fun getVisibility ()I
}
public final class app/revanced/patcher/util/proxy/mutableTypes/MutableAnnotation$Companion {
public final fun toMutable (Lcom/android/tools/smali/dexlib2/iface/Annotation;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableAnnotation;
}
public final class app/revanced/patcher/util/proxy/mutableTypes/MutableAnnotationElement : com/android/tools/smali/dexlib2/base/BaseAnnotationElement {
public static final field Companion Lapp/revanced/patcher/util/proxy/mutableTypes/MutableAnnotationElement$Companion;
public fun <init> (Lcom/android/tools/smali/dexlib2/iface/AnnotationElement;)V
public fun getName ()Ljava/lang/String;
public fun getValue ()Lcom/android/tools/smali/dexlib2/iface/value/EncodedValue;
public final fun setName (Ljava/lang/String;)V
public final fun setValue (Lapp/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableEncodedValue;)V
}
public final class app/revanced/patcher/util/proxy/mutableTypes/MutableAnnotationElement$Companion {
public final fun toMutable (Lcom/android/tools/smali/dexlib2/iface/AnnotationElement;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableAnnotationElement;
}
public final class app/revanced/patcher/util/proxy/mutableTypes/MutableClass : com/android/tools/smali/dexlib2/base/reference/BaseTypeReference, com/android/tools/smali/dexlib2/iface/ClassDef {
public static final field Companion Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass$Companion;
public fun <init> (Lcom/android/tools/smali/dexlib2/iface/ClassDef;)V
public final fun charAt (I)C
public fun get (I)C
public fun getAccessFlags ()I
public fun getAnnotations ()Ljava/util/Set;
public synthetic fun getDirectMethods ()Ljava/lang/Iterable;
public fun getDirectMethods ()Ljava/util/Set;
public synthetic fun getFields ()Ljava/lang/Iterable;
public fun getFields ()Ljava/util/Set;
public synthetic fun getInstanceFields ()Ljava/lang/Iterable;
public fun getInstanceFields ()Ljava/util/Set;
public fun getInterfaces ()Ljava/util/List;
public fun getLength ()I
public synthetic fun getMethods ()Ljava/lang/Iterable;
public fun getMethods ()Ljava/util/Set;
public fun getSourceFile ()Ljava/lang/String;
public synthetic fun getStaticFields ()Ljava/lang/Iterable;
public fun getStaticFields ()Ljava/util/Set;
public fun getSuperclass ()Ljava/lang/String;
public fun getType ()Ljava/lang/String;
public synthetic fun getVirtualMethods ()Ljava/lang/Iterable;
public fun getVirtualMethods ()Ljava/util/Set;
public final fun length ()I
public final fun setAccessFlags (I)V
public final fun setSourceFile (Ljava/lang/String;)V
public final fun setSuperClass (Ljava/lang/String;)V
public final fun setType (Ljava/lang/String;)V
}
public final class app/revanced/patcher/util/proxy/mutableTypes/MutableClass$Companion {
public final fun toMutable (Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;
}
public final class app/revanced/patcher/util/proxy/mutableTypes/MutableField : com/android/tools/smali/dexlib2/base/reference/BaseFieldReference, com/android/tools/smali/dexlib2/iface/Field {
public static final field Companion Lapp/revanced/patcher/util/proxy/mutableTypes/MutableField$Companion;
public fun <init> (Lcom/android/tools/smali/dexlib2/iface/Field;)V
public fun getAccessFlags ()I
public fun getAnnotations ()Ljava/util/Set;
public fun getDefiningClass ()Ljava/lang/String;
public fun getHiddenApiRestrictions ()Ljava/util/Set;
public fun getInitialValue ()Lapp/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableEncodedValue;
public synthetic fun getInitialValue ()Lcom/android/tools/smali/dexlib2/iface/value/EncodedValue;
public fun getName ()Ljava/lang/String;
public fun getType ()Ljava/lang/String;
public final fun setAccessFlags (I)V
public final fun setDefiningClass (Ljava/lang/String;)V
public final fun setInitialValue (Lapp/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableEncodedValue;)V
public final fun setName (Ljava/lang/String;)V
public final fun setType (Ljava/lang/String;)V
}
public final class app/revanced/patcher/util/proxy/mutableTypes/MutableField$Companion {
public final fun toMutable (Lcom/android/tools/smali/dexlib2/iface/Field;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableField;
}
public final class app/revanced/patcher/util/proxy/mutableTypes/MutableMethod : com/android/tools/smali/dexlib2/base/reference/BaseMethodReference, com/android/tools/smali/dexlib2/iface/Method {
public static final field Companion Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod$Companion;
public fun <init> (Lcom/android/tools/smali/dexlib2/iface/Method;)V
public fun getAccessFlags ()I
public fun getAnnotations ()Ljava/util/Set;
public fun getDefiningClass ()Ljava/lang/String;
public fun getHiddenApiRestrictions ()Ljava/util/Set;
public fun getImplementation ()Lcom/android/tools/smali/dexlib2/builder/MutableMethodImplementation;
public synthetic fun getImplementation ()Lcom/android/tools/smali/dexlib2/iface/MethodImplementation;
public fun getName ()Ljava/lang/String;
public fun getParameterTypes ()Ljava/util/List;
public fun getParameters ()Ljava/util/List;
public fun getReturnType ()Ljava/lang/String;
public final fun setAccessFlags (I)V
public final fun setDefiningClass (Ljava/lang/String;)V
public final fun setName (Ljava/lang/String;)V
public final fun setReturnType (Ljava/lang/String;)V
}
public final class app/revanced/patcher/util/proxy/mutableTypes/MutableMethod$Companion {
public final fun toMutable (Lcom/android/tools/smali/dexlib2/iface/Method;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;
}
public final class app/revanced/patcher/util/proxy/mutableTypes/MutableMethodParameter : com/android/tools/smali/dexlib2/base/BaseMethodParameter, com/android/tools/smali/dexlib2/iface/MethodParameter {
public static final field Companion Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethodParameter$Companion;
public fun <init> (Lcom/android/tools/smali/dexlib2/iface/MethodParameter;)V
public final fun charAt (I)C
public fun get (I)C
public fun getAnnotations ()Ljava/util/Set;
public fun getLength ()I
public fun getName ()Ljava/lang/String;
public fun getSignature ()Ljava/lang/String;
public fun getType ()Ljava/lang/String;
public final fun length ()I
}
public final class app/revanced/patcher/util/proxy/mutableTypes/MutableMethodParameter$Companion {
public final fun toMutable (Lcom/android/tools/smali/dexlib2/iface/MethodParameter;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethodParameter;
}
public final class app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableAnnotationEncodedValue : com/android/tools/smali/dexlib2/base/value/BaseAnnotationEncodedValue, app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableEncodedValue {
public static final field Companion Lapp/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableAnnotationEncodedValue$Companion;
public fun <init> (Lcom/android/tools/smali/dexlib2/iface/value/AnnotationEncodedValue;)V
public fun getElements ()Ljava/util/Set;
public fun getType ()Ljava/lang/String;
public final fun setType (Ljava/lang/String;)V
}
public final class app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableAnnotationEncodedValue$Companion {
public final fun toMutable (Lcom/android/tools/smali/dexlib2/iface/value/AnnotationEncodedValue;)Lapp/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableAnnotationEncodedValue;
}
public final class app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableArrayEncodedValue : com/android/tools/smali/dexlib2/base/value/BaseArrayEncodedValue, app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableEncodedValue {
public static final field Companion Lapp/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableArrayEncodedValue$Companion;
public fun <init> (Lcom/android/tools/smali/dexlib2/iface/value/ArrayEncodedValue;)V
public fun getValue ()Ljava/util/List;
}
public final class app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableArrayEncodedValue$Companion {
public final fun toMutable (Lcom/android/tools/smali/dexlib2/iface/value/ArrayEncodedValue;)Lapp/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableArrayEncodedValue;
}
public final class app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableBooleanEncodedValue : com/android/tools/smali/dexlib2/base/value/BaseBooleanEncodedValue, app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableEncodedValue {
public static final field Companion Lapp/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableBooleanEncodedValue$Companion;
public fun <init> (Lcom/android/tools/smali/dexlib2/iface/value/BooleanEncodedValue;)V
public fun getValue ()Z
public final fun setValue (Z)V
}
public final class app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableBooleanEncodedValue$Companion {
public final fun toMutable (Lcom/android/tools/smali/dexlib2/iface/value/BooleanEncodedValue;)Lapp/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableBooleanEncodedValue;
}
public final class app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableByteEncodedValue : com/android/tools/smali/dexlib2/base/value/BaseByteEncodedValue, app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableEncodedValue {
public static final field Companion Lapp/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableByteEncodedValue$Companion;
public fun <init> (Lcom/android/tools/smali/dexlib2/iface/value/ByteEncodedValue;)V
public fun getValue ()B
public final fun setValue (B)V
}
public final class app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableByteEncodedValue$Companion {
public final fun toMutable (Lcom/android/tools/smali/dexlib2/iface/value/ByteEncodedValue;)Lapp/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableByteEncodedValue;
}
public final class app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableCharEncodedValue : com/android/tools/smali/dexlib2/base/value/BaseCharEncodedValue, app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableEncodedValue {
public static final field Companion Lapp/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableCharEncodedValue$Companion;
public fun <init> (Lcom/android/tools/smali/dexlib2/iface/value/CharEncodedValue;)V
public fun getValue ()C
public final fun setValue (C)V
}
public final class app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableCharEncodedValue$Companion {
public final fun toMutable (Lcom/android/tools/smali/dexlib2/iface/value/CharEncodedValue;)Lapp/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableCharEncodedValue;
}
public final class app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableDoubleEncodedValue : com/android/tools/smali/dexlib2/base/value/BaseDoubleEncodedValue, app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableEncodedValue {
public static final field Companion Lapp/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableDoubleEncodedValue$Companion;
public fun <init> (Lcom/android/tools/smali/dexlib2/iface/value/DoubleEncodedValue;)V
public fun getValue ()D
public final fun setValue (D)V
}
public final class app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableDoubleEncodedValue$Companion {
public final fun toMutable (Lcom/android/tools/smali/dexlib2/iface/value/DoubleEncodedValue;)Lapp/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableDoubleEncodedValue;
}
public abstract interface class app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableEncodedValue : com/android/tools/smali/dexlib2/iface/value/EncodedValue {
public static final field Companion Lapp/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableEncodedValue$Companion;
}
public final class app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableEncodedValue$Companion {
public final fun toMutable (Lcom/android/tools/smali/dexlib2/iface/value/EncodedValue;)Lapp/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableEncodedValue;
}
public final class app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableEnumEncodedValue : com/android/tools/smali/dexlib2/base/value/BaseEnumEncodedValue, app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableEncodedValue {
public static final field Companion Lapp/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableEnumEncodedValue$Companion;
public fun <init> (Lcom/android/tools/smali/dexlib2/iface/value/EnumEncodedValue;)V
public fun getValue ()Lcom/android/tools/smali/dexlib2/iface/reference/FieldReference;
public final fun setValue (Lcom/android/tools/smali/dexlib2/iface/reference/FieldReference;)V
}
public final class app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableEnumEncodedValue$Companion {
public final fun toMutable (Lcom/android/tools/smali/dexlib2/iface/value/EnumEncodedValue;)Lapp/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableEnumEncodedValue;
}
public final class app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableFieldEncodedValue : com/android/tools/smali/dexlib2/base/value/BaseFieldEncodedValue, app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableEncodedValue {
public static final field Companion Lapp/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableFieldEncodedValue$Companion;
public fun <init> (Lcom/android/tools/smali/dexlib2/iface/value/FieldEncodedValue;)V
public fun getValue ()Lcom/android/tools/smali/dexlib2/iface/reference/FieldReference;
public fun getValueType ()I
public final fun setValue (Lcom/android/tools/smali/dexlib2/iface/reference/FieldReference;)V
}
public final class app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableFieldEncodedValue$Companion {
public final fun toMutable (Lcom/android/tools/smali/dexlib2/iface/value/FieldEncodedValue;)Lapp/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableFieldEncodedValue;
}
public final class app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableFloatEncodedValue : com/android/tools/smali/dexlib2/base/value/BaseFloatEncodedValue, app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableEncodedValue {
public static final field Companion Lapp/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableFloatEncodedValue$Companion;
public fun <init> (Lcom/android/tools/smali/dexlib2/iface/value/FloatEncodedValue;)V
public fun getValue ()F
public final fun setValue (F)V
}
public final class app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableFloatEncodedValue$Companion {
public final fun toMutable (Lcom/android/tools/smali/dexlib2/iface/value/FloatEncodedValue;)Lapp/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableFloatEncodedValue;
}
public final class app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableIntEncodedValue : com/android/tools/smali/dexlib2/base/value/BaseIntEncodedValue, app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableEncodedValue {
public static final field Companion Lapp/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableIntEncodedValue$Companion;
public fun <init> (Lcom/android/tools/smali/dexlib2/iface/value/IntEncodedValue;)V
public fun getValue ()I
public final fun setValue (I)V
}
public final class app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableIntEncodedValue$Companion {
public final fun toMutable (Lcom/android/tools/smali/dexlib2/iface/value/IntEncodedValue;)Lapp/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableIntEncodedValue;
}
public final class app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableLongEncodedValue : com/android/tools/smali/dexlib2/base/value/BaseLongEncodedValue, app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableEncodedValue {
public static final field Companion Lapp/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableLongEncodedValue$Companion;
public fun <init> (Lcom/android/tools/smali/dexlib2/iface/value/LongEncodedValue;)V
public fun getValue ()J
public final fun setValue (J)V
}
public final class app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableLongEncodedValue$Companion {
public final fun toMutable (Lcom/android/tools/smali/dexlib2/iface/value/LongEncodedValue;)Lapp/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableLongEncodedValue;
}
public final class app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableMethodEncodedValue : com/android/tools/smali/dexlib2/base/value/BaseMethodEncodedValue, app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableEncodedValue {
public static final field Companion Lapp/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableMethodEncodedValue$Companion;
public fun <init> (Lcom/android/tools/smali/dexlib2/iface/value/MethodEncodedValue;)V
public fun getValue ()Lcom/android/tools/smali/dexlib2/iface/reference/MethodReference;
public final fun setValue (Lcom/android/tools/smali/dexlib2/iface/reference/MethodReference;)V
}
public final class app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableMethodEncodedValue$Companion {
public final fun toMutable (Lcom/android/tools/smali/dexlib2/iface/value/MethodEncodedValue;)Lapp/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableMethodEncodedValue;
}
public final class app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableMethodHandleEncodedValue : com/android/tools/smali/dexlib2/base/value/BaseMethodHandleEncodedValue, app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableEncodedValue {
public static final field Companion Lapp/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableMethodHandleEncodedValue$Companion;
public fun <init> (Lcom/android/tools/smali/dexlib2/iface/value/MethodHandleEncodedValue;)V
public fun getValue ()Lcom/android/tools/smali/dexlib2/iface/reference/MethodHandleReference;
public final fun setValue (Lcom/android/tools/smali/dexlib2/iface/reference/MethodHandleReference;)V
}
public final class app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableMethodHandleEncodedValue$Companion {
public final fun toMutable (Lcom/android/tools/smali/dexlib2/iface/value/MethodHandleEncodedValue;)Lapp/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableMethodHandleEncodedValue;
}
public final class app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableMethodTypeEncodedValue : com/android/tools/smali/dexlib2/base/value/BaseMethodTypeEncodedValue, app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableEncodedValue {
public static final field Companion Lapp/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableMethodTypeEncodedValue$Companion;
public fun <init> (Lcom/android/tools/smali/dexlib2/iface/value/MethodTypeEncodedValue;)V
public fun getValue ()Lcom/android/tools/smali/dexlib2/iface/reference/MethodProtoReference;
public final fun setValue (Lcom/android/tools/smali/dexlib2/iface/reference/MethodProtoReference;)V
}
public final class app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableMethodTypeEncodedValue$Companion {
public final fun toMutable (Lcom/android/tools/smali/dexlib2/iface/value/MethodTypeEncodedValue;)Lapp/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableMethodTypeEncodedValue;
}
public final class app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableNullEncodedValue : com/android/tools/smali/dexlib2/base/value/BaseNullEncodedValue, app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableEncodedValue {
public static final field Companion Lapp/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableNullEncodedValue$Companion;
public fun <init> ()V
}
public final class app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableNullEncodedValue$Companion {
public final fun toMutable (Lcom/android/tools/smali/dexlib2/iface/value/ByteEncodedValue;)Lapp/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableByteEncodedValue;
}
public final class app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableShortEncodedValue : com/android/tools/smali/dexlib2/base/value/BaseShortEncodedValue, app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableEncodedValue {
public static final field Companion Lapp/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableShortEncodedValue$Companion;
public fun <init> (Lcom/android/tools/smali/dexlib2/iface/value/ShortEncodedValue;)V
public fun getValue ()S
public final fun setValue (S)V
}
public final class app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableShortEncodedValue$Companion {
public final fun toMutable (Lcom/android/tools/smali/dexlib2/iface/value/ShortEncodedValue;)Lapp/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableShortEncodedValue;
}
public final class app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableStringEncodedValue : com/android/tools/smali/dexlib2/base/value/BaseStringEncodedValue, app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableEncodedValue {
public static final field Companion Lapp/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableStringEncodedValue$Companion;
public fun <init> (Lcom/android/tools/smali/dexlib2/iface/value/StringEncodedValue;)V
public fun getValue ()Ljava/lang/String;
public final fun setValue (Ljava/lang/String;)V
}
public final class app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableStringEncodedValue$Companion {
public final fun toMutable (Lcom/android/tools/smali/dexlib2/iface/value/ByteEncodedValue;)Lapp/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableByteEncodedValue;
}
public final class app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableTypeEncodedValue : com/android/tools/smali/dexlib2/base/value/BaseTypeEncodedValue, app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableEncodedValue {
public static final field Companion Lapp/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableTypeEncodedValue$Companion;
public fun <init> (Lcom/android/tools/smali/dexlib2/iface/value/TypeEncodedValue;)V
public fun getValue ()Ljava/lang/String;
public final fun setValue (Ljava/lang/String;)V
}
public final class app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableTypeEncodedValue$Companion {
public final fun toMutable (Lcom/android/tools/smali/dexlib2/iface/value/TypeEncodedValue;)Lapp/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableTypeEncodedValue;
}
public final class app/revanced/patcher/util/smali/ExternalLabel {
public fun <init> (Ljava/lang/String;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;)V
public final fun copy (Ljava/lang/String;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;)Lapp/revanced/patcher/util/smali/ExternalLabel;
public static synthetic fun copy$default (Lapp/revanced/patcher/util/smali/ExternalLabel;Ljava/lang/String;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;ILjava/lang/Object;)Lapp/revanced/patcher/util/smali/ExternalLabel;
public fun equals (Ljava/lang/Object;)Z
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
public final class app/revanced/patcher/util/smali/InlineSmaliCompiler {
public static final field Companion Lapp/revanced/patcher/util/smali/InlineSmaliCompiler$Companion;
public fun <init> ()V
}
public final class app/revanced/patcher/util/smali/InlineSmaliCompiler$Companion {
public final fun compile (Ljava/lang/String;Ljava/lang/String;IZ)Ljava/util/List;
}
public final class app/revanced/patcher/util/smali/InlineSmaliCompilerKt {
public static final fun toInstruction (Ljava/lang/String;Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;)Lcom/android/tools/smali/dexlib2/builder/BuilderInstruction;
public static synthetic fun toInstruction$default (Ljava/lang/String;Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;ILjava/lang/Object;)Lcom/android/tools/smali/dexlib2/builder/BuilderInstruction;
public static final fun toInstructions (Ljava/lang/String;Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;)Ljava/util/List;
public static synthetic fun toInstructions$default (Ljava/lang/String;Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;ILjava/lang/Object;)Ljava/util/List;
}

View File

@@ -1,119 +1,5 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.kotlin)
alias(libs.plugins.binary.compatibility.validator)
`maven-publish`
signing
}
group = "app.revanced"
tasks {
processResources {
expand("projectVersion" to project.version)
}
test {
useJUnitPlatform()
testLogging {
events("PASSED", "SKIPPED", "FAILED")
}
}
}
repositories {
mavenCentral()
google()
maven {
// A repository must be specified for some reason. "registry" is a dummy.
url = uri("https://maven.pkg.github.com/revanced/registry")
credentials {
username = project.findProperty("gpr.user") as String? ?: System.getenv("GITHUB_ACTOR")
password = project.findProperty("gpr.key") as String? ?: System.getenv("GITHUB_TOKEN")
}
}
}
dependencies {
// TODO: Convert project to KMP.
compileOnly(libs.android) {
// Exclude, otherwise the org.w3c.dom API breaks.
exclude(group = "xerces", module = "xmlParserAPIs")
}
implementation(libs.apktool.lib)
implementation(libs.kotlin.reflect)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.multidexlib2)
implementation(libs.smali)
implementation(libs.xpp3)
testImplementation(libs.mockk)
testImplementation(libs.kotlin.test)
}
kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
freeCompilerArgs = listOf("-Xcontext-receivers")
}
}
java {
targetCompatibility = JavaVersion.VERSION_11
withSourcesJar()
}
publishing {
repositories {
maven {
name = "GitHubPackages"
url = uri("https://maven.pkg.github.com/revanced/revanced-patcher")
credentials {
username = System.getenv("GITHUB_ACTOR")
password = System.getenv("GITHUB_TOKEN")
}
}
}
publications {
create<MavenPublication>("revanced-patcher-publication") {
from(components["java"])
version = project.version.toString()
pom {
name = "ReVanced Patcher"
description = "Patcher used by ReVanced."
url = "https://revanced.app"
licenses {
license {
name = "GNU General Public License v3.0"
url = "https://www.gnu.org/licenses/gpl-3.0.en.html"
}
}
developers {
developer {
id = "ReVanced"
name = "ReVanced"
email = "contact@revanced.app"
}
}
scm {
connection = "scm:git:git://github.com/revanced/revanced-patcher.git"
developerConnection = "scm:git:git@github.com:revanced/revanced-patcher.git"
url = "https://github.com/revanced/revanced-patcher"
}
}
}
}
}
signing {
useGpgCmd()
sign(publishing.publications["revanced-patcher-publication"])
}
alias(libs.plugins.android.kotlin.multiplatform.library) apply false
alias(libs.plugins.kotlinMultiplatform) apply false
alias(libs.plugins.vanniktech.mavenPublish) apply false
}

View File

@@ -1,3 +1,11 @@
org.gradle.parallel = true
org.gradle.caching = true
version = 21.1.0-dev.5
version = 22.0.0-local
#Kotlin
kotlin.code.style=official
kotlin.daemon.jvmargs=-Xmx3072M
#Gradle
org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M"
org.gradle.caching=true
org.gradle.configuration-cache=true
#Android
android.nonTransitiveRClass=true
android.useAndroidX=true

View File

@@ -1,21 +1,21 @@
[versions]
android = "4.1.1.4"
agp = "8.12.3"
android-compileSdk = "36"
android-minSdk = "26"
kotlin = "2.3.0"
apktool-lib = "2.10.1.1"
binary-compatibility-validator = "0.18.1"
kotlin = "2.0.20"
kotlinx-coroutines-core = "1.10.2"
mockk = "1.14.5"
mockk = "1.14.7"
multidexlib2 = "3.0.3.r3"
# Tracking https://github.com/google/smali/issues/64.
#noinspection GradleDependency
smali = "3.0.9"
xpp3 = "1.1.4c"
vanniktechMavenPublish = "0.35.0"
[libraries]
android = { module = "com.google.android:android", version.ref = "android" }
apktool-lib = { module = "app.revanced:apktool-lib", version.ref = "apktool-lib" }
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines-core" }
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
multidexlib2 = { module = "app.revanced:multidexlib2", version.ref = "multidexlib2" }
@@ -23,5 +23,6 @@ smali = { module = "com.android.tools.smali:smali", version.ref = "smali" }
xpp3 = { module = "xpp3:xpp3", version.ref = "xpp3" }
[plugins]
binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" }
kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
android-kotlin-multiplatform-library = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" }
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
vanniktech-mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "vanniktechMavenPublish" }

Binary file not shown.

View File

@@ -1,7 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

12
gradlew vendored
View File

@@ -1,7 +1,7 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -86,8 +86,7 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -115,7 +114,6 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
@@ -173,7 +171,6 @@ fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
@@ -206,15 +203,14 @@ fi
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.

3
gradlew.bat vendored
View File

@@ -70,11 +70,10 @@ goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell

1108
patcher/api/android/core.api Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1108
patcher/api/jvm/core.api Normal file

File diff suppressed because it is too large Load Diff

1218
patcher/api/jvm/patcher.api Normal file

File diff suppressed because it is too large Load Diff

110
patcher/build.gradle.kts Normal file
View File

@@ -0,0 +1,110 @@
import com.android.build.api.dsl.androidLibrary
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.android.kotlin.multiplatform.library)
alias(libs.plugins.vanniktech.mavenPublish)
}
group = "app.revanced"
kotlin {
@OptIn(ExperimentalAbiValidation::class)
abiValidation {
enabled = true
}
jvm()
androidLibrary {
namespace = "app.revanced.patcher"
compileSdk = libs.versions.android.compileSdk.get().toInt()
minSdk = libs.versions.android.minSdk.get().toInt()
withHostTestBuilder {}.configure {}
withDeviceTestBuilder {
sourceSetTreeName = "test"
}
compilations.configureEach {
compilerOptions.configure {
jvmTarget.set(
JvmTarget.JVM_11
)
}
}
}
sourceSets {
commonMain.dependencies {
implementation(libs.apktool.lib)
implementation(libs.kotlin.reflect)
implementation(libs.multidexlib2)
implementation(libs.smali)
implementation(libs.xpp3)
}
jvmTest.dependencies {
implementation(libs.mockk)
implementation(libs.kotlin.test)
}
}
compilerOptions {
freeCompilerArgs.addAll(
"-Xexplicit-backing-fields",
"-Xcontext-parameters"
)
}
}
tasks {
named<Test>("jvmTest") {
useJUnitPlatform()
}
}
mavenPublishing {
publishing {
repositories {
maven {
name = "githubPackages"
url = uri("https://maven.pkg.github.com/revanced/revanced-patcher")
credentials(PasswordCredentials::class)
}
}
}
signAllPublications()
extensions.getByType<SigningExtension>().useGpgCmd()
coordinates(group.toString(), project.name, version.toString())
pom {
name = "ReVanced Patcher"
description = "Patcher used by ReVanced."
inceptionYear = "2022"
url = "https://revanced.app"
licenses {
license {
name = "GNU General Public License v3.0"
url = "https://www.gnu.org/licenses/gpl-3.0.en.html"
}
}
developers {
developer {
id = "ReVanced"
name = "ReVanced"
email = "contact@revanced.app"
}
}
scm {
connection = "scm:git:git://github.com/revanced/revanced-patcher.git"
developerConnection = "scm:git:git@github.com:revanced/revanced-patcher.git"
url = "https://github.com/revanced/revanced-patcher"
}
}
}

View File

@@ -0,0 +1,39 @@
package app.revanced.patcher.patch
import dalvik.system.DexClassLoader
import lanchon.multidexlib2.BasicDexFileNamer
import lanchon.multidexlib2.MultiDexIO
import java.io.File
actual val Class<*>.isPatch get() = Patch::class.java.isAssignableFrom(this)
/**
* Loads patches from DEX files declared as public static fields
* or returned by public static and non-parametrized methods.
* Patches with no name are not loaded. If a patches file fails to load,
* the [onFailedToLoad] callback is invoked with the file and the throwable
* and the loading continues for the other files.
*
* @param patchesFiles The DEX files to load the patches from.
* @param onFailedToLoad A callback invoked when a patches file fails to load.
*
* @return The loaded patches.
*/
actual fun loadPatches(
vararg patchesFiles: File,
onFailedToLoad: (patchesFile: File, throwable: Throwable) -> Unit,
) = loadPatches(
patchesFiles = patchesFiles,
{ patchBundle ->
MultiDexIO.readDexFile(true, patchBundle, BasicDexFileNamer(), null, null).classes
.map { classDef ->
classDef.type.substring(1, classDef.length - 1)
}
},
DexClassLoader(
patchesFiles.joinToString(File.pathSeparator) { it.absolutePath },
null,
null, null
),
onFailedToLoad
)

View File

@@ -0,0 +1,7 @@
package collections
actual fun <K, V> MutableMap<K, V>.kmpMerge(
key: K,
value: V,
remappingFunction: (oldValue: V, newValue: V) -> V
) = merge(key, value, remappingFunction)

View File

@@ -0,0 +1,8 @@
package java.io
import java.nio.charset.Charset
internal actual fun File.kmpResolve(child: String) = resolve(child)
internal actual fun File.kmpDeleteRecursively() = deleteRecursively()
internal actual fun File.kmpInputStream() = inputStream()
internal actual fun File.kmpBufferedWriter(charset: Charset) = bufferedWriter(charset)

View File

@@ -0,0 +1,363 @@
@file:Suppress("unused", "MemberVisibilityCanBePrivate")
package app.revanced.patcher
import app.revanced.patcher.extensions.instructionsOrNull
import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patcher.patch.PatchException
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.ClassDef
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
import com.android.tools.smali.dexlib2.iface.reference.StringReference
import com.android.tools.smali.dexlib2.util.MethodUtil
@Deprecated("Use the matcher API instead.")
class Fingerprint internal constructor(
internal val accessFlags: Int?,
internal val returnType: String?,
internal val parameters: List<String>?,
internal val opcodes: List<Opcode?>?,
internal val strings: List<String>?,
internal val custom: ((method: Method, classDef: ClassDef) -> Boolean)?,
private val fuzzyPatternScanThreshold: Int,
) {
@Suppress("ktlint:standard:backing-property-naming")
// Backing field needed for lazy initialization.
private var _matchOrNull: FingerprintMatch? = null
context(_: BytecodePatchContext)
private val matchOrNull: FingerprintMatch?
get() = matchOrNull()
context(context: BytecodePatchContext)
internal fun matchOrNull(): FingerprintMatch? {
if (_matchOrNull != null) return _matchOrNull
var match = strings?.mapNotNull {
context.classDefs.methodsByString[it]
}?.minByOrNull { it.size }?.let { methodClasses ->
methodClasses.forEach { method ->
val match = matchOrNull(method, context.classDefs[method.definingClass]!!)
if (match != null) return@let match
}
null
}
if (match != null) return match
context.classDefs.forEach { classDef ->
match = matchOrNull(classDef)
if (match != null) return match
}
return null
}
context(_: BytecodePatchContext)
fun matchOrNull(
classDef: ClassDef,
): FingerprintMatch? {
if (_matchOrNull != null) return _matchOrNull
for (method in classDef.methods) {
val match = matchOrNull(method, classDef)
if (match != null) return match
}
return null
}
context(context: BytecodePatchContext)
fun matchOrNull(
method: Method,
) = matchOrNull(method, context.classDefs[method.definingClass]!!)
context(context: BytecodePatchContext)
fun matchOrNull(
method: Method,
classDef: ClassDef,
): FingerprintMatch? {
if (_matchOrNull != null) return _matchOrNull
if (returnType != null && !method.returnType.startsWith(returnType)) {
return null
}
if (accessFlags != null && accessFlags != method.accessFlags) {
return null
}
fun parametersEqual(
parameters1: Iterable<CharSequence>,
parameters2: Iterable<CharSequence>,
): Boolean {
if (parameters1.count() != parameters2.count()) return false
val iterator1 = parameters1.iterator()
parameters2.forEach {
if (!it.startsWith(iterator1.next())) return false
}
return true
}
// TODO: parseParameters()
if (parameters != null && !parametersEqual(parameters, method.parameterTypes)) {
return null
}
if (custom != null && !custom.invoke(method, classDef)) {
return null
}
val stringMatches: List<FingerprintMatch.StringMatch>? =
if (strings != null) {
buildList {
val instructions = method.instructionsOrNull ?: return null
val stringsList = strings.toMutableList()
instructions.forEachIndexed { instructionIndex, instruction ->
if (
instruction.opcode != Opcode.CONST_STRING &&
instruction.opcode != Opcode.CONST_STRING_JUMBO
) {
return@forEachIndexed
}
val string = ((instruction as ReferenceInstruction).reference as StringReference).string
val index = stringsList.indexOfFirst(string::contains)
if (index == -1) return@forEachIndexed
add(FingerprintMatch.StringMatch(string, instructionIndex))
stringsList.removeAt(index)
}
if (stringsList.isNotEmpty()) return null
}
} else {
null
}
val patternMatch = if (opcodes != null) {
val instructions = method.instructionsOrNull ?: return null
fun patternScan(): FingerprintMatch.PatternMatch? {
val fingerprintFuzzyPatternScanThreshold = fuzzyPatternScanThreshold
val instructionLength = instructions.count()
val patternLength = opcodes.size
for (index in 0 until instructionLength) {
var patternIndex = 0
var threshold = fingerprintFuzzyPatternScanThreshold
while (index + patternIndex < instructionLength) {
val originalOpcode = instructions.elementAt(index + patternIndex).opcode
val patternOpcode = opcodes.elementAt(patternIndex)
if (patternOpcode != null && patternOpcode.ordinal != originalOpcode.ordinal) {
// Reaching maximum threshold (0) means,
// the pattern does not match to the current instructions.
if (threshold-- == 0) break
}
if (patternIndex < patternLength - 1) {
// If the entire pattern has not been scanned yet, continue the scan.
patternIndex++
continue
}
// The entire pattern has been scanned.
return FingerprintMatch.PatternMatch(
index,
index + patternIndex,
)
}
}
return null
}
patternScan() ?: return null
} else {
null
}
_matchOrNull = FingerprintMatch(
context,
classDef,
method,
patternMatch,
stringMatches,
)
return _matchOrNull
}
private val exception get() = PatchException("Failed to match the fingerprint: $this")
context(_: BytecodePatchContext)
private val match
get() = matchOrNull ?: throw exception
context(_: BytecodePatchContext)
fun match(
classDef: ClassDef,
) = matchOrNull(classDef) ?: throw exception
context(_: BytecodePatchContext)
fun match(
method: Method,
) = matchOrNull(method) ?: throw exception
context(_: BytecodePatchContext)
fun match(
method: Method,
classDef: ClassDef,
) = matchOrNull(method, classDef) ?: throw exception
context(_: BytecodePatchContext)
val originalClassDefOrNull
get() = matchOrNull?.originalClassDef
context(_: BytecodePatchContext)
val originalMethodOrNull
get() = matchOrNull?.originalMethod
context(_: BytecodePatchContext)
val classDefOrNull
get() = matchOrNull?.classDef
context(_: BytecodePatchContext)
val methodOrNull
get() = matchOrNull?.method
context(_: BytecodePatchContext)
val patternMatchOrNull
get() = matchOrNull?.patternMatch
context(_: BytecodePatchContext)
val stringMatchesOrNull
get() = matchOrNull?.stringMatches
context(_: BytecodePatchContext)
val originalClassDef
get() = match.originalClassDef
context(_: BytecodePatchContext)
val originalMethod
get() = match.originalMethod
context(_: BytecodePatchContext)
val classDef
get() = match.classDef
context(_: BytecodePatchContext)
val method
get() = match.method
context(_: BytecodePatchContext)
val patternMatch
get() = match.patternMatch
context(_: BytecodePatchContext)
val stringMatches
get() = match.stringMatches
}
@Deprecated("Use the matcher API instead.")
class FingerprintMatch internal constructor(
val context: BytecodePatchContext,
val originalClassDef: ClassDef,
val originalMethod: Method,
val patternMatch: PatternMatch?,
val stringMatches: List<StringMatch>?,
) {
val classDef by lazy {
val classDef = context.classDefs[originalClassDef.type]!!
context.classDefs.getOrReplaceMutable(classDef)
}
val method by lazy { classDef.methods.first { MethodUtil.methodSignaturesMatch(it, originalMethod) } }
class PatternMatch internal constructor(
val startIndex: Int,
val endIndex: Int,
)
class StringMatch internal constructor(val string: String, val index: Int)
}
@Deprecated("Use the matcher API instead.")
class FingerprintBuilder internal constructor(
private val fuzzyPatternScanThreshold: Int = 0,
) {
private var accessFlags: Int? = null
private var returnType: String? = null
private var parameters: List<String>? = null
private var opcodes: List<Opcode?>? = null
private var strings: List<String>? = null
private var customBlock: ((method: Method, classDef: ClassDef) -> Boolean)? = null
fun accessFlags(accessFlags: Int) {
this.accessFlags = accessFlags
}
fun accessFlags(vararg accessFlags: AccessFlags) {
this.accessFlags = accessFlags.fold(0) { acc, it -> acc or it.value }
}
fun returns(returnType: String) {
this.returnType = returnType
}
fun parameters(vararg parameters: String) {
this.parameters = parameters.toList()
}
fun opcodes(vararg opcodes: Opcode?) {
this.opcodes = opcodes.toList()
}
fun opcodes(instructions: String) {
this.opcodes = instructions.trimIndent().split("\n").filter {
it.isNotBlank()
}.map {
// Remove any operands.
val name = it.split(" ", limit = 1).first().trim()
if (name == "null") return@map null
opcodesByName[name] ?: throw Exception("Unknown opcode: $name")
}
}
fun strings(vararg strings: String) {
this.strings = strings.toList()
}
fun custom(customBlock: (method: Method, classDef: ClassDef) -> Boolean) {
this.customBlock = customBlock
}
internal fun build() = Fingerprint(
accessFlags,
returnType,
parameters,
opcodes,
strings,
customBlock,
fuzzyPatternScanThreshold,
)
private companion object {
val opcodesByName = Opcode.entries.associateBy { it.name }
}
}
@Deprecated("Use the matcher API instead.")
fun fingerprint(
fuzzyPatternScanThreshold: Int = 0,
block: FingerprintBuilder.() -> Unit,
) = FingerprintBuilder(fuzzyPatternScanThreshold).apply(block).build()

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,152 @@
package app.revanced.patcher
import app.revanced.patcher.patch.*
import java.io.File
import java.io.InputStream
import java.io.deleteRecursively
import java.io.resolve
import java.util.logging.Logger
fun patcher(
apkFile: File,
temporaryFilesPath: File = File("revanced-patcher-temporary-files"),
aaptBinaryPath: File? = null,
frameworkFileDirectory: String? = null,
getPatches: (packageName: String, versionName: String) -> Set<Patch>,
): (emit: (PatchResult) -> Unit) -> PatchesResult {
val logger = Logger.getLogger("Patcher")
if (temporaryFilesPath.exists()) {
logger.info("Deleting existing temporary files directory")
if (!temporaryFilesPath.deleteRecursively())
logger.severe("Failed to delete existing temporary files directory")
}
val apkFilesPath = temporaryFilesPath.resolve("apk").also { it.mkdirs() }
val patchedFilesPath = temporaryFilesPath.resolve("patched").also { it.mkdirs() }
val resourcePatchContext = ResourcePatchContext(
apkFile,
apkFilesPath,
patchedFilesPath,
aaptBinaryPath,
frameworkFileDirectory
)
val (packageName, versionName) = resourcePatchContext.decodeManifest()
val patches = getPatches(packageName, versionName)
return { emit: (PatchResult) -> Unit ->
if (patches.any { patch -> patch.patchesResources }) resourcePatchContext.decodeResources()
// After initializing the resource context, to keep memory usage time low.
val bytecodePatchContext = BytecodePatchContext(
apkFile,
patchedFilesPath
)
logger.info("Warming up the cache")
bytecodePatchContext.classDefs.initializeCache()
logger.info("Applying patches")
patches.apply(bytecodePatchContext, resourcePatchContext, emit)
}
}
// Public for testing.
fun Set<Patch>.apply(
bytecodePatchContext: BytecodePatchContext,
resourcePatchContext: ResourcePatchContext,
emit: (PatchResult) -> Unit
): PatchesResult {
val appliedPatches = LinkedHashMap<Patch, PatchResult>()
sortedBy { it.name }.forEach { patch ->
fun Patch.apply(): PatchResult {
val result = appliedPatches[this]
return if (result == null) {
val failedDependency = dependencies.asSequence().map { it.apply() }.firstOrNull { it.exception != null }
if (failedDependency != null) return patchResult(
"The dependant patch \"$failedDependency\" of the patch \"$this\" raised an exception:\n" +
failedDependency.exception!!.stackTraceToString(),
)
val exception = runCatching { apply(bytecodePatchContext, resourcePatchContext) }
.exceptionOrNull() as? Exception
patchResult(exception).also { result -> appliedPatches[this] = result }
} else if (result.exception == null) result
else patchResult("The patch '$this' has failed previously")
}
val patchResult = patch.apply()
// If an exception occurred or the patch has no finalize block, emit the result.
if (patchResult.exception != null || patch.afterDependents == null) {
emit(patchResult)
}
}
val succeededPatchesWithFinalizeBlock = appliedPatches.values.filter {
it.exception == null && it.patch.afterDependents != null
}
succeededPatchesWithFinalizeBlock.asReversed().forEach { result ->
val patch = result.patch
runCatching { patch.afterDependents!!.invoke(bytecodePatchContext, resourcePatchContext) }.fold(
{ emit(result) },
{
emit(
PatchResult(
patch,
PatchException(
"The patch \"$patch\" raised an exception:\n" + it.stackTraceToString(),
it,
),
)
)
}
)
}
return PatchesResult(bytecodePatchContext.get(), resourcePatchContext.get())
}
/**
* The result of applying patches.
*
* @param dexFiles The patched dex files.
* @param resources The patched resources.
*/
class PatchesResult internal constructor(
val dexFiles: Set<PatchedDexFile>,
val resources: PatchedResources?,
) {
/**
* A dex file.
*
* @param name The original name of the dex file.
* @param stream The dex file as [InputStream].
*/
class PatchedDexFile internal 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 resources that should be deleted.
*/
class PatchedResources internal constructor(
val resourcesApk: File?,
val otherResources: File?,
val doNotCompress: Set<String>,
val deleteResources: Set<String>,
)
}

View File

@@ -0,0 +1,115 @@
package app.revanced.patcher.extensions
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcodes
import com.android.tools.smali.dexlib2.builder.BuilderInstruction
import com.android.tools.smali.dexlib2.iface.instruction.Instruction
import com.android.tools.smali.dexlib2.iface.instruction.NarrowLiteralInstruction
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
import com.android.tools.smali.dexlib2.iface.reference.*
import com.android.tools.smali.dexlib2.writer.builder.DexBuilder
import com.android.tools.smali.smali.smaliFlexLexer
import com.android.tools.smali.smali.smaliParser
import com.android.tools.smali.smali.smaliTreeWalker
import org.antlr.runtime.CommonTokenStream
import org.antlr.runtime.TokenSource
import org.antlr.runtime.tree.CommonTreeNodeStream
import java.io.StringReader
private inline fun <reified T : Reference> Instruction.reference(): T? =
(this as? ReferenceInstruction)?.reference as? T
val Instruction.reference: Reference?
get() = reference()
val Instruction.methodReference
get() = reference<MethodReference>()
val Instruction.fieldReference
get() = reference<FieldReference>()
val Instruction.typeReference
get() = reference<TypeReference>()
val Instruction.stringReference
get() = reference<StringReference>()
/** TODO: This is technically missing for consistency:
private inline fun <reified T : Reference> Instruction.reference2(): T? =
(this as? DualReferenceInstruction)?.reference2 as? T
val Instruction.reference2: Reference?
get() = reference2()
val Instruction.methodReference2
get() = reference2<MethodReference>()
val Instruction.fieldReference2
get() = reference2<FieldReference>()
val Instruction.typeReference2
get() = reference2<TypeReference>()
val Instruction.stringReference2
get() = reference2<StringReference>()
**/
val Instruction.type
get() = typeReference?.type
val Instruction.string
get() = stringReference?.string
val Instruction.wideLiteral
get() = (this as? NarrowLiteralInstruction)?.wideLiteral
private const val CLASS_HEADER = ".class LInlineCompiler;\n.super Ljava/lang/Object;\n"
private const val STATIC_HEADER = "$CLASS_HEADER.method public static dummyMethod("
private const val HEADER = "$CLASS_HEADER.method public dummyMethod("
private val sb by lazy { StringBuilder(512) }
/**
* Compile lines of Smali code to a list of instructions.
*
* Note: Adding compiled instructions to an existing method with
* offset instructions WITHOUT specifying a parent method will not work.
* @param templateMethod The method to compile the instructions against.
* @returns A list of instructions.
*/
fun String.toInstructions(templateMethod: com.android.tools.smali.dexlib2.mutable.MutableMethod? = null): List<BuilderInstruction> {
val parameters = templateMethod?.parameterTypes?.joinToString("") { it } ?: ""
val registers = templateMethod?.implementation?.registerCount ?: 1 // TODO: Should this be 0?
val isStatic = templateMethod?.let { AccessFlags.STATIC.isSet(it.accessFlags) } ?: true
sb.setLength(0) // reset
if (isStatic) sb.append(STATIC_HEADER) else sb.append(HEADER)
sb.append(parameters).append(")V\n")
sb.append(" .registers ").append(registers).append("\n")
sb.append(trimIndent()).append("\n")
sb.append(".end method")
val reader = StringReader(sb.toString())
val lexer = smaliFlexLexer(reader, 15)
val tokens = CommonTokenStream(lexer as TokenSource)
val parser = smaliParser(tokens)
val fileTree = parser.smali_file()
if (lexer.numberOfSyntaxErrors > 0 || parser.numberOfSyntaxErrors > 0) {
throw IllegalStateException(
"Lexer errors: ${lexer.numberOfSyntaxErrors}, Parser errors: ${parser.numberOfSyntaxErrors}"
)
}
val treeStream = CommonTreeNodeStream(fileTree.tree).apply {
tokenStream = tokens
}
val walker = smaliTreeWalker(treeStream)
walker.setDexBuilder(DexBuilder(Opcodes.getDefault()))
val classDef = walker.smali_file()
return classDef.methods.first().instructions.map { it as BuilderInstruction }
}

View File

@@ -0,0 +1,446 @@
package app.revanced.patcher.extensions
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.builder.BuilderInstruction
import com.android.tools.smali.dexlib2.builder.BuilderOffsetInstruction
import com.android.tools.smali.dexlib2.builder.Label
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
import com.android.tools.smali.dexlib2.builder.instruction.*
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.MethodImplementation
import com.android.tools.smali.dexlib2.iface.instruction.Instruction
import com.android.tools.smali.dexlib2.mutable.MutableMethod
fun Method.accessFlags(vararg flags: AccessFlags) =
accessFlags.and(flags.map { it.ordinal }.reduce { acc, i -> acc or i }) != 0
/**
* Add instructions to a method at the given index.
*
* @param index The index to add the instructions at.
* @param instructions The instructions to add.
*/
fun MutableMethodImplementation.addInstructions(
index: Int,
instructions: List<BuilderInstruction>,
) = instructions.asReversed().forEach { addInstruction(index, it) }
/**
* Add instructions to a method.
* The instructions will be added at the end of the method.
*
* @param instructions The instructions to add.
*/
fun MutableMethodImplementation.addInstructions(instructions: List<BuilderInstruction>) =
instructions.forEach { addInstruction(it) }
/**
* Remove instructions from a method at the given index.
*
* @param index The index to remove the instructions at.
* @param count The amount of instructions to remove.
*/
fun MutableMethodImplementation.removeInstructions(
index: Int,
count: Int,
) = repeat(count) {
removeInstruction(index)
}
/**
* Remove the first instructions from a method.
*
* @param count The amount of instructions to remove.
*/
fun MutableMethodImplementation.removeInstructions(count: Int) = removeInstructions(0, count)
/**
* Replace instructions at the given index with the given instructions.
* The amount of instructions to replace is the amount of instructions in the given list.
*
* @param index The index to replace the instructions at.
* @param instructions The instructions to replace the instructions with.
*/
fun MutableMethodImplementation.replaceInstructions(
index: Int,
instructions: List<BuilderInstruction>,
) = instructions.forEachIndexed { i, instruction -> replaceInstruction(index + i, instruction) }
/**
* Add an instruction to a method at the given index.
*
* @param index The index to add the instruction at.
* @param instruction The instruction to add.
*/
fun MutableMethod.addInstruction(
index: Int,
instruction: BuilderInstruction,
) = implementation!!.addInstruction(index, instruction)
/**
* Add an instruction to a method.
*
* @param instruction The instructions to add.
*/
fun MutableMethod.addInstruction(instruction: BuilderInstruction) = implementation!!.addInstruction(instruction)
/**
* Add an instruction to a method at the given index.
*
* @param index The index to add the instruction at.
* @param smaliInstructions The instruction to add.
*/
fun MutableMethod.addInstruction(
index: Int,
smaliInstructions: String,
) = implementation!!.addInstruction(index, smaliInstructions.toInstructions(this).first())
/**
* Add an instruction to a method.
*
* @param smaliInstructions The instruction to add.
*/
fun MutableMethod.addInstruction(smaliInstructions: String) =
implementation!!.addInstruction(smaliInstructions.toInstructions(this).first())
/**
* Add instructions to a method at the given index.
*
* @param index The index to add the instructions at.
* @param instructions The instructions to add.
*/
fun MutableMethod.addInstructions(
index: Int,
instructions: List<BuilderInstruction>,
) = implementation!!.addInstructions(index, instructions)
/**
* Add instructions to a method.
*
* @param instructions The instructions to add.
*/
fun MutableMethod.addInstructions(instructions: List<BuilderInstruction>) =
implementation!!.addInstructions(instructions)
/**
* Add instructions to a method.
*
* @param smaliInstructions The instructions to add.
*/
fun MutableMethod.addInstructions(
index: Int,
smaliInstructions: String,
) = implementation!!.addInstructions(index, smaliInstructions.toInstructions(this))
/**
* Add instructions to a method.
*
* @param smaliInstructions The instructions to add.
*/
fun MutableMethod.addInstructions(smaliInstructions: String) =
implementation!!.addInstructions(smaliInstructions.toInstructions(this))
/**
* Add instructions to a method at the given index.
*
* @param index The index to add the instructions at.
* @param smaliInstructions The instructions to add.
* @param externalLabels A list of [ExternalLabel] for instructions outside of [smaliInstructions].
*/
// Special function for adding instructions with external labels.
fun MutableMethod.addInstructionsWithLabels(
index: Int,
smaliInstructions: String,
vararg externalLabels: ExternalLabel,
) {
// Create reference dummy instructions for the instructions.
val nopSmali =
StringBuilder(smaliInstructions).also { builder ->
externalLabels.forEach { (name, _) ->
builder.append("\n:$name\nnop")
}
}.toString()
// Compile the instructions with the dummy labels
val compiledInstructions = nopSmali.toInstructions(this)
// Add the compiled list of instructions to the method.
addInstructions(
index,
compiledInstructions.subList(0, compiledInstructions.size - externalLabels.size),
)
implementation!!.apply {
this@apply.instructions.subList(index, index + compiledInstructions.size - externalLabels.size)
.forEachIndexed { compiledInstructionIndex, compiledInstruction ->
// If the compiled instruction is not an offset instruction, skip it.
if (compiledInstruction !is BuilderOffsetInstruction) return@forEachIndexed
/**
* Create a new label for the instruction
* and replace it with the label of the [compiledInstruction] at [compiledInstructionIndex].
*/
fun Instruction.makeNewLabel() {
fun replaceOffset(
i: BuilderOffsetInstruction,
label: Label,
): BuilderOffsetInstruction {
return when (i) {
is BuilderInstruction10t -> BuilderInstruction10t(i.opcode, label)
is BuilderInstruction20t -> BuilderInstruction20t(i.opcode, label)
is BuilderInstruction21t -> BuilderInstruction21t(i.opcode, i.registerA, label)
is BuilderInstruction22t ->
BuilderInstruction22t(
i.opcode,
i.registerA,
i.registerB,
label,
)
is BuilderInstruction30t -> BuilderInstruction30t(i.opcode, label)
is BuilderInstruction31t -> BuilderInstruction31t(i.opcode, i.registerA, label)
else -> throw IllegalStateException(
"A non-offset instruction was given, this should never happen!",
)
}
}
// Create the final label.
val label = newLabelForIndex(this@apply.instructions.indexOf(this))
// Create the final instruction with the new label.
val newInstruction =
replaceOffset(
compiledInstruction,
label,
)
// Replace the instruction pointing to the dummy label
// with the new instruction pointing to the real instruction.
replaceInstruction(index + compiledInstructionIndex, newInstruction)
}
// If the compiled instruction targets its own instruction,
// which means it points to some of its own, simply an offset has to be applied.
val labelIndex = compiledInstruction.target.location.index
if (labelIndex < compiledInstructions.size - externalLabels.size) {
// Get the targets index (insertion index + the index of the dummy instruction).
this.instructions[index + labelIndex].makeNewLabel()
return@forEachIndexed
}
// Since the compiled instruction points to a dummy instruction,
// we can find the real instruction which it was created for by calculation.
// Get the index of the instruction in the externalLabels list
// which the dummy instruction was created for.
// This works because we created the dummy instructions in the same order as the externalLabels list.
val (_, instruction) = externalLabels[(compiledInstructions.size - 1) - labelIndex]
instruction.makeNewLabel()
}
}
}
/**
* Remove an instruction at the given index.
*
* @param index The index to remove the instruction at.
*/
fun MutableMethod.removeInstruction(index: Int) = implementation!!.removeInstruction(index)
/**
* Remove instructions at the given index.
*
* @param index The index to remove the instructions at.
* @param count The amount of instructions to remove.
*/
fun MutableMethod.removeInstructions(
index: Int,
count: Int,
) = implementation!!.removeInstructions(index, count)
/**
* Remove instructions at the given index.
*
* @param count The amount of instructions to remove.
*/
fun MutableMethod.removeInstructions(count: Int) = implementation!!.removeInstructions(count)
/**
* Replace an instruction at the given index.
*
* @param index The index to replace the instruction at.
* @param instruction The instruction to replace the instruction with.
*/
fun MutableMethod.replaceInstruction(
index: Int,
instruction: BuilderInstruction,
) = implementation!!.replaceInstruction(index, instruction)
/**
* Replace an instruction at the given index.
*
* @param index The index to replace the instruction at.
* @param smaliInstruction The smali instruction to replace the instruction with.
*/
fun MutableMethod.replaceInstruction(
index: Int,
smaliInstruction: String,
) = implementation!!.replaceInstruction(index, smaliInstruction.toInstructions(this).first())
/**
* Replace instructions at the given index.
*
* @param index The index to replace the instructions at.
* @param instructions The instructions to replace the instructions with.
*/
fun MutableMethod.replaceInstructions(
index: Int,
instructions: List<BuilderInstruction>,
) = implementation!!.replaceInstructions(index, instructions)
/**
* Replace instructions at the given index.
*
* @param index The index to replace the instructions at.
* @param smaliInstructions The smali instructions to replace the instructions with.
*/
fun MutableMethod.replaceInstructions(
index: Int,
smaliInstructions: String,
) = implementation!!.replaceInstructions(index, smaliInstructions.toInstructions(this))
/**
* Get an instruction at the given index.
*
* @param index The index to get the instruction at.
* @return The instruction.
*/
fun MethodImplementation.getInstruction(index: Int) = instructions.elementAt(index)
/**
* Get an instruction at the given index.
*
* @param index The index to get the instruction at.
* @param T The type of instruction to return.
* @return The instruction.
*/
@Suppress("UNCHECKED_CAST")
fun <T> MethodImplementation.getInstruction(index: Int): T = getInstruction(index) as T
/**
* Get an instruction at the given index.
*
* @param index The index to get the instruction at.
* @return The instruction.
*/
fun MutableMethodImplementation.getInstruction(index: Int): BuilderInstruction = instructions[index]
/**
* Get an instruction at the given index.
*
* @param index The index to get the instruction at.
* @param T The type of instruction to return.
* @return The instruction.
*/
@Suppress("UNCHECKED_CAST")
fun <T> MutableMethodImplementation.getInstruction(index: Int): T = getInstruction(index) as T
/**
* Get an instruction at the given index.
* @param index The index to get the instruction at.
* @return The instruction or null if the method has no implementation.
*/
fun Method.getInstructionOrNull(index: Int): Instruction? = implementation?.getInstruction(index)
/**
* Get an instruction at the given index.
* @param index The index to get the instruction at.
* @return The instruction.
*/
fun Method.getInstruction(index: Int): Instruction = getInstructionOrNull(index)!!
/**
* Get an instruction at the given index.
* @param index The index to get the instruction at.
* @param T The type of instruction to return.
* @return The instruction or null if the method has no implementation.
*/
fun <T> Method.getInstructionOrNull(index: Int): T? = implementation?.getInstruction<T>(index)
/**
* Get an instruction at the given index.
* @param index The index to get the instruction at.
* @param T The type of instruction to return.
* @return The instruction.
*/
fun <T> Method.getInstruction(index: Int): T = getInstructionOrNull<T>(index)!!
/**
* Get an instruction at the given index.
* @param index The index to get the instruction at.
* @return The instruction or null if the method has no implementation.
*/
fun MutableMethod.getInstructionOrNull(index: Int): BuilderInstruction? = implementation?.getInstruction(index)
/**
* Get an instruction at the given index.
* @param index The index to get the instruction at.
* @return The instruction.
*/
fun MutableMethod.getInstruction(index: Int): BuilderInstruction = getInstructionOrNull(index)!!
/**
* Get an instruction at the given index.
* @param index The index to get the instruction at.
* @param T The type of instruction to return.
* @return The instruction or null if the method has no implementation.
*/
fun <T> MutableMethod.getInstructionOrNull(index: Int): T? = implementation?.getInstruction<T>(index)
/**
* Get an instruction at the given index.
* @param index The index to get the instruction at.
* @param T The type of instruction to return.
* @return The instruction.
*/
fun <T> MutableMethod.getInstruction(index: Int): T = getInstructionOrNull<T>(index)!!
/**
* The instructions of a method.
* @return The instructions or null if the method has no implementation.
*/
val Method.instructionsOrNull: Iterable<Instruction>? get() = implementation?.instructions
/**
* The instructions of a method.
* @return The instructions.
*/
val Method.instructions: Iterable<Instruction> get() = instructionsOrNull!!
/**
* The instructions of a method.
* @return The instructions or null if the method has no implementation.
*/
val MutableMethod.instructionsOrNull: MutableList<BuilderInstruction>? get() = implementation?.instructions
/**
* The instructions of a method.
* @return The instructions.
*/
val MutableMethod.instructions: MutableList<BuilderInstruction> get() = instructionsOrNull!!
/**
* Create a label for the instruction at given index.
*
* @param index The index to create the label for the instruction at.
* @return The label.
*/
fun MutableMethod.newLabel(index: Int) = implementation!!.newLabelForIndex(index)
/**
* A class that represents a label for an instruction.
* @param name The label name.
* @param instruction The instruction that this label is for.
*/
data class ExternalLabel(internal val name: String, internal val instruction: Instruction)

View File

@@ -0,0 +1,264 @@
package app.revanced.patcher.patch
import app.revanced.patcher.PatchesResult
import app.revanced.patcher.extensions.instructionsOrNull
import app.revanced.patcher.extensions.string
import app.revanced.patcher.util.ClassMerger.merge
import app.revanced.patcher.util.MethodNavigator
import com.android.tools.smali.dexlib2.iface.ClassDef
import com.android.tools.smali.dexlib2.iface.DexFile
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
import com.android.tools.smali.dexlib2.mutable.MutableClassDef
import com.android.tools.smali.dexlib2.mutable.MutableClassDef.Companion.toMutable
import lanchon.multidexlib2.BasicDexFileNamer
import lanchon.multidexlib2.DexIO
import lanchon.multidexlib2.MultiDexIO
import lanchon.multidexlib2.RawDexIO
import java.io.*
import java.util.logging.Logger
import kotlin.reflect.jvm.jvmName
/**
* A context for patches containing the current state of the bytecode.
*
* @param apkFile The apk [File] to patch.
* @param patchedFilesPath The path to the temporary apk files directory.
*/
@Suppress("MemberVisibilityCanBePrivate")
class BytecodePatchContext internal constructor(
internal val apkFile: File,
internal val patchedFilesPath: File,
) : PatchContext<Set<PatchesResult.PatchedDexFile>> {
private val logger = Logger.getLogger(this::class.jvmName)
inner class ClassDefs private constructor(
dexFile: DexFile,
private val classDefs: MutableSet<ClassDef> = dexFile.classes.toMutableSet()
) :
MutableSet<ClassDef> by classDefs {
private val byType = mutableMapOf<String, ClassDef>()
operator fun get(name: String): ClassDef? = byType[name]
// Better performance according to
// https://github.com/LisoUseInAIKyrios/revanced-patcher/commit/9b6d95d4f414a35ed68da37b0ecd8549df1ef63a
private val _methodsByStrings =
LinkedHashMap<String, MutableSet<Method>>(2 * size, 0.5f)
val methodsByString: Map<String, Set<Method>> = _methodsByStrings
// Can have a use-case in the future:
// private val _methodsWithString = methodsByString.values.flatten().toMutableSet()
// val methodsWithString: Set<Method> = _methodsWithString
constructor() : this(
MultiDexIO.readDexFile(
true,
apkFile,
BasicDexFileNamer(),
null,
null
)
)
internal val opcodes = dexFile.opcodes
override fun add(element: ClassDef): Boolean {
val added = classDefs.add(element)
if (added) addCache(element)
return added
}
override fun addAll(elements: Collection<ClassDef>): Boolean {
var anyAdded = false
elements.forEach { element ->
val added = classDefs.add(element)
if (added) {
addCache(element)
anyAdded = true
}
}
return anyAdded
}
// TODO: There is one default method "removeIf" in MutableSet, which we cannot override in the common module.
// The method must be overloaded with a NotImplementedException to avoid cache desynchronization.
override fun clear() {
classDefs.clear()
byType.clear()
_methodsByStrings.clear()
}
override fun remove(element: ClassDef): Boolean {
val removed = classDefs.remove(element)
if (removed) removeCache(element)
return removed
}
override fun removeAll(elements: Collection<ClassDef>): Boolean {
var anyRemoved = false
elements.forEach { element ->
val removed = classDefs.remove(element)
if (removed) {
removeCache(element)
anyRemoved = true
}
}
return anyRemoved
}
override fun retainAll(elements: Collection<ClassDef>) =
removeAll(classDefs.asSequence().filter { it !in elements })
private fun addCache(classDef: ClassDef) {
byType[classDef.type] = classDef
classDef.forEachString { method, string ->
_methodsByStrings.getOrPut(string) {
// Maybe adjusting load factor/ initial size can improve performance.
mutableSetOf()
} += method
}
}
private fun removeCache(classDef: ClassDef) {
byType -= classDef.type
classDef.forEachString { method, string ->
if (_methodsByStrings[string]?.also { it -= method }?.isEmpty() == true)
_methodsByStrings -= string
}
}
private fun ClassDef.forEachString(action: (Method, String) -> Unit) {
methods.asSequence().forEach { method ->
method.instructionsOrNull?.asSequence()
?.mapNotNull { it.string }
?.forEach { string -> action(method, string) }
}
}
/**
* Get a mutable version of the given [classDef], replacing it in the set if necessary.
*
* @param classDef The [ClassDef] to get or replace.
* @return The mutable version of the [classDef].
* @see MutableClassDef
* @see toMutable
*/
fun getOrReplaceMutable(classDef: ClassDef): MutableClassDef {
if (classDef !is MutableClassDef) {
val mutableClassDef = classDef.toMutable()
this -= classDef
this += mutableClassDef
return mutableClassDef
}
return classDef
}
internal fun initializeCache() = classDefs.forEach(::addCache)
internal fun clearCache() {
byType.clear()
_methodsByStrings.clear()
}
}
/**
* The list of classes.
*/
val classDefs = ClassDefs()
/**
* Extend this [BytecodePatchContext] with [extensionInputStream].
*
* @param extensionInputStream The input stream for an extension dex file.
*/
internal fun extendWith(extensionInputStream: InputStream) {
RawDexIO.readRawDexFile(
extensionInputStream, 0, null
).classes.forEach { classDef ->
val existingClass = classDefs[classDef.type] ?: run {
logger.fine { "Adding class \"$classDef\"" }
classDefs += classDef
return@forEach
}
logger.fine { "Class \"$classDef\" exists already. Adding missing methods and fields." }
existingClass.merge(classDef, this@BytecodePatchContext).let { mergedClass ->
// If the class was merged, replace the original class with the merged class.
if (mergedClass === existingClass) {
return@let
}
classDefs -= existingClass
classDefs += mergedClass
}
}
extensionInputStream.close()
}
/**
* Navigate a method.
*
* @param method The method to navigate.
*
* @return A [MethodNavigator] for the method.
*/
fun navigate(method: MethodReference) = MethodNavigator(method)
/**
* Compile bytecode from the [BytecodePatchContext].
*
* @return The compiled bytecode.
*/
override fun get(): Set<PatchesResult.PatchedDexFile> {
logger.info("Compiling patched dex files")
classDefs.clearCache()
System.gc()
val patchedDexFileResults =
patchedFilesPath.resolve("dex").also {
it.deleteRecursively() // Make sure the directory is empty.
it.mkdirs()
}.apply {
MultiDexIO.writeDexFile(
true,
-1,
this,
BasicDexFileNamer(),
object : DexFile {
override fun getClasses() = classDefs.let {
// More performant according to
// https://github.com/LisoUseInAIKyrios/revanced-patcher/
// commit/8c26ad08457fb1565ea5794b7930da42a1c81cf1
// #diff-be698366d9868784ecf7da3fd4ac9d2b335b0bb637f9f618fbe067dbd6830b8fR197
// TODO: Benchmark, if actually faster.
HashSet<ClassDef>(it.size * 3 / 2).apply { addAll(it) }
}
override fun getOpcodes() = classDefs.opcodes
},
DexIO.DEFAULT_MAX_DEX_POOL_SIZE,
) { _, entryName, _ -> logger.info { "Compiled $entryName" } }
}.listFiles { it.isFile }!!.map {
PatchesResult.PatchedDexFile(it.name, it.inputStream())
}.toSet()
return patchedDexFileResults
}
}

View File

@@ -1,17 +1,19 @@
@file:Suppress("unused")
package app.revanced.patcher.patch
import kotlin.reflect.KProperty
import kotlin.reflect.KType
import kotlin.reflect.jvm.jvmName
import kotlin.reflect.typeOf
/**
* An option.
*
* @param T The value type of the option.
* @param key The key.
* @param name The name.
* @param default The default value.
* @param values Eligible option values mapped to a human-readable name.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param type The type of the option value (to handle type erasure).
@@ -22,49 +24,15 @@ import kotlin.reflect.typeOf
@Suppress("MemberVisibilityCanBePrivate", "unused")
class Option<T>
@PublishedApi
@Deprecated("Use the constructor with the name instead of a key instead.")
internal constructor(
@Deprecated("Use the name property instead.")
val key: String,
val name: String,
val default: T? = null,
val values: Map<String, T?>? = null,
@Deprecated("Use the name property instead.")
val title: String? = null,
val description: String? = null,
val required: Boolean = false,
val type: KType,
val validator: Option<T>.(T?) -> Boolean = { true },
) {
/**
* The name.
*/
val name = key
/**
* An option.
*
* @param T The value type of the option.
* @param name The name.
* @param default The default value.
* @param values Eligible option values mapped to a human-readable name.
* @param description A description.
* @param required Whether the option is required.
* @param type The type of the option value (to handle type erasure).
* @param validator The function to validate the option value.
*
* @constructor Create a new [Option].
*/
@PublishedApi
internal constructor(
name: String,
default: T? = null,
values: Map<String, T?>? = null,
description: String? = null,
required: Boolean = false,
type: KType,
validator: Option<T>.(T?) -> Boolean = { true },
) : this(name, default, values, name, description, required, type, validator)
/**
* The value of the [Option].
*/
@@ -83,7 +51,6 @@ internal constructor(
uncheckedValue = value
}
/**
* Get the value of the [Option].
*
@@ -149,21 +116,21 @@ class Options internal constructor(
/**
* Set an option's value.
*
* @param key The key.
* @param name The name.
* @param value The value.
*
* @throws OptionException.OptionNotFoundException If the option does not exist.
*/
operator fun <T : Any> set(key: String, value: T?) {
val option = this[key]
operator fun <T : Any> set(name: String, value: T?) {
val option = this[name]
try {
@Suppress("UNCHECKED_CAST")
(option as Option<T>).value = value
} catch (e: ClassCastException) {
throw OptionException.InvalidValueTypeException(
value?.let { it::class.java.name } ?: "null",
option.value?.let { it::class.java.name } ?: "null",
value?.let { it::class.jvmName } ?: "null",
option.value?.let { it::class.jvmName } ?: "null",
)
}
}
@@ -171,7 +138,7 @@ class Options internal constructor(
/**
* Get an option.
*
* @param key The key.
* @param key The name.
*
* @return The option.
*/
@@ -181,10 +148,9 @@ class Options internal constructor(
/**
* Create a new [Option] with a string value.
*
* @param key The key.
* @param name The name.
* @param default The default value.
* @param values Eligible option values mapped to a human-readable name.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
@@ -194,18 +160,16 @@ class Options internal constructor(
* @see Option
*/
fun stringOption(
key: String,
name: String,
default: String? = null,
values: Map<String, String?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: Option<String>.(String?) -> Boolean = { true },
) = option(
key,
name,
default,
values,
title,
description,
required,
validator,
@@ -214,10 +178,9 @@ fun stringOption(
/**
* Create a new [Option] with a string value and add it to the current [PatchBuilder].
*
* @param key The key.
* @param name The name.
* @param default The default value.
* @param values Eligible option values mapped to a human-readable name.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
@@ -227,18 +190,16 @@ fun stringOption(
* @see Option
*/
fun PatchBuilder<*>.stringOption(
key: String,
name: String,
default: String? = null,
values: Map<String, String?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: Option<String>.(String?) -> Boolean = { true },
) = option(
key,
name,
default,
values,
title,
description,
required,
validator,
@@ -247,10 +208,9 @@ fun PatchBuilder<*>.stringOption(
/**
* Create a new [Option] with an integer value.
*
* @param key The key.
* @param name The name.
* @param default The default value.
* @param values Eligible option values mapped to a human-readable name.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
@@ -260,18 +220,16 @@ fun PatchBuilder<*>.stringOption(
* @see Option
*/
fun intOption(
key: String,
name: String,
default: Int? = null,
values: Map<String, Int?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: Option<Int>.(Int?) -> Boolean = { true },
) = option(
key,
name,
default,
values,
title,
description,
required,
validator,
@@ -280,10 +238,9 @@ fun intOption(
/**
* Create a new [Option] with an integer value and add it to the current [PatchBuilder].
*
* @param key The key.
* @param name The name.
* @param default The default value.
* @param values Eligible option values mapped to a human-readable name.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
@@ -293,18 +250,16 @@ fun intOption(
* @see Option
*/
fun PatchBuilder<*>.intOption(
key: String,
name: String,
default: Int? = null,
values: Map<String, Int?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: Option<Int>.(Int?) -> Boolean = { true },
) = option(
key,
name,
default,
values,
title,
description,
required,
validator,
@@ -313,10 +268,9 @@ fun PatchBuilder<*>.intOption(
/**
* Create a new [Option] with a boolean value.
*
* @param key The key.
* @param name The name.
* @param default The default value.
* @param values Eligible option values mapped to a human-readable name.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
@@ -326,18 +280,16 @@ fun PatchBuilder<*>.intOption(
* @see Option
*/
fun booleanOption(
key: String,
name: String,
default: Boolean? = null,
values: Map<String, Boolean?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: Option<Boolean>.(Boolean?) -> Boolean = { true },
) = option(
key,
name,
default,
values,
title,
description,
required,
validator,
@@ -346,10 +298,9 @@ fun booleanOption(
/**
* Create a new [Option] with a boolean value and add it to the current [PatchBuilder].
*
* @param key The key.
* @param name The name.
* @param default The default value.
* @param values Eligible option values mapped to a human-readable name.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
@@ -359,18 +310,16 @@ fun booleanOption(
* @see Option
*/
fun PatchBuilder<*>.booleanOption(
key: String,
name: String,
default: Boolean? = null,
values: Map<String, Boolean?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: Option<Boolean>.(Boolean?) -> Boolean = { true },
) = option(
key,
name,
default,
values,
title,
description,
required,
validator,
@@ -379,10 +328,9 @@ fun PatchBuilder<*>.booleanOption(
/**
* Create a new [Option] with a float value.
*
* @param key The key.
* @param name The name.
* @param default The default value.
* @param values Eligible option values mapped to a human-readable name.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
@@ -392,18 +340,16 @@ fun PatchBuilder<*>.booleanOption(
* @see Option
*/
fun floatOption(
key: String,
name: String,
default: Float? = null,
values: Map<String, Float?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: Option<Float>.(Float?) -> Boolean = { true },
) = option(
key,
name,
default,
values,
title,
description,
required,
validator,
@@ -412,10 +358,9 @@ fun floatOption(
/**
* Create a new [Option] with a float value and add it to the current [PatchBuilder].
*
* @param key The key.
* @param name The name.
* @param default The default value.
* @param values Eligible option values mapped to a human-readable name.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
@@ -425,18 +370,16 @@ fun floatOption(
* @see Option
*/
fun PatchBuilder<*>.floatOption(
key: String,
name: String,
default: Float? = null,
values: Map<String, Float?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: Option<Float>.(Float?) -> Boolean = { true },
) = option(
key,
name,
default,
values,
title,
description,
required,
validator,
@@ -445,10 +388,9 @@ fun PatchBuilder<*>.floatOption(
/**
* Create a new [Option] with a long value.
*
* @param key The key.
* @param name The name.
* @param default The default value.
* @param values Eligible option values mapped to a human-readable name.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
@@ -458,18 +400,16 @@ fun PatchBuilder<*>.floatOption(
* @see Option
*/
fun longOption(
key: String,
name: String,
default: Long? = null,
values: Map<String, Long?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: Option<Long>.(Long?) -> Boolean = { true },
) = option(
key,
name,
default,
values,
title,
description,
required,
validator,
@@ -478,10 +418,9 @@ fun longOption(
/**
* Create a new [Option] with a long value and add it to the current [PatchBuilder].
*
* @param key The key.
* @param name The name.
* @param default The default value.
* @param values Eligible option values mapped to a human-readable name.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
@@ -491,18 +430,16 @@ fun longOption(
* @see Option
*/
fun PatchBuilder<*>.longOption(
key: String,
name: String,
default: Long? = null,
values: Map<String, Long?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: Option<Long>.(Long?) -> Boolean = { true },
) = option(
key,
name,
default,
values,
title,
description,
required,
validator,
@@ -511,10 +448,9 @@ fun PatchBuilder<*>.longOption(
/**
* Create a new [Option] with a string list value.
*
* @param key The key.
* @param name The name.
* @param default The default value.
* @param values Eligible option values mapped to a human-readable name.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
@@ -524,18 +460,16 @@ fun PatchBuilder<*>.longOption(
* @see Option
*/
fun stringsOption(
key: String,
name: String,
default: List<String>? = null,
values: Map<String, List<String>?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: Option<List<String>>.(List<String>?) -> Boolean = { true },
) = option(
key,
name,
default,
values,
title,
description,
required,
validator,
@@ -544,10 +478,9 @@ fun stringsOption(
/**
* Create a new [Option] with a string list value and add it to the current [PatchBuilder].
*
* @param key The key.
* @param name The name.
* @param default The default value.
* @param values Eligible option values mapped to a human-readable name.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
@@ -557,18 +490,16 @@ fun stringsOption(
* @see Option
*/
fun PatchBuilder<*>.stringsOption(
key: String,
name: String,
default: List<String>? = null,
values: Map<String, List<String>?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: Option<List<String>>.(List<String>?) -> Boolean = { true },
) = option(
key,
name,
default,
values,
title,
description,
required,
validator,
@@ -577,10 +508,9 @@ fun PatchBuilder<*>.stringsOption(
/**
* Create a new [Option] with an integer list value.
*
* @param key The key.
* @param name The name.
* @param default The default value.
* @param values Eligible option values mapped to a human-readable name.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
@@ -590,18 +520,16 @@ fun PatchBuilder<*>.stringsOption(
* @see Option
*/
fun intsOption(
key: String,
name: String,
default: List<Int>? = null,
values: Map<String, List<Int>?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: Option<List<Int>>.(List<Int>?) -> Boolean = { true },
) = option(
key,
name,
default,
values,
title,
description,
required,
validator,
@@ -610,10 +538,9 @@ fun intsOption(
/**
* Create a new [Option] with an integer list value and add it to the current [PatchBuilder].
*
* @param key The key.
* @param name The name.
* @param default The default value.
* @param values Eligible option values mapped to a human-readable name.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
@@ -623,18 +550,16 @@ fun intsOption(
* @see Option
*/
fun PatchBuilder<*>.intsOption(
key: String,
name: String,
default: List<Int>? = null,
values: Map<String, List<Int>?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: Option<List<Int>>.(List<Int>?) -> Boolean = { true },
) = option(
key,
name,
default,
values,
title,
description,
required,
validator,
@@ -643,10 +568,9 @@ fun PatchBuilder<*>.intsOption(
/**
* Create a new [Option] with a boolean list value.
*
* @param key The key.
* @param name The name.
* @param default The default value.
* @param values Eligible option values mapped to a human-readable name.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
@@ -656,18 +580,16 @@ fun PatchBuilder<*>.intsOption(
* @see Option
*/
fun booleansOption(
key: String,
name: String,
default: List<Boolean>? = null,
values: Map<String, List<Boolean>?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: Option<List<Boolean>>.(List<Boolean>?) -> Boolean = { true },
) = option(
key,
name,
default,
values,
title,
description,
required,
validator,
@@ -676,10 +598,9 @@ fun booleansOption(
/**
* Create a new [Option] with a boolean list value and add it to the current [PatchBuilder].
*
* @param key The key.
* @param name The name.
* @param default The default value.
* @param values Eligible option values mapped to a human-readable name.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
@@ -689,18 +610,16 @@ fun booleansOption(
* @see Option
*/
fun PatchBuilder<*>.booleansOption(
key: String,
name: String,
default: List<Boolean>? = null,
values: Map<String, List<Boolean>?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: Option<List<Boolean>>.(List<Boolean>?) -> Boolean = { true },
) = option(
key,
name,
default,
values,
title,
description,
required,
validator,
@@ -709,10 +628,9 @@ fun PatchBuilder<*>.booleansOption(
/**
* Create a new [Option] with a float list value and add it to the current [PatchBuilder].
*
* @param key The key.
* @param name The name.
* @param default The default value.
* @param values Eligible option values mapped to a human-readable name.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
@@ -722,18 +640,16 @@ fun PatchBuilder<*>.booleansOption(
* @see Option
*/
fun PatchBuilder<*>.floatsOption(
key: String,
name: String,
default: List<Float>? = null,
values: Map<String, List<Float>?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: Option<List<Float>>.(List<Float>?) -> Boolean = { true },
) = option(
key,
name,
default,
values,
title,
description,
required,
validator,
@@ -742,10 +658,9 @@ fun PatchBuilder<*>.floatsOption(
/**
* Create a new [Option] with a long list value.
*
* @param key The key.
* @param name The name.
* @param default The default value.
* @param values Eligible option values mapped to a human-readable name.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
@@ -755,18 +670,16 @@ fun PatchBuilder<*>.floatsOption(
* @see Option
*/
fun longsOption(
key: String,
name: String,
default: List<Long>? = null,
values: Map<String, List<Long>?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: Option<List<Long>>.(List<Long>?) -> Boolean = { true },
) = option(
key,
name,
default,
values,
title,
description,
required,
validator,
@@ -775,10 +688,9 @@ fun longsOption(
/**
* Create a new [Option] with a long list value and add it to the current [PatchBuilder].
*
* @param key The key.
* @param name The name.
* @param default The default value.
* @param values Eligible option values mapped to a human-readable name.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
@@ -788,18 +700,16 @@ fun longsOption(
* @see Option
*/
fun PatchBuilder<*>.longsOption(
key: String,
name: String,
default: List<Long>? = null,
values: Map<String, List<Long>?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: Option<List<Long>>.(List<Long>?) -> Boolean = { true },
) = option(
key,
name,
default,
values,
title,
description,
required,
validator,
@@ -808,10 +718,9 @@ fun PatchBuilder<*>.longsOption(
/**
* Create a new [Option].
*
* @param key The key.
* @param name The name.
* @param default The default value.
* @param values Eligible option values mapped to a human-readable name.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
@@ -821,18 +730,16 @@ fun PatchBuilder<*>.longsOption(
* @see Option
*/
inline fun <reified T> option(
key: String,
name: String,
default: T? = null,
values: Map<String, T?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
noinline validator: Option<T>.(T?) -> Boolean = { true },
) = Option(
key,
name,
default,
values,
title,
description,
required,
typeOf<T>(),
@@ -842,10 +749,9 @@ inline fun <reified T> option(
/**
* Create a new [Option] and add it to the current [PatchBuilder].
*
* @param key The key.
* @param name The name.
* @param default The default value.
* @param values Eligible option values mapped to a human-readable name.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
@@ -855,18 +761,16 @@ inline fun <reified T> option(
* @see Option
*/
inline fun <reified T> PatchBuilder<*>.option(
key: String,
name: String,
default: T? = null,
values: Map<String, T?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
noinline validator: Option<T>.(T?) -> Boolean = { true },
) = app.revanced.patcher.patch.option(
key,
name,
default,
values,
title,
description,
required,
validator,
@@ -884,26 +788,29 @@ sealed class OptionException(errorMessage: String) : Exception(errorMessage, nul
* @param invalidType The type of the value that was passed.
* @param expectedType The type of the value that was expected.
*/
class InvalidValueTypeException(invalidType: String, expectedType: String) : OptionException("Type $expectedType was expected but received type $invalidType")
class InvalidValueTypeException(invalidType: String, expectedType: String) :
OptionException("Type $expectedType was expected but received type $invalidType")
/**
* An exception thrown when a value did not satisfy the value conditions specified by the [Option].
*
* @param value The value that failed validation.
*/
class ValueValidationException(value: Any?, option: Option<*>) : OptionException("The option value \"$value\" failed validation for ${option.name}")
class ValueValidationException(value: Any?, option: Option<*>) :
OptionException("The option value \"$value\" failed validation for ${option.name}")
/**
* An exception thrown when a value is required but null was passed.
*
* @param option The [Option] that requires a value.
*/
class ValueRequiredException(option: Option<*>) : OptionException("The option ${option.name} requires a value, but the value was null")
class ValueRequiredException(option: Option<*>) :
OptionException("The option ${option.name} requires a value, but the value was null")
/**
* An exception thrown when a [Option] is not found.
*
* @param key The key of the [Option].
* @param name The name of the [Option].
*/
class OptionNotFoundException(key: String) : OptionException("No option with key $key")
class OptionNotFoundException(name: String) : OptionException("No option with name $name")
}

View File

@@ -0,0 +1,273 @@
@file:Suppress("MemberVisibilityCanBePrivate", "unused")
package app.revanced.patcher.patch
import java.io.File
import java.io.InputStream
import java.lang.reflect.Member
import java.lang.reflect.Method
import java.lang.reflect.Modifier
import java.util.function.Supplier
import kotlin.properties.ReadOnlyProperty
typealias PackageName = String
typealias VersionName = String
typealias Package = Pair<PackageName, Set<VersionName>?>
enum class PatchType(internal val prefix: String) {
BYTECODE("Bytecode"),
RAW_RESOURCE("RawResource"),
RESOURCE("Resource")
}
internal val Patch.patchesResources: Boolean get() = type == PatchType.RESOURCE || dependencies.any { it.patchesResources }
open class Patch internal constructor(
val name: String?,
val description: String?,
val use: Boolean,
val dependencies: Set<Patch>,
val compatiblePackages: Set<Package>?,
options: Set<Option<*>>,
internal val apply: context(BytecodePatchContext, ResourcePatchContext) () -> Unit,
// Must be nullable, so that Patcher.invoke can check,
// if a patch has an "afterDependents" in order to not emit it twice.
internal var afterDependents: (context(BytecodePatchContext, ResourcePatchContext) () -> Unit)?,
internal val type: PatchType,
) {
val options = Options(options)
override fun toString() = name ?: "${type.prefix}Patch@${System.identityHashCode(this)}"
}
sealed class PatchBuilder<C : PatchContext<*>>(
private val type: PatchType,
private val getPatchContext: context(BytecodePatchContext, ResourcePatchContext) () -> C
) {
private var compatiblePackages: MutableSet<Package>? = null
private val dependencies = mutableSetOf<Patch>()
private val options = mutableSetOf<Option<*>>()
internal var apply: context(BytecodePatchContext, ResourcePatchContext) () -> Unit = { }
internal var afterDependents: (context(BytecodePatchContext, ResourcePatchContext) () -> Unit)? = null
context(_: BytecodePatchContext, _: ResourcePatchContext)
private val patchContext get() = getPatchContext()
fun apply(block: C.() -> Unit) {
apply = { block(patchContext) }
}
fun afterDependents(block: C.() -> Unit) {
afterDependents = { block(patchContext) }
}
operator fun <T> Option<T>.invoke() = apply {
options += this
}
operator fun String.invoke(vararg versions: VersionName) = invoke(versions.toSet())
private operator fun String.invoke(versions: Set<VersionName>? = null): Package = this to versions
fun compatibleWith(vararg packages: Package) {
if (compatiblePackages == null) {
compatiblePackages = mutableSetOf()
}
compatiblePackages!! += packages
}
fun compatibleWith(vararg packages: String) = compatibleWith(*packages.map { it() }.toTypedArray())
fun dependsOn(vararg patches: Patch) {
dependencies += patches
}
fun build(name: String?, description: String?, use: Boolean) = Patch(
name,
description,
use,
dependencies,
compatiblePackages,
options,
apply,
afterDependents,
type,
)
}
class BytecodePatchBuilder private constructor(
private var extensionInputStream: InputStream? = null
) : PatchBuilder<BytecodePatchContext>(
PatchType.BYTECODE,
{
// Extend the context with the extension, before returning it to the patch before applying it.
contextOf<BytecodePatchContext>().apply {
if (extensionInputStream != null) extendWith(extensionInputStream)
}
}
) {
internal constructor() : this(null)
fun extendWith(extension: String) {
// Should be the classloader which loaded the patch class.
val classLoader = Class.forName(Thread.currentThread().stackTrace[2].className).classLoader!!
extensionInputStream = classLoader.getResourceAsStream(extension)
?: throw PatchException("Extension \"$extension\" not found")
}
}
open class ResourcePatchBuilder internal constructor(type: PatchType) : PatchBuilder<ResourcePatchContext>(
type,
{ contextOf<ResourcePatchContext>() }
) {
internal constructor() : this(PatchType.RESOURCE)
}
class RawResourcePatchBuilder internal constructor() : ResourcePatchBuilder()
fun bytecodePatch(
name: String? = null,
description: String? = null,
use: Boolean = true,
block: BytecodePatchBuilder.() -> Unit
) = BytecodePatchBuilder().apply(block).build(name, description, use)
fun resourcePatch(
name: String? = null,
description: String? = null,
use: Boolean = true,
block: ResourcePatchBuilder.() -> Unit
) = ResourcePatchBuilder().apply(block).build(name, description, use)
fun rawResourcePatch(
name: String? = null,
description: String? = null,
use: Boolean = true,
block: RawResourcePatchBuilder.() -> Unit
) = RawResourcePatchBuilder().apply(block).build(name, description, use)
private fun <B : PatchBuilder<*>> creatingPatch(
description: String? = null,
use: Boolean = true,
block: B.() -> Unit,
patchSupplier: (String?, String?, Boolean, B.() -> Unit) -> Patch
) = ReadOnlyProperty<Any?, Patch> { _, property -> patchSupplier(property.name, description, use, block) }
fun creatingBytecodePatch(
description: String? = null,
use: Boolean = true,
block: BytecodePatchBuilder.() -> Unit,
) = creatingPatch(description, use, block) { name, description, use, block ->
bytecodePatch(name, description, use, block)
}
fun creatingResourcePatch(
description: String? = null,
use: Boolean = true,
block: ResourcePatchBuilder.() -> Unit,
) = creatingPatch(description, use, block) { name, description, use, block ->
resourcePatch(name, description, use, block)
}
fun creatingRawResourcePatch(
description: String? = null,
use: Boolean = true,
block: RawResourcePatchBuilder.() -> Unit,
) = creatingPatch(description, use, block) { name, description, use, block ->
rawResourcePatch(name, description, use, block)
}
/**
* A common interface for contexts such as [ResourcePatchContext] and [BytecodePatchContext].
*/
sealed interface PatchContext<T> : Supplier<T>
/**
* An exception thrown when patching.
*
* @param errorMessage The exception message.
* @param cause The corresponding [Throwable].
*/
class PatchException(errorMessage: String?, cause: Throwable?) : Exception(errorMessage, cause) {
constructor(errorMessage: String) : this(errorMessage, null)
constructor(cause: Throwable) : this(cause.message, cause)
}
/**
* A result of applying a [Patch].
*
* @param patch The [Patch] that ran.
* @param exception The [PatchException] thrown, if any.
*/
class PatchResult internal constructor(val patch: Patch, val exception: PatchException? = null)
/**
* Creates a [PatchResult] for this [Patch].
*
* @param exception The [PatchException] thrown, if any.
* @return The created [PatchResult].
*/
internal fun Patch.patchResult(exception: Exception? = null) = PatchResult(this, exception?.toPatchException())
/**
* Creates a [PatchResult] for this [Patch] with the given error message.
*
* @param errorMessage The error message.
* @return The created [PatchResult].
*/
internal fun Patch.patchResult(errorMessage: String) = PatchResult(this, PatchException(errorMessage))
private fun Exception.toPatchException() = this as? PatchException ?: PatchException(this)
/**
* A collection of patches loaded from patches files.
*
* @property patchesByFile The patches mapped by their patches file.
*/
class Patches internal constructor(val patchesByFile: Map<File, Set<Patch>>) : Set<Patch>
by patchesByFile.values.flatten().toSet()
// Must be internal and a separate function for testing.
@Suppress("MISSING_DEPENDENCY_IN_INFERRED_TYPE_ANNOTATION_WARNING")
internal fun getPatches(classNames: List<String>, classLoader: ClassLoader): Set<Patch> {
fun Member.isUsable() =
Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers) && (this !is Method || parameterCount == 0)
fun Class<*>.getPatchFields() = fields
.filter { it.type.isPatch && it.isUsable() }
.map { it.get(null) as Patch }
fun Class<*>.getPatchMethods() = methods
.filter { it.returnType.isPatch && it.parameterCount == 0 && it.isUsable() }
.map { it.invoke(null) as Patch }
return classNames
.map { classLoader.loadClass(it) }
.flatMap { it.getPatchMethods() + it.getPatchFields() }
.filter { it.name != null }
.toSet()
}
internal fun loadPatches(
vararg patchesFiles: File,
getBinaryClassNames: (patchesFile: File) -> List<String>,
classLoader: ClassLoader,
onFailedToLoad: (File, Throwable) -> Unit
) = Patches(patchesFiles.map { file ->
file to getBinaryClassNames(file)
}.mapNotNull { (file, classNames) ->
runCatching { file to getPatches(classNames, classLoader) }
.onFailure { onFailedToLoad(file, it) }.getOrNull()
}.toMap())
expect fun loadPatches(
vararg patchesFiles: File,
onFailedToLoad: (patchesFile: File, throwable: Throwable) -> Unit = { _, _ -> },
): Patches
internal expect val Class<*>.isPatch: Boolean

View File

@@ -0,0 +1,229 @@
package app.revanced.patcher.patch
import app.revanced.patcher.PatchesResult
import app.revanced.patcher.util.Document
import brut.androlib.AaptInvoker
import brut.androlib.ApkDecoder
import brut.androlib.Config
import brut.androlib.apk.ApkInfo
import brut.androlib.apk.UsesFramework
import brut.androlib.res.Framework
import brut.androlib.res.ResourcesDecoder
import brut.androlib.res.decoder.AndroidManifestPullStreamDecoder
import brut.androlib.res.decoder.AndroidManifestResourceParser
import brut.androlib.res.xml.ResXmlUtils
import brut.directory.ExtFile
import java.io.File
import java.io.InputStream
import java.io.OutputStream
import java.io.resolve
import java.nio.file.Files
import java.util.logging.Logger
import kotlin.reflect.jvm.jvmName
/**
* A context for patches containing the current state of resources.
*
* @param apkFile The apk file to patch.
* @param apkFilesPath The path to the temporary apk files directory.
* @param patchedFilesPath The path to the temporary patched files directory.
* @param aaptBinaryPath The path to a custom aapt binary.
* @param frameworkFileDirectory The path to the directory to cache the framework file in.
*/
class ResourcePatchContext internal constructor(
private val apkFile: File,
private val apkFilesPath: File,
private val patchedFilesPath: File,
aaptBinaryPath: File? = null,
frameworkFileDirectory: String? = null,
) : PatchContext<PatchesResult.PatchedResources?> {
private val apkInfo = ApkInfo(ExtFile(apkFile))
private val logger = Logger.getLogger(ResourcePatchContext::class.jvmName)
private val resourceConfig = Config.getDefaultConfig().apply {
aaptBinary = aaptBinaryPath
frameworkDirectory = frameworkFileDirectory
}
internal var decodingMode = ResourceDecodingMode.MANIFEST
/**
* Read a document from an [InputStream].
*/
fun document(inputStream: InputStream) = Document(inputStream)
/**
* Read and write documents in the [apkFile].
*/
fun document(path: String) = Document(get(path))
/**
* Set of resources from [apkFile] to delete.
*/
private val deleteResources = mutableSetOf<String>()
internal fun decodeManifest(): Pair<PackageName, VersionName> {
logger.info("Decoding manifest")
val resourcesDecoder = ResourcesDecoder(resourceConfig, apkInfo)
// Decode manually instead of using resourceDecoder.decodeManifest
// because it does not support decoding to an OutputStream.
AndroidManifestPullStreamDecoder(
AndroidManifestResourceParser(resourcesDecoder.resTable),
resourcesDecoder.newXmlSerializer(),
).decode(
apkInfo.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.
// The call to AndroidManifestPullStreamDecoder.decode() above sets apkInfo.
val packageName = resourcesDecoder.resTable.packageRenamed
val packageVersion =
apkInfo.versionInfo.versionName ?: apkInfo.versionInfo.versionCode
/*
When the main resource package is not loaded, the ResTable is flagged as sparse.
Because ResourcesDecoder.decodeResources loads the main package and is not called here,
set sparseResources to false again to prevent the ResTable from being flagged as sparse falsely,
in case ResourcesDecoder.decodeResources is not later used in the patching process
to set sparseResources correctly.
See ARSCDecoder.readTableType for more info.
*/
apkInfo.sparseResources = false
return packageName to packageVersion
}
internal fun decodeResources() {
logger.info("Decoding resources")
val resourcesDecoder = ResourcesDecoder(resourceConfig, apkInfo).also {
it.decodeResources(apkFilesPath)
it.decodeManifest(apkFilesPath)
}
// Record uncompressed files to preserve their state when recompiling.
ApkDecoder(apkInfo, resourceConfig).recordUncompressedFiles(resourcesDecoder.resFileMapping)
// Get the ids of the used framework packages to include them for reference when recompiling.
apkInfo.usesFramework = UsesFramework().apply {
ids = resourcesDecoder.resTable.listFramePackages().map { it.id }
}
}
/**
* Compile resources in [apkFilesPath].
*
* @return The [PatchesResult.PatchedResources].
*/
override fun get(): PatchesResult.PatchedResources {
logger.info("Compiling patched resources")
val resourcesPath = patchedFilesPath.resolve("resources").also { it.mkdirs() }
val resourcesApkFile = if (decodingMode == ResourceDecodingMode.ALL) {
val resourcesApkFile = resourcesPath.resolve("resources.apk").also { it.createNewFile() }
val manifestFile = apkFilesPath.resolve("AndroidManifest.xml").also {
ResXmlUtils.fixingPublicAttrsInProviderAttributes(it)
}
val resPath = apkFilesPath.resolve("res")
val frameworkApkFiles = with(Framework(resourceConfig)) {
apkInfo.usesFramework.ids.map { id -> getFrameworkApk(id, null) }
}.toTypedArray()
AaptInvoker(
resourceConfig,
apkInfo
).invoke(resourcesApkFile, manifestFile, resPath, null, null, frameworkApkFiles)
resourcesApkFile
} else null
val otherFiles = apkFilesPath.listFiles()!!.filter {
// Excluded because present in resources.other.
// TODO: We are reusing apkFiles as a temporarily directory for extracting resources.
// This is not ideal as it could conflict with files such as the ones that are filtered here.
// The problem is that ResourcePatchContext#get returns a File relative to apkFiles,
// and we need to extract files to that directory.
// A solution would be to use 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.
resourcesPath.resolve("other").also { it.mkdirs() }.apply {
otherFiles.forEach { file ->
Files.move(file.toPath(), resolve(file.name).toPath())
}
}
} else null
return PatchesResult.PatchedResources(
resourcesApkFile,
otherResourceFiles,
apkInfo.doNotCompress?.toSet() ?: emptySet(),
deleteResources,
)
}
/**
* Get a file from [apkFilesPath].
*
* @param path The path of the file.
* @param copy Whether to copy the file from [apkFile] if it does not exist yet in [apkFilesPath].
*/
operator fun get(
path: String,
copy: Boolean = true,
) = apkFilesPath.resolve(path).apply {
if (copy && !exists()) {
with(ExtFile(apkFile).directory) {
if (containsFile(path) || containsDir(path)) {
copyToDir(apkFilesPath, path)
}
}
}
}
/**
* Mark a file for deletion when the APK is rebuilt.
*
* @param name The name of the file to delete.
*/
fun delete(name: String) = deleteResources.add(name)
/**
* How to handle resources decoding and compiling.
*/
internal enum class ResourceDecodingMode {
/**
* Decode and compile all resources.
*/
ALL,
/**
* Do not decode or compile any resources.
*/
NONE,
/**
* Do not decode or compile any resources.
*/
MANIFEST,
}
}

View File

@@ -1,5 +1,11 @@
package app.revanced.patcher.util
import com.android.tools.smali.dexlib2.mutable.MutableClassDef
import com.android.tools.smali.dexlib2.mutable.MutableClassDef.Companion.toMutable
import com.android.tools.smali.dexlib2.mutable.MutableField
import com.android.tools.smali.dexlib2.mutable.MutableField.Companion.toMutable
import com.android.tools.smali.dexlib2.mutable.MutableMethod
import com.android.tools.smali.dexlib2.mutable.MutableMethod.Companion.toMutable
import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patcher.util.ClassMerger.Utils.asMutableClass
import app.revanced.patcher.util.ClassMerger.Utils.filterAny
@@ -7,24 +13,19 @@ import app.revanced.patcher.util.ClassMerger.Utils.filterNotAny
import app.revanced.patcher.util.ClassMerger.Utils.isPublic
import app.revanced.patcher.util.ClassMerger.Utils.toPublic
import app.revanced.patcher.util.ClassMerger.Utils.traverseClassHierarchy
import app.revanced.patcher.util.proxy.mutableTypes.MutableClass
import app.revanced.patcher.util.proxy.mutableTypes.MutableClass.Companion.toMutable
import app.revanced.patcher.util.proxy.mutableTypes.MutableField
import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.iface.ClassDef
import com.android.tools.smali.dexlib2.util.MethodUtil
import java.util.logging.Logger
import kotlin.reflect.KFunction2
import kotlin.reflect.jvm.jvmName
/**
* Experimental class to merge a [ClassDef] with another.
* Note: This will not consider method implementations or if the class is missing a superclass or interfaces.
*/
internal object ClassMerger {
private val logger = Logger.getLogger(ClassMerger::class.java.name)
private val logger = Logger.getLogger(ClassMerger::class.jvmName)
/**
* Merge a class with [otherClass].
@@ -175,18 +176,17 @@ internal object ClassMerger {
* @param callback function that is called for every class in the hierarchy
*/
fun BytecodePatchContext.traverseClassHierarchy(
targetClass: MutableClass,
callback: MutableClass.() -> Unit,
targetClass: MutableClassDef,
callback: MutableClassDef.() -> Unit,
) {
callback(targetClass)
targetClass.superclass ?: return
this.classBy { targetClass.superclass == it.type }?.mutableClass?.let {
traverseClassHierarchy(it, callback)
classDefs[targetClass.superclass ?: return]?.let { classDef ->
traverseClassHierarchy(classDefs.getOrReplaceMutable(classDef), callback)
}
}
fun ClassDef.asMutableClass() = if (this is MutableClass) this else this.toMutable()
fun ClassDef.asMutableClass() = this as? MutableClassDef ?: this.toMutable()
/**
* Check if the [AccessFlags.PUBLIC] flag is set.

View File

@@ -1,14 +1,19 @@
package app.revanced.patcher.util
import collections.merge
import com.google.common.base.Charsets
import org.w3c.dom.Document
import java.io.Closeable
import java.io.File
import java.io.InputStream
import java.io.bufferedWriter
import java.io.inputStream
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.transform.OutputKeys
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult
import kotlin.use
class Document internal constructor(
inputStream: InputStream,
@@ -29,7 +34,7 @@ class Document internal constructor(
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.",
"To be able to close this instance, no other instances may be reading $it at the same time.",
)
} else {
readerCount.remove(it)
@@ -40,9 +45,9 @@ class Document internal constructor(
if (isAndroid) {
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-16")
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes")
it.bufferedWriter(charset = Charsets.UTF_8).use { writer ->
transformer.transform(DOMSource(this), StreamResult(writer))
}
val writer = it.bufferedWriter(charset = Charsets.UTF_8)
transformer.transform(DOMSource(this), StreamResult(writer))
writer.close()
} else {
transformer.transform(DOMSource(this), StreamResult(it))
}

View File

@@ -2,10 +2,9 @@
package app.revanced.patcher.util
import app.revanced.patcher.extensions.InstructionExtensions.instructionsOrNull
import com.android.tools.smali.dexlib2.mutable.MutableMethod
import app.revanced.patcher.extensions.instructionsOrNull
import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patcher.util.MethodNavigator.NavigateException
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import com.android.tools.smali.dexlib2.iface.ClassDef
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.instruction.Instruction
@@ -24,12 +23,12 @@ import kotlin.reflect.KProperty
* @throws NavigateException If the method does not have an implementation.
* @throws NavigateException If the instruction at the specified index is not a method reference.
*/
context(BytecodePatchContext)
class MethodNavigator internal constructor(
private var startMethod: MethodReference,
) {
private var lastNavigatedMethodReference = startMethod
context(_: BytecodePatchContext)
private val lastNavigatedMethodInstructions
get() = with(original()) {
instructionsOrNull ?: throw NavigateException("Method $this does not have an implementation.")
@@ -42,6 +41,7 @@ class MethodNavigator internal constructor(
*
* @return This [MethodNavigator].
*/
context(_: BytecodePatchContext)
fun to(vararg index: Int): MethodNavigator {
index.forEach {
lastNavigatedMethodReference = lastNavigatedMethodInstructions.getMethodReferenceAt(it)
@@ -56,6 +56,7 @@ class MethodNavigator internal constructor(
* @param index The index of the method to navigate to.
* @param predicate The predicate to match.
*/
context(_: BytecodePatchContext)
fun to(index: Int = 0, predicate: (Instruction) -> Boolean): MethodNavigator {
lastNavigatedMethodReference = lastNavigatedMethodInstructions.asSequence()
.filter(predicate).asIterable().getMethodReferenceAt(index)
@@ -80,29 +81,26 @@ class MethodNavigator internal constructor(
*
* @return The last navigated method mutably.
*/
fun stop() = classBy(matchesCurrentMethodReferenceDefiningClass)!!.mutableClass.firstMethodBySignature
as MutableMethod
context(context: BytecodePatchContext)
fun stop() = context.classDefs[lastNavigatedMethodReference.definingClass]!!
.firstMethodBySignature as MutableMethod
/**
* Get the last navigated method mutably.
*
* @return The last navigated method mutably.
*/
operator fun getValue(nothing: Nothing?, property: KProperty<*>) = stop()
operator fun getValue(context: BytecodePatchContext?, property: KProperty<*>) =
context(requireNotNull(context)) { stop() }
/**
* Get the last navigated method immutably.
*
* @return The last navigated method immutably.
*/
fun original(): Method = classes.first(matchesCurrentMethodReferenceDefiningClass).firstMethodBySignature
/**
* Predicate to match the class defining the current method reference.
*/
private val matchesCurrentMethodReferenceDefiningClass = { classDef: ClassDef ->
classDef.type == lastNavigatedMethodReference.definingClass
}
context(context: BytecodePatchContext)
fun original(): Method = context.classDefs[lastNavigatedMethodReference.definingClass]!!.firstMethodBySignature
/**
* Find the first [lastNavigatedMethodReference] in the class.

View File

@@ -0,0 +1,13 @@
package collections
internal expect fun <K, V> MutableMap<K, V>.kmpMerge(
key: K,
value: V,
remappingFunction: (oldValue: V, newValue: V) -> V,
)
internal fun <K, V> MutableMap<K, V>.merge(
key: K,
value: V,
remappingFunction: (oldValue: V, newValue: V) -> V,
) = kmpMerge(key, value, remappingFunction)

View File

@@ -0,0 +1,26 @@
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.base.value.BaseAnnotationEncodedValue
import com.android.tools.smali.dexlib2.mutable.MutableAnnotationElement.Companion.toMutable
class MutableAnnotationEncodedValue(annotationEncodedValue: AnnotationEncodedValue) :
BaseAnnotationEncodedValue(),
MutableEncodedValue {
private var type = annotationEncodedValue.type
private val _elements by lazy {
annotationEncodedValue.elements.map { annotationElement -> annotationElement.toMutable() }.toMutableSet()
}
fun setType(type: String) {
this.type = type
}
override fun getType() = this.type
override fun getElements() = _elements
companion object {
fun AnnotationEncodedValue.toMutable() = MutableAnnotationEncodedValue(this)
}
}

View File

@@ -0,0 +1,16 @@
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.base.value.BaseArrayEncodedValue
import com.android.tools.smali.dexlib2.iface.value.MutableEncodedValue.Companion.toMutable
class MutableArrayEncodedValue(arrayEncodedValue: ArrayEncodedValue) : BaseArrayEncodedValue(), MutableEncodedValue {
private val _value by lazy {
arrayEncodedValue.value.map { encodedValue -> encodedValue.toMutable() }.toMutableList()
}
override fun getValue() = _value
companion object {
fun ArrayEncodedValue.toMutable(): MutableArrayEncodedValue = MutableArrayEncodedValue(this)
}
}

View File

@@ -1,19 +1,18 @@
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.base.value.BaseBooleanEncodedValue
import com.android.tools.smali.dexlib2.iface.value.BooleanEncodedValue
class MutableBooleanEncodedValue(booleanEncodedValue: BooleanEncodedValue) :
BaseBooleanEncodedValue(),
MutableEncodedValue {
private var value = booleanEncodedValue.value
override fun getValue(): Boolean = this.value
fun setValue(value: Boolean) {
this.value = value
}
override fun getValue(): Boolean = this.value
companion object {
fun BooleanEncodedValue.toMutable(): MutableBooleanEncodedValue = MutableBooleanEncodedValue(this)
}

View File

@@ -1,19 +1,16 @@
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.base.value.BaseByteEncodedValue
import com.android.tools.smali.dexlib2.iface.value.ByteEncodedValue
class MutableByteEncodedValue(byteEncodedValue: ByteEncodedValue) :
BaseByteEncodedValue(),
MutableEncodedValue {
class MutableByteEncodedValue(byteEncodedValue: ByteEncodedValue) : BaseByteEncodedValue(), MutableEncodedValue {
private var value = byteEncodedValue.value
override fun getValue(): Byte = this.value
fun setValue(value: Byte) {
this.value = value
}
override fun getValue(): Byte = this.value
companion object {
fun ByteEncodedValue.toMutable(): MutableByteEncodedValue = MutableByteEncodedValue(this)
}

View File

@@ -1,19 +1,16 @@
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.base.value.BaseCharEncodedValue
import com.android.tools.smali.dexlib2.iface.value.CharEncodedValue
class MutableCharEncodedValue(charEncodedValue: CharEncodedValue) :
BaseCharEncodedValue(),
MutableEncodedValue {
class MutableCharEncodedValue(charEncodedValue: CharEncodedValue) : BaseCharEncodedValue(), MutableEncodedValue {
private var value = charEncodedValue.value
override fun getValue(): Char = this.value
fun setValue(value: Char) {
this.value = value
}
override fun getValue(): Char = this.value
companion object {
fun CharEncodedValue.toMutable(): MutableCharEncodedValue = MutableCharEncodedValue(this)
}

View File

@@ -1,19 +1,18 @@
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.base.value.BaseDoubleEncodedValue
import com.android.tools.smali.dexlib2.iface.value.DoubleEncodedValue
class MutableDoubleEncodedValue(doubleEncodedValue: DoubleEncodedValue) :
BaseDoubleEncodedValue(),
MutableEncodedValue {
private var value = doubleEncodedValue.value
override fun getValue(): Double = this.value
fun setValue(value: Double) {
this.value = value
}
override fun getValue(): Double = this.value
companion object {
fun DoubleEncodedValue.toMutable(): MutableDoubleEncodedValue = MutableDoubleEncodedValue(this)
}

View File

@@ -1,7 +1,6 @@
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.ValueType
import com.android.tools.smali.dexlib2.iface.value.*
interface MutableEncodedValue : EncodedValue {
companion object {

View File

@@ -1,20 +1,17 @@
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.base.value.BaseEnumEncodedValue
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
import com.android.tools.smali.dexlib2.iface.value.EnumEncodedValue
class MutableEnumEncodedValue(enumEncodedValue: EnumEncodedValue) :
BaseEnumEncodedValue(),
MutableEncodedValue {
class MutableEnumEncodedValue(enumEncodedValue: EnumEncodedValue) : BaseEnumEncodedValue(), MutableEncodedValue {
private var value = enumEncodedValue.value
override fun getValue(): FieldReference = this.value
fun setValue(value: FieldReference) {
this.value = value
}
override fun getValue(): FieldReference = this.value
companion object {
fun EnumEncodedValue.toMutable(): MutableEnumEncodedValue = MutableEnumEncodedValue(this)
}

View File

@@ -1,23 +1,20 @@
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.ValueType
import com.android.tools.smali.dexlib2.base.value.BaseFieldEncodedValue
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
import com.android.tools.smali.dexlib2.iface.value.FieldEncodedValue
class MutableFieldEncodedValue(fieldEncodedValue: FieldEncodedValue) :
BaseFieldEncodedValue(),
MutableEncodedValue {
class MutableFieldEncodedValue(fieldEncodedValue: FieldEncodedValue) : BaseFieldEncodedValue(), MutableEncodedValue {
private var value = fieldEncodedValue.value
override fun getValueType(): Int = ValueType.FIELD
override fun getValue(): FieldReference = this.value
fun setValue(value: FieldReference) {
this.value = value
}
override fun getValueType(): Int = ValueType.FIELD
override fun getValue(): FieldReference = this.value
companion object {
fun FieldEncodedValue.toMutable(): MutableFieldEncodedValue = MutableFieldEncodedValue(this)
}

View File

@@ -1,19 +1,16 @@
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.base.value.BaseFloatEncodedValue
import com.android.tools.smali.dexlib2.iface.value.FloatEncodedValue
class MutableFloatEncodedValue(floatEncodedValue: FloatEncodedValue) :
BaseFloatEncodedValue(),
MutableEncodedValue {
class MutableFloatEncodedValue(floatEncodedValue: FloatEncodedValue) : BaseFloatEncodedValue(), MutableEncodedValue {
private var value = floatEncodedValue.value
override fun getValue(): Float = this.value
fun setValue(value: Float) {
this.value = value
}
override fun getValue(): Float = this.value
companion object {
fun FloatEncodedValue.toMutable(): MutableFloatEncodedValue = MutableFloatEncodedValue(this)
}

View File

@@ -1,19 +1,16 @@
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.base.value.BaseIntEncodedValue
import com.android.tools.smali.dexlib2.iface.value.IntEncodedValue
class MutableIntEncodedValue(intEncodedValue: IntEncodedValue) :
BaseIntEncodedValue(),
MutableEncodedValue {
class MutableIntEncodedValue(intEncodedValue: IntEncodedValue) : BaseIntEncodedValue(), MutableEncodedValue {
private var value = intEncodedValue.value
override fun getValue(): Int = this.value
fun setValue(value: Int) {
this.value = value
}
override fun getValue(): Int = this.value
companion object {
fun IntEncodedValue.toMutable(): MutableIntEncodedValue = MutableIntEncodedValue(this)
}

View File

@@ -1,19 +1,16 @@
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.base.value.BaseLongEncodedValue
import com.android.tools.smali.dexlib2.iface.value.LongEncodedValue
class MutableLongEncodedValue(longEncodedValue: LongEncodedValue) :
BaseLongEncodedValue(),
MutableEncodedValue {
class MutableLongEncodedValue(longEncodedValue: LongEncodedValue) : BaseLongEncodedValue(), MutableEncodedValue {
private var value = longEncodedValue.value
override fun getValue(): Long = this.value
fun setValue(value: Long) {
this.value = value
}
override fun getValue(): Long = this.value
companion object {
fun LongEncodedValue.toMutable(): MutableLongEncodedValue = MutableLongEncodedValue(this)
}

View File

@@ -1,20 +1,19 @@
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.base.value.BaseMethodEncodedValue
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
import com.android.tools.smali.dexlib2.iface.value.MethodEncodedValue
class MutableMethodEncodedValue(methodEncodedValue: MethodEncodedValue) :
BaseMethodEncodedValue(),
MutableEncodedValue {
private var value = methodEncodedValue.value
override fun getValue(): MethodReference = this.value
fun setValue(value: MethodReference) {
this.value = value
}
override fun getValue(): MethodReference = this.value
companion object {
fun MethodEncodedValue.toMutable(): MutableMethodEncodedValue = MutableMethodEncodedValue(this)
}

View File

@@ -1,20 +1,19 @@
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.base.value.BaseMethodHandleEncodedValue
import com.android.tools.smali.dexlib2.iface.reference.MethodHandleReference
import com.android.tools.smali.dexlib2.iface.value.MethodHandleEncodedValue
class MutableMethodHandleEncodedValue(methodHandleEncodedValue: MethodHandleEncodedValue) :
BaseMethodHandleEncodedValue(),
MutableEncodedValue {
private var value = methodHandleEncodedValue.value
override fun getValue(): MethodHandleReference = this.value
fun setValue(value: MethodHandleReference) {
this.value = value
}
override fun getValue(): MethodHandleReference = this.value
companion object {
fun MethodHandleEncodedValue.toMutable(): MutableMethodHandleEncodedValue = MutableMethodHandleEncodedValue(this)
}

View File

@@ -1,20 +1,17 @@
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.base.value.BaseMethodTypeEncodedValue
import com.android.tools.smali.dexlib2.iface.reference.MethodProtoReference
import com.android.tools.smali.dexlib2.iface.value.MethodTypeEncodedValue
class MutableMethodTypeEncodedValue(methodTypeEncodedValue: MethodTypeEncodedValue) :
BaseMethodTypeEncodedValue(),
MutableEncodedValue {
class MutableMethodTypeEncodedValue(methodTypeEncodedValue: MethodTypeEncodedValue) : BaseMethodTypeEncodedValue(), MutableEncodedValue {
private var value = methodTypeEncodedValue.value
override fun getValue(): MethodProtoReference = this.value
fun setValue(value: MethodProtoReference) {
this.value = value
}
override fun getValue(): MethodProtoReference = this.value
companion object {
fun MethodTypeEncodedValue.toMutable(): MutableMethodTypeEncodedValue = MutableMethodTypeEncodedValue(this)
}

View File

@@ -0,0 +1,9 @@
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.base.value.BaseNullEncodedValue
class MutableNullEncodedValue : BaseNullEncodedValue(), MutableEncodedValue {
companion object {
fun ByteEncodedValue.toMutable(): MutableByteEncodedValue = MutableByteEncodedValue(this)
}
}

View File

@@ -1,19 +1,18 @@
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.base.value.BaseShortEncodedValue
import com.android.tools.smali.dexlib2.iface.value.ShortEncodedValue
class MutableShortEncodedValue(shortEncodedValue: ShortEncodedValue) :
BaseShortEncodedValue(),
MutableEncodedValue {
private var value = shortEncodedValue.value
override fun getValue(): Short = this.value
fun setValue(value: Short) {
this.value = value
}
override fun getValue(): Short = this.value
companion object {
fun ShortEncodedValue.toMutable(): MutableShortEncodedValue = MutableShortEncodedValue(this)
}

View File

@@ -1,20 +1,18 @@
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.base.value.BaseStringEncodedValue
import com.android.tools.smali.dexlib2.iface.value.ByteEncodedValue
import com.android.tools.smali.dexlib2.iface.value.StringEncodedValue
class MutableStringEncodedValue(stringEncodedValue: StringEncodedValue) :
BaseStringEncodedValue(),
MutableEncodedValue {
private var value = stringEncodedValue.value
override fun getValue(): String = this.value
fun setValue(value: String) {
this.value = value
}
override fun getValue(): String = this.value
companion object {
fun ByteEncodedValue.toMutable(): MutableByteEncodedValue = MutableByteEncodedValue(this)
}

View File

@@ -1,19 +1,16 @@
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.base.value.BaseTypeEncodedValue
import com.android.tools.smali.dexlib2.iface.value.TypeEncodedValue
class MutableTypeEncodedValue(typeEncodedValue: TypeEncodedValue) :
BaseTypeEncodedValue(),
MutableEncodedValue {
class MutableTypeEncodedValue(typeEncodedValue: TypeEncodedValue) : BaseTypeEncodedValue(), MutableEncodedValue {
private var value = typeEncodedValue.value
override fun getValue(): String = this.value
fun setValue(value: String) {
this.value = value
}
override fun getValue() = this.value
companion object {
fun TypeEncodedValue.toMutable(): MutableTypeEncodedValue = MutableTypeEncodedValue(this)
}

View File

@@ -1,21 +1,21 @@
package app.revanced.patcher.util.proxy.mutableTypes
package com.android.tools.smali.dexlib2.mutable
import app.revanced.patcher.util.proxy.mutableTypes.MutableAnnotationElement.Companion.toMutable
import com.android.tools.smali.dexlib2.base.BaseAnnotation
import com.android.tools.smali.dexlib2.iface.Annotation
import com.android.tools.smali.dexlib2.mutable.MutableAnnotationElement.Companion.toMutable
class MutableAnnotation(annotation: Annotation) : BaseAnnotation() {
private val visibility = annotation.visibility
private val type = annotation.type
private val _elements by lazy { annotation.elements.map { element -> element.toMutable() }.toMutableSet() }
override fun getType(): String = type
override fun getType() = type
override fun getElements(): MutableSet<MutableAnnotationElement> = _elements
override fun getElements() = _elements
override fun getVisibility(): Int = visibility
override fun getVisibility() = visibility
companion object {
fun Annotation.toMutable(): MutableAnnotation = MutableAnnotation(this)
fun Annotation.toMutable() = MutableAnnotation(this)
}
}

View File

@@ -1,10 +1,9 @@
package app.revanced.patcher.util.proxy.mutableTypes
package com.android.tools.smali.dexlib2.mutable
import app.revanced.patcher.util.proxy.mutableTypes.encodedValue.MutableEncodedValue
import app.revanced.patcher.util.proxy.mutableTypes.encodedValue.MutableEncodedValue.Companion.toMutable
import com.android.tools.smali.dexlib2.base.BaseAnnotationElement
import com.android.tools.smali.dexlib2.iface.AnnotationElement
import com.android.tools.smali.dexlib2.iface.value.EncodedValue
import com.android.tools.smali.dexlib2.iface.value.MutableEncodedValue
import com.android.tools.smali.dexlib2.iface.value.MutableEncodedValue.Companion.toMutable
class MutableAnnotationElement(annotationElement: AnnotationElement) : BaseAnnotationElement() {
private var name = annotationElement.name
@@ -18,11 +17,11 @@ class MutableAnnotationElement(annotationElement: AnnotationElement) : BaseAnnot
this.value = value
}
override fun getName(): String = name
override fun getName() = name
override fun getValue(): EncodedValue = value
override fun getValue() = value
companion object {
fun AnnotationElement.toMutable(): MutableAnnotationElement = MutableAnnotationElement(this)
fun AnnotationElement.toMutable() = MutableAnnotationElement(this)
}
}

View File

@@ -1,16 +1,14 @@
package app.revanced.patcher.util.proxy.mutableTypes
package com.android.tools.smali.dexlib2.mutable
import app.revanced.patcher.util.proxy.mutableTypes.MutableAnnotation.Companion.toMutable
import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
import com.android.tools.smali.dexlib2.base.reference.BaseTypeReference
import com.android.tools.smali.dexlib2.iface.ClassDef
import com.android.tools.smali.dexlib2.mutable.MutableAnnotation.Companion.toMutable
import com.android.tools.smali.dexlib2.mutable.MutableField.Companion.toMutable
import com.android.tools.smali.dexlib2.mutable.MutableMethod.Companion.toMutable
import com.android.tools.smali.dexlib2.util.FieldUtil
import com.android.tools.smali.dexlib2.util.MethodUtil
class MutableClass(classDef: ClassDef) :
BaseTypeReference(),
ClassDef {
class MutableClassDef(classDef: ClassDef) : BaseTypeReference(), ClassDef {
// Class
private var type = classDef.type
private var sourceFile = classDef.sourceFile
@@ -24,8 +22,8 @@ class MutableClass(classDef: ClassDef) :
// Methods
private val _methods by lazy { classDef.methods.map { method -> method.toMutable() }.toMutableSet() }
private val _directMethods by lazy { _methods.filter { method -> MethodUtil.isDirect(method) }.toMutableSet() }
private val _virtualMethods by lazy { _methods.filter { method -> !MethodUtil.isDirect(method) }.toMutableSet() }
private val _directMethods by lazy { methods.filter { method -> MethodUtil.isDirect(method) }.toMutableSet() }
private val _virtualMethods by lazy { methods.filter { method -> !MethodUtil.isDirect(method) }.toMutableSet() }
// Fields
private val _fields by lazy { classDef.fields.map { field -> field.toMutable() }.toMutableSet() }
@@ -48,31 +46,31 @@ class MutableClass(classDef: ClassDef) :
this.superclass = superclass
}
override fun getType(): String = type
override fun getType() = type
override fun getAccessFlags(): Int = accessFlags
override fun getAccessFlags() = accessFlags
override fun getSourceFile(): String? = sourceFile
override fun getSourceFile() = sourceFile
override fun getSuperclass(): String? = superclass
override fun getSuperclass() = superclass
override fun getInterfaces(): MutableList<String> = _interfaces
override fun getInterfaces() = _interfaces
override fun getAnnotations(): MutableSet<MutableAnnotation> = _annotations
override fun getAnnotations() = _annotations
override fun getStaticFields(): MutableSet<MutableField> = _staticFields
override fun getStaticFields() = _staticFields
override fun getInstanceFields(): MutableSet<MutableField> = _instanceFields
override fun getInstanceFields() = _instanceFields
override fun getFields(): MutableSet<MutableField> = _fields
override fun getFields() = _fields
override fun getDirectMethods(): MutableSet<MutableMethod> = _directMethods
override fun getDirectMethods() = _directMethods
override fun getVirtualMethods(): MutableSet<MutableMethod> = _virtualMethods
override fun getVirtualMethods() = _virtualMethods
override fun getMethods(): MutableSet<MutableMethod> = _methods
override fun getMethods() = _methods
companion object {
fun ClassDef.toMutable(): MutableClass = MutableClass(this)
fun ClassDef.toMutable(): MutableClassDef = MutableClassDef(this)
}
}

View File

@@ -1,15 +1,12 @@
package app.revanced.patcher.util.proxy.mutableTypes
package com.android.tools.smali.dexlib2.mutable
import app.revanced.patcher.util.proxy.mutableTypes.MutableAnnotation.Companion.toMutable
import app.revanced.patcher.util.proxy.mutableTypes.encodedValue.MutableEncodedValue
import app.revanced.patcher.util.proxy.mutableTypes.encodedValue.MutableEncodedValue.Companion.toMutable
import com.android.tools.smali.dexlib2.HiddenApiRestriction
import com.android.tools.smali.dexlib2.base.reference.BaseFieldReference
import com.android.tools.smali.dexlib2.iface.Field
import com.android.tools.smali.dexlib2.iface.value.MutableEncodedValue
import com.android.tools.smali.dexlib2.iface.value.MutableEncodedValue.Companion.toMutable
import com.android.tools.smali.dexlib2.mutable.MutableAnnotation.Companion.toMutable
class MutableField(field: Field) :
BaseFieldReference(),
Field {
class MutableField(field: Field) : BaseFieldReference(), Field {
private var definingClass = field.definingClass
private var name = field.name
private var type = field.type
@@ -39,21 +36,21 @@ class MutableField(field: Field) :
this.initialValue = initialValue
}
override fun getDefiningClass(): String = this.definingClass
override fun getDefiningClass() = this.definingClass
override fun getName(): String = this.name
override fun getName() = this.name
override fun getType(): String = this.type
override fun getType() = this.type
override fun getAnnotations(): MutableSet<MutableAnnotation> = this._annotations
override fun getAnnotations() = this._annotations
override fun getAccessFlags(): Int = this.accessFlags
override fun getAccessFlags() = this.accessFlags
override fun getHiddenApiRestrictions(): MutableSet<HiddenApiRestriction> = this._hiddenApiRestrictions
override fun getHiddenApiRestrictions() = this._hiddenApiRestrictions
override fun getInitialValue(): MutableEncodedValue? = this.initialValue
override fun getInitialValue() = this.initialValue
companion object {
fun Field.toMutable(): MutableField = MutableField(this)
fun Field.toMutable() = MutableField(this)
}
}

View File

@@ -0,0 +1,63 @@
package com.android.tools.smali.dexlib2.mutable
import com.android.tools.smali.dexlib2.base.reference.BaseMethodReference
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.mutable.MutableAnnotation.Companion.toMutable
import com.android.tools.smali.dexlib2.mutable.MutableMethodParameter.Companion.toMutable
class MutableMethod(method: Method) : BaseMethodReference(), Method {
private var definingClass = method.definingClass
private var name = method.name
private var accessFlags = method.accessFlags
private var returnType = method.returnType
// TODO: Create own mutable MethodImplementation (due to not being able to change members like register count).
private var implementation = method.implementation?.let(::MutableMethodImplementation)
private val _annotations by lazy { method.annotations.map { annotation -> annotation.toMutable() }.toMutableSet() }
private val _parameters by lazy { method.parameters.map { parameter -> parameter.toMutable() }.toMutableList() }
private val _parameterTypes by lazy { method.parameterTypes.toMutableList() }
private val _hiddenApiRestrictions by lazy { method.hiddenApiRestrictions }
fun setDefiningClass(definingClass: String) {
this.definingClass = definingClass
}
fun setName(name: String) {
this.name = name
}
fun setAccessFlags(accessFlags: Int) {
this.accessFlags = accessFlags
}
fun setReturnType(returnType: String) {
this.returnType = returnType
}
fun setImplementation(implementation: MutableMethodImplementation?) {
this.implementation = implementation
}
override fun getDefiningClass() = definingClass
override fun getName() = name
override fun getParameterTypes() = _parameterTypes
override fun getReturnType() = returnType
override fun getAnnotations() = _annotations
override fun getAccessFlags() = accessFlags
override fun getHiddenApiRestrictions() = _hiddenApiRestrictions
override fun getParameters() = _parameters
override fun getImplementation() = implementation
companion object {
fun Method.toMutable() = MutableMethod(this)
}
}

View File

@@ -0,0 +1,26 @@
package com.android.tools.smali.dexlib2.mutable
import com.android.tools.smali.dexlib2.base.BaseMethodParameter
import com.android.tools.smali.dexlib2.iface.MethodParameter
import com.android.tools.smali.dexlib2.mutable.MutableAnnotation.Companion.toMutable
class MutableMethodParameter(parameter: MethodParameter) : BaseMethodParameter(), MethodParameter {
private var type = parameter.type
private var name = parameter.name
private var signature = parameter.signature
private val _annotations by lazy {
parameter.annotations.map { annotation -> annotation.toMutable() }.toMutableSet()
}
override fun getType() = type
override fun getName() = name
override fun getSignature() = signature
override fun getAnnotations() = _annotations
companion object {
fun MethodParameter.toMutable() = MutableMethodParameter(this)
}
}

View File

@@ -0,0 +1,17 @@
package java.io
internal expect fun File.kmpResolve(child: String): File
internal fun File.resolve(child: String) = kmpResolve(child)
internal expect fun File.kmpDeleteRecursively(): Boolean
internal fun File.deleteRecursively() = kmpDeleteRecursively()
internal expect fun File.kmpInputStream(): InputStream
internal fun File.inputStream() = kmpInputStream()
internal expect fun File.kmpBufferedWriter(charset: java.nio.charset.Charset): BufferedWriter
internal fun File.bufferedWriter(charset: java.nio.charset.Charset) = kmpBufferedWriter(charset)

View File

@@ -0,0 +1,32 @@
package app.revanced.patcher.patch
import java.io.File
import java.net.URLClassLoader
import java.util.jar.JarFile
actual val Class<*>.isPatch get() = Patch::class.java.isAssignableFrom(this)
/**
* Loads patches from JAR files declared as public static fields
* or returned by public static and non-parametrized methods.
* Patches with no name are not loaded. If a patches file fails to load,
* the [onFailedToLoad] callback is invoked with the file and the throwable
* and the loading continues for the other files.
*
* @param patchesFiles The JAR files to load the patches from.
* @param onFailedToLoad A callback invoked when a patches file fails to load.
*
* @return The loaded patches.
*/
actual fun loadPatches(
vararg patchesFiles: File,
onFailedToLoad: (patchesFile: File, throwable: Throwable) -> Unit,
) = loadPatches(
patchesFiles = patchesFiles,
{ file ->
JarFile(file).entries().toList().filter { it.name.endsWith(".class") }
.map { it.name.substringBeforeLast('.').replace('/', '.') }
},
URLClassLoader(patchesFiles.map { it.toURI().toURL() }.toTypedArray()),
onFailedToLoad = onFailedToLoad
)

View File

@@ -0,0 +1,7 @@
package collections
internal actual fun <K, V> MutableMap<K, V>.kmpMerge(
key: K,
value: V,
remappingFunction: (oldValue: V, newValue: V) -> V
) = merge(key, value, remappingFunction)

View File

@@ -0,0 +1,9 @@
package java.io
import java.nio.charset.Charset
internal actual fun File.kmpResolve(child: String) = resolve(child)
internal actual fun File.kmpDeleteRecursively() = deleteRecursively()
internal actual fun File.kmpInputStream() = inputStream()
internal actual fun File.kmpBufferedWriter(charset: Charset) = bufferedWriter(charset)

View File

@@ -0,0 +1,27 @@
package app.revanced.patcher
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.TestInstance
import kotlin.test.Test
import kotlin.test.assertNotNull
import kotlin.test.assertNull
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class FingerprintTest : PatcherTestBase() {
@BeforeAll
fun setup() = setupMock()
@Test
fun `matches fingerprints correctly`() {
with(bytecodePatchContext) {
assertNotNull(
fingerprint { returns("V") }.originalMethodOrNull,
"Fingerprints should match correctly."
)
assertNull(
fingerprint { returns("does not exist") }.originalMethodOrNull,
"Fingerprints should match correctly."
)
}
}
}

View File

@@ -0,0 +1,165 @@
package app.revanced.patcher
import app.revanced.patcher.BytecodePatchContextMethodMatching.firstMethod
import app.revanced.patcher.BytecodePatchContextMethodMatching.firstMethodDeclarativelyOrNull
import app.revanced.patcher.patch.bytecodePatch
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction22t
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.assertDoesNotThrow
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class MatchingTest : PatcherTestBase() {
@BeforeAll
fun setup() = setupMock()
@Test
fun `finds via builder api`() {
fun firstMethodComposite(fail: Boolean = false) = firstMethodComposite {
name("method")
definingClass("class")
if (fail) returnType("doesnt exist")
instructions(
at(Opcode.CONST_STRING()),
`is`<TwoRegisterInstruction>(),
noneOf(registers()),
string("test", String::contains),
after(1..3, allOf(Opcode.INVOKE_VIRTUAL(), registers(1, 0))),
allOf(),
type("PrintStream;", String::endsWith)
)
}
with(bytecodePatchContext) {
assertNotNull(firstMethodComposite().methodOrNull) { "Expected to find a method" }
Assertions.assertNull(firstMethodComposite(fail = true).immutableMethodOrNull) { "Expected to not find a method" }
Assertions.assertNotNull(
firstMethodComposite().match(classDefs.first()).methodOrNull
) { "Expected to find a method matching in a specific class" }
}
}
@Test
fun `finds via declarative api`() {
bytecodePatch {
apply {
val method = firstMethodDeclarativelyOrNull {
anyOf {
predicate { name == "method" }
add { false }
}
allOf {
predicate { returnType == "V" }
}
predicate { definingClass == "class" }
}
assertNotNull(method) { "Expected to find a method" }
}
}()
}
@Test
fun `predicate api works correctly`() {
bytecodePatch {
apply {
assertDoesNotThrow("Should find method") { firstMethod { name == "method" } }
}
}
}
@Test
fun `matcher finds indices correctly`() {
val iterable = (1..10).toList()
val matcher = indexedMatcher<Int>()
matcher.apply {
+at<Int> { this > 5 }
}
assertFalse(
matcher(iterable),
"Should not match at any other index than first"
)
matcher.clear()
matcher.apply { +at<Int> { this == 1 } }(iterable)
assertEquals(
listOf(0),
matcher.indices,
"Should match at first index."
)
matcher.clear()
matcher.apply { add { _, _, _ -> this > 0 } }(iterable)
assertEquals(1, matcher.indices.size, "Should only match once.")
matcher.clear()
matcher.apply { add { _, _, _ -> this == 2 } }(iterable)
assertEquals(
listOf(1),
matcher.indices,
"Should find the index correctly."
)
matcher.clear()
matcher.apply {
+at<Int> { this == 1 }
add { _, _, _ -> this == 2 }
add { _, _, _ -> this == 4 }
}(iterable)
assertEquals(
listOf(0, 1, 3),
matcher.indices,
"Should match 1, 2 and 4 at indices 0, 1 and 3."
)
matcher.clear()
matcher.apply {
+after<Int> { this == 1 }
}(iterable)
assertEquals(
listOf(0),
matcher.indices,
"Should match index 0 after nothing"
)
matcher.clear()
matcher.apply {
+after<Int>(2..Int.MAX_VALUE) { this == 1 }
}
assertFalse(
matcher(iterable),
"Should not match, because 1 is out of range"
)
matcher.clear()
matcher.apply {
+after<Int>(1..1) { this == 2 }
}
assertFalse(
matcher(iterable),
"Should not match, because 2 is at index 1"
)
matcher.clear()
matcher.apply {
+at<Int> { this == 1 }
+after<Int>(2..5) { this == 4 }
add { _, _, _ -> this == 8 }
add { _, _, _ -> this == 9 }
}(iterable)
assertEquals(
listOf(0, 3, 7, 8),
matcher.indices,
"Should match indices correctly."
)
}
}

View File

@@ -0,0 +1,80 @@
package app.revanced.patcher
import app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.patch.bytecodePatch
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.assertThrows
import kotlin.test.Test
import kotlin.test.assertEquals
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class PatcherTest : PatcherTestBase() {
@BeforeAll
fun setup() = setupMock()
@Test
fun `applies patches in correct order`() {
val applied = mutableListOf<String>()
infix fun Patch.resultsIn(equals: List<String>) = this to equals
infix fun Pair<Patch, List<String>>.because(reason: String) {
runCatching { setOf(first)() }
assertEquals(second, applied, reason)
applied.clear()
}
bytecodePatch {
dependsOn(
bytecodePatch {
apply { applied += "1" }
afterDependents { applied += "-2" }
},
bytecodePatch { apply { applied += "2" } },
)
apply { applied += "3" }
afterDependents { applied += "-1" }
} resultsIn listOf("1", "2", "3", "-1", "-2") because
"Patches should apply in post-order and afterDependents in pre-order."
bytecodePatch {
dependsOn(
bytecodePatch {
apply { throw PatchException("1") }
afterDependents { applied += "-2" }
},
)
apply { applied += "2" }
afterDependents { applied += "-1" }
} resultsIn emptyList() because
"Patches that depend on a patched that failed to apply should not be applied."
bytecodePatch {
dependsOn(
bytecodePatch {
apply { applied += "1" }
afterDependents { applied += "-2" }
},
)
apply { throw PatchException("2") }
afterDependents { applied += "-1" }
} resultsIn listOf("1", "-2") because
"afterDependents of a patch should not be called if it failed to apply."
bytecodePatch {
dependsOn(
bytecodePatch {
apply { applied += "1" }
afterDependents { applied += "-2" }
},
)
apply { applied += "2" }
afterDependents { throw PatchException("-1") }
} resultsIn listOf("1", "2", "-2") because
"afterDependents of a patch should be called " +
"regardless of dependant patches failing."
}
}

View File

@@ -0,0 +1,105 @@
package app.revanced.patcher
import app.revanced.patcher.extensions.toInstructions
import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.ResourcePatchContext
import com.android.tools.smali.dexlib2.Opcodes
import com.android.tools.smali.dexlib2.iface.DexFile
import com.android.tools.smali.dexlib2.immutable.ImmutableClassDef
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
import com.android.tools.smali.dexlib2.immutable.ImmutableMethodImplementation
import io.mockk.every
import io.mockk.justRun
import io.mockk.mockk
import io.mockk.mockkStatic
import lanchon.multidexlib2.MultiDexIO
import java.io.File
import java.io.InputStream
abstract class PatcherTestBase {
protected lateinit var bytecodePatchContext: BytecodePatchContext
protected lateinit var resourcePatchContext: ResourcePatchContext
protected fun setupMock(
method: ImmutableMethod = ImmutableMethod(
"class",
"method",
emptyList(),
"V",
0,
null,
null,
ImmutableMethodImplementation(
2,
"""
const-string v0, "Hello, World!"
iput-object v0, p0, Ljava/lang/System;->out:Ljava/io/PrintStream;
iget-object v0, p0, Ljava/lang/System;->out:Ljava/io/PrintStream;
return-void
const-string v0, "This is a test."
return-object v0
invoke-virtual { p0, v0 }, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
invoke-static { p0 }, Ljava/lang/System;->currentTimeMillis()J
check-cast p0, Ljava/io/PrintStream;
""".toInstructions(),
null,
null
),
),
) {
resourcePatchContext = mockk<ResourcePatchContext>(relaxed = true)
bytecodePatchContext = mockk<BytecodePatchContext> bytecodePatchContext@{
mockkStatic(MultiDexIO::readDexFile)
every {
MultiDexIO.readDexFile(
any(),
any(),
any(),
any(),
any()
)
} returns mockk<DexFile> {
every { classes } returns mutableSetOf(
ImmutableClassDef(
"class",
0,
null,
null,
null,
null,
null,
listOf(method),
)
)
every { opcodes } returns Opcodes.getDefault()
}
every { this@bytecodePatchContext.getProperty("apkFile") } returns mockk<File>()
every { this@bytecodePatchContext.classDefs } returns ClassDefs().apply {
javaClass.getDeclaredMethod($$"initializeCache$patcher").apply {
isAccessible = true
}.invoke(this)
}
every { get() } returns emptySet()
justRun { this@bytecodePatchContext["extendWith"](any<InputStream>()) }
}
}
protected operator fun Set<Patch>.invoke() {
runCatching {
apply(
bytecodePatchContext,
resourcePatchContext
) { }
}.fold(
{ it.dexFiles },
{ it.printStackTrace() }
)
}
protected operator fun Patch.invoke() = setOf(this)()
}

View File

@@ -1,16 +1,5 @@
package app.revanced.patcher.extensions
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction
import app.revanced.patcher.extensions.InstructionExtensions.removeInstructions
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstructions
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
import app.revanced.patcher.util.smali.ExternalLabel
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.builder.BuilderOffsetInstruction
@@ -18,28 +7,30 @@ import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21s
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
import com.android.tools.smali.dexlib2.mutable.MutableMethod
import com.android.tools.smali.dexlib2.mutable.MutableMethod.Companion.toMutable
import org.junit.jupiter.api.BeforeEach
import kotlin.test.Test
import kotlin.test.assertEquals
private object InstructionExtensionsTest {
private lateinit var testMethod: MutableMethod
private lateinit var testMethodImplementation: MutableMethodImplementation
internal class MethodExtensionsTest {
private val testInstructions = (0..9).map { i -> TestInstruction(i) }
private var method = ImmutableMethod(
"TestClass;",
"testMethod",
null,
"V",
AccessFlags.PUBLIC.value,
null,
null,
MutableMethodImplementation(16)
).toMutable()
@BeforeEach
fun createTestMethod() =
ImmutableMethod(
"TestClass;",
"testMethod",
null,
"V",
AccessFlags.PUBLIC.value,
null,
null,
MutableMethodImplementation(16).also { testMethodImplementation = it }.apply {
repeat(10) { i -> this.addInstruction(TestInstruction(i)) }
},
).let { testMethod = it.toMutable() }
fun setup() {
method.instructions.clear()
method.addInstructions(testInstructions)
}
@Test
fun addInstructionsToImplementationIndexed() =
@@ -227,11 +218,11 @@ private object InstructionExtensionsTest {
// region Helper methods
private fun applyToImplementation(block: MutableMethodImplementation.() -> Unit) {
testMethodImplementation.apply(block)
method.implementation!!.apply(block)
}
private fun applyToMethod(block: MutableMethod.() -> Unit) {
testMethod.apply(block)
method.apply(block)
}
private fun MutableMethodImplementation.assertRegisterIs(

View File

@@ -1,10 +1,12 @@
package app.revanced.patcher.patch.options
package app.revanced.patcher.patch
import app.revanced.patcher.patch.*
import org.junit.jupiter.api.assertDoesNotThrow
import org.junit.jupiter.api.assertThrows
import kotlin.reflect.typeOf
import kotlin.test.*
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlin.test.assertTrue
internal object OptionsTest {
private val externalOption = stringOption("external", "default")
@@ -141,4 +143,4 @@ internal object OptionsTest {
}
private fun options(block: Options.() -> Unit) = optionsTestPatch.options.let(block)
}
}

View File

@@ -0,0 +1,85 @@
package app.revanced.patcher.patch
import kotlin.reflect.jvm.javaField
import kotlin.test.Test
import kotlin.test.assertEquals
internal object PatchTest {
@Test
fun `can create patch with name`() {
val patch = bytecodePatch(name = "Test") {}
assertEquals("Test", patch.name)
}
@Test
fun `can create patch with compatible packages`() {
val patch = bytecodePatch(name = "Test") {
compatibleWith(
"compatible.package"("1.0.0"),
)
}
assertEquals(1, patch.compatiblePackages!!.size)
assertEquals("compatible.package", patch.compatiblePackages!!.first().first)
}
@Test
fun `can create patch with dependencies`() {
val patch = bytecodePatch(name = "Test") {
dependsOn(resourcePatch {})
}
assertEquals(1, patch.dependencies.size)
}
@Test
fun `can create patch with options`() {
val patch = bytecodePatch(name = "Test") {
val print by stringOption("print")
val custom = option<String>("custom")()
this.apply {
println(print)
println(custom.value)
}
}
assertEquals(2, patch.options.size)
}
@Test
fun `loads patches correctly`() {
val patchesClass = ::Public.javaField!!.declaringClass.name
val classLoader = ::Public.javaClass.classLoader
val patches = getPatches(listOf(patchesClass), classLoader)
assertEquals(
2,
patches.size,
"Expected 2 patches to be loaded, " +
"because there's only two named patches declared as public static fields " +
"or returned by public static and non-parametrized methods.",
)
}
}
val publicUnnamedPatch = bytecodePatch {} // Not loaded, because it's unnamed.
val Public by creatingBytecodePatch {} // Loaded, because it's named.
private val privateUnnamedPatch = bytecodePatch {} // Not loaded, because it's private.
private val Private by creatingBytecodePatch {} // Not loaded, because it's private.
fun publicUnnamedPatchFunction() = publicUnnamedPatch // Not loaded, because it's unnamed.
fun publicNamedPatchFunction() = bytecodePatch("Public") { } // Loaded, because it's named.
fun parameterizedFunction(@Suppress("UNUSED_PARAMETER") param: Any) =
publicNamedPatchFunction() // Not loaded, because it's parameterized.
private fun privateUnnamedPatchFunction() = privateUnnamedPatch // Not loaded, because it's private.
private fun privateNamedPatchFunction() = Private // Not loaded, because it's private.

View File

@@ -0,0 +1,76 @@
package app.revanced.patcher.util
import app.revanced.patcher.extensions.*
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.builder.BuilderOffsetInstruction
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21t
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
import com.android.tools.smali.dexlib2.mutable.MutableMethod.Companion.toMutable
import org.junit.jupiter.api.BeforeEach
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
internal class SmaliTest {
val method = ImmutableMethod(
"Ldummy;",
"name",
emptyList(), // parameters
"V",
AccessFlags.PUBLIC.value,
null,
null,
MutableMethodImplementation(1),
).toMutable()
@BeforeEach
fun setup() {
method.instructions.clear()
}
@Test
fun `own branches work`() {
method.addInstructionsWithLabels(
0,
"""
:test
const/4 v0, 0x1
if-eqz v0, :test
""",
)
val targetLocationIndex = method.getInstruction<BuilderOffsetInstruction>(1).target.location.index
assertEquals(0, targetLocationIndex, "Label should point to index 0")
}
@Test
fun `external branches work`() {
val instructionIndex = 3
val labelIndex = 1
method.addInstructions(
"""
const/4 v0, 0x1
const/4 v0, 0x0
""",
)
method.addInstructionsWithLabels(
method.instructions.size,
"""
const/4 v0, 0x1
if-eqz v0, :test
return-void
""",
ExternalLabel("test", method.getInstruction(1)),
)
val instruction = method.getInstruction<BuilderInstruction21t>(instructionIndex)
assertTrue(instruction.target.isPlaced, "Label should be placed")
assertEquals(labelIndex, instruction.target.location.index)
}
}

View File

@@ -1 +1,24 @@
rootProject.name = "revanced-patcher"
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
pluginManagement {
repositories {
mavenCentral()
google()
}
}
dependencyResolutionManagement {
repositories {
mavenCentral()
google()
maven {
name = "githubPackages"
// A repository must be specified for some reason. "registry" is a dummy.
url = uri("https://maven.pkg.github.com/revanced/registry")
credentials(PasswordCredentials::class)
}
}
}
include(":patcher")

View File

@@ -1,599 +0,0 @@
@file:Suppress("unused", "MemberVisibilityCanBePrivate")
package app.revanced.patcher
import app.revanced.patcher.extensions.InstructionExtensions.instructionsOrNull
import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.util.proxy.ClassProxy
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.ClassDef
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
import com.android.tools.smali.dexlib2.iface.reference.StringReference
import com.android.tools.smali.dexlib2.util.MethodUtil
/**
* A fingerprint for a method. A fingerprint is a partial description of a method.
* It is used to uniquely match a method by its characteristics.
*
* An example fingerprint for a public method that takes a single string parameter and returns void:
* ```
* fingerprint {
* accessFlags(AccessFlags.PUBLIC)
* returns("V")
* parameters("Ljava/lang/String;")
* }
* ```
*
* @param accessFlags The exact access flags using values of [AccessFlags].
* @param returnType The return type. Compared using [String.startsWith].
* @param parameters The parameters. Partial matches allowed and follow the same rules as [returnType].
* @param opcodes A pattern of instruction opcodes. `null` can be used as a wildcard.
* @param strings A list of the strings. Compared using [String.contains].
* @param custom A custom condition for this fingerprint.
* @param fuzzyPatternScanThreshold The threshold for fuzzy scanning the [opcodes] pattern.
*/
class Fingerprint internal constructor(
internal val accessFlags: Int?,
internal val returnType: String?,
internal val parameters: List<String>?,
internal val opcodes: List<Opcode?>?,
internal val strings: List<String>?,
internal val custom: ((method: Method, classDef: ClassDef) -> Boolean)?,
private val fuzzyPatternScanThreshold: Int,
) {
@Suppress("ktlint:standard:backing-property-naming")
// Backing field needed for lazy initialization.
private var _matchOrNull: Match? = null
/**
* The match for this [Fingerprint]. Null if unmatched.
*/
context(BytecodePatchContext)
private val matchOrNull: Match?
get() = matchOrNull()
/**
* Match using [BytecodePatchContext.lookupMaps].
*
* Generally faster than the other [matchOrNull] overloads when there are many methods to check for a match.
*
* Fingerprints can be optimized for performance:
* - Slowest: Specify [custom] or [opcodes] and nothing else.
* - Fast: Specify [accessFlags], [returnType].
* - Faster: Specify [accessFlags], [returnType] and [parameters].
* - Fastest: Specify [strings], with at least one string being an exact (non-partial) match.
*
* @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise.
*/
context(BytecodePatchContext)
internal fun matchOrNull(): Match? {
if (_matchOrNull != null) return _matchOrNull
var match = strings?.mapNotNull {
lookupMaps.methodsByStrings[it]
}?.minByOrNull { it.size }?.let { methodClasses ->
methodClasses.forEach { (classDef, method) ->
val match = matchOrNull(classDef, method)
if (match != null) return@let match
}
null
}
if (match != null) return match
classes.forEach { classDef ->
match = matchOrNull(classDef)
if (match != null) return match
}
return null
}
/**
* Match using a [ClassDef].
*
* @param classDef The class to match against.
* @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise.
*/
context(BytecodePatchContext)
fun matchOrNull(
classDef: ClassDef,
): Match? {
if (_matchOrNull != null) return _matchOrNull
for (method in classDef.methods) {
val match = matchOrNull(method, classDef)
if (match != null) return match
}
return null
}
/**
* Match using a [Method].
* The class is retrieved from the method.
*
* @param method The method to match against.
* @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise.
*/
context(BytecodePatchContext)
fun matchOrNull(
method: Method,
) = matchOrNull(method, classBy { method.definingClass == it.type }!!.immutableClass)
/**
* Match using a [Method].
*
* @param method The method to match against.
* @param classDef The class the method is a member of.
* @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise.
*/
context(BytecodePatchContext)
fun matchOrNull(
method: Method,
classDef: ClassDef,
): Match? {
if (_matchOrNull != null) return _matchOrNull
if (returnType != null && !method.returnType.startsWith(returnType)) {
return null
}
if (accessFlags != null && accessFlags != method.accessFlags) {
return null
}
fun parametersEqual(
parameters1: Iterable<CharSequence>,
parameters2: Iterable<CharSequence>,
): Boolean {
if (parameters1.count() != parameters2.count()) return false
val iterator1 = parameters1.iterator()
parameters2.forEach {
if (!it.startsWith(iterator1.next())) return false
}
return true
}
// TODO: parseParameters()
if (parameters != null && !parametersEqual(parameters, method.parameterTypes)) {
return null
}
if (custom != null && !custom.invoke(method, classDef)) {
return null
}
val stringMatches: List<Match.StringMatch>? =
if (strings != null) {
buildList {
val instructions = method.instructionsOrNull ?: return null
val stringsList = strings.toMutableList()
instructions.forEachIndexed { instructionIndex, instruction ->
if (
instruction.opcode != Opcode.CONST_STRING &&
instruction.opcode != Opcode.CONST_STRING_JUMBO
) {
return@forEachIndexed
}
val string = ((instruction as ReferenceInstruction).reference as StringReference).string
val index = stringsList.indexOfFirst(string::contains)
if (index == -1) return@forEachIndexed
add(Match.StringMatch(string, instructionIndex))
stringsList.removeAt(index)
}
if (stringsList.isNotEmpty()) return null
}
} else {
null
}
val patternMatch = if (opcodes != null) {
val instructions = method.instructionsOrNull ?: return null
fun patternScan(): Match.PatternMatch? {
val fingerprintFuzzyPatternScanThreshold = fuzzyPatternScanThreshold
val instructionLength = instructions.count()
val patternLength = opcodes.size
for (index in 0 until instructionLength) {
var patternIndex = 0
var threshold = fingerprintFuzzyPatternScanThreshold
while (index + patternIndex < instructionLength) {
val originalOpcode = instructions.elementAt(index + patternIndex).opcode
val patternOpcode = opcodes.elementAt(patternIndex)
if (patternOpcode != null && patternOpcode.ordinal != originalOpcode.ordinal) {
// Reaching maximum threshold (0) means,
// the pattern does not match to the current instructions.
if (threshold-- == 0) break
}
if (patternIndex < patternLength - 1) {
// If the entire pattern has not been scanned yet, continue the scan.
patternIndex++
continue
}
// The entire pattern has been scanned.
return Match.PatternMatch(
index,
index + patternIndex,
)
}
}
return null
}
patternScan() ?: return null
} else {
null
}
_matchOrNull = Match(
method,
patternMatch,
stringMatches,
classDef,
)
return _matchOrNull
}
private val exception get() = PatchException("Failed to match the fingerprint: $this")
/**
* The match for this [Fingerprint].
*
* @throws PatchException If the [Fingerprint] has not been matched.
*/
context(BytecodePatchContext)
private val match
get() = matchOrNull ?: throw exception
/**
* Match using a [ClassDef].
*
* @param classDef The class to match against.
* @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise.
* @throws PatchException If the fingerprint has not been matched.
*/
context(BytecodePatchContext)
fun match(
classDef: ClassDef,
) = matchOrNull(classDef) ?: throw exception
/**
* Match using a [Method].
* The class is retrieved from the method.
*
* @param method The method to match against.
* @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise.
* @throws PatchException If the fingerprint has not been matched.
*/
context(BytecodePatchContext)
fun match(
method: Method,
) = matchOrNull(method) ?: throw exception
/**
* Match using a [Method].
*
* @param method The method to match against.
* @param classDef The class the method is a member of.
* @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise.
* @throws PatchException If the fingerprint has not been matched.
*/
context(BytecodePatchContext)
fun match(
method: Method,
classDef: ClassDef,
) = matchOrNull(method, classDef) ?: throw exception
/**
* The class the matching method is a member of.
*/
context(BytecodePatchContext)
val originalClassDefOrNull
get() = matchOrNull?.originalClassDef
/**
* The matching method.
*/
context(BytecodePatchContext)
val originalMethodOrNull
get() = matchOrNull?.originalMethod
/**
* The mutable version of [originalClassDefOrNull].
*
* Accessing this property allocates a [ClassProxy].
* Use [originalClassDefOrNull] if mutable access is not required.
*/
context(BytecodePatchContext)
val classDefOrNull
get() = matchOrNull?.classDef
/**
* The mutable version of [originalMethodOrNull].
*
* Accessing this property allocates a [ClassProxy].
* Use [originalMethodOrNull] if mutable access is not required.
*/
context(BytecodePatchContext)
val methodOrNull
get() = matchOrNull?.method
/**
* The match for the opcode pattern.
*/
context(BytecodePatchContext)
val patternMatchOrNull
get() = matchOrNull?.patternMatch
/**
* The matches for the strings.
*/
context(BytecodePatchContext)
val stringMatchesOrNull
get() = matchOrNull?.stringMatches
/**
* The class the matching method is a member of.
*
* @throws PatchException If the fingerprint has not been matched.
*/
context(BytecodePatchContext)
val originalClassDef
get() = match.originalClassDef
/**
* The matching method.
*
* @throws PatchException If the fingerprint has not been matched.
*/
context(BytecodePatchContext)
val originalMethod
get() = match.originalMethod
/**
* The mutable version of [originalClassDef].
*
* Accessing this property allocates a [ClassProxy].
* Use [originalClassDef] if mutable access is not required.
*
* @throws PatchException If the fingerprint has not been matched.
*/
context(BytecodePatchContext)
val classDef
get() = match.classDef
/**
* The mutable version of [originalMethod].
*
* Accessing this property allocates a [ClassProxy].
* Use [originalMethod] if mutable access is not required.
*
* @throws PatchException If the fingerprint has not been matched.
*/
context(BytecodePatchContext)
val method
get() = match.method
/**
* The match for the opcode pattern.
*
* @throws PatchException If the fingerprint has not been matched.
*/
context(BytecodePatchContext)
val patternMatch
get() = match.patternMatch
/**
* The matches for the strings.
*
* @throws PatchException If the fingerprint has not been matched.
*/
context(BytecodePatchContext)
val stringMatches
get() = match.stringMatches
}
/**
* A match of a [Fingerprint].
*
* @param originalClassDef The class the matching method is a member of.
* @param originalMethod The matching method.
* @param patternMatch The match for the opcode pattern.
* @param stringMatches The matches for the strings.
*/
context(BytecodePatchContext)
class Match internal constructor(
val originalMethod: Method,
val patternMatch: PatternMatch?,
val stringMatches: List<StringMatch>?,
val originalClassDef: ClassDef,
) {
/**
* The mutable version of [originalClassDef].
*
* Accessing this property allocates a [ClassProxy].
* Use [originalClassDef] if mutable access is not required.
*/
val classDef by lazy { proxy(originalClassDef).mutableClass }
/**
* The mutable version of [originalMethod].
*
* Accessing this property allocates a [ClassProxy].
* Use [originalMethod] if mutable access is not required.
*/
val method by lazy { classDef.methods.first { MethodUtil.methodSignaturesMatch(it, originalMethod) } }
/**
* A match for an opcode pattern.
* @param startIndex The index of the first opcode of the pattern in the method.
* @param endIndex The index of the last opcode of the pattern in the method.
*/
class PatternMatch internal constructor(
val startIndex: Int,
val endIndex: Int,
)
/**
* A match for a string.
*
* @param string The string that matched.
* @param index The index of the instruction in the method.
*/
class StringMatch internal constructor(val string: String, val index: Int)
}
/**
* A builder for [Fingerprint].
*
* @property accessFlags The exact access flags using values of [AccessFlags].
* @property returnType The return type compared using [String.startsWith].
* @property parameters The parameters of the method. Partial matches allowed and follow the same rules as [returnType].
* @property opcodes An opcode pattern of the instructions. Wildcard or unknown opcodes can be specified by `null`.
* @property strings A list of the strings compared each using [String.contains].
* @property customBlock A custom condition for this fingerprint.
* @property fuzzyPatternScanThreshold The threshold for fuzzy pattern scanning.
*
* @constructor Create a new [FingerprintBuilder].
*/
class FingerprintBuilder internal constructor(
private val fuzzyPatternScanThreshold: Int = 0,
) {
private var accessFlags: Int? = null
private var returnType: String? = null
private var parameters: List<String>? = null
private var opcodes: List<Opcode?>? = null
private var strings: List<String>? = null
private var customBlock: ((method: Method, classDef: ClassDef) -> Boolean)? = null
/**
* Set the access flags.
*
* @param accessFlags The exact access flags using values of [AccessFlags].
*/
fun accessFlags(accessFlags: Int) {
this.accessFlags = accessFlags
}
/**
* Set the access flags.
*
* @param accessFlags The exact access flags using values of [AccessFlags].
*/
fun accessFlags(vararg accessFlags: AccessFlags) {
this.accessFlags = accessFlags.fold(0) { acc, it -> acc or it.value }
}
/**
* Set the return type.
*
* @param returnType The return type compared using [String.startsWith].
*/
fun returns(returnType: String) {
this.returnType = returnType
}
/**
* Set the parameters.
*
* @param parameters The parameters of the method. Partial matches allowed and follow the same rules as [returnType].
*/
fun parameters(vararg parameters: String) {
this.parameters = parameters.toList()
}
/**
* Set the opcodes.
*
* @param opcodes An opcode pattern of instructions.
* Wildcard or unknown opcodes can be specified by `null`.
*/
fun opcodes(vararg opcodes: Opcode?) {
this.opcodes = opcodes.toList()
}
/**
* Set the opcodes.
*
* @param instructions A list of instructions or opcode names in SMALI format.
* - Wildcard or unknown opcodes can be specified by `null`.
* - Empty lines are ignored.
* - Each instruction must be on a new line.
* - The opcode name is enough, no need to specify the operands.
*
* @throws Exception If an unknown opcode is used.
*/
fun opcodes(instructions: String) {
this.opcodes = instructions.trimIndent().split("\n").filter {
it.isNotBlank()
}.map {
// Remove any operands.
val name = it.split(" ", limit = 1).first().trim()
if (name == "null") return@map null
opcodesByName[name] ?: throw Exception("Unknown opcode: $name")
}
}
/**
* Set the strings.
*
* @param strings A list of strings compared each using [String.contains].
*/
fun strings(vararg strings: String) {
this.strings = strings.toList()
}
/**
* Set a custom condition for this fingerprint.
*
* @param customBlock A custom condition for this fingerprint.
*/
fun custom(customBlock: (method: Method, classDef: ClassDef) -> Boolean) {
this.customBlock = customBlock
}
internal fun build() = Fingerprint(
accessFlags,
returnType,
parameters,
opcodes,
strings,
customBlock,
fuzzyPatternScanThreshold,
)
private companion object {
val opcodesByName = Opcode.entries.associateBy { it.name }
}
}
/**
* Create a [Fingerprint].
*
* @param fuzzyPatternScanThreshold The threshold for fuzzy pattern scanning. Default is 0.
* @param block The block to build the [Fingerprint].
*
* @return The created [Fingerprint].
*/
fun fingerprint(
fuzzyPatternScanThreshold: Int = 0,
block: FingerprintBuilder.() -> Unit,
) = FingerprintBuilder(fuzzyPatternScanThreshold).apply(block).build()

View File

@@ -1,7 +0,0 @@
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

@@ -1,16 +0,0 @@
package app.revanced.patcher
import brut.androlib.apk.ApkInfo
/**
* Metadata about a package.
*
* @param apkInfo The [ApkInfo] of the apk file.
*/
class PackageMetadata internal constructor(internal val apkInfo: ApkInfo) {
lateinit var packageName: String
internal set
lateinit var packageVersion: String
internal set
}

View File

@@ -1,160 +0,0 @@
package app.revanced.patcher
import app.revanced.patcher.patch.*
import kotlinx.coroutines.flow.flow
import java.io.Closeable
import java.util.logging.Logger
/**
* A Patcher.
*
* @param config The configuration to use for the patcher.
*/
class Patcher(private val config: PatcherConfig) : Closeable {
private val logger = Logger.getLogger(this::class.java.name)
/**
* The context containing the current state of the patcher.
*/
val context = PatcherContext(config)
init {
context.resourceContext.decodeResources(ResourcePatchContext.ResourceMode.NONE)
}
/**
* Add patches.
*
* @param patches The patches to add.
*/
operator fun plusAssign(patches: Set<Patch<*>>) {
// Add all patches to the executablePatches set.
context.executablePatches += patches
// Add all patches and their dependencies to the allPatches set.
patches.forEach { patch ->
fun Patch<*>.addRecursively() =
also(context.allPatches::add).dependencies.forEach(Patch<*>::addRecursively)
patch.addRecursively()
}
context.allPatches.let { allPatches ->
// Check, if what kind of resource mode is required.
config.resourceMode = if (allPatches.any { patch -> patch.anyRecursively { it is ResourcePatch } }) {
ResourcePatchContext.ResourceMode.FULL
} else if (allPatches.any { patch -> patch.anyRecursively { it is RawResourcePatch } }) {
ResourcePatchContext.ResourceMode.RAW_ONLY
} else {
ResourcePatchContext.ResourceMode.NONE
}
}
}
/**
* Execute added patches.
*
* @return A flow of [PatchResult]s.
*/
operator fun invoke() = flow {
fun Patch<*>.execute(
executedPatches: LinkedHashMap<Patch<*>, PatchResult>,
): PatchResult {
// If the patch was executed before or failed, return it's the result.
executedPatches[this]?.let { patchResult ->
patchResult.exception ?: return patchResult
return PatchResult(this, PatchException("The patch '$this' failed previously"))
}
// Recursively execute all dependency patches.
dependencies.forEach { dependency ->
dependency.execute(executedPatches).exception?.let {
return PatchResult(
this,
PatchException(
"The patch \"$this\" depends on \"$dependency\", which raised an exception:\n${it.stackTraceToString()}",
),
)
}
}
// Execute the patch.
return try {
execute(context)
PatchResult(this)
} catch (exception: PatchException) {
PatchResult(this, exception)
} catch (exception: Exception) {
PatchResult(this, PatchException(exception))
}.also { executedPatches[this] = it }
}
// Prevent decoding the app manifest twice if it is not needed.
if (config.resourceMode != ResourcePatchContext.ResourceMode.NONE) {
context.resourceContext.decodeResources(config.resourceMode)
}
logger.info("Initializing lookup maps")
// Accessing the lazy lookup maps to initialize them.
context.bytecodeContext.lookupMaps
logger.info("Executing patches")
val executedPatches = LinkedHashMap<Patch<*>, PatchResult>()
context.executablePatches.sortedBy { it.name }.forEach { patch ->
val patchResult = patch.execute(executedPatches)
// If an exception occurred or the patch has no finalize block, emit the result.
if (patchResult.exception != null || patch.finalizeBlock == null) {
emit(patchResult)
}
}
val succeededPatchesWithFinalizeBlock = executedPatches.values.filter {
it.exception == null && it.patch.finalizeBlock != null
}
succeededPatchesWithFinalizeBlock.asReversed().forEach { executionResult ->
val patch = executionResult.patch
val result =
try {
patch.finalize(context)
executionResult
} catch (exception: PatchException) {
PatchResult(patch, exception)
} catch (exception: Exception) {
PatchResult(patch, PatchException(exception))
}
if (result.exception != null) {
emit(
PatchResult(
patch,
PatchException(
"The patch \"$patch\" raised an exception: ${result.exception.stackTraceToString()}",
result.exception,
),
),
)
} else if (patch in context.executablePatches) {
emit(result)
}
}
}
override fun close() = context.close()
/**
* Compile and save patched APK files.
*
* @return The [PatcherResult] containing the patched APK files.
*/
@OptIn(InternalApi::class)
fun get() = PatcherResult(context.bytecodeContext.get(), context.resourceContext.get())
}

View File

@@ -1,87 +0,0 @@
package app.revanced.patcher
import app.revanced.patcher.patch.ResourcePatchContext
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.
*/
class PatcherConfig(
internal val apkFile: File,
private val temporaryFilesPath: File = File("revanced-temporary-files"),
aaptBinaryPath: File? = null,
frameworkFileDirectory: String? = null,
) {
/**
* 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.
*/
@Deprecated(
"Use the constructor with a File for aaptBinaryPath instead.",
ReplaceWith("PatcherConfig(apkFile, temporaryFilesPath, aaptBinaryPath?.let { File(it) }, frameworkFileDirectory)"),
)
constructor(
apkFile: File,
temporaryFilesPath: File = File("revanced-temporary-files"),
aaptBinaryPath: String? = null,
frameworkFileDirectory: String? = null,
) : this(apkFile, temporaryFilesPath, aaptBinaryPath?.let { File(it) }, frameworkFileDirectory)
private val logger = Logger.getLogger(PatcherConfig::class.java.name)
/**
* The mode to use for resource decoding and compiling.
*
* @see ResourcePatchContext.ResourceMode
*/
internal var resourceMode = ResourcePatchContext.ResourceMode.NONE
/**
* The configuration for decoding and compiling resources.
*/
internal val resourceConfig =
Config.getDefaultConfig().apply {
aaptBinary = 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

@@ -1,43 +0,0 @@
package app.revanced.patcher
import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.ResourcePatchContext
import brut.androlib.apk.ApkInfo
import brut.directory.ExtFile
import java.io.Closeable
/**
* A context for the patcher containing the current state of the patcher.
*
* @param config The configuration for the patcher.
*/
@Suppress("MemberVisibilityCanBePrivate")
class PatcherContext internal constructor(config: PatcherConfig): Closeable {
/**
* [PackageMetadata] of the supplied [PatcherConfig.apkFile].
*/
val packageMetadata = PackageMetadata(ApkInfo(ExtFile(config.apkFile)))
/**
* The set of [Patch]es.
*/
internal val executablePatches = mutableSetOf<Patch<*>>()
/**
* The set of all [Patch]es and their dependencies.
*/
internal val allPatches = mutableSetOf<Patch<*>>()
/**
* The context for patches containing the current state of the resources.
*/
internal val resourceContext = ResourcePatchContext(packageMetadata, config)
/**
* The context for patches containing the current state of the bytecode.
*/
internal val bytecodeContext = BytecodePatchContext(config)
override fun close() = bytecodeContext.close()
}

View File

@@ -1,40 +0,0 @@
package app.revanced.patcher
import java.io.File
import java.io.InputStream
/**
* The result of a patcher.
*
* @param dexFiles The patched dex files.
* @param resources The patched resources.
*/
@Suppress("MemberVisibilityCanBePrivate")
class PatcherResult internal constructor(
val dexFiles: Set<PatchedDexFile>,
val resources: PatchedResources?,
) {
/**
* A dex file.
*
* @param name The original name of the dex file.
* @param stream The dex file as [InputStream].
*/
class PatchedDexFile internal 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 resources that should be deleted.
*/
class PatchedResources internal constructor(
val resourcesApk: File?,
val otherResources: File?,
val doNotCompress: Set<String>,
val deleteResources: Set<String>,
)
}

View File

@@ -1,11 +0,0 @@
package app.revanced.patcher.extensions
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
/**
* Create a label for the instruction at given index.
*
* @param index The index to create the label for the instruction at.
* @return The label.
*/
fun MutableMethod.newLabel(index: Int) = implementation!!.newLabelForIndex(index)

View File

@@ -1,434 +0,0 @@
package app.revanced.patcher.extensions
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import app.revanced.patcher.util.smali.ExternalLabel
import app.revanced.patcher.util.smali.toInstruction
import app.revanced.patcher.util.smali.toInstructions
import com.android.tools.smali.dexlib2.builder.BuilderInstruction
import com.android.tools.smali.dexlib2.builder.BuilderOffsetInstruction
import com.android.tools.smali.dexlib2.builder.Label
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
import com.android.tools.smali.dexlib2.builder.instruction.*
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.MethodImplementation
import com.android.tools.smali.dexlib2.iface.instruction.Instruction
object InstructionExtensions {
/**
* Add instructions to a method at the given index.
*
* @param index The index to add the instructions at.
* @param instructions The instructions to add.
*/
fun MutableMethodImplementation.addInstructions(
index: Int,
instructions: List<BuilderInstruction>,
) = instructions.asReversed().forEach { addInstruction(index, it) }
/**
* Add instructions to a method.
* The instructions will be added at the end of the method.
*
* @param instructions The instructions to add.
*/
fun MutableMethodImplementation.addInstructions(instructions: List<BuilderInstruction>) =
instructions.forEach { addInstruction(it) }
/**
* Remove instructions from a method at the given index.
*
* @param index The index to remove the instructions at.
* @param count The amount of instructions to remove.
*/
fun MutableMethodImplementation.removeInstructions(
index: Int,
count: Int,
) = repeat(count) {
removeInstruction(index)
}
/**
* Remove the first instructions from a method.
*
* @param count The amount of instructions to remove.
*/
fun MutableMethodImplementation.removeInstructions(count: Int) = removeInstructions(0, count)
/**
* Replace instructions at the given index with the given instructions.
* The amount of instructions to replace is the amount of instructions in the given list.
*
* @param index The index to replace the instructions at.
* @param instructions The instructions to replace the instructions with.
*/
fun MutableMethodImplementation.replaceInstructions(
index: Int,
instructions: List<BuilderInstruction>,
) {
// Remove the instructions at the given index.
removeInstructions(index, instructions.size)
// Add the instructions at the given index.
addInstructions(index, instructions)
}
/**
* Add an instruction to a method at the given index.
*
* @param index The index to add the instruction at.
* @param instruction The instruction to add.
*/
fun MutableMethod.addInstruction(
index: Int,
instruction: BuilderInstruction,
) = implementation!!.addInstruction(index, instruction)
/**
* Add an instruction to a method.
*
* @param instruction The instructions to add.
*/
fun MutableMethod.addInstruction(instruction: BuilderInstruction) = implementation!!.addInstruction(instruction)
/**
* Add an instruction to a method at the given index.
*
* @param index The index to add the instruction at.
* @param smaliInstructions The instruction to add.
*/
fun MutableMethod.addInstruction(
index: Int,
smaliInstructions: String,
) = implementation!!.addInstruction(index, smaliInstructions.toInstruction(this))
/**
* Add an instruction to a method.
*
* @param smaliInstructions The instruction to add.
*/
fun MutableMethod.addInstruction(smaliInstructions: String) = implementation!!.addInstruction(smaliInstructions.toInstruction(this))
/**
* Add instructions to a method at the given index.
*
* @param index The index to add the instructions at.
* @param instructions The instructions to add.
*/
fun MutableMethod.addInstructions(
index: Int,
instructions: List<BuilderInstruction>,
) = implementation!!.addInstructions(index, instructions)
/**
* Add instructions to a method.
*
* @param instructions The instructions to add.
*/
fun MutableMethod.addInstructions(instructions: List<BuilderInstruction>) = implementation!!.addInstructions(instructions)
/**
* Add instructions to a method.
*
* @param smaliInstructions The instructions to add.
*/
fun MutableMethod.addInstructions(
index: Int,
smaliInstructions: String,
) = implementation!!.addInstructions(index, smaliInstructions.toInstructions(this))
/**
* Add instructions to a method.
*
* @param smaliInstructions The instructions to add.
*/
fun MutableMethod.addInstructions(smaliInstructions: String) = implementation!!.addInstructions(smaliInstructions.toInstructions(this))
/**
* Add instructions to a method at the given index.
*
* @param index The index to add the instructions at.
* @param smaliInstructions The instructions to add.
* @param externalLabels A list of [ExternalLabel] for instructions outside of [smaliInstructions].
*/
// Special function for adding instructions with external labels.
fun MutableMethod.addInstructionsWithLabels(
index: Int,
smaliInstructions: String,
vararg externalLabels: ExternalLabel,
) {
// Create reference dummy instructions for the instructions.
val nopSmali =
StringBuilder(smaliInstructions).also { builder ->
externalLabels.forEach { (name, _) ->
builder.append("\n:$name\nnop")
}
}.toString()
// Compile the instructions with the dummy labels
val compiledInstructions = nopSmali.toInstructions(this)
// Add the compiled list of instructions to the method.
addInstructions(
index,
compiledInstructions.subList(0, compiledInstructions.size - externalLabels.size),
)
implementation!!.apply {
this@apply.instructions.subList(index, index + compiledInstructions.size - externalLabels.size)
.forEachIndexed { compiledInstructionIndex, compiledInstruction ->
// If the compiled instruction is not an offset instruction, skip it.
if (compiledInstruction !is BuilderOffsetInstruction) return@forEachIndexed
/**
* Create a new label for the instruction
* and replace it with the label of the [compiledInstruction] at [compiledInstructionIndex].
*/
fun Instruction.makeNewLabel() {
fun replaceOffset(
i: BuilderOffsetInstruction,
label: Label,
): BuilderOffsetInstruction {
return when (i) {
is BuilderInstruction10t -> BuilderInstruction10t(i.opcode, label)
is BuilderInstruction20t -> BuilderInstruction20t(i.opcode, label)
is BuilderInstruction21t -> BuilderInstruction21t(i.opcode, i.registerA, label)
is BuilderInstruction22t ->
BuilderInstruction22t(
i.opcode,
i.registerA,
i.registerB,
label,
)
is BuilderInstruction30t -> BuilderInstruction30t(i.opcode, label)
is BuilderInstruction31t -> BuilderInstruction31t(i.opcode, i.registerA, label)
else -> throw IllegalStateException(
"A non-offset instruction was given, this should never happen!",
)
}
}
// Create the final label.
val label = newLabelForIndex(this@apply.instructions.indexOf(this))
// Create the final instruction with the new label.
val newInstruction =
replaceOffset(
compiledInstruction,
label,
)
// Replace the instruction pointing to the dummy label
// with the new instruction pointing to the real instruction.
replaceInstruction(index + compiledInstructionIndex, newInstruction)
}
// If the compiled instruction targets its own instruction,
// which means it points to some of its own, simply an offset has to be applied.
val labelIndex = compiledInstruction.target.location.index
if (labelIndex < compiledInstructions.size - externalLabels.size) {
// Get the targets index (insertion index + the index of the dummy instruction).
this.instructions[index + labelIndex].makeNewLabel()
return@forEachIndexed
}
// Since the compiled instruction points to a dummy instruction,
// we can find the real instruction which it was created for by calculation.
// Get the index of the instruction in the externalLabels list
// which the dummy instruction was created for.
// This works because we created the dummy instructions in the same order as the externalLabels list.
val (_, instruction) = externalLabels[(compiledInstructions.size - 1) - labelIndex]
instruction.makeNewLabel()
}
}
}
/**
* Remove an instruction at the given index.
*
* @param index The index to remove the instruction at.
*/
fun MutableMethod.removeInstruction(index: Int) = implementation!!.removeInstruction(index)
/**
* Remove instructions at the given index.
*
* @param index The index to remove the instructions at.
* @param count The amount of instructions to remove.
*/
fun MutableMethod.removeInstructions(
index: Int,
count: Int,
) = implementation!!.removeInstructions(index, count)
/**
* Remove instructions at the given index.
*
* @param count The amount of instructions to remove.
*/
fun MutableMethod.removeInstructions(count: Int) = implementation!!.removeInstructions(count)
/**
* Replace an instruction at the given index.
*
* @param index The index to replace the instruction at.
* @param instruction The instruction to replace the instruction with.
*/
fun MutableMethod.replaceInstruction(
index: Int,
instruction: BuilderInstruction,
) = implementation!!.replaceInstruction(index, instruction)
/**
* Replace an instruction at the given index.
*
* @param index The index to replace the instruction at.
* @param smaliInstruction The smali instruction to replace the instruction with.
*/
fun MutableMethod.replaceInstruction(
index: Int,
smaliInstruction: String,
) = implementation!!.replaceInstruction(index, smaliInstruction.toInstruction(this))
/**
* Replace instructions at the given index.
*
* @param index The index to replace the instructions at.
* @param instructions The instructions to replace the instructions with.
*/
fun MutableMethod.replaceInstructions(
index: Int,
instructions: List<BuilderInstruction>,
) = implementation!!.replaceInstructions(index, instructions)
/**
* Replace instructions at the given index.
*
* @param index The index to replace the instructions at.
* @param smaliInstructions The smali instructions to replace the instructions with.
*/
fun MutableMethod.replaceInstructions(
index: Int,
smaliInstructions: String,
) = implementation!!.replaceInstructions(index, smaliInstructions.toInstructions(this))
/**
* Get an instruction at the given index.
*
* @param index The index to get the instruction at.
* @return The instruction.
*/
fun MethodImplementation.getInstruction(index: Int) = instructions.elementAt(index)
/**
* Get an instruction at the given index.
*
* @param index The index to get the instruction at.
* @param T The type of instruction to return.
* @return The instruction.
*/
@Suppress("UNCHECKED_CAST")
fun <T> MethodImplementation.getInstruction(index: Int): T = getInstruction(index) as T
/**
* Get an instruction at the given index.
*
* @param index The index to get the instruction at.
* @return The instruction.
*/
fun MutableMethodImplementation.getInstruction(index: Int): BuilderInstruction = instructions[index]
/**
* Get an instruction at the given index.
*
* @param index The index to get the instruction at.
* @param T The type of instruction to return.
* @return The instruction.
*/
@Suppress("UNCHECKED_CAST")
fun <T> MutableMethodImplementation.getInstruction(index: Int): T = getInstruction(index) as T
/**
* Get an instruction at the given index.
* @param index The index to get the instruction at.
* @return The instruction or null if the method has no implementation.
*/
fun Method.getInstructionOrNull(index: Int): Instruction? = implementation?.getInstruction(index)
/**
* Get an instruction at the given index.
* @param index The index to get the instruction at.
* @return The instruction.
*/
fun Method.getInstruction(index: Int): Instruction = getInstructionOrNull(index)!!
/**
* Get an instruction at the given index.
* @param index The index to get the instruction at.
* @param T The type of instruction to return.
* @return The instruction or null if the method has no implementation.
*/
fun <T> Method.getInstructionOrNull(index: Int): T? = implementation?.getInstruction<T>(index)
/**
* Get an instruction at the given index.
* @param index The index to get the instruction at.
* @param T The type of instruction to return.
* @return The instruction.
*/
fun <T> Method.getInstruction(index: Int): T = getInstructionOrNull<T>(index)!!
/**
* Get an instruction at the given index.
* @param index The index to get the instruction at.
* @return The instruction or null if the method has no implementation.
*/
fun MutableMethod.getInstructionOrNull(index: Int): BuilderInstruction? = implementation?.getInstruction(index)
/**
* Get an instruction at the given index.
* @param index The index to get the instruction at.
* @return The instruction.
*/
fun MutableMethod.getInstruction(index: Int): BuilderInstruction = getInstructionOrNull(index)!!
/**
* Get an instruction at the given index.
* @param index The index to get the instruction at.
* @param T The type of instruction to return.
* @return The instruction or null if the method has no implementation.
*/
fun <T> MutableMethod.getInstructionOrNull(index: Int): T? = implementation?.getInstruction<T>(index)
/**
* Get an instruction at the given index.
* @param index The index to get the instruction at.
* @param T The type of instruction to return.
* @return The instruction.
*/
fun <T> MutableMethod.getInstruction(index: Int): T = getInstructionOrNull<T>(index)!!
/**
* The instructions of a method.
* @return The instructions or null if the method has no implementation.
*/
val Method.instructionsOrNull: Iterable<Instruction>? get() = implementation?.instructions
/**
* The instructions of a method.
* @return The instructions.
*/
val Method.instructions: Iterable<Instruction> get() = instructionsOrNull!!
/**
* The instructions of a method.
* @return The instructions or null if the method has no implementation.
*/
val MutableMethod.instructionsOrNull: MutableList<BuilderInstruction>? get() = implementation?.instructions
/**
* The instructions of a method.
* @return The instructions.
*/
val MutableMethod.instructions: MutableList<BuilderInstruction> get() = instructionsOrNull!!
}

View File

@@ -1,234 +0,0 @@
package app.revanced.patcher.patch
import app.revanced.patcher.InternalApi
import app.revanced.patcher.PatcherConfig
import app.revanced.patcher.PatcherResult
import app.revanced.patcher.extensions.InstructionExtensions.instructionsOrNull
import app.revanced.patcher.util.ClassMerger.merge
import app.revanced.patcher.util.MethodNavigator
import app.revanced.patcher.util.ProxyClassList
import app.revanced.patcher.util.proxy.ClassProxy
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.Opcodes
import com.android.tools.smali.dexlib2.iface.ClassDef
import com.android.tools.smali.dexlib2.iface.DexFile
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
import com.android.tools.smali.dexlib2.iface.reference.StringReference
import lanchon.multidexlib2.BasicDexFileNamer
import lanchon.multidexlib2.DexIO
import lanchon.multidexlib2.MultiDexIO
import lanchon.multidexlib2.RawDexIO
import java.io.Closeable
import java.io.FileFilter
import java.util.*
import java.util.logging.Logger
/**
* A context for patches containing the current state of the bytecode.
*
* @param config The [PatcherConfig] used to create this context.
*/
@Suppress("MemberVisibilityCanBePrivate")
class BytecodePatchContext internal constructor(private val config: PatcherConfig) :
PatchContext<Set<PatcherResult.PatchedDexFile>>,
Closeable {
private val logger = Logger.getLogger(this::class.java.name)
/**
* [Opcodes] of the supplied [PatcherConfig.apkFile].
*/
internal val opcodes: Opcodes
/**
* The list of classes.
*/
val classes = ProxyClassList(
MultiDexIO.readDexFile(
true,
config.apkFile,
BasicDexFileNamer(),
null,
null,
).also { opcodes = it.opcodes }.classes.toMutableList(),
)
/**
* The lookup maps for methods and the class they are a member of from the [classes].
*/
internal val lookupMaps by lazy { LookupMaps(classes) }
/**
* Merge the extension of [bytecodePatch] into the [BytecodePatchContext].
* If no extension is present, the function will return early.
*
* @param bytecodePatch The [BytecodePatch] to merge the extension of.
*/
internal fun mergeExtension(bytecodePatch: BytecodePatch) {
bytecodePatch.extensionInputStream?.get()?.use { extensionStream ->
RawDexIO.readRawDexFile(extensionStream, 0, null).classes.forEach { classDef ->
val existingClass = lookupMaps.classesByType[classDef.type] ?: run {
logger.fine { "Adding class \"$classDef\"" }
classes += classDef
lookupMaps.classesByType[classDef.type] = classDef
return@forEach
}
logger.fine { "Class \"$classDef\" exists already. Adding missing methods and fields." }
existingClass.merge(classDef, this@BytecodePatchContext).let { mergedClass ->
// If the class was merged, replace the original class with the merged class.
if (mergedClass === existingClass) {
return@let
}
classes -= existingClass
classes += mergedClass
}
}
} ?: logger.fine("No extension to merge")
}
/**
* Find a class with a predicate.
*
* @param predicate A predicate to match the class.
* @return A proxy for the first class that matches the predicate.
*/
fun classBy(predicate: (ClassDef) -> Boolean) =
classes.proxyPool.find { predicate(it.immutableClass) } ?: classes.find(predicate)?.let { proxy(it) }
/**
* Proxy the class to allow mutation.
*
* @param classDef The class to proxy.
*
* @return A proxy for the class.
*/
fun proxy(classDef: ClassDef) = classes.proxyPool.find {
it.immutableClass.type == classDef.type
} ?: ClassProxy(classDef).also { classes.proxyPool.add(it) }
/**
* Navigate a method.
*
* @param method The method to navigate.
*
* @return A [MethodNavigator] for the method.
*/
fun navigate(method: MethodReference) = MethodNavigator(method)
/**
* Compile bytecode from the [BytecodePatchContext].
*
* @return The compiled bytecode.
*/
@InternalApi
override fun get(): Set<PatcherResult.PatchedDexFile> {
logger.info("Compiling patched dex files")
// Free up memory before compiling the dex files.
lookupMaps.close()
val patchedDexFileResults =
config.patchedFiles.resolve("dex").also {
it.deleteRecursively() // Make sure the directory is empty.
it.mkdirs()
}.apply {
MultiDexIO.writeDexFile(
true,
-1,
this,
BasicDexFileNamer(),
object : DexFile {
override fun getClasses() =
this@BytecodePatchContext.classes.also(ProxyClassList::replaceClasses).toSet()
override fun getOpcodes() = this@BytecodePatchContext.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
}
/**
* A lookup map for methods and the class they are a member of and classes.
*
* @param classes The list of classes to create the lookup maps from.
*/
internal class LookupMaps internal constructor(classes: List<ClassDef>) : Closeable {
/**
* Methods associated by strings referenced in it.
*/
internal val methodsByStrings = MethodClassPairsLookupMap()
// Lookup map for fast checking if a class exists by its type.
val classesByType = mutableMapOf<String, ClassDef>().apply {
classes.forEach { classDef -> put(classDef.type, classDef) }
}
init {
classes.forEach { classDef ->
classDef.methods.forEach { method ->
val methodClassPair: MethodClassPair = method to classDef
// Add strings contained in the method as the key.
method.instructionsOrNull?.forEach instructions@{ instruction ->
if (instruction.opcode != Opcode.CONST_STRING && instruction.opcode != Opcode.CONST_STRING_JUMBO) {
return@instructions
}
val string = ((instruction as ReferenceInstruction).reference as StringReference).string
methodsByStrings[string] = methodClassPair
}
// In the future, the class type could be added to the lookup map.
// This would require MethodFingerprint to be changed to include the class type.
}
}
}
override fun close() {
methodsByStrings.clear()
classesByType.clear()
}
}
override fun close() {
lookupMaps.close()
classes.clear()
}
}
/**
* A pair of a [Method] and the [ClassDef] it is a member of.
*/
internal typealias MethodClassPair = Pair<Method, ClassDef>
/**
* A list of [MethodClassPair]s.
*/
internal typealias MethodClassPairs = LinkedList<MethodClassPair>
/**
* A lookup map for [MethodClassPairs]s.
* The key is a string and the value is a list of [MethodClassPair]s.
*/
internal class MethodClassPairsLookupMap : MutableMap<String, MethodClassPairs> by mutableMapOf() {
/**
* Add a [MethodClassPair] associated by any key.
* If the key does not exist, a new list is created and the [MethodClassPair] is added to it.
*/
internal operator fun set(key: String, methodClassPair: MethodClassPair) =
apply { getOrPut(key) { MethodClassPairs() }.add(methodClassPair) }
}

View File

@@ -1,687 +0,0 @@
@file:Suppress("MemberVisibilityCanBePrivate", "unused")
package app.revanced.patcher.patch
import app.revanced.patcher.Patcher
import app.revanced.patcher.PatcherContext
import dalvik.system.DexClassLoader
import lanchon.multidexlib2.BasicDexFileNamer
import lanchon.multidexlib2.MultiDexIO
import java.io.File
import java.io.InputStream
import java.lang.reflect.Member
import java.lang.reflect.Method
import java.lang.reflect.Modifier
import java.net.URLClassLoader
import java.util.function.Supplier
import java.util.jar.JarFile
typealias PackageName = String
typealias VersionName = String
typealias Package = Pair<PackageName, Set<VersionName>?>
/**
* A patch.
*
* @param C The [PatchContext] to execute and finalize the patch with.
* @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
* @param description The description of the patch.
* @param use Weather or not the patch should be used.
* @param dependencies Other patches this patch depends on.
* @param compatiblePackages The packages the patch is compatible with.
* If null, the patch is compatible with all packages.
* @param options The options of the patch.
* @param executeBlock The execution block of the patch.
* @param finalizeBlock The finalizing block of the patch. Called after all patches have been executed,
* in reverse order of execution.
*
* @constructor Create a new patch.
*/
sealed class Patch<C : PatchContext<*>>(
val name: String?,
val description: String?,
val use: Boolean,
val dependencies: Set<Patch<*>>,
val compatiblePackages: Set<Package>?,
options: Set<Option<*>>,
private val executeBlock: (C) -> Unit,
// Must be internal and nullable, so that Patcher.invoke can check,
// if a patch has a finalizing block in order to not emit it twice.
internal var finalizeBlock: ((C) -> Unit)?,
) {
/**
* The options of the patch.
*/
val options = Options(options)
/**
* Calls the execution block of the patch.
* This function is called by [Patcher.invoke].
*
* @param context The [PatcherContext] to get the [PatchContext] from to execute the patch with.
*/
internal abstract fun execute(context: PatcherContext)
/**
* Calls the execution block of the patch.
*
* @param context The [PatchContext] to execute the patch with.
*/
fun execute(context: C) = executeBlock(context)
/**
* Calls the finalizing block of the patch.
* This function is called by [Patcher.invoke].
*
* @param context The [PatcherContext] to get the [PatchContext] from to finalize the patch with.
*/
internal abstract fun finalize(context: PatcherContext)
/**
* Calls the finalizing block of the patch.
*
* @param context The [PatchContext] to finalize the patch with.
*/
fun finalize(context: C) {
finalizeBlock?.invoke(context)
}
override fun toString() = name ?:
"Patch@${System.identityHashCode(this)}"
}
internal fun Patch<*>.anyRecursively(
visited: MutableSet<Patch<*>> = mutableSetOf(),
predicate: (Patch<*>) -> Boolean,
): Boolean {
if (this in visited) return false
if (predicate(this)) return true
visited += this
return dependencies.any { it.anyRecursively(visited, predicate) }
}
internal fun Iterable<Patch<*>>.forEachRecursively(
visited: MutableSet<Patch<*>> = mutableSetOf(),
action: (Patch<*>) -> Unit,
): Unit = forEach {
if (it in visited) return@forEach
visited += it
action(it)
it.dependencies.forEachRecursively(visited, action)
}
/**
* A bytecode patch.
*
* @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
* @param description The description of the patch.
* @param use Weather or not the patch should be used.
* @param compatiblePackages The packages the patch is compatible with.
* If null, the patch is compatible with all packages.
* @param dependencies Other patches this patch depends on.
* @param options The options of the patch.
* @property extensionInputStream Getter for the extension input stream of the patch.
* An extension is a precompiled DEX file that is merged into the patched app before this patch is executed.
* @param executeBlock The execution block of the patch.
* @param finalizeBlock The finalizing block of the patch. Called after all patches have been executed,
* in reverse order of execution.
*
* @constructor Create a new bytecode patch.
*/
class BytecodePatch internal constructor(
name: String?,
description: String?,
use: Boolean,
compatiblePackages: Set<Package>?,
dependencies: Set<Patch<*>>,
options: Set<Option<*>>,
val extensionInputStream: Supplier<InputStream>?,
executeBlock: (BytecodePatchContext) -> Unit,
finalizeBlock: ((BytecodePatchContext) -> Unit)?,
) : Patch<BytecodePatchContext>(
name,
description,
use,
dependencies,
compatiblePackages,
options,
executeBlock,
finalizeBlock,
) {
override fun execute(context: PatcherContext) = with(context.bytecodeContext) {
mergeExtension(this@BytecodePatch)
execute(this)
}
override fun finalize(context: PatcherContext) = finalize(context.bytecodeContext)
override fun toString() = name ?: "Bytecode${super.toString()}"
}
/**
* A raw resource patch.
*
* @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
* @param description The description of the patch.
* @param use Weather or not the patch should be used.
* @param compatiblePackages The packages the patch is compatible with.
* If null, the patch is compatible with all packages.
* @param dependencies Other patches this patch depends on.
* @param options The options of the patch.
* @param executeBlock The execution block of the patch.
* @param finalizeBlock The finalizing block of the patch. Called after all patches have been executed,
* in reverse order of execution.
*
* @constructor Create a new raw resource patch.
*/
class RawResourcePatch internal constructor(
name: String?,
description: String?,
use: Boolean,
compatiblePackages: Set<Package>?,
dependencies: Set<Patch<*>>,
options: Set<Option<*>>,
executeBlock: (ResourcePatchContext) -> Unit,
finalizeBlock: ((ResourcePatchContext) -> Unit)?,
) : Patch<ResourcePatchContext>(
name,
description,
use,
dependencies,
compatiblePackages,
options,
executeBlock,
finalizeBlock,
) {
override fun execute(context: PatcherContext) = execute(context.resourceContext)
override fun finalize(context: PatcherContext) = finalize(context.resourceContext)
override fun toString() = name ?: "RawResource${super.toString()}"
}
/**
* A resource patch.
*
* @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
* @param description The description of the patch.
* @param use Weather or not the patch should be used.
* @param compatiblePackages The packages the patch is compatible with.
* If null, the patch is compatible with all packages.
* @param dependencies Other patches this patch depends on.
* @param options The options of the patch.
* @param executeBlock The execution block of the patch.
* @param finalizeBlock The finalizing block of the patch. Called after all patches have been executed,
* in reverse order of execution.
*
* @constructor Create a new resource patch.
*/
class ResourcePatch internal constructor(
name: String?,
description: String?,
use: Boolean,
compatiblePackages: Set<Package>?,
dependencies: Set<Patch<*>>,
options: Set<Option<*>>,
executeBlock: (ResourcePatchContext) -> Unit,
finalizeBlock: ((ResourcePatchContext) -> Unit)?,
) : Patch<ResourcePatchContext>(
name,
description,
use,
dependencies,
compatiblePackages,
options,
executeBlock,
finalizeBlock,
) {
override fun execute(context: PatcherContext) = execute(context.resourceContext)
override fun finalize(context: PatcherContext) = finalize(context.resourceContext)
override fun toString() = name ?: "Resource${super.toString()}"
}
/**
* A [Patch] builder.
*
* @param C The [PatchContext] to execute and finalize the patch with.
* @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
* @param description The description of the patch.
* @param use Weather or not the patch should be used.
* @property compatiblePackages The packages the patch is compatible with.
* If null, the patch is compatible with all packages.
* @property dependencies Other patches this patch depends on.
* @property options The options of the patch.
* @property executionBlock The execution block of the patch.
* @property finalizeBlock The finalizing block of the patch. Called after all patches have been executed,
* in reverse order of execution.
*
* @constructor Create a new [Patch] builder.
*/
sealed class PatchBuilder<C : PatchContext<*>>(
protected val name: String?,
protected val description: String?,
protected val use: Boolean,
) {
protected var compatiblePackages: MutableSet<Package>? = null
protected var dependencies = mutableSetOf<Patch<*>>()
protected val options = mutableSetOf<Option<*>>()
protected var executionBlock: ((C) -> Unit) = { }
protected var finalizeBlock: ((C) -> Unit)? = null
/**
* Add an option to the patch.
*
* @return The added option.
*/
operator fun <T> Option<T>.invoke() = apply {
options += this
}
/**
* Create a package a patch is compatible with.
*
* @param versions The versions of the package.
*/
operator fun String.invoke(vararg versions: String) = invoke(versions.toSet())
/**
* Create a package a patch is compatible with.
*
* @param versions The versions of the package.
*/
private operator fun String.invoke(versions: Set<String>? = null) = this to versions
/**
* Add packages the patch is compatible with.
*
* @param packages The packages the patch is compatible with.
*/
fun compatibleWith(vararg packages: Package) {
if (compatiblePackages == null) {
compatiblePackages = mutableSetOf()
}
compatiblePackages!! += packages
}
/**
* Set the compatible packages of the patch.
*
* @param packages The packages the patch is compatible with.
*/
fun compatibleWith(vararg packages: String) = compatibleWith(*packages.map { it() }.toTypedArray())
/**
* Add dependencies to the patch.
*
* @param patches The patches the patch depends on.
*/
fun dependsOn(vararg patches: Patch<*>) {
dependencies += patches
}
/**
* Set the execution block of the patch.
*
* @param block The execution block of the patch.
*/
fun execute(block: C.() -> Unit) {
executionBlock = block
}
/**
* Set the finalizing block of the patch.
*
* @param block The finalizing block of the patch.
*/
fun finalize(block: C.() -> Unit) {
finalizeBlock = block
}
/**
* Build the patch.
*
* @return The built patch.
*/
internal abstract fun build(): Patch<C>
}
/**
* Builds a [Patch].
*
* @param B The [PatchBuilder] to build the patch with.
* @param block The block to build the patch.
*
* @return The built [Patch].
*/
private fun <B : PatchBuilder<*>> B.buildPatch(block: B.() -> Unit = {}) = apply(block).build()
/**
* A [BytecodePatchBuilder] builder.
*
* @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
* @param description The description of the patch.
* @param use Weather or not the patch should be used.
* @property extensionInputStream Getter for the extension input stream of the patch.
* An extension is a precompiled DEX file that is merged into the patched app before this patch is executed.
*
* @constructor Create a new [BytecodePatchBuilder] builder.
*/
class BytecodePatchBuilder internal constructor(
name: String?,
description: String?,
use: Boolean,
) : PatchBuilder<BytecodePatchContext>(name, description, use) {
// Must be internal for the inlined function "extendWith".
@PublishedApi
internal var extensionInputStream: Supplier<InputStream>? = null
// Inlining is necessary to get the class loader that loaded the patch
// to load the extension from the resources.
/**
* Set the extension of the patch.
*
* @param extension The name of the extension resource.
*/
@Suppress("NOTHING_TO_INLINE")
inline fun extendWith(extension: String) = apply {
val classLoader = object {}.javaClass.classLoader
extensionInputStream = Supplier {
classLoader.getResourceAsStream(extension) ?: throw PatchException("Extension \"$extension\" not found")
}
}
override fun build() = BytecodePatch(
name,
description,
use,
compatiblePackages,
dependencies,
options,
extensionInputStream,
executionBlock,
finalizeBlock,
)
}
/**
* Create a new [BytecodePatch].
*
* @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
* @param description The description of the patch.
* @param use Weather or not the patch should be used.
* @param block The block to build the patch.
*
* @return The created [BytecodePatch].
*/
fun bytecodePatch(
name: String? = null,
description: String? = null,
use: Boolean = true,
block: BytecodePatchBuilder.() -> Unit = {},
) = BytecodePatchBuilder(name, description, use).buildPatch(block) as BytecodePatch
/**
* A [RawResourcePatch] builder.
*
* @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
* @param description The description of the patch.
* @param use Weather or not the patch should be used.
*
* @constructor Create a new [RawResourcePatch] builder.
*/
class RawResourcePatchBuilder internal constructor(
name: String?,
description: String?,
use: Boolean,
) : PatchBuilder<ResourcePatchContext>(name, description, use) {
override fun build() = RawResourcePatch(
name,
description,
use,
compatiblePackages,
dependencies,
options,
executionBlock,
finalizeBlock,
)
}
/**
* Create a new [RawResourcePatch].
*
* @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
* @param description The description of the patch.
* @param use Weather or not the patch should be used.
* @param block The block to build the patch.
* @return The created [RawResourcePatch].
*/
fun rawResourcePatch(
name: String? = null,
description: String? = null,
use: Boolean = true,
block: RawResourcePatchBuilder.() -> Unit = {},
) = RawResourcePatchBuilder(name, description, use).buildPatch(block) as RawResourcePatch
/**
* A [ResourcePatch] builder.
*
* @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
* @param description The description of the patch.
* @param use Weather or not the patch should be used.
*
* @constructor Create a new [ResourcePatch] builder.
*/
class ResourcePatchBuilder internal constructor(
name: String?,
description: String?,
use: Boolean,
) : PatchBuilder<ResourcePatchContext>(name, description, use) {
override fun build() = ResourcePatch(
name,
description,
use,
compatiblePackages,
dependencies,
options,
executionBlock,
finalizeBlock,
)
}
/**
* Create a new [ResourcePatch].
*
* @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
* @param description The description of the patch.
* @param use Weather or not the patch should be used.
* @param block The block to build the patch.
*
* @return The created [ResourcePatch].
*/
fun resourcePatch(
name: String? = null,
description: String? = null,
use: Boolean = true,
block: ResourcePatchBuilder.() -> Unit = {},
) = ResourcePatchBuilder(name, description, use).buildPatch(block) as ResourcePatch
/**
* An exception thrown when patching.
*
* @param errorMessage The exception message.
* @param cause The corresponding [Throwable].
*/
class PatchException(errorMessage: String?, cause: Throwable?) : Exception(errorMessage, cause) {
constructor(errorMessage: String) : this(errorMessage, null)
constructor(cause: Throwable) : this(cause.message, cause)
}
/**
* A result of executing a [Patch].
*
* @param patch The [Patch] that was executed.
* @param exception The [PatchException] thrown, if any.
*/
class PatchResult internal constructor(val patch: Patch<*>, val exception: PatchException? = null)
/**
* A loader for patches.
*
* Loads unnamed patches from JAR or DEX files declared as public static fields
* or returned by public static and non-parametrized methods.
*
* @param byPatchesFile The patches associated by the patches file they were loaded from.
*/
sealed class PatchLoader private constructor(
val byPatchesFile: Map<File, Set<Patch<*>>>,
) : Set<Patch<*>> by byPatchesFile.values.flatten().toSet() {
/**
* @param patchesFiles A set of JAR or DEX files to load the patches from.
* @param getBinaryClassNames A function that returns the binary names of all classes accessible by the class loader.
* @param classLoader The [ClassLoader] to use for loading the classes.
*/
private constructor(
patchesFiles: Set<File>,
getBinaryClassNames: (patchesFile: File) -> List<String>,
classLoader: ClassLoader,
) : this(classLoader.loadPatches(patchesFiles.associateWith { getBinaryClassNames(it).toSet() }))
/**
* A [PatchLoader] for JAR files.
*
* @param patchesFiles The JAR files to load the patches from.
*
* @constructor Create a new [PatchLoader] for JAR files.
*/
class Jar(patchesFiles: Set<File>) :
PatchLoader(
patchesFiles,
{ file ->
JarFile(file).entries().toList().filter { it.name.endsWith(".class") }
.map { it.name.substringBeforeLast('.').replace('/', '.') }
},
URLClassLoader(patchesFiles.map { it.toURI().toURL() }.toTypedArray()),
)
/**
* A [PatchLoader] for [Dex] files.
*
* @param patchesFiles The DEX files to load the patches from.
* @param optimizedDexDirectory The directory to store optimized DEX files in.
* This parameter is deprecated and has no effect since API level 26.
*
* @constructor Create a new [PatchLoader] for [Dex] files.
*/
class Dex(patchesFiles: Set<File>, optimizedDexDirectory: File? = null) :
PatchLoader(
patchesFiles,
{ patchBundle ->
MultiDexIO.readDexFile(true, patchBundle, BasicDexFileNamer(), null, null).classes
.map { classDef ->
classDef.type.substring(1, classDef.length - 1)
}
},
DexClassLoader(
patchesFiles.joinToString(File.pathSeparator) { it.absolutePath },
optimizedDexDirectory?.absolutePath,
null,
this::class.java.classLoader,
),
)
// Companion object required for unit tests.
private companion object {
val Class<*>.isPatch get() = Patch::class.java.isAssignableFrom(this)
/**
* Public static fields that are patches.
*/
private val Class<*>.patchFields
get() = fields.filter { field ->
field.type.isPatch && field.canAccess()
}.map { field ->
field.get(null) as Patch<*>
}
/**
* Public static and non-parametrized methods that return patches.
*/
private val Class<*>.patchMethods
get() = methods.filter { method ->
method.returnType.isPatch && method.parameterCount == 0 && method.canAccess()
}.map { method ->
method.invoke(null) as Patch<*>
}
/**
* Loads unnamed patches declared as public static fields
* or returned by public static and non-parametrized methods.
*
* @param binaryClassNamesByPatchesFile The binary class name of the classes to load the patches from
* associated by the patches file.
*
* @return The loaded patches associated by the patches file.
*/
private fun ClassLoader.loadPatches(binaryClassNamesByPatchesFile: Map<File, Set<String>>) =
binaryClassNamesByPatchesFile.mapValues { (_, binaryClassNames) ->
binaryClassNames.asSequence().map {
loadClass(it)
}.flatMap {
it.patchFields + it.patchMethods
}.filter {
it.name != null
}.toSet()
}
private fun Member.canAccess(): Boolean {
if (this is Method && parameterCount != 0) return false
return Modifier.isStatic(modifiers) && Modifier.isPublic(modifiers)
}
}
}
/**
* Loads patches from JAR files declared as public static fields
* or returned by public static and non-parametrized methods.
* Patches with no name are not loaded.
*
* @param patchesFiles The JAR files to load the patches from.
*
* @return The loaded patches.
*/
fun loadPatchesFromJar(patchesFiles: Set<File>) =
PatchLoader.Jar(patchesFiles)
/**
* Loads patches from DEX files declared as public static fields
* or returned by public static and non-parametrized methods.
* Patches with no name are not loaded.
*
* @param patchesFiles The DEX files to load the patches from.
*
* @return The loaded patches.
*/
fun loadPatchesFromDex(patchesFiles: Set<File>, optimizedDexDirectory: File? = null) =
PatchLoader.Dex(patchesFiles, optimizedDexDirectory)

View File

@@ -1,9 +0,0 @@
package app.revanced.patcher.patch
import java.util.function.Supplier
/**
* A common interface for contexts such as [ResourcePatchContext] and [BytecodePatchContext].
*/
sealed interface PatchContext<T> : Supplier<T>

View File

@@ -1,233 +0,0 @@
package app.revanced.patcher.patch
import app.revanced.patcher.InternalApi
import app.revanced.patcher.PackageMetadata
import app.revanced.patcher.PatcherConfig
import app.revanced.patcher.PatcherResult
import app.revanced.patcher.util.Document
import brut.androlib.AaptInvoker
import brut.androlib.ApkDecoder
import brut.androlib.apk.UsesFramework
import brut.androlib.res.Framework
import brut.androlib.res.ResourcesDecoder
import brut.androlib.res.decoder.AndroidManifestPullStreamDecoder
import brut.androlib.res.decoder.AndroidManifestResourceParser
import brut.androlib.res.xml.ResXmlUtils
import brut.directory.ExtFile
import java.io.InputStream
import java.io.OutputStream
import java.nio.file.Files
import java.util.logging.Logger
/**
* A context for patches containing the current state of resources.
*
* @param packageMetadata The [PackageMetadata] of the apk file.
* @param config The [PatcherConfig] used to create this context.
*/
class ResourcePatchContext internal constructor(
private val packageMetadata: PackageMetadata,
private val config: PatcherConfig,
) : PatchContext<PatcherResult.PatchedResources?> {
private val logger = Logger.getLogger(ResourcePatchContext::class.java.name)
/**
* Read a document from an [InputStream].
*/
fun document(inputStream: InputStream) = Document(inputStream)
/**
* Read and write documents in the [PatcherConfig.apkFiles].
*/
fun document(path: String) = Document(get(path))
/**
* Set of resources from [PatcherConfig.apkFiles] to delete.
*/
private val deleteResources = mutableSetOf<String>()
/**
* Decode resources of [PatcherConfig.apkFile].
*
* @param mode The [ResourceMode] to use.
*/
internal fun decodeResources(mode: ResourceMode) = with(packageMetadata.apkInfo) {
config.initializeTemporaryFilesDirectories()
// Needed to decode resources.
val resourcesDecoder = ResourcesDecoder(config.resourceConfig, this)
if (mode == ResourceMode.FULL) {
logger.info("Decoding resources")
resourcesDecoder.decodeResources(config.apkFiles)
resourcesDecoder.decodeManifest(config.apkFiles)
// Needed to record uncompressed files.
ApkDecoder(this, config.resourceConfig).recordUncompressedFiles(resourcesDecoder.resFileMapping)
usesFramework =
UsesFramework().apply {
ids = resourcesDecoder.resTable.listFramePackages().map { it.id }
}
} else {
logger.info("Decoding app manifest")
// Decode manually instead of using resourceDecoder.decodeManifest
// because it does not support decoding to an OutputStream.
AndroidManifestPullStreamDecoder(
AndroidManifestResourceParser(resourcesDecoder.resTable),
resourcesDecoder.newXmlSerializer(),
).decode(
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.
// AndroidManifestPullStreamDecoder.decode() sets metadata.apkInfo.
packageMetadata.let { metadata ->
metadata.packageName = resourcesDecoder.resTable.packageRenamed
versionInfo.let {
metadata.packageVersion = it.versionName ?: it.versionCode
}
/*
The ResTable if flagged as sparse if the main package is not loaded, which is the case here,
because ResourcesDecoder.decodeResources loads the main package
and not AndroidManifestPullStreamDecoder.decode.
See ARSCDecoder.readTableType for more info.
Set this to false again to prevent the ResTable from being flagged as sparse falsely.
*/
metadata.apkInfo.sparseResources = false
}
}
}
/**
* Compile resources in [PatcherConfig.apkFiles].
*
* @return The [PatcherResult.PatchedResources].
*/
@InternalApi
override fun get(): PatcherResult.PatchedResources? {
if (config.resourceMode == ResourceMode.NONE) return null
logger.info("Compiling modified resources")
val resources = config.patchedFiles.resolve("resources").also { it.mkdirs() }
val resourcesApkFile =
if (config.resourceMode == ResourceMode.FULL) {
resources.resolve("resources.apk").apply {
// Compile the resources.apk file.
AaptInvoker(
config.resourceConfig,
packageMetadata.apkInfo,
).invoke(
resources.resolve("resources.apk"),
config.apkFiles.resolve("AndroidManifest.xml").also {
ResXmlUtils.fixingPublicAttrsInProviderAttributes(it)
},
config.apkFiles.resolve("res"),
null,
null,
packageMetadata.apkInfo.usesFramework.let { usesFramework ->
usesFramework.ids.map { id ->
Framework(config.resourceConfig).getFrameworkApk(id, usesFramework.tag)
}.toTypedArray()
},
)
}
} else {
null
}
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 ResourcePatchContext#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,
)
}
/**
* 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].
*/
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)
}
}
}
}
/**
* Mark a file for deletion when the APK is rebuilt.
*
* @param name The name of the file to delete.
*/
fun delete(name: String) = deleteResources.add(name)
/**
* How to handle resources decoding and compiling.
*/
internal enum class ResourceMode {
/**
* Decode and compile all resources.
*/
FULL,
/**
* Only extract resources from the APK.
* The AndroidManifest.xml and resources inside /res are not decoded or compiled.
*/
RAW_ONLY,
/**
* Do not decode or compile any resources.
*/
NONE,
}
}

View File

@@ -1,29 +0,0 @@
package app.revanced.patcher.util
import app.revanced.patcher.util.proxy.ClassProxy
import com.android.tools.smali.dexlib2.iface.ClassDef
/**
* A list of classes and proxies.
*
* @param classes The classes to be backed by proxies.
*/
class ProxyClassList internal constructor(classes: MutableList<ClassDef>) : MutableList<ClassDef> by classes {
internal val proxyPool = mutableListOf<ClassProxy>()
/**
* Replace all classes with their mutated versions.
*/
internal fun replaceClasses() =
proxyPool.removeIf { proxy ->
// If the proxy is unused, return false to keep it in the proxies list.
if (!proxy.resolved) return@removeIf false
// If it has been used, replace the original class with the mutable class.
remove(proxy.immutableClass)
add(proxy.mutableClass)
// Return true to remove the proxy from the proxies list.
return@removeIf true
}
}

View File

@@ -1,35 +0,0 @@
package app.revanced.patcher.util.proxy
import app.revanced.patcher.util.proxy.mutableTypes.MutableClass
import com.android.tools.smali.dexlib2.iface.ClassDef
/**
* A proxy class for a [ClassDef].
*
* A class proxy simply holds a reference to the original class
* and allocates a mutable clone for the original class if needed.
*
* @param immutableClass The class to proxy.
*/
class ClassProxy internal constructor(
val immutableClass: ClassDef,
) {
/**
* Weather the proxy was actually used.
*/
internal var resolved = false
/**
* The mutable clone of the original class.
*
* Note: This is only allocated if the proxy is actually used.
*/
val mutableClass by lazy {
resolved = true
if (immutableClass is MutableClass) {
immutableClass
} else {
MutableClass(immutableClass)
}
}
}

View File

@@ -1,62 +0,0 @@
package app.revanced.patcher.util.proxy.mutableTypes
import app.revanced.patcher.util.proxy.mutableTypes.MutableAnnotation.Companion.toMutable
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethodParameter.Companion.toMutable
import com.android.tools.smali.dexlib2.HiddenApiRestriction
import com.android.tools.smali.dexlib2.base.reference.BaseMethodReference
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
import com.android.tools.smali.dexlib2.iface.Method
class MutableMethod(method: Method) :
BaseMethodReference(),
Method {
private var definingClass = method.definingClass
private var name = method.name
private var accessFlags = method.accessFlags
private var returnType = method.returnType
// Create own mutable MethodImplementation (due to not being able to change members like register count)
private val _implementation by lazy { method.implementation?.let { MutableMethodImplementation(it) } }
private val _annotations by lazy { method.annotations.map { annotation -> annotation.toMutable() }.toMutableSet() }
private val _parameters by lazy { method.parameters.map { parameter -> parameter.toMutable() }.toMutableList() }
private val _parameterTypes by lazy { method.parameterTypes.toMutableList() }
private val _hiddenApiRestrictions by lazy { method.hiddenApiRestrictions }
fun setDefiningClass(definingClass: String) {
this.definingClass = definingClass
}
fun setName(name: String) {
this.name = name
}
fun setAccessFlags(accessFlags: Int) {
this.accessFlags = accessFlags
}
fun setReturnType(returnType: String) {
this.returnType = returnType
}
override fun getDefiningClass(): String = definingClass
override fun getName(): String = name
override fun getParameterTypes(): MutableList<CharSequence> = _parameterTypes
override fun getReturnType(): String = returnType
override fun getAnnotations(): MutableSet<MutableAnnotation> = _annotations
override fun getAccessFlags(): Int = accessFlags
override fun getHiddenApiRestrictions(): MutableSet<HiddenApiRestriction> = _hiddenApiRestrictions
override fun getParameters(): MutableList<MutableMethodParameter> = _parameters
override fun getImplementation(): MutableMethodImplementation? = _implementation
companion object {
fun Method.toMutable(): MutableMethod = MutableMethod(this)
}
}

View File

@@ -1,29 +0,0 @@
package app.revanced.patcher.util.proxy.mutableTypes
import app.revanced.patcher.util.proxy.mutableTypes.MutableAnnotation.Companion.toMutable
import com.android.tools.smali.dexlib2.base.BaseMethodParameter
import com.android.tools.smali.dexlib2.iface.MethodParameter
// TODO: finish overriding all members if necessary
class MutableMethodParameter(parameter: MethodParameter) :
BaseMethodParameter(),
MethodParameter {
private var type = parameter.type
private var name = parameter.name
private var signature = parameter.signature
private val _annotations by lazy {
parameter.annotations.map { annotation -> annotation.toMutable() }.toMutableSet()
}
override fun getType(): String = type
override fun getName(): String? = name
override fun getSignature(): String? = signature
override fun getAnnotations(): MutableSet<MutableAnnotation> = _annotations
companion object {
fun MethodParameter.toMutable(): MutableMethodParameter = MutableMethodParameter(this)
}
}

View File

@@ -1,28 +0,0 @@
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
import app.revanced.patcher.util.proxy.mutableTypes.MutableAnnotationElement.Companion.toMutable
import com.android.tools.smali.dexlib2.base.value.BaseAnnotationEncodedValue
import com.android.tools.smali.dexlib2.iface.AnnotationElement
import com.android.tools.smali.dexlib2.iface.value.AnnotationEncodedValue
class MutableAnnotationEncodedValue(annotationEncodedValue: AnnotationEncodedValue) :
BaseAnnotationEncodedValue(),
MutableEncodedValue {
private var type = annotationEncodedValue.type
private val _elements by lazy {
annotationEncodedValue.elements.map { annotationElement -> annotationElement.toMutable() }.toMutableSet()
}
override fun getType(): String = this.type
fun setType(type: String) {
this.type = type
}
override fun getElements(): MutableSet<out AnnotationElement> = _elements
companion object {
fun AnnotationEncodedValue.toMutable(): MutableAnnotationEncodedValue = MutableAnnotationEncodedValue(this)
}
}

View File

@@ -1,20 +0,0 @@
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
import app.revanced.patcher.util.proxy.mutableTypes.encodedValue.MutableEncodedValue.Companion.toMutable
import com.android.tools.smali.dexlib2.base.value.BaseArrayEncodedValue
import com.android.tools.smali.dexlib2.iface.value.ArrayEncodedValue
import com.android.tools.smali.dexlib2.iface.value.EncodedValue
class MutableArrayEncodedValue(arrayEncodedValue: ArrayEncodedValue) :
BaseArrayEncodedValue(),
MutableEncodedValue {
private val _value by lazy {
arrayEncodedValue.value.map { encodedValue -> encodedValue.toMutable() }.toMutableList()
}
override fun getValue(): MutableList<out EncodedValue> = _value
companion object {
fun ArrayEncodedValue.toMutable(): MutableArrayEncodedValue = MutableArrayEncodedValue(this)
}
}

View File

@@ -1,12 +0,0 @@
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
import com.android.tools.smali.dexlib2.base.value.BaseNullEncodedValue
import com.android.tools.smali.dexlib2.iface.value.ByteEncodedValue
class MutableNullEncodedValue :
BaseNullEncodedValue(),
MutableEncodedValue {
companion object {
fun ByteEncodedValue.toMutable(): MutableByteEncodedValue = MutableByteEncodedValue(this)
}
}

View File

@@ -1,10 +0,0 @@
package app.revanced.patcher.util.smali
import com.android.tools.smali.dexlib2.iface.instruction.Instruction
/**
* A class that represents a label for an instruction.
* @param name The label name.
* @param instruction The instruction that this label is for.
*/
data class ExternalLabel(internal val name: String, internal val instruction: Instruction)

View File

@@ -1,95 +0,0 @@
package app.revanced.patcher.util.smali
import app.revanced.patcher.extensions.InstructionExtensions.instructions
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcodes
import com.android.tools.smali.dexlib2.builder.BuilderInstruction
import com.android.tools.smali.dexlib2.writer.builder.DexBuilder
import com.android.tools.smali.smali.LexerErrorInterface
import com.android.tools.smali.smali.smaliFlexLexer
import com.android.tools.smali.smali.smaliParser
import com.android.tools.smali.smali.smaliTreeWalker
import org.antlr.runtime.CommonTokenStream
import org.antlr.runtime.TokenSource
import org.antlr.runtime.tree.CommonTreeNodeStream
import java.io.InputStreamReader
import java.util.*
private const val METHOD_TEMPLATE = """
.class LInlineCompiler;
.super Ljava/lang/Object;
.method %s dummyMethod(%s)V
.registers %d
%s
.end method
"""
class InlineSmaliCompiler {
companion object {
/**
* Compiles a string of Smali code to a list of instructions.
* Special registers (such as p0, p1) will only work correctly
* if the parameters and registers of the method are passed.
*/
fun compile(
instructions: String,
parameters: String,
registers: Int,
forStaticMethod: Boolean,
): List<BuilderInstruction> {
val input =
METHOD_TEMPLATE.format(
Locale.ENGLISH,
if (forStaticMethod) {
"static"
} else {
""
},
parameters,
registers,
instructions,
)
val reader = InputStreamReader(input.byteInputStream(), Charsets.UTF_8)
val lexer: LexerErrorInterface = smaliFlexLexer(reader, 15)
val tokens = CommonTokenStream(lexer as TokenSource)
val parser = smaliParser(tokens)
val result = parser.smali_file()
if (parser.numberOfSyntaxErrors > 0 || lexer.numberOfSyntaxErrors > 0) {
throw IllegalStateException(
"Encountered ${parser.numberOfSyntaxErrors} parser syntax errors and ${lexer.numberOfSyntaxErrors} lexer syntax errors!",
)
}
val treeStream = CommonTreeNodeStream(result.tree)
treeStream.tokenStream = tokens
val dexGen = smaliTreeWalker(treeStream)
dexGen.setDexBuilder(DexBuilder(Opcodes.getDefault()))
val classDef = dexGen.smali_file()
return classDef.methods.first().instructions.map { it as BuilderInstruction }
}
}
}
/**
* Compile lines of Smali code to a list of instructions.
*
* Note: Adding compiled instructions to an existing method with
* offset instructions WITHOUT specifying a parent method will not work.
* @param method The method to compile the instructions against.
* @returns A list of instructions.
*/
fun String.toInstructions(method: MutableMethod? = null): List<BuilderInstruction> {
return InlineSmaliCompiler.compile(
this,
method?.parameters?.joinToString("") { it } ?: "",
method?.implementation?.registerCount ?: 1,
method?.let { AccessFlags.STATIC.isSet(it.accessFlags) } ?: true,
)
}
/**
* Compile a line of Smali code to an instruction.
* @param templateMethod The method to compile the instructions against.
* @return The instruction.
*/
fun String.toInstruction(templateMethod: MutableMethod? = null) = this.toInstructions(templateMethod).first()

View File

@@ -1 +0,0 @@
version=${projectVersion}

View File

@@ -1,236 +0,0 @@
package app.revanced.patcher
import app.revanced.patcher.patch.*
import app.revanced.patcher.patch.BytecodePatchContext.LookupMaps
import app.revanced.patcher.util.ProxyClassList
import com.android.tools.smali.dexlib2.immutable.ImmutableClassDef
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
import io.mockk.*
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.assertAll
import java.util.logging.Logger
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
internal object PatcherTest {
private lateinit var patcher: Patcher
@BeforeEach
fun setUp() {
patcher = mockk<Patcher> {
// Can't mock private fields, until https://github.com/mockk/mockk/issues/1244 is resolved.
setPrivateField(
"config",
mockk<PatcherConfig> {
every { resourceMode } returns ResourcePatchContext.ResourceMode.NONE
},
)
setPrivateField(
"logger",
Logger.getAnonymousLogger(),
)
every { context.bytecodeContext.classes } returns mockk(relaxed = true)
every { this@mockk() } answers { callOriginal() }
}
}
@Test
fun `executes patches in correct order`() {
val executed = mutableListOf<String>()
val patches = setOf(
bytecodePatch { execute { executed += "1" } },
bytecodePatch {
dependsOn(
bytecodePatch {
execute { executed += "2" }
finalize { executed += "-2" }
},
bytecodePatch { execute { executed += "3" } },
)
execute { executed += "4" }
finalize { executed += "-1" }
},
)
assert(executed.isEmpty())
patches()
assertEquals(
listOf("1", "2", "3", "4", "-1", "-2"),
executed,
"Expected patches to be executed in correct order.",
)
}
@Test
fun `handles execution of patches correctly when exceptions occur`() {
val executed = mutableListOf<String>()
infix fun Patch<*>.produces(equals: List<String>) {
val patches = setOf(this)
patches()
assertEquals(equals, executed, "Expected patches to be executed in correct order.")
executed.clear()
}
// No patches execute successfully,
// because the dependency patch throws an exception inside the execute block.
bytecodePatch {
dependsOn(
bytecodePatch {
execute { throw PatchException("1") }
finalize { executed += "-2" }
},
)
execute { executed += "2" }
finalize { executed += "-1" }
} produces emptyList()
// The dependency patch is executed successfully,
// because only the dependant patch throws an exception inside the finalize block.
// Patches that depend on a failed patch should not be executed,
// but patches that are depended on by a failed patch should be executed.
bytecodePatch {
dependsOn(
bytecodePatch {
execute { executed += "1" }
finalize { executed += "-2" }
},
)
execute { throw PatchException("2") }
finalize { executed += "-1" }
} produces listOf("1", "-2")
// Because the finalize block of the dependency patch is executed after the finalize block of the dependant patch,
// the dependant patch executes successfully, but the dependency patch raises an exception in the finalize block.
bytecodePatch {
dependsOn(
bytecodePatch {
execute { executed += "1" }
finalize { throw PatchException("-2") }
},
)
execute { executed += "2" }
finalize { executed += "-1" }
} produces listOf("1", "2", "-1")
// The dependency patch is executed successfully,
// because the dependant patch raises an exception in the finalize block.
// Patches that depend on a failed patch should not be executed,
// but patches that are depended on by a failed patch should be executed.
bytecodePatch {
dependsOn(
bytecodePatch {
execute { executed += "1" }
finalize { executed += "-2" }
},
)
execute { executed += "2" }
finalize { throw PatchException("-1") }
} produces listOf("1", "2", "-2")
}
@Test
fun `throws if unmatched fingerprint match is delegated`() {
val patch = bytecodePatch {
execute {
// Fingerprint can never match.
val fingerprint = fingerprint { }
// Throws, because the fingerprint can't be matched.
fingerprint.patternMatch
}
}
assertTrue(
patch().exception != null,
"Expected an exception because the fingerprint can't match.",
)
}
@Test
fun `matches fingerprint`() {
every { patcher.context.bytecodeContext.classes } returns ProxyClassList(
mutableListOf(
ImmutableClassDef(
"class",
0,
null,
null,
null,
null,
null,
listOf(
ImmutableMethod(
"class",
"method",
emptyList(),
"V",
0,
null,
null,
null,
),
),
),
),
)
val fingerprint = fingerprint { returns("V") }
val fingerprint2 = fingerprint { returns("V") }
val fingerprint3 = fingerprint { returns("V") }
val patches = setOf(
bytecodePatch {
execute {
fingerprint.match(classes.first().methods.first())
fingerprint2.match(classes.first())
fingerprint3.originalClassDef
}
},
)
patches()
with(patcher.context.bytecodeContext) {
assertAll(
"Expected fingerprints to match.",
{ assertNotNull(fingerprint.originalClassDefOrNull) },
{ assertNotNull(fingerprint2.originalClassDefOrNull) },
{ assertNotNull(fingerprint3.originalClassDefOrNull) },
)
}
}
private operator fun Set<Patch<*>>.invoke(): List<PatchResult> {
every { patcher.context.executablePatches } returns toMutableSet()
every { patcher.context.bytecodeContext.lookupMaps } returns LookupMaps(patcher.context.bytecodeContext.classes)
every { with(patcher.context.bytecodeContext) { mergeExtension(any<BytecodePatch>()) } } just runs
return runBlocking { patcher().toList() }
}
private operator fun Patch<*>.invoke() = setOf(this)().first()
private fun Any.setPrivateField(field: String, value: Any) {
this::class.java.getDeclaredField(field).apply {
this.isAccessible = true
set(this@setPrivateField, value)
}
}
}

View File

@@ -1,90 +0,0 @@
@file:Suppress("unused")
package app.revanced.patcher.patch
import org.junit.jupiter.api.Test
import java.io.File
import kotlin.reflect.KFunction
import kotlin.reflect.full.companionObject
import kotlin.reflect.full.declaredFunctions
import kotlin.reflect.jvm.isAccessible
import kotlin.reflect.jvm.javaField
import kotlin.test.assertEquals
// region Test patches.
// Not loaded, because it's unnamed.
val publicUnnamedPatch = bytecodePatch {
}
// Loaded, because it's named.
val publicPatch = bytecodePatch("Public") {
}
// Not loaded, because it's private.
private val privateUnnamedPatch = bytecodePatch {
}
// Not loaded, because it's private.
private val privatePatch = bytecodePatch("Private") {
}
// Not loaded, because it's unnamed.
fun publicUnnamedPatchFunction() = publicUnnamedPatch
// Loaded, because it's named.
fun publicNamedPatchFunction() = bytecodePatch("Public") { }
// Not loaded, because it's parameterized.
fun parameterizedFunction(@Suppress("UNUSED_PARAMETER") param: Any) = publicNamedPatchFunction()
// Not loaded, because it's private.
private fun privateUnnamedPatchFunction() = privateUnnamedPatch
// Not loaded, because it's private.
private fun privateNamedPatchFunction() = privatePatch
// endregion
internal object PatchLoaderTest {
private const val LOAD_PATCHES_FUNCTION_NAME = "loadPatches"
private val TEST_PATCHES_CLASS = ::publicPatch.javaField!!.declaringClass.name
private val TEST_PATCHES_CLASS_LOADER = ::publicPatch.javaClass.classLoader
@Test
fun `loads patches correctly`() {
// Get instance of private PatchLoader.Companion class.
val patchLoaderCompanionObject = getPrivateFieldByType(
PatchLoader::class.java,
PatchLoader::class.companionObject!!.javaObjectType,
)
// Get private PatchLoader.Companion.loadPatches function from PatchLoader.Companion.
@Suppress("UNCHECKED_CAST")
val loadPatchesFunction = getPrivateFunctionByName(
patchLoaderCompanionObject,
LOAD_PATCHES_FUNCTION_NAME,
) as KFunction<Map<File, Set<Patch<*>>>>
// Call private PatchLoader.Companion.loadPatches function.
val patches = loadPatchesFunction.call(
patchLoaderCompanionObject,
TEST_PATCHES_CLASS_LOADER,
mapOf(File("patchesFile") to setOf(TEST_PATCHES_CLASS)),
).values.first()
assertEquals(
2,
patches.size,
"Expected 2 patches to be loaded, " +
"because there's only two named patches declared as public static fields " +
"or returned by public static and non-parametrized methods.",
)
}
private fun getPrivateFieldByType(cls: Class<*>, fieldType: Class<*>) =
cls.declaredFields.first { it.type == fieldType }.apply { isAccessible = true }.get(null)
private fun getPrivateFunctionByName(obj: Any, @Suppress("SameParameterValue") methodName: String) =
obj::class.declaredFunctions.first { it.name == methodName }.apply { isAccessible = true }
}

View File

@@ -1,49 +0,0 @@
package app.revanced.patcher.patch
import kotlin.test.Test
import kotlin.test.assertEquals
internal object PatchTest {
@Test
fun `can create patch with name`() {
val patch = bytecodePatch(name = "Test") {}
assertEquals("Test", patch.name)
}
@Test
fun `can create patch with compatible packages`() {
val patch = bytecodePatch(name = "Test") {
compatibleWith(
"compatible.package"("1.0.0"),
)
}
assertEquals(1, patch.compatiblePackages!!.size)
assertEquals("compatible.package", patch.compatiblePackages!!.first().first)
}
@Test
fun `can create patch with dependencies`() {
val patch = bytecodePatch(name = "Test") {
dependsOn(resourcePatch {})
}
assertEquals(1, patch.dependencies.size)
}
@Test
fun `can create patch with options`() {
val patch = bytecodePatch(name = "Test") {
val print by stringOption("print")
val custom = option<String>("custom")()
execute {
println(print)
println(custom.value)
}
}
assertEquals(2, patch.options.size)
}
}

View File

@@ -1,107 +0,0 @@
package app.revanced.patcher.util.smali
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.extensions.newLabel
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.builder.BuilderInstruction
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21c
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21t
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
import com.android.tools.smali.dexlib2.immutable.reference.ImmutableStringReference
import java.util.*
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
internal object InlineSmaliCompilerTest {
@Test
fun `outputs valid instruction`() {
val want = BuilderInstruction21c(Opcode.CONST_STRING, 0, ImmutableStringReference("Test")) as BuilderInstruction
val have = "const-string v0, \"Test\"".toInstruction()
assertInstructionsEqual(want, have)
}
@Test
fun `supports branching with own branches`() {
val method = createMethod()
val instructionCount = 8
val instructionIndex = instructionCount - 2
val targetIndex = instructionIndex - 1
method.addInstructions(
arrayOfNulls<String>(instructionCount).also {
Arrays.fill(it, "const/4 v0, 0x0")
}.joinToString("\n"),
)
method.addInstructionsWithLabels(
targetIndex,
"""
:test
const/4 v0, 0x1
if-eqz v0, :test
""",
)
val instruction = method.getInstruction<BuilderInstruction21t>(instructionIndex)
assertEquals(targetIndex, instruction.target.location.index)
}
@Test
fun `supports branching to outside branches`() {
val method = createMethod()
val instructionIndex = 3
val labelIndex = 1
method.addInstructions(
"""
const/4 v0, 0x1
const/4 v0, 0x0
""",
)
assertEquals(labelIndex, method.newLabel(labelIndex).location.index)
method.addInstructionsWithLabels(
method.implementation!!.instructions.size,
"""
const/4 v0, 0x1
if-eqz v0, :test
return-void
""",
ExternalLabel("test", method.getInstruction(1)),
)
val instruction = method.getInstruction<BuilderInstruction21t>(instructionIndex)
assertTrue(instruction.target.isPlaced, "Label was not placed")
assertEquals(labelIndex, instruction.target.location.index)
}
private fun createMethod(
name: String = "dummy",
returnType: String = "V",
accessFlags: Int = AccessFlags.STATIC.value,
registerCount: Int = 1,
) = ImmutableMethod(
"Ldummy;",
name,
emptyList(), // parameters
returnType,
accessFlags,
emptySet(),
emptySet(),
MutableMethodImplementation(registerCount),
).toMutable()
private fun assertInstructionsEqual(want: BuilderInstruction, have: BuilderInstruction) {
assertEquals(want.opcode, have.opcode)
assertEquals(want.format, have.format)
assertEquals(want.codeUnits, have.codeUnits)
}
}