From 91a63dfa5d65e187d6ae4de2ea2e375224ad84c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B3=D0=BE=D1=80=D1=8C=20=D0=A7=D0=B5=D1=85=D0=BE?= =?UTF-8?q?=D0=BD=D0=B8=D0=BD?= Date: Sat, 6 Sep 2025 16:58:42 +0300 Subject: [PATCH 1/4] added cycle check in marshal methods --- config.go | 65 +++++++++++++++++++++++++++++++++++++++++++++--------- reflect.go | 2 +- 2 files changed, 56 insertions(+), 11 deletions(-) diff --git a/config.go b/config.go index 2adcdc3b..e12a1696 100644 --- a/config.go +++ b/config.go @@ -2,6 +2,7 @@ package jsoniter import ( "encoding/json" + "fmt" "io" "reflect" "sync" @@ -284,26 +285,70 @@ func (cfg *frozenConfig) cleanEncoders() { } func (cfg *frozenConfig) MarshalToString(v interface{}) (string, error) { - stream := cfg.BorrowStream(nil) - defer cfg.ReturnStream(stream) - stream.WriteVal(v) - if stream.Error != nil { - return "", stream.Error + result, err := cfg.marshalToStream(v) + if err != nil { + return "", err } - return string(stream.Buffer()), nil + + return string(result), nil } func (cfg *frozenConfig) Marshal(v interface{}) ([]byte, error) { + result, err := cfg.marshalToStream(v) + if err != nil { + return nil, err + } + + copied := make([]byte, len(result)) + copy(copied, result) + return copied, nil +} + +func hasCycle(v interface{}) bool { + visited := make(map[uintptr]bool) + queue := []reflect.Value{reflect.ValueOf(v)} + + for len(queue) > 0 { + val := queue[0] + queue = queue[1:] + + for val.Kind() == reflect.Ptr || val.Kind() == reflect.Interface { + if val.IsNil() { + break + } + if val.Kind() == reflect.Ptr { + ptr := val.Pointer() + if visited[ptr] { + return true + } + visited[ptr] = true + } + val = val.Elem() + } + + if val.Kind() == reflect.Struct { + for i := 0; i < val.NumField(); i++ { + queue = append(queue, val.Field(i)) + } + } + } + return false +} + +// marshalToStream writes v to a borrowed stream and returns stream.Buffer() with error. +func (cfg *frozenConfig) marshalToStream(v interface{}) ([]byte, error) { + if hasCycle(v) { + return nil, fmt.Errorf("jsoniter: unsupported type: encountered a cycle") + } + stream := cfg.BorrowStream(nil) defer cfg.ReturnStream(stream) + stream.WriteVal(v) if stream.Error != nil { return nil, stream.Error } - result := stream.Buffer() - copied := make([]byte, len(result)) - copy(copied, result) - return copied, nil + return stream.Buffer(), nil } func (cfg *frozenConfig) MarshalIndent(v interface{}, prefix, indent string) ([]byte, error) { diff --git a/reflect.go b/reflect.go index 39acb320..95b48c8f 100644 --- a/reflect.go +++ b/reflect.go @@ -85,7 +85,7 @@ func (iter *Iterator) ReadVal(obj interface{}) { // WriteVal copy the go interface into underlying JSON, same as json.Marshal func (stream *Stream) WriteVal(val interface{}) { - if nil == val { + if val == nil { stream.WriteNil() return } From 584bc71b9f72f1c7cf9c9b8c1dd14506d5f13f19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B3=D0=BE=D1=80=D1=8C=20=D0=A7=D0=B5=D1=85=D0=BE?= =?UTF-8?q?=D0=BD=D0=B8=D0=BD?= Date: Sun, 7 Sep 2025 12:41:43 +0300 Subject: [PATCH 2/4] moved cycle check to (*Stream).WriteVal method --- config.go | 36 ------------------------------------ reflect.go | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 36 deletions(-) diff --git a/config.go b/config.go index e12a1696..208b7c59 100644 --- a/config.go +++ b/config.go @@ -2,7 +2,6 @@ package jsoniter import ( "encoding/json" - "fmt" "io" "reflect" "sync" @@ -304,43 +303,8 @@ func (cfg *frozenConfig) Marshal(v interface{}) ([]byte, error) { return copied, nil } -func hasCycle(v interface{}) bool { - visited := make(map[uintptr]bool) - queue := []reflect.Value{reflect.ValueOf(v)} - - for len(queue) > 0 { - val := queue[0] - queue = queue[1:] - - for val.Kind() == reflect.Ptr || val.Kind() == reflect.Interface { - if val.IsNil() { - break - } - if val.Kind() == reflect.Ptr { - ptr := val.Pointer() - if visited[ptr] { - return true - } - visited[ptr] = true - } - val = val.Elem() - } - - if val.Kind() == reflect.Struct { - for i := 0; i < val.NumField(); i++ { - queue = append(queue, val.Field(i)) - } - } - } - return false -} - // marshalToStream writes v to a borrowed stream and returns stream.Buffer() with error. func (cfg *frozenConfig) marshalToStream(v interface{}) ([]byte, error) { - if hasCycle(v) { - return nil, fmt.Errorf("jsoniter: unsupported type: encountered a cycle") - } - stream := cfg.BorrowStream(nil) defer cfg.ReturnStream(stream) diff --git a/reflect.go b/reflect.go index 95b48c8f..60447873 100644 --- a/reflect.go +++ b/reflect.go @@ -1,6 +1,7 @@ package jsoniter import ( + "errors" "fmt" "reflect" "unsafe" @@ -83,8 +84,46 @@ func (iter *Iterator) ReadVal(obj interface{}) { } } +func hasCycle(v interface{}) bool { + visited := make(map[uintptr]bool) + queue := []reflect.Value{reflect.ValueOf(v)} + + for len(queue) > 0 { + val := queue[0] + queue = queue[1:] + + for val.Kind() == reflect.Ptr || val.Kind() == reflect.Interface { + if val.IsNil() { + break + } + if val.Kind() == reflect.Ptr { + ptr := val.Pointer() + if visited[ptr] { + return true + } + visited[ptr] = true + } + val = val.Elem() + } + + if val.Kind() == reflect.Struct { + for i := 0; i < val.NumField(); i++ { + queue = append(queue, val.Field(i)) + } + } + } + return false +} + +var errCycleEncountered = errors.New("jsoniter: unsupported type: encountered a cycle") + // WriteVal copy the go interface into underlying JSON, same as json.Marshal func (stream *Stream) WriteVal(val interface{}) { + if hasCycle(val) { + stream.Error = errCycleEncountered + return + } + if val == nil { stream.WriteNil() return From 1c449d2527f0065ed899ecbb77aa2b4b7647b0f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B3=D0=BE=D1=80=D1=8C=20=D0=A7=D0=B5=D1=85=D0=BE?= =?UTF-8?q?=D0=BD=D0=B8=D0=BD?= Date: Sun, 7 Sep 2025 12:53:45 +0300 Subject: [PATCH 3/4] added test --- any_tests/jsoniter_any_object_test.go | 2 +- api_tests/marshal_json_test.go | 26 ++++++++++++++++++++++---- reflect.go | 4 ++-- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/any_tests/jsoniter_any_object_test.go b/any_tests/jsoniter_any_object_test.go index 5af292da..1a0d55b4 100644 --- a/any_tests/jsoniter_any_object_test.go +++ b/any_tests/jsoniter_any_object_test.go @@ -3,7 +3,7 @@ package any_tests import ( "testing" - "github.com/json-iterator/go" + jsoniter "github.com/json-iterator/go" "github.com/stretchr/testify/require" ) diff --git a/api_tests/marshal_json_test.go b/api_tests/marshal_json_test.go index 635a24ee..1a57effa 100644 --- a/api_tests/marshal_json_test.go +++ b/api_tests/marshal_json_test.go @@ -3,12 +3,13 @@ package test import ( "bytes" "encoding/json" - "github.com/json-iterator/go" + "errors" "testing" + + jsoniter "github.com/json-iterator/go" "github.com/stretchr/testify/require" ) - type Foo struct { Bar interface{} } @@ -19,11 +20,10 @@ func (f Foo) MarshalJSON() ([]byte, error) { return buf.Bytes(), err } - // Standard Encoder has trailing newline. func TestEncodeMarshalJSON(t *testing.T) { - foo := Foo { + foo := Foo{ Bar: 123, } should := require.New(t) @@ -34,3 +34,21 @@ func TestEncodeMarshalJSON(t *testing.T) { stdenc.Encode(foo) should.Equal(stdbuf.Bytes(), buf.Bytes()) } + +func TestMarshalObjectWithCycle(t *testing.T) { + type A struct { + A *A + } + a := A{} + a.A = &a + + api := jsoniter.ConfigCompatibleWithStandardLibrary + + if _, err := jsoniter.Marshal(a); !errors.Is(err, jsoniter.ErrCycleEncountered) { + t.Fatal(err) + } + + if err := api.NewEncoder(nil).Encode(a); !errors.Is(err, jsoniter.ErrCycleEncountered) { + t.Fatal(err) + } +} diff --git a/reflect.go b/reflect.go index 60447873..27257a10 100644 --- a/reflect.go +++ b/reflect.go @@ -115,12 +115,12 @@ func hasCycle(v interface{}) bool { return false } -var errCycleEncountered = errors.New("jsoniter: unsupported type: encountered a cycle") +var ErrCycleEncountered = errors.New("jsoniter: unsupported type: encountered a cycle") // WriteVal copy the go interface into underlying JSON, same as json.Marshal func (stream *Stream) WriteVal(val interface{}) { if hasCycle(val) { - stream.Error = errCycleEncountered + stream.Error = ErrCycleEncountered return } From 0b429fb1c8ee57c66afdbe32c7d05ff9240e6e73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B3=D0=BE=D1=80=D1=8C=20=D0=A7=D0=B5=D1=85=D0=BE?= =?UTF-8?q?=D0=BD=D0=B8=D0=BD?= Date: Sun, 7 Sep 2025 14:32:57 +0300 Subject: [PATCH 4/4] fix --- reflect.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/reflect.go b/reflect.go index 27257a10..942c84f9 100644 --- a/reflect.go +++ b/reflect.go @@ -119,15 +119,16 @@ var ErrCycleEncountered = errors.New("jsoniter: unsupported type: encountered a // WriteVal copy the go interface into underlying JSON, same as json.Marshal func (stream *Stream) WriteVal(val interface{}) { - if hasCycle(val) { - stream.Error = ErrCycleEncountered + if val == nil { + stream.WriteNil() return } - if val == nil { - stream.WriteNil() + if hasCycle(val) { + stream.Error = ErrCycleEncountered return } + cacheKey := reflect2.RTypeOf(val) encoder := stream.cfg.getEncoderFromCache(cacheKey) if encoder == nil {