You've already forked revanced-patcher
mirror of
https://github.com/revanced/revanced-patcher
synced 2025-09-17 07:30:49 +02:00
Compare commits
177 Commits
arsclib-re
...
v18.0.0-de
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f77624b3b9 | ||
![]() |
a76ac04214 | ||
![]() |
0447fa9c28 | ||
![]() |
54ac1394a9 | ||
![]() |
0b04c73ac5 | ||
![]() |
079de45238 | ||
![]() |
56ce9ec2f9 | ||
![]() |
1b52e4b0f9 | ||
![]() |
098c2c1efa | ||
![]() |
64343e5a7c | ||
![]() |
0caf6caeb9 | ||
![]() |
55f6c2a9fc | ||
![]() |
c6095bc38a | ||
![]() |
09cd6aa568 | ||
![]() |
ebbaafb78e | ||
![]() |
e6de90d300 | ||
![]() |
c7922e90d0 | ||
![]() |
c1f4c0445a | ||
![]() |
caa634fac6 | ||
![]() |
f28bfe0dbd | ||
![]() |
155e787ff4 | ||
![]() |
c38f0ef42a | ||
![]() |
4456031459 | ||
![]() |
5fb59a227f | ||
![]() |
d9fb241d57 | ||
![]() |
642c4ea97e | ||
![]() |
8c8a251626 | ||
![]() |
5953d6cfb5 | ||
![]() |
a1962fe600 | ||
![]() |
77dbee3d6a | ||
![]() |
cb5e39d73e | ||
![]() |
38ef2f470a | ||
![]() |
129d84e108 | ||
![]() |
affeba76b8 | ||
![]() |
6059d3ca26 | ||
![]() |
444dee5a16 | ||
![]() |
d314466ce2 | ||
![]() |
fdaf9c21c8 | ||
![]() |
06c2b76f11 | ||
![]() |
3896b30738 | ||
![]() |
2c4b88e1a0 | ||
![]() |
dfc7e1596b | ||
![]() |
f590436399 | ||
![]() |
cbfb9ba02f | ||
![]() |
b4cfe80ad5 | ||
![]() |
b37906fa35 | ||
![]() |
356f1f1553 | ||
![]() |
e882af74ee | ||
![]() |
46875fb28e | ||
![]() |
417c3e4234 | ||
![]() |
6d2c28807b | ||
![]() |
4d6e08a650 | ||
![]() |
5cebc1fd30 | ||
![]() |
ac61731dc6 | ||
![]() |
9e4ffabd5c | ||
![]() |
3f410bd39f | ||
![]() |
d51bc32e37 | ||
![]() |
b7f6aa94cc | ||
![]() |
ff965e6953 | ||
![]() |
468d5d7421 | ||
![]() |
fc95b28c49 | ||
![]() |
69184187d9 | ||
![]() |
a802d0df46 | ||
![]() |
8de30633ae | ||
![]() |
a1fbb7990f | ||
![]() |
aa71146b1b | ||
![]() |
9fdb8f087f | ||
![]() |
670f0153de | ||
![]() |
1d7aeca696 | ||
![]() |
4e7811ea07 | ||
![]() |
e11283744a | ||
![]() |
91cdfd53ef | ||
![]() |
bc7d6b9941 | ||
![]() |
6b1e0a1656 | ||
![]() |
72c9eb2129 | ||
![]() |
4bc4b0dc01 | ||
![]() |
637d48746f | ||
![]() |
9a109c129b | ||
![]() |
d49e4ee5ea | ||
![]() |
30f0ea29a3 | ||
![]() |
49930f6565 | ||
![]() |
909d89fa8d | ||
![]() |
81d1d7f544 | ||
![]() |
67b7dff67a | ||
![]() |
4b76d19596 | ||
![]() |
080fbe9feb | ||
![]() |
d3721229bf | ||
![]() |
86c1c9c772 | ||
![]() |
c299817193 | ||
![]() |
fcc1de45ed | ||
![]() |
a29931f2ec | ||
![]() |
3fc6a139ee | ||
![]() |
4dd04975d9 | ||
![]() |
3b4db3ddb7 | ||
![]() |
c4a7117ee8 | ||
![]() |
b4e900fde8 | ||
![]() |
9818d730e4 | ||
![]() |
11a3378659 | ||
![]() |
1bb05f22d3 | ||
![]() |
26b70554c4 | ||
![]() |
93b29d2e83 | ||
![]() |
072986374a | ||
![]() |
2c590d212a | ||
![]() |
6cc863efb3 | ||
![]() |
b832812767 | ||
![]() |
c44558cacd | ||
![]() |
6d83a720cd | ||
![]() |
8d0dd9c448 | ||
![]() |
64020eec49 | ||
![]() |
4dedfb85cb | ||
![]() |
55d694579a | ||
![]() |
86db64edff | ||
![]() |
983563efb6 | ||
![]() |
37abb2db99 | ||
![]() |
5ba0b47e60 | ||
![]() |
e8f2087a6f | ||
![]() |
6ce99f5cdf | ||
![]() |
13c0c9cdd3 | ||
![]() |
58ffdb60d7 | ||
![]() |
ba56a6a2ee | ||
![]() |
ccccf5b1d2 | ||
![]() |
b507ac0a54 | ||
![]() |
e985676c2d | ||
![]() |
f7f4ba6c55 | ||
![]() |
4292f43814 | ||
![]() |
30bd4fd9fe | ||
![]() |
76de39369d | ||
![]() |
88a703ce36 | ||
![]() |
5938f6b7ea | ||
![]() |
5c0c0d6c37 | ||
![]() |
0f15077225 | ||
![]() |
273dd8d388 | ||
![]() |
1795f376ef | ||
![]() |
e7360a7692 | ||
![]() |
e1fc86934f | ||
![]() |
6b8977f178 | ||
![]() |
12c6c73de0 | ||
![]() |
db62a1607b | ||
![]() |
58bb879ef5 | ||
![]() |
254912438a | ||
![]() |
0e48918bcc | ||
![]() |
783ccf8529 | ||
![]() |
8fb2f2dc1d | ||
![]() |
2a8cc283c7 | ||
![]() |
433fe3af9f | ||
![]() |
c2d89c622e | ||
![]() |
02d6ff15fe | ||
![]() |
f2cb7ee7df | ||
![]() |
a2ac44dcc1 | ||
![]() |
3cf9d74efa | ||
![]() |
d5f89a903f | ||
![]() |
496c2242bc | ||
![]() |
98fbff87df | ||
![]() |
ddb51a1c45 | ||
![]() |
8df1155215 | ||
![]() |
53f2a61409 | ||
![]() |
746544f9d5 | ||
![]() |
c65c3df11c | ||
![]() |
b29b8f12b3 | ||
![]() |
d6945677c4 | ||
![]() |
aedf4aea08 | ||
![]() |
dc28d414dc | ||
![]() |
9755bab298 | ||
![]() |
fae4029cfc | ||
![]() |
1790f0d706 | ||
![]() |
0ba2c51676 | ||
![]() |
03cd97b49c | ||
![]() |
16a162c1dd | ||
![]() |
c9bbcf2bf2 | ||
![]() |
86e1bf6078 | ||
![]() |
1bca84ef0b | ||
![]() |
e0f8e1b71a | ||
![]() |
416d69142f | ||
![]() |
426807aeaa | ||
![]() |
90cb075a97 | ||
![]() |
ac2ca8fbd3 | ||
![]() |
69e4a49065 |
6
.github/workflows/pull_request.yml
vendored
6
.github/workflows/pull_request.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: PR to main
|
name: Open a PR to main
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -7,7 +7,7 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
MESSAGE: merge branch `${{ github.head_ref || github.ref_name }}` to `main`
|
MESSAGE: Merge branch `${{ github.head_ref || github.ref_name }}` to `main`
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
pull-request:
|
pull-request:
|
||||||
@@ -15,7 +15,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
- name: Open pull request
|
- name: Open pull request
|
||||||
uses: repo-sync/pull-request@v2
|
uses: repo-sync/pull-request@v2
|
||||||
with:
|
with:
|
||||||
|
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
# Make sure the release step uses its own credentials:
|
# Make sure the release step uses its own credentials:
|
||||||
# https://github.com/cycjimmy/semantic-release-action#private-packages
|
# https://github.com/cycjimmy/semantic-release-action#private-packages
|
||||||
@@ -36,7 +36,7 @@ jobs:
|
|||||||
- name: Build with Gradle
|
- name: Build with Gradle
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: ./gradlew clean --no-daemon
|
run: ./gradlew build clean --no-daemon
|
||||||
- name: Setup semantic-release
|
- name: Setup semantic-release
|
||||||
run: npm install
|
run: npm install
|
||||||
- name: Release
|
- name: Release
|
||||||
|
@@ -7,7 +7,13 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"@semantic-release/commit-analyzer",
|
[
|
||||||
|
"@semantic-release/commit-analyzer", {
|
||||||
|
"releaseRules": [
|
||||||
|
{ "type": "build", "scope": "Needs bump", "release": "patch" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
"@semantic-release/release-notes-generator",
|
"@semantic-release/release-notes-generator",
|
||||||
"@semantic-release/changelog",
|
"@semantic-release/changelog",
|
||||||
"gradle-semantic-release-plugin",
|
"gradle-semantic-release-plugin",
|
||||||
|
561
CHANGELOG.md
561
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
842
api/revanced-patcher.api
Normal file
842
api/revanced-patcher.api
Normal file
File diff suppressed because it is too large
Load Diff
42
arsclib-utils/.gitignore
vendored
42
arsclib-utils/.gitignore
vendored
@@ -1,42 +0,0 @@
|
|||||||
.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
|
|
@@ -1,18 +0,0 @@
|
|||||||
plugins {
|
|
||||||
kotlin("jvm")
|
|
||||||
`maven-publish`
|
|
||||||
}
|
|
||||||
|
|
||||||
group = "app.revanced"
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation("io.github.reandroid:ARSCLib:1.1.7")
|
|
||||||
}
|
|
||||||
|
|
||||||
java {
|
|
||||||
withSourcesJar()
|
|
||||||
}
|
|
||||||
|
|
||||||
kotlin {
|
|
||||||
jvmToolchain(11)
|
|
||||||
}
|
|
@@ -1,72 +0,0 @@
|
|||||||
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.")
|
|
||||||
}
|
|
@@ -1,28 +0,0 @@
|
|||||||
@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()
|
|
||||||
}
|
|
@@ -1,7 +0,0 @@
|
|||||||
package app.revanced.arsc.logging
|
|
||||||
interface Logger {
|
|
||||||
fun error(msg: String)
|
|
||||||
fun warn(msg: String)
|
|
||||||
fun info(msg: String)
|
|
||||||
fun trace(msg: String)
|
|
||||||
}
|
|
@@ -1,166 +0,0 @@
|
|||||||
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))
|
|
||||||
}
|
|
@@ -1,167 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,91 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
@@ -1,100 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,56 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,3 +1,86 @@
|
|||||||
plugins {
|
plugins {
|
||||||
kotlin("jvm") version "1.8.20" apply false
|
kotlin("jvm") version "1.9.10"
|
||||||
|
alias(libs.plugins.binary.compatibility.validator)
|
||||||
|
`maven-publish`
|
||||||
|
signing
|
||||||
|
java
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "app.revanced"
|
||||||
|
|
||||||
|
tasks {
|
||||||
|
processResources {
|
||||||
|
expand("projectVersion" to project.version)
|
||||||
|
}
|
||||||
|
|
||||||
|
test {
|
||||||
|
useJUnitPlatform()
|
||||||
|
testLogging {
|
||||||
|
events("PASSED", "SKIPPED", "FAILED")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
mavenLocal()
|
||||||
|
maven { url = uri("https://jitpack.io") }
|
||||||
|
google()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
|
implementation(libs.xpp3)
|
||||||
|
implementation(libs.smali)
|
||||||
|
implementation(libs.multidexlib2)
|
||||||
|
implementation(libs.apktool.lib)
|
||||||
|
implementation(libs.kotlin.reflect)
|
||||||
|
|
||||||
|
compileOnly(libs.android)
|
||||||
|
|
||||||
|
testImplementation(libs.kotlin.test)
|
||||||
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
withJavadocJar()
|
||||||
|
withSourcesJar()
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(11)
|
||||||
|
}
|
||||||
|
|
||||||
|
publishing {
|
||||||
|
publications {
|
||||||
|
create<MavenPublication>("revanced-patcher-publication") {
|
||||||
|
from(components["java"])
|
||||||
|
|
||||||
|
version = project.version.toString()
|
||||||
|
|
||||||
|
pom {
|
||||||
|
name = "ReVanced Patcher"
|
||||||
|
description = "Patcher used by ReVanced."
|
||||||
|
url = "https://revanced.app"
|
||||||
|
|
||||||
|
licenses {
|
||||||
|
license {
|
||||||
|
name = "GNU General Public License v3.0"
|
||||||
|
url = "https://www.gnu.org/licenses/gpl-3.0.en.html"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
developers {
|
||||||
|
developer {
|
||||||
|
id = "ReVanced"
|
||||||
|
name = "ReVanced"
|
||||||
|
email = "contact@revanced.app"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scm {
|
||||||
|
connection = "scm:git:git://github.com/revanced/revanced-patcher.git"
|
||||||
|
developerConnection = "scm:git:git@github.com:revanced/revanced-patcher.git"
|
||||||
|
url = "https://github.com/revanced/revanced-patcher"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
org.gradle.parallel=true
|
org.gradle.parallel = true
|
||||||
org.gradle.caching=true
|
org.gradle.caching = true
|
||||||
kotlin.code.style = official
|
kotlin.code.style = official
|
||||||
version = 11.0.4
|
version = 18.0.0-dev.5
|
||||||
|
23
gradle/libs.versions.toml
Normal file
23
gradle/libs.versions.toml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
[versions]
|
||||||
|
android = "4.1.1.4"
|
||||||
|
kotlin-reflect = "1.9.0"
|
||||||
|
apktool-lib = "2.9.1"
|
||||||
|
kotlin-test = "1.8.20-RC"
|
||||||
|
kotlinx-coroutines-core = "1.7.3"
|
||||||
|
multidexlib2 = "3.0.3.r3"
|
||||||
|
smali = "3.0.3"
|
||||||
|
xpp3 = "1.1.4c"
|
||||||
|
binary-compatibility-validator = "0.13.2"
|
||||||
|
|
||||||
|
[libraries]
|
||||||
|
android = { module = "com.google.android:android", version.ref = "android" }
|
||||||
|
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin-reflect" }
|
||||||
|
apktool-lib = { module = "app.revanced:apktool", version.ref = "apktool-lib" }
|
||||||
|
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin-test" }
|
||||||
|
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines-core" }
|
||||||
|
multidexlib2 = { module = "app.revanced:multidexlib2", version.ref = "multidexlib2" }
|
||||||
|
smali = { module = "com.android.tools.smali:smali", version.ref = "smali" }
|
||||||
|
xpp3 = { module = "xpp3:xpp3", version.ref = "xpp3" }
|
||||||
|
|
||||||
|
[plugins]
|
||||||
|
binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" }
|
@@ -1,61 +0,0 @@
|
|||||||
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"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1 +0,0 @@
|
|||||||
rootProject.name = "revanced-patcher"
|
|
@@ -1,113 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,222 +0,0 @@
|
|||||||
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)
|
|
@@ -1,55 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,14 +0,0 @@
|
|||||||
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
|
|
||||||
)
|
|
@@ -1,33 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,23 +0,0 @@
|
|||||||
package app.revanced.patcher.annotation
|
|
||||||
|
|
||||||
import app.revanced.patcher.patch.Patch
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Annotation to constrain a [Patch] to compatible packages.
|
|
||||||
* @param compatiblePackages A list of packages a [Patch] is compatible with.
|
|
||||||
*/
|
|
||||||
@Target(AnnotationTarget.CLASS)
|
|
||||||
annotation class Compatibility(
|
|
||||||
val compatiblePackages: Array<Package>,
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Annotation to represent packages a patch can be compatible with.
|
|
||||||
* @param name The package identifier name.
|
|
||||||
* @param versions The versions of the package the [Patch] is compatible with.
|
|
||||||
*/
|
|
||||||
@Target()
|
|
||||||
annotation class Package(
|
|
||||||
val name: String,
|
|
||||||
val versions: Array<String> = [],
|
|
||||||
)
|
|
@@ -1,32 +0,0 @@
|
|||||||
package app.revanced.patcher.annotation
|
|
||||||
|
|
||||||
import app.revanced.patcher.patch.Patch
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Annotation to name a [Patch].
|
|
||||||
* @param name A suggestive name for the [Patch].
|
|
||||||
*/
|
|
||||||
@Target(AnnotationTarget.CLASS)
|
|
||||||
annotation class Name(
|
|
||||||
val name: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Annotation to describe a [Patch].
|
|
||||||
* @param description A description for the [Patch].
|
|
||||||
*/
|
|
||||||
@Target(AnnotationTarget.CLASS)
|
|
||||||
annotation class Description(
|
|
||||||
val description: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Annotation to version a [Patch].
|
|
||||||
* @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,
|
|
||||||
)
|
|
File diff suppressed because it is too large
Load Diff
@@ -1,105 +0,0 @@
|
|||||||
@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)
|
|
||||||
}
|
|
@@ -1,72 +0,0 @@
|
|||||||
package app.revanced.patcher.extensions
|
|
||||||
|
|
||||||
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.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
|
|
||||||
import kotlin.reflect.KVisibility
|
|
||||||
import kotlin.reflect.full.companionObject
|
|
||||||
import kotlin.reflect.full.companionObjectInstance
|
|
||||||
|
|
||||||
object PatchExtensions {
|
|
||||||
/**
|
|
||||||
* The name of a [Patch].
|
|
||||||
*/
|
|
||||||
val PatchClass.patchName: String
|
|
||||||
get() = findAnnotationRecursively(Name::class)?.name ?: this.simpleName
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The version of a [Patch].
|
|
||||||
*/
|
|
||||||
@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 PatchClass.include
|
|
||||||
get() = findAnnotationRecursively(app.revanced.patcher.patch.annotations.Patch::class)!!.include
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The description of a [Patch].
|
|
||||||
*/
|
|
||||||
val PatchClass.description
|
|
||||||
get() = findAnnotationRecursively(Description::class)?.description
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The dependencies of a [Patch].
|
|
||||||
*/
|
|
||||||
val PatchClass.dependencies
|
|
||||||
get() = findAnnotationRecursively(DependsOn::class)?.dependencies
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The packages a [Patch] is compatible with.
|
|
||||||
*/
|
|
||||||
val PatchClass.compatiblePackages
|
|
||||||
get() = findAnnotationRecursively(Compatibility::class)?.compatiblePackages
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Weather or not a [Patch] requires integrations.
|
|
||||||
*/
|
|
||||||
internal val PatchClass.requiresIntegrations
|
|
||||||
get() = findAnnotationRecursively(RequiresIntegrations::class) != null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The options of a [Patch].
|
|
||||||
*/
|
|
||||||
val PatchClass.options: PatchOptions?
|
|
||||||
get() = kotlin.companionObject?.let { cl ->
|
|
||||||
if (cl.visibility != KVisibility.PUBLIC) return null
|
|
||||||
kotlin.companionObjectInstance?.let {
|
|
||||||
(it as? OptionsContainer)?.options
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,20 +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) {}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
@@ -1,18 +0,0 @@
|
|||||||
package app.revanced.patcher.patch
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A container for patch options.
|
|
||||||
*/
|
|
||||||
abstract class OptionsContainer {
|
|
||||||
/**
|
|
||||||
* A list of [PatchOption]s.
|
|
||||||
* @see PatchOptions
|
|
||||||
*/
|
|
||||||
@Suppress("MemberVisibilityCanBePrivate")
|
|
||||||
val options = PatchOptions()
|
|
||||||
|
|
||||||
protected fun <T> option(opt: PatchOption<T>): PatchOption<T> {
|
|
||||||
options.register(opt)
|
|
||||||
return opt
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user