use crate::{
Date, Error, OpenOptions, Time, Timestamp, XlColumn, XlColumnSpec, XlInferredSchema,
XlOpenOptions, XlTable, XlWorkbook, XL_BUFFER_TOO_SMALL, XL_ERROR, XL_FORMAT_AUTO, XL_OK,
XL_T_BOOL, XL_T_DATE, XL_T_F64, XL_T_I64, XL_T_STRING, XL_T_TIME, XL_T_TIMESTAMP,
};
use std::marker::PhantomData;
fn last_error(code: i32) -> Error {
unsafe {
let mut len: i32 = 0;
let ptr = crate::xl_last_error_ptr(&mut len);
let message = if ptr.is_null() || len <= 0 {
"unknown error".to_string()
} else {
let bytes = std::slice::from_raw_parts(ptr, len as usize);
String::from_utf8_lossy(bytes).into_owned()
};
Error { code, message }
}
}
fn check(code: i32) -> Result<(), Error> {
if code == XL_OK {
Ok(())
} else {
Err(last_error(code))
}
}
fn check_abi_version() -> Result<(), Error> {
use std::sync::OnceLock;
static CHECKED: OnceLock<Result<(), Error>> = OnceLock::new();
CHECKED
.get_or_init(|| {
let loaded = unsafe { crate::xl_abi_version() };
if loaded == crate::XL_ABI_VERSION {
Ok(())
} else {
Err(Error {
code: XL_ERROR,
message: format!(
"ExcelReader native library reports ABI version {loaded}, but this crate \
was built against {}. Update the crate and the native library together.",
crate::XL_ABI_VERSION
),
})
}
})
.clone()
}
pub struct Workbook {
handle: *mut XlWorkbook,
}
impl Workbook {
pub fn open(path: &str) -> Result<Workbook, Error> {
Self::open_with(path, XL_FORMAT_AUTO, None)
}
pub fn open_with(
path: &str,
format: i32,
options: Option<&OpenOptions>,
) -> Result<Workbook, Error> {
check_abi_version()?;
let raw = options.map(OpenOptions::to_raw);
let raw_ptr = raw
.as_ref()
.map_or(std::ptr::null(), |o| o as *const XlOpenOptions);
let mut handle: *mut XlWorkbook = std::ptr::null_mut();
let status = unsafe {
crate::xl_open_file_ex(
path.as_ptr(),
path.len() as i32,
format,
raw_ptr,
&mut handle,
)
};
check(status)?;
Ok(Workbook { handle })
}
pub fn open_memory(
data: &[u8],
format: i32,
options: Option<&OpenOptions>,
) -> Result<Workbook, Error> {
check_abi_version()?;
let raw = options.map(OpenOptions::to_raw);
let raw_ptr = raw
.as_ref()
.map_or(std::ptr::null(), |o| o as *const XlOpenOptions);
let mut handle: *mut XlWorkbook = std::ptr::null_mut();
let status = unsafe {
crate::xl_open_memory_ex(
data.as_ptr(),
data.len() as i32,
format,
raw_ptr,
&mut handle,
)
};
check(status)?;
Ok(Workbook { handle })
}
pub fn sheet_count(&self) -> Result<i32, Error> {
let mut count: i32 = 0;
check(unsafe { crate::xl_sheet_count(self.handle, &mut count) })?;
Ok(count)
}
pub fn sheet_name(&self) -> Result<String, Error> {
self.fill_string(|handle, buffer, capacity, out_len| unsafe {
crate::xl_sheet_name(handle, buffer, capacity, out_len)
})
}
pub fn sheet_name_at(&self, index: i32) -> Result<String, Error> {
self.fill_string(|handle, buffer, capacity, out_len| unsafe {
crate::xl_sheet_name_at(handle, index, buffer, capacity, out_len)
})
}
pub fn sheet_names(&self) -> Result<Vec<String>, Error> {
(0..self.sheet_count()?)
.map(|index| self.sheet_name_at(index))
.collect()
}
pub fn move_to_sheet(&mut self, index: i32) -> Result<(), Error> {
check(unsafe { crate::xl_move_to_sheet(self.handle, index) })
}
pub fn is_date1904(&self) -> Result<bool, Error> {
let mut flag: i32 = 0;
check(unsafe { crate::xl_is_date1904(self.handle, &mut flag) })?;
Ok(flag != 0)
}
pub fn infer_schema(
&self,
header_row: i32,
sample_size: i32,
) -> Result<Vec<InferredColumn>, Error> {
let mut schema = XlInferredSchema {
columns: std::ptr::null_mut(),
column_count: 0,
};
check(unsafe {
crate::xl_infer_schema(self.handle, header_row, sample_size, &mut schema)
})?;
let columns = unsafe { copy_inferred(&schema) };
unsafe { crate::xl_free_schema(&mut schema) };
Ok(columns)
}
fn fill_string(
&self,
call: impl Fn(*mut XlWorkbook, *mut u8, i32, *mut i32) -> i32,
) -> Result<String, Error> {
let mut buffer = vec![0u8; 128];
let mut len: i32 = 0;
let mut status = call(
self.handle,
buffer.as_mut_ptr(),
buffer.len() as i32,
&mut len,
);
if status == XL_BUFFER_TOO_SMALL {
buffer = vec![0u8; len.max(0) as usize];
status = call(
self.handle,
buffer.as_mut_ptr(),
buffer.len() as i32,
&mut len,
);
}
check(status)?;
buffer.truncate(len.max(0) as usize);
String::from_utf8(buffer).map_err(|e| Error {
code: XL_ERROR,
message: format!("native library returned a non-UTF-8 name: {e}"),
})
}
}
unsafe fn copy_inferred(schema: &XlInferredSchema) -> Vec<InferredColumn> {
if schema.columns.is_null() || schema.column_count <= 0 {
return Vec::new();
}
let specs = std::slice::from_raw_parts(schema.columns, schema.column_count as usize);
specs
.iter()
.map(|spec| InferredColumn {
name: if spec.name_count <= 0 || spec.names.is_null() || spec.name_lens.is_null() {
None
} else {
let name_ptr = *spec.names;
let name_len = *spec.name_lens;
if name_ptr.is_null() || name_len <= 0 {
None
} else {
let bytes = std::slice::from_raw_parts(name_ptr, name_len as usize);
Some(String::from_utf8_lossy(bytes).into_owned())
}
},
index: spec.index,
column_type: spec.r#type,
nullable: spec.nullable != 0,
})
.collect()
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct InferredColumn {
pub name: Option<String>,
pub index: i32,
pub column_type: i32,
pub nullable: bool,
}
impl Drop for Workbook {
fn drop(&mut self) {
if !self.handle.is_null() {
unsafe {
crate::xl_close(self.handle);
}
self.handle = std::ptr::null_mut();
}
}
}
impl std::fmt::Debug for Workbook {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Workbook")
.field("open", &!self.handle.is_null())
.finish()
}
}
pub struct ColumnBinding<T> {
pub names: &'static [&'static str],
pub xl_type: i32,
pub assign: fn(&mut T, &XlColumn, i64),
}
pub trait ExcelMapper: Sized {
fn bindings() -> Vec<ColumnBinding<Self>>;
}
pub use excelreader_derive::ExcelMapper;
fn is_valid(col: &XlColumn, row: i64) -> bool {
if col.validity.is_null() {
return true;
}
unsafe {
let byte = *col.validity.offset((row / 8) as isize);
(byte & (1 << (row % 8))) != 0
}
}
#[inline]
fn check_access(col: &XlColumn, row: i64, expected_type: i32, accessor: &str) {
assert_eq!(
col.r#type, expected_type,
"{accessor} called on a column of type {}",
col.r#type
);
assert!(
row >= 0 && row < col.length,
"{accessor}: row {row} is out of bounds for a column of length {}",
col.length
);
}
pub fn column_str(col: &XlColumn, row: i64) -> &str {
check_access(col, row, XL_T_STRING, "column_str");
unsafe {
let offsets = col.values as *const i32;
let start = *offsets.offset(row as isize);
let end = *offsets.offset(row as isize + 1);
let bytes =
std::slice::from_raw_parts(col.data.offset(start as isize), (end - start) as usize);
std::str::from_utf8(bytes)
.expect("native library returned a non-UTF-8 string, violating the ABI contract")
}
}
pub fn column_i64(col: &XlColumn, row: i64) -> i64 {
check_access(col, row, XL_T_I64, "column_i64");
unsafe { *(col.values as *const i64).offset(row as isize) }
}
pub fn column_f64(col: &XlColumn, row: i64) -> f64 {
check_access(col, row, XL_T_F64, "column_f64");
unsafe { *(col.values as *const f64).offset(row as isize) }
}
pub fn column_bool(col: &XlColumn, row: i64) -> bool {
check_access(col, row, XL_T_BOOL, "column_bool");
unsafe { *(col.values as *const u8).offset(row as isize) != 0 }
}
pub fn column_date(col: &XlColumn, row: i64) -> Date {
check_access(col, row, XL_T_DATE, "column_date");
Date::new(unsafe { *(col.values as *const i32).offset(row as isize) })
}
pub fn column_time(col: &XlColumn, row: i64) -> Time {
check_access(col, row, XL_T_TIME, "column_time");
Time::new(unsafe { *(col.values as *const i64).offset(row as isize) })
}
pub fn column_timestamp(col: &XlColumn, row: i64) -> Timestamp {
check_access(col, row, XL_T_TIMESTAMP, "column_timestamp");
Timestamp::new(unsafe { *(col.values as *const i64).offset(row as isize) })
}
pub struct TableView<T: ExcelMapper> {
table: XlTable,
bindings: Vec<ColumnBinding<T>>,
_marker: PhantomData<T>,
}
impl<T: ExcelMapper> TableView<T> {
pub fn len(&self) -> i64 {
self.table.row_count
}
pub fn is_empty(&self) -> bool {
self.table.row_count == 0
}
pub fn get(&self, row: i64) -> Option<T>
where
T: Default,
{
if row < 0 || row >= self.len() {
return None;
}
let mut instance = T::default();
let columns = unsafe {
std::slice::from_raw_parts(self.table.columns, self.table.column_count as usize)
};
for (col, binding) in columns.iter().zip(self.bindings.iter()) {
if is_valid(col, row) {
(binding.assign)(&mut instance, col, row);
}
}
Some(instance)
}
pub fn iter(&self) -> TableViewIter<'_, T>
where
T: Default,
{
TableViewIter { view: self, row: 0 }
}
}
impl<T: ExcelMapper> Drop for TableView<T> {
fn drop(&mut self) {
unsafe {
crate::xl_free_table(&mut self.table);
}
}
}
impl<T: ExcelMapper> std::fmt::Debug for TableView<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TableView")
.field("rows", &self.table.row_count)
.field("columns", &self.table.column_count)
.finish_non_exhaustive()
}
}
pub struct TableViewIter<'a, T: ExcelMapper> {
view: &'a TableView<T>,
row: i64,
}
impl<T: ExcelMapper + Default> Iterator for TableViewIter<'_, T> {
type Item = T;
fn next(&mut self) -> Option<T> {
let item = self.view.get(self.row)?;
self.row += 1;
Some(item)
}
fn size_hint(&self) -> (usize, Option<usize>) {
let remaining = (self.view.len() - self.row).max(0) as usize;
(remaining, Some(remaining))
}
}
impl<T: ExcelMapper + Default> ExactSizeIterator for TableViewIter<'_, T> {}
pub fn parse_sheet<T: ExcelMapper>(
workbook: &mut Workbook,
header_row: i32,
) -> Result<TableView<T>, Error> {
let bindings = T::bindings();
let name_ptrs: Vec<Vec<*const u8>> = bindings
.iter()
.map(|b| b.names.iter().map(|n| n.as_ptr()).collect())
.collect();
let name_lens: Vec<Vec<i32>> = bindings
.iter()
.map(|b| b.names.iter().map(|n| n.len() as i32).collect())
.collect();
let specs: Vec<XlColumnSpec> = bindings
.iter()
.enumerate()
.map(|(i, b)| XlColumnSpec {
names: name_ptrs[i].as_ptr(),
name_lens: name_lens[i].as_ptr(),
name_count: b.names.len() as i32,
index: 0,
r#type: b.xl_type,
nullable: 1,
})
.collect();
let mut table = XlTable {
column_count: 0,
row_count: 0,
columns: std::ptr::null_mut(),
};
unsafe {
let status = crate::xl_parse_typed(
workbook.handle,
specs.as_ptr(),
specs.len() as i32,
header_row,
&mut table,
);
if status != XL_OK {
return Err(last_error(status));
}
}
if table.column_count as usize != bindings.len() {
let column_count = table.column_count;
unsafe { crate::xl_free_table(&mut table) };
return Err(Error {
code: XL_ERROR,
message: format!(
"xl_parse_typed returned {column_count} columns for {} specs",
bindings.len()
),
});
}
Ok(TableView {
table,
bindings,
_marker: PhantomData,
})
}