Skip to main content

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 unknown subcommand or wrong number of arguments for 'X'. Try CLIENT
164/// HELP.`
165///
166/// The other sentence a container says, and Redis keeps the two apart by which
167/// end of the parse found the problem. A name that is no subcommand at all gets
168/// the one above, and a subcommand that is real but was handed something it
169/// cannot read gets this one, which is why it names the subcommand rather than
170/// what was wrong with it.
171#[must_use]
172pub fn subcommand_syntax(sub: &[u8], container: &str) -> Error {
173    yo_alloc::allow(|| {
174        Error::fmt(
175            Code::Unsupported,
176            format_args!(
177                "unknown subcommand or wrong number of arguments for '{}'. Try {} HELP.",
178                String::from_utf8_lossy(sub),
179                container
180            ),
181        )
182    })
183}
184
185/// `ERR invalid expire time in 'x' command`.
186#[must_use]
187pub fn invalid_expire(name: &str) -> Error {
188    Error::fmt(
189        Code::Invalid,
190        format_args!("invalid expire time in '{name}' command"),
191    )
192}
193
194/// `ERR unknown command 'X', with args beginning with: 'a' 'b' `.
195///
196/// With no arguments at all the sentence stops early, at `unknown command 'X'`,
197/// and that is a real difference and not a tidier way of saying the same thing.
198/// Redis 8.10.1 answers a bare `NOTACOMMAND` without the second clause, and we
199/// were sending it with an empty list hanging off the end.
200///
201/// Redis quotes each argument, separates them with a space and leaves the
202/// trailing one, which looks like an oversight and is not worth diverging
203/// over. It stops once the argument list reaches 128 bytes rather than echoing
204/// a megabyte back at a client that sent one, and it truncates the argument
205/// that crosses the line rather than dropping it, and so does this.
206///
207/// An argument that is not UTF-8 comes back with the replacement character
208/// where Redis would send the raw bytes, because an error carries a `String`.
209/// It is inside the message for a command that does not exist, so nothing can
210/// be depending on it.
211///
212/// The whole body is inside [`yo_alloc::allow`] rather than only the final
213/// constructor, because the message is built before the constructor sees it
214/// and a shard thread that allocates aborts.
215#[must_use]
216pub fn unknown_command(args: Args<'_>) -> Error {
217    yo_alloc::allow(|| {
218        let mut msg = String::from("unknown command '");
219        msg.push_str(&String::from_utf8_lossy(args.name()));
220        msg.push('\'');
221        if args.len() == 1 {
222            return Error::new(Code::Unsupported, msg);
223        }
224        msg.push_str(", with args beginning with: ");
225        let start = msg.len();
226        for i in 1..args.len() {
227            let used = msg.len() - start;
228            if used >= 128 {
229                break;
230            }
231            let arg = args.get(i);
232            let arg = &arg[..arg.len().min(128 - used)];
233            msg.push('\'');
234            msg.push_str(&String::from_utf8_lossy(arg));
235            msg.push_str("' ");
236        }
237        Error::new(Code::Unsupported, msg)
238    })
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244    use crate::dispatch::tests::encode;
245    use crate::proto::Limits;
246    use crate::request::Step;
247
248    #[test]
249    fn arguments_are_slices_of_the_read_buffer() {
250        let wire = encode(&[b"SET", b"k", b"v"]);
251        let mut argv = Argv::new();
252        assert!(matches!(
253            argv.decode(&wire, &Limits::default()).unwrap(),
254            Step::Command { .. }
255        ));
256        let args = Args::new(&argv, &wire);
257        assert_eq!(args.len(), 3);
258        assert_eq!(args.name(), b"SET");
259        assert_eq!(args.get(2), b"v");
260        // Past the end is empty and not a panic.
261        assert_eq!(args.get(9), b"");
262        assert_eq!(args.opt(9), None);
263    }
264
265    #[test]
266    fn keywords_match_whatever_case_the_client_used() {
267        assert!(is(b"nx", b"NX"));
268        assert!(is(b"Nx", b"NX"));
269        assert!(!is(b"nxx", b"NX"));
270        assert!(!is(b"n", b"NX"));
271    }
272
273    #[test]
274    fn the_unknown_command_message_is_redis_own() {
275        let wire = encode(&[b"NOPE", b"a", b"b"]);
276        let mut argv = Argv::new();
277        argv.decode(&wire, &Limits::default()).unwrap();
278        let e = unknown_command(Args::new(&argv, &wire));
279        // Checked against a real 8.8, trailing space included.
280        assert_eq!(
281            e.message(),
282            "unknown command 'NOPE', with args beginning with: 'a' 'b' "
283        );
284    }
285
286    #[test]
287    fn a_command_with_no_arguments_gets_the_short_sentence() {
288        let wire = encode(&[b"NOPE"]);
289        let mut argv = Argv::new();
290        argv.decode(&wire, &Limits::default()).unwrap();
291        let e = unknown_command(Args::new(&argv, &wire));
292        // Checked against a real 8.10.1. The second clause is not there at all,
293        // rather than being there with nothing after it.
294        assert_eq!(e.message(), "unknown command 'NOPE'");
295    }
296
297    #[test]
298    fn a_client_that_sends_a_megabyte_does_not_get_it_back() {
299        let big = vec![b'x'; 1024];
300        let wire = encode(&[b"NOPE", &big, &big]);
301        let mut argv = Argv::new();
302        argv.decode(&wire, &Limits::default()).unwrap();
303        let e = unknown_command(Args::new(&argv, &wire));
304        // The argument list is 128 bytes of `x` plus the two quotes and the
305        // space Redis puts around it, and the second argument never starts.
306        assert_eq!(
307            e.message().len(),
308            "unknown command 'NOPE', with args beginning with: ".len() + 131
309        );
310    }
311}