Variant A (PAD_THRESHOLD=12000) bounds the pad but still re-enters win_redraw() from the print path on every message once the *live* buffer renders >= the threshold — reachable with a single oversized message (huge paste, narrow terminal, crafted incoming), reintroducing the per-message redraw storm.
Trigger the reclaim redraw on dead space (getcury - buffer->lines) instead of absolute pad height. After a redraw the cursor equals buffer->lines, so dead space is 0 regardless of buffer size: the reclaim can never fire while scrolling, and an oversized message no longer storms (and is shown in full rather than clipped). Dead space only accrues in a long append-only session that trimmed old entries — exactly when reclaiming is wanted.
- OTR: strip the whitespace tag by shifting the full message tail incl. the NUL, not tag_length bytes, so the body is no longer duplicated for messages longer than the tag.
- presence: snapshot the resource fields before connection_add_available_resource() takes ownership, removing a use-after-free in the own-presence path.
- OMEMO: propagate _omemo_finalize_identity_load() failure on connect (log + cons_show_error + stop) instead of leaving OMEMO silently unavailable.
- OMEMO: guard NULL fingerprint decode in _omemo_fingerprint_decode / omemo_is_trusted_identity / omemo_trust and log every decode failure instead of failing silently.
- stanza-id: gate XEP-0359 dedup on disco urn:xmpp:sid:0 (the `by` JID or its domain), falling back to no-dedup when caps are unknown; add functional tests for trusted vs untrusted server.
- accounts: narrow the group-name sanitizer to the characters GKeyFile forbids in headers ([ ] \n \r), keeping `=` and `#`, so read/write stays symmetric.
- build: re-introduce compiler/sanitizer flags in a Pikaur-safe form (opt-in sanitizers, -Wsign-compare) and fix the resulting -Wsign-compare warnings (incl. proftest _mkdir_recursive).fix: OTR/presence/OMEMO correctness, stanza-id disco gate, build hardening
Author: jabber.developer2 <jabber.developer2@jabber.space>
A tall multi-line message printed near the bottom of the ncurses pad was clipped because _win_print_internal grew the pad from the current cursor only (_win_ensure_pad_capacity(getcury)), not the height of the message about to be printed. The clipped message's captured height (e.g. 1 row for a 49-row message) was wrong while win_redraw later rendered it in full; the resulting buffer->lines / y_start_pos mismatch desynced the page-up scroll anchor, producing a ~one-page jump when scrolling past such messages from history.
Estimate the rendered height (hard newlines + soft-wrap over the usable width) and reserve that many rows before printing, so the message is never clipped and its captured height matches the redraw. All print paths go through _win_print_internal, so incoming/outgoing/history are covered.
Regression from the upstream sync (72f4f186d), which replaced the fixed-size pad with the dynamic _win_ensure_pad_capacity model.
The parameter `template` in format_call_external_argv() was renamed
to `template_fmt` to avoid collision with the `template` keyword in
C++. This improves compatibility when the header is included in
C++ code.
Some functions were declared without parameters in header files,
despite their implementations specifying arguments. This mismatch
is deprecated in all versions of C and is disallowed in C23.
This change updates the declarations to match their definitions,
ensuring compatibility with C standards, avoiding potential
undefined behavior, and improving support in tools and editors.
This change adds necessary `#include` directives that were previously
omitted in some header files. Without these includes, compilation
could fail or produce undefined behavior when the headers are used
in isolation (i.e., before their dependencies are included elsewhere).
Ensures better modularity and reliability of header usage.
PAD_THRESHOLD (3000) sat below the buffer line cap (PAD_SIZE - PAD_SIZE/10 = 9000), so _win_ensure_pad_capacity() fired a full win_redraw() from the hot print path on every message once a chat exceeded 3000 rendered lines. During history paging this turned one redraw per fetch into dozens, corrupting per-entry y_start_pos (offset jumps, dropped/skipped messages) and causing ~10s lag per page. The flat-file backend was byte-identical across the regression; the fault was purely in the UI pad path from the upstream merge.
Raise PAD_THRESHOLD to 12000 (above the cap) so the reclaim redraw only fires to drop dead pad space in long append-only sessions, never during scrolling. Also size the pad once to buffer->lines+100 in win_redraw() instead of shrinking to PAD_MIN_HEIGHT and regrowing per entry, removing the residual per-redraw wresize churn.
The trigger was wired into outgoing / outgoing-carbon / history paths
too, which made plugins like sounds.py play the "new message" sound
when opening a chat (history loads), sending a message yourself, or
receiving a carbon of your own sent message from another device.
Restrict the call to chatwin_incoming_msg — matches the semantic of
"a remote party sent a chat message and it was displayed".
Guard against re-entrant editor launches, abort editor on Ctrl-Z,
and centralize suspend-aware redraws through prof_doupdate() to prevent
terminal corruption when SIGTSTP is received while editor is active.
Removes SIGUSR1 editor escape in favor of standard pkill <editor> recovery.
First stage of removing the chatlog subsystem that writes plain-text
per-day message logs to $XDG_DATA_HOME/profanity/chatlogs/. These files
were never read back by Profanity (history replay reads from the
database via log_database_get_previous_chat), so the feature only ever
wrote to disk for users to inspect with external tools.
This change reduces chatlog.c to no-op stubs, preserving all public
signatures so the ~20 call sites in event/, otr/ and profanity.c
continue to compile and link unchanged. Subsequent stages will remove
the call sites, the related preferences (PREF_CHLOG, PREF_GRLOG and the
per-encryption PREF_*_LOG) and the now-dead commands (/logging,
/otr log, /pgp log, /omemo log, /ox log).
Database-based history (PREF_DBLOG, /history) and the debug log
(log.c, /log) are unaffected.
This enforces explicit provider specification.
Automatic fallback to preferences or "openai" is removed.
Users must now pass the provider name explicitly.
default provider functionality was improperly removed and implemented (some parts still remain intact), leading to UAF.
Add helper functions to parse hex digits and encode UTF-8 characters. Update buffer allocation to account for UTF-8 expansion and implement full surrogate pair support for characters outside the BMP. Previously, \uXXXX sequences were passed through verbatim; they are now correctly decoded into proper UTF-8 strings.
_win_printf dropped buffer append and render for any window in paged
state, but only WIN_CHAT can recover lost messages via chatwin_db_history()
on scroll-down. WIN_MUC, WIN_PRIVATE and WIN_AI have no such fallback, so
incoming and outgoing messages were silently lost when the user was viewing
history.
Introduce log_database_can_recover_messages() to check whether the DB
backend can replay messages (returns FALSE when PREF_DBLOG is "off",
"redact", or no backend is active). Gate the WIN_CHAT early return in
_win_printf() on this check: when recovery is impossible, fall through
and append to the buffer so messages remain visible on scroll-down.
Add a buffer-bottom reset in win_page_down() for non-chat windows.
WIN_SCROLL_REACHED_BOTTOM is only set on the is_chat DB branch, so
non-chat windows never clear paged on their own; reset paged and
unread_msg when the last line of the buffer reaches the screen.
Add missing scroll rendering for AI windows in the title bar draw
function, ensuring AI windows display their scrolled state consistently
with other window types.
Remove the manual paged/unread_msg reset before printing the user
message in cl_ev_send_ai_msg() -- it was a local workaround for the
same drop and is no longer needed.
Deprecate /logging command in favor of /history for chat logging control.
/history off now stops persistence (sets PREF_DBLOG=off + PREF_CHLOG=false)
in addition to hiding history on open. /history on restores persistence
(re-enabling PREF_DBLOG=on if it was off) and PREF_CHLOG.
statusbar
- PREF_STATUSBAR_SHOW_DBBACKEND (default ON) gates the [sqlite] /
[flatfile] indicator; toggle via "/statusbar show|hide dbbackend"
/history off|on
- "/history off" now stops persistence as well as hiding history on
open: sets PREF_DBLOG=off + PREF_CHLOG=false in addition to
PREF_HISTORY=false
- "/history on" restores persistence (re-enabling PREF_DBLOG=on if
it was off) and PREF_CHLOG, in addition to PREF_HISTORY=true
/logging
- Deprecated /logging command. It now prints a single notice
pointing to /history; CMD_PREAMBLE trimmed (min_args=0, no
setting_func, syntax/args/examples dropped); subcommand 'group'
removed from autocomplete
- All "use '/logging chat on' to enable" hints replaced with
"/history on" across /omemo-log, /pgp-log, /otr-log, and /ox-log
/privacy logging
- Single-pass validation across {on, off, redact, flatfile}
- Backend-switching values (on, flatfile) now take effect
immediately when connected (via log_database_switch_backend),
matching "/history switch" behaviour; off/redact only flip
pref bits and keep the live backend open
/correction off
- win_print_outgoing and win_print_outgoing_with_receipt now gate
_win_correct on PREF_CORRECTION_ALLOW, matching incoming-msg
paths. Previously a peer's correction reflected via XEP-0280
carbons (or the user's own /correct invocation) was applied
in-buffer regardless of the pref
console
- Drop orphaned /logging chat reference from cons_privacy_setting()
- Delete cons_logging_setting() and remove its call from
cons_show_log_prefs()
autocomplete
- Add dbbackend to statusbar_show_ac
- Remove logging_ac entries for chat/group subcommands
- Remove _logging_autocomplete() param handler for chat subcommand
database_flatfile
- Two local g_strndup allocations switched from char* with manual
g_free to auto_gchar gchar* for automatic cleanup
tests
- Update test_cmd_otr.c to expect new /history-on warning messages
@author: jabber.developer2 <jabber.developer2@jabber.space>
Add missing `ai_session_unref` calls to prevent memory leaks.
- In `_ai_request_thread`, release session on error paths and
after successful processing.
- In `cmd_ai_start`, release reference after passing ownership
to the AI window.
Replace manual g_free calls with auto_gchar for local_provider_name,
local_model, local_api_key, and response_data to ensure automatic
cleanup and prevent memory leaks.
Add an AI client module that integrates with OpenAI-compatible API
providers (OpenAI, Perplexity, and custom endpoints) to provide
AI-assisted chat within CProof. Users can start sessions with /ai start,
send prompts, receive responses in a dedicated AI window, switch between
providers and models, and manage API keys — all with tab-completion.
Providers are configured via /ai set commands with per-provider API keys,
endpoints, default models, and custom settings. Two default providers
(openai, perplexity) are seeded on first use. Provider state persists in
[ai/<name>] sections of the preferences keyfile with automatic migration
from the previous flat-key format.
The /ai command integrates into the existing command system with 8
subcommands covering provider management, session lifecycle, model
fetching, and conversation clearing. Autocomplete uses the standard
flat prefix-matching chain for reliable tab-completion at every nesting
level. A new ProfAiWin window type is added to the window system.
Architecture:
Async design: HTTP requests run on a background thread (pthread) to avoid blocking the ncurses UI loop; results are displayed on the main thread via direct function calls
Thread safety: AIProvider and AISession use atomic ref-counting and mutex-protected session state; the request thread snapshots all session data before making the HTTP call
Window validation: wins_ai_exists() prevents use-after-free when the user closes the AI window during an in-flight HTTP request (~60s)
Privacy: store:false is sent with every request to prevent providers from persisting conversations or using them for training
Response size limit: 10MB cap with immediate curl abort via CURL_WRITEFUNC_ERROR to prevent OOM
JSON parsing uses unified helpers for both chat responses and error
envelopes with consistent escape decoding. The response parser tries
Perplexity /v1/responses "text" field first, then falls back to OpenAI
"content". Error parsing extracts provider error.message from the
standard envelope format. Model parsing handles multiple API response
formats (OpenAI list, Perplexity, array) including edge cases.
Tests include 470+ lines of unit tests covering provider management,
session lifecycle, JSON parsing (multiple formats), autocomplete cycling,
and error handling, plus functional tests for /ai command dispatch.
A stub_ai.c module isolates unit tests from UI dependencies.
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>
Per XEP-0030 section 3.1: 'if an entity has no associated items,
it MUST return an empty <query/> element.'
The client should display 'No service discovery items for X' when
receiving an empty result, not silently ignore it.
Previous version introduced in commit f28655c5c (2016-05-08) which added an early
return when child == NULL, preventing the message from being shown.
The /connect command can take: account + server <s> + port <p> + tls <t> + auth <a>
which totals 9 arguments, not 7. This fixes argument parsing for full command usage.
- Track unread message indicators and paging state in the window component
- Clear and restore unread markers correctly when navigating between conversations
- Suppress buffer updates and message printing while viewing history to prevent
unwanted scroll jumps and viewport lock
Also includes minor code-style, whitespace, and comment cleanups as well as
updated documentation for metrics and buffer handling.
Add debug output tracking connection lifecycle.
- Track disconnects and reconnects
- Record session login, logout, and reconnection attempts
- Use [CONNDBG] tag for easy log filtering
Add prof_get_current_window API to retrieve the title of the currently active window, matching the titlebar display.
The feature is especially useful to track currently used plugin window.
Remove autodisable logic (prefs_set_autoping(0)) and early return in autoping
to treat intermittent false negatives from connection_supports(XMPP_FEATURE_PING).
Add one-time error display with debug features print for monitoring; warn on first failing
check without aborting to maintain functionality while investigating root cause.
Prior commit (6ad8a190) did not properly handle overflow by long (9000 lines) message,
to address this issue, multiple changes were made:
- Add lines recalculation on win_redraw
- Move `prof_buff_t` struct to header file so its lines could be externally changed
- Call win_redraw once overflow is detected: it allows to recalculate sizes in lines and apply changes to the buffer
Refactor buffer trimming logic to use dynamic line counts (_lines per entry)
against PAD_SIZE (10k) instead of fixed entry count (MAX_BUFFER_SIZE=200). This
prevents premature cleanups for short messages, reduces scrolling disruptions
during history loads, and scales better with rendered content size.
Delete oldest/newest entry from opposite end only when total lines approach
ncurses pad limit, minimizing unnecessary deletions.
Simplify overflow warning log by removing unused MAX_BUFFER_SIZE reference.
Update buffer.c header for improved DX:
- Add detailed module overview explaining role (in-memory UI buffer vs. DB
persistence), key features (trimming, metadata), and integration (ncurses).
- Shorten copyright/license boilerplate to concise pointer (LICENSE ref).
- Add fork notice; preserve original copyrights per GPLv3.
Partially addresses #36 (stuck scroll mitigation via fewer cleanups)
Related to #36
When running profanity under Valgrind with slashguard enabled, the
following error could occur:
```
[...]
==4021347== Invalid read of size 1
==4021347== at 0x4851F49: memchr (vg_replace_strmem.c:986)
==4021347== by 0x45CEAD: _inp_slashguard_check (inputwin.c:183)
==4021347== by 0x45CEAD: inp_readline (inputwin.c:225)
==4021347== by 0x431184: prof_run (profanity.c:121)
==4021347== by 0x42C609: main (main.c:176)
==4021347== Address 0xe850883 is 0 bytes after a block of size 3 alloc'd
==4021347== at 0x48477C4: malloc (vg_replace_malloc.c:446)
[...]
```
`memchr()` requires the complete memory that shall be searched to be
accessible. Using `strchr()` could work for shorter strings, but we only
want to search in the first 4 chars.
Instead of somehow working around those limitations, simply search manually
in the first 4 bytes.
Fixes: 3c56b289 ("Add slashguard feature")
Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
(cherry picked from commit 7f48452d84)
In the past `Py_XDECREF()` was a macro. Preserve compat to ancient Python
versions by having a trampoline which calls `Py_XDECREF()`.
Fixes: #2043
Fixes: c0da36c4 ("Rage-cleanup.")
Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
(cherry picked from commit 3299dd8fc6)
- Modified chatwin_outgoing_msg to call _chatwin_set_last_message for corrected messages, using replace_id per XEP-0308.
- Updated _chatwin_set_last_message to skip redundant and invalid free/strdup for same pointers.
- Added null checks in _chatwin_set_last_message for safety.
- Fixes autocompletion suggesting original message content instead of corrected content.
Fixes#24
Added null check for jidp in cmd_sub to handle jid_create returning NULL.
Crash occurred when processing malformed JID inputs like @example.com.
Ensures robust handling of invalid JIDs.
Fixes#22
- Added null check for `jid` in `cmd_caps` to handle `jid_create` returning NULL.
- Crash occurred when processing malformed inputs like `@example.com` in `/caps`.
- Ensures robust handling of invalid JID strings in `WIN_CHAT` and `WIN_CONSOLE` cases.
Fixes#20
Implement /force-encryption command to configure forced encryption
settings with on|off and policy resend-to-confirm|block options.
The changes enhance user experience by clarifying encryption requirements
and providing actionable commands. Users can now press Enter again to
confirm unencrypted messages in resend-to-confirm mode.