aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--ccl.go60
-rw-r--r--ccl_test.go81
2 files changed, 84 insertions, 57 deletions
diff --git a/ccl.go b/ccl.go
index 5d37310..b592822 100644
--- a/ccl.go
+++ b/ccl.go
@@ -268,7 +268,7 @@ func (p *parser) peek() ([]byte, error) {
return p.tok, nil
}
-func (p *parser) next() ([]byte, error) {
+func (p *parser) nextEOF() ([]byte, error) {
tok, err := p.peek()
if err != nil {
return nil, err
@@ -277,6 +277,14 @@ func (p *parser) next() ([]byte, error) {
return tok, nil
}
+func (p *parser) next() ([]byte, error) {
+ tok, err := p.nextEOF()
+ if err == errEOF {
+ return nil, newSyntaxError(p.data, len(p.data), "premature EOF")
+ }
+ return tok, err
+}
+
func checkNum(b []byte) bool {
if b[0] == '-' || b[0] == '+' {
b = b[1:]
@@ -342,7 +350,7 @@ func (p *parser) parseInt(numBytes []byte) (integer, error) {
}
un, err := strconv.ParseUint(string(n), 10, 64)
if err != nil {
- return integer{}, p.error("invalid number (unreachable)")
+ return integer{}, p.error("(unreachable) invalid number: %s", err)
}
return integer{un, sgn}, nil
}
@@ -353,14 +361,16 @@ func (p *parser) parseFloat(nBytes []byte) (float64, error) {
}
n, err := strconv.ParseFloat(string(nBytes), 64)
if err != nil {
- return 0, p.error("invalid number (unreachable)")
+ return 0, p.error("(unreachable) invalid number: %s", err)
}
return n, nil
}
func (p *parser) unescape(rawStr []byte) ([]byte, error) {
+ tokStart := p.i
var escaped []byte
for i := 0; i < len(rawStr); i++ {
+ p.i++
if i+1 < len(rawStr) && rawStr[i] == '\r' && rawStr[i+1] == '\n' {
continue
}
@@ -417,7 +427,7 @@ func (p *parser) unescape(rawStr []byte) ([]byte, error) {
}
n, err := strconv.ParseUint(string(rawStr[i:end]), 16, 8)
if err != nil {
- return nil, p.error("invalid hex escape %q (unreachable)", rawStr[i-2:end])
+ return nil, p.error("(unreachable) invalid hex escape %q: %s", rawStr[i-2:end], err)
}
i = end - 1
b = []byte{byte(n)}
@@ -452,6 +462,7 @@ func (p *parser) unescape(rawStr []byte) ([]byte, error) {
}
escaped = append(escaped, b...)
}
+ p.i = tokStart
if !utf8.Valid(escaped) {
return nil, p.error("string %q is not UTF-8 encoded", escaped)
}
@@ -492,21 +503,6 @@ func (p *parser) parseMessage(out reflect.Value, field []byte) error {
}
}
-func (p *parser) parsePossiblyRepeatedVal(fieldVal reflect.Value, parsedFields map[string]bool, tok, field []byte) error {
- if fieldVal.Kind() == reflect.Slice && fieldVal.Type() != reflect.TypeFor[[]byte]() {
- if tok[0] == '[' {
- return p.parseList(fieldVal, field)
- }
- fieldVal.Set(reflect.Append(fieldVal, reflect.Zero(fieldVal.Type().Elem())))
- return p.parseVal(fieldVal.Index(fieldVal.Len()-1), tok, field)
- }
- if parsedFields[string(field)] {
- return p.error("duplicate field %q but type is not repeated", field)
- }
- parsedFields[string(field)] = true
- return p.parseVal(fieldVal, tok, field)
-}
-
func (p *parser) parseVal(fieldVal reflect.Value, tok, field []byte) error {
switch tok[0] {
case '[':
@@ -621,33 +617,41 @@ func (p *parser) parseFieldVal(out reflect.Value, parsedFields map[string]bool,
return p.error("no field named %q", field)
}
fieldVal := out.Field(fieldIdx)
+ repeated := fieldVal.Kind() == reflect.Slice && fieldVal.Type() != reflect.TypeFor[[]byte]()
+ if !repeated {
+ if parsedFields[string(field)] {
+ return p.error("duplicate field %q but type is not repeated", field)
+ }
+ parsedFields[string(field)] = true
+ }
tok, err := p.next()
if err != nil {
return err
}
switch tok[0] {
case '{':
- if err := p.parsePossiblyRepeatedVal(fieldVal, parsedFields, tok, field); err != nil {
- return err
- }
case ':':
- tok, err := p.next()
+ tok, err = p.next()
if err != nil {
return err
}
- if err := p.parsePossiblyRepeatedVal(fieldVal, parsedFields, tok, field); err != nil {
- return err
- }
default:
return p.error("expecting colon")
}
- return nil
+ if repeated {
+ if tok[0] == '[' {
+ return p.parseList(fieldVal, field)
+ }
+ fieldVal.Set(reflect.Append(fieldVal, reflect.Zero(fieldVal.Type().Elem())))
+ return p.parseVal(fieldVal.Index(fieldVal.Len()-1), tok, field)
+ }
+ return p.parseVal(fieldVal, tok, field)
}
func (p *parser) parse(out reflect.Value) error {
seen := make(map[string]bool)
for {
- tok, err := p.next()
+ tok, err := p.nextEOF()
if err != nil {
if err == errEOF {
return nil
diff --git a/ccl_test.go b/ccl_test.go
index 42c71a9..ac64d58 100644
--- a/ccl_test.go
+++ b/ccl_test.go
@@ -6,6 +6,7 @@ import (
"time"
"github.com/google/go-cmp/cmp"
+ "github.com/google/go-cmp/cmp/cmpopts"
)
func ptr[T any](v T) *T {
@@ -405,116 +406,162 @@ func TestUnmarshal_Invalid(t *testing.T) {
for _, tc := range []struct {
desc string
msg string
+ want *syntaxError
}{{
desc: "BadNum",
msg: `int: .`,
+ want: &syntaxError{line: 1, col: 6},
}, {
desc: "WeirdNum",
msg: `float:1e+`,
+ want: &syntaxError{line: 1, col: 7},
}, {
desc: "BadHex",
msg: `int:0xgg`,
+ want: &syntaxError{line: 1, col: 5},
}, {
desc: "BadStringEscape",
msg: `string: '\g'`,
+ want: &syntaxError{line: 1, col: 10},
}, {
desc: "BadDoubleStringEscape",
msg: `string: "\g"`,
+ want: &syntaxError{line: 1, col: 10},
}, {
desc: "StringBadReturnEscape",
msg: "string:'\\\r'",
+ want: &syntaxError{line: 1, col: 9},
}, {
desc: "StringBadHex",
msg: `string:"\xgg"`,
+ want: &syntaxError{line: 1, col: 9},
}, {
desc: "StringShortUnicode",
msg: `string:"\u001"`,
+ want: &syntaxError{line: 1, col: 9},
}, {
desc: "StringBadUnicode",
msg: `string:"\ugggg"`,
+ want: &syntaxError{line: 1, col: 9},
}, {
desc: "StringControlCharacter",
msg: "string:'\a'",
+ want: &syntaxError{line: 1, col: 9},
}, {
desc: "StringCarriageReturnNotFollowedByNewline",
msg: "string:'\r'",
+ want: &syntaxError{line: 1, col: 9},
}, {
desc: "UnterminatedString",
msg: `string: '`,
+ want: &syntaxError{line: 1, col: 9},
}, {
desc: "UnterminatedDoubleString",
msg: `string: "`,
+ want: &syntaxError{line: 1, col: 9},
}, {
desc: "NoFieldName",
msg: `10`,
+ want: &syntaxError{line: 1, col: 1},
}, {
desc: "MsgNoFieldName",
msg: `msg {10}`,
+ want: &syntaxError{line: 1, col: 6},
}, {
desc: "ListMissingColon",
msg: `repeated []`,
+ want: &syntaxError{line: 1, col: 10},
}, {
desc: "ListMissingComma",
msg: `repeated: [1 2]`,
+ want: &syntaxError{line: 1, col: 14},
}, {
desc: "ListBadVal",
msg: `repeated: [asdf]`,
+ want: &syntaxError{line: 1, col: 12},
}, {
desc: "ListBadMsgVal",
msg: `repeated_msg: [{asdf}]`,
+ want: &syntaxError{line: 1, col: 17},
}, {
desc: "IntLeadingZero",
msg: `int: 0644`,
+ want: &syntaxError{line: 1, col: 6},
}, {
desc: "InvalidOctal",
msg: `string: "\777"`,
+ want: &syntaxError{line: 1, col: 10},
}, {
desc: "InvalidUTF8",
msg: `string: "\x80"`,
+ want: &syntaxError{line: 1, col: 9},
}, {
desc: "FieldMissingVal",
msg: `string`,
+ want: &syntaxError{line: 1, col: 7},
}, {
desc: "FieldMissingColon",
msg: `string "abc"`,
+ want: &syntaxError{line: 1, col: 8},
}, {
desc: "Repeated",
msg: `int:5 int:6`,
+ want: &syntaxError{line: 1, col: 7},
}, {
desc: "IntOutOfRange",
msg: `int8:512`,
+ want: &syntaxError{line: 1, col: 6},
}, {
desc: "IntOutOfRangeNegative",
msg: `int8:-512`,
+ want: &syntaxError{line: 1, col: 6},
}, {
desc: "Base64",
msg: `bytes:"dGVzdAo"`,
+ want: &syntaxError{line: 1, col: 7},
}, {
desc: "NotBase64",
msg: `bytes:[1,2,3]`,
+ want: &syntaxError{line: 1, col: 7},
}, {
desc: "BadField",
msg: `asdfasdfasdf:"asdf"`,
+ want: &syntaxError{line: 1, col: 1},
}, {
desc: "NestedRepeated",
msg: `repeated: [[1]]`,
+ want: &syntaxError{line: 1, col: 12},
}, {
desc: "NestedRepeatedNestedType",
- msg: `nested_repeated: [[1]]`,
+ msg: `nested_repeated: [[{}]]`,
+ want: &syntaxError{line: 1, col: 19},
}, {
desc: "FloatMissingExponent",
msg: `float:1e`,
+ want: &syntaxError{line: 1, col: 7},
}, {
desc: "UnterminatedComment",
msg: `/*`,
+ want: &syntaxError{line: 1, col: 1},
+ }, {
+ desc: "BadToken",
+ msg: `###### This is a very important file please do not modify
+#########################################################
+################ The more ## I put the more secure it is######
+int:12345; # oops typo
+`,
+ want: &syntaxError{line: 4, col: 10},
}} {
t.Run(tc.desc, func(t *testing.T) {
t.Parallel()
- var got message
- err := Unmarshal([]byte(tc.msg), &got)
- if err == nil {
- t.Errorf("Unmarshal(%q) returned success, want error", tc.msg)
+ err := Unmarshal([]byte(tc.msg), new(message))
+ got, ok := err.(*syntaxError)
+ if !ok {
+ t.Fatalf("Unmarshal(%q): expected *syntaxError, got error %T %[2]v", tc.msg, err)
+ }
+ if diff := cmp.Diff(tc.want, got, cmp.AllowUnexported(syntaxError{}), cmpopts.IgnoreFields(syntaxError{}, "reason")); diff != "" {
+ t.Errorf("Unmarshal(%q) returned unexpected error diff (-want +got):\n%s", tc.msg, diff)
}
})
}
@@ -656,30 +703,6 @@ func TestUnmarshal_InvalidType(t *testing.T) {
}
}
-func TestUnmarshal_ErrorLineCol(t *testing.T) {
- t.Parallel()
-
- type message struct {
- Secret int64 `ccl:"secret"`
- }
-
- msg := `
- ###### This is a very important file please do not modify
- #########################################################
- ################ The more ## I put the more secure it is######
- secret:12345; # oops typo
- `
- err := Unmarshal([]byte(msg), new(message))
- syntaxErr, ok := err.(*syntaxError)
- if !ok {
- t.Fatalf("Unmarshal(%q): expected *syntaxError, got error %T %[2]v", msg, err)
- }
- want := &syntaxError{line: 5, col: 15}
- if syntaxErr.line != want.line || syntaxErr.col != want.col {
- t.Errorf("Unmarshal(%q) returned error %+v, want line %d, col %d", msg, syntaxErr, want.line, want.col)
- }
-}
-
func ExampleUnmarshal() {
// Pretend this was loaded from a file
msg := []byte(`