diff --git a/api/revanced-library.api b/api/revanced-library.api index e020e1e..ba73750 100644 --- a/api/revanced-library.api +++ b/api/revanced-library.api @@ -67,6 +67,47 @@ public final class app/revanced/library/PatchUtils { public static synthetic fun getMostCommonCompatibleVersions$default (Lapp/revanced/library/PatchUtils;Ljava/util/Set;Ljava/util/Set;ZILjava/lang/Object;)Ljava/util/Map; } +public final class app/revanced/library/PatchUtils$Json { + public static final field INSTANCE Lapp/revanced/library/PatchUtils$Json; + public final fun deserialize (Ljava/io/InputStream;Ljava/lang/Class;)Ljava/util/Set; + public final fun serialize (Ljava/util/Set;Lkotlin/jvm/functions/Function1;ZLjava/io/OutputStream;)V + public static synthetic fun serialize$default (Lapp/revanced/library/PatchUtils$Json;Ljava/util/Set;Lkotlin/jvm/functions/Function1;ZLjava/io/OutputStream;ILjava/lang/Object;)V +} + +public final class app/revanced/library/PatchUtils$Json$FullJsonPatch : app/revanced/library/PatchUtils$Json$JsonPatch { + public static final field Companion Lapp/revanced/library/PatchUtils$Json$FullJsonPatch$Companion; + public final fun getCompatiblePackages ()Ljava/util/Set; + public final fun getDependencies ()Ljava/util/Set; + public final fun getDescription ()Ljava/lang/String; + public final fun getName ()Ljava/lang/String; + public final fun getOptions ()Ljava/util/Map; + public final fun getRequiresIntegrations ()Z + public final fun getUse ()Z + public final fun setRequiresIntegrations (Z)V +} + +public final class app/revanced/library/PatchUtils$Json$FullJsonPatch$Companion { + public final fun fromPatch (Lapp/revanced/patcher/patch/Patch;)Lapp/revanced/library/PatchUtils$Json$FullJsonPatch; +} + +public final class app/revanced/library/PatchUtils$Json$FullJsonPatch$FullJsonPatchOption { + public static final field Companion Lapp/revanced/library/PatchUtils$Json$FullJsonPatch$FullJsonPatchOption$Companion; + public final fun getDefault ()Ljava/lang/Object; + public final fun getDescription ()Ljava/lang/String; + public final fun getKey ()Ljava/lang/String; + public final fun getRequired ()Z + public final fun getTitle ()Ljava/lang/String; + public final fun getValueType ()Ljava/lang/String; + public final fun getValues ()Ljava/util/Map; +} + +public final class app/revanced/library/PatchUtils$Json$FullJsonPatch$FullJsonPatchOption$Companion { + public final fun fromPatchOption (Lapp/revanced/patcher/patch/options/PatchOption;)Lapp/revanced/library/PatchUtils$Json$FullJsonPatch$FullJsonPatchOption; +} + +public abstract interface class app/revanced/library/PatchUtils$Json$JsonPatch { +} + public abstract class app/revanced/library/adb/AdbManager { public static final field Companion Lapp/revanced/library/adb/AdbManager$Companion; public synthetic fun (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V diff --git a/src/main/kotlin/app/revanced/library/Options.kt b/src/main/kotlin/app/revanced/library/Options.kt index dd53918..2233d9e 100644 --- a/src/main/kotlin/app/revanced/library/Options.kt +++ b/src/main/kotlin/app/revanced/library/Options.kt @@ -9,6 +9,7 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import java.io.File import java.util.logging.Logger +@Suppress("unused") object Options { private val logger = Logger.getLogger(Options::class.java.name) diff --git a/src/main/kotlin/app/revanced/library/PatchUtils.kt b/src/main/kotlin/app/revanced/library/PatchUtils.kt index 1cc700b..1a6fa1d 100644 --- a/src/main/kotlin/app/revanced/library/PatchUtils.kt +++ b/src/main/kotlin/app/revanced/library/PatchUtils.kt @@ -1,7 +1,12 @@ package app.revanced.library +import app.revanced.patcher.PatchClass import app.revanced.patcher.PatchSet import app.revanced.patcher.patch.Patch +import app.revanced.patcher.patch.options.PatchOption +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import java.io.InputStream +import java.io.OutputStream typealias PackageName = String typealias Version = String @@ -90,4 +95,104 @@ object PatchUtils { .associate { it.key to it.value } as VersionMap } } + + object Json { + private val mapper = jacksonObjectMapper() + + /** + * Serializes a set of [Patch]es to a JSON string and writes it to an output stream. + * + * @param patches The set of [Patch]es to serialize. + * @param transform A function to transform the [Patch]es to [JsonPatch]es. + * @param prettyPrint Whether to pretty print the JSON. + * @param outputStream The output stream to write the JSON to. + */ + fun serialize( + patches: PatchSet, + transform: (Patch<*>) -> JsonPatch = { patch -> FullJsonPatch.fromPatch(patch) }, + prettyPrint: Boolean = false, + outputStream: OutputStream, + ) { + patches.map(transform).let { transformed -> + if (prettyPrint) { + mapper.writerWithDefaultPrettyPrinter().writeValue(outputStream, transformed) + } else { + mapper.writeValue(outputStream, transformed) + } + } + } + + /** + * Deserializes a JSON string to a set of [FullJsonPatch]es from an input stream. + * + * @param inputStream The input stream to read the JSON from. + * @param jsonPatchElementClass The class of the [JsonPatch]es to deserialize. + * @return A set of [JsonPatch]es. + * @see FullJsonPatch + */ + fun deserialize( + inputStream: InputStream, + jsonPatchElementClass: Class, + ): Set = + mapper.readValue( + inputStream, + mapper.typeFactory.constructCollectionType(Set::class.java, jsonPatchElementClass), + ) + + interface JsonPatch + + /** + * A JSON representation of a [Patch]. + * @see Patch + */ + class FullJsonPatch internal constructor( + val name: String?, + val description: String?, + val compatiblePackages: Set?, + val dependencies: Set?, + val use: Boolean, + var requiresIntegrations: Boolean, + val options: Map>, + ) : JsonPatch { + companion object { + fun fromPatch(patch: Patch<*>) = + FullJsonPatch( + patch.name, + patch.description, + patch.compatiblePackages, + patch.dependencies, + patch.use, + patch.requiresIntegrations, + patch.options.mapValues { FullJsonPatchOption.fromPatchOption(it.value) }, + ) + } + + /** + * A JSON representation of a [PatchOption]. + * @see PatchOption + */ + class FullJsonPatchOption internal constructor( + val key: String, + val default: T?, + val values: Map?, + val title: String?, + val description: String?, + val required: Boolean, + val valueType: String, + ) { + companion object { + fun fromPatchOption(option: PatchOption<*>) = + FullJsonPatchOption( + option.key, + option.default, + option.values, + option.title, + option.description, + option.required, + option.valueType, + ) + } + } + } + } } diff --git a/src/test/kotlin/app/revanced/library/PatchUtilsTest.kt b/src/test/kotlin/app/revanced/library/PatchUtilsTest.kt index 5cca387..7e63358 100644 --- a/src/test/kotlin/app/revanced/library/PatchUtilsTest.kt +++ b/src/test/kotlin/app/revanced/library/PatchUtilsTest.kt @@ -3,19 +3,25 @@ package app.revanced.library import app.revanced.patcher.PatchSet import app.revanced.patcher.data.BytecodeContext import app.revanced.patcher.patch.BytecodePatch +import app.revanced.patcher.patch.Patch +import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.booleanPatchOption +import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.intArrayPatchOption +import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.stringPatchOption import org.junit.jupiter.api.Test +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream import kotlin.test.assertEquals internal object PatchUtilsTest { private val patches = arrayOf( - newPatch("some.package", setOf("a")), + newPatch("some.package", setOf("a")) { stringPatchOption("string", "value") }, newPatch("some.package", setOf("a", "b"), use = false), newPatch("some.package", setOf("a", "b", "c"), use = false), newPatch("some.other.package", setOf("b"), use = false), - newPatch("some.other.package", setOf("b", "c")), + newPatch("some.other.package", setOf("b", "c")) { booleanPatchOption("bool", true) }, newPatch("some.other.package", setOf("b", "c", "d")), - newPatch("some.other.other.package"), + newPatch("some.other.other.package") { intArrayPatchOption("intArray", arrayOf(1, 2, 3)) }, newPatch("some.other.other.package", setOf("a")), newPatch("some.other.other.package", setOf("b")), newPatch("some.other.other.other.package", use = false), @@ -135,6 +141,20 @@ internal object PatchUtilsTest { assertEqualsVersion(null, patches, "other.package") } + @Test + fun `serializes to and deserializes from JSON string correctly`() { + val out = ByteArrayOutputStream() + PatchUtils.Json.serialize(patches, outputStream = out) + + val deserialized = + PatchUtils.Json.deserialize( + ByteArrayInputStream(out.toByteArray()), + PatchUtils.Json.FullJsonPatch::class.java, + ) + + assert(patches.size == deserialized.size) + } + private fun assertEqualsVersions( expected: PackageNameMap, patches: PatchSet, @@ -169,10 +189,16 @@ internal object PatchUtilsTest { packageName: String, versions: Set? = null, use: Boolean = true, + options: Patch<*>.() -> Unit = {}, ) = object : BytecodePatch( + name = "test", compatiblePackages = setOf(CompatiblePackage(packageName, versions?.toSet())), use = use, ) { + init { + options() + } + override fun execute(context: BytecodeContext) {} // Needed to make the patches unique.