From 67d54ea8ec7bbda051c88df9647a58567d5256e9 Mon Sep 17 00:00:00 2001 From: Roman Atachiants Date: Mon, 20 Jul 2026 20:05:15 +0100 Subject: [PATCH 1/2] Modernize CI and docs for Go 1.25 Bump the module to Go 1.25, align the GitHub Actions workflow with the tales coverage/gocognit pipeline, and expand the README with custom serialization docs and full bench results. Also clean up testlint hygiene and split scanType so cognitive complexity stays under the CI limit. Co-authored-by: Cursor --- .github/workflows/test.yml | 71 +++- README.md | 288 ++++++++++++++-- bench/go.mod | 2 +- bench/main_test.go | 18 + codecs_test.go | 321 ++++++++++-------- decoder_test.go | 4 +- encoder_test.go | 4 +- go.mod | 5 +- nocopy/codecs.go | 244 -------------- nocopy/types.go | 666 +++++++++++++++++++++++++------------ nocopy/types_test.go | 448 ++++++++++--------------- scanner.go | 200 +++++------ sorted/types_test.go | 150 ++++----- unsafe/codecs.go | 46 --- unsafe/types.go | 38 +++ unsafe/types_test.go | 188 +++++------ 16 files changed, 1424 insertions(+), 1269 deletions(-) create mode 100644 bench/main_test.go delete mode 100644 nocopy/codecs.go delete mode 100644 unsafe/codecs.go diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b882b4b..51111d5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,29 +1,70 @@ name: Test -on: [push, pull_request] + +on: + pull_request: + push: + branches: + - master + +permissions: + contents: read + pull-requests: write + env: - GITHUB_TOKEN: ${{ secrets.COVERALLS_TOKEN }} GO111MODULE: "on" + GOCOGNIT_MAX: "20" + COVER_FAIL_UNDER: "75" + jobs: test: name: Test with Coverage runs-on: ubuntu-latest - strategy: - matrix: - go: ["1.19", "1.20"] steps: - - name: Set up Go ${{ matrix.go }} - uses: actions/setup-go@v3 + - name: Checkout + uses: actions/checkout@v4 with: - go-version: ${{ matrix.go }} - - name: Check out code - uses: actions/checkout@v3 + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - name: Install dependencies + run: go mod download + + - name: Run unit tests with coverage run: | - go mod download - - name: Run Unit Tests + mkdir -p coverage + go test -race -count=1 -covermode=atomic -coverprofile=coverage/go \ + $(go list ./... | grep -v -E '/bench$') + + - name: Check cognitive complexity run: | - go test -race -covermode atomic -coverprofile=profile.cov ./... - - name: Upload Coverage + go install github.com/uudashr/gocognit/cmd/gocognit@v1.2.1 + echo "### Cognitive complexity (max ${GOCOGNIT_MAX})" >> "$GITHUB_STEP_SUMMARY" + gocognit -ignore "_test|testdata|bench" -avg . | tee -a "$GITHUB_STEP_SUMMARY" + echo '```' >> "$GITHUB_STEP_SUMMARY" + gocognit -ignore "_test|testdata|bench" -top 10 . | tee -a "$GITHUB_STEP_SUMMARY" + echo '```' >> "$GITHUB_STEP_SUMMARY" + gocognit -ignore "_test|testdata|bench" -over "${GOCOGNIT_MAX}" . + + - name: Upload coverage to Coveralls uses: shogo82148/actions-goveralls@v1 with: - path-to-profile: profile.cov + path-to-profile: coverage/go + env: + GITHUB_TOKEN: ${{ secrets.COVERALLS_TOKEN }} + + - name: Convert Go coverage to lcov + run: | + go install github.com/jandelgado/gcov2lcov@v1.1.1 + gcov2lcov -infile=coverage/go -outfile=coverage/go.lcov + + - name: Enforce new-code coverage + if: github.event_name == 'pull_request' + uses: Affanmir/diff-cover-action@v2 + with: + coverage-files: coverage/go.lcov + compare-branch: origin/${{ github.base_ref }} + fail-under: ${{ env.COVER_FAIL_UNDER }} diff --git a/README.md b/README.md index 1782611..2d70f9c 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,283 @@ -# Generic and Fast Binary Serializer for Go +

+Go Version +PkgGoDev +Go Report Card +License +

-This repository contains a fast binary packer for Golang, this allows to encode/decode arbtitrary golang data structures of variable size. [Documentation](https://godoc.org/github.com/Kelindar/binary) can be found on [https://godoc.org/github.com/Kelindar/binary](https://godoc.org/github.com/Kelindar/binary). +## Fast Binary Serializer for Go -This package extends support to arbitrary, variable-sized values by prefixing these values with their varint-encoded size, recursively. This was originally inspired by Alec Thomas's binary package, but I've reworked the serialization format and improved the performance and size. Here's a few notable features/goals of this `binary` package: - * Zero-allocation encoding. I'm hoping to make the encoding to be as fast as possible, simply writing binary to the `io.Writer` without unncessary allocations. - * Support for `maps`, `arrays`, `slices`, `structs`, primitive and nested types. - * This is essentially a `json.Marshal` and `json.Unmarshal` drop-in replacement, I wanted this package to be simple to use and leverage the power of `reflect` package of golang. - * The `ints` and `uints` are encoded using `varint`, making the payload small as possible. - * Fast-paths encoding and decoding of `[]byte`, as I've designed this package to be used for inter-broker message encoding for [emitter](https://github.com/emitter-io/emitter). - * Support for custom `BinaryMarshaler` and `BinaryUnmarshaler` for tighter packing control and built-in types such as `time.Time`. +This package contains a **high-performance binary serializer** for Go that encodes and decodes arbitrary data structures of variable size. It is designed as a simple `json.Marshal` / `json.Unmarshal` drop-in for cases where you control both ends and want compact, fast payloads — for example inter-broker message encoding in [emitter](https://github.com/emitter-io/emitter). +## Features +- **Simple API** that mirrors `encoding/json` with `Marshal`, `Unmarshal`, and `MarshalTo`. +- **Compact payloads** using varint encoding for integers and size-prefixed variable-length values. +- **Zero-allocation encoding** path via `MarshalTo` writing directly to an `io.Writer`. +- **Reflect-based** support for structs, maps, slices, arrays, pointers, and nested types. +- **Fast paths** for `[]byte` and other common slice types. +- **Custom serialization** via `encoding.BinaryMarshaler` / `BinaryUnmarshaler` or a full `Codec` through `GetBinaryCodec`. +- **Field skipping** with the `binary:"-"` struct tag. +- Optional subpackages for **sorted**, **unsafe**, and **nocopy** typed slices when you need smaller payloads or lower decode cost. + +## Documentation + +Variable-sized values are prefixed with a varint-encoded size and encoded recursively. The format is intentionally not versioned or cross-language — this is for efficient exchange of known Go types between systems you control. + +- [Quick Start](#quick-start) +- [Streaming Encode and Decode](#streaming-encode-and-decode) +- [Skipping Fields](#skipping-fields) +- [Custom Serialization](#custom-serialization) +- [Typed Slice Subpackages](#typed-slice-subpackages) +- [Benchmarks](#benchmarks) +- [Disclaimer](#disclaimer) +- [Contributing](#contributing) +- [License](#license) + +## Quick Start + +Define a message and marshal it the same way you would with JSON: + +```go +type message struct { + Name string + Timestamp int64 + Payload []byte + Ssid []uint32 +} -# Usage -To serialize a message, simply `Marshal`: -``` v := &message{ - Name: "Roman", - Timestamp: 1242345235, - Payload: []byte("hi"), - Ssid: []uint32{1, 2, 3}, + Name: "Roman", + Timestamp: 1242345235, + Payload: []byte("hi"), + Ssid: []uint32{1, 2, 3}, } encoded, err := binary.Marshal(v) +if err != nil { + panic(err) +} + +var out message +err = binary.Unmarshal(encoded, &out) +``` + +## Streaming Encode and Decode + +For hot paths, write into a reused buffer with `MarshalTo`, or use `Encoder` / `Decoder` directly against an `io.Writer` / `io.Reader`: + +```go +var buf bytes.Buffer +if err := binary.MarshalTo(v, &buf); err != nil { + panic(err) +} + +dec := binary.NewDecoder(&buf) +var out message +if err := dec.Decode(&out); err != nil { + panic(err) +} +``` + +## Skipping Fields + +Fields tagged with `binary:"-"` are ignored during encode and decode. Useful for locks, caches, or derived state: + +```go +type Cache struct { + mu sync.Mutex `binary:"-"` + Key string + Value []byte +} +``` + +## Custom Serialization + +By default, values are encoded through reflection. You can override that for a type in two ways, checked in this order: + +1. **`GetBinaryCodec()`** — return a `binary.Codec` for full control over the wire format (no extra length prefix). +2. **`MarshalBinary` / `UnmarshalBinary`** — the standard `encoding.BinaryMarshaler` / `BinaryUnmarshaler` pair; the package length-prefixes the returned bytes for you. + +Use `MarshalBinary` when you already have a `[]byte` representation. Use `GetBinaryCodec` when you want to stream fields through the encoder without an intermediate buffer. + +### Option 1: `MarshalBinary` and `UnmarshalBinary` + +This matches `encoding.BinaryMarshaler` and `encoding.BinaryUnmarshaler`. On encode, the returned slice is written as `uvarint(length) + bytes`. On decode, that framed blob is passed to `UnmarshalBinary`: + +```go +type CompactHeader struct { + Code uint8 +} + +func (h CompactHeader) MarshalBinary() ([]byte, error) { + return []byte{h.Code}, nil +} + +func (h *CompactHeader) UnmarshalBinary(data []byte) error { + if len(data) != 1 { + return fmt.Errorf("CompactHeader: want 1 byte, got %d", len(data)) + } + h.Code = data[0] + return nil +} + +encoded, err := binary.Marshal(CompactHeader{Code: 0x13}) +// encoded == []byte{0x01, 0x13} // length + payload + +var out CompactHeader +err = binary.Unmarshal(encoded, &out) ``` -To deserialize, `Unmarshal`: +This is enough for most custom types, including anything that already implements the standard library interfaces (for example `time.Time`). + +### Option 2: `GetBinaryCodec` + +For tighter packing or to avoid the intermediate `[]byte`, implement `GetBinaryCodec` on a **pointer receiver**. It must return a `binary.Codec`: + +```go +type Codec interface { + EncodeTo(*Encoder, reflect.Value) error + DecodeTo(*Decoder, reflect.Value) error +} ``` -var v message -err := binary.Unmarshal(encoded, &v) + +Example: encode a 2D point as two raw `float64` values with no struct field metadata: + +```go +type Point struct { + X float64 + Y float64 +} + +func (p *Point) GetBinaryCodec() binary.Codec { + return pointCodec{} +} + +type pointCodec struct{} + +func (pointCodec) EncodeTo(e *binary.Encoder, rv reflect.Value) error { + p := rv.Interface().(Point) + e.WriteFloat64(p.X) + e.WriteFloat64(p.Y) + return nil +} + +func (pointCodec) DecodeTo(d *binary.Decoder, rv reflect.Value) error { + x, err := d.ReadFloat64() + if err != nil { + return err + } + y, err := d.ReadFloat64() + if err != nil { + return err + } + rv.Set(reflect.ValueOf(Point{X: x, Y: y})) + return nil +} +``` + +Because `GetBinaryCodec` takes priority, a type should not also rely on `MarshalBinary` for the same purpose — pick one approach per type. + +Nested use works automatically: if a struct field's type implements either customization, that field uses the custom path while the rest of the struct uses the default reflect codecs. + +```go +type Packet struct { + ID uint64 + At time.Time // uses time.Time's BinaryMarshaler + Pos Point // uses GetBinaryCodec above + Raw []byte +} ``` -# Disclaimer +## Typed Slice Subpackages + +Optional helpers live in subpackages when the default reflect path is not enough: + +| Package | Purpose | +|---------|---------| +| [`sorted`](./sorted) | Delta-encoded sorted integer / timestamp slices for smaller wire size | +| [`unsafe`](./unsafe) | Memory-cast numeric slices (faster, not portable across endianness) | +| [`nocopy`](./nocopy) | Like `unsafe`, but decode reuses the input buffer (zero-copy; lifetime tied to the buffer) | + +```go +import ( + "github.com/kelindar/binary" + "github.com/kelindar/binary/sorted" +) + +v := sorted.Int32s{4, 5, 6, 1, 2, 3} +encoded, err := binary.Marshal(&v) + +var out sorted.Int32s +err = binary.Unmarshal(encoded, &out) +``` + +See each subpackage README for trade-offs and warnings. + +## Benchmarks + +Numbers from `bench/`. Run locally with: + +```bash +cd bench && go run . +``` + +``` +name time/op ops/s allocs/op +-------------------- ------------ ------------ ------------ +binary/enc 139.2 ns 7.2M 2 +binary/enc-to 104.7 ns 9.5M 0 +binary/dec 175.6 ns 5.7M 5 +binary/map-enc 9.5 µs 105.2K 210 +binary/map-dec 14.4 µs 69.4K 511 +binary/slice-enc 9.4 µs 106.5K 9 +binary/slice-dec 14.9 µs 67.2K 502 +binary/nest-enc 5.0 µs 201.9K 14 +binary/nest-dec 8.3 µs 120.8K 271 +binary/bytes-enc 835.2 ns 1.2M 3 +binary/bytes-dec 821.2 ns 1.2M 2 +binary/u64-enc 75.3 µs 13.3K 11 +binary/u64-dec 58.4 µs 17.1K 2 +binary/reuse-enc 113.2 ns 8.8M 0 +binary/stream-dec 240.8 ns 4.2M 5 +nocopy/str-enc 109.7 ns 9.1M 3 +nocopy/str-dec 39.2 ns 25.5M 0 +nocopy/dict-enc 185.7 ns 5.4M 2 +nocopy/dict-dec 154.5 ns 6.5M 2 +nocopy/bmap-enc 313.3 ns 3.2M 5 +nocopy/bmap-dec 160.9 ns 6.2M 2 +nocopy/hmap-enc 307.6 ns 3.3M 5 +nocopy/hmap-dec 146.6 ns 6.8M 2 +nocopy/bytes-enc 862.0 ns 1.2M 3 +nocopy/bytes-dec 41.6 ns 24.0M 0 +nocopy/u64-enc 5.3 µs 187.6K 3 +nocopy/u64-dec 39.8 ns 25.1M 0 +nocopy/col-enc 524.3 ns 1.9M 9 +nocopy/col-dec 532.2 ns 1.9M 8 +nocopy/struct-enc 162.9 ns 6.1M 3 +nocopy/struct-dec 82.7 ns 12.1M 0 +sorted/i32-enc 78.9 µs 12.7K 5 +sorted/i32-dec 551.8 µs 1.8K 29.8K +sorted/u32-enc 76.2 µs 13.1K 5 +sorted/u32-dec 543.5 µs 1.8K 29.8K +sorted/ts-enc 52.5 µs 19.0K 6 +sorted/ts-dec 21.8 µs 45.8K 2 +sorted/tsz-enc 126.4 µs 7.9K 7 +sorted/tsz-dec 119.3 µs 8.4K 3 +sorted/tcz-enc 85.9 µs 11.6K 6 +sorted/tcz-dec 76.1 µs 13.1K 3 +unsafe/u64-enc 459.8 ns 2.2M 3 +unsafe/u64-dec 450.1 ns 2.2M 2 +``` + +## Disclaimer + +This is **not** a replacement for JSON, protobuf, or other versioned interchange formats. The codec does not maintain schema evolution or cross-language compatibility. Use it to exchange binary data of a known format between Go services where you control both ends. + +## Contributing + +Contributions are welcome — open a pull request and we will review it as quickly as we can. This library is maintained by [Roman Atachiants](https://www.linkedin.com/in/atachiants/). -This is not intended as a replacement for JSON or protobuf, this codec does not maintain any versioning or compatibility - and not intended to become one. The goal of this binary codec is to efficiently exchange binary data of known format between systems where you control both ends and both of them are written in Go. +## License +Binary is licensed under the [MIT License](LICENSE). diff --git a/bench/go.mod b/bench/go.mod index 902dd8c..401e05f 100644 --- a/bench/go.mod +++ b/bench/go.mod @@ -1,6 +1,6 @@ module github.com/kelindar/binary/bench -go 1.24.0 +go 1.25.0 require ( github.com/kelindar/bench v0.3.2 diff --git a/bench/main_test.go b/bench/main_test.go new file mode 100644 index 0000000..7a9c754 --- /dev/null +++ b/bench/main_test.go @@ -0,0 +1,18 @@ +package main + +import "testing" + +func TestHelpers(t *testing.T) { + t.Run("uint64s", func(t *testing.T) { + got := makeUint64s(5) + if len(got) != 5 || got[4] != 4 { + t.Fatalf("makeUint64s(5) = %v", got) + } + }) + t.Run("bytes", func(t *testing.T) { + got := makeBytes(5) + if len(got) != 5 || got[4] != 4 { + t.Fatalf("makeBytes(5) = %v", got) + } + }) +} diff --git a/codecs_test.go b/codecs_test.go index 4be67bf..097cc48 100644 --- a/codecs_test.go +++ b/codecs_test.go @@ -14,14 +14,6 @@ import ( "github.com/stretchr/testify/assert" ) -// Message represents a message to be flushed -type msg struct { - Name string - Timestamp int64 - Payload []byte - Ssid []uint32 -} - type s0 struct { A string B string @@ -33,7 +25,81 @@ var ( s0b = []byte{0x1, 0x41, 0x1, 0x42, 0x2} ) -func TestBinaryTime(t *testing.T) { +type simpleStruct struct { + Name string + Timestamp time.Time + Payload []byte + Ssid []uint32 +} + +type sliceStruct struct { + Payload []byte +} + +type s1 struct { + Name string + BirthDay time.Time + Phone string + Siblings int + Spouse bool + Money float64 + Tags map[string]string + Aliases []string +} + +var ( + s1v = &s1{ + Name: "Bob Smith", + BirthDay: time.Date(2013, 1, 2, 3, 4, 5, 6, time.UTC), + Phone: "5551234567", + Siblings: 2, + Spouse: false, + Money: 100.0, + Tags: map[string]string{"key": "value"}, + Aliases: []string{"Bobby", "Robert"}, + } + + svb = []byte{0x9, 0x42, 0x6f, 0x62, 0x20, 0x53, 0x6d, 0x69, 0x74, 0x68, 0xf, 0x1, 0x0, 0x0, 0x0, 0xe, 0xc8, 0x75, 0x9a, 0xa5, 0x0, 0x0, 0x0, + 0x6, 0xff, 0xff, 0xa, 0x35, 0x35, 0x35, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x59, 0x40, 0x1, + 0x3, 0x0, 0x6b, 0x65, 0x79, 0x5, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x2, 0x5, 0x42, 0x6f, 0x62, 0x62, 0x79, 0x6, 0x52, 0x6f, 0x62, 0x65, 0x72, 0x74} +) + +type s2 struct { + b []byte +} + +func (s *s2) UnmarshalBinary(data []byte) error { + if len(data) != 1 { + return errors.New("expected data to be length 1") + } + s.b = data + return nil +} + +func (s *s2) MarshalBinary() (data []byte, err error) { + return s.b, nil +} + +func TestMarshal(t *testing.T) { + tests := map[string]func(*testing.T){ + "time slice": testMarshalTimeSlice, + "nil slice EOF": testMarshalNilSliceEOF, + "simple struct": testMarshalSimpleStruct, + "simple struct slice": testMarshalSimpleStructSlice, + "complex struct": testMarshalComplexStruct, + "binary marshaler": testMarshalBinaryMarshaler, + "type alias": testMarshalTypeAlias, + "non-pointer value": testMarshalNonPointer, + "big struct": testMarshalBigStruct, + } + for name, fn := range tests { + t.Run(name, func(t *testing.T) { + fn(t) + }) + } +} + +func testMarshalTimeSlice(t *testing.T) { input := []time.Time{ time.Date(2013, 1, 2, 3, 4, 5, 6, time.UTC), } @@ -52,19 +118,7 @@ func TestBinaryTime(t *testing.T) { assert.Equal(t, 1, len(v)) } -// Message represents a message to be flushed -type simpleStruct struct { - Name string - Timestamp time.Time - Payload []byte - Ssid []uint32 -} - -type sliceStruct struct { - Payload []byte -} - -func TestBinaryEncode_EOF(t *testing.T) { +func testMarshalNilSliceEOF(t *testing.T) { v := &sliceStruct{ Payload: nil, } @@ -80,7 +134,7 @@ func TestBinaryEncode_EOF(t *testing.T) { assert.Equal(t, v, s) } -func TestBinaryEncodeSimpleStruct(t *testing.T) { +func testMarshalSimpleStruct(t *testing.T) { v := &simpleStruct{ Name: "Roman", Timestamp: time.Date(2013, 1, 2, 3, 4, 5, 6, time.UTC), @@ -99,7 +153,7 @@ func TestBinaryEncodeSimpleStruct(t *testing.T) { assert.Equal(t, v, s) } -func TestBinarySimpleStructSlice(t *testing.T) { +func testMarshalSimpleStructSlice(t *testing.T) { input := []simpleStruct{{ Name: "Roman", Timestamp: time.Date(2013, 1, 2, 3, 4, 5, 6, time.UTC), @@ -122,35 +176,7 @@ func TestBinarySimpleStructSlice(t *testing.T) { assert.Equal(t, 2, len(v)) } -type s1 struct { - Name string - BirthDay time.Time - Phone string - Siblings int - Spouse bool - Money float64 - Tags map[string]string - Aliases []string -} - -var ( - s1v = &s1{ - Name: "Bob Smith", - BirthDay: time.Date(2013, 1, 2, 3, 4, 5, 6, time.UTC), - Phone: "5551234567", - Siblings: 2, - Spouse: false, - Money: 100.0, - Tags: map[string]string{"key": "value"}, - Aliases: []string{"Bobby", "Robert"}, - } - - svb = []byte{0x9, 0x42, 0x6f, 0x62, 0x20, 0x53, 0x6d, 0x69, 0x74, 0x68, 0xf, 0x1, 0x0, 0x0, 0x0, 0xe, 0xc8, 0x75, 0x9a, 0xa5, 0x0, 0x0, 0x0, - 0x6, 0xff, 0xff, 0xa, 0x35, 0x35, 0x35, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x59, 0x40, 0x1, - 0x3, 0x0, 0x6b, 0x65, 0x79, 0x5, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x2, 0x5, 0x42, 0x6f, 0x62, 0x62, 0x79, 0x6, 0x52, 0x6f, 0x62, 0x65, 0x72, 0x74} -) - -func TestBinaryEncodeComplex(t *testing.T) { +func testMarshalComplexStruct(t *testing.T) { b, err := Marshal(s1v) assert.NoError(t, err) assert.Equal(t, svb, b) @@ -161,30 +187,14 @@ func TestBinaryEncodeComplex(t *testing.T) { assert.Equal(t, s1v, s) } -type s2 struct { - b []byte -} - -func (s *s2) UnmarshalBinary(data []byte) error { - if len(data) != 1 { - return errors.New("expected data to be length 1") - } - s.b = data - return nil -} - -func (s *s2) MarshalBinary() (data []byte, err error) { - return s.b, nil -} - -func TestBinaryMarshalUnMarshaler(t *testing.T) { +func testMarshalBinaryMarshaler(t *testing.T) { s2v := &s2{[]byte{0x13}} b, err := Marshal(s2v) assert.NoError(t, err) assert.Equal(t, []byte{0x1, 0x13}, b) } -func TestMarshalUnMarshalTypeAliases(t *testing.T) { +func testMarshalTypeAlias(t *testing.T) { type Foo int64 f := Foo(32) b, err := Marshal(f) @@ -192,7 +202,49 @@ func TestMarshalUnMarshalTypeAliases(t *testing.T) { assert.Equal(t, []byte{0x40}, b) } -func TestStructWithStruct(t *testing.T) { +func testMarshalNonPointer(t *testing.T) { + type S struct { + A int + } + s := S{A: 1} + data, err := Marshal(s) + if err != nil { + t.Fatal(err) + } + var res S + if err := Unmarshal(data, &res); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(res, s) { + t.Fatalf("expect %v got %v", s, res) + } +} + +func testMarshalBigStruct(t *testing.T) { + input := newBigStruct() + b, err := Marshal(input) + assert.NoError(t, err) + + var output bigStruct + assert.NoError(t, Unmarshal(b, &output)) + assert.Equal(t, input, &output) +} + +func TestStruct(t *testing.T) { + tests := map[string]func(*testing.T){ + "nested fields": testStructNestedFields, + "embedded struct": testStructEmbedded, + "array of struct": testStructArray, + "slice of struct": testStructSlice, + } + for name, fn := range tests { + t.Run(name, func(t *testing.T) { + fn(t) + }) + } +} + +func testStructNestedFields(t *testing.T) { type T1 struct { ID uint64 Name string @@ -223,10 +275,9 @@ func TestStructWithStruct(t *testing.T) { if !reflect.DeepEqual(s, v) { t.Fatalf("got= %#v\nwant=%#v\n", v, s) } - } -func TestStructWithEmbeddedStruct(t *testing.T) { +func testStructEmbedded(t *testing.T) { type T1 struct { ID uint64 Name string @@ -257,10 +308,9 @@ func TestStructWithEmbeddedStruct(t *testing.T) { if !reflect.DeepEqual(s, v) { t.Fatalf("got= %#v\nwant=%#v\n", v, s) } - } -func TestArrayOfStructWithStruct(t *testing.T) { +func testStructArray(t *testing.T) { type T1 struct { ID uint64 Name string @@ -293,10 +343,9 @@ func TestArrayOfStructWithStruct(t *testing.T) { if !reflect.DeepEqual(s, v) { t.Fatalf("got= %#v\nwant=%#v\n", v, s) } - } -func TestSliceOfStructWithStruct(t *testing.T) { +func testStructSlice(t *testing.T) { type T1 struct { ID uint64 Name string @@ -329,10 +378,24 @@ func TestSliceOfStructWithStruct(t *testing.T) { if !reflect.DeepEqual(s, v) { t.Fatalf("got= %#v\nwant=%#v\n", v, s) } +} +func TestPointer(t *testing.T) { + tests := map[string]func(*testing.T){ + "basic types": testPointerBasicTypes, + "pointer of pointer": testPointerOfPointer, + "struct pointer field": testPointerStructField, + "slice of pointers": testPointerSlice, + "slice of time pointers": testPointerTimeSlice, + } + for name, fn := range tests { + t.Run(name, func(t *testing.T) { + fn(t) + }) + } } -func TestBasicTypePointers(t *testing.T) { +func testPointerBasicTypes(t *testing.T) { type BT struct { B *bool S *string @@ -441,7 +504,7 @@ func TestBasicTypePointers(t *testing.T) { } } -func TestPointerOfPointer(t *testing.T) { +func testPointerOfPointer(t *testing.T) { type S struct { V **int } @@ -477,7 +540,7 @@ func TestPointerOfPointer(t *testing.T) { } } -func TestStructPointer(t *testing.T) { +func testPointerStructField(t *testing.T) { type T struct { V int } @@ -512,51 +575,7 @@ func TestStructPointer(t *testing.T) { } } -func TestMarshalNonPointer(t *testing.T) { - type S struct { - A int - } - s := S{A: 1} - data, err := Marshal(s) - if err != nil { - t.Fatal(err) - } - var res S - if err := Unmarshal(data, &res); err != nil { - t.Fatal(err) - } - if !reflect.DeepEqual(res, s) { - t.Fatalf("expect %v got %v", s, res) - } -} - -func Test_Float32(t *testing.T) { - v := float32(1.15) - - b, err := Marshal(&v) - assert.NoError(t, err) - assert.NotNil(t, b) - - var o float32 - err = Unmarshal(b, &o) - assert.NoError(t, err) - assert.Equal(t, v, o) -} - -func Test_Float64(t *testing.T) { - v := float64(1.15) - - b, err := Marshal(&v) - assert.NoError(t, err) - assert.NotNil(t, b) - - var o float64 - err = Unmarshal(b, &o) - assert.NoError(t, err) - assert.Equal(t, v, o) -} - -func TestSliceOfPtrs(t *testing.T) { +func testPointerSlice(t *testing.T) { type A struct { V int64 } @@ -572,7 +591,7 @@ func TestSliceOfPtrs(t *testing.T) { assert.Equal(t, v, o) } -func TestSliceOfTimePtrs(t *testing.T) { +func testPointerTimeSlice(t *testing.T) { type A struct { T0 *time.Time T1 *time.Time @@ -591,12 +610,46 @@ func TestSliceOfTimePtrs(t *testing.T) { assert.Equal(t, v, o) } -func TestEncodeBigStruct(t *testing.T) { - input := newBigStruct() - b, err := Marshal(input) - assert.NoError(t, err) +func TestFloat(t *testing.T) { + tests := map[string]struct { + marshal func() (interface{}, []byte, error) + unmarshal func([]byte) (interface{}, error) + want interface{} + }{ + "float32": { + marshal: func() (interface{}, []byte, error) { + v := float32(1.15) + b, err := Marshal(&v) + return v, b, err + }, + unmarshal: func(b []byte) (interface{}, error) { + var o float32 + err := Unmarshal(b, &o) + return o, err + }, + }, + "float64": { + marshal: func() (interface{}, []byte, error) { + v := float64(1.15) + b, err := Marshal(&v) + return v, b, err + }, + unmarshal: func(b []byte) (interface{}, error) { + var o float64 + err := Unmarshal(b, &o) + return o, err + }, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + want, b, err := tc.marshal() + assert.NoError(t, err) + assert.NotNil(t, b) - var output bigStruct - assert.NoError(t, Unmarshal(b, &output)) - assert.Equal(t, input, &output) + got, err := tc.unmarshal(b) + assert.NoError(t, err) + assert.Equal(t, want, got) + }) + } } diff --git a/decoder_test.go b/decoder_test.go index 5462fe6..fa49203 100644 --- a/decoder_test.go +++ b/decoder_test.go @@ -10,14 +10,14 @@ import ( "github.com/stretchr/testify/assert" ) -func TestBinaryDecodeStruct(t *testing.T) { +func TestDecodeStruct(t *testing.T) { s := &s0{} err := Unmarshal(s0b, s) assert.NoError(t, err) assert.Equal(t, s0v, s) } -func TestBinaryDecodeToValueErrors(t *testing.T) { +func TestDecodeErrors(t *testing.T) { b := []byte{1, 0, 0, 0} var v uint32 err := Unmarshal(b, v) diff --git a/encoder_test.go b/encoder_test.go index 2c70451..0b94c6c 100644 --- a/encoder_test.go +++ b/encoder_test.go @@ -78,7 +78,7 @@ func newComposite() composite { return v } -func TestBinaryEncodeStruct(t *testing.T) { +func TestEncodeStruct(t *testing.T) { b, err := Marshal(s0v) assert.NoError(t, err) assert.Equal(t, s0b, b) @@ -89,7 +89,7 @@ func TestEncoderSizeOf(t *testing.T) { assert.Equal(t, 56, int(unsafe.Sizeof(e))) } -func TestMarshalWithCustomCodec(t *testing.T) { +func TestCustomCodec(t *testing.T) { v := testCustom("custom codec") b, err := Marshal(v) diff --git a/go.mod b/go.mod index 1a93049..ddb9b2e 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,10 @@ module github.com/kelindar/binary -go 1.17 +go 1.25.0 + +require github.com/stretchr/testify v1.2.2 require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/stretchr/testify v1.2.2 ) diff --git a/nocopy/codecs.go b/nocopy/codecs.go deleted file mode 100644 index 467dc85..0000000 --- a/nocopy/codecs.go +++ /dev/null @@ -1,244 +0,0 @@ -// Copyright (c) Roman Atachiants and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for details. - -package nocopy - -import ( - "reflect" - "unsafe" - - "github.com/kelindar/binary" -) - -type integerSliceCodec struct { - sliceType reflect.Type - sizeOfInt int -} - -// EncodeTo encodes a value into the encoder. -func (c *integerSliceCodec) EncodeTo(e *binary.Encoder, rv reflect.Value) (err error) { - var out reflect.SliceHeader - out.Data = rv.Pointer() - out.Len = rv.Len() * c.sizeOfInt - out.Cap = out.Len - - e.WriteUint64(uint64(rv.Len() * c.sizeOfInt)) - e.Write(*(*[]byte)(unsafe.Pointer(&out))) - return -} - -// DecodeTo decodes into a reflect value from the decoder. -func (c *integerSliceCodec) DecodeTo(d *binary.Decoder, rv reflect.Value) (err error) { - var l uint64 - var b []byte - - if l, err = d.ReadUint64(); err == nil && l > 0 { - if b, err = d.Slice(int(l)); err == nil { - out := (*reflect.SliceHeader)(unsafe.Pointer(rv.UnsafeAddr())) - out.Data = (*reflect.SliceHeader)(unsafe.Pointer(&b)).Data - out.Len = int(l) / c.sizeOfInt - out.Cap = int(l) / c.sizeOfInt - } - } - return -} - -// ------------------------------------------------------------------------------ - -type byteSliceCodec struct{} - -// Encode encodes a value into the encoder. -func (c *byteSliceCodec) EncodeTo(e *binary.Encoder, rv reflect.Value) (err error) { - e.WriteUvarint(uint64(rv.Len())) - e.Write(rv.Bytes()) - return -} - -// Decode decodes into a reflect value from the decoder. -func (c *byteSliceCodec) DecodeTo(d *binary.Decoder, rv reflect.Value) (err error) { - var l uint64 - var b []byte - - if l, err = d.ReadUvarint(); err == nil && l > 0 { - if b, err = d.Slice(int(l)); err == nil { - rv.SetBytes(b) - } - } - return -} - -// ------------------------------------------------------------------------------ - -type stringCodec struct{} - -// Encode encodes a value into the encoder. -func (c *stringCodec) EncodeTo(e *binary.Encoder, rv reflect.Value) error { - v := rv.String() - e.WriteUvarint(uint64(len(v))) - e.Write(binary.ToBytes(v)) - return nil -} - -// Decode decodes into a reflect value from the decoder. -func (c *stringCodec) DecodeTo(d *binary.Decoder, rv reflect.Value) (err error) { - var l uint64 - var v []byte - - if l, err = d.ReadUvarint(); err == nil { - if v, err = d.Slice(int(l)); err == nil { - rv.SetString(binary.ToString(&v)) - } - } - return -} - -// ------------------------------------------------------------------------------ - -type boolSliceCodec struct{} - -// Encode encodes a value into the encoder. -func (c *boolSliceCodec) EncodeTo(e *binary.Encoder, rv reflect.Value) (err error) { - l := rv.Len() - e.WriteUvarint(uint64(l)) - if l > 0 { - v := rv.Interface().(Bools) - e.Write(boolsToBinary(&v)) - } - return -} - -// Decode decodes into a reflect value from the decoder. -func (c *boolSliceCodec) DecodeTo(d *binary.Decoder, rv reflect.Value) (err error) { - var l uint64 - var v []byte - - if l, err = d.ReadUvarint(); err == nil && l > 0 { - if v, err = d.Slice(int(l)); err == nil { - rv.Set(reflect.ValueOf(binaryToBools(&v))) - } - } - return -} - -// ----------------------------------------------------------------------------- - -// The codec to use for marshaling the properties -type byteMapCodec struct{} - -// Encode encodes a value into the encoder. -func (c *byteMapCodec) EncodeTo(e *binary.Encoder, rv reflect.Value) (err error) { - dict := rv.Interface().(ByteMap) - e.WriteUint16(uint16(len(dict))) - for k, v := range dict { - encodeString(e, k) - e.WriteUvarint(uint64(len(v))) - e.Write(v) - } - return -} - -// Decode decodes into a reflect value from the decoder. -func (c *byteMapCodec) DecodeTo(d *binary.Decoder, rv reflect.Value) (err error) { - var size uint16 - if size, err = d.ReadUint16(); err == nil { - dict := make(ByteMap, int(size)) - rv.Set(reflect.ValueOf(dict)) - for i := 0; i < int(size); i++ { - k, _ := decodeString(d) - var l uint64 - var b []byte - if l, err = d.ReadUvarint(); err == nil && l > 0 { - if b, err = d.Slice(int(l)); err == nil { - dict[k] = b - } - } - } - } - return -} - -// ----------------------------------------------------------------------------- - -// The codec to use for marshaling the pre-hashed hash maps -type hashMapCodec struct{} - -// Encode encodes a value into the encoder. -func (c *hashMapCodec) EncodeTo(e *binary.Encoder, rv reflect.Value) (err error) { - dict := rv.Interface().(HashMap) - e.WriteUint32(uint32(len(dict))) - for k, v := range dict { - e.WriteUint64(k) - e.WriteUint32(uint32(len(v))) - e.Write(v) - } - return -} - -// Decode decodes into a reflect value from the decoder. -func (c *hashMapCodec) DecodeTo(d *binary.Decoder, rv reflect.Value) (err error) { - var size uint32 - if size, err = d.ReadUint32(); err == nil { - dict := make(HashMap, int(size)) - rv.Set(reflect.ValueOf(dict)) - for i := 0; i < int(size); i++ { - k, _ := d.ReadUint64() - var l uint32 - var b []byte - if l, err = d.ReadUint32(); err == nil && l > 0 { - if b, err = d.Slice(int(l)); err == nil { - dict[k] = b - } - } - } - } - return -} - -// ----------------------------------------------------------------------------- - -// The codec to use for marshaling the properties -type dictionaryCodec struct{} - -// Encode encodes a value into the encoder. -func (c *dictionaryCodec) EncodeTo(e *binary.Encoder, rv reflect.Value) (err error) { - dict := rv.Interface().(Dictionary) - e.WriteUint16(uint16(len(dict))) - for k, v := range dict { - encodeString(e, k) - encodeString(e, v) - } - return -} - -// Decode decodes into a reflect value from the decoder. -func (c *dictionaryCodec) DecodeTo(d *binary.Decoder, rv reflect.Value) (err error) { - var size uint16 - if size, err = d.ReadUint16(); err == nil { - dict := make(Dictionary) - rv.Set(reflect.ValueOf(dict)) - for i := 0; i < int(size); i++ { - k, _ := decodeString(d) - v, _ := decodeString(d) - dict[k] = v - } - } - return -} - -// encodeString writes a string to the encoder -func encodeString(e *binary.Encoder, v string) { - e.WriteUvarint(uint64(len(v))) - e.Write(binary.ToBytes(v)) -} - -// decodeString reads a string from the decoder -func decodeString(d *binary.Decoder) (v string, err error) { - var l uint64 - var b []byte - if l, err = d.ReadUvarint(); err == nil { - if b, err = d.Slice(int(l)); err == nil { - v = binary.ToString(&b) - } - } - return -} diff --git a/nocopy/types.go b/nocopy/types.go index bc8f5b2..938839b 100644 --- a/nocopy/types.go +++ b/nocopy/types.go @@ -1,215 +1,451 @@ -// Copyright (c) Roman Atachiants and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for details. - -package nocopy - -import ( - "reflect" - - "github.com/kelindar/binary" -) - -// ------------------------------------------------------------------------------ - -// String represents a type serialized in an unsafe, non portable manner. Moreover, when -// decoding it simply reuses the underlying byte array to store the data and does not -// perform a memory copy. This can be dangerous in many cases, be careful how this is used. -type String string - -// GetBinaryCodec retrieves a custom binary codec. -func (s *String) GetBinaryCodec() binary.Codec { - return new(stringCodec) -} - -// ------------------------------------------------------------------------------ - -// Bytes represents a type serialized in an unsafe, non portable manner. Moreover, when -// decoding it simply reuses the underlying byte array to store the data and does not -// perform a memory copy. This can be dangerous in many cases, be careful how this is used. -type Bytes []byte - -// GetBinaryCodec retrieves a custom binary codec. -func (s *Bytes) GetBinaryCodec() binary.Codec { - return new(byteSliceCodec) -} - -// ------------------------------------------------------------------------------ - -// Bools represents a type serialized in an unsafe, non portable manner. Moreover, when -// decoding it simply reuses the underlying byte array to store the data and does not -// perform a memory copy. This can be dangerous in many cases, be careful how this is used. -type Bools []bool - -// GetBinaryCodec retrieves a custom binary codec. -func (s *Bools) GetBinaryCodec() binary.Codec { - return new(boolSliceCodec) -} - -// ------------------------------------------------------------------------------ - -// Uint16s represents a slice serialized in an unsafe, non portable manner. -type Uint16s []uint16 - -func (s Uint16s) Len() int { return len(s) } -func (s Uint16s) Less(i, j int) bool { return s[i] < s[j] } -func (s Uint16s) Swap(i, j int) { s[i], s[j] = s[j], s[i] } - -// GetBinaryCodec retrieves a custom binary codec. -func (s *Uint16s) GetBinaryCodec() binary.Codec { - return &integerSliceCodec{ - sliceType: reflect.TypeOf(Uint16s{}), - sizeOfInt: 2, - } -} - -// ------------------------------------------------------------------------------ - -// Int16s represents a slice serialized in an unsafe, non portable manner. -type Int16s []int16 - -func (s Int16s) Len() int { return len(s) } -func (s Int16s) Less(i, j int) bool { return s[i] < s[j] } -func (s Int16s) Swap(i, j int) { s[i], s[j] = s[j], s[i] } - -// GetBinaryCodec retrieves a custom binary codec. -func (s *Int16s) GetBinaryCodec() binary.Codec { - return &integerSliceCodec{ - sliceType: reflect.TypeOf(Int16s{}), - sizeOfInt: 2, - } -} - -// ------------------------------------------------------------------------------ - -// Uint32s represents a slice serialized in an unsafe, non portable manner. -type Uint32s []uint32 - -func (s Uint32s) Len() int { return len(s) } -func (s Uint32s) Less(i, j int) bool { return s[i] < s[j] } -func (s Uint32s) Swap(i, j int) { s[i], s[j] = s[j], s[i] } - -// GetBinaryCodec retrieves a custom binary codec. -func (s *Uint32s) GetBinaryCodec() binary.Codec { - return &integerSliceCodec{ - sliceType: reflect.TypeOf(Uint32s{}), - sizeOfInt: 4, - } -} - -// ------------------------------------------------------------------------------ - -// Int32s represents a slice serialized in an unsafe, non portable manner. -type Int32s []int32 - -func (s Int32s) Len() int { return len(s) } -func (s Int32s) Less(i, j int) bool { return s[i] < s[j] } -func (s Int32s) Swap(i, j int) { s[i], s[j] = s[j], s[i] } - -// GetBinaryCodec retrieves a custom binary codec. -func (s *Int32s) GetBinaryCodec() binary.Codec { - return &integerSliceCodec{ - sliceType: reflect.TypeOf(Int32s{}), - sizeOfInt: 4, - } -} - -// ------------------------------------------------------------------------------ - -// Uint64s represents a slice serialized in an unsafe, non portable manner. -type Uint64s []uint64 - -func (s Uint64s) Len() int { return len(s) } -func (s Uint64s) Less(i, j int) bool { return s[i] < s[j] } -func (s Uint64s) Swap(i, j int) { s[i], s[j] = s[j], s[i] } - -// GetBinaryCodec retrieves a custom binary codec. -func (s *Uint64s) GetBinaryCodec() binary.Codec { - return &integerSliceCodec{ - sliceType: reflect.TypeOf(Uint64s{}), - sizeOfInt: 8, - } -} - -// ------------------------------------------------------------------------------ - -// Int64s represents a slice serialized in an unsafe, non portable manner. -type Int64s []int64 - -func (s Int64s) Len() int { return len(s) } -func (s Int64s) Less(i, j int) bool { return s[i] < s[j] } -func (s Int64s) Swap(i, j int) { s[i], s[j] = s[j], s[i] } - -// GetBinaryCodec retrieves a custom binary codec. -func (s *Int64s) GetBinaryCodec() binary.Codec { - return &integerSliceCodec{ - sliceType: reflect.TypeOf(Int64s{}), - sizeOfInt: 8, - } -} - -// ------------------------------------------------------------------------------ - -// Float32s represents a slice serialized in an unsafe, non portable manner. -type Float32s []float32 - -func (s Float32s) Len() int { return len(s) } -func (s Float32s) Less(i, j int) bool { return s[i] < s[j] } -func (s Float32s) Swap(i, j int) { s[i], s[j] = s[j], s[i] } - -// GetBinaryCodec retrieves a custom binary codec. -func (s *Float32s) GetBinaryCodec() binary.Codec { - return &integerSliceCodec{ - sliceType: reflect.TypeOf(Float32s{}), - sizeOfInt: 4, - } -} - -// ------------------------------------------------------------------------------ - -// Float64s represents a slice serialized in an unsafe, non portable manner. -type Float64s []float64 - -func (s Float64s) Len() int { return len(s) } -func (s Float64s) Less(i, j int) bool { return s[i] < s[j] } -func (s Float64s) Swap(i, j int) { s[i], s[j] = s[j], s[i] } - -// GetBinaryCodec retrieves a custom binary codec. -func (s *Float64s) GetBinaryCodec() binary.Codec { - return &integerSliceCodec{ - sliceType: reflect.TypeOf(Float64s{}), - sizeOfInt: 8, - } -} - -// ------------------------------------------------------------------------------ - -// Dictionary represents a map where both keys and values are strings. It is -// serialized in an unsafe, non portable manner. -type Dictionary map[string]string - -// GetBinaryCodec retrieves a custom binary codec. -func (d *Dictionary) GetBinaryCodec() binary.Codec { - return new(dictionaryCodec) -} - -// ------------------------------------------------------------------------------ - -// ByteMap represents a map where keys are strings but the values are slices of -// bytes. It is encoded in an unsafe, non portable mapper. -type ByteMap map[string][]byte - -// GetBinaryCodec retrieves a custom binary codec. -func (d *ByteMap) GetBinaryCodec() binary.Codec { - return new(byteMapCodec) -} - -// ------------------------------------------------------------------------------ - -// HashMap represents a map where keys are uint64 but the values are slices of -// bytes. It is encoded in an unsafe, non portable mapper. -type HashMap map[uint64][]byte - -// GetBinaryCodec retrieves a custom binary codec. -func (d *HashMap) GetBinaryCodec() binary.Codec { - return new(hashMapCodec) -} +// Copyright (c) Roman Atachiants and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +package nocopy + +import ( + "reflect" + "unsafe" + + "github.com/kelindar/binary" +) + +// ------------------------------------------------------------------------------ + +// String represents a type serialized in an unsafe, non portable manner. Moreover, when +// decoding it simply reuses the underlying byte array to store the data and does not +// perform a memory copy. This can be dangerous in many cases, be careful how this is used. +type String string + +// GetBinaryCodec retrieves a custom binary codec. +func (s *String) GetBinaryCodec() binary.Codec { + return new(stringCodec) +} + +// ------------------------------------------------------------------------------ + +// Bytes represents a type serialized in an unsafe, non portable manner. Moreover, when +// decoding it simply reuses the underlying byte array to store the data and does not +// perform a memory copy. This can be dangerous in many cases, be careful how this is used. +type Bytes []byte + +// GetBinaryCodec retrieves a custom binary codec. +func (s *Bytes) GetBinaryCodec() binary.Codec { + return new(byteSliceCodec) +} + +// ------------------------------------------------------------------------------ + +// Bools represents a type serialized in an unsafe, non portable manner. Moreover, when +// decoding it simply reuses the underlying byte array to store the data and does not +// perform a memory copy. This can be dangerous in many cases, be careful how this is used. +type Bools []bool + +// GetBinaryCodec retrieves a custom binary codec. +func (s *Bools) GetBinaryCodec() binary.Codec { + return new(boolSliceCodec) +} + +// ------------------------------------------------------------------------------ + +// Uint16s represents a slice serialized in an unsafe, non portable manner. +type Uint16s []uint16 + +func (s Uint16s) Len() int { return len(s) } +func (s Uint16s) Less(i, j int) bool { return s[i] < s[j] } +func (s Uint16s) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +// GetBinaryCodec retrieves a custom binary codec. +func (s *Uint16s) GetBinaryCodec() binary.Codec { + return &integerSliceCodec{ + sliceType: reflect.TypeOf(Uint16s{}), + sizeOfInt: 2, + } +} + +// ------------------------------------------------------------------------------ + +// Int16s represents a slice serialized in an unsafe, non portable manner. +type Int16s []int16 + +func (s Int16s) Len() int { return len(s) } +func (s Int16s) Less(i, j int) bool { return s[i] < s[j] } +func (s Int16s) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +// GetBinaryCodec retrieves a custom binary codec. +func (s *Int16s) GetBinaryCodec() binary.Codec { + return &integerSliceCodec{ + sliceType: reflect.TypeOf(Int16s{}), + sizeOfInt: 2, + } +} + +// ------------------------------------------------------------------------------ + +// Uint32s represents a slice serialized in an unsafe, non portable manner. +type Uint32s []uint32 + +func (s Uint32s) Len() int { return len(s) } +func (s Uint32s) Less(i, j int) bool { return s[i] < s[j] } +func (s Uint32s) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +// GetBinaryCodec retrieves a custom binary codec. +func (s *Uint32s) GetBinaryCodec() binary.Codec { + return &integerSliceCodec{ + sliceType: reflect.TypeOf(Uint32s{}), + sizeOfInt: 4, + } +} + +// ------------------------------------------------------------------------------ + +// Int32s represents a slice serialized in an unsafe, non portable manner. +type Int32s []int32 + +func (s Int32s) Len() int { return len(s) } +func (s Int32s) Less(i, j int) bool { return s[i] < s[j] } +func (s Int32s) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +// GetBinaryCodec retrieves a custom binary codec. +func (s *Int32s) GetBinaryCodec() binary.Codec { + return &integerSliceCodec{ + sliceType: reflect.TypeOf(Int32s{}), + sizeOfInt: 4, + } +} + +// ------------------------------------------------------------------------------ + +// Uint64s represents a slice serialized in an unsafe, non portable manner. +type Uint64s []uint64 + +func (s Uint64s) Len() int { return len(s) } +func (s Uint64s) Less(i, j int) bool { return s[i] < s[j] } +func (s Uint64s) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +// GetBinaryCodec retrieves a custom binary codec. +func (s *Uint64s) GetBinaryCodec() binary.Codec { + return &integerSliceCodec{ + sliceType: reflect.TypeOf(Uint64s{}), + sizeOfInt: 8, + } +} + +// ------------------------------------------------------------------------------ + +// Int64s represents a slice serialized in an unsafe, non portable manner. +type Int64s []int64 + +func (s Int64s) Len() int { return len(s) } +func (s Int64s) Less(i, j int) bool { return s[i] < s[j] } +func (s Int64s) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +// GetBinaryCodec retrieves a custom binary codec. +func (s *Int64s) GetBinaryCodec() binary.Codec { + return &integerSliceCodec{ + sliceType: reflect.TypeOf(Int64s{}), + sizeOfInt: 8, + } +} + +// ------------------------------------------------------------------------------ + +// Float32s represents a slice serialized in an unsafe, non portable manner. +type Float32s []float32 + +func (s Float32s) Len() int { return len(s) } +func (s Float32s) Less(i, j int) bool { return s[i] < s[j] } +func (s Float32s) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +// GetBinaryCodec retrieves a custom binary codec. +func (s *Float32s) GetBinaryCodec() binary.Codec { + return &integerSliceCodec{ + sliceType: reflect.TypeOf(Float32s{}), + sizeOfInt: 4, + } +} + +// ------------------------------------------------------------------------------ + +// Float64s represents a slice serialized in an unsafe, non portable manner. +type Float64s []float64 + +func (s Float64s) Len() int { return len(s) } +func (s Float64s) Less(i, j int) bool { return s[i] < s[j] } +func (s Float64s) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +// GetBinaryCodec retrieves a custom binary codec. +func (s *Float64s) GetBinaryCodec() binary.Codec { + return &integerSliceCodec{ + sliceType: reflect.TypeOf(Float64s{}), + sizeOfInt: 8, + } +} + +// ------------------------------------------------------------------------------ + +// Dictionary represents a map where both keys and values are strings. It is +// serialized in an unsafe, non portable manner. +type Dictionary map[string]string + +// GetBinaryCodec retrieves a custom binary codec. +func (d *Dictionary) GetBinaryCodec() binary.Codec { + return new(dictionaryCodec) +} + +// ------------------------------------------------------------------------------ + +// ByteMap represents a map where keys are strings but the values are slices of +// bytes. It is encoded in an unsafe, non portable mapper. +type ByteMap map[string][]byte + +// GetBinaryCodec retrieves a custom binary codec. +func (d *ByteMap) GetBinaryCodec() binary.Codec { + return new(byteMapCodec) +} + +// ------------------------------------------------------------------------------ + +// HashMap represents a map where keys are uint64 but the values are slices of +// bytes. It is encoded in an unsafe, non portable mapper. +type HashMap map[uint64][]byte + +// GetBinaryCodec retrieves a custom binary codec. +func (d *HashMap) GetBinaryCodec() binary.Codec { + return new(hashMapCodec) +} + +// ------------------------------------------------------------------------------ + +type integerSliceCodec struct { + sliceType reflect.Type + sizeOfInt int +} + +// EncodeTo encodes a value into the encoder. +func (c *integerSliceCodec) EncodeTo(e *binary.Encoder, rv reflect.Value) (err error) { + var out reflect.SliceHeader + out.Data = rv.Pointer() + out.Len = rv.Len() * c.sizeOfInt + out.Cap = out.Len + + e.WriteUint64(uint64(rv.Len() * c.sizeOfInt)) + e.Write(*(*[]byte)(unsafe.Pointer(&out))) + return +} + +// DecodeTo decodes into a reflect value from the decoder. +func (c *integerSliceCodec) DecodeTo(d *binary.Decoder, rv reflect.Value) (err error) { + var l uint64 + var b []byte + + if l, err = d.ReadUint64(); err == nil && l > 0 { + if b, err = d.Slice(int(l)); err == nil { + out := (*reflect.SliceHeader)(unsafe.Pointer(rv.UnsafeAddr())) + out.Data = (*reflect.SliceHeader)(unsafe.Pointer(&b)).Data + out.Len = int(l) / c.sizeOfInt + out.Cap = int(l) / c.sizeOfInt + } + } + return +} + +// ------------------------------------------------------------------------------ + +type byteSliceCodec struct{} + +// Encode encodes a value into the encoder. +func (c *byteSliceCodec) EncodeTo(e *binary.Encoder, rv reflect.Value) (err error) { + e.WriteUvarint(uint64(rv.Len())) + e.Write(rv.Bytes()) + return +} + +// Decode decodes into a reflect value from the decoder. +func (c *byteSliceCodec) DecodeTo(d *binary.Decoder, rv reflect.Value) (err error) { + var l uint64 + var b []byte + + if l, err = d.ReadUvarint(); err == nil && l > 0 { + if b, err = d.Slice(int(l)); err == nil { + rv.SetBytes(b) + } + } + return +} + +// ------------------------------------------------------------------------------ + +type stringCodec struct{} + +// Encode encodes a value into the encoder. +func (c *stringCodec) EncodeTo(e *binary.Encoder, rv reflect.Value) error { + v := rv.String() + e.WriteUvarint(uint64(len(v))) + e.Write(binary.ToBytes(v)) + return nil +} + +// Decode decodes into a reflect value from the decoder. +func (c *stringCodec) DecodeTo(d *binary.Decoder, rv reflect.Value) (err error) { + var l uint64 + var v []byte + + if l, err = d.ReadUvarint(); err == nil { + if v, err = d.Slice(int(l)); err == nil { + rv.SetString(binary.ToString(&v)) + } + } + return +} + +// ------------------------------------------------------------------------------ + +type boolSliceCodec struct{} + +// Encode encodes a value into the encoder. +func (c *boolSliceCodec) EncodeTo(e *binary.Encoder, rv reflect.Value) (err error) { + l := rv.Len() + e.WriteUvarint(uint64(l)) + if l > 0 { + v := rv.Interface().(Bools) + e.Write(boolsToBinary(&v)) + } + return +} + +// Decode decodes into a reflect value from the decoder. +func (c *boolSliceCodec) DecodeTo(d *binary.Decoder, rv reflect.Value) (err error) { + var l uint64 + var v []byte + + if l, err = d.ReadUvarint(); err == nil && l > 0 { + if v, err = d.Slice(int(l)); err == nil { + rv.Set(reflect.ValueOf(binaryToBools(&v))) + } + } + return +} + +// ----------------------------------------------------------------------------- + +// The codec to use for marshaling the properties +type byteMapCodec struct{} + +// Encode encodes a value into the encoder. +func (c *byteMapCodec) EncodeTo(e *binary.Encoder, rv reflect.Value) (err error) { + dict := rv.Interface().(ByteMap) + e.WriteUint16(uint16(len(dict))) + for k, v := range dict { + encodeString(e, k) + e.WriteUvarint(uint64(len(v))) + e.Write(v) + } + return +} + +// Decode decodes into a reflect value from the decoder. +func (c *byteMapCodec) DecodeTo(d *binary.Decoder, rv reflect.Value) (err error) { + var size uint16 + if size, err = d.ReadUint16(); err == nil { + dict := make(ByteMap, int(size)) + rv.Set(reflect.ValueOf(dict)) + for i := 0; i < int(size); i++ { + k, _ := decodeString(d) + var l uint64 + var b []byte + if l, err = d.ReadUvarint(); err == nil && l > 0 { + if b, err = d.Slice(int(l)); err == nil { + dict[k] = b + } + } + } + } + return +} + +// ----------------------------------------------------------------------------- + +// The codec to use for marshaling the pre-hashed hash maps +type hashMapCodec struct{} + +// Encode encodes a value into the encoder. +func (c *hashMapCodec) EncodeTo(e *binary.Encoder, rv reflect.Value) (err error) { + dict := rv.Interface().(HashMap) + e.WriteUint32(uint32(len(dict))) + for k, v := range dict { + e.WriteUint64(k) + e.WriteUint32(uint32(len(v))) + e.Write(v) + } + return +} + +// Decode decodes into a reflect value from the decoder. +func (c *hashMapCodec) DecodeTo(d *binary.Decoder, rv reflect.Value) (err error) { + var size uint32 + if size, err = d.ReadUint32(); err == nil { + dict := make(HashMap, int(size)) + rv.Set(reflect.ValueOf(dict)) + for i := 0; i < int(size); i++ { + k, _ := d.ReadUint64() + var l uint32 + var b []byte + if l, err = d.ReadUint32(); err == nil && l > 0 { + if b, err = d.Slice(int(l)); err == nil { + dict[k] = b + } + } + } + } + return +} + +// ----------------------------------------------------------------------------- + +// The codec to use for marshaling the properties +type dictionaryCodec struct{} + +// Encode encodes a value into the encoder. +func (c *dictionaryCodec) EncodeTo(e *binary.Encoder, rv reflect.Value) (err error) { + dict := rv.Interface().(Dictionary) + e.WriteUint16(uint16(len(dict))) + for k, v := range dict { + encodeString(e, k) + encodeString(e, v) + } + return +} + +// Decode decodes into a reflect value from the decoder. +func (c *dictionaryCodec) DecodeTo(d *binary.Decoder, rv reflect.Value) (err error) { + var size uint16 + if size, err = d.ReadUint16(); err == nil { + dict := make(Dictionary) + rv.Set(reflect.ValueOf(dict)) + for i := 0; i < int(size); i++ { + k, _ := decodeString(d) + v, _ := decodeString(d) + dict[k] = v + } + } + return +} + +// encodeString writes a string to the encoder +func encodeString(e *binary.Encoder, v string) { + e.WriteUvarint(uint64(len(v))) + e.Write(binary.ToBytes(v)) +} + +// decodeString reads a string from the decoder +func decodeString(d *binary.Decoder) (v string, err error) { + var l uint64 + var b []byte + if l, err = d.ReadUvarint(); err == nil { + if b, err = d.Slice(int(l)); err == nil { + v = binary.ToString(&b) + } + } + return +} diff --git a/nocopy/types_test.go b/nocopy/types_test.go index 45d5932..046c3b1 100644 --- a/nocopy/types_test.go +++ b/nocopy/types_test.go @@ -1,274 +1,174 @@ -// Copyright (c) Roman Atachiants and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for details. - -package nocopy - -import ( - "testing" - - "github.com/kelindar/binary" - "github.com/stretchr/testify/assert" -) - -type composite map[string]column - -type column struct { - Varchar columnVarchar - Float64 columnFloat64 - Float32 columnFloat32 -} - -type columnVarchar struct { - Nulls Bools - Sizes Uint32s - Bytes Bytes -} - -type columnFloat64 struct { - Nulls Bools - Floats Float64s -} - -type columnFloat32 struct { - Nulls Bools - Floats Float32s -} - -func Test_Full(t *testing.T) { - v := composite{} - v["a"] = column{ - Varchar: columnVarchar{ - Nulls: Bools{false, false, false, true, false}, - Sizes: Uint32s{2, 2, 2, 0, 2}, - Bytes: Bytes{10, 10, 10, 10, 10, 10, 10, 10}, - }, - } - v["b"] = column{ - Float64: columnFloat64{ - Nulls: Bools{false, false, false, true, false}, - Floats: Float64s{1.1, 2.2, 3.3, 0, 4.4}, - }, - } - - b, err := binary.Marshal(&v) - assert.NoError(t, err) - assert.NotNil(t, b) - - var o composite - err = binary.Unmarshal(b, &o) - assert.NoError(t, err) - assert.Equal(t, v, o) -} - -func Test_Dictionary(t *testing.T) { - v := Dictionary{ - "name": "Roman", - "race": "human", - "status": "happy", - } - - b, err := binary.Marshal(&v) - assert.NoError(t, err) - assert.NotNil(t, b) - - var o Dictionary - err = binary.Unmarshal(b, &o) - assert.NoError(t, err) - assert.Equal(t, v, o) -} - -func Test_ByteMap(t *testing.T) { - v := ByteMap{ - "name": []byte("Roman"), - "race": []byte("human"), - "status": []byte("happy"), - } - - b, err := binary.Marshal(&v) - assert.NoError(t, err) - assert.NotNil(t, b) - - var o ByteMap - err = binary.Unmarshal(b, &o) - assert.NoError(t, err) - assert.Equal(t, v, o) -} - -func Test_HashMap(t *testing.T) { - v := HashMap{ - 1: []byte("Roman"), - 2: []byte("human"), - 3: []byte("happy"), - } - - b, err := binary.Marshal(&v) - assert.NoError(t, err) - assert.NotNil(t, b) - - var o HashMap - err = binary.Unmarshal(b, &o) - assert.NoError(t, err) - assert.Equal(t, v, o) -} - -func Test_String(t *testing.T) { - v := String("ABCD") - - b, err := binary.Marshal(&v) - assert.NoError(t, err) - assert.NotNil(t, b) - - var o String - err = binary.Unmarshal(b, &o) - assert.NoError(t, err) - assert.Equal(t, v, o) -} - -func Test_Bytes(t *testing.T) { - v := Bytes([]byte("ABCD")) - - b, err := binary.Marshal(&v) - assert.NoError(t, err) - assert.NotNil(t, b) - - var o Bytes - err = binary.Unmarshal(b, &o) - assert.NoError(t, err) - assert.Equal(t, v, o) -} - -func Test_Bools(t *testing.T) { - v := Bools{true, false, true, true, false, false} - - b, err := binary.Marshal(&v) - assert.NoError(t, err) - assert.NotNil(t, b) - - var o Bools - err = binary.Unmarshal(b, &o) - assert.NoError(t, err) - assert.Equal(t, v, o) -} - -func Test_Uint16(t *testing.T) { - v := Uint16s{4, 5, 6, 1, 2, 3} - - b, err := binary.Marshal(&v) - assert.NoError(t, err) - assert.NotNil(t, b) - - var o Uint16s - err = binary.Unmarshal(b, &o) - assert.NoError(t, err) - assert.Equal(t, v, o) -} - -func Test_Int16(t *testing.T) { - v := Int16s{4, 5, 6, 1, 2, 3} - - b, err := binary.Marshal(&v) - assert.NoError(t, err) - assert.NotNil(t, b) - - var o Int16s - err = binary.Unmarshal(b, &o) - assert.NoError(t, err) - assert.Equal(t, v, o) -} - -func Test_Uint32(t *testing.T) { - v := Uint32s{4, 5, 6, 1, 2, 3} - - b, err := binary.Marshal(&v) - assert.NoError(t, err) - assert.NotNil(t, b) - - var o Uint32s - err = binary.Unmarshal(b, &o) - assert.NoError(t, err) - assert.Equal(t, v, o) -} - -func Test_Int32(t *testing.T) { - v := Int32s{4, 5, 6, 1, 2, 3} - - b, err := binary.Marshal(&v) - assert.NoError(t, err) - assert.NotNil(t, b) - - var o Int32s - err = binary.Unmarshal(b, &o) - assert.NoError(t, err) - assert.Equal(t, v, o) -} - -func Test_Uint64(t *testing.T) { - v := Uint64s{4, 5, 6, 1, 2, 3} - - b, err := binary.Marshal(&v) - assert.NoError(t, err) - assert.NotNil(t, b) - - var o Uint64s - err = binary.Unmarshal(b, &o) - assert.NoError(t, err) - assert.Equal(t, v, o) -} - -func Test_Int64(t *testing.T) { - v := Int64s{4, 5, 6, 1, 2, 3} - - b, err := binary.Marshal(&v) - assert.NoError(t, err) - assert.NotNil(t, b) - - var o Int64s - err = binary.Unmarshal(b, &o) - assert.NoError(t, err) - assert.Equal(t, v, o) -} - -func Test_Float32(t *testing.T) { - v := Float32s{4.5, 5.01, 6.61, 1.12, 2.1, 3} - - b, err := binary.Marshal(&v) - assert.NoError(t, err) - assert.NotNil(t, b) - - var o Float32s - err = binary.Unmarshal(b, &o) - assert.NoError(t, err) - assert.Equal(t, v, o) -} - -func Test_Float64(t *testing.T) { - v := Float64s{4.5, 5.01, 6.61, 1.12, 2.1, 3} - - b, err := binary.Marshal(&v) - assert.NoError(t, err) - assert.NotNil(t, b) - - var o Float64s - err = binary.Unmarshal(b, &o) - assert.NoError(t, err) - assert.Equal(t, v, o) -} - -type nested struct { - Numbers Uint64s -} - -func Test_NestedUint64(t *testing.T) { - v := nested{ - Numbers: Uint64s{4, 5, 6, 1, 2, 3}, - } - - b, err := binary.Marshal(&v) - assert.NoError(t, err) - assert.NotNil(t, b) - - var o nested - err = binary.Unmarshal(b, &o) - assert.NoError(t, err) - assert.Equal(t, v, o) -} +// Copyright (c) Roman Atachiants and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +package nocopy + +import ( + "testing" + + "github.com/kelindar/binary" + "github.com/stretchr/testify/assert" +) + +type composite map[string]column + +type column struct { + Varchar columnVarchar + Float64 columnFloat64 + Float32 columnFloat32 +} + +type columnVarchar struct { + Nulls Bools + Sizes Uint32s + Bytes Bytes +} + +type columnFloat64 struct { + Nulls Bools + Floats Float64s +} + +type columnFloat32 struct { + Nulls Bools + Floats Float32s +} + +type nested struct { + Numbers Uint64s +} + +func TestTypes(t *testing.T) { + tests := map[string]struct { + value interface{} + out interface{} + }{ + "composite": { + value: composite{ + "a": column{ + Varchar: columnVarchar{ + Nulls: Bools{false, false, false, true, false}, + Sizes: Uint32s{2, 2, 2, 0, 2}, + Bytes: Bytes{10, 10, 10, 10, 10, 10, 10, 10}, + }, + }, + "b": column{ + Float64: columnFloat64{ + Nulls: Bools{false, false, false, true, false}, + Floats: Float64s{1.1, 2.2, 3.3, 0, 4.4}, + }, + }, + }, + out: &composite{}, + }, + "dictionary": { + value: Dictionary{"name": "Roman", "race": "human", "status": "happy"}, + out: &Dictionary{}, + }, + "bytemap": { + value: ByteMap{"name": []byte("Roman"), "race": []byte("human"), "status": []byte("happy")}, + out: &ByteMap{}, + }, + "hashmap": { + value: HashMap{1: []byte("Roman"), 2: []byte("human"), 3: []byte("happy")}, + out: &HashMap{}, + }, + "string": { + value: String("ABCD"), + out: new(String), + }, + "bytes": { + value: Bytes([]byte("ABCD")), + out: new(Bytes), + }, + "bools": { + value: Bools{true, false, true, true, false, false}, + out: new(Bools), + }, + "uint16": { + value: Uint16s{4, 5, 6, 1, 2, 3}, + out: new(Uint16s), + }, + "int16": { + value: Int16s{4, 5, 6, 1, 2, 3}, + out: new(Int16s), + }, + "uint32": { + value: Uint32s{4, 5, 6, 1, 2, 3}, + out: new(Uint32s), + }, + "int32": { + value: Int32s{4, 5, 6, 1, 2, 3}, + out: new(Int32s), + }, + "uint64": { + value: Uint64s{4, 5, 6, 1, 2, 3}, + out: new(Uint64s), + }, + "int64": { + value: Int64s{4, 5, 6, 1, 2, 3}, + out: new(Int64s), + }, + "float32": { + value: Float32s{4.5, 5.01, 6.61, 1.12, 2.1, 3}, + out: new(Float32s), + }, + "float64": { + value: Float64s{4.5, 5.01, 6.61, 1.12, 2.1, 3}, + out: new(Float64s), + }, + "nested uint64": { + value: nested{Numbers: Uint64s{4, 5, 6, 1, 2, 3}}, + out: &nested{}, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + b, err := binary.Marshal(tc.value) + assert.NoError(t, err) + assert.NotNil(t, b) + assert.NoError(t, binary.Unmarshal(b, tc.out)) + assert.Equal(t, tc.value, deref(tc.out)) + }) + } +} + +func deref(v interface{}) interface{} { + switch x := v.(type) { + case *composite: + return *x + case *Dictionary: + return *x + case *ByteMap: + return *x + case *HashMap: + return *x + case *String: + return *x + case *Bytes: + return *x + case *Bools: + return *x + case *Uint16s: + return *x + case *Int16s: + return *x + case *Uint32s: + return *x + case *Int32s: + return *x + case *Uint64s: + return *x + case *Int64s: + return *x + case *Float32s: + return *x + case *Float64s: + return *x + case *nested: + return *x + default: + return v + } +} diff --git a/scanner.go b/scanner.go index 235801a..5257dc1 100644 --- a/scanner.go +++ b/scanner.go @@ -56,149 +56,123 @@ func scanType(t reflect.Type) (Codec, error) { if custom, ok := scanCustomCodec(t); ok { return custom, nil } - if custom, ok := scanBinaryMarshaler(t); ok { return custom, nil } switch t.Kind() { case reflect.Ptr: - elemCodec, err := scanType(t.Elem()) - if err != nil { - return nil, err + return scanPointer(t) + case reflect.Array: + return scanArray(t) + case reflect.Slice: + return scanSlice(t) + case reflect.Struct: + return scanStructCodec(t) + case reflect.Map: + return scanMap(t) + default: + if c := scanPrimitive(t.Kind()); c != nil { + return c, nil } + return nil, errors.New("binary: unsupported type " + t.String()) + } +} - return &reflectPointerCodec{ - elemCodec: elemCodec, - }, nil +func scanPointer(t reflect.Type) (Codec, error) { + elemCodec, err := scanType(t.Elem()) + if err != nil { + return nil, err + } + return &reflectPointerCodec{elemCodec: elemCodec}, nil +} - case reflect.Array: - elemCodec, err := scanType(t.Elem()) +func scanArray(t reflect.Type) (Codec, error) { + elemCodec, err := scanType(t.Elem()) + if err != nil { + return nil, err + } + return &reflectArrayCodec{elemCodec: elemCodec}, nil +} + +func scanSlice(t reflect.Type) (Codec, error) { + switch t.Elem().Kind() { + case reflect.Uint8: + return new(byteSliceCodec), nil + case reflect.Bool: + return new(boolSliceCodec), nil + case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return new(varuintSliceCodec), nil + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return new(varintSliceCodec), nil + case reflect.Ptr: + elemCodec, err := scanType(t.Elem().Elem()) if err != nil { return nil, err } - - return &reflectArrayCodec{ + return &reflectSliceOfPtrCodec{ + elemType: t.Elem().Elem(), elemCodec: elemCodec, }, nil - - case reflect.Slice: - - // Fast-paths for simple numeric slices and string slices - switch t.Elem().Kind() { - case reflect.Uint8: - return new(byteSliceCodec), nil - case reflect.Bool: - return new(boolSliceCodec), nil - case reflect.Uint: - fallthrough - case reflect.Uint16: - fallthrough - case reflect.Uint32: - fallthrough - case reflect.Uint64: - return new(varuintSliceCodec), nil - case reflect.Int: - fallthrough - case reflect.Int8: - fallthrough - case reflect.Int16: - fallthrough - case reflect.Int32: - fallthrough - case reflect.Int64: - return new(varintSliceCodec), nil - case reflect.Ptr: - elemCodec, err := scanType(t.Elem().Elem()) - if err != nil { - return nil, err - } - - return &reflectSliceOfPtrCodec{ - elemType: t.Elem().Elem(), - elemCodec: elemCodec, - }, nil - default: - elemCodec, err := scanType(t.Elem()) - if err != nil { - return nil, err - } - - return &reflectSliceCodec{ - elemCodec: elemCodec, - }, nil - } - - case reflect.Struct: - s := scanStruct(t) - v := make(reflectStructCodec, 0, len(s.fields)) - for _, i := range s.fields { - field := t.Field(i) - codec, err := scanType(field.Type) - if err != nil { - return nil, err - } - - // Append since unexported fields are skipped - v = append(v, fieldCodec{ - Index: i, - Codec: codec, - }) - } - - return &v, nil - - case reflect.Map: - key, err := scanType(t.Key()) + default: + elemCodec, err := scanType(t.Elem()) if err != nil { return nil, err } + return &reflectSliceCodec{elemCodec: elemCodec}, nil + } +} - val, err := scanType(t.Elem()) +func scanStructCodec(t reflect.Type) (Codec, error) { + s := scanStruct(t) + v := make(reflectStructCodec, 0, len(s.fields)) + for _, i := range s.fields { + field := t.Field(i) + codec, err := scanType(field.Type) if err != nil { return nil, err } + v = append(v, fieldCodec{ + Index: i, + Codec: codec, + }) + } + return &v, nil +} - return &reflectMapCodec{ - key: key, - val: val, - }, nil +func scanMap(t reflect.Type) (Codec, error) { + key, err := scanType(t.Key()) + if err != nil { + return nil, err + } + val, err := scanType(t.Elem()) + if err != nil { + return nil, err + } + return &reflectMapCodec{key: key, val: val}, nil +} +func scanPrimitive(kind reflect.Kind) Codec { + switch kind { case reflect.String: - return new(stringCodec), nil + return new(stringCodec) case reflect.Bool: - return new(boolCodec), nil - case reflect.Int8: - fallthrough - case reflect.Int16: - fallthrough - case reflect.Int32: - fallthrough - case reflect.Int: - fallthrough - case reflect.Int64: - return new(varintCodec), nil - case reflect.Uint8: - fallthrough - case reflect.Uint16: - fallthrough - case reflect.Uint32: - fallthrough - case reflect.Uint: - fallthrough - case reflect.Uint64: - return new(varuintCodec), nil + return new(boolCodec) + case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int, reflect.Int64: + return new(varintCodec) + case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint, reflect.Uint64: + return new(varuintCodec) case reflect.Complex64: - return new(complex64Codec), nil + return new(complex64Codec) case reflect.Complex128: - return new(complex128Codec), nil + return new(complex128Codec) case reflect.Float32: - return new(float32Codec), nil + return new(float32Codec) case reflect.Float64: - return new(float64Codec), nil + return new(float64Codec) + default: + return nil } - - return nil, errors.New("binary: unsupported type " + t.String()) } type scannedStruct struct { diff --git a/sorted/types_test.go b/sorted/types_test.go index 4809bf4..0f16383 100644 --- a/sorted/types_test.go +++ b/sorted/types_test.go @@ -10,93 +10,69 @@ import ( "github.com/stretchr/testify/assert" ) -func Test_Uint16(t *testing.T) { - v := Uint16s{4, 5, 6, 1, 2, 3} - - b, err := binary.Marshal(&v) - assert.NoError(t, err) - assert.NotNil(t, b) - - var o Uint16s - err = binary.Unmarshal(b, &o) - assert.NoError(t, err) - assert.Equal(t, v, o) -} - -func Test_Int16(t *testing.T) { - v := Int16s{4, 5, 6, 1, 2, 3} - - b, err := binary.Marshal(&v) - assert.NoError(t, err) - assert.NotNil(t, b) - - var o Int16s - err = binary.Unmarshal(b, &o) - assert.NoError(t, err) - assert.Equal(t, v, o) -} - -func Test_Uint32(t *testing.T) { - v := Uint32s{4, 5, 6, 1, 2, 3} - - b, err := binary.Marshal(&v) - assert.NoError(t, err) - assert.NotNil(t, b) - - var o Uint32s - err = binary.Unmarshal(b, &o) - assert.NoError(t, err) - assert.Equal(t, v, o) -} - -func Test_Int32(t *testing.T) { - v := Int32s{4, 5, 6, 1, 2, 3} - - b, err := binary.Marshal(&v) - assert.NoError(t, err) - assert.NotNil(t, b) - - var o Int32s - err = binary.Unmarshal(b, &o) - assert.NoError(t, err) - assert.Equal(t, v, o) -} - -func Test_Uint64(t *testing.T) { - v := Uint64s{4, 5, 6, 1, 2, 3} - - b, err := binary.Marshal(&v) - assert.NoError(t, err) - assert.NotNil(t, b) - - var o Uint64s - err = binary.Unmarshal(b, &o) - assert.NoError(t, err) - assert.Equal(t, v, o) +func TestTypes(t *testing.T) { + tests := map[string]struct { + value interface{} + out interface{} + }{ + "uint16": { + value: Uint16s{4, 5, 6, 1, 2, 3}, + out: new(Uint16s), + }, + "int16": { + value: Int16s{4, 5, 6, 1, 2, 3}, + out: new(Int16s), + }, + "uint32": { + value: Uint32s{4, 5, 6, 1, 2, 3}, + out: new(Uint32s), + }, + "int32": { + value: Int32s{4, 5, 6, 1, 2, 3}, + out: new(Int32s), + }, + "uint64": { + value: Uint64s{4, 5, 6, 1, 2, 3}, + out: new(Uint64s), + }, + "int64": { + value: Int64s{4, 5, 6, 1, 2, 3}, + out: new(Int64s), + }, + "timestamps": { + value: Timestamps{4, 5, 6, 1, 2, 3}, + out: new(Timestamps), + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + b, err := binary.Marshal(tc.value) + assert.NoError(t, err) + assert.NotNil(t, b) + assert.NoError(t, binary.Unmarshal(b, tc.out)) + assert.Equal(t, tc.value, deref(tc.out)) + }) + } } -func Test_Int64(t *testing.T) { - v := Int64s{4, 5, 6, 1, 2, 3} - - b, err := binary.Marshal(&v) - assert.NoError(t, err) - assert.NotNil(t, b) - - var o Int64s - err = binary.Unmarshal(b, &o) - assert.NoError(t, err) - assert.Equal(t, v, o) -} - -func Test_Timestamps(t *testing.T) { - v := Timestamps{4, 5, 6, 1, 2, 3} - - b, err := binary.Marshal(&v) - assert.NoError(t, err) - assert.NotNil(t, b) - - var o Timestamps - err = binary.Unmarshal(b, &o) - assert.NoError(t, err) - assert.Equal(t, v, o) +func deref(v interface{}) interface{} { + switch x := v.(type) { + case *Uint16s: + return *x + case *Int16s: + return *x + case *Uint32s: + return *x + case *Int32s: + return *x + case *Uint64s: + return *x + case *Int64s: + return *x + case *Timestamps: + return *x + default: + return v + } } diff --git a/unsafe/codecs.go b/unsafe/codecs.go deleted file mode 100644 index aac815a..0000000 --- a/unsafe/codecs.go +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) Roman Atachiants and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for details. - -package unsafe - -import ( - "reflect" - "unsafe" - - "github.com/kelindar/binary" -) - -type integerSliceCodec struct { - sliceType reflect.Type - sizeOfInt int -} - -// EncodeTo encodes a value into the encoder. -func (c *integerSliceCodec) EncodeTo(e *binary.Encoder, rv reflect.Value) (err error) { - var out reflect.SliceHeader - out.Data = rv.Pointer() - out.Len = rv.Len() * c.sizeOfInt - out.Cap = out.Len - - e.WriteUint64(uint64(rv.Len())) - e.Write(*(*[]byte)(unsafe.Pointer(&out))) - return -} - -// DecodeTo decodes into a reflect value from the decoder. -func (c *integerSliceCodec) DecodeTo(d *binary.Decoder, rv reflect.Value) (err error) { - var l uint64 - if l, err = d.ReadUint64(); err == nil && l > 0 { - src := reflect.MakeSlice(c.sliceType, int(l), int(l)) - - var out reflect.SliceHeader - out.Data = src.Pointer() - out.Len = int(l) * c.sizeOfInt - out.Cap = int(l) * c.sizeOfInt - data := *(*[]byte)(unsafe.Pointer(&out)) - if _, err = d.Read(data); err == nil { - rv.Set(src) - } - } - return -} diff --git a/unsafe/types.go b/unsafe/types.go index b82d8f4..9053787 100644 --- a/unsafe/types.go +++ b/unsafe/types.go @@ -5,6 +5,7 @@ package unsafe import ( "reflect" + "unsafe" "github.com/kelindar/binary" ) @@ -157,3 +158,40 @@ func (s *Float64s) GetBinaryCodec() binary.Codec { sizeOfInt: 8, } } + +// ------------------------------------------------------------------------------ + +type integerSliceCodec struct { + sliceType reflect.Type + sizeOfInt int +} + +// EncodeTo encodes a value into the encoder. +func (c *integerSliceCodec) EncodeTo(e *binary.Encoder, rv reflect.Value) (err error) { + var out reflect.SliceHeader + out.Data = rv.Pointer() + out.Len = rv.Len() * c.sizeOfInt + out.Cap = out.Len + + e.WriteUint64(uint64(rv.Len())) + e.Write(*(*[]byte)(unsafe.Pointer(&out))) + return +} + +// DecodeTo decodes into a reflect value from the decoder. +func (c *integerSliceCodec) DecodeTo(d *binary.Decoder, rv reflect.Value) (err error) { + var l uint64 + if l, err = d.ReadUint64(); err == nil && l > 0 { + src := reflect.MakeSlice(c.sliceType, int(l), int(l)) + + var out reflect.SliceHeader + out.Data = src.Pointer() + out.Len = int(l) * c.sizeOfInt + out.Cap = int(l) * c.sizeOfInt + data := *(*[]byte)(unsafe.Pointer(&out)) + if _, err = d.Read(data); err == nil { + rv.Set(src) + } + } + return +} diff --git a/unsafe/types_test.go b/unsafe/types_test.go index 66033f8..fdfde9e 100644 --- a/unsafe/types_test.go +++ b/unsafe/types_test.go @@ -10,119 +10,81 @@ import ( "github.com/stretchr/testify/assert" ) -func Test_Bools(t *testing.T) { - v := Bools{true, false, true, true, false, false} - - b, err := binary.Marshal(&v) - assert.NoError(t, err) - assert.NotNil(t, b) - - var o Bools - err = binary.Unmarshal(b, &o) - assert.NoError(t, err) - assert.Equal(t, v, o) -} - -func Test_Uint16(t *testing.T) { - v := Uint16s{4, 5, 6, 1, 2, 3} - - b, err := binary.Marshal(&v) - assert.NoError(t, err) - assert.NotNil(t, b) - - var o Uint16s - err = binary.Unmarshal(b, &o) - assert.NoError(t, err) - assert.Equal(t, v, o) -} - -func Test_Int16(t *testing.T) { - v := Int16s{4, 5, 6, 1, 2, 3} - - b, err := binary.Marshal(&v) - assert.NoError(t, err) - assert.NotNil(t, b) - - var o Int16s - err = binary.Unmarshal(b, &o) - assert.NoError(t, err) - assert.Equal(t, v, o) -} - -func Test_Uint32(t *testing.T) { - v := Uint32s{4, 5, 6, 1, 2, 3} - - b, err := binary.Marshal(&v) - assert.NoError(t, err) - assert.NotNil(t, b) - - var o Uint32s - err = binary.Unmarshal(b, &o) - assert.NoError(t, err) - assert.Equal(t, v, o) -} - -func Test_Int32(t *testing.T) { - v := Int32s{4, 5, 6, 1, 2, 3} - - b, err := binary.Marshal(&v) - assert.NoError(t, err) - assert.NotNil(t, b) - - var o Int32s - err = binary.Unmarshal(b, &o) - assert.NoError(t, err) - assert.Equal(t, v, o) -} - -func Test_Uint64(t *testing.T) { - v := Uint64s{4, 5, 6, 1, 2, 3} - - b, err := binary.Marshal(&v) - assert.NoError(t, err) - assert.NotNil(t, b) - - var o Uint64s - err = binary.Unmarshal(b, &o) - assert.NoError(t, err) - assert.Equal(t, v, o) -} - -func Test_Int64(t *testing.T) { - v := Int64s{4, 5, 6, 1, 2, 3} - - b, err := binary.Marshal(&v) - assert.NoError(t, err) - assert.NotNil(t, b) - - var o Int64s - err = binary.Unmarshal(b, &o) - assert.NoError(t, err) - assert.Equal(t, v, o) -} - -func Test_Float32(t *testing.T) { - v := Float32s{4.5, 5.01, 6.61, 1.12, 2.1, 3} - - b, err := binary.Marshal(&v) - assert.NoError(t, err) - assert.NotNil(t, b) - - var o Float32s - err = binary.Unmarshal(b, &o) - assert.NoError(t, err) - assert.Equal(t, v, o) +func TestTypes(t *testing.T) { + tests := map[string]struct { + value interface{} + out interface{} + }{ + "bools": { + value: Bools{true, false, true, true, false, false}, + out: new(Bools), + }, + "uint16": { + value: Uint16s{4, 5, 6, 1, 2, 3}, + out: new(Uint16s), + }, + "int16": { + value: Int16s{4, 5, 6, 1, 2, 3}, + out: new(Int16s), + }, + "uint32": { + value: Uint32s{4, 5, 6, 1, 2, 3}, + out: new(Uint32s), + }, + "int32": { + value: Int32s{4, 5, 6, 1, 2, 3}, + out: new(Int32s), + }, + "uint64": { + value: Uint64s{4, 5, 6, 1, 2, 3}, + out: new(Uint64s), + }, + "int64": { + value: Int64s{4, 5, 6, 1, 2, 3}, + out: new(Int64s), + }, + "float32": { + value: Float32s{4.5, 5.01, 6.61, 1.12, 2.1, 3}, + out: new(Float32s), + }, + "float64": { + value: Float64s{4.5, 5.01, 6.61, 1.12, 2.1, 3}, + out: new(Float64s), + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + b, err := binary.Marshal(tc.value) + assert.NoError(t, err) + assert.NotNil(t, b) + assert.NoError(t, binary.Unmarshal(b, tc.out)) + assert.Equal(t, tc.value, deref(tc.out)) + }) + } } -func Test_Float64(t *testing.T) { - v := Float64s{4.5, 5.01, 6.61, 1.12, 2.1, 3} - - b, err := binary.Marshal(&v) - assert.NoError(t, err) - assert.NotNil(t, b) - - var o Float64s - err = binary.Unmarshal(b, &o) - assert.NoError(t, err) - assert.Equal(t, v, o) +func deref(v interface{}) interface{} { + switch x := v.(type) { + case *Bools: + return *x + case *Uint16s: + return *x + case *Int16s: + return *x + case *Uint32s: + return *x + case *Int32s: + return *x + case *Uint64s: + return *x + case *Int64s: + return *x + case *Float32s: + return *x + case *Float64s: + return *x + default: + return v + } } From 46b330104e3404da7cc448bef6ee32ed3b6e6b8f Mon Sep 17 00:00:00 2001 From: Roman Atachiants Date: Mon, 20 Jul 2026 20:06:31 +0100 Subject: [PATCH 2/2] Drop gocognit from the CI workflow Cognitive complexity checks aren't needed for this library. Co-authored-by: Cursor --- .github/workflows/test.yml | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 51111d5..7e8f5e3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,6 @@ permissions: env: GO111MODULE: "on" - GOCOGNIT_MAX: "20" COVER_FAIL_UNDER: "75" jobs: @@ -39,16 +38,6 @@ jobs: go test -race -count=1 -covermode=atomic -coverprofile=coverage/go \ $(go list ./... | grep -v -E '/bench$') - - name: Check cognitive complexity - run: | - go install github.com/uudashr/gocognit/cmd/gocognit@v1.2.1 - echo "### Cognitive complexity (max ${GOCOGNIT_MAX})" >> "$GITHUB_STEP_SUMMARY" - gocognit -ignore "_test|testdata|bench" -avg . | tee -a "$GITHUB_STEP_SUMMARY" - echo '```' >> "$GITHUB_STEP_SUMMARY" - gocognit -ignore "_test|testdata|bench" -top 10 . | tee -a "$GITHUB_STEP_SUMMARY" - echo '```' >> "$GITHUB_STEP_SUMMARY" - gocognit -ignore "_test|testdata|bench" -over "${GOCOGNIT_MAX}" . - - name: Upload coverage to Coveralls uses: shogo82148/actions-goveralls@v1 with: