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;
7/// Query carrying positional `$N` parameters (Task 4). Pure protocol
8/// addition: old clients never send it, and old servers reject it with the
9/// existing "unknown message type" error — no existing frame changes shape.
10const MSG_QUERY_PARAMS: u8 = 0x04;
11/// SQL query frame. Plain Query remains PowQL for backward compatibility.
12const MSG_QUERY_SQL: u8 = 0x05;
13const MSG_RESULT_ROWS: u8 = 0x07;
14const MSG_RESULT_SCALAR: u8 = 0x08;
15const MSG_RESULT_OK: u8 = 0x09;
16const MSG_ERROR: u8 = 0x0A;
17const MSG_RESULT_MSG: u8 = 0x0B;
18const MSG_DISCONNECT: u8 = 0x10;
19const MSG_PING: u8 = 0x11;
20const MSG_PONG: u8 = 0x12;
21
22/// Maximum payload size accepted from the wire (64 MB).
23const MAX_PAYLOAD_SIZE: usize = 64 * 1024 * 1024;
24
25/// Maximum payload size for pre-auth CONNECT messages (4 KB).
26/// Only a database name and password are needed before authentication.
27const MAX_CONNECT_PAYLOAD_SIZE: usize = 4096;
28
29/// Maximum number of columns allowed in a result set.
30const MAX_COLUMNS: usize = 4096;
31
32/// Maximum number of rows allowed in a single result message.
33const MAX_ROWS: usize = 10_000_000;
34
35/// Maximum number of bound parameters in a single QueryWithParams message.
36const MAX_PARAMS: usize = 4096;
37
38/// A positional parameter value carried by [`Message::QueryWithParams`].
39///
40/// Wire encoding per param: a 1-byte tag followed by the body —
41///   `0` null (no body), `1` int (8B LE i64), `2` float (8B LE f64),
42///   `3` bool (1B), `4` str (length-prefixed UTF-8).
43#[derive(Debug, Clone, PartialEq)]
44pub enum WireParam {
45    Null,
46    Int(i64),
47    Float(f64),
48    Bool(bool),
49    Str(String),
50}
51
52#[derive(Debug, Clone)]
53pub enum Message {
54    Connect {
55        db_name: String,
56        /// Client-supplied candidate password. Wrapped in `Zeroizing` so the
57        /// raw bytes from the wire are wiped from memory once the `Message`
58        /// (and thus this field) is dropped after the constant-time compare —
59        /// the candidate never lingers in a plain `String`.
60        password: Option<Zeroizing<String>>,
61        /// Optional user name for multi-user authentication. Appended after the
62        /// password on the wire so the format is a pure, backward-compatible
63        /// extension: old clients that omit it decode as `None`.
64        username: Option<String>,
65    },
66    ConnectOk {
67        version: String,
68    },
69    Query {
70        query: String,
71    },
72    /// A SQL query string.
73    QuerySql {
74        query: String,
75    },
76    /// A query string with positional `$N` parameters bound at the server.
77    QueryWithParams {
78        query: String,
79        params: Vec<WireParam>,
80    },
81    ResultRows {
82        columns: Vec<String>,
83        rows: Vec<Vec<String>>,
84    },
85    ResultScalar {
86        value: String,
87    },
88    ResultOk {
89        affected: u64,
90    },
91    /// A descriptive status message (e.g. "type User created", "index dropped").
92    ResultMessage {
93        message: String,
94    },
95    Error {
96        message: String,
97    },
98    Disconnect,
99    Ping,
100    Pong,
101}
102
103impl Message {
104    /// Encode message into wire format: [type(1)][flags(1)][len(4)][payload]
105    pub fn encode(&self) -> Vec<u8> {
106        let (msg_type, payload) = match self {
107            Message::Connect {
108                db_name,
109                password,
110                username,
111            } => {
112                let mut buf = encode_string(db_name);
113                // Password is encoded as a length-prefixed string. Empty (len=0) means None.
114                match password {
115                    Some(p) => buf.extend_from_slice(&encode_string(p)),
116                    None => buf.extend_from_slice(&0u32.to_le_bytes()),
117                }
118                // Username is appended after the password (append-only extension).
119                // Length-prefixed string; empty (len=0) means None.
120                match username {
121                    Some(u) => buf.extend_from_slice(&encode_string(u)),
122                    None => buf.extend_from_slice(&0u32.to_le_bytes()),
123                }
124                (MSG_CONNECT, buf)
125            }
126            Message::ConnectOk { version } => (MSG_CONNECT_OK, encode_string(version)),
127            Message::Query { query } => (MSG_QUERY, encode_string(query)),
128            Message::QuerySql { query } => (MSG_QUERY_SQL, encode_string(query)),
129            Message::QueryWithParams { query, params } => {
130                let mut buf = encode_string(query);
131                buf.extend_from_slice(&(params.len() as u16).to_le_bytes());
132                for p in params {
133                    match p {
134                        WireParam::Null => buf.push(0),
135                        WireParam::Int(v) => {
136                            buf.push(1);
137                            buf.extend_from_slice(&v.to_le_bytes());
138                        }
139                        WireParam::Float(v) => {
140                            buf.push(2);
141                            buf.extend_from_slice(&v.to_le_bytes());
142                        }
143                        WireParam::Bool(v) => {
144                            buf.push(3);
145                            buf.push(if *v { 1 } else { 0 });
146                        }
147                        WireParam::Str(s) => {
148                            buf.push(4);
149                            buf.extend_from_slice(&encode_string(s));
150                        }
151                    }
152                }
153                (MSG_QUERY_PARAMS, buf)
154            }
155            Message::ResultRows { columns, rows } => {
156                let mut buf = Vec::new();
157                buf.extend_from_slice(&(columns.len() as u16).to_le_bytes());
158                for col in columns {
159                    buf.extend_from_slice(&encode_string(col));
160                }
161                buf.extend_from_slice(&(rows.len() as u32).to_le_bytes());
162                for row in rows {
163                    for val in row {
164                        buf.extend_from_slice(&encode_string(val));
165                    }
166                }
167                (MSG_RESULT_ROWS, buf)
168            }
169            Message::ResultScalar { value } => (MSG_RESULT_SCALAR, encode_string(value)),
170            Message::ResultOk { affected } => (MSG_RESULT_OK, affected.to_le_bytes().to_vec()),
171            Message::ResultMessage { message } => (MSG_RESULT_MSG, encode_string(message)),
172            Message::Error { message } => (MSG_ERROR, encode_string(message)),
173            Message::Disconnect => (MSG_DISCONNECT, Vec::new()),
174            Message::Ping => (MSG_PING, Vec::new()),
175            Message::Pong => (MSG_PONG, Vec::new()),
176        };
177
178        let mut frame = Vec::with_capacity(6 + payload.len());
179        frame.push(msg_type);
180        frame.push(0); // flags
181        frame.extend_from_slice(&(payload.len() as u32).to_le_bytes());
182        frame.extend_from_slice(&payload);
183        frame
184    }
185
186    /// Decode message from wire format.
187    pub fn decode(data: &[u8]) -> Result<Message, String> {
188        if data.len() < 6 {
189            return Err("frame too short".into());
190        }
191        let msg_type = data[0];
192        let _flags = data[1];
193        let len_bytes: [u8; 4] = data[2..6]
194            .try_into()
195            .map_err(|_| "invalid header length field".to_string())?;
196        let payload_len = u32::from_le_bytes(len_bytes) as usize;
197        if 6 + payload_len > data.len() {
198            return Err("payload length exceeds frame".into());
199        }
200        let payload = &data[6..6 + payload_len];
201
202        match msg_type {
203            MSG_CONNECT => {
204                let mut pos = 0;
205                let db_name = decode_string(payload, &mut pos)?;
206                // Password is optional. If there are no more bytes, treat as None
207                // (backwards compatible with old clients that don't send a password).
208                let password = if pos < payload.len() {
209                    // Wrap the candidate immediately so the only owned copy of
210                    // the secret lives inside `Zeroizing` and is wiped on drop.
211                    let p = Zeroizing::new(decode_string(payload, &mut pos)?);
212                    if p.is_empty() {
213                        None
214                    } else {
215                        Some(p)
216                    }
217                } else {
218                    None
219                };
220                // Username is optional and appended after the password. Old
221                // clients omit it entirely → no more bytes → None.
222                let username = if pos < payload.len() {
223                    let u = decode_string(payload, &mut pos)?;
224                    if u.is_empty() {
225                        None
226                    } else {
227                        Some(u)
228                    }
229                } else {
230                    None
231                };
232                Ok(Message::Connect {
233                    db_name,
234                    password,
235                    username,
236                })
237            }
238            MSG_CONNECT_OK => {
239                let version = decode_string(payload, &mut 0)?;
240                Ok(Message::ConnectOk { version })
241            }
242            MSG_QUERY => {
243                let query = decode_string(payload, &mut 0)?;
244                Ok(Message::Query { query })
245            }
246            MSG_QUERY_SQL => {
247                let query = decode_string(payload, &mut 0)?;
248                Ok(Message::QuerySql { query })
249            }
250            MSG_QUERY_PARAMS => {
251                let mut pos = 0;
252                let query = decode_string(payload, &mut pos)?;
253                if pos + 2 > payload.len() {
254                    return Err("truncated param count".into());
255                }
256                let count_bytes: [u8; 2] = payload[pos..pos + 2]
257                    .try_into()
258                    .map_err(|_| "invalid param count bytes".to_string())?;
259                let count = u16::from_le_bytes(count_bytes) as usize;
260                pos += 2;
261                if count > MAX_PARAMS {
262                    return Err("too many parameters".into());
263                }
264                let mut params = Vec::with_capacity(count);
265                for _ in 0..count {
266                    if pos >= payload.len() {
267                        return Err("truncated param tag".into());
268                    }
269                    let tag = payload[pos];
270                    pos += 1;
271                    let p = match tag {
272                        0 => WireParam::Null,
273                        1 => {
274                            if pos + 8 > payload.len() {
275                                return Err("truncated int param".into());
276                            }
277                            let b: [u8; 8] = payload[pos..pos + 8]
278                                .try_into()
279                                .map_err(|_| "invalid int param bytes".to_string())?;
280                            pos += 8;
281                            WireParam::Int(i64::from_le_bytes(b))
282                        }
283                        2 => {
284                            if pos + 8 > payload.len() {
285                                return Err("truncated float param".into());
286                            }
287                            let b: [u8; 8] = payload[pos..pos + 8]
288                                .try_into()
289                                .map_err(|_| "invalid float param bytes".to_string())?;
290                            pos += 8;
291                            WireParam::Float(f64::from_le_bytes(b))
292                        }
293                        3 => {
294                            if pos + 1 > payload.len() {
295                                return Err("truncated bool param".into());
296                            }
297                            let v = payload[pos] != 0;
298                            pos += 1;
299                            WireParam::Bool(v)
300                        }
301                        4 => WireParam::Str(decode_string(payload, &mut pos)?),
302                        other => return Err(format!("unknown param tag: {other}")),
303                    };
304                    params.push(p);
305                }
306                Ok(Message::QueryWithParams { query, params })
307            }
308            MSG_RESULT_ROWS => {
309                let mut pos = 0;
310                if pos + 2 > payload.len() {
311                    return Err("truncated column count".into());
312                }
313                let col_bytes: [u8; 2] = payload[pos..pos + 2]
314                    .try_into()
315                    .map_err(|_| "invalid column count bytes".to_string())?;
316                let col_count = u16::from_le_bytes(col_bytes) as usize;
317                pos += 2;
318                if col_count > MAX_COLUMNS {
319                    return Err("too many columns".into());
320                }
321                let mut columns = Vec::with_capacity(col_count);
322                for _ in 0..col_count {
323                    columns.push(decode_string(payload, &mut pos)?);
324                }
325                if pos + 4 > payload.len() {
326                    return Err("truncated row count".into());
327                }
328                let row_bytes: [u8; 4] = payload[pos..pos + 4]
329                    .try_into()
330                    .map_err(|_| "invalid row count bytes".to_string())?;
331                let row_count = u32::from_le_bytes(row_bytes) as usize;
332                pos += 4;
333                if row_count > MAX_ROWS {
334                    return Err("too many rows".into());
335                }
336                let mut rows = Vec::with_capacity(row_count);
337                for _ in 0..row_count {
338                    let mut row = Vec::with_capacity(col_count);
339                    for _ in 0..col_count {
340                        row.push(decode_string(payload, &mut pos)?);
341                    }
342                    rows.push(row);
343                }
344                Ok(Message::ResultRows { columns, rows })
345            }
346            MSG_RESULT_SCALAR => {
347                let value = decode_string(payload, &mut 0)?;
348                Ok(Message::ResultScalar { value })
349            }
350            MSG_RESULT_OK => {
351                if payload.len() < 8 {
352                    return Err("truncated result ok payload".into());
353                }
354                let aff_bytes: [u8; 8] = payload[0..8]
355                    .try_into()
356                    .map_err(|_| "invalid affected count bytes".to_string())?;
357                let affected = u64::from_le_bytes(aff_bytes);
358                Ok(Message::ResultOk { affected })
359            }
360            MSG_RESULT_MSG => {
361                let message = decode_string(payload, &mut 0)?;
362                Ok(Message::ResultMessage { message })
363            }
364            MSG_ERROR => {
365                let message = decode_string(payload, &mut 0)?;
366                Ok(Message::Error { message })
367            }
368            MSG_DISCONNECT => Ok(Message::Disconnect),
369            MSG_PING => Ok(Message::Ping),
370            MSG_PONG => Ok(Message::Pong),
371            _ => Err(format!("unknown message type: {msg_type:#x}")),
372        }
373    }
374
375    /// Write this message to an async writer.
376    pub async fn write_to<W: AsyncWriteExt + Unpin>(&self, writer: &mut W) -> std::io::Result<()> {
377        let bytes = self.encode();
378        writer.write_all(&bytes).await
379    }
380
381    /// Read a message from an async reader.
382    pub async fn read_from<R: AsyncReadExt + Unpin>(
383        reader: &mut R,
384    ) -> std::io::Result<Option<Message>> {
385        Self::read_from_with_limit(reader, MAX_PAYLOAD_SIZE).await
386    }
387
388    /// Read a pre-auth message with a smaller payload limit (4 KB).
389    /// Use this before authentication is complete to prevent oversized
390    /// CONNECT payloads from consuming server memory.
391    pub async fn read_from_preauth<R: AsyncReadExt + Unpin>(
392        reader: &mut R,
393    ) -> std::io::Result<Option<Message>> {
394        Self::read_from_with_limit(reader, MAX_CONNECT_PAYLOAD_SIZE).await
395    }
396
397    /// Read a message from an async reader with a configurable payload limit.
398    async fn read_from_with_limit<R: AsyncReadExt + Unpin>(
399        reader: &mut R,
400        max_payload: usize,
401    ) -> std::io::Result<Option<Message>> {
402        let mut header = [0u8; 6];
403        match reader.read_exact(&mut header).await {
404            Ok(_) => {}
405            Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None),
406            Err(e) => return Err(e),
407        }
408        let len_bytes: [u8; 4] = header[2..6].try_into().map_err(|_| {
409            std::io::Error::new(
410                std::io::ErrorKind::InvalidData,
411                "invalid header length field",
412            )
413        })?;
414        let payload_len = u32::from_le_bytes(len_bytes) as usize;
415        if payload_len > max_payload {
416            return Err(std::io::Error::new(
417                std::io::ErrorKind::InvalidData,
418                format!("payload too large: {payload_len} bytes (max {max_payload})"),
419            ));
420        }
421        let mut payload = vec![0u8; payload_len];
422        if payload_len > 0 {
423            reader.read_exact(&mut payload).await?;
424        }
425
426        let mut full = Vec::with_capacity(6 + payload_len);
427        full.extend_from_slice(&header);
428        full.extend_from_slice(&payload);
429
430        Message::decode(&full)
431            .map(Some)
432            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
433    }
434}
435
436fn encode_string(s: &str) -> Vec<u8> {
437    let mut buf = Vec::with_capacity(4 + s.len());
438    buf.extend_from_slice(&(s.len() as u32).to_le_bytes());
439    buf.extend_from_slice(s.as_bytes());
440    buf
441}
442
443fn decode_string(data: &[u8], pos: &mut usize) -> Result<String, String> {
444    if *pos + 4 > data.len() {
445        return Err("truncated string length".into());
446    }
447    let len_bytes: [u8; 4] = data[*pos..*pos + 4]
448        .try_into()
449        .map_err(|_| "invalid string length bytes".to_string())?;
450    let len = u32::from_le_bytes(len_bytes) as usize;
451    *pos += 4;
452    if *pos + len > data.len() {
453        return Err("truncated string data".into());
454    }
455    let s = String::from_utf8_lossy(&data[*pos..*pos + len]).into_owned();
456    *pos += len;
457    Ok(s)
458}
459
460#[cfg(test)]
461mod tests {
462    use super::*;
463
464    #[test]
465    fn test_encode_decode_query() {
466        let msg = Message::Query {
467            query: "User filter .age > 30".into(),
468        };
469        let bytes = msg.encode();
470        let decoded = Message::decode(&bytes).unwrap();
471        match decoded {
472            Message::Query { query } => assert_eq!(query, "User filter .age > 30"),
473            _ => panic!("expected Query"),
474        }
475    }
476
477    #[test]
478    fn test_encode_decode_connect_with_username() {
479        let msg = Message::Connect {
480            db_name: "mydb".into(),
481            password: Some(Zeroizing::new("secret".into())),
482            username: Some("alice".into()),
483        };
484        let bytes = msg.encode();
485        match Message::decode(&bytes).unwrap() {
486            Message::Connect {
487                db_name,
488                password,
489                username,
490            } => {
491                assert_eq!(db_name, "mydb");
492                assert_eq!(password.as_deref().map(|s| s.as_str()), Some("secret"));
493                assert_eq!(username.as_deref(), Some("alice"));
494            }
495            other => panic!("expected Connect, got {other:?}"),
496        }
497    }
498
499    #[test]
500    fn test_encode_decode_connect_without_username() {
501        // New-format Connect that explicitly carries no username.
502        let msg = Message::Connect {
503            db_name: "mydb".into(),
504            password: Some(Zeroizing::new("secret".into())),
505            username: None,
506        };
507        let bytes = msg.encode();
508        match Message::decode(&bytes).unwrap() {
509            Message::Connect {
510                db_name,
511                password,
512                username,
513            } => {
514                assert_eq!(db_name, "mydb");
515                assert_eq!(password.as_deref().map(|s| s.as_str()), Some("secret"));
516                assert_eq!(username, None);
517            }
518            other => panic!("expected Connect, got {other:?}"),
519        }
520    }
521
522    #[test]
523    fn test_decode_old_client_connect_db_and_password_only() {
524        // Simulate an OLD client frame: db_name + password, with NO username
525        // bytes at all. Must decode with username: None (backward compat).
526        let mut payload = encode_string("mydb");
527        payload.extend_from_slice(&encode_string("pw"));
528        let mut frame = vec![MSG_CONNECT, 0];
529        frame.extend_from_slice(&(payload.len() as u32).to_le_bytes());
530        frame.extend_from_slice(&payload);
531        match Message::decode(&frame).unwrap() {
532            Message::Connect {
533                db_name,
534                password,
535                username,
536            } => {
537                assert_eq!(db_name, "mydb");
538                assert_eq!(password.as_deref().map(|s| s.as_str()), Some("pw"));
539                assert_eq!(username, None, "old-client frame must yield username=None");
540            }
541            other => panic!("expected Connect, got {other:?}"),
542        }
543    }
544
545    #[test]
546    fn test_decode_old_client_connect_db_only() {
547        // Oldest client: db_name only, no password and no username bytes.
548        let payload = encode_string("mydb");
549        let mut frame = vec![MSG_CONNECT, 0];
550        frame.extend_from_slice(&(payload.len() as u32).to_le_bytes());
551        frame.extend_from_slice(&payload);
552        match Message::decode(&frame).unwrap() {
553            Message::Connect {
554                db_name,
555                password,
556                username,
557            } => {
558                assert_eq!(db_name, "mydb");
559                assert_eq!(password, None);
560                assert_eq!(username, None);
561            }
562            other => panic!("expected Connect, got {other:?}"),
563        }
564    }
565
566    #[test]
567    fn test_encode_decode_result_rows() {
568        let msg = Message::ResultRows {
569            columns: vec!["name".into(), "age".into()],
570            rows: vec![
571                vec!["Alice".into(), "30".into()],
572                vec!["Bob".into(), "25".into()],
573            ],
574        };
575        let bytes = msg.encode();
576        let decoded = Message::decode(&bytes).unwrap();
577        match decoded {
578            Message::ResultRows { columns, rows } => {
579                assert_eq!(columns, vec!["name", "age"]);
580                assert_eq!(rows.len(), 2);
581            }
582            _ => panic!("expected ResultRows"),
583        }
584    }
585
586    #[test]
587    fn test_encode_decode_result_message() {
588        let msg = Message::ResultMessage {
589            message: "type User created".into(),
590        };
591        let bytes = msg.encode();
592        let decoded = Message::decode(&bytes).unwrap();
593        match decoded {
594            Message::ResultMessage { message } => assert_eq!(message, "type User created"),
595            _ => panic!("expected ResultMessage"),
596        }
597    }
598
599    #[test]
600    fn test_encode_decode_error() {
601        let msg = Message::Error {
602            message: "table not found".into(),
603        };
604        let bytes = msg.encode();
605        let decoded = Message::decode(&bytes).unwrap();
606        match decoded {
607            Message::Error { message } => assert_eq!(message, "table not found"),
608            _ => panic!("expected Error"),
609        }
610    }
611
612    #[test]
613    fn test_encode_decode_query_sql() {
614        let msg = Message::QuerySql {
615            query: "SELECT * FROM User".into(),
616        };
617        let decoded = Message::decode(&msg.encode()).unwrap();
618        match decoded {
619            Message::QuerySql { query } => assert_eq!(query, "SELECT * FROM User"),
620            other => panic!("expected QuerySql, got {other:?}"),
621        }
622    }
623
624    #[test]
625    fn test_encode_decode_query_with_params() {
626        let msg = Message::QueryWithParams {
627            query: "insert User { name := $1, age := $2, ok := $3, note := $4 }".into(),
628            params: vec![
629                WireParam::Str(r#"a"b\c; drop User"#.into()),
630                WireParam::Int(-7),
631                WireParam::Bool(true),
632                WireParam::Null,
633            ],
634        };
635        let bytes = msg.encode();
636        // The new frame must use the dedicated 0x04 tag.
637        assert_eq!(bytes[0], 0x04);
638        match Message::decode(&bytes).unwrap() {
639            Message::QueryWithParams { query, params } => {
640                assert!(query.contains("$1"));
641                assert_eq!(params.len(), 4);
642                assert!(matches!(&params[0], WireParam::Str(s) if s == r#"a"b\c; drop User"#));
643                assert!(matches!(&params[1], WireParam::Int(-7)));
644                assert!(matches!(&params[2], WireParam::Bool(true)));
645                assert!(matches!(&params[3], WireParam::Null));
646            }
647            other => panic!("expected QueryWithParams, got {other:?}"),
648        }
649    }
650
651    #[test]
652    fn test_query_with_params_float_round_trip() {
653        let msg = Message::QueryWithParams {
654            query: "T filter .f = $1".into(),
655            params: vec![WireParam::Float(2.5)],
656        };
657        match Message::decode(&msg.encode()).unwrap() {
658            Message::QueryWithParams { params, .. } => {
659                assert!(matches!(&params[0], WireParam::Float(f) if (*f - 2.5).abs() < 1e-12));
660            }
661            other => panic!("expected QueryWithParams, got {other:?}"),
662        }
663    }
664
665    #[test]
666    fn test_decode_garbage_never_panics() {
667        // Feed a wide range of malformed/truncated byte sequences to the
668        // wire-protocol decode path. Every one must return Err, never panic:
669        // a malformed client message must not crash the server.
670        let cases: Vec<Vec<u8>> = vec![
671            vec![],                                   // empty
672            vec![0x03],                               // 1 byte, shorter than header
673            vec![0x03, 0x00, 0x00, 0x00, 0x00],       // 5 bytes, header truncated by one
674            vec![0xFF, 0x00, 0x00, 0x00, 0x00, 0x00], // unknown message type
675            // QUERY with payload_len far exceeding the frame.
676            vec![0x03, 0x00, 0xFF, 0xFF, 0xFF, 0xFF],
677            // CONNECT claiming a string len of 0xFFFFFFFF but no data.
678            vec![0x01, 0x00, 0x04, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF],
679            // RESULT_ROWS claiming a huge column count with no data.
680            vec![0x07, 0x00, 0x02, 0x00, 0x00, 0x00, 0xFF, 0xFF],
681            // RESULT_OK with a truncated 8-byte affected field.
682            vec![0x09, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03],
683            // QUERY_PARAMS (0x04) claiming a query string len with no data.
684            vec![0x04, 0x00, 0x04, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF],
685            // QUERY_PARAMS: empty query string, claims 1 param, no param bytes.
686            vec![
687                0x04, 0x00, 0x06, 0x00, 0x00, 0x00, // header, payload_len=6
688                0x00, 0x00, 0x00, 0x00, // query string len = 0
689                0x01, 0x00, // param count = 1, then nothing
690            ],
691            // QUERY_PARAMS: 1 int param with a truncated i64 body.
692            vec![
693                0x04, 0x00, 0x0B, 0x00, 0x00, 0x00, // header, payload_len=11
694                0x00, 0x00, 0x00, 0x00, // query len = 0
695                0x01, 0x00, // param count = 1
696                0x01, // tag = int, then only 3 of 8 bytes
697                0x01, 0x02, 0x03,
698            ],
699            // QUERY_PARAMS: 1 str param with a truncated string body.
700            vec![
701                0x04, 0x00, 0x0F, 0x00, 0x00, 0x00, // header, payload_len=15
702                0x00, 0x00, 0x00, 0x00, // query len = 0
703                0x01, 0x00, // param count = 1
704                0x04, // tag = str
705                0xFF, 0xFF, 0xFF, 0xFF, // str len huge, no data
706            ],
707            // QUERY_PARAMS: unknown param tag byte.
708            vec![
709                0x04, 0x00, 0x0B, 0x00, 0x00, 0x00, // header, payload_len=11
710                0x00, 0x00, 0x00, 0x00, // query len = 0
711                0x01, 0x00, // param count = 1
712                0x63, // bogus tag
713            ],
714        ];
715        for bytes in cases {
716            let result = Message::decode(&bytes);
717            assert!(
718                result.is_err(),
719                "expected Err for malformed input {bytes:?}, got {result:?}"
720            );
721        }
722    }
723
724    #[test]
725    fn test_decode_arbitrary_prefixes_never_panic() {
726        // Exhaustively walk every truncation of a well-formed frame plus a
727        // few byte mutations. None may panic.
728        let valid = Message::ResultRows {
729            columns: vec!["a".into(), "b".into()],
730            rows: vec![vec!["1".into(), "2".into()]],
731        }
732        .encode();
733        for end in 0..valid.len() {
734            // Every prefix that is not the full frame must be rejected, not panic.
735            let _ = Message::decode(&valid[..end]);
736        }
737        // Mutate each byte to a high value and confirm no panic.
738        for i in 0..valid.len() {
739            let mut m = valid.clone();
740            m[i] = 0xFF;
741            let _ = Message::decode(&m);
742        }
743    }
744
745    #[test]
746    fn test_frame_length() {
747        let msg = Message::Query {
748            query: "User".into(),
749        };
750        let bytes = msg.encode();
751        assert!(bytes.len() >= 6);
752        let payload_len = u32::from_le_bytes(bytes[2..6].try_into().unwrap()) as usize;
753        assert_eq!(bytes.len(), 6 + payload_len);
754    }
755}