Skip to content

CBOR unsigned number encoding is wrong #3143

@JesusMcCloud

Description

@JesusMcCloud

Describe the bug

Direct byte encoding/decoding of Kotlin unsigned primitives is currently two’s-complement via signed primitive, which round-trips within the library, but doesn’t match the CBOR representation. CBOR has native signed and native unsigned Integer types and those should be used

Context:
While working on structured CBOR and pondering extensions/shorthands for numbers other than Long I noticed that unsigned primitives must not take the detour through signed types, because CBOR represents signed and unsigned types with a different header/mask. This is perfectly fine for the newly introducedCborInteger, because it stores a numbers signum and magnitude separately.

Problem:
When rebasing onto dev I noticed range checks got introduced to the CBOR decoder and it clicked:

Kotlin’s built-in serializers for UByte/UShort/UInt/ULong are inline wrappers over the signed primitives (Byte/Short/Int/Long) and literally call encodeByte(value.toByte()), decodeByte().toUByte(), etc. (core/commonMain/src/kotlinx/serialization/internal/ValueClasses.kt).

  • CBOR’s “unsigned integer” is major type 0 (0..2⁶⁴-1). So for example UByte(200) “should” be encoded as CBOR unsigned integer 200.
  • With the range checks (pulled from dev) in CborReader.decodeByte/decodeShort/decodeInt, decoding a CBOR unsigned integer 200 into a Byte now throws (since 200 is not within [-128..127]). That means UByte decoding via the standard serializer also fails for values > 127 even though an unsigned CBOR Int up to 255 would be within range.
  • Encoding is also flawed: UByte(200).toByte() is -56, so the default encodeByte path emits a CBOR negative integer, not an unsigned 200.

To Reproduce
See Testcase that always fails.

Expected behavior
Unsigned Kotlin number types should always be encoded as Major Type 0

Environment

  • Kotlin version: any
  • Library version: any
  • Kotlin platforms: all
  • Gradle version: any
  • Other relevant context: Was always there and the range checks made it even worse, which helped discover the issue

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions