diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/PreLoginResponseJson.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/PreLoginResponseJson.kt index 8b2b3b44d..20f458cb5 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/PreLoginResponseJson.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/PreLoginResponseJson.kt @@ -1,10 +1,7 @@ package com.x8bit.bitwarden.data.auth.datasource.network.model -import kotlinx.serialization.KSerializer +import com.x8bit.bitwarden.data.platform.datasource.network.serializer.BaseSurrogateSerializer import kotlinx.serialization.Serializable -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder private const val KDF_TYPE_ARGON2_ID = 1 private const val KDF_TYPE_PBKDF2_SHA256 = 0 @@ -40,38 +37,37 @@ data class PreLoginResponseJson( } } -private class PreLoginResponseSerializer : KSerializer { +private class PreLoginResponseSerializer : + BaseSurrogateSerializer() { - private val surrogateSerializer = InternalPreLoginResponseJson.serializer() + override val surrogateSerializer = InternalPreLoginResponseJson.serializer() - override val descriptor: SerialDescriptor = surrogateSerializer.descriptor + override fun InternalPreLoginResponseJson.toExternalType(): PreLoginResponseJson = + PreLoginResponseJson( + kdfParams = when (this.kdfType) { + KDF_TYPE_PBKDF2_SHA256 -> { + PreLoginResponseJson.KdfParams.Pbkdf2( + iterations = this.kdfIterations, + ) + } - override fun deserialize(decoder: Decoder): PreLoginResponseJson { - val surrogate = decoder.decodeSerializableValue(surrogateSerializer) - val kdfParams = when (surrogate.kdfType) { - KDF_TYPE_PBKDF2_SHA256 -> { - PreLoginResponseJson.KdfParams.Pbkdf2( - iterations = surrogate.kdfIterations, + KDF_TYPE_ARGON2_ID -> { + @Suppress("UnsafeCallOnNullableType") + PreLoginResponseJson.KdfParams.Argon2ID( + iterations = this.kdfIterations, + memory = this.kdfMemory!!, + parallelism = this.kdfParallelism!!, + ) + } + + else -> throw IllegalStateException( + "Unable to parse KDF params for unknown kdfType: ${this.kdfType}", ) - } + }, + ) - KDF_TYPE_ARGON2_ID -> { - PreLoginResponseJson.KdfParams.Argon2ID( - iterations = surrogate.kdfIterations, - memory = surrogate.kdfMemory!!, - parallelism = surrogate.kdfParallelism!!, - ) - } - - else -> throw IllegalStateException( - "Unable to parse KDF params for unknown kdfType: ${surrogate.kdfType}", - ) - } - return PreLoginResponseJson(kdfParams = kdfParams) - } - - override fun serialize(encoder: Encoder, value: PreLoginResponseJson) { - val surrogate = when (val params = value.kdfParams) { + override fun PreLoginResponseJson.toSurrogateType(): InternalPreLoginResponseJson = + when (val params = this.kdfParams) { is PreLoginResponseJson.KdfParams.Argon2ID -> { InternalPreLoginResponseJson( kdfType = KDF_TYPE_ARGON2_ID, @@ -88,9 +84,4 @@ private class PreLoginResponseSerializer : KSerializer { ) } } - encoder.encodeSerializableValue( - surrogateSerializer, - surrogate, - ) - } } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/serializer/BaseSurrogateSerializer.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/serializer/BaseSurrogateSerializer.kt new file mode 100644 index 000000000..6e00ba526 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/serializer/BaseSurrogateSerializer.kt @@ -0,0 +1,52 @@ +package com.x8bit.bitwarden.data.platform.datasource.network.serializer + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +/** + * A helper class that simplifies the process of providing a "surrogate" [KSerializer]. These are + * used to provide mappings between an "internal" type [R] (the "surrogate") to an external type + * [T]. + * + * See the [official surrogate documentation](https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/serializers.md#composite-serializer-via-surrogate) + * for details. + */ +abstract class BaseSurrogateSerializer : KSerializer { + + /** + * The [KSerializer] naturally associated with the type [R], which is a typical class annotated + * with [Serializable]. + */ + abstract val surrogateSerializer: KSerializer + + /** + * A conversion from the internal/surrogate type [R] to external type [T]. + */ + abstract fun R.toExternalType(): T + + /** + * A conversion from the external type [T] to the internal/surrogate type [R]. + */ + abstract fun T.toSurrogateType(): R + + //region KSerializer overrides + + override val descriptor: SerialDescriptor + get() = surrogateSerializer.descriptor + + final override fun deserialize(decoder: Decoder): T = + decoder + .decodeSerializableValue(surrogateSerializer) + .toExternalType() + + final override fun serialize(encoder: Encoder, value: T) { + encoder.encodeSerializableValue( + surrogateSerializer, + value.toSurrogateType(), + ) + } + + //endregion KSerializer overrides +} diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/serializer/BaseSurrogateSerializerTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/serializer/BaseSurrogateSerializerTest.kt new file mode 100644 index 000000000..76e35cc0c --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/serializer/BaseSurrogateSerializerTest.kt @@ -0,0 +1,71 @@ +package com.x8bit.bitwarden.data.platform.datasource.network.serializer + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.encodeToJsonElement +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class BaseSurrogateSerializerTest { + private val json = Json + + @Test + fun `properly deserializes raw JSON to the external model`() { + assertEquals( + ExternalData( + dataAsInt = 100, + ), + json.decodeFromString( + """ + { + "dataAsString": "100" + } + """, + ), + ) + } + + @Test + fun `properly serializes external model back to raw JSON`() { + assertEquals( + json.parseToJsonElement( + """ + { + "dataAsString": "100" + } + """, + ), + json.encodeToJsonElement( + ExternalData( + dataAsInt = 100, + ), + ), + ) + } +} + +@Serializable +private data class InternalData( + val dataAsString: String, +) + +@Serializable(TestSurrogateSerializer::class) +private data class ExternalData( + val dataAsInt: Int, +) + +private class TestSurrogateSerializer : BaseSurrogateSerializer() { + override val surrogateSerializer: KSerializer + get() = InternalData.serializer() + + override fun InternalData.toExternalType(): ExternalData = + ExternalData( + dataAsInt = this.dataAsString.toInt(), + ) + + override fun ExternalData.toSurrogateType(): InternalData = + InternalData( + dataAsString = this.dataAsInt.toString(), + ) +}