add: resource patcher

Signed-off-by: oSumAtrIX <johan.melkonyan1@web.de>
This commit is contained in:
oSumAtrIX 2022-05-04 23:48:11 +02:00
parent f6d60a3460
commit 57af32208d
No known key found for this signature in database
GPG Key ID: A9B3094ACDB604B4
7 changed files with 206 additions and 127 deletions

View File

@ -1,40 +1,41 @@
package app.revanced.cli
import app.revanced.cli.MainCommand.excludedPatches
import app.revanced.cli.MainCommand.patchBundles
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 app.revanced.patch.PatchLoader
import app.revanced.patch.Patches
import picocli.CommandLine
import picocli.CommandLine.*
import java.io.File
@Command(
name = "ReVanced-CLI",
version = ["1.0.0"],
mixinStandardHelpOptions = true
name = "ReVanced-CLI", version = ["1.0.0"], mixinStandardHelpOptions = true
)
object MainCommand : Runnable {
internal object MainCommand : Runnable {
@Option(names = ["-p", "--patches"], description = ["One or more bundles of patches"])
var patchBundles = arrayOf<File>()
internal var patchBundles = arrayOf<File>()
@Parameters(paramLabel = "EXCLUDE", description = ["Which patches to exclude"])
var excludedPatches = arrayOf<String>()
@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 = ["-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"])
var mergeFiles = listOf<File>()
internal var mergeFiles = listOf<File>()
@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)
lateinit var outputPath: String
internal lateinit var outputPath: String
override fun run() {
if (listOnly) {
@ -47,64 +48,10 @@ object MainCommand : Runnable {
return
}
val patcher = Patcher(inputFile)
// 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
val outFile = File(outputPath)
inputFile.copyTo(outFile)
DexReplacer.replaceDex(outFile, patcher.save())
// sign the apk file
Signer.signApk(outFile)
Patcher.run()
}
}
private fun Patcher.addPatchesFiltered() {
// 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>) {
internal fun main(args: Array<String>) {
CommandLine(MainCommand).execute(*args)
}

View 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)
}
}
}
}

View File

@ -1,12 +1,12 @@
package app.revanced.utils.patch
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.utils.patch
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

@ -2,13 +2,13 @@ package app.revanced.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 {
internal object Scripts {
private const val PACKAGE_NAME = "com.google.android.apps.youtube.music"
private const val DATA_PATH = "/data/adb/ReVanced"
const val APK_PATH = "/sdcard/base.apk"
const val SCRIPT_PATH = "/sdcard/mount.sh"
internal const val APK_PATH = "/sdcard/base.apk"
internal const val SCRIPT_PATH = "/sdcard/mount.sh"
val MOUNT_SCRIPT =
internal val MOUNT_SCRIPT =
"""
base_path="$DATA_PATH/base.apk"
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
""".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)"
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"
internal const val CREATE_DIR_COMMAND = "su -c \"mkdir -p $DATA_PATH/\""
internal const val MV_MOUNT_COMMAND = "su -c \"mv /sdcard/mount.sh $DATA_PATH/\""
internal const val CHMOD_MOUNT_COMMAND = "su -c \"chmod +x $DATA_PATH/mount.sh\""
internal const val START_MOUNT_COMMAND = "su -c $DATA_PATH/mount.sh"
internal const val UNMOUNT_COMMAND =
"su -c \"umount -l $(pm path $PACKAGE_NAME | grep base | sed 's/package://g')\""
internal const val LOGCAT_COMMAND = "su -c \"logcat -c && logcat --pid=$PIDOF_APP\""
internal const val STOP_APP_COMMAND = "su -c \"kill $PIDOF_APP\""
internal const val START_APP_COMMAND = "monkey -p $PACKAGE_NAME 1"
}

View File

@ -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)
}
}
}
}

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()
}
}