From 0e8f3daf994a4f7714564e1c69d1095177c727de Mon Sep 17 00:00:00 2001 From: sergeych Date: Mon, 13 Mar 2023 16:27:06 +0100 Subject: [PATCH] support for @Unsigned fields and fox bad error of kotlinjs `x is Float` glitch: it returns true for integers --- .../kotlin/net.sergeych.bintools/DataSink.kt | 6 +-- .../net.sergeych.bintools/DataSource.kt | 3 +- .../net.sergeych.bipack/BipackDecoder.kt | 9 ++-- .../net.sergeych.bipack/BipackEncoder.kt | 54 +++++++++++++------ .../kotlin/net.sergeych.bipack/annotations.kt | 18 +++++++ .../kotlin/bintools/SmartintTest.kt | 1 + .../kotlin/bipack/BipackEncoderTest.kt | 42 +++++++++++++-- 7 files changed, 103 insertions(+), 30 deletions(-) diff --git a/src/commonMain/kotlin/net.sergeych.bintools/DataSink.kt b/src/commonMain/kotlin/net.sergeych.bintools/DataSink.kt index 6db314f..0ed3dea 100644 --- a/src/commonMain/kotlin/net.sergeych.bintools/DataSink.kt +++ b/src/commonMain/kotlin/net.sergeych.bintools/DataSink.kt @@ -25,11 +25,7 @@ interface DataSink { } inline fun DataSink.writeNumber(value: T) { - when(value) { - is Float -> writeFloat(value) - is Double -> writeDouble(value) - else -> Smartint.encode(value, this) - } + Smartint.encode(value, this) } fun DataSink.writeI32(value: Int) { diff --git a/src/commonMain/kotlin/net.sergeych.bintools/DataSource.kt b/src/commonMain/kotlin/net.sergeych.bintools/DataSource.kt index 2e71d19..fd8fde4 100644 --- a/src/commonMain/kotlin/net.sergeych.bintools/DataSource.kt +++ b/src/commonMain/kotlin/net.sergeych.bintools/DataSource.kt @@ -13,7 +13,7 @@ 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 @@ -51,3 +51,4 @@ inline fun DataSource.readNumber(): T = when(typeOf()) { typeOf() -> readFloat() as T else -> Smartint.decode(this) } + diff --git a/src/commonMain/kotlin/net.sergeych.bipack/BipackDecoder.kt b/src/commonMain/kotlin/net.sergeych.bipack/BipackDecoder.kt index e0e9887..6bf28b0 100644 --- a/src/commonMain/kotlin/net.sergeych.bipack/BipackDecoder.kt +++ b/src/commonMain/kotlin/net.sergeych.bipack/BipackDecoder.kt @@ -17,12 +17,14 @@ import net.sergeych.bintools.* class BipackDecoder(val input: DataSource, var elementsCount: Int = 0) : AbstractDecoder() { private var elementIndex = 0 + private var nextIsUnsigned = false + override val serializersModule: SerializersModule = EmptySerializersModule override fun decodeBoolean(): Boolean = input.readByte().toInt() != 0 override fun decodeByte(): Byte = input.readByte() - override fun decodeShort(): Short = input.readNumber() - override fun decodeInt(): Int = input.readNumber() - override fun decodeLong(): Long = input.readNumber() + override fun decodeShort(): Short = if (nextIsUnsigned) input.readNumber().toShort() else input.readNumber() + override fun decodeInt(): Int = if (nextIsUnsigned) input.readNumber().toInt() else input.readNumber() + override fun decodeLong(): Long = if (nextIsUnsigned) input.readNumber().toLong() else input.readNumber() override fun decodeFloat(): Float = input.readFloat() override fun decodeDouble(): Double = input.readDouble() override fun decodeChar(): Char = Char(input.readNumber().toInt()) @@ -37,6 +39,7 @@ class BipackDecoder(val input: DataSource, var elementsCount: Int = 0) : Abstrac override fun decodeElementIndex(descriptor: SerialDescriptor): Int { if (elementIndex >= elementsCount) return CompositeDecoder.DECODE_DONE + nextIsUnsigned = descriptor.getElementAnnotations(elementIndex).any { it is Unsigned } return elementIndex++ } diff --git a/src/commonMain/kotlin/net.sergeych.bipack/BipackEncoder.kt b/src/commonMain/kotlin/net.sergeych.bipack/BipackEncoder.kt index 3e4fe17..e61db38 100644 --- a/src/commonMain/kotlin/net.sergeych.bipack/BipackEncoder.kt +++ b/src/commonMain/kotlin/net.sergeych.bipack/BipackEncoder.kt @@ -14,26 +14,49 @@ class BipackEncoder(var output: DataSink) : AbstractEncoder() { // used when CRC calculation on the fly private var crcSink: CRC32Sink? = null + private var nextIsUnsigned = false + + override fun encodeElement(descriptor: SerialDescriptor, index: Int): Boolean { + return super.encodeElement(descriptor, index).also { + println(">> $index: ${descriptor.getElementAnnotations(index)}") + nextIsUnsigned = descriptor.getElementAnnotations(index).any { it is Unsigned } + println("${descriptor.getElementDescriptor(index)} -> $nextIsUnsigned") + } + } +// fun isUnsigned(): Boolean { +// +// return +// } + + 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()) - override fun encodeShort(value: Short) = output.writeNumber(value.toInt()) - override fun encodeInt(value: Int) = output.writeNumber(value) + override fun encodeShort(value: Short) = if( nextIsUnsigned ) + output.writeNumber(value.toUInt()) + else + output.writeNumber(value.toInt()) + override fun encodeInt(value: Int) { + println("EncodeInt: $value / $nextIsUnsigned") + if (nextIsUnsigned) + output.writeNumber(value.toUInt()) + else + output.writeNumber(value) + } fun encodeUInt(value: UInt) = output.writeNumber(value) - override fun encodeLong(value: Long) = output.writeNumber(value) - override fun encodeFloat(value: Float) = output.writeNumber(value) - override fun encodeDouble(value: Double) = output.writeI64(value.toRawBits()) + override fun encodeLong(value: Long) = if( nextIsUnsigned ) + output.writeNumber(value.toULong()) + else + output.writeNumber(value) + override fun encodeFloat(value: Float) = output.writeFloat(value) + override fun encodeDouble(value: Double) = output.writeDouble(value) override fun encodeChar(value: Char) = output.writeNumber(value.code.toUInt()) override fun encodeString(value: String) { // output.writeUTF(value) 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) @@ -48,19 +71,18 @@ class BipackEncoder(var output: DataSink) : AbstractEncoder() { override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder { // frame protection should start before anything else: - if( descriptor.annotations.any { it is CrcProtected }) { + 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) { output.writeU32( CRC.crc32(descriptor.serialName.encodeToByteArray()) ) - } - else if( a is ExtendableFormat) { + } else if (a is ExtendableFormat) { encodeUInt(descriptor.elementsCount.toUInt()) } } @@ -80,16 +102,16 @@ class BipackEncoder(var output: DataSink) : AbstractEncoder() { override fun encodeNotNullMark() = encodeBoolean(true) companion object { - fun encode(serializer: SerializationStrategy, value: T,sink: DataSink) { + fun encode(serializer: SerializationStrategy, value: T, sink: DataSink) { val encoder = BipackEncoder(sink) encoder.encodeSerializableValue(serializer, value) } fun encode(serializer: SerializationStrategy, value: T): ByteArray = - ArrayDataSink().also { encode(serializer, value, it)}.toByteArray() + ArrayDataSink().also { encode(serializer, value, it) }.toByteArray() inline fun encode(value: T) = encode(serializer(), value) - inline fun encode(value: T,sink: DataSink) = encode(serializer(), value, sink) + inline fun encode(value: T, sink: DataSink) = encode(serializer(), value, sink) } } diff --git a/src/commonMain/kotlin/net.sergeych.bipack/annotations.kt b/src/commonMain/kotlin/net.sergeych.bipack/annotations.kt index 5eabdc3..aa872be 100644 --- a/src/commonMain/kotlin/net.sergeych.bipack/annotations.kt +++ b/src/commonMain/kotlin/net.sergeych.bipack/annotations.kt @@ -26,9 +26,27 @@ annotation class ExtendableFormat @SerialInfo annotation class Framed +/** + * Allow to CRC-protect structures (we suppose to use it with classes only). After the + * data block its CRC32 will be written and checked. It is memory-wise: it calculates CRC + * on the fly without buffering the data. If used with [Framed] and [ExtendableFormat] the extra + * data is protected too. + * + * __Common pitfalls__. When unpacking corrupted data protected this way, the not only [InvalidFrameCRCException] + * can be thrown. Actually, most often you will see [DataSource.EndOfData] exception + */ @SerialInfo annotation class CrcProtected +/** + * Allow marking data fields as being serialized as usnsigned (applyable also to signed fields lite Int, Long and + * Short, if you are shure they will not be negative). As unsigned types are not cully supported by kotlinx.serialization + * it is the conly way to tell the serialized to use more compact unsigned variable length encoding. + */ +@SerialInfo +@Target(AnnotationTarget.FIELD,AnnotationTarget.PROPERTY) +annotation class Unsigned + 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/bintools/SmartintTest.kt b/src/commonTest/kotlin/bintools/SmartintTest.kt index febb8cd..33a52d2 100644 --- a/src/commonTest/kotlin/bintools/SmartintTest.kt +++ b/src/commonTest/kotlin/bintools/SmartintTest.kt @@ -64,4 +64,5 @@ class SmartintTest { // } } } + } \ No newline at end of file diff --git a/src/commonTest/kotlin/bipack/BipackEncoderTest.kt b/src/commonTest/kotlin/bipack/BipackEncoderTest.kt index e87696a..091d107 100644 --- a/src/commonTest/kotlin/bipack/BipackEncoderTest.kt +++ b/src/commonTest/kotlin/bipack/BipackEncoderTest.kt @@ -40,6 +40,17 @@ data class FoobarFP1(val bar: Int, val foo: Int,val other: Int = -1) class BipackEncoderTest { + @Serializable + data class FoobarSize(val i: Int) + + @Test + fun testSize() { + println(BipackEncoder.encode(17).toDump()) + assertEquals(1, BipackEncoder.encode(17).size) +// assertEquals(1, BipackEncoder.encode(FoobarSize(17)).size) + } + + @Test fun encodeSimple() { val a = Foobar1(42)//, "bum") @@ -87,13 +98,34 @@ class BipackEncoderTest { } @Serializable - data class FBU(val u: UInt, val i: Int) + data class FBU( + @Unsigned + val u: UInt, + @Unsigned + val i: Int, + val k: UInt = 3u) @Test fun testByteArray() { +// val z = Foobar1(42)//, "bum") +// println(BipackEncoder.encode(z).toDump()) + 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()) + var d = BipackEncoder.encode(x) + println(d.toDump()) + assertEquals(0x0c, d[0]) + + d = BipackEncoder.encode("123") + println(d.toDump()) + assertEquals(0x0c, d[0]) + + val f = FBU(3u, 3) + d = BipackEncoder.encode(f) + println(d.toDump()) + assertEquals(0x0c, d[0]) + assertEquals(0x0c, d[1]) + // Signed encoding despite UInt type: + assertEquals(0x18, d[2]) + + assertEquals(f, BipackDecoder.decode(d)) } } \ No newline at end of file