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

github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git


The following commit(s) were added to refs/heads/main by this push:
     new c0acd1d7c1 fix(parquet): Properly account for boolean lists in Parquet 
Thrift skipping code (#11923)
c0acd1d7c1 is described below

commit c0acd1d7c149a937ba82b8ab30431de743fcad67
Author: Ed Seidl <[email protected]>
AuthorDate: Tue Oct 27 04:41:50 2026 +0000

    fix(parquet): Properly account for boolean lists in Parquet Thrift skipping 
code (#11923)
    
    # Which issue does this PR close?
    
    - Closes #11926
    
    # Rationale for this change
    
    See issue, but `list<bool>` is not skipped properly by the Thrift
    decoder.
    
    # What changes are included in this PR?
    
    Add extra testing for boolean lists/sets as well as booleans in Thrift
    maps.
    
    # Are these changes tested?
    
    Yes.
    
    # Are there any significant user-facing changes?
    
    No, this is a minor correctness fix that changes no public APIs. In
    practice this should have no effect because the only bool list in the
    current parquet IDL is in the column index, which we do not skip via
    this path.
    
    # AI Use
    
    Issue was discovered by and initial fix implemented by ChatGPT 6.1 Sol.
    I modified the fix to remove some performance regressions and fixed one
    test so it would actually error on main.
---
 parquet/src/parquet_thrift.rs | 101 ++++++++++++++++++++++++++++++++++--------
 1 file changed, 82 insertions(+), 19 deletions(-)

diff --git a/parquet/src/parquet_thrift.rs b/parquet/src/parquet_thrift.rs
index 39d0128f14..de3c674483 100644
--- a/parquet/src/parquet_thrift.rs
+++ b/parquet/src/parquet_thrift.rs
@@ -474,7 +474,7 @@ pub(crate) trait ThriftCompactInputProtocol<'a> {
     ///
     /// [binary]: 
https://github.com/apache/thrift/blob/master/doc/specs/thrift-compact-protocol.md#binary-encoding
     fn skip_binary(&mut self) -> ThriftProtocolResult<()> {
-        let len = self.read_vlq()? as usize;
+        let len = usize::try_from(self.read_vlq()?)?;
         self.skip_bytes(len)
     }
 
@@ -528,31 +528,63 @@ pub(crate) trait ThriftCompactInputProtocol<'a> {
             // see 
https://github.com/apache/thrift/blob/master/doc/specs/thrift-compact-protocol.md#list-and-set
             FieldType::List | FieldType::Set => {
                 let list_ident = self.read_list_begin()?;
-                let element_type = FieldType::from(list_ident.element_type);
-                for _ in 0..list_ident.size {
-                    self.skip_till_depth(element_type, depth - 1)?;
-                }
-                Ok(())
-            }
-            // see 
https://github.com/apache/thrift/blob/master/doc/specs/thrift-compact-protocol.md#map
-            FieldType::Map => {
-                let size = i32::try_from(self.read_vlq()?)?;
-                if size > 0 {
-                    let kv = self.read_byte()?;
-                    let key_type = FieldType::from(ElementType::try_from(kv >> 
4)?);
-                    let val_type = FieldType::from(ElementType::try_from(kv & 
0xf)?);
-                    for _ in 0..size {
-                        self.skip_till_depth(key_type, depth - 1)?;
-                        self.skip_till_depth(val_type, depth - 1)?;
+                if list_ident.element_type == ElementType::Bool {
+                    self.skip_bool_list(list_ident.size, depth - 1)
+                } else {
+                    let field_type = list_ident.element_type.into();
+                    for _ in 0..list_ident.size {
+                        self.skip_till_depth(field_type, depth - 1)?;
                     }
+                    Ok(())
                 }
-                Ok(())
             }
+            // see 
https://github.com/apache/thrift/blob/master/doc/specs/thrift-compact-protocol.md#map
+            FieldType::Map => self.skip_map(depth),
             // see 
https://github.com/apache/thrift/blob/master/doc/specs/thrift-compact-protocol.md#universal-unique-identifier-encoding
             FieldType::Uuid => self.skip_bytes(16),
             FieldType::Stop => 
Err(ThriftProtocolError::SkipUnsupportedType(field_type)),
         }
     }
+
+    /// Skip boolean list or set payloads after reading their header.
+    #[cold]
+    fn skip_bool_list(&mut self, size: i32, depth: i8) -> 
ThriftProtocolResult<()> {
+        if size > 0 && depth == 0 {
+            return Err(ThriftProtocolError::SkipDepth(FieldType::BooleanTrue));
+        }
+        self.skip_bytes(size as usize)
+    }
+
+    /// Skip maps, which are not used by the current Parquet metadata schema.
+    #[cold]
+    fn skip_map(&mut self, depth: i8) -> ThriftProtocolResult<()> {
+        let size = i32::try_from(self.read_vlq()?)?;
+        if size > 0 {
+            let kv = self.read_byte()?;
+            let key_type = ElementType::try_from(kv >> 4)?;
+            let val_type = ElementType::try_from(kv & 0xf)?;
+            for _ in 0..size {
+                self.skip_element_till_depth(key_type, depth - 1)?;
+                self.skip_element_till_depth(val_type, depth - 1)?;
+            }
+        }
+        Ok(())
+    }
+
+    /// Collection booleans have a payload byte, unlike inline struct fields.
+    fn skip_element_till_depth(
+        &mut self,
+        element_type: ElementType,
+        depth: i8,
+    ) -> ThriftProtocolResult<()> {
+        if depth == 0 {
+            return Err(ThriftProtocolError::SkipDepth(element_type.into()));
+        }
+        match element_type {
+            ElementType::Bool => self.skip_bytes(1),
+            other => self.skip_till_depth(other.into(), depth),
+        }
+    }
 }
 
 /// A high performance Thrift reader that reads from a slice of bytes.
@@ -646,10 +678,13 @@ impl<'a, R: Read> ThriftCompactInputProtocol<'a> for 
ThriftReadInputProtocol<R>
     }
 
     fn skip_bytes(&mut self, n: usize) -> ThriftProtocolResult<()> {
-        std::io::copy(
+        let skipped = std::io::copy(
             &mut self.reader.by_ref().take(n as u64),
             &mut std::io::sink(),
         )?;
+        if skipped != n as u64 {
+            return Err(ThriftProtocolError::Eof);
+        }
         Ok(())
     }
 
@@ -1230,6 +1265,34 @@ pub(crate) mod tests {
         assert!(result.is_err(), "expected error, got {result:?}");
     }
 
+    // Unknown fields must leave the following known field aligned.
+    crate::thrift_struct! {
+        struct KnownField {
+            2: required i32 value;
+        }
+    }
+
+    #[test]
+    fn test_skip_boolean_collections_before_known_field() {
+        for collection_type in [FieldType::List, FieldType::Set] {
+            // False then true must not be mistaken for inline field headers.
+            // Known field 2 follows with i32(42).
+            let data = [0x10 | collection_type as u8, 0x22, 2, 1, 0x15, 84, 0];
+            let mut protocol = ThriftSliceInputProtocol::new(&data);
+            assert_eq!(KnownField::read_thrift(&mut protocol).unwrap().value, 
42);
+            assert!(protocol.as_slice().is_empty());
+        }
+    }
+
+    #[test]
+    fn test_skip_map_before_known_field() {
+        // Unknown field 1: map<bool, i32> with two entries. Known field 2: 
i32(42).
+        let data = [0x1b, 2, 0x25, 1, 14, 2, 16, 0x15, 84, 0];
+        let mut protocol = ThriftSliceInputProtocol::new(&data);
+        assert_eq!(KnownField::read_thrift(&mut protocol).unwrap().value, 42);
+        assert!(protocol.as_slice().is_empty());
+    }
+
     #[test]
     fn test_read_list_wrong_type() {
         // list header: 4 elements of `Boolean`

Reply via email to