use crate::{
rows::RowRef, workbook, Error, XlCsvAggregation, XlCsvParallelOptions, XlRow, XL_OK,
};
use std::any::Any;
use std::ffi::c_void;
use std::panic::{catch_unwind, resume_unwind, AssertUnwindSafe};
use std::path::Path;
use std::sync::Mutex;
const PANIC_STATUS: i32 = i32::MAX;
#[repr(C)]
struct State<A> {
value: A,
_nonzero: u8,
}
pub trait CsvAccumulator: Send {
fn accumulate(&mut self, row: RowRef<'_>) -> Result<(), i32>;
fn combine(&mut self, other: &mut Self) -> Result<(), i32>;
}
#[derive(Clone, Copy, Debug, Default)]
pub struct CsvParallelOptions {
pub degree_of_parallelism: i32,
pub header_row: i32,
pub delimiter: i32,
pub quote: i32,
pub detect_bom: i32,
pub max_cell_bytes: i32,
}
impl CsvParallelOptions {
fn to_raw(self) -> XlCsvParallelOptions {
XlCsvParallelOptions {
struct_size: std::mem::size_of::<XlCsvParallelOptions>() as i32,
degree_of_parallelism: self.degree_of_parallelism,
header_row: self.header_row,
delimiter: self.delimiter,
quote: self.quote,
detect_bom: self.detect_bom,
max_cell_bytes: self.max_cell_bytes,
}
}
}
struct Shared<'a, A, F> {
seed: &'a F,
panic: Mutex<Option<Box<dyn Any + Send>>>,
_marker: std::marker::PhantomData<fn() -> A>,
}
impl<A, F> Shared<'_, A, F> {
fn store_panic(&self, payload: Box<dyn Any + Send>) -> i32 {
let mut slot = self.panic.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
if slot.is_none() {
*slot = Some(payload);
}
PANIC_STATUS
}
}
unsafe extern "C" fn seed_shim<A, F>(out_state: *mut *mut c_void, user_data: *mut c_void) -> i32
where
A: CsvAccumulator,
F: Fn() -> A,
{
let shared = unsafe { &*(user_data as *const Shared<'_, A, F>) };
unsafe { *out_state = std::ptr::null_mut() };
match catch_unwind(AssertUnwindSafe(|| (shared.seed)())) {
Ok(accumulator) => {
unsafe {
*out_state = Box::into_raw(Box::new(State { value: accumulator, _nonzero: 0 })) as *mut c_void
};
XL_OK
}
Err(payload) => shared.store_panic(payload),
}
}
unsafe extern "C" fn accumulate_shim<A, F>(
state: *mut c_void,
row: *const XlRow,
user_data: *mut c_void,
) -> i32
where
A: CsvAccumulator,
F: Fn() -> A,
{
let shared = unsafe { &*(user_data as *const Shared<'_, A, F>) };
let accumulator = unsafe { &mut (*(state as *mut State<A>)).value };
let row = unsafe { &*row };
match catch_unwind(AssertUnwindSafe(|| {
accumulator.accumulate(unsafe { RowRef::from_decoded(row.cells, row.cell_count) })
})) {
Ok(Ok(())) => XL_OK,
Ok(Err(code)) => code,
Err(payload) => shared.store_panic(payload),
}
}
unsafe extern "C" fn combine_shim<A, F>(
acc: *mut c_void,
next: *mut c_void,
user_data: *mut c_void,
) -> i32
where
A: CsvAccumulator,
F: Fn() -> A,
{
let shared = unsafe { &*(user_data as *const Shared<'_, A, F>) };
let accumulator = unsafe { &mut (*(acc as *mut State<A>)).value };
let other = unsafe { &mut (*(next as *mut State<A>)).value };
match catch_unwind(AssertUnwindSafe(|| accumulator.combine(other))) {
Ok(Ok(())) => XL_OK,
Ok(Err(code)) => code,
Err(payload) => shared.store_panic(payload),
}
}
unsafe extern "C" fn free_state_shim<A, F>(state: *mut c_void, user_data: *mut c_void)
where
A: CsvAccumulator,
F: Fn() -> A,
{
let shared = unsafe { &*(user_data as *const Shared<'_, A, F>) };
if let Err(payload) =
catch_unwind(AssertUnwindSafe(|| drop(unsafe { Box::from_raw(state as *mut State<A>) })))
{
shared.store_panic(payload);
}
}
fn raw_aggregation<A, F>(shared: &Shared<'_, A, F>) -> XlCsvAggregation
where
A: CsvAccumulator,
F: Fn() -> A,
{
XlCsvAggregation {
struct_size: std::mem::size_of::<XlCsvAggregation>() as i32,
seed: Some(seed_shim::<A, F>),
accumulate: Some(accumulate_shim::<A, F>),
combine: Some(combine_shim::<A, F>),
free_state: Some(free_state_shim::<A, F>),
user_data: shared as *const Shared<'_, A, F> as *mut c_void,
}
}
fn shared<A, F>(seed: &F) -> Shared<'_, A, F> {
Shared { seed, panic: Mutex::new(None), _marker: std::marker::PhantomData }
}
fn length(len: usize, what: &str) -> Result<i32, Error> {
i32::try_from(len).map_err(|_| {
Error::from_status(
crate::XL_INVALID_ARGUMENT,
format!("{what} is {len} bytes, past the ABI's int32 length limit"),
)
})
}
pub fn aggregate_csv_file<A, F>(
path: &Path,
seed: F,
options: &CsvParallelOptions,
) -> Result<A, Error>
where
A: CsvAccumulator,
F: Fn() -> A + Sync,
{
workbook::check_abi_version()?;
let bytes = path
.to_str()
.ok_or_else(|| {
Error::from_status(
crate::XL_INVALID_ARGUMENT,
format!("path {} is not valid UTF-8", path.display()),
)
})?
.as_bytes();
let path_len = length(bytes.len(), "path")?;
let shared = shared::<A, F>(&seed);
let agg = raw_aggregation(&shared);
let raw_options = options.to_raw();
let mut state: *mut c_void = std::ptr::null_mut();
let status = unsafe {
crate::xl_csv_aggregate_file(
bytes.as_ptr(),
path_len,
&agg,
&raw_options,
&mut state,
)
};
finish(shared, status, state)
}
pub fn aggregate_csv_memory<A, F>(
data: &[u8],
seed: F,
options: &CsvParallelOptions,
) -> Result<A, Error>
where
A: CsvAccumulator,
F: Fn() -> A + Sync,
{
workbook::check_abi_version()?;
aggregate_csv_memory_unchecked(data, seed, options)
}
pub(crate) fn aggregate_csv_memory_unchecked<A, F>(
data: &[u8],
seed: F,
options: &CsvParallelOptions,
) -> Result<A, Error>
where
A: CsvAccumulator,
F: Fn() -> A + Sync,
{
let data_len = length(data.len(), "buffer")?;
let shared = shared::<A, F>(&seed);
let agg = raw_aggregation(&shared);
let raw_options = options.to_raw();
let mut state: *mut c_void = std::ptr::null_mut();
let status = unsafe {
crate::xl_csv_aggregate_memory(
data.as_ptr(),
data_len,
&agg,
&raw_options,
&mut state,
)
};
finish(shared, status, state)
}
fn finish<A, F>(shared: Shared<'_, A, F>, status: i32, state: *mut c_void) -> Result<A, Error>
where
A: CsvAccumulator,
F: Fn() -> A,
{
let winner = if state.is_null() {
None
} else {
Some(unsafe { Box::from_raw(state as *mut State<A>) })
};
if status == PANIC_STATUS {
if let Some(payload) =
shared.panic.into_inner().unwrap_or_else(|poisoned| poisoned.into_inner())
{
drop(winner);
resume_unwind(payload);
}
}
if status != XL_OK {
return Err(status_error(status));
}
winner.map(|boxed| boxed.value).ok_or_else(|| {
Error::from_status(
crate::XL_ERROR,
"native reported success but wrote no aggregation state".to_string(),
)
})
}
fn status_error(status: i32) -> Error {
if status > 0 {
Error::from_status(status, format!("an aggregation callback returned status {status}"))
} else {
workbook::last_error(status)
}
}