feat(sql): the database helper and the TOTP helper, as subcommands

`crafter-review sql` hands the SQL to the driver's broker named in
REVIEW_GATES_DB; the broker runs it as one transaction, rolled back, and
streams the lines back with the exit code last. The client speaks protocol
3.0 over Crafter.Network's TCP client: cleartext, MD5 and SCRAM-SHA-256,
one Parse per statement, the transaction status as the backstop. The
splitter, the refusals, the table layout and every message work on code
points; connect and socket errors read "[Errno 111] Connection refused"
or "timed out". `crafter-review totp` prints "123456 (valid 17s)".

No sleep after the last connect attempt; TOTP reads the clock once; a
broker token that is no string gets the NO DATABASE answer and the exit
mark.

The goldens add the splitter, the comment stripping, the fingerprint, the
refusals, the first words, the headline, the cell, table, error and notice
layouts, the run's refusals and limits and 40 TOTP codes (2774 checks
now). tests/PgWire runs the helper over a Postgres stand-in and compares
its output with the goldens, line for line, plus the broker's protocol.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jorijn van der Graaf 2026-10-31 07:20:00 +01:00
commit e8a58f893f
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
10 changed files with 3666 additions and 3 deletions

View file

@ -10,6 +10,8 @@ import :Process;
import :Inventory;
import :Config;
import :Freeze;
import :PgWire;
import :Totp;
import std;
namespace fs = std::filesystem;
@ -247,6 +249,10 @@ inventory unit, against a frozen commit range.)";
const std::string_view command = rest[at];
const std::span<const std::string_view> after = rest.subspan(at + 1);
try {
// The helpers a lens runs: they read neither the home nor the
// config, only their arguments, stdin and REVIEW_GATES_DB.
if (command == "sql") return PgWire::SqlMain(after);
if (command == "totp") return Totp::Main(after);
if (command == "inventory" && !configPath) return Inventory::Main(after);
if (command == "config") {
if (!after.empty()) return UsageError(Usage, "crafter-review", std::format("unrecognized arguments: {}", after.front()));

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,97 @@
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
module Crafter.Review:Totp_impl;
import :Totp;
import :Json;
import :Text;
import :Crypto;
import :Platform;
import std;
namespace Crafter::Review::Totp {
namespace {
constexpr std::string_view Usage = R"(A current TOTP code (RFC 6238, SHA-1, 30 s, 6 digits) for a login fixture - what the functional lens types into the
one-time-code field.
crafter-review totp <fixture.json> the fixture's `totpSecret` (base32), as the consumer's login seeder writes it
crafter-review totp --secret <base32>
Prints the code and the seconds left in its window; the lens regenerates when the window rolled over.)";
// Floor division and modulo of floats, for a positive step: fmod, then
// the quotient of what is left, floored.
double FloorDiv(double x, double y) {
const double mod = std::fmod(x, y);
double div = (x - mod) / y;
if (mod != 0.0 && ((y < 0) != (mod < 0))) div -= 1.0;
if (div == 0.0) return std::copysign(0.0, x / y);
double floored = std::floor(div);
if (div - floored > 0.5) floored += 1.0;
return floored;
}
double Mod(double x, double y) {
double mod = std::fmod(x, y);
if (mod != 0.0 && ((y < 0) != (mod < 0))) mod += y;
return mod;
}
}
Code At(std::string_view secret, double now, std::int32_t step, std::int32_t digits) {
// secret.strip().replace(' ', '').upper().rstrip('='), then padded.
const std::string s = Dyn::RStrip(Dyn::Upper(Dyn::Replace(Dyn::Strip(secret), " ", "")), "=");
const std::size_t length = Dyn::Length(s);
const std::string padded = s + std::string((8 - length % 8) % 8, '=');
if (!std::ranges::all_of(padded, [](char c) { return static_cast<std::uint8_t>(c) < 0x80; })) {
throw std::invalid_argument("string argument should contain only ASCII characters");
}
const std::string key = Base32Decode(padded);
const auto counter = static_cast<std::uint64_t>(FloorDiv(now, step));
std::string message(8, '\0');
for (std::int32_t i = 0; i < 8; ++i) message[static_cast<std::size_t>(i)] = static_cast<char>((counter >> (56 - 8 * i)) & 0xFF);
const std::string h = HmacSha1(key, message);
const std::size_t o = static_cast<std::uint8_t>(h.back()) & 0x0F;
std::uint32_t value = 0;
for (std::size_t i = 0; i < 4; ++i) value = (value << 8) | static_cast<std::uint8_t>(h[o + i]);
std::uint64_t modulus = 1;
for (std::int32_t i = 0; i < digits; ++i) modulus *= 10;
std::string code = std::to_string((value & 0x7FFFFFFF) % modulus);
if (code.size() < static_cast<std::size_t>(digits)) code.insert(0, static_cast<std::size_t>(digits) - code.size(), '0');
return {code, step - static_cast<std::int32_t>(Mod(now, step))};
}
std::int32_t Main(std::span<const std::string_view> args) {
std::string secret;
try {
if (args.size() == 2 && args[0] == "--secret") {
secret = std::string(args[1]);
} else if (args.size() == 1) {
const Json fixture = Dyn::LoadJsonFile(std::string(args[0]));
const Json* found = nullptr;
for (const std::string_view key : {"totpSecret", "totp_secret", "secret"}) {
const Json& value = Dyn::Get(fixture, key);
if (Dyn::Truthy(value)) {
found = &value;
break;
}
}
if (!found) {
std::println(std::cerr, "{}: no totpSecret in the fixture", args[0]);
return 1;
}
if (!found->is_string()) throw Dyn::Error("AttributeError", std::format("'{}' object has no attribute 'strip'", Dyn::TypeName(*found)));
secret = found->get<std::string>();
} else {
std::println(std::cerr, "{}", Usage);
return 1;
}
const Code code = At(secret, Platform::Now());
std::println("{} (valid {}s)", code.code, code.left);
return 0;
} catch (const std::exception& error) {
std::println(std::cerr, "totp: {}", error.what());
return 1;
}
}
}

View file

@ -64,4 +64,155 @@ export namespace Crafter::Review::PgWire {
std::string Redact(std::string_view url);
// "<database> on <host>:<port> as <user>"
std::string Where(std::string_view url);
// ---- statements ------------------------------------------------------------
// The statements of a script, cut at the semicolons outside quotes, dollar
// quotes, comments, parentheses and the BEGIN ... END body of a
// SQL-standard routine (psql's rules); empty statements dropped. A cut this
// gets wrong is not a hole: every piece goes to the server as one Parse,
// which refuses two commands and fails on half of one.
std::vector<std::string> SplitStatements(std::string_view sql);
// The SQL without its comments, quotes and dollar quotes kept whole.
std::string StripComments(std::string_view sql);
// What a migration's SQL does, blind to comments, whitespace and
// semicolons: a migration file and what the Supabase CLI recorded when it
// applied it compare equal.
std::string Fingerprint(std::string_view sql);
// The first k words, lower case, past whitespace, comments and parentheses.
std::vector<std::string> FirstWords(std::string_view statement, std::size_t k = 3);
// Why the helper will not send this statement, or nullopt. ROLLBACK
// [WORK | TRANSACTION] TO [SAVEPOINT] goes through.
std::optional<std::string> Refused(std::string_view statement);
std::string Headline(std::string_view statement, std::size_t width = 120);
// ---- the connection ----------------------------------------------------------
// The database did not answer: no connection, a refused login, a startup
// failure, a broken exchange. what() is the text the messages print.
class DbError : public std::runtime_error {
public:
using std::runtime_error::runtime_error;
};
// An ErrorResponse or NoticeResponse: {field code: text}.
using Fields = std::map<char, std::string>;
class SqlError : public std::runtime_error {
public:
explicit SqlError(Fields fields);
Fields fields;
};
struct Answer {
std::optional<std::vector<std::string>> columns;
std::vector<std::vector<std::optional<std::string>>> rows;
std::optional<std::string> tag;
std::optional<Fields> error;
std::vector<Fields> notices;
};
// A protocol 3.0 session over TCP: the startup message, cleartext, MD5 or
// SCRAM-SHA-256 (without channel binding, as libpq over a plain
// connection), then simple and extended queries. Throws DbError.
class Connection {
public:
explicit Connection(std::string_view url, std::chrono::seconds timeout = std::chrono::seconds(StatementTimeoutS + 15),
std::string_view application = "review-gates-sql");
~Connection();
Connection(const Connection&) = delete;
Connection& operator=(const Connection&) = delete;
Answer Simple(std::string_view query);
// One statement as Parse / Bind / Describe / Execute / Sync: the
// server refuses two commands in one Parse.
Answer Extended(std::string_view statement);
void Close();
// The server's transaction status after the last ReadyForQuery: I, T
// or E.
char Status() const;
const std::map<std::string, std::string>& Parameters() const;
private:
struct State;
std::unique_ptr<State> state;
};
// SCRAM-SHA-256 (RFC 5802 / 7677). user: empty for Postgres, which takes
// the startup message's; user and nonce are set by the RFC 7677 vector.
class Scram {
public:
explicit Scram(std::string password, std::string user = "", std::string nonce = "");
std::string ClientFirst() const;
std::string ClientFinal(std::string_view serverFirst);
bool Verify(std::string_view serverFinal) const;
private:
std::string password;
std::string nonce;
std::string bare;
std::string authMessage;
std::string serverKey;
};
using Connector = std::function<std::unique_ptr<Connection>(std::string_view url)>;
// A connection, retried while the server is busy (too many connections,
// starting up) or briefly unreachable. No sleep after the last attempt.
std::unique_ptr<Connection> Connect(std::string_view url, std::int32_t attempts = 8,
const std::function<void(double)>& sleep = {});
// ---- output ----------------------------------------------------------------------
std::string Cell(const std::optional<std::string>& value);
std::string Table(const std::vector<std::string>& columns, const std::vector<std::vector<std::optional<std::string>>>& rows);
std::string ErrorText(const Fields& fields, std::string_view statement = {});
std::string NoticeText(const Fields& fields);
// ---- one call of the helper ---------------------------------------------------------
using Out = std::function<void(const std::string&)>;
using Refuser = std::function<std::optional<std::string>(std::string_view statement)>;
// Refuse, connect, BEGIN, the statements one by one, ROLLBACK; the exit
// code. connect and refuse replace Connect and Refused, for the tests
// (the backstop is tested with a refusal list that misses a COMMIT).
std::int32_t RunSql(std::string_view url, std::string_view sql, const Out& out, const Connector& connect = {}, const Refuser& refuse = {});
// (columns, rows) of one read-only query, for the driver: the probe and
// the migrations check. Throws DbError or SqlError.
std::pair<std::vector<std::string>, std::vector<std::vector<std::optional<std::string>>>> Query(std::string_view url, std::string_view sql);
// Does the helper's role bypass row-level security? nullopt when it cannot
// be read.
std::optional<bool> BypassesRls(std::string_view url);
// (ok, detail): does the database under test answer?
std::pair<bool, std::string> Probe(std::string_view url);
// ---- the broker: the URL stays in the driver -------------------------------------------
// The database under test as a pass reaches it: the URL, its password
// included, stays in the driver. A pass's process gets the broker's
// address and its stack's token (BrokerEnv); `crafter-review sql` sends
// the SQL there and the broker runs it as RunSql does and streams the
// lines back, the exit code last. One broker per run, on 127.0.0.1, a
// thread per call.
class Broker {
public:
using Runner = std::function<std::int32_t(const std::string& url, const std::string& sql, const Out& out)>;
using Prober = std::function<std::pair<bool, std::string>(const std::string& url)>;
explicit Broker(Runner run = {}, Prober probe = {});
~Broker();
Broker(const Broker&) = delete;
Broker& operator=(const Broker&) = delete;
void Start();
// A token per URL, made once (secrets.token_hex(16)).
std::string Token(const std::string& url);
// The URL a token names, for the tests.
std::optional<std::string> UrlOf(std::string_view token);
// BrokerEnv's value for a pass whose database is url: "" when there
// is none (the helper then says NO DATABASE).
std::string Address(const std::string& url);
void Close();
private:
struct State;
std::shared_ptr<State> state;
};
// `crafter-review sql`'s side of a call: the SQL to the broker at address
// (BrokerEnv's value), its lines printed; the exit code.
std::int32_t CallBroker(std::string_view address, std::string_view sql, bool probeOnly, const Out& out);
// `crafter-review sql [SQL...|--probe]`: the statements as arguments or on
// stdin. Exit 0 every statement ran, 1 one failed or was refused, 2 no
// database, 64 no statement.
std::int32_t SqlMain(std::span<const std::string_view> args);
}

View file

@ -0,0 +1,23 @@
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
export module Crafter.Review:Totp;
import std;
// A current TOTP code (RFC 6238, SHA-1, 30 s, 6 digits) for a login fixture:
// what the functional lens types into the one-time-code field.
export namespace Crafter::Review::Totp {
struct Code {
std::string code;
// The seconds left in its window.
std::int32_t left;
};
// secret: base32, spaces and case as a seeder writes it; std::invalid_argument
// with binascii's message for one that is no base32. The clock is read once,
// so the code and the seconds left belong to the same window.
Code At(std::string_view secret, double now, std::int32_t step = 30, std::int32_t digits = 6);
// `crafter-review totp <fixture.json>` or `--secret <base32>`: prints
// "123456 (valid 17s)". Exit 1 for a fixture without a secret or a bad
// command line.
std::int32_t Main(std::span<const std::string_view> args);
}

View file

@ -19,6 +19,7 @@ export import :Dispositions;
export import :State;
export import :Normalize;
export import :PgWire;
export import :Totp;
export import :Stream;
export import :Prompt;
export import :Config;

View file

@ -13,7 +13,7 @@ using namespace Crafter;
namespace {
// The umbrella, then the partitions in import order: each after the ones
// it imports, which is also the order the module map in AGENTS.md uses.
constexpr std::array<std::string_view, 22> Interfaces = {
constexpr std::array<std::string_view, 23> Interfaces = {
"interfaces/Crafter.Review",
"interfaces/Crafter.Review-Unicode",
"interfaces/Crafter.Review-Json",
@ -31,13 +31,14 @@ namespace {
"interfaces/Crafter.Review-State",
"interfaces/Crafter.Review-Normalize",
"interfaces/Crafter.Review-PgWire",
"interfaces/Crafter.Review-Totp",
"interfaces/Crafter.Review-Stream",
"interfaces/Crafter.Review-Prompt",
"interfaces/Crafter.Review-Config",
"interfaces/Crafter.Review-Freeze",
"interfaces/Crafter.Review-Driver",
};
constexpr std::array<std::string_view, 19> Implementations = {
constexpr std::array<std::string_view, 20> Implementations = {
"implementations/Crafter.Review-Text",
"implementations/Crafter.Review-Crypto",
"implementations/Crafter.Review-Platform",
@ -52,6 +53,7 @@ namespace {
"implementations/Crafter.Review-State",
"implementations/Crafter.Review-Normalize",
"implementations/Crafter.Review-PgWire",
"implementations/Crafter.Review-Totp",
"implementations/Crafter.Review-Stream",
"implementations/Crafter.Review-Prompt",
"implementations/Crafter.Review-Config",
@ -66,7 +68,7 @@ namespace {
// One per directory under tests/, each an ordinary main() that links the
// library: exit 0 passes, nonzero fails, 77 skips.
constexpr std::array<std::string_view, 13> Tests = {
constexpr std::array<std::string_view, 14> Tests = {
"Crypto",
"Text",
"Platform",
@ -80,6 +82,7 @@ namespace {
"CommitsMode",
"CodeKeys",
"Freeze",
"PgWire",
};
// The version the binary reports: CI sets it from the tag, a checkout

File diff suppressed because it is too large Load diff

View file

@ -410,6 +410,60 @@ namespace {
for (const Json& entry : c.at("redact")) Same(PgWire::Redact(entry.at(0).get<std::string>()), entry.at(1).get<std::string>(), std::format("redact {}", entry.at(0).get<std::string>()));
for (const Json& entry : c.at("where")) Same(PgWire::Where(entry.at(0).get<std::string>()), entry.at(1).get<std::string>(), std::format("where {}", entry.at(0).get<std::string>()));
}
PgWire::Fields FieldsOf(const Json& dict) {
PgWire::Fields out;
for (auto it = dict.begin(); it != dict.end(); ++it) out[it.key().at(0)] = it.value().get<std::string>();
return out;
}
std::vector<std::vector<std::optional<std::string>>> RowsOf(const Json& rows) {
std::vector<std::vector<std::optional<std::string>>> out;
for (const Json& row : rows) {
std::vector<std::optional<std::string>> cells;
for (const Json& cell : row) cells.push_back(OptionalString(cell));
out.push_back(std::move(cells));
}
return out;
}
void CheckPgWire(const Json& g) {
const Json& p = g.at("pgwire");
for (const Json& entry : p.at("split")) SameJson(ToJson(PgWire::SplitStatements(entry.at(0).get<std::string>())), entry.at(1), std::format("split_statements {}", Dyn::Repr(entry.at(0).get<std::string>())));
for (const Json& entry : p.at("stripComments")) Same(PgWire::StripComments(entry.at(0).get<std::string>()), entry.at(1).get<std::string>(), std::format("strip_comments {}", Dyn::Repr(entry.at(0).get<std::string>())));
for (const Json& entry : p.at("fingerprint")) Same(PgWire::Fingerprint(entry.at(0).get<std::string>()), entry.at(1).get<std::string>(), std::format("fingerprint {}", Dyn::Repr(entry.at(0).get<std::string>())));
for (const Json& entry : p.at("refused")) SameJson(Optional(PgWire::Refused(entry.at(0).get<std::string>())), entry.at(1), std::format("refused {}", Dyn::Repr(entry.at(0).get<std::string>())));
for (const Json& entry : p.at("firstWords")) SameJson(ToJson(PgWire::FirstWords(entry.at(0).get<std::string>(), 4)), entry.at(1), std::format("first_words {}", Dyn::Repr(entry.at(0).get<std::string>())));
for (const Json& entry : p.at("headline")) Same(PgWire::Headline(entry.at(0).get<std::string>()), entry.at(1).get<std::string>(), std::format("headline {}", Dyn::Repr(entry.at(0).get<std::string>())));
for (const Json& entry : p.at("cell")) Same(PgWire::Cell(OptionalString(entry.at(0))), entry.at(1).get<std::string>(), std::format("_cell {}", DumpCompactSorted(entry.at(0))));
for (const Json& entry : p.at("table")) Same(PgWire::Table(Strings(entry.at(0)), RowsOf(entry.at(1))), entry.at(2).get<std::string>(), std::format("table {}", DumpCompactSorted(entry.at(0))));
for (const Json& entry : p.at("errorText")) {
Same(PgWire::ErrorText(FieldsOf(entry.at(0)), OptionalString(entry.at(1)).value_or("")), entry.at(2).get<std::string>(), std::format("error_text {}", DumpCompactSorted(entry.at(0))));
}
for (const Json& entry : p.at("noticeText")) Same(PgWire::NoticeText(FieldsOf(entry.at(0))), entry.at(1).get<std::string>(), std::format("notice_text {}", DumpCompactSorted(entry.at(0))));
for (const Json& entry : p.at("runs")) {
if (!entry.contains("noServer")) continue;
std::string sql;
if (entry.at("sql").is_object()) {
const std::string unit = entry.at("sql").at("repeat").get<std::string>();
const auto length = entry.at("sql").at("length").get<std::size_t>();
while (sql.size() < length) sql += unit;
sql.resize(length);
} else {
sql = entry.at("sql").get<std::string>();
}
std::vector<std::string> lines;
const std::int32_t code = PgWire::RunSql("postgresql://u:pw@127.0.0.1:1/db", sql, [&](const std::string& line) { lines.push_back(line); });
SameJson(Json::array({code, ToJson(lines)}), Json::array({entry.at("code"), entry.at("out")}), std::format("run (no server) {}", Dyn::Repr(Dyn::Slice(sql, 0, 40))));
}
std::vector<std::string> hosted;
const std::int32_t code = PgWire::RunSql("postgresql://u@db.x.supabase.co/x", "select 1", [&](const std::string& line) { hosted.push_back(line); });
SameJson(Json::array({code, ToJson(hosted)}), p.at("hosted"), "run (a hosted database)");
for (const Json& entry : p.at("totp")) {
const Totp::Code got = Totp::At(entry.at(0).get<std::string>(), entry.at(1).get<double>());
SameJson(Json::array({got.code, got.left}), entry.at(2), std::format("totp {} at {}", entry.at(0).get<std::string>(), Dyn::ReprValue(entry.at(1))));
}
}
}
int main() {
@ -425,6 +479,7 @@ int main() {
CheckStream(g);
CheckState(g);
CheckConfig(g);
CheckPgWire(g);
std::println("{} checks against the goldens", Checks);
if (Failures) std::println(std::cerr, "{} failed", Failures);
return Failures ? 1 : 0;

383
tests/PgWire/main.cpp Normal file
View file

@ -0,0 +1,383 @@
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// The SQL helper: its statements, refusals, the URL, SCRAM, the protocol on a
// stand-in server (FakePg, below). Then what the helper prints over the same
// stand-in, line for line against the goldens (fixture/pipeline.json,
// `runs`), and the broker: the URL stays in the driver, a pass gets an
// address and a token.
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
import std;
import Crafter.Review;
namespace fs = std::filesystem;
using namespace Crafter::Review;
namespace {
std::int32_t Failures = 0;
void Check(std::string_view name, bool cond, std::string_view detail = {}) {
std::println("{}{}{}", cond ? " ok " : " FAIL ", name, !cond && !detail.empty() ? std::format(" ({})", detail) : std::string());
if (!cond) ++Failures;
}
std::string Be32(std::uint32_t v) {
return {static_cast<char>(v >> 24), static_cast<char>((v >> 16) & 0xFF), static_cast<char>((v >> 8) & 0xFF), static_cast<char>(v & 0xFF)};
}
std::string Msg(char type, const std::string& body = {}) {
return std::format("{}{}{}", type, Be32(static_cast<std::uint32_t>(body.size() + 4)), body);
}
std::string Err(const std::string& code, const std::string& text) {
return Msg('E', std::format("SERROR{}C{}{}M{}{}{}", std::string(1, '\0'), code, std::string(1, '\0'), text, std::string(1, '\0'), std::string(1, '\0')));
}
// A Postgres stand-in on 127.0.0.1 for one connection: cleartext
// password, simple Query, Parse/Bind/Describe/Execute/Sync. It records
// what it is sent and answers as the server does where the helper relies
// on it: two commands in one Parse are an error; COMMIT ends the
// transaction (status I); a statement after an error fails.
class FakePg {
public:
explicit FakePg(std::string password = "pw") : password(std::move(password)) {
listener = socket(AF_INET, SOCK_STREAM, 0);
sockaddr_in address {};
address.sin_family = AF_INET;
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
bind(listener, reinterpret_cast<sockaddr*>(&address), sizeof(address));
listen(listener, 1);
socklen_t length = sizeof(address);
getsockname(listener, reinterpret_cast<sockaddr*>(&address), &length);
port = ntohs(address.sin_port);
thread = std::thread([this] { Serve(); });
}
~FakePg() {
Served();
}
// The log once the connection is over.
std::vector<std::pair<std::string, std::string>> Served() {
if (thread.joinable()) thread.join();
return log;
}
std::uint16_t port = 0;
private:
std::string password;
std::int32_t listener = -1;
std::thread thread;
std::vector<std::pair<std::string, std::string>> log;
void Serve() {
const std::int32_t c = accept(listener, nullptr, nullptr);
std::string buffer;
auto read = [&](std::size_t n) {
while (buffer.size() < n) {
std::array<char, 4096> chunk {};
const ssize_t got = recv(c, chunk.data(), chunk.size(), 0);
if (got <= 0) break;
buffer.append(chunk.data(), static_cast<std::size_t>(got));
}
const std::string out = buffer.substr(0, std::min(n, buffer.size()));
buffer.erase(0, out.size());
return out;
};
auto length = [](const std::string& bytes) {
return (static_cast<std::uint32_t>(static_cast<std::uint8_t>(bytes[0])) << 24) | (static_cast<std::uint32_t>(static_cast<std::uint8_t>(bytes[1])) << 16)
| (static_cast<std::uint32_t>(static_cast<std::uint8_t>(bytes[2])) << 8) | static_cast<std::uint8_t>(bytes[3]);
};
auto send = [&](const std::string& data) { ::send(c, data.data(), data.size(), MSG_NOSIGNAL); };
const std::uint32_t startup = length(read(4));
read(startup - 4);
send(Msg('R', Be32(3)));
read(1);
std::string got = read(length(read(4)) - 4);
while (!got.empty() && got.back() == '\0') got.pop_back();
log.emplace_back("password", got);
if (got != password) {
send(Err("28P01", "password authentication failed"));
close(c);
close(listener);
return;
}
send(Msg('S', std::string("server_version\x00" "17.6\x00", 20)) + Msg('R', Be32(0)) + Msg('Z', "I"));
std::string status = "I";
std::string pending;
while (true) {
const std::string type = read(1);
if (type.empty()) break;
const std::string body = read(length(read(4)) - 4);
if (type == "Q") {
std::string q = body;
while (!q.empty() && q.back() == '\0') q.pop_back();
log.emplace_back("Q", q);
status = q.starts_with("BEGIN") ? "T" : (q == "ROLLBACK" ? "I" : status);
send(Msg('C', Dyn::Split(q)[0] + std::string(1, '\0')) + Msg('Z', status));
} else if (type == "P") {
pending = body.substr(1, body.find('\0', 1) - 1);
log.emplace_back("P", pending);
} else if (type == "S") {
const std::string low = Dyn::Lower(pending);
std::string out;
if (status == "E") {
out = Err("25P02", "current transaction is aborted");
} else if (pending.contains(';')) {
out = Err("42601", "cannot insert multiple commands into a prepared statement");
status = "E";
} else if (low.starts_with("commit")) {
out = Msg('C', std::string("COMMIT\0", 7));
status = "I";
} else if (low.starts_with("select")) {
out = Msg('T', std::string("\x00\x01x\x00", 4) + std::string(18, '\0')) + Msg('D', std::string("\x00\x01\x00\x00\x00\x01" "1", 7))
+ Msg('N', std::string("SNOTICE\0Mhello\0\0", 16)) + Msg('C', std::string("SELECT 1\0", 9));
} else if (low.starts_with("boom")) {
out = Err("42601", "syntax error at or near \"boom\"");
status = "E";
} else {
out = Msg('C', Dyn::Upper(Dyn::Split(pending)[0]) + std::string(1, '\0'));
}
send(Msg('1') + Msg('2') + out + Msg('Z', status));
} else if (type == "X") {
log.emplace_back("X", "");
break;
}
}
close(c);
close(listener);
}
};
std::uint16_t DeadPort() {
const std::int32_t s = socket(AF_INET, SOCK_STREAM, 0);
sockaddr_in address {};
address.sin_family = AF_INET;
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
bind(s, reinterpret_cast<sockaddr*>(&address), sizeof(address));
socklen_t length = sizeof(address);
getsockname(s, reinterpret_cast<sockaddr*>(&address), &length);
close(s);
return ntohs(address.sin_port);
}
std::vector<std::string> Of(const std::vector<std::pair<std::string, std::string>>& log, std::string_view kind) {
std::vector<std::string> out;
for (const auto& [k, x] : log) {
if (k == kind) out.push_back(x);
}
return out;
}
bool Any(const std::vector<std::string>& lines, std::string_view part) {
return std::ranges::any_of(lines, [&](const std::string& line) { return line.contains(part); });
}
std::string Show(const std::vector<std::string>& lines) {
Json out = Json::array();
for (const std::string& line : lines) out.push_back(line);
return Dyn::ReprValue(out);
}
// A raw call of the broker: one line in, every line back.
std::vector<std::string> RawCall(std::uint16_t port, const std::string& line) {
const std::int32_t s = socket(AF_INET, SOCK_STREAM, 0);
sockaddr_in address {};
address.sin_family = AF_INET;
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
address.sin_port = htons(port);
connect(s, reinterpret_cast<sockaddr*>(&address), sizeof(address));
::send(s, line.data(), line.size(), MSG_NOSIGNAL);
shutdown(s, SHUT_WR);
std::string all;
std::array<char, 4096> chunk {};
ssize_t got = 0;
while ((got = recv(s, chunk.data(), chunk.size(), 0)) > 0) all.append(chunk.data(), static_cast<std::size_t>(got));
close(s);
return Dyn::Split(Dyn::RStrip(all, "\n"), "\n");
}
}
int main() {
std::println("the SQL helper (pgwire): statements, refusals, the URL, SCRAM, the protocol on a stand-in server");
using Pieces = std::vector<std::string>;
auto sp = [](std::string_view sql) { return PgWire::SplitStatements(sql); };
Check("split: semicolons in quotes, E-strings, dollar quotes (tagged, and $ inside identifiers), comments (nested, -- to \\r), double quotes and parentheses do not cut; empty statements drop",
sp("select ';' ; select 2;") == Pieces{"select ';'", "select 2"} && sp("select $x$ a; $y$ ; $x$; commit") == Pieces{"select $x$ a; $y$ ; $x$", "commit"}
&& sp("select 1 as a$b$; commit; select $b$") == Pieces{"select 1 as a$b$", "commit", "select $b$"} && sp("select E'a\\'; commit; '") == Pieces{"select E'a\\'; commit; '"}
&& sp("-- c;\nselect 1; /* x; /* y; */ z; */ select 2") == Pieces{"-- c;\nselect 1", "/* x; /* y; */ z; */ select 2"} && sp("select 1 --x\r; commit") == Pieces{"select 1 --x", "commit"}
&& sp("create rule r as on insert to t do also (insert into a values (1); update b set c = 1); select 3")
== Pieces{"create rule r as on insert to t do also (insert into a values (1); update b set c = 1)", "select 3"}
&& sp("select \"a;b\" from t; select 2") == Pieces{"select \"a;b\" from t", "select 2"} && sp(" ;; -- only\n ; ").empty());
const std::vector<std::string> refuse = {"commit", "COMMIT AND CHAIN", "end", "END WORK", "rollback", "rollback work", "abort", "begin", "start transaction", "prepare transaction 'x'",
"commit prepared 'x'", "rollback prepared 'x'", "/* c */ commit", "-- c\ncommit", "--c\rcommit", "(commit)", "copy t to stdout", "COPY t FROM PROGRAM 'x'"};
const std::vector<std::string> allow = {"rollback to s1", "ROLLBACK TO SAVEPOINT s", "savepoint s", "release s", "prepare p as select 1", "select 1", "with x as (select 1) select * from x",
"do $$ begin commit; end $$", "call p()", "endless_fn()", "end1()"};
std::vector<std::string> wrong;
for (const std::string& s : refuse) {
if (!PgWire::Refused(s)) wrong.push_back(s);
}
for (const std::string& s : allow) {
if (PgWire::Refused(s)) wrong.push_back(s);
}
Check("refused: every statement that ends, restarts or prepares the transaction, and COPY - past comments and parentheses; ROLLBACK TO, SAVEPOINT, DO and CALL go to the server (which refuses COMMIT inside them in a transaction block)",
wrong.empty(), Show(wrong));
Check("URL: this machine only; the password masked for a log or a dump", !PgWire::CheckUrl("postgresql://postgres:postgres@127.0.0.1:54322/postgres") && !PgWire::CheckUrl("postgres://u@localhost/x")
&& !PgWire::CheckUrl("postgresql://u@[::1]:5/x") && PgWire::CheckUrl("postgresql://u:p@db.abcdefgh.supabase.co:5432/postgres").value_or("").contains("not this machine")
&& PgWire::CheckUrl("http://127.0.0.1:54321") && PgWire::CheckUrl("") && PgWire::CheckUrl(nullptr)
&& PgWire::Redact("postgresql://postgres:hunter2@127.0.0.1:54322/postgres") == "postgresql://postgres:***@127.0.0.1:54322/postgres"
&& PgWire::Redact("postgresql://127.0.0.1/x") == "postgresql://127.0.0.1/x");
PgWire::Scram sc("pencil", "user", "rOprNGfwEbeRWgbNEkqO");
const std::string sf = "r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096";
Check("SCRAM-SHA-256: the RFC 7677 test vector - the client-first, the client proof, the server signature verified (and a wrong one refused)",
sc.ClientFirst() == "n,,n=user,r=rOprNGfwEbeRWgbNEkqO"
&& sc.ClientFinal(sf) == "c=biws,r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,p=dHzbZapWIk4jUhN+Ute9ytag9zjfMHgsqmmiz7AndVQ="
&& sc.Verify("v=6rriTRBi23WpRR/wtup+mMhUZUn/dB5nLTJRsjl95G4=") && !sc.Verify("v=AAAATRBi23WpRR/wtup+mMhUZUn/dB5nLTJRsjl95G4="));
std::vector<std::string> out;
const PgWire::Out collect = [&](const std::string& line) { out.push_back(line); };
{
FakePg pg;
const std::int32_t code = PgWire::RunSql(std::format("postgresql://u:pw@127.0.0.1:{}/db", pg.port), "select 1; insert into t values ('a'); boom; select 3", collect);
const auto log = pg.Served();
const std::vector<std::string> queries = Of(log, "Q");
const std::vector<std::string> setup(queries.begin(), queries.begin() + std::min<std::ptrdiff_t>(4, static_cast<std::ptrdiff_t>(queries.size())));
Check("run: the password, BEGIN and the three SET LOCALs, then one Parse per statement up to the failing one, then ROLLBACK and Terminate; the call says it connected, prints rows, the notice, the error and that it stopped; exit 1",
!log.empty() && log[0] == std::pair<std::string, std::string>{"password", "pw"}
&& setup == std::vector<std::string>{"BEGIN", "SET LOCAL statement_timeout = '20s'", "SET LOCAL lock_timeout = '3s'", "SET LOCAL idle_in_transaction_session_timeout = '60s'"}
&& Of(log, "P") == std::vector<std::string>{"select 1", "insert into t values ('a')", "boom"} && log.size() >= 2 && log[log.size() - 2] == std::pair<std::string, std::string>{"Q", "ROLLBACK"}
&& log.back() == std::pair<std::string, std::string>{"X", ""} && code == 1 && out.at(0).starts_with(PgWire::Answered) && std::ranges::contains(out, std::string("NOTICE: hello"))
&& Any(out, "syntax error") && Any(out, "statement 3 failed") && out.back() == "-- rolled back: nothing of this call was committed", Show(out));
}
{
FakePg pg;
std::unique_ptr<PgWire::Connection> cn = PgWire::Connect(std::format("postgresql://u:pw@127.0.0.1:{}/db", pg.port), 1);
cn->Simple("BEGIN");
const PgWire::Answer two = cn->Extended("select 1; select 2");
cn->Simple("ROLLBACK");
cn->Close();
const auto log = pg.Served();
Check("extended: a whole script in one Parse comes back as the server refuses it (two commands), the transaction failed - never run", two.error && two.error->at('C') == "42601"
&& two.rows.empty() && std::ranges::contains(log, std::pair<std::string, std::string>{"P", "select 1; select 2"}));
}
{
out.clear();
FakePg pg;
// A statement the refusal list misses: the transaction status is the
// backstop.
const std::int32_t code = PgWire::RunSql(std::format("postgresql://u:pw@127.0.0.1:{}/db", pg.port), "create table x (a int); commit; select 1", collect, {},
[](std::string_view) { return std::optional<std::string>(); });
const auto log = pg.Served();
Check("run: the backstop - after a statement that ended the transaction nothing more is sent, the call fails and says what may have been committed",
Of(log, "P") == std::vector<std::string>{"create table x (a int)", "commit"} && code == 1 && Any(out, "STOPPED") && out.back().contains("may have been committed"), Show(out));
}
{
out.clear();
FakePg pg;
const std::string url = std::format("postgresql://u:pw@127.0.0.1:{}/db", pg.port);
const std::int32_t code = PgWire::RunSql(url, "select 1; commit", collect);
PgWire::Connect(url, 1)->Close();
const auto log = pg.Served();
Check("run: a refused statement stops the call before it connects - nothing is sent, no connected line", code == 1 && out.at(0).starts_with("review-gates sql: refused, nothing ran")
&& out.at(0).contains("statement 2: COMMIT") && !Any(out, PgWire::Answered) && Of(log, "Q").empty() && Of(log, "P").empty());
}
{
out.clear();
const std::uint16_t dead = DeadPort();
const PgWire::Connector once = [](std::string_view url) { return PgWire::Connect(url, 1, [](double) {}); };
const std::int32_t code = PgWire::RunSql(std::format("postgresql://u:pw@127.0.0.1:{}/db", dead), "select 1", collect, once);
std::vector<std::string> ignored;
const PgWire::Out drop = [&](const std::string& line) { ignored.push_back(line); };
Check("run: nothing listening -> NO DATABASE, exit 2, no connected line; a hosted URL never connects; no statement is a usage error", code == 2 && out.at(0).starts_with(PgWire::NoDatabase)
&& PgWire::RunSql("postgresql://u@db.x.supabase.co/x", "select 1", drop) == 2 && PgWire::RunSql(std::format("postgresql://u@127.0.0.1:{}/x", dead), " -- nothing\n", drop) == 64);
Check("...and the reason: cannot connect to 127.0.0.1:<port> ([Errno 111] Connection refused)",
out.at(0) == std::format("{} - cannot connect to 127.0.0.1:{} ([Errno 111] Connection refused)", PgWire::NoDatabase, dead), out.at(0));
}
{
out.clear();
FakePg pg("other");
const PgWire::Connector once = [](std::string_view url) { return PgWire::Connect(url, 1); };
const std::int32_t code = PgWire::RunSql(std::format("postgresql://u:pw@127.0.0.1:{}/db", pg.port), "select 1", collect, once);
const std::string detail = out.at(0).substr(out.at(0).find(" - ") + 3);
Check("run: a refused login is NO DATABASE (exit 2), its message without the password", code == 2 && out.at(0).starts_with(PgWire::NoDatabase) && out.at(0).contains("28P01")
&& !detail.contains("pw"), Show(out));
}
{
// A busy server is asked again, but not slept on after the last try.
std::vector<double> slept;
try {
PgWire::Connect(std::format("postgresql://u@127.0.0.1:{}/x", DeadPort()), 3, [&](double s) { slept.push_back(s); });
} catch (const PgWire::DbError&) {
// Expected: nothing listens.
}
Check("connect: an unreachable server is tried three times, slept on between tries only", slept == std::vector<double>{0.5, 1.0});
}
// What the helper prints over the stand-in, line for line against the
// goldens.
const Json goldens = Dyn::LoadJsonFile(fs::current_path() / "tests" / "Goldens" / "fixture" / "pipeline.json");
std::int32_t compared = 0;
for (const Json& entry : goldens.at("pgwire").at("runs")) {
if (entry.contains("noServer")) continue;
out.clear();
FakePg pg;
const std::int32_t code = PgWire::RunSql(std::format("postgresql://u:pw@127.0.0.1:{}/db", pg.port), entry.at("sql").get<std::string>(), collect);
pg.Served();
std::vector<std::string> normalized;
for (const std::string& line : out) normalized.push_back(Dyn::Replace(line, std::format(":{}", pg.port), ":PORT"));
std::vector<std::string> want;
for (const Json& line : entry.at("out")) want.push_back(line.get<std::string>());
if (code == entry.at("code").get<std::int32_t>() && normalized == want) ++compared;
else Check(std::format("run over the stand-in, as the goldens record it: {}", entry.at("sql").get<std::string>()), false, Show(normalized));
}
Check(std::format("run over the stand-in prints what the goldens record, line for line ({} scripts)", compared), compared == 6);
// The broker.
std::vector<std::string> ran;
PgWire::Broker broker(
[&](const std::string& url, const std::string& sql, const PgWire::Out& say) {
ran.push_back(std::format("{}|{}", url, sql));
say(std::format("{} to x", PgWire::Answered));
say("é line");
return 1;
},
[](const std::string& url) { return std::pair<bool, std::string>{url.contains("good"), "detail"}; });
broker.Start();
const std::string url = "postgresql://postgres:hunter2@127.0.0.1:54322/postgres";
const std::string address = broker.Address(url);
const std::string token = address.substr(address.rfind('#') + 1);
Check("broker: the address is 127.0.0.1:<port>#<32 hex>, one token per URL, and the token names its URL", address.starts_with("127.0.0.1:") && token.size() == 32
&& std::ranges::all_of(token, [](char c) { return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'); }) && broker.Address(url) == address
&& broker.Address("postgresql://other@127.0.0.1/x") != address && broker.UrlOf(token) == url && broker.Address("").empty() && !address.contains("hunter2"));
out.clear();
std::int32_t code = PgWire::CallBroker(address, "select 1;\nselect 'é';", false, collect);
Check("broker: a call runs the SQL on its token's URL, streams the lines back and returns the call's exit code", code == 1 && ran.size() == 1 && ran[0] == std::format("{}|select 1;\nselect 'é';", url)
&& out == std::vector<std::string>{std::format("{} to x", PgWire::Answered), "é line"}, Show(out));
out.clear();
code = PgWire::CallBroker(broker.Address("postgresql://good@127.0.0.1/x"), "", true, collect);
Check("broker: a probe answers with the connected line and exit 0", code == 0 && out == std::vector<std::string>{std::format("{} - detail", PgWire::Answered)}, Show(out));
out.clear();
code = PgWire::CallBroker(std::format("{}#{}", address.substr(0, address.rfind('#')), std::string(32, '0')), "select 1", false, collect);
Check("broker: an unknown token is NO DATABASE, exit 2, and nothing runs", code == 2 && out.size() == 1 && out[0].starts_with(PgWire::NoDatabase) && ran.size() == 1, Show(out));
const std::uint16_t brokerPort = static_cast<std::uint16_t>(std::stoi(address.substr(10, address.rfind('#') - 10)));
const std::vector<std::string> notWhole = RawCall(brokerPort, "{\"token\": \"x\"");
Check("broker: a call that never ends its line did not arrive whole - nothing runs, exit 64", notWhole.size() == 2 && notWhole[0].contains("did not arrive whole")
&& notWhole[1] == std::format("{}64", PgWire::ExitMark), Show(notWhole));
const std::vector<std::string> listToken = RawCall(brokerPort, "{\"token\": [1], \"sql\": \"select 1\"}\n");
Check("broker: a token that is no string gets NO DATABASE and the exit mark", listToken.size() == 2
&& listToken[0].starts_with(PgWire::NoDatabase) && listToken[1] == std::format("{}2", PgWire::ExitMark), Show(listToken));
out.clear();
Check("call_broker: no address, a bad port, a closed port - NO DATABASE, exit 2, with the reason",
PgWire::CallBroker("", "x", false, collect) == 2 && PgWire::CallBroker("127.0.0.1:x#t", "x", false, collect) == 2
&& PgWire::CallBroker(std::format("127.0.0.1:{}#t", DeadPort()), "x", false, collect) == 2 && out.size() == 3
&& out[0] == std::format("{} - this pass has no database (the driver gives one to a pass with the supabase environment)", PgWire::NoDatabase)
&& out[1] == std::format("{} - the driver's SQL broker does not answer (invalid literal for int() with base 10: 'x')", PgWire::NoDatabase)
&& out[2].ends_with("does not answer ([Errno 111] Connection refused)"), Show(out));
broker.Close();
out.clear();
Check("broker: closed, its port answers no more", PgWire::CallBroker(address, "select 1", false, collect) == 2 && out.at(0).ends_with("([Errno 111] Connection refused)"), Show(out));
return Failures ? 1 : 0;
}