mirror of
https://github.com/revanced/revanced-cli.git
synced 2024-12-05 01:42:54 +01:00
Merge pull request #5 from ReVancedTeam/resource-patcher
add: support for resource patching
This commit is contained in:
commit
eab58aa0a2
@ -1,40 +1,45 @@
|
|||||||
package app.revanced.cli
|
package app.revanced.cli
|
||||||
|
|
||||||
import app.revanced.cli.MainCommand.excludedPatches
|
import app.revanced.patch.PatchLoader
|
||||||
import app.revanced.cli.MainCommand.patchBundles
|
import app.revanced.patch.Patches
|
||||||
import app.revanced.patcher.Patcher
|
|
||||||
import app.revanced.patcher.patch.Patch
|
|
||||||
import app.revanced.utils.dex.DexReplacer
|
|
||||||
import app.revanced.utils.patch.PatchLoader
|
|
||||||
import app.revanced.utils.patch.Patches
|
|
||||||
import app.revanced.utils.signing.Signer
|
|
||||||
import picocli.CommandLine
|
import picocli.CommandLine
|
||||||
import picocli.CommandLine.*
|
import picocli.CommandLine.*
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@Command(
|
@Command(
|
||||||
name = "ReVanced-CLI",
|
name = "ReVanced-CLI", version = ["1.0.0"], mixinStandardHelpOptions = true
|
||||||
version = ["1.0.0"],
|
|
||||||
mixinStandardHelpOptions = true
|
|
||||||
)
|
)
|
||||||
object MainCommand : Runnable {
|
internal object MainCommand : Runnable {
|
||||||
@Option(names = ["-p", "--patches"], description = ["One or more bundles of patches"])
|
@Option(names = ["-p", "--patches"], description = ["One or more bundles of patches"])
|
||||||
var patchBundles = arrayOf<File>()
|
internal var patchBundles = arrayOf<File>()
|
||||||
|
|
||||||
|
@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 = ["-c", "--cache"], description = ["Output 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 = ["-w", "--wipe-after"], description = ["Wipe the temporal directory before exiting the patcher"])
|
||||||
|
internal var wipe: Boolean = false
|
||||||
|
|
||||||
@Parameters(paramLabel = "EXCLUDE", description = ["Which patches to exclude"])
|
|
||||||
var excludedPatches = arrayOf<String>()
|
|
||||||
|
|
||||||
@Option(names = ["-l", "--list"], description = ["List patches only"])
|
@Option(names = ["-l", "--list"], description = ["List patches only"])
|
||||||
var listOnly: Boolean = false
|
internal var listOnly: Boolean = false
|
||||||
|
|
||||||
@Option(names = ["-m", "--merge"], description = ["One or more dex file containers to merge"])
|
@Option(names = ["-m", "--merge"], description = ["One or more dex file containers to merge"])
|
||||||
var mergeFiles = listOf<File>()
|
internal var mergeFiles = listOf<File>()
|
||||||
|
|
||||||
@Option(names = ["-a", "--apk"], description = ["Input file to be patched"], required = true)
|
@Option(names = ["-a", "--apk"], description = ["Input file to be patched"], required = true)
|
||||||
lateinit var inputFile: File
|
internal lateinit var inputFile: File
|
||||||
|
|
||||||
@Option(names = ["-o", "--out"], description = ["Output file path"], required = true)
|
@Option(names = ["-o", "--out"], description = ["Output file path"], required = true)
|
||||||
lateinit var outputPath: String
|
internal lateinit var outputPath: String
|
||||||
|
|
||||||
override fun run() {
|
override fun run() {
|
||||||
if (listOnly) {
|
if (listOnly) {
|
||||||
@ -47,64 +52,13 @@ object MainCommand : Runnable {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val patcher = Patcher(inputFile)
|
Patcher.run()
|
||||||
// merge files like necessary integrations
|
|
||||||
patcher.addFiles(mergeFiles)
|
|
||||||
// add patches, but filter incompatible or excluded patches
|
|
||||||
patcher.addPatchesFiltered()
|
|
||||||
// apply patches
|
|
||||||
for (patchResult in patcher.applyPatches {
|
|
||||||
println("Applying: $it")
|
|
||||||
}) {
|
|
||||||
println(patchResult)
|
|
||||||
}
|
|
||||||
|
|
||||||
// write output file
|
if (!wipe) return
|
||||||
val outFile = File(outputPath)
|
File(cacheDirectory).deleteRecursively()
|
||||||
inputFile.copyTo(outFile)
|
|
||||||
DexReplacer.replaceDex(outFile, patcher.save())
|
|
||||||
|
|
||||||
// sign the apk file
|
|
||||||
Signer.signApk(outFile)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Patcher.addPatchesFiltered() {
|
internal fun main(args: Array<String>) {
|
||||||
// TODO: get package metadata (outside of this method) for apk file which needs to be patched
|
|
||||||
val packageName = "com.example.exampleApp"
|
|
||||||
val packageVersion = "1.2.3"
|
|
||||||
|
|
||||||
patchBundles.forEach { bundle ->
|
|
||||||
PatchLoader.injectPatches(bundle)
|
|
||||||
val includedPatches = mutableListOf<Patch>()
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
// TODO: report to stdout
|
|
||||||
return@patch
|
|
||||||
}
|
|
||||||
|
|
||||||
if (excludedPatches.contains(patch.metadata.shortName)) {
|
|
||||||
// TODO: report to stdout
|
|
||||||
return@patch
|
|
||||||
}
|
|
||||||
|
|
||||||
includedPatches.add(patch)
|
|
||||||
}
|
|
||||||
this.addPatches(includedPatches)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun main(args: Array<String>) {
|
|
||||||
CommandLine(MainCommand).execute(*args)
|
CommandLine(MainCommand).execute(*args)
|
||||||
}
|
}
|
96
src/main/kotlin/app/revanced/cli/Patcher.kt
Normal file
96
src/main/kotlin/app/revanced/cli/Patcher.kt
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
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 run() {
|
||||||
|
val patcher = app.revanced.patcher.Patcher(
|
||||||
|
MainCommand.inputFile,
|
||||||
|
MainCommand.cacheDirectory,
|
||||||
|
MainCommand.patchResources
|
||||||
|
)
|
||||||
|
|
||||||
|
// merge files like necessary integrations
|
||||||
|
patcher.addFiles(MainCommand.mergeFiles)
|
||||||
|
// add patches, but filter incompatible or excluded patches
|
||||||
|
patcher.addPatchesFiltered()
|
||||||
|
// apply patches
|
||||||
|
for (patchResult in patcher.applyPatches {
|
||||||
|
println("Applying: $it")
|
||||||
|
}) {
|
||||||
|
println(patchResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
// TODO: report to stdout
|
||||||
|
return@patch
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checkInclude && !MainCommand.includedPatches.contains(patch.metadata.shortName)) {
|
||||||
|
return@patch
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: report to stdout
|
||||||
|
includedPatches.add(patch)
|
||||||
|
|
||||||
|
}
|
||||||
|
this.addPatches(includedPatches)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,12 +1,12 @@
|
|||||||
package app.revanced.utils.patch
|
package app.revanced.patch
|
||||||
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.net.URLClassLoader
|
import java.net.URLClassLoader
|
||||||
|
|
||||||
class PatchLoader {
|
internal class PatchLoader {
|
||||||
companion object {
|
internal companion object {
|
||||||
fun injectPatches(file: File) {
|
internal fun injectPatches(file: File) {
|
||||||
// This function will fail on Java 9 and above.
|
// This function will fail on Java 9 and above.
|
||||||
try {
|
try {
|
||||||
val url = file.toURI().toURL()
|
val url = file.toURI().toURL()
|
@ -1,15 +1,15 @@
|
|||||||
package app.revanced.utils.patch
|
package app.revanced.patch
|
||||||
|
|
||||||
import app.revanced.patches.Index
|
import app.revanced.patches.Index
|
||||||
|
|
||||||
class Patches {
|
internal class Patches {
|
||||||
companion object {
|
internal companion object {
|
||||||
// You may ask yourself, "why do this?".
|
// You may ask yourself, "why do this?".
|
||||||
// We do it like this, because we don't want the Index class
|
// We do it like this, because we don't want the Index class
|
||||||
// to be loaded while the dependency hasn't been injected yet.
|
// to be loaded while the dependency hasn't been injected yet.
|
||||||
// You can see this as "controlled class loading".
|
// You can see this as "controlled class loading".
|
||||||
// Whenever this class is loaded (because it is invoked), all the imports
|
// 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.
|
// 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
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -2,13 +2,13 @@ package app.revanced.utils
|
|||||||
|
|
||||||
// TODO: make this a class with PACKAGE_NAME as argument, then use that everywhere.
|
// 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.
|
// make sure to remove the "const" from all the vals, they won't compile obviously.
|
||||||
object Scripts {
|
internal object Scripts {
|
||||||
private const val PACKAGE_NAME = "com.google.android.apps.youtube.music"
|
private const val PACKAGE_NAME = "com.google.android.apps.youtube.music"
|
||||||
private const val DATA_PATH = "/data/adb/ReVanced"
|
private const val DATA_PATH = "/data/adb/ReVanced"
|
||||||
const val APK_PATH = "/sdcard/base.apk"
|
internal const val APK_PATH = "/sdcard/base.apk"
|
||||||
const val SCRIPT_PATH = "/sdcard/mount.sh"
|
internal const val SCRIPT_PATH = "/sdcard/mount.sh"
|
||||||
|
|
||||||
val MOUNT_SCRIPT =
|
internal val MOUNT_SCRIPT =
|
||||||
"""
|
"""
|
||||||
base_path="$DATA_PATH/base.apk"
|
base_path="$DATA_PATH/base.apk"
|
||||||
stock_path=${'$'}{ pm path $PACKAGE_NAME | grep base | sed 's/package://g' }
|
stock_path=${'$'}{ pm path $PACKAGE_NAME | grep base | sed 's/package://g' }
|
||||||
@ -21,14 +21,15 @@ object Scripts {
|
|||||||
mount -o bind ${'$'}base_path ${'$'}stock_path
|
mount -o bind ${'$'}base_path ${'$'}stock_path
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
|
||||||
const val PIDOF_APP_COMMAND = "pidof -s $PACKAGE_NAME"
|
internal const val PIDOF_APP_COMMAND = "pidof -s $PACKAGE_NAME"
|
||||||
private const val PIDOF_APP = "\$($PIDOF_APP_COMMAND)"
|
private const val PIDOF_APP = "\$($PIDOF_APP_COMMAND)"
|
||||||
const val CREATE_DIR_COMMAND = "su -c \"mkdir -p $DATA_PATH/\""
|
internal const val CREATE_DIR_COMMAND = "su -c \"mkdir -p $DATA_PATH/\""
|
||||||
const val MV_MOUNT_COMMAND = "su -c \"mv /sdcard/mount.sh $DATA_PATH/\""
|
internal 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\""
|
internal const val CHMOD_MOUNT_COMMAND = "su -c \"chmod +x $DATA_PATH/mount.sh\""
|
||||||
const val START_MOUNT_COMMAND = "su -c $DATA_PATH/mount.sh"
|
internal 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')\""
|
internal const val UNMOUNT_COMMAND =
|
||||||
const val LOGCAT_COMMAND = "su -c \"logcat -c && logcat --pid=$PIDOF_APP\""
|
"su -c \"umount -l $(pm path $PACKAGE_NAME | grep base | sed 's/package://g')\""
|
||||||
const val STOP_APP_COMMAND = "su -c \"kill $PIDOF_APP\""
|
internal const val LOGCAT_COMMAND = "su -c \"logcat -c && logcat --pid=$PIDOF_APP\""
|
||||||
const val START_APP_COMMAND = "monkey -p $PACKAGE_NAME 1"
|
internal const val STOP_APP_COMMAND = "su -c \"kill $PIDOF_APP\""
|
||||||
|
internal const val START_APP_COMMAND = "monkey -p $PACKAGE_NAME 1"
|
||||||
}
|
}
|
@ -1,31 +0,0 @@
|
|||||||
package app.revanced.utils.dex
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user