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_QUERY_PARAMS: u8 = 0x04;
11const 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
22const MAX_PAYLOAD_SIZE: usize = 64 * 1024 * 1024;
24
25const MAX_CONNECT_PAYLOAD_SIZE: usize = 4096;
28
29const MAX_COLUMNS: usize = 4096;
31
32const MAX_ROWS: usize = 10_000_000;
34
35const MAX_PARAMS: usize = 4096;
37
38const STRING_LEN_PREFIX: usize = 4; #[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 password: Option<Zeroizing<String>>,
63 username: Option<String>,
67 },
68 ConnectOk {
69 version: String,
70 },
71 Query {
72 query: String,
73 },
74 QuerySql {
76 query: String,
77 },
78 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 ResultMessage {
95 message: String,
96 },
97 Error {
98 message: String,
99 },
100 Disconnect,
101 Ping,
102 Pong,
103}
104
105impl Message {
106 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 match password {
117 Some(p) => buf.extend_from_slice(&encode_string(p)),
118 None => buf.extend_from_slice(&0u32.to_le_bytes()),
119 }
120 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); frame.extend_from_slice(&(payload.len() as u32).to_le_bytes());
184 frame.extend_from_slice(&payload);
185 frame
186 }
187
188 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 let password = if pos < payload.len() {
211 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 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 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 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 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 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 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 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 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 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 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!(¶ms[0], WireParam::Str(s) if s == r#"a"b\c; drop User"#));
658 assert!(matches!(¶ms[1], WireParam::Int(-7)));
659 assert!(matches!(¶ms[2], WireParam::Bool(true)));
660 assert!(matches!(¶ms[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!(¶ms[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 let cases: Vec<Vec<u8>> = vec![
686 vec![], vec![0x03], vec![0x03, 0x00, 0x00, 0x00, 0x00], vec![0xFF, 0x00, 0x00, 0x00, 0x00, 0x00], vec![0x03, 0x00, 0xFF, 0xFF, 0xFF, 0xFF],
692 vec![0x01, 0x00, 0x04, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF],
694 vec![0x07, 0x00, 0x02, 0x00, 0x00, 0x00, 0xFF, 0xFF],
696 vec![0x09, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03],
698 vec![0x04, 0x00, 0x04, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF],
700 vec![
702 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, ],
706 vec![
708 0x04, 0x00, 0x0B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x01, 0x02, 0x03,
713 ],
714 vec![
716 0x04, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, ],
722 vec![
724 0x04, 0x00, 0x0B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x63, ],
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 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 let _ = Message::decode(&valid[..end]);
751 }
752 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 let mut payload = Vec::new();
767 payload.extend_from_slice(&0u16.to_le_bytes()); payload.extend_from_slice(&10_000_000u32.to_le_bytes()); 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 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}