Skip to main content

kevy_resp/
request.rs

1//! Request-side parser: turns a byte stream from a client into an [`Argv`].
2//!
3//! Handles the two RESP2 request forms — `*N\r\n$L\r\n…` multi-bulk (the
4//! normal client encoding) and the inline form (whitespace-separated, a
5//! convenience for raw-typed PING / DEBUG / etc). Parsing is incremental:
6//! returning `Ok(None)` asks the caller to read more bytes and retry.
7
8use crate::argv::{Argv, Command};
9use crate::error::ProtocolError;
10
11/// Upper bound on a multi-bulk element count, matching Redis's
12/// `PROTO_MAX_MULTIBULK_LEN`. A request declaring more elements than
13/// this is rejected before any capacity is reserved — without the
14/// cap a ~20-byte `*9999999999\r\n` frame would drive a
15/// multi-gigabyte `Vec::with_capacity` and abort the process
16/// (`handle_alloc_error`). Untrusted, so this is a hard protocol
17/// limit, not a tunable.
18pub const MAX_MULTIBULK_LEN: usize = 1024 * 1024;
19
20/// Upper bound on a single bulk-string length, matching Redis's
21/// 512 MiB `proto-max-bulk-len` default. A header declaring a longer
22/// string is rejected at parse time, so neither the parser's
23/// capacity reservation nor the connection's input buffer can be
24/// driven unbounded by a declared-but-never-sent length.
25pub const MAX_BULK_LEN: usize = 512 * 1024 * 1024;
26
27/// Attempt to parse one command from the front of `buf`.
28///
29/// - `Ok(Some((cmd, consumed)))` — a full command; `consumed` bytes may be dropped.
30/// - `Ok(None)` — need more bytes; call again after reading more.
31/// - `Err(_)` — the stream is corrupt; the caller should reply with an error
32///   and close the connection.
33///
34/// This is the convenience form that allocates a fresh `Argv` per call. The
35/// reactor's hot path uses [`parse_command_into`] with a reused scratch
36/// `Argv` to keep per-cmd malloc rate at 0.
37pub fn parse_command(buf: &[u8]) -> Result<Option<(Command, usize)>, ProtocolError> {
38    let mut argv = Argv::default();
39    match parse_command_into(buf, &mut argv)? {
40        Some(consumed) => Ok(Some((argv, consumed))),
41        None => Ok(None),
42    }
43}
44
45/// Same as [`parse_command`], but writes into a caller-provided scratch
46/// `Argv` instead of allocating a new one each call. The reactor stores one
47/// `Argv` per shard and reuses it for every cmd on the local hot path; the
48/// internal `Vec<u8>` + `Vec<u32>` capacities amortise to zero allocations
49/// per command after the first few cmds warm them.
50///
51/// `dst` is cleared at the start of every call; on `Ok(None)` and `Err`, `dst`
52/// is left empty (so the caller doesn't see partial state).
53pub fn parse_command_into(buf: &[u8], dst: &mut Argv) -> Result<Option<usize>, ProtocolError> {
54    dst.clear();
55    if buf.is_empty() {
56        return Ok(None);
57    }
58    if buf[0] == b'*' {
59        parse_multibulk_into(buf, dst)
60    } else {
61        parse_inline_into(buf, dst)
62    }
63}
64
65// Signature mirrors `parse_multibulk_into` so `parse_command_into` can dispatch
66// on the leading byte without converting between Result and Option shapes —
67// the inline path can't actually fail, but the Result wrap is a price we pay
68// for arm symmetry, not a hidden error path.
69#[allow(clippy::unnecessary_wraps)]
70fn parse_inline_into(buf: &[u8], dst: &mut Argv) -> Result<Option<usize>, ProtocolError> {
71    let Some(eol) = find_crlf(buf, 0) else {
72        return Ok(None);
73    };
74    let line = &buf[..eol];
75    for tok in line
76        .split(u8::is_ascii_whitespace)
77        .filter(|s| !s.is_empty())
78    {
79        dst.push(tok);
80    }
81    Ok(Some(eol + 2))
82}
83
84/// Validate the multi-bulk frame is fully present and report `(end_pos,
85/// total_arg_bytes)` if so. `start_pos` is the offset of the first `$`
86/// after the `*N\r\n` header. `Ok(None)` = need more bytes; `Err` = malformed.
87pub(crate) fn validate_multibulk_frame(
88    buf: &[u8],
89    start_pos: usize,
90    count: usize,
91) -> Result<Option<(usize, usize)>, ProtocolError> {
92    let mut pos = start_pos;
93    let mut total = 0usize;
94    for _ in 0..count {
95        if pos >= buf.len() {
96            return Ok(None);
97        }
98        if buf[pos] != b'$' {
99            return Err(ProtocolError::Malformed("expected bulk string"));
100        }
101        let Some(len_end) = find_crlf(buf, pos + 1) else {
102            return Ok(None);
103        };
104        let len = parse_int(&buf[pos + 1..len_end])
105            .ok_or(ProtocolError::Malformed("bad bulk length"))?;
106        if len < 0 {
107            return Err(ProtocolError::Malformed("negative bulk length in request"));
108        }
109        let len = len as usize;
110        if len > MAX_BULK_LEN {
111            return Err(ProtocolError::Malformed("bulk length exceeds proto-max-bulk-len"));
112        }
113        let data_end = len_end + 2 + len;
114        if buf.len() < data_end + 2 {
115            return Ok(None);
116        }
117        if &buf[data_end..data_end + 2] != b"\r\n" {
118            return Err(ProtocolError::Malformed("bulk string not CRLF-terminated"));
119        }
120        total += len;
121        pos = data_end + 2;
122    }
123    Ok(Some((pos, total)))
124}
125
126/// Copy `count` already-validated bulk args from `buf[start_pos..]` into `dst`.
127/// Caller must have called [`validate_multibulk_frame`] first.
128fn copy_multibulk_args(buf: &[u8], start_pos: usize, count: usize, dst: &mut Argv) {
129    let mut p = start_pos;
130    for _ in 0..count {
131        let len_end = find_crlf(buf, p + 1).expect("validated in pass 1");
132        let len = parse_int(&buf[p + 1..len_end]).expect("validated in pass 1") as usize;
133        let data_start = len_end + 2;
134        dst.push(&buf[data_start..data_start + len]);
135        p = data_start + len + 2;
136    }
137}
138
139fn parse_multibulk_into(buf: &[u8], dst: &mut Argv) -> Result<Option<usize>, ProtocolError> {
140    let Some(hdr_end) = find_crlf(buf, 1) else {
141        return Ok(None);
142    };
143    let count =
144        parse_int(&buf[1..hdr_end]).ok_or(ProtocolError::Malformed("bad multibulk count"))?;
145    if count < 0 {
146        // Null array → empty argv (already cleared).
147        return Ok(Some(hdr_end + 2));
148    }
149    let count = count as usize;
150    if count > MAX_MULTIBULK_LEN {
151        return Err(ProtocolError::Malformed("multibulk count exceeds limit"));
152    }
153    let start = hdr_end + 2;
154
155    let Some((end_pos, total)) = validate_multibulk_frame(buf, start, count)? else {
156        return Ok(None);
157    };
158
159    // `reserve` is a no-op when the scratch Argv has already amortised
160    // enough capacity from earlier cmds.
161    dst.reserve_for(count, total);
162    copy_multibulk_args(buf, start, count, dst);
163    Ok(Some(end_pos))
164}
165
166/// Parse a bulk-string length header `$<len>\r\n` whose `$` sits at
167/// `buf[pos]` (the caller has already checked that byte). One fused pass:
168/// the digits accumulate while the same loop walks to the terminating
169/// CRLF — bulk headers are 2-21 bytes, so this short byte loop beats the
170/// `find_crlf` + [`parse_int`] double scan the two-pass parser paid per
171/// arg. Accepts the same shapes as `parse_int` (optional `+`/`-` sign,
172/// checked i64 accumulation); a negative length is malformed in a
173/// request, matching [`validate_multibulk_frame`].
174///
175/// Returns `(len, data_start)`; `Ok(None)` = need more bytes.
176#[inline]
177pub(crate) fn parse_bulk_len(
178    buf: &[u8],
179    pos: usize,
180) -> Result<Option<(usize, usize)>, ProtocolError> {
181    let mut q = pos + 1;
182    let neg = match buf.get(q) {
183        None => return Ok(None),
184        Some(b'-') => {
185            q += 1;
186            true
187        }
188        Some(b'+') => {
189            q += 1;
190            false
191        }
192        _ => false,
193    };
194    let digits_start = q;
195    let mut acc: i64 = 0;
196    loop {
197        match buf.get(q) {
198            None => return Ok(None),
199            Some(&b) if b.is_ascii_digit() => {
200                acc = acc
201                    .checked_mul(10)
202                    .and_then(|a| a.checked_add(i64::from(b - b'0')))
203                    .ok_or(ProtocolError::Malformed("bad bulk length"))?;
204                q += 1;
205            }
206            Some(b'\r') => break,
207            Some(_) => return Err(ProtocolError::Malformed("bad bulk length")),
208        }
209    }
210    if q == digits_start {
211        return Err(ProtocolError::Malformed("bad bulk length"));
212    }
213    match buf.get(q + 1) {
214        None => return Ok(None),
215        Some(b'\n') => {}
216        Some(_) => return Err(ProtocolError::Malformed("bad bulk length")),
217    }
218    if neg {
219        return Err(ProtocolError::Malformed("negative bulk length in request"));
220    }
221    let len = acc as usize;
222    if len > MAX_BULK_LEN {
223        return Err(ProtocolError::Malformed("bulk length exceeds proto-max-bulk-len"));
224    }
225    Ok(Some((len, q + 2)))
226}
227
228/// Find the index of `\r\n` at or after `start`, returning the index of `\r`.
229///
230/// Delegates to `kevy_bytes::find_crlf`, which picks
231/// AVX2 (x86_64 runtime-detected) / NEON (aarch64 baseline) / u64 SWAR
232/// fallback. An earlier in-crate SWAR loop is now the fallback tier
233/// of that dispatch. Pulling the SIMD path into kevy-bytes keeps this
234/// crate under #![forbid(unsafe_code)] — kevy-bytes already wraps
235/// SmallBytes' unsafe union work so it's the right home for arch
236/// intrinsics.
237#[inline]
238pub(crate) fn find_crlf(buf: &[u8], start: usize) -> Option<usize> {
239    kevy_bytes::find_crlf(buf, start)
240}
241
242/// Parse a base-10 signed integer from ASCII bytes (no surrounding whitespace).
243#[inline]
244pub(crate) fn parse_int(bytes: &[u8]) -> Option<i64> {
245    if bytes.is_empty() {
246        return None;
247    }
248    let (neg, digits) = match bytes[0] {
249        b'-' => (true, &bytes[1..]),
250        b'+' => (false, &bytes[1..]),
251        _ => (false, bytes),
252    };
253    if digits.is_empty() {
254        return None;
255    }
256    let mut acc: i64 = 0;
257    for &b in digits {
258        if !b.is_ascii_digit() {
259            return None;
260        }
261        acc = acc.checked_mul(10)?.checked_add(i64::from(b - b'0'))?;
262    }
263    Some(if neg { -acc } else { acc })
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269    use crate::encode_command;
270    use crate::parse_command_borrowed;
271
272    // A tiny frame declaring a huge multibulk count must be rejected —
273    // NOT drive a multi-gigabyte capacity reservation (remote
274    // alloc-abort). Owned and borrowed parsers gate identically.
275    #[test]
276    fn absurd_multibulk_count_rejected_not_preallocated() {
277        let frame = b"*9999999999\r\n";
278        assert!(parse_command(frame).is_err(), "owned parser must reject");
279        assert!(parse_command_borrowed(frame).is_err(), "borrowed parser must reject");
280        // Exactly at the limit + 1 is still rejected; the limit itself
281        // is a legal (if large) count that simply needs more bytes.
282        let over = format!("*{}\r\n", MAX_MULTIBULK_LEN + 1);
283        assert!(parse_command(over.as_bytes()).is_err());
284        let at = format!("*{}\r\n", MAX_MULTIBULK_LEN);
285        assert!(matches!(parse_command(at.as_bytes()), Ok(None)));
286    }
287
288    // A bulk header declaring more than proto-max-bulk-len is rejected
289    // at parse time, so a declared-but-never-sent length can't grow
290    // the connection input buffer without bound.
291    #[test]
292    fn oversized_bulk_length_rejected() {
293        let over = format!("*1\r\n${}\r\n", MAX_BULK_LEN + 1);
294        assert!(parse_command(over.as_bytes()).is_err());
295        assert!(parse_command_borrowed(over.as_bytes()).is_err());
296        // A legal-size header just needs its bytes.
297        let ok = b"*1\r\n$5\r\n";
298        assert!(matches!(parse_command(ok), Ok(None)));
299    }
300
301    // SWAR find_crlf fuzz: planted CRLFs at every offset 0..40, lone-CR
302    // distractors, no-CRLF inputs, near-end boundaries. The SWAR window is
303    // 8 bytes, so transitions at offsets 0/7/8/15/16/… stress alignment.
304    #[test]
305    fn find_crlf_at_every_offset() {
306        for off in 0..40 {
307            let mut buf = vec![b'a'; 60];
308            buf[off] = b'\r';
309            buf[off + 1] = b'\n';
310            assert_eq!(find_crlf(&buf, 0), Some(off), "off={off}");
311        }
312    }
313
314    #[test]
315    fn find_crlf_skips_lone_cr() {
316        // Lone \r at the front, then a real CRLF later.
317        let mut buf = vec![b'a'; 32];
318        buf[3] = b'\r';
319        buf[4] = b'b'; // not \n → skip
320        buf[20] = b'\r';
321        buf[21] = b'\n';
322        assert_eq!(find_crlf(&buf, 0), Some(20));
323    }
324
325    #[test]
326    fn find_crlf_none_when_absent() {
327        let buf = vec![b'a'; 32];
328        assert_eq!(find_crlf(&buf, 0), None);
329        let buf = b"";
330        assert_eq!(find_crlf(buf, 0), None);
331        let buf = b"\r"; // only CR, no LF available
332        assert_eq!(find_crlf(buf, 0), None);
333    }
334
335    #[test]
336    fn find_crlf_at_buffer_end() {
337        let buf = b"abcdefghij\r\n"; // CRLF at offset 10
338        assert_eq!(find_crlf(buf, 0), Some(10));
339        // Start past the CR.
340        assert_eq!(find_crlf(buf, 11), None);
341    }
342
343    #[test]
344    fn find_crlf_with_many_lone_crs() {
345        // 7 lone CRs followed by a real CRLF. SWAR finds one CR per iter
346        // but must keep going until it finds the real pair.
347        let mut buf = Vec::new();
348        for _ in 0..7 {
349            buf.push(b'\r');
350            buf.push(b'x'); // not \n
351        }
352        buf.extend_from_slice(b"\r\n");
353        // Real CRLF starts at offset 14 (7 * 2).
354        assert_eq!(find_crlf(&buf, 0), Some(14));
355    }
356
357    #[test]
358    fn find_crlf_from_nonzero_start() {
359        let buf = b"\r\n\r\n\r\n";
360        // Starts at offset 0 → first CRLF.
361        assert_eq!(find_crlf(buf, 0), Some(0));
362        // Skip the first CRLF.
363        assert_eq!(find_crlf(buf, 2), Some(2));
364        assert_eq!(find_crlf(buf, 4), Some(4));
365    }
366
367    #[test]
368    fn parse_multibulk_ping() {
369        let (cmd, used) = parse_command(b"*1\r\n$4\r\nPING\r\n").unwrap().unwrap();
370        assert_eq!(cmd, vec![b"PING".to_vec()]);
371        assert_eq!(used, 14);
372    }
373
374    #[test]
375    fn parse_multibulk_echo() {
376        let frame = b"*2\r\n$4\r\nECHO\r\n$5\r\nhello\r\n";
377        let (cmd, used) = parse_command(frame).unwrap().unwrap();
378        assert_eq!(cmd, vec![b"ECHO".to_vec(), b"hello".to_vec()]);
379        assert_eq!(used, frame.len());
380    }
381
382    #[test]
383    fn parse_incomplete_returns_none() {
384        assert_eq!(parse_command(b"*1\r\n$4\r\nPI").unwrap(), None);
385        assert_eq!(parse_command(b"*2\r\n$4\r\nECHO\r\n").unwrap(), None);
386        assert_eq!(parse_command(b"").unwrap(), None);
387    }
388
389    #[test]
390    fn parse_inline_command() {
391        let (cmd, used) = parse_command(b"PING\r\n").unwrap().unwrap();
392        assert_eq!(cmd, vec![b"PING".to_vec()]);
393        assert_eq!(used, 6);
394        let (cmd, _) = parse_command(b"ECHO  hi there\r\n").unwrap().unwrap();
395        assert_eq!(
396            cmd,
397            vec![b"ECHO".to_vec(), b"hi".to_vec(), b"there".to_vec()]
398        );
399    }
400
401    #[test]
402    fn parse_malformed_errors() {
403        assert!(parse_command(b"*1\r\n+OK\r\n").is_err());
404        assert!(parse_command(b"*x\r\n").is_err());
405    }
406
407    #[test]
408    fn round_trip_command() {
409        let mut buf = Vec::new();
410        encode_command(&mut buf, &[b"SET".to_vec(), b"k".to_vec(), b"v".to_vec()]);
411        let (cmd, used) = parse_command(&buf).unwrap().unwrap();
412        assert_eq!(cmd, vec![b"SET".to_vec(), b"k".to_vec(), b"v".to_vec()]);
413        assert_eq!(used, buf.len());
414    }
415
416}