pub const MAX_COMMAND_LINE: usize = 8 * 1024;
pub const MAX_KEY_LEN: usize = 250;
pub const MAX_VALUE_LEN: usize = 1024 * 1024;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Command {
Retrieval {
command: &'static str,
keys: Vec<String>,
},
Storage {
command: &'static str,
key: String,
flags: u32,
exptime: i64,
bytes: usize,
cas_unique: Option<u64>,
noreply: bool,
data: Vec<u8>,
},
Delete { key: String, noreply: bool },
Arithmetic {
command: &'static str,
key: String,
delta: u64,
noreply: bool,
},
Touch {
key: String,
exptime: i64,
noreply: bool,
},
Stats { argument: Option<String> },
Version,
FlushAll { delay: i64, noreply: bool },
Quit,
Unknown { line: String },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParseError {
ClientError(String),
Incomplete,
}
#[derive(Debug)]
pub enum Parsed {
Complete { command: Command, consumed: usize },
Incomplete,
Invalid { message: String, consumed: usize },
Fatal { message: String, too_large: bool },
}
pub fn parse_command(buffer: &[u8]) -> Parsed {
let Some(line_end) = find_crlf(buffer) else {
if buffer.len() > MAX_COMMAND_LINE {
return Parsed::Invalid {
message: format!("command line exceeds {} bytes", MAX_COMMAND_LINE),
consumed: buffer.len(),
};
}
return Parsed::Incomplete;
};
let line_bytes = &buffer[..line_end];
let header_len = line_end + 2;
let line = match std::str::from_utf8(line_bytes) {
Ok(s) => s,
Err(_) => {
return Parsed::Invalid {
message: "command line is not valid UTF-8".to_string(),
consumed: header_len,
}
}
};
let parts: Vec<&str> = line.split_ascii_whitespace().collect();
if parts.is_empty() {
return Parsed::Invalid {
message: "empty command".to_string(),
consumed: header_len,
};
}
match parts[0] {
"get" | "gets" => {
let command = if parts[0] == "get" { "get" } else { "gets" };
if parts.len() < 2 {
return Parsed::Invalid {
message: format!("{} requires at least one key", command),
consumed: header_len,
};
}
let keys: Vec<String> = parts[1..].iter().map(|s| s.to_string()).collect();
if let Some(bad) = keys.iter().find(|k| k.len() > MAX_KEY_LEN) {
return Parsed::Invalid {
message: format!("key is {} bytes, maximum is {}", bad.len(), MAX_KEY_LEN),
consumed: header_len,
};
}
Parsed::Complete {
command: Command::Retrieval { command, keys },
consumed: header_len,
}
}
cmd @ ("set" | "add" | "replace" | "append" | "prepend" | "cas") => {
parse_storage(cmd, &parts, buffer, header_len)
}
"delete" => {
if parts.len() < 2 {
return Parsed::Invalid {
message: "delete requires a key".to_string(),
consumed: header_len,
};
}
Parsed::Complete {
command: Command::Delete {
key: parts[1].to_string(),
noreply: has_noreply(&parts),
},
consumed: header_len,
}
}
cmd @ ("incr" | "decr") => {
let command = if cmd == "incr" { "incr" } else { "decr" };
if parts.len() < 3 {
return Parsed::Invalid {
message: format!("{} requires a key and a value", command),
consumed: header_len,
};
}
let delta = match parts[2].parse::<u64>() {
Ok(d) => d,
Err(_) => {
return Parsed::Invalid {
message: "invalid numeric delta argument".to_string(),
consumed: header_len,
}
}
};
Parsed::Complete {
command: Command::Arithmetic {
command,
key: parts[1].to_string(),
delta,
noreply: has_noreply(&parts),
},
consumed: header_len,
}
}
"touch" => {
if parts.len() < 3 {
return Parsed::Invalid {
message: "touch requires a key and an expiry".to_string(),
consumed: header_len,
};
}
let exptime = match parts[2].parse::<i64>() {
Ok(v) => v,
Err(_) => {
return Parsed::Invalid {
message: "invalid exptime argument".to_string(),
consumed: header_len,
}
}
};
Parsed::Complete {
command: Command::Touch {
key: parts[1].to_string(),
exptime,
noreply: has_noreply(&parts),
},
consumed: header_len,
}
}
"stats" => Parsed::Complete {
command: Command::Stats {
argument: parts.get(1).map(|s| s.to_string()),
},
consumed: header_len,
},
"version" => Parsed::Complete {
command: Command::Version,
consumed: header_len,
},
"flush_all" => {
let delay = parts
.get(1)
.filter(|s| **s != "noreply")
.and_then(|s| s.parse::<i64>().ok())
.unwrap_or(0);
Parsed::Complete {
command: Command::FlushAll {
delay,
noreply: has_noreply(&parts),
},
consumed: header_len,
}
}
"quit" => Parsed::Complete {
command: Command::Quit,
consumed: header_len,
},
_ => Parsed::Complete {
command: Command::Unknown {
line: line.to_string(),
},
consumed: header_len,
},
}
}
fn has_noreply(parts: &[&str]) -> bool {
parts.last().map(|s| *s == "noreply").unwrap_or(false)
}
fn parse_storage(cmd: &str, parts: &[&str], buffer: &[u8], header_len: usize) -> Parsed {
let is_cas = cmd == "cas";
let min_parts = if is_cas { 6 } else { 5 };
let Some(bytes_field) = parts.get(4) else {
return Parsed::Fatal {
message: format!("bad data chunk: {} needs {} arguments", cmd, min_parts - 1),
too_large: false,
};
};
let bytes = match bytes_field.parse::<usize>() {
Ok(v) => v,
Err(_) => {
return Parsed::Fatal {
message: "bad command line format: bytes must be a non-negative integer"
.to_string(),
too_large: false,
}
}
};
if bytes > MAX_VALUE_LEN {
return Parsed::Fatal {
message: format!("object too large for cache ({} bytes)", bytes),
too_large: true,
};
}
let frame_len = header_len + bytes + 2;
if buffer.len() < frame_len {
return Parsed::Incomplete;
}
let reject = |message: String| Parsed::Invalid {
message,
consumed: frame_len,
};
if parts.len() < min_parts {
return reject(format!(
"bad data chunk: {} needs {} arguments",
cmd,
min_parts - 1
));
}
let key = parts[1].to_string();
if key.len() > MAX_KEY_LEN {
return reject(format!(
"key is {} bytes, maximum is {}",
key.len(),
MAX_KEY_LEN
));
}
let flags = match parts[2].parse::<u32>() {
Ok(v) => v,
Err(_) => {
return reject(
"bad command line format: flags must be a 32-bit unsigned integer".to_string(),
)
}
};
let exptime = match parts[3].parse::<i64>() {
Ok(v) => v,
Err(_) => return reject("bad command line format: exptime must be an integer".to_string()),
};
let cas_unique = if is_cas {
match parts[5].parse::<u64>() {
Ok(v) => Some(v),
Err(_) => {
return reject(
"bad command line format: cas unique must be a 64-bit integer".to_string(),
)
}
}
} else {
None
};
let data = buffer[header_len..header_len + bytes].to_vec();
if &buffer[header_len + bytes..frame_len] != b"\r\n" {
return reject("bad data chunk".to_string());
}
let command = match cmd {
"set" => "set",
"add" => "add",
"replace" => "replace",
"append" => "append",
"prepend" => "prepend",
_ => "cas",
};
Parsed::Complete {
command: Command::Storage {
command,
key,
flags,
exptime,
bytes,
cas_unique,
noreply: has_noreply(parts),
data,
},
consumed: frame_len,
}
}
fn find_crlf(buffer: &[u8]) -> Option<usize> {
buffer.windows(2).position(|w| w == b"\r\n")
}
pub fn is_noreply(command: &Command) -> bool {
match command {
Command::Storage { noreply, .. }
| Command::Delete { noreply, .. }
| Command::Arithmetic { noreply, .. }
| Command::Touch { noreply, .. }
| Command::FlushAll { noreply, .. } => *noreply,
_ => false,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValueItem {
pub key: String,
pub flags: u32,
pub data: Vec<u8>,
pub cas_unique: Option<u64>,
}
pub fn encode_values(items: &[ValueItem], include_cas: bool) -> Vec<u8> {
let mut out = Vec::new();
for item in items {
let header = match (include_cas, item.cas_unique) {
(true, Some(cas)) => format!(
"VALUE {} {} {} {}\r\n",
item.key,
item.flags,
item.data.len(),
cas
),
_ => format!("VALUE {} {} {}\r\n", item.key, item.flags, item.data.len()),
};
out.extend_from_slice(header.as_bytes());
out.extend_from_slice(&item.data);
out.extend_from_slice(b"\r\n");
}
out.extend_from_slice(b"END\r\n");
out
}
pub fn retain_requested_values(reply: &[u8], keys: &[String]) -> Option<(Vec<u8>, Vec<String>)> {
let mut kept = Vec::with_capacity(reply.len());
let mut dropped = Vec::new();
let mut rest = reply;
loop {
if rest == b"END\r\n" {
kept.extend_from_slice(b"END\r\n");
return Some((kept, dropped));
}
let header_end = find_crlf(rest)?;
let header = std::str::from_utf8(&rest[..header_end]).ok()?;
let mut fields = header.split(' ');
if fields.next()? != "VALUE" {
return None;
}
let key = fields.next()?;
let _flags = fields.next()?;
let len: usize = fields.next()?.parse().ok()?;
let block_end = header_end + 2 + len + 2;
if rest.len() < block_end || &rest[block_end - 2..block_end] != b"\r\n" {
return None;
}
if keys.iter().any(|k| k == key) {
kept.extend_from_slice(&rest[..block_end]);
} else {
dropped.push(key.to_string());
}
rest = &rest[block_end..];
}
}
pub fn encode_stats(entries: &[(String, String)]) -> Vec<u8> {
let mut out = Vec::new();
for (name, value) in entries {
out.extend_from_slice(format!("STAT {} {}\r\n", name, value).as_bytes());
}
out.extend_from_slice(b"END\r\n");
out
}
pub fn status_line(status: &str) -> Option<&'static str> {
match status {
"STORED" => Some("STORED\r\n"),
"NOT_STORED" => Some("NOT_STORED\r\n"),
"EXISTS" => Some("EXISTS\r\n"),
"NOT_FOUND" => Some("NOT_FOUND\r\n"),
"DELETED" => Some("DELETED\r\n"),
"TOUCHED" => Some("TOUCHED\r\n"),
"OK" => Some("OK\r\n"),
"ERROR" => Some("ERROR\r\n"),
_ => None,
}
}