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        debug_assert!(
190            len <= MAX_BUFFER_CELLS,
191            "into_sized_buffer() received len={len} above the {MAX_BUFFER_CELLS} limit"
192        );
193        let len = len.min(self.max_cells).min(MAX_BUFFER_CELLS);
194        Buffer::new(self.inner, len)
195    }
196
197    /// Pointer to the first cell.
198    #[inline]
199    #[must_use]
200    pub fn as_ptr(&self) -> *const i32 {
201        self.inner.as_ptr()
202    }
203
204    /// Mutable pointer to the first cell.
205    #[inline]
206    pub fn as_mut_ptr(&mut self) -> *mut i32 {
207        self.inner.as_mut_ptr()
208    }
209
210    /// Constructor for tests/benchmarks — not part of the stable API.
211    #[doc(hidden)]
212    #[must_use]
213    pub fn from_raw_parts(inner: Ref<'amx, i32>) -> Self {
214        UnsizedBuffer {
215            inner,
216            max_cells: usize::MAX,
217        }
218    }
219
220    /// Sizes the buffer to `max_len` and writes `s` in one call.
221    ///
222    /// Equivalent to `into_sized_buffer(max_len).write_str(s)`. This is the
223    /// recommended way to fill an output string in natives.
224    ///
225    /// # Errors
226    /// `AmxError::General` if the encoded `s` is >= `max_len` (no room for
227    /// the `0` terminator).
228    pub fn write_str(self, max_len: usize, s: &str) -> AmxResult<()> {
229        let mut buf = self.into_sized_buffer(max_len);
230        string::put_in_buffer(&mut buf, s)
231    }
232
233    /// Writes `s` like [`write_str`], reporting whether the configured encoding
234    /// had to substitute characters it cannot represent — see
235    /// [`Buffer::write_str_checked`].
236    ///
237    /// # Errors
238    /// `AmxError::General` if the encoded string does not fit in `max_len`.
239    ///
240    /// [`write_str`]: UnsizedBuffer::write_str
241    pub fn write_str_checked(self, max_len: usize, s: &str) -> AmxResult<bool> {
242        let mut buf = self.into_sized_buffer(max_len);
243        string::put_in_buffer_checked(&mut buf, s)
244    }
245}
246
247impl<'amx> AmxCell<'amx> for UnsizedBuffer<'amx> {
248    fn from_raw(amx: &'amx Amx, cell: i32) -> AmxResult<UnsizedBuffer<'amx>> {
249        let inner = amx.get_ref(cell)?;
250        // Cells reachable from `cell` before leaving the data region `[0, stp)`.
251        // Bounds a caller-supplied size larger than the real Pawn array away from
252        // reading/writing past the AMX allocation. `None` (null VM) → no clamp.
253        let max_cells = amx.stp().map_or(usize::MAX, |stp| {
254            usize::try_from((stp - cell).max(0) / 4).unwrap_or(0)
255        });
256        Ok(UnsizedBuffer { inner, max_cells })
257    }
258
259    #[inline]
260    fn as_cell(&self) -> i32 {
261        self.inner.as_cell()
262    }
263}
264
265#[cfg(test)]
266mod tests {
267    use super::*;
268    use crate::cell::Ref;
269    use crate::cell::repr::CellConvert;
270
271    fn make_ref(data: &mut Vec<i32>) -> Ref<'_, i32> {
272        unsafe { Ref::new(0, data.as_mut_ptr()) }
273    }
274
275    fn make_buffer(data: &mut Vec<i32>) -> Buffer<'_> {
276        let len = data.len();
277        let r = make_ref(data);
278        Buffer::new(r, len)
279    }
280
281    fn make_unsized(data: &mut Vec<i32>) -> UnsizedBuffer<'_> {
282        UnsizedBuffer {
283            inner: make_ref(data),
284            max_cells: usize::MAX,
285        }
286    }
287
288    // --- Buffer ---
289
290    #[test]
291    fn buffer_len_and_is_empty() {
292        let mut data = vec![0i32; 4];
293        let buf = make_buffer(&mut data);
294        assert_eq!(buf.len(), 4);
295        assert!(!buf.is_empty());
296
297        let mut empty = vec![];
298        let empty_buf = make_buffer(&mut empty);
299        assert_eq!(empty_buf.len(), 0);
300        assert!(empty_buf.is_empty());
301    }
302
303    #[test]
304    fn buffer_deref_reads_values() {
305        let mut data = vec![10i32, 20, 30];
306        let buf = make_buffer(&mut data);
307        assert_eq!(&buf[..], &[10, 20, 30]);
308        assert_eq!(buf[0], 10);
309        assert_eq!(buf[2], 30);
310    }
311
312    #[test]
313    fn buffer_deref_mut_writes_values() {
314        let mut data = vec![0i32; 3];
315        let mut buf = make_buffer(&mut data);
316        buf[0] = 100;
317        buf[1] = 200;
318        buf[2] = 300;
319        assert_eq!(&data, &[100, 200, 300]);
320    }
321
322    #[test]
323    fn buffer_iter_works() {
324        let mut data = vec![1i32, 2, 3, 4];
325        let buf = make_buffer(&mut data);
326        let sum: i32 = buf.iter().sum();
327        assert_eq!(sum, 10);
328    }
329
330    #[test]
331    fn buffer_iter_mut_modifies_in_place() {
332        let mut data = vec![1i32, 2, 3];
333        let mut buf = make_buffer(&mut data);
334        buf.iter_mut().for_each(|x| *x *= 2);
335        assert_eq!(&data, &[2, 4, 6]);
336    }
337
338    #[test]
339    fn buffer_debug_format() {
340        let mut data = vec![1i32, 2, 3];
341        let buf = make_buffer(&mut data);
342        assert_eq!(format!("{buf:?}"), "[1, 2, 3]");
343    }
344
345    #[test]
346    fn buffer_as_cell_returns_amx_addr() {
347        let mut data = vec![0i32; 4];
348        let buf = make_buffer(&mut data);
349        // as_cell() returns the AMX address of the inner Ref (0 in our helper)
350        assert_eq!(buf.as_cell(), 0);
351    }
352
353    // --- UnsizedBuffer ---
354
355    #[test]
356    fn unsized_into_sized_normal_len() {
357        let mut data = vec![1i32, 2, 3, 4, 5];
358        let ub = make_unsized(&mut data);
359        let buf = ub.into_sized_buffer(3);
360        assert_eq!(buf.len(), 3);
361        assert_eq!(buf[0], 1);
362        assert_eq!(buf[2], 3);
363    }
364
365    /// In debug, `debug_assert!` fires for values above the limit.
366    /// In release, the value is silently clamped.
367    #[test]
368    #[cfg_attr(
369        debug_assertions,
370        should_panic(expected = "into_sized_buffer() received len=")
371    )]
372    fn unsized_into_sized_clamps_to_max_in_release() {
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        // Only reaches here in release — verifies the clamp
377        assert_eq!(buf.len(), 1024 * 1024);
378    }
379
380    #[test]
381    fn unsized_into_sized_at_exact_max() {
382        let mut data = vec![0i32; 8];
383        let ub = make_unsized(&mut data);
384        let buf = ub.into_sized_buffer(1024 * 1024);
385        assert_eq!(buf.len(), 1024 * 1024);
386    }
387
388    #[test]
389    fn into_sized_clamps_to_segment_bound() {
390        // A caller-supplied size larger than the data-region bound (`max_cells`)
391        // is clamped to it, so the resulting slice can never read past the VM.
392        let mut data = vec![0i32; 8];
393        let ub = UnsizedBuffer {
394            inner: make_ref(&mut data),
395            max_cells: 3,
396        };
397        let buf = ub.into_sized_buffer(1000);
398        assert_eq!(buf.len(), 3);
399    }
400
401    #[test]
402    fn unsized_as_ptr_not_null() {
403        let mut data = vec![42i32];
404        let ub = make_unsized(&mut data);
405        assert!(!ub.as_ptr().is_null());
406    }
407
408    #[test]
409    fn unsized_as_cell_returns_amx_addr() {
410        let mut data = vec![0i32];
411        let ub = make_unsized(&mut data);
412        assert_eq!(ub.as_cell(), 0);
413    }
414
415    // --- Buffer::get_as / set_as ---
416
417    #[test]
418    fn get_as_i32_reads_value() {
419        let mut data = vec![10i32, 20, 30];
420        let buf = make_buffer(&mut data);
421        assert_eq!(buf.get_as::<i32>(0), Some(10));
422        assert_eq!(buf.get_as::<i32>(2), Some(30));
423    }
424
425    #[test]
426    fn get_as_out_of_bounds_returns_none() {
427        let mut data = vec![1i32, 2];
428        let buf = make_buffer(&mut data);
429        assert_eq!(buf.get_as::<i32>(2), None);
430        assert_eq!(buf.get_as::<i32>(99), None);
431    }
432
433    #[test]
434    fn set_as_i32_writes_value() {
435        let mut data = vec![0i32; 3];
436        let mut buf = make_buffer(&mut data);
437        assert!(buf.set_as(1, 42i32));
438        assert_eq!(data[1], 42);
439    }
440
441    #[test]
442    fn set_as_out_of_bounds_returns_false() {
443        let mut data = vec![0i32; 2];
444        let mut buf = make_buffer(&mut data);
445        assert!(!buf.set_as(5, 99i32));
446    }
447
448    #[test]
449    fn get_as_f32_roundtrip() {
450        let value = 1.5f32; // exact IEEE-754 value, no approx_constant risk
451        let mut data = vec![value.into_cell()];
452        let buf = make_buffer(&mut data);
453        let recovered: f32 = buf.get_as::<f32>(0).unwrap();
454        assert!(
455            (recovered - value).abs() < f32::EPSILON,
456            "f32 roundtrip failed: {recovered} != {value}"
457        );
458    }
459
460    #[test]
461    fn set_as_f32_stores_bits_correctly() {
462        let mut data = vec![0i32];
463        let mut buf = make_buffer(&mut data);
464        buf.set_as(0, 1.5f32);
465        assert_eq!(data[0].cast_unsigned(), 1.5f32.to_bits());
466    }
467
468    #[test]
469    fn get_as_bool_true_and_false() {
470        let mut data = vec![1i32, 0, 42];
471        let buf = make_buffer(&mut data);
472        assert_eq!(buf.get_as::<bool>(0), Some(true));
473        assert_eq!(buf.get_as::<bool>(1), Some(false));
474        // any non-zero value is true
475        assert_eq!(buf.get_as::<bool>(2), Some(true));
476    }
477
478    #[test]
479    fn set_as_bool_writes_zero_and_one() {
480        let mut data = vec![0i32; 2];
481        let mut buf = make_buffer(&mut data);
482        buf.set_as(0, true);
483        buf.set_as(1, false);
484        assert_eq!(data[0], 1);
485        assert_eq!(data[1], 0);
486    }
487
488    #[test]
489    fn get_as_u8_reads_byte() {
490        let mut data = vec![255i32];
491        let buf = make_buffer(&mut data);
492        assert_eq!(buf.get_as::<u8>(0), Some(255u8));
493    }
494
495    // --- Buffer::iter_as ---
496
497    #[test]
498    fn iter_as_i32_collects_all() {
499        let mut data = vec![1i32, 2, 3, 4];
500        let buf = make_buffer(&mut data);
501        let vals: Vec<i32> = buf.iter_as::<i32>().collect();
502        assert_eq!(vals, vec![1, 2, 3, 4]);
503    }
504
505    #[test]
506    fn iter_as_i32_sum() {
507        let mut data = vec![10i32, 20, 30];
508        let buf = make_buffer(&mut data);
509        let sum: i32 = buf.iter_as::<i32>().sum();
510        assert_eq!(sum, 60);
511    }
512
513    #[test]
514    fn iter_as_f32_roundtrip() {
515        let values = [1.0f32, 2.5, 1.25];
516        let mut data: Vec<i32> = values.iter().map(|&v| v.into_cell()).collect();
517        let buf = make_buffer(&mut data);
518        let recovered: Vec<f32> = buf.iter_as::<f32>().collect();
519        for (orig, got) in values.iter().zip(recovered.iter()) {
520            assert!((orig - got).abs() < f32::EPSILON, "{orig} != {got}");
521        }
522    }
523
524    #[test]
525    fn iter_as_bool_any_and_all() {
526        let mut data = vec![1i32, 0, 1, 1];
527        let buf = make_buffer(&mut data);
528        assert!(buf.iter_as::<bool>().any(|v| !v));
529        assert!(!buf.iter_as::<bool>().all(|v| v));
530    }
531
532    #[test]
533    fn iter_as_empty_buffer() {
534        let mut data: Vec<i32> = vec![];
535        let buf = make_buffer(&mut data);
536        assert_eq!(buf.iter_as::<i32>().count(), 0);
537    }
538
539    #[test]
540    fn iter_as_matches_get_as_loop() {
541        let mut data = vec![10i32, 20, 30, 40];
542        let buf = make_buffer(&mut data);
543        let via_iter: Vec<i32> = buf.iter_as::<i32>().collect();
544        let via_loop: Vec<i32> = (0..buf.len())
545            .filter_map(|i| buf.get_as::<i32>(i))
546            .collect();
547        assert_eq!(via_iter, via_loop);
548    }
549
550    // --- Buffer::write_str ---
551
552    #[test]
553    fn write_str_encodes_string_into_cells() {
554        // "hi" -> cells [104, 105, 0] (h=104, i=105, nul=0)
555        let mut data = vec![0i32; 3];
556        let mut buf = make_buffer(&mut data);
557        assert!(buf.write_str("hi").is_ok());
558        assert_eq!(data[0], i32::from(b'h'));
559        assert_eq!(data[1], i32::from(b'i'));
560        assert_eq!(data[2], 0); // null terminator
561    }
562
563    #[test]
564    fn write_str_empty_string_writes_null_terminator() {
565        let mut data = vec![99i32; 2];
566        let mut buf = make_buffer(&mut data);
567        assert!(buf.write_str("").is_ok());
568        assert_eq!(data[0], 0);
569    }
570
571    #[test]
572    fn write_str_exact_fit_fails() {
573        // A 3-cell buffer cannot hold "abc" (it would need 4: a, b, c, nul)
574        let mut data = vec![0i32; 3];
575        let mut buf = make_buffer(&mut data);
576        assert!(buf.write_str("abc").is_err());
577    }
578
579    // --- UnsizedBuffer::write_str ---
580
581    #[test]
582    fn unsized_write_str_sizes_and_writes() {
583        let mut data = vec![0i32; 5];
584        let ub = make_unsized(&mut data);
585        assert!(ub.write_str(5, "hi").is_ok());
586        assert_eq!(data[0], i32::from(b'h'));
587        assert_eq!(data[1], i32::from(b'i'));
588        assert_eq!(data[2], 0);
589    }
590
591    #[test]
592    fn unsized_write_str_too_long_returns_err() {
593        let mut data = vec![0i32; 3];
594        let ub = make_unsized(&mut data);
595        assert!(ub.write_str(3, "abc").is_err());
596    }
597
598    // --- Adversarial / property tests: indexed access must stay in bounds for
599    //     any index, and sizing must never exceed the declared length. ---
600
601    fn lcg(seed: &mut u64) -> u32 {
602        *seed = seed
603            .wrapping_mul(6_364_136_223_846_793_005)
604            .wrapping_add(1_442_695_040_888_963_407);
605        (*seed >> 33) as u32
606    }
607
608    #[test]
609    fn fuzz_get_set_as_respects_bounds() {
610        let mut seed = 0xDEAD_C0DE_1234_5678u64;
611        for _ in 0..4000 {
612            let len = (lcg(&mut seed) % 10) as usize; // 0..=9 cells
613            let mut data: Vec<i32> = vec![0; len];
614            let mut buf = make_buffer(&mut data);
615            let idx = (lcg(&mut seed) % 32) as usize; // may exceed len
616
617            // get_as: Some iff in bounds, never panics.
618            assert_eq!(buf.get_as::<i32>(idx).is_some(), idx < len);
619            // set_as: writes iff in bounds, returns the same predicate.
620            let wrote = buf.set_as::<i32>(idx, 7);
621            assert_eq!(wrote, idx < len);
622            if wrote {
623                assert_eq!(buf.get_as::<i32>(idx), Some(7));
624            }
625        }
626    }
627
628    #[test]
629    fn fuzz_into_sized_never_exceeds_declared_len() {
630        // With a finite segment bound, the resulting buffer length is always
631        // min(requested, bound, 1 MiB) and never larger than the real data.
632        let mut seed = 0x00C0_FFEE_BADD_F00Du64;
633        for _ in 0..2000 {
634            let cells = (lcg(&mut seed) % 8) as usize + 1;
635            let mut data: Vec<i32> = vec![0; cells];
636            let bound = (lcg(&mut seed) % 16) as usize;
637            let ub = UnsizedBuffer {
638                inner: make_ref(&mut data),
639                max_cells: bound,
640            };
641            let requested = (lcg(&mut seed) % 1000) as usize;
642            let buf = ub.into_sized_buffer(requested);
643            // Exact invariant: clamp to the segment bound and the 1 MiB ceiling.
644            assert_eq!(buf.len(), requested.min(bound).min(1024 * 1024));
645        }
646    }
647}