feat: Set patch options via CLI (#336)

BREAKING CHANGE: This commit changes various CLI options and removes the `options.json` file. Instead, patch options can now be passed via CLI options
This commit is contained in:
oSumAtrIX 2024-08-13 00:12:45 +04:00 committed by GitHub
parent 54ae01cd76
commit 23002434b2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 409 additions and 203 deletions

View File

@ -13,10 +13,12 @@ java -jar revanced-cli.jar -h
## 📃 List patches ## 📃 List patches
```bash ```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 ```bash
java -jar revanced-cli.jar patch -b revanced-patches.rvp input.apk 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 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`. To change the default set of used patches, use the option `-i` or `-e` to use or disuse specific patches.
Keep in mind the name of the patch must be an exact match. You can use the `list-patches` command to see which patches are used by default.
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 only use specific patches, you can use the option `--exclusive` combined with `-i`.
To know the indices of patches, use the option `--with-indices` when listing patches: Remember that the options `-i` and `-e` match the patch's name exactly. Here is an example:
```bash ```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 ```bash
java -jar revanced-cli.jar patch -b revanced-patches.rvp --ii 123 --ie 456 input.apk 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] > [!TIP]
> You can use the option `-d` to automatically install the patched app after patching. > You can use the option `-d` to automatically install the patched app after patching.
> Make sure ADB is working: > Make sure ADB is working:
@ -62,6 +79,60 @@ java -jar revanced-cli.jar patch -b revanced-patches.rvp --ii 123 --ie 456 input
> adb install input.apk > adb install input.apk
> ``` > ```
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 ## 📦 Install an app manually
```bash ```bash

View File

@ -0,0 +1,105 @@
package app.revanced.cli.command
import picocli.CommandLine
class OptionKeyConverter : CommandLine.ITypeConverter<String> {
override fun convert(value: String): String = value
}
class OptionValueConverter : CommandLine.ITypeConverter<Any?> {
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<Int>()
value == "long[]" -> emptyList<Long>()
value == "double[]" -> emptyList<Double>()
value == "float[]" -> emptyList<Float>()
value == "boolean[]" -> emptyList<Boolean>()
value == "string[]" -> emptyList<String>()
else -> value
}
}
}

View File

@ -1,8 +1,8 @@
package app.revanced.cli.command package app.revanced.cli.command
import app.revanced.library.PackageName import app.revanced.library.PackageName
import app.revanced.library.PatchUtils
import app.revanced.library.VersionMap import app.revanced.library.VersionMap
import app.revanced.library.mostCommonCompatibleVersions
import app.revanced.patcher.patch.loadPatchesFromJar import app.revanced.patcher.patch.loadPatchesFromJar
import picocli.CommandLine import picocli.CommandLine
import java.io.File import java.io.File
@ -16,7 +16,7 @@ import java.util.logging.Logger
], ],
) )
internal class ListCompatibleVersions : Runnable { internal class ListCompatibleVersions : Runnable {
private val logger = Logger.getLogger(ListCompatibleVersions::class.java.name) private val logger = Logger.getLogger(this::class.java.name)
@CommandLine.Parameters( @CommandLine.Parameters(
description = ["Paths to patch bundles."], description = ["Paths to patch bundles."],
@ -58,8 +58,7 @@ internal class ListCompatibleVersions : Runnable {
val patches = loadPatchesFromJar(patchBundles) val patches = loadPatchesFromJar(patchBundles)
PatchUtils.getMostCommonCompatibleVersions( patches.mostCommonCompatibleVersions(
patches,
packageNames, packageNames,
countUnusedPatches, countUnusedPatches,
).entries.joinToString("\n", transform = ::buildString).let(logger::info) ).entries.joinToString("\n", transform = ::buildString).let(logger::info)

View File

@ -14,7 +14,7 @@ import app.revanced.patcher.patch.Option as PatchOption
description = ["List patches from supplied patch bundles."], description = ["List patches from supplied patch bundles."],
) )
internal object ListPatchesCommand : Runnable { internal object ListPatchesCommand : Runnable {
private val logger = Logger.getLogger(ListPatchesCommand::class.java.name) private val logger = Logger.getLogger(this::class.java.name)
@Parameters( @Parameters(
description = ["Paths to patch bundles."], description = ["Paths to patch bundles."],
@ -95,9 +95,11 @@ internal object ListPatchesCommand : Runnable {
} ?: append("Key: $key") } ?: append("Key: $key")
values?.let { values -> values?.let { values ->
appendLine("\nValid values:") appendLine("\nPossible values:")
append(values.map { "${it.value} (${it.key})" }.joinToString("\n").prependIndent("\t")) append(values.map { "${it.value} (${it.key})" }.joinToString("\n").prependIndent("\t"))
} }
append("\nType: $type")
} }
fun IndexedValue<Patch<*>>.buildString() = fun IndexedValue<Patch<*>>.buildString() =

View File

@ -34,7 +34,6 @@ private object CLIVersionProvider : IVersionProvider {
versionProvider = CLIVersionProvider::class, versionProvider = CLIVersionProvider::class,
subcommands = [ subcommands = [
PatchCommand::class, PatchCommand::class,
OptionsCommand::class,
ListPatchesCommand::class, ListPatchesCommand::class,
ListCompatibleVersions::class, ListCompatibleVersions::class,
UtilityCommand::class, UtilityCommand::class,

View File

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

View File

@ -2,15 +2,15 @@ package app.revanced.cli.command
import app.revanced.library.ApkUtils import app.revanced.library.ApkUtils
import app.revanced.library.ApkUtils.applyTo 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.installation.installer.*
import app.revanced.library.setOptions
import app.revanced.patcher.Patcher import app.revanced.patcher.Patcher
import app.revanced.patcher.PatcherConfig import app.revanced.patcher.PatcherConfig
import app.revanced.patcher.patch.Patch import app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.loadPatchesFromJar import app.revanced.patcher.patch.loadPatchesFromJar
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import picocli.CommandLine import picocli.CommandLine
import picocli.CommandLine.ArgGroup
import picocli.CommandLine.Help.Visibility.ALWAYS import picocli.CommandLine.Help.Visibility.ALWAYS
import picocli.CommandLine.Model.CommandSpec import picocli.CommandLine.Model.CommandSpec
import picocli.CommandLine.Spec import picocli.CommandLine.Spec
@ -24,44 +24,71 @@ import java.util.logging.Logger
description = ["Patch an APK file."], description = ["Patch an APK file."],
) )
internal object PatchCommand : Runnable { internal object PatchCommand : Runnable {
private val logger = Logger.getLogger(PatchCommand::class.java.name) private val logger = Logger.getLogger(this::class.java.name)
@Spec @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<Selection>
private var patchBundles = emptySet<File>() internal class Selection {
@ArgGroup(exclusive = false, multiplicity = "1")
internal var include: IncludeSelection? = null
internal class IncludeSelection {
@ArgGroup(multiplicity = "1")
internal lateinit var selector: IncludeSelector
internal class IncludeSelector {
@CommandLine.Option( @CommandLine.Option(
names = ["-i", "--include"], names = ["-i", "--include"],
description = ["List of patches to include."], description = ["The name of the patch."],
required = true,
) )
private var includedPatches = hashSetOf<String>() internal var name: String? = null
@CommandLine.Option( @CommandLine.Option(
names = ["--ii"], names = ["--ii"],
description = ["List of patches to include by their index in relation to the supplied patch bundles."], description = ["The index of the patch in the combined list of all supplied patch bundles."],
required = true,
) )
private var includedPatchesByIndex = arrayOf<Int>() internal var index: Int? = null
}
@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<String, Any?>()
}
@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( @CommandLine.Option(
names = ["-e", "--exclude"], names = ["-e", "--exclude"],
description = ["List of patches to exclude."], description = ["The name of the patch."],
required = true,
) )
private var excludedPatches = hashSetOf<String>() internal var name: String? = null
@CommandLine.Option( @CommandLine.Option(
names = ["--ei"], names = ["--ie"],
description = ["List of patches to exclude by their index in relation to the supplied patch bundles."], description = ["The index of the patch in the combined list of all supplied patch bundles."],
required = true,
) )
private var excludedPatchesByIndex = arrayOf<Int>() internal var index: Int? = null
}
@CommandLine.Option( }
names = ["--options"], }
description = ["Path to patch options JSON file."],
)
private var optionsFile: File? = null
@CommandLine.Option( @CommandLine.Option(
names = ["--exclusive"], names = ["--exclusive"],
@ -141,7 +168,7 @@ internal object PatchCommand : Runnable {
@CommandLine.Option( @CommandLine.Option(
names = ["-t", "--temporary-files-path"], names = ["-t", "--temporary-files-path"],
description = ["Path to temporary files directory."], description = ["Path to store temporary files."],
) )
private var temporaryFilesPath: File? = null private var temporaryFilesPath: File? = null
@ -154,13 +181,6 @@ internal object PatchCommand : Runnable {
) )
private var purge: Boolean = false 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( @CommandLine.Parameters(
description = ["APK file to be patched."], description = ["APK file to be patched."],
arity = "1..1", arity = "1..1",
@ -176,14 +196,7 @@ internal object PatchCommand : Runnable {
this.apk = apk this.apk = apk
} }
@CommandLine.Option( private lateinit var apk: File
names = ["-m", "--merge"],
description = ["One or more DEX files or containers to merge into the APK."],
)
@Suppress("unused")
private fun setIntegrations(integrations: Set<File>) {
logger.warning("The --merge option is not used anymore.")
}
@CommandLine.Option( @CommandLine.Option(
names = ["-b", "--patch-bundle"], names = ["-b", "--patch-bundle"],
@ -198,6 +211,8 @@ internal object PatchCommand : Runnable {
this.patchBundles = patchBundles this.patchBundles = patchBundles
} }
private var patchBundles = emptySet<File>()
@CommandLine.Option( @CommandLine.Option(
names = ["--custom-aapt2-binary"], names = ["--custom-aapt2-binary"],
description = ["Path to a custom AAPT binary to compile resources with."], description = ["Path to a custom AAPT binary to compile resources with."],
@ -226,11 +241,6 @@ internal object PatchCommand : Runnable {
"${outputFilePath.nameWithoutExtension}-temporary-files", "${outputFilePath.nameWithoutExtension}-temporary-files",
) )
val optionsFile =
optionsFile ?: outputFilePath.parentFile.resolve(
"${outputFilePath.nameWithoutExtension}-options.json",
)
val keystoreFilePath = val keystoreFilePath =
keystoreFilePath ?: outputFilePath.parentFile keystoreFilePath ?: outputFilePath.parentFile
.resolve("${outputFilePath.nameWithoutExtension}.keystore") .resolve("${outputFilePath.nameWithoutExtension}.keystore")
@ -243,21 +253,10 @@ internal object PatchCommand : Runnable {
val patches = loadPatchesFromJar(patchBundles) 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 // endregion
val patcherTemporaryFilesPath = temporaryFilesPath.resolve("patcher") val patcherTemporaryFilesPath = temporaryFilesPath.resolve("patcher")
val (packageName, patcherResult) = Patcher( val (packageName, patcherResult) = Patcher(
PatcherConfig( PatcherConfig(
apk, apk,
@ -267,16 +266,20 @@ internal object PatchCommand : Runnable {
true, true,
), ),
).use { patcher -> ).use { patcher ->
val filteredPatches = val packageName = patcher.context.packageMetadata.packageName
patcher.filterPatchSelection(patches).also { patches -> val packageVersion = patcher.context.packageMetadata.packageVersion
val filteredPatches = patches.filterPatchSelection(packageName, packageVersion)
logger.info("Setting patch options") logger.info("Setting patch options")
if (optionsFile.exists()) { val patchesList = patches.toList()
patches.setOptions(optionsFile) selection.filter { it.include != null }.associate {
} else { val includeSelection = it.include!!
Options.serialize(patches, prettyPrint = true).let(optionsFile::writeText)
} (includeSelection.selector.name ?: patchesList[includeSelection.selector.index!!].name!!) to
} includeSelection.options
}.let(filteredPatches::setOptions)
patcher += filteredPatches patcher += filteredPatches
@ -297,7 +300,7 @@ internal object PatchCommand : Runnable {
patcher.context.packageMetadata.packageName to patcher.get() patcher.context.packageMetadata.packageName to patcher.get()
} }
// region Save // region Save.
apk.copyTo(temporaryFilesPath.resolve(apk.name), overwrite = true).apply { apk.copyTo(temporaryFilesPath.resolve(apk.name), overwrite = true).apply {
patcherResult.applyTo(this) patcherResult.applyTo(this)
@ -323,9 +326,9 @@ internal object PatchCommand : Runnable {
// endregion // endregion
// region Install // region Install.
deviceSerial?.let { it -> deviceSerial?.let {
val deviceSerial = it.ifEmpty { null } val deviceSerial = it.ifEmpty { null }
runBlocking { runBlocking {
@ -352,35 +355,44 @@ 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. * @return The filtered patches.
*/ */
private fun Patcher.filterPatchSelection(patches: Set<Patch<*>>): Set<Patch<*>> = private fun Set<Patch<*>>.filterPatchSelection(
buildSet { packageName: String,
val packageName = context.packageMetadata.packageName packageVersion: String,
val packageVersion = context.packageMetadata.packageVersion ): Set<Patch<*>> = buildSet {
val includedPatchesByName =
selection.asSequence().mapNotNull { it.include?.selector?.name }.toSet()
val includedPatchesByIndex =
selection.asSequence().mapNotNull { it.include?.selector?.index }.toSet()
patches.withIndex().forEach patch@{ (i, patch) -> 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 patchName = patch.name!!
val explicitlyExcluded = excludedPatches.contains(patchName) || excludedPatchesByIndex.contains(i) val isManuallyExcluded = patchName in excludedPatches || i in excludedPatchesByIndex
if (explicitlyExcluded) return@patch logger.info("\"$patchName\" excluded manually") if (isManuallyExcluded) return@patchLoop logger.info("\"$patchName\" excluded manually")
// Make sure the patch is compatible with the supplied APK files package name and version. // Make sure the patch is compatible with the supplied APK files package name and version.
patch.compatiblePackages?.let { packages -> patch.compatiblePackages?.let { packages ->
packages.singleOrNull { (name, _) -> name == packageName }?.let { (_, versions) -> packages.singleOrNull { (name, _) -> name == packageName }?.let { (_, versions) ->
if (versions?.isEmpty() == true) { if (versions?.isEmpty() == true) {
return@patch logger.warning("\"$patchName\" incompatible with \"$packageName\"") return@patchLoop logger.warning("\"$patchName\" incompatible with \"$packageName\"")
} }
val matchesVersion = force || val matchesVersion =
versions?.let { it.any { version -> version == packageVersion } } force || versions?.let { it.any { version -> version == packageVersion } } ?: true
?: true
if (!matchesVersion) { if (!matchesVersion) {
return@patch logger.warning( return@patchLoop logger.warning(
"\"$patchName\" incompatible with $packageName $packageVersion " + "\"$patchName\" incompatible with $packageName $packageVersion " +
"but compatible with " + "but compatible with " +
packages.joinToString("; ") { (packageName, versions) -> packages.joinToString("; ") { (packageName, versions) ->
@ -388,7 +400,7 @@ internal object PatchCommand : Runnable {
}, },
) )
} }
} ?: return@patch logger.fine( } ?: return@patchLoop logger.fine(
"\"$patchName\" incompatible with $packageName. " + "\"$patchName\" incompatible with $packageName. " +
"It is only compatible with " + "It is only compatible with " +
packages.joinToString(", ") { (name, _) -> name }, packages.joinToString(", ") { (name, _) -> name },
@ -397,13 +409,12 @@ internal object PatchCommand : Runnable {
return@let return@let
} ?: logger.fine("\"$patchName\" has no package constraints") } ?: logger.fine("\"$patchName\" has no package constraints")
// If the patch is implicitly used, it will be only included if [exclusive] is false. val isIncluded = !exclusive && patch.use
val implicitlyIncluded = !exclusive && patch.use val isManuallyIncluded = patchName in includedPatchesByName || i in includedPatchesByIndex
// 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 (!(isIncluded || isManuallyIncluded)) {
if (!included) return@patch logger.info("\"$patchName\" excluded") // Case 1. return@patchLoop logger.info("\"$patchName\" excluded")
}
add(patch) add(patch)

View File

@ -13,7 +13,7 @@ import java.util.logging.Logger
description = ["Install an APK file to devices with the supplied ADB device serials"], description = ["Install an APK file to devices with the supplied ADB device serials"],
) )
internal object InstallCommand : Runnable { internal object InstallCommand : Runnable {
private val logger = Logger.getLogger(InstallCommand::class.java.name) private val logger = Logger.getLogger(this::class.java.name)
@Parameters( @Parameters(
description = ["ADB device serials. If not supplied, the first connected device will be used."], description = ["ADB device serials. If not supplied, the first connected device will be used."],

View File

@ -1,6 +1,9 @@
package app.revanced.cli.command.utility 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.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking 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"], description = ["Uninstall a patched app from the devices with the supplied ADB device serials"],
) )
internal object UninstallCommand : Runnable { internal object UninstallCommand : Runnable {
private val logger = Logger.getLogger(UninstallCommand::class.java.name) private val logger = Logger.getLogger(this::class.java.name)
@Parameters( @Parameters(
description = ["ADB device serials. If not supplied, the first connected device will be used."], description = ["ADB device serials. If not supplied, the first connected device will be used."],

View File

@ -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<Any>() because "Empty untyped lists should convert to empty lists of any"
"int[]" convertsTo emptyList<Int>() because "Empty typed lists should convert to lists of the specified type"
"[[int[]]]" convertsTo listOf(listOf(emptyList<Int>())) 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<Any?, Any?>.because(reason: String) = assert(this.first == this.second) { reason }
}