diff --git a/build.gradle.kts b/build.gradle.kts index af31797..9be26ee 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 41647df..bad34ce 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()) - // save patched apk - val dexFiles: Map = patcher.save() - dexFiles.forEach { (t, p) -> - Files.write(File(args[1], t).toPath(), p.buffer) + 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 + + 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 0000000..9da9825 --- /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 0000000..65af28a --- /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 0000000..ff0da2d --- /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 04d0d31..d586b0f 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 }