Skip to main content

rustlavel_cache/redis/
resp.rs

1//! RESP2: the Redis serialization protocol, encoder and decoder.
2//!
3//! RESP is five type-tagged, CRLF-terminated forms:
4//!
5//! ```text
6//! +OK\r\n                     simple string
7//! -ERR unknown command\r\n    error
8//! :42\r\n                     integer
9//! $5\r\nhello\r\n             bulk string (a length, then exactly that many bytes)
10//! $-1\r\n                     the null bulk string — "no such key"
11//! *2\r\n:1\r\n:2\r\n          array, which may nest
12//! ```
13//!
14//! Commands go the other way as an array of bulk strings, which is the only
15//! form a Redis server accepts from a client and the only one that is safe:
16//! because every argument carries its own byte length, a value containing a
17//! newline, a space, or a quote cannot be read as a second argument. That is
18//! this module's answer to command injection, and it is why nothing here ever
19//! builds a command by formatting a string.
20//!
21//! The decoder is incremental: [`decode`] returns `Ok(None)` when the buffer
22//! holds only part of a reply, so the connection can read more bytes and try
23//! again without any framing state of its own.
24
25use rustlavel_core::{Error, Result};
26
27/// The largest reply this client will assemble, as a guard against a malformed
28/// or hostile length header asking for a terabyte-sized allocation.
29const MAX_BULK: i64 = 512 * 1024 * 1024;
30
31/// How deeply arrays may nest, so a crafted reply cannot exhaust the stack.
32const MAX_DEPTH: usize = 32;
33
34/// One decoded RESP value.
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub enum Value {
37    /// `+OK` — a status reply.
38    Simple(String),
39    /// `-ERR ...` — an error reply. Carried as a value rather than a Rust error
40    /// so the decoder stays total and the caller decides what is fatal.
41    Error(String),
42    /// `:42`
43    Integer(i64),
44    /// `$5\r\nhello` — arbitrary bytes, not necessarily UTF-8.
45    Bulk(Vec<u8>),
46    /// `$-1` or `*-1` — the absence of a value, which is how Redis says
47    /// "no such key".
48    Nil,
49    /// `*2\r\n...`
50    Array(Vec<Value>),
51}
52
53impl Value {
54    /// Interpret a reply as text, whatever shape it arrived in.
55    pub fn as_str(&self) -> Option<&str> {
56        match self {
57            Value::Simple(s) => Some(s),
58            Value::Bulk(bytes) => std::str::from_utf8(bytes).ok(),
59            _ => None,
60        }
61    }
62
63    pub fn as_bytes(&self) -> Option<&[u8]> {
64        match self {
65            Value::Simple(s) => Some(s.as_bytes()),
66            Value::Bulk(bytes) => Some(bytes),
67            _ => None,
68        }
69    }
70
71    /// An integer reply, or a bulk string that happens to hold digits — Redis
72    /// answers `TTL` with the first and `GET` on a counter with the second.
73    pub fn as_i64(&self) -> Option<i64> {
74        match self {
75            Value::Integer(n) => Some(*n),
76            Value::Simple(s) => s.trim().parse().ok(),
77            Value::Bulk(bytes) => std::str::from_utf8(bytes).ok()?.trim().parse().ok(),
78            _ => None,
79        }
80    }
81
82    pub fn is_nil(&self) -> bool {
83        matches!(self, Value::Nil)
84    }
85
86    /// Convert an error reply into a Rust error, leaving anything else alone.
87    pub fn into_result(self) -> Result<Value> {
88        match self {
89            Value::Error(message) => Err(Error::msg(format!("redis: {message}"))),
90            other => Ok(other),
91        }
92    }
93
94    /// Serialize back to the wire. Used by the round-trip tests and by the
95    /// in-process fake server they run against.
96    pub fn encode(&self) -> Vec<u8> {
97        let mut out = Vec::new();
98        self.encode_into(&mut out);
99        out
100    }
101
102    fn encode_into(&self, out: &mut Vec<u8>) {
103        match self {
104            Value::Simple(s) => {
105                out.push(b'+');
106                out.extend_from_slice(s.as_bytes());
107                out.extend_from_slice(b"\r\n");
108            }
109            Value::Error(s) => {
110                out.push(b'-');
111                out.extend_from_slice(s.as_bytes());
112                out.extend_from_slice(b"\r\n");
113            }
114            Value::Integer(n) => {
115                out.push(b':');
116                out.extend_from_slice(n.to_string().as_bytes());
117                out.extend_from_slice(b"\r\n");
118            }
119            Value::Bulk(bytes) => {
120                out.push(b'$');
121                out.extend_from_slice(bytes.len().to_string().as_bytes());
122                out.extend_from_slice(b"\r\n");
123                out.extend_from_slice(bytes);
124                out.extend_from_slice(b"\r\n");
125            }
126            Value::Nil => out.extend_from_slice(b"$-1\r\n"),
127            Value::Array(items) => {
128                out.push(b'*');
129                out.extend_from_slice(items.len().to_string().as_bytes());
130                out.extend_from_slice(b"\r\n");
131                for item in items {
132                    item.encode_into(out);
133                }
134            }
135        }
136    }
137}
138
139/// Encode a command as an array of bulk strings.
140///
141/// Every argument is length-prefixed, so no argument can ever be mistaken for
142/// another one however strange its bytes are.
143pub fn encode_command(args: &[&[u8]]) -> Vec<u8> {
144    let mut out = Vec::with_capacity(16 + args.iter().map(|a| a.len() + 16).sum::<usize>());
145    out.push(b'*');
146    out.extend_from_slice(args.len().to_string().as_bytes());
147    out.extend_from_slice(b"\r\n");
148
149    for arg in args {
150        out.push(b'$');
151        out.extend_from_slice(arg.len().to_string().as_bytes());
152        out.extend_from_slice(b"\r\n");
153        out.extend_from_slice(arg);
154        out.extend_from_slice(b"\r\n");
155    }
156    out
157}
158
159/// Decode one value from the front of `input`.
160///
161/// Returns the value and how many bytes it consumed, or `Ok(None)` when the
162/// buffer does not yet hold a complete reply.
163pub fn decode(input: &[u8]) -> Result<Option<(Value, usize)>> {
164    decode_at(input, 0, 0)
165}
166
167fn decode_at(input: &[u8], from: usize, depth: usize) -> Result<Option<(Value, usize)>> {
168    if depth > MAX_DEPTH {
169        return Err(protocol("reply nests deeper than this client will follow"));
170    }
171    let Some(&tag) = input.get(from) else {
172        return Ok(None);
173    };
174
175    let Some((line, after_line)) = read_line(input, from + 1)? else {
176        return Ok(None);
177    };
178
179    match tag {
180        b'+' => Ok(Some((Value::Simple(to_text(line)?), after_line))),
181        b'-' => Ok(Some((Value::Error(to_text(line)?), after_line))),
182        b':' => Ok(Some((Value::Integer(parse_int(line)?), after_line))),
183        b'$' => {
184            let length = parse_int(line)?;
185            if length < 0 {
186                // Any negative length is the null bulk string; only -1 is
187                // canonical, but older servers were not always careful.
188                return Ok(Some((Value::Nil, after_line)));
189            }
190            if length > MAX_BULK {
191                return Err(protocol(&format!("bulk reply of {length} bytes is implausible")));
192            }
193
194            let length = length as usize;
195            // The payload plus its trailing CRLF must all have arrived.
196            if input.len() < after_line + length + 2 {
197                return Ok(None);
198            }
199            let payload = input[after_line..after_line + length].to_vec();
200            Ok(Some((Value::Bulk(payload), after_line + length + 2)))
201        }
202        b'*' => {
203            let count = parse_int(line)?;
204            if count < 0 {
205                return Ok(Some((Value::Nil, after_line)));
206            }
207            if count > MAX_BULK {
208                return Err(protocol(&format!("array reply of {count} items is implausible")));
209            }
210
211            let mut items = Vec::with_capacity((count as usize).min(1024));
212            let mut cursor = after_line;
213            for _ in 0..count {
214                match decode_at(input, cursor, depth + 1)? {
215                    Some((item, next)) => {
216                        items.push(item);
217                        cursor = next;
218                    }
219                    // One element short: the whole array is still incomplete.
220                    None => return Ok(None),
221                }
222            }
223            Ok(Some((Value::Array(items), cursor)))
224        }
225        other => Err(protocol(&format!(
226            "unknown RESP type byte {:?} — is this actually a Redis server?",
227            other as char
228        ))),
229    }
230}
231
232/// Find the CRLF-terminated line starting at `from`, returning it and the index
233/// just past the CRLF.
234fn read_line(input: &[u8], from: usize) -> Result<Option<(&[u8], usize)>> {
235    let mut index = from;
236    while index + 1 < input.len() {
237        if input[index] == b'\r' && input[index + 1] == b'\n' {
238            return Ok(Some((&input[from..index], index + 2)));
239        }
240        index += 1;
241    }
242    Ok(None)
243}
244
245fn to_text(bytes: &[u8]) -> Result<String> {
246    std::str::from_utf8(bytes)
247        .map(str::to_string)
248        .map_err(|_| protocol("a status or error reply was not valid UTF-8"))
249}
250
251fn parse_int(bytes: &[u8]) -> Result<i64> {
252    std::str::from_utf8(bytes)
253        .ok()
254        .and_then(|text| text.trim().parse().ok())
255        .ok_or_else(|| protocol(&format!("`{}` is not a RESP integer", String::from_utf8_lossy(bytes))))
256}
257
258fn protocol(message: &str) -> Error {
259    Error::Protocol(format!("RESP: {message}"))
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265
266    fn decoded(bytes: &[u8]) -> Value {
267        let (value, consumed) = decode(bytes).unwrap().expect("a complete reply");
268        assert_eq!(consumed, bytes.len(), "the decoder must consume exactly the reply");
269        value
270    }
271
272    #[test]
273    fn decodes_a_simple_string() {
274        assert_eq!(decoded(b"+OK\r\n"), Value::Simple("OK".into()));
275        assert_eq!(decoded(b"+PONG\r\n"), Value::Simple("PONG".into()));
276        assert_eq!(decoded(b"+\r\n"), Value::Simple(String::new()));
277    }
278
279    #[test]
280    fn decodes_an_error_and_keeps_it_out_of_the_result_type_until_asked() {
281        let value = decoded(b"-WRONGTYPE Operation against a key holding the wrong kind of value\r\n");
282        assert_eq!(
283            value,
284            Value::Error("WRONGTYPE Operation against a key holding the wrong kind of value".into())
285        );
286
287        let error = value.into_result().unwrap_err();
288        assert!(error.to_string().contains("WRONGTYPE"));
289    }
290
291    #[test]
292    fn decodes_integers_including_negative_and_zero() {
293        assert_eq!(decoded(b":0\r\n"), Value::Integer(0));
294        assert_eq!(decoded(b":1000\r\n"), Value::Integer(1000));
295        assert_eq!(decoded(b":-42\r\n"), Value::Integer(-42));
296    }
297
298    #[test]
299    fn decodes_a_bulk_string_including_empty_and_binary_payloads() {
300        assert_eq!(decoded(b"$5\r\nhello\r\n"), Value::Bulk(b"hello".to_vec()));
301        assert_eq!(decoded(b"$0\r\n\r\n"), Value::Bulk(Vec::new()));
302
303        // A payload with an embedded CRLF is exactly why bulk strings carry a
304        // length: a line-oriented parser would stop halfway.
305        assert_eq!(decoded(b"$7\r\na\r\nb\r\nc\r\n"), Value::Bulk(b"a\r\nb\r\nc".to_vec()));
306    }
307
308    #[test]
309    fn decodes_the_null_bulk_string_as_nil() {
310        assert_eq!(decoded(b"$-1\r\n"), Value::Nil);
311        assert!(decoded(b"$-1\r\n").is_nil());
312        assert_eq!(decoded(b"*-1\r\n"), Value::Nil, "a null array is also an absence");
313    }
314
315    #[test]
316    fn decodes_an_array_of_mixed_types() {
317        assert_eq!(
318            decoded(b"*3\r\n:1\r\n$3\r\ntwo\r\n+three\r\n"),
319            Value::Array(vec![
320                Value::Integer(1),
321                Value::Bulk(b"two".to_vec()),
322                Value::Simple("three".into()),
323            ])
324        );
325        assert_eq!(decoded(b"*0\r\n"), Value::Array(Vec::new()));
326    }
327
328    #[test]
329    fn decodes_nested_arrays() {
330        let value = decoded(b"*2\r\n*2\r\n:1\r\n:2\r\n*2\r\n$3\r\nfoo\r\n$-1\r\n");
331        assert_eq!(
332            value,
333            Value::Array(vec![
334                Value::Array(vec![Value::Integer(1), Value::Integer(2)]),
335                Value::Array(vec![Value::Bulk(b"foo".to_vec()), Value::Nil]),
336            ])
337        );
338    }
339
340    #[test]
341    fn a_partial_reply_asks_for_more_bytes_instead_of_failing() {
342        let complete: &[u8] = b"*2\r\n$5\r\nhello\r\n$5\r\nworld\r\n";
343        for cut in 1..complete.len() {
344            assert_eq!(
345                decode(&complete[..cut]).unwrap(),
346                None,
347                "a {cut}-byte prefix must not decode"
348            );
349        }
350        assert!(decode(complete).unwrap().is_some());
351    }
352
353    #[test]
354    fn decoding_stops_at_the_end_of_one_reply_when_two_are_pipelined() {
355        let buffer: &[u8] = b"+OK\r\n:7\r\n";
356        let (first, consumed) = decode(buffer).unwrap().unwrap();
357
358        assert_eq!(first, Value::Simple("OK".into()));
359        assert_eq!(consumed, 5);
360
361        let (second, _) = decode(&buffer[consumed..]).unwrap().unwrap();
362        assert_eq!(second, Value::Integer(7));
363    }
364
365    #[test]
366    fn every_value_survives_an_encode_decode_round_trip() {
367        let values = [
368            Value::Simple("OK".into()),
369            Value::Error("ERR no such key".into()),
370            Value::Integer(-9_000_000_000),
371            Value::Bulk(b"payload with \r\n inside".to_vec()),
372            Value::Bulk(vec![0x00, 0xff, 0x7f]),
373            Value::Nil,
374            Value::Array(vec![]),
375            Value::Array(vec![
376                Value::Integer(1),
377                Value::Nil,
378                Value::Array(vec![Value::Bulk(b"deep".to_vec())]),
379            ]),
380        ];
381
382        for value in values {
383            let bytes = value.encode();
384            let (back, consumed) = decode(&bytes).unwrap().expect("a complete reply");
385            assert_eq!(back, value);
386            assert_eq!(consumed, bytes.len());
387        }
388    }
389
390    #[test]
391    fn a_command_is_encoded_as_an_array_of_bulk_strings() {
392        assert_eq!(
393            encode_command(&[b"SET", b"name", b"ada"]),
394            b"*3\r\n$3\r\nSET\r\n$4\r\nname\r\n$3\r\nada\r\n".to_vec()
395        );
396        assert_eq!(encode_command(&[b"PING"]), b"*1\r\n$4\r\nPING\r\n".to_vec());
397    }
398
399    #[test]
400    fn an_argument_full_of_protocol_syntax_stays_one_argument() {
401        // The whole point of length-prefixing: this cannot become `FLUSHALL`.
402        let hostile = b"value\r\nFLUSHALL\r\n";
403        let encoded = encode_command(&[b"SET", b"key", hostile]);
404
405        let (decoded, _) = decode(&encoded).unwrap().unwrap();
406        let Value::Array(parts) = decoded else { panic!("a command is an array") };
407
408        assert_eq!(parts.len(), 3, "the injected newline must not create a fourth argument");
409        assert_eq!(parts[2], Value::Bulk(hostile.to_vec()));
410    }
411
412    #[test]
413    fn a_reply_that_is_not_resp_at_all_is_reported_as_a_protocol_error() {
414        let error = decode(b"HTTP/1.1 200 OK\r\n").unwrap_err();
415        assert!(error.to_string().contains("Redis server"), "got: {error}");
416    }
417
418    #[test]
419    fn an_implausible_length_is_refused_before_anything_is_allocated() {
420        assert!(decode(b"$999999999999\r\n").is_err());
421        assert!(decode(b"*999999999999\r\n").is_err());
422    }
423
424    #[test]
425    fn a_length_header_that_is_not_a_number_is_a_protocol_error() {
426        assert!(decode(b"$abc\r\n").is_err());
427        assert!(decode(b":not-an-integer\r\n").is_err());
428    }
429
430    #[test]
431    fn values_read_as_text_and_numbers_whatever_form_they_arrived_in() {
432        assert_eq!(Value::Bulk(b"42".to_vec()).as_i64(), Some(42));
433        assert_eq!(Value::Integer(42).as_i64(), Some(42));
434        assert_eq!(Value::Simple("42".into()).as_i64(), Some(42));
435        assert_eq!(Value::Nil.as_i64(), None);
436        assert_eq!(Value::Bulk(b"hello".to_vec()).as_str(), Some("hello"));
437        assert_eq!(Value::Bulk(vec![0xff]).as_str(), None, "invalid UTF-8 is not text");
438    }
439}