This is an automated email from the ASF dual-hosted git repository.
iemejia pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/avro.git
The following commit(s) were added to refs/heads/main by this push:
new bb72c294b1 AVRO-4242: [Java] Fix NPE in DataFileStream and
DataFileReader when schema metadata is missing
bb72c294b1 is described below
commit bb72c294b1c9a6290b6b190edef75fb51340bc2e
Author: Ismaël Mejía <[email protected]>
AuthorDate: Mon Apr 6 19:54:14 2026 +0000
AVRO-4242: [Java] Fix NPE in DataFileStream and DataFileReader when schema
metadata is missing
Malformed Avro container files without the 'avro.schema' metadata entry
caused a NullPointerException in both DataFileStream and DataFileReader12
when the null value was passed directly to Schema.Parser.parse(). Replace
inline parsing with null-safe helper methods that throw a descriptive
IOException instead.
---
.../org/apache/avro/file/DataFileReader12.java | 11 +--
.../java/org/apache/avro/file/DataFileStream.java | 17 ++++-
.../java/org/apache/avro/TestDataFileReader.java | 83 ++++++++++++++++++++++
3 files changed, 106 insertions(+), 5 deletions(-)
diff --git
a/lang/java/avro/src/main/java/org/apache/avro/file/DataFileReader12.java
b/lang/java/avro/src/main/java/org/apache/avro/file/DataFileReader12.java
index 0c0a670ea4..ae9bcdd966 100644
--- a/lang/java/avro/src/main/java/org/apache/avro/file/DataFileReader12.java
+++ b/lang/java/avro/src/main/java/org/apache/avro/file/DataFileReader12.java
@@ -17,8 +17,8 @@
*/
package org.apache.avro.file;
-import java.io.IOException;
import java.io.Closeable;
+import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
@@ -27,12 +27,11 @@ import java.util.Iterator;
import java.util.Map;
import org.apache.avro.InvalidAvroMagicException;
-import org.apache.avro.JsonSchemaParser;
import org.apache.avro.Schema;
import org.apache.avro.UnknownAvroCodecException;
+import org.apache.avro.io.BinaryDecoder;
import org.apache.avro.io.DatumReader;
import org.apache.avro.io.DecoderFactory;
-import org.apache.avro.io.BinaryDecoder;
/** Read files written by Avro version 1.2. */
public class DataFileReader12<D> implements FileReader<D>, Closeable {
@@ -89,7 +88,7 @@ public class DataFileReader12<D> implements FileReader<D>,
Closeable {
if (codec != null && !codec.equals(NULL_CODEC)) {
throw new UnknownAvroCodecException("Unknown codec: " + codec);
}
- this.schema = JsonSchemaParser.parseInternal(getMetaString(SCHEMA));
+ this.schema = parseSchema();
this.reader = reader;
reader.setSchema(schema);
@@ -116,6 +115,10 @@ public class DataFileReader12<D> implements FileReader<D>,
Closeable {
return Long.parseLong(getMetaString(key));
}
+ private Schema parseSchema() throws IOException {
+ return DataFileStream.parseSchemaFromMetadata(getMetaString(SCHEMA),
SCHEMA);
+ }
+
/** Return the schema used in this file. */
@Override
public Schema getSchema() {
diff --git
a/lang/java/avro/src/main/java/org/apache/avro/file/DataFileStream.java
b/lang/java/avro/src/main/java/org/apache/avro/file/DataFileStream.java
index f93ad8e5fd..9bb15183ec 100644
--- a/lang/java/avro/src/main/java/org/apache/avro/file/DataFileStream.java
+++ b/lang/java/avro/src/main/java/org/apache/avro/file/DataFileStream.java
@@ -141,7 +141,7 @@ public class DataFileStream<D> implements Iterator<D>,
Iterable<D>, Closeable {
// finalize the header
header.metaKeyList = Collections.unmodifiableList(header.metaKeyList);
- header.schema =
JsonSchemaParser.parseInternal(getMetaString(DataFileConstants.SCHEMA));
+ header.schema = parseHeaderSchema();
this.codec = resolveCodec();
reader.setSchema(header.schema);
}
@@ -198,6 +198,21 @@ public class DataFileStream<D> implements Iterator<D>,
Iterable<D>, Closeable {
return Long.parseLong(getMetaString(key));
}
+ static Schema parseSchemaFromMetadata(String schemaJson, String
schemaMetadataKey) throws IOException {
+ if (schemaJson == null) {
+ throw new IOException("Missing required metadata: " + schemaMetadataKey);
+ }
+ try {
+ return JsonSchemaParser.parseInternal(schemaJson);
+ } catch (AvroRuntimeException e) {
+ throw new IOException("Invalid schema in metadata: " +
schemaMetadataKey, e);
+ }
+ }
+
+ private Schema parseHeaderSchema() throws IOException {
+ return parseSchemaFromMetadata(getMetaString(DataFileConstants.SCHEMA),
DataFileConstants.SCHEMA);
+ }
+
/**
* Returns an iterator over entries in this file. Note that this iterator is
* shared with other users of the file: it does not contain a separate
pointer
diff --git
a/lang/java/avro/src/test/java/org/apache/avro/TestDataFileReader.java
b/lang/java/avro/src/test/java/org/apache/avro/TestDataFileReader.java
index 3919765603..0e13c8388c 100644
--- a/lang/java/avro/src/test/java/org/apache/avro/TestDataFileReader.java
+++ b/lang/java/avro/src/test/java/org/apache/avro/TestDataFileReader.java
@@ -24,19 +24,27 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
import java.lang.management.ManagementFactory;
import java.lang.management.OperatingSystemMXBean;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import com.sun.management.UnixOperatingSystemMXBean;
import org.apache.avro.file.DataFileReader;
+import org.apache.avro.file.DataFileConstants;
+import org.apache.avro.file.DataFileReader12;
import org.apache.avro.file.DataFileStream;
import org.apache.avro.file.DataFileWriter;
import org.apache.avro.file.FileReader;
+import org.apache.avro.file.SeekableByteArrayInput;
import org.apache.avro.file.SeekableFileInput;
import org.apache.avro.file.SeekableInput;
import org.apache.avro.generic.GenericDatumReader;
import org.apache.avro.generic.GenericDatumWriter;
+import org.apache.avro.io.BinaryEncoder;
+import org.apache.avro.io.EncoderFactory;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
@@ -236,4 +244,79 @@ public class TestDataFileReader {
() -> DataFileReader.openReader(fileInput, new
GenericDatumReader<>()));
}
}
+
+ @Test
+ void missingSchemaMetadataDoesNotThrowNullPointerException() throws
IOException {
+ byte[] malformedFile = buildContainerHeaderWithoutSchema();
+
+ IOException streamException = assertThrows(IOException.class,
+ () -> new DataFileStream<>(new ByteArrayInputStream(malformedFile),
new GenericDatumReader<>()));
+ assertNotNull(streamException.getMessage());
+
assertTrue(streamException.getMessage().contains(DataFileConstants.SCHEMA));
+
+ IOException readerException = assertThrows(IOException.class,
+ () -> new DataFileReader<>(new SeekableByteArrayInput(malformedFile),
new GenericDatumReader<>()));
+ assertNotNull(readerException.getMessage());
+
assertTrue(readerException.getMessage().contains(DataFileConstants.SCHEMA));
+ }
+
+ private static byte[] buildContainerHeaderWithoutSchema() throws IOException
{
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ output.write(DataFileConstants.MAGIC);
+
+ BinaryEncoder encoder = EncoderFactory.get().binaryEncoder(output, null);
+ encoder.writeMapStart();
+ encoder.setItemCount(1);
+ encoder.startItem();
+ encoder.writeString(DataFileConstants.CODEC);
+ encoder.writeBytes("null".getBytes(StandardCharsets.UTF_8));
+ encoder.writeMapEnd();
+ encoder.writeFixed(new byte[DataFileConstants.SYNC_SIZE]);
+ encoder.flush();
+
+ return output.toByteArray();
+ }
+
+ @Test
+ void missingSchemaMetadataInVersion12DoesNotThrowNullPointerException()
throws IOException {
+ byte[] malformedFile = buildVersion12ContainerWithoutSchema();
+
+ IOException exception = assertThrows(IOException.class,
+ () -> new DataFileReader12<>(new
SeekableByteArrayInput(malformedFile), new GenericDatumReader<>()));
+ assertNotNull(exception.getMessage());
+ assertTrue(exception.getMessage().contains("schema"));
+ }
+
+ /**
+ * Builds a minimal Avro 1.2 format container with the footer metadata map
+ * containing only a sync marker but no schema entry.
+ */
+ private static byte[] buildVersion12ContainerWithoutSchema() throws
IOException {
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ // Avro 1.2 magic: 'O' 'b' 'j' 0x00
+ output.write(new byte[] { (byte) 'O', (byte) 'b', (byte) 'j', 0 });
+
+ // Write the footer (metadata map with sync but no schema)
+ ByteArrayOutputStream footer = new ByteArrayOutputStream();
+ BinaryEncoder encoder = EncoderFactory.get().binaryEncoder(footer, null);
+ encoder.writeMapStart();
+ encoder.setItemCount(1);
+ encoder.startItem();
+ encoder.writeString("sync");
+ encoder.writeBytes(new byte[16]); // 16-byte sync marker
+ encoder.writeMapEnd();
+ encoder.flush();
+
+ byte[] footerBytes = footer.toByteArray();
+ // Footer size includes the 4 bytes for the size itself
+ int footerSize = footerBytes.length + 4;
+ output.write(footerBytes);
+ // Write footer size as big-endian 4 bytes at the end
+ output.write((footerSize >> 24) & 0xFF);
+ output.write((footerSize >> 16) & 0xFF);
+ output.write((footerSize >> 8) & 0xFF);
+ output.write(footerSize & 0xFF);
+
+ return output.toByteArray();
+ }
}