Skip to main content

powdb_server/
protocol.rs

1use tokio::io::{AsyncReadExt, AsyncWriteExt};
2use zeroize::Zeroizing;
3
4const MSG_CONNECT: u8 = 0x01;
5const MSG_CONNECT_OK: u8 = 0x02;
6const MSG_QUERY: u8 = 0x03;
7const MSG_RESULT_ROWS: u8 = 0x07;
8const MSG_RESULT_SCALAR: u8 = 0x08;
9const MSG_RESULT_OK: u8 = 0x09;
10const MSG_ERROR: u8 = 0x0A;
11const MSG_RESULT_MSG: u8 = 0x0B;
12const MSG_DISCONNECT: u8 = 0x10;
13const MSG_PING: u8 = 0x11;
14const MSG_PONG: u8 = 0x12;
15
16/// Maximum payload size accepted from the wire (64 MB).
17const MAX_PAYLOAD_SIZE: usize = 64 * 1024 * 1024;
18
19/// Maximum payload size for pre-auth CONNECT messages (4 KB).
20/// Only a database name and password are needed before authentication.
21const MAX_CONNECT_PAYLOAD_SIZE: usize = 4096;
22
23/// Maximum number of columns allowed in a result set.
24const MAX_COLUMNS: usize = 4096;
25
26/// Maximum number of rows allowed in a single result message.
27const MAX_ROWS: usize = 10_000_000;
28
29#[derive(Debug, Clone)]
30pub enum Message {
31    Connect {
32        db_name: String,
33        /// Client-supplied candidate password. Wrapped in `Zeroizing` so the
34        /// raw bytes from the wire are wiped from memory once the `Message`
35        /// (and thus this field) is dropped after the constant-time compare —
36        /// the candidate never lingers in a plain `String`.
37        password: Option<Zeroizing<String>>,
38        /// Optional user name for multi-user authentication. Appended after the
39        /// password on the wire so the format is a pure, backward-compatible
40        /// extension: old clients that omit it decode as `None`.
41        username: Option<String>,
42    },
43    ConnectOk {
44        version: String,
45    },
46    Query {
47        query: String,
48    },
49    ResultRows {
50        columns: Vec<String>,
51        rows: Vec<Vec<String>>,
52    },
53    ResultScalar {
54        value: String,
55    },
56    ResultOk {
57        affected: u64,
58    },
59    /// A descriptive status message (e.g. "type User created", "index dropped").
60    ResultMessage {
61        message: String,
62    },
63    Error {
64        message: String,
65    },
66    Disconnect,
67    Ping,
68    Pong,
69}
70
71impl Message {
72    /// Encode message into wire format: [type(1)][flags(1)][len(4)][payload]
73    pub fn encode(&self) -> Vec<u8> {
74        let (msg_type, payload) = match self {
75            Message::Connect {
76                db_name,
77                password,
78                username,
79            } => {
80                let mut buf = encode_string(db_name);
81                // Password is encoded as a length-prefixed string. Empty (len=0) means None.
82                match password {
83                    Some(p) => buf.extend_from_slice(&encode_string(p)),
84                    None => buf.extend_from_slice(&0u32.to_le_bytes()),
85                }
86                // Username is appended after the password (append-only extension).
87                // Length-prefixed string; empty (len=0) means None.
88                match username {
89                    Some(u) => buf.extend_from_slice(&encode_string(u)),
90                    None => buf.extend_from_slice(&0u32.to_le_bytes()),
91                }
92                (MSG_CONNECT, buf)
93            }
94            Message::ConnectOk { version } => (MSG_CONNECT_OK, encode_string(version)),
95            Message::Query { query } => (MSG_QUERY, encode_string(query)),
96            Message::ResultRows { columns, rows } => {
97                let mut buf = Vec::new();
98                buf.extend_from_slice(&(columns.len() as u16).to_le_bytes());
99                for col in columns {
100                    buf.extend_from_slice(&encode_string(col));
101                }
102                buf.extend_from_slice(&(rows.len() as u32).to_le_bytes());
103                for row in rows {
104                    for val in row {
105                        buf.extend_from_slice(&encode_string(val));
106                    }
107                }
108                (MSG_RESULT_ROWS, buf)
109            }
110            Message::ResultScalar { value } => (MSG_RESULT_SCALAR, encode_string(value)),
111            Message::ResultOk { affected } => (MSG_RESULT_OK, affected.to_le_bytes().to_vec()),
112            Message::ResultMessage { message } => (MSG_RESULT_MSG, encode_string(message)),
113            Message::Error { message } => (MSG_ERROR, encode_string(message)),
114            Message::Disconnect => (MSG_DISCONNECT, Vec::new()),
115            Message::Ping => (MSG_PING, Vec::new()),
116            Message::Pong => (MSG_PONG, Vec::new()),
117        };
118
119        let mut frame = Vec::with_capacity(6 + payload.len());
120        frame.push(msg_type);
121        frame.push(0); // flags
122        frame.extend_from_slice(&(payload.len() as u32).to_le_bytes());
123        frame.extend_from_slice(&payload);
124        frame
125    }
126
127    /// Decode message from wire format.
128    pub fn decode(data: &[u8]) -> Result<Message, String> {
129        if data.len() < 6 {
130            return Err("frame too short".into());
131        }
132        let msg_type = data[0];
133        let _flags = data[1];
134        let len_bytes: [u8; 4] = data[2..6]
135            .try_into()
136            .map_err(|_| "invalid header length field".to_string())?;
137        let payload_len = u32::from_le_bytes(len_bytes) as usize;
138        if 6 + payload_len > data.len() {
139            return Err("payload length exceeds frame".into());
140        }
141        let payload = &data[6..6 + payload_len];
142
143        match msg_type {
144            MSG_CONNECT => {
145                let mut pos = 0;
146                let db_name = decode_string(payload, &mut pos)?;
147                // Password is optional. If there are no more bytes, treat as None
148                // (backwards compatible with old clients that don't send a password).
149                let password = if pos < payload.len() {
150                    // Wrap the candidate immediately so the only owned copy of
151                    // the secret lives inside `Zeroizing` and is wiped on drop.
152                    let p = Zeroizing::new(decode_string(payload, &mut pos)?);
153                    if p.is_empty() {
154                        None
155                    } else {
156                        Some(p)
157                    }
158                } else {
159                    None
160                };
161                // Username is optional and appended after the password. Old
162                // clients omit it entirely → no more bytes → None.
163                let username = if pos < payload.len() {
164                    let u = decode_string(payload, &mut pos)?;
165                    if u.is_empty() {
166                        None
167                    } else {
168                        Some(u)
169                    }
170                } else {
171                    None
172                };
173                Ok(Message::Connect {
174                    db_name,
175                    password,
176                    username,
177                })
178            }
179            MSG_CONNECT_OK => {
180                let version = decode_string(payload, &mut 0)?;
181                Ok(Message::ConnectOk { version })
182            }
183            MSG_QUERY => {
184                let query = decode_string(payload, &mut 0)?;
185                Ok(Message::Query { query })
186            }
187            MSG_RESULT_ROWS => {
188                let mut pos = 0;
189                if pos + 2 > payload.len() {
190                    return Err("truncated column count".into());
191                }
192                let col_bytes: [u8; 2] = payload[pos..pos + 2]
193                    .try_into()
194                    .map_err(|_| "invalid column count bytes".to_string())?;
195                let col_count = u16::from_le_bytes(col_bytes) as usize;
196                pos += 2;
197                if col_count > MAX_COLUMNS {
198                    return Err("too many columns".into());
199                }
200                let mut columns = Vec::with_capacity(col_count);
201                for _ in 0..col_count {
202                    columns.push(decode_string(payload, &mut pos)?);
203                }
204                if pos + 4 > payload.len() {
205                    return Err("truncated row count".into());
206                }
207                let row_bytes: [u8; 4] = payload[pos..pos + 4]
208                    .try_into()
209                    .map_err(|_| "invalid row count bytes".to_string())?;
210                let row_count = u32::from_le_bytes(row_bytes) as usize;
211                pos += 4;
212                if row_count > MAX_ROWS {
213                    return Err("too many rows".into());
214                }
215                let mut rows = Vec::with_capacity(row_count);
216                for _ in 0..row_count {
217                    let mut row = Vec::with_capacity(col_count);
218                    for _ in 0..col_count {
219                        row.push(decode_string(payload, &mut pos)?);
220                    }
221                    rows.push(row);
222                }
223                Ok(Message::ResultRows { columns, rows })
224            }
225            MSG_RESULT_SCALAR => {
226                let value = decode_string(payload, &mut 0)?;
227                Ok(Message::ResultScalar { value })
228            }
229            MSG_RESULT_OK => {
230                if payload.len() < 8 {
231                    return Err("truncated result ok payload".into());
232                }
233                let aff_bytes: [u8; 8] = payload[0..8]
234                    .try_into()
235                    .map_err(|_| "invalid affected count bytes".to_string())?;
236                let affected = u64::from_le_bytes(aff_bytes);
237                Ok(Message::ResultOk { affected })
238            }
239            MSG_RESULT_MSG => {
240                let message = decode_string(payload, &mut 0)?;
241                Ok(Message::ResultMessage { message })
242            }
243            MSG_ERROR => {
244                let message = decode_string(payload, &mut 0)?;
245                Ok(Message::Error { message })
246            }
247            MSG_DISCONNECT => Ok(Message::Disconnect),
248            MSG_PING => Ok(Message::Ping),
249            MSG_PONG => Ok(Message::Pong),
250            _ => Err(format!("unknown message type: {msg_type:#x}")),
251        }
252    }
253
254    /// Write this message to an async writer.
255    pub async fn write_to<W: AsyncWriteExt + Unpin>(&self, writer: &mut W) -> std::io::Result<()> {
256        let bytes = self.encode();
257        writer.write_all(&bytes).await
258    }
259
260    /// Read a message from an async reader.
261    pub async fn read_from<R: AsyncReadExt + Unpin>(
262        reader: &mut R,
263    ) -> std::io::Result<Option<Message>> {
264        Self::read_from_with_limit(reader, MAX_PAYLOAD_SIZE).await
265    }
266
267    /// Read a pre-auth message with a smaller payload limit (4 KB).
268    /// Use this before authentication is complete to prevent oversized
269    /// CONNECT payloads from consuming server memory.
270    pub async fn read_from_preauth<R: AsyncReadExt + Unpin>(
271        reader: &mut R,
272    ) -> std::io::Result<Option<Message>> {
273        Self::read_from_with_limit(reader, MAX_CONNECT_PAYLOAD_SIZE).await
274    }
275
276    /// Read a message from an async reader with a configurable payload limit.
277    async fn read_from_with_limit<R: AsyncReadExt + Unpin>(
278        reader: &mut R,
279        max_payload: usize,
280    ) -> std::io::Result<Option<Message>> {
281        let mut header = [0u8; 6];
282        match reader.read_exact(&mut header).await {
283            Ok(_) => {}
284            Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None),
285            Err(e) => return Err(e),
286        }
287        let len_bytes: [u8; 4] = header[2..6].try_into().map_err(|_| {
288            std::io::Error::new(
289                std::io::ErrorKind::InvalidData,
290                "invalid header length field",
291            )
292        })?;
293        let payload_len = u32::from_le_bytes(len_bytes) as usize;
294        if payload_len > max_payload {
295            return Err(std::io::Error::new(
296                std::io::ErrorKind::InvalidData,
297                format!("payload too large: {payload_len} bytes (max {max_payload})"),
298            ));
299        }
300        let mut payload = vec![0u8; payload_len];
301        if payload_len > 0 {
302            reader.read_exact(&mut payload).await?;
303        }
304
305        let mut full = Vec::with_capacity(6 + payload_len);
306        full.extend_from_slice(&header);
307        full.extend_from_slice(&payload);
308
309        Message::decode(&full)
310            .map(Some)
311            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
312    }
313}
314
315fn encode_string(s: &str) -> Vec<u8> {
316    let mut buf = Vec::with_capacity(4 + s.len());
317    buf.extend_from_slice(&(s.len() as u32).to_le_bytes());
318    buf.extend_from_slice(s.as_bytes());
319    buf
320}
321
322fn decode_string(data: &[u8], pos: &mut usize) -> Result<String, String> {
323    if *pos + 4 > data.len() {
324        return Err("truncated string length".into());
325    }
326    let len_bytes: [u8; 4] = data[*pos..*pos + 4]
327        .try_into()
328        .map_err(|_| "invalid string length bytes".to_string())?;
329    let len = u32::from_le_bytes(len_bytes) as usize;
330    *pos += 4;
331    if *pos + len > data.len() {
332        return Err("truncated string data".into());
333    }
334    let s = String::from_utf8_lossy(&data[*pos..*pos + len]).into_owned();
335    *pos += len;
336    Ok(s)
337}
338
339#[cfg(test)]
340mod tests {
341    use super::*;
342
343    #[test]
344    fn test_encode_decode_query() {
345        let msg = Message::Query {
346            query: "User filter .age > 30".into(),
347        };
348        let bytes = msg.encode();
349        let decoded = Message::decode(&bytes).unwrap();
350        match decoded {
351            Message::Query { query } => assert_eq!(query, "User filter .age > 30"),
352            _ => panic!("expected Query"),
353        }
354    }
355
356    #[test]
357    fn test_encode_decode_connect_with_username() {
358        let msg = Message::Connect {
359            db_name: "mydb".into(),
360            password: Some(Zeroizing::new("secret".into())),
361            username: Some("alice".into()),
362        };
363        let bytes = msg.encode();
364        match Message::decode(&bytes).unwrap() {
365            Message::Connect {
366                db_name,
367                password,
368                username,
369            } => {
370                assert_eq!(db_name, "mydb");
371                assert_eq!(password.as_deref().map(|s| s.as_str()), Some("secret"));
372                assert_eq!(username.as_deref(), Some("alice"));
373            }
374            other => panic!("expected Connect, got {other:?}"),
375        }
376    }
377
378    #[test]
379    fn test_encode_decode_connect_without_username() {
380        // New-format Connect that explicitly carries no username.
381        let msg = Message::Connect {
382            db_name: "mydb".into(),
383            password: Some(Zeroizing::new("secret".into())),
384            username: None,
385        };
386        let bytes = msg.encode();
387        match Message::decode(&bytes).unwrap() {
388            Message::Connect {
389                db_name,
390                password,
391                username,
392            } => {
393                assert_eq!(db_name, "mydb");
394                assert_eq!(password.as_deref().map(|s| s.as_str()), Some("secret"));
395                assert_eq!(username, None);
396            }
397            other => panic!("expected Connect, got {other:?}"),
398        }
399    }
400
401    #[test]
402    fn test_decode_old_client_connect_db_and_password_only() {
403        // Simulate an OLD client frame: db_name + password, with NO username
404        // bytes at all. Must decode with username: None (backward compat).
405        let mut payload = encode_string("mydb");
406        payload.extend_from_slice(&encode_string("pw"));
407        let mut frame = vec![MSG_CONNECT, 0];
408        frame.extend_from_slice(&(payload.len() as u32).to_le_bytes());
409        frame.extend_from_slice(&payload);
410        match Message::decode(&frame).unwrap() {
411            Message::Connect {
412                db_name,
413                password,
414                username,
415            } => {
416                assert_eq!(db_name, "mydb");
417                assert_eq!(password.as_deref().map(|s| s.as_str()), Some("pw"));
418                assert_eq!(username, None, "old-client frame must yield username=None");
419            }
420            other => panic!("expected Connect, got {other:?}"),
421        }
422    }
423
424    #[test]
425    fn test_decode_old_client_connect_db_only() {
426        // Oldest client: db_name only, no password and no username bytes.
427        let payload = encode_string("mydb");
428        let mut frame = vec![MSG_CONNECT, 0];
429        frame.extend_from_slice(&(payload.len() as u32).to_le_bytes());
430        frame.extend_from_slice(&payload);
431        match Message::decode(&frame).unwrap() {
432            Message::Connect {
433                db_name,
434                password,
435                username,
436            } => {
437                assert_eq!(db_name, "mydb");
438                assert_eq!(password, None);
439                assert_eq!(username, None);
440            }
441            other => panic!("expected Connect, got {other:?}"),
442        }
443    }
444
445    #[test]
446    fn test_encode_decode_result_rows() {
447        let msg = Message::ResultRows {
448            columns: vec!["name".into(), "age".into()],
449            rows: vec![
450                vec!["Alice".into(), "30".into()],
451                vec!["Bob".into(), "25".into()],
452            ],
453        };
454        let bytes = msg.encode();
455        let decoded = Message::decode(&bytes).unwrap();
456        match decoded {
457            Message::ResultRows { columns, rows } => {
458                assert_eq!(columns, vec!["name", "age"]);
459                assert_eq!(rows.len(), 2);
460            }
461            _ => panic!("expected ResultRows"),
462        }
463    }
464
465    #[test]
466    fn test_encode_decode_result_message() {
467        let msg = Message::ResultMessage {
468            message: "type User created".into(),
469        };
470        let bytes = msg.encode();
471        let decoded = Message::decode(&bytes).unwrap();
472        match decoded {
473            Message::ResultMessage { message } => assert_eq!(message, "type User created"),
474            _ => panic!("expected ResultMessage"),
475        }
476    }
477
478    #[test]
479    fn test_encode_decode_error() {
480        let msg = Message::Error {
481            message: "table not found".into(),
482        };
483        let bytes = msg.encode();
484        let decoded = Message::decode(&bytes).unwrap();
485        match decoded {
486            Message::Error { message } => assert_eq!(message, "table not found"),
487            _ => panic!("expected Error"),
488        }
489    }
490
491    #[test]
492    fn test_decode_garbage_never_panics() {
493        // Feed a wide range of malformed/truncated byte sequences to the
494        // wire-protocol decode path. Every one must return Err, never panic:
495        // a malformed client message must not crash the server.
496        let cases: Vec<Vec<u8>> = vec![
497            vec![],                                   // empty
498            vec![0x03],                               // 1 byte, shorter than header
499            vec![0x03, 0x00, 0x00, 0x00, 0x00],       // 5 bytes, header truncated by one
500            vec![0xFF, 0x00, 0x00, 0x00, 0x00, 0x00], // unknown message type
501            // QUERY with payload_len far exceeding the frame.
502            vec![0x03, 0x00, 0xFF, 0xFF, 0xFF, 0xFF],
503            // CONNECT claiming a string len of 0xFFFFFFFF but no data.
504            vec![0x01, 0x00, 0x04, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF],
505            // RESULT_ROWS claiming a huge column count with no data.
506            vec![0x07, 0x00, 0x02, 0x00, 0x00, 0x00, 0xFF, 0xFF],
507            // RESULT_OK with a truncated 8-byte affected field.
508            vec![0x09, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03],
509        ];
510        for bytes in cases {
511            let result = Message::decode(&bytes);
512            assert!(
513                result.is_err(),
514                "expected Err for malformed input {bytes:?}, got {result:?}"
515            );
516        }
517    }
518
519    #[test]
520    fn test_decode_arbitrary_prefixes_never_panic() {
521        // Exhaustively walk every truncation of a well-formed frame plus a
522        // few byte mutations. None may panic.
523        let valid = Message::ResultRows {
524            columns: vec!["a".into(), "b".into()],
525            rows: vec![vec!["1".into(), "2".into()]],
526        }
527        .encode();
528        for end in 0..valid.len() {
529            // Every prefix that is not the full frame must be rejected, not panic.
530            let _ = Message::decode(&valid[..end]);
531        }
532        // Mutate each byte to a high value and confirm no panic.
533        for i in 0..valid.len() {
534            let mut m = valid.clone();
535            m[i] = 0xFF;
536            let _ = Message::decode(&m);
537        }
538    }
539
540    #[test]
541    fn test_frame_length() {
542        let msg = Message::Query {
543            query: "User".into(),
544        };
545        let bytes = msg.encode();
546        assert!(bytes.len() >= 6);
547        let payload_len = u32::from_le_bytes(bytes[2..6].try_into().unwrap()) as usize;
548        assert_eq!(bytes.len(), 6 + payload_len);
549    }
550}