You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@avro.apache.org by ie...@apache.org on 2026/04/04 17:29:14 UTC
(avro) 07/09: Expand Java fuzz coverage for roundtrip paths
This is an automated email from the ASF dual-hosted git repository.
iemejia pushed a commit to branch fuzzer
in repository https://gitbox.apache.org/repos/asf/avro.git
commit 7ddcc9cd2bd44613109b6f91f30edf8744a7c5a8
Author: Ismaël Mejía <ie...@gmail.com>
AuthorDate: Sat Apr 4 17:14:40 2026 +0000
Expand Java fuzz coverage for roundtrip paths
Add roundtrip, typed projection, and single-object fuzz targets with valid corpora so Jazzer exercises successful encode/decode paths instead of only malformed-input handling. Normalize semantic comparisons in the fuzz harness to avoid false regressions from Avro's runtime representations.
---
lang/java/fuzz/README.md | 15 +-
lang/java/fuzz/REPORT.md | 12 +-
.../java/org/apache/avro/fuzz/FuzzSupport.java | 161 +++++++++++++++++++++
.../org/apache/avro/fuzz/ProjectionFuzzer.java | 133 +++++++++++++++++
.../java/org/apache/avro/fuzz/RoundTripFuzzer.java | 128 ++++++++++++++++
.../org/apache/avro/fuzz/SeedCorpusGenerator.java | 108 ++++++++++++++
.../org/apache/avro/fuzz/SingleObjectFuzzer.java | 68 +++++++++
.../avro/fuzz/model/ReflectRoundTripRecord.java | 51 +++++++
.../avro/fuzz/model/SpecificProjectionEnum.java | 37 +++++
.../avro/fuzz/model/SpecificProjectionRecord.java | 125 ++++++++++++++++
.../fuzzDataFileReader/seed-valid-container.avro | Bin 516 -> 516 bytes
.../seed-valid-container.avro | Bin 516 -> 516 bytes
.../fuzzDataFileStream/seed-valid-container.avro | Bin 516 -> 516 bytes
.../seed-valid-container.avro | Bin 516 -> 516 bytes
.../fuzzReflectProjection/seed-valid-reflect.bin | Bin 0 -> 48 bytes
.../fuzzSpecificProjection/seed-valid-specific.bin | Bin 0 -> 22 bytes
.../fuzzBinaryRoundTrip/seed-valid-roundtrip.bin | Bin 0 -> 62 bytes
.../crash-d84cec3ecd031ce35c724f6ee349f201d40a61f3 | Bin 0 -> 101 bytes
.../seed-valid-roundtrip.avro | Bin 0 -> 873 bytes
.../fuzzJsonRoundTrip/seed-valid-roundtrip.json | 1 +
.../seed-valid-single-object.bin | Bin 0 -> 72 bytes
21 files changed, 834 insertions(+), 5 deletions(-)
diff --git a/lang/java/fuzz/README.md b/lang/java/fuzz/README.md
index 1b63bf8455..e2ff957c4b 100644
--- a/lang/java/fuzz/README.md
+++ b/lang/java/fuzz/README.md
@@ -11,14 +11,23 @@ This module hosts Jazzer-based fuzz tests for the Java Avro implementation.
| `BinaryDecodingFuzzer` | `fuzzDirectBinaryDecoding` | Exercises direct binary decoding on `InputStream`-backed inputs |
| `JsonDecodingFuzzer` | `fuzzJsonDecoding` | Exercises JSON decoding with unions, maps, arrays, and logical types |
| `DataFileReaderFuzzer` | `fuzzDataFileReader` | Exercises seekable object container file parsing |
+| `DataFileReaderFuzzer` | `fuzzDataFileReaderWithResolution` | Exercises seekable container parsing with explicit reader-schema resolution |
| `DataFileReaderFuzzer` | `fuzzDataFileStream` | Exercises streaming object container file parsing |
+| `DataFileReaderFuzzer` | `fuzzDataFileStreamWithResolution` | Exercises streaming container parsing with explicit reader-schema resolution |
+| `RoundTripFuzzer` | `fuzzBinaryRoundTrip` | Roundtrips structured valid generic records through binary encoding |
+| `RoundTripFuzzer` | `fuzzJsonRoundTrip` | Roundtrips structured valid generic records through JSON encoding |
+| `RoundTripFuzzer` | `fuzzDataFileRoundTrip` | Roundtrips structured valid generic records through object container files |
+| `ProjectionFuzzer` | `fuzzSpecificProjection` | Projects valid binary payloads through `SpecificDatumReader` |
+| `ProjectionFuzzer` | `fuzzReflectProjection` | Projects valid binary payloads through `ReflectDatumReader` |
+| `SingleObjectFuzzer` | `fuzzSingleObjectRoundTrip` | Roundtrips Avro single-object encoding with schema fingerprints |
+| `SingleObjectFuzzer` | `fuzzSingleObjectDecoding` | Fuzzes malformed and valid single-object decoding |
## Design Notes
- Targets only suppress exceptions that are expected from malformed Avro input.
- Unexpected runtime failures are allowed to escape so Jazzer reports them.
- The checked-in corpus includes both historical crash reproducers and valid seeds.
-- Valid seeds help regression mode reach successful decode paths instead of replaying only prior failures.
+- Valid seeds help regression mode reach successful encode/decode and projection paths instead of replaying only prior failures.
## Running
@@ -26,7 +35,7 @@ Run from `lang/java/`:
```bash
# Replay the checked-in regression corpus only.
-mvn test -pl fuzz
+mvn test -pl fuzz -am
# Run continuous fuzzing for 60 seconds per target.
mvn test -pl fuzz -Pfuzz
@@ -35,7 +44,7 @@ mvn test -pl fuzz -Pfuzz
mvn test -pl fuzz -Pfuzz -Dtest=BinaryDecodingFuzzer#fuzzDirectBinaryDecoding
# Equivalent environment-variable toggle.
-JAZZER_FUZZ=1 mvn test -pl fuzz
+JAZZER_FUZZ=1 mvn test -pl fuzz -am
```
## Corpus Layout
diff --git a/lang/java/fuzz/REPORT.md b/lang/java/fuzz/REPORT.md
index 36a70a5ca6..f4e47ace3d 100644
--- a/lang/java/fuzz/REPORT.md
+++ b/lang/java/fuzz/REPORT.md
@@ -4,7 +4,7 @@
`lang/java/fuzz/` is a Jazzer-based fuzzing module for the Java Avro implementation.
-Current coverage includes 4 fuzz classes and 6 fuzz targets:
+Current coverage includes parser, decoding, roundtrip, projection, and single-object targets:
- schema parsing via `SchemaParser`
- buffered binary decoding
@@ -12,6 +12,9 @@ Current coverage includes 4 fuzz classes and 6 fuzz targets:
- JSON decoding
- seekable container-file decoding
- streaming container-file decoding
+- generic record roundtrips across binary, JSON, and object container formats
+- typed projection through `SpecificDatumReader` and `ReflectDatumReader`
+- Avro single-object encoding and decoding
The module is wired into `lang/java/pom.xml` and can be run in regression or continuous-fuzzing mode through Maven.
@@ -65,6 +68,9 @@ The fuzz module has been strengthened beyond the initial implementation:
- successful binary and JSON decodes now require full input consumption
- the direct binary target now uses a short-read stream to exercise hostile streaming behavior
- container-file fuzzing now also covers reader-schema resolution paths
+- roundtrip fuzzing now checks semantic equality across binary, JSON, and container formats
+- Java model projection now covers both specific and reflect readers
+- single-object message framing is now fuzzed directly
## BinaryDecoder Fix Notes
@@ -86,6 +92,8 @@ Checked-in regression inputs now include:
- valid JSON record seeds
- valid binary datum seeds
- valid object container file seeds for both reader and stream targets
+- valid roundtrip binary, JSON, and container seeds
+- valid specific, reflect, and single-object seeds
This makes `mvn test -pl fuzz` useful as both a regression suite and a shallow successful-path smoke test.
@@ -94,7 +102,7 @@ This makes `mvn test -pl fuzz` useful as both a regression suite and a shallow s
Verified locally with:
```bash
-mvn test -pl fuzz
+mvn -Dmaven.build.cache.enabled=false test -pl fuzz -am
```
The module builds, replays the regression corpus, and runs all fuzz targets successfully.
diff --git a/lang/java/fuzz/src/test/java/org/apache/avro/fuzz/FuzzSupport.java b/lang/java/fuzz/src/test/java/org/apache/avro/fuzz/FuzzSupport.java
index e62e33d76f..ceafd41a09 100644
--- a/lang/java/fuzz/src/test/java/org/apache/avro/fuzz/FuzzSupport.java
+++ b/lang/java/fuzz/src/test/java/org/apache/avro/fuzz/FuzzSupport.java
@@ -24,11 +24,21 @@ import org.apache.avro.Schema;
import org.apache.avro.SchemaParseException;
import org.apache.avro.SystemLimitException;
import org.apache.avro.UnresolvedUnionException;
+import org.apache.avro.generic.GenericData;
+import org.apache.avro.generic.GenericEnumSymbol;
+import org.apache.avro.generic.GenericFixed;
+import org.apache.avro.generic.GenericRecord;
import java.io.ByteArrayInputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
final class FuzzSupport {
static final Schema BINARY_WRITER_SCHEMA = new Schema.Parser()
@@ -62,6 +72,20 @@ final class FuzzSupport {
+ "{\"name\":\"inner\",\"type\":{\"type\":\"record\",\"name\":\"Inner\",\"fields\":["
+ "{\"name\":\"x\",\"type\":\"int\"}," + "{\"name\":\"y\",\"type\":\"int\"}]}}]}");
+ static final Schema ROUND_TRIP_SCHEMA = new Schema.Parser()
+ .parse("{\"type\":\"record\",\"name\":\"RoundTripRoot\",\"fields\":[" + "{\"name\":\"id\",\"type\":\"long\"},"
+ + "{\"name\":\"name\",\"type\":\"string\"},"
+ + "{\"name\":\"createdDate\",\"type\":{\"type\":\"int\",\"logicalType\":\"date\"}},"
+ + "{\"name\":\"active\",\"type\":\"boolean\"}," + "{\"name\":\"score\",\"type\":\"double\"},"
+ + "{\"name\":\"payload\",\"type\":\"bytes\"},"
+ + "{\"name\":\"tags\",\"type\":{\"type\":\"array\",\"items\":\"string\"}},"
+ + "{\"name\":\"counts\",\"type\":{\"type\":\"map\",\"values\":\"long\"}},"
+ + "{\"name\":\"choice\",\"type\":[\"null\",\"string\",\"long\"],\"default\":null},"
+ + "{\"name\":\"hash\",\"type\":{\"type\":\"fixed\",\"name\":\"RoundTripHash\",\"size\":4}},"
+ + "{\"name\":\"inner\",\"type\":{\"type\":\"record\",\"name\":\"RoundTripInner\",\"fields\":["
+ + "{\"name\":\"x\",\"type\":\"int\"}," + "{\"name\":\"y\",\"type\":\"int\"}]}},"
+ + "{\"name\":\"status\",\"type\":{\"type\":\"enum\",\"name\":\"RoundTripStatus\",\"symbols\":[\"NEW\",\"READY\",\"DONE\"]}}]}");
+
private FuzzSupport() {
}
@@ -137,10 +161,147 @@ final class FuzzSupport {
+ "\"},\"extra\":null,\"inner\":{\"x\":" + x + ",\"y\":" + y + "}}";
}
+ static GenericRecord buildRoundTripRecord(FuzzedDataProvider data) {
+ GenericRecord record = new GenericData.Record(ROUND_TRIP_SCHEMA);
+ record.put("id", sanitizeLong(data.consumeLong()));
+ record.put("name", safeString(data.consumeString(24), "roundtrip"));
+ record.put("createdDate", data.consumeInt(0, 36500));
+ record.put("active", data.consumeBoolean());
+ record.put("score", boundedScore(data.consumeInt()));
+ record.put("payload", ByteBuffer.wrap(data.consumeBytes(data.consumeInt(0, 16))));
+ record.put("tags", buildStringList(data));
+ record.put("counts", buildCountMap(data));
+ record.put("choice", buildUnionValue(data));
+ record.put("hash", new GenericData.Fixed(ROUND_TRIP_SCHEMA.getField("hash").schema(), buildFixedBytes(data)));
+ record.put("inner", buildInnerRecord(data));
+ record.put("status", new GenericData.EnumSymbol(ROUND_TRIP_SCHEMA.getField("status").schema(),
+ ROUND_TRIP_SCHEMA.getField("status").schema().getEnumSymbols().get(data.consumeInt(0, 2))));
+ return record;
+ }
+
+ static String safeString(String value, String fallback) {
+ return value.isEmpty() ? fallback : value;
+ }
+
+ static boolean semanticEquals(Object left, Object right) {
+ return Objects.equals(normalizeDatum(left), normalizeDatum(right));
+ }
+
+ static boolean roundTripRecordsEqual(GenericRecord left, GenericRecord right) {
+ for (Schema.Field field : ROUND_TRIP_SCHEMA.getFields()) {
+ if (!semanticEquals(left.get(field.name()), right.get(field.name()))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
private static double boundedScore(int value) {
return (value % 1000) / 10.0d;
}
+ private static GenericRecord buildInnerRecord(FuzzedDataProvider data) {
+ GenericRecord inner = new GenericData.Record(ROUND_TRIP_SCHEMA.getField("inner").schema());
+ inner.put("x", data.consumeInt());
+ inner.put("y", data.consumeInt());
+ return inner;
+ }
+
+ private static List<String> buildStringList(FuzzedDataProvider data) {
+ List<String> list = new ArrayList<>();
+ int size = data.consumeInt(0, 4);
+ for (int i = 0; i < size; i++) {
+ list.add(safeString(data.consumeString(16), "tag" + i));
+ }
+ return list;
+ }
+
+ private static Map<String, Long> buildCountMap(FuzzedDataProvider data) {
+ Map<String, Long> map = new LinkedHashMap<>();
+ int size = data.consumeInt(0, 4);
+ for (int i = 0; i < size; i++) {
+ map.put(safeString(toIdentifier(data.consumeString(12), "k" + i), "k" + i), sanitizeLong(data.consumeLong()));
+ }
+ return map;
+ }
+
+ private static Object buildUnionValue(FuzzedDataProvider data) {
+ switch (data.consumeInt(0, 2)) {
+ case 0:
+ return null;
+ case 1:
+ return safeString(data.consumeString(16), "choice");
+ default:
+ return sanitizeLong(data.consumeLong());
+ }
+ }
+
+ private static byte[] buildFixedBytes(FuzzedDataProvider data) {
+ byte[] bytes = data.consumeBytes(4);
+ if (bytes.length == 4) {
+ return bytes;
+ }
+ byte[] fixed = new byte[4];
+ System.arraycopy(bytes, 0, fixed, 0, Math.min(bytes.length, fixed.length));
+ return fixed;
+ }
+
+ private static Object normalizeDatum(Object datum) {
+ if (datum == null) {
+ return null;
+ }
+ if (datum instanceof CharSequence) {
+ return datum.toString();
+ }
+ if (datum instanceof GenericEnumSymbol<?> || datum instanceof Enum<?>) {
+ return datum.toString();
+ }
+ if (datum instanceof ByteBuffer) {
+ return normalizeBytes((ByteBuffer) datum);
+ }
+ if (datum instanceof GenericFixed) {
+ return normalizeBytes(ByteBuffer.wrap(((GenericFixed) datum).bytes()));
+ }
+ if (datum instanceof GenericRecord) {
+ GenericRecord record = (GenericRecord) datum;
+ Map<String, Object> normalized = new LinkedHashMap<>();
+ for (Schema.Field field : record.getSchema().getFields()) {
+ normalized.put(field.name(), normalizeDatum(record.get(field.name())));
+ }
+ return normalized;
+ }
+ if (datum instanceof List<?>) {
+ List<Object> normalized = new ArrayList<>();
+ for (Object value : (List<?>) datum) {
+ normalized.add(normalizeDatum(value));
+ }
+ return normalized;
+ }
+ if (datum instanceof Map<?, ?>) {
+ Map<String, Object> normalized = new LinkedHashMap<>();
+ for (Map.Entry<?, ?> entry : ((Map<?, ?>) datum).entrySet()) {
+ normalized.put(String.valueOf(normalizeDatum(entry.getKey())), normalizeDatum(entry.getValue()));
+ }
+ return normalized;
+ }
+ if (datum instanceof Double || datum instanceof Float) {
+ return ((Number) datum).doubleValue();
+ }
+ if (datum instanceof Number) {
+ return ((Number) datum).longValue();
+ }
+ return datum;
+ }
+
+ private static List<Integer> normalizeBytes(ByteBuffer buffer) {
+ ByteBuffer copy = buffer.duplicate();
+ List<Integer> bytes = new ArrayList<>(copy.remaining());
+ while (copy.hasRemaining()) {
+ bytes.add(copy.get() & 0xff);
+ }
+ return bytes;
+ }
+
private static boolean isExpectedAvroRuntimeFailure(RuntimeException exception) {
if (!(exception instanceof AvroRuntimeException)) {
return false;
diff --git a/lang/java/fuzz/src/test/java/org/apache/avro/fuzz/ProjectionFuzzer.java b/lang/java/fuzz/src/test/java/org/apache/avro/fuzz/ProjectionFuzzer.java
new file mode 100644
index 0000000000..4068d38d42
--- /dev/null
+++ b/lang/java/fuzz/src/test/java/org/apache/avro/fuzz/ProjectionFuzzer.java
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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
+ *
+ * https://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 org.apache.avro.fuzz;
+
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+import com.code_intelligence.jazzer.junit.FuzzTest;
+import org.apache.avro.fuzz.model.ReflectRoundTripRecord;
+import org.apache.avro.fuzz.model.SpecificProjectionEnum;
+import org.apache.avro.fuzz.model.SpecificProjectionRecord;
+import org.apache.avro.io.BinaryDecoder;
+import org.apache.avro.io.BinaryEncoder;
+import org.apache.avro.io.DecoderFactory;
+import org.apache.avro.io.EncoderFactory;
+import org.apache.avro.reflect.ReflectData;
+import org.apache.avro.reflect.ReflectDatumReader;
+import org.apache.avro.reflect.ReflectDatumWriter;
+import org.apache.avro.specific.SpecificDatumReader;
+import org.apache.avro.specific.SpecificDatumWriter;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+class ProjectionFuzzer {
+ private static final ReflectData REFLECT_DATA = ReflectData.get();
+
+ @FuzzTest
+ void fuzzSpecificProjection(FuzzedDataProvider data) throws IOException {
+ SpecificProjectionRecord record = buildSpecificRecord(data);
+ byte[] encoded = encodeSpecific(record);
+
+ BinaryDecoder decoder = DecoderFactory.get().binaryDecoder(encoded, null);
+ SpecificDatumReader<SpecificProjectionRecord> reader = new SpecificDatumReader<>(SpecificProjectionRecord.class);
+ SpecificProjectionRecord decoded = reader.read(null, decoder);
+
+ if (!specificRecordsEqual(record, decoded)) {
+ throw new AssertionError("Specific projection changed semantic value");
+ }
+ if (!decoder.isEnd()) {
+ throw new AssertionError("Specific projection left trailing bytes unread");
+ }
+ }
+
+ @FuzzTest
+ void fuzzReflectProjection(FuzzedDataProvider data) throws IOException {
+ ReflectRoundTripRecord record = buildReflectRecord(data);
+ byte[] encoded = encodeReflect(record);
+
+ BinaryDecoder decoder = DecoderFactory.get().binaryDecoder(encoded, null);
+ ReflectDatumReader<ReflectRoundTripRecord> reader = new ReflectDatumReader<>(ReflectRoundTripRecord.class);
+ ReflectRoundTripRecord decoded = reader.read(null, decoder);
+
+ if (!record.equals(decoded)) {
+ throw new AssertionError("Reflect projection changed semantic value");
+ }
+ if (!decoder.isEnd()) {
+ throw new AssertionError("Reflect projection left trailing bytes unread");
+ }
+ }
+
+ private static byte[] encodeSpecific(SpecificProjectionRecord record) throws IOException {
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ BinaryEncoder encoder = EncoderFactory.get().binaryEncoder(output, null);
+ SpecificDatumWriter<SpecificProjectionRecord> writer = new SpecificDatumWriter<>(SpecificProjectionRecord.class);
+ writer.write(record, encoder);
+ encoder.flush();
+ return output.toByteArray();
+ }
+
+ private static byte[] encodeReflect(ReflectRoundTripRecord record) throws IOException {
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ BinaryEncoder encoder = EncoderFactory.get().binaryEncoder(output, null);
+ ReflectDatumWriter<ReflectRoundTripRecord> writer = new ReflectDatumWriter<>(ReflectRoundTripRecord.class,
+ REFLECT_DATA);
+ writer.write(record, encoder);
+ encoder.flush();
+ return output.toByteArray();
+ }
+
+ private static SpecificProjectionRecord buildSpecificRecord(FuzzedDataProvider data) {
+ List<String> nicknames = new ArrayList<>();
+ int nicknameCount = data.consumeInt(0, 3);
+ for (int i = 0; i < nicknameCount; i++) {
+ nicknames.add(FuzzSupport.safeString(data.consumeString(12), "nick" + i));
+ }
+
+ SpecificProjectionEnum typeEnum = data.consumeBoolean() ? null
+ : SpecificProjectionEnum.values()[data.consumeInt(0, SpecificProjectionEnum.values().length - 1)];
+ return new SpecificProjectionRecord(data.consumeInt(), FuzzSupport.safeString(data.consumeString(24), "name"),
+ nicknames, typeEnum);
+ }
+
+ private static ReflectRoundTripRecord buildReflectRecord(FuzzedDataProvider data) {
+ ReflectRoundTripRecord record = new ReflectRoundTripRecord();
+ record.id = data.consumeLong();
+ record.name = FuzzSupport.safeString(data.consumeString(24), "reflect");
+ record.tags = new ArrayList<>();
+ for (int i = 0, size = data.consumeInt(0, 3); i < size; i++) {
+ record.tags.add(FuzzSupport.safeString(data.consumeString(16), "tag" + i));
+ }
+ record.counts = new LinkedHashMap<>();
+ for (int i = 0, size = data.consumeInt(0, 3); i < size; i++) {
+ record.counts.put(FuzzSupport.safeString(data.consumeString(12), "k" + i), data.consumeLong());
+ }
+ record.alias = data.consumeBoolean() ? null : FuzzSupport.safeString(data.consumeString(16), "alias");
+ return record;
+ }
+
+ private static boolean specificRecordsEqual(SpecificProjectionRecord left, SpecificProjectionRecord right) {
+ return FuzzSupport.semanticEquals(left.get(0), right.get(0))
+ && FuzzSupport.semanticEquals(left.get(1), right.get(1))
+ && FuzzSupport.semanticEquals(left.get(2), right.get(2))
+ && FuzzSupport.semanticEquals(left.get(3), right.get(3));
+ }
+}
diff --git a/lang/java/fuzz/src/test/java/org/apache/avro/fuzz/RoundTripFuzzer.java b/lang/java/fuzz/src/test/java/org/apache/avro/fuzz/RoundTripFuzzer.java
new file mode 100644
index 0000000000..78417aa6f0
--- /dev/null
+++ b/lang/java/fuzz/src/test/java/org/apache/avro/fuzz/RoundTripFuzzer.java
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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
+ *
+ * https://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 org.apache.avro.fuzz;
+
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+import com.code_intelligence.jazzer.junit.FuzzTest;
+import org.apache.avro.file.DataFileReader;
+import org.apache.avro.file.DataFileWriter;
+import org.apache.avro.file.SeekableByteArrayInput;
+import org.apache.avro.generic.GenericData;
+import org.apache.avro.generic.GenericDatumReader;
+import org.apache.avro.generic.GenericDatumWriter;
+import org.apache.avro.generic.GenericRecord;
+import org.apache.avro.io.BinaryDecoder;
+import org.apache.avro.io.BinaryEncoder;
+import org.apache.avro.io.DecoderFactory;
+import org.apache.avro.io.EncoderFactory;
+import org.apache.avro.io.JsonDecoder;
+import org.apache.avro.io.JsonEncoder;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+class RoundTripFuzzer {
+ @FuzzTest
+ void fuzzBinaryRoundTrip(FuzzedDataProvider data) throws IOException {
+ GenericRecord record = FuzzSupport.buildRoundTripRecord(data);
+ byte[] encoded = encodeBinary(record);
+
+ BinaryDecoder decoder = DecoderFactory.get().binaryDecoder(encoded, null);
+ GenericDatumReader<GenericRecord> reader = new GenericDatumReader<>(FuzzSupport.ROUND_TRIP_SCHEMA);
+ GenericRecord decoded = reader.read(null, decoder);
+
+ assertSemanticallyEqual(record, decoded);
+ if (!decoder.isEnd()) {
+ throw new AssertionError("Binary roundtrip left trailing bytes unread");
+ }
+ }
+
+ @FuzzTest
+ void fuzzJsonRoundTrip(FuzzedDataProvider data) throws IOException {
+ GenericRecord record = FuzzSupport.buildRoundTripRecord(data);
+ byte[] encoded = encodeJson(record);
+
+ JsonDecoder decoder = DecoderFactory.get().jsonDecoder(FuzzSupport.ROUND_TRIP_SCHEMA,
+ new java.io.ByteArrayInputStream(encoded));
+ GenericDatumReader<GenericRecord> reader = new GenericDatumReader<>(FuzzSupport.ROUND_TRIP_SCHEMA);
+ GenericRecord decoded = reader.read(null, decoder);
+
+ assertSemanticallyEqual(record, decoded);
+ }
+
+ @FuzzTest
+ void fuzzDataFileRoundTrip(FuzzedDataProvider data) throws IOException {
+ GenericRecord record = FuzzSupport.buildRoundTripRecord(data);
+ byte[] encoded = encodeContainer(record);
+
+ try (DataFileReader<GenericRecord> reader = new DataFileReader<>(new SeekableByteArrayInput(encoded),
+ new GenericDatumReader<>(FuzzSupport.ROUND_TRIP_SCHEMA))) {
+ if (!reader.hasNext()) {
+ throw new AssertionError("Container roundtrip lost the encoded datum");
+ }
+ GenericRecord decoded = reader.next();
+ assertSemanticallyEqual(record, decoded);
+ if (reader.hasNext()) {
+ throw new AssertionError("Container roundtrip unexpectedly produced multiple data items");
+ }
+ }
+ }
+
+ private static byte[] encodeBinary(GenericRecord record) throws IOException {
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ BinaryEncoder encoder = EncoderFactory.get().binaryEncoder(output, null);
+ GenericDatumWriter<GenericRecord> writer = new GenericDatumWriter<>(FuzzSupport.ROUND_TRIP_SCHEMA);
+ writer.write(record, encoder);
+ encoder.flush();
+ return output.toByteArray();
+ }
+
+ private static byte[] encodeJson(GenericRecord record) throws IOException {
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ JsonEncoder encoder = EncoderFactory.get().jsonEncoder(FuzzSupport.ROUND_TRIP_SCHEMA, output);
+ GenericDatumWriter<GenericRecord> writer = new GenericDatumWriter<>(FuzzSupport.ROUND_TRIP_SCHEMA);
+ writer.write(record, encoder);
+ encoder.flush();
+ return output.toByteArray();
+ }
+
+ private static byte[] encodeContainer(GenericRecord record) throws IOException {
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ try (DataFileWriter<GenericRecord> writer = new DataFileWriter<>(
+ new GenericDatumWriter<>(FuzzSupport.ROUND_TRIP_SCHEMA))) {
+ writer.create(FuzzSupport.ROUND_TRIP_SCHEMA, output);
+ writer.append(record);
+ }
+ return output.toByteArray();
+ }
+
+ private static void assertSemanticallyEqual(GenericRecord expected, GenericRecord actual) {
+ if (!GenericData.get().validate(FuzzSupport.ROUND_TRIP_SCHEMA, actual)) {
+ throw new AssertionError("Decoded roundtrip record does not validate against the schema");
+ }
+ if (!FuzzSupport.roundTripRecordsEqual(expected, actual)) {
+ throw new AssertionError("Roundtrip record changed semantic value");
+ }
+
+ GenericRecord copied = GenericData.get().deepCopy(FuzzSupport.ROUND_TRIP_SCHEMA, actual);
+ if (!FuzzSupport.roundTripRecordsEqual(actual, copied)) {
+ throw new AssertionError("Deep copy changed roundtrip record semantics");
+ }
+ }
+}
diff --git a/lang/java/fuzz/src/test/java/org/apache/avro/fuzz/SeedCorpusGenerator.java b/lang/java/fuzz/src/test/java/org/apache/avro/fuzz/SeedCorpusGenerator.java
index 4252317ea4..6776bc245c 100644
--- a/lang/java/fuzz/src/test/java/org/apache/avro/fuzz/SeedCorpusGenerator.java
+++ b/lang/java/fuzz/src/test/java/org/apache/avro/fuzz/SeedCorpusGenerator.java
@@ -18,11 +18,19 @@
package org.apache.avro.fuzz;
import org.apache.avro.file.DataFileWriter;
+import org.apache.avro.fuzz.model.ReflectRoundTripRecord;
+import org.apache.avro.fuzz.model.SpecificProjectionEnum;
+import org.apache.avro.fuzz.model.SpecificProjectionRecord;
import org.apache.avro.generic.GenericData;
import org.apache.avro.generic.GenericDatumWriter;
import org.apache.avro.generic.GenericRecord;
import org.apache.avro.io.BinaryEncoder;
import org.apache.avro.io.EncoderFactory;
+import org.apache.avro.io.JsonEncoder;
+import org.apache.avro.message.BinaryMessageEncoder;
+import org.apache.avro.reflect.ReflectData;
+import org.apache.avro.reflect.ReflectDatumWriter;
+import org.apache.avro.specific.SpecificDatumWriter;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
@@ -49,6 +57,14 @@ class SeedCorpusGenerator {
writeBytes("DataFileReaderFuzzerInputs/fuzzDataFileStream/seed-valid-container.avro", writeContainerFile());
writeBytes("DataFileReaderFuzzerInputs/fuzzDataFileStreamWithResolution/seed-valid-container.avro",
writeContainerFile());
+ writeBytes("RoundTripFuzzerInputs/fuzzBinaryRoundTrip/seed-valid-roundtrip.bin", writeRoundTripBinary());
+ writeBytes("RoundTripFuzzerInputs/fuzzJsonRoundTrip/seed-valid-roundtrip.json", writeRoundTripJson());
+ writeBytes("RoundTripFuzzerInputs/fuzzDataFileRoundTrip/seed-valid-roundtrip.avro", writeRoundTripContainer());
+ writeBytes("ProjectionFuzzerInputs/fuzzSpecificProjection/seed-valid-specific.bin",
+ writeSpecificProjectionRecord());
+ writeBytes("ProjectionFuzzerInputs/fuzzReflectProjection/seed-valid-reflect.bin", writeReflectProjectionRecord());
+ writeBytes("SingleObjectFuzzerInputs/fuzzSingleObjectDecoding/seed-valid-single-object.bin",
+ writeSingleObjectRecord());
}
private static byte[] writeBinaryRecord() throws IOException {
@@ -79,6 +95,55 @@ class SeedCorpusGenerator {
return output.toByteArray();
}
+ private static byte[] writeRoundTripBinary() throws IOException {
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ BinaryEncoder encoder = EncoderFactory.get().binaryEncoder(output, null);
+ new GenericDatumWriter<GenericRecord>(FuzzSupport.ROUND_TRIP_SCHEMA).write(sampleRoundTripRecord(), encoder);
+ encoder.flush();
+ return output.toByteArray();
+ }
+
+ private static byte[] writeRoundTripJson() throws IOException {
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ JsonEncoder encoder = EncoderFactory.get().jsonEncoder(FuzzSupport.ROUND_TRIP_SCHEMA, output);
+ new GenericDatumWriter<GenericRecord>(FuzzSupport.ROUND_TRIP_SCHEMA).write(sampleRoundTripRecord(), encoder);
+ encoder.flush();
+ return output.toByteArray();
+ }
+
+ private static byte[] writeRoundTripContainer() throws IOException {
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ try (DataFileWriter<GenericRecord> writer = new DataFileWriter<>(
+ new GenericDatumWriter<>(FuzzSupport.ROUND_TRIP_SCHEMA))) {
+ writer.create(FuzzSupport.ROUND_TRIP_SCHEMA, output);
+ writer.append(sampleRoundTripRecord());
+ }
+ return output.toByteArray();
+ }
+
+ private static byte[] writeSpecificProjectionRecord() throws IOException {
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ BinaryEncoder encoder = EncoderFactory.get().binaryEncoder(output, null);
+ new SpecificDatumWriter<SpecificProjectionRecord>(SpecificProjectionRecord.class).write(sampleSpecificRecord(),
+ encoder);
+ encoder.flush();
+ return output.toByteArray();
+ }
+
+ private static byte[] writeReflectProjectionRecord() throws IOException {
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ BinaryEncoder encoder = EncoderFactory.get().binaryEncoder(output, null);
+ new ReflectDatumWriter<ReflectRoundTripRecord>(ReflectRoundTripRecord.class, ReflectData.AllowNull.get())
+ .write(sampleReflectRecord(), encoder);
+ encoder.flush();
+ return output.toByteArray();
+ }
+
+ private static byte[] writeSingleObjectRecord() throws IOException {
+ return toArray(
+ new BinaryMessageEncoder<>(GenericData.get(), FuzzSupport.ROUND_TRIP_SCHEMA).encode(sampleRoundTripRecord()));
+ }
+
private static GenericRecord sampleBinaryRecord() {
GenericRecord nodeTail = new GenericData.Record(NODE_SCHEMA);
nodeTail.put("value", "tail");
@@ -115,6 +180,49 @@ class SeedCorpusGenerator {
return record;
}
+ private static GenericRecord sampleRoundTripRecord() {
+ GenericRecord record = new GenericData.Record(FuzzSupport.ROUND_TRIP_SCHEMA);
+ record.put("id", 19L);
+ record.put("name", "roundtrip");
+ record.put("createdDate", 42);
+ record.put("active", true);
+ record.put("score", 12.3d);
+ record.put("payload", java.nio.ByteBuffer.wrap("blob".getBytes(StandardCharsets.UTF_8)));
+ record.put("tags", java.util.List.of("alpha", "beta"));
+ record.put("counts", java.util.Map.of("x", 1L, "y", 2L));
+ record.put("choice", "choice");
+ record.put("hash",
+ new GenericData.Fixed(FuzzSupport.ROUND_TRIP_SCHEMA.getField("hash").schema(), new byte[] { 4, 3, 2, 1 }));
+ GenericRecord inner = new GenericData.Record(FuzzSupport.ROUND_TRIP_SCHEMA.getField("inner").schema());
+ inner.put("x", 7);
+ inner.put("y", 8);
+ record.put("inner", inner);
+ record.put("status",
+ new GenericData.EnumSymbol(FuzzSupport.ROUND_TRIP_SCHEMA.getField("status").schema(), "READY"));
+ return record;
+ }
+
+ private static SpecificProjectionRecord sampleSpecificRecord() {
+ return new SpecificProjectionRecord(9, "specific", java.util.List.of("one", "two"), SpecificProjectionEnum.BETA);
+ }
+
+ private static ReflectRoundTripRecord sampleReflectRecord() {
+ ReflectRoundTripRecord record = new ReflectRoundTripRecord();
+ record.id = 5L;
+ record.name = "reflect";
+ record.tags = java.util.List.of("first", "second");
+ record.counts = java.util.Map.of("left", 1L, "right", 2L);
+ record.alias = "seen";
+ return record;
+ }
+
+ private static byte[] toArray(java.nio.ByteBuffer buffer) {
+ java.nio.ByteBuffer copy = buffer.duplicate();
+ byte[] bytes = new byte[copy.remaining()];
+ copy.get(bytes);
+ return bytes;
+ }
+
private static void writeBytes(String relativePath, byte[] data) throws IOException {
Path output = RESOURCE_ROOT.resolve(relativePath);
Files.createDirectories(output.getParent());
diff --git a/lang/java/fuzz/src/test/java/org/apache/avro/fuzz/SingleObjectFuzzer.java b/lang/java/fuzz/src/test/java/org/apache/avro/fuzz/SingleObjectFuzzer.java
new file mode 100644
index 0000000000..c122e4bc67
--- /dev/null
+++ b/lang/java/fuzz/src/test/java/org/apache/avro/fuzz/SingleObjectFuzzer.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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
+ *
+ * https://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 org.apache.avro.fuzz;
+
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+import com.code_intelligence.jazzer.junit.FuzzTest;
+import org.apache.avro.generic.GenericData;
+import org.apache.avro.generic.GenericRecord;
+import org.apache.avro.message.BadHeaderException;
+import org.apache.avro.message.BinaryMessageDecoder;
+import org.apache.avro.message.BinaryMessageEncoder;
+import org.apache.avro.message.MissingSchemaException;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+class SingleObjectFuzzer {
+ @FuzzTest
+ void fuzzSingleObjectRoundTrip(FuzzedDataProvider data) throws IOException {
+ GenericRecord record = FuzzSupport.buildRoundTripRecord(data);
+ BinaryMessageEncoder<GenericRecord> encoder = new BinaryMessageEncoder<>(GenericData.get(),
+ FuzzSupport.ROUND_TRIP_SCHEMA);
+ BinaryMessageDecoder<GenericRecord> decoder = new BinaryMessageDecoder<>(GenericData.get(),
+ FuzzSupport.ROUND_TRIP_SCHEMA);
+
+ ByteBuffer encoded = encoder.encode(record);
+ GenericRecord decoded = decoder.decode(encoded);
+
+ if (!FuzzSupport.roundTripRecordsEqual(record, decoded)) {
+ throw new AssertionError("Single-object roundtrip changed semantic value");
+ }
+ }
+
+ @FuzzTest
+ void fuzzSingleObjectDecoding(byte[] data) throws IOException {
+ BinaryMessageDecoder<GenericRecord> decoder = new BinaryMessageDecoder<>(GenericData.get(),
+ FuzzSupport.ROUND_TRIP_SCHEMA);
+ try (ByteArrayInputStream input = new ByteArrayInputStream(data)) {
+ decoder.decode(input, null);
+ if (input.available() > 0) {
+ throw new AssertionError("Single-object decoder left trailing bytes unread");
+ }
+ } catch (BadHeaderException | MissingSchemaException e) {
+ // Expected for malformed or unknown single-object payloads.
+ } catch (RuntimeException e) {
+ if (!FuzzSupport.isExpectedDecodingFailure(e)) {
+ throw e;
+ }
+ }
+ }
+}
diff --git a/lang/java/fuzz/src/test/java/org/apache/avro/fuzz/model/ReflectRoundTripRecord.java b/lang/java/fuzz/src/test/java/org/apache/avro/fuzz/model/ReflectRoundTripRecord.java
new file mode 100644
index 0000000000..a5750ec532
--- /dev/null
+++ b/lang/java/fuzz/src/test/java/org/apache/avro/fuzz/model/ReflectRoundTripRecord.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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
+ *
+ * https://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 org.apache.avro.fuzz.model;
+
+import org.apache.avro.reflect.Nullable;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+public class ReflectRoundTripRecord {
+ public long id;
+ public String name;
+ public List<String> tags;
+ public Map<String, Long> counts;
+ @Nullable
+ public String alias;
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof ReflectRoundTripRecord)) {
+ return false;
+ }
+ ReflectRoundTripRecord that = (ReflectRoundTripRecord) other;
+ return id == that.id && Objects.equals(name, that.name) && Objects.equals(tags, that.tags)
+ && Objects.equals(counts, that.counts) && Objects.equals(alias, that.alias);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, name, tags, counts, alias);
+ }
+}
diff --git a/lang/java/fuzz/src/test/java/org/apache/avro/fuzz/model/SpecificProjectionEnum.java b/lang/java/fuzz/src/test/java/org/apache/avro/fuzz/model/SpecificProjectionEnum.java
new file mode 100644
index 0000000000..af5e453e96
--- /dev/null
+++ b/lang/java/fuzz/src/test/java/org/apache/avro/fuzz/model/SpecificProjectionEnum.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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
+ *
+ * https://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 org.apache.avro.fuzz.model;
+
+import org.apache.avro.Schema;
+import org.apache.avro.generic.GenericEnumSymbol;
+
+public enum SpecificProjectionEnum implements GenericEnumSymbol<SpecificProjectionEnum> {
+ ALPHA, BETA, GAMMA;
+
+ public static final Schema SCHEMA$ = new Schema.Parser().parse(
+ "{\"type\":\"enum\",\"name\":\"SpecificProjectionEnum\",\"namespace\":\"org.apache.avro.fuzz.model\",\"symbols\":[\"ALPHA\",\"BETA\",\"GAMMA\"]}");
+
+ public static Schema getClassSchema() {
+ return SCHEMA$;
+ }
+
+ @Override
+ public Schema getSchema() {
+ return SCHEMA$;
+ }
+}
diff --git a/lang/java/fuzz/src/test/java/org/apache/avro/fuzz/model/SpecificProjectionRecord.java b/lang/java/fuzz/src/test/java/org/apache/avro/fuzz/model/SpecificProjectionRecord.java
new file mode 100644
index 0000000000..3e0aa4d12f
--- /dev/null
+++ b/lang/java/fuzz/src/test/java/org/apache/avro/fuzz/model/SpecificProjectionRecord.java
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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
+ *
+ * https://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 org.apache.avro.fuzz.model;
+
+import org.apache.avro.AvroRuntimeException;
+import org.apache.avro.Schema;
+import org.apache.avro.specific.SpecificRecord;
+import org.apache.avro.specific.SpecificRecordBase;
+
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Objects;
+
+public class SpecificProjectionRecord extends SpecificRecordBase implements SpecificRecord {
+ public static final Schema SCHEMA$ = new Schema.Parser().parse(
+ "{\"type\":\"record\",\"name\":\"SpecificProjectionRecord\",\"namespace\":\"org.apache.avro.fuzz.model\",\"fields\":["
+ + "{\"name\":\"id\",\"type\":\"int\"}," + "{\"name\":\"name\",\"type\":\"string\"},"
+ + "{\"name\":\"tags\",\"type\":{\"type\":\"array\",\"items\":\"string\"}},"
+ + "{\"name\":\"status\",\"type\":[\"null\",{\"type\":\"enum\",\"name\":\"SpecificProjectionEnum\",\"symbols\":[\"ALPHA\",\"BETA\",\"GAMMA\"]}],\"default\":null}]}");
+
+ private int id;
+ private String name;
+ private List<String> tags;
+ private SpecificProjectionEnum status;
+
+ public SpecificProjectionRecord() {
+ }
+
+ public SpecificProjectionRecord(int id, String name, List<String> tags, SpecificProjectionEnum status) {
+ this.id = id;
+ this.name = name;
+ this.tags = tags;
+ this.status = status;
+ }
+
+ public static Schema getClassSchema() {
+ return SCHEMA$;
+ }
+
+ @Override
+ public Schema getSchema() {
+ return SCHEMA$;
+ }
+
+ @Override
+ public Object get(int field) {
+ switch (field) {
+ case 0:
+ return id;
+ case 1:
+ return name;
+ case 2:
+ return tags;
+ case 3:
+ return status;
+ default:
+ throw new AvroRuntimeException("Bad index: " + field);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void put(int field, Object value) {
+ switch (field) {
+ case 0:
+ id = (Integer) value;
+ break;
+ case 1:
+ name = value == null ? null : value.toString();
+ break;
+ case 2:
+ tags = normalizeStrings((List<?>) value);
+ break;
+ case 3:
+ status = (SpecificProjectionEnum) value;
+ break;
+ default:
+ throw new AvroRuntimeException("Bad index: " + field);
+ }
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof SpecificProjectionRecord)) {
+ return false;
+ }
+ SpecificProjectionRecord that = (SpecificProjectionRecord) other;
+ return id == that.id && Objects.equals(name, that.name) && Objects.equals(tags, that.tags) && status == that.status;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, name, tags, status);
+ }
+
+ private static List<String> normalizeStrings(List<?> values) {
+ if (values == null) {
+ return null;
+ }
+
+ List<String> normalized = new ArrayList<>(values.size());
+ for (Object value : values) {
+ normalized.add(value == null ? null : value.toString());
+ }
+ return normalized;
+ }
+}
diff --git a/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/DataFileReaderFuzzerInputs/fuzzDataFileReader/seed-valid-container.avro b/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/DataFileReaderFuzzerInputs/fuzzDataFileReader/seed-valid-container.avro
index 4d36604052..aac8a615a1 100644
Binary files a/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/DataFileReaderFuzzerInputs/fuzzDataFileReader/seed-valid-container.avro and b/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/DataFileReaderFuzzerInputs/fuzzDataFileReader/seed-valid-container.avro differ
diff --git a/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/DataFileReaderFuzzerInputs/fuzzDataFileReaderWithResolution/seed-valid-container.avro b/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/DataFileReaderFuzzerInputs/fuzzDataFileReaderWithResolution/seed-valid-container.avro
index 0a1867af3f..2ede2a00ef 100644
Binary files a/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/DataFileReaderFuzzerInputs/fuzzDataFileReaderWithResolution/seed-valid-container.avro and b/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/DataFileReaderFuzzerInputs/fuzzDataFileReaderWithResolution/seed-valid-container.avro differ
diff --git a/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/DataFileReaderFuzzerInputs/fuzzDataFileStream/seed-valid-container.avro b/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/DataFileReaderFuzzerInputs/fuzzDataFileStream/seed-valid-container.avro
index a7b94e7500..f83e6178d3 100644
Binary files a/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/DataFileReaderFuzzerInputs/fuzzDataFileStream/seed-valid-container.avro and b/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/DataFileReaderFuzzerInputs/fuzzDataFileStream/seed-valid-container.avro differ
diff --git a/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/DataFileReaderFuzzerInputs/fuzzDataFileStreamWithResolution/seed-valid-container.avro b/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/DataFileReaderFuzzerInputs/fuzzDataFileStreamWithResolution/seed-valid-container.avro
index b10d4dfe32..ee00c33089 100644
Binary files a/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/DataFileReaderFuzzerInputs/fuzzDataFileStreamWithResolution/seed-valid-container.avro and b/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/DataFileReaderFuzzerInputs/fuzzDataFileStreamWithResolution/seed-valid-container.avro differ
diff --git a/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/ProjectionFuzzerInputs/fuzzReflectProjection/seed-valid-reflect.bin b/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/ProjectionFuzzerInputs/fuzzReflectProjection/seed-valid-reflect.bin
new file mode 100644
index 0000000000..0242ee9bfa
Binary files /dev/null and b/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/ProjectionFuzzerInputs/fuzzReflectProjection/seed-valid-reflect.bin differ
diff --git a/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/ProjectionFuzzerInputs/fuzzSpecificProjection/seed-valid-specific.bin b/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/ProjectionFuzzerInputs/fuzzSpecificProjection/seed-valid-specific.bin
new file mode 100644
index 0000000000..8b6dbb7a9b
Binary files /dev/null and b/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/ProjectionFuzzerInputs/fuzzSpecificProjection/seed-valid-specific.bin differ
diff --git a/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/RoundTripFuzzerInputs/fuzzBinaryRoundTrip/seed-valid-roundtrip.bin b/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/RoundTripFuzzerInputs/fuzzBinaryRoundTrip/seed-valid-roundtrip.bin
new file mode 100644
index 0000000000..19f0ea0c2c
Binary files /dev/null and b/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/RoundTripFuzzerInputs/fuzzBinaryRoundTrip/seed-valid-roundtrip.bin differ
diff --git a/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/RoundTripFuzzerInputs/fuzzDataFileRoundTrip/crash-d84cec3ecd031ce35c724f6ee349f201d40a61f3 b/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/RoundTripFuzzerInputs/fuzzDataFileRoundTrip/crash-d84cec3ecd031ce35c724f6ee349f201d40a61f3
new file mode 100644
index 0000000000..7a1a338ec2
Binary files /dev/null and b/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/RoundTripFuzzerInputs/fuzzDataFileRoundTrip/crash-d84cec3ecd031ce35c724f6ee349f201d40a61f3 differ
diff --git a/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/RoundTripFuzzerInputs/fuzzDataFileRoundTrip/seed-valid-roundtrip.avro b/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/RoundTripFuzzerInputs/fuzzDataFileRoundTrip/seed-valid-roundtrip.avro
new file mode 100644
index 0000000000..9ca7433e0d
Binary files /dev/null and b/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/RoundTripFuzzerInputs/fuzzDataFileRoundTrip/seed-valid-roundtrip.avro differ
diff --git a/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/RoundTripFuzzerInputs/fuzzJsonRoundTrip/seed-valid-roundtrip.json b/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/RoundTripFuzzerInputs/fuzzJsonRoundTrip/seed-valid-roundtrip.json
new file mode 100644
index 0000000000..e0ba1505a5
--- /dev/null
+++ b/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/RoundTripFuzzerInputs/fuzzJsonRoundTrip/seed-valid-roundtrip.json
@@ -0,0 +1 @@
+{"id":19,"name":"roundtrip","createdDate":42,"active":true,"score":12.3,"payload":"blob","tags":["alpha","beta"],"counts":{"y":2,"x":1},"choice":{"string":"choice"},"hash":"\u0004\u0003\u0002\u0001","inner":{"x":7,"y":8},"status":"READY"}
\ No newline at end of file
diff --git a/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/SingleObjectFuzzerInputs/fuzzSingleObjectDecoding/seed-valid-single-object.bin b/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/SingleObjectFuzzerInputs/fuzzSingleObjectDecoding/seed-valid-single-object.bin
new file mode 100644
index 0000000000..495bf1e6b0
Binary files /dev/null and b/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/SingleObjectFuzzerInputs/fuzzSingleObjectDecoding/seed-valid-single-object.bin differ