code-intelligence.com/cifuzz@v0.40.0/internal/cmdutils/jazzer.go (about) 1 package cmdutils 2 3 import ( 4 "os" 5 "path/filepath" 6 "regexp" 7 "strings" 8 9 "github.com/mattn/go-zglob" 10 "github.com/pkg/errors" 11 12 "code-intelligence.com/cifuzz/util/fileutil" 13 "code-intelligence.com/cifuzz/util/regexutil" 14 ) 15 16 var jazzerFuzzTestRegex = regexp.MustCompile(`@FuzzTest|\sfuzzerTestOneInput\s*\(`) 17 18 func JazzerSeedCorpus(targetClass string, projectDir string) string { 19 seedCorpus := targetClass + "Inputs" 20 path := strings.Split(seedCorpus, ".") 21 path = append([]string{"src", "test", "resources"}, path...) 22 23 return filepath.Join(projectDir, filepath.Join(path...)) 24 } 25 26 // GetTargetMethodsFromJVMFuzzTestFile returns a list of target methods from 27 // a given fuzz test file. 28 func GetTargetMethodsFromJVMFuzzTestFile(path string) ([]string, error) { 29 bytes, err := os.ReadFile(path) 30 if err != nil { 31 return nil, errors.WithStack(err) 32 } 33 34 var targetMethods []string 35 36 // Regular expression pattern to match @FuzzTest and @FuzzTest() annotations 37 fuzzTestRegex := regexp.MustCompile(`@FuzzTest(\((?P<parameter>.[^\)]*)\))*\s+(?P<prefix>\w*\s)*(?P<targetName>\w+)\s*\(`) 38 matches, _ := regexutil.FindAllNamedGroupsMatches(fuzzTestRegex, string(bytes)) 39 40 // Extract the function targetName from each match and append it to the 41 // targetMethods slice 42 for _, match := range matches { 43 targetMethods = append(targetMethods, match["targetName"]) 44 } 45 46 // Check if the file contains a fuzzerTestOneInput method 47 // and append it to the targetMethods slice if it does 48 fuzzerTestOneInputRegex := regexp.MustCompile(`\sfuzzerTestOneInput\s*\(`) 49 if len(fuzzerTestOneInputRegex.FindAllStringSubmatch(string(bytes), -1)) > 0 { 50 targetMethods = append(targetMethods, "fuzzerTestOneInput") 51 } 52 53 return targetMethods, nil 54 } 55 56 // ConstructJVMFuzzTestIdentifier constructs a fully qualified class name for a 57 // given fuzz test file from the directory the file is in and the file name. 58 func ConstructJVMFuzzTestIdentifier(path, testDir string) (string, error) { 59 bytes, err := os.ReadFile(path) 60 if err != nil { 61 return "", errors.WithStack(err) 62 } 63 64 match := jazzerFuzzTestRegex.MatchString(string(bytes)) 65 if match { 66 67 classFilePath, err := filepath.Rel(testDir, path) 68 if err != nil { 69 return "", errors.WithStack(err) 70 } 71 72 className := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)) 73 74 fuzzTestIdentifier := filepath.Join( 75 filepath.Dir(classFilePath), 76 className, 77 ) 78 fuzzTestIdentifier = strings.ReplaceAll(fuzzTestIdentifier, string(os.PathSeparator), ".") 79 // remove language specific paths from identifier for example src/test/(java|kotlin) 80 fuzzTestIdentifier = strings.TrimPrefix(fuzzTestIdentifier, "java.") 81 fuzzTestIdentifier = strings.TrimPrefix(fuzzTestIdentifier, "kotlin.") 82 83 return fuzzTestIdentifier, nil 84 } 85 86 return "", nil 87 } 88 89 // ListJVMFuzzTests returns a list of all fuzz tests inside 90 // the given directories. 91 // The returned list contains the fully qualified class name of the fuzz test. 92 // to filter files based on the fqcn you can use the prefix filter parameter 93 func ListJVMFuzzTests(testDirs []string, prefixFilter string) ([]string, error) { 94 var fuzzTests []string 95 for _, testDir := range testDirs { 96 exists, err := fileutil.Exists(testDir) 97 if err != nil { 98 return nil, err 99 } 100 // skip non-existing directories 101 if !exists { 102 continue 103 } 104 105 // use zglob to support globbing in windows 106 matches, err := zglob.Glob(filepath.Join(testDir, "**", "*.{java,kt}")) 107 if err != nil { 108 return nil, errors.WithStack(err) 109 } 110 111 for _, match := range matches { 112 // Get the target methods from the fuzz test file 113 methods, err := GetTargetMethodsFromJVMFuzzTestFile(match) 114 if err != nil { 115 return nil, err 116 } 117 118 // For files with a single fuzz method, identify it only by the file name 119 if len(methods) == 1 { 120 fuzzTestIdentifier, err := ConstructJVMFuzzTestIdentifier(match, testDir) 121 if err != nil { 122 return nil, err 123 } 124 125 if fuzzTestIdentifier != "" && (prefixFilter == "" || strings.HasPrefix(fuzzTestIdentifier, prefixFilter)) { 126 fuzzTests = append(fuzzTests, fuzzTestIdentifier) 127 } 128 continue 129 } 130 131 // add the fuzz test identifier to the fuzzTests slice 132 for _, method := range methods { 133 fuzzTestIdentifier, err := ConstructJVMFuzzTestIdentifier(match, testDir) 134 if err != nil { 135 return nil, err 136 } 137 138 fuzzTestIdentifier = fuzzTestIdentifier + "::" + method 139 if fuzzTestIdentifier != "" && (prefixFilter == "" || strings.HasPrefix(fuzzTestIdentifier, prefixFilter)) { 140 // add the method name to the identifier 141 fuzzTests = append(fuzzTests, fuzzTestIdentifier) 142 } 143 } 144 } 145 } 146 return fuzzTests, nil 147 } 148 149 // SeparateTargetClassAndMethod splits up the given fuzz test into target class 150 // and method if it follows the pattern <class>::<method>. If it doesn't follow 151 // the pattern, it will return the given string and an empty string. 152 func SeparateTargetClassAndMethod(fuzzTest string) (string, string) { 153 if !strings.Contains(fuzzTest, "::") { 154 return fuzzTest, "" 155 } 156 157 split := strings.Split(fuzzTest, "::") 158 return split[0], split[1] 159 }