diff --git a/example_test.go b/example_test.go index 792707a..ef44099 100644 --- a/example_test.go +++ b/example_test.go @@ -20,6 +20,7 @@ import ( "encoding/json" "fmt" "math/rand" + "strings" "github.com/google/gofuzz" ) @@ -223,3 +224,42 @@ func ExampleEnum() { } // Output: } + +func ExampleCustomString() { + a2z := "abcdefghijklmnopqrstuvwxyz" + a2z0to9 := "abcdefghijklmnopqrstuvwxyz0123456789" + + // example for generating custom string within one unicode range. + var A string + unicodeRange := fuzz.UnicodeRange{'a', 'z'} + + f := fuzz.New().Funcs(unicodeRange.CustomStringFuzzFunc()) + f.Fuzz(&A) + + for i := range A { + if !strings.ContainsRune(a2z, rune(A[i])) { + fmt.Printf("A[%d]: %v is not in range of a-z.\n", i, A[i]) + } + } + fmt.Println("Got a string, each character is selected from a-z.") + + // example for generating custom string within multiple unicode range. + var B string + unicodeRanges := fuzz.UnicodeRanges{ + {'a', 'z'}, + {'0', '9'}, // You can also use 0x0030 as 0, 0x0039 as 9. + } + ff := fuzz.New().Funcs(unicodeRanges.CustomStringFuzzFunc()) + ff.Fuzz(&B) + + for i := range B { + if !strings.ContainsRune(a2z0to9, rune(B[i])) { + fmt.Printf("A[%d]: %v is not in range list [ a-z, 0-9 ].\n", i, string(B[i])) + } + } + fmt.Println("Got a string, each character is selected from a-z, 0-9.") + + // Output: + // Got a string, each character is selected from a-z. + // Got a string, each character is selected from a-z, 0-9. +} diff --git a/fuzz.go b/fuzz.go index 1356e2d..a076b4f 100644 --- a/fuzz.go +++ b/fuzz.go @@ -271,6 +271,7 @@ func (fc *fuzzerContext) doFuzz(v reflect.Value, flags uint64) { fn(v, fc.fuzzer.r) return } + switch v.Kind() { case reflect.Map: if fc.fuzzer.genShouldFill() { @@ -503,35 +504,100 @@ type int63nPicker interface { Int63n(int64) int64 } -type charRange struct { - first, last rune +// UnicodeRange describes a sequential range of unicode characters. +// Last must be numerically greater than First. +type UnicodeRange struct { + First, Last rune } +// UnicodeRanges describes an arbitrary number of sequential ranges of unicode characters. +// To be useful, each range must have at least one character (First <= Last) and +// there must be at least one range. +type UnicodeRanges []UnicodeRange + // choose returns a random unicode character from the given range, using the // given randomness source. -func (cr charRange) choose(r int63nPicker) rune { - count := int64(cr.last - cr.first + 1) - return cr.first + rune(r.Int63n(count)) +func (ur UnicodeRange) choose(r int63nPicker) rune { + count := int64(ur.Last - ur.First + 1) + return ur.First + rune(r.Int63n(count)) +} + +// CustomStringFuzzFunc constructs a FuzzFunc which produces random strings. +// Each character is selected from the range ur. If there are no characters +// in the range (cr.Last < cr.First), this will panic. +func (ur UnicodeRange) CustomStringFuzzFunc() func(s *string, c Continue) { + ur.check() + return func(s *string, c Continue) { + *s = ur.randString(c.Rand) + } +} + +// check is a function that used to check whether the first of ur(UnicodeRange) +// is greater than the last one. +func (ur UnicodeRange) check() { + if ur.Last < ur.First { + panic("The last encoding must be greater than the first one.") + } +} + +// randString of UnicodeRange makes a random string up to 20 characters long. +// Each character is selected form ur(UnicodeRange). +func (ur UnicodeRange) randString(r *rand.Rand) string { + n := r.Intn(20) + sb := strings.Builder{} + sb.Grow(n) + for i := 0; i < n; i++ { + sb.WriteRune(ur.choose(r)) + } + return sb.String() } -var unicodeRanges = []charRange{ +// defaultUnicodeRanges sets a default unicode range when user do not set +// CustomStringFuzzFunc() but wants fuzz string. +var defaultUnicodeRanges = UnicodeRanges{ {' ', '~'}, // ASCII characters {'\u00a0', '\u02af'}, // Multi-byte encoded characters {'\u4e00', '\u9fff'}, // Common CJK (even longer encodings) } -// randString makes a random string up to 20 characters long. The returned string -// may include a variety of (valid) UTF-8 encodings. -func randString(r *rand.Rand) string { +// CustomStringFuzzFunc constructs a FuzzFunc which produces random strings. +// Each character is selected from one of the ranges of ur(UnicodeRanges). +// Each range has an equal probability of being chosen. If there are no ranges, +// or a selected range has no characters (.Last < .First), this will panic. +// Do not modify any of the ranges in ur after calling this function. +func (ur UnicodeRanges) CustomStringFuzzFunc() func(s *string, c Continue) { + // Check unicode ranges slice is empty. + if len(ur) == 0 { + panic("UnicodeRanges is empty.") + } + // if not empty, each range should be checked. + for i := range ur { + ur[i].check() + } + return func(s *string, c Continue) { + *s = ur.randString(c.Rand) + } +} + +// randString of UnicodeRanges makes a random string up to 20 characters long. +// Each character is selected form one of the ranges of ur(UnicodeRanges), +// and each range has an equal probability of being chosen. +func (ur UnicodeRanges) randString(r *rand.Rand) string { n := r.Intn(20) sb := strings.Builder{} sb.Grow(n) for i := 0; i < n; i++ { - sb.WriteRune(unicodeRanges[r.Intn(len(unicodeRanges))].choose(r)) + sb.WriteRune(ur[r.Intn(len(ur))].choose(r)) } return sb.String() } +// randString makes a random string up to 20 characters long. The returned string +// may include a variety of (valid) UTF-8 encodings. +func randString(r *rand.Rand) string { + return defaultUnicodeRanges.randString(r) +} + // randUint64 makes random 64 bit numbers. // Weirdly, rand doesn't have a function that gives you 64 random bits. func randUint64(r *rand.Rand) uint64 { diff --git a/fuzz_test.go b/fuzz_test.go index 7adbe1b..19bfb5b 100644 --- a/fuzz_test.go +++ b/fuzz_test.go @@ -20,6 +20,7 @@ import ( "math/rand" "reflect" "regexp" + "strings" "testing" "time" ) @@ -538,7 +539,7 @@ func (c customInt63) Int63n(n int64) int64 { } func Test_charRange_choose(t *testing.T) { - lowercaseLetters := charRange{'a', 'z'} + lowercaseLetters := UnicodeRange{'a', 'z'} t.Run("Picks first", func(t *testing.T) { r := customInt63{mode: modeFirst} @@ -557,6 +558,49 @@ func Test_charRange_choose(t *testing.T) { }) } +func Test_UnicodeRange_CustomStringFuzzFunc(t *testing.T) { + a2z := "abcdefghijklmnopqrstuvwxyz" + + unicodeRange := UnicodeRange{'a', 'z'} + f := New().Funcs(unicodeRange.CustomStringFuzzFunc()) + var myString string + f.Fuzz(&myString) + + t.Run("Picks a-z string", func(t *testing.T) { + for i := range myString { + if !strings.ContainsRune(a2z, rune(myString[i])) { + t.Errorf("Expected a-z, got %v", string(myString[i])) + } + } + }) +} + +func Test_UnicodeRange_Check(t *testing.T) { + unicodeRange := UnicodeRange{'a', 'z'} + + unicodeRange.check() +} + +func Test_UnicodeRanges_CustomStringFuzzFunc(t *testing.T) { + a2z0to9 := "abcdefghijklmnopqrstuvwxyz0123456789" + + unicodeRanges := UnicodeRanges{ + {'a', 'z'}, + {'0', '9'}, + } + f := New().Funcs(unicodeRanges.CustomStringFuzzFunc()) + var myString string + f.Fuzz(&myString) + + t.Run("Picks a-z0-9 string", func(t *testing.T) { + for i := range myString { + if !strings.ContainsRune(a2z0to9, rune(myString[i])) { + t.Errorf("Expected a-z0-9, got %v", string(myString[i])) + } + } + }) +} + func TestNewFromGoFuzz(t *testing.T) { t.Parallel() @@ -585,3 +629,26 @@ func BenchmarkRandString(b *testing.B) { randString(rs) } } + +func BenchmarkUnicodeRangeRandString(b *testing.B) { + unicodeRange := UnicodeRange{'a', 'z'} + + rs := rand.New(rand.NewSource(123)) + + for i := 0; i < b.N; i++ { + unicodeRange.randString(rs) + } +} + +func BenchmarkUnicodeRangesRandString(b *testing.B) { + unicodeRanges := UnicodeRanges{ + {'a', 'z'}, + {'0', '9'}, + } + + rs := rand.New(rand.NewSource(123)) + + for i := 0; i < b.N; i++ { + unicodeRanges.randString(rs) + } +}