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    #[cfg(feature = "encoding")]
241    let bytes = encoding::get().encode(string).0;
242
243    #[cfg(not(feature = "encoding"))]
244    let bytes = std::borrow::Cow::from(string.as_bytes());
245
246    let bytes = bytes.as_ref();
247
248    if bytes.len() >= buffer.len() {
249        return Err(crate::error::AmxError::General);
250    }
251
252    buffer.as_mut_slice()[..bytes.len()]
253        .iter_mut()
254        .zip(bytes)
255        .for_each(|(cell, &byte)| *cell = i32::from(byte));
256
257    buffer[bytes.len()] = 0;
258
259    Ok(())
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265    use crate::cell::Ref;
266
267    fn make_buffer(data: &mut Vec<i32>) -> Buffer<'_> {
268        let len = data.len();
269        let r = unsafe { Ref::new(0, data.as_mut_ptr()) };
270        Buffer::new(r, len)
271    }
272
273    // --- Unpacked strings (one byte per cell) ---
274
275    #[test]
276    fn new_empty_string() {
277        let mut data = vec![0i32; 4];
278        let buf = make_buffer(&mut data);
279        let s = unsafe { AmxString::new(buf, b"") };
280        assert!(s.is_empty());
281        assert_eq!(s.len(), 0);
282        assert_eq!(&*s, "");
283        assert_eq!(s.to_bytes(), b"");
284    }
285
286    #[test]
287    fn new_ascii_string() {
288        let mut data = vec![0i32; 16];
289        let buf = make_buffer(&mut data);
290        let s = unsafe { AmxString::new(buf, b"hello") };
291        assert_eq!(s.len(), 5);
292        assert_eq!(&*s, "hello");
293        assert_eq!(s.to_bytes(), b"hello");
294        assert!(!s.is_empty());
295    }
296
297    #[test]
298    fn deref_str_enables_string_methods() {
299        let mut data = vec![0i32; 32];
300        let buf = make_buffer(&mut data);
301        let s = unsafe { AmxString::new(buf, b"hello world") };
302        // &str methods without .to_string()
303        assert!(s.contains("world"));
304        assert!(s.starts_with("hello"));
305        assert!(s.ends_with("world"));
306        assert_eq!(s.to_uppercase(), "HELLO WORLD");
307        assert_eq!(s.split_once(' ').unwrap(), ("hello", "world"));
308    }
309
310    #[test]
311    fn deref_is_lazy_and_cached() {
312        let mut data = vec![0i32; 16];
313        let buf = make_buffer(&mut data);
314        let s = unsafe { AmxString::new(buf, b"world") };
315        // OnceCell has not been initialized yet
316        assert!(s.decoded.get().is_none());
317        // First access via Deref -> initializes
318        let _ = &*s;
319        assert!(s.decoded.get().is_some());
320        // Second access -> same pointer (cache hit)
321        let a = s.decoded.get().unwrap().as_ptr();
322        let _ = &*s;
323        let b = s.decoded.get().unwrap().as_ptr();
324        assert_eq!(a, b);
325    }
326
327    #[test]
328    fn display_and_deref_are_consistent() {
329        let mut data = vec![0i32; 16];
330        let buf = make_buffer(&mut data);
331        let s = unsafe { AmxString::new(buf, b"world") };
332        assert_eq!(s.to_string(), "world");
333        assert_eq!(&*s, "world");
334        assert_eq!(format!("{s}"), "world");
335    }
336
337    #[test]
338    fn bytes_len_reflects_buffer_size() {
339        let mut data = vec![0i32; 8];
340        let buf = make_buffer(&mut data);
341        let s = unsafe { AmxString::new(buf, b"abc") };
342        assert_eq!(s.bytes_len(), 8);
343        assert_eq!(s.len(), 3);
344    }
345
346    #[test]
347    fn unpacked_to_bytes_ascii() {
348        let text = b"SA-MP Plugin";
349        let mut data: Vec<i32> = text
350            .iter()
351            .map(|&b| i32::from(b))
352            .chain(std::iter::once(0))
353            .collect();
354        let buf = make_buffer(&mut data);
355        let s = unsafe { AmxString::new(buf, text) };
356        assert_eq!(s.to_bytes(), text);
357    }
358
359    #[test]
360    fn unpacked_single_char() {
361        let mut data = vec![0x41i32, 0];
362        let buf = make_buffer(&mut data);
363        let s = unsafe { AmxString::new(buf, b"A") };
364        assert_eq!(s.len(), 1);
365        assert_eq!(&*s, "A");
366    }
367
368    // --- Packed strings (4 bytes per cell) ---
369    //
370    // Bytes read from each cell: bits[31..24], [23..16], [15..8], [7..0].
371    // "ABCD" -> cell = 0x41424344, next cell = 0x00000000 (null)
372
373    #[test]
374    fn packed_four_chars_one_cell() {
375        let mut data = vec![0x4142_4344i32, 0x0000_0000i32];
376        let buf = make_buffer(&mut data);
377        let s = AmxString::from_buffer_parts(buf, 4);
378        assert_eq!(s.to_bytes(), b"ABCD");
379        assert_eq!(&*s, "ABCD");
380    }
381
382    #[test]
383    fn packed_five_chars_two_cells() {
384        // "ABCDE": 4 chars in cell[0], 1 in cell[1]
385        let mut data = vec![0x4142_4344i32, 0x4500_0000i32, 0x0000_0000i32];
386        let buf = make_buffer(&mut data);
387        let s = AmxString::from_buffer_parts(buf, 5);
388        assert_eq!(s.to_bytes(), b"ABCDE");
389        assert_eq!(&*s, "ABCDE");
390    }
391
392    #[test]
393    fn packed_truncates_at_len() {
394        let mut data = vec![0x4142_4344i32, 0x0000_0000i32];
395        let buf = make_buffer(&mut data);
396        let s = AmxString::from_buffer_parts(buf, 2);
397        assert_eq!(s.to_bytes(), b"AB");
398    }
399
400    #[test]
401    fn packed_stops_at_null_byte() {
402        // "AB\0D" -> stops at \0, returns "AB"
403        let mut data = vec![0x4142_0044i32, 0x0000_0000i32];
404        let buf = make_buffer(&mut data);
405        let s = AmxString::from_buffer_parts(buf, 4);
406        assert_eq!(s.to_bytes(), b"AB");
407    }
408
409    // --- as_str ---
410
411    #[test]
412    fn as_str_returns_decoded() {
413        let mut data = vec![0i32; 16];
414        let buf = make_buffer(&mut data);
415        let s = unsafe { AmxString::new(buf, b"hello") };
416        assert_eq!(s.as_str(), "hello");
417    }
418
419    #[test]
420    fn as_str_and_deref_are_same_pointer() {
421        let mut data = vec![0i32; 16];
422        let buf = make_buffer(&mut data);
423        let s = unsafe { AmxString::new(buf, b"rust") };
424        // Both trigger the same OnceCell — same &str pointer
425        let a: &str = s.as_str();
426        let b: &str = &s;
427        assert_eq!(a.as_ptr(), b.as_ptr());
428    }
429
430    // --- PartialEq ---
431
432    #[test]
433    fn partial_eq_str_literal() {
434        let mut data = vec![0i32; 16];
435        let buf = make_buffer(&mut data);
436        let s = unsafe { AmxString::new(buf, b"Admin") };
437        assert!(s == "Admin");
438        assert!(s != "admin");
439    }
440
441    #[test]
442    fn partial_eq_ref_str() {
443        let mut data = vec![0i32; 16];
444        let buf = make_buffer(&mut data);
445        let s = unsafe { AmxString::new(buf, b"samp") };
446        let key: &str = "samp";
447        assert!(s == key);
448    }
449
450    #[test]
451    fn partial_eq_string() {
452        let mut data = vec![0i32; 16];
453        let buf = make_buffer(&mut data);
454        let s = unsafe { AmxString::new(buf, b"plugin") };
455        let owned_match: String = "plugin".to_string();
456        let owned_other: String = "other".to_string();
457        assert!(s == owned_match);
458        assert!(s != owned_other);
459    }
460
461    #[test]
462    fn partial_eq_empty() {
463        let mut data = vec![0i32; 4];
464        let buf = make_buffer(&mut data);
465        let s = unsafe { AmxString::new(buf, b"") };
466        assert!(s.is_empty());
467        assert!(s != "x");
468    }
469
470    // --- put_in_buffer ---
471
472    #[test]
473    fn put_in_buffer_writes_correctly() {
474        let mut data = vec![0i32; 16];
475        let mut buf = make_buffer(&mut data);
476        put_in_buffer(&mut buf, "hello").unwrap();
477        assert_eq!(buf[0], i32::from(b'h'));
478        assert_eq!(buf[4], i32::from(b'o'));
479        assert_eq!(buf[5], 0);
480    }
481
482    #[test]
483    fn put_in_buffer_exact_fit_fails() {
484        let mut data = vec![0i32; 5];
485        let mut buf = make_buffer(&mut data);
486        assert!(put_in_buffer(&mut buf, "hello").is_err());
487    }
488
489    #[test]
490    fn put_in_buffer_empty_string() {
491        let mut data = vec![0i32; 4];
492        let mut buf = make_buffer(&mut data);
493        put_in_buffer(&mut buf, "").unwrap();
494        assert_eq!(buf[0], 0);
495    }
496
497    // --- Adversarial / property tests: decoding must never panic or overrun,
498    //     whatever garbage (or a corrupted length) the script hands over. ---
499
500    /// Tiny deterministic LCG — dependency-free pseudo-randomness for fuzzing.
501    fn lcg(seed: &mut u64) -> u32 {
502        *seed = seed
503            .wrapping_mul(6_364_136_223_846_793_005)
504            .wrapping_add(1_442_695_040_888_963_407);
505        (*seed >> 33) as u32
506    }
507
508    #[test]
509    fn to_bytes_declared_len_larger_than_buffer_is_bounded() {
510        // A corrupted length far beyond the backing cells must read only what
511        // exists, never past the slice.
512        let mut data = vec![0x41i32, 0x42, 0x43]; // "ABC", no terminator
513        let buf = make_buffer(&mut data);
514        let s = AmxString::from_buffer_parts(buf, 9999);
515        let bytes = s.to_bytes();
516        assert!(bytes.len() <= 3, "read past the backing buffer: {bytes:?}");
517    }
518
519    #[test]
520    fn to_bytes_non_utf8_decodes_lossy_without_panic() {
521        // 0xFF is not valid UTF-8; decoding must produce replacement chars,
522        // never panic.
523        let mut data = vec![0xFFi32, 0xFE, 0x41, 0];
524        let buf = make_buffer(&mut data);
525        let s = AmxString::from_buffer_parts(buf, 3);
526        let _ = &*s; // triggers decode
527        assert!(!s.is_empty());
528    }
529
530    #[test]
531    fn fuzz_decode_never_panics() {
532        // Random cell contents + a possibly-corrupted declared length, for both
533        // packed and unpacked interpretations. The contract: decoding is total
534        // (no panic, no overrun) no matter what the VM memory holds.
535        let mut seed = 0x0BAD_F00D_DEAD_BEEFu64;
536        for _ in 0..4000 {
537            let cells = (lcg(&mut seed) % 12) as usize + 1; // 1..=12 cells
538            let mut data: Vec<i32> = (0..cells).map(|_| lcg(&mut seed) as i32).collect();
539            // Declared length may be anything, including far beyond `cells`.
540            let declared = (lcg(&mut seed) % 64) as usize;
541            let buf = make_buffer(&mut data);
542            let s = AmxString::from_buffer_parts(buf, declared);
543
544            let bytes = s.to_bytes();
545            assert!(bytes.len() <= 1024 * 1024);
546            // Deref decodes and caches — must also be total.
547            let decoded = &*s;
548            assert!(decoded.len() <= bytes.len().max(4 * bytes.len() + 4));
549        }
550    }
551}