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    },
39    ConnectOk {
40        version: String,
41    },
42    Query {
43        query: String,
44    },
45    ResultRows {
46        columns: Vec<String>,
47        rows: Vec<Vec<String>>,
48    },
49    ResultScalar {
50        value: String,
51    },
52    ResultOk {
53        affected: u64,
54    },
55    /// A descriptive status message (e.g. "type User created", "index dropped").
56    ResultMessage {
57        message: String,
58    },
59    Error {
60        message: String,
61    },
62    Disconnect,
63    Ping,
64    Pong,
65}
66
67impl Message {
68    /// Encode message into wire format: [type(1)][flags(1)][len(4)][payload]
69    pub fn encode(&self) -> Vec<u8> {
70        let (msg_type, payload) = match self {
71            Message::Connect { db_name, password } => {
72                let mut buf = encode_string(db_name);
73                // Password is encoded as a length-prefixed string. Empty (len=0) means None.
74                match password {
75                    Some(p) => buf.extend_from_slice(&encode_string(p)),
76                    None => buf.extend_from_slice(&0u32.to_le_bytes()),
77                }
78                (MSG_CONNECT, buf)
79            }
80            Message::ConnectOk { version } => (MSG_CONNECT_OK, encode_string(version)),
81            Message::Query { query } => (MSG_QUERY, encode_string(query)),
82            Message::ResultRows { columns, rows } => {
83                let mut buf = Vec::new();
84                buf.extend_from_slice(&(columns.len() as u16).to_le_bytes());
85                for col in columns {
86                    buf.extend_from_slice(&encode_string(col));
87                }
88                buf.extend_from_slice(&(rows.len() as u32).to_le_bytes());
89                for row in rows {
90                    for val in row {
91                        buf.extend_from_slice(&encode_string(val));
92                    }
93                }
94                (MSG_RESULT_ROWS, buf)
95            }
96            Message::ResultScalar { value } => (MSG_RESULT_SCALAR, encode_string(value)),
97            Message::ResultOk { affected } => (MSG_RESULT_OK, affected.to_le_bytes().to_vec()),
98            Message::ResultMessage { message } => (MSG_RESULT_MSG, encode_string(message)),
99            Message::Error { message } => (MSG_ERROR, encode_string(message)),
100            Message::Disconnect => (MSG_DISCONNECT, Vec::new()),
101            Message::Ping => (MSG_PING, Vec::new()),
102            Message::Pong => (MSG_PONG, Vec::new()),
103        };
104
105        let mut frame = Vec::with_capacity(6 + payload.len());
106        frame.push(msg_type);
107        frame.push(0); // flags
108        frame.extend_from_slice(&(payload.len() as u32).to_le_bytes());
109        frame.extend_from_slice(&payload);
110        frame
111    }
112
113    /// Decode message from wire format.
114    pub fn decode(data: &[u8]) -> Result<Message, String> {
115        if data.len() < 6 {
116            return Err("frame too short".into());
117        }
118        let msg_type = data[0];
119        let _flags = data[1];
120        let len_bytes: [u8; 4] = data[2..6]
121            .try_into()
122            .map_err(|_| "invalid header length field".to_string())?;
123        let payload_len = u32::from_le_bytes(len_bytes) as usize;
124        if 6 + payload_len > data.len() {
125            return Err("payload length exceeds frame".into());
126        }
127        let payload = &data[6..6 + payload_len];
128
129        match msg_type {
130            MSG_CONNECT => {
131                let mut pos = 0;
132                let db_name = decode_string(payload, &mut pos)?;
133                // Password is optional. If there are no more bytes, treat as None
134                // (backwards compatible with old clients that don't send a password).
135                let password = if pos < payload.len() {
136                    // Wrap the candidate immediately so the only owned copy of
137                    // the secret lives inside `Zeroizing` and is wiped on drop.
138                    let p = Zeroizing::new(decode_string(payload, &mut pos)?);
139                    if p.is_empty() {
140                        None
141                    } else {
142                        Some(p)
143                    }
144                } else {
145                    None
146                };
147                Ok(Message::Connect { db_name, password })
148            }
149            MSG_CONNECT_OK => {
150                let version = decode_string(payload, &mut 0)?;
151                Ok(Message::ConnectOk { version })
152            }
153            MSG_QUERY => {
154                let query = decode_string(payload, &mut 0)?;
155                Ok(Message::Query { query })
156            }
157            MSG_RESULT_ROWS => {
158                let mut pos = 0;
159                if pos + 2 > payload.len() {
160                    return Err("truncated column count".into());
161                }
162                let col_bytes: [u8; 2] = payload[pos..pos + 2]
163                    .try_into()
164                    .map_err(|_| "invalid column count bytes".to_string())?;
165                let col_count = u16::from_le_bytes(col_bytes) as usize;
166                pos += 2;
167                if col_count > MAX_COLUMNS {
168                    return Err("too many columns".into());
169                }
170                let mut columns = Vec::with_capacity(col_count);
171                for _ in 0..col_count {
172                    columns.push(decode_string(payload, &mut pos)?);
173                }
174                if pos + 4 > payload.len() {
175                    return Err("truncated row count".into());
176                }
177                let row_bytes: [u8; 4] = payload[pos..pos + 4]
178                    .try_into()
179                    .map_err(|_| "invalid row count bytes".to_string())?;
180                let row_count = u32::from_le_bytes(row_bytes) as usize;
181                pos += 4;
182                if row_count > MAX_ROWS {
183                    return Err("too many rows".into());
184                }
185                let mut rows = Vec::with_capacity(row_count);
186                for _ in 0..row_count {
187                    let mut row = Vec::with_capacity(col_count);
188                    for _ in 0..col_count {
189                        row.push(decode_string(payload, &mut pos)?);
190                    }
191                    rows.push(row);
192                }
193                Ok(Message::ResultRows { columns, rows })
194            }
195            MSG_RESULT_SCALAR => {
196                let value = decode_string(payload, &mut 0)?;
197                Ok(Message::ResultScalar { value })
198            }
199            MSG_RESULT_OK => {
200                if payload.len() < 8 {
201                    return Err("truncated result ok payload".into());
202                }
203                let aff_bytes: [u8; 8] = payload[0..8]
204                    .try_into()
205                    .map_err(|_| "invalid affected count bytes".to_string())?;
206                let affected = u64::from_le_bytes(aff_bytes);
207                Ok(Message::ResultOk { affected })
208            }
209            MSG_RESULT_MSG => {
210                let message = decode_string(payload, &mut 0)?;
211                Ok(Message::ResultMessage { message })
212            }
213            MSG_ERROR => {
214                let message = decode_string(payload, &mut 0)?;
215                Ok(Message::Error { message })
216            }
217            MSG_DISCONNECT => Ok(Message::Disconnect),
218            MSG_PING => Ok(Message::Ping),
219            MSG_PONG => Ok(Message::Pong),
220            _ => Err(format!("unknown message type: {msg_type:#x}")),
221        }
222    }
223
224    /// Write this message to an async writer.
225    pub async fn write_to<W: AsyncWriteExt + Unpin>(&self, writer: &mut W) -> std::io::Result<()> {
226        let bytes = self.encode();
227        writer.write_all(&bytes).await
228    }
229
230    /// Read a message from an async reader.
231    pub async fn read_from<R: AsyncReadExt + Unpin>(
232        reader: &mut R,
233    ) -> std::io::Result<Option<Message>> {
234        Self::read_from_with_limit(reader, MAX_PAYLOAD_SIZE).await
235    }
236
237    /// Read a pre-auth message with a smaller payload limit (4 KB).
238    /// Use this before authentication is complete to prevent oversized
239    /// CONNECT payloads from consuming server memory.
240    pub async fn read_from_preauth<R: AsyncReadExt + Unpin>(
241        reader: &mut R,
242    ) -> std::io::Result<Option<Message>> {
243        Self::read_from_with_limit(reader, MAX_CONNECT_PAYLOAD_SIZE).await
244    }
245
246    /// Read a message from an async reader with a configurable payload limit.
247    async fn read_from_with_limit<R: AsyncReadExt + Unpin>(
248        reader: &mut R,
249        max_payload: usize,
250    ) -> std::io::Result<Option<Message>> {
251        let mut header = [0u8; 6];
252        match reader.read_exact(&mut header).await {
253            Ok(_) => {}
254            Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None),
255            Err(e) => return Err(e),
256        }
257        let len_bytes: [u8; 4] = header[2..6].try_into().map_err(|_| {
258            std::io::Error::new(
259                std::io::ErrorKind::InvalidData,
260                "invalid header length field",
261            )
262        })?;
263        let payload_len = u32::from_le_bytes(len_bytes) as usize;
264        if payload_len > max_payload {
265            return Err(std::io::Error::new(
266                std::io::ErrorKind::InvalidData,
267                format!("payload too large: {payload_len} bytes (max {max_payload})"),
268            ));
269        }
270        let mut payload = vec![0u8; payload_len];
271        if payload_len > 0 {
272            reader.read_exact(&mut payload).await?;
273        }
274
275        let mut full = Vec::with_capacity(6 + payload_len);
276        full.extend_from_slice(&header);
277        full.extend_from_slice(&payload);
278
279        Message::decode(&full)
280            .map(Some)
281            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
282    }
283}
284
285fn encode_string(s: &str) -> Vec<u8> {
286    let mut buf = Vec::with_capacity(4 + s.len());
287    buf.extend_from_slice(&(s.len() as u32).to_le_bytes());
288    buf.extend_from_slice(s.as_bytes());
289    buf
290}
291
292fn decode_string(data: &[u8], pos: &mut usize) -> Result<String, String> {
293    if *pos + 4 > data.len() {
294        return Err("truncated string length".into());
295    }
296    let len_bytes: [u8; 4] = data[*pos..*pos + 4]
297        .try_into()
298        .map_err(|_| "invalid string length bytes".to_string())?;
299    let len = u32::from_le_bytes(len_bytes) as usize;
300    *pos += 4;
301    if *pos + len > data.len() {
302        return Err("truncated string data".into());
303    }
304    let s = String::from_utf8_lossy(&data[*pos..*pos + len]).into_owned();
305    *pos += len;
306    Ok(s)
307}
308
309#[cfg(test)]
310mod tests {
311    use super::*;
312
313    #[test]
314    fn test_encode_decode_query() {
315        let msg = Message::Query {
316            query: "User filter .age > 30".into(),
317        };
318        let bytes = msg.encode();
319        let decoded = Message::decode(&bytes).unwrap();
320        match decoded {
321            Message::Query { query } => assert_eq!(query, "User filter .age > 30"),
322            _ => panic!("expected Query"),
323        }
324    }
325
326    #[test]
327    fn test_encode_decode_result_rows() {
328        let msg = Message::ResultRows {
329            columns: vec!["name".into(), "age".into()],
330            rows: vec![
331                vec!["Alice".into(), "30".into()],
332                vec!["Bob".into(), "25".into()],
333            ],
334        };
335        let bytes = msg.encode();
336        let decoded = Message::decode(&bytes).unwrap();
337        match decoded {
338            Message::ResultRows { columns, rows } => {
339                assert_eq!(columns, vec!["name", "age"]);
340                assert_eq!(rows.len(), 2);
341            }
342            _ => panic!("expected ResultRows"),
343        }
344    }
345
346    #[test]
347    fn test_encode_decode_result_message() {
348        let msg = Message::ResultMessage {
349            message: "type User created".into(),
350        };
351        let bytes = msg.encode();
352        let decoded = Message::decode(&bytes).unwrap();
353        match decoded {
354            Message::ResultMessage { message } => assert_eq!(message, "type User created"),
355            _ => panic!("expected ResultMessage"),
356        }
357    }
358
359    #[test]
360    fn test_encode_decode_error() {
361        let msg = Message::Error {
362            message: "table not found".into(),
363        };
364        let bytes = msg.encode();
365        let decoded = Message::decode(&bytes).unwrap();
366        match decoded {
367            Message::Error { message } => assert_eq!(message, "table not found"),
368            _ => panic!("expected Error"),
369        }
370    }
371
372    #[test]
373    fn test_decode_garbage_never_panics() {
374        // Feed a wide range of malformed/truncated byte sequences to the
375        // wire-protocol decode path. Every one must return Err, never panic:
376        // a malformed client message must not crash the server.
377        let cases: Vec<Vec<u8>> = vec![
378            vec![],                                   // empty
379            vec![0x03],                               // 1 byte, shorter than header
380            vec![0x03, 0x00, 0x00, 0x00, 0x00],       // 5 bytes, header truncated by one
381            vec![0xFF, 0x00, 0x00, 0x00, 0x00, 0x00], // unknown message type
382            // QUERY with payload_len far exceeding the frame.
383            vec![0x03, 0x00, 0xFF, 0xFF, 0xFF, 0xFF],
384            // CONNECT claiming a string len of 0xFFFFFFFF but no data.
385            vec![0x01, 0x00, 0x04, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF],
386            // RESULT_ROWS claiming a huge column count with no data.
387            vec![0x07, 0x00, 0x02, 0x00, 0x00, 0x00, 0xFF, 0xFF],
388            // RESULT_OK with a truncated 8-byte affected field.
389            vec![0x09, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03],
390        ];
391        for bytes in cases {
392            let result = Message::decode(&bytes);
393            assert!(
394                result.is_err(),
395                "expected Err for malformed input {bytes:?}, got {result:?}"
396            );
397        }
398    }
399
400    #[test]
401    fn test_decode_arbitrary_prefixes_never_panic() {
402        // Exhaustively walk every truncation of a well-formed frame plus a
403        // few byte mutations. None may panic.
404        let valid = Message::ResultRows {
405            columns: vec!["a".into(), "b".into()],
406            rows: vec![vec!["1".into(), "2".into()]],
407        }
408        .encode();
409        for end in 0..valid.len() {
410            // Every prefix that is not the full frame must be rejected, not panic.
411            let _ = Message::decode(&valid[..end]);
412        }
413        // Mutate each byte to a high value and confirm no panic.
414        for i in 0..valid.len() {
415            let mut m = valid.clone();
416            m[i] = 0xFF;
417            let _ = Message::decode(&m);
418        }
419    }
420
421    #[test]
422    fn test_frame_length() {
423        let msg = Message::Query {
424            query: "User".into(),
425        };
426        let bytes = msg.encode();
427        assert!(bytes.len() >= 6);
428        let payload_len = u32::from_le_bytes(bytes[2..6].try_into().unwrap()) as usize;
429        assert_eq!(bytes.len(), 6 + payload_len);
430    }
431}