Merge pull request #3 from ReVancedTeam/picocli

refactor: migrate to `picocli`
This commit is contained in:
oSumAtrIX 2022-05-06 03:09:17 +02:00 committed by GitHub
commit d72d04d24f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 440 additions and 445 deletions

View File

@ -24,31 +24,25 @@ 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-patcher:+")
implementation(patchesDependency)
implementation("info.picocli:picocli:+")
implementation("com.google.code.gson:gson:2.9.0")
implementation("me.tongfei:progressbar:0.9.3")
implementation("me.tongfei:progressbar:+")
implementation("com.github.li-wjohnson:jadb:master-SNAPSHOT") // using a fork instead.
implementation("org.bouncycastle:bcpkix-jdk15on:1.70")
implementation("org.bouncycastle:bcpkix-jdk15on:+")
}
val cliMainClass = "app.revanced.cli.Main"
tasks {
build {
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("Main-Class" to cliMainClass)
attributes("Main-Class" to "app.revanced.cli.MainKt")
attributes("Implementation-Title" to project.name)
attributes("Implementation-Version" to project.version)
}

View File

@ -1,178 +1,7 @@
package app.revanced.cli
import app.revanced.cli.runner.AdbRunner
import app.revanced.cli.utils.PatchLoader
import app.revanced.cli.utils.Patches
import app.revanced.cli.utils.Preconditions
import app.revanced.patcher.Patcher
import app.revanced.patcher.patch.PatchMetadata
import app.revanced.patcher.patch.PatchResult
import kotlinx.cli.ArgParser
import kotlinx.cli.ArgType
import kotlinx.cli.default
import kotlinx.cli.required
import me.tongfei.progressbar.ProgressBarBuilder
import me.tongfei.progressbar.ProgressBarStyle
import java.io.File
import java.nio.file.Files
import picocli.CommandLine
private const val CLI_NAME = "ReVanced CLI"
private val CLI_VERSION = Main::class.java.`package`.implementationVersion ?: "0.0.0-unknown"
class Main {
companion object {
private fun runCLI(
inApk: String,
inPatches: String,
inIntegrations: String?,
inOutput: String,
inRunOnAdb: String?,
hideResults: Boolean,
noLogging: Boolean,
) {
val bar = ProgressBarBuilder()
.setTaskName("Working..")
.setUpdateIntervalMillis(25)
.continuousUpdate()
.setStyle(ProgressBarStyle.ASCII)
.build()
.maxHint(1)
.setExtraMessage("Initializing")
val apk = Preconditions.isFile(inApk)
val patchesFile = Preconditions.isFile(inPatches)
val output = Preconditions.isDirectory(inOutput)
bar.step()
val patcher = Patcher(apk)
inIntegrations?.let {
bar.reset().maxHint(1)
.extraMessage = "Merging integrations"
val integrations = Preconditions.isFile(it)
patcher.addFiles(listOf(integrations))
bar.step()
}
bar.reset().maxHint(1)
.extraMessage = "Loading patches"
PatchLoader.injectPatches(patchesFile)
val patches = Patches.loadPatches().map { it() }
patcher.addPatches(patches)
bar.step()
bar.reset().maxHint(1)
.extraMessage = "Resolving signatures"
patcher.resolveSignatures()
bar.step()
val szPatches = patches.size.toLong()
bar.reset().maxHint(szPatches)
.extraMessage = "Applying patches"
val results = patcher.applyPatches {
bar.step().extraMessage = "Applying $it"
}
bar.reset().maxHint(-1)
.extraMessage = "Generating dex files"
val dexFiles = patcher.save()
val szDexFiles = dexFiles.size.toLong()
bar.reset().maxHint(szDexFiles)
.extraMessage = "Saving dex files"
dexFiles.forEach { (dexName, dexData) ->
Files.write(File(output, dexName).toPath(), dexData.data)
bar.step()
}
bar.stepTo(szDexFiles)
bar.close()
inRunOnAdb?.let { device ->
AdbRunner.runApk(
apk,
dexFiles,
output,
device,
noLogging
)
}
println("All done!")
if (!hideResults) {
printResults(results)
}
}
private fun printResults(results: Map<PatchMetadata, Result<PatchResult>>) {
for ((metadata, result) in results) {
if (result.isSuccess) {
println("${metadata.shortName} was applied successfully!")
} else {
println("${metadata.shortName} failed to apply! Cause:")
result.exceptionOrNull()!!.printStackTrace()
}
}
}
@JvmStatic
fun main(args: Array<String>) {
println("$CLI_NAME version $CLI_VERSION")
val parser = ArgParser(CLI_NAME)
// TODO: add some kind of incremental building, so merging integrations can be skipped.
// this can be achieved manually, but doing it automatically is better.
val apk by parser.option(
ArgType.String,
fullName = "apk",
shortName = "a",
description = "APK file"
).required()
val patches by parser.option(
ArgType.String,
fullName = "patches",
shortName = "p",
description = "Patches JAR file"
).required()
val integrations by parser.option(
ArgType.String,
fullName = "integrations",
shortName = "i",
description = "Integrations APK file"
)
val output by parser.option(
ArgType.String,
fullName = "output",
shortName = "o",
description = "Output directory"
).required()
val runOnAdb by parser.option(
ArgType.String,
fullName = "run-on",
description = "After the CLI is done building, which ADB device should it run on?"
)
// TODO: package name
val hideResults by parser.option(
ArgType.Boolean,
fullName = "hide-results",
description = "Don't print the patch results."
).default(false)
val noLogging by parser.option(
ArgType.Boolean,
fullName = "no-logging",
description = "Don't print the output of the application when used in combination with \"run-on\"."
).default(false)
parser.parse(args)
runCLI(
apk,
patches,
integrations,
output,
runOnAdb,
hideResults,
noLogging,
)
}
}
internal fun main(args: Array<String>) {
CommandLine(MainCommand).execute(*args)
}

View File

@ -0,0 +1,84 @@
package app.revanced.cli
import app.revanced.patch.PatchLoader
import app.revanced.patch.Patches
import app.revanced.utils.adb.Adb
import picocli.CommandLine.*
import java.io.File
@Command(
name = "ReVanced-CLI", version = ["1.0.0"], mixinStandardHelpOptions = true
)
internal object MainCommand : Runnable {
@Parameters(
paramLabel = "INCLUDE",
description = ["Which patches to include. If none is specified, all compatible patches will be included"]
)
internal var includedPatches = arrayOf<String>()
@Option(names = ["-p", "--patches"], description = ["One or more bundles of patches"])
internal var patchBundles = arrayOf<File>()
@Option(names = ["-t", "--temp-dir"], description = ["Temporal resource cache directory"], required = true)
internal lateinit var cacheDirectory: String
@Option(names = ["-r", "--resource-patcher"], description = ["Enable patching resources"])
internal var patchResources: Boolean = false
@Option(
names = ["-c", "--clean"],
description = ["Clean the temporal resource cache directory. This will be done anyways when running the patcher"]
)
internal var clean: Boolean = false
@Option(names = ["-l", "--list"], description = ["List patches only"])
internal var listOnly: Boolean = false
@Option(names = ["-m", "--merge"], description = ["One or more dex file containers to merge"])
internal var mergeFiles = listOf<File>()
@Option(names = ["-a", "--apk"], description = ["Input file to be patched"], required = true)
internal lateinit var inputFile: File
@Option(names = ["-o", "--out"], description = ["Output file path"], required = true)
internal lateinit var outputPath: String
@Option(names = ["-d", "--deploy-on"], description = ["If specified, deploy to adb device with given name"])
internal var deploy: String? = null
override fun run() {
if (listOnly) {
patchBundles.forEach {
PatchLoader.injectPatches(it)
Patches.loadPatches().forEach {
println(it().metadata)
}
}
return
}
val patcher = app.revanced.patcher.Patcher(
inputFile,
cacheDirectory,
patchResources
)
Patcher.start(patcher)
if (clean) {
File(cacheDirectory).deleteRecursively()
}
val outputFile = File(outputPath)
deploy?.let {
Adb(
outputFile,
patcher.packageName,
deploy!!
).deploy()
}
if (clean) outputFile.delete()
}
}

View File

@ -0,0 +1,91 @@
package app.revanced.cli
import app.revanced.patch.PatchLoader
import app.revanced.patch.Patches
import app.revanced.patcher.data.base.Data
import app.revanced.patcher.patch.base.Patch
import app.revanced.utils.filesystem.FileSystemUtils
import app.revanced.utils.signing.Signer
import java.io.File
internal class Patcher {
internal companion object {
internal fun start(patcher: app.revanced.patcher.Patcher) {
// merge files like necessary integrations
patcher.addFiles(MainCommand.mergeFiles)
// add patches, but filter incompatible or excluded patches
patcher.addPatchesFiltered()
// apply patches
for ((meta, result) in patcher.applyPatches {
println("Applying $it.")
}) {
println("Applied ${meta.name}. The result was $result.")
}
// write output file
val outFile = File(MainCommand.outputPath)
if (outFile.exists()) outFile.delete()
MainCommand.inputFile.copyTo(outFile)
val zipFileSystem = FileSystemUtils(outFile)
// replace all dex files
for ((name, data) in patcher.save()) {
zipFileSystem.replaceFile(name, data.data)
}
if (MainCommand.patchResources) {
for (file in File(MainCommand.cacheDirectory).resolve("build/").listFiles().first().listFiles()) {
if (!file.isDirectory) {
zipFileSystem.replaceFile(file.name, file.readBytes())
continue
}
zipFileSystem.replaceDirectory(file)
}
}
// finally close the stream
zipFileSystem.close()
// and sign the apk file
Signer.signApk(outFile)
}
private fun app.revanced.patcher.Patcher.addPatchesFiltered() {
// TODO: get package metadata (outside of this method) for apk file which needs to be patched
val packageName = this.packageName
val packageVersion = this.packageVersion
val checkInclude = MainCommand.includedPatches.isNotEmpty()
MainCommand.patchBundles.forEach { bundle ->
PatchLoader.injectPatches(bundle)
val includedPatches = mutableListOf<Patch<Data>>()
Patches.loadPatches().forEach patch@{
val patch = it()
// TODO: filter out incompatible patches with package metadata
val filterOutPatches = true
if (filterOutPatches && !patch.metadata.compatiblePackages.any { packageMetadata ->
packageMetadata.name == packageName && packageMetadata.versions.any {
it == packageVersion
}
}) {
println("Skipping ${patch.metadata.name} due to incompatibility with current package $packageName.")
return@patch
}
if (checkInclude && !MainCommand.includedPatches.contains(patch.metadata.shortName)) {
return@patch
}
println("Adding ${patch.metadata.name}.")
includedPatches.add(patch)
}
this.addPatches(includedPatches)
}
}
}
}

View File

@ -1,160 +0,0 @@
package app.revanced.cli.runner
import app.revanced.cli.utils.DexReplacer
import app.revanced.cli.utils.Scripts
import app.revanced.cli.utils.signer.Signer
import me.tongfei.progressbar.ProgressBar
import me.tongfei.progressbar.ProgressBarBuilder
import me.tongfei.progressbar.ProgressBarStyle
import org.jf.dexlib2.writer.io.MemoryDataStore
import se.vidstige.jadb.JadbConnection
import se.vidstige.jadb.JadbDevice
import se.vidstige.jadb.RemoteFile
import se.vidstige.jadb.ShellProcessBuilder
import java.io.File
import java.util.concurrent.Executors
object AdbRunner {
fun runApk(
apk: File,
dexFiles: Map<String, MemoryDataStore>,
outputDir: File,
deviceName: String,
noLogging: Boolean
) {
lateinit var dvc: JadbDevice
pbar("Initializing").use { bar ->
dvc = JadbConnection().findDevice(deviceName)
?: throw IllegalArgumentException("No such device with name $deviceName")
if (!dvc.hasSu())
throw IllegalArgumentException("Device $deviceName is not rooted or does not have su")
bar.step()
}
lateinit var tmpFile: File // we need this file at the end to clean up.
pbar("Generating APK file", 3).use { bar ->
bar.step().extraMessage = "Creating APK file"
tmpFile = File(outputDir, "revanced.apk")
apk.copyTo(tmpFile, true)
bar.step().extraMessage = "Replacing dex files"
DexReplacer.replaceDex(tmpFile, dexFiles)
bar.step().extraMessage = "Signing APK file"
try {
Signer.signApk(tmpFile)
} catch (e: SecurityException) {
throw IllegalStateException(
"A security exception occurred when signing the APK! " +
"If it has anything to with \"cannot authenticate\" then please make sure " +
"you are using Zulu or OpenJDK as they do work when using the adb runner.",
e
)
}
}
pbar("Running application", 6, false).use { bar ->
bar.step().extraMessage = "Pushing mount scripts"
dvc.push(Scripts.MOUNT_SCRIPT, RemoteFile(Scripts.SCRIPT_PATH))
dvc.cmd(Scripts.CREATE_DIR_COMMAND).assertZero()
dvc.cmd(Scripts.MV_MOUNT_COMMAND).assertZero()
dvc.cmd(Scripts.CHMOD_MOUNT_COMMAND).assertZero()
bar.step().extraMessage = "Pushing APK file"
dvc.push(tmpFile, RemoteFile(Scripts.APK_PATH))
bar.step().extraMessage = "Mounting APK file"
dvc.cmd(Scripts.STOP_APP_COMMAND).startAndWait()
dvc.cmd(Scripts.START_MOUNT_COMMAND).assertZero()
bar.step().extraMessage = "Starting APK file"
dvc.cmd(Scripts.START_APP_COMMAND).assertZero()
bar.step().setExtraMessage("Debugging APK file").refresh()
println("\nWaiting until app is closed.")
val executor = Executors.newSingleThreadExecutor()
val pipe = if (noLogging) {
ProcessBuilder.Redirect.PIPE
} else {
ProcessBuilder.Redirect.INHERIT
}
val p = dvc.cmd(Scripts.LOGCAT_COMMAND)
.redirectOutput(pipe)
.redirectError(pipe)
.useExecutor(executor)
.start()
Thread.sleep(250) // give the app some time to start up.
while (true) {
try {
while (dvc.cmd(Scripts.PIDOF_APP_COMMAND).startAndWait() == 0) {
Thread.sleep(250)
}
break
} catch (e: Exception) {
throw RuntimeException("An error occurred while monitoring state of app", e)
}
}
println("App closed, continuing.")
p.destroy()
executor.shutdown()
bar.step().extraMessage = "Unmounting APK file"
var exitCode: Int
do {
exitCode = dvc.cmd(Scripts.UNMOUNT_COMMAND).startAndWait()
} while (exitCode != 0)
}
}
}
private fun JadbDevice.push(s: String, remoteFile: RemoteFile) =
this.push(s.byteInputStream(), System.currentTimeMillis(), 644, remoteFile)
private fun JadbConnection.findDevice(device: String): JadbDevice? {
return devices.find { it.serial == device }
}
private fun JadbDevice.cmd(s: String): ShellProcessBuilder {
val args = s.split(" ") as ArrayList<String>
val cmd = args.removeFirst()
return shellProcessBuilder(cmd, *args.toTypedArray())
}
private fun JadbDevice.hasSu(): Boolean {
return cmd("su -h").startAndWait() == 0
}
private fun ShellProcessBuilder.startAndWait(): Int {
return start().waitFor()
}
private fun ShellProcessBuilder.assertZero() {
if (startAndWait() != 0) {
val cmd = getcmd()
throw IllegalStateException("ADB returned non-zero status code for command: $cmd")
}
}
private fun pbar(task: String, steps: Long = 1, update: Boolean = true): ProgressBar {
val b = ProgressBarBuilder().setTaskName(task)
if (update) b
.setUpdateIntervalMillis(250)
.continuousUpdate()
return b
.setStyle(ProgressBarStyle.ASCII)
.build()
.maxHint(steps + 1)
}
private fun ProgressBar.use(block: (ProgressBar) -> Unit) {
block(this)
stepTo(max) // step to 100%
extraMessage = "" // clear extra message
close()
}
private fun ShellProcessBuilder.getcmd(): String {
val f = this::class.java.getDeclaredField("command")
f.isAccessible = true
return f.get(this) as String
}

View File

@ -1,31 +0,0 @@
package app.revanced.cli.utils
import lanchon.multidexlib2.BasicDexFileNamer
import org.jf.dexlib2.writer.io.MemoryDataStore
import java.io.File
import java.nio.file.FileSystems
import java.nio.file.Files
val NAMER = BasicDexFileNamer()
object DexReplacer {
fun replaceDex(source: File, dexFiles: Map<String, MemoryDataStore>) {
FileSystems.newFileSystem(
source.toPath(),
null
).use { fs ->
// Delete all classes?.dex files
Files.walk(fs.rootDirectories.first()).forEach {
if (
it.toString().endsWith(".dex") &&
NAMER.isValidName(it.fileName.toString())
) Files.delete(it)
}
// Write new dex files
dexFiles
.forEach { (dexName, dexData) ->
Files.write(fs.getPath("/$dexName"), dexData.data)
}
}
}
}

View File

@ -1,24 +0,0 @@
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
}
}
}

View File

@ -1,34 +0,0 @@
package app.revanced.cli.utils
// TODO: make this a class with PACKAGE_NAME as argument, then use that everywhere.
// make sure to remove the "const" from all the vals, they won't compile obviously.
object Scripts {
private const val PACKAGE_NAME = "com.google.android.youtube"
private const val DATA_PATH = "/data/adb/ReVanced"
const val APK_PATH = "/sdcard/base.apk"
const val SCRIPT_PATH = "/sdcard/mount.sh"
val MOUNT_SCRIPT =
"""
base_path="$DATA_PATH/base.apk"
stock_path=${'$'}{ pm path $PACKAGE_NAME | grep base | sed 's/package://g' }
umount -l ${'$'}stock_path
rm ${'$'}base_path
mv "$APK_PATH" ${'$'}base_path
chmod 644 ${'$'}base_path
chown system:system ${'$'}base_path
chcon u:object_r:apk_data_file:s0 ${'$'}base_path
mount -o bind ${'$'}base_path ${'$'}stock_path
""".trimIndent()
const val PIDOF_APP_COMMAND = "pidof -s $PACKAGE_NAME"
private const val PIDOF_APP = "\$($PIDOF_APP_COMMAND)"
const val CREATE_DIR_COMMAND = "su -c \"mkdir -p $DATA_PATH/\""
const val MV_MOUNT_COMMAND = "su -c \"mv /sdcard/mount.sh $DATA_PATH/\""
const val CHMOD_MOUNT_COMMAND = "su -c \"chmod +x $DATA_PATH/mount.sh\""
const val START_MOUNT_COMMAND = "su -c $DATA_PATH/mount.sh"
const val UNMOUNT_COMMAND = "su -c \"umount -l $(pm path $PACKAGE_NAME | grep base | sed 's/package://g')\""
const val LOGCAT_COMMAND = "su -c \"logcat -c && logcat --pid=$PIDOF_APP\""
const val STOP_APP_COMMAND = "su -c \"kill $PIDOF_APP\""
const val START_APP_COMMAND = "monkey -p $PACKAGE_NAME 1"
}

View File

@ -1,12 +1,12 @@
package app.revanced.cli.utils
package app.revanced.patch
import java.io.File
import java.net.URL
import java.net.URLClassLoader
class PatchLoader {
companion object {
fun injectPatches(file: File) {
internal class PatchLoader {
internal companion object {
internal fun injectPatches(file: File) {
// This function will fail on Java 9 and above.
try {
val url = file.toURI().toURL()

View File

@ -1,15 +1,15 @@
package app.revanced.cli.utils
package app.revanced.patch
import app.revanced.patches.Index
class Patches {
companion object {
internal class Patches {
internal 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
internal fun loadPatches() = Index.patches
}
}

View File

@ -0,0 +1,94 @@
package app.revanced.utils.adb
import se.vidstige.jadb.JadbConnection
import se.vidstige.jadb.JadbDevice
import java.io.File
import java.util.concurrent.Executors
internal class Adb(
private val apk: File,
private val packageName: String,
deviceName: String,
private val logging: Boolean = true
) {
private val device: JadbDevice
init {
device = JadbConnection().devices.find { it.serial == deviceName }
?: throw IllegalArgumentException("No such device with name $deviceName")
if (device.run("su -h", false) != 0)
throw IllegalArgumentException("Root required on $deviceName. Deploying failed.")
}
private fun String.replacePlaceholder(): String {
return this.replace(Constants.PLACEHOLDER, packageName)
}
internal fun deploy() {
// create revanced path
device.run("${Constants.COMMAND_CREATE_DIR} ${Constants.PATH_REVANCED}")
// push patched file
device.copy(Constants.PATH_INIT_PUSH, apk)
// install apk
device.run(Constants.COMMAND_INSTALL_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())
// push umount script
device.createFile(
Constants.PATH_INIT_PUSH,
Constants.CONTENT_UMOUNT_SCRIPT.replacePlaceholder()
)
// install mount script
device.run(Constants.COMMAND_INSTALL_UMOUNT.replacePlaceholder())
// unmount the apk for sanity
device.run(Constants.PATH_UMOUNT.replacePlaceholder())
// mount the apk
device.run(Constants.PATH_MOUNT.replacePlaceholder())
// relaunch app
device.run(Constants.COMMAND_RESTART.replacePlaceholder())
// log the app
log()
}
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 state of app", e)
}
}
println("App closed, continuing.")
process.destroy()
executor.shutdown()
}
}

View File

@ -0,0 +1,29 @@
package app.revanced.utils.adb
import se.vidstige.jadb.JadbDevice
import se.vidstige.jadb.RemoteFile
import se.vidstige.jadb.ShellProcessBuilder
import java.io.File
internal fun JadbDevice.buildCommand(command: String, su: Boolean = true): ShellProcessBuilder {
if (su) {
return shellProcessBuilder("su -c \'$command\'")
}
val args = command.split(" ") as ArrayList<String>
val cmd = args.removeFirst()
return shellProcessBuilder(cmd, *args.toTypedArray())
}
internal fun JadbDevice.run(command: String, su: Boolean = true): Int {
return this.buildCommand(command, su).start().waitFor()
}
internal fun JadbDevice.copy(targetPath: String, file: File) {
push(file, RemoteFile(targetPath))
}
internal fun JadbDevice.createFile(targetFile: String, content: String) {
push(content.byteInputStream(), System.currentTimeMillis(), 644, RemoteFile(targetFile))
}

View File

@ -0,0 +1,57 @@
package app.revanced.utils.adb
internal object Constants {
// template placeholder to replace a string in commands
internal const val PLACEHOLDER = "TEMPLATE_PACKAGE_NAME"
// utility commands
private const val COMMAND_CHMOD_MOUNT = "chmod +x"
internal const val COMMAND_PID_OF = "pidof -s"
internal const val COMMAND_CREATE_DIR = "mkdir -p"
internal const val COMMAND_LOGCAT = "logcat -c && logcat --pid=$($COMMAND_PID_OF $PLACEHOLDER)"
internal const val COMMAND_RESTART = "monkey -p $PLACEHOLDER 1 && kill ${'$'}($COMMAND_PID_OF $PLACEHOLDER)"
// default mount file name
private const val NAME_MOUNT_SCRIPT = "mount_$PLACEHOLDER.sh"
// initial directory to push files to via adb push
internal const val PATH_INIT_PUSH = "/sdcard/revanced.delete"
// revanced path
internal const val PATH_REVANCED = "/data/adb/revanced/"
// revanced apk path
private const val PATH_REVANCED_APP = "$PATH_REVANCED$PLACEHOLDER.apk"
// (un)mount script paths
internal const val PATH_MOUNT = "/data/adb/service.d/$NAME_MOUNT_SCRIPT"
internal const val PATH_UMOUNT = "/data/adb/post-fs-data.d/un$NAME_MOUNT_SCRIPT"
// move to revanced apk path & set permissions
internal const val COMMAND_INSTALL_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"
// install mount script & set permissions
internal const val COMMAND_INSTALL_MOUNT = "mv $PATH_INIT_PUSH $PATH_MOUNT && $COMMAND_CHMOD_MOUNT $PATH_MOUNT"
// install umount script & set permissions
internal const val COMMAND_INSTALL_UMOUNT = "mv $PATH_INIT_PUSH $PATH_UMOUNT && $COMMAND_CHMOD_MOUNT $PATH_UMOUNT"
// unmount script
internal val CONTENT_UMOUNT_SCRIPT =
"""
#!/system/bin/sh
while read line; do echo ${'$'}{line} | grep $PLACEHOLDER | awk '{print ${'$'}2}' | xargs umount -l; done< /proc/mounts
""".trimIndent()
// mount script
internal val CONTENT_MOUNT_SCRIPT =
"""
#!/system/bin/sh
while [ "${'$'}(getprop sys.boot_completed | tr -d '\r')" != "1" ]; do sleep 1; done
base_path="$PATH_REVANCED_APP"
stock_path=${'$'}{ pm path $PLACEHOLDER | grep base | sed 's/package://g' }
mount -o bind ${'$'}base_path ${'$'}stock_path
""".trimIndent()
}

View File

@ -0,0 +1,66 @@
package app.revanced.utils.filesystem
import java.io.Closeable
import java.io.File
import java.nio.file.FileSystem
import java.nio.file.FileSystems
import java.nio.file.Files
internal class FileSystemUtils(
file: File
) : Closeable {
private var fileSystem: FileSystem
init {
fileSystem = FileSystems.newFileSystem(
file.toPath(),
null
)
}
private fun deleteDirectory(dirPath: String) {
val files = Files.walk(fileSystem.getPath("$dirPath/"))
files
.sorted(Comparator.reverseOrder())
.forEach {
Files.delete(it)
}
files.close()
}
internal fun replaceDirectory(replacement: File) {
if (!replacement.isDirectory) throw Exception("${replacement.name} is not a directory.")
// FIXME: make this delete the directory recursively
//deleteDirectory(replacement.name)
//val path = Files.createDirectory(fileSystem.getPath(replacement.name))
val excludeFromPath = replacement.path.removeSuffix(replacement.name)
for (path in Files.walk(replacement.toPath())) {
val file = path.toFile()
if (file.isDirectory) {
val relativePath = path.toString().removePrefix(excludeFromPath)
val fileSystemPath = fileSystem.getPath(relativePath)
if (!Files.exists(fileSystemPath)) Files.createDirectory(fileSystemPath)
continue
}
replaceFile(path.toString().removePrefix(excludeFromPath), file.readBytes())
}
}
internal fun replaceFile(sourceFile: String, content: ByteArray) {
val path = fileSystem.getPath(sourceFile)
Files.deleteIfExists(path)
Files.write(path, content)
}
override fun close() {
fileSystem.close()
}
}

View File

@ -1,4 +1,4 @@
package app.revanced.cli.utils.signer
package app.revanced.utils.signing
import java.security.PrivateKey
import java.security.cert.X509Certificate

View File

@ -3,7 +3,7 @@
* Licensed under the Open Software License version 3.0
*/
package app.revanced.cli.utils.signer
package app.revanced.utils.signing
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
@ -41,7 +41,7 @@ val PASSWORD = "revanced".toCharArray() // TODO: make it secure; random password
/**
* APK Signer.
* @author Aliucord authors
* @author ReVanced Team
* @author ReVanced team
*/
object Signer {
private fun newKeystore(out: File) {