Skip to main content

kevy_resp/
reply_parse.rs

1//! Reply-side parser (client perspective): parse server responses into a
2//! [`Reply`] enum. Mirror of the encoders in [`crate::reply_encode`] and
3//! [`crate::reply_encode_resp3`].
4//!
5//! Speaks RESP2 (the seven legacy prefixes — `+`/`-`/`:`/`$`/`*`/`$-1`/`*-1`)
6//! and the additive RESP3 set (`%` map, `~` set, `,` double, `#` boolean,
7//! `=` verbatim string, `(` big number, `_` null, `>` push, `!` blob error,
8//! `|` attributes). RESP2-only callers can still ignore the new [`Reply`]
9//! variants — the parser only produces them when the server speaks RESP3.
10
11use crate::error::ProtocolError;
12use crate::request::{find_crlf, parse_int};
13
14/// A parsed RESP reply (server → client) — the client-side counterpart of
15/// the crate's `encode_*` functions (server-side encoders).
16///
17/// Variants prefixed with `Resp3:` in their doc are only ever produced by
18/// a server speaking RESP3; an `HELLO 2` (or no `HELLO`) session sees the
19/// RESP2 subset (`Simple` / `Error` / `Int` / `Bulk` / `Nil` / `Array`)
20/// exclusively. Adding new variants is non-breaking: an exhaustive
21/// `match` on `Reply` is forced to opt into RESP3 by listing each variant
22/// (rust 2024 will not warn on missing arms only after `#[non_exhaustive]`
23/// — which we deliberately omit so RESP2-only code stays compile-checked
24/// for completeness).
25#[derive(Debug, Clone, PartialEq)]
26pub enum Reply {
27    /// `+OK`
28    Simple(Vec<u8>),
29    /// `-ERR ...`
30    Error(Vec<u8>),
31    /// `:42`
32    Int(i64),
33    /// `$5\r\nhello\r\n`
34    Bulk(Vec<u8>),
35    /// `$-1` or `*-1` — the RESP2 null sentinel; in RESP3 the dedicated
36    /// [`Reply::Null`] (`_\r\n`) is used instead. Both round-trip here.
37    Nil,
38    /// `*N ...`
39    Array(Vec<Reply>),
40    /// **Resp3:** `%N\r\n<key1><value1>...<keyN><valueN>` — N pairs (the
41    /// header count is the pair count, NOT the element count, so a map of
42    /// 3 pairs is `%3` plus 6 sub-replies). Parsed/exposed as a Vec of
43    /// pairs so duplicate keys + insertion order are preserved.
44    Map(Vec<(Reply, Reply)>),
45    /// **Resp3:** `~N\r\n<item1>...<itemN>` — set semantics on the wire;
46    /// dedup is the application's job (RESP3 doesn't require it).
47    Set(Vec<Reply>),
48    /// **Resp3:** `,1.5\r\n` — double. `inf` / `-inf` / `nan` are valid
49    /// payloads per the RESP3 spec and survive the round-trip.
50    Double(f64),
51    /// **Resp3:** `#t\r\n` / `#f\r\n` — boolean.
52    Boolean(bool),
53    /// **Resp3:** `=15\r\ntxt:Some bytes\r\n` — verbatim string carrying
54    /// a 3-char format tag (`txt` / `mkd` / etc.) + raw bytes. The colon
55    /// separator is part of the wire encoding but not part of `data`.
56    Verbatim {
57        /// 3-char format tag (e.g. `b"txt"` for plain text, `b"mkd"` for markdown).
58        fmt: [u8; 3],
59        /// Payload bytes following the `:` separator.
60        data: Vec<u8>,
61    },
62    /// **Resp3:** `(170141183460469231731687303715884105727\r\n` — arbitrary-
63    /// precision integer; carried as the raw digit bytes since we don't
64    /// pull in a bignum crate (charter: zero deps).
65    BigNumber(Vec<u8>),
66    /// **Resp3:** `_\r\n` — true null. RESP2 falls back to [`Reply::Nil`].
67    Null,
68    /// **Resp3:** `>N\r\n...` — like [`Reply::Array`] but tagged as an
69    /// out-of-band server-push frame (pub/sub messages in RESP3). The
70    /// client must dispatch these separately from regular replies.
71    Push(Vec<Reply>),
72    /// **Resp3:** `!8\r\nERR ohno\r\n` — error carried as a length-prefixed
73    /// bulk (handles errors containing CRLF that the simple-string `-`
74    /// shape can't encode).
75    BlobError(Vec<u8>),
76}
77
78/// Parse one RESP reply from the front of `buf`. Speaks RESP2 + RESP3.
79///
80/// - `Ok(Some((reply, consumed)))` — a complete reply.
81/// - `Ok(None)` — need more bytes.
82/// - `Err(_)` — malformed.
83///
84/// Attributes (`|N\r\n…<reply>`) are transparently consumed and
85/// discarded — they decorate the *next* reply but the parser surfaces
86/// only the underlying reply, matching what every RESP3 client library
87/// does today. Exposing them is a future addition once a real consumer
88/// (e.g. CLIENT TRACE) ships.
89pub fn parse_reply(buf: &[u8]) -> Result<Option<(Reply, usize)>, ProtocolError> {
90    let Some(&tag) = buf.first() else {
91        return Ok(None);
92    };
93    match tag {
94        b'+' => Ok(reply_line(buf).map(|(b, used)| (Reply::Simple(b.to_vec()), used))),
95        b'-' => Ok(reply_line(buf).map(|(b, used)| (Reply::Error(b.to_vec()), used))),
96        b':' => match reply_line(buf) {
97            None => Ok(None),
98            Some((b, used)) => {
99                let n = parse_int(b).ok_or(ProtocolError::Malformed("bad integer reply"))?;
100                Ok(Some((Reply::Int(n), used)))
101            }
102        },
103        b'$' => parse_bulk_reply(buf),
104        b'*' => parse_array_reply(buf, false),
105        // ── RESP3 additions ──────────────────────────────────────────
106        b'%' => parse_map_reply(buf),
107        b'~' => parse_set_reply(buf),
108        b',' => parse_double_reply(buf),
109        b'#' => parse_boolean_reply(buf),
110        b'=' => parse_verbatim_reply(buf),
111        b'(' => match reply_line(buf) {
112            None => Ok(None),
113            Some((b, used)) => Ok(Some((Reply::BigNumber(b.to_vec()), used))),
114        },
115        b'_' => parse_null_reply(buf),
116        b'>' => parse_array_reply(buf, true),
117        b'!' => parse_blob_error_reply(buf),
118        b'|' => parse_attributed_reply(buf),
119        _ => Err(ProtocolError::Malformed("unknown reply type")),
120    }
121}
122
123/// The CRLF-terminated payload after the type byte, plus bytes consumed.
124fn reply_line(buf: &[u8]) -> Option<(&[u8], usize)> {
125    find_crlf(buf, 1).map(|eol| (&buf[1..eol], eol + 2))
126}
127
128fn parse_bulk_reply(buf: &[u8]) -> Result<Option<(Reply, usize)>, ProtocolError> {
129    let Some(hdr_end) = find_crlf(buf, 1) else {
130        return Ok(None);
131    };
132    let len = parse_int(&buf[1..hdr_end]).ok_or(ProtocolError::Malformed("bad bulk length"))?;
133    if len < 0 {
134        return Ok(Some((Reply::Nil, hdr_end + 2)));
135    }
136    let data_start = hdr_end + 2;
137    let data_end = data_start + len as usize;
138    if buf.len() < data_end + 2 {
139        return Ok(None);
140    }
141    Ok(Some((Reply::Bulk(buf[data_start..data_end].to_vec()), data_end + 2)))
142}
143
144/// Shared parser for `*` (array, RESP2) and `>` (push, RESP3) — both
145/// are length-prefixed sequences of replies. `push=true` wraps the
146/// result in `Reply::Push`, otherwise `Reply::Array` (or `Reply::Nil`
147/// for the RESP2 `*-1` shape, which RESP3 push frames never emit).
148fn parse_array_reply(buf: &[u8], push: bool) -> Result<Option<(Reply, usize)>, ProtocolError> {
149    let Some(hdr_end) = find_crlf(buf, 1) else {
150        return Ok(None);
151    };
152    let count = parse_int(&buf[1..hdr_end]).ok_or(ProtocolError::Malformed("bad array length"))?;
153    if count < 0 {
154        if push {
155            return Err(ProtocolError::Malformed("push frame cannot be null"));
156        }
157        return Ok(Some((Reply::Nil, hdr_end + 2)));
158    }
159    let mut pos = hdr_end + 2;
160    // Cap initial capacity by remaining buffer bytes — an attacker-controlled
161    // `*999999999999\r\n` header would otherwise panic via `Vec::with_capacity`'s
162    // capacity overflow. Each item costs ≥ 1 byte (a CRLF for Nil/Int/Simple),
163    // so a real array of N items needs ≥ N bytes left. Push will grow the vec
164    // amortized if the genuine count is higher but bytes are present. Found by
165    // cargo-fuzz (crash-4c4ee6777903d009f93289eb428b3b371d027137).
166    let cap = (count as usize).min(buf.len().saturating_sub(pos));
167    let mut items = Vec::with_capacity(cap);
168    for _ in 0..count {
169        match parse_reply(&buf[pos..])? {
170            None => return Ok(None),
171            Some((r, used)) => {
172                items.push(r);
173                pos += used;
174            }
175        }
176    }
177    let reply = if push { Reply::Push(items) } else { Reply::Array(items) };
178    Ok(Some((reply, pos)))
179}
180
181/// `%N\r\n` followed by 2N sub-replies (N key/value pairs).
182fn parse_map_reply(buf: &[u8]) -> Result<Option<(Reply, usize)>, ProtocolError> {
183    let Some(hdr_end) = find_crlf(buf, 1) else {
184        return Ok(None);
185    };
186    let count = parse_int(&buf[1..hdr_end]).ok_or(ProtocolError::Malformed("bad map length"))?;
187    if count < 0 {
188        return Err(ProtocolError::Malformed("map length cannot be negative"));
189    }
190    let mut pos = hdr_end + 2;
191    // Same fuzz-driven cap as parse_array_reply — each pair costs ≥ 2 bytes.
192    let cap = (count as usize).min(buf.len().saturating_sub(pos) / 2);
193    let mut pairs: Vec<(Reply, Reply)> = Vec::with_capacity(cap);
194    for _ in 0..count {
195        let Some((k, used_k)) = parse_reply(&buf[pos..])? else {
196            return Ok(None);
197        };
198        pos += used_k;
199        let Some((v, used_v)) = parse_reply(&buf[pos..])? else {
200            return Ok(None);
201        };
202        pos += used_v;
203        pairs.push((k, v));
204    }
205    Ok(Some((Reply::Map(pairs), pos)))
206}
207
208/// `~N\r\n` followed by N sub-replies — set on the wire, no dedup.
209fn parse_set_reply(buf: &[u8]) -> Result<Option<(Reply, usize)>, ProtocolError> {
210    let Some(hdr_end) = find_crlf(buf, 1) else {
211        return Ok(None);
212    };
213    let count = parse_int(&buf[1..hdr_end]).ok_or(ProtocolError::Malformed("bad set length"))?;
214    if count < 0 {
215        return Err(ProtocolError::Malformed("set length cannot be negative"));
216    }
217    let mut pos = hdr_end + 2;
218    let cap = (count as usize).min(buf.len().saturating_sub(pos));
219    let mut items = Vec::with_capacity(cap);
220    for _ in 0..count {
221        match parse_reply(&buf[pos..])? {
222            None => return Ok(None),
223            Some((r, used)) => {
224                items.push(r);
225                pos += used;
226            }
227        }
228    }
229    Ok(Some((Reply::Set(items), pos)))
230}
231
232/// `,N\r\n` — double. RESP3 spec carries `inf` / `-inf` / `nan` as
233/// literal byte strings; `f64::from_str` already handles all three.
234fn parse_double_reply(buf: &[u8]) -> Result<Option<(Reply, usize)>, ProtocolError> {
235    let Some((bytes, used)) = reply_line(buf) else {
236        return Ok(None);
237    };
238    let s = std::str::from_utf8(bytes).map_err(|_| ProtocolError::Malformed("bad double utf8"))?;
239    let v: f64 = s.parse().map_err(|_| ProtocolError::Malformed("bad double"))?;
240    Ok(Some((Reply::Double(v), used)))
241}
242
243/// `#t\r\n` / `#f\r\n` — boolean. Any other payload is malformed.
244fn parse_boolean_reply(buf: &[u8]) -> Result<Option<(Reply, usize)>, ProtocolError> {
245    let Some((bytes, used)) = reply_line(buf) else {
246        return Ok(None);
247    };
248    let v = match bytes {
249        b"t" => true,
250        b"f" => false,
251        _ => return Err(ProtocolError::Malformed("bad boolean payload")),
252    };
253    Ok(Some((Reply::Boolean(v), used)))
254}
255
256/// `=N\r\n<fmt>:<data>\r\n` — verbatim string. The 3-char `fmt` tag +
257/// `:` separator are inside the N-byte body.
258fn parse_verbatim_reply(buf: &[u8]) -> Result<Option<(Reply, usize)>, ProtocolError> {
259    let Some(hdr_end) = find_crlf(buf, 1) else {
260        return Ok(None);
261    };
262    let len = parse_int(&buf[1..hdr_end]).ok_or(ProtocolError::Malformed("bad verbatim length"))?;
263    if len < 4 {
264        return Err(ProtocolError::Malformed("verbatim length < 4 (fmt + ':')"));
265    }
266    let data_start = hdr_end + 2;
267    let data_end = data_start + len as usize;
268    if buf.len() < data_end + 2 {
269        return Ok(None);
270    }
271    let body = &buf[data_start..data_end];
272    if body[3] != b':' {
273        return Err(ProtocolError::Malformed("verbatim missing fmt:data separator"));
274    }
275    let mut fmt = [0u8; 3];
276    fmt.copy_from_slice(&body[..3]);
277    let data = body[4..].to_vec();
278    Ok(Some((Reply::Verbatim { fmt, data }, data_end + 2)))
279}
280
281/// `_\r\n` — RESP3 true null (5 bytes counting the `_` and CRLF).
282fn parse_null_reply(buf: &[u8]) -> Result<Option<(Reply, usize)>, ProtocolError> {
283    if buf.len() < 3 {
284        return Ok(None);
285    }
286    if &buf[..3] != b"_\r\n" {
287        return Err(ProtocolError::Malformed("bad null payload"));
288    }
289    Ok(Some((Reply::Null, 3)))
290}
291
292/// `!N\r\n<error>\r\n` — length-prefixed error (carries CRLF safely).
293fn parse_blob_error_reply(buf: &[u8]) -> Result<Option<(Reply, usize)>, ProtocolError> {
294    let Some(hdr_end) = find_crlf(buf, 1) else {
295        return Ok(None);
296    };
297    let len =
298        parse_int(&buf[1..hdr_end]).ok_or(ProtocolError::Malformed("bad blob error length"))?;
299    if len < 0 {
300        return Err(ProtocolError::Malformed("blob error length cannot be negative"));
301    }
302    let data_start = hdr_end + 2;
303    let data_end = data_start + len as usize;
304    if buf.len() < data_end + 2 {
305        return Ok(None);
306    }
307    Ok(Some((Reply::BlobError(buf[data_start..data_end].to_vec()), data_end + 2)))
308}
309
310/// `|N\r\n<map of N pairs><reply>` — attributes decorate the next reply.
311/// We parse the attribute map then transparently return the decorated
312/// reply, mirroring what RESP3 client libraries do today. The attributes
313/// themselves are dropped (see [`parse_reply`] docs).
314fn parse_attributed_reply(buf: &[u8]) -> Result<Option<(Reply, usize)>, ProtocolError> {
315    // Re-use the map parser but throw away the result; then parse the
316    // actual reply that follows.
317    let Some((_attrs, used_attrs)) = parse_map_reply(buf)? else {
318        return Ok(None);
319    };
320    match parse_reply(&buf[used_attrs..])? {
321        None => Ok(None),
322        Some((r, used)) => Ok(Some((r, used_attrs + used))),
323    }
324}
325
326#[cfg(test)]
327mod tests {
328    use super::*;
329
330    #[test]
331    fn parse_replies() {
332        let r = |b: &[u8]| parse_reply(b).unwrap().unwrap().0;
333        assert_eq!(r(b"+OK\r\n"), Reply::Simple(b"OK".to_vec()));
334        assert_eq!(r(b"-ERR bad\r\n"), Reply::Error(b"ERR bad".to_vec()));
335        assert_eq!(r(b":42\r\n"), Reply::Int(42));
336        assert_eq!(r(b"$5\r\nhello\r\n"), Reply::Bulk(b"hello".to_vec()));
337        assert_eq!(r(b"$-1\r\n"), Reply::Nil);
338        assert_eq!(r(b"*-1\r\n"), Reply::Nil);
339
340        let (arr, used) = parse_reply(b"*2\r\n:1\r\n$2\r\nhi\r\n").unwrap().unwrap();
341        assert_eq!(arr, Reply::Array(vec![Reply::Int(1), Reply::Bulk(b"hi".to_vec())]));
342        assert_eq!(used, 16);
343
344        // Incomplete replies ask for more bytes.
345        assert_eq!(parse_reply(b"$5\r\nhel").unwrap(), None);
346        assert_eq!(parse_reply(b"*2\r\n:1\r\n").unwrap(), None);
347        // RESP3 `!N\r\n...` (blob error) IS a valid prefix now — verify the
348        // old "unknown prefix" test moved to a genuinely unknown byte.
349        assert!(parse_reply(b"@huh\r\n").is_err());
350    }
351
352    #[test]
353    fn parse_resp3_scalars() {
354        let r = |b: &[u8]| parse_reply(b).unwrap().unwrap().0;
355        assert_eq!(r(b"_\r\n"), Reply::Null);
356        assert_eq!(r(b"#t\r\n"), Reply::Boolean(true));
357        assert_eq!(r(b"#f\r\n"), Reply::Boolean(false));
358        assert_eq!(r(b",1.5\r\n"), Reply::Double(1.5));
359        assert_eq!(r(b",inf\r\n"), Reply::Double(f64::INFINITY));
360        assert_eq!(r(b",-inf\r\n"), Reply::Double(f64::NEG_INFINITY));
361        // NaN doesn't satisfy `PartialEq` — match manually.
362        match r(b",nan\r\n") {
363            Reply::Double(v) => assert!(v.is_nan()),
364            other => panic!("expected Double(nan), got {other:?}"),
365        }
366        assert_eq!(
367            r(b"(170141183460469231731687303715884105727\r\n"),
368            Reply::BigNumber(b"170141183460469231731687303715884105727".to_vec())
369        );
370        assert_eq!(r(b"!11\r\nERR bad cmd\r\n"), Reply::BlobError(b"ERR bad cmd".to_vec()));
371    }
372
373    #[test]
374    fn parse_resp3_verbatim() {
375        let r = |b: &[u8]| parse_reply(b).unwrap().unwrap().0;
376        assert_eq!(
377            r(b"=15\r\ntxt:Some string\r\n"),
378            Reply::Verbatim { fmt: *b"txt", data: b"Some string".to_vec() }
379        );
380        // len < 4 (no room for fmt + ':') is rejected.
381        assert!(parse_reply(b"=3\r\ntxt\r\n").is_err());
382        // Missing `:` separator is rejected.
383        assert!(parse_reply(b"=7\r\ntxt+abc\r\n").is_err());
384    }
385
386    #[test]
387    fn parse_resp3_map_and_set() {
388        let r = |b: &[u8]| parse_reply(b).unwrap().unwrap().0;
389        // %2\r\n :1\r\n $1\r\n a\r\n :2\r\n $1\r\n b\r\n
390        let m = r(b"%2\r\n:1\r\n$1\r\na\r\n:2\r\n$1\r\nb\r\n");
391        assert_eq!(
392            m,
393            Reply::Map(vec![
394                (Reply::Int(1), Reply::Bulk(b"a".to_vec())),
395                (Reply::Int(2), Reply::Bulk(b"b".to_vec())),
396            ])
397        );
398        // ~3\r\n :1\r\n :2\r\n :3\r\n
399        let s = r(b"~3\r\n:1\r\n:2\r\n:3\r\n");
400        assert_eq!(s, Reply::Set(vec![Reply::Int(1), Reply::Int(2), Reply::Int(3)]));
401        // Empty map / set.
402        assert_eq!(r(b"%0\r\n"), Reply::Map(vec![]));
403        assert_eq!(r(b"~0\r\n"), Reply::Set(vec![]));
404        // Negative count is malformed (only `*` / `$` allow -1 for nil).
405        assert!(parse_reply(b"%-1\r\n").is_err());
406        assert!(parse_reply(b"~-1\r\n").is_err());
407    }
408
409    #[test]
410    fn parse_resp3_push_frame() {
411        let r = |b: &[u8]| parse_reply(b).unwrap().unwrap().0;
412        let push = r(b">3\r\n+message\r\n$4\r\nnews\r\n$5\r\nhello\r\n");
413        assert_eq!(
414            push,
415            Reply::Push(vec![
416                Reply::Simple(b"message".to_vec()),
417                Reply::Bulk(b"news".to_vec()),
418                Reply::Bulk(b"hello".to_vec()),
419            ])
420        );
421        // Push frames have no null shape.
422        assert!(parse_reply(b">-1\r\n").is_err());
423    }
424
425    #[test]
426    fn parse_resp3_attributes_are_skipped() {
427        // |1\r\n +key-popularity\r\n %2\r\n $1\r\n a\r\n ,0.5\r\n $1\r\n b\r\n ,0.3\r\n
428        // followed by the actual reply: *2\r\n :1\r\n :2\r\n
429        let frame =
430            b"|1\r\n+key-popularity\r\n%2\r\n$1\r\na\r\n,0.5\r\n$1\r\nb\r\n,0.3\r\n*2\r\n:1\r\n:2\r\n";
431        let (r, used) = parse_reply(frame).unwrap().unwrap();
432        assert_eq!(r, Reply::Array(vec![Reply::Int(1), Reply::Int(2)]));
433        assert_eq!(used, frame.len());
434    }
435
436    #[test]
437    fn parse_resp3_partial_returns_none() {
438        // Each new shape: cut at every CRLF boundary and assert None.
439        for cut in &[b"_".as_slice(), b"_\r", b"#t", b"#t\r"] {
440            assert_eq!(parse_reply(cut).unwrap(), None);
441        }
442        assert_eq!(parse_reply(b"=15\r\ntxt:Some str").unwrap(), None);
443        // Map mid-frame.
444        assert_eq!(parse_reply(b"%2\r\n:1\r\n$1\r\na\r\n:2\r\n").unwrap(), None);
445    }
446}