use crate::server::memcached::protocol::{MAX_KEY_LEN, MAX_VALUE_LEN};
use serde_json::{json, Map, Value};
pub const MAX_REPLY_LINE: usize = 2048;
pub const MAX_STATS_ENTRIES: usize = 4096;
pub const MAX_RESPONSE_BYTES: usize = 4 * 1024 * 1024;
pub const MAX_GET_KEYS: usize = 32;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StoreVerb {
Set,
Add,
Replace,
Append,
Prepend,
Cas,
}
impl StoreVerb {
pub fn as_str(self) -> &'static str {
match self {
StoreVerb::Set => "set",
StoreVerb::Add => "add",
StoreVerb::Replace => "replace",
StoreVerb::Append => "append",
StoreVerb::Prepend => "prepend",
StoreVerb::Cas => "cas",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Request {
Get {
keys: Vec<String>,
with_cas: bool,
},
Store {
verb: StoreVerb,
key: String,
flags: u32,
exptime: i64,
value: String,
cas_unique: Option<u64>,
},
Delete {
key: String,
},
Incr {
key: String,
delta: u64,
},
Decr {
key: String,
delta: u64,
},
Touch {
key: String,
exptime: i64,
},
Stats {
group: Option<String>,
},
Version,
FlushAll {
delay: Option<u64>,
},
}
impl Request {
pub fn command(&self) -> &'static str {
match self {
Request::Get {
with_cas: false, ..
} => "get",
Request::Get { with_cas: true, .. } => "gets",
Request::Store { verb, .. } => verb.as_str(),
Request::Delete { .. } => "delete",
Request::Incr { .. } => "incr",
Request::Decr { .. } => "decr",
Request::Touch { .. } => "touch",
Request::Stats { .. } => "stats",
Request::Version => "version",
Request::FlushAll { .. } => "flush_all",
}
}
pub fn encode(&self) -> Vec<u8> {
let line = match self {
Request::Get { keys, .. } => format!("{} {}", self.command(), keys.join(" ")),
Request::Store {
verb,
key,
flags,
exptime,
value,
cas_unique,
} => {
let mut head = format!(
"{} {} {} {} {}",
verb.as_str(),
key,
flags,
exptime,
value.len()
);
if let Some(cas) = cas_unique {
head.push_str(&format!(" {cas}"));
}
let mut out = head.into_bytes();
out.extend_from_slice(b"\r\n");
out.extend_from_slice(value.as_bytes());
out.extend_from_slice(b"\r\n");
return out;
}
Request::Delete { key } => format!("delete {key}"),
Request::Incr { key, delta } => format!("incr {key} {delta}"),
Request::Decr { key, delta } => format!("decr {key} {delta}"),
Request::Touch { key, exptime } => format!("touch {key} {exptime}"),
Request::Stats { group: None } => "stats".to_string(),
Request::Stats { group: Some(g) } => format!("stats {g}"),
Request::Version => "version".to_string(),
Request::FlushAll { delay: None } => "flush_all".to_string(),
Request::FlushAll { delay: Some(d) } => format!("flush_all {d}"),
};
let mut out = line.into_bytes();
out.extend_from_slice(b"\r\n");
out
}
pub fn expect(&self) -> Expect {
let command = self.command();
match self {
Request::Get { keys, with_cas } => Expect::Values {
keys: keys.clone(),
with_cas: *with_cas,
},
Request::Store { key, .. }
| Request::Delete { key }
| Request::Incr { key, .. }
| Request::Decr { key, .. }
| Request::Touch { key, .. } => Expect::Status {
command,
key: Some(key.clone()),
},
Request::Stats { group } => Expect::Stats {
group: group.clone(),
},
Request::Version | Request::FlushAll { .. } => Expect::Status { command, key: None },
}
}
}
pub fn validate_key(key: &str) -> Result<(), String> {
if key.is_empty() {
return Err("a memcached key cannot be empty".to_string());
}
if key.len() > MAX_KEY_LEN {
return Err(format!(
"key is {} bytes; memcached refuses keys longer than {MAX_KEY_LEN}",
key.len()
));
}
if key.bytes().any(|b| b <= b' ' || b == 0x7f) {
return Err(format!(
"key {key:?} contains a space or a control character; a memcached key is one \
token on the command line"
));
}
Ok(())
}
pub fn validate_stats_group(group: &str) -> Result<(), String> {
if group.is_empty() || group.len() > 32 || !group.bytes().all(|b| b.is_ascii_lowercase()) {
return Err(format!(
"stats group {group:?} is not one lowercase word such as settings, items or slabs"
));
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Expect {
Values { keys: Vec<String>, with_cas: bool },
Status {
command: &'static str,
key: Option<String>,
},
Stats { group: Option<String> },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Item {
pub key: String,
pub flags: u32,
pub cas: Option<u64>,
pub data: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Reply {
Values {
items: Vec<Item>,
misses: Vec<String>,
},
Status { line: String },
Stats { entries: Vec<(String, String)> },
Error { kind: &'static str, message: String },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WireError {
LineTooLong,
ValueTooLarge { key: String, declared: u64 },
UnexpectedLine(String),
UnrequestedKey(String),
TooManyStats,
BadValueTerminator,
ResponseTooLarge,
}
impl std::fmt::Display for WireError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
WireError::LineTooLong => write!(
f,
"server sent more than {MAX_REPLY_LINE} octets without a CRLF"
),
WireError::ValueTooLarge { key, declared } => write!(
f,
"VALUE for {key:?} declares {declared} octets, over the {MAX_VALUE_LEN}-octet \
item limit this client reads"
),
WireError::UnexpectedLine(line) => write!(
f,
"reply line {:?} is not a valid answer to the request in flight",
crate::utils::truncate::truncate_for_log(line, 80)
),
WireError::UnrequestedKey(key) => {
write!(
f,
"server returned a VALUE for {key:?}, which was not requested"
)
}
WireError::TooManyStats => {
write!(f, "stats response exceeded {MAX_STATS_ENTRIES} STAT lines")
}
WireError::BadValueTerminator => {
write!(f, "a VALUE data block was not followed by CRLF")
}
WireError::ResponseTooLarge => write!(
f,
"one response exceeded {MAX_RESPONSE_BYTES} octets before it was complete"
),
}
}
}
impl std::error::Error for WireError {}
fn line_at(buf: &[u8], at: usize) -> Result<Option<(&str, usize)>, WireError> {
let rest = &buf[at..];
let window = &rest[..rest.len().min(MAX_REPLY_LINE + 2)];
match window.windows(2).position(|w| w == b"\r\n") {
Some(end) => {
let line = std::str::from_utf8(&rest[..end]).map_err(|_| {
WireError::UnexpectedLine(String::from_utf8_lossy(&rest[..end]).into())
})?;
Ok(Some((line, at + end + 2)))
}
None if rest.len() > MAX_REPLY_LINE => Err(WireError::LineTooLong),
None => Ok(None),
}
}
fn error_line(line: &str) -> Option<Reply> {
if line == "ERROR" {
return Some(Reply::Error {
kind: "error",
message: "the server did not recognise the command".to_string(),
});
}
if let Some(msg) = line.strip_prefix("CLIENT_ERROR") {
return Some(Reply::Error {
kind: "client_error",
message: msg.trim().to_string(),
});
}
if let Some(msg) = line.strip_prefix("SERVER_ERROR") {
return Some(Reply::Error {
kind: "server_error",
message: msg.trim().to_string(),
});
}
None
}
pub fn parse_reply(buf: &[u8], expect: &Expect) -> Result<Option<(Reply, usize)>, WireError> {
let Some((first, mut pos)) = line_at(buf, 0)? else {
return Ok(None);
};
if let Some(err) = error_line(first) {
return Ok(Some((err, pos)));
}
match expect {
Expect::Status { .. } => {
if first.starts_with("VALUE ") || first.starts_with("STAT ") || first == "END" {
return Err(WireError::UnexpectedLine(first.to_string()));
}
Ok(Some((
Reply::Status {
line: first.to_string(),
},
pos,
)))
}
Expect::Stats { .. } => {
let mut entries = Vec::new();
let mut line = first;
loop {
if line == "END" {
return Ok(Some((Reply::Stats { entries }, pos)));
}
let Some(rest) = line.strip_prefix("STAT ") else {
return Err(WireError::UnexpectedLine(line.to_string()));
};
if entries.len() >= MAX_STATS_ENTRIES {
return Err(WireError::TooManyStats);
}
if pos > MAX_RESPONSE_BYTES {
return Err(WireError::ResponseTooLarge);
}
let (name, value) = rest.split_once(' ').unwrap_or((rest, ""));
entries.push((name.to_string(), value.to_string()));
match line_at(buf, pos)? {
Some((next, next_pos)) => {
line = next;
pos = next_pos;
}
None => return Ok(None),
}
}
}
Expect::Values { keys, with_cas } => {
let mut items: Vec<Item> = Vec::new();
let mut line = first;
loop {
if line == "END" {
let misses = keys
.iter()
.filter(|k| !items.iter().any(|i| &i.key == *k))
.cloned()
.collect();
return Ok(Some((Reply::Values { items, misses }, pos)));
}
let Some(rest) = line.strip_prefix("VALUE ") else {
return Err(WireError::UnexpectedLine(line.to_string()));
};
let fields: Vec<&str> = rest.split(' ').collect();
let (key, flags, bytes, cas) = match (fields.as_slice(), with_cas) {
([k, f, b], false) => (*k, *f, *b, None),
([k, f, b, c], true) => (*k, *f, *b, Some(*c)),
_ => return Err(WireError::UnexpectedLine(line.to_string())),
};
if !keys.iter().any(|k| k == key) || items.iter().any(|i| i.key == key) {
return Err(WireError::UnrequestedKey(key.to_string()));
}
let bad = || WireError::UnexpectedLine(line.to_string());
let flags: u32 = flags.parse().map_err(|_| bad())?;
let declared: u64 = bytes.parse().map_err(|_| bad())?;
let cas = match cas {
Some(c) => Some(c.parse::<u64>().map_err(|_| bad())?),
None => None,
};
let len = usize::try_from(declared)
.ok()
.filter(|n| *n <= MAX_VALUE_LEN)
.ok_or_else(|| WireError::ValueTooLarge {
key: key.to_string(),
declared,
})?;
let key = key.to_string();
if pos + len + 2 > MAX_RESPONSE_BYTES {
return Err(WireError::ResponseTooLarge);
}
if buf.len() < pos + len + 2 {
return Ok(None);
}
if &buf[pos + len..pos + len + 2] != b"\r\n" {
return Err(WireError::BadValueTerminator);
}
items.push(Item {
key,
flags,
cas,
data: buf[pos..pos + len].to_vec(),
});
pos += len + 2;
match line_at(buf, pos)? {
Some((next, next_pos)) => {
line = next;
pos = next_pos;
}
None => return Ok(None),
}
}
}
}
}
pub fn stats_json(entries: &[(String, String)]) -> Value {
let mut map = Map::new();
for (k, v) in entries {
map.insert(k.clone(), json!(v));
}
Value::Object(map)
}