blob: 3f50854843af23f4929f865d90c6c31bf365c826 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package codec
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"math"
"runtime"
"strings"
"testing"
)
// --- byte-construction helpers ---
func u16le(v uint16) []byte {
b := make([]byte, 2)
binary.LittleEndian.PutUint16(b, v)
return b
}
func u32le(v uint32) []byte {
b := make([]byte, 4)
binary.LittleEndian.PutUint32(b, v)
return b
}
func u64le(v uint64) []byte {
b := make([]byte, 8)
binary.LittleEndian.PutUint64(b, v)
return b
}
func cat(slices ...[]byte) []byte {
var out []byte
for _, s := range slices {
out = append(out, s...)
}
return out
}
// testPoint is a simple type that implements BinaryMarshaler/BinaryUnmarshaler
// as [x, y] two-byte encoding.
type testPoint struct {
x, y uint8
}
func (p *testPoint) MarshalBinary() ([]byte, error) {
return []byte{p.x, p.y}, nil
}
func (p *testPoint) UnmarshalBinary(b []byte) error {
if len(b) < 2 {
return fmt.Errorf("testPoint: need 2 bytes, got %d", len(b))
}
p.x = b[0]
p.y = b[1]
return nil
}
// errMarshaler always returns an error from MarshalBinary.
type errMarshaler struct{}
func (errMarshaler) MarshalBinary() ([]byte, error) {
return nil, errors.New("marshal error")
}
// errUnmarshaler always returns an error from UnmarshalBinary.
type errUnmarshaler struct{}
func (e *errUnmarshaler) UnmarshalBinary(_ []byte) error {
return errors.New("unmarshal error")
}
// TestReader_reads exercises every read method in sequence.
func TestReader_reads(t *testing.T) {
const wantU8 uint8 = math.MaxUint8
const wantU16 uint16 = math.MaxUint16
const wantU32 uint32 = math.MaxUint32
const wantU64 uint64 = math.MaxUint64
const wantF32 float32 = math.Pi
const wantRem = 1
wantStr := "str"
wantU32LenStr := "uint32"
wantU8LenStr := "uint8"
wantRaw := []byte{0xDE, 0xAD}
wantObj := testPoint{1, 2}
payload := cat(
[]byte{wantU8}, // U8
u16le(wantU16), // U16
u32le(wantU32), // U32
u64le(wantU64), // U64
u32le(math.Float32bits(wantF32)), // F32
[]byte(wantStr), // Str(len(wantStr))
u32le(uint32(len(wantU32LenStr))), []byte(wantU32LenStr), // U32LenStr
[]byte{uint8(len(wantU8LenStr))}, []byte(wantU8LenStr), // U8LenStr
wantRaw, // Raw(len(wantRaw))
[]byte{1, 2}, // Obj(testPoint{1, 2})
[]byte{0xFF}, // wantRem trailing bytes for Remaining()
)
r := NewReader(payload)
u8 := r.U8()
u16 := r.U16()
u32 := r.U32()
u64 := r.U64()
f32 := r.F32()
str := r.Str(len(wantStr))
u32LenStr := r.U32LenStr()
u8LenStr := r.U8LenStr()
raw := r.Raw(len(wantRaw))
var obj testPoint
r.Obj(2, &obj)
rem := r.Remaining()
if err := r.Err(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if u8 != wantU8 {
t.Errorf("U8: got %#x, want %#x", u8, wantU8)
}
if u16 != wantU16 {
t.Errorf("U16: got %#x, want %#x", u16, wantU16)
}
if u32 != wantU32 {
t.Errorf("U32: got %#x, want %#x", u32, wantU32)
}
if u64 != wantU64 {
t.Errorf("U64: got %#x, want %#x", u64, wantU64)
}
if f32 != wantF32 {
t.Errorf("F32: got %v, want %v", f32, wantF32)
}
if str != wantStr {
t.Errorf("Str: got %q, want %q", str, wantStr)
}
if u32LenStr != wantU32LenStr {
t.Errorf("U32LenStr: got %q, want %q", u32LenStr, wantU32LenStr)
}
if u8LenStr != wantU8LenStr {
t.Errorf("U8LenStr: got %q, want %q", u8LenStr, wantU8LenStr)
}
if !bytes.Equal(raw, wantRaw) {
t.Errorf("Raw: got %v, want %v", raw, wantRaw)
}
if obj != wantObj {
t.Errorf("Obj: got %v, want %v", obj, wantObj)
}
if rem != wantRem {
t.Errorf("Remaining: got %d, want %d", rem, wantRem)
}
}
// TestReader_truncation verifies that every read method returns a descriptive error
// when the buffer is too short, including mid-sequence truncation.
func TestReader_truncation(t *testing.T) {
cases := []struct {
name string
payload []byte
read func(*Reader)
}{
{"U8", []byte{}, func(r *Reader) { r.U8() }},
{"U16", []byte{0x01}, func(r *Reader) { r.U16() }}, // 1 byte, need 2
{"U32", []byte{0x01, 0x02, 0x03}, func(r *Reader) { r.U32() }}, // 3 bytes, need 4
{"U64", []byte{0x01, 0x02, 0x03, 0x04}, func(r *Reader) { r.U64() }}, // 4 bytes, need 8
{"F32", []byte{0x01, 0x02, 0x03}, func(r *Reader) { r.F32() }}, // 3 bytes, need 4
{"Str", []byte("hi"), func(r *Reader) { r.Str(5) }}, // claims 5, has 2
{"Raw", []byte("hi"), func(r *Reader) { r.Raw(5) }}, // claims 5, has 2
{"Obj", []byte{1}, func(r *Reader) {
var p testPoint
r.Obj(2, &p)
}},
{"U32LenStr/short-len-prefix", []byte{0x05, 0x00}, func(r *Reader) { r.U32LenStr() }}, // len prefix needs 4 bytes, got 2
{"U32LenStr/short-body", cat(u32le(10), []byte("short")), func(r *Reader) { r.U32LenStr() }}, // claims 10, has 5
{"U8LenStr/short-len-prefix", []byte{}, func(r *Reader) { r.U8LenStr() }}, // len prefix needs 1 byte, got 0
{"U8LenStr/short-body", cat([]byte{10}, []byte("short")), func(r *Reader) { r.U8LenStr() }}, // claims 10, has 5
{"mid-sequence", cat(u32le(1), []byte{0xFF}), func(r *Reader) { r.U32(); r.U32() }},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
r := NewReader(tc.payload)
tc.read(r)
err := r.Err()
if err == nil || !strings.HasPrefix(err.Error(), "reader: need ") {
t.Fatalf("got %v, want overrun error", err)
}
})
}
}
// TestReader_errSentinel verifies that once an error is set, all subsequent
// read methods are no-ops and return zero values without overwriting the error.
func TestReader_errSentinel(t *testing.T) {
r := NewReader([]byte{})
r.U8() // triggers overrun, sets r.err
if r.Err() == nil {
t.Fatal("expected error after overrun, got nil")
}
err := r.Err()
if v := r.U8(); v != 0 {
t.Errorf("U8: got %v, want 0", v)
}
if v := r.U16(); v != 0 {
t.Errorf("U16: got %v, want 0", v)
}
if v := r.U32(); v != 0 {
t.Errorf("U32: got %v, want 0", v)
}
if v := r.U64(); v != 0 {
t.Errorf("U64: got %v, want 0", v)
}
if v := r.F32(); v != 0 {
t.Errorf("F32: got %v, want 0", v)
}
if v := r.Str(1); v != "" {
t.Errorf("Str: got %q, want empty", v)
}
if v := r.Raw(1); v != nil {
t.Errorf("Raw: got %v, want nil", v)
}
if v := r.U32LenStr(); v != "" {
t.Errorf("U32LenStr: got %q, want empty", v)
}
if v := r.U8LenStr(); v != "" {
t.Errorf("U8LenStr: got %q, want empty", v)
}
var p testPoint
r.Obj(2, &p)
if p != (testPoint{}) {
t.Errorf("Obj: got %v, want zero value", p)
}
if r.Err() != err {
t.Errorf("error was overwritten: got %v, want %v", r.Err(), err)
}
}
// TestReader_Obj_unmarshalError verifies that Reader.Obj propagates an error
// returned by UnmarshalBinary.
func TestReader_Obj_unmarshalError(t *testing.T) {
r := NewReader([]byte{1, 2})
_, file, line, _ := runtime.Caller(0)
r.Obj(2, &errUnmarshaler{})
checkLoc(t, r.Err(), file, line+1)
}
// TestReader_overrun_error_location verifies that the error message contains
// the file and line of the call site that triggered the overrun, for every
// public read method.
func TestReader_overrun_error_location(t *testing.T) {
cases := []struct {
name string
payload []byte
fn func(r *Reader) (wantFile string, wantLine int)
}{
{"U8", []byte{}, func(r *Reader) (string, int) {
_, file, line, _ := runtime.Caller(0)
r.U8()
return file, line + 1
}},
{"U16", []byte{}, func(r *Reader) (string, int) {
_, file, line, _ := runtime.Caller(0)
r.U16()
return file, line + 1
}},
{"U32", []byte{}, func(r *Reader) (string, int) {
_, file, line, _ := runtime.Caller(0)
r.U32()
return file, line + 1
}},
{"U64", []byte{}, func(r *Reader) (string, int) {
_, file, line, _ := runtime.Caller(0)
r.U64()
return file, line + 1
}},
{"F32", []byte{}, func(r *Reader) (string, int) {
_, file, line, _ := runtime.Caller(0)
r.F32()
return file, line + 1
}},
{"Str", []byte{}, func(r *Reader) (string, int) {
_, file, line, _ := runtime.Caller(0)
r.Str(1)
return file, line + 1
}},
{"Raw", []byte{}, func(r *Reader) (string, int) {
_, file, line, _ := runtime.Caller(0)
r.Raw(1)
return file, line + 1
}},
{"Obj", []byte{1}, func(r *Reader) (string, int) {
var p testPoint
_, file, line, _ := runtime.Caller(0)
r.Obj(2, &p)
return file, line + 1
}},
{"U32LenStr/prefix", []byte{}, func(r *Reader) (string, int) {
_, file, line, _ := runtime.Caller(0)
r.U32LenStr()
return file, line + 1
}},
{"U32LenStr/body", cat(u32le(100), []byte("short")), func(r *Reader) (string, int) {
_, file, line, _ := runtime.Caller(0)
r.U32LenStr()
return file, line + 1
}},
{"U8LenStr/prefix", []byte{}, func(r *Reader) (string, int) {
_, file, line, _ := runtime.Caller(0)
r.U8LenStr()
return file, line + 1
}},
{"U8LenStr/body", cat([]byte{100}, []byte("short")), func(r *Reader) (string, int) {
_, file, line, _ := runtime.Caller(0)
r.U8LenStr()
return file, line + 1
}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
r := NewReader(tc.payload)
wantFile, wantLine := tc.fn(r)
checkLoc(t, r.Err(), wantFile, wantLine)
})
}
}
func checkLoc(t *testing.T, err error, wantFile string, wantLine int) {
t.Helper()
if err == nil {
t.Error("expected error, got nil")
return
}
wantLoc := fmt.Sprintf("%s:%d", wantFile, wantLine)
if !strings.Contains(err.Error(), wantLoc) {
t.Errorf("error %q does not contain location %q", err.Error(), wantLoc)
}
}