CRC-protected frames

This commit is contained in:
Sergey Chernov 2023-03-13 15:28:08 +01:00
parent ee2c958445
commit 8c73ec9e30
8 changed files with 155 additions and 27 deletions

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

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

View File

@ -10,6 +10,9 @@ import kotlin.reflect.typeOf
*/
interface DataSource {
/**
* Exception that implementations must throw on end of data
*/
class EndOfData() : Exception("no more data available")
fun readByte(): Byte

View File

@ -8,11 +8,12 @@ import kotlinx.serialization.encoding.CompositeDecoder
import kotlinx.serialization.modules.EmptySerializersModule
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.serializer
import net.sergeych.bintools.CRC
import net.sergeych.bintools.DataSource
import net.sergeych.bintools.readNumber
import net.sergeych.bintools.toDataSource
import net.sergeych.bintools.*
/**
* 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() {
private var elementIndex = 0
@ -40,18 +41,37 @@ class BipackDecoder(val input: DataSource, var elementsCount: Int = 0) : Abstrac
}
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
for( a in descriptor.annotations ) {
if( a is ExtendableFormat )
count = input.readNumber<UInt>().toInt()
else if( a is Framed ) {
for (a in descriptor.annotations) {
if (a is ExtendableFormat)
count = source.readNumber<UInt>().toInt()
else if (a is Framed) {
val code = CRC.crc32(descriptor.serialName.encodeToByteArray())
val actual = input.readU32()
if( code != actual )
throw InvalidFrameException()
// if we fail to read CRC, it is IO error, so DataSource.EndOfData will be
// thrown here, and it is better than invalid frame exception:
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 =

View File

@ -9,7 +9,11 @@ import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.serializer
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 fun encodeBoolean(value: Boolean) = output.writeByte(if (value) 1 else 0)
override fun encodeByte(value: Byte) = output.writeByte(value.toInt())
@ -26,6 +30,10 @@ class BipackEncoder(val output: DataSink) : AbstractEncoder() {
writeBytes(value.encodeToByteArray())
}
override fun encodeElement(descriptor: SerialDescriptor, index: Int): Boolean {
return super.encodeElement(descriptor, index)
}
fun writeBytes(value: ByteArray) {
output.writeNumber(value.size.toUInt())
output.writeBytes(value)
@ -39,6 +47,13 @@ class BipackEncoder(val output: DataSink) : AbstractEncoder() {
}
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) {
if (a is Framed) {
output.writeU32(
@ -52,6 +67,15 @@ class BipackEncoder(val output: DataSink) : AbstractEncoder() {
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 encodeNotNullMark() = encodeBoolean(true)

View File

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

View File

@ -16,3 +16,22 @@ import kotlinx.serialization.SerialInfo
@Target(AnnotationTarget.CLASS)
@SerialInfo
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")

View File

@ -4,6 +4,7 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import net.sergeych.bintools.toDump
import net.sergeych.bipack.*
import kotlin.experimental.xor
import kotlin.test.*
@Serializable
@ -31,6 +32,12 @@ data class FoobarF2(val bar: Int, val foo: Int,val other: Int = -1)
@ExtendableFormat
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 {
@Test
@ -64,4 +71,29 @@ class BipackEncoderTest {
val b = BipackDecoder.decode<Foobar1N>(BipackEncoder.encode(a))
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())
}
}