beck_rt/
pgwire.rs

1//! The PostgreSQL wire protocol, served over a program's read models.
2//!
3//! [`docs/05-tier-lowering.md`](../../../../../docs/05-tier-lowering.md) §5.3 asks for "pgwire access
4//! for the outside world: `psql`, BI tools, DBeaver see materialized views as ordinary tables — the
5//! single cheapest trust-builder for adopting teams", and
6//! [`07`](../../../../../docs/07-dependencies.md) §7.2 files it under external-tool compatibility
7//! with no alternative listed. [`beck_core::read`] is what the tables are; this is the socket.
8//!
9//! # Why it is written here rather than taken
10//!
11//! There are crates that implement this protocol server-side. What they carry is the rest of a
12//! database — a type registry, an extended-protocol state machine over prepared statements with
13//! parameters, portals that suspend — and a read model has none of those to expose. What is
14//! actually needed is the startup exchange, the simple query, the extended query with no
15//! parameters, and four type OIDs a driver already knows. That is this file, and it adds no
16//! dependency to a workspace whose §7.9 pins everything
17//! ([`adr/0020`](../../../../../docs/adr/0020-the-read-model-speaks-pgwire-by-hand.md)).
18//!
19//! # What it deliberately does not do
20//!
21//! * **No authentication.** It answers `AuthenticationOk` to everyone, which is why [`serve`]
22//!   refuses to bind anywhere but the loopback interface. An unauthenticated read of an
23//!   application's whole state must not be reachable from another host, and a flag that turns that
24//!   off is a decision with an ADR rather than a convenience.
25//! * **No TLS.** The same reason and the same bound.
26//! * **No writes.** The log is the only way state changes; a read model that accepted an `insert`
27//!   would be a second way, which is the property [`01`](../../../../../docs/01-vision-and-premise.md)
28//!   §1.1 is about.
29//!
30//! # `pg_catalog`, and why it is not here either
31//!
32//! `psql`'s `\d` sends a join against four catalogue relations, and those relations are read
33//! models: [`beck_core::pg`] derives `pg_class`, `pg_namespace`, `pg_attribute` and the rest from
34//! the same [`Schema`] this file serves, and they are scanned, filtered and joined by the
35//! operators every other query goes through. **Nothing in this file knows what a backslash command
36//! is.** A catalogue answered by matching the query text would be a second query path — one that
37//! could drift from the schema, and one no `select` could reach — where this one cannot disagree
38//! with the schema because it *is* the schema under another set of column names.
39//!
40//! `\d`, `\d <table>`, `\dt`, `\dn` and `\l` are what that answers. Anything else asks for an
41//! object a read model does not have — a function, a role, an index — and is refused by the name
42//! of the relation it asked for ([`beck_core::pg::Rel::missing`]) rather than by an empty answer.
43//! [`beck_core::read::Schema::CATALOGUE`] is still the table that says what a read model is
44//! derived *from*, which is the question `pg_catalog` has no column for.
45
46use std::net::SocketAddr;
47use std::sync::Arc;
48
49use anyhow::{bail, Result};
50use beck_core::read::{self, Answer, Cardinality, Column, Datum, Schema, SqlError, Table};
51use beck_core::Value;
52use tokio::io::{AsyncReadExt, AsyncWriteExt};
53use tokio::net::{TcpListener, TcpStream};
54
55use crate::app::App;
56
57/// The protocol version this speaks: 3.0, which every client since PostgreSQL 7.4 speaks.
58const PROTOCOL_3: i32 = 196_608;
59const SSL_REQUEST: i32 = 80_877_103;
60const GSSENC_REQUEST: i32 = 80_877_104;
61const CANCEL_REQUEST: i32 = 80_877_102;
62
63/// The largest message this will read, before it has any reason to trust the sender.
64///
65/// A startup packet says how long it is and then sends that many bytes; believing an unbounded
66/// length is how a listener becomes a memory allocator for whoever connects
67/// ([`docs/82`](../../../../../docs/82-the-edge-report.md) made the same argument about the
68/// websocket edge). A query long enough to reach this is a query this SQL cannot parse anyway.
69const MAX_MESSAGE: usize = 1 << 20;
70
71/// Serve a program's read models on the PostgreSQL wire protocol.
72pub async fn serve(app: Arc<App>, addr: SocketAddr) -> Result<()> {
73    serve_on(bind(addr).await?, app).await
74}
75
76/// Bind the read-model port, refusing anything but loopback.
77///
78/// There is no authentication here — see the module docs — and this bound is what stands in for
79/// one: a port that answers every question about an application's state belongs on the same host as
80/// the process, reached by whatever forwards it there.
81///
82/// Separate from [`serve`] so a caller can fail the *command* rather than a background task: an
83/// address this process will not serve on should be an error the person who typed it sees.
84pub async fn bind(addr: SocketAddr) -> Result<TcpListener> {
85    if !addr.ip().is_loopback() {
86        bail!(
87            "{addr} is not a loopback address. The read-model port has no authentication and no \
88             transport security, so it is bound to localhost only; forward it (kubectl \
89             port-forward, an SSH tunnel, a sidecar) rather than exposing it — \
90             docs/adr/0020 is the record that would have to change first"
91        );
92    }
93    Ok(TcpListener::bind(addr).await?)
94}
95
96/// Serve on an already-bound listener.
97pub async fn serve_on(listener: TcpListener, app: Arc<App>) -> Result<()> {
98    let schema = Arc::new(Schema::of(app.runtime().placed(), app.runtime().plan()));
99    tracing::info!(
100        addr = %listener.local_addr()?,
101        tables = schema.tables.len(),
102        "read models on pgwire (no authentication; loopback only)"
103    );
104    loop {
105        let (socket, peer) = listener.accept().await?;
106        let app = app.clone();
107        let schema = schema.clone();
108        tokio::spawn(async move {
109            if let Err(e) = session(socket, app, schema).await {
110                tracing::debug!(peer = %peer, error = %e, "pgwire session ended");
111            }
112        });
113    }
114}
115
116// -------------------------------------------------------------------------------------------
117// One connection
118// -------------------------------------------------------------------------------------------
119
120/// A parsed statement, held between `Parse` and `Execute`.
121#[derive(Clone, Default)]
122struct Statement {
123    sql: String,
124    columns: Vec<Column>,
125}
126
127/// A bound statement, and the format its caller wants each column in.
128#[derive(Clone, Default)]
129struct Portal {
130    statement: Statement,
131    /// Empty for all-text, one entry for all-of-that, or one per column.
132    formats: Vec<i16>,
133}
134
135async fn session(mut socket: TcpStream, app: Arc<App>, schema: Arc<Schema>) -> Result<()> {
136    socket.set_nodelay(true)?;
137    if !startup(&mut socket).await? {
138        return Ok(());
139    }
140
141    // The connection is a reader of the shared dataflow for as long as it is open: the same reader
142    // set a subscription joins, for the same reason (`beck_core::engine::Reader`).
143    let reader = app.shared_dataflow().reader();
144
145    let mut out = Vec::new();
146    authentication_ok(&mut out);
147    parameter_status(&mut out, "server_version", "15.0 (beck)");
148    parameter_status(&mut out, "server_encoding", "UTF8");
149    parameter_status(&mut out, "client_encoding", "UTF8");
150    parameter_status(&mut out, "DateStyle", "ISO, MDY");
151    parameter_status(&mut out, "TimeZone", "UTC");
152    parameter_status(&mut out, "integer_datetimes", "on");
153    parameter_status(&mut out, "standard_conforming_strings", "on");
154    // A cancel request is refused rather than honoured, so the key is a constant rather than a
155    // secret: there is nothing to cancel that is not already `O(rows)`.
156    message(&mut out, b'K', |b| {
157        b.extend_from_slice(&0i32.to_be_bytes());
158        b.extend_from_slice(&0i32.to_be_bytes());
159    });
160    ready(&mut out);
161    socket.write_all(&out).await?;
162
163    let mut statements: std::collections::HashMap<String, Statement> = Default::default();
164    let mut portals: std::collections::HashMap<String, Portal> = Default::default();
165    // Between an error and the next `Sync`, the extended protocol says every message is skipped.
166    let mut failed = false;
167
168    loop {
169        let mut tag = [0u8; 1];
170        if socket.read_exact(&mut tag).await.is_err() {
171            return Ok(());
172        }
173        let body = read_body(&mut socket).await?;
174        let mut out = Vec::new();
175        match tag[0] {
176            b'X' => return Ok(()),
177            b'S' => {
178                failed = false;
179                ready(&mut out);
180            }
181            _ if failed => {}
182            b'Q' => {
183                let sql = cstr(&body, &mut 0)?;
184                simple_query(&app, &schema, &reader, &sql, &mut out).await;
185                ready(&mut out);
186            }
187            b'P' => {
188                let mut i = 0;
189                let name = cstr(&body, &mut i)?;
190                let sql = cstr(&body, &mut i)?;
191                match schema.describe(&sql) {
192                    Ok(columns) => {
193                        statements.insert(name, Statement { sql, columns });
194                        message(&mut out, b'1', |_| {});
195                    }
196                    Err(e) => {
197                        error_response(&mut out, &e);
198                        failed = true;
199                    }
200                }
201            }
202            b'B' => {
203                let mut i = 0;
204                let portal = cstr(&body, &mut i)?;
205                let statement = cstr(&body, &mut i)?;
206                // Parameter formats and parameters: this SQL has no placeholders, so they are read
207                // to advance past them rather than used.
208                let formats = i16s(&body, &mut i)?;
209                let _ = formats;
210                let params = i16_count(&body, &mut i)?;
211                for _ in 0..params {
212                    let len = i32_at(&body, &mut i)?;
213                    if len > 0 {
214                        i += len as usize;
215                    }
216                }
217                let results = i16s(&body, &mut i)?;
218                match statements.get(&statement) {
219                    Some(s) => {
220                        portals.insert(
221                            portal,
222                            Portal {
223                                statement: s.clone(),
224                                formats: results,
225                            },
226                        );
227                        message(&mut out, b'2', |_| {});
228                    }
229                    None => {
230                        error_response(
231                            &mut out,
232                            &SqlError {
233                                message: format!("there is no prepared statement \"{statement}\""),
234                                code: "26000",
235                            },
236                        );
237                        failed = true;
238                    }
239                }
240            }
241            b'D' => {
242                let mut i = 0;
243                let what = *body.first().unwrap_or(&b'S');
244                i += 1;
245                let name = cstr(&body, &mut i)?;
246                let (columns, formats) = match what {
247                    b'P' => match portals.get(&name) {
248                        Some(p) => (p.statement.columns.clone(), p.formats.clone()),
249                        None => (Vec::new(), Vec::new()),
250                    },
251                    _ => {
252                        // A statement description says what the *statement* takes and returns; the
253                        // formats are a property of a portal, so this is always text here, exactly
254                        // as a real server answers it.
255                        message(&mut out, b't', |b| {
256                            b.extend_from_slice(&0i16.to_be_bytes());
257                        });
258                        match statements.get(&name) {
259                            Some(s) => (s.columns.clone(), Vec::new()),
260                            None => (Vec::new(), Vec::new()),
261                        }
262                    }
263                };
264                if columns.is_empty() {
265                    message(&mut out, b'n', |_| {});
266                } else {
267                    row_description(&mut out, &columns, &formats);
268                }
269            }
270            b'E' => {
271                let mut i = 0;
272                let name = cstr(&body, &mut i)?;
273                let max = i32_at(&body, &mut i)?.max(0) as usize;
274                match portals.get(&name).cloned() {
275                    Some(p) => match run(&app, &schema, &reader, &p.statement.sql).await {
276                        Ok(answer) => {
277                            let total = answer.rows.len();
278                            let take = if max == 0 { total } else { max.min(total) };
279                            for row in answer.rows.iter().take(take) {
280                                data_row(&mut out, row, &p.formats);
281                            }
282                            if take < total {
283                                message(&mut out, b's', |_| {});
284                            } else {
285                                command_complete(&mut out, &answer.tag);
286                            }
287                        }
288                        Err(e) => {
289                            error_response(&mut out, &e);
290                            failed = true;
291                        }
292                    },
293                    None => {
294                        error_response(
295                            &mut out,
296                            &SqlError {
297                                message: format!("there is no portal \"{name}\""),
298                                code: "34000",
299                            },
300                        );
301                        failed = true;
302                    }
303                }
304            }
305            b'C' => {
306                let mut i = 0;
307                let what = *body.first().unwrap_or(&b'S');
308                i += 1;
309                let name = cstr(&body, &mut i)?;
310                if what == b'P' {
311                    portals.remove(&name);
312                } else {
313                    statements.remove(&name);
314                }
315                message(&mut out, b'3', |_| {});
316            }
317            b'H' => {}
318            other => {
319                error_response(
320                    &mut out,
321                    &SqlError {
322                        message: format!("message type '{}' is not supported", other as char),
323                        code: "08P01",
324                    },
325                );
326                failed = true;
327            }
328        }
329        if !out.is_empty() {
330            socket.write_all(&out).await?;
331        }
332    }
333}
334
335/// The startup exchange. Answers whether the connection continues.
336async fn startup(socket: &mut TcpStream) -> Result<bool> {
337    loop {
338        let mut len = [0u8; 4];
339        if socket.read_exact(&mut len).await.is_err() {
340            return Ok(false);
341        }
342        let len = i32::from_be_bytes(len);
343        if !(8..=MAX_MESSAGE as i32).contains(&len) {
344            bail!("a startup packet of {len} bytes");
345        }
346        let mut body = vec![0u8; len as usize - 4];
347        socket.read_exact(&mut body).await?;
348        let version = i32::from_be_bytes([body[0], body[1], body[2], body[3]]);
349        match version {
350            // "No" to both, in the one-byte form the protocol reserves for the answer, and the
351            // client retries in plaintext. A refusal is not a failure here — `sslmode=prefer`, the
352            // default in most drivers, is exactly this exchange.
353            SSL_REQUEST | GSSENC_REQUEST => {
354                socket.write_all(b"N").await?;
355            }
356            CANCEL_REQUEST => return Ok(false),
357            PROTOCOL_3 => return Ok(true),
358            other => {
359                let mut out = Vec::new();
360                error_response(
361                    &mut out,
362                    &SqlError {
363                        message: format!(
364                            "this read model speaks protocol 3.0; the client asked for {}.{}",
365                            other >> 16,
366                            other & 0xffff
367                        ),
368                        code: "0A000",
369                    },
370                );
371                socket.write_all(&out).await?;
372                return Ok(false);
373            }
374        }
375    }
376}
377
378async fn simple_query(
379    app: &Arc<App>,
380    schema: &Schema,
381    reader: &beck_core::engine::Reader,
382    sql: &str,
383    out: &mut Vec<u8>,
384) {
385    if sql.trim().is_empty() {
386        message(out, b'I', |_| {});
387        return;
388    }
389    match run(app, schema, reader, sql).await {
390        Ok(answer) => {
391            if !answer.columns.is_empty() {
392                row_description(out, &answer.columns, &[]);
393                for row in &answer.rows {
394                    data_row(out, row, &[]);
395                }
396            }
397            command_complete(out, &answer.tag);
398        }
399        Err(e) => error_response(out, &e),
400    }
401}
402
403/// Answer one query against a consistent snapshot of the program's state.
404///
405/// The whole query runs under the accumulator's read lock. That is what makes it a *snapshot*: the
406/// sequencer takes the write lock to commit, so while this runs nothing can move the state, and
407/// therefore nothing can advance the shared dataflow past the version the base tables were read at.
408/// Two tables in one query cannot disagree about which events have happened.
409///
410/// The cost is stated rather than hidden: a scan of a large table delays the next commit by the
411/// length of the scan. The alternative — copy the accumulator and let the arrangements move — is
412/// cheaper for the writer and gives a query that sees two versions at once, which is the wrong
413/// trade for a read model whose selling point is that it cannot disagree with the page.
414async fn run(
415    app: &Arc<App>,
416    schema: &Schema,
417    reader: &beck_core::engine::Reader,
418    sql: &str,
419) -> Result<Answer, SqlError> {
420    app.read_snapshot(|state, version| {
421        let rows = Snapshot {
422            app,
423            reader,
424            state,
425            version,
426        };
427        schema.run(sql, &rows)
428    })
429    .await
430}
431
432/// Where a table's rows come from at one version.
433struct Snapshot<'a> {
434    app: &'a Arc<App>,
435    reader: &'a beck_core::engine::Reader,
436    state: &'a Value,
437    version: u64,
438}
439
440impl read::Rows for Snapshot<'_> {
441    fn scan(&self, table: &Table) -> Result<Vec<Value>, SqlError> {
442        let values = match &table.source {
443            // Built here rather than read from the program, and `Schema::builtin_rows` answers
444            // them before a scan is asked for — so this arm is what a reader that went looking
445            // anyway is told, rather than a second way to build them.
446            read::Source::Catalogue | read::Source::Pg(_) => return Ok(Vec::new()),
447            read::Source::State(path) => {
448                let at = read::at_path(self.state, path).ok_or_else(|| SqlError {
449                    message: format!(
450                        "\"{}\" is not in this state — the accumulator has no such field",
451                        table.name
452                    ),
453                    code: "42P01",
454                })?;
455                match table.cardinality {
456                    Cardinality::Many => read::elements(&at),
457                    Cardinality::One => vec![at],
458                }
459            }
460            read::Source::View(op) => {
461                let vals = self
462                    .reader
463                    .read(self.state, self.version, *op)
464                    .map_err(|e| SqlError {
465                        message: format!("the view this table reads could not be maintained: {e}"),
466                        code: "58000",
467                    })?;
468                match table.cardinality {
469                    Cardinality::One => vals.into_iter().take(1).collect(),
470                    // A maintained arrangement answers its entries; a pointwise operator answers
471                    // one value, which for a collection-shaped table is the collection.
472                    Cardinality::Many => match vals.as_slice() {
473                        [one @ (Value::List(_) | Value::Map(_))] => read::elements(one),
474                        _ => vals,
475                    },
476                }
477            }
478        };
479        Ok(values)
480    }
481
482    /// The size of a table without building a row of it — [`read::Rows::count`]'s half of
483    /// [`docs/23`](../../../../../docs/23-incremental-views-report.md) §23.19.
484    ///
485    /// Both sources know their own size and neither was being asked. A maintained arrangement is a
486    /// `BTreeMap`, so `count(*)` over a derived collection is §3.8's "never a recount" reaching
487    /// `psql`; a `Map` or a `list` in the accumulator knows its length too, and the scan path was
488    /// cloning every value out of it before counting them.
489    ///
490    /// The catalogue and `pg_catalog` answer `None` and are scanned: each is a handful of rows
491    /// built on demand, and a second way to count them would be a second thing to keep true.
492    fn count(&self, table: &Table) -> Result<Option<u64>, SqlError> {
493        Ok(match &table.source {
494            read::Source::Catalogue | read::Source::Pg(_) => None,
495            read::Source::State(path) => match read::at_path(self.state, path) {
496                Some(Value::Map(m)) => Some(m.len() as u64),
497                Some(Value::List(xs)) => Some(xs.len() as u64),
498                // A record is one row, and anything else is a shape the scan path decides about.
499                _ => None,
500            },
501            read::Source::View(op) => {
502                self.reader
503                    .len(self.state, self.version, *op)
504                    .map_err(|e| SqlError {
505                        message: format!("the view this table reads could not be maintained: {e}"),
506                        code: "58000",
507                    })?
508            }
509        })
510    }
511
512    /// The backend a `join`, a `group by` and a `distinct` are prepared against — the same one the
513    /// program itself runs on, so a query and the page it is a view of are executed by one
514    /// implementation ([`beck_core::query`]).
515    fn backend(&self) -> Option<&dyn beck_core::backend::Backend> {
516        Some(self.app.runtime().executor())
517    }
518}
519
520// -------------------------------------------------------------------------------------------
521// Messages
522// -------------------------------------------------------------------------------------------
523
524/// Frame a message: a tag, a length that counts itself, and a body.
525fn message(out: &mut Vec<u8>, tag: u8, body: impl FnOnce(&mut Vec<u8>)) {
526    out.push(tag);
527    let at = out.len();
528    out.extend_from_slice(&0i32.to_be_bytes());
529    body(out);
530    let len = (out.len() - at) as i32;
531    out[at..at + 4].copy_from_slice(&len.to_be_bytes());
532}
533
534fn authentication_ok(out: &mut Vec<u8>) {
535    message(out, b'R', |b| {
536        b.extend_from_slice(&0i32.to_be_bytes());
537    });
538}
539
540fn parameter_status(out: &mut Vec<u8>, key: &str, value: &str) {
541    message(out, b'S', |b| {
542        put_cstr(b, key);
543        put_cstr(b, value);
544    });
545}
546
547fn ready(out: &mut Vec<u8>) {
548    // Always 'I': idle, never in a transaction. A read model has nothing to be in one for, and a
549    // driver that opened one was answered `BEGIN` and told nothing changed.
550    message(out, b'Z', |b| b.push(b'I'));
551}
552
553fn command_complete(out: &mut Vec<u8>, tag: &str) {
554    message(out, b'C', |b| put_cstr(b, tag));
555}
556
557fn row_description(out: &mut Vec<u8>, columns: &[Column], formats: &[i16]) {
558    message(out, b'T', |b| {
559        b.extend_from_slice(&(columns.len() as i16).to_be_bytes());
560        for (i, c) in columns.iter().enumerate() {
561            put_cstr(b, &c.name);
562            // No table and no attribute number: these columns are not from a table a catalogue
563            // knows about, and zero is what the protocol reserves for saying so.
564            b.extend_from_slice(&0i32.to_be_bytes());
565            b.extend_from_slice(&0i16.to_be_bytes());
566            b.extend_from_slice(&c.ty.oid().to_be_bytes());
567            b.extend_from_slice(&c.ty.width().to_be_bytes());
568            b.extend_from_slice(&(-1i32).to_be_bytes());
569            b.extend_from_slice(&format_of(formats, i).to_be_bytes());
570        }
571    });
572}
573
574fn format_of(formats: &[i16], i: usize) -> i16 {
575    match formats.len() {
576        0 => 0,
577        1 => formats[0],
578        _ => formats.get(i).copied().unwrap_or(0),
579    }
580}
581
582fn data_row(out: &mut Vec<u8>, row: &[Option<Datum>], formats: &[i16]) {
583    message(out, b'D', |b| {
584        b.extend_from_slice(&(row.len() as i16).to_be_bytes());
585        for (i, cell) in row.iter().enumerate() {
586            match cell {
587                None => b.extend_from_slice(&(-1i32).to_be_bytes()),
588                Some(d) => {
589                    let bytes = if format_of(formats, i) == 1 {
590                        binary(d)
591                    } else {
592                        d.text().into_bytes()
593                    };
594                    b.extend_from_slice(&(bytes.len() as i32).to_be_bytes());
595                    b.extend_from_slice(&bytes);
596                }
597            }
598        }
599    });
600}
601
602/// The binary form of a datum, which is what every Rust and Java driver asks for.
603fn binary(d: &Datum) -> Vec<u8> {
604    match d {
605        Datum::Boolean(v) => vec![u8::from(*v)],
606        Datum::Bigint(v) => v.to_be_bytes().to_vec(),
607        Datum::Double(v) => v.to_bits().to_be_bytes().to_vec(),
608        Datum::Text(s) => s.as_bytes().to_vec(),
609    }
610}
611
612fn error_response(out: &mut Vec<u8>, e: &SqlError) {
613    message(out, b'E', |b| {
614        b.push(b'S');
615        put_cstr(b, "ERROR");
616        b.push(b'V');
617        put_cstr(b, "ERROR");
618        b.push(b'C');
619        put_cstr(b, e.code);
620        b.push(b'M');
621        put_cstr(b, &e.message);
622        b.push(0);
623    });
624}
625
626fn put_cstr(out: &mut Vec<u8>, s: &str) {
627    // A NUL inside a string would end it early, which is a way to smuggle a second field into a
628    // message. Beck strings are UTF-8 and may contain one, so it is replaced rather than trusted.
629    for byte in s.bytes() {
630        out.push(if byte == 0 { b' ' } else { byte });
631    }
632    out.push(0);
633}
634
635// -------------------------------------------------------------------------------------------
636// Reading
637// -------------------------------------------------------------------------------------------
638
639async fn read_body(socket: &mut TcpStream) -> Result<Vec<u8>> {
640    let mut len = [0u8; 4];
641    socket.read_exact(&mut len).await?;
642    let len = i32::from_be_bytes(len);
643    if !(4..=MAX_MESSAGE as i32).contains(&len) {
644        bail!("a message of {len} bytes");
645    }
646    let mut body = vec![0u8; len as usize - 4];
647    socket.read_exact(&mut body).await?;
648    Ok(body)
649}
650
651fn cstr(body: &[u8], i: &mut usize) -> Result<String> {
652    let start = *i;
653    while *i < body.len() && body[*i] != 0 {
654        *i += 1;
655    }
656    if *i >= body.len() {
657        bail!("a string in a message is not terminated");
658    }
659    let s = String::from_utf8_lossy(&body[start..*i]).into_owned();
660    *i += 1;
661    Ok(s)
662}
663
664fn i32_at(body: &[u8], i: &mut usize) -> Result<i32> {
665    if *i + 4 > body.len() {
666        bail!("a message ends inside a number");
667    }
668    let v = i32::from_be_bytes([body[*i], body[*i + 1], body[*i + 2], body[*i + 3]]);
669    *i += 4;
670    Ok(v)
671}
672
673fn i16_count(body: &[u8], i: &mut usize) -> Result<usize> {
674    if *i + 2 > body.len() {
675        bail!("a message ends inside a count");
676    }
677    let v = i16::from_be_bytes([body[*i], body[*i + 1]]);
678    *i += 2;
679    Ok(v.max(0) as usize)
680}
681
682fn i16s(body: &[u8], i: &mut usize) -> Result<Vec<i16>> {
683    let n = i16_count(body, i)?;
684    let mut out = Vec::with_capacity(n);
685    for _ in 0..n {
686        if *i + 2 > body.len() {
687            bail!("a message ends inside a format code");
688        }
689        out.push(i16::from_be_bytes([body[*i], body[*i + 1]]));
690        *i += 2;
691    }
692    Ok(out)
693}
694
695#[cfg(test)]
696mod tests {
697    use super::*;
698
699    #[test]
700    fn a_message_carries_its_own_length() {
701        let mut out = Vec::new();
702        command_complete(&mut out, "SELECT 2");
703        assert_eq!(out[0], b'C');
704        let len = i32::from_be_bytes([out[1], out[2], out[3], out[4]]) as usize;
705        assert_eq!(len, out.len() - 1);
706        assert_eq!(&out[5..out.len() - 1], b"SELECT 2");
707        assert_eq!(out[out.len() - 1], 0);
708    }
709
710    #[test]
711    fn a_nul_inside_a_string_cannot_end_it() {
712        let mut out = Vec::new();
713        put_cstr(&mut out, "a\0b");
714        assert_eq!(out, b"a b\0");
715    }
716
717    #[test]
718    fn binary_is_big_endian_and_text_is_not() {
719        assert_eq!(binary(&Datum::Bigint(1)), vec![0, 0, 0, 0, 0, 0, 0, 1]);
720        assert_eq!(Datum::Bigint(1).text(), "1");
721        assert_eq!(binary(&Datum::Boolean(true)), vec![1]);
722        assert_eq!(Datum::Boolean(true).text(), "t");
723    }
724
725    #[test]
726    fn one_format_code_covers_every_column() {
727        assert_eq!(format_of(&[], 3), 0);
728        assert_eq!(format_of(&[1], 3), 1);
729        assert_eq!(format_of(&[0, 1], 1), 1);
730    }
731}