diff --git a/CHANGELOG.md b/CHANGELOG.md index 76f9546..abf6c6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,6 +71,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 cannot be carried verbatim by the literal form: a run of three single quotes, a control character, or a lone carriage return. Such values previously produced output that did not re-parse. +- A `[]any` holding only tables marshals in the value-array form with inline + tables, keeping the type `Parse` produces for such an array. It previously + took the `[[header]]` form, so a round-trip changed the value's type from + `[]any` to `[]map[string]any`. - Decoding into a `uint` destination checks the type's platform width instead of only the fixed widths, so a 32-bit `uint` no longer truncates silently; decoding a finite float beyond the `float32` range is an overflow error diff --git a/docs/API.md b/docs/API.md index 72f65c0..58d5e2a 100644 --- a/docs/API.md +++ b/docs/API.md @@ -331,6 +331,11 @@ tree, _ := interpres.Parse([]byte(`arr = [1, {a = 2}, "x"]`)) out, _ := interpres.Marshal(tree) // arr = [1, {a = 2}, "x"] ``` +A `[]any` holding only tables keeps the value-array form as well, because that +is the shape `Parse` gives a value array of inline tables; emitting it as +`[[headers]]` would re-parse as `[]map[string]any` and change the value's type +across a round-trip. + ### Empty arrays A nil slice is always omitted. An empty (length 0) array of tables is always diff --git a/encode.go b/encode.go index 1efc647..d85a397 100644 --- a/encode.go +++ b/encode.go @@ -388,6 +388,13 @@ func addArrayValue(doc *tomlDoc, name string, v reflect.Value, ctx string) error break } } + // A []any of tables is what Parse produces for a value array of inline + // tables; the [[header]] form would re-parse as []map[string]any and so + // change the value's Go type across a round-trip. The header form is + // reserved for typed table slices. + if v.Type().Elem().Kind() == reflect.Interface { + allTables = false + } if allTables { subs := make([]*tomlDoc, n) for i := range n { diff --git a/encode_test.go b/encode_test.go index e23e47d..c5e74b4 100644 --- a/encode_test.go +++ b/encode_test.go @@ -597,6 +597,36 @@ func TestMarshalMixedArrayWithInlineTable(t *testing.T) { } } +// A []any of tables is what Parse produces for a value array of inline +// tables; it must stay in the value-array form, or the output would re-parse +// as []map[string]any and the round-trip would change the value's type. +func TestMarshalValueArrayOfTablesStaysInline(t *testing.T) { + for _, doc := range []string{ + "0=[{}]", + "a = [{x = 1}, {x = 2}]\n", + "b = [{x = 1}, 2, \"three\"]\n", + } { + tree, err := Parse([]byte(doc)) + if err != nil { + t.Fatalf("%s: parse: %v", doc, err) + } + out, err := Marshal(tree) + if err != nil { + t.Fatalf("%s: marshal: %v", doc, err) + } + if bytes.HasPrefix(out, []byte("[[")) { + t.Errorf("%s: emitted the [[header]] form for a value array:\n%s", doc, out) + } + re, err := Parse(out) + if err != nil { + t.Fatalf("%s: re-parse: %v\ndoc:\n%s", doc, err, out) + } + if !tomlEqual(tree, re) { + t.Errorf("%s: round-trip changed the tree:\nwas: %#v\nnow: %#v\ndoc:\n%s", doc, tree, re, out) + } + } +} + func TestMarshalNestedInlineTables(t *testing.T) { tree := map[string]any{ "mix": []any{ diff --git a/testdata/fuzz/FuzzParse/d0e5d6fc3b865cfa b/testdata/fuzz/FuzzParse/d0e5d6fc3b865cfa new file mode 100644 index 0000000..8e4f10e --- /dev/null +++ b/testdata/fuzz/FuzzParse/d0e5d6fc3b865cfa @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("0=[{}]")