JingsongLi commented on code in PR #10836:
URL: https://github.com/apache/paimon/pull/10836#discussion_r4479602797


##########
paimon-format/src/main/java/org/apache/paimon/format/parquet/ParquetReaderFactory.java:
##########
@@ -247,6 +249,41 @@ public FileRecordReader<InternalRow> 
createReader(FormatReaderFactory.Context co
                 ParquetInputFile.fromPath(context.fileIO(), 
context.filePath(), context.fileSize());
         ParquetReadOptions.Builder readOptionsBuilder =
                 ParquetUtil.getParquetReadOptionsBuilder(conf).withRange(0, 
context.fileSize());
+
+        // Enable selective footer parsing when no predicates are present and 
the feature is
+        // enabled.
+        // This improves performance by only parsing metadata for requested 
columns.
+        // NOTE: When predicates are present, statistics are needed for 
row-group filtering,
+        // so selective parser is currently disabled in that case. If 
selective parser is
+        // extended to support predicates in the future, set 
parseStatistics=true to preserve
+        // statistics metadata.
+        boolean enableSelectiveParser =
+                conf.get(CoreOptions.PARQUET_FOOTER_SELECTIVE_PARSER_ENABLED);
+        if (enableSelectiveParser && (predicates == null || 
predicates.isEmpty())) {
+            StringBuilder requestedColumns = new StringBuilder();
+            for (DataField field : readType.getFields()) {
+                if (requestedColumns.length() > 0) {
+                    // Use null character as separator to support column names 
containing commas
+                    
requestedColumns.append(SelectiveFooterParser.COLUMN_NAME_SEPARATOR);
+                }

Review Comment:
   [P2] Fall back when a column name contains the separator. A valid Parquet 
file with BIGINT columns `a`, `b`, and `a\u0000b` reads `[21,22,23]` when 
projecting the third column through the official path. With this option 
enabled, the requested name is split into `a` and `b`. Those chunks are 
retained, so the all-unmatched fallback does not run, and the actual reader 
fails because the requested column is missing from the column store. Reject 
this encoding collision by falling back for NUL-bearing names, or use a 
lossless representation.



##########
paimon-format/src/main/java/org/apache/paimon/format/parquet/metadata/ThriftCompactReader.java:
##########
@@ -0,0 +1,346 @@
+/*
+ * 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.paimon.format.parquet.metadata;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Hand-written reader of the Thrift <b>Compact</b> protocol over an in-memory 
byte array.
+ *
+ * <p>It is the IO foundation of {@link SelectiveFooterParser}: parquet 
footers are serialized with
+ * the compact protocol, whose variable-length encoding forces any parser to 
scan every byte, but
+ * <i>scanning</i> and <i>materializing</i> are different costs. This reader 
exposes per-field
+ * control to the caller: fields it does not need are consumed via {@link 
#skip(byte)} (zero
+ * allocation).
+ *
+ * <p>Only the subset of the protocol used by parquet footers is implemented; 
unknown fields are
+ * skip-consumed so the reader stays forward compatible with future 
parquet.thrift revisions.
+ */
+final class ThriftCompactReader {
+
+    // Compact protocol element types.
+    static final byte STOP = 0;
+    static final byte BOOLEAN_TRUE = 1;
+    static final byte BOOLEAN_FALSE = 2;
+    static final byte BYTE = 3;
+    static final byte I16 = 4;
+    static final byte I32 = 5;
+    static final byte I64 = 6;
+    static final byte DOUBLE = 7;
+    static final byte BINARY = 8;
+    static final byte LIST = 9;
+    static final byte SET = 10;
+    static final byte MAP = 11;
+    static final byte STRUCT = 12;
+
+    private final byte[] buf;
+    private final int offset;
+    private final int limit;
+    private int pos;
+
+    /** Field id stack for nested struct walks, mirroring TCompactProtocol's 
bookkeeping. */
+    private int[] lastFieldIds = new int[16];
+
+    private int structDepth;
+
+    ThriftCompactReader(byte[] buf, int offset, int limit) {
+        this.buf = buf;
+        this.offset = offset;
+        this.limit = limit;
+        this.pos = offset;
+    }
+
+    ThriftCompactReader(byte[] buf) {
+        this(buf, 0, buf.length);
+    }
+
+    int position() {
+        return pos;
+    }
+
+    // ------------------------------------------------------------------ 
primitives
+
+    private byte readByte() throws EOFException {
+        if (pos >= limit) {
+            throw new EOFException("thrift compact reader overrun at " + pos);
+        }
+        return buf[pos++];
+    }
+
+    /** Reads a single raw byte (compact i8 value). */
+    byte readByteValue() throws EOFException {
+        return readByte();
+    }
+
+    void readBytes(byte[] target, int off, int len) throws IOException {
+        if (pos + len > limit) {
+            throw new EOFException("thrift compact reader overrun at " + pos);
+        }
+        System.arraycopy(buf, pos, target, off, len);
+        pos += len;
+    }
+
+    /** Unsigned varint (up to 10 bytes for a long). */
+    private long readVarint() throws IOException {
+        long result = 0;
+        int shift = 0;
+        while (true) {
+            byte b = readByte();
+            result |= (long) (b & 0x7f) << shift;
+            if ((b & 0x80) == 0) {
+                return result;
+            }
+            shift += 7;
+            if (shift >= 70) {
+                throw new IOException("malformed varint in parquet footer");
+            }
+        }
+    }
+
+    private long zigzag(long v) {
+        return (v >>> 1) ^ -(v & 1);
+    }
+
+    long readI64() throws IOException {
+        return zigzag(readVarint());
+    }
+
+    int readI32() throws IOException {
+        return (int) zigzag(readVarint());
+    }
+
+    short readI16() throws IOException {
+        return (short) zigzag(readVarint());
+    }
+
+    byte[] readBinary() throws IOException {
+        int len = (int) readVarint();
+        if (len < 0 || pos + len > limit) {
+            throw new EOFException("malformed binary length " + len + " at " + 
pos);
+        }
+        byte[] result = new byte[len];
+        System.arraycopy(buf, pos, result, 0, len);
+        pos += len;
+        return result;
+    }
+
+    String readString() throws IOException {
+        return new String(readBinary(), StandardCharsets.UTF_8);
+    }
+
+    // ------------------------------------------------------------------ 
struct / field
+
+    /** Enters a struct; must be paired with {@link #structEnd()} after the 
STOP field. */
+    void structBegin() {
+        if (structDepth == lastFieldIds.length) {
+            int[] grown = new int[lastFieldIds.length * 2];
+            System.arraycopy(lastFieldIds, 0, grown, 0, structDepth);
+            lastFieldIds = grown;
+        }
+        lastFieldIds[structDepth++] = 0;
+    }
+
+    void structEnd() {
+        structDepth--;
+    }
+
+    /**
+     * Reads the next field header of the current struct. Booleans carry their 
value in the header
+     * itself, so the caller must inspect {@code type} for {@link 
#BOOLEAN_TRUE}/{@link
+     * #BOOLEAN_FALSE} instead of reading a value byte.
+     *
+     * @return {@code fieldId << 8 | type}; {@code type == STOP} means the 
struct ended
+     */
+    int readFieldHeader() throws IOException {
+        byte b = readByte();
+        int type = b & 0x0f;
+        if (type == STOP) {
+            lastFieldIds[structDepth - 1] = 0;
+            return STOP;
+        }
+        int delta = (b & 0xf0) >>> 4;
+        int fieldId;
+        if (delta == 0) {
+            // short field id, zigzag varint
+            fieldId = (int) zigzag(readVarint());
+        } else {
+            fieldId = lastFieldIds[structDepth - 1] + delta;
+        }
+        lastFieldIds[structDepth - 1] = fieldId;
+        return (fieldId << 8) | type;
+    }
+
+    static int fieldId(int fieldHeader) {
+        return fieldHeader >>> 8;
+    }
+
+    static byte type(int fieldHeader) {
+        return (byte) (fieldHeader & 0xff);
+    }
+
+    // ------------------------------------------------------------------ list 
/ map
+
+    /** @return {@code size << 8 | elemType} */
+    int readListHeader() throws IOException {
+        byte b = readByte();
+        int size = (b & 0xf0) >>> 4;
+        int elemType = b & 0x0f;
+        if (size == 15) {
+            size = (int) readVarint();
+        }
+        if (size < 0) {
+            throw new IOException("malformed list size " + size + " at " + 
pos);
+        }
+        return (size << 8) | elemType;
+    }
+
+    static int listSize(int listHeader) {
+        return listHeader >>> 8;
+    }
+
+    static byte listElemType(int listHeader) {
+        return (byte) (listHeader & 0xff);
+    }
+
+    // ------------------------------------------------------------------ 
skipping
+
+    /** Skips the value of a field of the given compact type (struct-member 
context). */
+    void skip(byte type) throws IOException {
+        switch (type) {
+            case BOOLEAN_TRUE:
+            case BOOLEAN_FALSE:
+                return; // value is in the field header
+            case BYTE:
+                skipBytes(1);
+                return;
+            case I16:
+            case I32:
+            case I64:
+                readVarint();
+                return;
+            case DOUBLE:
+                skipBytes(8);
+                return;
+            case BINARY:
+                skipBytes((int) readVarint());
+                return;
+            case LIST:
+            case SET:
+                {
+                    int header = readListHeader();
+                    skipElements(listSize(header), listElemType(header));
+                    return;
+                }
+            case MAP:
+                {
+                    long size = readVarint();
+                    if (size > 0) {
+                        int kvTypes = readByte() & 0xff;
+                        skipMapEntries(size, (byte) (kvTypes >>> 4), (byte) 
(kvTypes & 0x0f));
+                    }
+                    return;
+                }
+            case STRUCT:
+                structBegin();
+                int header;
+                while ((header = readFieldHeader()) != STOP) {
+                    skip(type(header));
+                }
+                structEnd();
+                return;
+            default:
+                throw new IOException("unknown thrift compact type " + type + 
" at " + pos);
+        }
+    }
+
+    /** Skips {@code count} elements of the given type (list-element context). 
*/
+    private void skipElements(int count, byte elemType) throws IOException {
+        for (int i = 0; i < count; i++) {
+            skipElement(elemType);
+        }
+    }
+
+    /**
+     * Skips {@code size} map entries. Compact protocol map entries are 
interleaved — each key is
+     * immediately followed by its value — so they must be consumed in 
k1,v1,k2,v2 order; consuming
+     * all keys before all values would leave the cursor mid-entry, and a 
trailing zero value would
+     * then be mistaken for the enclosing struct's STOP.
+     */
+    private void skipMapEntries(long size, byte keyType, byte valueType) 
throws IOException {
+        for (long i = 0; i < size; i++) {
+            skipElement(keyType);
+            skipElement(valueType);
+        }
+    }
+
+    /** Skips a single element of the given type (list-element context). */
+    private void skipElement(byte elemType) throws IOException {
+        switch (elemType) {
+            case BOOLEAN_TRUE:
+            case BOOLEAN_FALSE:
+                skipBytes(1); // booleans are real bytes inside lists
+                break;
+            case BYTE:
+                skipBytes(1);
+                break;
+            case I16:
+            case I32:
+            case I64:
+                readVarint();
+                break;
+            case DOUBLE:
+                skipBytes(8);
+                break;
+            case BINARY:
+                skipBytes((int) readVarint());
+                break;
+            case LIST:
+            case SET:
+                {
+                    int header = readListHeader();
+                    skipElements(listSize(header), listElemType(header));
+                    break;
+                }
+            case MAP:
+                {
+                    long size = readVarint();
+                    if (size > 0) {
+                        int kvTypes = readByte() & 0xff;
+                        skipMapEntries(size, (byte) (kvTypes >>> 4), (byte) 
(kvTypes & 0x0f));
+                    }
+                    break;
+                }
+            case STRUCT:
+                skip(STRUCT);
+                break;
+            default:
+                throw new IOException(
+                        "unknown thrift compact element type " + elemType + " 
at " + pos);
+        }
+    }
+
+    private void skipBytes(int n) throws EOFException {
+        if (pos + n > limit) {
+            throw new EOFException("thrift compact reader overrun at " + pos);
+        }
+        pos += n;

Review Comment:
   [P1] Reject negative and overflowing skip lengths. The BINARY skip path 
narrows the unsigned varint length to `int`, and this method allows a negative 
value to move the cursor backwards. A 198-byte malformed footer with an unknown 
binary field and length `0xfffffff8` repeatedly returns to the same field 
header. Both the parser and actual `ParquetFileReader.readFooter` remain in 
that loop, while the official path rejects the same file immediately. This can 
leave a read task running indefinitely instead of reaching the fallback. 
Validate the length before narrowing and check `0 <= n <= limit - pos` before 
advancing. Please cover this through the actual reader.



-- 
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]

Reply via email to