CRC-protected frames
This commit is contained in:
parent
ee2c958445
commit
8c73ec9e30
26
src/commonMain/kotlin/net.sergeych.bintools/CRC32Sink.kt
Normal file
26
src/commonMain/kotlin/net.sergeych.bintools/CRC32Sink.kt
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package net.sergeych.bintools
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The filter-type sink that calculates CRC32 on the fly passing data to
|
||||||
|
* another sink
|
||||||
|
*/
|
||||||
|
class CRC32Sink(val sink: DataSink,
|
||||||
|
polynomial:UInt = 0x04C11DB7U): DataSink {
|
||||||
|
|
||||||
|
private val checksum = CRC32(polynomial)
|
||||||
|
override fun writeByte(data: Byte) {
|
||||||
|
checksum.update(data.toUByte())
|
||||||
|
sink.writeByte(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun writeBytes(data: ByteArray) {
|
||||||
|
checksum.update(data.toUByteArray())
|
||||||
|
sink.writeBytes(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* current value of the CRC32 checkcum, using the bytes that
|
||||||
|
* were written by now.
|
||||||
|
*/
|
||||||
|
val crc: UInt get() = checksum.value
|
||||||
|
}
|
18
src/commonMain/kotlin/net.sergeych.bintools/CRC32Source.kt
Normal file
18
src/commonMain/kotlin/net.sergeych.bintools/CRC32Source.kt
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package net.sergeych.bintools
|
||||||
|
|
||||||
|
class CRC32Source(
|
||||||
|
val source: DataSource,
|
||||||
|
polynomial: UInt = 0x04C11DB7U
|
||||||
|
): DataSource {
|
||||||
|
|
||||||
|
private val checksum = CRC32(polynomial)
|
||||||
|
override fun readByte(): Byte = source.readByte().also {
|
||||||
|
checksum.update(it.toUByte())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun readBytes(size: Int): ByteArray = source.readBytes(size).also {
|
||||||
|
checksum.update(it.toUByteArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
val crc: UInt get() = checksum.value
|
||||||
|
}
|
@ -10,6 +10,9 @@ import kotlin.reflect.typeOf
|
|||||||
*/
|
*/
|
||||||
interface DataSource {
|
interface DataSource {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception that implementations must throw on end of data
|
||||||
|
*/
|
||||||
class EndOfData() : Exception("no more data available")
|
class EndOfData() : Exception("no more data available")
|
||||||
|
|
||||||
fun readByte(): Byte
|
fun readByte(): Byte
|
||||||
|
@ -8,11 +8,12 @@ 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.*
|
||||||
import net.sergeych.bintools.DataSource
|
|
||||||
import net.sergeych.bintools.readNumber
|
|
||||||
import net.sergeych.bintools.toDataSource
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode BiPack format. Note that it relies on [DataSource] so can throw [DataSource.EndOfData]
|
||||||
|
* excpetion. Specific frames when used can throw [InvalidFrameException] and its derivatives.e
|
||||||
|
*/
|
||||||
class BipackDecoder(val input: DataSource, var elementsCount: Int = 0) : AbstractDecoder() {
|
class BipackDecoder(val input: DataSource, var elementsCount: Int = 0) : AbstractDecoder() {
|
||||||
private var elementIndex = 0
|
private var elementIndex = 0
|
||||||
|
|
||||||
@ -40,18 +41,37 @@ class BipackDecoder(val input: DataSource, var elementsCount: Int = 0) : Abstrac
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder {
|
override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder {
|
||||||
|
var source = if (descriptor.annotations.any { it is CrcProtected })
|
||||||
|
CRC32Source(input)
|
||||||
|
else
|
||||||
|
input
|
||||||
|
|
||||||
|
// Note: we should read from 'source' explicitely as it might ve
|
||||||
|
// CRC-calculating one, and the fields below are CRC protected too:
|
||||||
var count = descriptor.elementsCount
|
var count = descriptor.elementsCount
|
||||||
for( a in descriptor.annotations ) {
|
for (a in descriptor.annotations) {
|
||||||
if( a is ExtendableFormat )
|
if (a is ExtendableFormat)
|
||||||
count = input.readNumber<UInt>().toInt()
|
count = source.readNumber<UInt>().toInt()
|
||||||
else if( a is Framed ) {
|
else if (a is Framed) {
|
||||||
val code = CRC.crc32(descriptor.serialName.encodeToByteArray())
|
val code = CRC.crc32(descriptor.serialName.encodeToByteArray())
|
||||||
val actual = input.readU32()
|
// if we fail to read CRC, it is IO error, so DataSource.EndOfData will be
|
||||||
if( code != actual )
|
// thrown here, and it is better than invalid frame exception:
|
||||||
throw InvalidFrameException()
|
val actual = source.readU32()
|
||||||
|
if (code != actual)
|
||||||
|
throw InvalidFrameHeaderException()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return BipackDecoder(input, count)
|
return BipackDecoder(source, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun endStructure(descriptor: SerialDescriptor) {
|
||||||
|
if (input is CRC32Source) {
|
||||||
|
val actual = input.crc
|
||||||
|
val expected = input.readU32()
|
||||||
|
if (actual != expected)
|
||||||
|
throw InvalidFrameCRCException()
|
||||||
|
}
|
||||||
|
super.endStructure(descriptor)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun decodeCollectionSize(descriptor: SerialDescriptor): Int =
|
override fun decodeCollectionSize(descriptor: SerialDescriptor): Int =
|
||||||
|
@ -9,7 +9,11 @@ import kotlinx.serialization.modules.SerializersModule
|
|||||||
import kotlinx.serialization.serializer
|
import kotlinx.serialization.serializer
|
||||||
import net.sergeych.bintools.*
|
import net.sergeych.bintools.*
|
||||||
|
|
||||||
class BipackEncoder(val output: DataSink) : AbstractEncoder() {
|
class BipackEncoder(var output: DataSink) : AbstractEncoder() {
|
||||||
|
|
||||||
|
// used when CRC calculation on the fly
|
||||||
|
private var crcSink: CRC32Sink? = null
|
||||||
|
|
||||||
override val serializersModule: SerializersModule = EmptySerializersModule
|
override val serializersModule: SerializersModule = EmptySerializersModule
|
||||||
override fun encodeBoolean(value: Boolean) = output.writeByte(if (value) 1 else 0)
|
override fun encodeBoolean(value: Boolean) = output.writeByte(if (value) 1 else 0)
|
||||||
override fun encodeByte(value: Byte) = output.writeByte(value.toInt())
|
override fun encodeByte(value: Byte) = output.writeByte(value.toInt())
|
||||||
@ -26,6 +30,10 @@ class BipackEncoder(val output: DataSink) : AbstractEncoder() {
|
|||||||
writeBytes(value.encodeToByteArray())
|
writeBytes(value.encodeToByteArray())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun encodeElement(descriptor: SerialDescriptor, index: Int): Boolean {
|
||||||
|
return super.encodeElement(descriptor, index)
|
||||||
|
}
|
||||||
|
|
||||||
fun writeBytes(value: ByteArray) {
|
fun writeBytes(value: ByteArray) {
|
||||||
output.writeNumber(value.size.toUInt())
|
output.writeNumber(value.size.toUInt())
|
||||||
output.writeBytes(value)
|
output.writeBytes(value)
|
||||||
@ -39,6 +47,13 @@ class BipackEncoder(val output: DataSink) : AbstractEncoder() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder {
|
override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder {
|
||||||
|
// frame protection should start before anything else:
|
||||||
|
if( descriptor.annotations.any { it is CrcProtected }) {
|
||||||
|
crcSink = CRC32Sink(output).also {
|
||||||
|
output = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// now it is safe to process anything else
|
||||||
for( a in descriptor.annotations) {
|
for( a in descriptor.annotations) {
|
||||||
if (a is Framed) {
|
if (a is Framed) {
|
||||||
output.writeU32(
|
output.writeU32(
|
||||||
@ -52,6 +67,15 @@ class BipackEncoder(val output: DataSink) : AbstractEncoder() {
|
|||||||
return super.beginStructure(descriptor)
|
return super.beginStructure(descriptor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun endStructure(descriptor: SerialDescriptor) {
|
||||||
|
crcSink?.let {
|
||||||
|
output = it.sink
|
||||||
|
crcSink = null
|
||||||
|
output.writeU32(it.crc)
|
||||||
|
}
|
||||||
|
super.endStructure(descriptor)
|
||||||
|
}
|
||||||
|
|
||||||
override fun encodeNull() = encodeBoolean(false)
|
override fun encodeNull() = encodeBoolean(false)
|
||||||
override fun encodeNotNullMark() = encodeBoolean(true)
|
override fun encodeNotNullMark() = encodeBoolean(true)
|
||||||
|
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
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")
|
|
@ -16,3 +16,22 @@ import kotlinx.serialization.SerialInfo
|
|||||||
@Target(AnnotationTarget.CLASS)
|
@Target(AnnotationTarget.CLASS)
|
||||||
@SerialInfo
|
@SerialInfo
|
||||||
annotation class ExtendableFormat
|
annotation class ExtendableFormat
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
|
||||||
|
@SerialInfo
|
||||||
|
@Target(AnnotationTarget.CLASS)
|
||||||
|
annotation class CrcProtected
|
||||||
|
|
||||||
|
open class InvalidFrameException(reason: String) : Exception(reason)
|
||||||
|
class InvalidFrameHeaderException(reason: String = "Frame header does not match") : InvalidFrameException(reason)
|
||||||
|
class InvalidFrameCRCException : InvalidFrameException("Checksum CRC32 failed")
|
||||||
|
|
@ -4,6 +4,7 @@ import kotlinx.serialization.SerialName
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import net.sergeych.bintools.toDump
|
import net.sergeych.bintools.toDump
|
||||||
import net.sergeych.bipack.*
|
import net.sergeych.bipack.*
|
||||||
|
import kotlin.experimental.xor
|
||||||
import kotlin.test.*
|
import kotlin.test.*
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@ -31,6 +32,12 @@ data class FoobarF2(val bar: Int, val foo: Int,val other: Int = -1)
|
|||||||
@ExtendableFormat
|
@ExtendableFormat
|
||||||
data class FoobarF3(val bar: Int, val foo: Int,val other: Int = -1)
|
data class FoobarF3(val bar: Int, val foo: Int,val other: Int = -1)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@Framed
|
||||||
|
@ExtendableFormat
|
||||||
|
@CrcProtected()
|
||||||
|
data class FoobarFP1(val bar: Int, val foo: Int,val other: Int = -1)
|
||||||
|
|
||||||
class BipackEncoderTest {
|
class BipackEncoderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -64,4 +71,29 @@ class BipackEncoderTest {
|
|||||||
val b = BipackDecoder.decode<Foobar1N>(BipackEncoder.encode(a))
|
val b = BipackDecoder.decode<Foobar1N>(BipackEncoder.encode(a))
|
||||||
assertEquals(a, b)
|
assertEquals(a, b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun encodeProtectedFrame() {
|
||||||
|
val a = FoobarFP1(42, 117)//, "bum")
|
||||||
|
println(BipackEncoder.encode(a).toDump())
|
||||||
|
val b = BipackDecoder.decode<FoobarFP1>(BipackEncoder.encode(a))
|
||||||
|
assertEquals(a, b)
|
||||||
|
val bad = BipackEncoder.encode(a)
|
||||||
|
bad[6] = bad[6] xor 0x20
|
||||||
|
println(bad.toDump())
|
||||||
|
assertFailsWith(InvalidFrameCRCException::class) {
|
||||||
|
BipackDecoder.decode<FoobarFP1>(bad)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class FBU(val u: UInt, val i: Int)
|
||||||
|
@Test
|
||||||
|
fun testByteArray() {
|
||||||
|
val x = byteArrayOf(1,2,3)
|
||||||
|
println(BipackEncoder.encode(x).toDump())
|
||||||
|
println(BipackEncoder.encode("123").toDump())
|
||||||
|
println(BipackEncoder.encode(FBU(3u, 3)).toDump())
|
||||||
|
// println(BipackEncoder.encode(1U).toDump())
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user