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    /// Writes `s` like [`write_str`], reporting whether the configured encoding
113    /// had to substitute characters it cannot represent.
114    ///
115    /// `true` means the Pawn side received a `?` (or a numeric reference) where
116    /// the Rust string had a character — a Cyrillic name on a Windows-1252
117    /// server, say. The write still happened; what the flag buys is the chance
118    /// to log it instead of shipping corrupted text silently.
119    /// [`crate::encoding::unmappable_chars`] then names the characters.
120    ///
121    /// # Errors
122    /// `AmxError::General` if the encoded string does not fit in the buffer.
123    ///
124    /// [`write_str`]: Buffer::write_str
125    pub fn write_str_checked(&mut self, s: &str) -> AmxResult<bool> {
126        string::put_in_buffer_checked(self, s)
127    }
128}
129
130// `Buffer` cannot be parsed directly from a cell — use `UnsizedBuffer`
131// as the native argument and then `.into_sized_buffer(len)`.
132impl<'amx> AmxCell<'amx> for Buffer<'amx> {
133    #[inline]
134    fn as_cell(&self) -> i32 {
135        self.inner.as_cell()
136    }
137}
138
139impl Deref for Buffer<'_> {
140    type Target = [i32];
141
142    fn deref(&self) -> &[i32] {
143        self.as_slice()
144    }
145}
146
147impl DerefMut for Buffer<'_> {
148    fn deref_mut(&mut self) -> &mut [i32] {
149        self.as_mut_slice()
150    }
151}
152
153impl std::fmt::Debug for Buffer<'_> {
154    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
155        write!(f, "{:?}", self.as_slice())
156    }
157}
158
159/// Array with unknown size — received as a native argument when the
160/// Pawn signature is `array[]` without a fixed dimension.
161///
162/// The actual size usually comes as another parameter (`sizeof(array)`). Use
163/// [`into_sized_buffer`] to convert into [`Buffer`] before iterating.
164///
165/// [`into_sized_buffer`]: UnsizedBuffer::into_sized_buffer
166pub struct UnsizedBuffer<'amx> {
167    inner: Ref<'amx, i32>,
168    /// Upper bound on cells reachable from the first cell without leaving the
169    /// VM's data region `[0, stp)`. Computed from `amx_StrLen`-independent VM
170    /// state at parse time and used to clamp [`into_sized_buffer`]. `usize::MAX`
171    /// when unknown (test constructor).
172    ///
173    /// [`into_sized_buffer`]: UnsizedBuffer::into_sized_buffer
174    max_cells: usize,
175}
176
177impl<'amx> UnsizedBuffer<'amx> {
178    /// Converts into `Buffer` by declaring the size.
179    ///
180    /// `len` should be the real Pawn array size (`sizeof(arr)`). A `len` larger
181    /// than the actual array still reads/writes neighbouring cells of the same
182    /// script (wrong data, not a crash), so never pass a size the script can
183    /// control independently. The SDK clamps `len` two ways as a safety net:
184    /// to the VM data region `[0, stp)` (so an oversized `len` cannot read or
185    /// write past the AMX allocation and segfault) and to a 1 MiB ceiling.
186    #[must_use]
187    pub fn into_sized_buffer(self, len: usize) -> Buffer<'amx> {
188        const MAX_BUFFER_CELLS: usize = 1024 * 1024;
189        // No assertion on `len`: it is often a script argument, and the clamp
190        // below is the defence.
191        let len = len.min(self.max_cells).min(MAX_BUFFER_CELLS);
192        Buffer::new(self.inner, len)
193    }
194
195    /// Pointer to the first cell.
196    #[inline]
197    #[must_use]
198    pub fn as_ptr(&self) -> *const i32 {
199        self.inner.as_ptr()
200    }
201
202    /// Mutable pointer to the first cell.
203    #[inline]
204    pub fn as_mut_ptr(&mut self) -> *mut i32 {
205        self.inner.as_mut_ptr()
206    }
207
208    /// Cells from the first one to the end of the AMX data region;
209    /// `usize::MAX` when unknown.
210    pub(crate) fn max_cells(&self) -> usize {
211        self.max_cells
212    }
213
214    /// Constructor for tests/benchmarks — not part of the stable API.
215    #[doc(hidden)]
216    #[must_use]
217    pub fn from_raw_parts(inner: Ref<'amx, i32>) -> Self {
218        UnsizedBuffer {
219            inner,
220            max_cells: usize::MAX,
221        }
222    }
223
224    /// Sizes the buffer to `max_len` and writes `s` in one call.
225    ///
226    /// Equivalent to `into_sized_buffer(max_len).write_str(s)`. This is the
227    /// recommended way to fill an output string in natives.
228    ///
229    /// # Errors
230    /// `AmxError::General` if the encoded `s` is >= `max_len` (no room for
231    /// the `0` terminator).
232    pub fn write_str(self, max_len: usize, s: &str) -> AmxResult<()> {
233        let mut buf = self.into_sized_buffer(max_len);
234        string::put_in_buffer(&mut buf, s)
235    }
236
237    /// Writes `s` like [`write_str`], reporting whether the configured encoding
238    /// had to substitute characters it cannot represent — see
239    /// [`Buffer::write_str_checked`].
240    ///
241    /// # Errors
242    /// `AmxError::General` if the encoded string does not fit in `max_len`.
243    ///
244    /// [`write_str`]: UnsizedBuffer::write_str
245    pub fn write_str_checked(self, max_len: usize, s: &str) -> AmxResult<bool> {
246        let mut buf = self.into_sized_buffer(max_len);
247        string::put_in_buffer_checked(&mut buf, s)
248    }
249}
250
251impl<'amx> AmxCell<'amx> for UnsizedBuffer<'amx> {
252    fn from_raw(amx: &'amx Amx, cell: i32) -> AmxResult<UnsizedBuffer<'amx>> {
253        let inner = amx.get_ref(cell)?;
254        // Cells reachable from `cell` before leaving the data region `[0, stp)`.
255        // Bounds a caller-supplied size larger than the real Pawn array away from
256        // reading/writing past the AMX allocation. `None` (null VM) → no clamp.
257        let max_cells = amx.stp().map_or(usize::MAX, |stp| {
258            usize::try_from((stp - cell).max(0) / 4).unwrap_or(0)
259        });
260        Ok(UnsizedBuffer { inner, max_cells })
261    }
262
263    #[inline]
264    fn as_cell(&self) -> i32 {
265        self.inner.as_cell()
266    }
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272    use crate::cell::Ref;
273    use crate::cell::repr::CellConvert;
274
275    fn make_ref(data: &mut Vec<i32>) -> Ref<'_, i32> {
276        unsafe { Ref::new(0, data.as_mut_ptr()) }
277    }
278
279    fn make_buffer(data: &mut Vec<i32>) -> Buffer<'_> {
280        let len = data.len();
281        let r = make_ref(data);
282        Buffer::new(r, len)
283    }
284
285    fn make_unsized(data: &mut Vec<i32>) -> UnsizedBuffer<'_> {
286        UnsizedBuffer {
287            inner: make_ref(data),
288            max_cells: usize::MAX,
289        }
290    }
291
292    // --- Buffer ---
293
294    #[test]
295    fn buffer_len_and_is_empty() {
296        let mut data = vec![0i32; 4];
297        let buf = make_buffer(&mut data);
298        assert_eq!(buf.len(), 4);
299        assert!(!buf.is_empty());
300
301        let mut empty = vec![];
302        let empty_buf = make_buffer(&mut empty);
303        assert_eq!(empty_buf.len(), 0);
304        assert!(empty_buf.is_empty());
305    }
306
307    #[test]
308    fn buffer_deref_reads_values() {
309        let mut data = vec![10i32, 20, 30];
310        let buf = make_buffer(&mut data);
311        assert_eq!(&buf[..], &[10, 20, 30]);
312        assert_eq!(buf[0], 10);
313        assert_eq!(buf[2], 30);
314    }
315
316    #[test]
317    fn buffer_deref_mut_writes_values() {
318        let mut data = vec![0i32; 3];
319        let mut buf = make_buffer(&mut data);
320        buf[0] = 100;
321        buf[1] = 200;
322        buf[2] = 300;
323        assert_eq!(&data, &[100, 200, 300]);
324    }
325
326    #[test]
327    fn buffer_iter_works() {
328        let mut data = vec![1i32, 2, 3, 4];
329        let buf = make_buffer(&mut data);
330        let sum: i32 = buf.iter().sum();
331        assert_eq!(sum, 10);
332    }
333
334    #[test]
335    fn buffer_iter_mut_modifies_in_place() {
336        let mut data = vec![1i32, 2, 3];
337        let mut buf = make_buffer(&mut data);
338        buf.iter_mut().for_each(|x| *x *= 2);
339        assert_eq!(&data, &[2, 4, 6]);
340    }
341
342    #[test]
343    fn buffer_debug_format() {
344        let mut data = vec![1i32, 2, 3];
345        let buf = make_buffer(&mut data);
346        assert_eq!(format!("{buf:?}"), "[1, 2, 3]");
347    }
348
349    #[test]
350    fn buffer_as_cell_returns_amx_addr() {
351        let mut data = vec![0i32; 4];
352        let buf = make_buffer(&mut data);
353        // as_cell() returns the AMX address of the inner Ref (0 in our helper)
354        assert_eq!(buf.as_cell(), 0);
355    }
356
357    // --- UnsizedBuffer ---
358
359    #[test]
360    fn unsized_into_sized_normal_len() {
361        let mut data = vec![1i32, 2, 3, 4, 5];
362        let ub = make_unsized(&mut data);
363        let buf = ub.into_sized_buffer(3);
364        assert_eq!(buf.len(), 3);
365        assert_eq!(buf[0], 1);
366        assert_eq!(buf[2], 3);
367    }
368
369    /// The size is often a script argument: clamped in every build, never a
370    /// panic.
371    #[test]
372    fn unsized_into_sized_clamps_to_max() {
373        let mut data = vec![0i32; 8];
374        let ub = make_unsized(&mut data);
375        let buf = ub.into_sized_buffer(1024 * 1024 + 1);
376        assert_eq!(buf.len(), 1024 * 1024);
377    }
378
379    #[test]
380    fn unsized_into_sized_at_exact_max() {
381        let mut data = vec![0i32; 8];
382        let ub = make_unsized(&mut data);
383        let buf = ub.into_sized_buffer(1024 * 1024);
384        assert_eq!(buf.len(), 1024 * 1024);
385    }
386
387    #[test]
388    fn into_sized_clamps_to_segment_bound() {
389        // A caller-supplied size larger than the data-region bound (`max_cells`)
390        // is clamped to it, so the resulting slice can never read past the VM.
391        let mut data = vec![0i32; 8];
392        let ub = UnsizedBuffer {
393            inner: make_ref(&mut data),
394            max_cells: 3,
395        };
396        let buf = ub.into_sized_buffer(1000);
397        assert_eq!(buf.len(), 3);
398    }
399
400    #[test]
401    fn unsized_as_ptr_not_null() {
402        let mut data = vec![42i32];
403        let ub = make_unsized(&mut data);
404        assert!(!ub.as_ptr().is_null());
405    }
406
407    #[test]
408    fn unsized_as_cell_returns_amx_addr() {
409        let mut data = vec![0i32];
410        let ub = make_unsized(&mut data);
411        assert_eq!(ub.as_cell(), 0);
412    }
413
414    // --- Buffer::get_as / set_as ---
415
416    #[test]
417    fn get_as_i32_reads_value() {
418        let mut data = vec![10i32, 20, 30];
419        let buf = make_buffer(&mut data);
420        assert_eq!(buf.get_as::<i32>(0), Some(10));
421        assert_eq!(buf.get_as::<i32>(2), Some(30));
422    }
423
424    #[test]
425    fn get_as_out_of_bounds_returns_none() {
426        let mut data = vec![1i32, 2];
427        let buf = make_buffer(&mut data);
428        assert_eq!(buf.get_as::<i32>(2), None);
429        assert_eq!(buf.get_as::<i32>(99), None);
430    }
431
432    #[test]
433    fn set_as_i32_writes_value() {
434        let mut data = vec![0i32; 3];
435        let mut buf = make_buffer(&mut data);
436        assert!(buf.set_as(1, 42i32));
437        assert_eq!(data[1], 42);
438    }
439
440    #[test]
441    fn set_as_out_of_bounds_returns_false() {
442        let mut data = vec![0i32; 2];
443        let mut buf = make_buffer(&mut data);
444        assert!(!buf.set_as(5, 99i32));
445    }
446
447    #[test]
448    fn get_as_f32_roundtrip() {
449        let value = 1.5f32; // exact IEEE-754 value, no approx_constant risk
450        let mut data = vec![value.into_cell()];
451        let buf = make_buffer(&mut data);
452        let recovered: f32 = buf.get_as::<f32>(0).unwrap();
453        assert!(
454            (recovered - value).abs() < f32::EPSILON,
455            "f32 roundtrip failed: {recovered} != {value}"
456        );
457    }
458
459    #[test]
460    fn set_as_f32_stores_bits_correctly() {
461        let mut data = vec![0i32];
462        let mut buf = make_buffer(&mut data);
463        buf.set_as(0, 1.5f32);
464        assert_eq!(data[0].cast_unsigned(), 1.5f32.to_bits());
465    }
466
467    #[test]
468    fn get_as_bool_true_and_false() {
469        let mut data = vec![1i32, 0, 42];
470        let buf = make_buffer(&mut data);
471        assert_eq!(buf.get_as::<bool>(0), Some(true));
472        assert_eq!(buf.get_as::<bool>(1), Some(false));
473        // any non-zero value is true
474        assert_eq!(buf.get_as::<bool>(2), Some(true));
475    }
476
477    #[test]
478    fn set_as_bool_writes_zero_and_one() {
479        let mut data = vec![0i32; 2];
480        let mut buf = make_buffer(&mut data);
481        buf.set_as(0, true);
482        buf.set_as(1, false);
483        assert_eq!(data[0], 1);
484        assert_eq!(data[1], 0);
485    }
486
487    #[test]
488    fn get_as_u8_reads_byte() {
489        let mut data = vec![255i32];
490        let buf = make_buffer(&mut data);
491        assert_eq!(buf.get_as::<u8>(0), Some(255u8));
492    }
493
494    // --- Buffer::iter_as ---
495
496    #[test]
497    fn iter_as_i32_collects_all() {
498        let mut data = vec![1i32, 2, 3, 4];
499        let buf = make_buffer(&mut data);
500        let vals: Vec<i32> = buf.iter_as::<i32>().collect();
501        assert_eq!(vals, vec![1, 2, 3, 4]);
502    }
503
504    #[test]
505    fn iter_as_i32_sum() {
506        let mut data = vec![10i32, 20, 30];
507        let buf = make_buffer(&mut data);
508        let sum: i32 = buf.iter_as::<i32>().sum();
509        assert_eq!(sum, 60);
510    }
511
512    #[test]
513    fn iter_as_f32_roundtrip() {
514        let values = [1.0f32, 2.5, 1.25];
515        let mut data: Vec<i32> = values.iter().map(|&v| v.into_cell()).collect();
516        let buf = make_buffer(&mut data);
517        let recovered: Vec<f32> = buf.iter_as::<f32>().collect();
518        for (orig, got) in values.iter().zip(recovered.iter()) {
519            assert!((orig - got).abs() < f32::EPSILON, "{orig} != {got}");
520        }
521    }
522
523    #[test]
524    fn iter_as_bool_any_and_all() {
525        let mut data = vec![1i32, 0, 1, 1];
526        let buf = make_buffer(&mut data);
527        assert!(buf.iter_as::<bool>().any(|v| !v));
528        assert!(!buf.iter_as::<bool>().all(|v| v));
529    }
530
531    #[test]
532    fn iter_as_empty_buffer() {
533        let mut data: Vec<i32> = vec![];
534        let buf = make_buffer(&mut data);
535        assert_eq!(buf.iter_as::<i32>().count(), 0);
536    }
537
538    #[test]
539    fn iter_as_matches_get_as_loop() {
540        let mut data = vec![10i32, 20, 30, 40];
541        let buf = make_buffer(&mut data);
542        let via_iter: Vec<i32> = buf.iter_as::<i32>().collect();
543        let via_loop: Vec<i32> = (0..buf.len())
544            .filter_map(|i| buf.get_as::<i32>(i))
545            .collect();
546        assert_eq!(via_iter, via_loop);
547    }
548
549    // --- Buffer::write_str ---
550
551    #[test]
552    fn write_str_encodes_string_into_cells() {
553        // "hi" -> cells [104, 105, 0] (h=104, i=105, nul=0)
554        let mut data = vec![0i32; 3];
555        let mut buf = make_buffer(&mut data);
556        assert!(buf.write_str("hi").is_ok());
557        assert_eq!(data[0], i32::from(b'h'));
558        assert_eq!(data[1], i32::from(b'i'));
559        assert_eq!(data[2], 0); // null terminator
560    }
561
562    #[test]
563    fn write_str_empty_string_writes_null_terminator() {
564        let mut data = vec![99i32; 2];
565        let mut buf = make_buffer(&mut data);
566        assert!(buf.write_str("").is_ok());
567        assert_eq!(data[0], 0);
568    }
569
570    #[test]
571    fn write_str_exact_fit_fails() {
572        // A 3-cell buffer cannot hold "abc" (it would need 4: a, b, c, nul)
573        let mut data = vec![0i32; 3];
574        let mut buf = make_buffer(&mut data);
575        assert!(buf.write_str("abc").is_err());
576    }
577
578    // --- UnsizedBuffer::write_str ---
579
580    #[test]
581    fn unsized_write_str_sizes_and_writes() {
582        let mut data = vec![0i32; 5];
583        let ub = make_unsized(&mut data);
584        assert!(ub.write_str(5, "hi").is_ok());
585        assert_eq!(data[0], i32::from(b'h'));
586        assert_eq!(data[1], i32::from(b'i'));
587        assert_eq!(data[2], 0);
588    }
589
590    #[test]
591    fn unsized_write_str_too_long_returns_err() {
592        let mut data = vec![0i32; 3];
593        let ub = make_unsized(&mut data);
594        assert!(ub.write_str(3, "abc").is_err());
595    }
596
597    // --- Adversarial / property tests: indexed access must stay in bounds for
598    //     any index, and sizing must never exceed the declared length. ---
599
600    fn lcg(seed: &mut u64) -> u32 {
601        *seed = seed
602            .wrapping_mul(6_364_136_223_846_793_005)
603            .wrapping_add(1_442_695_040_888_963_407);
604        (*seed >> 33) as u32
605    }
606
607    #[test]
608    fn fuzz_get_set_as_respects_bounds() {
609        let mut seed = 0xDEAD_C0DE_1234_5678u64;
610        for _ in 0..4000 {
611            let len = (lcg(&mut seed) % 10) as usize; // 0..=9 cells
612            let mut data: Vec<i32> = vec![0; len];
613            let mut buf = make_buffer(&mut data);
614            let idx = (lcg(&mut seed) % 32) as usize; // may exceed len
615
616            // get_as: Some iff in bounds, never panics.
617            assert_eq!(buf.get_as::<i32>(idx).is_some(), idx < len);
618            // set_as: writes iff in bounds, returns the same predicate.
619            let wrote = buf.set_as::<i32>(idx, 7);
620            assert_eq!(wrote, idx < len);
621            if wrote {
622                assert_eq!(buf.get_as::<i32>(idx), Some(7));
623            }
624        }
625    }
626
627    #[test]
628    fn fuzz_into_sized_never_exceeds_declared_len() {
629        // With a finite segment bound, the resulting buffer length is always
630        // min(requested, bound, 1 MiB) and never larger than the real data.
631        let mut seed = 0x00C0_FFEE_BADD_F00Du64;
632        for _ in 0..2000 {
633            let cells = (lcg(&mut seed) % 8) as usize + 1;
634            let mut data: Vec<i32> = vec![0; cells];
635            let bound = (lcg(&mut seed) % 16) as usize;
636            let ub = UnsizedBuffer {
637                inner: make_ref(&mut data),
638                max_cells: bound,
639            };
640            let requested = (lcg(&mut seed) % 1000) as usize;
641            let buf = ub.into_sized_buffer(requested);
642            // Exact invariant: clamp to the segment bound and the 1 MiB ceiling.
643            assert_eq!(buf.len(), requested.min(bound).min(1024 * 1024));
644        }
645    }
646}