diff --git a/protobuf_serialization/codec.nim b/protobuf_serialization/codec.nim index 19670fd..d39b157 100644 --- a/protobuf_serialization/codec.nim +++ b/protobuf_serialization/codec.nim @@ -65,6 +65,7 @@ type fixed32 | sfixed32 | pfloat # Mappings of proto type to wire type + # https://protobuf.dev/programming-guides/encoding/#cheat-sheet SomeVarint* = pint32 | pint64 | puint32 | puint64 | sint32 | sint64 | pbool | penum SomeFixed64* = fixed64 | sfixed64 | pdouble @@ -246,6 +247,7 @@ proc skipValue*[T: SomeFixed32 | SomeFixed64](input: InputStream, _: type T) {.r static: doAssert sizeof(T) in {4, 8} input.skipBytes(sizeof(T)) +# https://protobuf.dev/programming-guides/encoding/#cheat-sheet # https://protobuf.dev/programming-guides/encoding/#length-types # https://protobuf.dev/programming-guides/proto-limits/#total proc readLength*(input: InputStream): int {.raises: [SerializationError, IOError].} = diff --git a/tests/conformance/conformance_nim.nim b/tests/conformance/conformance_nim.nim index 1e9cde3..fd227eb 100644 --- a/tests/conformance/conformance_nim.nim +++ b/tests/conformance/conformance_nim.nim @@ -1,3 +1,12 @@ +# nim-protobuf-serialization +# Copyright (c) 2026 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + import os import ../../protobuf_serialization import ../../protobuf_serialization/files/type_generator diff --git a/tests/test_all.nim b/tests/test_all.nim index fa3cac3..45ae0dd 100644 --- a/tests/test_all.nim +++ b/tests/test_all.nim @@ -1,8 +1,18 @@ +# nim-protobuf-serialization +# Copyright (c) 2026 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + {.warning[UnusedImport]: off} import ../protobuf_serialization import + test_all_types, test_bool, test_codec, test_fixed, @@ -12,5 +22,6 @@ import test_repeated, test_protobuf2_semantics, test_thirty_three_fields, + test_truncation, test_wire_type_mismatch, files/test_proto3 diff --git a/tests/test_all_types.nim b/tests/test_all_types.nim new file mode 100644 index 0000000..608d921 --- /dev/null +++ b/tests/test_all_types.nim @@ -0,0 +1,165 @@ +import unittest2 + +import ./utils, ../protobuf_serialization + +type + StringObj {.proto3.} = object + x {.fieldNumber: 1.}: string + + BytesObj {.proto3.} = object + x {.fieldNumber: 1.}: seq[byte] + + PInt32Obj {.proto3.} = object + x {.fieldNumber: 1, pint.}: int32 + + PUInt32Obj {.proto3.} = object + x {.fieldNumber: 1, pint.}: uint32 + + PInt64Obj {.proto3.} = object + x {.fieldNumber: 1, pint.}: int64 + + PUInt64Obj {.proto3.} = object + x {.fieldNumber: 1, pint.}: uint64 + + SInt32Obj {.proto3.} = object + x {.fieldNumber: 1, sint.}: int32 + + SInt64Obj {.proto3.} = object + x {.fieldNumber: 1, sint.}: int64 + + FixedInt32Obj {.proto3.} = object + x {.fieldNumber: 1, fixed.}: int32 + + FixedInt64Obj {.proto3.} = object + x {.fieldNumber: 1, fixed.}: int64 + + FixedUInt32Obj {.proto3.} = object + x {.fieldNumber: 1, fixed.}: uint32 + + FixedUInt64Obj {.proto3.} = object + x {.fieldNumber: 1, fixed.}: uint64 + + Float32Obj {.proto3.} = object + x {.fieldNumber: 1.}: float32 + + Float64Obj {.proto3.} = object + x {.fieldNumber: 1.}: float64 + +suite "Test String Encoding/Decoding": + test "Non-empty string values": + # echo "0a0161" | xxd -r -p | protoc --decode=StringObj test_all_types.proto + # echo 'x: "a"' | protoc --encode=StringObj test_all_types.proto | hexdump -ve '1/1 "%.2x"' + roundtrip(StringObj(x: "a"), "0a0161") + roundtrip(StringObj(x: "hi"), "0a026869") + roundtrip(StringObj(x: "abc"), "0a03616263") + roundtrip(StringObj(x: "ü"), "0a02c3bc") + roundtrip(StringObj(x: "水"), "0a03e6b0b4") + roundtrip(StringObj(x: "𐅑"), "0a04f0908591") + + test "Empty string is default": + check Protobuf.encode(StringObj()).len == 0 + +suite "Test Bytes Encoding/Decoding": + test "Non-empty byte sequences": + # echo "0a0101" | xxd -r -p | protoc --decode=BytesObj test_all_types.proto + # echo 'x: "\001"' | protoc --encode=BytesObj test_all_types.proto | hexdump -ve '1/1 "%.2x"' + roundtrip(BytesObj(x: @[0x01'u8]), "0a0101") + roundtrip(BytesObj(x: @[0x01'u8, 0x02, 0x03]), "0a03010203") + roundtrip(BytesObj(x: @[0xFF'u8, 0x00, 0xFF]), "0a03ff00ff") + + test "Empty bytes is default": + check Protobuf.encode(BytesObj()).len == 0 + +suite "Test pint Encoding/Decoding": + test "pint int32 single-byte varint": + # echo "0801" | xxd -r -p | protoc --decode=PInt32Obj test_all_types.proto + # echo 'x: 1' | protoc --encode=PInt32Obj test_all_types.proto | hexdump -ve '1/1 "%.2x"' + roundtrip(PInt32Obj(x: 1'i32), "0801") + roundtrip(PInt32Obj(x: 127'i32), "087f") + + test "pint int32 multi-byte varint": + # echo "088001" | xxd -r -p | protoc --decode=PInt32Obj test_all_types.proto + # echo 'x: 128' | protoc --encode=PInt32Obj test_all_types.proto | hexdump -ve '1/1 "%.2x"' + roundtrip(PInt32Obj(x: 128'i32), "088001") + roundtrip(PInt32Obj(x: 300'i32), "08ac02") + roundtrip(PInt32Obj(x: int32.high), "08ffffffff07") + + test "pint uint32 max value": + # echo "08ffffffff0f" | xxd -r -p | protoc --decode=PUInt32Obj test_all_types.proto + # echo 'x: 4294967295' | protoc --encode=PUInt32Obj test_all_types.proto | hexdump -ve '1/1 "%.2x"' + roundtrip(PUInt32Obj(x: uint32.high), "08ffffffff0f") + + test "pint int64 max value": + # echo "08ffffffffffffffff7f" | xxd -r -p | protoc --decode=PInt64Obj test_all_types.proto + # echo 'x: 9223372036854775807' | protoc --encode=PInt64Obj test_all_types.proto | hexdump -ve '1/1 "%.2x"' + roundtrip(PInt64Obj(x: int64.high), "08ffffffffffffffff7f") + + test "pint uint64 max value": + # echo "08ffffffffffffffffff01" | xxd -r -p | protoc --decode=PUInt64Obj test_all_types.proto + # echo 'x: 18446744073709551615' | protoc --encode=PUInt64Obj test_all_types.proto | hexdump -ve '1/1 "%.2x"' + roundtrip(PUInt64Obj(x: uint64.high), "08ffffffffffffffffff01") + +suite "Test sint Encoding/Decoding": + test "sint int32 positive values": + # echo "0802" | xxd -r -p | protoc --decode=SInt32Obj test_all_types.proto + # echo 'x: 1' | protoc --encode=SInt32Obj test_all_types.proto | hexdump -ve '1/1 "%.2x"' + roundtrip(SInt32Obj(x: 1'i32), "0802") + roundtrip(SInt32Obj(x: 63'i32), "087e") + + test "sint int32 negative values": + # echo "0801" | xxd -r -p | protoc --decode=SInt32Obj test_all_types.proto + # echo 'x: -1' | protoc --encode=SInt32Obj test_all_types.proto | hexdump -ve '1/1 "%.2x"' + roundtrip(SInt32Obj(x: -1'i32), "0801") + roundtrip(SInt32Obj(x: -2'i32), "0803") + + test "sint int32 boundary values": + # echo "08feffffff0f" | xxd -r -p | protoc --decode=SInt32Obj test_all_types.proto + # echo 'x: 2147483647' | protoc --encode=SInt32Obj test_all_types.proto | hexdump -ve '1/1 "%.2x"' + roundtrip(SInt32Obj(x: int32.high), "08feffffff0f") + roundtrip(SInt32Obj(x: int32.low), "08ffffffff0f") + + test "sint int64 negative values": + # echo "0801" | xxd -r -p | protoc --decode=SInt64Obj test_all_types.proto + # echo 'x: -1' | protoc --encode=SInt64Obj test_all_types.proto | hexdump -ve '1/1 "%.2x"' + roundtrip(SInt64Obj(x: -1'i64), "0801") + roundtrip(SInt64Obj(x: int64.low), "08ffffffffffffffffff01") + +suite "Test Fixed-Width Encoding/Decoding": + test "fixed int32 (sfixed32)": + # echo "0d01000000" | xxd -r -p | protoc --decode=FixedInt32Obj test_all_types.proto + # echo 'x: 1' | protoc --encode=FixedInt32Obj test_all_types.proto | hexdump -ve '1/1 "%.2x"' + roundtrip(FixedInt32Obj(x: 1'i32), "0d01000000") + roundtrip(FixedInt32Obj(x: -1'i32), "0dffffffff") + roundtrip(FixedInt32Obj(x: int32.high), "0dffffff7f") + roundtrip(FixedInt32Obj(x: int32.low), "0d00000080") + + test "fixed int64 (sfixed64)": + # echo "090100000000000000" | xxd -r -p | protoc --decode=FixedInt64Obj test_all_types.proto + # echo 'x: 1' | protoc --encode=FixedInt64Obj test_all_types.proto | hexdump -ve '1/1 "%.2x"' + roundtrip(FixedInt64Obj(x: 1'i64), "090100000000000000") + roundtrip(FixedInt64Obj(x: -1'i64), "09ffffffffffffffff") + + test "fixed uint32 (fixed32)": + # echo "0d01000000" | xxd -r -p | protoc --decode=FixedUInt32Obj test_all_types.proto + # echo 'x: 1' | protoc --encode=FixedUInt32Obj test_all_types.proto | hexdump -ve '1/1 "%.2x"' + roundtrip(FixedUInt32Obj(x: 1'u32), "0d01000000") + roundtrip(FixedUInt32Obj(x: uint32.high), "0dffffffff") + + test "fixed uint64 (fixed64)": + # echo "090100000000000000" | xxd -r -p | protoc --decode=FixedUInt64Obj test_all_types.proto + # echo 'x: 1' | protoc --encode=FixedUInt64Obj test_all_types.proto | hexdump -ve '1/1 "%.2x"' + roundtrip(FixedUInt64Obj(x: 1'u64), "090100000000000000") + roundtrip(FixedUInt64Obj(x: uint64.high), "09ffffffffffffffff") + +suite "Test Float Encoding/Decoding": + test "float32 values": + # echo "0d0000803f" | xxd -r -p | protoc --decode=Float32Obj test_all_types.proto + # echo 'x: 1' | protoc --encode=Float32Obj test_all_types.proto | hexdump -ve '1/1 "%.2x"' + roundtrip(Float32Obj(x: 1.0'f32), "0d0000803f") + roundtrip(Float32Obj(x: -1.0'f32), "0d000080bf") + + test "float64 values": + # echo "09000000000000f03f" | xxd -r -p | protoc --decode=Float64Obj test_all_types.proto + # echo 'x: 1' | protoc --encode=Float64Obj test_all_types.proto | hexdump -ve '1/1 "%.2x"' + roundtrip(Float64Obj(x: 1.0'f64), "09000000000000f03f") + roundtrip(Float64Obj(x: -1.0'f64), "09000000000000f0bf") diff --git a/tests/test_all_types.proto b/tests/test_all_types.proto new file mode 100644 index 0000000..1988b8c --- /dev/null +++ b/tests/test_all_types.proto @@ -0,0 +1,57 @@ +syntax = "proto3"; + +message StringObj { + string x = 1; +} + +message BytesObj { + bytes x = 1; +} + +message PInt32Obj { + int32 x = 1; +} + +message PUInt32Obj { + uint32 x = 1; +} + +message PInt64Obj { + int64 x = 1; +} + +message PUInt64Obj { + uint64 x = 1; +} + +message SInt32Obj { + sint32 x = 1; +} + +message SInt64Obj { + sint64 x = 1; +} + +message FixedInt32Obj { + sfixed32 x = 1; +} + +message FixedInt64Obj { + sfixed64 x = 1; +} + +message FixedUInt32Obj { + fixed32 x = 1; +} + +message FixedUInt64Obj { + fixed64 x = 1; +} + +message Float32Obj { + float x = 1; +} + +message Float64Obj { + double x = 1; +} diff --git a/tests/test_bool.nim b/tests/test_bool.nim index 27ffc51..c1c6354 100644 --- a/tests/test_bool.nim +++ b/tests/test_bool.nim @@ -1,6 +1,15 @@ +# nim-protobuf-serialization +# Copyright (c) 2026 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + import unittest2 -import ../protobuf_serialization +import ./utils, ../protobuf_serialization type PIntType {.proto3.} = object @@ -15,28 +24,33 @@ type BoolType {.proto3.} = object x {.fieldNumber: 1.}: bool -proc writeRead[W, R](toWrite: W, value: R) = - let encoded = Protobuf.encode(toWrite) - check: - encoded.len == Protobuf.computeSize(toWrite) - Protobuf.decode(encoded, R) == value - suite "Test Boolean Encoding/Decoding": test "Can encode/decode boolean without subtype specification": - writeRead(BoolType(x: true), BoolType(x: true)) - writeRead(BoolType(x: false), BoolType(x: false)) + # echo "0801" | xxd -r -p | protoc --decode=BoolType test_bool.proto + # x: true + # echo "x: true" | protoc --encode=BoolType test_bool.proto | hexdump -ve '1/1 "%.2x"' + # 0801 + roundtrip(BoolType(x: true), BoolType(x: true), "0801") + roundtrip(BoolType(x: false), BoolType(x: false), "") #Skipping subtype specification only works when every encoding has the same truthiness. #That's what this tests. It should be noted 1 encodes as 1/1/2 for the following. test "Can encode/decode boolean as signed VarInt": - writeRead(PIntType(x: 1), BoolType(x: true)) - writeRead(PIntType(x: 0), BoolType(x: false)) + # echo "x: 1" | protoc --encode=PIntType test_bool.proto | hexdump -ve '1/1 "%.2x"' + # 0801 + roundtrip(PIntType(x: 1), BoolType(x: true), "0801") + roundtrip(PIntType(x: 0), BoolType(x: false), "") test "Can encode/decode boolean as unsigned VarInt": - writeRead(UIntType(x: 1), BoolType(x: true)) - writeRead(UIntType(x: 0), BoolType(x: false)) + # echo "x: 1" | protoc --encode=UIntType test_bool.proto | hexdump -ve '1/1 "%.2x"' + # 0801 + roundtrip(UIntType(x: 1), BoolType(x: true), "0801") + roundtrip(UIntType(x: 0), BoolType(x: false), "") test "Can encode/decode boolean as zig-zagged VarInt": - # TODO 1 encodes as 2 in zig-zah - should we truncate? see `readVarint` - writeRead(SIntType(x: 1), BoolType(x: true)) - writeRead(SIntType(x: 0), BoolType(x: false)) + # echo "x: 1" | protoc --encode=SIntType test_bool.proto | hexdump -ve '1/1 "%.2x"' + # 0802 + # echo "0802" | xxd -r -p | protoc --decode=BoolType test_bool.proto + # x: true + roundtrip(SIntType(x: 1), BoolType(x: true), "0802") + roundtrip(SIntType(x: 0), BoolType(x: false), "") diff --git a/tests/test_bool.proto b/tests/test_bool.proto new file mode 100644 index 0000000..8ef1a49 --- /dev/null +++ b/tests/test_bool.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +message PIntType { + int32 x = 1; +} + +message UIntType { + uint32 x = 1; +} + +message SIntType { + sint32 x = 1; +} + +message BoolType { + bool x = 1; +} diff --git a/tests/test_codec.nim b/tests/test_codec.nim index 71e0cb0..4814f4e 100644 --- a/tests/test_codec.nim +++ b/tests/test_codec.nim @@ -1,5 +1,5 @@ -# Nim-Libp2p -# Copyright (c) 2022 Status Research & Development GmbH +# nim-protobuf-serialization +# Copyright (c) 2026 Status Research & Development GmbH # Licensed under either of # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) # * MIT license ([LICENSE-MIT](LICENSE-MIT)) diff --git a/tests/test_empty.nim b/tests/test_empty.nim index 1380b80..87ce66a 100644 --- a/tests/test_empty.nim +++ b/tests/test_empty.nim @@ -1,19 +1,41 @@ +# nim-protobuf-serialization +# Copyright (c) 2026 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + import unittest2 import ../protobuf_serialization type - X {.proto3.} = object - Y {.proto3.} = object - a {.fieldNumber: 1, pint.}: int32 - Z {.proto3.} = object - b {.fieldNumber: 1.}: string + Empty {.proto3.} = object + + Bytes {.proto3.} = object + x {.fieldNumber: 1.}: seq[byte] -proc writeEmpty[T](value: T) = - check Protobuf.encode(value).len == 0 + AllDefaults {.proto3.} = object + x01 {.fieldNumber: 1.}: seq[byte] + x02 {.fieldNumber: 2.}: string + x03 {.fieldNumber: 3, pint.}: int32 + x04 {.fieldNumber: 4, pint.}: uint32 + x05 {.fieldNumber: 5, pint.}: int64 + x06 {.fieldNumber: 6, pint.}: uint64 + x07 {.fieldNumber: 7, sint.}: int32 + x08 {.fieldNumber: 8, sint.}: int64 + x09 {.fieldNumber: 9, fixed.}: int32 + x10 {.fieldNumber: 10, fixed.}: int64 + x11 {.fieldNumber: 11.}: float64 + x12 {.fieldNumber: 12.}: float32 + x13 {.fieldNumber: 13.}: bool + x14 {.fieldNumber: 14.}: Bytes suite "Test Encoding of Empty Objects/Values": test "Empty object": - writeEmpty(X()) - writeEmpty(Y()) - writeEmpty(Z()) + check Protobuf.encode(Empty()).len == 0 + + test "All default values produce no output": + check Protobuf.encode(AllDefaults()).len == 0 diff --git a/tests/test_enum.nim b/tests/test_enum.nim index f61bf8c..b178491 100644 --- a/tests/test_enum.nim +++ b/tests/test_enum.nim @@ -1,3 +1,12 @@ +# nim-protobuf-serialization +# Copyright (c) 2026 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + import unittest2 import diff --git a/tests/test_fixed.nim b/tests/test_fixed.nim index 4543087..3c73922 100644 --- a/tests/test_fixed.nim +++ b/tests/test_fixed.nim @@ -1,3 +1,12 @@ +# nim-protobuf-serialization +# Copyright (c) 2026 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + import unittest2 import ../protobuf_serialization diff --git a/tests/test_groups.nim b/tests/test_groups.nim index 62753d5..49df85f 100644 --- a/tests/test_groups.nim +++ b/tests/test_groups.nim @@ -1,3 +1,12 @@ +# nim-protobuf-serialization +# Copyright (c) 2026 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + import unittest2 import ../protobuf_serialization diff --git a/tests/test_malformed.nim b/tests/test_malformed.nim index 59ca37c..cc77c3d 100644 --- a/tests/test_malformed.nim +++ b/tests/test_malformed.nim @@ -1,3 +1,12 @@ +# nim-protobuf-serialization +# Copyright (c) 2026 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + import unittest2 import stew/byteutils import ../protobuf_serialization @@ -9,6 +18,21 @@ type Varint {.proto3.} = object x {.fieldNumber: 1, pint.}: int64 + StringObj {.proto3.} = object + x {.fieldNumber: 1.}: string + + PInt32Obj {.proto3.} = object + x {.fieldNumber: 1, pint.}: int32 + + PInt64Obj {.proto3.} = object + x {.fieldNumber: 1, pint.}: int64 + + FixedInt32Obj {.proto3.} = object + x {.fieldNumber: 1, fixed.}: int32 + + FixedInt64Obj {.proto3.} = object + x {.fieldNumber: 1, fixed.}: int64 + suite "Test well formed messages": test "Bytes len 0": # field 1, length-delimited, varint for 0 @@ -41,6 +65,12 @@ suite "Test malformed messages": expect(ProtobufValueError): discard ProtoBuf.decode(encoded, Bytes) + test "Max uint32 + 2 length": + # this must be rejected, not truncated; truncated == 1 (valid) + let encoded = "0A818080801061".hexToSeqByte + expect(ProtobufValueError): + discard ProtoBuf.decode(encoded, Bytes) + test "Max uint32 length": # echo "0AFFFFFFFF0F" | xxd -r -p | protoc --decode=Bytes test_malformed.proto # Failed to parse input. @@ -114,3 +144,46 @@ suite "Test malformed messages": let encoded = "08FFFFFFFFFFFFFFFFFFFF01".hexToSeqByte expect(ProtobufValueError): discard ProtoBuf.decode(encoded, Varint) + + test "Malformed unicode string": + # echo 'x: "\xE2\x28\xA1"' | protoc --encode=StringObj test_malformed.proto | hexdump -ve '1/1 "%.2x"' + # echo "0a03e228a1" | xxd -r -p | protoc --decode=StringObj test_malformed.proto + let encoded = "0a03e228a1".hexToSeqByte + expect(ProtobufValueError): + discard Protobuf.decode(encoded, StringObj) + + test "Truncated length varint": + # echo "0A81" | xxd -r -p | protoc --decode=StringObj test_malformed.proto + let encoded = "0A81".hexToSeqByte + expect(ProtobufValueError): + discard Protobuf.decode(encoded, StringObj) + + test "Length greater than available data": + # echo "0A056162" | xxd -r -p | protoc --decode=StringObj test_malformed.proto + let encoded = "0A056162".hexToSeqByte + expect(ProtobufValueError): + discard Protobuf.decode(encoded, StringObj) + + test "Truncated varint": + # echo "0881" | xxd -r -p | protoc --decode=PInt32Obj test_malformed.proto + let encoded = "0881".hexToSeqByte + expect(ProtobufValueError): + discard Protobuf.decode(encoded, PInt32Obj) + + test "Varint overflow (11 bytes)": + # echo "08FFFFFFFFFFFFFFFFFFFF01" | xxd -r -p | protoc --decode=PInt64Obj test_malformed.proto + let encoded = "08FFFFFFFFFFFFFFFFFFFF01".hexToSeqByte + expect(ProtobufValueError): + discard Protobuf.decode(encoded, PInt64Obj) + + test "Truncated fixed32 field (3 bytes instead of 4)": + # echo "0D010000" | xxd -r -p | protoc --decode=FixedInt32Obj test_malformed.proto + let encoded = "0D010000".hexToSeqByte + expect(ProtobufValueError): + discard Protobuf.decode(encoded, FixedInt32Obj) + + test "Truncated fixed64 field (7 bytes instead of 8)": + # echo "0901000000000000" | xxd -r -p | protoc --decode=FixedInt64Obj test_malformed.proto + let encoded = "0901000000000000".hexToSeqByte + expect(ProtobufValueError): + discard Protobuf.decode(encoded, FixedInt64Obj) diff --git a/tests/test_malformed.proto b/tests/test_malformed.proto index a9fcc04..b1aa6c2 100644 --- a/tests/test_malformed.proto +++ b/tests/test_malformed.proto @@ -7,3 +7,23 @@ message Bytes { message Varint { int64 x = 1; } + +message StringObj { + string x = 1; +} + +message PInt32Obj { + int32 x = 1; +} + +message PInt64Obj { + int64 x = 1; +} + +message FixedInt32Obj { + sfixed32 x = 1; +} + +message FixedInt64Obj { + sfixed64 x = 1; +} \ No newline at end of file diff --git a/tests/test_objects.nim b/tests/test_objects.nim index c8ca60e..e5bc153 100644 --- a/tests/test_objects.nim +++ b/tests/test_objects.nim @@ -1,3 +1,12 @@ +# nim-protobuf-serialization +# Copyright (c) 2026 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + import unittest2 import diff --git a/tests/test_protobuf2_semantics.nim b/tests/test_protobuf2_semantics.nim index 30c0fb5..75e9c31 100644 --- a/tests/test_protobuf2_semantics.nim +++ b/tests/test_protobuf2_semantics.nim @@ -1,3 +1,12 @@ +# nim-protobuf-serialization +# Copyright (c) 2026 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + import unittest2 import ../protobuf_serialization diff --git a/tests/test_repeated.nim b/tests/test_repeated.nim index ac8a26b..d821c24 100644 --- a/tests/test_repeated.nim +++ b/tests/test_repeated.nim @@ -1,3 +1,12 @@ +# nim-protobuf-serialization +# Copyright (c) 2026 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + import sets import unittest2 import stew/byteutils diff --git a/tests/test_thirty_three_fields.nim b/tests/test_thirty_three_fields.nim index ba8f373..e7c7202 100644 --- a/tests/test_thirty_three_fields.nim +++ b/tests/test_thirty_three_fields.nim @@ -1,3 +1,12 @@ +# nim-protobuf-serialization +# Copyright (c) 2026 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + import unittest2 import ../protobuf_serialization diff --git a/tests/test_truncation.nim b/tests/test_truncation.nim new file mode 100644 index 0000000..0c8c8cb --- /dev/null +++ b/tests/test_truncation.nim @@ -0,0 +1,158 @@ +# nim-protobuf-serialization +# Copyright (c) 2026 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +import unittest2 + +import ./utils, ../protobuf_serialization + +type + BoolObj {.proto3.} = object + x {.fieldNumber: 1.}: bool + + PInt32Obj {.proto3.} = object + x {.fieldNumber: 1, pint.}: int32 + + PUInt32Obj {.proto3.} = object + x {.fieldNumber: 1, pint.}: uint32 + + PInt64Obj {.proto3.} = object + x {.fieldNumber: 1, pint.}: int64 + + PUInt64Obj {.proto3.} = object + x {.fieldNumber: 1, pint.}: uint64 + + FixedInt32Obj {.proto3.} = object + x {.fieldNumber: 1, fixed.}: int32 + + FixedInt64Obj {.proto3.} = object + x {.fieldNumber: 1, fixed.}: int64 + + FixedUInt32Obj {.proto3.} = object + x {.fieldNumber: 1, fixed.}: uint32 + + FixedUInt64Obj {.proto3.} = object + x {.fieldNumber: 1, fixed.}: uint64 + + SInt32Obj {.proto3.} = object + x {.fieldNumber: 1, sint.}: int32 + + SInt64Obj {.proto3.} = object + x {.fieldNumber: 1, sint.}: int64 + +suite "Test int64 to int32 truncation": + test "Int64 is truncated to int32": + # echo 'x: 0x7FFFFFFFFFFFFFFF' | protoc --encode=PInt64Obj test_truncation.proto | hexdump -ve '1/1 "%.2x"' + # 08ffffffffffffffff7f + # echo "08ffffffffffffffff7f" | xxd -r -p | protoc --decode=PInt32Obj test_truncation.proto + # x: -1 + roundtrip(PInt64Obj(x: int64.high), PInt32Obj(x: -1'i32), "08ffffffffffffffff7f") + roundtrip(PInt64Obj(x: 0xFFFFFFFF0000'i64), PInt32Obj(x: 0xFFFF0000'i32), "088080fcffffff3f") + roundtrip(PInt64Obj(x: 0xFFFF0000FFFF'i64), PInt32Obj(x: 0xFFFF'i32), "08ffff8380f0ff3f") + + test "Int64 is truncated to uint32": + # echo "08ffffffffffffffff7f" | xxd -r -p | protoc --decode=PUInt32Obj test_truncation.proto + # x: 4294967295 + roundtrip(PInt64Obj(x: int64.high), PUInt32Obj(x: uint32.high), "08ffffffffffffffff7f") + roundtrip(PInt64Obj(x: 0xFFFFFFFF0000'i64), PUInt32Obj(x: 0xFFFF0000'u32), "088080fcffffff3f") + roundtrip(PInt64Obj(x: 0xFFFF0000FFFF'i64), PUInt32Obj(x: 0xFFFF'u32), "08ffff8380f0ff3f") + +suite "Test uint64 truncation": + test "Uint64 is truncated to int32": + # echo 'x: 0xFFFFFFFFFFFFFFFF' | protoc --encode=PUInt64Obj test_truncation.proto | hexdump -ve '1/1 "%.2x"' + # 08ffffffffffffffffff01 + # echo "08ffffffffffffffffff01" | xxd -r -p | protoc --decode=PInt32Obj test_truncation.proto + # x: -1 + roundtrip(PUInt64Obj(x: uint64.high), PInt32Obj(x: -1'i32), "08ffffffffffffffffff01") + roundtrip(PUInt64Obj(x: 0xFFFF0000FFFF0000'u64), PInt32Obj(x: 0xFFFF0000'i32), "088080fcff8f80c0ffff01") + roundtrip(PUInt64Obj(x: 0xFFFF00000000FFFF'u64), PInt32Obj(x: 0xFFFF'i32), "08ffff83808080c0ffff01") + + test "Uint64 is truncated to uint32": + # echo "08ffffffffffffffffff01" | xxd -r -p | protoc --decode=PUInt32Obj test_truncation.proto + # x: 4294967295 + roundtrip(PUInt64Obj(x: uint64.high), PUInt32Obj(x: uint32.high), "08ffffffffffffffffff01") + roundtrip(PUInt64Obj(x: 0xFFFF0000FFFF0000'u64), PUInt32Obj(x: 0xFFFF0000'u32), "088080fcff8f80c0ffff01") + roundtrip(PUInt64Obj(x: 0xFFFF00000000FFFF'u64), PUInt32Obj(x: 0xFFFF'u32), "08ffff83808080c0ffff01") + + test "Uint64 is truncated to int64": + # echo "08ffffffffffffffffff01" | xxd -r -p | protoc --decode=PInt64Obj test_truncation.proto + # x: -1 + roundtrip(PUInt64Obj(x: uint64.high), PInt64Obj(x: -1'i64), "08ffffffffffffffffff01") + roundtrip(PUInt64Obj(x: uint64.high - 1), PInt64Obj(x: -2'i64), "08feffffffffffffffff01") + +suite "Test bool compatibility": + test "Bool true decoded as int32": + # echo "0801" | xxd -r -p | protoc --decode=PInt32Obj test_truncation.proto + # x: 1 + roundtrip(BoolObj(x: true), PInt32Obj(x: 1'i32), "0801") + + test "Bool true decoded as uint32": + # echo "0801" | xxd -r -p | protoc --decode=PUInt32Obj test_truncation.proto + # x: 1 + roundtrip(BoolObj(x: true), PUInt32Obj(x: 1'u32), "0801") + + test "Bool true decoded as int64": + # echo "0801" | xxd -r -p | protoc --decode=PInt64Obj test_truncation.proto + # x: 1 + roundtrip(BoolObj(x: true), PInt64Obj(x: 1'i64), "0801") + + test "Bool true decoded as uint64": + # echo "0801" | xxd -r -p | protoc --decode=PUInt64Obj test_truncation.proto + # x: 1 + roundtrip(BoolObj(x: true), PUInt64Obj(x: 1'u64), "0801") + + test "Non-zero varint decoded as bool": + # echo "088002" | xxd -r -p | protoc --decode=BoolObj test_truncation.proto + # x: true + roundtrip(PInt32Obj(x: 256'i32), BoolObj(x: true), "088002") + # echo 'x: 0xFFFF0000' | protoc --encode=PInt64Obj test_truncation.proto | hexdump -ve '1/1 "%.2x"' + # 088080fcff0f + # echo "088080fcff0f" | xxd -r -p | protoc --decode=BoolObj test_truncation.proto + roundtrip(PInt64Obj(x: 0xFFFF0000'i64), BoolObj(x: true), "088080fcff0f") + +suite "Test sint64 to sint32 zig-zag": + test "In-range sint64 is compatible with sint32": + # echo 'x: 1' | protoc --encode=SInt64Obj test_truncation.proto | hexdump -ve '1/1 "%.2x"' + # echo "0802" | xxd -r -p | protoc --decode=SInt32Obj test_truncation.proto + # x: 1 + roundtrip(SInt64Obj(x: 1'i64), SInt32Obj(x: 1'i32), "0802") + roundtrip(SInt64Obj(x: 2'i64), SInt32Obj(x: 2'i32), "0804") + roundtrip(SInt64Obj(x: 3'i64), SInt32Obj(x: 3'i32), "0806") + roundtrip(SInt32Obj(x: 1'i32), SInt64Obj(x: 1'i64), "0802") + roundtrip(SInt32Obj(x: 2'i32), SInt64Obj(x: 2'i64), "0804") + roundtrip(SInt32Obj(x: 3'i32), SInt64Obj(x: 3'i64), "0806") + roundtrip(SInt64Obj(x: -1'i64), SInt32Obj(x: -1'i32), "0801") + roundtrip(SInt64Obj(x: -2'i64), SInt32Obj(x: -2'i32), "0803") + roundtrip(SInt64Obj(x: -3'i64), SInt32Obj(x: -3'i32), "0805") + roundtrip(SInt32Obj(x: -1'i32), SInt64Obj(x: -1'i64), "0801") + roundtrip(SInt32Obj(x: -2'i32), SInt64Obj(x: -2'i64), "0803") + roundtrip(SInt32Obj(x: -3'i32), SInt64Obj(x: -3'i64), "0805") + roundtrip(SInt64Obj(x: int32.high), SInt32Obj(x: int32.high), "08feffffff0f") + roundtrip(SInt32Obj(x: int32.high), SInt64Obj(x: int32.high), "08feffffff0f") + roundtrip(SInt64Obj(x: int32.low), SInt32Obj(x: int32.low), "08ffffffff0f") + roundtrip(SInt32Obj(x: int32.low), SInt64Obj(x: int32.low), "08ffffffff0f") + + test "Out-of-range sint64 is truncated to sint32": + # echo 'x: 2147483648' | protoc --encode=SInt64Obj test_truncation.proto | hexdump -ve '1/1 "%.2x"' + # echo "088080808010" | xxd -r -p | protoc --decode=SInt32Obj test_truncation.proto + roundtrip(SInt64Obj(x: int64(int32.high) + 1), SInt32Obj(x: 0'i32), "088080808010") + roundtrip(SInt64Obj(x: int64(int32.high) + 2), SInt32Obj(x: 1'i32), "088280808010") + roundtrip(SInt64Obj(x: int64(int32.high) + 3), SInt32Obj(x: 2'i32), "088480808010") + roundtrip(SInt64Obj(x: int64(int32.low) - 1), SInt32Obj(x: -1'i32), "088180808010") + roundtrip(SInt64Obj(x: int64(int32.low) - 2), SInt32Obj(x: -2'i32), "088380808010") + roundtrip(SInt64Obj(x: int64(int32.low) - 3), SInt32Obj(x: -3'i32), "088580808010") + roundtrip(SInt64Obj(x: int64.high), SInt32Obj(x: int32.high), "08feffffffffffffffff01") + roundtrip(SInt64Obj(x: int64.low), SInt32Obj(x: int32.low), "08ffffffffffffffffff01") + +suite "Test incompatible types": + test "uint64 to sint64": + # echo 'x: 0xFFFFFFFFFFFFFFFF' | protoc --encode=PUInt64Obj test_truncation.proto | hexdump -ve '1/1 "%.2x"' + # 08ffffffffffffffffff01 + # echo "08ffffffffffffffffff01" | xxd -r -p | protoc --decode=SInt64Obj test_truncation.proto + # x: int64.low + roundtrip(PUInt64Obj(x: uint64.high), SInt64Obj(x: int64.low), "08ffffffffffffffffff01") diff --git a/tests/test_truncation.proto b/tests/test_truncation.proto new file mode 100644 index 0000000..462701b --- /dev/null +++ b/tests/test_truncation.proto @@ -0,0 +1,45 @@ +syntax = "proto3"; + +message BoolObj { + bool x = 1; +} + +message PInt32Obj { + int32 x = 1; +} + +message PUInt32Obj { + uint32 x = 1; +} + +message PInt64Obj { + int64 x = 1; +} + +message PUInt64Obj { + uint64 x = 1; +} + +message SInt32Obj { + sint32 x = 1; +} + +message SInt64Obj { + sint64 x = 1; +} + +message FixedInt32Obj { + sfixed32 x = 1; +} + +message FixedInt64Obj { + sfixed64 x = 1; +} + +message FixedUInt32Obj { + fixed32 x = 1; +} + +message FixedUInt64Obj { + fixed64 x = 1; +} diff --git a/tests/test_wire_type_mismatch.proto b/tests/test_wire_type_mismatch.proto index d94396d..aea9481 100644 --- a/tests/test_wire_type_mismatch.proto +++ b/tests/test_wire_type_mismatch.proto @@ -9,9 +9,9 @@ message Varint { } message Fixed32Obj { - fixed32 x = 1; + sfixed32 x = 1; } message Fixed64Obj { - fixed64 x = 1; + sfixed64 x = 1; } diff --git a/tests/utils.nim b/tests/utils.nim new file mode 100644 index 0000000..92200cd --- /dev/null +++ b/tests/utils.nim @@ -0,0 +1,26 @@ +# nim-protobuf-serialization +# Copyright (c) 2026 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +import unittest2 +import stew/byteutils +import ../protobuf_serialization + +proc roundtrip*[W, R](toWrite: W, value: R, expected: string) = + let encoded = Protobuf.encode(toWrite) + check: + encoded.len == Protobuf.computeSize(toWrite) + encoded == hexToSeqByte expected + Protobuf.decode(encoded, R) == value + +proc roundtrip*[R](value: R, expected: string) = + let encoded = Protobuf.encode(value) + check: + encoded.len == Protobuf.computeSize(value) + encoded == hexToSeqByte expected + Protobuf.decode(encoded, R) == value