| // 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) | |
| } | |
| } |