use super::args::Args;
use super::table::Spec;
use yo_common::parse_i64;
#[derive(Debug, Clone, Copy)]
pub struct KeySpec {
pub notes: &'static str,
pub flags: &'static [&'static str],
pub begin: Begin,
pub find: Find,
}
#[derive(Debug, Clone, Copy)]
pub enum Begin {
At(u32),
After(&'static [u8], i32),
Unknown,
}
#[derive(Debug, Clone, Copy)]
pub enum Find {
Range {
last: i32,
step: u32,
limit: u32,
},
Counted {
count: u32,
first: u32,
step: u32,
},
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Access(u8);
impl Access {
pub const READ: Access = Access(1);
pub const WRITE: Access = Access(2);
pub const BOTH: Access = Access(3);
pub const NONE: Access = Access(0);
#[must_use]
pub const fn covers(self, other: Access) -> bool {
self.0 & other.0 == other.0
}
#[must_use]
pub const fn and(self, other: Access) -> Access {
Access(self.0 | other.0)
}
#[must_use]
pub const fn is_none(self) -> bool {
self.0 == 0
}
#[must_use]
pub const fn bits(self) -> u8 {
self.0
}
#[must_use]
pub const fn from_bits(bits: u8) -> Access {
Access(bits & 3)
}
}
impl KeySpec {
#[must_use]
pub fn access(&self) -> Access {
access_of(self.flags)
}
#[must_use]
pub fn incomplete(&self) -> bool {
self.flags.contains(&"incomplete")
|| matches!(self.begin, Begin::Unknown)
|| matches!(self.find, Find::Unknown)
}
#[must_use]
pub fn fake(&self) -> bool {
self.flags.contains(&"not_key")
}
fn step(&self) -> u32 {
match self.find {
Find::Range { step, .. } | Find::Counted { step, .. } => step.max(1),
Find::Unknown => 1,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct Run {
pub first: usize,
pub count: usize,
pub step: usize,
pub flags: &'static [&'static str],
}
impl Run {
#[must_use]
pub fn need(&self) -> Access {
access_of(self.flags)
}
}
#[must_use]
pub fn access_of(flags: &[&str]) -> Access {
if flags.contains(&"not_key") {
return Access::NONE;
}
let mut need = Access::NONE;
if flags.contains(&"access") {
need = need.and(Access::READ);
}
if flags.contains(&"update") || flags.contains(&"insert") || flags.contains(&"delete") {
need = need.and(Access::WRITE);
}
need
}
pub fn find(spec: &Spec, args: Args<'_>, base: usize, each: &mut dyn FnMut(Run)) -> bool {
let keys = keys_of(spec, args, base);
let real = keys.iter().any(|k| !k.fake());
let varying = keys.iter().any(|k| k.flags.contains(&"variable_flags"));
if real && !varying && walk(keys, args, base, each) {
return true;
}
if let Some(finder) = finder_for(spec.name) {
return finder(args, base, each);
}
!real
}
pub fn takes_keys(spec: &Spec, args: Args<'_>, base: usize) -> bool {
finder_for(spec.name).is_some() || keys_of(spec, args, base).iter().any(|k| !k.fake())
}
fn keys_of(spec: &Spec, args: Args<'_>, base: usize) -> &'static [KeySpec] {
if !spec.keys.is_empty() {
return spec.keys;
}
if args.len() <= base + 1 {
return &[];
}
of_sub(spec.name, args.get(base + 1))
}
fn walk(keys: &[KeySpec], args: Args<'_>, base: usize, each: &mut dyn FnMut(Run)) -> bool {
let mut runs = [None::<Run>; MOST_SPECS];
let mut at = 0;
for key in keys {
if key.fake() {
continue;
}
match resolve(key, args, base) {
Resolved::Run(run) => {
if at == runs.len() {
return false;
}
runs[at] = Some(run);
at += 1;
if key.incomplete() {
return false;
}
}
Resolved::None => {}
Resolved::Invalid => return false,
}
}
for run in runs.iter().flatten() {
if run.count > 0 {
each(*run);
}
}
true
}
const MOST_SPECS: usize = 4;
enum Resolved {
Run(Run),
None,
Invalid,
}
fn resolve(key: &KeySpec, args: Args<'_>, base: usize) -> Resolved {
let argc = (args.len() - base) as i64;
let at = |i: i64| args.get(base + i as usize);
let mut first = match key.begin {
Begin::At(index) => i64::from(index),
Begin::After(word, from) => {
let start = if from > 0 {
i64::from(from)
} else {
argc + i64::from(from)
};
let end = if from > 0 { argc - 1 } else { 1 };
let mut found = 0;
let mut i = start;
while i != end {
if i >= argc || i < 1 {
break;
}
if at(i).eq_ignore_ascii_case(word) {
found = i + 1;
break;
}
i += if start <= end { 1 } else { -1 };
}
if found == 0 {
return Resolved::None;
}
found
}
Begin::Unknown => return Resolved::Invalid,
};
let step = i64::from(key.step());
let last = match key.find {
Find::Range { last, limit, .. } => {
if last >= 0 {
first + i64::from(last)
} else if limit == 0 {
argc + i64::from(last)
} else {
first + ((argc - first) / i64::from(limit) + i64::from(last))
}
}
Find::Counted {
count, first: from, ..
} => {
let index = first + i64::from(count);
if index >= argc || index < 0 {
return Resolved::Invalid;
}
let Some(n) = parse_i64(at(index)).filter(|&n| n >= 0) else {
return Resolved::Invalid;
};
first += i64::from(from);
match n
.checked_sub(1)
.and_then(|n| n.checked_mul(step))
.and_then(|n| first.checked_add(n))
{
Some(last) => last,
None => return Resolved::Invalid,
}
}
Find::Unknown => return Resolved::Invalid,
};
if last >= argc || last < first || first >= argc {
return Resolved::Invalid;
}
Resolved::Run(Run {
first: base + first as usize,
count: ((last - first) / step + 1) as usize,
step: step as usize,
flags: key.flags,
})
}
const READ: &[&str] = &["RO", "access"];
const OVERWRITE: &[&str] = &["OW", "update"];
const BOTH: &[&str] = &["RW", "access", "update"];
const MOVED: &[&str] = &["RW", "access", "delete"];
const MERGED: &[&str] = &["RW", "access", "insert"];
const COMPARED: &[&str] = &["RW", "delete"];
const REMOVED: &[&str] = &["RM", "delete"];
type Finder = fn(Args<'_>, usize, &mut dyn FnMut(Run)) -> bool;
fn finder_for(name: &str) -> Option<Finder> {
Some(match name {
"sort" => sort_keys,
"sort_ro" => sort_ro_keys,
"migrate" => migrate_keys,
"xread" | "xreadgroup" => xread_keys,
"georadius" | "georadiusbymember" => georadius_keys,
"set" => set_keys,
"bitfield" => bitfield_keys,
"delex" => delex_keys,
"pfmerge" => pfmerge_keys,
_ => return None,
})
}
fn one(at: usize, flags: &'static [&'static str], each: &mut dyn FnMut(Run)) {
each(Run {
first: at,
count: 1,
step: 1,
flags,
});
}
fn sort_keys(args: Args<'_>, base: usize, each: &mut dyn FnMut(Run)) -> bool {
one(base + 1, READ, each);
let argc = args.len() - base;
let mut store = None;
let mut i = 2;
while i < argc {
let arg = args.get(base + i);
if arg.eq_ignore_ascii_case(b"limit") {
i += 2;
} else if arg.eq_ignore_ascii_case(b"get") || arg.eq_ignore_ascii_case(b"by") {
i += 1;
} else if arg.eq_ignore_ascii_case(b"store") && i + 1 < argc {
store = Some(base + i + 1);
}
i += 1;
}
if let Some(at) = store {
one(at, OVERWRITE, each);
}
true
}
fn sort_ro_keys(_args: Args<'_>, base: usize, each: &mut dyn FnMut(Run)) -> bool {
one(base + 1, READ, each);
true
}
fn migrate_keys(args: Args<'_>, base: usize, each: &mut dyn FnMut(Run)) -> bool {
let argc = args.len() - base;
let mut first = 3;
let mut count = 1;
if argc > 6 {
let mut i = 6;
while i < argc {
let arg = args.get(base + i);
if arg.eq_ignore_ascii_case(b"keys") {
if args.get(base + 3).is_empty() {
first = i + 1;
count = argc - first;
} else {
count = 0;
}
break;
}
if arg.eq_ignore_ascii_case(b"auth") {
i += 1;
} else if arg.eq_ignore_ascii_case(b"auth2") {
i += 2;
}
i += 1;
}
}
if count > 0 {
each(Run {
first: base + first,
count,
step: 1,
flags: MOVED,
});
}
true
}
fn xread_keys(args: Args<'_>, base: usize, each: &mut dyn FnMut(Run)) -> bool {
let argc = args.len() - base;
let mut streams = None;
let mut i = 1;
while i < argc {
let arg = args.get(base + i);
if arg.eq_ignore_ascii_case(b"block") || arg.eq_ignore_ascii_case(b"count") {
i += 1;
} else if arg.eq_ignore_ascii_case(b"group") {
i += 2;
} else if arg.eq_ignore_ascii_case(b"noack") {
} else if arg.eq_ignore_ascii_case(b"streams") {
streams = Some(i);
break;
} else {
break;
}
i += 1;
}
let Some(streams) = streams else {
return false;
};
let tail = argc - streams - 1;
if tail == 0 || !tail.is_multiple_of(2) {
return false;
}
each(Run {
first: base + streams + 1,
count: tail / 2,
step: 1,
flags: READ,
});
true
}
fn georadius_keys(args: Args<'_>, base: usize, each: &mut dyn FnMut(Run)) -> bool {
one(base + 1, READ, each);
let argc = args.len() - base;
let mut store = None;
let mut i = 5;
while i < argc {
let arg = args.get(base + i);
if (arg.eq_ignore_ascii_case(b"store") || arg.eq_ignore_ascii_case(b"storedist"))
&& i + 1 < argc
{
store = Some(base + i + 1);
i += 1;
}
i += 1;
}
if let Some(at) = store {
one(at, OVERWRITE, each);
}
true
}
fn set_keys(args: Args<'_>, base: usize, each: &mut dyn FnMut(Run)) -> bool {
let gets = (base + 3..args.len()).any(|i| args.get(i).eq_ignore_ascii_case(b"get"));
one(base + 1, if gets { BOTH } else { OVERWRITE }, each);
true
}
fn bitfield_keys(args: Args<'_>, base: usize, each: &mut dyn FnMut(Run)) -> bool {
let argc = args.len() - base;
let mut reads = true;
let mut i = 2;
while i < argc {
let left = argc - i - 1;
let arg = args.get(base + i);
if arg.eq_ignore_ascii_case(b"get") && left >= 2 {
i += 2;
} else if (arg.eq_ignore_ascii_case(b"set") || arg.eq_ignore_ascii_case(b"incrby"))
&& left >= 3
{
reads = false;
break;
} else if arg.eq_ignore_ascii_case(b"overflow") && left >= 1 {
i += 1;
} else {
reads = false;
break;
}
i += 1;
}
one(base + 1, if reads { READ } else { BOTH }, each);
true
}
fn delex_keys(args: Args<'_>, base: usize, each: &mut dyn FnMut(Run)) -> bool {
let compares = args.opt(base + 2).is_some_and(|a| {
a.eq_ignore_ascii_case(b"ifeq")
|| a.eq_ignore_ascii_case(b"ifne")
|| a.eq_ignore_ascii_case(b"ifdeq")
|| a.eq_ignore_ascii_case(b"ifdne")
});
one(base + 1, if compares { COMPARED } else { REMOVED }, each);
true
}
fn pfmerge_keys(args: Args<'_>, base: usize, each: &mut dyn FnMut(Run)) -> bool {
one(base + 1, MERGED, each);
let argc = args.len() - base;
if argc > 2 {
each(Run {
first: base + 2,
count: argc - 2,
step: 1,
flags: READ,
});
}
true
}
pub const NOT_KEY_AT1: KeySpec = KeySpec {
notes: "",
flags: &["not_key"],
begin: Begin::At(1),
find: Find::Range {
last: 0,
step: 1,
limit: 0,
},
};
pub const NOT_KEY_AT1_RM1_1_0: KeySpec = KeySpec {
notes: "",
flags: &["not_key"],
begin: Begin::At(1),
find: Find::Range {
last: -1,
step: 1,
limit: 0,
},
};
pub const OW_INSERT_AT1: KeySpec = KeySpec {
notes: "",
flags: &["OW", "insert"],
begin: Begin::At(1),
find: Find::Range {
last: 0,
step: 1,
limit: 0,
},
};
pub const OW_INSERT_AT1_RM1_2_0: KeySpec = KeySpec {
notes: "",
flags: &["OW", "insert"],
begin: Begin::At(1),
find: Find::Range {
last: -1,
step: 2,
limit: 0,
},
};
pub const OW_INSERT_AT2: KeySpec = KeySpec {
notes: "",
flags: &["OW", "insert"],
begin: Begin::At(2),
find: Find::Range {
last: 0,
step: 1,
limit: 0,
},
};
pub const OW_UPDATE_AT1: KeySpec = KeySpec {
notes: "",
flags: &["OW", "update"],
begin: Begin::At(1),
find: Find::Range {
last: 0,
step: 1,
limit: 0,
},
};
pub const OW_UPDATE_AT1_COUNTED: KeySpec = KeySpec {
notes: "",
flags: &["OW", "update"],
begin: Begin::At(1),
find: Find::Counted {
count: 0,
first: 1,
step: 2,
},
};
pub const OW_UPDATE_AT1_RM1_2_0: KeySpec = KeySpec {
notes: "",
flags: &["OW", "update"],
begin: Begin::At(1),
find: Find::Range {
last: -1,
step: 2,
limit: 0,
},
};
pub const OW_UPDATE_AT2: KeySpec = KeySpec {
notes: "",
flags: &["OW", "update"],
begin: Begin::At(2),
find: Find::Range {
last: 0,
step: 1,
limit: 0,
},
};
pub const GEORADIUS_STORE: KeySpec = KeySpec {
notes: "Incomplete because duplicate STORE options use last-wins; fall back to georadiusGetKeys",
flags: &["OW", "update", "incomplete"],
begin: Begin::After(b"STORE", 6),
find: Find::Range {
last: 0,
step: 1,
limit: 0,
},
};
pub const GEORADIUS_STOREDIST: KeySpec = KeySpec {
notes: "Incomplete because duplicate STOREDIST options use last-wins; fall back to georadiusGetKeys",
flags: &["OW", "update", "incomplete"],
begin: Begin::After(b"STOREDIST", 6),
find: Find::Range {
last: 0,
step: 1,
limit: 0,
},
};
pub const BYMEMBER_STOREDIST: KeySpec = KeySpec {
notes: "Incomplete because duplicate STOREDIST options use last-wins; fall back to georadiusGetKeys",
flags: &["OW", "update", "incomplete"],
begin: Begin::After(b"STOREDIST", 5),
find: Find::Range {
last: 0,
step: 1,
limit: 0,
},
};
pub const BYMEMBER_STORE: KeySpec = KeySpec {
notes: "Incomplete because duplicate STORE options use last-wins; fall back to georadiusGetKeys",
flags: &["OW", "update", "incomplete"],
begin: Begin::After(b"STORE", 5),
find: Find::Range {
last: 0,
step: 1,
limit: 0,
},
};
pub const SORT_STORE: KeySpec = KeySpec {
notes: "For the optional STORE keyword. It is marked 'unknown' because the keyword can appear anywhere in the argument array",
flags: &["OW", "update"],
begin: Begin::Unknown,
find: Find::Unknown,
};
pub const RM_DELETE_AT1_RM1_1_0: KeySpec = KeySpec {
notes: "",
flags: &["RM", "delete"],
begin: Begin::At(1),
find: Find::Range {
last: -1,
step: 1,
limit: 0,
},
};
pub const RO_ACCESS_AT1: KeySpec = KeySpec {
notes: "",
flags: &["RO", "access"],
begin: Begin::At(1),
find: Find::Range {
last: 0,
step: 1,
limit: 0,
},
};
pub const RO_ACCESS_AT1_COUNTED: KeySpec = KeySpec {
notes: "",
flags: &["RO", "access"],
begin: Begin::At(1),
find: Find::Counted {
count: 0,
first: 1,
step: 1,
},
};
pub const RO_ACCESS_AT1_R1_1_0: KeySpec = KeySpec {
notes: "",
flags: &["RO", "access"],
begin: Begin::At(1),
find: Find::Range {
last: 1,
step: 1,
limit: 0,
},
};
pub const RO_ACCESS_AT1_RM1_1_0: KeySpec = KeySpec {
notes: "",
flags: &["RO", "access"],
begin: Begin::At(1),
find: Find::Range {
last: -1,
step: 1,
limit: 0,
},
};
pub const RO_ACCESS_AT1_RM2_1_0: KeySpec = KeySpec {
notes: "",
flags: &["RO", "access"],
begin: Begin::At(1),
find: Find::Range {
last: -2,
step: 1,
limit: 0,
},
};
pub const RO_ACCESS_AT2: KeySpec = KeySpec {
notes: "",
flags: &["RO", "access"],
begin: Begin::At(2),
find: Find::Range {
last: 0,
step: 1,
limit: 0,
},
};
pub const SCRIPT_KEYS_RO: KeySpec = KeySpec {
notes: "We cannot tell how the keys will be used so we assume the worst, RO and ACCESS",
flags: &["RO", "access"],
begin: Begin::At(2),
find: Find::Counted {
count: 0,
first: 1,
step: 1,
},
};
pub const RO_ACCESS_AT2_COUNTED: KeySpec = KeySpec {
notes: "",
flags: &["RO", "access"],
begin: Begin::At(2),
find: Find::Counted {
count: 0,
first: 1,
step: 1,
},
};
pub const RO_ACCESS_AT2_RM1_1_0: KeySpec = KeySpec {
notes: "",
flags: &["RO", "access"],
begin: Begin::At(2),
find: Find::Range {
last: -1,
step: 1,
limit: 0,
},
};
pub const RO_ACCESS_AT3_RM1_1_0: KeySpec = KeySpec {
notes: "",
flags: &["RO", "access"],
begin: Begin::At(3),
find: Find::Range {
last: -1,
step: 1,
limit: 0,
},
};
pub const XREAD_STREAMS: KeySpec = KeySpec {
notes: "Incomplete because a stream key named STREAMS (or options before it) can shift the STREAMS keyword; fall back to xreadGetKeys",
flags: &["RO", "access", "incomplete"],
begin: Begin::After(b"STREAMS", 1),
find: Find::Range {
last: -1,
step: 1,
limit: 2,
},
};
pub const XREADGROUP_STREAMS: KeySpec = KeySpec {
notes: "Incomplete because a consumer/group named STREAMS (or options before GROUP) can shift the STREAMS keyword; fall back to xreadGetKeys",
flags: &["RO", "access", "incomplete"],
begin: Begin::After(b"STREAMS", 4),
find: Find::Range {
last: -1,
step: 1,
limit: 2,
},
};
pub const SORT_BY_AND_GET: KeySpec = KeySpec {
notes: "For the optional BY/GET keyword. It is marked 'unknown' because the key names derive from the content of the key we sort",
flags: &["RO", "access"],
begin: Begin::Unknown,
find: Find::Unknown,
};
pub const RO_AT1: KeySpec = KeySpec {
notes: "",
flags: &["RO"],
begin: Begin::At(1),
find: Find::Range {
last: 0,
step: 1,
limit: 0,
},
};
pub const RO_AT1_RM1_1_0: KeySpec = KeySpec {
notes: "",
flags: &["RO"],
begin: Begin::At(1),
find: Find::Range {
last: -1,
step: 1,
limit: 0,
},
};
pub const RO_AT2: KeySpec = KeySpec {
notes: "",
flags: &["RO"],
begin: Begin::At(2),
find: Find::Range {
last: 0,
step: 1,
limit: 0,
},
};
pub const RW_ACCESS_AT1_RM1_1_0: KeySpec = KeySpec {
notes: "RW because it may change the internal representation of the key, and propagate to replicas",
flags: &["RW", "access"],
begin: Begin::At(1),
find: Find::Range {
last: -1,
step: 1,
limit: 0,
},
};
pub const RW_ACCESS_AT2: KeySpec = KeySpec {
notes: "",
flags: &["RW", "access"],
begin: Begin::At(2),
find: Find::Range {
last: 0,
step: 1,
limit: 0,
},
};
pub const RW_ACCESS_DELETE_AT1: KeySpec = KeySpec {
notes: "",
flags: &["RW", "access", "delete"],
begin: Begin::At(1),
find: Find::Range {
last: 0,
step: 1,
limit: 0,
},
};
pub const RW_ACCESS_DELETE_AT1_COUNTED: KeySpec = KeySpec {
notes: "",
flags: &["RW", "access", "delete"],
begin: Begin::At(1),
find: Find::Counted {
count: 0,
first: 1,
step: 1,
},
};
pub const RW_ACCESS_DELETE_AT1_RM2_1_0: KeySpec = KeySpec {
notes: "",
flags: &["RW", "access", "delete"],
begin: Begin::At(1),
find: Find::Range {
last: -2,
step: 1,
limit: 0,
},
};
pub const RW_ACCESS_DELETE_AT2_COUNTED: KeySpec = KeySpec {
notes: "",
flags: &["RW", "access", "delete"],
begin: Begin::At(2),
find: Find::Counted {
count: 0,
first: 1,
step: 1,
},
};
pub const RW_ACCESS_DELETE_AT3: KeySpec = KeySpec {
notes: "",
flags: &["RW", "access", "delete"],
begin: Begin::At(3),
find: Find::Range {
last: 0,
step: 1,
limit: 0,
},
};
pub const MIGRATE_KEYS: KeySpec = KeySpec {
notes: "",
flags: &["RW", "access", "delete", "incomplete"],
begin: Begin::After(b"KEYS", -2),
find: Find::Range {
last: -1,
step: 1,
limit: 0,
},
};
pub const RW_ACCESS_INSERT_AT1: KeySpec = KeySpec {
notes: "",
flags: &["RW", "access", "insert"],
begin: Begin::At(1),
find: Find::Range {
last: 0,
step: 1,
limit: 0,
},
};
pub const RW_ACCESS_UPDATE_AT1: KeySpec = KeySpec {
notes: "",
flags: &["RW", "access", "update"],
begin: Begin::At(1),
find: Find::Range {
last: 0,
step: 1,
limit: 0,
},
};
pub const RW_ACCESS_UPDATE_AT1_TTL: KeySpec = KeySpec {
notes: "RW and UPDATE because it changes the TTL",
flags: &["RW", "access", "update"],
begin: Begin::At(1),
find: Find::Range {
last: 0,
step: 1,
limit: 0,
},
};
pub const RW_ACCESS_UPDATE_AT1_R1_1_0: KeySpec = KeySpec {
notes: "",
flags: &["RW", "access", "update"],
begin: Begin::At(1),
find: Find::Range {
last: 1,
step: 1,
limit: 0,
},
};
pub const RW_ACCESS_UPDATE_AT1_RM1_3_0: KeySpec = KeySpec {
notes: "",
flags: &["RW", "access", "update"],
begin: Begin::At(1),
find: Find::Range {
last: -1,
step: 3,
limit: 0,
},
};
pub const RW_ACCESS_UPDATE_AT2: KeySpec = KeySpec {
notes: "",
flags: &["RW", "access", "update"],
begin: Begin::At(2),
find: Find::Range {
last: 0,
step: 1,
limit: 0,
},
};
pub const SCRIPT_KEYS_RW: KeySpec = KeySpec {
notes: "We cannot tell how the keys will be used so we assume the worst, RW and UPDATE",
flags: &["RW", "access", "update"],
begin: Begin::At(2),
find: Find::Counted {
count: 0,
first: 1,
step: 1,
},
};
pub const RW_ACCESS_UPDATE_AT2_COUNTED: KeySpec = KeySpec {
notes: "",
flags: &["RW", "access", "update"],
begin: Begin::At(2),
find: Find::Counted {
count: 0,
first: 1,
step: 1,
},
};
pub const BITFIELD_KEY: KeySpec = KeySpec {
notes: "This command allows both access and modification of the key",
flags: &["RW", "access", "update", "variable_flags"],
begin: Begin::At(1),
find: Find::Range {
last: 0,
step: 1,
limit: 0,
},
};
pub const SET_KEY: KeySpec = KeySpec {
notes: "RW and ACCESS due to the optional `GET` argument",
flags: &["RW", "access", "update", "variable_flags"],
begin: Begin::At(1),
find: Find::Range {
last: 0,
step: 1,
limit: 0,
},
};
pub const RW_DELETE_AT1: KeySpec = KeySpec {
notes: "",
flags: &["RW", "delete"],
begin: Begin::At(1),
find: Find::Range {
last: 0,
step: 1,
limit: 0,
},
};
pub const RW_DELETE_AT2: KeySpec = KeySpec {
notes: "",
flags: &["RW", "delete"],
begin: Begin::At(2),
find: Find::Range {
last: 0,
step: 1,
limit: 0,
},
};
pub const DELEX_KEY: KeySpec = KeySpec {
notes: "",
flags: &["RW", "delete", "variable_flags"],
begin: Begin::At(1),
find: Find::Range {
last: 0,
step: 1,
limit: 0,
},
};
pub const RW_INSERT_AT1: KeySpec = KeySpec {
notes: "",
flags: &["RW", "insert"],
begin: Begin::At(1),
find: Find::Range {
last: 0,
step: 1,
limit: 0,
},
};
pub const RW_INSERT_AT2: KeySpec = KeySpec {
notes: "",
flags: &["RW", "insert"],
begin: Begin::At(2),
find: Find::Range {
last: 0,
step: 1,
limit: 0,
},
};
pub const RW_UPDATE_AT1: KeySpec = KeySpec {
notes: "",
flags: &["RW", "update"],
begin: Begin::At(1),
find: Find::Range {
last: 0,
step: 1,
limit: 0,
},
};
pub const RW_UPDATE_AT1_TRIMMING: KeySpec = KeySpec {
notes: "UPDATE instead of INSERT because of the optional trimming feature",
flags: &["RW", "update"],
begin: Begin::At(1),
find: Find::Range {
last: 0,
step: 1,
limit: 0,
},
};
pub const RW_UPDATE_AT2: KeySpec = KeySpec {
notes: "",
flags: &["RW", "update"],
begin: Begin::At(2),
find: Find::Range {
last: 0,
step: 1,
limit: 0,
},
};
pub const RW_UPDATE_DELETE_AT1: KeySpec = KeySpec {
notes: "",
flags: &["RW", "update", "delete"],
begin: Begin::At(1),
find: Find::Range {
last: 0,
step: 1,
limit: 0,
},
};
type SubSpec = (&'static [u8], &'static [u8], &'static [KeySpec]);
static SUBS: &[SubSpec] = &[
(b"himport", b"set", &[OW_UPDATE_AT2]),
(b"json.debug", b"memory", &[RO_ACCESS_AT2]),
(b"memory", b"usage", &[RO_AT2]),
(b"object", b"encoding", &[RO_AT2]),
(b"object", b"freq", &[RO_AT2]),
(b"object", b"idletime", &[RO_AT2]),
(b"object", b"refcount", &[RO_AT2]),
(b"xgroup", b"create", &[RW_INSERT_AT2]),
(b"xgroup", b"createconsumer", &[RW_INSERT_AT2]),
(b"xgroup", b"delconsumer", &[RW_DELETE_AT2]),
(b"xgroup", b"destroy", &[RW_DELETE_AT2]),
(b"xgroup", b"setid", &[RW_UPDATE_AT2]),
(b"xinfo", b"consumers", &[RO_ACCESS_AT2]),
(b"xinfo", b"groups", &[RO_ACCESS_AT2]),
(b"xinfo", b"stream", &[RO_ACCESS_AT2]),
];
#[must_use]
pub fn of_sub(container: &str, sub: &[u8]) -> &'static [KeySpec] {
SUBS.iter()
.find(|(c, s, _)| *c == container.as_bytes() && s.eq_ignore_ascii_case(sub))
.map_or(&[][..], |(_, _, keys)| keys)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dispatch::table;
use crate::proto::Limits;
use crate::request::{Argv, Step};
fn flagged(words: &[&str]) -> Option<Vec<(String, Vec<&'static str>)>> {
let mut buf = format!("*{}\r\n", words.len()).into_bytes();
for word in words {
buf.extend_from_slice(format!("${}\r\n{word}\r\n", word.len()).as_bytes());
}
let mut argv = Argv::new();
let Ok(Step::Command { .. }) = argv.decode(&buf, &Limits::default()) else {
panic!("the test wrote a command that does not decode");
};
let args = Args::new(&argv, &buf);
let spec = table::lookup(args.name()).expect("a command this server has");
let mut found = Vec::new();
let whole = find(spec, args, 0, &mut |run| {
for i in 0..run.count {
let key = args.get(run.first + i * run.step);
found.push((
String::from_utf8_lossy(key).into_owned(),
run.flags.to_vec(),
));
}
});
whole.then_some(found)
}
fn named(words: &[&str]) -> Option<Vec<String>> {
flagged(words).map(|keys| keys.into_iter().map(|(key, _)| key).collect())
}
#[test]
fn a_counted_run_names_every_key_it_counts() {
assert_eq!(
named(&["zunionstore", "d", "2", "a", "b"]).unwrap(),
["d", "a", "b"]
);
assert_eq!(
named(&["lmpop", "2", "a", "b", "LEFT"]).unwrap(),
["a", "b"]
);
assert_eq!(named(&["smove", "a", "b", "m"]).unwrap(), ["a", "b"]);
}
#[test]
fn a_run_that_reaches_past_the_last_argument_names_nothing() {
assert_eq!(named(&["zunionstore", "d", "3", "a"]), None);
assert_eq!(named(&["zunionstore", "d", "-1", "a"]), None);
assert_eq!(named(&["zunionstore", "d", "x", "a"]), None);
assert_eq!(named(&["eval", "body", "0"]), None);
}
#[test]
fn the_streams_keyword_is_walked_to_rather_than_counted_from() {
let two = ["xread", "COUNT", "2", "STREAMS", "a", "b", "0", "0"];
assert_eq!(named(&two).unwrap(), ["a", "b"]);
assert_eq!(named(&["xread", "STREAMS", "a", "b", "0"]), None);
let group = ["xreadgroup", "GROUP", "STREAMS", "c", "STREAMS", "a", ">"];
assert_eq!(named(&group).unwrap(), ["a"]);
}
#[test]
fn pfmerge_names_its_destination_when_it_was_given_no_sources() {
assert_eq!(named(&["pfmerge", "k0"]).unwrap(), ["k0"]);
assert_eq!(named(&["pfmerge", "d", "a", "b"]).unwrap(), ["d", "a", "b"]);
}
#[test]
fn an_argument_that_only_looks_like_a_key_is_not_one() {
let spec = table::lookup(b"spublish").expect("a command this server has");
let mut argv = Argv::new();
let buf = b"*3\r\n$8\r\nspublish\r\n$2\r\nch\r\n$1\r\nm\r\n";
let Ok(Step::Command { .. }) = argv.decode(buf, &Limits::default()) else {
panic!("the test wrote a command that does not decode");
};
assert!(!takes_keys(spec, Args::new(&argv, buf), 0));
}
#[test]
fn a_last_wins_destination_is_the_last_one_written() {
let two = ["sort", "k", "STORE", "a", "STORE", "b"];
assert_eq!(named(&two).unwrap(), ["k", "b"]);
let geo = [
"georadius",
"k",
"0",
"0",
"1",
"m",
"STORE",
"d",
"STOREDIST",
"e",
];
assert_eq!(named(&geo).unwrap(), ["k", "e"]);
assert_eq!(
named(&["sort", "k", "BY", "STORE", "GET", "d"]).unwrap(),
["k"]
);
}
#[test]
fn a_keyword_the_command_does_not_carry_is_not_a_failure() {
let single = ["migrate", "h", "1", "k", "0", "0"];
assert_eq!(named(&single).unwrap(), ["k"]);
let listed = ["migrate", "h", "1", "", "0", "0", "KEYS", "a", "b"];
assert_eq!(named(&listed).unwrap(), ["a", "b"]);
assert_eq!(
named(&["migrate", "h", "1", "", "0", "0", "KEYS"]).unwrap(),
[""]
);
}
#[test]
fn what_a_command_does_to_a_key_can_depend_on_its_options() {
let flags = |words: &[&str]| flagged(words).unwrap()[0].1.clone();
assert_eq!(flags(&["set", "k", "v"]), ["OW", "update"]);
assert_eq!(flags(&["set", "k", "v", "GET"]), ["RW", "access", "update"]);
assert_eq!(
flags(&["bitfield", "k", "GET", "u8", "0"]),
["RO", "access"]
);
assert_eq!(
flags(&["bitfield", "k", "SET", "u8", "0", "1"]),
["RW", "access", "update"]
);
assert_eq!(flags(&["delex", "k"]), ["RM", "delete"]);
assert_eq!(flags(&["delex", "k", "IFDEQ", "d"]), ["RW", "delete"]);
}
#[test]
fn a_container_is_asked_about_the_word_behind_it() {
assert_eq!(named(&["object", "encoding", "k"]).unwrap(), ["k"]);
assert_eq!(named(&["xgroup", "create", "s", "g", "$"]).unwrap(), ["s"]);
assert_eq!(named(&["object", "help"]).unwrap(), Vec::<String>::new());
}
}