yo_resp/proto.rs
1//! The protocol version a connection is speaking, and the limits it is held to.
2
3/// RESP2 or RESP3, per connection.
4///
5/// A connection starts at RESP2 and moves to RESP3 when the client sends
6/// `HELLO 3`. It can move back. The version is a property of the connection and
7/// never of the command, which is why every reply writer takes it once at
8/// construction rather than at each call.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10pub enum Proto {
11 /// The protocol every client understands. Five types and nothing else.
12 #[default]
13 Resp2,
14 /// Redis 6 and later. Maps, sets, doubles, booleans, push messages, and a
15 /// null that is not a length of minus one.
16 Resp3,
17}
18
19impl Proto {
20 /// The number a `HELLO` reply reports, and the number `HELLO` takes.
21 #[inline]
22 pub const fn version(self) -> i64 {
23 match self {
24 Proto::Resp2 => 2,
25 Proto::Resp3 => 3,
26 }
27 }
28
29 /// The protocol for a `HELLO` argument, or `None` if there is no such
30 /// version. `HELLO 4` is an error and this is where that is decided.
31 #[inline]
32 pub const fn from_version(v: i64) -> Option<Proto> {
33 match v {
34 2 => Some(Proto::Resp2),
35 3 => Some(Proto::Resp3),
36 _ => None,
37 }
38 }
39
40 /// Whether the richer type set is available.
41 #[inline]
42 pub const fn is_resp3(self) -> bool {
43 matches!(self, Proto::Resp3)
44 }
45}
46
47/// The bounds the codec enforces, which are Redis's bounds.
48///
49/// These are not tuning knobs, they are the difference between a protocol error
50/// and an allocation the size of whatever a stranger asked for. A count line
51/// saying two billion arguments has to be refused before anything is reserved
52/// for it, which is why the multibulk limit is checked against the parsed
53/// number and not against what arrives.
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub struct Limits {
56 /// The largest `*` count accepted. Redis's `PROTO_MAX_MULTIBULK`.
57 pub max_multibulk: usize,
58 /// The largest `$` length accepted. Redis's `proto-max-bulk-len`, which is
59 /// configurable there and so is configurable here.
60 pub max_bulk: usize,
61 /// The longest an inline request or an unterminated count line may get
62 /// before it is refused. Redis's `PROTO_INLINE_MAX_SIZE`.
63 pub max_inline: usize,
64 /// How deeply a reply may nest before the decoder gives up.
65 ///
66 /// Redis has no equivalent because Redis never parses a reply. This exists
67 /// because the reply decoder recurses, and `*1\r\n` repeated a million
68 /// times would otherwise be a stack overflow rather than an error. There is
69 /// no legitimate reply anywhere near this deep.
70 pub max_depth: usize,
71}
72
73impl Limits {
74 /// Redis's defaults: a million arguments, a 512 MiB bulk, a 64 KiB inline
75 /// request.
76 pub const DEFAULT: Limits = Limits {
77 max_multibulk: 1024 * 1024,
78 max_bulk: 512 * 1024 * 1024,
79 max_inline: 64 * 1024,
80 max_depth: 128,
81 };
82}
83
84impl Default for Limits {
85 fn default() -> Limits {
86 Limits::DEFAULT
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93
94 #[test]
95 fn a_connection_starts_at_resp2() {
96 assert_eq!(Proto::default(), Proto::Resp2);
97 assert!(!Proto::default().is_resp3());
98 }
99
100 #[test]
101 fn hello_takes_two_and_three_and_nothing_else() {
102 assert_eq!(Proto::from_version(2), Some(Proto::Resp2));
103 assert_eq!(Proto::from_version(3), Some(Proto::Resp3));
104 for v in [-1, 0, 1, 4, 300] {
105 assert_eq!(Proto::from_version(v), None, "HELLO {v}");
106 }
107 for p in [Proto::Resp2, Proto::Resp3] {
108 assert_eq!(Proto::from_version(p.version()), Some(p));
109 }
110 }
111
112 #[test]
113 fn the_limits_are_the_redis_numbers() {
114 let l = Limits::default();
115 assert_eq!(l.max_multibulk, 1024 * 1024);
116 assert_eq!(l.max_bulk, 512 * 1024 * 1024);
117 assert_eq!(l.max_inline, 64 * 1024);
118 }
119}