use kevy_config::Config;
use kevy_resp::{ArgvView, encode_array_len, encode_bulk, encode_error, encode_simple_string};
use super::{appendfsync_str, eviction_str, log_level_str, wrong_args};
pub(super) fn cmd_config<A: ArgvView + ?Sized>(cfg: &Config, args: &A, out: &mut Vec<u8>) {
let sub = match args.get(1) {
Some(s) => s.to_ascii_uppercase(),
None => return wrong_args(out, "config"),
};
match sub.as_slice() {
b"GET" => cmd_config_get(cfg, args, out),
b"SET" => cmd_config_set(args, out),
b"REWRITE" => cmd_config_rewrite(out),
b"RESETSTAT" => encode_simple_string(out, "OK"),
_ => encode_error(
out,
&format!(
"ERR unknown CONFIG subcommand '{}'",
String::from_utf8_lossy(args.get(1).unwrap_or(&[][..]))
),
),
}
}
fn cmd_config_get<A: ArgvView + ?Sized>(cfg: &Config, args: &A, out: &mut Vec<u8>) {
if args.len() < 3 {
return wrong_args(out, "config|get");
}
let mut hits: Vec<(&'static str, String)> = Vec::new();
for i in 2..args.len() {
let pat = args[i].to_ascii_lowercase();
for (key, val) in config_pairs(cfg) {
if glob_match(&pat, key.as_bytes()) && !hits.iter().any(|(k, _)| *k == key) {
hits.push((key, val));
}
}
}
encode_array_len(out, (hits.len() * 2) as i64);
for (k, v) in hits {
encode_bulk(out, k.as_bytes());
encode_bulk(out, v.as_bytes());
}
}
fn cmd_config_set<A: ArgvView + ?Sized>(args: &A, out: &mut Vec<u8>) {
if args.len() != 4 {
return wrong_args(out, "config|set");
}
encode_error(
out,
"ERR CONFIG SET is read-only in kevy v1.0 — edit kevy.toml and \
restart. Hot-settable in v1.x Wave 2 (maxmemory, maxmemory-policy, \
appendfsync, hz, sample, log-level)",
);
}
fn cmd_config_rewrite(out: &mut Vec<u8>) {
encode_error(
out,
"ERR CONFIG REWRITE not yet supported — kevy v1.0's TOML file is \
user-owned; lands alongside CONFIG SET in v1.x Wave 2",
);
}
fn config_pairs(cfg: &Config) -> Vec<(&'static str, String)> {
let mut v: Vec<(&'static str, String)> = Vec::new();
let [a, b, c, d] = cfg.server.bind;
v.push(("bind", format!("{a}.{b}.{c}.{d}")));
v.push(("port", cfg.server.port.to_string()));
v.push(("io-threads", cfg.server.threads.to_string()));
v.push(("dir", cfg.server.data_dir.display().to_string()));
v.push(("appendonly", yes_no(cfg.persistence.aof)));
v.push((
"appendfsync",
appendfsync_str(cfg.persistence.appendfsync).to_string(),
));
v.push((
"auto-aof-rewrite-percentage",
cfg.persistence.auto_aof_rewrite_percentage.to_string(),
));
v.push((
"auto-aof-rewrite-min-size",
cfg.persistence.auto_aof_rewrite_min_size.to_string(),
));
v.push(("maxmemory", cfg.memory.maxmemory.to_string()));
v.push((
"maxmemory-policy",
eviction_str(cfg.memory.maxmemory_policy).to_string(),
));
v.push(("hz", cfg.expiry.hz.to_string()));
v.push(("maxmemory-samples", cfg.expiry.sample.to_string()));
v.push(("loglevel", log_level_str(cfg.log.level).to_string()));
v
}
fn glob_match(pat: &[u8], s: &[u8]) -> bool {
fn go(pat: &[u8], s: &[u8]) -> bool {
match pat.split_first() {
None => s.is_empty(),
Some((&b'*', rest)) => {
if rest.is_empty() {
return true;
}
(0..=s.len()).any(|i| go(rest, &s[i..]))
}
Some((&c, rest)) => match s.split_first() {
Some((&first, srest)) if first == c => go(rest, srest),
_ => false,
},
}
}
go(pat, s)
}
fn yes_no(b: bool) -> String {
if b { "yes".into() } else { "no".into() }
}
#[cfg(test)]
mod tests {
use super::*;
use kevy_resp::Argv;
fn run(verb: &[u8], rest: &[&[u8]]) -> Vec<u8> {
let mut a = Argv::default();
a.push(verb);
for r in rest {
a.push(r);
}
let mut out = Vec::new();
let cfg = Config::default();
cmd_config(&cfg, &a, &mut out);
let _ = verb;
out
}
#[test]
fn glob_matcher_handles_star_and_literal() {
assert!(glob_match(b"*", b"anything"));
assert!(glob_match(b"max*", b"maxmemory"));
assert!(glob_match(b"max*", b"maxmemory-policy"));
assert!(glob_match(b"*memory*", b"maxmemory-policy"));
assert!(glob_match(b"port", b"port"));
assert!(!glob_match(b"port", b"ports"));
assert!(!glob_match(b"max*", b"min-memory"));
assert!(glob_match(b"", b""));
assert!(!glob_match(b"", b"x"));
}
#[test]
fn config_get_exact_key() {
let out = run(b"CONFIG", &[b"GET", b"port"]);
let s = String::from_utf8(out).unwrap();
assert!(s.starts_with("*2\r\n"), "got: {s:?}");
assert!(s.contains("port"));
assert!(s.contains("6004")); }
#[test]
fn config_get_glob() {
let out = run(b"CONFIG", &[b"GET", b"max*"]);
let s = String::from_utf8(out).unwrap();
assert!(s.starts_with("*6\r\n"), "got: {s:?}");
assert!(s.contains("maxmemory"));
assert!(s.contains("maxmemory-policy"));
}
#[test]
fn config_get_unknown_key_returns_empty_array() {
let out = run(b"CONFIG", &[b"GET", b"nonexistent-setting"]);
assert_eq!(out, b"*0\r\n");
}
#[test]
fn config_get_multiple_patterns() {
let out = run(b"CONFIG", &[b"GET", b"port", b"bind"]);
let s = String::from_utf8(out).unwrap();
assert!(s.starts_with("*4\r\n"));
assert!(s.contains("port"));
assert!(s.contains("bind"));
}
#[test]
fn config_set_returns_useful_error() {
let out = run(b"CONFIG", &[b"SET", b"maxmemory", b"2gb"]);
let s = String::from_utf8(out).unwrap();
assert!(s.starts_with("-ERR"));
assert!(s.contains("read-only in kevy v1.0"));
}
#[test]
fn config_rewrite_returns_useful_error() {
let out = run(b"CONFIG", &[b"REWRITE"]);
let s = String::from_utf8(out).unwrap();
assert!(s.starts_with("-ERR"));
assert!(s.contains("REWRITE"));
}
#[test]
fn config_unknown_subcommand_errors() {
let out = run(b"CONFIG", &[b"NUKE"]);
let s = String::from_utf8(out).unwrap();
assert!(s.starts_with("-ERR"));
assert!(s.contains("unknown CONFIG subcommand"));
}
}