From 4624384f28378efeb5cae54365169905a0ed4de7 Mon Sep 17 00:00:00 2001 From: Lucaskyy Date: Sun, 10 Apr 2022 22:21:57 +0200 Subject: [PATCH] feat: load patches dynamically & use kotlinx.cli Patches are now loaded dynamically and the CLI now links to the patches library. Also decided to use the CLI library from kotlinx, since that's friendlier than whatever we had before. --- build.gradle.kts | 14 ++- src/main/kotlin/app/revanced/cli/Main.kt | 98 +++++++++++++++---- .../app/revanced/cli/utils/PatchLoader.kt | 25 +++++ .../kotlin/app/revanced/cli/utils/Patches.kt | 15 +++ .../app/revanced/cli/utils/Preconditions.kt | 24 +++++ .../app/revanced/cli/utils/SignatureParser.kt | 6 +- 6 files changed, 156 insertions(+), 26 deletions(-) create mode 100644 src/main/kotlin/app/revanced/cli/utils/PatchLoader.kt create mode 100644 src/main/kotlin/app/revanced/cli/utils/Patches.kt create mode 100644 src/main/kotlin/app/revanced/cli/utils/Preconditions.kt diff --git a/build.gradle.kts b/build.gradle.kts index af31797e..9be26ee3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -18,11 +18,14 @@ repositories { } } +val patchesDependency = "app.revanced:revanced-patches:1.0.0-dev.4" + dependencies { implementation(kotlin("stdlib")) + implementation("org.jetbrains.kotlinx:kotlinx-cli:0.3.4") implementation("app.revanced:revanced-patcher:1.0.0-dev.8") - implementation("app.revanced:revanced-patches:1.0.0-dev.4") + implementation(patchesDependency) implementation("com.google.code.gson:gson:2.9.0") } @@ -32,8 +35,15 @@ tasks { dependsOn(shadowJar) } shadowJar { + dependencies { + // This makes sure we link to the library, but don't include it. + // So, a "runtime only" dependency. + exclude(dependency(patchesDependency)) + } manifest { - attributes(Pair("Main-Class", "app.revanced.cli.MainKt")) + attributes("Main-Class" to "app.revanced.cli.Main") + attributes("Implementation-Title" to project.name) + attributes("Implementation-Version" to project.version) } } } \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/cli/Main.kt b/src/main/kotlin/app/revanced/cli/Main.kt index 41647dfe..bad34cef 100644 --- a/src/main/kotlin/app/revanced/cli/Main.kt +++ b/src/main/kotlin/app/revanced/cli/Main.kt @@ -1,32 +1,92 @@ package app.revanced.cli +import app.revanced.cli.utils.PatchLoader +import app.revanced.cli.utils.Patches +import app.revanced.cli.utils.Preconditions import app.revanced.cli.utils.SignatureParser import app.revanced.patcher.Patcher -import app.revanced.patches.Index.patches -import org.jf.dexlib2.writer.io.MemoryDataStore +import kotlinx.cli.ArgParser +import kotlinx.cli.ArgType +import kotlinx.cli.required import java.io.File import java.nio.file.Files -fun main(args: Array) { - val patcher = Patcher( - File(args[0]), // in.apk - SignatureParser.parse(args[2]).toTypedArray() // signatures.json - ) +private const val CLI_NAME = "ReVanced CLI" +private val CLI_VERSION = Main::class.java.`package`.implementationVersion ?: "0.0.0-unknown" - // add integrations dex container - patcher.addFiles(File(args[3])) +class Main { + companion object { + private fun runCLI( + inApk: String, + inSignatures: String, + inPatches: String, + inOutput: String, + ) { + val apk = Preconditions.isFile(inApk) + val signatures = Preconditions.isFile(inSignatures) + val patchesFile = Preconditions.isFile(inPatches) + val output = Preconditions.isDirectory(inOutput) - for (patch in patches) { - patcher.addPatches(patch()) - } + val patcher = Patcher( + apk, + SignatureParser + .parse(signatures.readText()) + .toTypedArray() + ) - patcher.applyPatches().forEach { (name, result) -> - println("$name: $result") - } + PatchLoader.injectPatches(patchesFile) + val patches = Patches.loadPatches() + patcher.addPatches(*patches.map { it() }.toTypedArray()) + + val results = patcher.applyPatches() + for ((name, result) in results) { + println("$name: $result") + } + + val dexFiles = patcher.save() + dexFiles.forEach { (dexName, dexData) -> + Files.write(File(output, dexName).toPath(), dexData.buffer) + } + } + + @JvmStatic + fun main(args: Array) { + println("$CLI_NAME version $CLI_VERSION") + val parser = ArgParser(CLI_NAME) + + val apk by parser.option( + ArgType.String, + fullName = "apk", + shortName = "a", + description = "APK file" + ).required() + val signatures by parser.option( + ArgType.String, + fullName = "signatures", + shortName = "s", + description = "Signatures JSON file" + ).required() + val patches by parser.option( + ArgType.String, + fullName = "patches", + shortName = "p", + description = "Patches JAR file" + ).required() + val output by parser.option( + ArgType.String, + fullName = "output", + shortName = "o", + description = "Output directory" + ).required() + // TODO: merge dex file - // save patched apk - val dexFiles: Map = patcher.save() - dexFiles.forEach { (t, p) -> - Files.write(File(args[1], t).toPath(), p.buffer) + parser.parse(args) + runCLI( + apk, + signatures, + patches, + output, + ) + } } } \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/cli/utils/PatchLoader.kt b/src/main/kotlin/app/revanced/cli/utils/PatchLoader.kt new file mode 100644 index 00000000..9da98257 --- /dev/null +++ b/src/main/kotlin/app/revanced/cli/utils/PatchLoader.kt @@ -0,0 +1,25 @@ +package app.revanced.cli.utils + +import java.io.File +import java.net.URL +import java.net.URLClassLoader + +class PatchLoader { + companion object { + fun injectPatches(file: File) { + // This function will fail on Java 9 and above. + try { + val url = file.toURI().toURL() + val classLoader = Thread.currentThread().contextClassLoader as URLClassLoader + val method = URLClassLoader::class.java.getDeclaredMethod("addURL", URL::class.java) + method.isAccessible = true + method.invoke(classLoader, url) + } catch (e: Exception) { + throw Exception( + "Failed to inject patches! The CLI does NOT work on Java 9 and above, please use Java 8!", + e // propagate exception + ) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/cli/utils/Patches.kt b/src/main/kotlin/app/revanced/cli/utils/Patches.kt new file mode 100644 index 00000000..65af28a5 --- /dev/null +++ b/src/main/kotlin/app/revanced/cli/utils/Patches.kt @@ -0,0 +1,15 @@ +package app.revanced.cli.utils + +import app.revanced.patches.Index + +class Patches { + companion object { + // You may ask yourself, "why do this?". + // We do it like this, because we don't want the Index class + // to be loaded while the dependency hasn't been injected yet. + // You can see this as "controlled class loading". + // Whenever this class is loaded (because it is invoked), all the imports + // will be loaded too. We don't want to do this until we've injected the class. + fun loadPatches() = Index.patches + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/cli/utils/Preconditions.kt b/src/main/kotlin/app/revanced/cli/utils/Preconditions.kt new file mode 100644 index 00000000..ff0da2d4 --- /dev/null +++ b/src/main/kotlin/app/revanced/cli/utils/Preconditions.kt @@ -0,0 +1,24 @@ +package app.revanced.cli.utils + +import java.io.File +import java.io.FileNotFoundException + +class Preconditions { + companion object { + fun isFile(path: String): File { + val f = File(path) + if (!f.exists()) { + throw FileNotFoundException(f.toString()) + } + return f + } + + fun isDirectory(path: String): File { + val f = isFile(path) + if (!f.isDirectory) { + throw IllegalArgumentException("$f is not a directory") + } + return f + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/cli/utils/SignatureParser.kt b/src/main/kotlin/app/revanced/cli/utils/SignatureParser.kt index 04d0d318..d586b0fb 100644 --- a/src/main/kotlin/app/revanced/cli/utils/SignatureParser.kt +++ b/src/main/kotlin/app/revanced/cli/utils/SignatureParser.kt @@ -4,19 +4,15 @@ import app.revanced.patcher.signature.MethodSignature import com.google.gson.JsonParser import org.jf.dexlib2.AccessFlags import org.jf.dexlib2.Opcodes -import java.io.File class SignatureParser { companion object { - fun parse(signatureJsonPath: String): List { - val json = File(signatureJsonPath).readText() + fun parse(json: String): List { val signatures = JsonParser.parseString(json).asJsonObject.get("signatures").asJsonArray.map { sig -> val signature = sig.asJsonObject - val returnType = signature.get("returns").asString var accessFlags = 0 - signature .get("accessors").asJsonArray .forEach { accessFlags = accessFlags or AccessFlags.getAccessFlag(it.asString).value }