1
mirror of https://github.com/revanced/revanced-patcher synced 2025-09-03 03:43:05 +02:00

Compare commits

...

29 Commits

Author SHA1 Message Date
semantic-release-bot
fc4b673087 chore(release): 1.0.0-dev.5 [skip ci]
# [1.0.0-dev.5](https://github.com/ReVancedTeam/revanced-patcher/compare/v1.0.0-dev.4...v1.0.0-dev.5) (2022-03-24)

### Bug Fixes

* **MethodResolver:** strip labels and line numbers so opcode patterns match ([8d1bb5f](8d1bb5f3d9))
2022-03-24 20:30:54 +00:00
Lucaskyy
8d1bb5f3d9 fix(MethodResolver): strip labels and line numbers so opcode patterns match 2022-03-24 21:27:44 +01:00
Lucaskyy
c8a017a4c0 refactor: only compute maxs and use existing stack frames 2022-03-24 19:45:13 +01:00
semantic-release-bot
51fb59a43c chore(release): 1.0.0-dev.4 [skip ci]
# [1.0.0-dev.4](https://github.com/ReVancedTeam/revanced-patcher/compare/v1.0.0-dev.3...v1.0.0-dev.4) (2022-03-23)

### Bug Fixes

* give ClassWriter a ClassReader for symtable ([e8f6973](e8f6973938))
2022-03-23 22:02:18 +00:00
Lucaskyy
a78715133c Merge remote-tracking branch 'origin/dev' into dev 2022-03-23 23:01:20 +01:00
Lucaskyy
e8f6973938 fix: give ClassWriter a ClassReader for symtable
removed SafeClassWriter as it was unused
2022-03-23 23:01:13 +01:00
semantic-release-bot
3cb1e01587 chore(release): 1.0.0-dev.3 [skip ci]
# [1.0.0-dev.3](https://github.com/ReVancedTeam/revanced-patcher/compare/v1.0.0-dev.2...v1.0.0-dev.3) (2022-03-23)

### Features

* add SafeClassWriter ([ca6b94d](ca6b94d943))
2022-03-23 21:34:05 +00:00
Lucaskyy
cb4ee207e1 Merge remote-tracking branch 'origin/dev' into dev 2022-03-23 22:32:58 +01:00
Lucaskyy
ca6b94d943 feat: add SafeClassWriter
the standard ClassWriter implementation uses the ClassLoader to find a common superclass. this won't work for us since we are not loading the JAR into the classpath. using this SafeClassWriter should fix that issue.
2022-03-23 22:32:50 +01:00
semantic-release-bot
6cdb6887d4 chore(release): 1.0.0-dev.2 [skip ci]
# [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](ab6453ca8a))
2022-03-23 21:10:02 +00:00
Lucaskyy
ab6453ca8a fix: set marklimit to Integer.MAX_VALUE 2022-03-23 22:08:51 +01:00
semantic-release-bot
e8182c17ad chore(release): 1.0.0-dev.1 [skip ci]
# 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](d5a3c76389))
* current must be calculated after increment ([5f12bab](5f12bab5df))
* **gradle:** publish source and javadocs ([87bbde5](87bbde5e06))
* **Io:** fix finding classes by name ([460d62a](460d62a24c))
* **Io:** JAR loading and saving ([#8](https://github.com/ReVancedTeam/revanced-patcher/issues/8)) ([4d98cbc](4d98cbc9e8))
* nullable signature members ([#10](https://github.com/ReVancedTeam/revanced-patcher/issues/10)) ([8db8893](8db8893ab1))
* Patch should have access to the Cache ([6c0f082](6c0f0823c9))
* remove broken code ([0e72a6e](0e72a6e85f))
* set index for insertAt to 0 by default ([1769132](1769132a9e))
* workflow on dev branch ([7e67daf](7e67daf878))

### Code Refactoring

* convert Patch to abstract class ([23e897a](23e897a7a9))
* Optimize Signature class ([#11](https://github.com/ReVancedTeam/revanced-patcher/issues/11)) ([49beec9](49beec9fc6))
* Rename `net.revanced` to `app.revanced` ([3ab42a9](3ab42a932c))

### Features

* Add `findParentMethod` utility method ([#4](https://github.com/ReVancedTeam/revanced-patcher/issues/4)) ([00c6ab7](00c6ab7faf))

### 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.
2022-03-23 19:01:41 +00:00
Lucaskyy
49beec9fc6 refactor: Optimize Signature class (#11)
BREAKING CHANGE: Array<Int> was changed to IntArray. This breaks existing patches.
2022-03-23 20:00:35 +01:00
Lucaskyy
3ab42a932c refactor: Rename net.revanced to app.revanced
BREAKING CHANGE: Package name was changed from "net.revanced" to "app.revanced"
2022-03-23 19:56:37 +01:00
oSumAtrIX
4d98cbc9e8 fix(Io): JAR loading and saving (#8)
* refactor: Complete rewrite of `Io`

* style: format code

* style: rewrite todos

* fix: use lateinit instead of nonnull assert for zipEntry

* fix: use lateinit instead of nonnull assert for jarEntry & reuse zipEntry

* docs: add docs to `Patcher`

* test: match output of patcher

* chore: add todo to `Io` for removing non-class files

Co-authored-by: Sculas <contact@sculas.xyz>
2022-03-23 19:56:35 +01:00
Lucaskyy
87bbde5e06 fix(gradle): publish source and javadocs 2022-03-23 19:56:34 +01:00
oSumAtrIX
8db8893ab1 fix: nullable signature members (#10)
This commit will allow "partial" signatures, basically we will be allowed to exclude members to match for the signature
2022-03-23 19:56:33 +01:00
oSumAtrIX
00c6ab7faf feat: Add findParentMethod utility method (#4)
* feat: Add `findParentMethod` utitly method

* refactor: add `resolveMethod` to `MethodResolver`

added some assertions and some tests

Co-authored-by: Lucaskyy <contact@sculas.xyz>
2022-03-23 19:56:31 +01:00
Bleuzen
460d62a24c fix(Io): fix finding classes by name 2022-03-23 19:55:40 +01:00
Lucaskyy
89e4b9f762 chore: push IntelliJ project files 2022-03-23 19:55:39 +01:00
Lucaskyy
a8fd7c00c3 refactor: target java 8 instead of java 17 2022-03-23 19:55:38 +01:00
Lucaskyy
1769132a9e fix: set index for insertAt to 0 by default 2022-03-23 19:55:37 +01:00
Lucaskyy
6c0f0823c9 fix: Patch should have access to the Cache
BREAKING CHANGE: Method signature of execute() was changed to include the cache, this will break existing implementations of the Patch class.
2022-03-23 19:55:35 +01:00
Lucaskyy
23e897a7a9 refactor: convert Patch to abstract class
BREAKING CHANGE: Patch class is now an abstract class. You must implement it. You can use anonymous implements, like done in the tests.
2022-03-23 19:55:34 +01:00
Lucaskyy
7e67daf878 fix: workflow on dev branch 2022-03-20 20:42:55 +01:00
Lucaskyy
593c83f29f style: remove tab 2022-03-20 20:39:47 +01:00
Sculas
72e123dd01 Merge pull request #3 from ReVancedTeam/ci-semantic-release
ci: add semantic-release
2022-03-20 20:34:31 +01:00
she11sh0cked
599a401ed9 ci: add gradle-semantic-release-plugin and remove the github release assets 2022-03-20 19:32:20 +01:00
she11sh0cked
3f8500b059 ci: add semantic-release 2022-03-20 19:03:05 +01:00
28 changed files with 692 additions and 421 deletions

39
.github/workflows/release.yml vendored Normal file
View 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
View 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
View File

@@ -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
View File

@@ -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
View 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"
]
}

63
CHANGELOG.md Normal file
View File

@@ -0,0 +1,63 @@
# [1.0.0-dev.5](https://github.com/ReVancedTeam/revanced-patcher/compare/v1.0.0-dev.4...v1.0.0-dev.5) (2022-03-24)
### Bug Fixes
* **MethodResolver:** strip labels and line numbers so opcode patterns match ([8d1bb5f](https://github.com/ReVancedTeam/revanced-patcher/commit/8d1bb5f3d9da544cf6e3e3848bfcc56327cde810))
# [1.0.0-dev.4](https://github.com/ReVancedTeam/revanced-patcher/compare/v1.0.0-dev.3...v1.0.0-dev.4) (2022-03-23)
### Bug Fixes
* give ClassWriter a ClassReader for symtable ([e8f6973](https://github.com/ReVancedTeam/revanced-patcher/commit/e8f6973938c70002f04a86f329aa5b134f6ef649))
# [1.0.0-dev.3](https://github.com/ReVancedTeam/revanced-patcher/compare/v1.0.0-dev.2...v1.0.0-dev.3) (2022-03-23)
### Features
* add SafeClassWriter ([ca6b94d](https://github.com/ReVancedTeam/revanced-patcher/commit/ca6b94d943b7067aae87a4e282cfb323811c0462))
# [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.

View File

@@ -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 {

View File

@@ -1,2 +1,2 @@
kotlin.code.style=official
version=1.0.0
kotlin.code.style = official
version = 1.0.0-dev.5

View 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
}
}
}
}

View File

@@ -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
)

View 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
)

View 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
}

View File

@@ -1,4 +1,4 @@
package net.revanced.patcher.patch
package app.revanced.patcher.patch
interface PatchResult {
fun error(): PatchResultError? {

View File

@@ -0,0 +1,157 @@
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.*
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.stripLabels().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() = this
private operator fun ClassNode.component2() = 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()
}
private fun InsnList.stripLabels(): InsnList {
this.removeAll { it is LabelNode || it is LineNumberNode }
return this
}

View File

@@ -1,4 +1,4 @@
package net.revanced.patcher.resolver
package app.revanced.patcher.resolver
internal data class ScanResult(
val found: Boolean,

View File

@@ -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?
)

View File

@@ -1,4 +1,4 @@
package net.revanced.patcher.util
package app.revanced.patcher.util
import org.objectweb.asm.Type

View File

@@ -0,0 +1,94 @@
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)
val classReaders = mutableMapOf<String, ClassReader>()
// 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) {
if (zipEntry.name.endsWith(".class")) {
classReaders[zipEntry.name] = ClassReader(jis.readBytes())
continue
}
// create a new zipEntry and write the contents of the zipEntry to the output stream and close it
jos.putNextEntry(ZipEntry(zipEntry))
jos.write(jis.readBytes())
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
val name = patchedClass.name + ".class"
jos.putNextEntry(JarEntry(name))
// parse the patched class to a byte array and write it to the output stream
val cw = ClassWriter(classReaders[name]!!, ClassWriter.COMPUTE_MAXS)
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()
}
}

View File

@@ -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())
}

View File

@@ -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)
}
}

View File

@@ -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
)

View File

@@ -1,9 +0,0 @@
package net.revanced.patcher.patch
class Patch(val patchName: String, val fn: () -> PatchResult) {
fun execute(): PatchResult {
return fn()
}
}

View File

@@ -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()
}

View File

@@ -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()
}
}
}

View 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
))
)
}
}
}

View File

@@ -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).save() // reusing test sigs from PatcherTest
}
}

View File

@@ -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
@@ -38,7 +38,8 @@ private class NodeStringBuilder {
}
override fun toString(): String {
if (sb.isEmpty()) return ""
val s = sb.toString()
return s.substring(0 until s.length - 2) // remove the last ", "
return s.substring(0 .. (s.length - 2).coerceAtLeast(0)) // remove the last ", "
}
}

View File

@@ -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()
//}
}