// Copyright (c) 2026 Petr BalvĂ­n (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", // 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", } for _, s := range seeds { f.Add([]byte(s)) } f.Fuzz(func(t *testing.T, data []byte) { tree, err := ParseMap(data) if err != nil { return } out, err := Marshal(tree) if err != nil { t.Fatalf("marshal of a parsed tree failed: %v\ntree: %#v", err, tree) } re, err := ParseMap(out) 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) } }