Skip to content

Commit eba2c75

Browse files
authored
Merge pull request #37 from Basekick-Labs/fix/float32-double-narrowing
Fix/float32 double narrowing
2 parents 19c91df + fbe1260 commit eba2c75

15 files changed

Lines changed: 296 additions & 54 deletions

.github/workflows/build.yml

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,26 @@ name: Go
22

33
on:
44
push:
5-
branches: [v5]
5+
branches: [v6]
66
pull_request:
7-
branches: [v5]
7+
branches: [v6]
88

99
jobs:
1010
build:
11-
name: build
11+
name: test (Go ${{ matrix.go-version }})
1212
runs-on: ubuntu-latest
1313
strategy:
1414
matrix:
15-
go-version: [1.21.x]
15+
go-version: ['1.25.x', '1.26.x']
1616

1717
steps:
18-
- name: Set up ${{ matrix.go-version }}
19-
uses: actions/setup-go@v2
18+
- name: Checkout code
19+
uses: actions/checkout@v4
20+
21+
- name: Set up Go ${{ matrix.go-version }}
22+
uses: actions/setup-go@v5
2023
with:
2124
go-version: ${{ matrix.go-version }}
2225

23-
- name: Checkout code
24-
uses: actions/checkout@v2
25-
2626
- name: Test
2727
run: make test

.github/workflows/commitlint.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ jobs:
55
commitlint:
66
runs-on: ubuntu-latest
77
steps:
8-
- uses: actions/checkout@v2
8+
- uses: actions/checkout@v4
99
with:
1010
fetch-depth: 0
11-
- uses: wagoid/commitlint-github-action@v4
11+
- uses: wagoid/commitlint-github-action@v6

.github/workflows/release.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ jobs:
99
build:
1010
runs-on: ubuntu-latest
1111
steps:
12-
- uses: actions/checkout@v2
12+
- uses: actions/checkout@v4
1313
- uses: ncipollo/release-action@v1
1414
with:
1515
body:
1616
Please refer to
17-
[CHANGELOG.md](https://github.com/vmihailenco/msgpack/blob/v5/CHANGELOG.md) for details
17+
[CHANGELOG.md](https://github.com/Basekick-Labs/msgpack/blob/v6/CHANGELOG.md) for details

CHANGELOG.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,29 @@
1+
## v6 (Basekick-Labs fork)
2+
3+
### Performance
4+
5+
- **decode:** zero-allocation byte-slice reader for `Unmarshal()` — replaces `bytes.NewReader` + `bufio.NewReader` with a direct `byteSliceReader`, eliminating 2 allocations per call (~21% faster decode, ~50% less memory)
6+
- **decode:** `*interface{}` fast path in `Decode()` — skips `reflect.ValueOf` for the most common `Unmarshal(b, &interface{})` pattern (~14% faster)
7+
- **encode:** pooled byte buffer in `Marshal()` — replaces per-call `bytes.Buffer` with a reusable `[]byte` embedded in the pooled `Encoder` struct
8+
- **encode:** `byteSliceWriter` for `Marshal()` path — native `WriteByte` implementation eliminates per-byte heap allocation
9+
- **encode:** `byteWriter.WriteByte` scratch fix for streaming path — uses `[1]byte` scratch instead of allocating `[]byte{c}`
10+
- **encode:** `Encode()` fast paths for `map[string]interface{}` and `[]interface{}` — bypasses `reflect.ValueOf` + sync.Map encoder lookup
11+
12+
### Bug Fixes
13+
14+
- **decode:** cap `decodeSlice()` allocation at `sliceAllocLimit` (1M) to prevent OOM from malicious payloads ([#1](https://github.com/Basekick-Labs/msgpack/issues/1))
15+
- **decode:** cap `DecodeMap()` allocation at `maxMapSize` (1M) — same OOM vector for `map[string]interface{}` path
16+
- **decode:** fix `disableAllocLimitFlag` check in `decodeSliceValue``!= 1` was always true because the flag value is `1 << 3 = 8`, so the alloc limit in `growSliceValue()` was never applied
17+
- **decode:** fix error message in `DecodeFloat64` — said "decoding float32" instead of "decoding float64" ([#13](https://github.com/Basekick-Labs/msgpack/issues/13))
18+
19+
### Chores
20+
21+
- Modernize GitHub Actions (checkout@v4, setup-go@v5)
22+
- Go version matrix: 1.25.x, 1.26.x
23+
- Bump `go.mod` to Go 1.26
24+
25+
---
26+
127
## [5.4.1](https://github.com/vmihailenco/msgpack/compare/v5.4.0...v5.4.1) (2023-10-26)
228

329

README.md

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,33 @@
11
# MessagePack encoding for Golang
22

3-
[![Build Status](https://travis-ci.org/vmihailenco/msgpack.svg)](https://travis-ci.org/vmihailenco/msgpack)
3+
[![Build Status](https://github.com/Basekick-Labs/msgpack/actions/workflows/build.yml/badge.svg?branch=v6)](https://github.com/Basekick-Labs/msgpack/actions/workflows/build.yml)
44
[![PkgGoDev](https://pkg.go.dev/badge/github.com/vmihailenco/msgpack/v5)](https://pkg.go.dev/github.com/vmihailenco/msgpack/v5)
5-
[![Documentation](https://img.shields.io/badge/msgpack-documentation-informational)](https://msgpack.uptrace.dev/)
6-
[![Chat](https://discordapp.com/api/guilds/752070105847955518/widget.png)](https://discord.gg/rWtp5Aj)
5+
[![Discord](https://img.shields.io/badge/discord-chat-5865F2?logo=discord&logoColor=white)](https://discord.gg/nxnWfUxsdm)
76

8-
> msgpack is brought to you by :star: [**uptrace/uptrace**](https://github.com/uptrace/uptrace).
9-
> Uptrace is an [open source APM](https://uptrace.dev/get/open-source-apm.html) and blazingly fast
10-
> [distributed tracing tool](https://get.uptrace.dev/compare/distributed-tracing-tools.html) powered
11-
> by OpenTelemetry and ClickHouse. Give it a star as well!
7+
> A performance-optimized fork of [vmihailenco/msgpack/v5](https://github.com/vmihailenco/msgpack),
8+
> maintained by [Basekick Labs](https://github.com/Basekick-Labs). Built for
9+
> [Arc](https://github.com/Basekick-Labs/arc), a high-performance time-series database.
10+
> The upstream module path is preserved for drop-in compatibility.
11+
12+
## What's New in v6
13+
14+
**Decode**~21% faster, ~50% less memory:
15+
- Zero-allocation byte-slice reader for `Unmarshal()`
16+
- `*interface{}` fast path bypasses reflect for the most common decode pattern
17+
18+
**Encode**~12% faster, ~43% fewer allocations:
19+
- Pooled byte buffer in `Marshal()` eliminates per-call `bytes.Buffer`
20+
- Native `WriteByte` on the Marshal path removes per-byte heap allocations
21+
- Type-switch fast paths for `map[string]interface{}` and `[]interface{}`
22+
23+
**Security:**
24+
- OOM protection: slice and map allocations from untrusted input are capped at 1M elements
25+
26+
See [CHANGELOG.md](CHANGELOG.md) for full details.
1227

1328
## Resources
1429

15-
- [Documentation](https://msgpack.uptrace.dev)
16-
- [Chat](https://discord.gg/rWtp5Aj)
30+
- [Discord](https://discord.gg/nxnWfUxsdm)
1731
- [Reference](https://pkg.go.dev/github.com/vmihailenco/msgpack/v5)
1832
- [Examples](https://pkg.go.dev/github.com/vmihailenco/msgpack/v5#pkg-examples)
1933

@@ -53,7 +67,7 @@ msgpack supports 2 last Go versions and requires support for
5367
go mod init github.com/my/repo
5468
```
5569

56-
And then install msgpack/v5 (note _v5_ in the import; omitting it is a popular mistake):
70+
And then install msgpack (the module path is unchanged from upstream for drop-in compatibility):
5771

5872
```shell
5973
go get github.com/vmihailenco/msgpack/v5
@@ -84,17 +98,10 @@ func ExampleMarshal() {
8498
}
8599
```
86100

87-
## See also
88-
89-
- [Golang ORM](https://github.com/uptrace/bun) for PostgreSQL, MySQL, MSSQL, and SQLite
90-
- [Golang PostgreSQL](https://bun.uptrace.dev/postgres/)
91-
- [Golang HTTP router](https://github.com/uptrace/bunrouter)
92-
- [Golang ClickHouse ORM](https://github.com/uptrace/go-clickhouse)
93-
94101
## Contributors
95102

96103
Thanks to all the people who already contributed!
97104

98-
<a href="https://github.com/vmihailenco/msgpack/graphs/contributors">
99-
<img src="https://contributors-img.web.app/image?repo=vmihailenco/msgpack" />
105+
<a href="https://github.com/Basekick-Labs/msgpack/graphs/contributors">
106+
<img src="https://contributors-img.web.app/image?repo=Basekick-Labs/msgpack" />
100107
</a>

byteslice_reader.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package msgpack
2+
3+
import (
4+
"errors"
5+
"io"
6+
)
7+
8+
// byteSliceReader implements bufReader (io.Reader + io.ByteScanner) for
9+
// zero-allocation decoding from a []byte. When Unmarshal is called with
10+
// a complete byte slice, this avoids the overhead of bytes.NewReader +
11+
// bufio.NewReader (2 allocations + 4KB buffer + interface dispatch per byte).
12+
type byteSliceReader struct {
13+
data []byte
14+
pos int
15+
}
16+
17+
func (r *byteSliceReader) reset(data []byte) {
18+
r.data = data
19+
r.pos = 0
20+
}
21+
22+
func (r *byteSliceReader) ReadByte() (byte, error) {
23+
if r.pos >= len(r.data) {
24+
return 0, io.EOF
25+
}
26+
c := r.data[r.pos]
27+
r.pos++
28+
return c, nil
29+
}
30+
31+
func (r *byteSliceReader) UnreadByte() error {
32+
if r.pos <= 0 {
33+
return errors.New("msgpack: at beginning of input")
34+
}
35+
r.pos--
36+
return nil
37+
}
38+
39+
func (r *byteSliceReader) Read(p []byte) (int, error) {
40+
if r.pos >= len(r.data) {
41+
return 0, io.EOF
42+
}
43+
n := copy(p, r.data[r.pos:])
44+
r.pos += n
45+
return n, nil
46+
}

byteslice_writer.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package msgpack
2+
3+
// byteSliceWriter is a zero-allocation writer that appends directly to a
4+
// []byte buffer. It implements the writer interface (io.Writer + WriteByte).
5+
// Used by Marshal() to avoid allocating a bytes.Buffer on every call.
6+
type byteSliceWriter struct {
7+
buf *[]byte
8+
}
9+
10+
func (w *byteSliceWriter) Write(p []byte) (int, error) {
11+
*w.buf = append(*w.buf, p...)
12+
return len(p), nil
13+
}
14+
15+
func (w *byteSliceWriter) WriteByte(c byte) error {
16+
*w.buf = append(*w.buf, c)
17+
return nil
18+
}

decode.go

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package msgpack
22

33
import (
44
"bufio"
5-
"bytes"
65
"errors"
76
"fmt"
87
"io"
@@ -46,6 +45,10 @@ func GetDecoder() *Decoder {
4645
func PutDecoder(dec *Decoder) {
4746
dec.r = nil
4847
dec.s = nil
48+
dec.bsr.data = nil
49+
if dec.buf != nil {
50+
dec.buf = dec.buf[:0]
51+
}
4952
decPool.Put(dec)
5053
}
5154

@@ -56,7 +59,7 @@ func PutDecoder(dec *Decoder) {
5659
func Unmarshal(data []byte, v interface{}) error {
5760
dec := GetDecoder()
5861
dec.UsePreallocateValues(true)
59-
dec.Reset(bytes.NewReader(data))
62+
dec.ResetBytes(data)
6063
err := dec.Decode(v)
6164

6265
PutDecoder(dec)
@@ -68,6 +71,7 @@ func Unmarshal(data []byte, v interface{}) error {
6871
type Decoder struct {
6972
r io.Reader
7073
s io.ByteScanner
74+
bsr byteSliceReader
7175
mapDecoder func(*Decoder) (interface{}, error)
7276
structTag string
7377
buf []byte
@@ -126,6 +130,19 @@ func (d *Decoder) ResetReader(r io.Reader) {
126130
}
127131
}
128132

133+
// ResetBytes is like Reset but optimized for decoding from a byte slice.
134+
// It avoids allocating bytes.NewReader and bufio.NewReader by using an
135+
// embedded byte-slice reader with zero-copy sub-slicing.
136+
func (d *Decoder) ResetBytes(data []byte) {
137+
d.bsr.reset(data)
138+
d.r = &d.bsr
139+
d.s = &d.bsr
140+
d.mapDecoder = nil
141+
d.flags = 0
142+
d.structTag = ""
143+
d.dict = nil
144+
}
145+
129146
func (d *Decoder) SetMapDecoder(fn func(*Decoder) (interface{}, error)) {
130147
d.mapDecoder = fn
131148
}
@@ -194,6 +211,11 @@ func (d *Decoder) Buffered() io.Reader {
194211
func (d *Decoder) Decode(v interface{}) error {
195212
var err error
196213
switch v := v.(type) {
214+
case *interface{}:
215+
if v != nil && *v == nil {
216+
*v, err = d.decodeInterfaceCond()
217+
return err
218+
}
197219
case *string:
198220
if v != nil {
199221
*v, err = d.DecodeString()
@@ -625,6 +647,19 @@ func (d *Decoder) readFull(b []byte) error {
625647
}
626648

627649
func (d *Decoder) readN(n int) ([]byte, error) {
650+
// Fast path: byte-slice reader — zero-copy sub-slice of input buffer.
651+
if d.bsr.data != nil {
652+
if d.bsr.pos+n > len(d.bsr.data) {
653+
return nil, io.ErrUnexpectedEOF
654+
}
655+
b := d.bsr.data[d.bsr.pos : d.bsr.pos+n]
656+
d.bsr.pos += n
657+
if d.rec != nil {
658+
d.rec = append(d.rec, b...)
659+
}
660+
return b, nil
661+
}
662+
628663
var err error
629664
if d.flags&disableAllocLimitFlag != 0 {
630665
d.buf, err = readN(d.r, d.buf, n)
@@ -635,7 +670,6 @@ func (d *Decoder) readN(n int) ([]byte, error) {
635670
return nil, err
636671
}
637672
if d.rec != nil {
638-
// TODO: read directly into d.rec?
639673
d.rec = append(d.rec, d.buf...)
640674
}
641675
return d.buf, nil

decode_map.go

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,27 @@ func (d *Decoder) decodeMapDefault() (interface{}, error) {
5252
if d.mapDecoder != nil {
5353
return d.mapDecoder(d)
5454
}
55-
return d.DecodeMap()
55+
56+
n, err := d.DecodeMapLen()
57+
if err != nil {
58+
return nil, err
59+
}
60+
if n == -1 {
61+
return nil, nil
62+
}
63+
if n == 0 {
64+
return make(map[string]interface{}), nil
65+
}
66+
67+
code, err := d.PeekCode()
68+
if err != nil {
69+
return nil, err
70+
}
71+
72+
if msgpcode.IsString(code) {
73+
return d.decodeMapStringInterfaceN(n)
74+
}
75+
return d.decodeTypedMapN(n)
5676
}
5777

5878
// DecodeMapLen decodes map length. Length is -1 when map is nil.
@@ -152,12 +172,19 @@ func (d *Decoder) DecodeMap() (map[string]interface{}, error) {
152172
if err != nil {
153173
return nil, err
154174
}
155-
156175
if n == -1 {
157176
return nil, nil
158177
}
178+
return d.decodeMapStringInterfaceN(n)
179+
}
159180

160-
m := make(map[string]interface{}, n)
181+
func (d *Decoder) decodeMapStringInterfaceN(n int) (map[string]interface{}, error) {
182+
ln := n
183+
if d.flags&disableAllocLimitFlag == 0 && ln > maxMapSize {
184+
ln = maxMapSize
185+
}
186+
187+
m := make(map[string]interface{}, ln)
161188

162189
for i := 0; i < n; i++ {
163190
mk, err := d.DecodeString()
@@ -213,7 +240,10 @@ func (d *Decoder) DecodeTypedMap() (interface{}, error) {
213240
if n <= 0 {
214241
return nil, nil
215242
}
243+
return d.decodeTypedMapN(n)
244+
}
216245

246+
func (d *Decoder) decodeTypedMapN(n int) (interface{}, error) {
217247
key, err := d.decodeInterfaceCond()
218248
if err != nil {
219249
return nil, err

0 commit comments

Comments
 (0)