diff --git a/INDEX.md b/INDEX.md new file mode 100644 index 0000000..aa769c6 --- /dev/null +++ b/INDEX.md @@ -0,0 +1,77 @@ +# INDEX + +One-page map of every context file. Churn labels: **low** (years), **med** +(months), **high** (weeks). + +## Top level + +| File | Churn | When to load | +|---|---|---| +| `README.md` | low | Never — repo metadata for humans. | +| `SKILL.md` | low | Always (entry hint). | +| `INDEX.md` | low | Always (this file). | + +## architecture/ — structural reference + +| File | Churn | When to load | +|---|---|---| +| `overview.md` | low | First load on any cproof task. | +| `source-map.md` | low | When choosing where to put new code, or locating an unfamiliar module. | +| `test-map.md` | low | When writing or running tests. | +| `data-flow.md` | low | When reasoning about input → output paths (commands, events, UI updates). | + +## patterns/ — conventions and idioms + +| File | Churn | When to load | +|---|---|---| +| `memory.md` | low | Any C edit that allocates / frees. | +| `commands.md` | med | Adding, changing, or understanding a `/command`. | +| `autocomplete.md` | med | Anything that touches `cmd_ac.c` or registers an `Autocomplete`. | +| `events.md` | med | Touching `src/event/`. | +| `xmpp.md` | med | Touching `src/xmpp/` or any XMPP-gated code path. | +| `encryption.md` | med | Touching `src/omemo/`, `src/otr/`, or `src/pgp/`. | +| `ui.md` | med | Touching `src/ui/`. | +| `plugins.md` | med | Touching `src/plugins/`, plugin C/Python API. | + +## testing/ — how tests work + +| File | Churn | When to load | +|---|---|---| +| `unit-tests.md` | med | Writing or extending unit tests. | +| `stubs.md` | med | Adding a function whose callers are unit-tested, or wiring a new stub. | +| `functional-tests.md` | med | Touching `tests/functionaltests/`. | +| `bench.md` | low | Touching `tests/bench/` or interpreting bench output. | + +## build/ — how to build + +| File | Churn | When to load | +|---|---|---| +| `local.md` | low | Reference for `autogen.sh` / `configure` / `make` flags. | +| `docker.md` | low | Always when building — canonical environment. | +| `ci.md` | med | Diagnosing CI failures, reading CI logs. | + +## playbooks/ — step-by-step recipes + +| File | Churn | When to load | +|---|---|---| +| `add-command.md` | med | Adding a new `/command`. | +| `add-test.md` | med | Adding a new unit test file. | +| `add-autocomplete.md` | med | Adding autocomplete for an existing command. | +| `add-event-handler.md` | med | Adding a server- or client-event handler. | +| `add-encryption.md` | med | Touching encryption modules safely. | + +## gotchas.md + +| File | Churn | When to load | +|---|---|---| +| `gotchas.md` | high (append-only) | Always scan before finishing a task. | + +## wip/ — branch-specific notes + +Files describing in-flight feature branches. Each file is **deleted** when +its branch merges to `master`; the surviving content (if any) is folded into +the stable layers above. + +| File | Churn | When to load | +|---|---|---| +| `wip/feat-ai.md` | high (deleted on merge) | Working on `feat/ai`, or trying to understand AI-related code that does not yet exist on `master`. | diff --git a/README.md b/README.md index 2b6df6d..b76e9b5 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,68 @@ # cproof-context +Agent-oriented context documentation for the [cproof](https://git.jabber.space/devs/cproof) +project (a fork of Profanity, a terminal-based XMPP client written in C). + +This repository is **not** human-onboarding documentation. It is structured for +consumption by AI/agent skills — tables over prose, concrete identifiers over +descriptions, layered files loaded on demand. + +## Layout + +| Path | Churn | Purpose | +|---|---|---| +| `SKILL.md` | low | Always-loaded entry hint. Points to `INDEX.md`. | +| `INDEX.md` | low | One-page map of every doc with churn label. | +| `architecture/` | low | Stable structural reference: source map, test map, data flow. | +| `patterns/` | medium | Codified conventions: memory, commands, autocomplete, events, etc. | +| `testing/` | medium | How tests are organised, written, and stubbed. | +| `build/` | medium | Local, Docker, and CI build flows. | +| `playbooks/` | medium | Step-by-step recipes for common tasks. | +| `gotchas.md` | append-only | Dated entries: one per known pitfall. | +| `wip/` | high | Branch-specific notes for in-flight features. Deleted on merge. | + +See `INDEX.md` for the full file list. + +## Consumer + +Consumed by an agent skill that operates on the cproof codebase. The skill +should pin this repo to a specific SHA (not a branch) and treat the loaded +files as authoritative for their stated scope. + +## Update flow + +1. Edits land via PRs against `master`. +2. Drift checks (path / identifier existence in cproof HEAD) run in CI before + merge — see `architecture/test-map.md` and `build/ci.md` for details once + wired up. +3. After major cproof refactors, the skill consumer bumps the pinned SHA to a + commit known to pass drift checks. + +## Out of scope + +- User-specific agent preferences (language, commit-trailer policy, etc.) — + belong in agent memory, not here. +- Git history snapshots, changelogs, PR summaries — derivable from `git log` / + `gh pr`. +- Aspirational guidance — this repo describes cproof as it is, not as it + should be. + +## Baseline and WIP + +The stable layers (`architecture/`, `patterns/`, `testing/`, `build/`, +`playbooks/`, `gotchas.md`) describe **cproof on `master`**. They do not +mention symbols, files, or commits that exist only on a feature branch. + +In-flight feature branches that materially change the surface (new module, +new commands, new stubs) get a single file under `wip/.md`. On +merge to `master`, that file is deleted and any surviving content is folded +into the stable layers. + +Current WIP: `wip/feat-ai.md` (cproof branch `feat/ai`). + +## Status + +Layout is being authored on `draft/split-context`. The single +`file-structure.md` (still present on `feat/add-context`) is being decomposed +into the structure above; once the split is complete, `file-structure.md` is +deleted. diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..b6b83d3 --- /dev/null +++ b/SKILL.md @@ -0,0 +1,35 @@ +--- +name: cproof-context +description: Architectural and conventional context for the cproof XMPP terminal client. Load INDEX.md first; pull individual files on demand based on the task at hand. +--- + +# cproof skill entry + +cproof is a terminal-based XMPP client in C, fork of Profanity (renaming WIP). +Build: autotools + GLib. Tests: cmocka (unit) + custom runner (functional) + +custom benches. + +## How to use this context + +1. Read `INDEX.md` for the full file map and churn labels. +2. For any non-trivial task, pull only the files relevant to it: + - Touching commands? → `patterns/commands.md`, `patterns/autocomplete.md`, + `playbooks/add-command.md`. + - Touching tests? → `testing/unit-tests.md`, `testing/stubs.md`, + `playbooks/add-test.md`. + - Touching XMPP? → `patterns/xmpp.md`. + - Building? → `build/docker.md` (canonical) or `build/local.md`. +3. Always consult `gotchas.md` before finishing — it lists known pitfalls. +4. If the task is on a feature branch listed under `wip/`, load the matching + WIP file as well — the stable layers describe `master` only. +5. Treat path/identifier references in this context as claims to verify + against cproof HEAD before acting on them. + +## Hard rules + +- Build inside Docker (`Dockerfile.debian`). Do not run `autogen.sh` / + `./configure` / `make` on the host. +- Do not mix architecture, patterns, status, and gotchas in a single doc — each + has its own home. +- Do not duplicate what `ls` / `grep` would answer cheaper. This context exists + for invariants, decisions, and "why". diff --git a/architecture/data-flow.md b/architecture/data-flow.md new file mode 100644 index 0000000..28bd075 --- /dev/null +++ b/architecture/data-flow.md @@ -0,0 +1,76 @@ +# Data flow + +High-level paths through the codebase. Use this when reasoning about where a +change should land or where a bug originates. + +## User input → action + +``` +keypress + → src/ui/inputwin.c (line editor, key dispatch) + → src/command/cmd_ac.c (autocomplete on Tab) + → src/command/cmd_defs.c (parse / lookup) + → src/command/cmd_funcs.c (handler: cmd_) + → side effects: src/xmpp/, src/config/, src/ui/, src/event/ +``` + +- Tab triggers autocomplete via `cmd_ac.c` lookup in `ac_funcs` hash table + (key: `/cmd`, value: per-command `__autocomplete` function). +- Enter parses input via `parse_args()` (`src/tools/parser.c`), looks up the + `Command*` via `cmd_get()` (`src/command/cmd_defs.c`), invokes the handler + declared with `CMD_MAINFUNC` or one of the `CMD_SUBFUNCS`. + +## Outgoing chat message + +``` +cmd_msg / typing-into-chatwin + → src/xmpp/message.c (build message stanza) + → src/xmpp/stanza.c (assemble XML) + → libstrophe (send) + → src/chatlog.c (persist locally) + → src/ui/chatwin.c (echo into window) +``` + +Encryption injects between message build and send: + +- OMEMO via `src/xmpp/omemo.c` → `src/omemo/omemo.c` (libsignal-protocol). +- OTR via `src/otr/otr.c` (libotr). +- PGP / OX via `src/pgp/gpg.c` / `src/pgp/ox.c` → `src/xmpp/ox.c`. + +## Incoming stanza + +``` +libstrophe callback + → src/xmpp/{message,presence,iq,muc,roster}.c (parse, validate) + → src/event/server_events.c (sv_ev_*) + → src/ui/{chatwin,mucwin,console,...}.c (render) + → src/chatlog.c (persist) +``` + +`server_events.c` is the chokepoint — every inbound XMPP event flows through a +`sv_ev_*` function. UI updates and persistence fan out from there. + +## Client-initiated events + +`src/event/client_events.c` mirrors `server_events.c` for events that +originate locally (UI actions, command-driven state changes) but need the +same downstream fan-out. + +## Connection state + +- `src/xmpp/connection.c` owns the `jabber_conn_status_t` (declared in + `connection.h`). +- `connection_get_status()` returns the current state. Many code paths gate + on `JABBER_CONNECTED` — see `patterns/xmpp.md`. +- Tests must `will_return(connection_get_status, JABBER_CONNECTED)` (or the + desired state) before exercising any code path that consults it. See + `gotchas.md`. + +## Plugin hook points + +Plugin callbacks (`src/plugins/callbacks.c`) sit on: +- Pre/post message send and receive (around `src/xmpp/message.c` flow). +- Connection lifecycle (around `src/xmpp/session.c`). +- Window-switch events (around `src/ui/window_list.c`). + +See `patterns/plugins.md`. diff --git a/architecture/overview.md b/architecture/overview.md new file mode 100644 index 0000000..a2d2dc9 --- /dev/null +++ b/architecture/overview.md @@ -0,0 +1,59 @@ +# Overview + +## What cproof is + +- Terminal-based XMPP (chat) client written in C. +- Fork of [Profanity](https://github.com/profanity-im/profanity); rename to + cproof is **work in progress** — many internal symbols, copyright headers, + and identifiers still say "Profanity". +- Heavy GLib usage (strings, lists, hash tables, key files). +- Optional integrations: OMEMO (libsignal-protocol), OTR (libotr), PGP/OX + (gpgme), Python plugins, libnotify, libgcrypt, libreadline, ncurses. + +## Build system + +- Autotools: `configure.ac` + a top-level `Makefile.am`. No CMake. +- `autogen.sh` regenerates the configure script. +- `configure-debug` is a thin helper that invokes `./configure` with + debug-friendly flags. +- Canonical build environment: Docker (`Dockerfile.debian` at repo root). + See `build/docker.md`. + +## Test framework + +- **Unit tests:** cmocka, run via `make check`. Live in `tests/unittests/`. +- **Functional tests:** custom runner in `tests/functionaltests/` against a + real running cproof instance. +- **Benches:** custom runners in `tests/bench//` for database / + flatfile / long-message workloads. +- See `architecture/test-map.md` and the `testing/` directory for detail. + +## High-level module split + +Source lives under `src/` in module directories — see +`architecture/source-map.md` for the full table. Major chunks: + +- **Command layer** (`src/command/`) — `/foo`-style commands the user types. +- **XMPP layer** (`src/xmpp/`) — protocol, sessions, stanza building. +- **UI layer** (`src/ui/`) — windows, buffers, status bar, input. +- **Event layer** (`src/event/`) — bridges between XMPP and UI / commands. +- **Encryption** (`src/omemo/`, `src/otr/`, `src/pgp/`) — three independent + encryption stacks. +- **Plugins** (`src/plugins/`) — C and Python plugin APIs. +- **Tools** (`src/tools/`) — generic utilities (parser, autocomplete, HTTP, + clipboard, editor). + +Active feature branches (e.g. `feat/ai`, which adds `src/ai/`) are not +described in the stable architecture files. See `wip/` for branch-specific +notes. + +## Entry points + +| File | Role | +|---|---| +| `src/main.c` | Process entry. | +| `src/profanity.c` | Application init/shutdown lifecycle. | +| `src/common.h` | Project-wide macros (`auto_*`, `FREE_SET_NULL`, `ARRAY_SIZE`). | +| `src/database.c` / `src/database.h` | Database (SQLite + flatfile) abstraction. | +| `src/chatlog.c` | Chat history persistence. | +| `src/log.c` | Application log. | diff --git a/architecture/source-map.md b/architecture/source-map.md new file mode 100644 index 0000000..bd18ab4 --- /dev/null +++ b/architecture/source-map.md @@ -0,0 +1,38 @@ +# Source map + +## `src/` module directories + +| Path | Purpose | Key files | +|---|---|---| +| `src/command/` | `/command` definition, dispatch, autocompletion. | `cmd_defs.c/h`, `cmd_funcs.c/h`, `cmd_ac.c/h` | +| `src/config/` | Persistent configuration: accounts, prefs, themes, colors, CA file, scripts. | `accounts.c/h`, `preferences.c/h`, `theme.c/h`, `color.c/h`, `cafile.c/h`, `scripts.c/h`, `tlscerts.c/h`, `conflists.c/h`, `files.c/h`, `account.c/h` | +| `src/event/` | Mediates between XMPP layer and UI/commands. | `client_events.c/h`, `server_events.c/h`, `common.c/h` | +| `src/omemo/` | OMEMO encryption (libsignal-protocol). | `omemo.c/h`, `crypto.c/h`, `store.c/h` | +| `src/otr/` | OTR encryption (libotr v4). | `otr.c/h`, `otrlib.h`, `otrlibv4.c` | +| `src/pgp/` | OpenPGP / OX (gpgme). | `gpg.c/h`, `ox.c/h` | +| `src/plugins/` | C and Python plugin support. | `plugins.c/h`, `c_plugins.c/h`, `python_plugins.c/h`, `c_api.c/h`, `python_api.c/h`, `api.c/h`, `profapi.c/h`, `callbacks.c/h`, `autocompleters.c/h`, `disco.c/h`, `settings.c/h`, `themes.c/h` | +| `src/tools/` | Generic utilities. | `parser.c/h`, `autocomplete.c/h`, `clipboard.c/h`, `editor.c/h`, `http_*.c/h`, `aesgcm_download.c/h`, `bookmark_ignore.c/h`, `plugin_download.c/h` | +| `src/ui/` | Windows, buffers, status bar, input. | `window.c/h`, `window_list.c/h`, `chatwin.c`, `mucwin.c`, `console.c`, `confwin.c`, `privwin.c`, `xmlwin.c`, `vcardwin.c`, `rosterwin.c`, `occupantswin.c`, `inputwin.c/h`, `statusbar.c/h`, `titlebar.c/h`, `tray.c/h`, `screen.c/h`, `notifier.c`, `core.c`, `buffer.c/h`, `ui.h`, `win_types.h` | +| `src/xmpp/` | XMPP protocol layer. | `connection.c/h`, `session.c/h`, `roster.c/h`, `roster_list.c/h`, `muc.c/h`, `presence.c/h`, `message.c/h`, `iq.c/h`, `stanza.c/h`, `vcard.c/h`, `vcard_funcs.h`, `jid.c/h`, `capabilities.c/h`, `omemo.c/h`, `ox.c/h`, `blocking.c/h`, `bookmark.c/h`, `chat_session.c/h`, `chat_state.c/h`, `avatar.c/h`, `contact.c/h`, `form.c/h`, `resource.c/h` | + +## Top-level files in `src/` + +| File | Role | +|---|---| +| `main.c` | Process entry. | +| `profanity.c` / `profanity.h` | Application init/shutdown. | +| `common.h` / `common.c` | Project-wide macros and small helpers. | +| `database.c` / `database.h` | Database facade (SQLite + flatfile). | +| `chatlog.c` / `chatlog.h` | Chat log writer/reader. | +| `log.c` / `log.h` | Application log. | +| `config.h.in` | autoconf-generated config header (do not edit `config.h`). | +| `gitversion.h.in` | Generated `gitversion.h` carrying build-time SHA. | + +## Naming notes + +- The fork-rename (Profanity → cproof) is incomplete. Many symbols, file + paths, and headers still carry "Profanity" / "prof_" prefixes. Treat them as + current and authoritative; do not rename opportunistically. +- Encryption modules under `src/omemo/`, `src/otr/`, `src/pgp/` use module- + internal helpers; the XMPP-side bridges live in `src/xmpp/omemo.c` and + `src/xmpp/ox.c` (separate files with the same names — be precise about path). diff --git a/architecture/test-map.md b/architecture/test-map.md new file mode 100644 index 0000000..0f836e0 --- /dev/null +++ b/architecture/test-map.md @@ -0,0 +1,69 @@ +# Test map + +## Top-level test directories + +| Path | Runner | Purpose | +|---|---|---| +| `tests/unittests/` | cmocka via `make check` | Unit tests with stubbed dependencies. | +| `tests/functionaltests/` | custom binary, runs against a real cproof | End-to-end behaviour tests. | +| `tests/bench/` | custom benches, one binary per runner | Database / flatfile / long-message benchmarks. | +| `tests/prof_cmocka.h` | — | Thin cmocka wrapper used by all unit tests. | + +## `tests/unittests/` layout + +- `unittests.c` — single test runner. Registers every test via + `cmocka_unit_test()` / `cmocka_unit_test_setup_teardown()`. +- `helpers.c` / `helpers.h` — shared test utilities. +- `test_.c` + `test_.h` — paired test files. The header + declares each test function; the runner includes the header to register it. +- Per-module subdirectories carry **stubs** for that module's public symbols: + +| Stub directory | Stubs for | +|---|---| +| `tests/unittests/chatlog/` | `src/chatlog.c` | +| `tests/unittests/command/` | `src/command/` | +| `tests/unittests/config/` | `src/config/` (accounts, cafile, etc.) | +| `tests/unittests/database/` | `src/database.c` | +| `tests/unittests/event/` | `src/event/` | +| `tests/unittests/log/` | `src/log.c` | +| `tests/unittests/omemo/` | `src/omemo/` | +| `tests/unittests/otr/` | `src/otr/` | +| `tests/unittests/pgp/` | `src/pgp/` | +| `tests/unittests/plugins/` | `src/plugins/` | +| `tests/unittests/tools/` | `src/tools/` | +| `tests/unittests/ui/` | `src/ui/` | +| `tests/unittests/xmpp/` | `src/xmpp/` | +| `tests/unittests/unittests/` | top-level utility stubs | + +Branch-specific stubs (e.g. `tests/unittests/ai/` on `feat/ai`) are not +listed here — see `wip/` for branch notes. + +Each stub file is named `stub_.c`. See `testing/stubs.md` for when +to add or extend a stub. + +## `tests/functionaltests/` layout + +- `functionaltests.c` — runner. +- `proftest.c` / `proftest.h` — fixture: spawns cproof under PTY, talks XMPP. +- `test_.c` — feature suites: connect, disconnect, message, presence, + carbons, roster, history, ping, autoping, lastactivity, software, disco, + receipts, rooms, muc, chat_session, export_import. + +Functional tests require: +- A test XMPP server reachable from the test harness. +- A pre-baked `.profrc` (see recent perf work). +- See `testing/functional-tests.md` for prerequisites and runner flags. + +## `tests/bench/` layout + +Each runner is a separate directory + binary: + +| Runner | Measures | +|---|---| +| `bench_runner` | Combined runner for general bench scenarios. | +| `bench_export_import` | Database export/import throughput. | +| `bench_failure_modes` | Flatfile parser failure / recovery paths. | +| `bench_long_messages` | Flatfile parser with very long messages. | +| `gen_history` | Generates synthetic chat history for benches. | + +See `testing/bench.md` for invocation and interpretation. diff --git a/build/ci.md b/build/ci.md new file mode 100644 index 0000000..3aed4c1 --- /dev/null +++ b/build/ci.md @@ -0,0 +1,55 @@ +# CI + +## Workflows + +`.github/workflows/`: + +| Workflow | Purpose | +|---|---| +| `ci-code.yml` | Build + unit tests across the Linux distro matrix. | +| `ci-api-docs.yml` | API/plugin docs build check. | + +Plus an OpenBSD pipeline at `.builds/openbsd.yml` (sourcehut-style builds). + +## Distro matrix + +The code CI runs across the Dockerfiles at repo root: Debian, Ubuntu, +Fedora, Tumbleweed, Arch. Each job builds the matching image, mounts the +checkout, and runs `autogen.sh && configure && make && make check`. + +## Log naming + +CI logs in this project are named: + +``` +ci-code-Linux (arch)-NNNN.log +``` + +— where `arch` is the distro flavour (debian, ubuntu, fedora, tumbleweed, +arch) and `NNNN` is the run number. Use the distro name to pick the right +log when triaging matrix failures. + +## Known matrix-specific notes + +- **Arch (`Dockerfile.arch`):** Pikaur in the image was hit by a duplicated-flag + failure (cf. cproof commit `0722dc9e3`). If `Dockerfile.arch` changes, + re-check the Pikaur invocation. +- **OpenBSD (`.builds/openbsd.yml`):** runs on sourcehut, not GitHub. Failures + there don't appear in the GitHub PR check list. + +## Reproducing a CI failure locally + +1. Identify the failing distro from the log name. +2. Build the matching Dockerfile locally — see `build/docker.md`. +3. Run the same `autogen && configure && make && make check` chain inside the + container. +4. Compare versions of any drifting dependency between the image and the CI + image (`apt list --installed` or equivalent inside both). + +## What CI does not run + +- Functional tests (`tests/functionaltests/`) — require a test XMPP server. +- Benches (`tests/bench/`) — not part of CI. + +If you need either, run them locally inside the Docker image — see +`testing/functional-tests.md` and `testing/bench.md`. diff --git a/build/docker.md b/build/docker.md new file mode 100644 index 0000000..cd6d8a7 --- /dev/null +++ b/build/docker.md @@ -0,0 +1,75 @@ +# Docker build + +Canonical build environment. **Use this; do not build on the host.** + +## Dockerfiles + +All at repo root: + +| File | Base | Purpose | +|---|---|---| +| `Dockerfile.debian` | `debian:testing` | **Canonical.** Mirrors CI for Linux. | +| `Dockerfile.ubuntu` | `ubuntu` | Alternate Linux for compatibility checks. | +| `Dockerfile.fedora` | `fedora` | Fedora compatibility. | +| `Dockerfile.tumbleweed` | `opensuse/tumbleweed` | openSUSE compatibility. | +| `Dockerfile.arch` | `archlinux` | Arch CI matrix. | + +Default: `Dockerfile.debian` for local dev, build, and `make check`. + +## Build the image + +```sh +docker build -t cproof-debian -f Dockerfile.debian . +``` + +`Dockerfile.debian` installs the autotools toolchain plus all optional +dependencies (cmocka, libsignal-protocol-c, libotr5, gpgme, libcurl, +libmicrohttpd, libnotify, ncurses, GLib, libgcrypt, libreadline, libssl, +Python). Treat its `apt-get install` block as the authoritative dependency +list. + +## Build cproof inside the container + +Bind-mount the working tree and run the autotools sequence: + +```sh +docker run --rm -it \ + -v "$PWD":/src -w /src \ + cproof-debian \ + bash -c './autogen.sh && ./configure && make -j$(nproc)' +``` + +For tests: + +```sh +docker run --rm -it \ + -v "$PWD":/src -w /src \ + cproof-debian \ + bash -c './autogen.sh && ./configure && make -j$(nproc) check' +``` + +`ccache` is enabled in the image (`CC="ccache gcc"`) — share a ccache volume +between runs for faster rebuilds: + +```sh +-v "$HOME/.ccache":/root/.ccache +``` + +## Why Docker + +- Pinned OS + package set; the same toolchain CI uses. +- Host glib / openssl / libstrophe drift cannot mask or invent failures. +- Encryption-stack dependencies (libsignal-protocol-c especially) are + awkward on host package managers; the image already has them. + +## When to use a non-Debian image + +- Reproducing a CI failure that only triggers on Arch / Fedora / Ubuntu / + Tumbleweed. Use the matching Dockerfile, same bind-mount pattern. +- Otherwise: stick with Debian. + +## Hard rule + +Do not run `./autogen.sh`, `./configure`, or `make` on the host shell unless +you have an explicit reason and have verified the host toolchain matches the +image. CI is the source of truth; CI uses Debian. diff --git a/build/local.md b/build/local.md new file mode 100644 index 0000000..a670dd7 --- /dev/null +++ b/build/local.md @@ -0,0 +1,68 @@ +# Local build + +Reference for `autogen.sh` / `configure` / `make` flags. **Do not run these +on the host as a default workflow** — see `build/docker.md`. This file +documents what the canonical Docker build does internally. + +## Toolchain + +- autotools: `autoconf`, `automake`, `libtool`, `autoconf-archive`. +- C compiler: `gcc` (CI) or `clang`. +- GLib 2, ncursesw, OpenSSL, libstrophe, libreadline. +- Optional: libcmocka (tests), libsignal-protocol-c (OMEMO), libotr5 (OTR), + gpgme (PGP), libcurl (HTTP), libmicrohttpd (HTTP server features), + libnotify, libgcrypt, Python (plugins). + +The full Debian package list is in `Dockerfile.debian` — treat that as the +authoritative dependency list. + +## Sequence + +```sh +./autogen.sh # generate configure from configure.ac +./configure [opts] +make -jN +``` + +`autogen.sh` runs `aclocal`, `autoheader`, `automake`, `autoconf`, `libtoolize`. + +## Useful `./configure` flags + +| Flag | Effect | +|---|---| +| `--enable-debug` | Debug build (the `configure-debug` script wraps this). | +| `--disable-omemo` / `--disable-otr` / `--disable-pgp` | Drop encryption stack. | +| `--disable-python-plugins` | C-only plugins. | +| `--disable-notifications` | No libnotify. | +| `--prefix=` | Install prefix. | + +`./configure --help` prints the full set; flag names track upstream Profanity +closely. + +## `configure-debug` + +Convenience script at repo root. Roughly: + +```sh +./autogen.sh +./configure --enable-debug --prefix=$(pwd)/install +``` + +Tweak in-script if you want different debug knobs. + +## `make` targets + +| Target | What | +|---|---| +| `make` | Builds the `profanity` binary (and `cproof` symlink, if rename has progressed to it). | +| `make check` | Builds and runs the unit-test suite. | +| `make clean` | Wipes object files. | +| `make distclean` | Wipes generated configure artefacts too. | + +## Why this is host-unfriendly + +- Library version drift between the host (a workstation) and the canonical + Debian build means the host build can succeed while CI fails (or vice + versa) — bugs that don't reproduce in either env are very expensive. +- `Dockerfile.debian` pins the OS and package set used by CI. Mirror it + locally instead of fighting the host. diff --git a/file-structure.md b/file-structure.md deleted file mode 100644 index 5d4b3a6..0000000 --- a/file-structure.md +++ /dev/null @@ -1,112 +0,0 @@ -## CProof Project Structure (for skill context) - -**CProof** is a terminal-based XMPP (chat) client written in C. It uses autotools for building and cmocka for unit testing. CProof is a fork of Profanity, renaming is WIP. - -### Build System -- `configure.ac` / `Makefile.am` - Autotools build configuration -- `autogen.sh` - Script to generate configure scripts -- `configure-debug` - Debug build helper -- `tests/prof_cmocka.h` - Test framework header (cmocka wrapper) - -### Source Directory (`src/`) - -| Directory | Purpose | -|-----------|---------| -| `src/ai/` | AI client implementation (`ai_client.c/h`) - OpenAI/Perplexity provider support | -| `src/command/` | Command handling (`cmd_ac.c/h` - autocomplete, `cmd_defs.c/h` - command definitions, `cmd_funcs.c/h` - command implementations) | -| `src/config/` | Configuration management (accounts, preferences, themes, colors, CA certs, scripts) | -| `src/event/` | Event handling (client events, server events, common) | -| `src/omemo/` | OMEMO encryption (crypto, store) | -| `src/otr/` | OTR encryption (`otr.c/h`, `otrlib.c/h`, `otrlibv4.c`) | -| `src/pgp/` | PGP/OpenPGP integration (`gpg.c/h`, `ox.c/h`) | -| `src/plugins/` | Plugin system (C API, Python API, autocompleters, callbacks, disco, settings, themes) | -| `src/tools/` | Utility functions (autocomplete, parser, clipboard, editor, HTTP download/upload, bookmark ignore) | -| `src/ui/` | User interface (buffers, windows, input, chat/muc/console windows, statusbar, tray, window management) | -| `src/xmpp/` | XMPP protocol layer (connection, roster, muc, presence, message, stanza, vcard, session, JID, capabilities, OMEMO, Ox, blocking, bookmark, chat state, avatar) | - -**Key files at `src/` root:** -- `main.c` - Entry point -- `profanity.c` - Application initialization/shutdown -- `common.h` - Common types/macros (including `auto_gchar` for memory management) -- `database.c` - Database layer -- `chatlog.c` - Chat log handling -- `log.c` - Logging - -### Tests Directory (`tests/`) - -| Directory | Purpose | -|-----------|---------| -| `tests/functionaltests/` | Integration tests (requires running CProof instance) | -| `tests/unittests/` | Unit tests (cmocka-based, run via `make check`) | - -**Unit test structure:** -- `tests/unittests/unittests.c` - Test runner (registers all tests) -- `tests/unittests/test_*.c/h` - Test files paired with headers -- `tests/unittests/helpers.c/h` - Test utilities -- `tests/unittests//stub_*.c` - Stub implementations for dependencies - -**Stub directories:** -- `unittests/ai/stub_ai_client.c` - AI client stubs -- `unittests/config/stub_accounts.c`, `stub_cafile.c` -- `unittests/database/stub_database.c` -- `unittests/log/stub_log.c` -- `unittests/omemo/stub_omemo.c` -- `unittests/otr/stub_otr.c` -- `unittests/pgp/stub_gpg.c`, `stub_ox.c` -- `unittests/tools/stub_*.c` -- `unittests/ui/stub_ai.c`, `stub_ui.c`, `stub_vcardwin.c` -- `unittests/xmpp/stub_*.c` - -### Key Patterns - -**Memory Management:** -- Uses `auto_gchar` macro (from `common.h`) for automatic GString/GString cleanup -- `g_free()`, `g_list_free_full()`, `g_hash_table_destroy()` for manual cleanup -- `FREE_SET_NULL()` / `GFREE_SET_NULL()` macros - -**Autocomplete System:** -- `Autocomplete` type (GList-based) defined in `tools/autocomplete.h` -- Functions: `autocomplete_new()`, `autocomplete_add()`, `autocomplete_complete()`, `autocomplete_free()` -- `autocomplete_param_with_ac()` - Complete using an Autocomplete object -- `autocomplete_param_with_func()` - Complete using a callback function -- `_autocomplete_param_common()` - Internal helper - -**Command System:** -- Commands defined in `cmd_defs.c` with `CMD_MAINFUNC()` and `CMD_TAGS()` macros -- Autocomplete registered in `cmd_ac.c` via `g_hash_table_insert(ac_funcs, "/cmd", _cmd_autocomplete)` -- `parse_args()` from `tools/parser.c` splits input into args array - -**Testing Patterns:** -- Use `will_return(func, value)` before calling `func()` to mock return values -- Use `expect_string()` for struct field verification -- Setup/teardown via `cmocka_unit_test_setup_teardown(test_func, setup, teardown)`, centralized in unittests.c -- Mock `connection_get_status()` with `will_return(connection_get_status, JABBER_CONNECTED)` if connection is needed - -### Current Work Context - -The `/ai` command autocomplete was recently added with: -- `ai_subcommands_ac` - Top-level subcommands: set, remove, start, clear, correct, providers -- `ai_set_subcommands_ac` - Set subcommands: provider, token, org -- `ai_remove_subcommands_ac` - Remove subcommands: provider -- `ai_providers_find()` - Provider name autocomplete callback (stateless, no `_last_provider_match`) -- Tests in `tests/unittests/test_ai_client.c` and `tests/unittests/test_ai_client.h` - -## Full List of `auto_` Macros (Automatic Memory Management) - -These macros use GCC's `__cleanup__` attribute to automatically free/close resources when they go out of scope. - -| Macro | Type | Cleanup Function | Description | -|-------|------|-----------------|-------------| -| `auto_gchar` | `gchar*` | `auto_free_gchar()` | Automatically frees a GLib character string | -| `auto_gcharv` | `gchar**` | `auto_free_gcharv()` | Automatically frees a GLib character string array (e.g., from `g_strsplit`) | -| `auto_char` | `char*` | `auto_free_char()` | Automatically frees a standard C string (from `strdup`) | -| `auto_guchar` | `guchar*` | `auto_free_guchar()` | Automatically frees a GLib unsigned char string (e.g., base64 decoded) | -| `auto_gfd` | `gint*` | `auto_close_gfd()` | Automatically closes a GFileDescriptor | -| `auto_FILE` | `FILE*` | `auto_close_FILE()` | Automatically closes a FILE stream | -| `auto_jid` | `Jid*` | `jid_auto_destroy()` | Automatically frees a Jid struct (defined only in `src/xmpp/jid.h`) | - -**Example usage:** -```c -auto_gchar gchar* myString = g_strdup("Hello, world!"); -// myString is automatically freed when scope exits -``` diff --git a/gotchas.md b/gotchas.md new file mode 100644 index 0000000..67371a8 --- /dev/null +++ b/gotchas.md @@ -0,0 +1,114 @@ +# Gotchas + +Append-only log of known pitfalls. One entry per pitfall. Format: + +``` +## YYYY-MM-DD — + + +``` + +Scan this file before finishing any non-trivial task. + +--- + +## 2026-04-30 — `g_strsplit` result must use `g_strfreev` + +`g_strsplit` (and `g_strdupv`) return a `gchar**` whose elements are +individually GLib-allocated. Free with `g_strfreev`, **not** `g_free`. The +latter compiles fine and silently leaks the inner strings. + +The cleanup macro is `auto_gcharv` (declared in `src/common.h`): + +```c +auto_gcharv gchar** parts = g_strsplit(line, " ", -1); +``` + +Mixing `g_free` on a `gchar**` is the most common allocator-mismatch bug in +this codebase. + +--- + +## 2026-04-30 — `JABBER_CONNECTED` must be queued in unit tests + +Most XMPP-touching code paths gate on `connection_get_status()`. The unit- +test stub returns `mock()`, so every test reaching that gate must: + +```c +will_return(connection_get_status, JABBER_CONNECTED); +// or +will_return(connection_get_status, JABBER_DISCONNECTED); +``` + +Forgetting this aborts cmocka with a "missing mock value" error. If you see +that error, it's almost always `connection_get_status` you forgot. + +--- + +## 2026-04-30 — Autocomplete callbacks must be stateless + +Some older callbacks keep "last match" state in a file-static `_last_*_match` +variable. Do not copy that pattern. A clean canonical example is +`roster_contact_autocomplete` (`src/xmpp/roster_list.c`) — it delegates +straight to `autocomplete_complete` against a roster-owned `Autocomplete` +object and keeps no callback-local state. + +Stateful callbacks misbehave under shift-tab cycling and break when two +completers run concurrently in a layered command (e.g. `/foo set bar ` +followed by `/foo set baz `). + +--- + +## 2026-04-30 — CWE-134 format-string discipline + +Never pass user-controlled or arbitrary strings as the format argument to +`printf`-family or GLib formatted-print functions. A recent compiler- +hardening pass (cf. cproof commit `9ec01fa8c`) audited and tightened these +sites; do not regress. + +Right: + +```c +cons_show("%s", user_text); +``` + +Wrong: + +```c +cons_show(user_text); +``` + +The compiler does not always warn, especially across helper layers. Enable +and respect `-Wformat -Wformat-security` warnings in any build you trust. + +--- + +## 2026-04-30 — Build inside Docker, not on the host + +Library version drift between a developer host and `Dockerfile.debian` +(GLib, OpenSSL, libstrophe in particular) can mask or invent failures. CI +is the source of truth; CI builds inside the Debian image. See +`build/docker.md`. + +If you absolutely must build on the host, mirror the package set in +`Dockerfile.debian` exactly and pin versions — you will spend more time +chasing version drift than the Docker build would have cost. + +--- + +## 2026-04-30 — Beware of "Profanity" vs "cproof" naming + +The fork-rename is incomplete. Many files, symbols, copyright headers, and +config keys still say `Profanity` / `prof_` / `profanity`. These are +authoritative as-is; do **not** rename them opportunistically alongside +unrelated changes. Rename work, when it happens, will be a dedicated PR. + +--- + +## 2026-04-30 — Encryption disable builds must compile + +Each encryption stack (`HAVE_OMEMO`, `HAVE_LIBOTR`, `HAVE_LIBGPGME`) can be +turned off at `configure` time. New code that calls into these modules +must wrap calls in the matching `#ifdef` and provide a graceful fallback +in the `#else`. CI does not always test every disable combination — verify +locally with at least the off-build for each stack you touched. diff --git a/patterns/autocomplete.md b/patterns/autocomplete.md new file mode 100644 index 0000000..1b14e56 --- /dev/null +++ b/patterns/autocomplete.md @@ -0,0 +1,97 @@ +# Autocomplete + +## The `Autocomplete` type + +`src/tools/autocomplete.h` defines an opaque `Autocomplete` (GList-backed +sorted set with a stateful "current iterator" for cycling through matches). + +| Function | Purpose | +|---|---| +| `autocomplete_new()` | Create empty AC. | +| `autocomplete_clear(ac)` | Drop all entries (keep object). | +| `autocomplete_free(ac)` | Destroy. | +| `autocomplete_add(ac, item)` | Insert sorted. | +| `autocomplete_add_unsorted(ac, item, reversed)` | Append, no sort. | +| `autocomplete_add_all(ac, items)` | Bulk add from `char**`. | +| `autocomplete_update(ac, items)` | Clear + add_all. | +| `autocomplete_remove(ac, item)` | Drop one. | +| `autocomplete_remove_all(ac, items)` | Drop many. | +| `autocomplete_complete(ac, search, quote, previous)` | Cycle to next/previous match. | +| `autocomplete_reset(ac)` | Reset cycle position. | +| `autocomplete_contains(ac, value)` | Membership. | + +## Two completion flavours used in commands + +In `src/command/cmd_ac.c`: + +```c +autocomplete_param_with_ac(input, "/cmd sub", static_ac, quote, previous); +autocomplete_param_with_func(input, "/cmd sub", callback, previous, ctx); +``` + +| When to use | Choose | +|---|---| +| Fixed token set known at startup. | `autocomplete_param_with_ac` + a static `Autocomplete` (allocated in `cmd_ac_init`, freed in `cmd_ac_uninit`). | +| Token set is dynamic / queried from state. | `autocomplete_param_with_func` + a callback `char* fn(const char* search, gboolean previous, void* ctx)`. | + +## Static Autocompletes + +Pattern (from `cmd_ac.c`): + +```c +// File-scope: +static Autocomplete account_ac; + +// In cmd_ac_init(): +account_ac = autocomplete_new(); +autocomplete_add(account_ac, "list"); +autocomplete_add(account_ac, "show"); +autocomplete_add(account_ac, "add"); +// ... + +// Registered with: +g_hash_table_insert(ac_funcs, "/account", _account_autocomplete); +``` + +Static `Autocomplete` instances are added to a file-scope free-list array +near the top of `cmd_ac.c` so `cmd_ac_uninit` can call `autocomplete_free` +on each at shutdown. **Add new static ACs to that free-list.** + +## Function-callback completers + +```c +char* roster_contact_autocomplete(const char* const search_str, + gboolean previous, void* context); +``` + +- Stateless across invocations — no module-level `_last_match` globals. +- Returns a freshly-allocated `char*` (caller frees), or `NULL` when no more + matches. +- `previous` cycles backward (Shift+Tab). +- `context` is whatever was passed in `autocomplete_param_with_func`'s last + arg (often `NULL`). + +`roster_contact_autocomplete` (`src/xmpp/roster_list.c`) is a clean +canonical example: it delegates straight to `autocomplete_complete` against +a roster-owned `Autocomplete` object — no callback-local state. + +**Avoid the legacy "store previous match in a static" pattern.** Some older +completers do it; do not copy them. Keeping callbacks stateless makes +shift-tab cycling and concurrent completers safe. + +## Per-command dispatcher + +Every command's autocomplete is a single static function in `cmd_ac.c`: + +```c +static char* _account_autocomplete(ProfWin* window, const char* const input, gboolean previous); +``` + +Registered in `cmd_ac_init`: + +```c +g_hash_table_insert(ac_funcs, "/account", _account_autocomplete); +``` + +The dispatcher inspects the input prefix and picks the right +`autocomplete_param_with_*` call. diff --git a/patterns/commands.md b/patterns/commands.md new file mode 100644 index 0000000..32a60d9 --- /dev/null +++ b/patterns/commands.md @@ -0,0 +1,109 @@ +# Commands + +## Three files, three concerns + +| File | Holds | +|---|---| +| `src/command/cmd_defs.c` (`cmd_defs.h`) | The big static array of `Command` records — every `/foo` is one entry. | +| `src/command/cmd_funcs.c` (`cmd_funcs.h`) | Handler implementations: `gboolean cmd_(ProfWin*, const char* const, gchar**)`. | +| `src/command/cmd_ac.c` (`cmd_ac.h`) | Per-command autocomplete logic and registration. | + +## Anatomy of a command record + +Defined inline in `cmd_defs.c` using uppercase macros (declared in `cmd_defs.c`): + +```c +{ CMD_PREAMBLE("/caps", + parse_args, 0, 1, NULL) + CMD_MAINFUNC(cmd_caps) + CMD_TAGS( + CMD_TAG_DISCOVERY, + CMD_TAG_CHAT, + CMD_TAG_GROUPCHAT) + CMD_SYN( + "/caps", + "/caps |") + CMD_DESC("...") + CMD_ARGS( + { "", "..." }, + { "", "..." }) + CMD_EXAMPLES( + "/caps user@host/res") +} +``` + +| Macro | Role | +|---|---| +| `CMD_PREAMBLE(name, parse_fn, min, max, pre_hook)` | Command name, parser, arg count bounds, optional pre-hook. | +| `CMD_MAINFUNC(fn)` | Single handler for the whole command. | +| `CMD_SUBFUNCS({"sub", fn}, ...)` | Dispatch by first argument. Use **instead of** `CMD_MAINFUNC` when the command has subcommands (e.g. `/status get`, `/status set`). | +| `CMD_TAGS(...)` | One or more `CMD_TAG_*` (UI grouping, search). | +| `CMD_SYN(...)` | Synopsis lines for `/help `. | +| `CMD_DESC(...)` | Long description. | +| `CMD_ARGS({"name", "desc"}, ...)` | Argument table for help. | +| `CMD_EXAMPLES(...)` | Examples shown in help. | + +The full set of `CMD_*` macro definitions is at the top of `cmd_defs.c` — +read them when authoring a new command. + +## Argument parsing + +`parse_args()` lives in `src/tools/parser.c`: + +```c +gchar** parse_args(const char* const inp, int min, int max, gboolean* result); +``` + +- Splits on whitespace, honours quoted segments. +- Returns `NULL` on parse failure (sets `*result = FALSE`). +- The handler receives the result as `gchar** args` — free is owned by the + caller in `cmd_defs.c`, the handler does **not** free. + +Parser variants used in `CMD_PREAMBLE`: +- `parse_args` — standard quoted-aware split. +- `parse_args_with_freetext` — last argument is the rest of the line, no + splitting (used by `/msg`, `/me`, etc.). +- `parse_args_as_one` — concatenates remaining tokens into a single string. + +## Handler signature + +```c +gboolean cmd_foo(ProfWin* window, const char* const command, gchar** args); +``` + +Return `TRUE` on success, `FALSE` to print bad-usage. `command` is the literal +`/foo` typed (useful when the same handler is registered under multiple names). + +## Connection-state gates + +Most commands that touch XMPP must verify connection state first: + +```c +if (connection_get_status() != JABBER_CONNECTED) { + cons_show("You are not currently connected."); + return TRUE; +} +``` + +`cons_bad_cmd_usage(cmd)` is the canonical "wrong arguments" path; pass the +literal command name. + +## Adding a new command + +See `playbooks/add-command.md` for the full walkthrough. Short version: + +1. New entry in `cmd_defs.c` with `CMD_PREAMBLE`/`CMD_MAINFUNC`/... +2. Implement `cmd_()` in `cmd_funcs.c` (declare in `cmd_funcs.h`). +3. (If autocompletion needed) implement `__autocomplete()` in + `cmd_ac.c` and register it via `g_hash_table_insert(ac_funcs, "/name", ...)`. +4. Add a unit test pair `tests/unittests/test_cmd_.{c,h}` and register + tests in `tests/unittests/unittests.c`. + +## Conventions + +- `cmd_` for handlers; `__autocomplete` (file-static) for the + autocomplete callback. +- Subcommand dispatch via `CMD_SUBFUNCS` is preferred over `if/else` chains + inside `cmd_` when possible. +- Keep handlers in `cmd_funcs.c` thin: parse & validate args, then delegate + to a domain module (`xmpp/`, `config/`, `ui/`). diff --git a/patterns/encryption.md b/patterns/encryption.md new file mode 100644 index 0000000..c1016d1 --- /dev/null +++ b/patterns/encryption.md @@ -0,0 +1,71 @@ +# Encryption + +Three independent stacks, all optional at build time (`./configure` flags): + +| Module | Backend | XMPP bridge | +|---|---|---| +| `src/omemo/` | libsignal-protocol-c | `src/xmpp/omemo.c` | +| `src/otr/` | libotr v4 | (in-stream, no separate xmpp bridge) | +| `src/pgp/` | gpgme | `src/xmpp/ox.c` (for OX/XEP-0373) | + +## Module layouts + +### OMEMO (`src/omemo/`) + +| File | Role | +|---|---| +| `omemo.c/h` | Public surface: session setup, encrypt/decrypt, key publish/fetch. | +| `crypto.c/h` | AES-GCM payload encryption; libsignal-protocol crypto provider. | +| `store.c/h` | Persistent identity / pre-key / signed pre-key / session storage. | + +`src/xmpp/omemo.c` is the bridge — it talks to `src/omemo/omemo.c` and to +libstrophe stanza building. + +### OTR (`src/otr/`) + +| File | Role | +|---|---| +| `otr.c/h` | Public surface: start/end/secure messaging, fingerprint mgmt. | +| `otrlib.h` | Compatibility shim across libotr versions. | +| `otrlibv4.c` | libotr v4-specific implementation. | + +OTR runs in-band on the message stream, no separate XMPP bridge file. + +### PGP / OX (`src/pgp/`) + +| File | Role | +|---|---| +| `gpg.c/h` | Classic PGP signing/encrypting via gpgme. | +| `ox.c/h` | XEP-0373 / XEP-0374 (OX): OpenPGP for XMPP. | + +`src/xmpp/ox.c` is the OX bridge; `gpg.c` is invoked directly from message +handlers. + +## Conditional compilation + +Each module is gated by `HAVE_OMEMO`, `HAVE_LIBOTR`, `HAVE_LIBGPGME` (set by +`configure.ac`). Code that calls into a module guards with `#ifdef +HAVE_` and supplies a graceful fallback. When editing: + +- Search for the existing `#ifdef` use of the symbol you are touching to + confirm the gating macro. +- Stubs in `tests/unittests/{omemo,otr,pgp}/stub_*.c` exist regardless — they + satisfy the test linker even when the feature is "off" at runtime. + +## Encryption-aware tests + +`tests/unittests/test_forced_encryption.{c,h}` exercises the policy where +sending plain text is refused if a session is OMEMO/OTR/PGP-locked. Touching +encryption-policy code → check this test does not regress. + +## Adding or modifying calls into encryption + +1. Locate the bridge: outgoing call → typically `src/xmpp/omemo.c` / + `src/xmpp/ox.c` / `src/otr/otr.c`. +2. Confirm the `#ifdef HAVE_*` guard at the call site. +3. If the module's public surface (header) changes, update the corresponding + stub in `tests/unittests/{omemo,otr,pgp}/stub_*.c`. +4. Run the relevant unit tests (`test_forced_encryption`, plus anything in + `test_cmd_otr.c`, `test_cmd_pgp.c`). + +See `playbooks/add-encryption.md`. diff --git a/patterns/events.md b/patterns/events.md new file mode 100644 index 0000000..5496f4a --- /dev/null +++ b/patterns/events.md @@ -0,0 +1,59 @@ +# Events + +`src/event/` mediates between the XMPP protocol layer and consumers (UI, +config, chatlog). Two main directions plus one common module. + +## Files + +| File | Purpose | +|---|---| +| `server_events.c/h` | `sv_ev_*` — events triggered by inbound stanzas. | +| `client_events.c/h` | `cl_ev_*` — events triggered locally (UI / commands). | +| `common.c/h` | Shared helpers used by both. | + +## Server events (`sv_ev_*`) + +Called from `src/xmpp/` parsers (`message.c`, `presence.c`, `iq.c`, `muc.c`, +`roster.c`) once a stanza has been validated and decomposed. Each `sv_ev_*`: + +1. Updates persistent state (chatlog, roster, accounts). +2. Pushes UI updates (`chatwin_*`, `mucwin_*`, `cons_*`, `console_*`). +3. May invoke plugin callbacks (`src/plugins/callbacks.c`). + +Examples (current symbols, grep `sv_ev_` in `server_events.c`): +- `sv_ev_incoming_message` — chat message arrived. +- `sv_ev_room_message` — MUC message. +- `sv_ev_presence_update` — presence change. +- `sv_ev_roster_received` — roster pushed by server. + +## Client events (`cl_ev_*`) + +Called from `cmd_funcs.c` and UI code when the user does something locally +that needs the same downstream fan-out as a server event. + +Examples: +- `cl_ev_send_msg` — outgoing chat message. +- `cl_ev_send_presence` — presence update from `/status` or autoaway. +- `cl_ev_disconnect` — `/disconnect` initiated locally. + +## Adding an event handler + +1. Decide direction: inbound (server) or outbound (client). +2. Add `sv_ev_` or `cl_ev_` in the matching `.c` and declare in + the matching `.h`. +3. Call it from the trigger site (a parser in `src/xmpp/` for server events; + a command handler or UI input handler for client events). +4. Inside, perform the fan-out: state update → UI → plugin callback. +5. If unit-tested, add a stub for it in `tests/unittests/event/stub_*.c` + (or extend the existing stub). + +See `playbooks/add-event-handler.md`. + +## Conventions + +- Keep XMPP-protocol concerns in `src/xmpp/`. The event module is a + *dispatcher*, not a parser. +- Keep UI rendering decisions in `src/ui/`. The event module *invokes* UI + functions but does not draw. +- Plugin callbacks are the **last** step — state must already be consistent + before they run, since plugins may re-enter the codebase. diff --git a/patterns/memory.md b/patterns/memory.md new file mode 100644 index 0000000..fcaf17f --- /dev/null +++ b/patterns/memory.md @@ -0,0 +1,65 @@ +# Memory management + +## `auto_*` macros + +GCC/Clang `__cleanup__` attribute, declared in `src/common.h` (one exception: +`auto_jid` lives in `src/xmpp/jid.h` because it needs the `Jid` type). + +| Macro | Type | Cleanup | Use for | +|---|---|---|---| +| `auto_gchar` | `gchar*` | `auto_free_gchar()` | GLib strings (`g_strdup`, `g_strdup_printf`, etc.) | +| `auto_gcharv` | `gchar**` | `auto_free_gcharv()` | GLib string arrays (`g_strsplit`, `g_strjoinv` arg) | +| `auto_char` | `char*` | `auto_free_char()` | C strings (`strdup`, `malloc`'d) | +| `auto_guchar` | `guchar*` | `auto_free_guchar()` | Unsigned char buffers (e.g. `g_base64_decode`) | +| `auto_gfd` | `gint` | `auto_close_gfd()` | File descriptors held in a `gint` | +| `auto_FILE` | `FILE*` | `auto_close_FILE()` | `FILE*` from `fopen` | +| `auto_jid` | `Jid*` | `jid_auto_destroy()` | `Jid` structs (XMPP) | + +Place the macro **before** the type, like `gboolean`-style attribute: + +```c +auto_gchar gchar* msg = g_strdup_printf("hello %s", name); +auto_gcharv gchar** parts = g_strsplit(line, " ", -1); +``` + +The cleanup runs at scope exit, including early returns. Do **not** call the +matching `g_free`/`g_strfreev`/`fclose` manually — that double-frees. + +## Manual cleanup helpers + +In `common.h`: + +| Macro | Behaviour | +|---|---| +| `FREE_SET_NULL(ptr)` | `free(ptr); ptr = NULL;` (use for `malloc`'d) | +| `GFREE_SET_NULL(ptr)` | `g_free(ptr); ptr = NULL;` (use for GLib-allocated) | + +Use these when the pointer is a struct field that must remain accessible +after the free (so a later `if (x->p)` is safe). + +## GLib free-function reference + +| Allocator | Free with | +|---|---| +| `g_strdup`, `g_strdup_printf`, `g_strconcat`, ... | `g_free` (or `auto_gchar`) | +| `g_strsplit`, `g_strdupv` | `g_strfreev` (or `auto_gcharv`) | +| `g_list_*` of allocated items | `g_list_free_full(list, free_fn)` | +| `g_hash_table_new[_full]` | `g_hash_table_destroy` (uses key/value destroy fns if supplied) | +| `g_base64_decode` | `g_free` (or `auto_guchar`) | +| `g_key_file_*` | `g_key_file_free` | + +**Common pitfall:** mixing `g_free` and `free`. GLib uses its own allocator +shim; never cross the boundary. If you got the buffer from a GLib function, +free it with the matching GLib function. + +**Common pitfall:** `g_strsplit` returns `gchar**` — free with `g_strfreev`, +not `g_free`. (See `gotchas.md`.) + +## Adding a new `auto_*` + +1. Declare cleanup function: `void auto_close_foo(Foo** p);` +2. Define the macro: `#define auto_foo __attribute__((__cleanup__(auto_close_foo)))` +3. Place both in the header that owns the type — `common.h` for project-wide, + the type's own header otherwise. +4. The cleanup must tolerate `NULL` and idempotent re-entry; assign `*p = NULL` + inside if you keep the variable accessible after free. diff --git a/patterns/plugins.md b/patterns/plugins.md new file mode 100644 index 0000000..0eb4c79 --- /dev/null +++ b/patterns/plugins.md @@ -0,0 +1,67 @@ +# Plugins + +`src/plugins/` exposes both a C plugin ABI and a Python plugin runtime. Both +funnel through a shared core that fires callbacks on cproof events. + +## Files + +| File | Role | +|---|---| +| `plugins.c/h` | Public surface: load/unload, list, dispatch hooks. | +| `c_plugins.c/h` | C plugin loader (dlopen). | +| `python_plugins.c/h` | Python plugin loader (embedded interpreter). | +| `c_api.c/h` | C-side API exposed **to** plugins. | +| `python_api.c/h` | Python-side API exposed **to** plugins. | +| `api.c/h` | Shared API helpers. | +| `profapi.c/h` | The `prof` Python module surface. | +| `callbacks.c/h` | Hooks: pre/post message, pre/post presence, etc. | +| `autocompleters.c/h` | Plugin-registered autocompleters. | +| `disco.c/h` | Service-discovery feature contributions from plugins. | +| `settings.c/h` | Per-plugin settings storage. | +| `themes.c/h` | Plugin-supplied themes. | + +## Hook points + +Plugins fire on events around (cf. `src/plugins/callbacks.c`): +- Pre/post message send, pre/post message receive. +- Pre/post chat-message-display. +- Pre/post connect, disconnect. +- Pre/post quit. +- Window-switch, room-join, room-leave. + +The callback names follow `prof_pre_*` / `prof_post_*` in the plugin API +surface; in cproof internals they are dispatched via +`plugins_pre_chat_message_send()` and similar. + +## API surface for plugins + +C API (`c_api.h`) and Python API (`python_api.h`) are kept in sync — any +new function in one should land in the other unless explicitly C-only or +Python-only. + +The Python wrapper is in `profapi.c/h` and is exposed as the `prof` module +inside plugins. + +## Adding a plugin hook point + +1. Define dispatcher in `plugins.c/h`: + `void plugins__();` +2. Wire callback storage in `callbacks.c/h`. +3. Expose to plugins via `c_api.c/h` and `python_api.c/h` (matching names). +4. Call the dispatcher at the appropriate site (usually in `src/event/` or + `src/xmpp/`). +5. Document the hook in cproof's plugin docs (out of scope for this repo). + +## Testing + +`tests/unittests/test_plugins_disco.{c,h}` covers plugin-disco feature +contribution; broader plugin-system testing is light. Stubs: +`tests/unittests/plugins/stub_*.c`. + +## Pitfalls + +- Plugin callbacks may re-enter cproof via the C/Python API. Ensure state is + consistent **before** firing a callback — see `patterns/events.md`. +- The Python interpreter is global; plugins share state. Don't assume + isolation. +- Plugin loading is not sandboxed; treat plugins as trusted code. diff --git a/patterns/ui.md b/patterns/ui.md new file mode 100644 index 0000000..5582882 --- /dev/null +++ b/patterns/ui.md @@ -0,0 +1,63 @@ +# UI + +`src/ui/` is built on ncurses (wide-char). It owns windows, the input line, +status/title bars, and tray notifications. + +## Window types + +| File | Window | Notes | +|---|---|---| +| `chatwin.c` | One-on-one chat | One per peer JID. | +| `mucwin.c` | MUC room | Tracks occupants list, subject, role. | +| `privwin.c` | MUC private chat | Per-occupant private window inside a room. | +| `console.c` | Console | Always present, index 1; system messages. | +| `confwin.c` | Form / config | Used for ad-hoc commands and data forms. | +| `xmlwin.c` | XML console | Raw stanza tracer. | +| `vcardwin.c` | vCard view | Read-only viewer. | + +`win_types.h` declares the window-type enum used to discriminate at runtime. +`window.c/h` is the base type; `window_list.c/h` manages the open-windows +collection. + +## Buffers + +`buffer.c/h` — append-only ring of rendered lines per window. UI rendering +reads from the buffer; chatlog persists separately. + +## Bars and chrome + +| File | Role | +|---|---| +| `statusbar.c/h` | Bottom status line (window list, unread counts). | +| `titlebar.c/h` | Top bar (current window title, encryption state). | +| `tray.c/h` | System-tray notifications via libnotify. | +| `screen.c/h` | Screen-level helpers (resize, refresh). | +| `notifier.c` | Sound and visual notifications dispatch. | +| `inputwin.c/h` | Input line: line editor, history, completion trigger. | +| `core.c` | UI lifecycle (init / shutdown / refresh loop). | + +## Roster panel + +`rosterwin.c` and `occupantswin.c` render the side panels (roster and MUC +occupants). They subscribe to roster / muc state changes via the event layer. + +## Input → completion + +`inputwin.c` calls into `cmd_ac.c` on Tab. See `patterns/autocomplete.md` for +how that flow continues. + +## Conventions + +- Public UI calls used from outside `src/ui/`: + - `cons_show("...")` — formatted line into the console. + - `cons_bad_cmd_usage(cmd)` — print canonical "wrong usage" line. + - `chatwin_*`, `mucwin_*`, `privwin_*` — write into a specific window type. +- UI code does not block on XMPP. State queries go through getters in + `src/xmpp/` and `src/config/`. +- Color / theme lookups go through `src/config/color.c` and + `src/config/theme.c` — do not hardcode ncurses attribute pairs. + +## Testing + +UI is heavily stubbed in unit tests — see `tests/unittests/ui/stub_ui.c` and +sibling stubs. Real ncurses calls do not run under `make check`. diff --git a/patterns/xmpp.md b/patterns/xmpp.md new file mode 100644 index 0000000..275737f --- /dev/null +++ b/patterns/xmpp.md @@ -0,0 +1,95 @@ +# XMPP layer + +`src/xmpp/` wraps libstrophe and exposes higher-level operations to the +command, event, and UI layers. + +## Connection lifecycle + +`src/xmpp/connection.c` owns the connection state machine. Public surface in +`connection.h`: + +- `jabber_conn_status_t connection_get_status(void);` +- `jabber_conn_status_t connection_connect(const char* fulljid, const char* passwd, ...);` +- `jabber_conn_status_t connection_register(...);` +- `void connection_disconnect(void);` + +`jabber_conn_status_t` values: + +| Value | Meaning | +|---|---| +| `JABBER_DISCONNECTED` | Not connected. | +| `JABBER_CONNECTING` | Connect in progress. | +| `JABBER_CONNECTED` | Live session. | +| `JABBER_DISCONNECTING` | Tearing down. | + +**Most code paths gate on `JABBER_CONNECTED`.** A canonical command-handler +guard: + +```c +if (connection_get_status() != JABBER_CONNECTED) { + cons_show("You are not currently connected."); + return TRUE; +} +``` + +In tests, `will_return(connection_get_status, JABBER_CONNECTED)` (or +`JABBER_DISCONNECTED`) before the call. See `gotchas.md`. + +## Stanza building + +`src/xmpp/stanza.c` is the assembler. Helpers like +`stanza_create_message(...)`, `stanza_create_presence(...)`, plus low-level +`xmpp_stanza_*` from libstrophe. + +Outgoing flow: +- `cmd_funcs.c` or `client_events.c` → `xmpp/message.c` (or `presence.c`, + `iq.c`) → `stanza.c` to build → libstrophe to send. + +Incoming flow: +- libstrophe handler → `xmpp/message.c` (or sibling) parses → `event/server_events.c` + fans out → `ui/`, `chatlog.c`, plugin callbacks. + +## JIDs + +`src/xmpp/jid.h` defines `Jid` and the `auto_jid` cleanup macro. Use +`auto_jid` rather than manual `jid_destroy()` whenever practical: + +```c +auto_jid Jid* parsed = jid_create(input); +if (!parsed || !parsed->barejid) { + return FALSE; +} +``` + +`Jid` exposes `barejid`, `fulljid`, `localpart`, `domainpart`, `resourcepart`. + +## Sessions, MUC, presence, roster + +| File | Responsibility | +|---|---| +| `session.c` | Logical XMPP session: account, fulljid, presence, settings. | +| `muc.c` | Multi-user chat rooms (joins, occupants, configuration). | +| `presence.c` | Presence stanzas, subscription requests. | +| `roster.c` / `roster_list.c` | Roster fetch, push, contact lookup. | +| `chat_session.c` | Per-conversation state (carbons, OTR channel, etc.). | +| `chat_state.c` | Composing / paused / inactive notifications (XEP-0085). | +| `bookmark.c` | Bookmarked rooms (XEP-0048). | +| `blocking.c` | Block list (XEP-0191). | +| `vcard.c` / `vcard_funcs.h` | vCard fetch / publish. | +| `avatar.c` | Avatar publish / fetch (XEP-0084). | +| `iq.c` | Generic IQ handling and pending-IQ map. | +| `capabilities.c` | Entity capabilities cache (XEP-0115). | + +## Encryption bridges + +`src/xmpp/omemo.c` and `src/xmpp/ox.c` are *bridges* into the encryption +modules — they live alongside same-named files in `src/omemo/` and +`src/pgp/`. Be precise about which path you mean. + +## Testing + +When unit-testing code that calls into `xmpp/`, expect to: + +- Mock `connection_get_status` with `will_return(...)`. +- Stub session getters (`session_get_account_name`) and roster lookups. +- Use stubs in `tests/unittests/xmpp/stub_*.c`. See `testing/stubs.md`. diff --git a/playbooks/add-autocomplete.md b/playbooks/add-autocomplete.md new file mode 100644 index 0000000..9c81483 --- /dev/null +++ b/playbooks/add-autocomplete.md @@ -0,0 +1,119 @@ +# Playbook: add autocomplete to a command + +Pre-req: command already exists in `cmd_defs.c`. If not, see +`playbooks/add-command.md`. + +## Choose the flavour + +| Token set | Use | +|---|---| +| Static, known at startup. | `autocomplete_param_with_ac` + a static `Autocomplete`. | +| Dynamic — depends on roster, accounts, DB query, etc. | `autocomplete_param_with_func` + a stateless callback. | + +## Static token set + +`src/command/cmd_ac.c`: + +```c +// 1. Declare the AC at file scope: +static Autocomplete foo_ac; + +// 2. Add it to the static array near the top of the file (the free-list +// that cmd_ac_uninit walks at shutdown): +static Autocomplete* all_acs[] = { + // ... existing entries ... + &foo_ac, +}; + +// 3. Initialise in cmd_ac_init(): +foo_ac = autocomplete_new(); +autocomplete_add(foo_ac, "alpha"); +autocomplete_add(foo_ac, "beta"); + +// 4. Implement the dispatcher: +static char* +_foo_autocomplete(ProfWin* window, const char* const input, gboolean previous) +{ + return autocomplete_param_with_ac(input, "/foo", foo_ac, TRUE, previous); +} + +// 5. Register: +g_hash_table_insert(ac_funcs, "/foo", _foo_autocomplete); +``` + +## Dynamic — function callback + +If suggestions come from runtime state (roster, accounts, providers list): + +```c +// In a domain module (e.g. src/foo/foo.c), implement a stateless callback: +char* +foo_suggestions(const char* const search_str, gboolean previous, void* context) +{ + // Compute and return a freshly-allocated char* (caller frees), or NULL. + // No module-level "_last_match" globals — keep state on the stack / + // derive deterministically from search_str and previous. +} +``` + +Wire into `cmd_ac.c`: + +```c +static char* +_foo_autocomplete(ProfWin* window, const char* const input, gboolean previous) +{ + return autocomplete_param_with_func(input, "/foo", foo_suggestions, previous, NULL); +} + +g_hash_table_insert(ac_funcs, "/foo", _foo_autocomplete); +``` + +A clean canonical example is `roster_contact_autocomplete` in +`src/xmpp/roster_list.c` — it delegates to `autocomplete_complete` against a +roster-owned `Autocomplete` and keeps no callback-local state. **Do not +copy older callbacks that keep state in a file-static `_last_match` +variable** — those have known issues with shift-tab cycling and concurrent +completers. + +## Subcommand autocompletion + +If `/foo` has subcommands, branch inside `_foo_autocomplete`: + +```c +static char* +_foo_autocomplete(ProfWin* window, const char* const input, gboolean previous) +{ + char* result = NULL; + + // First the subcommand list itself: + result = autocomplete_param_with_ac(input, "/foo", foo_subcommands_ac, TRUE, previous); + if (result) return result; + + // Then per-subcommand argument completion: + if (g_str_has_prefix(input, "/foo set ")) { + result = autocomplete_param_with_func(input, "/foo set", foo_set_options, previous, NULL); + if (result) return result; + } + + return NULL; +} +``` + +`/roster` (`src/command/cmd_ac.c`, search for `_roster_autocomplete`) is a +current example of this layered subcommand layout. + +## Unit-test the callback + +Stateless callbacks are easy to unit-test directly: + +```c +void +test_foo_suggestions_returns_first_match(void** state) +{ + char* r = foo_suggestions("al", FALSE, NULL); + assert_string_equal(r, "alpha"); + free(r); +} +``` + +Add the test pair to `tests/unittests/` (see `playbooks/add-test.md`). diff --git a/playbooks/add-command.md b/playbooks/add-command.md new file mode 100644 index 0000000..31a795a --- /dev/null +++ b/playbooks/add-command.md @@ -0,0 +1,161 @@ +# Playbook: add a `/command` + +End-to-end recipe for adding a new user-facing command. Example: `/foo`. + +## 1. Define the handler + +`src/command/cmd_funcs.h`: + +```c +gboolean cmd_foo(ProfWin* window, const char* const command, gchar** args); +``` + +`src/command/cmd_funcs.c`: + +```c +gboolean +cmd_foo(ProfWin* window, const char* const command, gchar** args) +{ + if (connection_get_status() != JABBER_CONNECTED) { + cons_show("You are not currently connected."); + return TRUE; + } + if (!args[0]) { + cons_bad_cmd_usage(command); + return TRUE; + } + // ... do the thing ... + return TRUE; +} +``` + +Keep the handler thin: validate, then delegate to a domain module. + +## 2. Register the command + +`src/command/cmd_defs.c` — add an entry to the static `Command` array: + +```c +{ CMD_PREAMBLE("/foo", + parse_args, 0, 1, NULL) + CMD_MAINFUNC(cmd_foo) + CMD_TAGS(CMD_TAG_CHAT) + CMD_SYN("/foo []") + CMD_DESC("Do the foo thing.") + CMD_ARGS( + { "", "Optional argument to pass." }) + CMD_EXAMPLES("/foo bar") +} +``` + +For a command with subcommands, swap `CMD_MAINFUNC(cmd_foo)` for +`CMD_SUBFUNCS({"sub", cmd_foo_sub}, ...)`. + +## 3. Autocompletion (optional) + +`src/command/cmd_ac.c`: + +a. (If a static token list) declare and initialise an `Autocomplete`: + +```c +static Autocomplete foo_ac; + +// in cmd_ac_init(): +foo_ac = autocomplete_new(); +autocomplete_add(foo_ac, "bar"); +autocomplete_add(foo_ac, "baz"); + +// add &foo_ac to the static free-list near top of file +``` + +b. Implement the per-command dispatcher: + +```c +static char* +_foo_autocomplete(ProfWin* window, const char* const input, gboolean previous) +{ + return autocomplete_param_with_ac(input, "/foo", foo_ac, TRUE, previous); +} +``` + +c. Register it: + +```c +g_hash_table_insert(ac_funcs, "/foo", _foo_autocomplete); +``` + +For dynamic suggestions, use `autocomplete_param_with_func` and a stateless +callback. See `patterns/autocomplete.md`. + +## 4. Unit test + +Create `tests/unittests/test_cmd_foo.c` and `test_cmd_foo.h`: + +`test_cmd_foo.h`: + +```c +void test_cmd_foo_when_disconnected_shows_message(void** state); +void test_cmd_foo_when_no_arg_shows_usage(void** state); +void test_cmd_foo_happy_path(void** state); +``` + +`test_cmd_foo.c`: + +```c +#include "config.h" +#include "prof_cmocka.h" +#include "test_cmd_foo.h" +// ... includes for stubs and the unit ... + +void +test_cmd_foo_when_disconnected_shows_message(void** state) +{ + will_return(connection_get_status, JABBER_DISCONNECTED); + expect_string(cons_show, msg, "You are not currently connected."); + gchar* args[] = { NULL }; + assert_true(cmd_foo(NULL, "/foo", args)); +} +``` + +## 5. Register the test + +`tests/unittests/unittests.c`: + +```c +#include "test_cmd_foo.h" + +// ... inside the tests[] array ... +cmocka_unit_test(test_cmd_foo_when_disconnected_shows_message), +cmocka_unit_test(test_cmd_foo_when_no_arg_shows_usage), +cmocka_unit_test(test_cmd_foo_happy_path), +``` + +## 6. Stubs + +If `cmd_foo` calls a function that is not yet stubbed, add the stub. See +`testing/stubs.md`. + +## 7. Wire into Make + +- `cmd_defs.c`, `cmd_funcs.c`, `cmd_ac.c` are already in the build. +- New test file: add `tests/unittests/test_cmd_foo.c` to the unittests + sources in `tests/unittests/Makefile.am` (or whatever wires it). +- New stub file (if any): add to the same Makefile. + +## 8. Build & check + +Inside Docker (`build/docker.md`): + +```sh +./autogen.sh && ./configure && make -j$(nproc) && make check +``` + +## 9. Help text + +`/help foo` should now produce the synopsis / description / args from the +`Command` entry. No separate help file to update. + +## 10. Commit + +Single commit, conventional-commit style, English. No AI-attribution +trailer. diff --git a/playbooks/add-encryption.md b/playbooks/add-encryption.md new file mode 100644 index 0000000..5538493 --- /dev/null +++ b/playbooks/add-encryption.md @@ -0,0 +1,103 @@ +# Playbook: touch encryption modules safely + +Encryption stacks (OMEMO, OTR, PGP/OX) are independent and each is gated +behind a `configure` option. Edits must respect three things: the gating +macro, the bridge file split, and the matching unit-test stubs. + +See `patterns/encryption.md` for the architectural split. + +## 1. Identify the bridge + +| Stack | Module dir | Bridge file (XMPP side) | +|---|---|---| +| OMEMO | `src/omemo/` | `src/xmpp/omemo.c` | +| OTR | `src/otr/` | (none — in-stream on `src/xmpp/message.c`) | +| PGP / OX | `src/pgp/` | `src/xmpp/ox.c` | + +Outbound calls (cproof → encryption) usually live in the bridge file. +Inbound (decrypted message → cproof) usually surfaces in the bridge or +directly in `src/xmpp/message.c`. + +## 2. Identify the gating macro + +```sh +grep -n "HAVE_OMEMO\|HAVE_LIBOTR\|HAVE_LIBGPGME" src/xmpp/.c +``` + +Every call into the encryption module is wrapped: + +```c +#ifdef HAVE_OMEMO + omemo_encrypt_message(jid, plaintext, &ciphertext); +#else + // graceful fallback or skip +#endif +``` + +When you add a new call, copy the existing guard. Do **not** drop the +fallback — the build must succeed with the feature disabled. + +## 3. Update the public surface (header) + +If you add or change an exported function in `src/omemo/omemo.h` / +`src/otr/otr.h` / `src/pgp/gpg.h` / `src/pgp/ox.h`, update **two** places: + +1. The header itself. +2. The corresponding stub: `tests/unittests/{omemo,otr,pgp}/stub_*.c`. + +Keep the stub signature byte-for-byte identical to the header. + +## 4. Stubs + +Stubs exist regardless of whether a stack is enabled at runtime — they +satisfy the unit-test linker. Three flavours, see `testing/stubs.md`. + +If a unit test needs to drive the new function, switch the stub to `mock()` +and queue with `will_return`. If a unit test needs to assert arguments, +switch to `check_expected` and queue with `expect_string` / `expect_value`. + +## 5. `test_forced_encryption` + +`tests/unittests/test_forced_encryption.{c,h}` exercises the policy that +refuses plaintext sends when a session is encryption-locked. **Run it after +any encryption-policy change.** + +## 6. Encryption-aware command tests + +| Test file | Covers | +|---|---| +| `test_cmd_otr.c` / `.h` | `/otr` command surface. | +| `test_cmd_pgp.c` / `.h` | `/pgp` command surface. | + +(No `test_cmd_omemo.c` exists at the time of writing — confirm with `ls +tests/unittests/test_cmd_*.c` before assuming.) + +## 7. Build matrix + +After changes, build with each encryption flag in turn: + +```sh +./configure --disable-omemo +make check + +./configure --disable-otr +make check + +./configure --disable-pgp +make check +``` + +(Inside Docker — see `build/docker.md`.) + +CI does not necessarily run every disable combination; verify locally if +your change affects gating logic. + +## Conventions + +- Never strip the `#ifdef HAVE_*` guard "for clarity". The disabled-build + must still compile. +- Encryption keys / fingerprints persist via the module's own store (`src/ + omemo/store.c`, libotr's keystore, gpgme's keyring). Do not invent + parallel storage. +- Outbound encryption sits between message build and stanza send — see + `patterns/xmpp.md` for the flow. diff --git a/playbooks/add-event-handler.md b/playbooks/add-event-handler.md new file mode 100644 index 0000000..3622e3a --- /dev/null +++ b/playbooks/add-event-handler.md @@ -0,0 +1,109 @@ +# Playbook: add an event handler + +`src/event/` mediates between `src/xmpp/` (and locally-triggered actions) +and the rest of the codebase. Two directions: + +- **Server events** (`sv_ev_*`) — triggered by inbound stanzas. +- **Client events** (`cl_ev_*`) — triggered by local actions (UI, command). + +See `patterns/events.md` for the conceptual split. + +## Decide direction + +| Trigger | Direction | File | +|---|---|---| +| Inbound stanza parsed in `src/xmpp/`. | Server. | `src/event/server_events.c/h` | +| User typed a command, UI key, or internal timer. | Client. | `src/event/client_events.c/h` | + +## Add the handler — server example + +`src/event/server_events.h`: + +```c +void sv_ev_foo_received(const char* const from, const char* const payload); +``` + +`src/event/server_events.c`: + +```c +void +sv_ev_foo_received(const char* const from, const char* const payload) +{ + // 1. Update persistent state. + chatlog_msg_in(from, payload); + + // 2. Push UI update. + ProfChatWin* win = wins_get_chat(from); + if (win) { + chatwin_incoming_msg(win, payload, NULL, FALSE); + } else { + cons_show_incoming_message(from, payload); + } + + // 3. Plugin callbacks last — state must be consistent. + plugins_post_chat_message_received(from, payload); +} +``` + +## Wire the trigger site + +For a server event, the matching parser in `src/xmpp/` calls the new +handler. For instance, if `` stanzas are parsed in +`src/xmpp/message.c`: + +```c +// ... after parsing the stanza ... +sv_ev_foo_received(from, payload); +``` + +For a client event, call from `cmd_funcs.c` (or wherever the user-facing +trigger lives). + +## Stub the handler in tests + +`tests/unittests/event/stub_*.c` — add a stub for the new function so units +that exercise the trigger site link cleanly: + +```c +void +sv_ev_foo_received(const char* const from, const char* const payload) +{ + // pass-through (or check_expected, depending on test needs) +} +``` + +If a test needs to verify the handler is called with specific args, switch +the stub to `check_expected` and the test queues `expect_string` / `expect_value`. + +See `testing/stubs.md`. + +## Plugin hook (if applicable) + +If the event should fire a plugin callback, add a dispatcher in +`src/plugins/plugins.c/h` (e.g. `plugins_post_foo_received`) and call it +**last** in the handler — after state and UI are consistent. Keep C and +Python plugin APIs in sync (`c_api.h` / `python_api.h`). + +## Unit test the handler + +`tests/unittests/test_server_events.c` (or `test_client_events.c`) holds +event-side tests. Pattern: + +```c +void +test_sv_ev_foo_received_writes_chatlog(void** state) +{ + expect_string(chatlog_msg_in, jid, "alice@example.com"); + expect_string(chatlog_msg_in, msg, "hello"); + sv_ev_foo_received("alice@example.com", "hello"); +} +``` + +## Conventions + +- One handler, one event. Don't pile multiple unrelated events into the same + function. +- Order inside the handler: state → UI → plugins. Never call plugins before + state is committed. +- `sv_ev_*` and `cl_ev_*` should be straight-line — no XMPP protocol parsing, + no UI rendering. They orchestrate; they don't implement. diff --git a/playbooks/add-test.md b/playbooks/add-test.md new file mode 100644 index 0000000..372521f --- /dev/null +++ b/playbooks/add-test.md @@ -0,0 +1,97 @@ +# Playbook: add a unit test file + +Recipe for a new `tests/unittests/test_.c` + `.h` pair. + +## 1. Create the header + +`tests/unittests/test_.h`: + +```c +#ifndef TEST__H +#define TEST__H + +void test__(void** state); +void test__(void** state); + +#endif +``` + +One declaration per test function. Headers exist solely so `unittests.c` can +include them and reference each function symbol. + +## 2. Create the source + +`tests/unittests/test_.c`: + +```c +#include "config.h" +#include "prof_cmocka.h" + +#include +#include +#include +#include + +#include "test_.h" +// ... includes for the unit under test and any types it needs ... + +void +test__(void** state) +{ + // Arrange — `will_return`, `expect_string`, etc. + // Act — call the unit + // Assert — `assert_*` +} +``` + +## 3. Register in `unittests.c` + +```c +#include "test_.h" + +// inside the tests[] array passed to cmocka_run_group_tests: +cmocka_unit_test(test__), +cmocka_unit_test(test__), +``` + +If multiple tests share fixture, add a setup / teardown pair and use +`cmocka_unit_test_setup_teardown` (or define a per-topic macro near the top +of `unittests.c`, like `muc_unit_test`). + +## 4. Stubs + +For each external function the unit calls, ensure a stub exists in the +matching `tests/unittests//stub_*.c`. Three flavours: + +- **Pass-through** — no observation needed. +- **`mock()` / `will_return`** — test injects return values. +- **`check_expected()` / `expect_*`** — test asserts arguments. + +See `testing/stubs.md`. + +## 5. Wire into Make + +`tests/unittests/Makefile.am` (or the active wiring file): add +`test_.c` to the `unittests_SOURCES` (or equivalent) list. Same for +any new stub file. + +## 6. Build & run + +Inside Docker: + +```sh +./autogen.sh && ./configure && make -j$(nproc) check +``` + +Diagnose failures via `tests/unittests/unittests.log` and the cmocka stderr +output. + +## 7. Conventions + +- Test functions: `test__` — never `test_topic1`, + `test_topic2`. Names should describe the scenario. +- One assertion focus per test (multiple `assert_*` calls are fine; multiple + *unrelated* assertions are not). +- Don't reuse stubs across topic suites unless the call truly is uniform — + diverging behaviour is a strong sign you want a dedicated stub. +- No I/O in unit tests. Filesystem, network, ncurses are all stubbed. diff --git a/testing/bench.md b/testing/bench.md new file mode 100644 index 0000000..e8a0f00 --- /dev/null +++ b/testing/bench.md @@ -0,0 +1,49 @@ +# Benchmarks + +`tests/bench/` holds performance benches for storage / parser hot paths. Each +runner is a separate binary built into its own subdirectory. + +## Runners + +| Runner | Path | Measures | +|---|---|---| +| `bench_runner` | `tests/bench/bench_runner/` | General-purpose runner; orchestrates other benches. | +| `bench_export_import` | `tests/bench/bench_export_import/` | Database export/import throughput (SQLite + flatfile). | +| `bench_failure_modes` | `tests/bench/bench_failure_modes/` | Flatfile parser error/recovery paths. | +| `bench_long_messages` | `tests/bench/bench_long_messages/` | Flatfile parser on very long messages. | +| `gen_history` | `tests/bench/gen_history/` | Generates synthetic chat history fixtures used by other benches. | + +Each runner pulls in a small set of `src/*.c` files (`common.c`, +`database.c`, `database_flatfile*.c`, etc.) — see the per-runner Makefile or +top-level `Makefile.am` for exact dependencies. + +## Stubs + +Benches use their own minimal stubs (typically `bench_stubs.c`, +`bench_common.c`, `bench_csv.c` per runner) — separate from +`tests/unittests/` stubs to keep the bench builds small. + +## Running + +Benches are not part of `make check`. Build and invoke each runner directly +once configured: + +```sh +make tests/bench/bench_runner/bench_runner +./tests/bench/bench_runner/bench_runner [args] +``` + +(Argument shape per-runner; check `--help`.) + +## Output + +Runners typically emit CSV (`bench_csv.c`) for downstream tooling. Treat +output as machine-readable; do not parse the human header lines. + +## When to add a bench + +- A storage / parser hot path is sensitive to regression. +- A perf claim in a PR needs to be reproducible. + +If it's just a one-off measurement, run it locally and don't commit. The +bench runners are for sustained, comparable measurement. diff --git a/testing/functional-tests.md b/testing/functional-tests.md new file mode 100644 index 0000000..dd9c922 --- /dev/null +++ b/testing/functional-tests.md @@ -0,0 +1,62 @@ +# Functional tests + +`tests/functionaltests/` exercises a real cproof binary under a PTY against a +real (test) XMPP server. + +## Files + +| File | Role | +|---|---| +| `functionaltests.c` | Runner (entry point). | +| `proftest.c/h` | Fixture: spawn cproof under PTY, drive XMPP server, assertions. | +| `test_.c` | Per-feature suites. | + +Existing suites (grep for `test_*.c`): + +- Connection / disconnection: `test_connect.c`, `test_disconnect.c`, + `test_autoping.c`, `test_ping.c`. +- Messaging: `test_message.c`, `test_chat_session.c`, `test_carbons.c`, + `test_receipts.c`, `test_history.c`. +- Presence and roster: `test_presence.c`, `test_roster.c`, + `test_lastactivity.c`. +- Discovery: `test_disco.c`, `test_software.c`. +- MUC: `test_muc.c`, `test_rooms.c`. +- Storage: `test_export_import.c`. + +## Prerequisites + +- A test XMPP server reachable from the harness (typically a local Prosody + instance configured for the tests). +- A pre-baked `.profrc` (recent perf work pre-bakes this for speed — see + cproof commit `f84ed1bf6` for context). +- PTY availability (`/dev/ptmx` on Linux). + +## Running + +Functional tests are not part of `make check` (which runs unit tests only). +They are built and invoked via a separate target in `tests/functionaltests/` +— inspect `tests/functionaltests/Makefile.am` for the current target name and +arguments. + +Recent perf work (commit `f84ed1bf6`) added: +- Profrc pre-baking — fixture writes a known-good `.profrc` once, not per-test. +- pty-close shutdown — fixture closes the PTY to terminate cproof faster than + signalling. +- Parallel port pools — separate XMPP servers / ports per parallel suite. + +## When to add functional vs unit + +| Test what | Where | +|---|---| +| Pure logic, internal API, given a state. | Unit. | +| Stanza serialization / parsing round-trip with real libstrophe. | Functional. | +| Connection state machine across multiple network events. | Functional. | +| UI rendering (visible output). | **Neither** (UI stubbed in unit; not asserted in functional). | + +## Failure-mode notes + +- A leaked / orphan cproof process between suites poisons later runs. The + fixture aggressively kills children — but if you abort a test by hand, + `pkill -f tests/functionaltests` first. +- Port-pool exhaustion under parallelism: bump the pool size, do not lower + `-jN`. diff --git a/testing/stubs.md b/testing/stubs.md new file mode 100644 index 0000000..65fc55a --- /dev/null +++ b/testing/stubs.md @@ -0,0 +1,113 @@ +# Stubs + +Unit tests link against the unit-under-test plus a parallel set of "stub" +implementations of every dependency. Stubs are organised by module under +`tests/unittests//`. + +## Layout + +| Stub directory | Replaces | +|---|---| +| `tests/unittests/chatlog/` | `src/chatlog.c` | +| `tests/unittests/command/` | parts of `src/command/` | +| `tests/unittests/config/` | `src/config/` (accounts, cafile, ...) | +| `tests/unittests/database/` | `src/database.c` | +| `tests/unittests/event/` | `src/event/` | +| `tests/unittests/log/` | `src/log.c` | +| `tests/unittests/omemo/` | `src/omemo/` | +| `tests/unittests/otr/` | `src/otr/` | +| `tests/unittests/pgp/` | `src/pgp/` | +| `tests/unittests/plugins/` | `src/plugins/` | +| `tests/unittests/tools/` | `src/tools/` | +| `tests/unittests/ui/` | `src/ui/` | +| `tests/unittests/xmpp/` | `src/xmpp/` | +| `tests/unittests/unittests/` | top-level utility stubs | + +Files are named `stub_.c` (sometimes split, e.g. +`tests/unittests/config/stub_accounts.c`, `stub_cafile.c`). + +## Two stub flavours + +### Pass-through default + +Returns a benign default (NULL, FALSE, 0). Used when the test does not care +about the call. + +```c +void +chatlog_msg_in(...) +{ + // no-op +} +``` + +### `will_return`-driven + +The stub pops a value queued by the test: + +```c +jabber_conn_status_t +connection_get_status(void) +{ + return (jabber_conn_status_t)mock(); +} +``` + +Test side: + +```c +will_return(connection_get_status, JABBER_CONNECTED); +``` + +If the stub is `will_return`-driven, **every test** that exercises a code +path through it must queue a value, or cmocka aborts. + +### `expect_*`-driven + +The stub asserts argument values against expectations: + +```c +void +cons_bad_cmd_usage(const char* const cmd) +{ + check_expected(cmd); +} +``` + +Test side: + +```c +expect_string(cons_bad_cmd_usage, cmd, CMD_ACCOUNT); +``` + +## When to add a stub + +1. You call a new function from production code, and the call site is + reachable from a unit test. +2. `make check` link fails with an undefined-symbol error. + +Procedure: + +- Locate the matching stub module by callee path: `src/foo/bar.c` → + `tests/unittests/foo/stub_bar.c`. Create the stub file if absent. +- Stub the new function with the right flavour: + - Pass-through if tests don't need to observe / drive it. + - `mock()` if a test needs to inject a return value. + - `check_expected()` if a test needs to assert an argument. +- Wire the new stub `.c` into the build. See `tests/unittests/Makefile.am` + (or whatever wires the stubs into the `unittests` target). + +## When to extend an existing stub + +If a stub already pass-through and a test now needs to drive it, **change the +stub to `mock()`**. Then every test (existing and new) reaching that stub +must `will_return` — be ready to update unrelated tests. + +## Convention notes + +- Stubs do **not** include real headers from `src//` if doing so + would pull in the very symbol they replace. Forward-declare instead. +- `tests/unittests/ui/stub_ai.c` is a UI-side stub for the AI client surface + consumed by `src/ui/`. +- The `tests/unittests/unittests/` directory holds top-level catch-alls that + do not fit a single module. diff --git a/testing/unit-tests.md b/testing/unit-tests.md new file mode 100644 index 0000000..3870dfb --- /dev/null +++ b/testing/unit-tests.md @@ -0,0 +1,96 @@ +# Unit tests + +Framework: cmocka, accessed via `tests/prof_cmocka.h` (a thin compatibility +wrapper). Run via `make check`. + +## File layout + +- One test file per topic: `tests/unittests/test_.c` + matching + `test_.h`. +- Header declares each test function (one per `cmocka_unit_test*`). +- Runner: `tests/unittests/unittests.c` — `#include`s every header, calls + `cmocka_run_group_tests()` over a single big array. + +## Registration + +Inside `unittests.c`, every test name appears in the array passed to +`cmocka_run_group_tests`: + +```c +const struct CMUnitTest tests[] = { + cmocka_unit_test(test_caps_show_basic), + cmocka_unit_test_setup_teardown(test_join_room, muc_before_test, muc_after_test), + // ... +}; +``` + +Convenience macros are defined near the top, e.g.: + +```c +#define muc_unit_test(f) cmocka_unit_test_setup_teardown(f, muc_before_test, muc_after_test) +``` + +## Test function shape + +```c +void +test_account_set_jid_succeeds(void** state) +{ + // Arrange + will_return(connection_get_status, JABBER_CONNECTED); + will_return(session_get_account_name, "alice@example.com"); + + ProfAccount* account = account_new("alice@example.com", ...); + will_return(accounts_get_account, account); + + // Act + gchar* args[] = { "set", "alice@example.com", "jid", "alice@example.org", NULL }; + gboolean ok = cmd_account(NULL, CMD_ACCOUNT, args); + + // Assert + assert_true(ok); +} +``` + +## cmocka primitives in heavy use + +| Primitive | When | +|---|---| +| `will_return(fn, value)` | Queue a return value for a stubbed function. Each call to the stub pops one. | +| `will_return_count(fn, value, n)` | Queue the same value `n` times. | +| `expect_string(fn, param, "...")` | Stub asserts that the named argument equals this string. | +| `expect_value(fn, param, value)` | Stub asserts equality on a non-string. | +| `expect_any(fn, param)` | Stub accepts any value (still pops the queue). | +| `assert_true / assert_false / assert_string_equal / assert_int_equal` | Assertions inside the test. | +| `cmocka_unit_test(fn)` | Plain registration. | +| `cmocka_unit_test_setup_teardown(fn, setup, teardown)` | With per-test fixture. | + +`expect_*` queues are checked at test end — missing calls fail the test. + +## Group setup / teardown + +For sets of tests sharing fixture: define a `_setup` and `_teardown` and +register via `cmocka_unit_test_setup_teardown`. Examples in `unittests.c`: +`muc_before_test`, `muc_after_test`, `keyhandlers_setup`, `keyhandlers_teardown`. + +## What gets stubbed + +Tests run against the production `.c` files of the unit under test, with +stubs replacing all dependencies. See `testing/stubs.md` for the layout. +Standard idiom: every external function the unit calls must have a stub +returning either a fixed value or a `will_return`-driven one. + +## Running a single test + +`make check` runs everything. To narrow: + +```sh +./tests/unittests/unittests --filter +``` + +(Confirm via `unittests --help`; cmocka filter support depends on version.) + +## See also + +- `testing/stubs.md` — adding/extending stubs. +- `playbooks/add-test.md` — adding a new test file end-to-end. diff --git a/wip/feat-ai.md b/wip/feat-ai.md new file mode 100644 index 0000000..b10ea3c --- /dev/null +++ b/wip/feat-ai.md @@ -0,0 +1,84 @@ +# WIP: `feat/ai` — AI assistant client + +**Branch:** `feat/ai` in cproof. +**Status:** in flight at the time of writing. Delete this file when the branch +merges to `master`; fold the relevant bits into the stable layer. + +This file describes the delta `feat/ai` adds on top of `master`. Load it only +when working on the `feat/ai` branch. + +## What the branch adds + +A new module: `src/ai/`. A multi-provider AI assistant accessible via the +`/ai` command (OpenAI, Perplexity). + +## New files + +| Path | Role | +|---|---| +| `src/ai/ai_client.c` | Provider abstraction, request/response, provider registry. | +| `src/ai/ai_client.h` | Public surface. Notable: `char* ai_providers_find(const char*, gboolean, void*);` (stateless autocomplete callback). | +| `tests/unittests/test_ai_client.c` | Unit tests for the AI client. | +| `tests/unittests/test_ai_client.h` | Header pair for the above. | +| `tests/unittests/ai/stub_ai_client.c` | Stubs for `ai_client.*` consumers in other unit tests. | +| `tests/unittests/ui/stub_ai.c` | UI-side stub for the AI surface consumed by `src/ui/`. | + +Plus added entries in: + +| File | Addition | +|---|---| +| `src/command/cmd_defs.c` | `/ai` command record. | +| `src/command/cmd_funcs.{c,h}` | `cmd_ai` handler. | +| `src/command/cmd_ac.c` | `_ai_autocomplete` dispatcher; static `ai_subcommands_ac`, `ai_set_subcommands_ac`, `ai_remove_subcommands_ac`. | +| `tests/unittests/unittests.c` | Test registrations for `test_ai_*`. | + +## Subcommand layout + +``` +/ai set {provider|token|org} +/ai remove provider +/ai start +/ai clear +/ai correct +/ai providers +``` + +## Autocomplete + +- `_ai_autocomplete` in `cmd_ac.c` is the per-command dispatcher. +- Top-level subcommand list: `ai_subcommands_ac` → `set`, `remove`, `start`, + `clear`, `correct`, `providers`. +- Set subcommands: `ai_set_subcommands_ac` → `provider`, `token`, `org`. +- Remove subcommands: `ai_remove_subcommands_ac` → `provider`. +- Provider-name completion: `ai_providers_find()` (`src/ai/ai_client.c`) — a + **stateless** callback (no `_last_match` global). On the `feat/ai` + branch, this is the most current canonical example of a stateless + autocomplete callback. + +## CI deltas + +`feat/ai` also brings (unrelated to AI itself): + +- `0feacbc9d ci: simulate Pikaur flag duplication in Arch Linux CI` — adds a + CI step that simulates the Pikaur duplicated-flag failure mode in the + Arch Linux job. Survives independently of the AI feature. + +If the AI work is merged but the CI delta is rebased separately, update +`build/ci.md` to mention this commit alongside `0722dc9e3`. + +## On merge — what to fold into stable layers + +When `feat/ai` lands on `master`, edit: + +| File | Change | +|---|---| +| `architecture/source-map.md` | Add `src/ai/` row to the module table. | +| `architecture/overview.md` | Add AI to the optional integrations / module list; drop the "active feature branches" caveat. | +| `architecture/test-map.md` | Add `tests/unittests/ai/` stub directory; mention `test_ai_client.{c,h}`. | +| `patterns/autocomplete.md` | Optionally add `ai_providers_find` as a second canonical stateless-callback example (or leave `roster_contact_autocomplete` as the sole example). | +| `playbooks/add-autocomplete.md` | Same. | +| `gotchas.md` | Same. | +| `build/ci.md` | Mention the Pikaur CI simulation commit alongside the original Pikaur fix. | + +Then **delete this file**. Update `INDEX.md` to drop the `wip/` row if no +other WIP entries remain.