diff --git a/src/commonMain/kotlin/net.sergeych.bintools/CRC32Sink.kt b/src/commonMain/kotlin/net.sergeych.bintools/CRC32Sink.kt new file mode 100644 index 0000000..a696878 --- /dev/null +++ b/src/commonMain/kotlin/net.sergeych.bintools/CRC32Sink.kt @@ -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 +} \ No newline at end of file diff --git a/src/commonMain/kotlin/net.sergeych.bintools/CRC32Source.kt b/src/commonMain/kotlin/net.sergeych.bintools/CRC32Source.kt new file mode 100644 index 0000000..9633f87 --- /dev/null +++ b/src/commonMain/kotlin/net.sergeych.bintools/CRC32Source.kt @@ -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 +} diff --git a/src/commonMain/kotlin/net.sergeych.bintools/DataSource.kt b/src/commonMain/kotlin/net.sergeych.bintools/DataSource.kt index 2c2ea5b..2e71d19 100644 --- a/src/commonMain/kotlin/net.sergeych.bintools/DataSource.kt +++ b/src/commonMain/kotlin/net.sergeych.bintools/DataSource.kt @@ -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 diff --git a/src/commonMain/kotlin/net.sergeych.bipack/BipackDecoder.kt b/src/commonMain/kotlin/net.sergeych.bipack/BipackDecoder.kt index ba1eaab..e0e9887 100644 --- a/src/commonMain/kotlin/net.sergeych.bipack/BipackDecoder.kt +++ b/src/commonMain/kotlin/net.sergeych.bipack/BipackDecoder.kt @@ -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().toInt() - else if( a is Framed ) { + for (a in descriptor.annotations) { + if (a is ExtendableFormat) + count = source.readNumber().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 = diff --git a/src/commonMain/kotlin/net.sergeych.bipack/BipackEncoder.kt b/src/commonMain/kotlin/net.sergeych.bipack/BipackEncoder.kt index 2dd1b2b..3e4fe17 100644 --- a/src/commonMain/kotlin/net.sergeych.bipack/BipackEncoder.kt +++ b/src/commonMain/kotlin/net.sergeych.bipack/BipackEncoder.kt @@ -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) diff --git a/src/commonMain/kotlin/net.sergeych.bipack/Framed.kt b/src/commonMain/kotlin/net.sergeych.bipack/Framed.kt deleted file mode 100644 index 43a9525..0000000 --- a/src/commonMain/kotlin/net.sergeych.bipack/Framed.kt +++ /dev/null @@ -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") diff --git a/src/commonMain/kotlin/net.sergeych.bipack/Extendable.kt b/src/commonMain/kotlin/net.sergeych.bipack/annotations.kt similarity index 50% rename from src/commonMain/kotlin/net.sergeych.bipack/Extendable.kt rename to src/commonMain/kotlin/net.sergeych.bipack/annotations.kt index 24d99d5..9dd1ea0 100644 --- a/src/commonMain/kotlin/net.sergeych.bipack/Extendable.kt +++ b/src/commonMain/kotlin/net.sergeych.bipack/annotations.kt @@ -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") + diff --git a/src/commonTest/kotlin/bipack/BipackEncoderTest.kt b/src/commonTest/kotlin/bipack/BipackEncoderTest.kt index 9387e1c..e87696a 100644 --- a/src/commonTest/kotlin/bipack/BipackEncoderTest.kt +++ b/src/commonTest/kotlin/bipack/BipackEncoderTest.kt @@ -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(BipackEncoder.encode(a)) assertEquals(a, b) } + + @Test + fun encodeProtectedFrame() { + val a = FoobarFP1(42, 117)//, "bum") + println(BipackEncoder.encode(a).toDump()) + val b = BipackDecoder.decode(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(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()) + } } \ No newline at end of file