1use std::io;
21
22use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
23
24pub const PG_PROTOCOL_V3: u32 = 3 << 16;
26
27pub const PG_SSL_REQUEST: u32 = 80877103;
31pub const PG_GSSENC_REQUEST: u32 = 80877104;
32pub const PG_CANCEL_REQUEST: u32 = 80877102;
33
34#[derive(Debug)]
37pub enum PgWireError {
38 Io(io::Error),
39 Protocol(String),
40 Eof,
42}
43
44impl From<io::Error> for PgWireError {
45 fn from(err: io::Error) -> Self {
46 if err.kind() == io::ErrorKind::UnexpectedEof {
47 PgWireError::Eof
48 } else {
49 PgWireError::Io(err)
50 }
51 }
52}
53
54impl std::fmt::Display for PgWireError {
55 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56 match self {
57 PgWireError::Io(e) => write!(f, "pg wire io: {e}"),
58 PgWireError::Protocol(m) => write!(f, "pg wire protocol: {m}"),
59 PgWireError::Eof => write!(f, "pg wire eof"),
60 }
61 }
62}
63
64impl std::error::Error for PgWireError {}
65
66#[derive(Debug, Clone)]
68pub enum FrontendMessage {
69 Startup(StartupParams),
71 SslRequest,
73 GssEncRequest,
75 Query(String),
77 Parse(ParseMessage),
79 Bind(BindMessage),
81 Describe(DescribeMessage),
83 Execute(ExecuteMessage),
85 Close(CloseMessage),
87 PasswordMessage(Vec<u8>),
90 Terminate,
92 Flush,
94 Sync,
96 Unknown { tag: u8, payload: Vec<u8> },
99}
100
101#[derive(Debug, Clone)]
102pub struct ParseMessage {
103 pub statement: String,
104 pub query: String,
105 pub param_type_oids: Vec<u32>,
106}
107
108#[derive(Debug, Clone)]
109pub struct BindMessage {
110 pub portal: String,
111 pub statement: String,
112 pub param_format_codes: Vec<i16>,
113 pub params: Vec<Option<Vec<u8>>>,
114 pub result_format_codes: Vec<i16>,
115}
116
117#[derive(Debug, Clone)]
118pub struct DescribeMessage {
119 pub target: DescribeTarget,
120 pub name: String,
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub enum DescribeTarget {
125 Statement,
126 Portal,
127}
128
129#[derive(Debug, Clone)]
130pub struct ExecuteMessage {
131 pub portal: String,
132 pub max_rows: u32,
133}
134
135#[derive(Debug, Clone)]
136pub struct CloseMessage {
137 pub target: DescribeTarget,
138 pub name: String,
139}
140
141#[derive(Debug, Clone, Default)]
142pub struct StartupParams {
143 pub params: Vec<(String, String)>,
145}
146
147impl StartupParams {
148 pub fn get(&self, key: &str) -> Option<&str> {
149 self.params
150 .iter()
151 .find(|(k, _)| k == key)
152 .map(|(_, v)| v.as_str())
153 }
154}
155
156#[derive(Debug, Clone)]
158pub enum BackendMessage {
159 AuthenticationOk,
161 AuthenticationCleartextPassword,
165 ParameterStatus { name: String, value: String },
167 BackendKeyData { pid: u32, key: u32 },
169 ReadyForQuery(TransactionStatus),
171 RowDescription(Vec<ColumnDescriptor>),
173 DataRow(Vec<Option<Vec<u8>>>),
175 CommandComplete(String),
177 ParseComplete,
179 BindComplete,
181 CloseComplete,
183 ParameterDescription(Vec<u32>),
185 PortalSuspended,
187 NoData,
189 ErrorResponse {
191 severity: String,
192 code: String,
193 message: String,
194 },
195 NoticeResponse { message: String },
197 EmptyQueryResponse,
199}
200
201#[derive(Debug, Clone, Copy, PartialEq, Eq)]
202pub enum TransactionStatus {
203 Idle,
205 InTransaction,
207 Failed,
209}
210
211impl TransactionStatus {
212 pub fn as_byte(self) -> u8 {
213 match self {
214 TransactionStatus::Idle => b'I',
215 TransactionStatus::InTransaction => b'T',
216 TransactionStatus::Failed => b'E',
217 }
218 }
219}
220
221#[derive(Debug, Clone)]
222pub struct ColumnDescriptor {
223 pub name: String,
224 pub table_oid: u32,
226 pub column_attr: i16,
228 pub type_oid: u32,
230 pub type_size: i16,
232 pub type_mod: i32,
234 pub format: i16,
236}
237
238pub async fn read_startup<R: AsyncRead + Unpin>(
246 stream: &mut R,
247) -> Result<FrontendMessage, PgWireError> {
248 let mut len_buf = [0u8; 4];
249 stream.read_exact(&mut len_buf).await?;
250 let len = u32::from_be_bytes(len_buf);
251 if !(8..=65536).contains(&len) {
252 return Err(PgWireError::Protocol(format!(
253 "startup length {len} out of range"
254 )));
255 }
256 let body_len = (len as usize) - 4;
257 let mut body = vec![0u8; body_len];
258 stream.read_exact(&mut body).await?;
259 if body_len < 4 {
260 return Err(PgWireError::Protocol("startup payload too short".into()));
261 }
262 let version = u32::from_be_bytes([body[0], body[1], body[2], body[3]]);
263
264 match version {
265 PG_SSL_REQUEST => Ok(FrontendMessage::SslRequest),
266 PG_GSSENC_REQUEST => Ok(FrontendMessage::GssEncRequest),
267 PG_PROTOCOL_V3 => {
268 let mut params: Vec<(String, String)> = Vec::new();
271 let mut pos = 4usize;
272 while pos < body_len {
273 if body[pos] == 0 {
274 break;
275 }
276 let key = read_cstring(&body, &mut pos)?;
277 if pos >= body_len {
278 return Err(PgWireError::Protocol(
279 "startup parameter missing value".into(),
280 ));
281 }
282 let value = read_cstring(&body, &mut pos)?;
283 params.push((key, value));
284 }
285 Ok(FrontendMessage::Startup(StartupParams { params }))
286 }
287 PG_CANCEL_REQUEST => Ok(FrontendMessage::Unknown {
290 tag: b'K',
291 payload: body,
292 }),
293 _ => Err(PgWireError::Protocol(format!(
294 "unsupported protocol version {version}"
295 ))),
296 }
297}
298
299pub async fn read_frame<R: AsyncRead + Unpin>(
301 stream: &mut R,
302) -> Result<FrontendMessage, PgWireError> {
303 let mut tag_buf = [0u8; 1];
304 match stream.read_exact(&mut tag_buf).await {
305 Ok(_) => {}
306 Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Err(PgWireError::Eof),
307 Err(e) => return Err(PgWireError::Io(e)),
308 }
309 let tag = tag_buf[0];
310
311 let mut len_buf = [0u8; 4];
312 stream.read_exact(&mut len_buf).await?;
313 let len = u32::from_be_bytes(len_buf);
314 if !(4..=1_048_576).contains(&len) {
315 return Err(PgWireError::Protocol(format!(
316 "frame length {len} out of bounds"
317 )));
318 }
319 let payload_len = (len as usize) - 4;
320 let mut payload = vec![0u8; payload_len];
321 stream.read_exact(&mut payload).await?;
322
323 Ok(match tag {
324 b'Q' => {
325 let mut pos = 0;
327 let query = read_cstring(&payload, &mut pos)?;
328 FrontendMessage::Query(query)
329 }
330 b'P' => FrontendMessage::Parse(parse_parse_message(&payload)?),
331 b'B' => FrontendMessage::Bind(parse_bind_message(&payload)?),
332 b'D' => FrontendMessage::Describe(parse_describe_message(&payload)?),
333 b'E' => FrontendMessage::Execute(parse_execute_message(&payload)?),
334 b'C' => FrontendMessage::Close(parse_close_message(&payload)?),
335 b'p' => FrontendMessage::PasswordMessage(payload),
336 b'X' => FrontendMessage::Terminate,
337 b'H' => FrontendMessage::Flush,
338 b'S' => FrontendMessage::Sync,
339 other => FrontendMessage::Unknown {
340 tag: other,
341 payload,
342 },
343 })
344}
345
346pub async fn write_raw_byte<W: AsyncWrite + Unpin>(
353 stream: &mut W,
354 byte: u8,
355) -> Result<(), PgWireError> {
356 stream.write_all(&[byte]).await?;
357 Ok(())
358}
359
360pub async fn write_frame<W: AsyncWrite + Unpin>(
362 stream: &mut W,
363 msg: &BackendMessage,
364) -> Result<(), PgWireError> {
365 let (tag, payload) = encode_backend(msg);
366 let length = (payload.len() + 4) as u32;
368 stream.write_all(&[tag]).await?;
369 stream.write_all(&length.to_be_bytes()).await?;
370 stream.write_all(&payload).await?;
371 Ok(())
372}
373
374fn sanitize_cstring_bytes(input: &[u8]) -> Vec<u8> {
390 if !input.contains(&0) {
391 return input.to_vec();
392 }
393 let mut out = Vec::with_capacity(input.len() + 8);
394 for &b in input {
395 if b == 0 {
396 out.extend_from_slice(&[0xEF, 0xBF, 0xBD]);
398 } else {
399 out.push(b);
400 }
401 }
402 out
403}
404
405#[inline]
406fn push_cstring(buf: &mut Vec<u8>, value: &str) {
407 buf.extend_from_slice(&sanitize_cstring_bytes(value.as_bytes()));
408 buf.push(0);
409}
410
411fn encode_backend(msg: &BackendMessage) -> (u8, Vec<u8>) {
412 match msg {
413 BackendMessage::AuthenticationOk => {
414 (b'R', vec![0, 0, 0, 0])
416 }
417 BackendMessage::AuthenticationCleartextPassword => {
418 (b'R', vec![0, 0, 0, 3])
420 }
421 BackendMessage::ParameterStatus { name, value } => {
422 let mut buf = Vec::with_capacity(name.len() + value.len() + 2);
423 push_cstring(&mut buf, name);
425 push_cstring(&mut buf, value);
426 (b'S', buf)
427 }
428 BackendMessage::BackendKeyData { pid, key } => {
429 let mut buf = Vec::with_capacity(8);
430 buf.extend_from_slice(&pid.to_be_bytes());
431 buf.extend_from_slice(&key.to_be_bytes());
432 (b'K', buf)
433 }
434 BackendMessage::ReadyForQuery(status) => (b'Z', vec![status.as_byte()]),
435 BackendMessage::RowDescription(cols) => {
436 let mut buf = Vec::new();
437 buf.extend_from_slice(&(cols.len() as i16).to_be_bytes());
438 for col in cols {
439 push_cstring(&mut buf, &col.name);
441 buf.extend_from_slice(&col.table_oid.to_be_bytes());
442 buf.extend_from_slice(&col.column_attr.to_be_bytes());
443 buf.extend_from_slice(&col.type_oid.to_be_bytes());
444 buf.extend_from_slice(&col.type_size.to_be_bytes());
445 buf.extend_from_slice(&col.type_mod.to_be_bytes());
446 buf.extend_from_slice(&col.format.to_be_bytes());
447 }
448 (b'T', buf)
449 }
450 BackendMessage::DataRow(fields) => {
451 let mut buf = Vec::new();
452 buf.extend_from_slice(&(fields.len() as i16).to_be_bytes());
453 for field in fields {
454 match field {
455 None => {
456 buf.extend_from_slice(&(-1i32).to_be_bytes());
458 }
459 Some(bytes) => {
460 buf.extend_from_slice(&(bytes.len() as i32).to_be_bytes());
464 buf.extend_from_slice(bytes);
465 }
466 }
467 }
468 (b'D', buf)
469 }
470 BackendMessage::CommandComplete(tag) => {
471 let mut buf = Vec::with_capacity(tag.len() + 1);
472 push_cstring(&mut buf, tag);
475 (b'C', buf)
476 }
477 BackendMessage::ParseComplete => (b'1', Vec::new()),
478 BackendMessage::BindComplete => (b'2', Vec::new()),
479 BackendMessage::CloseComplete => (b'3', Vec::new()),
480 BackendMessage::ParameterDescription(oids) => {
481 let mut buf = Vec::with_capacity(2 + oids.len() * 4);
482 buf.extend_from_slice(&(oids.len() as i16).to_be_bytes());
483 for oid in oids {
484 buf.extend_from_slice(&oid.to_be_bytes());
485 }
486 (b't', buf)
487 }
488 BackendMessage::PortalSuspended => (b's', Vec::new()),
489 BackendMessage::NoData => (b'n', Vec::new()),
490 BackendMessage::ErrorResponse {
491 severity,
492 code,
493 message,
494 } => {
495 let mut buf = Vec::new();
496 buf.push(b'S');
498 push_cstring(&mut buf, severity);
499 buf.push(b'V');
501 push_cstring(&mut buf, severity);
502 buf.push(b'C');
504 push_cstring(&mut buf, code);
505 buf.push(b'M');
507 push_cstring(&mut buf, message);
508 buf.push(0);
510 (b'E', buf)
511 }
512 BackendMessage::NoticeResponse { message } => {
513 let mut buf = Vec::new();
514 buf.push(b'S');
515 buf.extend_from_slice(b"NOTICE");
516 buf.push(0);
517 buf.push(b'M');
518 push_cstring(&mut buf, message);
520 buf.push(0);
521 (b'N', buf)
522 }
523 BackendMessage::EmptyQueryResponse => (b'I', Vec::new()),
524 }
525}
526
527fn read_cstring(buf: &[u8], pos: &mut usize) -> Result<String, PgWireError> {
534 let start = *pos;
535 while *pos < buf.len() && buf[*pos] != 0 {
536 *pos += 1;
537 }
538 if *pos >= buf.len() {
539 return Err(PgWireError::Protocol("cstring missing terminator".into()));
540 }
541 let s = std::str::from_utf8(&buf[start..*pos])
542 .map_err(|e| PgWireError::Protocol(format!("invalid utf8: {e}")))?
543 .to_string();
544 *pos += 1; Ok(s)
546}
547
548fn parse_parse_message(payload: &[u8]) -> Result<ParseMessage, PgWireError> {
549 let mut pos = 0;
550 let statement = read_cstring(payload, &mut pos)?;
551 let query = read_cstring(payload, &mut pos)?;
552 let nparams = read_i16(payload, &mut pos, "Parse parameter count")?;
553 if nparams < 0 {
554 return Err(PgWireError::Protocol(
555 "negative Parse parameter count".into(),
556 ));
557 }
558 let mut param_type_oids = Vec::with_capacity(nparams as usize);
559 for _ in 0..nparams {
560 param_type_oids.push(read_u32(payload, &mut pos, "Parse parameter type OID")?);
561 }
562 ensure_consumed(payload, pos, "Parse")?;
563 Ok(ParseMessage {
564 statement,
565 query,
566 param_type_oids,
567 })
568}
569
570fn parse_bind_message(payload: &[u8]) -> Result<BindMessage, PgWireError> {
571 let mut pos = 0;
572 let portal = read_cstring(payload, &mut pos)?;
573 let statement = read_cstring(payload, &mut pos)?;
574
575 let nformats = read_i16(payload, &mut pos, "Bind format count")?;
576 if nformats < 0 {
577 return Err(PgWireError::Protocol("negative Bind format count".into()));
578 }
579 let mut param_format_codes = Vec::with_capacity(nformats as usize);
580 for _ in 0..nformats {
581 param_format_codes.push(read_i16(payload, &mut pos, "Bind format code")?);
582 }
583
584 let nparams = read_i16(payload, &mut pos, "Bind parameter count")?;
585 if nparams < 0 {
586 return Err(PgWireError::Protocol(
587 "negative Bind parameter count".into(),
588 ));
589 }
590 let mut params = Vec::with_capacity(nparams as usize);
591 for _ in 0..nparams {
592 let len = read_i32(payload, &mut pos, "Bind parameter length")?;
593 if len == -1 {
594 params.push(None);
595 } else if len < -1 {
596 return Err(PgWireError::Protocol(
597 "invalid Bind parameter length".into(),
598 ));
599 } else {
600 params.push(Some(
601 read_bytes(payload, &mut pos, len as usize, "Bind parameter")?.to_vec(),
602 ));
603 }
604 }
605
606 let nresult_formats = read_i16(payload, &mut pos, "Bind result format count")?;
607 if nresult_formats < 0 {
608 return Err(PgWireError::Protocol(
609 "negative Bind result format count".into(),
610 ));
611 }
612 let mut result_format_codes = Vec::with_capacity(nresult_formats as usize);
613 for _ in 0..nresult_formats {
614 result_format_codes.push(read_i16(payload, &mut pos, "Bind result format code")?);
615 }
616 ensure_consumed(payload, pos, "Bind")?;
617
618 Ok(BindMessage {
619 portal,
620 statement,
621 param_format_codes,
622 params,
623 result_format_codes,
624 })
625}
626
627fn parse_describe_message(payload: &[u8]) -> Result<DescribeMessage, PgWireError> {
628 let mut pos = 0;
629 let target = read_describe_target(payload, &mut pos, "Describe")?;
630 let name = read_cstring(payload, &mut pos)?;
631 ensure_consumed(payload, pos, "Describe")?;
632 Ok(DescribeMessage { target, name })
633}
634
635fn parse_execute_message(payload: &[u8]) -> Result<ExecuteMessage, PgWireError> {
636 let mut pos = 0;
637 let portal = read_cstring(payload, &mut pos)?;
638 let max_rows = read_u32(payload, &mut pos, "Execute max rows")?;
639 ensure_consumed(payload, pos, "Execute")?;
640 Ok(ExecuteMessage { portal, max_rows })
641}
642
643fn parse_close_message(payload: &[u8]) -> Result<CloseMessage, PgWireError> {
644 let mut pos = 0;
645 let target = read_describe_target(payload, &mut pos, "Close")?;
646 let name = read_cstring(payload, &mut pos)?;
647 ensure_consumed(payload, pos, "Close")?;
648 Ok(CloseMessage { target, name })
649}
650
651fn read_describe_target(
652 payload: &[u8],
653 pos: &mut usize,
654 frame: &'static str,
655) -> Result<DescribeTarget, PgWireError> {
656 let byte = *read_bytes(payload, pos, 1, frame)?
657 .first()
658 .expect("one target byte");
659 match byte {
660 b'S' => Ok(DescribeTarget::Statement),
661 b'P' => Ok(DescribeTarget::Portal),
662 other => Err(PgWireError::Protocol(format!(
663 "{frame} target must be 'S' or 'P', got 0x{other:02x}"
664 ))),
665 }
666}
667
668fn read_i16(payload: &[u8], pos: &mut usize, field: &'static str) -> Result<i16, PgWireError> {
669 let bytes = read_bytes(payload, pos, 2, field)?;
670 Ok(i16::from_be_bytes([bytes[0], bytes[1]]))
671}
672
673fn read_i32(payload: &[u8], pos: &mut usize, field: &'static str) -> Result<i32, PgWireError> {
674 let bytes = read_bytes(payload, pos, 4, field)?;
675 Ok(i32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
676}
677
678fn read_u32(payload: &[u8], pos: &mut usize, field: &'static str) -> Result<u32, PgWireError> {
679 let bytes = read_bytes(payload, pos, 4, field)?;
680 Ok(u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
681}
682
683fn read_bytes<'a>(
684 payload: &'a [u8],
685 pos: &mut usize,
686 len: usize,
687 field: &'static str,
688) -> Result<&'a [u8], PgWireError> {
689 let end = pos
690 .checked_add(len)
691 .ok_or_else(|| PgWireError::Protocol(format!("{field} length overflow")))?;
692 if end > payload.len() {
693 return Err(PgWireError::Protocol(format!("{field} truncated")));
694 }
695 let bytes = &payload[*pos..end];
696 *pos = end;
697 Ok(bytes)
698}
699
700fn ensure_consumed(payload: &[u8], pos: usize, frame: &'static str) -> Result<(), PgWireError> {
701 if pos == payload.len() {
702 Ok(())
703 } else {
704 Err(PgWireError::Protocol(format!(
705 "{frame} had {} trailing bytes",
706 payload.len() - pos
707 )))
708 }
709}
710
711#[cfg(test)]
712mod tests {
713 use super::*;
714
715 #[tokio::test]
716 async fn parse_startup_v3() {
717 let mut payload: Vec<u8> = Vec::new();
719 payload.extend_from_slice(&PG_PROTOCOL_V3.to_be_bytes());
720 payload.extend_from_slice(b"user\0alice\0");
721 payload.push(0);
722 let len = (4 + payload.len()) as u32;
723 let mut frame = Vec::new();
724 frame.extend_from_slice(&len.to_be_bytes());
725 frame.extend_from_slice(&payload);
726
727 let mut cursor = std::io::Cursor::new(frame);
728 let msg = read_startup(&mut cursor).await.unwrap();
729 match msg {
730 FrontendMessage::Startup(params) => {
731 assert_eq!(params.get("user"), Some("alice"));
732 }
733 other => panic!("expected Startup, got {:?}", other),
734 }
735 }
736
737 #[tokio::test]
738 async fn parse_ssl_request() {
739 let mut frame: Vec<u8> = Vec::new();
740 frame.extend_from_slice(&8u32.to_be_bytes());
741 frame.extend_from_slice(&PG_SSL_REQUEST.to_be_bytes());
742 let mut cursor = std::io::Cursor::new(frame);
743 assert!(matches!(
744 read_startup(&mut cursor).await.unwrap(),
745 FrontendMessage::SslRequest
746 ));
747 }
748
749 #[tokio::test]
750 async fn parse_query_frame() {
751 let query = "SELECT 1\0";
752 let mut frame = Vec::new();
753 frame.push(b'Q');
754 let len = (4 + query.len()) as u32;
755 frame.extend_from_slice(&len.to_be_bytes());
756 frame.extend_from_slice(query.as_bytes());
757 let mut cursor = std::io::Cursor::new(frame);
758 match read_frame(&mut cursor).await.unwrap() {
759 FrontendMessage::Query(s) => assert_eq!(s, "SELECT 1"),
760 other => panic!("expected Query, got {:?}", other),
761 }
762 }
763
764 #[tokio::test]
765 async fn parse_extended_query_frames() {
766 let mut parse_payload = Vec::new();
767 push_test_cstring(&mut parse_payload, "");
768 push_test_cstring(&mut parse_payload, "SELECT $1");
769 parse_payload.extend_from_slice(&1i16.to_be_bytes());
770 parse_payload.extend_from_slice(&23u32.to_be_bytes());
771 let mut frame = tagged_frame(b'P', parse_payload);
772 let mut cursor = std::io::Cursor::new(frame);
773 match read_frame(&mut cursor).await.unwrap() {
774 FrontendMessage::Parse(msg) => {
775 assert_eq!(msg.statement, "");
776 assert_eq!(msg.query, "SELECT $1");
777 assert_eq!(msg.param_type_oids, vec![23]);
778 }
779 other => panic!("expected Parse, got {other:?}"),
780 }
781
782 let mut bind_payload = Vec::new();
783 push_test_cstring(&mut bind_payload, "");
784 push_test_cstring(&mut bind_payload, "");
785 bind_payload.extend_from_slice(&1i16.to_be_bytes());
786 bind_payload.extend_from_slice(&0i16.to_be_bytes());
787 bind_payload.extend_from_slice(&1i16.to_be_bytes());
788 bind_payload.extend_from_slice(&2i32.to_be_bytes());
789 bind_payload.extend_from_slice(b"42");
790 bind_payload.extend_from_slice(&0i16.to_be_bytes());
791 frame = tagged_frame(b'B', bind_payload);
792 let mut cursor = std::io::Cursor::new(frame);
793 match read_frame(&mut cursor).await.unwrap() {
794 FrontendMessage::Bind(msg) => {
795 assert_eq!(msg.portal, "");
796 assert_eq!(msg.statement, "");
797 assert_eq!(msg.param_format_codes, vec![0]);
798 assert_eq!(msg.params, vec![Some(b"42".to_vec())]);
799 assert!(msg.result_format_codes.is_empty());
800 }
801 other => panic!("expected Bind, got {other:?}"),
802 }
803
804 let mut describe_payload = vec![b'P'];
805 push_test_cstring(&mut describe_payload, "");
806 let mut cursor = std::io::Cursor::new(tagged_frame(b'D', describe_payload));
807 assert!(matches!(
808 read_frame(&mut cursor).await.unwrap(),
809 FrontendMessage::Describe(DescribeMessage {
810 target: DescribeTarget::Portal,
811 ..
812 })
813 ));
814 }
815
816 #[tokio::test]
817 async fn emit_ready_for_query() {
818 let mut out: Vec<u8> = Vec::new();
819 write_frame(
820 &mut out,
821 &BackendMessage::ReadyForQuery(TransactionStatus::Idle),
822 )
823 .await
824 .unwrap();
825 assert_eq!(out, vec![b'Z', 0, 0, 0, 5, b'I']);
826 }
827
828 #[tokio::test]
829 async fn emit_row_description_and_data_row() {
830 let mut out: Vec<u8> = Vec::new();
831 write_frame(
832 &mut out,
833 &BackendMessage::RowDescription(vec![ColumnDescriptor {
834 name: "id".to_string(),
835 table_oid: 0,
836 column_attr: 0,
837 type_oid: 23,
838 type_size: 4,
839 type_mod: -1,
840 format: 0,
841 }]),
842 )
843 .await
844 .unwrap();
845 assert_eq!(out[0], b'T');
846
847 let mut data: Vec<u8> = Vec::new();
848 write_frame(
849 &mut data,
850 &BackendMessage::DataRow(vec![Some(b"42".to_vec()), None]),
851 )
852 .await
853 .unwrap();
854 assert_eq!(data[0], b'D');
855 }
856
857 #[tokio::test]
858 async fn emit_extended_completion_frames() {
859 let mut out = Vec::new();
860 write_frame(&mut out, &BackendMessage::ParseComplete)
861 .await
862 .unwrap();
863 write_frame(&mut out, &BackendMessage::BindComplete)
864 .await
865 .unwrap();
866 write_frame(
867 &mut out,
868 &BackendMessage::ParameterDescription(vec![23, 25]),
869 )
870 .await
871 .unwrap();
872 write_frame(&mut out, &BackendMessage::NoData)
873 .await
874 .unwrap();
875 write_frame(&mut out, &BackendMessage::CloseComplete)
876 .await
877 .unwrap();
878 assert_eq!(collect_tags(&out), vec![b'1', b'2', b't', b'n', b'3']);
879 }
880
881 fn count_nul(buf: &[u8]) -> usize {
889 buf.iter().filter(|&&b| b == 0).count()
890 }
891
892 #[tokio::test]
893 async fn pg3_nul_error_response_message_field_sanitized() {
894 let mut out: Vec<u8> = Vec::new();
895 write_frame(
896 &mut out,
897 &BackendMessage::ErrorResponse {
898 severity: "ERROR".to_string(),
899 code: "42000".to_string(),
900 message: "smuggled\0M\x00injection".to_string(),
901 },
902 )
903 .await
904 .unwrap();
905 assert_eq!(out[0], b'E');
906 let body = &out[5..];
910 assert_eq!(
911 count_nul(body),
912 5,
913 "expected 5 NULs (4 field + 1 list-end), got {} :: body={:?}",
914 count_nul(body),
915 body
916 );
917 assert!(
919 body.windows(3).any(|w| w == [0xEF, 0xBF, 0xBD]),
920 "expected U+FFFD substitution in body"
921 );
922 }
923
924 #[tokio::test]
925 async fn pg3_nul_notice_response_sanitized() {
926 let mut out: Vec<u8> = Vec::new();
927 write_frame(
928 &mut out,
929 &BackendMessage::NoticeResponse {
930 message: "evil\0field".to_string(),
931 },
932 )
933 .await
934 .unwrap();
935 assert_eq!(out[0], b'N');
936 let body = &out[5..];
937 assert_eq!(count_nul(body), 3);
939 assert!(body.windows(3).any(|w| w == [0xEF, 0xBF, 0xBD]));
940 }
941
942 #[tokio::test]
943 async fn pg3_nul_command_complete_sanitized() {
944 let mut out: Vec<u8> = Vec::new();
945 write_frame(
946 &mut out,
947 &BackendMessage::CommandComplete("SELECT\0;DROP".to_string()),
948 )
949 .await
950 .unwrap();
951 assert_eq!(out[0], b'C');
952 let body = &out[5..];
953 assert_eq!(count_nul(body), 1);
955 }
956
957 #[tokio::test]
958 async fn pg3_nul_row_description_column_name_sanitized() {
959 let mut out: Vec<u8> = Vec::new();
960 write_frame(
961 &mut out,
962 &BackendMessage::RowDescription(vec![ColumnDescriptor {
963 name: "evil\0col".to_string(),
964 table_oid: 0,
965 column_attr: 0,
966 type_oid: 23,
967 type_size: 4,
968 type_mod: -1,
969 format: 0,
970 }]),
971 )
972 .await
973 .unwrap();
974 assert_eq!(out[0], b'T');
975 let body = &out[5..];
978 let name_region = &body[2..];
981 let first_nul = name_region.iter().position(|&b| b == 0).unwrap();
982 assert!(
983 name_region[..first_nul]
984 .windows(3)
985 .any(|w| w == [0xEF, 0xBF, 0xBD]),
986 "U+FFFD missing from sanitized column name"
987 );
988 }
989
990 #[test]
991 fn sanitize_cstring_fastpath_no_nul() {
992 let s = "no nuls here";
993 let out = sanitize_cstring_bytes(s.as_bytes());
994 assert_eq!(out, s.as_bytes());
995 }
996
997 #[test]
998 fn sanitize_cstring_substitutes_nul_with_replacement_codepoint() {
999 let s = b"a\0b\0c";
1000 let out = sanitize_cstring_bytes(s);
1001 assert_eq!(out.len(), 9);
1003 assert!(!out.contains(&0));
1004 assert_eq!(&out[1..4], &[0xEF, 0xBF, 0xBD]);
1005 assert_eq!(&out[5..8], &[0xEF, 0xBF, 0xBD]);
1006 }
1007
1008 fn tagged_frame(tag: u8, payload: Vec<u8>) -> Vec<u8> {
1009 let mut frame = vec![tag];
1010 frame.extend_from_slice(&((payload.len() + 4) as u32).to_be_bytes());
1011 frame.extend_from_slice(&payload);
1012 frame
1013 }
1014
1015 fn push_test_cstring(out: &mut Vec<u8>, value: &str) {
1016 out.extend_from_slice(value.as_bytes());
1017 out.push(0);
1018 }
1019
1020 fn collect_tags(bytes: &[u8]) -> Vec<u8> {
1021 let mut tags = Vec::new();
1022 let mut pos = 0;
1023 while pos < bytes.len() {
1024 tags.push(bytes[pos]);
1025 let len = u32::from_be_bytes([
1026 bytes[pos + 1],
1027 bytes[pos + 2],
1028 bytes[pos + 3],
1029 bytes[pos + 4],
1030 ]) as usize;
1031 pos += 1 + len;
1032 }
1033 tags
1034 }
1035}