Skip to content

Commit

Permalink
feat: load patches dynamically & use kotlinx.cli
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Sculas committed Apr 10, 2022
1 parent e50071a commit 4624384
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 26 deletions.
14 changes: 12 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand All @@ -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)
}
}
}
98 changes: 79 additions & 19 deletions src/main/kotlin/app/revanced/cli/Main.kt
Original file line number Diff line number Diff line change
@@ -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<String>) {
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<String>) {
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<String, MemoryDataStore> = patcher.save()
dexFiles.forEach { (t, p) ->
Files.write(File(args[1], t).toPath(), p.buffer)
parser.parse(args)
runCLI(
apk,
signatures,
patches,
output,
)
}
}
}
25 changes: 25 additions & 0 deletions src/main/kotlin/app/revanced/cli/utils/PatchLoader.kt
Original file line number Diff line number Diff line change
@@ -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
)
}
}
}
}
15 changes: 15 additions & 0 deletions src/main/kotlin/app/revanced/cli/utils/Patches.kt
Original file line number Diff line number Diff line change
@@ -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
}
}
24 changes: 24 additions & 0 deletions src/main/kotlin/app/revanced/cli/utils/Preconditions.kt
Original file line number Diff line number Diff line change
@@ -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
}
}
}
6 changes: 1 addition & 5 deletions src/main/kotlin/app/revanced/cli/utils/SignatureParser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<MethodSignature> {
val json = File(signatureJsonPath).readText()
fun parse(json: String): List<MethodSignature> {
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 }
Expand Down

0 comments on commit 4624384

Please sign in to comment.