Skip to main content

pgwire_replication/protocol/
framing.rs

1use bytes::{BufMut, Bytes, BytesMut};
2use std::io;
3use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
4
5use crate::error::{PgWireError, Result};
6
7/// Maximum backend message size (1GB) - prevents memory exhaustion from malformed length fields
8/// This is more than enough.
9///
10/// The default for [`MessageReader`] and for
11/// [`ReplicationConfig::max_message_size`](crate::config::ReplicationConfig::max_message_size).
12pub const MAX_MESSAGE_SIZE: usize = 1024 * 1024 * 1024;
13
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct BackendMessage {
16    pub tag: u8,
17    pub payload: Bytes, // payload excludes the 4-byte length field
18}
19
20impl BackendMessage {
21    /// Returns true if this is an ErrorResponse ('E')
22    #[inline]
23    pub fn is_error(&self) -> bool {
24        self.tag == b'E'
25    }
26
27    /// Returns true if this is a ReadyForQuery ('Z')
28    #[inline]
29    pub fn is_ready_for_query(&self) -> bool {
30        self.tag == b'Z'
31    }
32
33    /// Returns true if this is CopyBothResponse ('W')
34    #[inline]
35    pub fn is_copy_both_response(&self) -> bool {
36        self.tag == b'W'
37    }
38
39    /// Returns true if this is CopyData ('d')
40    #[inline]
41    pub fn is_copy_data(&self) -> bool {
42        self.tag == b'd'
43    }
44
45    /// Returns true if this is AuthenticationRequest ('R')
46    #[inline]
47    pub fn is_auth_request(&self) -> bool {
48        self.tag == b'R'
49    }
50}
51
52pub async fn read_backend_message<R: AsyncRead + Unpin>(rd: &mut R) -> Result<BackendMessage> {
53    let mut reader = MessageReader::new();
54    reader.read(rd).await
55}
56
57/// Cancellation-safe backend message reader.
58///
59/// PostgreSQL backend messages span multiple `read` operations (5-byte header,
60/// then a variable payload). A naive implementation using `read_exact` is
61/// **not** cancellation-safe: if the future is dropped between reads (e.g. by
62/// `tokio::select!` or `tokio::time::timeout`), bytes already pulled from the
63/// underlying stream are lost and the next read mis-parses the wire stream.
64///
65/// `MessageReader` externalizes the partial-read state so it survives across
66/// dropped futures. Each call to [`read`](Self::read) uses one-shot
67/// `AsyncReadExt::read` (which **is** cancel-safe) and accumulates progress
68/// on `self`. If the returned future is dropped, no bytes are lost; the next
69/// invocation resumes from where the previous one left off.
70pub struct MessageReader {
71    hdr: [u8; 5],
72    hdr_filled: usize,
73    payload: BytesMut,
74    payload_filled: usize,
75    /// `Some` once the header has been fully read and parsed; reset to
76    /// `None` after each completed message.
77    payload_len: Option<usize>,
78    tag: u8,
79    /// Largest payload accepted; a longer message is refused before any of
80    /// it is buffered.
81    max_payload: usize,
82    /// Set once a message has been refused for exceeding `max_payload`.
83    /// The refused payload is still on the wire and is never consumed, so
84    /// the reader cannot resynchronize: every later read fails the same way.
85    refused: Option<(usize, usize)>,
86}
87
88impl MessageReader {
89    pub fn new() -> Self {
90        Self::with_capacity(4096)
91    }
92
93    pub fn with_capacity(capacity: usize) -> Self {
94        Self {
95            hdr: [0u8; 5],
96            hdr_filled: 0,
97            payload: BytesMut::with_capacity(capacity),
98            payload_filled: 0,
99            payload_len: None,
100            tag: 0,
101            max_payload: MAX_MESSAGE_SIZE,
102            refused: None,
103        }
104    }
105
106    /// Refuse any message whose payload is larger than `max` bytes.
107    ///
108    /// `max` is compared against the message's length field minus 4, so it
109    /// excludes the tag and the length field itself. The payload buffer is
110    /// sized from the length the server announces, so this bounds the memory
111    /// one message can take. It can only lower the limit: a value above
112    /// [`MAX_MESSAGE_SIZE`], the default, is capped to it.
113    ///
114    /// A refused message is terminal. Its payload is left on the wire, so the
115    /// reader cannot resynchronize and every later [`read`](Self::read)
116    /// returns the same [`PgWireError::MessageTooLarge`]. Discard the reader
117    /// and the connection.
118    pub fn with_max_message_size(mut self, max: usize) -> Self {
119        self.max_payload = max.min(MAX_MESSAGE_SIZE);
120        self
121    }
122
123    /// Read the next complete backend message.
124    ///
125    /// Cancellation-safe: dropping the returned future preserves all progress
126    /// so far on `self`. Re-call to resume.
127    pub async fn read<R: AsyncRead + Unpin>(&mut self, rd: &mut R) -> Result<BackendMessage> {
128        // Phase 0: a refused message is terminal. Its payload is still on the
129        // wire and carries WAL data we must not skip past, so there is nothing
130        // to resynchronize to — fail the same way until the caller discards
131        // this reader and its connection.
132        if let Some((payload_bytes, limit_bytes)) = self.refused {
133            return Err(PgWireError::MessageTooLarge {
134                payload_bytes,
135                limit_bytes,
136            });
137        }
138
139        // Phase 1: fill the 5-byte header
140        while self.hdr_filled < 5 {
141            let n = rd.read(&mut self.hdr[self.hdr_filled..]).await?;
142            if n == 0 {
143                return Err(PgWireError::Io(std::sync::Arc::new(io::Error::new(
144                    io::ErrorKind::UnexpectedEof,
145                    "EOF while reading backend message header",
146                ))));
147            }
148            self.hdr_filled += n;
149        }
150
151        // Phase 2: parse the header (idempotent — runs once per message)
152        if self.payload_len.is_none() {
153            let len = i32::from_be_bytes([self.hdr[1], self.hdr[2], self.hdr[3], self.hdr[4]]);
154
155            if len < 4 {
156                // Reset so the reader is reusable after a protocol error is
157                // surfaced (callers typically tear down on this anyway).
158                self.hdr_filled = 0;
159                return Err(PgWireError::Protocol(format!(
160                    "invalid backend message length: {len}"
161                )));
162            }
163
164            let payload_len = (len - 4) as usize;
165
166            if payload_len > self.max_payload {
167                self.hdr_filled = 0;
168                self.refused = Some((payload_len, self.max_payload));
169                return Err(PgWireError::MessageTooLarge {
170                    payload_bytes: payload_len,
171                    limit_bytes: self.max_payload,
172                });
173            }
174
175            self.tag = self.hdr[0];
176            self.payload.clear();
177            self.payload.resize(payload_len, 0);
178            self.payload_filled = 0;
179            self.payload_len = Some(payload_len);
180        }
181
182        let payload_len = self.payload_len.unwrap();
183
184        // Phase 3: fill the payload
185        while self.payload_filled < payload_len {
186            let n = rd.read(&mut self.payload[self.payload_filled..]).await?;
187            if n == 0 {
188                return Err(PgWireError::Io(std::sync::Arc::new(io::Error::new(
189                    io::ErrorKind::UnexpectedEof,
190                    "EOF while reading backend message payload",
191                ))));
192            }
193            self.payload_filled += n;
194        }
195
196        // Phase 4: take payload, reset state for next message
197        let payload = self.payload.split().freeze();
198        let tag = self.tag;
199        self.hdr_filled = 0;
200        self.payload_len = None;
201        self.payload_filled = 0;
202
203        Ok(BackendMessage { tag, payload })
204    }
205}
206
207impl Default for MessageReader {
208    fn default() -> Self {
209        Self::new()
210    }
211}
212
213/// Read a single backend message, reusing the provided buffer.
214///
215/// **Not** cancellation-safe — see [`MessageReader`] for a cancel-safe
216/// alternative used in the streaming loop.
217pub async fn read_backend_message_into<R: AsyncRead + Unpin>(
218    rd: &mut R,
219    buf: &mut BytesMut,
220) -> Result<BackendMessage> {
221    let mut hdr = [0u8; 5];
222    rd.read_exact(&mut hdr).await?;
223    let tag = hdr[0];
224    let len = i32::from_be_bytes([hdr[1], hdr[2], hdr[3], hdr[4]]);
225
226    if len < 4 {
227        return Err(PgWireError::Protocol(format!(
228            "invalid backend message length: {len}"
229        )));
230    }
231
232    let payload_len = (len - 4) as usize;
233
234    if payload_len > MAX_MESSAGE_SIZE {
235        return Err(PgWireError::MessageTooLarge {
236            payload_bytes: payload_len,
237            limit_bytes: MAX_MESSAGE_SIZE,
238        });
239    }
240
241    buf.clear();
242    buf.resize(payload_len, 0);
243    rd.read_exact(&mut buf[..]).await?;
244    Ok(BackendMessage {
245        tag,
246        payload: buf.split().freeze(),
247    })
248}
249
250pub async fn write_ssl_request<W: AsyncWrite + Unpin>(wr: &mut W) -> Result<()> {
251    let mut buf = [0u8; 8];
252    buf[0..4].copy_from_slice(&(8i32).to_be_bytes());
253    buf[4..8].copy_from_slice(&(80877103i32).to_be_bytes());
254    wr.write_all(&buf).await?;
255    wr.flush().await?;
256    Ok(())
257}
258
259pub async fn write_startup_message<W: AsyncWrite + Unpin>(
260    wr: &mut W,
261    protocol_version: i32,
262    params: &[(&str, &str)],
263) -> Result<()> {
264    let mut buf = BytesMut::with_capacity(256);
265    buf.put_i32(0); // length placeholder
266    buf.put_i32(protocol_version);
267
268    for (k, v) in params {
269        buf.extend_from_slice(k.as_bytes());
270        buf.put_u8(0);
271        buf.extend_from_slice(v.as_bytes());
272        buf.put_u8(0);
273    }
274    buf.put_u8(0); // terminator
275
276    let len = buf.len() as i32;
277    buf[0..4].copy_from_slice(&len.to_be_bytes());
278
279    wr.write_all(&buf).await?;
280    wr.flush().await?;
281    Ok(())
282}
283
284pub async fn write_query<W: AsyncWrite + Unpin>(wr: &mut W, sql: &str) -> Result<()> {
285    let mut buf = BytesMut::with_capacity(sql.len() + 64);
286    buf.put_u8(b'Q');
287    buf.put_i32(0);
288    buf.extend_from_slice(sql.as_bytes());
289    buf.put_u8(0);
290
291    let len = (buf.len() - 1) as i32;
292    buf[1..5].copy_from_slice(&len.to_be_bytes());
293
294    wr.write_all(&buf).await?;
295    wr.flush().await?;
296    Ok(())
297}
298
299pub async fn write_password_message<W: AsyncWrite + Unpin>(
300    wr: &mut W,
301    payload: &[u8],
302) -> Result<()> {
303    let mut buf = BytesMut::with_capacity(payload.len() + 16);
304    buf.put_u8(b'p');
305    buf.put_i32(0);
306    buf.extend_from_slice(payload);
307
308    let len = (buf.len() - 1) as i32;
309    buf[1..5].copy_from_slice(&len.to_be_bytes());
310
311    wr.write_all(&buf).await?;
312    wr.flush().await?;
313    Ok(())
314}
315
316pub async fn write_copy_data<W: AsyncWrite + Unpin>(wr: &mut W, payload: &[u8]) -> Result<()> {
317    let mut buf = BytesMut::with_capacity(payload.len() + 16);
318    buf.put_u8(b'd');
319    buf.put_i32(0);
320    buf.extend_from_slice(payload);
321
322    let len = (buf.len() - 1) as i32;
323    buf[1..5].copy_from_slice(&len.to_be_bytes());
324
325    wr.write_all(&buf).await?;
326    wr.flush().await?;
327    Ok(())
328}
329
330pub async fn write_copy_done<W: AsyncWrite + Unpin>(wr: &mut W) -> Result<()> {
331    let mut buf = BytesMut::with_capacity(5);
332    buf.put_u8(b'c'); // CopyDone
333    buf.put_i32(4); // length includes itself; CopyDone has no payload
334    wr.write_all(&buf).await?;
335    wr.flush().await?;
336    Ok(())
337}
338
339#[cfg(test)]
340mod tests {
341    use super::*;
342    use std::io::Cursor;
343    use tokio::io::AsyncWriteExt;
344
345    #[tokio::test]
346    async fn read_backend_message_parses_valid_message() {
347        // Tag 'Z' (ReadyForQuery), length=5 (4 + 1 byte payload), payload='I' (idle)
348        let data = [b'Z', 0, 0, 0, 5, b'I'];
349        let mut cursor = Cursor::new(&data[..]);
350
351        let msg = read_backend_message(&mut cursor).await.unwrap();
352        assert_eq!(msg.tag, b'Z');
353        assert_eq!(&msg.payload[..], b"I");
354        assert!(msg.is_ready_for_query());
355    }
356
357    #[tokio::test]
358    async fn read_backend_message_handles_empty_payload() {
359        // Tag 'N' (NoticeResponse placeholder), length=4 (no payload)
360        let data = [b'N', 0, 0, 0, 4];
361        let mut cursor = Cursor::new(&data[..]);
362
363        let msg = read_backend_message(&mut cursor).await.unwrap();
364        assert_eq!(msg.tag, b'N');
365        assert!(msg.payload.is_empty());
366    }
367
368    #[tokio::test]
369    async fn read_backend_message_rejects_invalid_length() {
370        // length < 4 is invalid
371        let data = [b'Z', 0, 0, 0, 3];
372        let mut cursor = Cursor::new(&data[..]);
373
374        let err = read_backend_message(&mut cursor).await.unwrap_err();
375        assert!(err.to_string().contains("invalid backend message length"));
376    }
377
378    #[tokio::test]
379    async fn message_reader_reads_complete_message() {
380        // Tag 'Z' (ReadyForQuery), length=5 (4 + 1 byte payload), payload='I'
381        let data = [b'Z', 0, 0, 0, 5, b'I'];
382        let mut cursor = Cursor::new(&data[..]);
383
384        let mut reader = MessageReader::new();
385        let msg = reader.read(&mut cursor).await.unwrap();
386        assert_eq!(msg.tag, b'Z');
387        assert_eq!(&msg.payload[..], b"I");
388    }
389
390    #[tokio::test]
391    async fn message_reader_reads_back_to_back_messages() {
392        // Two messages on one stream: ReadyForQuery + NoticeResponse w/ empty payload
393        let data = [b'Z', 0, 0, 0, 5, b'I', b'N', 0, 0, 0, 4];
394        let mut cursor = Cursor::new(&data[..]);
395
396        let mut reader = MessageReader::new();
397
398        let m1 = reader.read(&mut cursor).await.unwrap();
399        assert_eq!(m1.tag, b'Z');
400        assert_eq!(&m1.payload[..], b"I");
401
402        let m2 = reader.read(&mut cursor).await.unwrap();
403        assert_eq!(m2.tag, b'N');
404        assert!(m2.payload.is_empty());
405    }
406
407    /// Regression test for issue #5: reading a backend message must be
408    /// cancellation-safe so that `tokio::select!` / `tokio::time::timeout`
409    /// dropping the read future mid-message does not corrupt the stream.
410    ///
411    /// With the old `read_backend_message_into`, dropping the future after
412    /// 3 of 5 header bytes were consumed would lose those 3 bytes and
413    /// re-parse the next bytes as a new header, producing a bogus length
414    /// and a Protocol error (or worse, a silent desync).
415    #[tokio::test]
416    async fn message_reader_resumes_after_cancellation_mid_header() {
417        let (mut writer, mut rd) = tokio::io::duplex(64);
418        let mut reader = MessageReader::new();
419
420        // Tag 'd' (CopyData), length = 8 (4 + 4-byte payload), payload b"abcd"
421        let header = [b'd', 0, 0, 0, 8];
422        let payload = b"abcd";
423
424        // Deliver only the first 3 header bytes, then cancel.
425        writer.write_all(&header[..3]).await.unwrap();
426
427        let timed_out =
428            tokio::time::timeout(std::time::Duration::from_millis(20), reader.read(&mut rd)).await;
429        assert!(
430            timed_out.is_err(),
431            "read must time out while waiting for remaining header bytes"
432        );
433
434        // Deliver the remaining bytes. A correct cancel-safe reader resumes
435        // and returns the original message intact.
436        writer.write_all(&header[3..]).await.unwrap();
437        writer.write_all(payload).await.unwrap();
438
439        let msg = reader.read(&mut rd).await.unwrap();
440        assert_eq!(msg.tag, b'd');
441        assert_eq!(&msg.payload[..], payload);
442    }
443
444    /// Ensures partial-payload cancellation also resumes correctly.
445    #[tokio::test]
446    async fn message_reader_resumes_after_cancellation_mid_payload() {
447        let (mut writer, mut rd) = tokio::io::duplex(64);
448        let mut reader = MessageReader::new();
449
450        // 16-byte payload to ensure we can split it.
451        let payload: [u8; 16] = std::array::from_fn(|i| i as u8);
452        let len = (4 + payload.len()) as i32;
453        let header = [
454            b'd',
455            (len >> 24) as u8,
456            (len >> 16) as u8,
457            (len >> 8) as u8,
458            len as u8,
459        ];
460
461        // Full header + first 5 bytes of payload, then cancel.
462        writer.write_all(&header).await.unwrap();
463        writer.write_all(&payload[..5]).await.unwrap();
464
465        let timed_out =
466            tokio::time::timeout(std::time::Duration::from_millis(20), reader.read(&mut rd)).await;
467        assert!(
468            timed_out.is_err(),
469            "read must time out while waiting for remaining payload bytes"
470        );
471
472        // Deliver the rest.
473        writer.write_all(&payload[5..]).await.unwrap();
474
475        let msg = reader.read(&mut rd).await.unwrap();
476        assert_eq!(msg.tag, b'd');
477        assert_eq!(&msg.payload[..], &payload[..]);
478    }
479
480    #[tokio::test]
481    async fn message_reader_rejects_invalid_length() {
482        let data = [b'Z', 0, 0, 0, 3];
483        let mut cursor = Cursor::new(&data[..]);
484
485        let mut reader = MessageReader::new();
486        let err = reader.read(&mut cursor).await.unwrap_err();
487        assert!(err.to_string().contains("invalid backend message length"));
488    }
489
490    #[tokio::test]
491    async fn message_reader_rejects_a_message_over_its_configured_limit() {
492        // A 10-byte payload against a limit of 8.
493        let data = [b'd', 0, 0, 0, 14];
494        let mut cursor = Cursor::new(&data[..]);
495
496        let mut reader = MessageReader::new().with_max_message_size(8);
497        let err = reader.read(&mut cursor).await.unwrap_err();
498        assert!(matches!(
499            err,
500            PgWireError::MessageTooLarge {
501                payload_bytes: 10,
502                limit_bytes: 8
503            }
504        ));
505        assert!(!err.is_transient());
506    }
507
508    #[tokio::test]
509    async fn message_reader_is_poisoned_after_refusing_an_oversized_message() {
510        // A 10-byte payload over the limit of 8, then a valid message that
511        // must never be handed out: the refused payload sits between them and
512        // may carry WAL changes, so skipping to the next frame would lose data.
513        let mut data = vec![b'd', 0, 0, 0, 14];
514        data.extend_from_slice(b"0123456789");
515        data.extend_from_slice(&[b'k', 0, 0, 0, 7]);
516        data.extend_from_slice(b"abc");
517        let mut cursor = Cursor::new(&data[..]);
518
519        let mut reader = MessageReader::new().with_max_message_size(8);
520        assert!(reader.read(&mut cursor).await.is_err());
521
522        for _ in 0..3 {
523            let err = reader.read(&mut cursor).await.unwrap_err();
524            assert!(matches!(
525                err,
526                PgWireError::MessageTooLarge {
527                    payload_bytes: 10,
528                    limit_bytes: 8
529                }
530            ));
531        }
532    }
533
534    #[tokio::test]
535    async fn message_reader_does_not_consume_a_refused_payload() {
536        // Nothing past the 5-byte header is read, so a bogus length cannot
537        // make the reader pull an arbitrary number of bytes off the wire.
538        let bogus = i32::MAX;
539        let mut data = vec![b'd'];
540        data.extend_from_slice(&bogus.to_be_bytes());
541        data.extend_from_slice(b"payload bytes that must stay on the wire");
542        let mut cursor = Cursor::new(&data[..]);
543
544        let mut reader = MessageReader::new().with_max_message_size(8);
545        assert!(reader.read(&mut cursor).await.is_err());
546        assert_eq!(cursor.position(), 5);
547    }
548
549    #[tokio::test]
550    async fn message_reader_reaches_the_oversized_verdict_across_a_cancelled_header() {
551        use tokio::io::AsyncWriteExt;
552
553        // 10_000-byte payload announced, against a limit of 8.
554        let len = 4 + 10_000i32;
555        let hdr = {
556            let mut v = vec![b'd'];
557            v.extend_from_slice(&len.to_be_bytes());
558            v
559        };
560
561        let (mut client, mut server) = tokio::io::duplex(64);
562        let mut reader = MessageReader::new().with_max_message_size(8);
563
564        // Two of the five header bytes arrive, then the read is cancelled
565        // while genuinely waiting for the rest.
566        server.write_all(&hdr[..2]).await.unwrap();
567        assert!(
568            tokio::time::timeout(
569                std::time::Duration::from_millis(50),
570                reader.read(&mut client)
571            )
572            .await
573            .is_err(),
574            "expected the read to be waiting for the rest of the header"
575        );
576
577        // The rest arrives. If the first two bytes had been lost with the
578        // dropped future, this read would mis-parse or block instead.
579        server.write_all(&hdr[2..]).await.unwrap();
580        let err = tokio::time::timeout(std::time::Duration::from_secs(5), reader.read(&mut client))
581            .await
582            .expect("reader lost the partial header and is still waiting")
583            .unwrap_err();
584
585        assert!(matches!(
586            err,
587            PgWireError::MessageTooLarge {
588                payload_bytes: 10_000,
589                limit_bytes: 8
590            }
591        ));
592
593        // The verdict is terminal even though it was reached across a
594        // cancellation: the payload arriving afterwards changes nothing.
595        server.write_all(&[b'x'; 32]).await.unwrap();
596        let err = reader.read(&mut client).await.unwrap_err();
597        assert!(matches!(
598            err,
599            PgWireError::MessageTooLarge {
600                payload_bytes: 10_000,
601                limit_bytes: 8
602            }
603        ));
604    }
605
606    #[tokio::test]
607    async fn message_reader_accepts_a_message_at_its_configured_limit() {
608        let mut data = vec![b'd', 0, 0, 0, 12];
609        data.extend_from_slice(b"12345678");
610        let mut cursor = Cursor::new(&data[..]);
611
612        let mut reader = MessageReader::new().with_max_message_size(8);
613        let msg = reader.read(&mut cursor).await.unwrap();
614        assert_eq!(&msg.payload[..], b"12345678");
615    }
616
617    #[test]
618    fn message_reader_limit_cannot_be_raised_above_the_protocol_cap() {
619        let reader = MessageReader::new().with_max_message_size(usize::MAX);
620        assert_eq!(reader.max_payload, MAX_MESSAGE_SIZE);
621    }
622
623    #[tokio::test]
624    async fn read_backend_message_rejects_oversized_message() {
625        // length = MAX_MESSAGE_SIZE + 5 (over limit)
626        let huge_len = (MAX_MESSAGE_SIZE as i32) + 5;
627        let data = [
628            b'Z',
629            (huge_len >> 24) as u8,
630            (huge_len >> 16) as u8,
631            (huge_len >> 8) as u8,
632            huge_len as u8,
633        ];
634        let mut cursor = Cursor::new(&data[..]);
635
636        let err = read_backend_message(&mut cursor).await.unwrap_err();
637        assert!(err.to_string().contains("too large"));
638    }
639
640    #[tokio::test]
641    async fn write_ssl_request_produces_valid_bytes() {
642        let mut buf = Vec::new();
643        write_ssl_request(&mut buf).await.unwrap();
644
645        assert_eq!(buf.len(), 8);
646        // length = 8
647        assert_eq!(&buf[0..4], &8i32.to_be_bytes());
648        // SSL request code = 80877103
649        assert_eq!(&buf[4..8], &80877103i32.to_be_bytes());
650    }
651
652    #[tokio::test]
653    async fn write_startup_message_includes_params() {
654        let mut buf = Vec::new();
655        let params = [("user", "postgres"), ("database", "test")];
656        write_startup_message(&mut buf, 196608, &params)
657            .await
658            .unwrap();
659
660        // Should contain the parameter strings
661        let s = String::from_utf8_lossy(&buf);
662        assert!(s.contains("user"));
663        assert!(s.contains("postgres"));
664        assert!(s.contains("database"));
665        assert!(s.contains("test"));
666
667        // Length field should be at start
668        let len = i32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]) as usize;
669        assert_eq!(len, buf.len());
670    }
671
672    #[tokio::test]
673    async fn write_query_produces_valid_message() {
674        let mut buf = Vec::new();
675        write_query(&mut buf, "SELECT 1").await.unwrap();
676
677        // Should start with 'Q'
678        assert_eq!(buf[0], b'Q');
679
680        // Length should be correct (excludes tag byte)
681        let len = i32::from_be_bytes([buf[1], buf[2], buf[3], buf[4]]) as usize;
682        assert_eq!(len, buf.len() - 1);
683
684        // Should contain the SQL
685        assert!(buf[5..].starts_with(b"SELECT 1"));
686
687        // Should be null-terminated
688        assert_eq!(buf[buf.len() - 1], 0);
689    }
690
691    #[tokio::test]
692    async fn write_password_message_produces_valid_message() {
693        let mut buf = Vec::new();
694        write_password_message(&mut buf, b"secret").await.unwrap();
695
696        assert_eq!(buf[0], b'p');
697        let len = i32::from_be_bytes([buf[1], buf[2], buf[3], buf[4]]) as usize;
698        assert_eq!(len, buf.len() - 1);
699        assert_eq!(&buf[5..], b"secret");
700    }
701
702    #[tokio::test]
703    async fn write_copy_data_produces_valid_message() {
704        let mut buf = Vec::new();
705        write_copy_data(&mut buf, b"payload").await.unwrap();
706
707        assert_eq!(buf[0], b'd');
708        let len = i32::from_be_bytes([buf[1], buf[2], buf[3], buf[4]]) as usize;
709        assert_eq!(len, buf.len() - 1);
710        assert_eq!(&buf[5..], b"payload");
711    }
712
713    #[tokio::test]
714    async fn write_copy_done_produces_valid_message() {
715        let mut buf = Vec::new();
716        write_copy_done(&mut buf).await.unwrap();
717
718        assert_eq!(buf.len(), 5);
719        assert_eq!(buf[0], b'c');
720        // Length = 4 (just the length field itself, no payload)
721        assert_eq!(&buf[1..5], &4i32.to_be_bytes());
722    }
723
724    #[test]
725    fn backend_message_helper_methods() {
726        let error = BackendMessage {
727            tag: b'E',
728            payload: Bytes::new(),
729        };
730        assert!(error.is_error());
731        assert!(!error.is_ready_for_query());
732
733        let ready = BackendMessage {
734            tag: b'Z',
735            payload: Bytes::from_static(b"I"),
736        };
737        assert!(ready.is_ready_for_query());
738        assert!(!ready.is_error());
739
740        let copy_both = BackendMessage {
741            tag: b'W',
742            payload: Bytes::new(),
743        };
744        assert!(copy_both.is_copy_both_response());
745
746        let copy_data = BackendMessage {
747            tag: b'd',
748            payload: Bytes::new(),
749        };
750        assert!(copy_data.is_copy_data());
751
752        let auth = BackendMessage {
753            tag: b'R',
754            payload: Bytes::new(),
755        };
756        assert!(auth.is_auth_request());
757    }
758}