Skip to content

Commit 68fb0c7

Browse files
committed
feat: ValuePool can load files that match directory patterns
1 parent 87d348f commit 68fb0c7

9 files changed

Lines changed: 881 additions & 51 deletions

File tree

src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExecutor.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,8 @@ private static Path addInputAndSeedDirs(
178178
Paths.get(context.getConfigurationParameter("jazzer.internal.basedir").orElse(""))
179179
.toAbsolutePath();
180180

181+
System.setProperty("jazzer.internal.basedir", baseDir.toString());
182+
181183
// Use the specified corpus dir, if given, otherwise store the generated corpus in a per-class
182184
// directory under the project root.
183185
// The path is specified relative to the current working directory, which with JUnit is the

src/main/java/com/code_intelligence/jazzer/mutation/annotation/ValuePool.java

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
import java.lang.annotation.Target;
2828

2929
/**
30-
* Provides values to user-selected mutator types to start fuzzing from.
30+
* Provides values to user-selected types that will be used during mutation.
3131
*
3232
* <p>This annotation can be applied to fuzz test methods and any parameter type or subtype. By
3333
* default, this annotation is propagated to all nested subtypes unless specified otherwise via the
@@ -68,11 +68,35 @@
6868
public @interface ValuePool {
6969
/**
7070
* Specifies supplier methods that generate values for fuzzing the annotated method or type. The
71-
* specified supplier methods must be static and return a {@code Stream <?>} of values. The values
71+
* specified supplier methods must be static and return a {@code Stream<?>} of values. The values
7272
* don't need to match the type of the annotated method or parameter. The mutation framework will
7373
* extract only the values that are compatible with the target type.
7474
*/
75-
String[] value();
75+
String[] value() default {};
76+
77+
/**
78+
* Specifies glob patterns matching files that should be provided as {@code byte[]} to the
79+
* annotated type. The syntax follows closely to Java's {@link
80+
* java.nio.file.FileSystem#getPathMatcher(String) PathMatcher} "glob:" syntax.
81+
*
82+
* <p>Relative glob patterns are resolved against the working directory.
83+
*
84+
* <p><i>Note: Patterns that start with <code>{</code> or <code>[</code> are treated as relative
85+
* to the working directory.</i>
86+
*
87+
* <p>Examples:
88+
*
89+
* <ul>
90+
* <li>{@code *.jpeg} - matches all jpegs in the working directory
91+
* <li>{@code **.xml} - matches all xml files recursively
92+
* <li>{@code src/test/resources/dict/*.txt} - matches txt files in a specific directory
93+
* <li>{@code /absolute/path/to/some/directory/**} - matches all files in an absolute path
94+
* recursively
95+
* <li><code>{"*.jpg", "**.png"}</code> - matches all jpg in the working directory, and png
96+
* files recursively
97+
* </ul>
98+
*/
99+
String[] files() default {};
76100

77101
/**
78102
* This {@code ValuePool} will be used with probability {@code p} by the mutator responsible for
@@ -82,7 +106,7 @@
82106

83107
/**
84108
* Defines the scope of the annotation. Possible values are defined in {@link
85-
* com.code_intelligence.jazzer.mutation.utils.PropertyConstraint}. By default it's {@code
109+
* com.code_intelligence.jazzer.mutation.utils.PropertyConstraint}. By default, it's {@code
86110
* RECURSIVE}.
87111
*/
88112
String constraint() default RECURSIVE;

src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/ValuePoolMutatorFactory.java

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@
4141
import java.util.Optional;
4242
import java.util.function.Predicate;
4343
import java.util.stream.Collectors;
44-
import java.util.stream.Stream;
4544

4645
public class ValuePoolMutatorFactory implements MutatorFactory {
4746
/** Types annotated with this marker wil not be re-wrapped by this factory. */
@@ -91,14 +90,9 @@ static <T> SerializingMutator<T> wrapIfValuesExist(
9190
return mutator;
9291
}
9392

94-
Optional<Stream<?>> rawUserValues = valuePoolRegistry.extractRawValues(type);
95-
if (!rawUserValues.isPresent()) {
96-
return mutator;
97-
}
98-
9993
List<T> userValues =
100-
rawUserValues
101-
.get()
94+
valuePoolRegistry
95+
.extractUserValues(type)
10296
// Values whose round trip serialization is not stable violate either some user
10397
// annotations on the type (e.g. @InRange), or the default mutator limits (e.g.
10498
// default List size limits) and are therefore not suitable for inclusion in the value
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/*
2+
* Copyright 2026 Code Intelligence GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.code_intelligence.jazzer.mutation.support;
18+
19+
import static java.util.Collections.EMPTY_LIST;
20+
21+
import java.io.IOException;
22+
import java.nio.file.FileSystems;
23+
import java.nio.file.FileVisitResult;
24+
import java.nio.file.Files;
25+
import java.nio.file.Path;
26+
import java.nio.file.PathMatcher;
27+
import java.nio.file.Paths;
28+
import java.nio.file.SimpleFileVisitor;
29+
import java.nio.file.attribute.BasicFileAttributes;
30+
import java.util.ArrayList;
31+
import java.util.Arrays;
32+
import java.util.List;
33+
34+
public class GlobUtils {
35+
protected static List<Path> collectPathsForGlob(Path baseDir, String glob) {
36+
if (ON_WINDOWS) {
37+
glob = glob.replace("\\\\", "/");
38+
}
39+
int firstGlobChar = indexOfFirstGlobChar(glob);
40+
if (firstGlobChar == -1) {
41+
Path target = baseDir.resolve(unescapeGlobChars(glob)).toAbsolutePath().normalize();
42+
return Files.isRegularFile(target) ? Arrays.asList(target) : EMPTY_LIST;
43+
}
44+
45+
String prefix = glob.substring(0, firstGlobChar);
46+
int lastSeparator =
47+
ON_WINDOWS
48+
? Math.max(prefix.lastIndexOf('/'), prefix.lastIndexOf("\\\\"))
49+
: prefix.lastIndexOf('/');
50+
51+
// The 'start' path is always absolute
52+
Path start;
53+
String remainingPattern;
54+
if (lastSeparator == -1) {
55+
start = baseDir.toAbsolutePath().normalize();
56+
remainingPattern = glob;
57+
} else {
58+
String basePrefix = prefix.substring(0, lastSeparator);
59+
start = baseDir.resolve(unescapeGlobChars(basePrefix)).toAbsolutePath().normalize();
60+
remainingPattern = glob.substring(lastSeparator + 1);
61+
}
62+
if (!Files.exists(start)) {
63+
return EMPTY_LIST;
64+
}
65+
66+
// The matcher is always relative to start path
67+
PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:" + remainingPattern);
68+
69+
List<Path> matches = new ArrayList<>();
70+
try {
71+
Files.walkFileTree(
72+
start,
73+
new SimpleFileVisitor<Path>() {
74+
@Override
75+
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
76+
if (!Files.isRegularFile(file)) {
77+
return FileVisitResult.CONTINUE;
78+
}
79+
Path relativePath = start.relativize(file);
80+
if (matcher.matches(relativePath)) {
81+
matches.add(file);
82+
}
83+
return FileVisitResult.CONTINUE;
84+
}
85+
86+
@Override
87+
public FileVisitResult visitFileFailed(Path file, IOException exc) {
88+
return FileVisitResult.CONTINUE;
89+
}
90+
});
91+
} catch (IOException ignored) {
92+
// Best effort - return what we found so far
93+
}
94+
return matches;
95+
}
96+
97+
private static final String SPECIAL_CHARS = "*{}[]-?\\";
98+
99+
private static boolean isSpecialChar(char c) {
100+
return SPECIAL_CHARS.indexOf(c) != -1;
101+
}
102+
103+
protected static Path unescapeGlobChars(String glob) {
104+
StringBuilder sb = new StringBuilder();
105+
char[] chars = glob.toCharArray();
106+
boolean escaped = false;
107+
108+
for (char c : chars) {
109+
if (escaped) {
110+
if (!isSpecialChar(c)) {
111+
sb.append('\\');
112+
}
113+
sb.append(c);
114+
escaped = false;
115+
} else if (c == '\\') {
116+
escaped = true;
117+
} else {
118+
sb.append(c);
119+
}
120+
}
121+
122+
return Paths.get(sb.toString());
123+
}
124+
125+
private static final String GLOB_CHARS = "*?[{";
126+
127+
private static boolean isGlobChar(char c) {
128+
return GLOB_CHARS.indexOf(c) != -1;
129+
}
130+
131+
protected static final boolean ON_WINDOWS = FileSystems.getDefault().getSeparator().equals("\\");
132+
133+
private static int indexOfFirstGlobChar(String glob) {
134+
char[] chars = glob.toCharArray();
135+
boolean escaped = false;
136+
for (int i = 0; i < chars.length; i++) {
137+
char c = chars[i];
138+
if (escaped) {
139+
escaped = false;
140+
continue;
141+
} else if (c == '\\') {
142+
escaped = true;
143+
}
144+
if (isGlobChar(c)) {
145+
return i;
146+
}
147+
}
148+
return -1;
149+
}
150+
}

0 commit comments

Comments
 (0)