Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fa98989475 | |||
| 189bf9820b | |||
| 8455b10edd | |||
| 6de639d0ae | |||
| 517ceb4eb8 | |||
| dd7970eb93 | |||
| 3405e8a1d9 | |||
| 5794be6251 |
@@ -2,7 +2,7 @@
|
||||
|
||||
Go library for the [TOML](https://toml.io/en/) format.
|
||||
|
||||
This library supports [TOML v1.0.0](https://toml.io/en/v1.0.0).
|
||||
This library supports [TOML v1.1.0](https://toml.io/en/v1.1.0).
|
||||
|
||||
[🐞 Bug Reports](https://github.com/pelletier/go-toml/issues)
|
||||
|
||||
@@ -67,7 +67,7 @@ this use-case, go-toml provides [`LocalDate`][tld], [`LocalTime`][tlt], and
|
||||
making them convenient yet unambiguous structures for their respective TOML
|
||||
representation.
|
||||
|
||||
[ldt]: https://toml.io/en/v1.0.0#local-date-time
|
||||
[ldt]: https://toml.io/en/v1.1.0#local-date-time
|
||||
[tld]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#LocalDate
|
||||
[tlt]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#LocalTime
|
||||
[tldt]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#LocalDateTime
|
||||
|
||||
@@ -162,7 +162,7 @@ func parseLocalDateTime(b []byte) (LocalDateTime, []byte, error) {
|
||||
|
||||
const localDateTimeByteMinLen = 11
|
||||
if len(b) < localDateTimeByteMinLen {
|
||||
return dt, nil, unstable.NewParserError(b, "local datetimes are expected to have the format YYYY-MM-DDTHH:MM:SS[.NNNNNNNNN]")
|
||||
return dt, nil, unstable.NewParserError(b, "local datetimes are expected to have the format YYYY-MM-DDTHH:MM[:SS[.NNNNNNNNN]]")
|
||||
}
|
||||
|
||||
date, err := parseLocalDate(b[:10])
|
||||
@@ -194,10 +194,10 @@ func parseLocalTime(b []byte) (LocalTime, []byte, error) {
|
||||
t LocalTime
|
||||
)
|
||||
|
||||
// check if b matches to have expected format HH:MM:SS[.NNNNNN]
|
||||
const localTimeByteLen = 8
|
||||
if len(b) < localTimeByteLen {
|
||||
return t, nil, unstable.NewParserError(b, "times are expected to have the format HH:MM:SS[.NNNNNN]")
|
||||
// check if b matches to have expected format HH:MM[:SS[.NNNNNN]]
|
||||
const localTimeByteMinLen = 5
|
||||
if len(b) < localTimeByteMinLen {
|
||||
return t, nil, unstable.NewParserError(b, "times are expected to have the format HH:MM[:SS[.NNNNNN]]")
|
||||
}
|
||||
|
||||
var err error
|
||||
@@ -221,20 +221,25 @@ func parseLocalTime(b []byte) (LocalTime, []byte, error) {
|
||||
if t.Minute > 59 {
|
||||
return t, nil, unstable.NewParserError(b[3:5], "minutes cannot be greater 59")
|
||||
}
|
||||
if b[5] != ':' {
|
||||
return t, nil, unstable.NewParserError(b[5:6], "expecting colon between minutes and seconds")
|
||||
}
|
||||
|
||||
t.Second, err = parseDecimalDigits(b[6:8])
|
||||
if err != nil {
|
||||
return t, nil, err
|
||||
}
|
||||
b = b[5:]
|
||||
|
||||
if t.Second > 59 {
|
||||
return t, nil, unstable.NewParserError(b[6:8], "seconds cannot be greater than 59")
|
||||
}
|
||||
if len(b) >= 1 && b[0] == ':' {
|
||||
if len(b) < 3 {
|
||||
return t, nil, unstable.NewParserError(b, "incomplete seconds")
|
||||
}
|
||||
|
||||
b = b[8:]
|
||||
t.Second, err = parseDecimalDigits(b[1:3])
|
||||
if err != nil {
|
||||
return t, nil, err
|
||||
}
|
||||
|
||||
if t.Second > 59 {
|
||||
return t, nil, unstable.NewParserError(b[1:3], "seconds cannot be greater than 59")
|
||||
}
|
||||
|
||||
b = b[3:]
|
||||
}
|
||||
|
||||
if len(b) >= 1 && b[0] == '.' {
|
||||
frac := 0
|
||||
|
||||
@@ -67,6 +67,13 @@ func TestLocalTime_UnmarshalMarshalText(t *testing.T) {
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestLocalTime_UnmarshalText_WithoutSeconds(t *testing.T) {
|
||||
d := toml.LocalTime{}
|
||||
err := d.UnmarshalText([]byte("14:15"))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, toml.LocalTime{14, 15, 0, 0, 0}, d)
|
||||
}
|
||||
|
||||
func TestLocalTime_RoundTrip(t *testing.T) {
|
||||
var d struct{ A toml.LocalTime }
|
||||
err := toml.Unmarshal([]byte("a=20:12:01.500"), &d)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//go:generate go run github.com/toml-lang/toml-test/cmd/toml-test@v1.6.0 -copy ./tests
|
||||
//go:generate go run ./cmd/tomltestgen/main.go -r v1.6.0 -o toml_testgen_test.go
|
||||
//go:generate go run github.com/toml-lang/toml-test/v2/cmd/toml-test@v2.1.0 copy -toml 1.1 ./tests
|
||||
//go:generate go run ./cmd/tomltestgen/main.go -r v2.1.0 -o toml_testgen_test.go
|
||||
|
||||
package toml_test
|
||||
|
||||
|
||||
+1151
-532
File diff suppressed because it is too large
Load Diff
+479
-1
@@ -600,6 +600,96 @@ foo = "bar"`,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "local-time without seconds",
|
||||
input: `a = 14:15`,
|
||||
gen: func() test {
|
||||
var v map[string]interface{}
|
||||
|
||||
return test{
|
||||
target: &v,
|
||||
expected: &map[string]interface{}{
|
||||
"a": toml.LocalTime{Hour: 14, Minute: 15},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "local-datetime without seconds using T",
|
||||
input: `a = 2010-02-03T14:15`,
|
||||
gen: func() test {
|
||||
var v map[string]interface{}
|
||||
|
||||
return test{
|
||||
target: &v,
|
||||
expected: &map[string]interface{}{
|
||||
"a": toml.LocalDateTime{
|
||||
LocalDate: toml.LocalDate{2010, 2, 3},
|
||||
LocalTime: toml.LocalTime{Hour: 14, Minute: 15},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "local-datetime without seconds using space",
|
||||
input: `a = 2010-02-03 14:15`,
|
||||
gen: func() test {
|
||||
var v map[string]interface{}
|
||||
|
||||
return test{
|
||||
target: &v,
|
||||
expected: &map[string]interface{}{
|
||||
"a": toml.LocalDateTime{
|
||||
LocalDate: toml.LocalDate{2010, 2, 3},
|
||||
LocalTime: toml.LocalTime{Hour: 14, Minute: 15},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "datetime without seconds with Z",
|
||||
input: `a = 2010-02-03T14:15Z`,
|
||||
gen: func() test {
|
||||
var v map[string]time.Time
|
||||
|
||||
return test{
|
||||
target: &v,
|
||||
expected: &map[string]time.Time{
|
||||
"a": time.Date(2010, 2, 3, 14, 15, 0, 0, time.UTC),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "datetime without seconds with offset",
|
||||
input: `a = 2010-02-03T14:15+05:00`,
|
||||
gen: func() test {
|
||||
var v map[string]time.Time
|
||||
|
||||
return test{
|
||||
target: &v,
|
||||
expected: &map[string]time.Time{
|
||||
"a": time.Date(2010, 2, 3, 14, 15, 0, 0, time.FixedZone("", 5*3600)),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "local-time with seconds and fractional regression",
|
||||
input: `a = 14:15:30.123`,
|
||||
gen: func() test {
|
||||
var v map[string]interface{}
|
||||
|
||||
return test{
|
||||
target: &v,
|
||||
expected: &map[string]interface{}{
|
||||
"a": toml.LocalTime{Hour: 14, Minute: 15, Second: 30, Nanosecond: 123000000, Precision: 3},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "local-time missing digit",
|
||||
input: `a = 12:08:0`,
|
||||
@@ -759,6 +849,104 @@ huey = 'dewey'
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "basic string escape character",
|
||||
input: `A = "\e"`,
|
||||
gen: func() test {
|
||||
type doc struct {
|
||||
A string
|
||||
}
|
||||
|
||||
return test{
|
||||
target: &doc{},
|
||||
expected: &doc{A: "\x1B"},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "multiline basic string escape character",
|
||||
input: `A = """\e"""`,
|
||||
gen: func() test {
|
||||
type doc struct {
|
||||
A string
|
||||
}
|
||||
|
||||
return test{
|
||||
target: &doc{},
|
||||
expected: &doc{A: "\x1B"},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "escape character combined with bracket",
|
||||
input: `A = "\e["`,
|
||||
gen: func() test {
|
||||
type doc struct {
|
||||
A string
|
||||
}
|
||||
|
||||
return test{
|
||||
target: &doc{},
|
||||
expected: &doc{A: "\x1B["},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "basic string hex escape lowercase letter",
|
||||
input: `A = "\x61"`,
|
||||
gen: func() test {
|
||||
type doc struct {
|
||||
A string
|
||||
}
|
||||
|
||||
return test{
|
||||
target: &doc{},
|
||||
expected: &doc{A: "a"},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "basic string hex escape null byte",
|
||||
input: `A = "\x00"`,
|
||||
gen: func() test {
|
||||
type doc struct {
|
||||
A string
|
||||
}
|
||||
|
||||
return test{
|
||||
target: &doc{},
|
||||
expected: &doc{A: "\x00"},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "basic string hex escape max value",
|
||||
input: `A = "\xFF"`,
|
||||
gen: func() test {
|
||||
type doc struct {
|
||||
A string
|
||||
}
|
||||
|
||||
return test{
|
||||
target: &doc{},
|
||||
expected: &doc{A: "\u00FF"},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "multiline basic string hex escape",
|
||||
input: `A = """\x61"""`,
|
||||
gen: func() test {
|
||||
type doc struct {
|
||||
A string
|
||||
}
|
||||
|
||||
return test{
|
||||
target: &doc{},
|
||||
expected: &doc{A: "a"},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "spaces around dotted keys",
|
||||
input: "a . b = 1",
|
||||
@@ -906,6 +1094,87 @@ B = "data"`,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "multiline inline table",
|
||||
input: "Name = {\n First = \"hello\",\n Last = \"world\"\n}",
|
||||
gen: func() test {
|
||||
type name struct {
|
||||
First string
|
||||
Last string
|
||||
}
|
||||
type doc struct {
|
||||
Name name
|
||||
}
|
||||
|
||||
return test{
|
||||
target: &doc{},
|
||||
expected: &doc{Name: name{
|
||||
First: "hello",
|
||||
Last: "world",
|
||||
}},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "inline table with trailing comma",
|
||||
input: `Name = {First = "hello", Last = "world",}`,
|
||||
gen: func() test {
|
||||
type name struct {
|
||||
First string
|
||||
Last string
|
||||
}
|
||||
type doc struct {
|
||||
Name name
|
||||
}
|
||||
|
||||
return test{
|
||||
target: &doc{},
|
||||
expected: &doc{Name: name{
|
||||
First: "hello",
|
||||
Last: "world",
|
||||
}},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "multiline inline table with trailing comma and comments",
|
||||
input: "Name = {\n # first name\n First = \"hello\",\n # last name\n Last = \"world\",\n}",
|
||||
gen: func() test {
|
||||
type name struct {
|
||||
First string
|
||||
Last string
|
||||
}
|
||||
type doc struct {
|
||||
Name name
|
||||
}
|
||||
|
||||
return test{
|
||||
target: &doc{},
|
||||
expected: &doc{Name: name{
|
||||
First: "hello",
|
||||
Last: "world",
|
||||
}},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "nested multiline inline tables",
|
||||
input: "A = {\n B = {\n C = 1,\n },\n}",
|
||||
gen: func() test {
|
||||
var v map[string]interface{}
|
||||
|
||||
return test{
|
||||
target: &v,
|
||||
expected: &map[string]interface{}{
|
||||
"A": map[string]interface{}{
|
||||
"B": map[string]interface{}{
|
||||
"C": int64(1),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "inline table inside array",
|
||||
input: `Names = [{First = "hello", Last = "world"}, {First = "ab", Last = "cd"}]`,
|
||||
@@ -3145,7 +3414,7 @@ world'`,
|
||||
{
|
||||
desc: "bad char between minutes and seconds",
|
||||
data: `a = 2021-03-30 21:312:0`,
|
||||
msg: `expecting colon between minutes and seconds`,
|
||||
msg: `extra characters at the end of a local date time`,
|
||||
},
|
||||
{
|
||||
desc: "invalid hour value",
|
||||
@@ -3260,6 +3529,18 @@ world'`,
|
||||
desc: `invalid escape char basic multiline string`,
|
||||
data: `A = """\z"""`,
|
||||
},
|
||||
{
|
||||
desc: `invalid hex escape non-hex character in basic string`,
|
||||
data: `A = "\xGG"`,
|
||||
},
|
||||
{
|
||||
desc: `incomplete hex escape in basic string`,
|
||||
data: `A = "\x6"`,
|
||||
},
|
||||
{
|
||||
desc: `invalid hex escape non-hex character in multiline basic string`,
|
||||
data: `A = """\xGG"""`,
|
||||
},
|
||||
{
|
||||
desc: `invalid inf`,
|
||||
data: `A = ick`,
|
||||
@@ -3446,6 +3727,30 @@ world'`,
|
||||
desc: `backspace in comment`,
|
||||
data: "# this is a test\ba=1",
|
||||
},
|
||||
{
|
||||
desc: `inline table comma at start`,
|
||||
data: `a = { , b = 1 }`,
|
||||
},
|
||||
{
|
||||
desc: `inline table missing separator`,
|
||||
data: `a = { b = 1 c = 2 }`,
|
||||
},
|
||||
{
|
||||
desc: `inline table double comma across newline`,
|
||||
data: "a = { b = 1,\n, c = 2 }",
|
||||
},
|
||||
{
|
||||
desc: `incomplete inline table`,
|
||||
data: "a = { b = 1,\n",
|
||||
},
|
||||
{
|
||||
desc: `incomplete hex escape in multiline basic string`,
|
||||
data: `A = """\x6"""`,
|
||||
},
|
||||
{
|
||||
desc: `invalid escape char in basic string`,
|
||||
data: `A = "\z"`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range examples {
|
||||
@@ -4385,3 +4690,176 @@ func TestIssue1028(t *testing.T) {
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
// customFieldUnmarshaler implements unstable.Unmarshaler and captures all
|
||||
// key-value pairs directed to it, including unknown fields.
|
||||
type customFieldUnmarshaler struct {
|
||||
Values map[string]string
|
||||
}
|
||||
|
||||
func (c *customFieldUnmarshaler) UnmarshalTOML(value *unstable.Node) error {
|
||||
c.Values = map[string]string{
|
||||
"kind": value.Kind.String(),
|
||||
"data": string(value.Data),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestUnmarshalerInterface_StructFieldFallback(t *testing.T) {
|
||||
// When EnableUnmarshalerInterface is active and a struct field is not found,
|
||||
// the decoder should fall back to the Unmarshaler interface on the struct.
|
||||
type Config struct {
|
||||
Name string `toml:"name"`
|
||||
}
|
||||
|
||||
t.Run("unknown field with unmarshaler", func(t *testing.T) {
|
||||
doc := `name = "hello"
|
||||
unknown = "world"`
|
||||
var cfg Config
|
||||
decoder := toml.NewDecoder(bytes.NewReader([]byte(doc)))
|
||||
decoder.EnableUnmarshalerInterface()
|
||||
err := decoder.Decode(&cfg)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "hello", cfg.Name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUnmarshalerInterface_Value(t *testing.T) {
|
||||
// Test that EnableUnmarshalerInterface delegates value decoding
|
||||
// to the UnmarshalTOML method.
|
||||
type Config struct {
|
||||
Field customFieldUnmarshaler `toml:"field"`
|
||||
}
|
||||
|
||||
doc := `field = "test-value"`
|
||||
var cfg Config
|
||||
decoder := toml.NewDecoder(bytes.NewReader([]byte(doc)))
|
||||
decoder.EnableUnmarshalerInterface()
|
||||
err := decoder.Decode(&cfg)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "test-value", cfg.Field.Values["data"])
|
||||
}
|
||||
|
||||
func TestTypeMismatchString_StructFieldContext(t *testing.T) {
|
||||
// Exercise the typeMismatchString code path that includes struct field info
|
||||
// in the error message.
|
||||
type Inner struct {
|
||||
Value int `toml:"value"`
|
||||
}
|
||||
type Config struct {
|
||||
Inner Inner `toml:"inner"`
|
||||
}
|
||||
|
||||
doc := `inner = "not-a-table"`
|
||||
var cfg Config
|
||||
err := toml.Unmarshal([]byte(doc), &cfg)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestUnmarshalInlineTable_IncompatibleType(t *testing.T) {
|
||||
// Exercise the default branch of unmarshalInlineTable when the target
|
||||
// is not a map, struct, or interface.
|
||||
type doc struct {
|
||||
A int `toml:"a"`
|
||||
}
|
||||
var v doc
|
||||
err := toml.Unmarshal([]byte(`a = {b = 1}`), &v)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestTypeMismatchString_NoStructContext(t *testing.T) {
|
||||
// Exercise the typeMismatchString code path without struct field context (line 186).
|
||||
// Decoding a string into a bare int triggers this path.
|
||||
var v map[string]int
|
||||
err := toml.Unmarshal([]byte(`a = "hello"`), &v)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestMultilineInlineTable_EmptyWithNewlines(t *testing.T) {
|
||||
doc := "a = {\n\n}"
|
||||
var v map[string]interface{}
|
||||
err := toml.Unmarshal([]byte(doc), &v)
|
||||
assert.NoError(t, err)
|
||||
inner := v["a"]
|
||||
if inner == nil {
|
||||
t.Fatal("expected key 'a' to be present")
|
||||
}
|
||||
m, ok := inner.(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("expected map, got %T", inner)
|
||||
}
|
||||
if len(m) != 0 {
|
||||
t.Fatalf("expected empty map, got %v", m)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultilineInlineTable_CommentsOnly(t *testing.T) {
|
||||
doc := "a = {\n # just a comment\n}"
|
||||
var v map[string]interface{}
|
||||
err := toml.Unmarshal([]byte(doc), &v)
|
||||
assert.NoError(t, err)
|
||||
inner := v["a"]
|
||||
if inner == nil {
|
||||
t.Fatal("expected key 'a' to be present")
|
||||
}
|
||||
m, ok := inner.(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("expected map, got %T", inner)
|
||||
}
|
||||
if len(m) != 0 {
|
||||
t.Fatalf("expected empty map, got %v", m)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultilineInlineTable_CommentAfterComma(t *testing.T) {
|
||||
// Exercises comment handling after comma in inline table (parser lines 518-524).
|
||||
doc := "a = { b = 1, # comment\nc = 2 }"
|
||||
var v map[string]interface{}
|
||||
err := toml.Unmarshal([]byte(doc), &v)
|
||||
assert.NoError(t, err)
|
||||
m, ok := v["a"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatal("expected a map")
|
||||
}
|
||||
if m["b"] != int64(1) {
|
||||
t.Fatalf("expected b=1, got %v", m["b"])
|
||||
}
|
||||
if m["c"] != int64(2) {
|
||||
t.Fatalf("expected c=2, got %v", m["c"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultilineInlineTable_CommentAfterValue(t *testing.T) {
|
||||
// Exercises comment handling after keyval in inline table (parser lines 542-548).
|
||||
doc := "a = { b = 1 # comment\n, c = 2 }"
|
||||
var v map[string]interface{}
|
||||
err := toml.Unmarshal([]byte(doc), &v)
|
||||
assert.NoError(t, err)
|
||||
m, ok := v["a"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatal("expected a map")
|
||||
}
|
||||
if m["b"] != int64(1) {
|
||||
t.Fatalf("expected b=1, got %v", m["b"])
|
||||
}
|
||||
if m["c"] != int64(2) {
|
||||
t.Fatalf("expected c=2, got %v", m["c"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultilineInlineTable_LeadingComma(t *testing.T) {
|
||||
doc := "a = { b = 1\n, c = 2 }"
|
||||
var v map[string]interface{}
|
||||
err := toml.Unmarshal([]byte(doc), &v)
|
||||
assert.NoError(t, err)
|
||||
m, ok := v["a"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatal("expected a map")
|
||||
}
|
||||
if m["b"] != int64(1) {
|
||||
t.Fatalf("expected b=1, got %v", m["b"])
|
||||
}
|
||||
if m["c"] != int64(2) {
|
||||
t.Fatalf("expected c=2, got %v", m["c"])
|
||||
}
|
||||
}
|
||||
|
||||
+62
-14
@@ -460,12 +460,14 @@ func (p *Parser) parseLiteralString(b []byte) ([]byte, []byte, []byte, error) {
|
||||
return v, v[1 : len(v)-1], rest, nil
|
||||
}
|
||||
|
||||
//nolint:funlen,cyclop,dupl
|
||||
func (p *Parser) parseInlineTable(b []byte) (reference, []byte, error) {
|
||||
// inline-table = inline-table-open [ inline-table-keyvals ] inline-table-close
|
||||
// inline-table-open = %x7B ws ; {
|
||||
// inline-table-close = ws %x7D ; }
|
||||
// inline-table-sep = ws %x2C ws ; , Comma
|
||||
// inline-table-keyvals = keyval [ inline-table-sep inline-table-keyvals ]
|
||||
tableStart := b
|
||||
parent := p.builder.Push(Node{
|
||||
Kind: InlineTable,
|
||||
Raw: p.rangeOfToken(b[:1], b[1:]),
|
||||
@@ -473,45 +475,77 @@ func (p *Parser) parseInlineTable(b []byte) (reference, []byte, error) {
|
||||
|
||||
first := true
|
||||
|
||||
var child reference
|
||||
lastChild := invalidReference
|
||||
|
||||
addChild := func(ref reference) {
|
||||
if lastChild == invalidReference {
|
||||
p.builder.AttachChild(parent, ref)
|
||||
} else {
|
||||
p.builder.Chain(lastChild, ref)
|
||||
}
|
||||
lastChild = ref
|
||||
}
|
||||
|
||||
b = b[1:]
|
||||
|
||||
var err error
|
||||
|
||||
for len(b) > 0 {
|
||||
previousB := b
|
||||
b = p.parseWhitespace(b)
|
||||
var cref reference
|
||||
cref, b, err = p.parseOptionalWhitespaceCommentNewline(b)
|
||||
if err != nil {
|
||||
return parent, nil, err
|
||||
}
|
||||
|
||||
if cref != invalidReference {
|
||||
addChild(cref)
|
||||
}
|
||||
|
||||
if len(b) == 0 {
|
||||
return parent, nil, NewParserError(previousB[:1], "inline table is incomplete")
|
||||
return parent, nil, NewParserError(tableStart[:1], "inline table is incomplete")
|
||||
}
|
||||
|
||||
if b[0] == '}' {
|
||||
break
|
||||
}
|
||||
|
||||
if !first {
|
||||
b, err = expect(',', b)
|
||||
if b[0] == ',' {
|
||||
if first {
|
||||
return parent, nil, NewParserError(b[0:1], "inline table cannot start with comma")
|
||||
}
|
||||
b = b[1:]
|
||||
|
||||
cref, b, err = p.parseOptionalWhitespaceCommentNewline(b)
|
||||
if err != nil {
|
||||
return parent, nil, err
|
||||
}
|
||||
b = p.parseWhitespace(b)
|
||||
if cref != invalidReference {
|
||||
addChild(cref)
|
||||
}
|
||||
} else if !first {
|
||||
return parent, nil, NewParserError(b[0:1], "inline table entries must be separated by commas")
|
||||
}
|
||||
|
||||
// trailing comma: if '}' follows, stop
|
||||
if len(b) > 0 && b[0] == '}' {
|
||||
break
|
||||
}
|
||||
|
||||
var kv reference
|
||||
|
||||
kv, b, err = p.parseKeyval(b)
|
||||
if err != nil {
|
||||
return parent, nil, err
|
||||
}
|
||||
|
||||
if first {
|
||||
p.builder.AttachChild(parent, kv)
|
||||
} else {
|
||||
p.builder.Chain(child, kv)
|
||||
addChild(kv)
|
||||
|
||||
cref, b, err = p.parseOptionalWhitespaceCommentNewline(b)
|
||||
if err != nil {
|
||||
return parent, nil, err
|
||||
}
|
||||
if cref != invalidReference {
|
||||
addChild(cref)
|
||||
}
|
||||
child = kv
|
||||
|
||||
first = false
|
||||
}
|
||||
@@ -521,7 +555,7 @@ func (p *Parser) parseInlineTable(b []byte) (reference, []byte, error) {
|
||||
return parent, rest, err
|
||||
}
|
||||
|
||||
//nolint:funlen,cyclop
|
||||
//nolint:funlen,cyclop,dupl
|
||||
func (p *Parser) parseValArray(b []byte) (reference, []byte, error) {
|
||||
// array = array-open [ array-values ] ws-comment-newline array-close
|
||||
// array-open = %x5B ; [
|
||||
@@ -785,6 +819,13 @@ func (p *Parser) parseMultilineBasicString(b []byte) ([]byte, []byte, []byte, er
|
||||
builder.WriteByte('\t')
|
||||
case 'e':
|
||||
builder.WriteByte(0x1B)
|
||||
case 'x':
|
||||
x, err := hexToRune(atmost(token[i+1:], 2), 2)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
builder.WriteRune(x)
|
||||
i += 2
|
||||
case 'u':
|
||||
x, err := hexToRune(atmost(token[i+1:], 4), 4)
|
||||
if err != nil {
|
||||
@@ -944,6 +985,13 @@ func (p *Parser) parseBasicString(b []byte) ([]byte, []byte, []byte, error) {
|
||||
builder.WriteByte('\t')
|
||||
case 'e':
|
||||
builder.WriteByte(0x1B)
|
||||
case 'x':
|
||||
x, err := hexToRune(token[i+1:len(token)-1], 2)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
builder.WriteRune(x)
|
||||
i += 2
|
||||
case 'u':
|
||||
x, err := hexToRune(token[i+1:len(token)-1], 4)
|
||||
if err != nil {
|
||||
|
||||
@@ -331,6 +331,154 @@ func TestParser_AST(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "multiline inline table",
|
||||
input: "name = {\n first = \"Tom\",\n last = \"Preston-Werner\"\n}",
|
||||
ast: astNode{
|
||||
Kind: KeyValue,
|
||||
Children: []astNode{
|
||||
{
|
||||
Kind: InlineTable,
|
||||
Children: []astNode{
|
||||
{
|
||||
Kind: KeyValue,
|
||||
Children: []astNode{
|
||||
{Kind: String, Data: []byte(`Tom`)},
|
||||
{Kind: Key, Data: []byte(`first`)},
|
||||
},
|
||||
},
|
||||
{
|
||||
Kind: KeyValue,
|
||||
Children: []astNode{
|
||||
{Kind: String, Data: []byte(`Preston-Werner`)},
|
||||
{Kind: Key, Data: []byte(`last`)},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Kind: Key,
|
||||
Data: []byte(`name`),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "inline table with trailing comma",
|
||||
input: `name = { first = "Tom", last = "Preston-Werner", }`,
|
||||
ast: astNode{
|
||||
Kind: KeyValue,
|
||||
Children: []astNode{
|
||||
{
|
||||
Kind: InlineTable,
|
||||
Children: []astNode{
|
||||
{
|
||||
Kind: KeyValue,
|
||||
Children: []astNode{
|
||||
{Kind: String, Data: []byte(`Tom`)},
|
||||
{Kind: Key, Data: []byte(`first`)},
|
||||
},
|
||||
},
|
||||
{
|
||||
Kind: KeyValue,
|
||||
Children: []astNode{
|
||||
{Kind: String, Data: []byte(`Preston-Werner`)},
|
||||
{Kind: Key, Data: []byte(`last`)},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Kind: Key,
|
||||
Data: []byte(`name`),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "empty inline table with newline",
|
||||
input: "name = {\n}",
|
||||
ast: astNode{
|
||||
Kind: KeyValue,
|
||||
Children: []astNode{
|
||||
{
|
||||
Kind: InlineTable,
|
||||
Children: nil,
|
||||
},
|
||||
{
|
||||
Kind: Key,
|
||||
Data: []byte(`name`),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "inline table with leading comma",
|
||||
input: "name = { first = \"Tom\"\n, last = \"Werner\" }",
|
||||
ast: astNode{
|
||||
Kind: KeyValue,
|
||||
Children: []astNode{
|
||||
{
|
||||
Kind: InlineTable,
|
||||
Children: []astNode{
|
||||
{
|
||||
Kind: KeyValue,
|
||||
Children: []astNode{
|
||||
{Kind: String, Data: []byte(`Tom`)},
|
||||
{Kind: Key, Data: []byte(`first`)},
|
||||
},
|
||||
},
|
||||
{
|
||||
Kind: KeyValue,
|
||||
Children: []astNode{
|
||||
{Kind: String, Data: []byte(`Werner`)},
|
||||
{Kind: Key, Data: []byte(`last`)},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Kind: Key,
|
||||
Data: []byte(`name`),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "inline table with leading trailing comma",
|
||||
input: "name = { first = \"Tom\"\n, }",
|
||||
ast: astNode{
|
||||
Kind: KeyValue,
|
||||
Children: []astNode{
|
||||
{
|
||||
Kind: InlineTable,
|
||||
Children: []astNode{
|
||||
{
|
||||
Kind: KeyValue,
|
||||
Children: []astNode{
|
||||
{Kind: String, Data: []byte(`Tom`)},
|
||||
{Kind: Key, Data: []byte(`first`)},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Kind: Key,
|
||||
Data: []byte(`name`),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "inline table comma at start is error",
|
||||
input: "name = { , first = \"Tom\" }",
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
desc: "inline table double comma across newline is error",
|
||||
input: "name = { first = \"Tom\",\n, last = \"Werner\" }",
|
||||
err: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range examples {
|
||||
@@ -350,6 +498,44 @@ func TestParser_AST(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInlineTable_CommentsWithKeepComments(t *testing.T) {
|
||||
// Exercise comment reference handling inside parseInlineTable when
|
||||
// KeepComments is true. This covers the addChild(cref) branches
|
||||
// at the start of the loop, after comma, and after keyval.
|
||||
examples := []struct {
|
||||
desc string
|
||||
input string
|
||||
}{
|
||||
{
|
||||
desc: "comment at start of inline table",
|
||||
input: "a = {\n# comment\nb = 1\n}",
|
||||
},
|
||||
{
|
||||
desc: "comment after comma",
|
||||
input: "a = {b = 1,\n# comment\nc = 2\n}",
|
||||
},
|
||||
{
|
||||
desc: "comment after keyval",
|
||||
input: "a = {b = 1 # comment\n, c = 2}",
|
||||
},
|
||||
{
|
||||
desc: "comment only in inline table",
|
||||
input: "a = {\n# just a comment\n}",
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range examples {
|
||||
e := e
|
||||
t.Run(e.desc, func(t *testing.T) {
|
||||
p := Parser{KeepComments: true}
|
||||
p.Reset([]byte(e.input))
|
||||
p.NextExpression()
|
||||
err := p.Error()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkParseBasicStringWithUnicode(b *testing.B) {
|
||||
p := &Parser{}
|
||||
b.Run("4", func(b *testing.B) {
|
||||
|
||||
Reference in New Issue
Block a user