Skip to main content

heliosdb_proxy/
protocol.rs

1//! Protocol Handling
2//!
3//! Wire protocol parsing and serialization for HeliosDB proxy.
4
5use crate::{ProxyError, Result};
6use bytes::{Buf, BufMut, Bytes, BytesMut};
7use std::collections::HashMap;
8
9/// Protocol message types
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub enum MessageType {
12    /// Startup message from client
13    Startup,
14    /// SSL request
15    SSLRequest,
16    /// Authentication request
17    AuthRequest,
18    /// Password message
19    Password,
20    /// Query message
21    Query,
22    /// Parse message (prepared statement)
23    Parse,
24    /// Bind message
25    Bind,
26    /// Describe message
27    Describe,
28    /// Execute message
29    Execute,
30    /// Sync message
31    Sync,
32    /// Flush message
33    Flush,
34    /// Close message
35    Close,
36    /// Terminate message
37    Terminate,
38    /// Copy data
39    CopyData,
40    /// Copy done
41    CopyDone,
42    /// Copy fail
43    CopyFail,
44    /// Function call (deprecated)
45    FunctionCall,
46    /// Backend key data
47    BackendKeyData,
48    /// Parameter status
49    ParameterStatus,
50    /// Ready for query
51    ReadyForQuery,
52    /// Row description
53    RowDescription,
54    /// Data row
55    DataRow,
56    /// Command complete
57    CommandComplete,
58    /// Empty query response
59    EmptyQueryResponse,
60    /// Error response
61    ErrorResponse,
62    /// Notice response
63    NoticeResponse,
64    /// Notification response
65    NotificationResponse,
66    /// Parse complete
67    ParseComplete,
68    /// Bind complete
69    BindComplete,
70    /// Close complete
71    CloseComplete,
72    /// Portal suspended
73    PortalSuspended,
74    /// No data
75    NoData,
76    /// Parameter description
77    ParameterDescription,
78    /// Unknown message
79    Unknown(u8),
80}
81
82impl MessageType {
83    /// Get message type from tag byte
84    pub fn from_tag(tag: u8) -> Self {
85        match tag {
86            b'Q' => MessageType::Query,
87            b'P' => MessageType::Parse,
88            b'B' => MessageType::Bind,
89            b'D' => MessageType::Describe,
90            b'E' => MessageType::Execute,
91            b'S' => MessageType::Sync,
92            b'H' => MessageType::Flush,
93            b'C' => MessageType::Close,
94            b'X' => MessageType::Terminate,
95            b'd' => MessageType::CopyData,
96            b'c' => MessageType::CopyDone,
97            b'f' => MessageType::CopyFail,
98            b'F' => MessageType::FunctionCall,
99            b'p' => MessageType::Password,
100            b'R' => MessageType::AuthRequest,
101            b'K' => MessageType::BackendKeyData,
102            // Note: server-side D/E/C/S tags (DataRow, ErrorResponse,
103            // CommandComplete, ParameterStatus) collide with client-side
104            // Describe/Execute/Close/Sync above; from_tag() is direction-
105            // agnostic and resolves them to the client-side variants.
106            // Disambiguation, when needed, lives at the call site.
107            b'Z' => MessageType::ReadyForQuery,
108            b'T' => MessageType::RowDescription,
109            b'I' => MessageType::EmptyQueryResponse,
110            b'N' => MessageType::NoticeResponse,
111            b'A' => MessageType::NotificationResponse,
112            b'1' => MessageType::ParseComplete,
113            b'2' => MessageType::BindComplete,
114            b'3' => MessageType::CloseComplete,
115            b's' => MessageType::PortalSuspended,
116            b'n' => MessageType::NoData,
117            b't' => MessageType::ParameterDescription,
118            _ => MessageType::Unknown(tag),
119        }
120    }
121
122    /// Get tag byte for message type
123    pub fn to_tag(&self) -> Option<u8> {
124        match self {
125            MessageType::Query => Some(b'Q'),
126            MessageType::Parse => Some(b'P'),
127            MessageType::Bind => Some(b'B'),
128            MessageType::Describe => Some(b'D'),
129            MessageType::Execute => Some(b'E'),
130            MessageType::Sync => Some(b'S'),
131            MessageType::Flush => Some(b'H'),
132            MessageType::Close => Some(b'C'),
133            MessageType::Terminate => Some(b'X'),
134            MessageType::CopyData => Some(b'd'),
135            MessageType::CopyDone => Some(b'c'),
136            MessageType::CopyFail => Some(b'f'),
137            MessageType::FunctionCall => Some(b'F'),
138            MessageType::Password => Some(b'p'),
139            MessageType::AuthRequest => Some(b'R'),
140            MessageType::BackendKeyData => Some(b'K'),
141            MessageType::ParameterStatus => Some(b'S'),
142            MessageType::ReadyForQuery => Some(b'Z'),
143            MessageType::RowDescription => Some(b'T'),
144            MessageType::DataRow => Some(b'D'),
145            MessageType::CommandComplete => Some(b'C'),
146            MessageType::EmptyQueryResponse => Some(b'I'),
147            MessageType::ErrorResponse => Some(b'E'),
148            MessageType::NoticeResponse => Some(b'N'),
149            MessageType::NotificationResponse => Some(b'A'),
150            MessageType::ParseComplete => Some(b'1'),
151            MessageType::BindComplete => Some(b'2'),
152            MessageType::CloseComplete => Some(b'3'),
153            MessageType::PortalSuspended => Some(b's'),
154            MessageType::NoData => Some(b'n'),
155            MessageType::ParameterDescription => Some(b't'),
156            _ => None,
157        }
158    }
159}
160
161/// A protocol message
162#[derive(Debug, Clone)]
163pub struct Message {
164    /// Message type
165    pub msg_type: MessageType,
166    /// Message payload
167    pub payload: BytesMut,
168}
169
170impl Message {
171    /// Create a new message
172    pub fn new(msg_type: MessageType, payload: BytesMut) -> Self {
173        Self { msg_type, payload }
174    }
175
176    /// Create an empty message
177    pub fn empty(msg_type: MessageType) -> Self {
178        Self {
179            msg_type,
180            payload: BytesMut::new(),
181        }
182    }
183
184    /// Encode message to bytes.
185    ///
186    /// Pre-sizes the buffer to the exact final length (1 tag + 4 length +
187    /// payload) so a forwarded message never pays reallocation growth on the
188    /// hot path — `BytesMut::new()` starts at capacity 0 and would realloc as
189    /// the tag/length/payload are appended.
190    pub fn encode(&self) -> BytesMut {
191        let has_tag = self.msg_type.to_tag().is_some();
192        let mut buf = BytesMut::with_capacity(self.payload.len() + if has_tag { 5 } else { 4 });
193
194        if let Some(tag) = self.msg_type.to_tag() {
195            buf.put_u8(tag);
196        }
197
198        // Length includes itself (4 bytes)
199        let len = self.payload.len() as u32 + 4;
200        buf.put_u32(len);
201        buf.extend_from_slice(&self.payload);
202
203        buf
204    }
205}
206
207/// Protocol codec for framing messages
208pub struct ProtocolCodec {
209    /// Maximum message size
210    max_message_size: usize,
211}
212
213impl Default for ProtocolCodec {
214    fn default() -> Self {
215        Self::new()
216    }
217}
218
219impl ProtocolCodec {
220    /// Create a new codec
221    pub fn new() -> Self {
222        Self {
223            max_message_size: 100 * 1024 * 1024, // 100MB max
224        }
225    }
226
227    /// Create codec with custom max message size
228    pub fn with_max_size(max_message_size: usize) -> Self {
229        Self { max_message_size }
230    }
231
232    /// Decode a startup message (no tag byte)
233    pub fn decode_startup(&self, src: &mut BytesMut) -> Result<Option<StartupMessage>> {
234        if src.len() < 4 {
235            return Ok(None);
236        }
237
238        let len = u32::from_be_bytes([src[0], src[1], src[2], src[3]]) as usize;
239
240        if len > self.max_message_size {
241            return Err(ProxyError::Protocol(format!(
242                "Message too large: {} bytes",
243                len
244            )));
245        }
246
247        // A startup frame is at minimum length(4) + version(4) = 8 bytes.
248        // Anything shorter is malformed: reject it instead of underflowing
249        // `len - 8` (below) or panicking in `get_u32`. This is the very first
250        // bytes of an *unauthenticated* connection, so a 4-byte `00 00 00 04`
251        // must not be able to crash the pre-auth handler task.
252        if len < 8 {
253            return Err(ProxyError::Protocol(format!(
254                "startup message length {} below minimum of 8",
255                len
256            )));
257        }
258
259        if src.len() < len {
260            return Ok(None);
261        }
262
263        src.advance(4);
264        let protocol_version = src.get_u32();
265
266        // Check for SSL request
267        if protocol_version == 80877103 {
268            return Ok(Some(StartupMessage::SSLRequest));
269        }
270
271        // Check for cancel request
272        if protocol_version == 80877102 {
273            let pid = src.get_u32();
274            let key = src.get_u32();
275            return Ok(Some(StartupMessage::CancelRequest { pid, key }));
276        }
277
278        // Parse parameters
279        let mut params = HashMap::new();
280        let remaining = len - 8; // Already read length and version
281        let mut param_bytes = src.split_to(remaining);
282
283        while param_bytes.has_remaining() {
284            let key = read_cstring(&mut param_bytes)?;
285            if key.is_empty() {
286                break;
287            }
288            let value = read_cstring(&mut param_bytes)?;
289            params.insert(key, value);
290        }
291
292        Ok(Some(StartupMessage::Startup {
293            protocol_version,
294            params,
295        }))
296    }
297
298    /// Decode a regular message (with tag byte)
299    pub fn decode_message(&self, src: &mut BytesMut) -> Result<Option<Message>> {
300        if src.len() < 5 {
301            return Ok(None);
302        }
303
304        let tag = src[0];
305        let len = u32::from_be_bytes([src[1], src[2], src[3], src[4]]) as usize;
306
307        if len > self.max_message_size {
308            return Err(ProxyError::Protocol(format!(
309                "Message too large: {} bytes",
310                len
311            )));
312        }
313
314        // The length field counts itself (4 bytes), so a well-formed message
315        // has `len >= 4`. A declared `len < 4` (e.g. the 5 bytes
316        // `Q 00 00 00 00`) would underflow `len - 4` to a huge usize and panic
317        // in `split_to`. Reject it as a protocol error — this decoder runs on
318        // every client frame and every backend response.
319        if len < 4 {
320            return Err(ProxyError::Protocol(format!(
321                "message length {} below minimum of 4",
322                len
323            )));
324        }
325
326        // Length includes itself, so total message is 1 (tag) + len
327        let total_len = 1 + len;
328        if src.len() < total_len {
329            return Ok(None);
330        }
331
332        src.advance(5); // Skip tag and length
333        let payload = src.split_to(len - 4); // Length includes the 4-byte length field
334
335        let msg_type = MessageType::from_tag(tag);
336        Ok(Some(Message::new(msg_type, payload)))
337    }
338
339    /// Encode a message
340    pub fn encode_message(&self, msg: &Message) -> BytesMut {
341        msg.encode()
342    }
343}
344
345/// Startup message variants
346#[derive(Debug, Clone)]
347pub enum StartupMessage {
348    /// Regular startup
349    Startup {
350        protocol_version: u32,
351        params: HashMap<String, String>,
352    },
353    /// SSL request
354    SSLRequest,
355    /// Cancel request
356    CancelRequest { pid: u32, key: u32 },
357}
358
359/// Read a null-terminated string from the buffer.
360///
361/// Scans for the null terminator in a single pass (no per-byte `get_u8`
362/// loop, no Vec growth), then hands the exact-size byte slice to `String`.
363/// On `BytesMut`, `split_to` is O(1), and `BytesMut -> Vec<u8>` is
364/// zero-copy when (as here) the split-off buffer has a single owner.
365fn read_cstring(buf: &mut BytesMut) -> Result<String> {
366    let end = buf.iter().position(|&b| b == 0).ok_or_else(|| {
367        ProxyError::Protocol("unterminated cstring in protocol buffer".to_string())
368    })?;
369
370    let bytes = buf.split_to(end);
371    buf.advance(1); // consume the null terminator
372
373    String::from_utf8(bytes.into())
374        .map_err(|e| ProxyError::Protocol(format!("Invalid UTF-8 in cstring: {}", e)))
375}
376
377/// Write a null-terminated string to buffer
378fn write_cstring(buf: &mut BytesMut, s: &str) {
379    buf.extend_from_slice(s.as_bytes());
380    buf.put_u8(0);
381}
382
383/// Borrow the SQL text out of a Query/Parse-style payload without
384/// copying. Mirrors `read_cstring` semantics (bytes up to the first
385/// NUL, strict UTF-8) but never allocates — for hot-path inspection
386/// where the message itself is forwarded verbatim.
387pub fn query_text(payload: &[u8]) -> Option<&str> {
388    let end = payload.iter().position(|&b| b == 0)?;
389    std::str::from_utf8(&payload[..end]).ok()
390}
391
392/// Case-insensitive ASCII prefix test without allocating an
393/// uppercased copy of the haystack.
394pub fn starts_with_ci(s: &str, prefix: &str) -> bool {
395    s.len() >= prefix.len() && s.as_bytes()[..prefix.len()].eq_ignore_ascii_case(prefix.as_bytes())
396}
397
398/// Case-insensitive ASCII substring test without allocating.
399pub fn contains_ci(haystack: &str, needle: &str) -> bool {
400    if needle.is_empty() {
401        return true;
402    }
403    if haystack.len() < needle.len() {
404        return false;
405    }
406    haystack
407        .as_bytes()
408        .windows(needle.len())
409        .any(|w| w.eq_ignore_ascii_case(needle.as_bytes()))
410}
411
412/// Query message payload
413#[derive(Debug, Clone)]
414pub struct QueryMessage {
415    pub query: String,
416}
417
418impl QueryMessage {
419    /// Parse from message payload
420    pub fn parse(mut payload: BytesMut) -> Result<Self> {
421        let query = read_cstring(&mut payload)?;
422        Ok(Self { query })
423    }
424
425    /// Encode to message
426    pub fn encode(&self) -> Message {
427        let mut payload = BytesMut::new();
428        write_cstring(&mut payload, &self.query);
429        Message::new(MessageType::Query, payload)
430    }
431}
432
433/// Parse message payload (prepared statement)
434#[derive(Debug, Clone)]
435pub struct ParseMessage {
436    pub name: String,
437    pub query: String,
438    pub param_types: Vec<u32>,
439}
440
441impl ParseMessage {
442    /// Parse from message payload
443    pub fn parse(mut payload: BytesMut) -> Result<Self> {
444        let name = read_cstring(&mut payload)?;
445        let query = read_cstring(&mut payload)?;
446
447        let num_params = payload.get_u16() as usize;
448        let mut param_types = Vec::with_capacity(num_params);
449
450        for _ in 0..num_params {
451            param_types.push(payload.get_u32());
452        }
453
454        Ok(Self {
455            name,
456            query,
457            param_types,
458        })
459    }
460
461    /// Encode to message
462    pub fn encode(&self) -> Message {
463        let mut payload = BytesMut::new();
464        write_cstring(&mut payload, &self.name);
465        write_cstring(&mut payload, &self.query);
466        payload.put_u16(self.param_types.len() as u16);
467        for &t in &self.param_types {
468            payload.put_u32(t);
469        }
470        Message::new(MessageType::Parse, payload)
471    }
472}
473
474/// Bind message payload
475///
476/// `param_values` uses [`bytes::Bytes`] so parameter values are held by
477/// reference into the original protocol buffer — no per-parameter `Vec`
478/// allocation during parse.
479#[derive(Debug, Clone)]
480pub struct BindMessage {
481    pub portal: String,
482    pub statement: String,
483    pub param_formats: Vec<i16>,
484    pub param_values: Vec<Option<Bytes>>,
485    pub result_formats: Vec<i16>,
486}
487
488impl BindMessage {
489    /// Parse from message payload
490    pub fn parse(mut payload: BytesMut) -> Result<Self> {
491        let portal = read_cstring(&mut payload)?;
492        let statement = read_cstring(&mut payload)?;
493
494        // Parameter formats
495        let num_formats = payload.get_u16() as usize;
496        let mut param_formats = Vec::with_capacity(num_formats);
497        for _ in 0..num_formats {
498            param_formats.push(payload.get_i16());
499        }
500
501        // Parameter values — zero-copy: `split_to` slices the Arc'd buffer
502        // and `freeze()` turns the split-off `BytesMut` into a shared
503        // `Bytes` without allocating.
504        let num_values = payload.get_u16() as usize;
505        let mut param_values = Vec::with_capacity(num_values);
506        for _ in 0..num_values {
507            let len = payload.get_i32();
508            if len == -1 {
509                param_values.push(None);
510            } else {
511                let value = payload.split_to(len as usize).freeze();
512                param_values.push(Some(value));
513            }
514        }
515
516        // Result formats
517        let num_result_formats = payload.get_u16() as usize;
518        let mut result_formats = Vec::with_capacity(num_result_formats);
519        for _ in 0..num_result_formats {
520            result_formats.push(payload.get_i16());
521        }
522
523        Ok(Self {
524            portal,
525            statement,
526            param_formats,
527            param_values,
528            result_formats,
529        })
530    }
531}
532
533/// Execute message payload
534#[derive(Debug, Clone)]
535pub struct ExecuteMessage {
536    pub portal: String,
537    pub max_rows: i32,
538}
539
540impl ExecuteMessage {
541    /// Parse from message payload
542    pub fn parse(mut payload: BytesMut) -> Result<Self> {
543        let portal = read_cstring(&mut payload)?;
544        let max_rows = payload.get_i32();
545        Ok(Self { portal, max_rows })
546    }
547
548    /// Encode to message
549    pub fn encode(&self) -> Message {
550        let mut payload = BytesMut::new();
551        write_cstring(&mut payload, &self.portal);
552        payload.put_i32(self.max_rows);
553        Message::new(MessageType::Execute, payload)
554    }
555}
556
557/// Error response message
558#[derive(Debug, Clone)]
559pub struct ErrorResponse {
560    pub fields: HashMap<char, String>,
561}
562
563impl ErrorResponse {
564    /// Parse from message payload
565    pub fn parse(mut payload: BytesMut) -> Result<Self> {
566        let mut fields = HashMap::new();
567
568        while payload.has_remaining() {
569            let code = payload.get_u8();
570            if code == 0 {
571                break;
572            }
573            let value = read_cstring(&mut payload)?;
574            fields.insert(code as char, value);
575        }
576
577        Ok(Self { fields })
578    }
579
580    /// Get severity
581    pub fn severity(&self) -> Option<&str> {
582        self.fields.get(&'S').map(|s| s.as_str())
583    }
584
585    /// Get error code
586    pub fn code(&self) -> Option<&str> {
587        self.fields.get(&'C').map(|s| s.as_str())
588    }
589
590    /// Get message
591    pub fn message(&self) -> Option<&str> {
592        self.fields.get(&'M').map(|s| s.as_str())
593    }
594
595    /// Encode to message
596    pub fn encode(&self) -> Message {
597        let mut payload = BytesMut::new();
598        for (&code, value) in &self.fields {
599            payload.put_u8(code as u8);
600            write_cstring(&mut payload, value);
601        }
602        payload.put_u8(0);
603        Message::new(MessageType::ErrorResponse, payload)
604    }
605}
606
607/// Ready for query message
608#[derive(Debug, Clone, Copy, PartialEq, Eq)]
609pub enum TransactionStatus {
610    /// Idle (not in transaction)
611    Idle,
612    /// In transaction block
613    InTransaction,
614    /// In failed transaction block
615    Failed,
616}
617
618impl TransactionStatus {
619    /// Parse from byte
620    pub fn from_byte(b: u8) -> Self {
621        match b {
622            b'I' => TransactionStatus::Idle,
623            b'T' => TransactionStatus::InTransaction,
624            b'E' => TransactionStatus::Failed,
625            _ => TransactionStatus::Idle,
626        }
627    }
628
629    /// Convert to byte
630    pub fn to_byte(&self) -> u8 {
631        match self {
632            TransactionStatus::Idle => b'I',
633            TransactionStatus::InTransaction => b'T',
634            TransactionStatus::Failed => b'E',
635        }
636    }
637}
638
639/// Command complete message
640#[derive(Debug, Clone)]
641pub struct CommandComplete {
642    pub tag: String,
643}
644
645impl CommandComplete {
646    /// Parse from message payload
647    pub fn parse(mut payload: BytesMut) -> Result<Self> {
648        let tag = read_cstring(&mut payload)?;
649        Ok(Self { tag })
650    }
651
652    /// Encode to message
653    pub fn encode(&self) -> Message {
654        let mut payload = BytesMut::new();
655        write_cstring(&mut payload, &self.tag);
656        Message::new(MessageType::CommandComplete, payload)
657    }
658
659    /// Get rows affected for INSERT/UPDATE/DELETE
660    pub fn rows_affected(&self) -> Option<u64> {
661        let parts: Vec<&str> = self.tag.split_whitespace().collect();
662        if parts.len() >= 2 {
663            parts.last()?.parse().ok()
664        } else {
665            None
666        }
667    }
668}
669
670/// Authentication request types
671#[derive(Debug, Clone)]
672pub enum AuthRequest {
673    /// Authentication OK
674    Ok,
675    /// Cleartext password
676    CleartextPassword,
677    /// MD5 password
678    Md5Password { salt: [u8; 4] },
679    /// SASL
680    SASL { mechanisms: Vec<String> },
681    /// SASL continue
682    SASLContinue { data: Vec<u8> },
683    /// SASL final
684    SASLFinal { data: Vec<u8> },
685    /// Unknown
686    Unknown(i32),
687}
688
689impl AuthRequest {
690    /// Parse from message payload
691    pub fn parse(mut payload: BytesMut) -> Result<Self> {
692        let auth_type = payload.get_i32();
693
694        Ok(match auth_type {
695            0 => AuthRequest::Ok,
696            3 => AuthRequest::CleartextPassword,
697            5 => {
698                let mut salt = [0u8; 4];
699                payload.copy_to_slice(&mut salt);
700                AuthRequest::Md5Password { salt }
701            }
702            10 => {
703                let mut mechanisms = Vec::new();
704                loop {
705                    let mech = read_cstring(&mut payload)?;
706                    if mech.is_empty() {
707                        break;
708                    }
709                    mechanisms.push(mech);
710                }
711                AuthRequest::SASL { mechanisms }
712            }
713            11 => {
714                let data = payload.to_vec();
715                AuthRequest::SASLContinue { data }
716            }
717            12 => {
718                let data = payload.to_vec();
719                AuthRequest::SASLFinal { data }
720            }
721            _ => AuthRequest::Unknown(auth_type),
722        })
723    }
724
725    /// Encode to message
726    pub fn encode(&self) -> Message {
727        let mut payload = BytesMut::new();
728
729        match self {
730            AuthRequest::Ok => {
731                payload.put_i32(0);
732            }
733            AuthRequest::CleartextPassword => {
734                payload.put_i32(3);
735            }
736            AuthRequest::Md5Password { salt } => {
737                payload.put_i32(5);
738                payload.extend_from_slice(salt);
739            }
740            AuthRequest::SASL { mechanisms } => {
741                payload.put_i32(10);
742                for mech in mechanisms {
743                    write_cstring(&mut payload, mech);
744                }
745                payload.put_u8(0);
746            }
747            AuthRequest::SASLContinue { data } => {
748                payload.put_i32(11);
749                payload.extend_from_slice(data);
750            }
751            AuthRequest::SASLFinal { data } => {
752                payload.put_i32(12);
753                payload.extend_from_slice(data);
754            }
755            AuthRequest::Unknown(t) => {
756                payload.put_i32(*t);
757            }
758        }
759
760        Message::new(MessageType::AuthRequest, payload)
761    }
762}
763
764#[cfg(test)]
765mod tests {
766    use super::*;
767
768    #[test]
769    fn test_message_type_round_trip() {
770        let types = vec![
771            MessageType::Query,
772            MessageType::Parse,
773            MessageType::Bind,
774            MessageType::Execute,
775            MessageType::Sync,
776        ];
777
778        for msg_type in types {
779            if let Some(tag) = msg_type.to_tag() {
780                let decoded = MessageType::from_tag(tag);
781                assert_eq!(decoded, msg_type);
782            }
783        }
784    }
785
786    #[test]
787    fn test_auth_request_tag_mapping() {
788        // Regression: 'R' (AuthenticationRequest) must decode to AuthRequest,
789        // not Unknown(82) — the backend client matches on this to authenticate.
790        assert_eq!(MessageType::from_tag(b'R'), MessageType::AuthRequest);
791        assert_eq!(MessageType::AuthRequest.to_tag(), Some(b'R'));
792    }
793
794    #[test]
795    fn test_query_message() {
796        let query = QueryMessage {
797            query: "SELECT 1".to_string(),
798        };
799        let msg = query.encode();
800        assert_eq!(msg.msg_type, MessageType::Query);
801
802        let decoded = QueryMessage::parse(msg.payload).unwrap();
803        assert_eq!(decoded.query, "SELECT 1");
804    }
805
806    #[test]
807    fn test_error_response() {
808        let mut fields = HashMap::new();
809        fields.insert('S', "ERROR".to_string());
810        fields.insert('C', "42P01".to_string());
811        fields.insert('M', "relation does not exist".to_string());
812
813        let err = ErrorResponse { fields };
814        assert_eq!(err.severity(), Some("ERROR"));
815        assert_eq!(err.code(), Some("42P01"));
816        assert_eq!(err.message(), Some("relation does not exist"));
817    }
818
819    #[test]
820    fn test_command_complete() {
821        let cmd = CommandComplete {
822            tag: "INSERT 0 5".to_string(),
823        };
824        assert_eq!(cmd.rows_affected(), Some(5));
825
826        let cmd2 = CommandComplete {
827            tag: "SELECT 100".to_string(),
828        };
829        assert_eq!(cmd2.rows_affected(), Some(100));
830    }
831
832    #[test]
833    fn test_transaction_status() {
834        assert_eq!(TransactionStatus::from_byte(b'I'), TransactionStatus::Idle);
835        assert_eq!(
836            TransactionStatus::from_byte(b'T'),
837            TransactionStatus::InTransaction
838        );
839        assert_eq!(
840            TransactionStatus::from_byte(b'E'),
841            TransactionStatus::Failed
842        );
843
844        assert_eq!(TransactionStatus::Idle.to_byte(), b'I');
845        assert_eq!(TransactionStatus::InTransaction.to_byte(), b'T');
846        assert_eq!(TransactionStatus::Failed.to_byte(), b'E');
847    }
848
849    #[test]
850    fn test_protocol_codec() {
851        let codec = ProtocolCodec::new();
852        let query = QueryMessage {
853            query: "SELECT 1".to_string(),
854        };
855        let msg = query.encode();
856        let encoded = codec.encode_message(&msg);
857
858        assert!(encoded.len() > 5);
859        assert_eq!(encoded[0], b'Q');
860    }
861
862    /// An unterminated cstring must surface a protocol error, not be
863    /// silently treated as the full remaining buffer (as the old
864    /// incremental-push loop did).
865    #[test]
866    fn test_read_cstring_unterminated() {
867        let mut buf = BytesMut::from("not-null-terminated");
868        let err = read_cstring(&mut buf).expect_err("should reject unterminated cstring");
869        assert!(
870            matches!(err, ProxyError::Protocol(_)),
871            "expected Protocol error, got {err:?}"
872        );
873    }
874
875    /// Multiple cstrings back-to-back in the same buffer must parse
876    /// independently and leave the tail intact for subsequent fields.
877    #[test]
878    fn test_read_cstring_sequence() {
879        let mut buf = BytesMut::new();
880        buf.put_slice(b"first\0second\0tail");
881        let a = read_cstring(&mut buf).unwrap();
882        let b = read_cstring(&mut buf).unwrap();
883        assert_eq!(a, "first");
884        assert_eq!(b, "second");
885        assert_eq!(&buf[..], b"tail");
886    }
887
888    /// A regular frame whose declared length is below the 4-byte minimum must
889    /// surface a protocol error, not underflow `len - 4` and panic in
890    /// `split_to`. Guards the unauthenticated hot path.
891    #[test]
892    fn test_decode_message_rejects_short_length() {
893        let codec = ProtocolCodec::new();
894        // 'Q' + length field 0 — a legal 5-byte frame with an illegal length.
895        let mut buf = BytesMut::from(&b"Q\x00\x00\x00\x00"[..]);
896        let err = codec
897            .decode_message(&mut buf)
898            .expect_err("len < 4 must be rejected");
899        assert!(matches!(err, ProxyError::Protocol(_)), "got {err:?}");
900        // Length 3 (still < 4) with a byte of body present.
901        let mut buf = BytesMut::from(&b"Q\x00\x00\x00\x03x"[..]);
902        assert!(codec.decode_message(&mut buf).is_err());
903    }
904
905    /// A startup frame shorter than the 8-byte minimum (length+version) must be
906    /// rejected rather than panicking in `get_u32` / underflowing `len - 8`.
907    /// This is the first packet of every connection, pre-auth.
908    #[test]
909    fn test_decode_startup_rejects_short_length() {
910        let codec = ProtocolCodec::new();
911        // Declared length 4, no version — the classic 4-byte crash probe.
912        let mut buf = BytesMut::from(&b"\x00\x00\x00\x04"[..]);
913        let err = codec
914            .decode_startup(&mut buf)
915            .expect_err("len < 8 must be rejected");
916        assert!(matches!(err, ProxyError::Protocol(_)), "got {err:?}");
917        // Declared length 7 (still < 8) with the bytes present.
918        let mut buf = BytesMut::from(&b"\x00\x00\x00\x07\x00\x03\x00"[..]);
919        assert!(codec.decode_startup(&mut buf).is_err());
920    }
921
922    /// BindMessage parameter values are now `Bytes` (zero-copy), not
923    /// `Vec<u8>`. Round-trip a synthetic payload and confirm the
924    /// parsed values match.
925    #[test]
926    fn test_bind_message_param_values_are_bytes() {
927        let mut payload = BytesMut::new();
928        // portal, statement (both empty)
929        payload.put_u8(0);
930        payload.put_u8(0);
931        // one param format: 0 (text)
932        payload.put_u16(1);
933        payload.put_i16(0);
934        // two params: "hi" (2 bytes) and NULL (-1)
935        payload.put_u16(2);
936        payload.put_i32(2);
937        payload.put_slice(b"hi");
938        payload.put_i32(-1);
939        // zero result formats
940        payload.put_u16(0);
941
942        let bind = BindMessage::parse(payload).expect("parse failed");
943        assert_eq!(bind.param_values.len(), 2);
944        match &bind.param_values[0] {
945            Some(b) => assert_eq!(b.as_ref(), b"hi"),
946            None => panic!("first param must be Some"),
947        }
948        assert!(bind.param_values[1].is_none());
949    }
950}