Skip to content

Commit f0cbb84

Browse files
authored
feat: deserialize DataSplit from Paimon-native bytes (core + C binding) (#565)
1 parent 9908c47 commit f0cbb84

6 files changed

Lines changed: 1077 additions & 5 deletions

File tree

bindings/c/src/table.rs

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ use arrow_array::ffi::{FFI_ArrowArray, FFI_ArrowSchema};
2222
use arrow_array::{Array, StructArray};
2323
use futures::StreamExt;
2424
use paimon::spec::{DataField, DataType, Datum, Predicate, PredicateBuilder};
25-
use paimon::table::{ArrowRecordBatchStream, Table};
25+
use paimon::table::{ArrowRecordBatchStream, DataSplit, Table};
2626
use paimon::Plan;
2727

2828
use crate::error::{check_non_null, paimon_error, validate_cstr, PaimonErrorCode};
@@ -472,10 +472,54 @@ pub unsafe extern "C" fn paimon_table_scan_plan(
472472

473473
// ======================= Plan ===============================
474474

475+
/// Build a one-split `paimon_plan` from a serialized Paimon-native `DataSplit`
476+
/// byte buffer (the wire form produced by `DataSplit::serialize` / Java
477+
/// `DataSplit#serialize`). `data` must be raw bytes (Base64 already decoded by
478+
/// the caller).
479+
///
480+
/// The returned plan is usable with `paimon_table_read_to_arrow` and must be
481+
/// freed with `paimon_plan_free`.
482+
///
483+
/// # Safety
484+
/// `data` must point to `len` valid bytes, or be null when `len == 0`.
485+
#[no_mangle]
486+
pub unsafe extern "C" fn paimon_plan_from_split_bytes(
487+
data: *const u8,
488+
len: usize,
489+
) -> paimon_result_plan {
490+
if data.is_null() || len == 0 {
491+
return paimon_result_plan {
492+
plan: std::ptr::null_mut(),
493+
error: paimon_error::new(
494+
PaimonErrorCode::InvalidInput,
495+
"paimon_plan_from_split_bytes: null or empty buffer".to_string(),
496+
),
497+
};
498+
}
499+
let bytes = std::slice::from_raw_parts(data, len);
500+
match DataSplit::deserialize(bytes) {
501+
Ok(split) => {
502+
let plan = Plan::new(vec![split]);
503+
let wrapper = Box::new(paimon_plan {
504+
inner: Box::into_raw(Box::new(plan)) as *mut c_void,
505+
});
506+
paimon_result_plan {
507+
plan: Box::into_raw(wrapper),
508+
error: std::ptr::null_mut(),
509+
}
510+
}
511+
Err(e) => paimon_result_plan {
512+
plan: std::ptr::null_mut(),
513+
error: paimon_error::from_paimon(e),
514+
},
515+
}
516+
}
517+
475518
/// Free a paimon_plan.
476519
///
477520
/// # Safety
478521
/// Only call with a plan returned from `paimon_table_scan_plan`.
522+
/// A plan returned from `paimon_plan_from_split_bytes` is also a valid source.
479523
#[no_mangle]
480524
pub unsafe extern "C" fn paimon_plan_free(plan: *mut paimon_plan) {
481525
if !plan.is_null() {
@@ -490,6 +534,7 @@ pub unsafe extern "C" fn paimon_plan_free(plan: *mut paimon_plan) {
490534
///
491535
/// # Safety
492536
/// `plan` must be a valid pointer from `paimon_table_scan_plan`, or null (returns 0).
537+
/// A plan returned from `paimon_plan_from_split_bytes` is also a valid source.
493538
#[no_mangle]
494539
pub unsafe extern "C" fn paimon_plan_num_splits(plan: *const paimon_plan) -> usize {
495540
if plan.is_null() {
@@ -1736,6 +1781,12 @@ const _: unsafe extern "C" fn(
17361781
usize,
17371782
) -> paimon_result_read_builder = paimon_table_new_read_builder_with_options;
17381783

1784+
// Plan constructor ABI signature guard. Pins the symbol that builds a plan from
1785+
// serialized split bytes so an accidental signature change fails to compile
1786+
// rather than silently breaking header consumers.
1787+
const _: unsafe extern "C" fn(*const u8, usize) -> paimon_result_plan =
1788+
paimon_plan_from_split_bytes;
1789+
17391790
#[cfg(test)]
17401791
mod tests {
17411792
use super::*;

bindings/c/src/tests.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2418,3 +2418,59 @@ fn vector_search_empty_result_is_eof_stream() {
24182418
unwrap_table(handle);
24192419
}
24202420
}
2421+
2422+
// =========================================================================
2423+
// paimon_plan_from_split_bytes
2424+
// =========================================================================
2425+
2426+
#[test]
2427+
fn plan_from_split_bytes_round_trips() {
2428+
let path = "memory:/test_plan_from_split_bytes";
2429+
let file_io = memory_file_io();
2430+
setup_table_dirs(&file_io, path);
2431+
let table = Table::new(
2432+
file_io.clone(),
2433+
Identifier::new("default", "test"),
2434+
path.to_string(),
2435+
simple_table_schema(),
2436+
None,
2437+
);
2438+
write_data_rust(&table, &[make_batch(vec![1, 2, 3], vec!["a", "b", "c"])]);
2439+
2440+
// Obtain a real DataSplit from a plan via the Rust API, then serialize it.
2441+
let splits = crate::runtime().block_on(async {
2442+
let rb = table.new_read_builder();
2443+
let scan = rb.new_scan();
2444+
scan.plan().await.unwrap().splits().to_vec()
2445+
});
2446+
assert!(!splits.is_empty(), "expected at least one planned split");
2447+
let bytes = splits[0].serialize().unwrap();
2448+
2449+
let result = unsafe { paimon_plan_from_split_bytes(bytes.as_ptr(), bytes.len()) };
2450+
assert!(result.error.is_null());
2451+
assert_eq!(unsafe { paimon_plan_num_splits(result.plan) }, 1);
2452+
unsafe { paimon_plan_free(result.plan) };
2453+
}
2454+
2455+
#[test]
2456+
fn plan_from_split_bytes_rejects_null_and_empty() {
2457+
let r = unsafe { paimon_plan_from_split_bytes(std::ptr::null(), 0) };
2458+
assert!(r.plan.is_null());
2459+
assert!(!r.error.is_null());
2460+
unsafe { paimon_error_free(r.error) };
2461+
2462+
let dummy = [0u8; 1];
2463+
let r2 = unsafe { paimon_plan_from_split_bytes(dummy.as_ptr(), 0) };
2464+
assert!(r2.plan.is_null());
2465+
assert!(!r2.error.is_null());
2466+
unsafe { paimon_error_free(r2.error) };
2467+
}
2468+
2469+
#[test]
2470+
fn plan_from_split_bytes_rejects_garbage() {
2471+
let garbage = [1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10];
2472+
let r = unsafe { paimon_plan_from_split_bytes(garbage.as_ptr(), garbage.len()) };
2473+
assert!(r.plan.is_null());
2474+
assert!(!r.error.is_null());
2475+
unsafe { paimon_error_free(r.error) };
2476+
}

crates/paimon/src/spec/binary_row.rs

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -757,6 +757,104 @@ pub fn serialize_binary_array_long(values: &[Option<i64>]) -> Vec<u8> {
757757
data
758758
}
759759

760+
/// Reverse of [`serialize_binary_array_str`].
761+
pub fn deserialize_binary_array_str(data: &[u8]) -> crate::Result<Vec<String>> {
762+
let n = read_binary_array_len(data)?;
763+
let header = binary_array_header(n);
764+
// The fixed element region is `n * 8` bytes after the header; reject any
765+
// count whose slots cannot fit in the buffer up front. This bounds both the
766+
// reservation and the loop, so a forged large count (with or without
767+
// element slots) cannot amplify memory before per-element validation.
768+
check_binary_array_fits(n, header, data.len())?;
769+
let mut out = Vec::with_capacity(n);
770+
for k in 0..n {
771+
let eo = header + k * 8;
772+
let slot = data
773+
.get(eo..eo + 8)
774+
.ok_or_else(|| bin_arr_err("string element slot out of range"))?;
775+
let marker = slot[7];
776+
let bytes = if marker & 0x80 != 0 {
777+
let len = (marker & 0x7F) as usize;
778+
slot.get(..len)
779+
.ok_or_else(|| bin_arr_err("inline string length out of range"))?
780+
} else {
781+
let encoded = u64::from_le_bytes(slot.try_into().unwrap());
782+
let var_off = (encoded >> 32) as usize;
783+
let len = (encoded & 0xFFFF_FFFF) as usize;
784+
let end = var_off
785+
.checked_add(len)
786+
.ok_or_else(|| bin_arr_err("variable string bytes out of range"))?;
787+
data.get(var_off..end)
788+
.ok_or_else(|| bin_arr_err("variable string bytes out of range"))?
789+
};
790+
out.push(
791+
std::str::from_utf8(bytes)
792+
.map_err(|_| bin_arr_err("string element is not valid UTF-8"))?
793+
.to_string(),
794+
);
795+
}
796+
Ok(out)
797+
}
798+
799+
/// Reverse of [`serialize_binary_array_long`].
800+
pub fn deserialize_binary_array_long(data: &[u8]) -> crate::Result<Vec<Option<i64>>> {
801+
let n = read_binary_array_len(data)?;
802+
let header = binary_array_header(n);
803+
// See `deserialize_binary_array_str`: reject a count whose fixed element
804+
// region overflows the buffer before allocating. Null elements skip the
805+
// per-slot read, so this up-front check is what prevents a forged
806+
// "large count + all-null bitmap + no slots" input from amplifying memory.
807+
check_binary_array_fits(n, header, data.len())?;
808+
let mut out = Vec::with_capacity(n);
809+
for k in 0..n {
810+
let null = data
811+
.get(4 + k / 8)
812+
.map(|b| b & (1 << (k % 8)) != 0)
813+
.unwrap_or(false);
814+
if null {
815+
out.push(None);
816+
} else {
817+
let eo = header + k * 8;
818+
let slot = data
819+
.get(eo..eo + 8)
820+
.ok_or_else(|| bin_arr_err("long element slot out of range"))?;
821+
out.push(Some(i64::from_le_bytes(slot.try_into().unwrap())));
822+
}
823+
}
824+
Ok(out)
825+
}
826+
827+
fn read_binary_array_len(data: &[u8]) -> crate::Result<usize> {
828+
let raw = data
829+
.get(0..4)
830+
.ok_or_else(|| bin_arr_err("binary array too short for length prefix"))?;
831+
let n = i32::from_le_bytes(raw.try_into().unwrap());
832+
if n < 0 {
833+
return Err(bin_arr_err("binary array has negative length"));
834+
}
835+
Ok(n as usize)
836+
}
837+
838+
/// Reject a binary array whose `n` fixed 8-byte element slots cannot fit in the
839+
/// buffer after its `header`. Computed without overflow so a forged count
840+
/// cannot wrap; guards allocation and iteration for both decoders (`None`
841+
/// elements otherwise skip the per-slot bounds check).
842+
fn check_binary_array_fits(n: usize, header: usize, data_len: usize) -> crate::Result<()> {
843+
if n > data_len.saturating_sub(header) / 8 {
844+
return Err(bin_arr_err(
845+
"binary array element region exceeds buffer length",
846+
));
847+
}
848+
Ok(())
849+
}
850+
851+
fn bin_arr_err(msg: &str) -> crate::Error {
852+
crate::Error::DataInvalid {
853+
message: msg.to_string(),
854+
source: None,
855+
}
856+
}
857+
760858
/// Extract a Datum from an Arrow RecordBatch column at the given row index.
761859
pub fn extract_datum_from_arrow(
762860
batch: &RecordBatch,
@@ -1917,4 +2015,59 @@ mod tests {
19172015
"binary-row write path must store euclidean timestamp parts"
19182016
);
19192017
}
2018+
2019+
#[test]
2020+
fn binary_array_str_round_trips() {
2021+
for v in [
2022+
vec![],
2023+
vec!["a".to_string()],
2024+
vec!["".to_string(), "short".to_string(), "x".repeat(20)],
2025+
vec!["1234567".to_string(), "12345678".to_string()], // 7-byte inline vs 8-byte pointer
2026+
] {
2027+
let bytes = serialize_binary_array_str(&v);
2028+
assert_eq!(deserialize_binary_array_str(&bytes).unwrap(), v);
2029+
}
2030+
}
2031+
2032+
#[test]
2033+
fn binary_array_long_round_trips() {
2034+
for v in [
2035+
vec![],
2036+
vec![Some(1i64), None, Some(-5), Some(i64::MAX)],
2037+
vec![None, None],
2038+
] {
2039+
let bytes = serialize_binary_array_long(&v);
2040+
assert_eq!(deserialize_binary_array_long(&bytes).unwrap(), v);
2041+
}
2042+
}
2043+
2044+
#[test]
2045+
fn binary_array_str_rejects_truncated() {
2046+
assert!(deserialize_binary_array_str(&[1, 0]).is_err()); // < 4 header bytes
2047+
}
2048+
2049+
#[test]
2050+
fn binary_array_rejects_huge_length_prefix() {
2051+
// A 4-byte buffer whose length prefix decodes to i32::MAX must return an
2052+
// Err rather than eagerly reserving a huge Vec (capacity-overflow / OOM).
2053+
let huge = [0xFF, 0xFF, 0xFF, 0x7F];
2054+
assert!(deserialize_binary_array_str(&huge).is_err());
2055+
assert!(deserialize_binary_array_long(&huge).is_err());
2056+
}
2057+
2058+
#[test]
2059+
fn binary_array_long_rejects_all_null_amplification() {
2060+
// Forged input: a large element count with an all-ones null bitmap and
2061+
// NO element slots. Null elements skip the per-slot bounds check, so
2062+
// without an up-front `count * 8 <= remaining` guard the loop would
2063+
// push `count` `None`s from a tiny buffer (~128x memory amplification),
2064+
// reachable through the C entry point (OOM risk). Must error, not
2065+
// allocate.
2066+
let count: i32 = 8000;
2067+
let bitmap_len = (count as usize).div_ceil(8); // 1000 bytes
2068+
let mut buf = Vec::with_capacity(4 + bitmap_len);
2069+
buf.extend_from_slice(&count.to_le_bytes());
2070+
buf.extend(std::iter::repeat_n(0xFFu8, bitmap_len)); // every element null
2071+
assert!(deserialize_binary_array_long(&buf).is_err());
2072+
}
19202073
}

0 commit comments

Comments
 (0)