Decoder: prevent duplicates of inline tables (#667)

* seen: prevent duplicates of inline tables

* Provide clearer error message for redefined keys

For example:

``
toml: key b is already defined
```
This commit is contained in:
Thomas Pelletier
2021-11-10 10:04:43 -05:00
committed by GitHub
parent 2dbd29a565
commit 4dff8eaa4d
2 changed files with 353 additions and 350 deletions
+14 -15
View File
@@ -216,35 +216,34 @@ func (s *SeenTracker) checkKeyValue(parentIdx int, node *ast.Node) error {
idx := s.find(parentIdx, k) idx := s.find(parentIdx, k)
if idx >= 0 { if idx < 0 {
if s.entries[idx].kind != tableKind { idx = s.create(parentIdx, k, tableKind, false)
return fmt.Errorf("toml: expected %s to be a table, not a %s", string(k), s.entries[idx].kind) } else {
} entry := s.entries[idx]
if s.entries[idx].explicit { if it.IsLast() {
return fmt.Errorf("toml: key %s is already defined", string(k))
} else if entry.kind != tableKind {
return fmt.Errorf("toml: expected %s to be a table, not a %s", string(k), entry.kind)
} else if entry.explicit {
return fmt.Errorf("toml: cannot redefine table %s that has already been explicitly defined", string(k)) return fmt.Errorf("toml: cannot redefine table %s that has already been explicitly defined", string(k))
} }
} else {
idx = s.create(parentIdx, k, tableKind, false)
} }
parentIdx = idx parentIdx = idx
} }
kind := valueKind s.entries[parentIdx].kind = valueKind
var err error
value := node.Value() value := node.Value()
switch value.Kind { switch value.Kind {
case ast.InlineTable: case ast.InlineTable:
kind = tableKind return s.checkInlineTable(parentIdx, value)
err = s.checkInlineTable(parentIdx, value)
case ast.Array: case ast.Array:
err = s.checkArray(parentIdx, value) return s.checkArray(parentIdx, value)
} }
s.entries[parentIdx].kind = kind return nil
return err
} }
func (s *SeenTracker) checkArray(parentIdx int, node *ast.Node) error { func (s *SeenTracker) checkArray(parentIdx int, node *ast.Node) error {
+339 -335
View File
@@ -16,6 +16,66 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func ExampleDecoder_SetStrict() {
type S struct {
Key1 string
Key3 string
}
doc := `
key1 = "value1"
key2 = "value2"
key3 = "value3"
`
r := strings.NewReader(doc)
d := toml.NewDecoder(r)
d.SetStrict(true)
s := S{}
err := d.Decode(&s)
fmt.Println(err.Error())
var details *toml.StrictMissingError
if !errors.As(err, &details) {
panic(fmt.Sprintf("err should have been a *toml.StrictMissingError, but got %s (%T)", err, err))
}
fmt.Println(details.String())
// Output:
// strict mode: fields in the document are missing in the target struct
// 2| key1 = "value1"
// 3| key2 = "value2"
// | ~~~~ missing field
// 4| key3 = "value3"
}
func ExampleUnmarshal() {
type MyConfig struct {
Version int
Name string
Tags []string
}
doc := `
version = 2
name = "go-toml"
tags = ["go", "toml"]
`
var cfg MyConfig
err := toml.Unmarshal([]byte(doc), &cfg)
if err != nil {
panic(err)
}
fmt.Println("version:", cfg.Version)
fmt.Println("name:", cfg.Name)
fmt.Println("tags:", cfg.Tags)
// Output:
// version: 2
// name: go-toml
// tags: [go toml]
}
type badReader struct{} type badReader struct{}
func (r *badReader) Read([]byte) (int, error) { func (r *badReader) Read([]byte) (int, error) {
@@ -1723,6 +1783,174 @@ func TestUnmarshalFloat32(t *testing.T) {
}) })
} }
func TestDecoderStrict(t *testing.T) {
examples := []struct {
desc string
input string
expected string
target interface{}
}{
{
desc: "multiple missing root keys",
input: `
key1 = "value1"
key2 = "missing2"
key3 = "missing3"
key4 = "value4"
`,
expected: `
2| key1 = "value1"
3| key2 = "missing2"
| ~~~~ missing field
4| key3 = "missing3"
5| key4 = "value4"
---
2| key1 = "value1"
3| key2 = "missing2"
4| key3 = "missing3"
| ~~~~ missing field
5| key4 = "value4"
`,
target: &struct {
Key1 string
Key4 string
}{},
},
{
desc: "multi-part key",
input: `a.short.key="foo"`,
expected: `
1| a.short.key="foo"
| ~~~~~~~~~~~ missing field
`,
},
{
desc: "missing table",
input: `
[foo]
bar = 42
`,
expected: `
2| [foo]
| ~~~ missing table
3| bar = 42
`,
},
{
desc: "missing array table",
input: `
[[foo]]
bar = 42
`,
expected: `
2| [[foo]]
| ~~~ missing table
3| bar = 42
`,
},
}
for _, e := range examples {
e := e
t.Run(e.desc, func(t *testing.T) {
t.Run("strict", func(t *testing.T) {
r := strings.NewReader(e.input)
d := toml.NewDecoder(r)
d.SetStrict(true)
x := e.target
if x == nil {
x = &struct{}{}
}
err := d.Decode(x)
var tsm *toml.StrictMissingError
if errors.As(err, &tsm) {
equalStringsIgnoreNewlines(t, e.expected, tsm.String())
} else {
t.Fatalf("err should have been a *toml.StrictMissingError, but got %s (%T)", err, err)
}
})
t.Run("default", func(t *testing.T) {
r := strings.NewReader(e.input)
d := toml.NewDecoder(r)
d.SetStrict(false)
x := e.target
if x == nil {
x = &struct{}{}
}
err := d.Decode(x)
require.NoError(t, err)
})
})
}
}
func TestIssue252(t *testing.T) {
type config struct {
Val1 string `toml:"val1"`
Val2 string `toml:"val2"`
}
configFile := []byte(
`
val1 = "test1"
`)
cfg := &config{
Val2: "test2",
}
err := toml.Unmarshal(configFile, cfg)
require.NoError(t, err)
require.Equal(t, "test2", cfg.Val2)
}
func TestIssue287(t *testing.T) {
b := `y=[[{}]]`
v := map[string]interface{}{}
err := toml.Unmarshal([]byte(b), &v)
require.NoError(t, err)
expected := map[string]interface{}{
"y": []interface{}{
[]interface{}{
map[string]interface{}{},
},
},
}
require.Equal(t, expected, v)
}
type (
Map458 map[string]interface{}
Slice458 []interface{}
)
func (m Map458) A(s string) Slice458 {
return m[s].([]interface{})
}
func TestIssue458(t *testing.T) {
s := []byte(`[[package]]
dependencies = ["regex"]
name = "decode"
version = "0.1.0"`)
m := Map458{}
err := toml.Unmarshal(s, &m)
require.NoError(t, err)
a := m.A("package")
expected := Slice458{
map[string]interface{}{
"dependencies": []interface{}{"regex"},
"name": "decode",
"version": "0.1.0",
},
}
assert.Equal(t, expected, a)
}
type Integer484 struct { type Integer484 struct {
Value int Value int
} }
@@ -1756,54 +1984,6 @@ func TestIssue484(t *testing.T) {
}, cfg) }, cfg)
} }
type (
Map458 map[string]interface{}
Slice458 []interface{}
)
func (m Map458) A(s string) Slice458 {
return m[s].([]interface{})
}
func TestIssue458(t *testing.T) {
s := []byte(`[[package]]
dependencies = ["regex"]
name = "decode"
version = "0.1.0"`)
m := Map458{}
err := toml.Unmarshal(s, &m)
require.NoError(t, err)
a := m.A("package")
expected := Slice458{
map[string]interface{}{
"dependencies": []interface{}{"regex"},
"name": "decode",
"version": "0.1.0",
},
}
assert.Equal(t, expected, a)
}
func TestIssue252(t *testing.T) {
type config struct {
Val1 string `toml:"val1"`
Val2 string `toml:"val2"`
}
configFile := []byte(
`
val1 = "test1"
`)
cfg := &config{
Val2: "test2",
}
err := toml.Unmarshal(configFile, cfg)
require.NoError(t, err)
require.Equal(t, "test2", cfg.Val2)
}
func TestIssue494(t *testing.T) { func TestIssue494(t *testing.T) {
data := ` data := `
foo = 2021-04-08 foo = 2021-04-08
@@ -1819,6 +1999,23 @@ bar = 2021-04-08
require.NoError(t, err) require.NoError(t, err)
} }
func TestIssue508(t *testing.T) {
type head struct {
Title string `toml:"title"`
}
type text struct {
head
}
b := []byte(`title = "This is a title"`)
t1 := text{}
err := toml.Unmarshal(b, &t1)
require.NoError(t, err)
require.Equal(t, "This is a title", t1.head.Title)
}
func TestIssue507(t *testing.T) { func TestIssue507(t *testing.T) {
data := []byte{'0', '=', '\n', '0', 'a', 'm', 'e'} data := []byte{'0', '=', '\n', '0', 'a', 'm', 'e'}
m := map[string]interface{}{} m := map[string]interface{}{}
@@ -1826,6 +2023,84 @@ func TestIssue507(t *testing.T) {
require.Error(t, err) require.Error(t, err)
} }
type uuid [16]byte
func (u *uuid) UnmarshalText(text []byte) (err error) {
// Note: the original reported issue had a more complex implementation
// of this function. But the important part is to verify that a
// non-struct type implementing UnmarshalText works with the unmarshal
// process.
placeholder := bytes.Repeat([]byte{0xAA}, 16)
copy(u[:], placeholder)
return nil
}
func TestIssue564(t *testing.T) {
type Config struct {
ID uuid
}
var config Config
err := toml.Unmarshal([]byte(`id = "0818a52b97b94768941ba1172c76cf6c"`), &config)
require.NoError(t, err)
require.Equal(t, uuid{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, config.ID)
}
func TestIssue575(t *testing.T) {
b := []byte(`
[pkg.cargo]
version = "0.55.0 (5ae8d74b3 2021-06-22)"
git_commit_hash = "a178d0322ce20e33eac124758e837cbd80a6f633"
[pkg.cargo.target.aarch64-apple-darwin]
available = true
url = "https://static.rust-lang.org/dist/2021-07-29/cargo-1.54.0-aarch64-apple-darwin.tar.gz"
hash = "7bac3901d8eb6a4191ffeebe75b29c78bcb270158ec901addb31f588d965d35d"
xz_url = "https://static.rust-lang.org/dist/2021-07-29/cargo-1.54.0-aarch64-apple-darwin.tar.xz"
xz_hash = "5207644fd6379f3e5b8ae60016b854efa55a381b0c363bff7f9b2f25bfccc430"
[pkg.cargo.target.aarch64-pc-windows-msvc]
available = true
url = "https://static.rust-lang.org/dist/2021-07-29/cargo-1.54.0-aarch64-pc-windows-msvc.tar.gz"
hash = "eb8ccd9b1f6312b06dc749c17896fa4e9c163661c273dcb61cd7a48376227f6d"
xz_url = "https://static.rust-lang.org/dist/2021-07-29/cargo-1.54.0-aarch64-pc-windows-msvc.tar.xz"
xz_hash = "1a48f723fea1f17d786ce6eadd9d00914d38062d28fd9c455ed3c3801905b388"
`)
type target struct {
XZ_URL string
}
type pkg struct {
Target map[string]target
}
type doc struct {
Pkg map[string]pkg
}
var dist doc
err := toml.Unmarshal(b, &dist)
require.NoError(t, err)
expected := doc{
Pkg: map[string]pkg{
"cargo": pkg{
Target: map[string]target{
"aarch64-apple-darwin": {
XZ_URL: "https://static.rust-lang.org/dist/2021-07-29/cargo-1.54.0-aarch64-apple-darwin.tar.xz",
},
"aarch64-pc-windows-msvc": {
XZ_URL: "https://static.rust-lang.org/dist/2021-07-29/cargo-1.54.0-aarch64-pc-windows-msvc.tar.xz",
},
},
},
},
}
require.Equal(t, expected, dist)
}
func TestIssue579(t *testing.T) { func TestIssue579(t *testing.T) {
var v interface{} var v interface{}
err := toml.Unmarshal([]byte(`[foo`), &v) err := toml.Unmarshal([]byte(`[foo`), &v)
@@ -1838,6 +2113,12 @@ func TestIssue581(t *testing.T) {
require.Error(t, err) require.Error(t, err)
} }
func TestIssue585(t *testing.T) {
var v interface{}
err := toml.Unmarshal([]byte(`a=1979-05127T 0`), &v)
require.Error(t, err)
}
func TestIssue586(t *testing.T) { func TestIssue586(t *testing.T) {
var v interface{} var v interface{}
err := toml.Unmarshal([]byte(`a={ `), &v) err := toml.Unmarshal([]byte(`a={ `), &v)
@@ -1850,12 +2131,6 @@ func TestIssue588(t *testing.T) {
require.Error(t, err) require.Error(t, err)
} }
func TestIssue585(t *testing.T) {
var v interface{}
err := toml.Unmarshal([]byte(`a=1979-05127T 0`), &v)
require.Error(t, err)
}
// Support lowercase 'T' and 'Z' // Support lowercase 'T' and 'Z'
func TestIssue600(t *testing.T) { func TestIssue600(t *testing.T) {
var v interface{} var v interface{}
@@ -1891,28 +2166,10 @@ foo = "bar"`
require.Error(t, err) require.Error(t, err)
} }
type uuid [16]byte func TestIssue631(t *testing.T) {
v := map[string]interface{}{}
func (u *uuid) UnmarshalText(text []byte) (err error) { err := toml.Unmarshal([]byte("\"\\b\u007f\"= 2"), &v)
// Note: the original reported issue had a more complex implementation require.Error(t, err)
// of this function. But the important part is to verify that a
// non-struct type implementing UnmarshalText works with the unmarshal
// process.
placeholder := bytes.Repeat([]byte{0xAA}, 16)
copy(u[:], placeholder)
return nil
}
func TestIssue564(t *testing.T) {
type Config struct {
ID uuid
}
var config Config
err := toml.Unmarshal([]byte(`id = "0818a52b97b94768941ba1172c76cf6c"`), &config)
require.NoError(t, err)
require.Equal(t, uuid{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, config.ID)
} }
func TestIssue658(t *testing.T) { func TestIssue658(t *testing.T) {
@@ -1927,6 +2184,12 @@ func TestIssue662(t *testing.T) {
require.Error(t, err) require.Error(t, err)
} }
func TestIssue666(t *testing.T) {
var v map[string]interface{}
err := toml.Unmarshal([]byte("a={}\na={}"), &v)
require.Error(t, err)
}
//nolint:funlen //nolint:funlen
func TestUnmarshalDecodeErrors(t *testing.T) { func TestUnmarshalDecodeErrors(t *testing.T) {
examples := []struct { examples := []struct {
@@ -2458,205 +2721,6 @@ func TestLocalDateTime(t *testing.T) {
} }
} }
func TestIssue287(t *testing.T) {
b := `y=[[{}]]`
v := map[string]interface{}{}
err := toml.Unmarshal([]byte(b), &v)
require.NoError(t, err)
expected := map[string]interface{}{
"y": []interface{}{
[]interface{}{
map[string]interface{}{},
},
},
}
require.Equal(t, expected, v)
}
func TestIssue508(t *testing.T) {
type head struct {
Title string `toml:"title"`
}
type text struct {
head
}
b := []byte(`title = "This is a title"`)
t1 := text{}
err := toml.Unmarshal(b, &t1)
require.NoError(t, err)
require.Equal(t, "This is a title", t1.head.Title)
}
func TestIssue575(t *testing.T) {
b := []byte(`
[pkg.cargo]
version = "0.55.0 (5ae8d74b3 2021-06-22)"
git_commit_hash = "a178d0322ce20e33eac124758e837cbd80a6f633"
[pkg.cargo.target.aarch64-apple-darwin]
available = true
url = "https://static.rust-lang.org/dist/2021-07-29/cargo-1.54.0-aarch64-apple-darwin.tar.gz"
hash = "7bac3901d8eb6a4191ffeebe75b29c78bcb270158ec901addb31f588d965d35d"
xz_url = "https://static.rust-lang.org/dist/2021-07-29/cargo-1.54.0-aarch64-apple-darwin.tar.xz"
xz_hash = "5207644fd6379f3e5b8ae60016b854efa55a381b0c363bff7f9b2f25bfccc430"
[pkg.cargo.target.aarch64-pc-windows-msvc]
available = true
url = "https://static.rust-lang.org/dist/2021-07-29/cargo-1.54.0-aarch64-pc-windows-msvc.tar.gz"
hash = "eb8ccd9b1f6312b06dc749c17896fa4e9c163661c273dcb61cd7a48376227f6d"
xz_url = "https://static.rust-lang.org/dist/2021-07-29/cargo-1.54.0-aarch64-pc-windows-msvc.tar.xz"
xz_hash = "1a48f723fea1f17d786ce6eadd9d00914d38062d28fd9c455ed3c3801905b388"
`)
type target struct {
XZ_URL string
}
type pkg struct {
Target map[string]target
}
type doc struct {
Pkg map[string]pkg
}
var dist doc
err := toml.Unmarshal(b, &dist)
require.NoError(t, err)
expected := doc{
Pkg: map[string]pkg{
"cargo": pkg{
Target: map[string]target{
"aarch64-apple-darwin": {
XZ_URL: "https://static.rust-lang.org/dist/2021-07-29/cargo-1.54.0-aarch64-apple-darwin.tar.xz",
},
"aarch64-pc-windows-msvc": {
XZ_URL: "https://static.rust-lang.org/dist/2021-07-29/cargo-1.54.0-aarch64-pc-windows-msvc.tar.xz",
},
},
},
},
}
require.Equal(t, expected, dist)
}
func TestIssue631(t *testing.T) {
v := map[string]interface{}{}
err := toml.Unmarshal([]byte("\"\\b\u007f\"= 2"), &v)
require.Error(t, err)
}
//nolint:funlen
func TestDecoderStrict(t *testing.T) {
examples := []struct {
desc string
input string
expected string
target interface{}
}{
{
desc: "multiple missing root keys",
input: `
key1 = "value1"
key2 = "missing2"
key3 = "missing3"
key4 = "value4"
`,
expected: `
2| key1 = "value1"
3| key2 = "missing2"
| ~~~~ missing field
4| key3 = "missing3"
5| key4 = "value4"
---
2| key1 = "value1"
3| key2 = "missing2"
4| key3 = "missing3"
| ~~~~ missing field
5| key4 = "value4"
`,
target: &struct {
Key1 string
Key4 string
}{},
},
{
desc: "multi-part key",
input: `a.short.key="foo"`,
expected: `
1| a.short.key="foo"
| ~~~~~~~~~~~ missing field
`,
},
{
desc: "missing table",
input: `
[foo]
bar = 42
`,
expected: `
2| [foo]
| ~~~ missing table
3| bar = 42
`,
},
{
desc: "missing array table",
input: `
[[foo]]
bar = 42
`,
expected: `
2| [[foo]]
| ~~~ missing table
3| bar = 42
`,
},
}
for _, e := range examples {
e := e
t.Run(e.desc, func(t *testing.T) {
t.Run("strict", func(t *testing.T) {
r := strings.NewReader(e.input)
d := toml.NewDecoder(r)
d.SetStrict(true)
x := e.target
if x == nil {
x = &struct{}{}
}
err := d.Decode(x)
var tsm *toml.StrictMissingError
if errors.As(err, &tsm) {
equalStringsIgnoreNewlines(t, e.expected, tsm.String())
} else {
t.Fatalf("err should have been a *toml.StrictMissingError, but got %s (%T)", err, err)
}
})
t.Run("default", func(t *testing.T) {
r := strings.NewReader(e.input)
d := toml.NewDecoder(r)
d.SetStrict(false)
x := e.target
if x == nil {
x = &struct{}{}
}
err := d.Decode(x)
require.NoError(t, err)
})
})
}
}
func TestUnmarshal_RecursiveTable(t *testing.T) { func TestUnmarshal_RecursiveTable(t *testing.T) {
type Foo struct { type Foo struct {
I int I int
@@ -2827,63 +2891,3 @@ func TestUnmarshal_RecursiveTableArray(t *testing.T) {
}) })
} }
} }
func ExampleDecoder_SetStrict() {
type S struct {
Key1 string
Key3 string
}
doc := `
key1 = "value1"
key2 = "value2"
key3 = "value3"
`
r := strings.NewReader(doc)
d := toml.NewDecoder(r)
d.SetStrict(true)
s := S{}
err := d.Decode(&s)
fmt.Println(err.Error())
var details *toml.StrictMissingError
if !errors.As(err, &details) {
panic(fmt.Sprintf("err should have been a *toml.StrictMissingError, but got %s (%T)", err, err))
}
fmt.Println(details.String())
// Output:
// strict mode: fields in the document are missing in the target struct
// 2| key1 = "value1"
// 3| key2 = "value2"
// | ~~~~ missing field
// 4| key3 = "value3"
}
func ExampleUnmarshal() {
type MyConfig struct {
Version int
Name string
Tags []string
}
doc := `
version = 2
name = "go-toml"
tags = ["go", "toml"]
`
var cfg MyConfig
err := toml.Unmarshal([]byte(doc), &cfg)
if err != nil {
panic(err)
}
fmt.Println("version:", cfg.Version)
fmt.Println("name:", cfg.Name)
fmt.Println("tags:", cfg.Tags)
// Output:
// version: 2
// name: go-toml
// tags: [go toml]
}