chore: merge branch dev to main (#236)

This commit is contained in:
oSumAtrIX 2023-08-27 01:21:45 +02:00 committed by GitHub
commit 2a3dbafd17
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 1151 additions and 770 deletions

View File

@ -33,9 +33,10 @@ jobs:
build build
node_modules node_modules
key: ${{ runner.os }}-gradle-npm-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', 'package-lock.json') }} key: ${{ runner.os }}-gradle-npm-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', 'package-lock.json') }}
- name: Build with Gradle - name: Build
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Cleaning is necessary to avoid uploading two identical artifacts with different versions
run: ./gradlew clean --no-daemon run: ./gradlew clean --no-daemon
- name: Setup semantic-release - name: Setup semantic-release
run: npm install run: npm install

View File

@ -10,7 +10,7 @@
[ [
"@semantic-release/commit-analyzer", { "@semantic-release/commit-analyzer", {
"releaseRules": [ "releaseRules": [
{ "type": "build", "scope": "revanced-patcher", "release": "patch" } { "type": "build", "scope": "Needs bump", "release": "patch" }
] ]
} }
], ],

View File

@ -1,3 +1,118 @@
# [3.0.0-dev.10](https://github.com/ReVanced/revanced-cli/compare/v3.0.0-dev.9...v3.0.0-dev.10) (2023-08-25)
### Bug Fixes
* filtration of patches malfunctioning ([2d5a7fd](https://github.com/ReVanced/revanced-cli/commit/2d5a7fdf1eb2e13f5013a790b03f09851b167fe0))
# [3.0.0-dev.9](https://github.com/ReVanced/revanced-cli/compare/v3.0.0-dev.8...v3.0.0-dev.9) (2023-08-25)
### Features
* Check for missing integrations ([c93186f](https://github.com/ReVanced/revanced-cli/commit/c93186fb9700907e65f33442e88073783cc163de))
# [3.0.0-dev.8](https://github.com/ReVanced/revanced-cli/compare/v3.0.0-dev.7...v3.0.0-dev.8) (2023-08-24)
### Bug Fixes
* do not delete output file ([0f3e090](https://github.com/ReVanced/revanced-cli/commit/0f3e090418771e951dfd15e5c193421f72cbe459))
# [3.0.0-dev.7](https://github.com/ReVanced/revanced-cli/compare/v3.0.0-dev.6...v3.0.0-dev.7) (2023-08-24)
### Bug Fixes
* print stack trace when a patch failed ([924c1f8](https://github.com/ReVanced/revanced-cli/commit/924c1f80ec0d17a3bdc07a0fb2015e44c49162e4))
# [3.0.0-dev.6](https://github.com/ReVanced/revanced-cli/compare/v3.0.0-dev.5...v3.0.0-dev.6) (2023-08-24)
# [3.0.0-dev.5](https://github.com/ReVanced/revanced-cli/compare/v3.0.0-dev.4...v3.0.0-dev.5) (2023-08-24)
### Bug Fixes
* also delete temporary files when uninstalling ([52c3be2](https://github.com/ReVanced/revanced-cli/commit/52c3be23f2915dccaee7f9941413c8f81e14acc8))
* delete temporary files after root installation ([a3d8705](https://github.com/ReVanced/revanced-cli/commit/a3d8705e89732a0dd4f51de28c405b6af13c8633))
* fix running commands not running ([2c7fcaf](https://github.com/ReVanced/revanced-cli/commit/2c7fcaf4add65a12052afc5bef779dbc73debd69))
* only check once for patch options ([11c3a6c](https://github.com/ReVanced/revanced-cli/commit/11c3a6cfd4fe59ba5d703358634a1853e1cc22a5))
### Features
* add install command ([0350b7f](https://github.com/ReVanced/revanced-cli/commit/0350b7f1a276d9dc795b22442ba4f202855ea090))
* use friendly descriptions ([3dd875d](https://github.com/ReVanced/revanced-cli/commit/3dd875d14cca488ade6d21bbd4cce0d481692134))
# [3.0.0-dev.4](https://github.com/ReVanced/revanced-cli/compare/v3.0.0-dev.3...v3.0.0-dev.4) (2023-08-24)
### Features
* properly make use of logging facade ([41898d7](https://github.com/ReVanced/revanced-cli/commit/41898d7547690e3130372414515c5645e5dc2634))
# [3.0.0-dev.3](https://github.com/ReVanced/revanced-cli/compare/v3.0.0-dev.2...v3.0.0-dev.3) (2023-08-23)
# [3.0.0-dev.2](https://github.com/ReVanced/revanced-cli/compare/v3.0.0-dev.1...v3.0.0-dev.2) (2023-08-23)
### Bug Fixes
* specify correct class containing entry-point ([1fcc591](https://github.com/ReVanced/revanced-cli/commit/1fcc591222ab67112f2b78174a8b94106846838c))
# [3.0.0-dev.1](https://github.com/ReVanced/revanced-cli/compare/v2.23.0-dev.5...v3.0.0-dev.1) (2023-08-23)
### Bug Fixes
* do not use absolute path from custom AAPT2 binary option ([a9c2a5f](https://github.com/ReVanced/revanced-cli/commit/a9c2a5f096627dbbf8ab1b8da26fb14529ce6bc3))
* use correct option name ([f8972ea](https://github.com/ReVanced/revanced-cli/commit/f8972eac3e5ee0a4a186c12cbe711925656d657b))
* refactor!: restructure code ([07da528](https://github.com/ReVanced/revanced-cli/commit/07da528ce2223582f84bf64d2fec69714c647ddc))
### Features
* add options command ([9edbbf3](https://github.com/ReVanced/revanced-cli/commit/9edbbf31635603f89fc7bc5dcc6c023d4cdbb5a6))
* use better logging text ([b0e748d](https://github.com/ReVanced/revanced-cli/commit/b0e748daff527ee7f417b3069882e074896fc131))
* use separate command to list patches ([b74213f](https://github.com/ReVanced/revanced-cli/commit/b74213f66e0d04d3a0ae6197d069631388e06580))
* use separate command to patch ([32da961](https://github.com/ReVanced/revanced-cli/commit/32da961d57537e99b39fd92b625a1c73f8314bc6))
* use separate command to uninstall ([c0cc909](https://github.com/ReVanced/revanced-cli/commit/c0cc90962646cfffd5e2730ae556423271a7990b))
* use simpler log ([ba758f0](https://github.com/ReVanced/revanced-cli/commit/ba758f00f4ce18791439b7e72fe1ad2e7f11f8af))
### BREAKING CHANGES
* This introduces major changes to how ReVanced CLI is used from the command line.
# [2.23.0-dev.5](https://github.com/ReVanced/revanced-cli/compare/v2.23.0-dev.4...v2.23.0-dev.5) (2023-08-14)
# [2.23.0-dev.4](https://github.com/ReVanced/revanced-cli/compare/v2.23.0-dev.3...v2.23.0-dev.4) (2023-08-13)
### Features
* show full package name when listing patches ([#240](https://github.com/ReVanced/revanced-cli/issues/240)) ([7174364](https://github.com/ReVanced/revanced-cli/commit/7174364ef8ef5d6ce8351a8340f9c1a5b58eac3c))
# [2.23.0-dev.3](https://github.com/ReVanced/revanced-cli/compare/v2.23.0-dev.2...v2.23.0-dev.3) (2023-08-03)
# [2.23.0-dev.2](https://github.com/ReVanced/revanced-cli/compare/v2.23.0-dev.1...v2.23.0-dev.2) (2023-08-03)
# [2.23.0-dev.1](https://github.com/ReVanced/revanced-cli/compare/v2.22.1-dev.1...v2.23.0-dev.1) (2023-07-30)
### Features
* Improve command line argument descriptions ([f9cf7d2](https://github.com/ReVanced/revanced-cli/commit/f9cf7d21b7f1c2f11234d604a1047b9d2b165f83))
## [2.22.1-dev.1](https://github.com/ReVanced/revanced-cli/compare/v2.22.0...v2.22.1-dev.1) (2023-07-24)
### Bug Fixes
* print original instead of kebab cased names ([5eaad33](https://github.com/ReVanced/revanced-cli/commit/5eaad33dc1fbd24c36e1498f04e21d068e85f53e))
# [2.22.0](https://github.com/revanced/revanced-cli/compare/v2.21.5...v2.22.0) (2023-07-11) # [2.22.0](https://github.com/revanced/revanced-cli/compare/v2.21.5...v2.22.0) (2023-07-11)

View File

@ -1,3 +1,3 @@
# 💻 ReVanced CLI # 💻 ReVanced CLI
Command line application as an alternative to the ReVanced Manager. Command line application to use ReVanced.

View File

@ -1,42 +1,23 @@
plugins { plugins {
kotlin("jvm") version "1.8.20" kotlin("jvm") version "1.8.20"
id("com.github.johnrengelman.shadow") version "7.1.2" alias(libs.plugins.shadow)
} }
group = "app.revanced" 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()
mavenLocal()
maven {
url = uri("https://maven.pkg.github.com/revanced/revanced-patcher")
credentials {
username = githubUsername
password = githubPassword
}
}
maven { url = uri("https://jitpack.io") }
google()
}
dependencies { dependencies {
implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.20-RC") implementation(libs.revanced.patcher)
implementation(libs.kotlin.reflect)
implementation("app.revanced:revanced-patcher:11.0.3") implementation(libs.kotlinx.coroutines.core)
implementation("info.picocli:picocli:4.7.1") implementation(libs.picocli)
implementation("com.github.revanced:jadb:2531a28109") // updated fork implementation(libs.jadb) // Updated fork
implementation("com.android.tools.build:apksig:8.1.0-alpha09") implementation(libs.apksig)
implementation("org.bouncycastle:bcpkix-jdk15on:1.70") implementation(libs.bcpkix.jdk15on)
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.14.+") implementation(libs.jackson.module.kotlin)
testImplementation("org.jetbrains.kotlin:kotlin-test:1.8.20-RC") testImplementation(libs.kotlin.test)
} }
kotlin { kotlin { jvmToolchain(11) }
jvmToolchain(11)
}
tasks { tasks {
test { test {
@ -45,12 +26,14 @@ tasks {
events("PASSED", "SKIPPED", "FAILED") events("PASSED", "SKIPPED", "FAILED")
} }
} }
build {
dependsOn(shadowJar) processResources {
expand("projectVersion" to project.version)
} }
shadowJar { shadowJar {
manifest { manifest {
attributes("Main-Class" to "app.revanced.cli.main.MainKt") attributes("Main-Class" to "app.revanced.cli.command.MainCommandKt")
} }
minimize { minimize {
exclude(dependency("org.jetbrains.kotlin:.*")) exclude(dependency("org.jetbrains.kotlin:.*"))
@ -58,6 +41,11 @@ tasks {
exclude(dependency("app.revanced:.*")) exclude(dependency("app.revanced:.*"))
} }
} }
build {
dependsOn(shadowJar)
}
// Dummy task to fix the Gradle semantic-release plugin. // Dummy task to fix the Gradle semantic-release plugin.
// Remove this if you forked it to support building only. // Remove this if you forked it to support building only.
// Tracking issue: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435 // Tracking issue: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435

View File

@ -1,15 +1,17 @@
# 💼 Prerequisites # 💼 Prerequisites
To use ReVanced CLI, you will need to fulfill certain requirements. To use ReVanced CLI, you will need to fulfil specific requirements.
## 🤝 Requirements ## 🤝 Requirements
- Java SDK 11 (Azul JDK or OpenJDK) - Java SDK 11 (Azul Zulu JDK or OpenJDK)
- [Android Debug Bridge (adb)](https://developer.android.com/studio/command-line/adb) if you want to deploy the patched APK file on your device - [Android Debug Bridge (adb)](https://developer.android.com/studio/command-line/adb) if you want to install the patched APK file on your device
- An ABI other than ARMv7 such as x86 or x86-64 (or a custom AAPT binary that supports ARMv7) - An ABI other than ARMv7 such as x86 or x86-64 (or a custom AAPT binary that supports ARMv7)
- ReVanced Patches
- ReVanced Integrations, if the patches require it
## ⏭️ Whats next ## ⏭️ Whats next
The next section will show, how to use [ReVanced CLI](https://github.com/revanced/revanced-cli). The following section will show you how to use ReVanced CLI.
Continue: [🛠️ Using ReVanced CLI](1_usage.md) Continue: [🛠️ Using ReVanced CLI](1_usage.md)

View File

@ -10,13 +10,14 @@ Learn how to ReVanced CLI.
adb shell exit adb shell exit
``` ```
If you want to deploy the patched APK file on your device by mounting it on top of the original APK file, you will need root access. This is optional. Optionally, you can install the patched APK file on your device by mounting it on top of the original APK file.
You will need root permissions for this. Check if you have root permissions by running the following command:
```bash ```bash
adb shell su -c exit adb shell su -c exit
``` ```
2. Get the name of your device 2. Get your device's serial
```bash ```bash
adb devices adb devices
@ -30,47 +31,81 @@ Learn how to ReVanced CLI.
java -jar revanced-cli.jar -h java -jar revanced-cli.jar -h
``` ```
- ### 📃 List all available patches from supplied patch bundles - ### 📃 List patches from supplied patch bundles
```bash ```bash
java -jar revanced-cli.jar java -jar revanced-cli.jar list-patches \
-b revanced-patches.jar \ --with-packages \
-l # Names of all patches will be in kebab-case --with-versions \
--with-options \
revanced-patches.jar [<patch-bundle> ...]
``` ```
- ### 💉 Use ReVanced CLI to patch an APK file but deploy without root permissions - ### ⚙️ Generate options from patches using ReVanced CLI
This will deploy the patched APK file on your device by installing it. This will generate an `options.json` file for the patches from a list of supplied patch bundles.
The file can be supplied to ReVanced CLI later on.
- ```bash
java -jar revanced-cli.jar options \
--path options.json \
--overwrite \
revanced-patches.jar [<patch-bundle> ...]
```
> **Note**: A default `options.json` file will be automatically generated, if it does not exist
without any need for intervention when using the `patch` command.
- ### 💉 Use ReVanced CLI to patch an APK file but install without root permissions
This will install the patched APK file regularly on your device.
```bash ```bash
java -jar revanced-cli.jar \ java -jar revanced-cli.jar patch \
-a input.apk \ --patch-bundle revanced-patches.jar \
-o patched-output.apk \ --out output.apk \
-b revanced-patches.jar \ --device-serial <device-serial> \
-d device-name input.apk
``` ```
- ### 👾 Use ReVanced CLI to patch an APK file but deploy with root permissions - ### 👾 Use ReVanced CLI to patch an APK file but install with root permissions
This will deploy the patched APK file on your device by mounting it on top of the original APK file. This will install the patched APK file on your device by mounting it on top of the original APK file.
```bash ```bash
adb install input.apk adb install input.apk
java -jar revanced-cli.jar \ java -jar revanced-cli.jar patch \
-a input.apk \ --patch-bundle revanced-patches.jar \
-o patched-output.apk \ --include some-other-patch \
-b revanced-patches.jar \ --exclude some-patch \
-e vanced-microg-support \ --out patched-output.apk \
-d device-name \ --device-serial <device-serial> \
--mount --mount \
input.apk
``` ```
> **Note**: Some patches from [ReVanced Patches](https://github.com/revanced/revanced-patches) also require [ReVanced Integrations](https://github.com/revanced/revanced-integrations). Supply them with the option `-m`. ReVanced Patcher will merge ReVanced Integrations automatically, depending on if the supplied patches require them. > **Note**: Some patches may require integrations
such as [ReVanced Integrations](https://github.com/revanced/revanced-integrations).
Supply them with the option `--merge`. If any patches accepted by ReVanced Patcher require ReVanced Integrations,
they will be merged into the APK file automatically.
- ### ⚙️ Supply options to patches using ReVanced CLI - ### 🗑️ Uninstall a patched APK file
```bash
java -jar revanced-cli.jar utility uninstall \
--package-name <package-name> \
<device-serial>
```
Some patches provide options. Currently, ReVanced CLI will generate and consume an `options.json` file at the location that is specified in `-o`. If the option is not specified, the options file will be generated in the current working directory. > **Note**: You can unmount an APK file
with the option `--unmount`.
The options file contains all options from supplied patch bundles.
> **Note**: The `options.json` file will be generated at the first time you use ReVanced CLI to patch an APK file for now. This will be changed in the future. - ### ⚙️ Manually install an APK file
```bash
java -jar revanced-cli.jar utility install \
-a input.apk \
<device-serial>
```
> **Note**: You can mount an APK file
by supplying the package name of the app to mount the supplied APK file to over the option `--mount`.

View File

@ -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 = 2.22.0 version = 3.0.0-dev.10

25
gradle/libs.versions.toml Normal file
View File

@ -0,0 +1,25 @@
[versions]
shadow = "8.1.1"
apksig = "8.1.0"
bcpkix-jdk15on = "1.70"
jackson-module-kotlin = "2.14.3"
jadb = "2531a28109"
kotlin-reflect = "1.9.0"
kotlin-test = "1.8.20-RC"
kotlinx-coroutines-core = "1.7.1"
picocli = "4.7.3"
revanced-patcher = "14.1.0"
[libraries]
apksig = { module = "com.android.tools.build:apksig", version.ref = "apksig" }
bcpkix-jdk15on = { module = "org.bouncycastle:bcpkix-jdk15on", version.ref = "bcpkix-jdk15on" }
jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson-module-kotlin" }
jadb = { module = "com.github.revanced:jadb", version.ref = "jadb" }
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin-reflect" }
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" }
picocli = { module = "info.picocli:picocli", version.ref = "picocli" }
revanced-patcher = { module = "app.revanced:revanced-patcher", version.ref = "revanced-patcher" }
[plugins]
shadow = { id = "com.github.johnrengelman.shadow", version.ref = "shadow" }

View File

@ -1 +1,23 @@
val githubUsername: String = providers.gradleProperty("gpr.user").orNull ?: System.getenv("GITHUB_ACTOR")
val githubPassword: String = providers.gradleProperty("gpr.key").orNull ?: System.getenv("GITHUB_TOKEN")
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
mavenCentral()
mavenLocal()
google()
maven { url = uri("https://jitpack.io") }
listOf("revanced-patcher", "jadb").forEach { repo ->
maven {
url = uri("https://maven.pkg.github.com/revanced/$repo")
credentials {
username = githubUsername
password = githubPassword
}
}
}
}
}
rootProject.name = "revanced-cli" rootProject.name = "revanced-cli"

View File

@ -1,37 +0,0 @@
package app.revanced.cli.aligning
import app.revanced.cli.command.MainCommand.logger
import app.revanced.patcher.PatcherResult
import app.revanced.utils.signing.align.ZipAligner
import app.revanced.utils.signing.align.zip.ZipFile
import app.revanced.utils.signing.align.zip.structures.ZipEntry
import java.io.File
object Aligning {
fun align(result: PatcherResult, inputFile: File, outputFile: File) {
logger.info("Aligning ${inputFile.name} to ${outputFile.name}")
if (outputFile.exists()) outputFile.delete()
ZipFile(outputFile).use { file ->
result.dexFiles.forEach {
file.addEntryCompressData(
ZipEntry.createWithName(it.name),
it.stream.readBytes()
)
}
result.resourceFile?.let {
file.copyEntriesFromFileAligned(
ZipFile(it),
ZipAligner::getEntryAlignment
)
}
file.copyEntriesFromFileAligned(
ZipFile(inputFile),
ZipAligner::getEntryAlignment
)
}
}
}

View File

@ -0,0 +1,91 @@
package app.revanced.cli.command
import app.revanced.patcher.PatchBundleLoader
import app.revanced.patcher.annotation.Package
import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages
import app.revanced.patcher.extensions.PatchExtensions.description
import app.revanced.patcher.extensions.PatchExtensions.options
import app.revanced.patcher.extensions.PatchExtensions.patchName
import app.revanced.patcher.patch.PatchClass
import app.revanced.patcher.patch.PatchOption
import picocli.CommandLine.*
import picocli.CommandLine.Help.Visibility.ALWAYS
import java.io.File
import java.util.logging.Logger
@Command(name = "list-patches", description = ["List patches from supplied patch bundles"])
internal object ListPatchesCommand : Runnable {
private val logger = Logger.getLogger(ListPatchesCommand::class.java.name)
@Parameters(
description = ["Paths to patch bundles"], arity = "1..*"
)
private lateinit var patchBundles: Array<File>
@Option(
names = ["-d", "--with-descriptions"], description = ["List their descriptions"], showDefaultValue = ALWAYS
)
private var withDescriptions: Boolean = true
@Option(
names = ["-p", "--with-packages"],
description = ["List the packages the patches are compatible with"],
showDefaultValue = ALWAYS
)
private var withPackages: Boolean = false
@Option(
names = ["-v", "--with-versions"],
description = ["List the versions of the apps the patches are compatible with"],
showDefaultValue = ALWAYS
)
private var withVersions: Boolean = false
@Option(
names = ["-o", "--with-options"], description = ["List the options of the patches"], showDefaultValue = ALWAYS
)
private var withOptions: Boolean = false
override fun run() {
fun Package.buildString() = buildString {
if (withVersions && versions.isNotEmpty()) {
appendLine("Package name: $name")
appendLine("Compatible versions:")
append(versions.joinToString("\n") { version -> version }.prependIndent("\t"))
} else append("Package name: $name")
}
fun PatchOption<*>.buildString() = buildString {
appendLine("Title: $title")
appendLine("Description: $description")
value?.let {
appendLine("Key: $key")
append("Value: $it")
} ?: append("Key: $key")
}
fun PatchClass.buildString() = buildString {
append("Name: $patchName")
if (withDescriptions) append("\nDescription: $description")
if (withOptions && options != null) {
appendLine("\nOptions:")
append(
options!!.joinToString("\n\n") { option -> option.buildString() }.prependIndent("\t")
)
}
if (withPackages && compatiblePackages != null) {
appendLine("\nCompatible packages:")
append(
compatiblePackages!!.joinToString("\n") { it.buildString() }.prependIndent("\t")
)
}
}
logger.info(PatchBundleLoader.Jar(*patchBundles).joinToString("\n\n") { it.buildString() })
}
}

View File

@ -1,265 +1,67 @@
package app.revanced.cli.command package app.revanced.cli.command
import app.revanced.cli.aligning.Aligning import app.revanced.cli.command.utility.UtilityCommand
import app.revanced.cli.logging.impl.DefaultCliLogger import app.revanced.patcher.patch.PatchClass
import app.revanced.cli.patcher.Patcher import picocli.CommandLine
import app.revanced.cli.patcher.logging.impl.PatcherLogger import picocli.CommandLine.Command
import app.revanced.cli.signing.Signing import picocli.CommandLine.IVersionProvider
import app.revanced.cli.signing.SigningOptions import java.util.*
import app.revanced.patcher.PatcherOptions import java.util.logging.*
import app.revanced.patcher.data.Context
import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages
import app.revanced.patcher.extensions.PatchExtensions.description
import app.revanced.patcher.extensions.PatchExtensions.patchName
import app.revanced.patcher.patch.Patch
import app.revanced.patcher.util.patch.PatchBundle
import app.revanced.utils.Options
import app.revanced.utils.Options.setOptions
import app.revanced.utils.adb.Adb
import picocli.CommandLine.*
import java.io.File
import java.nio.file.Files
/**
* Alias for return type of [PatchBundle.loadPatches].
*/
internal typealias PatchList = List<Class<out Patch<Context>>>
private class CLIVersionProvider : IVersionProvider { fun main(args: Array<String>) {
override fun getVersion() = arrayOf( System.setProperty("java.util.logging.SimpleFormatter.format", "%4\$s: %5\$s %n")
MainCommand::class.java.`package`.implementationVersion ?: "unknown" Logger.getLogger("").apply {
) handlers.forEach {
it.close()
removeHandler(it)
}
object : Handler() {
override fun publish(record: LogRecord) = formatter.format(record).toByteArray().let {
if (record.level.intValue() > Level.INFO.intValue())
System.err.write(it)
else
System.out.write(it)
}
override fun flush() {
System.out.flush()
System.err.flush()
}
override fun close() = flush()
}.also {
it.level = Level.ALL
it.formatter = SimpleFormatter()
}.let(::addHandler)
}
CommandLine(MainCommand).execute(*args)
}
internal typealias PatchList = List<PatchClass>
private object CLIVersionProvider : IVersionProvider {
override fun getVersion(): Array<String> {
Properties().apply {
load(MainCommand::class.java.getResourceAsStream("/app/revanced/cli/version.properties"))
}.let {
return arrayOf("ReVanced CLI v${it.getProperty("version")}")
}
}
} }
@Command( @Command(
name = "ReVanced-CLI", name = "revanced-cli",
description = ["Command line application to use ReVanced"],
mixinStandardHelpOptions = true, mixinStandardHelpOptions = true,
versionProvider = CLIVersionProvider::class versionProvider = CLIVersionProvider::class,
subcommands = [
ListPatchesCommand::class,
PatchCommand::class,
OptionsCommand::class,
UtilityCommand::class,
]
) )
internal object MainCommand : Runnable { private object MainCommand
val logger = DefaultCliLogger()
@ArgGroup(exclusive = false, multiplicity = "1")
lateinit var args: Args
class Args {
// TODO: Move this so it is not required when listing patches
@Option(names = ["-a", "--apk"], description = ["Input APK file to be patched"], required = true)
lateinit var inputFile: File
@Option(names = ["--uninstall"], description = ["Uninstall the mount variant"])
var uninstall: Boolean = false
@Option(
names = ["-d", "--deploy-on"],
description = ["If specified, deploy to device over ADB with given name"]
)
var deploy: String? = null
@ArgGroup(exclusive = false)
var patchArgs: PatchArgs? = null
}
class PatchArgs {
@Option(names = ["-b", "--bundle"], description = ["One or more bundles of patches"], required = true)
var patchBundles = arrayOf<String>()
@Option(names = ["--options"], description = ["Path to patch options JSON file"])
var optionsFile: File = File("options.json")
@ArgGroup(exclusive = false)
var listingArgs: ListingArgs? = null
@ArgGroup(exclusive = false)
var patchingArgs: PatchingArgs? = null
}
class ListingArgs {
@Option(names = ["-l", "--list"], description = ["List patches"], required = true)
var listOnly: Boolean = false
@Option(names = ["--with-versions"], description = ["List patches with compatible versions"])
var withVersions: Boolean = false
@Option(names = ["--with-packages"], description = ["List patches with compatible packages"])
var withPackages: Boolean = false
}
class PatchingArgs {
@Option(names = ["-o", "--out"], description = ["Output file path"], required = true)
lateinit var outputPath: String
@Option(names = ["-e", "--exclude"], description = ["Explicitly exclude patches"])
var excludedPatches = arrayOf<String>()
@Option(
names = ["--exclusive"],
description = ["Only installs the patches you include, not including any patch by default"]
)
var exclusive = false
@Option(names = ["-i", "--include"], description = ["Include patches"])
var includedPatches = arrayOf<String>()
@Option(names = ["--experimental"], description = ["Disable patch version compatibility patch"])
var experimental: Boolean = false
@Option(names = ["-m", "--merge"], description = ["One or more dex file containers to merge"])
var mergeFiles = listOf<File>()
@Option(names = ["--mount"], description = ["If specified, instead of installing, mount"])
var mount: Boolean = false
@Option(names = ["--cn"], description = ["Overwrite the default CN for the signed file"])
var cn = "ReVanced"
@Option(names = ["--keystore"], description = ["File path to your keystore"])
var keystorePath: String? = null
@Option(names = ["-p", "--password"], description = ["Overwrite the default password for the signed file"])
var password = "ReVanced"
@Option(names = ["-t", "--temp-dir"], description = ["Temporary resource cache directory"])
var cacheDirectory = "revanced-cache"
@Option(
names = ["-c", "--clean"],
description = ["Clean the temporary resource cache directory. This will be done anyways when running the patcher"]
)
var clean: Boolean = false
@Option(names = ["--custom-aapt2-binary"], description = ["Path to custom aapt2 binary"])
var aaptPath: String = ""
}
override fun run() {
if (args.patchArgs?.listingArgs?.listOnly == true) return printListOfPatches()
if (args.uninstall) return uninstall()
val pArgs = this.args.patchArgs?.patchingArgs ?: return
val outputFile = File(pArgs.outputPath) // the file to write to
val allPatches = args.patchArgs!!.patchBundles.flatMap { bundle ->
PatchBundle.Jar(bundle).loadPatches()
}
args.patchArgs!!.optionsFile.let {
if (it.exists()) allPatches.setOptions(it, logger)
else Options.serialize(allPatches, prettyPrint = true).let(it::writeText)
}
val patcher = app.revanced.patcher.Patcher(
PatcherOptions(
args.inputFile.also { if (!it.exists()) return logger.error("Input file ${args.inputFile} does not exist.") },
pArgs.cacheDirectory,
pArgs.aaptPath,
pArgs.cacheDirectory,
PatcherLogger
)
)
// prepare adb
val adb: Adb? = args.deploy?.let {
Adb(outputFile, patcher.context.packageMetadata.packageName, args.deploy!!, !pArgs.mount)
}
// start the patcher
val result = Patcher.start(patcher, allPatches)
val cacheDirectory = File(pArgs.cacheDirectory)
// align the file
val alignedFile = cacheDirectory.resolve("${outputFile.nameWithoutExtension}_aligned.apk")
Aligning.align(result, args.inputFile, alignedFile)
// sign the file
val finalFile = if (!pArgs.mount) {
val signedOutput = cacheDirectory.resolve("${outputFile.nameWithoutExtension}_signed.apk")
Signing.sign(
alignedFile,
signedOutput,
SigningOptions(
pArgs.cn,
pArgs.password,
pArgs.keystorePath ?: outputFile.absoluteFile.parentFile
.resolve("${outputFile.nameWithoutExtension}.keystore")
.canonicalPath
)
)
signedOutput
} else
alignedFile
// finally copy to the specified output file
logger.info("Copying ${finalFile.name} to ${outputFile.name}")
finalFile.copyTo(outputFile, overwrite = true)
// clean up the cache directory if needed
if (pArgs.clean)
cleanUp(pArgs.cacheDirectory)
// deploy if specified
adb?.deploy()
if (pArgs.clean && args.deploy != null) Files.delete(outputFile.toPath())
logger.info("Finished")
}
private fun cleanUp(cacheDirectory: String) {
val result = if (File(cacheDirectory).deleteRecursively())
"Cleaned up cache directory"
else
"Failed to clean up cache directory"
logger.info(result)
}
private fun uninstall() {
val adb: Adb? = args.deploy?.let {
Adb(
File("placeholder_file"),
app.revanced.patcher.Patcher(PatcherOptions(args.inputFile, "")).context.packageMetadata.packageName,
args.deploy!!,
false
)
}
adb?.uninstall()
}
private fun printListOfPatches() {
val logged = mutableListOf<String>()
for (patchBundlePath in args.patchArgs?.patchBundles!!) for (patch in PatchBundle.Jar(patchBundlePath)
.loadPatches()) {
if (patch.patchName in logged) continue
for (compatiblePackage in patch.compatiblePackages ?: continue) {
val packageEntryStr = buildString {
// Add package if flag is set
if (args.patchArgs?.listingArgs?.withPackages == true) {
val packageName = compatiblePackage.name.substringAfterLast(".").padStart(10)
append(packageName)
append("\t")
}
// Add patch name
val patchName = patch.patchName.lowercase().replace(" ", "-").padStart(25)
append(patchName)
// Add description if flag is set.
append("\t")
append(patch.description)
// Add compatible versions, if flag is set
if (args.patchArgs?.listingArgs?.withVersions == true) {
val compatibleVersions = compatiblePackage.versions.joinToString(separator = ", ")
append("\t")
append(compatibleVersions)
}
}
logged.add(patch.patchName)
logger.info(packageEntryStr)
}
}
}
}

View File

@ -0,0 +1,46 @@
package app.revanced.cli.command
import app.revanced.patcher.PatchBundleLoader
import app.revanced.utils.Options
import app.revanced.utils.Options.setOptions
import picocli.CommandLine
import picocli.CommandLine.Help.Visibility.ALWAYS
import java.io.File
import java.util.logging.Logger
@CommandLine.Command(
name = "options",
description = ["Generate options file from patches"],
)
internal object OptionsCommand : Runnable {
private val logger = Logger.getLogger(OptionsCommand::class.java.name)
@CommandLine.Parameters(
description = ["Paths to patch bundles"], arity = "1..*"
)
private lateinit var patchBundles: Array<File>
@CommandLine.Option(
names = ["-p", "--path"], description = ["Path to patch options JSON file"], showDefaultValue = ALWAYS
)
private var path: File = File("options.json")
@CommandLine.Option(
names = ["-o", "--overwrite"], description = ["Overwrite existing options file"], showDefaultValue = ALWAYS
)
private var overwrite: Boolean = false
@CommandLine.Option(
names = ["-u", "--update"],
description = ["Update existing options by adding missing and removing non-existent options"],
showDefaultValue = ALWAYS
)
private var update: Boolean = false
override fun run() = if (!path.exists() || overwrite) with(PatchBundleLoader.Jar(*patchBundles)) {
if (update) setOptions(path)
Options.serialize(this, prettyPrint = true).let(path::writeText)
}
else logger.severe("Options file already exists, use --override to override it")
}

View File

@ -0,0 +1,342 @@
package app.revanced.cli.command
import app.revanced.patcher.PatchBundleLoader
import app.revanced.patcher.Patcher
import app.revanced.patcher.PatcherOptions
import app.revanced.patcher.PatcherResult
import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages
import app.revanced.patcher.extensions.PatchExtensions.include
import app.revanced.patcher.extensions.PatchExtensions.patchName
import app.revanced.utils.Options
import app.revanced.utils.Options.setOptions
import app.revanced.utils.adb.AdbManager
import app.revanced.utils.align.ZipAligner
import app.revanced.utils.align.zip.ZipFile
import app.revanced.utils.align.zip.structures.ZipEntry
import app.revanced.utils.signing.ApkSigner
import app.revanced.utils.signing.SigningOptions
import kotlinx.coroutines.runBlocking
import picocli.CommandLine
import picocli.CommandLine.Help.Visibility.ALWAYS
import java.io.File
import java.io.PrintWriter
import java.io.StringWriter
import java.util.logging.Logger
@CommandLine.Command(
name = "patch", description = ["Patch the supplied APK file with the supplied patches and integrations"]
)
internal object PatchCommand : Runnable {
private val logger = Logger.getLogger(PatchCommand::class.java.name)
@CommandLine.Parameters(
description = ["APK file to be patched"], arity = "1..1"
)
private lateinit var apk: File
@CommandLine.Option(
names = ["-b", "--patch-bundle"], description = ["One or more bundles of patches"], required = true
)
private var patchBundles = emptyList<File>()
@CommandLine.Option(
names = ["-m", "--merge"], description = ["One or more DEX files or containers to merge into the APK"]
)
private var integrations = listOf<File>()
@CommandLine.Option(
names = ["-i", "--include"], description = ["List of patches to include"]
)
private var includedPatches = arrayOf<String>()
@CommandLine.Option(
names = ["-e", "--exclude"], description = ["List of patches to exclude"]
)
private var excludedPatches = arrayOf<String>()
@CommandLine.Option(
names = ["--options"], description = ["Path to patch options JSON file"], showDefaultValue = ALWAYS
)
private var optionsFile: File = File("options.json")
@CommandLine.Option(
names = ["--exclusive"],
description = ["Only include patches that are explicitly specified to be included"],
showDefaultValue = ALWAYS
)
private var exclusive = false
@CommandLine.Option(
names = ["-f","--force"],
description = ["Force inclusion of patches that are incompatible with the supplied APK file's version"],
showDefaultValue = ALWAYS
)
private var force: Boolean = false
@CommandLine.Option(
names = ["-o", "--out"], description = ["Path to save the patched APK file to"], required = true
)
private lateinit var outputFilePath: File
@CommandLine.Option(
names = ["-d", "--device-serial"], description = ["ADB device serial to install to"], showDefaultValue = ALWAYS
)
private var deviceSerial: String? = null
@CommandLine.Option(
names = ["--mount"], description = ["Install by mounting the patched APK file"], showDefaultValue = ALWAYS
)
private var mount: Boolean = false
@CommandLine.Option(
names = ["--common-name"],
description = ["The common name of the signer of the patched APK file"],
showDefaultValue = ALWAYS
)
private var commonName = "ReVanced"
@CommandLine.Option(
names = ["--keystore"], description = ["Path to the keystore to sign the patched APK file with"]
)
private var keystorePath: String? = null
@CommandLine.Option(
names = ["--password"], description = ["The password of the keystore to sign the patched APK file with"]
)
private var password = "ReVanced"
@CommandLine.Option(
names = ["-r", "--resource-cache"],
description = ["Path to temporary resource cache directory"],
showDefaultValue = ALWAYS
)
private var resourceCachePath = File("revanced-resource-cache")
@CommandLine.Option(
names = ["--custom-aapt2-binary"], description = ["Path to a custom AAPT binary to compile resources with"]
)
private var aaptBinaryPath = File("")
@CommandLine.Option(
names = ["-p", "--purge"],
description = ["Purge the temporary resource cache directory after patching"],
showDefaultValue = ALWAYS
)
private var purge: Boolean = false
override fun run() {
// region Prepare
if (!apk.exists()) {
logger.severe("APK file ${apk.name} does not exist")
return
}
integrations.filter { !it.exists() }.let {
if (it.isEmpty()) return@let
it.forEach { integration ->
logger.severe("Integration file ${integration.name} does not exist")
}
return
}
val adbManager = deviceSerial?.let { serial ->
if (mount) AdbManager.RootAdbManager(serial)
else AdbManager.UserAdbManager(serial)
}
// endregion
// region Load patches
logger.info("Loading patches")
val patches = PatchBundleLoader.Jar(*patchBundles.toTypedArray())
val integrations = integrations
logger.info("Setting patch options")
optionsFile.let {
if (it.exists()) patches.setOptions(it)
else Options.serialize(patches, prettyPrint = true).let(it::writeText)
}
// endregion
// region Patch
val patcher = Patcher(
PatcherOptions(
apk,
resourceCachePath,
aaptBinaryPath.path,
resourceCachePath.absolutePath,
)
)
val result = patcher.apply {
acceptIntegrations(integrations)
acceptPatches(filterPatchSelection(patches))
// Execute patches.
runBlocking {
apply(false).collect { patchResult ->
patchResult.exception?.let {
StringWriter().use { writer ->
it.printStackTrace(PrintWriter(writer))
logger.severe("${patchResult.patchName} failed: $writer")
}
} ?: logger.info("${patchResult.patchName} succeeded")
}
}
}.get()
patcher.close()
// endregion
// region Finish
val alignAndSignedFile = sign(
apk.newAlignedFile(
result, resourceCachePath.resolve("${outputFilePath.nameWithoutExtension}_aligned.apk")
)
)
logger.info("Copying to ${outputFilePath.name}")
alignAndSignedFile.copyTo(outputFilePath, overwrite = true)
adbManager?.install(AdbManager.Apk(outputFilePath, patcher.context.packageMetadata.packageName))
if (purge) {
logger.info("Purging temporary files")
purge(resourceCachePath)
}
// endregion
}
/**
* Filter the patches to be added to the patcher. The filter is based on the following:
* - [includedPatches] (explicitly included)
* - [excludedPatches] (explicitly excluded)
* - [exclusive] (only include patches that are explicitly included)
* - [force] (ignore patches incompatibility to versions)
* - Package name and version of the input APK file (if [force] is false)
*
* @param patches The patches to filter.
* @return The filtered patches.
*/
private fun Patcher.filterPatchSelection(patches: PatchList) = buildList {
val packageName = context.packageMetadata.packageName
val packageVersion = context.packageMetadata.packageVersion
patches.forEach patch@{ patch ->
val formattedPatchName = patch.patchName.lowercase().replace(" ", "-")
val explicitlyExcluded = excludedPatches.contains(formattedPatchName)
if (explicitlyExcluded) return@patch logger.info("Excluding ${patch.patchName}")
// If the patch is explicitly included, it will be included if [exclusive] is false.
val explicitlyIncluded = exclusive && includedPatches.contains(formattedPatchName)
// If the patch is implicitly included, it will be only included if [exclusive] is false.
val implicitlyIncluded = !exclusive && patch.include
val included = implicitlyIncluded || explicitlyIncluded
if (!included) return@patch logger.info("${patch.patchName} excluded by default") // Case 1.
// At last make sure the patch is compatible with the supplied APK files package name and version.
patch.compatiblePackages?.let { packages ->
packages.singleOrNull { it.name == packageName }?.let { `package` ->
val matchesVersion = force || `package`.versions.let {
it.isEmpty() || it.any { version -> version == packageVersion }
}
if (!matchesVersion) return@patch logger.warning("${patch.patchName} is incompatible with version $packageVersion. " + "This patch is only compatible with version " + packages.joinToString(
";"
) { pkg ->
"${pkg.name}: ${pkg.versions.joinToString(", ")}"
})
} ?: return@patch logger.fine("${patch.patchName} is incompatible with $packageName. "
+ "This patch is only compatible with "
+ packages.joinToString(", ") { `package` -> `package`.name })
return@let
} ?: logger.fine("$formattedPatchName: No constraint on packages.")
logger.fine("Adding $formattedPatchName")
add(patch)
}
}
/**
* Create a new aligned APK file.
*
* @param result The result of the patching process.
* @param outputFile The file to save the aligned APK to.
*/
private fun File.newAlignedFile(
result: PatcherResult, outputFile: File
): File {
logger.info("Aligning $name")
if (outputFile.exists()) outputFile.delete()
ZipFile(outputFile).use { file ->
result.dexFiles.forEach {
file.addEntryCompressData(
ZipEntry.createWithName(it.name), it.stream.readBytes()
)
}
result.resourceFile?.let {
file.copyEntriesFromFileAligned(
ZipFile(it), ZipAligner::getEntryAlignment
)
}
// TODO: Do not compress result.doNotCompress
file.copyEntriesFromFileAligned(
ZipFile(this), ZipAligner::getEntryAlignment
)
}
return outputFile
}
/**
* Sign the APK file.
*
* @param inputFile The APK file to sign.
* @return The signed APK file. If [mount] is true, the input file will be returned.
*/
private fun sign(inputFile: File) = if (mount) inputFile
else {
logger.info("Signing ${inputFile.name}")
val keyStoreFilePath = keystorePath
?: outputFilePath.absoluteFile.parentFile.resolve("${outputFilePath.nameWithoutExtension}.keystore").canonicalPath
val options = SigningOptions(
commonName, password, keyStoreFilePath
)
ApkSigner(options).signApk(
inputFile, resourceCachePath.resolve("${outputFilePath.nameWithoutExtension}_signed.apk")
)
}
private fun purge(resourceCachePath: File) {
val result = if (resourceCachePath.deleteRecursively()) "Purged resource cache directory"
else "Failed to purge resource cache directory"
logger.info(result)
}
}

View File

@ -0,0 +1,42 @@
package app.revanced.cli.command.utility
import app.revanced.utils.adb.AdbManager
import picocli.CommandLine.*
import java.io.File
import java.util.logging.Logger
@Command(
name = "install", description = ["Install an APK file to devices with the supplied ADB device serials"]
)
internal object InstallCommand : Runnable {
private val logger = Logger.getLogger(InstallCommand::class.java.name)
@Parameters(
description = ["ADB device serials"], arity = "1..*"
)
private lateinit var deviceSerials: Array<String>
@Option(
names = ["-a", "--apk"], description = ["APK file to be installed"], required = true
)
private lateinit var apk: File
@Option(
names = ["-m", "--mount"],
description = ["Mount the supplied APK file over the app with the supplied package name"],
)
private var packageName: String? = null
override fun run() = try {
deviceSerials.forEach { deviceSerial ->
if (packageName != null) {
AdbManager.RootAdbManager(deviceSerial)
} else {
AdbManager.UserAdbManager(deviceSerial)
}.install(AdbManager.Apk(apk, packageName))
}
} catch (e: AdbManager.DeviceNotFoundException) {
logger.severe(e.toString())
}
}

View File

@ -0,0 +1,40 @@
package app.revanced.cli.command.utility
import app.revanced.utils.adb.AdbManager
import picocli.CommandLine.*
import picocli.CommandLine.Help.Visibility.ALWAYS
import java.util.logging.Logger
@Command(
name = "uninstall",
description = ["Uninstall a patched app from the devices with the supplied ADB device serials"]
)
internal object UninstallCommand : Runnable {
private val logger = Logger.getLogger(UninstallCommand::class.java.name)
@Parameters(description = ["ADB device serials"], arity = "1..*")
private lateinit var deviceSerials: Array<String>
@Option(names = ["-p", "--package-name"], description = ["Package name of the app to uninstall"], required = true)
private lateinit var packageName: String
@Option(
names = ["-u", "--unmount"],
description = ["Uninstall by unmounting the patched APK file"],
showDefaultValue = ALWAYS
)
private var unmount: Boolean = false
override fun run() = try {
deviceSerials.forEach { deviceSerial ->
if (unmount) {
AdbManager.RootAdbManager(deviceSerial)
} else {
AdbManager.UserAdbManager(deviceSerial)
}.uninstall(packageName)
}
} catch (e: AdbManager.DeviceNotFoundException) {
logger.severe(e.toString())
}
}

View File

@ -0,0 +1,10 @@
package app.revanced.cli.command.utility
import picocli.CommandLine
@CommandLine.Command(
name = "utility",
description = ["Commands for utility purposes"],
subcommands = [InstallCommand::class, UninstallCommand::class],
)
internal object UtilityCommand

View File

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

View File

@ -1,30 +0,0 @@
package app.revanced.cli.logging.impl
import app.revanced.cli.command.MainCommand
import app.revanced.cli.logging.CliLogger
import java.util.logging.Logger
import java.util.logging.SimpleFormatter
internal class DefaultCliLogger(
private val logger: Logger = Logger.getLogger(MainCommand::class.java.name),
private val errorLogger: Logger = Logger.getLogger(logger.name + "Err")
) : CliLogger {
init {
logger.useParentHandlers = false
if (logger.handlers.isEmpty()) {
logger.addHandler(FlushingStreamHandler(System.out, SimpleFormatter()))
}
}
companion object {
init {
System.setProperty("java.util.logging.SimpleFormatter.format", "%4\$s: %5\$s %n")
}
}
override fun error(msg: String) = errorLogger.severe(msg)
override fun info(msg: String) = logger.info(msg)
override fun trace(msg: String) = logger.finest(msg)
override fun warn(msg: String) = errorLogger.warning(msg)
}

View File

@ -1,13 +0,0 @@
package app.revanced.cli.logging.impl
import java.io.OutputStream
import java.util.logging.Formatter
import java.util.logging.LogRecord
import java.util.logging.StreamHandler
internal class FlushingStreamHandler(out: OutputStream, format: Formatter) : StreamHandler(out, format) {
override fun publish(record: LogRecord) {
super.publish(record)
flush()
}
}

View File

@ -1,8 +0,0 @@
package app.revanced.cli.main
import app.revanced.cli.command.MainCommand
import picocli.CommandLine
internal fun main(args: Array<String>) {
CommandLine(MainCommand).execute(*args)
}

View File

@ -1,23 +0,0 @@
package app.revanced.cli.patcher
import app.revanced.cli.command.PatchList
import app.revanced.patcher.PatcherResult
import app.revanced.utils.patcher.addPatchesFiltered
import app.revanced.utils.patcher.applyPatchesVerbose
import app.revanced.utils.patcher.mergeFiles
internal object Patcher {
internal fun start(
patcher: app.revanced.patcher.Patcher,
allPatches: PatchList
): PatcherResult {
// merge files like necessary integrations
patcher.mergeFiles()
// add patches, but filter incompatible or excluded patches
patcher.addPatchesFiltered(allPatches)
// apply patches
patcher.applyPatchesVerbose()
return patcher.save()
}
}

View File

@ -1,13 +0,0 @@
package app.revanced.cli.patcher.logging.impl
import app.revanced.cli.logging.impl.DefaultCliLogger
import java.util.logging.Logger
internal object PatcherLogger : app.revanced.patcher.logging.Logger{
private val logger = DefaultCliLogger(Logger.getLogger(app.revanced.patcher.Patcher::class.java.name))
override fun error(msg: String) = logger.error(msg)
override fun info(msg: String) = logger.info(msg)
override fun warn(msg: String)= logger.warn(msg)
override fun trace(msg: String)= logger.trace(msg)
}

View File

@ -1,12 +0,0 @@
package app.revanced.cli.signing
import app.revanced.cli.command.MainCommand.logger
import app.revanced.utils.signing.Signer
import java.io.File
object Signing {
fun sign(alignedFile: File, signedOutput: File, signingOptions: SigningOptions) {
logger.info("Signing ${alignedFile.name} to ${signedOutput.name}")
Signer(signingOptions).signApk(alignedFile, signedOutput)
}
}

View File

@ -1,16 +1,18 @@
package app.revanced.utils package app.revanced.utils
import app.revanced.cli.command.PatchList import app.revanced.cli.command.PatchList
import app.revanced.cli.logging.CliLogger
import app.revanced.patcher.extensions.PatchExtensions.options import app.revanced.patcher.extensions.PatchExtensions.options
import app.revanced.patcher.extensions.PatchExtensions.patchName import app.revanced.patcher.extensions.PatchExtensions.patchName
import app.revanced.patcher.patch.NoSuchOptionException import app.revanced.patcher.patch.NoSuchOptionException
import app.revanced.utils.Options.PatchOption.Option import app.revanced.utils.Options.PatchOption.Option
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import java.io.File import java.io.File
import java.util.logging.Logger
internal object Options { internal object Options {
private val logger = Logger.getLogger(Options::class.java.name)
private var mapper = jacksonObjectMapper() private var mapper = jacksonObjectMapper()
/** /**
@ -53,22 +55,24 @@ internal object Options {
* Sets the options for the patches in the list. * Sets the options for the patches in the list.
* *
* @param json The JSON string containing the options. * @param json The JSON string containing the options.
* @param logger The logger to use for logging.
*/ */
fun PatchList.setOptions(json: String, logger: CliLogger? = null) { fun PatchList.setOptions(json: String) {
filter { it.options?.any() == true }.let { patches -> filter { it.options?.any() == true }.let { patches ->
if (patches.isEmpty()) return if (patches.isEmpty()) return
val patchOptions = deserialize(json) val patchOptions = deserialize(json)
patches.forEach { patch -> patches.forEach patch@{ patch ->
patchOptions.find { option -> option.patchName == patch.patchName }?.let { patchOptions.find { option -> option.patchName == patch.patchName }?.let {
it.options.forEach { option -> it.options.forEach { option ->
try { try {
patch.options?.set(option.key, option.value) patch.options?.set(option.key, option.value)
?: logger?.warn("${patch.patchName} has no options") ?: run{
logger.warning("${patch.patchName} has no options")
return@patch
}
} catch (e: NoSuchOptionException) { } catch (e: NoSuchOptionException) {
logger?.error(e.message ?: "Unknown error") logger.info(e.toString())
} }
} }
} }
@ -80,10 +84,9 @@ internal object Options {
* Sets the options for the patches in the list. * Sets the options for the patches in the list.
* *
* @param file The file containing the JSON string containing the options. * @param file The file containing the JSON string containing the options.
* @param logger The logger to use for logging.
* @see setOptions * @see setOptions
*/ */
fun PatchList.setOptions(file: File, logger: CliLogger? = null) = setOptions(file.readText(), logger) fun PatchList.setOptions(file: File) = setOptions(file.readText())
/** /**
* Data class for a patch and its [Option]s. * Data class for a patch and its [Option]s.

View File

@ -1,113 +0,0 @@
package app.revanced.utils.adb
import app.revanced.cli.command.MainCommand.logger
import se.vidstige.jadb.JadbConnection
import se.vidstige.jadb.JadbDevice
import se.vidstige.jadb.managers.PackageManager
import java.io.File
import java.util.concurrent.Executors
internal class Adb(
private val file: File,
private val packageName: String,
deviceName: String,
private val modeInstall: Boolean = false,
private val logging: Boolean = true
) {
private val device: JadbDevice
init {
device = JadbConnection().devices.let { device -> device.find { it.serial == deviceName } ?: device.first() }
?: throw IllegalArgumentException("No such device with name $deviceName")
if (!modeInstall && device.run("su -h", false) != 0)
throw IllegalArgumentException("Root required on $deviceName. Task failed")
}
private fun String.replacePlaceholder(with: String? = null): String {
return this.replace(Constants.PLACEHOLDER, with ?: packageName)
}
internal fun deploy() {
if (modeInstall) {
logger.info("Installing without mounting")
PackageManager(device).install(file)
} else {
logger.info("Installing by mounting")
// push patched file
device.copy(Constants.PATH_INIT_PUSH, file)
// create revanced folder path
device.run("${Constants.COMMAND_CREATE_DIR} ${Constants.PATH_REVANCED}")
// prepare mounting the apk
device.run(Constants.COMMAND_PREPARE_MOUNT_APK.replacePlaceholder())
// push mount script
device.createFile(
Constants.PATH_INIT_PUSH,
Constants.CONTENT_MOUNT_SCRIPT.replacePlaceholder()
)
// install mount script
device.run(Constants.COMMAND_INSTALL_MOUNT.replacePlaceholder())
// unmount the apk for sanity
device.run(Constants.COMMAND_UMOUNT.replacePlaceholder())
// mount the apk
device.run(Constants.PATH_MOUNT.replacePlaceholder())
// relaunch app
device.run(Constants.COMMAND_RESTART.replacePlaceholder())
// log the app
log()
}
}
internal fun uninstall() {
logger.info("Uninstalling by unmounting")
// unmount the apk
device.run(Constants.COMMAND_UMOUNT.replacePlaceholder())
// delete revanced app
device.run(Constants.COMMAND_DELETE.replacePlaceholder(Constants.PATH_REVANCED_APP).replacePlaceholder())
// delete mount script
device.run(Constants.COMMAND_DELETE.replacePlaceholder(Constants.PATH_MOUNT).replacePlaceholder())
logger.info("Finished uninstalling")
}
private fun log() {
val executor = Executors.newSingleThreadExecutor()
val pipe = if (logging) {
ProcessBuilder.Redirect.INHERIT
} else {
ProcessBuilder.Redirect.PIPE
}
val process = device.buildCommand(Constants.COMMAND_LOGCAT.replacePlaceholder())
.redirectOutput(pipe)
.redirectError(pipe)
.useExecutor(executor)
.start()
Thread.sleep(500) // give the app some time to start up.
while (true) {
try {
while (device.run("${Constants.COMMAND_PID_OF} $packageName") == 0) {
Thread.sleep(1000)
}
break
} catch (e: Exception) {
throw RuntimeException("An error occurred while monitoring the state of app", e)
}
}
logger.info("Stopped logging because the app was closed")
process.destroy()
executor.shutdown()
}
}

View File

@ -0,0 +1,140 @@
package app.revanced.utils.adb
import app.revanced.utils.adb.AdbManager.Apk
import app.revanced.utils.adb.Constants.CREATE_DIR
import app.revanced.utils.adb.Constants.DELETE
import app.revanced.utils.adb.Constants.INSTALLATION_PATH
import app.revanced.utils.adb.Constants.INSTALL_MOUNT
import app.revanced.utils.adb.Constants.INSTALL_PATCHED_APK
import app.revanced.utils.adb.Constants.MOUNT_PATH
import app.revanced.utils.adb.Constants.MOUNT_SCRIPT
import app.revanced.utils.adb.Constants.PATCHED_APK_PATH
import app.revanced.utils.adb.Constants.PLACEHOLDER
import app.revanced.utils.adb.Constants.RESTART
import app.revanced.utils.adb.Constants.TMP_PATH
import app.revanced.utils.adb.Constants.UMOUNT
import se.vidstige.jadb.JadbConnection
import se.vidstige.jadb.managers.Package
import se.vidstige.jadb.managers.PackageManager
import java.io.Closeable
import java.io.File
import java.util.logging.Logger
/**
* Adb manager. Used to install and uninstall [Apk] files.
*
* @param deviceSerial The serial of the device.
*/
internal sealed class AdbManager(deviceSerial: String? = null) : Closeable {
protected val logger: Logger = Logger.getLogger(AdbManager::class.java.name)
protected val device = JadbConnection().devices.find { device -> device.serial == deviceSerial }
?: throw DeviceNotFoundException(deviceSerial)
init {
logger.fine("Established connection to $deviceSerial")
}
/**
* Installs the [Apk] file.
*
* @param apk The [Apk] file.
*/
open fun install(apk: Apk) {
logger.info("Finished installing ${apk.file.name}")
}
/**
* Uninstalls the package.
*
* @param packageName The package name.
*/
open fun uninstall(packageName: String) {
logger.info("Finished uninstalling $packageName")
}
/**
* Closes the [AdbManager] instance.
*/
override fun close() {
logger.fine("Closed")
}
class RootAdbManager(deviceSerial: String) : AdbManager(deviceSerial) {
init {
if (!device.hasSu()) throw IllegalArgumentException("Root required on $deviceSerial. Task failed")
}
override fun install(apk: Apk) {
logger.info("Installing by mounting")
val applyReplacement = getPlaceholderReplacement(
apk.packageName ?: throw IllegalArgumentException("Package name is required")
)
device.push(apk.file, TMP_PATH)
device.run("$CREATE_DIR $INSTALLATION_PATH")
device.run(INSTALL_PATCHED_APK.applyReplacement())
device.createFile(TMP_PATH, MOUNT_SCRIPT.applyReplacement())
device.run(INSTALL_MOUNT.applyReplacement())
device.run(UMOUNT.applyReplacement()) // Sanity check.
device.run(MOUNT_PATH.applyReplacement())
device.run(RESTART.applyReplacement())
device.run(DELETE.applyReplacement(TMP_PATH).applyReplacement())
super.install(apk)
}
override fun uninstall(packageName: String) {
logger.info("Uninstalling $packageName by unmounting")
val applyReplacement = getPlaceholderReplacement(packageName)
device.run(UMOUNT.applyReplacement(packageName))
device.run(DELETE.applyReplacement(PATCHED_APK_PATH).applyReplacement())
device.run(DELETE.applyReplacement(MOUNT_PATH).applyReplacement())
device.run(DELETE.applyReplacement(TMP_PATH).applyReplacement())
super.uninstall(packageName)
}
companion object Utils {
private fun getPlaceholderReplacement(with: String): String.() -> String = { replace(PLACEHOLDER, with) }
private fun String.applyReplacement(with: String) = replace(PLACEHOLDER, with)
}
}
class UserAdbManager(deviceSerial: String) : AdbManager(deviceSerial) {
private val packageManager = PackageManager(device)
override fun install(apk: Apk) {
PackageManager(device).install(apk.file)
super.install(apk)
}
override fun uninstall(packageName: String) {
logger.info("Uninstalling $packageName")
packageManager.uninstall(Package(packageName))
super.uninstall(packageName)
}
}
/**
* Apk file for [AdbManager].
*
* @param file The [Apk] file.
* @param packageName The package name of the [Apk] file.
*/
internal class Apk(val file: File, val packageName: String? = null)
internal class DeviceNotFoundException(deviceSerial: String?) :
Exception(deviceSerial?.let {
"The device with the ADB device serial \"$deviceSerial\" can not be found"
} ?: "No ADB device found")
}

View File

@ -5,10 +5,9 @@ import se.vidstige.jadb.RemoteFile
import se.vidstige.jadb.ShellProcessBuilder import se.vidstige.jadb.ShellProcessBuilder
import java.io.File import java.io.File
internal fun JadbDevice.buildCommand(command: String, su: Boolean = true): ShellProcessBuilder { internal fun JadbDevice.buildCommand(command: String, su: Boolean = true): ShellProcessBuilder {
if (su) { if (su) return shellProcessBuilder("su -c \'$command\'")
return shellProcessBuilder("su -c \'$command\'")
}
val args = command.split(" ") as ArrayList<String> val args = command.split(" ") as ArrayList<String>
val cmd = args.removeFirst() val cmd = args.removeFirst()
@ -20,10 +19,15 @@ internal fun JadbDevice.run(command: String, su: Boolean = true): Int {
return this.buildCommand(command, su).start().waitFor() return this.buildCommand(command, su).start().waitFor()
} }
internal fun JadbDevice.copy(targetPath: String, file: File) { internal fun JadbDevice.hasSu() =
push(file, RemoteFile(targetPath)) this.startCommand("su -h", false).waitFor() == 0
}
internal fun JadbDevice.createFile(targetFile: String, content: String) { internal fun JadbDevice.push(file: File, targetFilePath: String) =
push(file, RemoteFile(targetFilePath))
internal fun JadbDevice.createFile(targetFile: String, content: String) =
push(content.byteInputStream(), System.currentTimeMillis(), 644, RemoteFile(targetFile)) push(content.byteInputStream(), System.currentTimeMillis(), 644, RemoteFile(targetFile))
}
private fun JadbDevice.startCommand(command: String, su: Boolean) =
shellProcessBuilder(if (su) "su -c '$command'" else command).start()

View File

@ -1,54 +1,37 @@
package app.revanced.utils.adb package app.revanced.utils.adb
internal object Constants { internal object Constants {
// template placeholder to replace a string in commands internal const val PLACEHOLDER = "PLACEHOLDER"
internal const val PLACEHOLDER = "TEMPLATE_PACKAGE_NAME"
// utility commands internal const val TMP_PATH = "/data/local/tmp/revanced.tmp"
private const val COMMAND_CHMOD_MOUNT = "chmod +x" internal const val INSTALLATION_PATH = "/data/adb/revanced/"
internal const val COMMAND_PID_OF = "pidof -s" internal const val PATCHED_APK_PATH = "$INSTALLATION_PATH$PLACEHOLDER.apk"
internal const val COMMAND_CREATE_DIR = "mkdir -p" internal const val MOUNT_PATH = "/data/adb/service.d/mount_revanced_$PLACEHOLDER.sh"
internal const val COMMAND_LOGCAT = "logcat -c && logcat | grep AndroidRuntime"
internal const val COMMAND_RESTART = "pm resolve-activity --brief $PLACEHOLDER | tail -n 1 | xargs am start -n && kill ${'$'}($COMMAND_PID_OF $PLACEHOLDER)"
// default mount file name internal const val DELETE = "rm -rf $PLACEHOLDER"
private const val NAME_MOUNT_SCRIPT = "mount_revanced_$PLACEHOLDER.sh" internal const val CREATE_DIR = "mkdir -p"
internal const val RESTART = "pm resolve-activity --brief $PLACEHOLDER | tail -n 1 | " +
"xargs am start -n && kill ${'$'}(pidof -s $PLACEHOLDER)"
// initial directory to push files to via adb push internal const val INSTALL_PATCHED_APK = "base_path=\"$PATCHED_APK_PATH\" && " +
internal const val PATH_INIT_PUSH = "/data/local/tmp/revanced.delete" "mv $TMP_PATH ${'$'}base_path && " +
"chmod 644 ${'$'}base_path && " +
"chown system:system ${'$'}base_path && " +
"chcon u:object_r:apk_data_file:s0 ${'$'}base_path"
// revanced path internal const val UMOUNT =
internal const val PATH_REVANCED = "/data/adb/revanced/"
// revanced apk path
internal const val PATH_REVANCED_APP = "$PATH_REVANCED$PLACEHOLDER.apk"
// delete command
internal const val COMMAND_DELETE = "rm -rf $PLACEHOLDER"
// mount script path
internal const val PATH_MOUNT = "/data/adb/service.d/$NAME_MOUNT_SCRIPT"
// move to revanced apk path & set permissions
internal const val COMMAND_PREPARE_MOUNT_APK =
"base_path=\"$PATH_REVANCED_APP\" && mv $PATH_INIT_PUSH ${'$'}base_path && chmod 644 ${'$'}base_path && chown system:system ${'$'}base_path && chcon u:object_r:apk_data_file:s0 ${'$'}base_path"
// unmount command
internal const val COMMAND_UMOUNT =
"grep $PLACEHOLDER /proc/mounts | while read -r line; do echo ${'$'}line | cut -d \" \" -f 2 | sed 's/apk.*/apk/' | xargs -r umount -l; done" "grep $PLACEHOLDER /proc/mounts | while read -r line; do echo ${'$'}line | cut -d \" \" -f 2 | sed 's/apk.*/apk/' | xargs -r umount -l; done"
// install mount script & set permissions internal const val INSTALL_MOUNT = "mv $TMP_PATH $MOUNT_PATH && chmod +x $MOUNT_PATH"
internal const val COMMAND_INSTALL_MOUNT = "mv $PATH_INIT_PUSH $PATH_MOUNT && $COMMAND_CHMOD_MOUNT $PATH_MOUNT"
// mount script internal val MOUNT_SCRIPT =
internal val CONTENT_MOUNT_SCRIPT =
""" """
#!/system/bin/sh #!/system/bin/sh
MAGISKTMP="${'$'}(magisk --path)" || MAGISKTMP=/sbin MAGISKTMP="${'$'}(magisk --path)" || MAGISKTMP=/sbin
MIRROR="${'$'}MAGISKTMP/.magisk/mirror" MIRROR="${'$'}MAGISKTMP/.magisk/mirror"
while [ "${'$'}(getprop sys.boot_completed | tr -d '\r')" != "1" ]; do sleep 1; done while [ "${'$'}(getprop sys.boot_completed | tr -d '\r')" != "1" ]; do sleep 1; done
base_path="$PATH_REVANCED_APP" base_path="$PATCHED_APK_PATH"
stock_path=${'$'}( pm path $PLACEHOLDER | grep base | sed 's/package://g' ) stock_path=${'$'}( pm path $PLACEHOLDER | grep base | sed 's/package://g' )
chcon u:object_r:apk_data_file:s0 ${'$'}base_path chcon u:object_r:apk_data_file:s0 ${'$'}base_path

View File

@ -1,6 +1,6 @@
package app.revanced.utils.signing.align package app.revanced.utils.align
import app.revanced.utils.signing.align.zip.structures.ZipEntry import app.revanced.utils.align.zip.structures.ZipEntry
internal object ZipAligner { internal object ZipAligner {
private const val DEFAULT_ALIGNMENT = 4 private const val DEFAULT_ALIGNMENT = 4

View File

@ -1,4 +1,4 @@
package app.revanced.utils.signing.align.zip package app.revanced.utils.align.zip
import java.io.DataInput import java.io.DataInput
import java.io.DataOutput import java.io.DataOutput

View File

@ -1,7 +1,7 @@
package app.revanced.utils.signing.align.zip package app.revanced.utils.align.zip
import app.revanced.utils.signing.align.zip.structures.ZipEndRecord import app.revanced.utils.align.zip.structures.ZipEndRecord
import app.revanced.utils.signing.align.zip.structures.ZipEntry import app.revanced.utils.align.zip.structures.ZipEntry
import java.io.Closeable import java.io.Closeable
import java.io.File import java.io.File
import java.io.RandomAccessFile import java.io.RandomAccessFile
@ -11,15 +11,15 @@ import java.util.zip.CRC32
import java.util.zip.Deflater import java.util.zip.Deflater
class ZipFile(file: File) : Closeable { class ZipFile(file: File) : Closeable {
var entries: MutableList<ZipEntry> = mutableListOf() private var entries: MutableList<ZipEntry> = mutableListOf()
private val filePointer: RandomAccessFile = RandomAccessFile(file, "rw") private val filePointer: RandomAccessFile = RandomAccessFile(file, "rw")
private var CDNeedsRewrite = false private var centralDirectoryNeedsRewrite = false
private val compressionLevel = 5 private val compressionLevel = 5
init { init {
//if file isn't empty try to load entries // If file isn't empty try to load entries.
if (file.length() > 0) { if (file.length() > 0) {
val endRecord = findEndRecord() val endRecord = findEndRecord()
@ -29,17 +29,17 @@ class ZipFile(file: File) : Closeable {
entries = readEntries(endRecord).toMutableList() entries = readEntries(endRecord).toMutableList()
} }
//seek back to start for writing // Seek back to start for writing.
filePointer.seek(0) filePointer.seek(0)
} }
private fun findEndRecord(): ZipEndRecord { private fun findEndRecord(): ZipEndRecord {
//look from end to start since end record is at the end // Look from end to start since end record is at the end.
for (i in filePointer.length() - 1 downTo 0) { for (i in filePointer.length() - 1 downTo 0) {
filePointer.seek(i) filePointer.seek(i)
//possible beginning of signature // Possible beginning of signature.
if (filePointer.readByte() == 0x50.toByte()) { if (filePointer.readByte() == 0x50.toByte()) {
//seek back to get the full int // Seek back to get the full int.
filePointer.seek(i) filePointer.seek(i)
val possibleSignature = filePointer.readUIntLE() val possibleSignature = filePointer.readUIntLE()
if (possibleSignature == ZipEndRecord.ECD_SIGNATURE) { if (possibleSignature == ZipEndRecord.ECD_SIGNATURE) {
@ -76,7 +76,7 @@ class ZipFile(file: File) : Closeable {
} }
private fun writeCD() { private fun writeCD() {
val CDStart = filePointer.channel.position().toUInt() val centralDirectoryStartOffset = filePointer.channel.position().toUInt()
entries.forEach { entries.forEach {
filePointer.channel.write(it.toCDE()) filePointer.channel.write(it.toCDE())
@ -89,8 +89,8 @@ class ZipFile(file: File) : Closeable {
0u, 0u,
entriesCount, entriesCount,
entriesCount, entriesCount,
filePointer.channel.position().toUInt() - CDStart, filePointer.channel.position().toUInt() - centralDirectoryStartOffset,
CDStart, centralDirectoryStartOffset,
"" ""
) )
@ -98,7 +98,7 @@ class ZipFile(file: File) : Closeable {
} }
private fun addEntry(entry: ZipEntry, data: ByteBuffer) { private fun addEntry(entry: ZipEntry, data: ByteBuffer) {
CDNeedsRewrite = true centralDirectoryNeedsRewrite = true
entry.localHeaderOffset = filePointer.channel.position().toUInt() entry.localHeaderOffset = filePointer.channel.position().toUInt()
@ -114,8 +114,7 @@ class ZipFile(file: File) : Closeable {
compressor.finish() compressor.finish()
val uncompressedSize = data.size val uncompressedSize = data.size
val compressedData = val compressedData = ByteArray(uncompressedSize) // I'm guessing compression won't make the data bigger.
ByteArray(uncompressedSize) //i'm guessing compression won't make the data bigger
val compressedDataLength = compressor.deflate(compressedData) val compressedDataLength = compressor.deflate(compressedData)
val compressedBuffer = val compressedBuffer =
@ -126,7 +125,7 @@ class ZipFile(file: File) : Closeable {
val crc = CRC32() val crc = CRC32()
crc.update(data) crc.update(data)
entry.compression = 8u //deflate compression entry.compression = 8u // Deflate compression.
entry.uncompressedSize = uncompressedSize.toUInt() entry.uncompressedSize = uncompressedSize.toUInt()
entry.compressedSize = compressedDataLength.toUInt() entry.compressedSize = compressedDataLength.toUInt()
entry.crc32 = crc.value.toUInt() entry.crc32 = crc.value.toUInt()
@ -136,14 +135,14 @@ class ZipFile(file: File) : Closeable {
private fun addEntryCopyData(entry: ZipEntry, data: ByteBuffer, alignment: Int? = null) { private fun addEntryCopyData(entry: ZipEntry, data: ByteBuffer, alignment: Int? = null) {
alignment?.let { alignment?.let {
//calculate where data would end up // Calculate where data would end up.
val dataOffset = filePointer.filePointer + entry.LFHSize val dataOffset = filePointer.filePointer + entry.LFHSize
val mod = dataOffset % alignment val mod = dataOffset % alignment
//wrong alignment // Wrong alignment.
if (mod != 0L) { if (mod != 0L) {
//add padding at end of extra field // Add padding at end of extra field.
entry.localExtraField = entry.localExtraField =
entry.localExtraField.copyOf((entry.localExtraField.size + (alignment - mod)).toInt()) entry.localExtraField.copyOf((entry.localExtraField.size + (alignment - mod)).toInt())
} }
@ -152,7 +151,7 @@ class ZipFile(file: File) : Closeable {
addEntry(entry, data) addEntry(entry, data)
} }
fun getDataForEntry(entry: ZipEntry): ByteBuffer { private fun getDataForEntry(entry: ZipEntry): ByteBuffer {
return filePointer.channel.map( return filePointer.channel.map(
FileChannel.MapMode.READ_ONLY, FileChannel.MapMode.READ_ONLY,
entry.dataOffset.toLong(), entry.dataOffset.toLong(),
@ -160,9 +159,15 @@ class ZipFile(file: File) : Closeable {
) )
} }
/**
* Copies all entries from [file] to this file but skip already existing entries.
*
* @param file The file to copy entries from.
* @param entryAlignment A function that returns the alignment for a given entry.
*/
fun copyEntriesFromFileAligned(file: ZipFile, entryAlignment: (entry: ZipEntry) -> Int?) { fun copyEntriesFromFileAligned(file: ZipFile, entryAlignment: (entry: ZipEntry) -> Int?) {
for (entry in file.entries) { for (entry in file.entries) {
if (entries.any { it.fileName == entry.fileName }) continue //don't add duplicates if (entries.any { it.fileName == entry.fileName }) continue // Skip duplicates
val data = file.getDataForEntry(entry) val data = file.getDataForEntry(entry)
addEntryCopyData(entry, data, entryAlignment(entry)) addEntryCopyData(entry, data, entryAlignment(entry))
@ -170,7 +175,7 @@ class ZipFile(file: File) : Closeable {
} }
override fun close() { override fun close() {
if (CDNeedsRewrite) writeCD() if (centralDirectoryNeedsRewrite) writeCD()
filePointer.close() filePointer.close()
} }
} }

View File

@ -1,9 +1,9 @@
package app.revanced.utils.signing.align.zip.structures package app.revanced.utils.align.zip.structures
import app.revanced.utils.signing.align.zip.putUInt import app.revanced.utils.align.zip.putUInt
import app.revanced.utils.signing.align.zip.putUShort import app.revanced.utils.align.zip.putUShort
import app.revanced.utils.signing.align.zip.readUIntLE import app.revanced.utils.align.zip.readUIntLE
import app.revanced.utils.signing.align.zip.readUShortLE import app.revanced.utils.align.zip.readUShortLE
import java.io.DataInput import java.io.DataInput
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.ByteOrder import java.nio.ByteOrder

View File

@ -1,6 +1,6 @@
package app.revanced.utils.signing.align.zip.structures package app.revanced.utils.align.zip.structures
import app.revanced.utils.signing.align.zip.* import app.revanced.utils.align.zip.*
import java.io.DataInput import java.io.DataInput
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.ByteOrder import java.nio.ByteOrder

View File

@ -1,75 +0,0 @@
package app.revanced.utils.patcher
import app.revanced.cli.command.MainCommand.args
import app.revanced.cli.command.MainCommand.logger
import app.revanced.cli.command.PatchList
import app.revanced.patcher.Patcher
import app.revanced.patcher.data.Context
import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages
import app.revanced.patcher.extensions.PatchExtensions.include
import app.revanced.patcher.extensions.PatchExtensions.patchName
import app.revanced.patcher.patch.Patch
fun Patcher.addPatchesFiltered(allPatches: PatchList) {
val packageName = this.context.packageMetadata.packageName
val packageVersion = this.context.packageMetadata.packageVersion
val includedPatches = mutableListOf<Class<out Patch<Context>>>()
allPatches.forEach patchLoop@{ patch ->
val compatiblePackages = patch.compatiblePackages
val patchName = patch.patchName.lowercase().replace(" ", "-")
val args = args.patchArgs?.patchingArgs!!
val prefix = "Skipping $patchName"
if (compatiblePackages == null) logger.trace("$patchName: No constraint on packages.")
else {
if (!compatiblePackages.any { it.name == packageName }) {
logger.trace("$prefix: Incompatible with $packageName. This patch is only compatible with ${
compatiblePackages.joinToString(
", "
) { it.name }
}")
return@patchLoop
}
if (!(args.experimental || compatiblePackages.any { it.versions.isEmpty() || it.versions.any { version -> version == packageVersion } })) {
val compatibleWith = compatiblePackages.joinToString(";") { _package ->
"${_package.name}: ${_package.versions.joinToString(", ")}"
}
logger.warn("$prefix: Incompatible with version $packageVersion. This patch is only compatible with $compatibleWith")
return@patchLoop
}
}
if (args.excludedPatches.contains(patchName)) {
logger.info("$prefix: Manually excluded")
return@patchLoop
} else if ((!patch.include || args.exclusive) && !args.includedPatches.contains(patchName)) {
logger.info("$prefix: Excluded by default")
return@patchLoop
}
logger.trace("Adding $patchName")
includedPatches.add(patch)
}
this.addPatches(includedPatches)
}
fun Patcher.applyPatchesVerbose() {
this.executePatches().forEach { (patch, result) ->
if (result.isSuccess) {
logger.info("$patch succeeded")
return@forEach
}
logger.error("$patch failed:")
result.exceptionOrNull()!!.printStackTrace()
}
}
fun Patcher.mergeFiles() {
this.addIntegrations(args.patchArgs?.patchingArgs!!.mergeFiles) { file ->
logger.info("Merging $file")
}
}

View File

@ -1,7 +1,5 @@
package app.revanced.utils.signing package app.revanced.utils.signing
import app.revanced.cli.command.MainCommand.logger
import app.revanced.cli.signing.SigningOptions
import com.android.apksig.ApkSigner import com.android.apksig.ApkSigner
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
@ -17,11 +15,44 @@ import java.math.BigInteger
import java.security.* import java.security.*
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import java.util.* import java.util.*
import java.util.logging.Logger
internal class Signer( internal class ApkSigner(
private val signingOptions: SigningOptions private val signingOptions: SigningOptions
) { ) {
private val logger = Logger.getLogger(ApkSigner::class.java.name)
private val signer: ApkSigner.Builder
private val passwordCharArray = signingOptions.password.toCharArray() private val passwordCharArray = signingOptions.password.toCharArray()
init {
Security.addProvider(BouncyCastleProvider())
val keyStore = KeyStore.getInstance("BKS", "BC")
val alias = keyStore.let { store ->
FileInputStream(File(signingOptions.keyStoreFilePath).also {
if (!it.exists()) {
logger.info("Creating keystore at ${it.absolutePath}")
newKeystore(it)
} else {
logger.info("Using keystore at ${it.absolutePath}")
}
}).use { fis -> store.load(fis, null) }
store.aliases().nextElement()
}
with(
ApkSigner.SignerConfig.Builder(
signingOptions.cn,
keyStore.getKey(alias, passwordCharArray) as PrivateKey,
listOf(keyStore.getCertificate(alias) as X509Certificate)
).build()
) {
this@ApkSigner.signer = ApkSigner.Builder(listOf(this))
signer.setCreatedBy(signingOptions.cn)
}
}
private fun newKeystore(out: File) { private fun newKeystore(out: File) {
val (publicKey, privateKey) = createKey() val (publicKey, privateKey) = createKey()
val privateKS = KeyStore.getInstance("BKS", "BC") val privateKS = KeyStore.getInstance("BKS", "BC")
@ -50,30 +81,12 @@ internal class Signer(
return JcaX509CertificateConverter().getCertificate(builder.build(signer)) to pair.private return JcaX509CertificateConverter().getCertificate(builder.build(signer)) to pair.private
} }
fun signApk(input: File, output: File) { fun signApk(input: File, output: File): File {
Security.addProvider(BouncyCastleProvider())
// TODO: keystore should be saved securely
val ks = File(signingOptions.keyStoreFilePath)
if (!ks.exists()) newKeystore(ks) else {
logger.info("Found existing keystore: ${ks.name}")
}
val keyStore = KeyStore.getInstance("BKS", "BC")
FileInputStream(ks).use { fis -> keyStore.load(fis, null) }
val alias = keyStore.aliases().nextElement()
val config = ApkSigner.SignerConfig.Builder(
signingOptions.cn,
keyStore.getKey(alias, passwordCharArray) as PrivateKey,
listOf(keyStore.getCertificate(alias) as X509Certificate)
).build()
val signer = ApkSigner.Builder(listOf(config))
signer.setCreatedBy(signingOptions.cn)
signer.setInputApk(input) signer.setInputApk(input)
signer.setOutputApk(output) signer.setOutputApk(output)
signer.build().sign() signer.build().sign()
return output
} }
} }

View File

@ -1,4 +1,4 @@
package app.revanced.cli.signing package app.revanced.utils.signing
data class SigningOptions( data class SigningOptions(
val cn: String, val cn: String,

View File

@ -0,0 +1 @@
version=${projectVersion}

View File

@ -2,7 +2,10 @@ package app.revanced.patcher.options
import app.revanced.patcher.data.BytecodeContext import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.data.Context import app.revanced.patcher.data.Context
import app.revanced.patcher.patch.* import app.revanced.patcher.patch.BytecodePatch
import app.revanced.patcher.patch.OptionsContainer
import app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.PatchOption
import app.revanced.utils.Options import app.revanced.utils.Options
import app.revanced.utils.Options.setOptions import app.revanced.utils.Options.setOptions
import org.junit.jupiter.api.MethodOrderer import org.junit.jupiter.api.MethodOrderer
@ -11,8 +14,8 @@ import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestMethodOrder import org.junit.jupiter.api.TestMethodOrder
class PatchOptionsTestPatch : BytecodePatch() { class PatchOptionsTestPatch : BytecodePatch() {
override fun execute(context: BytecodeContext): PatchResult { override fun execute(context: BytecodeContext) {
return PatchResultSuccess() // Do nothing
} }
companion object : OptionsContainer() { companion object : OptionsContainer() {
@ -32,7 +35,7 @@ class PatchOptionsTestPatch : BytecodePatch() {
@TestMethodOrder(MethodOrderer.OrderAnnotation::class) @TestMethodOrder(MethodOrderer.OrderAnnotation::class)
internal object PatchOptionOptionsTest { internal object PatchOptionOptionsTest {
private var patches = listOf(PatchOptionsTestPatch::class.java as Class<out Patch<Context>>) private var patches = listOf(PatchOptionsTestPatch::class.java as Class<out Patch<Context<*>>>)
@Test @Test
@Order(1) @Order(1)