support for serialized frames
This commit is contained in:
parent
63b231975b
commit
be03163a96
51
src/commonMain/kotlin/net.sergeych.bintools/CRC.kt
Normal file
51
src/commonMain/kotlin/net.sergeych.bintools/CRC.kt
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
@file:OptIn(ExperimentalUnsignedTypes::class)
|
||||||
|
|
||||||
|
package net.sergeych.bintools
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common interfacte for all CRC variants. See [crc16], [crc32] and [crc8] for useful shortcuts.
|
||||||
|
*/
|
||||||
|
interface CRC<T> {
|
||||||
|
val lookupTable: List<T>
|
||||||
|
val value: T
|
||||||
|
|
||||||
|
fun update(inputs: UByteArray)
|
||||||
|
fun reset()
|
||||||
|
|
||||||
|
fun update(input: UByte) {
|
||||||
|
update(ubyteArrayOf(input))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun update(inputs: ByteArray) {
|
||||||
|
update(inputs.toUByteArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Calculate crc8 for a data array using a given polynomial (uses CRC8-Bluetooth's polynomial by default)
|
||||||
|
*/
|
||||||
|
fun crc8(data: ByteArray, polynomial: UByte = 0xA7.toUByte()): UByte =
|
||||||
|
CRC8(polynomial).also { it.update(data) }.value
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate CRC16 for a data array using a given polynomial (CRC16-CCITT polynomial (0x1021) by default)
|
||||||
|
*/
|
||||||
|
fun crc16(data: ByteArray, polynomial: UShort = 0x1021.toUShort()): UShort =
|
||||||
|
CRC16(polynomial).also { it.update(data) }.value
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate crc32 for a given data and polynomial (using CRC32 polynomial by default)
|
||||||
|
*/
|
||||||
|
fun crc32(data: ByteArray, polynomial: UInt = 0x04C11DB7.toUInt()): UInt =
|
||||||
|
CRC32(polynomial).also { it.update(data) }.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
infix fun UShort.shl(bitCount: Int): UShort = (this.toUInt() shl bitCount).toUShort()
|
||||||
|
infix fun UShort.shr(bitCount: Int): UShort = (this.toUInt() shr bitCount).toUShort()
|
||||||
|
|
||||||
|
infix fun UByte.shl(bitCount: Int): UByte = (this.toUInt() shl bitCount).toUByte()
|
||||||
|
infix fun UByte.shr(bitCount: Int): UByte = (this.toUInt() shr bitCount).toUByte()
|
||||||
|
|
||||||
|
fun UByte.toBigEndianUShort(): UShort = this.toUShort() shl 8
|
||||||
|
fun UByte.toBigEndianUInt(): UInt = this.toUInt() shl 24
|
43
src/commonMain/kotlin/net.sergeych.bintools/CRC16.kt
Normal file
43
src/commonMain/kotlin/net.sergeych.bintools/CRC16.kt
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
@file:OptIn(ExperimentalUnsignedTypes::class)
|
||||||
|
|
||||||
|
package net.sergeych.bintools
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to conveniently calculate CRC-16. It uses the CRC16-CCITT polynomial (0x1021) by default
|
||||||
|
*/
|
||||||
|
class CRC16(val polynomial: UShort = 0x1021.toUShort()) : CRC<UShort> {
|
||||||
|
override val lookupTable: List<UShort> = (0 until 256).map { crc16(it.toUByte(), polynomial) }
|
||||||
|
|
||||||
|
override var value: UShort = 0.toUShort()
|
||||||
|
private set
|
||||||
|
|
||||||
|
override fun update(inputs: UByteArray) {
|
||||||
|
value = crc16(inputs, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun reset() {
|
||||||
|
value = 0.toUShort()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun crc16(inputs: UByteArray, initialValue: UShort = 0.toUShort()): UShort {
|
||||||
|
return inputs.fold(initialValue) { remainder, byte ->
|
||||||
|
val bigEndianInput = byte.toBigEndianUShort()
|
||||||
|
val index = (bigEndianInput xor remainder) shr 8
|
||||||
|
lookupTable[index.toInt()] xor (remainder shl 8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun crc16(input: UByte, polynomial: UShort): UShort {
|
||||||
|
val bigEndianInput = input.toBigEndianUShort()
|
||||||
|
|
||||||
|
return (0 until 8).fold(bigEndianInput) { result, _ ->
|
||||||
|
val isMostSignificantBitOne = result and 0x8000.toUShort() != 0.toUShort()
|
||||||
|
val shiftedResult = result shl 1
|
||||||
|
|
||||||
|
when (isMostSignificantBitOne) {
|
||||||
|
true -> shiftedResult xor polynomial
|
||||||
|
false -> shiftedResult
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
43
src/commonMain/kotlin/net.sergeych.bintools/CRC32.kt
Normal file
43
src/commonMain/kotlin/net.sergeych.bintools/CRC32.kt
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
@file:OptIn(ExperimentalUnsignedTypes::class)
|
||||||
|
|
||||||
|
package net.sergeych.bintools
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to conveniently calculate CRC-32 using CRC32 polynomial (0x04C11DB7) by default
|
||||||
|
*/
|
||||||
|
class CRC32(val polynomial: UInt = 0x04C11DB7.toUInt()) : CRC<UInt> {
|
||||||
|
override val lookupTable: List<UInt> = (0 until 256).map { crc32(it.toUByte(), polynomial) }
|
||||||
|
|
||||||
|
override var value: UInt = 0.toUInt()
|
||||||
|
private set
|
||||||
|
|
||||||
|
override fun update(inputs: UByteArray) {
|
||||||
|
value = crc32(inputs, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun reset() {
|
||||||
|
value = 0.toUInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun crc32(inputs: UByteArray, initialValue: UInt = 0.toUInt()): UInt {
|
||||||
|
return inputs.fold(initialValue) { remainder, byte ->
|
||||||
|
val bigEndianInput = byte.toBigEndianUInt()
|
||||||
|
val index = (bigEndianInput xor remainder) shr 24
|
||||||
|
lookupTable[index.toInt()] xor (remainder shl 8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun crc32(input: UByte, polynomial: UInt): UInt {
|
||||||
|
val bigEndianInput = input.toBigEndianUInt()
|
||||||
|
|
||||||
|
return (0 until 8).fold(bigEndianInput) { result, _ ->
|
||||||
|
val isMostSignificantBitOne = result and 0x80000000.toUInt() != 0.toUInt()
|
||||||
|
val shiftedResult = result shl 1
|
||||||
|
|
||||||
|
when (isMostSignificantBitOne) {
|
||||||
|
true -> shiftedResult xor polynomial
|
||||||
|
false -> shiftedResult
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
35
src/commonMain/kotlin/net.sergeych.bintools/CRC8.kt
Normal file
35
src/commonMain/kotlin/net.sergeych.bintools/CRC8.kt
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package net.sergeych.bintools
|
||||||
|
|
||||||
|
class CRC8(val polynomial: UByte = 0xA7.toUByte()) : CRC<UByte> {
|
||||||
|
override val lookupTable: List<UByte> = (0 until 256).map { crc8(it.toUByte(), polynomial) }
|
||||||
|
|
||||||
|
override var value: UByte = 0.toUByte()
|
||||||
|
private set
|
||||||
|
|
||||||
|
override fun update(inputs: UByteArray) {
|
||||||
|
value = crc8(inputs, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun reset() {
|
||||||
|
value = 0.toUByte()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun crc8(inputs: UByteArray, initialValue: UByte = 0.toUByte()): UByte {
|
||||||
|
return inputs.fold(initialValue) { remainder, byte ->
|
||||||
|
val index = byte xor remainder
|
||||||
|
lookupTable[index.toInt()]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun crc8(input: UByte, polynomial: UByte): UByte {
|
||||||
|
return (0 until 8).fold(input) { result, _ ->
|
||||||
|
val isMostSignificantBitOne = result and 0x80.toUByte() != 0.toUByte()
|
||||||
|
val shiftedResult = result shl 1
|
||||||
|
|
||||||
|
when (isMostSignificantBitOne) {
|
||||||
|
true -> shiftedResult xor polynomial
|
||||||
|
false -> shiftedResult
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -35,6 +35,9 @@ inline fun <reified T:Any>DataSink.writeNumber(value: T) {
|
|||||||
fun DataSink.writeI32(value: Int) {
|
fun DataSink.writeI32(value: Int) {
|
||||||
writeBytes(intToBytes(value))
|
writeBytes(intToBytes(value))
|
||||||
}
|
}
|
||||||
|
fun DataSink.writeU32(value: UInt) {
|
||||||
|
writeBytes(uintToBytes(value))
|
||||||
|
}
|
||||||
|
|
||||||
fun DataSink.writeI64(value: Long) {
|
fun DataSink.writeI64(value: Long) {
|
||||||
writeBytes(longToBytes(value))
|
writeBytes(longToBytes(value))
|
||||||
|
@ -24,6 +24,7 @@ interface DataSource {
|
|||||||
a[i] = readByte()
|
a[i] = readByte()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun readU32(): UInt = bytesToUInt(readBytes(4))
|
||||||
fun readI32(): Int = bytesToInt(readBytes(4))
|
fun readI32(): Int = bytesToInt(readBytes(4))
|
||||||
fun readI64(): Long = bytesToLong(readBytes(8))
|
fun readI64(): Long = bytesToLong(readBytes(8))
|
||||||
|
|
||||||
|
@ -21,6 +21,16 @@ fun intToBytes(value: Int): ByteArray {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun uintToBytes(value: UInt): ByteArray {
|
||||||
|
var l = value
|
||||||
|
val result = ByteArray(4)
|
||||||
|
for (i in 3 downTo 0) {
|
||||||
|
result[i] = (l and 0xFFu).toByte()
|
||||||
|
l = l shr 8
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert 8 bytes to LE long
|
* Convert 8 bytes to LE long
|
||||||
*/
|
*/
|
||||||
@ -42,6 +52,15 @@ fun bytesToInt(b: ByteArray): Int {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun bytesToUInt(b: ByteArray): UInt {
|
||||||
|
var result = 0u
|
||||||
|
for (i in 0 until 4) {
|
||||||
|
result = result shl 8
|
||||||
|
result = result or (b[i].toUInt() and 0xFFu)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private val hexDigits = "0123456789ABCDEF"
|
private val hexDigits = "0123456789ABCDEF"
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ import kotlinx.serialization.encoding.CompositeDecoder
|
|||||||
import kotlinx.serialization.modules.EmptySerializersModule
|
import kotlinx.serialization.modules.EmptySerializersModule
|
||||||
import kotlinx.serialization.modules.SerializersModule
|
import kotlinx.serialization.modules.SerializersModule
|
||||||
import kotlinx.serialization.serializer
|
import kotlinx.serialization.serializer
|
||||||
|
import net.sergeych.bintools.CRC
|
||||||
import net.sergeych.bintools.DataSource
|
import net.sergeych.bintools.DataSource
|
||||||
import net.sergeych.bintools.readNumber
|
import net.sergeych.bintools.readNumber
|
||||||
import net.sergeych.bintools.toDataSource
|
import net.sergeych.bintools.toDataSource
|
||||||
@ -39,12 +40,18 @@ class BipackDecoder(val input: DataSource, var elementsCount: Int = 0) : Abstrac
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder {
|
override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder {
|
||||||
return BipackDecoder(
|
var count = descriptor.elementsCount
|
||||||
input,
|
for( a in descriptor.annotations ) {
|
||||||
if (descriptor.annotations.any { it is ExtendableFormat })
|
if( a is ExtendableFormat )
|
||||||
input.readNumber<UInt>().toInt()
|
count = input.readNumber<UInt>().toInt()
|
||||||
else descriptor.elementsCount
|
else if( a is Framed ) {
|
||||||
)
|
val code = CRC.crc32(descriptor.serialName.encodeToByteArray())
|
||||||
|
val actual = input.readU32()
|
||||||
|
if( code != actual )
|
||||||
|
throw InvalidFrameException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return BipackDecoder(input, count)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun decodeCollectionSize(descriptor: SerialDescriptor): Int =
|
override fun decodeCollectionSize(descriptor: SerialDescriptor): Int =
|
||||||
|
@ -7,10 +7,7 @@ import kotlinx.serialization.encoding.CompositeEncoder
|
|||||||
import kotlinx.serialization.modules.EmptySerializersModule
|
import kotlinx.serialization.modules.EmptySerializersModule
|
||||||
import kotlinx.serialization.modules.SerializersModule
|
import kotlinx.serialization.modules.SerializersModule
|
||||||
import kotlinx.serialization.serializer
|
import kotlinx.serialization.serializer
|
||||||
import net.sergeych.bintools.ArrayDataSink
|
import net.sergeych.bintools.*
|
||||||
import net.sergeych.bintools.DataSink
|
|
||||||
import net.sergeych.bintools.writeI64
|
|
||||||
import net.sergeych.bintools.writeNumber
|
|
||||||
|
|
||||||
class BipackEncoder(val output: DataSink) : AbstractEncoder() {
|
class BipackEncoder(val output: DataSink) : AbstractEncoder() {
|
||||||
override val serializersModule: SerializersModule = EmptySerializersModule
|
override val serializersModule: SerializersModule = EmptySerializersModule
|
||||||
@ -42,8 +39,16 @@ class BipackEncoder(val output: DataSink) : AbstractEncoder() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder {
|
override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder {
|
||||||
if( descriptor.annotations.any { it is ExtendableFormat } )
|
for( a in descriptor.annotations) {
|
||||||
encodeUInt(descriptor.elementsCount.toUInt())
|
if (a is Framed) {
|
||||||
|
output.writeU32(
|
||||||
|
CRC.crc32(descriptor.serialName.encodeToByteArray())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else if( a is ExtendableFormat) {
|
||||||
|
encodeUInt(descriptor.elementsCount.toUInt())
|
||||||
|
}
|
||||||
|
}
|
||||||
return super.beginStructure(descriptor)
|
return super.beginStructure(descriptor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
14
src/commonMain/kotlin/net.sergeych.bipack/Framed.kt
Normal file
14
src/commonMain/kotlin/net.sergeych.bipack/Framed.kt
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package net.sergeych.bipack
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialInfo
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serializable classes annotated as Framed will have leading checked mark as CRC32
|
||||||
|
* of its name (uses `@SerialName` internally). On deserializing, if frame will not
|
||||||
|
* match the expected name, the [InvalidFrameException] will be thrown.
|
||||||
|
*/
|
||||||
|
@SerialInfo
|
||||||
|
@Target(AnnotationTarget.CLASS)
|
||||||
|
annotation class Framed
|
||||||
|
|
||||||
|
class InvalidFrameException : Exception("invalid CRC32 serialization frame value")
|
@ -1,17 +1,37 @@
|
|||||||
package net.sergeych.bipack
|
package net.sergeych.bipack
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import net.sergeych.bintools.toDump
|
import net.sergeych.bintools.toDump
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Assertions.assertThrows
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Foobar1N(val bar: Int, val foo: Int = 117)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@ExtendableFormat
|
@ExtendableFormat
|
||||||
data class Foobar1(val bar: Int, val foo: Int = 117)
|
data class Foobar1(val bar: Int, val foo: Int = 117)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@ExtendableFormat
|
@ExtendableFormat
|
||||||
|
@SerialName("net.sergeych.bipack.Foobar1")
|
||||||
data class Foobar2(val bar: Int, val foo: Int,val other: Int = -1)
|
data class Foobar2(val bar: Int, val foo: Int,val other: Int = -1)
|
||||||
|
@Serializable
|
||||||
|
@Framed
|
||||||
|
@ExtendableFormat
|
||||||
|
data class FoobarF1(val bar: Int, val foo: Int = 117)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@Framed
|
||||||
|
@ExtendableFormat
|
||||||
|
@SerialName("net.sergeych.bipack.FoobarF1")
|
||||||
|
data class FoobarF2(val bar: Int, val foo: Int,val other: Int = -1)
|
||||||
|
@Serializable
|
||||||
|
@Framed
|
||||||
|
@ExtendableFormat
|
||||||
|
data class FoobarF3(val bar: Int, val foo: Int,val other: Int = -1)
|
||||||
|
|
||||||
class BipackEncoderTest {
|
class BipackEncoderTest {
|
||||||
|
|
||||||
@ -23,8 +43,27 @@ class BipackEncoderTest {
|
|||||||
assertEquals(a, b)
|
assertEquals(a, b)
|
||||||
val c = BipackDecoder.decode<Foobar2>(BipackEncoder.encode(a))
|
val c = BipackDecoder.decode<Foobar2>(BipackEncoder.encode(a))
|
||||||
assertEquals(-1, c.other)
|
assertEquals(-1, c.other)
|
||||||
// assertEquals(a.foo, c.foo)
|
|
||||||
assertEquals(a.bar, c.bar)
|
assertEquals(a.bar, c.bar)
|
||||||
// assertEquals(a.buzz, c.buzz)
|
}
|
||||||
|
@Test
|
||||||
|
fun encodeFramed() {
|
||||||
|
val a = FoobarF1(42)//, "bum")
|
||||||
|
println(BipackEncoder.encode(a).toDump())
|
||||||
|
val b = BipackDecoder.decode<FoobarF1>(BipackEncoder.encode(a))
|
||||||
|
assertEquals(a, b)
|
||||||
|
val c = BipackDecoder.decode<FoobarF2>(BipackEncoder.encode(a))
|
||||||
|
assertEquals(-1, c.other)
|
||||||
|
assertEquals(a.bar, c.bar)
|
||||||
|
assertThrows(InvalidFrameException::class.java) {
|
||||||
|
BipackDecoder.decode<FoobarF3>(BipackEncoder.encode(a))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun encodeNonExtendable() {
|
||||||
|
val a = Foobar1N(42)//, "bum")
|
||||||
|
println(BipackEncoder.encode(a).toDump())
|
||||||
|
val b = BipackDecoder.decode<Foobar1N>(BipackEncoder.encode(a))
|
||||||
|
assertEquals(a, b)
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user