aboutsummaryrefslogtreecommitdiff
path: root/bazel/tools
diff options
context:
space:
mode:
Diffstat (limited to 'bazel/tools')
-rw-r--r--bazel/tools/java/com/code_intelligence/jazzer/tools/FuzzTargetTestWrapper.java291
-rw-r--r--bazel/tools/java/com/code_intelligence/jazzer/tools/JarStripper.java86
2 files changed, 269 insertions, 108 deletions
diff --git a/bazel/tools/java/com/code_intelligence/jazzer/tools/FuzzTargetTestWrapper.java b/bazel/tools/java/com/code_intelligence/jazzer/tools/FuzzTargetTestWrapper.java
index 107d8526..fe59c054 100644
--- a/bazel/tools/java/com/code_intelligence/jazzer/tools/FuzzTargetTestWrapper.java
+++ b/bazel/tools/java/com/code_intelligence/jazzer/tools/FuzzTargetTestWrapper.java
@@ -13,9 +13,16 @@
// limitations under the License.
package com.code_intelligence.jazzer.tools;
+import static java.util.stream.Collectors.toList;
+
+import com.google.devtools.build.runfiles.AutoBazelRepository;
import com.google.devtools.build.runfiles.Runfiles;
+import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.lang.ProcessBuilder.Redirect;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
@@ -25,46 +32,58 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.Comparator;
import java.util.List;
-import java.util.Map;
import java.util.Set;
+import java.util.regex.Pattern;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
import javax.tools.JavaCompiler;
import javax.tools.JavaCompiler.CompilationTask;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
+@AutoBazelRepository
public class FuzzTargetTestWrapper {
- private static final boolean JAZZER_CI = "1".equals(System.getenv("JAZZER_CI"));
+ private static final String EXCEPTION_PREFIX = "== Java Exception: ";
+ private static final String FRAME_PREFIX = "\tat ";
+ private static final Pattern SANITIZER_FINDING = Pattern.compile("^SUMMARY: \\w*Sanitizer");
+ private static final String THREAD_DUMP_HEADER = "Stack traces of all JVM threads:";
+ private static final Set<String> PUBLIC_JAZZER_PACKAGES = Collections.unmodifiableSet(
+ Stream.of("api", "replay", "sanitizers").collect(Collectors.toSet()));
public static void main(String[] args) {
Runfiles runfiles;
- String driverActualPath;
- String apiActualPath;
- String jarActualPath;
- boolean verifyCrashInput;
- boolean verifyCrashReproducer;
+ Path driverActualPath;
+ Path apiActualPath;
+ Path targetJarActualPath;
+ Path hookJarActualPath;
+ boolean shouldVerifyCrashInput;
+ boolean shouldVerifyCrashReproducer;
boolean expectCrash;
- Set<String> expectedFindings;
+ boolean usesJavaLauncher;
+ Set<String> allowedFindings;
List<String> arguments;
try {
- runfiles = Runfiles.create();
- driverActualPath = lookUpRunfile(runfiles, args[0]);
- apiActualPath = lookUpRunfile(runfiles, args[1]);
- jarActualPath = lookUpRunfile(runfiles, args[2]);
- verifyCrashInput = Boolean.parseBoolean(args[3]);
- verifyCrashReproducer = Boolean.parseBoolean(args[4]);
- expectCrash = Boolean.parseBoolean(args[5]);
- expectedFindings =
- Arrays.stream(args[6].split(",")).filter(s -> !s.isEmpty()).collect(Collectors.toSet());
+ runfiles =
+ Runfiles.preload().withSourceRepository(AutoBazelRepository_FuzzTargetTestWrapper.NAME);
+ driverActualPath = Paths.get(runfiles.rlocation(args[0]));
+ apiActualPath = Paths.get(runfiles.rlocation(args[1]));
+ targetJarActualPath = Paths.get(runfiles.rlocation(args[2]));
+ hookJarActualPath = args[3].isEmpty() ? null : Paths.get(runfiles.rlocation(args[3]));
+ shouldVerifyCrashInput = Boolean.parseBoolean(args[4]);
+ shouldVerifyCrashReproducer = Boolean.parseBoolean(args[5]);
+ expectCrash = Boolean.parseBoolean(args[6]);
+ usesJavaLauncher = Boolean.parseBoolean(args[7]);
+ allowedFindings =
+ Arrays.stream(args[8].split(",")).filter(s -> !s.isEmpty()).collect(Collectors.toSet());
// Map all files/dirs to real location
- arguments =
- Arrays.stream(args)
- .skip(7)
- .map(arg -> arg.startsWith("-") ? arg : lookUpRunfileWithFallback(runfiles, arg))
- .collect(Collectors.toList());
+ arguments = Arrays.stream(args)
+ .skip(9)
+ .map(arg -> arg.startsWith("-") ? arg : runfiles.rlocation(arg))
+ .collect(toList());
} catch (IOException | ArrayIndexOutOfBoundsException e) {
e.printStackTrace();
System.exit(1);
@@ -72,34 +91,56 @@ public class FuzzTargetTestWrapper {
}
ProcessBuilder processBuilder = new ProcessBuilder();
- Map<String, String> environment = processBuilder.environment();
// Ensure that Jazzer can find its runfiles.
- environment.putAll(runfiles.getEnvVars());
+ processBuilder.environment().putAll(runfiles.getEnvVars());
+ // Ensure that sanitizers behave consistently across OSes and use a dedicated exit code to make
+ // them distinguishable from unexpected crashes.
+ processBuilder.environment().put("ASAN_OPTIONS", "abort_on_error=0:exitcode=76");
+ processBuilder.environment().put("UBSAN_OPTIONS", "abort_on_error=0:exitcode=76");
// Crashes will be available as test outputs. These are cleared on the next run,
// so this is only useful for examples.
- String outputDir = System.getenv("TEST_UNDECLARED_OUTPUTS_DIR");
+ Path outputDir = Paths.get(System.getenv("TEST_UNDECLARED_OUTPUTS_DIR"));
List<String> command = new ArrayList<>();
- command.add(driverActualPath);
+ command.add(driverActualPath.toString());
+ if (usesJavaLauncher) {
+ if (hookJarActualPath != null) {
+ command.add(String.format("--main_advice_classpath=%s", hookJarActualPath));
+ }
+ if (System.getenv("JAZZER_DEBUG") != null) {
+ command.add("--debug");
+ }
+ } else {
+ command.add(String.format("--cp=%s",
+ hookJarActualPath == null
+ ? targetJarActualPath
+ : String.join(System.getProperty("path.separator"), targetJarActualPath.toString(),
+ hookJarActualPath.toString())));
+ }
command.add(String.format("-artifact_prefix=%s/", outputDir));
command.add(String.format("--reproducer_path=%s", outputDir));
- command.add(String.format("--cp=%s", jarActualPath));
if (System.getenv("JAZZER_NO_EXPLICIT_SEED") == null) {
command.add("-seed=2735196724");
}
command.addAll(arguments);
- processBuilder.inheritIO();
- if (JAZZER_CI) {
- // Make JVM error reports available in test outputs.
- processBuilder.environment().put(
- "JAVA_TOOL_OPTIONS", String.format("-XX:ErrorFile=%s/hs_err_pid%%p.log", outputDir));
- }
+ // Make JVM error reports available in test outputs.
+ processBuilder.environment().put(
+ "JAVA_TOOL_OPTIONS", String.format("-XX:ErrorFile=%s/hs_err_pid%%p.log", outputDir));
+ processBuilder.redirectOutput(Redirect.INHERIT);
+ processBuilder.redirectInput(Redirect.INHERIT);
processBuilder.command(command);
try {
- int exitCode = processBuilder.start().waitFor();
+ Process process = processBuilder.start();
+ try {
+ verifyFuzzerOutput(
+ process.getErrorStream(), allowedFindings, arguments.contains("--nohooks"));
+ } finally {
+ process.getErrorStream().close();
+ }
+ int exitCode = process.waitFor();
if (!expectCrash) {
if (exitCode != 0) {
System.err.printf(
@@ -108,26 +149,32 @@ public class FuzzTargetTestWrapper {
}
System.exit(0);
}
- // Assert that we either found a crash in Java (exit code 77) or a sanitizer crash (exit code
- // 76).
- if (exitCode != 76 && exitCode != 77) {
+ // Assert that we either found a crash in Java (exit code 77), a sanitizer crash (exit code
+ // 76), or a timeout (exit code 70).
+ if (exitCode != 76 && exitCode != 77
+ && !(allowedFindings.contains("timeout") && exitCode == 70)) {
System.err.printf("Did expect a crash, but Jazzer exited with exit code %d%n", exitCode);
System.exit(1);
}
- String[] outputFiles = new File(outputDir).list();
- if (outputFiles == null) {
+ List<Path> outputFiles = Files.list(outputDir).collect(toList());
+ if (outputFiles.isEmpty()) {
System.err.printf("Jazzer did not write a crashing input into %s%n", outputDir);
System.exit(1);
}
// Verify that libFuzzer dumped a crashing input.
- if (JAZZER_CI && verifyCrashInput
- && Arrays.stream(outputFiles).noneMatch(name -> name.startsWith("crash-"))) {
+ if (shouldVerifyCrashInput
+ && outputFiles.stream().noneMatch(
+ name -> name.getFileName().toString().startsWith("crash-"))
+ && !(allowedFindings.contains("timeout")
+ && outputFiles.stream().anyMatch(
+ name -> name.getFileName().toString().startsWith("timeout-")))) {
System.err.printf("No crashing input found in %s%n", outputDir);
System.exit(1);
}
// Verify that libFuzzer dumped a crash reproducer.
- if (JAZZER_CI && verifyCrashReproducer
- && Arrays.stream(outputFiles).noneMatch(name -> name.startsWith("Crash_"))) {
+ if (shouldVerifyCrashReproducer
+ && outputFiles.stream().noneMatch(
+ name -> name.getFileName().toString().startsWith("Crash_"))) {
System.err.printf("No crash reproducer found in %s%n", outputDir);
System.exit(1);
}
@@ -136,10 +183,9 @@ public class FuzzTargetTestWrapper {
System.exit(1);
}
- if (JAZZER_CI && verifyCrashReproducer) {
+ if (shouldVerifyCrashReproducer) {
try {
- verifyCrashReproducer(
- outputDir, driverActualPath, apiActualPath, jarActualPath, expectedFindings);
+ verifyCrashReproducer(outputDir, apiActualPath, targetJarActualPath, allowedFindings);
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
@@ -148,42 +194,85 @@ public class FuzzTargetTestWrapper {
System.exit(0);
}
- // Looks up a Bazel "rootpath" in this binary's runfiles and returns the resulting path.
- private static String lookUpRunfile(Runfiles runfiles, String rootpath) {
- return runfiles.rlocation(rlocationPath(rootpath));
- }
-
- // Looks up a Bazel "rootpath" in this binary's runfiles and returns the resulting path if it
- // exists. If not, returns the original path unmodified.
- private static String lookUpRunfileWithFallback(Runfiles runfiles, String rootpath) {
- String candidatePath;
- try {
- candidatePath = lookUpRunfile(runfiles, rootpath);
- } catch (IllegalArgumentException unused) {
- // The argument to Runfiles.rlocation had an invalid format, which indicates that rootpath
- // is not a Bazel "rootpath" but a user-supplied path that should be returned unchanged.
- return rootpath;
+ private static void verifyFuzzerOutput(
+ InputStream fuzzerOutput, Set<String> expectedFindings, boolean noHooks) throws IOException {
+ List<String> stackTrace;
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(fuzzerOutput))) {
+ stackTrace =
+ reader.lines()
+ .peek(System.err::println)
+ .filter(line
+ -> line.startsWith(EXCEPTION_PREFIX) || line.startsWith(FRAME_PREFIX)
+ || line.equals(THREAD_DUMP_HEADER) || SANITIZER_FINDING.matcher(line).find())
+ .collect(toList());
}
- if (new File(candidatePath).exists()) {
- return candidatePath;
- } else {
- return rootpath;
+ if (expectedFindings.isEmpty()) {
+ if (stackTrace.isEmpty()) {
+ return;
+ }
+ throw new IllegalStateException(String.format(
+ "Did not expect a finding, but got a stack trace:%n%s", String.join("\n", stackTrace)));
}
- }
-
- // Turns the result of Bazel's `$(rootpath ...)` into the correct format for rlocation.
- private static String rlocationPath(String rootpath) {
- if (rootpath.startsWith("external/")) {
- return rootpath.substring("external/".length());
- } else {
- return "jazzer/" + rootpath;
+ if (expectedFindings.contains("native")) {
+ // Expect a native sanitizer finding as well as a thread dump with at least one frame.
+ if (stackTrace.stream().noneMatch(line -> SANITIZER_FINDING.matcher(line).find())) {
+ throw new IllegalStateException("Expected native sanitizer finding, but did not get any");
+ }
+ if (!stackTrace.contains(THREAD_DUMP_HEADER) || stackTrace.size() < 3) {
+ throw new IllegalStateException(
+ "Expected stack traces for all threads, but did not get any");
+ }
+ if (expectedFindings.size() != 1) {
+ throw new IllegalStateException("Cannot expect both a native and other findings");
+ }
+ return;
+ }
+ if (expectedFindings.contains("timeout")) {
+ if (!stackTrace.contains(THREAD_DUMP_HEADER) || stackTrace.size() < 3) {
+ throw new IllegalStateException(
+ "Expected stack traces for all threads, but did not get any");
+ }
+ if (expectedFindings.size() != 1) {
+ throw new IllegalStateException("Cannot expect both a timeout and other findings");
+ }
+ return;
+ }
+ List<String> findings =
+ stackTrace.stream()
+ .filter(line -> line.startsWith(EXCEPTION_PREFIX))
+ .map(line -> line.substring(EXCEPTION_PREFIX.length()).split(":", 2)[0])
+ .collect(toList());
+ if (findings.isEmpty()) {
+ throw new IllegalStateException("Expected a crash, but did not get a stack trace");
+ }
+ for (String finding : findings) {
+ if (!expectedFindings.contains(finding)) {
+ throw new IllegalStateException(String.format("Got finding %s, but expected one of: %s",
+ findings.get(0), String.join(", ", expectedFindings)));
+ }
+ }
+ List<String> unexpectedFrames =
+ stackTrace.stream()
+ .filter(line -> line.startsWith(FRAME_PREFIX))
+ .map(line -> line.substring(FRAME_PREFIX.length()))
+ .filter(line -> line.startsWith("com.code_intelligence.jazzer."))
+ // With --nohooks, Jazzer does not filter out its own stack frames.
+ .filter(line
+ -> !noHooks
+ && !PUBLIC_JAZZER_PACKAGES.contains(
+ line.substring("com.code_intelligence.jazzer.".length()).split("\\.")[0]))
+ .collect(toList());
+ if (!unexpectedFrames.isEmpty()) {
+ throw new IllegalStateException(
+ String.format("Unexpected strack trace frames:%n%n%s%n%nin:%n%s",
+ String.join("\n", unexpectedFrames), String.join("\n", stackTrace)));
}
}
- private static void verifyCrashReproducer(String outputDir, String driver, String api, String jar,
- Set<String> expectedFindings) throws Exception {
+ private static void verifyCrashReproducer(
+ Path outputDir, Path api, Path targetJar, Set<String> expectedFindings) throws Exception {
File source =
- Files.list(Paths.get(outputDir))
+ Files.list(outputDir)
.filter(f -> f.toFile().getName().endsWith(".java"))
// Verify the crash reproducer that was created last in order to reproduce the last
// crash when using --keep_going.
@@ -191,17 +280,16 @@ public class FuzzTargetTestWrapper {
.map(Path::toFile)
.orElseThrow(
() -> new IllegalStateException("Could not find crash reproducer in " + outputDir));
- String crashReproducer = compile(source, driver, api, jar);
- execute(crashReproducer, outputDir, expectedFindings);
+ String reproducerClassName = compile(source, api, targetJar);
+ execute(reproducerClassName, outputDir, api, targetJar, expectedFindings);
}
- private static String compile(File source, String driver, String api, String jar)
- throws IOException {
+ private static String compile(File source, Path api, Path targetJar) throws IOException {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null)) {
Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjects(source);
- List<String> options =
- Arrays.asList("-classpath", String.join(File.pathSeparator, driver, api, jar));
+ List<String> options = Arrays.asList(
+ "-classpath", String.join(File.pathSeparator, api.toString(), targetJar.toString()));
System.out.printf(
"Compile crash reproducer %s with options %s%n", source.getAbsolutePath(), options);
CompilationTask task =
@@ -213,33 +301,52 @@ public class FuzzTargetTestWrapper {
}
}
- private static void execute(String classFile, String outputDir, Set<String> expectedFindings)
- throws IOException, ReflectiveOperationException {
+ private static void execute(String className, Path outputDir, Path api, Path targetJar,
+ Set<String> expectedFindings) throws IOException, ReflectiveOperationException {
try {
- System.out.printf("Execute crash reproducer %s%n", classFile);
- URLClassLoader classLoader =
- new URLClassLoader(new URL[] {new URL("file://" + outputDir + "/")});
- Class<?> crashReproducerClass = classLoader.loadClass(classFile);
+ System.out.printf("Execute crash reproducer %s%n", className);
+ URLClassLoader classLoader = new URLClassLoader(
+ new URL[] {
+ outputDir.toUri().toURL(),
+ api.toUri().toURL(),
+ targetJar.toUri().toURL(),
+ },
+ getPlatformClassLoader());
+ Class<?> crashReproducerClass = classLoader.loadClass(className);
Method main = crashReproducerClass.getMethod("main", String[].class);
System.setProperty("jazzer.is_reproducer", "true");
main.invoke(null, new Object[] {new String[] {}});
if (!expectedFindings.isEmpty()) {
throw new IllegalStateException("Expected crash with any of "
- + String.join(", ", expectedFindings) + " not reproduced by " + classFile);
+ + String.join(", ", expectedFindings) + " not reproduced by " + className);
}
System.out.println("Reproducer finished successfully without finding");
} catch (InvocationTargetException e) {
// expect the invocation to fail with the prescribed finding
Throwable finding = e.getCause();
if (expectedFindings.isEmpty()) {
- throw new IllegalStateException("Did not expect " + classFile + " to crash", finding);
+ throw new IllegalStateException("Did not expect " + className + " to crash", finding);
} else if (expectedFindings.contains(finding.getClass().getName())) {
- System.out.printf("Reproduced exception \"%s\"%n", finding.getMessage());
+ System.out.printf("Reproduced exception \"%s\"%n", finding);
} else {
throw new IllegalStateException(
- classFile + " did not crash with any of " + String.join(", ", expectedFindings),
+ className + " did not crash with any of " + String.join(", ", expectedFindings),
finding);
}
}
}
+
+ private static ClassLoader getPlatformClassLoader() {
+ try {
+ Method getter = ClassLoader.class.getMethod("getPlatformClassLoader");
+ // Java 9 and higher
+ return (ClassLoader) getter.invoke(null);
+ } catch (NoSuchMethodException e) {
+ // Java 8: All standard library classes are visible through the ClassLoader represented by
+ // null.
+ return null;
+ } catch (InvocationTargetException | IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
}
diff --git a/bazel/tools/java/com/code_intelligence/jazzer/tools/JarStripper.java b/bazel/tools/java/com/code_intelligence/jazzer/tools/JarStripper.java
index 2a567c68..72f53cd7 100644
--- a/bazel/tools/java/com/code_intelligence/jazzer/tools/JarStripper.java
+++ b/bazel/tools/java/com/code_intelligence/jazzer/tools/JarStripper.java
@@ -14,21 +14,30 @@
package com.code_intelligence.jazzer.tools;
+import static java.util.Collections.unmodifiableMap;
+import static java.util.stream.Collectors.joining;
+import static java.util.stream.Collectors.mapping;
+import static java.util.stream.Collectors.partitioningBy;
+import static java.util.stream.Collectors.toList;
+
import java.io.IOException;
+import java.io.UncheckedIOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.nio.file.PathMatcher;
import java.nio.file.Paths;
+import java.util.AbstractMap.SimpleEntry;
import java.util.Arrays;
-import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.TimeZone;
-import java.util.stream.Collectors;
+import java.util.stream.IntStream;
import java.util.stream.Stream;
public class JarStripper {
@@ -43,14 +52,23 @@ public class JarStripper {
if (args.length < 2) {
System.err.println(
"Hermetically removes files and directories from .jar files by relative paths.");
- System.err.println("Usage: in.jar out.jar [relative path]...");
+ System.err.println("Usage: in.jar out.jar [[+]path]...");
System.exit(1);
}
Path inFile = Paths.get(args[0]);
Path outFile = Paths.get(args[1]);
- Iterable<String> pathsToDelete =
- Collections.unmodifiableList(Arrays.stream(args).skip(2).collect(Collectors.toList()));
+ Map<Boolean, List<String>> rawPaths = unmodifiableMap(
+ Arrays.stream(args)
+ .skip(2)
+ .map(arg -> {
+ if (arg.startsWith("+")) {
+ return new SimpleEntry<>(true, arg.substring(1));
+ } else {
+ return new SimpleEntry<>(false, arg);
+ }
+ })
+ .collect(partitioningBy(e -> e.getKey(), mapping(e -> e.getValue(), toList()))));
try {
Files.copy(inFile, outFile);
@@ -76,19 +94,55 @@ public class JarStripper {
TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
try (FileSystem zipFs = FileSystems.newFileSystem(outUri, ZIP_FS_PROPERTIES)) {
- for (String pathToDelete : pathsToDelete) {
- // Visit files before the directory they are contained in by sorting in reverse order.
- try (Stream<Path> walk = Files.walk(zipFs.getPath(pathToDelete))) {
- Iterable<Path> subpaths =
- walk.sorted(Comparator.reverseOrder()).collect(Collectors.toList());
- for (Path subpath : subpaths) {
- Files.delete(subpath);
- }
- }
+ PathMatcher pathsToDelete = toPathMatcher(zipFs, rawPaths.get(false), false);
+ PathMatcher pathsToKeep = toPathMatcher(zipFs, rawPaths.get(true), true);
+ try (Stream<Path> walk = Files.walk(zipFs.getPath(""))) {
+ walk.sorted(Comparator.reverseOrder())
+ .filter(path
+ -> (pathsToKeep != null && !pathsToKeep.matches(path))
+ || (pathsToDelete != null && pathsToDelete.matches(path)))
+ .forEach(path -> {
+ try {
+ Files.delete(path);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ });
}
- } catch (IOException e) {
- e.printStackTrace();
+ } catch (Throwable e) {
+ Throwable throwable = e;
+ if (throwable instanceof UncheckedIOException) {
+ throwable = throwable.getCause();
+ }
+ throwable.printStackTrace();
System.exit(1);
}
}
+
+ private static PathMatcher toPathMatcher(FileSystem fs, List<String> paths, boolean keep) {
+ if (paths.isEmpty()) {
+ return null;
+ }
+ return fs.getPathMatcher(String.format("glob:{%s}",
+ paths.stream()
+ .flatMap(pattern -> keep ? toKeepGlobs(pattern) : toRemoveGlobs(pattern))
+ .collect(joining(","))));
+ }
+
+ private static Stream<String> toRemoveGlobs(String path) {
+ if (path.endsWith("/**")) {
+ // When removing all contents of a directory, also remove the directory itself.
+ return Stream.of(path, path.substring(0, path.length() - "/**".length()));
+ } else {
+ return Stream.of(path);
+ }
+ }
+
+ private static Stream<String> toKeepGlobs(String path) {
+ // When keeping something, also keep all parents.
+ String[] segments = path.split("/");
+ return Stream.concat(Stream.of(path),
+ IntStream.range(0, segments.length)
+ .mapToObj(i -> Arrays.stream(segments).limit(i).collect(joining("/"))));
+ }
}