From d310246852504b08a15f6376bbf25ac7c6fae76f Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Thu, 1 Aug 2024 17:39:35 +0200 Subject: [PATCH] feat: Add ability to create options outside of a patch --- api/revanced-patcher.api | 18 + docs/2_2_patch_anatomy.md | 12 + .../app/revanced/patcher/patch/Option.kt | 348 +++++++++++++++++- .../app/revanced/patcher/patch/Patch.kt | 3 + .../patcher/patch/options/OptionsTest.kt | 16 + 5 files changed, 386 insertions(+), 11 deletions(-) diff --git a/api/revanced-patcher.api b/api/revanced-patcher.api index 694b29f..3091f38 100644 --- a/api/revanced-patcher.api +++ b/api/revanced-patcher.api @@ -205,25 +205,43 @@ public final class app/revanced/patcher/patch/OptionException$ValueValidationExc public final class app/revanced/patcher/patch/OptionKt { public static final fun booleanOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/Boolean;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option; + public static final fun booleanOption (Ljava/lang/String;Ljava/lang/Boolean;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option; public static synthetic fun booleanOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/Boolean;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option; + public static synthetic fun booleanOption$default (Ljava/lang/String;Ljava/lang/Boolean;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option; public static final fun booleansOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option; + public static final fun booleansOption (Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option; public static synthetic fun booleansOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option; + public static synthetic fun booleansOption$default (Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option; public static final fun floatOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/Float;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option; + public static final fun floatOption (Ljava/lang/String;Ljava/lang/Float;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option; public static synthetic fun floatOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/Float;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option; + public static synthetic fun floatOption$default (Ljava/lang/String;Ljava/lang/Float;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option; public static final fun floatsOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option; public static synthetic fun floatsOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option; public static final fun intOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/Integer;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option; + public static final fun intOption (Ljava/lang/String;Ljava/lang/Integer;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option; public static synthetic fun intOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/Integer;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option; + public static synthetic fun intOption$default (Ljava/lang/String;Ljava/lang/Integer;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option; public static final fun intsOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option; + public static final fun intsOption (Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option; public static synthetic fun intsOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option; + public static synthetic fun intsOption$default (Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option; public static final fun longOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/Long;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option; + public static final fun longOption (Ljava/lang/String;Ljava/lang/Long;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option; public static synthetic fun longOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/Long;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option; + public static synthetic fun longOption$default (Ljava/lang/String;Ljava/lang/Long;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option; public static final fun longsOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option; + public static final fun longsOption (Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option; public static synthetic fun longsOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option; + public static synthetic fun longsOption$default (Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option; public static final fun stringOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option; + public static final fun stringOption (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option; public static synthetic fun stringOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option; + public static synthetic fun stringOption$default (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option; public static final fun stringsOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option; + public static final fun stringsOption (Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option; public static synthetic fun stringsOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option; + public static synthetic fun stringsOption$default (Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option; } public final class app/revanced/patcher/patch/Options : java/util/Map, kotlin/jvm/internal/markers/KMappedMarker { diff --git a/docs/2_2_patch_anatomy.md b/docs/2_2_patch_anatomy.md index 89e0971..e6137e9 100644 --- a/docs/2_2_patch_anatomy.md +++ b/docs/2_2_patch_anatomy.md @@ -149,6 +149,18 @@ The type of an option can be obtained from the `type` property of the option: option.type // The KType of the option. ``` +Options can be declared outside of a patch and added to a patch manually: + +```kt +val option = stringOption(key = "option") + +bytecodePatch(name = "Patch") { + val value by option() +} +``` + +This is useful when the same option is referenced in multiple patches. + ### 🧩 Extensions An extension is a precompiled DEX file merged into the patched app before a patch is executed. diff --git a/src/main/kotlin/app/revanced/patcher/patch/Option.kt b/src/main/kotlin/app/revanced/patcher/patch/Option.kt index aa02b30..ec24941 100644 --- a/src/main/kotlin/app/revanced/patcher/patch/Option.kt +++ b/src/main/kotlin/app/revanced/patcher/patch/Option.kt @@ -100,7 +100,7 @@ class Option @PublishedApi internal constructor( } /** - * A collection of [Option]s where options can be set and retrieved by key. + * A collection of [Option]s where options can be set and retrieved by their key. * * @param options The options. * @@ -143,6 +143,39 @@ class Options internal constructor( override fun get(key: String) = options[key] ?: throw OptionException.OptionNotFoundException(key) } +/** + * Create a new [Option] with a string value. + * + * @param key The key. + * @param default The default value. + * @param values Eligible option values mapped to a human-readable name. + * @param title The title. + * @param description A description. + * @param required Whether the option is required. + * @param validator The function to validate the option value. + * + * @return The created [Option]. + * + * @see Option + */ +fun stringOption( + key: String, + default: String? = null, + values: Map? = null, + title: String? = null, + description: String? = null, + required: Boolean = false, + validator: Option.(String?) -> Boolean = { true }, +) = option( + key, + default, + values, + title, + description, + required, + validator, +) + /** * Create a new [Option] with a string value and add it to the current [PatchBuilder]. * @@ -176,6 +209,39 @@ fun PatchBuilder<*>.stringOption( validator, ) +/** + * Create a new [Option] with an integer value. + * + * @param key The key. + * @param default The default value. + * @param values Eligible option values mapped to a human-readable name. + * @param title The title. + * @param description A description. + * @param required Whether the option is required. + * @param validator The function to validate the option value. + * + * @return The created [Option]. + * + * @see Option + */ +fun intOption( + key: String, + default: Int? = null, + values: Map? = null, + title: String? = null, + description: String? = null, + required: Boolean = false, + validator: Option.(Int?) -> Boolean = { true }, +) = option( + key, + default, + values, + title, + description, + required, + validator, +) + /** * Create a new [Option] with an integer value and add it to the current [PatchBuilder]. * @@ -209,6 +275,39 @@ fun PatchBuilder<*>.intOption( validator, ) +/** + * Create a new [Option] with a boolean value. + * + * @param key The key. + * @param default The default value. + * @param values Eligible option values mapped to a human-readable name. + * @param title The title. + * @param description A description. + * @param required Whether the option is required. + * @param validator The function to validate the option value. + * + * @return The created [Option]. + * + * @see Option + */ +fun booleanOption( + key: String, + default: Boolean? = null, + values: Map? = null, + title: String? = null, + description: String? = null, + required: Boolean = false, + validator: Option.(Boolean?) -> Boolean = { true }, +) = option( + key, + default, + values, + title, + description, + required, + validator, +) + /** * Create a new [Option] with a boolean value and add it to the current [PatchBuilder]. * @@ -242,6 +341,39 @@ fun PatchBuilder<*>.booleanOption( validator, ) +/** + * Create a new [Option] with a float value. + * + * @param key The key. + * @param default The default value. + * @param values Eligible option values mapped to a human-readable name. + * @param title The title. + * @param description A description. + * @param required Whether the option is required. + * @param validator The function to validate the option value. + * + * @return The created [Option]. + * + * @see Option + */ +fun floatOption( + key: String, + default: Float? = null, + values: Map? = null, + title: String? = null, + description: String? = null, + required: Boolean = false, + validator: Option.(Float?) -> Boolean = { true }, +) = option( + key, + default, + values, + title, + description, + required, + validator, +) + /** * Create a new [Option] with a float value and add it to the current [PatchBuilder]. * @@ -275,6 +407,39 @@ fun PatchBuilder<*>.floatOption( validator, ) +/** + * Create a new [Option] with a long value. + * + * @param key The key. + * @param default The default value. + * @param values Eligible option values mapped to a human-readable name. + * @param title The title. + * @param description A description. + * @param required Whether the option is required. + * @param validator The function to validate the option value. + * + * @return The created [Option]. + * + * @see Option + */ +fun longOption( + key: String, + default: Long? = null, + values: Map? = null, + title: String? = null, + description: String? = null, + required: Boolean = false, + validator: Option.(Long?) -> Boolean = { true }, +) = option( + key, + default, + values, + title, + description, + required, + validator, +) + /** * Create a new [Option] with a long value and add it to the current [PatchBuilder]. * @@ -308,6 +473,39 @@ fun PatchBuilder<*>.longOption( validator, ) +/** + * Create a new [Option] with a string list value. + * + * @param key The key. + * @param default The default value. + * @param values Eligible option values mapped to a human-readable name. + * @param title The title. + * @param description A description. + * @param required Whether the option is required. + * @param validator The function to validate the option value. + * + * @return The created [Option]. + * + * @see Option + */ +fun stringsOption( + key: String, + default: List? = null, + values: Map?>? = null, + title: String? = null, + description: String? = null, + required: Boolean = false, + validator: Option>.(List?) -> Boolean = { true }, +) = option( + key, + default, + values, + title, + description, + required, + validator, +) + /** * Create a new [Option] with a string list value and add it to the current [PatchBuilder]. * @@ -341,6 +539,39 @@ fun PatchBuilder<*>.stringsOption( validator, ) +/** + * Create a new [Option] with an integer list value. + * + * @param key The key. + * @param default The default value. + * @param values Eligible option values mapped to a human-readable name. + * @param title The title. + * @param description A description. + * @param required Whether the option is required. + * @param validator The function to validate the option value. + * + * @return The created [Option]. + * + * @see Option + */ +fun intsOption( + key: String, + default: List? = null, + values: Map?>? = null, + title: String? = null, + description: String? = null, + required: Boolean = false, + validator: Option>.(List?) -> Boolean = { true }, +) = option( + key, + default, + values, + title, + description, + required, + validator, +) + /** * Create a new [Option] with an integer list value and add it to the current [PatchBuilder]. * @@ -374,6 +605,39 @@ fun PatchBuilder<*>.intsOption( validator, ) +/** + * Create a new [Option] with a boolean list value. + * + * @param key The key. + * @param default The default value. + * @param values Eligible option values mapped to a human-readable name. + * @param title The title. + * @param description A description. + * @param required Whether the option is required. + * @param validator The function to validate the option value. + * + * @return The created [Option]. + * + * @see Option + */ +fun booleansOption( + key: String, + default: List? = null, + values: Map?>? = null, + title: String? = null, + description: String? = null, + required: Boolean = false, + validator: Option>.(List?) -> Boolean = { true }, +) = option( + key, + default, + values, + title, + description, + required, + validator, +) + /** * Create a new [Option] with a boolean list value and add it to the current [PatchBuilder]. * @@ -440,6 +704,39 @@ fun PatchBuilder<*>.floatsOption( validator, ) +/** + * Create a new [Option] with a long list value. + * + * @param key The key. + * @param default The default value. + * @param values Eligible option values mapped to a human-readable name. + * @param title The title. + * @param description A description. + * @param required Whether the option is required. + * @param validator The function to validate the option value. + * + * @return The created [Option]. + * + * @see Option + */ +fun longsOption( + key: String, + default: List? = null, + values: Map?>? = null, + title: String? = null, + description: String? = null, + required: Boolean = false, + validator: Option>.(List?) -> Boolean = { true }, +) = option( + key, + default, + values, + title, + description, + required, + validator, +) + /** * Create a new [Option] with a long list value and add it to the current [PatchBuilder]. * @@ -473,6 +770,40 @@ fun PatchBuilder<*>.longsOption( validator, ) +/** + * Create a new [Option]. + * + * @param key The key. + * @param default The default value. + * @param values Eligible option values mapped to a human-readable name. + * @param title The title. + * @param description A description. + * @param required Whether the option is required. + * @param validator The function to validate the option value. + * + * @return The created [Option]. + * + * @see Option + */ +inline fun option( + key: String, + default: T? = null, + values: Map? = null, + title: String? = null, + description: String? = null, + required: Boolean = false, + noinline validator: Option.(T?) -> Boolean = { true }, +) = Option( + key, + default, + values, + title, + description, + required, + typeOf(), + validator, +) + /** * Create a new [Option] and add it to the current [PatchBuilder]. * @@ -496,14 +827,13 @@ inline fun PatchBuilder<*>.option( description: String? = null, required: Boolean = false, noinline validator: Option.(T?) -> Boolean = { true }, -) = Option( +) = app.revanced.patcher.patch.option( key, default, values, title, description, required, - typeOf(), validator, ).also { it() } @@ -519,30 +849,26 @@ sealed class OptionException(errorMessage: String) : Exception(errorMessage, nul * @param invalidType The type of the value that was passed. * @param expectedType The type of the value that was expected. */ - class InvalidValueTypeException(invalidType: String, expectedType: String) : - OptionException("Type $expectedType was expected but received type $invalidType") + class InvalidValueTypeException(invalidType: String, expectedType: String) : OptionException("Type $expectedType was expected but received type $invalidType") /** * An exception thrown when a value did not satisfy the value conditions specified by the [Option]. * * @param value The value that failed validation. */ - class ValueValidationException(value: Any?, option: Option<*>) : - OptionException("The option value \"$value\" failed validation for ${option.key}") + class ValueValidationException(value: Any?, option: Option<*>) : OptionException("The option value \"$value\" failed validation for ${option.key}") /** * An exception thrown when a value is required but null was passed. * * @param option The [Option] that requires a value. */ - class ValueRequiredException(option: Option<*>) : - OptionException("The option ${option.key} requires a value, but null was passed") + class ValueRequiredException(option: Option<*>) : OptionException("The option ${option.key} requires a value, but null was passed") /** * An exception thrown when a [Option] is not found. * * @param key The key of the [Option]. */ - class OptionNotFoundException(key: String) : - OptionException("No option with key $key") + class OptionNotFoundException(key: String) : OptionException("No option with key $key") } diff --git a/src/main/kotlin/app/revanced/patcher/patch/Patch.kt b/src/main/kotlin/app/revanced/patcher/patch/Patch.kt index 53a7736..6c089ab 100644 --- a/src/main/kotlin/app/revanced/patcher/patch/Patch.kt +++ b/src/main/kotlin/app/revanced/patcher/patch/Patch.kt @@ -48,6 +48,9 @@ sealed class Patch>( // if a patch has a finalizing block in order to not emit it twice. internal var finalizeBlock: (Patch.(C) -> Unit)?, ) { + /** + * The options of the patch. + */ val options = Options(options) /** diff --git a/src/test/kotlin/app/revanced/patcher/patch/options/OptionsTest.kt b/src/test/kotlin/app/revanced/patcher/patch/options/OptionsTest.kt index 788b68e..c5aba42 100644 --- a/src/test/kotlin/app/revanced/patcher/patch/options/OptionsTest.kt +++ b/src/test/kotlin/app/revanced/patcher/patch/options/OptionsTest.kt @@ -7,7 +7,11 @@ import kotlin.reflect.typeOf import kotlin.test.* internal object OptionsTest { + private val externalOption = stringOption("external", "default") + private val optionsTestPatch = bytecodePatch { + externalOption() + booleanOption("bool", true) stringOption("required", "default", required = true) @@ -124,5 +128,17 @@ internal object OptionsTest { } } + @Test + fun `external option should be accessible`() { + assertDoesNotThrow { + externalOption.value = "test" + } + } + + @Test + fun `should allow getting the external option from the patch`() { + assertEquals(optionsTestPatch.options["external"].value, externalOption.value) + } + private fun options(block: Options.() -> Unit) = optionsTestPatch.options.let(block) }