Skip to content

Commit 85cb9b6

Browse files
committed
improve logic for getting file contents on parse error
1 parent e46d034 commit 85cb9b6

File tree

3 files changed

+268
-23
lines changed

3 files changed

+268
-23
lines changed

cli/cli_test.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cli
22

33
import (
4+
"io"
45
"os"
56
"strings"
67
"testing"
@@ -20,6 +21,23 @@ func setLocation(loc *time.Location) func() {
2021
return func() { time.Local = orig }
2122
}
2223

24+
// This reader does not implement io.Seeker to emulate standard input.
25+
type stringReader string
26+
27+
func newStringReader(s string) io.Reader {
28+
r := stringReader(s)
29+
return &r
30+
}
31+
32+
func (r *stringReader) Read(b []byte) (n int, err error) {
33+
n = copy(b, *r)
34+
*r = (*r)[n:]
35+
if len(*r) == 0 {
36+
err = io.EOF
37+
}
38+
return
39+
}
40+
2341
func TestCliRun(t *testing.T) {
2442
if err := os.Setenv("NO_COLOR", ""); err != nil {
2543
t.Fatal(err)
@@ -61,7 +79,7 @@ func TestCliRun(t *testing.T) {
6179
}()
6280
var outStream, errStream strings.Builder
6381
cli := cli{
64-
inStream: strings.NewReader(tc.Input),
82+
inStream: newStringReader(tc.Input),
6583
outStream: &outStream,
6684
errStream: &errStream,
6785
}

cli/inputs.go

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ import (
1515

1616
type inputReader struct {
1717
io.Reader
18-
file *os.File
19-
buf *bytes.Buffer
18+
rs io.ReadSeeker
19+
buf *bytes.Buffer
2020
}
2121

2222
func newInputReader(r io.Reader) *inputReader {
23-
if r, ok := r.(*os.File); ok {
23+
if r, ok := r.(io.ReadSeeker); ok {
2424
if _, err := r.Seek(0, io.SeekCurrent); err == nil {
2525
return &inputReader{r, r, nil}
2626
}
@@ -33,31 +33,29 @@ func (ir *inputReader) getContents(offset *int64, line *int) string {
3333
if buf := ir.buf; buf != nil {
3434
return buf.String()
3535
}
36-
if current, err := ir.file.Seek(0, io.SeekCurrent); err == nil {
37-
defer func() { ir.file.Seek(current, io.SeekStart) }()
36+
if current, err := ir.rs.Seek(0, io.SeekCurrent); err == nil {
37+
defer ir.rs.Seek(current, io.SeekStart)
3838
}
39-
ir.file.Seek(0, io.SeekStart)
39+
_, _ = ir.rs.Seek(0, io.SeekStart)
4040
const bufSize = 16 * 1024
4141
var buf bytes.Buffer // do not use strings.Builder because we need to Reset
42-
if offset != nil && *offset > bufSize {
43-
buf.Grow(bufSize)
44-
for *offset > bufSize {
45-
n, err := io.Copy(&buf, io.LimitReader(ir.file, bufSize))
46-
*offset -= int64(n)
47-
*line += bytes.Count(buf.Bytes(), []byte{'\n'})
48-
buf.Reset()
49-
if err != nil || n == 0 {
50-
break
51-
}
42+
for offset != nil && *offset > bufSize*3/4 {
43+
n, err := io.Copy(&buf,
44+
io.LimitReader(ir.rs, min(bufSize, *offset-bufSize/4)))
45+
*offset -= n
46+
*line += bytes.Count(buf.Bytes(), []byte{'\n'})
47+
buf.Reset()
48+
if err != nil || n == 0 {
49+
break
5250
}
5351
}
5452
var r io.Reader
5553
if offset == nil {
56-
r = ir.file
54+
r = ir.rs
5755
} else {
58-
r = io.LimitReader(ir.file, bufSize*2)
56+
r = io.LimitReader(ir.rs, bufSize)
5957
}
60-
io.Copy(&buf, r)
58+
_, _ = io.Copy(&buf, r)
6159
return buf.String()
6260
}
6361

@@ -96,9 +94,13 @@ func (i *jsonInputIter) Next() (any, bool) {
9694
}
9795
var offset *int64
9896
var line *int
99-
if err, ok := err.(*json.SyntaxError); ok {
100-
err.Offset -= i.offset
101-
offset, line = &err.Offset, &i.line
97+
if e, ok := err.(*json.SyntaxError); ok {
98+
e.Offset -= i.offset
99+
offset, line = &e.Offset, &i.line
100+
} else if err == io.ErrUnexpectedEOF && i.ir.rs != nil {
101+
if pos, err := i.ir.rs.Seek(0, io.SeekEnd); err == nil {
102+
offset, line = &pos, &i.line
103+
}
102104
}
103105
i.err = &jsonParseError{i.fname, i.ir.getContents(offset, line), i.line, err}
104106
return i.err, true

cli/inputs_test.go

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
package cli
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
7+
"reflect"
8+
"slices"
9+
"strings"
10+
"testing"
11+
)
12+
13+
func TestJSONInputIter(t *testing.T) {
14+
for _, tc := range []struct {
15+
name string
16+
input string
17+
expected []any
18+
expectedErr string
19+
}{
20+
{
21+
name: "empty",
22+
input: "",
23+
expected: []any{},
24+
},
25+
{
26+
name: "scalars",
27+
input: "null true false 1 2.3 \"hello\"",
28+
expected: []any{nil, true, false, json.Number("1"), json.Number("2.3"), "hello"},
29+
},
30+
{
31+
name: "arrays",
32+
input: "[][[]][1.2][3,[4]]",
33+
expected: []any{
34+
[]any{},
35+
[]any{[]any{}},
36+
[]any{json.Number("1.2")},
37+
[]any{json.Number("3"), []any{json.Number("4")}},
38+
},
39+
},
40+
{
41+
name: "objects",
42+
input: `{}{"a":1,"b":2}{"a":{"b":3,"c":4}}`,
43+
expected: []any{
44+
map[string]any{},
45+
map[string]any{"a": json.Number("1"), "b": json.Number("2")},
46+
map[string]any{"a": map[string]any{"b": json.Number("3"), "c": json.Number("4")}},
47+
},
48+
},
49+
{
50+
name: "unexpected EOF error",
51+
input: "0[1",
52+
expected: []any{json.Number("0")},
53+
expectedErr: `invalid json: test.json
54+
0[1
55+
^ unexpected EOF`,
56+
},
57+
{
58+
name: "array value error",
59+
input: `0["a",]`,
60+
expected: []any{json.Number("0")},
61+
expectedErr: `invalid json: test.json
62+
0["a",]
63+
^ invalid character ']' looking for beginning of value`,
64+
},
65+
{
66+
name: "object key error",
67+
input: "0\n{\n 0",
68+
expected: []any{json.Number("0")},
69+
expectedErr: `invalid json: test.json:3
70+
3 | 0
71+
^ invalid character '0' looking for beginning of object key string`,
72+
},
73+
{
74+
name: "object value error",
75+
input: "0\n{\n \"a\":\n}",
76+
expected: []any{json.Number("0")},
77+
expectedErr: `invalid json: test.json:4
78+
4 | }
79+
^ invalid character '}' looking for beginning of value`,
80+
},
81+
{
82+
name: "large input with unexpected EOF error",
83+
input: "0[0," + strings.Repeat("\n", 40*1024) + "1\n",
84+
expected: []any{json.Number("0")},
85+
expectedErr: fmt.Sprintf(`invalid json: test.json:%[1]d
86+
%[1]d | 1
87+
^ unexpected EOF`, 40*1024+1),
88+
},
89+
{
90+
name: "large input with array value error",
91+
input: "0[0," + strings.Repeat("\n", 40*1024) + "]",
92+
expected: []any{json.Number("0")},
93+
expectedErr: fmt.Sprintf(`invalid json: test.json:%[1]d
94+
%[1]d | ]
95+
^ invalid character ']' looking for beginning of value`, 40*1024+1),
96+
},
97+
{
98+
name: "large input with object key error",
99+
input: `0{"a"` + strings.Repeat("\n", 40*1024) + ":0,1}",
100+
expected: []any{json.Number("0")},
101+
expectedErr: fmt.Sprintf(`invalid json: test.json:%[1]d
102+
%[1]d | :0,1}
103+
^ invalid character '1' looking for beginning of object key string`, 40*1024+1),
104+
},
105+
{
106+
name: "many input values with value error",
107+
input: strings.Repeat("0\n", 40*1024) + ":\n",
108+
expected: slices.Repeat([]any{json.Number("0")}, 40*1024),
109+
expectedErr: fmt.Sprintf(`invalid json: test.json:%[1]d
110+
%[1]d | :
111+
^ invalid character ':' looking for beginning of value`, 40*1024+1),
112+
},
113+
} {
114+
for _, r := range []io.Reader{strings.NewReader(tc.input), newStringReader(tc.input)} {
115+
t.Run(fmt.Sprintf("%s_%T", tc.name, r), func(t *testing.T) {
116+
iter := newJSONInputIter(r, "test.json")
117+
got, gotErr := []any{}, error(nil)
118+
for {
119+
v, ok := iter.Next()
120+
if !ok {
121+
break
122+
}
123+
if gotErr, ok = v.(error); ok {
124+
continue
125+
}
126+
got = append(got, v)
127+
}
128+
if !reflect.DeepEqual(got, tc.expected) {
129+
t.Errorf("newJSONInputIter(%T).Next():\n"+
130+
" got: %#v\n"+
131+
"expected: %#v", r, got, tc.expected)
132+
}
133+
if (tc.expectedErr == "") != (gotErr == nil) ||
134+
gotErr != nil && gotErr.Error() != tc.expectedErr {
135+
t.Errorf("newJSONInputIter(%T).Next():\n"+
136+
" got error: %v\n"+
137+
"expected error: %v", r, gotErr, tc.expectedErr)
138+
}
139+
})
140+
}
141+
}
142+
}
143+
144+
func TestYAMLInputIter(t *testing.T) {
145+
for _, tc := range []struct {
146+
name string
147+
input string
148+
expected []any
149+
expectedErr string
150+
}{
151+
{
152+
name: "empty",
153+
input: "",
154+
expected: []any{},
155+
},
156+
{
157+
name: "scalars",
158+
input: "null\n---\ntrue\n---\nfalse\n---\n1\n---\n2.3\n---\nhello",
159+
expected: []any{nil, true, false, json.Number("1"), json.Number("2.3"), "hello"},
160+
},
161+
{
162+
name: "arrays",
163+
input: "[]\n---\n- []\n---\n- 1.2\n---\n- 3\n- - 4",
164+
expected: []any{
165+
[]any{},
166+
[]any{[]any{}},
167+
[]any{json.Number("1.2")},
168+
[]any{json.Number("3"), []any{json.Number("4")}},
169+
},
170+
},
171+
{
172+
name: "objects",
173+
input: "{}\n---\na: 1\nb: 2\n---\na:\n b: 3\n c: 4",
174+
expected: []any{
175+
map[string]any{},
176+
map[string]any{"a": json.Number("1"), "b": json.Number("2")},
177+
map[string]any{"a": map[string]any{"b": json.Number("3"), "c": json.Number("4")}},
178+
},
179+
},
180+
{
181+
name: "unexpected EOF error",
182+
input: "0\n---\n[",
183+
expected: []any{json.Number("0")},
184+
expectedErr: `invalid yaml: test.yaml:3
185+
3 | [
186+
^ did not find expected node content`,
187+
},
188+
{
189+
name: "large input with unexpected EOF error",
190+
input: strings.Repeat("0\n---\n", 20*1024) + "{",
191+
expected: slices.Repeat([]any{json.Number("0")}, 20*1024),
192+
expectedErr: fmt.Sprintf(`invalid yaml: test.yaml:%[1]d
193+
%[1]d | {
194+
^ did not find expected node content`, 40*1024+1),
195+
},
196+
} {
197+
for _, r := range []io.Reader{strings.NewReader(tc.input), newStringReader(tc.input)} {
198+
t.Run(fmt.Sprintf("%s_%T", tc.name, r), func(t *testing.T) {
199+
iter := newYAMLInputIter(r, "test.yaml")
200+
got, gotErr := []any{}, error(nil)
201+
for {
202+
v, ok := iter.Next()
203+
if !ok {
204+
break
205+
}
206+
if gotErr, ok = v.(error); ok {
207+
continue
208+
}
209+
got = append(got, v)
210+
}
211+
if !reflect.DeepEqual(got, tc.expected) {
212+
t.Errorf("newYAMLInputIter(%T).Next():\n"+
213+
" got: %#v\n"+
214+
"expected: %#v", r, got, tc.expected)
215+
}
216+
if (tc.expectedErr == "") != (gotErr == nil) ||
217+
gotErr != nil && gotErr.Error() != tc.expectedErr {
218+
t.Errorf("newYAMLInputIter(%T).Next():\n"+
219+
" got error: %v\n"+
220+
"expected error: %v", r, gotErr, tc.expectedErr)
221+
}
222+
})
223+
}
224+
}
225+
}

0 commit comments

Comments
 (0)