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