Skip to main content

ocular_protocol/
memcached.rs

1// Memcached text protocol parser
2
3/// Parse a memcached request into a human-readable summary
4pub fn parse_memcached_request(buf: &[u8]) -> Option<String> {
5    let s = std::str::from_utf8(buf).ok()?;
6    let line = s.lines().next()?;
7    let parts: Vec<&str> = line.split_whitespace().collect();
8    if parts.is_empty() { return None; }
9    let cmd = parts[0].to_uppercase();
10    match cmd.as_str() {
11        "GET" | "GETS" => Some(parts.join(" ")),
12        "SET" | "ADD" | "REPLACE" | "APPEND" | "PREPEND" | "CAS" => {
13            // set <key> <flags> <exptime> <bytes> [noreply]\r\n<data>\r\n
14            if parts.len() >= 5 {
15                let key = parts[1];
16                let bytes: usize = parts[4].parse().unwrap_or(0);
17                // Try to extract the data block
18                if let Some(data_start) = s.find("\r\n").map(|i| i + 2) {
19                    let data = &s[data_start..];
20                    let value = data.get(..bytes.min(64)).unwrap_or(data).trim_end_matches("\r\n");
21                    Some(format!("{} {} \"{}\"", cmd, key, value))
22                } else {
23                    Some(format!("{} {} ({} bytes)", cmd, key, bytes))
24                }
25            } else {
26                Some(parts.join(" "))
27            }
28        }
29        "DELETE" => Some(format!("DELETE {}", parts.get(1).unwrap_or(&""))),
30        "INCR" | "DECR" => {
31            let key = parts.get(1).unwrap_or(&"");
32            let val = parts.get(2).unwrap_or(&"1");
33            Some(format!("{} {} {}", cmd, key, val))
34        }
35        "TOUCH" => {
36            let key = parts.get(1).unwrap_or(&"");
37            let exp = parts.get(2).unwrap_or(&"0");
38            Some(format!("TOUCH {} {}", key, exp))
39        }
40        "STATS" | "VERSION" | "FLUSH_ALL" | "QUIT" => Some(cmd),
41        _ => Some(parts.join(" ")),
42    }
43}
44
45/// Parse a memcached response into a short summary
46pub fn parse_memcached_response(buf: &[u8]) -> Option<String> {
47    let s = std::str::from_utf8(buf).ok()?;
48    let line = s.lines().next()?;
49    match line {
50        "STORED" => Some("STORED".into()),
51        "NOT_STORED" => Some("NOT_STORED".into()),
52        "EXISTS" => Some("EXISTS".into()),
53        "NOT_FOUND" => Some("NOT_FOUND".into()),
54        "DELETED" => Some("DELETED".into()),
55        "TOUCHED" => Some("TOUCHED".into()),
56        "OK" => Some("OK".into()),
57        "ERROR" => Some("ERROR".into()),
58        "END" => Some("(empty)".into()),
59        _ if line.starts_with("VALUE ") => {
60            // Count VALUE lines
61            let count = s.lines().filter(|l| l.starts_with("VALUE ")).count();
62            if count == 1 {
63                let parts: Vec<&str> = line.split_whitespace().collect();
64                let key = parts.get(1).unwrap_or(&"?");
65                let bytes: usize = parts.get(3).and_then(|b| b.parse().ok()).unwrap_or(0);
66                Some(format!("VALUE {} ({} bytes)", key, bytes))
67            } else {
68                Some(format!("{} values", count))
69            }
70        }
71        _ if line.starts_with("VERSION ") => Some(line.to_string()),
72        _ if line.starts_with("STAT ") => {
73            let count = s.lines().filter(|l| l.starts_with("STAT ")).count();
74            Some(format!("STATS ({} entries)", count))
75        }
76        _ if line.starts_with("SERVER_ERROR") || line.starts_with("CLIENT_ERROR") => {
77            Some(line.to_string())
78        }
79        // INCR/DECR response is just a number
80        _ => line.parse::<u64>().ok().map(|n| format!("{}", n)),
81    }
82}
83
84/// Format response detail for the Detail panel
85pub fn format_memcached_response_detail(buf: &[u8]) -> Option<String> {
86    let s = std::str::from_utf8(buf).ok()?;
87    let first = s.lines().next()?;
88    if first.starts_with("VALUE ") {
89        // Show all VALUE blocks with their data
90        let mut result = String::new();
91        let mut lines = s.lines().peekable();
92        while let Some(line) = lines.next() {
93            if line.starts_with("VALUE ") {
94                result.push_str(line);
95                result.push('\n');
96                if let Some(data) = lines.next() {
97                    if data != "END" {
98                        result.push_str(data);
99                        result.push('\n');
100                    }
101                }
102            }
103        }
104        Some(result.trim_end().to_string())
105    } else if first.starts_with("STAT ") {
106        Some(s.lines().take_while(|l| l.starts_with("STAT ")).collect::<Vec<_>>().join("\n"))
107    } else {
108        Some(first.to_string())
109    }
110}
111
112/// Check if a memcached request is complete (ends with \r\n, and for storage commands includes data block)
113pub fn memcached_request_complete(buf: &[u8]) -> bool {
114    let s = match std::str::from_utf8(buf) {
115        Ok(s) => s,
116        Err(_) => return buf.ends_with(b"\r\n"),
117    };
118    let Some(first_crlf) = s.find("\r\n") else { return false };
119    let line = &s[..first_crlf];
120    let parts: Vec<&str> = line.split_whitespace().collect();
121    let cmd = parts.first().map(|c| c.to_uppercase()).unwrap_or_default();
122    match cmd.as_str() {
123        "SET" | "ADD" | "REPLACE" | "APPEND" | "PREPEND" | "CAS" => {
124            // Need command line + data block + \r\n
125            let bytes: usize = parts.get(4).and_then(|b| b.parse().ok()).unwrap_or(0);
126            let expected = first_crlf + 2 + bytes + 2;
127            buf.len() >= expected
128        }
129        _ => buf.ends_with(b"\r\n"),
130    }
131}
132
133/// Check if a memcached response is complete
134pub fn memcached_response_complete(buf: &[u8]) -> bool {
135    let s = match std::str::from_utf8(buf) {
136        Ok(s) => s,
137        Err(_) => return buf.ends_with(b"\r\n"),
138    };
139    // VALUE responses end with "END\r\n"
140    if s.starts_with("VALUE ") {
141        return s.ends_with("END\r\n");
142    }
143    // STAT responses end with "END\r\n"
144    if s.starts_with("STAT ") {
145        return s.ends_with("END\r\n");
146    }
147    // Single-line responses
148    s.ends_with("\r\n")
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn test_parse_get() {
157        assert_eq!(parse_memcached_request(b"get user:1\r\n"), Some("get user:1".into()));
158    }
159
160    #[test]
161    fn test_parse_set() {
162        let req = b"set user:1 0 300 5\r\nhello\r\n";
163        assert_eq!(parse_memcached_request(req), Some("SET user:1 \"hello\"".into()));
164    }
165
166    #[test]
167    fn test_parse_response_stored() {
168        assert_eq!(parse_memcached_response(b"STORED\r\n"), Some("STORED".into()));
169    }
170
171    #[test]
172    fn test_parse_response_value() {
173        let resp = b"VALUE user:1 0 5\r\nhello\r\nEND\r\n";
174        assert_eq!(parse_memcached_response(resp), Some("VALUE user:1 (5 bytes)".into()));
175    }
176
177    #[test]
178    fn test_request_complete_get() {
179        assert!(memcached_request_complete(b"get key\r\n"));
180        assert!(!memcached_request_complete(b"get key"));
181    }
182
183    #[test]
184    fn test_request_complete_set() {
185        assert!(memcached_request_complete(b"set k 0 0 3\r\nabc\r\n"));
186        assert!(!memcached_request_complete(b"set k 0 0 3\r\nab"));
187    }
188
189    #[test]
190    fn test_response_complete_value() {
191        assert!(memcached_response_complete(b"VALUE k 0 3\r\nabc\r\nEND\r\n"));
192        assert!(!memcached_response_complete(b"VALUE k 0 3\r\nabc\r\n"));
193    }
194}