Skip to main content

samp_sdk/
args.rs

1//! Parsing of arguments from a Pawn native call.
2//!
3//! The AMX delivers arguments in a `*mut i32` pointer where:
4//! - `args[0]` = number of bytes used by the arguments (not the count)
5//! - `args[1..]` = the cells with each argument, in signature order
6//!
7//! This module wraps that indirection and converts each cell to the correct
8//! Rust type via [`AmxCell`].
9
10use crate::amx::Amx;
11use crate::cell::AmxCell;
12
13/// Typed list of arguments for a native function.
14///
15/// Generally the `#[native]` proc macro builds and consumes an `Args` automatically
16/// — call manually only in `raw` natives that receive `(amx, args)` directly.
17pub struct Args<'a> {
18    amx: &'a Amx,
19    params: *const i32,
20    offset: usize,
21}
22
23impl<'a> Args<'a> {
24    /// Builds from the `Amx` and the `args` pointer received by the native.
25    ///
26    /// # Example
27    /// ```
28    /// use samp_sdk::args::Args;
29    /// use samp_sdk::amx::Amx;
30    /// use samp_sdk::cell::AmxString;
31    /// # use samp_sdk::raw::types::AMX;
32    ///
33    /// // native RawNative(const say_that[]);
34    /// extern "C" fn raw_native(amx: *mut AMX, args: *mut i32) -> i32 {
35    ///     # let amx_exports = 0;
36    ///     let amx = Amx::new(amx, amx_exports);
37    ///     let mut args = Args::new(&amx, args);
38    ///     let Some(text) = args.next_arg::<AmxString>() else { return 0 };
39    ///     println!("RawNative: {}", &*text);
40    ///     1
41    /// }
42    /// ```
43    #[must_use]
44    pub fn new(amx: &'a Amx, params: *const i32) -> Args<'a> {
45        Args {
46            amx,
47            params,
48            offset: 0,
49        }
50    }
51
52    /// Next argument in signature order. `None` at the end of the list.
53    pub fn next_arg<T: AmxCell<'a> + 'a>(&mut self) -> Option<T> {
54        let result = self.get(self.offset);
55        self.offset += 1;
56
57        result
58    }
59
60    /// Argument at position `offset` (zero-indexed). `None` if out of bounds.
61    #[must_use]
62    pub fn get<T: AmxCell<'a> + 'a>(&self, offset: usize) -> Option<T> {
63        if offset >= self.count() {
64            return None;
65        }
66
67        unsafe { T::from_raw(self.amx, self.params.add(offset + 1).read()).ok() }
68    }
69
70    /// Resets the [`next_arg`] cursor back to the start of the list.
71    ///
72    /// [`next_arg`]: Args::next_arg
73    pub fn reset(&mut self) {
74        self.offset = 0;
75    }
76
77    /// How many arguments were received.
78    ///
79    /// Reads from `args[0]` (total bytes) and divides by 4 (cell size).
80    /// Negative or zero values return `0` — defense against dirty pointers.
81    #[must_use]
82    pub fn count(&self) -> usize {
83        let raw = unsafe { self.params.read() };
84        if raw <= 0 {
85            return 0;
86        }
87        // `raw > 0` was validated above; `/ 4` keeps it positive.
88        #[allow(clippy::cast_sign_loss)]
89        let count = (raw / 4) as usize;
90        count
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn count_with_zero_returns_zero() {
100        let data: [i32; 1] = [0];
101        let amx = Amx::new(std::ptr::null_mut(), 0);
102        let args = Args::new(&amx, data.as_ptr());
103        assert_eq!(args.count(), 0);
104    }
105
106    #[test]
107    fn count_with_negative_returns_zero() {
108        let data: [i32; 1] = [-8];
109        let amx = Amx::new(std::ptr::null_mut(), 0);
110        let args = Args::new(&amx, data.as_ptr());
111        assert_eq!(args.count(), 0);
112    }
113
114    #[test]
115    fn count_with_valid_args() {
116        // 3 arguments = 12 bytes (3 * 4)
117        let data: [i32; 4] = [12, 100, 200, 300];
118        let amx = Amx::new(std::ptr::null_mut(), 0);
119        let args = Args::new(&amx, data.as_ptr());
120        assert_eq!(args.count(), 3);
121    }
122
123    #[test]
124    fn get_out_of_bounds_returns_none() {
125        let data: [i32; 2] = [4, 42]; // 1 argument
126        let amx = Amx::new(std::ptr::null_mut(), 0);
127        let args = Args::new(&amx, data.as_ptr());
128        // offset == count should return None
129        assert!(args.get::<crate::cell::Ref<i32>>(1).is_none());
130    }
131
132    #[test]
133    fn reset_resets_offset() {
134        let data: [i32; 1] = [0];
135        let amx = Amx::new(std::ptr::null_mut(), 0);
136        let mut args = Args::new(&amx, data.as_ptr());
137        args.offset = 5;
138        args.reset();
139        assert_eq!(args.offset, 0);
140    }
141
142    fn lcg(seed: &mut u64) -> u32 {
143        *seed = seed
144            .wrapping_mul(6_364_136_223_846_793_005)
145            .wrapping_add(1_442_695_040_888_963_407);
146        (*seed >> 33) as u32
147    }
148
149    #[test]
150    fn count_never_negative_or_absurd() {
151        // Whatever the header cell holds (including negatives and huge values),
152        // count() stays non-negative and consistent with `raw / 4`.
153        let mut seed = 0xA5A5_1234_9999_0001u64;
154        for _ in 0..4000 {
155            let raw = lcg(&mut seed) as i32; // full i32 range incl. negatives
156            let data: [i32; 1] = [raw];
157            let amx = Amx::new(std::ptr::null_mut(), 0);
158            let args = Args::new(&amx, data.as_ptr());
159            let count = args.count();
160            if raw <= 0 {
161                assert_eq!(count, 0);
162            } else {
163                assert_eq!(count, (raw / 4) as usize);
164            }
165        }
166    }
167
168    #[test]
169    fn get_beyond_count_is_none() {
170        // A header claiming 2 args: any index >= 2 must be None, never an OOB read.
171        let data: [i32; 3] = [2 * 4, 11, 22];
172        let amx = Amx::new(std::ptr::null_mut(), 0);
173        let args = Args::new(&amx, data.as_ptr());
174        assert!(args.get::<crate::cell::Ref<i32>>(2).is_none());
175        assert!(args.get::<crate::cell::Ref<i32>>(999).is_none());
176    }
177}