vinooganesh commented on code in PR #3397:
URL: https://github.com/apache/parquet-java/pull/3397#discussion_r4305080764


##########
parquet-column/src/main/java/org/apache/parquet/column/values/alp/AlpValuesReaderForDouble.java:
##########
@@ -0,0 +1,149 @@
+/*
+ * 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
+ *
+ *   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 org.apache.parquet.column.values.alp;
+
+import static 
org.apache.parquet.column.values.alp.AlpConstants.DOUBLE_FOR_INFO_SIZE;
+import static 
org.apache.parquet.column.values.alp.AlpConstants.DOUBLE_MAX_EXPONENT;
+import static 
org.apache.parquet.column.values.alp.AlpConstants.PACK_GROUP_SIZE;
+
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import org.apache.parquet.bytes.BytesUtils;
+import org.apache.parquet.column.values.bitpacking.BytePackerForLong;
+import org.apache.parquet.column.values.bitpacking.Packer;
+import org.apache.parquet.io.ParquetDecodingException;
+
+/**
+ * ALP values reader for DOUBLE type with lazy per-vector decoding.
+ *
+ * <p>Reads ALP-encoded double values from the interleaved page layout.
+ * Each vector is decoded on first access using BytePackerForLong-based 
unpacking.
+ */
+public class AlpValuesReaderForDouble extends AlpValuesReader {
+
+  private double[] decodedValues;
+  private long[] deltasBuffer;
+  private final long[] unpackPadBuf = new long[PACK_GROUP_SIZE];
+  private byte[] unpackByteBuf;
+
+  public AlpValuesReaderForDouble() {
+    super();
+  }
+
+  @Override
+  protected void allocateDecodedBuffer(int capacity) {
+    this.decodedValues = new double[capacity];
+    this.deltasBuffer = new long[capacity];
+    this.unpackByteBuf = new byte[Long.SIZE]; // max bit width for long = 64 
bytes
+  }
+
+  @Override
+  protected int maxExponent() {
+    return DOUBLE_MAX_EXPONENT;
+  }
+
+  @Override
+  protected String typeName() {
+    return "double";
+  }
+
+  @Override
+  protected int forInfoSize() {
+    return DOUBLE_FOR_INFO_SIZE;
+  }
+
+  @Override
+  protected int exceptionValueSize() {
+    return Double.BYTES;
+  }
+
+  @Override
+  public double readDouble() {
+    if (pageValueIndex >= totalCount) {
+      throw new ParquetDecodingException("ALP double data was already 
exhausted.");
+    }
+    ensureVectorDecoded();
+    int vectorSlot = pageValueIndex % vectorSize;

Review Comment:
   Fixed, in both the double and float readers.



##########
parquet-column/src/main/java/org/apache/parquet/column/values/alp/AlpValuesReaderForDouble.java:
##########
@@ -0,0 +1,149 @@
+/*
+ * 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
+ *
+ *   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 org.apache.parquet.column.values.alp;
+
+import static 
org.apache.parquet.column.values.alp.AlpConstants.DOUBLE_FOR_INFO_SIZE;
+import static 
org.apache.parquet.column.values.alp.AlpConstants.DOUBLE_MAX_EXPONENT;
+import static 
org.apache.parquet.column.values.alp.AlpConstants.PACK_GROUP_SIZE;
+
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import org.apache.parquet.bytes.BytesUtils;
+import org.apache.parquet.column.values.bitpacking.BytePackerForLong;
+import org.apache.parquet.column.values.bitpacking.Packer;
+import org.apache.parquet.io.ParquetDecodingException;
+
+/**
+ * ALP values reader for DOUBLE type with lazy per-vector decoding.
+ *
+ * <p>Reads ALP-encoded double values from the interleaved page layout.
+ * Each vector is decoded on first access using BytePackerForLong-based 
unpacking.
+ */
+public class AlpValuesReaderForDouble extends AlpValuesReader {
+
+  private double[] decodedValues;
+  private long[] deltasBuffer;
+  private final long[] unpackPadBuf = new long[PACK_GROUP_SIZE];
+  private byte[] unpackByteBuf;
+
+  public AlpValuesReaderForDouble() {
+    super();
+  }
+
+  @Override
+  protected void allocateDecodedBuffer(int capacity) {
+    this.decodedValues = new double[capacity];
+    this.deltasBuffer = new long[capacity];
+    this.unpackByteBuf = new byte[Long.SIZE]; // max bit width for long = 64 
bytes
+  }
+
+  @Override
+  protected int maxExponent() {
+    return DOUBLE_MAX_EXPONENT;
+  }
+
+  @Override
+  protected String typeName() {
+    return "double";
+  }
+
+  @Override
+  protected int forInfoSize() {
+    return DOUBLE_FOR_INFO_SIZE;
+  }
+
+  @Override
+  protected int exceptionValueSize() {
+    return Double.BYTES;
+  }
+
+  @Override
+  public double readDouble() {
+    if (pageValueIndex >= totalCount) {
+      throw new ParquetDecodingException("ALP double data was already 
exhausted.");
+    }
+    ensureVectorDecoded();
+    int vectorSlot = pageValueIndex % vectorSize;
+    pageValueIndex++;
+    return decodedValues[vectorSlot];
+  }
+
+  @Override
+  protected int decodeBody(int pos, int vectorNumber, int vectorLen, int 
exponent, int factor) {
+    long frameOfReference = vectorsData.getLong(pos);
+    int bitWidth = vectorsData.get(pos + Long.BYTES) & 0xFF;
+    if (bitWidth > Long.SIZE) {
+      throw new ParquetDecodingException(
+          "Invalid ALP double bitWidth " + bitWidth + " > 64 in vector " + 
vectorNumber);
+    }
+    pos += DOUBLE_FOR_INFO_SIZE;
+
+    if (bitWidth > 0) {
+      pos = unpackLongsWithBytePacker(vectorsData, pos, deltasBuffer, 
vectorLen, bitWidth);
+    } else {
+      Arrays.fill(deltasBuffer, 0, vectorLen, 0L);
+    }
+
+    for (int i = 0; i < vectorLen; i++) {
+      long encoded = deltasBuffer[i] + frameOfReference;
+      decodedValues[i] = AlpCodec.decodeDouble(encoded, exponent, factor);
+    }
+    return pos;
+  }
+
+  @Override
+  protected void applyExceptionValues(int pos, int numExceptions) {

Review Comment:
   Moved reading the exception positions into this method, and the logic itself 
now lives in static methods (`readExceptionPositions` and 
`applyExceptionValues`) that take everything as arguments. The override just 
passes the fields in, since it can't be static itself. Same change in the float 
reader.



##########
parquet-column/src/main/java/org/apache/parquet/column/values/alp/AlpValuesReader.java:
##########
@@ -0,0 +1,306 @@
+/*
+ * 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
+ *
+ *   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 org.apache.parquet.column.values.alp;
+
+import static 
org.apache.parquet.column.values.alp.AlpConstants.ALP_COMPRESSION_MODE;
+import static 
org.apache.parquet.column.values.alp.AlpConstants.ALP_HEADER_SIZE;
+import static org.apache.parquet.column.values.alp.AlpConstants.ALP_INFO_SIZE;
+import static 
org.apache.parquet.column.values.alp.AlpConstants.ALP_INTEGER_ENCODING_FOR;
+import static 
org.apache.parquet.column.values.alp.AlpConstants.MAX_LOG_VECTOR_SIZE;
+import static 
org.apache.parquet.column.values.alp.AlpConstants.MIN_LOG_VECTOR_SIZE;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import org.apache.parquet.bytes.ByteBufferInputStream;
+import org.apache.parquet.column.values.ValuesReader;
+import org.apache.parquet.io.ParquetDecodingException;
+
+/**
+ * Abstract base class for ALP values readers with lazy per-vector decoding.
+ *
+ * <p>Reads ALP-encoded values from the interleaved page layout:
+ * <pre>
+ * ┌─────────┬──────────────────────┬──────────────┬──────────────┬─────┐
+ * │ Header  │ Offset Array         │ Vector 0     │ Vector 1     │ ... │
+ * │ 7 bytes │ 4B × numVectors      │ (interleaved)│ (interleaved)│     │
+ * └─────────┴──────────────────────┴──────────────┴──────────────┴─────┘
+ * </pre>
+ *
+ * <p>Each vector is decoded lazily on first access. Skipping values does not
+ * trigger decoding of intermediate vectors.
+ */
+abstract class AlpValuesReader extends ValuesReader {
+
+  protected int vectorSize;
+  protected int totalCount;
+  protected int numVectors;
+  protected int pageValueIndex;
+  protected int currentVectorNumber;
+
+  protected int[] vectorOffsets;
+  protected ByteBuffer vectorsData;
+  protected int offsetArraySize;
+
+  // Scratch buffer for exception positions within a vector; shared by both 
readers (int[] in each).
+  protected int[] excPositionsBuffer;
+
+  AlpValuesReader() {
+    this.pageValueIndex = 0;
+    this.totalCount = 0;
+    this.currentVectorNumber = -1;
+  }
+
+  @Override
+  public void initFromPage(int valuesCount, ByteBufferInputStream stream)
+      throws ParquetDecodingException, IOException {
+    ByteBuffer headerBuf = 
stream.slice(ALP_HEADER_SIZE).order(ByteOrder.LITTLE_ENDIAN);
+    int compressionMode = headerBuf.get() & 0xFF;
+    int integerEncoding = headerBuf.get() & 0xFF;
+    int logVectorSize = headerBuf.get() & 0xFF;
+    int numElements = headerBuf.getInt();
+
+    if (compressionMode != ALP_COMPRESSION_MODE) {
+      throw new ParquetDecodingException("Unsupported ALP compression mode: " 
+ compressionMode);
+    }
+    if (integerEncoding != ALP_INTEGER_ENCODING_FOR) {
+      throw new ParquetDecodingException("Unsupported ALP integer encoding: " 
+ integerEncoding);
+    }
+    if (logVectorSize < MIN_LOG_VECTOR_SIZE || logVectorSize > 
MAX_LOG_VECTOR_SIZE) {
+      throw new ParquetDecodingException("Invalid ALP log vector size: " + 
logVectorSize + ", must be between "
+          + MIN_LOG_VECTOR_SIZE + " and " + MAX_LOG_VECTOR_SIZE);
+    }
+    if (numElements < 0) {
+      throw new ParquetDecodingException("Invalid ALP element count: " + 
numElements);
+    }
+    // ALP's num_elements is the count of non-null values that went through 
encoding;
+    // valuesCount is the page row count, which is larger when the column has 
nulls.
+    // The two are equal only for required (non-null) columns.
+    if (numElements > valuesCount) {
+      throw new ParquetDecodingException(
+          "ALP header element count " + numElements + " exceeds page 
valuesCount " + valuesCount);
+    }
+
+    this.vectorSize = 1 << logVectorSize;
+    this.totalCount = numElements;
+
+    // Bound the vector count against the bytes actually present before 
allocating anything.
+    // numElements is only capped by valuesCount, which a forged page can set 
as high as
+    // Integer.MAX_VALUE, so computing this in int would overflow to a 
negative count, and even
+    // without overflow a large count would drive a huge int[] and a huge 
stream.slice allocation.
+    // Every vector costs a 4-byte offset entry plus at least its ALP and FOR 
headers, which gives
+    // a hard ceiling on how many vectors the remaining bytes can possibly 
describe.
+    int minVectorBytes = ALP_INFO_SIZE + forInfoSize();
+    long bodyBytes = stream.available();
+    long numVectorsLong = ((long) numElements + vectorSize - 1) / vectorSize;
+    long minBodyBytes = numVectorsLong * (Integer.BYTES + minVectorBytes);
+    if (minBodyBytes > bodyBytes) {

Review Comment:
   I added the first-offset check, and it now runs before the offsets array is 
allocated.
   
   I kept the byte-count bound too, though. I think the math here is off: with 
1024-value vectors, `num_elements = 2^31-1` is 2,097,152 vectors, and each one 
needs at least 17 bytes (4 for its offset plus 13 for the AlpInfo and ForInfo 
headers for doubles). So it's a multiplication, and a page would need about 36 
MB to get past the check. Anything a 1 MB page can't actually hold gets 
rejected.
   
   The two checks also catch different problems. The offset check catches 
garbage in the offset array, and the bound catches a truncated or forged page 
that claims a huge count, before we allocate based on that count. So I'd like 
to keep both, but let me know if you feel strongly.



##########
parquet-column/src/main/java/org/apache/parquet/column/values/alp/AlpValuesReader.java:
##########
@@ -0,0 +1,306 @@
+/*
+ * 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
+ *
+ *   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 org.apache.parquet.column.values.alp;
+
+import static 
org.apache.parquet.column.values.alp.AlpConstants.ALP_COMPRESSION_MODE;
+import static 
org.apache.parquet.column.values.alp.AlpConstants.ALP_HEADER_SIZE;
+import static org.apache.parquet.column.values.alp.AlpConstants.ALP_INFO_SIZE;
+import static 
org.apache.parquet.column.values.alp.AlpConstants.ALP_INTEGER_ENCODING_FOR;
+import static 
org.apache.parquet.column.values.alp.AlpConstants.MAX_LOG_VECTOR_SIZE;
+import static 
org.apache.parquet.column.values.alp.AlpConstants.MIN_LOG_VECTOR_SIZE;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import org.apache.parquet.bytes.ByteBufferInputStream;
+import org.apache.parquet.column.values.ValuesReader;
+import org.apache.parquet.io.ParquetDecodingException;
+
+/**
+ * Abstract base class for ALP values readers with lazy per-vector decoding.
+ *
+ * <p>Reads ALP-encoded values from the interleaved page layout:
+ * <pre>
+ * ┌─────────┬──────────────────────┬──────────────┬──────────────┬─────┐
+ * │ Header  │ Offset Array         │ Vector 0     │ Vector 1     │ ... │
+ * │ 7 bytes │ 4B × numVectors      │ (interleaved)│ (interleaved)│     │
+ * └─────────┴──────────────────────┴──────────────┴──────────────┴─────┘
+ * </pre>
+ *
+ * <p>Each vector is decoded lazily on first access. Skipping values does not
+ * trigger decoding of intermediate vectors.
+ */
+abstract class AlpValuesReader extends ValuesReader {
+
+  protected int vectorSize;
+  protected int totalCount;
+  protected int numVectors;
+  protected int pageValueIndex;
+  protected int currentVectorNumber;
+
+  protected int[] vectorOffsets;
+  protected ByteBuffer vectorsData;
+  protected int offsetArraySize;
+
+  // Scratch buffer for exception positions within a vector; shared by both 
readers (int[] in each).
+  protected int[] excPositionsBuffer;
+
+  AlpValuesReader() {
+    this.pageValueIndex = 0;
+    this.totalCount = 0;
+    this.currentVectorNumber = -1;
+  }
+
+  @Override
+  public void initFromPage(int valuesCount, ByteBufferInputStream stream)
+      throws ParquetDecodingException, IOException {
+    ByteBuffer headerBuf = 
stream.slice(ALP_HEADER_SIZE).order(ByteOrder.LITTLE_ENDIAN);
+    int compressionMode = headerBuf.get() & 0xFF;
+    int integerEncoding = headerBuf.get() & 0xFF;
+    int logVectorSize = headerBuf.get() & 0xFF;
+    int numElements = headerBuf.getInt();
+
+    if (compressionMode != ALP_COMPRESSION_MODE) {
+      throw new ParquetDecodingException("Unsupported ALP compression mode: " 
+ compressionMode);
+    }
+    if (integerEncoding != ALP_INTEGER_ENCODING_FOR) {
+      throw new ParquetDecodingException("Unsupported ALP integer encoding: " 
+ integerEncoding);
+    }
+    if (logVectorSize < MIN_LOG_VECTOR_SIZE || logVectorSize > 
MAX_LOG_VECTOR_SIZE) {
+      throw new ParquetDecodingException("Invalid ALP log vector size: " + 
logVectorSize + ", must be between "
+          + MIN_LOG_VECTOR_SIZE + " and " + MAX_LOG_VECTOR_SIZE);
+    }
+    if (numElements < 0) {
+      throw new ParquetDecodingException("Invalid ALP element count: " + 
numElements);
+    }
+    // ALP's num_elements is the count of non-null values that went through 
encoding;
+    // valuesCount is the page row count, which is larger when the column has 
nulls.
+    // The two are equal only for required (non-null) columns.
+    if (numElements > valuesCount) {
+      throw new ParquetDecodingException(
+          "ALP header element count " + numElements + " exceeds page 
valuesCount " + valuesCount);
+    }
+
+    this.vectorSize = 1 << logVectorSize;
+    this.totalCount = numElements;
+
+    // Bound the vector count against the bytes actually present before 
allocating anything.
+    // numElements is only capped by valuesCount, which a forged page can set 
as high as
+    // Integer.MAX_VALUE, so computing this in int would overflow to a 
negative count, and even
+    // without overflow a large count would drive a huge int[] and a huge 
stream.slice allocation.
+    // Every vector costs a 4-byte offset entry plus at least its ALP and FOR 
headers, which gives
+    // a hard ceiling on how many vectors the remaining bytes can possibly 
describe.
+    int minVectorBytes = ALP_INFO_SIZE + forInfoSize();
+    long bodyBytes = stream.available();
+    long numVectorsLong = ((long) numElements + vectorSize - 1) / vectorSize;
+    long minBodyBytes = numVectorsLong * (Integer.BYTES + minVectorBytes);
+    if (minBodyBytes > bodyBytes) {
+      throw new ParquetDecodingException("ALP header element count " + 
numElements + " requires at least "
+          + minBodyBytes + " bytes but only " + bodyBytes + " remain in the 
page");
+    }
+    this.numVectors = (int) numVectorsLong;
+    this.pageValueIndex = 0;
+    this.currentVectorNumber = -1;
+
+    this.offsetArraySize = numVectors * Integer.BYTES;
+    ByteBuffer offsetBuf = 
stream.slice(offsetArraySize).order(ByteOrder.LITTLE_ENDIAN);
+    this.vectorOffsets = new int[numVectors];
+    for (int v = 0; v < numVectors; v++) {
+      vectorOffsets[v] = offsetBuf.getInt();
+    }
+
+    // Slice remaining bytes into a 0-based view so decodeVector can use
+    // absolute get methods (vectorsData.get(pos)) directly.
+    int remainingBytes = (int) stream.available();
+    ByteBuffer rawSlice = stream.slice(remainingBytes);
+    this.vectorsData = rawSlice.slice().order(ByteOrder.LITTLE_ENDIAN);
+
+    validateVectorOffsets(minVectorBytes);
+
+    allocateDecodedBuffer(vectorSize);
+    this.excPositionsBuffer = new int[vectorSize];
+  }
+
+  /**
+   * Validates the offset array against the page body before any vector is 
decoded. Offsets are
+   * relative to the start of the compression body (the offset array itself), 
so the first vector
+   * must begin exactly where the offset array ends, offsets must increase by 
at least one vector's
+   * fixed headers, and every vector must start early enough to fit those 
headers. Without this a
+   * forged offset can point into another vector and silently decode the wrong 
values.
+   */
+  private void validateVectorOffsets(int minVectorBytes) {
+    int bodyLimit = offsetArraySize + vectorsData.limit();
+    for (int v = 0; v < numVectors; v++) {
+      int offset = vectorOffsets[v];
+      if (v == 0) {
+        if (offset != offsetArraySize) {
+          throw new ParquetDecodingException("ALP first vector offset " + 
offset
+              + " must equal the offset array size " + offsetArraySize);
+        }

Review Comment:
   Done, the loop now runs from 1 to `numVectors`.



##########
parquet-column/src/main/java/org/apache/parquet/column/values/alp/AlpValuesReader.java:
##########
@@ -0,0 +1,306 @@
+/*
+ * 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
+ *
+ *   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 org.apache.parquet.column.values.alp;
+
+import static 
org.apache.parquet.column.values.alp.AlpConstants.ALP_COMPRESSION_MODE;
+import static 
org.apache.parquet.column.values.alp.AlpConstants.ALP_HEADER_SIZE;
+import static org.apache.parquet.column.values.alp.AlpConstants.ALP_INFO_SIZE;
+import static 
org.apache.parquet.column.values.alp.AlpConstants.ALP_INTEGER_ENCODING_FOR;
+import static 
org.apache.parquet.column.values.alp.AlpConstants.MAX_LOG_VECTOR_SIZE;
+import static 
org.apache.parquet.column.values.alp.AlpConstants.MIN_LOG_VECTOR_SIZE;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import org.apache.parquet.bytes.ByteBufferInputStream;
+import org.apache.parquet.column.values.ValuesReader;
+import org.apache.parquet.io.ParquetDecodingException;
+
+/**
+ * Abstract base class for ALP values readers with lazy per-vector decoding.
+ *
+ * <p>Reads ALP-encoded values from the interleaved page layout:
+ * <pre>
+ * ┌─────────┬──────────────────────┬──────────────┬──────────────┬─────┐
+ * │ Header  │ Offset Array         │ Vector 0     │ Vector 1     │ ... │
+ * │ 7 bytes │ 4B × numVectors      │ (interleaved)│ (interleaved)│     │
+ * └─────────┴──────────────────────┴──────────────┴──────────────┴─────┘
+ * </pre>
+ *
+ * <p>Each vector is decoded lazily on first access. Skipping values does not
+ * trigger decoding of intermediate vectors.
+ */
+abstract class AlpValuesReader extends ValuesReader {
+
+  protected int vectorSize;
+  protected int totalCount;
+  protected int numVectors;
+  protected int pageValueIndex;
+  protected int currentVectorNumber;
+
+  protected int[] vectorOffsets;
+  protected ByteBuffer vectorsData;
+  protected int offsetArraySize;
+
+  // Scratch buffer for exception positions within a vector; shared by both 
readers (int[] in each).
+  protected int[] excPositionsBuffer;
+
+  AlpValuesReader() {
+    this.pageValueIndex = 0;
+    this.totalCount = 0;
+    this.currentVectorNumber = -1;
+  }
+
+  @Override
+  public void initFromPage(int valuesCount, ByteBufferInputStream stream)
+      throws ParquetDecodingException, IOException {
+    ByteBuffer headerBuf = 
stream.slice(ALP_HEADER_SIZE).order(ByteOrder.LITTLE_ENDIAN);
+    int compressionMode = headerBuf.get() & 0xFF;
+    int integerEncoding = headerBuf.get() & 0xFF;
+    int logVectorSize = headerBuf.get() & 0xFF;
+    int numElements = headerBuf.getInt();
+
+    if (compressionMode != ALP_COMPRESSION_MODE) {
+      throw new ParquetDecodingException("Unsupported ALP compression mode: " 
+ compressionMode);
+    }
+    if (integerEncoding != ALP_INTEGER_ENCODING_FOR) {
+      throw new ParquetDecodingException("Unsupported ALP integer encoding: " 
+ integerEncoding);
+    }
+    if (logVectorSize < MIN_LOG_VECTOR_SIZE || logVectorSize > 
MAX_LOG_VECTOR_SIZE) {
+      throw new ParquetDecodingException("Invalid ALP log vector size: " + 
logVectorSize + ", must be between "
+          + MIN_LOG_VECTOR_SIZE + " and " + MAX_LOG_VECTOR_SIZE);
+    }
+    if (numElements < 0) {
+      throw new ParquetDecodingException("Invalid ALP element count: " + 
numElements);
+    }
+    // ALP's num_elements is the count of non-null values that went through 
encoding;
+    // valuesCount is the page row count, which is larger when the column has 
nulls.
+    // The two are equal only for required (non-null) columns.
+    if (numElements > valuesCount) {
+      throw new ParquetDecodingException(
+          "ALP header element count " + numElements + " exceeds page 
valuesCount " + valuesCount);
+    }
+
+    this.vectorSize = 1 << logVectorSize;
+    this.totalCount = numElements;
+
+    // Bound the vector count against the bytes actually present before 
allocating anything.
+    // numElements is only capped by valuesCount, which a forged page can set 
as high as
+    // Integer.MAX_VALUE, so computing this in int would overflow to a 
negative count, and even
+    // without overflow a large count would drive a huge int[] and a huge 
stream.slice allocation.
+    // Every vector costs a 4-byte offset entry plus at least its ALP and FOR 
headers, which gives
+    // a hard ceiling on how many vectors the remaining bytes can possibly 
describe.
+    int minVectorBytes = ALP_INFO_SIZE + forInfoSize();
+    long bodyBytes = stream.available();
+    long numVectorsLong = ((long) numElements + vectorSize - 1) / vectorSize;
+    long minBodyBytes = numVectorsLong * (Integer.BYTES + minVectorBytes);
+    if (minBodyBytes > bodyBytes) {
+      throw new ParquetDecodingException("ALP header element count " + 
numElements + " requires at least "
+          + minBodyBytes + " bytes but only " + bodyBytes + " remain in the 
page");
+    }
+    this.numVectors = (int) numVectorsLong;
+    this.pageValueIndex = 0;
+    this.currentVectorNumber = -1;
+
+    this.offsetArraySize = numVectors * Integer.BYTES;
+    ByteBuffer offsetBuf = 
stream.slice(offsetArraySize).order(ByteOrder.LITTLE_ENDIAN);
+    this.vectorOffsets = new int[numVectors];
+    for (int v = 0; v < numVectors; v++) {
+      vectorOffsets[v] = offsetBuf.getInt();
+    }
+
+    // Slice remaining bytes into a 0-based view so decodeVector can use
+    // absolute get methods (vectorsData.get(pos)) directly.
+    int remainingBytes = (int) stream.available();
+    ByteBuffer rawSlice = stream.slice(remainingBytes);
+    this.vectorsData = rawSlice.slice().order(ByteOrder.LITTLE_ENDIAN);
+
+    validateVectorOffsets(minVectorBytes);
+
+    allocateDecodedBuffer(vectorSize);
+    this.excPositionsBuffer = new int[vectorSize];
+  }
+
+  /**
+   * Validates the offset array against the page body before any vector is 
decoded. Offsets are
+   * relative to the start of the compression body (the offset array itself), 
so the first vector
+   * must begin exactly where the offset array ends, offsets must increase by 
at least one vector's
+   * fixed headers, and every vector must start early enough to fit those 
headers. Without this a
+   * forged offset can point into another vector and silently decode the wrong 
values.
+   */
+  private void validateVectorOffsets(int minVectorBytes) {
+    int bodyLimit = offsetArraySize + vectorsData.limit();
+    for (int v = 0; v < numVectors; v++) {
+      int offset = vectorOffsets[v];
+      if (v == 0) {
+        if (offset != offsetArraySize) {
+          throw new ParquetDecodingException("ALP first vector offset " + 
offset
+              + " must equal the offset array size " + offsetArraySize);
+        }
+      } else if ((long) offset - vectorOffsets[v - 1] < minVectorBytes) {

Review Comment:
   Added. One difference from the bound you suggested on `:113`: it has to 
leave room for exceptions, which cost 2 bytes for the position plus the value 
itself. Random doubles are almost all exceptions, so a 1024-value vector of 
them takes about 10,253 bytes even at bit width 0, and `17 + 8 * vectorSize` 
only allows 8,209. Using that bound breaks 20 of the existing round-trip tests. 
So the limit assumes every value is packed at full width and also stored as an 
exception. It's loose, but it won't reject a valid page, and with the extra end 
entry from `:176` it covers the last vector too.



##########
parquet-column/src/main/java/org/apache/parquet/column/values/alp/AlpValuesReader.java:
##########
@@ -0,0 +1,306 @@
+/*
+ * 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
+ *
+ *   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 org.apache.parquet.column.values.alp;
+
+import static 
org.apache.parquet.column.values.alp.AlpConstants.ALP_COMPRESSION_MODE;
+import static 
org.apache.parquet.column.values.alp.AlpConstants.ALP_HEADER_SIZE;
+import static org.apache.parquet.column.values.alp.AlpConstants.ALP_INFO_SIZE;
+import static 
org.apache.parquet.column.values.alp.AlpConstants.ALP_INTEGER_ENCODING_FOR;
+import static 
org.apache.parquet.column.values.alp.AlpConstants.MAX_LOG_VECTOR_SIZE;
+import static 
org.apache.parquet.column.values.alp.AlpConstants.MIN_LOG_VECTOR_SIZE;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import org.apache.parquet.bytes.ByteBufferInputStream;
+import org.apache.parquet.column.values.ValuesReader;
+import org.apache.parquet.io.ParquetDecodingException;
+
+/**
+ * Abstract base class for ALP values readers with lazy per-vector decoding.
+ *
+ * <p>Reads ALP-encoded values from the interleaved page layout:
+ * <pre>
+ * ┌─────────┬──────────────────────┬──────────────┬──────────────┬─────┐
+ * │ Header  │ Offset Array         │ Vector 0     │ Vector 1     │ ... │
+ * │ 7 bytes │ 4B × numVectors      │ (interleaved)│ (interleaved)│     │
+ * └─────────┴──────────────────────┴──────────────┴──────────────┴─────┘
+ * </pre>
+ *
+ * <p>Each vector is decoded lazily on first access. Skipping values does not
+ * trigger decoding of intermediate vectors.
+ */
+abstract class AlpValuesReader extends ValuesReader {
+
+  protected int vectorSize;
+  protected int totalCount;
+  protected int numVectors;
+  protected int pageValueIndex;
+  protected int currentVectorNumber;
+
+  protected int[] vectorOffsets;
+  protected ByteBuffer vectorsData;
+  protected int offsetArraySize;
+
+  // Scratch buffer for exception positions within a vector; shared by both 
readers (int[] in each).
+  protected int[] excPositionsBuffer;
+
+  AlpValuesReader() {
+    this.pageValueIndex = 0;
+    this.totalCount = 0;
+    this.currentVectorNumber = -1;
+  }
+
+  @Override
+  public void initFromPage(int valuesCount, ByteBufferInputStream stream)
+      throws ParquetDecodingException, IOException {
+    ByteBuffer headerBuf = 
stream.slice(ALP_HEADER_SIZE).order(ByteOrder.LITTLE_ENDIAN);
+    int compressionMode = headerBuf.get() & 0xFF;
+    int integerEncoding = headerBuf.get() & 0xFF;
+    int logVectorSize = headerBuf.get() & 0xFF;
+    int numElements = headerBuf.getInt();
+
+    if (compressionMode != ALP_COMPRESSION_MODE) {
+      throw new ParquetDecodingException("Unsupported ALP compression mode: " 
+ compressionMode);
+    }
+    if (integerEncoding != ALP_INTEGER_ENCODING_FOR) {
+      throw new ParquetDecodingException("Unsupported ALP integer encoding: " 
+ integerEncoding);
+    }
+    if (logVectorSize < MIN_LOG_VECTOR_SIZE || logVectorSize > 
MAX_LOG_VECTOR_SIZE) {
+      throw new ParquetDecodingException("Invalid ALP log vector size: " + 
logVectorSize + ", must be between "
+          + MIN_LOG_VECTOR_SIZE + " and " + MAX_LOG_VECTOR_SIZE);
+    }
+    if (numElements < 0) {
+      throw new ParquetDecodingException("Invalid ALP element count: " + 
numElements);
+    }
+    // ALP's num_elements is the count of non-null values that went through 
encoding;
+    // valuesCount is the page row count, which is larger when the column has 
nulls.
+    // The two are equal only for required (non-null) columns.
+    if (numElements > valuesCount) {
+      throw new ParquetDecodingException(
+          "ALP header element count " + numElements + " exceeds page 
valuesCount " + valuesCount);
+    }
+
+    this.vectorSize = 1 << logVectorSize;
+    this.totalCount = numElements;
+
+    // Bound the vector count against the bytes actually present before 
allocating anything.
+    // numElements is only capped by valuesCount, which a forged page can set 
as high as
+    // Integer.MAX_VALUE, so computing this in int would overflow to a 
negative count, and even
+    // without overflow a large count would drive a huge int[] and a huge 
stream.slice allocation.
+    // Every vector costs a 4-byte offset entry plus at least its ALP and FOR 
headers, which gives
+    // a hard ceiling on how many vectors the remaining bytes can possibly 
describe.
+    int minVectorBytes = ALP_INFO_SIZE + forInfoSize();
+    long bodyBytes = stream.available();
+    long numVectorsLong = ((long) numElements + vectorSize - 1) / vectorSize;
+    long minBodyBytes = numVectorsLong * (Integer.BYTES + minVectorBytes);
+    if (minBodyBytes > bodyBytes) {
+      throw new ParquetDecodingException("ALP header element count " + 
numElements + " requires at least "
+          + minBodyBytes + " bytes but only " + bodyBytes + " remain in the 
page");
+    }
+    this.numVectors = (int) numVectorsLong;
+    this.pageValueIndex = 0;
+    this.currentVectorNumber = -1;
+
+    this.offsetArraySize = numVectors * Integer.BYTES;
+    ByteBuffer offsetBuf = 
stream.slice(offsetArraySize).order(ByteOrder.LITTLE_ENDIAN);
+    this.vectorOffsets = new int[numVectors];
+    for (int v = 0; v < numVectors; v++) {
+      vectorOffsets[v] = offsetBuf.getInt();
+    }
+
+    // Slice remaining bytes into a 0-based view so decodeVector can use
+    // absolute get methods (vectorsData.get(pos)) directly.
+    int remainingBytes = (int) stream.available();
+    ByteBuffer rawSlice = stream.slice(remainingBytes);
+    this.vectorsData = rawSlice.slice().order(ByteOrder.LITTLE_ENDIAN);
+
+    validateVectorOffsets(minVectorBytes);
+
+    allocateDecodedBuffer(vectorSize);
+    this.excPositionsBuffer = new int[vectorSize];
+  }
+
+  /**
+   * Validates the offset array against the page body before any vector is 
decoded. Offsets are
+   * relative to the start of the compression body (the offset array itself), 
so the first vector
+   * must begin exactly where the offset array ends, offsets must increase by 
at least one vector's
+   * fixed headers, and every vector must start early enough to fit those 
headers. Without this a
+   * forged offset can point into another vector and silently decode the wrong 
values.
+   */
+  private void validateVectorOffsets(int minVectorBytes) {
+    int bodyLimit = offsetArraySize + vectorsData.limit();
+    for (int v = 0; v < numVectors; v++) {
+      int offset = vectorOffsets[v];
+      if (v == 0) {
+        if (offset != offsetArraySize) {
+          throw new ParquetDecodingException("ALP first vector offset " + 
offset
+              + " must equal the offset array size " + offsetArraySize);
+        }
+      } else if ((long) offset - vectorOffsets[v - 1] < minVectorBytes) {
+        throw new ParquetDecodingException("ALP vector offsets must increase 
by at least "
+            + minVectorBytes + " bytes, but vector " + v + " starts at " + 
offset + " after vector "
+            + (v - 1) + " at " + vectorOffsets[v - 1]);
+      }
+      if (offset > bodyLimit - minVectorBytes) {
+        throw new ParquetDecodingException("ALP vector " + v + " offset " + 
offset
+            + " leaves no room for its headers before the end of the page body 
at " + bodyLimit);
+      }
+    }
+  }
+
+  /**
+   * The position just past the last byte vector {@code vectorNumber} may 
read, derived from the
+   * next vector's offset (or the end of the body for the final vector).
+   */
+  private int getVectorEndPosition(int vectorNumber) {
+    if (vectorNumber < numVectors - 1) {
+      return vectorOffsets[vectorNumber + 1] - offsetArraySize;
+    }
+    return vectorsData.limit();

Review Comment:
   Changed `vectorOffsets` to have `numVectors + 1` entries, with the last one 
marking the end of the vector data. Out-of-range indexes now throw 
`IllegalArgumentException`, so `Integer.MAX_VALUE` no longer returns `limit()`.



##########
parquet-column/src/main/java/org/apache/parquet/column/values/alp/AlpValuesReader.java:
##########
@@ -0,0 +1,306 @@
+/*
+ * 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
+ *
+ *   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 org.apache.parquet.column.values.alp;
+
+import static 
org.apache.parquet.column.values.alp.AlpConstants.ALP_COMPRESSION_MODE;
+import static 
org.apache.parquet.column.values.alp.AlpConstants.ALP_HEADER_SIZE;
+import static org.apache.parquet.column.values.alp.AlpConstants.ALP_INFO_SIZE;
+import static 
org.apache.parquet.column.values.alp.AlpConstants.ALP_INTEGER_ENCODING_FOR;
+import static 
org.apache.parquet.column.values.alp.AlpConstants.MAX_LOG_VECTOR_SIZE;
+import static 
org.apache.parquet.column.values.alp.AlpConstants.MIN_LOG_VECTOR_SIZE;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import org.apache.parquet.bytes.ByteBufferInputStream;
+import org.apache.parquet.column.values.ValuesReader;
+import org.apache.parquet.io.ParquetDecodingException;
+
+/**
+ * Abstract base class for ALP values readers with lazy per-vector decoding.
+ *
+ * <p>Reads ALP-encoded values from the interleaved page layout:
+ * <pre>
+ * ┌─────────┬──────────────────────┬──────────────┬──────────────┬─────┐
+ * │ Header  │ Offset Array         │ Vector 0     │ Vector 1     │ ... │
+ * │ 7 bytes │ 4B × numVectors      │ (interleaved)│ (interleaved)│     │
+ * └─────────┴──────────────────────┴──────────────┴──────────────┴─────┘
+ * </pre>
+ *
+ * <p>Each vector is decoded lazily on first access. Skipping values does not
+ * trigger decoding of intermediate vectors.
+ */
+abstract class AlpValuesReader extends ValuesReader {
+
+  protected int vectorSize;
+  protected int totalCount;
+  protected int numVectors;
+  protected int pageValueIndex;
+  protected int currentVectorNumber;
+
+  protected int[] vectorOffsets;
+  protected ByteBuffer vectorsData;
+  protected int offsetArraySize;
+
+  // Scratch buffer for exception positions within a vector; shared by both 
readers (int[] in each).
+  protected int[] excPositionsBuffer;
+
+  AlpValuesReader() {
+    this.pageValueIndex = 0;
+    this.totalCount = 0;
+    this.currentVectorNumber = -1;
+  }
+
+  @Override
+  public void initFromPage(int valuesCount, ByteBufferInputStream stream)
+      throws ParquetDecodingException, IOException {
+    ByteBuffer headerBuf = 
stream.slice(ALP_HEADER_SIZE).order(ByteOrder.LITTLE_ENDIAN);
+    int compressionMode = headerBuf.get() & 0xFF;
+    int integerEncoding = headerBuf.get() & 0xFF;
+    int logVectorSize = headerBuf.get() & 0xFF;
+    int numElements = headerBuf.getInt();
+
+    if (compressionMode != ALP_COMPRESSION_MODE) {
+      throw new ParquetDecodingException("Unsupported ALP compression mode: " 
+ compressionMode);
+    }
+    if (integerEncoding != ALP_INTEGER_ENCODING_FOR) {
+      throw new ParquetDecodingException("Unsupported ALP integer encoding: " 
+ integerEncoding);
+    }
+    if (logVectorSize < MIN_LOG_VECTOR_SIZE || logVectorSize > 
MAX_LOG_VECTOR_SIZE) {
+      throw new ParquetDecodingException("Invalid ALP log vector size: " + 
logVectorSize + ", must be between "
+          + MIN_LOG_VECTOR_SIZE + " and " + MAX_LOG_VECTOR_SIZE);
+    }
+    if (numElements < 0) {
+      throw new ParquetDecodingException("Invalid ALP element count: " + 
numElements);
+    }
+    // ALP's num_elements is the count of non-null values that went through 
encoding;
+    // valuesCount is the page row count, which is larger when the column has 
nulls.
+    // The two are equal only for required (non-null) columns.
+    if (numElements > valuesCount) {
+      throw new ParquetDecodingException(
+          "ALP header element count " + numElements + " exceeds page 
valuesCount " + valuesCount);
+    }
+
+    this.vectorSize = 1 << logVectorSize;
+    this.totalCount = numElements;
+
+    // Bound the vector count against the bytes actually present before 
allocating anything.
+    // numElements is only capped by valuesCount, which a forged page can set 
as high as
+    // Integer.MAX_VALUE, so computing this in int would overflow to a 
negative count, and even
+    // without overflow a large count would drive a huge int[] and a huge 
stream.slice allocation.
+    // Every vector costs a 4-byte offset entry plus at least its ALP and FOR 
headers, which gives
+    // a hard ceiling on how many vectors the remaining bytes can possibly 
describe.
+    int minVectorBytes = ALP_INFO_SIZE + forInfoSize();
+    long bodyBytes = stream.available();
+    long numVectorsLong = ((long) numElements + vectorSize - 1) / vectorSize;
+    long minBodyBytes = numVectorsLong * (Integer.BYTES + minVectorBytes);
+    if (minBodyBytes > bodyBytes) {
+      throw new ParquetDecodingException("ALP header element count " + 
numElements + " requires at least "
+          + minBodyBytes + " bytes but only " + bodyBytes + " remain in the 
page");
+    }
+    this.numVectors = (int) numVectorsLong;
+    this.pageValueIndex = 0;
+    this.currentVectorNumber = -1;
+
+    this.offsetArraySize = numVectors * Integer.BYTES;
+    ByteBuffer offsetBuf = 
stream.slice(offsetArraySize).order(ByteOrder.LITTLE_ENDIAN);
+    this.vectorOffsets = new int[numVectors];
+    for (int v = 0; v < numVectors; v++) {
+      vectorOffsets[v] = offsetBuf.getInt();
+    }
+
+    // Slice remaining bytes into a 0-based view so decodeVector can use
+    // absolute get methods (vectorsData.get(pos)) directly.
+    int remainingBytes = (int) stream.available();
+    ByteBuffer rawSlice = stream.slice(remainingBytes);
+    this.vectorsData = rawSlice.slice().order(ByteOrder.LITTLE_ENDIAN);
+
+    validateVectorOffsets(minVectorBytes);
+
+    allocateDecodedBuffer(vectorSize);
+    this.excPositionsBuffer = new int[vectorSize];
+  }
+
+  /**
+   * Validates the offset array against the page body before any vector is 
decoded. Offsets are
+   * relative to the start of the compression body (the offset array itself), 
so the first vector
+   * must begin exactly where the offset array ends, offsets must increase by 
at least one vector's
+   * fixed headers, and every vector must start early enough to fit those 
headers. Without this a
+   * forged offset can point into another vector and silently decode the wrong 
values.
+   */
+  private void validateVectorOffsets(int minVectorBytes) {
+    int bodyLimit = offsetArraySize + vectorsData.limit();

Review Comment:
   Good point. The data size is now recorded once when the data is sliced, and 
nothing reads `limit()` or `remaining()` after that. We hit a similar bug 
earlier with absolute reads on the page header, so there's also a test now with 
a page buffer that doesn't start at position 0.



##########
parquet-column/src/main/java/org/apache/parquet/column/values/alp/AlpValuesReader.java:
##########
@@ -0,0 +1,306 @@
+/*
+ * 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
+ *
+ *   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 org.apache.parquet.column.values.alp;
+
+import static 
org.apache.parquet.column.values.alp.AlpConstants.ALP_COMPRESSION_MODE;
+import static 
org.apache.parquet.column.values.alp.AlpConstants.ALP_HEADER_SIZE;
+import static org.apache.parquet.column.values.alp.AlpConstants.ALP_INFO_SIZE;
+import static 
org.apache.parquet.column.values.alp.AlpConstants.ALP_INTEGER_ENCODING_FOR;
+import static 
org.apache.parquet.column.values.alp.AlpConstants.MAX_LOG_VECTOR_SIZE;
+import static 
org.apache.parquet.column.values.alp.AlpConstants.MIN_LOG_VECTOR_SIZE;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import org.apache.parquet.bytes.ByteBufferInputStream;
+import org.apache.parquet.column.values.ValuesReader;
+import org.apache.parquet.io.ParquetDecodingException;
+
+/**
+ * Abstract base class for ALP values readers with lazy per-vector decoding.
+ *
+ * <p>Reads ALP-encoded values from the interleaved page layout:
+ * <pre>
+ * ┌─────────┬──────────────────────┬──────────────┬──────────────┬─────┐
+ * │ Header  │ Offset Array         │ Vector 0     │ Vector 1     │ ... │
+ * │ 7 bytes │ 4B × numVectors      │ (interleaved)│ (interleaved)│     │
+ * └─────────┴──────────────────────┴──────────────┴──────────────┴─────┘
+ * </pre>
+ *
+ * <p>Each vector is decoded lazily on first access. Skipping values does not
+ * trigger decoding of intermediate vectors.
+ */
+abstract class AlpValuesReader extends ValuesReader {
+
+  protected int vectorSize;
+  protected int totalCount;
+  protected int numVectors;
+  protected int pageValueIndex;
+  protected int currentVectorNumber;
+
+  protected int[] vectorOffsets;
+  protected ByteBuffer vectorsData;
+  protected int offsetArraySize;
+
+  // Scratch buffer for exception positions within a vector; shared by both 
readers (int[] in each).
+  protected int[] excPositionsBuffer;
+
+  AlpValuesReader() {
+    this.pageValueIndex = 0;
+    this.totalCount = 0;
+    this.currentVectorNumber = -1;
+  }
+
+  @Override
+  public void initFromPage(int valuesCount, ByteBufferInputStream stream)
+      throws ParquetDecodingException, IOException {
+    ByteBuffer headerBuf = 
stream.slice(ALP_HEADER_SIZE).order(ByteOrder.LITTLE_ENDIAN);
+    int compressionMode = headerBuf.get() & 0xFF;
+    int integerEncoding = headerBuf.get() & 0xFF;
+    int logVectorSize = headerBuf.get() & 0xFF;
+    int numElements = headerBuf.getInt();
+
+    if (compressionMode != ALP_COMPRESSION_MODE) {
+      throw new ParquetDecodingException("Unsupported ALP compression mode: " 
+ compressionMode);
+    }
+    if (integerEncoding != ALP_INTEGER_ENCODING_FOR) {
+      throw new ParquetDecodingException("Unsupported ALP integer encoding: " 
+ integerEncoding);
+    }
+    if (logVectorSize < MIN_LOG_VECTOR_SIZE || logVectorSize > 
MAX_LOG_VECTOR_SIZE) {
+      throw new ParquetDecodingException("Invalid ALP log vector size: " + 
logVectorSize + ", must be between "
+          + MIN_LOG_VECTOR_SIZE + " and " + MAX_LOG_VECTOR_SIZE);
+    }
+    if (numElements < 0) {
+      throw new ParquetDecodingException("Invalid ALP element count: " + 
numElements);
+    }
+    // ALP's num_elements is the count of non-null values that went through 
encoding;
+    // valuesCount is the page row count, which is larger when the column has 
nulls.
+    // The two are equal only for required (non-null) columns.
+    if (numElements > valuesCount) {
+      throw new ParquetDecodingException(
+          "ALP header element count " + numElements + " exceeds page 
valuesCount " + valuesCount);
+    }
+
+    this.vectorSize = 1 << logVectorSize;
+    this.totalCount = numElements;
+
+    // Bound the vector count against the bytes actually present before 
allocating anything.
+    // numElements is only capped by valuesCount, which a forged page can set 
as high as
+    // Integer.MAX_VALUE, so computing this in int would overflow to a 
negative count, and even
+    // without overflow a large count would drive a huge int[] and a huge 
stream.slice allocation.
+    // Every vector costs a 4-byte offset entry plus at least its ALP and FOR 
headers, which gives
+    // a hard ceiling on how many vectors the remaining bytes can possibly 
describe.
+    int minVectorBytes = ALP_INFO_SIZE + forInfoSize();
+    long bodyBytes = stream.available();
+    long numVectorsLong = ((long) numElements + vectorSize - 1) / vectorSize;
+    long minBodyBytes = numVectorsLong * (Integer.BYTES + minVectorBytes);
+    if (minBodyBytes > bodyBytes) {
+      throw new ParquetDecodingException("ALP header element count " + 
numElements + " requires at least "
+          + minBodyBytes + " bytes but only " + bodyBytes + " remain in the 
page");
+    }
+    this.numVectors = (int) numVectorsLong;
+    this.pageValueIndex = 0;
+    this.currentVectorNumber = -1;
+
+    this.offsetArraySize = numVectors * Integer.BYTES;
+    ByteBuffer offsetBuf = 
stream.slice(offsetArraySize).order(ByteOrder.LITTLE_ENDIAN);
+    this.vectorOffsets = new int[numVectors];
+    for (int v = 0; v < numVectors; v++) {
+      vectorOffsets[v] = offsetBuf.getInt();
+    }
+
+    // Slice remaining bytes into a 0-based view so decodeVector can use
+    // absolute get methods (vectorsData.get(pos)) directly.
+    int remainingBytes = (int) stream.available();
+    ByteBuffer rawSlice = stream.slice(remainingBytes);
+    this.vectorsData = rawSlice.slice().order(ByteOrder.LITTLE_ENDIAN);
+
+    validateVectorOffsets(minVectorBytes);
+
+    allocateDecodedBuffer(vectorSize);
+    this.excPositionsBuffer = new int[vectorSize];
+  }
+
+  /**
+   * Validates the offset array against the page body before any vector is 
decoded. Offsets are
+   * relative to the start of the compression body (the offset array itself), 
so the first vector
+   * must begin exactly where the offset array ends, offsets must increase by 
at least one vector's
+   * fixed headers, and every vector must start early enough to fit those 
headers. Without this a
+   * forged offset can point into another vector and silently decode the wrong 
values.
+   */
+  private void validateVectorOffsets(int minVectorBytes) {

Review Comment:
   It's static now: `validateVectorOffsets(int[] vectorOffsets, int 
minVectorBytes, int maxVectorBytes)`. I didn't add a separate data size 
argument because the data size is now the last entry of `vectorOffsets` (from 
`:176`), so the last vector gets checked by the same loop. I can pass it 
separately if you'd rather.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to