Skip to main content

netget/server/redis/
resp3.rs

1//! RESP3: the `HELLO` negotiation and the reply types RESP2 does not have.
2//!
3//! A connection starts in RESP2. `HELLO 3` (answered here, in Rust: it is protocol negotiation,
4//! not data) switches it to RESP3 for the rest of its life, and `HELLO 2` switches it back.
5//! From then on the reply verbs encode for the connection's version, the way Redis itself
6//! does: the RESP3-only types are downgraded on a RESP2 connection (a map becomes a flat
7//! array, a set an array, a double or big number a bulk string, a boolean the integer 1 or 0,
8//! a verbatim string its text), and a push is refused there, because RESP2 has no
9//! out-of-band frame.
10//!
11//! Requests are RESP2 arrays in both versions, so the decoder does not change.
12use anyhow::{bail, ensure, Context, Result};
13use serde_json::Value;
14
15/// How deep a model-authored RESP3 aggregate may nest.
16pub const MAX_REPLY_DEPTH: usize = 32;
17
18/// What `HELLO` asked for.
19#[derive(Debug, Default, PartialEq)]
20pub struct Hello {
21    pub protover: Option<u8>,
22    pub auth: Option<(String, String)>,
23    pub setname: Option<String>,
24}
25
26/// Parse a `HELLO [protover [AUTH username password] [SETNAME clientname]]` command's
27/// arguments (the command name excluded). `Err` carries the RESP error to send.
28pub fn parse_hello(args: &[String]) -> std::result::Result<Hello, String> {
29    let mut hello = Hello::default();
30    let mut rest = args;
31    if let Some(first) = rest.first() {
32        let v: i64 = first
33            .parse()
34            .map_err(|_| "ERR Protocol version is not an integer or out of range".to_string())?;
35        if v != 2 && v != 3 {
36            return Err("NOPROTO unsupported protocol version".into());
37        }
38        hello.protover = Some(v as u8);
39        rest = &rest[1..];
40    }
41    while let Some(opt) = rest.first() {
42        if opt.eq_ignore_ascii_case("AUTH") && rest.len() >= 3 {
43            hello.auth = Some((rest[1].clone(), rest[2].clone()));
44            rest = &rest[3..];
45        } else if opt.eq_ignore_ascii_case("SETNAME") && rest.len() >= 2 {
46            hello.setname = Some(rest[1].clone());
47            rest = &rest[2..];
48        } else {
49            return Err(format!("ERR Syntax error in HELLO option '{opt}'"));
50        }
51    }
52    Ok(hello)
53}
54
55/// The server properties `HELLO` answers with: a map in RESP3, a flat array in RESP2. `mode`
56/// is `standalone`, or `cluster` for a server started with `cluster_nodes`.
57pub fn hello_reply(proto: u8, connection_id: u64, mode: &str) -> Vec<u8> {
58    let fields: Vec<(&str, Value)> = vec![
59        ("server", Value::from("redis")),
60        ("version", Value::from(REPORTED_VERSION)),
61        ("proto", Value::from(proto)),
62        ("id", Value::from(connection_id)),
63        ("mode", Value::from(mode)),
64        ("role", Value::from("master")),
65        ("modules", Value::Array(vec![])),
66    ];
67    let mut out = Vec::new();
68    if proto == 3 {
69        out.extend_from_slice(format!("%{}\r\n", fields.len()).as_bytes());
70    } else {
71        out.extend_from_slice(format!("*{}\r\n", fields.len() * 2).as_bytes());
72    }
73    for (k, v) in fields {
74        out.extend_from_slice(&super::actions::encode_bulk_string(k.as_bytes()));
75        match v {
76            Value::String(s) => {
77                out.extend_from_slice(&super::actions::encode_bulk_string(s.as_bytes()))
78            }
79            Value::Number(n) => {
80                out.extend_from_slice(&super::actions::encode_integer(n.as_i64().unwrap_or(0)))
81            }
82            _ => out.extend_from_slice(b"*0\r\n"),
83        }
84    }
85    out
86}
87
88/// The Redis version `HELLO` reports. Clients gate features on it; 7.4 is the release whose
89/// RESP3 behaviour this follows.
90pub const REPORTED_VERSION: &str = "7.4.0";
91
92/// A null: `_` in RESP3, a nil bulk string in RESP2.
93pub fn null(proto: u8) -> Vec<u8> {
94    if proto == 3 {
95        b"_\r\n".to_vec()
96    } else {
97        super::actions::encode_null()
98    }
99}
100
101/// A double's RESP3 text: `inf`, `-inf`, `nan`, or the number as JSON writes it.
102fn double_text(v: &Value) -> Result<String> {
103    match v {
104        Value::Number(n) => {
105            let f = n.as_f64().context("not a number")?;
106            Ok(if f.fract() == 0.0 && f.abs() < 1e15 {
107                format!("{f:.0}")
108            } else {
109                n.to_string()
110            })
111        }
112        Value::String(s) if matches!(s.as_str(), "inf" | "-inf" | "nan") => Ok(s.clone()),
113        Value::String(s) => {
114            let f: f64 = s.parse().context("value is a number, inf, -inf or nan")?;
115            ensure!(f.is_finite(), "value is a number, inf, -inf or nan");
116            Ok(s.clone())
117        }
118        _ => bail!("value is a number, inf, -inf or nan"),
119    }
120}
121
122pub fn double(v: &Value, proto: u8) -> Result<Vec<u8>> {
123    let text = double_text(v)?;
124    Ok(if proto == 3 {
125        format!(",{text}\r\n").into_bytes()
126    } else {
127        super::actions::encode_bulk_string(text.as_bytes())
128    })
129}
130
131pub fn boolean(b: bool, proto: u8) -> Vec<u8> {
132    if proto == 3 {
133        if b {
134            b"#t\r\n".to_vec()
135        } else {
136            b"#f\r\n".to_vec()
137        }
138    } else {
139        super::actions::encode_integer(i64::from(b))
140    }
141}
142
143pub fn big_number(digits: &str, proto: u8) -> Result<Vec<u8>> {
144    let body = digits.strip_prefix('-').unwrap_or(digits);
145    ensure!(
146        !body.is_empty() && body.len() <= 4096 && body.bytes().all(|b| b.is_ascii_digit()),
147        "value is a decimal integer, optionally negative"
148    );
149    Ok(if proto == 3 {
150        format!("({digits}\r\n").into_bytes()
151    } else {
152        super::actions::encode_bulk_string(digits.as_bytes())
153    })
154}
155
156pub fn verbatim(format: &str, text: &str, proto: u8) -> Result<Vec<u8>> {
157    ensure!(
158        format.len() == 3 && format.bytes().all(|b| b.is_ascii_alphanumeric()),
159        "format is three letters, such as txt or mkd"
160    );
161    Ok(if proto == 3 {
162        let body = format!("{format}:{text}");
163        let mut out = format!("={}\r\n", body.len()).into_bytes();
164        out.extend_from_slice(body.as_bytes());
165        out.extend_from_slice(b"\r\n");
166        out
167    } else {
168        super::actions::encode_bulk_string(text.as_bytes())
169    })
170}
171
172/// One element of a RESP3 aggregate, from JSON: a string is a bulk string, an integer a RESP
173/// integer, a fraction a double, a boolean a boolean, null a null, an array an array and an
174/// object a map.
175pub fn value(v: &Value, depth: usize) -> Result<Vec<u8>> {
176    ensure!(
177        depth <= MAX_REPLY_DEPTH,
178        "the reply nests deeper than {MAX_REPLY_DEPTH} levels"
179    );
180    Ok(match v {
181        Value::String(s) => super::actions::encode_bulk_string(s.as_bytes()),
182        Value::Number(n) => match n.as_i64() {
183            Some(i) => super::actions::encode_integer(i),
184            None => double(v, 3)?,
185        },
186        Value::Bool(b) => boolean(*b, 3),
187        Value::Null => null(3),
188        Value::Array(items) => aggregate(b'*', items, depth)?,
189        Value::Object(map) => {
190            let mut out = format!("%{}\r\n", map.len()).into_bytes();
191            for (k, v) in map {
192                out.extend_from_slice(&super::actions::encode_bulk_string(k.as_bytes()));
193                out.extend_from_slice(&value(v, depth + 1)?);
194            }
195            out
196        }
197    })
198}
199
200fn aggregate(kind: u8, items: &[Value], depth: usize) -> Result<Vec<u8>> {
201    let mut out = vec![kind];
202    out.extend_from_slice(format!("{}\r\n", items.len()).as_bytes());
203    for item in items {
204        out.extend_from_slice(&value(item, depth + 1)?);
205    }
206    Ok(out)
207}
208
209/// A map's entries, in order: a JSON object, or `[[key, value], …]` where order matters.
210pub fn entries(v: &Value) -> Result<Vec<(Value, Value)>> {
211    match v {
212        Value::Object(map) => Ok(map
213            .iter()
214            .map(|(k, v)| (Value::String(k.clone()), v.clone()))
215            .collect()),
216        Value::Array(pairs) => pairs
217            .iter()
218            .map(|p| match p.as_array().map(Vec::as_slice) {
219                Some([k, v]) => Ok((k.clone(), v.clone())),
220                _ => bail!("each entry is a [key, value] pair"),
221            })
222            .collect(),
223        _ => bail!("entries is an object or an array of [key, value] pairs"),
224    }
225}
226
227/// A map: `%` in RESP3, a flat key-value array in RESP2 (elements as `redis_array` encodes them).
228pub fn map(entries: &[(Value, Value)], proto: u8) -> Result<Vec<u8>> {
229    if proto == 3 {
230        let mut out = format!("%{}\r\n", entries.len()).into_bytes();
231        for (k, v) in entries {
232            out.extend_from_slice(&value(k, 1)?);
233            out.extend_from_slice(&value(v, 1)?);
234        }
235        Ok(out)
236    } else {
237        let flat: Vec<Value> = entries
238            .iter()
239            .flat_map(|(k, v)| [k.clone(), v.clone()])
240            .collect();
241        Ok(super::actions::encode_array(&flat))
242    }
243}
244
245/// A set: `~` in RESP3, an array in RESP2.
246pub fn set(items: &[Value], proto: u8) -> Result<Vec<u8>> {
247    if proto == 3 {
248        aggregate(b'~', items, 0)
249    } else {
250        Ok(super::actions::encode_array(items))
251    }
252}
253
254/// An array: nested aggregates and RESP3 scalars in RESP3; `redis_array`'s RESP2 rules otherwise.
255pub fn array(items: &[Value], proto: u8) -> Result<Vec<u8>> {
256    if proto == 3 {
257        aggregate(b'*', items, 0)
258    } else {
259        Ok(super::actions::encode_array(items))
260    }
261}
262
263/// An out-of-band push (`>`), RESP3 only.
264pub fn push(items: &[Value], proto: u8) -> Result<Vec<u8>> {
265    ensure!(
266        proto == 3,
267        "this connection speaks RESP2, which has no push frame; a client must send HELLO 3 first"
268    );
269    ensure!(
270        !items.is_empty(),
271        "a push names its kind as its first element"
272    );
273    aggregate(b'>', items, 0)
274}