diff --git a/CHANGELOG.md b/CHANGELOG.md index 32726f8..9bb1c9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,13 @@ All notable changes to this project will be documented in this file. This change log follows the conventions of [keepachangelog.com](http://keepachangelog.com/). ## [Unreleased] +### Added - Upgraded kotlin version to 1.5.0 - Upgraded kotlinx-serialization version to 1.2.2 +- Added support for timestamp extension ([#10][i10]) + +### Fixed +- Bug with failing to decode extension types with variable data size ## [0.2.1] - 2020-09-07 ### Added @@ -36,6 +41,7 @@ All notable changes to this project will be documented in this file. This change [0.2.1]: https://github.com/esensar/kotlinx-serialization-msgpack/compare/0.2.0...0.2.1 [i6]: https://github.com/esensar/kotlinx-serialization-msgpack/issues/6 [i9]: https://github.com/esensar/kotlinx-serialization-msgpack/issues/9 +[i10]: https://github.com/esensar/kotlinx-serialization-msgpack/issues/10 [i11]: https://github.com/esensar/kotlinx-serialization-msgpack/issues/11 [i13]: https://github.com/esensar/kotlinx-serialization-msgpack/issues/13 [i14]: https://github.com/esensar/kotlinx-serialization-msgpack/issues/14 diff --git a/build.gradle.kts b/build.gradle.kts index 189a322..470c3bf 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -14,6 +14,14 @@ buildscript { } val snapshot: String? by project +val sonatypeStaging = "https://oss.sonatype.org/service/local/staging/deploy/maven2" +val sonatypeSnapshots = "https://oss.sonatype.org/content/repositories/snapshots" + +val sonatypePassword: String? by project +val sonatypeUsername: String? by project + +val sonatypePasswordEnv: String? = System.getenv()["SONATYPE_PASSWORD"] +val sonatypeUsernameEnv: String? = System.getenv()["SONATYPE_USERNAME"] allprojects { group = Config.group @@ -26,3 +34,82 @@ allprojects { mavenCentral() } } + +subprojects { + afterEvaluate { + apply(plugin = "maven-publish") + apply(plugin = "signing") + apply(plugin = "org.jetbrains.dokka") + + val dokkaHtml = tasks["dokkaHtml"] + tasks { + create("javadocJar") { + dependsOn(dokkaHtml) + archiveClassifier.set("javadoc") + from(dokkaHtml) + } + } + + configure { + isRequired = false + sign(extensions.getByType().publications) + } + + configure { + publications.withType(MavenPublication::class) { + artifact(tasks["javadocJar"]) + pom { + name.set("Kotlinx Serialization MsgPack") + description.set("MsgPack format support for kotlinx.serialization") + url.set("https://github.com/esensar/kotlinx-serialization-msgpack") + licenses { + license { + name.set("MIT License") + url.set("https://opensource.org/licenses/mit-license.php") + } + } + developers { + developer { + id.set("esensar") + name.set("Ensar Sarajčić") + url.set("https://ensarsarajcic.com") + email.set("es.ensar@gmail.com") + } + } + scm { + url.set("https://github.com/esensar/kotlinx-serialization-msgpack") + connection.set("scm:git:https://github.com/esensar/kotlinx-serialization-msgpack.git") + developerConnection.set("scm:git:git@github.com:esensar/kotlinx-serialization-msgpack.git") + } + } + } + repositories { + maven { + url = uri(sonatypeStaging) + credentials { + username = sonatypeUsername ?: sonatypeUsernameEnv ?: "" + password = sonatypePassword ?: sonatypePasswordEnv ?: "" + } + } + + maven { + name = "snapshot" + url = uri(sonatypeSnapshots) + credentials { + username = sonatypeUsername ?: sonatypeUsernameEnv ?: "" + password = sonatypePassword ?: sonatypePasswordEnv ?: "" + } + } + + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/esensar/kotlinx-serialization-msgpack") + credentials { + username = System.getenv("GITHUB_ACTOR") + password = System.getenv("GITHUB_TOKEN") + } + } + } + } + } +} diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt index 936f6a4..b813c63 100644 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -2,6 +2,7 @@ object Dependencies { object Versions { const val kotlin = "1.5.30" const val serialization = "1.2.2" + const val datetime = "0.2.1" const val ktlintGradle = "10.2.0" const val dokkaGradle = "1.5.0" } diff --git a/serialization-msgpack-timestamp-extension/build.gradle.kts b/serialization-msgpack-timestamp-extension/build.gradle.kts new file mode 100644 index 0000000..7181949 --- /dev/null +++ b/serialization-msgpack-timestamp-extension/build.gradle.kts @@ -0,0 +1,64 @@ +plugins { + kotlin("multiplatform") + kotlin("plugin.serialization") +} + +kotlin { + jvm { + compilations.all { + kotlinOptions.jvmTarget = "1.8" + } + } + js { + browser { + testTask { + useKarma { + useChromeHeadless() + webpackConfig.cssSupport.enabled = true + } + } + } + } + ios() + val hostOs = System.getProperty("os.name") + val isMingwX64 = hostOs.startsWith("Windows") + val nativeTarget = when { + hostOs == "Mac OS X" -> macosX64("native") + hostOs == "Linux" -> linuxX64("native") + isMingwX64 -> mingwX64("native") + else -> throw GradleException("Host OS is not supported in Kotlin/Native.") + } + + fun kotlinx(name: String, version: String): String = "org.jetbrains.kotlinx:kotlinx-$name:$version" + fun kotlinxSerialization(name: String) = kotlinx("serialization-$name", Dependencies.Versions.serialization) + + sourceSets { + all { + languageSettings.useExperimentalAnnotation("kotlin.RequiresOptIn") + languageSettings.useExperimentalAnnotation("kotlinx.serialization.ExperimentalSerializationApi") + } + + val commonMain by getting { + dependencies { + implementation(project(":serialization-msgpack")) + implementation(kotlinx("datetime", Dependencies.Versions.datetime)) + } + } + val commonTest by getting { + dependencies { + implementation(kotlin("test-common")) + implementation(kotlin("test-annotations-common")) + } + } + val jvmTest by getting { + dependencies { + implementation(kotlin("test-junit")) + } + } + val jsTest by getting { + dependencies { + implementation(kotlin("test-js")) + } + } + } +} diff --git a/serialization-msgpack-timestamp-extension/src/commonMain/kotlin/com.ensarsarajcic.kotlinx.serialization.msgpack/datetime/MsgPackDatetimeSerializer.kt b/serialization-msgpack-timestamp-extension/src/commonMain/kotlin/com.ensarsarajcic.kotlinx.serialization.msgpack/datetime/MsgPackDatetimeSerializer.kt new file mode 100644 index 0000000..2af0371 --- /dev/null +++ b/serialization-msgpack-timestamp-extension/src/commonMain/kotlin/com.ensarsarajcic.kotlinx.serialization.msgpack/datetime/MsgPackDatetimeSerializer.kt @@ -0,0 +1,40 @@ +package com.ensarsarajcic.kotlinx.serialization.msgpack.datetime + +import com.ensarsarajcic.kotlinx.serialization.msgpack.extensions.BaseMsgPackExtensionSerializer +import com.ensarsarajcic.kotlinx.serialization.msgpack.extensions.MsgPackExtension +import com.ensarsarajcic.kotlinx.serialization.msgpack.extensions.MsgPackTimestamp +import com.ensarsarajcic.kotlinx.serialization.msgpack.extensions.MsgPackTimestampExtensionSerializer +import kotlinx.datetime.Instant + +sealed class BaseMsgPackDatetimeSerializer(private val outputType: Byte) : BaseMsgPackExtensionSerializer() { + private val timestampSerializer = MsgPackTimestampExtensionSerializer() + override fun deserialize(extension: MsgPackExtension): Instant { + return when (val timestamp = timestampSerializer.deserialize(extension)) { + is MsgPackTimestamp.T32 -> Instant.fromEpochSeconds(timestamp.seconds, 0) + is MsgPackTimestamp.T64 -> Instant.fromEpochSeconds(timestamp.seconds, timestamp.nanoseconds) + is MsgPackTimestamp.T92 -> Instant.fromEpochSeconds(timestamp.seconds, timestamp.nanoseconds) + } + } + + override fun serialize(extension: Instant): MsgPackExtension { + val timestamp = when (outputType) { + 0.toByte() -> { + MsgPackTimestamp.T32(extension.epochSeconds) + } + 1.toByte() -> { + MsgPackTimestamp.T64(extension.epochSeconds, extension.nanosecondsOfSecond) + } + 2.toByte() -> { + MsgPackTimestamp.T92(extension.epochSeconds, extension.nanosecondsOfSecond.toLong()) + } + else -> TODO("Needs more info") + } + return timestampSerializer.serialize(timestamp) + } + + override val extTypeId: Byte = -1 +} + +class MsgPackTimestamp32DatetimeSerializer() : BaseMsgPackDatetimeSerializer(0) +class MsgPackTimestamp64DatetimeSerializer() : BaseMsgPackDatetimeSerializer(1) +class MsgPackTimestamp92DatetimeSerializer() : BaseMsgPackDatetimeSerializer(2) diff --git a/serialization-msgpack-timestamp-extension/src/commonTest/kotlin/com.ensarsarajcic.kotlinx.serialization.msgpack/datetime/MsgPackDatetimeSerializerTest.kt b/serialization-msgpack-timestamp-extension/src/commonTest/kotlin/com.ensarsarajcic.kotlinx.serialization.msgpack/datetime/MsgPackDatetimeSerializerTest.kt new file mode 100644 index 0000000..ff9d1c6 --- /dev/null +++ b/serialization-msgpack-timestamp-extension/src/commonTest/kotlin/com.ensarsarajcic.kotlinx.serialization.msgpack/datetime/MsgPackDatetimeSerializerTest.kt @@ -0,0 +1,141 @@ +package com.ensarsarajcic.kotlinx.serialization.msgpack.datetime + +import com.ensarsarajcic.kotlinx.serialization.msgpack.extensions.MsgPackExtension +import com.ensarsarajcic.kotlinx.serialization.msgpack.extensions.MsgPackTimestamp +import kotlinx.datetime.Instant +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.fail + +class MsgPackDatetimeSerializerTest { + private val t32TestPairs = arrayOf( + Instant.fromEpochSeconds(0) to MsgPackExtension(MsgPackExtension.Type.FIXEXT4, -1, byteArrayOf(0x00, 0x00, 0x00, 0x00)), + Instant.fromEpochSeconds(1000) to MsgPackExtension(MsgPackExtension.Type.FIXEXT4, -1, byteArrayOf(0x00, 0x00, 0x03, 0xe8.toByte())), + Instant.fromEpochSeconds(4_294_967_295) to MsgPackExtension( + MsgPackExtension.Type.FIXEXT4, -1, + byteArrayOf( + 0xff.toByte(), + 0xff.toByte(), + 0xff.toByte(), + 0xff.toByte() + ) + ) + ) + + private val t64TestPairs = arrayOf( + Instant.fromEpochSeconds(0, 999_999_999) to MsgPackExtension( + MsgPackExtension.Type.FIXEXT8, + -1, + byteArrayOf( + 0xee.toByte(), 0x6b, 0x27, 0xfc.toByte(), + 0x00, 0x00, 0x00, 0x00 + ) + ), + Instant.fromEpochSeconds(50000, 1000) to MsgPackExtension( + MsgPackExtension.Type.FIXEXT8, + -1, + byteArrayOf( + 0x00, 0x00, 0x0f, 0xa0.toByte(), + 0x00, 0x00, 0xc3.toByte(), 0x50 + ) + ) + ) + + private val t92TestPairs = arrayOf( + Instant.fromEpochSeconds(-1000) to MsgPackExtension( + MsgPackExtension.Type.EXT8, + -1, + byteArrayOf( + 0x00, 0x00, 0x00, 0x00, + 0xff.toByte(), 0xff.toByte(), 0xff.toByte(), 0xff.toByte(), + 0xff.toByte(), 0xff.toByte(), 0xfc.toByte(), 0x18 + ) + ), + Instant.fromEpochSeconds(-1000, 1000) to MsgPackExtension( + MsgPackExtension.Type.EXT8, + -1, + byteArrayOf( + 0x00, 0x00, 0x03, 0xe8.toByte(), + 0xff.toByte(), 0xff.toByte(), 0xff.toByte(), 0xff.toByte(), + 0xff.toByte(), 0xff.toByte(), 0xfc.toByte(), 0x18 + ) + ), + Instant.fromEpochSeconds(Instant.DISTANT_FUTURE.epochSeconds) to MsgPackExtension( + MsgPackExtension.Type.EXT8, + -1, + byteArrayOf( + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x02, 0xd0.toByte(), + 0x44, 0xa2.toByte(), 0xeb.toByte(), 0x00 + ) + ), + Instant.fromEpochSeconds(Instant.DISTANT_PAST.epochSeconds) to MsgPackExtension( + MsgPackExtension.Type.EXT8, + -1, + byteArrayOf( + 0x00, 0x00, 0x00, 0x00, + 0xff.toByte(), 0xff.toByte(), 0xfd.toByte(), 0x12, + 0xc8.toByte(), 0x74, 0x1c, 0xff.toByte() + ) + ) + ) + + @Test + fun testT32Encode() { + testEncodePairs(t32TestPairs) + } + + @Test + fun testT32Decode() { + testDecodePairs(t32TestPairs) + } + + @Test + fun testT64Encode() { + testEncodePairs(t64TestPairs) + } + + @Test + fun testT64Decode() { + testDecodePairs(t64TestPairs) + } + + @Test + fun testT92Encode() { + testEncodePairs(t92TestPairs) + } + + @Test + fun testT92Decode() { + testDecodePairs(t92TestPairs) + } + + private inline fun testEncodePairs(pairs: Array>) { + pairs.forEach { (time, expected) -> + val serializer = when (T::class) { + MsgPackTimestamp.T32::class -> MsgPackTimestamp32DatetimeSerializer() + MsgPackTimestamp.T64::class -> MsgPackTimestamp64DatetimeSerializer() + MsgPackTimestamp.T92::class -> MsgPackTimestamp92DatetimeSerializer() + else -> fail() + } + val result = serializer.serialize(time) + assertEquals(expected.type, result.type) + assertEquals(expected.extTypeId, result.extTypeId) + assertContentEquals(expected.data, result.data) + } + } + + private inline fun testDecodePairs(pairs: Array>) { + pairs.forEach { (expected, extension) -> + val serializer = when (T::class) { + MsgPackTimestamp.T32::class -> MsgPackTimestamp32DatetimeSerializer() + MsgPackTimestamp.T64::class -> MsgPackTimestamp64DatetimeSerializer() + MsgPackTimestamp.T92::class -> MsgPackTimestamp92DatetimeSerializer() + else -> fail() + } + val result = serializer.deserialize(extension) + assertEquals(expected, result) + } + } +} diff --git a/serialization-msgpack/build.gradle.kts b/serialization-msgpack/build.gradle.kts index f267b05..8d94cc4 100644 --- a/serialization-msgpack/build.gradle.kts +++ b/serialization-msgpack/build.gradle.kts @@ -1,23 +1,8 @@ plugins { kotlin("multiplatform") kotlin("plugin.serialization") - id("maven-publish") - id("signing") - id("org.jetbrains.dokka") } -val sonatypeStaging = "https://oss.sonatype.org/service/local/staging/deploy/maven2" -val sonatypeSnapshots = "https://oss.sonatype.org/content/repositories/snapshots" - -val sonatypePassword: String? by project -val sonatypeUsername: String? by project - -val sonatypePasswordEnv: String? = System.getenv()["SONATYPE_PASSWORD"] -val sonatypeUsernameEnv: String? = System.getenv()["SONATYPE_USERNAME"] - -repositories { - mavenCentral() -} kotlin { jvm { compilations.all { @@ -64,13 +49,11 @@ kotlin { implementation(kotlin("test-annotations-common")) } } - val jvmMain by getting val jvmTest by getting { dependencies { implementation(kotlin("test-junit")) } } - val jsMain by getting val jsTest by getting { dependencies { implementation(kotlin("test-js")) @@ -82,73 +65,3 @@ kotlin { val iosTest by getting } } - -tasks { - create("javadocJar") { - dependsOn(dokkaHtml) - archiveClassifier.set("javadoc") - from(dokkaHtml.get().outputDirectory) - } -} - -signing { - isRequired = false - sign(publishing.publications) -} - -publishing { - publications.withType(MavenPublication::class) { - artifact(tasks["javadocJar"]) - pom { - name.set("Kotlinx Serialization MsgPack") - description.set("MsgPack format support for kotlinx.serialization") - url.set("https://github.com/esensar/kotlinx-serialization-msgpack") - licenses { - license { - name.set("MIT License") - url.set("https://opensource.org/licenses/mit-license.php") - } - } - developers { - developer { - id.set("esensar") - name.set("Ensar Sarajčić") - url.set("https://ensarsarajcic.com") - email.set("es.ensar@gmail.com") - } - } - scm { - url.set("https://github.com/esensar/kotlinx-serialization-msgpack") - connection.set("scm:git:https://github.com/esensar/kotlinx-serialization-msgpack.git") - developerConnection.set("scm:git:git@github.com:esensar/kotlinx-serialization-msgpack.git") - } - } - } - repositories { - maven { - url = uri(sonatypeStaging) - credentials { - username = sonatypeUsername ?: sonatypeUsernameEnv ?: "" - password = sonatypePassword ?: sonatypePasswordEnv ?: "" - } - } - - maven { - name = "snapshot" - url = uri(sonatypeSnapshots) - credentials { - username = sonatypeUsername ?: sonatypeUsernameEnv ?: "" - password = sonatypePassword ?: sonatypePasswordEnv ?: "" - } - } - - maven { - name = "GitHubPackages" - url = uri("https://maven.pkg.github.com/esensar/kotlinx-serialization-msgpack") - credentials { - username = System.getenv("GITHUB_ACTOR") - password = System.getenv("GITHUB_TOKEN") - } - } - } -} diff --git a/serialization-msgpack/src/commonMain/kotlin/com.ensarsarajcic.kotlinx.serialization.msgpack/MsgPackDecoder.kt b/serialization-msgpack/src/commonMain/kotlin/com.ensarsarajcic.kotlinx.serialization.msgpack/MsgPackDecoder.kt index 37d51f8..0d568e6 100644 --- a/serialization-msgpack/src/commonMain/kotlin/com.ensarsarajcic.kotlinx.serialization.msgpack/MsgPackDecoder.kt +++ b/serialization-msgpack/src/commonMain/kotlin/com.ensarsarajcic.kotlinx.serialization.msgpack/MsgPackDecoder.kt @@ -2,6 +2,7 @@ package com.ensarsarajcic.kotlinx.serialization.msgpack import com.ensarsarajcic.kotlinx.serialization.msgpack.stream.MsgPackDataInputBuffer import com.ensarsarajcic.kotlinx.serialization.msgpack.types.MsgPackType +import com.ensarsarajcic.kotlinx.serialization.msgpack.utils.joinToNumber import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.builtins.ByteArraySerializer import kotlinx.serialization.descriptors.SerialDescriptor @@ -244,14 +245,19 @@ internal class MsgPackDecoder( bytesRead += sizeSize!! size = dataBuffer.takeNext(sizeSize).joinToNumber() val byte = dataBuffer.requireNextByte() - bytesRead++ typeId = byte typeId!! } else { throw TODO("Handle?") } } - override fun decodeElementIndex(descriptor: SerialDescriptor): Int = if (bytesRead <= 2) bytesRead else CompositeDecoder.DECODE_DONE + override fun decodeElementIndex(descriptor: SerialDescriptor): Int { + return if (bytesRead <= 2) bytesRead else CompositeDecoder.DECODE_DONE + } + + override fun decodeCollectionSize(descriptor: SerialDescriptor): Int { + return size ?: 0 + } override fun decodeSerializableValue(deserializer: DeserializationStrategy): T { bytesRead += 1 @@ -260,19 +266,4 @@ internal class MsgPackDecoder( ) as T } } - - private inline fun ByteArray.joinToNumber(): T { - val number = mapIndexed { index, byte -> - (byte.toLong() and 0xff) shl (8 * (size - (index + 1))) - }.fold(0L) { acc, it -> - acc or it - } - return when (T::class) { - Byte::class -> number.toByte() - Short::class -> number.toShort() - Int::class -> number.toInt() - Long::class -> number - else -> throw UnsupportedOperationException("Can't build ${T::class} from ByteArray (${this.toList()})") - } as T - } } diff --git a/serialization-msgpack/src/commonMain/kotlin/com.ensarsarajcic.kotlinx.serialization.msgpack/MsgPackEncoder.kt b/serialization-msgpack/src/commonMain/kotlin/com.ensarsarajcic.kotlinx.serialization.msgpack/MsgPackEncoder.kt index d673346..12b7780 100644 --- a/serialization-msgpack/src/commonMain/kotlin/com.ensarsarajcic.kotlinx.serialization.msgpack/MsgPackEncoder.kt +++ b/serialization-msgpack/src/commonMain/kotlin/com.ensarsarajcic.kotlinx.serialization.msgpack/MsgPackEncoder.kt @@ -2,6 +2,7 @@ package com.ensarsarajcic.kotlinx.serialization.msgpack import com.ensarsarajcic.kotlinx.serialization.msgpack.stream.MsgPackDataOutputBuffer import com.ensarsarajcic.kotlinx.serialization.msgpack.types.MsgPackType +import com.ensarsarajcic.kotlinx.serialization.msgpack.utils.splitToByteArray import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerializationStrategy import kotlinx.serialization.builtins.ByteArraySerializer @@ -256,11 +257,11 @@ internal class MsgPackEncoder( if (size!!.toLong() > maxSize) throw TODO("Size exceeded") result.addAll( when (type) { - MsgPackType.Ext.EXT8 -> size!!.toByte() - MsgPackType.Ext.EXT16 -> size!!.toShort() - MsgPackType.Ext.EXT32 -> size!!.toInt() + MsgPackType.Ext.EXT8 -> size!!.toByte().splitToByteArray() + MsgPackType.Ext.EXT16 -> size!!.toShort().splitToByteArray() + MsgPackType.Ext.EXT32 -> size!!.toInt().splitToByteArray() else -> TODO("HANDLE") - }.splitToByteArray() + } ) result.add(typeId!!) } else { @@ -353,20 +354,4 @@ internal class MsgPackEncoder( encodeSerializableValue(serializer, value) } } - - private inline fun T.splitToByteArray(): ByteArray { - val byteCount = when (T::class) { - Byte::class -> 1 - Short::class -> 2 - Int::class -> 4 - Long::class -> 8 - else -> throw UnsupportedOperationException("Can't split number of type ${T::class} to bytes!") - } - - val result = ByteArray(byteCount) - (byteCount - 1).downTo(0).forEach { - result[byteCount - (it + 1)] = ((this.toLong() shr (8 * it)) and 0xff).toByte() - } - return result - } } diff --git a/serialization-msgpack/src/commonMain/kotlin/com.ensarsarajcic.kotlinx.serialization.msgpack/extensions/BaseMsgPackExtensionSerializer.kt b/serialization-msgpack/src/commonMain/kotlin/com.ensarsarajcic.kotlinx.serialization.msgpack/extensions/BaseMsgPackExtensionSerializer.kt index 79964c3..8e75c1a 100644 --- a/serialization-msgpack/src/commonMain/kotlin/com.ensarsarajcic.kotlinx.serialization.msgpack/extensions/BaseMsgPackExtensionSerializer.kt +++ b/serialization-msgpack/src/commonMain/kotlin/com.ensarsarajcic.kotlinx.serialization.msgpack/extensions/BaseMsgPackExtensionSerializer.kt @@ -13,9 +13,6 @@ abstract class BaseMsgPackExtensionSerializer : KSerializer { if (extension.extTypeId != extTypeId) { throw TODO("Add more info") } - if (extension.type != type) { - throw TODO("Add more info") - } return deserialize(extension) } @@ -26,14 +23,10 @@ abstract class BaseMsgPackExtensionSerializer : KSerializer { if (extension.extTypeId != extTypeId) { throw TODO("Add more info") } - if (extension.type != type) { - throw TODO("Add more info") - } encoder.encodeSerializableValue(serializer, serialize(value)) } abstract fun deserialize(extension: MsgPackExtension): T abstract fun serialize(extension: T): MsgPackExtension abstract val extTypeId: Byte - abstract val type: Byte } diff --git a/serialization-msgpack/src/commonMain/kotlin/com.ensarsarajcic.kotlinx.serialization.msgpack/extensions/MsgPackExtension.kt b/serialization-msgpack/src/commonMain/kotlin/com.ensarsarajcic.kotlinx.serialization.msgpack/extensions/MsgPackExtension.kt index e1bde27..ae56c33 100644 --- a/serialization-msgpack/src/commonMain/kotlin/com.ensarsarajcic.kotlinx.serialization.msgpack/extensions/MsgPackExtension.kt +++ b/serialization-msgpack/src/commonMain/kotlin/com.ensarsarajcic.kotlinx.serialization.msgpack/extensions/MsgPackExtension.kt @@ -1,5 +1,6 @@ package com.ensarsarajcic.kotlinx.serialization.msgpack.extensions +import com.ensarsarajcic.kotlinx.serialization.msgpack.types.MsgPackType import kotlinx.serialization.Serializable @Serializable @@ -7,4 +8,15 @@ class MsgPackExtension( val type: Byte, val extTypeId: Byte, val data: ByteArray -) +) { + object Type { + const val FIXEXT1 = MsgPackType.Ext.FIXEXT1 + const val FIXEXT2 = MsgPackType.Ext.FIXEXT2 + const val FIXEXT4 = MsgPackType.Ext.FIXEXT4 + const val FIXEXT8 = MsgPackType.Ext.FIXEXT8 + const val FIXEXT16 = MsgPackType.Ext.FIXEXT16 + const val EXT8 = MsgPackType.Ext.EXT8 + const val EXT16 = MsgPackType.Ext.EXT16 + const val EXT32 = MsgPackType.Ext.EXT32 + } +} diff --git a/serialization-msgpack/src/commonMain/kotlin/com.ensarsarajcic.kotlinx.serialization.msgpack/extensions/MsgPackTimestampExtension.kt b/serialization-msgpack/src/commonMain/kotlin/com.ensarsarajcic.kotlinx.serialization.msgpack/extensions/MsgPackTimestampExtension.kt new file mode 100644 index 0000000..49e3d1c --- /dev/null +++ b/serialization-msgpack/src/commonMain/kotlin/com.ensarsarajcic.kotlinx.serialization.msgpack/extensions/MsgPackTimestampExtension.kt @@ -0,0 +1,104 @@ +package com.ensarsarajcic.kotlinx.serialization.msgpack.extensions + +import com.ensarsarajcic.kotlinx.serialization.msgpack.utils.joinToNumber +import com.ensarsarajcic.kotlinx.serialization.msgpack.utils.splitToByteArray +import kotlinx.serialization.Serializable + +@Serializable(with = MsgPackTimestampExtensionSerializer::class) +sealed class MsgPackTimestamp { + companion object { + private const val NANOSECONDS_MAX = 999999999 + } + data class T32(val seconds: Long) : MsgPackTimestamp() + data class T64(val seconds: Long, val nanoseconds: Int = 0) : MsgPackTimestamp() { + init { + if (nanoseconds > NANOSECONDS_MAX) { + throw IllegalArgumentException("Nanoseconds part may not be larger than $NANOSECONDS_MAX. Found: $nanoseconds") + } + } + } + data class T92(val seconds: Long, val nanoseconds: Long = 0) : MsgPackTimestamp() { + init { + if (nanoseconds > NANOSECONDS_MAX) { + throw IllegalArgumentException("Nanoseconds part may not be larger than $NANOSECONDS_MAX. Found: $nanoseconds") + } + } + } +} + +class MsgPackTimestampExtensionSerializer : + BaseMsgPackExtensionSerializer() { + private companion object { + const val TIMESTAMP_96_DATA_SIZE = 12 + } + + override val extTypeId: Byte = -1 + + override fun deserialize(extension: MsgPackExtension): MsgPackTimestamp { + when (extension.type) { + MsgPackExtension.Type.FIXEXT4 -> { + // Working with Timestamp 32 + val timestamp = extension.data.joinToNumber() + return MsgPackTimestamp.T32(timestamp) + } + MsgPackExtension.Type.FIXEXT8 -> { + // Working with Timestamp 64 + val nanoseconds = extension.data + .take(4) + .toByteArray() + .joinToNumber() + .shr(2) // Shift right by 2 to get 30-bit number + .toInt() + val seconds = extension.data[3].toLong().shl(62).ushr(30) + + extension.data + .takeLast(4) + .toByteArray() + .joinToNumber() + return MsgPackTimestamp.T64(seconds, nanoseconds) + } + MsgPackExtension.Type.EXT8 -> { + // Working with Timestamp 96 + if (extension.data.size != TIMESTAMP_96_DATA_SIZE) { + throw TODO("Needs more info") + } + val nanoseconds = extension.data + .take(4) + .toByteArray() + .joinToNumber() + val seconds = extension.data + .takeLast(8) + .toByteArray() + .joinToNumber() + return MsgPackTimestamp.T92(seconds, nanoseconds) + } + else -> TODO("Needs more info") + } + } + + override fun serialize(extension: MsgPackTimestamp): MsgPackExtension { + return when (extension) { + is MsgPackTimestamp.T32 -> MsgPackExtension( + MsgPackExtension.Type.FIXEXT4, + extTypeId, + extension.seconds.toInt().splitToByteArray() + ) + is MsgPackTimestamp.T64 -> { + val nanoseconds = extension.nanoseconds.shl(2).splitToByteArray() + val nanoLastByte = nanoseconds.last().toInt() and 0xff + val seconds = extension.seconds.splitToByteArray().takeLast(5) + val secondsFirstByte = seconds.first().toInt() and 0xff + val combinedByte: Byte = ((nanoLastByte or (secondsFirstByte ushr 6)) and 0xff).toByte() + MsgPackExtension( + MsgPackExtension.Type.FIXEXT8, + extTypeId, + nanoseconds.take(3).toByteArray() + combinedByte + seconds.takeLast(4) + ) + } + is MsgPackTimestamp.T92 -> MsgPackExtension( + MsgPackExtension.Type.EXT8, + extTypeId, + extension.nanoseconds.toInt().splitToByteArray() + extension.seconds.splitToByteArray() + ) + } + } +} diff --git a/serialization-msgpack/src/commonMain/kotlin/com.ensarsarajcic.kotlinx.serialization.msgpack/utils/ByteArrayUtils.kt b/serialization-msgpack/src/commonMain/kotlin/com.ensarsarajcic.kotlinx.serialization.msgpack/utils/ByteArrayUtils.kt new file mode 100644 index 0000000..37767a2 --- /dev/null +++ b/serialization-msgpack/src/commonMain/kotlin/com.ensarsarajcic.kotlinx.serialization.msgpack/utils/ByteArrayUtils.kt @@ -0,0 +1,32 @@ +package com.ensarsarajcic.kotlinx.serialization.msgpack.utils + +internal inline fun T.splitToByteArray(): ByteArray { + val byteCount = when (T::class) { + Byte::class -> 1 + Short::class -> 2 + Int::class -> 4 + Long::class -> 8 + else -> throw UnsupportedOperationException("Can't split number of type ${T::class} to bytes!") + } + + val result = ByteArray(byteCount) + (byteCount - 1).downTo(0).forEach { + result[byteCount - (it + 1)] = ((this.toLong() shr (8 * it)) and 0xff).toByte() + } + return result +} + +internal inline fun ByteArray.joinToNumber(): T { + val number = mapIndexed { index, byte -> + (byte.toLong() and 0xff) shl (8 * (size - (index + 1))) + }.fold(0L) { acc, it -> + acc or it + } + return when (T::class) { + Byte::class -> number.toByte() + Short::class -> number.toShort() + Int::class -> number.toInt() + Long::class -> number + else -> throw UnsupportedOperationException("Can't build ${T::class} from ByteArray (${this.toList()})") + } as T +} diff --git a/serialization-msgpack/src/commonTest/kotlin/com/ensarsarajcic/kotlinx/serialization/msgpack/MsgPackDecoderTest.kt b/serialization-msgpack/src/commonTest/kotlin/com/ensarsarajcic/kotlinx/serialization/msgpack/MsgPackDecoderTest.kt index 34a619e..6b32da9 100644 --- a/serialization-msgpack/src/commonTest/kotlin/com/ensarsarajcic/kotlinx/serialization/msgpack/MsgPackDecoderTest.kt +++ b/serialization-msgpack/src/commonTest/kotlin/com/ensarsarajcic/kotlinx/serialization/msgpack/MsgPackDecoderTest.kt @@ -1,5 +1,6 @@ package com.ensarsarajcic.kotlinx.serialization.msgpack +import com.ensarsarajcic.kotlinx.serialization.msgpack.extensions.MsgPackTimestamp import com.ensarsarajcic.kotlinx.serialization.msgpack.stream.toMsgPackBuffer import kotlinx.serialization.builtins.ArraySerializer import kotlinx.serialization.builtins.MapSerializer @@ -148,6 +149,15 @@ internal class MsgPackDecoderTest { } } + @Test + fun testTimestampDecode() { + TestData.timestampTestPairs.forEach { (input, result) -> + val decoder = MsgPackDecoder(MsgPackConfiguration.default, SerializersModule {}, input.hexStringToByteArray().toMsgPackBuffer()) + val serializer = MsgPackTimestamp.serializer() + assertEquals(result, serializer.deserialize(decoder)) + } + } + private fun testPairs(decodeFunction: MsgPackDecoder.() -> RESULT, vararg pairs: Pair) { pairs.forEach { (input, result) -> MsgPackDecoder(MsgPackConfiguration.default, SerializersModule {}, input.hexStringToByteArray().toMsgPackBuffer()).also { diff --git a/serialization-msgpack/src/commonTest/kotlin/com/ensarsarajcic/kotlinx/serialization/msgpack/MsgPackEncoderTest.kt b/serialization-msgpack/src/commonTest/kotlin/com/ensarsarajcic/kotlinx/serialization/msgpack/MsgPackEncoderTest.kt index fcfa552..6da6ab9 100644 --- a/serialization-msgpack/src/commonTest/kotlin/com/ensarsarajcic/kotlinx/serialization/msgpack/MsgPackEncoderTest.kt +++ b/serialization-msgpack/src/commonTest/kotlin/com/ensarsarajcic/kotlinx/serialization/msgpack/MsgPackEncoderTest.kt @@ -1,5 +1,6 @@ package com.ensarsarajcic.kotlinx.serialization.msgpack +import com.ensarsarajcic.kotlinx.serialization.msgpack.extensions.MsgPackTimestamp import kotlinx.serialization.builtins.ArraySerializer import kotlinx.serialization.builtins.MapSerializer import kotlinx.serialization.builtins.serializer @@ -142,6 +143,16 @@ internal class MsgPackEncoderTest { } } + @Test + fun testTimestampEncode() { + TestData.timestampTestPairs.forEach { (result, input) -> + val encoder = MsgPackEncoder(MsgPackConfiguration.default, SerializersModule {}) + val serializer = MsgPackTimestamp.serializer() + serializer.serialize(encoder, input) + assertEquals(result, encoder.result.toByteArray().toHex()) + } + } + private fun testPairs(encodeFunction: MsgPackEncoder.(INPUT) -> Unit, vararg pairs: Pair) { pairs.forEach { (result, input) -> MsgPackEncoder(MsgPackConfiguration.default, SerializersModule {}).also { diff --git a/serialization-msgpack/src/commonTest/kotlin/com/ensarsarajcic/kotlinx/serialization/msgpack/MsgPackTest.kt b/serialization-msgpack/src/commonTest/kotlin/com/ensarsarajcic/kotlinx/serialization/msgpack/MsgPackTest.kt index 0ca9591..8c20acb 100644 --- a/serialization-msgpack/src/commonTest/kotlin/com/ensarsarajcic/kotlinx/serialization/msgpack/MsgPackTest.kt +++ b/serialization-msgpack/src/commonTest/kotlin/com/ensarsarajcic/kotlinx/serialization/msgpack/MsgPackTest.kt @@ -1,5 +1,6 @@ package com.ensarsarajcic.kotlinx.serialization.msgpack +import com.ensarsarajcic.kotlinx.serialization.msgpack.extensions.MsgPackTimestamp import kotlinx.serialization.KSerializer import kotlinx.serialization.builtins.ArraySerializer import kotlinx.serialization.builtins.ByteArraySerializer @@ -253,6 +254,22 @@ internal class MsgPackTest { ) } + @Test + fun testTimestampEncode() { + testEncodePairs( + MsgPackTimestamp.serializer(), + *TestData.timestampTestPairs + ) + } + + @Test + fun testTimestampDecode() { + testDecodePairs( + MsgPackTimestamp.serializer(), + *TestData.timestampTestPairs + ) + } + private fun testEncodePairs(serializer: KSerializer, vararg pairs: Pair) { pairs.forEach { (expectedResult, value) -> assertEquals(expectedResult, MsgPack.default.encodeToByteArray(serializer, value).toHex()) diff --git a/serialization-msgpack/src/commonTest/kotlin/com/ensarsarajcic/kotlinx/serialization/msgpack/TestData.kt b/serialization-msgpack/src/commonTest/kotlin/com/ensarsarajcic/kotlinx/serialization/msgpack/TestData.kt index 8cc9b2c..ba36c50 100644 --- a/serialization-msgpack/src/commonTest/kotlin/com/ensarsarajcic/kotlinx/serialization/msgpack/TestData.kt +++ b/serialization-msgpack/src/commonTest/kotlin/com/ensarsarajcic/kotlinx/serialization/msgpack/TestData.kt @@ -2,6 +2,7 @@ package com.ensarsarajcic.kotlinx.serialization.msgpack import com.ensarsarajcic.kotlinx.serialization.msgpack.extensions.BaseMsgPackExtensionSerializer import com.ensarsarajcic.kotlinx.serialization.msgpack.extensions.MsgPackExtension +import com.ensarsarajcic.kotlinx.serialization.msgpack.extensions.MsgPackTimestamp import com.ensarsarajcic.kotlinx.serialization.msgpack.types.MsgPackType import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -108,6 +109,17 @@ object TestData { val mapTestPairs = arrayOf( "81a3616263a3646566" to mapOf("abc" to "def") ) + val timestampTestPairs = arrayOf( + "d6ff00000000" to MsgPackTimestamp.T32(0), + "d6ff000003e8" to MsgPackTimestamp.T32(1000), + "d6ffffffffff" to MsgPackTimestamp.T32(4_294_967_295), + "d7ffee6b27fc00000000" to MsgPackTimestamp.T64(0, 999_999_999), + "d7ff00000fa00000c350" to MsgPackTimestamp.T64(50000, 1000), + "c70cff00000000fffffffffffffc18" to MsgPackTimestamp.T92(-1000), + "c70cff000003e8fffffffffffffc18" to MsgPackTimestamp.T92(-1000, 1000), + "c70cff000000007fffffffffffffff" to MsgPackTimestamp.T92(9_223_372_036_854_775_807), + "c70cff000000008000000000000001" to MsgPackTimestamp.T92(-9_223_372_036_854_775_807), + ) @Serializable data class SampleClass( @@ -127,10 +139,9 @@ data class CustomExtensionType(val data: List) class CustomExtensionSerializer() : BaseMsgPackExtensionSerializer() { - override val type: Byte = MsgPackType.Ext.FIXEXT2 override val extTypeId: Byte = 3 override fun deserialize(extension: MsgPackExtension): CustomExtensionType = CustomExtensionType(extension.data.toList()) - override fun serialize(extension: CustomExtensionType): MsgPackExtension = MsgPackExtension(type, extTypeId, extension.data.toByteArray()) + override fun serialize(extension: CustomExtensionType): MsgPackExtension = MsgPackExtension(MsgPackType.Ext.FIXEXT2, extTypeId, extension.data.toByteArray()) } diff --git a/serialization-msgpack/src/commonTest/kotlin/com/ensarsarajcic/kotlinx/serialization/msgpack/extension/MsgPackExtensionDecoderTest.kt b/serialization-msgpack/src/commonTest/kotlin/com/ensarsarajcic/kotlinx/serialization/msgpack/extension/MsgPackExtensionDecoderTest.kt index d59562b..3228553 100644 --- a/serialization-msgpack/src/commonTest/kotlin/com/ensarsarajcic/kotlinx/serialization/msgpack/extension/MsgPackExtensionDecoderTest.kt +++ b/serialization-msgpack/src/commonTest/kotlin/com/ensarsarajcic/kotlinx/serialization/msgpack/extension/MsgPackExtensionDecoderTest.kt @@ -25,6 +25,18 @@ internal class MsgPackExtensionDecoderTest { assertEquals(expectedOutput.data.toList(), result.data.toList()) } + @Test + fun testExtensionVariableDecode() { + val input = "c71a016c6a686673646c6a6668736b66687364686b6673646a66687364" + val expectedOutput = MsgPackExtension(0xc7.toByte(), 0x01, "6c6a686673646c6a6668736b66687364686b6673646a66687364".hexStringToByteArray()) + val decoder = MsgPackDecoder(MsgPackConfiguration.default, SerializersModule {}, input.hexStringToByteArray().toMsgPackBuffer()) + val serializer = MsgPackExtension.serializer() + val result = serializer.deserialize(decoder) + assertEquals(expectedOutput.type, result.type) + assertEquals(expectedOutput.extTypeId, result.extTypeId) + assertEquals(expectedOutput.data.toList(), result.data.toList()) + } + @Test fun testCustomExtensionDecode() { val input = "d5030102" diff --git a/serialization-msgpack/src/commonTest/kotlin/com/ensarsarajcic/kotlinx/serialization/msgpack/utils/ByteArrayUtilsKtTest.kt b/serialization-msgpack/src/commonTest/kotlin/com/ensarsarajcic/kotlinx/serialization/msgpack/utils/ByteArrayUtilsKtTest.kt new file mode 100644 index 0000000..d29f345 --- /dev/null +++ b/serialization-msgpack/src/commonTest/kotlin/com/ensarsarajcic/kotlinx/serialization/msgpack/utils/ByteArrayUtilsKtTest.kt @@ -0,0 +1,100 @@ +package com.ensarsarajcic.kotlinx.serialization.msgpack.utils + +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals + +internal class ByteArrayUtilsKtTest { + private val byteTestPairs = arrayOf( + 1.toByte() to byteArrayOf(0x01), + 0.toByte() to byteArrayOf(0x00), + Byte.MAX_VALUE to byteArrayOf(Byte.MAX_VALUE), + Byte.MIN_VALUE to byteArrayOf(Byte.MIN_VALUE) + ) + private val shortTestPairs = arrayOf( + 1.toShort() to byteArrayOf(0x00, 0x01), + 0.toShort() to byteArrayOf(0x00, 0x00), + Byte.MAX_VALUE.toShort() to byteArrayOf(0x00, Byte.MAX_VALUE), + 256.toShort() to byteArrayOf(0x01, 0x00), + (-128).toShort() to byteArrayOf(0xff.toByte(), 0x80.toByte()), + Short.MAX_VALUE to byteArrayOf(0x7f, 0xff.toByte()), + Short.MIN_VALUE to byteArrayOf(0x80.toByte(), 0x00), + ) + private val intTestPairs = arrayOf( + 65536 to byteArrayOf(0x00, 0x01, 0x00, 0x00), + -65000 to byteArrayOf(0xff.toByte(), 0xff.toByte(), 0x02, 0x18), + -32769 to byteArrayOf(0xff.toByte(), 0xff.toByte(), 0x7f, 0xff.toByte()), + Int.MAX_VALUE to byteArrayOf(0x7f, 0xff.toByte(), 0xff.toByte(), 0xff.toByte()), + Int.MIN_VALUE to byteArrayOf(0x80.toByte(), 0x00, 0x00, 0x00) + ) + private val longTestPairs = arrayOf( + 4294967296 to byteArrayOf(0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00), + 5000000000 to byteArrayOf(0x00, 0x00, 0x00, 0x01, 0x2a, 0x05, 0xf2.toByte(), 0x00), + 9223372036854775807 to byteArrayOf( + 0x7f, 0xff.toByte(), 0xff.toByte(), + 0xff.toByte(), 0xff.toByte(), 0xff.toByte(), 0xff.toByte(), 0xff.toByte() + ), + -2147483649 to byteArrayOf( + 0xff.toByte(), 0xff.toByte(), 0xff.toByte(), 0xff.toByte(), + 0x7f.toByte(), 0xff.toByte(), 0xff.toByte(), 0xff.toByte() + ), + -5000000000 to byteArrayOf( + 0xff.toByte(), 0xff.toByte(), 0xff.toByte(), 0xfe.toByte(), + 0xd5.toByte(), 0xfa.toByte(), 0x0e.toByte(), 0x00.toByte() + ) + ) + + @Test + fun testSplitToByteArrayByte() { + testSplitToByteArrayPairs(byteTestPairs) + } + + @Test + fun testJoinToNumberByte() { + testJoinToNumberPairs(byteTestPairs) + } + + @Test + fun testSplitToByteArrayShort() { + testSplitToByteArrayPairs(shortTestPairs) + } + + @Test + fun testJoinToNumberShort() { + testJoinToNumberPairs(shortTestPairs) + } + + @Test + fun testSplitToByteArrayInt() { + testSplitToByteArrayPairs(intTestPairs) + } + + @Test + fun testJoinToNumberInt() { + testJoinToNumberPairs(intTestPairs) + } + + @Test + fun testSplitToByteArrayLong() { + testSplitToByteArrayPairs(longTestPairs) + } + + @Test + fun testJoinToNumberLong() { + testJoinToNumberPairs(longTestPairs) + } + + private inline fun testSplitToByteArrayPairs(pairs: Array>) { + pairs.forEach { (number, expected) -> + val result = number.splitToByteArray() + assertContentEquals(expected, result) + } + } + + private inline fun testJoinToNumberPairs(pairs: Array>) { + pairs.forEach { (expected, array) -> + val result = array.joinToNumber() + assertEquals(expected, result) + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index f65f607..f5a15d5 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1,5 @@ rootProject.name = "kotlinx-serialization-msgpack" include(":serialization-msgpack") + +include(":serialization-msgpack-timestamp-extension")