Skip to content

Commit 0a9f2b0

Browse files
committed
Don't panic encoding arrays where the first item is a table, but others 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.
1 parent 28ff18d commit 0a9f2b0

File tree

5 files changed

+82
-33
lines changed

5 files changed

+82
-33
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
toml.test
1+
/toml.test
22
/toml-test

encode.go

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -551,7 +551,7 @@ func tomlTypeOfGo(rv reflect.Value) tomlType {
551551
case reflect.Float32, reflect.Float64:
552552
return tomlFloat
553553
case reflect.Array, reflect.Slice:
554-
if typeEqual(tomlHash, tomlArrayType(rv)) {
554+
if isTableArray(rv) {
555555
return tomlArrayHash
556556
}
557557
return tomlArray
@@ -599,29 +599,25 @@ func isMarshaler(rv reflect.Value) bool {
599599
return false
600600
}
601601

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

612608
/// Don't allow nil.
613-
rvlen := rv.Len()
614-
for i := 1; i < rvlen; i++ {
615-
if tomlTypeOfGo(rv.Index(i)) == nil {
609+
for i := 0; i < arr.Len(); i++ {
610+
if tomlTypeOfGo(arr.Index(i)) == nil {
616611
encPanic(errArrayNilElement)
617612
}
618613
}
619614

620-
firstType := tomlTypeOfGo(rv.Index(0))
621-
if firstType == nil {
622-
encPanic(errArrayNilElement)
615+
for i := 0; i < arr.Len(); i++ {
616+
if !typeEqual(tomlHash, tomlTypeOfGo(arr.Index(i))) {
617+
return false
618+
}
623619
}
624-
return firstType
620+
return true
625621
}
626622

627623
type tagOptions struct {

fuzz_test.go

Lines changed: 46 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,29 +12,59 @@ func FuzzDecode(f *testing.F) {
1212
buf := make([]byte, 0, 2048)
1313

1414
f.Add(`
15-
# This is a TOML document
15+
# This is an example TOML document which shows most of its features.
1616
17-
title = "TOML Example"
17+
# Simple key/value with a string.
18+
title = "TOML example \U0001F60A"
1819
19-
[owner]
20-
name = "Tom Preston-Werner"
21-
dob = 1979-05-27T07:32:00-08:00
20+
desc = """
21+
An example TOML document. \
22+
"""
2223
23-
[database]
24-
enabled = true
25-
ports = [ 8000, 8001, 8002 ]
26-
data = [ ["delta", "phi"], [3.14] ]
27-
temp_targets = { cpu = 79.5, case = 72.0 }
24+
# Array with integers and floats in the various allowed formats.
25+
integers = [42, 0x42, 0o42, 0b0110]
26+
floats = [1.42, 1e-02]
2827
29-
[servers]
28+
# Array with supported datetime formats.
29+
times = [
30+
2021-11-09T15:16:17+01:00, # datetime with timezone.
31+
2021-11-09T15:16:17Z, # UTC datetime.
32+
2021-11-09T15:16:17, # local datetime.
33+
2021-11-09, # local date.
34+
15:16:17, # local time.
35+
]
3036
31-
[servers.alpha]
32-
ip = "10.0.0.1"
33-
role = "frontend"
37+
# Durations.
38+
duration = ["4m49s", "8m03s", "1231h15m55s"]
39+
40+
# Table with inline tables.
41+
distros = [
42+
{name = "Arch Linux", packages = "pacman"},
43+
{name = "Void Linux", packages = "xbps"},
44+
{name = "Debian", packages = "apt"},
45+
]
3446
47+
# Create new table; note the "servers" table is created implicitly.
48+
[servers.alpha]
49+
# You can indent as you please, tabs or spaces.
50+
ip = '10.0.0.1'
51+
hostname = 'server1'
52+
enabled = false
3553
[servers.beta]
36-
ip = "10.0.0.2"
37-
role = "backend"
54+
ip = '10.0.0.2'
55+
hostname = 'server2'
56+
enabled = true
57+
58+
# Start a new table array; note that the "characters" table is created implicitly.
59+
[[characters.star-trek]]
60+
name = "James Kirk"
61+
rank = "Captain\u0012 \t"
62+
[[characters.star-trek]]
63+
name = "Spock"
64+
rank = "Science officer"
65+
66+
[undecoded] # To show the MetaData.Undecoded() feature.
67+
key = "This table intentionally left undecoded"
3868
`)
3969
f.Fuzz(func(t *testing.T, file string) {
4070
var m map[string]interface{}

internal/toml-test/tests/valid/array/mixed-string-table.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,21 @@
1818
"value": "https://example.com/bazqux"
1919
}
2020
}
21+
],
22+
"mixed": [
23+
{
24+
"k": {
25+
"type": "string",
26+
"value": "a"
27+
}
28+
},
29+
{
30+
"type": "string",
31+
"value": "b"
32+
},
33+
{
34+
"type": "integer",
35+
"value": "1"
36+
}
2137
]
2238
}

internal/toml-test/tests/valid/array/mixed-string-table.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,10 @@ contributors = [
22
"Foo Bar <[email protected]>",
33
{ name = "Baz Qux", email = "[email protected]", url = "https://example.com/bazqux" }
44
]
5+
6+
# Start with a table as the first element. This tests a case that some libraries
7+
# might have where they will check if the first entry is a table/map/hash/assoc
8+
# array and then encode it as a table array. This was a reasonable thing to do
9+
# before TOML 1.0 since arrays could only contain one type, but now it's no
10+
# longer.
11+
mixed = [{k="a"}, "b", 1]

0 commit comments

Comments
 (0)