-
Notifications
You must be signed in to change notification settings - Fork 249
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
re-introduce and fix declcfg.Meta unmarshal error (#1109)
Signed-off-by: Joe Lanford <[email protected]>
- Loading branch information
1 parent
647537d
commit 6c60284
Showing
3 changed files
with
252 additions
and
46 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |