Skip to content

Commit

Permalink
re-introduce and fix declcfg.Meta unmarshal error (#1109)
Browse files Browse the repository at this point in the history
Signed-off-by: Joe Lanford <[email protected]>
  • Loading branch information
joelanford authored Jun 9, 2023
1 parent 647537d commit 6c60284
Show file tree
Hide file tree
Showing 3 changed files with 252 additions and 46 deletions.
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
}

0 comments on commit 6c60284

Please sign in to comment.