readstat/
rs_buffer_io.rs

1//! Buffer-based I/O handlers for parsing SAS files from in-memory byte slices.
2//!
3//! Provides [`ReadStatBufferCtx`] and a set of `extern "C"` callback functions that
4//! implement the `ReadStat` I/O interface over a `&[u8]` buffer instead of a file.
5//! This enables parsing `.sas7bdat` data without filesystem access — useful for
6//! WASM targets, cloud storage, HTTP uploads, and testing.
7
8#![allow(
9    clippy::cast_possible_wrap,
10    clippy::cast_possible_truncation,
11    clippy::cast_sign_loss,
12    clippy::cast_precision_loss,
13    clippy::ptr_as_ptr
14)]
15
16use std::os::raw::{c_char, c_int, c_long, c_void};
17use std::ptr;
18
19use crate::err::ReadStatError;
20use crate::rs_parser::ReadStatParser;
21
22/// In-memory buffer context for `ReadStat` I/O callbacks.
23///
24/// Wraps a borrowed byte slice and tracks the current read position.
25/// Passed as the `io_ctx` pointer to all I/O handler callbacks.
26#[repr(C)]
27pub struct ReadStatBufferCtx {
28    data: *const u8,
29    len: usize,
30    pos: usize,
31}
32
33impl ReadStatBufferCtx {
34    /// Creates a new buffer context from a byte slice.
35    ///
36    /// The caller must ensure the byte slice outlives the context and any
37    /// parsing operations that use it.
38    pub const fn new(bytes: &[u8]) -> Self {
39        Self {
40            data: bytes.as_ptr(),
41            len: bytes.len(),
42            pos: 0,
43        }
44    }
45
46    /// Configures a [`ReadStatParser`] to read from this buffer context
47    /// instead of from a file.
48    pub fn configure_parser(
49        &mut self,
50        parser: ReadStatParser,
51    ) -> Result<ReadStatParser, ReadStatError> {
52        let ctx_ptr = std::ptr::from_mut::<Self>(self) as *mut c_void;
53        parser
54            .set_open_handler(Some(buffer_open))
55            .and_then(|p| p.set_close_handler(Some(buffer_close)))
56            .and_then(|p| p.set_seek_handler(Some(buffer_seek)))
57            .and_then(|p| p.set_read_handler(Some(buffer_read)))
58            .and_then(|p| p.set_update_handler(Some(buffer_update)))
59            .and_then(|p| p.set_io_ctx(ctx_ptr))
60    }
61}
62
63/// No-op open handler — the buffer is already "open".
64unsafe extern "C" fn buffer_open(_path: *const c_char, _io_ctx: *mut c_void) -> c_int {
65    0
66}
67
68/// No-op close handler — nothing to close for an in-memory buffer.
69unsafe extern "C" fn buffer_close(_io_ctx: *mut c_void) -> c_int {
70    0
71}
72
73/// Seek handler that repositions the read cursor within the buffer.
74unsafe extern "C" fn buffer_seek(
75    offset: readstat_sys::readstat_off_t,
76    whence: readstat_sys::readstat_io_flags_t,
77    io_ctx: *mut c_void,
78) -> readstat_sys::readstat_off_t {
79    let ctx = unsafe { &mut *(io_ctx as *mut ReadStatBufferCtx) };
80
81    let newpos: i64 = match whence {
82        readstat_sys::readstat_io_flags_e_READSTAT_SEEK_SET => offset,
83        readstat_sys::readstat_io_flags_e_READSTAT_SEEK_CUR => ctx.pos as i64 + offset,
84        readstat_sys::readstat_io_flags_e_READSTAT_SEEK_END => ctx.len as i64 + offset,
85        _ => return -1,
86    };
87
88    if newpos < 0 || newpos > ctx.len as i64 {
89        return -1;
90    }
91
92    ctx.pos = newpos as usize;
93    newpos
94}
95
96/// Read handler that copies bytes from the buffer into the caller's buffer.
97unsafe extern "C" fn buffer_read(buf: *mut c_void, nbytes: usize, io_ctx: *mut c_void) -> isize {
98    let ctx = unsafe { &mut *(io_ctx as *mut ReadStatBufferCtx) };
99    let bytes_left = ctx.len.saturating_sub(ctx.pos);
100
101    let to_copy = if nbytes <= bytes_left {
102        nbytes
103    } else if bytes_left > 0 {
104        bytes_left
105    } else {
106        return 0;
107    };
108
109    unsafe {
110        ptr::copy_nonoverlapping(ctx.data.add(ctx.pos), buf as *mut u8, to_copy);
111    }
112    ctx.pos += to_copy;
113    to_copy as isize
114}
115
116/// Update/progress handler for buffer I/O.
117unsafe extern "C" fn buffer_update(
118    _file_size: c_long,
119    progress_handler: readstat_sys::readstat_progress_handler,
120    user_ctx: *mut c_void,
121    io_ctx: *mut c_void,
122) -> readstat_sys::readstat_error_t {
123    let Some(handler) = progress_handler else {
124        return readstat_sys::readstat_error_e_READSTAT_OK;
125    };
126
127    let ctx = unsafe { &*(io_ctx as *mut ReadStatBufferCtx) };
128    let progress = if ctx.len > 0 {
129        ctx.pos as f64 / ctx.len as f64
130    } else {
131        1.0
132    };
133
134    if unsafe { handler(progress, user_ctx) } != 0 {
135        return readstat_sys::readstat_error_e_READSTAT_ERROR_USER_ABORT;
136    }
137
138    readstat_sys::readstat_error_e_READSTAT_OK
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn buffer_ctx_new() {
147        let data = vec![1u8, 2, 3, 4, 5];
148        let ctx = ReadStatBufferCtx::new(&data);
149        assert_eq!(ctx.len, 5);
150        assert_eq!(ctx.pos, 0);
151        assert_eq!(ctx.data, data.as_ptr());
152    }
153
154    #[test]
155    fn buffer_ctx_empty() {
156        let data: Vec<u8> = vec![];
157        let ctx = ReadStatBufferCtx::new(&data);
158        assert_eq!(ctx.len, 0);
159        assert_eq!(ctx.pos, 0);
160    }
161}