Skip to main content

samp_sdk/cell/
buffer.rs

1//! AMX cell vectors (Pawn arrays) — `Buffer` (sized) and
2//! `UnsizedBuffer` (unsized, received as a native argument).
3
4use std::ops::{Deref, DerefMut};
5
6use super::{AmxCell, Ref};
7use crate::amx::Amx;
8use crate::cell::repr::CellConvert;
9use crate::cell::string;
10use crate::error::AmxResult;
11
12/// AMX cell array with a known size.
13///
14/// Implements [`Deref<Target = [i32]>`], so the full `&[i32]` API is
15/// available (`iter`, `len`, indexing, etc). For non-`i32` types (`f32`,
16/// `bool`, etc), use [`iter_as`], [`get_as`], [`set_as`].
17///
18/// # Example
19/// ```
20/// use samp_sdk::cell::{UnsizedBuffer, Buffer};
21/// # use samp_sdk::amx::Amx;
22/// fn double_all(amx: &Amx, buffer: UnsizedBuffer, size: usize) {
23///     let mut buffer: Buffer = buffer.into_sized_buffer(size);
24///     buffer.iter_mut().for_each(|cell| *cell *= 2);
25/// }
26/// ```
27///
28/// [`iter_as`]: Buffer::iter_as
29/// [`get_as`]: Buffer::get_as
30/// [`set_as`]: Buffer::set_as
31pub struct Buffer<'amx> {
32    inner: Ref<'amx, i32>,
33    len: usize,
34}
35
36impl<'amx> Buffer<'amx> {
37    /// Builds a `Buffer` from the `Ref` to the first cell and its size.
38    #[must_use]
39    pub fn new(reference: Ref<'amx, i32>, len: usize) -> Buffer<'amx> {
40        Buffer {
41            inner: reference,
42            len,
43        }
44    }
45
46    /// Number of cells in the buffer.
47    #[must_use]
48    pub fn len(&self) -> usize {
49        self.len
50    }
51
52    /// `true` if the buffer has no cells.
53    #[must_use]
54    pub fn is_empty(&self) -> bool {
55        self.len == 0
56    }
57
58    /// Read-only slice covering every cell.
59    #[inline]
60    #[must_use]
61    pub fn as_slice(&self) -> &[i32] {
62        unsafe { std::slice::from_raw_parts(self.inner.as_ptr(), self.len) }
63    }
64
65    /// Mutable slice covering every cell.
66    #[inline]
67    pub fn as_mut_slice(&mut self) -> &mut [i32] {
68        unsafe { std::slice::from_raw_parts_mut(self.inner.as_mut_ptr(), self.len) }
69    }
70
71    /// Iterator that converts each cell to `T` via [`CellConvert`].
72    ///
73    /// Idiomatic ergonomics for arrays of `f32`, `bool` etc. — combine with
74    /// iterator adapters (`sum`, `filter_map`, ...).
75    ///
76    /// ```rust,no_run
77    /// # use samp_sdk::cell::Buffer;
78    /// fn sum_floats(buf: &Buffer) -> f32 { buf.iter_as::<f32>().sum() }
79    /// ```
80    pub fn iter_as<T: CellConvert>(&self) -> impl Iterator<Item = T> + '_ {
81        self.as_slice().iter().map(|&raw| T::from_cell(raw))
82    }
83
84    /// Reads the cell at `index`, converting to `T`. `None` if out of bounds.
85    #[must_use]
86    pub fn get_as<T: CellConvert>(&self, index: usize) -> Option<T> {
87        self.as_slice().get(index).map(|&raw| T::from_cell(raw))
88    }
89
90    /// Converts `value` to a raw cell and writes it at `index`.
91    ///
92    /// Returns `true` if the write happened, `false` if `index` was out of bounds.
93    pub fn set_as<T: CellConvert>(&mut self, index: usize, value: T) -> bool {
94        if let Some(cell) = self.as_mut_slice().get_mut(index) {
95            *cell = value.into_cell();
96            true
97        } else {
98            false
99        }
100    }
101
102    /// Writes a Rust string into the buffer (unpacked format, `0` terminator).
103    ///
104    /// Requires `s.len() + 1` cells of space.
105    ///
106    /// # Errors
107    /// `AmxError::General` if the encoded string is >= the buffer size.
108    pub fn write_str(&mut self, s: &str) -> AmxResult<()> {
109        string::put_in_buffer(self, s)
110    }
111}
112
113// `Buffer` cannot be parsed directly from a cell — use `UnsizedBuffer`
114// as the native argument and then `.into_sized_buffer(len)`.
115impl<'amx> AmxCell<'amx> for Buffer<'amx> {
116    #[inline]
117    fn as_cell(&self) -> i32 {
118        self.inner.as_cell()
119    }
120}
121
122impl Deref for Buffer<'_> {
123    type Target = [i32];
124
125    fn deref(&self) -> &[i32] {
126        self.as_slice()
127    }
128}
129
130impl DerefMut for Buffer<'_> {
131    fn deref_mut(&mut self) -> &mut [i32] {
132        self.as_mut_slice()
133    }
134}
135
136impl std::fmt::Debug for Buffer<'_> {
137    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
138        write!(f, "{:?}", self.as_slice())
139    }
140}
141
142/// Array with unknown size — received as a native argument when the
143/// Pawn signature is `array[]` without a fixed dimension.
144///
145/// The actual size usually comes as another parameter (`sizeof(array)`). Use
146/// [`into_sized_buffer`] to convert into [`Buffer`] before iterating.
147///
148/// [`into_sized_buffer`]: UnsizedBuffer::into_sized_buffer
149pub struct UnsizedBuffer<'amx> {
150    inner: Ref<'amx, i32>,
151    /// Upper bound on cells reachable from the first cell without leaving the
152    /// VM's data region `[0, stp)`. Computed from `amx_StrLen`-independent VM
153    /// state at parse time and used to clamp [`into_sized_buffer`]. `usize::MAX`
154    /// when unknown (test constructor).
155    ///
156    /// [`into_sized_buffer`]: UnsizedBuffer::into_sized_buffer
157    max_cells: usize,
158}
159
160impl<'amx> UnsizedBuffer<'amx> {
161    /// Converts into `Buffer` by declaring the size.
162    ///
163    /// `len` should be the real Pawn array size (`sizeof(arr)`). A `len` larger
164    /// than the actual array still reads/writes neighbouring cells of the same
165    /// script (wrong data, not a crash), so never pass a size the script can
166    /// control independently. The SDK clamps `len` two ways as a safety net:
167    /// to the VM data region `[0, stp)` (so an oversized `len` cannot read or
168    /// write past the AMX allocation and segfault) and to a 1 MiB ceiling.
169    #[must_use]
170    pub fn into_sized_buffer(self, len: usize) -> Buffer<'amx> {
171        const MAX_BUFFER_CELLS: usize = 1024 * 1024;
172        debug_assert!(
173            len <= MAX_BUFFER_CELLS,
174            "into_sized_buffer() received len={len} above the {MAX_BUFFER_CELLS} limit"
175        );
176        let len = len.min(self.max_cells).min(MAX_BUFFER_CELLS);
177        Buffer::new(self.inner, len)
178    }
179
180    /// Pointer to the first cell.
181    #[inline]
182    #[must_use]
183    pub fn as_ptr(&self) -> *const i32 {
184        self.inner.as_ptr()
185    }
186
187    /// Mutable pointer to the first cell.
188    #[inline]
189    pub fn as_mut_ptr(&mut self) -> *mut i32 {
190        self.inner.as_mut_ptr()
191    }
192
193    /// Constructor for tests/benchmarks — not part of the stable API.
194    #[doc(hidden)]
195    #[must_use]
196    pub fn from_raw_parts(inner: Ref<'amx, i32>) -> Self {
197        UnsizedBuffer {
198            inner,
199            max_cells: usize::MAX,
200        }
201    }
202
203    /// Sizes the buffer to `max_len` and writes `s` in one call.
204    ///
205    /// Equivalent to `into_sized_buffer(max_len).write_str(s)`. This is the
206    /// recommended way to fill an output string in natives.
207    ///
208    /// # Errors
209    /// `AmxError::General` if the encoded `s` is >= `max_len` (no room for
210    /// the `0` terminator).
211    pub fn write_str(self, max_len: usize, s: &str) -> AmxResult<()> {
212        let mut buf = self.into_sized_buffer(max_len);
213        string::put_in_buffer(&mut buf, s)
214    }
215}
216
217impl<'amx> AmxCell<'amx> for UnsizedBuffer<'amx> {
218    fn from_raw(amx: &'amx Amx, cell: i32) -> AmxResult<UnsizedBuffer<'amx>> {
219        let inner = amx.get_ref(cell)?;
220        // Cells reachable from `cell` before leaving the data region `[0, stp)`.
221        // Bounds a caller-supplied size larger than the real Pawn array away from
222        // reading/writing past the AMX allocation. `None` (null VM) → no clamp.
223        let max_cells = amx.stp().map_or(usize::MAX, |stp| {
224            usize::try_from((stp - cell).max(0) / 4).unwrap_or(0)
225        });
226        Ok(UnsizedBuffer { inner, max_cells })
227    }
228
229    #[inline]
230    fn as_cell(&self) -> i32 {
231        self.inner.as_cell()
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238    use crate::cell::Ref;
239    use crate::cell::repr::CellConvert;
240
241    fn make_ref(data: &mut Vec<i32>) -> Ref<'_, i32> {
242        unsafe { Ref::new(0, data.as_mut_ptr()) }
243    }
244
245    fn make_buffer(data: &mut Vec<i32>) -> Buffer<'_> {
246        let len = data.len();
247        let r = make_ref(data);
248        Buffer::new(r, len)
249    }
250
251    fn make_unsized(data: &mut Vec<i32>) -> UnsizedBuffer<'_> {
252        UnsizedBuffer {
253            inner: make_ref(data),
254            max_cells: usize::MAX,
255        }
256    }
257
258    // --- Buffer ---
259
260    #[test]
261    fn buffer_len_and_is_empty() {
262        let mut data = vec![0i32; 4];
263        let buf = make_buffer(&mut data);
264        assert_eq!(buf.len(), 4);
265        assert!(!buf.is_empty());
266
267        let mut empty = vec![];
268        let empty_buf = make_buffer(&mut empty);
269        assert_eq!(empty_buf.len(), 0);
270        assert!(empty_buf.is_empty());
271    }
272
273    #[test]
274    fn buffer_deref_reads_values() {
275        let mut data = vec![10i32, 20, 30];
276        let buf = make_buffer(&mut data);
277        assert_eq!(&buf[..], &[10, 20, 30]);
278        assert_eq!(buf[0], 10);
279        assert_eq!(buf[2], 30);
280    }
281
282    #[test]
283    fn buffer_deref_mut_writes_values() {
284        let mut data = vec![0i32; 3];
285        let mut buf = make_buffer(&mut data);
286        buf[0] = 100;
287        buf[1] = 200;
288        buf[2] = 300;
289        assert_eq!(&data, &[100, 200, 300]);
290    }
291
292    #[test]
293    fn buffer_iter_works() {
294        let mut data = vec![1i32, 2, 3, 4];
295        let buf = make_buffer(&mut data);
296        let sum: i32 = buf.iter().sum();
297        assert_eq!(sum, 10);
298    }
299
300    #[test]
301    fn buffer_iter_mut_modifies_in_place() {
302        let mut data = vec![1i32, 2, 3];
303        let mut buf = make_buffer(&mut data);
304        buf.iter_mut().for_each(|x| *x *= 2);
305        assert_eq!(&data, &[2, 4, 6]);
306    }
307
308    #[test]
309    fn buffer_debug_format() {
310        let mut data = vec![1i32, 2, 3];
311        let buf = make_buffer(&mut data);
312        assert_eq!(format!("{buf:?}"), "[1, 2, 3]");
313    }
314
315    #[test]
316    fn buffer_as_cell_returns_amx_addr() {
317        let mut data = vec![0i32; 4];
318        let buf = make_buffer(&mut data);
319        // as_cell() returns the AMX address of the inner Ref (0 in our helper)
320        assert_eq!(buf.as_cell(), 0);
321    }
322
323    // --- UnsizedBuffer ---
324
325    #[test]
326    fn unsized_into_sized_normal_len() {
327        let mut data = vec![1i32, 2, 3, 4, 5];
328        let ub = make_unsized(&mut data);
329        let buf = ub.into_sized_buffer(3);
330        assert_eq!(buf.len(), 3);
331        assert_eq!(buf[0], 1);
332        assert_eq!(buf[2], 3);
333    }
334
335    /// In debug, `debug_assert!` fires for values above the limit.
336    /// In release, the value is silently clamped.
337    #[test]
338    #[cfg_attr(
339        debug_assertions,
340        should_panic(expected = "into_sized_buffer() received len=")
341    )]
342    fn unsized_into_sized_clamps_to_max_in_release() {
343        let mut data = vec![0i32; 8];
344        let ub = make_unsized(&mut data);
345        let buf = ub.into_sized_buffer(1024 * 1024 + 1);
346        // Only reaches here in release — verifies the clamp
347        assert_eq!(buf.len(), 1024 * 1024);
348    }
349
350    #[test]
351    fn unsized_into_sized_at_exact_max() {
352        let mut data = vec![0i32; 8];
353        let ub = make_unsized(&mut data);
354        let buf = ub.into_sized_buffer(1024 * 1024);
355        assert_eq!(buf.len(), 1024 * 1024);
356    }
357
358    #[test]
359    fn into_sized_clamps_to_segment_bound() {
360        // A caller-supplied size larger than the data-region bound (`max_cells`)
361        // is clamped to it, so the resulting slice can never read past the VM.
362        let mut data = vec![0i32; 8];
363        let ub = UnsizedBuffer {
364            inner: make_ref(&mut data),
365            max_cells: 3,
366        };
367        let buf = ub.into_sized_buffer(1000);
368        assert_eq!(buf.len(), 3);
369    }
370
371    #[test]
372    fn unsized_as_ptr_not_null() {
373        let mut data = vec![42i32];
374        let ub = make_unsized(&mut data);
375        assert!(!ub.as_ptr().is_null());
376    }
377
378    #[test]
379    fn unsized_as_cell_returns_amx_addr() {
380        let mut data = vec![0i32];
381        let ub = make_unsized(&mut data);
382        assert_eq!(ub.as_cell(), 0);
383    }
384
385    // --- Buffer::get_as / set_as ---
386
387    #[test]
388    fn get_as_i32_reads_value() {
389        let mut data = vec![10i32, 20, 30];
390        let buf = make_buffer(&mut data);
391        assert_eq!(buf.get_as::<i32>(0), Some(10));
392        assert_eq!(buf.get_as::<i32>(2), Some(30));
393    }
394
395    #[test]
396    fn get_as_out_of_bounds_returns_none() {
397        let mut data = vec![1i32, 2];
398        let buf = make_buffer(&mut data);
399        assert_eq!(buf.get_as::<i32>(2), None);
400        assert_eq!(buf.get_as::<i32>(99), None);
401    }
402
403    #[test]
404    fn set_as_i32_writes_value() {
405        let mut data = vec![0i32; 3];
406        let mut buf = make_buffer(&mut data);
407        assert!(buf.set_as(1, 42i32));
408        assert_eq!(data[1], 42);
409    }
410
411    #[test]
412    fn set_as_out_of_bounds_returns_false() {
413        let mut data = vec![0i32; 2];
414        let mut buf = make_buffer(&mut data);
415        assert!(!buf.set_as(5, 99i32));
416    }
417
418    #[test]
419    fn get_as_f32_roundtrip() {
420        let value = 1.5f32; // exact IEEE-754 value, no approx_constant risk
421        let mut data = vec![value.into_cell()];
422        let buf = make_buffer(&mut data);
423        let recovered: f32 = buf.get_as::<f32>(0).unwrap();
424        assert!(
425            (recovered - value).abs() < f32::EPSILON,
426            "f32 roundtrip failed: {recovered} != {value}"
427        );
428    }
429
430    #[test]
431    fn set_as_f32_stores_bits_correctly() {
432        let mut data = vec![0i32];
433        let mut buf = make_buffer(&mut data);
434        buf.set_as(0, 1.5f32);
435        assert_eq!(data[0].cast_unsigned(), 1.5f32.to_bits());
436    }
437
438    #[test]
439    fn get_as_bool_true_and_false() {
440        let mut data = vec![1i32, 0, 42];
441        let buf = make_buffer(&mut data);
442        assert_eq!(buf.get_as::<bool>(0), Some(true));
443        assert_eq!(buf.get_as::<bool>(1), Some(false));
444        // any non-zero value is true
445        assert_eq!(buf.get_as::<bool>(2), Some(true));
446    }
447
448    #[test]
449    fn set_as_bool_writes_zero_and_one() {
450        let mut data = vec![0i32; 2];
451        let mut buf = make_buffer(&mut data);
452        buf.set_as(0, true);
453        buf.set_as(1, false);
454        assert_eq!(data[0], 1);
455        assert_eq!(data[1], 0);
456    }
457
458    #[test]
459    fn get_as_u8_reads_byte() {
460        let mut data = vec![255i32];
461        let buf = make_buffer(&mut data);
462        assert_eq!(buf.get_as::<u8>(0), Some(255u8));
463    }
464
465    // --- Buffer::iter_as ---
466
467    #[test]
468    fn iter_as_i32_collects_all() {
469        let mut data = vec![1i32, 2, 3, 4];
470        let buf = make_buffer(&mut data);
471        let vals: Vec<i32> = buf.iter_as::<i32>().collect();
472        assert_eq!(vals, vec![1, 2, 3, 4]);
473    }
474
475    #[test]
476    fn iter_as_i32_sum() {
477        let mut data = vec![10i32, 20, 30];
478        let buf = make_buffer(&mut data);
479        let sum: i32 = buf.iter_as::<i32>().sum();
480        assert_eq!(sum, 60);
481    }
482
483    #[test]
484    fn iter_as_f32_roundtrip() {
485        let values = [1.0f32, 2.5, 1.25];
486        let mut data: Vec<i32> = values.iter().map(|&v| v.into_cell()).collect();
487        let buf = make_buffer(&mut data);
488        let recovered: Vec<f32> = buf.iter_as::<f32>().collect();
489        for (orig, got) in values.iter().zip(recovered.iter()) {
490            assert!((orig - got).abs() < f32::EPSILON, "{orig} != {got}");
491        }
492    }
493
494    #[test]
495    fn iter_as_bool_any_and_all() {
496        let mut data = vec![1i32, 0, 1, 1];
497        let buf = make_buffer(&mut data);
498        assert!(buf.iter_as::<bool>().any(|v| !v));
499        assert!(!buf.iter_as::<bool>().all(|v| v));
500    }
501
502    #[test]
503    fn iter_as_empty_buffer() {
504        let mut data: Vec<i32> = vec![];
505        let buf = make_buffer(&mut data);
506        assert_eq!(buf.iter_as::<i32>().count(), 0);
507    }
508
509    #[test]
510    fn iter_as_matches_get_as_loop() {
511        let mut data = vec![10i32, 20, 30, 40];
512        let buf = make_buffer(&mut data);
513        let via_iter: Vec<i32> = buf.iter_as::<i32>().collect();
514        let via_loop: Vec<i32> = (0..buf.len())
515            .filter_map(|i| buf.get_as::<i32>(i))
516            .collect();
517        assert_eq!(via_iter, via_loop);
518    }
519
520    // --- Buffer::write_str ---
521
522    #[test]
523    fn write_str_encodes_string_into_cells() {
524        // "hi" -> cells [104, 105, 0] (h=104, i=105, nul=0)
525        let mut data = vec![0i32; 3];
526        let mut buf = make_buffer(&mut data);
527        assert!(buf.write_str("hi").is_ok());
528        assert_eq!(data[0], i32::from(b'h'));
529        assert_eq!(data[1], i32::from(b'i'));
530        assert_eq!(data[2], 0); // null terminator
531    }
532
533    #[test]
534    fn write_str_empty_string_writes_null_terminator() {
535        let mut data = vec![99i32; 2];
536        let mut buf = make_buffer(&mut data);
537        assert!(buf.write_str("").is_ok());
538        assert_eq!(data[0], 0);
539    }
540
541    #[test]
542    fn write_str_exact_fit_fails() {
543        // A 3-cell buffer cannot hold "abc" (it would need 4: a, b, c, nul)
544        let mut data = vec![0i32; 3];
545        let mut buf = make_buffer(&mut data);
546        assert!(buf.write_str("abc").is_err());
547    }
548
549    // --- UnsizedBuffer::write_str ---
550
551    #[test]
552    fn unsized_write_str_sizes_and_writes() {
553        let mut data = vec![0i32; 5];
554        let ub = make_unsized(&mut data);
555        assert!(ub.write_str(5, "hi").is_ok());
556        assert_eq!(data[0], i32::from(b'h'));
557        assert_eq!(data[1], i32::from(b'i'));
558        assert_eq!(data[2], 0);
559    }
560
561    #[test]
562    fn unsized_write_str_too_long_returns_err() {
563        let mut data = vec![0i32; 3];
564        let ub = make_unsized(&mut data);
565        assert!(ub.write_str(3, "abc").is_err());
566    }
567
568    // --- Adversarial / property tests: indexed access must stay in bounds for
569    //     any index, and sizing must never exceed the declared length. ---
570
571    fn lcg(seed: &mut u64) -> u32 {
572        *seed = seed
573            .wrapping_mul(6_364_136_223_846_793_005)
574            .wrapping_add(1_442_695_040_888_963_407);
575        (*seed >> 33) as u32
576    }
577
578    #[test]
579    fn fuzz_get_set_as_respects_bounds() {
580        let mut seed = 0xDEAD_C0DE_1234_5678u64;
581        for _ in 0..4000 {
582            let len = (lcg(&mut seed) % 10) as usize; // 0..=9 cells
583            let mut data: Vec<i32> = vec![0; len];
584            let mut buf = make_buffer(&mut data);
585            let idx = (lcg(&mut seed) % 32) as usize; // may exceed len
586
587            // get_as: Some iff in bounds, never panics.
588            assert_eq!(buf.get_as::<i32>(idx).is_some(), idx < len);
589            // set_as: writes iff in bounds, returns the same predicate.
590            let wrote = buf.set_as::<i32>(idx, 7);
591            assert_eq!(wrote, idx < len);
592            if wrote {
593                assert_eq!(buf.get_as::<i32>(idx), Some(7));
594            }
595        }
596    }
597
598    #[test]
599    fn fuzz_into_sized_never_exceeds_declared_len() {
600        // With a finite segment bound, the resulting buffer length is always
601        // min(requested, bound, 1 MiB) and never larger than the real data.
602        let mut seed = 0x00C0_FFEE_BADD_F00Du64;
603        for _ in 0..2000 {
604            let cells = (lcg(&mut seed) % 8) as usize + 1;
605            let mut data: Vec<i32> = vec![0; cells];
606            let bound = (lcg(&mut seed) % 16) as usize;
607            let ub = UnsizedBuffer {
608                inner: make_ref(&mut data),
609                max_cells: bound,
610            };
611            let requested = (lcg(&mut seed) % 1000) as usize;
612            let buf = ub.into_sized_buffer(requested);
613            // Exact invariant: clamp to the segment bound and the 1 MiB ceiling.
614            assert_eq!(buf.len(), requested.min(bound).min(1024 * 1024));
615        }
616    }
617}