use std::collections::HashMap;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
pub(crate) const HELLO_REPLY: &[u8] = b"%7\r\n\
$6\r\nserver\r\n$5\r\nredis\r\n\
$7\r\nversion\r\n$5\r\n7.4.0\r\n\
$5\r\nproto\r\n:3\r\n\
$2\r\nid\r\n:1\r\n\
$4\r\nmode\r\n$10\r\nstandalone\r\n\
$4\r\nrole\r\n$6\r\nmaster\r\n\
$7\r\nmodules\r\n*0\r\n";
#[derive(Clone, Default)]
pub(crate) struct FakeServer {
replies: HashMap<String, Vec<u8>>,
}
impl FakeServer {
pub(crate) fn new() -> Self {
let mut replies = HashMap::new();
replies.insert("HELLO".to_owned(), HELLO_REPLY.to_vec());
Self { replies }
}
pub(crate) fn reply(mut self, command: &str, reply: &[u8]) -> Self {
self.replies
.insert(command.to_ascii_uppercase(), reply.to_vec());
self
}
pub(crate) async fn serve<S>(&self, mut stream: S)
where
S: AsyncRead + AsyncWrite + Unpin,
{
let mut buf = Vec::new();
let mut chunk = [0u8; 1024];
loop {
let n = match stream.read(&mut chunk).await {
Ok(0) | Err(_) => return,
Ok(n) => n,
};
buf.extend_from_slice(&chunk[..n]);
let mut cursor = 0;
while let Some((name, next)) = parse_request(&buf[cursor..]) {
cursor += next;
let reply =
self.replies.get(&name).cloned().unwrap_or_else(|| {
format!("-ERR unknown command '{name}'\r\n").into_bytes()
});
if stream.write_all(&reply).await.is_err() {
return;
}
}
buf.drain(..cursor);
}
}
}
fn parse_request(buf: &[u8]) -> Option<(String, usize)> {
let (count, mut pos) = parse_prefixed_len(buf, b'*')?;
let mut name = None;
for _ in 0..count {
let (len, next) = parse_prefixed_len(&buf[pos..], b'$')?;
pos += next;
let end = pos.checked_add(len)?;
if buf.len() < end + 2 {
return None;
}
if name.is_none() {
name = Some(String::from_utf8_lossy(&buf[pos..end]).to_ascii_uppercase());
}
pos = end + 2;
}
Some((name?, pos))
}
fn parse_prefixed_len(buf: &[u8], tag: u8) -> Option<(usize, usize)> {
if buf.first() != Some(&tag) {
return None;
}
let crlf = buf.windows(2).position(|w| w == b"\r\n")?;
let len = std::str::from_utf8(&buf[1..crlf]).ok()?.parse().ok()?;
Some((len, crlf + 2))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_request_is_parsed_into_its_command_name() {
let (name, len) = parse_request(b"*2\r\n$3\r\nget\r\n$1\r\nk\r\n").unwrap();
assert_eq!("GET", name);
assert_eq!(20, len);
}
#[test]
fn an_incomplete_request_is_not_parsed() {
assert!(parse_request(b"*2\r\n$3\r\nget\r\n$1\r\n").is_none());
assert!(parse_request(b"*2\r\n$3\r\nge").is_none());
assert!(parse_request(b"*2\r\n").is_none());
}
#[tokio::test]
async fn the_handshake_and_a_scripted_command_are_answered_in_order() {
let (client_side, server_side) = tokio::io::duplex(1024);
let server = FakeServer::new().reply("PING", b"+PONG\r\n");
tokio::spawn(async move { server.serve(server_side).await });
let mut client_side = client_side;
client_side
.write_all(b"*2\r\n$5\r\nhello\r\n$1\r\n3\r\n*1\r\n$4\r\nping\r\n")
.await
.unwrap();
let mut out = vec![0u8; HELLO_REPLY.len() + 7];
client_side.read_exact(&mut out).await.unwrap();
assert_eq!(&out[..HELLO_REPLY.len()], HELLO_REPLY);
assert_eq!(&out[HELLO_REPLY.len()..], b"+PONG\r\n");
}
}