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        let len = self.len.min(MAX_STRING_LEN);
98        let mut vec = Vec::with_capacity(len);
99
100        // packed string
101        if self.inner[0] > MAX_UNPACKED {
102            let cells = self.inner.as_slice();
103            let max_cells = cells.len();
104            let mut cell_idx = 0usize;
105            let mut mark = 3usize;
106            for _ in 0..len {
107                if cell_idx >= max_cells {
108                    break;
109                }
110                // Byte extraction from a packed i32 cell — truncation is intentional.
111                #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
112                let ch = (cells[cell_idx] >> (mark * 8)) as u8;
113                if ch == b'\0' {
114                    break;
115                }
116                vec.push(ch);
117                mark = (mark + 3) % 4;
118                if mark == 3 {
119                    cell_idx += 1;
120                }
121            }
122        } else {
123            for item in self.inner.iter().take(len) {
124                // An unpacked cell holds a single byte; truncation is intentional.
125                #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
126                let byte = *item as u8;
127                vec.push(byte);
128            }
129        }
130
131        vec
132    }
133
134    /// String length in characters (excluding the `0` terminator).
135    pub fn len(&self) -> usize {
136        self.len
137    }
138
139    /// `true` if the string is empty.
140    pub fn is_empty(&self) -> bool {
141        self.len == 0
142    }
143
144    /// Size of the underlying buffer in cells — always `>= len + 1`.
145    pub fn bytes_len(&self) -> usize {
146        self.inner.len()
147    }
148
149    /// Explicit form of the `Deref` to `&str`.
150    ///
151    /// Useful when type inference does not trigger auto-deref (e.g. a generic
152    /// context with `T: AsRef<str>`).
153    pub fn as_str(&self) -> &str {
154        self
155    }
156}
157
158/// Decodes the raw bytes using the configured encoding (UTF-8 by default;
159/// Windows-1251 etc. via the `encoding` feature).
160fn decode_bytes(bytes: &[u8]) -> String {
161    #[cfg(feature = "encoding")]
162    return encoding::get().decode(bytes).0.into_owned();
163
164    #[cfg(not(feature = "encoding"))]
165    return String::from_utf8_lossy(bytes).into_owned();
166}
167
168impl<'amx> AmxCell<'amx> for AmxString<'amx> {
169    fn from_raw(amx: &'amx Amx, cell: i32) -> AmxResult<AmxString<'amx>> {
170        let buffer = UnsizedBuffer::from_raw(amx, cell)?;
171        let ptr = buffer.as_ptr();
172        let str_len = amx.strlen(ptr)?;
173        let buf_len = str_len + 1;
174
175        Ok(AmxString {
176            inner: buffer.into_sized_buffer(buf_len),
177            len: str_len,
178            decoded: OnceCell::new(),
179        })
180    }
181
182    fn as_cell(&self) -> i32 {
183        self.inner.as_cell()
184    }
185}
186
187impl Deref for AmxString<'_> {
188    type Target = str;
189
190    /// Decodes on the first call and caches in [`OnceCell`] — subsequent
191    /// accesses return the same `&str` without allocation.
192    fn deref(&self) -> &str {
193        self.decoded.get_or_init(|| decode_bytes(&self.to_bytes()))
194    }
195}
196
197impl fmt::Display for AmxString<'_> {
198    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
199        fmt.write_str(self)
200    }
201}
202
203impl PartialEq<str> for AmxString<'_> {
204    /// Direct comparison with `&str` (`name == "Admin"`) — no extra allocation.
205    fn eq(&self, other: &str) -> bool {
206        &**self == other
207    }
208}
209
210impl PartialEq<&str> for AmxString<'_> {
211    fn eq(&self, other: &&str) -> bool {
212        &**self == *other
213    }
214}
215
216impl PartialEq<String> for AmxString<'_> {
217    fn eq(&self, other: &String) -> bool {
218        &**self == other.as_str()
219    }
220}
221
222/// Copies a Rust string into an AMX `Buffer` (1 byte per cell, `0`
223/// terminator at the end).
224///
225/// Internal implementation shared by [`Buffer::write_str`] and
226/// [`UnsizedBuffer::write_str`] — the public API goes through them.
227///
228/// [`Buffer::write_str`]: crate::cell::buffer::Buffer::write_str
229/// [`UnsizedBuffer::write_str`]: crate::cell::buffer::UnsizedBuffer::write_str
230///
231/// # Errors
232/// `AmxError::General` if `string` (after encoding) is >= the buffer size.
233pub(crate) fn put_in_buffer(buffer: &mut Buffer, string: &str) -> AmxResult<()> {
234    #[cfg(feature = "encoding")]
235    let bytes = encoding::get().encode(string).0;
236
237    #[cfg(not(feature = "encoding"))]
238    let bytes = std::borrow::Cow::from(string.as_bytes());
239
240    let bytes = bytes.as_ref();
241
242    if bytes.len() >= buffer.len() {
243        return Err(crate::error::AmxError::General);
244    }
245
246    buffer.as_mut_slice()[..bytes.len()]
247        .iter_mut()
248        .zip(bytes)
249        .for_each(|(cell, &byte)| *cell = i32::from(byte));
250
251    buffer[bytes.len()] = 0;
252
253    Ok(())
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259    use crate::cell::Ref;
260
261    fn make_buffer(data: &mut Vec<i32>) -> Buffer<'_> {
262        let len = data.len();
263        let r = unsafe { Ref::new(0, data.as_mut_ptr()) };
264        Buffer::new(r, len)
265    }
266
267    // --- Unpacked strings (one byte per cell) ---
268
269    #[test]
270    fn new_empty_string() {
271        let mut data = vec![0i32; 4];
272        let buf = make_buffer(&mut data);
273        let s = unsafe { AmxString::new(buf, b"") };
274        assert!(s.is_empty());
275        assert_eq!(s.len(), 0);
276        assert_eq!(&*s, "");
277        assert_eq!(s.to_bytes(), b"");
278    }
279
280    #[test]
281    fn new_ascii_string() {
282        let mut data = vec![0i32; 16];
283        let buf = make_buffer(&mut data);
284        let s = unsafe { AmxString::new(buf, b"hello") };
285        assert_eq!(s.len(), 5);
286        assert_eq!(&*s, "hello");
287        assert_eq!(s.to_bytes(), b"hello");
288        assert!(!s.is_empty());
289    }
290
291    #[test]
292    fn deref_str_enables_string_methods() {
293        let mut data = vec![0i32; 32];
294        let buf = make_buffer(&mut data);
295        let s = unsafe { AmxString::new(buf, b"hello world") };
296        // &str methods without .to_string()
297        assert!(s.contains("world"));
298        assert!(s.starts_with("hello"));
299        assert!(s.ends_with("world"));
300        assert_eq!(s.to_uppercase(), "HELLO WORLD");
301        assert_eq!(s.split_once(' ').unwrap(), ("hello", "world"));
302    }
303
304    #[test]
305    fn deref_is_lazy_and_cached() {
306        let mut data = vec![0i32; 16];
307        let buf = make_buffer(&mut data);
308        let s = unsafe { AmxString::new(buf, b"world") };
309        // OnceCell has not been initialized yet
310        assert!(s.decoded.get().is_none());
311        // First access via Deref -> initializes
312        let _ = &*s;
313        assert!(s.decoded.get().is_some());
314        // Second access -> same pointer (cache hit)
315        let a = s.decoded.get().unwrap().as_ptr();
316        let _ = &*s;
317        let b = s.decoded.get().unwrap().as_ptr();
318        assert_eq!(a, b);
319    }
320
321    #[test]
322    fn display_and_deref_are_consistent() {
323        let mut data = vec![0i32; 16];
324        let buf = make_buffer(&mut data);
325        let s = unsafe { AmxString::new(buf, b"world") };
326        assert_eq!(s.to_string(), "world");
327        assert_eq!(&*s, "world");
328        assert_eq!(format!("{s}"), "world");
329    }
330
331    #[test]
332    fn bytes_len_reflects_buffer_size() {
333        let mut data = vec![0i32; 8];
334        let buf = make_buffer(&mut data);
335        let s = unsafe { AmxString::new(buf, b"abc") };
336        assert_eq!(s.bytes_len(), 8);
337        assert_eq!(s.len(), 3);
338    }
339
340    #[test]
341    fn unpacked_to_bytes_ascii() {
342        let text = b"SA-MP Plugin";
343        let mut data: Vec<i32> = text
344            .iter()
345            .map(|&b| i32::from(b))
346            .chain(std::iter::once(0))
347            .collect();
348        let buf = make_buffer(&mut data);
349        let s = unsafe { AmxString::new(buf, text) };
350        assert_eq!(s.to_bytes(), text);
351    }
352
353    #[test]
354    fn unpacked_single_char() {
355        let mut data = vec![0x41i32, 0];
356        let buf = make_buffer(&mut data);
357        let s = unsafe { AmxString::new(buf, b"A") };
358        assert_eq!(s.len(), 1);
359        assert_eq!(&*s, "A");
360    }
361
362    // --- Packed strings (4 bytes per cell) ---
363    //
364    // Bytes read from each cell: bits[31..24], [23..16], [15..8], [7..0].
365    // "ABCD" -> cell = 0x41424344, next cell = 0x00000000 (null)
366
367    #[test]
368    fn packed_four_chars_one_cell() {
369        let mut data = vec![0x4142_4344i32, 0x0000_0000i32];
370        let buf = make_buffer(&mut data);
371        let s = AmxString::from_buffer_parts(buf, 4);
372        assert_eq!(s.to_bytes(), b"ABCD");
373        assert_eq!(&*s, "ABCD");
374    }
375
376    #[test]
377    fn packed_five_chars_two_cells() {
378        // "ABCDE": 4 chars in cell[0], 1 in cell[1]
379        let mut data = vec![0x4142_4344i32, 0x4500_0000i32, 0x0000_0000i32];
380        let buf = make_buffer(&mut data);
381        let s = AmxString::from_buffer_parts(buf, 5);
382        assert_eq!(s.to_bytes(), b"ABCDE");
383        assert_eq!(&*s, "ABCDE");
384    }
385
386    #[test]
387    fn packed_truncates_at_len() {
388        let mut data = vec![0x4142_4344i32, 0x0000_0000i32];
389        let buf = make_buffer(&mut data);
390        let s = AmxString::from_buffer_parts(buf, 2);
391        assert_eq!(s.to_bytes(), b"AB");
392    }
393
394    #[test]
395    fn packed_stops_at_null_byte() {
396        // "AB\0D" -> stops at \0, returns "AB"
397        let mut data = vec![0x4142_0044i32, 0x0000_0000i32];
398        let buf = make_buffer(&mut data);
399        let s = AmxString::from_buffer_parts(buf, 4);
400        assert_eq!(s.to_bytes(), b"AB");
401    }
402
403    // --- as_str ---
404
405    #[test]
406    fn as_str_returns_decoded() {
407        let mut data = vec![0i32; 16];
408        let buf = make_buffer(&mut data);
409        let s = unsafe { AmxString::new(buf, b"hello") };
410        assert_eq!(s.as_str(), "hello");
411    }
412
413    #[test]
414    fn as_str_and_deref_are_same_pointer() {
415        let mut data = vec![0i32; 16];
416        let buf = make_buffer(&mut data);
417        let s = unsafe { AmxString::new(buf, b"rust") };
418        // Both trigger the same OnceCell — same &str pointer
419        let a: &str = s.as_str();
420        let b: &str = &s;
421        assert_eq!(a.as_ptr(), b.as_ptr());
422    }
423
424    // --- PartialEq ---
425
426    #[test]
427    fn partial_eq_str_literal() {
428        let mut data = vec![0i32; 16];
429        let buf = make_buffer(&mut data);
430        let s = unsafe { AmxString::new(buf, b"Admin") };
431        assert!(s == "Admin");
432        assert!(s != "admin");
433    }
434
435    #[test]
436    fn partial_eq_ref_str() {
437        let mut data = vec![0i32; 16];
438        let buf = make_buffer(&mut data);
439        let s = unsafe { AmxString::new(buf, b"samp") };
440        let key: &str = "samp";
441        assert!(s == key);
442    }
443
444    #[test]
445    fn partial_eq_string() {
446        let mut data = vec![0i32; 16];
447        let buf = make_buffer(&mut data);
448        let s = unsafe { AmxString::new(buf, b"plugin") };
449        let owned_match: String = "plugin".to_string();
450        let owned_other: String = "other".to_string();
451        assert!(s == owned_match);
452        assert!(s != owned_other);
453    }
454
455    #[test]
456    fn partial_eq_empty() {
457        let mut data = vec![0i32; 4];
458        let buf = make_buffer(&mut data);
459        let s = unsafe { AmxString::new(buf, b"") };
460        assert!(s.is_empty());
461        assert!(s != "x");
462    }
463
464    // --- put_in_buffer ---
465
466    #[test]
467    fn put_in_buffer_writes_correctly() {
468        let mut data = vec![0i32; 16];
469        let mut buf = make_buffer(&mut data);
470        put_in_buffer(&mut buf, "hello").unwrap();
471        assert_eq!(buf[0], i32::from(b'h'));
472        assert_eq!(buf[4], i32::from(b'o'));
473        assert_eq!(buf[5], 0);
474    }
475
476    #[test]
477    fn put_in_buffer_exact_fit_fails() {
478        let mut data = vec![0i32; 5];
479        let mut buf = make_buffer(&mut data);
480        assert!(put_in_buffer(&mut buf, "hello").is_err());
481    }
482
483    #[test]
484    fn put_in_buffer_empty_string() {
485        let mut data = vec![0i32; 4];
486        let mut buf = make_buffer(&mut data);
487        put_in_buffer(&mut buf, "").unwrap();
488        assert_eq!(buf[0], 0);
489    }
490}