use std::str::from_utf8;
use super::{
basic_commands::try_get_client_name,
cmd_strings as cs,
cmd_strings::{abort_with_error_message, abort_with_wrong_number_of_arguments, write_raw},
parser::{resp_ext::RespVecExt, session_parse_state::strict_i64},
resp_server_session::RespServerSession,
};
impl RespServerSession {
pub fn network_clientlist<'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_CANNOT_LIST_CLIENTS);
Ok(true)
}
pub fn network_clientinfo<'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, "client|info");
return Ok(true);
}
let mut result = String::new();
self.write_client_info_state(&mut result);
result.push('\n');
output.write_resp_bulk_string(result.as_bytes());
Ok(true)
}
pub fn network_clientkill<'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_CANNOT_LIST_CLIENTS);
Ok(true)
}
pub fn network_clientgetname<'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, "CLIENT|GETNAME");
return Ok(true);
}
match self.client_name.as_deref() {
Some(name) if !name.is_empty() => output.write_resp_bulk_string(name.as_bytes()),
_ => output.write_resp_null(),
}
Ok(true)
}
pub fn network_clientsetname<'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, "CLIENT|SETNAME");
return Ok(true);
}
match try_get_client_name(parse_state[0]) {
Some(name) => {
self.set_client_name(Some(name));
write_raw(output, cs::RESP_OK);
}
None => abort_with_error_message(output, cs::RESP_ERR_INVALID_CLIENT_NAME),
}
Ok(true)
}
pub fn network_clientsetinfo<'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, "CLIENT|SETINFO");
return Ok(true);
}
let option = parse_state[0];
let value = parse_state[1];
let Ok(value) = from_utf8(value) else {
write_raw(output, cs::RESP_OK);
return Ok(true);
};
if option == b"LIB-NAME" || option == b"lib-name" {
self.client_lib_name = Some(value.to_string());
write_raw(output, cs::RESP_OK);
} else if option == b"LIB-VER" || option == b"lib-ver" {
self.client_lib_version = Some(value.to_string());
write_raw(output, cs::RESP_OK);
} else {
abort_with_error_message(output, cs::RESP_ERR_GENERIC_SYNTAX_ERROR);
}
Ok(true)
}
pub fn network_clientunblock<'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 && parse_state.len() != 2 {
abort_with_wrong_number_of_arguments(output, "client|unblock");
return Ok(true);
}
let Some(_client_id) = strict_i64(parse_state[0]) else {
abort_with_error_message(output, cs::RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER);
return Ok(true);
};
if parse_state.len() == 2 {
let option = parse_state[1];
if !option.eq_ignore_ascii_case(b"TIMEOUT") && !option.eq_ignore_ascii_case(b"ERROR") {
abort_with_error_message(output, cs::RESP_ERR_INVALID_CLIENT_UNBLOCK_REASON);
return Ok(true);
}
}
output.write_resp_int(0);
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("client.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 list_and_kill_report_unavailable() {
with_batch(|s, batch| {
let mut out = Vec::new();
let _ = s
.network_clientlist(&[b"GARBAGE"], batch, &mut out)
.unwrap();
assert_eq!(out, b"-ERR Clients cannot be listed.\r\n");
let mut out = Vec::new();
let _ = s.network_clientkill(&[], batch, &mut out).unwrap();
assert_eq!(out, b"-ERR Clients cannot be listed.\r\n");
});
}
#[test]
fn clientinfo_frame() {
with_batch(|s, batch| {
s.id = 42;
s.remote_endpoint = "127.0.0.1:9999".to_string();
s.set_client_lib_info(Some("redis-py"), Some("5.0.1"));
let mut out = Vec::new();
let _ = s.network_clientinfo(&[], batch, &mut out).unwrap();
let expected = "id=42 addr=127.0.0.1:9999 laddr= age=0 flags=N db=0 resp=2 lib-name=redis-py lib-ver=5.0.1\n";
let frame = format!("${}\r\n{expected}\r\n", expected.len());
assert_eq!(out, frame.as_bytes());
let mut out = Vec::new();
let _ = s.network_clientinfo(&[b"x"], batch, &mut out).unwrap();
assert_eq!(
out,
b"-ERR wrong number of arguments for 'client|info' command\r\n"
);
});
}
#[test]
fn getname_returns_session_name() {
with_batch(|s, batch| {
let mut out = Vec::new();
let _ = s.network_clientgetname(&[], batch, &mut out).unwrap();
assert_eq!(out, b"$-1\r\n");
let mut out = Vec::new();
let _ = s
.network_clientsetname(&[b"client-1"], batch, &mut out)
.unwrap();
assert_eq!(out, b"+OK\r\n");
let mut out = Vec::new();
let _ = s.network_clientgetname(&[], batch, &mut out).unwrap();
assert_eq!(out, b"$8\r\nclient-1\r\n");
let mut out = Vec::new();
let _ = s.network_clientsetname(&[b""], batch, &mut out).unwrap();
assert_eq!(out, b"+OK\r\n");
let mut out = Vec::new();
let _ = s.network_clientgetname(&[], batch, &mut out).unwrap();
assert_eq!(out, b"$-1\r\n");
let mut out = Vec::new();
let _ = s.network_clientgetname(&[b"x"], batch, &mut out).unwrap();
assert_eq!(
out,
b"-ERR wrong number of arguments for 'CLIENT|GETNAME' command\r\n"
);
});
}
#[test]
fn setname_validation() {
with_batch(|s, batch| {
let mut out = Vec::new();
let _ = s
.network_clientsetname(&[b"client-1"], batch, &mut out)
.unwrap();
assert_eq!(out, b"+OK\r\n");
let mut out = Vec::new();
let _ = s.network_clientsetname(&[b""], batch, &mut out).unwrap();
assert_eq!(out, b"+OK\r\n");
let mut out = Vec::new();
let _ = s.network_clientsetname(&[b"a b"], batch, &mut out).unwrap();
assert_eq!(
out,
b"-ERR Client names cannot contain spaces, newlines or special characters.\r\n"
);
let mut out = Vec::new();
let _ = s.network_clientsetname(&[], batch, &mut out).unwrap();
assert_eq!(
out,
b"-ERR wrong number of arguments for 'CLIENT|SETNAME' command\r\n"
);
});
}
#[test]
fn setinfo_exact_case_match_and_state() {
with_batch(|s, batch| {
for option in [b"LIB-NAME".as_slice(), b"lib-name".as_slice()] {
let mut out = Vec::new();
let _ = s
.network_clientsetinfo(&[option, b"phpredis"], batch, &mut out)
.unwrap();
assert_eq!(out, b"+OK\r\n");
}
assert_eq!(s.client_lib_name.as_deref(), Some("phpredis"));
let mut out = Vec::new();
let _ = s
.network_clientsetinfo(&[b"lib-ver", b"6.0.2".as_slice()], batch, &mut out)
.unwrap();
assert_eq!(out, b"+OK\r\n");
assert_eq!(s.client_lib_version.as_deref(), Some("6.0.2"));
for option in [
b"lib-nameX".as_slice(),
b"Lib-Name".as_slice(),
b"BAD".as_slice(),
] {
let mut out = Vec::new();
let _ = s
.network_clientsetinfo(&[option, b"v"], batch, &mut out)
.unwrap();
assert_eq!(out, b"-ERR syntax error\r\n");
}
let mut out = Vec::new();
let _ = s
.network_clientsetinfo(&[b"lib-ver"], batch, &mut out)
.unwrap();
assert_eq!(
out,
b"-ERR wrong number of arguments for 'CLIENT|SETINFO' command\r\n"
);
});
}
#[test]
fn unblock_validation() {
with_batch(|s, batch| {
let mut out = Vec::new();
let _ = s.network_clientunblock(&[b"123"], batch, &mut out).unwrap();
assert_eq!(out, b":0\r\n");
let mut out = Vec::new();
let _ = s
.network_clientunblock(&[b"123", b"error"], batch, &mut out)
.unwrap();
assert_eq!(out, b":0\r\n");
let mut out = Vec::new();
let _ = s.network_clientunblock(&[b"1x"], 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_clientunblock(&[b"0123"], 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_clientunblock(&[b"1", b"WHATEVER"], batch, &mut out)
.unwrap();
assert_eq!(
out,
b"-ERR CLIENT UNBLOCK reason should be TIMEOUT or ERROR\r\n"
);
let mut out = Vec::new();
let _ = s.network_clientunblock(&[], batch, &mut out).unwrap();
assert_eq!(
out,
b"-ERR wrong number of arguments for 'client|unblock' command\r\n"
);
let mut out = Vec::new();
let _ = s
.network_clientunblock(&[b"1", b"TIMEOUT", b"x"], batch, &mut out)
.unwrap();
assert_eq!(
out,
b"-ERR wrong number of arguments for 'client|unblock' command\r\n"
);
});
}
}