This is an automated email from the ASF dual-hosted git repository.

Fokko pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/parquet-java.git


The following commit(s) were added to refs/heads/master by this push:
     new 0b1da61cc GH-3542: Fix `LocalInputFile` `ByteBuffer` read methods 
(#3543)
0b1da61cc is described below

commit 0b1da61cc705dc4539a765b4a08a7d00d315fc97
Author: André Rouél <[email protected]>
AuthorDate: Thu Jun 25 22:49:05 2026 +0200

    GH-3542: Fix `LocalInputFile` `ByteBuffer` read methods (#3543)
    
    Summary
    Root cause 
(parquet-common/src/main/java/org/apache/parquet/io/LocalInputFile.java)
    Both read(ByteBuffer) and readFully(ByteBuffer) called:
    buf.put(buffer, buf.position() + buf.arrayOffset(), buf.remaining());
    Two independent bugs:
    1. Wrong put overload semantics. ByteBuffer.put(byte[] src, int offset, int 
length) reads src[offset..offset+length]. The code passed buf.position() + 
buf.arrayOffset() as the source offset, but the source is the freshly-allocated 
buffer array whose indices have no relationship to buf.position() or 
buf.arrayOffset(). On heap buffers with position=0 and arrayOffset=0 this 
happens to work; any other state reads from the wrong offset (or throws 
ArrayIndexOutOfBoundsException).
    2. arrayOffset() is not universally defined. Direct buffers, memory-mapped 
buffers, and read-only views all throw UnsupportedOperationException from 
arrayOffset(). Any Parquet consumer passing such a buffer (which is exactly 
what the steampipe integration test triggers via ParquetFileReader.readFooter) 
blows up at the first call.
    3. Partial-read accounting. read(ByteBuffer) unconditionally copied 
buf.remaining() bytes regardless of how many read(byte[]) actually returned, 
corrupting the destination buffer on short reads / EOF.
    Fix
    - readFully(ByteBuffer) → buf.put(buffer) (straight array-to-buffer copy, 
no arrayOffset()).
    - read(ByteBuffer) → only put read bytes when read > 0, return read 
(including -1 at EOF).
    Tests added
    TestLocalInputOutput gained 8 regression cases in 
parquet-common/src/test/java/org/apache/parquet/io/:
    - readFullyIntoHeapByteBuffer — baseline happy path.
    - readFullyIntoHeapByteBufferWithNonZeroPosition — exercises the old 
wrong-source-offset bug.
    - readFullyIntoDirectByteBuffer — direct buffers; arrayOffset() would have 
thrown.
    - readFullyIntoReadOnlyByteBuffer — read-only views; asserts 
ReadOnlyBufferException (correct JDK behaviour after the fix).
    - readIntoHeapByteBuffer — baseline.
    - readIntoByteBufferAdvancesPositionByBytesRead — exercises the 
partial-read accounting bug.
    - readIntoByteBufferReturnsMinusOneAtEof — EOF signalling.
    - readIntoDirectByteBuffer — direct buffer read path.
---
 .../java/org/apache/parquet/io/LocalInputFile.java |  10 +-
 .../apache/parquet/io/TestLocalInputOutput.java    | 144 +++++++++++++++++++++
 2 files changed, 150 insertions(+), 4 deletions(-)

diff --git 
a/parquet-common/src/main/java/org/apache/parquet/io/LocalInputFile.java 
b/parquet-common/src/main/java/org/apache/parquet/io/LocalInputFile.java
index 7174b42d5..59e2c142d 100644
--- a/parquet-common/src/main/java/org/apache/parquet/io/LocalInputFile.java
+++ b/parquet-common/src/main/java/org/apache/parquet/io/LocalInputFile.java
@@ -81,16 +81,18 @@ public class LocalInputFile implements InputFile {
       @Override
       public int read(ByteBuffer buf) throws IOException {
         byte[] buffer = new byte[buf.remaining()];
-        int code = read(buffer);
-        buf.put(buffer, buf.position() + buf.arrayOffset(), buf.remaining());
-        return code;
+        int bytesRead = randomAccessFile.read(buffer);
+        if (bytesRead > 0) {
+          buf.put(buffer, 0, bytesRead);
+        }
+        return bytesRead;
       }
 
       @Override
       public void readFully(ByteBuffer buf) throws IOException {
         byte[] buffer = new byte[buf.remaining()];
         readFully(buffer);
-        buf.put(buffer, buf.position() + buf.arrayOffset(), buf.remaining());
+        buf.put(buffer);
       }
 
       @Override
diff --git 
a/parquet-common/src/test/java/org/apache/parquet/io/TestLocalInputOutput.java 
b/parquet-common/src/test/java/org/apache/parquet/io/TestLocalInputOutput.java
index 84d728f14..f7401a1fc 100644
--- 
a/parquet-common/src/test/java/org/apache/parquet/io/TestLocalInputOutput.java
+++ 
b/parquet-common/src/test/java/org/apache/parquet/io/TestLocalInputOutput.java
@@ -18,11 +18,15 @@
  */
 package org.apache.parquet.io;
 
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertThrows;
 
+import java.io.EOFException;
 import java.io.File;
 import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ReadOnlyBufferException;
 import java.nio.file.FileAlreadyExistsException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -89,4 +93,144 @@ public class TestLocalInputOutput {
     tmp.delete();
     return tmp;
   }
+
+  @Test
+  public void readFullyIntoHeapByteBuffer() throws IOException {
+    Path path = writeBytes(new byte[] {1, 2, 3, 4, 5});
+    try (SeekableInputStream stream = new LocalInputFile(path).newStream()) {
+      ByteBuffer buf = ByteBuffer.allocate(5);
+      stream.readFully(buf);
+      assertEquals(5, buf.position());
+      buf.flip();
+      byte[] out = new byte[5];
+      buf.get(out);
+      assertArrayEquals(new byte[] {1, 2, 3, 4, 5}, out);
+    }
+  }
+
+  @Test
+  public void readFullyIntoHeapByteBufferWithNonZeroPosition() throws 
IOException {
+    Path path = writeBytes(new byte[] {10, 20, 30, 40});
+    try (SeekableInputStream stream = new LocalInputFile(path).newStream()) {
+      ByteBuffer buf = ByteBuffer.allocate(6);
+      buf.put(new byte[] {99, 99}); // advance position to 2
+      stream.readFully(buf);
+      assertEquals(6, buf.position());
+      buf.flip();
+      byte[] out = new byte[6];
+      buf.get(out);
+      assertArrayEquals(new byte[] {99, 99, 10, 20, 30, 40}, out);
+    }
+  }
+
+  @Test
+  public void readFullyIntoDirectByteBuffer() throws IOException {
+    Path path = writeBytes(new byte[] {7, 8, 9});
+    try (SeekableInputStream stream = new LocalInputFile(path).newStream()) {
+      ByteBuffer buf = ByteBuffer.allocateDirect(3);
+      stream.readFully(buf);
+      assertEquals(3, buf.position());
+      buf.flip();
+      byte[] out = new byte[3];
+      buf.get(out);
+      assertArrayEquals(new byte[] {7, 8, 9}, out);
+    }
+  }
+
+  @Test
+  public void readFullyIntoReadOnlyByteBuffer() throws IOException {
+    Path path = writeBytes(new byte[] {7, 8, 9});
+    try (SeekableInputStream stream = new LocalInputFile(path).newStream()) {
+      ByteBuffer backing = ByteBuffer.allocate(3);
+      ByteBuffer buf = backing.asReadOnlyBuffer();
+      assertThrows(ReadOnlyBufferException.class, () -> stream.readFully(buf));
+    }
+  }
+
+  @Test
+  public void readIntoHeapByteBuffer() throws IOException {
+    Path path = writeBytes(new byte[] {1, 2, 3, 4});
+    try (SeekableInputStream stream = new LocalInputFile(path).newStream()) {
+      ByteBuffer buf = ByteBuffer.allocate(4);
+      int read = stream.read(buf);
+      assertEquals(4, read);
+      assertEquals(4, buf.position());
+      buf.flip();
+      byte[] out = new byte[4];
+      buf.get(out);
+      assertArrayEquals(new byte[] {1, 2, 3, 4}, out);
+    }
+  }
+
+  @Test
+  public void readIntoByteBufferAdvancesPositionByBytesRead() throws 
IOException {
+    Path path = writeBytes(new byte[] {1, 2, 3});
+    try (SeekableInputStream stream = new LocalInputFile(path).newStream()) {
+      ByteBuffer buf = ByteBuffer.allocate(10);
+      int read = stream.read(buf);
+      assertEquals(3, read);
+      assertEquals(3, buf.position());
+    }
+  }
+
+  @Test
+  public void readIntoByteBufferReturnsMinusOneAtEof() throws IOException {
+    Path path = writeBytes(new byte[] {1});
+    try (SeekableInputStream stream = new LocalInputFile(path).newStream()) {
+      assertEquals(1, stream.read());
+      ByteBuffer buf = ByteBuffer.allocate(4);
+      int read = stream.read(buf);
+      assertEquals(-1, read);
+      assertEquals(0, buf.position());
+    }
+  }
+
+  @Test
+  public void readIntoDirectByteBuffer() throws IOException {
+    Path path = writeBytes(new byte[] {7, 8, 9});
+    try (SeekableInputStream stream = new LocalInputFile(path).newStream()) {
+      ByteBuffer buf = ByteBuffer.allocateDirect(3);
+      int read = stream.read(buf);
+      assertEquals(3, read);
+      assertEquals(3, buf.position());
+      buf.flip();
+      byte[] out = new byte[3];
+      buf.get(out);
+      assertArrayEquals(new byte[] {7, 8, 9}, out);
+    }
+  }
+
+  @Test
+  public void readIntoByteBufferWithNonZeroPosition() throws IOException {
+    Path path = writeBytes(new byte[] {10, 20, 30});
+    try (SeekableInputStream stream = new LocalInputFile(path).newStream()) {
+      ByteBuffer buf = ByteBuffer.allocate(5);
+      buf.put(new byte[] {99, 99}); // advance position to 2
+      int read = stream.read(buf);
+      assertEquals(3, read);
+      assertEquals(5, buf.position());
+      buf.flip();
+      byte[] out = new byte[5];
+      buf.get(out);
+      assertArrayEquals(new byte[] {99, 99, 10, 20, 30}, out);
+    }
+  }
+
+  @Test
+  public void readFullyThrowsEofWhenStreamTooShort() throws IOException {
+    Path path = writeBytes(new byte[] {1, 2});
+    try (SeekableInputStream stream = new LocalInputFile(path).newStream()) {
+      ByteBuffer buf = ByteBuffer.allocate(10);
+      assertThrows(EOFException.class, () -> stream.readFully(buf));
+    }
+  }
+
+  private Path writeBytes(byte[] data) throws IOException {
+    Path path = Paths.get(createTempFile().getPath());
+    OutputFile write = new LocalOutputFile(path);
+    try (PositionOutputStream stream = write.createOrOverwrite(512)) {
+      stream.write(data);
+    }
+    return path;
+  }
 }

Reply via email to