Skip to main content

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::marker::PhantomData;
17use std::os::raw::{c_char, c_int, c_long, c_void};
18use std::ptr;
19
20use crate::cb::catch_callback;
21use crate::err::ReadStatError;
22use crate::rs_parser::ReadStatParser;
23
24/// In-memory buffer context for `ReadStat` I/O callbacks.
25///
26/// Wraps a borrowed byte slice and tracks the current read position.
27/// Passed as the `io_ctx` pointer to all I/O handler callbacks.
28///
29/// The `'a` lifetime ties the context to the borrowed byte slice so the
30/// compiler enforces that the buffer outlives the context (and therefore any
31/// parse driven through it). The trailing `PhantomData` carries that lifetime
32/// without changing the C-visible layout — it is zero-sized, so the `#[repr(C)]`
33/// field order seen from C (`data`, `len`, `pos`) is unaffected.
34#[repr(C)]
35pub struct ReadStatBufferCtx<'a> {
36    data: *const u8,
37    len: usize,
38    pos: usize,
39    _marker: PhantomData<&'a [u8]>,
40}
41
42impl<'a> ReadStatBufferCtx<'a> {
43    /// Creates a new buffer context from a byte slice.
44    ///
45    /// The returned context borrows `bytes`; the borrow checker guarantees the
46    /// slice outlives the context and any parsing operations that use it.
47    pub const fn new(bytes: &'a [u8]) -> Self {
48        Self {
49            data: bytes.as_ptr(),
50            len: bytes.len(),
51            pos: 0,
52            _marker: PhantomData,
53        }
54    }
55
56    /// Configures a [`ReadStatParser`] to read from this buffer context
57    /// instead of from a file.
58    pub fn configure_parser(
59        &mut self,
60        parser: ReadStatParser,
61    ) -> Result<ReadStatParser, ReadStatError> {
62        let ctx_ptr = std::ptr::from_mut::<Self>(self) as *mut c_void;
63        parser
64            .set_open_handler(Some(buffer_open))
65            .and_then(|p| p.set_close_handler(Some(buffer_close)))
66            .and_then(|p| p.set_seek_handler(Some(buffer_seek)))
67            .and_then(|p| p.set_read_handler(Some(buffer_read)))
68            .and_then(|p| p.set_update_handler(Some(buffer_update)))
69            .and_then(|p| p.set_io_ctx(ctx_ptr))
70    }
71}
72
73/// No-op open handler — the buffer is already "open".
74unsafe extern "C" fn buffer_open(_path: *const c_char, _io_ctx: *mut c_void) -> c_int {
75    catch_callback(-1, || 0)
76}
77
78/// No-op close handler — nothing to close for an in-memory buffer.
79unsafe extern "C" fn buffer_close(_io_ctx: *mut c_void) -> c_int {
80    catch_callback(-1, || 0)
81}
82
83/// Seek handler that repositions the read cursor within the buffer.
84unsafe extern "C" fn buffer_seek(
85    offset: readstat_sys::readstat_off_t,
86    whence: readstat_sys::readstat_io_flags_t,
87    io_ctx: *mut c_void,
88) -> readstat_sys::readstat_off_t {
89    catch_callback(-1, || unsafe { buffer_seek_inner(offset, whence, io_ctx) })
90}
91
92unsafe fn buffer_seek_inner(
93    offset: readstat_sys::readstat_off_t,
94    whence: readstat_sys::readstat_io_flags_t,
95    io_ctx: *mut c_void,
96) -> readstat_sys::readstat_off_t {
97    let ctx = unsafe { &mut *(io_ctx as *mut ReadStatBufferCtx<'_>) };
98
99    // Use checked addition: `offset` is attacker-influenced, so an unchecked
100    // `i64 + i64` could overflow (a debug build would panic inside this
101    // `extern "C"` boundary, which aborts the process). On overflow, fail the
102    // seek the same way an out-of-range position does.
103    let newpos: i64 = match whence {
104        readstat_sys::readstat_io_flags_e_READSTAT_SEEK_SET => offset,
105        readstat_sys::readstat_io_flags_e_READSTAT_SEEK_CUR => {
106            match i64::try_from(ctx.pos)
107                .ok()
108                .and_then(|p| p.checked_add(offset))
109            {
110                Some(n) => n,
111                None => return -1,
112            }
113        }
114        readstat_sys::readstat_io_flags_e_READSTAT_SEEK_END => {
115            match i64::try_from(ctx.len)
116                .ok()
117                .and_then(|l| l.checked_add(offset))
118            {
119                Some(n) => n,
120                None => return -1,
121            }
122        }
123        _ => return -1,
124    };
125
126    if newpos < 0 || newpos > ctx.len as i64 {
127        return -1;
128    }
129
130    ctx.pos = newpos as usize;
131    newpos
132}
133
134/// Read handler that copies bytes from the buffer into the caller's buffer.
135unsafe extern "C" fn buffer_read(buf: *mut c_void, nbytes: usize, io_ctx: *mut c_void) -> isize {
136    catch_callback(-1, || unsafe { buffer_read_inner(buf, nbytes, io_ctx) })
137}
138
139unsafe fn buffer_read_inner(buf: *mut c_void, nbytes: usize, io_ctx: *mut c_void) -> isize {
140    let ctx = unsafe { &mut *(io_ctx as *mut ReadStatBufferCtx<'_>) };
141    let bytes_left = ctx.len.saturating_sub(ctx.pos);
142
143    let to_copy = if nbytes <= bytes_left {
144        nbytes
145    } else if bytes_left > 0 {
146        bytes_left
147    } else {
148        return 0;
149    };
150
151    unsafe {
152        ptr::copy_nonoverlapping(ctx.data.add(ctx.pos), buf as *mut u8, to_copy);
153    }
154    ctx.pos += to_copy;
155    to_copy as isize
156}
157
158/// Update/progress handler for buffer I/O.
159unsafe extern "C" fn buffer_update(
160    _file_size: c_long,
161    progress_handler: readstat_sys::readstat_progress_handler,
162    user_ctx: *mut c_void,
163    io_ctx: *mut c_void,
164) -> readstat_sys::readstat_error_t {
165    catch_callback(
166        readstat_sys::readstat_error_e_READSTAT_ERROR_USER_ABORT,
167        || unsafe { buffer_update_inner(_file_size, progress_handler, user_ctx, io_ctx) },
168    )
169}
170
171unsafe fn buffer_update_inner(
172    _file_size: c_long,
173    progress_handler: readstat_sys::readstat_progress_handler,
174    user_ctx: *mut c_void,
175    io_ctx: *mut c_void,
176) -> readstat_sys::readstat_error_t {
177    let Some(handler) = progress_handler else {
178        return readstat_sys::readstat_error_e_READSTAT_OK;
179    };
180
181    let ctx = unsafe { &*(io_ctx as *mut ReadStatBufferCtx<'_>) };
182    let progress = if ctx.len > 0 {
183        ctx.pos as f64 / ctx.len as f64
184    } else {
185        1.0
186    };
187
188    if unsafe { handler(progress, user_ctx) } != 0 {
189        return readstat_sys::readstat_error_e_READSTAT_ERROR_USER_ABORT;
190    }
191
192    readstat_sys::readstat_error_e_READSTAT_OK
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    #[test]
200    fn buffer_ctx_new() {
201        let data = vec![1u8, 2, 3, 4, 5];
202        let ctx = ReadStatBufferCtx::new(&data);
203        assert_eq!(ctx.len, 5);
204        assert_eq!(ctx.pos, 0);
205        assert_eq!(ctx.data, data.as_ptr());
206    }
207
208    #[test]
209    fn buffer_ctx_empty() {
210        let data: Vec<u8> = vec![];
211        let ctx = ReadStatBufferCtx::new(&data);
212        assert_eq!(ctx.len, 0);
213        assert_eq!(ctx.pos, 0);
214    }
215}