diff options
| -rw-r--r-- | ccl.go | 60 | ||||
| -rw-r--r-- | ccl_test.go | 81 |
2 files changed, 84 insertions, 57 deletions
@@ -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(` |
