Skip to main content

powdb_server/
protocol.rs

1use tokio::io::{AsyncReadExt, AsyncWriteExt};
2use zeroize::Zeroizing;
3
4const MSG_CONNECT: u8 = 0x01;
5const MSG_CONNECT_OK: u8 = 0x02;
6const MSG_QUERY: u8 = 0x03;
7/// Query carrying positional `$N` parameters (Task 4). Pure protocol
8/// addition: old clients never send it, and old servers reject it with the
9/// existing "unknown message type" error — no existing frame changes shape.
10const MSG_QUERY_PARAMS: u8 = 0x04;
11/// SQL query frame. Plain Query remains PowQL for backward compatibility.
12const MSG_QUERY_SQL: u8 = 0x05;
13/// Private sync control frames. These are append-only protocol extensions:
14/// legacy query/result frames keep their original tags and shape.
15const MSG_SYNC_STATUS: u8 = 0x20;
16const MSG_SYNC_PULL: u8 = 0x21;
17const MSG_SYNC_ACK: u8 = 0x22;
18const MSG_SYNC_STATUS_RESULT: u8 = 0x23;
19const MSG_SYNC_PULL_RESULT: u8 = 0x24;
20const MSG_SYNC_ACK_RESULT: u8 = 0x25;
21const MSG_RESULT_ROWS: u8 = 0x07;
22const MSG_RESULT_SCALAR: u8 = 0x08;
23const MSG_RESULT_OK: u8 = 0x09;
24const MSG_ERROR: u8 = 0x0A;
25const MSG_RESULT_MSG: u8 = 0x0B;
26const MSG_DISCONNECT: u8 = 0x10;
27const MSG_PING: u8 = 0x11;
28const MSG_PONG: u8 = 0x12;
29
30/// Maximum payload size accepted from the wire (64 MB).
31const MAX_PAYLOAD_SIZE: usize = 64 * 1024 * 1024;
32
33/// Maximum payload size for pre-auth CONNECT messages (4 KB).
34/// Only a database name and password are needed before authentication.
35const MAX_CONNECT_PAYLOAD_SIZE: usize = 4096;
36
37/// Maximum number of columns allowed in a result set.
38const MAX_COLUMNS: usize = 4096;
39
40/// Maximum number of rows allowed in a single result message.
41const MAX_ROWS: usize = 10_000_000;
42
43/// Maximum number of bound parameters in a single QueryWithParams message.
44const MAX_PARAMS: usize = 4096;
45
46/// Maximum retained units accepted in one sync pull frame.
47const MAX_SYNC_UNITS: usize = 4096;
48
49const STRING_LEN_PREFIX: usize = 4; // decode_string reads a 4-byte length prefix
50
51/// A positional parameter value carried by [`Message::QueryWithParams`].
52///
53/// Wire encoding per param: a 1-byte tag followed by the body —
54///   `0` null (no body), `1` int (8B LE i64), `2` float (8B LE f64),
55///   `3` bool (1B), `4` str (length-prefixed UTF-8).
56#[derive(Debug, Clone, PartialEq)]
57pub enum WireParam {
58    Null,
59    Int(i64),
60    Float(f64),
61    Bool(bool),
62    Str(String),
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum WireSyncRepairAction {
67    None,
68    Pull,
69    AwaitArchive,
70    Rebootstrap,
71}
72
73#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct WireSyncStatus {
75    pub replica_id: String,
76    pub active: bool,
77    pub last_applied_lsn: Option<u64>,
78    pub remote_lsn: u64,
79    pub servable_lsn: Option<u64>,
80    pub unarchived_lsn: Option<u64>,
81    pub lag_lsn: Option<u64>,
82    pub lag_bytes: Option<u64>,
83    pub lag_ms: Option<u64>,
84    pub stale: bool,
85    pub repair_action: WireSyncRepairAction,
86    pub last_sync_error: Option<String>,
87}
88
89#[derive(Debug, Clone, PartialEq, Eq)]
90pub struct WireRetainedUnit {
91    pub tx_id: u64,
92    pub record_type: u8,
93    pub lsn: u64,
94    pub data: Vec<u8>,
95}
96
97impl WireRetainedUnit {
98    /// Encoded byte length of this retained unit inside a sync pull result
99    /// payload. Keep this next to `encode_retained_unit` so metrics and
100    /// max-byte enforcement evolve with the wire shape.
101    pub fn encoded_len(&self) -> Result<u64, String> {
102        let data_len = u64::try_from(self.data.len())
103            .map_err(|_| "sync retained unit payload too large".to_string())?;
104        8u64.checked_add(1)
105            .and_then(|n| n.checked_add(8))
106            .and_then(|n| n.checked_add(4))
107            .and_then(|n| n.checked_add(data_len))
108            .ok_or_else(|| "sync retained unit encoded length overflow".to_string())
109    }
110}
111
112#[derive(Debug, Clone)]
113pub enum Message {
114    Connect {
115        db_name: String,
116        /// Client-supplied candidate password. Wrapped in `Zeroizing` so the
117        /// raw bytes from the wire are wiped from memory once the `Message`
118        /// (and thus this field) is dropped after the constant-time compare —
119        /// the candidate never lingers in a plain `String`.
120        password: Option<Zeroizing<String>>,
121        /// Optional user name for multi-user authentication. Appended after the
122        /// password on the wire so the format is a pure, backward-compatible
123        /// extension: old clients that omit it decode as `None`.
124        username: Option<String>,
125    },
126    ConnectOk {
127        version: String,
128    },
129    Query {
130        query: String,
131    },
132    /// A SQL query string.
133    QuerySql {
134        query: String,
135    },
136    /// A query string with positional `$N` parameters bound at the server.
137    QueryWithParams {
138        query: String,
139        params: Vec<WireParam>,
140    },
141    /// Request primary-side status for one embedded replica cursor.
142    SyncStatus {
143        replica_id: String,
144    },
145    /// Pull a bounded retained-unit chunk after the server-side replica cursor.
146    SyncPull {
147        replica_id: String,
148        since_lsn: u64,
149        max_units: u32,
150        max_bytes: u64,
151        database_id: [u8; 16],
152        primary_generation: u64,
153        wal_format_version: u16,
154        catalog_version: u16,
155        segment_format_version: u16,
156    },
157    /// Acknowledge that the replica applied retained history through `applied_lsn`.
158    SyncAck {
159        replica_id: String,
160        applied_lsn: u64,
161        remote_lsn: u64,
162    },
163    SyncStatusResult {
164        status: WireSyncStatus,
165    },
166    SyncPullResult {
167        status: WireSyncStatus,
168        units: Vec<WireRetainedUnit>,
169        has_more: bool,
170    },
171    SyncAckResult {
172        previous_applied_lsn: u64,
173        applied_lsn: u64,
174        remote_lsn: u64,
175        advanced: bool,
176        status: WireSyncStatus,
177    },
178    ResultRows {
179        columns: Vec<String>,
180        rows: Vec<Vec<String>>,
181    },
182    ResultScalar {
183        value: String,
184    },
185    ResultOk {
186        affected: u64,
187    },
188    /// A descriptive status message (e.g. "type User created", "index dropped").
189    ResultMessage {
190        message: String,
191    },
192    Error {
193        message: String,
194    },
195    Disconnect,
196    Ping,
197    Pong,
198}
199
200impl Message {
201    /// Encode message into wire format: [type(1)][flags(1)][len(4)][payload]
202    pub fn encode(&self) -> Vec<u8> {
203        let (msg_type, payload) = match self {
204            Message::Connect {
205                db_name,
206                password,
207                username,
208            } => {
209                let mut buf = encode_string(db_name);
210                // Password is encoded as a length-prefixed string. Empty (len=0) means None.
211                match password {
212                    Some(p) => buf.extend_from_slice(&encode_string(p)),
213                    None => buf.extend_from_slice(&0u32.to_le_bytes()),
214                }
215                // Username is appended after the password (append-only extension).
216                // Length-prefixed string; empty (len=0) means None.
217                match username {
218                    Some(u) => buf.extend_from_slice(&encode_string(u)),
219                    None => buf.extend_from_slice(&0u32.to_le_bytes()),
220                }
221                (MSG_CONNECT, buf)
222            }
223            Message::ConnectOk { version } => (MSG_CONNECT_OK, encode_string(version)),
224            Message::Query { query } => (MSG_QUERY, encode_string(query)),
225            Message::QuerySql { query } => (MSG_QUERY_SQL, encode_string(query)),
226            Message::QueryWithParams { query, params } => {
227                let mut buf = encode_string(query);
228                buf.extend_from_slice(&(params.len() as u16).to_le_bytes());
229                for p in params {
230                    match p {
231                        WireParam::Null => buf.push(0),
232                        WireParam::Int(v) => {
233                            buf.push(1);
234                            buf.extend_from_slice(&v.to_le_bytes());
235                        }
236                        WireParam::Float(v) => {
237                            buf.push(2);
238                            buf.extend_from_slice(&v.to_le_bytes());
239                        }
240                        WireParam::Bool(v) => {
241                            buf.push(3);
242                            buf.push(if *v { 1 } else { 0 });
243                        }
244                        WireParam::Str(s) => {
245                            buf.push(4);
246                            buf.extend_from_slice(&encode_string(s));
247                        }
248                    }
249                }
250                (MSG_QUERY_PARAMS, buf)
251            }
252            Message::SyncStatus { replica_id } => (MSG_SYNC_STATUS, encode_string(replica_id)),
253            Message::SyncPull {
254                replica_id,
255                since_lsn,
256                max_units,
257                max_bytes,
258                database_id,
259                primary_generation,
260                wal_format_version,
261                catalog_version,
262                segment_format_version,
263            } => {
264                let mut buf = encode_string(replica_id);
265                buf.extend_from_slice(&since_lsn.to_le_bytes());
266                buf.extend_from_slice(&max_units.to_le_bytes());
267                buf.extend_from_slice(&max_bytes.to_le_bytes());
268                buf.extend_from_slice(database_id);
269                buf.extend_from_slice(&primary_generation.to_le_bytes());
270                buf.extend_from_slice(&wal_format_version.to_le_bytes());
271                buf.extend_from_slice(&catalog_version.to_le_bytes());
272                buf.extend_from_slice(&segment_format_version.to_le_bytes());
273                (MSG_SYNC_PULL, buf)
274            }
275            Message::SyncAck {
276                replica_id,
277                applied_lsn,
278                remote_lsn,
279            } => {
280                let mut buf = encode_string(replica_id);
281                buf.extend_from_slice(&applied_lsn.to_le_bytes());
282                buf.extend_from_slice(&remote_lsn.to_le_bytes());
283                (MSG_SYNC_ACK, buf)
284            }
285            Message::SyncStatusResult { status } => {
286                (MSG_SYNC_STATUS_RESULT, encode_sync_status(status))
287            }
288            Message::SyncPullResult {
289                status,
290                units,
291                has_more,
292            } => {
293                let mut buf = encode_sync_status(status);
294                buf.extend_from_slice(&(units.len() as u32).to_le_bytes());
295                for unit in units {
296                    encode_retained_unit(&mut buf, unit);
297                }
298                buf.push(u8::from(*has_more));
299                (MSG_SYNC_PULL_RESULT, buf)
300            }
301            Message::SyncAckResult {
302                previous_applied_lsn,
303                applied_lsn,
304                remote_lsn,
305                advanced,
306                status,
307            } => {
308                let mut buf = Vec::new();
309                buf.extend_from_slice(&previous_applied_lsn.to_le_bytes());
310                buf.extend_from_slice(&applied_lsn.to_le_bytes());
311                buf.extend_from_slice(&remote_lsn.to_le_bytes());
312                buf.push(u8::from(*advanced));
313                buf.extend_from_slice(&encode_sync_status(status));
314                (MSG_SYNC_ACK_RESULT, buf)
315            }
316            Message::ResultRows { columns, rows } => {
317                let mut buf = Vec::new();
318                buf.extend_from_slice(&(columns.len() as u16).to_le_bytes());
319                for col in columns {
320                    buf.extend_from_slice(&encode_string(col));
321                }
322                buf.extend_from_slice(&(rows.len() as u32).to_le_bytes());
323                for row in rows {
324                    for val in row {
325                        buf.extend_from_slice(&encode_string(val));
326                    }
327                }
328                (MSG_RESULT_ROWS, buf)
329            }
330            Message::ResultScalar { value } => (MSG_RESULT_SCALAR, encode_string(value)),
331            Message::ResultOk { affected } => (MSG_RESULT_OK, affected.to_le_bytes().to_vec()),
332            Message::ResultMessage { message } => (MSG_RESULT_MSG, encode_string(message)),
333            Message::Error { message } => (MSG_ERROR, encode_string(message)),
334            Message::Disconnect => (MSG_DISCONNECT, Vec::new()),
335            Message::Ping => (MSG_PING, Vec::new()),
336            Message::Pong => (MSG_PONG, Vec::new()),
337        };
338
339        let mut frame = Vec::with_capacity(6 + payload.len());
340        frame.push(msg_type);
341        frame.push(0); // flags
342        frame.extend_from_slice(&(payload.len() as u32).to_le_bytes());
343        frame.extend_from_slice(&payload);
344        frame
345    }
346
347    /// Decode message from wire format.
348    pub fn decode(data: &[u8]) -> Result<Message, String> {
349        if data.len() < 6 {
350            return Err("frame too short".into());
351        }
352        let msg_type = data[0];
353        let _flags = data[1];
354        let len_bytes: [u8; 4] = data[2..6]
355            .try_into()
356            .map_err(|_| "invalid header length field".to_string())?;
357        let payload_len = u32::from_le_bytes(len_bytes) as usize;
358        if 6 + payload_len > data.len() {
359            return Err("payload length exceeds frame".into());
360        }
361        let payload = &data[6..6 + payload_len];
362
363        match msg_type {
364            MSG_CONNECT => {
365                let mut pos = 0;
366                let db_name = decode_string(payload, &mut pos)?;
367                // Password is optional. If there are no more bytes, treat as None
368                // (backwards compatible with old clients that don't send a password).
369                let password = if pos < payload.len() {
370                    // Wrap the candidate immediately so the only owned copy of
371                    // the secret lives inside `Zeroizing` and is wiped on drop.
372                    let p = Zeroizing::new(decode_string(payload, &mut pos)?);
373                    if p.is_empty() {
374                        None
375                    } else {
376                        Some(p)
377                    }
378                } else {
379                    None
380                };
381                // Username is optional and appended after the password. Old
382                // clients omit it entirely → no more bytes → None.
383                let username = if pos < payload.len() {
384                    let u = decode_string(payload, &mut pos)?;
385                    if u.is_empty() {
386                        None
387                    } else {
388                        Some(u)
389                    }
390                } else {
391                    None
392                };
393                Ok(Message::Connect {
394                    db_name,
395                    password,
396                    username,
397                })
398            }
399            MSG_CONNECT_OK => {
400                let version = decode_string(payload, &mut 0)?;
401                Ok(Message::ConnectOk { version })
402            }
403            MSG_QUERY => {
404                let query = decode_string(payload, &mut 0)?;
405                Ok(Message::Query { query })
406            }
407            MSG_QUERY_SQL => {
408                let query = decode_string(payload, &mut 0)?;
409                Ok(Message::QuerySql { query })
410            }
411            MSG_QUERY_PARAMS => {
412                let mut pos = 0;
413                let query = decode_string(payload, &mut pos)?;
414                if pos + 2 > payload.len() {
415                    return Err("truncated param count".into());
416                }
417                let count_bytes: [u8; 2] = payload[pos..pos + 2]
418                    .try_into()
419                    .map_err(|_| "invalid param count bytes".to_string())?;
420                let count = u16::from_le_bytes(count_bytes) as usize;
421                pos += 2;
422                if count > MAX_PARAMS {
423                    return Err("too many parameters".into());
424                }
425                let mut params = Vec::with_capacity(count.min(payload.len() - pos));
426                for _ in 0..count {
427                    if pos >= payload.len() {
428                        return Err("truncated param tag".into());
429                    }
430                    let tag = payload[pos];
431                    pos += 1;
432                    let p = match tag {
433                        0 => WireParam::Null,
434                        1 => {
435                            if pos + 8 > payload.len() {
436                                return Err("truncated int param".into());
437                            }
438                            let b: [u8; 8] = payload[pos..pos + 8]
439                                .try_into()
440                                .map_err(|_| "invalid int param bytes".to_string())?;
441                            pos += 8;
442                            WireParam::Int(i64::from_le_bytes(b))
443                        }
444                        2 => {
445                            if pos + 8 > payload.len() {
446                                return Err("truncated float param".into());
447                            }
448                            let b: [u8; 8] = payload[pos..pos + 8]
449                                .try_into()
450                                .map_err(|_| "invalid float param bytes".to_string())?;
451                            pos += 8;
452                            WireParam::Float(f64::from_le_bytes(b))
453                        }
454                        3 => {
455                            if pos + 1 > payload.len() {
456                                return Err("truncated bool param".into());
457                            }
458                            let v = payload[pos] != 0;
459                            pos += 1;
460                            WireParam::Bool(v)
461                        }
462                        4 => WireParam::Str(decode_string(payload, &mut pos)?),
463                        other => return Err(format!("unknown param tag: {other}")),
464                    };
465                    params.push(p);
466                }
467                Ok(Message::QueryWithParams { query, params })
468            }
469            MSG_SYNC_STATUS => {
470                let replica_id = decode_string(payload, &mut 0)?;
471                Ok(Message::SyncStatus { replica_id })
472            }
473            MSG_SYNC_PULL => {
474                let mut pos = 0;
475                let replica_id = decode_string(payload, &mut pos)?;
476                let since_lsn = decode_u64(payload, &mut pos, "sync pull since LSN")?;
477                let max_units = decode_u32(payload, &mut pos, "sync pull max units")?;
478                let max_bytes = decode_u64(payload, &mut pos, "sync pull max bytes")?;
479                let database_id = decode_16_bytes(payload, &mut pos, "sync database id")?;
480                let primary_generation = decode_u64(payload, &mut pos, "sync primary generation")?;
481                let wal_format_version = decode_u16(payload, &mut pos, "sync WAL format version")?;
482                let catalog_version = decode_u16(payload, &mut pos, "sync catalog version")?;
483                let segment_format_version =
484                    decode_u16(payload, &mut pos, "sync segment format version")?;
485                Ok(Message::SyncPull {
486                    replica_id,
487                    since_lsn,
488                    max_units,
489                    max_bytes,
490                    database_id,
491                    primary_generation,
492                    wal_format_version,
493                    catalog_version,
494                    segment_format_version,
495                })
496            }
497            MSG_SYNC_ACK => {
498                let mut pos = 0;
499                let replica_id = decode_string(payload, &mut pos)?;
500                let applied_lsn = decode_u64(payload, &mut pos, "sync ack applied LSN")?;
501                let remote_lsn = decode_u64(payload, &mut pos, "sync ack remote LSN")?;
502                Ok(Message::SyncAck {
503                    replica_id,
504                    applied_lsn,
505                    remote_lsn,
506                })
507            }
508            MSG_SYNC_STATUS_RESULT => {
509                let mut pos = 0;
510                let status = decode_sync_status(payload, &mut pos)?;
511                Ok(Message::SyncStatusResult { status })
512            }
513            MSG_SYNC_PULL_RESULT => {
514                let mut pos = 0;
515                let status = decode_sync_status(payload, &mut pos)?;
516                let count = decode_u32(payload, &mut pos, "sync retained unit count")? as usize;
517                if count > MAX_SYNC_UNITS {
518                    return Err("too many retained units".into());
519                }
520                let mut units = Vec::with_capacity(count.min(payload.len().saturating_sub(pos)));
521                for _ in 0..count {
522                    units.push(decode_retained_unit(payload, &mut pos)?);
523                }
524                let has_more = decode_bool(payload, &mut pos, "sync has_more")?;
525                Ok(Message::SyncPullResult {
526                    status,
527                    units,
528                    has_more,
529                })
530            }
531            MSG_SYNC_ACK_RESULT => {
532                let mut pos = 0;
533                let previous_applied_lsn = decode_u64(payload, &mut pos, "previous applied LSN")?;
534                let applied_lsn = decode_u64(payload, &mut pos, "applied LSN")?;
535                let remote_lsn = decode_u64(payload, &mut pos, "remote LSN")?;
536                let advanced = decode_bool(payload, &mut pos, "sync ack advanced")?;
537                let status = decode_sync_status(payload, &mut pos)?;
538                Ok(Message::SyncAckResult {
539                    previous_applied_lsn,
540                    applied_lsn,
541                    remote_lsn,
542                    advanced,
543                    status,
544                })
545            }
546            MSG_RESULT_ROWS => {
547                let mut pos = 0;
548                if pos + 2 > payload.len() {
549                    return Err("truncated column count".into());
550                }
551                let col_bytes: [u8; 2] = payload[pos..pos + 2]
552                    .try_into()
553                    .map_err(|_| "invalid column count bytes".to_string())?;
554                let col_count = u16::from_le_bytes(col_bytes) as usize;
555                pos += 2;
556                if col_count > MAX_COLUMNS {
557                    return Err("too many columns".into());
558                }
559                let mut columns =
560                    Vec::with_capacity(col_count.min((payload.len() - pos) / STRING_LEN_PREFIX));
561                for _ in 0..col_count {
562                    columns.push(decode_string(payload, &mut pos)?);
563                }
564                if pos + 4 > payload.len() {
565                    return Err("truncated row count".into());
566                }
567                let row_bytes: [u8; 4] = payload[pos..pos + 4]
568                    .try_into()
569                    .map_err(|_| "invalid row count bytes".to_string())?;
570                let row_count = u32::from_le_bytes(row_bytes) as usize;
571                pos += 4;
572                if row_count > MAX_ROWS {
573                    return Err("too many rows".into());
574                }
575                // Never preallocate (or iterate) proportional to an untrusted count: each row
576                // carries `col_count` length-prefixed strings of >= STRING_LEN_PREFIX bytes, so
577                // the remaining payload bounds how many rows can follow. A zero-column row
578                // consumes no bytes (vacuous bound), so a tiny frame could otherwise declare
579                // millions of rows and force a huge allocation (reachable pre-auth). Reject it.
580                let max_rows = match col_count.checked_mul(STRING_LEN_PREFIX) {
581                    Some(0) | None => 0,
582                    Some(per_row) => (payload.len() - pos) / per_row,
583                };
584                if row_count > max_rows {
585                    return Err("row count exceeds payload size".into());
586                }
587                let mut rows = Vec::with_capacity(row_count);
588                for _ in 0..row_count {
589                    let mut row = Vec::with_capacity(col_count);
590                    for _ in 0..col_count {
591                        row.push(decode_string(payload, &mut pos)?);
592                    }
593                    rows.push(row);
594                }
595                Ok(Message::ResultRows { columns, rows })
596            }
597            MSG_RESULT_SCALAR => {
598                let value = decode_string(payload, &mut 0)?;
599                Ok(Message::ResultScalar { value })
600            }
601            MSG_RESULT_OK => {
602                if payload.len() < 8 {
603                    return Err("truncated result ok payload".into());
604                }
605                let aff_bytes: [u8; 8] = payload[0..8]
606                    .try_into()
607                    .map_err(|_| "invalid affected count bytes".to_string())?;
608                let affected = u64::from_le_bytes(aff_bytes);
609                Ok(Message::ResultOk { affected })
610            }
611            MSG_RESULT_MSG => {
612                let message = decode_string(payload, &mut 0)?;
613                Ok(Message::ResultMessage { message })
614            }
615            MSG_ERROR => {
616                let message = decode_string(payload, &mut 0)?;
617                Ok(Message::Error { message })
618            }
619            MSG_DISCONNECT => Ok(Message::Disconnect),
620            MSG_PING => Ok(Message::Ping),
621            MSG_PONG => Ok(Message::Pong),
622            _ => Err(format!("unknown message type: {msg_type:#x}")),
623        }
624    }
625
626    /// Write this message to an async writer.
627    pub async fn write_to<W: AsyncWriteExt + Unpin>(&self, writer: &mut W) -> std::io::Result<()> {
628        let bytes = self.encode();
629        writer.write_all(&bytes).await
630    }
631
632    /// Read a message from an async reader.
633    pub async fn read_from<R: AsyncReadExt + Unpin>(
634        reader: &mut R,
635    ) -> std::io::Result<Option<Message>> {
636        Self::read_from_with_limit(reader, MAX_PAYLOAD_SIZE).await
637    }
638
639    /// Read a pre-auth message with a smaller payload limit (4 KB).
640    /// Use this before authentication is complete to prevent oversized
641    /// CONNECT payloads from consuming server memory.
642    pub async fn read_from_preauth<R: AsyncReadExt + Unpin>(
643        reader: &mut R,
644    ) -> std::io::Result<Option<Message>> {
645        Self::read_from_with_limit(reader, MAX_CONNECT_PAYLOAD_SIZE).await
646    }
647
648    /// Read a message from an async reader with a configurable payload limit.
649    async fn read_from_with_limit<R: AsyncReadExt + Unpin>(
650        reader: &mut R,
651        max_payload: usize,
652    ) -> std::io::Result<Option<Message>> {
653        let mut header = [0u8; 6];
654        match reader.read_exact(&mut header).await {
655            Ok(_) => {}
656            Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None),
657            Err(e) => return Err(e),
658        }
659        let len_bytes: [u8; 4] = header[2..6].try_into().map_err(|_| {
660            std::io::Error::new(
661                std::io::ErrorKind::InvalidData,
662                "invalid header length field",
663            )
664        })?;
665        let payload_len = u32::from_le_bytes(len_bytes) as usize;
666        if payload_len > max_payload {
667            return Err(std::io::Error::new(
668                std::io::ErrorKind::InvalidData,
669                format!("payload too large: {payload_len} bytes (max {max_payload})"),
670            ));
671        }
672        let mut payload = vec![0u8; payload_len];
673        if payload_len > 0 {
674            reader.read_exact(&mut payload).await?;
675        }
676
677        let mut full = Vec::with_capacity(6 + payload_len);
678        full.extend_from_slice(&header);
679        full.extend_from_slice(&payload);
680
681        Message::decode(&full)
682            .map(Some)
683            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
684    }
685}
686
687fn encode_string(s: &str) -> Vec<u8> {
688    let mut buf = Vec::with_capacity(4 + s.len());
689    buf.extend_from_slice(&(s.len() as u32).to_le_bytes());
690    buf.extend_from_slice(s.as_bytes());
691    buf
692}
693
694fn decode_string(data: &[u8], pos: &mut usize) -> Result<String, String> {
695    if *pos + 4 > data.len() {
696        return Err("truncated string length".into());
697    }
698    let len_bytes: [u8; 4] = data[*pos..*pos + 4]
699        .try_into()
700        .map_err(|_| "invalid string length bytes".to_string())?;
701    let len = u32::from_le_bytes(len_bytes) as usize;
702    *pos += 4;
703    if *pos + len > data.len() {
704        return Err("truncated string data".into());
705    }
706    let s = String::from_utf8_lossy(&data[*pos..*pos + len]).into_owned();
707    *pos += len;
708    Ok(s)
709}
710
711fn encode_option_u64(out: &mut Vec<u8>, value: Option<u64>) {
712    match value {
713        Some(value) => {
714            out.push(1);
715            out.extend_from_slice(&value.to_le_bytes());
716        }
717        None => out.push(0),
718    }
719}
720
721fn decode_option_u64(data: &[u8], pos: &mut usize, label: &str) -> Result<Option<u64>, String> {
722    let present = decode_bool(data, pos, label)?;
723    if present {
724        Ok(Some(decode_u64(data, pos, label)?))
725    } else {
726        Ok(None)
727    }
728}
729
730fn encode_option_string(out: &mut Vec<u8>, value: Option<&String>) {
731    match value {
732        Some(value) => {
733            out.push(1);
734            out.extend_from_slice(&encode_string(value));
735        }
736        None => out.push(0),
737    }
738}
739
740fn decode_option_string(
741    data: &[u8],
742    pos: &mut usize,
743    label: &str,
744) -> Result<Option<String>, String> {
745    let present = decode_bool(data, pos, label)?;
746    if present {
747        Ok(Some(decode_string(data, pos)?))
748    } else {
749        Ok(None)
750    }
751}
752
753fn encode_sync_status(status: &WireSyncStatus) -> Vec<u8> {
754    let mut out = Vec::new();
755    out.extend_from_slice(&encode_string(&status.replica_id));
756    out.push(u8::from(status.active));
757    encode_option_u64(&mut out, status.last_applied_lsn);
758    out.extend_from_slice(&status.remote_lsn.to_le_bytes());
759    encode_option_u64(&mut out, status.servable_lsn);
760    encode_option_u64(&mut out, status.unarchived_lsn);
761    encode_option_u64(&mut out, status.lag_lsn);
762    encode_option_u64(&mut out, status.lag_bytes);
763    encode_option_u64(&mut out, status.lag_ms);
764    out.push(u8::from(status.stale));
765    out.push(match status.repair_action {
766        WireSyncRepairAction::None => 0,
767        WireSyncRepairAction::Pull => 1,
768        WireSyncRepairAction::AwaitArchive => 2,
769        WireSyncRepairAction::Rebootstrap => 3,
770    });
771    encode_option_string(&mut out, status.last_sync_error.as_ref());
772    out
773}
774
775fn decode_sync_status(data: &[u8], pos: &mut usize) -> Result<WireSyncStatus, String> {
776    let replica_id = decode_string(data, pos)?;
777    let active = decode_bool(data, pos, "sync status active")?;
778    let last_applied_lsn = decode_option_u64(data, pos, "sync status last applied LSN")?;
779    let remote_lsn = decode_u64(data, pos, "sync status remote LSN")?;
780    let servable_lsn = decode_option_u64(data, pos, "sync status servable LSN")?;
781    let unarchived_lsn = decode_option_u64(data, pos, "sync status unarchived LSN")?;
782    let lag_lsn = decode_option_u64(data, pos, "sync status lag LSN")?;
783    let lag_bytes = decode_option_u64(data, pos, "sync status lag bytes")?;
784    let lag_ms = decode_option_u64(data, pos, "sync status lag milliseconds")?;
785    let stale = decode_bool(data, pos, "sync status stale")?;
786    if *pos >= data.len() {
787        return Err("truncated sync repair action".into());
788    }
789    let repair_action = match data[*pos] {
790        0 => WireSyncRepairAction::None,
791        1 => WireSyncRepairAction::Pull,
792        2 => WireSyncRepairAction::AwaitArchive,
793        3 => WireSyncRepairAction::Rebootstrap,
794        other => return Err(format!("unknown sync repair action: {other}")),
795    };
796    *pos += 1;
797    let last_sync_error = decode_option_string(data, pos, "sync status last error")?;
798    Ok(WireSyncStatus {
799        replica_id,
800        active,
801        last_applied_lsn,
802        remote_lsn,
803        servable_lsn,
804        unarchived_lsn,
805        lag_lsn,
806        lag_bytes,
807        lag_ms,
808        stale,
809        repair_action,
810        last_sync_error,
811    })
812}
813
814fn encode_retained_unit(out: &mut Vec<u8>, unit: &WireRetainedUnit) {
815    out.extend_from_slice(&unit.tx_id.to_le_bytes());
816    out.push(unit.record_type);
817    out.extend_from_slice(&unit.lsn.to_le_bytes());
818    out.extend_from_slice(&(unit.data.len() as u32).to_le_bytes());
819    out.extend_from_slice(&unit.data);
820}
821
822fn decode_retained_unit(data: &[u8], pos: &mut usize) -> Result<WireRetainedUnit, String> {
823    let tx_id = decode_u64(data, pos, "sync retained unit tx id")?;
824    if *pos >= data.len() {
825        return Err("truncated sync retained unit record type".into());
826    }
827    let record_type = data[*pos];
828    *pos += 1;
829    let lsn = decode_u64(data, pos, "sync retained unit LSN")?;
830    let data = decode_bytes(data, pos, "sync retained unit payload")?;
831    Ok(WireRetainedUnit {
832        tx_id,
833        record_type,
834        lsn,
835        data,
836    })
837}
838
839fn decode_bool(data: &[u8], pos: &mut usize, label: &str) -> Result<bool, String> {
840    if *pos >= data.len() {
841        return Err(format!("truncated {label}"));
842    }
843    let raw = data[*pos];
844    *pos += 1;
845    match raw {
846        0 => Ok(false),
847        1 => Ok(true),
848        other => Err(format!("invalid boolean for {label}: {other}")),
849    }
850}
851
852fn decode_u16(data: &[u8], pos: &mut usize, label: &str) -> Result<u16, String> {
853    if *pos + 2 > data.len() {
854        return Err(format!("truncated {label}"));
855    }
856    let bytes: [u8; 2] = data[*pos..*pos + 2]
857        .try_into()
858        .map_err(|_| format!("invalid {label} bytes"))?;
859    *pos += 2;
860    Ok(u16::from_le_bytes(bytes))
861}
862
863fn decode_u32(data: &[u8], pos: &mut usize, label: &str) -> Result<u32, String> {
864    if *pos + 4 > data.len() {
865        return Err(format!("truncated {label}"));
866    }
867    let bytes: [u8; 4] = data[*pos..*pos + 4]
868        .try_into()
869        .map_err(|_| format!("invalid {label} bytes"))?;
870    *pos += 4;
871    Ok(u32::from_le_bytes(bytes))
872}
873
874fn decode_u64(data: &[u8], pos: &mut usize, label: &str) -> Result<u64, String> {
875    if *pos + 8 > data.len() {
876        return Err(format!("truncated {label}"));
877    }
878    let bytes: [u8; 8] = data[*pos..*pos + 8]
879        .try_into()
880        .map_err(|_| format!("invalid {label} bytes"))?;
881    *pos += 8;
882    Ok(u64::from_le_bytes(bytes))
883}
884
885fn decode_16_bytes(data: &[u8], pos: &mut usize, label: &str) -> Result<[u8; 16], String> {
886    if *pos + 16 > data.len() {
887        return Err(format!("truncated {label}"));
888    }
889    let mut out = [0u8; 16];
890    out.copy_from_slice(&data[*pos..*pos + 16]);
891    *pos += 16;
892    Ok(out)
893}
894
895fn decode_bytes(data: &[u8], pos: &mut usize, label: &str) -> Result<Vec<u8>, String> {
896    let len = decode_u32(data, pos, label)? as usize;
897    if *pos + len > data.len() {
898        return Err(format!("truncated {label}"));
899    }
900    let out = data[*pos..*pos + len].to_vec();
901    *pos += len;
902    Ok(out)
903}
904
905#[cfg(test)]
906mod tests {
907    use super::*;
908
909    #[test]
910    fn test_encode_decode_query() {
911        let msg = Message::Query {
912            query: "User filter .age > 30".into(),
913        };
914        let bytes = msg.encode();
915        let decoded = Message::decode(&bytes).unwrap();
916        match decoded {
917            Message::Query { query } => assert_eq!(query, "User filter .age > 30"),
918            _ => panic!("expected Query"),
919        }
920    }
921
922    #[test]
923    fn test_encode_decode_connect_with_username() {
924        let msg = Message::Connect {
925            db_name: "mydb".into(),
926            password: Some(Zeroizing::new("secret".into())),
927            username: Some("alice".into()),
928        };
929        let bytes = msg.encode();
930        match Message::decode(&bytes).unwrap() {
931            Message::Connect {
932                db_name,
933                password,
934                username,
935            } => {
936                assert_eq!(db_name, "mydb");
937                assert_eq!(password.as_deref().map(|s| s.as_str()), Some("secret"));
938                assert_eq!(username.as_deref(), Some("alice"));
939            }
940            other => panic!("expected Connect, got {other:?}"),
941        }
942    }
943
944    #[test]
945    fn test_encode_decode_connect_without_username() {
946        // New-format Connect that explicitly carries no username.
947        let msg = Message::Connect {
948            db_name: "mydb".into(),
949            password: Some(Zeroizing::new("secret".into())),
950            username: None,
951        };
952        let bytes = msg.encode();
953        match Message::decode(&bytes).unwrap() {
954            Message::Connect {
955                db_name,
956                password,
957                username,
958            } => {
959                assert_eq!(db_name, "mydb");
960                assert_eq!(password.as_deref().map(|s| s.as_str()), Some("secret"));
961                assert_eq!(username, None);
962            }
963            other => panic!("expected Connect, got {other:?}"),
964        }
965    }
966
967    #[test]
968    fn test_decode_old_client_connect_db_and_password_only() {
969        // Simulate an OLD client frame: db_name + password, with NO username
970        // bytes at all. Must decode with username: None (backward compat).
971        let mut payload = encode_string("mydb");
972        payload.extend_from_slice(&encode_string("pw"));
973        let mut frame = vec![MSG_CONNECT, 0];
974        frame.extend_from_slice(&(payload.len() as u32).to_le_bytes());
975        frame.extend_from_slice(&payload);
976        match Message::decode(&frame).unwrap() {
977            Message::Connect {
978                db_name,
979                password,
980                username,
981            } => {
982                assert_eq!(db_name, "mydb");
983                assert_eq!(password.as_deref().map(|s| s.as_str()), Some("pw"));
984                assert_eq!(username, None, "old-client frame must yield username=None");
985            }
986            other => panic!("expected Connect, got {other:?}"),
987        }
988    }
989
990    #[test]
991    fn test_decode_old_client_connect_db_only() {
992        // Oldest client: db_name only, no password and no username bytes.
993        let payload = encode_string("mydb");
994        let mut frame = vec![MSG_CONNECT, 0];
995        frame.extend_from_slice(&(payload.len() as u32).to_le_bytes());
996        frame.extend_from_slice(&payload);
997        match Message::decode(&frame).unwrap() {
998            Message::Connect {
999                db_name,
1000                password,
1001                username,
1002            } => {
1003                assert_eq!(db_name, "mydb");
1004                assert_eq!(password, None);
1005                assert_eq!(username, None);
1006            }
1007            other => panic!("expected Connect, got {other:?}"),
1008        }
1009    }
1010
1011    #[test]
1012    fn test_encode_decode_result_rows() {
1013        let msg = Message::ResultRows {
1014            columns: vec!["name".into(), "age".into()],
1015            rows: vec![
1016                vec!["Alice".into(), "30".into()],
1017                vec!["Bob".into(), "25".into()],
1018            ],
1019        };
1020        let bytes = msg.encode();
1021        let decoded = Message::decode(&bytes).unwrap();
1022        match decoded {
1023            Message::ResultRows { columns, rows } => {
1024                assert_eq!(columns, vec!["name", "age"]);
1025                assert_eq!(rows.len(), 2);
1026            }
1027            _ => panic!("expected ResultRows"),
1028        }
1029    }
1030
1031    #[test]
1032    fn test_encode_decode_result_message() {
1033        let msg = Message::ResultMessage {
1034            message: "type User created".into(),
1035        };
1036        let bytes = msg.encode();
1037        let decoded = Message::decode(&bytes).unwrap();
1038        match decoded {
1039            Message::ResultMessage { message } => assert_eq!(message, "type User created"),
1040            _ => panic!("expected ResultMessage"),
1041        }
1042    }
1043
1044    #[test]
1045    fn test_encode_decode_error() {
1046        let msg = Message::Error {
1047            message: "table not found".into(),
1048        };
1049        let bytes = msg.encode();
1050        let decoded = Message::decode(&bytes).unwrap();
1051        match decoded {
1052            Message::Error { message } => assert_eq!(message, "table not found"),
1053            _ => panic!("expected Error"),
1054        }
1055    }
1056
1057    #[test]
1058    fn test_encode_decode_query_sql() {
1059        let msg = Message::QuerySql {
1060            query: "SELECT * FROM User".into(),
1061        };
1062        let decoded = Message::decode(&msg.encode()).unwrap();
1063        match decoded {
1064            Message::QuerySql { query } => assert_eq!(query, "SELECT * FROM User"),
1065            other => panic!("expected QuerySql, got {other:?}"),
1066        }
1067    }
1068
1069    #[test]
1070    fn test_encode_decode_query_with_params() {
1071        let msg = Message::QueryWithParams {
1072            query: "insert User { name := $1, age := $2, ok := $3, note := $4 }".into(),
1073            params: vec![
1074                WireParam::Str(r#"a"b\c; drop User"#.into()),
1075                WireParam::Int(-7),
1076                WireParam::Bool(true),
1077                WireParam::Null,
1078            ],
1079        };
1080        let bytes = msg.encode();
1081        // The new frame must use the dedicated 0x04 tag.
1082        assert_eq!(bytes[0], 0x04);
1083        match Message::decode(&bytes).unwrap() {
1084            Message::QueryWithParams { query, params } => {
1085                assert!(query.contains("$1"));
1086                assert_eq!(params.len(), 4);
1087                assert!(matches!(&params[0], WireParam::Str(s) if s == r#"a"b\c; drop User"#));
1088                assert!(matches!(&params[1], WireParam::Int(-7)));
1089                assert!(matches!(&params[2], WireParam::Bool(true)));
1090                assert!(matches!(&params[3], WireParam::Null));
1091            }
1092            other => panic!("expected QueryWithParams, got {other:?}"),
1093        }
1094    }
1095
1096    #[test]
1097    fn test_query_with_params_float_round_trip() {
1098        let msg = Message::QueryWithParams {
1099            query: "T filter .f = $1".into(),
1100            params: vec![WireParam::Float(2.5)],
1101        };
1102        match Message::decode(&msg.encode()).unwrap() {
1103            Message::QueryWithParams { params, .. } => {
1104                assert!(matches!(&params[0], WireParam::Float(f) if (*f - 2.5).abs() < 1e-12));
1105            }
1106            other => panic!("expected QueryWithParams, got {other:?}"),
1107        }
1108    }
1109
1110    fn sample_sync_status() -> WireSyncStatus {
1111        WireSyncStatus {
1112            replica_id: "replica-a".into(),
1113            active: true,
1114            last_applied_lsn: Some(7),
1115            remote_lsn: 10,
1116            servable_lsn: Some(10),
1117            unarchived_lsn: Some(0),
1118            lag_lsn: Some(3),
1119            lag_bytes: Some(2048),
1120            lag_ms: Some(5000),
1121            stale: true,
1122            repair_action: WireSyncRepairAction::Pull,
1123            last_sync_error: None,
1124        }
1125    }
1126
1127    #[test]
1128    fn test_encode_decode_sync_requests() {
1129        let database_id = *b"sync-protocol!!!";
1130        let pull = Message::SyncPull {
1131            replica_id: "replica-a".into(),
1132            since_lsn: 7,
1133            max_units: 128,
1134            max_bytes: 4096,
1135            database_id,
1136            primary_generation: 9,
1137            wal_format_version: 1,
1138            catalog_version: 2,
1139            segment_format_version: 1,
1140        };
1141        let bytes = pull.encode();
1142        assert_eq!(bytes[0], MSG_SYNC_PULL);
1143        match Message::decode(&bytes).unwrap() {
1144            Message::SyncPull {
1145                replica_id,
1146                since_lsn,
1147                max_units,
1148                max_bytes,
1149                database_id: decoded_database_id,
1150                primary_generation,
1151                wal_format_version,
1152                catalog_version,
1153                segment_format_version,
1154            } => {
1155                assert_eq!(replica_id, "replica-a");
1156                assert_eq!(since_lsn, 7);
1157                assert_eq!(max_units, 128);
1158                assert_eq!(max_bytes, 4096);
1159                assert_eq!(decoded_database_id, database_id);
1160                assert_eq!(primary_generation, 9);
1161                assert_eq!(wal_format_version, 1);
1162                assert_eq!(catalog_version, 2);
1163                assert_eq!(segment_format_version, 1);
1164            }
1165            other => panic!("expected SyncPull, got {other:?}"),
1166        }
1167
1168        let ack = Message::SyncAck {
1169            replica_id: "replica-a".into(),
1170            applied_lsn: 10,
1171            remote_lsn: 10,
1172        };
1173        match Message::decode(&ack.encode()).unwrap() {
1174            Message::SyncAck {
1175                replica_id,
1176                applied_lsn,
1177                remote_lsn,
1178            } => {
1179                assert_eq!(replica_id, "replica-a");
1180                assert_eq!(applied_lsn, 10);
1181                assert_eq!(remote_lsn, 10);
1182            }
1183            other => panic!("expected SyncAck, got {other:?}"),
1184        }
1185    }
1186
1187    #[test]
1188    fn test_encode_decode_sync_results() {
1189        let status = sample_sync_status();
1190        match Message::decode(
1191            &Message::SyncStatusResult {
1192                status: status.clone(),
1193            }
1194            .encode(),
1195        )
1196        .unwrap()
1197        {
1198            Message::SyncStatusResult { status: decoded } => assert_eq!(decoded, status),
1199            other => panic!("expected SyncStatusResult, got {other:?}"),
1200        }
1201
1202        let await_archive_status = WireSyncStatus {
1203            servable_lsn: Some(7),
1204            unarchived_lsn: Some(3),
1205            repair_action: WireSyncRepairAction::AwaitArchive,
1206            last_sync_error: Some("primary WAL is not yet archived".into()),
1207            ..status.clone()
1208        };
1209        match Message::decode(
1210            &Message::SyncStatusResult {
1211                status: await_archive_status.clone(),
1212            }
1213            .encode(),
1214        )
1215        .unwrap()
1216        {
1217            Message::SyncStatusResult { status: decoded } => {
1218                assert_eq!(decoded, await_archive_status)
1219            }
1220            other => panic!("expected AwaitArchive SyncStatusResult, got {other:?}"),
1221        }
1222
1223        let units = vec![
1224            WireRetainedUnit {
1225                tx_id: 1,
1226                record_type: 4,
1227                lsn: 8,
1228                data: vec![1, 2, 3],
1229            },
1230            WireRetainedUnit {
1231                tx_id: 1,
1232                record_type: 4,
1233                lsn: 9,
1234                data: vec![4, 5],
1235            },
1236        ];
1237        let empty_pull_len = Message::SyncPullResult {
1238            status: status.clone(),
1239            units: Vec::new(),
1240            has_more: true,
1241        }
1242        .encode()
1243        .len();
1244        let populated_pull_len = Message::SyncPullResult {
1245            status: status.clone(),
1246            units: units.clone(),
1247            has_more: true,
1248        }
1249        .encode()
1250        .len();
1251        let expected_unit_len: u64 = units
1252            .iter()
1253            .map(WireRetainedUnit::encoded_len)
1254            .collect::<Result<Vec<_>, _>>()
1255            .unwrap()
1256            .into_iter()
1257            .sum();
1258        assert_eq!(
1259            u64::try_from(populated_pull_len - empty_pull_len).unwrap(),
1260            expected_unit_len,
1261            "retained-unit encoded length must track SyncPullResult wire shape"
1262        );
1263
1264        match Message::decode(
1265            &Message::SyncPullResult {
1266                status: status.clone(),
1267                units: units.clone(),
1268                has_more: true,
1269            }
1270            .encode(),
1271        )
1272        .unwrap()
1273        {
1274            Message::SyncPullResult {
1275                status: decoded_status,
1276                units: decoded_units,
1277                has_more,
1278            } => {
1279                assert_eq!(decoded_status, status);
1280                assert_eq!(decoded_units, units);
1281                assert!(has_more);
1282            }
1283            other => panic!("expected SyncPullResult, got {other:?}"),
1284        }
1285
1286        match Message::decode(
1287            &Message::SyncAckResult {
1288                previous_applied_lsn: 7,
1289                applied_lsn: 10,
1290                remote_lsn: 10,
1291                advanced: true,
1292                status: WireSyncStatus {
1293                    stale: false,
1294                    repair_action: WireSyncRepairAction::None,
1295                    lag_lsn: Some(0),
1296                    lag_bytes: Some(0),
1297                    lag_ms: Some(0),
1298                    ..status
1299                },
1300            }
1301            .encode(),
1302        )
1303        .unwrap()
1304        {
1305            Message::SyncAckResult {
1306                previous_applied_lsn,
1307                applied_lsn,
1308                remote_lsn,
1309                advanced,
1310                status,
1311            } => {
1312                assert_eq!(previous_applied_lsn, 7);
1313                assert_eq!(applied_lsn, 10);
1314                assert_eq!(remote_lsn, 10);
1315                assert!(advanced);
1316                assert!(!status.stale);
1317            }
1318            other => panic!("expected SyncAckResult, got {other:?}"),
1319        }
1320    }
1321
1322    #[test]
1323    fn test_decode_garbage_never_panics() {
1324        // Feed a wide range of malformed/truncated byte sequences to the
1325        // wire-protocol decode path. Every one must return Err, never panic:
1326        // a malformed client message must not crash the server.
1327        let cases: Vec<Vec<u8>> = vec![
1328            vec![],                                   // empty
1329            vec![0x03],                               // 1 byte, shorter than header
1330            vec![0x03, 0x00, 0x00, 0x00, 0x00],       // 5 bytes, header truncated by one
1331            vec![0xFF, 0x00, 0x00, 0x00, 0x00, 0x00], // unknown message type
1332            // QUERY with payload_len far exceeding the frame.
1333            vec![0x03, 0x00, 0xFF, 0xFF, 0xFF, 0xFF],
1334            // SYNC_PULL with only a replica id and no fixed fields.
1335            {
1336                let mut payload = encode_string("replica-a");
1337                let mut frame = vec![MSG_SYNC_PULL, 0];
1338                frame.extend_from_slice(&(payload.len() as u32).to_le_bytes());
1339                frame.append(&mut payload);
1340                frame
1341            },
1342            // SYNC_PULL_RESULT with an amplified retained-unit count.
1343            {
1344                let mut payload = encode_sync_status(&sample_sync_status());
1345                payload.extend_from_slice(&((MAX_SYNC_UNITS as u32) + 1).to_le_bytes());
1346                let mut frame = vec![MSG_SYNC_PULL_RESULT, 0];
1347                frame.extend_from_slice(&(payload.len() as u32).to_le_bytes());
1348                frame.extend_from_slice(&payload);
1349                frame
1350            },
1351            // CONNECT claiming a string len of 0xFFFFFFFF but no data.
1352            vec![0x01, 0x00, 0x04, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF],
1353            // RESULT_ROWS claiming a huge column count with no data.
1354            vec![0x07, 0x00, 0x02, 0x00, 0x00, 0x00, 0xFF, 0xFF],
1355            // RESULT_OK with a truncated 8-byte affected field.
1356            vec![0x09, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03],
1357            // QUERY_PARAMS (0x04) claiming a query string len with no data.
1358            vec![0x04, 0x00, 0x04, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF],
1359            // QUERY_PARAMS: empty query string, claims 1 param, no param bytes.
1360            vec![
1361                0x04, 0x00, 0x06, 0x00, 0x00, 0x00, // header, payload_len=6
1362                0x00, 0x00, 0x00, 0x00, // query string len = 0
1363                0x01, 0x00, // param count = 1, then nothing
1364            ],
1365            // QUERY_PARAMS: 1 int param with a truncated i64 body.
1366            vec![
1367                0x04, 0x00, 0x0B, 0x00, 0x00, 0x00, // header, payload_len=11
1368                0x00, 0x00, 0x00, 0x00, // query len = 0
1369                0x01, 0x00, // param count = 1
1370                0x01, // tag = int, then only 3 of 8 bytes
1371                0x01, 0x02, 0x03,
1372            ],
1373            // QUERY_PARAMS: 1 str param with a truncated string body.
1374            vec![
1375                0x04, 0x00, 0x0F, 0x00, 0x00, 0x00, // header, payload_len=15
1376                0x00, 0x00, 0x00, 0x00, // query len = 0
1377                0x01, 0x00, // param count = 1
1378                0x04, // tag = str
1379                0xFF, 0xFF, 0xFF, 0xFF, // str len huge, no data
1380            ],
1381            // QUERY_PARAMS: unknown param tag byte.
1382            vec![
1383                0x04, 0x00, 0x0B, 0x00, 0x00, 0x00, // header, payload_len=11
1384                0x00, 0x00, 0x00, 0x00, // query len = 0
1385                0x01, 0x00, // param count = 1
1386                0x63, // bogus tag
1387            ],
1388        ];
1389        for bytes in cases {
1390            let result = Message::decode(&bytes);
1391            assert!(
1392                result.is_err(),
1393                "expected Err for malformed input {bytes:?}, got {result:?}"
1394            );
1395        }
1396    }
1397
1398    #[test]
1399    fn test_decode_arbitrary_prefixes_never_panic() {
1400        // Exhaustively walk every truncation of a well-formed frame plus a
1401        // few byte mutations. None may panic.
1402        let valid = Message::ResultRows {
1403            columns: vec!["a".into(), "b".into()],
1404            rows: vec![vec!["1".into(), "2".into()]],
1405        }
1406        .encode();
1407        for end in 0..valid.len() {
1408            // Every prefix that is not the full frame must be rejected, not panic.
1409            let _ = Message::decode(&valid[..end]);
1410        }
1411        // Mutate each byte to a high value and confirm no panic.
1412        for i in 0..valid.len() {
1413            let mut m = valid.clone();
1414            m[i] = 0xFF;
1415            let _ = Message::decode(&m);
1416        }
1417    }
1418
1419    #[test]
1420    fn test_decode_result_rows_rejects_amplified_row_count() {
1421        // A tiny frame that declares col_count=0 and row_count=10_000_000.
1422        // With zero columns each row consumes no bytes, so the old decoder
1423        // would allocate/iterate 10M empty rows from ~12 bytes (reachable
1424        // pre-auth). The amplification guard must reject it.
1425        let mut payload = Vec::new();
1426        payload.extend_from_slice(&0u16.to_le_bytes()); // col_count = 0
1427        payload.extend_from_slice(&10_000_000u32.to_le_bytes()); // row_count = 10M
1428        let mut frame = vec![MSG_RESULT_ROWS, 0];
1429        frame.extend_from_slice(&(payload.len() as u32).to_le_bytes());
1430        frame.extend_from_slice(&payload);
1431        assert!(
1432            Message::decode(&frame).is_err(),
1433            "amplified row count must be rejected"
1434        );
1435
1436        // A normal small ResultRows must still round-trip unchanged.
1437        let msg = Message::ResultRows {
1438            columns: vec!["a".into()],
1439            rows: vec![vec!["x".into()]],
1440        };
1441        match Message::decode(&msg.encode()).unwrap() {
1442            Message::ResultRows { columns, rows } => {
1443                assert_eq!(columns, vec!["a"]);
1444                assert_eq!(rows, vec![vec!["x".to_string()]]);
1445            }
1446            other => panic!("expected ResultRows, got {other:?}"),
1447        }
1448    }
1449
1450    #[test]
1451    fn test_frame_length() {
1452        let msg = Message::Query {
1453            query: "User".into(),
1454        };
1455        let bytes = msg.encode();
1456        assert!(bytes.len() >= 6);
1457        let payload_len = u32::from_le_bytes(bytes[2..6].try_into().unwrap()) as usize;
1458        assert_eq!(bytes.len(), 6 + payload_len);
1459    }
1460}