bipack: auto-detect unsigned inline types and add @Varint field codec

This commit is contained in:
sergeych committed 2026-03-25 02:49:52 +03:00
1 parent b8ac3e20e0
commit a9f07ddb6f
8 files changed
+101 -18

No files matched your search

@@ -2,8 +2,8 @@
package net.sergeych.bintools
/**
* Variable-length long integer encoding. the MSB (0x80) bit of each byte flags
* that it is not the last one, and all ncecssary bits are encoded with 7-bit
* Variable-length long integer encoding. The MSB (0x80) bit of each byte flags
* that it is not the last one, and all necessary bits are encoded with 7-bit
* portions, LSB to MSB (big endian of sorts).
*
* There is slower but more compact encoding variant, [Smartint] that is better when
@@ -25,6 +25,7 @@ class BipackDecoder(
private var elementIndex = 0
private var nextIsUnsigned = false
private var nextIsVarint = false
private var fixedSize = -1
private var fixedNumber = false
@@ -33,6 +34,9 @@ class BipackDecoder(
override fun decodeByte(): Byte = input.readByte()
override fun decodeShort(): Short =
if (fixedNumber) input.readI16()
else if (nextIsVarint)
if (nextIsUnsigned) input.readVarUInt().toShort()
else input.readVarInt().toShort()
else if (nextIsUnsigned)
input.readNumber<UInt>().toShort()
else
@@ -40,10 +44,16 @@ class BipackDecoder(
override fun decodeInt(): Int =
if (fixedNumber) input.readI32()
else if (nextIsVarint)
if (nextIsUnsigned) input.readVarUInt().toInt()
else input.readVarInt()
else if (nextIsUnsigned) input.readNumber<UInt>().toInt() else input.readNumber()
override fun decodeLong(): Long =
if (fixedNumber) input.readI64()
else if (nextIsVarint)
if (nextIsUnsigned) net.sergeych.bintools.Varint.decodeUnsigned(input).toLong()
else net.sergeych.bintools.Varint.decodeSigned(input)
else if (nextIsUnsigned) input.readNumber<ULong>().toLong() else input.readNumber()
override fun decodeFloat(): Float = input.readFloat()
@@ -63,9 +73,11 @@ class BipackDecoder(
if (elementIndex >= elementsCount)
return CompositeDecoder.DECODE_DONE
nextIsUnsigned = false
nextIsVarint = false
for (a in descriptor.getElementAnnotations(elementIndex)) {
when (a) {
is Unsigned -> nextIsUnsigned = true
is Varint -> nextIsVarint = true
is FixedSize -> fixedSize = a.size
is Fixed -> fixedNumber = true
}
@@ -73,6 +85,12 @@ class BipackDecoder(
return elementIndex++
}
override fun decodeInline(descriptor: SerialDescriptor): BipackDecoder {
if (descriptor.isUnsignedInlinePrimitive())
nextIsUnsigned = true
return this
}
override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>): T {
return if (deserializer == serializer<Instant>())
Instant.fromEpochMilliseconds(decodeLong()) as T
@@ -136,6 +154,9 @@ class BipackDecoder(
@ExperimentalSerializationApi
override fun decodeNull(): Nothing? = null
private fun SerialDescriptor.isUnsignedInlinePrimitive(): Boolean =
isInline && serialName in setOf("kotlin.UInt", "kotlin.ULong", "kotlin.UShort", "kotlin.UByte")
companion object {
fun <T> decode(source: DataSource, deserializer: DeserializationStrategy<T>): T =
BipackDecoder(source).decodeSerializableValue(deserializer)
@@ -156,4 +177,3 @@ class BipackDecoder(
inline fun <reified T> ByteArray.decodeFromBipack() = BipackDecoder.decode<T>(this)
@Suppress("unused")
inline fun <reified T> UByteArray.decodeFromBipack() = BipackDecoder.decode<T>(this)
@@ -13,26 +13,38 @@ import kotlin.time.Instant
class BipackEncoder(val output: DataSink) : AbstractEncoder() {
private var nextIsUnsigned = false
private var nextIsVarint = false
private var fixedSize: Int = -1
private var fixedNumber: Boolean = false
override fun encodeElement(descriptor: SerialDescriptor, index: Int): Boolean =
super.encodeElement(descriptor, index).also {
nextIsUnsigned = false
nextIsVarint = false
for (a in descriptor.getElementAnnotations(index)) {
when (a) {
is Unsigned -> nextIsUnsigned = true
is Varint -> nextIsVarint = true
is FixedSize -> fixedSize = a.size
is Fixed -> fixedNumber = true
}
}
}
override fun encodeInline(descriptor: SerialDescriptor): BipackEncoder {
if (descriptor.isUnsignedInlinePrimitive())
nextIsUnsigned = true
return this
}
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) =
if (fixedNumber) output.writeI16(value)
else if (nextIsVarint)
if (nextIsUnsigned) output.writeVarUInt(value.toUShort().toUInt())
else output.writeVarInt(value.toInt())
else if (nextIsUnsigned)
output.writeNumber(value.toUShort())
else
@@ -41,6 +53,9 @@ class BipackEncoder(val output: DataSink) : AbstractEncoder() {
override fun encodeInt(value: Int) =
if (fixedNumber)
output.writeI32(value)
else if (nextIsVarint)
if (nextIsUnsigned) output.writeVarUInt(value.toUInt())
else output.writeVarInt(value)
else if (nextIsUnsigned) output.writeNumber(value.toUInt())
else output.writeNumber(value)
@@ -48,6 +63,9 @@ class BipackEncoder(val output: DataSink) : AbstractEncoder() {
override fun encodeLong(value: Long) =
if (fixedNumber)
output.writeI64(value)
else if (nextIsVarint)
if (nextIsUnsigned) net.sergeych.bintools.Varint.encodeUnsigned(value.toULong(), output)
else net.sergeych.bintools.Varint.encodeSigned(value, output)
else if (nextIsUnsigned)
output.writeNumber(value.toULong())
else
@@ -111,6 +129,9 @@ class BipackEncoder(val output: DataSink) : AbstractEncoder() {
override fun encodeNull() = encodeBoolean(false)
override fun encodeNotNullMark() = encodeBoolean(true)
private fun SerialDescriptor.isUnsignedInlinePrimitive(): Boolean =
isInline && serialName in setOf("kotlin.UInt", "kotlin.ULong", "kotlin.UShort", "kotlin.UByte")
companion object {
fun <T> encode(serializer: SerializationStrategy<T>, value: T, sink: DataSink) {
val encoder = BipackEncoder(sink)
@@ -43,14 +43,25 @@ annotation class Framed
annotation class CrcProtected
/**
* Allow marking data fields as being serialized as unsigned (applicable also to signed fields lite Int, Long and
* Short, if you are sure they will not be negative). As unsigned types are not cully supported by `kotlinx.serialization`
* it is the only way to tell the serialized to use more compact unsigned variable length encoding.
* Marks signed integer fields (`Int`, `Long`, `Short`) that should be encoded using unsigned variable-length format.
*
* Native unsigned Kotlin types (`UInt`, `ULong`, `UShort`, `UByte`) are handled automatically by modern
* `kotlinx.serialization` and do not require this annotation.
*/
@SerialInfo
@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY)
annotation class Unsigned
/**
* Marks integer fields that should use [net.sergeych.bintools.Varint] codec instead of default [net.sergeych.bintools.Smartint].
*
* Applicable to all integer types (`Byte`, `Short`, `Int`, `Long`, `UByte`, `UShort`, `UInt`, `ULong`).
* If used together with [Unsigned] on signed types, unsigned varint encoding is used.
*/
@SerialInfo
@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY)
annotation class Varint
/**
* Fixed size collection of a given size. __Use it only with collections!__
*
@@ -102,5 +113,3 @@ class InvalidFrameHeaderException(reason: String = "Frame header does not match"
class InvalidFrameCRCException : InvalidFrameException("Checksum CRC32 failed")
class WrongCollectionSize(reason: String) : Exception(reason)