// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) // SPDX-License-Identifier: MIT package xdr import ( "bytes" "errors" "math" "strings" "testing" ) // wantEncoding asserts the exact bytes of one encoding. func wantEncoding(t *testing.T, got, want []byte) { t.Helper() if !bytes.Equal(got, want) { t.Fatalf("encoding mismatch:\n got: %x\nwant: %x", got, want) } } func TestUint32Layout(t *testing.T) { wantEncoding(t, AppendUint32(nil, 0), []byte{0, 0, 0, 0}) wantEncoding(t, AppendUint32(nil, 1), []byte{0, 0, 0, 1}) wantEncoding(t, AppendUint32(nil, math.MaxUint32), []byte{0xff, 0xff, 0xff, 0xff}) wantEncoding(t, AppendUint32(nil, 0xdeadbeef), []byte{0xde, 0xad, 0xbe, 0xef}) } func TestInt32Layout(t *testing.T) { wantEncoding(t, AppendInt32(nil, 0), []byte{0, 0, 0, 0}) wantEncoding(t, AppendInt32(nil, -1), []byte{0xff, 0xff, 0xff, 0xff}) wantEncoding(t, AppendInt32(nil, math.MinInt32), []byte{0x80, 0, 0, 0}) wantEncoding(t, AppendInt32(nil, math.MaxInt32), []byte{0x7f, 0xff, 0xff, 0xff}) } func TestUint64Layout(t *testing.T) { wantEncoding(t, AppendUint64(nil, 0), make([]byte, 8)) wantEncoding(t, AppendUint64(nil, math.MaxUint64), []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}) wantEncoding(t, AppendUint64(nil, 1), []byte{0, 0, 0, 0, 0, 0, 0, 1}) wantEncoding(t, AppendUint64(nil, 1<<32), []byte{0, 0, 0, 1, 0, 0, 0, 0}) } func TestInt64Layout(t *testing.T) { wantEncoding(t, AppendInt64(nil, -1), []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}) wantEncoding(t, AppendInt64(nil, math.MinInt64), []byte{0x80, 0, 0, 0, 0, 0, 0, 0}) } func TestBoolLayout(t *testing.T) { wantEncoding(t, AppendBool(nil, true), []byte{0, 0, 0, 1}) wantEncoding(t, AppendBool(nil, false), []byte{0, 0, 0, 0}) } func TestOpaquePadding(t *testing.T) { // A body of n bytes is followed by (4 - n%4) % 4 zero bytes. for n := range 9 { got := AppendFixedOpaque(nil, bytes.Repeat([]byte{0xa5}, n)) pad := (4 - n%4) % 4 if len(got) != n+pad { t.Fatalf("fixed opaque of %d bytes encoded as %d bytes, want %d", n, len(got), n+pad) } for _, b := range got[n:] { if b != 0 { t.Fatalf("fixed opaque of %d bytes carries nonzero padding", n) } } } wantEncoding(t, AppendVarOpaque(nil, []byte("ab")), []byte{0, 0, 0, 2, 'a', 'b', 0, 0}) wantEncoding(t, AppendString(nil, "abcd"), []byte{0, 0, 0, 4, 'a', 'b', 'c', 'd'}) wantEncoding(t, AppendString(nil, ""), []byte{0, 0, 0, 0}) } func TestRoundTrip(t *testing.T) { const long = "pěkný řetězec s diakritikou, aby UTF-8 prošlo bez úhony" inputs := struct { u32 []uint32 i32 []int32 u64 []uint64 i64 []int64 bl []bool str []string op [][]byte }{ u32: []uint32{0, 1, 2049, math.MaxUint32}, i32: []int32{0, -1, 1, math.MinInt32, math.MaxInt32}, u64: []uint64{0, 1, 2049, 1 << 40, math.MaxUint64}, i64: []int64{0, -1, math.MinInt64, math.MaxInt64}, bl: []bool{true, false}, str: []string{"", "a", "abcd", "abcde", long, strings.Repeat("x", 1000)}, op: [][]byte{nil, {}, []byte("ab"), bytes.Repeat([]byte{0x5a}, 7)}, } var buf []byte for _, v := range inputs.u32 { buf = AppendUint32(buf, v) } for _, v := range inputs.i32 { buf = AppendInt32(buf, v) } for _, v := range inputs.u64 { buf = AppendUint64(buf, v) } for _, v := range inputs.i64 { buf = AppendInt64(buf, v) } for _, v := range inputs.bl { buf = AppendBool(buf, v) } for _, v := range inputs.str { buf = AppendString(buf, v) } for _, v := range inputs.op { buf = AppendVarOpaque(buf, v) } if len(buf)%4 != 0 { t.Fatalf("the joined encoding has %d bytes, not a multiple of four", len(buf)) } d := NewDecoder(buf) for _, want := range inputs.u32 { got, err := d.Uint32() if err != nil || got != want { t.Fatalf("uint32: got %d, %v; want %d", got, err, want) } } for _, want := range inputs.i32 { got, err := d.Int32() if err != nil || got != want { t.Fatalf("int32: got %d, %v; want %d", got, err, want) } } for _, want := range inputs.u64 { got, err := d.Uint64() if err != nil || got != want { t.Fatalf("uint64: got %d, %v; want %d", got, err, want) } } for _, want := range inputs.i64 { got, err := d.Int64() if err != nil || got != want { t.Fatalf("int64: got %d, %v; want %d", got, err, want) } } for _, want := range inputs.bl { got, err := d.Bool() if err != nil || got != want { t.Fatalf("bool: got %v, %v; want %v", got, err, want) } } for _, want := range inputs.str { got, err := d.String() if err != nil || got != want { t.Fatalf("string: got %q, %v; want %q", got, err, want) } } for _, want := range inputs.op { got, err := d.VarOpaque() if err != nil || !bytes.Equal(got, want) { t.Fatalf("var opaque: got %x, %v; want %x", got, err, want) } } if d.Remaining() != 0 { t.Fatalf("%d bytes left over after the full round trip", d.Remaining()) } } func TestFixedOpaqueRoundTrip(t *testing.T) { for n := range 9 { want := bytes.Repeat([]byte{0x3c}, n) got, err := NewDecoder(AppendFixedOpaque(nil, want)).FixedOpaque(n) if err != nil || !bytes.Equal(got, want) { t.Fatalf("fixed opaque %d: got %x, %v", n, got, err) } } } func TestTruncated(t *testing.T) { // Every proper prefix of an encoded value fails to decode, and the // failure is a truncation rather than anything else. u64 := AppendUint64(nil, math.MaxUint64) for n := range len(u64) { if _, err := NewDecoder(u64[:n]).Uint64(); !errors.Is(err, ErrTruncated) { t.Fatalf("a %d byte prefix decoded as a uint64: %v", n, err) } } vo := AppendVarOpaque(nil, []byte("0123456789")) for n := range len(vo) { if _, err := NewDecoder(vo[:n]).VarOpaque(); !errors.Is(err, ErrTruncated) { t.Fatalf("a %d byte prefix decoded as a var opaque: %v", n, err) } } } func TestTruncatedPadding(t *testing.T) { // A two byte body carries two padding bytes, so an input that ends // after one of them is truncated, not complete. _, err := NewDecoder([]byte{0, 0, 0, 2, 'a', 'b', 0}).VarOpaque() if !errors.Is(err, ErrTruncated) { t.Fatalf("a short padding decoded as a value: %v", err) } } func TestImpossibleLengths(t *testing.T) { // A count of 0xffffffff on a tiny input is refused; on a 32 bit // platform the count does not fit an int at all and the bad length // guard fires first, which the test accepts either way. d := NewDecoder([]byte{0xff, 0xff, 0xff, 0xff}) if _, err := d.VarOpaque(); !errors.Is(err, ErrTruncated) && !errors.Is(err, ErrBadLength) { t.Fatalf("a count of 0xffffffff on a tiny input returned %v", err) } if _, err := d.FixedOpaque(-1); !errors.Is(err, ErrBadLength) { t.Fatalf("a negative fixed length returned %v", err) } } func TestRaw(t *testing.T) { in := []byte{1, 2, 3, 4, 5} d := NewDecoder(in) got, err := d.Raw(3) if err != nil || !bytes.Equal(got, in[:3]) { t.Fatalf("raw: %x, %v", got, err) } if d.Remaining() != 2 { t.Fatalf("%d bytes left, want 2", d.Remaining()) } if _, err := d.Raw(3); !errors.Is(err, ErrTruncated) { t.Fatalf("a raw read past the end: %v", err) } if _, err := NewDecoder(nil).Raw(-1); !errors.Is(err, ErrBadLength) { t.Fatalf("a negative raw read: %v", err) } } func TestNonzeroBool(t *testing.T) { // The receiver accepts any nonzero word as true. got, err := NewDecoder([]byte{0, 0, 0, 0x7f}).Bool() if err != nil || !got { t.Fatalf("the word 0x7f decoded as %v, %v", got, err) } }