Skip to content

Pure Pascal Firebird wire protocol client: connect without fbclient - #9

Open
mariuz wants to merge 70 commits into
MWASoftware:mainfrom
mariuz:pure-pascal-wire-protocol
Open

Pure Pascal Firebird wire protocol client: connect without fbclient#9
mariuz wants to merge 70 commits into
MWASoftware:mainfrom
mariuz:pure-pascal-wire-protocol

Conversation

@mariuz

@mariuz mariuz commented Jul 31, 2026

Copy link
Copy Markdown

Adds a pure Pascal implementation of the Firebird remote (wire) protocol, so
fbintf can reach a Firebird 3.0, 4.0, 5.0 or 6.0 server over TCP with no
fbclient library installed
. It is a third provider alongside the existing
two, so code written against the fbintf interfaces runs unchanged.

Offered for consideration rather than as a finished proposal: it is a large
addition and it touches shared build files, so if the direction is welcome
but the packaging is not, I am happy to rework it. Everything new lives under
client/wire; the existing providers are untouched.

uses IB, FBWireClientAPI;

API := WireFirebirdAPI;      {instead of IB.FirebirdAPI - loads no library}
DPB := API.AllocateDPB;
DPB.Add(isc_dpb_user_name).AsString := 'SYSDBA';
DPB.Add(isc_dpb_password).AsString := 'masterkey';
Attachment := API.OpenDatabase('localhost:employee',DPB);

Everything below that line is the ordinary object model: IAttachment,
ITransaction, IStatement, IResultSet, IBlob.

Why

client/2.5 and client/3.0 both dynamically load fbclient. That is the
right answer on a developer machine and an awkward one for containers,
single file deployments, cross compiled targets, and any machine whose
installed client library is older than the server it has to reach. This
provider implements the protocol those libraries speak, so the dependency
disappears: the compiled program is the whole client.

What works

  • handshake with protocol negotiation, versions 13 to 17
  • Srp256 and Srp authentication, in both the op_cond_accept flow
    (authentication completes before the attach) and the op_accept_data
    flow (the proof travels in the DPB)
  • wire encryption: ChaCha64, ChaCha, Arc4
  • attach, create and drop database, detach
  • transactions: start, commit, rollback, the retaining variants, 2PC prepare
  • DSQL: allocate, prepare with full describe, execute, op_execute2 for
    singleton results, fetch, close, drop, set cursor name
  • every data type, including INT128, DECFLOAT(16), DECFLOAT(34),
    BOOLEAN and the time zone types
  • blobs: create, open, segmented read and write, close, cancel
  • information calls for database, transaction, statement and blob

The password is used for the SRP exchange and stripped from the DPB before
the attach, so it never reaches the network in any form.

What does not, yet

Each raises ibxeNotSupported rather than failing obscurely:

Feature What it needs
Events the auxiliary connection from op_connect_request and a listener thread
Services the packet exchanges exist in FBWireProtocol; the IServiceManager wrapper does not
Arrays op_get_slice / op_put_slice with SDL descriptions
Batches the protocol 16 op_batch_* family
Scrollable cursors op_fetch_scroll, protocol 18
Multi database transactions a two phase commit coordinator

FPC only for now. The transport is written against the FPC socket units;
everything above it is compiler neutral, so a Delphi transport is the only
missing piece. The Delphi packages are deliberately left untouched.

Layout

Thirteen units under client/wire, in three layers:

Layer Units
Transport FBWireStream (buffered TCP, per direction ciphers, XDR), FBWireCrypto, FBWireSRP, FBWireBigInt, FBWireConst
Protocol FBWireProtocol (handshake, one method per packet), FBWireMessage, FBWireDescribe
Provider FBWireClientAPI, FBWireAttachment, FBWireTransaction, FBWireStatement, FBWireBlob

FBWireProtocol is usable on its own if you want the protocol without the
fbintf object model.

The provider reuses the existing machinery rather than duplicating it:
FBParamBlock already builds exactly the clumplet buffers the protocol
carries, FBOutputBlock already parses the information responses, and
TSQLDataItem supplies every data conversion — the provider only points it
at the right offset in the message buffer.

Testing

testsuite/WireTest.pas is self contained and needs no fbclient:

fpc -Fuclient -Fuclient/2.5 -Fuclient/3.0 -Fuclient/3.0/firebird \
    -Fuclient/wire -Ficlient/include -FEbuild testsuite/WireTest.pas
./build/WireTest localhost:employee SYSDBA masterkey localhost:/tmp/scratch.fdb

Four offline layers that need no server — big integer arithmetic including a
1024 bit modular exponentiation of the size SRP performs, SHA-1, SHA-256,
RC4 and ChaCha20 against the published FIPS and RFC 8439 vectors, a complete
SRP exchange against a fixed reference exchange, and the message layout and
generated BLR — then the live ones: the handshake, what version is
negotiated when the offer is capped at 14/15/16/17, queries and parameters,
every data type, DML, null handling, accented text, blob round trips,
rollback, and create and drop database.

Every supported server version has been run, all with 0 failures:

Server WireCrypt Negotiated Encryption Result
6.0 (CI) Enabled, Required 17 ChaCha64 81 tests
6.0.0 (local, LI-T6.0.0.2076) Required 17 ChaCha64 81 tests
5.0 (CI) Enabled, Required 17 ChaCha64 81 tests
5.0.4 (local container) Enabled, Required 17 ChaCha64 81 tests
4.0 (CI) Enabled, Required 17 ChaCha64 81 tests
3.0 (CI) Enabled 15 Arc4 81 tests
no server 36 tests, live sections skipped

Firebird 3 settles on protocol 15 with Arc4 because that is the newest
protocol it knows and it predates the ChaCha plugins. Everything else in
the suite behaves identically there.

Capping the offer, against a modern server: 14 → Arc4, 15 → Arc4,
16 → ChaCha64, 17 → ChaCha64. Protocol 13 is refused with
isc_miss_wirecrypt by a server that requires encryption, which is correct:
it predates wire encryption. It is still offered for Firebird 3 and for
servers set to WireCrypt = Enabled.

CI

.github/workflows/wire-protocol.yml, modelled on the node-firebird matrix.

The server job runs Firebird 3, 4, 5 and 6 containers against both
WireCrypt = Enabled and WireCrypt = Required — seven combinations.
Firebird 3 with Required is excluded because protocol 13 cannot satisfy it
by definition. Each job starts the container, creates a database with isql
inside it, builds with the distribution's FPC and runs the test over TCP.
Nothing Firebird related is installed on the runner, which is the whole
point of the client. The negotiated protocol and the totals go to the step
summary, so the matrix view shows what each version settled on.

The offline job builds and runs the same binary on Linux and Windows
with no server at all, so an arithmetic or cryptography regression cannot be
masked by a server problem and the code keeps compiling on Windows.

All nine jobs are green on this branch.

The matrix earned its keep on the first run. It found two things: the
negotiation test wrongly demanded that capping the offer at protocol N make
the server negotiate exactly N, which is false for a server that does not
know N (Firebird 3 tops out at 15); and, from a compiler warning the matrix
surfaced, that the character set of a text column was never being read.
if SQLType in [SQL_TEXT,SQL_VARYING] compares against a Pascal set, which
holds 0..255, while the type codes are 452 and 448 — so the test matched
truncated values, never fired, and every text column fell back to character
set NONE. That was masked locally because the sample database is character
set NONE. Both are fixed, and a UTF8 column now correctly reports character
set 4.

Roadmap

doc/WireProtocol.md carries a twelve point roadmap saying what each missing
piece needs and what already exists to support it. The order it proposes:

# Milestone Needs
1 Run the existing test suite against this provider a provider switch in TTestApplication
2 Events op_connect_request, a second socket, a listener thread
3 Services the IServiceManager wrapper over exchanges that already exist
4 A Delphi transport TFBWireTransport over Winsock and Posix sockets
5 Array columns op_get_slice, op_put_slice, SDL generation
6 Statement timeouts and cancellation op_cancel, the P16 timeout field
7 Scrollable cursors op_fetch_scroll
8 The batch API the op_batch_* family
9 Inline blobs op_inline_blob
10 Firebird 6 protocol 20 schema search path, named arguments
11 Wire compression zlib either side of the cipher
12 Engine message text a firebird.msg reader or a generated table

The first is deliberately first: testsuite/ already contains twenty two
programs written against the interfaces rather than against a provider, and
they exercise far more of the API than WireTest does. A switch in
TTestApplication.GetFirebirdAPI would run all of them over the wire, and
that run is then the acceptance criterion for the events, services and array
milestones. I did not add the switch here because it changes shared test
code and seemed better raised than assumed.

Deliberately not planned, with reasons in the document: Legacy_Auth (it
produces no session key, so it cannot satisfy a server that requires
encryption), Win_SSPI, and protocols below 13.

Protocol details worth recording

These are the places where a plausible reading of the packet layouts
produces a client that does not work. Each cost a debugging cycle and each
is documented in doc/WireProtocol.md and commented where it is
implemented.

  • The op_crypt key type is the type the server advertised, in practice
    Symmetric — not the authentication plugin name. Sending the plugin name
    gets the connection closed with "unknown key".
  • Text columns must be described with blr_text2 / blr_varying2 naming
    the column's character set. With plain blr_text / blr_varying the
    engine assumes the connection character set and reinterprets the byte
    length as length div max bytes per character: a VARCHAR(37) read over a
    UTF8 connection silently becomes 9 characters and the fetch fails with a
    string truncation error.
  • XDR sends a boolean as one value byte padded to four, value first. As a
    big endian integer the value lands in the last byte and every boolean reads
    back false.
  • A batched op_fetch must be drained completely before anything else is
    sent; the server streams one op_fetch_response per row and ends the batch
    with a message count of zero. Rows are cached as they arrive.
  • ISC_QUAD puts the high word first, which is not the memory layout of
    an Int64 on a little endian machine.
  • SRP deviates from RFC 5054 in the engine: the exponent is reduced mod
    N, H(N) xor H(g) is a modular exponentiation, the salt is hashed as its
    hexadecimal text, and the session key is always SHA-1 even for Srp256.
  • arch_generic must be sent in the version entries: matching the
    server's own architecture makes it transmit messages as raw memory images
    instead of XDR.

Bugs found and fixed while bringing the provider up against a live server,
all of which are now covered by tests: parameters were all being sent as
null (the encoder reads the buffer's null indicator, so IsNull must read it
directly rather than gate on the column's nullable flag); a VARCHAR value
was overwriting its own two byte length prefix; CHAR values were not blank
padded, so a parameter never compared equal to a padded column; closing a
cursor whose transaction had ended was treated as an error when the server
has already closed it.

One behaviour worth flagging

An attachment that has read a table keeps an interest in it that outlives
the transaction, and Firebird 5 then refuses drop table on that same
attachment
with isc_no_meta_update. This is not a property of this
client: the stock fbclient provider fails in the same place with the same
code, and a second attachment issuing the drop blocks rather than
succeeding. WireTest therefore treats dropping its test table as best
effort and drops it at the start of the next run.

Error messages

firebird.msg is a client library resource, so it is not available. The
status vector is decoded and rebuilt into a standard ISC vector, so
EIBInterBaseError, GetIBErrorCode and CheckStatusVector behave as
usual, and the interpreted text the server supplies is used where it is
present. Where the stock provider says "unsuccessful metadata update" this
one says Firebird Error Code: 335544351. Closing that gap needs either a
firebird.msg reader or a generated table of the common codes.

Documentation

  • doc/WireProtocol.md — design, protocol details, SRP, encryption, message
    encoding, error reporting, testing and CI
  • client/wire/README.md — status and the measured results
  • README.md — a short section pointing at both

Commits

  1. the crypto, XDR and protocol engine
  2. the fbintf provider on top of it
  3. configurable protocol version capping, and the test for it
  4. the CI matrix and the documentation
  5. the character set and negotiation fixes the matrix found
  6. the CI verified server matrix recorded in the documentation
  7. the roadmap

Notes for review

The reference throughout was the Firebird server's own C++ implementation:
src/remote/protocol.h, protocol.cpp, interface.cpp and
src/auth/SecureRemotePassword. Nothing here is derived from another
client; the SRP test vectors were computed independently and cross checked
against a client/server pair rather than taken from an existing
implementation.

Worth a careful look: the message buffer layout in FBWireMessage
(alignment rules and the null bitmap), the lifetime handling in
FBWireStatement where the base class prepares lazily, and the encryption
changeover in TFBWireConnection.StartWireEncryption, which is asymmetric —
the receive cipher is installed before the op_crypt response is read
because the server encrypts from the moment it receives the request.

Changes outside client/wire, all additive:

  • fbintf.lpk and Makefile.fpcclient/wire added to the unit search
    path, and the thirteen units added to the package file list
  • fbintf.pas — the units added to the uses clause
  • README.md — a short section pointing at the new documentation
  • testsuite/WireTest.pas and .github/workflows/wire-protocol.yml — new
    files

The Delphi packages (fbintf.dpk, fbintf.dproj) are deliberately
untouched, because the transport is FPC only for now. If you would rather
the wire units were not compiled into the default package at all, dropping
them from fbintf.pas and fbintf.lpk leaves them available on the search
path for anyone who wants them and changes nothing for anyone who does not.

mariuz and others added 30 commits July 31, 2026 19:05
Adds the foundations of a Firebird client that speaks the remote (wire)
protocol directly over TCP, with no dependency on fbclient. Verified
against a Firebird 6.0 server: protocol 17 negotiation, Srp256
authentication, ChaCha64 wire encryption, DDL, parameterised DML,
multi-column fetches and blob round trips.

New units under client/wire:

  FBWireBigInt   arbitrary precision unsigned arithmetic (Knuth algorithm D
                 division, square and multiply modular exponentiation) as
                 needed by SRP
  FBWireCrypto   SHA-1, SHA-256, RC4 and ChaCha20, all verified against the
                 published test vectors
  FBWireSRP      the client half of SRP-6a as implemented by the Srp and
                 Srp256 plugins, including the engine's deviations from the
                 specification (see the unit header)
  FBWireStream   buffered TCP transport with per direction stream ciphers,
                 plus the XDR encoder/decoder
  FBWireConst    operation codes and protocol constants from protocol.h
  FBWireMessage  message buffer layout, BLR message descriptions and the
                 protocol 13+ null bitmap message encoding
  FBWireDescribe isc_info_sql describe response parser
  FBWireProtocol the connection handshake and the request/response
                 exchanges for attachments, transactions, DSQL, blobs,
                 information calls and services

Points worth recording, all of which cost a debugging cycle:

  - the wire encryption key type sent in op_crypt is the type the server
    advertised in its key clumplets ("Symmetric"), not the name of the
    authentication plugin
  - text fields must be described with blr_text2/blr_varying2 naming the
    column character set; with plain blr_text/blr_varying the engine
    assumes the connection character set and divides the byte length by
    the maximum character size, silently truncating the column
  - XDR sends a boolean as a one byte opaque value padded to four bytes,
    not as a big endian integer
  - a batched op_fetch must be drained completely before any other request
    is sent, so rows are cached as they arrive

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adapts the wire protocol engine to the fbintf object model, so existing
code written against IAttachment, ITransaction, IStatement and IResultSet
runs unchanged against a server with no client library installed:

    API := WireFirebirdAPI;
    DPB := API.AllocateDPB;
    DPB.Add(isc_dpb_user_name).AsString := 'SYSDBA';
    DPB.Add(isc_dpb_password).AsString := 'masterkey';
    Attachment := API.OpenDatabase('localhost:employee',DPB);

New units:

  FBWireClientAPI   IFirebirdAPI, the status object and the date and time
                    conversions (performed arithmetically, since there is
                    no isc_encode_* to call)
  FBWireAttachment  attach, create, drop, detach and the database info calls
  FBWireTransaction the transaction primitives
  FBWireStatement   prepare, execute, fetch and the SQLDA equivalent: the
                    column data lives in a flat message buffer that
                    TSQLDataItem points into, so the whole fbintf
                    conversion layer applies unchanged
  FBWireBlob        blob metadata, segmented read and write

Events, services, arrays, batches and scrollable cursors raise
ibxeNotSupported rather than failing obscurely; client/wire/README.md
records what is and is not implemented.

The plain text password is used only for the SRP exchange and is stripped
from the DPB before the attach, so it never reaches the network.

Bugs found and fixed while bringing the provider up against a live server:

  - the null indicator in the message buffer is what the encoder
    transmits, so IsNull must read it directly rather than gate on the
    column's nullable flag, and writing a value must clear it. Otherwise
    every parameter was sent as null.
  - a VARCHAR value must be written after its two byte length prefix; the
    previous code overwrote the first two characters with the length.
  - CHAR values are blank padded to their full width, otherwise a
    parameter never compares equal to a blank padded column.
  - closing a cursor whose transaction has ended is not an error: the
    server has already closed it (isc_dsql_cursor_close_err).
  - preparation is lazy in the base class, so anything needing the
    statement handle must prepare before checking it.

testsuite/WireTest.pas is a self contained regression test: the
cryptographic primitives against their published vectors, the SRP
exchange against a fixed reference exchange, the message layout, then the
protocol and the provider against a live server. 72 tests, all passing
against Firebird 6.0 over protocol 17 with Srp256 and ChaCha64.

client/wire is added to the FPC search paths and to fbintf.lpk. The
Delphi packages are left alone: the transport is written against the FPC
socket units and a Delphi transport is still to be written.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TFBWireConnection.MaxProtocol caps the highest version offered in
op_connect. It defaults to the highest the client implements, and lowering
it lets a caller force an older dialect of the protocol - which is how the
version matrix in client/wire/README.md was measured, and what the new
negotiation test in WireTest exercises.

Against Firebird 6.0 the client negotiates 14, 15, 16 and 17 and completes
an attach, ping and detach at each. Protocols 14 and 15 fall back to Arc4
because the ChaCha initialisation vector is only advertised from protocol
16. Protocol 13 is refused by this server with isc_miss_wirecrypt, since
it predates wire encryption and the server requires it; the version is
still offered for Firebird 3 servers and for servers configured with
WireCrypt = Enabled.

WireTest is now 77 tests, all passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion

Adds .github/workflows/wire-protocol.yml. The server job runs the wire
protocol test against Firebird 3, 4, 5 and 6 containers under both
WireCrypt = Enabled and WireCrypt = Required, seven combinations: Firebird 3
with Required is excluded because protocol 13, the only one it offers
without encryption support, cannot satisfy it. Each job starts the
container, creates a database with isql inside it, builds with the
distribution's FPC and runs the test over TCP; nothing Firebird related is
installed on the runner, which is the point of the client. The negotiated
protocol and the test totals go to the step summary so the matrix view
shows what each server version settled on.

A second job builds and runs the same binary on Linux and Windows with no
server, so a regression in the arithmetic, the hashes, the SRP exchange or
the message layout cannot be hidden by a server problem and the code keeps
compiling on Windows. It points the test at a port with nothing listening;
the live sections then report SKIP and the offline ones still run.

Measured locally while writing this, all with 0 failures: Firebird 6.0.0
with WireCrypt required (81 tests), Firebird 5.0.4 in a container under
both WireCrypt settings (81 tests each), and no server at all (36 tests,
live sections skipped). Firebird 3 and 4 are left to CI because their
images are published for amd64 only and this machine is arm64.

Documentation: doc/WireProtocol.md describes the design, the protocol
details that a plausible reading of the packet layouts gets wrong, the SRP
deviations, the encryption changeover, the message and BLR encoding, the
error reporting gap, the testing layers and the CI matrix.
client/wire/README.md gains the measured results, and the top level README
gains a short section pointing at both.

Also in this commit:

  - the test now honours a port in the connect string, which is what lets
    the offline job fail fast instead of waiting for a timeout
  - it takes an optional scratch database argument and, when given one,
    tests create and drop database
  - Check flushes, so a log is complete even when the next call raises
  - dropping the test table is best effort. An attachment that has read a
    table keeps an interest in it that outlives the transaction and
    Firebird 5 then refuses to drop it on that attachment; the stock
    fbclient provider fails in exactly the same place with exactly the same
    error, and a second attachment issuing the drop blocks instead, so this
    is the server's behaviour rather than the wire client's. The table is
    dropped at the start of the next run.
  - TCP_NODELAY is only set where the platform unit declares it, so the
    transport compiles on targets whose sockets unit omits it

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
protocol negotiation assertion

Two problems the CI matrix found on its first run.

The character set of a text column was never picked up. The test read

    if SQLType in [SQL_TEXT,SQL_VARYING] then

and a Pascal set holds 0..255, while the type codes are 452 and 448, so
the comparison was against truncated values and never matched. Every text
column therefore fell back to character set 0 and the BLR named NONE. The
compiler had been saying so all along:

    FBWireDescribe.pas(213,36) Warning: range check error while evaluating
    constants (452 must be between 0 and 255)

Rewritten as explicit comparisons. A UTF8 column now reports character set
4 where it previously reported 0, and the BLR names it, so the engine no
longer has to transliterate a column it was told was NONE. This was masked
in testing because the sample database is character set NONE and because
blank padding CHAR parameters made the ASCII comparisons work anyway; it
would have corrupted text in a database that actually uses a multi byte
character set.

The negotiation test asserted that capping the offer at protocol N makes
the server negotiate exactly N. That only holds if the server knows N.
Firebird 3 tops out at 15, so capping at 16 or 17 still yields 15 and the
test failed against it - while the client itself connected perfectly well
at protocol 15 with Srp256 and Arc4, and passed the other 79 checks. The
assertion now requires the negotiated version to be at least 13 and no
higher than the cap, which is the actual contract, and reports the version
it got.

Verified after the change: Firebird 6.0.0 and Firebird 5.0.4 against a
character set UTF8 database, 81 tests and 0 failures each, accented text
included.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every supported Firebird version has now been run: 3.0 negotiates protocol
15 with Srp256 and Arc4, and 4.0, 5.0 and 6.0 negotiate protocol 17 with
Srp256 and ChaCha64. All of them pass the full suite, 81 tests with 0
failures, under both WireCrypt = Enabled and WireCrypt = Required (Firebird
3 under Enabled only, since protocol 13 cannot satisfy Required).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces the short list of missing features with a roadmap that says what
each piece actually needs, in the order worth doing, and what already
exists to support it. Twelve milestones, from running the existing test
suite against this provider through events, services, a Delphi transport,
arrays, cancellation, scrollable cursors, batches, inline blobs, protocol
20, compression and engine message text.

Running the existing testsuite against the wire provider is first because
everything after it benefits: those twenty two programs are written against
the interfaces rather than a provider, and exercise far more of the API
than WireTest does. A switch in TTestApplication.GetFirebirdAPI is all the
plumbing it needs, and that run then becomes the acceptance criterion for
the events, services and array milestones.

Also records what is deliberately not planned and why: Legacy_Auth (no
session key, so it cannot satisfy a server requiring encryption), Win_SSPI,
and protocols below 13.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…st suite against the wire provider

Design and implementation plan for milestone 01 of the wire protocol
client roadmap (doc/WireProtocol.md). Documentation only; no code
changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Design and implementation plan for milestone 02 of the wire protocol
client roadmap (doc/WireProtocol.md). Documentation only; no code
changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Design and implementation plan for milestone 03 of the wire protocol
client roadmap (doc/WireProtocol.md). Documentation only; no code
changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Design and implementation plan for milestone 04 of the wire protocol
client roadmap (doc/WireProtocol.md). Documentation only; no code
changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Design and implementation plan for milestone 05 of the wire protocol
client roadmap (doc/WireProtocol.md). Documentation only; no code
changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…and cancellation

Design and implementation plan for milestone 06 of the wire protocol
client roadmap (doc/WireProtocol.md). Documentation only; no code
changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Design and implementation plan for milestone 07 of the wire protocol
client roadmap (doc/WireProtocol.md). Documentation only; no code
changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Design and implementation plan for milestone 08 of the wire protocol
client roadmap (doc/WireProtocol.md). Documentation only; no code
changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Design and implementation plan for milestone 09 of the wire protocol
client roadmap (doc/WireProtocol.md). Documentation only; no code
changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Design and implementation plan for milestone 10 of the wire protocol
client roadmap (doc/WireProtocol.md). Documentation only; no code
changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Design and implementation plan for milestone 11 of the wire protocol
client roadmap (doc/WireProtocol.md). Documentation only; no code
changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Design and implementation plan for milestone 12 of the wire protocol
client roadmap (doc/WireProtocol.md). Documentation only; no code
changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TFBWireServiceManager (client/wire/FBWireServices.pas) surfaces the
op_service_attach/detach/start/info exchanges, which FBWireProtocol
already implemented, as IServiceManager. A service session is its own
connection: ConnectTo performs the SRP authentication and starts wire
encryption when negotiated exactly as for a database attach, and the
password is consumed by SRP and stripped from the SPB, with the proof
travelling as isc_spb_specific_auth_data in the op_accept_data flow -
the same treatment TFBWireAttachment gives the DPB. HasServiceAPI now
answers true and AllocateSPB returns the shared TSPB.

Detaching and reattaching a service manager was the first code path to
reconnect a TFBWireConnection, and it exposed a latent transport bug:
TFBWireTransport.Disconnect left the session ciphers installed, so the
reconnect's cleartext handshake was sent through the dead session's
cipher and the server dropped the connection. Disconnect now discards
both ciphers.

The plan's one protocol nuance turned out to be a non-issue: the
op_connect packet's p_cnct_operation field is vestigial - the stock
client sends op_attach for service connections too - so ConnectTo is
unchanged. Verified against Firebird 5 (WireCrypt Required, ChaCha64)
and Firebird 6: WireTest's new services section covers the version
query, detach and reattach, and header page statistics, 88 tests with
0 failures including create and drop database.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every row of the server matrix - Firebird 3, 4, 5 and 6, WireCrypt
Enabled and Required - now measures 88 tests with 0 failures, including
the new services section. Firebird 3 exercises it over protocol 15 with
Arc4; everything else negotiates 17 with ChaCha64. The offline count is
unchanged at 36: the services tests need a live server.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
testsuite -a wire (--api wire) makes TTestApplication obtain the API
from WireFirebirdAPI instead of IB.FirebirdAPI. All twenty two test
programs now run over the wire provider and runtest.sh compares the
output against the new FBWirereference.log, normalising the run
dependent values (transaction ids, page counters, journal timestamps,
the database id line) on both sides of the diff. A CI job runs the
suite against a Firebird 6 container, restoring the employee example
database from the checked in testsuite/employee.gbk, and fails on any
difference from the reference log.

Harness changes: the two GetFBLibrary dereferences are guarded (the
wire provider has no client library), and IAttachment gains
HasArraySupport and HasEventSupport alongside the existing capability
checks, so tests 2, 7, 8, 10, 13, 17, 18, 20 and 22 skip unsupported
features with a fixed message instead of erroring.

The suite found seven wire provider defects on its first run, all in
paths WireTest never exercised:

- SRP account upper casing used AnsiUpperCase, which under fpwidestring
  (loaded by the suite) returns the string with a trailing #0 included
  in its length (fpc bug 39746), poisoning the proof hashes; every
  login from the suite binary failed. ASCII UpperCase is used instead,
  matching the engine.
- ExecuteStatement2 was called with its statement and transaction
  handles swapped, killing the connection on any execute procedure or
  insert ... returning.
- Execute returned nil for update/insert ... returning: Firebird 5+
  describe those as selects (cursor plus one row), older servers as
  SQLExecProcedure (op_execute2 singleton); both paths are implemented.
- The bind after prepare overwrote named parameter names with the
  describe response's empty names.
- Parameter metadata was immutable; type coercion (AsInteger on a
  SMALLINT, strings to blobs) now updates the message format and
  relays out the buffer - the client owns the BLR it sends.
- DECFLOAT conversions inherited the base class codec, which has no
  implementation and returned garbage; the provider now carries its own
  IEEE 754 densely packed decimal codec, verified against the server in
  both directions.
- GetBlobMetaData never looked the column up, so blob subtypes were
  wrong; it now runs the same system table query as the 3.0 provider.

Also: create database from a SQL statement extracts the file spec
locally (the stock providers delegate that preparse to fbclient), a nil
DPB no longer crashes, the DPB sent with the attach is a verbatim
clumplet copy minus the password items rather than a re-encoded copy,
the transport discards its session ciphers on disconnect so a
connection object can reconnect, and WireIBError takes ownership of a
non-wire exception before re-raising it from the caller's handler.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Milestone 2 builds on milestone 1: the suite's Test 10 is the
acceptance criterion for events, and the wire fixes that milestone
delivered (cipher reset on reconnect, the capability checks) are
needed here.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TFBWireEvents (client/wire/FBWireEvents.pas) implements IEvents over
the auxiliary connection: op_connect_request on the main connection
returns the port of the server's event channel - only the port, because
the address in the response is the server's own view of itself, which
behind NAT is not reachable, so the client reuses the host it already
connected to, as the stock client does. The auxiliary connection needs
no handshake, authentication or encryption: the server associates it
with the session by the accept, and it only ever delivers op_event
packets, drained by a listener thread.

One TFBWireEventManager per attachment, created on the first
GetEventHandler call, owns the auxiliary transport and the listener and
serves all the attachment's IEvents instances, dispatching by event id.
All of the event block machinery - the EPB that op_que_events carries,
the count diffing and the callback dispatch - is inherited from
TFBEvents. Both asynchronous and synchronous waits are supported; the
handler is called from the listener thread, as the 2.5 provider calls
its handler from an AST thread.

The one discovery worth recording: each op_que_events must carry a
fresh event id. An interest is one shot, and re-arming under the id of
an interest that has already been delivered is accepted by the server
but not honoured immediately - counts accumulated while nobody waited
were only delivered when the next event fired, one delivery late. The
stock client increments its id on every queue, and doing the same made
Test 10's output match the stock provider reference line for line.

The transport gains an Abort method (socket shutdown without close) so
that the listener's blocking read can be released from another thread
when the attachment disconnects. Test 10 gains a settling sleep before
one output line so that the report order is deterministic; WireTest
gains an events section (delivery, deferred counts, cancellation) and
now measures 89 tests, and the wire reference log is regenerated with
Test 10's full output - byte identical across three consecutive suite
runs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The first CI run of the suite job showed that the reference log is
architecture flavoured: FPC's float to text rendering differs in the
last digit between x86_64 and ARM, and the implementation id, security
class naming and page counts are environment values. The reference is
now the normalised output of the CI job itself - the environment that
enforces it - captured from the wire-suite-testout artifact, and the
documentation records how to regenerate it after an intended output
change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The first CI run of the events milestone hung: the server opens a
random port for the auxiliary event connection, and a container that
only publishes 3050 silently blackholes it - the client blocked in
connect for minutes and the server logged a select timeout on its side.
The containers now pin the port with RemoteAuxPort=3051 and publish it,
which is the standard operational answer for events through firewalls
and NAT, and the documentation says so. WireTest treats an unreachable
auxiliary port as a SKIP with a pointer to RemoteAuxPort instead of
dying with an unhandled exception.

The same run showed more run dependent values in the suite output than
the first normalisation pass caught: per table operation counters,
server memory, the database creation date (the employee database is
restored afresh in CI, so its creation date is the run date), the total
page count, the server version string (the 6-snapshot container updates
over time), the implementation codes and the security class name.
runtest.sh now normalises all of them on both sides of the comparison
and the reference log is stored in that form.

The reference log itself is the milestone 1 CI (x86_64) output with
Test 10's section replaced by the events output, which contains no
architecture sensitive text. Test 10 gains bounded polls with a settling
CheckSynchronize where it previously relied on fixed sleeps, so that
the report line order no longer depends on the load of the test host -
six consecutive runs now reproduce the reference section exactly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The first suite job rerun showed more values that change between CI
runs than the first normalisation pass caught: per table operation
counters, server memory, the total page count, the employee database
creation date (it is restored afresh in CI, so its creation date is
the run date), the server version string (the 6-snapshot container
updates over time), the implementation codes and the security class
name. runtest.sh now normalises all of them on both sides of the
comparison and the reference log is stored in that form.

Also taken from the events milestone so that the files stay identical
across the two branches: the CI containers pin the auxiliary event
port with RemoteAuxPort=3051 and publish it, and Test 10 polls with a
settling CheckSynchronize where it previously relied on fixed sleeps,
so its report line order does not depend on the load of the test host.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mariuz and others added 29 commits August 1, 2026 15:30
The wire-suite-testout artifact of the batch CI run, unchanged. Three
kinds of difference from the previous reference. The batch skip lines
of Tests 19 and 20 are replaced by the tests' real output - the
milestone showing up in the log. The parameter metadata lines change
with the default text type: SQL_TEXT becomes SQL_VARYING with the
described subtype and size, matching what the fbclient providers
report for the same statements. And the parameter value echoes lose
their blank padding - Test 12's INCLEAR hex dumps and CHAR echoes now
match the fbclient reference logs byte for byte, where the old wire
output had diverged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The connection now offers protocol 19. At 19 op_execute/op_execute2
carry the inline blob size limit the client accepts - taken from the
existing IAttachment.GetInlineBlobLimit (default 8KB, zero opts out) -
and the server pushes each qualifying blob as an op_inline_blob packet
ahead of the row that references it: transaction handle, blob id, the
standard blob info response, and the whole blob as one segmented
stream, the same two byte length prefixed form op_get_segment returns.

ReadOperation intercepts the packets at the same point that already
skips op_dummy and op_response_piggyback, and hands them to the
attachment through an OnInlineBlob callback, keeping the protocol unit
free of provider types. The attachment caches them keyed by transaction
handle and blob id, bounded at 16MB - beyond the cap pushes are dropped
and the blob opens the classic way, a round trip but never an error.
Entries are consumed on open, mirroring the server sending each id
once and the engine consuming batch blob registrations the same way,
and a transaction's entries are discarded when it ends, including the
retaining forms. TFBWireBlob consults the cache before op_open_blob2:
on a hit the open, every Read and GetInfo happen with no wire traffic
at all - the pushed info buffer carries exactly the four items every
fbintf GetInfo caller asks for and is served verbatim.

The transport gains a PacketsSent counter (one per socket write, i.e.
per round trip) which the new WireTest inline blob section uses to
prove the point: a small blob read after the fetch adds zero packets,
an over-limit blob and a zero limit both fall back to the classic
exchanges, and all three read back intact - 158 tests. The feature is
transparent by design and measurement: the full suite diff against the
previous reference log is the negotiated version string, 18 to 19, and
nothing else, so the reference is regenerated from the CI artifact in a
follow up commit. The CI matrix now expects protocol 19 on Firebird 5
and 6.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The wire-suite-testout artifact of the inline blobs CI run, unchanged.
The whole difference from the previous reference is the negotiated
version in the getFBVersion output moving from 18 to 19 in its six
occurrences: inline blobs change no other observable output, which is
the milestone's transparency claim, now measured in the CI environment
with the suite's small blobs actually travelling inline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The offer now goes to protocol 20, which Firebird 6 accepts. The field
by field audit of protocol.cpp found exactly one unconditional packet
change: p_sqlst_flags, a flags word at the end of op_prepare_statement
and op_exec_immediate - they share the XDR block - written as zero
since fbintf exposes no special prepare flags. Everything else is
opt-in.

The schema search path travels as an ordinary DPB item:
isc_dpb_search_path joins the constants with the rest of the Firebird 6
DPB tags (97..107) and the DPB name table grows to match, so
DPB.Add(isc_dpb_search_path).AsString works like any other item. On
protocol 20 connections the describe item list gains
isc_info_sql_relation_schema, parsed into the column format's new
SchemaName - ParsePrepareResponse already skipped unknown items with
their length, so the defensive property the design doc asked to verify
was already in place. fbintf's IColumnMetaData has no schema member and
the 3.0 provider surfaces none, so the name is stored rather than
surfaced; tests reach it through TFBWireStatement.ColumnSchemaName and
an interface member can follow when fbintf grows one. Named arguments
turned out to be a SQL level feature, not a describe change - nothing
to do on the wire.

WireTest gains a schema section - two schemas with a same named table,
an attachment whose search path picks the second, a describe assertion
on the per column schema name, and a qualified name bypassing the
path - and the negotiation ladder extends to 20: now 164 tests. The
suite output is byte identical apart from the negotiated version
string, so the reference log is regenerated from the CI artifact in a
follow up commit, and the CI matrix expects protocol 20 on Firebird 6.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The wire-suite-testout artifact of the protocol 20 CI run, unchanged.
As with the inline blobs milestone, the whole difference from the
previous reference is the negotiated version in the getFBVersion
output, 19 to 20 in its six occurrences: the schema aware describe and
the prepare flags word change nothing the suite can observe.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Requested per attachment with an isc_dpb_config item of
WireCompression=true - the same knob the stock client reads, so
application code stays provider portable - and effective when the
server also enables it: pflag_compress rides on each offered protocol
entry's max type field, and the accept's echo is captured before the
ptype_mask masking that used to discard it unseen.

Compression is a property of the byte stream, not of packets: one zlib
stream per direction for the life of the session, using FPC's pure
Pascal paszlib so the no-external-dependencies property survives. The
streams start immediately after the accept packet has been read in
full - the packet itself travels clear, and an op_accept_data payload
is part of it - so the remaining handshake, op_crypt included, is
compressed. Each packet flush deflates with Z_SYNC_FLUSH: a
request/response protocol deadlocks if complete packets do not reach
the peer promptly.

The pipeline keeps the cipher against the socket - deflate then
encrypt on send, decrypt then inflate on receive - so the op_crypt
changeover logic is untouched. op_cancel's out of band packet routes
through the same deflate state under the existing send lock: the peer
inflates one continuous stream and raw bytes must never bypass it.
The receive side keeps ReadBytes and the plaintext buffer semantics
untouched by staging the not yet inflated stream in a second buffer
inside FillRecvBuffer, and HasBufferedData counts those pending bytes,
keeping the cipher changeover guard and event polling honest. With
compression off the code path is the previous one verbatim: the suite
reference log needed no regeneration for the first time since the
suite milestone.

WireTest gains a compression section - a second attachment negotiates
it, then round trips a bulk query and a blob through the compressed
(and encrypted) stream - now 167 tests, all passing against the local
Firebird 6, which accepts compression out of the box. The CI matrix
gains a dedicated WireCompression = true row on Firebird 5, made a new
combination rather than an extension by naming the wirecompression
axis explicitly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
generate_messages.py reads the message headers of a pinned Firebird
tree - src/include/firebird/impl/msg, the modern home of what
messages2.sql used to hold - and emits the committed FBWireMessages
unit: 1682 messages for the facilities a server round trip can produce
(JRD, DSQL, DYN, SQLERR, SQLWARN), each with its fb_interpret format
string and its SQLCODE, behind a binary search. About 200KB of binary,
and the provider still depends on nothing but itself.

FormatWireStatus renders a decoded status vector the way fb_interpret
does - each isc_arg_gds item's format string with @1..@n arguments
substituted, interpreted items verbatim, successive items joined with
a "-" continuation line - and becomes the one formatter behind both
EIBInterBaseError messages and the protocol level exception, replacing
the two numeric fallbacks. SetFromWireStatus now keeps the decoded
vector with its argument structure intact instead of flattening the
strings away.

The SQLERR facility turned out to carry more than the plan knew: the
"At line @1, column @2" companion of every DSQL error, and the per
SQLCODE interpretation texts isc_sql_interprete looks up at
1000+sqlcode. With those, the wire status object also answers
Getsqlcode (the gds__sqlcode rules: an isc_sqlerr item's number
argument wins outright, else the first item's mapping decides) and
GetSQLMessage client side, so the full fbclient error shape - SQLCODE
line, SQL message, engine code and message text - now appears over the
wire. The base class's SQLCODE methods became virtual to allow the
override.

Parity testing against the 3.0 provider on the same server exposed two
behavioural gaps beyond message text, both closed: prepare errors now
append " When Executing: <sql>" as the other providers do, and the
stale reference checks were missing entirely - a prepared statement
used across a transaction restart now raises ibxeInterfaceOutofDate
exactly as the 3.0 provider does. Test 16's output is now identical to
fbclient apart from the invalid-server-name section the test skips for
the wire provider by design.

WireTest gains offline assertions - the table lookup, a parameterised
format, and a two item vector formatted byte for byte - now 171 tests.
The suite reference log is regenerated from the CI artifact in a
follow up commit: its numeric Engine Code fallbacks give way to the
real text everywhere.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The wire-suite-testout artifact of the message text CI run, unchanged.
Every difference from the previous reference is an error message
upgrade: numeric Engine Code fallbacks give way to fb_interpret text,
exception reports gain their message and stack lines, and the SQLCODE
preamble appears where the fbclient providers show one - the wire
provider's error output now reads as theirs does.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Roadmap 01: full test suite over the wire provider (design + implementation)
Roadmap 02: events for the wire protocol provider (design + implementation)
Roadmap 05: Array columns for the wire protocol provider (design + implementation)
Roadmap 06: Statement timeouts and cancellation (design + implementation)
Roadmap 07: Scrollable cursors for the wire protocol provider (design + implementation)
Roadmap 08: The batch API for the wire protocol provider (design + implementation)
Roadmap 09: Inline blobs for the wire protocol provider (design + implementation)
Roadmap 10: Firebird 6 protocol 20 for the wire provider (design + implementation)
Roadmap 11: Wire compression for the wire protocol provider (design + implementation)
Roadmap 12: Engine message text for the wire provider (design + implementation)
…services-land

# Conflicts:
#	client/wire/FBWireClientAPI.pas
#	client/wire/FBWireStream.pas
#	client/wire/README.md
#	doc/WireProtocol.md
#	fbintf.lpk
#	fbintf.pas
#	testsuite/WireTest.pas
…chain

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Roadmap 03: Services for the wire protocol provider (design + implementation)
TFBWireTransport now branches per compiler inside the same four
methods, per doc/roadmap/04-delphi-transport.md: Winapi.Winsock2 on
Windows (one time WSAStartup behind a unit variable) and the Posix.*
units elsewhere; getaddrinfo preferring an IPv4 result to match the FPC
resolver, TCP_NODELAY, SO_RCVTIMEO/SO_SNDTIMEO from aTimeout,
recv/send, and a graceful shutdown in Disconnect. The wire units and
FBSDL are listed in fbintf.dpk and fbintf.dproj.

Written without a Delphi toolchain: nothing here has been compiled by
Delphi, and the docs say so. Wire compression stays FPC only (paszlib);
EnableCompression raises a clear error under Delphi. The FPC branch is
untouched and WireTest still reports 173 tests, 0 failures locally.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Roadmap 04 design: A Delphi transport
The full suite job now exercises the services API for real instead of
reporting it unsupported, so the reference log gains that output. Taken
from the CI x86_64 wire-suite-testout artifact of the landing run, as
always.

The services output includes the raw server version string, which the
comparison did not normalise - it would have broken on the next
container update - so runtest.sh gains a Server Version rule alongside
the existing Implementation one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mariuz

mariuz commented Aug 1, 2026

Copy link
Copy Markdown
Author

Update: the roadmap has been implemented — this branch now carries the full feature set

Since this PR was opened, the twelve milestones from the roadmap in doc/WireProtocol.md have been implemented and merged into this branch. Each milestone was developed as its own stacked PR in the fork, design document first, implementation after, so every feature has a written plan, a record of what the implementation actually found, and its own reviewable diff:

# Milestone Fork PR
1 Full fbintf test suite over the wire (testsuite -a wire) mariuz#2
2 Events (auxiliary connection + listener thread) mariuz#3
3 Services API (IServiceManager) mariuz#4
4 Delphi transport (written, unverified — see below) mariuz#5
5 Array columns (op_get_slice/op_put_slice, SDL) mariuz#6
6 Statement timeouts and cancellation (op_cancel) mariuz#7
7 Scrollable cursors (protocol 18) mariuz#8
8 Batch API (protocol 16 op_batch_*) mariuz#9
9 Inline blobs (protocol 19) mariuz#10
10 Firebird 6 / protocol 20 (schemas, search path) mariuz#11
11 zlib wire compression (WireCompression=true) mariuz#12
12 Engine error message text without firebird.msg mariuz#13

What that means for the provider

The gaps listed under "not implemented yet" in the original description are closed. The provider now negotiates protocols 13 through 20 and implements events, services, arrays, statement timeouts and cancellation, scrollable cursors, the batch API, inline blobs, schema support on Firebird 6, optional zlib wire compression beneath the wire encryption, and fb_interpret-style error message text from a generated message table (1682 messages) — so status vectors read the same as with fbclient, firebird.msg not required.

Testing

  • The entire existing fbintf test suite (all 22 programs) runs over the wire provider via testsuite -a wire and is compared against a committed reference log in CI. Getting there found and fixed real wire bugs (among them an SRP breaker from AnsiUpperCase on trailing #0, swapped execute2 handles, and a missing DECFLOAT DPD codec), which is the strongest compatibility evidence this provider has: the same programs, the same output, fbclient absent.
  • The provider's own WireTest grew from 81 to 178 checks.
  • The CI matrix runs Firebird 3, 4, 5 and 6 × WireCrypt Enabled/Required, plus a dedicated wire-compression row, plus offline builds on Linux and Windows. All green on the current head.

Documentation

  • doc/WireProtocol.md — architecture, the handshake and per-protocol-version packet field differences (13→20) as implemented, and the roadmap with each milestone's outcome.
  • doc/roadmap/*.md — one design document per milestone, updated after implementation with what was actually found (these record the protocol subtleties: batch clumplet widths, inline blob cache semantics, compression stream placement relative to the cipher, and so on).
  • client/wire/README.md — user-facing summary and the tested-server matrix.

Two things reviewers should know

  1. The Delphi transport (milestone 4) is written but unverified. TFBWireTransport has Delphi branches (Winapi.WinSock2 on Windows, the Posix.* units elsewhere) and the wire units are listed in fbintf.dpk/fbintf.dproj, but no Delphi toolchain has compiled them — CI is FPC-only. The docs say so explicitly; expect a first-compile pass. FPC behaviour is untouched.
  2. Milestone 6 adds small public interface members across all providers: IAttachment.CancelOperation and IStatement.SetStatementTimeout/GetStatementTimeout, implemented for the 2.5, 3.0 and wire providers. That and a few virtuals in FBClientAPI/FBAttachment are the only touches to shared code beyond the build file listings; everything else lives under client/wire.

As before: offered for consideration, and if the direction is welcome but any of the packaging or the shared-interface additions are not, I am happy to rework them.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant