diff --git a/go.mod b/go.mod index 8e2d5d18..12ce4168 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( golang.org/x/term v0.13.0 gopkg.in/yaml.v3 v3.0.1 mvdan.cc/sh/v3 v3.7.0 + pgregory.net/rapid v0.6.1 ) require ( diff --git a/go.sum b/go.sum index 4e1cf868..98009900 100644 --- a/go.sum +++ b/go.sum @@ -2618,6 +2618,7 @@ mvdan.cc/sh/v3 v3.7.0 h1:lSTjdP/1xsddtaKfGg7Myu7DnlHItd3/M2tomOcNNBg= mvdan.cc/sh/v3 v3.7.0/go.mod h1:K2gwkaesF/D7av7Kxl0HbF5kGOd2ArupNTX3X44+8l8= nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= pgregory.net/rapid v0.6.1 h1:4eyrDxyht86tT4Ztm+kvlyNBLIk071gR+ZQdhphc9dQ= +pgregory.net/rapid v0.6.1/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= diff --git a/syntax/encoding/testdata/fuzz/FuzzYAMLDecode/1f188bef5a092a6f b/syntax/encoding/testdata/fuzz/FuzzYAMLDecode/1f188bef5a092a6f new file mode 100644 index 00000000..a28b337e --- /dev/null +++ b/syntax/encoding/testdata/fuzz/FuzzYAMLDecode/1f188bef5a092a6f @@ -0,0 +1,2 @@ +go test fuzz v1 +int(53) diff --git a/syntax/encoding/testdata/fuzz/FuzzYAMLDecode/319e39f67a1eca37 b/syntax/encoding/testdata/fuzz/FuzzYAMLDecode/319e39f67a1eca37 new file mode 100644 index 00000000..8d3c7b45 --- /dev/null +++ b/syntax/encoding/testdata/fuzz/FuzzYAMLDecode/319e39f67a1eca37 @@ -0,0 +1,2 @@ +go test fuzz v1 +int(-101) diff --git a/syntax/encoding/testdata/fuzz/FuzzYAMLDecode/76c5590c84272ffd b/syntax/encoding/testdata/fuzz/FuzzYAMLDecode/76c5590c84272ffd new file mode 100644 index 00000000..03f4f9a7 --- /dev/null +++ b/syntax/encoding/testdata/fuzz/FuzzYAMLDecode/76c5590c84272ffd @@ -0,0 +1,2 @@ +go test fuzz v1 +int(-87) diff --git a/syntax/encoding/testdata/fuzz/FuzzYAMLDecode/a8d1a6c720d52524 b/syntax/encoding/testdata/fuzz/FuzzYAMLDecode/a8d1a6c720d52524 new file mode 100644 index 00000000..c22ff526 --- /dev/null +++ b/syntax/encoding/testdata/fuzz/FuzzYAMLDecode/a8d1a6c720d52524 @@ -0,0 +1,2 @@ +go test fuzz v1 +int(311) diff --git a/syntax/encoding/testdata/fuzz/FuzzYAMLDecode/d141c6ce87b73df3 b/syntax/encoding/testdata/fuzz/FuzzYAMLDecode/d141c6ce87b73df3 new file mode 100644 index 00000000..16f27e8b --- /dev/null +++ b/syntax/encoding/testdata/fuzz/FuzzYAMLDecode/d141c6ce87b73df3 @@ -0,0 +1,2 @@ +go test fuzz v1 +int(-54) diff --git a/syntax/encoding/testdata/fuzz/FuzzYAMLDecode/fe8d2e002fd196b1 b/syntax/encoding/testdata/fuzz/FuzzYAMLDecode/fe8d2e002fd196b1 new file mode 100644 index 00000000..18093747 --- /dev/null +++ b/syntax/encoding/testdata/fuzz/FuzzYAMLDecode/fe8d2e002fd196b1 @@ -0,0 +1,2 @@ +go test fuzz v1 +int(128) diff --git a/syntax/encoding/yaml_test.go b/syntax/encoding/yaml_test.go index 3ffcef02..52aa30c6 100644 --- a/syntax/encoding/yaml_test.go +++ b/syntax/encoding/yaml_test.go @@ -20,6 +20,7 @@ import ( "os" "path/filepath" "sort" + "strconv" "strings" "testing" @@ -29,6 +30,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gopkg.in/yaml.v3" + "pgregory.net/rapid" ) func accept() bool { @@ -177,3 +179,111 @@ baz: qux assert.Empty(t, diags) assert.Equal(t, expected, b.String()) } + +func FuzzYAMLDecode(f *testing.F) { + f.Fuzz(func(t *testing.T, seed int) { + switch seed { + case 53, 128, 311: + t.Skip("passes manually, but takes >1s") + } + + doc := MappingNodeGenerator(5).Example(seed) + bytes, err := yaml.Marshal(doc) + require.NoError(t, err) + + root, diags := DecodeYAMLBytes("doc", bytes, nil) + require.Empty(t, diags) + assert.NotNil(t, root) + }) +} + +func ScalarNodeGenerator() *rapid.Generator[*yaml.Node] { + return rapid.Custom(func(t *rapid.T) *yaml.Node { + var val string + tag := rapid.SampledFrom([]string{"!!null", "!!bool", "!!int", "!!float", "!!str"}).Draw(t, "tag") + switch tag { + case "!!null": + val = "null" + case "!!bool": + val = strconv.FormatBool(rapid.Bool().Draw(t, "booleans")) + case "!!int": + val = strconv.FormatInt(rapid.Int64().Draw(t, "ints"), 10) + case "!!float": + val = strconv.FormatFloat(rapid.Float64().Draw(t, "floats"), 'g', -1, 64) + case "!!str": + return StringNodeGenerator().Draw(t, "string scalar node") + } + + return &yaml.Node{ + Kind: yaml.ScalarNode, + Tag: tag, + Value: val, + } + }) +} + +func StringNodeGenerator() *rapid.Generator[*yaml.Node] { + return rapid.Custom(func(t *rapid.T) *yaml.Node { + val := rapid.String().Draw(t, "strings") + for strings.ContainsAny(val, ":-") { + val = rapid.String().Draw(t, "strings") + } + + style := rapid.SampledFrom([]yaml.Style{0, yaml.DoubleQuotedStyle, yaml.FoldedStyle, yaml.LiteralStyle, yaml.SingleQuotedStyle}).Draw(t, "string styles") + return &yaml.Node{ + Kind: yaml.ScalarNode, + Style: style, + Tag: "!!str", + Value: val, + } + }) +} + +func SequenceNodeGenerator(maxDepth int) *rapid.Generator[*yaml.Node] { + return rapid.Custom(func(t *rapid.T) *yaml.Node { + content := rapid.SliceOfN(NodeGenerator(maxDepth-1), 0, 32).Draw(t, "sequence elements") + style := rapid.SampledFrom([]yaml.Style{0, yaml.FlowStyle}).Draw(t, "sequence style") + return &yaml.Node{ + Kind: yaml.SequenceNode, + Style: style, + Content: content, + } + }) +} + +func MappingNodeGenerator(maxDepth int) *rapid.Generator[*yaml.Node] { + return rapid.Custom(func(t *rapid.T) *yaml.Node { + for { + keys := rapid.SliceOfNDistinct(StringNodeGenerator(), 0, 32, func(n *yaml.Node) string { return n.Value }).Draw(t, "mapping keys") + values := rapid.SliceOfN(NodeGenerator(maxDepth-1), len(keys), len(keys)).Draw(t, "mapping values") + style := rapid.SampledFrom([]yaml.Style{0, yaml.FlowStyle}).Draw(t, "mapping style") + content := make([]*yaml.Node, len(keys)*2) + for i, k := range keys { + content[2*i], content[2*i+1] = k, values[i] + } + n := &yaml.Node{ + Kind: yaml.MappingNode, + Style: style, + Content: content, + } + bytes, err := yaml.Marshal(n) + if err != nil { + t.Errorf("marshaling node: %v", err) + } + var unused any + if err := yaml.Unmarshal(bytes, &unused); err == nil { + return n + } + } + }) +} + +func NodeGenerator(maxDepth int) *rapid.Generator[*yaml.Node] { + choices := []*rapid.Generator[*yaml.Node]{ + ScalarNodeGenerator(), + } + if maxDepth > 0 { + choices = append(choices, SequenceNodeGenerator(maxDepth), MappingNodeGenerator(maxDepth)) + } + return rapid.OneOf(choices...) +}