use super::{
cmd_strings as cs,
cmd_strings::{
abort_with_error_message, abort_with_wrong_number_of_arguments, write_error_raw, write_raw,
},
parser::{
resp_ext::{RespSliceExt, RespVecExt},
session_parse_state::strict_i32,
},
resp_server_session::RespServerSession,
};
const MAX_DATABASES: i64 = 16;
const CLUSTER_ENABLED: bool = false;
const PROTECTION_OPTION: ConnectionProtection = ConnectionProtection::No;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ConnectionProtection {
No,
}
impl ConnectionProtection {
const fn can_run(self) -> bool {
false
}
}
impl RespServerSession {
pub fn process_admin_commands<'a, D: wdev::Device>(
&mut self,
_parse_state: &[&[u8]],
_store: &wkv::BatchStoreSession<'a, D>,
output: &mut Vec<u8>,
) -> wresp::Result<bool> {
let _ = output;
Ok(true)
}
pub fn check_script_permissions<'a, D: wdev::Device>(
&mut self,
_parse_state: &[&[u8]],
_store: &wkv::BatchStoreSession<'a, D>,
output: &mut Vec<u8>,
) -> wresp::Result<bool> {
let _ = output;
Ok(true)
}
pub fn check_acl_permissions<'a, D: wdev::Device>(
&mut self,
_parse_state: &[&[u8]],
_store: &wkv::BatchStoreSession<'a, D>,
output: &mut Vec<u8>,
) -> wresp::Result<bool> {
let _ = output;
Ok(true)
}
pub fn check_acl_permissions_for_custom_command<'a, D: wdev::Device>(
&mut self,
_parse_state: &[&[u8]],
_store: &wkv::BatchStoreSession<'a, D>,
output: &mut Vec<u8>,
) -> wresp::Result<bool> {
let _ = output;
Ok(true)
}
pub fn on_acl_or_no_script_failure<'a, D: wdev::Device>(
&mut self,
_parse_state: &[&[u8]],
_store: &wkv::BatchStoreSession<'a, D>,
output: &mut Vec<u8>,
) -> wresp::Result<bool> {
let _ = output;
Ok(true)
}
pub fn commit_aof_async<'a, D: wdev::Device>(
&mut self,
_parse_state: &[&[u8]],
_store: &wkv::BatchStoreSession<'a, D>,
output: &mut Vec<u8>,
) -> wresp::Result<bool> {
let _ = output;
Ok(true)
}
pub fn network_monitor<'a, D: wdev::Device>(
&mut self,
parse_state: &[&[u8]],
_store: &wkv::BatchStoreSession<'a, D>,
output: &mut Vec<u8>,
) -> wresp::Result<bool> {
if !parse_state.is_empty() {
abort_with_wrong_number_of_arguments(output, "MONITOR");
return Ok(true);
}
write_error_raw(output, cs::RESP_ERR_GENERIC_UNK_CMD);
Ok(true)
}
pub fn try_import_commands_data<'a, D: wdev::Device>(
&mut self,
_parse_state: &[&[u8]],
_store: &wkv::BatchStoreSession<'a, D>,
output: &mut Vec<u8>,
) -> wresp::Result<bool> {
let _ = output;
Ok(false)
}
pub fn try_register_custom_commands<'a, D: wdev::Device>(
&mut self,
_parse_state: &[&[u8]],
_store: &wkv::BatchStoreSession<'a, D>,
output: &mut Vec<u8>,
) -> wresp::Result<bool> {
let _ = output;
Ok(false)
}
pub fn network_register_cs<'a, D: wdev::Device>(
&mut self,
parse_state: &[&[u8]],
_store: &wkv::BatchStoreSession<'a, D>,
output: &mut Vec<u8>,
) -> wresp::Result<bool> {
if parse_state.len() < 6 {
abort_with_wrong_number_of_arguments(output, "REGISTERCS");
return Ok(true);
}
if !PROTECTION_OPTION.can_run() {
abort_with_error_message(
output,
&cs::GENERIC_ERR_COMMAND_DISALLOWED_WITH_OPTION
.replace("{0}", "REGISTERCS")
.replace("{1}", "enable-module-command"),
);
return Ok(true);
}
abort_with_error_message(output, cs::RESP_ERR_GENERIC_INSTANTIATING_CLASS);
Ok(true)
}
pub fn network_module_load<'a, D: wdev::Device>(
&mut self,
parse_state: &[&[u8]],
_store: &wkv::BatchStoreSession<'a, D>,
output: &mut Vec<u8>,
) -> wresp::Result<bool> {
if parse_state.is_empty() {
abort_with_wrong_number_of_arguments(output, "MODULE|LOADCS");
return Ok(true);
}
if !PROTECTION_OPTION.can_run() {
abort_with_error_message(
output,
&cs::GENERIC_ERR_COMMAND_DISALLOWED_WITH_OPTION
.replace("{0}", "MODULE")
.replace("{1}", "enable-module-command"),
);
return Ok(true);
}
abort_with_error_message(output, cs::RESP_ERR_GENERIC_INSTANTIATING_CLASS);
Ok(true)
}
pub fn network_commitaof<'a, D: wdev::Device>(
&mut self,
parse_state: &[&[u8]],
store: &wkv::BatchStoreSession<'a, D>,
output: &mut Vec<u8>,
) -> wresp::Result<bool> {
if parse_state.len() > 1 {
abort_with_wrong_number_of_arguments(output, "COMMITAOF");
return Ok(true);
}
if parse_state.len() == 1 && !self.try_parse_database_id(parse_state, store, output)? {
return Ok(true);
}
output.write_resp_error("generic error");
Ok(true)
}
pub fn network_hcollect<'a, D: wdev::Device>(
&mut self,
parse_state: &[&[u8]],
_store: &wkv::BatchStoreSession<'a, D>,
output: &mut Vec<u8>,
) -> wresp::Result<bool> {
if parse_state.is_empty() {
abort_with_wrong_number_of_arguments(output, "HCOLLECT");
return Ok(true);
}
write_error_raw(output, cs::RESP_ERR_HCOLLECT_ALREADY_IN_PROGRESS);
Ok(true)
}
pub fn network_zcollect<'a, D: wdev::Device>(
&mut self,
parse_state: &[&[u8]],
_store: &wkv::BatchStoreSession<'a, D>,
output: &mut Vec<u8>,
) -> wresp::Result<bool> {
if parse_state.is_empty() {
abort_with_wrong_number_of_arguments(output, "ZCOLLECT");
return Ok(true);
}
write_error_raw(output, cs::RESP_ERR_ZCOLLECT_ALREADY_IN_PROGRESS);
Ok(true)
}
pub fn network_process_cluster_command<'a, D: wdev::Device>(
&mut self,
_parse_state: &[&[u8]],
_store: &wkv::BatchStoreSession<'a, D>,
output: &mut Vec<u8>,
) -> wresp::Result<bool> {
abort_with_error_message(output, cs::RESP_ERR_GENERIC_CLUSTER_DISABLED);
Ok(true)
}
pub fn network_debug(
&mut self,
parse_state: &[&[u8]],
output: &mut Vec<u8>,
) -> wresp::Result<bool> {
if parse_state.is_empty() {
abort_with_wrong_number_of_arguments(output, "DEBUG");
return Ok(true);
}
if !PROTECTION_OPTION.can_run() {
abort_with_error_message(
output,
&cs::GENERIC_ERR_COMMAND_DISALLOWED_WITH_OPTION
.replace("{0}", "DEBUG")
.replace("{1}", "enable-debug-command"),
);
return Ok(true);
}
let command = parse_state[0];
if command.eq_ignore_ascii_case(b"PANIC") {
output.write_resp_error("generic error");
return Ok(true);
}
if command.eq_ignore_ascii_case(b"ERROR") {
if parse_state.len() != 2 {
return self.abort_with_wrong_number_of_arguments_or_unknown_subcommand(
command.as_str_safe(),
"DEBUG",
output,
);
}
write_error_raw(output, parse_state[1].as_str_safe());
return Ok(true);
}
if command.eq_ignore_ascii_case(b"LOG") {
if parse_state.len() != 2 {
return self.abort_with_wrong_number_of_arguments_or_unknown_subcommand(
command.as_str_safe(),
"DEBUG",
output,
);
}
let _ = parse_state[1].as_str_safe();
write_raw(output, cs::RESP_OK);
return Ok(true);
}
if command.eq_ignore_ascii_case(b"FLUSHANDEVICT") {
if parse_state.len() != 1 {
return self.abort_with_wrong_number_of_arguments_or_unknown_subcommand(
command.as_str_safe(),
"DEBUG",
output,
);
}
output.write_resp_error("generic error");
return Ok(true);
}
if command.eq_ignore_ascii_case(b"FORCEGC") {
if parse_state.len() > 2 {
return self.abort_with_wrong_number_of_arguments_or_unknown_subcommand(
command.as_str_safe(),
"DEBUG",
output,
);
}
if parse_state.len() == 2 {
let Some(generation) = strict_i32(parse_state[1]) else {
abort_with_error_message(output, "ERR Invalid GC generation.");
return Ok(true);
};
if !(0..=2).contains(&generation) {
abort_with_error_message(output, "ERR Invalid GC generation.");
return Ok(true);
}
}
output.write_resp_simple_string("GC completed");
return Ok(true);
}
if command.eq_ignore_ascii_case(b"PURGEBP") {
if parse_state.len() != 2 {
return self.abort_with_wrong_number_of_arguments_or_unknown_subcommand(
command.as_str_safe(),
"DEBUG",
output,
);
}
output.write_resp_error("generic error");
return Ok(true);
}
if command.eq_ignore_ascii_case(b"HELP") {
const DEBUG_HELP: [&str; 18] = [
"DEBUG <subcommand> [<arg> [value] [opt] ...]. Subcommands are:",
"ERROR <string>",
"\tReturn a Redis protocol error with <string> as message. Useful for clients",
"\tunit tests to simulate Redis errors.",
"LOG <message>",
"\tWrite <message> to the server log.",
"FLUSHANDEVICT",
"\tFlush the main store's in-memory log to disk and evict it (shifts HeadAddress to",
"\tTailAddress) so subsequent reads are served from disk.",
"FORCEGC [generation]",
"\tForce a blocking garbage collection of the given generation (default: max).",
"PURGEBP <manager-type>",
"\tPurge the network buffer pool for the given manager (MigrationManager,",
"\tReplicationManager, or ServerListener) and force a blocking GC.",
"PANIC",
"\tCrash the server simulating a panic.",
"HELP",
"\tPrints this help",
];
output.write_resp_array_len(DEBUG_HELP.len());
for line in DEBUG_HELP {
output.write_resp_simple_string(line);
}
return Ok(true);
}
let error_msg = cs::GENERIC_ERR_UNKNOWN_SUB_COMMAND
.replace("{0}", parse_state[0].as_str_safe())
.replace("{1}", "DEBUG");
write_error_raw(output, &error_msg);
Ok(true)
}
pub fn network_role(
&mut self,
parse_state: &[&[u8]],
output: &mut Vec<u8>,
) -> wresp::Result<bool> {
if !parse_state.is_empty() {
abort_with_wrong_number_of_arguments(output, "ROLE");
return Ok(true);
}
output.write_resp_array_len(3);
output.write_resp_bulk_string(b"master");
output.write_resp_int(0);
output.extend_from_slice(b"*0\r\n");
Ok(true)
}
pub fn network_save<'a, D: wdev::Device>(
&mut self,
parse_state: &[&[u8]],
store: &wkv::BatchStoreSession<'a, D>,
output: &mut Vec<u8>,
) -> wresp::Result<bool> {
if parse_state.len() > 1 {
abort_with_wrong_number_of_arguments(output, "SAVE");
return Ok(true);
}
if parse_state.len() == 1 && !self.try_parse_database_id(parse_state, store, output)? {
return Ok(true);
}
output.write_resp_error("generic error");
Ok(true)
}
pub fn network_expdelscan<'a, D: wdev::Device>(
&mut self,
parse_state: &[&[u8]],
store: &wkv::BatchStoreSession<'a, D>,
output: &mut Vec<u8>,
) -> wresp::Result<bool> {
if parse_state.len() > 1 {
abort_with_wrong_number_of_arguments(output, "EXPDELSCAN");
return Ok(true);
}
let mut db_args: [&[u8]; 1] = [&[]];
if !parse_state.is_empty() {
db_args[0] = parse_state[0];
if !self.try_parse_database_id(&db_args, store, output)? {
return Ok(true);
}
}
output.write_resp_error("generic error");
Ok(true)
}
pub fn network_lastsave<'a, D: wdev::Device>(
&mut self,
parse_state: &[&[u8]],
store: &wkv::BatchStoreSession<'a, D>,
output: &mut Vec<u8>,
) -> wresp::Result<bool> {
if parse_state.len() > 1 {
abort_with_wrong_number_of_arguments(output, "LASTSAVE");
return Ok(true);
}
if parse_state.len() == 1 && !self.try_parse_database_id(parse_state, store, output)? {
return Ok(true);
}
output.write_resp_error("generic error");
Ok(true)
}
pub fn network_bgsave<'a, D: wdev::Device>(
&mut self,
parse_state: &[&[u8]],
store: &wkv::BatchStoreSession<'a, D>,
output: &mut Vec<u8>,
) -> wresp::Result<bool> {
if parse_state.len() > 2 {
abort_with_wrong_number_of_arguments(output, "BGSAVE");
return Ok(true);
}
let mut token_idx = 0usize;
if !parse_state.is_empty() && parse_state[0].eq_ignore_ascii_case(b"SCHEDULE") {
token_idx = 1;
}
if parse_state.len() > token_idx {
let db_args: [&[u8]; 1] = [parse_state[token_idx]];
if !self.try_parse_database_id(&db_args, store, output)? {
return Ok(true);
}
}
output.write_resp_error("generic error");
Ok(true)
}
pub fn try_parse_database_id<'a, D: wdev::Device>(
&mut self,
parse_state: &[&[u8]],
_store: &wkv::BatchStoreSession<'a, D>,
output: &mut Vec<u8>,
) -> wresp::Result<bool> {
let Some(db_id) = strict_i32(parse_state[0]) else {
abort_with_error_message(output, cs::RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER);
return Ok(false);
};
let db_id = i64::from(db_id);
if CLUSTER_ENABLED && db_id > 0 {
abort_with_error_message(output, cs::RESP_ERR_DB_ID_CLUSTER_MODE);
return Ok(false);
}
if !(0..MAX_DATABASES).contains(&db_id) {
abort_with_error_message(output, cs::RESP_ERR_DB_INDEX_OUT_OF_RANGE);
return Ok(false);
}
Ok(true)
}
fn abort_with_wrong_number_of_arguments_or_unknown_subcommand(
&mut self,
sub_command: &str,
cmd_name: &str,
output: &mut Vec<u8>,
) -> wresp::Result<bool> {
let error_msg = cs::GENERIC_ERR_UNKNOWN_SUB_COMMAND_OR_WRONG_NUM_ARGS
.replace("{0}", sub_command)
.replace("{1}", cmd_name);
write_error_raw(output, &error_msg);
Ok(true)
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use compio::runtime::Runtime;
use wdev::SegmentedDevice;
use wkv::{StoreConfig, WedbStore};
use super::*;
type Batch<'a> = wkv::BatchStoreSession<'a, SegmentedDevice>;
fn with_batch(f: impl FnOnce(&mut RespServerSession, &Batch)) {
let rt = Runtime::new().unwrap();
rt.block_on(async {
let dir = tempfile::tempdir().unwrap();
let device = Arc::new(SegmentedDevice::single_file(dir.path().join("admin.db")).unwrap());
let mut config = StoreConfig::new(1024, 4096, 16, 0.5).unwrap();
config.gc.enabled = false;
let store = Arc::new(WedbStore::open(config, device).unwrap());
let session = store.new_session().unwrap();
let batch = session.enter_batch();
let mut s = RespServerSession::default();
f(&mut s, &batch);
});
}
#[test]
fn monitor_frames_unk_cmd_like_csharp() {
with_batch(|s, batch| {
let mut out = Vec::new();
let _ = s.network_monitor(&[], batch, &mut out).unwrap();
assert_eq!(out, b"-ERR unknown command\r\n");
let mut out = Vec::new();
let _ = s.network_monitor(&[b"x"], batch, &mut out).unwrap();
assert_eq!(
out,
b"-ERR wrong number of arguments for 'MONITOR' command\r\n"
);
});
}
#[test]
fn debug_gated_by_protection_option() {
with_batch(|s, _batch| {
let mut out = Vec::new();
let _ = s.network_debug(&[], &mut out).unwrap();
assert_eq!(
out,
b"-ERR wrong number of arguments for 'DEBUG' command\r\n"
);
let mut out = Vec::new();
let _ = s.network_debug(&[b"HELP"], &mut out).unwrap();
assert_eq!(
out,
&b"-ERR DEBUG command not allowed. If the enable-debug-command option is set to \"local\", you can run it from a local connection, otherwise you need to set this option in the configuration file, and then restart the server.\r\n"[..]
);
});
}
#[test]
fn role_standalone_frame() {
with_batch(|s, _batch| {
let mut out = Vec::new();
let _ = s.network_role(&[], &mut out).unwrap();
assert_eq!(out, b"*3\r\n$6\r\nmaster\r\n:0\r\n*0\r\n");
let mut out = Vec::new();
let _ = s.network_role(&[b"x"], &mut out).unwrap();
assert_eq!(
out,
b"-ERR wrong number of arguments for 'ROLE' command\r\n"
);
});
}
#[test]
fn cluster_command_reports_disabled() {
with_batch(|s, batch| {
let mut out = Vec::new();
let _ = s
.network_process_cluster_command(&[b"MEET", b"h", b"1"], batch, &mut out)
.unwrap();
assert_eq!(out, b"-ERR This instance has cluster support disabled\r\n");
});
}
#[test]
fn registercs_and_module_load_gated() {
with_batch(|s, batch| {
let mut out = Vec::new();
let _ = s.network_register_cs(&[b"READ"], batch, &mut out).unwrap();
assert_eq!(
out,
b"-ERR wrong number of arguments for 'REGISTERCS' command\r\n"
);
let mut out = Vec::new();
let _ = s
.network_register_cs(
&[b"READ", b"c", b"1", b"Cls", b"SRC", b"p"],
batch,
&mut out,
)
.unwrap();
assert_eq!(
out,
&b"-ERR REGISTERCS command not allowed. If the enable-module-command option is set to \"local\", you can run it from a local connection, otherwise you need to set this option in the configuration file, and then restart the server.\r\n"[..]
);
let mut out = Vec::new();
let _ = s.network_module_load(&[], batch, &mut out).unwrap();
assert_eq!(
out,
b"-ERR wrong number of arguments for 'MODULE|LOADCS' command\r\n"
);
let mut out = Vec::new();
let _ = s.network_module_load(&[b"path"], batch, &mut out).unwrap();
assert_eq!(
out,
&b"-ERR MODULE command not allowed. If the enable-module-command option is set to \"local\", you can run it from a local connection, otherwise you need to set this option in the configuration file, and then restart the server.\r\n"[..]
);
});
}
#[test]
fn hcollect_zcollect_validation() {
with_batch(|s, batch| {
let mut out = Vec::new();
let _ = s.network_hcollect(&[], batch, &mut out).unwrap();
assert_eq!(
out,
b"-ERR wrong number of arguments for 'HCOLLECT' command\r\n"
);
let mut out = Vec::new();
let _ = s.network_hcollect(&[b"k"], batch, &mut out).unwrap();
assert_eq!(out, b"-ERR HCOLLECT scan already in progress\r\n");
let mut out = Vec::new();
let _ = s.network_zcollect(&[], batch, &mut out).unwrap();
assert_eq!(
out,
b"-ERR wrong number of arguments for 'ZCOLLECT' command\r\n"
);
let mut out = Vec::new();
let _ = s.network_zcollect(&[b"k"], batch, &mut out).unwrap();
assert_eq!(out, b"-ERR ZCOLLECT scan already in progress\r\n");
});
}
#[test]
fn try_parse_database_id_validation() {
with_batch(|s, batch| {
let mut out = Vec::new();
let ok = s.try_parse_database_id(&[b"abc"], batch, &mut out).unwrap();
assert!(!ok);
assert_eq!(out, b"-ERR value is not an integer or out of range.\r\n");
let mut out = Vec::new();
let ok = s.try_parse_database_id(&[b"16"], batch, &mut out).unwrap();
assert!(!ok);
assert_eq!(out, b"-ERR DB index is out of range.\r\n");
let mut out = Vec::new();
let ok = s.try_parse_database_id(&[b"-1"], batch, &mut out).unwrap();
assert!(!ok);
assert_eq!(out, b"-ERR DB index is out of range.\r\n");
let mut out = Vec::new();
let ok = s.try_parse_database_id(&[b"3"], batch, &mut out).unwrap();
assert!(ok);
assert!(out.is_empty());
});
}
#[test]
fn save_bgsave_lastsave_commitdb_validation_then_gap() {
with_batch(|s, batch| {
let mut out = Vec::new();
let _ = s.network_save(&[b"1", b"2"], batch, &mut out).unwrap();
assert_eq!(
out,
b"-ERR wrong number of arguments for 'SAVE' command\r\n"
);
let mut out = Vec::new();
let _ = s
.network_bgsave(&[b"SCHEDULE", b"99"], batch, &mut out)
.unwrap();
assert_eq!(out, b"-ERR DB index is out of range.\r\n");
let mut out = Vec::new();
let _ = s.network_lastsave(&[b"x"], batch, &mut out).unwrap();
assert_eq!(out, b"-ERR value is not an integer or out of range.\r\n");
let mut out = Vec::new();
let _ = s.network_commitaof(&[b"a", b"b"], batch, &mut out).unwrap();
assert_eq!(
out,
b"-ERR wrong number of arguments for 'COMMITAOF' command\r\n"
);
let mut out = Vec::new();
let _ = s
.network_expdelscan(&[b"1", b"2"], batch, &mut out)
.unwrap();
assert_eq!(
out,
b"-ERR wrong number of arguments for 'EXPDELSCAN' command\r\n"
);
let mut out = Vec::new();
let _ = s.network_save(&[], batch, &mut out).unwrap();
assert_eq!(out, b"-ERR generic error\r\n");
let mut out = Vec::new();
let _ = s.network_bgsave(&[b"SCHEDULE"], batch, &mut out).unwrap();
assert_eq!(out, b"-ERR generic error\r\n");
let mut out = Vec::new();
let _ = s.network_commitaof(&[], batch, &mut out).unwrap();
assert_eq!(out, b"-ERR generic error\r\n");
let mut out = Vec::new();
let _ = s.network_lastsave(&[], batch, &mut out).unwrap();
assert_eq!(out, b"-ERR generic error\r\n");
let mut out = Vec::new();
let _ = s.network_expdelscan(&[], batch, &mut out).unwrap();
assert_eq!(out, b"-ERR generic error\r\n");
});
}
#[test]
fn acl_and_script_helpers_allow_by_default() {
with_batch(|s, batch| {
let mut out = Vec::new();
let _ = s.check_acl_permissions(&[], batch, &mut out).unwrap();
assert!(out.is_empty());
let _ = s.check_script_permissions(&[], batch, &mut out).unwrap();
assert!(out.is_empty());
let _ = s.on_acl_or_no_script_failure(&[], batch, &mut out).unwrap();
assert!(out.is_empty());
});
}
}