use std::sync::OnceLock;
use gxhash::{GxBuildHasher, HashMap, HashSet};
use serde::Serialize;
use sonic_rs::Deserialize;
use super::{
resp_command_data_common::try_import_resp_commands_data,
resp_command_data_provider::IRespCommandData,
resp_command_info_simplified_structs::{SimpleRespCommandInfo, populate_simple_command_info},
resp_command_key_specification::{
BeginSearchMethod, FindKeysMethod, KeySpecificationFlags, RespCommandKeySpecification,
},
resp_commands_info_data::{
FIRST_DATA_COMMAND, LAST_DATA_COMMAND, LAST_VALID_COMMAND, resp_command_from_cs_name,
},
resp_memory_writer::RespMemoryWriter,
};
use crate::{acl::RespAclCategories, types::RespCommand};
const RESP_COMMANDS_INFO_JSON: &str = include_str!("RespCommandsInfo.json");
const UNKNOWN_COMMAND_NAME: &str = "UNKNOWN";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RespCommandFlags(pub u32);
impl RespCommandFlags {
pub const ADMIN: Self = Self(1);
pub const ASKING: Self = Self(1 << 1);
pub const BLOCKING: Self = Self(1 << 2);
pub const DENY_OOM: Self = Self(1 << 3);
pub const FAST: Self = Self(1 << 4);
pub const LOADING: Self = Self(1 << 5);
pub const MOVABLE_KEYS: Self = Self(1 << 6);
pub const NO_AUTH: Self = Self(1 << 7);
pub const NO_ASYNC_LOADING: Self = Self(1 << 8);
pub const NO_MANDATORY_KEYS: Self = Self(1 << 9);
pub const NO_MULTI: Self = Self(1 << 10);
pub const NO_SCRIPT: Self = Self(1 << 11);
pub const PUB_SUB: Self = Self(1 << 12);
pub const RANDOM: Self = Self(1 << 13);
pub const READ_ONLY: Self = Self(1 << 14);
pub const SORT_FOR_SCRIPT: Self = Self(1 << 15);
pub const SKIP_MONITOR: Self = Self(1 << 16);
pub const SKIP_SLOW_LOG: Self = Self(1 << 17);
pub const STALE: Self = Self(1 << 18);
pub const WRITE: Self = Self(1 << 19);
pub const ALLOW_BUSY: Self = Self(1 << 20);
pub const MODULE: Self = Self(1 << 21);
#[inline]
pub const fn empty() -> Self {
Self(0)
}
#[inline]
pub fn is_empty(&self) -> bool {
self.0 == 0
}
#[inline]
pub fn intersects(&self, other: Self) -> bool {
self.0 & other.0 != 0
}
const TABLE: [(u32, &'static str, &'static str); 22] = [
(Self::ADMIN.0, "Admin", "admin"),
(Self::ASKING.0, "Asking", "asking"),
(Self::BLOCKING.0, "Blocking", "blocking"),
(Self::DENY_OOM.0, "DenyOom", "denyoom"),
(Self::FAST.0, "Fast", "fast"),
(Self::LOADING.0, "Loading", "loading"),
(Self::MOVABLE_KEYS.0, "MovableKeys", "movablekeys"),
(Self::NO_AUTH.0, "NoAuth", "no_auth"),
(
Self::NO_ASYNC_LOADING.0,
"NoAsyncLoading",
"no_async_loading",
),
(
Self::NO_MANDATORY_KEYS.0,
"NoMandatoryKeys",
"no_mandatory_keys",
),
(Self::NO_MULTI.0, "NoMulti", "no_multi"),
(Self::NO_SCRIPT.0, "NoScript", "noscript"),
(Self::PUB_SUB.0, "PubSub", "pubsub"),
(Self::RANDOM.0, "Random", "random"),
(Self::READ_ONLY.0, "ReadOnly", "readonly"),
(Self::SORT_FOR_SCRIPT.0, "SortForScript", "sort_for_script"),
(Self::SKIP_MONITOR.0, "SkipMonitor", "skip_monitor"),
(Self::SKIP_SLOW_LOG.0, "SkipSlowLog", "skip_slowlog"),
(Self::STALE.0, "Stale", "stale"),
(Self::WRITE.0, "Write", "write"),
(Self::ALLOW_BUSY.0, "AllowBusy", "allow_busy"),
(Self::MODULE.0, "Module", "module"),
];
pub fn descriptions(&self) -> Vec<&'static str> {
Self::TABLE
.iter()
.filter(|(bit, ..)| self.0 & bit != 0)
.map(|(_, _, desc)| *desc)
.collect()
}
pub fn member_names(&self) -> Vec<&'static str> {
Self::TABLE
.iter()
.filter(|(bit, ..)| self.0 & bit != 0)
.map(|(_, name, _)| *name)
.collect()
}
pub fn from_member_names(names: &str) -> Option<Self> {
let mut out = Self::empty();
for name in names.split(',') {
let trimmed = name.trim().to_ascii_uppercase();
let bit = Self::TABLE
.iter()
.find(|(_, member, _)| member.to_ascii_uppercase() == trimmed)
.map(|(bit, ..)| *bit)?;
out.0 |= bit;
}
Some(out)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum StoreType {
#[default]
None,
Main,
Object,
All,
}
impl StoreType {
pub fn from_member_name(name: &str) -> Option<Self> {
Some(match name.to_ascii_uppercase().as_str() {
"NONE" => Self::None,
"MAIN" => Self::Main,
"OBJECT" => Self::Object,
"ALL" => Self::All,
_ => return None,
})
}
}
pub(crate) fn acl_category_descriptions(cats: RespAclCategories) -> Vec<&'static str> {
const ALL: [(u32, &str); 24] = [
(RespAclCategories::ADMIN.bits(), "admin"),
(RespAclCategories::BITMAP.bits(), "bitmap"),
(RespAclCategories::BLOCKING.bits(), "blocking"),
(RespAclCategories::CONNECTION.bits(), "connection"),
(RespAclCategories::DANGEROUS.bits(), "dangerous"),
(RespAclCategories::GEO.bits(), "geo"),
(RespAclCategories::HASH.bits(), "hash"),
(RespAclCategories::HYPERLOGLOG.bits(), "hyperloglog"),
(RespAclCategories::FAST.bits(), "fast"),
(RespAclCategories::KEYSPACE.bits(), "keyspace"),
(RespAclCategories::LIST.bits(), "list"),
(RespAclCategories::PUBSUB.bits(), "pubsub"),
(RespAclCategories::READ.bits(), "read"),
(RespAclCategories::SCRIPTING.bits(), "scripting"),
(RespAclCategories::SET.bits(), "set"),
(RespAclCategories::SORTEDSET.bits(), "sortedset"),
(RespAclCategories::SLOW.bits(), "slow"),
(RespAclCategories::STREAM.bits(), "stream"),
(RespAclCategories::STRING.bits(), "string"),
(RespAclCategories::TRANSACTION.bits(), "transaction"),
(RespAclCategories::WRITE.bits(), "write"),
(RespAclCategories::GARNET.bits(), "garnet"),
(RespAclCategories::CUSTOM.bits(), "custom"),
(RespAclCategories::VECTOR.bits(), "vector"),
];
ALL
.iter()
.filter(|(bit, _)| cats.bits() & bit != 0)
.map(|(_, desc)| *desc)
.collect()
}
#[cfg_attr(not(test), allow(dead_code))] pub(crate) fn acl_category_member_names(cats: RespAclCategories) -> Vec<&'static str> {
const ALL: [(u32, &str); 24] = [
(1, "Admin"),
(1 << 1, "Bitmap"),
(1 << 2, "Blocking"),
(1 << 3, "Connection"),
(1 << 4, "Dangerous"),
(1 << 5, "Geo"),
(1 << 6, "Hash"),
(1 << 7, "HyperLogLog"),
(1 << 8, "Fast"),
(1 << 9, "KeySpace"),
(1 << 10, "List"),
(1 << 11, "PubSub"),
(1 << 12, "Read"),
(1 << 13, "Scripting"),
(1 << 14, "Set"),
(1 << 15, "SortedSet"),
(1 << 16, "Slow"),
(1 << 17, "Stream"),
(1 << 18, "String"),
(1 << 19, "Transaction"),
(1 << 20, "Write"),
(1 << 21, "Garnet"),
(1 << 22, "Custom"),
(1 << 23, "Vector"),
];
ALL
.iter()
.filter(|(bit, _)| cats.bits() & bit != 0)
.map(|(_, name)| *name)
.collect()
}
pub(crate) fn acl_categories_from_member_names(names: &str) -> Option<RespAclCategories> {
const ALL: [(&str, u32); 24] = [
("ADMIN", 1),
("BITMAP", 1 << 1),
("BLOCKING", 1 << 2),
("CONNECTION", 1 << 3),
("DANGEROUS", 1 << 4),
("GEO", 1 << 5),
("HASH", 1 << 6),
("HYPERLOGLOG", 1 << 7),
("FAST", 1 << 8),
("KEYSPACE", 1 << 9),
("LIST", 1 << 10),
("PUBSUB", 1 << 11),
("READ", 1 << 12),
("SCRIPTING", 1 << 13),
("SET", 1 << 14),
("SORTEDSET", 1 << 15),
("SLOW", 1 << 16),
("STREAM", 1 << 17),
("STRING", 1 << 18),
("TRANSACTION", 1 << 19),
("WRITE", 1 << 20),
("GARNET", 1 << 21),
("CUSTOM", 1 << 22),
("VECTOR", 1 << 23),
];
let mut bits = 0u32;
for name in names.split(',') {
let trimmed = name.trim().to_ascii_uppercase();
let bit = ALL
.iter()
.find(|(member, _)| *member == trimmed)
.map(|(_, bit)| *bit)?;
bits |= bit;
}
Some(RespAclCategories::from_bits_retain(bits))
}
#[derive(Debug, Clone)]
pub struct RespCommandsInfo {
pub command: RespCommand,
pub name: String,
pub is_internal: bool,
pub arity: i32,
pub flags: RespCommandFlags,
pub first_key: i32,
pub last_key: i32,
pub step: i32,
pub acl_categories: RespAclCategories,
pub tips: Vec<String>,
pub key_specifications: Vec<RespCommandKeySpecification>,
pub store_type: StoreType,
pub sub_commands: Vec<RespCommandsInfo>,
pub is_sub_command: bool,
pub parent_is_internal: bool,
}
impl RespCommandsInfo {
pub fn try_get_resp_command_info(name: &str) -> Option<&'static Self> {
try_get_resp_command_info_by_name(name, false, true)
}
}
impl RespCommandsInfo {
pub fn to_resp_format(&self, writer: &mut RespMemoryWriter) {
if self.name.trim().is_empty() {
writer.write_null();
return;
}
writer.write_array_length(10);
writer.write_ascii_bulk_string(&self.name);
writer.write_int32(self.arity);
let resp_format_flags = self.flags.descriptions();
writer.write_set_length(resp_format_flags.len());
for flag in resp_format_flags {
writer.write_simple_string(flag);
}
writer.write_int32(self.first_key);
writer.write_int32(self.last_key);
writer.write_int32(self.step);
let resp_format_acl_categories = acl_category_descriptions(self.acl_categories);
writer.write_set_length(resp_format_acl_categories.len());
for acl_cat in resp_format_acl_categories {
writer.write_simple_string(&format!("@{acl_cat}"));
}
writer.write_set_length(self.tips.len());
for tip in &self.tips {
writer.write_ascii_bulk_string(tip);
}
writer.write_set_length(self.key_specifications.len());
for ks in &self.key_specifications {
ks.to_resp_format(writer);
}
writer.write_array_length(self.sub_commands.len());
for sub_command in &self.sub_commands {
sub_command.to_resp_format(writer);
}
}
#[cfg_attr(not(test), allow(dead_code))] pub(crate) fn to_import(&self) -> RespCommandsInfoImport {
let flags = {
let names = self.flags.member_names();
(!names.is_empty()).then(|| names.join(", "))
};
let acl_categories = {
let names = acl_category_member_names(self.acl_categories);
(!names.is_empty()).then(|| names.join(", "))
};
let store_type = (!matches!(self.store_type, StoreType::None))
.then_some(match self.store_type {
StoreType::Main => "Main",
StoreType::Object => "Object",
StoreType::All => "All",
StoreType::None => "None",
})
.map(str::to_string);
let key_specifications = (!self.key_specifications.is_empty())
.then(|| self.key_specifications.iter().map(ks_to_import).collect());
let sub_commands = (!self.sub_commands.is_empty())
.then(|| self.sub_commands.iter().map(|sc| sc.to_import()).collect());
RespCommandsInfoImport {
command: cs_name_of(self.command).to_string(),
name: self.name.clone(),
is_internal: self.is_internal,
arity: self.arity,
flags,
first_key: self.first_key,
last_key: self.last_key,
step: self.step,
acl_categories,
tips: (!self.tips.is_empty()).then(|| self.tips.clone()),
key_specifications,
store_type,
sub_commands,
}
}
}
#[cfg_attr(not(test), allow(dead_code))]
fn cs_name_of(cmd: RespCommand) -> &'static str {
super::resp_commands_info_data::resp_command_to_cs_name(cmd)
}
#[cfg_attr(not(test), allow(dead_code))]
fn ks_to_import(ks: &RespCommandKeySpecification) -> KeySpecificationImport {
let begin_search = ks.begin_search.as_ref().map(|m| match m {
BeginSearchMethod::Index(index) => KeySpecMethodImport {
discriminator: m.discriminator().to_string(),
index: Some(*index),
..Default::default()
},
BeginSearchMethod::Keyword {
keyword,
start_from,
} => KeySpecMethodImport {
discriminator: m.discriminator().to_string(),
keyword: Some(keyword.clone()),
start_from: Some(*start_from),
..Default::default()
},
BeginSearchMethod::Unknown => KeySpecMethodImport {
discriminator: m.discriminator().to_string(),
..Default::default()
},
});
let find_keys = ks.find_keys.as_ref().map(|m| match m {
FindKeysMethod::Range {
last_key,
key_step,
limit,
} => KeySpecMethodImport {
discriminator: m.discriminator().to_string(),
last_key: Some(*last_key),
key_step: Some(*key_step),
limit: Some(*limit),
..Default::default()
},
FindKeysMethod::KeyNum {
key_num_idx,
first_key,
key_step,
} => KeySpecMethodImport {
discriminator: m.discriminator().to_string(),
key_num_idx: Some(*key_num_idx),
first_key: Some(*first_key),
key_step: Some(*key_step),
..Default::default()
},
FindKeysMethod::Unknown => KeySpecMethodImport {
discriminator: m.discriminator().to_string(),
..Default::default()
},
});
KeySpecificationImport {
begin_search,
find_keys,
notes: ks.notes.clone(),
flags: {
let names = ks.flags.descriptions();
(!names.is_empty()).then(|| names.join(", "))
},
}
}
#[derive(Deserialize, Serialize, Clone, Default)]
struct KeySpecMethodImport {
#[serde(rename = "TypeDiscriminator")]
discriminator: String,
#[serde(rename = "Index")]
index: Option<i32>,
#[serde(rename = "Keyword")]
keyword: Option<String>,
#[serde(rename = "StartFrom")]
start_from: Option<i32>,
#[serde(rename = "LastKey")]
last_key: Option<i32>,
#[serde(rename = "KeyStep")]
key_step: Option<i32>,
#[serde(rename = "Limit")]
limit: Option<i32>,
#[serde(rename = "KeyNumIdx")]
key_num_idx: Option<i32>,
#[serde(rename = "FirstKey")]
first_key: Option<i32>,
}
#[derive(Deserialize, Serialize, Clone, Default)]
struct KeySpecificationImport {
#[serde(rename = "BeginSearch")]
begin_search: Option<KeySpecMethodImport>,
#[serde(rename = "FindKeys")]
find_keys: Option<KeySpecMethodImport>,
#[serde(rename = "Notes")]
notes: Option<String>,
#[serde(rename = "Flags")]
flags: Option<String>,
}
impl KeySpecificationImport {
fn convert(self) -> Option<RespCommandKeySpecification> {
let flags = match self.flags {
Some(f) => KeySpecificationFlags::from_wire_names(&f)?,
None => super::resp_command_key_specification::KeySpecificationFlags::NONE,
};
Some(RespCommandKeySpecification {
begin_search: self.begin_search.and_then(|m| m.into_begin_search()),
find_keys: self.find_keys.and_then(|m| m.into_find_keys()),
notes: self.notes,
flags,
})
}
}
impl KeySpecMethodImport {
fn into_begin_search(self) -> Option<BeginSearchMethod> {
if !BeginSearchMethod::can_convert(&self.discriminator) {
return None;
}
Some(match self.discriminator.as_str() {
"BeginSearchIndex" => BeginSearchMethod::Index(self.index.unwrap_or(0)),
"BeginSearchKeyword" => BeginSearchMethod::Keyword {
keyword: self.keyword?,
start_from: self.start_from.unwrap_or(0),
},
_ => BeginSearchMethod::Unknown,
})
}
fn into_find_keys(self) -> Option<FindKeysMethod> {
if !FindKeysMethod::can_convert(&self.discriminator) {
return None;
}
Some(match self.discriminator.as_str() {
"FindKeysRange" => FindKeysMethod::Range {
last_key: self.last_key.unwrap_or(0),
key_step: self.key_step.unwrap_or(0),
limit: self.limit.unwrap_or(0),
},
"FindKeysKeyNum" => FindKeysMethod::KeyNum {
key_num_idx: self.key_num_idx.unwrap_or(0),
first_key: self.first_key.unwrap_or(0),
key_step: self.key_step.unwrap_or(0),
},
_ => FindKeysMethod::Unknown,
})
}
}
#[derive(Deserialize, Serialize, Clone, Default)]
pub(crate) struct RespCommandsInfoImport {
#[serde(rename = "Command")]
command: String,
#[serde(rename = "Name")]
name: String,
#[serde(rename = "IsInternal", default)]
is_internal: bool,
#[serde(rename = "Arity", default)]
arity: i32,
#[serde(rename = "Flags")]
flags: Option<String>,
#[serde(rename = "FirstKey", default)]
first_key: i32,
#[serde(rename = "LastKey", default)]
last_key: i32,
#[serde(rename = "Step", default)]
step: i32,
#[serde(rename = "AclCategories")]
acl_categories: Option<String>,
#[serde(rename = "Tips")]
tips: Option<Vec<String>>,
#[serde(rename = "KeySpecifications")]
key_specifications: Option<Vec<KeySpecificationImport>>,
#[serde(rename = "StoreType")]
store_type: Option<String>,
#[serde(rename = "SubCommands")]
sub_commands: Option<Vec<RespCommandsInfoImport>>,
}
impl IRespCommandData for RespCommandsInfoImport {
fn name(&self) -> &str {
&self.name
}
}
impl RespCommandsInfoImport {
fn convert(self, parent_is_internal: bool, depth: usize) -> Option<RespCommandsInfo> {
if self.name.is_empty() {
return None;
}
let command = resp_command_from_cs_name(&self.command)?;
let flags = match &self.flags {
Some(f) => RespCommandFlags::from_member_names(f)?,
None => RespCommandFlags::empty(),
};
let acl_categories = match &self.acl_categories {
Some(a) => acl_categories_from_member_names(a)?,
None => RespAclCategories::from_bits_retain(0),
};
let store_type = match &self.store_type {
Some(s) => StoreType::from_member_name(s)?,
None => StoreType::None,
};
let mut key_specifications = Vec::new();
for ks in self.key_specifications.unwrap_or_default() {
key_specifications.push(ks.convert()?);
}
let mut sub_commands = Vec::new();
if depth < 4 {
for sc in self.sub_commands.unwrap_or_default() {
sub_commands.push(sc.convert(self.is_internal, depth + 1)?);
}
}
Some(RespCommandsInfo {
command,
name: self.name,
is_internal: self.is_internal,
arity: self.arity,
flags,
first_key: self.first_key,
last_key: self.last_key,
step: self.step,
acl_categories,
tips: self.tips.unwrap_or_default(),
key_specifications,
store_type,
sub_commands,
is_sub_command: depth > 0,
parent_is_internal,
})
}
}
pub(crate) struct RespCommandsTables {
pub all: HashMap<String, RespCommandsInfo>,
pub all_sub: HashMap<String, RespCommandsInfo>,
pub external: HashMap<String, RespCommandsInfo>,
pub external_sub: HashMap<String, RespCommandsInfo>,
pub all_names: HashSet<String>,
pub external_names: HashSet<String>,
pub flattened: HashMap<u16, RespCommandsInfo>,
pub simple: Vec<SimpleRespCommandInfo>,
pub acl_command_info: HashMap<u32, Vec<RespCommandsInfo>>,
pub fast_basic: Vec<Option<RespCommandsInfo>>,
}
static TABLES: OnceLock<Option<RespCommandsTables>> = OnceLock::new();
fn try_initialize() -> bool {
TABLES
.get_or_init(|| {
if try_initialize_resp_commands_info() {
Some(build_tables())
} else {
None
}
})
.is_some()
}
static IMPORTED: OnceLock<Vec<RespCommandsInfo>> = OnceLock::new();
fn try_initialize_resp_commands_info() -> bool {
let imported = try_import_resp_commands_data::<RespCommandsInfoImport>(RESP_COMMANDS_INFO_JSON);
let Some(imported) = imported else {
return false;
};
let mut converted = Vec::with_capacity(imported.len());
for entry in imported {
let Some(info) = entry.convert(false, 0) else {
return false;
};
converted.push(info);
}
IMPORTED.set(converted).is_ok()
}
fn build_tables() -> RespCommandsTables {
let imported = IMPORTED.get().expect("导入已完成");
let mut all: HashMap<String, RespCommandsInfo> = HashMap::with_hasher(GxBuildHasher::default());
let mut all_sub: HashMap<String, RespCommandsInfo> =
HashMap::with_hasher(GxBuildHasher::default());
let mut external: HashMap<String, RespCommandsInfo> =
HashMap::with_hasher(GxBuildHasher::default());
let mut external_sub: HashMap<String, RespCommandsInfo> =
HashMap::with_hasher(GxBuildHasher::default());
let mut flattened: HashMap<u16, RespCommandsInfo> =
HashMap::with_hasher(GxBuildHasher::default());
let mut acl_command_info: HashMap<u32, Vec<RespCommandsInfo>> =
HashMap::with_hasher(GxBuildHasher::default());
for entry in imported {
if entry.command == RespCommand::None {
continue;
}
if entry.name == "SLAVEOF" {
continue;
}
flattened.insert(entry.command as u16, entry.clone());
for sc in &entry.sub_commands {
flattened.insert(sc.command as u16, sc.clone());
}
}
for entry in imported {
all.insert(entry.name.to_lowercase(), entry.clone());
if !entry.is_internal {
external.insert(entry.name.to_lowercase(), entry.clone());
}
for sc in &entry.sub_commands {
all_sub.insert(sc.name.to_lowercase(), sc.clone());
if !entry.is_internal && !sc.is_internal {
external_sub.insert(sc.name.to_lowercase(), sc.clone());
}
}
for cmd in [entry].into_iter().chain(entry.sub_commands.iter()) {
for single in individual_acls(cmd.acl_categories) {
acl_command_info
.entry(single)
.or_default()
.push(cmd.clone());
}
}
}
let mut all_names: HashSet<String> = HashSet::with_hasher(GxBuildHasher::default());
for k in all.keys() {
all_names.insert(k.clone());
}
let mut external_names: HashSet<String> = HashSet::with_hasher(GxBuildHasher::default());
for k in external.keys() {
external_names.insert(k.clone());
}
let table_len = LAST_VALID_COMMAND as usize + 1;
let mut simple = vec![SimpleRespCommandInfo::default(); table_len];
for (cmd_id, slot) in simple
.iter_mut()
.enumerate()
.take(table_len)
.skip(FIRST_DATA_COMMAND as usize)
{
let Some(cmd_info) = flattened.get(&(cmd_id as u16)) else {
continue;
};
populate_simple_command_info(cmd_info, slot);
}
let fast_len = LAST_DATA_COMMAND as usize - FIRST_DATA_COMMAND as usize + 1;
let mut fast_basic: Vec<Option<RespCommandsInfo>> = (0..fast_len).map(|_| None).collect();
for (i, slot) in fast_basic.iter_mut().enumerate() {
if let Some(info) = flattened.get(&((i + FIRST_DATA_COMMAND as usize) as u16)) {
*slot = Some(info.clone());
}
}
RespCommandsTables {
all,
all_sub,
external,
external_sub,
all_names,
external_names,
flattened,
simple,
acl_command_info,
fast_basic,
}
}
fn tables() -> Option<&'static RespCommandsTables> {
if !try_initialize() {
return None;
}
TABLES.get().and_then(|t| t.as_ref())
}
pub(crate) fn individual_acls(acl_categories: RespAclCategories) -> Vec<u32> {
let mut out = Vec::new();
let mut remaining = acl_categories.bits();
while remaining != 0 {
let single = remaining.isolate_lowest_one();
remaining &= !single;
out.push(single);
}
out
}
pub fn try_get_commandsfor_acl_category(
acl: RespAclCategories,
) -> Option<Vec<&'static RespCommandsInfo>> {
let tables = tables()?;
if acl.bits().count_ones() != 1 {
return None;
}
tables
.acl_command_info
.get(&acl.bits())
.map(|v| v.iter().collect())
}
pub fn try_get_resp_commands_info_count(external_only: bool) -> Option<usize> {
let tables = tables()?;
Some(if external_only {
tables.external.len()
} else {
tables.all.len()
})
}
pub fn try_get_resp_commands_info(
external_only: bool,
) -> Option<&'static HashMap<String, RespCommandsInfo>> {
let tables = tables()?;
Some(if external_only {
&tables.external
} else {
&tables.all
})
}
pub fn try_get_resp_command_names(external_only: bool) -> Option<&'static HashSet<String>> {
let tables = tables()?;
Some(if external_only {
&tables.external_names
} else {
&tables.all_names
})
}
pub fn try_get_resp_command_info_by_name(
cmd_name: &str,
external_only: bool,
include_sub_commands: bool,
) -> Option<&'static RespCommandsInfo> {
let tables = tables()?;
let key = cmd_name.to_lowercase();
let primary = if external_only {
&tables.external
} else {
&tables.all
};
primary.get(&key).or_else(|| {
if include_sub_commands {
let sub = if external_only {
&tables.external_sub
} else {
&tables.all_sub
};
sub.get(&key)
} else {
None
}
})
}
pub fn try_get_resp_command_info_by_cmd(
cmd: RespCommand,
txn_only: bool,
) -> Option<&'static RespCommandsInfo> {
let tables = tables()?;
let info = tables.flattened.get(&(cmd as u16))?;
if txn_only && info.flags.intersects(RespCommandFlags::NO_MULTI) {
return None;
}
Some(info)
}
pub fn try_fast_get_resp_command_info(cmd: RespCommand) -> Option<&'static RespCommandsInfo> {
let tables = tables()?;
let offset = cmd as usize - FIRST_DATA_COMMAND as usize;
if offset >= tables.fast_basic.len() {
return None;
}
tables.fast_basic[offset].as_ref()
}
pub fn try_get_resp_sub_commands_info(
external_only: bool,
) -> Option<&'static HashMap<String, RespCommandsInfo>> {
let tables = tables()?;
Some(if external_only {
&tables.external_sub
} else {
&tables.all_sub
})
}
pub fn try_get_simple_resp_command_info(cmd: RespCommand) -> Option<SimpleRespCommandInfo> {
let tables = tables()?;
let cmd_id = cmd as usize;
if cmd_id >= tables.simple.len() {
return None;
}
Some(tables.simple[cmd_id].clone())
}
pub fn get_resp_command_name(cmd: RespCommand) -> String {
match try_get_resp_command_info_by_cmd(cmd, false) {
Some(info) => info.name.clone(),
None => UNKNOWN_COMMAND_NAME.to_string(),
}
}
#[cfg(test)]
mod tests {
use gxhash::{GxBuildHasher, HashMap};
use super::{
super::resp_memory_writer::RespMemoryWriter, RespCommandFlags, StoreType,
acl_categories_from_member_names, get_resp_command_name, individual_acls,
try_fast_get_resp_command_info, try_get_commandsfor_acl_category,
try_get_resp_command_info_by_cmd, try_get_resp_command_info_by_name,
try_get_resp_commands_info, try_get_resp_commands_info_count,
};
use crate::{acl::RespAclCategories, types::RespCommand};
#[test]
fn tables_initialize_and_lookup() {
assert!(super::try_initialize());
let all = super::try_get_resp_commands_info(false).unwrap();
let external = super::try_get_resp_commands_info(true).unwrap();
assert_eq!(all.len(), 262, "根命令数快照");
assert_eq!(external.len(), 258, "外部根命令数快照");
assert_eq!(try_get_resp_commands_info_count(false), Some(all.len()));
assert_eq!(try_get_resp_commands_info_count(true), Some(external.len()));
let get = try_get_resp_command_info_by_name("GET", false, false).unwrap();
assert_eq!(get.arity, 2);
assert_eq!(get.first_key, 1);
assert_eq!(get.last_key, 1);
assert_eq!(get.step, 1);
assert_eq!(get.store_type, StoreType::Main);
assert_eq!(
get.flags,
RespCommandFlags::from_member_names("Fast, ReadOnly").unwrap()
);
let acl_cat = try_get_resp_command_info_by_name("ACL|CAT", false, true).unwrap();
assert_eq!(acl_cat.command, RespCommand::AclCat);
assert!(
try_get_resp_command_info_by_name("ACL|CAT", false, false).is_none(),
"不带子命令检索时不可达"
);
let set = try_get_resp_command_info_by_cmd(RespCommand::Set, false).unwrap();
assert_eq!(set.name, "SET");
assert!(try_get_resp_command_info_by_cmd(RespCommand::Async, true).is_none());
assert!(try_get_resp_command_info_by_cmd(RespCommand::Async, false).is_some());
assert!(try_get_resp_command_info_by_cmd(RespCommand::Secondaryof, false).is_some());
assert!(try_get_resp_command_info_by_cmd(RespCommand::Replicaof, false).is_some());
let fast = try_fast_get_resp_command_info(RespCommand::Append).unwrap();
assert_eq!(fast.name, "APPEND");
assert!(try_fast_get_resp_command_info(RespCommand::Quit).is_none());
assert_eq!(get_resp_command_name(RespCommand::Bitcount), "BITCOUNT");
assert_eq!(get_resp_command_name(RespCommand::Invalid), "UNKNOWN");
}
#[test]
fn acl_category_index() {
let bitmap = try_get_commandsfor_acl_category(RespAclCategories::BITMAP).unwrap();
let names: Vec<&str> = bitmap.iter().map(|c| c.name.as_str()).collect();
assert!(names.contains(&"SETBIT"));
assert!(names.contains(&"GETBIT"));
assert!(names.contains(&"BITCOUNT"));
assert!(names.contains(&"BITPOS"));
assert!(names.contains(&"BITFIELD"));
assert!(names.contains(&"BITFIELD_RO"));
assert!(names.contains(&"BITOP"));
assert!(
try_get_commandsfor_acl_category(RespAclCategories::BITMAP | RespAclCategories::STRING)
.is_none()
);
}
#[test]
fn command_info_resp3_snapshot_get() {
let get = try_get_resp_command_info_by_cmd(RespCommand::Get, false).unwrap();
let mut w = RespMemoryWriter::new(true);
get.to_resp_format(&mut w);
let expected = concat!(
"*10\r\n$3\r\nGET\r\n:2\r\n~2\r\n+fast\r\n+readonly\r\n:1\r\n:1\r\n:1\r\n",
"~3\r\n+@fast\r\n+@read\r\n+@string\r\n~0\r\n~1\r\n%3\r\n$5\r\nflags\r\n~2\r\n+RO\r\n+access\r\n",
"$12\r\nbegin_search\r\n%2\r\n$4\r\ntype\r\n$5\r\nindex\r\n$4\r\nspec\r\n%1\r\n$5\r\nindex\r\n:1\r\n",
"$9\r\nfind_keys\r\n%2\r\n$4\r\ntype\r\n$5\r\nrange\r\n$4\r\nspec\r\n%3\r\n$7\r\nlastkey\r\n:0\r\n$7\r\nkeystep\r\n:1\r\n$5\r\nlimit\r\n:0\r\n*0\r\n",
);
assert_eq!(String::from_utf8(w.out).unwrap(), expected);
}
#[test]
fn command_info_resp3_snapshot_bitfield() {
let bf = try_get_resp_command_info_by_cmd(RespCommand::Bitfield, false).unwrap();
let mut w = RespMemoryWriter::new(true);
bf.to_resp_format(&mut w);
let expected = concat!(
"*10\r\n$8\r\nBITFIELD\r\n:-2\r\n~2\r\n+denyoom\r\n+write\r\n:1\r\n:1\r\n:1\r\n",
"~3\r\n+@bitmap\r\n+@slow\r\n+@write\r\n~0\r\n~1\r\n%4\r\n$5\r\nnotes\r\n$59\r\nThis command allows both access and modification of the key\r\n",
"$5\r\nflags\r\n~4\r\n+RW\r\n+access\r\n+update\r\n+variable_flags\r\n",
"$12\r\nbegin_search\r\n%2\r\n$4\r\ntype\r\n$5\r\nindex\r\n$4\r\nspec\r\n%1\r\n$5\r\nindex\r\n:1\r\n",
"$9\r\nfind_keys\r\n%2\r\n$4\r\ntype\r\n$5\r\nrange\r\n$4\r\nspec\r\n%3\r\n$7\r\nlastkey\r\n:0\r\n$7\r\nkeystep\r\n:1\r\n$5\r\nlimit\r\n:0\r\n*0\r\n",
);
assert_eq!(String::from_utf8(w.out).unwrap(), expected);
}
#[test]
fn command_info_resp2_snapshot_setbit() {
let setbit = try_get_resp_command_info_by_cmd(RespCommand::Setbit, false).unwrap();
let mut w = RespMemoryWriter::new(false);
setbit.to_resp_format(&mut w);
let expected = concat!(
"*10\r\n$6\r\nSETBIT\r\n:4\r\n*2\r\n+denyoom\r\n+write\r\n:1\r\n:1\r\n:1\r\n",
"*3\r\n+@bitmap\r\n+@slow\r\n+@write\r\n*0\r\n*1\r\n*6\r\n$5\r\nflags\r\n*3\r\n+RW\r\n+access\r\n+update\r\n",
"$12\r\nbegin_search\r\n*4\r\n$4\r\ntype\r\n$5\r\nindex\r\n$4\r\nspec\r\n*2\r\n$5\r\nindex\r\n:1\r\n",
"$9\r\nfind_keys\r\n*4\r\n$4\r\ntype\r\n$5\r\nrange\r\n$4\r\nspec\r\n*6\r\n$7\r\nlastkey\r\n:0\r\n$7\r\nkeystep\r\n:1\r\n$5\r\nlimit\r\n:0\r\n*0\r\n",
);
assert_eq!(String::from_utf8(w.out).unwrap(), expected);
}
#[test]
fn export_import_roundtrip() {
use super::super::resp_command_data_provider::get_resp_commands_data_provider;
let all = try_get_resp_commands_info(false).unwrap();
let mut exports: Vec<super::RespCommandsInfoImport> = Vec::with_capacity(all.len());
let mut expected_arity: HashMap<String, i32> = HashMap::with_hasher(GxBuildHasher::default());
for info in all.values() {
exports.push(info.to_import());
expected_arity.insert(info.name.to_lowercase(), info.arity);
}
let provider = get_resp_commands_data_provider();
let json = provider.try_export_resp_commands_data(&exports).unwrap();
let reimported = provider
.try_import_resp_commands_data::<super::RespCommandsInfoImport>(&json)
.unwrap();
assert_eq!(reimported.len(), all.len(), "重导入条目数一致");
for entry in reimported {
let info = entry.convert(false, 0).expect("重导入可转换");
assert_eq!(
expected_arity.get(&info.name.to_lowercase()),
Some(&info.arity),
"{} arity 保持",
info.name
);
}
}
#[test]
fn individual_acls_yields_single_bits() {
let cats = acl_categories_from_member_names("Fast, String, Write").unwrap();
let bits = individual_acls(cats);
assert_eq!(
bits,
vec![
RespAclCategories::FAST.bits(),
RespAclCategories::STRING.bits(),
RespAclCategories::WRITE.bits()
]
);
}
}