Skip to content

Commit 6c60284

Browse files
authored
re-introduce and fix declcfg.Meta unmarshal error (#1109)
Signed-off-by: Joe Lanford <[email protected]>
1 parent 647537d commit 6c60284

File tree

3 files changed

+252
-46
lines changed

3 files changed

+252
-46
lines changed

alpha/declcfg/declcfg.go

Lines changed: 5 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"encoding/json"
66
"errors"
77
"fmt"
8-
"strings"
98

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

110113
// TODO: this function ensures we do not break backwards compatibility with
@@ -176,47 +179,3 @@ func extractUniqueMetaKeys(blobMap map[string]any, m *Meta) error {
176179
}
177180
return nil
178181
}
179-
180-
func resolveUnmarshalErr(data []byte, err error) string {
181-
var te *json.UnmarshalTypeError
182-
if errors.As(err, &te) {
183-
return formatUnmarshallErrorString(data, te.Error(), te.Offset)
184-
}
185-
var se *json.SyntaxError
186-
if errors.As(err, &se) {
187-
return formatUnmarshallErrorString(data, se.Error(), se.Offset)
188-
}
189-
return err.Error()
190-
}
191-
192-
func formatUnmarshallErrorString(data []byte, errmsg string, offset int64) string {
193-
sb := new(strings.Builder)
194-
_, _ = sb.WriteString(fmt.Sprintf("%s at offset %d (indicated by <==)\n ", errmsg, offset))
195-
// attempt to present the erroneous JSON in indented, human-readable format
196-
// errors result in presenting the original, unformatted output
197-
var pretty bytes.Buffer
198-
err := json.Indent(&pretty, data, "", " ")
199-
if err == nil {
200-
pString := pretty.String()
201-
// calc the prettified string offset which correlates to the original string offset
202-
var pOffset, origOffset int64
203-
origOffset = 0
204-
for origOffset = 0; origOffset < offset; {
205-
if pString[pOffset] != '\n' && pString[pOffset] != ' ' {
206-
origOffset++
207-
}
208-
pOffset++
209-
}
210-
_, _ = sb.WriteString(pString[:pOffset])
211-
_, _ = sb.WriteString(" <== ")
212-
_, _ = sb.WriteString(pString[pOffset:])
213-
} else {
214-
for i := int64(0); i < offset; i++ {
215-
_ = sb.WriteByte(data[i])
216-
}
217-
_, _ = sb.WriteString(" <== ")
218-
_, _ = sb.Write(data[offset:])
219-
}
220-
221-
return sb.String()
222-
}

alpha/declcfg/errors.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package declcfg
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
"strings"
9+
)
10+
11+
type jsonUnmarshalError struct {
12+
data []byte
13+
offset int64
14+
err error
15+
}
16+
17+
func newJSONUnmarshalError(data []byte, err error) *jsonUnmarshalError {
18+
var te *json.UnmarshalTypeError
19+
if errors.As(err, &te) {
20+
return &jsonUnmarshalError{data: data, offset: te.Offset, err: te}
21+
}
22+
var se *json.SyntaxError
23+
if errors.As(err, &se) {
24+
return &jsonUnmarshalError{data: data, offset: se.Offset, err: se}
25+
}
26+
return &jsonUnmarshalError{data: data, offset: -1, err: err}
27+
}
28+
29+
func (e *jsonUnmarshalError) Error() string {
30+
return e.err.Error()
31+
}
32+
33+
func (e *jsonUnmarshalError) Pretty() string {
34+
if len(e.data) == 0 || e.offset < 0 || e.offset > int64(len(e.data)) {
35+
return e.err.Error()
36+
}
37+
38+
const marker = " <=="
39+
40+
var sb strings.Builder
41+
_, _ = sb.WriteString(fmt.Sprintf("%s at offset %d (indicated by%s)\n", e.err.Error(), e.offset, marker))
42+
43+
prettyBuf := bytes.NewBuffer(make([]byte, 0, len(e.data)))
44+
err := json.Indent(prettyBuf, e.data, "", " ")
45+
46+
// If there was an error indenting the JSON, just treat the original data as the pretty data.
47+
if err != nil {
48+
prettyBuf = bytes.NewBuffer(e.data)
49+
}
50+
51+
// If the offset is at the end of the data, just print the pretty data and the marker at the end.
52+
if int(e.offset) == len(e.data) {
53+
_, _ = sb.WriteString(prettyBuf.String())
54+
_, _ = sb.WriteString(marker)
55+
return sb.String()
56+
}
57+
58+
// If the offset is within the data, find the corresponding offset in the pretty data.
59+
var (
60+
pIndex int
61+
pOffset int
62+
)
63+
pretty := prettyBuf.Bytes()
64+
for dIndex, b := range e.data {
65+
// If we've reached the offset, record it and break out of the loop
66+
if dIndex == int(e.offset) {
67+
pOffset = pIndex
68+
break
69+
}
70+
71+
// Fast-forward the pretty index until we find the byte in the pretty data
72+
// that matches the byte in the original data.
73+
for pretty[pIndex] != b {
74+
pIndex++
75+
if pIndex >= len(pretty) {
76+
// Something went wrong. For example, if the pretty data somehow reordered
77+
// the bytes or is missing a byte
78+
return e.err.Error()
79+
}
80+
}
81+
82+
// We found the byte in the pretty data that matches the byte in the original data,
83+
// so increment the pretty index.
84+
pIndex++
85+
86+
}
87+
88+
_, _ = sb.Write(pretty[:pOffset])
89+
_, _ = sb.WriteString(fmt.Sprintf("%s ", marker))
90+
_, _ = sb.Write(pretty[pOffset:])
91+
92+
return sb.String()
93+
}

alpha/declcfg/errors_test.go

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package declcfg
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"fmt"
7+
"reflect"
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
func TestJsonUnmarshalError(t *testing.T) {
14+
type testCase struct {
15+
name string
16+
data []byte
17+
inErr error
18+
expectErrorString string
19+
expectPrettyString string
20+
}
21+
validData := []byte(`{"messages": ["Hello", "world!"]}`)
22+
invalidData := []byte(`{"messages": ["Hello", "world!"]`)
23+
for _, tc := range []testCase{
24+
{
25+
name: "unknown error",
26+
data: validData,
27+
inErr: errors.New("unknown error"),
28+
expectErrorString: "unknown error",
29+
expectPrettyString: "unknown error",
30+
},
31+
{
32+
name: "unmarshal type error: no data",
33+
data: nil,
34+
inErr: &json.UnmarshalTypeError{Value: "foo", Type: reflect.TypeOf(""), Offset: 0},
35+
expectErrorString: `json: cannot unmarshal foo into Go value of type string`,
36+
expectPrettyString: `json: cannot unmarshal foo into Go value of type string`,
37+
},
38+
{
39+
name: "unmarshal type error: negative offset",
40+
data: validData,
41+
inErr: &json.UnmarshalTypeError{Value: "foo", Type: reflect.TypeOf(""), Offset: -1},
42+
expectErrorString: `json: cannot unmarshal foo into Go value of type string`,
43+
expectPrettyString: `json: cannot unmarshal foo into Go value of type string`,
44+
},
45+
{
46+
name: "unmarshal type error: greater than length",
47+
data: validData,
48+
inErr: &json.UnmarshalTypeError{Value: "foo", Type: reflect.TypeOf(""), Offset: int64(len(validData) + 1)},
49+
expectErrorString: `json: cannot unmarshal foo into Go value of type string`,
50+
expectPrettyString: `json: cannot unmarshal foo into Go value of type string`,
51+
},
52+
{
53+
name: "unmarshal type error: offset at beginning",
54+
data: validData,
55+
inErr: &json.UnmarshalTypeError{Value: "foo", Type: reflect.TypeOf(""), Offset: 0},
56+
expectErrorString: `json: cannot unmarshal foo into Go value of type string`,
57+
expectPrettyString: `json: cannot unmarshal foo into Go value of type string at offset 0 (indicated by <==)
58+
<== {
59+
"messages": [
60+
"Hello",
61+
"world!"
62+
]
63+
}`,
64+
},
65+
{
66+
name: "unmarshal type error: offset at 1",
67+
data: validData,
68+
inErr: &json.UnmarshalTypeError{Value: "foo", Type: reflect.TypeOf(""), Offset: 1},
69+
expectErrorString: `json: cannot unmarshal foo into Go value of type string`,
70+
expectPrettyString: `json: cannot unmarshal foo into Go value of type string at offset 1 (indicated by <==)
71+
{ <==
72+
"messages": [
73+
"Hello",
74+
"world!"
75+
]
76+
}`,
77+
},
78+
{
79+
name: "unmarshal type error: offset at end",
80+
data: validData,
81+
inErr: &json.UnmarshalTypeError{Value: "foo", Type: reflect.TypeOf(""), Offset: int64(len(validData))},
82+
expectErrorString: `json: cannot unmarshal foo into Go value of type string`,
83+
expectPrettyString: fmt.Sprintf(`json: cannot unmarshal foo into Go value of type string at offset %d (indicated by <==)
84+
{
85+
"messages": [
86+
"Hello",
87+
"world!"
88+
]
89+
} <==`, len(validData)),
90+
},
91+
{
92+
name: "syntax error: no data",
93+
data: nil,
94+
inErr: json.Unmarshal(invalidData, nil),
95+
expectErrorString: `unexpected end of JSON input`,
96+
expectPrettyString: `unexpected end of JSON input`,
97+
},
98+
{
99+
name: "syntax error: negative offset",
100+
data: invalidData,
101+
inErr: customOffsetSyntaxError(invalidData, -1),
102+
expectErrorString: `unexpected end of JSON input`,
103+
expectPrettyString: `unexpected end of JSON input`,
104+
},
105+
{
106+
name: "syntax error: greater than length",
107+
data: invalidData,
108+
inErr: customOffsetSyntaxError(invalidData, int64(len(invalidData)+1)),
109+
expectErrorString: `unexpected end of JSON input`,
110+
expectPrettyString: `unexpected end of JSON input`,
111+
},
112+
{
113+
name: "syntax error: offset at beginning",
114+
data: invalidData,
115+
inErr: customOffsetSyntaxError(invalidData, 0),
116+
expectErrorString: `unexpected end of JSON input`,
117+
expectPrettyString: `unexpected end of JSON input at offset 0 (indicated by <==)
118+
<== {"messages": ["Hello", "world!"]`,
119+
},
120+
{
121+
name: "syntax error: offset at 1",
122+
data: invalidData,
123+
inErr: customOffsetSyntaxError(invalidData, 1),
124+
expectErrorString: `unexpected end of JSON input`,
125+
expectPrettyString: `unexpected end of JSON input at offset 1 (indicated by <==)
126+
{ <== "messages": ["Hello", "world!"]`,
127+
},
128+
{
129+
name: "syntax error: offset at end",
130+
data: invalidData,
131+
inErr: customOffsetSyntaxError(invalidData, int64(len(invalidData))),
132+
expectErrorString: `unexpected end of JSON input`,
133+
expectPrettyString: fmt.Sprintf(`unexpected end of JSON input at offset %d (indicated by <==)
134+
{"messages": ["Hello", "world!"] <==`, len(invalidData)),
135+
},
136+
} {
137+
t.Run(tc.name, func(t *testing.T) {
138+
actualErr := newJSONUnmarshalError(tc.data, tc.inErr)
139+
assert.Equal(t, tc.expectErrorString, actualErr.Error())
140+
assert.Equal(t, tc.expectPrettyString, actualErr.Pretty())
141+
})
142+
}
143+
}
144+
145+
// customOffsetSyntaxError returns a json.SyntaxError with the given offset.
146+
// json.SyntaxError does not have a public constructor, so we have to use
147+
// json.Unmarshal to create one and then set the offset manually.
148+
//
149+
// If the data does not cause a syntax error, this function will panic.
150+
func customOffsetSyntaxError(data []byte, offset int64) *json.SyntaxError {
151+
err := json.Unmarshal(data, nil).(*json.SyntaxError)
152+
err.Offset = offset
153+
return err
154+
}

0 commit comments

Comments
 (0)