├── .gitignore ├── .idea ├── .gitignore ├── compiler.xml ├── gradle.xml ├── misc.xml └── vcs.xml ├── LICENSE ├── README.md ├── build.gradle ├── demo ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── me │ │ └── fycz │ │ └── maple │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── me │ │ │ └── fycz │ │ │ └── demo │ │ │ └── MainActivity.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── values-night │ │ └── themes.xml │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ └── test │ └── java │ └── me │ └── fycz │ └── maple │ └── ExampleUnitTest.java ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── maple ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── me │ │ └── fycz │ │ └── maple │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ ├── me │ │ │ └── fycz │ │ │ │ └── maple │ │ │ │ ├── MapleBridge.java │ │ │ │ ├── MapleUtils.java │ │ │ │ ├── MethodHook.java │ │ │ │ └── MethodReplacement.java │ │ └── org │ │ │ └── apache │ │ │ └── commons │ │ │ └── lang3 │ │ │ └── reflect │ │ │ └── MemberUtilsX.java │ └── jni │ │ ├── CMakeLists.txt │ │ ├── elf_util.cpp │ │ ├── elf_util.h │ │ ├── logging.h │ │ └── maple.cpp │ └── test │ └── java │ └── me │ └── fycz │ └── maple │ └── ExampleUnitTest.java └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | /demo/release/ 17 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 20 | 21 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | 14 | 16 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2022 fengyuecanzhu 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Maple 2 | 3 | A Java hook framwork (Xposed style) for Android Runtime (ART) implemented by [LSPlant](https://github.com/LSPosed/LSPlant). 4 | 5 | ## Features 6 | 7 | * Support Android 5.0 - 13 (API level 21 - 33) 8 | * Support armeabi-v7a, arm64-v8a, x86, x86-64 9 | * Xposed style hook api 10 | 11 | ## Usage 12 | 13 | #### 1、Before usage 14 | 15 | Import method: 16 | 17 | Add the maven repository in your build.gradle(Project) (new version Android Studio please in settings.gradle): 18 | 19 | ```groovy 20 | allprojects { 21 | repositories { 22 | mavenCentral() 23 | } 24 | } 25 | ``` 26 | 27 | Then import the framework in your build.gradle(app): 28 | 29 | ```groovy 30 | dependencies { 31 | implementation "me.fycz.maple:maple:2.1" 32 | } 33 | ``` 34 | 35 | #### 2、Usage in codes 36 | 37 | All APIs are xposed style, you can use as simple as using xposed. 38 | 39 | kotlin: 40 | 41 | ```kotlin 42 | MapleUtils.findAndHookMethod( 43 | Activity::class.java, 44 | "onCreate", 45 | Bundle::class.java, 46 | object : MethodHook() { 47 | override fun beforeHookedMethod(param: MapleBridge.MethodHookParam) { 48 | //TODO: Hook before the method onCreate in the Activity is called. 49 | } 50 | } 51 | ) 52 | ``` 53 | 54 | java: 55 | 56 | ```java 57 | MapleUtils.findAndHookMethod( 58 | Activity.class, 59 | "onCreate", 60 | Bundle.class, 61 | new MethodHook() { 62 | @Override 63 | public void afterHookedMethod(MapleBridge.MethodHookParam param) throws Throwable { 64 | //TODO: Hook after the method onCreate in the Activity is called. 65 | } 66 | } 67 | ); 68 | ``` 69 | 70 | ## Credits 71 | 72 | Inspired by the following frameworks: 73 | 74 | - [LSPlant](https://github.com/LSPosed/LSPlant) 75 | - [Dobby](https://github.com/LSPosed/Dobby) 76 | - [Pine](https://github.com/canyie/Pine) 77 | - [XposedBridge](https://github.com/rovo89/XposedBridge) 78 | 79 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | ext.kotlin_version = '1.6.21' 4 | repositories { 5 | mavenCentral() 6 | } 7 | dependencies { 8 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 9 | } 10 | } 11 | plugins { 12 | id 'com.android.application' version '7.1.2' apply false 13 | id 'com.android.library' version '7.1.2' apply false 14 | } 15 | 16 | task clean(type: Delete) { 17 | delete rootProject.buildDir 18 | } -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /demo/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | } 5 | 6 | android { 7 | compileSdk 32 8 | 9 | defaultConfig { 10 | applicationId "me.fycz.maple" 11 | minSdk 21 12 | targetSdk 32 13 | versionCode 1 14 | versionName "1.0" 15 | 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | multiDexEnabled true 18 | 19 | ndk { 20 | abiFilters 'x86', 'x86_64','armeabi-v7a','arm64-v8a' 21 | } 22 | } 23 | 24 | buildFeatures { 25 | viewBinding true 26 | } 27 | 28 | buildTypes { 29 | release { 30 | minifyEnabled false 31 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 32 | } 33 | } 34 | compileOptions { 35 | sourceCompatibility JavaVersion.VERSION_11 36 | targetCompatibility JavaVersion.VERSION_11 37 | } 38 | } 39 | 40 | dependencies { 41 | implementation 'androidx.appcompat:appcompat:1.4.1' 42 | implementation 'com.google.android.material:material:1.5.0' 43 | testImplementation 'junit:junit:4.13.2' 44 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 45 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 46 | implementation "androidx.core:core-ktx:+" 47 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 48 | implementation(project(":maple")) 49 | } -------------------------------------------------------------------------------- /demo/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /demo/src/androidTest/java/me/fycz/maple/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package me.fycz.maple; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.test.platform.app.InstrumentationRegistry; 6 | import androidx.test.ext.junit.runners.AndroidJUnit4; 7 | 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | 11 | import static org.junit.Assert.*; 12 | 13 | /** 14 | * Instrumented test, which will execute on an Android device. 15 | * 16 | * @see Testing documentation 17 | */ 18 | @RunWith(AndroidJUnit4.class) 19 | public class ExampleInstrumentedTest { 20 | @Test 21 | public void useAppContext() { 22 | // Context of the app under test. 23 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 24 | assertEquals("me.fycz.maple", appContext.getPackageName()); 25 | } 26 | } -------------------------------------------------------------------------------- /demo/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 13 | 14 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /demo/src/main/java/me/fycz/demo/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package me.fycz.demo 2 | 3 | import android.annotation.SuppressLint 4 | import android.os.Bundle 5 | import android.view.View 6 | import android.widget.Button 7 | import android.widget.TextView 8 | import androidx.appcompat.app.AppCompatActivity 9 | import me.fycz.demo.databinding.ActivityMainBinding 10 | import me.fycz.maple.MapleBridge 11 | import me.fycz.maple.MapleUtils 12 | import me.fycz.maple.MethodHook 13 | import me.fycz.maple.MethodReplacement 14 | 15 | /** 16 | * @author fengyue 17 | * @date 2022/3/28 21:15 18 | */ 19 | class MainActivity : AppCompatActivity() { 20 | private lateinit var binding: ActivityMainBinding 21 | private var bridge: MapleBridge? = null 22 | 23 | override fun onCreate(savedInstanceState: Bundle?) { 24 | super.onCreate(savedInstanceState) 25 | binding = ActivityMainBinding.inflate(layoutInflater) 26 | setContentView(binding.root) 27 | init() 28 | } 29 | 30 | private fun init() { 31 | binding.btTest.setOnClickListener { 32 | console(it) 33 | console(normal("1", 2, 3F)) 34 | } 35 | 36 | binding.btHookBefore.setOnClickListener { 37 | console(it) 38 | try { 39 | bridge = 40 | MapleUtils.findAndHookMethod( 41 | "me.fycz.demo.MainActivity", 42 | this.classLoader, 43 | "normal", 44 | String::class.java, 45 | Int::class.java, 46 | Float::class.java, 47 | object : MethodHook() { 48 | override fun beforeHookedMethod(param: MapleBridge.MethodHookParam) { 49 | param.args[0] = "Hook函数Before-----" + param.args[0] 50 | } 51 | } 52 | ) 53 | } catch (e: Exception) { 54 | console(e.stackTraceToString()) 55 | } 56 | } 57 | 58 | binding.btHookAfter.setOnClickListener { 59 | console(it) 60 | try { 61 | bridge = 62 | MapleUtils.findAndHookMethod( 63 | "me.fycz.demo.MainActivity", 64 | this.classLoader, 65 | "normal", 66 | String::class.java, 67 | Int::class.java, 68 | Float::class.java, 69 | object : MethodHook() { 70 | override fun afterHookedMethod(param: MapleBridge.MethodHookParam) { 71 | console("Hook函数After-----" + param.args[0]) 72 | } 73 | } 74 | ) 75 | } catch (e: Exception) { 76 | console(e.stackTraceToString()) 77 | } 78 | } 79 | 80 | binding.btHookReplace.setOnClickListener { 81 | console(it) 82 | try { 83 | bridge = 84 | MapleUtils.findAndHookMethod( 85 | "me.fycz.demo.MainActivity", 86 | this.classLoader, 87 | "normal", 88 | String::class.java, 89 | Int::class.java, 90 | Float::class.java, 91 | object : MethodReplacement() { 92 | override fun replaceHookedMethod(param: MapleBridge.MethodHookParam?): Any { 93 | return "Hook函数replace-----" 94 | } 95 | } 96 | ) 97 | } catch (e: Exception) { 98 | console(e.stackTraceToString()) 99 | } 100 | } 101 | 102 | binding.btUnhook.setOnClickListener { 103 | console(it) 104 | bridge?.unhook() 105 | bridge = null 106 | } 107 | } 108 | 109 | private fun normal(a: String, b: Int, c: Float): String { 110 | return a + b + c 111 | } 112 | 113 | private fun console(v: View) { 114 | if (v is Button) 115 | binding.tvConsole.myAppend(v.text.toString()) 116 | } 117 | 118 | private fun console(s: String) { 119 | binding.tvConsole.myAppend(s) 120 | } 121 | 122 | @SuppressLint("SetTextI18n") 123 | fun TextView.myAppend(s: String) { 124 | text = text.toString() + "\n" + s 125 | } 126 | } -------------------------------------------------------------------------------- /demo/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /demo/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /demo/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | 17 | 18 | 24 | 25 | 31 | 32 | 38 | 39 | 45 | 46 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengyuecanzhu/Maple/ca3a57b49bef652d88268df7d92597d93451d03c/demo/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengyuecanzhu/Maple/ca3a57b49bef652d88268df7d92597d93451d03c/demo/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengyuecanzhu/Maple/ca3a57b49bef652d88268df7d92597d93451d03c/demo/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengyuecanzhu/Maple/ca3a57b49bef652d88268df7d92597d93451d03c/demo/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengyuecanzhu/Maple/ca3a57b49bef652d88268df7d92597d93451d03c/demo/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengyuecanzhu/Maple/ca3a57b49bef652d88268df7d92597d93451d03c/demo/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengyuecanzhu/Maple/ca3a57b49bef652d88268df7d92597d93451d03c/demo/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengyuecanzhu/Maple/ca3a57b49bef652d88268df7d92597d93451d03c/demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengyuecanzhu/Maple/ca3a57b49bef652d88268df7d92597d93451d03c/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengyuecanzhu/Maple/ca3a57b49bef652d88268df7d92597d93451d03c/demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /demo/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /demo/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /demo/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Maple 3 | -------------------------------------------------------------------------------- /demo/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /demo/src/test/java/me/fycz/maple/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package me.fycz.maple; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Enables namespacing of each library's R class so that its R class includes only the 19 | # resources declared in the library itself and none from the library's dependencies, 20 | # thereby reducing the size of the R class for that library 21 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengyuecanzhu/Maple/ca3a57b49bef652d88268df7d92597d93451d03c/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Mar 28 12:43:35 CST 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /maple/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /maple/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | id("maven-publish") 4 | id("signing") 5 | } 6 | 7 | android { 8 | compileSdk 32 9 | ndkVersion "23.1.7779620" 10 | 11 | defaultConfig { 12 | minSdk 21 13 | targetSdk 32 14 | 15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 16 | consumerProguardFiles "consumer-rules.pro" 17 | 18 | ndk { 19 | abiFilters 'x86', 'x86_64','armeabi-v7a','arm64-v8a' 20 | } 21 | externalNativeBuild { 22 | cmake { 23 | arguments += "-DANDROID_STL=c++_shared" 24 | } 25 | } 26 | } 27 | externalNativeBuild { 28 | cmake { 29 | path = file("src/main/jni/CMakeLists.txt") 30 | version "3.18.1" 31 | } 32 | } 33 | 34 | buildFeatures { 35 | prefab true 36 | } 37 | 38 | buildTypes { 39 | release { 40 | minifyEnabled false 41 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 42 | } 43 | } 44 | compileOptions { 45 | sourceCompatibility JavaVersion.VERSION_11 46 | targetCompatibility JavaVersion.VERSION_11 47 | } 48 | 49 | publishing { 50 | singleVariant("release") { 51 | withSourcesJar() 52 | withJavadocJar() 53 | } 54 | } 55 | } 56 | 57 | task sourcesJar(type: Jar) { 58 | from android.sourceSets.main.java.srcDirs 59 | classifier = 'sources' 60 | } 61 | 62 | 63 | publishing { 64 | publications { 65 | maven(MavenPublication) { 66 | group = "me.fycz.maple" 67 | artifactId = "maple" 68 | version = "2.1" 69 | afterEvaluate { 70 | artifact bundleReleaseAar 71 | artifact sourcesJar 72 | } 73 | pom { 74 | name = "Maple" 75 | description = "A hook framework for Android Runtime (ART)" 76 | url = "https://github.com/fengyuecanzhu/Maple" 77 | licenses { 78 | license { 79 | name = "GNU Lesser General Public License v3.0" 80 | url = "https://github.com/fengyuecanzhu/Maple/blob/master/LICENSE" 81 | } 82 | } 83 | developers { 84 | developer { 85 | name = "fengyuecanzhu" 86 | url = "https://github.com/fengyuecanzhu" 87 | } 88 | } 89 | scm { 90 | connection = "scm:git:https://github.com/fengyuecanzhu/Maple.git" 91 | url = "https://github.com/fengyuecanzhu/Maple" 92 | } 93 | } 94 | } 95 | } 96 | repositories { 97 | maven { 98 | name = "OSSRH" 99 | if (project.version.toString().endsWith("-SNAPSHOT")) { 100 | url = "https://s01.oss.sonatype.org/content/repositories/snapshots" 101 | } else { 102 | url = "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/" 103 | } 104 | credentials { 105 | username = findProperty("ossrhUsername") ?: System.getenv("OSSRH_USERNAME") 106 | password = findProperty("ossrhPassword") ?: System.getenv("OSSRH_PASSWORD") 107 | } 108 | } 109 | } 110 | } 111 | 112 | signing { 113 | def signingKey = findProperty("signingKey") 114 | def signingPassword = findProperty("signingPassword") 115 | def secretKeyRingFile = findProperty("signing.secretKeyRingFile") 116 | 117 | if (secretKeyRingFile != null && file(secretKeyRingFile).exists()) { 118 | sign publishing.publications 119 | } else if (signingKey != null) { 120 | useInMemoryPgpKeys(signingKey, signingPassword) 121 | sign publishing.publications 122 | } 123 | } 124 | 125 | dependencies { 126 | implementation("org.lsposed.lsplant:lsplant:5.2") 127 | implementation("io.github.vvb2060.ndk:dobby:1.2") 128 | compileOnly("androidx.annotation:annotation:1.3.0") 129 | implementation("org.apache.commons:commons-lang3:3.12.0") 130 | } -------------------------------------------------------------------------------- /maple/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fengyuecanzhu/Maple/ca3a57b49bef652d88268df7d92597d93451d03c/maple/consumer-rules.pro -------------------------------------------------------------------------------- /maple/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /maple/src/androidTest/java/me/fycz/maple/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package me.fycz.maple; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.test.platform.app.InstrumentationRegistry; 6 | import androidx.test.ext.junit.runners.AndroidJUnit4; 7 | 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | 11 | import static org.junit.Assert.*; 12 | 13 | /** 14 | * Instrumented test, which will execute on an Android device. 15 | * 16 | * @see Testing documentation 17 | */ 18 | @RunWith(AndroidJUnit4.class) 19 | public class ExampleInstrumentedTest { 20 | @Test 21 | public void useAppContext() { 22 | // Context of the app under test. 23 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 24 | assertEquals("me.fycz.maple.test", appContext.getPackageName()); 25 | } 26 | } -------------------------------------------------------------------------------- /maple/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /maple/src/main/java/me/fycz/maple/MapleBridge.java: -------------------------------------------------------------------------------- 1 | package me.fycz.maple; 2 | 3 | import java.lang.reflect.InvocationTargetException; 4 | import java.lang.reflect.Member; 5 | import java.lang.reflect.Method; 6 | import java.lang.reflect.Modifier; 7 | 8 | /** 9 | * @author fengyue 10 | * @date 2022/3/28 14:52 11 | */ 12 | public class MapleBridge { 13 | 14 | private Member target; 15 | private Method backup; 16 | public MethodHookParam param; 17 | private MethodHook callback; 18 | 19 | private MapleBridge() { 20 | } 21 | 22 | private native Method doHook(Member original, Method callback); 23 | 24 | public static native boolean doUnhook(Member target); 25 | 26 | public static native boolean hasInitHook(); 27 | 28 | public static native boolean isHooked(Member method); 29 | 30 | public static native boolean makeClassInheritable(Class clazz); 31 | 32 | public Object callback(Object[] args) throws Throwable { 33 | param = new MethodHookParam(); 34 | param.method = backup; 35 | if (Modifier.isStatic(target.getModifiers())) { 36 | param.thisObject = null; 37 | param.args = args; 38 | } else { 39 | param.thisObject = args[0]; 40 | param.args = new Object[args.length - 1]; 41 | System.arraycopy(args, 1, param.args, 0, args.length - 1); 42 | } 43 | // call "before method" callbacks 44 | try { 45 | callback.beforeHookedMethod(param); 46 | } catch (Throwable t) { 47 | MapleUtils.log(t); 48 | // reset result (ignoring what the unexpectedly exiting callback did) 49 | param.setResult(null); 50 | param.returnEarly = false; 51 | } 52 | // call original method if not requested otherwise 53 | if (!param.returnEarly) { 54 | try { 55 | param.setResult(backup.invoke(param.thisObject, param.args)); 56 | } catch (InvocationTargetException e) { 57 | param.setThrowable(e.getCause()); 58 | } 59 | } 60 | // call "after method" callbacks 61 | Object lastResult = param.getResult(); 62 | Throwable lastThrowable = param.getThrowable(); 63 | try { 64 | callback.afterHookedMethod(param); 65 | } catch (Throwable t) { 66 | MapleUtils.log(t); 67 | // reset to last result (ignoring what the unexpectedly exiting callback did) 68 | if (lastThrowable == null) 69 | param.setResult(lastResult); 70 | else 71 | param.setThrowable(lastThrowable); 72 | } 73 | // return 74 | if (param.hasThrowable()) 75 | throw param.getThrowable(); 76 | else { 77 | var result = param.getResult(); 78 | if (target instanceof Method) { 79 | var returnType = ((Method) target).getReturnType(); 80 | if (!returnType.isPrimitive()) 81 | return returnType.cast(result); 82 | } 83 | return result; 84 | } 85 | } 86 | 87 | public boolean unhook() { 88 | return doUnhook(target); 89 | } 90 | 91 | public static MapleBridge hookMethod(Member target, MethodHook callback) { 92 | if (!hasInitHook()) { 93 | throw new RuntimeException("Uninitialized the maple hook!"); 94 | } 95 | if (isHooked(target)) { 96 | doUnhook(target); 97 | } 98 | MapleBridge bridge = new MapleBridge(); 99 | try { 100 | var callbackMethod = MapleBridge.class.getDeclaredMethod("callback", Object[].class); 101 | var result = bridge.doHook(target, callbackMethod); 102 | if (result == null) return null; 103 | bridge.backup = result; 104 | bridge.target = target; 105 | bridge.callback = callback; 106 | } catch (Exception e) { 107 | e.printStackTrace(); 108 | } 109 | return bridge; 110 | } 111 | 112 | /** 113 | * Wraps information about the method call and allows to influence it. 114 | */ 115 | public static final class MethodHookParam { 116 | 117 | /** 118 | * The hooked method/constructor backup. 119 | */ 120 | public Member method; 121 | 122 | /** 123 | * The {@code this} reference for an instance method, or {@code null} for static methods. 124 | */ 125 | public Object thisObject; 126 | 127 | /** 128 | * Arguments to the method call. 129 | */ 130 | public Object[] args; 131 | 132 | private Object result = null; 133 | private Throwable throwable = null; 134 | public boolean returnEarly = false; 135 | 136 | /** 137 | * Returns the result of the method call. 138 | */ 139 | public Object getResult() { 140 | return result; 141 | } 142 | 143 | /** 144 | * Modify the result of the method call. 145 | * 146 | *

If called from {@link MethodHook#beforeHookedMethod}, it prevents the call to the original method. 147 | */ 148 | public void setResult(Object result) { 149 | this.result = result; 150 | this.throwable = null; 151 | this.returnEarly = true; 152 | } 153 | 154 | /** 155 | * Returns the {@link Throwable} thrown by the method, or {@code null}. 156 | */ 157 | public Throwable getThrowable() { 158 | return throwable; 159 | } 160 | 161 | /** 162 | * Returns true if an exception was thrown by the method. 163 | */ 164 | public boolean hasThrowable() { 165 | return throwable != null; 166 | } 167 | 168 | /** 169 | * Modify the exception thrown of the method call. 170 | * 171 | *

If called from {@link MethodHook#beforeHookedMethod}, it prevents the call to the original method. 172 | */ 173 | public void setThrowable(Throwable throwable) { 174 | this.throwable = throwable; 175 | this.result = null; 176 | this.returnEarly = true; 177 | } 178 | 179 | /** 180 | * Returns the result of the method call, or throws the Throwable caused by it. 181 | */ 182 | public Object getResultOrThrowable() throws Throwable { 183 | if (throwable != null) 184 | throw throwable; 185 | return result; 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /maple/src/main/java/me/fycz/maple/MapleUtils.java: -------------------------------------------------------------------------------- 1 | package me.fycz.maple; 2 | 3 | import android.content.res.AssetManager; 4 | import android.content.res.Resources; 5 | import android.os.Build; 6 | import android.util.Log; 7 | 8 | import androidx.annotation.NonNull; 9 | import androidx.annotation.Nullable; 10 | import androidx.annotation.RequiresApi; 11 | 12 | import org.apache.commons.lang3.ClassUtils; 13 | import org.apache.commons.lang3.reflect.MemberUtilsX; 14 | 15 | import java.io.ByteArrayOutputStream; 16 | import java.io.FileInputStream; 17 | import java.io.IOException; 18 | import java.io.InputStream; 19 | import java.lang.reflect.Constructor; 20 | import java.lang.reflect.Field; 21 | import java.lang.reflect.InvocationTargetException; 22 | import java.lang.reflect.Member; 23 | import java.lang.reflect.Method; 24 | import java.lang.reflect.Modifier; 25 | import java.math.BigInteger; 26 | import java.security.MessageDigest; 27 | import java.security.NoSuchAlgorithmException; 28 | import java.util.Arrays; 29 | import java.util.HashMap; 30 | import java.util.LinkedList; 31 | import java.util.List; 32 | import java.util.Objects; 33 | import java.util.Optional; 34 | import java.util.WeakHashMap; 35 | import java.util.concurrent.ConcurrentHashMap; 36 | import java.util.concurrent.atomic.AtomicInteger; 37 | 38 | /** 39 | * @author fengyue 40 | * @date 2022/3/28 14:57 41 | */ 42 | public final class MapleUtils { 43 | static { 44 | try { 45 | System.loadLibrary("maple"); 46 | } catch (Throwable e) { 47 | log(e); 48 | throw new RuntimeException("Load maple hook library failed!"); 49 | } 50 | } 51 | 52 | private static final String TAG = "MapleUtils"; 53 | /** 54 | * The system class loader which can be used to locate Android framework classes. 55 | * Application classes cannot be retrieved from it. 56 | * 57 | * @see ClassLoader#getSystemClassLoader 58 | */ 59 | private static final ClassLoader BOOTCLASSLOADER = MapleUtils.class.getClassLoader(); 60 | 61 | private static final ConcurrentHashMap fieldCache = new ConcurrentHashMap<>(); 62 | private static final ConcurrentHashMap methodCache = new ConcurrentHashMap<>(); 63 | private static final ConcurrentHashMap> constructorCache = new ConcurrentHashMap<>(); 64 | private static final WeakHashMap> additionalFields = new WeakHashMap<>(); 65 | 66 | private static final HashMap> sMethodDepth = new HashMap<>(); 67 | 68 | private static Field emptyFiled; 69 | private static Method emptyMethod; 70 | private static Constructor emptyConstructor; 71 | private static byte empty; 72 | private void empty(){} 73 | 74 | public static Field getEmptyFiled() throws NoSuchFieldException { 75 | if (emptyFiled == null) { 76 | emptyFiled = MapleUtils.class.getDeclaredField("empty"); 77 | } 78 | return emptyFiled; 79 | } 80 | 81 | public static Method getEmptyMethod() throws NoSuchMethodException { 82 | if (emptyMethod == null) { 83 | emptyMethod = MapleUtils.class.getDeclaredMethod("empty"); 84 | } 85 | return emptyMethod; 86 | } 87 | 88 | public static Constructor getEmptyConstructor() throws NoSuchMethodException { 89 | if (emptyConstructor == null){ 90 | emptyConstructor = MapleUtils.class.getDeclaredConstructor(); 91 | } 92 | return emptyConstructor; 93 | } 94 | 95 | private MapleUtils() { 96 | } 97 | 98 | public static void log(Throwable t) { 99 | String logStr = Log.getStackTraceString(t); 100 | log(logStr); 101 | } 102 | 103 | public static void log(String msg) { 104 | Log.e(TAG, msg); 105 | } 106 | 107 | /** 108 | * Note that we use object key instead of string here, because string calculation will lose all 109 | * the benefits of 'HashMap', this is basically the solution of performance traps. 110 | *

111 | * So in fact we only need to use the structural comparison results of the reflection object. 112 | * 113 | * @see benchmarks for ART 114 | * @see benchmarks for JVM 115 | */ 116 | private abstract static class MemberCacheKey { 117 | private final int hash; 118 | 119 | protected MemberCacheKey(int hash) { 120 | this.hash = hash; 121 | } 122 | 123 | @Override 124 | public abstract boolean equals(@Nullable Object obj); 125 | 126 | @Override 127 | public final int hashCode() { 128 | return hash; 129 | } 130 | 131 | static final class Constructor extends MemberCacheKey { 132 | private final Class clazz; 133 | private final Class[] parameters; 134 | private final boolean isExact; 135 | 136 | public Constructor(Class clazz, Class[] parameters, boolean isExact) { 137 | super(31 * Objects.hash(clazz, isExact) + Arrays.hashCode(parameters)); 138 | this.clazz = clazz; 139 | this.parameters = parameters; 140 | this.isExact = isExact; 141 | } 142 | 143 | @Override 144 | public boolean equals(Object o) { 145 | if (this == o) return true; 146 | if (!(o instanceof MemberCacheKey.Constructor)) return false; 147 | MemberCacheKey.Constructor that = (MemberCacheKey.Constructor) o; 148 | return isExact == that.isExact && Objects.equals(clazz, that.clazz) && Arrays.equals(parameters, that.parameters); 149 | } 150 | 151 | @NonNull 152 | @Override 153 | public String toString() { 154 | var str = clazz.getName() + getParametersString(parameters); 155 | if (isExact) { 156 | return str + "#exact"; 157 | } else { 158 | return str; 159 | } 160 | } 161 | } 162 | 163 | static final class Field extends MemberCacheKey { 164 | private final Class clazz; 165 | private final String name; 166 | 167 | public Field(Class clazz, String name) { 168 | super(Objects.hash(clazz, name)); 169 | this.clazz = clazz; 170 | this.name = name; 171 | } 172 | 173 | @Override 174 | public boolean equals(Object o) { 175 | if (this == o) return true; 176 | if (!(o instanceof MemberCacheKey.Field)) return false; 177 | MemberCacheKey.Field field = (MemberCacheKey.Field) o; 178 | return Objects.equals(clazz, field.clazz) && Objects.equals(name, field.name); 179 | } 180 | 181 | @NonNull 182 | @Override 183 | public String toString() { 184 | return clazz.getName() + "#" + name; 185 | } 186 | } 187 | 188 | static final class Method extends MemberCacheKey { 189 | private final Class clazz; 190 | private final String name; 191 | private final Class[] parameters; 192 | private final boolean isExact; 193 | 194 | public Method(Class clazz, String name, Class[] parameters, boolean isExact) { 195 | super(31 * Objects.hash(clazz, name, isExact) + Arrays.hashCode(parameters)); 196 | this.clazz = clazz; 197 | this.name = name; 198 | this.parameters = parameters; 199 | this.isExact = isExact; 200 | } 201 | 202 | @Override 203 | public boolean equals(Object o) { 204 | if (this == o) return true; 205 | if (!(o instanceof MemberCacheKey.Method)) return false; 206 | MemberCacheKey.Method method = (MemberCacheKey.Method) o; 207 | return isExact == method.isExact && Objects.equals(clazz, method.clazz) && Objects.equals(name, method.name) && Arrays.equals(parameters, method.parameters); 208 | } 209 | 210 | @NonNull 211 | @Override 212 | public String toString() { 213 | var str = clazz.getName() + '#' + name + getParametersString(parameters); 214 | if (isExact) { 215 | return str + "#exact"; 216 | } else { 217 | return str; 218 | } 219 | } 220 | } 221 | } 222 | 223 | /** 224 | * Look up a class with the specified class loader. 225 | * 226 | *

There are various allowed syntaxes for the class name, but it's recommended to use one of 227 | * these: 228 | *

234 | * 235 | * @param className The class name in one of the formats mentioned above. 236 | * @param classLoader The class loader, or {@code null} for the boot class loader. 237 | * @return A reference to the class. 238 | * @throws ClassNotFoundException In case the class was not found. 239 | */ 240 | public static Class findClass(String className, ClassLoader classLoader) throws ClassNotFoundException { 241 | if (classLoader == null) 242 | classLoader = BOOTCLASSLOADER; 243 | try { 244 | return ClassUtils.getClass(classLoader, className, false); 245 | } catch (ClassNotFoundException e) { 246 | throw e; 247 | } 248 | } 249 | /** 250 | * Look up and return a class if it exists. 251 | * Like {@link #findClass}, but doesn't throw an exception if the class doesn't exist. 252 | * 253 | * @param className The class name. 254 | * @param classLoader The class loader, or {@code null} for the boot class loader. 255 | * @return A reference to the class, or {@code null} if it doesn't exist. 256 | */ 257 | public static Class findClassIfExists(String className, ClassLoader classLoader) { 258 | try { 259 | return findClass(className, classLoader); 260 | } catch (ClassNotFoundException e) { 261 | return null; 262 | } 263 | } 264 | 265 | /** 266 | * Look up adn make a class inheritable. 267 | * It will make the class non-final and make all its private constructors protected. 268 | * @param className The class name. 269 | * @param classLoader The class loader, or {@code null} for the boot class loader. 270 | * @return Indicate whether the operation has succeed. 271 | */ 272 | public static boolean findAndMakeClassInheritable(String className, ClassLoader classLoader) throws ClassNotFoundException { 273 | return MapleBridge.makeClassInheritable(findClass(className, classLoader)); 274 | } 275 | 276 | /** 277 | * Look up a field in a class and set it to accessible. 278 | * 279 | * @param clazz The class which either declares or inherits the field. 280 | * @param fieldName The field name. 281 | * @return A reference to the field. 282 | * @throws NoSuchFieldException() In case the field was not found. 283 | */ 284 | public static Field findField(Class clazz, String fieldName) throws NoSuchFieldException { 285 | var key = new MemberCacheKey.Field(clazz, fieldName); 286 | if (fieldCache.containsKey(key)) { 287 | Field field = fieldCache.get(key); 288 | if (field == getEmptyFiled()) 289 | throw new NoSuchFieldException(key.toString()); 290 | return field; 291 | } 292 | try { 293 | Field field = findFieldRecursiveImpl(key.clazz, key.name); 294 | field.setAccessible(true); 295 | fieldCache.put(key, field); 296 | return field; 297 | } catch (NoSuchFieldException e) { 298 | fieldCache.put(key, getEmptyFiled()); 299 | throw new NoSuchFieldException(key.toString()); 300 | } 301 | } 302 | 303 | /** 304 | * Look up and return a field if it exists. 305 | * Like {@link #findField}, but doesn't throw an exception if the field doesn't exist. 306 | * 307 | * @param clazz The class which either declares or inherits the field. 308 | * @param fieldName The field name. 309 | * @return A reference to the field, or {@code null} if it doesn't exist. 310 | */ 311 | public static Field findFieldIfExists(Class clazz, String fieldName) { 312 | try { 313 | return findField(clazz, fieldName); 314 | } catch (NoSuchFieldException e) { 315 | return null; 316 | } 317 | } 318 | 319 | private static Field findFieldRecursiveImpl(Class clazz, String fieldName) throws NoSuchFieldException { 320 | try { 321 | return clazz.getDeclaredField(fieldName); 322 | } catch (NoSuchFieldException e) { 323 | while (true) { 324 | clazz = clazz.getSuperclass(); 325 | if (clazz == null || clazz.equals(Object.class)) 326 | break; 327 | 328 | try { 329 | return clazz.getDeclaredField(fieldName); 330 | } catch (NoSuchFieldException ignored) { 331 | } 332 | } 333 | throw e; 334 | } 335 | } 336 | 337 | /** 338 | * Returns the first field of the given type in a class. 339 | * Might be useful for Proguard'ed classes to identify fields with unique types. 340 | * 341 | * @param clazz The class which either declares or inherits the field. 342 | * @param type The type of the field. 343 | * @return A reference to the first field of the given type. 344 | * @throws NoSuchFieldException() In case no matching field was not found. 345 | */ 346 | public static Field findFirstFieldByExactType(Class clazz, Class type) throws NoSuchFieldException { 347 | Class clz = clazz; 348 | do { 349 | for (Field field : clz.getDeclaredFields()) { 350 | if (field.getType() == type) { 351 | field.setAccessible(true); 352 | return field; 353 | } 354 | } 355 | } while ((clz = clz.getSuperclass()) != null); 356 | 357 | throw new NoSuchFieldException("Field of type " + type.getName() + " in class " + clazz.getName()); 358 | } 359 | 360 | /** 361 | * Look up a method and hook it. See {@link #findAndHookMethod(String, ClassLoader, String, Object...)} 362 | * for details. 363 | */ 364 | public static MapleBridge findAndHookMethod(Class clazz, String methodName, Object... parameterTypesAndCallback) throws ClassNotFoundException, NoSuchMethodException { 365 | if (parameterTypesAndCallback.length == 0 || !(parameterTypesAndCallback[parameterTypesAndCallback.length - 1] instanceof MethodHook)) 366 | throw new IllegalArgumentException("no callback defined"); 367 | 368 | MethodHook callback = (MethodHook) parameterTypesAndCallback[parameterTypesAndCallback.length - 1]; 369 | Method m = findMethodExact(clazz, methodName, getParameterClasses(clazz.getClassLoader(), parameterTypesAndCallback)); 370 | 371 | return MapleBridge.hookMethod(m, callback); 372 | } 373 | 374 | /** 375 | * Look up a method and hook it. The last argument must be the callback for the hook. 376 | * 377 | *

This combines calls to {@link #findMethodExact(Class, String, Object...)} and 378 | * {@link MapleBridge#hookMethod}. 379 | * 380 | *

The method must be declared or overridden in the given class, inherited 381 | * methods are not considered! That's because each method implementation exists only once in 382 | * the memory, and when classes inherit it, they just get another reference to the implementation. 383 | * Hooking a method therefore applies to all classes inheriting the same implementation. You 384 | * have to expect that the hook applies to subclasses (unless they override the method), but you 385 | * shouldn't have to worry about hooks applying to superclasses, hence this "limitation". 386 | * There could be undesired or even dangerous hooks otherwise, e.g. if you hook 387 | * {@code SomeClass.equals()} and that class doesn't override the {@code equals()} on some ROMs, 388 | * making you hook {@code Object.equals()} instead. 389 | * 390 | *

There are two ways to specify the parameter types. If you already have a reference to the 391 | * {@link Class}, use that. For Android framework classes, you can often use something like 392 | * {@code String.class}. If you don't have the class reference, you can simply use the 393 | * full class name as a string, e.g. {@code java.lang.String} or {@code com.example.MyClass}. 394 | * It will be passed to {@link #findClass} with the same class loader that is used for the target 395 | * method, see its documentation for the allowed notations. 396 | * 397 | *

Primitive types, such as {@code int}, can be specified using {@code int.class} (recommended) 398 | * or {@code Integer.TYPE}. Note that {@code Integer.class} doesn't refer to {@code int} but to 399 | * {@code Integer}, which is a normal class (boxed primitive). Therefore it must not be used when 400 | * the method expects an {@code int} parameter - it has to be used for {@code Integer} parameters 401 | * though, so check the method signature in detail. 402 | * 403 | *

As last argument to this method (after the list of target method parameters), you need 404 | * to specify the callback that should be executed when the method is invoked. It's usually 405 | * an anonymous subclass of {@link MethodHook} or {@link MethodReplacement}. 406 | * 407 | *

Example 408 | *

 409 |      * // In order to hook this method ...
 410 |      * package com.example;
 411 |      * public class SomeClass {
 412 |      *   public int doSomething(String s, int i, MyClass m) {
 413 |      *     ...
 414 |      *   }
 415 |      * }
 416 |      *
 417 |      * // ... you can use this call:
 418 |      * findAndHookMethod("com.example.SomeClass", lpparam.classLoader, String.class, int.class, "com.example.MyClass", new MethodHook() {
 419 |      *   @Override
 420 |      *   protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
 421 |      *     String oldText = (String) param.args[0];
 422 |      *     Log.d("MyModule", oldText);
 423 |      *
 424 |      *     param.args[0] = "test";
 425 |      *     param.args[1] = 42; // auto-boxing is working here
 426 |      *     setBooleanField(param.args[2], "great", true);
 427 |      *
 428 |      *     // This would not work (as MyClass can't be resolved at compile time):
 429 |      *     //   MyClass myClass = (MyClass) param.args[2];
 430 |      *     //   myClass.great = true;
 431 |      *   }
 432 |      * });
 433 |      * 
434 | * 435 | * @param className The name of the class which implements the method. 436 | * @param classLoader The class loader for resolving the target and parameter classes. 437 | * @param methodName The target method name. 438 | * @param parameterTypesAndCallback The parameter types of the target method, plus the callback. 439 | * @return An object which can be used to remove the callback again. 440 | * @throws NoSuchMethodException In case the method was not found. 441 | * @throws ClassNotFoundException In case the target class or one of the parameter types couldn't be resolved. 442 | */ 443 | public static MapleBridge findAndHookMethod(String className, ClassLoader classLoader, String methodName, Object... parameterTypesAndCallback) throws ClassNotFoundException, NoSuchMethodException { 444 | return findAndHookMethod(findClass(className, classLoader), methodName, parameterTypesAndCallback); 445 | } 446 | 447 | /** 448 | * Look up a method and judge it hooked whether or not. See {@link #findMethodIsHooked(String, ClassLoader, String, Object...)} 449 | * for details. 450 | */ 451 | public static boolean findMethodIsHooked(Class clazz, String methodName, Object... parameterTypes) throws ClassNotFoundException, NoSuchMethodException { 452 | return MapleBridge.isHooked(findMethodExact(clazz, methodName, parameterTypes)); 453 | } 454 | 455 | /** 456 | * Look up a method and judge it hooked whether or not. 457 | * @param className The name of the class which implements the method. 458 | * @param classLoader The class loader for resolving the target and parameter classes. 459 | * @param methodName The target method name. 460 | * @param parameterTypes The parameter types of the target method. 461 | * @return The method is hooked whether or not. 462 | * @throws NoSuchMethodException In case the method was not found. 463 | * @throws ClassNotFoundException In case the target class or one of the parameter types couldn't be resolved. 464 | */ 465 | public static boolean findMethodIsHooked(String className, ClassLoader classLoader, String methodName, Object... parameterTypes) throws ClassNotFoundException, NoSuchMethodException { 466 | return MapleBridge.isHooked(findMethodExact(findClass(className, classLoader), methodName, parameterTypes)); 467 | } 468 | 469 | /** 470 | * Look up a method in a class and set it to accessible. 471 | * See {@link #findMethodExact(String, ClassLoader, String, Object...)} for details. 472 | */ 473 | public static Method findMethodExact(Class clazz, String methodName, Object... parameterTypes) throws ClassNotFoundException, NoSuchMethodException { 474 | return findMethodExact(clazz, methodName, getParameterClasses(clazz.getClassLoader(), parameterTypes)); 475 | } 476 | 477 | /** 478 | * Look up and return a method if it exists. 479 | * See {@link #findMethodExactIfExists(String, ClassLoader, String, Object...)} for details. 480 | */ 481 | public static Method findMethodExactIfExists(Class clazz, String methodName, Object... parameterTypes) { 482 | try { 483 | return findMethodExact(clazz, methodName, parameterTypes); 484 | } catch (ClassNotFoundException | NoSuchMethodException e) { 485 | return null; 486 | } 487 | } 488 | 489 | /** 490 | * Look up a method in a class and set it to accessible. 491 | * The method must be declared or overridden in the given class. 492 | * 493 | *

See {@link #findAndHookMethod(String, ClassLoader, String, Object...)} for details about 494 | * the method and parameter type resolution. 495 | * 496 | * @param className The name of the class which implements the method. 497 | * @param classLoader The class loader for resolving the target and parameter classes. 498 | * @param methodName The target method name. 499 | * @param parameterTypes The parameter types of the target method. 500 | * @return A reference to the method. 501 | * @throws NoSuchMethodException In case the method was not found. 502 | * @throws ClassNotFoundException In case the target class or one of the parameter types couldn't be resolved. 503 | */ 504 | public static Method findMethodExact(String className, ClassLoader classLoader, String methodName, Object... parameterTypes) throws ClassNotFoundException, NoSuchMethodException { 505 | return findMethodExact(findClass(className, classLoader), methodName, getParameterClasses(classLoader, parameterTypes)); 506 | } 507 | 508 | /** 509 | * Look up and return a method if it exists. 510 | * Like {@link #findMethodExact(String, ClassLoader, String, Object...)}, but doesn't throw an 511 | * exception if the method doesn't exist. 512 | * 513 | * @param className The name of the class which implements the method. 514 | * @param classLoader The class loader for resolving the target and parameter classes. 515 | * @param methodName The target method name. 516 | * @param parameterTypes The parameter types of the target method. 517 | * @return A reference to the method, or {@code null} if it doesn't exist. 518 | */ 519 | public static Method findMethodExactIfExists(String className, ClassLoader classLoader, String methodName, Object... parameterTypes) { 520 | try { 521 | return findMethodExact(className, classLoader, methodName, parameterTypes); 522 | } catch (ClassNotFoundException | NoSuchMethodException e) { 523 | return null; 524 | } 525 | } 526 | 527 | /** 528 | * Look up a method in a class and set it to accessible. 529 | * See {@link #findMethodExact(String, ClassLoader, String, Object...)} for details. 530 | * 531 | *

This variant requires that you already have reference to all the parameter types. 532 | */ 533 | public static Method findMethodExact(Class clazz, String methodName, Class... parameterTypes) throws NoSuchMethodException { 534 | var key = new MemberCacheKey.Method(clazz, methodName, parameterTypes, true); 535 | 536 | if (methodCache.containsKey(key)) { 537 | Method method = methodCache.get(key); 538 | if (method == getEmptyMethod()) 539 | throw new NoSuchMethodException(key.toString()); 540 | return method; 541 | } 542 | try { 543 | Method method = key.clazz.getDeclaredMethod(methodName, parameterTypes); 544 | method.setAccessible(true); 545 | methodCache.put(key, method); 546 | return method; 547 | } catch (NoSuchMethodException e) { 548 | methodCache.put(key, getEmptyMethod()); 549 | throw new NoSuchMethodException(key.toString()); 550 | } 551 | } 552 | 553 | /** 554 | * Returns an array of all methods declared/overridden in a class with the specified parameter types. 555 | * 556 | *

The return type is optional, it will not be compared if it is {@code null}. 557 | * Use {@code void.class} if you want to search for methods returning nothing. 558 | * 559 | * @param clazz The class to look in. 560 | * @param returnType The return type, or {@code null} (see above). 561 | * @param parameterTypes The parameter types. 562 | * @return An array with matching methods, all set to accessible already. 563 | */ 564 | public static Method[] findMethodsByExactParameters(Class clazz, Class returnType, Class... parameterTypes) { 565 | List result = new LinkedList<>(); 566 | for (Method method : clazz.getDeclaredMethods()) { 567 | if (returnType != null && returnType != method.getReturnType()) 568 | continue; 569 | 570 | Class[] methodParameterTypes = method.getParameterTypes(); 571 | if (parameterTypes.length != methodParameterTypes.length) 572 | continue; 573 | 574 | boolean match = true; 575 | for (int i = 0; i < parameterTypes.length; i++) { 576 | if (parameterTypes[i] != methodParameterTypes[i]) { 577 | match = false; 578 | break; 579 | } 580 | } 581 | 582 | if (!match) 583 | continue; 584 | 585 | method.setAccessible(true); 586 | result.add(method); 587 | } 588 | return result.toArray(new Method[result.size()]); 589 | } 590 | 591 | /** 592 | * Look up a method in a class and set it to accessible. 593 | * 594 | *

This does'nt only look for exact matches, but for the best match. All considered candidates 595 | * must be compatible with the given parameter types, i.e. the parameters must be assignable 596 | * to the method's formal parameters. Inherited methods are considered here. 597 | * 598 | * @param clazz The class which declares, inherits or overrides the method. 599 | * @param methodName The method name. 600 | * @param parameterTypes The types of the method's parameters. 601 | * @return A reference to the best-matching method. 602 | * @throws NoSuchMethodException In case no suitable method was found. 603 | */ 604 | public static Method findMethodBestMatch(Class clazz, String methodName, Class... parameterTypes) throws NoSuchMethodException { 605 | // then find the best match 606 | var key = new MemberCacheKey.Method(clazz, methodName, parameterTypes, false); 607 | 608 | if (methodCache.containsKey(key)) { 609 | Method method = methodCache.get(key); 610 | if (method == getEmptyMethod()) 611 | throw new NoSuchMethodException(key.toString()); 612 | return method; 613 | } 614 | 615 | try { 616 | Method method = findMethodExact(clazz, methodName, parameterTypes); 617 | methodCache.put(key, method); 618 | return method; 619 | } catch (NoSuchMethodException ignored) {} 620 | 621 | Method bestMatch = null; 622 | Class clz = key.clazz; 623 | boolean considerPrivateMethods = true; 624 | do { 625 | for (Method method : clz.getDeclaredMethods()) { 626 | // don't consider private methods of superclasses 627 | if (!considerPrivateMethods && Modifier.isPrivate(method.getModifiers())) 628 | continue; 629 | 630 | // compare name and parameters 631 | if (method.getName().equals(key.name) && ClassUtils.isAssignable( 632 | key.parameters, 633 | method.getParameterTypes(), 634 | true)) { 635 | // get accessible version of method 636 | if (bestMatch == null || MemberUtilsX.compareMethodFit( 637 | method, 638 | bestMatch, 639 | key.parameters) < 0) { 640 | bestMatch = method; 641 | } 642 | } 643 | } 644 | considerPrivateMethods = false; 645 | } while ((clz = clz.getSuperclass()) != null); 646 | 647 | if (bestMatch != null) { 648 | bestMatch.setAccessible(true); 649 | methodCache.put(key, bestMatch); 650 | return bestMatch; 651 | } else { 652 | NoSuchMethodException e = new NoSuchMethodException(key.toString()); 653 | methodCache.put(key, getEmptyMethod()); 654 | throw e; 655 | } 656 | } 657 | 658 | /** 659 | * Look up a method in a class and set it to accessible. 660 | * 661 | *

See {@link #findMethodBestMatch(Class, String, Class...)} for details. This variant 662 | * determines the parameter types from the classes of the given objects. 663 | */ 664 | public static Method findMethodBestMatch(Class clazz, String methodName, Object... args) throws NoSuchMethodException { 665 | return findMethodBestMatch(clazz, methodName, getParameterTypes(args)); 666 | } 667 | 668 | /** 669 | * Look up a method in a class and set it to accessible. 670 | * 671 | *

See {@link #findMethodBestMatch(Class, String, Class...)} for details. This variant 672 | * determines the parameter types from the classes of the given objects. For any item that is 673 | * {@code null}, the type is taken from {@code parameterTypes} instead. 674 | */ 675 | public static Method findMethodBestMatch(Class clazz, String methodName, Class[] parameterTypes, Object[] args) throws NoSuchMethodException { 676 | Class[] argsClasses = null; 677 | for (int i = 0; i < parameterTypes.length; i++) { 678 | if (parameterTypes[i] != null) 679 | continue; 680 | if (argsClasses == null) 681 | argsClasses = getParameterTypes(args); 682 | parameterTypes[i] = argsClasses[i]; 683 | } 684 | return findMethodBestMatch(clazz, methodName, parameterTypes); 685 | } 686 | 687 | /** 688 | * Returns an array with the classes of the given objects. 689 | */ 690 | public static Class[] getParameterTypes(Object... args) { 691 | Class[] clazzes = new Class[args.length]; 692 | for (int i = 0; i < args.length; i++) { 693 | clazzes[i] = (args[i] != null) ? args[i].getClass() : null; 694 | } 695 | return clazzes; 696 | } 697 | 698 | /** 699 | * Retrieve classes from an array, where each element might either be a Class 700 | * already, or a String with the full class name. 701 | */ 702 | private static Class[] getParameterClasses(ClassLoader classLoader, Object[] parameterTypesAndCallback) throws ClassNotFoundException { 703 | Class[] parameterClasses = null; 704 | for (int i = parameterTypesAndCallback.length - 1; i >= 0; i--) { 705 | Object type = parameterTypesAndCallback[i]; 706 | if (type == null) 707 | throw new ClassNotFoundException("parameter type must not be null", null); 708 | 709 | // ignore trailing callback 710 | if (type instanceof MethodHook) 711 | continue; 712 | 713 | if (parameterClasses == null) 714 | parameterClasses = new Class[i + 1]; 715 | 716 | if (type instanceof Class) 717 | parameterClasses[i] = (Class) type; 718 | else if (type instanceof String) 719 | parameterClasses[i] = findClass((String) type, classLoader); 720 | else 721 | throw new ClassNotFoundException("parameter type must either be specified as Class or String", null); 722 | } 723 | 724 | // if there are no arguments for the method 725 | if (parameterClasses == null) 726 | parameterClasses = new Class[0]; 727 | 728 | return parameterClasses; 729 | } 730 | 731 | /** 732 | * Returns an array of the given classes. 733 | */ 734 | public static Class[] getClassesAsArray(Class... clazzes) { 735 | return clazzes; 736 | } 737 | 738 | private static String getParametersString(Class... clazzes) { 739 | StringBuilder sb = new StringBuilder("("); 740 | boolean first = true; 741 | for (Class clazz : clazzes) { 742 | if (first) 743 | first = false; 744 | else 745 | sb.append(","); 746 | 747 | if (clazz != null) 748 | sb.append(clazz.getCanonicalName()); 749 | else 750 | sb.append("null"); 751 | } 752 | sb.append(")"); 753 | return sb.toString(); 754 | } 755 | 756 | /** 757 | * Look up a constructor of a class and set it to accessible. 758 | * See {@link #findMethodExact(String, ClassLoader, String, Object...)} for details. 759 | */ 760 | public static Constructor findConstructorExact(Class clazz, Object... parameterTypes) throws NoSuchMethodException, ClassNotFoundException { 761 | return findConstructorExact(clazz, getParameterClasses(clazz.getClassLoader(), parameterTypes)); 762 | } 763 | 764 | /** 765 | * Look up and return a constructor if it exists. 766 | * See {@link #findMethodExactIfExists(String, ClassLoader, String, Object...)} for details. 767 | */ 768 | public static Constructor findConstructorExactIfExists(Class clazz, Object... parameterTypes) { 769 | try { 770 | return findConstructorExact(clazz, parameterTypes); 771 | } catch (NoSuchMethodException | ClassNotFoundException e) { 772 | return null; 773 | } 774 | } 775 | 776 | /** 777 | * Look up a constructor of a class and set it to accessible. 778 | * See {@link #findMethodExact(String, ClassLoader, String, Object...)} for details. 779 | */ 780 | public static Constructor findConstructorExact(String className, ClassLoader classLoader, Object... parameterTypes) throws NoSuchMethodException, ClassNotFoundException { 781 | return findConstructorExact(findClass(className, classLoader), getParameterClasses(classLoader, parameterTypes)); 782 | } 783 | 784 | /** 785 | * Look up and return a constructor if it exists. 786 | * See {@link #findMethodExactIfExists(String, ClassLoader, String, Object...)} for details. 787 | */ 788 | public static Constructor findConstructorExactIfExists(String className, ClassLoader classLoader, Object... parameterTypes) { 789 | try { 790 | return findConstructorExact(className, classLoader, parameterTypes); 791 | } catch (NoSuchMethodException | ClassNotFoundException e) { 792 | return null; 793 | } 794 | } 795 | 796 | /** 797 | * Look up a constructor of a class and set it to accessible. 798 | * See {@link #findMethodExact(String, ClassLoader, String, Object...)} for details. 799 | */ 800 | public static Constructor findConstructorExact(Class clazz, Class... parameterTypes) throws NoSuchMethodException { 801 | var key = new MemberCacheKey.Constructor(clazz, parameterTypes, true); 802 | 803 | if (constructorCache.containsKey(key)) { 804 | Constructor constructor = constructorCache.get(key); 805 | if (constructor == getEmptyConstructor()) 806 | throw new NoSuchMethodException(key.toString()); 807 | return constructor; 808 | } 809 | 810 | try { 811 | Constructor constructor = key.clazz.getDeclaredConstructor(parameterTypes); 812 | constructor.setAccessible(true); 813 | constructorCache.put(key, constructor); 814 | return constructor; 815 | } catch (NoSuchMethodException e) { 816 | constructorCache.put(key, getEmptyConstructor()); 817 | throw new NoSuchMethodException(key.toString()); 818 | } 819 | } 820 | 821 | /** 822 | * Look up a constructor and hook it. See {@link #findAndHookMethod(String, ClassLoader, String, Object...)} 823 | * for details. 824 | */ 825 | public static MapleBridge findAndHookConstructor(Class clazz, Object... parameterTypesAndCallback) throws NoSuchMethodException, ClassNotFoundException { 826 | if (parameterTypesAndCallback.length == 0 || !(parameterTypesAndCallback[parameterTypesAndCallback.length - 1] instanceof MethodHook)) 827 | throw new IllegalArgumentException("no callback defined"); 828 | 829 | MethodHook callback = (MethodHook) parameterTypesAndCallback[parameterTypesAndCallback.length - 1]; 830 | Constructor m = findConstructorExact(clazz, getParameterClasses(clazz.getClassLoader(), parameterTypesAndCallback)); 831 | 832 | return MapleBridge.hookMethod(m, callback); 833 | } 834 | 835 | /** 836 | * Look up a constructor and hook it. See {@link #findAndHookMethod(String, ClassLoader, String, Object...)} 837 | * for details. 838 | */ 839 | public static MapleBridge findAndHookConstructor(String className, ClassLoader classLoader, Object... parameterTypesAndCallback) throws NoSuchMethodException, ClassNotFoundException { 840 | return findAndHookConstructor(findClass(className, classLoader), parameterTypesAndCallback); 841 | } 842 | 843 | /** 844 | * Look up a constructor in a class and set it to accessible. 845 | * 846 | *

See {@link #findMethodBestMatch(Class, String, Class...)} for details. 847 | */ 848 | public static Constructor findConstructorBestMatch(Class clazz, Class... parameterTypes) throws NoSuchMethodException { 849 | // then find the best match 850 | var key = new MemberCacheKey.Constructor(clazz, parameterTypes, false); 851 | if (constructorCache.containsKey(key)) { 852 | Constructor constructor = constructorCache.get(key); 853 | if (constructor == getEmptyConstructor()) 854 | throw new NoSuchMethodException(key.toString()); 855 | return constructor; 856 | } 857 | 858 | try { 859 | Constructor constructor = findConstructorExact(clazz, parameterTypes); 860 | constructorCache.put(key, constructor); 861 | return constructor; 862 | } catch (NoSuchMethodException ignored) { } 863 | 864 | Constructor bestMatch = null; 865 | Constructor[] constructors = key.clazz.getDeclaredConstructors(); 866 | for (Constructor constructor : constructors) { 867 | // compare name and parameters 868 | if (ClassUtils.isAssignable( 869 | key.parameters, 870 | constructor.getParameterTypes(), 871 | true)) { 872 | // get accessible version of method 873 | if (bestMatch == null || MemberUtilsX.compareConstructorFit( 874 | constructor, 875 | bestMatch, 876 | key.parameters) < 0) { 877 | bestMatch = constructor; 878 | } 879 | } 880 | } 881 | 882 | if (bestMatch != null) { 883 | bestMatch.setAccessible(true); 884 | constructorCache.put(key, bestMatch); 885 | return bestMatch; 886 | } else { 887 | NoSuchMethodException e = new NoSuchMethodException(key.toString()); 888 | constructorCache.put(key, getEmptyConstructor()); 889 | throw e; 890 | } 891 | } 892 | 893 | /** 894 | * Look up a constructor in a class and set it to accessible. 895 | * 896 | *

See {@link #findMethodBestMatch(Class, String, Class...)} for details. This variant 897 | * determines the parameter types from the classes of the given objects. 898 | */ 899 | public static Constructor findConstructorBestMatch(Class clazz, Object... args) throws NoSuchMethodException { 900 | return findConstructorBestMatch(clazz, getParameterTypes(args)); 901 | } 902 | 903 | /** 904 | * Look up a constructor in a class and set it to accessible. 905 | * 906 | *

See {@link #findMethodBestMatch(Class, String, Class...)} for details. This variant 907 | * determines the parameter types from the classes of the given objects. For any item that is 908 | * {@code null}, the type is taken from {@code parameterTypes} instead. 909 | */ 910 | public static Constructor findConstructorBestMatch(Class clazz, Class[] parameterTypes, Object[] args) throws NoSuchMethodException { 911 | Class[] argsClasses = null; 912 | for (int i = 0; i < parameterTypes.length; i++) { 913 | if (parameterTypes[i] != null) 914 | continue; 915 | if (argsClasses == null) 916 | argsClasses = getParameterTypes(args); 917 | parameterTypes[i] = argsClasses[i]; 918 | } 919 | return findConstructorBestMatch(clazz, parameterTypes); 920 | } 921 | 922 | 923 | /** 924 | * Returns the index of the first parameter declared with the given type. 925 | * 926 | * @throws NoSuchFieldException() if there is no parameter with that type. 927 | * @hide 928 | */ 929 | public static int getFirstParameterIndexByType(Member method, Class type) throws NoSuchFieldException { 930 | Class[] classes = (method instanceof Method) ? 931 | ((Method) method).getParameterTypes() : ((Constructor) method).getParameterTypes(); 932 | for (int i = 0; i < classes.length; i++) { 933 | if (classes[i] == type) { 934 | return i; 935 | } 936 | } 937 | throw new NoSuchFieldException("No parameter of type " + type + " found in " + method); 938 | } 939 | 940 | /** 941 | * Returns the index of the parameter declared with the given type, ensuring that there is exactly one such parameter. 942 | * 943 | * @throws NoSuchFieldException() if there is no or more than one parameter with that type. 944 | * @hide 945 | */ 946 | public static int getParameterIndexByType(Member method, Class type) throws NoSuchFieldException { 947 | Class[] classes = (method instanceof Method) ? 948 | ((Method) method).getParameterTypes() : ((Constructor) method).getParameterTypes(); 949 | int idx = -1; 950 | for (int i = 0; i < classes.length; i++) { 951 | if (classes[i] == type) { 952 | if (idx == -1) { 953 | idx = i; 954 | } else { 955 | throw new NoSuchFieldException("More than one parameter of type " + type + " found in " + method); 956 | } 957 | } 958 | } 959 | if (idx != -1) { 960 | return idx; 961 | } else { 962 | throw new NoSuchFieldException("No parameter of type " + type + " found in " + method); 963 | } 964 | } 965 | 966 | //################################################################################################# 967 | 968 | /** 969 | * Sets the value of an object field in the given object instance. A class reference is not sufficient! See also {@link #findField}. 970 | */ 971 | public static void setObjectField(Object obj, String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException { 972 | try { 973 | findField(obj.getClass(), fieldName).set(obj, value); 974 | } catch (IllegalAccessException e) { 975 | // should not happen 976 | log(e); 977 | throw new IllegalAccessException(e.getMessage()); 978 | } catch (IllegalArgumentException | NoSuchFieldException e) { 979 | throw e; 980 | } 981 | } 982 | 983 | /** 984 | * Sets the value of a {@code boolean} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. 985 | */ 986 | public static void setBooleanField(Object obj, String fieldName, boolean value) throws NoSuchFieldException, IllegalAccessException { 987 | try { 988 | findField(obj.getClass(), fieldName).setBoolean(obj, value); 989 | } catch (IllegalAccessException e) { 990 | // should not happen 991 | log(e); 992 | throw new IllegalAccessException(e.getMessage()); 993 | } catch (IllegalArgumentException | NoSuchFieldException e) { 994 | throw e; 995 | } 996 | } 997 | 998 | /** 999 | * Sets the value of a {@code byte} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. 1000 | */ 1001 | public static void setByteField(Object obj, String fieldName, byte value) throws NoSuchFieldException, IllegalAccessException { 1002 | try { 1003 | findField(obj.getClass(), fieldName).setByte(obj, value); 1004 | } catch (IllegalAccessException e) { 1005 | // should not happen 1006 | log(e); 1007 | throw new IllegalAccessException(e.getMessage()); 1008 | } catch (IllegalArgumentException | NoSuchFieldException e) { 1009 | throw e; 1010 | } 1011 | } 1012 | 1013 | /** 1014 | * Sets the value of a {@code char} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. 1015 | */ 1016 | public static void setCharField(Object obj, String fieldName, char value) throws NoSuchFieldException, IllegalAccessException { 1017 | try { 1018 | findField(obj.getClass(), fieldName).setChar(obj, value); 1019 | } catch (IllegalAccessException e) { 1020 | // should not happen 1021 | log(e); 1022 | throw new IllegalAccessException(e.getMessage()); 1023 | } catch (IllegalArgumentException | NoSuchFieldException e) { 1024 | throw e; 1025 | } 1026 | } 1027 | 1028 | /** 1029 | * Sets the value of a {@code double} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. 1030 | */ 1031 | public static void setDoubleField(Object obj, String fieldName, double value) throws NoSuchFieldException, IllegalAccessException { 1032 | try { 1033 | findField(obj.getClass(), fieldName).setDouble(obj, value); 1034 | } catch (IllegalAccessException e) { 1035 | // should not happen 1036 | log(e); 1037 | throw new IllegalAccessException(e.getMessage()); 1038 | } catch (IllegalArgumentException | NoSuchFieldException e) { 1039 | throw e; 1040 | } 1041 | } 1042 | 1043 | /** 1044 | * Sets the value of a {@code float} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. 1045 | */ 1046 | public static void setFloatField(Object obj, String fieldName, float value) throws NoSuchFieldException, IllegalAccessException { 1047 | try { 1048 | findField(obj.getClass(), fieldName).setFloat(obj, value); 1049 | } catch (IllegalAccessException e) { 1050 | // should not happen 1051 | log(e); 1052 | throw new IllegalAccessException(e.getMessage()); 1053 | } catch (IllegalArgumentException | NoSuchFieldException e) { 1054 | throw e; 1055 | } 1056 | } 1057 | 1058 | /** 1059 | * Sets the value of an {@code int} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. 1060 | */ 1061 | public static void setIntField(Object obj, String fieldName, int value) throws NoSuchFieldException, IllegalAccessException { 1062 | try { 1063 | findField(obj.getClass(), fieldName).setInt(obj, value); 1064 | } catch (IllegalAccessException e) { 1065 | // should not happen 1066 | log(e); 1067 | throw new IllegalAccessException(e.getMessage()); 1068 | } catch (IllegalArgumentException | NoSuchFieldException e) { 1069 | throw e; 1070 | } 1071 | } 1072 | 1073 | /** 1074 | * Sets the value of a {@code long} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. 1075 | */ 1076 | public static void setLongField(Object obj, String fieldName, long value) throws NoSuchFieldException, IllegalAccessException { 1077 | try { 1078 | findField(obj.getClass(), fieldName).setLong(obj, value); 1079 | } catch (IllegalAccessException e) { 1080 | // should not happen 1081 | log(e); 1082 | throw new IllegalAccessException(e.getMessage()); 1083 | } catch (IllegalArgumentException | NoSuchFieldException e) { 1084 | throw e; 1085 | } 1086 | } 1087 | 1088 | /** 1089 | * Sets the value of a {@code short} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. 1090 | */ 1091 | public static void setShortField(Object obj, String fieldName, short value) throws NoSuchFieldException, IllegalAccessException { 1092 | try { 1093 | findField(obj.getClass(), fieldName).setShort(obj, value); 1094 | } catch (IllegalAccessException e) { 1095 | // should not happen 1096 | log(e); 1097 | throw new IllegalAccessException(e.getMessage()); 1098 | } catch (IllegalArgumentException | NoSuchFieldException e) { 1099 | throw e; 1100 | } 1101 | } 1102 | 1103 | //################################################################################################# 1104 | 1105 | /** 1106 | * Returns the value of an object field in the given object instance. A class reference is not sufficient! See also {@link #findField}. 1107 | */ 1108 | public static Object getObjectField(Object obj, String fieldName) throws IllegalAccessException, NoSuchFieldException { 1109 | try { 1110 | return findField(obj.getClass(), fieldName).get(obj); 1111 | } catch (IllegalAccessException e) { 1112 | // should not happen 1113 | log(e); 1114 | throw new IllegalAccessException(e.getMessage()); 1115 | } catch (IllegalArgumentException | NoSuchFieldException e) { 1116 | throw e; 1117 | } 1118 | } 1119 | 1120 | /** 1121 | * For inner classes, returns the surrounding instance, i.e. the {@code this} reference of the surrounding class. 1122 | */ 1123 | public static Object getSurroundingThis(Object obj) throws NoSuchFieldException, IllegalAccessException { 1124 | return getObjectField(obj, "this$0"); 1125 | } 1126 | 1127 | /** 1128 | * Returns the value of a {@code boolean} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. 1129 | */ 1130 | @SuppressWarnings("BooleanMethodIsAlwaysInverted") 1131 | public static boolean getBooleanField(Object obj, String fieldName) throws IllegalAccessException, NoSuchFieldException { 1132 | try { 1133 | return findField(obj.getClass(), fieldName).getBoolean(obj); 1134 | } catch (IllegalAccessException e) { 1135 | // should not happen 1136 | log(e); 1137 | throw new IllegalAccessException(e.getMessage()); 1138 | } catch (IllegalArgumentException | NoSuchFieldException e) { 1139 | throw e; 1140 | } 1141 | } 1142 | 1143 | /** 1144 | * Returns the value of a {@code byte} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. 1145 | */ 1146 | public static byte getByteField(Object obj, String fieldName) throws IllegalAccessException, NoSuchFieldException { 1147 | try { 1148 | return findField(obj.getClass(), fieldName).getByte(obj); 1149 | } catch (IllegalAccessException e) { 1150 | // should not happen 1151 | log(e); 1152 | throw new IllegalAccessException(e.getMessage()); 1153 | } catch (IllegalArgumentException | NoSuchFieldException e) { 1154 | throw e; 1155 | } 1156 | } 1157 | 1158 | /** 1159 | * Returns the value of a {@code char} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. 1160 | */ 1161 | public static char getCharField(Object obj, String fieldName) throws NoSuchFieldException, IllegalAccessException { 1162 | try { 1163 | return findField(obj.getClass(), fieldName).getChar(obj); 1164 | } catch (IllegalAccessException e) { 1165 | // should not happen 1166 | log(e); 1167 | throw new IllegalAccessException(e.getMessage()); 1168 | } catch (IllegalArgumentException | NoSuchFieldException e) { 1169 | throw e; 1170 | } 1171 | } 1172 | 1173 | /** 1174 | * Returns the value of a {@code double} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. 1175 | */ 1176 | public static double getDoubleField(Object obj, String fieldName) throws IllegalAccessException, NoSuchFieldException { 1177 | try { 1178 | return findField(obj.getClass(), fieldName).getDouble(obj); 1179 | } catch (IllegalAccessException e) { 1180 | // should not happen 1181 | log(e); 1182 | throw new IllegalAccessException(e.getMessage()); 1183 | } catch (IllegalArgumentException | NoSuchFieldException e) { 1184 | throw e; 1185 | } 1186 | } 1187 | 1188 | /** 1189 | * Returns the value of a {@code float} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. 1190 | */ 1191 | public static float getFloatField(Object obj, String fieldName) throws NoSuchFieldException, IllegalAccessException { 1192 | try { 1193 | return findField(obj.getClass(), fieldName).getFloat(obj); 1194 | } catch (IllegalAccessException e) { 1195 | // should not happen 1196 | log(e); 1197 | throw new IllegalAccessException(e.getMessage()); 1198 | } catch (IllegalArgumentException | NoSuchFieldException e) { 1199 | throw e; 1200 | } 1201 | } 1202 | 1203 | /** 1204 | * Returns the value of an {@code int} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. 1205 | */ 1206 | public static int getIntField(Object obj, String fieldName) throws IllegalAccessException, NoSuchFieldException { 1207 | try { 1208 | return findField(obj.getClass(), fieldName).getInt(obj); 1209 | } catch (IllegalAccessException e) { 1210 | // should not happen 1211 | log(e); 1212 | throw new IllegalAccessException(e.getMessage()); 1213 | } catch (IllegalArgumentException | NoSuchFieldException e) { 1214 | throw e; 1215 | } 1216 | } 1217 | 1218 | /** 1219 | * Returns the value of a {@code long} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. 1220 | */ 1221 | public static long getLongField(Object obj, String fieldName) throws IllegalAccessException, NoSuchFieldException { 1222 | try { 1223 | return findField(obj.getClass(), fieldName).getLong(obj); 1224 | } catch (IllegalAccessException e) { 1225 | // should not happen 1226 | log(e); 1227 | throw new IllegalAccessException(e.getMessage()); 1228 | } catch (IllegalArgumentException | NoSuchFieldException e) { 1229 | throw e; 1230 | } 1231 | } 1232 | 1233 | /** 1234 | * Returns the value of a {@code short} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. 1235 | */ 1236 | public static short getShortField(Object obj, String fieldName) throws NoSuchFieldException, IllegalAccessException { 1237 | try { 1238 | return findField(obj.getClass(), fieldName).getShort(obj); 1239 | } catch (IllegalAccessException e) { 1240 | // should not happen 1241 | log(e); 1242 | throw new IllegalAccessException(e.getMessage()); 1243 | } catch (IllegalArgumentException | NoSuchFieldException e) { 1244 | throw e; 1245 | } 1246 | } 1247 | 1248 | //################################################################################################# 1249 | 1250 | /** 1251 | * Sets the value of a static object field in the given class. See also {@link #findField}. 1252 | */ 1253 | public static void setStaticObjectField(Class clazz, String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException { 1254 | try { 1255 | findField(clazz, fieldName).set(null, value); 1256 | } catch (IllegalAccessException e) { 1257 | // should not happen 1258 | log(e); 1259 | throw new IllegalAccessException(e.getMessage()); 1260 | } catch (IllegalArgumentException | NoSuchFieldException e) { 1261 | throw e; 1262 | } 1263 | } 1264 | 1265 | /** 1266 | * Sets the value of a static {@code boolean} field in the given class. See also {@link #findField}. 1267 | */ 1268 | public static void setStaticBooleanField(Class clazz, String fieldName, boolean value) throws IllegalAccessException, NoSuchFieldException { 1269 | try { 1270 | findField(clazz, fieldName).setBoolean(null, value); 1271 | } catch (IllegalAccessException e) { 1272 | // should not happen 1273 | log(e); 1274 | throw new IllegalAccessException(e.getMessage()); 1275 | } catch (IllegalArgumentException | NoSuchFieldException e) { 1276 | throw e; 1277 | } 1278 | } 1279 | 1280 | /** 1281 | * Sets the value of a static {@code byte} field in the given class. See also {@link #findField}. 1282 | */ 1283 | public static void setStaticByteField(Class clazz, String fieldName, byte value) throws NoSuchFieldException, IllegalAccessException { 1284 | try { 1285 | findField(clazz, fieldName).setByte(null, value); 1286 | } catch (IllegalAccessException e) { 1287 | // should not happen 1288 | log(e); 1289 | throw new IllegalAccessException(e.getMessage()); 1290 | } catch (IllegalArgumentException | NoSuchFieldException e) { 1291 | throw e; 1292 | } 1293 | } 1294 | 1295 | /** 1296 | * Sets the value of a static {@code char} field in the given class. See also {@link #findField}. 1297 | */ 1298 | public static void setStaticCharField(Class clazz, String fieldName, char value) throws NoSuchFieldException, IllegalAccessException { 1299 | try { 1300 | findField(clazz, fieldName).setChar(null, value); 1301 | } catch (IllegalAccessException e) { 1302 | // should not happen 1303 | log(e); 1304 | throw new IllegalAccessException(e.getMessage()); 1305 | } catch (IllegalArgumentException | NoSuchFieldException e) { 1306 | throw e; 1307 | } 1308 | } 1309 | 1310 | /** 1311 | * Sets the value of a static {@code double} field in the given class. See also {@link #findField}. 1312 | */ 1313 | public static void setStaticDoubleField(Class clazz, String fieldName, double value) throws NoSuchFieldException, IllegalAccessException { 1314 | try { 1315 | findField(clazz, fieldName).setDouble(null, value); 1316 | } catch (IllegalAccessException e) { 1317 | // should not happen 1318 | log(e); 1319 | throw new IllegalAccessException(e.getMessage()); 1320 | } catch (IllegalArgumentException | NoSuchFieldException e) { 1321 | throw e; 1322 | } 1323 | } 1324 | 1325 | /** 1326 | * Sets the value of a static {@code float} field in the given class. See also {@link #findField}. 1327 | */ 1328 | public static void setStaticFloatField(Class clazz, String fieldName, float value) throws NoSuchFieldException, IllegalAccessException { 1329 | try { 1330 | findField(clazz, fieldName).setFloat(null, value); 1331 | } catch (IllegalAccessException e) { 1332 | // should not happen 1333 | log(e); 1334 | throw new IllegalAccessException(e.getMessage()); 1335 | } catch (IllegalArgumentException | NoSuchFieldException e) { 1336 | throw e; 1337 | } 1338 | } 1339 | 1340 | /** 1341 | * Sets the value of a static {@code int} field in the given class. See also {@link #findField}. 1342 | */ 1343 | public static void setStaticIntField(Class clazz, String fieldName, int value) throws NoSuchFieldException, IllegalAccessException { 1344 | try { 1345 | findField(clazz, fieldName).setInt(null, value); 1346 | } catch (IllegalAccessException e) { 1347 | // should not happen 1348 | log(e); 1349 | throw new IllegalAccessException(e.getMessage()); 1350 | } catch (IllegalArgumentException | NoSuchFieldException e) { 1351 | throw e; 1352 | } 1353 | } 1354 | 1355 | /** 1356 | * Sets the value of a static {@code long} field in the given class. See also {@link #findField}. 1357 | */ 1358 | public static void setStaticLongField(Class clazz, String fieldName, long value) throws IllegalAccessException, NoSuchFieldException { 1359 | try { 1360 | findField(clazz, fieldName).setLong(null, value); 1361 | } catch (IllegalAccessException e) { 1362 | // should not happen 1363 | log(e); 1364 | throw new IllegalAccessException(e.getMessage()); 1365 | } catch (IllegalArgumentException | NoSuchFieldException e) { 1366 | throw e; 1367 | } 1368 | } 1369 | 1370 | /** 1371 | * Sets the value of a static {@code short} field in the given class. See also {@link #findField}. 1372 | */ 1373 | public static void setStaticShortField(Class clazz, String fieldName, short value) throws NoSuchFieldException, IllegalAccessException { 1374 | try { 1375 | findField(clazz, fieldName).setShort(null, value); 1376 | } catch (IllegalAccessException e) { 1377 | // should not happen 1378 | log(e); 1379 | throw new IllegalAccessException(e.getMessage()); 1380 | } catch (IllegalArgumentException | NoSuchFieldException e) { 1381 | throw e; 1382 | } 1383 | } 1384 | 1385 | //################################################################################################# 1386 | 1387 | /** 1388 | * Returns the value of a static object field in the given class. See also {@link #findField}. 1389 | */ 1390 | public static Object getStaticObjectField(Class clazz, String fieldName) throws NoSuchFieldException, IllegalAccessException { 1391 | try { 1392 | return findField(clazz, fieldName).get(null); 1393 | } catch (IllegalAccessException e) { 1394 | // should not happen 1395 | log(e); 1396 | throw new IllegalAccessException(e.getMessage()); 1397 | } catch (IllegalArgumentException | NoSuchFieldException e) { 1398 | throw e; 1399 | } 1400 | } 1401 | 1402 | /** 1403 | * Returns the value of a static {@code boolean} field in the given class. See also {@link #findField}. 1404 | */ 1405 | public static boolean getStaticBooleanField(Class clazz, String fieldName) throws NoSuchFieldException, IllegalAccessException { 1406 | try { 1407 | return findField(clazz, fieldName).getBoolean(null); 1408 | } catch (IllegalAccessException e) { 1409 | // should not happen 1410 | log(e); 1411 | throw new IllegalAccessException(e.getMessage()); 1412 | } catch (IllegalArgumentException | NoSuchFieldException e) { 1413 | throw e; 1414 | } 1415 | } 1416 | 1417 | /** 1418 | * Sets the value of a static {@code byte} field in the given class. See also {@link #findField}. 1419 | */ 1420 | public static byte getStaticByteField(Class clazz, String fieldName) throws IllegalAccessException, NoSuchFieldException { 1421 | try { 1422 | return findField(clazz, fieldName).getByte(null); 1423 | } catch (IllegalAccessException e) { 1424 | // should not happen 1425 | log(e); 1426 | throw new IllegalAccessException(e.getMessage()); 1427 | } catch (IllegalArgumentException | NoSuchFieldException e) { 1428 | throw e; 1429 | } 1430 | } 1431 | 1432 | /** 1433 | * Sets the value of a static {@code char} field in the given class. See also {@link #findField}. 1434 | */ 1435 | public static char getStaticCharField(Class clazz, String fieldName) throws IllegalAccessException, NoSuchFieldException { 1436 | try { 1437 | return findField(clazz, fieldName).getChar(null); 1438 | } catch (IllegalAccessException e) { 1439 | // should not happen 1440 | log(e); 1441 | throw new IllegalAccessException(e.getMessage()); 1442 | } catch (IllegalArgumentException | NoSuchFieldException e) { 1443 | throw e; 1444 | } 1445 | } 1446 | 1447 | /** 1448 | * Sets the value of a static {@code double} field in the given class. See also {@link #findField}. 1449 | */ 1450 | public static double getStaticDoubleField(Class clazz, String fieldName) throws NoSuchFieldException, IllegalAccessException { 1451 | try { 1452 | return findField(clazz, fieldName).getDouble(null); 1453 | } catch (IllegalAccessException e) { 1454 | // should not happen 1455 | log(e); 1456 | throw new IllegalAccessException(e.getMessage()); 1457 | } catch (IllegalArgumentException | NoSuchFieldException e) { 1458 | throw e; 1459 | } 1460 | } 1461 | 1462 | /** 1463 | * Sets the value of a static {@code float} field in the given class. See also {@link #findField}. 1464 | */ 1465 | public static float getStaticFloatField(Class clazz, String fieldName) throws IllegalAccessException, NoSuchFieldException { 1466 | try { 1467 | return findField(clazz, fieldName).getFloat(null); 1468 | } catch (IllegalAccessException e) { 1469 | // should not happen 1470 | log(e); 1471 | throw new IllegalAccessException(e.getMessage()); 1472 | } catch (IllegalArgumentException | NoSuchFieldException e) { 1473 | throw e; 1474 | } 1475 | } 1476 | 1477 | /** 1478 | * Sets the value of a static {@code int} field in the given class. See also {@link #findField}. 1479 | */ 1480 | public static int getStaticIntField(Class clazz, String fieldName) throws IllegalAccessException, NoSuchFieldException { 1481 | try { 1482 | return findField(clazz, fieldName).getInt(null); 1483 | } catch (IllegalAccessException e) { 1484 | // should not happen 1485 | log(e); 1486 | throw new IllegalAccessException(e.getMessage()); 1487 | } catch (IllegalArgumentException | NoSuchFieldException e) { 1488 | throw e; 1489 | } 1490 | } 1491 | 1492 | /** 1493 | * Sets the value of a static {@code long} field in the given class. See also {@link #findField}. 1494 | */ 1495 | public static long getStaticLongField(Class clazz, String fieldName) throws IllegalAccessException, NoSuchFieldException { 1496 | try { 1497 | return findField(clazz, fieldName).getLong(null); 1498 | } catch (IllegalAccessException e) { 1499 | // should not happen 1500 | log(e); 1501 | throw new IllegalAccessException(e.getMessage()); 1502 | } catch (IllegalArgumentException | NoSuchFieldException e) { 1503 | throw e; 1504 | } 1505 | } 1506 | 1507 | /** 1508 | * Sets the value of a static {@code short} field in the given class. See also {@link #findField}. 1509 | */ 1510 | public static short getStaticShortField(Class clazz, String fieldName) throws IllegalAccessException, NoSuchFieldException { 1511 | try { 1512 | return findField(clazz, fieldName).getShort(null); 1513 | } catch (IllegalAccessException e) { 1514 | // should not happen 1515 | log(e); 1516 | throw new IllegalAccessException(e.getMessage()); 1517 | } catch (IllegalArgumentException | NoSuchFieldException e) { 1518 | throw e; 1519 | } 1520 | } 1521 | 1522 | //################################################################################################# 1523 | 1524 | /** 1525 | * Calls an instance or static method of the given object. 1526 | * The method is resolved using {@link #findMethodBestMatch(Class, String, Object...)}. 1527 | * 1528 | * @param obj The object instance. A class reference is not sufficient! 1529 | * @param methodName The method name. 1530 | * @param args The arguments for the method call. 1531 | * @throws NoSuchMethodException In case no suitable method was found. 1532 | * @throws InvocationTargetException() In case an exception was thrown by the invoked method. 1533 | */ 1534 | public static Object callMethod(Object obj, String methodName, Object... args) throws InvocationTargetException, NoSuchMethodException, IllegalAccessException { 1535 | try { 1536 | return findMethodBestMatch(obj.getClass(), methodName, args).invoke(obj, args); 1537 | } catch (IllegalAccessException e) { 1538 | // should not happen 1539 | log(e); 1540 | throw new IllegalAccessException(e.getMessage()); 1541 | } catch (IllegalArgumentException | InvocationTargetException | NoSuchMethodException e) { 1542 | throw e; 1543 | } 1544 | } 1545 | 1546 | /** 1547 | * Calls an instance or static method of the given object. 1548 | * See {@link #callMethod(Object, String, Object...)}. 1549 | * 1550 | *

This variant allows you to specify parameter types, which can help in case there are multiple 1551 | * methods with the same name, especially if you call it with {@code null} parameters. 1552 | */ 1553 | public static Object callMethod(Object obj, String methodName, Class[] parameterTypes, Object... args) throws InvocationTargetException, NoSuchMethodException, IllegalAccessException { 1554 | try { 1555 | return findMethodBestMatch(obj.getClass(), methodName, parameterTypes, args).invoke(obj, args); 1556 | } catch (IllegalAccessException e) { 1557 | // should not happen 1558 | log(e); 1559 | throw new IllegalAccessException(e.getMessage()); 1560 | } catch (IllegalArgumentException | InvocationTargetException | NoSuchMethodException e) { 1561 | throw e; 1562 | } 1563 | } 1564 | 1565 | /** 1566 | * Calls a static method of the given class. 1567 | * The method is resolved using {@link #findMethodBestMatch(Class, String, Object...)}. 1568 | * 1569 | * @param clazz The class reference. 1570 | * @param methodName The method name. 1571 | * @param args The arguments for the method call. 1572 | * @throws NoSuchMethodException In case no suitable method was found. 1573 | * @throws InvocationTargetException() In case an exception was thrown by the invoked method. 1574 | */ 1575 | public static Object callStaticMethod(Class clazz, String methodName, Object... args) throws InvocationTargetException, NoSuchMethodException, IllegalAccessException { 1576 | try { 1577 | return findMethodBestMatch(clazz, methodName, args).invoke(null, args); 1578 | } catch (IllegalAccessException e) { 1579 | // should not happen 1580 | log(e); 1581 | throw new IllegalAccessException(e.getMessage()); 1582 | } catch (IllegalArgumentException | InvocationTargetException | NoSuchMethodException e) { 1583 | throw e; 1584 | } 1585 | } 1586 | 1587 | /** 1588 | * Calls a static method of the given class. 1589 | * See {@link #callStaticMethod(Class, String, Object...)}. 1590 | * 1591 | *

This variant allows you to specify parameter types, which can help in case there are multiple 1592 | * methods with the same name, especially if you call it with {@code null} parameters. 1593 | */ 1594 | public static Object callStaticMethod(Class clazz, String methodName, Class[] parameterTypes, Object... args) throws InvocationTargetException, NoSuchMethodException, IllegalAccessException { 1595 | try { 1596 | return findMethodBestMatch(clazz, methodName, parameterTypes, args).invoke(null, args); 1597 | } catch (IllegalAccessException e) { 1598 | // should not happen 1599 | log(e); 1600 | throw new IllegalAccessException(e.getMessage()); 1601 | } catch (IllegalArgumentException | InvocationTargetException | NoSuchMethodException e) { 1602 | throw e; 1603 | } 1604 | } 1605 | 1606 | //################################################################################################# 1607 | 1608 | /** 1609 | * Creates a new instance of the given class. 1610 | * The constructor is resolved using {@link #findConstructorBestMatch(Class, Object...)}. 1611 | * 1612 | * @param clazz The class reference. 1613 | * @param args The arguments for the constructor call. 1614 | * @throws NoSuchMethodException In case no suitable constructor was found. 1615 | * @throws java.lang.reflect.InvocationTargetException() In case an exception was thrown by the invoked method. 1616 | * @throws InstantiationError In case the class cannot be instantiated. 1617 | */ 1618 | public static Object newInstance(Class clazz, Object... args) throws InstantiationException, InvocationTargetException, NoSuchMethodException, IllegalAccessException { 1619 | try { 1620 | return findConstructorBestMatch(clazz, args).newInstance(args); 1621 | } catch (IllegalAccessException e) { 1622 | // should not happen 1623 | log(e); 1624 | throw new IllegalAccessException(e.getMessage()); 1625 | } catch (IllegalArgumentException | InvocationTargetException | InstantiationException | NoSuchMethodException e) { 1626 | throw e; 1627 | } 1628 | } 1629 | 1630 | /** 1631 | * Creates a new instance of the given class. 1632 | * See {@link #newInstance(Class, Object...)}. 1633 | * 1634 | *

This variant allows you to specify parameter types, which can help in case there are multiple 1635 | * constructors with the same name, especially if you call it with {@code null} parameters. 1636 | */ 1637 | public static Object newInstance(Class clazz, Class[] parameterTypes, Object... args) throws InvocationTargetException, InstantiationException, NoSuchMethodException, IllegalAccessException { 1638 | try { 1639 | return findConstructorBestMatch(clazz, parameterTypes, args).newInstance(args); 1640 | } catch (IllegalAccessException e) { 1641 | // should not happen 1642 | log(e); 1643 | throw new IllegalAccessException(e.getMessage()); 1644 | } catch (IllegalArgumentException | InvocationTargetException | InstantiationException | NoSuchMethodException e) { 1645 | throw e; 1646 | } 1647 | } 1648 | 1649 | //################################################################################################# 1650 | 1651 | /** 1652 | * Attaches any value to an object instance. This simulates adding an instance field. 1653 | * The value can be retrieved again with {@link #getAdditionalInstanceField}. 1654 | * 1655 | * @param obj The object instance for which the value should be stored. 1656 | * @param key The key in the value map for this object instance. 1657 | * @param value The value to store. 1658 | * @return The previously stored value for this instance/key combination, or {@code null} if there was none. 1659 | */ 1660 | public static Object setAdditionalInstanceField(Object obj, String key, Object value) { 1661 | if (obj == null) 1662 | throw new NullPointerException("object must not be null"); 1663 | if (key == null) 1664 | throw new NullPointerException("key must not be null"); 1665 | 1666 | HashMap objectFields; 1667 | synchronized (additionalFields) { 1668 | objectFields = additionalFields.get(obj); 1669 | if (objectFields == null) { 1670 | objectFields = new HashMap<>(); 1671 | additionalFields.put(obj, objectFields); 1672 | } 1673 | } 1674 | 1675 | synchronized (objectFields) { 1676 | return objectFields.put(key, value); 1677 | } 1678 | } 1679 | 1680 | /** 1681 | * Returns a value which was stored with {@link #setAdditionalInstanceField}. 1682 | * 1683 | * @param obj The object instance for which the value has been stored. 1684 | * @param key The key in the value map for this object instance. 1685 | * @return The stored value for this instance/key combination, or {@code null} if there is none. 1686 | */ 1687 | public static Object getAdditionalInstanceField(Object obj, String key) { 1688 | if (obj == null) 1689 | throw new NullPointerException("object must not be null"); 1690 | if (key == null) 1691 | throw new NullPointerException("key must not be null"); 1692 | 1693 | HashMap objectFields; 1694 | synchronized (additionalFields) { 1695 | objectFields = additionalFields.get(obj); 1696 | if (objectFields == null) 1697 | return null; 1698 | } 1699 | 1700 | synchronized (objectFields) { 1701 | return objectFields.get(key); 1702 | } 1703 | } 1704 | 1705 | /** 1706 | * Removes and returns a value which was stored with {@link #setAdditionalInstanceField}. 1707 | * 1708 | * @param obj The object instance for which the value has been stored. 1709 | * @param key The key in the value map for this object instance. 1710 | * @return The previously stored value for this instance/key combination, or {@code null} if there was none. 1711 | */ 1712 | public static Object removeAdditionalInstanceField(Object obj, String key) { 1713 | if (obj == null) 1714 | throw new NullPointerException("object must not be null"); 1715 | if (key == null) 1716 | throw new NullPointerException("key must not be null"); 1717 | 1718 | HashMap objectFields; 1719 | synchronized (additionalFields) { 1720 | objectFields = additionalFields.get(obj); 1721 | if (objectFields == null) 1722 | return null; 1723 | } 1724 | 1725 | synchronized (objectFields) { 1726 | return objectFields.remove(key); 1727 | } 1728 | } 1729 | 1730 | /** 1731 | * Like {@link #setAdditionalInstanceField}, but the value is stored for the class of {@code obj}. 1732 | */ 1733 | public static Object setAdditionalStaticField(Object obj, String key, Object value) { 1734 | return setAdditionalInstanceField(obj.getClass(), key, value); 1735 | } 1736 | 1737 | /** 1738 | * Like {@link #getAdditionalInstanceField}, but the value is returned for the class of {@code obj}. 1739 | */ 1740 | public static Object getAdditionalStaticField(Object obj, String key) { 1741 | return getAdditionalInstanceField(obj.getClass(), key); 1742 | } 1743 | 1744 | /** 1745 | * Like {@link #removeAdditionalInstanceField}, but the value is removed and returned for the class of {@code obj}. 1746 | */ 1747 | public static Object removeAdditionalStaticField(Object obj, String key) { 1748 | return removeAdditionalInstanceField(obj.getClass(), key); 1749 | } 1750 | 1751 | /** 1752 | * Like {@link #setAdditionalInstanceField}, but the value is stored for {@code clazz}. 1753 | */ 1754 | public static Object setAdditionalStaticField(Class clazz, String key, Object value) { 1755 | return setAdditionalInstanceField(clazz, key, value); 1756 | } 1757 | 1758 | /** 1759 | * Like {@link #setAdditionalInstanceField}, but the value is returned for {@code clazz}. 1760 | */ 1761 | public static Object getAdditionalStaticField(Class clazz, String key) { 1762 | return getAdditionalInstanceField(clazz, key); 1763 | } 1764 | 1765 | /** 1766 | * Like {@link #setAdditionalInstanceField}, but the value is removed and returned for {@code clazz}. 1767 | */ 1768 | public static Object removeAdditionalStaticField(Class clazz, String key) { 1769 | return removeAdditionalInstanceField(clazz, key); 1770 | } 1771 | 1772 | //################################################################################################# 1773 | 1774 | /** 1775 | * Loads an asset from a resource object and returns the content as {@code byte} array. 1776 | * 1777 | * @param res The resources from which the asset should be loaded. 1778 | * @param path The path to the asset, as in {@link AssetManager#open}. 1779 | * @return The content of the asset. 1780 | */ 1781 | public static byte[] assetAsByteArray(Resources res, String path) throws IOException { 1782 | return inputStreamToByteArray(res.getAssets().open(path)); 1783 | } 1784 | 1785 | /*package*/ 1786 | static byte[] inputStreamToByteArray(InputStream is) throws IOException { 1787 | ByteArrayOutputStream buf = new ByteArrayOutputStream(); 1788 | byte[] temp = new byte[1024]; 1789 | int read; 1790 | 1791 | while ((read = is.read(temp)) > 0) { 1792 | buf.write(temp, 0, read); 1793 | } 1794 | is.close(); 1795 | return buf.toByteArray(); 1796 | } 1797 | 1798 | /** 1799 | * Returns the lowercase hex string representation of a file's MD5 hash sum. 1800 | */ 1801 | public static String getMD5Sum(String file) throws IOException { 1802 | try { 1803 | MessageDigest digest = MessageDigest.getInstance("MD5"); 1804 | InputStream is = new FileInputStream(file); 1805 | byte[] buffer = new byte[8192]; 1806 | int read; 1807 | while ((read = is.read(buffer)) > 0) { 1808 | digest.update(buffer, 0, read); 1809 | } 1810 | is.close(); 1811 | byte[] md5sum = digest.digest(); 1812 | BigInteger bigInt = new BigInteger(1, md5sum); 1813 | return bigInt.toString(16); 1814 | } catch (NoSuchAlgorithmException e) { 1815 | return ""; 1816 | } 1817 | } 1818 | 1819 | //################################################################################################# 1820 | 1821 | /** 1822 | * Increments the depth counter for the given method. 1823 | * 1824 | *

The intention of the method depth counter is to keep track of the call depth for recursive 1825 | * methods, e.g. to override parameters only for the outer call. The Xposed framework uses this 1826 | * to load drawable replacements only once per call, even when multiple 1827 | * {@link Resources#getDrawable} variants call each other. 1828 | * 1829 | * @param method The method name. Should be prefixed with a unique, module-specific string. 1830 | * @return The updated depth. 1831 | */ 1832 | public static int incrementMethodDepth(String method) { 1833 | return getMethodDepthCounter(method).get().incrementAndGet(); 1834 | } 1835 | 1836 | /** 1837 | * Decrements the depth counter for the given method. 1838 | * See {@link #incrementMethodDepth} for details. 1839 | * 1840 | * @param method The method name. Should be prefixed with a unique, module-specific string. 1841 | * @return The updated depth. 1842 | */ 1843 | public static int decrementMethodDepth(String method) { 1844 | return getMethodDepthCounter(method).get().decrementAndGet(); 1845 | } 1846 | 1847 | /** 1848 | * Returns the current depth counter for the given method. 1849 | * See {@link #incrementMethodDepth} for details. 1850 | * 1851 | * @param method The method name. Should be prefixed with a unique, module-specific string. 1852 | * @return The updated depth. 1853 | */ 1854 | public static int getMethodDepth(String method) { 1855 | return getMethodDepthCounter(method).get().get(); 1856 | } 1857 | 1858 | private static ThreadLocal getMethodDepthCounter(String method) { 1859 | synchronized (sMethodDepth) { 1860 | ThreadLocal counter = sMethodDepth.get(method); 1861 | if (counter == null) { 1862 | counter = new ThreadLocal() { 1863 | @Override 1864 | protected AtomicInteger initialValue() { 1865 | return new AtomicInteger(); 1866 | } 1867 | }; 1868 | sMethodDepth.put(method, counter); 1869 | } 1870 | return counter; 1871 | } 1872 | } 1873 | } 1874 | -------------------------------------------------------------------------------- /maple/src/main/java/me/fycz/maple/MethodHook.java: -------------------------------------------------------------------------------- 1 | package me.fycz.maple; 2 | 3 | 4 | /** 5 | * @author fengyue 6 | * @date 2022/3/28 15:08 7 | */ 8 | public abstract class MethodHook { 9 | /** 10 | * Called before the invocation of the method. 11 | * 12 | *

You can use {@link MapleBridge.MethodHookParam#setResult} and {@link MapleBridge.MethodHookParam#setThrowable} 13 | * to prevent the original method from being called. 14 | * 15 | *

Note that implementations shouldn't call {@code super(param)}, it's not necessary. 16 | * 17 | * @param param Information about the method call. 18 | * @throws Throwable Everything the callback throws is caught and logged. 19 | */ 20 | protected void beforeHookedMethod(MapleBridge.MethodHookParam param) throws Throwable { 21 | } 22 | 23 | /** 24 | * Called after the invocation of the method. 25 | * 26 | *

You can use {@link MapleBridge.MethodHookParam#setResult} and {@link MapleBridge.MethodHookParam#setThrowable} 27 | * to modify the return value of the original method. 28 | * 29 | *

Note that implementations shouldn't call {@code super(param)}, it's not necessary. 30 | * 31 | * @param param Information about the method call. 32 | * @throws Throwable Everything the callback throws is caught and logged. 33 | */ 34 | protected void afterHookedMethod(MapleBridge.MethodHookParam param) throws Throwable { 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /maple/src/main/java/me/fycz/maple/MethodReplacement.java: -------------------------------------------------------------------------------- 1 | package me.fycz.maple; 2 | 3 | 4 | /** 5 | * A special case of {@link MethodHook} which completely replaces the original method. 6 | */ 7 | public abstract class MethodReplacement extends MethodHook { 8 | 9 | /** 10 | * @hide 11 | */ 12 | @Override 13 | protected final void beforeHookedMethod(MapleBridge.MethodHookParam param) throws Throwable { 14 | try { 15 | Object result = replaceHookedMethod(param); 16 | param.setResult(result); 17 | } catch (Throwable t) { 18 | MapleUtils.log(t); 19 | param.setThrowable(t); 20 | } 21 | } 22 | 23 | /** 24 | * @hide 25 | */ 26 | @Override 27 | @SuppressWarnings("EmptyMethod") 28 | protected final void afterHookedMethod(MapleBridge.MethodHookParam param) throws Throwable { 29 | } 30 | 31 | /** 32 | * Shortcut for replacing a method completely. Whatever is returned/thrown here is taken 33 | * instead of the result of the original method (which will not be called). 34 | * 35 | *

Note that implementations shouldn't call {@code super(param)}, it's not necessary. 36 | * 37 | * @param param Information about the method call. 38 | * @throws Throwable Anything that is thrown by the callback will be passed on to the original caller. 39 | */ 40 | @SuppressWarnings("UnusedParameters") 41 | protected abstract Object replaceHookedMethod(MapleBridge.MethodHookParam param) throws Throwable; 42 | 43 | /** 44 | * Predefined callback that skips the method without replacements. 45 | */ 46 | public static final MethodReplacement DO_NOTHING = new MethodReplacement() { 47 | @Override 48 | protected Object replaceHookedMethod(MapleBridge.MethodHookParam param) throws Throwable { 49 | return null; 50 | } 51 | }; 52 | 53 | /** 54 | * Creates a callback which always returns a specific value. 55 | * 56 | * @param result The value that should be returned to callers of the hooked method. 57 | */ 58 | public static MethodReplacement returnConstant(final Object result) { 59 | return new MethodReplacement() { 60 | @Override 61 | protected Object replaceHookedMethod(MapleBridge.MethodHookParam param) throws Throwable { 62 | return result; 63 | } 64 | }; 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /maple/src/main/java/org/apache/commons/lang3/reflect/MemberUtilsX.java: -------------------------------------------------------------------------------- 1 | package org.apache.commons.lang3.reflect; 2 | 3 | import java.lang.reflect.Constructor; 4 | import java.lang.reflect.Method; 5 | 6 | /** 7 | * @author fengyue 8 | * @date 2022/3/28 16:16 9 | */ 10 | public class MemberUtilsX { 11 | public static int compareConstructorFit(final Constructor left, final Constructor right, final Class[] actual) { 12 | return MemberUtils.compareConstructorFit(left, right, actual); 13 | } 14 | 15 | public static int compareMethodFit(final Method left, final Method right, final Class[] actual) { 16 | return MemberUtils.compareMethodFit(left, right, actual); 17 | } 18 | } -------------------------------------------------------------------------------- /maple/src/main/jni/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.18.1) 2 | project("Maple") 3 | 4 | set(CMAKE_CXX_STANDARD 20) 5 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 6 | 7 | add_library(maple SHARED maple.cpp elf_util.cpp) 8 | find_package(dobby REQUIRED CONFIG) 9 | find_package(lsplant REQUIRED CONFIG) 10 | target_link_libraries(maple log dobby::dobby lsplant::lsplant) 11 | -------------------------------------------------------------------------------- /maple/src/main/jni/elf_util.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "logging.h" 9 | #include "elf_util.h" 10 | 11 | using namespace SandHook; 12 | 13 | template 14 | inline constexpr auto offsetOf(ElfW(Ehdr) *head, ElfW(Off) off) { 15 | return reinterpret_cast, T, T *>>( 16 | reinterpret_cast(head) + off); 17 | } 18 | 19 | ElfImg::ElfImg(std::string_view base_name) : elf(base_name) { 20 | if (!findModuleBase()) { 21 | base = nullptr; 22 | return; 23 | } 24 | 25 | //load elf 26 | int fd = open(elf.data(), O_RDONLY); 27 | if (fd < 0) { 28 | LOGE("failed to open %s", elf.data()); 29 | return; 30 | } 31 | 32 | size = lseek(fd, 0, SEEK_END); 33 | if (size <= 0) { 34 | LOGE("lseek() failed for %s", elf.data()); 35 | } 36 | 37 | header = reinterpret_cast(mmap(nullptr, size, PROT_READ, MAP_SHARED, fd, 0)); 38 | 39 | close(fd); 40 | 41 | section_header = offsetOf(header, header->e_shoff); 42 | 43 | auto shoff = reinterpret_cast(section_header); 44 | char *section_str = offsetOf(header, section_header[header->e_shstrndx].sh_offset); 45 | 46 | for (int i = 0; i < header->e_shnum; i++, shoff += header->e_shentsize) { 47 | auto *section_h = (ElfW(Shdr) *) shoff; 48 | char *sname = section_h->sh_name + section_str; 49 | auto entsize = section_h->sh_entsize; 50 | switch (section_h->sh_type) { 51 | case SHT_DYNSYM: { 52 | if (bias == -4396) { 53 | dynsym = section_h; 54 | dynsym_offset = section_h->sh_offset; 55 | dynsym_start = offsetOf(header, dynsym_offset); 56 | } 57 | break; 58 | } 59 | case SHT_SYMTAB: { 60 | if (strcmp(sname, ".symtab") == 0) { 61 | symtab = section_h; 62 | symtab_offset = section_h->sh_offset; 63 | symtab_size = section_h->sh_size; 64 | symtab_count = symtab_size / entsize; 65 | symtab_start = offsetOf(header, symtab_offset); 66 | } 67 | break; 68 | } 69 | case SHT_STRTAB: { 70 | if (bias == -4396) { 71 | strtab = section_h; 72 | symstr_offset = section_h->sh_offset; 73 | strtab_start = offsetOf(header, symstr_offset); 74 | } 75 | if (strcmp(sname, ".strtab") == 0) { 76 | symstr_offset_for_symtab = section_h->sh_offset; 77 | } 78 | break; 79 | } 80 | case SHT_PROGBITS: { 81 | if (strtab == nullptr || dynsym == nullptr) break; 82 | if (bias == -4396) { 83 | bias = (off_t) section_h->sh_addr - (off_t) section_h->sh_offset; 84 | } 85 | break; 86 | } 87 | case SHT_HASH: { 88 | auto *d_un = offsetOf(header, section_h->sh_offset); 89 | nbucket_ = d_un[0]; 90 | bucket_ = d_un + 2; 91 | chain_ = bucket_ + nbucket_; 92 | break; 93 | } 94 | case SHT_GNU_HASH: { 95 | auto *d_buf = reinterpret_cast(((size_t) header) + 96 | section_h->sh_offset); 97 | gnu_nbucket_ = d_buf[0]; 98 | gnu_symndx_ = d_buf[1]; 99 | gnu_bloom_size_ = d_buf[2]; 100 | gnu_shift2_ = d_buf[3]; 101 | gnu_bloom_filter_ = reinterpret_cast(d_buf + 4); 102 | gnu_bucket_ = reinterpret_cast(gnu_bloom_filter_ + 103 | gnu_bloom_size_); 104 | gnu_chain_ = gnu_bucket_ + gnu_nbucket_ - gnu_symndx_; 105 | break; 106 | } 107 | } 108 | } 109 | } 110 | 111 | ElfW(Addr) ElfImg::ElfLookup(std::string_view name, uint32_t hash) const { 112 | if (nbucket_ == 0) return 0; 113 | 114 | char *strings = (char *) strtab_start; 115 | 116 | for (auto n = bucket_[hash % nbucket_]; n != 0; n = chain_[n]) { 117 | auto *sym = dynsym_start + n; 118 | if (name == strings + sym->st_name) { 119 | return sym->st_value; 120 | } 121 | } 122 | return 0; 123 | } 124 | 125 | ElfW(Addr) ElfImg::GnuLookup(std::string_view name, uint32_t hash) const { 126 | static constexpr auto bloom_mask_bits = sizeof(ElfW(Addr)) * 8; 127 | 128 | if (gnu_nbucket_ == 0 || gnu_bloom_size_ == 0) return 0; 129 | 130 | auto bloom_word = gnu_bloom_filter_[(hash / bloom_mask_bits) % gnu_bloom_size_]; 131 | uintptr_t mask = 0 132 | | (uintptr_t) 1 << (hash % bloom_mask_bits) 133 | | (uintptr_t) 1 << ((hash >> gnu_shift2_) % bloom_mask_bits); 134 | if ((mask & bloom_word) == mask) { 135 | auto sym_index = gnu_bucket_[hash % gnu_nbucket_]; 136 | if (sym_index >= gnu_symndx_) { 137 | char *strings = (char *) strtab_start; 138 | do { 139 | auto *sym = dynsym_start + sym_index; 140 | if (((gnu_chain_[sym_index] ^ hash) >> 1) == 0 141 | && name == strings + sym->st_name) { 142 | return sym->st_value; 143 | } 144 | } while ((gnu_chain_[sym_index++] & 1) == 0); 145 | } 146 | } 147 | return 0; 148 | } 149 | 150 | ElfW(Addr) ElfImg::LinearLookup(std::string_view name) const { 151 | if (symtabs_.empty()) { 152 | symtabs_.reserve(symtab_count); 153 | if (symtab_start != nullptr && symstr_offset_for_symtab != 0) { 154 | for (ElfW(Off) i = 0; i < symtab_count; i++) { 155 | unsigned int st_type = ELF_ST_TYPE(symtab_start[i].st_info); 156 | const char *st_name = offsetOf(header, symstr_offset_for_symtab + 157 | symtab_start[i].st_name); 158 | if ((st_type == STT_FUNC || st_type == STT_OBJECT) && symtab_start[i].st_size) { 159 | symtabs_.emplace(st_name, &symtab_start[i]); 160 | } 161 | } 162 | } 163 | } 164 | if (auto i = symtabs_.find(name); i != symtabs_.end()) { 165 | return i->second->st_value; 166 | } else { 167 | return 0; 168 | } 169 | } 170 | 171 | 172 | ElfImg::~ElfImg() { 173 | //open elf file local 174 | if (buffer) { 175 | free(buffer); 176 | buffer = nullptr; 177 | } 178 | //use mmap 179 | if (header) { 180 | munmap(header, size); 181 | } 182 | } 183 | 184 | ElfW(Addr) 185 | ElfImg::getSymbOffset(std::string_view name, uint32_t gnu_hash, uint32_t elf_hash) const { 186 | if (auto offset = GnuLookup(name, gnu_hash); offset > 0) { 187 | LOGD("found %s %p in %s in dynsym by gnuhash", name.data(), 188 | reinterpret_cast(offset), elf.data()); 189 | return offset; 190 | } else if (offset = ElfLookup(name, elf_hash); offset > 0) { 191 | LOGD("found %s %p in %s in dynsym by elfhash", name.data(), 192 | reinterpret_cast(offset), elf.data()); 193 | return offset; 194 | } else if (offset = LinearLookup(name); offset > 0) { 195 | LOGD("found %s %p in %s in symtab by linear lookup", name.data(), 196 | reinterpret_cast(offset), elf.data()); 197 | return offset; 198 | } else { 199 | return 0; 200 | } 201 | 202 | } 203 | 204 | constexpr inline bool contains(std::string_view a, std::string_view b) { 205 | return a.find(b) != std::string_view::npos; 206 | } 207 | 208 | bool ElfImg::findModuleBase() { 209 | off_t load_addr; 210 | bool found = false; 211 | FILE *maps = fopen("/proc/self/maps", "r"); 212 | 213 | char *buff = nullptr; 214 | size_t len = 0; 215 | ssize_t nread; 216 | 217 | while ((nread = getline(&buff, &len, maps)) != -1) { 218 | std::string_view line{buff, static_cast(nread)}; 219 | 220 | if ((contains(line, "r-xp") || contains(line, "r--p")) && contains(line, elf)) { 221 | LOGD("found: %*s", static_cast(line.size()), line.data()); 222 | if (auto begin = line.find_last_of(' '); begin != std::string_view::npos && 223 | line[++begin] == '/') { 224 | found = true; 225 | elf = line.substr(begin); 226 | if (elf.back() == '\n') elf.pop_back(); 227 | LOGD("update path: %s", elf.data()); 228 | break; 229 | } 230 | } 231 | } 232 | if (!found) { 233 | if (buff) free(buff); 234 | LOGE("failed to read load address for %s", elf.data()); 235 | fclose(maps); 236 | return false; 237 | } 238 | 239 | if (char *next = buff; load_addr = strtoul(buff, &next, 16), next == buff) { 240 | LOGE("failed to read load address for %s", elf.data()); 241 | } 242 | 243 | if (buff) free(buff); 244 | 245 | fclose(maps); 246 | 247 | LOGD("get module base %s: %lx", elf.data(), load_addr); 248 | 249 | base = reinterpret_cast(load_addr); 250 | return true; 251 | } 252 | -------------------------------------------------------------------------------- /maple/src/main/jni/elf_util.h: -------------------------------------------------------------------------------- 1 | #ifndef SANDHOOK_ELF_UTIL_H 2 | #define SANDHOOK_ELF_UTIL_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #define SHT_GNU_HASH 0x6ffffff6 12 | 13 | namespace SandHook { 14 | class ElfImg { 15 | public: 16 | 17 | ElfImg(std::string_view elf); 18 | 19 | constexpr ElfW(Addr) getSymbOffset(std::string_view name) const { 20 | return getSymbOffset(name, GnuHash(name), ElfHash(name)); 21 | } 22 | 23 | constexpr ElfW(Addr) getSymbAddress(std::string_view name) const { 24 | ElfW(Addr) offset = getSymbOffset(name); 25 | if (offset > 0 && base != nullptr) { 26 | return static_cast((uintptr_t) base + offset - bias); 27 | } else { 28 | return 0; 29 | } 30 | } 31 | 32 | template 33 | requires(std::is_pointer_v) 34 | constexpr T getSymbAddress(std::string_view name) const { 35 | return reinterpret_cast(getSymbAddress(name)); 36 | } 37 | 38 | bool isValid() const { 39 | return base != nullptr; 40 | } 41 | 42 | const std::string name() const { 43 | return elf; 44 | } 45 | 46 | ~ElfImg(); 47 | 48 | private: 49 | ElfW(Addr) getSymbOffset(std::string_view name, uint32_t gnu_hash, uint32_t elf_hash) const; 50 | 51 | ElfW(Addr) ElfLookup(std::string_view name, uint32_t hash) const; 52 | 53 | ElfW(Addr) GnuLookup(std::string_view name, uint32_t hash) const; 54 | 55 | ElfW(Addr) LinearLookup(std::string_view name) const; 56 | 57 | constexpr static uint32_t ElfHash(std::string_view name); 58 | 59 | constexpr static uint32_t GnuHash(std::string_view name); 60 | 61 | bool findModuleBase(); 62 | 63 | std::string elf; 64 | void *base = nullptr; 65 | char *buffer = nullptr; 66 | off_t size = 0; 67 | off_t bias = -4396; 68 | ElfW(Ehdr) *header = nullptr; 69 | ElfW(Shdr) *section_header = nullptr; 70 | ElfW(Shdr) *symtab = nullptr; 71 | ElfW(Shdr) *strtab = nullptr; 72 | ElfW(Shdr) *dynsym = nullptr; 73 | ElfW(Sym) *symtab_start = nullptr; 74 | ElfW(Sym) *dynsym_start = nullptr; 75 | ElfW(Sym) *strtab_start = nullptr; 76 | ElfW(Off) symtab_count = 0; 77 | ElfW(Off) symstr_offset = 0; 78 | ElfW(Off) symstr_offset_for_symtab = 0; 79 | ElfW(Off) symtab_offset = 0; 80 | ElfW(Off) dynsym_offset = 0; 81 | ElfW(Off) symtab_size = 0; 82 | 83 | uint32_t nbucket_{}; 84 | uint32_t *bucket_ = nullptr; 85 | uint32_t *chain_ = nullptr; 86 | 87 | uint32_t gnu_nbucket_{}; 88 | uint32_t gnu_symndx_{}; 89 | uint32_t gnu_bloom_size_; 90 | uint32_t gnu_shift2_; 91 | uintptr_t *gnu_bloom_filter_; 92 | uint32_t *gnu_bucket_; 93 | uint32_t *gnu_chain_; 94 | 95 | mutable std::unordered_map symtabs_; 96 | }; 97 | 98 | constexpr uint32_t ElfImg::ElfHash(std::string_view name) { 99 | uint32_t h = 0, g = 0; 100 | for (unsigned char p: name) { 101 | h = (h << 4) + p; 102 | g = h & 0xf0000000; 103 | h ^= g; 104 | h ^= g >> 24; 105 | } 106 | return h; 107 | } 108 | 109 | constexpr uint32_t ElfImg::GnuHash(std::string_view name) { 110 | uint32_t h = 5381; 111 | for (unsigned char p: name) { 112 | h += (h << 5) + p; 113 | } 114 | return h; 115 | } 116 | } 117 | 118 | #endif //SANDHOOK_ELF_UTIL_H 119 | -------------------------------------------------------------------------------- /maple/src/main/jni/logging.h: -------------------------------------------------------------------------------- 1 | #ifndef _LOGGING_H 2 | #define _LOGGING_H 3 | 4 | #include 5 | 6 | #ifndef LOG_TAG 7 | #define LOG_TAG "Maple" 8 | #endif 9 | 10 | #ifdef LOG_DISABLED 11 | #define LOGD(...) 12 | #define LOGV(...) 13 | #define LOGI(...) 14 | #define LOGW(...) 15 | #define LOGE(...) 16 | #else 17 | #ifndef NDEBUG 18 | #define LOGD(fmt, ...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, "%s:%d#%s" ": " fmt, __FILE_NAME__, __LINE__, __PRETTY_FUNCTION__ __VA_OPT__(,) __VA_ARGS__) 19 | #define LOGV(fmt, ...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, "%s:%d#%s" ": " fmt, __FILE_NAME__, __LINE__, __PRETTY_FUNCTION__ __VA_OPT__(,) __VA_ARGS__) 20 | #else 21 | #define LOGD(...) 22 | #define LOGV(...) 23 | #endif 24 | #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) 25 | #define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) 26 | #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) 27 | #define LOGF(...) __android_log_print(ANDROID_LOG_FATAL, LOG_TAG, __VA_ARGS__) 28 | #define PLOGE(fmt, args...) LOGE(fmt " failed with %d: %s", ##args, errno, strerror(errno)) 29 | #endif 30 | 31 | #endif // _LOGGING_H 32 | -------------------------------------------------------------------------------- /maple/src/main/jni/maple.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // Created by fengyue on 2022/3/28. 3 | // 4 | 5 | #include 6 | #include 7 | #include 8 | #include "elf_util.h" 9 | #include "logging.h" 10 | 11 | #define _uintval(p) reinterpret_cast(p) 12 | #define _ptr(p) reinterpret_cast(p) 13 | #define _align_up(x, n) (((x) + ((n) - 1)) & ~((n) - 1)) 14 | #define _align_down(x, n) ((x) & -(n)) 15 | #define _page_size 4096 16 | #define _page_align(n) _align_up(static_cast(n), _page_size) 17 | #define _ptr_align(x) _ptr(_align_down(reinterpret_cast(x), _page_size)) 18 | #define _make_rwx(p, n) ::mprotect(_ptr_align(p), \ 19 | _page_align(_uintval(p) + n) != _page_align(_uintval(p)) ? _page_align(n) + _page_size : _page_align(n), \ 20 | PROT_READ | PROT_WRITE | PROT_EXEC) 21 | 22 | bool init_result; 23 | 24 | void *InlineHooker(void *target, void *hooker) { 25 | _make_rwx(target, _page_size); 26 | void *origin_call; 27 | if (DobbyHook(target, hooker, &origin_call) == RS_SUCCESS) { 28 | return origin_call; 29 | } else { 30 | return nullptr; 31 | } 32 | } 33 | 34 | bool InlineUnhooker(void *func) { 35 | return DobbyDestroy(func) == RT_SUCCESS; 36 | } 37 | 38 | extern "C" 39 | JNIEXPORT jboolean JNICALL 40 | Java_me_fycz_maple_MapleBridge_hasInitHook(JNIEnv *, jclass) { 41 | return init_result; 42 | } 43 | 44 | extern "C" 45 | JNIEXPORT jobject JNICALL 46 | Java_me_fycz_maple_MapleBridge_doHook(JNIEnv *env, jobject thiz, jobject original, 47 | jobject callback) { 48 | return lsplant::Hook(env, original, thiz, callback); 49 | } 50 | 51 | extern "C" 52 | JNIEXPORT jboolean JNICALL 53 | Java_me_fycz_maple_MapleBridge_doUnhook(JNIEnv *env, jclass, jobject target) { 54 | return lsplant::UnHook(env, target); 55 | } 56 | 57 | extern "C" 58 | JNIEXPORT jboolean JNICALL 59 | Java_me_fycz_maple_MapleBridge_isHooked(JNIEnv *env, jclass, jobject method) { 60 | return lsplant::IsHooked(env, method); 61 | } 62 | 63 | extern "C" 64 | JNIEXPORT jboolean JNICALL 65 | Java_me_fycz_maple_MapleBridge_makeClassInheritable(JNIEnv *env, jclass, jclass clazz) { 66 | return lsplant::MakeClassInheritable(env, clazz); 67 | } 68 | 69 | JNIEXPORT jint JNICALL 70 | JNI_OnLoad(JavaVM *vm, void *reserved) { 71 | JNIEnv *env; 72 | if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) { 73 | return JNI_ERR; 74 | } 75 | SandHook::ElfImg art("libart.so"); 76 | lsplant::InitInfo initInfo{ 77 | .inline_hooker = InlineHooker, 78 | .inline_unhooker = InlineUnhooker, 79 | .art_symbol_resolver = [&art](std::string_view symbol) -> void * { 80 | auto *out = reinterpret_cast(art.getSymbAddress(symbol)); 81 | return out; 82 | } 83 | }; 84 | init_result = lsplant::Init(env, initInfo); 85 | return JNI_VERSION_1_6; 86 | } 87 | -------------------------------------------------------------------------------- /maple/src/test/java/me/fycz/maple/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package me.fycz.maple; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | } 14 | } 15 | rootProject.name = "Maple" 16 | include ':demo' 17 | include ':maple' 18 | --------------------------------------------------------------------------------