diff --git a/docs/1_usage.md b/docs/1_usage.md index 31ee72a3..7e515426 100644 --- a/docs/1_usage.md +++ b/docs/1_usage.md @@ -13,10 +13,12 @@ java -jar revanced-cli.jar -h ## 📃 List patches ```bash -java -jar revanced-cli.jar list-patches --with-descriptions --with-packages --with-versions --with-options --with-universal-patches revanced-patches.rvp +java -jar revanced-cli.jar list-patches --with-packages --with-versions --with-options revanced-patches.rvp ``` -## 💉 Patch an app with the default list of patches +## 💉 Patch an app + +To patch an app using the default list of patches, use the `patch` command: ```bash java -jar revanced-cli.jar patch -b revanced-patches.rvp input.apk @@ -28,22 +30,37 @@ You can also use multiple patch bundles: java -jar revanced-cli.jar patch -b revanced-patches.rvp -b another-patches.rvp input.apk ``` -To manually include or exclude patches, use the options `-i` and `-e`. -Keep in mind the name of the patch must be an exact match. -You can also use the options `--ii` and `--ie` to include or exclude patches by their index -if two patches have the same name. -To know the indices of patches, use the option `--with-indices` when listing patches: +To change the default set of used patches, use the option `-i` or `-e` to use or disuse specific patches. +You can use the `list-patches` command to see which patches are used by default. + +To only use specific patches, you can use the option `--exclusive` combined with `-i`. +Remember that the options `-i` and `-e` match the patch's name exactly. Here is an example: ```bash -java -jar revanced-cli.jar list-patches --with-indices revanced-patches.rvp +java -jar revanced-cli.jar patch -b revanced-patches.rvp --exclusive -i "Patch name" -i "Another patch name" input.apk ``` -Then you can use the indices to include or exclude patches: +You can also use the options `--ii` and `--ie` to use or disuse patches by their index. +This is useful, if two patches happen to have the same name. +To know the indices of patches, use the command `list-patches`: + +```bash +java -jar revanced-cli.jar list-patches revanced-patches.rvp +``` + +Then you can use the indices to use or disuse patches: ```bash java -jar revanced-cli.jar patch -b revanced-patches.rvp --ii 123 --ie 456 input.apk ``` +You can combine the option `-i`, `-e`, `--ii`, `--ie` and `--exclusive`. Here is an example: + +```bash +java -jar revanced-cli.jar patch -b revanced-patches.rvp --exclusive -i "Patch name" --ii 123 input.apk +``` + + > [!TIP] > You can use the option `-d` to automatically install the patched app after patching. > Make sure ADB is working: @@ -62,7 +79,61 @@ java -jar revanced-cli.jar patch -b revanced-patches.rvp --ii 123 --ie 456 input > adb install input.apk > ``` -## 📦 Install an app manually +Patches can have options you can set using the option `-O` alongside the option to include the patch by name or index. +To know the options of a patch, use the option `--with-options` when listing patches: + +```bash +java -jar revanced-cli.jar list-patches --with-options revanced-patches.rvp +``` + +Each patch can have multiple options. You can set them using the option `-O`. +For example, to set the options for the patch with the name `Patch name` +with the key `key1` and `key2` to `value1` and `value2` respectively, use the following command: + +```bash +java -jar revanced-cli.jar patch -b revanced-patches.rvp -i "Patch name" -Okey1=value1 -Okey2=value2 input.apk +``` + +If you want to set a value to `null`, you can omit the value: + +```bash +java -jar revanced-cli.jar patch -b revanced-patches.rvp -i "Patch name" -Okey1 input.apk +``` + +> [!WARNING] +> Option values are usually typed. If you set a value with the wrong type, the patch can fail. +> Option value types can be seen when listing patches with the option `--with-options`. +> +> Example option values: +> +> - String: `string` +> - Boolean: `true`, `false` +> - Integer: `123` +> - Double: `1.0` +> - Float: `1.0f` +> - Long: `1234567890`, `1L` +> - List: `[item1,item2,item3]` +> - List of type `Any`: `[item1,123,true,1.0]` +> - Empty list of type `Any`: `[]` +> - Typed empty list: `int[]` +> - Typed and nested empty list: `[int[]]` +> - List with null value and two empty strings: `[null,\'\',\"\"]` +> +> Quotes and commas escaped in strings (`\"`, `\'`, `\,`) are parsed as part of the string. +> List items are recursively parsed, so you can escape values in lists: +> +> - Escaped integer as a string: `[\'123\']` +> - Escaped boolean as a string: `[\'true\']` +> - Escaped list as a string: `[\'[item1,item2]\']` +> - Escaped null value as a string: `[\'null\']` +> - List with an integer, an integer as a string and a string with a comma, and an escaped list: [`123,\'123\',str\,ing`,`\'[]\'`] +> +> Example command with an escaped integer as a string: +> +> ```bash +> java -jar revanced-cli.jar -b revanced-patches.rvp -i "Patch name" -OstringKey=\'1\' input.apk +> ``` +## 📦 Install an app manually ```bash java -jar revanced-cli.jar utility install -a input.apk diff --git a/src/main/kotlin/app/revanced/cli/command/CommandUtils.kt b/src/main/kotlin/app/revanced/cli/command/CommandUtils.kt new file mode 100644 index 00000000..afb365b6 --- /dev/null +++ b/src/main/kotlin/app/revanced/cli/command/CommandUtils.kt @@ -0,0 +1,105 @@ +package app.revanced.cli.command + +import picocli.CommandLine + +class OptionKeyConverter : CommandLine.ITypeConverter { + override fun convert(value: String): String = value +} + +class OptionValueConverter : CommandLine.ITypeConverter { + override fun convert(value: String?): Any? { + value ?: return null + + return when { + value.startsWith("[") && value.endsWith("]") -> { + val innerValue = value.substring(1, value.length - 1) + + buildList { + var nestLevel = 0 + var insideQuote = false + var escaped = false + + val item = buildString { + for (char in innerValue) { + when (char) { + '\\' -> { + if (escaped || nestLevel != 0) { + append(char) + } + + escaped = !escaped + } + + '"', '\'' -> { + if (!escaped) { + insideQuote = !insideQuote + } else { + escaped = false + } + + append(char) + } + + '[' -> { + if (!insideQuote) { + nestLevel++ + } + + append(char) + } + + ']' -> { + if (!insideQuote) { + nestLevel-- + + if (nestLevel == -1) { + return value + } + } + + append(char) + } + + ',' -> if (nestLevel == 0) { + if (insideQuote) { + append(char) + } else { + add(convert(toString())) + setLength(0) + } + } else { + append(char) + } + + else -> append(char) + } + } + } + + if (item.isNotEmpty()) { + add(convert(item)) + } + } + } + + value.startsWith("\"") && value.endsWith("\"") -> value.substring(1, value.length - 1) + value.startsWith("'") && value.endsWith("'") -> value.substring(1, value.length - 1) + value.endsWith("f") -> value.dropLast(1).toFloat() + value.endsWith("L") -> value.dropLast(1).toLong() + value.equals("true", ignoreCase = true) -> true + value.equals("false", ignoreCase = true) -> false + value.toIntOrNull() != null -> value.toInt() + value.toLongOrNull() != null -> value.toLong() + value.toDoubleOrNull() != null -> value.toDouble() + value.toFloatOrNull() != null -> value.toFloat() + value == "null" -> null + value == "int[]" -> emptyList() + value == "long[]" -> emptyList() + value == "double[]" -> emptyList() + value == "float[]" -> emptyList() + value == "boolean[]" -> emptyList() + value == "string[]" -> emptyList() + else -> value + } + } +} diff --git a/src/main/kotlin/app/revanced/cli/command/ListCompatibleVersions.kt b/src/main/kotlin/app/revanced/cli/command/ListCompatibleVersions.kt index 31f5cfa4..ba993c7c 100644 --- a/src/main/kotlin/app/revanced/cli/command/ListCompatibleVersions.kt +++ b/src/main/kotlin/app/revanced/cli/command/ListCompatibleVersions.kt @@ -1,8 +1,8 @@ package app.revanced.cli.command import app.revanced.library.PackageName -import app.revanced.library.PatchUtils import app.revanced.library.VersionMap +import app.revanced.library.mostCommonCompatibleVersions import app.revanced.patcher.patch.loadPatchesFromJar import picocli.CommandLine import java.io.File @@ -12,11 +12,11 @@ import java.util.logging.Logger name = "list-versions", description = [ "List the most common compatible versions of apps that are compatible " + - "with the patches in the supplied patch bundles.", + "with the patches in the supplied patch bundles.", ], ) internal class ListCompatibleVersions : Runnable { - private val logger = Logger.getLogger(ListCompatibleVersions::class.java.name) + private val logger = Logger.getLogger(this::class.java.name) @CommandLine.Parameters( description = ["Paths to patch bundles."], @@ -58,8 +58,7 @@ internal class ListCompatibleVersions : Runnable { val patches = loadPatchesFromJar(patchBundles) - PatchUtils.getMostCommonCompatibleVersions( - patches, + patches.mostCommonCompatibleVersions( packageNames, countUnusedPatches, ).entries.joinToString("\n", transform = ::buildString).let(logger::info) diff --git a/src/main/kotlin/app/revanced/cli/command/ListPatchesCommand.kt b/src/main/kotlin/app/revanced/cli/command/ListPatchesCommand.kt index d3c66a10..fcdb3a57 100644 --- a/src/main/kotlin/app/revanced/cli/command/ListPatchesCommand.kt +++ b/src/main/kotlin/app/revanced/cli/command/ListPatchesCommand.kt @@ -14,7 +14,7 @@ import app.revanced.patcher.patch.Option as PatchOption description = ["List patches from supplied patch bundles."], ) internal object ListPatchesCommand : Runnable { - private val logger = Logger.getLogger(ListPatchesCommand::class.java.name) + private val logger = Logger.getLogger(this::class.java.name) @Parameters( description = ["Paths to patch bundles."], @@ -95,9 +95,11 @@ internal object ListPatchesCommand : Runnable { } ?: append("Key: $key") values?.let { values -> - appendLine("\nValid values:") + appendLine("\nPossible values:") append(values.map { "${it.value} (${it.key})" }.joinToString("\n").prependIndent("\t")) } + + append("\nType: $type") } fun IndexedValue>.buildString() = diff --git a/src/main/kotlin/app/revanced/cli/command/MainCommand.kt b/src/main/kotlin/app/revanced/cli/command/MainCommand.kt index 4d33a4de..5ceeef15 100644 --- a/src/main/kotlin/app/revanced/cli/command/MainCommand.kt +++ b/src/main/kotlin/app/revanced/cli/command/MainCommand.kt @@ -34,7 +34,6 @@ private object CLIVersionProvider : IVersionProvider { versionProvider = CLIVersionProvider::class, subcommands = [ PatchCommand::class, - OptionsCommand::class, ListPatchesCommand::class, ListCompatibleVersions::class, UtilityCommand::class, diff --git a/src/main/kotlin/app/revanced/cli/command/OptionsCommand.kt b/src/main/kotlin/app/revanced/cli/command/OptionsCommand.kt deleted file mode 100644 index dbc1ada3..00000000 --- a/src/main/kotlin/app/revanced/cli/command/OptionsCommand.kt +++ /dev/null @@ -1,62 +0,0 @@ -package app.revanced.cli.command - -import app.revanced.library.Options -import app.revanced.library.Options.setOptions -import app.revanced.patcher.patch.loadPatchesFromJar -import picocli.CommandLine -import picocli.CommandLine.Help.Visibility.ALWAYS -import java.io.File -import java.util.logging.Logger - -@CommandLine.Command( - name = "options", - description = ["Generate options file from patches."], -) -internal object OptionsCommand : Runnable { - private val logger = Logger.getLogger(OptionsCommand::class.java.name) - - @CommandLine.Parameters( - description = ["Paths to patch bundles."], - arity = "1..*", - ) - private lateinit var patchBundles: Set - - @CommandLine.Option( - names = ["-p", "--path"], - description = ["Path to patch options JSON file."], - showDefaultValue = ALWAYS, - ) - private var filePath: File = File("options.json") - - @CommandLine.Option( - names = ["-o", "--overwrite"], - description = ["Overwrite existing options file."], - showDefaultValue = ALWAYS, - ) - private var overwrite: Boolean = false - - @CommandLine.Option( - names = ["-u", "--update"], - description = ["Update existing options by adding missing and removing non-existent options."], - showDefaultValue = ALWAYS, - ) - private var update: Boolean = false - - override fun run() = - try { - loadPatchesFromJar(patchBundles).let { patches -> - val exists = filePath.exists() - if (!exists || overwrite) { - if (exists && update) patches.setOptions(filePath) - - Options.serialize(patches, prettyPrint = true).let(filePath::writeText) - } else { - throw OptionsFileAlreadyExistsException() - } - } - } catch (ex: OptionsFileAlreadyExistsException) { - logger.severe("Options file already exists, use --overwrite to override it") - } - - class OptionsFileAlreadyExistsException : Exception() -} diff --git a/src/main/kotlin/app/revanced/cli/command/PatchCommand.kt b/src/main/kotlin/app/revanced/cli/command/PatchCommand.kt index 69f24f16..96d8d7c4 100644 --- a/src/main/kotlin/app/revanced/cli/command/PatchCommand.kt +++ b/src/main/kotlin/app/revanced/cli/command/PatchCommand.kt @@ -2,15 +2,15 @@ package app.revanced.cli.command import app.revanced.library.ApkUtils import app.revanced.library.ApkUtils.applyTo -import app.revanced.library.Options -import app.revanced.library.Options.setOptions import app.revanced.library.installation.installer.* +import app.revanced.library.setOptions import app.revanced.patcher.Patcher import app.revanced.patcher.PatcherConfig import app.revanced.patcher.patch.Patch import app.revanced.patcher.patch.loadPatchesFromJar import kotlinx.coroutines.runBlocking import picocli.CommandLine +import picocli.CommandLine.ArgGroup import picocli.CommandLine.Help.Visibility.ALWAYS import picocli.CommandLine.Model.CommandSpec import picocli.CommandLine.Spec @@ -24,44 +24,71 @@ import java.util.logging.Logger description = ["Patch an APK file."], ) internal object PatchCommand : Runnable { - private val logger = Logger.getLogger(PatchCommand::class.java.name) + private val logger = Logger.getLogger(this::class.java.name) @Spec - lateinit var spec: CommandSpec // injected by picocli + private lateinit var spec: CommandSpec - private lateinit var apk: File + @ArgGroup(multiplicity = "0..*") + private lateinit var selection: Set - private var patchBundles = emptySet() + internal class Selection { + @ArgGroup(exclusive = false, multiplicity = "1") + internal var include: IncludeSelection? = null - @CommandLine.Option( - names = ["-i", "--include"], - description = ["List of patches to include."], - ) - private var includedPatches = hashSetOf() + internal class IncludeSelection { + @ArgGroup(multiplicity = "1") + internal lateinit var selector: IncludeSelector - @CommandLine.Option( - names = ["--ii"], - description = ["List of patches to include by their index in relation to the supplied patch bundles."], - ) - private var includedPatchesByIndex = arrayOf() + internal class IncludeSelector { + @CommandLine.Option( + names = ["-i", "--include"], + description = ["The name of the patch."], + required = true, + ) + internal var name: String? = null - @CommandLine.Option( - names = ["-e", "--exclude"], - description = ["List of patches to exclude."], - ) - private var excludedPatches = hashSetOf() + @CommandLine.Option( + names = ["--ii"], + description = ["The index of the patch in the combined list of all supplied patch bundles."], + required = true, + ) + internal var index: Int? = null + } - @CommandLine.Option( - names = ["--ei"], - description = ["List of patches to exclude by their index in relation to the supplied patch bundles."], - ) - private var excludedPatchesByIndex = arrayOf() + @CommandLine.Option( + names = ["-O", "--options"], + description = ["The option values keyed by the option keys."], + mapFallbackValue = CommandLine.Option.NULL_VALUE, + converter = [OptionKeyConverter::class, OptionValueConverter::class], + ) + internal var options = mutableMapOf() + } - @CommandLine.Option( - names = ["--options"], - description = ["Path to patch options JSON file."], - ) - private var optionsFile: File? = null + @ArgGroup(exclusive = false, multiplicity = "1") + internal var exclude: ExcludeSelection? = null + + internal class ExcludeSelection { + @ArgGroup(multiplicity = "1") + internal lateinit var selector: ExcludeSelector + + internal class ExcludeSelector { + @CommandLine.Option( + names = ["-e", "--exclude"], + description = ["The name of the patch."], + required = true, + ) + internal var name: String? = null + + @CommandLine.Option( + names = ["--ie"], + description = ["The index of the patch in the combined list of all supplied patch bundles."], + required = true, + ) + internal var index: Int? = null + } + } + } @CommandLine.Option( names = ["--exclusive"], @@ -141,7 +168,7 @@ internal object PatchCommand : Runnable { @CommandLine.Option( names = ["-t", "--temporary-files-path"], - description = ["Path to temporary files directory."], + description = ["Path to store temporary files."], ) private var temporaryFilesPath: File? = null @@ -154,13 +181,6 @@ internal object PatchCommand : Runnable { ) private var purge: Boolean = false - @CommandLine.Option( - names = ["-w", "--warn"], - description = ["Warn if a patch can not be found in the supplied patch bundles."], - showDefaultValue = ALWAYS, - ) - private var warn: Boolean = false - @CommandLine.Parameters( description = ["APK file to be patched."], arity = "1..1", @@ -176,14 +196,7 @@ internal object PatchCommand : Runnable { this.apk = apk } - @CommandLine.Option( - names = ["-m", "--merge"], - description = ["One or more DEX files or containers to merge into the APK."], - ) - @Suppress("unused") - private fun setIntegrations(integrations: Set) { - logger.warning("The --merge option is not used anymore.") - } + private lateinit var apk: File @CommandLine.Option( names = ["-b", "--patch-bundle"], @@ -198,6 +211,8 @@ internal object PatchCommand : Runnable { this.patchBundles = patchBundles } + private var patchBundles = emptySet() + @CommandLine.Option( names = ["--custom-aapt2-binary"], description = ["Path to a custom AAPT binary to compile resources with."], @@ -226,11 +241,6 @@ internal object PatchCommand : Runnable { "${outputFilePath.nameWithoutExtension}-temporary-files", ) - val optionsFile = - optionsFile ?: outputFilePath.parentFile.resolve( - "${outputFilePath.nameWithoutExtension}-options.json", - ) - val keystoreFilePath = keystoreFilePath ?: outputFilePath.parentFile .resolve("${outputFilePath.nameWithoutExtension}.keystore") @@ -243,21 +253,10 @@ internal object PatchCommand : Runnable { val patches = loadPatchesFromJar(patchBundles) - // Warn if a patch can not be found in the supplied patch bundles. - if (warn) { - patches.map { it.name }.toHashSet().let { availableNames -> - (includedPatches + excludedPatches).filter { name -> - !availableNames.contains(name) - } - }.let { unknownPatches -> - if (unknownPatches.isEmpty()) return@let - logger.warning("Unknown input of patches:\n${unknownPatches.joinToString("\n")}") - } - } - // endregion val patcherTemporaryFilesPath = temporaryFilesPath.resolve("patcher") + val (packageName, patcherResult) = Patcher( PatcherConfig( apk, @@ -267,16 +266,20 @@ internal object PatchCommand : Runnable { true, ), ).use { patcher -> - val filteredPatches = - patcher.filterPatchSelection(patches).also { patches -> - logger.info("Setting patch options") - - if (optionsFile.exists()) { - patches.setOptions(optionsFile) - } else { - Options.serialize(patches, prettyPrint = true).let(optionsFile::writeText) - } - } + val packageName = patcher.context.packageMetadata.packageName + val packageVersion = patcher.context.packageMetadata.packageVersion + + val filteredPatches = patches.filterPatchSelection(packageName, packageVersion) + + logger.info("Setting patch options") + + val patchesList = patches.toList() + selection.filter { it.include != null }.associate { + val includeSelection = it.include!! + + (includeSelection.selector.name ?: patchesList[includeSelection.selector.index!!].name!!) to + includeSelection.options + }.let(filteredPatches::setOptions) patcher += filteredPatches @@ -297,7 +300,7 @@ internal object PatchCommand : Runnable { patcher.context.packageMetadata.packageName to patcher.get() } - // region Save + // region Save. apk.copyTo(temporaryFilesPath.resolve(apk.name), overwrite = true).apply { patcherResult.applyTo(this) @@ -323,9 +326,9 @@ internal object PatchCommand : Runnable { // endregion - // region Install + // region Install. - deviceSerial?.let { it -> + deviceSerial?.let { val deviceSerial = it.ifEmpty { null } runBlocking { @@ -352,64 +355,72 @@ internal object PatchCommand : Runnable { } /** - * Filter the patches to be added to the patcher. The filter is based on the following: + * Filter the patches based on the selection. * - * @param patches The patches to filter. + * @param packageName The package name of the APK file to be patched. + * @param packageVersion The version of the APK file to be patched. * @return The filtered patches. */ - private fun Patcher.filterPatchSelection(patches: Set>): Set> = - buildSet { - val packageName = context.packageMetadata.packageName - val packageVersion = context.packageMetadata.packageVersion - - patches.withIndex().forEach patch@{ (i, patch) -> - val patchName = patch.name!! - - val explicitlyExcluded = excludedPatches.contains(patchName) || excludedPatchesByIndex.contains(i) - if (explicitlyExcluded) return@patch logger.info("\"$patchName\" excluded manually") - - // Make sure the patch is compatible with the supplied APK files package name and version. - patch.compatiblePackages?.let { packages -> - packages.singleOrNull { (name, _) -> name == packageName }?.let { (_, versions) -> - if (versions?.isEmpty() == true) { - return@patch logger.warning("\"$patchName\" incompatible with \"$packageName\"") - } - - val matchesVersion = force || - versions?.let { it.any { version -> version == packageVersion } } - ?: true - - if (!matchesVersion) { - return@patch logger.warning( - "\"$patchName\" incompatible with $packageName $packageVersion " + - "but compatible with " + - packages.joinToString("; ") { (packageName, versions) -> - packageName + " " + versions!!.joinToString(", ") - }, - ) - } - } ?: return@patch logger.fine( - "\"$patchName\" incompatible with $packageName. " + - "It is only compatible with " + - packages.joinToString(", ") { (name, _) -> name }, - ) - - return@let - } ?: logger.fine("\"$patchName\" has no package constraints") - - // If the patch is implicitly used, it will be only included if [exclusive] is false. - val implicitlyIncluded = !exclusive && patch.use - // If the patch is explicitly used, it will be included even if [exclusive] is false. - val explicitlyIncluded = includedPatches.contains(patchName) || includedPatchesByIndex.contains(i) - - val included = implicitlyIncluded || explicitlyIncluded - if (!included) return@patch logger.info("\"$patchName\" excluded") // Case 1. - - add(patch) - - logger.fine("\"$patchName\" added") + private fun Set>.filterPatchSelection( + packageName: String, + packageVersion: String, + ): Set> = buildSet { + val includedPatchesByName = + selection.asSequence().mapNotNull { it.include?.selector?.name }.toSet() + val includedPatchesByIndex = + selection.asSequence().mapNotNull { it.include?.selector?.index }.toSet() + + val excludedPatches = + selection.asSequence().mapNotNull { it.exclude?.selector?.name }.toSet() + val excludedPatchesByIndex = + selection.asSequence().mapNotNull { it.exclude?.selector?.index }.toSet() + + this@filterPatchSelection.withIndex().forEach patchLoop@{ (i, patch) -> + val patchName = patch.name!! + + val isManuallyExcluded = patchName in excludedPatches || i in excludedPatchesByIndex + if (isManuallyExcluded) return@patchLoop logger.info("\"$patchName\" excluded manually") + + // Make sure the patch is compatible with the supplied APK files package name and version. + patch.compatiblePackages?.let { packages -> + packages.singleOrNull { (name, _) -> name == packageName }?.let { (_, versions) -> + if (versions?.isEmpty() == true) { + return@patchLoop logger.warning("\"$patchName\" incompatible with \"$packageName\"") + } + + val matchesVersion = + force || versions?.let { it.any { version -> version == packageVersion } } ?: true + + if (!matchesVersion) { + return@patchLoop logger.warning( + "\"$patchName\" incompatible with $packageName $packageVersion " + + "but compatible with " + + packages.joinToString("; ") { (packageName, versions) -> + packageName + " " + versions!!.joinToString(", ") + }, + ) + } + } ?: return@patchLoop logger.fine( + "\"$patchName\" incompatible with $packageName. " + + "It is only compatible with " + + packages.joinToString(", ") { (name, _) -> name }, + ) + + return@let + } ?: logger.fine("\"$patchName\" has no package constraints") + + val isIncluded = !exclusive && patch.use + val isManuallyIncluded = patchName in includedPatchesByName || i in includedPatchesByIndex + + if (!(isIncluded || isManuallyIncluded)) { + return@patchLoop logger.info("\"$patchName\" excluded") } + + add(patch) + + logger.fine("\"$patchName\" added") } + } private fun purge(resourceCachePath: File) { val result = diff --git a/src/main/kotlin/app/revanced/cli/command/utility/InstallCommand.kt b/src/main/kotlin/app/revanced/cli/command/utility/InstallCommand.kt index 573205e0..6fbb5467 100644 --- a/src/main/kotlin/app/revanced/cli/command/utility/InstallCommand.kt +++ b/src/main/kotlin/app/revanced/cli/command/utility/InstallCommand.kt @@ -13,7 +13,7 @@ import java.util.logging.Logger description = ["Install an APK file to devices with the supplied ADB device serials"], ) internal object InstallCommand : Runnable { - private val logger = Logger.getLogger(InstallCommand::class.java.name) + private val logger = Logger.getLogger(this::class.java.name) @Parameters( description = ["ADB device serials. If not supplied, the first connected device will be used."], diff --git a/src/main/kotlin/app/revanced/cli/command/utility/UninstallCommand.kt b/src/main/kotlin/app/revanced/cli/command/utility/UninstallCommand.kt index e446e001..6f825c65 100644 --- a/src/main/kotlin/app/revanced/cli/command/utility/UninstallCommand.kt +++ b/src/main/kotlin/app/revanced/cli/command/utility/UninstallCommand.kt @@ -1,6 +1,9 @@ package app.revanced.cli.command.utility -import app.revanced.library.installation.installer.* +import app.revanced.library.installation.installer.AdbInstaller +import app.revanced.library.installation.installer.AdbInstallerResult +import app.revanced.library.installation.installer.AdbRootInstaller +import app.revanced.library.installation.installer.RootInstallerResult import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.runBlocking @@ -13,7 +16,7 @@ import java.util.logging.Logger description = ["Uninstall a patched app from the devices with the supplied ADB device serials"], ) internal object UninstallCommand : Runnable { - private val logger = Logger.getLogger(UninstallCommand::class.java.name) + private val logger = Logger.getLogger(this::class.java.name) @Parameters( description = ["ADB device serials. If not supplied, the first connected device will be used."], diff --git a/src/test/kotlin/app/revanced/cli/command/OptionValueConverterTest.kt b/src/test/kotlin/app/revanced/cli/command/OptionValueConverterTest.kt new file mode 100644 index 00000000..c03358c4 --- /dev/null +++ b/src/test/kotlin/app/revanced/cli/command/OptionValueConverterTest.kt @@ -0,0 +1,78 @@ +package app.revanced.cli.command + +import kotlin.test.Test + +class OptionValueConverterTest { + @Test + fun `converts to string`() { + "string" convertsTo "string" because "Strings should remain the same" + } + + @Test + fun `converts to null`() { + "null" convertsTo null because "null should convert to null" + "\"null\"" convertsTo "null" because "Escaped null should convert to a string" + } + + @Test + fun `converts to boolean`() { + "true" convertsTo true because "true should convert to a boolean true" + "True" convertsTo true because "Casing should not matter" + "\"true\"" convertsTo "true" because "Escaped booleans should be converted to strings" + "\'True\'" convertsTo "True" because "Casing in escaped booleans should not matter" + "tr ue" convertsTo "tr ue" because "Malformed booleans should be converted to strings" + } + + @Test + fun `converts to numbers`() { + "1" convertsTo 1 because "Integers should convert to integers" + "1.0" convertsTo 1.0 because "Doubles should convert to doubles" + "1.0f" convertsTo 1.0f because "The suffix f should convert to a float" + Long.MAX_VALUE.toString() convertsTo Long.MAX_VALUE because "Values that are too large for an integer should convert to longs" + "1L" convertsTo 1L because "The suffix L should convert to a long" + } + + @Test + fun `converts escaped numbers to string`() { + "\"1\"" convertsTo "1" because "Escaped numbers should convert to strings" + "\"1.0\"" convertsTo "1.0" because "Escaped doubles should convert to strings" + "\"1L\"" convertsTo "1L" because "Escaped longs should convert to strings" + "\'1\'" convertsTo "1" because "Single quotes should not be treated as escape symbols" + "\'.0\'" convertsTo ".0" because "Single quotes should not be treated as escape symbols" + "\'1L\'" convertsTo "1L" because "Single quotes should not be treated as escape symbols" + } + + @Test + fun `trims escape symbols once`() { + "\"\"\"1\"\"\"" convertsTo "\"\"1\"\"" because "The escape symbols should be trimmed once" + "\'\'\'1\'\'\'" convertsTo "''1''" because "Single quotes should not be treated as escape symbols" + } + + @Test + fun `converts lists`() { + "1,2" convertsTo "1,2" because "Lists without square brackets should not be converted to lists" + "[1,2" convertsTo "[1,2" because "Invalid lists should not be converted to lists" + "\"[1,2]\"" convertsTo "[1,2]" because "Lists with escaped square brackets should not be converted to lists" + + "[]" convertsTo emptyList() because "Empty untyped lists should convert to empty lists of any" + "int[]" convertsTo emptyList() because "Empty typed lists should convert to lists of the specified type" + "[[int[]]]" convertsTo listOf(listOf(emptyList())) because "Nested typed lists should convert to nested lists of the specified type" + "[\"int[]\"]" convertsTo listOf("int[]") because "Lists of escaped empty typed lists should not be converted to lists" + + "[1,2,3]" convertsTo listOf(1, 2, 3) because "Lists of integers should convert to lists of integers" + "[[1]]" convertsTo listOf(listOf(1)) because "Nested lists with one element should convert to nested lists" + "[[1,2],[3,4]]" convertsTo listOf(listOf(1, 2), listOf(3, 4)) because "Nested lists should convert to nested lists" + + "[\"1,2\"]" convertsTo listOf("1,2") because "Values in lists should not be split by commas in strings" + "[[\"1,2\"]]" convertsTo listOf(listOf("1,2")) because "Values in nested lists should not be split by commas in strings" + + "[\"\\\"\"]" convertsTo listOf("\"") because "Escaped quotes in strings should be converted to quotes" + "[[\"\\\"\"]]" convertsTo listOf(listOf("\"")) because "Escaped quotes in strings nested in lists should be converted to quotes" + "[.1,.2f,,true,FALSE]" convertsTo listOf(.1, .2f, "", true, false) because "Values in lists should be converted to the correct type" + } + + private val convert = OptionValueConverter()::convert + + private infix fun String.convertsTo(to: Any?) = convert(this) to to + private infix fun Pair.because(reason: String) = assert(this.first == this.second) { reason } +}