2026-09-17 19:52:45 +02:00
|
|
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
|
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
|
|
|
|
|
|
package interpres
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"math"
|
|
|
|
|
"reflect"
|
|
|
|
|
"testing"
|
|
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// FuzzParse drives the parser with arbitrary input and holds it to the
|
|
|
|
|
// round-trip invariant: every document Parse accepts must survive its own
|
|
|
|
|
// re-emission. Marshal of the parsed tree must succeed, the emitted document
|
|
|
|
|
// must parse again, and the re-parsed tree must equal the original one.
|
|
|
|
|
func FuzzParse(f *testing.F) {
|
|
|
|
|
seeds := []string{
|
|
|
|
|
"",
|
|
|
|
|
"title = \"interpres\"\n",
|
|
|
|
|
"[server]\nhost = \"localhost\"\nport = 8080\n\n[server.tls]\nenabled = true\n",
|
|
|
|
|
"[[items]]\nname = \"a\"\n\n[[items]]\nname = \"b\"\n",
|
|
|
|
|
"inline = { a = 1, b = [2, 3], c = { d = 4 } }\n",
|
|
|
|
|
"arr = [1, 2.5, \"three\", true, 1979-05-27T07:32:00Z]\n",
|
|
|
|
|
"mix = [1, {a = 2}, \"x\"]\n",
|
|
|
|
|
"when = 1979-05-27T07:32:00Z\nlocal = 1979-05-27T07:32:00.999\nd = 1979-05-27\nt = 07:32:00\n",
|
|
|
|
|
"multi = \"\"\"\nlines\n\"\"\"\nlit = 'literal'\n",
|
|
|
|
|
"esc = \"\\u0000\\t\\n\\\"\\\\\"\n",
|
|
|
|
|
"neg = -0.0\nnan = nan\ninf = -inf\nexp = 1e6\n",
|
|
|
|
|
"\"quoted key\" = 'value'\n'1979-05-27' = 1\na.b.c = { d = \"dotted\" }\n",
|
|
|
|
|
"hex = 0xFF\noct = 0o755\nbin = 0b1010\nsep = 1_000_000\n",
|
|
|
|
|
"x = \"unterminated\n",
|
|
|
|
|
"[a]\n[a]\n",
|
|
|
|
|
"n = 0x1_0000_0000_0000_0000\n",
|
2026-09-17 22:06:26 +02:00
|
|
|
// TOML 1.1 forms.
|
|
|
|
|
"t = 13:37\ndt = 1979-05-27T07:32\nodt = 1979-05-27 07:32Z\n",
|
|
|
|
|
"esc = \"\\e\\x41\\x7f\\x00\"\n",
|
|
|
|
|
"m = {\n\ta = 1,\n\tb = [1, 2,],\n\tc = { d = 2 },\n} # close\n",
|
2026-09-17 19:52:45 +02:00
|
|
|
}
|
|
|
|
|
for _, s := range seeds {
|
|
|
|
|
f.Add([]byte(s))
|
|
|
|
|
}
|
|
|
|
|
f.Fuzz(func(t *testing.T, data []byte) {
|
2026-09-20 10:40:57 +02:00
|
|
|
tree, err := ParseMap(data)
|
2026-09-17 19:52:45 +02:00
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
out, err := Marshal(tree)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("marshal of a parsed tree failed: %v\ntree: %#v", err, tree)
|
|
|
|
|
}
|
2026-09-20 10:40:57 +02:00
|
|
|
re, err := ParseMap(out)
|
2026-09-17 19:52:45 +02:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("re-parse of the emitted document failed: %v\ndoc:\n%s", err, out)
|
|
|
|
|
}
|
|
|
|
|
if !tomlEqual(tree, re) {
|
|
|
|
|
t.Fatalf("round-trip changed the tree\ninput: %q\ndoc:\n%s\nwas: %#v\nnow: %#v", data, out, tree, re)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// tomlEqual reports whether two parsed trees are equal as TOML values. It
|
|
|
|
|
// differs from reflect.DeepEqual where DeepEqual is wrong for this domain:
|
|
|
|
|
// NaN compares equal to itself, date-times compare by their canonical TOML
|
|
|
|
|
// rendering so two parses of one document stay equal, and the local variants
|
|
|
|
|
// compare through their String form, which fully determines the value.
|
|
|
|
|
func tomlEqual(a, b any) bool {
|
|
|
|
|
switch av := a.(type) {
|
|
|
|
|
case nil:
|
|
|
|
|
return b == nil
|
|
|
|
|
case float64:
|
|
|
|
|
bv, ok := b.(float64)
|
|
|
|
|
return ok && (av == bv || (math.IsNaN(av) && math.IsNaN(bv)))
|
|
|
|
|
case time.Time:
|
|
|
|
|
bv, ok := b.(time.Time)
|
|
|
|
|
return ok && av.Format(time.RFC3339Nano) == bv.Format(time.RFC3339Nano)
|
|
|
|
|
case LocalDateTime:
|
|
|
|
|
bv, ok := b.(LocalDateTime)
|
|
|
|
|
return ok && av.String() == bv.String()
|
|
|
|
|
case LocalDate:
|
|
|
|
|
bv, ok := b.(LocalDate)
|
|
|
|
|
return ok && av.String() == bv.String()
|
|
|
|
|
case LocalTime:
|
|
|
|
|
bv, ok := b.(LocalTime)
|
|
|
|
|
return ok && av.String() == bv.String()
|
|
|
|
|
case []any:
|
|
|
|
|
bv, ok := b.([]any)
|
|
|
|
|
if !ok || len(av) != len(bv) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
for i := range av {
|
|
|
|
|
if !tomlEqual(av[i], bv[i]) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
case []map[string]any:
|
|
|
|
|
bv, ok := b.([]map[string]any)
|
|
|
|
|
if !ok || len(av) != len(bv) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
for i := range av {
|
|
|
|
|
if !tomlEqual(av[i], bv[i]) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
case map[string]any:
|
|
|
|
|
bv, ok := b.(map[string]any)
|
|
|
|
|
if !ok || len(av) != len(bv) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
for k, v := range av {
|
|
|
|
|
other, ok := bv[k]
|
|
|
|
|
if !ok || !tomlEqual(v, other) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
default:
|
|
|
|
|
return reflect.DeepEqual(a, b)
|
|
|
|
|
}
|
|
|
|
|
}
|