yo_resp/dispatch/args.rs
1//! The arguments a command body sees, and the errors it raises about them.
2//!
3//! A command never gets a `Vec<Vec<u8>>`. It gets this, which is the decoder's
4//! ranges and the connection's read buffer travelling together, so an argument
5//! is a slice of the bytes that came off the socket and nothing is copied
6//! between the two. That is the whole reason `Argv` records ranges instead of
7//! building strings, and a dispatcher that immediately materialised them would
8//! have thrown the saving away at the first opportunity.
9//!
10//! The error helpers are here because they are the same handful of sentences
11//! over and over, and because they have to be Redis's sentences exactly. A
12//! client that matches on the text of `ERR value is not an integer or out of
13//! range` is doing something ugly, and it is doing it against every Redis
14//! deployment in the world, so the text is part of the contract.
15
16use crate::request::Argv;
17use yo_common::num::{parse_f64, parse_i64};
18use yo_common::{Code, Error, Result};
19
20/// What Redis says when an argument should have been an integer and was not.
21pub const NOT_AN_INT: &str = "value is not an integer or out of range";
22/// What Redis says when an argument should have been a float and was not.
23pub const NOT_A_FLOAT: &str = "value is not a valid float";
24/// What Redis says about an option it did not expect where it found it.
25pub const SYNTAX: &str = "syntax error";
26
27/// One command's arguments, borrowed from the connection's read buffer.
28///
29/// Index zero is the command name, the same as Redis's `argv`, so an argument
30/// index in this file matches the argument index in Redis's source and in its
31/// error messages.
32#[derive(Clone, Copy)]
33pub struct Args<'a> {
34 argv: &'a Argv,
35 buf: &'a [u8],
36}
37
38impl<'a> Args<'a> {
39 /// The arguments of the command `argv` last decoded out of `buf`.
40 #[must_use]
41 pub fn new(argv: &'a Argv, buf: &'a [u8]) -> Args<'a> {
42 Args { argv, buf }
43 }
44
45 /// How many arguments there are, counting the command name.
46 #[must_use]
47 pub fn len(&self) -> usize {
48 self.argv.len()
49 }
50
51 /// Whether there is not even a command name.
52 #[must_use]
53 pub fn is_empty(&self) -> bool {
54 self.argv.is_empty()
55 }
56
57 /// Argument `i`, or an empty slice past the end.
58 ///
59 /// Past the end is empty rather than a panic because every caller has
60 /// already been through the arity check, so an index past the end is a bug
61 /// in the arity table rather than something a client can cause, and a
62 /// wrong answer in the reply is a better way to find that bug than a
63 /// process that stops answering.
64 #[must_use]
65 pub fn get(&self, i: usize) -> &'a [u8] {
66 self.argv.arg(self.buf, i).unwrap_or(b"")
67 }
68
69 /// Argument `i`, or `None` past the end.
70 #[must_use]
71 pub fn opt(&self, i: usize) -> Option<&'a [u8]> {
72 self.argv.arg(self.buf, i)
73 }
74
75 /// The command name, which is argument zero.
76 #[must_use]
77 pub fn name(&self) -> &'a [u8] {
78 self.get(0)
79 }
80
81 /// Argument `i` as an integer, with Redis's message when it is not one.
82 ///
83 /// # Errors
84 ///
85 /// [`Code::Invalid`] when the argument is not an integer in `i64`.
86 pub fn int(&self, i: usize) -> Result<i64> {
87 parse_i64(self.get(i)).ok_or_else(|| Error::new(Code::Invalid, NOT_AN_INT).at(i as u32))
88 }
89
90 /// Argument `i` as a float, with Redis's message when it is not one.
91 ///
92 /// # Errors
93 ///
94 /// [`Code::Invalid`] when the argument is not a float, which includes NaN.
95 pub fn float(&self, i: usize) -> Result<f64> {
96 parse_f64(self.get(i)).ok_or_else(|| Error::new(Code::Invalid, NOT_A_FLOAT).at(i as u32))
97 }
98}
99
100/// Whether an argument is this keyword, ignoring case the way Redis does.
101///
102/// Option keywords are matched case insensitively and command names are too,
103/// so `set k v nx` works and always has. This compares in place rather than
104/// upper casing into a buffer, because the buffer would be the only allocation
105/// on the whole dispatch path.
106#[must_use]
107pub fn is(arg: &[u8], keyword: &[u8]) -> bool {
108 arg.len() == keyword.len() && arg.eq_ignore_ascii_case(keyword)
109}
110
111/// `ERR syntax error`, which is what an option in the wrong place gets.
112#[must_use]
113pub fn syntax() -> Error {
114 Error::new(Code::Invalid, SYNTAX)
115}
116
117/// `ERR wrong number of arguments for 'x' command`.
118///
119/// The name is lower cased into the message because Redis reports the name
120/// from its own table and not the spelling the client used, so `GET` and `get`
121/// produce the same sentence.
122#[must_use]
123pub fn wrong_arity(name: &str) -> Error {
124 Error::fmt(
125 Code::Invalid,
126 format_args!("wrong number of arguments for '{name}' command"),
127 )
128}
129
130/// `ERR wrong number of arguments for 'config|get' command`.
131///
132/// A subcommand is reported with the container in front of it and a bar
133/// between, which is how Redis names them everywhere including in `COMMAND
134/// INFO`.
135#[must_use]
136pub fn wrong_arity_sub(name: &str, sub: &str) -> Error {
137 Error::fmt(
138 Code::Invalid,
139 format_args!("wrong number of arguments for '{name}|{sub}' command"),
140 )
141}
142
143/// `ERR unknown subcommand 'X'. Try CONFIG HELP.`
144///
145/// The subcommand is quoted exactly as the client spelled it, which is what a
146/// real server does, so `config nosuch` and `config NOSUCH` produce different
147/// sentences. The container is upper case because that is how Redis writes it
148/// in this message and lower case everywhere else.
149#[must_use]
150pub fn unknown_subcommand(sub: &[u8], container: &str) -> Error {
151 yo_alloc::allow(|| {
152 Error::fmt(
153 Code::Unsupported,
154 format_args!(
155 "unknown subcommand '{}'. Try {} HELP.",
156 String::from_utf8_lossy(sub),
157 container
158 ),
159 )
160 })
161}
162
163/// `ERR invalid expire time in 'x' command`.
164#[must_use]
165pub fn invalid_expire(name: &str) -> Error {
166 Error::fmt(
167 Code::Invalid,
168 format_args!("invalid expire time in '{name}' command"),
169 )
170}
171
172/// `ERR unknown command 'X', with args beginning with: 'a' 'b' `.
173///
174/// With no arguments at all the sentence stops early, at `unknown command 'X'`,
175/// and that is a real difference and not a tidier way of saying the same thing.
176/// Redis 8.10.1 answers a bare `NOTACOMMAND` without the second clause, and we
177/// were sending it with an empty list hanging off the end.
178///
179/// Redis quotes each argument, separates them with a space and leaves the
180/// trailing one, which looks like an oversight and is not worth diverging
181/// over. It stops once the argument list reaches 128 bytes rather than echoing
182/// a megabyte back at a client that sent one, and it truncates the argument
183/// that crosses the line rather than dropping it, and so does this.
184///
185/// An argument that is not UTF-8 comes back with the replacement character
186/// where Redis would send the raw bytes, because an error carries a `String`.
187/// It is inside the message for a command that does not exist, so nothing can
188/// be depending on it.
189///
190/// The whole body is inside [`yo_alloc::allow`] rather than only the final
191/// constructor, because the message is built before the constructor sees it
192/// and a shard thread that allocates aborts.
193#[must_use]
194pub fn unknown_command(args: Args<'_>) -> Error {
195 yo_alloc::allow(|| {
196 let mut msg = String::from("unknown command '");
197 msg.push_str(&String::from_utf8_lossy(args.name()));
198 msg.push('\'');
199 if args.len() == 1 {
200 return Error::new(Code::Unsupported, msg);
201 }
202 msg.push_str(", with args beginning with: ");
203 let start = msg.len();
204 for i in 1..args.len() {
205 let used = msg.len() - start;
206 if used >= 128 {
207 break;
208 }
209 let arg = args.get(i);
210 let arg = &arg[..arg.len().min(128 - used)];
211 msg.push('\'');
212 msg.push_str(&String::from_utf8_lossy(arg));
213 msg.push_str("' ");
214 }
215 Error::new(Code::Unsupported, msg)
216 })
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222 use crate::dispatch::tests::encode;
223 use crate::proto::Limits;
224 use crate::request::Step;
225
226 #[test]
227 fn arguments_are_slices_of_the_read_buffer() {
228 let wire = encode(&[b"SET", b"k", b"v"]);
229 let mut argv = Argv::new();
230 assert!(matches!(
231 argv.decode(&wire, &Limits::default()).unwrap(),
232 Step::Command { .. }
233 ));
234 let args = Args::new(&argv, &wire);
235 assert_eq!(args.len(), 3);
236 assert_eq!(args.name(), b"SET");
237 assert_eq!(args.get(2), b"v");
238 // Past the end is empty and not a panic.
239 assert_eq!(args.get(9), b"");
240 assert_eq!(args.opt(9), None);
241 }
242
243 #[test]
244 fn keywords_match_whatever_case_the_client_used() {
245 assert!(is(b"nx", b"NX"));
246 assert!(is(b"Nx", b"NX"));
247 assert!(!is(b"nxx", b"NX"));
248 assert!(!is(b"n", b"NX"));
249 }
250
251 #[test]
252 fn the_unknown_command_message_is_redis_own() {
253 let wire = encode(&[b"NOPE", b"a", b"b"]);
254 let mut argv = Argv::new();
255 argv.decode(&wire, &Limits::default()).unwrap();
256 let e = unknown_command(Args::new(&argv, &wire));
257 // Checked against a real 8.8, trailing space included.
258 assert_eq!(
259 e.message(),
260 "unknown command 'NOPE', with args beginning with: 'a' 'b' "
261 );
262 }
263
264 #[test]
265 fn a_command_with_no_arguments_gets_the_short_sentence() {
266 let wire = encode(&[b"NOPE"]);
267 let mut argv = Argv::new();
268 argv.decode(&wire, &Limits::default()).unwrap();
269 let e = unknown_command(Args::new(&argv, &wire));
270 // Checked against a real 8.10.1. The second clause is not there at all,
271 // rather than being there with nothing after it.
272 assert_eq!(e.message(), "unknown command 'NOPE'");
273 }
274
275 #[test]
276 fn a_client_that_sends_a_megabyte_does_not_get_it_back() {
277 let big = vec![b'x'; 1024];
278 let wire = encode(&[b"NOPE", &big, &big]);
279 let mut argv = Argv::new();
280 argv.decode(&wire, &Limits::default()).unwrap();
281 let e = unknown_command(Args::new(&argv, &wire));
282 // The argument list is 128 bytes of `x` plus the two quotes and the
283 // space Redis puts around it, and the second argument never starts.
284 assert_eq!(
285 e.message().len(),
286 "unknown command 'NOPE', with args beginning with: ".len() + 131
287 );
288 }
289}