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

re-introduce and fix declcfg.Meta unmarshal error #1109

Merged
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
51 changes: 5 additions & 46 deletions alpha/declcfg/declcfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"encoding/json"
"errors"
"fmt"
"strings"

"golang.org/x/text/cases"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
Expand Down Expand Up @@ -104,7 +103,11 @@ func (m Meta) MarshalJSON() ([]byte, error) {
func (m *Meta) UnmarshalJSON(blob []byte) error {
blobMap := map[string]interface{}{}
if err := json.Unmarshal(blob, &blobMap); err != nil {
return err
// TODO: unfortunately, there are libraries between here and the original caller
// that eat our error type and return a generic error, such that we lose the
// ability to errors.As to get this error on the other side. For now, just return
// a string error that includes the pretty printed message.
return errors.New(newJSONUnmarshalError(blob, err).Pretty())
}

// TODO: this function ensures we do not break backwards compatibility with
Expand Down Expand Up @@ -176,47 +179,3 @@ func extractUniqueMetaKeys(blobMap map[string]any, m *Meta) error {
}
return nil
}

func resolveUnmarshalErr(data []byte, err error) string {
var te *json.UnmarshalTypeError
if errors.As(err, &te) {
return formatUnmarshallErrorString(data, te.Error(), te.Offset)
}
var se *json.SyntaxError
if errors.As(err, &se) {
return formatUnmarshallErrorString(data, se.Error(), se.Offset)
}
return err.Error()
}

func formatUnmarshallErrorString(data []byte, errmsg string, offset int64) string {
sb := new(strings.Builder)
_, _ = sb.WriteString(fmt.Sprintf("%s at offset %d (indicated by <==)\n ", errmsg, offset))
// attempt to present the erroneous JSON in indented, human-readable format
// errors result in presenting the original, unformatted output
var pretty bytes.Buffer
err := json.Indent(&pretty, data, "", " ")
if err == nil {
pString := pretty.String()
// calc the prettified string offset which correlates to the original string offset
var pOffset, origOffset int64
origOffset = 0
for origOffset = 0; origOffset < offset; {
if pString[pOffset] != '\n' && pString[pOffset] != ' ' {
origOffset++
}
pOffset++
}
_, _ = sb.WriteString(pString[:pOffset])
_, _ = sb.WriteString(" <== ")
_, _ = sb.WriteString(pString[pOffset:])
} else {
for i := int64(0); i < offset; i++ {
_ = sb.WriteByte(data[i])
}
_, _ = sb.WriteString(" <== ")
_, _ = sb.Write(data[offset:])
}

return sb.String()
}
93 changes: 93 additions & 0 deletions alpha/declcfg/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package declcfg

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"strings"
)

type jsonUnmarshalError struct {
data []byte
offset int64
err error
}

func newJSONUnmarshalError(data []byte, err error) *jsonUnmarshalError {
var te *json.UnmarshalTypeError
if errors.As(err, &te) {
return &jsonUnmarshalError{data: data, offset: te.Offset, err: te}
}
var se *json.SyntaxError
if errors.As(err, &se) {
return &jsonUnmarshalError{data: data, offset: se.Offset, err: se}
}
return &jsonUnmarshalError{data: data, offset: -1, err: err}
}

func (e *jsonUnmarshalError) Error() string {
return e.err.Error()
}

func (e *jsonUnmarshalError) Pretty() string {
if len(e.data) == 0 || e.offset < 0 || e.offset > int64(len(e.data)) {
return e.err.Error()
}

const marker = " <=="

var sb strings.Builder
_, _ = sb.WriteString(fmt.Sprintf("%s at offset %d (indicated by%s)\n", e.err.Error(), e.offset, marker))

prettyBuf := bytes.NewBuffer(make([]byte, 0, len(e.data)))
err := json.Indent(prettyBuf, e.data, "", " ")

// If there was an error indenting the JSON, just treat the original data as the pretty data.
if err != nil {
prettyBuf = bytes.NewBuffer(e.data)
}

// If the offset is at the end of the data, just print the pretty data and the marker at the end.
if int(e.offset) == len(e.data) {
_, _ = sb.WriteString(prettyBuf.String())
_, _ = sb.WriteString(marker)
return sb.String()
}

// If the offset is within the data, find the corresponding offset in the pretty data.
var (
pIndex int
pOffset int
)
pretty := prettyBuf.Bytes()
for dIndex, b := range e.data {
// If we've reached the offset, record it and break out of the loop
if dIndex == int(e.offset) {
pOffset = pIndex
break
}

// Fast-forward the pretty index until we find the byte in the pretty data
// that matches the byte in the original data.
for pretty[pIndex] != b {
pIndex++
if pIndex >= len(pretty) {
// Something went wrong. For example, if the pretty data somehow reordered
// the bytes or is missing a byte
return e.err.Error()
}
}

// We found the byte in the pretty data that matches the byte in the original data,
// so increment the pretty index.
pIndex++

}

_, _ = sb.Write(pretty[:pOffset])
_, _ = sb.WriteString(fmt.Sprintf("%s ", marker))
_, _ = sb.Write(pretty[pOffset:])

return sb.String()
}
154 changes: 154 additions & 0 deletions alpha/declcfg/errors_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package declcfg

import (
"encoding/json"
"errors"
"fmt"
"reflect"
"testing"

"github.com/stretchr/testify/assert"
)

func TestJsonUnmarshalError(t *testing.T) {
type testCase struct {
name string
data []byte
inErr error
expectErrorString string
expectPrettyString string
}
validData := []byte(`{"messages": ["Hello", "world!"]}`)
invalidData := []byte(`{"messages": ["Hello", "world!"]`)
for _, tc := range []testCase{
{
name: "unknown error",
data: validData,
inErr: errors.New("unknown error"),
expectErrorString: "unknown error",
expectPrettyString: "unknown error",
},
{
name: "unmarshal type error: no data",
data: nil,
inErr: &json.UnmarshalTypeError{Value: "foo", Type: reflect.TypeOf(""), Offset: 0},
expectErrorString: `json: cannot unmarshal foo into Go value of type string`,
expectPrettyString: `json: cannot unmarshal foo into Go value of type string`,
},
{
name: "unmarshal type error: negative offset",
data: validData,
inErr: &json.UnmarshalTypeError{Value: "foo", Type: reflect.TypeOf(""), Offset: -1},
expectErrorString: `json: cannot unmarshal foo into Go value of type string`,
expectPrettyString: `json: cannot unmarshal foo into Go value of type string`,
},
{
name: "unmarshal type error: greater than length",
data: validData,
inErr: &json.UnmarshalTypeError{Value: "foo", Type: reflect.TypeOf(""), Offset: int64(len(validData) + 1)},
expectErrorString: `json: cannot unmarshal foo into Go value of type string`,
expectPrettyString: `json: cannot unmarshal foo into Go value of type string`,
},
{
name: "unmarshal type error: offset at beginning",
data: validData,
inErr: &json.UnmarshalTypeError{Value: "foo", Type: reflect.TypeOf(""), Offset: 0},
expectErrorString: `json: cannot unmarshal foo into Go value of type string`,
expectPrettyString: `json: cannot unmarshal foo into Go value of type string at offset 0 (indicated by <==)
<== {
"messages": [
"Hello",
"world!"
]
}`,
},
{
name: "unmarshal type error: offset at 1",
data: validData,
inErr: &json.UnmarshalTypeError{Value: "foo", Type: reflect.TypeOf(""), Offset: 1},
expectErrorString: `json: cannot unmarshal foo into Go value of type string`,
expectPrettyString: `json: cannot unmarshal foo into Go value of type string at offset 1 (indicated by <==)
{ <==
"messages": [
"Hello",
"world!"
]
}`,
},
{
name: "unmarshal type error: offset at end",
data: validData,
inErr: &json.UnmarshalTypeError{Value: "foo", Type: reflect.TypeOf(""), Offset: int64(len(validData))},
expectErrorString: `json: cannot unmarshal foo into Go value of type string`,
expectPrettyString: fmt.Sprintf(`json: cannot unmarshal foo into Go value of type string at offset %d (indicated by <==)
{
"messages": [
"Hello",
"world!"
]
} <==`, len(validData)),
},
{
name: "syntax error: no data",
data: nil,
inErr: json.Unmarshal(invalidData, nil),
expectErrorString: `unexpected end of JSON input`,
expectPrettyString: `unexpected end of JSON input`,
},
{
name: "syntax error: negative offset",
data: invalidData,
inErr: customOffsetSyntaxError(invalidData, -1),
expectErrorString: `unexpected end of JSON input`,
expectPrettyString: `unexpected end of JSON input`,
},
{
name: "syntax error: greater than length",
data: invalidData,
inErr: customOffsetSyntaxError(invalidData, int64(len(invalidData)+1)),
expectErrorString: `unexpected end of JSON input`,
expectPrettyString: `unexpected end of JSON input`,
},
{
name: "syntax error: offset at beginning",
data: invalidData,
inErr: customOffsetSyntaxError(invalidData, 0),
expectErrorString: `unexpected end of JSON input`,
expectPrettyString: `unexpected end of JSON input at offset 0 (indicated by <==)
<== {"messages": ["Hello", "world!"]`,
},
{
name: "syntax error: offset at 1",
data: invalidData,
inErr: customOffsetSyntaxError(invalidData, 1),
expectErrorString: `unexpected end of JSON input`,
expectPrettyString: `unexpected end of JSON input at offset 1 (indicated by <==)
{ <== "messages": ["Hello", "world!"]`,
},
{
name: "syntax error: offset at end",
data: invalidData,
inErr: customOffsetSyntaxError(invalidData, int64(len(invalidData))),
expectErrorString: `unexpected end of JSON input`,
expectPrettyString: fmt.Sprintf(`unexpected end of JSON input at offset %d (indicated by <==)
{"messages": ["Hello", "world!"] <==`, len(invalidData)),
},
} {
t.Run(tc.name, func(t *testing.T) {
actualErr := newJSONUnmarshalError(tc.data, tc.inErr)
assert.Equal(t, tc.expectErrorString, actualErr.Error())
assert.Equal(t, tc.expectPrettyString, actualErr.Pretty())
})
}
}

// customOffsetSyntaxError returns a json.SyntaxError with the given offset.
// json.SyntaxError does not have a public constructor, so we have to use
// json.Unmarshal to create one and then set the offset manually.
//
// If the data does not cause a syntax error, this function will panic.
func customOffsetSyntaxError(data []byte, offset int64) *json.SyntaxError {
err := json.Unmarshal(data, nil).(*json.SyntaxError)
err.Offset = offset
return err
}