Skip to main content

yo_resp/dispatch/
keyspec.rs

1//! Where a command's keys are, and what it does to each of them.
2//!
3//! # Why the old triple was not enough
4//!
5//! Until now the answer was three numbers on every table row: the first
6//! argument that is a key, the last, and how far apart they sit. That triple is
7//! Redis's own legacy answer, it is what `COMMAND INFO` has reported since 2.8,
8//! and it is wrong often enough to matter. `ZUNIONSTORE dst 2 a b` has three
9//! keys and the triple names one of them. `XREAD COUNT 2 STREAMS a b 0 0` has
10//! two and the triple names none. `SORT k STORE d` has two and the triple names
11//! one.
12//!
13//! Nothing minded very much while the only caller was `COMMAND GETKEYS`, where
14//! a wrong answer is a cluster client routing to the wrong node and a real but
15//! distant problem. The ACL is a different matter. A user given `~cache:*` and
16//! nothing else must not be able to reach `secret` through `ZUNIONSTORE`, and a
17//! permission check built on a key finder that misses keys is not a permission
18//! check. So this is the model Redis replaced the triple with in 7.0, copied
19//! rather than approximated, and the triple stays beside it because
20//! `COMMAND INFO` still reports it.
21//!
22//! # The model
23//!
24//! A command has a list of key specs and each one is two questions. Where does
25//! this run of keys start, which is [`Begin`], and how far does it go, which is
26//! [`Find`]. `ZUNIONSTORE` has two: one key at argument one, and a counted run
27//! starting at argument two. `SMOVE` has two, one for each end of the move,
28//! which is how the source can be a delete and the destination an insert.
29//!
30//! Beside them are the flags, and those are the half the ACL reads. `RO`, `RW`,
31//! `OW` and `RM` say how the value is touched, and `access`, `update`, `insert`
32//! and `delete` say what a caller has to be allowed to do: a key with `access`
33//! needs read permission and a key with any of the other three needs write
34//! permission. That is why `SET k v` needs only write while `INCR k` needs both,
35//! which is not a rule anybody would guess and is not derivable from the command
36//! flags. It comes off the reference one command at a time.
37//!
38//! # What incomplete means
39//!
40//! Three specs cannot be trusted on their own and say so. `SORT`'s `BY`, `GET`
41//! and `STORE` keys are [`Begin::Unknown`], because the first two are patterns
42//! that name keys only after the sorted value has been read and the third can
43//! appear anywhere. `XREAD` and `XREADGROUP` look for the word `STREAMS`, which
44//! a stream could be called. `MIGRATE` and the two `GEORADIUS` writes look for a
45//! keyword that may appear twice.
46//!
47//! A spec carrying `incomplete`, or one whose search is unknown, means the
48//! answer this module gives is a floor rather than the whole of it. `COMMAND
49//! GETKEYS` fills the rest in per command, and the ACL treats a command with one
50//! as needing permission over every key, which is the safe way round: a user
51//! who may reach every key is allowed and one who may not is refused.
52
53use super::args::Args;
54use super::table::Spec;
55use yo_common::parse_i64;
56
57/// One run of keys in a command's arguments.
58#[derive(Debug, Clone, Copy)]
59pub struct KeySpec {
60    /// What the reference says about this spec, empty for most of them.
61    ///
62    /// Carried because `COMMAND DOCS` reports it and because every one of them
63    /// explains a spec that would otherwise read as a mistake.
64    pub notes: &'static str,
65    /// How the value is touched and what a caller has to be allowed to do.
66    pub flags: &'static [&'static str],
67    /// Where the run starts.
68    pub begin: Begin,
69    /// How far it goes.
70    pub find: Find,
71}
72
73/// Where a run of keys starts.
74#[derive(Debug, Clone, Copy)]
75pub enum Begin {
76    /// At a fixed argument, which is nearly all of them.
77    At(u32),
78    /// After a keyword, searched for from an argument.
79    ///
80    /// A positive start searches forward from there and a negative one searches
81    /// backward from that far before the end, which is how `MIGRATE` finds a
82    /// `KEYS` that has to be the last option.
83    After(&'static [u8], i32),
84    /// Somewhere only the command itself can work out.
85    Unknown,
86}
87
88/// How far a run of keys goes.
89#[derive(Debug, Clone, Copy)]
90pub enum Find {
91    /// A fixed run, counting from the start of it.
92    ///
93    /// `last` is relative: nought is one key, and a negative number counts back
94    /// from the last argument. `limit` is nought except where the run is one
95    /// part of what is left, which is `XREAD` splitting the tail into keys and
96    /// ids.
97    Range {
98        /// The last key, relative to the start of the run.
99        last: i32,
100        /// How many arguments apart consecutive keys are.
101        step: u32,
102        /// How many ways what is left is divided, nought for all but `XREAD`.
103        limit: u32,
104    },
105    /// A run whose length is a number in the arguments.
106    ///
107    /// `count` is where that number is, measured from the start of the run, and
108    /// `first` is where the keys begin from the same place.
109    Counted {
110        /// Where the number sits, from the start of the run.
111        count: u32,
112        /// Where the keys begin, from the same place.
113        first: u32,
114        /// How many arguments apart consecutive keys are.
115        step: u32,
116    },
117    /// A run only the command itself can work out.
118    Unknown,
119}
120
121/// What a caller has to be allowed to do with a key.
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
123pub struct Access(u8);
124
125impl Access {
126    /// The value is read, so a caller needs read permission over the key.
127    pub const READ: Access = Access(1);
128    /// The value is changed, added or taken away.
129    pub const WRITE: Access = Access(2);
130    /// Both, which is what an incomplete command is treated as needing.
131    pub const BOTH: Access = Access(3);
132    /// Neither, which is a key a command only asks about.
133    pub const NONE: Access = Access(0);
134
135    /// Whether everything `other` asks for is in here.
136    #[must_use]
137    pub const fn covers(self, other: Access) -> bool {
138        self.0 & other.0 == other.0
139    }
140
141    /// Both together.
142    #[must_use]
143    pub const fn and(self, other: Access) -> Access {
144        Access(self.0 | other.0)
145    }
146
147    /// Whether this asks for nothing at all.
148    #[must_use]
149    pub const fn is_none(self) -> bool {
150        self.0 == 0
151    }
152
153    /// The two bits on their own, for something that has to store a lot of
154    /// these and cannot spare a byte each.
155    ///
156    /// The ACL is the caller. A user can carry hundreds of key patterns and
157    /// every one of them holds a copy of this, so it is packed in beside the
158    /// glob rather than kept as a type.
159    #[must_use]
160    pub const fn bits(self) -> u8 {
161        self.0
162    }
163
164    /// The other half of [`Access::bits`].
165    ///
166    /// Anything outside the two bits is dropped rather than trusted, so a
167    /// caller that has stored the byte somewhere and read back rubbish gets a
168    /// permission that is too small rather than one that is too large.
169    #[must_use]
170    pub const fn from_bits(bits: u8) -> Access {
171        Access(bits & 3)
172    }
173}
174
175impl KeySpec {
176    /// What a caller needs over the keys this spec names.
177    #[must_use]
178    pub fn access(&self) -> Access {
179        access_of(self.flags)
180    }
181
182    /// Whether this spec is a floor rather than the whole answer.
183    #[must_use]
184    pub fn incomplete(&self) -> bool {
185        self.flags.contains(&"incomplete")
186            || matches!(self.begin, Begin::Unknown)
187            || matches!(self.find, Find::Unknown)
188    }
189
190    /// Whether this spec names an argument that looks like a key and is not.
191    ///
192    /// `SPUBLISH`'s channel is one, and it is the reason that command answers
193    /// `COMMAND GETKEYS` with an error rather than the channel name.
194    #[must_use]
195    pub fn fake(&self) -> bool {
196        self.flags.contains(&"not_key")
197    }
198
199    /// How many arguments apart consecutive keys are, which is never nought.
200    fn step(&self) -> u32 {
201        match self.find {
202            Find::Range { step, .. } | Find::Counted { step, .. } => step.max(1),
203            Find::Unknown => 1,
204        }
205    }
206}
207
208/// One run of keys, resolved against the arguments a client actually sent.
209#[derive(Debug, Clone, Copy)]
210pub struct Run {
211    /// The argument index of the first key.
212    pub first: usize,
213    /// How many keys there are, which can be nought.
214    pub count: usize,
215    /// How many arguments apart consecutive keys are.
216    pub step: usize,
217    /// The flags every key in the run carries, which is a spec's own list.
218    pub flags: &'static [&'static str],
219}
220
221impl Run {
222    /// What a caller needs over every key in this run.
223    #[must_use]
224    pub fn need(&self) -> Access {
225        access_of(self.flags)
226    }
227}
228
229/// What a caller needs over a key carrying `flags`.
230///
231/// `access` is read and any of `update`, `insert` and `delete` is write, which is
232/// Redis's own rule. A key flagged `not_key` is an argument that looks like one
233/// and is not, so it asks for nothing: that is `GEORADIUS`'s member and `LPOS`'s
234/// element.
235#[must_use]
236pub fn access_of(flags: &[&str]) -> Access {
237    if flags.contains(&"not_key") {
238        return Access::NONE;
239    }
240    let mut need = Access::NONE;
241    if flags.contains(&"access") {
242        need = need.and(Access::READ);
243    }
244    if flags.contains(&"update") || flags.contains(&"insert") || flags.contains(&"delete") {
245        need = need.and(Access::WRITE);
246    }
247    need
248}
249
250/// Every key `spec` names in `args`, handed to `each` a run at a time.
251///
252/// `base` is how many arguments sit in front of the command itself, which is two
253/// for `COMMAND GETKEYS <command> ...` and nought for a command that is running.
254///
255/// False means the arguments do not resolve: a count that is not a number, a run
256/// that reaches past the end, a keyword that cannot be where it says it is. That
257/// is a command that is about to fail on its own arguments, and the caller
258/// decides what to do about it. Nothing was handed to `each` in that case.
259///
260/// The specs are tried first and a finder is what is left when they cannot
261/// answer, which is the order `getKeysFromCommandWithSpecs` goes in and is not
262/// the same as choosing between them. `PFMERGE dst` is the plainest case: its
263/// second spec starts at argument two of a two argument command, so the specs
264/// fail and the finder is the only thing that names the destination.
265pub fn find(spec: &Spec, args: Args<'_>, base: usize, each: &mut dyn FnMut(Run)) -> bool {
266    let keys = keys_of(spec, args, base);
267    let real = keys.iter().any(|k| !k.fake());
268    // Three commands decide from their options what they do to their key, and
269    // for those the flags a spec carries are the wrong ones, so the specs are
270    // skipped rather than tried.
271    let varying = keys.iter().any(|k| k.flags.contains(&"variable_flags"));
272    if real && !varying && walk(keys, args, base, each) {
273        return true;
274    }
275    if let Some(finder) = finder_for(spec.name) {
276        return finder(args, base, each);
277    }
278    // No finder, so either the specs failed and the command is about to fail
279    // with them, or there were never any specs and there are no keys.
280    !real
281}
282
283/// Whether this command ever names a key, whatever it is sent.
284///
285/// This is not the same question as whether it names one here. `GEORADIUS`
286/// without a `STORE` names one key and `SUBSCRIBE` names none ever, and the
287/// second is the one a real server refuses `COMMAND GETKEYS` for. A container is
288/// asked about the word behind it, since that is where its keys live.
289pub fn takes_keys(spec: &Spec, args: Args<'_>, base: usize) -> bool {
290    finder_for(spec.name).is_some() || keys_of(spec, args, base).iter().any(|k| !k.fake())
291}
292
293/// The specs to read for this command, which for a container are its
294/// subcommand's.
295///
296/// A container row carries none of its own, so it is asked about the word behind
297/// it. Every other row with no specs simply has no keys.
298fn keys_of(spec: &Spec, args: Args<'_>, base: usize) -> &'static [KeySpec] {
299    if !spec.keys.is_empty() {
300        return spec.keys;
301    }
302    if args.len() <= base + 1 {
303        return &[];
304    }
305    of_sub(spec.name, args.get(base + 1))
306}
307
308/// Every key a list of specs names, with no per command finder in the way.
309fn walk(keys: &[KeySpec], args: Args<'_>, base: usize, each: &mut dyn FnMut(Run)) -> bool {
310    // Nothing is handed over until every spec has resolved, because a command
311    // that fails halfway names no keys at all rather than the ones found so far.
312    let mut runs = [None::<Run>; MOST_SPECS];
313    let mut at = 0;
314    for key in keys {
315        // An argument that looks like a key and is not is not one here either.
316        if key.fake() {
317            continue;
318        }
319        match resolve(key, args, base) {
320            Resolved::Run(run) => {
321                if at == runs.len() {
322                    return false;
323                }
324                runs[at] = Some(run);
325                at += 1;
326                // A spec that says it is a floor has given what it can, and what
327                // is left is the finder's, so this is a failure like any other.
328                if key.incomplete() {
329                    return false;
330                }
331            }
332            // A keyword that is not there is not a failure. `MIGRATE k 0 1 0` has
333            // no `KEYS` in it and no keys hiding behind one, and the spec that
334            // looks for it has simply found that out.
335            Resolved::None => {}
336            Resolved::Invalid => return false,
337        }
338    }
339    for run in runs.iter().flatten() {
340        if run.count > 0 {
341            each(*run);
342        }
343    }
344    true
345}
346
347/// The most key specs any one command has, which is `GEORADIUS` with three.
348const MOST_SPECS: usize = 4;
349
350/// What one spec came to against one set of arguments.
351enum Resolved {
352    /// A run of keys, which can be an empty one.
353    Run(Run),
354    /// The keyword this spec looks for is not there, so it names nothing.
355    None,
356    /// The arguments do not fit the spec, so the command is about to fail.
357    Invalid,
358}
359
360/// One spec against one set of arguments.
361///
362/// This is `getKeysUsingKeySpecs` line for line, including the two places it
363/// looks careless and is not. The forward keyword search stops one short of the
364/// end, because a keyword in the last argument has no key behind it. And a run
365/// that reaches past the last argument is invalid rather than cut short, because
366/// the command it came from is about to be refused on its arity anyway and a
367/// short answer would be a wrong one.
368fn resolve(key: &KeySpec, args: Args<'_>, base: usize) -> Resolved {
369    let argc = (args.len() - base) as i64;
370    let at = |i: i64| args.get(base + i as usize);
371    let mut first = match key.begin {
372        Begin::At(index) => i64::from(index),
373        Begin::After(word, from) => {
374            // Forward from where it says, stopping one short of the end, or
375            // backward from that far before the end, stopping at argument two.
376            // Both directions are the reference's own loop: the bound is a stop
377            // rather than a limit, and running off either end gives up quietly.
378            let start = if from > 0 {
379                i64::from(from)
380            } else {
381                argc + i64::from(from)
382            };
383            let end = if from > 0 { argc - 1 } else { 1 };
384            let mut found = 0;
385            let mut i = start;
386            while i != end {
387                if i >= argc || i < 1 {
388                    break;
389                }
390                if at(i).eq_ignore_ascii_case(word) {
391                    found = i + 1;
392                    break;
393                }
394                i += if start <= end { 1 } else { -1 };
395            }
396            if found == 0 {
397                return Resolved::None;
398            }
399            found
400        }
401        Begin::Unknown => return Resolved::Invalid,
402    };
403    let step = i64::from(key.step());
404    let last = match key.find {
405        Find::Range { last, limit, .. } => {
406            if last >= 0 {
407                first + i64::from(last)
408            } else if limit == 0 {
409                argc + i64::from(last)
410            } else {
411                // The run is one part of what is left, which only `XREAD` and
412                // `XREADGROUP` are: the tail after `STREAMS` is half keys and
413                // half ids, so the last key is halfway along it.
414                first + ((argc - first) / i64::from(limit) + i64::from(last))
415            }
416        }
417        Find::Counted {
418            count, first: from, ..
419        } => {
420            let index = first + i64::from(count);
421            if index >= argc || index < 0 {
422                return Resolved::Invalid;
423            }
424            // A count is read as a number from nought upward and nothing else, so
425            // a negative one or a word is the command failing rather than a
426            // command with no keys.
427            let Some(n) = parse_i64(at(index)).filter(|&n| n >= 0) else {
428                return Resolved::Invalid;
429            };
430            first += i64::from(from);
431            match n
432                .checked_sub(1)
433                .and_then(|n| n.checked_mul(step))
434                .and_then(|n| first.checked_add(n))
435            {
436                Some(last) => last,
437                None => return Resolved::Invalid,
438            }
439        }
440        Find::Unknown => return Resolved::Invalid,
441    };
442    // Off either end is a syntax error rather than a shorter run, because the
443    // command it came from is about to be refused anyway and a short answer
444    // would be a wrong one.
445    if last >= argc || last < first || first >= argc {
446        return Resolved::Invalid;
447    }
448    Resolved::Run(Run {
449        first: base + first as usize,
450        count: ((last - first) / step + 1) as usize,
451        step: step as usize,
452        flags: key.flags,
453    })
454}
455
456// ------------------------------------------------------------- the finders
457//
458// Eight commands keep their keys somewhere a spec cannot say, and three more
459// decide what they do to a key from the options they were given. A real server
460// answers all eleven with a function written for that command, and so does this:
461// the specs are what `COMMAND INFO` reports and these are what everything else
462// reads, which is exactly the split the reference has.
463
464/// The flags a key that is only read carries.
465const READ: &[&str] = &["RO", "access"];
466/// The flags a destination written over carries.
467const OVERWRITE: &[&str] = &["OW", "update"];
468/// The flags a key read and written carries.
469const BOTH: &[&str] = &["RW", "access", "update"];
470/// The flags `MIGRATE` gives every key it moves.
471const MOVED: &[&str] = &["RW", "access", "delete"];
472/// The flags `PFMERGE` gives the counter it merges into.
473const MERGED: &[&str] = &["RW", "access", "insert"];
474/// The flags a key compared against a value and then removed carries.
475const COMPARED: &[&str] = &["RW", "delete"];
476/// The flags a key removed without its value being looked at carries.
477const REMOVED: &[&str] = &["RM", "delete"];
478
479/// A finder written for one command, standing in for specs that cannot say
480/// where its keys are or what it does to them.
481type Finder = fn(Args<'_>, usize, &mut dyn FnMut(Run)) -> bool;
482
483/// The finder for `name`, for the eleven commands that need one.
484fn finder_for(name: &str) -> Option<Finder> {
485    Some(match name {
486        "sort" => sort_keys,
487        "sort_ro" => sort_ro_keys,
488        "migrate" => migrate_keys,
489        "xread" | "xreadgroup" => xread_keys,
490        "georadius" | "georadiusbymember" => georadius_keys,
491        "set" => set_keys,
492        "bitfield" => bitfield_keys,
493        "delex" => delex_keys,
494        "pfmerge" => pfmerge_keys,
495        _ => return None,
496    })
497}
498
499/// One key at one argument.
500fn one(at: usize, flags: &'static [&'static str], each: &mut dyn FnMut(Run)) {
501    each(Run {
502        first: at,
503        count: 1,
504        step: 1,
505        flags,
506    });
507}
508
509/// `SORT key [BY pat] [LIMIT o c] [GET pat ...] [STORE dst]`.
510///
511/// The `BY` and `GET` patterns name keys and are not reported, because which keys
512/// they name is only known once the sorted value has been read. A real server
513/// deals with that by refusing the pattern outright when the user cannot reach
514/// every key, which is the ACL's problem rather than this one.
515///
516/// `STORE` is last wins, so a command naming two destinations names the second.
517fn sort_keys(args: Args<'_>, base: usize, each: &mut dyn FnMut(Run)) -> bool {
518    one(base + 1, READ, each);
519    let argc = args.len() - base;
520    let mut store = None;
521    let mut i = 2;
522    while i < argc {
523        let arg = args.get(base + i);
524        if arg.eq_ignore_ascii_case(b"limit") {
525            i += 2;
526        } else if arg.eq_ignore_ascii_case(b"get") || arg.eq_ignore_ascii_case(b"by") {
527            i += 1;
528        } else if arg.eq_ignore_ascii_case(b"store") && i + 1 < argc {
529            store = Some(base + i + 1);
530        }
531        i += 1;
532    }
533    if let Some(at) = store {
534        one(at, OVERWRITE, each);
535    }
536    true
537}
538
539/// `SORT_RO key [BY pat] [LIMIT o c] [GET pat ...]`, which has no destination.
540fn sort_ro_keys(_args: Args<'_>, base: usize, each: &mut dyn FnMut(Run)) -> bool {
541    one(base + 1, READ, each);
542    true
543}
544
545/// `MIGRATE host port key|"" db timeout [COPY] [REPLACE] [AUTH pw] [AUTH2 u pw] [KEYS k ...]`.
546///
547/// The single key form names argument three and the `KEYS` form names everything
548/// behind the keyword. Naming both is a syntax error the command itself reports,
549/// so this names nothing and lets it.
550fn migrate_keys(args: Args<'_>, base: usize, each: &mut dyn FnMut(Run)) -> bool {
551    let argc = args.len() - base;
552    let mut first = 3;
553    let mut count = 1;
554    if argc > 6 {
555        let mut i = 6;
556        while i < argc {
557            let arg = args.get(base + i);
558            if arg.eq_ignore_ascii_case(b"keys") {
559                if args.get(base + 3).is_empty() {
560                    first = i + 1;
561                    count = argc - first;
562                } else {
563                    count = 0;
564                }
565                break;
566            }
567            if arg.eq_ignore_ascii_case(b"auth") {
568                i += 1;
569            } else if arg.eq_ignore_ascii_case(b"auth2") {
570                i += 2;
571            }
572            i += 1;
573        }
574    }
575    if count > 0 {
576        each(Run {
577            first: base + first,
578            count,
579            step: 1,
580            flags: MOVED,
581        });
582    }
583    true
584}
585
586/// `XREAD [COUNT n] [BLOCK ms] STREAMS key ... id ...` and the group form.
587///
588/// The options in front are walked rather than counted, because a stream, a
589/// group or a consumer may itself be called `STREAMS` and the first one that is
590/// an option value has to be stepped over rather than matched.
591fn xread_keys(args: Args<'_>, base: usize, each: &mut dyn FnMut(Run)) -> bool {
592    let argc = args.len() - base;
593    let mut streams = None;
594    let mut i = 1;
595    while i < argc {
596        let arg = args.get(base + i);
597        if arg.eq_ignore_ascii_case(b"block") || arg.eq_ignore_ascii_case(b"count") {
598            i += 1;
599        } else if arg.eq_ignore_ascii_case(b"group") {
600            i += 2;
601        } else if arg.eq_ignore_ascii_case(b"noack") {
602            // Nothing follows it.
603        } else if arg.eq_ignore_ascii_case(b"streams") {
604            streams = Some(i);
605            break;
606        } else {
607            // Anything else is a syntax error the command will report.
608            break;
609        }
610        i += 1;
611    }
612    let Some(streams) = streams else {
613        return false;
614    };
615    let tail = argc - streams - 1;
616    if tail == 0 || !tail.is_multiple_of(2) {
617        return false;
618    }
619    each(Run {
620        first: base + streams + 1,
621        count: tail / 2,
622        step: 1,
623        flags: READ,
624    });
625    true
626}
627
628/// `GEORADIUS key ... [STORE dst] [STOREDIST dst]` and the member form.
629///
630/// Both destinations write to the same slot in a real server, so naming `STORE`
631/// and `STOREDIST` names one key and it is the last of the two.
632fn georadius_keys(args: Args<'_>, base: usize, each: &mut dyn FnMut(Run)) -> bool {
633    one(base + 1, READ, each);
634    let argc = args.len() - base;
635    let mut store = None;
636    let mut i = 5;
637    while i < argc {
638        let arg = args.get(base + i);
639        if (arg.eq_ignore_ascii_case(b"store") || arg.eq_ignore_ascii_case(b"storedist"))
640            && i + 1 < argc
641        {
642            store = Some(base + i + 1);
643            i += 1;
644        }
645        i += 1;
646    }
647    if let Some(at) = store {
648        one(at, OVERWRITE, each);
649    }
650    true
651}
652
653/// `SET key value [GET] ...`, which reads the key only when it is asked to.
654///
655/// Without `GET` the old value is never looked at, so a user who may write the
656/// key and not read it may run it. That is the whole of what `variable_flags`
657/// means and it is why this cannot be a spec.
658fn set_keys(args: Args<'_>, base: usize, each: &mut dyn FnMut(Run)) -> bool {
659    let gets = (base + 3..args.len()).any(|i| args.get(i).eq_ignore_ascii_case(b"get"));
660    one(base + 1, if gets { BOTH } else { OVERWRITE }, each);
661    true
662}
663
664/// `BITFIELD key [GET ...] [SET ...] [INCRBY ...] [OVERFLOW ...]`.
665///
666/// A command that only gets is a read, and anything else, including a command
667/// that is about to be refused for its syntax, is a read and a write.
668fn bitfield_keys(args: Args<'_>, base: usize, each: &mut dyn FnMut(Run)) -> bool {
669    let argc = args.len() - base;
670    let mut reads = true;
671    let mut i = 2;
672    while i < argc {
673        let left = argc - i - 1;
674        let arg = args.get(base + i);
675        if arg.eq_ignore_ascii_case(b"get") && left >= 2 {
676            i += 2;
677        } else if (arg.eq_ignore_ascii_case(b"set") || arg.eq_ignore_ascii_case(b"incrby"))
678            && left >= 3
679        {
680            reads = false;
681            break;
682        } else if arg.eq_ignore_ascii_case(b"overflow") && left >= 1 {
683            i += 1;
684        } else {
685            reads = false;
686            break;
687        }
688        i += 1;
689    }
690    one(base + 1, if reads { READ } else { BOTH }, each);
691    true
692}
693
694/// `DELEX key [IFEQ v|IFNE v|IFDEQ d|IFDNE d]`.
695///
696/// A condition of any of the four kinds reads the key before deciding, so it is
697/// `RW`. Without one the key goes whatever is in it, which is `RM`. Neither form
698/// needs read permission, so the two answers differ only in the letters
699/// `COMMAND GETKEYSANDFLAGS` prints, which is a thing worth getting right on its
700/// own.
701fn delex_keys(args: Args<'_>, base: usize, each: &mut dyn FnMut(Run)) -> bool {
702    let compares = args.opt(base + 2).is_some_and(|a| {
703        a.eq_ignore_ascii_case(b"ifeq")
704            || a.eq_ignore_ascii_case(b"ifne")
705            || a.eq_ignore_ascii_case(b"ifdeq")
706            || a.eq_ignore_ascii_case(b"ifdne")
707    });
708    one(base + 1, if compares { COMPARED } else { REMOVED }, each);
709    true
710}
711
712/// `PFMERGE dst [src ...]`, whose sources are optional.
713///
714/// The specs cannot say that, because a run starting at argument two of a two
715/// argument command is off the end and off the end is a syntax error. So a real
716/// server answers this one from a function and `PFMERGE dst` names its
717/// destination rather than failing.
718fn pfmerge_keys(args: Args<'_>, base: usize, each: &mut dyn FnMut(Run)) -> bool {
719    one(base + 1, MERGED, each);
720    let argc = args.len() - base;
721    if argc > 2 {
722        each(Run {
723            first: base + 2,
724            count: argc - 2,
725            step: 1,
726            flags: READ,
727        });
728    }
729    true
730}
731
732// --------------------------------------------------------------- the shapes
733//
734// Every distinct spec in the table, named once and shared by every command that
735// has it. Fifty eight of them cover four hundred and twenty nine commands, and
736// naming them rather than writing each row out is what keeps a key spec
737// readable next to the command it belongs to.
738//
739// The numbers are the reference's own, read off `COMMAND INFO` on 8.10.1 rather
740// than out of the documentation. The commands a real server has never heard of,
741// which is the module groups, have a spec built from the legacy triple their row
742// already carried, with read permission for a command flagged `readonly` and
743// both for one flagged `write`.
744
745/// not_key.
746pub const NOT_KEY_AT1: KeySpec = KeySpec {
747    notes: "",
748    flags: &["not_key"],
749    begin: Begin::At(1),
750    find: Find::Range {
751        last: 0,
752        step: 1,
753        limit: 0,
754    },
755};
756
757/// not_key.
758pub const NOT_KEY_AT1_RM1_1_0: KeySpec = KeySpec {
759    notes: "",
760    flags: &["not_key"],
761    begin: Begin::At(1),
762    find: Find::Range {
763        last: -1,
764        step: 1,
765        limit: 0,
766    },
767};
768
769/// OW, insert.
770pub const OW_INSERT_AT1: KeySpec = KeySpec {
771    notes: "",
772    flags: &["OW", "insert"],
773    begin: Begin::At(1),
774    find: Find::Range {
775        last: 0,
776        step: 1,
777        limit: 0,
778    },
779};
780
781/// OW, insert.
782pub const OW_INSERT_AT1_RM1_2_0: KeySpec = KeySpec {
783    notes: "",
784    flags: &["OW", "insert"],
785    begin: Begin::At(1),
786    find: Find::Range {
787        last: -1,
788        step: 2,
789        limit: 0,
790    },
791};
792
793/// OW, insert.
794pub const OW_INSERT_AT2: KeySpec = KeySpec {
795    notes: "",
796    flags: &["OW", "insert"],
797    begin: Begin::At(2),
798    find: Find::Range {
799        last: 0,
800        step: 1,
801        limit: 0,
802    },
803};
804
805/// OW, update.
806pub const OW_UPDATE_AT1: KeySpec = KeySpec {
807    notes: "",
808    flags: &["OW", "update"],
809    begin: Begin::At(1),
810    find: Find::Range {
811        last: 0,
812        step: 1,
813        limit: 0,
814    },
815};
816
817/// OW, update.
818pub const OW_UPDATE_AT1_COUNTED: KeySpec = KeySpec {
819    notes: "",
820    flags: &["OW", "update"],
821    begin: Begin::At(1),
822    find: Find::Counted {
823        count: 0,
824        first: 1,
825        step: 2,
826    },
827};
828
829/// OW, update.
830pub const OW_UPDATE_AT1_RM1_2_0: KeySpec = KeySpec {
831    notes: "",
832    flags: &["OW", "update"],
833    begin: Begin::At(1),
834    find: Find::Range {
835        last: -1,
836        step: 2,
837        limit: 0,
838    },
839};
840
841/// OW, update.
842pub const OW_UPDATE_AT2: KeySpec = KeySpec {
843    notes: "",
844    flags: &["OW", "update"],
845    begin: Begin::At(2),
846    find: Find::Range {
847        last: 0,
848        step: 1,
849        limit: 0,
850    },
851};
852
853/// Incomplete because duplicate STORE options use last-wins; fall back to georadiusGetKeys
854pub const GEORADIUS_STORE: KeySpec = KeySpec {
855    notes: "Incomplete because duplicate STORE options use last-wins; fall back to georadiusGetKeys",
856    flags: &["OW", "update", "incomplete"],
857    begin: Begin::After(b"STORE", 6),
858    find: Find::Range {
859        last: 0,
860        step: 1,
861        limit: 0,
862    },
863};
864
865/// Incomplete because duplicate STOREDIST options use last-wins; fall back to georadiusGetKeys
866pub const GEORADIUS_STOREDIST: KeySpec = KeySpec {
867    notes: "Incomplete because duplicate STOREDIST options use last-wins; fall back to georadiusGetKeys",
868    flags: &["OW", "update", "incomplete"],
869    begin: Begin::After(b"STOREDIST", 6),
870    find: Find::Range {
871        last: 0,
872        step: 1,
873        limit: 0,
874    },
875};
876
877/// Incomplete because duplicate STOREDIST options use last-wins; fall back to georadiusGetKeys
878pub const BYMEMBER_STOREDIST: KeySpec = KeySpec {
879    notes: "Incomplete because duplicate STOREDIST options use last-wins; fall back to georadiusGetKeys",
880    flags: &["OW", "update", "incomplete"],
881    begin: Begin::After(b"STOREDIST", 5),
882    find: Find::Range {
883        last: 0,
884        step: 1,
885        limit: 0,
886    },
887};
888
889/// Incomplete because duplicate STORE options use last-wins; fall back to georadiusGetKeys
890pub const BYMEMBER_STORE: KeySpec = KeySpec {
891    notes: "Incomplete because duplicate STORE options use last-wins; fall back to georadiusGetKeys",
892    flags: &["OW", "update", "incomplete"],
893    begin: Begin::After(b"STORE", 5),
894    find: Find::Range {
895        last: 0,
896        step: 1,
897        limit: 0,
898    },
899};
900
901/// For the optional STORE keyword. It is marked 'unknown' because the keyword can appear anywhere in the argument array
902pub const SORT_STORE: KeySpec = KeySpec {
903    notes: "For the optional STORE keyword. It is marked 'unknown' because the keyword can appear anywhere in the argument array",
904    flags: &["OW", "update"],
905    begin: Begin::Unknown,
906    find: Find::Unknown,
907};
908
909/// RM, delete.
910pub const RM_DELETE_AT1_RM1_1_0: KeySpec = KeySpec {
911    notes: "",
912    flags: &["RM", "delete"],
913    begin: Begin::At(1),
914    find: Find::Range {
915        last: -1,
916        step: 1,
917        limit: 0,
918    },
919};
920
921/// RO, access.
922pub const RO_ACCESS_AT1: KeySpec = KeySpec {
923    notes: "",
924    flags: &["RO", "access"],
925    begin: Begin::At(1),
926    find: Find::Range {
927        last: 0,
928        step: 1,
929        limit: 0,
930    },
931};
932
933/// RO, access.
934pub const RO_ACCESS_AT1_COUNTED: KeySpec = KeySpec {
935    notes: "",
936    flags: &["RO", "access"],
937    begin: Begin::At(1),
938    find: Find::Counted {
939        count: 0,
940        first: 1,
941        step: 1,
942    },
943};
944
945/// RO, access.
946pub const RO_ACCESS_AT1_R1_1_0: KeySpec = KeySpec {
947    notes: "",
948    flags: &["RO", "access"],
949    begin: Begin::At(1),
950    find: Find::Range {
951        last: 1,
952        step: 1,
953        limit: 0,
954    },
955};
956
957/// RO, access.
958pub const RO_ACCESS_AT1_RM1_1_0: KeySpec = KeySpec {
959    notes: "",
960    flags: &["RO", "access"],
961    begin: Begin::At(1),
962    find: Find::Range {
963        last: -1,
964        step: 1,
965        limit: 0,
966    },
967};
968
969/// RO, access.
970pub const RO_ACCESS_AT1_RM2_1_0: KeySpec = KeySpec {
971    notes: "",
972    flags: &["RO", "access"],
973    begin: Begin::At(1),
974    find: Find::Range {
975        last: -2,
976        step: 1,
977        limit: 0,
978    },
979};
980
981/// RO, access.
982pub const RO_ACCESS_AT2: KeySpec = KeySpec {
983    notes: "",
984    flags: &["RO", "access"],
985    begin: Begin::At(2),
986    find: Find::Range {
987        last: 0,
988        step: 1,
989        limit: 0,
990    },
991};
992
993/// We cannot tell how the keys will be used so we assume the worst, RO and ACCESS
994pub const SCRIPT_KEYS_RO: KeySpec = KeySpec {
995    notes: "We cannot tell how the keys will be used so we assume the worst, RO and ACCESS",
996    flags: &["RO", "access"],
997    begin: Begin::At(2),
998    find: Find::Counted {
999        count: 0,
1000        first: 1,
1001        step: 1,
1002    },
1003};
1004
1005/// RO, access.
1006pub const RO_ACCESS_AT2_COUNTED: KeySpec = KeySpec {
1007    notes: "",
1008    flags: &["RO", "access"],
1009    begin: Begin::At(2),
1010    find: Find::Counted {
1011        count: 0,
1012        first: 1,
1013        step: 1,
1014    },
1015};
1016
1017/// RO, access.
1018pub const RO_ACCESS_AT2_RM1_1_0: KeySpec = KeySpec {
1019    notes: "",
1020    flags: &["RO", "access"],
1021    begin: Begin::At(2),
1022    find: Find::Range {
1023        last: -1,
1024        step: 1,
1025        limit: 0,
1026    },
1027};
1028
1029/// RO, access.
1030pub const RO_ACCESS_AT3_RM1_1_0: KeySpec = KeySpec {
1031    notes: "",
1032    flags: &["RO", "access"],
1033    begin: Begin::At(3),
1034    find: Find::Range {
1035        last: -1,
1036        step: 1,
1037        limit: 0,
1038    },
1039};
1040
1041/// Incomplete because a stream key named STREAMS (or options before it) can shift the STREAMS keyword; fall back to xreadGetKeys
1042pub const XREAD_STREAMS: KeySpec = KeySpec {
1043    notes: "Incomplete because a stream key named STREAMS (or options before it) can shift the STREAMS keyword; fall back to xreadGetKeys",
1044    flags: &["RO", "access", "incomplete"],
1045    begin: Begin::After(b"STREAMS", 1),
1046    find: Find::Range {
1047        last: -1,
1048        step: 1,
1049        limit: 2,
1050    },
1051};
1052
1053/// Incomplete because a consumer/group named STREAMS (or options before GROUP) can shift the STREAMS keyword; fall back to xreadGetKeys
1054pub const XREADGROUP_STREAMS: KeySpec = KeySpec {
1055    notes: "Incomplete because a consumer/group named STREAMS (or options before GROUP) can shift the STREAMS keyword; fall back to xreadGetKeys",
1056    flags: &["RO", "access", "incomplete"],
1057    begin: Begin::After(b"STREAMS", 4),
1058    find: Find::Range {
1059        last: -1,
1060        step: 1,
1061        limit: 2,
1062    },
1063};
1064
1065/// For the optional BY/GET keyword. It is marked 'unknown' because the key names derive from the content of the key we sort
1066pub const SORT_BY_AND_GET: KeySpec = KeySpec {
1067    notes: "For the optional BY/GET keyword. It is marked 'unknown' because the key names derive from the content of the key we sort",
1068    flags: &["RO", "access"],
1069    begin: Begin::Unknown,
1070    find: Find::Unknown,
1071};
1072
1073/// RO.
1074pub const RO_AT1: KeySpec = KeySpec {
1075    notes: "",
1076    flags: &["RO"],
1077    begin: Begin::At(1),
1078    find: Find::Range {
1079        last: 0,
1080        step: 1,
1081        limit: 0,
1082    },
1083};
1084
1085/// RO.
1086pub const RO_AT1_RM1_1_0: KeySpec = KeySpec {
1087    notes: "",
1088    flags: &["RO"],
1089    begin: Begin::At(1),
1090    find: Find::Range {
1091        last: -1,
1092        step: 1,
1093        limit: 0,
1094    },
1095};
1096
1097/// RO.
1098pub const RO_AT2: KeySpec = KeySpec {
1099    notes: "",
1100    flags: &["RO"],
1101    begin: Begin::At(2),
1102    find: Find::Range {
1103        last: 0,
1104        step: 1,
1105        limit: 0,
1106    },
1107};
1108
1109/// RW because it may change the internal representation of the key, and propagate to replicas
1110pub const RW_ACCESS_AT1_RM1_1_0: KeySpec = KeySpec {
1111    notes: "RW because it may change the internal representation of the key, and propagate to replicas",
1112    flags: &["RW", "access"],
1113    begin: Begin::At(1),
1114    find: Find::Range {
1115        last: -1,
1116        step: 1,
1117        limit: 0,
1118    },
1119};
1120
1121/// RW, access.
1122pub const RW_ACCESS_AT2: KeySpec = KeySpec {
1123    notes: "",
1124    flags: &["RW", "access"],
1125    begin: Begin::At(2),
1126    find: Find::Range {
1127        last: 0,
1128        step: 1,
1129        limit: 0,
1130    },
1131};
1132
1133/// RW, access, delete.
1134pub const RW_ACCESS_DELETE_AT1: KeySpec = KeySpec {
1135    notes: "",
1136    flags: &["RW", "access", "delete"],
1137    begin: Begin::At(1),
1138    find: Find::Range {
1139        last: 0,
1140        step: 1,
1141        limit: 0,
1142    },
1143};
1144
1145/// RW, access, delete.
1146pub const RW_ACCESS_DELETE_AT1_COUNTED: KeySpec = KeySpec {
1147    notes: "",
1148    flags: &["RW", "access", "delete"],
1149    begin: Begin::At(1),
1150    find: Find::Counted {
1151        count: 0,
1152        first: 1,
1153        step: 1,
1154    },
1155};
1156
1157/// RW, access, delete.
1158pub const RW_ACCESS_DELETE_AT1_RM2_1_0: KeySpec = KeySpec {
1159    notes: "",
1160    flags: &["RW", "access", "delete"],
1161    begin: Begin::At(1),
1162    find: Find::Range {
1163        last: -2,
1164        step: 1,
1165        limit: 0,
1166    },
1167};
1168
1169/// RW, access, delete.
1170pub const RW_ACCESS_DELETE_AT2_COUNTED: KeySpec = KeySpec {
1171    notes: "",
1172    flags: &["RW", "access", "delete"],
1173    begin: Begin::At(2),
1174    find: Find::Counted {
1175        count: 0,
1176        first: 1,
1177        step: 1,
1178    },
1179};
1180
1181/// RW, access, delete.
1182pub const RW_ACCESS_DELETE_AT3: KeySpec = KeySpec {
1183    notes: "",
1184    flags: &["RW", "access", "delete"],
1185    begin: Begin::At(3),
1186    find: Find::Range {
1187        last: 0,
1188        step: 1,
1189        limit: 0,
1190    },
1191};
1192
1193/// RW, access, delete, incomplete.
1194pub const MIGRATE_KEYS: KeySpec = KeySpec {
1195    notes: "",
1196    flags: &["RW", "access", "delete", "incomplete"],
1197    begin: Begin::After(b"KEYS", -2),
1198    find: Find::Range {
1199        last: -1,
1200        step: 1,
1201        limit: 0,
1202    },
1203};
1204
1205/// RW, access, insert.
1206pub const RW_ACCESS_INSERT_AT1: KeySpec = KeySpec {
1207    notes: "",
1208    flags: &["RW", "access", "insert"],
1209    begin: Begin::At(1),
1210    find: Find::Range {
1211        last: 0,
1212        step: 1,
1213        limit: 0,
1214    },
1215};
1216
1217/// RW, access, update.
1218pub const RW_ACCESS_UPDATE_AT1: KeySpec = KeySpec {
1219    notes: "",
1220    flags: &["RW", "access", "update"],
1221    begin: Begin::At(1),
1222    find: Find::Range {
1223        last: 0,
1224        step: 1,
1225        limit: 0,
1226    },
1227};
1228
1229/// RW and UPDATE because it changes the TTL
1230pub const RW_ACCESS_UPDATE_AT1_TTL: KeySpec = KeySpec {
1231    notes: "RW and UPDATE because it changes the TTL",
1232    flags: &["RW", "access", "update"],
1233    begin: Begin::At(1),
1234    find: Find::Range {
1235        last: 0,
1236        step: 1,
1237        limit: 0,
1238    },
1239};
1240
1241/// RW, access, update.
1242pub const RW_ACCESS_UPDATE_AT1_R1_1_0: KeySpec = KeySpec {
1243    notes: "",
1244    flags: &["RW", "access", "update"],
1245    begin: Begin::At(1),
1246    find: Find::Range {
1247        last: 1,
1248        step: 1,
1249        limit: 0,
1250    },
1251};
1252
1253/// RW, access, update.
1254pub const RW_ACCESS_UPDATE_AT1_RM1_3_0: KeySpec = KeySpec {
1255    notes: "",
1256    flags: &["RW", "access", "update"],
1257    begin: Begin::At(1),
1258    find: Find::Range {
1259        last: -1,
1260        step: 3,
1261        limit: 0,
1262    },
1263};
1264
1265/// RW, access, update.
1266pub const RW_ACCESS_UPDATE_AT2: KeySpec = KeySpec {
1267    notes: "",
1268    flags: &["RW", "access", "update"],
1269    begin: Begin::At(2),
1270    find: Find::Range {
1271        last: 0,
1272        step: 1,
1273        limit: 0,
1274    },
1275};
1276
1277/// We cannot tell how the keys will be used so we assume the worst, RW and UPDATE
1278pub const SCRIPT_KEYS_RW: KeySpec = KeySpec {
1279    notes: "We cannot tell how the keys will be used so we assume the worst, RW and UPDATE",
1280    flags: &["RW", "access", "update"],
1281    begin: Begin::At(2),
1282    find: Find::Counted {
1283        count: 0,
1284        first: 1,
1285        step: 1,
1286    },
1287};
1288
1289/// RW, access, update.
1290pub const RW_ACCESS_UPDATE_AT2_COUNTED: KeySpec = KeySpec {
1291    notes: "",
1292    flags: &["RW", "access", "update"],
1293    begin: Begin::At(2),
1294    find: Find::Counted {
1295        count: 0,
1296        first: 1,
1297        step: 1,
1298    },
1299};
1300
1301/// This command allows both access and modification of the key
1302pub const BITFIELD_KEY: KeySpec = KeySpec {
1303    notes: "This command allows both access and modification of the key",
1304    flags: &["RW", "access", "update", "variable_flags"],
1305    begin: Begin::At(1),
1306    find: Find::Range {
1307        last: 0,
1308        step: 1,
1309        limit: 0,
1310    },
1311};
1312
1313/// RW and ACCESS due to the optional `GET` argument
1314pub const SET_KEY: KeySpec = KeySpec {
1315    notes: "RW and ACCESS due to the optional `GET` argument",
1316    flags: &["RW", "access", "update", "variable_flags"],
1317    begin: Begin::At(1),
1318    find: Find::Range {
1319        last: 0,
1320        step: 1,
1321        limit: 0,
1322    },
1323};
1324
1325/// RW, delete.
1326pub const RW_DELETE_AT1: KeySpec = KeySpec {
1327    notes: "",
1328    flags: &["RW", "delete"],
1329    begin: Begin::At(1),
1330    find: Find::Range {
1331        last: 0,
1332        step: 1,
1333        limit: 0,
1334    },
1335};
1336
1337/// RW, delete.
1338pub const RW_DELETE_AT2: KeySpec = KeySpec {
1339    notes: "",
1340    flags: &["RW", "delete"],
1341    begin: Begin::At(2),
1342    find: Find::Range {
1343        last: 0,
1344        step: 1,
1345        limit: 0,
1346    },
1347};
1348
1349/// RW, delete, variable_flags.
1350pub const DELEX_KEY: KeySpec = KeySpec {
1351    notes: "",
1352    flags: &["RW", "delete", "variable_flags"],
1353    begin: Begin::At(1),
1354    find: Find::Range {
1355        last: 0,
1356        step: 1,
1357        limit: 0,
1358    },
1359};
1360
1361/// RW, insert.
1362pub const RW_INSERT_AT1: KeySpec = KeySpec {
1363    notes: "",
1364    flags: &["RW", "insert"],
1365    begin: Begin::At(1),
1366    find: Find::Range {
1367        last: 0,
1368        step: 1,
1369        limit: 0,
1370    },
1371};
1372
1373/// RW, insert.
1374pub const RW_INSERT_AT2: KeySpec = KeySpec {
1375    notes: "",
1376    flags: &["RW", "insert"],
1377    begin: Begin::At(2),
1378    find: Find::Range {
1379        last: 0,
1380        step: 1,
1381        limit: 0,
1382    },
1383};
1384
1385/// RW, update.
1386pub const RW_UPDATE_AT1: KeySpec = KeySpec {
1387    notes: "",
1388    flags: &["RW", "update"],
1389    begin: Begin::At(1),
1390    find: Find::Range {
1391        last: 0,
1392        step: 1,
1393        limit: 0,
1394    },
1395};
1396
1397/// UPDATE instead of INSERT because of the optional trimming feature
1398pub const RW_UPDATE_AT1_TRIMMING: KeySpec = KeySpec {
1399    notes: "UPDATE instead of INSERT because of the optional trimming feature",
1400    flags: &["RW", "update"],
1401    begin: Begin::At(1),
1402    find: Find::Range {
1403        last: 0,
1404        step: 1,
1405        limit: 0,
1406    },
1407};
1408
1409/// RW, update.
1410pub const RW_UPDATE_AT2: KeySpec = KeySpec {
1411    notes: "",
1412    flags: &["RW", "update"],
1413    begin: Begin::At(2),
1414    find: Find::Range {
1415        last: 0,
1416        step: 1,
1417        limit: 0,
1418    },
1419};
1420
1421/// RW, update, delete.
1422pub const RW_UPDATE_DELETE_AT1: KeySpec = KeySpec {
1423    notes: "",
1424    flags: &["RW", "update", "delete"],
1425    begin: Begin::At(1),
1426    find: Find::Range {
1427        last: 0,
1428        step: 1,
1429        limit: 0,
1430    },
1431};
1432
1433// ---------------------------------------------------------- the subcommands
1434//
1435// The container commands whose keys sit inside a subcommand. The table has one
1436// row a container and none a subcommand, which is D-114, so these are named
1437// here until it does.
1438
1439/// A container, one of its subcommands, and where that subcommand's keys are.
1440type SubSpec = (&'static [u8], &'static [u8], &'static [KeySpec]);
1441
1442/// The fifteen subcommands that take a key.
1443static SUBS: &[SubSpec] = &[
1444    (b"himport", b"set", &[OW_UPDATE_AT2]),
1445    // `JSON.DEBUG MEMORY key [path]` is the only subcommand of a module
1446    // container that takes one, and the reference has never heard of it, so this
1447    // row is ours rather than measured.
1448    (b"json.debug", b"memory", &[RO_ACCESS_AT2]),
1449    (b"memory", b"usage", &[RO_AT2]),
1450    (b"object", b"encoding", &[RO_AT2]),
1451    (b"object", b"freq", &[RO_AT2]),
1452    (b"object", b"idletime", &[RO_AT2]),
1453    (b"object", b"refcount", &[RO_AT2]),
1454    (b"xgroup", b"create", &[RW_INSERT_AT2]),
1455    (b"xgroup", b"createconsumer", &[RW_INSERT_AT2]),
1456    (b"xgroup", b"delconsumer", &[RW_DELETE_AT2]),
1457    (b"xgroup", b"destroy", &[RW_DELETE_AT2]),
1458    (b"xgroup", b"setid", &[RW_UPDATE_AT2]),
1459    (b"xinfo", b"consumers", &[RO_ACCESS_AT2]),
1460    (b"xinfo", b"groups", &[RO_ACCESS_AT2]),
1461    (b"xinfo", b"stream", &[RO_ACCESS_AT2]),
1462];
1463
1464/// The key specs for `container sub`, empty for a subcommand that has none.
1465#[must_use]
1466pub fn of_sub(container: &str, sub: &[u8]) -> &'static [KeySpec] {
1467    SUBS.iter()
1468        .find(|(c, s, _)| *c == container.as_bytes() && s.eq_ignore_ascii_case(sub))
1469        .map_or(&[][..], |(_, _, keys)| keys)
1470}
1471
1472#[cfg(test)]
1473mod tests {
1474    use super::*;
1475    use crate::dispatch::table;
1476    use crate::proto::Limits;
1477    use crate::request::{Argv, Step};
1478
1479    /// Every key a command names, with the flags each one carries.
1480    ///
1481    /// `None` where the arguments do not resolve, which is a command about to
1482    /// fail. Every expected answer here was read off a real 8.10.1 first.
1483    fn flagged(words: &[&str]) -> Option<Vec<(String, Vec<&'static str>)>> {
1484        let mut buf = format!("*{}\r\n", words.len()).into_bytes();
1485        for word in words {
1486            buf.extend_from_slice(format!("${}\r\n{word}\r\n", word.len()).as_bytes());
1487        }
1488        let mut argv = Argv::new();
1489        let Ok(Step::Command { .. }) = argv.decode(&buf, &Limits::default()) else {
1490            panic!("the test wrote a command that does not decode");
1491        };
1492        let args = Args::new(&argv, &buf);
1493        let spec = table::lookup(args.name()).expect("a command this server has");
1494        let mut found = Vec::new();
1495        let whole = find(spec, args, 0, &mut |run| {
1496            for i in 0..run.count {
1497                let key = args.get(run.first + i * run.step);
1498                found.push((
1499                    String::from_utf8_lossy(key).into_owned(),
1500                    run.flags.to_vec(),
1501                ));
1502            }
1503        });
1504        whole.then_some(found)
1505    }
1506
1507    /// The names alone, for the cases where the flags are not the point.
1508    fn named(words: &[&str]) -> Option<Vec<String>> {
1509        flagged(words).map(|keys| keys.into_iter().map(|(key, _)| key).collect())
1510    }
1511
1512    #[test]
1513    fn a_counted_run_names_every_key_it_counts() {
1514        assert_eq!(
1515            named(&["zunionstore", "d", "2", "a", "b"]).unwrap(),
1516            ["d", "a", "b"]
1517        );
1518        assert_eq!(
1519            named(&["lmpop", "2", "a", "b", "LEFT"]).unwrap(),
1520            ["a", "b"]
1521        );
1522        assert_eq!(named(&["smove", "a", "b", "m"]).unwrap(), ["a", "b"]);
1523    }
1524
1525    #[test]
1526    fn a_run_that_reaches_past_the_last_argument_names_nothing() {
1527        // Not two keys and not one. The command is about to be refused, and a
1528        // short answer would send a cluster client to the wrong node.
1529        assert_eq!(named(&["zunionstore", "d", "3", "a"]), None);
1530        assert_eq!(named(&["zunionstore", "d", "-1", "a"]), None);
1531        assert_eq!(named(&["zunionstore", "d", "x", "a"]), None);
1532        // A count of nought resolves to a run ending before it starts, which is
1533        // the same refusal, and the caller turns it into an empty reply for the
1534        // commands that are allowed to name no keys.
1535        assert_eq!(named(&["eval", "body", "0"]), None);
1536    }
1537
1538    #[test]
1539    fn the_streams_keyword_is_walked_to_rather_than_counted_from() {
1540        let two = ["xread", "COUNT", "2", "STREAMS", "a", "b", "0", "0"];
1541        assert_eq!(named(&two).unwrap(), ["a", "b"]);
1542        // An odd tail is a stream with no id or an id with no stream.
1543        assert_eq!(named(&["xread", "STREAMS", "a", "b", "0"]), None);
1544        // A group called STREAMS is stepped over rather than matched.
1545        let group = ["xreadgroup", "GROUP", "STREAMS", "c", "STREAMS", "a", ">"];
1546        assert_eq!(named(&group).unwrap(), ["a"]);
1547    }
1548
1549    #[test]
1550    fn pfmerge_names_its_destination_when_it_was_given_no_sources() {
1551        // The specs cannot say this, because a source run starting at argument
1552        // two of a two argument command is off the end.
1553        assert_eq!(named(&["pfmerge", "k0"]).unwrap(), ["k0"]);
1554        assert_eq!(named(&["pfmerge", "d", "a", "b"]).unwrap(), ["d", "a", "b"]);
1555    }
1556
1557    #[test]
1558    fn an_argument_that_only_looks_like_a_key_is_not_one() {
1559        let spec = table::lookup(b"spublish").expect("a command this server has");
1560        let mut argv = Argv::new();
1561        let buf = b"*3\r\n$8\r\nspublish\r\n$2\r\nch\r\n$1\r\nm\r\n";
1562        let Ok(Step::Command { .. }) = argv.decode(buf, &Limits::default()) else {
1563            panic!("the test wrote a command that does not decode");
1564        };
1565        assert!(!takes_keys(spec, Args::new(&argv, buf), 0));
1566    }
1567
1568    #[test]
1569    fn a_last_wins_destination_is_the_last_one_written() {
1570        let two = ["sort", "k", "STORE", "a", "STORE", "b"];
1571        assert_eq!(named(&two).unwrap(), ["k", "b"]);
1572        let geo = [
1573            "georadius",
1574            "k",
1575            "0",
1576            "0",
1577            "1",
1578            "m",
1579            "STORE",
1580            "d",
1581            "STOREDIST",
1582            "e",
1583        ];
1584        assert_eq!(named(&geo).unwrap(), ["k", "e"]);
1585        // A pattern names keys only once the value has been read, so neither BY
1586        // nor GET is reported and neither is the word STORE inside one.
1587        assert_eq!(
1588            named(&["sort", "k", "BY", "STORE", "GET", "d"]).unwrap(),
1589            ["k"]
1590        );
1591    }
1592
1593    #[test]
1594    fn a_keyword_the_command_does_not_carry_is_not_a_failure() {
1595        let single = ["migrate", "h", "1", "k", "0", "0"];
1596        assert_eq!(named(&single).unwrap(), ["k"]);
1597        let listed = ["migrate", "h", "1", "", "0", "0", "KEYS", "a", "b"];
1598        assert_eq!(named(&listed).unwrap(), ["a", "b"]);
1599        // The keyword is looked for backward from two before the end, so a
1600        // command that stops on the word itself has nothing behind it and the
1601        // empty key in the middle is what is left.
1602        assert_eq!(
1603            named(&["migrate", "h", "1", "", "0", "0", "KEYS"]).unwrap(),
1604            [""]
1605        );
1606    }
1607
1608    #[test]
1609    fn what_a_command_does_to_a_key_can_depend_on_its_options() {
1610        let flags = |words: &[&str]| flagged(words).unwrap()[0].1.clone();
1611        assert_eq!(flags(&["set", "k", "v"]), ["OW", "update"]);
1612        assert_eq!(flags(&["set", "k", "v", "GET"]), ["RW", "access", "update"]);
1613        assert_eq!(
1614            flags(&["bitfield", "k", "GET", "u8", "0"]),
1615            ["RO", "access"]
1616        );
1617        assert_eq!(
1618            flags(&["bitfield", "k", "SET", "u8", "0", "1"]),
1619            ["RW", "access", "update"]
1620        );
1621        assert_eq!(flags(&["delex", "k"]), ["RM", "delete"]);
1622        assert_eq!(flags(&["delex", "k", "IFDEQ", "d"]), ["RW", "delete"]);
1623    }
1624
1625    #[test]
1626    fn a_container_is_asked_about_the_word_behind_it() {
1627        assert_eq!(named(&["object", "encoding", "k"]).unwrap(), ["k"]);
1628        assert_eq!(named(&["xgroup", "create", "s", "g", "$"]).unwrap(), ["s"]);
1629        assert_eq!(named(&["object", "help"]).unwrap(), Vec::<String>::new());
1630    }
1631}