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  }