support for serialized frames

This commit is contained in:
Sergey Chernov 2023-03-12 23:22:21 +01:00
parent 63b231975b
commit be03163a96
11 changed files with 274 additions and 14 deletions

View 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

View 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
}
}
}
}

View 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
}
}
}
}

View 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
}
}
}
}

View File

@ -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))

View File

@ -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))

View File

@ -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"

View File

@ -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 =

View File

@ -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) {
if (a is Framed) {
output.writeU32(
CRC.crc32(descriptor.serialName.encodeToByteArray())
)
}
else if( a is ExtendableFormat) {
encodeUInt(descriptor.elementsCount.toUInt()) encodeUInt(descriptor.elementsCount.toUInt())
}
}
return super.beginStructure(descriptor) return super.beginStructure(descriptor)
} }

View 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")

View File

@ -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)
} }
} }