![JAR search and dependency download from the Maven repository](/logo.png)
com.code_intelligence.jazzer.junit.SeedArgumentsProvider Maven / Gradle / Ivy
Show all versions of jazzer-junit Show documentation
// Copyright 2022 Code Intelligence GmbH
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.code_intelligence.jazzer.junit;
import static com.code_intelligence.jazzer.junit.Utils.isFuzzing;
import static com.code_intelligence.jazzer.junit.Utils.runFromCommandLine;
import static org.junit.jupiter.api.Named.named;
import static org.junit.jupiter.params.provider.Arguments.arguments;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitOption;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.HashMap;
import java.util.Map;
import java.util.function.BiPredicate;
import java.util.stream.Stream;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.ArgumentsProvider;
class SeedArgumentsProvider implements ArgumentsProvider {
@Override
public Stream extends Arguments> provideArguments(ExtensionContext extensionContext)
throws IOException {
if (runFromCommandLine(extensionContext)) {
// libFuzzer always runs on the file-based seeds first anyway and the additional visual
// indication provided by test invocations for seeds isn't effective on the command line, so
// we skip these invocations.
return Stream.empty();
}
Class> testClass = extensionContext.getRequiredTestClass();
Method testMethod = extensionContext.getRequiredTestMethod();
Stream> rawSeeds =
Stream.of(new SimpleImmutableEntry<>("", new byte[0]));
rawSeeds = Stream.concat(rawSeeds, walkInputs(testClass, testMethod));
if (Utils.isCoverageAgentPresent()
&& Files.isDirectory(Utils.generatedCorpusPath(testClass, testMethod))) {
rawSeeds =
Stream.concat(
rawSeeds,
walkInputsInPath(
Utils.generatedCorpusPath(testClass, testMethod), Integer.MAX_VALUE));
}
SeedSerializer serializer = SeedSerializer.of(testMethod);
return rawSeeds
.map(
entry -> {
Object[] args = serializer.read(entry.getValue());
args[0] = named(entry.getKey(), args[0]);
return arguments(args);
})
.onClose(
() -> {
if (!isFuzzing(extensionContext)) {
extensionContext.publishReportEntry(
"No fuzzing has been performed, the fuzz test has only been executed on the"
+ " fixed set of inputs in the seed corpus.\n"
+ "To start fuzzing, run a test with the environment variable JAZZER_FUZZ"
+ " set to a non-empty value.");
}
if (!serializer.allReadsValid()) {
extensionContext.publishReportEntry(
"Some files in the seed corpus do not match the fuzz target signature.\n"
+ "This indicates that they were generated with a different signature and"
+ " may cause issues reproducing previous findings.");
}
});
}
/**
* Used in regression mode to get test cases for the associated {@code testMethod} This will
* return a stream of files consisting of:
*
*
* - {@code resources/
/Inputs/*}
* - {@code resources/
/Inputs//**}
*
*
* Or the equivalent behavior on resources inside a jar file.
*
* Note that the first {@code Inputs} path will not recursively search all
* directories but only gives files in that directory whereas the {@code }
* directory is searched recursively. This allows for multiple tests to share inputs without
* needing to explicitly copy them into each test's directory.
*
* @param testClass the class of the test being run
* @param testMethod the test function being run
* @return a stream of findings files to use as inputs for the test function
*/
private Stream> walkInputs(Class> testClass, Method testMethod)
throws IOException {
URL classInputsDirUrl = testClass.getResource(Utils.inputsDirectoryResourcePath(testClass));
if (classInputsDirUrl == null) {
return Stream.empty();
}
URI classInputsDirUri;
try {
classInputsDirUri = classInputsDirUrl.toURI();
} catch (URISyntaxException e) {
throw new IOException("Failed to open inputs resource directory: " + classInputsDirUrl, e);
}
if (classInputsDirUri.getScheme().equals("file")) {
// The test is executed from class files, which usually happens when run from inside an IDE.
Path classInputsPath = Paths.get(classInputsDirUri);
return Stream.concat(
walkClassInputs(classInputsPath), walkTestInputs(classInputsPath, testMethod));
} else if (classInputsDirUri.getScheme().equals("jar")) {
FileSystem jar = FileSystems.newFileSystem(classInputsDirUri, new HashMap<>());
// inputsDirUrl looks like this:
// file:/tmp/testdata/ExampleFuzzTest_deploy.jar!/com/code_intelligence/jazzer/junit/testdata/ExampleFuzzTestInputs
String pathInJar =
classInputsDirUrl.getFile().substring(classInputsDirUrl.getFile().indexOf('!') + 1);
Path classPathInJar = jar.getPath(pathInJar);
return Stream.concat(
walkClassInputs(classPathInJar), walkTestInputs(classPathInJar, testMethod))
.onClose(
() -> {
try {
jar.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
});
} else {
throw new IOException(
"Unsupported protocol for inputs resource directory: " + classInputsDirUrl);
}
}
/**
* Walks over the inputs for the method being tested, recurses into subdirectories
*
* @param classInputsPath the path of the class being tested, used as the base path where the test
* method's directory should be
* @param testMethod the method being tested
* @return a stream of all files under {@code /}
* @throws IOException can be thrown by the underlying call to {@link Files#find}
*/
private static Stream> walkTestInputs(
Path classInputsPath, Method testMethod) throws IOException {
Path testInputsPath = classInputsPath.resolve(testMethod.getName());
try {
return walkInputsInPath(testInputsPath, Integer.MAX_VALUE);
} catch (NoSuchFileException e) {
return Stream.empty();
}
}
/**
* Walks over the inputs for the class being tested. Does not recurse into subdirectories
*
* @param path the path to search to files
* @return a stream of all files (without directories) within {@code path}. If {@code path} is not
* found, an empty stream is returned.
* @throws IOException can be thrown by the underlying call to {@link Files#find}
*/
private static Stream> walkClassInputs(Path path) throws IOException {
try {
// using a depth of 1 will get all files within the given path but does not recurse into
// subdirectories
return walkInputsInPath(path, 1);
} catch (NoSuchFileException e) {
return Stream.empty();
}
}
/**
* Gets a sorted stream of all files (without directories) within under the given {@code path}
*
* @param path the path to walk
* @param depth the maximum depth of subdirectories to search from within {@code path}. 1
* indicates it should return only the files directly in {@code path} and not search any of
* its subdirectories
* @return a stream of file name -> file contents as a raw byte array
* @throws IOException can be thrown by the call to {@link Files#find(Path, int, BiPredicate,
* FileVisitOption...)}
*/
private static Stream> walkInputsInPath(Path path, int depth)
throws IOException {
// @ParameterTest automatically closes Streams and AutoCloseable instances.
// noinspection resource
return Files.find(
path,
depth,
(fileOrDir, basicFileAttributes) -> !basicFileAttributes.isDirectory(),
FileVisitOption.FOLLOW_LINKS)
// JUnit identifies individual runs of a `@ParameterizedTest` via their invocation number.
// In order to get reproducible behavior e.g. when trying to debug a particular input, all
// inputs thus have to be provided in deterministic order.
.sorted()
.map(
file ->
new SimpleImmutableEntry<>(
file.getFileName().toString(), readAllBytesUnchecked(file)));
}
private static byte[] readAllBytesUnchecked(Path path) {
try {
return Files.readAllBytes(path);
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
}