Skip to main content

luaur_rt/
buffer.rs

1//! The [`Buffer`] handle (Luau `buffer` type). Mirrors `mlua::Buffer`.
2//!
3//! A buffer is a fixed-size, mutable, GC-managed byte array — Luau's answer to
4//! a `Vec<u8>` / `ArrayBuffer`. Like the other reference-typed handles
5//! ([`Table`](crate::Table), [`Function`](crate::Function)) it holds a registry
6//! reference ([`LuaRef`]) that keeps the underlying object alive and lets us
7//! re-push it onto the stack on demand.
8//!
9//! The data lives in VM-managed memory; [`Buffer::as_slice`] / [`as_slice_mut`]
10//! borrow the raw bytes directly through luaur's `lua_tobuffer`, so reads and
11//! writes go straight to the buffer with no copy.
12
13use std::io;
14
15use crate::error::Result;
16use crate::state::{Lua, LuaRef};
17use crate::sync::{NotSync, XRc, NOT_SYNC};
18use crate::sys::*;
19
20/// A Luau buffer type.
21///
22/// See the buffer [documentation] for more information.
23///
24/// Mirrors `mlua::Buffer`. Holds a registry reference keeping the buffer alive.
25///
26/// Under the `send` feature it is `Send` but never `Sync` — see
27/// [`crate::sync::NotSync`].
28///
29/// [documentation]: https://luau.org/library#buffer-library
30#[derive(Clone)]
31pub struct Buffer {
32    pub(crate) reference: XRc<LuaRef>,
33    pub(crate) _not_sync: NotSync,
34}
35
36impl Buffer {
37    pub(crate) fn from_ref(reference: LuaRef) -> Buffer {
38        Buffer {
39            reference: XRc::new(reference),
40            _not_sync: NOT_SYNC,
41        }
42    }
43
44    /// Push this buffer onto the owning state's stack.
45    pub(crate) unsafe fn push_to_stack(&self) {
46        self.reference.push();
47    }
48
49    /// The owning [`Lua`].
50    pub(crate) fn lua(&self) -> Lua {
51        self.reference.lua()
52    }
53
54    /// Copies the buffer data into a new `Vec<u8>`.
55    ///
56    /// Mirrors `mlua::Buffer::to_vec`.
57    pub fn to_vec(&self) -> Vec<u8> {
58        self.as_slice().to_vec()
59    }
60
61    /// Returns the length of the buffer.
62    ///
63    /// Mirrors `mlua::Buffer::len`.
64    pub fn len(&self) -> usize {
65        self.as_slice().len()
66    }
67
68    /// Returns `true` if the buffer is empty.
69    ///
70    /// Mirrors `mlua::Buffer::is_empty`.
71    pub fn is_empty(&self) -> bool {
72        self.len() == 0
73    }
74
75    /// Reads given number of bytes from the buffer at the given offset.
76    ///
77    /// Offset is 0-based.
78    ///
79    /// Mirrors `mlua::Buffer::read_bytes`.
80    #[track_caller]
81    pub fn read_bytes<const N: usize>(&self, offset: usize) -> [u8; N] {
82        let data = self.as_slice();
83        let mut bytes = [0u8; N];
84        bytes.copy_from_slice(&data[offset..offset + N]);
85        bytes
86    }
87
88    /// Writes given bytes to the buffer at the given offset.
89    ///
90    /// Offset is 0-based.
91    ///
92    /// Mirrors `mlua::Buffer::write_bytes`.
93    #[track_caller]
94    pub fn write_bytes(&self, offset: usize, bytes: &[u8]) {
95        let data = self.as_slice_mut();
96        data[offset..offset + bytes.len()].copy_from_slice(bytes);
97    }
98
99    /// Returns an adaptor implementing [`io::Read`], [`io::Write`] and
100    /// [`io::Seek`] over the buffer.
101    ///
102    /// Buffer operations are infallible, none of the read/write functions will
103    /// return an `Err`.
104    ///
105    /// Mirrors `mlua::Buffer::cursor`.
106    pub fn cursor(self) -> impl io::Read + io::Write + io::Seek {
107        BufferCursor(self, 0)
108    }
109
110    /// A raw pointer identifying this buffer (for identity comparison).
111    /// Mirrors `Value::Buffer(_).to_pointer()`.
112    pub(crate) fn to_pointer(&self) -> *const std::ffi::c_void {
113        let state = self.reference.state();
114        unsafe {
115            self.reference.push();
116            let p = lua_topointer(state, -1);
117            lua_pop(state, 1);
118            p
119        }
120    }
121
122    /// Borrow the buffer's bytes directly (no copy).
123    pub(crate) fn as_slice(&self) -> &[u8] {
124        unsafe {
125            let (buf, size) = self.as_raw_parts();
126            std::slice::from_raw_parts(buf, size)
127        }
128    }
129
130    /// Mutably borrow the buffer's bytes directly (no copy).
131    #[allow(clippy::mut_from_ref)]
132    pub(crate) fn as_slice_mut(&self) -> &mut [u8] {
133        unsafe {
134            let (buf, size) = self.as_raw_parts();
135            std::slice::from_raw_parts_mut(buf, size)
136        }
137    }
138
139    /// The raw `(ptr, len)` of the underlying buffer object via luaur's
140    /// `lua_tobuffer`. Pushes the buffer, reads the parts, then pops — the
141    /// pointer remains valid because the registry ref keeps the object alive.
142    unsafe fn as_raw_parts(&self) -> (*mut u8, usize) {
143        let state = self.reference.state();
144        unsafe {
145            self.reference.push();
146            let mut size = 0usize;
147            let buf = lua_tobuffer(state, -1, &mut size);
148            lua_pop(state, 1);
149            assert!(!buf.is_null(), "invalid Luau buffer");
150            (buf as *mut u8, size)
151        }
152    }
153}
154
155impl std::fmt::Debug for Buffer {
156    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157        // Mirror mlua: a buffer renders as its byte contents (a byte slice).
158        write!(f, "Buffer({:?})", self.as_slice())
159    }
160}
161
162impl PartialEq for Buffer {
163    fn eq(&self, other: &Self) -> bool {
164        // Reference (pointer) identity, matching mlua: two handles are equal iff
165        // they point at the *same* buffer object (NOT byte-wise content).
166        self.to_pointer() == other.to_pointer()
167    }
168}
169
170/// Cursor adapter returned by [`Buffer::cursor`]. The `usize` is the current
171/// 0-based offset into the buffer. Mirrors mlua's `BufferCursor`.
172struct BufferCursor(Buffer, usize);
173
174impl io::Read for BufferCursor {
175    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
176        let data = self.0.as_slice();
177        if self.1 == data.len() {
178            return Ok(0);
179        }
180        let len = buf.len().min(data.len() - self.1);
181        buf[..len].copy_from_slice(&data[self.1..self.1 + len]);
182        self.1 += len;
183        Ok(len)
184    }
185}
186
187impl io::Write for BufferCursor {
188    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
189        let data = self.0.as_slice_mut();
190        if self.1 == data.len() {
191            return Ok(0);
192        }
193        let len = buf.len().min(data.len() - self.1);
194        data[self.1..self.1 + len].copy_from_slice(&buf[..len]);
195        self.1 += len;
196        Ok(len)
197    }
198
199    fn flush(&mut self) -> io::Result<()> {
200        Ok(())
201    }
202}
203
204impl io::Seek for BufferCursor {
205    fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
206        let data = self.0.as_slice();
207        let new_offset = match pos {
208            io::SeekFrom::Start(offset) => offset as i64,
209            io::SeekFrom::End(offset) => data.len() as i64 + offset,
210            io::SeekFrom::Current(offset) => self.1 as i64 + offset,
211        };
212        if new_offset < 0 {
213            return Err(io::Error::new(
214                io::ErrorKind::InvalidInput,
215                "invalid seek to a negative position",
216            ));
217        }
218        if new_offset as usize > data.len() {
219            return Err(io::Error::new(
220                io::ErrorKind::InvalidInput,
221                "invalid seek to a position beyond the end of the buffer",
222            ));
223        }
224        self.1 = new_offset as usize;
225        Ok(self.1 as u64)
226    }
227}
228
229// ---------------------------------------------------------------------------
230// Buffer creation
231//
232// `lua_newbuffer` allocates a GC buffer and pushes it. When `size` exceeds the
233// VM's `MAX_BUFFER_SIZE` (1GB) the underlying `luaM_toobig` *raises* a Lua
234// error (longjmp) rather than returning. Unwinding that across Rust frames is
235// UB, so we run `lua_newbuffer` inside `lua_pcall` via a small C trampoline:
236// the requested size is passed as a number argument, and a raising allocation
237// is reported as an ordinary non-zero status with the error on the stack.
238// ---------------------------------------------------------------------------
239
240/// C trampoline: stack is `[size]` (a number). Allocates a buffer of that many
241/// bytes via `lua_newbuffer`, leaving the buffer object on top.
242unsafe fn c_newbuffer(state: *mut lua_State) -> c_int {
243    unsafe {
244        let size = lua_tonumberx(state, 1, core::ptr::null_mut()) as usize;
245        lua_settop(state, 0);
246        lua_newbuffer(state, size);
247        1
248    }
249}
250
251/// Create a buffer of `size` zero-initialized bytes, catching an over-limit
252/// allocation as an `Err` rather than letting the VM longjmp.
253pub(crate) fn create_buffer_with_capacity(lua: &Lua, size: usize) -> Result<Buffer> {
254    let state = lua.state();
255    unsafe {
256        lua_pushcclosurek(
257            state,
258            Some(c_newbuffer),
259            c"luaur-rt-newbuffer".as_ptr(),
260            0,
261            None,
262        );
263        lua_pushnumber(state, size as f64);
264        let status = lua_pcall(state, 1, 1, 0);
265        if status != 0 {
266            return Err(lua.pop_error(status));
267        }
268        Ok(Buffer::from_ref(lua.pop_ref()))
269    }
270}