Skip to content

Commit

Permalink
Don't panic encoding arrays where the first item is a table, but othe…
Browse files Browse the repository at this point in the history
…rs aren't

It would try to encode this:

	arr = [{}, 0]

As:

	[[arr]]

	[[arr]]

Because it would only check the type of the first array entry. This made
sense before TOML 1.0 since all array entries had to be the same type,
but now we need to check all elements.
  • Loading branch information
arp242 committed Jun 7, 2022
1 parent 28ff18d commit 0a9f2b0
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 33 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
toml.test
/toml.test
/toml-test
28 changes: 12 additions & 16 deletions encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -551,7 +551,7 @@ func tomlTypeOfGo(rv reflect.Value) tomlType {
case reflect.Float32, reflect.Float64:
return tomlFloat
case reflect.Array, reflect.Slice:
if typeEqual(tomlHash, tomlArrayType(rv)) {
if isTableArray(rv) {
return tomlArrayHash
}
return tomlArray
Expand Down Expand Up @@ -599,29 +599,25 @@ func isMarshaler(rv reflect.Value) bool {
return false
}

// tomlArrayType returns the element type of a TOML array. The type returned
// may be nil if it cannot be determined (e.g., a nil slice or a zero length
// slize). This function may also panic if it finds a type that cannot be
// expressed in TOML (such as nil elements, heterogeneous arrays or directly
// nested arrays of tables).
func tomlArrayType(rv reflect.Value) tomlType {
if isNil(rv) || !rv.IsValid() || rv.Len() == 0 {
return nil
// isTableArray reports if all entries in the array or slice are a table.
func isTableArray(arr reflect.Value) bool {
if isNil(arr) || !arr.IsValid() || arr.Len() == 0 {
return false
}

/// Don't allow nil.
rvlen := rv.Len()
for i := 1; i < rvlen; i++ {
if tomlTypeOfGo(rv.Index(i)) == nil {
for i := 0; i < arr.Len(); i++ {
if tomlTypeOfGo(arr.Index(i)) == nil {
encPanic(errArrayNilElement)
}
}

firstType := tomlTypeOfGo(rv.Index(0))
if firstType == nil {
encPanic(errArrayNilElement)
for i := 0; i < arr.Len(); i++ {
if !typeEqual(tomlHash, tomlTypeOfGo(arr.Index(i))) {
return false
}
}
return firstType
return true
}

type tagOptions struct {
Expand Down
62 changes: 46 additions & 16 deletions fuzz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,59 @@ func FuzzDecode(f *testing.F) {
buf := make([]byte, 0, 2048)

f.Add(`
# This is a TOML document
# This is an example TOML document which shows most of its features.
title = "TOML Example"
# Simple key/value with a string.
title = "TOML example \U0001F60A"
[owner]
name = "Tom Preston-Werner"
dob = 1979-05-27T07:32:00-08:00
desc = """
An example TOML document. \
"""
[database]
enabled = true
ports = [ 8000, 8001, 8002 ]
data = [ ["delta", "phi"], [3.14] ]
temp_targets = { cpu = 79.5, case = 72.0 }
# Array with integers and floats in the various allowed formats.
integers = [42, 0x42, 0o42, 0b0110]
floats = [1.42, 1e-02]
[servers]
# Array with supported datetime formats.
times = [
2021-11-09T15:16:17+01:00, # datetime with timezone.
2021-11-09T15:16:17Z, # UTC datetime.
2021-11-09T15:16:17, # local datetime.
2021-11-09, # local date.
15:16:17, # local time.
]
[servers.alpha]
ip = "10.0.0.1"
role = "frontend"
# Durations.
duration = ["4m49s", "8m03s", "1231h15m55s"]
# Table with inline tables.
distros = [
{name = "Arch Linux", packages = "pacman"},
{name = "Void Linux", packages = "xbps"},
{name = "Debian", packages = "apt"},
]
# Create new table; note the "servers" table is created implicitly.
[servers.alpha]
# You can indent as you please, tabs or spaces.
ip = '10.0.0.1'
hostname = 'server1'
enabled = false
[servers.beta]
ip = "10.0.0.2"
role = "backend"
ip = '10.0.0.2'
hostname = 'server2'
enabled = true
# Start a new table array; note that the "characters" table is created implicitly.
[[characters.star-trek]]
name = "James Kirk"
rank = "Captain\u0012 \t"
[[characters.star-trek]]
name = "Spock"
rank = "Science officer"
[undecoded] # To show the MetaData.Undecoded() feature.
key = "This table intentionally left undecoded"
`)
f.Fuzz(func(t *testing.T, file string) {
var m map[string]interface{}
Expand Down
16 changes: 16 additions & 0 deletions internal/toml-test/tests/valid/array/mixed-string-table.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,21 @@
"value": "https://example.com/bazqux"
}
}
],
"mixed": [
{
"k": {
"type": "string",
"value": "a"
}
},
{
"type": "string",
"value": "b"
},
{
"type": "integer",
"value": "1"
}
]
}
7 changes: 7 additions & 0 deletions internal/toml-test/tests/valid/array/mixed-string-table.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,10 @@ contributors = [
"Foo Bar <foo@example.com>",
{ name = "Baz Qux", email = "bazqux@example.com", url = "https://example.com/bazqux" }
]

# Start with a table as the first element. This tests a case that some libraries
# might have where they will check if the first entry is a table/map/hash/assoc
# array and then encode it as a table array. This was a reasonable thing to do
# before TOML 1.0 since arrays could only contain one type, but now it's no
# longer.
mixed = [{k="a"}, "b", 1]

0 comments on commit 0a9f2b0

Please sign in to comment.