diff --git a/src/main/kotlin/app/revanced/cli/MainCommand.kt b/src/main/kotlin/app/revanced/cli/MainCommand.kt index 7527e4e..17b8f2f 100644 --- a/src/main/kotlin/app/revanced/cli/MainCommand.kt +++ b/src/main/kotlin/app/revanced/cli/MainCommand.kt @@ -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() + internal var patchBundles = arrayOf() - @Parameters(paramLabel = "EXCLUDE", description = ["Which patches to exclude"]) - var excludedPatches = arrayOf() + @Parameters( + paramLabel = "INCLUDE", + description = ["Which patches to include. If none is specified, all compatible patches will be included."] + ) + internal var includedPatches = arrayOf() + + @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() + internal var mergeFiles = listOf() @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() - 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) { +internal fun main(args: Array) { CommandLine(MainCommand).execute(*args) } \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/cli/Patcher.kt b/src/main/kotlin/app/revanced/cli/Patcher.kt new file mode 100644 index 0000000..26c78ce --- /dev/null +++ b/src/main/kotlin/app/revanced/cli/Patcher.kt @@ -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>() + 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) + } + } + } +} diff --git a/src/main/kotlin/app/revanced/utils/patch/PatchLoader.kt b/src/main/kotlin/app/revanced/patch/PatchLoader.kt similarity index 84% rename from src/main/kotlin/app/revanced/utils/patch/PatchLoader.kt rename to src/main/kotlin/app/revanced/patch/PatchLoader.kt index a848e62..74ca2fd 100644 --- a/src/main/kotlin/app/revanced/utils/patch/PatchLoader.kt +++ b/src/main/kotlin/app/revanced/patch/PatchLoader.kt @@ -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() diff --git a/src/main/kotlin/app/revanced/utils/patch/Patches.kt b/src/main/kotlin/app/revanced/patch/Patches.kt similarity index 77% rename from src/main/kotlin/app/revanced/utils/patch/Patches.kt rename to src/main/kotlin/app/revanced/patch/Patches.kt index c561ba6..7ba8147 100644 --- a/src/main/kotlin/app/revanced/utils/patch/Patches.kt +++ b/src/main/kotlin/app/revanced/patch/Patches.kt @@ -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 } } \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/utils/Scripts.kt b/src/main/kotlin/app/revanced/utils/Scripts.kt index eeef972..31043fb 100644 --- a/src/main/kotlin/app/revanced/utils/Scripts.kt +++ b/src/main/kotlin/app/revanced/utils/Scripts.kt @@ -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" } \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/utils/dex/DexReplacer.kt b/src/main/kotlin/app/revanced/utils/dex/DexReplacer.kt deleted file mode 100644 index 9fe1cbd..0000000 --- a/src/main/kotlin/app/revanced/utils/dex/DexReplacer.kt +++ /dev/null @@ -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) { - 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) - } - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/utils/filesystem/FileSystemUtils.kt b/src/main/kotlin/app/revanced/utils/filesystem/FileSystemUtils.kt new file mode 100644 index 0000000..10559d8 --- /dev/null +++ b/src/main/kotlin/app/revanced/utils/filesystem/FileSystemUtils.kt @@ -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() + } +} \ No newline at end of file