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.
|
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)
|
[🐞 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
|
making them convenient yet unambiguous structures for their respective TOML
|
||||||
representation.
|
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
|
[tld]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#LocalDate
|
||||||
[tlt]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#LocalTime
|
[tlt]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#LocalTime
|
||||||
[tldt]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#LocalDateTime
|
[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
|
const localDateTimeByteMinLen = 11
|
||||||
if len(b) < localDateTimeByteMinLen {
|
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])
|
date, err := parseLocalDate(b[:10])
|
||||||
@@ -194,10 +194,10 @@ func parseLocalTime(b []byte) (LocalTime, []byte, error) {
|
|||||||
t LocalTime
|
t LocalTime
|
||||||
)
|
)
|
||||||
|
|
||||||
// check if b matches to have expected format HH:MM:SS[.NNNNNN]
|
// check if b matches to have expected format HH:MM[:SS[.NNNNNN]]
|
||||||
const localTimeByteLen = 8
|
const localTimeByteMinLen = 5
|
||||||
if len(b) < localTimeByteLen {
|
if len(b) < localTimeByteMinLen {
|
||||||
return t, nil, unstable.NewParserError(b, "times are expected to have the format HH:MM:SS[.NNNNNN]")
|
return t, nil, unstable.NewParserError(b, "times are expected to have the format HH:MM[:SS[.NNNNNN]]")
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
@@ -221,20 +221,25 @@ func parseLocalTime(b []byte) (LocalTime, []byte, error) {
|
|||||||
if t.Minute > 59 {
|
if t.Minute > 59 {
|
||||||
return t, nil, unstable.NewParserError(b[3:5], "minutes cannot be greater 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")
|
b = b[5:]
|
||||||
|
|
||||||
|
if len(b) >= 1 && b[0] == ':' {
|
||||||
|
if len(b) < 3 {
|
||||||
|
return t, nil, unstable.NewParserError(b, "incomplete seconds")
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Second, err = parseDecimalDigits(b[6:8])
|
t.Second, err = parseDecimalDigits(b[1:3])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return t, nil, err
|
return t, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if t.Second > 59 {
|
if t.Second > 59 {
|
||||||
return t, nil, unstable.NewParserError(b[6:8], "seconds cannot be greater than 59")
|
return t, nil, unstable.NewParserError(b[1:3], "seconds cannot be greater than 59")
|
||||||
}
|
}
|
||||||
|
|
||||||
b = b[8:]
|
b = b[3:]
|
||||||
|
}
|
||||||
|
|
||||||
if len(b) >= 1 && b[0] == '.' {
|
if len(b) >= 1 && b[0] == '.' {
|
||||||
frac := 0
|
frac := 0
|
||||||
|
|||||||
@@ -67,6 +67,13 @@ func TestLocalTime_UnmarshalMarshalText(t *testing.T) {
|
|||||||
assert.Error(t, err)
|
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) {
|
func TestLocalTime_RoundTrip(t *testing.T) {
|
||||||
var d struct{ A toml.LocalTime }
|
var d struct{ A toml.LocalTime }
|
||||||
err := toml.Unmarshal([]byte("a=20:12:01.500"), &d)
|
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 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 v1.6.0 -o toml_testgen_test.go
|
//go:generate go run ./cmd/tomltestgen/main.go -r v2.1.0 -o toml_testgen_test.go
|
||||||
|
|
||||||
package toml_test
|
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",
|
desc: "local-time missing digit",
|
||||||
input: `a = 12:08:0`,
|
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",
|
desc: "spaces around dotted keys",
|
||||||
input: "a . b = 1",
|
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",
|
desc: "inline table inside array",
|
||||||
input: `Names = [{First = "hello", Last = "world"}, {First = "ab", Last = "cd"}]`,
|
input: `Names = [{First = "hello", Last = "world"}, {First = "ab", Last = "cd"}]`,
|
||||||
@@ -3145,7 +3414,7 @@ world'`,
|
|||||||
{
|
{
|
||||||
desc: "bad char between minutes and seconds",
|
desc: "bad char between minutes and seconds",
|
||||||
data: `a = 2021-03-30 21:312:0`,
|
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",
|
desc: "invalid hour value",
|
||||||
@@ -3260,6 +3529,18 @@ world'`,
|
|||||||
desc: `invalid escape char basic multiline string`,
|
desc: `invalid escape char basic multiline string`,
|
||||||
data: `A = """\z"""`,
|
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`,
|
desc: `invalid inf`,
|
||||||
data: `A = ick`,
|
data: `A = ick`,
|
||||||
@@ -3446,6 +3727,30 @@ world'`,
|
|||||||
desc: `backspace in comment`,
|
desc: `backspace in comment`,
|
||||||
data: "# this is a test\ba=1",
|
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 {
|
for _, e := range examples {
|
||||||
@@ -4385,3 +4690,176 @@ func TestIssue1028(t *testing.T) {
|
|||||||
assert.Error(t, err)
|
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
|
return v, v[1 : len(v)-1], rest, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//nolint:funlen,cyclop,dupl
|
||||||
func (p *Parser) parseInlineTable(b []byte) (reference, []byte, error) {
|
func (p *Parser) parseInlineTable(b []byte) (reference, []byte, error) {
|
||||||
// inline-table = inline-table-open [ inline-table-keyvals ] inline-table-close
|
// inline-table = inline-table-open [ inline-table-keyvals ] inline-table-close
|
||||||
// inline-table-open = %x7B ws ; {
|
// inline-table-open = %x7B ws ; {
|
||||||
// inline-table-close = ws %x7D ; }
|
// inline-table-close = ws %x7D ; }
|
||||||
// inline-table-sep = ws %x2C ws ; , Comma
|
// inline-table-sep = ws %x2C ws ; , Comma
|
||||||
// inline-table-keyvals = keyval [ inline-table-sep inline-table-keyvals ]
|
// inline-table-keyvals = keyval [ inline-table-sep inline-table-keyvals ]
|
||||||
|
tableStart := b
|
||||||
parent := p.builder.Push(Node{
|
parent := p.builder.Push(Node{
|
||||||
Kind: InlineTable,
|
Kind: InlineTable,
|
||||||
Raw: p.rangeOfToken(b[:1], b[1:]),
|
Raw: p.rangeOfToken(b[:1], b[1:]),
|
||||||
@@ -473,45 +475,77 @@ func (p *Parser) parseInlineTable(b []byte) (reference, []byte, error) {
|
|||||||
|
|
||||||
first := true
|
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:]
|
b = b[1:]
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
for len(b) > 0 {
|
for len(b) > 0 {
|
||||||
previousB := b
|
var cref reference
|
||||||
b = p.parseWhitespace(b)
|
cref, b, err = p.parseOptionalWhitespaceCommentNewline(b)
|
||||||
|
if err != nil {
|
||||||
|
return parent, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if cref != invalidReference {
|
||||||
|
addChild(cref)
|
||||||
|
}
|
||||||
|
|
||||||
if len(b) == 0 {
|
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] == '}' {
|
if b[0] == '}' {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if !first {
|
if b[0] == ',' {
|
||||||
b, err = expect(',', b)
|
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 {
|
if err != nil {
|
||||||
return parent, nil, err
|
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
|
var kv reference
|
||||||
|
|
||||||
kv, b, err = p.parseKeyval(b)
|
kv, b, err = p.parseKeyval(b)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return parent, nil, err
|
return parent, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if first {
|
addChild(kv)
|
||||||
p.builder.AttachChild(parent, kv)
|
|
||||||
} else {
|
cref, b, err = p.parseOptionalWhitespaceCommentNewline(b)
|
||||||
p.builder.Chain(child, kv)
|
if err != nil {
|
||||||
|
return parent, nil, err
|
||||||
|
}
|
||||||
|
if cref != invalidReference {
|
||||||
|
addChild(cref)
|
||||||
}
|
}
|
||||||
child = kv
|
|
||||||
|
|
||||||
first = false
|
first = false
|
||||||
}
|
}
|
||||||
@@ -521,7 +555,7 @@ func (p *Parser) parseInlineTable(b []byte) (reference, []byte, error) {
|
|||||||
return parent, rest, err
|
return parent, rest, err
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:funlen,cyclop
|
//nolint:funlen,cyclop,dupl
|
||||||
func (p *Parser) parseValArray(b []byte) (reference, []byte, error) {
|
func (p *Parser) parseValArray(b []byte) (reference, []byte, error) {
|
||||||
// array = array-open [ array-values ] ws-comment-newline array-close
|
// array = array-open [ array-values ] ws-comment-newline array-close
|
||||||
// array-open = %x5B ; [
|
// array-open = %x5B ; [
|
||||||
@@ -785,6 +819,13 @@ func (p *Parser) parseMultilineBasicString(b []byte) ([]byte, []byte, []byte, er
|
|||||||
builder.WriteByte('\t')
|
builder.WriteByte('\t')
|
||||||
case 'e':
|
case 'e':
|
||||||
builder.WriteByte(0x1B)
|
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':
|
case 'u':
|
||||||
x, err := hexToRune(atmost(token[i+1:], 4), 4)
|
x, err := hexToRune(atmost(token[i+1:], 4), 4)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -944,6 +985,13 @@ func (p *Parser) parseBasicString(b []byte) ([]byte, []byte, []byte, error) {
|
|||||||
builder.WriteByte('\t')
|
builder.WriteByte('\t')
|
||||||
case 'e':
|
case 'e':
|
||||||
builder.WriteByte(0x1B)
|
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':
|
case 'u':
|
||||||
x, err := hexToRune(token[i+1:len(token)-1], 4)
|
x, err := hexToRune(token[i+1:len(token)-1], 4)
|
||||||
if err != nil {
|
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 {
|
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) {
|
func BenchmarkParseBasicStringWithUnicode(b *testing.B) {
|
||||||
p := &Parser{}
|
p := &Parser{}
|
||||||
b.Run("4", func(b *testing.B) {
|
b.Run("4", func(b *testing.B) {
|
||||||
|
|||||||
Reference in New Issue
Block a user