-
-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Use Base64 for array encoding in SerializableContainer
- Loading branch information
Showing
10 changed files
with
336 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
27 changes: 27 additions & 0 deletions
27
...Main/kotlin/com/arkivanov/essenty/statekeeper/base64/ByteArrayAsBase64StringSerializer.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package com.arkivanov.essenty.statekeeper.base64 | ||
|
||
import kotlinx.serialization.KSerializer | ||
import kotlinx.serialization.descriptors.PrimitiveKind | ||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor | ||
import kotlinx.serialization.descriptors.SerialDescriptor | ||
import kotlinx.serialization.encoding.Decoder | ||
import kotlinx.serialization.encoding.Encoder | ||
|
||
/** | ||
* Serializer that encodes and decodes [ByteArray] using [Base64](https://en.wikipedia.org/wiki/Base64) encodings. | ||
* This is usually makes sense with text formats like JSON. | ||
*/ | ||
internal object ByteArrayAsBase64StringSerializer : KSerializer<ByteArray> { | ||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor( | ||
"kotlinx.serialization.ByteArrayAsBase64StringSerializer", | ||
PrimitiveKind.STRING | ||
) | ||
|
||
override fun deserialize(decoder: Decoder): ByteArray { | ||
return decode(decoder.decodeString()) | ||
} | ||
|
||
override fun serialize(encoder: Encoder, value: ByteArray) { | ||
encoder.encodeString(encode(value)) | ||
} | ||
} |
64 changes: 64 additions & 0 deletions
64
state-keeper/src/commonMain/kotlin/com/arkivanov/essenty/statekeeper/base64/Decoder.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
package com.arkivanov.essenty.statekeeper.base64 | ||
|
||
internal fun decode(encoded: String): ByteArray { | ||
if (encoded.isBlank()) return ByteArray(0) | ||
val result = ByteArray(encoded.length) | ||
var resultSize = 0 | ||
|
||
val backDictionary = backDictionary | ||
var buffer = 0 | ||
var buffered = 0 | ||
var index = 0 | ||
|
||
while (index < encoded.length) { | ||
val ch = encoded[index++] | ||
if (ch <= ' ') continue | ||
if (ch == '=') { | ||
index-- | ||
break | ||
} | ||
val value = backDictionary.getOrElse(ch.code) { -1 } | ||
if (value == -1) error("Unexpected character $ch (${ch.code})) in $encoded") | ||
|
||
buffer = buffer shl 6 or value | ||
buffered++ | ||
|
||
if (buffered == 4) { | ||
result[resultSize] = (buffer shr 16).toByte() | ||
result[resultSize + 1] = (buffer shr 8 and 0xff).toByte() | ||
result[resultSize + 2] = (buffer and 0xff).toByte() | ||
resultSize += 3 | ||
buffered = 0 | ||
buffer = 0 | ||
} | ||
} | ||
|
||
var padding = 0 | ||
while (index < encoded.length) { | ||
val ch = encoded[index++] | ||
if (ch <= ' ') continue | ||
check(ch == '=') | ||
padding++ | ||
buffer = buffer shl 6 | ||
buffered++ | ||
} | ||
|
||
if (buffered == 4) { | ||
result[resultSize] = (buffer shr 16).toByte() | ||
result[resultSize + 1] = (buffer shr 8 and 0xff).toByte() | ||
result[resultSize + 2] = (buffer and 0xff).toByte() | ||
resultSize += 3 | ||
|
||
resultSize -= padding | ||
buffered = 0 | ||
} | ||
|
||
check(buffered == 0) { | ||
"buffered: $buffered" | ||
} | ||
|
||
return when { | ||
resultSize < result.size -> result.copyOf(resultSize) | ||
else -> result | ||
} | ||
} |
7 changes: 7 additions & 0 deletions
7
state-keeper/src/commonMain/kotlin/com/arkivanov/essenty/statekeeper/base64/Dictionaries.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package com.arkivanov.essenty.statekeeper.base64 | ||
|
||
internal val dictionary: CharArray = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray() | ||
|
||
internal val backDictionary: IntArray = IntArray(0x80) { code -> | ||
dictionary.indexOf(code.toChar()) | ||
} |
50 changes: 50 additions & 0 deletions
50
state-keeper/src/commonMain/kotlin/com/arkivanov/essenty/statekeeper/base64/Encoder.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
package com.arkivanov.essenty.statekeeper.base64 | ||
|
||
internal fun encode(array: ByteArray): String = buildString(capacity = (array.size / 3) * 4 + 1) { | ||
var index = 0 | ||
|
||
while (index < array.size) { | ||
if (index + 3 > array.size) break | ||
|
||
val buffer = array[index].toInt() and 0xff shl 16 or | ||
(array[index + 1].toInt() and 0xff shl 8) or | ||
(array[index + 2].toInt() and 0xff shl 0) | ||
|
||
append(dictionary[buffer shr 18]) | ||
append(dictionary[buffer shr 12 and 0x3f]) | ||
append(dictionary[buffer shr 6 and 0x3f]) | ||
append(dictionary[buffer and 0x3f]) | ||
|
||
index += 3 | ||
} | ||
|
||
if (index < array.size) { | ||
var buffer = 0 | ||
while (index < array.size) { | ||
buffer = buffer shl 8 or (array[index].toInt() and 0xff) | ||
index++ | ||
} | ||
val padding = 3 - (index % 3) | ||
buffer = buffer shl (padding * 8) | ||
|
||
append(dictionary[buffer shr 18]) | ||
append(dictionary[buffer shr 12 and 0x3f]) | ||
|
||
val a = dictionary[buffer shr 6 and 0x3f] | ||
val b = dictionary[buffer and 0x3f] | ||
|
||
when (padding) { | ||
0 -> { | ||
append(a) | ||
append(b) | ||
} | ||
1 -> { | ||
append(a) | ||
append('=') | ||
} | ||
2 -> { | ||
append("==") | ||
} | ||
} | ||
} | ||
} |
3 changes: 3 additions & 0 deletions
3
...keeper/src/commonMain/kotlin/com/arkivanov/essenty/statekeeper/base64/README.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
The content of this package was copied from https://github.com/cy6erGn0m/kotlinx.serialization/tree/cy/base64/formats/base64/commonMain/src/kotlinx.serialization.base64/impl. | ||
|
||
Waiting for https://github.com/Kotlin/kotlinx.serialization/issues/1633. |
76 changes: 76 additions & 0 deletions
76
...eper/src/commonTest/kotlin/com/arkivanov/essenty/statekeeper/SerializableContainerTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
package com.arkivanov.essenty.statekeeper | ||
|
||
import kotlin.test.Test | ||
import kotlin.test.assertEquals | ||
import kotlin.test.assertNull | ||
|
||
@Suppress("TestFunctionName") | ||
class SerializableContainerTest { | ||
|
||
@Test | ||
fun GIVEN_value_not_set_WHEN_consume_THEN_returns_null() { | ||
val container = SerializableContainer() | ||
|
||
val data = container.consume(SerializableData.serializer()) | ||
|
||
assertNull(data) | ||
} | ||
|
||
@Test | ||
fun GIVEN_value_set_WHEN_consume_THEN_returns_value() { | ||
val data = SerializableData() | ||
val container = SerializableContainer(value = data, strategy = SerializableData.serializer()) | ||
|
||
val newData = container.consume(SerializableData.serializer()) | ||
|
||
assertEquals(data, newData) | ||
} | ||
|
||
@Test | ||
fun GIVEN_value_set_and_consumed_WHEN_consume_second_time_THEN_returns_null() { | ||
val container = SerializableContainer(value = SerializableData(), strategy = SerializableData.serializer()) | ||
container.consume(SerializableData.serializer()) | ||
|
||
val newData = container.consume(SerializableData.serializer()) | ||
|
||
assertNull(newData) | ||
} | ||
|
||
@Test | ||
fun serializes_and_deserializes_data() { | ||
val data = SerializableData() | ||
val container = SerializableContainer(value = data, strategy = SerializableData.serializer()) | ||
val newContainer = container.serializeAndDeserialize() | ||
val newData = newContainer.consume(strategy = SerializableData.serializer()) | ||
|
||
assertEquals(data, newData) | ||
} | ||
|
||
@Test | ||
fun serializes_and_deserializes_data_twice() { | ||
val data = SerializableData() | ||
val container = SerializableContainer(value = data, strategy = SerializableData.serializer()) | ||
val newContainer = container.serializeAndDeserialize().serializeAndDeserialize() | ||
val newData = newContainer.consume(strategy = SerializableData.serializer()) | ||
|
||
assertEquals(data, newData) | ||
} | ||
|
||
@Test | ||
fun serializes_and_deserializes_null() { | ||
val container = SerializableContainer() | ||
val newContainer = container.serializeAndDeserialize() | ||
val newData = newContainer.consume(strategy = SerializableData.serializer()) | ||
|
||
assertNull(newData) | ||
} | ||
|
||
@Test | ||
fun serializes_and_deserializes_null_twice() { | ||
val container = SerializableContainer() | ||
val newContainer = container.serializeAndDeserialize().serializeAndDeserialize() | ||
val newData = newContainer.consume(strategy = SerializableData.serializer()) | ||
|
||
assertNull(newData) | ||
} | ||
} |
62 changes: 62 additions & 0 deletions
62
...e-keeper/src/commonTest/kotlin/com/arkivanov/essenty/statekeeper/base64/Base64ImplTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
package com.arkivanov.essenty.statekeeper.base64 | ||
|
||
import kotlin.test.Test | ||
import kotlin.test.assertEquals | ||
|
||
class Base64ImplTest { | ||
|
||
@Test | ||
fun encodeSmokeTests() { | ||
testEncode("123", "MTIz") | ||
testEncode("abcdef", "YWJjZGVm") | ||
|
||
testEncode("1", "MQ==") | ||
testEncode("2", "Mg==") | ||
testEncode("12", "MTI=") | ||
|
||
testEncode("abcd", "YWJjZA==") | ||
testEncode("abcde", "YWJjZGU=") | ||
|
||
// RFC's testcases | ||
testEncode("", "") | ||
testEncode("f", "Zg==") | ||
testEncode("fo", "Zm8=") | ||
testEncode("foo", "Zm9v") | ||
testEncode("foob", "Zm9vYg==") | ||
testEncode("fooba", "Zm9vYmE=") | ||
testEncode("foobar", "Zm9vYmFy") | ||
} | ||
|
||
@Test | ||
fun decodeSmokeTests() { | ||
testDecode("123", "MTIz") | ||
testDecode("abcdef", "YWJjZGVm") | ||
|
||
testDecode("1", "MQ==") | ||
testDecode("2", "Mg==") | ||
testDecode("12", "MTI=") | ||
|
||
testDecode("abcd", "YWJjZA==") | ||
testDecode("abcde", "YWJjZGU=") | ||
|
||
// RFC | ||
// RFC's testcases | ||
testDecode("", "") | ||
testDecode("f", "Zg==") | ||
testDecode("fo", "Zm8=") | ||
testDecode("foo", "Zm9v") | ||
testDecode("foob", "Zm9vYg==") | ||
testDecode("fooba", "Zm9vYmE=") | ||
testDecode("foobar", "Zm9vYmFy") | ||
} | ||
|
||
private fun testEncode(input: String, expected: String) { | ||
val result = encode(input.encodeToByteArray()) | ||
assertEquals(expected, result) | ||
} | ||
|
||
private fun testDecode(expected: String, encoded: String) { | ||
val result = decode(encoded).decodeToString() | ||
assertEquals(expected, result) | ||
} | ||
} |
34 changes: 34 additions & 0 deletions
34
...src/commonTest/kotlin/com/arkivanov/essenty/statekeeper/base64/ByteArraySerializerTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
package com.arkivanov.essenty.statekeeper.base64 | ||
|
||
import kotlinx.serialization.Serializable | ||
import kotlinx.serialization.json.Json | ||
import kotlinx.serialization.json.encodeToJsonElement | ||
import kotlinx.serialization.json.jsonObject | ||
import kotlinx.serialization.json.jsonPrimitive | ||
import kotlin.test.Test | ||
import kotlin.test.assertContentEquals | ||
import kotlin.test.assertEquals | ||
|
||
class ByteArraySerializerTest { | ||
@Serializable | ||
class E( | ||
@Serializable(with = ByteArrayAsBase64StringSerializer::class) | ||
val bytes: ByteArray | ||
) | ||
|
||
@Test | ||
fun serialize() { | ||
val element = Json.encodeToJsonElement(E(byteArrayOf(0x31, 0x32, 0x33))).jsonObject | ||
val base64 = element["bytes"]?.jsonPrimitive?.content!! | ||
|
||
assertEquals("MTIz", base64) | ||
} | ||
|
||
@Test | ||
fun deserialize() { | ||
val json = "{\"bytes\": \"MTIz\"}" | ||
val decoded = Json.decodeFromString<E>(json).bytes | ||
|
||
assertContentEquals(byteArrayOf(0x31, 0x32, 0x33), decoded) | ||
} | ||
} |
3 changes: 3 additions & 0 deletions
3
...keeper/src/commonTest/kotlin/com/arkivanov/essenty/statekeeper/base64/README.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
The content of this package was copied from https://github.com/cy6erGn0m/kotlinx.serialization/tree/cy/base64/formats/base64/commonTest/src/kotlinx/serialization/base64. | ||
|
||
Waiting for https://github.com/Kotlin/kotlinx.serialization/issues/1633. |