// 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) } } // FuzzMarshal drives the encoder with generated Go values and holds it to // the same round-trip invariant FuzzParse holds the parser to: a value built // only of encodable kinds must marshal, the document must re-parse, and the // tree must equal the value it came from. func FuzzMarshal(f *testing.F) { seeds := [][]byte{ {}, {0, 0, 1, 2}, {1, 1, 2, 3, 2, 2, 3, 4}, {0, 3, 1, 9, 3, 3, 2, 8, 1, 0, 1, 7}, } for _, s := range seeds { f.Add(s) } f.Fuzz(func(t *testing.T, data []byte) { v := fuzzValue(data) out, err := Marshal(v) if err != nil { t.Fatalf("marshal of an encodable value failed: %v\nvalue: %#v", err, v) } tree, err := ParseMap(out) if err != nil { t.Fatalf("re-parse of the emitted document failed: %v\ndoc:\n%s", err, out) } if !tomlEqual(v, tree) { t.Fatalf("round-trip changed the value\nvalue: %#v\ndoc:\n%s\ntree: %#v", v, out, tree) } }) } // fuzzKeys is the fixed key pool the generated values draw from, so keys are // always valid bare keys and repeat often. var fuzzKeys = []string{"alpha", "beta", "gamma", "delta"} // fuzzValue builds a map[string]any of encodable kinds from data: integers, // positive floats, short strings, nested tables and scalar arrays. The bytes // decide the shape deterministically. func fuzzValue(data []byte) map[string]any { root := map[string]any{} cur := root depth := 0 for i := 0; i+3 < len(data); i += 4 { key := fuzzKeys[int(data[i])%len(fuzzKeys)] switch data[i+1] % 5 { case 0: cur[key] = int64(data[i+2])<<8 | int64(data[i+3]) case 1: cur[key] = float64(int(data[i+2])%1000)/8.0 + 0.125 case 2: cur[key] = string(rune('a' + int(data[i+2])%26)) case 3: cur[key] = []any{ int64(data[i+2]), float64(int(data[i+3])%100)/4.0 + 0.25, string(rune('a' + int(data[i+3])%26)), } case 4: if depth < 6 { next := map[string]any{} cur[key] = next cur = next depth++ } } } return root }