Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support encoding/emitting nested structures with binary fields #1

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ require (
github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f // indirect
github.com/gookit/color v1.5.4 // indirect
github.com/gorilla/websocket v1.5.1 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/onsi/ginkgo/v2 v2.12.0 // indirect
github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/quic-go v0.44.0 // indirect
Expand Down
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,21 @@ github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f h1:pDhu5sgp8yJlEF/g6osliIIpF9K4F5jvkULXa4daRDQ=
github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/onsi/ginkgo/v2 v2.12.0 h1:UIVDowFPwpg6yMUpPjGkYvf06K3RAiJXUhCxEwQVHRI=
github.com/onsi/ginkgo/v2 v2.12.0/go.mod h1:ZNEzXISYlqpb8S36iN71ifqLi3vVD1rVJGvWRCJOUpQ=
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
Expand All @@ -32,6 +39,7 @@ github.com/quic-go/quic-go v0.44.0/go.mod h1:z4cx/9Ny9UtGITIPzmPTXh1ULfOyWh4qGQl
github.com/quic-go/webtransport-go v0.8.0 h1:HxSrwun11U+LlmwpgM1kEqIqH90IT4N8auv/cD7QFJg=
github.com/quic-go/webtransport-go v0.8.0/go.mod h1:N99tjprW432Ut5ONql/aUhSLT0YVSlwHohQsuac9WaM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
Expand Down
76 changes: 35 additions & 41 deletions parser/binary.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package parser

import (
"bytes"
"errors"
"io"
"unsafe"

jsoniter "github.com/json-iterator/go"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The last update to the standard library was two years ago, and it seems to be out of maintenance.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I don't love this fact, but it was the only high-performance json library I could find that supported passing context through each individual serialization versus having to do some horrible thing with finding pointers to all arrays in a first pass, storing them in a global lookup, and then using that to back-calculate the "current stream" from inside the custom marshal functions. It's well-regarded across the internet though, has 13k stars, etc., so I'm not that worried, but your call about how worried you are about this.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For a rapidly developing network, stopping updates means many security risks.

"github.com/mitchellh/mapstructure"
"github.com/zishang520/engine.io-go-parser/types"
)
Expand All @@ -13,54 +15,46 @@ type Placeholder struct {
Num int `json:"num" mapstructure:"num" msgpack:"num"`
}

func init() {
jsoniter.RegisterTypeEncoderFunc("types.BytesBuffer", func(ptr unsafe.Pointer, stream *jsoniter.Stream) {
bb := ((*types.BytesBuffer)(ptr))

bufList := stream.Attachment.([]types.BufferInterface)
_placeholder := &Placeholder{Placeholder: true, Num: len(bufList)}
stream.WriteVal(_placeholder)
stream.Attachment = append(bufList, bb)
}, nil)

jsoniter.RegisterTypeEncoderFunc("[]uint8", func(ptr unsafe.Pointer, stream *jsoniter.Stream) {
bb := types.NewBytesBuffer(nil)
barr := ((*[]byte)(ptr))
bb.Write(*barr)

bufList := stream.Attachment.([]types.BufferInterface)
_placeholder := &Placeholder{Placeholder: true, Num: len(bufList)}
stream.WriteVal(_placeholder)
stream.Attachment = append(bufList, bb)
}, nil)
}

// Replaces every io.Reader | []byte in packet with a numbered placeholder.
func DeconstructPacket(packet *Packet) (pack *Packet, buffers []types.BufferInterface) {
pack = packet
pack.Data = _deconstructPacket(packet.Data, &buffers)

// Run the serialization now, replacing any bytebuffers/[]byte found along the way with placeholders
buf := &bytes.Buffer{}
ns := jsoniter.NewStream(jsoniter.ConfigDefault, buf, buf.Cap())
ns.Attachment = buffers
ns.WriteVal(pack.Data)
buffers = ns.Attachment.([]types.BufferInterface)
ns.Flush()
pack.preSerializedData = buf.String()

attachments := uint64(len(buffers))
pack.Attachments = &attachments // number of binary 'attachments'
return pack, buffers
}

func _deconstructPacket(data any, buffers *[]types.BufferInterface) any {
if data == nil {
return nil
}

if IsBinary(data) {
_placeholder := &Placeholder{Placeholder: true, Num: len(*buffers)}
rdata := types.NewBytesBuffer(nil)
switch tdata := data.(type) {
case io.Reader:
if c, ok := data.(io.Closer); ok {
defer c.Close()
}
rdata.ReadFrom(tdata)
case []byte:
rdata.Write(tdata)
}
*buffers = append(*buffers, rdata)
return _placeholder
}

switch tdata := data.(type) {
case []any:
newData := make([]any, 0, len(tdata))
for _, v := range tdata {
newData = append(newData, _deconstructPacket(v, buffers))
}
return newData
case map[string]any:
newData := map[string]any{}
for k, v := range tdata {
newData[k] = _deconstructPacket(v, buffers)
}
return newData
default:
return data
}
}

// Reconstructs a binary packet from its placeholder packet and buffers
func ReconstructPacket(packet *Packet, buffers []types.BufferInterface) (*Packet, error) {
data, err := _reconstructPacket(packet.Data, &buffers)
Expand Down
13 changes: 9 additions & 4 deletions parser/encoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ func _encodeData(data any) any {
newData[k] = _encodeData(v)
}
return newData
default:
return data
}

return data
}

// Encode packet as string.
Expand All @@ -79,8 +79,13 @@ func (e *encoder) encodeAsString(packet *Packet) types.BufferInterface {
}
// json data
if nil != packet.Data {
if b, err := json.Marshal(_encodeData(packet.Data)); err == nil {
str.Write(b)
if packet.preSerializedData != "" {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to run more test cases to ensure that this request is OK. It may take longer.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no rush. I've already taken the encoder from this branch and I'm using it in my project that way via the config.Encoder.

// Already serialized in the DeconstructPacket function
str.WriteString(packet.preSerializedData)
} else {
if b, err := json.Marshal(_encodeData(packet.Data)); err == nil {
str.Write(b)
}
}
}
parser_log.Debug("encoded %v as %v", packet, str)
Expand Down
42 changes: 40 additions & 2 deletions parser/is-binary.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package parser

import (
"io"
"reflect"
"strings"

"github.com/zishang520/engine.io-go-parser/types"
Expand Down Expand Up @@ -30,14 +31,51 @@ func HasBinary(data any) bool {
return true
}
}
return false
case map[string]any:
for _, value := range v {
if HasBinary(value) {
return true
}
}
return false
}

if IsBinary(data) {
return true
}

dv := reflect.ValueOf(data)
switch dv.Kind() {
case reflect.Pointer:
return IsBinary(dv.Elem().Interface())
case reflect.Struct:
for fi := range dv.NumField() {
dfv := dv.Field(fi)
if dfv.CanInterface() && HasBinary(dfv.Interface()) {
return true
}
}
return false
case reflect.Array, reflect.Slice:
for i := range dv.Len() {
av := dv.Index(i)
if av.CanInterface() && HasBinary(av.Interface()) {
return true
}
}
return false
case reflect.Map:
mr := dv.MapRange()
for mr.Next() {
// Keys can't be binary blobs in json, so only check values
mv := mr.Value()
if mv.CanInterface() && HasBinary(mv.Interface()) {
return true
}
}
return false
default:
return IsBinary(data)
return false
}
return false
}
11 changes: 6 additions & 5 deletions parser/type.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ type (
PacketType byte

Packet struct {
Type PacketType `json:"type" mapstructure:"type" msgpack:"type"`
Nsp string `json:"nsp" mapstructure:"nsp" msgpack:"nsp"`
Data any `json:"data,omitempty" mapstructure:"data,omitempty" msgpack:"data,omitempty"`
Id *uint64 `json:"id,omitempty" mapstructure:"id,omitempty" msgpack:"id,omitempty"`
Attachments *uint64 `json:"attachments,omitempty" mapstructure:"attachments,omitempty" msgpack:"attachments,omitempty"`
Type PacketType `json:"type" mapstructure:"type" msgpack:"type"`
Nsp string `json:"nsp" mapstructure:"nsp" msgpack:"nsp"`
preSerializedData string
Data any `json:"data,omitempty" mapstructure:"data,omitempty" msgpack:"data,omitempty"`
Id *uint64 `json:"id,omitempty" mapstructure:"id,omitempty" msgpack:"id,omitempty"`
Attachments *uint64 `json:"attachments,omitempty" mapstructure:"attachments,omitempty" msgpack:"attachments,omitempty"`
}
)

Expand Down
Loading