Skip to main content

ping_proto/
lib.rs

1//! Simple ASCII PING/PONG protocol implementation.
2//!
3//! This crate provides a minimal protocol for connectivity testing:
4//! - Request: `PING\r\n`
5//! - Response: `PONG\r\n` or `+PONG\r\n` (RESP-style)
6//!
7//! # Example
8//!
9//! ```
10//! use ping_proto::{Request, Response};
11//!
12//! // Encode a PING request
13//! let mut buf = [0u8; 16];
14//! let len = Request::Ping.encode(&mut buf);
15//! assert_eq!(&buf[..len], b"PING\r\n");
16//!
17//! // Parse a PONG response
18//! let data = b"PONG\r\n";
19//! let (response, consumed) = Response::parse(data).unwrap();
20//! assert_eq!(response, Response::Pong);
21//! assert_eq!(consumed, 6);
22//! ```
23
24/// Parse error types.
25#[derive(Debug, Clone, thiserror::Error)]
26pub enum ParseError {
27    /// Need more data to complete parsing.
28    #[error("incomplete")]
29    Incomplete,
30    /// Invalid response format.
31    #[error("invalid response")]
32    Invalid,
33}
34
35/// A PING protocol request.
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum Request {
38    /// PING command
39    Ping,
40}
41
42impl Request {
43    /// Encode the request into the buffer.
44    ///
45    /// Returns the number of bytes written.
46    pub fn encode(self, buf: &mut [u8]) -> usize {
47        const PING_CMD: &[u8] = b"PING\r\n";
48        let len = PING_CMD.len();
49        buf[..len].copy_from_slice(PING_CMD);
50        len
51    }
52
53    /// Returns the encoded length of this request.
54    pub const fn encoded_len(self) -> usize {
55        6 // "PING\r\n"
56    }
57}
58
59/// A PING protocol response.
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum Response {
62    /// Successful PONG response.
63    Pong,
64    /// Error response.
65    Error,
66}
67
68impl Response {
69    /// Parse a response from the buffer.
70    ///
71    /// Returns the parsed response and number of bytes consumed, or an error.
72    ///
73    /// Accepts both simple ASCII `PONG\r\n` and RESP-style `+PONG\r\n`.
74    pub fn parse(data: &[u8]) -> Result<(Self, usize), ParseError> {
75        const PONG_SIMPLE: &[u8] = b"PONG\r\n";
76        const PONG_RESP: &[u8] = b"+PONG\r\n";
77
78        if data.is_empty() {
79            return Err(ParseError::Incomplete);
80        }
81
82        // Check for RESP-style "+PONG\r\n" or other RESP simple strings
83        if data[0] == b'+' {
84            if data.len() < PONG_RESP.len() {
85                if PONG_RESP.starts_with(data) {
86                    return Err(ParseError::Incomplete);
87                }
88            } else if data.starts_with(PONG_RESP) {
89                return Ok((Response::Pong, PONG_RESP.len()));
90            }
91
92            // Look for line ending - any +... response is treated as success
93            if let Some(pos) = data.iter().position(|&b| b == b'\n') {
94                return Ok((Response::Pong, pos + 1));
95            }
96            return Err(ParseError::Incomplete);
97        }
98
99        // Check for simple ASCII "PONG\r\n"
100        if data.starts_with(b"PONG") {
101            if data.len() < PONG_SIMPLE.len() {
102                if PONG_SIMPLE.starts_with(data) {
103                    return Err(ParseError::Incomplete);
104                }
105            } else if data.starts_with(PONG_SIMPLE) {
106                return Ok((Response::Pong, PONG_SIMPLE.len()));
107            }
108        }
109
110        // Check for error responses "-ERR ...\r\n"
111        if data[0] == b'-' {
112            if let Some(pos) = data.iter().position(|&b| b == b'\n') {
113                return Ok((Response::Error, pos + 1));
114            }
115            return Err(ParseError::Incomplete);
116        }
117
118        // Check for partial "PONG" match
119        if PONG_SIMPLE.starts_with(data) {
120            return Err(ParseError::Incomplete);
121        }
122
123        // Invalid response
124        Err(ParseError::Invalid)
125    }
126
127    /// Encode the response into the buffer.
128    ///
129    /// Returns the number of bytes written.
130    pub fn encode(self, buf: &mut [u8]) -> usize {
131        let data = match self {
132            Response::Pong => b"PONG\r\n",
133            Response::Error => b"-ERR\r\n",
134        };
135        buf[..data.len()].copy_from_slice(data);
136        data.len()
137    }
138
139    /// Returns true if this is an error response.
140    pub fn is_error(self) -> bool {
141        matches!(self, Response::Error)
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    #[test]
150    fn test_encode_ping() {
151        let mut buf = [0u8; 16];
152        let len = Request::Ping.encode(&mut buf);
153        assert_eq!(&buf[..len], b"PING\r\n");
154        assert_eq!(len, 6);
155    }
156
157    #[test]
158    fn test_parse_pong_simple() {
159        let (resp, consumed) = Response::parse(b"PONG\r\n").unwrap();
160        assert_eq!(resp, Response::Pong);
161        assert_eq!(consumed, 6);
162    }
163
164    #[test]
165    fn test_parse_pong_resp() {
166        let (resp, consumed) = Response::parse(b"+PONG\r\n").unwrap();
167        assert_eq!(resp, Response::Pong);
168        assert_eq!(consumed, 7);
169    }
170
171    #[test]
172    fn test_parse_pong_resp_other() {
173        // Redis might respond with +OK or other simple strings
174        let (resp, consumed) = Response::parse(b"+OK\r\n").unwrap();
175        assert_eq!(resp, Response::Pong);
176        assert_eq!(consumed, 5);
177    }
178
179    #[test]
180    fn test_parse_error() {
181        let (resp, consumed) = Response::parse(b"-ERR unknown command\r\n").unwrap();
182        assert_eq!(resp, Response::Error);
183        assert_eq!(consumed, 22);
184    }
185
186    #[test]
187    fn test_parse_incomplete() {
188        assert!(matches!(Response::parse(b""), Err(ParseError::Incomplete)));
189        assert!(matches!(
190            Response::parse(b"PO"),
191            Err(ParseError::Incomplete)
192        ));
193        assert!(matches!(
194            Response::parse(b"PONG"),
195            Err(ParseError::Incomplete)
196        ));
197        assert!(matches!(
198            Response::parse(b"PONG\r"),
199            Err(ParseError::Incomplete)
200        ));
201        assert!(matches!(
202            Response::parse(b"+PON"),
203            Err(ParseError::Incomplete)
204        ));
205    }
206
207    #[test]
208    fn test_parse_invalid() {
209        assert!(matches!(
210            Response::parse(b"INVALID\r\n"),
211            Err(ParseError::Invalid)
212        ));
213        assert!(matches!(
214            Response::parse(b"XONG\r\n"),
215            Err(ParseError::Invalid)
216        ));
217    }
218
219    #[test]
220    fn test_encode_response() {
221        let mut buf = [0u8; 16];
222
223        let len = Response::Pong.encode(&mut buf);
224        assert_eq!(&buf[..len], b"PONG\r\n");
225
226        let len = Response::Error.encode(&mut buf);
227        assert_eq!(&buf[..len], b"-ERR\r\n");
228    }
229
230    #[test]
231    fn test_roundtrip() {
232        let mut buf = [0u8; 16];
233        let len = Response::Pong.encode(&mut buf);
234        let (resp, consumed) = Response::parse(&buf[..len]).unwrap();
235        assert_eq!(resp, Response::Pong);
236        assert_eq!(consumed, len);
237    }
238}