You've already forked revanced-patcher
mirror of
https://github.com/revanced/revanced-patcher
synced 2025-09-06 16:38:50 +02:00
Compare commits
20 Commits
v15.0.1
...
v1.0.0-dev
Author | SHA1 | Date | |
---|---|---|---|
![]() |
6cdb6887d4 | ||
![]() |
ab6453ca8a | ||
![]() |
e8182c17ad | ||
![]() |
49beec9fc6 | ||
![]() |
3ab42a932c | ||
![]() |
4d98cbc9e8 | ||
![]() |
87bbde5e06 | ||
![]() |
8db8893ab1 | ||
![]() |
00c6ab7faf | ||
![]() |
460d62a24c | ||
![]() |
89e4b9f762 | ||
![]() |
a8fd7c00c3 | ||
![]() |
1769132a9e | ||
![]() |
6c0f0823c9 | ||
![]() |
23e897a7a9 | ||
![]() |
7e67daf878 | ||
![]() |
593c83f29f | ||
![]() |
72e123dd01 | ||
![]() |
599a401ed9 | ||
![]() |
3f8500b059 |
39
.github/workflows/release.yml
vendored
Normal file
39
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: Release
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup JDK
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
java-version: '8'
|
||||
distribution: 'adopt'
|
||||
cache: gradle
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
- name: Make gradlew executable
|
||||
run: chmod +x gradlew
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew build
|
||||
- name: Setup semantic-release
|
||||
run: npm install -g semantic-release @semantic-release/git @semantic-release/changelog gradle-semantic-release-plugin -D
|
||||
- name: Release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: npx semantic-release
|
15
.idea/git_toolbox_prj.xml
generated
Normal file
15
.idea/git_toolbox_prj.xml
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GitToolBoxProjectSettings">
|
||||
<option name="commitMessageIssueKeyValidationOverride">
|
||||
<BoolValueOverride>
|
||||
<option name="enabled" value="true" />
|
||||
</BoolValueOverride>
|
||||
</option>
|
||||
<option name="commitMessageValidationEnabledOverride">
|
||||
<BoolValueOverride>
|
||||
<option name="enabled" value="true" />
|
||||
</BoolValueOverride>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@@ -4,7 +4,7 @@
|
||||
<component name="FrameworkDetectionExcludesConfiguration">
|
||||
<file type="web" url="file://$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="azul-17" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
6
.idea/vcs.xml
generated
6
.idea/vcs.xml
generated
@@ -1,5 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CommitMessageInspectionProfile">
|
||||
<profile version="1.0">
|
||||
<inspection_tool class="CommitFormat" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="CommitNamingConvention" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
</profile>
|
||||
</component>
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
|
25
.releaserc
Normal file
25
.releaserc
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"branches": [
|
||||
"main",
|
||||
{
|
||||
"name": "dev",
|
||||
"prerelease": true
|
||||
}
|
||||
],
|
||||
"plugins": [
|
||||
"@semantic-release/commit-analyzer",
|
||||
"@semantic-release/release-notes-generator",
|
||||
"@semantic-release/changelog",
|
||||
"gradle-semantic-release-plugin",
|
||||
[
|
||||
"@semantic-release/git",
|
||||
{
|
||||
"assets": [
|
||||
"CHANGELOG.md",
|
||||
"gradle.properties"
|
||||
]
|
||||
}
|
||||
],
|
||||
"@semantic-release/github"
|
||||
]
|
||||
}
|
42
CHANGELOG.md
Normal file
42
CHANGELOG.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# [1.0.0-dev.2](https://github.com/ReVancedTeam/revanced-patcher/compare/v1.0.0-dev.1...v1.0.0-dev.2) (2022-03-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* set marklimit to Integer.MAX_VALUE ([ab6453c](https://github.com/ReVancedTeam/revanced-patcher/commit/ab6453ca8a02af70da4468c1a63c68dde4d392ef))
|
||||
|
||||
# 1.0.0-dev.1 (2022-03-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* avoid ignoring test resources (fixes [#1](https://github.com/ReVancedTeam/revanced-patcher/issues/1)) ([d5a3c76](https://github.com/ReVancedTeam/revanced-patcher/commit/d5a3c76389ba902c22ddc8b7ba1a110b7ff852df))
|
||||
* current must be calculated after increment ([5f12bab](https://github.com/ReVancedTeam/revanced-patcher/commit/5f12bab5df97fbe6e2e62c1bf2814a2e682ab4f3))
|
||||
* **gradle:** publish source and javadocs ([87bbde5](https://github.com/ReVancedTeam/revanced-patcher/commit/87bbde5e06d038d8f6ddaac391e1db397f5a5590))
|
||||
* **Io:** fix finding classes by name ([460d62a](https://github.com/ReVancedTeam/revanced-patcher/commit/460d62a24c4cad05691c4b269c2faeda47fee3b7))
|
||||
* **Io:** JAR loading and saving ([#8](https://github.com/ReVancedTeam/revanced-patcher/issues/8)) ([4d98cbc](https://github.com/ReVancedTeam/revanced-patcher/commit/4d98cbc9e8fe1e39b3d9d4185b3c5b4882093af6))
|
||||
* nullable signature members ([#10](https://github.com/ReVancedTeam/revanced-patcher/issues/10)) ([8db8893](https://github.com/ReVancedTeam/revanced-patcher/commit/8db8893ab1bda55f11cc75db55c7c1a38f1d1b16))
|
||||
* Patch should have access to the Cache ([6c0f082](https://github.com/ReVancedTeam/revanced-patcher/commit/6c0f0823c91dc643dd80205b1e840e59827bee06))
|
||||
* remove broken code ([0e72a6e](https://github.com/ReVancedTeam/revanced-patcher/commit/0e72a6e85ff9a6035510680fc5e33ab0cd14144f))
|
||||
* set index for insertAt to 0 by default ([1769132](https://github.com/ReVancedTeam/revanced-patcher/commit/1769132a9e29cf3a0c5ae0917209c83c138c0216))
|
||||
* workflow on dev branch ([7e67daf](https://github.com/ReVancedTeam/revanced-patcher/commit/7e67daf8789c534bed0091a3975776eb95039acc))
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* convert Patch to abstract class ([23e897a](https://github.com/ReVancedTeam/revanced-patcher/commit/23e897a7a9125f4ac4266263e7dd94fe63a0bfa1))
|
||||
* Optimize Signature class ([#11](https://github.com/ReVancedTeam/revanced-patcher/issues/11)) ([49beec9](https://github.com/ReVancedTeam/revanced-patcher/commit/49beec9fc6eee6ccf52a6185761a200a6ed2b16e))
|
||||
* Rename `net.revanced` to `app.revanced` ([3ab42a9](https://github.com/ReVancedTeam/revanced-patcher/commit/3ab42a932c8d5027d554106dfe8e1299ebc1ac7f))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add `findParentMethod` utility method ([#4](https://github.com/ReVancedTeam/revanced-patcher/issues/4)) ([00c6ab7](https://github.com/ReVancedTeam/revanced-patcher/commit/00c6ab7fafe2a59dec0052cc5b7d1d16939076b2))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* Array<Int> was changed to IntArray. This breaks existing patches.
|
||||
* Package name was changed from "net.revanced" to "app.revanced"
|
||||
* Method signature of execute() was changed to include the cache, this will break existing implementations of the Patch class.
|
||||
* Patch class is now an abstract class. You must implement it. You can use anonymous implements, like done in the tests.
|
@@ -1,9 +1,10 @@
|
||||
plugins {
|
||||
kotlin("jvm") version "1.6.10"
|
||||
java
|
||||
`maven-publish`
|
||||
}
|
||||
|
||||
group = "net.revanced"
|
||||
group = "app.revanced"
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
@@ -27,6 +28,11 @@ tasks.test {
|
||||
}
|
||||
}
|
||||
|
||||
java {
|
||||
withSourcesJar()
|
||||
withJavadocJar()
|
||||
}
|
||||
|
||||
publishing {
|
||||
repositories {
|
||||
maven {
|
||||
|
@@ -1,2 +1,2 @@
|
||||
kotlin.code.style=official
|
||||
version=1.0.0
|
||||
kotlin.code.style = official
|
||||
version = 1.0.0-dev.2
|
||||
|
70
src/main/kotlin/app/revanced/patcher/Patcher.kt
Normal file
70
src/main/kotlin/app/revanced/patcher/Patcher.kt
Normal file
@@ -0,0 +1,70 @@
|
||||
package app.revanced.patcher
|
||||
|
||||
import app.revanced.patcher.cache.Cache
|
||||
import app.revanced.patcher.patch.Patch
|
||||
import app.revanced.patcher.resolver.MethodResolver
|
||||
import app.revanced.patcher.signature.Signature
|
||||
import app.revanced.patcher.util.Io
|
||||
import org.objectweb.asm.tree.ClassNode
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
/**
|
||||
* The Patcher class.
|
||||
* ***It is of utmost importance that the input and output streams are NEVER closed.***
|
||||
*
|
||||
* @param input the input stream to read from, must be a JAR
|
||||
* @param output the output stream to write to
|
||||
* @param signatures the signatures
|
||||
* @sample app.revanced.patcher.PatcherTest
|
||||
* @throws IOException if one of the streams are closed
|
||||
*/
|
||||
class Patcher(
|
||||
private val input: InputStream,
|
||||
private val output: OutputStream,
|
||||
signatures: Array<Signature>,
|
||||
) {
|
||||
var cache: Cache
|
||||
|
||||
private var io: Io
|
||||
private val patches = mutableListOf<Patch>()
|
||||
|
||||
init {
|
||||
val classes = mutableListOf<ClassNode>()
|
||||
io = Io(input, output, classes)
|
||||
io.readFromJar()
|
||||
cache = Cache(classes, MethodResolver(classes, signatures).resolve())
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the output to the output stream.
|
||||
* Calling this method will close the input and output streams,
|
||||
* meaning this method should NEVER be called after.
|
||||
*
|
||||
* @throws IOException if one of the streams are closed
|
||||
*/
|
||||
fun save() {
|
||||
io.saveAsJar()
|
||||
}
|
||||
|
||||
fun addPatches(vararg patches: Patch) {
|
||||
this.patches.addAll(patches)
|
||||
}
|
||||
|
||||
fun applyPatches(stopOnError: Boolean = false): Map<String, Result<Nothing?>> {
|
||||
return buildMap {
|
||||
for (patch in patches) {
|
||||
val result: Result<Nothing?> = try {
|
||||
val pr = patch.execute(cache)
|
||||
if (pr.isSuccess()) continue
|
||||
Result.failure(Exception(pr.error()?.errorMessage() ?: "Unknown error"))
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
this[patch.patchName] = result
|
||||
if (stopOnError && result.isFailure) break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,8 +1,8 @@
|
||||
package net.revanced.patcher.cache
|
||||
package app.revanced.patcher.cache
|
||||
|
||||
import org.objectweb.asm.tree.ClassNode
|
||||
|
||||
class Cache (
|
||||
class Cache(
|
||||
val classes: List<ClassNode>,
|
||||
val methods: MethodMap
|
||||
)
|
22
src/main/kotlin/app/revanced/patcher/cache/PatchData.kt
vendored
Normal file
22
src/main/kotlin/app/revanced/patcher/cache/PatchData.kt
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
package app.revanced.patcher.cache
|
||||
|
||||
import app.revanced.patcher.resolver.MethodResolver
|
||||
import app.revanced.patcher.signature.Signature
|
||||
import org.objectweb.asm.tree.ClassNode
|
||||
import org.objectweb.asm.tree.MethodNode
|
||||
|
||||
data class PatchData(
|
||||
val declaringClass: ClassNode,
|
||||
val method: MethodNode,
|
||||
val scanData: PatternScanData
|
||||
) {
|
||||
@Suppress("Unused") // TODO(Sculas): remove this when we have coverage for this method.
|
||||
fun findParentMethod(signature: Signature): PatchData? {
|
||||
return MethodResolver.resolveMethod(declaringClass, signature)
|
||||
}
|
||||
}
|
||||
|
||||
data class PatternScanData(
|
||||
val startIndex: Int,
|
||||
val endIndex: Int
|
||||
)
|
7
src/main/kotlin/app/revanced/patcher/patch/Patch.kt
Normal file
7
src/main/kotlin/app/revanced/patcher/patch/Patch.kt
Normal file
@@ -0,0 +1,7 @@
|
||||
package app.revanced.patcher.patch
|
||||
|
||||
import app.revanced.patcher.cache.Cache
|
||||
|
||||
abstract class Patch(val patchName: String) {
|
||||
abstract fun execute(cache: Cache): PatchResult
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package net.revanced.patcher.patch
|
||||
package app.revanced.patcher.patch
|
||||
|
||||
interface PatchResult {
|
||||
fun error(): PatchResultError? {
|
159
src/main/kotlin/app/revanced/patcher/resolver/MethodResolver.kt
Normal file
159
src/main/kotlin/app/revanced/patcher/resolver/MethodResolver.kt
Normal file
@@ -0,0 +1,159 @@
|
||||
package app.revanced.patcher.resolver
|
||||
|
||||
import mu.KotlinLogging
|
||||
import app.revanced.patcher.cache.MethodMap
|
||||
import app.revanced.patcher.cache.PatchData
|
||||
import app.revanced.patcher.cache.PatternScanData
|
||||
import app.revanced.patcher.signature.Signature
|
||||
import app.revanced.patcher.util.ExtraTypes
|
||||
import org.objectweb.asm.Type
|
||||
import org.objectweb.asm.tree.ClassNode
|
||||
import org.objectweb.asm.tree.InsnList
|
||||
import org.objectweb.asm.tree.MethodNode
|
||||
|
||||
private val logger = KotlinLogging.logger("MethodResolver")
|
||||
|
||||
internal class MethodResolver(private val classList: List<ClassNode>, private val signatures: Array<Signature>) {
|
||||
fun resolve(): MethodMap {
|
||||
val methodMap = MethodMap()
|
||||
|
||||
for ((classNode, methods) in classList) {
|
||||
for (method in methods) {
|
||||
for (signature in signatures) {
|
||||
if (methodMap.containsKey(signature.name)) { // method already found for this sig
|
||||
logger.debug { "Sig ${signature.name} already found, skipping." }
|
||||
continue
|
||||
}
|
||||
logger.debug { "Resolving sig ${signature.name}: ${classNode.name} / ${method.name}" }
|
||||
val (r, sr) = cmp(method, signature)
|
||||
if (!r || sr == null) {
|
||||
logger.debug { "Compare result for sig ${signature.name} has failed!" }
|
||||
continue
|
||||
}
|
||||
logger.debug { "Method for sig ${signature.name} found!" }
|
||||
methodMap[signature.name] = PatchData(
|
||||
classNode,
|
||||
method,
|
||||
PatternScanData(
|
||||
// sadly we cannot create contracts for a data class, so we must assert
|
||||
sr.startIndex!!,
|
||||
sr.endIndex!!
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (signature in signatures) {
|
||||
if (methodMap.containsKey(signature.name)) continue
|
||||
logger.error { "Could not find method for sig ${signature.name}!" }
|
||||
}
|
||||
|
||||
return methodMap
|
||||
}
|
||||
|
||||
// These functions do not require the constructor values, so they can be static.
|
||||
companion object {
|
||||
fun resolveMethod(classNode: ClassNode, signature: Signature): PatchData? {
|
||||
for (method in classNode.methods) {
|
||||
val (r, sr) = cmp(method, signature)
|
||||
if (!r || sr == null) continue
|
||||
return PatchData(
|
||||
classNode,
|
||||
method,
|
||||
PatternScanData(0, 0) // opcode list is always ignored.
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun cmp(method: MethodNode, signature: Signature): Pair<Boolean, ScanResult?> {
|
||||
signature.returns?.let { _ ->
|
||||
val methodReturns = Type.getReturnType(method.desc).convertObject()
|
||||
if (signature.returns != methodReturns) {
|
||||
logger.debug {
|
||||
"""
|
||||
Comparing sig ${signature.name}: invalid return type:
|
||||
expected ${signature.returns},
|
||||
got $methodReturns
|
||||
""".trimIndent()
|
||||
}
|
||||
return@cmp false to null
|
||||
}
|
||||
}
|
||||
|
||||
signature.accessors?.let { _ ->
|
||||
if (signature.accessors != method.access) {
|
||||
logger.debug {
|
||||
"""
|
||||
Comparing sig ${signature.name}: invalid accessors:
|
||||
expected ${signature.accessors},
|
||||
got ${method.access}
|
||||
""".trimIndent()
|
||||
}
|
||||
return@cmp false to null
|
||||
}
|
||||
}
|
||||
|
||||
signature.parameters?.let { _ ->
|
||||
val parameters = Type.getArgumentTypes(method.desc).convertObjects()
|
||||
if (!signature.parameters.contentEquals(parameters)) {
|
||||
logger.debug {
|
||||
"""
|
||||
Comparing sig ${signature.name}: invalid parameter types:
|
||||
expected ${signature.parameters.joinToString()}},
|
||||
got ${parameters.joinToString()}
|
||||
""".trimIndent()
|
||||
}
|
||||
return@cmp false to null
|
||||
}
|
||||
}
|
||||
|
||||
signature.opcodes?.let { _ ->
|
||||
val result = method.instructions.scanFor(signature.opcodes)
|
||||
if (!result.found) {
|
||||
logger.debug { "Comparing sig ${signature.name}: invalid opcode pattern" }
|
||||
return@cmp false to null
|
||||
}
|
||||
return@cmp true to result
|
||||
}
|
||||
|
||||
return true to ScanResult(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private operator fun ClassNode.component1(): ClassNode {
|
||||
return this
|
||||
}
|
||||
|
||||
private operator fun ClassNode.component2(): List<MethodNode> {
|
||||
return this.methods
|
||||
}
|
||||
|
||||
private fun InsnList.scanFor(pattern: IntArray): ScanResult {
|
||||
for (i in 0 until this.size()) {
|
||||
var occurrence = 0
|
||||
while (i + occurrence < this.size()) {
|
||||
if (this[i + occurrence].opcode != pattern[occurrence]) break
|
||||
if (++occurrence >= pattern.size) {
|
||||
val current = i + occurrence
|
||||
return ScanResult(true, current - pattern.size, current)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ScanResult(false)
|
||||
}
|
||||
|
||||
private fun Type.convertObject(): Type {
|
||||
return when (this.sort) {
|
||||
Type.OBJECT -> ExtraTypes.Any
|
||||
Type.ARRAY -> ExtraTypes.ArrayAny
|
||||
else -> this
|
||||
}
|
||||
}
|
||||
|
||||
private fun Array<Type>.convertObjects(): Array<Type> {
|
||||
return this.map { it.convertObject() }.toTypedArray()
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package net.revanced.patcher.resolver
|
||||
package app.revanced.patcher.resolver
|
||||
|
||||
internal data class ScanResult(
|
||||
val found: Boolean,
|
@@ -1,4 +1,4 @@
|
||||
package net.revanced.patcher.signature
|
||||
package app.revanced.patcher.signature
|
||||
|
||||
import org.objectweb.asm.Type
|
||||
|
||||
@@ -9,7 +9,9 @@ import org.objectweb.asm.Type
|
||||
* Do not use the actual method name, instead try to guess what the method name originally was.
|
||||
* If you are unable to guess a method name, doing something like "patch-name-1" is fine too.
|
||||
* For example: "override-codec-1".
|
||||
* This method name will be used to find the corresponding patch.
|
||||
* This method name will be mapped to the method matching the signature.
|
||||
* Even though this is technically not needed for the `findParentMethod` method,
|
||||
* it is still recommended giving the method a name, so it can be identified easily.
|
||||
* @param returns The return type/signature of the method.
|
||||
* @param accessors The accessors of the method.
|
||||
* @param parameters The parameter types of the method.
|
||||
@@ -18,8 +20,8 @@ import org.objectweb.asm.Type
|
||||
@Suppress("ArrayInDataClass")
|
||||
data class Signature(
|
||||
val name: String,
|
||||
val returns: Type,
|
||||
val accessors: Int,
|
||||
val parameters: Array<Type>,
|
||||
val opcodes: Array<Int>
|
||||
val returns: Type?,
|
||||
val accessors: Int?,
|
||||
val parameters: Array<Type>?,
|
||||
val opcodes: IntArray?
|
||||
)
|
@@ -1,4 +1,4 @@
|
||||
package net.revanced.patcher.util
|
||||
package app.revanced.patcher.util
|
||||
|
||||
import org.objectweb.asm.Type
|
||||
|
93
src/main/kotlin/app/revanced/patcher/util/Io.kt
Normal file
93
src/main/kotlin/app/revanced/patcher/util/Io.kt
Normal file
@@ -0,0 +1,93 @@
|
||||
package app.revanced.patcher.util
|
||||
|
||||
import org.objectweb.asm.ClassReader
|
||||
import org.objectweb.asm.ClassWriter
|
||||
import org.objectweb.asm.tree.ClassNode
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.util.jar.JarEntry
|
||||
import java.util.jar.JarInputStream
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
internal class Io(
|
||||
private val input: InputStream,
|
||||
private val output: OutputStream,
|
||||
private val classes: MutableList<ClassNode>
|
||||
) {
|
||||
private val bufferedInputStream = BufferedInputStream(input)
|
||||
|
||||
fun readFromJar() {
|
||||
bufferedInputStream.mark(Integer.MAX_VALUE)
|
||||
// create a BufferedInputStream in order to read the input stream again when calling saveAsJar(..)
|
||||
val jis = JarInputStream(bufferedInputStream)
|
||||
|
||||
// read all entries from the input stream
|
||||
// we use JarEntry because we only read .class files
|
||||
lateinit var jarEntry: JarEntry
|
||||
while (jis.nextJarEntry.also { if (it != null) jarEntry = it } != null) {
|
||||
// if the current entry ends with .class (indicating a java class file), add it to our list of classes to return
|
||||
if (jarEntry.name.endsWith(".class")) {
|
||||
// create a new ClassNode
|
||||
val classNode = ClassNode()
|
||||
// read the bytes with a ClassReader into the ClassNode
|
||||
ClassReader(jis.readBytes()).accept(classNode, ClassReader.EXPAND_FRAMES)
|
||||
// add it to our list
|
||||
classes.add(classNode)
|
||||
}
|
||||
|
||||
// finally, close the entry
|
||||
jis.closeEntry()
|
||||
}
|
||||
|
||||
// at last reset the buffered input stream
|
||||
bufferedInputStream.reset()
|
||||
}
|
||||
|
||||
fun saveAsJar() {
|
||||
val jis = ZipInputStream(bufferedInputStream)
|
||||
val jos = ZipOutputStream(output)
|
||||
|
||||
// first write all non .class zip entries from the original input stream to the output stream
|
||||
// we read it first to close the input stream as fast as possible
|
||||
// TODO(oSumAtrIX): There is currently no way to remove non .class files.
|
||||
lateinit var zipEntry: ZipEntry
|
||||
while (jis.nextEntry.also { if (it != null) zipEntry = it } != null) {
|
||||
// skip all class files because we added them in the loop above
|
||||
// TODO(oSumAtrIX): Check for zipEntry.isDirectory
|
||||
if (zipEntry.name.endsWith(".class")) continue
|
||||
|
||||
// create a new zipEntry and write the contents of the zipEntry to the output stream
|
||||
jos.putNextEntry(ZipEntry(zipEntry))
|
||||
jos.write(jis.readBytes())
|
||||
|
||||
// close the newly created zipEntry
|
||||
jos.closeEntry()
|
||||
}
|
||||
|
||||
// finally, close the input stream
|
||||
jis.close()
|
||||
bufferedInputStream.close()
|
||||
input.close()
|
||||
|
||||
// now write all the patched classes to the output stream
|
||||
for (patchedClass in classes) {
|
||||
// create a new entry of the patched class
|
||||
jos.putNextEntry(JarEntry(patchedClass.name + ".class"))
|
||||
|
||||
// parse the patched class to a byte array and write it to the output stream
|
||||
val cw = ClassWriter(ClassWriter.COMPUTE_MAXS or ClassWriter.COMPUTE_FRAMES)
|
||||
patchedClass.accept(cw)
|
||||
jos.write(cw.toByteArray())
|
||||
|
||||
// close the newly created jar entry
|
||||
jos.closeEntry()
|
||||
}
|
||||
|
||||
// finally, close the rest of the streams
|
||||
jos.close()
|
||||
output.close()
|
||||
}
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package net.revanced.patcher.writer
|
||||
package app.revanced.patcher.writer
|
||||
|
||||
import org.objectweb.asm.tree.AbstractInsnNode
|
||||
import org.objectweb.asm.tree.InsnList
|
||||
@@ -7,7 +7,8 @@ object ASMWriter {
|
||||
fun InsnList.setAt(index: Int, node: AbstractInsnNode) {
|
||||
this[this.get(index)] = node
|
||||
}
|
||||
fun InsnList.insertAt(index: Int, vararg nodes: AbstractInsnNode) {
|
||||
|
||||
fun InsnList.insertAt(index: Int = 0, vararg nodes: AbstractInsnNode) {
|
||||
this.insert(this.get(index), nodes.toInsnList())
|
||||
}
|
||||
|
@@ -1,53 +0,0 @@
|
||||
package net.revanced.patcher
|
||||
|
||||
import net.revanced.patcher.cache.Cache
|
||||
import net.revanced.patcher.patch.Patch
|
||||
import net.revanced.patcher.resolver.MethodResolver
|
||||
import net.revanced.patcher.signature.Signature
|
||||
import net.revanced.patcher.util.Io
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
/**
|
||||
* The patcher. (docs WIP)
|
||||
*
|
||||
* @param input the input stream to read from, must be a JAR
|
||||
* @param signatures the signatures
|
||||
* @sample net.revanced.patcher.PatcherTest
|
||||
*/
|
||||
class Patcher(
|
||||
private val input: InputStream,
|
||||
signatures: Array<Signature>,
|
||||
) {
|
||||
var cache: Cache
|
||||
private val patches: MutableList<Patch> = mutableListOf()
|
||||
|
||||
init {
|
||||
val classes = Io.readClassesFromJar(input);
|
||||
cache = Cache(classes, MethodResolver(classes, signatures).resolve())
|
||||
}
|
||||
|
||||
fun addPatches(vararg patches: Patch) {
|
||||
this.patches.addAll(patches)
|
||||
}
|
||||
|
||||
fun applyPatches(stopOnError: Boolean = false): Map<String, Result<Nothing?>> {
|
||||
return buildMap {
|
||||
for (patch in patches) {
|
||||
val result: Result<Nothing?> = try {
|
||||
val pr = patch.execute()
|
||||
if (pr.isSuccess()) continue
|
||||
Result.failure(Exception(pr.error()?.errorMessage() ?: "Unknown error"))
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
this[patch.patchName] = result
|
||||
if (stopOnError && result.isFailure) break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveTo(output: OutputStream) {
|
||||
Io.writeClassesToJar(input, output, cache.classes)
|
||||
}
|
||||
}
|
@@ -1,15 +0,0 @@
|
||||
package net.revanced.patcher.cache
|
||||
|
||||
import org.objectweb.asm.tree.ClassNode
|
||||
import org.objectweb.asm.tree.MethodNode
|
||||
|
||||
data class PatchData(
|
||||
val declaringClass: ClassNode,
|
||||
val method: MethodNode,
|
||||
val scanData: PatternScanData
|
||||
)
|
||||
|
||||
data class PatternScanData(
|
||||
val startIndex: Int,
|
||||
val endIndex: Int
|
||||
)
|
@@ -1,9 +0,0 @@
|
||||
package net.revanced.patcher.patch
|
||||
|
||||
class Patch(val patchName: String, val fn: () -> PatchResult) {
|
||||
fun execute(): PatchResult {
|
||||
return fn()
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,128 +0,0 @@
|
||||
package net.revanced.patcher.resolver
|
||||
|
||||
import mu.KotlinLogging
|
||||
import net.revanced.patcher.cache.MethodMap
|
||||
import net.revanced.patcher.cache.PatchData
|
||||
import net.revanced.patcher.cache.PatternScanData
|
||||
import net.revanced.patcher.signature.Signature
|
||||
import net.revanced.patcher.util.ExtraTypes
|
||||
import org.objectweb.asm.Type
|
||||
import org.objectweb.asm.tree.ClassNode
|
||||
import org.objectweb.asm.tree.InsnList
|
||||
import org.objectweb.asm.tree.MethodNode
|
||||
|
||||
private val logger = KotlinLogging.logger("MethodResolver")
|
||||
|
||||
internal class MethodResolver(private val classList: List<ClassNode>, private val signatures: Array<Signature>) {
|
||||
fun resolve(): MethodMap {
|
||||
val methodMap = MethodMap()
|
||||
|
||||
for ((classNode, methods) in classList) {
|
||||
for (method in methods) {
|
||||
for (signature in signatures) {
|
||||
if (methodMap.containsKey(signature.name)) { // method already found for this sig
|
||||
logger.debug { "Sig ${signature.name} already found, skipping." }
|
||||
continue
|
||||
}
|
||||
logger.debug { "Resolving sig ${signature.name}: ${classNode.name} / ${method.name}" }
|
||||
val (r, sr) = this.cmp(method, signature)
|
||||
if (!r || sr == null) {
|
||||
logger.debug { "Compare result for sig ${signature.name} has failed!" }
|
||||
continue
|
||||
}
|
||||
logger.debug { "Method for sig ${signature.name} found!" }
|
||||
methodMap[signature.name] = PatchData(
|
||||
classNode,
|
||||
method,
|
||||
PatternScanData(
|
||||
// sadly we cannot create contracts for a data class, so we must assert
|
||||
sr.startIndex!!,
|
||||
sr.endIndex!!
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (signature in signatures) {
|
||||
if (methodMap.containsKey(signature.name)) continue
|
||||
logger.error { "Could not find method for sig ${signature.name}!" }
|
||||
}
|
||||
|
||||
return methodMap
|
||||
}
|
||||
|
||||
private fun cmp(method: MethodNode, signature: Signature): Pair<Boolean, ScanResult?> {
|
||||
val returns = Type.getReturnType(method.desc).convertObject()
|
||||
if (signature.returns != returns) {
|
||||
logger.debug {
|
||||
"""
|
||||
Comparing sig ${signature.name}: invalid return type:
|
||||
expected ${signature.returns}},
|
||||
got $returns
|
||||
""".trimIndent()
|
||||
}
|
||||
return false to null
|
||||
}
|
||||
|
||||
if (signature.accessors != method.access) {
|
||||
logger.debug { "Comparing sig ${signature.name}: invalid accessors:\nexpected ${signature.accessors},\ngot ${method.access}" }
|
||||
return false to null
|
||||
}
|
||||
|
||||
val parameters = Type.getArgumentTypes(method.desc).convertObjects()
|
||||
if (!signature.parameters.contentEquals(parameters)) {
|
||||
logger.debug {
|
||||
"""
|
||||
Comparing sig ${signature.name}: invalid parameter types:
|
||||
expected ${signature.parameters.joinToString()}},
|
||||
got ${parameters.joinToString()}
|
||||
""".trimIndent()
|
||||
}
|
||||
return false to null
|
||||
}
|
||||
|
||||
val result = method.instructions.scanFor(signature.opcodes)
|
||||
if (!result.found) {
|
||||
logger.debug { "Comparing sig ${signature.name}: invalid opcode pattern" }
|
||||
return false to null
|
||||
}
|
||||
|
||||
return true to result
|
||||
}
|
||||
}
|
||||
|
||||
private operator fun ClassNode.component1(): ClassNode {
|
||||
return this
|
||||
}
|
||||
|
||||
private operator fun ClassNode.component2(): List<MethodNode> {
|
||||
return this.methods
|
||||
}
|
||||
|
||||
private fun InsnList.scanFor(pattern: Array<Int>): ScanResult {
|
||||
for (i in 0 until this.size()) {
|
||||
var occurrence = 0
|
||||
while (i + occurrence < this.size()) {
|
||||
if (this[i + occurrence].opcode != pattern[occurrence]) break
|
||||
if (++occurrence >= pattern.size) {
|
||||
val current = i + occurrence
|
||||
return ScanResult(true, current - pattern.size, current)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ScanResult(false)
|
||||
}
|
||||
|
||||
private fun Type.convertObject(): Type {
|
||||
return when (this.sort) {
|
||||
Type.OBJECT -> ExtraTypes.Any
|
||||
Type.ARRAY -> ExtraTypes.ArrayAny
|
||||
else -> this
|
||||
}
|
||||
}
|
||||
|
||||
private fun Array<Type>.convertObjects(): Array<Type> {
|
||||
return this.map { it.convertObject() }.toTypedArray()
|
||||
}
|
@@ -1,49 +0,0 @@
|
||||
package net.revanced.patcher.util
|
||||
|
||||
import org.objectweb.asm.ClassReader
|
||||
import org.objectweb.asm.ClassWriter
|
||||
import org.objectweb.asm.tree.ClassNode
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.util.jar.JarEntry
|
||||
import java.util.jar.JarInputStream
|
||||
import java.util.jar.JarOutputStream
|
||||
|
||||
object Io {
|
||||
fun readClassesFromJar(input: InputStream) = mutableListOf<ClassNode>().apply {
|
||||
val jar = JarInputStream(input)
|
||||
while (true) {
|
||||
val e = jar.nextJarEntry ?: break
|
||||
if (e.name.endsWith(".class")) {
|
||||
val classNode = ClassNode()
|
||||
ClassReader(jar.readAllBytes()).accept(classNode, ClassReader.EXPAND_FRAMES)
|
||||
this.add(classNode)
|
||||
}
|
||||
jar.closeEntry()
|
||||
}
|
||||
}
|
||||
|
||||
fun writeClassesToJar(input: InputStream, output: OutputStream, classes: List<ClassNode>) {
|
||||
val jis = JarInputStream(input)
|
||||
val jos = JarOutputStream(output)
|
||||
|
||||
// TODO: Add support for adding new/custom classes
|
||||
while (true) {
|
||||
val next = jis.nextJarEntry ?: break
|
||||
val e = JarEntry(next) // clone it, to not modify the input (if possible)
|
||||
jos.putNextEntry(e)
|
||||
|
||||
val clazz = classes.singleOrNull {
|
||||
clazz -> clazz.name == e.name
|
||||
};
|
||||
if (clazz != null) {
|
||||
val cw = ClassWriter(ClassWriter.COMPUTE_MAXS or ClassWriter.COMPUTE_FRAMES)
|
||||
clazz.accept(cw)
|
||||
jos.write(cw.toByteArray())
|
||||
} else {
|
||||
jos.write(jis.readAllBytes())
|
||||
}
|
||||
jos.closeEntry()
|
||||
}
|
||||
}
|
||||
}
|
161
src/test/kotlin/app/revanced/patcher/PatcherTest.kt
Normal file
161
src/test/kotlin/app/revanced/patcher/PatcherTest.kt
Normal file
@@ -0,0 +1,161 @@
|
||||
package app.revanced.patcher
|
||||
|
||||
import app.revanced.patcher.cache.Cache
|
||||
import app.revanced.patcher.patch.Patch
|
||||
import app.revanced.patcher.patch.PatchResult
|
||||
import app.revanced.patcher.patch.PatchResultSuccess
|
||||
import app.revanced.patcher.signature.Signature
|
||||
import app.revanced.patcher.util.ExtraTypes
|
||||
import app.revanced.patcher.util.TestUtil
|
||||
import app.revanced.patcher.writer.ASMWriter.insertAt
|
||||
import app.revanced.patcher.writer.ASMWriter.setAt
|
||||
import org.junit.jupiter.api.assertDoesNotThrow
|
||||
import org.objectweb.asm.Opcodes.*
|
||||
import org.objectweb.asm.Type
|
||||
import org.objectweb.asm.tree.FieldInsnNode
|
||||
import org.objectweb.asm.tree.LdcInsnNode
|
||||
import org.objectweb.asm.tree.MethodInsnNode
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.PrintStream
|
||||
import kotlin.test.Test
|
||||
|
||||
internal class PatcherTest {
|
||||
companion object {
|
||||
val testSignatures: Array<Signature> = arrayOf(
|
||||
// Java:
|
||||
// public static void main(String[] args) {
|
||||
// System.out.println("Hello, world!");
|
||||
// }
|
||||
// Bytecode:
|
||||
// public static main(java.lang.String[] arg0) { // Method signature: ([Ljava/lang/String;)V
|
||||
// getstatic java/lang/System.out:java.io.PrintStream
|
||||
// ldc "Hello, world!" (java.lang.String)
|
||||
// invokevirtual java/io/PrintStream.println(Ljava/lang/String;)V
|
||||
// return
|
||||
// }
|
||||
Signature(
|
||||
"mainMethod",
|
||||
Type.VOID_TYPE,
|
||||
ACC_PUBLIC or ACC_STATIC,
|
||||
arrayOf(ExtraTypes.ArrayAny),
|
||||
intArrayOf(
|
||||
LDC,
|
||||
INVOKEVIRTUAL
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPatcher() {
|
||||
val patcher = Patcher(
|
||||
PatcherTest::class.java.getResourceAsStream("/test1.jar")!!,
|
||||
ByteArrayOutputStream(),
|
||||
testSignatures
|
||||
)
|
||||
|
||||
patcher.addPatches(
|
||||
object : Patch("TestPatch") {
|
||||
override fun execute(cache: Cache): PatchResult {
|
||||
// Get the method from the resolver cache
|
||||
val mainMethod = patcher.cache.methods["mainMethod"]
|
||||
// Get the instruction list
|
||||
val instructions = mainMethod.method.instructions!!
|
||||
|
||||
// Let's modify it, so it prints "Hello, ReVanced! Editing bytecode."
|
||||
// Get the start index of our opcode pattern.
|
||||
// This will be the index of the LDC instruction.
|
||||
val startIndex = mainMethod.scanData.startIndex
|
||||
TestUtil.assertNodeEqual(LdcInsnNode("Hello, world!"), instructions[startIndex]!!)
|
||||
// Create a new LDC node and replace the LDC instruction.
|
||||
val stringNode = LdcInsnNode("Hello, ReVanced! Editing bytecode.")
|
||||
instructions.setAt(startIndex, stringNode)
|
||||
|
||||
// Now lets print our string twice!
|
||||
// Insert our instructions after the second instruction by our pattern.
|
||||
// This will place our instructions after the original INVOKEVIRTUAL call.
|
||||
// You could also copy the instructions from the list and then modify the LDC instruction again,
|
||||
// but this is to show a more advanced example of writing bytecode using the patcher and ASM.
|
||||
instructions.insertAt(
|
||||
startIndex + 1,
|
||||
FieldInsnNode(
|
||||
GETSTATIC,
|
||||
Type.getInternalName(System::class.java), // "java/lang/System"
|
||||
"out",
|
||||
"L" + Type.getInternalName(PrintStream::class.java) // "Ljava/io/PrintStream"
|
||||
),
|
||||
LdcInsnNode("Hello, ReVanced! Adding bytecode."),
|
||||
MethodInsnNode(
|
||||
INVOKEVIRTUAL,
|
||||
Type.getInternalName(PrintStream::class.java), // "java/io/PrintStream"
|
||||
"println",
|
||||
Type.getMethodDescriptor(
|
||||
Type.VOID_TYPE,
|
||||
Type.getType(String::class.java)
|
||||
) // "(Ljava/lang/String;)V"
|
||||
)
|
||||
)
|
||||
|
||||
// Our code now looks like this:
|
||||
// public static main(java.lang.String[] arg0) { // Method signature: ([Ljava/lang/String;)V
|
||||
// getstatic java/lang/System.out:java.io.PrintStream
|
||||
// ldc "Hello, ReVanced! Editing bytecode." (java.lang.String) // We overwrote this instruction.
|
||||
// invokevirtual java/io/PrintStream.println(Ljava/lang/String;)V
|
||||
// getstatic java/lang/System.out:java.io.PrintStream // This instruction and the 2 instructions below are written manually.
|
||||
// ldc "Hello, ReVanced! Adding bytecode." (java.lang.String)
|
||||
// invokevirtual java/io/PrintStream.println(Ljava/lang/String;)V
|
||||
// return
|
||||
// }
|
||||
|
||||
// Finally, tell the patcher that this patch was a success.
|
||||
// You can also return PatchResultError with a message.
|
||||
// If an exception is thrown inside this function,
|
||||
// a PatchResultError will be returned with the error message.
|
||||
return PatchResultSuccess()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Apply all patches loaded in the patcher
|
||||
val patchResult = patcher.applyPatches()
|
||||
// You can check if an error occurred
|
||||
for ((patchName, result) in patchResult) {
|
||||
if (result.isFailure) {
|
||||
throw Exception("Patch $patchName failed", result.exceptionOrNull()!!)
|
||||
}
|
||||
}
|
||||
|
||||
patcher.save()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test patcher with no changes`() {
|
||||
val testData = PatcherTest::class.java.getResourceAsStream("/test1.jar")!!
|
||||
// val available = testData.available()
|
||||
val out = ByteArrayOutputStream()
|
||||
Patcher(testData, out, testSignatures).save()
|
||||
// FIXME(Sculas): There seems to be a 1-byte difference, not sure what it is.
|
||||
// assertEquals(available, out.size())
|
||||
out.close()
|
||||
}
|
||||
|
||||
@Test()
|
||||
fun `should not raise an exception if any signature member except the name is missing`() {
|
||||
val sigName = "testMethod"
|
||||
|
||||
assertDoesNotThrow("Should raise an exception because opcodes is empty") {
|
||||
Patcher(
|
||||
PatcherTest::class.java.getResourceAsStream("/test1.jar")!!,
|
||||
ByteArrayOutputStream(),
|
||||
arrayOf(
|
||||
Signature(
|
||||
sigName,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,12 +1,12 @@
|
||||
package net.revanced.patcher
|
||||
package app.revanced.patcher
|
||||
|
||||
import java.io.ByteArrayOutputStream
|
||||
import kotlin.test.Test
|
||||
|
||||
internal class ReaderTest {
|
||||
@Test
|
||||
fun `read jar containing multiple classes`() {
|
||||
val testData = PatcherTest::class.java.getResourceAsStream("/test2.jar")!!
|
||||
Patcher(testData, PatcherTest.testSigs) // reusing test sigs from PatcherTest
|
||||
testData.close()
|
||||
Patcher(testData, ByteArrayOutputStream(), PatcherTest.testSignatures) // reusing test sigs from PatcherTest
|
||||
}
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package net.revanced.patcher.util
|
||||
package app.revanced.patcher.util
|
||||
|
||||
import org.objectweb.asm.tree.AbstractInsnNode
|
||||
import org.objectweb.asm.tree.FieldInsnNode
|
||||
@@ -17,7 +17,7 @@ object TestUtil {
|
||||
private fun AbstractInsnNode.nodeString(): String {
|
||||
val sb = NodeStringBuilder()
|
||||
when (this) {
|
||||
// TODO: Add more types
|
||||
// TODO(Sculas): Add more types
|
||||
is LdcInsnNode -> sb
|
||||
.addType("cst", cst)
|
||||
is FieldInsnNode -> sb
|
@@ -1,144 +0,0 @@
|
||||
package net.revanced.patcher
|
||||
|
||||
import net.revanced.patcher.patch.Patch
|
||||
import net.revanced.patcher.patch.PatchResultSuccess
|
||||
import net.revanced.patcher.signature.Signature
|
||||
import net.revanced.patcher.util.ExtraTypes
|
||||
import net.revanced.patcher.util.TestUtil
|
||||
import net.revanced.patcher.writer.ASMWriter.insertAt
|
||||
import net.revanced.patcher.writer.ASMWriter.setAt
|
||||
import org.objectweb.asm.Opcodes.*
|
||||
import org.objectweb.asm.Type
|
||||
import org.objectweb.asm.tree.*
|
||||
import java.io.PrintStream
|
||||
import kotlin.test.Test
|
||||
|
||||
internal class PatcherTest {
|
||||
companion object {
|
||||
val testSigs: Array<Signature> = arrayOf(
|
||||
// Java:
|
||||
// public static void main(String[] args) {
|
||||
// System.out.println("Hello, world!");
|
||||
// }
|
||||
// Bytecode:
|
||||
// public static main(java.lang.String[] arg0) { // Method signature: ([Ljava/lang/String;)V
|
||||
// getstatic java/lang/System.out:java.io.PrintStream
|
||||
// ldc "Hello, world!" (java.lang.String)
|
||||
// invokevirtual java/io/PrintStream.println(Ljava/lang/String;)V
|
||||
// return
|
||||
// }
|
||||
Signature(
|
||||
"mainMethod",
|
||||
Type.VOID_TYPE,
|
||||
ACC_PUBLIC or ACC_STATIC,
|
||||
arrayOf(ExtraTypes.ArrayAny),
|
||||
arrayOf(
|
||||
LDC,
|
||||
INVOKEVIRTUAL
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPatcher() {
|
||||
val testData = PatcherTest::class.java.getResourceAsStream("/test1.jar")!!
|
||||
val patcher = Patcher(testData, testSigs)
|
||||
|
||||
patcher.addPatches(
|
||||
Patch ("TestPatch") {
|
||||
// Get the method from the resolver cache
|
||||
val mainMethod = patcher.cache.methods["mainMethod"]
|
||||
// Get the instruction list
|
||||
val instructions = mainMethod.method.instructions!!
|
||||
|
||||
// Let's modify it, so it prints "Hello, ReVanced! Editing bytecode."
|
||||
// Get the start index of our opcode pattern.
|
||||
// This will be the index of the LDC instruction.
|
||||
val startIndex = mainMethod.scanData.startIndex
|
||||
TestUtil.assertNodeEqual(LdcInsnNode("Hello, world!"), instructions[startIndex]!!)
|
||||
// Create a new LDC node and replace the LDC instruction.
|
||||
val stringNode = LdcInsnNode("Hello, ReVanced! Editing bytecode.")
|
||||
instructions.setAt(startIndex, stringNode)
|
||||
|
||||
// Now lets print our string twice!
|
||||
// Insert our instructions after the second instruction by our pattern.
|
||||
// This will place our instructions after the original INVOKEVIRTUAL call.
|
||||
// You could also copy the instructions from the list and then modify the LDC instruction again,
|
||||
// but this is to show a more advanced example of writing bytecode using the patcher and ASM.
|
||||
instructions.insertAt(
|
||||
startIndex + 1,
|
||||
FieldInsnNode(
|
||||
GETSTATIC,
|
||||
Type.getInternalName(System::class.java), // "java/io/System"
|
||||
"out",
|
||||
Type.getInternalName(PrintStream::class.java) // "java.io.PrintStream"
|
||||
),
|
||||
LdcInsnNode("Hello, ReVanced! Adding bytecode."),
|
||||
MethodInsnNode(
|
||||
INVOKEVIRTUAL,
|
||||
Type.getInternalName(PrintStream::class.java), // "java/io/PrintStream"
|
||||
"println",
|
||||
Type.getMethodDescriptor(
|
||||
Type.VOID_TYPE,
|
||||
Type.getType(String::class.java)
|
||||
) // "(Ljava/lang/String;)V"
|
||||
)
|
||||
)
|
||||
|
||||
// Our code now looks like this:
|
||||
// public static main(java.lang.String[] arg0) { // Method signature: ([Ljava/lang/String;)V
|
||||
// getstatic java/lang/System.out:java.io.PrintStream
|
||||
// ldc "Hello, ReVanced! Editing bytecode." (java.lang.String) // We overwrote this instruction.
|
||||
// invokevirtual java/io/PrintStream.println(Ljava/lang/String;)V
|
||||
// getstatic java/lang/System.out:java.io.PrintStream // This instruction and the 2 instructions below are written manually.
|
||||
// ldc "Hello, ReVanced! Adding bytecode." (java.lang.String)
|
||||
// invokevirtual java/io/PrintStream.println(Ljava/lang/String;)V
|
||||
// return
|
||||
// }
|
||||
|
||||
// Finally, tell the patcher that this patch was a success.
|
||||
// You can also return PatchResultError with a message.
|
||||
// If an exception is thrown inside this function,
|
||||
// a PatchResultError will be returned with the error message.
|
||||
PatchResultSuccess()
|
||||
}
|
||||
)
|
||||
|
||||
// Apply all patches loaded in the patcher
|
||||
val result = patcher.applyPatches()
|
||||
// You can check if an error occurred
|
||||
for ((s, r) in result) {
|
||||
if (r.isFailure) {
|
||||
throw Exception("Patch $s failed", r.exceptionOrNull()!!)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Doesn't work, needs to be fixed.
|
||||
//val out = ByteArrayOutputStream()
|
||||
//patcher.saveTo(out)
|
||||
//assertTrue(
|
||||
// // 8 is a random value, it's just weird if it's any lower than that
|
||||
// out.size() > 8,
|
||||
// "Output must be at least 8 bytes"
|
||||
//)
|
||||
//
|
||||
//out.close()
|
||||
testData.close()
|
||||
}
|
||||
|
||||
// TODO Doesn't work, needs to be fixed.
|
||||
//@Test
|
||||
//fun `test patcher with no changes`() {
|
||||
// val testData = PatcherTest::class.java.getResourceAsStream("/test1.jar")!!
|
||||
// val available = testData.available()
|
||||
// val patcher = Patcher(testData, testSigs)
|
||||
//
|
||||
// val out = ByteArrayOutputStream()
|
||||
// patcher.saveTo(out)
|
||||
// assertEquals(available, out.size())
|
||||
//
|
||||
// out.close()
|
||||
// testData.close()
|
||||
//}
|
||||
}
|
Reference in New Issue
Block a user