Skip to main content

netget/client/memcached/
wire.rs

1//! The memcached **text** protocol, client side: encode the model's requests, and read the
2//! server's replies back one whole response at a time.
3//!
4//! Pure functions over bytes. The connection loop owns the socket and the FIFO of requests in
5//! flight; this module owns the grammar and every bound on what a server may make the client
6//! buffer.
7//!
8//! # Why the reader needs the request
9//!
10//! A memcached reply does not say which command it answers. `STORED`, `NOT_FOUND`, a bare
11//! number and `END` are only meaningful against the request they follow, and the protocol
12//! answers strictly in order. So [`parse_reply`] takes the [`Expect`] for the oldest request
13//! in flight and returns one complete [`Reply`] for it, or `None` when more bytes are needed.
14//!
15//! # Bounds
16//!
17//! * **A line** — every unit except a value's data block is one CRLF-terminated line of at
18//!   most [`MAX_REPLY_LINE`] octets. A server that sends more without a CRLF has lost framing
19//!   and the connection is failed.
20//! * **A value** — the `<bytes>` a `VALUE` header **declares** is checked against
21//!   [`MAX_VALUE_LEN`] (memcached's own default item limit) before one octet of the block is
22//!   waited for, so `VALUE k 0 4000000000` costs its header line and nothing more.
23//! * **A `get` response** — at most one `VALUE` per requested key, and only for a key that was
24//!   requested. Anything else is a server answering a question nobody asked.
25//! * **`stats`** — at most [`MAX_STATS_ENTRIES`] `STAT` lines per response.
26//!
27//! There is no nesting anywhere in this grammar, so there is no depth to bound.
28
29use crate::server::memcached::protocol::{MAX_KEY_LEN, MAX_VALUE_LEN};
30use serde_json::{json, Map, Value};
31
32/// Longest reply line the client will buffer before declaring the stream unframed.
33///
34/// A `VALUE` header is the longest legitimate line: a 250-octet key, a 32-bit flags field, a
35/// 20-digit byte count and a 20-digit CAS unique. `SERVER_ERROR` messages are short. 2 KiB is
36/// four times the longest real line.
37pub const MAX_REPLY_LINE: usize = 2048;
38
39/// Most `STAT` lines accepted in one `stats` response.
40///
41/// memcached 1.6's `stats` prints ~90 lines, `stats settings` ~70, and `stats items` /
42/// `stats slabs` ~20 per slab class across at most 64 classes. 4096 covers every group with
43/// room to spare and stops a server that streams `STAT` lines forever.
44pub const MAX_STATS_ENTRIES: usize = 4096;
45
46/// Most octets one response may occupy, headers and data blocks together.
47///
48/// Per-value and per-line bounds do not bound a *response*: 32 keys of 1 MiB each, or 4096
49/// `STAT` lines of 2 KiB, are each legal one piece at a time. The transport task buffers one
50/// response until it is complete, so this is also the ceiling on that buffer.
51pub const MAX_RESPONSE_BYTES: usize = 4 * 1024 * 1024;
52
53/// Most keys one `get`/`gets` may ask for. Every key becomes its own model event, so this is
54/// also the most model turns one request can cause.
55pub const MAX_GET_KEYS: usize = 32;
56
57/// A storage verb.
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum StoreVerb {
60    Set,
61    Add,
62    Replace,
63    Append,
64    Prepend,
65    Cas,
66}
67
68impl StoreVerb {
69    pub fn as_str(self) -> &'static str {
70        match self {
71            StoreVerb::Set => "set",
72            StoreVerb::Add => "add",
73            StoreVerb::Replace => "replace",
74            StoreVerb::Append => "append",
75            StoreVerb::Prepend => "prepend",
76            StoreVerb::Cas => "cas",
77        }
78    }
79}
80
81/// One request, validated. Built from the model's action by `actions.rs`.
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub enum Request {
84    Get {
85        keys: Vec<String>,
86        with_cas: bool,
87    },
88    Store {
89        verb: StoreVerb,
90        key: String,
91        flags: u32,
92        exptime: i64,
93        value: String,
94        /// Required for `cas`, absent otherwise.
95        cas_unique: Option<u64>,
96    },
97    Delete {
98        key: String,
99    },
100    Incr {
101        key: String,
102        delta: u64,
103    },
104    Decr {
105        key: String,
106        delta: u64,
107    },
108    Touch {
109        key: String,
110        exptime: i64,
111    },
112    Stats {
113        group: Option<String>,
114    },
115    Version,
116    FlushAll {
117        delay: Option<u64>,
118    },
119}
120
121impl Request {
122    /// The command word, for events and logs.
123    pub fn command(&self) -> &'static str {
124        match self {
125            Request::Get {
126                with_cas: false, ..
127            } => "get",
128            Request::Get { with_cas: true, .. } => "gets",
129            Request::Store { verb, .. } => verb.as_str(),
130            Request::Delete { .. } => "delete",
131            Request::Incr { .. } => "incr",
132            Request::Decr { .. } => "decr",
133            Request::Touch { .. } => "touch",
134            Request::Stats { .. } => "stats",
135            Request::Version => "version",
136            Request::FlushAll { .. } => "flush_all",
137        }
138    }
139
140    /// The bytes on the wire. Never `noreply`: every request must be answered, or the FIFO
141    /// that pairs replies with requests would be off by one from then on.
142    pub fn encode(&self) -> Vec<u8> {
143        let line = match self {
144            Request::Get { keys, .. } => format!("{} {}", self.command(), keys.join(" ")),
145            Request::Store {
146                verb,
147                key,
148                flags,
149                exptime,
150                value,
151                cas_unique,
152            } => {
153                let mut head = format!(
154                    "{} {} {} {} {}",
155                    verb.as_str(),
156                    key,
157                    flags,
158                    exptime,
159                    value.len()
160                );
161                if let Some(cas) = cas_unique {
162                    head.push_str(&format!(" {cas}"));
163                }
164                let mut out = head.into_bytes();
165                out.extend_from_slice(b"\r\n");
166                out.extend_from_slice(value.as_bytes());
167                out.extend_from_slice(b"\r\n");
168                return out;
169            }
170            Request::Delete { key } => format!("delete {key}"),
171            Request::Incr { key, delta } => format!("incr {key} {delta}"),
172            Request::Decr { key, delta } => format!("decr {key} {delta}"),
173            Request::Touch { key, exptime } => format!("touch {key} {exptime}"),
174            Request::Stats { group: None } => "stats".to_string(),
175            Request::Stats { group: Some(g) } => format!("stats {g}"),
176            Request::Version => "version".to_string(),
177            Request::FlushAll { delay: None } => "flush_all".to_string(),
178            Request::FlushAll { delay: Some(d) } => format!("flush_all {d}"),
179        };
180        let mut out = line.into_bytes();
181        out.extend_from_slice(b"\r\n");
182        out
183    }
184
185    /// What the reply to this request looks like.
186    pub fn expect(&self) -> Expect {
187        let command = self.command();
188        match self {
189            Request::Get { keys, with_cas } => Expect::Values {
190                keys: keys.clone(),
191                with_cas: *with_cas,
192            },
193            Request::Store { key, .. }
194            | Request::Delete { key }
195            | Request::Incr { key, .. }
196            | Request::Decr { key, .. }
197            | Request::Touch { key, .. } => Expect::Status {
198                command,
199                key: Some(key.clone()),
200            },
201            Request::Stats { group } => Expect::Stats {
202                group: group.clone(),
203            },
204            Request::Version | Request::FlushAll { .. } => Expect::Status { command, key: None },
205        }
206    }
207}
208
209/// Refuse a key memcached would refuse, or that would break the command line it sits in.
210///
211/// A key is one space-delimited token on a CRLF-terminated line, so a space or a control
212/// octet in it would turn the model's key into a different command. Refused, never rewritten.
213pub fn validate_key(key: &str) -> Result<(), String> {
214    if key.is_empty() {
215        return Err("a memcached key cannot be empty".to_string());
216    }
217    if key.len() > MAX_KEY_LEN {
218        return Err(format!(
219            "key is {} bytes; memcached refuses keys longer than {MAX_KEY_LEN}",
220            key.len()
221        ));
222    }
223    if key.bytes().any(|b| b <= b' ' || b == 0x7f) {
224        return Err(format!(
225            "key {key:?} contains a space or a control character; a memcached key is one \
226             token on the command line"
227        ));
228    }
229    Ok(())
230}
231
232/// A `stats` group is one lowercase word (`settings`, `items`, `slabs`, `sizes`, `conns`).
233pub fn validate_stats_group(group: &str) -> Result<(), String> {
234    if group.is_empty() || group.len() > 32 || !group.bytes().all(|b| b.is_ascii_lowercase()) {
235        return Err(format!(
236            "stats group {group:?} is not one lowercase word such as settings, items or slabs"
237        ));
238    }
239    Ok(())
240}
241
242/// What the reply to the oldest request in flight looks like.
243#[derive(Debug, Clone, PartialEq, Eq)]
244pub enum Expect {
245    /// `VALUE … END` for these keys.
246    Values { keys: Vec<String>, with_cas: bool },
247    /// One status line (`STORED`, `DELETED`, a number, `VERSION …`, `OK`).
248    Status {
249        command: &'static str,
250        key: Option<String>,
251    },
252    /// `STAT … END`.
253    Stats { group: Option<String> },
254}
255
256/// One `VALUE` block.
257#[derive(Debug, Clone, PartialEq, Eq)]
258pub struct Item {
259    pub key: String,
260    pub flags: u32,
261    pub cas: Option<u64>,
262    pub data: Vec<u8>,
263}
264
265/// One complete response.
266#[derive(Debug, Clone, PartialEq, Eq)]
267pub enum Reply {
268    /// A `get`/`gets` response: the hits in the order the server sent them, and the keys it
269    /// did not return.
270    Values {
271        items: Vec<Item>,
272        misses: Vec<String>,
273    },
274    /// A status line, uninterpreted: `STORED`, `NOT_STORED`, `EXISTS`, `NOT_FOUND`,
275    /// `DELETED`, `TOUCHED`, `OK`, `VERSION x`, or a counter's decimal value.
276    Status { line: String },
277    /// `STAT` lines, in order.
278    Stats { entries: Vec<(String, String)> },
279    /// `ERROR`, `CLIENT_ERROR <msg>` or `SERVER_ERROR <msg>`.
280    Error { kind: &'static str, message: String },
281}
282
283/// Why the byte stream can no longer be read as memcached replies. Always fatal for the
284/// connection: after any of these the next reply's start is unknown.
285#[derive(Debug, Clone, PartialEq, Eq)]
286pub enum WireError {
287    LineTooLong,
288    ValueTooLarge { key: String, declared: u64 },
289    UnexpectedLine(String),
290    UnrequestedKey(String),
291    TooManyStats,
292    BadValueTerminator,
293    ResponseTooLarge,
294}
295
296impl std::fmt::Display for WireError {
297    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
298        match self {
299            WireError::LineTooLong => write!(
300                f,
301                "server sent more than {MAX_REPLY_LINE} octets without a CRLF"
302            ),
303            WireError::ValueTooLarge { key, declared } => write!(
304                f,
305                "VALUE for {key:?} declares {declared} octets, over the {MAX_VALUE_LEN}-octet \
306                 item limit this client reads"
307            ),
308            WireError::UnexpectedLine(line) => write!(
309                f,
310                "reply line {:?} is not a valid answer to the request in flight",
311                crate::utils::truncate::truncate_for_log(line, 80)
312            ),
313            WireError::UnrequestedKey(key) => {
314                write!(
315                    f,
316                    "server returned a VALUE for {key:?}, which was not requested"
317                )
318            }
319            WireError::TooManyStats => {
320                write!(f, "stats response exceeded {MAX_STATS_ENTRIES} STAT lines")
321            }
322            WireError::BadValueTerminator => {
323                write!(f, "a VALUE data block was not followed by CRLF")
324            }
325            WireError::ResponseTooLarge => write!(
326                f,
327                "one response exceeded {MAX_RESPONSE_BYTES} octets before it was complete"
328            ),
329        }
330    }
331}
332
333impl std::error::Error for WireError {}
334
335/// The next CRLF-terminated line starting at `at`, as `(line, next_offset)`.
336fn line_at(buf: &[u8], at: usize) -> Result<Option<(&str, usize)>, WireError> {
337    let rest = &buf[at..];
338    let window = &rest[..rest.len().min(MAX_REPLY_LINE + 2)];
339    match window.windows(2).position(|w| w == b"\r\n") {
340        Some(end) => {
341            let line = std::str::from_utf8(&rest[..end]).map_err(|_| {
342                WireError::UnexpectedLine(String::from_utf8_lossy(&rest[..end]).into())
343            })?;
344            Ok(Some((line, at + end + 2)))
345        }
346        None if rest.len() > MAX_REPLY_LINE => Err(WireError::LineTooLong),
347        None => Ok(None),
348    }
349}
350
351/// An error line, which may stand in for any reply.
352fn error_line(line: &str) -> Option<Reply> {
353    if line == "ERROR" {
354        return Some(Reply::Error {
355            kind: "error",
356            message: "the server did not recognise the command".to_string(),
357        });
358    }
359    if let Some(msg) = line.strip_prefix("CLIENT_ERROR") {
360        return Some(Reply::Error {
361            kind: "client_error",
362            message: msg.trim().to_string(),
363        });
364    }
365    if let Some(msg) = line.strip_prefix("SERVER_ERROR") {
366        return Some(Reply::Error {
367            kind: "server_error",
368            message: msg.trim().to_string(),
369        });
370    }
371    None
372}
373
374/// Read one complete reply for `expect` off the front of `buf`.
375///
376/// `Ok(None)` means more bytes are needed and nothing was consumed; `Ok(Some((reply, n)))`
377/// consumed `n` octets.
378pub fn parse_reply(buf: &[u8], expect: &Expect) -> Result<Option<(Reply, usize)>, WireError> {
379    let Some((first, mut pos)) = line_at(buf, 0)? else {
380        return Ok(None);
381    };
382    if let Some(err) = error_line(first) {
383        return Ok(Some((err, pos)));
384    }
385
386    match expect {
387        Expect::Status { .. } => {
388            if first.starts_with("VALUE ") || first.starts_with("STAT ") || first == "END" {
389                return Err(WireError::UnexpectedLine(first.to_string()));
390            }
391            Ok(Some((
392                Reply::Status {
393                    line: first.to_string(),
394                },
395                pos,
396            )))
397        }
398        Expect::Stats { .. } => {
399            let mut entries = Vec::new();
400            let mut line = first;
401            loop {
402                if line == "END" {
403                    return Ok(Some((Reply::Stats { entries }, pos)));
404                }
405                let Some(rest) = line.strip_prefix("STAT ") else {
406                    return Err(WireError::UnexpectedLine(line.to_string()));
407                };
408                if entries.len() >= MAX_STATS_ENTRIES {
409                    return Err(WireError::TooManyStats);
410                }
411                if pos > MAX_RESPONSE_BYTES {
412                    return Err(WireError::ResponseTooLarge);
413                }
414                let (name, value) = rest.split_once(' ').unwrap_or((rest, ""));
415                entries.push((name.to_string(), value.to_string()));
416                match line_at(buf, pos)? {
417                    Some((next, next_pos)) => {
418                        line = next;
419                        pos = next_pos;
420                    }
421                    None => return Ok(None),
422                }
423            }
424        }
425        Expect::Values { keys, with_cas } => {
426            let mut items: Vec<Item> = Vec::new();
427            let mut line = first;
428            loop {
429                if line == "END" {
430                    let misses = keys
431                        .iter()
432                        .filter(|k| !items.iter().any(|i| &i.key == *k))
433                        .cloned()
434                        .collect();
435                    return Ok(Some((Reply::Values { items, misses }, pos)));
436                }
437                let Some(rest) = line.strip_prefix("VALUE ") else {
438                    return Err(WireError::UnexpectedLine(line.to_string()));
439                };
440                let fields: Vec<&str> = rest.split(' ').collect();
441                let (key, flags, bytes, cas) = match (fields.as_slice(), with_cas) {
442                    ([k, f, b], false) => (*k, *f, *b, None),
443                    ([k, f, b, c], true) => (*k, *f, *b, Some(*c)),
444                    _ => return Err(WireError::UnexpectedLine(line.to_string())),
445                };
446                if !keys.iter().any(|k| k == key) || items.iter().any(|i| i.key == key) {
447                    return Err(WireError::UnrequestedKey(key.to_string()));
448                }
449                let bad = || WireError::UnexpectedLine(line.to_string());
450                let flags: u32 = flags.parse().map_err(|_| bad())?;
451                let declared: u64 = bytes.parse().map_err(|_| bad())?;
452                let cas = match cas {
453                    Some(c) => Some(c.parse::<u64>().map_err(|_| bad())?),
454                    None => None,
455                };
456                // The declared size, before anything is waited for or allocated.
457                let len = usize::try_from(declared)
458                    .ok()
459                    .filter(|n| *n <= MAX_VALUE_LEN)
460                    .ok_or_else(|| WireError::ValueTooLarge {
461                        key: key.to_string(),
462                        declared,
463                    })?;
464                let key = key.to_string();
465                if pos + len + 2 > MAX_RESPONSE_BYTES {
466                    return Err(WireError::ResponseTooLarge);
467                }
468                if buf.len() < pos + len + 2 {
469                    return Ok(None);
470                }
471                if &buf[pos + len..pos + len + 2] != b"\r\n" {
472                    return Err(WireError::BadValueTerminator);
473                }
474                items.push(Item {
475                    key,
476                    flags,
477                    cas,
478                    data: buf[pos..pos + len].to_vec(),
479                });
480                pos += len + 2;
481                match line_at(buf, pos)? {
482                    Some((next, next_pos)) => {
483                        line = next;
484                        pos = next_pos;
485                    }
486                    None => return Ok(None),
487                }
488            }
489        }
490    }
491}
492
493/// Render a `stats` response as one JSON object. A name that repeats keeps its last value.
494pub fn stats_json(entries: &[(String, String)]) -> Value {
495    let mut map = Map::new();
496    for (k, v) in entries {
497        map.insert(k.clone(), json!(v));
498    }
499    Value::Object(map)
500}