Files
cproof/Makefile.am
Jabber Developer 3f36c303c2
All checks were successful
CI Code / Check spelling (push) Successful in 21s
CI Code / Check coding style (push) Successful in 31s
CI Code / Code Coverage (push) Successful in 2m21s
CI Code / Linux (ubuntu) (push) Successful in 4m30s
CI Code / Linux (debian) (push) Successful in 6m43s
CI Code / Linux (arch) (push) Successful in 10m8s
feat(history): flat-file backend with bidirectional SQLite migration
A flat-file alternative to the SQLite chatlog backend with runtime
switching, full migration tooling, integrity verification, and a
synthetic load harness. SQLite remains the default; both backends share
one dispatch layer (db_backend_t vtable) so callers don't change.

Storage layout
- Per-contact append-only `flatlog/<account>/<contact>/history.log`
  under XDG_DATA_HOME, one line per message
- Single-line file header with embedded format-version marker
  (FLATFILE_FORMAT_VERSION); reader warns on missing or mismatched
  marker, writer and checker stay in sync via preprocessor
  stringification
- Deterministic key=value metadata (`id`, `aid`, `corrects`, `to`,
  `to_res`, `read`) plus escaped body \u2014 `\|`, `\]`, `\\`, `\n`, `\r`
  literals prevent log injection
- Sparse byte-offset index (FF_INDEX_STEP=500) per contact for
  O(log n) time-range lookups; rebuilt on inode / size / mtime
  change, extended in-place when the file just grew
- Per-contact GHashTable caches for archive_id presence and
  stanza_id \u2192 from_jid mapping (O(1) MAM dedup, O(1) LMC sender
  validation)

Hardening
- Path-traversal protection: JID directory name normalisation
  (`@` \u2192 `_at_`, slashes and `..` rejected at construction); every
  per-contact path is anchored under the account's flatlog/
  directory and validated before open
- Symlink-attack protection: every fopen / open uses O_NOFOLLOW; on
  ELOOP the operation aborts with an error rather than following
- Filesystem permissions: log files created with mode 0600,
  directories with mode 0700; both enforced at creation, verified
  on each open and reported on drift by `/history verify`
- Atomic crash-safe export: write to a temp file via mkstemp (mode
  0600, random suffix, no name collisions between concurrent
  exports), fsync, then rename \u2014 partial state never replaces the
  live file
- Concurrency: advisory flock(LOCK_EX) held for the duration of
  every write, including append from live messages and full rewrite
  from export, so two profanity processes can't interleave bytes
  on the same log
- DoS / abuse guards:
    * FF_MAX_LINE_LEN = 10 MB \u2014 lines longer than this are rejected
      at read with a warning; the parser will not allocate
      unbounded memory for a single record
    * FF_MAX_LMC_DEPTH = 100 \u2014 `corrects:` chain walk stops at this
      depth and emits a warning, preventing a malicious correction
      cycle from spinning the apply pass
    * FF_VERSION_SCAN_MAX = 16 \u2014 header version probe never reads
      past 16 leading comment lines, even on garbage input
    * Empty / inverted byte-range early-return in page-up read path
      so a malformed time filter cannot cause an unbounded scan
    * Zero-entry index guard so a file whose every line failed to
      parse cannot cause a NULL deref on later page-up
- LMC sender validation: an incoming correction whose sender does
  not match the original message's sender is rejected at write
  time and surfaced via cons_show_error; a cycle in the apply pass
  is broken via a visited-set
- jid_create_from_bare_and_resource treats NULL, empty string, and
  the literal "(null)" as no resource and returns a bare jid;
  similar normalisation for barejid eliminates the legacy
  "user@host/(null)" artefact that leaked into stored fulljids
  whenever g_strdup_printf("%s", NULL) ran inside create_fulljid

Commands
- `/history switch sqlite|flatfile` \u2014 runtime backend swap, closes
  the old backend and opens the new one without reconnecting
- `/history export [<jid>]` \u2014 SQLite -> flat-file, merging with any
  existing flatlog (dedup keyed on a SHA-256 hash mixing stanza_id,
  timestamp, from_jid, body \u2014 robust against id reuse by older
  clients)
- `/history import [<jid>]` \u2014 flat-file -> SQLite, same merge
  semantics, runs inside a single SQLite transaction with rollback
  on per-contact failure
- `/history verify [<jid>]` \u2014 integrity check; emits a structured
  list of issues (ERROR / WARNING / INFO) per file:
    * file-level: missing log, wrong permissions (\u2260 0600), UTF-8
      BOM present, CRLF line endings, empty file
    * line-level: invalid UTF-8 (with byte offset), embedded
      control characters, unparsable lines, timestamps out of
      order, duplicate `id:` and `aid:` (tracked separately so a
      stanza/archive id collision isn't double-reported)
    * cross-line: broken `corrects:` references whose target id is
      not present in the file
- `/history backend` \u2014 show currently active backend
- Active backend indicator `[sqlite]` / `[flatfile]` in the status
  bar next to the JID
- Roster-JID autocomplete for verify / export / import
- export and import open a SQLite handle on demand when the
  flatfile backend is currently active, so migration works
  regardless of which backend is live

Tests
- Unit: database_export (parser round-trip, escape/unescape, dedup
  key stability, JID normalisation), database_stress (14 cases
  exercising rapid writes, large messages, deep LMC chains, MAM
  dedup, concurrent contacts)
- Functional: history persistence across reconnects, export /
  import round-trip with content equality, MUC migration,
  timestamp normalisation across timezones
- Bench harness P1\u2013P5 (synthetic load: bulk insert, time-range
  read, page-up scroll, MAM ingest, mixed workload) and failure
  modes F1\u2013F17 (page-up cursor and forward-iteration symmetry,
  oversized lines, MAM dedup, LMC depth and cycles, BOM/CRLF,
  missing log, empty file, mtime+inode flip, broken corrects, etc.)
- All bench tests integrate with the existing make targets and
  emit CSV rows for baseline comparison

Author: jabber.developer2 <jabber.developer2@jabber.space>
Reviewed-by: jabber.developer <jabber.developer@jabber.space>
2026-05-05 19:26:07 +00:00

698 lines
28 KiB
Makefile
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

core_sources = \
src/xmpp/contact.c src/xmpp/contact.h \
src/log.c src/common.c \
src/chatlog.c src/chatlog.h \
src/database.h src/database.c \
src/database_flatfile.c src/database_flatfile.h \
src/database_flatfile_parser.c \
src/database_flatfile_verify.c \
src/database_export.c \
src/log.h src/profanity.c src/common.h \
src/profanity.h src/xmpp/chat_session.c \
src/xmpp/chat_session.h src/xmpp/muc.c src/xmpp/muc.h src/xmpp/jid.h src/xmpp/jid.c \
src/xmpp/chat_state.h src/xmpp/chat_state.c \
src/xmpp/resource.c src/xmpp/resource.h \
src/xmpp/roster_list.c src/xmpp/roster_list.h \
src/xmpp/xmpp.h src/xmpp/capabilities.c src/xmpp/session.c \
src/xmpp/connection.h src/xmpp/connection.c \
src/xmpp/iq.c src/xmpp/message.c src/xmpp/presence.c src/xmpp/stanza.c \
src/xmpp/stanza.h src/xmpp/message.h src/xmpp/iq.h src/xmpp/presence.h \
src/xmpp/capabilities.h src/xmpp/session.h \
src/xmpp/roster.c src/xmpp/roster.h \
src/xmpp/bookmark.c src/xmpp/bookmark.h \
src/xmpp/blocking.c src/xmpp/blocking.h \
src/xmpp/form.c src/xmpp/form.h \
src/xmpp/avatar.c src/xmpp/avatar.h \
src/xmpp/ox.c src/xmpp/ox.h \
src/xmpp/vcard.c src/xmpp/vcard.h src/xmpp/vcard_funcs.h \
src/event/common.c src/event/common.h \
src/event/server_events.c src/event/server_events.h \
src/event/client_events.c src/event/client_events.h \
src/ui/ui.h src/ui/window.c src/ui/window.h src/ui/core.c \
src/ui/titlebar.c src/ui/statusbar.c src/ui/inputwin.c \
src/ui/titlebar.h src/ui/statusbar.h src/ui/inputwin.h \
src/ui/screen.h src/ui/screen.c \
src/ui/console.c src/ui/notifier.c \
src/ui/win_types.h \
src/ui/window_list.c src/ui/window_list.h \
src/ui/rosterwin.c src/ui/occupantswin.c \
src/ui/buffer.c src/ui/buffer.h \
src/ui/chatwin.c \
src/ui/mucwin.c \
src/ui/privwin.c \
src/ui/confwin.c \
src/ui/xmlwin.c \
src/ui/vcardwin.c \
src/command/cmd_defs.h src/command/cmd_defs.c \
src/command/cmd_funcs.h src/command/cmd_funcs.c \
src/command/cmd_ac.h src/command/cmd_ac.c \
src/tools/parser.c \
src/tools/parser.h \
src/tools/http_common.c \
src/tools/http_common.h \
src/tools/http_upload.c \
src/tools/http_upload.h \
src/tools/http_download.c \
src/tools/http_download.h \
src/tools/plugin_download.c \
src/tools/plugin_download.h \
src/tools/bookmark_ignore.c \
src/tools/bookmark_ignore.h \
src/tools/autocomplete.c src/tools/autocomplete.h \
src/tools/clipboard.c src/tools/clipboard.h \
src/tools/editor.c src/tools/editor.h \
src/config/files.c src/config/files.h \
src/config/conflists.c src/config/conflists.h \
src/config/accounts.c src/config/accounts.h \
src/config/tlscerts.c src/config/tlscerts.h \
src/config/account.c src/config/account.h \
src/config/preferences.c src/config/preferences.h \
src/config/theme.c src/config/theme.h \
src/config/color.c src/config/color.h \
src/config/scripts.c src/config/scripts.h \
src/config/cafile.c src/config/cafile.h \
src/plugins/plugins.h src/plugins/plugins.c \
src/plugins/api.h src/plugins/api.c \
src/plugins/callbacks.h src/plugins/callbacks.c \
src/plugins/autocompleters.c src/plugins/autocompleters.h \
src/plugins/themes.c src/plugins/themes.h \
src/plugins/settings.c src/plugins/settings.h \
src/plugins/disco.c src/plugins/disco.h \
src/ui/tray.h src/ui/tray.c
unittest_sources = \
src/xmpp/contact.c src/xmpp/contact.h src/common.c \
src/log.h src/profanity.c src/common.h \
src/profanity.h src/xmpp/chat_session.c \
src/xmpp/chat_session.h src/xmpp/muc.c src/xmpp/muc.h src/xmpp/jid.h src/xmpp/jid.c \
src/xmpp/resource.c src/xmpp/resource.h \
src/xmpp/chat_state.h src/xmpp/chat_state.c \
src/xmpp/roster_list.c src/xmpp/roster_list.h \
src/xmpp/xmpp.h src/xmpp/form.c \
src/ui/ui.h \
src/otr/otr.h \
src/pgp/gpg.h \
src/pgp/ox.h \
src/omemo/omemo.h \
src/omemo/crypto.h \
src/omemo/store.h \
src/xmpp/vcard.h src/xmpp/vcard_funcs.h \
src/database_flatfile.h \
src/database_flatfile_parser.c \
src/command/cmd_defs.h src/command/cmd_defs.c \
src/command/cmd_funcs.h src/command/cmd_funcs.c \
src/command/cmd_ac.h src/command/cmd_ac.c \
src/tools/parser.c \
src/tools/parser.h \
src/tools/autocomplete.c src/tools/autocomplete.h \
src/tools/clipboard.c src/tools/clipboard.h \
src/tools/editor.c src/tools/editor.h \
src/tools/bookmark_ignore.c \
src/tools/bookmark_ignore.h \
src/config/accounts.h \
src/config/account.c src/config/account.h \
src/config/files.c src/config/files.h \
src/config/tlscerts.c src/config/tlscerts.h \
src/config/preferences.c src/config/preferences.h \
src/config/theme.c src/config/theme.h \
src/config/color.c src/config/color.h \
src/config/scripts.c src/config/scripts.h \
src/config/conflists.c src/config/conflists.h \
src/plugins/plugins.h src/plugins/plugins.c \
src/plugins/api.h src/plugins/api.c \
src/plugins/callbacks.h src/plugins/callbacks.c \
src/plugins/autocompleters.c src/plugins/autocompleters.h \
src/plugins/themes.c src/plugins/themes.h \
src/plugins/settings.c src/plugins/settings.h \
src/plugins/disco.c src/plugins/disco.h \
src/ui/window_list.c src/ui/window_list.h \
src/event/common.c src/event/common.h \
src/event/server_events.c src/event/server_events.h \
src/event/client_events.c src/event/client_events.h \
src/ui/tray.h src/ui/tray.c \
tests/prof_cmocka.h \
tests/unittests/xmpp/stub_vcard.c \
tests/unittests/xmpp/stub_avatar.c \
tests/unittests/xmpp/stub_ox.c \
tests/unittests/xmpp/stub_xmpp.c \
tests/unittests/xmpp/stub_message.c \
tests/unittests/ui/stub_ui.c tests/unittests/ui/stub_ui.h \
tests/unittests/ui/stub_vcardwin.c \
tests/unittests/log/stub_log.c \
tests/unittests/chatlog/stub_chatlog.c \
tests/unittests/database/stub_database.c \
tests/unittests/config/stub_accounts.c \
tests/unittests/config/stub_cafile.c \
tests/unittests/tools/stub_http_upload.c \
tests/unittests/tools/stub_http_download.c \
tests/unittests/tools/stub_aesgcm_download.c \
tests/unittests/tools/stub_plugin_download.c \
tests/unittests/helpers.c tests/unittests/helpers.h \
tests/unittests/test_form.c tests/unittests/test_form.h \
tests/unittests/test_common.c tests/unittests/test_common.h \
tests/unittests/test_autocomplete.c tests/unittests/test_autocomplete.h \
tests/unittests/test_jid.c tests/unittests/test_jid.h \
tests/unittests/test_parser.c tests/unittests/test_parser.h \
tests/unittests/test_roster_list.c tests/unittests/test_roster_list.h \
tests/unittests/test_chat_session.c tests/unittests/test_chat_session.h \
tests/unittests/test_contact.c tests/unittests/test_contact.h \
tests/unittests/test_preferences.c tests/unittests/test_preferences.h \
tests/unittests/test_server_events.c tests/unittests/test_server_events.h \
tests/unittests/test_muc.c tests/unittests/test_muc.h \
tests/unittests/test_cmd_presence.c tests/unittests/test_cmd_presence.h \
tests/unittests/test_cmd_alias.c tests/unittests/test_cmd_alias.h \
tests/unittests/test_cmd_connect.c tests/unittests/test_cmd_connect.h \
tests/unittests/test_cmd_rooms.c tests/unittests/test_cmd_rooms.h \
tests/unittests/test_cmd_account.c tests/unittests/test_cmd_account.h \
tests/unittests/test_cmd_sub.c tests/unittests/test_cmd_sub.h \
tests/unittests/test_cmd_bookmark.c tests/unittests/test_cmd_bookmark.h \
tests/unittests/test_cmd_otr.c tests/unittests/test_cmd_otr.h \
tests/unittests/test_cmd_pgp.c tests/unittests/test_cmd_pgp.h \
tests/unittests/test_cmd_join.c tests/unittests/test_cmd_join.h \
tests/unittests/test_cmd_roster.c tests/unittests/test_cmd_roster.h \
tests/unittests/test_cmd_disconnect.c tests/unittests/test_cmd_disconnect.h \
tests/unittests/test_callbacks.c tests/unittests/test_callbacks.h \
tests/unittests/test_plugins_disco.c tests/unittests/test_plugins_disco.h \
tests/unittests/test_forced_encryption.c tests/unittests/test_forced_encryption.h \
tests/unittests/test_database_export.c tests/unittests/test_database_export.h \
tests/unittests/test_database_stress.c tests/unittests/test_database_stress.h \
tests/unittests/unittests.c
functionaltest_sources = \
tests/prof_cmocka.h \
tests/functionaltests/proftest.c tests/functionaltests/proftest.h \
tests/functionaltests/test_connect.c tests/functionaltests/test_connect.h \
tests/functionaltests/test_ping.c tests/functionaltests/test_ping.h \
tests/functionaltests/test_rooms.c tests/functionaltests/test_rooms.h \
tests/functionaltests/test_presence.c tests/functionaltests/test_presence.h \
tests/functionaltests/test_message.c tests/functionaltests/test_message.h \
tests/functionaltests/test_chat_session.c tests/functionaltests/test_chat_session.h \
tests/functionaltests/test_carbons.c tests/functionaltests/test_carbons.h \
tests/functionaltests/test_receipts.c tests/functionaltests/test_receipts.h \
tests/functionaltests/test_roster.c tests/functionaltests/test_roster.h \
tests/functionaltests/test_software.c tests/functionaltests/test_software.h \
tests/functionaltests/test_muc.c tests/functionaltests/test_muc.h \
tests/functionaltests/test_disconnect.c tests/functionaltests/test_disconnect.h \
tests/functionaltests/test_history.c tests/functionaltests/test_history.h \
tests/functionaltests/test_lastactivity.c tests/functionaltests/test_lastactivity.h \
tests/functionaltests/test_autoping.c tests/functionaltests/test_autoping.h \
tests/functionaltests/test_disco.c tests/functionaltests/test_disco.h \
tests/functionaltests/test_export_import.c tests/functionaltests/test_export_import.h \
tests/functionaltests/functionaltests.c
main_source = src/main.c
python_sources = \
src/plugins/python_plugins.h src/plugins/python_plugins.c \
src/plugins/python_api.h src/plugins/python_api.c
c_sources = \
src/plugins/c_plugins.h src/plugins/c_plugins.c \
src/plugins/c_api.h src/plugins/c_api.c
git_include = src/gitversion.h
pgp_sources = \
src/pgp/gpg.h src/pgp/gpg.c \
src/pgp/ox.h src/pgp/ox.c
pgp_unittest_sources = \
tests/unittests/pgp/stub_gpg.c \
tests/unittests/pgp/stub_ox.c
otr4_sources = \
src/otr/otrlib.h src/otr/otrlibv4.c src/otr/otr.h src/otr/otr.c
omemo_sources = \
src/omemo/omemo.h src/omemo/omemo.c src/omemo/crypto.h src/omemo/crypto.c \
src/omemo/store.h src/omemo/store.c src/xmpp/omemo.h src/xmpp/omemo.c \
src/tools/aesgcm_download.h src/tools/aesgcm_download.c
omemo_unittest_sources = \
tests/unittests/omemo/stub_omemo.c
if BUILD_PYTHON_API
core_sources += $(python_sources)
unittest_sources += $(python_sources)
endif
if BUILD_C_API
core_sources += $(c_sources)
unittest_sources += $(c_sources)
endif
otr_unittest_sources = \
tests/unittests/otr/stub_otr.c
themes_sources = $(top_srcdir)/themes/*
icons_sources = $(top_srcdir)/icons/*
script_sources = bootstrap.sh
man1_sources = $(wildcard $(top_srcdir)/docs/profanity*.1)
if BUILD_PGP
core_sources += $(pgp_sources)
unittest_sources += $(pgp_unittest_sources)
endif
if BUILD_OTR
unittest_sources += $(otr_unittest_sources)
core_sources += $(otr4_sources)
endif
if BUILD_OMEMO
core_sources += $(omemo_sources)
unittest_sources += $(omemo_unittest_sources)
endif
sqlite_sources = src/database_sqlite.c
if BUILD_SQLITE
core_sources += $(sqlite_sources)
endif
all_c_sources = $(core_sources) $(unittest_sources) \
$(pgp_sources) $(pgp_unittest_sources) \
$(otr4_sources) $(otr_unittest_sources) \
$(omemo_sources) $(omemo_unittest_sources) \
$(c_sources) $(python_sources) \
$(main_source)
AM_CFLAGS = @AM_CFLAGS@ -I$(srcdir)/src
bin_PROGRAMS = profanity
profanity_SOURCES = $(core_sources) $(main_source)
if THEMES_INSTALL
profanity_themesdir = @THEMES_PATH@
profanity_themes_DATA = $(themes_sources)
endif
if INCLUDE_GIT_VERSION
BUILT_SOURCES = $(git_include)
endif
profanity_iconsdir = @ICONS_PATH@
profanity_icons_DATA = $(icons_sources)
if BUILD_C_API
lib_LTLIBRARIES = libprofanity.la
libprofanity_la_LDFLAGS = -version-info 0:0:0 -shared -no-undefined
libprofanity_la_SOURCES = src/plugins/profapi.c
library_includedir=$(includedir)
library_include_HEADERS = src/plugins/profapi.h
endif
TESTS = tests/unittests/unittests
check_PROGRAMS = tests/unittests/unittests
tests_unittests_unittests_CPPFLAGS = -I$(srcdir)/tests
tests_unittests_unittests_SOURCES = $(unittest_sources)
tests_unittests_unittests_LDADD = -lcmocka
# Functional tests require libstabber.
# They are only built when libstabber is available.
# See: https://github.com/profanity-im/profanity/pull/1010
# https://github.com/profanity-im/stabber/issues/5
#
# Note: We use forkpty() instead of libexpect for PTY handling.
if HAVE_STABBER
if HAVE_FORKPTY
TESTS += tests/functionaltests/functionaltests
check_PROGRAMS += tests/functionaltests/functionaltests
tests_functionaltests_functionaltests_SOURCES = $(functionaltest_sources)
tests_functionaltests_functionaltests_CPPFLAGS = -I$(srcdir)/tests
tests_functionaltests_functionaltests_CFLAGS = $(AM_CFLAGS)
tests_functionaltests_functionaltests_LDADD = -lcmocka -lstabber @FORKPTY_LIB@
# Parallel functional tests target (~3x faster than sequential)
# Usage: make check-functional-parallel
# To add more groups: increase FUNC_TEST_GROUPS and add group in functionaltests.c
FUNC_TEST_GROUPS = 1 2 3 4
check-functional-parallel: tests/functionaltests/functionaltests
@echo "Running functional tests in parallel ($(words $(FUNC_TEST_GROUPS)) groups)..."
@mkdir -p $(builddir)/test-logs $(builddir)/test-files
@pids=""; \
for g in $(FUNC_TEST_GROUPS); do \
./tests/functionaltests/functionaltests $$g > $(builddir)/test-logs/group$$g.log 2>&1 & \
pids="$$pids $$!"; \
done; \
failed=0; i=1; \
for pid in $$pids; do \
wait $$pid || { echo "Group $$i FAILED"; cat $(builddir)/test-logs/group$$i.log; failed=1; }; \
i=$$((i + 1)); \
done; \
echo "=== Test Results Summary ==="; \
grep -E 'PASSED|FAILED|Running' $(builddir)/test-logs/group*.log || true; \
if [ $$failed -ne 0 ]; then echo "FUNCTIONAL TESTS FAILED"; exit 1; fi; \
echo "All functional test groups passed!"
# Run functional tests with the flat-file database backend
# Usage: make check-functional-flatfile
check-functional-flatfile: tests/functionaltests/functionaltests
@echo "Running functional tests with flat-file backend ($(words $(FUNC_TEST_GROUPS)) groups)..."
@mkdir -p $(builddir)/test-logs $(builddir)/test-files
@pids=""; \
for g in $(FUNC_TEST_GROUPS); do \
PROF_FLATFILE=1 ./tests/functionaltests/functionaltests $$g > $(builddir)/test-logs/group$$g-flatfile.log 2>&1 & \
pids="$$pids $$!"; \
done; \
failed=0; i=1; \
for pid in $$pids; do \
wait $$pid || { echo "Group $$i FAILED (flatfile)"; cat $(builddir)/test-logs/group$$i-flatfile.log; failed=1; }; \
i=$$((i + 1)); \
done; \
echo "=== Flat-file Test Results Summary ==="; \
grep -E 'PASSED|FAILED|Running' $(builddir)/test-logs/group*-flatfile.log || true; \
if [ $$failed -ne 0 ]; then echo "FLAT-FILE FUNCTIONAL TESTS FAILED"; exit 1; fi; \
echo "All flat-file functional test groups passed!"
endif
endif
man1_MANS = $(man1_sources)
# ---------------------------------------------------------------------------
# Bench harness — synthetic database load tests. Not part of `make check`.
# Build only when invoked via `make bench` / `make bench-quick`.
# Sources live in tests/bench/. See tests/bench/README.md.
bench_common_sources = \
tests/bench/bench_stubs.c \
tests/bench/bench_common.c tests/bench/bench_common.h \
tests/bench/bench_csv.c tests/bench/bench_csv.h \
src/database_flatfile.c src/database_flatfile.h \
src/database_flatfile_parser.c \
src/database_flatfile_verify.c \
src/common.c src/common.h
# Sources needed only by the export/import bench (S7/S8). Pulls in the SQLite
# backend, the cross-backend export/import code, and the dispatcher.
bench_export_sources = \
src/database.c src/database.h \
src/database_sqlite.c \
src/database_export.c
EXTRA_PROGRAMS = \
tests/bench/gen_history \
tests/bench/bench_runner \
tests/bench/bench_long_messages \
tests/bench/bench_failure_modes \
tests/bench/bench_export_import
tests_bench_gen_history_SOURCES = tests/bench/gen_history.c $(bench_common_sources)
tests_bench_gen_history_CPPFLAGS = -I$(srcdir)/src -I$(srcdir)/tests/bench
tests_bench_gen_history_CFLAGS = $(AM_CFLAGS)
tests_bench_gen_history_LDADD =
tests_bench_bench_runner_SOURCES = tests/bench/bench_runner.c $(bench_common_sources)
tests_bench_bench_runner_CPPFLAGS = -I$(srcdir)/src -I$(srcdir)/tests/bench
tests_bench_bench_runner_CFLAGS = $(AM_CFLAGS)
tests_bench_bench_runner_LDADD =
tests_bench_bench_long_messages_SOURCES = tests/bench/bench_long_messages.c $(bench_common_sources)
tests_bench_bench_long_messages_CPPFLAGS = -I$(srcdir)/src -I$(srcdir)/tests/bench
tests_bench_bench_long_messages_CFLAGS = $(AM_CFLAGS)
tests_bench_bench_long_messages_LDADD =
tests_bench_bench_failure_modes_SOURCES = tests/bench/bench_failure_modes.c $(bench_common_sources)
tests_bench_bench_failure_modes_CPPFLAGS = -I$(srcdir)/src -I$(srcdir)/tests/bench
tests_bench_bench_failure_modes_CFLAGS = $(AM_CFLAGS)
tests_bench_bench_failure_modes_LDADD =
tests_bench_bench_export_import_SOURCES = tests/bench/bench_export_import.c \
$(bench_common_sources) $(bench_export_sources)
tests_bench_bench_export_import_CPPFLAGS = -I$(srcdir)/src -I$(srcdir)/tests/bench
tests_bench_bench_export_import_CFLAGS = $(AM_CFLAGS)
tests_bench_bench_export_import_LDADD =
# Volume control: BENCH_VOLUME={small|medium|max}. Default `small` is safe
# even on tight disks (~5 MB). `max` is heavyweight — see README.
BENCH_VOLUME ?= small
BENCH_DATA_DIR ?= /tmp/cproof-bench-corpus
BENCH_CSV ?= tests/bench/current.csv
bench-build: tests/bench/gen_history tests/bench/bench_runner tests/bench/bench_long_messages tests/bench/bench_failure_modes tests/bench/bench_export_import
# Resolve --lines / --years for the configured volume.
bench-gen: bench-build
@mkdir -p $(BENCH_DATA_DIR)
@case "$(BENCH_VOLUME)" in \
small) L=10000; Y=1; P=mixed ;; \
medium) L=500000; Y=5; P=mixed ;; \
max) L=5000000; Y=10; P=long ;; \
*) echo "BENCH_VOLUME must be small|medium|max"; exit 2 ;; \
esac; \
echo "==> generating volume=$(BENCH_VOLUME) lines=$$L years=$$Y profile=$$P"; \
./tests/bench/gen_history --lines=$$L --years=$$Y --msg-len-profile=$$P \
--lmc-rate=3 --mam-ooo-rate=0 --resources-per-contact=3 \
--output=$(BENCH_DATA_DIR) --seed=42
bench-run: bench-build
@echo "==> running scenarios (csv=$(BENCH_CSV))"
@BENCH_VOLUME=$(BENCH_VOLUME) BENCH_DATA_DIR=$(BENCH_DATA_DIR) \
./tests/bench/bench_runner --data=$(BENCH_DATA_DIR) --csv=$(BENCH_CSV)
@echo "==> CSV: $(BENCH_CSV)"
@cat $(BENCH_CSV)
bench: bench-gen bench-run
bench-quick:
@$(MAKE) bench BENCH_VOLUME=small
# Long-message tests (L1L14). Independent of corpus volume, uses its own tmp dir.
bench-longmsg: bench-build
@echo "==> long-message tests (csv=$(BENCH_CSV))"
@./tests/bench/bench_long_messages \
--tmp=$(BENCH_DATA_DIR)-longmsg --csv=$(BENCH_CSV)
@rm -rf $(BENCH_DATA_DIR)-longmsg
# Failure-injection (F1F15). Independent corpus, asserts behaviour on bad data.
bench-failure: bench-build
@echo "==> failure-injection tests (csv=$(BENCH_CSV))"
@./tests/bench/bench_failure_modes \
--tmp=$(BENCH_DATA_DIR)-fail --csv=$(BENCH_CSV)
@rm -rf $(BENCH_DATA_DIR)-fail
# Export/import (S7/S8) pipeline. Independent corpus under BENCH_DATA_DIR-export.
# Default scale: 100k rows (~530 s). Override with BENCH_PIPE_ROWS=N.
BENCH_PIPE_ROWS ?= 100000
BENCH_PIPE_VOLUME ?= pipe$(BENCH_PIPE_ROWS)
bench-export: bench-build
@echo "==> S7 export pipeline ($(BENCH_PIPE_ROWS) rows, csv=$(BENCH_CSV))"
@rm -rf $(BENCH_DATA_DIR)-export
@BENCH_DATA_DIR=$(BENCH_DATA_DIR)-export \
./tests/bench/bench_export_import seed --rows=$(BENCH_PIPE_ROWS) --account=bench@bench.example
@BENCH_DATA_DIR=$(BENCH_DATA_DIR)-export \
./tests/bench/bench_export_import export --account=bench@bench.example \
--csv=$(BENCH_CSV) --label=S7a_export_cold --volume=$(BENCH_PIPE_VOLUME)
@echo " -- second pass (dedup hot path)"
@BENCH_DATA_DIR=$(BENCH_DATA_DIR)-export \
./tests/bench/bench_export_import export --account=bench@bench.example \
--csv=$(BENCH_CSV) --label=S7b_export_dedup --volume=$(BENCH_PIPE_VOLUME)
@rm -rf $(BENCH_DATA_DIR)-export
bench-import: bench-build
@echo "==> S8 import pipeline ($(BENCH_PIPE_ROWS) rows, csv=$(BENCH_CSV))"
@rm -rf $(BENCH_DATA_DIR)-export
@# Seed account A, export it, then wipe its DB but keep flatlog. Import
@# back into the same account — that way the to: header in flatfile
@# matches the importing account so dedup works correctly.
@BENCH_DATA_DIR=$(BENCH_DATA_DIR)-export \
./tests/bench/bench_export_import seed --rows=$(BENCH_PIPE_ROWS) --account=bench@bench.example
@BENCH_DATA_DIR=$(BENCH_DATA_DIR)-export \
./tests/bench/bench_export_import export --account=bench@bench.example \
--csv=$(BENCH_CSV) --label=S7_seed_export --volume=$(BENCH_PIPE_VOLUME)
@rm -f $(BENCH_DATA_DIR)-export/database/bench_at_bench.example/chatlog.db*
@BENCH_DATA_DIR=$(BENCH_DATA_DIR)-export \
./tests/bench/bench_export_import import --account=bench@bench.example \
--csv=$(BENCH_CSV) --label=S8a_import_cold --volume=$(BENCH_PIPE_VOLUME)
@echo " -- second import (idempotency)"
@BENCH_DATA_DIR=$(BENCH_DATA_DIR)-export \
./tests/bench/bench_export_import import --account=bench@bench.example \
--csv=$(BENCH_CSV) --label=S8b_import_idempotent --volume=$(BENCH_PIPE_VOLUME)
@rm -rf $(BENCH_DATA_DIR)-export
# Full roundtrip with content diff at default volume. PASSes only if
# every row in the rebuilt DB matches the source byte-for-byte.
bench-roundtrip: bench-build
@echo "==> S8e roundtrip+full diff ($(BENCH_PIPE_ROWS) rows, csv=$(BENCH_CSV))"
@rm -rf $(BENCH_DATA_DIR)-export
@BENCH_DATA_DIR=$(BENCH_DATA_DIR)-export \
./tests/bench/bench_export_import roundtrip --rows=$(BENCH_PIPE_ROWS) \
--csv=$(BENCH_CSV) --label=S8e_roundtrip --volume=$(BENCH_PIPE_VOLUME) --full-diff
@rm -rf $(BENCH_DATA_DIR)-export
# Pipeline at medium volume (default BENCH_PIPE_ROWS=100k).
bench-pipeline: bench-export bench-import bench-roundtrip
# Pipeline at MAX volume — 1M rows. Heavy: ~510 min, ~500MB SQLite + ~500MB
# flatfile + ~500MB second SQLite. Run only when you can afford the time
# and the disk. Set BENCH_PIPE_ROWS_MAX to override (default 1_000_000).
BENCH_PIPE_ROWS_MAX ?= 1000000
bench-pipeline-max: bench-build
@echo "==> bench-pipeline-max: $(BENCH_PIPE_ROWS_MAX) rows (heavy!)"
@$(MAKE) bench-export BENCH_PIPE_ROWS=$(BENCH_PIPE_ROWS_MAX)
@$(MAKE) bench-import BENCH_PIPE_ROWS=$(BENCH_PIPE_ROWS_MAX)
@$(MAKE) bench-roundtrip BENCH_PIPE_ROWS=$(BENCH_PIPE_ROWS_MAX)
# S9: 200 contacts × 50k lines per contact = 10M lines distributed.
bench-multicontact: bench-build
@mkdir -p $(BENCH_DATA_DIR)-multi
@echo "==> S9: 200 contacts x 5000 lines"
@./tests/bench/gen_history --lines=1000000 --contacts=200 --years=3 \
--msg-len-profile=mixed --output=$(BENCH_DATA_DIR)-multi --seed=42
@BENCH_VOLUME=multicontact BENCH_DATA_DIR=$(BENCH_DATA_DIR)-multi \
./tests/bench/bench_runner --data=$(BENCH_DATA_DIR)-multi --csv=$(BENCH_CSV) \
--scenarios=S4,S6
@rm -rf $(BENCH_DATA_DIR)-multi
# S10: 30% LMC corrections.
bench-lmc: bench-build
@mkdir -p $(BENCH_DATA_DIR)-lmc
@echo "==> S10: LMC-heavy corpus (30%% corrections)"
@./tests/bench/gen_history --lines=100000 --years=2 --lmc-rate=30 \
--msg-len-profile=mixed --output=$(BENCH_DATA_DIR)-lmc --seed=42
@BENCH_VOLUME=lmc BENCH_DATA_DIR=$(BENCH_DATA_DIR)-lmc \
./tests/bench/bench_runner --data=$(BENCH_DATA_DIR)-lmc --csv=$(BENCH_CSV)
@rm -rf $(BENCH_DATA_DIR)-lmc
# S11: 20% MAM out-of-order timestamps.
bench-ooo: bench-build
@mkdir -p $(BENCH_DATA_DIR)-ooo
@echo "==> S11: MAM-OOO corpus (20%% out-of-order)"
@./tests/bench/gen_history --lines=100000 --years=2 --mam-ooo-rate=20 \
--msg-len-profile=mixed --output=$(BENCH_DATA_DIR)-ooo --seed=42
@BENCH_VOLUME=ooo BENCH_DATA_DIR=$(BENCH_DATA_DIR)-ooo \
./tests/bench/bench_runner --data=$(BENCH_DATA_DIR)-ooo --csv=$(BENCH_CSV) \
--scenarios=S6
@rm -rf $(BENCH_DATA_DIR)-ooo
# Run everything: P1 scenarios + long messages + failure injection + multicontact + LMC + OOO.
bench-full: bench bench-longmsg bench-failure bench-multicontact bench-lmc bench-ooo
# Compare current.csv against baseline.csv and exit non-zero on regressions.
BENCH_BASELINE ?= tests/bench/baseline.csv
BENCH_THRESHOLD ?= 25
bench-compare:
@python3 tests/bench/compare_baseline.py \
--baseline=$(BENCH_BASELINE) --current=$(BENCH_CSV) \
--threshold=$(BENCH_THRESHOLD)
# Snapshot current.csv as the new baseline. Run this only after manually
# reviewing that the numbers look healthy.
bench-update-baseline:
@cp $(BENCH_CSV) $(BENCH_BASELINE)
@echo "baseline updated: $(BENCH_BASELINE)"
bench-clean:
rm -rf $(BENCH_DATA_DIR) $(BENCH_DATA_DIR)-longmsg $(BENCH_DATA_DIR)-fail \
$(BENCH_DATA_DIR)-multi $(BENCH_DATA_DIR)-lmc $(BENCH_DATA_DIR)-ooo \
$(BENCH_DATA_DIR)-export $(BENCH_CSV)
.PHONY: bench bench-quick bench-build bench-gen bench-run bench-clean \
bench-longmsg bench-failure bench-multicontact bench-lmc bench-ooo \
bench-full bench-compare bench-update-baseline \
bench-export bench-import bench-roundtrip bench-pipeline bench-pipeline-max
EXTRA_DIST = $(man1_sources) $(icons_sources) $(themes_sources) $(script_sources) profrc.example theme_template LICENSE.txt README.md CHANGELOG
# Ship API documentation with `make dist`
EXTRA_DIST += \
apidocs/c/c-prof.conf \
apidocs/c/gen.sh \
apidocs/c/profapi.h \
apidocs/c/profhooks.h \
apidocs/python/conf.py \
apidocs/python/gen.sh \
apidocs/python/index.rst \
apidocs/python/Makefile \
apidocs/python/src/plugin.py \
apidocs/python/src/prof.py
# Bench harness sources (not built by default; see EXTRA_PROGRAMS above)
EXTRA_DIST += \
tests/bench/README.md \
tests/bench/compare_baseline.py \
tests/bench/gen_history.c \
tests/bench/bench_runner.c \
tests/bench/bench_long_messages.c \
tests/bench/bench_failure_modes.c \
tests/bench/bench_export_import.c \
tests/bench/bench_stubs.c \
tests/bench/bench_common.c tests/bench/bench_common.h \
tests/bench/bench_csv.c tests/bench/bench_csv.h \
tests/bench/baseline.csv
if INCLUDE_GIT_VERSION
EXTRA_DIST += .git/HEAD .git/index
$(git_include).in: .git/HEAD .git/index
rm -f $@
echo "#ifndef PROF_GIT_BRANCH" >> $@
echo "#define PROF_GIT_BRANCH \"$(shell git rev-parse --symbolic-full-name --abbrev-ref HEAD)\"" >> $@
echo "#endif" >> $@
echo "#ifndef PROF_GIT_REVISION" >> $@
echo "#define PROF_GIT_REVISION \"$(shell git log --pretty=format:'%h' -n 1)\"" >> $@
echo "#endif" >> $@
#
# Create $(git_include) atomically to catch possible race. The race can occur
# when $(git_include) is generated in parallel with building of src/profanity.c.
# So this hack allows to find and fix the problem earlier.
#
$(git_include): $(git_include).in
cp $< $@
clean-local:
rm -f $(git_include) $(git_include).in
endif
clean-functional-tests:
rm -rf $(builddir)/test-files $(builddir)/test-logs
.PHONY: my-prof.supp clean-functional-tests
my-prof.supp:
@sed '/^# AUTO-GENERATED START/q' prof.supp > $@
@printf "\n\n# glib\n" >> $@
@cat /usr/share/glib-2.0/valgrind/glib.supp >> $@
@printf "\n\n# Python\n" >> $@
@wget -O- https://raw.githubusercontent.com/python/cpython/refs/tags/v`python3 --version | cut -d' ' -f2`/Misc/valgrind-python.supp >> $@
@printf "\n\n# gtk\n" >> $@
@test -z "@GTK_VERSION@" || wget -O- https://raw.githubusercontent.com/GNOME/gtk/refs/tags/@GTK_VERSION@/gtk.supp >> $@
@printf "\n\n# more gtk\n" >> $@
@test -z "@GTK_VERSION@" || cat /usr/share/gtk-3.0/valgrind/gtk.supp >> $@
check-unit: tests/unittests/unittests
tests/unittests/unittests
@VALGRIND_CHECK_RULES@
VALGRIND_SUPPRESSIONS_FILES=$(srcdir)/prof.supp
# Code coverage targets (requires --enable-coverage)
coverage-clean:
find . -name '*.gcda' -delete
find . -name '*.gcno' -delete
rm -rf coverage-html coverage.info
coverage-report: check
lcov --capture --directory . --output-file coverage.info --ignore-errors inconsistent
lcov --remove coverage.info '/usr/*' '*/tests/*' --output-file coverage.info --ignore-errors inconsistent
genhtml coverage.info --output-directory coverage-html
@echo "Coverage report generated in coverage-html/index.html"
.PHONY: coverage-clean coverage-report
format: $(all_c_sources)
clang-format -i $(all_c_sources)
format-sources: $(core_sources) $(main_source)
clang-format -i $^
spell:
codespell
doublecheck: format check spell