Skip to main content

samp_sdk/cell/
string.rs

1//! AMX strings: cell vector with `0` terminator.
2//!
3//! Pawn supports two binary representations:
4//!
5//! - **Unpacked**: 1 character per cell (4x memory usage, default).
6//! - **Packed**: 4 characters packed into each i32 cell (bits 31..24,
7//!   23..16, 15..8, 7..0). The first cell signals the mode if its value
8//!   exceeds [`MAX_UNPACKED`]; the SDK detects it automatically in [`to_bytes`].
9//!
10//! [`to_bytes`]: AmxString::to_bytes
11
12use std::cell::OnceCell;
13use std::fmt;
14use std::ops::Deref;
15
16use super::{AmxCell, Buffer, UnsizedBuffer};
17use crate::amx::Amx;
18#[cfg(feature = "encoding")]
19use crate::encoding;
20use crate::error::AmxResult;
21
22/// Upper bound for the first cell of an unpacked string.
23///
24/// Values above this indicate a packed string (4 chars/cell).
25const MAX_UNPACKED: i32 = 0x00FF_FFFF;
26
27/// Native Pawn string — packed or unpacked.
28///
29/// Implements [`Deref<Target = str>`], so `&str` methods are available
30/// directly, without `.to_string()`:
31///
32/// ```no_run
33/// # use samp_sdk::cell::AmxString;
34/// # use samp_sdk::amx::Amx;
35/// # use samp_sdk::error::AmxResult;
36/// # struct Plugin;
37/// # impl Plugin {
38/// fn greet(&self, _amx: &Amx, name: AmxString) -> AmxResult<bool> {
39///     if name.starts_with("Admin") {
40///         println!("Welcome, {}!", &*name);
41///     }
42///     Ok(true)
43/// }
44/// # }
45/// ```
46///
47/// The decoded version (UTF-8 or Windows-1251 via the `encoding` feature) is
48/// computed on the first `Deref` call and cached — subsequent accesses
49/// return the `&str` without allocation.
50pub struct AmxString<'amx> {
51    inner: Buffer<'amx>,
52    len: usize,
53    decoded: OnceCell<String>,
54}
55
56impl<'amx> AmxString<'amx> {
57    /// Creates an `AmxString` from an allocated buffer and copies `bytes` (1 byte
58    /// per cell) with a trailing `0` terminator.
59    ///
60    /// # Safety
61    /// `buffer` must have at least `bytes.len() + 1` cells and remain
62    /// alive for `'amx`.
63    #[must_use]
64    pub unsafe fn new(mut buffer: Buffer<'amx>, bytes: &[u8]) -> AmxString<'amx> {
65        buffer.as_mut_slice()[..bytes.len()]
66            .iter_mut()
67            .zip(bytes)
68            .for_each(|(cell, &byte)| *cell = i32::from(byte));
69        buffer[bytes.len()] = 0;
70
71        AmxString {
72            len: bytes.len(),
73            inner: buffer,
74            decoded: OnceCell::new(),
75        }
76    }
77
78    /// Constructor for tests/benchmarks — assumes `inner` is already populated.
79    /// Not part of the stable API.
80    #[doc(hidden)]
81    #[must_use]
82    pub fn from_buffer_parts(inner: Buffer<'amx>, len: usize) -> AmxString<'amx> {
83        AmxString {
84            inner,
85            len,
86            decoded: OnceCell::new(),
87        }
88    }
89
90    /// Decodes the cells back into a `Vec<u8>`.
91    ///
92    /// Automatically detects packed (4 chars/cell) or unpacked (1 char/cell)
93    /// from the value of the first cell. Caps the read at 1 MiB to avoid
94    /// uncontrolled allocation if `len` is corrupted.
95    pub fn to_bytes(&self) -> Vec<u8> {
96        const MAX_STRING_LEN: usize = 1024 * 1024;
97        // An empty backing buffer has no first cell to probe for the
98        // packed/unpacked marker — return early instead of indexing `[0]`
99        // (which would panic). Reachable only via a corrupted length.
100        if self.inner.is_empty() {
101            return Vec::new();
102        }
103        let len = self.len.min(MAX_STRING_LEN);
104        let mut vec = Vec::with_capacity(len);
105
106        // packed string
107        if self.inner[0] > MAX_UNPACKED {
108            let cells = self.inner.as_slice();
109            let max_cells = cells.len();
110            let mut cell_idx = 0usize;
111            let mut mark = 3usize;
112            for _ in 0..len {
113                if cell_idx >= max_cells {
114                    break;
115                }
116                // Byte extraction from a packed i32 cell — truncation is intentional.
117                #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
118                let ch = (cells[cell_idx] >> (mark * 8)) as u8;
119                if ch == b'\0' {
120                    break;
121                }
122                vec.push(ch);
123                mark = (mark + 3) % 4;
124                if mark == 3 {
125                    cell_idx += 1;
126                }
127            }
128        } else {
129            for item in self.inner.iter().take(len) {
130                // An unpacked cell holds a single byte; truncation is intentional.
131                #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
132                let byte = *item as u8;
133                vec.push(byte);
134            }
135        }
136
137        vec
138    }
139
140    /// String length in characters (excluding the `0` terminator).
141    pub fn len(&self) -> usize {
142        self.len
143    }
144
145    /// `true` if the string is empty.
146    pub fn is_empty(&self) -> bool {
147        self.len == 0
148    }
149
150    /// Size of the underlying buffer in cells — always `>= len + 1`.
151    pub fn bytes_len(&self) -> usize {
152        self.inner.len()
153    }
154
155    /// Explicit form of the `Deref` to `&str`.
156    ///
157    /// Useful when type inference does not trigger auto-deref (e.g. a generic
158    /// context with `T: AsRef<str>`).
159    pub fn as_str(&self) -> &str {
160        self
161    }
162}
163
164/// Decodes the raw bytes using the configured encoding (UTF-8 by default;
165/// Windows-1251 etc. via the `encoding` feature).
166fn decode_bytes(bytes: &[u8]) -> String {
167    #[cfg(feature = "encoding")]
168    return encoding::get().decode(bytes).0.into_owned();
169
170    #[cfg(not(feature = "encoding"))]
171    return String::from_utf8_lossy(bytes).into_owned();
172}
173
174impl<'amx> AmxCell<'amx> for AmxString<'amx> {
175    fn from_raw(amx: &'amx Amx, cell: i32) -> AmxResult<AmxString<'amx>> {
176        let buffer = UnsizedBuffer::from_raw(amx, cell)?;
177        let ptr = buffer.as_ptr();
178        let str_len = amx.strlen(ptr)?;
179        let buf_len = str_len + 1;
180
181        Ok(AmxString {
182            inner: buffer.into_sized_buffer(buf_len),
183            len: str_len,
184            decoded: OnceCell::new(),
185        })
186    }
187
188    fn as_cell(&self) -> i32 {
189        self.inner.as_cell()
190    }
191}
192
193impl Deref for AmxString<'_> {
194    type Target = str;
195
196    /// Decodes on the first call and caches in [`OnceCell`] — subsequent
197    /// accesses return the same `&str` without allocation.
198    fn deref(&self) -> &str {
199        self.decoded.get_or_init(|| decode_bytes(&self.to_bytes()))
200    }
201}
202
203impl fmt::Display for AmxString<'_> {
204    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
205        fmt.write_str(self)
206    }
207}
208
209impl PartialEq<str> for AmxString<'_> {
210    /// Direct comparison with `&str` (`name == "Admin"`) — no extra allocation.
211    fn eq(&self, other: &str) -> bool {
212        &**self == other
213    }
214}
215
216impl PartialEq<&str> for AmxString<'_> {
217    fn eq(&self, other: &&str) -> bool {
218        &**self == *other
219    }
220}
221
222impl PartialEq<String> for AmxString<'_> {
223    fn eq(&self, other: &String) -> bool {
224        &**self == other.as_str()
225    }
226}
227
228/// Copies a Rust string into an AMX `Buffer` (1 byte per cell, `0`
229/// terminator at the end).
230///
231/// Internal implementation shared by [`Buffer::write_str`] and
232/// [`UnsizedBuffer::write_str`] — the public API goes through them.
233///
234/// [`Buffer::write_str`]: crate::cell::buffer::Buffer::write_str
235/// [`UnsizedBuffer::write_str`]: crate::cell::buffer::UnsizedBuffer::write_str
236///
237/// # Errors
238/// `AmxError::General` if `string` (after encoding) is >= the buffer size.
239pub(crate) fn put_in_buffer(buffer: &mut Buffer, string: &str) -> AmxResult<()> {
240    put_in_buffer_checked(buffer, string).map(|_| ())
241}
242
243/// Same as [`put_in_buffer`], reporting whether the encoding had to substitute
244/// characters it could not represent.
245pub(crate) fn put_in_buffer_checked(buffer: &mut Buffer, string: &str) -> AmxResult<bool> {
246    #[cfg(feature = "encoding")]
247    let (bytes, had_unmappable) = encoding::encode_checked(string);
248
249    // Without the feature the bytes are the string's own UTF-8: nothing to map,
250    // so nothing can be lost.
251    #[cfg(not(feature = "encoding"))]
252    let (bytes, had_unmappable) = (std::borrow::Cow::from(string.as_bytes()), false);
253
254    let bytes = bytes.as_ref();
255
256    if bytes.len() >= buffer.len() {
257        return Err(crate::error::AmxError::General);
258    }
259
260    buffer.as_mut_slice()[..bytes.len()]
261        .iter_mut()
262        .zip(bytes)
263        .for_each(|(cell, &byte)| *cell = i32::from(byte));
264
265    buffer[bytes.len()] = 0;
266
267    Ok(had_unmappable)
268}
269
270#[cfg(test)]
271mod tests {
272    use super::*;
273    use crate::cell::Ref;
274
275    fn make_buffer(data: &mut Vec<i32>) -> Buffer<'_> {
276        let len = data.len();
277        let r = unsafe { Ref::new(0, data.as_mut_ptr()) };
278        Buffer::new(r, len)
279    }
280
281    // --- Unpacked strings (one byte per cell) ---
282
283    #[test]
284    fn new_empty_string() {
285        let mut data = vec![0i32; 4];
286        let buf = make_buffer(&mut data);
287        let s = unsafe { AmxString::new(buf, b"") };
288        assert!(s.is_empty());
289        assert_eq!(s.len(), 0);
290        assert_eq!(&*s, "");
291        assert_eq!(s.to_bytes(), b"");
292    }
293
294    #[test]
295    fn new_ascii_string() {
296        let mut data = vec![0i32; 16];
297        let buf = make_buffer(&mut data);
298        let s = unsafe { AmxString::new(buf, b"hello") };
299        assert_eq!(s.len(), 5);
300        assert_eq!(&*s, "hello");
301        assert_eq!(s.to_bytes(), b"hello");
302        assert!(!s.is_empty());
303    }
304
305    #[test]
306    fn deref_str_enables_string_methods() {
307        let mut data = vec![0i32; 32];
308        let buf = make_buffer(&mut data);
309        let s = unsafe { AmxString::new(buf, b"hello world") };
310        // &str methods without .to_string()
311        assert!(s.contains("world"));
312        assert!(s.starts_with("hello"));
313        assert!(s.ends_with("world"));
314        assert_eq!(s.to_uppercase(), "HELLO WORLD");
315        assert_eq!(s.split_once(' ').unwrap(), ("hello", "world"));
316    }
317
318    #[test]
319    fn deref_is_lazy_and_cached() {
320        let mut data = vec![0i32; 16];
321        let buf = make_buffer(&mut data);
322        let s = unsafe { AmxString::new(buf, b"world") };
323        // OnceCell has not been initialized yet
324        assert!(s.decoded.get().is_none());
325        // First access via Deref -> initializes
326        let _ = &*s;
327        assert!(s.decoded.get().is_some());
328        // Second access -> same pointer (cache hit)
329        let a = s.decoded.get().unwrap().as_ptr();
330        let _ = &*s;
331        let b = s.decoded.get().unwrap().as_ptr();
332        assert_eq!(a, b);
333    }
334
335    #[test]
336    fn display_and_deref_are_consistent() {
337        let mut data = vec![0i32; 16];
338        let buf = make_buffer(&mut data);
339        let s = unsafe { AmxString::new(buf, b"world") };
340        assert_eq!(s.to_string(), "world");
341        assert_eq!(&*s, "world");
342        assert_eq!(format!("{s}"), "world");
343    }
344
345    #[test]
346    fn bytes_len_reflects_buffer_size() {
347        let mut data = vec![0i32; 8];
348        let buf = make_buffer(&mut data);
349        let s = unsafe { AmxString::new(buf, b"abc") };
350        assert_eq!(s.bytes_len(), 8);
351        assert_eq!(s.len(), 3);
352    }
353
354    #[test]
355    fn unpacked_to_bytes_ascii() {
356        let text = b"SA-MP Plugin";
357        let mut data: Vec<i32> = text
358            .iter()
359            .map(|&b| i32::from(b))
360            .chain(std::iter::once(0))
361            .collect();
362        let buf = make_buffer(&mut data);
363        let s = unsafe { AmxString::new(buf, text) };
364        assert_eq!(s.to_bytes(), text);
365    }
366
367    #[test]
368    fn unpacked_single_char() {
369        let mut data = vec![0x41i32, 0];
370        let buf = make_buffer(&mut data);
371        let s = unsafe { AmxString::new(buf, b"A") };
372        assert_eq!(s.len(), 1);
373        assert_eq!(&*s, "A");
374    }
375
376    // --- Packed strings (4 bytes per cell) ---
377    //
378    // Bytes read from each cell: bits[31..24], [23..16], [15..8], [7..0].
379    // "ABCD" -> cell = 0x41424344, next cell = 0x00000000 (null)
380
381    #[test]
382    fn packed_four_chars_one_cell() {
383        let mut data = vec![0x4142_4344i32, 0x0000_0000i32];
384        let buf = make_buffer(&mut data);
385        let s = AmxString::from_buffer_parts(buf, 4);
386        assert_eq!(s.to_bytes(), b"ABCD");
387        assert_eq!(&*s, "ABCD");
388    }
389
390    #[test]
391    fn packed_five_chars_two_cells() {
392        // "ABCDE": 4 chars in cell[0], 1 in cell[1]
393        let mut data = vec![0x4142_4344i32, 0x4500_0000i32, 0x0000_0000i32];
394        let buf = make_buffer(&mut data);
395        let s = AmxString::from_buffer_parts(buf, 5);
396        assert_eq!(s.to_bytes(), b"ABCDE");
397        assert_eq!(&*s, "ABCDE");
398    }
399
400    #[test]
401    fn packed_truncates_at_len() {
402        let mut data = vec![0x4142_4344i32, 0x0000_0000i32];
403        let buf = make_buffer(&mut data);
404        let s = AmxString::from_buffer_parts(buf, 2);
405        assert_eq!(s.to_bytes(), b"AB");
406    }
407
408    #[test]
409    fn packed_stops_at_null_byte() {
410        // "AB\0D" -> stops at \0, returns "AB"
411        let mut data = vec![0x4142_0044i32, 0x0000_0000i32];
412        let buf = make_buffer(&mut data);
413        let s = AmxString::from_buffer_parts(buf, 4);
414        assert_eq!(s.to_bytes(), b"AB");
415    }
416
417    // --- as_str ---
418
419    #[test]
420    fn as_str_returns_decoded() {
421        let mut data = vec![0i32; 16];
422        let buf = make_buffer(&mut data);
423        let s = unsafe { AmxString::new(buf, b"hello") };
424        assert_eq!(s.as_str(), "hello");
425    }
426
427    #[test]
428    fn as_str_and_deref_are_same_pointer() {
429        let mut data = vec![0i32; 16];
430        let buf = make_buffer(&mut data);
431        let s = unsafe { AmxString::new(buf, b"rust") };
432        // Both trigger the same OnceCell — same &str pointer
433        let a: &str = s.as_str();
434        let b: &str = &s;
435        assert_eq!(a.as_ptr(), b.as_ptr());
436    }
437
438    // --- PartialEq ---
439
440    #[test]
441    fn partial_eq_str_literal() {
442        let mut data = vec![0i32; 16];
443        let buf = make_buffer(&mut data);
444        let s = unsafe { AmxString::new(buf, b"Admin") };
445        assert!(s == "Admin");
446        assert!(s != "admin");
447    }
448
449    #[test]
450    fn partial_eq_ref_str() {
451        let mut data = vec![0i32; 16];
452        let buf = make_buffer(&mut data);
453        let s = unsafe { AmxString::new(buf, b"samp") };
454        let key: &str = "samp";
455        assert!(s == key);
456    }
457
458    #[test]
459    fn partial_eq_string() {
460        let mut data = vec![0i32; 16];
461        let buf = make_buffer(&mut data);
462        let s = unsafe { AmxString::new(buf, b"plugin") };
463        let owned_match: String = "plugin".to_string();
464        let owned_other: String = "other".to_string();
465        assert!(s == owned_match);
466        assert!(s != owned_other);
467    }
468
469    #[test]
470    fn partial_eq_empty() {
471        let mut data = vec![0i32; 4];
472        let buf = make_buffer(&mut data);
473        let s = unsafe { AmxString::new(buf, b"") };
474        assert!(s.is_empty());
475        assert!(s != "x");
476    }
477
478    // --- put_in_buffer ---
479
480    #[test]
481    fn put_in_buffer_writes_correctly() {
482        let mut data = vec![0i32; 16];
483        let mut buf = make_buffer(&mut data);
484        put_in_buffer(&mut buf, "hello").unwrap();
485        assert_eq!(buf[0], i32::from(b'h'));
486        assert_eq!(buf[4], i32::from(b'o'));
487        assert_eq!(buf[5], 0);
488    }
489
490    #[test]
491    fn put_in_buffer_exact_fit_fails() {
492        let mut data = vec![0i32; 5];
493        let mut buf = make_buffer(&mut data);
494        assert!(put_in_buffer(&mut buf, "hello").is_err());
495    }
496
497    #[test]
498    fn put_in_buffer_empty_string() {
499        let mut data = vec![0i32; 4];
500        let mut buf = make_buffer(&mut data);
501        put_in_buffer(&mut buf, "").unwrap();
502        assert_eq!(buf[0], 0);
503    }
504
505    // --- Adversarial / property tests: decoding must never panic or overrun,
506    //     whatever garbage (or a corrupted length) the script hands over. ---
507
508    /// Tiny deterministic LCG — dependency-free pseudo-randomness for fuzzing.
509    fn lcg(seed: &mut u64) -> u32 {
510        *seed = seed
511            .wrapping_mul(6_364_136_223_846_793_005)
512            .wrapping_add(1_442_695_040_888_963_407);
513        (*seed >> 33) as u32
514    }
515
516    #[test]
517    fn to_bytes_declared_len_larger_than_buffer_is_bounded() {
518        // A corrupted length far beyond the backing cells must read only what
519        // exists, never past the slice.
520        let mut data = vec![0x41i32, 0x42, 0x43]; // "ABC", no terminator
521        let buf = make_buffer(&mut data);
522        let s = AmxString::from_buffer_parts(buf, 9999);
523        let bytes = s.to_bytes();
524        assert!(bytes.len() <= 3, "read past the backing buffer: {bytes:?}");
525    }
526
527    #[test]
528    fn to_bytes_non_utf8_decodes_lossy_without_panic() {
529        // 0xFF is not valid UTF-8; decoding must produce replacement chars,
530        // never panic.
531        let mut data = vec![0xFFi32, 0xFE, 0x41, 0];
532        let buf = make_buffer(&mut data);
533        let s = AmxString::from_buffer_parts(buf, 3);
534        let _ = &*s; // triggers decode
535        assert!(!s.is_empty());
536    }
537
538    #[test]
539    fn fuzz_decode_never_panics() {
540        // Random cell contents + a possibly-corrupted declared length, for both
541        // packed and unpacked interpretations. The contract: decoding is total
542        // (no panic, no overrun) no matter what the VM memory holds.
543        let mut seed = 0x0BAD_F00D_DEAD_BEEFu64;
544        for _ in 0..4000 {
545            let cells = (lcg(&mut seed) % 12) as usize + 1; // 1..=12 cells
546            let mut data: Vec<i32> = (0..cells).map(|_| lcg(&mut seed) as i32).collect();
547            // Declared length may be anything, including far beyond `cells`.
548            let declared = (lcg(&mut seed) % 64) as usize;
549            let buf = make_buffer(&mut data);
550            let s = AmxString::from_buffer_parts(buf, declared);
551
552            let bytes = s.to_bytes();
553            assert!(bytes.len() <= 1024 * 1024);
554            // Deref decodes and caches — must also be total.
555            let decoded = &*s;
556            assert!(decoded.len() <= bytes.len().max(4 * bytes.len() + 4));
557        }
558    }
559}