Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import com.code_intelligence.jazzer.mutation.api.Serializer;
import com.code_intelligence.jazzer.mutation.api.SerializingInPlaceMutator;
import com.code_intelligence.jazzer.mutation.api.SerializingMutator;
import com.code_intelligence.jazzer.mutation.support.Preconditions;
import com.google.errorprone.annotations.ImmutableTypeParameter;
import java.io.DataInputStream;
import java.io.DataOutputStream;
Expand Down Expand Up @@ -428,6 +429,102 @@ public String toDebugString(Predicate<Debuggable> isInCycle) {
};
}

/**
* Mutates a sum type (e.g. a sealed interface), preferring to mutate the current state but
* occasionally switching to a different state.
*
* @param getState a function that returns the current state of the sum type as an index into
* {@code perStateMutators}, or -1 if the state is indeterminate.
* @param perStateMutators the mutators for each state
* @return a mutator that mutates the sum type
*/
@SafeVarargs
public static <T> SerializingMutator<?> mutateSum(
ToIntFunction<T> getState, SerializingMutator<T>... perStateMutators) {
Preconditions.require(perStateMutators.length > 0, "At least one mutator must be provided");
if (perStateMutators.length == 1) {
return perStateMutators[0];
}
boolean hasFixedSize = stream(perStateMutators).allMatch(SerializingMutator::hasFixedSize);
final SerializingMutator<T>[] mutators =
Arrays.copyOf(perStateMutators, perStateMutators.length);
return new SerializingMutator<T>() {
@Override
public T init(PseudoRandom prng) {
return mutators[prng.indexIn(mutators)].init(prng);
}

@Override
public T mutate(T value, PseudoRandom prng) {
int currentState = getState.applyAsInt(value);
if (currentState == -1) {
// The value is in an indeterminate state, initialize it.
return init(prng);
}
if (prng.trueInOneOutOf(100)) {
// Initialize to a different state.
return mutators[prng.otherIndexIn(mutators, currentState)].init(prng);
}
// Mutate within the current state.
return mutators[currentState].mutate(value, prng);
}

@Override
public T crossOver(T value, T otherValue, PseudoRandom prng) {
// Try to cross over in current state and leave state changes to the mutate step.
int currentState = getState.applyAsInt(value);
int otherState = getState.applyAsInt(otherValue);
if (currentState == -1) {
// If reference is not initialized to a concrete state yet, try to do so in
// the state of other reference, as that's at least some progress.
if (otherState == -1) {
// If both states are indeterminate, cross over can not be performed.
return value;
}
return mutators[otherState].init(prng);
}
if (currentState == otherState) {
return mutators[currentState].crossOver(value, otherValue, prng);
}
return value;
}

@Override
public T detach(T value) {
int currentState = getState.applyAsInt(value);
if (currentState == -1) {
return value;
}
return mutators[currentState].detach(value);
}

@Override
public T read(DataInputStream in) throws IOException {
int currentState = Math.floorMod(in.readInt(), mutators.length);
return mutators[currentState].read(in);
}

@Override
public void write(T value, DataOutputStream out) throws IOException {
int currentState = getState.applyAsInt(value);
out.writeInt(currentState);
mutators[currentState].write(value, out);
}

@Override
public boolean hasFixedSize() {
return hasFixedSize;
}

@Override
public String toDebugString(Predicate<Debuggable> isInCycle) {
return stream(mutators)
.map(mutator -> mutator.toDebugString(isInCycle))
.collect(joining(" | ", "(", ")"));
}
};
}

/**
* Use {@link #markAsRequiringRecursionBreaking(SerializingMutator)} instead for {@link
* com.code_intelligence.jazzer.mutation.api.ValueMutator}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ public static ExtendedMutatorFactory newFactory() {
CollectionMutators.newFactories(),
ProtoMutators.newFactories(),
LibFuzzerMutators.newFactories(),
AggregateMutators.newFactories(),
TimeMutators.newFactories());
TimeMutators.newFactories(),
// Keep generic aggregate mutators last in case a concrete type is also an aggregate type.
AggregateMutators.newFactories());
}

// Mutators for which the NullableMutatorFactory
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,26 +25,43 @@ private AggregateMutators() {}

public static Stream<MutatorFactory> newFactories() {
// Register the record mutator first as it is more specific.
return Stream.concat(
newRecordMutatorFactoryIfSupported(),
Stream.of(
new SetterBasedBeanMutatorFactory(),
new ConstructorBasedBeanMutatorFactory(),
new CachedConstructorMutatorFactory()));
return Stream.of(
newRecordMutatorFactoryIfSupported(),
newSealedClassMutatorFactoryIfSupported(),
Stream.of(
new SetterBasedBeanMutatorFactory(),
new ConstructorBasedBeanMutatorFactory(),
new CachedConstructorMutatorFactory()))
.flatMap(s -> s);
}

private static Stream<MutatorFactory> newRecordMutatorFactoryIfSupported() {
if (!supportsRecords()) {
try {
Class.forName("java.lang.Record");
return Stream.of(instantiateMutatorFactory("RecordMutatorFactory"));
} catch (ClassNotFoundException ignored) {
return Stream.empty();
}
}

private static Stream<MutatorFactory> newSealedClassMutatorFactoryIfSupported() {
try {
Class.class.getMethod("getPermittedSubclasses");
return Stream.of(instantiateMutatorFactory("SealedClassMutatorFactory"));
} catch (NoSuchMethodException e) {
return Stream.empty();
}
}

private static MutatorFactory instantiateMutatorFactory(String simpleClassName) {
try {
// Instantiate RecordMutatorFactory via reflection as making it a compile time dependency
// breaks the r8 step in the Android build.
Class<? extends MutatorFactory> recordMutatorFactory;
recordMutatorFactory =
Class.forName(AggregateMutators.class.getPackage().getName() + ".RecordMutatorFactory")
// Instantiate factory via reflection as making it a compile time dependency breaks the r8
// step in the Android build.
Class<? extends MutatorFactory> factory;
factory =
Class.forName(AggregateMutators.class.getPackage().getName() + "." + simpleClassName)
.asSubclass(MutatorFactory.class);
return Stream.of(recordMutatorFactory.getDeclaredConstructor().newInstance());
return factory.getDeclaredConstructor().newInstance();
} catch (ClassNotFoundException
| NoSuchMethodException
| InstantiationException
Expand All @@ -53,13 +70,4 @@ private static Stream<MutatorFactory> newRecordMutatorFactoryIfSupported() {
throw new IllegalStateException(e);
}
}

private static boolean supportsRecords() {
try {
Class.forName("java.lang.Record");
return true;
} catch (ClassNotFoundException ignored) {
return false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ java_library(
"AggregatesHelper.java",
"BeanSupport.java",
"RecordMutatorFactory.java",
"SealedClassMutatorFactory.java",
],
),
visibility = [
Expand All @@ -16,6 +17,7 @@ java_library(
"@platforms//os:android": [],
"//conditions:default": [
":record_mutator_factory",
":sealed_class_mutator_factory",
],
}),
deps = [
Expand All @@ -40,6 +42,20 @@ java_library(
],
)

java_library(
name = "sealed_class_mutator_factory",
srcs = ["SealedClassMutatorFactory.java"],
javacopts = [
"--release",
"17",
],
deps = [
"//src/main/java/com/code_intelligence/jazzer/mutation/api",
"//src/main/java/com/code_intelligence/jazzer/mutation/combinator",
"//src/main/java/com/code_intelligence/jazzer/mutation/support",
],
)

java_library(
name = "aggregates_helper",
srcs = [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright 2025 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.mutation.mutator.aggregate;

import static com.code_intelligence.jazzer.mutation.support.StreamSupport.toArrayOrEmpty;
import static java.util.Arrays.stream;

import com.code_intelligence.jazzer.mutation.api.ExtendedMutatorFactory;
import com.code_intelligence.jazzer.mutation.api.MutatorFactory;
import com.code_intelligence.jazzer.mutation.api.SerializingMutator;
import com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators;
import com.code_intelligence.jazzer.mutation.support.TypeSupport;
import java.lang.reflect.AnnotatedType;
import java.util.Optional;
import java.util.function.ToIntFunction;

final class SealedClassMutatorFactory<T> implements MutatorFactory {
@Override
public Optional<SerializingMutator<?>> tryCreate(
AnnotatedType type, ExtendedMutatorFactory factory) {
if (!(type.getType() instanceof Class<?>)) {
return Optional.empty();
}
Class<T>[] permittedSubclasses =
(Class<T>[]) ((Class<T>) type.getType()).getPermittedSubclasses();
if (permittedSubclasses == null) {
return Optional.empty();
}

ToIntFunction<T> getState =
(value) -> {
// We can't use value.getClass() as it might be a subclass of the permitted (direct)
// subclasses.
for (int i = 0; i < permittedSubclasses.length; i++) {
if (permittedSubclasses[i].isInstance(value)) {
return i;
}
}
return -1;
};
return toArrayOrEmpty(
stream(permittedSubclasses)
.map(TypeSupport::asAnnotatedType)
.map(TypeSupport::notNull)
.map(factory::tryCreate),
SerializingMutator<?>[]::new)
.map(
mutators -> MutatorCombinators.mutateSum(getState, (SerializingMutator<T>[]) mutators));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import com.code_intelligence.jazzer.mutation.annotation.WithLength;
import com.code_intelligence.jazzer.mutation.utils.PropertyConstraint;
import java.lang.annotation.Annotation;
import java.lang.annotation.Inherited;
import java.lang.reflect.AnnotatedArrayType;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.AnnotatedParameterizedType;
Expand Down Expand Up @@ -94,6 +95,65 @@ public static <T> Optional<Class<? extends T>> asSubclassOrEmpty(
return Optional.of(actualClazz.asSubclass(superclass));
}

/**
* Synthesizes an {@link AnnotatedType} for the given {@link Class}.
*
* <p>Usage of this method should be avoided in favor of obtaining annotated types in a natural
* way if possible (e.g. prefer {@link Class#getAnnotatedSuperclass()} to {@link
* Class#getSuperclass()}.
*/
public static AnnotatedType asAnnotatedType(Class<?> clazz) {
requireNonNull(clazz);
return new AnnotatedType() {
@Override
public Type getType() {
return clazz;
}

@Override
public <T extends Annotation> T getAnnotation(Class<T> annotationClass) {
return annotatedElementGetAnnotation(this, annotationClass);
}

@Override
public Annotation[] getAnnotations() {
// No directly present annotations, look for inheritable present annotations on the
// superclass.
if (clazz.getSuperclass() == null) {
return new Annotation[0];
}
return stream(clazz.getSuperclass().getAnnotations())
.filter(
annotation ->
annotation.annotationType().getDeclaredAnnotation(Inherited.class) != null)
.toArray(Annotation[]::new);
}

@Override
public Annotation[] getDeclaredAnnotations() {
// No directly present annotations.
return new Annotation[0];
}

@Override
public String toString() {
return annotatedTypeToString(this);
}

@Override
public int hashCode() {
throw new UnsupportedOperationException(
"hashCode() is not supported as its behavior isn't specified");
}

@Override
public boolean equals(Object obj) {
throw new UnsupportedOperationException(
"equals() is not supported as its behavior isn't specified");
}
};
}

/**
* Visits the individual classes and their directly present annotations that make up the given
* type.
Expand Down
Loading