# DataReel 7: AGENTS.md and AI guides in one file

Generated by docs/bin/make_ai_docs.py from AGENTS.md and docs/ai/*.md.

========================================================================
FILE: AGENTS.md
========================================================================

# AGENTS.md: DataReel 7 for AI coding assistants

DataReel 7 is a C++17 library for two jobs the C++ standard library does
not cover:

1. **An embedded object database**: block files with free space reuse
   and CRCs (`gxDatabase`), disk B-tree indexes (`gxBtree`), a data file
   plus its indexes opened together (`POD`), persistent objects with
   variable length members (`InfoHog`) and simple relational tables
   (`gxRDBMS`).
2. **TLS communications**: TLS 1.2/1.3 client and server connections
   (`gxsTLSConnection`, `gxsTLSServer`) and HTTPS, SMTP, POP3, FTPS and
   database-block (`gxStream`) protocols on top of them.

For anything else (strings, containers, threads, files, time, JSON,
SQL) use the C++ standard library or a dedicated library. DataReel 7
removed its old string, list, thread and file classes.

The task guides are in `docs/ai/`; `llms-full.txt` has all of them in
one file. Every sample program in them is built and run by
`tests/run_tests.sh`.

## Rules when writing code that uses DataReel

- Include the class header (`gxstls.h`, `gxshttpc.h`, `gxdbase.h`,
  `pod.h`, `infohog.h`, `gxrdbms.h`, ...). Everything is in
  `namespace datareel`; `using namespace datareel;` is added by the
  headers unless `DATAREEL_NO_GLOBAL_NAMES` is defined.
- Link with CMake: `find_package(DataReel 7 REQUIRED)` and
  `target_link_libraries(app PRIVATE DataReel::datareel)`. The target
  carries required compile definitions; do not compile DataReel
  headers without them (you get "You must define a target platform").
- Return values differ by area. Check each one:
  - Network classes (`Connect`, `Listen`, `Accept`, `Login`, `Get`,
    ...) return `0` on success and `-1` on failure, with the message
    in `Error()`. `ReadLine()` returns `bool`.
  - `gxDatabase`, `gxBtree::Create/Open`, `POD::Open` and
    `gxRDBMS::CreateTable` return `gxDatabaseError`;
    `gxDBASE_NO_ERROR` (0) is success. Get text with
    `DatabaseExceptionMessage()` or `gxDatabaseExceptionMessage(err)`.
  - `gxBtree::Insert/Find/Delete/FindFirst/FindNext`, `InfoHog`
    `WriteObject/ReadObject/FindObject/DeleteObject` and `gxRDBMS`
    `AddRecord/FindRecord/ChangeRecord` return non-zero (true) on
    success.
  - `gxStream` functions return `gxSOCKET_NO_ERROR` (0) on success.
- TLS: certificates are verified against the system store and the
  host name by default. For a private CA call `SetCAFile("ca.pem")`.
  Only use `SetVerifyPeer(false)` in tests. There is no plain text
  HTTP, SMTP, POP3 or FTP; do not try to add it. Only telnet and ping
  are plain text, for diagnostics.
- Records and keys are written as raw bytes. Use fixed-size members:
  `char name[N]`, `gxINT32`, `gxUINT32`, `gxINT64`, `gxFLOAT64`,
  `CDate`, `FAU`. Never store pointers, `std::string`, `std::vector`
  or classes with virtual functions in a record or key. `memset` a
  record to zero before filling it so padding bytes are not random.
- Key classes derive from `DatabaseKeyB`, pass the key data address to
  the base constructor, and implement `KeySize()` and
  `CompareKey()` (the CMake build defines `__USE_SINGLE_COMPARE__`).
  Also define `operator==` and `operator>` so the class works without
  that macro.
- File layout depends on two build settings: `DATAREEL_64BIT_DATABASE`
  and `DATAREEL_NATIVE_TYPES`. A program must use the settings the
  files were created with. Linux and Windows native builds write
  different layouts (64-bit Linux `long` is 8 bytes); use
  `DATAREEL_NATIVE_TYPES=OFF` on both to share files.
- `gxDatabase` objects are not copyable and one object must not be
  used from several threads at once without your own mutex.
- Object addresses (`FAU_t`) returned by `Alloc()` stay valid until
  the object is deleted; store them in your index keys.

## Rules when changing DataReel itself

- C++17 minimum (RHEL 8 has GCC 8). Keep the `gx` class names and the
  `datareel` namespace.
- Never change the on-disk format: block and file headers, B-tree
  nodes, `InfoHog` and `gxRDBMS` record layout, CRC tables, or
  `gxrdDatabaseRecord::CreateKey()` (index keys are CRCs of field
  bytes). 7.0 writes byte-identical files to 6.2; keep it that way.
- Treat every length and count read from a file or the network as
  untrusted: check it against the buffer or a limit before use.
- New network code uses `gxsTLSConnection`; do not add plain text
  protocols.
- Build and test before committing:

```sh
cmake -S . -B cmake-build -DCMAKE_BUILD_TYPE=Release
cmake --build cmake-build -j
tests/run_tests.sh cmake-build                    # 42 checks
CMAKE_OPTS="-DDATAREEL_64BIT_DATABASE=ON -DDATAREEL_NATIVE_TYPES=OFF" \
  tests/run_tests.sh /tmp/dr-b64                   # the other layouts
```

- Windows: `cmake -S . -B cmake-build -G "Visual Studio 17 2022" -A x64`
  then `cmake --build cmake-build --config Release`. Schannel is the
  default TLS backend; `-DDATAREEL_SSL=OPENSSL` selects OpenSSL.
- If you change a sample in `docs/ai/samples`, run
  `python3 docs/bin/make_ai_docs.py` to update the guides and
  `llms-full.txt`.

## Map

| Path | What |
|------|------|
| `include/`, `src/` | Library headers and sources |
| `examples/database/` | gxDatabase, gxBtree, POD, InfoHog, gxRDBMS programs |
| `examples/tls/` | TLS echo, HTTPS, SMTP, POP3, FTPS, gxStream programs |
| `examples/general/` | gxConfig, LogFile, ThreadPool |
| `docs/ai/` | Task guides and their sample programs |
| `docs/classes/` | HTML reference for each class |
| `tests/` | `run_tests.sh`, mock mail/FTPS servers, damaged file test |
| `utils/` | `db_debug`, `ix_debug` file analysis and repair tools |

========================================================================
FILE: docs/ai/tls-comms.md
========================================================================

# TLS communications with DataReel 7

DataReel 7 networking is TLS only (TLS 1.2 and 1.3), over IPv4 and
IPv6, on Linux (OpenSSL 1.1.1 or later) and Windows (Schannel or
OpenSSL 3.x). Telnet and ping are the only plain text protocols.

| Task | Class | Header |
|------|-------|--------|
| Any TLS client or server connection | `gxsTLSConnection` | `gxstls.h` |
| Accept TLS clients | `gxsTLSServer` | `gxstls.h` |
| HTTPS requests | `gxsHTTPClient`, `gxsHTTPMessage` | `gxshttpc.h` |
| HTTPS server messages | `gxsHTTPReadRequest`, `gxsHTTPWriteResponse` | `gxshttp.h` |
| Send mail | `gxsSMTPClient` | `gxsmtp.h` |
| Read mail | `gxsPOP3Client` | `gxspop3.h` |
| File transfer | `gxsFTPClient` (FTPS) | `gxsftp.h` |
| Database blocks client/server | `gxStream` | `gxstream.h` |
| Thread pool for servers | `ThreadPool` | `thrpool.h` |
| URL parsing, percent decoding | `gxsURL` | `gxsurl.h` |
| Base 64 | `gxsBase64Encode`, `gxsBase64Decode` | `gxs_b64.h` |
| Raw TCP/UDP socket | `gxSocket` | `gxsocket.h` |

## Conventions

- Functions return `0` on success and `-1` on failure; the reason is in
  `Error()`. `ReadLine()` and `ReadAll()` return `bool`.
- Set options (`SetTimeout`, `SetCAFile`, `SetVerifyPeer`,
  `SetClientCert`) before `Connect()`.
- The server certificate is checked against the system trust store
  (on Windows, the Windows certificate store) and the host name or IP
  address you connected to. Add a private CA with `SetCAFile()`.
  `SetVerifyPeer(false)` turns checking off: tests only.
- `Connect(host, port)` resolves the name and tries every address,
  IPv6 and IPv4. Numeric addresses such as `::1` and `127.0.0.1` work.
- `implicit_tls = true` (the default) starts TLS immediately. Pass
  `false` to connect in plain text, exchange the protocol's STARTTLS
  command yourself, then call `StartTLS()`.
- Timeouts are in seconds and apply to the connect and to each read.
- A `gxsTLSConnection` is not copyable. Servers create one per client,
  usually in a `std::shared_ptr` handed to a thread pool task.
- Socket library start-up on Windows (WSAStartup) is automatic.

## TLS client

```cpp
// docs/ai/samples/tls_client.cpp
// DataReel 7 sample: TLS client that sends lines and prints the replies.
// Works with examples/tls/tls_echo_server.
// Usage: tls_client host port [ca.pem]
#include <cstdlib>
#include <iostream>
#include <string>
#include "gxstls.h"

using namespace datareel;

int main(int argc, char **argv)
{
  if(argc < 3) {
    std::cerr << "Usage: tls_client host port [ca.pem]\n";
    return 1;
  }
  gxsTLSConnection conn;
  conn.SetTimeout(30);                 // Seconds, for connect and each read
  if(argc > 3) conn.SetCAFile(argv[3]); // Trust a private CA as well
  // Certificates are verified against the system store and the host
  // name by default. Connect() returns 0 on success.
  if(conn.Connect(argv[1], std::atoi(argv[2])) != 0) {
    std::cerr << "Connect failed: " << conn.Error() << "\n";
    return 1;
  }
  for(const char *msg : { "hello", "DataReel 7" }) {
    std::string reply;
    if(conn.SendLine(msg) < 0 || !conn.ReadLine(reply)) {
      std::cerr << "I/O failed: " << conn.Error() << "\n";
      return 1;
    }
    std::cout << "Sent \"" << msg << "\", got \"" << reply << "\"\n";
  }
  conn.SendLine("quit");
  conn.Close();
  return 0;
}
```

Other data functions: `Send(buf, n)` writes all bytes,
`ReadSome(buf, n)` returns what is available (up to n),
`Read(str, n)` reads exactly n bytes, `ReadAll(str)` reads until the
peer closes. For a client certificate call
`SetClientCert("client.pem", "client.key")` before `Connect()`.

## TLS server

`gxsTLSServer::Listen()` loads the certificate chain and key and
listens on one dual-stack socket. `Accept()` blocks for the next
client and completes its TLS handshake.

```cpp
// docs/ai/samples/tls_server.cpp
// DataReel 7 sample: multi-threaded TLS line server. Replies to each
// line with the line in upper case. "quit" ends a session and "stop"
// stops the server.
// Usage: tls_server port cert.pem key.pem
#include <atomic>
#include <cctype>
#include <cstdlib>
#include <iostream>
#include <memory>
#include <string>
#include "gxstls.h"
#include "thrpool.h"

using namespace datareel;

static std::atomic<bool> stopping(false);

static void session(std::shared_ptr<gxsTLSConnection> conn, int port)
{
  std::string line;
  while(conn->ReadLine(line)) { // false on close, timeout or error
    if(line == "quit") break;
    if(line == "stop") {
      stopping = true;
      gxSocket wake; // Wake the blocked Accept() so main() can exit
      wake.Connect("localhost", port, 5);
      break;
    }
    for(char &c : line) c = (char)std::toupper((unsigned char)c);
    if(conn->SendLine(line) < 0) break;
  }
  conn->Close();
}

int main(int argc, char **argv)
{
  if(argc < 4) {
    std::cerr << "Usage: tls_server port cert.pem key.pem\n";
    return 1;
  }
  gxsTLSServer server;
  // Listens on IPv4 and IPv6. Returns 0 on success.
  if(server.Listen(std::atoi(argv[1]), argv[2], argv[3]) != 0) {
    std::cerr << "Listen failed: " << server.Error() << "\n";
    return 1;
  }
  std::cout << "Listening on port " << server.Port() << std::endl;
  ThreadPool pool(8);
  while(!stopping) {
    auto conn = std::make_shared<gxsTLSConnection>();
    conn->SetTimeout(120); // Idle sessions are dropped after two minutes
    // Accept() completes the TLS handshake; a failed handshake only
    // affects that client.
    if(server.Accept(*conn) != 0) continue;
    if(stopping) break;
    pool.Submit(session, conn, server.Port());
  }
  pool.Wait();
  return 0;
}
```

Make a test certificate:

```sh
openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem \
  -days 30 -subj "/CN=localhost" \
  -addext "subjectAltName=DNS:localhost,IP:127.0.0.1,IP:::1"
# Clients then use SetCAFile("cert.pem")
```

## HTTPS client

`gxsHTTPClient` opens a new TLS connection per request, follows
redirects, decodes chunked bodies and refuses `http://` URLs. It does
not do HTTP/2, cookies or compression.

```cpp
// docs/ai/samples/https_fetch.cpp
// DataReel 7 sample: HTTPS GET and POST with gxsHTTPClient.
// Usage: https_fetch https://host/path [ca.pem]
#include <iostream>
#include <string>
#include "gxshttpc.h"

using namespace datareel;

int main(int argc, char **argv)
{
  if(argc < 2) {
    std::cerr << "Usage: https_fetch https://host/path [ca.pem]\n";
    return 1;
  }
  gxsHTTPClient client; // https:// only; http:// URLs are refused
  client.SetTimeout(30);
  if(argc > 2) client.SetCAFile(argv[2]);
  client.AddHeader("Accept", "application/json, text/plain, */*");

  gxsHTTPMessage resp;
  // Returns 0 when any response arrived, so check resp.status too.
  // Redirects are followed (5 by default).
  if(client.Get(argv[1], resp) != 0) {
    std::cerr << "GET failed: " << client.Error() << "\n";
    return 1;
  }
  std::cout << "GET " << client.FinalURL() << " -> " << resp.status << " "
	    << resp.reason << ", " << resp.body.size() << " bytes, "
	    << "Content-Type: " << resp.Header("content-type") << "\n";

  // POST a JSON body. Request() sends any other method.
  gxsHTTPMessage post_resp;
  if(client.Post(argv[1], "{\"name\":\"DataReel\"}", "application/json",
		 post_resp) != 0) {
    std::cerr << "POST failed: " << client.Error() << "\n";
    return 1;
  }
  std::cout << "POST -> " << post_resp.status << " " << post_resp.reason
	    << "\n";
  return (resp.status >= 200 && resp.status < 300) ? 0 : 2;
}
```

- `Download(url, file, resp)` streams the body to a file.
- `SetBasicAuth(user, password)` sends basic auth (only to the first
  host in a redirect chain). `AddHeader()` adds headers such as
  `Authorization: Bearer ...` to every request.
- `gxsHTTPMessage` fields: `status`, `reason`, `version`, `headers`
  (vector of name/value pairs), `body`; `Header(name)` looks up a
  header without regard to case.

## HTTPS server

There is no server framework; read a request and write a response on
each accepted `gxsTLSConnection`:

```cpp
#include "gxshttp.h"
#include "gxstls.h"
using namespace datareel;

void handle(gxsTLSConnection &conn)
{
  gxsHTTPMessage req, resp;
  if(gxsHTTPReadRequest(conn, req, 1024*1024) != 0) return; // 1 MB body limit
  resp.status = 200;
  resp.SetHeader("Content-Type", "application/json");
  resp.SetHeader("Connection", "close");
  resp.body = "{\"path\":\"" + req.target + "\"}"; // Escape in real code
  gxsHTTPWriteResponse(conn, resp); // Sets Content-Length
}
```

`examples/tls/https_server.cpp` is a complete static file server with
a thread pool and document root checks.

## Mail and file transfer

All three clients take a `gxsMailSecurity` value:
`gxsMAIL_IMPLICIT_TLS` (TLS from the start: SMTP 465, POP3 995, FTPS
990) or `gxsMAIL_STARTTLS` (STARTTLS, STLS or AUTH TLS after the
greeting: SMTP 587, POP3 110, FTP 21). A server that does not offer TLS
is refused.

```cpp
// docs/ai/samples/send_mail.cpp
// DataReel 7 sample: send a message through a mail server's
// submission port (587) with STARTTLS and a login.
// Usage: send_mail host port user password from to [ca.pem]
#include <cstdlib>
#include <iostream>
#include <string>
#include "gxsmtp.h"

using namespace datareel;

int main(int argc, char **argv)
{
  if(argc < 7) {
    std::cerr << "Usage: send_mail host port user password from to [ca.pem]\n";
    return 1;
  }
  gxsSMTPClient smtp;
  smtp.SetTimeout(30);
  if(argc > 7) smtp.SetCAFile(argv[7]);
  // Port 587 (submission) uses gxsMAIL_STARTTLS; port 465 uses
  // gxsMAIL_IMPLICIT_TLS, the default. A server that does not offer STARTTLS is
  // refused; nothing is ever sent in plain text.
  if(smtp.Connect(argv[1], std::atoi(argv[2]), gxsMAIL_STARTTLS) != 0 ||
     smtp.Login(argv[3], argv[4]) != 0) {
    std::cerr << "SMTP: " << smtp.Error() << "\n";
    return 1;
  }
  if(smtp.SendMessage(argv[5], { argv[6] }, "Test from DataReel 7",
		      "This message was sent over TLS.\n") != 0) {
    std::cerr << "Send failed: " << smtp.Error() << "\n";
    return 1;
  }
  smtp.Quit();
  std::cout << "Sent\n";
  return 0;
}
```

For several recipients, attachments or custom headers use the
low-level calls: `MailFrom()`, `RcptTo()` for each recipient, then
`Data(message)` with complete RFC 5322 headers and body (`smtp_time()`
in `systime.h` makes the Date header, `gxsBase64Encode` encodes
attachments).

POP3:

```cpp
gxsPOP3Client pop3;
if(pop3.Connect("mail.example.com") != 0 ||      // 995, implicit TLS
   pop3.Login("user", "password") != 0) { /* pop3.Error() */ }
int count; long long bytes;
pop3.Stat(count, bytes);
std::string msg;
if(count > 0) pop3.Retr(1, msg);                 // Whole message, dot-unstuffed
pop3.Quit();
```

FTPS (data connections are encrypted with PROT P and reuse the control
connection's TLS session, which most servers require):

```cpp
gxsFTPClient ftp;
long long bytes = 0;
if(ftp.Connect("ftp.example.com") != 0 ||         // 21, AUTH TLS
   ftp.Login("user", "password") != 0) { /* ftp.Error() */ }
ftp.Put("report.pdf", "upload/report.pdf", bytes);
ftp.Get("data/latest.csv", "latest.csv", bytes);
std::string listing;
ftp.List("data", listing);
ftp.Quit();
```

## Database blocks over TLS (gxStream)

`gxStream` sends `gxBlockHeader` framed blocks over a
`gxsTLSConnection`, used to add, request, change and delete database
records on a remote server. Each block's status byte says what it is
(`gxAddRemoteBlock`, `gxRequestBlock`, `gxCloseConnection`, ...). A
server accepts with `server.Accept(stream.Connection())`, then loops on
`ReadHeader()` / `ReadBlock()` and answers with `WriteBlock()` or
`WriteResultBlock()`. Block sizes from the network are limited by
`SetMaxBlockSize()` (64 MB by default). See
`examples/tls/stream_server.cpp` and `stream_client.cpp`.

## Raw sockets

`gxSocket` is the IPv4/IPv6 socket under everything above:
`Connect(host, port, timeout)`, `Listen(port)`, `Accept()` (returns a
new `gxSocket`), `Send`, `Recv` (exact count), `RecvSome`, and UDP with
`OpenDatagram()`, `SendTo()`, `RecvFrom()`. Use it only for protocols
that cannot use TLS; attach a connected socket to TLS with
`gxsTLSConnection::AttachClient()` or `AttachServer()`.

## Mistakes to avoid

- Expecting `http://`, port 25 plain SMTP or plain FTP to work: they are
  refused by design.
- Disabling certificate checks to "fix" a test: give the client the CA
  with `SetCAFile()` instead, and put the host name or IP address in
  the certificate's subjectAltName.
- Using one `gxsTLSConnection` from two threads at the same time.
- Treating a `0` return from `gxsHTTPClient::Get()` as HTTP success:
  it means a response arrived; check `resp.status`.

========================================================================
FILE: docs/ai/object-database.md
========================================================================

# The DataReel 7 object database

DataReel's database is an embedded library: your program opens files
directly, there is no server and no SQL. It suits programs that store
their own records locally and need them to survive crashes and
upgrades: configuration and state stores, message and event logs,
catalogs, caches, and data exchanged between programs as files. File
formats have not changed since 2001.

Pick a layer:

| Need | Use | Header |
|------|-----|--------|
| Store and reuse variable size blocks by address | `gxDatabase` | `gxdbase.h` |
| Sorted index of fixed-size keys on disk | `gxBtree` with your `DatabaseKeyB` class | `gxbtree.h` |
| A data file and its indexes opened together | `POD` | `pod.h` |
| Objects with a key and variable length members | `InfoHog<KeyType>` | `infohog.h` |
| Tables with typed columns, keys, search and sort | `gxRDBMS` | `gxrdbms.h` |
| Faster repeated reads | `gxDatabaseCache`, `BtreeCache` | `dbfcache.h`, `btcache.h` |
| Check and repair files | `db_debug`, `ix_debug` utilities | `utils/` |

## Concepts

- A database file (`.gxd` by convention) is a header followed by
  blocks. `Alloc(n)` returns the **object address** (`FAU_t`) of a
  block that holds at least n bytes, reusing deleted blocks first.
  `Write(buf, n, addr)` and `Read(buf, n, addr)` use that address.
  Addresses stay valid until the object is deleted, so indexes store
  them.
- Each block has a header with a status (normal, deleted, removed)
  and, in revision A and later files, an optional CRC
  (`WriteObjectChecksum`, `ReadObjectChecksum`). New files are revision
  E, which also supports file and record locks.
- Records are raw bytes. Use fixed-size types (`char[N]`, `gxINT32`,
  `gxUINT32`, `gxINT64`, `gxFLOAT64`, `CDate`, `FAU`), never pointers,
  `std::string` or virtual functions. With
  `DATAREEL_NATIVE_TYPES=OFF` the gx types are byte-order-portable
  classes and files can move between Linux and Windows.
- `gxDatabase`, `gxBtree`, `POD` and `gxRDBMS` are not copyable. Use
  one object per file per thread, or guard it with a `std::mutex`.
  `LockFile()`/`LockRecord()` coordinate several processes.
- Error codes are `gxDatabaseError`; `gxDBASE_NO_ERROR` is 0.
  `DatabaseExceptionMessage()` (member) or
  `gxDatabaseExceptionMessage(err)` gives the text.

## Records in a block file

```cpp
// docs/ai/samples/record_store.cpp
// DataReel 7 sample: store, read, walk and delete fixed-layout records
// in a gxDatabase block file.
// Usage: record_store [file.gxd]
#include <cstring>
#include <iostream>
#include "gxdbase.h"
#include "gxfloat.h"

using namespace datareel;

// Records are written as raw bytes, so use fixed-size members: char
// arrays and the gx integer and float types. No pointers, std::string
// or virtual functions.
struct Part
{
  char name[32];
  gxINT32 quantity;
  gxFLOAT64 price;
};

static Part make_part(const char *name, long qty, double price)
{
  Part p;
  memset(&p, 0, sizeof(p)); // Zero the padding too
  strncpy(p.name, name, sizeof(p.name) - 1);
  p.quantity = (gxINT32)qty;
  p.price = price;
  return p;
}

int main(int argc, char **argv)
{
  const char *fname = argc > 1 ? argv[1] : "parts.gxd";
  gxDatabase db;
  // Open() and Create() return gxDBASE_NO_ERROR (0) on success
  if(db.Open(fname) != gxDBASE_NO_ERROR && 
     db.Create(fname) != gxDBASE_NO_ERROR) {
    std::cerr << fname << ": " << db.DatabaseExceptionMessage() << "\n";
    return 1;
  }

  // Alloc() returns the address of a new (or reused) block, 0 on error
  Part parts[] = { make_part("bolt", 500, 0.10), make_part("nut", 800, 0.05),
		   make_part("washer", 1200, 0.02) };
  FAU_t nut_addr = 0;
  for(const Part &p : parts) {
    FAU_t addr = db.Alloc(sizeof(Part));
    if(addr == (FAU_t)0 || db.Write(&p, sizeof(Part), addr) != gxDBASE_NO_ERROR) {
      std::cerr << "Write failed: " << db.DatabaseExceptionMessage() << "\n";
      return 1;
    }
    db.WriteObjectChecksum(addr); // Optional CRC for this record
    if(strcmp(p.name, "nut") == 0) nut_addr = addr;
  }

  // Read one record back by its address
  Part p;
  if(db.Read(&p, sizeof(Part), nut_addr) != gxDBASE_NO_ERROR) return 1;
  std::cout << "At " << (long long)nut_addr << ": " << p.name << "\n";

  // Delete it. The block goes on the free list and the next Alloc()
  // of the same size or smaller reuses it.
  db.Delete(nut_addr);

  // Walk every record that is not deleted
  for(FAU_t addr = db.FindFirstObject(); addr != (FAU_t)0; 
      addr = db.FindNextObject(addr)) {
    if(db.ObjectLength(addr) != sizeof(Part)) continue; // Not a Part
    if(db.Read(&p, sizeof(Part), addr) != gxDBASE_NO_ERROR) break;
    std::cout << (long long)addr << " " << p.name << " qty " 
	      << (long)p.quantity << " price " << (double)p.price << "\n";
  }
  db.Close();
  return 0;
}
```

Useful calls: `ObjectLength(addr)`, `UnDelete(addr)`,
`Delete(addr, 1)` (remove permanently), `TotalBlocks()`,
`DeletedBlocks()`, `Flush()`, `Open(name, gxDBASE_READONLY)`.
`FindFirstObject()`, `FindNextObject(addr)`, `FindPrevObject(addr)`
walk the objects in file order, skipping deleted ones.

## Records with an index

A key class derives from `DatabaseKeyB`, keeps its data in one
fixed-size struct, passes that struct's address to the base class,
and implements `KeySize()` and the compare functions. Store the
record's address in the key so a lookup leads to the record. `POD`
opens (or creates) the data file and the index file together; it
needs the key class only as a type sample.

```cpp
// docs/ai/samples/indexed_records.cpp
// DataReel 7 sample: a data file plus a B-tree index (POD) with a
// custom key class. Records are found by name and listed in key order.
// Usage: indexed_records [data.gxd index.btx]
#include <cstring>
#include <iostream>
#include "pod.h"

using namespace datareel;

struct Customer
{
  char name[48];
  char email[64];
  gxINT32 orders;
};

// The index entry: a fixed-size key plus the record's address. Only
// the name is compared, so a lookup by name returns the address.
struct CustomerKeyData
{
  char name[48];
  FAU address;
};

class CustomerKey : public DatabaseKeyB
{
public:
  CustomerKey(const char *name = "", FAU_t addr = 0) : DatabaseKeyB(&data) {
    memset(&data, 0, sizeof(data));
    strncpy(data.name, name, sizeof(data.name) - 1);
    data.address = addr;
  }
  FAU_t Address() const { return (FAU_t)data.address; }
  const char *Name() const { return data.name; }

public: // DatabaseKeyB interface
  size_t KeySize() override { return sizeof(data); }
  // The CMake build defines __USE_SINGLE_COMPARE__, which uses
  // CompareKey(). Defining the dual compare functions as well keeps the
  // class working with either setting.
  int CompareKey(const DatabaseKeyB &key) const {
    return strcmp(data.name, ((const CustomerKeyData *)key.db_key)->name);
  }
  int operator==(const DatabaseKeyB &key) const { return CompareKey(key) == 0; }
  int operator>(const DatabaseKeyB &key) const { return CompareKey(key) > 0; }

private:
  CustomerKeyData data;
};

const BtreeNodeOrder_t ORDER = 31; // Keys per node + 1

static int add(POD &pod, const char *name, const char *email, long orders)
{
  CustomerKey key(name), found;
  if(pod.Index()->Find(key, found)) return -1; // Names are unique
  Customer c;
  memset(&c, 0, sizeof(c));
  strncpy(c.name, name, sizeof(c.name) - 1);
  strncpy(c.email, email, sizeof(c.email) - 1);
  c.orders = (gxINT32)orders;
  gxDatabase *db = pod.OpenDataFile();
  FAU_t addr = db->Alloc(sizeof(c));
  if(addr == (FAU_t)0 || db->Write(&c, sizeof(c), addr) != gxDBASE_NO_ERROR)
    return -1;
  CustomerKey entry(name, addr), compare;
  return pod.Index()->Insert(entry, compare) ? 0 : -1;
}

int main(int argc, char **argv)
{
  const char *dfile = argc > 2 ? argv[1] : "customers.gxd";
  const char *ifile = argc > 2 ? argv[2] : "customers.btx";
  CustomerKey key_type;
  POD pod;
  // Opens both files, creating them if they do not exist
  gxDatabaseError err = pod.Open(dfile, ifile, key_type, ORDER);
  if(err != gxDBASE_NO_ERROR) {
    std::cerr << gxDatabaseExceptionMessage(err) << "\n";
    return 1;
  }
  add(pod, "Wilson Supply", "orders@wilson.example", 12);
  add(pod, "Acme Tools", "buy@acme.example", 40);
  add(pod, "Baker Hardware", "info@baker.example", 7);

  // Look up one record by name. Find() fills in the stored key,
  // including the record address.
  CustomerKey key("Acme Tools"), compare;
  Customer c;
  if(pod.Index()->Find(key, compare) &&
     pod.OpenDataFile()->Read(&c, sizeof(c), key.Address()) == gxDBASE_NO_ERROR) {
    std::cout << "Found " << c.name << " <" << c.email << ">\n";
  }

  // List every record in name order
  CustomerKey k;
  if(pod.Index()->FindFirst(k)) {
    do {
      pod.OpenDataFile()->Read(&c, sizeof(c), k.Address());
      std::cout << "  " << c.name << ", " << (long)c.orders << " orders\n";
    } while(pod.Index()->FindNext(k, compare));
  }

  // Delete: remove the index entry, then the record
  CustomerKey gone("Baker Hardware");
  if(pod.Index()->Find(gone, compare)) {
    FAU_t addr = gone.Address();
    pod.Index()->Delete(gone, compare);
    pod.OpenDataFile()->Delete(addr);
  }
  pod.Close();
  return 0;
}
```

Notes:

- `Find(key, compare)` fills `key` with the stored key when it matches,
  so compare only the fields you search by and read the rest (the
  address) back from `key`.
- `Insert()` does not reject duplicates on its own; `Find()` first if
  keys must be unique.
- The node order (keys per node + 1) is fixed when the index is
  created; reopen with the same order and key size.
- `FindFirst`/`FindNext`/`FindPrev`/`FindLast` iterate in key order.
  `PartialFind()` finds the first key equal to or greater than a
  value, for prefix and range scans.
- An index file can hold several trees, and a `POD` can have several
  index files (`POD::Open` with an array of file names). When an
  index file did not exist and `POD::Open` created it empty,
  `pod.RebuildIndex()` returns true and the program must insert the
  keys again from the data file.
- `gxBtree` can be used alone: `gxBtree btx(key_type, order);
  btx.Create("file.btx")` or `btx.Open("file.btx")`.

## Persistent objects (InfoHog)

`InfoHog<Key>` stores an object made of a key (member 0) plus any
number of variable length members, with the key in a B-tree index.
Good for documents, contacts or settings whose fields change size.

```cpp
// examples/database/infohog/infohog.cpp (license header omitted)
#include "gxdlcode.h"

#include <cstring>
#include <iostream>
#include <string>
#include "infohog.h"

// Fixed-length key. InfoHog keys are stored in the index as raw
// bytes, so they must not hold pointers (no std::string).
struct ContactName
{
  ContactName() { memset(name, 0, sizeof(name)); }
  ContactName(const char *s) {
    memset(name, 0, sizeof(name));
    strncpy(name, s, sizeof(name) - 1);
  }
  int operator==(const ContactName &o) const { return strcmp(name, o.name) == 0; }
  int operator<(const ContactName &o) const { return strcmp(name, o.name) < 0; }
  int operator>(const ContactName &o) const { return strcmp(name, o.name) > 0; }

  char name[64];
};

typedef InfoHog<ContactName> Contact;
typedef InfoHogKey<ContactName> ContactKey;

// Member numbers. Member 0 is always the key.
enum { NAME = 0, EMAIL = 1, PHONE = 2, NOTES = 3, NUM_MEMBERS = 4 };

static void set_text(Contact &c, unsigned member, const std::string &s)
{
  c.SetMember(s.c_str(), (unsigned)s.size() + 1, member); // Keep the null
}

static std::string get_text(Contact &c, unsigned member)
{
  const char *p = (const char *)c.GetMember(member);
  unsigned len = c.GetMemberLen(member);
  if(!p || len == 0) return std::string();
  return std::string(p, strnlen(p, len));
}

static int open_pod(POD &pod, const char *data_file, const char *index_file)
{
  ContactKey key_type;
  gxDatabaseError err = pod.Open(data_file, index_file, key_type,
				 InfoHogNodeOrder, gxDBASE_READWRITE,
				 InfoHogUseIndexFile, InfoHogStaticArea,
				 InfoHogNumTrees);
  if(err != gxDBASE_NO_ERROR) {
    std::cerr << "Cannot open the database: " 
	      << gxDatabaseExceptionMessage(err) << "\n";
    return -1;
  }
  return 0;
}

static int add_contact(POD &pod, const char *name, const char *email,
		       const char *phone, const char *notes)
{
  ContactName key(name);
  Contact c(&pod, key, NUM_MEMBERS);
  set_text(c, EMAIL, email);
  set_text(c, PHONE, phone);
  set_text(c, NOTES, notes);
  if(c.FindObject()) { // Keys are unique
    std::cout << "Not added, " << name << " is already in the database\n";
    return -1;
  }
  if(!c.WriteObject(0)) {
    std::cout << "Could not add " << name << "\n";
    return -1;
  }
  std::cout << "Added " << name << " at address " 
	    << (long long)c.ObjectAddress() << "\n";
  return 0;
}

static int find_contact(POD &pod, const char *name, Contact &c)
{
  ContactName key(name);
  Contact probe(&pod, key, NUM_MEMBERS);
  if(!probe.FindObject()) return -1;
  if(!c.ReadObject(probe.ObjectAddress())) return -1;
  return 0;
}

static void list_contacts(POD &pod)
{
  ContactKey key, compare_key;
  gxBtree *btx = pod.Index();
  std::cout << "Contacts in key order:\n";
  if(!btx->FindFirst(key)) {
    std::cout << "  (none)\n";
    return;
  }
  do {
    Contact c(&pod, NUM_MEMBERS);
    if(c.ReadObject(key.ObjectID())) {
      std::cout << "  " << key.ObjectName().name << " <" 
		<< get_text(c, EMAIL) << "> " << get_text(c, PHONE) << "\n";
    }
  } while(btx->FindNext(key, compare_key));
}

int main(int argc, char **argv)
{
  const char *data_file = argc > 1 ? argv[1] : "contacts.ihd";
  const char *index_file = argc > 2 ? argv[2] : "contacts.ihx";

  POD pod;
  if(open_pod(pod, data_file, index_file) != 0) return 1;

  add_contact(pod, "Carol Smith", "carol@example.com", "555-0103", "Sales");
  add_contact(pod, "Alice Jones", "alice@example.com", "555-0101", "Engineering");
  add_contact(pod, "Bob Brown", "bob@example.com", "555-0102", "Support");
  add_contact(pod, "Alice Jones", "alice2@example.com", "555-0199", "Duplicate");
  list_contacts(pod);

  Contact c(&pod, NUM_MEMBERS);
  if(find_contact(pod, "Bob Brown", c) != 0) {
    std::cerr << "Bob Brown not found\n";
    return 1;
  }
  std::cout << "Found Bob Brown: notes = " << get_text(c, NOTES) << "\n";

  // Change one member in place. The record grows, so InfoHog moves it.
  std::string notes = "Support team lead since 2026";
  if(!c.ChangeMember(notes.c_str(), (unsigned)notes.size() + 1, NOTES)) {
    std::cerr << "ChangeMember failed\n";
    return 1;
  }
  std::cout << "Changed Bob Brown's notes\n";

  ContactName gone("Carol Smith");
  Contact del(&pod, gone, NUM_MEMBERS);
  if(del.FindObject() && del.DeleteObject()) {
    std::cout << "Deleted Carol Smith\n";
  }
  list_contacts(pod);
  pod.Close();

  // Reopen the files to show the changes were saved
  std::cout << "Reopening the database\n";
  POD pod2;
  if(open_pod(pod2, data_file, index_file) != 0) return 1;
  Contact again(&pod2, NUM_MEMBERS);
  if(find_contact(pod2, "Bob Brown", again) != 0) {
    std::cerr << "Bob Brown not found after reopening\n";
    return 1;
  }
  std::cout << "Bob Brown's notes: " << get_text(again, NOTES) << "\n";
  list_contacts(pod2);
  pod2.Close();
  return get_text(again, NOTES) == notes ? 0 : 1;
}
```

## Relational tables (gxRDBMS)

`gxRDBMS` creates a table from a text definition: a table name and
columns written `Name{display width}TYPE[size]`, plus a key
definition. The types are `INT16`, `INT32`, `INT64`, `FLOAT`,
`BOOL`, `CHAR[n]`, `VCHAR[n]`, `BINARY`, `BLOB`, `CURRENCY` and `DATE`. Records are `gxrdDatabaseRecord` objects whose
fields are set and read by zero-based column number; `Search()`
and `SortOrderList()` fill a `std::set<gxrdDatabaseRecord>`. It has no
SQL and no joins; use SQLite when you need those.

```cpp
// examples/database/rdbms/rdbms.cpp (license header omitted)
#include "gxdlcode.h"

#include <cstring>
#include <iostream>
#include <set>
#include <string>
#include "gxrdbms.h"

static void print_record(gxrdDatabaseRecord &r)
{
  gxINT32 id = 0;
  gxFLOAT64 salary = 0;
  CDate hired;
  r.GetField(&id, sizeof(id), 0);
  r.GetField(&salary, sizeof(salary), 3);
  r.GetField(&hired, sizeof(hired), 4);
  std::cout << "  " << (long)id << "  " << (const char *)r.GetField(1)
	    << ", " << (const char *)r.GetField(2) << ", "
	    << (double)salary << ", hired " << hired.str() << "\n";
}

static int add_employee(gxRDBMS &db, long id, const char *name,
			const char *dept, double salary, const char *hired)
{
  gxrdDatabaseRecord rec(db.RecordDef());
  gxINT32 i = (gxINT32)id;
  gxFLOAT64 s = salary;
  CDate d(hired);
  rec.SetField(&i, sizeof(i), 0);
  rec.SetField(name, (__ULWORD__)strlen(name) + 1, 1); // Variable length
  rec.SetField(dept, (__ULWORD__)strlen(dept) + 1, 2);
  rec.SetField(&s, sizeof(s), 3);
  rec.SetField(&d, sizeof(d), 4);
  if(!db.AddRecord(&rec)) {
    std::cout << "Not added: " << name << " ("
	      << db.GetDatabase()->DataFileExceptionMessage() << ")\n";
    return -1;
  }
  return 0;
}

int main(int argc, char **argv)
{
  // Columns: ID, Name (primary key), Department (secondary key),
  // Salary, Hire date. {n} is the display width and [n] the size.
  std::string tdef = "employees, ID{6}INT32, Name{30}VCHAR[64], "
    "Department{20}CHAR[32], Salary{10}FLOAT, Hired{10}DATE";
  // ALLOWDUP lets several records share a department key value
  std::string kdef = "PRI: COL 2, SEC: COL 3, ATTRIB: NOCASE, ATTRIB: ALLOWDUP";

  gxRDBMS db;
  gxDatabaseError err = argc > 1 ? db.CreateTable(tdef, kdef, argv[1]) : 
    db.CreateTable(tdef, kdef);
  if(err != gxDBASE_NO_ERROR) {
    std::cerr << gxDatabaseExceptionMessage(err) << "\n";
    return 1;
  }
  std::cout << "Record definition: " << db.RecordDef() << "\n";

  if(db.IsEmpty()) {
    add_employee(db, 101, "Alice Jones", "Engineering", 125000.0, "3/14/2019");
    add_employee(db, 102, "Bob Brown", "Support", 72000.0, "6/1/2021");
    add_employee(db, 103, "Carol Smith", "Sales", 88000.0, "11/20/2017");
    add_employee(db, 104, "Dave Lee", "Engineering", 118000.0, "1/9/2023");
    add_employee(db, 105, "Erin Diaz", "Support", 69000.0, "8/30/2024");
  }
  std::cout << db.NumRecords() << " records\n";

  // Find by primary key. Only the key field is needed and NOCASE
  // makes the match ignore case.
  gxrdDatabaseRecord found(db.RecordDef());
  found.SetField("BOB BROWN", 10, 1);
  FAU_t addr;
  if(!db.FindRecord(&found, addr)) {
    std::cerr << "Bob Brown not found\n";
    return 1;
  }
  std::cout << "Found:\n";
  print_record(found);

  // Change the salary
  gxrdDatabaseRecord changed(found);
  gxFLOAT64 raise = 76000.0;
  changed.SetField(&raise, sizeof(raise), 3);
  if(!db.ChangeRecord(&found, &changed)) {
    std::cerr << "Change failed\n";
    return 1;
  }
  std::cout << "Changed Bob Brown's salary\n";

  // Search the department index (one-based field 3) for Engineering
  std::set<gxrdDatabaseRecord> matches;
  std::string dept = "engineering";
  // Arguments: index 1 (the secondary key), one-based field 3, no case
  // compare, no alphanumeric compare, exact match, at most every record
  err = db.Search(matches, dept, 1, 3, 0, 0, 0, db.NumRecords());
  if(err != gxDBASE_NO_ERROR) {
    std::cerr << "Search failed: " << gxDatabaseExceptionMessage(err) << "\n";
    return 1;
  }
  std::cout << "Engineering (" << matches.size() << "):\n";
  for(const auto &m : matches) {
    gxrdDatabaseRecord r(m);
    print_record(r);
  }

  // List the whole table in primary key order
  std::set<gxrdDatabaseRecord> all;
  FAU_t n = db.NumRecords();
  db.SortOrderList(all, n, 1, n); // Limit, first and last record
  std::cout << "All employees:\n";
  for(const auto &m : all) {
    gxrdDatabaseRecord r(m);
    print_record(r);
  }
  db.Close();
  return matches.size() == 2 ? 0 : 1;
}
```

## Checking and repairing files

- `db_debug file.gxd` analyzes a database file, reports damaged
  blocks and can rebuild a damaged file.
- `ix_debug file.btx` checks a B-tree index file.
- `examples/database/defrag` shows first-fit and best-fit reuse of
  deleted blocks.
- The library checks lengths and counts it reads from files, so a
  damaged file returns an error instead of crashing (see
  `tests/damaged_files.cpp`).

## Mistakes to avoid

- Putting `std::string` or pointers in a record or key: the file then
  holds memory addresses. Use `char[N]` or an InfoHog member.
- Opening files with a different `DATAREEL_64BIT_DATABASE` or
  `DATAREEL_NATIVE_TYPES` setting than the one that created them.
- Forgetting to `memset` records and keys: padding bytes then hold
  random memory and identical records differ on disk.
- Deleting a record but not its index entry (or the reverse).
- Implementing only `operator==`/`operator>` in a key class: the CMake
  build uses `CompareKey()`.

========================================================================
FILE: docs/ai/building.md
========================================================================

# Building and linking DataReel 7

## Requirements

- A C++17 compiler: GCC 8 or later (RHEL 8, 9, 10) or Visual Studio
  2022/2026 (64-bit).
- CMake 3.16 or later.
- TLS: OpenSSL 1.1.1 or later on Linux (`openssl-devel`); on Windows
  Schannel (built in, the default) or OpenSSL 3.x.

## Build the library

```sh
# Linux
sudo dnf install gcc-c++ cmake openssl-devel
cmake -S . -B cmake-build -DCMAKE_BUILD_TYPE=Release
cmake --build cmake-build -j
sudo cmake --install cmake-build              # /usr/local
```

```bat
rem Windows, Developer Command Prompt
cmake -S . -B cmake-build -G "Visual Studio 17 2022" -A x64
cmake --build cmake-build --config Release
cmake --install cmake-build --config Release --prefix C:\datareel
rem OpenSSL instead of Schannel:
rem   -DDATAREEL_SSL=OPENSSL -DOPENSSL_ROOT_DIR=C:/openssl64
```

## Options

| Option | Default | Meaning |
|--------|---------|---------|
| `DATAREEL_SSL` | `OPENSSL` (Linux), `SCHANNEL` (Windows) | TLS backend |
| `DATAREEL_64BIT_DATABASE` | `OFF` | 64-bit file addresses (files over 2 GB). Changes the file format. |
| `DATAREEL_NATIVE_TYPES` | `ON` | Native integer/float types in records. `OFF` uses portable byte-order classes so files can move between Linux and Windows. Changes the file format. |
| `DATAREEL_GUI` | `CONSOLE` | `WX` shows errors in wxWidgets message boxes |
| `BUILD_SHARED_LIBS` | `OFF` | Shared library (`libdatareel.so.7`, `datareel.dll`) |
| `DATAREEL_BUILD_EXAMPLES` | `ON` | Examples, utilities, tests and the docs/ai samples |

Pick the two file format options once per project and keep them: a
program can only open files written with the same settings.

## Use it from your project

```cmake
cmake_minimum_required(VERSION 3.16)
project(myapp CXX)
set(CMAKE_CXX_STANDARD 17)
find_package(DataReel 7 REQUIRED)         # after cmake --install
# or: add_subdirectory(third_party/datareel)
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE DataReel::datareel)
```

The `DataReel::datareel` target adds the include directory, the
compile definitions that select the platform and file format, OpenSSL
or the Windows libraries, and threads. Compiling DataReel headers
without those definitions fails with "You must define a target
platform and/or compiler".

Without CMake (Linux), `build/makefile` builds `lib/libdatareel.a`
(`cd build; make; make install`; `make LARGEFILES=1` for the 64-bit
engine). Copy the compile flags from `env/linux.env`.

## Test

```sh
tests/run_tests.sh                       # build in ./cmake-build and run all checks
CMAKE_OPTS="-DDATAREEL_64BIT_DATABASE=ON" tests/run_tests.sh /tmp/dr-b64
CMAKE_OPTS="-DBUILD_SHARED_LIBS=ON" tests/run_tests.sh /tmp/dr-so
```

The script makes a test CA and server certificate, runs the database
examples, the TLS examples against each other, the mail and FTPS
clients against `tests/mock_servers.py` (needs python3), the docs/ai
samples, and `tests/damaged_files`, which opens thousands of damaged
database files.

## Run the examples

Built programs are in `cmake-build/examples/`. Database examples write
their files to the current directory. TLS examples take
`--ca ca.pem` to trust a test CA; see `docs/examples/examples.htm`.

========================================================================
FILE: docs/ai/porting-from-6x.md
========================================================================

# Porting DataReel 6.x code to DataReel 7

DataReel 7 keeps the database and TLS classes and replaces the general
purpose classes with the C++ standard library. Files written by 6.x
open unchanged; most porting work is replacing removed classes.

## Checklist

1. Build with CMake and C++17 (`find_package(DataReel 7)`,
   `DataReel::datareel`). The old nmake, Borland and MinGW makefiles,
   `dll/` and `winslib/` are gone.
2. Keep `DATAREEL_64BIT_DATABASE` and `DATAREEL_NATIVE_TYPES` the same
   as your 6.x build so existing files keep their layout. In the 6.x
   Linux makefile `LARGEFILES=1` meant the 64-bit engine and
   `TYPES_DEFMACS` with `__USE_NATIVE_INT_TYPES__` meant native types.
3. Replace the removed classes (table below).
4. Change network code to TLS (below).
5. Remove includes of headers that no longer exist (`gxstring.h`,
   `ustring.h`, `membuf.h`, `gxlist.h`, `gxbstree.h`, `dfileb.h`,
   `futils.h`, `gxthread.h`, `gxmutex.h`, `gxcond.h`, `gxsema.h`,
   `gxdatagm.h`, `gxsutils.h`, `stdafx.h`, `gxdlincs.h`). `systime.h`
   and `thrpool.h` still exist with new contents.

## Removed classes and replacements

| 6.x | DataReel 7 |
|-----|------------|
| `UString`, `gxString` | `std::string` plus `strutil.h` functions |
| `gxStringw` | `std::wstring` |
| `MemoryBuffer`, `MemoryBlock` | `std::vector<unsigned char>` |
| `gxList`, `gxListB`, `gxBStree` | `std::list`, `std::vector`, `std::set`, `std::map` |
| `DiskFileB`, `futils` | `std::fstream`, `std::filesystem` |
| `SysTime` | `systime.h` functions (`date_time_string()`, `smtp_time()`, `http_time()`, `iso8601_utc()`), `std::chrono` |
| `gxThread` and helpers | `std::thread` |
| `gxMutex`, `gxCondition`, `gxSemaphore` | `std::mutex`, `std::condition_variable`, `std::counting_semaphore` (C++20) |
| `thrPool` | `datareel::ThreadPool` (`thrpool.h`) |
| `gxsHTTPSClient` | `gxsHTTPClient` (HTTPS only) |
| `gxDatagram` | `gxSocket::OpenDatagram()`, `SendTo()`, `RecvFrom()` |
| `gxIP32`, `gxMAC48` | `gxsAddress`, `std::string` |
| `FString` | `char[N]` or `std::array` |
| `gxsHTML`, `gxsXML`, `gxsRSS` | a dedicated parser library |
| `HyperText`, `PostScriptDrv`, report writers | `std::ostream` |
| `Terminal` | ncurses |
| `gxDeviceCache` | none |

## Strings

`strutil.h` has std::string versions of the gxString members:

```cpp
#include "strutil.h"
using namespace datareel;

std::string s = "  Name: Alice  ";
trim_in_place(s);                          // s.TrimLeadingSpaces(); s.TrimTrailingSpaces();
delete_before_including(s, ": ");          // s.DeleteBeforeIncluding(": ");
std::size_t pos = ifind(s, "ali");         // s.IFind("ali"); npos (not -1) if absent
s = to_upper(s);                           // s.ToUpper();
int n = to_int("42");                      // gxString("42").Atoi()
std::vector<std::string> parts = split("a,b,c", ',');
replace_all(s, "Alice", "Bob");            // s.ReplaceString("Alice", "Bob")
std::string line;
str_append(line, "id=", 42, ' ', 3.5);     // line << "id=" << 42 << ' ' << 3.5;
```

`Find()` results are `std::string::npos` on failure instead of `-1`;
check every `if(pos == -1)` test.

## Memory buffers

```cpp
std::vector<unsigned char> buf;            // MemoryBuffer buf;
buf.assign(p, p + n);                      // buf.Load(p, n);
send(buf.data(), (int)buf.size());         // buf.m_buf(), buf.length()
```

## Threads

```cpp
#include "thrpool.h"
datareel::ThreadPool pool(8);              // thrPool + gxThread subclass
auto result = pool.Submit([](int x) { return x * 2; }, 21);
int v = result.get();                      // 42; exceptions propagate here
pool.Wait();                               // Wait for queued tasks
```

## Configuration and log files

- `gxConfig::Load()` and the write functions return `bool` (true on
  success). `GetStrValue()` returns `const char *`, or null when the
  name is not set.
- `LogFile` no longer derives from `DiskFileB`: `Open(name)` appends,
  `WriteLine(text)` writes a time-stamped line and is thread safe,
  `operator<<` writes text.
- `CDate::c_str()` is now `str()` and returns `std::string`.

## Network code

Plain text protocols were removed. Typical changes:

| 6.x | DataReel 7 |
|-----|------------|
| `gxSocket` server with `remote_socket`, `RemoteRecv`, `RemoteSend` | `gxsTLSServer::Accept()` gives a `gxsTLSConnection` per client |
| `gxSocket` client `Send`/`Recv` | `gxsTLSConnection::Connect()`, `Send()`, `ReadLine()` |
| `gxsHTTPClient` with `http://` | `gxsHTTPClient` with `https://` only |
| `gxsSMTPClient` on port 25 | `gxsSMTPClient::Connect(host, 587, gxsMAIL_STARTTLS)` or port 465 |
| `gxsPOP3Client` on port 110 plain | `Connect(host)` (995) or `Connect(host, 110, gxsMAIL_STARTTLS)` |
| `gxsFTPClient` plain FTP | FTPS: `Connect(host)` (AUTH TLS on 21) |
| `gxStream` over plain `gxSocket` | `gxStream` over TLS: servers `Accept(stream.Connection())` |
| `gxSSL` with SSLv3/TLS 1.0 methods | `gxSSL_TLS` (1.2 and 1.3), `gxSSL_TLSv1_2`, `gxSSL_TLSv1_3` |

Servers now need a certificate and private key. Clients verify the
server certificate; give them your CA with `SetCAFile()`.

## Namespace

All names are in `namespace datareel`. The headers add
`using namespace datareel;` so unqualified 6.x code compiles. Define
`DATAREEL_NO_GLOBAL_NAMES` before including DataReel headers (or in
your build) to keep the global namespace clean and write
`datareel::gxDatabase`.

## Behavior changes in classes that stayed

- `gxDatabase::FindNextObject()`, `FindNextBlock()`, `FindPrevBlock()`
  and `FindPrevObject()` take an object (or block) address and return
  0 at the end. `FindPrevBlock()` no longer skips deleted blocks.
- `gxRDBMS::Search()` and `SortOrderList()` fill a
  `std::set<gxrdDatabaseRecord>` instead of a `gxBStree`.
- `gxSSL::MakeRSAPrivateKey()` makes 3072-bit keys by default;
  `MakeDHParms()`, `LoadDHParms()`, `MakeRSAEphKey()` were removed.
