Compare commits

...

38 Commits

Author SHA1 Message Date
oSumAtrIX
c6fdf97794 refactor: do initial refactoring attempt 2023-07-30 00:10:49 +02:00
oSumAtrIX
c52f0b80f2 feat: Deprecate Version annotation 2023-07-30 00:10:49 +02:00
oSumAtrIX
4b5e25b29c chore: naming and better exception message 2023-07-30 00:10:48 +02:00
oSumAtrIX
c752a3c596 chore: add/fix comments 2023-07-30 00:10:48 +02:00
oSumAtrIX
740911a2a3 refactor: move stuff around and improve memory profile/performance 2023-07-30 00:10:48 +02:00
oSumAtrIX
242d805c6c chore: do not publish library, instead shade 2023-07-30 00:10:47 +02:00
oSumAtrIX
c543fdc18b chore: remove unnecessary naming 2023-07-30 00:10:47 +02:00
oSumAtrIX
d48a8e697f arsclib 2023-07-30 00:10:47 +02:00
Palm
8749a61d39 feat: remove Path option (#202)
BREAKING CHANGE: This removes the previously available `Path` option
2023-07-06 18:17:51 +02:00
oSumAtrIX
a4a030f2b2 build: update gradle 2023-07-06 18:15:40 +02:00
semantic-release-bot
dcc4ecd237 chore(release): 11.0.4 [skip ci]
## [11.0.4](https://github.com/revanced/revanced-patcher/compare/v11.0.3...v11.0.4) (2023-07-01)

### Bug Fixes

* clear method lookup maps ([#198](https://github.com/revanced/revanced-patcher/issues/198)) ([9d81baf](9d81baf4b4))
2023-07-01 00:42:50 +00:00
oSumAtrIX
57f3036a96 chore: merge branch dev to main (#199) 2023-07-01 02:41:21 +02:00
semantic-release-bot
753e55dfc3 chore(release): 11.0.4-dev.1 [skip ci]
## [11.0.4-dev.1](https://github.com/revanced/revanced-patcher/compare/v11.0.3...v11.0.4-dev.1) (2023-07-01)

### Bug Fixes

* clear method lookup maps ([#198](https://github.com/revanced/revanced-patcher/issues/198)) ([9d81baf](9d81baf4b4))
2023-07-01 00:37:30 +00:00
LisoUseInAIKyrios
9d81baf4b4 fix: clear method lookup maps (#198) 2023-07-01 02:35:49 +02:00
semantic-release-bot
7cb4d4c596 chore(release): 11.0.3 [skip ci]
## [11.0.3](https://github.com/revanced/revanced-patcher/compare/v11.0.2...v11.0.3) (2023-06-30)

### Bug Fixes

* NPE on method lookup ([#195](https://github.com/revanced/revanced-patcher/issues/195)) ([fcef434](fcef4342e8))
2023-06-30 23:56:27 +00:00
oSumAtrIX
2c8565508e chore: merge branch dev to main (#196) 2023-07-01 01:54:46 +02:00
semantic-release-bot
c7f156e4c9 chore(release): 11.0.3-dev.1 [skip ci]
## [11.0.3-dev.1](https://github.com/revanced/revanced-patcher/compare/v11.0.2...v11.0.3-dev.1) (2023-06-28)

### Bug Fixes

* NPE on method lookup ([#195](https://github.com/revanced/revanced-patcher/issues/195)) ([fcef434](fcef4342e8))
2023-06-28 19:57:40 +00:00
LisoUseInAIKyrios
fcef4342e8 fix: NPE on method lookup (#195) 2023-06-28 21:56:03 +02:00
semantic-release-bot
72783a5e74 chore(release): 11.0.2 [skip ci]
## [11.0.2](https://github.com/revanced/revanced-patcher/compare/v11.0.1...v11.0.2) (2023-06-27)

### Bug Fixes

* catch exceptions from closing patches ([d5d6f85](d5d6f85084))
* do not load annotations as patches ([519359a](519359a9eb))
* only close succeeded patches ([b8151eb](b8151ebccb))
* use `versionCode` if `versionName` is unavailable ([6e1b647](6e1b6479b6))

### Performance Improvements

* resolve fingerprints using method maps ([#185](https://github.com/revanced/revanced-patcher/issues/185)) ([d718134](d718134ab2))
2023-06-27 02:32:47 +00:00
oSumAtrIX
a379b69eeb chore: merge branch dev to main (#191) 2023-06-27 04:31:09 +02:00
semantic-release-bot
0a8ccba33e chore(release): 11.0.2-dev.4 [skip ci]
## [11.0.2-dev.4](https://github.com/revanced/revanced-patcher/compare/v11.0.2-dev.3...v11.0.2-dev.4) (2023-06-27)

### Bug Fixes

* do not load annotations as patches ([519359a](519359a9eb))
2023-06-27 02:11:33 +00:00
oSumAtrIX
519359a9eb fix: do not load annotations as patches 2023-06-27 04:09:03 +02:00
semantic-release-bot
b615ed6aab chore(release): 11.0.2-dev.3 [skip ci]
## [11.0.2-dev.3](https://github.com/revanced/revanced-patcher/compare/v11.0.2-dev.2...v11.0.2-dev.3) (2023-06-27)

### Performance Improvements

* resolve fingerprints using method maps ([#185](https://github.com/revanced/revanced-patcher/issues/185)) ([d718134](d718134ab2))
2023-06-27 02:08:29 +00:00
LisoUseInAIKyrios
d718134ab2 perf: resolve fingerprints using method maps (#185)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2023-06-27 04:06:51 +02:00
semantic-release-bot
5e681ed381 chore(release): 11.0.2-dev.2 [skip ci]
## [11.0.2-dev.2](https://github.com/revanced/revanced-patcher/compare/v11.0.2-dev.1...v11.0.2-dev.2) (2023-06-18)

### Bug Fixes

* use `versionCode` if `versionName` is unavailable ([6e1b647](6e1b6479b6))
2023-06-18 14:41:34 +00:00
oSumAtrIX
6e1b6479b6 fix: use versionCode if versionName is unavailable 2023-06-18 16:39:49 +02:00
semantic-release-bot
f3c9e28a62 chore(release): 11.0.2-dev.1 [skip ci]
## [11.0.2-dev.1](https://github.com/revanced/revanced-patcher/compare/v11.0.1...v11.0.2-dev.1) (2023-06-14)

### Bug Fixes

* catch exceptions from closing patches ([d5d6f85](d5d6f85084))
* only close succeeded patches ([b8151eb](b8151ebccb))
2023-06-14 00:16:26 +00:00
oSumAtrIX
d5d6f85084 fix: catch exceptions from closing patches 2023-06-14 02:14:37 +02:00
oSumAtrIX
b8151ebccb fix: only close succeeded patches 2023-06-14 01:43:19 +02:00
semantic-release-bot
5650e34432 chore(release): 11.0.1 [skip ci]
## [11.0.1](https://github.com/revanced/revanced-patcher/compare/v11.0.0...v11.0.1) (2023-06-12)

### Bug Fixes

* revert using `OutputStream.nullOutputStream` ([f02a426](f02a42610b))
2023-06-12 03:36:10 +00:00
oSumAtrIX
c893d16d52 chore: merge branch dev to main (#190) 2023-06-12 05:34:23 +02:00
semantic-release-bot
34f08bf206 chore(release): 11.0.1-dev.1 [skip ci]
## [11.0.1-dev.1](https://github.com/revanced/revanced-patcher/compare/v11.0.0...v11.0.1-dev.1) (2023-06-12)

### Bug Fixes

* revert using `OutputStream.nullOutputStream` ([f02a426](f02a42610b))
2023-06-12 03:33:53 +00:00
oSumAtrIX
f02a42610b fix: revert using OutputStream.nullOutputStream
Older Android versions don't support this API
2023-06-12 05:32:13 +02:00
oSumAtrIX
c95e6fa92f ci: add cache step 2023-06-12 02:55:09 +02:00
oSumAtrIX
fd738e723b ci: build before running semantic-release 2023-06-12 01:52:52 +02:00
oSumAtrIX
b1d1956323 ci: remove unnecessary steps 2023-06-12 01:47:26 +02:00
semantic-release-bot
725a8012ac chore(release): 11.0.0 [skip ci]
# [11.0.0](https://github.com/revanced/revanced-patcher/compare/v10.0.0...v11.0.0) (2023-06-10)

### Bug Fixes

* add imports to fix failing tests ([43d6868](43d6868d1f))

* refactor!: move extension functions to their corresponding classes ([a12fe7d](a12fe7dd9e))
* refactor!: use proper extension function names ([efdd01a](efdd01a988))
* fix!: implement extension functions consistently ([aacf900](aacf900764))

### BREAKING CHANGES

* This changes the import paths for extension functions.
* This changes the names of extension functions
* This changes the name of functions
2023-06-10 23:11:52 +00:00
oSumAtrIX
bb9a73e53b chore: merge branch dev to main (#187) 2023-06-11 01:10:59 +02:00
105 changed files with 2611 additions and 1616 deletions

9
.gitattributes vendored Normal file
View File

@@ -0,0 +1,9 @@
#
# https://help.github.com/articles/dealing-with-line-endings/
#
# Linux start script should use lf
/gradlew text eol=lf
# These are Windows script files and should use crlf
*.bat text eol=crlf

View File

@@ -23,17 +23,20 @@ jobs:
# https://github.com/cycjimmy/semantic-release-action#private-packages
persist-credentials: false
fetch-depth: 0
- name: Setup JDK
uses: actions/setup-java@v3
- name: Cache
uses: actions/cache@v3
with:
java-version: '17'
distribution: 'zulu'
cache: gradle
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: "18"
cache: 'npm'
path: |
${{ runner.home }}/.gradle/caches
${{ runner.home }}/.gradle/wrapper
.gradle
build
node_modules
key: ${{ runner.os }}-gradle-npm-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', 'package-lock.json') }}
- name: Build with Gradle
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./gradlew clean --no-daemon
- name: Setup semantic-release
run: npm install
- name: Release

View File

@@ -1,3 +1,108 @@
## [11.0.4](https://github.com/revanced/revanced-patcher/compare/v11.0.3...v11.0.4) (2023-07-01)
### Bug Fixes
* clear method lookup maps ([#198](https://github.com/revanced/revanced-patcher/issues/198)) ([9d81baf](https://github.com/revanced/revanced-patcher/commit/9d81baf4b4ca7514f8a1009e72218638609a7c7f))
## [11.0.4-dev.1](https://github.com/revanced/revanced-patcher/compare/v11.0.3...v11.0.4-dev.1) (2023-07-01)
### Bug Fixes
* clear method lookup maps ([#198](https://github.com/revanced/revanced-patcher/issues/198)) ([9d81baf](https://github.com/revanced/revanced-patcher/commit/9d81baf4b4ca7514f8a1009e72218638609a7c7f))
## [11.0.3](https://github.com/revanced/revanced-patcher/compare/v11.0.2...v11.0.3) (2023-06-30)
### Bug Fixes
* NPE on method lookup ([#195](https://github.com/revanced/revanced-patcher/issues/195)) ([fcef434](https://github.com/revanced/revanced-patcher/commit/fcef4342e8bde73945e8315aef6337cc8a8d8572))
## [11.0.3-dev.1](https://github.com/revanced/revanced-patcher/compare/v11.0.2...v11.0.3-dev.1) (2023-06-28)
### Bug Fixes
* NPE on method lookup ([#195](https://github.com/revanced/revanced-patcher/issues/195)) ([fcef434](https://github.com/revanced/revanced-patcher/commit/fcef4342e8bde73945e8315aef6337cc8a8d8572))
## [11.0.2](https://github.com/revanced/revanced-patcher/compare/v11.0.1...v11.0.2) (2023-06-27)
### Bug Fixes
* catch exceptions from closing patches ([d5d6f85](https://github.com/revanced/revanced-patcher/commit/d5d6f85084c03ed9c776632823ca12394a716167))
* do not load annotations as patches ([519359a](https://github.com/revanced/revanced-patcher/commit/519359a9eb0e9dfa390c5016e9fe4a7490b8ab18))
* only close succeeded patches ([b8151eb](https://github.com/revanced/revanced-patcher/commit/b8151ebccb5b27dd9e06fa63235cf9baeef1c0ee))
* use `versionCode` if `versionName` is unavailable ([6e1b647](https://github.com/revanced/revanced-patcher/commit/6e1b6479b677657c226693e9cc6b63f4ef2ee060))
### Performance Improvements
* resolve fingerprints using method maps ([#185](https://github.com/revanced/revanced-patcher/issues/185)) ([d718134](https://github.com/revanced/revanced-patcher/commit/d718134ab26423e02708e01eba711737f9260ba0))
## [11.0.2-dev.4](https://github.com/revanced/revanced-patcher/compare/v11.0.2-dev.3...v11.0.2-dev.4) (2023-06-27)
### Bug Fixes
* do not load annotations as patches ([519359a](https://github.com/revanced/revanced-patcher/commit/519359a9eb0e9dfa390c5016e9fe4a7490b8ab18))
## [11.0.2-dev.3](https://github.com/revanced/revanced-patcher/compare/v11.0.2-dev.2...v11.0.2-dev.3) (2023-06-27)
### Performance Improvements
* resolve fingerprints using method maps ([#185](https://github.com/revanced/revanced-patcher/issues/185)) ([d718134](https://github.com/revanced/revanced-patcher/commit/d718134ab26423e02708e01eba711737f9260ba0))
## [11.0.2-dev.2](https://github.com/revanced/revanced-patcher/compare/v11.0.2-dev.1...v11.0.2-dev.2) (2023-06-18)
### Bug Fixes
* use `versionCode` if `versionName` is unavailable ([6e1b647](https://github.com/revanced/revanced-patcher/commit/6e1b6479b677657c226693e9cc6b63f4ef2ee060))
## [11.0.2-dev.1](https://github.com/revanced/revanced-patcher/compare/v11.0.1...v11.0.2-dev.1) (2023-06-14)
### Bug Fixes
* catch exceptions from closing patches ([d5d6f85](https://github.com/revanced/revanced-patcher/commit/d5d6f85084c03ed9c776632823ca12394a716167))
* only close succeeded patches ([b8151eb](https://github.com/revanced/revanced-patcher/commit/b8151ebccb5b27dd9e06fa63235cf9baeef1c0ee))
## [11.0.1](https://github.com/revanced/revanced-patcher/compare/v11.0.0...v11.0.1) (2023-06-12)
### Bug Fixes
* revert using `OutputStream.nullOutputStream` ([f02a426](https://github.com/revanced/revanced-patcher/commit/f02a42610b7698fc8cc6bc5183c9ccba2ed96cda))
## [11.0.1-dev.1](https://github.com/revanced/revanced-patcher/compare/v11.0.0...v11.0.1-dev.1) (2023-06-12)
### Bug Fixes
* revert using `OutputStream.nullOutputStream` ([f02a426](https://github.com/revanced/revanced-patcher/commit/f02a42610b7698fc8cc6bc5183c9ccba2ed96cda))
# [11.0.0](https://github.com/revanced/revanced-patcher/compare/v10.0.0...v11.0.0) (2023-06-10)
### Bug Fixes
* add imports to fix failing tests ([43d6868](https://github.com/revanced/revanced-patcher/commit/43d6868d1f59922f9812f3e4a2a890f3b331def6))
* refactor!: move extension functions to their corresponding classes ([a12fe7d](https://github.com/revanced/revanced-patcher/commit/a12fe7dd9e976c38a0a82fe35e6650f58f815de4))
* refactor!: use proper extension function names ([efdd01a](https://github.com/revanced/revanced-patcher/commit/efdd01a9886b6f06af213731824621964367b2a3))
* fix!: implement extension functions consistently ([aacf900](https://github.com/revanced/revanced-patcher/commit/aacf9007647b1cc11bc40053625802573efda6ef))
### BREAKING CHANGES
* This changes the import paths for extension functions.
* This changes the names of extension functions
* This changes the name of functions
# [11.0.0-dev.2](https://github.com/revanced/revanced-patcher/compare/v11.0.0-dev.1...v11.0.0-dev.2) (2023-06-09)

42
arsclib-utils/.gitignore vendored Normal file
View File

@@ -0,0 +1,42 @@
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store

View File

@@ -0,0 +1,18 @@
plugins {
kotlin("jvm")
`maven-publish`
}
group = "app.revanced"
dependencies {
implementation("io.github.reandroid:ARSCLib:1.1.7")
}
java {
withSourcesJar()
}
kotlin {
jvmToolchain(11)
}

View File

@@ -0,0 +1,72 @@
package app.revanced.arsc
/**
* An exception thrown when there is an error with APK resources.
*
* @param message The exception message.
* @param throwable The corresponding [Throwable].
*/
sealed class ApkResourceException(message: String, throwable: Throwable? = null) : Exception(message, throwable) {
/**
* An exception when locking resources.
*
* @param message The exception message.
* @param throwable The corresponding [Throwable].
*/
class Locked(message: String, throwable: Throwable? = null) : ApkResourceException(message, throwable)
/**
* An exception when writing resources.
*
* @param message The exception message.
* @param throwable The corresponding [Throwable].
*/
class Write(message: String, throwable: Throwable? = null) : ApkResourceException(message, throwable)
/**
* An exception when reading resources.
*
* @param message The exception message.
* @param throwable The corresponding [Throwable].
*/
class Read(message: String, throwable: Throwable? = null) : ApkResourceException(message, throwable)
/**
* An exception when decoding resources.
*
* @param message The exception message.
* @param throwable The corresponding [Throwable].
*/
class Decode(message: String, throwable: Throwable? = null) : ApkResourceException(message, throwable)
/**
* An exception when encoding resources.
*
* @param message The exception message.
* @param throwable The corresponding [Throwable].
*/
class Encode(message: String, throwable: Throwable? = null) : ApkResourceException(message, throwable)
/**
* An exception thrown when a reference could not be resolved.
*
* @param reference The invalid reference.
* @param throwable The corresponding [Throwable].
*/
class InvalidReference(reference: String, throwable: Throwable? = null) :
ApkResourceException("Failed to resolve: $reference", throwable) {
/**
* An exception thrown when a reference could not be resolved.
*
* @param type The type of the reference.
* @param name The name of the reference.
* @param throwable The corresponding [Throwable].
*/
constructor(type: String, name: String, throwable: Throwable? = null) : this("@$type/$name", throwable)
}
/**
* An exception thrown when the Apk file not have a resource table, but was expected to have one.
*/
class MissingResourceTable : ApkResourceException("Apk does not have a resource table.")
}

View File

@@ -0,0 +1,28 @@
@file:Suppress("MemberVisibilityCanBePrivate")
package app.revanced.arsc.archive
import app.revanced.arsc.resource.ResourceContainer
import com.reandroid.apk.ApkModule
import com.reandroid.apk.DexFileInputSource
import com.reandroid.archive.InputSource
import java.io.File
import java.io.Flushable
/**
* A class for reading/writing files in an [ApkModule].
*
* @param module The [ApkModule] to operate on.
*/
class Archive(internal val module: ApkModule) : Flushable {
val mainPackageResources = ResourceContainer(this, module.tableBlock)
fun save(output: File) {
flush()
module.writeApk(output)
}
fun readDexFiles(): MutableList<DexFileInputSource> = module.listDexFiles()
fun write(inputSource: InputSource) = module.apkArchive.add(inputSource) // Overwrites existing files.
fun read(name: String): InputSource? = module.apkArchive.getInputSource(name)
override fun flush() = mainPackageResources.flush()
}

View File

@@ -0,0 +1,7 @@
package app.revanced.arsc.logging
interface Logger {
fun error(msg: String)
fun warn(msg: String)
fun info(msg: String)
fun trace(msg: String)
}

View File

@@ -0,0 +1,166 @@
package app.revanced.arsc.resource
import app.revanced.arsc.ApkResourceException
import com.reandroid.arsc.coder.EncodeResult
import com.reandroid.arsc.coder.ValueDecoder
import com.reandroid.arsc.value.Entry
import com.reandroid.arsc.value.ValueType
import com.reandroid.arsc.value.array.ArrayBag
import com.reandroid.arsc.value.array.ArrayBagItem
import com.reandroid.arsc.value.plurals.PluralsBag
import com.reandroid.arsc.value.plurals.PluralsBagItem
import com.reandroid.arsc.value.plurals.PluralsQuantity
import com.reandroid.arsc.value.style.StyleBag
import com.reandroid.arsc.value.style.StyleBagItem
/**
* A resource value.
*/
sealed class Resource {
internal abstract fun write(entry: Entry, resources: ResourceContainer)
}
internal val Resource.isComplex get() = when (this) {
is Scalar -> false
is Complex -> true
}
/**
* A simple resource.
*/
open class Scalar internal constructor(private val valueType: ValueType, private val value: Int) : Resource() {
protected open fun data(resources: ResourceContainer) = value
override fun write(entry: Entry, resources: ResourceContainer) {
entry.setValueAsRaw(valueType, data(resources))
}
internal open fun toArrayItem(resources: ResourceContainer) = ArrayBagItem.create(valueType, data(resources))
internal open fun toStyleItem(resources: ResourceContainer) = StyleBagItem.create(valueType, data(resources))
}
/**
* A marker class for complex resources.
*/
sealed class Complex : Resource()
private fun encoded(encodeResult: EncodeResult?) = encodeResult?.let { Scalar(it.valueType, it.value) }
?: throw ApkResourceException.Encode("Failed to encode value")
/**
* Encode a color.
*
* @param hex The hex value of the color.
* @return The encoded [Resource].
*/
fun color(hex: String) = encoded(ValueDecoder.encodeColor(hex))
/**
* Encode a dimension or fraction.
*
* @param value The dimension value such as 24dp.
* @return The encoded [Resource].
*/
fun dimension(value: String) = encoded(ValueDecoder.encodeDimensionOrFraction(value))
/**
* Encode a boolean resource.
*
* @param value The boolean.
* @return The encoded [Resource].
*/
fun boolean(value: Boolean) = Scalar(ValueType.INT_BOOLEAN, if (value) -Int.MAX_VALUE else 0)
/**
* Encode a float.
*
* @param n The number to encode.
* @return The encoded [Resource].
*/
fun float(n: Float) = Scalar(ValueType.FLOAT, n.toBits())
/**
* Create an integer [Resource].
*
* @param n The number to encode.
* @return The integer [Resource].
*/
fun integer(n: Int) = Scalar(ValueType.INT_DEC, n)
/**
* Create a reference [Resource].
*
* @param resourceId The target resource.
* @return The reference resource.
*/
fun reference(resourceId: Int) = Scalar(ValueType.REFERENCE, resourceId)
/**
* Resolve and create a reference [Resource].
*
* @see reference
* @param ref The reference string to resolve.
* @param resourceTable The resource table to resolve the reference with.
* @return The reference resource.
*/
fun reference(resourceTable: ResourceTable, ref: String) = reference(resourceTable.resolve(ref))
/**
* An array [Resource].
*
* @param elements The elements of the array.
*/
class Array(private val elements: Collection<Scalar>) : Complex() {
override fun write(entry: Entry, resources: ResourceContainer) {
ArrayBag.create(entry).addAll(elements.map { it.toArrayItem(resources) })
}
}
/**
* A style resource.
*
* @param elements The attributes to override.
* @param parent A reference to the parent style.
*/
class Style(private val elements: Map<String, Scalar>, private val parent: String? = null) : Complex() {
override fun write(entry: Entry, resources: ResourceContainer) {
val resTable = resources.resourceTable
val style = StyleBag.create(entry)
parent?.let {
style.parentId = resTable.resolve(parent)
}
style.putAll(
elements.asIterable().associate {
StyleBag.resolve(resTable.encodeMaterials, it.key) to it.value.toStyleItem(resources)
})
}
}
/**
* A quantity string [Resource].
*
* @param elements A map of the quantity to the corresponding string.
*/
class Plurals(private val elements: Map<String, String>) : Complex() {
override fun write(entry: Entry, resources: ResourceContainer) {
val plurals = PluralsBag.create(entry)
plurals.putAll(elements.asIterable().associate { (k, v) ->
PluralsQuantity.value(k) to PluralsBagItem.string(resources.getOrCreateString(v))
})
}
}
/**
* A string [Resource].
*
* @param value The string value.
*/
class StringResource(val value: String) : Scalar(ValueType.STRING, 0) {
private fun tableString(resources: ResourceContainer) = resources.getOrCreateString(value)
override fun data(resources: ResourceContainer) = tableString(resources).index
override fun toArrayItem(resources: ResourceContainer) = ArrayBagItem.string(tableString(resources))
override fun toStyleItem(resources: ResourceContainer) = StyleBagItem.string(tableString(resources))
}

View File

@@ -0,0 +1,167 @@
package app.revanced.arsc.resource
import app.revanced.arsc.ApkResourceException
import app.revanced.arsc.archive.Archive
import com.reandroid.apk.xmlencoder.EncodeUtil
import com.reandroid.arsc.chunk.TableBlock
import com.reandroid.arsc.chunk.xml.ResXmlDocument
import com.reandroid.arsc.value.Entry
import com.reandroid.arsc.value.ResConfig
import java.io.Closeable
import java.io.File
import java.io.Flushable
class ResourceContainer(private val archive: Archive, internal val tableBlock: TableBlock) : Flushable {
private val packageBlock = tableBlock.pickOne() // Pick the main package block.
internal lateinit var resourceTable: ResourceTable // TODO: Set this.
private val lockedResourceFileNames = mutableSetOf<String>()
private fun lock(resourceFile: ResourceFile) {
if (resourceFile.name in lockedResourceFileNames) {
throw ApkResourceException.Locked("Resource file ${resourceFile.name} is already locked.")
}
lockedResourceFileNames.add(resourceFile.name)
}
private fun unlock(resourceFile: ResourceFile) {
lockedResourceFileNames.remove(resourceFile.name)
}
fun <T : ResourceFile> openResource(name: String): ResourceFileEditor<T> {
val inputSource = archive.read(name)
?: throw ApkResourceException.Read("Resource file $name not found.")
val resourceFile = when {
ResXmlDocument.isResXmlBlock(inputSource.openStream()) -> {
val xmlDocument = archive.module
.loadResXmlDocument(inputSource)
.decodeToXml(resourceTable.entryStore, packageBlock.id)
ResourceFile.XmlResourceFile(name, xmlDocument)
}
else -> {
val bytes = inputSource.openStream().use { it.readAllBytes() }
ResourceFile.BinaryResourceFile(name, bytes)
}
}
try {
@Suppress("UNCHECKED_CAST")
return ResourceFileEditor(resourceFile as T).also {
lockedResourceFileNames.add(name)
}
} catch (e: ClassCastException) {
throw ApkResourceException.Decode("Resource file $name is not ${resourceFile::class}.", e)
}
}
inner class ResourceFileEditor<T : ResourceFile> internal constructor(
private val resourceFile: T,
) : Closeable {
fun use(block: (T) -> Unit) = block(resourceFile)
override fun close() {
lockedResourceFileNames.remove(resourceFile.name)
}
}
override fun flush() {
TODO("Not yet implemented")
}
/**
* Open a resource file, creating it if the file does not exist.
*
* @param path The resource file path.
* @return The corresponding [ResourceFiles],
*/
fun openFile(path: String) = ResourceFiles(createHandle(path), archive)
private fun getPackageBlock() = packageBlock ?: throw ApkResourceException.MissingResourceTable
internal fun getOrCreateString(value: String) =
tableBlock?.stringPool?.getOrCreate(value) ?: throw ApkResourceException.MissingResourceTable
private fun Entry.set(resource: Resource) {
val existingEntryNameReference = specReference
// Sets this.specReference if the entry is not yet initialized.
// Sets this.specReference to 0 if the resource type of the existing entry changes.
ensureComplex(resource.isComplex)
if (existingEntryNameReference != 0) {
// Preserve the entry name by restoring the previous spec block reference (if present).
specReference = existingEntryNameReference
}
resource.write(this, this@ResourceContainer)
resourceTable.registerChanged(this)
}
/**
* Retrieve an [Entry] from the resource table.
*
* @param type The resource type.
* @param name The resource name.
* @param qualifiers The variant to use.
*/
private fun getEntry(type: String, name: String, qualifiers: String?): Entry? {
val resourceId = try {
resourceTable.resolve("@$type/$name")
} catch (_: ApkResourceException.InvalidReference) {
return null
}
val config = ResConfig.parse(qualifiers)
return tableBlock?.resolveReference(resourceId)?.singleOrNull { it.resConfig == config }
}
/**
* Create a [ResourceFiles.Handle] that can be used to open a [ResourceFiles].
* This may involve looking it up in the resource table to find the actual location in the archive.
*
* @param path The path of the resource.
*/
private fun createHandle(path: String): ResourceFiles.Handle {
if (path.startsWith("res/values")) throw ApkResourceException.Decode("Decoding the resource table as a file is not supported")
var onClose = {}
var archivePath = path
if (tableBlock != null && path.startsWith("res/") && path.count { it == '/' } == 2) {
val file = File(path)
val qualifiers = EncodeUtil.getQualifiersFromResFile(file)
val type = EncodeUtil.getTypeNameFromResFile(file)
val name = file.nameWithoutExtension
// The resource file names that the app developers used may have been minified, so we have to resolve it with the resource table.
// Example: res/drawable-hdpi/icon.png -> res/4a.png
getEntry(type, name, qualifiers)?.resValue?.valueAsString?.let {
archivePath = it
} ?: run {
// An entry for this specific resource file was not found in the resource table, so we have to register it after we save.
onClose = { setResource(type, name, StringResource(archivePath), qualifiers) }
}
}
return ResourceFiles.Handle(path, archivePath, onClose)
}
fun setResource(type: String, entryName: String, resource: Resource, qualifiers: String? = null) =
getPackageBlock().getOrCreate(qualifiers, type, entryName).also { it.set(resource) }.resourceId
fun setResources(type: String, resources: Map<String, Resource>, configuration: String? = null) {
getPackageBlock().getOrCreateSpecTypePair(type).getOrCreateTypeBlock(configuration).apply {
resources.forEach { (entryName, resource) -> getOrCreateEntry(entryName).set(resource) }
}
}
override fun flush() {
packageBlock?.name = archive
}
}

View File

@@ -0,0 +1,91 @@
package app.revanced.arsc.resource
import app.revanced.arsc.ApkResourceException
import app.revanced.arsc.archive.Archive
import com.reandroid.archive.InputSource
import com.reandroid.xml.XMLDocument
import com.reandroid.xml.XMLException
import java.io.*
abstract class ResourceFile(val name: String) {
internal var realName: String? = null
class XmlResourceFile(name: String, val document: XMLDocument) : ResourceFile(name)
class BinaryResourceFile(name: String, var bytes: ByteArray) : ResourceFile(name)
}
class ResourceFiles private constructor(
) : Closeable {
/**
* Instantiate a [ResourceFiles].
*
* @param handle The [Handle] associated with this file.
* @param archive The [Archive] that the file resides in.
*/
internal constructor(handle: Handle, archive: Archive) : this(
handle,
archive,
try {
archive.read(handle.archivePath)
} catch (e: XMLException) {
throw ApkResourceException.Decode("Failed to decode XML while reading ${handle.virtualPath}", e)
} catch (e: IOException) {
throw ApkResourceException.Decode("Could not read ${handle.virtualPath}", e)
}
)
companion object {
const val DEFAULT_BUFFER_SIZE = 1024
}
var contents = readResult?.data ?: ByteArray(0)
set(value) {
pendingWrite = true
field = value
}
val exists = readResult != null
override fun toString() = handle.virtualPath
override fun close() {
if (pendingWrite) {
val path = handle.archivePath
if (isXmlResource) archive.writeXml(
path,
try {
XMLDocument.load(inputStream())
} catch (e: XMLException) {
throw ApkResourceException.Encode("Failed to parse XML while writing ${handle.virtualPath}", e)
}
) else archive.writeRaw(path, contents)
}
handle.onClose()
archive.unlock(this)
}
fun inputStream(): InputStream = ByteArrayInputStream(contents)
fun outputStream(bufferSize: Int = DEFAULT_BUFFER_SIZE): OutputStream =
object : ByteArrayOutputStream(bufferSize) {
override fun close() {
this@ResourceFiles.contents = if (buf.size > count) buf.copyOf(count) else buf
super.close()
}
}
/**
* @param virtualPath The resource file path. Example: /res/drawable-hdpi/icon.png.
* @param archivePath The actual file path in the archive. Example: res/4a.png.
* @param onClose An action to perform when the file associated with this handle is closed
*/
internal data class Handle(val virtualPath: String, val archivePath: String, val onClose: () -> Unit)
}

View File

@@ -0,0 +1,100 @@
package app.revanced.arsc.resource
import app.revanced.arsc.ApkResourceException
import com.reandroid.apk.xmlencoder.EncodeException
import com.reandroid.apk.xmlencoder.EncodeMaterials
import com.reandroid.arsc.util.FrameworkTable
import com.reandroid.arsc.value.Entry
import com.reandroid.common.TableEntryStore
/**
* A high-level API for resolving resources in the resource table, which spans the entire ApkBundle.
*/
class ResourceTable(base: ResourceContainer, all: Sequence<ResourceContainer>) {
private val packageName = base.tableBlock!!.name
/**
* A [TableEntryStore] used to decode XML.
*/
internal val entryStore = TableEntryStore()
/**
* The [EncodeMaterials] to use for resolving resources and encoding XML.
*/
internal val encodeMaterials: EncodeMaterials = object : EncodeMaterials() {
/*
Our implementation is more efficient because it does not have to loop through every single entry group
when the resource id cannot be found in the TableIdentifier, which does not update when you create a new resource.
It also looks at the entire table instead of just the current package.
*/
override fun resolveLocalResourceId(type: String, name: String) = resolveLocal(type, name)
}
/**
* The resource mappings which are generated when the ApkBundle is created.
*/
private val tableIdentifier = encodeMaterials.tableIdentifier
/**
* A table of all the resources that have been changed or added.
*/
private val modifiedResources = HashMap<String, HashMap<String, Int>>()
/**
* Resolve a resource id for the specified resource.
* Cannot resolve resources from the android framework.
*
* @param type The type of the resource.
* @param name The name of the resource.
* @return The id of the resource.
*/
fun resolveLocal(type: String, name: String) =
modifiedResources[type]?.get(name)
?: tableIdentifier.get(packageName, type, name)?.resourceId
?: throw ApkResourceException.InvalidReference(
type,
name
)
/**
* Resolve a resource id for the specified resource.
*
* @param reference The resource reference string.
* @return The id of the resource.
*/
fun resolve(reference: String) = try {
encodeMaterials.resolveReference(reference)
} catch (e: EncodeException) {
throw ApkResourceException.InvalidReference(reference, e)
}
/**
* Notify the [ResourceTable] that an [Entry] has been created or modified.
*/
internal fun registerChanged(entry: Entry) {
modifiedResources.getOrPut(entry.typeName, ::HashMap)[entry.name] = entry.resourceId
}
init {
all.forEach {
it.tableBlock?.let { table ->
entryStore.add(table)
tableIdentifier.load(table)
}
it.resourceTable = this
}
base.also {
encodeMaterials.currentPackage = it.tableBlock
it.tableBlock!!.frameWorks.forEach { fw ->
if (fw is FrameworkTable) {
entryStore.add(fw)
encodeMaterials.addFramework(fw)
}
}
}
}
}

View File

@@ -0,0 +1,56 @@
package app.revanced.arsc.xml
import app.revanced.arsc.resource.ResourceContainer
import app.revanced.arsc.resource.boolean
import com.reandroid.apk.xmlencoder.EncodeException
import com.reandroid.apk.xmlencoder.XMLEncodeSource
import com.reandroid.arsc.chunk.xml.ResXmlDocument
import com.reandroid.xml.XMLDocument
import com.reandroid.xml.XMLElement
import com.reandroid.xml.source.XMLDocumentSource
/**
* Archive input source to lazily encode an [XMLDocument] after it has been modified.
*
* @param name The file name of this input source.
* @param document The [XMLDocument] to encode.
* @param resources The [ResourceContainer] to use for encoding.
*/
internal class LazyXMLEncodeSource(
name: String,
val document: XMLDocument,
private val resources: ResourceContainer
) : XMLEncodeSource(resources.resourceTable.encodeMaterials, XMLDocumentSource(name, document)) {
private var encoded = false
override fun getResXmlBlock(): ResXmlDocument {
if (encoded) return super.getResXmlBlock()
XMLEncodeSource(resources.resourceTable.encodeMaterials, XMLDocumentSource(name, document))
fun XMLElement.registerIds() {
listAttributes().forEach { attr ->
if (!attr.value.startsWith("@+id/")) return@forEach
val name = attr.value.split('/').last()
resources.setResource("id", name, boolean(false))
attr.value = "@id/$name"
}
listChildElements().forEach { it.registerIds() }
}
// Handle all @+id/id_name references in the document.
document.documentElement.registerIds()
encoded = true
// This will call XMLEncodeSource.getResXmlBlock(),
// which will encode the document if it has not already been encoded.
try {
return super.getResXmlBlock()
} catch (e: EncodeException) {
throw EncodeException("Failed to encode $name", e)
}
}
}

View File

@@ -1,71 +1,3 @@
plugins {
kotlin("jvm") version "1.8.10"
`maven-publish`
}
group = "app.revanced"
val githubUsername: String = project.findProperty("gpr.user") as? String ?: System.getenv("GITHUB_ACTOR")
val githubPassword: String = project.findProperty("gpr.key") as? String ?: System.getenv("GITHUB_TOKEN")
repositories {
mavenCentral()
maven {
url = uri("https://maven.pkg.github.com/revanced/multidexlib2")
credentials {
username = githubUsername
password = githubPassword
}
}
}
dependencies {
implementation("xpp3:xpp3:1.1.4c")
implementation("app.revanced:smali:2.5.3-a3836654")
implementation("app.revanced:multidexlib2:2.5.3-a3836654")
implementation("app.revanced:apktool-lib:2.7.0")
implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.20-RC")
testImplementation("org.jetbrains.kotlin:kotlin-test:1.8.20-RC")
}
tasks {
test {
useJUnitPlatform()
testLogging {
events("PASSED", "SKIPPED", "FAILED")
}
}
processResources {
expand("projectVersion" to project.version)
}
}
java {
withSourcesJar()
}
kotlin {
jvmToolchain(11)
}
publishing {
repositories {
if (System.getenv("GITHUB_ACTOR") != null)
maven {
name = "GitHubPackages"
url = uri("https://maven.pkg.github.com/revanced/revanced-patcher")
credentials {
username = System.getenv("GITHUB_ACTOR")
password = System.getenv("GITHUB_TOKEN")
}
}
else
mavenLocal()
}
publications {
register<MavenPublication>("gpr") {
from(components["java"])
}
}
kotlin("jvm") version "1.8.20" apply false
}

View File

@@ -1,2 +1,4 @@
org.gradle.parallel=true
org.gradle.caching=true
kotlin.code.style = official
version = 11.0.0-dev.2
version = 11.0.4

Binary file not shown.

View File

@@ -1,5 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

40
gradlew vendored
View File

@@ -1,7 +1,7 @@
#!/bin/sh
#
# Copyright <EFBFBD> 2015-2021 the original authors.
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -32,10 +32,10 @@
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions <EFBFBD>$var<EFBFBD>, <EFBFBD>${var}<EFBFBD>, <EFBFBD>${var:-default}<EFBFBD>, <EFBFBD>${var+SET}<EFBFBD>,
# <EFBFBD>${var#prefix}<EFBFBD>, <EFBFBD>${var%suffix}<EFBFBD>, and <EFBFBD>$( cmd )<EFBFBD>;
# * compound commands having a testable exit status, especially <EFBFBD>case<EFBFBD>;
# * various built-in commands including <EFBFBD>command<EFBFBD>, <EFBFBD>set<EFBFBD>, and <EFBFBD>ulimit<EFBFBD>.
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
@@ -55,7 +55,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -80,13 +80,10 @@ do
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -133,22 +130,29 @@ location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@@ -193,6 +197,10 @@ if "$cygwin" || "$msys" ; then
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
@@ -205,6 +213,12 @@ set -- \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.

15
gradlew.bat vendored
View File

@@ -14,7 +14,7 @@
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@@ -25,7 +25,8 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal

View File

@@ -0,0 +1,61 @@
plugins {
kotlin("jvm")
`maven-publish`
}
group = "app.revanced"
dependencies {
implementation("xpp3:xpp3:1.1.4c")
implementation("app.revanced:smali:2.5.3-a3836654")
implementation("app.revanced:multidexlib2:2.5.3-a3836654")
implementation("io.github.reandroid:ARSCLib:1.1.7")
implementation(project(":arsclib-utils"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1")
implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.20-RC")
testImplementation("org.jetbrains.kotlin:kotlin-test:1.8.20-RC")
compileOnly("com.google.android:android:4.1.1.4")
}
tasks {
test {
useJUnitPlatform()
testLogging {
events("PASSED", "SKIPPED", "FAILED")
}
}
processResources {
expand("projectVersion" to project.version)
}
}
java {
withSourcesJar()
}
kotlin {
jvmToolchain(11)
}
publishing {
repositories {
if (System.getenv("GITHUB_ACTOR") != null)
maven {
name = "GitHubPackages"
url = uri("https://maven.pkg.github.com/revanced/revanced-patcher")
credentials {
username = System.getenv("GITHUB_ACTOR")
password = System.getenv("GITHUB_TOKEN")
}
}
else
mavenLocal()
}
publications {
register<MavenPublication>("gpr") {
from(components["java"])
}
}
}

View File

@@ -0,0 +1 @@
rootProject.name = "revanced-patcher"

View File

@@ -0,0 +1,113 @@
package app.revanced.patcher
import app.revanced.arsc.resource.ResourceContainer
import app.revanced.patcher.apk.Apk
import app.revanced.patcher.apk.ApkBundle
import app.revanced.arsc.resource.ResourceFiles
import app.revanced.patcher.util.method.MethodWalker
import org.jf.dexlib2.iface.Method
import org.w3c.dom.Document
import java.io.Closeable
import java.io.InputStream
import java.io.StringWriter
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult
/**
* A common class to constrain [Context] to [BytecodeContext] and [ResourceContext].
* @param apkBundle The [ApkBundle] for this context.
*/
sealed class Context(val apkBundle: ApkBundle)
/**
* A context for the bytecode of an [Apk.Base] file.
*
* @param apkBundle The [ApkBundle] for this context.
*/
class BytecodeContext internal constructor(apkBundle: ApkBundle) : Context(apkBundle) {
/**
* The list of classes.
*/
val classes = apkBundle.base.bytecodeData.classes
/**
* Create a [MethodWalker] instance for the current [BytecodeContext].
*
* @param startMethod The method to start at.
* @return A [MethodWalker] instance.
*/
fun traceMethodCalls(startMethod: Method) = MethodWalker(this, startMethod)
}
/**
* A context for [Apk] file resources.
*
* @param apkBundle the [ApkBundle] for this context.
*/
class ResourceContext internal constructor(apkBundle: ApkBundle) : Context(apkBundle) {
/**
* Open an [DomFileEditor] for a given DOM file.
*
* @param inputStream The input stream to read the DOM file from.
* @return A [DomFileEditor] instance.
*/
fun openXmlFile(inputStream: InputStream) = DomFileEditor(inputStream)
}
/**
* Open a [DomFileEditor] for a resource file in the archive.
*
* @see [ResourceContainer.openFile]
* @param path The resource file path.
* @return A [DomFileEditor].
*/
fun ResourceContainer.openXmlFile(path: String) = DomFileEditor(openFile(path))
/**
* Wrapper for a file that can be edited as a dom document.
*
* @param inputStream the input stream to read the xml file from.
* @param onSave A callback that will be called when the editor is closed to save the file.
*/
class DomFileEditor internal constructor(
private val inputStream: InputStream,
private val onSave: ((String) -> Unit)? = null
) : Closeable {
private var closed: Boolean = false
/**
* The document of the xml file.
*/
val file: Document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputStream)
.also(Document::normalize)
internal constructor(file: ResourceFiles) : this(
file.inputStream(),
{
file.contents = it.toByteArray()
file.close()
}
)
/**
* Closes the editor and writes back to the file.
*/
override fun close() {
if (closed) return
inputStream.close()
onSave?.let { callback ->
// Save the updated file.
val writer = StringWriter()
TransformerFactory.newInstance().newTransformer().transform(DOMSource(file), StreamResult(writer))
callback(writer.toString())
}
closed = true
}
}

View File

@@ -0,0 +1,222 @@
package app.revanced.patcher
import app.revanced.patcher.apk.Apk
import app.revanced.patcher.extensions.PatchExtensions.dependencies
import app.revanced.patcher.extensions.PatchExtensions.patchName
import app.revanced.patcher.extensions.PatchExtensions.requiresIntegrations
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint.Companion.resolveUsingLookupMap
import app.revanced.patcher.patch.*
import app.revanced.patcher.util.VersionReader
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import lanchon.multidexlib2.BasicDexFileNamer
import java.io.Closeable
import java.io.File
import java.util.function.Function
typealias ExecutedPatchResults = Flow<Pair<String, PatchException?>>
/**
* The ReVanced Patcher.
* @param options The options for the patcher.
* @param patches The patches to use.
* @param integrations The integrations to merge if necessary. Must be dex files or dex file container such as ZIP, APK or DEX files.
*/
class Patcher(private val options: PatcherOptions, patches: Iterable<PatchClass>, integrations: Iterable<File>) :
Function<Boolean, ExecutedPatchResults> {
private val context = PatcherContext(options, patches.toList(), integrations)
private val logger = options.logger
companion object {
/**
* The version of the ReVanced Patcher.
*/
@JvmStatic
val version = VersionReader.read()
@Suppress("SpellCheckingInspection")
internal val dexFileNamer = BasicDexFileNamer()
}
init {
/**
* Returns true if at least one patches or its dependencies matches the given predicate.
*/
fun PatchClass.anyRecursively(predicate: (PatchClass) -> Boolean): Boolean =
predicate(this) || dependencies?.any { it.java.anyRecursively(predicate) } == true
// Determine if merging integrations is required.
for (patch in context.patches) {
if (patch.anyRecursively { it.requiresIntegrations }) {
context.integrations.merge = true
break
}
}
}
/**
* Execute the patcher.
*
* @param stopOnError If true, the patches will stop on the first error.
* @return A pair of the name of the [Patch] and a [PatchException] if it failed.
*/
override fun apply(stopOnError: Boolean) = flow {
/**
* Execute a [Patch] and its dependencies recursively.
*
* @param patchClass The [Patch] to execute.
* @param executedPatches A map of [Patch]es paired to a boolean indicating their success, to prevent infinite recursion.
*/
suspend fun executePatch(
patchClass: PatchClass,
executedPatches: HashMap<String, ExecutedPatch>
) {
val patchName = patchClass.patchName
// If the patch has already executed silently skip it.
if (executedPatches.contains(patchName)) {
if (!executedPatches[patchName]!!.success)
throw PatchException("'$patchName' did not succeed previously")
logger.trace("Skipping '$patchName' because it has already been executed")
return
}
// Recursively execute all dependency patches.
patchClass.dependencies?.forEach { dependencyClass ->
val dependency = dependencyClass.java
try {
executePatch(dependency, executedPatches)
} catch (throwable: Throwable) {
throw PatchException(
"'$patchName' depends on '${dependency.patchName}' " +
"but the following exception was raised: ${throwable.cause?.stackTraceToString() ?: throwable.message}",
throwable
)
}
}
val isResourcePatch = ResourcePatch::class.java.isAssignableFrom(patchClass)
val patchInstance = patchClass.getDeclaredConstructor().newInstance()
// TODO: implement this in a more polymorphic way.
val patchContext = if (isResourcePatch) {
context.resourceContext
} else {
context.bytecodeContext.apply {
val bytecodePatch = patchInstance as BytecodePatch
bytecodePatch.fingerprints?.resolveUsingLookupMap(context.bytecodeContext)
}
}
logger.trace("Executing '$patchName' of type: ${if (isResourcePatch) "resource" else "bytecode"}")
var success = false
try {
patchInstance.execute(patchContext)
success = true
} catch (patchException: PatchException) {
throw patchException
} catch (throwable: Throwable) {
throw PatchException("Unhandled patch exception: ${throwable.message}", throwable)
} finally {
executedPatches[patchName] = ExecutedPatch(patchInstance, success)
}
}
if (context.integrations.merge) context.integrations.merge(logger)
logger.trace("Initialize lookup maps for method MethodFingerprint resolution")
MethodFingerprint.initializeFingerprintResolutionLookupMaps(context.bytecodeContext)
logger.info("Executing patches")
// Key is patch name.
LinkedHashMap<String, ExecutedPatch>().apply {
context.patches.forEach { patch ->
var exception: PatchException? = null
try {
executePatch(patch, this)
} catch (patchException: PatchException) {
exception = patchException
}
// TODO: only emit if the patch is not a closeable.
// If it is a closeable, this should be done when closing the patch.
emit(patch.patchName to exception)
if (stopOnError && exception != null) return@flow
}
}.let {
it.values
.filter(ExecutedPatch::success)
.map(ExecutedPatch::patchInstance)
.filterIsInstance(Closeable::class.java)
.asReversed().forEach { patch ->
try {
patch.close()
} catch (throwable: Throwable) {
val patchException =
if (throwable is PatchException) throwable
else PatchException(throwable)
val patchName = (patch as Patch<Context>).javaClass.patchName
logger.error("Failed to close '$patchName': ${patchException.stackTraceToString()}")
emit(patchName to patchException)
// This is not failsafe. If a patch throws an exception while closing,
// the other patches that depend on it may fail.
if (stopOnError) return@flow
}
}
}
MethodFingerprint.clearFingerprintResolutionLookupMaps()
}
/**
* Finish patching all [Apk]s.
*
* @return The [PatcherResult] of the [Patcher].
*/
fun finish(): PatcherResult {
val patchResults = buildList {
logger.info("Processing patched apks")
options.apkBundle.cleanup(options).forEach { result ->
if (result.exception != null) {
logger.error("Got exception while processing ${result.apk}: ${result.exception.stackTraceToString()}")
return@forEach
}
val patch = result.let {
when (it.apk) {
is Apk.Base -> PatcherResult.Patch.Base(it.apk)
is Apk.Split -> PatcherResult.Patch.Split(it.apk)
}
}
add(patch)
logger.info("Patched ${result.apk}")
}
}
return PatcherResult(patchResults)
}
}
/**
* A result of executing a [Patch].
*
* @param patchInstance The instance of the [Patch] that was executed.
* @param success The result of the [Patch].
*/
internal data class ExecutedPatch(val patchInstance: Patch<Context>, val success: Boolean)

View File

@@ -0,0 +1,55 @@
package app.revanced.patcher
import app.revanced.patcher.logging.Logger
import app.revanced.patcher.patch.PatchClass
import app.revanced.patcher.util.ClassMerger.merge
import lanchon.multidexlib2.MultiDexIO
import java.io.File
class PatcherContext(
options: PatcherOptions,
internal val patches: List<PatchClass>,
integrations: Iterable<File>
) {
internal val integrations = Integrations(this, integrations)
internal val bytecodeContext = BytecodeContext(options.apkBundle)
internal val resourceContext = ResourceContext(options.apkBundle)
internal class Integrations(val context: PatcherContext, private val dexContainers: Iterable<File>) {
var merge = false
/**
* Merge integrations.
* @param logger A logger.
*/
fun merge(logger: Logger) {
context.bytecodeContext.classes.apply {
for (integrations in dexContainers) {
logger.info("Merging $integrations")
for (classDef in MultiDexIO.readDexFile(true, integrations, Patcher.dexFileNamer, null, null).classes) {
val type = classDef.type
val existingClassIndex = this.indexOfFirst { it.type == type }
if (existingClassIndex == -1) {
logger.trace("Merging type $type")
add(classDef)
continue
}
logger.trace("Type $type exists. Adding missing methods and fields.")
get(existingClassIndex).apply {
merge(classDef, context.bytecodeContext, logger).let { mergedClass ->
if (mergedClass !== this) // referential equality check
set(existingClassIndex, mergedClass)
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,14 @@
package app.revanced.patcher
import app.revanced.patcher.apk.ApkBundle
import app.revanced.patcher.logging.Logger
/**
* Options for the [Patcher].
* @param apkBundle The [ApkBundle].
* @param logger Custom logger implementation for the [Patcher].
*/
class PatcherOptions(
internal val apkBundle: ApkBundle,
internal val logger: Logger = Logger.Nop
)

View File

@@ -0,0 +1,33 @@
package app.revanced.patcher
import app.revanced.patcher.apk.Apk
import java.io.File
/**
* The result of a patcher.
* @param apkFiles The patched [Apk] files.
*/
data class PatcherResult(val apkFiles: List<Patch>) {
/**
* The result of a patch.
*
* @param apk The patched [Apk] file.
*/
sealed class Patch(val apk: Apk) {
/**
* The result of a patch of an [Apk.Split] file.
*
* @param apk The patched [Apk.Split] file.
*/
class Split(apk: Apk.Split) : Patch(apk)
/**
* The result of a patch of an [Apk.Split] file.
*
* @param apk The patched [Apk.Base] file.
*/
class Base(apk: Apk.Base) : Patch(apk)
}
}

View File

@@ -26,6 +26,7 @@ annotation class Description(
* @param version The version of a [Patch].
*/
@Target(AnnotationTarget.CLASS)
@Deprecated("This annotation is deprecated and will be removed in the future.")
annotation class Version(
val version: String,
)

View File

@@ -0,0 +1,285 @@
@file:Suppress("MemberVisibilityCanBePrivate")
package app.revanced.patcher.apk
import app.revanced.arsc.ApkResourceException
import app.revanced.arsc.archive.Archive
import app.revanced.patcher.Patcher
import app.revanced.patcher.PatcherOptions
import app.revanced.patcher.logging.asArscLogger
import app.revanced.patcher.util.ProxyBackedClassList
import com.reandroid.apk.ApkModule
import com.reandroid.apk.xmlencoder.EncodeException
import com.reandroid.archive.InputSource
import com.reandroid.arsc.chunk.xml.AndroidManifestBlock
import com.reandroid.arsc.value.ResConfig
import lanchon.multidexlib2.*
import org.jf.dexlib2.Opcodes
import org.jf.dexlib2.dexbacked.DexBackedDexFile
import org.jf.dexlib2.iface.DexFile
import org.jf.dexlib2.iface.MultiDexContainer
import org.jf.dexlib2.writer.io.MemoryDataStore
import java.io.File
/**
* An [Apk] file.
*/
sealed class Apk private constructor(module: ApkModule) {
/**
* A wrapper around the zip archive of this [Apk].
*
* @see Archive
*/
private val archive = Archive(module)
/**
* The metadata of the [Apk].
*/
val packageMetadata = PackageMetadata(module.androidManifestBlock)
/**
* Refresh updated resources and close any open files.
*
* @param options The [PatcherOptions] of the [Patcher].
*/
internal open fun cleanup(options: PatcherOptions) {
try {
archive.cleanup(options.logger.asArscLogger())
} catch (e: EncodeException) {
throw ApkResourceException.Encode(e.message!!, e)
}
archive.mainPackageResources.refreshPackageName()
}
/**
* Write the [Apk] to a file.
*
* @param output The target file.
*/
fun write(output: File) = archive.save(output)
companion object {
const val MANIFEST_FILE_NAME = "AndroidManifest.xml"
/**
* Determine the [Module] and [Type] of an [ApkModule].
*
* @return A [Pair] containing the [Module] and [Type] of the [ApkModule].
*/
fun ApkModule.identify(): Pair<Module, Type> {
val manifestElement = androidManifestBlock.manifestElement
return when {
isBaseModule -> Module.Main to Type.Base
// The module is a base apk for a dynamic feature module if the "isFeatureModule" attribute is set to true.
manifestElement.searchAttributeByName("isFeatureModule")?.valueAsBoolean == true -> Module.DynamicFeature(
split
) to Type.Base
else -> {
val module = manifestElement.searchAttributeByName("configForSplit")
?.let { Module.DynamicFeature(it.valueAsString) } ?: Module.Main
// Examples:
// config.xhdpi
// df_my_feature.config.en
val config = this.split.split(".").last()
val type = when {
// Language splits have a two-letter country code.
config.length == 2 -> Type.Language(config)
// Library splits use the target CPU architecture.
Split.Library.architectures.contains(config) -> Type.Library(config)
// Asset splits use the density.
ResConfig.Density.valueOf(config) != null -> Type.Asset(config)
else -> throw IllegalArgumentException("Invalid split config: $config")
}
module to type
}
}
}
}
internal inner class BytecodeData {
private val opcodes: Opcodes
/**
* The classes and proxied classes of the [Base] apk file.
*/
val classes: ProxyBackedClassList
init {
MultiDexContainerBackedDexFile(object : MultiDexContainer<DexBackedDexFile> {
// Load all dex files from the apk module and create a dex entry for each of them.
private val entries = archive.readDexFiles().associateBy { it.name }
.mapValues { (name, inputSource) ->
BasicDexEntry(
this,
name,
RawDexIO.readRawDexFile(inputSource.openStream(), inputSource.length, null)
)
}
override fun getDexEntryNames() = entries.keys.toList()
override fun getEntry(entryName: String) = entries[entryName]
}).let {
opcodes = it.opcodes
classes = ProxyBackedClassList(it.classes)
}
}
/**
* Write [classes] to the archive.
*/
internal fun writeDexFiles() {
// Create patched dex files.
mutableMapOf<String, MemoryDataStore>().also {
val newDexFile = object : DexFile {
override fun getClasses() =
this@BytecodeData.classes.also(ProxyBackedClassList::applyProxies).toSet()
override fun getOpcodes() = this@BytecodeData.opcodes
}
// Write modified dex files.
MultiDexIO.writeDexFile(
true, -1, // Core count.
it, Patcher.dexFileNamer, newDexFile, DexIO.DEFAULT_MAX_DEX_POOL_SIZE, null
)
}.forEach { (name, store) ->
val dexFileInputSource = object : InputSource(name) {
override fun openStream() = store.readAt(0)
}
archive.write(dexFileInputSource)
}
}
}
/**
* Metadata about an [Apk] file.
*
* @param packageName The package name of the [Apk] file.
* @param packageVersion The package version of the [Apk] file.
*/
data class PackageMetadata(val packageName: String?, val packageVersion: String?) {
internal constructor(manifestBlock: AndroidManifestBlock) : this(
manifestBlock.packageName,
manifestBlock.versionName
)
}
/**
* An [Apk] of type [Split].
*
* @param config The device configuration associated with this [Split], such as arm64_v8a, en or xhdpi.
* @see Apk
*/
sealed class Split(val config: String, module: ApkModule) : Apk(module) {
override fun toString() = "split_config.$config.apk"
/**
* The split apk file which contains libraries.
*
* @see Split
*/
class Library internal constructor(config: String, module: ApkModule) : Split(config, module) {
companion object {
/**
* A set of all architectures supported by android.
*/
val architectures = setOf("armeabi_v7a", "arm64_v8a", "x86", "x86_64")
}
}
/**
* The split apk file which contains language strings.
*
* @see Split
*/
class Language internal constructor(config: String, module: ApkModule) : Split(config, module)
/**
* The split apk file which contains assets.
*
* @see Split
*/
class Asset internal constructor(config: String, module: ApkModule) : Split(config, module)
}
/**
* The base [Apk] file..
*
* @see Apk
*/
class Base internal constructor(module: ApkModule) : Apk(module) {
/**
* Data of the [Base] apk file.
*/
internal val bytecodeData = BytecodeData()
override fun toString() = "base.apk"
override fun cleanup(options: PatcherOptions) {
super.cleanup(options)
options.logger.info("Writing patched dex files")
bytecodeData.writeDexFiles()
}
}
/**
* The module that the [ApkModule] belongs to.
*/
sealed class Module {
/**
* The default [Module] that is always installed by software repositories.
*/
object Main : Module()
/**
* A [Module] that can be installed later by software repositories when requested by the application.
*
* @param name The name of the feature.
*/
data class DynamicFeature(val name: String) : Module()
}
/**
* The type of the [ApkModule].
*/
sealed class Type {
/**
* The main Apk of a [Module].
*/
object Base : Type()
/**
* A superclass for all split configuration types.
*
* @param target The target device configuration.
*/
sealed class SplitConfig(val target: String) : Type()
/**
* The [Type] of an apk containing native libraries.
*
* @param architecture The target CPU architecture.
*/
data class Library(val architecture: String) : SplitConfig(architecture)
/**
* The [Type] for an Apk containing language resources.
*
* @param language The target language code.
*/
data class Language(val language: String) : SplitConfig(language)
/**
* The [Type] for an Apk containing assets.
*
* @param pixelDensity The target screen density.
*/
data class Asset(val pixelDensity: String) : SplitConfig(pixelDensity)
}
}

View File

@@ -0,0 +1,105 @@
@file:Suppress("MemberVisibilityCanBePrivate")
package app.revanced.patcher.apk
import app.revanced.arsc.ApkResourceException
import app.revanced.arsc.resource.ResourceTable
import app.revanced.patcher.Patcher
import app.revanced.patcher.PatcherOptions
import app.revanced.patcher.apk.Apk.Companion.identify
import com.reandroid.apk.ApkModule
import java.io.File
/**
* An [Apk] file of type [Apk.Split].
*
* @param files A list of apk files to load.
*/
class ApkBundle(files: List<File>) : Sequence<Apk> {
/**
* The [Apk.Base] of this [ApkBundle].
*/
val base: Apk.Base
/**
* A map containing all the [Apk.Split]s in this bundle associated by their configuration.
*/
val splits: Map<String, Apk.Split>?
init {
var baseApk: Apk.Base? = null
splits = buildMap {
files.forEach {
val apk = ApkModule.loadApkFile(it)
val (module, type) = apk.identify()
if (module is Apk.Module.DynamicFeature) {
return@forEach // Dynamic feature modules are not supported yet.
}
when (type) {
Apk.Type.Base -> {
if (baseApk != null) {
throw IllegalArgumentException("Cannot have more than one base apk")
}
baseApk = Apk.Base(apk)
}
is Apk.Type.SplitConfig -> {
val target = type.target
if (this.contains(target)) {
throw IllegalArgumentException("Duplicate split: $target")
}
val constructor = when (type) {
is Apk.Type.Asset -> Apk.Split::Asset
is Apk.Type.Library -> Apk.Split::Library
is Apk.Type.Language -> Apk.Split::Language
}
this[target] = constructor(target, apk)
}
}
}
}.takeIf { it.isNotEmpty() }
base = baseApk ?: throw IllegalArgumentException("Base apk not found")
}
/**
* The [ResourceTable] of this [ApkBundle].
*/
val resources = ResourceTable(base.resources, map { it.resources })
override fun iterator() = sequence {
yield(base)
splits?.values?.let {
yieldAll(it)
}
}.iterator()
/**
* Refresh all updated resources in an [ApkBundle].
*
* @param options The [PatcherOptions] of the [Patcher].
* @return A sequence of the [Apk] files which are being refreshed.
*/
internal fun cleanup(options: PatcherOptions) = map {
var exception: ApkResourceException? = null
try {
it.cleanup(options)
} catch (e: ApkResourceException) {
exception = e
}
SplitApkResult(it, exception)
}
/**
* The result of writing an [Apk] file.
*
* @param apk The corresponding [Apk] file.
* @param exception The optional [ApkResourceException] when an exception occurred.
*/
data class SplitApkResult(val apk: Apk, val exception: ApkResourceException? = null)
}

View File

@@ -4,10 +4,10 @@ import app.revanced.patcher.annotation.Compatibility
import app.revanced.patcher.annotation.Description
import app.revanced.patcher.annotation.Name
import app.revanced.patcher.annotation.Version
import app.revanced.patcher.data.Context
import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively
import app.revanced.patcher.patch.OptionsContainer
import app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.PatchClass
import app.revanced.patcher.patch.PatchOptions
import app.revanced.patcher.patch.annotations.DependsOn
import app.revanced.patcher.patch.annotations.RequiresIntegrations
@@ -19,49 +19,50 @@ object PatchExtensions {
/**
* The name of a [Patch].
*/
val Class<out Patch<Context>>.patchName: String
val PatchClass.patchName: String
get() = findAnnotationRecursively(Name::class)?.name ?: this.simpleName
/**
* The version of a [Patch].
*/
val Class<out Patch<Context>>.version
@Deprecated("This property is deprecated and will be removed in the future.")
val PatchClass.version
get() = findAnnotationRecursively(Version::class)?.version
/**
* Weather or not a [Patch] should be included.
*/
val Class<out Patch<Context>>.include
val PatchClass.include
get() = findAnnotationRecursively(app.revanced.patcher.patch.annotations.Patch::class)!!.include
/**
* The description of a [Patch].
*/
val Class<out Patch<Context>>.description
val PatchClass.description
get() = findAnnotationRecursively(Description::class)?.description
/**
* The dependencies of a [Patch].
*/
val Class<out Patch<Context>>.dependencies
val PatchClass.dependencies
get() = findAnnotationRecursively(DependsOn::class)?.dependencies
/**
* The packages a [Patch] is compatible with.
*/
val Class<out Patch<Context>>.compatiblePackages
val PatchClass.compatiblePackages
get() = findAnnotationRecursively(Compatibility::class)?.compatiblePackages
/**
* Weather or not a [Patch] requires integrations.
*/
internal val Class<out Patch<Context>>.requiresIntegrations
internal val PatchClass.requiresIntegrations
get() = findAnnotationRecursively(RequiresIntegrations::class) != null
/**
* The options of a [Patch].
*/
val Class<out Patch<Context>>.options: PatchOptions?
val PatchClass.options: PatchOptions?
get() = kotlin.companionObject?.let { cl ->
if (cl.visibility != KVisibility.PUBLIC) return null
kotlin.companionObjectInstance?.let {

View File

@@ -0,0 +1,513 @@
package app.revanced.patcher.fingerprint.method.impl
import app.revanced.patcher.BytecodeContext
import app.revanced.patcher.extensions.MethodFingerprintExtensions.fuzzyPatternScanMethod
import app.revanced.patcher.fingerprint.Fingerprint
import app.revanced.patcher.fingerprint.method.annotation.FuzzyPatternScanMethod
import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.util.proxy.ClassProxy
import org.jf.dexlib2.AccessFlags
import org.jf.dexlib2.Opcode
import org.jf.dexlib2.iface.ClassDef
import org.jf.dexlib2.iface.Method
import org.jf.dexlib2.iface.instruction.Instruction
import org.jf.dexlib2.iface.instruction.ReferenceInstruction
import org.jf.dexlib2.iface.reference.StringReference
import org.jf.dexlib2.util.MethodUtil
import java.util.*
private typealias StringMatch = MethodFingerprintResult.MethodFingerprintScanResult.StringsScanResult.StringMatch
private typealias StringsScanResult = MethodFingerprintResult.MethodFingerprintScanResult.StringsScanResult
private typealias MethodClassPair = Pair<Method, ClassDef>
/**
* A fingerprint to resolve methods.
*
* @param returnType The method's return type compared using [String.startsWith].
* @param accessFlags The method's exact access flags using values of [AccessFlags].
* @param parameters The parameters of the method. Partial matches allowed and follow the same rules as [returnType].
* @param opcodes An opcode pattern of the method's instructions. Wildcard or unknown opcodes can be specified by `null`.
* @param strings A list of the method's strings compared each using [String.contains].
* @param customFingerprint A custom condition for this fingerprint.
*/
abstract class MethodFingerprint(
internal val returnType: String? = null,
internal val accessFlags: Int? = null,
internal val parameters: Iterable<String>? = null,
internal val opcodes: Iterable<Opcode?>? = null,
internal val strings: Iterable<String>? = null,
internal val customFingerprint: ((methodDef: Method, classDef: ClassDef) -> Boolean)? = null
) : Fingerprint {
/**
* The result of the [MethodFingerprint].
*/
var result: MethodFingerprintResult? = null
companion object {
/**
* A list of methods and the class they were found in.
*/
private val methods = mutableListOf<MethodClassPair>()
/**
* Lookup map for methods keyed to the methods access flags, return type and parameter.
*/
private val methodSignatureLookupMap = mutableMapOf<String, MutableList<MethodClassPair>>()
/**
* Lookup map for methods keyed to the strings contained in the method.
*/
private val methodStringsLookupMap = mutableMapOf<String, MutableList<MethodClassPair>>()
/**
* Appends a string based on the parameter reference types of this method.
*/
private fun StringBuilder.appendParameters(parameters: Iterable<CharSequence>) {
// Maximum parameters to use in the signature key.
// Some apps have methods with an incredible number of parameters (over 100 parameters have been seen).
// To keep the signature map from becoming needlessly bloated,
// group together in the same map entry all methods with the same access/return and 5 or more parameters.
// The value of 5 was chosen based on local performance testing and is not set in stone.
val maxSignatureParameters = 5
// Must append a unique value before the parameters to distinguish this key includes the parameters.
// If this is not appended, then methods with no parameters
// will collide with different keys that specify access/return but omit the parameters.
append("p:")
parameters.forEachIndexed { index, parameter ->
if (index >= maxSignatureParameters) return
append(parameter.first())
}
}
/**
* Initializes lookup maps for [MethodFingerprint] resolution
* using attributes of methods such as the method signature or strings.
*
* @param context The [BytecodeContext] containing the classes to initialize the lookup maps with.
*/
internal fun initializeFingerprintResolutionLookupMaps(context: BytecodeContext) {
fun MutableMap<String, MutableList<MethodClassPair>>.add(
key: String,
methodClassPair: MethodClassPair
) {
var methodClassPairs = this[key]
methodClassPairs ?: run {
methodClassPairs = LinkedList<MethodClassPair>().also { this[key] = it }
}
methodClassPairs!!.add(methodClassPair)
}
if (methods.isNotEmpty()) throw PatchException("Map already initialized")
context.classes.forEach { classDef ->
classDef.methods.forEach { method ->
val methodClassPair = method to classDef
// For fingerprints with no access or return type specified.
methods += methodClassPair
val accessFlagsReturnKey = method.accessFlags.toString() + method.returnType.first()
// Add <access><returnType> as the key.
methodSignatureLookupMap.add(accessFlagsReturnKey, methodClassPair)
// Add <access><returnType>[parameters] as the key.
methodSignatureLookupMap.add(
buildString {
append(accessFlagsReturnKey)
appendParameters(method.parameterTypes)
},
methodClassPair
)
// Add strings contained in the method as the key.
method.implementation?.instructions?.forEach instructions@{ instruction ->
if (instruction.opcode != Opcode.CONST_STRING && instruction.opcode != Opcode.CONST_STRING_JUMBO)
return@instructions
val string = ((instruction as ReferenceInstruction).reference as StringReference).string
methodStringsLookupMap.add(string, methodClassPair)
}
// In the future, the class type could be added to the lookup map.
// This would require MethodFingerprint to be changed to include the class type.
}
}
}
/**
* Clears the internal lookup maps created in [initializeFingerprintResolutionLookupMaps]
*/
internal fun clearFingerprintResolutionLookupMaps() {
methods.clear()
methodSignatureLookupMap.clear()
methodStringsLookupMap.clear()
}
/**
* Resolve a list of [MethodFingerprint] using the lookup map built by [initializeFingerprintResolutionLookupMaps].
*
* [MethodFingerprint] resolution is fast, but if many are present they can consume a noticeable
* amount of time because they are resolved in sequence.
*
* For apps with many fingerprints, resolving performance can be improved by:
* - Slowest: Specify [opcodes] and nothing else.
* - Fast: Specify [accessFlags], [returnType].
* - Faster: Specify [accessFlags], [returnType] and [parameters].
* - Fastest: Specify [strings], with at least one string being an exact (non-partial) match.
*/
internal fun Iterable<MethodFingerprint>.resolveUsingLookupMap(context: BytecodeContext) {
if (methods.isEmpty()) throw PatchException("lookup map not initialized")
for (fingerprint in this) {
fingerprint.resolveUsingLookupMap(context)
}
}
/**
* Resolve a [MethodFingerprint] using the lookup map built by [initializeFingerprintResolutionLookupMaps].
*
* [MethodFingerprint] resolution is fast, but if many are present they can consume a noticeable
* amount of time because they are resolved in sequence.
*
* For apps with many fingerprints, resolving performance can be improved by:
* - Slowest: Specify [opcodes] and nothing else.
* - Fast: Specify [accessFlags], [returnType].
* - Faster: Specify [accessFlags], [returnType] and [parameters].
* - Fastest: Specify [strings], with at least one string being an exact (non-partial) match.
*/
internal fun MethodFingerprint.resolveUsingLookupMap(context: BytecodeContext): Boolean {
/**
* Lookup [MethodClassPair]s that match the methods strings present in a [MethodFingerprint].
*
* @return A list of [MethodClassPair]s that match the methods strings present in a [MethodFingerprint].
*/
fun MethodFingerprint.methodStringsLookup(): List<MethodClassPair>? {
strings?.forEach {
val methods = methodStringsLookupMap[it]
if (methods != null) return methods
}
return null
}
/**
* Lookup [MethodClassPair]s that match the method signature present in a [MethodFingerprint].
*
* @return A list of [MethodClassPair]s that match the method signature present in a [MethodFingerprint].
*/
fun MethodFingerprint.methodSignatureLookup(): List<MethodClassPair> {
if (accessFlags == null) return methods
var returnTypeValue = returnType
if (returnTypeValue == null) {
if (AccessFlags.CONSTRUCTOR.isSet(accessFlags)) {
// Constructors always have void return type
returnTypeValue = "V"
} else {
return methods
}
}
val key = buildString {
append(accessFlags)
append(returnTypeValue.first())
if (parameters != null) appendParameters(parameters)
}
return methodSignatureLookupMap[key] ?: return emptyList()
}
/**
* Resolve a [MethodFingerprint] using a list of [MethodClassPair].
*
* @return True if the resolution was successful, false otherwise.
*/
fun MethodFingerprint.resolveUsingMethodClassPair(classMethods: Iterable<MethodClassPair>): Boolean {
classMethods.forEach { classAndMethod ->
if (resolve(context, classAndMethod.first, classAndMethod.second)) return true
}
return false
}
val methodsWithSameStrings = methodStringsLookup()
if (methodsWithSameStrings != null) if (resolveUsingMethodClassPair(methodsWithSameStrings)) return true
// No strings declared or none matched (partial matches are allowed).
// Use signature matching.
return resolveUsingMethodClassPair(methodSignatureLookup())
}
/**
* Resolve a list of [MethodFingerprint] against a list of [ClassDef].
*
* @param classes The classes on which to resolve the [MethodFingerprint] in.
* @param context The [BytecodeContext] to host proxies.
* @return True if the resolution was successful, false otherwise.
*/
fun Iterable<MethodFingerprint>.resolve(context: BytecodeContext, classes: Iterable<ClassDef>) {
for (fingerprint in this) // For each fingerprint...
classes@ for (classDef in classes) // ...search through all classes for the MethodFingerprint
if (fingerprint.resolve(context, classDef))
break@classes // ...if the resolution succeeded, continue with the next MethodFingerprint.
}
/**
* Resolve a [MethodFingerprint] against a [ClassDef].
*
* @param forClass The class on which to resolve the [MethodFingerprint] in.
* @param context The [BytecodeContext] to host proxies.
* @return True if the resolution was successful, false otherwise.
*/
fun MethodFingerprint.resolve(context: BytecodeContext, forClass: ClassDef): Boolean {
for (method in forClass.methods)
if (this.resolve(context, method, forClass))
return true
return false
}
/**
* Resolve a [MethodFingerprint] against a [Method].
*
* @param method The class on which to resolve the [MethodFingerprint] in.
* @param forClass The class on which to resolve the [MethodFingerprint].
* @param context The [BytecodeContext] to host proxies.
* @return True if the resolution was successful or if the fingerprint is already resolved, false otherwise.
*/
fun MethodFingerprint.resolve(context: BytecodeContext, method: Method, forClass: ClassDef): Boolean {
val methodFingerprint = this
if (methodFingerprint.result != null) return true
if (methodFingerprint.returnType != null && !method.returnType.startsWith(methodFingerprint.returnType))
return false
if (methodFingerprint.accessFlags != null && methodFingerprint.accessFlags != method.accessFlags)
return false
fun parametersEqual(
parameters1: Iterable<CharSequence>, parameters2: Iterable<CharSequence>
): Boolean {
if (parameters1.count() != parameters2.count()) return false
val iterator1 = parameters1.iterator()
parameters2.forEach {
if (!it.startsWith(iterator1.next())) return false
}
return true
}
if (methodFingerprint.parameters != null && !parametersEqual(
methodFingerprint.parameters, // TODO: parseParameters()
method.parameterTypes
)
) return false
@Suppress("UNNECESSARY_NOT_NULL_ASSERTION")
if (methodFingerprint.customFingerprint != null && !methodFingerprint.customFingerprint!!(method, forClass))
return false
val stringsScanResult: StringsScanResult? =
if (methodFingerprint.strings != null) {
StringsScanResult(
buildList {
val implementation = method.implementation ?: return false
val stringsList = methodFingerprint.strings.toMutableList()
implementation.instructions.forEachIndexed { instructionIndex, instruction ->
if (
instruction.opcode != Opcode.CONST_STRING &&
instruction.opcode != Opcode.CONST_STRING_JUMBO
) return@forEachIndexed
val string = ((instruction as ReferenceInstruction).reference as StringReference).string
val index = stringsList.indexOfFirst(string::contains)
if (index == -1) return@forEachIndexed
add(
StringMatch(
string,
instructionIndex
)
)
stringsList.removeAt(index)
}
if (stringsList.isNotEmpty()) return false
}
)
} else null
val patternScanResult = if (methodFingerprint.opcodes != null) {
method.implementation?.instructions ?: return false
fun Method.patternScan(
fingerprint: MethodFingerprint
): MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult? {
val instructions = this.implementation!!.instructions
val fingerprintFuzzyPatternScanThreshold = fingerprint.fuzzyPatternScanMethod?.threshold ?: 0
val pattern = fingerprint.opcodes!!
val instructionLength = instructions.count()
val patternLength = pattern.count()
for (index in 0 until instructionLength) {
var patternIndex = 0
var threshold = fingerprintFuzzyPatternScanThreshold
while (index + patternIndex < instructionLength) {
val originalOpcode = instructions.elementAt(index + patternIndex).opcode
val patternOpcode = pattern.elementAt(patternIndex)
if (patternOpcode != null && patternOpcode.ordinal != originalOpcode.ordinal) {
// Reaching maximum threshold (0) means,
// the pattern does not match to the current instructions.
if (threshold-- == 0) break
}
if (patternIndex < patternLength - 1) {
// If the entire pattern has not been scanned yet
// continue the scan.
patternIndex++
continue
}
// The pattern is valid, generate warnings if fuzzyPatternScanMethod is FuzzyPatternScanMethod
val result =
MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult(
index,
index + patternIndex
)
if (fingerprint.fuzzyPatternScanMethod !is FuzzyPatternScanMethod) return result
result.warnings = result.createWarnings(pattern, instructions)
return result
}
}
return null
}
method.patternScan(methodFingerprint) ?: return false
} else null
methodFingerprint.result = MethodFingerprintResult(
method,
forClass,
MethodFingerprintResult.MethodFingerprintScanResult(
patternScanResult,
stringsScanResult
),
context
)
return true
}
private fun MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult.createWarnings(
pattern: Iterable<Opcode?>, instructions: Iterable<Instruction>
) = buildList {
for ((patternIndex, instructionIndex) in (this@createWarnings.startIndex until this@createWarnings.endIndex).withIndex()) {
val originalOpcode = instructions.elementAt(instructionIndex).opcode
val patternOpcode = pattern.elementAt(patternIndex)
if (patternOpcode == null || patternOpcode.ordinal == originalOpcode.ordinal) continue
this.add(
MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult.Warning(
originalOpcode,
patternOpcode,
instructionIndex,
patternIndex
)
)
}
}
}
}
/**
* Represents the result of a [MethodFingerprintResult].
*
* @param method The matching method.
* @param classDef The [ClassDef] that contains the matching [method].
* @param scanResult The result of scanning for the [MethodFingerprint].
* @param context The [BytecodeContext] this [MethodFingerprintResult] is attached to, to create proxies.
*/
data class MethodFingerprintResult(
val method: Method,
val classDef: ClassDef,
val scanResult: MethodFingerprintScanResult,
internal val context: BytecodeContext
) {
/**
* Returns a mutable clone of [classDef]
*
* Please note, this method allocates a [ClassProxy].
* Use [classDef] where possible.
*/
@Suppress("MemberVisibilityCanBePrivate")
val mutableClass by lazy { context.classes.proxy(classDef).mutableClass }
/**
* Returns a mutable clone of [method]
*
* Please note, this method allocates a [ClassProxy].
* Use [method] where possible.
*/
val mutableMethod by lazy {
mutableClass.methods.first {
MethodUtil.methodSignaturesMatch(it, this.method)
}
}
/**
* The result of scanning on the [MethodFingerprint].
* @param patternScanResult The result of the pattern scan.
* @param stringsScanResult The result of the string scan.
*/
data class MethodFingerprintScanResult(
val patternScanResult: PatternScanResult?,
val stringsScanResult: StringsScanResult?
) {
/**
* The result of scanning strings on the [MethodFingerprint].
* @param matches The list of strings that were matched.
*/
data class StringsScanResult(val matches: List<StringMatch>) {
/**
* Represents a match for a string at an index.
* @param string The string that was matched.
* @param index The index of the string.
*/
data class StringMatch(val string: String, val index: Int)
}
/**
* The result of a pattern scan.
* @param startIndex The start index of the instructions where to which this pattern matches.
* @param endIndex The end index of the instructions where to which this pattern matches.
* @param warnings A list of warnings considering this [PatternScanResult].
*/
data class PatternScanResult(
val startIndex: Int,
val endIndex: Int,
var warnings: List<Warning>? = null
) {
/**
* Represents warnings of the pattern scan.
* @param correctOpcode The opcode the instruction list has.
* @param wrongOpcode The opcode the pattern list of the signature currently has.
* @param instructionIndex The index of the opcode relative to the instruction list.
* @param patternIndex The index of the opcode relative to the pattern list from the signature.
*/
data class Warning(
val correctOpcode: Opcode,
val wrongOpcode: Opcode,
val instructionIndex: Int,
val patternIndex: Int,
)
}
}
}

View File

@@ -0,0 +1,20 @@
package app.revanced.patcher.logging
interface Logger {
fun error(msg: String) {}
fun warn(msg: String) {}
fun info(msg: String) {}
fun trace(msg: String) {}
object Nop : Logger
}
/**
* Turn a Patcher [Logger] into an [app.revanced.arsc.logging.Logger].
*/
internal fun Logger.asArscLogger() = object : app.revanced.arsc.logging.Logger {
override fun error(msg: String) = this@asArscLogger.error(msg)
override fun warn(msg: String) = this@asArscLogger.warn(msg)
override fun info(msg: String) = this@asArscLogger.info(msg)
override fun trace(msg: String) = this@asArscLogger.error(msg)
}

View File

@@ -1,8 +1,8 @@
package app.revanced.patcher.patch
import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.data.Context
import app.revanced.patcher.data.ResourceContext
import app.revanced.patcher.BytecodeContext
import app.revanced.patcher.Context
import app.revanced.patcher.ResourceContext
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
import java.io.Closeable
@@ -12,21 +12,13 @@ import java.io.Closeable
* If it implements [Closeable], it will be closed after all patches have been executed.
* Closing will be done in reverse execution order.
*/
sealed interface Patch<out T : Context> : Closeable {
sealed interface Patch<out T : Context> {
/**
* The main function of the [Patch] which the patcher will call.
*
* @param context The [Context] the patch will work on.
* @return The result of executing the patch.
*/
fun execute(context: @UnsafeVariance T): PatchResult
/**
* The closing function for this patch.
*
* This can be treated like popping the patch from the current patch stack.
*/
override fun close() {}
suspend fun execute(context: @UnsafeVariance T)
}
/**
@@ -41,4 +33,10 @@ interface ResourcePatch : Patch<ResourceContext>
*/
abstract class BytecodePatch(
internal val fingerprints: Iterable<MethodFingerprint>? = null
) : Patch<BytecodeContext>
) : Patch<BytecodeContext>
// TODO: populate this everywhere where the alias is not used yet
/**
* The class type of [Patch].
*/
typealias PatchClass = Class<out Patch<Context>>

View File

@@ -0,0 +1,12 @@
package app.revanced.patcher.patch
/**
* An exception thrown when patching.
*
* @param errorMessage The exception message.
* @param cause The corresponding [Throwable].
*/
class PatchException(errorMessage: String?, cause: Throwable?) : Exception(errorMessage, cause) {
constructor(errorMessage: String) : this(errorMessage, null)
constructor(cause: Throwable) : this(cause.message, cause)
}

View File

@@ -2,8 +2,6 @@
package app.revanced.patcher.patch
import java.nio.file.Path
import kotlin.io.path.pathString
import kotlin.reflect.KProperty
class NoSuchOptionException(val option: String) : Exception("No such option: $option")
@@ -229,20 +227,4 @@ sealed class PatchOption<T>(
) : ListOption<Int>(
key, default, options, title, description, required, validator
)
/**
* A [PatchOption] representing a [Path], backed by a [String].
* The validator passes a [String], if you need a [Path] you will have to convert it yourself.
* @see PatchOption
*/
class PathOption(
key: String,
default: Path?,
title: String,
description: String,
required: Boolean = false,
validator: (String?) -> Boolean = { true }
) : PatchOption<String>(
key, default?.pathString, title, description, required, validator
)
}

View File

@@ -1,6 +1,6 @@
package app.revanced.patcher.patch.annotations
import app.revanced.patcher.data.Context
import app.revanced.patcher.Context
import app.revanced.patcher.patch.Patch
import kotlin.reflect.KClass
@@ -16,7 +16,7 @@ annotation class Patch(val include: Boolean = true)
*/
@Target(AnnotationTarget.CLASS)
annotation class DependsOn(
val dependencies: Array<KClass<out Patch<Context>>> = []
val dependencies: Array<KClass<out Patch<Context>>> = [] // TODO: This should be a list of PatchClass instead
)
@@ -24,4 +24,4 @@ annotation class DependsOn(
* Annotation to mark [Patch]es which depend on integrations.
*/
@Target(AnnotationTarget.CLASS)
annotation class RequiresIntegrations // required because integrations are decoupled from patches
annotation class RequiresIntegrations // TODO: Remove this annotation and replace it with a proper system

View File

@@ -1,6 +1,6 @@
package app.revanced.patcher.util
import app.revanced.patcher.PatcherContext
import app.revanced.patcher.BytecodeContext
import app.revanced.patcher.extensions.or
import app.revanced.patcher.logging.Logger
import app.revanced.patcher.util.ClassMerger.Utils.asMutableClass
@@ -32,7 +32,7 @@ internal object ClassMerger {
* @param context The context to traverse the class hierarchy in.
* @param logger A logger.
*/
fun ClassDef.merge(otherClass: ClassDef, context: PatcherContext, logger: Logger? = null) = this
fun ClassDef.merge(otherClass: ClassDef, context: BytecodeContext, logger: Logger? = null) = this
//.fixFieldAccess(otherClass, logger)
//.fixMethodAccess(otherClass, logger)
.addMissingFields(otherClass, logger)
@@ -89,10 +89,10 @@ internal object ClassMerger {
* @param context The context to traverse the class hierarchy in.
* @param logger A logger.
*/
private fun ClassDef.publicize(reference: ClassDef, context: PatcherContext, logger: Logger? = null) =
private fun ClassDef.publicize(reference: ClassDef, context: BytecodeContext, logger: Logger? = null) =
if (reference.accessFlags.isPublic() && !accessFlags.isPublic())
this.asMutableClass().apply {
context.bytecodeContext.traverseClassHierarchy(this) {
context.traverseClassHierarchy(this) {
if (accessFlags.isPublic()) return@traverseClassHierarchy
logger?.trace("Publicizing ${this.type}")

View File

@@ -0,0 +1,89 @@
package app.revanced.patcher.util
import app.revanced.patcher.util.proxy.ClassProxy
import org.jf.dexlib2.iface.ClassDef
/**
* A class that represents a list of classes and proxies.
*
* @param classes The classes to be backed by proxies.
*/
class ProxyBackedClassList(classes: Set<ClassDef>) : Iterable<ClassDef> {
// A list for pending proxied classes to be added to the current ProxyBackedClassList instance.
private val proxiedClasses = mutableListOf<ClassProxy>()
private val mutableClasses = classes.toMutableList()
/**
* Replace the [mutableClasses]es with their proxies.
*/
internal fun applyProxies() {
proxiedClasses.removeIf { proxy ->
// If the proxy is unused, keep it in the proxiedClasses list.
if (!proxy.resolved) return@removeIf false
with(mutableClasses) {
remove(proxy.immutableClass)
add(proxy.mutableClass)
}
return@removeIf true
}
}
/**
* Replace a [ClassDef] at a given [index].
*
* @param index The index of the class to be replaced.
* @param classDef The new class to replace the old one.
*/
operator fun set(index: Int, classDef: ClassDef) {
mutableClasses[index] = classDef
}
/**
* Get a [ClassDef] at a given [index].
*
* @param index The index of the class.
*/
operator fun get(index: Int) = mutableClasses[index]
/**
* Iterator for the classes in [ProxyBackedClassList].
*
* @return The iterator for the classes.
*/
override fun iterator() = mutableClasses.iterator()
/**
* Proxy a [ClassDef].
*
* Note: This creates a [ClassProxy] of the [ClassDef], if not already present.
*
* @return A proxy for the given class.
*/
fun proxy(classDef: ClassDef) = proxiedClasses
.find { it.immutableClass.type == classDef.type } ?: ClassProxy(classDef).also(proxiedClasses::add)
/**
* Add a [ClassDef].
*/
fun add(classDef: ClassDef) = mutableClasses.add(classDef)
/**
* Find a class by a given class name.
*
* @param className The name of the class.
* @return A proxy for the first class that matches the class name.
*/
fun findClassProxied(className: String) = findClassProxied { it.type.contains(className) }
/**
* Find a class by a given predicate.
*
* @param predicate A predicate to match the class.
* @return A proxy for the first class that matches the predicate.
*/
fun findClassProxied(predicate: (ClassDef) -> Boolean) = this.find(predicate)?.let(::proxy)
val size get() = mutableClasses.size
}

View File

@@ -0,0 +1,19 @@
package app.revanced.patcher.util
import app.revanced.patcher.BytecodeContext
import app.revanced.patcher.util.proxy.mutableTypes.MutableClass
object TypeUtil {
/**
* Traverse the class hierarchy starting from the given root class.
*
* @param targetClass The class to start traversing the class hierarchy from.
* @param callback The function that is called for every class in the hierarchy.
*/
fun BytecodeContext.traverseClassHierarchy(targetClass: MutableClass, callback: MutableClass.() -> Unit) {
callback(targetClass)
this.classes.findClassProxied(targetClass.superclass ?: return)?.mutableClass?.let {
traverseClassHierarchy(it, callback)
}
}
}

View File

@@ -0,0 +1,19 @@
package app.revanced.patcher.util
import java.util.*
@Deprecated("This class serves no purpose anymore")
internal object VersionReader {
@JvmStatic
private val properties = Properties().apply {
load(
VersionReader::class.java.getResourceAsStream("/app/revanced/patcher/version.properties")
?: throw IllegalStateException("Could not load version.properties")
)
}
@JvmStatic
fun read(): String {
return properties.getProperty("version") ?: throw IllegalStateException("Version not found")
}
}

View File

@@ -1,6 +1,6 @@
package app.revanced.patcher.util.method
import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.BytecodeContext
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import org.jf.dexlib2.iface.Method
import org.jf.dexlib2.iface.instruction.ReferenceInstruction
@@ -41,7 +41,7 @@ class MethodWalker internal constructor(
val instruction = instructions.elementAt(offset)
val newMethod = (instruction as ReferenceInstruction).reference as MethodReference
val proxy = bytecodeContext.findClass(newMethod.definingClass)!!
val proxy = bytecodeContext.classes.findClassProxied(newMethod.definingClass)!!
val methods = if (walkMutable) proxy.mutableClass.methods else proxy.immutableClass.methods
currentMethod = methods.first {

View File

@@ -0,0 +1,59 @@
@file:Suppress("unused")
package app.revanced.patcher.util.patch
import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively
import app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.PatchClass
import dalvik.system.PathClassLoader
import org.jf.dexlib2.DexFileFactory
import java.io.File
import java.net.URLClassLoader
import java.util.jar.JarFile
import kotlin.streams.toList
/**
* A patch bundle.
*
* @param fromClasses The classes to get [Patch]es from.
*/
sealed class PatchBundle private constructor(fromClasses: Iterable<Class<*>>) : Iterable<PatchClass> {
private val patches = fromClasses.filter {
if (it.isAnnotation) return@filter false
it.findAnnotationRecursively(app.revanced.patcher.patch.annotations.Patch::class) != null
}.map {
@Suppress("UNCHECKED_CAST")
it as PatchClass
}
override fun iterator() = patches.iterator()
/**
* A patch bundle of type [Jar].
*
* @param patchBundlePath The path to a patch bundle.
*/
class Jar(private val patchBundlePath: File) : PatchBundle(
with(URLClassLoader(arrayOf(patchBundlePath.toURI().toURL()), PatchBundle::class.java.classLoader)) {
JarFile(patchBundlePath).stream().filter { it.name.endsWith(".class") }.map {
loadClass(
it.realName.replace('/', '.').replace(".class", "")
)
}.toList()
}
)
/**
* A patch bundle of type [Dex] format.
*
* @param patchBundlePath The path to a patch bundle of dex format.
*/
class Dex(private val patchBundlePath: File) : PatchBundle(
with(PathClassLoader(patchBundlePath.absolutePath, null, PatchBundle::class.java.classLoader)) {
DexFileFactory.loadDexFile(patchBundlePath, null).classes.map { classDef ->
classDef.type.substring(1, classDef.length - 1).replace('/', '.')
}.map { loadClass(it) }
}
)
}

View File

@@ -13,7 +13,7 @@ class MutableMethod(method: Method) : Method, BaseMethodReference() {
private var accessFlags = method.accessFlags
private var returnType = method.returnType
// Create own mutable MethodImplementation (due to not being able to change members like register count)
// TODO: Create own mutable MethodImplementation (due to not being able to change members like register count).
private val _implementation by lazy { method.implementation?.let { MutableMethodImplementation(it) } }
private val _annotations by lazy { method.annotations.map { annotation -> annotation.toMutable() }.toMutableSet() }
private val _parameters by lazy { method.parameters.map { parameter -> parameter.toMutable() }.toMutableList() }

View File

@@ -4,7 +4,7 @@ import app.revanced.patcher.util.proxy.mutableTypes.MutableAnnotation.Companion.
import org.jf.dexlib2.base.BaseMethodParameter
import org.jf.dexlib2.iface.MethodParameter
// TODO: finish overriding all members if necessary
// TODO: Finish overriding all members if necessary.
class MutableMethodParameter(parameter: MethodParameter) : MethodParameter, BaseMethodParameter() {
private var type = parameter.type
private var name = parameter.name

View File

@@ -3,8 +3,6 @@ package app.revanced.patcher.patch
import app.revanced.patcher.usage.bytecode.ExampleBytecodePatch
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import kotlin.io.path.Path
import kotlin.io.path.pathString
import kotlin.test.assertNotEquals
internal class PatchOptionsTest {
@@ -35,10 +33,6 @@ internal class PatchOptionsTest {
println(choice)
}
}
is PatchOption.PathOption -> {
option.value = Path("test.txt").pathString
}
}
}
val option = options.get<String>("key1")

View File

@@ -1,13 +1,15 @@
package app.revanced.patcher.usage.bytecode
import app.revanced.patcher.BytecodeContext
import app.revanced.patcher.annotation.Description
import app.revanced.patcher.annotation.Name
import app.revanced.patcher.annotation.Version
import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
import app.revanced.patcher.extensions.or
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patcher.patch.*
import app.revanced.patcher.extensions.or
import app.revanced.patcher.patch.BytecodePatch
import app.revanced.patcher.patch.OptionsContainer
import app.revanced.patcher.patch.PatchOption
import app.revanced.patcher.patch.annotations.DependsOn
import app.revanced.patcher.patch.annotations.Patch
import app.revanced.patcher.usage.resource.annotation.ExampleResourceCompatibility
@@ -29,7 +31,6 @@ import org.jf.dexlib2.immutable.reference.ImmutableFieldReference
import org.jf.dexlib2.immutable.reference.ImmutableStringReference
import org.jf.dexlib2.immutable.value.ImmutableFieldEncodedValue
import org.jf.dexlib2.util.Preconditions
import kotlin.io.path.Path
@Patch
@Name("example-bytecode-patch")
@@ -40,7 +41,7 @@ import kotlin.io.path.Path
class ExampleBytecodePatch : BytecodePatch(listOf(ExampleFingerprint)) {
// This function will be executed by the patcher.
// You can treat it as a constructor
override fun execute(context: BytecodeContext): PatchResult {
override suspend fun execute(context: BytecodeContext) {
// Get the resolved method by its fingerprint from the resolver cache
val result = ExampleFingerprint.result!!
@@ -60,7 +61,7 @@ class ExampleBytecodePatch : BytecodePatch(listOf(ExampleFingerprint)) {
implementation.replaceStringAt(startIndex, "Hello, ReVanced! Editing bytecode.")
// Get the class in which the method matching our fingerprint is defined in.
val mainClass = context.findClass {
val mainClass = context.classes.findClassProxied {
it.type == result.classDef.type
}!!.mutableClass
@@ -127,12 +128,6 @@ class ExampleBytecodePatch : BytecodePatch(listOf(ExampleFingerprint)) {
invoke-virtual { v0, v1 }, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
"""
)
// 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()
}
/**
@@ -193,10 +188,5 @@ class ExampleBytecodePatch : BytecodePatch(listOf(ExampleFingerprint)) {
"key5", null, "title", "description", true
)
)
private var key6 by option(
PatchOption.PathOption(
"key6", Path("test.txt"), "title", "description", true
)
)
}
}

View File

@@ -1,11 +1,11 @@
package app.revanced.patcher.usage.resource.patch
import app.revanced.patcher.ResourceContext
import app.revanced.patcher.annotation.Description
import app.revanced.patcher.annotation.Name
import app.revanced.patcher.annotation.Version
import app.revanced.patcher.data.ResourceContext
import app.revanced.patcher.patch.PatchResult
import app.revanced.patcher.patch.PatchResultSuccess
import app.revanced.patcher.apk.Apk
import app.revanced.patcher.openXmlFile
import app.revanced.patcher.patch.ResourcePatch
import app.revanced.patcher.patch.annotations.Patch
import app.revanced.patcher.usage.resource.annotation.ExampleResourceCompatibility
@@ -17,8 +17,8 @@ import org.w3c.dom.Element
@ExampleResourceCompatibility
@Version("0.0.1")
class ExampleResourcePatch : ResourcePatch {
override fun execute(context: ResourceContext): PatchResult {
context.xmlEditor["AndroidManifest.xml"].use { editor ->
override suspend fun execute(context: ResourceContext) {
context.apkBundle.base.resources.openXmlFile(Apk.MANIFEST_FILE_NAME).use { editor ->
val element = editor // regular DomFileEditor
.file
.getElementsByTagName("application")
@@ -29,7 +29,5 @@ class ExampleResourcePatch : ResourcePatch {
"exampleValue"
)
}
return PatchResultSuccess()
}
}

View File

@@ -1 +1,28 @@
rootProject.name = "revanced-patcher"
pluginManagement {
repositories {
mavenCentral()
maven {
url = uri("https://maven.pkg.github.com/revanced/multidexlib2")
credentials {
username = providers.gradleProperty("gpr.user").orNull ?: System.getenv("GITHUB_ACTOR")
password = providers.gradleProperty("gpr.key").orNull ?: System.getenv("GITHUB_TOKEN")
}
}
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
mavenCentral()
maven {
url = uri("https://maven.pkg.github.com/revanced/multidexlib2")
credentials {
username = providers.gradleProperty("gpr.user").orNull ?: System.getenv("GITHUB_ACTOR")
password = providers.gradleProperty("gpr.key").orNull ?: System.getenv("GITHUB_TOKEN")
}
}
}
}
include("revanced-patcher", "arsclib-utils")

View File

@@ -1,391 +0,0 @@
package app.revanced.patcher
import app.revanced.patcher.data.Context
import app.revanced.patcher.extensions.PatchExtensions.dependencies
import app.revanced.patcher.extensions.PatchExtensions.patchName
import app.revanced.patcher.extensions.PatchExtensions.requiresIntegrations
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint.Companion.resolve
import app.revanced.patcher.patch.*
import app.revanced.patcher.util.VersionReader
import brut.androlib.Androlib
import brut.androlib.meta.UsesFramework
import brut.androlib.options.BuildOptions
import brut.androlib.res.AndrolibResources
import brut.androlib.res.data.ResPackage
import brut.androlib.res.decoder.AXmlResourceParser
import brut.androlib.res.decoder.ResAttrDecoder
import brut.androlib.res.decoder.XmlPullStreamDecoder
import brut.androlib.res.xml.ResXmlPatcher
import brut.directory.ExtFile
import lanchon.multidexlib2.BasicDexFileNamer
import lanchon.multidexlib2.DexIO
import lanchon.multidexlib2.MultiDexIO
import org.jf.dexlib2.Opcodes
import org.jf.dexlib2.iface.DexFile
import org.jf.dexlib2.writer.io.MemoryDataStore
import java.io.File
import java.io.OutputStream
import java.nio.file.Files
internal val NAMER = BasicDexFileNamer()
/**
* The ReVanced Patcher.
* @param options The options for the patcher.
*/
class Patcher(private val options: PatcherOptions) {
private val logger = options.logger
private val opcodes: Opcodes
private var resourceDecodingMode = ResourceDecodingMode.MANIFEST_ONLY
private var mergeIntegrations = false
val context: PatcherContext
companion object {
@JvmStatic
val version = VersionReader.read()
private fun BuildOptions.setBuildOptions(options: PatcherOptions) {
this.aaptPath = options.aaptPath
this.useAapt2 = true
this.frameworkFolderLocation = options.frameworkFolderLocation
}
}
init {
logger.info("Reading dex files")
// read dex files
val dexFile = MultiDexIO.readDexFile(true, options.inputFile, NAMER, null, null)
// get the opcodes
opcodes = dexFile.opcodes
// finally create patcher context
context = PatcherContext(dexFile.classes.toMutableList(), File(options.resourceCacheDirectory))
// decode manifest file
decodeResources(ResourceDecodingMode.MANIFEST_ONLY)
}
/**
* Add integrations to be merged by the patcher.
* The integrations will only be merged, if necessary.
*
* @param integrations The integrations, must be dex files or dex file container such as ZIP, APK or DEX files.
* @param callback The callback for [integrations] which are being added.
*/
fun addIntegrations(
integrations: List<File>,
callback: (File) -> Unit
) {
context.integrations.apply integrations@{
add(integrations)
this@integrations.callback = callback
}
}
/**
* Save the patched dex file.
*/
fun save(): PatcherResult {
val packageMetadata = context.packageMetadata
val metaInfo = packageMetadata.metaInfo
var resourceFile: File? = null
when (resourceDecodingMode) {
ResourceDecodingMode.FULL -> {
val cacheDirectory = ExtFile(options.resourceCacheDirectory)
try {
val androlibResources = AndrolibResources().also { resources ->
resources.buildOptions = BuildOptions().also { buildOptions ->
buildOptions.setBuildOptions(options)
buildOptions.isFramework = metaInfo.isFrameworkApk
buildOptions.resourcesAreCompressed = metaInfo.compressionType
buildOptions.doNotCompress = metaInfo.doNotCompress
}
resources.setSdkInfo(metaInfo.sdkInfo)
resources.setVersionInfo(metaInfo.versionInfo)
resources.setSharedLibrary(metaInfo.sharedLibrary)
resources.setSparseResources(metaInfo.sparseResources)
}
val manifestFile = cacheDirectory.resolve("AndroidManifest.xml")
ResXmlPatcher.fixingPublicAttrsInProviderAttributes(manifestFile)
val aaptFile = cacheDirectory.resolve("aapt_temp_file")
// delete if it exists
Files.deleteIfExists(aaptFile.toPath())
val resDirectory = cacheDirectory.resolve("res")
val includedFiles = metaInfo.usesFramework.ids.map { id ->
androlibResources.getFrameworkApk(
id, metaInfo.usesFramework.tag
)
}.toTypedArray()
logger.info("Compiling resources")
androlibResources.aaptPackage(
aaptFile, manifestFile, resDirectory, null, null, includedFiles
)
resourceFile = aaptFile
} finally {
cacheDirectory.close()
}
}
else -> logger.info("Not compiling resources because resource patching is not required")
}
logger.trace("Creating new dex file")
val newDexFile = object : DexFile {
override fun getClasses() = context.bytecodeContext.classes.also { it.replaceClasses() }
override fun getOpcodes() = this@Patcher.opcodes
}
// write modified dex files
logger.info("Writing modified dex files")
val dexFiles = mutableMapOf<String, MemoryDataStore>()
MultiDexIO.writeDexFile(
true, -1, // core count
dexFiles, NAMER, newDexFile, DexIO.DEFAULT_MAX_DEX_POOL_SIZE, null
)
return PatcherResult(
dexFiles.map {
app.revanced.patcher.util.dex.DexFile(it.key, it.value.readAt(0))
},
metaInfo.doNotCompress?.toList(),
resourceFile
)
}
/**
* Add [Patch]es to the patcher.
* @param patches [Patch]es The patches to add.
*/
fun addPatches(patches: Iterable<Class<out Patch<Context>>>) {
/**
* Returns true if at least one patches or its dependencies matches the given predicate.
*/
fun Class<out Patch<Context>>.anyRecursively(predicate: (Class<out Patch<Context>>) -> Boolean): Boolean =
predicate(this) || dependencies?.any { it.java.anyRecursively(predicate) } == true
// Determine if resource patching is required.
for (patch in patches) {
if (patch.anyRecursively { ResourcePatch::class.java.isAssignableFrom(it) }) {
resourceDecodingMode = ResourceDecodingMode.FULL
break
}
}
// Determine if merging integrations is required.
for (patch in patches) {
if (patch.anyRecursively { it.requiresIntegrations }) {
mergeIntegrations = true
break
}
}
context.patches.addAll(patches)
}
/**
* Decode resources for the patcher.
*
* @param mode The [ResourceDecodingMode] to use when decoding.
*/
private fun decodeResources(mode: ResourceDecodingMode) {
val extInputFile = ExtFile(options.inputFile)
try {
val androlib = Androlib(BuildOptions().also { it.setBuildOptions(options) })
val resourceTable = androlib.getResTable(extInputFile, true)
when (mode) {
ResourceDecodingMode.FULL -> {
val outDir = File(options.resourceCacheDirectory)
if (outDir.exists()) {
logger.info("Deleting existing resource cache directory")
if (!outDir.deleteRecursively()) {
logger.error("Failed to delete existing resource cache directory")
}
}
outDir.mkdirs()
logger.info("Decoding resources")
// decode resources to cache directory
androlib.decodeManifestWithResources(extInputFile, outDir, resourceTable)
androlib.decodeResourcesFull(extInputFile, outDir, resourceTable)
// read additional metadata from the resource table
context.packageMetadata.let { metadata ->
metadata.metaInfo.usesFramework = UsesFramework().also { framework ->
framework.ids = resourceTable.listFramePackages().map { it.id }.sorted()
}
// read files to not compress
metadata.metaInfo.doNotCompress = buildList {
androlib.recordUncompressedFiles(extInputFile, this)
}
}
}
ResourceDecodingMode.MANIFEST_ONLY -> {
logger.info("Decoding AndroidManifest.xml only, because resources are not needed")
// create decoder for the resource table
val decoder = ResAttrDecoder()
decoder.currentPackage = ResPackage(resourceTable, 0, null)
// create xml parser with the decoder
val axmlParser = AXmlResourceParser()
axmlParser.attrDecoder = decoder
// parse package information with the decoder and parser which will set required values in the resource table
// instead of decodeManifest another more low level solution can be created to make it faster/better
XmlPullStreamDecoder(
axmlParser, AndrolibResources().resXmlSerializer
).decodeManifest(
extInputFile.directory.getFileInput("AndroidManifest.xml"), OutputStream.nullOutputStream()
)
}
}
// read of the resourceTable which is created by reading the manifest file
context.packageMetadata.let { metadata ->
metadata.packageName = resourceTable.currentResPackage.name
metadata.packageVersion = resourceTable.versionInfo.versionName
metadata.metaInfo.versionInfo = resourceTable.versionInfo
metadata.metaInfo.sdkInfo = resourceTable.sdkInfo
}
} finally {
extInputFile.close()
}
}
/**
* Execute patches added the patcher.
*
* @param stopOnError If true, the patches will stop on the first error.
* @return A pair of the name of the [Patch] and its [PatchResult].
*/
fun executePatches(stopOnError: Boolean = false): Sequence<Pair<String, Result<PatchResultSuccess>>> {
/**
* Execute a [Patch] and its dependencies recursively.
*
* @param patchClass The [Patch] to execute.
* @param executedPatches A map of [Patch]es paired to a boolean indicating their success, to prevent infinite recursion.
* @return The result of executing the [Patch].
*/
fun executePatch(
patchClass: Class<out Patch<Context>>,
executedPatches: LinkedHashMap<String, ExecutedPatch>
): PatchResult {
val patchName = patchClass.patchName
// if the patch has already applied silently skip it
if (executedPatches.contains(patchName)) {
if (!executedPatches[patchName]!!.success)
return PatchResultError("'$patchName' did not succeed previously")
logger.trace("Skipping '$patchName' because it has already been applied")
return PatchResultSuccess()
}
// recursively execute all dependency patches
patchClass.dependencies?.forEach { dependencyClass ->
val dependency = dependencyClass.java
val result = executePatch(dependency, executedPatches)
if (result.isSuccess()) return@forEach
return PatchResultError(
"'$patchName' depends on '${dependency.patchName}' but the following error was raised: " +
result.error()!!.let { it.cause?.stackTraceToString() ?: it.message }
)
}
val isResourcePatch = ResourcePatch::class.java.isAssignableFrom(patchClass)
val patchInstance = patchClass.getDeclaredConstructor().newInstance()
// TODO: implement this in a more polymorphic way
val patchContext = if (isResourcePatch) {
context.resourceContext
} else {
context.bytecodeContext.also { context ->
(patchInstance as BytecodePatch).fingerprints?.resolve(
context,
context.classes.classes
)
}
}
logger.trace("Executing '$patchName' of type: ${if (isResourcePatch) "resource" else "bytecode"}")
return try {
patchInstance.execute(patchContext).also {
executedPatches[patchName] = ExecutedPatch(patchInstance, it.isSuccess())
}
} catch (e: Exception) {
PatchResultError(e).also {
executedPatches[patchName] = ExecutedPatch(patchInstance, false)
}
}
}
return sequence {
if (mergeIntegrations) context.integrations.merge(logger)
// prevent from decoding the manifest twice if it is not needed
if (resourceDecodingMode == ResourceDecodingMode.FULL) decodeResources(ResourceDecodingMode.FULL)
logger.trace("Executing all patches")
val executedPatches = LinkedHashMap<String, ExecutedPatch>() // first is name
try {
context.patches.forEach { patch ->
val patchResult = executePatch(patch, executedPatches)
val result = if (patchResult.isSuccess()) {
Result.success(patchResult.success()!!)
} else {
Result.failure(patchResult.error()!!)
}
yield(patch.patchName to result)
if (stopOnError && patchResult.isError()) return@sequence
}
} finally {
executedPatches.values.reversed().forEach { (patch, _) ->
patch.close()
}
}
}
}
/**
* The type of decoding the resources.
*/
private enum class ResourceDecodingMode {
/**
* Decode all resources.
*/
FULL,
/**
* Decode the manifest file only.
*/
MANIFEST_ONLY,
}
}
/**
* A result of executing a [Patch].
*
* @param patchInstance The instance of the [Patch] that was applied.
* @param success The result of the [Patch].
*/
internal data class ExecutedPatch(val patchInstance: Patch<Context>, val success: Boolean)

View File

@@ -1,64 +0,0 @@
package app.revanced.patcher
import app.revanced.patcher.data.*
import app.revanced.patcher.logging.Logger
import app.revanced.patcher.patch.Patch
import app.revanced.patcher.util.ClassMerger.merge
import org.jf.dexlib2.iface.ClassDef
import java.io.File
data class PatcherContext(
val classes: MutableList<ClassDef>,
val resourceCacheDirectory: File,
) {
val packageMetadata = PackageMetadata()
internal val patches = mutableListOf<Class<out Patch<Context>>>()
internal val integrations = Integrations(this)
internal val bytecodeContext = BytecodeContext(classes)
internal val resourceContext = ResourceContext(resourceCacheDirectory)
internal class Integrations(val context: PatcherContext) {
var callback: ((File) -> Unit)? = null
private val integrations: MutableList<File> = mutableListOf()
fun add(integrations: List<File>) = this@Integrations.integrations.addAll(integrations)
/**
* Merge integrations.
* @param logger A logger.
*/
fun merge(logger: Logger) {
with(context.bytecodeContext.classes) {
for (integrations in integrations) {
callback?.let { it(integrations) }
for (classDef in lanchon.multidexlib2.MultiDexIO.readDexFile(
true,
integrations,
NAMER,
null,
null
).classes) {
val type = classDef.type
val result = classes.findIndexed { it.type == type }
if (result == null) {
logger.trace("Merging type $type")
classes.add(classDef)
continue
}
val (existingClass, existingClassIndex) = result
logger.trace("Type $type exists. Adding missing methods and fields.")
existingClass.merge(classDef, context, logger).let { mergedClass ->
if (mergedClass !== existingClass) // referential equality check
classes[existingClassIndex] = mergedClass
}
}
}
}
}
}
}

View File

@@ -1,21 +0,0 @@
package app.revanced.patcher
import app.revanced.patcher.logging.Logger
import app.revanced.patcher.logging.impl.NopLogger
import java.io.File
/**
* Options for the [Patcher].
* @param inputFile The input file (usually an apk file).
* @param resourceCacheDirectory Directory to cache resources.
* @param aaptPath Optional path to a custom aapt binary.
* @param frameworkFolderLocation Optional path to a custom framework folder.
* @param logger Custom logger implementation for the [Patcher].
*/
data class PatcherOptions(
internal val inputFile: File,
internal val resourceCacheDirectory: String,
internal val aaptPath: String? = null,
internal val frameworkFolderLocation: String? = null,
internal val logger: Logger = NopLogger
)

View File

@@ -1,16 +0,0 @@
package app.revanced.patcher
import app.revanced.patcher.util.dex.DexFile
import java.io.File
/**
* The result of a patcher.
* @param dexFiles The patched dex files.
* @param doNotCompress List of relative paths to files to exclude from compressing.
* @param resourceFile File containing resources that need to be extracted into the APK.
*/
data class PatcherResult(
val dexFiles: List<DexFile>,
val doNotCompress: List<String>? = null,
val resourceFile: File?
)

View File

@@ -1,170 +0,0 @@
package app.revanced.patcher.data
import app.revanced.patcher.util.ProxyBackedClassList
import app.revanced.patcher.util.method.MethodWalker
import org.jf.dexlib2.iface.ClassDef
import org.jf.dexlib2.iface.Method
import org.w3c.dom.Document
import java.io.Closeable
import java.io.File
import java.io.InputStream
import java.io.OutputStream
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult
/**
* A common interface to constrain [Context] to [BytecodeContext] and [ResourceContext].
*/
sealed interface Context
class BytecodeContext internal constructor(classes: MutableList<ClassDef>) : Context {
/**
* The list of classes.
*/
val classes = ProxyBackedClassList(classes)
/**
* Find a class by a given class name.
*
* @param className The name of the class.
* @return A proxy for the first class that matches the class name.
*/
fun findClass(className: String) = findClass { it.type.contains(className) }
/**
* Find a class by a given predicate.
*
* @param predicate A predicate to match the class.
* @return A proxy for the first class that matches the predicate.
*/
fun findClass(predicate: (ClassDef) -> Boolean) =
// if we already proxied the class matching the predicate...
classes.proxies.firstOrNull { predicate(it.immutableClass) } ?:
// else resolve the class to a proxy and return it, if the predicate is matching a class
classes.find(predicate)?.let { proxy(it) }
fun proxy(classDef: ClassDef): app.revanced.patcher.util.proxy.ClassProxy {
var proxy = this.classes.proxies.find { it.immutableClass.type == classDef.type }
if (proxy == null) {
proxy = app.revanced.patcher.util.proxy.ClassProxy(classDef)
this.classes.add(proxy)
}
return proxy
}
}
/**
* Create a [MethodWalker] instance for the current [BytecodeContext].
*
* @param startMethod The method to start at.
* @return A [MethodWalker] instance.
*/
fun BytecodeContext.toMethodWalker(startMethod: Method): MethodWalker {
return MethodWalker(this, startMethod)
}
internal inline fun <T> Iterable<T>.findIndexed(predicate: (T) -> Boolean): Pair<T, Int>? {
for ((index, element) in this.withIndex()) {
if (predicate(element)) {
return element to index
}
}
return null
}
class ResourceContext internal constructor(private val resourceCacheDirectory: File) : Context, Iterable<File> {
val xmlEditor = XmlFileHolder()
operator fun get(path: String) = resourceCacheDirectory.resolve(path)
override fun iterator() = resourceCacheDirectory.walkTopDown().iterator()
inner class XmlFileHolder {
operator fun get(inputStream: InputStream) =
DomFileEditor(inputStream)
operator fun get(path: String): DomFileEditor {
return DomFileEditor(this@ResourceContext[path])
}
}
}
/**
* Wrapper for a file that can be edited as a dom document.
*
* This constructor does not check for locks to the file when writing.
* Use the secondary constructor.
*
* @param inputStream the input stream to read the xml file from.
* @param outputStream the output stream to write the xml file to. If null, the file will be read only.
*
*/
class DomFileEditor internal constructor(
private val inputStream: InputStream,
private val outputStream: Lazy<OutputStream>? = null,
) : Closeable {
// path to the xml file to unlock the resource when closing the editor
private var filePath: String? = null
private var closed: Boolean = false
/**
* The document of the xml file
*/
val file: Document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputStream)
.also(Document::normalize)
// lazily open an output stream
// this is required because when constructing a DomFileEditor the output stream is created along with the input stream, which is not allowed
// the workaround is to lazily create the output stream. This way it would be used after the input stream is closed, which happens in the constructor
constructor(file: File) : this(file.inputStream(), lazy { file.outputStream() }) {
// increase the lock
locks.merge(file.path, 1, Integer::sum)
filePath = file.path
}
/**
* Closes the editor. Write backs and decreases the lock count.
*
* Will not write back to the file if the file is still locked.
*/
override fun close() {
if (closed) return
inputStream.close()
// if the output stream is not null, do not close it
outputStream?.let {
// prevent writing to same file, if it is being locked
// isLocked will be false if the editor was created through a stream
val isLocked = filePath?.let { path ->
val isLocked = locks[path]!! > 1
// decrease the lock count if the editor was opened for a file
locks.merge(path, -1, Integer::sum)
isLocked
} ?: false
// if unlocked, write back to the file
if (!isLocked) {
it.value.use { stream ->
val result = StreamResult(stream)
TransformerFactory.newInstance().newTransformer().transform(DOMSource(file), result)
}
it.value.close()
return
}
}
closed = true
}
private companion object {
// map of concurrent open files
val locks = mutableMapOf<String, Int>()
}
}

View File

@@ -1,13 +0,0 @@
package app.revanced.patcher.data
import brut.androlib.meta.MetaInfo
/**
* Metadata about a package.
*/
class PackageMetadata {
lateinit var packageName: String
lateinit var packageVersion: String
internal val metaInfo: MetaInfo = MetaInfo()
}

View File

@@ -1,314 +0,0 @@
package app.revanced.patcher.fingerprint.method.impl
import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.extensions.MethodFingerprintExtensions.fuzzyPatternScanMethod
import app.revanced.patcher.fingerprint.Fingerprint
import app.revanced.patcher.fingerprint.method.annotation.FuzzyPatternScanMethod
import app.revanced.patcher.util.proxy.ClassProxy
import org.jf.dexlib2.Opcode
import org.jf.dexlib2.iface.ClassDef
import org.jf.dexlib2.iface.Method
import org.jf.dexlib2.iface.instruction.Instruction
import org.jf.dexlib2.iface.instruction.ReferenceInstruction
import org.jf.dexlib2.iface.reference.StringReference
import org.jf.dexlib2.util.MethodUtil
private typealias StringMatch = MethodFingerprintResult.MethodFingerprintScanResult.StringsScanResult.StringMatch
private typealias StringsScanResult = MethodFingerprintResult.MethodFingerprintScanResult.StringsScanResult
/**
* Represents the [MethodFingerprint] for a method.
* @param returnType The return type of the method.
* @param accessFlags The access flags of the method.
* @param parameters The parameters of the method.
* @param opcodes The list of opcodes of the method.
* @param strings A list of strings which a method contains.
* @param customFingerprint A custom condition for this fingerprint.
* A `null` opcode is equals to an unknown opcode.
*/
abstract class MethodFingerprint(
internal val returnType: String? = null,
internal val accessFlags: Int? = null,
internal val parameters: Iterable<String>? = null,
internal val opcodes: Iterable<Opcode?>? = null,
internal val strings: Iterable<String>? = null,
internal val customFingerprint: ((methodDef: Method, classDef: ClassDef) -> Boolean)? = null
) : Fingerprint {
/**
* The result of the [MethodFingerprint].
*/
var result: MethodFingerprintResult? = null
companion object {
/**
* Resolve a list of [MethodFingerprint] against a list of [ClassDef].
*
* @param classes The classes on which to resolve the [MethodFingerprint] in.
* @param context The [BytecodeContext] to host proxies.
* @return True if the resolution was successful, false otherwise.
*/
fun Iterable<MethodFingerprint>.resolve(context: BytecodeContext, classes: Iterable<ClassDef>) {
for (fingerprint in this) // For each fingerprint
classes@ for (classDef in classes) // search through all classes for the fingerprint
if (fingerprint.resolve(context, classDef))
break@classes // if the resolution succeeded, continue with the next fingerprint
}
/**
* Resolve a [MethodFingerprint] against a [ClassDef].
*
* @param forClass The class on which to resolve the [MethodFingerprint] in.
* @param context The [BytecodeContext] to host proxies.
* @return True if the resolution was successful, false otherwise.
*/
fun MethodFingerprint.resolve(context: BytecodeContext, forClass: ClassDef): Boolean {
for (method in forClass.methods)
if (this.resolve(context, method, forClass))
return true
return false
}
/**
* Resolve a [MethodFingerprint] against a [Method].
*
* @param method The class on which to resolve the [MethodFingerprint] in.
* @param forClass The class on which to resolve the [MethodFingerprint].
* @param context The [BytecodeContext] to host proxies.
* @return True if the resolution was successful or if the fingerprint is already resolved, false otherwise.
*/
fun MethodFingerprint.resolve(context: BytecodeContext, method: Method, forClass: ClassDef): Boolean {
val methodFingerprint = this
if (methodFingerprint.result != null) return true
if (methodFingerprint.returnType != null && !method.returnType.startsWith(methodFingerprint.returnType))
return false
if (methodFingerprint.accessFlags != null && methodFingerprint.accessFlags != method.accessFlags)
return false
fun parametersEqual(
parameters1: Iterable<CharSequence>, parameters2: Iterable<CharSequence>
): Boolean {
if (parameters1.count() != parameters2.count()) return false
val iterator1 = parameters1.iterator()
parameters2.forEach {
if (!it.startsWith(iterator1.next())) return false
}
return true
}
if (methodFingerprint.parameters != null && !parametersEqual(
methodFingerprint.parameters, // TODO: parseParameters()
method.parameterTypes
)
) return false
@Suppress("UNNECESSARY_NOT_NULL_ASSERTION")
if (methodFingerprint.customFingerprint != null && !methodFingerprint.customFingerprint!!(method, forClass))
return false
val stringsScanResult: StringsScanResult? =
if (methodFingerprint.strings != null) {
StringsScanResult(
buildList {
val implementation = method.implementation ?: return false
val stringsList = methodFingerprint.strings.toMutableList()
implementation.instructions.forEachIndexed { instructionIndex, instruction ->
if (
instruction.opcode != Opcode.CONST_STRING &&
instruction.opcode != Opcode.CONST_STRING_JUMBO
) return@forEachIndexed
val string = ((instruction as ReferenceInstruction).reference as StringReference).string
val index = stringsList.indexOfFirst(string::contains)
if (index == -1) return@forEachIndexed
add(
StringMatch(
string,
instructionIndex
)
)
stringsList.removeAt(index)
}
if (stringsList.isNotEmpty()) return false
}
)
} else null
val patternScanResult = if (methodFingerprint.opcodes != null) {
method.implementation?.instructions ?: return false
method.patternScan(methodFingerprint) ?: return false
} else null
methodFingerprint.result = MethodFingerprintResult(
method,
forClass,
MethodFingerprintResult.MethodFingerprintScanResult(
patternScanResult,
stringsScanResult
),
context
)
return true
}
private fun Method.patternScan(
fingerprint: MethodFingerprint
): MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult? {
val instructions = this.implementation!!.instructions
val fingerprintFuzzyPatternScanThreshold = fingerprint.fuzzyPatternScanMethod?.threshold ?: 0
val pattern = fingerprint.opcodes!!
val instructionLength = instructions.count()
val patternLength = pattern.count()
for (index in 0 until instructionLength) {
var patternIndex = 0
var threshold = fingerprintFuzzyPatternScanThreshold
while (index + patternIndex < instructionLength) {
val originalOpcode = instructions.elementAt(index + patternIndex).opcode
val patternOpcode = pattern.elementAt(patternIndex)
if (patternOpcode != null && patternOpcode.ordinal != originalOpcode.ordinal) {
// reaching maximum threshold (0) means,
// the pattern does not match to the current instructions
if (threshold-- == 0) break
}
if (patternIndex < patternLength - 1) {
// if the entire pattern has not been scanned yet
// continue the scan
patternIndex++
continue
}
// the pattern is valid, generate warnings if fuzzyPatternScanMethod is FuzzyPatternScanMethod
val result =
MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult(
index,
index + patternIndex
)
if (fingerprint.fuzzyPatternScanMethod !is FuzzyPatternScanMethod) return result
result.warnings = result.createWarnings(pattern, instructions)
return result
}
}
return null
}
private fun MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult.createWarnings(
pattern: Iterable<Opcode?>, instructions: Iterable<Instruction>
) = buildList {
for ((patternIndex, instructionIndex) in (this@createWarnings.startIndex until this@createWarnings.endIndex).withIndex()) {
val originalOpcode = instructions.elementAt(instructionIndex).opcode
val patternOpcode = pattern.elementAt(patternIndex)
if (patternOpcode == null || patternOpcode.ordinal == originalOpcode.ordinal) continue
this.add(
MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult.Warning(
originalOpcode,
patternOpcode,
instructionIndex,
patternIndex
)
)
}
}
}
}
/**
* Represents the result of a [MethodFingerprintResult].
*
* @param method The matching method.
* @param classDef The [ClassDef] that contains the matching [method].
* @param scanResult The result of scanning for the [MethodFingerprint].
* @param context The [BytecodeContext] this [MethodFingerprintResult] is attached to, to create proxies.
*/
data class MethodFingerprintResult(
val method: Method,
val classDef: ClassDef,
val scanResult: MethodFingerprintScanResult,
internal val context: BytecodeContext
) {
/**
* Returns a mutable clone of [classDef]
*
* Please note, this method allocates a [ClassProxy].
* Use [classDef] where possible.
*/
@Suppress("MemberVisibilityCanBePrivate")
val mutableClass by lazy { context.proxy(classDef).mutableClass }
/**
* Returns a mutable clone of [method]
*
* Please note, this method allocates a [ClassProxy].
* Use [method] where possible.
*/
val mutableMethod by lazy {
mutableClass.methods.first {
MethodUtil.methodSignaturesMatch(it, this.method)
}
}
/**
* The result of scanning on the [MethodFingerprint].
* @param patternScanResult The result of the pattern scan.
* @param stringsScanResult The result of the string scan.
*/
data class MethodFingerprintScanResult(
val patternScanResult: PatternScanResult?,
val stringsScanResult: StringsScanResult?
) {
/**
* The result of scanning strings on the [MethodFingerprint].
* @param matches The list of strings that were matched.
*/
data class StringsScanResult(val matches: List<StringMatch>) {
/**
* Represents a match for a string at an index.
* @param string The string that was matched.
* @param index The index of the string.
*/
data class StringMatch(val string: String, val index: Int)
}
/**
* The result of a pattern scan.
* @param startIndex The start index of the instructions where to which this pattern matches.
* @param endIndex The end index of the instructions where to which this pattern matches.
* @param warnings A list of warnings considering this [PatternScanResult].
*/
data class PatternScanResult(
val startIndex: Int,
val endIndex: Int,
var warnings: List<Warning>? = null
) {
/**
* Represents warnings of the pattern scan.
* @param correctOpcode The opcode the instruction list has.
* @param wrongOpcode The opcode the pattern list of the signature currently has.
* @param instructionIndex The index of the opcode relative to the instruction list.
* @param patternIndex The index of the opcode relative to the pattern list from the signature.
*/
data class Warning(
val correctOpcode: Opcode,
val wrongOpcode: Opcode,
val instructionIndex: Int,
val patternIndex: Int,
)
}
}
}

View File

@@ -1,8 +0,0 @@
package app.revanced.patcher.logging
interface Logger {
fun error(msg: String) {}
fun warn(msg: String) {}
fun info(msg: String) {}
fun trace(msg: String) {}
}

View File

@@ -1,5 +0,0 @@
package app.revanced.patcher.logging.impl
import app.revanced.patcher.logging.Logger
object NopLogger : Logger

View File

@@ -1,35 +0,0 @@
package app.revanced.patcher.patch
interface PatchResult {
fun error(): PatchResultError? {
if (this is PatchResultError) {
return this
}
return null
}
fun success(): PatchResultSuccess? {
if (this is PatchResultSuccess) {
return this
}
return null
}
fun isError(): Boolean {
return this is PatchResultError
}
fun isSuccess(): Boolean {
return this is PatchResultSuccess
}
}
class PatchResultError(
errorMessage: String?, cause: Exception?
) : Exception(errorMessage, cause), PatchResult {
constructor(errorMessage: String) : this(errorMessage, null)
constructor(cause: Exception) : this(cause.message, cause)
}
class PatchResultSuccess : PatchResult

View File

@@ -1,46 +0,0 @@
package app.revanced.patcher.util
import app.revanced.patcher.util.proxy.ClassProxy
import org.jf.dexlib2.iface.ClassDef
/**
* A class that represents a set of classes and proxies.
*
* @param classes The classes to be backed by proxies.
*/
class ProxyBackedClassList(internal val classes: MutableList<ClassDef>) : Set<ClassDef> {
internal val proxies = mutableListOf<ClassProxy>()
/**
* Add a [ClassDef].
*/
fun add(classDef: ClassDef) = classes.add(classDef)
/**
* Add a [ClassProxy].
*/
fun add(classProxy: ClassProxy) = proxies.add(classProxy)
/**
* Replace all classes with their mutated versions.
*/
internal fun replaceClasses() =
proxies.removeIf { proxy ->
// if the proxy is unused, keep it in the list
if (!proxy.resolved) return@removeIf false
// if it has been used, replace the original class with the new class
val index = classes.indexOfFirst { it.type == proxy.immutableClass.type }
classes[index] = proxy.mutableClass
// return true to remove it from the proxies list
return@removeIf true
}
override val size get() = classes.size
override fun contains(element: ClassDef) = classes.contains(element)
override fun containsAll(elements: Collection<ClassDef>) = classes.containsAll(elements)
override fun isEmpty() = classes.isEmpty()
override fun iterator() = classes.iterator()
}

Some files were not shown because too many files have changed in this diff Show More