use std::{fmt, sync::Arc};
use gxhash::HashMap;
use parking_lot::RwLock;
use crate::types::GarnetObjectType;
const MAX_CUSTOM_RAW_STRING_COMMANDS: usize = 256;
const CUSTOM_RAW_STRING_COMMAND_MIN_ID: u16 =
(u16::MAX - 1) - MAX_CUSTOM_RAW_STRING_COMMANDS as u16 + 1;
const CUSTOM_RAW_STRING_COMMAND_MAX_ID: u16 = u16::MAX - 1;
const CUSTOM_OBJECT_TYPE_MIN_ID: u8 = 0x40;
const CUSTOM_OBJECT_TYPE_MAX_ID: u8 = 0xFE;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum CommandType {
Read = 0,
ReadModifyWrite = 1,
}
pub type RawStringFn = Arc<dyn Fn(&[&[u8]]) -> Vec<u8> + Send + Sync>;
#[derive(Clone)]
pub struct CustomRawStringCommand {
pub name: String,
pub ext_id: u16,
pub command_type: CommandType,
pub arity: i32,
pub expiration_ticks: i64,
pub functions: RawStringFn,
}
pub struct RawStringCommandSpec<'a> {
pub name: &'a str,
pub command_type: CommandType,
pub functions: RawStringFn,
pub command_info: Option<CustomCommandInfo>,
pub command_docs: Option<CustomCommandDocs>,
pub expiration_ticks: i64,
}
impl fmt::Debug for CustomRawStringCommand {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CustomRawStringCommand")
.field("name", &self.name)
.field("ext_id", &self.ext_id)
.field("command_type", &self.command_type)
.field("arity", &self.arity)
.finish_non_exhaustive()
}
}
#[derive(Clone)]
pub struct CustomObjectCommand {
pub name: String,
pub ext_id: u8,
pub sub_id: u8,
pub command_type: CommandType,
pub arity: i32,
}
#[derive(Default)]
pub struct CustomObjectCommandWrapper {
pub ext_id: u8,
pub command_map: Vec<Option<CustomObjectCommand>>,
pub next_sub_id: usize,
}
#[derive(Clone)]
pub struct CustomTransaction {
pub name: String,
pub id: u8,
pub arity: i32,
}
#[derive(Clone)]
pub struct CustomProcedureWrapper {
pub name: String,
pub id: u8,
pub arity: i32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CustomCommandInfo {
pub name: String,
pub arity: i32,
pub acl_categories: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CustomCommandDocs {
pub name: String,
pub summary: String,
}
#[derive(Debug)]
struct IdSpace {
next: u64,
max: u64,
}
impl IdSpace {
fn new(min: u64, max: u64) -> Self {
Self { next: min, max }
}
fn try_get_next_id(&mut self, occupied: &dyn Fn(u64) -> bool) -> Option<u64> {
while self.next <= self.max {
let candidate = self.next;
self.next += 1;
if !occupied(candidate) {
return Some(candidate);
}
}
None
}
}
pub struct CustomCommandManager {
raw_string_commands: Vec<Option<CustomRawStringCommand>>,
raw_string_ids: IdSpace,
object_commands: Vec<Option<CustomObjectCommandWrapper>>,
object_type_ids: IdSpace,
transaction_procs: Vec<Option<CustomTransaction>>,
transaction_ids: IdSpace,
custom_procedures: Vec<Option<CustomProcedureWrapper>>,
procedure_ids: IdSpace,
modules: HashMap<String, u32>,
type_names: HashMap<String, u8>,
custom_commands_info: HashMap<String, CustomCommandInfo>,
custom_commands_docs: HashMap<String, CustomCommandDocs>,
custom_command_names: HashMap<String, u8>,
}
impl Default for CustomCommandManager {
fn default() -> Self {
Self::new()
}
}
impl CustomCommandManager {
pub fn new() -> Self {
Self {
raw_string_commands: Vec::new(),
raw_string_ids: IdSpace::new(
u64::from(CUSTOM_RAW_STRING_COMMAND_MIN_ID),
u64::from(CUSTOM_RAW_STRING_COMMAND_MAX_ID),
),
object_commands: Vec::new(),
object_type_ids: IdSpace::new(
u64::from(CUSTOM_OBJECT_TYPE_MIN_ID),
u64::from(CUSTOM_OBJECT_TYPE_MAX_ID),
),
transaction_procs: Vec::new(),
transaction_ids: IdSpace::new(0, u8::MAX as u64),
custom_procedures: Vec::new(),
procedure_ids: IdSpace::new(0, u8::MAX as u64),
modules: HashMap::default(),
type_names: HashMap::default(),
custom_commands_info: HashMap::default(),
custom_commands_docs: HashMap::default(),
custom_command_names: HashMap::default(),
}
}
pub fn register_raw_string_command(
&mut self,
spec: RawStringCommandSpec,
) -> Result<u16, &'static str> {
let cmd_id = self
.raw_string_ids
.try_get_next_id(&|id| {
self
.raw_string_commands
.get(id as usize)
.and_then(Option::as_ref)
.is_some()
})
.ok_or("Out of registration space")?;
let ext_id = (cmd_id - u64::from(CUSTOM_RAW_STRING_COMMAND_MIN_ID)) as u16;
let arity = spec.command_info.as_ref().map_or(0, |info| info.arity);
let new_cmd = CustomRawStringCommand {
name: spec.name.to_lowercase(),
ext_id,
command_type: spec.command_type,
arity,
expiration_ticks: spec.expiration_ticks,
functions: spec.functions,
};
let slot = cmd_id as usize;
if self.raw_string_commands.len() <= slot {
self.raw_string_commands.resize_with(slot + 1, || None);
}
self.raw_string_commands[slot] = Some(new_cmd);
self.track_registration(spec.name, spec.command_info, spec.command_docs)?;
Ok(ext_id)
}
pub fn register_transaction(
&mut self,
name: &str,
command_info: Option<CustomCommandInfo>,
command_docs: Option<CustomCommandDocs>,
) -> Result<u8, &'static str> {
let cmd_id = self
.transaction_ids
.try_get_next_id(&|id| {
self
.transaction_procs
.get(id as usize)
.and_then(Option::as_ref)
.is_some()
})
.ok_or("Out of registration space")?;
let arity = command_info.as_ref().map_or(0, |info| info.arity);
let new_cmd = CustomTransaction {
name: name.to_lowercase(),
id: cmd_id as u8,
arity,
};
let slot = cmd_id as usize;
if self.transaction_procs.len() <= slot {
self.transaction_procs.resize_with(slot + 1, || None);
}
self.transaction_procs[slot] = Some(new_cmd);
self.track_registration(name, command_info, command_docs)?;
Ok(cmd_id as u8)
}
pub fn register_type(&mut self, type_name: &str) -> Result<u8, &'static str> {
let type_key = type_name.to_lowercase();
if self.type_names.contains_key(&type_key) {
return Err("Type already registered with ID");
}
Ok(self.register_new_type(&type_key)? - CUSTOM_OBJECT_TYPE_MIN_ID)
}
pub fn register_object_command(
&mut self,
type_name: &str,
name: &str,
command_type: CommandType,
command_info: Option<CustomCommandInfo>,
command_docs: Option<CustomCommandDocs>,
) -> Result<(u8, u8), &'static str> {
let type_key = type_name.to_lowercase();
let type_slot = if let Some(&ext_id) = self.type_names.get(&type_key) {
ext_id as usize
} else {
(self.register_new_type(&type_key)? - CUSTOM_OBJECT_TYPE_MIN_ID) as usize
};
let wrapper = self.object_commands[type_slot]
.as_mut()
.ok_or("Out of registration space")?;
let sc_id = wrapper.next_sub_id;
if sc_id > u8::MAX as usize {
return Err("Out of registration space");
}
wrapper.next_sub_id += 1;
let ext_id = wrapper.ext_id;
let arity = command_info.as_ref().map_or(0, |info| info.arity);
let new_sub_cmd = CustomObjectCommand {
name: name.to_lowercase(),
ext_id,
sub_id: sc_id as u8,
command_type,
arity,
};
let slot = sc_id;
if wrapper.command_map.len() <= slot {
wrapper.command_map.resize_with(slot + 1, || None);
}
wrapper.command_map[slot] = Some(new_sub_cmd);
self.track_registration(name, command_info, command_docs)?;
Ok((ext_id, sc_id as u8))
}
pub fn register_procedure(
&mut self,
name: &str,
command_info: Option<CustomCommandInfo>,
command_docs: Option<CustomCommandDocs>,
) -> Result<u8, &'static str> {
let cmd_id = self
.procedure_ids
.try_get_next_id(&|id| {
self
.custom_procedures
.get(id as usize)
.and_then(Option::as_ref)
.is_some()
})
.ok_or("Out of registration space")?;
let arity = command_info.as_ref().map_or(0, |info| info.arity);
let new_cmd = CustomProcedureWrapper {
name: name.to_lowercase(),
id: cmd_id as u8,
arity,
};
let slot = cmd_id as usize;
if self.custom_procedures.len() <= slot {
self.custom_procedures.resize_with(slot + 1, || None);
}
self.custom_procedures[slot] = Some(new_cmd);
self.track_registration(name, command_info, command_docs)?;
Ok(cmd_id as u8)
}
pub fn register_module(&mut self, module_name: &str, version: u32) -> Result<(), &'static str> {
let initialized = !module_name.is_empty();
if !initialized {
return Err("ERR module failed to load");
}
self
.try_add_module(module_name, version)
.ok_or("ERR module failed to load")?;
Ok(())
}
pub fn try_add_module(&mut self, module_name: &str, version: u32) -> Option<()> {
if self.modules.contains_key(module_name) {
return None;
}
self.modules.insert(module_name.to_string(), version);
Some(())
}
pub fn try_get_custom_procedure(&self, id: u8) -> Option<CustomProcedureWrapper> {
self
.custom_procedures
.get(id as usize)
.and_then(Option::as_ref)
.cloned()
}
pub fn try_get_custom_transaction_procedure(&self, id: u8) -> Option<CustomTransaction> {
self
.transaction_procs
.get(id as usize)
.and_then(Option::as_ref)
.cloned()
}
pub fn try_get_custom_command(&self, id: u16) -> Option<CustomRawStringCommand> {
let slot = u64::from(CUSTOM_RAW_STRING_COMMAND_MIN_ID) + u64::from(id);
self
.raw_string_commands
.get(slot as usize)
.and_then(Option::as_ref)
.cloned()
}
pub fn try_get_custom_object_command(&self, id: u8) -> Option<&CustomObjectCommandWrapper> {
self
.object_commands
.get(id as usize)
.and_then(Option::as_ref)
}
pub fn try_get_custom_object_sub_command(
&self,
id: u8,
sub_id: u8,
) -> Option<CustomObjectCommand> {
self
.try_get_custom_object_command(id)?
.command_map
.get(sub_id as usize)
.and_then(Option::as_ref)
.cloned()
}
pub fn match_raw_string_command(&self, command: &[u8]) -> Option<CustomRawStringCommand> {
let name = String::from_utf8_lossy(command).to_lowercase();
self
.raw_string_commands
.iter()
.flatten()
.find(|cmd| cmd.name == name)
.cloned()
}
pub fn try_get_custom_command_info(&self, cmd_name: &str) -> Option<&CustomCommandInfo> {
self.custom_commands_info.get(&cmd_name.to_lowercase())
}
pub fn is_custom_command_registered(&self, cmd_name: &str) -> bool {
!cmd_name.is_empty()
&& self
.custom_command_names
.contains_key(&cmd_name.to_lowercase())
}
pub fn try_get_custom_command_docs(&self, cmd_name: &str) -> Option<&CustomCommandDocs> {
self.custom_commands_docs.get(&cmd_name.to_lowercase())
}
pub fn get_all_custom_commands_infos(&self) -> Vec<(String, CustomCommandInfo)> {
self
.custom_commands_info
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect()
}
pub fn get_all_custom_commands_docs(&self) -> Vec<(String, CustomCommandDocs)> {
self
.custom_commands_docs
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect()
}
pub fn get_custom_command_info_count(&self) -> usize {
self.custom_commands_info.len()
}
pub fn get_custom_resp_command(&self, id: u16) -> u16 {
CUSTOM_RAW_STRING_COMMAND_MIN_ID + id
}
pub fn get_custom_garnet_object_type(&self, id: u8) -> GarnetObjectType {
let _ = id;
GarnetObjectType::Null
}
fn register_new_type(&mut self, type_name: &str) -> Result<u8, &'static str> {
let type_id = self
.object_type_ids
.try_get_next_id(&|id| {
self
.object_commands
.get(id as usize)
.and_then(Option::as_ref)
.is_some()
})
.ok_or("Out of registration space")?;
let ext_id = (type_id - u64::from(CUSTOM_OBJECT_TYPE_MIN_ID)) as u8;
let wrapper = CustomObjectCommandWrapper {
ext_id,
command_map: Vec::new(),
next_sub_id: 0,
};
let slot = ext_id as usize;
if self.object_commands.len() <= slot {
self.object_commands.resize_with(slot + 1, || None);
}
self.object_commands[slot] = Some(wrapper);
self.type_names.insert(type_name.to_lowercase(), ext_id);
Ok(type_id as u8)
}
fn track_registration(
&mut self,
name: &str,
command_info: Option<CustomCommandInfo>,
command_docs: Option<CustomCommandDocs>,
) -> Result<(), &'static str> {
let key = name.to_lowercase();
self.custom_command_names.insert(key.clone(), 0);
if let Some(info) = command_info {
self.custom_commands_info.insert(key.clone(), info);
}
if let Some(docs) = command_docs {
self.custom_commands_docs.insert(key, docs);
}
Ok(())
}
}
pub type SharedCustomCommandManager = Arc<RwLock<CustomCommandManager>>;
#[cfg(test)]
mod tests {
use super::*;
fn info(name: &str, arity: i32) -> CustomCommandInfo {
CustomCommandInfo {
name: name.to_string(),
arity,
acl_categories: vec!["custom".to_string()],
}
}
fn docs(name: &str) -> CustomCommandDocs {
CustomCommandDocs {
name: name.to_string(),
summary: format!("{} docs", name),
}
}
fn echo_fn() -> RawStringFn {
Arc::new(|args: &[&[u8]]| args.first().copied().unwrap_or(b"").to_vec())
}
#[test]
fn raw_string_command_registration_and_lookup() {
let mut manager = CustomCommandManager::new();
let id = manager
.register_raw_string_command(RawStringCommandSpec {
name: "MYCMD",
command_type: CommandType::Read,
functions: echo_fn(),
command_info: Some(info("MYCMD", 2)),
command_docs: Some(docs("MYCMD")),
expiration_ticks: 0,
})
.unwrap();
assert_eq!(id, 0);
let cmd = manager.try_get_custom_command(id).unwrap();
assert_eq!(cmd.name, "mycmd");
assert_eq!(cmd.arity, 2);
assert_eq!(cmd.command_type, CommandType::Read);
let output = (cmd.functions)(&[b"payload".as_slice()]);
assert_eq!(output, b"payload".to_vec());
assert!(manager.match_raw_string_command(b"myCmd").is_some());
assert!(manager.match_raw_string_command(b"nope").is_none());
assert!(manager.is_custom_command_registered("MyCmd"));
assert!(!manager.is_custom_command_registered("Other"));
assert_eq!(manager.get_custom_command_info_count(), 1);
assert_eq!(
manager.try_get_custom_command_info("mycmd").unwrap().arity,
2
);
assert!(manager.try_get_custom_command_docs("MYCMD").is_some());
let resp_cmd_repr = manager.get_custom_resp_command(id);
assert_eq!(resp_cmd_repr, CUSTOM_RAW_STRING_COMMAND_MIN_ID);
}
#[test]
fn registration_space_exhaustion() {
let mut manager = CustomCommandManager::new();
for i in 0..MAX_CUSTOM_RAW_STRING_COMMANDS {
manager
.register_raw_string_command(RawStringCommandSpec {
name: &format!("cmd{i}"),
command_type: CommandType::ReadModifyWrite,
functions: echo_fn(),
command_info: None,
command_docs: None,
expiration_ticks: 0,
})
.unwrap();
}
assert!(
manager
.register_raw_string_command(RawStringCommandSpec {
name: "overflow",
command_type: CommandType::ReadModifyWrite,
functions: echo_fn(),
command_info: None,
command_docs: None,
expiration_ticks: 0,
})
.unwrap_err()
.contains("Out of registration space")
);
}
#[test]
fn object_type_and_sub_commands() {
let mut manager = CustomCommandManager::new();
let type_id = manager.register_type("MYOBJ").unwrap();
assert_eq!(type_id, 0);
assert!(manager.register_type("myobj").is_err());
let (ext_id, sub_id) = manager
.register_object_command(
"MYOBJ",
"GETPROP",
CommandType::Read,
Some(info("GETPROP", 1)),
None,
)
.unwrap();
assert_eq!((ext_id, sub_id), (0, 0));
let (_, sub_id2) = manager
.register_object_command("MYOBJ", "SETPROP", CommandType::ReadModifyWrite, None, None)
.unwrap();
assert_eq!(sub_id2, 1);
let sub = manager
.try_get_custom_object_sub_command(ext_id, sub_id)
.unwrap();
assert_eq!(sub.name, "getprop");
assert!(
manager
.try_get_custom_object_sub_command(ext_id, 9)
.is_none()
);
assert!(manager.try_get_custom_object_command(5).is_none());
let obj_type = manager.get_custom_garnet_object_type(ext_id);
assert_eq!(obj_type, GarnetObjectType::Null);
assert_eq!(manager.get_custom_command_info_count(), 1);
}
#[test]
fn multiple_object_types_resolve_by_name() {
let mut manager = CustomCommandManager::new();
manager.register_type("TYPE_A").unwrap();
let type_b = manager.register_type("TYPE_B").unwrap();
assert_eq!(type_b, 1);
let (ext_id, _) = manager
.register_object_command("TYPE_B", "CMD_B", CommandType::Read, None, None)
.unwrap();
assert_eq!(ext_id, type_b);
assert!(
manager
.try_get_custom_object_sub_command(type_b, 0)
.is_some()
);
assert!(manager.try_get_custom_object_sub_command(0, 0).is_none());
let (ext_c, _) = manager
.register_object_command("TYPE_C", "CMD_C", CommandType::Read, None, None)
.unwrap();
assert_eq!(ext_c, 2);
}
#[test]
fn transactions_and_procedures() {
let mut manager = CustomCommandManager::new();
let txn_id = manager
.register_transaction("MYTXN", Some(info("MYTXN", -3)), None)
.unwrap();
assert_eq!(txn_id, 0);
let txn = manager
.try_get_custom_transaction_procedure(txn_id)
.unwrap();
assert_eq!(txn.arity, -3);
assert!(manager.try_get_custom_transaction_procedure(9).is_none());
let proc_id = manager
.register_procedure("MYPROC", None, Some(docs("MYPROC")))
.unwrap();
assert_eq!(proc_id, 0);
let proc = manager.try_get_custom_procedure(proc_id).unwrap();
assert_eq!(proc.name, "myproc");
assert!(manager.try_get_custom_command_docs("myproc").is_some());
assert!(!manager.get_all_custom_commands_docs().is_empty());
assert!(!manager.get_all_custom_commands_infos().is_empty());
}
#[test]
fn module_registration() {
let mut manager = CustomCommandManager::new();
assert!(manager.register_module("MYMOD", 1).is_ok());
assert!(manager.try_add_module("MYMOD", 2).is_none());
assert!(manager.try_add_module("OTHER", 1).is_some());
assert!(manager.register_module("", 1).is_err());
}
#[test]
fn id_ranges_match_csharp() {
assert_eq!(CUSTOM_RAW_STRING_COMMAND_MAX_ID, u16::MAX - 1);
assert_eq!(
CUSTOM_RAW_STRING_COMMAND_MAX_ID - CUSTOM_RAW_STRING_COMMAND_MIN_ID + 1,
MAX_CUSTOM_RAW_STRING_COMMANDS as u16
);
assert_eq!(CUSTOM_OBJECT_TYPE_MIN_ID, 0x40);
assert_eq!(GarnetObjectType::All as u8, 0xfb);
}
}