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();
+  }
 }

Reply via email to