blob: 8e3800eb77f3ff205cc0ac67d6a6b6c04ccfc7ef [file] [log] [blame]
// Copyright 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package fuzz
import (
"math"
"strconv"
"testing"
"unicode"
)
func TestUnmarshalMarshal(t *testing.T) {
var tests = []struct {
desc string
in string
reject bool
want string // if different from in
}{
{
desc: "missing version",
in: "int(1234)",
reject: true,
},
{
desc: "malformed string",
in: `go test fuzz v1
string("a"bcad")`,
reject: true,
},
{
desc: "empty value",
in: `go test fuzz v1
int()`,
reject: true,
},
{
desc: "negative uint",
in: `go test fuzz v1
uint(-32)`,
reject: true,
},
{
desc: "int8 too large",
in: `go test fuzz v1
int8(1234456)`,
reject: true,
},
{
desc: "multiplication in int value",
in: `go test fuzz v1
int(20*5)`,
reject: true,
},
{
desc: "double negation",
in: `go test fuzz v1
int(--5)`,
reject: true,
},
{
desc: "malformed bool",
in: `go test fuzz v1
bool(0)`,
reject: true,
},
{
desc: "malformed byte",
in: `go test fuzz v1
byte('aa)`,
reject: true,
},
{
desc: "byte out of range",
in: `go test fuzz v1
byte('☃')`,
reject: true,
},
{
desc: "extra newline",
in: `go test fuzz v1
string("has extra newline")
`,
want: `go test fuzz v1
string("has extra newline")`,
},
{
desc: "trailing spaces",
in: `go test fuzz v1
string("extra")
[]byte("spacing")
`,
want: `go test fuzz v1
string("extra")
[]byte("spacing")`,
},
{
desc: "float types",
in: `go test fuzz v1
float64(0)
float32(0)`,
},
{
desc: "various types",
in: `go test fuzz v1
int(-23)
int8(-2)
int64(2342425)
uint(1)
uint16(234)
uint32(352342)
uint64(123)
rune('œ')
byte('K')
byte('ÿ')
[]byte("hello¿")
[]byte("a")
bool(true)
string("hello\\xbd\\xb2=\\xbc ⌘")
float64(-12.5)
float32(2.5)`,
},
{
desc: "float edge cases",
// The two IEEE 754 bit patterns used for the math.Float{64,32}frombits
// encodings are non-math.NAN quiet-NaN values. Since they are not equal
// to math.NaN(), they should be re-encoded to their bit patterns. They
// are, respectively:
// * math.Float64bits(math.NaN())+1
// * math.Float32bits(float32(math.NaN()))+1
in: `go test fuzz v1
float32(-0)
float64(-0)
float32(+Inf)
float32(-Inf)
float32(NaN)
float64(+Inf)
float64(-Inf)
float64(NaN)
math.Float64frombits(0x7ff8000000000002)
math.Float32frombits(0x7fc00001)`,
},
{
desc: "int variations",
// Although we arbitrarily choose default integer bases (0 or 16), we may
// want to change those arbitrary choices in the future and should not
// break the parser. Verify that integers in the opposite bases still
// parse correctly.
in: `go test fuzz v1
int(0x0)
int32(0x41)
int64(0xfffffffff)
uint32(0xcafef00d)
uint64(0xffffffffffffffff)
uint8(0b0000000)
byte(0x0)
byte('\000')
byte('\u0000')
byte('\'')
math.Float64frombits(9221120237041090562)
math.Float32frombits(2143289345)`,
want: `go test fuzz v1
int(0)
rune('A')
int64(68719476735)
uint32(3405705229)
uint64(18446744073709551615)
byte('\x00')
byte('\x00')
byte('\x00')
byte('\x00')
byte('\'')
math.Float64frombits(0x7ff8000000000002)
math.Float32frombits(0x7fc00001)`,
},
{
desc: "rune validation",
in: `go test fuzz v1
rune(0)
rune(0x41)
rune(-1)
rune(0xfffd)
rune(0xd800)
rune(0x10ffff)
rune(0x110000)
`,
want: `go test fuzz v1
rune('\x00')
rune('A')
int32(-1)
rune('�')
int32(55296)
rune('\U0010ffff')
int32(1114112)`,
},
{
desc: "int overflow",
in: `go test fuzz v1
int(0x7fffffffffffffff)
uint(0xffffffffffffffff)`,
want: func() string {
switch strconv.IntSize {
case 32:
return `go test fuzz v1
int(-1)
uint(4294967295)`
case 64:
return `go test fuzz v1
int(9223372036854775807)
uint(18446744073709551615)`
default:
panic("unreachable")
}
}(),
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
vals, err := unmarshalCorpusFile([]byte(test.in))
if test.reject {
if err == nil {
t.Fatalf("unmarshal unexpected success")
}
return
}
if err != nil {
t.Fatalf("unmarshal unexpected error: %v", err)
}
newB := marshalCorpusFile(vals...)
if err != nil {
t.Fatalf("marshal unexpected error: %v", err)
}
if newB[len(newB)-1] != '\n' {
t.Error("didn't write final newline to corpus file")
}
want := test.want
if want == "" {
want = test.in
}
want += "\n"
got := string(newB)
if got != want {
t.Errorf("unexpected marshaled value\ngot:\n%s\nwant:\n%s", got, want)
}
})
}
}
// BenchmarkMarshalCorpusFile measures the time it takes to serialize byte
// slices of various sizes to a corpus file. The slice contains a repeating
// sequence of bytes 0-255 to mix escaped and non-escaped characters.
func BenchmarkMarshalCorpusFile(b *testing.B) {
buf := make([]byte, 1024*1024)
for i := 0; i < len(buf); i++ {
buf[i] = byte(i)
}
for sz := 1; sz <= len(buf); sz <<= 1 {
sz := sz
b.Run(strconv.Itoa(sz), func(b *testing.B) {
for i := 0; i < b.N; i++ {
b.SetBytes(int64(sz))
marshalCorpusFile(buf[:sz])
}
})
}
}
// BenchmarkUnmarshalCorpusfile measures the time it takes to deserialize
// files encoding byte slices of various sizes. The slice contains a repeating
// sequence of bytes 0-255 to mix escaped and non-escaped characters.
func BenchmarkUnmarshalCorpusFile(b *testing.B) {
buf := make([]byte, 1024*1024)
for i := 0; i < len(buf); i++ {
buf[i] = byte(i)
}
for sz := 1; sz <= len(buf); sz <<= 1 {
sz := sz
data := marshalCorpusFile(buf[:sz])
b.Run(strconv.Itoa(sz), func(b *testing.B) {
for i := 0; i < b.N; i++ {
b.SetBytes(int64(sz))
unmarshalCorpusFile(data)
}
})
}
}
func TestByteRoundTrip(t *testing.T) {
for x := 0; x < 256; x++ {
b1 := byte(x)
buf := marshalCorpusFile(b1)
vs, err := unmarshalCorpusFile(buf)
if err != nil {
t.Fatal(err)
}
b2 := vs[0].(byte)
if b2 != b1 {
t.Fatalf("unmarshaled %v, want %v:\n%s", b2, b1, buf)
}
}
}
func TestInt8RoundTrip(t *testing.T) {
for x := -128; x < 128; x++ {
i1 := int8(x)
buf := marshalCorpusFile(i1)
vs, err := unmarshalCorpusFile(buf)
if err != nil {
t.Fatal(err)
}
i2 := vs[0].(int8)
if i2 != i1 {
t.Fatalf("unmarshaled %v, want %v:\n%s", i2, i1, buf)
}
}
}
func FuzzFloat64RoundTrip(f *testing.F) {
f.Add(math.Float64bits(0))
f.Add(math.Float64bits(math.Copysign(0, -1)))
f.Add(math.Float64bits(math.MaxFloat64))
f.Add(math.Float64bits(math.SmallestNonzeroFloat64))
f.Add(math.Float64bits(math.NaN()))
f.Add(uint64(0x7FF0000000000001)) // signaling NaN
f.Add(math.Float64bits(math.Inf(1)))
f.Add(math.Float64bits(math.Inf(-1)))
f.Fuzz(func(t *testing.T, u1 uint64) {
x1 := math.Float64frombits(u1)
b := marshalCorpusFile(x1)
t.Logf("marshaled math.Float64frombits(0x%x):\n%s", u1, b)
xs, err := unmarshalCorpusFile(b)
if err != nil {
t.Fatal(err)
}
if len(xs) != 1 {
t.Fatalf("unmarshaled %d values", len(xs))
}
x2 := xs[0].(float64)
u2 := math.Float64bits(x2)
if u2 != u1 {
t.Errorf("unmarshaled %v (bits 0x%x)", x2, u2)
}
})
}
func FuzzRuneRoundTrip(f *testing.F) {
f.Add(rune(-1))
f.Add(rune(0xd800))
f.Add(rune(0xdfff))
f.Add(rune(unicode.ReplacementChar))
f.Add(rune(unicode.MaxASCII))
f.Add(rune(unicode.MaxLatin1))
f.Add(rune(unicode.MaxRune))
f.Add(rune(unicode.MaxRune + 1))
f.Add(rune(-0x80000000))
f.Add(rune(0x7fffffff))
f.Fuzz(func(t *testing.T, r1 rune) {
b := marshalCorpusFile(r1)
t.Logf("marshaled rune(0x%x):\n%s", r1, b)
rs, err := unmarshalCorpusFile(b)
if err != nil {
t.Fatal(err)
}
if len(rs) != 1 {
t.Fatalf("unmarshaled %d values", len(rs))
}
r2 := rs[0].(rune)
if r2 != r1 {
t.Errorf("unmarshaled rune(0x%x)", r2)
}
})
}
func FuzzStringRoundTrip(f *testing.F) {
f.Add("")
f.Add("\x00")
f.Add(string([]rune{unicode.ReplacementChar}))
f.Fuzz(func(t *testing.T, s1 string) {
b := marshalCorpusFile(s1)
t.Logf("marshaled %q:\n%s", s1, b)
rs, err := unmarshalCorpusFile(b)
if err != nil {
t.Fatal(err)
}
if len(rs) != 1 {
t.Fatalf("unmarshaled %d values", len(rs))
}
s2 := rs[0].(string)
if s2 != s1 {
t.Errorf("unmarshaled %q", s2)
}
})
}