protobuf-go/internal/encoding/json/decode_test.go
Damien Neil 8c86fc5e7d all: remove non-fatal UTF-8 validation errors (and non-fatal in general)
Immediately abort (un)marshal operations when encountering invalid UTF-8
data in proto3 strings. No other proto implementation supports non-UTF-8
data in proto3 strings (and many reject it in proto2 strings as well).
Producing invalid output is an interoperability threat (other
implementations won't be able to read it).

The case where existing string data is found to contain non-UTF8 data is
better handled by changing the field to the `bytes` type, which (aside
from UTF-8 validation) is wire-compatible with `string`.

Remove the errors.NonFatal type, since there are no remaining cases
where it is needed. "Non-fatal" errors which produce results and a
non-nil error are problematic because they compose poorly; the better
approach is to take an option like AllowPartial indicating which
conditions to check for.

Change-Id: I9d189ec6ffda7b5d96d094aa1b290af2e3f23736
Reviewed-on: https://go-review.googlesource.com/c/protobuf/+/183098
Reviewed-by: Joe Tsai <thebrokentoaster@gmail.com>
2019-06-20 20:55:13 +00:00

1121 lines
21 KiB
Go

// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package json_test
import (
"strings"
"testing"
"unicode/utf8"
"google.golang.org/protobuf/internal/encoding/json"
)
type R struct {
// T is expected Type returned from calling Decoder.Read.
T json.Type
// E is expected error substring from calling Decoder.Read if set.
E string
// V is expected value from calling
// Value.{Bool()|Float()|Int()|Uint()|String()} depending on type.
V interface{}
// VE is expected error substring from calling
// Value.{Bool()|Float()|Int()|Uint()|String()} depending on type if set.
VE string
}
func TestDecoder(t *testing.T) {
const space = " \n\r\t"
tests := []struct {
input string
// want is a list of expected values returned from calling
// Decoder.Read. An item makes the test code invoke
// Decoder.Read and compare against R.T and R.E. For Bool,
// Number and String tokens, it invokes the corresponding getter method
// and compares the returned value against R.V or R.VE if it returned an
// error.
want []R
}{
{
input: ``,
want: []R{{T: json.EOF}},
},
{
input: space,
want: []R{{T: json.EOF}},
},
{
// Calling Read after EOF will keep returning EOF for
// succeeding Read calls.
input: space,
want: []R{
{T: json.EOF},
{T: json.EOF},
{T: json.EOF},
},
},
// JSON literals.
{
input: space + `null` + space,
want: []R{
{T: json.Null},
{T: json.EOF},
},
},
{
input: space + `true` + space,
want: []R{
{T: json.Bool, V: true},
{T: json.EOF},
},
},
{
input: space + `false` + space,
want: []R{
{T: json.Bool, V: false},
{T: json.EOF},
},
},
{
// Error returned will produce the same error again.
input: space + `foo` + space,
want: []R{
{E: `invalid value foo`},
{E: `invalid value foo`},
},
},
// JSON strings.
{
input: space + `""` + space,
want: []R{
{T: json.String, V: ""},
{T: json.EOF},
},
},
{
input: space + `"hello"` + space,
want: []R{
{T: json.String, V: "hello"},
{T: json.EOF},
},
},
{
input: `"hello`,
want: []R{{E: `unexpected EOF`}},
},
{
input: "\"\x00\"",
want: []R{{E: `invalid character '\x00' in string`}},
},
{
input: "\"\u0031\u0032\"",
want: []R{
{T: json.String, V: "12"},
{T: json.EOF},
},
},
{
// Invalid UTF-8 error is returned in ReadString instead of Read.
input: "\"\xff\"",
want: []R{{E: `syntax error (line 1:1): invalid UTF-8 in string`}},
},
{
input: `"` + string(utf8.RuneError) + `"`,
want: []R{
{T: json.String, V: string(utf8.RuneError)},
{T: json.EOF},
},
},
{
input: `"\uFFFD"`,
want: []R{
{T: json.String, V: string(utf8.RuneError)},
{T: json.EOF},
},
},
{
input: `"\x"`,
want: []R{{E: `invalid escape code "\\x" in string`}},
},
{
input: `"\uXXXX"`,
want: []R{{E: `invalid escape code "\\uXXXX" in string`}},
},
{
input: `"\uDEAD"`, // unmatched surrogate pair
want: []R{{E: `unexpected EOF`}},
},
{
input: `"\uDEAD\uBEEF"`, // invalid surrogate half
want: []R{{E: `invalid escape code "\\uBEEF" in string`}},
},
{
input: `"\uD800\udead"`, // valid surrogate pair
want: []R{
{T: json.String, V: `𐊭`},
{T: json.EOF},
},
},
{
input: `"\u0000\"\\\/\b\f\n\r\t"`,
want: []R{
{T: json.String, V: "\u0000\"\\/\b\f\n\r\t"},
{T: json.EOF},
},
},
// Invalid JSON numbers.
{
input: `-`,
want: []R{{E: `invalid number -`}},
},
{
input: `+0`,
want: []R{{E: `invalid value +0`}},
},
{
input: `-+`,
want: []R{{E: `invalid number -+`}},
},
{
input: `0.`,
want: []R{{E: `invalid number 0.`}},
},
{
input: `.1`,
want: []R{{E: `invalid value .1`}},
},
{
input: `1.0.1`,
want: []R{{E: `invalid number 1.0.1`}},
},
{
input: `1..1`,
want: []R{{E: `invalid number 1..1`}},
},
{
input: `-1-2`,
want: []R{{E: `invalid number -1-2`}},
},
{
input: `01`,
want: []R{{E: `invalid number 01`}},
},
{
input: `1e`,
want: []R{{E: `invalid number 1e`}},
},
{
input: `1e1.2`,
want: []R{{E: `invalid number 1e1.2`}},
},
{
input: `1Ee`,
want: []R{{E: `invalid number 1Ee`}},
},
{
input: `1.e1`,
want: []R{{E: `invalid number 1.e1`}},
},
{
input: `1.e+`,
want: []R{{E: `invalid number 1.e+`}},
},
{
input: `1e+-2`,
want: []R{{E: `invalid number 1e+-2`}},
},
{
input: `1e--2`,
want: []R{{E: `invalid number 1e--2`}},
},
{
input: `1.0true`,
want: []R{{E: `invalid number 1.0true`}},
},
// JSON numbers as floating point.
{
input: space + `0.0` + space,
want: []R{
{T: json.Number, V: float32(0)},
{T: json.EOF},
},
},
{
input: space + `0` + space,
want: []R{
{T: json.Number, V: float32(0)},
{T: json.EOF},
},
},
{
input: space + `-0` + space,
want: []R{
{T: json.Number, V: float32(0)},
{T: json.EOF},
},
},
{
input: `-1.02`,
want: []R{
{T: json.Number, V: float32(-1.02)},
{T: json.EOF},
},
},
{
input: `1.020000`,
want: []R{
{T: json.Number, V: float32(1.02)},
{T: json.EOF},
},
},
{
input: `-1.0e0`,
want: []R{
{T: json.Number, V: float32(-1)},
{T: json.EOF},
},
},
{
input: `1.0e-000`,
want: []R{
{T: json.Number, V: float32(1)},
{T: json.EOF},
},
},
{
input: `1e+00`,
want: []R{
{T: json.Number, V: float32(1)},
{T: json.EOF},
},
},
{
input: `1.02e3`,
want: []R{
{T: json.Number, V: float32(1.02e3)},
{T: json.EOF},
},
},
{
input: `-1.02E03`,
want: []R{
{T: json.Number, V: float32(-1.02e3)},
{T: json.EOF},
},
},
{
input: `1.0200e+3`,
want: []R{
{T: json.Number, V: float32(1.02e3)},
{T: json.EOF},
},
},
{
input: `-1.0200E+03`,
want: []R{
{T: json.Number, V: float32(-1.02e3)},
{T: json.EOF},
},
},
{
input: `1.0200e-3`,
want: []R{
{T: json.Number, V: float32(1.02e-3)},
{T: json.EOF},
},
},
{
input: `-1.0200E-03`,
want: []R{
{T: json.Number, V: float32(-1.02e-3)},
{T: json.EOF},
},
},
{
// Exceeds max float32 limit, but should be ok for float64.
input: `3.4e39`,
want: []R{
{T: json.Number, V: float64(3.4e39)},
{T: json.EOF},
},
},
{
// Exceeds max float32 limit.
input: `3.4e39`,
want: []R{
{T: json.Number, V: float32(0), VE: `value out of range`},
{T: json.EOF},
},
},
{
// Less than negative max float32 limit.
input: `-3.4e39`,
want: []R{
{T: json.Number, V: float32(0), VE: `value out of range`},
{T: json.EOF},
},
},
{
// Exceeds max float64 limit.
input: `1.79e+309`,
want: []R{
{T: json.Number, V: float64(0), VE: `value out of range`},
{T: json.EOF},
},
},
{
// Less than negative max float64 limit.
input: `-1.79e+309`,
want: []R{
{T: json.Number, V: float64(0), VE: `value out of range`},
{T: json.EOF},
},
},
// JSON numbers as signed integers.
{
input: space + `0` + space,
want: []R{
{T: json.Number, V: int32(0)},
{T: json.EOF},
},
},
{
input: space + `-0` + space,
want: []R{
{T: json.Number, V: int32(0)},
{T: json.EOF},
},
},
{
// Fractional part equals 0 is ok.
input: `1.00000`,
want: []R{
{T: json.Number, V: int32(1)},
{T: json.EOF},
},
},
{
// Fractional part not equals 0 returns error.
input: `1.0000000001`,
want: []R{
{T: json.Number, V: int32(0), VE: `cannot convert 1.0000000001 to integer`},
{T: json.EOF},
},
},
{
input: `0e0`,
want: []R{
{T: json.Number, V: int32(0)},
{T: json.EOF},
},
},
{
input: `0.0E0`,
want: []R{
{T: json.Number, V: int32(0)},
{T: json.EOF},
},
},
{
input: `0.0E10`,
want: []R{
{T: json.Number, V: int32(0)},
{T: json.EOF},
},
},
{
input: `-1`,
want: []R{
{T: json.Number, V: int32(-1)},
{T: json.EOF},
},
},
{
input: `1.0e+0`,
want: []R{
{T: json.Number, V: int32(1)},
{T: json.EOF},
},
},
{
input: `-1E-0`,
want: []R{
{T: json.Number, V: int32(-1)},
{T: json.EOF},
},
},
{
input: `1E1`,
want: []R{
{T: json.Number, V: int32(10)},
{T: json.EOF},
},
},
{
input: `-100.00e-02`,
want: []R{
{T: json.Number, V: int32(-1)},
{T: json.EOF},
},
},
{
input: `0.1200E+02`,
want: []R{
{T: json.Number, V: int64(12)},
{T: json.EOF},
},
},
{
input: `0.012e2`,
want: []R{
{T: json.Number, V: int32(0), VE: `cannot convert 0.012e2 to integer`},
{T: json.EOF},
},
},
{
input: `12e-2`,
want: []R{
{T: json.Number, V: int32(0), VE: `cannot convert 12e-2 to integer`},
{T: json.EOF},
},
},
{
// Exceeds math.MaxInt32.
input: `2147483648`,
want: []R{
{T: json.Number, V: int32(0), VE: `value out of range`},
{T: json.EOF},
},
},
{
// Exceeds math.MinInt32.
input: `-2147483649`,
want: []R{
{T: json.Number, V: int32(0), VE: `value out of range`},
{T: json.EOF},
},
},
{
// Exceeds math.MaxInt32, but ok for int64.
input: `2147483648`,
want: []R{
{T: json.Number, V: int64(2147483648)},
{T: json.EOF},
},
},
{
// Exceeds math.MinInt32, but ok for int64.
input: `-2147483649`,
want: []R{
{T: json.Number, V: int64(-2147483649)},
{T: json.EOF},
},
},
{
// Exceeds math.MaxInt64.
input: `9223372036854775808`,
want: []R{
{T: json.Number, V: int64(0), VE: `value out of range`},
{T: json.EOF},
},
},
{
// Exceeds math.MinInt64.
input: `-9223372036854775809`,
want: []R{
{T: json.Number, V: int64(0), VE: `value out of range`},
{T: json.EOF},
},
},
// JSON numbers as unsigned integers.
{
input: space + `0` + space,
want: []R{
{T: json.Number, V: uint32(0)},
{T: json.EOF},
},
},
{
input: space + `-0` + space,
want: []R{
{T: json.Number, V: uint32(0)},
{T: json.EOF},
},
},
{
input: `-1`,
want: []R{
{T: json.Number, V: uint32(0), VE: `invalid syntax`},
{T: json.EOF},
},
},
{
// Exceeds math.MaxUint32.
input: `4294967296`,
want: []R{
{T: json.Number, V: uint32(0), VE: `value out of range`},
{T: json.EOF},
},
},
{
// Exceeds math.MaxUint64.
input: `18446744073709551616`,
want: []R{
{T: json.Number, V: uint64(0), VE: `value out of range`},
{T: json.EOF},
},
},
// JSON sequence of values.
{
input: `true null`,
want: []R{
{T: json.Bool, V: true},
{E: `unexpected value null`},
},
},
{
input: "null false",
want: []R{
{T: json.Null},
{E: `unexpected value false`},
},
},
{
input: `true,false`,
want: []R{
{T: json.Bool, V: true},
{E: `unexpected character ,`},
},
},
{
input: `47"hello"`,
want: []R{
{T: json.Number, V: int32(47)},
{E: `unexpected value "hello"`},
},
},
{
input: `47 "hello"`,
want: []R{
{T: json.Number, V: int32(47)},
{E: `unexpected value "hello"`},
},
},
{
input: `true 42`,
want: []R{
{T: json.Bool, V: true},
{E: `unexpected value 42`},
},
},
// JSON arrays.
{
input: space + `[]` + space,
want: []R{
{T: json.StartArray},
{T: json.EndArray},
{T: json.EOF},
},
},
{
input: space + `[` + space + `]` + space,
want: []R{
{T: json.StartArray},
{T: json.EndArray},
{T: json.EOF},
},
},
{
input: space + `[` + space,
want: []R{
{T: json.StartArray},
{E: `unexpected EOF`},
},
},
{
input: space + `]` + space,
want: []R{{E: `unexpected character ]`}},
},
{
input: `[null,true,false, 1e1, "hello" ]`,
want: []R{
{T: json.StartArray},
{T: json.Null},
{T: json.Bool, V: true},
{T: json.Bool, V: false},
{T: json.Number, V: int32(10)},
{T: json.String, V: "hello"},
{T: json.EndArray},
{T: json.EOF},
},
},
{
input: `[` + space + `true` + space + `,` + space + `"hello"` + space + `]`,
want: []R{
{T: json.StartArray},
{T: json.Bool, V: true},
{T: json.String, V: "hello"},
{T: json.EndArray},
{T: json.EOF},
},
},
{
input: `[` + space + `true` + space + `,` + space + `]`,
want: []R{
{T: json.StartArray},
{T: json.Bool, V: true},
{E: `unexpected character ]`},
},
},
{
input: `[` + space + `false` + space + `]`,
want: []R{
{T: json.StartArray},
{T: json.Bool, V: false},
{T: json.EndArray},
{T: json.EOF},
},
},
{
input: `[` + space + `1` + space + `0` + space + `]`,
want: []R{
{T: json.StartArray},
{T: json.Number, V: int64(1)},
{E: `unexpected value 0`},
},
},
{
input: `[null`,
want: []R{
{T: json.StartArray},
{T: json.Null},
{E: `unexpected EOF`},
},
},
{
input: `[foo]`,
want: []R{
{T: json.StartArray},
{E: `invalid value foo`},
},
},
{
input: `[{}, "hello", [true, false], null]`,
want: []R{
{T: json.StartArray},
{T: json.StartObject},
{T: json.EndObject},
{T: json.String, V: "hello"},
{T: json.StartArray},
{T: json.Bool, V: true},
{T: json.Bool, V: false},
{T: json.EndArray},
{T: json.Null},
{T: json.EndArray},
{T: json.EOF},
},
},
{
input: `[{ ]`,
want: []R{
{T: json.StartArray},
{T: json.StartObject},
{E: `unexpected character ]`},
},
},
{
input: `[[ ]`,
want: []R{
{T: json.StartArray},
{T: json.StartArray},
{T: json.EndArray},
{E: `unexpected EOF`},
},
},
{
input: `[,]`,
want: []R{
{T: json.StartArray},
{E: `unexpected character ,`},
},
},
{
input: `[true "hello"]`,
want: []R{
{T: json.StartArray},
{T: json.Bool, V: true},
{E: `unexpected value "hello"`},
},
},
{
input: `[] null`,
want: []R{
{T: json.StartArray},
{T: json.EndArray},
{E: `unexpected value null`},
},
},
{
input: `true []`,
want: []R{
{T: json.Bool, V: true},
{E: `unexpected character [`},
},
},
// JSON objects.
{
input: space + `{}` + space,
want: []R{
{T: json.StartObject},
{T: json.EndObject},
{T: json.EOF},
},
},
{
input: space + `{` + space + `}` + space,
want: []R{
{T: json.StartObject},
{T: json.EndObject},
{T: json.EOF},
},
},
{
input: space + `{` + space,
want: []R{
{T: json.StartObject},
{E: `unexpected EOF`},
},
},
{
input: space + `}` + space,
want: []R{{E: `unexpected character }`}},
},
{
input: `{` + space + `null` + space + `}`,
want: []R{
{T: json.StartObject},
{E: `unexpected value null`},
},
},
{
input: `{[]}`,
want: []R{
{T: json.StartObject},
{E: `unexpected character [`},
},
},
{
input: `{,}`,
want: []R{
{T: json.StartObject},
{E: `unexpected character ,`},
},
},
{
input: `{"345678"}`,
want: []R{
{T: json.StartObject},
{E: `unexpected character }, missing ":" after object name`},
},
},
{
input: `{` + space + `"hello"` + space + `:` + space + `"world"` + space + `}`,
want: []R{
{T: json.StartObject},
{T: json.Name, V: "hello"},
{T: json.String, V: "world"},
{T: json.EndObject},
{T: json.EOF},
},
},
{
input: `{"hello" "world"}`,
want: []R{
{T: json.StartObject},
{E: `unexpected character ", missing ":" after object name`},
},
},
{
input: `{"hello":`,
want: []R{
{T: json.StartObject},
{T: json.Name, V: "hello"},
{E: `unexpected EOF`},
},
},
{
input: `{"hello":"world"`,
want: []R{
{T: json.StartObject},
{T: json.Name, V: "hello"},
{T: json.String, V: "world"},
{E: `unexpected EOF`},
},
},
{
input: `{"hello":"world",`,
want: []R{
{T: json.StartObject},
{T: json.Name, V: "hello"},
{T: json.String, V: "world"},
{E: `unexpected EOF`},
},
},
{
input: `{"34":"89",}`,
want: []R{
{T: json.StartObject},
{T: json.Name, V: "34"},
{T: json.String, V: "89"},
{E: `syntax error (line 1:12): unexpected character }`},
},
},
{
input: `{
"number": 123e2,
"bool" : false,
"object": {"string": "world"},
"null" : null,
"array" : [1.01, "hello", true],
"string": "hello"
}`,
want: []R{
{T: json.StartObject},
{T: json.Name, V: "number"},
{T: json.Number, V: int32(12300)},
{T: json.Name, V: "bool"},
{T: json.Bool, V: false},
{T: json.Name, V: "object"},
{T: json.StartObject},
{T: json.Name, V: "string"},
{T: json.String, V: "world"},
{T: json.EndObject},
{T: json.Name, V: "null"},
{T: json.Null},
{T: json.Name, V: "array"},
{T: json.StartArray},
{T: json.Number, V: float32(1.01)},
{T: json.String, V: "hello"},
{T: json.Bool, V: true},
{T: json.EndArray},
{T: json.Name, V: "string"},
{T: json.String, V: "hello"},
{T: json.EndObject},
{T: json.EOF},
},
},
{
input: `[
{"object": {"number": 47}},
["list"],
null
]`,
want: []R{
{T: json.StartArray},
{T: json.StartObject},
{T: json.Name, V: "object"},
{T: json.StartObject},
{T: json.Name, V: "number"},
{T: json.Number, V: uint32(47)},
{T: json.EndObject},
{T: json.EndObject},
{T: json.StartArray},
{T: json.String, V: "list"},
{T: json.EndArray},
{T: json.Null},
{T: json.EndArray},
{T: json.EOF},
},
},
// Tests for line and column info.
{
input: `12345678 x`,
want: []R{
{T: json.Number, V: int64(12345678)},
{E: `syntax error (line 1:10): invalid value x`},
},
},
{
input: "\ntrue\n x",
want: []R{
{T: json.Bool, V: true},
{E: `syntax error (line 3:4): invalid value x`},
},
},
{
input: `"💩"x`,
want: []R{
{T: json.String, V: "💩"},
{E: `syntax error (line 1:4): invalid value x`},
},
},
{
input: "\n\n[\"🔥🔥🔥\"x",
want: []R{
{T: json.StartArray},
{T: json.String, V: "🔥🔥🔥"},
{E: `syntax error (line 3:7): invalid value x`},
},
},
{
// Multi-rune emojis.
input: `["👍🏻👍🏿"x`,
want: []R{
{T: json.StartArray},
{T: json.String, V: "👍🏻👍🏿"},
{E: `syntax error (line 1:8): invalid value x`},
},
},
{
input: `{
"45678":-1
}`,
want: []R{
{T: json.StartObject},
{T: json.Name, V: "45678"},
{T: json.Number, V: uint64(1), VE: "error (line 2:11)"},
},
},
}
for _, tc := range tests {
tc := tc
t.Run("", func(t *testing.T) {
dec := json.NewDecoder([]byte(tc.input))
for i, want := range tc.want {
typ := dec.Peek()
if typ != want.T {
t.Errorf("input: %v\nPeek() got %v want %v", tc.input, typ, want.T)
}
value, err := dec.Read()
if err != nil {
if want.E == "" {
t.Errorf("input: %v\nRead() got unexpected error: %v", tc.input, err)
} else if !strings.Contains(err.Error(), want.E) {
t.Errorf("input: %v\nRead() got %q, want %q", tc.input, err, want.E)
}
} else {
if want.E != "" {
t.Errorf("input: %v\nRead() got nil error, want %q", tc.input, want.E)
}
}
token := value.Type()
if token != want.T {
t.Errorf("input: %v\nRead() got %v, want %v", tc.input, token, want.T)
break
}
checkValue(t, value, i, want)
}
})
}
}
func checkValue(t *testing.T, value json.Value, wantIdx int, want R) {
var got interface{}
var err error
switch value.Type() {
case json.Bool:
got, err = value.Bool()
case json.String:
got = value.String()
case json.Name:
got, err = value.Name()
case json.Number:
switch want.V.(type) {
case float32:
got, err = value.Float(32)
got = float32(got.(float64))
case float64:
got, err = value.Float(64)
case int32:
got, err = value.Int(32)
got = int32(got.(int64))
case int64:
got, err = value.Int(64)
case uint32:
got, err = value.Uint(32)
got = uint32(got.(uint64))
case uint64:
got, err = value.Uint(64)
}
default:
return
}
if err != nil {
if want.VE == "" {
t.Errorf("want%d: %v got unexpected error: %v", wantIdx, value, err)
} else if !strings.Contains(err.Error(), want.VE) {
t.Errorf("want#%d: %v got %q, want %q", wantIdx, value, err, want.VE)
}
return
} else {
if want.VE != "" {
t.Errorf("want#%d: %v got nil error, want %q", wantIdx, value, want.VE)
return
}
}
if got != want.V {
t.Errorf("want#%d: %v got %v, want %v", wantIdx, value, got, want.V)
}
}
func TestClone(t *testing.T) {
input := `{"outer":{"str":"hello", "number": 123}}`
dec := json.NewDecoder([]byte(input))
// Clone at the start should produce the same reads as the original.
clone := dec.Clone()
compareDecoders(t, dec, clone)
// Advance to inner object, clone and compare again.
dec.Read() // Read StartObject.
dec.Read() // Read Name.
clone = dec.Clone()
compareDecoders(t, dec, clone)
}
func compareDecoders(t *testing.T, d1 *json.Decoder, d2 *json.Decoder) {
for {
v1, err1 := d1.Read()
v2, err2 := d2.Read()
if v1.Type() != v2.Type() {
t.Errorf("cloned decoder: got Type %v, want %v", v2.Type(), v1.Type())
}
if v1.Raw() != v2.Raw() {
t.Errorf("cloned decoder: got Raw %v, want %v", v2.Raw(), v1.Raw())
}
if err1 != err2 {
t.Errorf("cloned decoder: got error %v, want %v", err2, err1)
}
if v1.Type() == json.EOF {
break
}
}
}