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

JingsongLi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-rust.git


The following commit(s) were added to refs/heads/main by this push:
     new dcc13995 fix(spec): enforce the writer's layout in the string array 
decoder (#770)
dcc13995 is described below

commit dcc13995cac62009dbdd55f147e51bb36c609f53
Author: Junrui Lee <[email protected]>
AuthorDate: Wed Sep 2 15:28:35 2026 +0800

    fix(spec): enforce the writer's layout in the string array decoder (#770)
---
 crates/paimon/src/spec/binary_row.rs | 174 ++++++++++++++++++++++++++++++++++-
 1 file changed, 170 insertions(+), 4 deletions(-)

diff --git a/crates/paimon/src/spec/binary_row.rs 
b/crates/paimon/src/spec/binary_row.rs
index 7f430a39..ab126897 100644
--- a/crates/paimon/src/spec/binary_row.rs
+++ b/crates/paimon/src/spec/binary_row.rs
@@ -765,6 +765,11 @@ pub fn serialize_binary_array_long(values: &[Option<i64>]) 
-> Vec<u8> {
     data
 }
 
+/// Largest string body the writer keeps in an element's own 8-byte slot
+/// (Java `BinarySection.MAX_FIX_PART_DATA_SIZE`): seven bytes of content plus 
the
+/// marker byte that carries the length.
+const MAX_INLINE_STRING_LEN: usize = 7;
+
 /// Reverse of [`serialize_binary_array_str`].
 pub fn deserialize_binary_array_str(data: &[u8]) -> crate::Result<Vec<String>> 
{
     let n = read_binary_array_len(data)?;
@@ -774,26 +779,58 @@ pub fn deserialize_binary_array_str(data: &[u8]) -> 
crate::Result<Vec<String>> {
     // reservation and the loop, so a forged large count (with or without
     // element slots) cannot amplify memory before per-element validation.
     check_binary_array_fits(n, header, data.len())?;
+    let fixed_part = round_to_word(header + n * 8);
+    if fixed_part > data.len() {
+        return Err(bin_arr_err(
+            "string array element region exceeds buffer length",
+        ));
+    }
+    // The writer's cursor starts after the fixed part and advances by each 
body's
+    // word-padded length, so a body starts exactly where the previous one 
left the
+    // cursor and the buffer ends at the last one. An inline body lives in the
+    // element's own slot and does not move the cursor. Requiring exactly that
+    // leaves no layout the writer cannot emit: no gap, no trailing bytes, and 
no
+    // two elements sharing a body -- which would let a small array drive a 
decode
+    // many times its own size, since every element is cloned into a `String`.
+    let mut next = fixed_part;
     let mut out = Vec::with_capacity(n);
     for k in 0..n {
+        // Every schema that reaches here declares non-null elements, so a set 
bit
+        // is not writer output. Java would read it as null; reading the slot 
as a
+        // value would yield a string that was never written.
+        if data.get(4 + k / 8).is_some_and(|b| b & (1 << (k % 8)) != 0) {
+            return Err(bin_arr_err("string element must not be null"));
+        }
         let eo = header + k * 8;
         let slot = data
             .get(eo..eo + 8)
             .ok_or_else(|| bin_arr_err("string element slot out of range"))?;
         let marker = slot[7];
         let bytes = if marker & 0x80 != 0 {
+            // The marker's seven length bits can claim up to 127 bytes, well 
past
+            // the slot the content has to fit in.
             let len = (marker & 0x7F) as usize;
-            slot.get(..len)
-                .ok_or_else(|| bin_arr_err("inline string length out of 
range"))?
+            if len > MAX_INLINE_STRING_LEN {
+                return Err(bin_arr_err("inline string length exceeds its 
slot"));
+            }
+            &slot[..len]
         } else {
             let encoded = u64::from_le_bytes(slot.try_into().unwrap());
             let var_off = (encoded >> 32) as usize;
             let len = (encoded & 0xFFFF_FFFF) as usize;
+            if var_off != next {
+                return Err(bin_arr_err(
+                    "string element body must start where the previous element 
ended",
+                ));
+            }
             let end = var_off
                 .checked_add(len)
                 .ok_or_else(|| bin_arr_err("variable string bytes out of 
range"))?;
-            data.get(var_off..end)
-                .ok_or_else(|| bin_arr_err("variable string bytes out of 
range"))?
+            let bytes = data
+                .get(var_off..end)
+                .ok_or_else(|| bin_arr_err("variable string bytes out of 
range"))?;
+            next = round_to_word(end);
+            bytes
         };
         out.push(
             std::str::from_utf8(bytes)
@@ -801,6 +838,9 @@ pub fn deserialize_binary_array_str(data: &[u8]) -> 
crate::Result<Vec<String>> {
                 .to_string(),
         );
     }
+    if next != data.len() {
+        return Err(bin_arr_err("string array has bytes after its last 
element"));
+    }
     Ok(out)
 }
 
@@ -2333,6 +2373,132 @@ mod tests {
         assert!(deserialize_binary_array_str(&[1, 0]).is_err()); // < 4 header 
bytes
     }
 
+    /// A string array's fixed part: 4-byte count, a 4-byte null word for up 
to 32
+    /// elements, then one 8-byte slot per element, word-padded. The 
variable-length
+    /// part starts there, which is where the writer's cursor starts.
+    fn str_array_fixed_part(n: usize) -> usize {
+        round_to_word(binary_array_header(n) + n * 8)
+    }
+
+    /// Point element `k`'s slot at a body of `len` bytes at `offset`, the 
pointer
+    /// form the writer uses for anything longer than 7 bytes.
+    fn set_str_pointer(data: &mut [u8], n: usize, k: usize, offset: usize, 
len: usize) {
+        let slot = binary_array_header(n) + k * 8;
+        let encoded = ((offset as u64) << 32) | (len as u64);
+        data[slot..slot + 8].copy_from_slice(&encoded.to_le_bytes());
+    }
+
+    /// An inline element is written into the fixed part and leaves the 
writer's
+    /// cursor alone, so an all-inline array is exactly its fixed part long. 
Pinning
+    /// that here keeps the cursor walk from over-rejecting the legal layout.
+    #[test]
+    fn binary_array_str_all_inline_is_exactly_its_fixed_part() {
+        let values = vec!["".to_string(), "1234567".to_string(), 
"ab".to_string()];
+        let bytes = serialize_binary_array_str(&values);
+        assert_eq!(bytes.len(), str_array_fixed_part(values.len()));
+        assert_eq!(deserialize_binary_array_str(&bytes).unwrap(), values);
+    }
+
+    /// The writer never points two elements at one body. Accepting it would 
let a
+    /// small array drive a decode many times its own size, since every 
element is
+    /// cloned into a `String` of its own.
+    #[test]
+    fn binary_array_str_rejects_aliased_elements() {
+        let n = 2;
+        let first_body = str_array_fixed_part(n);
+        let mut data = vec![0u8; first_body + 16];
+        data[0..4].copy_from_slice(&(n as i32).to_le_bytes());
+        data[first_body..first_body + 16].copy_from_slice(b"AAAAAAAABBBBBBBB");
+        set_str_pointer(&mut data, n, 0, first_body, 8);
+        set_str_pointer(&mut data, n, 1, first_body, 8);
+        let error = deserialize_binary_array_str(&data).unwrap_err();
+        assert!(
+            error.to_string().contains("must start where the previous"),
+            "unexpected error: {error}"
+        );
+    }
+
+    /// The writer leaves no gap between bodies either, so a body one word 
late is
+    /// as much a forgery as one that overlaps.
+    #[test]
+    fn binary_array_str_rejects_a_gap_before_an_element() {
+        let n = 1;
+        let fixed_part = str_array_fixed_part(n);
+        // The body itself is inside the buffer, so this can only fail on the 
gap.
+        let mut data = vec![0u8; fixed_part + 16];
+        data[0..4].copy_from_slice(&(n as i32).to_le_bytes());
+        data[fixed_part + 8..fixed_part + 16].copy_from_slice(b"AAAAAAAA");
+        set_str_pointer(&mut data, n, 0, fixed_part + 8, 8);
+        let error = deserialize_binary_array_str(&data).unwrap_err();
+        assert!(
+            error.to_string().contains("must start where the previous"),
+            "unexpected error: {error}"
+        );
+    }
+
+    /// The array ends where its last element ends, so trailing bytes are a 
layout
+    /// the writer cannot emit.
+    #[test]
+    fn binary_array_str_rejects_trailing_bytes() {
+        let mut data = serialize_binary_array_str(&["12345678".to_string()]);
+        data.push(0);
+        let error = deserialize_binary_array_str(&data).unwrap_err();
+        assert!(
+            error.to_string().contains("after its last element"),
+            "unexpected error: {error}"
+        );
+    }
+
+    /// A body addressed inside the fixed part reads the array's own header as 
a
+    /// value. Those bytes are valid UTF-8, so without the cursor walk this 
returns
+    /// a garbage string instead of an error.
+    #[test]
+    fn binary_array_str_rejects_a_body_in_the_fixed_part() {
+        let n = 1;
+        let mut data = vec![0u8; str_array_fixed_part(n)];
+        data[0..4].copy_from_slice(&(n as i32).to_le_bytes());
+        set_str_pointer(&mut data, n, 0, 0, 8);
+        let error = deserialize_binary_array_str(&data).unwrap_err();
+        assert!(
+            error.to_string().contains("must start where the previous"),
+            "unexpected error: {error}"
+        );
+    }
+
+    /// Every call site declares non-null elements, so a set null bit cannot 
come
+    /// from the writer. Java's reader would return null for it; reading the 
slot as
+    /// if it held a value yields a string that was never written.
+    #[test]
+    fn binary_array_str_rejects_a_set_null_bit() {
+        let mut data = serialize_binary_array_str(&["hello".to_string()]);
+        data[4] |= 1;
+        let error = deserialize_binary_array_str(&data).unwrap_err();
+        assert!(
+            error
+                .to_string()
+                .contains("string element must not be null"),
+            "unexpected error: {error}"
+        );
+    }
+
+    /// The inline marker carries 7 length bits, so it can claim up to 127 
bytes
+    /// while the writer emits at most 7 -- a length of 8 would read the marker
+    /// byte itself as content.
+    #[test]
+    fn binary_array_str_rejects_an_inline_length_past_the_slot() {
+        let n = 1;
+        let mut data = vec![0u8; str_array_fixed_part(n)];
+        data[0..4].copy_from_slice(&(n as i32).to_le_bytes());
+        let slot = binary_array_header(n);
+        data[slot..slot + 7].copy_from_slice(b"aaaaaaa");
+        data[slot + 7] = 0x80 | 8;
+        let error = deserialize_binary_array_str(&data).unwrap_err();
+        assert!(
+            error.to_string().contains("inline string length"),
+            "unexpected error: {error}"
+        );
+    }
+
     #[test]
     fn binary_array_rejects_huge_length_prefix() {
         // A 4-byte buffer whose length prefix decodes to i32::MAX must return 
an

Reply via email to