Compare commits

..

3 Commits

Author SHA1 Message Date
886372caac build: add cache to ./configure with -C flag
All checks were successful
CI Code / Check spelling (pull_request) Successful in 23s
CI Code / Check coding style (pull_request) Successful in 38s
CI Code / Linux (arch) (pull_request) Successful in 12m20s
CI Code / Linux (ubuntu) (pull_request) Successful in 14m59s
CI Code / Linux (debian) (pull_request) Successful in 15m18s
Add the -C flag to ./configure to enable caching, reducing redundant
resource fetching during builds when packages and the machine remain
unchanged. Caching may cause issues only if applied across different machines
or if packages are modified, neither of which is the case during the build.
2025-10-14 09:47:14 +00:00
0b0b02d6e0 build: use local libstrophe version and limit depth
`--depth 1` flag ensures that only latest version is loaded (since there is no need to sync full git)
Using local libstrophe version increases speed of fetching
2025-10-14 09:47:14 +00:00
d748ffc527 build: Add multithreading to make
nproc is a standard util that prints amount of available processes.
-j is a flag that specifies how many cores can be utilized by `make` (default is 1)
-j$(nproc) allows to use all the available machine cores, potentially significantly speeding up completion of CI actions
2025-10-14 09:47:14 +00:00
85 changed files with 1291 additions and 5436 deletions

View File

@@ -50,9 +50,6 @@ jobs:
run: |
grep -P 'auto_(char|gchar|gcharv|guchar|jid|sqlite|gfd|FILE)[\w *]*;$' -r src && exit -1 || true
- name: Check CWE-134 format string vulnerabilities
run: ./check-cwe134.sh
- name: Install clang-format
run: |
sudo apt-get update
@@ -101,13 +98,3 @@ jobs:
- name: Check spelling
run: |
codespell
coverage:
runs-on: ubuntu-latest
name: Code Coverage
steps:
- uses: actions/checkout@v4
- name: Build and run coverage
run: |
docker build -f Dockerfile.arch -t profanity-cov .
docker run profanity-cov ./ci-build.sh --coverage-only

7
.gitignore vendored
View File

@@ -62,8 +62,6 @@ tests/unittests/unittests
tests/unittests/unittests.log
tests/unittests/unittests.trs
test-suite.log
test-files/
test-logs/
# valgrind output
profval*
@@ -109,8 +107,3 @@ breaks
*.tar.*
*.zip
*.log*
coverage/
*.gcno
*.gcda
*.gcov
coverage.info

View File

@@ -1,16 +1,3 @@
0.16.0 (unreleased)
===================
Changes:
- Add flat-file database backend as alternative to SQLite for message history.
Stores messages as human-readable plain text files. Configure with `/privacy logging flatfile`.
Files are stored in ~/.local/share/profanity/flatlog/.
- Add `/history verify [<jid>]` command to check integrity of stored message
history (works with both SQLite and flat-file backends).
- Add vtable-based database backend abstraction allowing pluggable storage.
- Add `make check-functional-flatfile` target to run functional tests with
flat-file backend.
0.15.0 (2025-03-27)
===================

View File

@@ -90,24 +90,6 @@ set -e
```
This will run the same tests that the CI runs and refuse the push if it fails.
The CI script runs 4 parallel builds with different configurations:
- **Full** — all features enabled (+ coverage in `--coverage-only` mode)
- **Minimal** — all optional features disabled
- **NoEncrypt** — no encryption (OTR, PGP, OMEMO disabled)
- **Default** — default ./configure options
Each build runs Valgrind and functional tests on Linux.
Use `./ci-build.sh --coverage-only` to run only the Full build with coverage collection.
Output shows test results per build:
```
✓ Full PASSED
Unit tests: 437 passed, 0 failed
Functional tests: 69 passed, 0 failed
Coverage: Lines: 27.5% | Functions: 36.2% | Branches: 18.1%
Duration: 5m39s
```
Note that it will run on the actual content of the repository directory and not
what may have been staged/committed.
@@ -123,20 +105,6 @@ Test your changes with the following tools to find mistakes.
Run `make check` to run the unit tests with your current configuration or `./ci-build.sh` to check with different switches passed to configure.
### flat-file backend tests
To run functional tests with the flat-file database backend (instead of SQLite):
```bash
make check-functional-flatfile
```
Or manually for a single group:
```bash
PROF_FLATFILE=1 PROF_TEST_GROUP=1 ./tests/functionaltests/functionaltests 1
```
### valgrind
We provide a suppressions file `prof.supp`. It is a combination of the suppressions for shipped with glib2, python and custom rules.
@@ -158,16 +126,6 @@ scan-build make
scan-view ...
```
### Security checks
We have a static analyzer `check-cwe134.sh` that detects CWE-134 format string vulnerabilities. It runs automatically in CI but you can also run it locally:
```bash
./check-cwe134.sh
```
This checks for unsafe patterns where data could be passed directly as a format string to functions like `printf`, `cons_show`, etc. Never pass a raw string for formatting; use `"%s"` format specifier instead.
### Finding typos
We include a `.codespellrc` configuration file for `codespell` in the root directory.
@@ -179,55 +137,3 @@ You can run the `make spell` command for this.
`make doublecheck` will run the code formatter, spell checker and unit tests.
### Functional tests
The functional test suite uses [stabber](https://git.jabber.space/devs/stabber) as a mock XMPP server. Tests are located in `tests/functionaltests/`.
#### Running functional tests
Functional tests require stabber to be installed. Once installed, tests run as part of `make check`:
```bash
make check # Run all tests (unit + functional)
make check-functional-parallel # Run functional tests in parallel (~3x faster)
./tests/functionaltests/functionaltests # Run all functional tests sequentially
./tests/functionaltests/functionaltests 1 # Run specific group (1-4)
```
#### Test groups
Tests are organized into 4 groups for parallel execution:
| Group | Description |
|-------|-------------|
| 1 | Connect, Ping, Rooms, Software |
| 2 | Message, Receipts, Roster, Chat Session |
| 3 | Presence, Disconnect |
| 4 | MUC, Carbons |
To add a new group:
1. Define the test array in `functionaltests.c`
2. Add entry to `groups[]` array
3. Update `FUNC_TEST_GROUPS` in `Makefile.am`
#### Writing functional tests
Use content-based stubbing with stabber:
```c
// Use stbbr_for_query for IQ queries (roster, disco, etc.)
stbbr_for_query("jabber:iq:roster", "<iq type='result'>...</iq>");
// Use stbbr_send for presence, message, and push-style stanzas
stbbr_send("<presence from='buddy@localhost'>...</presence>");
```
Guidelines:
1. Use `stbbr_for_query(namespace, xml)` for IQ queries where the namespace is stable.
2. Use `stbbr_send(xml)` for presence, message, and other push-style stanzas.
3. Keep assertions tolerant of ordering when possible; use `prof_output_regex()` for flexible matching.
4. If timing issues appear, use `prof_timeout()` around critical expectations and reset afterwards.
5. When adding new tests, place them in the appropriate group based on functionality.

View File

@@ -1,25 +1,13 @@
FROM archlinux:latest
FROM archlinux
ENV TERM=xterm
ENV CC="ccache gcc"
RUN pacman -Syyu --noconfirm
# reflector is optional - if it fails due to network issues, continue with default mirrorlist
RUN pacman -S --needed --noconfirm reflector && \
(reflector --latest 20 --protocol https --sort rate --save /etc/pacman.d/mirrorlist || true)
RUN pacman-key --init
RUN pacman -S --needed --noconfirm \
RUN pacman -Syu --noconfirm && pacman -S --needed --noconfirm \
autoconf \
autoconf-archive \
automake \
base-devel \
ca-certificates \
ccache \
check \
cmake \
cmocka \
lcov \
curl \
debuginfod \
doxygen \
@@ -27,7 +15,7 @@ RUN pacman -S --needed --noconfirm \
gcc \
git \
gpgme \
gtk3 \
gtk2 \
libgcrypt \
libmicrohttpd \
libnotify \
@@ -66,13 +54,13 @@ USER root
RUN pacman -U --noconfirm libstrophe-git/libstrophe-git-*.pkg.tar.zst
WORKDIR /usr/src
RUN git clone --depth 1 https://git.jabber.space/devs/stabber
#RUN git clone https://github.com/boothj5/stabber
WORKDIR /usr/src/stabber
RUN ./bootstrap.sh
RUN ./configure --prefix=/usr --disable-dependency-tracking
RUN make -j$(nproc)
RUN make install
#WORKDIR /usr/src/stabber
#RUN ./bootstrap.sh
#RUN ./configure --prefix=/usr --disable-dependency-tracking
#RUN make
#RUN make install
WORKDIR /usr/src/profanity
COPY . /usr/src/profanity

View File

@@ -1,19 +1,14 @@
# Build the latest Debian testing image
FROM debian:testing
ENV DEBIAN_FRONTEND="noninteractive"
ENV TERM=xterm
ENV CC="ccache gcc"
RUN apt-get update && apt-get install -y --no-install-recommends \
autoconf \
autoconf-archive \
automake \
ca-certificates \
ccache \
expect \
gcc \
git \
lcov \
libcmocka-dev \
libcurl3-dev \
libgcrypt-dev \
@@ -41,14 +36,14 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
RUN mkdir -p /usr/src/{stabber,libstrophe,profanity}
WORKDIR /usr/src
RUN git clone --depth 1 https://git.jabber.space/devs/stabber
RUN git clone --depth 1 https://github.com/strophe/libstrophe
#RUN git clone https://github.com/boothj5/stabber
RUN git clone --depth 1 https://git.jabber.space/devs/libstrophe-gh.git libstrophe
WORKDIR /usr/src/stabber
RUN ./bootstrap.sh
RUN ./configure --prefix=/usr --disable-dependency-tracking
RUN make -j$(nproc)
RUN make install
#WORKDIR /usr/src/stabber
#RUN ./bootstrap.sh
#RUN ./configure --prefix=/usr --disable-dependency-tracking
#RUN make
#RUN make install
WORKDIR /usr/src/libstrophe
RUN ./bootstrap.sh

View File

@@ -1,9 +1,7 @@
# Build the latest Fedora image
FROM fedora:latest
ENV TERM=xterm
ENV CC="ccache gcc"
# expect - for functional tests
# libmicrohttpd - for stabber
# glibc-locale - to have en_US locale
RUN dnf install -y \
@@ -12,10 +10,9 @@ RUN dnf install -y \
automake \
awk \
ca-certificates \
ccache \
expect-devel \
gcc \
git \
lcov \
glib2-devel \
glibc-all-langpacks \
gtk2-devel \
@@ -46,21 +43,21 @@ RUN dnf install -y \
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
ENV LC_ALL en_US.UTF-8
ENV TERM=xterm
RUN mkdir -p /usr/src
WORKDIR /usr/src
RUN git clone --depth 1 https://git.jabber.space/devs/stabber
WORKDIR /usr/src/stabber
RUN ./bootstrap.sh
RUN ./configure --prefix=/usr --disable-dependency-tracking
RUN make -j$(nproc)
RUN make install
#RUN mkdir -p /usr/src/stabber
#RUN git clone https://github.com/boothj5/stabber
#WORKDIR /usr/src/stabber
#RUN ./bootstrap.sh
#RUN ./configure --prefix=/usr --disable-dependency-tracking
#RUN make
#RUN make install
WORKDIR /usr/src
RUN mkdir -p /usr/src/libstrophe
RUN git clone --depth 1 https://github.com/strophe/libstrophe
RUN git clone --depth 1 https://git.jabber.space/devs/libstrophe-gh.git libstrophe
WORKDIR /usr/src/libstrophe
RUN ./bootstrap.sh
RUN ./configure --prefix=/usr

View File

@@ -1,20 +1,16 @@
# Build the latest openSUSE Tumbleweed image
FROM opensuse/tumbleweed
ENV TERM=xterm
ENV CC="ccache gcc"
# expect - for functional tests
# libmicrohttpd - for stabber
# glibc-locale - to have en_US locale
RUN zypper --non-interactive in --no-recommends \
autoconf \
autoconf-archive \
automake \
ca-certificates \
ccache \
expect-devel \
gcc \
git \
lcov \
glib2-devel \
glibc-locale \
gtk2-devel \
@@ -45,17 +41,17 @@ RUN zypper --non-interactive in --no-recommends \
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
ENV LC_ALL en_US.UTF-8
ENV TERM=xterm
RUN mkdir -p /usr/src
WORKDIR /usr/src
RUN git clone --depth 1 https://git.jabber.space/devs/stabber
WORKDIR /usr/src/stabber
RUN ./bootstrap.sh
RUN ./configure --prefix=/usr --disable-dependency-tracking
RUN make -j$(nproc)
RUN make install
#RUN mkdir -p /usr/src/stabber
#RUN git clone git://github.com/boothj5/stabber.git
#WORKDIR /usr/src/stabber
#RUN ./bootstrap.sh
#RUN ./configure --prefix=/usr --disable-dependency-tracking
#RUN make
#RUN make install
RUN mkdir -p /usr/src/profanity
WORKDIR /usr/src/profanity

View File

@@ -1,18 +1,15 @@
FROM ubuntu:latest
ENV DEBIAN_FRONTEND="noninteractive"
ENV TERM=xterm
ENV CC="ccache gcc"
RUN apt-get update && apt-get install -y --no-install-recommends \
autoconf \
autoconf-archive \
automake \
ca-certificates \
ccache \
expect \
gcc \
git \
lcov \
libcmocka-dev \
libcurl3-dev \
libgcrypt-dev \
@@ -40,14 +37,15 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
RUN mkdir -p /usr/src/{stabber,libstrophe,profanity}
WORKDIR /usr/src
RUN git clone --depth 1 https://git.jabber.space/devs/stabber
RUN git clone --depth 1 https://github.com/strophe/libstrophe
#RUN git clone https://github.com/boothj5/stabber
RUN git clone --depth 1 https://git.jabber.space/devs/libstrophe-gh.git libstrophe
WORKDIR /usr/src/stabber
RUN ./bootstrap.sh
RUN ./configure --prefix=/usr --disable-dependency-tracking
RUN make -j$(nproc)
RUN make install
# TODO: Re-enable once libmicrohttpd-dev has been updated.
#WORKDIR /usr/src/stabber
#RUN ./bootstrap.sh
#RUN ./configure --prefix=/usr --disable-dependency-tracking
#RUN make
#RUN make install
WORKDIR /usr/src/libstrophe
RUN ./bootstrap.sh

View File

@@ -3,10 +3,6 @@ core_sources = \
src/log.c src/common.c \
src/chatlog.c src/chatlog.h \
src/database.h src/database.c \
src/database_sqlite.c \
src/database_flatfile.c src/database_flatfile.h \
src/database_flatfile_parser.c \
src/database_flatfile_verify.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 \
@@ -189,7 +185,6 @@ functionaltest_sources = \
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_lastactivity.c tests/functionaltests/test_lastactivity.h \
tests/functionaltests/functionaltests.c
main_source = src/main.c
@@ -291,70 +286,25 @@ endif
TESTS = tests/unittests/unittests
check_PROGRAMS = tests/unittests/unittests
tests_unittests_unittests_CPPFLAGS = -I$(srcdir)/tests
tests_unittests_unittests_CPPFLAGS = -Itests/
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
# Functional test were commented out because of:
# https://github.com/profanity-im/profanity/pull/1010
# An issue was raised for stabber:
# https://github.com/profanity-im/stabber/issues/5
# Once this issue is resolved functional tests should be enabled again
#
# 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
#if HAVE_STABBER
#if HAVE_EXPECT
#TESTS += tests/functionaltests/functionaltests
#check_PROGRAMS += tests/functionaltests/functionaltests
#tests_functionaltests_functionaltests_SOURCES = $(functionaltest_sources)
#tests_functionaltests_functionaltests_CFLAGS = $(AM_CFLAGS) -I/usr/include/tcl8.6 -I/usr/include/tcl8.5
#tests_functionaltests_functionaltests_LDADD = -lcmocka -lstabber -lexpect
#endif
#endif
man1_MANS = $(man1_sources)
@@ -397,10 +347,7 @@ 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
.PHONY: my-prof.supp
my-prof.supp:
@sed '/^# AUTO-GENERATED START/q' prof.supp > $@
@printf "\n\n# glib\n" >> $@
@@ -416,21 +363,7 @@ 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
VALGRIND_SUPPRESSIONS_FILES=prof.supp
format: $(all_c_sources)
clang-format -i $(all_c_sources)

View File

@@ -20,13 +20,13 @@
typedef char* PROF_WIN_TAG;
/** Function pointer for command callbacks, accepting an array of arguments. */
typedef void (*CMD_CB)(char** args);
typedef void(*CMD_CB)(char **args);
/** Function pointer for timed callbacks, accepting no arguments. */
typedef void (*TIMED_CB)(void);
typedef void(*TIMED_CB)(void);
/** Function pointer for window callbacks, accepting a window tag and input line. */
typedef void (*WINDOW_CB)(PROF_WIN_TAG win, char* line);
typedef void(*WINDOW_CB)(PROF_WIN_TAG win, char *line);
/** Highlights the console window in the status bar. */
void prof_cons_alert(void);
@@ -41,7 +41,7 @@ void prof_cons_alert(void);
* prof_cons_show("This will appear in the console window");
* @endcode
*/
int prof_cons_show(const char* const message);
int prof_cons_show(const char * const message);
/**
* Shows a message in the console with a specified theme.
@@ -57,7 +57,7 @@ int prof_cons_show(const char* const message);
* prof_cons_show_themed("myplugin", "text", NULL, "Plugin themed message");
* @endcode
*/
int prof_cons_show_themed(const char* const group, const char* const item, const char* const def, const char* const message);
int prof_cons_show_themed(const char *const group, const char *const item, const char *const def, const char *const message);
/**
* Shows a message indicating a command was used incorrectly.
@@ -69,7 +69,7 @@ int prof_cons_show_themed(const char* const group, const char* const item, const
* prof_cons_bad_cmd_usage("/mycommand");
* @endcode
*/
int prof_cons_bad_cmd_usage(const char* const cmd);
int prof_cons_bad_cmd_usage(const char *const cmd);
/**
* Registers a new command with help information and callback.
@@ -91,9 +91,9 @@ int prof_cons_bad_cmd_usage(const char* const cmd);
* prof_register_command("/newcommand", 1, 2, synopsis, "Example command", args, examples, my_function);
* @endcode
*/
void prof_register_command(const char* command_name, int min_args, int max_args,
char** synopsis, const char* description, char* arguments[][2], char** examples,
CMD_CB callback);
void prof_register_command(const char *command_name, int min_args, int max_args,
char **synopsis, const char *description, char *arguments[][2], char **examples,
CMD_CB callback);
/**
* Registers a function to be called periodically by CProof.
@@ -118,7 +118,7 @@ void prof_register_timed(TIMED_CB callback, int interval_seconds);
* prof_completer_add("/mycommand", items);
* @endcode
*/
void prof_completer_add(const char* key, char** items);
void prof_completer_add(const char *key, char **items);
/**
* Removes autocomplete values for a command or argument.
@@ -131,7 +131,7 @@ void prof_completer_add(const char* key, char** items);
* prof_completer_remove("/mycommand", items);
* @endcode
*/
void prof_completer_remove(const char* key, char** items);
void prof_completer_remove(const char *key, char **items);
/**
* Clears all autocomplete values for a command or argument.
@@ -142,7 +142,7 @@ void prof_completer_remove(const char* key, char** items);
* prof_completer_clear("/mycommand");
* @endcode
*/
void prof_completer_clear(const char* key);
void prof_completer_clear(const char *key);
/**
* Adds filepath autocompletion for a command or argument.
@@ -153,7 +153,7 @@ void prof_completer_clear(const char* key);
* prof_filepath_completer_add("/filecmd");
* @endcode
*/
void prof_filepath_completer_add(const char* prefix);
void prof_filepath_completer_add(const char *prefix);
/**
* Sends a desktop notification.
@@ -166,7 +166,7 @@ void prof_filepath_completer_add(const char* prefix);
* prof_notify("Example notification", 5000, "Example plugin");
* @endcode
*/
void prof_notify(const char* message, int timeout_ms, const char* category);
void prof_notify(const char *message, int timeout_ms, const char *category);
/**
* Sends a line of input to CProof to execute.
@@ -177,7 +177,7 @@ void prof_notify(const char* message, int timeout_ms, const char* category);
* prof_send_line("/who online");
* @endcode
*/
void prof_send_line(char* line);
void prof_send_line(char *line);
/**
* Gets the Jabber ID of the current chat recipient.
@@ -185,17 +185,6 @@ void prof_send_line(char* line);
*/
char* prof_get_current_recipient(void);
/**
* Gets the title of the current window as shown in the titlebar.
*
* Special cases:
* - MUC configuration window: Includes "config" and an asterisk if modified (e.g., "room@example.com config *").
* - vCard window: Includes the bare JID and an asterisk if modified (e.g., "vCard user@example.com *").
*
* @return A null-terminated string containing the window title. The caller must free the returned string.
*/
char* prof_get_current_window(void);
/**
* Gets the Jabber ID of the current chat room.
* @return The room Jabber ID (e.g., "metalchat@conference.example.com") or NULL if not in a room.
@@ -209,7 +198,7 @@ char* prof_get_current_muc(void);
int prof_current_win_is_console(void);
/**
* Gets the user's nickname in the current chat room.
* Gets the users nickname in the current chat room.
* @return The nickname (e.g., "eddie") or NULL if not in a room.
*/
char* prof_get_current_nick(void);
@@ -219,14 +208,14 @@ char* prof_get_current_nick(void);
* @param barejid The Jabber ID.
* @return The nickname (e.g., "eddie") or the barejid if not in the roster.
*/
char* prof_get_name_from_roster(const char* barejid);
char* prof_get_name_from_roster(const char *barejid);
/**
* Gets the barejid for a nickname from the roster.
* @param name The nickname.
* @return The barejid (e.g., "eddie@server.tld") or NULL if not in the roster.
*/
char* prof_get_barejid_from_roster(const char* name);
char* prof_get_barejid_from_roster(const char *name);
/**
* Gets nicknames of all occupants in the current chat room.
@@ -236,10 +225,10 @@ char** prof_get_current_occupants(void);
/**
* Gets the current nickname in a chat room.
* @param barejid The room's Jabber ID.
* @param barejid The rooms Jabber ID.
* @return The nickname.
*/
char* prof_get_room_nick(const char* barejid);
char* prof_get_room_nick(const char *barejid);
/**
* Logs a debug message to the CProof log.
@@ -250,7 +239,7 @@ char* prof_get_room_nick(const char* barejid);
* prof_log_debug("Debug message");
* @endcode
*/
void prof_log_debug(const char* message);
void prof_log_debug(const char *message);
/**
* Logs an info message to the CProof log.
@@ -261,7 +250,7 @@ void prof_log_debug(const char* message);
* prof_log_info("Info message");
* @endcode
*/
void prof_log_info(const char* message);
void prof_log_info(const char *message);
/**
* Logs a warning message to the CProof log.
@@ -272,7 +261,7 @@ void prof_log_info(const char* message);
* prof_log_warning("Warning message");
* @endcode
*/
void prof_log_warning(const char* message);
void prof_log_warning(const char *message);
/**
* Logs an error message to the CProof log.
@@ -283,7 +272,7 @@ void prof_log_warning(const char* message);
* prof_log_error("Error message");
* @endcode
*/
void prof_log_error(const char* message);
void prof_log_error(const char *message);
/**
* Creates a plugin window.
@@ -332,7 +321,7 @@ int prof_win_focus(PROF_WIN_TAG win);
* prof_win_show("My Plugin", "Message in plugin window");
* @endcode
*/
int prof_win_show(PROF_WIN_TAG win, char* message);
int prof_win_show(PROF_WIN_TAG win, char *message);
/**
* Shows a message in a plugin window with a specified theme.
@@ -349,7 +338,7 @@ int prof_win_show(PROF_WIN_TAG win, char* message);
* prof_win_show_themed("My Plugin", "myplugin", "text", NULL, "Themed message");
* @endcode
*/
int prof_win_show_themed(PROF_WIN_TAG tag, char* group, char* key, char* def, char* message);
int prof_win_show_themed(PROF_WIN_TAG tag, char *group, char *key, char *def, char *message);
/**
* Sends an XMPP stanza.
@@ -361,7 +350,7 @@ int prof_win_show_themed(PROF_WIN_TAG tag, char* group, char* key, char* def, ch
* prof_send_stanza("<iq to='juliet@capulet.lit' id='s2c1' type='get'><ping xmlns='urn:xmpp:ping'/></iq>");
* @endcode
*/
int prof_send_stanza(char* stanza);
int prof_send_stanza(char *stanza);
/**
* Gets a boolean setting.
@@ -376,7 +365,7 @@ int prof_send_stanza(char* stanza);
* prof_settings_boolean_get("myplugin", "notify", 0);
* @endcode
*/
int prof_settings_boolean_get(char* group, char* key, int def);
int prof_settings_boolean_get(char *group, char *key, int def);
/**
* Sets a boolean setting.
@@ -390,7 +379,7 @@ int prof_settings_boolean_get(char* group, char* key, int def);
* prof_settings_boolean_set("myplugin", "activate", 1);
* @endcode
*/
void prof_settings_boolean_set(char* group, char* key, int value);
void prof_settings_boolean_set(char *group, char *key, int value);
/**
* Gets a string setting.
@@ -405,7 +394,7 @@ void prof_settings_boolean_set(char* group, char* key, int value);
* prof_settings_string_get("myplugin", "prefix", "prefix-->");
* @endcode
*/
char* prof_settings_string_get(char* group, char* key, char* def);
char* prof_settings_string_get(char *group, char *key, char *def);
/**
* Sets a string setting.
@@ -419,7 +408,7 @@ char* prof_settings_string_get(char* group, char* key, char* def);
* prof_settings_string_set("myplugin", "prefix", "myplugin, ");
* @endcode
*/
void prof_settings_string_set(char* group, char* key, char* value);
void prof_settings_string_set(char *group, char *key, char *value);
/**
* Gets a string list setting, separated by semicolons.
@@ -433,7 +422,7 @@ void prof_settings_string_set(char* group, char* key, char* value);
* prof_settings_string_list_get("myplugin", "somelist");
* @endcode
*/
char** prof_settings_string_list_get(char* group, char* key);
char** prof_settings_string_list_get(char *group, char *key);
/**
* Adds an item to a string list setting.
@@ -448,7 +437,7 @@ char** prof_settings_string_list_get(char* group, char* key);
* prof_settings_string_list_add("myplugin", "somelist", "anelement");
* @endcode
*/
void prof_settings_string_list_add(char* group, char* key, char* value);
void prof_settings_string_list_add(char *group, char *key, char *value);
/**
* Removes an item from a string list setting.
@@ -463,7 +452,7 @@ void prof_settings_string_list_add(char* group, char* key, char* value);
* prof_settings_string_list_remove("myplugin", "somelist", "anelement");
* @endcode
*/
int prof_settings_string_list_remove(char* group, char* key, char* value);
int prof_settings_string_list_remove(char *group, char *key, char *value);
/**
* Clears all items from a string list setting.
@@ -477,7 +466,7 @@ int prof_settings_string_list_remove(char* group, char* key, char* value);
* prof_settings_string_list_clear("myplugin", "somelist");
* @endcode
*/
int prof_settings_string_list_clear(char* group, char* key);
int prof_settings_string_list_clear(char *group, char *key);
/**
* Gets an integer setting.
@@ -492,7 +481,7 @@ int prof_settings_string_list_clear(char* group, char* key);
* prof_settings_int_get("myplugin", "timeout", 10);
* @endcode
*/
int prof_settings_int_get(char* group, char* key, int def);
int prof_settings_int_get(char *group, char *key, int def);
/**
* Sets an integer setting.
@@ -506,7 +495,7 @@ int prof_settings_int_get(char* group, char* key, int def);
* prof_settings_int_set("myplugin", "timeout", 100);
* @endcode
*/
void prof_settings_int_set(char* group, char* key, int value);
void prof_settings_int_set(char *group, char *key, int value);
/**
* Triggers handling of an incoming message as if received by CProof.
@@ -519,10 +508,10 @@ void prof_settings_int_set(char* group, char* key, int value);
* prof_incoming_message("bob@server.org", "laptop", "Hello there");
* @endcode
*/
void prof_incoming_message(char* barejid, char* resource, char* message);
void prof_incoming_message(char *barejid, char *resource, char *message);
/**
* Adds a service discovery feature to CProof's supported features.
* Adds a service discovery feature to CProofs supported features.
* Sends a presence update if a session is connected.
* @param feature The feature to add.
*
@@ -531,7 +520,7 @@ void prof_incoming_message(char* barejid, char* resource, char* message);
* prof_disco_add_feature("urn:xmpp:omemo:0:devicelist+notify");
* @endcode
*/
void prof_disco_add_feature(char* feature);
void prof_disco_add_feature(char *feature);
/**
* Ends an encrypted session with a user.
@@ -542,7 +531,7 @@ void prof_disco_add_feature(char* feature);
* prof_encryption_reset("alice@server.org");
* @endcode
*/
void prof_encryption_reset(char* barejid);
void prof_encryption_reset(char *barejid);
/**
* Sets the titlebar encryption indicator text for a recipient.
@@ -555,10 +544,10 @@ void prof_encryption_reset(char* barejid);
* prof_chat_set_titlebar_enctext("bob@example.com", "safe");
* @endcode
*/
int prof_chat_set_titlebar_enctext(char* barejid, char* enctext);
int prof_chat_set_titlebar_enctext(char *barejid, char *enctext);
/**
* Resets the titlebar encryption indicator for a recipient to CProof's default.
* Resets the titlebar encryption indicator for a recipient to CProofs default.
* @param barejid Jabber ID of the recipient.
* @return 1 on success, 0 on failure.
*
@@ -567,7 +556,7 @@ int prof_chat_set_titlebar_enctext(char* barejid, char* enctext);
* prof_chat_unset_titlebar_enctext("bob@example.com");
* @endcode
*/
int prof_chat_unset_titlebar_enctext(char* barejid);
int prof_chat_unset_titlebar_enctext(char *barejid);
/**
* Sets the incoming message prefix character for a contact.
@@ -580,7 +569,7 @@ int prof_chat_unset_titlebar_enctext(char* barejid);
* prof_chat_set_incoming_char("kristine@example.com", "*");
* @endcode
*/
int prof_chat_set_incoming_char(char* barejid, char* ch);
int prof_chat_set_incoming_char(char *barejid, char *ch);
/**
* Resets the incoming message prefix character for a contact.
@@ -592,7 +581,7 @@ int prof_chat_set_incoming_char(char* barejid, char* ch);
* prof_chat_unset_incoming_char("kristine@example.com");
* @endcode
*/
int prof_chat_unset_incoming_char(char* barejid);
int prof_chat_unset_incoming_char(char *barejid);
/**
* Sets the outgoing message prefix character for a contact.
@@ -605,7 +594,7 @@ int prof_chat_unset_incoming_char(char* barejid);
* prof_chat_set_outgoing_char("david@example.com", "+");
* @endcode
*/
int prof_chat_set_outgoing_char(char* barejid, char* ch);
int prof_chat_set_outgoing_char(char *barejid, char *ch);
/**
* Resets the outgoing message prefix character for a contact.
@@ -617,7 +606,7 @@ int prof_chat_set_outgoing_char(char* barejid, char* ch);
* prof_chat_unset_outgoing_char("david@example.com");
* @endcode
*/
int prof_chat_unset_outgoing_char(char* barejid);
int prof_chat_unset_outgoing_char(char *barejid);
/**
* Sets the titlebar encryption indicator text for a room.
@@ -630,10 +619,10 @@ int prof_chat_unset_outgoing_char(char* barejid);
* prof_room_set_titlebar_enctext("generalchat@conference.service.com", "secret");
* @endcode
*/
int prof_room_set_titlebar_enctext(char* roomjid, char* enctext);
int prof_room_set_titlebar_enctext(char *roomjid, char *enctext);
/**
* Resets the titlebar encryption indicator for a room to CProof's default.
* Resets the titlebar encryption indicator for a room to CProofs default.
* @param roomjid Jabber ID of the room.
* @return 1 on success, 0 on failure.
*
@@ -642,7 +631,7 @@ int prof_room_set_titlebar_enctext(char* roomjid, char* enctext);
* prof_room_unset_titlebar_enctext("generalchat@conference.service.com");
* @endcode
*/
int prof_room_unset_titlebar_enctext(char* roomjid);
int prof_room_unset_titlebar_enctext(char *roomjid);
/**
* Sets the message prefix character for a room.
@@ -655,7 +644,7 @@ int prof_room_unset_titlebar_enctext(char* roomjid);
* prof_room_set_message_char("ohnoes@conference.example.com", "^");
* @endcode
*/
int prof_room_set_message_char(char* roomjid, char* ch);
int prof_room_set_message_char(char *roomjid, char *ch);
/**
* Resets the message prefix character for a room.
@@ -667,7 +656,7 @@ int prof_room_set_message_char(char* roomjid, char* ch);
* prof_room_unset_message_char("ohnoes@conference.example.com");
* @endcode
*/
int prof_room_unset_message_char(char* roomjid);
int prof_room_unset_message_char(char *roomjid);
/**
* Shows a message in a chat window.
@@ -680,7 +669,7 @@ int prof_room_unset_message_char(char* roomjid);
* prof_chat_show("bob@server.org", "From a plugin in the chat window");
* @endcode
*/
int prof_chat_show(char* barejid, char* message);
int prof_chat_show(char *barejid, char *message);
/**
* Shows a message in a chat window with a theme and prefix character.
@@ -698,7 +687,7 @@ int prof_chat_show(char* barejid, char* message);
* prof_chat_show_themed("bob@server.org", "myplugin", "text", NULL, "!", "Themed message");
* @endcode
*/
int prof_chat_show_themed(char* barejid, char* group, char* item, char* def, char* ch, char* message);
int prof_chat_show_themed(char *barejid, char *group, char *item, char *def, char *ch, char *message);
/**
* Shows a message in a chat room window.
@@ -711,7 +700,7 @@ int prof_chat_show_themed(char* barejid, char* group, char* item, char* def, cha
* prof_room_show("chat@conference.example.com", "From a plugin in the chat room");
* @endcode
*/
int prof_room_show(char* roomjid, char* message);
int prof_room_show(char *roomjid, char *message);
/**
* Shows a message in a chat room window with a theme and prefix character.
@@ -729,4 +718,4 @@ int prof_room_show(char* roomjid, char* message);
* prof_room_show_themed("chat@conference.example.com", "myplugin", "text", NULL, "!", "Themed message");
* @endcode
*/
int prof_room_show_themed(char* roomjid, char* group, char* item, char* def, char* ch, char* message);
int prof_room_show_themed(char *roomjid, char *group, char *item, char *def, char *ch, char *message);

View File

@@ -830,22 +830,6 @@ def get_current_recipient() -> str | None:
"""
pass
def get_current_window() -> str:
"""Retrieves the title of the current window as shown in the titlebar.
Returns the title of the currently active window, matching the titlebar display.
Special cases:
- For MUC configuration windows, includes "config" and an asterisk if modified (e.g., "room@example.com config \*").
- For vCard windows, includes the bare JID and an asterisk if modified (e.g., "vCard user@example.com \*").
:return: The window title as a string.
Example::
title = prof.get_current_window()
prof.cons_show(f"Current window: {title}")
"""
def get_current_muc() -> str | None:
"""Retrieves the Jabber ID of the current chat room.

View File

@@ -1,69 +0,0 @@
#!/bin/bash
# check-cwe134.sh - Static analysis for CWE-134 format string vulnerabilities
#
# This script detects potentially unsafe usage of format string functions
# where user-controlled data may be passed without "%s" wrapper.
#
# Usage: ./check-cwe134.sh [directory]
set -e
DIR="${1:-src}"
echo "=== CWE-134 Format String Vulnerability Check ==="
echo "Scanning: $DIR"
echo ""
# Functions that accept format strings
FORMAT_FUNCS="cons_show|cons_debug|cons_show_error|log_info|log_error|log_warning|log_debug|win_println|win_print"
ERRORS=0
echo "Checking for unsafe format string usage..."
echo ""
# Pattern 1: function call with single variable argument (no format string)
# Example: cons_show(variable); - BAD
# Example: cons_show("%s", variable); - OK
# Matches: func(identifier) or func(identifier->member) or func(identifier[index])
RESULTS=$(grep -rn --include="*.c" -P "($FORMAT_FUNCS)\s*\(\s*[a-zA-Z_][a-zA-Z0-9_]*(\s*->\s*\w+|\s*\[\s*\w+\s*\])?\s*\)\s*;" "$DIR" 2>/dev/null || true)
# Filter out function definitions, declarations, and safe api_* wrappers
RESULTS=$(echo "$RESULTS" | grep -v "const char\|void \|^[^:]*:[0-9]*:[a-z_]*(\|api_cons_show\|api_log_" || true)
if [ -n "$RESULTS" ]; then
echo "❌ POTENTIAL CWE-134 VULNERABILITIES FOUND:"
echo ""
echo "$RESULTS"
echo ""
ERRORS=$(echo "$RESULTS" | wc -l)
else
echo "✅ No obvious CWE-134 issues found."
fi
# Additional check: GString->str passed directly (not as %s argument)
echo ""
echo "Checking for GString->str passed to format functions..."
GSTRING_RESULTS=$(grep -rn --include="*.c" -P "($FORMAT_FUNCS)\s*\([^)]*->str\s*\)" "$DIR" 2>/dev/null | grep -v '"%s"' || true)
if [ -n "$GSTRING_RESULTS" ]; then
echo "⚠️ GString->str passed without \"%s\" (review manually):"
echo ""
echo "$GSTRING_RESULTS"
echo ""
fi
echo ""
echo "=== Summary ==="
echo "Critical issues: $ERRORS"
if [ "$ERRORS" -gt 0 ]; then
echo ""
echo "Fix by adding \"%s\" format specifier:"
echo " BAD: cons_show(variable);"
echo " GOOD: cons_show(\"%s\", variable);"
exit 1
fi
exit 0

View File

@@ -15,145 +15,14 @@ error_handler()
log_content ./test-suite.log
log_content ./test-suite-memcheck.log
echo >&2
echo "Error ${ERR_CODE} with command '${BASH_COMMAND}' on line ${BASH_LINENO[0]}. Exiting." >&2
echo >&2
echo
echo "Error ${ERR_CODE} with command '${BASH_COMMAND}' on line ${BASH_LINENO[0]}. Exiting."
echo
exit ${ERR_CODE}
}
trap error_handler ERR
# =============================================================================
# Constants
# =============================================================================
# Number of parallel build configurations
readonly TEST_BUILDS=4
# Human-readable names for each build configuration
readonly BUILD_NAMES=(
"Full" # 1. All features enabled
"Minimal" # 2. All optional features disabled
"NoEncrypt" # 3. No encryption (otr, pgp, omemo disabled)
"Default" # 4. Default ./configure options
)
# Regex patterns for parsing test output
readonly CMOCKA_PASSED_PATTERN='^\[ PASSED \] [0-9]+ test'
readonly CMOCKA_FAILED_PATTERN='^\[ FAILED \] [0-9]+ test'
# Coverage extraction patterns (matches both Docker and CI paths)
readonly COVERAGE_PATTERNS='*/profanity/src/* */src/src/*'
# =============================================================================
# Helper Functions
# =============================================================================
# Parse STATS line from build log and set global variables
# Usage: parse_build_stats <log_file>
parse_build_stats() {
local log_file="$1"
local stats_line
stats_line=$(grep "^STATS:" "$log_file" 2>/dev/null | tail -1)
STAT_UNIT_P=$(echo "$stats_line" | grep -oE "unit_passed=[0-9]+" | cut -d= -f2)
STAT_UNIT_F=$(echo "$stats_line" | grep -oE "unit_failed=[0-9]+" | cut -d= -f2)
STAT_FUNC_P=$(echo "$stats_line" | grep -oE "func_passed=[0-9]+" | cut -d= -f2)
STAT_FUNC_F=$(echo "$stats_line" | grep -oE "func_failed=[0-9]+" | cut -d= -f2)
STAT_COV_LINES=$(echo "$stats_line" | grep -oE "cov_lines=[0-9.]+%|cov_lines=n/a" | cut -d= -f2)
STAT_COV_FUNCS=$(echo "$stats_line" | grep -oE "cov_funcs=[0-9.]+%|cov_funcs=n/a" | cut -d= -f2)
STAT_COV_BRANCHES=$(echo "$stats_line" | grep -oE "cov_branches=[0-9.]+%|cov_branches=n/a" | cut -d= -f2)
STAT_TIME=$(echo "$stats_line" | grep -oE "time=[0-9]+m[0-9]+s" | cut -d= -f2)
: "${STAT_UNIT_P:=0}"
: "${STAT_UNIT_F:=0}"
: "${STAT_FUNC_P:=0}"
: "${STAT_FUNC_F:=0}"
}
# Extract test count from log file
# Usage: extract_test_count <log_file> <pattern>
extract_test_count() {
grep -E "$2" "$1" 2>/dev/null | grep -oE "[0-9]+" | head -1
}
# =============================================================================
# Test Verification
# =============================================================================
# Verify that test failures are properly detected
# This is a meta-test: it runs a deliberately failing test
# and checks that the test framework reports the failure correctly
verify_test_failure_detection()
{
echo
echo "==> Verifying test failure detection..."
# Create a simple failing test
cat > /tmp/test_must_fail.c << 'EOF'
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmocka.h>
static void test_that_must_fail(void **state) {
(void)state;
assert_true(0); // This MUST fail
}
int main(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_that_must_fail),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
EOF
# Compile the failing test
if ! gcc -o /tmp/test_must_fail /tmp/test_must_fail.c -lcmocka 2>/dev/null; then
echo "Warning: Could not compile test failure verification (cmocka not available?)"
echo "Skipping test failure detection verification"
return 0
fi
# Test 1: Single failing test detection
echo " Testing single test failure detection..."
if /tmp/test_must_fail > /tmp/test_must_fail.log 2>&1; then
echo "ERROR: Test that should fail returned success (exit code 0)" >&2
echo "This means the test framework is NOT detecting failures correctly!" >&2
echo "--- Test output ---" >&2
cat /tmp/test_must_fail.log >&2
echo "--- End output ---" >&2
rm -f /tmp/test_must_fail /tmp/test_must_fail.c /tmp/test_must_fail.log
exit 1
fi
echo " ✓ Single test failure correctly detected"
# Test 2: Parallel failure detection (simulates check-functional-parallel)
echo " Testing parallel test failure detection..."
failed=0
/tmp/test_must_fail > /tmp/p1.log 2>&1 & pid1=$!
true > /tmp/p2.log 2>&1 & pid2=$! # This passes
/tmp/test_must_fail > /tmp/p3.log 2>&1 & pid3=$!
true > /tmp/p4.log 2>&1 & pid4=$! # This passes
wait $pid1 || failed=$((failed + 1))
wait $pid2 || failed=$((failed + 1))
wait $pid3 || failed=$((failed + 1))
wait $pid4 || failed=$((failed + 1))
if [ $failed -ne 2 ]; then
echo "ERROR: Expected 2 failures in parallel tests, got $failed" >&2
echo "Parallel failure detection is broken!" >&2
rm -f /tmp/test_must_fail /tmp/test_must_fail.c /tmp/test_must_fail.log /tmp/p?.log
exit 1
fi
echo " ✓ Parallel test failures correctly detected (2 of 4 failed as expected)"
rm -f /tmp/test_must_fail /tmp/test_must_fail.c /tmp/test_must_fail.log /tmp/p?.log
echo "✓ Test failure detection verified"
}
num_cores()
{
# Check for cores, for systems with:
@@ -165,20 +34,6 @@ num_cores()
|| getconf _NPROCESSORS_ONLN 2>/dev/null
}
# Run test failure detection verification first
verify_test_failure_detection
# Parse arguments
COVERAGE_ONLY=no
for arg in "$@"; do
case "$arg" in
--coverage-only)
COVERAGE_ONLY=yes
shift
;;
esac
done
./bootstrap.sh
tests=()
@@ -189,39 +44,49 @@ ARCH="$(uname | tr '[:upper:]' '[:lower:]')"
case "$ARCH" in
linux*)
# 4 configurations for parallel CI
tests=(
# 1. Full build (all features enabled)
"--enable-notifications --enable-icons-and-clipboard --enable-otr --enable-pgp
--enable-omemo --enable-plugins --enable-c-plugins
--enable-python-plugins --with-xscreensaver --enable-omemo-qrcode --enable-gdk-pixbuf"
# 2. Minimal build (all optional features disabled)
"--disable-notifications --disable-icons-and-clipboard --disable-otr --disable-pgp
--disable-omemo --disable-plugins --disable-c-plugins
--disable-python-plugins --without-xscreensaver --disable-omemo-qrcode --disable-gdk-pixbuf"
# 3. No encryption (disable otr, pgp, omemo)
"--disable-pgp --disable-otr --disable-omemo --disable-omemo-qrcode"
# 4. Default configuration
""
)
--disable-python-plugins --without-xscreensaver"
"--disable-notifications"
"--disable-icons-and-clipboard"
"--disable-otr"
"--disable-pgp"
"--disable-omemo --disable-omemo-qrcode"
"--disable-pgp --disable-otr"
"--disable-pgp --disable-otr --disable-omemo"
"--disable-plugins"
"--disable-python-plugins"
"--disable-c-plugins"
"--disable-c-plugins --disable-python-plugins"
"--without-xscreensaver"
"--disable-gdk-pixbuf"
"")
source /etc/profile.d/debuginfod.sh 2>/dev/null || true
;;
darwin*)
# 4 configurations for parallel CI
tests=(
# 1. Full build (all features enabled)
"--enable-notifications --enable-icons-and-clipboard --enable-otr --enable-pgp
--enable-omemo --enable-plugins --enable-c-plugins
--enable-python-plugins"
# 2. Minimal build (all optional features disabled)
"--disable-notifications --disable-icons-and-clipboard --disable-otr --disable-pgp
--disable-omemo --disable-plugins --disable-c-plugins
--disable-python-plugins"
# 3. No encryption (disable otr, pgp, omemo)
"--disable-notifications"
"--disable-icons-and-clipboard"
"--disable-otr"
"--disable-pgp"
"--disable-omemo"
"--disable-pgp --disable-otr"
"--disable-pgp --disable-otr --disable-omemo"
# 4. Default configuration
""
)
"--disable-plugins"
"--disable-python-plugins"
"--disable-c-plugins"
"--disable-c-plugins --disable-python-plugins"
"")
;;
openbsd*)
MAKE="gmake"
@@ -231,242 +96,59 @@ case "$ARCH" in
# src/event/server_events.c:1454:19: error: universal character names are only valid in C++ and C99
CC="egcc -std=gnu99 -fexec-charset=UTF-8"
# 4 configurations for parallel CI
tests=(
# 1. Full build (all features enabled)
"--enable-notifications --enable-icons-and-clipboard --enable-otr --enable-pgp
--enable-omemo --enable-plugins --enable-c-plugins
--enable-python-plugins"
# 2. Minimal build (all optional features disabled)
"--disable-notifications --disable-icons-and-clipboard --disable-otr --disable-pgp
--disable-omemo --disable-plugins --disable-c-plugins
--disable-python-plugins"
# 3. No encryption (disable otr, pgp, omemo)
"--disable-notifications"
"--disable-icons-and-clipboard"
"--disable-otr"
"--disable-pgp"
"--disable-omemo"
"--disable-pgp --disable-otr"
"--disable-pgp --disable-otr --disable-omemo"
# 4. Default configuration
""
)
"--disable-plugins"
"--disable-python-plugins"
"--disable-c-plugins"
"--disable-c-plugins --disable-python-plugins"
"")
;;
esac
# Function to build and test a single configuration
build_and_test() {
local features="$1"
local extra_args="$2"
local idx="$3"
local run_valgrind="$4"
local run_coverage="$5"
local build_dir="build-$idx"
local log_file="build-$idx.log"
case "$ARCH" in
linux*)
echo
echo "--> Building with ./configure -C ${tests[0]} --enable-valgrind $*"
echo
{
echo "=== Build $idx started at $(date) ==="
echo "--> Building in $build_dir with ./configure -C $features $extra_args"
# shellcheck disable=SC2086
./configure -C ${tests[0]} --enable-valgrind $*
local start_time=$SECONDS
mkdir -p "$build_dir"
cd "$build_dir"
# shellcheck disable=SC2086
if ! ../configure -C $features $extra_args; then
echo "ERROR: configure failed"
exit 1
fi
if ! $MAKE CC="${CC}"; then
echo "ERROR: make failed"
exit 1
fi
# Run unit tests
local unit_passed=0 unit_failed=0
if [ "$run_valgrind" = "yes" ]; then
echo "--> Running unit tests under Valgrind..."
# Build unit tests first
$MAKE tests/unittests/unittests
# Run valgrind directly to capture cmocka output
valgrind --error-exitcode=1 --leak-check=full \
--suppressions=../prof.supp \
tests/unittests/unittests 2>&1 | tee unit-tests-output.log
valgrind_exit=${PIPESTATUS[0]}
if [ $valgrind_exit -ne 0 ]; then
echo "ERROR: Valgrind unit tests failed (exit code $valgrind_exit)"
exit 1
fi
$MAKE CC="${CC}"
if grep '^ID=' /etc/os-release | grep -q -e debian; then
$MAKE check-valgrind
else
echo "--> Running unit tests..."
$MAKE tests/unittests/unittests
tests/unittests/unittests 2>&1 | tee unit-tests-output.log
if [ ${PIPESTATUS[0]} -ne 0 ]; then
echo "ERROR: Unit tests failed"
exit 1
fi
$MAKE check-valgrind || log_content ./test-suite-memcheck.log
fi
# Extract unit test counts from cmocka output
unit_passed=$(extract_test_count unit-tests-output.log "$CMOCKA_PASSED_PATTERN")
unit_failed=$(extract_test_count unit-tests-output.log "$CMOCKA_FAILED_PATTERN")
: "${unit_passed:=0}" "${unit_failed:=0}"
echo "UNIT_TESTS: passed=$unit_passed failed=$unit_failed"
$MAKE distclean
;;
esac
# Set build index for port allocation: build 1 uses ports 5230-5233,
# build 2 uses 5234-5237, etc. This prevents port conflicts in parallel builds.
export PROF_BUILD_INDEX=$idx
local func_passed=0 func_failed=0
if ! $MAKE check-functional-parallel; then
echo "ERROR: functional tests failed"
exit 1
fi
# Extract functional test counts from group logs
echo "=== Functional test results ==="
for glog in ./test-logs/group*.log; do
if [ -f "$glog" ]; then
cnt=$(extract_test_count "$glog" "$CMOCKA_PASSED_PATTERN")
[ -n "$cnt" ] && func_passed=$((func_passed + cnt))
cnt=$(extract_test_count "$glog" "$CMOCKA_FAILED_PATTERN")
[ -n "$cnt" ] && func_failed=$((func_failed + cnt))
fi
done
echo "FUNC_TESTS: passed=$func_passed failed=$func_failed"
# Collect coverage data if enabled (lines, functions, branches)
# Must be done BEFORE make clean which removes .gcda files
local cov_lines="n/a" cov_funcs="n/a" cov_branches="n/a"
if [ "$run_coverage" = "yes" ]; then
echo "--> Collecting coverage data..."
if command -v lcov >/dev/null 2>&1; then
lcov --capture --directory . --output-file coverage-full.info \
--rc lcov_branch_coverage=1 --ignore-errors inconsistent 2>&1 || true
# Extract only production code from src/ directory, exclude tests
# shellcheck disable=SC2086
lcov --extract coverage-full.info $COVERAGE_PATTERNS \
--output-file coverage.info \
--rc lcov_branch_coverage=1 --ignore-errors inconsistent 2>&1 || true
if [ -f coverage.info ] && [ -s coverage.info ]; then
local summary
summary=$(lcov --summary coverage.info \
--rc lcov_branch_coverage=1 --ignore-errors inconsistent 2>&1 || true)
cov_lines=$(echo "$summary" | grep -E "lines\.*:" | grep -oE "[0-9]+\.[0-9]+%" | head -1)
cov_funcs=$(echo "$summary" | grep -E "functions\.*:" | grep -oE "[0-9]+\.[0-9]+%" | head -1)
cov_branches=$(echo "$summary" | grep -E "branches\.*:" | grep -oE "[0-9]+\.[0-9]+%" | head -1)
[ -z "$cov_lines" ] && cov_lines="n/a"
[ -z "$cov_funcs" ] && cov_funcs="n/a"
[ -z "$cov_branches" ] && cov_branches="n/a"
else
echo "WARNING: coverage.info is empty or not created"
fi
else
echo "WARNING: lcov not found"
fi
echo "COVERAGE: lines=$cov_lines funcs=$cov_funcs branches=$cov_branches"
fi
./profanity -v
# Save coverage.info before cleanup (for CI artifact)
# Only copy in CI environment to avoid leaving artifacts during local runs
if [ "$run_coverage" = "yes" ] && [ -f coverage.info ] && [ -n "$CI" ]; then
cp coverage.info ../coverage.info
echo "Coverage report saved to coverage.info"
fi
$MAKE clean
cd ..
rm -rf "$build_dir"
local elapsed=$((SECONDS - start_time))
local mins=$((elapsed / 60))
local secs=$((elapsed % 60))
echo "=== Build $idx completed at $(date) ==="
echo "STATS: unit_passed=$unit_passed unit_failed=$unit_failed func_passed=$func_passed func_failed=$func_failed cov_lines=$cov_lines cov_funcs=$cov_funcs cov_branches=$cov_branches time=${mins}m${secs}s"
} > "$log_file" 2>&1
}
# Run configurations
# Coverage enabled only for build 1 (Full) - it has most code paths
echo
echo "=== Start build ==="
echo
if [ "$COVERAGE_ONLY" = "yes" ]; then
echo "Running coverage-only mode (${BUILD_NAMES[0]} build)..."
for features in "${tests[@]}"
do
echo
run_valgrind="no"
run_coverage="yes"
extra_flags="--enable-coverage"
build_and_test "${tests[0]}" "$* $extra_flags" "1" "$run_valgrind" "$run_coverage" &
pids=("$!")
echo "${BUILD_NAMES[0]}: ${tests[0]} [+Coverage]"
else
echo "Starting $TEST_BUILDS parallel build configurations..."
echo "--> Building with ./configure -C ${features} $*"
echo
pids=()
for idx in $(seq 1 $TEST_BUILDS); do
if [ $idx -le ${#tests[@]} ]; then
# All builds run Valgrind on Linux
if [ "$ARCH" = "linux" ]; then
run_valgrind="yes"
extra_flags="--enable-valgrind"
else
run_valgrind="no"
extra_flags=""
fi
run_coverage="no"
build_and_test "${tests[$((idx-1))]}" "$* $extra_flags" "$idx" "$run_valgrind" "$run_coverage" &
pids+=("$!")
flags_desc=""
[ "$run_valgrind" = "yes" ] && flags_desc=" [+Valgrind]"
echo "${BUILD_NAMES[$((idx-1))]}: ${tests[$((idx-1))]}$flags_desc"
fi
done
fi
echo
# Wait for all builds and check exit codes
echo "Waiting for builds to complete..."
echo
failed_builds=()
for i in "${!pids[@]}"; do
idx=$((i + 1))
if wait "${pids[$i]}"; then
if [ -f "build-$idx.log" ]; then
parse_build_stats "build-$idx.log"
echo "${BUILD_NAMES[$i]} PASSED"
echo " Unit tests: $STAT_UNIT_P passed, $STAT_UNIT_F failed"
echo " Functional tests: $STAT_FUNC_P passed, $STAT_FUNC_F failed"
if [ "$STAT_COV_LINES" != "n/a" ] && [ -n "$STAT_COV_LINES" ]; then
echo " Coverage: Lines: $STAT_COV_LINES | Functions: $STAT_COV_FUNCS | Branches: $STAT_COV_BRANCHES"
fi
echo " Duration: ${STAT_TIME:-?}"
else
echo "${BUILD_NAMES[$i]} passed (no stats available)"
fi
else
echo "${BUILD_NAMES[$i]} FAILED" >&2
failed_builds+=("$idx")
fi
echo
# shellcheck disable=SC2086
./configure -C $features $*
$MAKE CC="${CC}"
$MAKE check
./profanity -v
$MAKE clean
done
# Show failed builds full logs
for idx in "${failed_builds[@]}"; do
if [ -f "build-$idx.log" ]; then
echo "=== ${BUILD_NAMES[$((idx-1))]} FAILURE LOG ===" >&2
cat "build-$idx.log" >&2
echo >&2
fi
done
if [ ${#failed_builds[@]} -gt 0 ]; then
echo "RESULT: FAILED (builds ${failed_builds[*]})" >&2
exit 1
else
if [ "$COVERAGE_ONLY" = "yes" ]; then
echo "RESULT: COVERAGE BUILD PASSED ✓"
else
echo "RESULT: ALL $TEST_BUILDS BUILDS PASSED ✓"
fi
fi

View File

@@ -69,8 +69,6 @@ AC_ARG_ENABLE([gdk-pixbuf],
[AS_HELP_STRING([--enable-gdk-pixbuf], [enable GDK Pixbuf support to scale avatars before uploading])])
AC_ARG_ENABLE([omemo-qrcode],
[AS_HELP_STRING([--enable-omemo-qrcode], [enable ability to display omemo qr code])])
AC_ARG_ENABLE([coverage],
[AS_HELP_STRING([--enable-coverage], [enable code coverage analysis])])
m4_include([m4/ax_valgrind_check.m4])
AX_VALGRIND_DFLT([drd], [off])
@@ -376,23 +374,14 @@ PKG_CHECK_MODULES([cmocka], [cmocka], [],
AM_CONDITIONAL([HAVE_STABBER], [false])
AC_CHECK_LIB([stabber], [stbbr_start], [AM_CONDITIONAL([HAVE_STABBER], [true])],
[AC_MSG_NOTICE([libstabber not found, will not be able to run functional tests])])
dnl Check for forkpty (needed for functional tests PTY handling)
dnl On Linux it's in libutil, on some BSDs it's in libc
AM_CONDITIONAL([HAVE_FORKPTY], [false])
AC_CHECK_LIB([util], [forkpty], [AM_CONDITIONAL([HAVE_FORKPTY], [true]) FORKPTY_LIB="-lutil"],
[AC_CHECK_FUNC([forkpty], [AM_CONDITIONAL([HAVE_FORKPTY], [true]) FORKPTY_LIB=""],
[AC_MSG_NOTICE([forkpty not found, will not be able to run functional tests])])])
AC_SUBST([FORKPTY_LIB])
AM_CONDITIONAL([HAVE_EXPECT], [false])
AC_CHECK_LIB([expect], [exp_expectl], [AM_CONDITIONAL([HAVE_EXPECT], [true])],
[AC_MSG_NOTICE([libexpect not found, will not be able to run functional tests])])
## Default parameters
AM_CFLAGS="$AM_CFLAGS -Wall -Wno-deprecated-declarations -std=gnu99 -ggdb3"
AM_LDFLAGS="$AM_LDFLAGS -export-dynamic"
AS_IF([test "x$enable_coverage" = xyes],
[AM_CFLAGS="$AM_CFLAGS --coverage -O0"
AM_LDFLAGS="$AM_LDFLAGS --coverage"
AC_MSG_NOTICE([Code coverage analysis enabled])])
AS_IF([test "x$PACKAGE_STATUS" = xdevelopment],
[AM_CFLAGS="$AM_CFLAGS -Wunused -Werror"])
AS_IF([test "x$PLATFORM" = xosx],

View File

@@ -197,35 +197,8 @@ Configuration for
.B Profanity
is stored in
.I $XDG_CONFIG_HOME/profanity/profrc
, details on commands for configuring Profanity can be found at <https://profanity-im.github.io/reference.html> or the respective built\-in help or man pages..SS Message History Storage
By default, message history is stored in an SQLite database. An alternative flat-file backend
stores messages as human-readable plain text files that can be edited with any text editor.
.PP
To enable flat-file logging, set in
.IR profrc :
.PP
.EX
[logging]
dblog=flatfile
.EE
.PP
Or use the command:
.B /privacy logging flatfile
.PP
Flat-file logs are stored under
.IR $XDG_DATA_HOME/profanity/flatlog/ ,
organized as
.IR {account_jid}/{contact_jid}/{YYYY_MM_DD}.log .
.PP
Each line has the format:
.br
.EX
{ISO8601} [{type}|{enc}|id:{id}|corrects:{id}] {sender}: {message}
.EE
.PP
Use
.B /history verify
to check integrity of stored history (both SQLite and flat-file)..SH BUGS
, details on commands for configuring Profanity can be found at <https://profanity-im.github.io/reference.html> or the respective built\-in help or man pages.
.SH BUGS
Bugs can either be reported by raising an issue at the Github issue tracker:
.br
.PP

381
prof.supp
View File

@@ -8,106 +8,6 @@
# * python suppressions file from https://github.com/python/cpython/blob/main/Misc/valgrind-python.supp
#
# ============================================
# glibc AVX2 optimizations (false positives)
# See: https://sourceware.org/bugzilla/show_bug.cgi?id=19796
# ============================================
{
glibc_wcpncpy_avx2
Memcheck:Addr32
fun:__wcpncpy_avx2
fun:wcsxfrm_l
fun:g_utf8_collate_key
...
}
{
glibc_wcsxfrm_avx2
Memcheck:Addr32
...
fun:wcsxfrm_l
fun:g_utf8_collate_key
...
}
# ============================================
# Functional tests suppressions (stabber/pthread)
# ============================================
{
stabber_pthread_create
Memcheck:Leak
match-leak-kinds: possible
fun:calloc
...
fun:allocate_dtv
fun:_dl_allocate_tls
...
fun:pthread_create*
...
fun:server_run
...
}
{
stabber_server_run
Memcheck:Leak
match-leak-kinds: possible
fun:calloc
...
fun:pthread_create*
...
obj:*/libstabber*
...
}
{
glib_time_zone_cache
Memcheck:Leak
match-leak-kinds: reachable
...
fun:g_time_zone_new*
...
}
{
glib_time_zone_local
Memcheck:Leak
match-leak-kinds: reachable
...
fun:g_strdup
...
fun:g_time_zone_new_identifier
fun:g_time_zone_new_local
...
}
{
glib_date_time_format
Memcheck:Leak
match-leak-kinds: reachable
...
fun:g_date_time_format
...
}
{
glib_date_time_format_locale
Memcheck:Leak
match-leak-kinds: reachable
fun:*alloc
...
obj:*/libglib*
...
fun:g_date_time_format
...
}
# ============================================
# Original suppressions
# ============================================
{
_dl_init
Memcheck:Leak
@@ -2803,284 +2703,3 @@
fun:calloc
fun:_dl_allocate_tls
}
# pthread TLS allocation in stabber server threads
{
pthread_create_tls_stabber
Memcheck:Leak
match-leak-kinds: possible
fun:calloc
...
fun:allocate_dtv
fun:_dl_allocate_tls
fun:allocate_stack
fun:pthread_create*
fun:server_run
}
# expect/tcl library allocations
{
tcl_alloc_expect
Memcheck:Leak
match-leak-kinds: possible
fun:malloc
...
fun:TclpAlloc
fun:Tcl_Alloc
...
fun:exp_expectl
}
{
exp_printify
Memcheck:Leak
match-leak-kinds: possible
fun:malloc
...
fun:exp_printify
...
fun:exp_expectl
}
# Additional suppressions for functional tests
# libexpect still reachable allocations
{
exp_spawnv_malloc
Memcheck:Leak
match-leak-kinds: reachable
fun:malloc
...
fun:exp_spawnv
fun:exp_spawnl
}
{
exp_spawnl_realloc
Memcheck:Leak
match-leak-kinds: reachable
fun:realloc
...
fun:exp_spawnv
fun:exp_spawnl
}
# libtcl memory pool allocations (expected)
{
tcl_alloc_pool
Memcheck:Leak
match-leak-kinds: reachable
fun:malloc
...
fun:TclpAlloc
fun:Tcl_Alloc
}
{
tcl_alloc_pool_calloc
Memcheck:Leak
match-leak-kinds: reachable
fun:calloc
...
fun:TclpAlloc
fun:Tcl_Alloc
}
# pthread thread-local storage (normal for multi-threaded programs)
{
pthread_tls_allocate_dtv
Memcheck:Leak
match-leak-kinds: possible
fun:calloc
fun:calloc
fun:allocate_dtv
fun:_dl_allocate_tls
fun:allocate_stack
fun:pthread_create*
}
# glib static initializations
{
glib_hash_table_init
Memcheck:Leak
match-leak-kinds: reachable
fun:malloc
fun:g_malloc
fun:g_hash_table_new_full
...
fun:call_init
fun:_dl_init
}
{
glib_array_init
Memcheck:Leak
match-leak-kinds: reachable
fun:realloc
fun:g_realloc
...
fun:call_init
fun:_dl_init
}
# fdopen/fopen allocations (FILE* buffers - normal)
{
fdopen_file_buffer
Memcheck:Leak
match-leak-kinds: reachable
fun:malloc
...
fun:fdopen*
}
{
fopen_file_buffer
Memcheck:Leak
match-leak-kinds: reachable
fun:malloc
...
fun:fopen*
}
# stabber log_init (server log file)
{
stabber_log_init
Memcheck:Leak
match-leak-kinds: reachable
fun:malloc
...
fun:log_init
fun:server_run
}
# glib time zone (static allocation)
{
g_time_zone_new_local
Memcheck:Leak
match-leak-kinds: reachable
...
fun:g_time_zone_new_local
}
{
g_time_zone_array
Memcheck:Leak
match-leak-kinds: reachable
fun:realloc
fun:g_realloc
...
fun:g_array_sized_new
fun:g_time_zone_new_identifier
}
# stabber log g_date_time_format invalid read - benign race in logging
{
stabber_g_date_time_format_invalid_read
Memcheck:Addr1
...
fun:g_date_time_format
fun:log_println
...
}
{
stabber_g_date_time_format_invalid_read2
Memcheck:Addr2
...
fun:g_date_time_format
fun:log_println
...
}
{
stabber_g_date_time_format_invalid_read4
Memcheck:Addr4
...
fun:g_date_time_format
fun:log_println
...
}
{
stabber_g_date_time_format_invalid_read8
Memcheck:Addr8
...
fun:g_date_time_format
fun:log_println
...
}
# More generic suppression for g_date_time_format race condition
{
g_date_time_format_cond
Memcheck:Cond
...
fun:g_date_time_format
...
}
{
g_date_time_format_value1
Memcheck:Value1
...
fun:g_date_time_format
...
}
{
g_date_time_format_value2
Memcheck:Value2
...
fun:g_date_time_format
...
}
{
g_date_time_format_value4
Memcheck:Value4
...
fun:g_date_time_format
...
}
{
g_date_time_format_value8
Memcheck:Value8
...
fun:g_date_time_format
...
}
# Suppress all Addr errors in log_println
{
log_println_addr1
Memcheck:Addr1
...
fun:log_println
...
}
{
log_println_addr2
Memcheck:Addr2
...
fun:log_println
...
}
{
log_println_addr4
Memcheck:Addr4
...
fun:log_println
...
}
{
log_println_addr8
Memcheck:Addr8
...
fun:log_println
...
}

View File

@@ -49,13 +49,6 @@ grlog=true
maxsize=1048580
rotate=true
shared=true
# Database backend for message history:
# on - SQLite database (default)
# off - no message logging
# redact - store with redacted message content
# flatfile - plain text files, editable with any text editor
# stored in ~/.local/share/profanity/flatlog/
dblog=on
[otr]
warn=true

View File

@@ -139,9 +139,8 @@ static const struct cmd_t command_defs[] = {
"Show version and license information.")
},
// Max args: account + server <s> + port <p> + tls <t> + auth <a> = 9
{ CMD_PREAMBLE("/connect",
parse_args, 0, 9, NULL)
parse_args, 0, 7, NULL)
CMD_MAINFUNC(cmd_connect)
CMD_TAGS(
CMD_TAG_CONNECTION)
@@ -1875,21 +1874,18 @@ static const struct cmd_t command_defs[] = {
},
{ CMD_PREAMBLE("/history",
parse_args, 1, 2, &cons_history_setting)
parse_args, 1, 1, &cons_history_setting)
CMD_MAINFUNC(cmd_history)
CMD_TAGS(
CMD_TAG_UI,
CMD_TAG_CHAT)
CMD_SYN(
"/history on|off",
"/history verify [<jid>]")
"/history on|off")
CMD_DESC(
"Switch chat history on or off, /logging chat will automatically be enabled when this setting is on. "
"When history is enabled, previous messages are shown in chat windows. "
"Use 'verify' to check integrity of stored message history.")
"When history is enabled, previous messages are shown in chat windows.")
CMD_ARGS(
{ "on|off", "Enable or disable showing chat history." },
{ "verify [<jid>]", "Verify integrity of message history. Optionally specify a JID to check only one contact." })
{ "on|off", "Enable or disable showing chat history." })
},
{ CMD_PREAMBLE("/log",
@@ -1906,9 +1902,9 @@ static const struct cmd_t command_defs[] = {
CMD_ARGS(
{ "where", "Show the current log file location." },
{ "rotate on|off", "Rotate log, default on. Does not take effect if you specified a filename yourself when starting Profanity." },
{ "maxsize <bytes>", "With rotate enabled, specifies the max log size, defaults to 10485760 (10MiB)." },
{ "maxsize <bytes>", "With rotate enabled, specifies the max log size, defaults to 10485760 (10MB)." },
{ "shared on|off", "Share logs between all instances, default: on. When off, the process id will be included in the log filename. Does not take effect if you specified a filename yourself when starting Profanity." },
{ "level INFO|DEBUG|WARN|ERROR", "Set the log level. Default is INFO." })
{"level INFO|DEBUG|WARN|ERROR", "Set the log level. Default is INFO. Only works with default log file, not with user provided log file during startup via -f." })
},
{ CMD_PREAMBLE("/carbons",
@@ -2721,7 +2717,7 @@ static const struct cmd_t command_defs[] = {
CMD_TAG_CHAT,
CMD_TAG_DISCOVERY)
CMD_SYN(
"/privacy logging on|redact|off|flatfile",
"/privacy logging on|redact|off",
"/privacy os on|off")
CMD_DESC(
"Configure privacy settings. "
@@ -2729,13 +2725,12 @@ static const struct cmd_t command_defs[] = {
"clientid to set the client identification name "
"session_alarm to configure an alarm when more clients log in.")
CMD_ARGS(
{ "logging on|redact|off|flatfile", "Switch chat logging. 'on' uses SQLite database (default). 'flatfile' stores messages as plain text files in ~/.local/share/profanity/flatlog/ that can be manually edited with any text editor. 'off' disables logging entirely. 'redact' stores messages with content replaced by '[redacted]'. Note: 'off' might have unintended consequences, such as not being able to decrypt OMEMO encrypted messages received later via MAM. The 'flatfile' setting takes effect on next connection." },
{ "logging on|redact|off", "Switch chat logging. This will also disable logging in the internally used SQL database. Your messages will not be saved anywhere locally. This might have unintended consequences, such as not being able to decrypt OMEMO encrypted messages received later via MAM, and should be used with caution." },
{ "os on|off", "Choose whether to include the OS name if a user asks for software information (XEP-0092)." }
)
CMD_EXAMPLES(
"/privacy",
"/privacy logging off",
"/privacy logging flatfile",
"/privacy os off")
},
@@ -2829,15 +2824,16 @@ cmd_search_index_any(char* term)
int terms_len = g_strv_length(processed_terms);
for (int i = 0; i < terms_len; i++) {
GHashTableIter iter;
gpointer key, value;
g_hash_table_iter_init(&iter, search_index);
while (g_hash_table_iter_next(&iter, &key, &value)) {
char* index_entry = (char*)value;
GList* index_keys = g_hash_table_get_keys(search_index);
GList* curr = index_keys;
while (curr) {
char* index_entry = g_hash_table_lookup(search_index, curr->data);
if (g_str_match_string(processed_terms[i], index_entry, FALSE)) {
results = g_list_append(results, key);
results = g_list_append(results, curr->data);
}
curr = g_list_next(curr);
}
g_list_free(index_keys);
}
return results;
@@ -2851,14 +2847,13 @@ cmd_search_index_all(char* term)
auto_gcharv gchar** terms = g_str_tokenize_and_fold(term, NULL, NULL);
int terms_len = g_strv_length(terms);
GHashTableIter iter;
gpointer key, value;
g_hash_table_iter_init(&iter, search_index);
while (g_hash_table_iter_next(&iter, &key, &value)) {
char* command = (char*)key;
char* command_index = (char*)value;
GList* commands = g_hash_table_get_keys(search_index);
GList* curr = commands;
while (curr) {
char* command = curr->data;
int matches = 0;
for (int i = 0; i < terms_len; i++) {
char* command_index = g_hash_table_lookup(search_index, command);
if (g_str_match_string(terms[i], command_index, FALSE)) {
matches++;
}
@@ -2866,8 +2861,11 @@ cmd_search_index_all(char* term)
if (matches == terms_len) {
results = g_list_append(results, command);
}
curr = g_list_next(curr);
}
g_list_free(commands);
return results;
}
@@ -2990,18 +2988,7 @@ command_docgen(void)
}
FILE* toc_fragment = fopen("toc_fragment.html", "w");
if (!toc_fragment) {
log_error("command_docgen(): unable to open toc_fragment.html for writing: %s", g_strerror(errno));
g_list_free(cmds);
return;
}
FILE* main_fragment = fopen("main_fragment.html", "w");
if (!main_fragment) {
log_error("command_docgen(): unable to open main_fragment.html for writing: %s", g_strerror(errno));
fclose(toc_fragment);
g_list_free(cmds);
return;
}
fputs("<ul><li><ul><li>\n", toc_fragment);
fputs("<hr>\n", main_fragment);
@@ -3106,11 +3093,6 @@ command_mangen(void)
return;
}
FILE* manpage = fopen(filename, "w");
if (!manpage) {
log_error("command_mangen(): unable to open %s for writing: %s", filename, g_strerror(errno));
curr = g_list_next(curr);
continue;
}
fprintf(manpage, "%s\n", header);
fputs(".SH NAME\n", manpage);

View File

@@ -184,7 +184,7 @@ _string_matches_one_of(const char* what, const char* is, bool is_can_be_null, co
}
va_end(ap);
if (s > 0)
cons_show("%s", errmsg);
cons_show(errmsg);
}
return ret;
}
@@ -418,7 +418,7 @@ cmd_connect(ProfWin* window, const char* const command, gchar** args)
auto_char char* err_msg = NULL;
gboolean res = strtoi_range(port_str, &port, 1, 65535, &err_msg);
if (!res) {
cons_show("%s", err_msg);
cons_show(err_msg);
cons_show("");
port = 0;
options_destroy(options);
@@ -711,7 +711,7 @@ _account_set_port(char* account_name, char* port)
auto_char char* err_msg = NULL;
gboolean res = strtoi_range(port, &porti, 1, 65535, &err_msg);
if (!res) {
cons_show("%s", err_msg);
cons_show(err_msg);
cons_show("");
} else {
accounts_set_port(account_name, porti);
@@ -903,7 +903,7 @@ _account_set_max_sessions(char* account_name, char* max_sessions_raw)
auto_char char* err_msg = NULL;
gboolean res = strtoi_range(max_sessions_raw, &max_sessions, 0, INT_MAX, &err_msg);
if (!res) {
cons_show("%s", err_msg);
cons_show(err_msg);
cons_show("");
return TRUE;
}
@@ -924,7 +924,7 @@ _account_set_presence_priority(char* account_name, char* presence, char* priorit
auto_char char* err_msg = NULL;
gboolean res = strtoi_range(priority, &intval, -128, 127, &err_msg);
if (!res) {
cons_show("%s", err_msg);
cons_show(err_msg);
return TRUE;
}
@@ -1580,7 +1580,7 @@ _cmd_list_commands(GList* commands)
while (curr) {
gchar* cmd = curr->data;
if (count == 5) {
cons_show("%s", cmds->str);
cons_show(cmds->str);
g_string_free(cmds, TRUE);
cmds = g_string_new("");
count = 0;
@@ -1589,7 +1589,7 @@ _cmd_list_commands(GList* commands)
curr = g_list_next(curr);
count++;
}
cons_show("%s", cmds->str);
cons_show(cmds->str);
g_string_free(cmds, TRUE);
g_list_free(curr);
@@ -2407,7 +2407,7 @@ cmd_roster(ProfWin* window, const char* const command, gchar** args)
}
return TRUE;
} else {
cons_show("%s", err_msg);
cons_show(err_msg);
return TRUE;
}
@@ -2467,7 +2467,7 @@ cmd_roster(ProfWin* window, const char* const command, gchar** args)
cons_show("Roster contact indent set to: %d", intval);
rosterwin_roster();
} else {
cons_show("%s", err_msg);
cons_show(err_msg);
}
}
} else {
@@ -2501,7 +2501,7 @@ cmd_roster(ProfWin* window, const char* const command, gchar** args)
cons_show("Roster resource indent set to: %d", intval);
rosterwin_roster();
} else {
cons_show("%s", err_msg);
cons_show(err_msg);
}
}
} else if (g_strcmp0(args[1], "join") == 0) {
@@ -2527,7 +2527,7 @@ cmd_roster(ProfWin* window, const char* const command, gchar** args)
cons_show("Roster presence indent set to: %d", intval);
rosterwin_roster();
} else {
cons_show("%s", err_msg);
cons_show(err_msg);
}
}
} else {
@@ -4371,7 +4371,7 @@ cmd_occupants(ProfWin* window, const char* const command, gchar** args)
wins_resize_all();
return TRUE;
} else {
cons_show("%s", err_msg);
cons_show(err_msg);
return TRUE;
}
}
@@ -4391,7 +4391,7 @@ cmd_occupants(ProfWin* window, const char* const command, gchar** args)
occupantswin_occupants_all();
} else {
cons_show("%s", err_msg);
cons_show(err_msg);
}
return TRUE;
}
@@ -4970,8 +4970,8 @@ cmd_sendfile(ProfWin* window, const char* const command, gchar** args)
alt_scheme = OMEMO_AESGCM_URL_SCHEME;
alt_fragment = _add_omemo_stream(&fd, &fh, &err);
if (err != NULL) {
cons_show_error("%s", err);
win_println(window, THEME_ERROR, "-", "%s", err);
cons_show_error(err);
win_println(window, THEME_ERROR, "-", err);
goto out;
}
#endif
@@ -5356,8 +5356,6 @@ cmd_time(ProfWin* window, const char* const command, gchar** args)
cons_bad_cmd_usage(command);
return TRUE;
}
if (!set_all)
break;
}
if (!set_all && n == ARRAY_SIZE(time_prefs)) {
cons_bad_cmd_usage(command);
@@ -5836,7 +5834,7 @@ cmd_inpblock(ProfWin* window, const char* const command, gchar** args)
prefs_set_inpblock(intval);
inp_nonblocking(FALSE);
} else {
cons_show("%s", err_msg);
cons_show(err_msg);
}
return TRUE;
@@ -6063,7 +6061,7 @@ cmd_statusbar(ProfWin* window, const char* const command, gchar** args)
ui_resize();
return TRUE;
} else {
cons_show("%s", err_msg);
cons_show(err_msg);
cons_bad_cmd_usage(command);
return TRUE;
}
@@ -6094,7 +6092,7 @@ cmd_statusbar(ProfWin* window, const char* const command, gchar** args)
ui_resize();
return TRUE;
} else {
cons_show("%s", err_msg);
cons_show(err_msg);
cons_bad_cmd_usage(command);
return TRUE;
}
@@ -6254,7 +6252,7 @@ cmd_log(ProfWin* window, const char* const command, gchar** args)
prefs_set_max_log_size(intval);
cons_show("Log maximum size set to %d bytes", intval);
} else {
cons_show("%s", err_msg);
cons_show(err_msg);
}
return TRUE;
}
@@ -6273,11 +6271,10 @@ cmd_log(ProfWin* window, const char* const command, gchar** args)
if (strcmp(subcmd, "level") == 0) {
log_level_t prof_log_level;
if (log_level_from_string(value, &prof_log_level) == 0) {
auto_char char* log_file = strdup(get_log_file_location());
log_close();
log_init(prof_log_level, log_file);
log_init(prof_log_level, NULL);
cons_show("Log level changed to: %s (log file: %s).", value, log_file ? log_file : "[default]");
cons_show("Log level changed to: %s.", value);
return TRUE;
}
}
@@ -6304,7 +6301,7 @@ cmd_reconnect(ProfWin* window, const char* const command, gchar** args)
cons_show("Reconnect interval set to %d seconds.", intval);
}
} else {
cons_show("%s", err_msg);
cons_show(err_msg);
cons_bad_cmd_usage(command);
}
@@ -6330,7 +6327,7 @@ cmd_autoping(ProfWin* window, const char* const command, gchar** args)
cons_show("Autoping interval set to %d seconds.", intval);
}
} else {
cons_show("%s", err_msg);
cons_show(err_msg);
cons_bad_cmd_usage(command);
}
@@ -6346,7 +6343,7 @@ cmd_autoping(ProfWin* window, const char* const command, gchar** args)
cons_show("Autoping timeout set to %d seconds.", intval);
}
} else {
cons_show("%s", err_msg);
cons_show(err_msg);
cons_bad_cmd_usage(command);
}
@@ -6416,7 +6413,7 @@ cmd_autoaway(ProfWin* window, const char* const command, gchar** args)
cons_show("Auto away time set to: %d minutes.", minutesval);
}
} else {
cons_show("%s", err_msg);
cons_show(err_msg);
}
return TRUE;
@@ -6439,7 +6436,7 @@ cmd_autoaway(ProfWin* window, const char* const command, gchar** args)
}
}
} else {
cons_show("%s", err_msg);
cons_show(err_msg);
}
return TRUE;
@@ -6506,7 +6503,7 @@ cmd_priority(ProfWin* window, const char* const command, gchar** args)
cl_ev_presence_send(last_presence, 0);
cons_show("Priority set to %d.", intval);
} else {
cons_show("%s", err_msg);
cons_show(err_msg);
}
return TRUE;
@@ -6576,7 +6573,7 @@ cmd_tray(ProfWin* window, const char* const command, gchar** args)
tray_set_timer(intval);
}
} else {
cons_show("%s", err_msg);
cons_show(err_msg);
}
return TRUE;
@@ -6686,11 +6683,6 @@ cmd_privacy(ProfWin* window, const char* const command, gchar** args)
} else if (g_strcmp0(arg, "redact") == 0) {
cons_show("Messages are going to be redacted.");
prefs_set_string(PREF_DBLOG, arg);
} else if (g_strcmp0(arg, "flatfile") == 0) {
cons_show("Using flat-file backend for message logging. Takes effect on next connection.");
prefs_set_string(PREF_DBLOG, arg);
prefs_set_boolean(PREF_CHLOG, TRUE);
prefs_set_boolean(PREF_HISTORY, TRUE);
} else {
cons_bad_cmd_usage(command);
return TRUE;
@@ -6735,58 +6727,6 @@ cmd_history(ProfWin* window, const char* const command, gchar** args)
return TRUE;
}
if (g_strcmp0(args[0], "verify") == 0) {
const gchar* contact_jid = args[1]; // may be NULL (verify all)
cons_show("Verifying history integrity...");
GSList* issues = log_database_verify_integrity(contact_jid);
int errors = 0, warnings = 0, infos = 0;
for (GSList* l = issues; l; l = l->next) {
integrity_issue_t* issue = l->data;
const char* level_str;
switch (issue->level) {
case INTEGRITY_ERROR:
level_str = "ERROR";
errors++;
break;
case INTEGRITY_WARNING:
level_str = "WARN";
warnings++;
break;
case INTEGRITY_INFO:
level_str = "INFO";
infos++;
break;
default:
level_str = "???";
break;
}
if (issue->line > 0) {
if (issue->level == INTEGRITY_ERROR) {
cons_show_error("[%s] %s:%d — %s", level_str, issue->file, issue->line, issue->message);
} else {
cons_show("[%s] %s:%d — %s", level_str, issue->file, issue->line, issue->message);
}
} else {
if (issue->level == INTEGRITY_ERROR) {
cons_show_error("[%s] %s — %s", level_str, issue->file, issue->message);
} else {
cons_show("[%s] %s — %s", level_str, issue->file, issue->message);
}
}
}
if (!issues) {
cons_show("Verification complete: no issues found.");
} else {
cons_show("Verification complete: %d error(s), %d warning(s), %d info(s).",
errors, warnings, infos);
g_slist_free_full(issues, (GDestroyNotify)integrity_issue_free);
}
return TRUE;
}
_cmd_set_boolean_preference(args[0], "Chat history", PREF_HISTORY);
// if set to on, set chlog (/logging chat on)
@@ -7238,21 +7178,22 @@ cmd_pgp(ProfWin* window, const char* const command, gchar** args)
}
cons_show("PGP keys:");
GHashTableIter iter;
gpointer key, value;
g_hash_table_iter_init(&iter, keys);
while (g_hash_table_iter_next(&iter, &key, &value)) {
ProfPGPKey* pgp_key = (ProfPGPKey*)value;
cons_show(" %s", pgp_key->name);
cons_show(" ID : %s", pgp_key->id);
auto_char char* format_fp = p_gpg_format_fp_str(pgp_key->fp);
GList* keylist = g_hash_table_get_keys(keys);
GList* curr = keylist;
while (curr) {
ProfPGPKey* key = g_hash_table_lookup(keys, curr->data);
cons_show(" %s", key->name);
cons_show(" ID : %s", key->id);
auto_char char* format_fp = p_gpg_format_fp_str(key->fp);
cons_show(" Fingerprint : %s", format_fp);
if (pgp_key->secret) {
if (key->secret) {
cons_show(" Type : PUBLIC, PRIVATE");
} else {
cons_show(" Type : PUBLIC");
}
curr = g_list_next(curr);
}
g_list_free(keylist);
p_gpg_free_keys(keys);
return TRUE;
}
@@ -7293,24 +7234,25 @@ cmd_pgp(ProfWin* window, const char* const command, gchar** args)
return TRUE;
}
GHashTable* pubkeys = p_gpg_pubkeys();
if (!pubkeys || g_hash_table_size(pubkeys) == 0) {
GList* jids = g_hash_table_get_keys(pubkeys);
if (!jids) {
cons_show("No contacts found with PGP public keys assigned.");
return TRUE;
}
cons_show("Assigned PGP public keys:");
GHashTableIter iter;
gpointer key, value;
g_hash_table_iter_init(&iter, pubkeys);
while (g_hash_table_iter_next(&iter, &key, &value)) {
char* jid = (char*)key;
ProfPGPPubKeyId* pubkeyid = (ProfPGPPubKeyId*)value;
GList* curr = jids;
while (curr) {
char* jid = curr->data;
ProfPGPPubKeyId* pubkeyid = g_hash_table_lookup(pubkeys, jid);
if (pubkeyid->received) {
cons_show(" %s: %s (received)", jid, pubkeyid->id);
} else {
cons_show(" %s: %s (stored)", jid, pubkeyid->id);
}
curr = g_list_next(curr);
}
g_list_free(jids);
return TRUE;
}
@@ -7523,21 +7465,22 @@ cmd_ox(ProfWin* window, const char* const command, gchar** args)
}
cons_show("OpenPGP keys:");
GHashTableIter iter;
gpointer key, value;
g_hash_table_iter_init(&iter, keys);
while (g_hash_table_iter_next(&iter, &key, &value)) {
ProfPGPKey* pgp_key = (ProfPGPKey*)value;
cons_show(" %s", pgp_key->name);
cons_show(" ID : %s", pgp_key->id);
auto_char char* format_fp = p_gpg_format_fp_str(pgp_key->fp);
GList* keylist = g_hash_table_get_keys(keys);
GList* curr = keylist;
while (curr) {
ProfPGPKey* key = g_hash_table_lookup(keys, curr->data);
cons_show(" %s", key->name);
cons_show(" ID : %s", key->id);
auto_char char* format_fp = p_gpg_format_fp_str(key->fp);
cons_show(" Fingerprint : %s", format_fp);
if (pgp_key->secret) {
if (key->secret) {
cons_show(" Type : PUBLIC, PRIVATE");
} else {
cons_show(" Type : PUBLIC");
}
curr = g_list_next(curr);
}
g_list_free(keylist);
p_gpg_free_keys(keys);
return TRUE;
}
@@ -7545,8 +7488,8 @@ cmd_ox(ProfWin* window, const char* const command, gchar** args)
else if (g_strcmp0(args[0], "contacts") == 0) {
GHashTable* keys = ox_gpg_public_keys();
cons_show("OpenPGP keys:");
GHashTableIter iter;
gpointer key, value;
GList* keylist = g_hash_table_get_keys(keys);
GList* curr = keylist;
GSList* roster_list = NULL;
jabber_conn_status_t conn_status = connection_get_status();
@@ -7556,16 +7499,15 @@ cmd_ox(ProfWin* window, const char* const command, gchar** args)
roster_list = roster_get_contacts(ROSTER_ORD_NAME);
}
g_hash_table_iter_init(&iter, keys);
while (g_hash_table_iter_next(&iter, &key, &value)) {
ProfPGPKey* pgp_key = (ProfPGPKey*)value;
while (curr) {
ProfPGPKey* key = g_hash_table_lookup(keys, curr->data);
PContact contact = NULL;
if (roster_list) {
GSList* curr_c = roster_list;
while (!contact && curr_c) {
contact = curr_c->data;
auto_gchar gchar* xmppuri = g_strdup_printf("xmpp:%s", p_contact_barejid(contact));
if (g_strcmp0(pgp_key->name, xmppuri)) {
if (g_strcmp0(key->name, xmppuri)) {
contact = NULL;
}
curr_c = g_slist_next(curr_c);
@@ -7573,10 +7515,11 @@ cmd_ox(ProfWin* window, const char* const command, gchar** args)
}
if (contact) {
cons_show("%s - %s", pgp_key->fp, pgp_key->name);
cons_show("%s - %s", key->fp, key->name);
} else {
cons_show("%s - %s (not in roster)", pgp_key->fp, pgp_key->name);
cons_show("%s - %s (not in roster)", key->fp, key->name);
}
curr = g_list_next(curr);
}
} else if (g_strcmp0(args[0], "start") == 0) {
@@ -9666,7 +9609,7 @@ cmd_register(ProfWin* window, const char* const command, gchar** args)
auto_char char* err_msg = NULL;
gboolean res = strtoi_range(port_str, &port, 1, 65535, &err_msg);
if (!res) {
cons_show("%s", err_msg);
cons_show(err_msg);
cons_show("");
port = 0;
options_destroy(options);
@@ -9736,7 +9679,7 @@ cmd_strophe(ProfWin* window, const char* const command, gchar** args)
prefs_set_string(PREF_STROPHE_VERBOSITY, args[1]);
return TRUE;
} else {
cons_show("%s", err_msg);
cons_show(err_msg);
}
} else if (g_strcmp0(args[0], "sm") == 0) {
if (g_strcmp0(args[1], "no-resend") == 0) {

View File

@@ -143,7 +143,7 @@ auto_close_gfd(gint* fd)
return;
if (close(*fd) == EOF)
log_error("%s", g_strerror(errno));
log_error(g_strerror(errno));
}
/**
@@ -158,7 +158,7 @@ auto_close_FILE(FILE** fd)
return;
if (fclose(*fd) == EOF)
log_error("%s", g_strerror(errno));
log_error(g_strerror(errno));
}
static gboolean

View File

@@ -57,7 +57,6 @@
#define DIR_OMEMO "omemo"
#define DIR_PLUGINS "plugins"
#define DIR_DATABASE "database"
#define DIR_FLATLOG "flatlog"
#define DIR_DOWNLOADS "downloads"
#define DIR_EDITOR "editor"
#define DIR_CERTS "certs"

View File

@@ -35,129 +35,664 @@
#include "config.h"
#include <sys/stat.h>
#include <sys/statvfs.h>
#include <sqlite3.h>
#include <glib.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include "log.h"
#include "common.h"
#include "config/files.h"
#include "database.h"
#include "config/preferences.h"
#include "ui/ui.h"
#include "xmpp/xmpp.h"
#include "xmpp/message.h"
db_backend_t* active_db_backend = NULL;
static sqlite3* g_chatlog_database;
void
integrity_issue_free(integrity_issue_t* issue)
static void _add_to_db(ProfMessage* message, char* type, const Jid* const from_jid, const Jid* const to_jid);
static char* _get_db_filename(ProfAccount* account);
static prof_msg_type_t _get_message_type_type(const char* const type);
static prof_enc_t _get_message_enc_type(const char* const encstr);
static int _get_db_version(void);
static gboolean _migrate_to_v2(void);
static gboolean _check_available_space_for_db_migration(char* path_to_db);
static const int latest_version = 2;
static char*
_db_strdup(const char* str)
{
if (issue) {
g_free(issue->file);
g_free(issue->message);
g_free(issue);
}
return str ? strdup(str) : NULL;
}
#define auto_sqlite __attribute__((__cleanup__(auto_free_sqlite)))
static void
auto_free_sqlite(gchar** str)
{
if (str == NULL)
return;
sqlite3_free(*str);
}
static char*
_get_db_filename(ProfAccount* account)
{
return files_file_in_account_data_path(DIR_DATABASE, account->jid, "chatlog.db");
}
gboolean
log_database_init(ProfAccount* account)
{
auto_gchar gchar* pref_dblog = prefs_get_string(PREF_DBLOG);
// Select backend based on preference
if (g_strcmp0(pref_dblog, "flatfile") == 0) {
active_db_backend = db_backend_flatfile();
log_info("Using flat-file database backend");
} else {
active_db_backend = db_backend_sqlite();
log_info("Using SQLite database backend");
}
if (!active_db_backend) {
log_error("log_database_init: no backend available");
int ret = sqlite3_initialize();
if (ret != SQLITE_OK) {
log_error("Error initializing SQLite database: %d", ret);
return FALSE;
}
return active_db_backend->init(account);
auto_char char* filename = _get_db_filename(account);
if (!filename) {
return FALSE;
}
ret = sqlite3_open(filename, &g_chatlog_database);
if (ret != SQLITE_OK) {
const char* err_msg = sqlite3_errmsg(g_chatlog_database);
log_error("Error opening SQLite database: %s", err_msg);
return FALSE;
}
char* err_msg;
int db_version = _get_db_version();
if (db_version == latest_version) {
return TRUE;
}
// ChatLogs Table
// Contains all chat messages
//
// id is primary key
// from_jid is the sender's jid
// to_jid is the receiver's jid
// from_resource is the sender's resource
// to_resource is the receiver's resource
// message is the message's text
// timestamp is the timestamp like "2020/03/24 11:12:14"
// type is there to distinguish: message (chat), MUC message (muc), muc pm (mucpm)
// stanza_id is the ID in <message>
// archive_id is the stanza-id from from XEP-0359: Unique and Stable Stanza IDs used for XEP-0313: Message Archive Management
// encryption is to distinguish: none, omemo, otr, pgp
// marked_read is 0/1 whether a message has been marked as read via XEP-0333: Chat Markers
// replace_id is the ID from XEP-0308: Last Message Correction
// replaces_db_id is ID (primary key) of the original message that LMC message corrects/replaces
// replaced_by_db_id is ID (primary key) of the last correcting (LMC) message for the original message
char* query = "CREATE TABLE IF NOT EXISTS `ChatLogs` ("
"`id` INTEGER PRIMARY KEY AUTOINCREMENT, "
"`from_jid` TEXT NOT NULL, "
"`to_jid` TEXT NOT NULL, "
"`from_resource` TEXT, "
"`to_resource` TEXT, "
"`message` TEXT, "
"`timestamp` TEXT, "
"`type` TEXT, "
"`stanza_id` TEXT, "
"`archive_id` TEXT, "
"`encryption` TEXT, "
"`marked_read` INTEGER, "
"`replace_id` TEXT, "
"`replaces_db_id` INTEGER, "
"`replaced_by_db_id` INTEGER)";
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, query, NULL, 0, &err_msg)) {
goto out;
}
query = "CREATE TRIGGER IF NOT EXISTS update_corrected_message "
"AFTER INSERT ON ChatLogs "
"FOR EACH ROW "
"WHEN NEW.replaces_db_id IS NOT NULL "
"BEGIN "
"UPDATE ChatLogs "
"SET replaced_by_db_id = NEW.id "
"WHERE id = NEW.replaces_db_id; "
"END;";
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, query, NULL, 0, &err_msg)) {
log_error("Unable to add `update_corrected_message` trigger.");
goto out;
}
query = "CREATE INDEX IF NOT EXISTS ChatLogs_timestamp_IDX ON `ChatLogs` (`timestamp`)";
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, query, NULL, 0, &err_msg)) {
log_error("Unable to create index for timestamp.");
goto out;
}
query = "CREATE INDEX IF NOT EXISTS ChatLogs_to_from_jid_IDX ON `ChatLogs` (`to_jid`, `from_jid`)";
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, query, NULL, 0, &err_msg)) {
log_error("Unable to create index for to_jid.");
goto out;
}
query = "CREATE TABLE IF NOT EXISTS `DbVersion` (`dv_id` INTEGER PRIMARY KEY, `version` INTEGER UNIQUE)";
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, query, NULL, 0, &err_msg)) {
goto out;
}
if (db_version == -1) {
query = "INSERT OR IGNORE INTO `DbVersion` (`version`) VALUES ('2')";
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, query, NULL, 0, &err_msg)) {
goto out;
}
db_version = _get_db_version();
}
// Unlikely event, but we don't want to migrate if we are just unable to determine the DB version
if (db_version == -1) {
cons_show_error("DB Initialization Error: Unable to check DB version.");
goto out;
}
if (db_version < latest_version) {
cons_show("Migrating database schema. This operation may take a while...");
if (db_version < 2 && (!_check_available_space_for_db_migration(filename) || !_migrate_to_v2())) {
cons_show_error("Database Initialization Error: Unable to migrate database to version 2. Please, check error logs for details.");
goto out;
}
cons_show("Database schema migration was successful.");
}
log_debug("Initialized SQLite database: %s", filename);
return TRUE;
out:
if (err_msg) {
log_error("SQLite error in log_database_init(): %s", err_msg);
sqlite3_free(err_msg);
} else {
log_error("Unknown SQLite error in log_database_init().");
}
return FALSE;
}
void
log_database_close(void)
{
if (active_db_backend) {
active_db_backend->close();
log_debug("log_database_close() called");
if (g_chatlog_database) {
sqlite3_close(g_chatlog_database);
sqlite3_shutdown();
g_chatlog_database = NULL;
}
active_db_backend = NULL;
}
void
log_database_add_incoming(ProfMessage* message)
{
if (active_db_backend && active_db_backend->add_incoming) {
active_db_backend->add_incoming(message);
if (message->to_jid) {
_add_to_db(message, NULL, message->from_jid, message->to_jid);
} else {
_add_to_db(message, NULL, message->from_jid, connection_get_jid());
}
}
static void
_log_database_add_outgoing(char* type, const char* const id, const char* const barejid, const char* const message, const char* const replace_id, prof_enc_t enc)
{
ProfMessage* msg = message_init();
msg->id = _db_strdup(id);
msg->from_jid = jid_create(barejid);
msg->plain = _db_strdup(message);
msg->replace_id = _db_strdup(replace_id);
msg->timestamp = g_date_time_new_now_local(); // TODO: get from outside. best to have whole ProfMessage from outside
msg->enc = enc;
_add_to_db(msg, type, connection_get_jid(), msg->from_jid); // TODO: myjid now in profmessage
message_free(msg);
}
void
log_database_add_outgoing_chat(const char* const id, const char* const barejid, const char* const message, const char* const replace_id, prof_enc_t enc)
{
if (active_db_backend && active_db_backend->add_outgoing_chat) {
active_db_backend->add_outgoing_chat(id, barejid, message, replace_id, enc);
}
_log_database_add_outgoing("chat", id, barejid, message, replace_id, enc);
}
void
log_database_add_outgoing_muc(const char* const id, const char* const barejid, const char* const message, const char* const replace_id, prof_enc_t enc)
{
if (active_db_backend && active_db_backend->add_outgoing_muc) {
active_db_backend->add_outgoing_muc(id, barejid, message, replace_id, enc);
}
_log_database_add_outgoing("muc", id, barejid, message, replace_id, enc);
}
void
log_database_add_outgoing_muc_pm(const char* const id, const char* const barejid, const char* const message, const char* const replace_id, prof_enc_t enc)
{
if (active_db_backend && active_db_backend->add_outgoing_muc_pm) {
active_db_backend->add_outgoing_muc_pm(id, barejid, message, replace_id, enc);
}
}
db_history_result_t
log_database_get_previous_chat(const gchar* const contact_barejid, const gchar* start_time, const gchar* end_time, gboolean from_start, gboolean flip, GSList** result)
{
if (active_db_backend && active_db_backend->get_previous_chat) {
return active_db_backend->get_previous_chat(contact_barejid, start_time, end_time, from_start, flip, result);
}
return DB_RESPONSE_ERROR;
_log_database_add_outgoing("mucpm", id, barejid, message, replace_id, enc);
}
// Get info (timestamp and stanza_id) of the first or last message in db
ProfMessage*
log_database_get_limits_info(const gchar* const contact_barejid, gboolean is_last)
{
if (active_db_backend && active_db_backend->get_limits_info) {
return active_db_backend->get_limits_info(contact_barejid, is_last);
sqlite3_stmt* stmt = NULL;
const Jid* myjid = connection_get_jid();
if (!myjid->str)
return NULL;
const char* order = is_last ? "DESC" : "ASC";
auto_sqlite char* query = sqlite3_mprintf("SELECT `archive_id`, `timestamp` FROM `ChatLogs` WHERE "
"(`from_jid` = %Q AND `to_jid` = %Q) OR "
"(`from_jid` = %Q AND `to_jid` = %Q) "
"ORDER BY `timestamp` %s LIMIT 1;",
contact_barejid, myjid->barejid, myjid->barejid, contact_barejid, order);
if (!query) {
log_error("Could not allocate memory for SQL query in log_database_get_limits_info()");
return NULL;
}
// Fallback: return an empty message with sane defaults
int rc = sqlite3_prepare_v2(g_chatlog_database, query, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
log_error("Unknown SQLite error in log_database_get_last_info().");
return NULL;
}
ProfMessage* msg = message_init();
if (is_last) {
msg->timestamp = g_date_time_new_now_utc();
if (sqlite3_step(stmt) == SQLITE_ROW) {
char* archive_id = (char*)sqlite3_column_text(stmt, 0);
char* date = (char*)sqlite3_column_text(stmt, 1);
msg->stanzaid = _db_strdup(archive_id);
msg->timestamp = g_date_time_new_from_iso8601(date, NULL);
}
sqlite3_finalize(stmt);
return msg;
}
GSList*
log_database_verify_integrity(const gchar* const contact_barejid)
// Query previous chats, constraints start_time and end_time. If end_time is
// null the current time is used. from_start gets first few messages if true
// otherwise the last ones. Flip flips the order of the results
db_history_result_t
log_database_get_previous_chat(const gchar* const contact_barejid, const gchar* start_time, const gchar* end_time, gboolean from_start, gboolean flip, GSList** result)
{
if (active_db_backend && active_db_backend->verify_integrity) {
return active_db_backend->verify_integrity(contact_barejid);
if (!g_chatlog_database) {
log_warning("log_database_get_previous_chat() called but db is not initialized");
return DB_RESPONSE_ERROR;
}
sqlite3_stmt* stmt = NULL;
const Jid* myjid = connection_get_jid();
if (!myjid->str) {
log_warning("log_database_get_previous_chat() called but no connection detected.");
return DB_RESPONSE_ERROR;
}
GSList* issues = NULL;
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
issue->level = INTEGRITY_WARNING;
issue->file = g_strdup("N/A");
issue->line = 0;
issue->message = g_strdup("Active backend does not support integrity verification");
issues = g_slist_append(issues, issue);
return issues;
// Flip order when querying older pages
gchar* sort1 = from_start ? "ASC" : "DESC";
gchar* sort2 = !flip ? "ASC" : "DESC";
GDateTime* now = g_date_time_new_now_local();
auto_gchar gchar* end_date_fmt = end_time ? g_strdup(end_time) : g_date_time_format_iso8601(now);
auto_sqlite gchar* query = sqlite3_mprintf("SELECT * FROM ("
"SELECT COALESCE(B.`message`, A.`message`) AS message, "
"A.`timestamp`, A.`from_jid`, A.`from_resource`, A.`to_jid`, A.`to_resource`, A.`type`, A.`encryption`, A.`stanza_id` FROM `ChatLogs` AS A "
"LEFT JOIN `ChatLogs` AS B ON (A.`replaced_by_db_id` = B.`id` AND A.`from_jid` = B.`from_jid`) "
"WHERE (A.`replaces_db_id` IS NULL) "
"AND ((A.`from_jid` = %Q AND A.`to_jid` = %Q) OR (A.`from_jid` = %Q AND A.`to_jid` = %Q)) "
"AND A.`timestamp` < %Q "
"AND (%Q IS NULL OR A.`timestamp` > %Q) "
"ORDER BY A.`timestamp` %s LIMIT %d) "
"ORDER BY `timestamp` %s;",
contact_barejid, myjid->barejid, myjid->barejid, contact_barejid, end_date_fmt, start_time, start_time, sort1, MESSAGES_TO_RETRIEVE, sort2);
g_date_time_unref(now);
if (!query) {
log_error("Could not allocate memory.");
return DB_RESPONSE_ERROR;
}
int rc = sqlite3_prepare_v2(g_chatlog_database, query, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
log_error("SQLite error in log_database_get_previous_chat(): (error code: %d) %s", rc, sqlite3_errmsg(g_chatlog_database));
return DB_RESPONSE_ERROR;
}
while (sqlite3_step(stmt) == SQLITE_ROW) {
char* message = (char*)sqlite3_column_text(stmt, 0);
char* date = (char*)sqlite3_column_text(stmt, 1);
char* from_jid = (char*)sqlite3_column_text(stmt, 2);
char* from_resource = (char*)sqlite3_column_text(stmt, 3);
char* to_jid = (char*)sqlite3_column_text(stmt, 4);
char* to_resource = (char*)sqlite3_column_text(stmt, 5);
char* type = (char*)sqlite3_column_text(stmt, 6);
char* encryption = (char*)sqlite3_column_text(stmt, 7);
char* id = (char*)sqlite3_column_text(stmt, 8);
ProfMessage* msg = message_init();
msg->id = id ? strdup(id) : NULL;
msg->from_jid = jid_create_from_bare_and_resource(from_jid, from_resource);
msg->to_jid = jid_create_from_bare_and_resource(to_jid, to_resource);
msg->plain = strdup(message ?: "");
msg->timestamp = g_date_time_new_from_iso8601(date, NULL);
msg->type = _get_message_type_type(type);
msg->enc = _get_message_enc_type(encryption);
*result = g_slist_append(*result, msg);
}
sqlite3_finalize(stmt);
return g_slist_length(*result) != 0 ? DB_RESPONSE_SUCCESS : DB_RESPONSE_EMPTY;
}
static const char*
_get_message_type_str(prof_msg_type_t type)
{
switch (type) {
case PROF_MSG_TYPE_CHAT:
return "chat";
case PROF_MSG_TYPE_MUC:
return "muc";
case PROF_MSG_TYPE_MUCPM:
return "mucpm";
case PROF_MSG_TYPE_UNINITIALIZED:
return NULL;
}
return NULL;
}
static prof_msg_type_t
_get_message_type_type(const char* const type)
{
if (g_strcmp0(type, "chat") == 0) {
return PROF_MSG_TYPE_CHAT;
} else if (g_strcmp0(type, "muc") == 0) {
return PROF_MSG_TYPE_MUC;
} else if (g_strcmp0(type, "mucpm") == 0) {
return PROF_MSG_TYPE_MUCPM;
} else {
return PROF_MSG_TYPE_UNINITIALIZED;
}
}
static const char*
_get_message_enc_str(prof_enc_t enc)
{
switch (enc) {
case PROF_MSG_ENC_OX:
return "ox";
case PROF_MSG_ENC_PGP:
return "pgp";
case PROF_MSG_ENC_OTR:
return "otr";
case PROF_MSG_ENC_OMEMO:
return "omemo";
case PROF_MSG_ENC_NONE:
return "none";
}
return "none";
}
static prof_enc_t
_get_message_enc_type(const char* const encstr)
{
if (g_strcmp0(encstr, "ox") == 0) {
return PROF_MSG_ENC_OX;
} else if (g_strcmp0(encstr, "pgp") == 0) {
return PROF_MSG_ENC_PGP;
} else if (g_strcmp0(encstr, "otr") == 0) {
return PROF_MSG_ENC_OTR;
} else if (g_strcmp0(encstr, "omemo") == 0) {
return PROF_MSG_ENC_OMEMO;
}
return PROF_MSG_ENC_NONE;
}
static void
_add_to_db(ProfMessage* message, char* type, const Jid* const from_jid, const Jid* const to_jid)
{
auto_gchar gchar* pref_dblog = prefs_get_string(PREF_DBLOG);
sqlite_int64 original_message_id = -1;
if (g_strcmp0(pref_dblog, "off") == 0) {
return;
} else if (g_strcmp0(pref_dblog, "redact") == 0) {
if (message->plain) {
free(message->plain);
}
message->plain = strdup("[REDACTED]");
}
if (!g_chatlog_database) {
log_debug("log_database_add() called but db is not initialized");
return;
}
char* err_msg;
auto_gchar gchar* date_fmt = NULL;
if (message->timestamp) {
date_fmt = g_date_time_format_iso8601(message->timestamp);
} else {
GDateTime* dt = g_date_time_new_now_local();
date_fmt = g_date_time_format_iso8601(dt);
g_date_time_unref(dt);
}
const char* enc = _get_message_enc_str(message->enc);
if (!type) {
type = (char*)_get_message_type_str(message->type);
}
// Apply LMC and check its validity (XEP-0308)
if (message->replace_id) {
auto_sqlite char* replace_check_query = sqlite3_mprintf("SELECT `id`, `from_jid`, `replaces_db_id` FROM `ChatLogs` WHERE `stanza_id` = %Q ORDER BY `timestamp` DESC LIMIT 1",
message->replace_id);
if (!replace_check_query) {
log_error("Could not allocate memory for SQL replace query in log_database_add()");
return;
}
sqlite3_stmt* lmc_stmt = NULL;
if (SQLITE_OK != sqlite3_prepare_v2(g_chatlog_database, replace_check_query, -1, &lmc_stmt, NULL)) {
log_error("SQLite error in _add_to_db() on selecting original message: %s", sqlite3_errmsg(g_chatlog_database));
return;
}
if (sqlite3_step(lmc_stmt) == SQLITE_ROW) {
original_message_id = sqlite3_column_int64(lmc_stmt, 0);
const char* from_jid_orig = (const char*)sqlite3_column_text(lmc_stmt, 1);
// Handle non-XEP-compliant replacement messages (edit->edit->original)
sqlite_int64 tmp = sqlite3_column_int64(lmc_stmt, 2);
original_message_id = tmp ? tmp : original_message_id;
if (g_strcmp0(from_jid_orig, from_jid->barejid) != 0) {
log_error("Mismatch in sender JIDs when trying to do LMC. Corrected message sender: %s. Original message sender: %s. Replace-ID: %s. Message: %s", from_jid->barejid, from_jid_orig, message->replace_id, message->plain);
cons_show_error("%s sent a message correction with mismatched sender. See log for details.", from_jid->barejid);
sqlite3_finalize(lmc_stmt);
return;
}
} else {
log_warning("Got LMC message that does not have original message counterpart in the database from %s", message->from_jid->fulljid);
}
sqlite3_finalize(lmc_stmt);
}
// stanza-id (XEP-0359) doesn't have to be present in the message.
// But if it's duplicated, it's a serious server-side problem, so we better track it.
// Unless it's MAM, in that case it's expected behaviour.
if (message->stanzaid && !message->is_mam) {
auto_sqlite char* duplicate_check_query = sqlite3_mprintf("SELECT 1 FROM `ChatLogs` WHERE (`archive_id` = %Q)",
message->stanzaid);
if (!duplicate_check_query) {
log_error("Could not allocate memory for SQL duplicate query in log_database_add()");
return;
}
sqlite3_stmt* stmt;
if (SQLITE_OK == sqlite3_prepare_v2(g_chatlog_database, duplicate_check_query, -1, &stmt, NULL)) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
log_error("Duplicate stanza-id found for the message. stanza_id: %s; archive_id: %s; sender: %s; content: %s", message->id, message->stanzaid, from_jid->barejid, message->plain);
cons_show_error("Got a message with duplicate (server-generated) stanza-id from %s.", from_jid->fulljid);
}
sqlite3_finalize(stmt);
}
}
auto_sqlite char* orig_message_id = original_message_id == -1 ? NULL : sqlite3_mprintf("%d", original_message_id);
auto_sqlite char* query = sqlite3_mprintf("INSERT INTO `ChatLogs` "
"(`from_jid`, `from_resource`, `to_jid`, `to_resource`, "
"`message`, `timestamp`, `stanza_id`, `archive_id`, "
"`replaces_db_id`, `replace_id`, `type`, `encryption`) "
"VALUES (%Q, %Q, %Q, %Q, %Q, %Q, %Q, %Q, %Q, %Q, %Q, %Q)",
from_jid->barejid,
from_jid->resourcepart,
to_jid->barejid,
to_jid->resourcepart,
message->plain,
date_fmt,
message->id,
message->stanzaid,
orig_message_id,
message->replace_id,
type,
enc);
if (!query) {
log_error("Could not allocate memory for SQL insert query in log_database_add()");
return;
}
log_debug("Writing to DB. Query: %s", query);
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, query, NULL, 0, &err_msg)) {
if (err_msg) {
log_error("SQLite error in _add_to_db(): %s", err_msg);
sqlite3_free(err_msg);
} else {
log_error("Unknown SQLite error in _add_to_db().");
}
} else {
int inserted_rows_count = sqlite3_changes(g_chatlog_database);
if (inserted_rows_count < 1) {
log_error("SQLite did not insert message (rows: %d, id: %s, content: %s)", inserted_rows_count, message->id, message->plain);
}
}
}
static int
_get_db_version(void)
{
int current_version = -1;
const char* query = "SELECT `version` FROM `DbVersion` LIMIT 1";
sqlite3_stmt* statement;
if (sqlite3_prepare_v2(g_chatlog_database, query, -1, &statement, NULL) == SQLITE_OK) {
if (sqlite3_step(statement) == SQLITE_ROW) {
current_version = sqlite3_column_int(statement, 0);
}
sqlite3_finalize(statement);
}
return current_version;
}
/**
* Migration to version 2 introduces new columns. Returns TRUE on success.
*
* New columns:
* `replaces_db_id` database ID for correcting message of the original message
* `replaced_by_db_id` database ID for original message of the last correcting message
*/
static gboolean
_migrate_to_v2(void)
{
char* err_msg = NULL;
// from_resource, to_resource, message, timestamp, stanza_id, archive_id, replace_id, type, encryption
const char* sql_statements[] = {
"BEGIN TRANSACTION",
"ALTER TABLE `ChatLogs` ADD COLUMN `replaces_db_id` INTEGER;",
"ALTER TABLE `ChatLogs` ADD COLUMN `replaced_by_db_id` INTEGER;",
"UPDATE `ChatLogs` AS A "
"SET `replaces_db_id` = B.`id` "
"FROM `ChatLogs` AS B "
"WHERE A.`replace_id` IS NOT NULL AND A.`replace_id` != '' "
"AND A.`replace_id` = B.`stanza_id` "
"AND A.`from_jid` = B.`from_jid` AND A.`to_jid` = B.`to_jid`;",
"UPDATE `ChatLogs` AS A "
"SET `replaced_by_db_id` = B.`id` "
"FROM `ChatLogs` AS B "
"WHERE (A.`replace_id` IS NULL OR A.`replace_id` = '') "
"AND A.`id` = B.`replaces_db_id` "
"AND A.`from_jid` = B.`from_jid`;",
"UPDATE ChatLogs SET "
"from_resource = COALESCE(NULLIF(from_resource, ''), NULL), "
"to_resource = COALESCE(NULLIF(to_resource, ''), NULL), "
"message = COALESCE(NULLIF(message, ''), NULL), "
"timestamp = COALESCE(NULLIF(timestamp, ''), NULL), "
"stanza_id = COALESCE(NULLIF(stanza_id, ''), NULL), "
"archive_id = COALESCE(NULLIF(archive_id, ''), NULL), "
"replace_id = COALESCE(NULLIF(replace_id, ''), NULL), "
"type = COALESCE(NULLIF(type, ''), NULL), "
"encryption = COALESCE(NULLIF(encryption, ''), NULL);",
"UPDATE `DbVersion` SET `version` = 2;",
"END TRANSACTION"
};
int statements_count = sizeof(sql_statements) / sizeof(sql_statements[0]);
for (int i = 0; i < statements_count; i++) {
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, sql_statements[i], NULL, 0, &err_msg)) {
log_error("SQLite error in _migrate_to_v2() on statement %d: %s", i, err_msg);
if (err_msg) {
sqlite3_free(err_msg);
err_msg = NULL;
}
goto cleanup;
}
}
return TRUE;
cleanup:
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, "ROLLBACK;", NULL, 0, &err_msg)) {
log_error("[DB Migration] Unable to ROLLBACK: %s", err_msg);
if (err_msg) {
sqlite3_free(err_msg);
}
}
return FALSE;
}
// Checks if there is more system storage space available than current database takes + 40% (for indexing and other potential size increases)
static gboolean
_check_available_space_for_db_migration(char* path_to_db)
{
struct stat file_stat;
struct statvfs fs_stat;
if (statvfs(path_to_db, &fs_stat) == 0 && stat(path_to_db, &file_stat) == 0) {
unsigned long long file_size = file_stat.st_size / 1024;
unsigned long long available_space_kb = fs_stat.f_frsize * fs_stat.f_bavail / 1024;
log_debug("_check_available_space_for_db_migration(): Available space on disk: %llu KB; DB size: %llu KB", available_space_kb, file_size);
return (available_space_kb >= (file_size + (file_size * 10 / 4)));
} else {
log_error("Error checking available space.");
return FALSE;
}
}

View File

@@ -48,47 +48,6 @@ typedef enum {
DB_RESPONSE_SUCCESS
} db_history_result_t;
// Integrity verification issue levels
typedef enum {
INTEGRITY_ERROR,
INTEGRITY_WARNING,
INTEGRITY_INFO
} integrity_level_t;
// A single integrity issue found during verification
typedef struct
{
integrity_level_t level;
char* file;
int line;
char* message;
} integrity_issue_t;
void integrity_issue_free(integrity_issue_t* issue);
// Backend vtable: pluggable storage backends implement this interface
typedef struct db_backend_t
{
const char* name;
gboolean (*init)(ProfAccount* account);
void (*close)(void);
void (*add_incoming)(ProfMessage* message);
void (*add_outgoing_chat)(const char* const id, const char* const barejid, const char* const message, const char* const replace_id, prof_enc_t enc);
void (*add_outgoing_muc)(const char* const id, const char* const barejid, const char* const message, const char* const replace_id, prof_enc_t enc);
void (*add_outgoing_muc_pm)(const char* const id, const char* const barejid, const char* const message, const char* const replace_id, prof_enc_t enc);
db_history_result_t (*get_previous_chat)(const gchar* const contact_barejid, const gchar* start_time, const gchar* end_time, gboolean from_start, gboolean flip, GSList** result);
ProfMessage* (*get_limits_info)(const gchar* const contact_barejid, gboolean is_last);
GSList* (*verify_integrity)(const gchar* const contact_barejid);
} db_backend_t;
// Active backend (set during init based on PREF_DBLOG)
extern db_backend_t* active_db_backend;
// Backend registry
db_backend_t* db_backend_sqlite(void);
db_backend_t* db_backend_flatfile(void);
// Public API (dispatches to active_db_backend)
gboolean log_database_init(ProfAccount* account);
void log_database_add_incoming(ProfMessage* message);
void log_database_add_outgoing_chat(const char* const id, const char* const barejid, const char* const message, const char* const replace_id, prof_enc_t enc);
@@ -97,6 +56,5 @@ void log_database_add_outgoing_muc_pm(const char* const id, const char* const ba
db_history_result_t log_database_get_previous_chat(const gchar* const contact_barejid, const gchar* start_time, const gchar* end_time, gboolean from_start, gboolean flip, GSList** result);
ProfMessage* log_database_get_limits_info(const gchar* const contact_barejid, gboolean is_last);
void log_database_close(void);
GSList* log_database_verify_integrity(const gchar* const contact_barejid);
#endif // DATABASE_H

View File

@@ -1,609 +0,0 @@
/*
* database_flatfile.c
* vim: expandtab:ts=4:sts=4:sw=4
*
* Copyright (C) 2026 Profanity Contributors
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* Flat-file database backend: init/close, message write, message read,
* LMC correction logic, and the backend vtable.
*
* Parser and escape helpers are in database_flatfile_parser.c.
* Integrity verification is in database_flatfile_verify.c.
* Shared types and prototypes are in database_flatfile.h.
*/
#include "config.h"
#include <fcntl.h>
#include <unistd.h>
#include <glib.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include "log.h"
#include "common.h"
#include "config/files.h"
#include "database.h"
#include "database_flatfile.h"
#include "config/preferences.h"
#include "ui/ui.h"
#include "xmpp/xmpp.h"
#include "xmpp/message.h"
// =========================================================================
// Shared global — account JID stored during init for path construction
// =========================================================================
char* g_flatfile_account_jid = NULL;
// =========================================================================
// Core write logic
// =========================================================================
static void
_ff_add_message(const char* type, const char* stanza_id, const char* archive_id,
const char* replace_id, const char* from_barejid, const char* from_resource,
const char* to_barejid, const char* message_text, GDateTime* timestamp,
prof_enc_t enc)
{
auto_gchar gchar* pref_dblog = prefs_get_string(PREF_DBLOG);
if (g_strcmp0(pref_dblog, "off") == 0) {
return;
}
// "redact" replaces the message content.
char* effective_msg = NULL;
if (g_strcmp0(pref_dblog, "redact") == 0) {
effective_msg = g_strdup("[REDACTED]");
} else {
effective_msg = g_strdup(message_text ? message_text : "");
}
GDateTime* dt = timestamp ? g_date_time_ref(timestamp) : g_date_time_new_now_local();
auto_gchar gchar* date_fmt = g_date_time_format_iso8601(dt);
// Determine which JID to use for the log file path (the "other" party)
const char* contact = NULL;
const Jid* myjid = connection_get_jid();
if (myjid && myjid->barejid && g_strcmp0(from_barejid, myjid->barejid) == 0) {
contact = to_barejid;
} else {
contact = from_barejid;
}
auto_gchar gchar* log_path = ff_get_log_path(contact, dt);
g_date_time_unref(dt);
if (!log_path) {
log_error("flatfile: could not determine log path for %s", contact);
g_free(effective_msg);
return;
}
// Ensure directory exists
auto_gchar gchar* dir = g_path_get_dirname(log_path);
if (!ff_ensure_dir(dir)) {
g_free(effective_msg);
return;
}
// Open the file with O_NOFOLLOW to prevent symlink attacks,
// and O_APPEND for safe concurrent appends.
// Try O_CREAT|O_EXCL first to detect new files atomically (no TOCTOU).
int fd = open(log_path, O_WRONLY | O_APPEND | O_CREAT | O_EXCL | O_NOFOLLOW,
S_IRUSR | S_IWUSR);
gboolean is_new = (fd >= 0);
if (fd < 0) {
if (errno == EEXIST) {
// File already exists — open for append
fd = open(log_path, O_WRONLY | O_APPEND | O_NOFOLLOW);
}
if (fd < 0) {
// ELOOP (symlink) or other error
log_error("flatfile: could not open %s for writing (errno=%d: %s)",
log_path, errno, strerror(errno));
g_free(effective_msg);
return;
}
}
FILE* fp = fdopen(fd, "a");
if (!fp) {
log_error("flatfile: fdopen failed for %s (errno=%d)", log_path, errno);
close(fd);
g_free(effective_msg);
return;
}
if (is_new) {
fprintf(fp, "%s", FLATFILE_HEADER);
}
ff_write_line(fp, date_fmt, type, ff_get_message_enc_str(enc),
stanza_id, archive_id, replace_id,
from_barejid, from_resource, effective_msg);
fflush(fp);
fclose(fp);
g_free(effective_msg);
}
// =========================================================================
// Backend callbacks: init / close
// =========================================================================
static gboolean
_flatfile_init(ProfAccount* account)
{
if (!account || !account->jid) {
log_error("flatfile: cannot init without account JID");
return FALSE;
}
g_free(g_flatfile_account_jid);
g_flatfile_account_jid = g_strdup(account->jid);
// Ensure base directory exists
auto_gchar gchar* data_path = files_get_data_path(DIR_FLATLOG);
auto_gchar gchar* my_dir = ff_jid_to_dir(g_flatfile_account_jid);
auto_gchar gchar* base_dir = g_strdup_printf("%s/%s", data_path, my_dir);
if (!ff_ensure_dir(base_dir)) {
return FALSE;
}
log_info("Initialized flat-file database backend: %s", base_dir);
return TRUE;
}
static void
_flatfile_close(void)
{
g_free(g_flatfile_account_jid);
g_flatfile_account_jid = NULL;
log_debug("flatfile: closed");
}
// =========================================================================
// Backend callbacks: add message
// =========================================================================
static void
_flatfile_add_incoming(ProfMessage* message)
{
if (!message || !message->from_jid)
return;
const Jid* to_jid = message->to_jid ? message->to_jid : connection_get_jid();
const char* type = ff_get_message_type_str(message->type);
_ff_add_message(type, message->id, message->stanzaid, message->replace_id,
message->from_jid->barejid, message->from_jid->resourcepart,
to_jid->barejid, message->plain,
message->timestamp, message->enc);
}
static void
_flatfile_add_outgoing_chat(const char* const id, const char* const barejid,
const char* const message, const char* const replace_id, prof_enc_t enc)
{
const Jid* myjid = connection_get_jid();
_ff_add_message("chat", id, NULL, replace_id,
myjid ? myjid->barejid : "me", myjid ? myjid->resourcepart : NULL,
barejid, message, NULL, enc);
}
static void
_flatfile_add_outgoing_muc(const char* const id, const char* const barejid,
const char* const message, const char* const replace_id, prof_enc_t enc)
{
const Jid* myjid = connection_get_jid();
_ff_add_message("muc", id, NULL, replace_id,
myjid ? myjid->barejid : "me", myjid ? myjid->resourcepart : NULL,
barejid, message, NULL, enc);
}
static void
_flatfile_add_outgoing_muc_pm(const char* const id, const char* const barejid,
const char* const message, const char* const replace_id, prof_enc_t enc)
{
const Jid* myjid = connection_get_jid();
_ff_add_message("mucpm", id, NULL, replace_id,
myjid ? myjid->barejid : "me", myjid ? myjid->resourcepart : NULL,
barejid, message, NULL, enc);
}
// =========================================================================
// Read logic
// =========================================================================
// List all log files for a contact, sorted by filename (date order)
static GSList*
_ff_list_log_files(const char* contact_barejid)
{
auto_gchar gchar* contact_dir = ff_get_contact_dir(contact_barejid);
if (!contact_dir || !g_file_test(contact_dir, G_FILE_TEST_IS_DIR)) {
return NULL;
}
GDir* dir = g_dir_open(contact_dir, 0, NULL);
if (!dir)
return NULL;
GSList* files = NULL;
const gchar* fname;
while ((fname = g_dir_read_name(dir)) != NULL) {
if (g_str_has_suffix(fname, ".log")) {
files = g_slist_insert_sorted(files,
g_strdup_printf("%s/%s", contact_dir, fname),
(GCompareFunc)g_strcmp0);
}
}
g_dir_close(dir);
return files;
}
// Read all parsed lines from a file, applying filters
static GSList*
_ff_read_file_messages(const char* filepath, const char* contact_barejid,
const char* start_time, const char* end_time,
const Jid* myjid)
{
GSList* messages = NULL;
FILE* fp = fopen(filepath, "r");
if (!fp)
return NULL;
// BOM detection
int c1 = fgetc(fp);
int c2 = fgetc(fp);
int c3 = fgetc(fp);
if (!(c1 == 0xEF && c2 == 0xBB && c3 == 0xBF)) {
// Not a BOM, rewind
fseek(fp, 0, SEEK_SET);
}
char* buf = NULL;
gboolean truncated = FALSE;
while ((buf = ff_readline(fp, &truncated)) != NULL) {
if (truncated) {
log_warning("flatfile: truncated line at EOF in %s, skipping", filepath);
free(buf);
break;
}
ff_parsed_line_t* pl = ff_parse_line(buf);
free(buf);
if (!pl)
continue;
// Check timestamp bounds
if (start_time) {
GDateTime* start_dt = g_date_time_new_from_iso8601(start_time, NULL);
if (start_dt) {
if (g_date_time_compare(pl->timestamp, start_dt) <= 0) {
g_date_time_unref(start_dt);
ff_parsed_line_free(pl);
continue;
}
g_date_time_unref(start_dt);
}
}
if (end_time) {
GDateTime* end_dt = g_date_time_new_from_iso8601(end_time, NULL);
if (end_dt) {
if (g_date_time_compare(pl->timestamp, end_dt) >= 0) {
g_date_time_unref(end_dt);
ff_parsed_line_free(pl);
continue;
}
g_date_time_unref(end_dt);
}
}
// Check JID filter: message must involve contact and our JID
gboolean matches = FALSE;
if (myjid && myjid->barejid && contact_barejid) {
if ((g_strcmp0(pl->from_jid, contact_barejid) == 0) || (g_strcmp0(pl->from_jid, myjid->barejid) == 0)) {
matches = TRUE;
}
} else {
matches = TRUE; // no filter
}
if (!matches) {
ff_parsed_line_free(pl);
continue;
}
messages = g_slist_append(messages, pl);
}
fclose(fp);
return messages;
}
// Apply LMC: for messages with corrects:X, find original and replace its text
static void
_ff_apply_lmc(GSList* parsed_lines, GSList** result)
{
// Build a hash: stanza_id -> parsed_line for quick lookup
GHashTable* id_map = g_hash_table_new(g_str_hash, g_str_equal);
for (GSList* l = parsed_lines; l; l = l->next) {
ff_parsed_line_t* pl = l->data;
if (pl->stanza_id && strlen(pl->stanza_id) > 0) {
g_hash_table_insert(id_map, pl->stanza_id, pl);
}
}
// Track which lines are corrections (skip them in output, apply to originals)
GHashTable* corrections = g_hash_table_new(g_direct_hash, g_direct_equal);
// Map: original line ptr -> latest correcting line ptr
GHashTable* correction_map = g_hash_table_new(g_direct_hash, g_direct_equal);
for (GSList* l = parsed_lines; l; l = l->next) {
ff_parsed_line_t* pl = l->data;
if (pl->replace_id && strlen(pl->replace_id) > 0) {
ff_parsed_line_t* original = g_hash_table_lookup(id_map, pl->replace_id);
if (original) {
// Follow chain to the root original, with cycle/depth guard
ff_parsed_line_t* root = original;
GHashTable* visited = g_hash_table_new(g_direct_hash, g_direct_equal);
g_hash_table_insert(visited, root, root);
int depth = 0;
while (root->replace_id && strlen(root->replace_id) > 0 && depth < FF_MAX_LMC_DEPTH) {
ff_parsed_line_t* parent = g_hash_table_lookup(id_map, root->replace_id);
if (parent && !g_hash_table_contains(visited, parent)) {
g_hash_table_insert(visited, parent, parent);
root = parent;
depth++;
} else {
break;
}
}
g_hash_table_destroy(visited);
if (depth >= FF_MAX_LMC_DEPTH) {
log_warning("flatfile: LMC correction chain too deep (>%d), ignoring", FF_MAX_LMC_DEPTH);
}
g_hash_table_insert(correction_map, root, pl);
g_hash_table_insert(corrections, pl, pl);
}
}
}
// Build result: for each non-correction line, output it (with corrected text if applicable)
for (GSList* l = parsed_lines; l; l = l->next) {
ff_parsed_line_t* pl = l->data;
if (g_hash_table_lookup(corrections, pl)) {
continue; // skip correction-only lines
}
ff_parsed_line_t* latest_correction = g_hash_table_lookup(correction_map, pl);
ProfMessage* msg;
if (latest_correction) {
// Use corrected text with original's metadata
msg = ff_parsed_to_profmessage(pl);
g_free(msg->plain);
msg->plain = g_strdup(latest_correction->message ? latest_correction->message : "");
} else {
msg = ff_parsed_to_profmessage(pl);
}
*result = g_slist_append(*result, msg);
}
g_hash_table_destroy(id_map);
g_hash_table_destroy(corrections);
g_hash_table_destroy(correction_map);
}
// =========================================================================
// Backend callbacks: query history
// =========================================================================
static db_history_result_t
_flatfile_get_previous_chat(const gchar* const contact_barejid, const gchar* start_time,
const gchar* end_time, gboolean from_start, gboolean flip,
GSList** result)
{
if (!g_flatfile_account_jid) {
log_warning("flatfile: get_previous_chat called but not initialized");
return DB_RESPONSE_ERROR;
}
const Jid* myjid = connection_get_jid();
if (!myjid || !myjid->barejid) {
log_warning("flatfile: no connection JID available");
return DB_RESPONSE_ERROR;
}
// If no end_time, use now
auto_gchar gchar* effective_end = NULL;
if (end_time) {
effective_end = g_strdup(end_time);
} else {
GDateTime* now = g_date_time_new_now_local();
effective_end = g_date_time_format_iso8601(now);
g_date_time_unref(now);
}
// Get all log files for this contact
GSList* files = _ff_list_log_files(contact_barejid);
if (!files) {
return DB_RESPONSE_EMPTY;
}
// Collect all matching parsed lines across files
GSList* all_parsed = NULL;
for (GSList* f = files; f; f = f->next) {
GSList* file_msgs = _ff_read_file_messages(f->data, contact_barejid,
start_time, effective_end, myjid);
all_parsed = g_slist_concat(all_parsed, file_msgs);
}
g_slist_free_full(files, g_free);
if (!all_parsed) {
return DB_RESPONSE_EMPTY;
}
// Apply LMC corrections and build ProfMessage list
GSList* pre_result = NULL;
_ff_apply_lmc(all_parsed, &pre_result);
// Free parsed lines
g_slist_free_full(all_parsed, (GDestroyNotify)ff_parsed_line_free);
if (!pre_result) {
return DB_RESPONSE_EMPTY;
}
// Apply pagination: take first or last MESSAGES_TO_RETRIEVE
guint total = g_slist_length(pre_result);
if (total > MESSAGES_TO_RETRIEVE) {
if (from_start) {
// Keep first N, free rest
GSList* nth = g_slist_nth(pre_result, MESSAGES_TO_RETRIEVE);
if (nth) {
GSList* prev = g_slist_nth(pre_result, MESSAGES_TO_RETRIEVE - 1);
if (prev)
prev->next = NULL;
g_slist_free_full(nth, (GDestroyNotify)message_free);
}
} else {
// Keep last N, free first (total - N)
guint skip = total - MESSAGES_TO_RETRIEVE;
GSList* keep_start = g_slist_nth(pre_result, skip);
GSList* prev = g_slist_nth(pre_result, skip - 1);
if (prev)
prev->next = NULL;
g_slist_free_full(pre_result, (GDestroyNotify)message_free);
pre_result = keep_start;
}
}
// Apply flip (reverse order)
if (flip) {
pre_result = g_slist_reverse(pre_result);
}
*result = pre_result;
return g_slist_length(*result) != 0 ? DB_RESPONSE_SUCCESS : DB_RESPONSE_EMPTY;
}
static ProfMessage*
_flatfile_get_limits_info(const gchar* const contact_barejid, gboolean is_last)
{
ProfMessage* msg = message_init();
if (!g_flatfile_account_jid || !contact_barejid) {
if (is_last)
msg->timestamp = g_date_time_new_now_utc();
return msg;
}
GSList* files = _ff_list_log_files(contact_barejid);
if (!files) {
if (is_last)
msg->timestamp = g_date_time_new_now_utc();
return msg;
}
// For first message: read first non-comment line of first file
// For last message: read last non-comment line of last file
const char* target_file = is_last ? (const char*)g_slist_last(files)->data
: (const char*)files->data;
FILE* fp = fopen(target_file, "r");
if (!fp) {
g_slist_free_full(files, g_free);
if (is_last)
msg->timestamp = g_date_time_new_now_utc();
return msg;
}
// BOM skip
int c1 = fgetc(fp);
int c2 = fgetc(fp);
int c3 = fgetc(fp);
if (!(c1 == 0xEF && c2 == 0xBB && c3 == 0xBF)) {
fseek(fp, 0, SEEK_SET);
}
char* buf = NULL;
ff_parsed_line_t* found = NULL;
if (!is_last) {
// Find first valid line
while ((buf = ff_readline(fp, NULL)) != NULL) {
found = ff_parse_line(buf);
free(buf);
if (found)
break;
}
} else {
// Find last valid line — read all and keep last
while ((buf = ff_readline(fp, NULL)) != NULL) {
ff_parsed_line_t* pl = ff_parse_line(buf);
free(buf);
if (pl) {
if (found)
ff_parsed_line_free(found);
found = pl;
}
}
}
fclose(fp);
if (found) {
msg->stanzaid = found->archive_id ? g_strdup(found->archive_id) : NULL;
msg->timestamp = g_date_time_ref(found->timestamp);
ff_parsed_line_free(found);
} else if (is_last) {
msg->timestamp = g_date_time_new_now_utc();
}
g_slist_free_full(files, g_free);
return msg;
}
// =========================================================================
// Backend vtable
// =========================================================================
static db_backend_t flatfile_backend = {
.name = "flatfile",
.init = _flatfile_init,
.close = _flatfile_close,
.add_incoming = _flatfile_add_incoming,
.add_outgoing_chat = _flatfile_add_outgoing_chat,
.add_outgoing_muc = _flatfile_add_outgoing_muc,
.add_outgoing_muc_pm = _flatfile_add_outgoing_muc_pm,
.get_previous_chat = _flatfile_get_previous_chat,
.get_limits_info = _flatfile_get_limits_info,
.verify_integrity = ff_verify_integrity,
};
db_backend_t*
db_backend_flatfile(void)
{
return &flatfile_backend;
}

View File

@@ -1,110 +0,0 @@
/*
* database_flatfile.h
* vim: expandtab:ts=4:sts=4:sw=4
*
* Copyright (C) 2026 Profanity Contributors
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* Internal header shared between database_flatfile*.c modules.
* Not part of the public API — do not include from outside the flatfile backend.
*/
#ifndef DATABASE_FLATFILE_H
#define DATABASE_FLATFILE_H
#include <glib.h>
#include <stdio.h>
#include "database.h"
#include "xmpp/xmpp.h"
#include "xmpp/message.h"
// --- Constants ---
#define DIR_FLATLOG "flatlog"
#define FLATFILE_HEADER "# profanity chat log — UTF-8, LF line endings\n# vim: set fileencoding=utf-8 fileformat=unix :\n"
#define FF_MAX_LINE_LEN (10 * 1024 * 1024) /* 10 MB — reject lines longer than this */
#define FF_MAX_LMC_DEPTH 100 /* max correction chain depth */
// --- Shared global ---
// Account JID stored during init for path construction.
// Defined in database_flatfile.c, used by all flatfile modules.
extern char* g_flatfile_account_jid;
// --- Parsed line structure ---
typedef struct
{
char* timestamp_str;
GDateTime* timestamp;
char* type;
char* enc;
char* stanza_id;
char* archive_id;
char* replace_id;
char* from_jid;
char* from_resource;
char* message;
} ff_parsed_line_t;
// --- Type conversion helpers ---
const char* ff_get_message_type_str(prof_msg_type_t type);
prof_msg_type_t ff_get_message_type_type(const char* const type);
const char* ff_get_message_enc_str(prof_enc_t enc);
prof_enc_t ff_get_message_enc_type(const char* const encstr);
// --- Path helpers ---
char* ff_jid_to_dir(const char* jid);
char* ff_get_contact_dir(const char* contact_barejid);
char* ff_get_log_path(const char* contact_barejid, GDateTime* dt);
gboolean ff_ensure_dir(const char* path);
// --- Escape / unescape ---
char* ff_escape_message(const char* text);
char* ff_unescape_message(const char* text);
char* ff_escape_meta_value(const char* val);
char* ff_unescape_meta_value(const char* val);
// --- I/O ---
char* ff_readline(FILE* fp, gboolean* truncated);
void ff_write_line(FILE* fp, const char* timestamp, const char* type, const char* enc,
const char* stanza_id, const char* archive_id, const char* replace_id,
const char* from_jid, const char* from_resource, const char* message_text);
// --- Parser helpers ---
const char* ff_find_unescaped_char(const char* str, char ch);
char** ff_split_meta(const char* meta);
const char* ff_find_unescaped_colonspace(const char* str);
char* ff_unescape_sender_resource(const char* res);
// --- Parser ---
void ff_parsed_line_free(ff_parsed_line_t* pl);
ff_parsed_line_t* ff_parse_line(const char* line);
ProfMessage* ff_parsed_to_profmessage(ff_parsed_line_t* pl);
// --- Integrity verification (database_flatfile_verify.c) ---
GSList* ff_verify_integrity(const gchar* const contact_barejid);
#endif

View File

@@ -1,752 +0,0 @@
/*
* database_flatfile_parser.c
* vim: expandtab:ts=4:sts=4:sw=4
*
* Copyright (C) 2026 Profanity Contributors
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* Flat-file backend: type helpers, path helpers, escape/unescape,
* line I/O, and the tolerant log-line parser.
*/
#include "config.h"
#include <sys/stat.h>
#include <glib.h>
#include <glib/gstdio.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include "log.h"
#include "common.h"
#include "config/files.h"
#include "database_flatfile.h"
// =========================================================================
// Type conversion helpers
// =========================================================================
const char*
ff_get_message_type_str(prof_msg_type_t type)
{
switch (type) {
case PROF_MSG_TYPE_CHAT:
return "chat";
case PROF_MSG_TYPE_MUC:
return "muc";
case PROF_MSG_TYPE_MUCPM:
return "mucpm";
case PROF_MSG_TYPE_UNINITIALIZED:
return "chat";
}
return "chat";
}
prof_msg_type_t
ff_get_message_type_type(const char* const type)
{
if (g_strcmp0(type, "chat") == 0) {
return PROF_MSG_TYPE_CHAT;
} else if (g_strcmp0(type, "muc") == 0) {
return PROF_MSG_TYPE_MUC;
} else if (g_strcmp0(type, "mucpm") == 0) {
return PROF_MSG_TYPE_MUCPM;
}
return PROF_MSG_TYPE_CHAT;
}
const char*
ff_get_message_enc_str(prof_enc_t enc)
{
switch (enc) {
case PROF_MSG_ENC_OX:
return "ox";
case PROF_MSG_ENC_PGP:
return "pgp";
case PROF_MSG_ENC_OTR:
return "otr";
case PROF_MSG_ENC_OMEMO:
return "omemo";
case PROF_MSG_ENC_NONE:
return "none";
}
return "none";
}
prof_enc_t
ff_get_message_enc_type(const char* const encstr)
{
if (g_strcmp0(encstr, "ox") == 0) {
return PROF_MSG_ENC_OX;
} else if (g_strcmp0(encstr, "pgp") == 0) {
return PROF_MSG_ENC_PGP;
} else if (g_strcmp0(encstr, "otr") == 0) {
return PROF_MSG_ENC_OTR;
} else if (g_strcmp0(encstr, "omemo") == 0) {
return PROF_MSG_ENC_OMEMO;
}
return PROF_MSG_ENC_NONE;
}
// =========================================================================
// Path helpers
// =========================================================================
// Sanitise a JID for use as a directory name.
// 1. Replace '@' with '_at_'
// 2. Reject / strip path-separator and traversal characters: '/', '\0', '..'
// This prevents a malicious federated JID like "../../../tmp/pwned" from
// escaping the log directory tree.
char*
ff_jid_to_dir(const char* jid)
{
if (!jid || jid[0] == '\0')
return NULL;
// Replace '@' first
char* step1 = str_replace(jid, "@", "_at_");
if (!step1)
return NULL;
// Replace '/' and '\\' with '_' to prevent path traversal
GString* out = g_string_sized_new(strlen(step1));
for (const char* p = step1; *p; p++) {
if (*p == '/' || *p == '\\') {
g_string_append_c(out, '_');
} else {
g_string_append_c(out, *p);
}
}
free(step1);
// Collapse any remaining ".." sequences to "__" (belt-and-suspenders)
char* result = g_string_free(out, FALSE);
char* dotdot;
while ((dotdot = strstr(result, "..")) != NULL) {
dotdot[0] = '_';
dotdot[1] = '_';
}
// Reject empty result
if (result[0] == '\0') {
g_free(result);
return NULL;
}
return result;
}
// Get the base directory for a contact's logs:
// ~/.local/share/profanity/flatlog/{my_jid_dir}/{contact_jid_dir}/
char*
ff_get_contact_dir(const char* contact_barejid)
{
if (!g_flatfile_account_jid || !contact_barejid)
return NULL;
auto_gchar gchar* data_path = files_get_data_path(DIR_FLATLOG);
auto_gchar gchar* my_dir = ff_jid_to_dir(g_flatfile_account_jid);
auto_gchar gchar* contact_dir = ff_jid_to_dir(contact_barejid);
char* result = g_strdup_printf("%s/%s/%s", data_path, my_dir, contact_dir);
return result;
}
// Get the log file path for a contact on a specific date:
// {contact_dir}/{YYYY_MM_DD}.log
char*
ff_get_log_path(const char* contact_barejid, GDateTime* dt)
{
auto_gchar gchar* contact_dir = ff_get_contact_dir(contact_barejid);
if (!contact_dir)
return NULL;
auto_gchar gchar* date_str = g_date_time_format(dt, "%Y_%m_%d");
char* result = g_strdup_printf("%s/%s.log", contact_dir, date_str);
return result;
}
// Ensure the directory exists, create if needed.
// Refuses to follow symlinks at the final component.
gboolean
ff_ensure_dir(const char* path)
{
if (g_file_test(path, G_FILE_TEST_IS_DIR)) {
// Verify it's not a symlink
struct stat st;
if (g_lstat(path, &st) == 0 && S_ISLNK(st.st_mode)) {
log_error("flatfile: directory path is a symlink, refusing: %s", path);
return FALSE;
}
return TRUE;
}
if (g_mkdir_with_parents(path, S_IRWXU) != 0) {
log_error("flatfile: Could not create directory: %s (errno=%d)", path, errno);
return FALSE;
}
return TRUE;
}
// =========================================================================
// Escape / unescape helpers
// =========================================================================
//
// Message text and metadata values from remote peers can contain arbitrary
// characters including newlines, pipes and brackets. Without escaping, a
// crafted message could inject fake log lines (log injection / format
// injection). We escape on write and unescape on read.
// Escape message body: \ -> \\, \n -> \n literal, \r -> \r literal
char*
ff_escape_message(const char* text)
{
if (!text)
return g_strdup("");
GString* out = g_string_sized_new(strlen(text));
for (const char* p = text; *p; p++) {
switch (*p) {
case '\\':
g_string_append(out, "\\\\");
break;
case '\n':
g_string_append(out, "\\n");
break;
case '\r':
g_string_append(out, "\\r");
break;
default:
g_string_append_c(out, *p);
break;
}
}
return g_string_free(out, FALSE);
}
// Unescape message body: \\ -> \, \n -> newline, \r -> CR
char*
ff_unescape_message(const char* text)
{
if (!text)
return g_strdup("");
GString* out = g_string_sized_new(strlen(text));
for (const char* p = text; *p; p++) {
if (*p == '\\' && *(p + 1)) {
p++;
switch (*p) {
case '\\':
g_string_append_c(out, '\\');
break;
case 'n':
g_string_append_c(out, '\n');
break;
case 'r':
g_string_append_c(out, '\r');
break;
default:
g_string_append_c(out, '\\');
g_string_append_c(out, *p);
break;
}
} else {
g_string_append_c(out, *p);
}
}
return g_string_free(out, FALSE);
}
// Escape metadata value (stanza_id, archive_id, replace_id):
// these come from remote servers and may contain |, ], \, newlines.
char*
ff_escape_meta_value(const char* val)
{
if (!val || strlen(val) == 0)
return NULL;
GString* out = g_string_sized_new(strlen(val));
for (const char* p = val; *p; p++) {
switch (*p) {
case '|':
g_string_append(out, "\\|");
break;
case ']':
g_string_append(out, "\\]");
break;
case '\\':
g_string_append(out, "\\\\");
break;
case '\n':
g_string_append(out, "\\n");
break;
case '\r':
g_string_append(out, "\\r");
break;
default:
g_string_append_c(out, *p);
break;
}
}
return g_string_free(out, FALSE);
}
// Unescape metadata value
char*
ff_unescape_meta_value(const char* val)
{
if (!val)
return NULL;
GString* out = g_string_sized_new(strlen(val));
for (const char* p = val; *p; p++) {
if (*p == '\\' && *(p + 1)) {
p++;
switch (*p) {
case '|':
g_string_append_c(out, '|');
break;
case ']':
g_string_append_c(out, ']');
break;
case '\\':
g_string_append_c(out, '\\');
break;
case 'n':
g_string_append_c(out, '\n');
break;
case 'r':
g_string_append_c(out, '\r');
break;
default:
g_string_append_c(out, '\\');
g_string_append_c(out, *p);
break;
}
} else {
g_string_append_c(out, *p);
}
}
return g_string_free(out, FALSE);
}
// =========================================================================
// Readline helper
// =========================================================================
//
// POSIX getline() for dynamic-length lines. Returns line without trailing
// newline, or NULL on EOF. Sets *truncated=TRUE if the line had no trailing
// newline at EOF (partial write detection).
char*
ff_readline(FILE* fp, gboolean* truncated)
{
char* line = NULL;
size_t cap = 0;
ssize_t nread = getline(&line, &cap, fp);
if (nread == -1) {
free(line);
return NULL;
}
// Guard against pathological lines that could exhaust memory.
// getline() already allocated, so we free and skip if too long.
if (nread > FF_MAX_LINE_LEN) {
log_error("flatfile: line too long (%zd bytes), skipping", nread);
// Check newline termination before freeing
gboolean had_newline = (nread > 0 && line[nread - 1] == '\n');
free(line);
// Skip to next newline if the overlength line wasn't newline-terminated
if (!had_newline) {
int ch;
while ((ch = fgetc(fp)) != EOF && ch != '\n') {}
}
if (truncated)
*truncated = FALSE;
// Return empty string so caller's loop continues (parse will reject it)
return g_strdup("");
}
if (truncated)
*truncated = FALSE;
if (nread > 0 && line[nread - 1] == '\n') {
line[--nread] = '\0';
} else if (feof(fp)) {
// Line without trailing newline at EOF — likely a partial write
if (truncated)
*truncated = TRUE;
}
return line;
}
// =========================================================================
// Line writer
// =========================================================================
//
// Format: {ISO8601} [{type}|{enc}|id:{stanza_id}|aid:{archive_id}|corrects:{replace_id}] {from_jid}/{resource}: {message}
//
// Message text is escaped: \\ -> \\\\, newline -> \\n, CR -> \\r
// Metadata values are escaped: |, ], \\, newline, CR
// Lines starting with '#' are comments.
// Empty lines are skipped.
void
ff_write_line(FILE* fp, const char* timestamp, const char* type, const char* enc,
const char* stanza_id, const char* archive_id, const char* replace_id,
const char* from_jid, const char* from_resource, const char* message_text)
{
// Escape metadata values from remote peers
auto_gchar gchar* safe_sid = ff_escape_meta_value(stanza_id);
auto_gchar gchar* safe_aid = ff_escape_meta_value(archive_id);
auto_gchar gchar* safe_rid = ff_escape_meta_value(replace_id);
// Build metadata section: [type|enc|id:...|aid:...|corrects:...]
GString* meta = g_string_new("[");
g_string_append(meta, type ? type : "chat");
g_string_append_c(meta, '|');
g_string_append(meta, enc ? enc : "none");
if (safe_sid) {
g_string_append_printf(meta, "|id:%s", safe_sid);
}
if (safe_aid) {
g_string_append_printf(meta, "|aid:%s", safe_aid);
}
if (safe_rid) {
g_string_append_printf(meta, "|corrects:%s", safe_rid);
}
g_string_append_c(meta, ']');
// Build sender — escape ": " in the resource part to prevent
// the parser from splitting at the wrong point.
GString* sender = g_string_new(from_jid ? from_jid : "unknown");
if (from_resource && strlen(from_resource) > 0) {
// Escape backslash and colon-space in resource
GString* safe_res = g_string_sized_new(strlen(from_resource));
for (const char* p = from_resource; *p; p++) {
if (*p == '\\') {
g_string_append(safe_res, "\\\\");
} else if (*p == ':' && *(p + 1) == ' ') {
g_string_append(safe_res, "\\: ");
p++; // skip the space too
} else {
g_string_append_c(safe_res, *p);
}
}
g_string_append_printf(sender, "/%s", safe_res->str);
g_string_free(safe_res, TRUE);
}
// Escape message body to prevent log injection
char* safe_msg = ff_escape_message(message_text);
// Build complete line and write with a single fwrite()
GString* full_line = g_string_new(NULL);
g_string_printf(full_line, "%s %s %s: %s\n",
timestamp, meta->str, sender->str, safe_msg);
size_t to_write = full_line->len;
ssize_t written = fwrite(full_line->str, 1, to_write, fp);
if (written != (ssize_t)to_write) {
log_error("flatfile: partial write (%zd/%zu)", written, to_write);
}
g_string_free(full_line, TRUE);
g_free(safe_msg);
g_string_free(meta, TRUE);
g_string_free(sender, TRUE);
}
// =========================================================================
// Parser helpers
// =========================================================================
// Find the first occurrence of 'ch' that is not preceded by an unescaped backslash.
const char*
ff_find_unescaped_char(const char* str, char ch)
{
if (!str)
return NULL;
for (const char* p = str; *p; p++) {
if (*p == '\\' && *(p + 1)) {
p++; // skip escaped character
continue;
}
if (*p == ch)
return p;
}
return NULL;
}
// Split metadata content on unescaped '|'. Returns a NULL-terminated array.
// Caller must g_strfreev() the result.
char**
ff_split_meta(const char* meta)
{
GPtrArray* arr = g_ptr_array_new();
const char* start = meta;
for (const char* p = meta;; p++) {
if (*p == '\\' && *(p + 1)) {
p++; // skip escaped char
continue;
}
if (*p == '|' || *p == '\0') {
g_ptr_array_add(arr, g_strndup(start, p - start));
if (*p == '\0')
break;
start = p + 1;
}
}
g_ptr_array_add(arr, NULL);
return (char**)g_ptr_array_free(arr, FALSE);
}
// Find first unescaped ": " (colon-space) in a string.
const char*
ff_find_unescaped_colonspace(const char* str)
{
if (!str)
return NULL;
for (const char* p = str; *p; p++) {
if (*p == '\\' && *(p + 1)) {
p++; // skip escaped character
continue;
}
if (*p == ':' && *(p + 1) == ' ') {
return p;
}
}
return NULL;
}
// Unescape a sender resource: "\\" -> '\', "\: " -> ": "
char*
ff_unescape_sender_resource(const char* res)
{
if (!res)
return NULL;
GString* out = g_string_sized_new(strlen(res));
for (const char* p = res; *p; p++) {
if (*p == '\\' && *(p + 1)) {
p++;
if (*p == '\\') {
g_string_append_c(out, '\\');
} else if (*p == ':' && *(p + 1) == ' ') {
g_string_append(out, ": ");
p++; // skip the space
} else {
// Unknown escape — preserve literally
g_string_append_c(out, '\\');
g_string_append_c(out, *p);
}
} else {
g_string_append_c(out, *p);
}
}
return g_string_free(out, FALSE);
}
// =========================================================================
// Line parser
// =========================================================================
void
ff_parsed_line_free(ff_parsed_line_t* pl)
{
if (!pl)
return;
g_free(pl->timestamp_str);
if (pl->timestamp)
g_date_time_unref(pl->timestamp);
g_free(pl->type);
g_free(pl->enc);
g_free(pl->stanza_id);
g_free(pl->archive_id);
g_free(pl->replace_id);
g_free(pl->from_jid);
g_free(pl->from_resource);
g_free(pl->message);
g_free(pl);
}
// Parse a single line. Returns NULL on parse failure.
// Line format: {timestamp} [{metadata}] {sender}: {message}
ff_parsed_line_t*
ff_parse_line(const char* line)
{
if (!line || line[0] == '\0' || line[0] == '#') {
return NULL;
}
// Strip trailing \r if present (CRLF handling)
char* work = g_strdup(line);
gsize len = strlen(work);
if (len > 0 && work[len - 1] == '\r') {
work[len - 1] = '\0';
len--;
}
if (len == 0) {
g_free(work);
return NULL;
}
// UTF-8 validation
const gchar* end;
if (!g_utf8_validate(work, -1, &end)) {
log_warning("flatfile: invalid UTF-8 at byte offset %ld", (long)(end - work));
// Attempt Latin-1 fallback
gsize br, bw;
GError* err = NULL;
char* converted = g_convert(work, -1, "UTF-8", "ISO-8859-1", &br, &bw, &err);
if (converted) {
g_free(work);
work = converted;
} else {
if (err)
g_error_free(err);
g_free(work);
return NULL;
}
}
ff_parsed_line_t* result = g_malloc0(sizeof(ff_parsed_line_t));
// Parse timestamp — everything up to first space followed by '['
char* bracket_start = strchr(work, '[');
char* first_space = strchr(work, ' ');
if (bracket_start && first_space && first_space < bracket_start) {
// Standard format with metadata: {timestamp} [{meta}] {sender}: {msg}
result->timestamp_str = g_strndup(work, first_space - work);
// Parse metadata section [...]
const char* bracket_end = ff_find_unescaped_char(bracket_start + 1, ']');
if (bracket_end) {
char* meta_content = g_strndup(bracket_start + 1, bracket_end - bracket_start - 1);
// Split by unescaped '|'
char** parts = ff_split_meta(meta_content);
if (parts) {
int i = 0;
for (; parts[i]; i++) {
if (i == 0) {
result->type = g_strdup(parts[i]);
} else if (i == 1) {
result->enc = g_strdup(parts[i]);
} else if (g_str_has_prefix(parts[i], "id:")) {
result->stanza_id = ff_unescape_meta_value(parts[i] + 3);
} else if (g_str_has_prefix(parts[i], "aid:")) {
result->archive_id = ff_unescape_meta_value(parts[i] + 4);
} else if (g_str_has_prefix(parts[i], "corrects:")) {
result->replace_id = ff_unescape_meta_value(parts[i] + 9);
}
}
g_strfreev(parts);
}
g_free(meta_content);
// Parse sender: message after '] '
const char* after_meta = bracket_end + 1;
if (*after_meta == ' ')
after_meta++;
// Find first *unescaped* ': ' which separates sender from message.
const char* colon = ff_find_unescaped_colonspace(after_meta);
if (colon) {
char* raw_sender = g_strndup(after_meta, colon - after_meta);
// Split sender into jid/resource, then unescape resource
char* slash = strchr(raw_sender, '/');
if (slash) {
result->from_jid = g_strndup(raw_sender, slash - raw_sender);
result->from_resource = ff_unescape_sender_resource(slash + 1);
} else {
result->from_jid = g_strdup(raw_sender);
}
g_free(raw_sender);
result->message = ff_unescape_message(colon + 2);
} else {
// No ': ' found, treat entire rest as message with unknown sender
result->from_jid = g_strdup("unknown");
result->message = ff_unescape_message(after_meta);
}
} else {
// No closing bracket — malformed metadata
ff_parsed_line_free(result);
g_free(work);
return NULL;
}
} else if (first_space) {
// Legacy/simple format without metadata: {timestamp} - {sender}: {msg}
result->timestamp_str = g_strndup(work, first_space - work);
result->type = g_strdup("chat");
result->enc = g_strdup("none");
char* rest = first_space + 1;
// Skip " - " if present (chatlog.c format)
if (g_str_has_prefix(rest, "- ")) {
rest += 2;
}
char* colon = strstr(rest, ": ");
if (colon) {
char* sender = g_strndup(rest, colon - rest);
char* slash = strchr(sender, '/');
if (slash) {
result->from_jid = g_strndup(sender, slash - sender);
result->from_resource = g_strdup(slash + 1);
} else {
result->from_jid = g_strdup(sender);
}
g_free(sender);
result->message = ff_unescape_message(colon + 2);
} else {
result->from_jid = g_strdup("unknown");
result->message = ff_unescape_message(rest);
}
} else {
// No space at all — can't parse
ff_parsed_line_free(result);
g_free(work);
return NULL;
}
// Parse timestamp
result->timestamp = g_date_time_new_from_iso8601(result->timestamp_str, NULL);
if (!result->timestamp) {
log_warning("flatfile: unparsable timestamp: %s", result->timestamp_str);
ff_parsed_line_free(result);
g_free(work);
return NULL;
}
// Default type/enc if missing
if (!result->type)
result->type = g_strdup("chat");
if (!result->enc)
result->enc = g_strdup("none");
g_free(work);
return result;
}
// Convert parsed line to ProfMessage
ProfMessage*
ff_parsed_to_profmessage(ff_parsed_line_t* pl)
{
ProfMessage* msg = message_init();
msg->id = pl->stanza_id ? g_strdup(pl->stanza_id) : NULL;
msg->from_jid = jid_create_from_bare_and_resource(pl->from_jid, pl->from_resource);
msg->plain = g_strdup(pl->message ? pl->message : "");
msg->timestamp = g_date_time_ref(pl->timestamp);
msg->type = ff_get_message_type_type(pl->type);
msg->enc = ff_get_message_enc_type(pl->enc);
return msg;
}

View File

@@ -1,378 +0,0 @@
/*
* database_flatfile_verify.c
* vim: expandtab:ts=4:sts=4:sw=4
*
* Copyright (C) 2026 Profanity Contributors
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* Flat-file backend: integrity verification (/history verify).
* Checks: parsability, timestamp ordering, duplicate IDs, broken LMC
* references, file permissions, BOM, CRLF, UTF-8, control chars.
*/
#include "config.h"
#include <sys/stat.h>
#include <glib.h>
#include <glib/gstdio.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "log.h"
#include "config/files.h"
#include "database_flatfile.h"
GSList*
ff_verify_integrity(const gchar* const contact_barejid)
{
GSList* issues = NULL;
if (!g_flatfile_account_jid) {
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
issue->level = INTEGRITY_ERROR;
issue->file = g_strdup("N/A");
issue->line = 0;
issue->message = g_strdup("Flat-file backend not initialized");
issues = g_slist_append(issues, issue);
return issues;
}
// If contact specified, verify just that contact; otherwise discover all contacts
GSList* contact_dirs = NULL;
if (contact_barejid) {
auto_gchar gchar* cdir = ff_get_contact_dir(contact_barejid);
if (cdir && g_file_test(cdir, G_FILE_TEST_IS_DIR)) {
contact_dirs = g_slist_append(contact_dirs, g_strdup(cdir));
} else {
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
issue->level = INTEGRITY_INFO;
issue->file = g_strdup(contact_barejid);
issue->line = 0;
issue->message = g_strdup("No log files found for this contact");
issues = g_slist_append(issues, issue);
return issues;
}
} else {
// Discover all contact directories
auto_gchar gchar* data_path = files_get_data_path(DIR_FLATLOG);
auto_gchar gchar* my_dir = ff_jid_to_dir(g_flatfile_account_jid);
auto_gchar gchar* base_dir = g_strdup_printf("%s/%s", data_path, my_dir);
GDir* dir = g_dir_open(base_dir, 0, NULL);
if (dir) {
const gchar* dname;
while ((dname = g_dir_read_name(dir)) != NULL) {
char* full = g_strdup_printf("%s/%s", base_dir, dname);
if (g_file_test(full, G_FILE_TEST_IS_DIR)) {
contact_dirs = g_slist_append(contact_dirs, full);
} else {
g_free(full);
}
}
g_dir_close(dir);
}
}
// Verify each contact directory
for (GSList* cd = contact_dirs; cd; cd = cd->next) {
const char* cdir_path = cd->data;
GDir* dir = g_dir_open(cdir_path, 0, NULL);
if (!dir)
continue;
GSList* log_files = NULL;
const gchar* fname;
while ((fname = g_dir_read_name(dir)) != NULL) {
if (g_str_has_suffix(fname, ".log")) {
log_files = g_slist_insert_sorted(log_files,
g_strdup_printf("%s/%s", cdir_path, fname),
(GCompareFunc)g_strcmp0);
}
}
g_dir_close(dir);
GDateTime* prev_file_last_ts = NULL;
GHashTable* seen_ids = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
GHashTable* all_stanza_ids = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
for (GSList* lf = log_files; lf; lf = lf->next) {
const char* filepath = lf->data;
const char* basename = strrchr(filepath, '/');
basename = basename ? basename + 1 : filepath;
// Check file permissions
struct stat st;
if (g_stat(filepath, &st) == 0) {
if ((st.st_mode & 0777) != (S_IRUSR | S_IWUSR)) {
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
issue->level = INTEGRITY_WARNING;
issue->file = g_strdup(basename);
issue->line = 0;
issue->message = g_strdup_printf("File permissions are %o, expected 600 (sensitive data)", st.st_mode & 0777);
issues = g_slist_append(issues, issue);
}
}
FILE* fp = fopen(filepath, "r");
if (!fp)
continue;
// BOM check
int c1 = fgetc(fp);
int c2 = fgetc(fp);
int c3 = fgetc(fp);
if (c1 == 0xEF && c2 == 0xBB && c3 == 0xBF) {
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
issue->level = INTEGRITY_INFO;
issue->file = g_strdup(basename);
issue->line = 0;
issue->message = g_strdup("File has UTF-8 BOM — harmless but unnecessary");
issues = g_slist_append(issues, issue);
} else {
fseek(fp, 0, SEEK_SET);
}
char* buf = NULL;
int lineno = 0;
GDateTime* prev_ts = NULL;
GDateTime* first_ts = NULL;
GDateTime* last_ts = NULL;
gboolean has_crlf = FALSE;
gboolean is_empty = TRUE;
while ((buf = ff_readline(fp, NULL)) != NULL) {
lineno++;
gsize len = strlen(buf);
// CRLF check
if (len > 0 && buf[len - 1] == '\r') {
has_crlf = TRUE;
buf[--len] = '\0';
}
// Skip empty lines and comments
if (len == 0 || buf[0] == '#') {
free(buf);
continue;
}
is_empty = FALSE;
// UTF-8 validation
const gchar* end;
if (!g_utf8_validate(buf, -1, &end)) {
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
issue->level = INTEGRITY_ERROR;
issue->file = g_strdup(basename);
issue->line = lineno;
issue->message = g_strdup_printf("Invalid UTF-8 at byte offset %ld", (long)(end - buf));
issues = g_slist_append(issues, issue);
free(buf);
continue;
}
// Control character check
for (gsize i = 0; i < len; i++) {
unsigned char ch = (unsigned char)buf[i];
if (ch < 0x20 && ch != '\t') {
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
issue->level = INTEGRITY_WARNING;
issue->file = g_strdup(basename);
issue->line = lineno;
issue->message = g_strdup_printf("Contains control character 0x%02x", ch);
issues = g_slist_append(issues, issue);
break;
}
}
// Parse line
ff_parsed_line_t* pl = ff_parse_line(buf);
if (!pl) {
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
issue->level = INTEGRITY_ERROR;
issue->file = g_strdup(basename);
issue->line = lineno;
issue->message = g_strdup("Unparsable line");
issues = g_slist_append(issues, issue);
free(buf);
continue;
}
free(buf); // done with raw line
if (!first_ts)
first_ts = g_date_time_ref(pl->timestamp);
if (last_ts)
g_date_time_unref(last_ts);
last_ts = g_date_time_ref(pl->timestamp);
// Timestamp order within file
if (prev_ts && g_date_time_compare(pl->timestamp, prev_ts) < 0) {
auto_gchar gchar* ts_cur = g_date_time_format_iso8601(pl->timestamp);
auto_gchar gchar* ts_prev = g_date_time_format_iso8601(prev_ts);
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
issue->level = INTEGRITY_WARNING;
issue->file = g_strdup(basename);
issue->line = lineno;
issue->message = g_strdup_printf("Timestamp out of order (%s after %s)", ts_cur, ts_prev);
issues = g_slist_append(issues, issue);
}
if (prev_ts)
g_date_time_unref(prev_ts);
prev_ts = g_date_time_ref(pl->timestamp);
// Duplicate stanza-id / archive-id
if (pl->stanza_id && strlen(pl->stanza_id) > 0) {
if (g_hash_table_contains(seen_ids, pl->stanza_id)) {
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
issue->level = INTEGRITY_WARNING;
issue->file = g_strdup(basename);
issue->line = lineno;
issue->message = g_strdup_printf("Duplicate stanza-id \"%s\"", pl->stanza_id);
issues = g_slist_append(issues, issue);
} else {
g_hash_table_insert(seen_ids, g_strdup(pl->stanza_id), GINT_TO_POINTER(lineno));
}
g_hash_table_insert(all_stanza_ids, g_strdup(pl->stanza_id), GINT_TO_POINTER(lineno));
}
if (pl->archive_id && strlen(pl->archive_id) > 0) {
if (g_hash_table_contains(seen_ids, pl->archive_id)) {
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
issue->level = INTEGRITY_WARNING;
issue->file = g_strdup(basename);
issue->line = lineno;
issue->message = g_strdup_printf("Duplicate archive-id \"%s\"", pl->archive_id);
issues = g_slist_append(issues, issue);
} else {
g_hash_table_insert(seen_ids, g_strdup(pl->archive_id), GINT_TO_POINTER(lineno));
}
}
ff_parsed_line_free(pl);
}
fclose(fp);
// CRLF warning for file
if (has_crlf) {
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
issue->level = INTEGRITY_WARNING;
issue->file = g_strdup(basename);
issue->line = 0;
issue->message = g_strdup("File uses Windows line endings (CRLF) — consider converting to LF");
issues = g_slist_append(issues, issue);
}
// Empty file
if (is_empty) {
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
issue->level = INTEGRITY_INFO;
issue->file = g_strdup(basename);
issue->line = 0;
issue->message = g_strdup("File is empty (no message lines)");
issues = g_slist_append(issues, issue);
}
// Cross-file timestamp ordering
if (first_ts && prev_file_last_ts) {
if (g_date_time_compare(first_ts, prev_file_last_ts) < 0) {
auto_gchar gchar* ts_first = g_date_time_format_iso8601(first_ts);
auto_gchar gchar* ts_prev_last = g_date_time_format_iso8601(prev_file_last_ts);
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
issue->level = INTEGRITY_WARNING;
issue->file = g_strdup(basename);
issue->line = 0;
issue->message = g_strdup_printf("First timestamp (%s) is before previous file's last timestamp (%s)",
ts_first, ts_prev_last);
issues = g_slist_append(issues, issue);
}
}
if (prev_file_last_ts)
g_date_time_unref(prev_file_last_ts);
prev_file_last_ts = last_ts ? g_date_time_ref(last_ts) : NULL;
if (prev_ts)
g_date_time_unref(prev_ts);
if (first_ts)
g_date_time_unref(first_ts);
if (last_ts)
g_date_time_unref(last_ts);
}
// Second pass: check LMC references across all files
for (GSList* lf = log_files; lf; lf = lf->next) {
const char* filepath = lf->data;
const char* basename_lmc = strrchr(filepath, '/');
basename_lmc = basename_lmc ? basename_lmc + 1 : filepath;
FILE* fp = fopen(filepath, "r");
if (!fp)
continue;
// Skip BOM
int b1 = fgetc(fp);
int b2 = fgetc(fp);
int b3 = fgetc(fp);
if (!(b1 == 0xEF && b2 == 0xBB && b3 == 0xBF)) {
fseek(fp, 0, SEEK_SET);
}
char* buf = NULL;
int lineno = 0;
while ((buf = ff_readline(fp, NULL)) != NULL) {
lineno++;
gsize len = strlen(buf);
if (len > 0 && buf[len - 1] == '\r')
buf[--len] = '\0';
if (len == 0 || buf[0] == '#') {
free(buf);
continue;
}
ff_parsed_line_t* pl = ff_parse_line(buf);
free(buf);
if (!pl)
continue;
if (pl->replace_id && strlen(pl->replace_id) > 0) {
if (!g_hash_table_contains(all_stanza_ids, pl->replace_id)) {
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
issue->level = INTEGRITY_ERROR;
issue->file = g_strdup(basename_lmc);
issue->line = lineno;
issue->message = g_strdup_printf("Broken correction reference: corrects:%s not found", pl->replace_id);
issues = g_slist_append(issues, issue);
}
}
ff_parsed_line_free(pl);
}
fclose(fp);
}
if (prev_file_last_ts)
g_date_time_unref(prev_file_last_ts);
g_hash_table_destroy(seen_ids);
g_hash_table_destroy(all_stanza_ids);
g_slist_free_full(log_files, g_free);
}
g_slist_free_full(contact_dirs, g_free);
return issues;
}

View File

@@ -1,819 +0,0 @@
/*
* database_sqlite.c
* vim: expandtab:ts=4:sts=4:sw=4
*
* Copyright (C) 2020 - 2025 Michael Vetter <jubalh@iodoru.org>
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
*
*/
#include "config.h"
#include <sys/stat.h>
#include <sys/statvfs.h>
#include <sqlite3.h>
#include <glib.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include "log.h"
#include "common.h"
#include "config/files.h"
#include "database.h"
#include "config/preferences.h"
#include "ui/ui.h"
#include "xmpp/xmpp.h"
#include "xmpp/message.h"
static sqlite3* g_chatlog_database;
static void _add_to_db(ProfMessage* message, char* type, const Jid* const from_jid, const Jid* const to_jid);
static char* _get_db_filename(ProfAccount* account);
static prof_msg_type_t _get_message_type_type(const char* const type);
static prof_enc_t _get_message_enc_type(const char* const encstr);
static int _get_db_version(void);
static gboolean _migrate_to_v2(void);
static gboolean _check_available_space_for_db_migration(char* path_to_db);
static const int latest_version = 2;
// Helper: close DB handle (if any), warn on busy, and shutdown SQLite
static void
_db_teardown(const char* ctx)
{
if (g_chatlog_database) {
int rc = sqlite3_close_v2(g_chatlog_database);
if (rc != SQLITE_OK) {
log_warning("sqlite3_close_v2 in %s returned %d; database may still have active statements.",
ctx ? ctx : "db_teardown", rc);
}
g_chatlog_database = NULL;
}
sqlite3_shutdown();
}
// Helper: prepare a statement and log a contextual error on failure
static gboolean
_db_prepare_ctx(const char* query, sqlite3_stmt** stmt, const char* ctx)
{
int rc = sqlite3_prepare_v2(g_chatlog_database, query, -1, stmt, NULL);
if (rc != SQLITE_OK) {
log_error("SQLite error in %s: (error code: %d) %s",
ctx ? ctx : "sqlite3_prepare_v2",
rc,
sqlite3_errmsg(g_chatlog_database));
return FALSE;
}
return TRUE;
}
static char*
_db_strdup(const char* str)
{
return str ? strdup(str) : NULL;
}
#define auto_sqlite __attribute__((__cleanup__(auto_free_sqlite)))
static void
auto_free_sqlite(gchar** str)
{
if (str == NULL)
return;
sqlite3_free(*str);
}
static char*
_get_db_filename(ProfAccount* account)
{
return files_file_in_account_data_path(DIR_DATABASE, account->jid, "chatlog.db");
}
static gboolean
_sqlite_init(ProfAccount* account)
{
int ret = sqlite3_initialize();
if (ret != SQLITE_OK) {
log_error("Error initializing SQLite database: %d", ret);
return FALSE;
}
auto_char char* filename = _get_db_filename(account);
if (!filename) {
sqlite3_shutdown();
return FALSE;
}
ret = sqlite3_open(filename, &g_chatlog_database);
if (ret != SQLITE_OK) {
const char* err_msg = g_chatlog_database ? sqlite3_errmsg(g_chatlog_database) : "(no handle)";
log_error("Error opening SQLite database: %s", err_msg);
_db_teardown("_sqlite_init(open)");
return FALSE;
}
char* err_msg = NULL;
int db_version = _get_db_version();
if (db_version == latest_version) {
return TRUE;
}
char* query = "CREATE TABLE IF NOT EXISTS `ChatLogs` ("
"`id` INTEGER PRIMARY KEY AUTOINCREMENT, "
"`from_jid` TEXT NOT NULL, "
"`to_jid` TEXT NOT NULL, "
"`from_resource` TEXT, "
"`to_resource` TEXT, "
"`message` TEXT, "
"`timestamp` TEXT, "
"`type` TEXT, "
"`stanza_id` TEXT, "
"`archive_id` TEXT, "
"`encryption` TEXT, "
"`marked_read` INTEGER, "
"`replace_id` TEXT, "
"`replaces_db_id` INTEGER, "
"`replaced_by_db_id` INTEGER)";
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, query, NULL, 0, &err_msg)) {
goto out;
}
query = "CREATE TRIGGER IF NOT EXISTS update_corrected_message "
"AFTER INSERT ON ChatLogs "
"FOR EACH ROW "
"WHEN NEW.replaces_db_id IS NOT NULL "
"BEGIN "
"UPDATE ChatLogs "
"SET replaced_by_db_id = NEW.id "
"WHERE id = NEW.replaces_db_id; "
"END;";
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, query, NULL, 0, &err_msg)) {
log_error("Unable to add `update_corrected_message` trigger.");
goto out;
}
query = "CREATE INDEX IF NOT EXISTS ChatLogs_timestamp_IDX ON `ChatLogs` (`timestamp`)";
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, query, NULL, 0, &err_msg)) {
log_error("Unable to create index for timestamp.");
goto out;
}
query = "CREATE INDEX IF NOT EXISTS ChatLogs_to_from_jid_IDX ON `ChatLogs` (`to_jid`, `from_jid`)";
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, query, NULL, 0, &err_msg)) {
log_error("Unable to create index for to_jid.");
goto out;
}
query = "CREATE TABLE IF NOT EXISTS `DbVersion` (`dv_id` INTEGER PRIMARY KEY, `version` INTEGER UNIQUE)";
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, query, NULL, 0, &err_msg)) {
goto out;
}
if (db_version == -1) {
query = "INSERT OR IGNORE INTO `DbVersion` (`version`) VALUES ('2')";
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, query, NULL, 0, &err_msg)) {
goto out;
}
db_version = _get_db_version();
}
if (db_version == -1) {
cons_show_error("DB Initialization Error: Unable to check DB version.");
goto out;
}
if (db_version < latest_version) {
cons_show("Migrating database schema. This operation may take a while...");
if (db_version < 2 && (!_check_available_space_for_db_migration(filename) || !_migrate_to_v2())) {
cons_show_error("Database Initialization Error: Unable to migrate database to version 2. Please, check error logs for details.");
goto out;
}
cons_show("Database schema migration was successful.");
}
log_debug("Initialized SQLite database: %s", filename);
return TRUE;
out:
if (err_msg) {
log_error("SQLite error in _sqlite_init(): %s", err_msg);
sqlite3_free(err_msg);
} else {
log_error("Unknown SQLite error in _sqlite_init().");
}
_db_teardown("_sqlite_init(out)");
return FALSE;
}
static void
_sqlite_close(void)
{
log_debug("_sqlite_close() called");
_db_teardown("_sqlite_close");
}
static void
_sqlite_add_incoming(ProfMessage* message)
{
if (message->to_jid) {
_add_to_db(message, NULL, message->from_jid, message->to_jid);
} else {
_add_to_db(message, NULL, message->from_jid, connection_get_jid());
}
}
static void
_log_database_add_outgoing(char* type, const char* const id, const char* const barejid, const char* const message, const char* const replace_id, prof_enc_t enc)
{
ProfMessage* msg = message_init();
msg->id = _db_strdup(id);
msg->from_jid = jid_create(barejid);
msg->plain = _db_strdup(message);
msg->replace_id = _db_strdup(replace_id);
msg->timestamp = g_date_time_new_now_local();
msg->enc = enc;
_add_to_db(msg, type, connection_get_jid(), msg->from_jid);
message_free(msg);
}
static void
_sqlite_add_outgoing_chat(const char* const id, const char* const barejid, const char* const message, const char* const replace_id, prof_enc_t enc)
{
_log_database_add_outgoing("chat", id, barejid, message, replace_id, enc);
}
static void
_sqlite_add_outgoing_muc(const char* const id, const char* const barejid, const char* const message, const char* const replace_id, prof_enc_t enc)
{
_log_database_add_outgoing("muc", id, barejid, message, replace_id, enc);
}
static void
_sqlite_add_outgoing_muc_pm(const char* const id, const char* const barejid, const char* const message, const char* const replace_id, prof_enc_t enc)
{
_log_database_add_outgoing("mucpm", id, barejid, message, replace_id, enc);
}
static ProfMessage*
_sqlite_get_limits_info(const gchar* const contact_barejid, gboolean is_last)
{
sqlite3_stmt* stmt = NULL;
const Jid* myjid = connection_get_jid();
ProfMessage* msg = message_init();
if (!myjid || !myjid->str) {
if (is_last) {
msg->timestamp = g_date_time_new_now_utc();
}
return msg;
}
const char* order = is_last ? "DESC" : "ASC";
auto_sqlite char* query = sqlite3_mprintf("SELECT `archive_id`, `timestamp` FROM `ChatLogs` WHERE "
"(`from_jid` = %Q AND `to_jid` = %Q) OR "
"(`from_jid` = %Q AND `to_jid` = %Q) "
"ORDER BY `timestamp` %s LIMIT 1;",
contact_barejid, myjid->barejid, myjid->barejid, contact_barejid, order);
if (!query) {
log_error("Could not allocate memory for SQL query in _sqlite_get_limits_info()");
if (is_last) {
msg->timestamp = g_date_time_new_now_utc();
}
return msg;
}
if (!_db_prepare_ctx(query, &stmt, "_sqlite_get_limits_info()")) {
if (is_last) {
msg->timestamp = g_date_time_new_now_utc();
}
return msg;
}
if (sqlite3_step(stmt) == SQLITE_ROW) {
char* archive_id = (char*)sqlite3_column_text(stmt, 0);
char* date = (char*)sqlite3_column_text(stmt, 1);
msg->stanzaid = _db_strdup(archive_id);
msg->timestamp = g_date_time_new_from_iso8601(date, NULL);
}
sqlite3_finalize(stmt);
if (!msg->timestamp && is_last) {
msg->timestamp = g_date_time_new_now_utc();
}
return msg;
}
static db_history_result_t
_sqlite_get_previous_chat(const gchar* const contact_barejid, const gchar* start_time, const gchar* end_time, gboolean from_start, gboolean flip, GSList** result)
{
if (!g_chatlog_database) {
log_warning("_sqlite_get_previous_chat() called but db is not initialized");
return DB_RESPONSE_ERROR;
}
sqlite3_stmt* stmt = NULL;
const Jid* myjid = connection_get_jid();
if (!myjid->str) {
log_warning("_sqlite_get_previous_chat() called but no connection detected.");
return DB_RESPONSE_ERROR;
}
gchar* sort1 = from_start ? "ASC" : "DESC";
gchar* sort2 = !flip ? "ASC" : "DESC";
GDateTime* now = g_date_time_new_now_local();
auto_gchar gchar* end_date_fmt = end_time ? g_strdup(end_time) : g_date_time_format_iso8601(now);
auto_sqlite gchar* query = sqlite3_mprintf("SELECT * FROM ("
"SELECT COALESCE(B.`message`, A.`message`) AS message, "
"A.`timestamp`, A.`from_jid`, A.`from_resource`, A.`to_jid`, A.`to_resource`, A.`type`, A.`encryption`, A.`stanza_id` FROM `ChatLogs` AS A "
"LEFT JOIN `ChatLogs` AS B ON (A.`replaced_by_db_id` = B.`id` AND A.`from_jid` = B.`from_jid`) "
"WHERE (A.`replaces_db_id` IS NULL) "
"AND ((A.`from_jid` = %Q AND A.`to_jid` = %Q) OR (A.`from_jid` = %Q AND A.`to_jid` = %Q)) "
"AND A.`timestamp` < %Q "
"AND (%Q IS NULL OR A.`timestamp` > %Q) "
"ORDER BY A.`timestamp` %s LIMIT %d) "
"ORDER BY `timestamp` %s;",
contact_barejid, myjid->barejid, myjid->barejid, contact_barejid, end_date_fmt, start_time, start_time, sort1, MESSAGES_TO_RETRIEVE, sort2);
g_date_time_unref(now);
if (!query) {
log_error("Could not allocate memory.");
return DB_RESPONSE_ERROR;
}
if (!_db_prepare_ctx(query, &stmt, "_sqlite_get_previous_chat()")) {
return DB_RESPONSE_ERROR;
}
while (sqlite3_step(stmt) == SQLITE_ROW) {
char* message = (char*)sqlite3_column_text(stmt, 0);
char* date = (char*)sqlite3_column_text(stmt, 1);
char* from_jid = (char*)sqlite3_column_text(stmt, 2);
char* from_resource = (char*)sqlite3_column_text(stmt, 3);
char* to_jid = (char*)sqlite3_column_text(stmt, 4);
char* to_resource = (char*)sqlite3_column_text(stmt, 5);
char* type = (char*)sqlite3_column_text(stmt, 6);
char* encryption = (char*)sqlite3_column_text(stmt, 7);
char* id = (char*)sqlite3_column_text(stmt, 8);
ProfMessage* msg = message_init();
msg->id = id ? strdup(id) : NULL;
msg->from_jid = jid_create_from_bare_and_resource(from_jid, from_resource);
msg->to_jid = jid_create_from_bare_and_resource(to_jid, to_resource);
msg->plain = strdup(message ?: "");
msg->timestamp = g_date_time_new_from_iso8601(date, NULL);
msg->type = _get_message_type_type(type);
msg->enc = _get_message_enc_type(encryption);
*result = g_slist_append(*result, msg);
}
sqlite3_finalize(stmt);
return g_slist_length(*result) != 0 ? DB_RESPONSE_SUCCESS : DB_RESPONSE_EMPTY;
}
static GSList*
_sqlite_verify_integrity(const gchar* const contact_barejid)
{
GSList* issues = NULL;
if (!g_chatlog_database) {
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
issue->level = INTEGRITY_ERROR;
issue->file = g_strdup("chatlog.db");
issue->line = 0;
issue->message = g_strdup("Database not initialized");
issues = g_slist_append(issues, issue);
return issues;
}
// PRAGMA integrity_check
sqlite3_stmt* stmt = NULL;
if (_db_prepare_ctx("PRAGMA integrity_check", &stmt, "_sqlite_verify_integrity()")) {
while (sqlite3_step(stmt) == SQLITE_ROW) {
const char* result = (const char*)sqlite3_column_text(stmt, 0);
if (g_strcmp0(result, "ok") != 0) {
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
issue->level = INTEGRITY_ERROR;
issue->file = g_strdup("chatlog.db");
issue->line = 0;
issue->message = g_strdup_printf("SQLite integrity check: %s", result);
issues = g_slist_append(issues, issue);
}
}
sqlite3_finalize(stmt);
}
// Check timestamp ordering for a specific contact or all
const Jid* myjid = connection_get_jid();
if (myjid && myjid->barejid) {
auto_sqlite char* query = NULL;
if (contact_barejid) {
query = sqlite3_mprintf(
"SELECT A.`id`, A.`timestamp`, B.`id`, B.`timestamp` FROM `ChatLogs` A "
"JOIN `ChatLogs` B ON B.`id` = A.`id` + 1 "
"WHERE A.`timestamp` > B.`timestamp` "
"AND ((A.`from_jid` = %Q AND A.`to_jid` = %Q) OR (A.`from_jid` = %Q AND A.`to_jid` = %Q)) "
"LIMIT 50",
contact_barejid, myjid->barejid, myjid->barejid, contact_barejid);
} else {
query = sqlite3_mprintf(
"SELECT A.`id`, A.`timestamp`, B.`id`, B.`timestamp` FROM `ChatLogs` A "
"JOIN `ChatLogs` B ON B.`id` = A.`id` + 1 "
"WHERE A.`timestamp` > B.`timestamp` "
"LIMIT 50");
}
if (query && _db_prepare_ctx(query, &stmt, "_sqlite_verify_integrity(timestamp_order)")) {
while (sqlite3_step(stmt) == SQLITE_ROW) {
int id_a = sqlite3_column_int(stmt, 0);
const char* ts_a = (const char*)sqlite3_column_text(stmt, 1);
int id_b = sqlite3_column_int(stmt, 2);
const char* ts_b = (const char*)sqlite3_column_text(stmt, 3);
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
issue->level = INTEGRITY_WARNING;
issue->file = g_strdup("chatlog.db");
issue->line = id_a;
issue->message = g_strdup_printf("Timestamp out of order: row %d (%s) > row %d (%s)",
id_a, ts_a ? ts_a : "NULL", id_b, ts_b ? ts_b : "NULL");
issues = g_slist_append(issues, issue);
}
sqlite3_finalize(stmt);
}
// Check broken LMC references
auto_sqlite char* lmc_query = sqlite3_mprintf(
"SELECT A.`id`, A.`replaces_db_id` FROM `ChatLogs` A "
"WHERE A.`replaces_db_id` IS NOT NULL "
"AND NOT EXISTS (SELECT 1 FROM `ChatLogs` B WHERE B.`id` = A.`replaces_db_id`) "
"LIMIT 50");
if (lmc_query && _db_prepare_ctx(lmc_query, &stmt, "_sqlite_verify_integrity(lmc_check)")) {
while (sqlite3_step(stmt) == SQLITE_ROW) {
int id = sqlite3_column_int(stmt, 0);
int replaces_id = sqlite3_column_int(stmt, 1);
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
issue->level = INTEGRITY_ERROR;
issue->file = g_strdup("chatlog.db");
issue->line = id;
issue->message = g_strdup_printf("Broken LMC reference: row %d references non-existent row %d",
id, replaces_id);
issues = g_slist_append(issues, issue);
}
sqlite3_finalize(stmt);
}
}
return issues;
}
// --- Type conversion helpers ---
static const char*
_get_message_type_str(prof_msg_type_t type)
{
switch (type) {
case PROF_MSG_TYPE_CHAT:
return "chat";
case PROF_MSG_TYPE_MUC:
return "muc";
case PROF_MSG_TYPE_MUCPM:
return "mucpm";
case PROF_MSG_TYPE_UNINITIALIZED:
return NULL;
}
return NULL;
}
static prof_msg_type_t
_get_message_type_type(const char* const type)
{
if (g_strcmp0(type, "chat") == 0) {
return PROF_MSG_TYPE_CHAT;
} else if (g_strcmp0(type, "muc") == 0) {
return PROF_MSG_TYPE_MUC;
} else if (g_strcmp0(type, "mucpm") == 0) {
return PROF_MSG_TYPE_MUCPM;
} else {
return PROF_MSG_TYPE_UNINITIALIZED;
}
}
static const char*
_get_message_enc_str(prof_enc_t enc)
{
switch (enc) {
case PROF_MSG_ENC_OX:
return "ox";
case PROF_MSG_ENC_PGP:
return "pgp";
case PROF_MSG_ENC_OTR:
return "otr";
case PROF_MSG_ENC_OMEMO:
return "omemo";
case PROF_MSG_ENC_NONE:
return "none";
}
return "none";
}
static prof_enc_t
_get_message_enc_type(const char* const encstr)
{
if (g_strcmp0(encstr, "ox") == 0) {
return PROF_MSG_ENC_OX;
} else if (g_strcmp0(encstr, "pgp") == 0) {
return PROF_MSG_ENC_PGP;
} else if (g_strcmp0(encstr, "otr") == 0) {
return PROF_MSG_ENC_OTR;
} else if (g_strcmp0(encstr, "omemo") == 0) {
return PROF_MSG_ENC_OMEMO;
}
return PROF_MSG_ENC_NONE;
}
// --- Core write logic ---
static void
_add_to_db(ProfMessage* message, char* type, const Jid* const from_jid, const Jid* const to_jid)
{
auto_gchar gchar* pref_dblog = prefs_get_string(PREF_DBLOG);
sqlite_int64 original_message_id = -1;
if (g_strcmp0(pref_dblog, "off") == 0) {
return;
} else if (g_strcmp0(pref_dblog, "redact") == 0) {
if (message->plain) {
free(message->plain);
}
message->plain = strdup("[REDACTED]");
}
if (!g_chatlog_database) {
log_debug("_add_to_db() called but db is not initialized");
return;
}
char* err_msg;
auto_gchar gchar* date_fmt = NULL;
if (message->timestamp) {
date_fmt = g_date_time_format_iso8601(message->timestamp);
} else {
GDateTime* dt = g_date_time_new_now_local();
date_fmt = g_date_time_format_iso8601(dt);
g_date_time_unref(dt);
}
const char* enc = _get_message_enc_str(message->enc);
if (!type) {
type = (char*)_get_message_type_str(message->type);
}
// Apply LMC and check its validity (XEP-0308)
if (message->replace_id) {
auto_sqlite char* replace_check_query = sqlite3_mprintf("SELECT `id`, `from_jid`, `replaces_db_id` FROM `ChatLogs` WHERE `stanza_id` = %Q ORDER BY `timestamp` DESC LIMIT 1",
message->replace_id);
if (!replace_check_query) {
log_error("Could not allocate memory for SQL replace query in _add_to_db()");
return;
}
sqlite3_stmt* lmc_stmt = NULL;
if (!_db_prepare_ctx(replace_check_query, &lmc_stmt, "_add_to_db(replace_check)")) {
return;
}
if (sqlite3_step(lmc_stmt) == SQLITE_ROW) {
original_message_id = sqlite3_column_int64(lmc_stmt, 0);
const char* from_jid_orig = (const char*)sqlite3_column_text(lmc_stmt, 1);
sqlite_int64 tmp = sqlite3_column_int64(lmc_stmt, 2);
original_message_id = tmp ? tmp : original_message_id;
if (g_strcmp0(from_jid_orig, from_jid->barejid) != 0) {
log_error("Mismatch in sender JIDs when trying to do LMC. Corrected message sender: %s. Original message sender: %s. Replace-ID: %s. Message: %s", from_jid->barejid, from_jid_orig, message->replace_id, message->plain);
cons_show_error("%s sent a message correction with mismatched sender. See log for details.", from_jid->barejid);
sqlite3_finalize(lmc_stmt);
return;
}
} else {
log_warning("Got LMC message that does not have original message counterpart in the database from %s", message->from_jid->fulljid);
}
sqlite3_finalize(lmc_stmt);
}
if (message->stanzaid && !message->is_mam) {
auto_sqlite char* duplicate_check_query = sqlite3_mprintf("SELECT 1 FROM `ChatLogs` WHERE (`archive_id` = %Q)",
message->stanzaid);
if (!duplicate_check_query) {
log_error("Could not allocate memory for SQL duplicate query in _add_to_db()");
return;
}
sqlite3_stmt* stmt;
if (_db_prepare_ctx(duplicate_check_query, &stmt, "_add_to_db(duplicate_check)")) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
log_error("Duplicate stanza-id found for the message. stanza_id: %s; archive_id: %s; sender: %s; content: %s", message->id, message->stanzaid, from_jid->barejid, message->plain);
cons_show_error("Got a message with duplicate (server-generated) stanza-id from %s.", from_jid->fulljid);
}
sqlite3_finalize(stmt);
}
}
auto_sqlite char* orig_message_id = original_message_id == -1 ? NULL : sqlite3_mprintf("%d", original_message_id);
auto_sqlite char* query = sqlite3_mprintf("INSERT INTO `ChatLogs` "
"(`from_jid`, `from_resource`, `to_jid`, `to_resource`, "
"`message`, `timestamp`, `stanza_id`, `archive_id`, "
"`replaces_db_id`, `replace_id`, `type`, `encryption`) "
"VALUES (%Q, %Q, %Q, %Q, %Q, %Q, %Q, %Q, %Q, %Q, %Q, %Q)",
from_jid->barejid,
from_jid->resourcepart,
to_jid->barejid,
to_jid->resourcepart,
message->plain,
date_fmt,
message->id,
message->stanzaid,
orig_message_id,
message->replace_id,
type,
enc);
if (!query) {
log_error("Could not allocate memory for SQL insert query in _add_to_db()");
return;
}
log_debug("Writing to DB. Query: %s", query);
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, query, NULL, 0, &err_msg)) {
if (err_msg) {
log_error("SQLite error in _add_to_db(): %s", err_msg);
sqlite3_free(err_msg);
} else {
log_error("Unknown SQLite error in _add_to_db().");
}
} else {
int inserted_rows_count = sqlite3_changes(g_chatlog_database);
if (inserted_rows_count < 1) {
log_error("SQLite did not insert message (rows: %d, id: %s, content: %s)", inserted_rows_count, message->id, message->plain);
}
}
}
// --- DB version and migration ---
static int
_get_db_version(void)
{
int current_version = -1;
const char* query = "SELECT `version` FROM `DbVersion` LIMIT 1";
sqlite3_stmt* statement;
if (_db_prepare_ctx(query, &statement, "_get_db_version()")) {
if (sqlite3_step(statement) == SQLITE_ROW) {
current_version = sqlite3_column_int(statement, 0);
}
sqlite3_finalize(statement);
}
return current_version;
}
static gboolean
_migrate_to_v2(void)
{
char* err_msg = NULL;
const char* sql_statements[] = {
"BEGIN TRANSACTION",
"ALTER TABLE `ChatLogs` ADD COLUMN `replaces_db_id` INTEGER;",
"ALTER TABLE `ChatLogs` ADD COLUMN `replaced_by_db_id` INTEGER;",
"UPDATE `ChatLogs` AS A "
"SET `replaces_db_id` = B.`id` "
"FROM `ChatLogs` AS B "
"WHERE A.`replace_id` IS NOT NULL AND A.`replace_id` != '' "
"AND A.`replace_id` = B.`stanza_id` "
"AND A.`from_jid` = B.`from_jid` AND A.`to_jid` = B.`to_jid`;",
"UPDATE `ChatLogs` AS A "
"SET `replaced_by_db_id` = B.`id` "
"FROM `ChatLogs` AS B "
"WHERE (A.`replace_id` IS NULL OR A.`replace_id` = '') "
"AND A.`id` = B.`replaces_db_id` "
"AND A.`from_jid` = B.`from_jid`;",
"UPDATE ChatLogs SET "
"from_resource = COALESCE(NULLIF(from_resource, ''), NULL), "
"to_resource = COALESCE(NULLIF(to_resource, ''), NULL), "
"message = COALESCE(NULLIF(message, ''), NULL), "
"timestamp = COALESCE(NULLIF(timestamp, ''), NULL), "
"stanza_id = COALESCE(NULLIF(stanza_id, ''), NULL), "
"archive_id = COALESCE(NULLIF(archive_id, ''), NULL), "
"replace_id = COALESCE(NULLIF(replace_id, ''), NULL), "
"type = COALESCE(NULLIF(type, ''), NULL), "
"encryption = COALESCE(NULLIF(encryption, ''), NULL);",
"UPDATE `DbVersion` SET `version` = 2;",
"END TRANSACTION"
};
int statements_count = sizeof(sql_statements) / sizeof(sql_statements[0]);
for (int i = 0; i < statements_count; i++) {
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, sql_statements[i], NULL, 0, &err_msg)) {
log_error("SQLite error in _migrate_to_v2() on statement %d: %s", i, err_msg);
if (err_msg) {
sqlite3_free(err_msg);
err_msg = NULL;
}
goto cleanup;
}
}
return TRUE;
cleanup:
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, "ROLLBACK;", NULL, 0, &err_msg)) {
log_error("[DB Migration] Unable to ROLLBACK: %s", err_msg);
if (err_msg) {
sqlite3_free(err_msg);
}
}
return FALSE;
}
static gboolean
_check_available_space_for_db_migration(char* path_to_db)
{
struct stat file_stat;
struct statvfs fs_stat;
if (statvfs(path_to_db, &fs_stat) == 0 && stat(path_to_db, &file_stat) == 0) {
unsigned long long file_size = file_stat.st_size / 1024;
unsigned long long available_space_kb = fs_stat.f_frsize * fs_stat.f_bavail / 1024;
log_debug("_check_available_space_for_db_migration(): Available space on disk: %llu KB; DB size: %llu KB", available_space_kb, file_size);
return (available_space_kb >= (file_size + (file_size * 10 / 4)));
} else {
log_error("Error checking available space.");
return FALSE;
}
}
// --- Backend vtable ---
static db_backend_t sqlite_backend = {
.name = "sqlite",
.init = _sqlite_init,
.close = _sqlite_close,
.add_incoming = _sqlite_add_incoming,
.add_outgoing_chat = _sqlite_add_outgoing_chat,
.add_outgoing_muc = _sqlite_add_outgoing_muc,
.add_outgoing_muc_pm = _sqlite_add_outgoing_muc_pm,
.get_previous_chat = _sqlite_get_previous_chat,
.get_limits_info = _sqlite_get_limits_info,
.verify_integrity = _sqlite_verify_integrity,
};
db_backend_t*
db_backend_sqlite(void)
{
return &sqlite_backend;
}

View File

@@ -186,7 +186,7 @@ log_error(const char* const msg, ...)
}
void
log_init(log_level_t filter, const char* const log_file)
log_init(log_level_t filter, char* log_file)
{
level_filter = filter;

View File

@@ -50,7 +50,7 @@ typedef enum {
PROF_LEVEL_ERROR
} log_level_t;
void log_init(log_level_t filter, const char* const log_file);
void log_init(log_level_t filter, char* log_file);
log_level_t log_get_filter(void);
void log_close(void);
const gchar* get_log_file_location(void);

View File

@@ -532,13 +532,11 @@ omemo_set_device_list(const char* const from, GList* device_list)
for (device_id = device_list; device_id != NULL; device_id = device_id->next) {
GHashTable* known_identities = g_hash_table_lookup(omemo_ctx.known_devices, jid->barejid);
if (known_identities) {
GHashTableIter iter;
gpointer key, value;
g_hash_table_iter_init(&iter, known_identities);
while (g_hash_table_iter_next(&iter, &key, &value)) {
if (device_id->data == value) {
cons_show("OMEMO: Adding firstusage trust for %s device %d - Fingerprint %s", jid->barejid, device_id->data, omemo_format_fingerprint(key));
omemo_trust(jid->barejid, omemo_format_fingerprint(key));
GList* fp = NULL;
for (fp = g_hash_table_get_keys(known_identities); fp != NULL; fp = fp->next) {
if (device_id->data == g_hash_table_lookup(known_identities, fp->data)) {
cons_show("OMEMO: Adding firstusage trust for %s device %d - Fingerprint %s", jid->barejid, device_id->data, omemo_format_fingerprint(fp->data));
omemo_trust(jid->barejid, omemo_format_fingerprint(fp->data));
}
}
}

View File

@@ -345,13 +345,14 @@ p_gpg_list_keys(void)
// TODO: move autocomplete in other place
autocomplete_clear(key_ac);
GHashTableIter iter;
gpointer key, value;
g_hash_table_iter_init(&iter, result);
while (g_hash_table_iter_next(&iter, &key, &value)) {
ProfPGPKey* pgp_key = (ProfPGPKey*)value;
autocomplete_add(key_ac, pgp_key->id);
GList* ids = g_hash_table_get_keys(result);
GList* curr = ids;
while (curr) {
ProfPGPKey* key = g_hash_table_lookup(result, curr->data);
autocomplete_add(key_ac, key->id);
curr = curr->next;
}
g_list_free(ids);
return result;
}

View File

@@ -54,7 +54,6 @@
#include "plugins/settings.h"
#include "plugins/disco.h"
#include "ui/ui.h"
#include "ui/win_types.h"
#include "ui/window_list.h"
#include "xmpp/roster_list.h"
@@ -206,13 +205,6 @@ api_get_current_recipient(void)
}
}
gchar*
api_get_current_window(void)
{
ProfWin* current = wins_get_current();
return win_get_title(current);
}
char*
api_get_current_muc(void)
{

View File

@@ -46,7 +46,6 @@ void api_notify(const char* message, const char* category, int timeout_ms);
void api_send_line(char* line);
char* api_get_current_recipient(void);
gchar* api_get_current_window(void);
char* api_get_current_muc(void);
gboolean api_current_win_is_console(void);
char* api_get_current_nick(void);

View File

@@ -141,16 +141,18 @@ autocompleters_complete(const char* const input, gboolean previous)
while (curr_hash) {
GHashTable* key_to_ac = curr_hash->data;
GHashTableIter iter;
gpointer key, value;
g_hash_table_iter_init(&iter, key_to_ac);
while (g_hash_table_iter_next(&iter, &key, &value)) {
result = autocomplete_param_with_ac(input, key, value, TRUE, previous);
GList* keys = g_hash_table_get_keys(key_to_ac);
GList* curr = keys;
while (curr) {
result = autocomplete_param_with_ac(input, curr->data, g_hash_table_lookup(key_to_ac, curr->data), TRUE, previous);
if (result) {
g_list_free(ac_hashes);
g_list_free(keys);
return result;
}
curr = g_list_next(curr);
}
g_list_free(keys);
curr_hash = g_list_next(curr_hash);
}
@@ -160,19 +162,22 @@ autocompleters_complete(const char* const input, gboolean previous)
curr_hash = filepath_hashes;
while (curr_hash) {
GHashTable* prefixes_hash = curr_hash->data;
GHashTableIter iter;
gpointer key, value;
g_hash_table_iter_init(&iter, prefixes_hash);
while (g_hash_table_iter_next(&iter, &key, &value)) {
char* prefix = (char*)key;
GList* prefixes = g_hash_table_get_keys(prefixes_hash);
GList* curr_prefix = prefixes;
while (curr_prefix) {
char* prefix = curr_prefix->data;
if (g_str_has_prefix(input, prefix)) {
result = cmd_ac_complete_filepath(input, prefix, previous);
if (result) {
g_list_free(filepath_hashes);
g_list_free(prefixes);
return result;
}
}
curr_prefix = g_list_next(curr_prefix);
}
g_list_free(prefixes);
curr_hash = g_list_next(curr_hash);
}

View File

@@ -165,12 +165,6 @@ c_api_get_current_recipient(void)
return api_get_current_recipient();
}
static char*
c_api_get_current_window(void)
{
return api_get_current_window();
}
static char*
c_api_get_current_muc(void)
{
@@ -483,7 +477,6 @@ c_api_init(void)
prof_notify = c_api_notify;
prof_send_line = c_api_send_line;
prof_get_current_recipient = c_api_get_current_recipient;
prof_get_current_window = c_api_get_current_window;
prof_get_current_muc = c_api_get_current_muc;
prof_current_win_is_console = c_api_current_win_is_console;
prof_get_current_nick = c_api_get_current_nick;

View File

@@ -149,14 +149,15 @@ callbacks_remove(const char* const plugin_name)
{
GHashTable* command_hash = g_hash_table_lookup(p_commands, plugin_name);
if (command_hash) {
GHashTableIter iter;
gpointer key, value;
g_hash_table_iter_init(&iter, command_hash);
while (g_hash_table_iter_next(&iter, &key, &value)) {
char* command = (char*)key;
GList* commands = g_hash_table_get_keys(command_hash);
GList* curr = commands;
while (curr) {
char* command = curr->data;
cmd_ac_remove(command);
cmd_ac_remove_help(&command[1]);
curr = g_list_next(curr);
}
g_list_free(commands);
}
g_hash_table_remove(p_commands, plugin_name);
@@ -164,12 +165,13 @@ callbacks_remove(const char* const plugin_name)
GHashTable* tag_to_win_cb_hash = g_hash_table_lookup(p_window_callbacks, plugin_name);
if (tag_to_win_cb_hash) {
GHashTableIter iter;
gpointer key, value;
g_hash_table_iter_init(&iter, tag_to_win_cb_hash);
while (g_hash_table_iter_next(&iter, &key, &value)) {
wins_close_plugin(key);
GList* tags = g_hash_table_get_keys(tag_to_win_cb_hash);
GList* curr = tags;
while (curr) {
wins_close_plugin(curr->data);
curr = g_list_next(curr);
}
g_list_free(tags);
}
g_hash_table_remove(p_window_callbacks, plugin_name);

View File

@@ -107,11 +107,10 @@ disco_remove_features(const char* plugin_name)
return;
}
GHashTableIter iter;
gpointer key, value;
g_hash_table_iter_init(&iter, plugin_features_set);
while (g_hash_table_iter_next(&iter, &key, &value)) {
char* feature = (char*)key;
GList* plugin_feature_list = g_hash_table_get_keys(plugin_features_set);
GList* curr = plugin_feature_list;
while (curr) {
char* feature = curr->data;
if (g_hash_table_contains(features, feature)) {
void* refcountp = g_hash_table_lookup(features, feature);
int refcount = GPOINTER_TO_INT(refcountp);
@@ -122,7 +121,10 @@ disco_remove_features(const char* plugin_name)
g_hash_table_replace(features, strdup(feature), GINT_TO_POINTER(refcount));
}
}
curr = g_list_next(curr);
}
g_list_free(plugin_feature_list);
}
GList*

View File

@@ -62,7 +62,6 @@ void (*prof_notify)(const char* message, int timeout_ms, const char* category) =
void (*prof_send_line)(char* line) = NULL;
char* (*prof_get_current_recipient)(void) = NULL;
char* (*prof_get_current_window)(void) = NULL;
char* (*prof_get_current_muc)(void) = NULL;
int (*prof_current_win_is_console)(void) = NULL;
char* (*prof_get_current_nick)(void) = NULL;

View File

@@ -71,7 +71,6 @@ void (*prof_notify)(const char* message, int timeout_ms, const char* category);
void (*prof_send_line)(char* line);
char* (*prof_get_current_recipient)(void);
char* (*prof_get_current_window)(void);
char* (*prof_get_current_muc)(void);
int (*prof_current_win_is_console)(void);
char* (*prof_get_current_nick)(void);

View File

@@ -415,15 +415,6 @@ python_api_get_current_recipient(PyObject* self, PyObject* args)
}
}
static PyObject*
python_api_get_current_window(PyObject* self, PyObject* args)
{
allow_python_threads();
auto_gchar gchar* recipient = api_get_current_window();
disable_python_threads();
return Py_BuildValue("s", recipient);
}
static PyObject*
python_api_get_current_muc(PyObject* self, PyObject* args)
{
@@ -1539,7 +1530,6 @@ static PyMethodDef apiMethods[] = {
{ "send_line", python_api_send_line, METH_VARARGS, "Send a line of input." },
{ "notify", python_api_notify, METH_VARARGS, "Send desktop notification." },
{ "get_current_recipient", python_api_get_current_recipient, METH_VARARGS, "Return the jid of the recipient of the current window." },
{ "get_current_window", python_api_get_current_window, METH_VARARGS, "Return title of the current window." },
{ "get_current_muc", python_api_get_current_muc, METH_VARARGS, "Return the jid of the room of the current window." },
{ "get_current_nick", python_api_get_current_nick, METH_VARARGS, "Return nickname in current room." },
{ "get_name_from_roster", python_api_get_name_from_roster, METH_VARARGS, "Return nickname in roster of barejid." },

View File

@@ -887,8 +887,8 @@ _python_undefined_error(ProfPlugin* plugin, char* hook, char* type)
g_string_append(err_msg, hook);
g_string_append(err_msg, "(): return value undefined, expected ");
g_string_append(err_msg, type);
log_error("%s", err_msg->str);
cons_show_error("%s", err_msg->str);
log_error(err_msg->str);
cons_show_error(err_msg->str);
g_string_free(err_msg, TRUE);
}
@@ -901,8 +901,8 @@ _python_type_error(ProfPlugin* plugin, char* hook, char* type)
g_string_append(err_msg, hook);
g_string_append(err_msg, "(): incorrect return type, expected ");
g_string_append(err_msg, type);
log_error("%s", err_msg->str);
cons_show_error("%s", err_msg->str);
log_error(err_msg->str);
cons_show_error(err_msg->str);
g_string_free(err_msg, TRUE);
}

View File

@@ -135,7 +135,7 @@ prof_run(gchar* log_level, gchar* account_name, gchar* config_file, gchar* log_f
*/
min_runtime += waittime;
} else {
log_error("%s", err_msg);
log_error(err_msg);
g_free(err_msg);
commands = NULL;
}
@@ -245,7 +245,7 @@ _init(char* log_level, char* config_file, char* log_file, char* theme_name)
if (prof_log_level == PROF_LEVEL_DEBUG) {
ProfWin* console = wins_get_console();
win_println(console, THEME_DEFAULT, "-", "Debug mode enabled! Logging to: ");
win_println(console, THEME_DEFAULT, "-", "%s", get_log_file_location());
win_println(console, THEME_DEFAULT, "-", get_log_file_location());
}
session_init();
cmd_init();

View File

@@ -311,7 +311,7 @@ http_file_put(void* userdata)
}
win_update_entry_message(upload->window, upload->put_url, err_msg);
}
cons_show_error("%s", err_msg);
cons_show_error(err_msg);
} else {
if (!upload->cancel) {
auto_gchar gchar* status_msg = g_strdup_printf("Uploading '%s': 100%%", upload->filename);
@@ -327,7 +327,7 @@ http_file_put(void* userdata)
if (!fail_msg) {
fail_msg = g_strdup(FALLBACK_MSG);
}
cons_show_error("%s", fail_msg);
cons_show_error(fail_msg);
} else {
switch (upload->window->type) {
case WIN_CHAT:

View File

@@ -140,10 +140,6 @@ void
buffer_remove_entry(ProfBuff buffer, int entry)
{
GSList* node = g_slist_nth(buffer->entries, entry);
if (node == NULL) {
// Index out of range; nothing to remove
return;
}
ProfBuffEntry* e = node->data;
buffer->lines -= e->_lines;
_free_entry(e);
@@ -172,9 +168,6 @@ ProfBuffEntry*
buffer_get_entry(ProfBuff buffer, int entry)
{
GSList* node = g_slist_nth(buffer->entries, entry);
if (node == NULL) {
return NULL;
}
return node->data;
}
@@ -196,7 +189,6 @@ buffer_get_entry_by_id(ProfBuff buffer, const char* const id)
static ProfBuffEntry*
_create_entry(const char* show_char, int pad_indent, GDateTime* time, int flags, theme_item_t theme_item, const char* const display_from, const char* const from_jid, const char* const message, DeliveryReceipt* receipt, const char* const id, int y_start_pos, int y_end_pos)
{
assert(time != NULL);
ProfBuffEntry* e = malloc(sizeof(struct prof_buff_entry_t));
e->show_char = STRDUP_OR_NULL(show_char);
e->pad_indent = pad_indent;

View File

@@ -592,11 +592,8 @@ chatwin_db_history(ProfChatWin* chatwin, const gchar* start_time, const gchar* e
{
auto_gchar gchar* _end_time = NULL;
if (!end_time && buffer_size(((ProfWin*)chatwin)->layout->buffer) > 0) {
ProfBuffEntry* first = buffer_get_entry(((ProfWin*)chatwin)->layout->buffer, 0);
if (first && first->time) {
_end_time = g_date_time_format_iso8601(first->time);
end_time = _end_time;
}
_end_time = g_date_time_format_iso8601(buffer_get_entry(((ProfWin*)chatwin)->layout->buffer, 0)->time);
end_time = _end_time;
}
GSList* history = NULL;

View File

@@ -152,7 +152,7 @@ cons_bad_cmd_usage(const char* const cmd)
g_string_printf(msg, "Invalid usage, see '/help %s' for details.", &cmd[1]);
cons_show("");
cons_show("%s", msg->str);
cons_show(msg->str);
g_string_free(msg, TRUE);
}
@@ -773,7 +773,7 @@ cons_show_disco_info(const char* jid, GSList* identities, GSList* features)
if (identity->category) {
identity_str = g_string_append(identity_str, identity->category);
}
cons_show("%s", identity_str->str);
cons_show(identity_str->str);
g_string_free(identity_str, TRUE);
identities = g_slist_next(identities);
}
@@ -938,7 +938,7 @@ cons_show_account_list(gchar** accounts)
theme_item_t presence_colour = theme_main_presence_attrs(string_from_resource_presence(presence));
win_println(console, presence_colour, "-", "%s", accounts[i]);
} else {
cons_show("%s", accounts[i]);
cons_show(accounts[i]);
}
}
cons_show("");
@@ -1019,7 +1019,7 @@ cons_show_account(ProfAccount* account)
}
curr = curr->next;
}
cons_show("%s", manual->str);
cons_show(manual->str);
g_string_free(manual, TRUE);
}
if (g_list_length(account->otr_opportunistic) > 0) {
@@ -1032,7 +1032,7 @@ cons_show_account(ProfAccount* account)
}
curr = curr->next;
}
cons_show("%s", opportunistic->str);
cons_show(opportunistic->str);
g_string_free(opportunistic, TRUE);
}
if (g_list_length(account->otr_always) > 0) {
@@ -1045,7 +1045,7 @@ cons_show_account(ProfAccount* account)
}
curr = curr->next;
}
cons_show("%s", always->str);
cons_show(always->str);
g_string_free(always, TRUE);
}
@@ -2297,7 +2297,7 @@ cons_show_themes(GSList* themes)
} else {
cons_show("Available themes:");
while (themes) {
cons_show("%s", themes->data);
cons_show(themes->data);
themes = g_slist_next(themes);
}
}
@@ -2315,7 +2315,7 @@ cons_show_scripts(GSList* scripts)
} else {
cons_show("Scripts:");
while (scripts) {
cons_show("%s", scripts->data);
cons_show(scripts->data);
scripts = g_slist_next(scripts);
}
}

View File

@@ -445,7 +445,7 @@ ui_handle_error(const char* const err_msg)
GString* msg = g_string_new("");
g_string_printf(msg, "Error %s", err_msg);
cons_show_error("%s", msg->str);
cons_show_error(msg->str);
g_string_free(msg, TRUE);
}
@@ -461,7 +461,7 @@ ui_invalid_command_usage(const char* const cmd, void (*setting_func)(void))
(*setting_func)();
} else {
cons_show("");
cons_show("%s", msg->str);
cons_show(msg->str);
ProfWin* current = wins_get_current();
if (current->type == WIN_CHAT) {
win_println(current, THEME_DEFAULT, "-", "%s", msg->str);

View File

@@ -144,14 +144,10 @@ void
create_input_window(void)
{
/* MB_CUR_MAX is evaluated at runtime depending on the current
* locale; ensure our own compiled-in maximum is sufficient.
* Fail gracefully instead of aborting in production.
* locale, therefore we check that our own version is big enough
* and bail out if it isn't.
*/
if (MB_CUR_MAX > PROF_MB_CUR_MAX) {
log_error("Locale MB_CUR_MAX (%zu) exceeds compiled limit (%d)", (size_t)MB_CUR_MAX, PROF_MB_CUR_MAX);
cons_show_error("Unsupported locale. Before running, execute in terminal: export LC_ALL=C.UTF-8");
return;
}
assert(MB_CUR_MAX <= PROF_MB_CUR_MAX);
#ifdef NCURSES_REENTRANT
set_escdelay(25);
#else
@@ -167,10 +163,6 @@ create_input_window(void)
rl_callback_handler_install(NULL, _inp_rl_linehandler);
inp_win = newpad(1, INP_WIN_MAX);
if (!inp_win) {
log_error("Failed to allocate input window pad");
return;
}
wbkgd(inp_win, theme_attrs(THEME_INPUT_TEXT));
keypad(inp_win, TRUE);
wmove(inp_win, 0, 0);
@@ -246,9 +238,6 @@ inp_readline(void)
void
inp_win_resize(void)
{
if (!inp_win) {
return;
}
int col = getcurx(inp_win);
int wcols = getmaxx(stdscr);
@@ -296,10 +285,8 @@ void
inp_close(void)
{
rl_callback_handler_remove();
if (inp_win) {
delwin(inp_win);
inp_win = NULL;
}
delwin(inp_win);
inp_win = NULL;
fclose(discard);
discard = NULL;
}
@@ -307,9 +294,6 @@ inp_close(void)
char*
inp_get_line(void)
{
if (!inp_win) {
return NULL;
}
werase(inp_win);
wmove(inp_win, 0, 0);
_inp_win_update_virtual();
@@ -334,9 +318,6 @@ inp_set_line(const char* const new_line)
char*
inp_get_password(void)
{
if (!inp_win) {
return NULL;
}
werase(inp_win);
wmove(inp_win, 0, 0);
_inp_win_update_virtual();

View File

@@ -40,8 +40,6 @@
#include <string.h>
#include <stdlib.h>
#include "log.h"
#ifdef HAVE_NCURSESW_NCURSES_H
#include <ncursesw/ncurses.h>
#elif HAVE_NCURSES_H
@@ -113,24 +111,16 @@ status_bar_init(void)
int row = screen_statusbar_row();
int cols = getmaxx(stdscr);
if (cols <= 0) {
log_warning("status_bar_init: invalid cols %d, defaulting to 1", cols);
cols = 1;
}
statusbar_win = newwin(1, cols, row, 0);
if (statusbar_win) {
status_bar_draw();
}
status_bar_draw();
}
void
status_bar_close(void)
{
if (statusbar_win) {
delwin(statusbar_win);
statusbar_win = NULL;
}
delwin(statusbar_win);
statusbar_win = NULL;
if (statusbar) {
if (statusbar->time) {
g_free(statusbar->time);
@@ -155,14 +145,7 @@ status_bar_close(void)
void
status_bar_resize(void)
{
if (!statusbar_win) {
return;
}
int cols = getmaxx(stdscr);
if (cols <= 0) {
log_warning("status_bar_resize: invalid cols %d, defaulting to 1", cols);
cols = 1;
}
werase(statusbar_win);
int row = screen_statusbar_row();
wresize(statusbar_win, 1, cols);
@@ -302,9 +285,6 @@ status_bar_clear_fulljid(void)
void
status_bar_draw(void)
{
if (!statusbar_win) {
return;
}
werase(statusbar_win);
wbkgd(statusbar_win, theme_attrs(THEME_STATUS_TEXT));
@@ -694,13 +674,8 @@ _display_name(StatusBarTab* tab)
fullname = g_strconcat(mucwin_title, " conf", NULL);
} else if (tab->window_type == WIN_PRIVATE) {
auto_jid Jid* jid = jid_create(tab->identifier);
if (jid) {
auto_gchar gchar* mucwin_title = mucwin_generate_title(jid->barejid, PREF_STATUSBAR_ROOM_TITLE);
fullname = g_strconcat(mucwin_title, "/", jid->resourcepart, NULL);
} else {
// Fallback: use identifier directly if JID parsing failed
fullname = strdup(tab->identifier);
}
auto_gchar gchar* mucwin_title = mucwin_generate_title(jid->barejid, PREF_STATUSBAR_ROOM_TITLE);
fullname = g_strconcat(mucwin_title, "/", jid->resourcepart, NULL);
} else {
fullname = strdup("window");
}

View File

@@ -73,16 +73,9 @@ void
create_title_bar(void)
{
int cols = getmaxx(stdscr);
if (cols <= 0) {
cols = 1;
}
int row = screen_titlebar_row();
win = newwin(1, cols, row, 0);
if (!win) {
// Failed to create title bar window; skip initialization to avoid NULL deref
return;
}
wbkgd(win, theme_attrs(THEME_TITLE_TEXT));
title_bar_console();
title_bar_set_presence(CONTACT_OFFLINE);
@@ -95,18 +88,13 @@ create_title_bar(void)
void
free_title_bar(void)
{
if (win) {
delwin(win);
win = NULL;
}
delwin(win);
win = NULL;
}
void
title_bar_update_virtual(void)
{
if (!win) {
return;
}
ProfWin* window = wins_get_current();
if (window->type != WIN_CONSOLE) {
if (typing_elapsed) {
@@ -126,13 +114,7 @@ title_bar_update_virtual(void)
void
title_bar_resize(void)
{
if (!win) {
return;
}
int cols = getmaxx(stdscr);
if (cols <= 0) {
cols = 1;
}
werase(win);
@@ -149,9 +131,6 @@ title_bar_resize(void)
void
title_bar_console(void)
{
if (!win) {
return;
}
werase(win);
if (typing_elapsed) {
g_timer_destroy(typing_elapsed);
@@ -213,9 +192,6 @@ title_bar_set_typing(gboolean is_typing)
static void
_title_bar_draw(void)
{
if (!win) {
return;
}
int pos;
int maxrightpos;
ProfWin* current = wins_get_current();
@@ -292,11 +268,7 @@ _show_scrolled(ProfWin* current)
wattroff(win, bracket_attrs);
wattron(win, scrolled_attrs);
if (current->layout->unread_msg == 0) {
wprintw(win, "SCROLLED");
} else {
wprintw(win, "SCROLLED, NEW MESSAGES");
}
wprintw(win, "SCROLLED");
wattroff(win, scrolled_attrs);
wattron(win, bracket_attrs);

View File

@@ -120,7 +120,6 @@ typedef struct prof_layout_t
ProfBuff buffer;
int y_pos;
int paged;
int unread_msg;
} ProfLayout;
typedef struct prof_layout_simple_t

View File

@@ -75,22 +75,12 @@ static void _win_print_internal(ProfWin* window, const char* show_char, int pad_
int flags, theme_item_t theme_item, const char* const from, const char* const message, DeliveryReceipt* receipt);
static void _win_print_wrapped(WINDOW* win, const char* const message, size_t indent, int pad_indent);
// Helper: clamp a subwindow width to a sane range [1, cols-1] if possible
static int
_check_subwin_width(int cols, int width)
{
return cols <= 1 ? 1 : CLAMP(width, 1, cols - 1);
}
int
win_roster_cols(void)
{
int roster_win_percent = prefs_get_roster_size();
int cols = getmaxx(stdscr);
int width = CEILING((((double)cols) / 100) * roster_win_percent);
// Clamp to a sane range to avoid zero/full-width pads
width = _check_subwin_width(cols, width);
return width;
return CEILING((((double)cols) / 100) * roster_win_percent);
}
int
@@ -98,10 +88,7 @@ win_occpuants_cols(void)
{
int occupants_win_percent = prefs_get_occupants_size();
int cols = getmaxx(stdscr);
int width = CEILING((((double)cols) / 100) * occupants_win_percent);
// Clamp to a sane range to avoid zero/full-width pads
width = _check_subwin_width(cols, width);
return width;
return CEILING((((double)cols) / 100) * occupants_win_percent);
}
static ProfLayout*
@@ -116,7 +103,6 @@ _win_create_simple_layout(void)
layout->base.buffer = buffer_create();
layout->base.y_pos = 0;
layout->base.paged = 0;
layout->base.unread_msg = 0;
scrollok(layout->base.win, TRUE);
return &layout->base;
@@ -134,7 +120,6 @@ _win_create_split_layout(void)
layout->base.buffer = buffer_create();
layout->base.y_pos = 0;
layout->base.paged = 0;
layout->base.unread_msg = 0;
scrollok(layout->base.win, TRUE);
layout->subwin = NULL;
layout->sub_y_pos = 0;
@@ -157,7 +142,6 @@ win_create_console(void)
ProfWin*
win_create_chat(const char* const barejid)
{
assert(barejid != NULL);
ProfChatWin* new_win = malloc(sizeof(ProfChatWin));
new_win->window.type = WIN_CHAT;
new_win->window.scroll_state = WIN_SCROLL_INNER;
@@ -189,7 +173,6 @@ win_create_chat(const char* const barejid)
ProfWin*
win_create_muc(const char* const roomjid)
{
assert(roomjid != NULL);
ProfMucWin* new_win = malloc(sizeof(ProfMucWin));
int cols = getmaxx(stdscr);
@@ -214,7 +197,6 @@ win_create_muc(const char* const roomjid)
layout->base.buffer = buffer_create();
layout->base.y_pos = 0;
layout->base.paged = 0;
layout->base.unread_msg = 0;
scrollok(layout->base.win, TRUE);
new_win->window.layout = (ProfLayout*)layout;
@@ -248,8 +230,6 @@ win_create_muc(const char* const roomjid)
ProfWin*
win_create_config(const char* const roomjid, DataForm* form, ProfConfWinCallback submit, ProfConfWinCallback cancel, const void* userdata)
{
assert(roomjid != NULL);
assert(form != NULL);
ProfConfWin* new_win = malloc(sizeof(ProfConfWin));
new_win->window.type = WIN_CONFIG;
new_win->window.scroll_state = WIN_SCROLL_INNER;
@@ -268,7 +248,6 @@ win_create_config(const char* const roomjid, DataForm* form, ProfConfWinCallback
ProfWin*
win_create_private(const char* const fulljid)
{
assert(fulljid != NULL);
ProfPrivateWin* new_win = malloc(sizeof(ProfPrivateWin));
new_win->window.type = WIN_PRIVATE;
new_win->window.scroll_state = WIN_SCROLL_INNER;
@@ -299,8 +278,6 @@ win_create_xmlconsole(void)
ProfWin*
win_create_plugin(const char* const plugin_name, const char* const tag)
{
assert(plugin_name != NULL);
assert(tag != NULL);
ProfPluginWin* new_win = malloc(sizeof(ProfPluginWin));
new_win->window.type = WIN_PLUGIN;
new_win->window.scroll_state = WIN_SCROLL_INNER;
@@ -317,7 +294,6 @@ win_create_plugin(const char* const plugin_name, const char* const tag)
ProfWin*
win_create_vcard(vCard* vcard)
{
assert(vcard != NULL);
ProfVcardWin* new_win = malloc(sizeof(ProfVcardWin));
new_win->window.type = WIN_VCARD;
new_win->window.scroll_state = WIN_SCROLL_INNER;
@@ -369,7 +345,7 @@ win_get_title(ProfWin* window)
const ProfConfWin* confwin = (ProfConfWin*)window;
assert(confwin->memcheck == PROFCONFWIN_MEMCHECK);
auto_gchar gchar* mucwin_title = mucwin_generate_title(confwin->roomjid, PREF_TITLEBAR_MUC_TITLE);
if (confwin->form && confwin->form->modified) {
if (confwin->form->modified) {
return g_strconcat(mucwin_title, " config *", NULL);
}
return g_strconcat(mucwin_title, " config", NULL);
@@ -577,25 +553,7 @@ win_show_subwin(ProfWin* window)
}
ProfLayoutSplit* layout = (ProfLayoutSplit*)window->layout;
// If a subwindow already exists (e.g. repeated call), destroy it to avoid leaks
if (layout->subwin) {
delwin(layout->subwin);
layout->subwin = NULL;
}
// Ensure minimum width to avoid creating a zero-width pad
if (subwin_cols <= 0) {
subwin_cols = 1;
}
layout->subwin = newpad(PAD_SIZE, subwin_cols);
if (layout->subwin == NULL) {
// Failed to allocate subwindow; keep base window resized to full width
log_error("Failed to create subwindow pad (cols=%d)", subwin_cols);
wresize(layout->base.win, PAD_SIZE, cols);
win_redraw(window);
return;
}
wbkgd(layout->subwin, theme_attrs(THEME_TEXT));
wresize(layout->base.win, PAD_SIZE, cols - subwin_cols);
win_redraw(window);
@@ -740,11 +698,9 @@ void
win_page_down(ProfWin* window, int scroll_size)
{
int total_rows = getcury(window->layout->win);
int total_rows_with_unread = total_rows + window->layout->unread_msg;
int* page_start = &(window->layout->y_pos);
int page_space = getmaxy(stdscr) - 4;
int page_start_initial = *page_start;
if (scroll_size == 0)
scroll_size = page_space;
win_scroll_state_t* scroll_state = &window->scroll_state;
@@ -753,11 +709,7 @@ win_page_down(ProfWin* window, int scroll_size)
*page_start += scroll_size;
// Scrolled down after reaching the bottom of the page
gboolean past_bottom = *page_start > total_rows_with_unread - page_space;
gboolean at_page_space_and_past_unread = (*page_start == page_space && *page_start >= total_rows_with_unread);
gboolean is_chat = window->type == WIN_CHAT;
if ((past_bottom || at_page_space_and_past_unread) && is_chat) {
if ((*page_start > total_rows - page_space || (*page_start == page_space && *page_start >= total_rows)) && window->type == WIN_CHAT) {
int bf_size = buffer_size(window->layout->buffer);
if (bf_size > 0 && *scroll_state != WIN_SCROLL_REACHED_BOTTOM) {
// How many lines are left until end of the screen
@@ -791,16 +743,13 @@ win_page_down(ProfWin* window, int scroll_size)
window->layout->paged = 1;
// update only if position has changed
if ((page_start_initial != *page_start) || window->layout->unread_msg) {
if (page_start_initial != *page_start) {
win_update_virtual(window);
}
/* Switch off page if no messages left to read.
* TODO: update buffer end handling to check messages just after last entry.
*/
if (*scroll_state == WIN_SCROLL_REACHED_BOTTOM) {
// switch off page if last line and space line visible
if (total_rows - *page_start == page_space) {
window->layout->paged = 0;
window->layout->unread_msg = 0;
}
}
@@ -861,7 +810,6 @@ win_clear(ProfWin* window)
int* page_start = &(window->layout->y_pos);
*page_start = y;
window->layout->paged = 1;
window->layout->unread_msg = 0;
win_update_virtual(window);
}
@@ -949,11 +897,6 @@ win_refresh_with_subwin(ProfWin* window)
int row_end = screen_mainwin_row_end();
ProfLayoutSplit* layout = (ProfLayoutSplit*)window->layout;
// Safety: if subwindow is not active, nothing to refresh
if (layout == NULL || layout->subwin == NULL) {
return;
}
if (window->type == WIN_MUC) {
subwin_cols = win_occpuants_cols();
} else if (window->type == WIN_CONSOLE) {
@@ -971,7 +914,6 @@ void
win_move_to_end(ProfWin* window)
{
window->layout->paged = 0;
window->layout->unread_msg = 0;
int rows = getmaxy(stdscr);
int y = getcury(window->layout->win);
@@ -1754,13 +1696,6 @@ win_newline(ProfWin* window)
static void
_win_printf(ProfWin* window, const char* show_char, int pad_indent, GDateTime* timestamp, int flags, theme_item_t theme_item, const char* const display_from, const char* const from_jid, const char* const message_id, const char* const message, ...)
{
/* Prevent printing and buffer update when user is viewing message history [SCROLLING]*/
if (window->layout->paged && wins_is_current(window)) {
window->layout->unread_msg++;
return;
}
if (timestamp == NULL) {
timestamp = g_date_time_new_now_local();
} else {
@@ -2082,14 +2017,7 @@ win_print_loading_history(ProfWin* window)
gboolean is_buffer_empty = buffer_size(window->layout->buffer) == 0;
if (!is_buffer_empty) {
ProfBuffEntry* first = buffer_get_entry(window->layout->buffer, 0);
if (first && first->time) {
timestamp = first->time;
} else {
// Fallback to current time if entry/time is unavailable
timestamp = g_date_time_new_now_local();
is_buffer_empty = TRUE; // ensure we unref fallback timestamp below
}
timestamp = buffer_get_entry(window->layout->buffer, 0)->time;
} else {
timestamp = g_date_time_new_now_local();
}
@@ -2289,7 +2217,7 @@ void
win_handle_command_exec_result_note(ProfWin* window, const char* const type, const char* const value)
{
assert(window != NULL);
win_println(window, THEME_DEFAULT, "!", "%s", value);
win_println(window, THEME_DEFAULT, "!", value);
}
void

View File

@@ -407,10 +407,8 @@ wins_get_by_string(const char* str)
if (barejid) {
ProfChatWin* chatwin = wins_get_chat(barejid);
if (chatwin) {
free(barejid);
return (ProfWin*)chatwin;
}
free(barejid);
}
}
@@ -605,9 +603,6 @@ wins_new_xmlconsole(void)
{
int result = _wins_get_next_available_num(keys);
ProfWin* newwin = win_create_xmlconsole();
if (!newwin) {
return NULL;
}
_wins_htable_insert(windows, GINT_TO_POINTER(result), newwin);
autocomplete_add(wins_ac, "xmlconsole");
autocomplete_add(wins_close_ac, "xmlconsole");
@@ -619,9 +614,6 @@ wins_new_chat(const char* const barejid)
{
int result = _wins_get_next_available_num(keys);
ProfWin* newwin = win_create_chat(barejid);
if (!newwin) {
return NULL;
}
_wins_htable_insert(windows, GINT_TO_POINTER(result), newwin);
autocomplete_add(wins_ac, barejid);
@@ -645,9 +637,6 @@ wins_new_muc(const char* const roomjid)
{
int result = _wins_get_next_available_num(keys);
ProfWin* newwin = win_create_muc(roomjid);
if (!newwin) {
return NULL;
}
_wins_htable_insert(windows, GINT_TO_POINTER(result), newwin);
autocomplete_add(wins_ac, roomjid);
autocomplete_add(wins_close_ac, roomjid);
@@ -662,9 +651,6 @@ wins_new_config(const char* const roomjid, DataForm* form, ProfConfWinCallback s
{
int result = _wins_get_next_available_num(keys);
ProfWin* newwin = win_create_config(roomjid, form, submit, cancel, userdata);
if (!newwin) {
return NULL;
}
_wins_htable_insert(windows, GINT_TO_POINTER(result), newwin);
return newwin;
@@ -675,9 +661,6 @@ wins_new_private(const char* const fulljid)
{
int result = _wins_get_next_available_num(keys);
ProfWin* newwin = win_create_private(fulljid);
if (!newwin) {
return NULL;
}
_wins_htable_insert(windows, GINT_TO_POINTER(result), newwin);
autocomplete_add(wins_ac, fulljid);
autocomplete_add(wins_close_ac, fulljid);
@@ -692,9 +675,6 @@ wins_new_plugin(const char* const plugin_name, const char* const tag)
{
int result = _wins_get_next_available_num(keys);
ProfWin* newwin = win_create_plugin(plugin_name, tag);
if (!newwin) {
return NULL;
}
_wins_htable_insert(windows, GINT_TO_POINTER(result), newwin);
autocomplete_add(wins_ac, tag);
autocomplete_add(wins_close_ac, tag);
@@ -706,9 +686,6 @@ wins_new_vcard(vCard* vcard)
{
int result = _wins_get_next_available_num(keys);
ProfWin* newwin = win_create_vcard(vcard);
if (!newwin) {
return NULL;
}
_wins_htable_insert(windows, GINT_TO_POINTER(result), newwin);
return newwin;

View File

@@ -171,15 +171,16 @@ caps_get_features(void)
{
GList* result = NULL;
GHashTableIter iter;
gpointer key, value;
g_hash_table_iter_init(&iter, prof_features);
while (g_hash_table_iter_next(&iter, &key, &value)) {
result = g_list_append(result, strdup((char*)key));
GList* features_as_list = g_hash_table_get_keys(prof_features);
GList* curr = features_as_list;
while (curr) {
result = g_list_append(result, strdup(curr->data));
curr = g_list_next(curr);
}
g_list_free(features_as_list);
GList* plugin_features = plugins_get_disco_features();
GList* curr = plugin_features;
curr = plugin_features;
while (curr) {
result = g_list_append(result, strdup(curr->data));
curr = g_list_next(curr);

View File

@@ -148,7 +148,7 @@ connection_init(void)
if (string_to_verbosity(v, &verbosity, &err_msg)) {
xmpp_ctx_set_verbosity(conn.xmpp_ctx, verbosity);
} else {
cons_show("%s", err_msg);
cons_show(err_msg);
}
conn.xmpp_conn = xmpp_conn_new(conn.xmpp_ctx);
@@ -312,7 +312,6 @@ iq_reg2_cb(xmpp_conn_t* xmpp_conn, xmpp_stanza_t* stanza, void* userdata)
goto quit;
quit:
log_debug("[CONNDBG] iq_reg2_cb: disconnecting after registration completion");
xmpp_disconnect(xmpp_conn);
return 0;
@@ -551,7 +550,6 @@ connection_disconnect(void)
// don't disconnect already disconnected connection,
// or we get infinite loop otherwise
if (conn.conn_last_event == XMPP_CONN_CONNECT) {
log_debug("[CONNDBG] connection_disconnect: user-initiated disconnect (status=%d)", (int)conn.conn_status);
conn.conn_status = JABBER_DISCONNECTING;
xmpp_disconnect(conn.xmpp_conn);
@@ -559,7 +557,6 @@ connection_disconnect(void)
session_process_events();
}
} else {
log_debug("[CONNDBG] connection_disconnect: already disconnected (last_event=%d)", (int)conn.conn_last_event);
conn.conn_status = JABBER_DISCONNECTED;
}
@@ -638,18 +635,22 @@ gboolean
connection_supports(const char* const feature)
{
gboolean ret = FALSE;
GHashTableIter iter;
gpointer key, value;
GList* jids = g_hash_table_get_keys(conn.features_by_jid);
g_hash_table_iter_init(&iter, conn.features_by_jid);
while (g_hash_table_iter_next(&iter, &key, &value)) {
GHashTable* features = (GHashTable*)value;
GList* curr = jids;
while (curr) {
char* jid = curr->data;
GHashTable* features = g_hash_table_lookup(conn.features_by_jid, jid);
if (features && g_hash_table_lookup(features, feature)) {
ret = TRUE;
break;
}
curr = g_list_next(curr);
}
g_list_free(jids);
return ret;
}
@@ -660,17 +661,22 @@ connection_jid_for_feature(const char* const feature)
return NULL;
}
GHashTableIter iter;
gpointer key, value;
GList* jids = g_hash_table_get_keys(conn.features_by_jid);
g_hash_table_iter_init(&iter, conn.features_by_jid);
while (g_hash_table_iter_next(&iter, &key, &value)) {
GHashTable* features = (GHashTable*)value;
GList* curr = jids;
while (curr) {
char* jid = curr->data;
GHashTable* features = g_hash_table_lookup(conn.features_by_jid, jid);
if (features && g_hash_table_lookup(features, feature)) {
return (const char*)key;
g_list_free(jids);
return jid;
}
curr = g_list_next(curr);
}
g_list_free(jids);
return NULL;
}
@@ -973,7 +979,6 @@ _connection_handler(xmpp_conn_t* const xmpp_conn, const xmpp_conn_event_t status
connection_get_jid();
conn.domain = strdup(conn.jid->domainpart);
log_debug("conn.domain=%s", conn.jid->domainpart);
connection_clear_data();
conn.features_by_jid = g_hash_table_new_full(g_str_hash, g_str_equal, free, (GDestroyNotify)g_hash_table_destroy);
@@ -1013,9 +1018,6 @@ _connection_handler(xmpp_conn_t* const xmpp_conn, const xmpp_conn_event_t status
// disconnected
case XMPP_CONN_DISCONNECT:
log_debug("Connection handler: XMPP_CONN_DISCONNECT");
log_debug("[CONNDBG] disconnect: previous_status=%d error=%d has_stream_error=%s",
(int)conn.conn_status, error,
(stream_error && stream_error->stanza) ? "yes" : "no");
// lost connection for unknown reason
if (conn.conn_status == JABBER_CONNECTED || conn.conn_status == JABBER_DISCONNECTING) {
@@ -1025,7 +1027,7 @@ _connection_handler(xmpp_conn_t* const xmpp_conn, const xmpp_conn_event_t status
conn.sm_state = xmpp_conn_get_sm_state(conn.xmpp_conn);
if (send_queue_len > 0 && prefs_get_boolean(PREF_STROPHE_SM_RESEND)) {
conn.queued_messages = calloc(send_queue_len + 1, sizeof(*conn.queued_messages));
for (int n = 0; n < send_queue_len; ++n) {
for (int n = 0; n < send_queue_len && conn.queued_messages[n]; ++n) {
conn.queued_messages[n] = xmpp_conn_send_queue_drop_element(conn.xmpp_conn, XMPP_QUEUE_OLDEST);
}
} else if (send_queue_len > 0) {
@@ -1161,33 +1163,3 @@ connection_get_profanity_identifier(void)
{
return prof_identifier;
}
void
connection_debug_print_features()
{
log_debug("=== Connection Features ===");
GHashTableIter iter;
gpointer jid_ptr, features_ptr;
g_hash_table_iter_init(&iter, conn.features_by_jid);
while (g_hash_table_iter_next(&iter, &jid_ptr, &features_ptr)) {
const char* jid = (const char*)jid_ptr;
GHashTable* features = (GHashTable*)features_ptr;
if (!features || g_hash_table_size(features) == 0) {
log_debug("%s:\t(no features)", jid);
continue;
}
GHashTableIter feature_iter;
gpointer feature_key, feature_value;
g_hash_table_iter_init(&feature_iter, features);
while (g_hash_table_iter_next(&feature_iter, &feature_key, &feature_value)) {
const char* feature = (const char*)feature_key;
log_debug("%s:\t%s", jid, feature);
}
}
log_debug("=== End of Features ===");
}

View File

@@ -437,7 +437,18 @@ form_get_form_type_field(DataForm* form)
gboolean
form_tag_exists(DataForm* form, const char* const tag)
{
return g_hash_table_contains(form->tag_to_var, tag);
GList* tags = g_hash_table_get_keys(form->tag_to_var);
GList* curr = tags;
while (curr) {
if (g_strcmp0(curr->data, tag) == 0) {
g_list_free(tags);
return TRUE;
}
curr = g_list_next(curr);
}
g_list_free(tags);
return FALSE;
}
form_field_type_t

View File

@@ -1417,8 +1417,6 @@ _manual_pong_id_handler(xmpp_stanza_t* const stanza, void* const userdata)
return 0;
}
static gboolean autoping_error_shown = false;
static int
_autoping_timed_send(xmpp_conn_t* const conn, void* const userdata)
{
@@ -1427,13 +1425,10 @@ _autoping_timed_send(xmpp_conn_t* const conn, void* const userdata)
}
if (connection_supports(XMPP_FEATURE_PING) == FALSE) {
// TODO: do we need to check it on each autoping call?
log_warning("Server doesn't advertise %s feature.", XMPP_FEATURE_PING);
if (!autoping_error_shown) {
connection_debug_print_features();
cons_show_error("Server ping not supported (%s). Check log for details.", XMPP_FEATURE_PING);
}
autoping_error_shown = 1;
log_warning("Server doesn't advertise %s feature, disabling autoping.", XMPP_FEATURE_PING);
prefs_set_autoping(0);
cons_show_error("Server ping not supported (%s), autoping disabled.", XMPP_FEATURE_PING);
return 0;
}
if (autoping_wait) {
@@ -1504,7 +1499,7 @@ _auto_pong_id_handler(xmpp_stanza_t* const stanza, void* const userdata)
if (g_strcmp0(errtype, STANZA_TYPE_CANCEL) == 0) {
log_warning("Server ping (id=%s) error type 'cancel', disabling autoping.", id);
prefs_set_autoping(0);
cons_show_error("Autoping is not supported by the server (stanza cancelled). The feature has been disabled automatically.");
cons_show_error("Server ping not supported, autoping disabled.");
xmpp_timed_handler_delete(connection_get_conn(), _autoping_timed_send);
}
@@ -2520,7 +2515,6 @@ _disco_items_result_handler(xmpp_stanza_t* const stanza)
GSList* items = NULL;
if ((g_strcmp0(id, "discoitemsreq") != 0) && (g_strcmp0(id, "discoitemsreq_onconnect") != 0)) {
log_warning("_disco_items_result_handler: Received unexpected disco id: %s", id);
return;
}
@@ -2532,6 +2526,9 @@ _disco_items_result_handler(xmpp_stanza_t* const stanza)
}
xmpp_stanza_t* child = xmpp_stanza_get_children(query);
if (child == NULL) {
return;
}
while (child) {
const char* stanza_name = xmpp_stanza_get_name(child);

View File

@@ -868,7 +868,7 @@ _handle_error(xmpp_stanza_t* const stanza)
g_string_append(log_msg, " error=");
g_string_append(log_msg, err_msg);
log_info("%s", log_msg->str);
log_info(log_msg->str);
g_string_free(log_msg, TRUE);

View File

@@ -326,7 +326,7 @@ _ox_metadata_result(xmpp_stanza_t* const stanza, void* const userdata)
if (fingerprint) {
if (strlen(fingerprint) == KEYID_LENGTH) {
cons_show("%s", fingerprint);
cons_show(fingerprint);
} else {
cons_show("OX: Wrong char size of public key");
log_error("[OX] Wrong chat size of public key %s", fingerprint);

View File

@@ -455,7 +455,7 @@ _presence_error_handler(xmpp_stanza_t* const stanza)
g_string_append(log_msg, " error=");
g_string_append(log_msg, err_msg);
log_info("%s", log_msg->str);
log_info(log_msg->str);
g_string_free(log_msg, TRUE);

View File

@@ -218,7 +218,6 @@ session_connect_with_details(const char* const jid, const char* const passwd, co
void
session_autoping_fail(void)
{
log_debug("[CONNDBG] session_autoping_fail: autoping timeout, triggering lost connection");
session_lost_connection();
}
@@ -265,14 +264,11 @@ session_process_events(void)
if ((reconnect_sec != 0) && reconnect_timer) {
int elapsed_sec = g_timer_elapsed(reconnect_timer, NULL);
if (elapsed_sec > reconnect_sec) {
log_debug("[CONNDBG] session_process_events: auto-reconnect triggered after %d seconds (threshold=%d)",
elapsed_sec, reconnect_sec);
session_reconnect_now();
}
}
break;
case JABBER_RECONNECT:
log_debug("[CONNDBG] session_process_events: JABBER_RECONNECT state, calling session_reconnect_now");
session_reconnect_now();
break;
default:
@@ -331,12 +327,12 @@ session_login_success(gboolean secured)
// logged in with account
if (saved_account.name) {
log_debug("[CONNDBG] Connection handler: logged in with account name: %s", saved_account.name);
log_debug("Connection handler: logged in with account name: %s", saved_account.name);
sv_ev_login_account_success(saved_account.name, secured);
// logged in without account, use details to create new account
} else {
log_debug("[CONNDBG] Connection handler: logged in with jid: %s", saved_details.name);
log_debug("Connection handler: logged in with jid: %s", saved_details.name);
accounts_add(saved_details.name, saved_details.altdomain, saved_details.port, saved_details.tls_policy, saved_details.auth_policy);
accounts_set_jid(saved_details.name, saved_details.jid);
@@ -375,11 +371,11 @@ void
session_login_failed(void)
{
if (reconnect_timer == NULL) {
log_debug("[CONNDBG] Connection handler: No reconnect timer");
log_debug("Connection handler: No reconnect timer");
sv_ev_failed_login();
_session_free_internals();
} else {
log_debug("[CONNDBG] Connection handler: Restarting reconnect timer");
log_debug("Connection handler: Restarting reconnect timer");
if (prefs_get_reconnect() != 0) {
g_timer_start(reconnect_timer);
}
@@ -393,16 +389,12 @@ session_login_failed(void)
void
session_lost_connection(void)
{
log_debug("[CONNDBG] session_lost_connection: connection lost, reconnect_interval=%d",
prefs_get_reconnect());
/* this callback also clears all cached data */
sv_ev_lost_connection();
if (prefs_get_reconnect() != 0) {
assert(reconnect_timer == NULL);
reconnect_timer = g_timer_new();
log_debug("[CONNDBG] session_lost_connection: reconnect timer started");
} else {
log_debug("[CONNDBG] session_lost_connection: auto-reconnect disabled, cleaning up");
_session_free_internals();
}
}

View File

@@ -208,8 +208,6 @@ const char* connection_jid_for_feature(const char* const feature);
const char* connection_get_profanity_identifier(void);
void connection_debug_print_features();
char* message_send_chat(const char* const barejid, const char* const msg, const char* const oob_url, gboolean request_receipt, const char* const replace_id);
char* message_send_chat_otr(const char* const barejid, const char* const msg, gboolean request_receipt, const char* const replace_id);
char* message_send_chat_pgp(const char* const barejid, const char* const msg, gboolean request_receipt, const char* const replace_id);

View File

@@ -1,44 +1,11 @@
/*
* functionaltests.c
*
* Functional tests for CProof XMPP client.
* Uses cmocka framework with stabber mock XMPP server.
*
* Each test is wrapped with PROF_FUNC_TEST macro which sets up
* init_prof_test (starts stabber server and profanity client)
* and close_prof_test (cleanup) as setup/teardown functions.
*
* NOTE: We restart client and server for each test to ensure complete
* isolation. This prevents state leakage between tests (roster entries,
* MUC rooms, presence subscriptions, etc.). While slower, it eliminates
* flaky tests caused by leftover state. The overhead is acceptable since
* functional tests run less frequently than unit tests.
*
* Tests are organized into groups for better maintainability and parallel execution:
* Group 1: Connect, Ping, Rooms, Software
* Group 2: Message, Receipts, Roster, Chat Session
* Group 3: Presence, Disconnect
* Group 4: MUC, Carbons
*
* Parallel execution:
* ./functionaltests - run all tests sequentially
* ./functionaltests N - run group N only (N = 1..num_groups)
*
* For parallel execution, run multiple groups simultaneously:
* ./functionaltests 1 & ./functionaltests 2 & ./functionaltests 3 & ./functionaltests 4 & wait
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include "prof_cmocka.h"
#include <sys/stat.h>
#include "config.h"
#include "common.h"
#include "proftest.h"
#include "test_connect.h"
#include "test_ping.h"
@@ -52,105 +19,31 @@
#include "test_software.h"
#include "test_muc.h"
#include "test_disconnect.h"
#include "test_lastactivity.h"
/* Macro to wrap each test with setup/teardown functions */
#define PROF_FUNC_TEST(test) cmocka_unit_test_setup_teardown(test, init_prof_test, close_prof_test)
int
main(int argc, char* argv[])
{
int group = 0; /* 0 = all groups */
if (argc > 1) {
group = atoi(argv[1]);
if (group < 1 || group > 4) {
fprintf(stderr, "Usage: %s [group]\n", argv[0]);
fprintf(stderr, " group: 1-4 to run specific group, or omit for all\n");
return 1;
}
}
int main(int argc, char* argv[]) {
char group_env[16];
snprintf(group_env, sizeof(group_env), "%d", group);
setenv("PROF_TEST_GROUP", group_env, 1);
fprintf(stderr, "[PROF_TEST] Starting functional tests, group=%d\n", group);
const struct CMUnitTest all_tests[] = {
/* ============================================================
* GROUP 1: Connect, Ping, Rooms, Software
* Basic XMPP session establishment and server queries
* ============================================================ */
const struct CMUnitTest group1_tests[] = {
/* Connection tests - verify login, roster, bookmarks */
PROF_FUNC_TEST(connect_jid_requests_roster),
PROF_FUNC_TEST(connect_jid_sends_presence_after_receiving_roster),
PROF_FUNC_TEST(connect_jid_requests_bookmarks),
PROF_FUNC_TEST(connect_bad_password),
PROF_FUNC_TEST(connect_shows_presence_updates),
/* Ping tests - XEP-0199 XMPP Ping */
PROF_FUNC_TEST(ping_server),
PROF_FUNC_TEST(ping_server_not_supported),
PROF_FUNC_TEST(ping_responds_to_server_request),
PROF_FUNC_TEST(ping_jid),
PROF_FUNC_TEST(ping_jid_not_supported),
/* Room discovery - XEP-0045 */
PROF_FUNC_TEST(rooms_query),
/* Software Version - XEP-0092 */
PROF_FUNC_TEST(send_software_version_request),
PROF_FUNC_TEST(display_software_version_result),
PROF_FUNC_TEST(shows_message_when_software_version_error),
PROF_FUNC_TEST(display_software_version_result_when_from_domainpart),
PROF_FUNC_TEST(show_message_in_chat_window_when_no_resource),
PROF_FUNC_TEST(display_software_version_result_in_chat),
/* Last Activity - XEP-0012 */
PROF_FUNC_TEST(responds_to_last_activity_request),
PROF_FUNC_TEST(last_activity_request_to_contact),
};
/* ============================================================
* GROUP 2: Message, Receipts, Roster, Chat Session
* Core messaging and contact management
* ============================================================ */
const struct CMUnitTest group2_tests[] = {
/* Basic message send/receive */
PROF_FUNC_TEST(message_send),
PROF_FUNC_TEST(message_receive_console),
PROF_FUNC_TEST(message_receive_chatwin),
/* Message receipts - XEP-0184 */
PROF_FUNC_TEST(does_not_send_receipt_request_to_barejid),
PROF_FUNC_TEST(send_receipt_request),
PROF_FUNC_TEST(send_receipt_on_request),
/* Roster management - add/remove/rename contacts */
PROF_FUNC_TEST(sends_new_item),
PROF_FUNC_TEST(sends_new_item_nick),
PROF_FUNC_TEST(sends_remove_item),
PROF_FUNC_TEST(sends_remove_item_nick),
PROF_FUNC_TEST(sends_nick_change),
/* Chat session management - bare/full JID routing */
PROF_FUNC_TEST(sends_message_to_barejid_when_contact_offline),
PROF_FUNC_TEST(sends_message_to_barejid_when_contact_online),
PROF_FUNC_TEST(sends_message_to_fulljid_when_received_from_fulljid),
PROF_FUNC_TEST(sends_subsequent_messages_to_fulljid),
PROF_FUNC_TEST(resets_to_barejid_after_presence_received),
PROF_FUNC_TEST(new_session_when_message_received_from_different_fulljid),
};
/* ============================================================
* GROUP 3: Presence, Disconnect
* Online/away/xa/dnd/chat status management
* ============================================================ */
const struct CMUnitTest group3_tests[] = {
PROF_FUNC_TEST(presence_online),
PROF_FUNC_TEST(presence_online_with_message),
PROF_FUNC_TEST(presence_away),
PROF_FUNC_TEST(presence_away_with_message),
PROF_FUNC_TEST(presence_online),
PROF_FUNC_TEST(presence_online_with_message),
PROF_FUNC_TEST(presence_xa),
PROF_FUNC_TEST(presence_xa_with_message),
PROF_FUNC_TEST(presence_dnd),
@@ -163,75 +56,57 @@ main(int argc, char* argv[])
PROF_FUNC_TEST(presence_received),
PROF_FUNC_TEST(presence_missing_resource_defaults),
/* Disconnect - clean session termination */
PROF_FUNC_TEST(disconnect_ends_session),
};
PROF_FUNC_TEST(message_send),
PROF_FUNC_TEST(message_receive_console),
PROF_FUNC_TEST(message_receive_chatwin),
/* ============================================================
* GROUP 4: MUC, Carbons
* Multi-user chat and message synchronization
* ============================================================ */
const struct CMUnitTest group4_tests[] = {
/* MUC room join with various options - XEP-0045 */
PROF_FUNC_TEST(sends_room_join),
PROF_FUNC_TEST(sends_room_join_with_nick),
PROF_FUNC_TEST(sends_room_join_with_password),
PROF_FUNC_TEST(sends_room_join_with_nick_and_password),
PROF_FUNC_TEST(sends_message_to_barejid_when_contact_offline),
PROF_FUNC_TEST(sends_message_to_barejid_when_contact_online),
PROF_FUNC_TEST(sends_message_to_fulljid_when_received_from_fulljid),
PROF_FUNC_TEST(sends_subsequent_messages_to_fulljid),
PROF_FUNC_TEST(resets_to_barejid_after_presence_received),
PROF_FUNC_TEST(new_session_when_message_received_from_different_fulljid),
/* MUC room information display */
PROF_FUNC_TEST(shows_role_and_affiliation_on_join),
PROF_FUNC_TEST(shows_subject_on_join),
PROF_FUNC_TEST(shows_occupant_join),
/* MUC messaging */
PROF_FUNC_TEST(shows_message),
PROF_FUNC_TEST(shows_me_message_from_occupant),
PROF_FUNC_TEST(shows_me_message_from_self),
/* MUC console notification settings */
PROF_FUNC_TEST(shows_all_messages_in_console_when_window_not_focussed),
PROF_FUNC_TEST(shows_first_message_in_console_when_window_not_focussed),
PROF_FUNC_TEST(shows_no_message_in_console_when_window_not_focussed),
/* MUC moderation - XEP-0045 room admin */
PROF_FUNC_TEST(sends_affiliation_list_request),
PROF_FUNC_TEST(sends_kick_request),
/* Message Carbons - XEP-0280 (message sync across devices) */
PROF_FUNC_TEST(send_enable_carbons),
PROF_FUNC_TEST(connect_with_carbons_enabled),
PROF_FUNC_TEST(send_disable_carbons),
PROF_FUNC_TEST(receive_carbon),
PROF_FUNC_TEST(receive_self_carbon),
PROF_FUNC_TEST(receive_private_carbon),
PROF_FUNC_TEST(send_receipt_request),
PROF_FUNC_TEST(send_receipt_on_request),
PROF_FUNC_TEST(does_not_send_receipt_request_to_barejid),
PROF_FUNC_TEST(sends_new_item),
PROF_FUNC_TEST(sends_new_item_nick),
PROF_FUNC_TEST(sends_remove_item),
PROF_FUNC_TEST(sends_remove_item_nick),
PROF_FUNC_TEST(sends_nick_change),
PROF_FUNC_TEST(send_software_version_request),
PROF_FUNC_TEST(display_software_version_result),
PROF_FUNC_TEST(shows_message_when_software_version_error),
PROF_FUNC_TEST(display_software_version_result_when_from_domainpart),
PROF_FUNC_TEST(show_message_in_chat_window_when_no_resource),
PROF_FUNC_TEST(display_software_version_result_in_chat),
PROF_FUNC_TEST(sends_room_join),
PROF_FUNC_TEST(sends_room_join_with_nick),
PROF_FUNC_TEST(sends_room_join_with_password),
PROF_FUNC_TEST(sends_room_join_with_nick_and_password),
PROF_FUNC_TEST(shows_role_and_affiliation_on_join),
PROF_FUNC_TEST(shows_subject_on_join),
PROF_FUNC_TEST(shows_history_message),
PROF_FUNC_TEST(shows_occupant_join),
PROF_FUNC_TEST(shows_message),
PROF_FUNC_TEST(shows_me_message_from_occupant),
PROF_FUNC_TEST(shows_me_message_from_self),
PROF_FUNC_TEST(shows_all_messages_in_console_when_window_not_focussed),
PROF_FUNC_TEST(shows_first_message_in_console_when_window_not_focussed),
PROF_FUNC_TEST(shows_no_message_in_console_when_window_not_focussed),
PROF_FUNC_TEST(disconnect_ends_session),
};
/* Test group registry for easy extension */
struct {
const char* name;
const struct CMUnitTest* tests;
size_t count;
} groups[] = {
{ "Group 1: Connect/Ping/Rooms/Software", group1_tests, ARRAY_SIZE(group1_tests) },
{ "Group 2: Message/Receipts/Roster/Session", group2_tests, ARRAY_SIZE(group2_tests) },
{ "Group 3: Presence/Disconnect", group3_tests, ARRAY_SIZE(group3_tests) },
{ "Group 4: MUC/Carbons", group4_tests, ARRAY_SIZE(group4_tests) },
};
const int num_groups = ARRAY_SIZE(groups);
int result = 0;
if (group > 0 && group <= num_groups) {
/* Run specific group */
result = _cmocka_run_group_tests(groups[group - 1].name, groups[group - 1].tests,
groups[group - 1].count, NULL, NULL);
} else {
/* Run all groups sequentially */
for (int i = 0; i < num_groups; i++) {
result |= _cmocka_run_group_tests(groups[i].name, groups[i].tests,
groups[i].count, NULL, NULL);
}
}
return result;
return cmocka_run_group_tests(all_tests, NULL, NULL);
}

View File

@@ -8,43 +8,16 @@
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <pty.h>
#include <fcntl.h>
#include <sys/select.h>
#include <regex.h>
#include <stabber.h>
#include <expect.h>
#include "proftest.h"
/* Number of parallel test groups for CI builds */
#define TEST_GROUPS 4
char *config_orig;
char *data_orig;
int fd = 0;
int stub_port = 5230;
pid_t child_pid = 0;
/*
* Dynamic XDG paths based on stub_port for parallel test execution.
* Each test instance gets unique directories to avoid file conflicts.
*/
char xdg_config_home[256];
char xdg_data_home[256];
/*
* Buffer for accumulating output from profanity.
* 64KB is sufficient for typical test output while keeping memory usage
* reasonable. When full, older half is discarded (ring buffer behavior).
*/
#define OUTPUT_BUF_SIZE 65536
static char output_buffer[OUTPUT_BUF_SIZE];
static size_t output_len = 0;
/* Timeout for expect operations in seconds */
static int expect_timeout = 30;
gboolean
_create_dir(const char *name)
@@ -87,7 +60,7 @@ _mkdir_recursive(const char *dir)
void
_create_config_dir(void)
{
GString *profanity_dir = g_string_new(xdg_config_home);
GString *profanity_dir = g_string_new(XDG_CONFIG_HOME);
g_string_append(profanity_dir, "/profanity");
if (!_mkdir_recursive(profanity_dir->str)) {
@@ -100,7 +73,7 @@ _create_config_dir(void)
void
_create_data_dir(void)
{
GString *profanity_dir = g_string_new(xdg_data_home);
GString *profanity_dir = g_string_new(XDG_DATA_HOME);
g_string_append(profanity_dir, "/profanity");
if (!_mkdir_recursive(profanity_dir->str)) {
@@ -113,7 +86,7 @@ _create_data_dir(void)
void
_create_chatlogs_dir(void)
{
GString *chatlogs_dir = g_string_new(xdg_data_home);
GString *chatlogs_dir = g_string_new(XDG_DATA_HOME);
g_string_append(chatlogs_dir, "/profanity/chatlogs");
if (!_mkdir_recursive(chatlogs_dir->str)) {
@@ -126,7 +99,7 @@ _create_chatlogs_dir(void)
void
_create_logs_dir(void)
{
GString *logs_dir = g_string_new(xdg_data_home);
GString *logs_dir = g_string_new(XDG_DATA_HOME);
g_string_append(logs_dir, "/profanity/logs");
if (!_mkdir_recursive(logs_dir->str)) {
@@ -139,174 +112,42 @@ _create_logs_dir(void)
void
_cleanup_dirs(void)
{
const char *group_env = getenv("PROF_TEST_GROUP");
int group = group_env ? atoi(group_env) : 0;
int dir_id = (group >= 1 && group <= TEST_GROUPS) ? group : stub_port;
printf("[PROF_TEST] Cleaning up directories for group %d (dir_id %d)\n", group, dir_id);
char cmd[512];
snprintf(cmd, sizeof(cmd), "rm -rf ./test-files/%d", dir_id);
int res = system(cmd);
int res = system("rm -rf ./tests/functionaltests/files");
if (res == -1) {
assert_true(FALSE);
}
}
/*
* Read available data from fd into output_buffer with timeout.
* Returns number of bytes read, 0 on timeout, -1 on error.
*/
static int
_read_output(int timeout_ms)
{
fd_set readfds;
struct timeval tv;
FD_ZERO(&readfds);
FD_SET(fd, &readfds);
tv.tv_sec = timeout_ms / 1000;
tv.tv_usec = (timeout_ms % 1000) * 1000;
int ret = select(fd + 1, &readfds, NULL, NULL, &tv);
if (ret <= 0) {
return ret;
}
size_t space = OUTPUT_BUF_SIZE - output_len - 1;
if (space <= 0) {
/* Buffer full, shift content */
memmove(output_buffer, output_buffer + OUTPUT_BUF_SIZE/2, OUTPUT_BUF_SIZE/2);
output_len = OUTPUT_BUF_SIZE/2;
space = OUTPUT_BUF_SIZE - output_len - 1;
}
ssize_t n = read(fd, output_buffer + output_len, space);
if (n > 0) {
output_len += n;
output_buffer[output_len] = '\0';
}
return n;
}
/*
* Custom implementation of exp_spawnl using forkpty.
* This avoids the segfault bug in libexpect on Arch Linux.
*/
void
prof_start(void)
{
struct winsize ws;
ws.ws_row = 24;
ws.ws_col = 300; /* Match COLUMNS=300 from start_profanity.sh */
ws.ws_xpixel = 0;
ws.ws_ypixel = 0;
/* Reset output buffer */
output_len = 0;
output_buffer[0] = '\0';
child_pid = forkpty(&fd, NULL, NULL, &ws);
if (child_pid < 0) {
fd = -1;
return;
}
if (child_pid == 0) {
/* Child process */
setenv("COLUMNS", "300", 1);
setenv("TERM", "xterm", 1);
execl("./profanity", "./profanity", "-l", "DEBUG", NULL);
/* If exec fails */
fprintf(stderr, "execl failed: %s\n", strerror(errno));
_exit(127);
}
/* Parent process */
/* Set non-blocking mode for reading */
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
/* Brief wait for process to initialize */
usleep(50000); /* 50ms */
// helper script sets terminal columns, avoids assertions failing
// based on the test runner terminal size
fd = exp_spawnl("sh",
"sh",
"-c",
"./tests/functionaltests/start_profanity.sh",
NULL);
FILE *fp = fdopen(fd, "r+");
assert_true(fp != NULL);
setbuf(fp, (char *)0);
}
int
init_prof_test(void **state)
{
/* Get test group from environment for static resource allocation */
const char *group_env = getenv("PROF_TEST_GROUP");
int group = group_env ? atoi(group_env) : 0;
/* Get build index for port offset (for parallel CI builds) */
const char *build_env = getenv("PROF_BUILD_INDEX");
int build_idx = build_env ? atoi(build_env) : 0;
/* Calculate port base: each build uses a different range of TEST_GROUPS ports.
* Build 0 (local/default): 5230-5233, Full: 5230-5233, Minimal: 5234-5237, etc.
* Build 0 and Full share the same range because build 0 is for local runs or sequential run (no parallel builds),
* while Full/Minimal/NoEncrypt/Default are used in CI where they run in parallel. */
int port_base = 5230 + ((build_idx > 0 ? build_idx - 1 : 0) * TEST_GROUPS);
/* Static resource allocation to avoid conflicts in parallel execution.
* Group 1-4: use static port assignment.
* Group 0 (all groups): use dynamic allocation as fallback. */
gboolean started = FALSE;
if (group >= 1 && group <= TEST_GROUPS) {
/* Static allocation: each group gets a dedicated port */
stub_port = port_base + group - 1;
printf("[PROF_TEST] Build %d, Group %d: trying port %d\n", build_idx, group, stub_port);
if (stbbr_start(STBBR_LOGDEBUG, stub_port, 0) == 0) {
started = TRUE;
printf("[PROF_TEST] Started stabber on port %d\n", stub_port);
} else {
printf("[PROF_TEST] Failed to start stabber on port %d\n", stub_port);
}
}
/* Fallback to dynamic allocation if static failed or group=0 */
if (!started) {
printf("[PROF_TEST] Using dynamic port allocation\n");
for (int p = port_base; p < port_base + 20; ++p) {
if (stbbr_start(STBBR_LOGDEBUG, p, 0) == 0) {
stub_port = p;
started = TRUE;
printf("[PROF_TEST] Started stabber on port %d\n", stub_port);
break;
}
}
}
if (!started) {
fprintf(stderr, "[PROF_TEST] ERROR: could not start stabber on any port\n");
if (stbbr_start(STBBR_LOGDEBUG ,5230, 0) != 0) {
assert_true(FALSE);
return -1;
}
/* Generate unique XDG paths based on group for parallel execution.
* Use ./test-files/ in current (build) directory for out-of-tree builds compatibility. */
int dir_id = (group >= 1 && group <= TEST_GROUPS) ? group : stub_port;
snprintf(xdg_config_home, sizeof(xdg_config_home),
"./test-files/%d/xdg_config_home", dir_id);
snprintf(xdg_data_home, sizeof(xdg_data_home),
"./test-files/%d/xdg_data_home", dir_id);
printf("[PROF_TEST] Group %d using directories: config=%s, data=%s\n",
group, xdg_config_home, xdg_data_home);
// Give stabber server thread time to start listening
usleep(100000); // 100ms
config_orig = getenv("XDG_CONFIG_HOME");
data_orig = getenv("XDG_DATA_HOME");
setenv("XDG_CONFIG_HOME", xdg_config_home, 1);
setenv("XDG_DATA_HOME", xdg_data_home, 1);
setenv("XDG_CONFIG_HOME", XDG_CONFIG_HOME, 1);
setenv("XDG_DATA_HOME", XDG_DATA_HOME, 1);
_cleanup_dirs();
@@ -315,84 +156,51 @@ init_prof_test(void **state)
_create_chatlogs_dir();
_create_logs_dir();
/* If PROF_FLATFILE=1 is set, write a profrc that selects the flat-file backend */
const char *flatfile_env = getenv("PROF_FLATFILE");
if (flatfile_env && strcmp(flatfile_env, "1") == 0) {
char profrc_path[512];
snprintf(profrc_path, sizeof(profrc_path),
"%s/profanity/profrc", xdg_config_home);
FILE *prc = fopen(profrc_path, "w");
if (prc) {
fprintf(prc, "[logging]\ndblog=flatfile\n");
fclose(prc);
printf("[PROF_TEST] Wrote profrc with dblog=flatfile: %s\n", profrc_path);
}
}
prof_start();
int prof_started = prof_output_regex("CProof\\. Type /help for help information\\.");
assert_true(prof_started);
assert_true(prof_output_exact("Profanity"));
// set UI options to make expect assertions faster and more reliable
prof_input("/inpblock timeout 5");
assert_true(prof_output_regex("Input blocking set to 5 milliseconds"));
assert_true(prof_output_exact("Input blocking set to 5 milliseconds"));
prof_input("/inpblock dynamic off");
assert_true(prof_output_regex("Dynamic input blocking disabled"));
assert_true(prof_output_exact("Dynamic input blocking disabled"));
prof_input("/notify chat off");
assert_true(prof_output_regex("Chat notifications disabled"));
assert_true(prof_output_exact("Chat notifications disabled"));
prof_input("/notify room off");
assert_true(prof_output_regex("Room notifications disabled"));
assert_true(prof_output_exact("Room notifications disabled"));
prof_input("/wrap off");
assert_true(prof_output_regex("Word wrap disabled"));
assert_true(prof_output_exact("Word wrap disabled"));
prof_input("/roster hide");
assert_true(prof_output_regex("Roster disabled"));
assert_true(prof_output_exact("Roster disabled"));
prof_input("/occupants default hide");
assert_true(prof_output_regex("Occupant list disabled"));
assert_true(prof_output_exact("Occupant list disabled"));
prof_input("/time console off");
prof_input("/time console off");
assert_true(prof_output_regex("Console time display disabled\\."));
assert_true(prof_output_exact("Console time display disabled."));
prof_input("/time chat off");
assert_true(prof_output_regex("Chat time display disabled\\."));
assert_true(prof_output_exact("Chat time display disabled."));
prof_input("/time muc off");
assert_true(prof_output_regex("MUC time display disabled\\."));
assert_true(prof_output_exact("MUC time display disabled."));
prof_input("/time config off");
assert_true(prof_output_regex("Config time display disabled\\."));
assert_true(prof_output_exact("config time display disabled."));
prof_input("/time private off");
assert_true(prof_output_regex("Private chat time display disabled\\."));
assert_true(prof_output_exact("Private chat time display disabled."));
prof_input("/time xml off");
assert_true(prof_output_regex("XML Console time display disabled\\."));
assert_true(prof_output_exact("XML Console time display disabled."));
return 0;
}
int
close_prof_test(void **state)
{
if (fd > 0 && child_pid > 0) {
prof_input("/quit");
// Give profanity time to process quit command
sleep(1);
waitpid(child_pid, NULL, 0);
close(fd);
fd = 0;
child_pid = 0;
}
prof_input("/quit");
waitpid(exp_pid, NULL, 0);
_cleanup_dirs();
if (config_orig) {
setenv("XDG_CONFIG_HOME", config_orig, 1);
}
if (data_orig) {
setenv("XDG_DATA_HOME", data_orig, 1);
}
setenv("XDG_CONFIG_HOME", config_orig, 1);
setenv("XDG_DATA_HOME", data_orig, 1);
stbbr_stop();
/*
* TODO: Replace with proper synchronization.
* stabber doesn't provide wait_stopped() API yet, so we use delay
* to ensure the port is released before the next test starts.
* See: https://git.jabber.space/devs/stabber/issues/3
*/
usleep(100000); // 100ms
return 0;
}
@@ -401,85 +209,20 @@ prof_input(const char *input)
{
GString *inp_str = g_string_new(input);
g_string_append(inp_str, "\r");
ssize_t _wn = write(fd, inp_str->str, inp_str->len);
(void)_wn;
write(fd, inp_str->str, inp_str->len);
g_string_free(inp_str, TRUE);
/* Small delay to let profanity process input */
usleep(10000);
}
/*
* Wait for exact text to appear in output.
* Returns 1 if found, 0 if timeout.
*/
int
prof_output_exact(const char *text)
{
time_t start = time(NULL);
while (time(NULL) - start < expect_timeout) {
/* Read any available output */
while (_read_output(100) > 0) {
/* Keep reading while data available */
}
/* Check if text is in buffer */
if (strstr(output_buffer, text) != NULL) {
return 1;
}
usleep(50000); /* 50ms */
}
return 0;
return (1 == exp_expectl(fd, exp_exact, text, 1, exp_end));
}
/*
* Wait for regex pattern to match in output.
* Returns 1 if found, 0 if timeout.
*/
int
prof_output_regex(const char *pattern)
prof_output_regex(const char *text)
{
regex_t regex;
int ret;
ret = regcomp(&regex, pattern, REG_EXTENDED | REG_NOSUB);
if (ret != 0) {
return 0;
}
time_t start = time(NULL);
while (time(NULL) - start < expect_timeout) {
/* Read any available output */
while (_read_output(100) > 0) {
/* Keep reading while data available */
}
/* Check if pattern matches */
ret = regexec(&regex, output_buffer, 0, NULL, 0);
if (ret == 0) {
regfree(&regex);
return 1;
}
usleep(50000); /* 50ms */
}
/* Timeout reached - log diagnostic info */
fprintf(stderr, "Timeout waiting for regex '%s' after %d seconds. Last output:\n", pattern, expect_timeout);
size_t len = strlen(output_buffer);
if (len > 500) {
fprintf(stderr, "...%s", output_buffer + len - 500);
} else {
fprintf(stderr, "%s", output_buffer);
}
fprintf(stderr, "\n");
regfree(&regex);
return 0;
return (1 == exp_expectl(fd, exp_regexp, text, 1, exp_end));
}
void
@@ -498,40 +241,33 @@ prof_connect_with_roster(const char *roster)
stbbr_for_query("jabber:iq:roster", roster_str->str);
g_string_free(roster_str, TRUE);
stbbr_auth_passwd("password");
stbbr_for_id("prof_presence_1",
"<presence id='prof_presence_1' lang='en' to='stabber@localhost/profanity' from='stabber@localhost/profanity'>"
"<priority>0</priority>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='http://profanity-im.github.io/' ver='f8mrtdyAmhnj8Ca+630bThSL718='/>"
"</presence>"
);
char connect_cmd[128];
snprintf(connect_cmd, sizeof(connect_cmd), "/connect stabber@localhost server 127.0.0.1 port %d tls disable auth legacy", stub_port);
prof_input(connect_cmd);
assert_true(prof_output_regex("password:"));
prof_input("/connect stabber@localhost server 127.0.0.1 port 5230 tls allow");
prof_input("password");
expect_timeout = 60;
assert_true(prof_output_regex("Connecting as stabber@localhost"));
assert_true(prof_output_regex("logged in successfully"));
assert_true(prof_output_regex(".+online.+ \\(priority 0\\)\\."));
expect_timeout = 60;
// Wait for presence stanza to be sent (content-based, not ID-based)
// Match the actual attribute order from stanza_attach_caps
assert_true(stbbr_received(
"<presence id=\"*\">"
"<c xmlns=\"http://jabber.org/protocol/caps\" hash=\"sha-1\" node=\"http://profanity-im.github.io\" ver=\"*\"/>"
"</presence>"
));
// Allow time for profanity to connect
exp_timeout = 30;
assert_true(prof_output_regex("stabber@localhost/profanity logged in successfully, .+online.+ \\(priority 0\\)\\."));
exp_timeout = 10;
stbbr_wait_for("prof_presence_*");
}
void
prof_timeout(int timeout)
{
expect_timeout = timeout;
exp_timeout = timeout;
}
void
prof_timeout_reset(void)
{
expect_timeout = 60;
exp_timeout = 10;
}
void

View File

@@ -1,15 +1,8 @@
#ifndef __H_PROFTEST
#define __H_PROFTEST
/*
* XDG paths are dynamic and generated per-test based on stub_port.
* Each test instance uses unique directories (./tests/functionaltests/files/{port}/xdg_*)
* to allow parallel test execution without file conflicts.
*/
extern char xdg_config_home[256];
extern char xdg_data_home[256];
extern int stub_port;
#define XDG_CONFIG_HOME "./tests/functionaltests/files/xdg_config_home"
#define XDG_DATA_HOME "./tests/functionaltests/files/xdg_data_home"
int init_prof_test(void **state);
int close_prof_test(void **state);

View File

@@ -4,6 +4,7 @@
#include <string.h>
#include <stabber.h>
#include <expect.h>
#include "proftest.h"

View File

@@ -1,10 +1,10 @@
#include <glib.h>
#include <unistd.h>
#include "prof_cmocka.h"
#include <stdlib.h>
#include <string.h>
#include <stabber.h>
#include <expect.h>
#include "proftest.h"
@@ -144,11 +144,7 @@ resets_to_barejid_after_presence_received(void **state)
"<show>dnd</show>"
"</presence>"
);
// Wait for presence to be processed before sending next message.
// The presence output may appear in console or chat window depending on focus,
// so we use a small delay to ensure the session is reset to barejid.
// Under Valgrind, processing is slower, so this delay is necessary.
g_usleep(500000); // 500ms
assert_true(prof_output_exact("Buddy1 (laptop) is dnd"));
prof_input("/msg buddy1@localhost Outgoing 2");
assert_true(stbbr_received(

View File

@@ -2,9 +2,9 @@
#include "prof_cmocka.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <stabber.h>
#include <expect.h>
#include "proftest.h"
@@ -47,9 +47,7 @@ connect_jid_requests_bookmarks(void **state)
void
connect_bad_password(void **state)
{
char connect_cmd[128];
snprintf(connect_cmd, sizeof(connect_cmd), "/connect stabber@localhost server 127.0.0.1 port %d tls allow", stub_port);
prof_input(connect_cmd);
prof_input("/connect stabber@localhost server 127.0.0.1 port 5230 tls allow");
prof_input("badpassword");
assert_true(prof_output_exact("Login failed."));

View File

@@ -4,6 +4,7 @@
#include <string.h>
#include <stabber.h>
#include <expect.h>
#include "proftest.h"

View File

@@ -1,63 +0,0 @@
/*
* test_lastactivity.c
* Functional tests for Last Activity (XEP-0012)
*/
#include <glib.h>
#include "prof_cmocka.h"
#include <stdlib.h>
#include <string.h>
#include <stabber.h>
#include "proftest.h"
void
responds_to_last_activity_request(void **state)
{
prof_connect();
// Send incoming last activity request
stbbr_send(
"<iq id='last1' type='get' to='stabber@localhost/profanity' from='buddy1@localhost/mobile'>"
"<query xmlns='jabber:iq:last'/>"
"</iq>"
);
// Verify that CProof responds with last activity info
// The 'seconds' attribute indicates idle time
assert_true(stbbr_received(
"<iq id='last1' type='result' to='buddy1@localhost/mobile'>"
"<query xmlns='jabber:iq:last' seconds='*'/>"
"</iq>"
));
}
void
last_activity_request_to_contact(void **state)
{
prof_connect();
stbbr_send(
"<presence to='stabber@localhost' from='buddy1@localhost/mobile'>"
"<priority>10</priority>"
"<status>I'm here</status>"
"</presence>"
);
assert_true(prof_output_exact("Buddy1 (mobile) is online, \"I'm here\""));
// Register response for last activity query
stbbr_for_query("jabber:iq:last",
"<iq id='*' type='result' from='buddy1@localhost/mobile' to='stabber@localhost/profanity'>"
"<query xmlns='jabber:iq:last' seconds='120'/>"
"</iq>"
);
prof_input("/lastactivity get buddy1@localhost/mobile");
// Verify the request was sent
assert_true(stbbr_received(
"<iq id='*' to='buddy1@localhost/mobile' type='get'>"
"<query xmlns='jabber:iq:last'/>"
"</iq>"
));
}

View File

@@ -1,7 +0,0 @@
/*
* test_lastactivity.h
* Header for Last Activity tests (XEP-0012)
*/
void responds_to_last_activity_request(void **state);
void last_activity_request_to_contact(void **state);

View File

@@ -4,6 +4,7 @@
#include <string.h>
#include <stabber.h>
#include <expect.h>
#include "proftest.h"

View File

@@ -1,21 +1,13 @@
#include <glib.h>
#include <unistd.h>
#include "prof_cmocka.h"
#include <stdlib.h>
#include <string.h>
#include <stabber.h>
#include <expect.h>
#include "proftest.h"
/*
* NOTE: We use prof_output_regex() throughout this file even for seemingly
* exact strings because some output may include timestamps, color codes,
* or other dynamic content depending on configuration. Using regex provides
* more robust matching. For patterns without regex metacharacters, the
* performance difference is negligible.
*/
void
sends_room_join(void **state)
{
@@ -23,7 +15,7 @@ sends_room_join(void **state)
prof_input("/join testroom@conference.localhost");
assert_true(stbbr_received(
assert_true(stbbr_last_received(
"<presence id='*' to='testroom@conference.localhost/stabber'>"
"<x xmlns='http://jabber.org/protocol/muc'/>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
@@ -38,7 +30,7 @@ sends_room_join_with_nick(void **state)
prof_input("/join testroom@conference.localhost nick testnick");
assert_true(stbbr_received(
assert_true(stbbr_last_received(
"<presence id='*' to='testroom@conference.localhost/testnick'>"
"<x xmlns='http://jabber.org/protocol/muc'/>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
@@ -53,7 +45,7 @@ sends_room_join_with_password(void **state)
prof_input("/join testroom@conference.localhost password testpassword");
assert_true(stbbr_received(
assert_true(stbbr_last_received(
"<presence id='*' to='testroom@conference.localhost/stabber'>"
"<x xmlns='http://jabber.org/protocol/muc'>"
"<password>testpassword</password>"
@@ -70,7 +62,7 @@ sends_room_join_with_nick_and_password(void **state)
prof_input("/join testroom@conference.localhost nick testnick password testpassword");
assert_true(stbbr_received(
assert_true(stbbr_last_received(
"<presence id='*' to='testroom@conference.localhost/testnick'>"
"<x xmlns='http://jabber.org/protocol/muc'>"
"<password>testpassword</password>"
@@ -85,8 +77,8 @@ shows_role_and_affiliation_on_join(void **state)
{
prof_connect();
stbbr_for_presence_to("testroom@conference.localhost/stabber",
"<presence id='*' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
stbbr_for_id("prof_join_4",
"<presence id='prof_join_4' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='http://profanity-im.github.io' ver='*'/>"
"<x xmlns='http://jabber.org/protocol/muc#user'>"
"<item role='participant' jid='stabber@localhost/profanity' affiliation='none'/>"
@@ -97,7 +89,7 @@ shows_role_and_affiliation_on_join(void **state)
prof_input("/join testroom@conference.localhost");
assert_true(prof_output_regex("-> You have joined the room as stabber, role: participant, affiliation: none"));
assert_true(prof_output_exact("-> You have joined the room as stabber, role: participant, affiliation: none"));
}
void
@@ -105,8 +97,8 @@ shows_subject_on_join(void **state)
{
prof_connect();
stbbr_for_presence_to("testroom@conference.localhost/stabber",
"<presence id='*' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
stbbr_for_id("prof_join_4",
"<presence id='prof_join_4' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='http://profanity-im.github.io' ver='*'/>"
"<x xmlns='http://jabber.org/protocol/muc#user'>"
"<item role='participant' jid='stabber@localhost/profanity' affiliation='none'/>"
@@ -116,7 +108,7 @@ shows_subject_on_join(void **state)
);
prof_input("/join testroom@conference.localhost");
assert_true(prof_output_regex("-> You have joined the room as stabber, role: participant, affiliation: none"));
assert_true(prof_output_exact("-> You have joined the room as stabber, role: participant, affiliation: none"));
stbbr_send(
"<message type='groupchat' to='stabber@localhost/profanity' from='testroom@conference.localhost'>"
@@ -133,8 +125,8 @@ shows_history_message(void **state)
{
prof_connect();
stbbr_for_presence_to("testroom@conference.localhost/stabber",
"<presence id='*' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
stbbr_for_id("prof_join_4",
"<presence id='prof_join_4' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='http://profanity-im.github.io' ver='*'/>"
"<x xmlns='http://jabber.org/protocol/muc#user'>"
"<item role='participant' jid='stabber@localhost/profanity' affiliation='none'/>"
@@ -144,7 +136,7 @@ shows_history_message(void **state)
);
prof_input("/join testroom@conference.localhost");
assert_true(prof_output_regex("-> You have joined the room as stabber, role: participant, affiliation: none"));
assert_true(prof_output_exact("-> You have joined the room as stabber, role: participant, affiliation: none"));
stbbr_send(
"<message type='groupchat' to='stabber@localhost/profanity' from='testroom@conference.localhost/testoccupant'>"
@@ -154,9 +146,7 @@ shows_history_message(void **state)
"</message>"
);
prof_timeout(10);
assert_true(prof_output_regex("testoccupant:.*an old message"));
prof_timeout_reset();
assert_true(prof_output_regex("testoccupant: an old message"));
}
void
@@ -164,12 +154,8 @@ shows_occupant_join(void **state)
{
prof_connect();
// Enable MUC status messages to see occupant join/leave
prof_input("/presence room all");
assert_true(prof_output_regex("All presence updates will appear"));
stbbr_for_presence_to("testroom@conference.localhost/stabber",
"<presence id='*' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
stbbr_for_id("prof_join_4",
"<presence id='prof_join_4' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='http://profanity-im.github.io' ver='*'/>"
"<x xmlns='http://jabber.org/protocol/muc#user'>"
"<item role='participant' jid='stabber@localhost/profanity' affiliation='none'/>"
@@ -179,7 +165,7 @@ shows_occupant_join(void **state)
);
prof_input("/join testroom@conference.localhost");
assert_true(prof_output_regex("-> You have joined the room as stabber, role: participant, affiliation: none"));
assert_true(prof_output_exact("-> You have joined the room as stabber, role: participant, affiliation: none"));
stbbr_send(
"<presence to='stabber@localhost/profanity' from='testroom@conference.localhost/testoccupant'>"
@@ -188,9 +174,8 @@ shows_occupant_join(void **state)
"</x>"
"</presence>"
);
sleep(1);
assert_true(prof_output_regex("testoccupant has joined"));
assert_true(prof_output_exact("-> testoccupant has joined the room, role: participant, affiliation: none"));
}
void
@@ -198,8 +183,8 @@ shows_message(void **state)
{
prof_connect();
stbbr_for_presence_to("testroom@conference.localhost/stabber",
"<presence id='*' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
stbbr_for_id("prof_join_4",
"<presence id='prof_join_4' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='http://profanity-im.github.io' ver='*'/>"
"<x xmlns='http://jabber.org/protocol/muc#user'>"
"<item role='participant' jid='stabber@localhost/profanity' affiliation='none'/>"
@@ -209,7 +194,7 @@ shows_message(void **state)
);
prof_input("/join testroom@conference.localhost");
assert_true(prof_output_regex("-> You have joined the room as stabber, role: participant, affiliation: none"));
assert_true(prof_output_exact("-> You have joined the room as stabber, role: participant, affiliation: none"));
stbbr_send(
"<message type='groupchat' to='stabber@localhost/profanity' from='testroom@conference.localhost/testoccupant'>"
@@ -225,8 +210,8 @@ shows_me_message_from_occupant(void **state)
{
prof_connect();
stbbr_for_presence_to("testroom@conference.localhost/stabber",
"<presence id='*' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
stbbr_for_id("prof_join_4",
"<presence id='prof_join_4' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='http://profanity-im.github.io' ver='*'/>"
"<x xmlns='http://jabber.org/protocol/muc#user'>"
"<item role='participant' jid='stabber@localhost/profanity' affiliation='none'/>"
@@ -236,7 +221,7 @@ shows_me_message_from_occupant(void **state)
);
prof_input("/join testroom@conference.localhost");
assert_true(prof_output_regex("-> You have joined the room as stabber, role: participant, affiliation: none"));
assert_true(prof_output_exact("-> You have joined the room as stabber, role: participant, affiliation: none"));
stbbr_send(
"<message type='groupchat' to='stabber@localhost/profanity' from='testroom@conference.localhost/testoccupant'>"
@@ -244,7 +229,7 @@ shows_me_message_from_occupant(void **state)
"</message>"
);
assert_true(prof_output_regex("\\*testoccupant did something"));
assert_true(prof_output_exact("*testoccupant did something"));
}
void
@@ -252,8 +237,8 @@ shows_me_message_from_self(void **state)
{
prof_connect();
stbbr_for_presence_to("testroom@conference.localhost/stabber",
"<presence id='*' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
stbbr_for_id("prof_join_4",
"<presence id='prof_join_4' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='http://profanity-im.github.io' ver='*'/>"
"<x xmlns='http://jabber.org/protocol/muc#user'>"
"<item role='participant' jid='stabber@localhost/profanity' affiliation='none'/>"
@@ -263,7 +248,7 @@ shows_me_message_from_self(void **state)
);
prof_input("/join testroom@conference.localhost");
assert_true(prof_output_regex("-> You have joined the room as stabber, role: participant, affiliation: none"));
assert_true(prof_output_exact("-> You have joined the room as stabber, role: participant, affiliation: none"));
stbbr_send(
"<message type='groupchat' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
@@ -271,7 +256,7 @@ shows_me_message_from_self(void **state)
"</message>"
);
assert_true(prof_output_regex("\\*stabber did something"));
assert_true(prof_output_exact("*stabber did something"));
}
void
@@ -279,8 +264,8 @@ shows_all_messages_in_console_when_window_not_focussed(void **state)
{
prof_connect();
stbbr_for_presence_to("testroom@conference.localhost/stabber",
"<presence id='*' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
stbbr_for_id("prof_join_4",
"<presence id='prof_join_4' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='http://profanity-im.github.io' ver='*'/>"
"<x xmlns='http://jabber.org/protocol/muc#user'>"
"<item role='participant' jid='stabber@localhost/profanity' affiliation='none'/>"
@@ -290,10 +275,10 @@ shows_all_messages_in_console_when_window_not_focussed(void **state)
);
prof_input("/join testroom@conference.localhost");
assert_true(prof_output_regex("-> You have joined the room as stabber, role: participant, affiliation: none"));
assert_true(prof_output_exact("-> You have joined the room as stabber, role: participant, affiliation: none"));
prof_input("/win 1");
assert_true(prof_output_regex("CProof\\. Type /help for help information\\."));
assert_true(prof_output_exact("CProof. Type /help for help information."));
stbbr_send(
"<message type='groupchat' to='stabber@localhost/profanity' from='testroom@conference.localhost/testoccupant'>"
@@ -301,7 +286,7 @@ shows_all_messages_in_console_when_window_not_focussed(void **state)
"</message>"
);
assert_true(prof_output_regex("<< room message: testoccupant in testroom@conference\\.localhost \\(win 2\\)"));
assert_true(prof_output_exact("<< room message: testoccupant in testroom@conference.localhost (win 2)"));
stbbr_send(
"<message type='groupchat' to='stabber@localhost/profanity' from='testroom@conference.localhost/anotheroccupant'>"
@@ -309,7 +294,7 @@ shows_all_messages_in_console_when_window_not_focussed(void **state)
"</message>"
);
assert_true(prof_output_regex("<< room message: anotheroccupant in testroom@conference\\.localhost \\(win 2\\)"));
assert_true(prof_output_exact("<< room message: anotheroccupant in testroom@conference.localhost (win 2)"));
}
void
@@ -318,10 +303,10 @@ shows_first_message_in_console_when_window_not_focussed(void **state)
prof_connect();
prof_input("/console muc first");
assert_true(prof_output_regex("Console MUC messages set: first"));
assert_true(prof_output_exact("Console MUC messages set: first"));
stbbr_for_presence_to("testroom@conference.localhost/stabber",
"<presence id='*' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
stbbr_for_id("prof_join_4",
"<presence id='prof_join_4' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='http://profanity-im.github.io' ver='*'/>"
"<x xmlns='http://jabber.org/protocol/muc#user'>"
"<item role='participant' jid='stabber@localhost/profanity' affiliation='none'/>"
@@ -331,22 +316,21 @@ shows_first_message_in_console_when_window_not_focussed(void **state)
);
prof_input("/join testroom@conference.localhost");
assert_true(prof_output_regex("-> You have joined the room as stabber, role: participant, affiliation: none"));
assert_true(prof_output_exact("-> You have joined the room as stabber, role: participant, affiliation: none"));
prof_input("/win 1");
assert_true(prof_output_regex("CProof\\. Type /help for help information\\."));
assert_true(prof_output_exact("CProof. Type /help for help information."));
stbbr_send(
"<message type='groupchat' to='stabber@localhost/profanity' from='testroom@conference.localhost/testoccupant'>"
"<body>a new message</body>"
"</message>"
);
sleep(1);
assert_true(prof_output_regex("room message.*testroom@conference\\.localhost"));
assert_true(prof_output_exact("<< room message: testroom@conference.localhost (win 2)"));
prof_input("/clear");
prof_input("/about");
assert_true(prof_output_regex("Type '/help' to show complete help\\."));
assert_true(prof_output_exact("Type '/help' to show complete help."));
stbbr_send(
"<message type='groupchat' to='stabber@localhost/profanity' from='testroom@conference.localhost/anotheroccupant'>"
@@ -355,7 +339,7 @@ shows_first_message_in_console_when_window_not_focussed(void **state)
);
prof_timeout(2);
assert_false(prof_output_regex("<< room message: testroom@conference\\.localhost \\(win 2\\)"));
assert_false(prof_output_exact("<< room message: testroom@conference.localhost (win 2)"));
prof_timeout_reset();
}
@@ -365,10 +349,10 @@ shows_no_message_in_console_when_window_not_focussed(void **state)
prof_connect();
prof_input("/console muc none");
assert_true(prof_output_regex("Console MUC messages set: none"));
assert_true(prof_output_exact("Console MUC messages set: none"));
stbbr_for_presence_to("testroom@conference.localhost/stabber",
"<presence id='*' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
stbbr_for_id("prof_join_4",
"<presence id='prof_join_4' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='http://profanity-im.github.io' ver='*'/>"
"<x xmlns='http://jabber.org/protocol/muc#user'>"
"<item role='participant' jid='stabber@localhost/profanity' affiliation='none'/>"
@@ -378,10 +362,10 @@ shows_no_message_in_console_when_window_not_focussed(void **state)
);
prof_input("/join testroom@conference.localhost");
assert_true(prof_output_regex("-> You have joined the room as stabber, role: participant, affiliation: none"));
assert_true(prof_output_exact("-> You have joined the room as stabber, role: participant, affiliation: none"));
prof_input("/win 1");
assert_true(prof_output_regex("CProof\\. Type /help for help information\\."));
assert_true(prof_output_exact("CProof. Type /help for help information."));
stbbr_send(
"<message type='groupchat' to='stabber@localhost/profanity' from='testroom@conference.localhost/testoccupant'>"
@@ -390,86 +374,6 @@ shows_no_message_in_console_when_window_not_focussed(void **state)
);
prof_timeout(2);
assert_false(prof_output_regex("testroom@conference\\.localhost \\(win 2\\)"));
assert_false(prof_output_exact("testroom@conference.localhost (win 2)"));
prof_timeout_reset();
}
void
sends_affiliation_list_request(void **state)
{
prof_connect();
stbbr_for_presence_to("testroom@conference.localhost/stabber",
"<presence id='*' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='http://profanity-im.github.io' ver='*'/>"
"<x xmlns='http://jabber.org/protocol/muc#user'>"
"<item role='moderator' jid='stabber@localhost/profanity' affiliation='owner'/>"
"</x>"
"<status code='110'/>"
"</presence>"
);
prof_input("/join testroom@conference.localhost");
assert_true(prof_output_regex("-> You have joined the room as stabber, role: moderator, affiliation: owner"));
prof_input("/affiliation owner list");
assert_true(stbbr_received(
"<iq id='*' to='testroom@conference.localhost' type='get'>"
"<query xmlns='http://jabber.org/protocol/muc#admin'>"
"<item affiliation='owner'/>"
"</query>"
"</iq>"
));
}
void
sends_kick_request(void **state)
{
prof_connect();
// Enable MUC presence messages to see occupant join/leave
prof_input("/presence room all");
assert_true(prof_output_regex("All presence updates will appear"));
stbbr_for_presence_to("testroom@conference.localhost/stabber",
"<presence id='*' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='http://profanity-im.github.io' ver='*'/>"
"<x xmlns='http://jabber.org/protocol/muc#user'>"
"<item role='moderator' jid='stabber@localhost/profanity' affiliation='admin'/>"
"</x>"
"<status code='110'/>"
"</presence>"
);
prof_input("/join testroom@conference.localhost");
assert_true(prof_output_regex("-> You have joined the room as stabber, role: moderator, affiliation: admin"));
// Simulate another user in the room
stbbr_send(
"<presence to='stabber@localhost/profanity' from='testroom@conference.localhost/baduser'>"
"<x xmlns='http://jabber.org/protocol/muc#user'>"
"<item role='participant' jid='baduser@localhost/phone' affiliation='none'/>"
"</x>"
"</presence>"
);
sleep(1);
assert_true(prof_output_regex("baduser has joined"));
// Register success response for kick
stbbr_for_query("http://jabber.org/protocol/muc#admin",
"<iq id='*' type='result' from='testroom@conference.localhost'/>"
);
prof_input("/kick baduser \"spamming\"");
assert_true(stbbr_received(
"<iq id='*' to='testroom@conference.localhost' type='set'>"
"<query xmlns='http://jabber.org/protocol/muc#admin'>"
"<item nick='baduser' role='none'>"
"<reason>spamming</reason>"
"</item>"
"</query>"
"</iq>"
));
}

View File

@@ -12,5 +12,3 @@ void shows_me_message_from_self(void **state);
void shows_all_messages_in_console_when_window_not_focussed(void **state);
void shows_first_message_in_console_when_window_not_focussed(void **state);
void shows_no_message_in_console_when_window_not_focussed(void **state);
void sends_affiliation_list_request(void **state);
void sends_kick_request(void **state);

View File

@@ -2,26 +2,17 @@
#include "prof_cmocka.h"
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stabber.h>
#include <expect.h>
#include "proftest.h"
void
ping_server(void **state)
{
/*
* This test verifies that profanity correctly handles the /ping command
* when the server supports ping (urn:xmpp:ping feature).
*
* We register disco#info response with ping support, then verify
* profanity sends ping and receives "Pinged server" confirmation.
*/
// Register disco#info response that includes ping support
stbbr_for_query("http://jabber.org/protocol/disco#info",
"<iq to='stabber@localhost/profanity' type='result' from='localhost'>"
stbbr_for_id("prof_disco_info_onconnect_2",
"<iq id='prof_disco_info_onconnect_2' to='stabber@localhost/profanity' type='result' from='localhost'>"
"<query xmlns='http://jabber.org/protocol/disco#info'>"
"<identity category='server' type='im' name='Prosody'/>"
"<feature var='urn:xmpp:ping'/>"
@@ -29,33 +20,37 @@ ping_server(void **state)
"</iq>"
);
// Register ping response
stbbr_for_query("urn:xmpp:ping",
"<iq from='localhost' to='stabber@localhost/profanity' type='result'/>"
stbbr_for_id("prof_ping_4",
"<iq id='prof_ping_4' type='result' to='stabber@localhost/profanity'/>"
);
stbbr_for_id("prof_ping_5",
"<iq id='prof_ping_5' type='result' to='stabber@localhost/profanity'/>"
);
prof_connect();
/*
* Wait for disco#info exchange to complete.
* TODO: Replace with proper wait mechanism (e.g., wait for capabilities
* to be cached). Currently we use sleep to ensure disco#info response
* is processed before sending /ping command.
*/
sleep(3);
prof_input("/ping");
prof_timeout(10);
assert_true(prof_output_regex("Pinged server"));
prof_timeout_reset();
assert_true(stbbr_received(
"<iq id='prof_ping_4' type='get'>"
"<ping xmlns='urn:xmpp:ping'/>"
"</iq>"
));
assert_true(prof_output_exact("Ping response from server"));
prof_input("/ping");
assert_true(stbbr_received(
"<iq id='prof_ping_5' type='get'>"
"<ping xmlns='urn:xmpp:ping'/>"
"</iq>"
));
assert_true(prof_output_exact("Ping response from server"));
}
void
ping_server_not_supported(void **state)
{
stbbr_for_query("http://jabber.org/protocol/disco#info",
"<iq to='stabber@localhost/profanity' type='result' from='localhost'>"
stbbr_for_id("prof_disco_info_onconnect_2",
"<iq id='prof_disco_info_onconnect_2' to='stabber@localhost/profanity' type='result' from='localhost'>"
"<query xmlns='http://jabber.org/protocol/disco#info'>"
"<identity category='server' type='im' name='Stabber'/>"
"</query>"
@@ -65,7 +60,7 @@ ping_server_not_supported(void **state)
prof_connect();
prof_input("/ping");
assert_true(prof_output_regex("Server does not support ping requests"));
assert_true(prof_output_exact("Server does not support ping requests."));
}
void
@@ -74,21 +69,20 @@ ping_responds_to_server_request(void **state)
prof_connect();
stbbr_send(
"<iq id=\"pingtest1\" type=\"get\" to=\"stabber@localhost/profanity\" from=\"localhost\">"
"<ping xmlns=\"urn:xmpp:ping\"/>"
"<iq id='pingtest1' type='get' to='stabber@localhost/profanity' from='localhost'>"
"<ping xmlns='urn:xmpp:ping'/>"
"</iq>"
);
assert_true(stbbr_received(
"<iq id='pingtest1' type='result' from='stabber@localhost/profanity' to='localhost'></iq>"
"<iq id='pingtest1' type='result' from='stabber@localhost/profanity' to='localhost'/>"
));
}
void ping_jid(void **state)
{
// Caps/disco info response without relying on a fixed id
stbbr_for_query("http://jabber.org/protocol/disco#info",
"<iq to='stabber@localhost/profanity' type='result' from='buddy1@localhost/mobile'>"
stbbr_for_id("prof_caps_4",
"<iq id='prof_caps_4' to='stabber@localhost/profanity' type='result' from='buddy1@localhost/mobile'>"
"<query xmlns='http://jabber.org/protocol/disco#info' node='http://profanity-im.github.io#LpT2xs3nun7jC2sq4gg3WRDQFZ4='>"
"<identity category='client' type='console' name='Profanity0.6.0'/>"
"<feature var='urn:xmpp:ping'/>"
@@ -115,30 +109,29 @@ void ping_jid(void **state)
assert_true(prof_output_exact("Buddy1 (mobile) is online, \"I'm here\""));
assert_true(stbbr_received(
"<iq id='*' to='buddy1@localhost/mobile' type='get'>"
"<iq id='prof_caps_4' to='buddy1@localhost/mobile' type='get'>"
"<query xmlns='http://jabber.org/protocol/disco#info' node='http://profanity-im.github.io#LpT2xs3nun7jC2sq4gg3WRDQFZ4='/>"
"</iq>"
));
// Respond to ping result regardless of id
stbbr_for_query("urn:xmpp:ping",
"<iq from='buddy1@localhost/mobile' to='stabber@localhost' type='result'/>"
stbbr_for_id("prof_ping_5",
"<iq from='buddy1@localhost/mobile' to='stabber@localhost' id='prof_ping_5' type='result'/>"
);
prof_input("/ping buddy1@localhost/mobile");
assert_true(stbbr_received(
"<iq id='*' type='get' to='buddy1@localhost/mobile'>"
"<iq id='prof_ping_5' type='get' to='buddy1@localhost/mobile'>"
"<ping xmlns='urn:xmpp:ping'/>"
"</iq>"
));
assert_true(prof_output_regex("Ping response from buddy1@localhost/mobile"));
assert_true(prof_output_exact("Ping response from buddy1@localhost/mobile"));
}
void ping_jid_not_supported(void **state)
{
stbbr_for_query("http://jabber.org/protocol/disco#info",
"<iq to='stabber@localhost/profanity' type='result' from='buddy1@localhost/mobile'>"
stbbr_for_id("prof_caps_4",
"<iq id='prof_caps_4' to='stabber@localhost/profanity' type='result' from='buddy1@localhost/mobile'>"
"<query xmlns='http://jabber.org/protocol/disco#info' node='http://profanity-im.github.io#LpT2xs3nun7jC2sq4gg3WRDQFZ4='>"
"<identity category='client' type='console' name='Profanity0.6.0'/>"
"<feature var='http://jabber.org/protocol/disco#info'/>"
@@ -164,7 +157,7 @@ void ping_jid_not_supported(void **state)
assert_true(prof_output_exact("Buddy1 (mobile) is online, \"I'm here\""));
assert_true(stbbr_received(
"<iq id='*' to='buddy1@localhost/mobile' type='get'>"
"<iq id='prof_caps_4' to='buddy1@localhost/mobile' type='get'>"
"<query xmlns='http://jabber.org/protocol/disco#info' node='http://profanity-im.github.io#LpT2xs3nun7jC2sq4gg3WRDQFZ4='/>"
"</iq>"
));

View File

@@ -4,6 +4,7 @@
#include <string.h>
#include <stabber.h>
#include <expect.h>
#include "proftest.h"
@@ -12,16 +13,15 @@ presence_online(void **state)
{
prof_connect();
prof_input("/status set online");
assert_true(prof_output_exact("Status set to online (priority 0), \"online\"."));
prof_input("/online");
assert_true(stbbr_received(
"<presence id='*'>"
"<status>online</status>"
"<presence id='prof_presence_3'>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"</presence>"
));
assert_true(prof_output_exact("Status set to online (priority 0)"));
}
void
@@ -29,16 +29,16 @@ presence_online_with_message(void **state)
{
prof_connect();
prof_input("/status set online \"Hi there\"");
assert_true(prof_output_exact("Status set to online (priority 0), \"Hi there\"."));
prof_input("/online \"Hi there\"");
assert_true(stbbr_received(
"<presence id='*'>"
"<presence id='prof_presence_4'>"
"<status>Hi there</status>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"</presence>"
));
assert_true(prof_output_exact("Status set to online (priority 0), \"Hi there\"."));
}
void
@@ -46,17 +46,16 @@ presence_away(void **state)
{
prof_connect();
prof_input("/status set away");
assert_true(prof_output_exact("Status set to away (priority 0), \"away\"."));
prof_input("/away");
assert_true(stbbr_received(
"<presence id='*'>"
"<presence id='prof_presence_4'>"
"<show>away</show>"
"<status>away</status>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"</presence>"
));
assert_true(prof_output_exact("Status set to away (priority 0)"));
}
void
@@ -64,17 +63,17 @@ presence_away_with_message(void **state)
{
prof_connect();
prof_input("/status set away \"I'm not here for a bit\"");
assert_true(prof_output_exact("Status set to away (priority 0), \"I'm not here for a bit\"."));
prof_input("/away \"I'm not here for a bit\"");
assert_true(stbbr_received(
"<presence id='*'>"
"<presence id='prof_presence_4'>"
"<show>away</show>"
"<status>I'm not here for a bit</status>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"</presence>"
));
assert_true(prof_output_exact("Status set to away (priority 0), \"I'm not here for a bit\"."));
}
void
@@ -82,17 +81,16 @@ presence_xa(void **state)
{
prof_connect();
prof_input("/status set xa");
assert_true(prof_output_exact("Status set to xa (priority 0), \"xa\"."));
prof_input("/xa");
assert_true(stbbr_received(
"<presence id='*'>"
"<presence id='prof_presence_4'>"
"<show>xa</show>"
"<status>xa</status>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"</presence>"
));
assert_true(prof_output_exact("Status set to xa (priority 0)"));
}
void
@@ -100,17 +98,17 @@ presence_xa_with_message(void **state)
{
prof_connect();
prof_input("/status set xa \"Gone to the shops\"");
assert_true(prof_output_exact("Status set to xa (priority 0), \"Gone to the shops\"."));
prof_input("/xa \"Gone to the shops\"");
assert_true(stbbr_received(
"<presence id='*'>"
"<presence id='prof_presence_4'>"
"<show>xa</show>"
"<status>Gone to the shops</status>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"</presence>"
));
assert_true(prof_output_exact("Status set to xa (priority 0), \"Gone to the shops\"."));
}
void
@@ -118,17 +116,16 @@ presence_dnd(void **state)
{
prof_connect();
prof_input("/status set dnd");
assert_true(prof_output_exact("Status set to dnd (priority 0), \"dnd\"."));
prof_input("/dnd");
assert_true(stbbr_received(
"<presence id='*'>"
"<presence id='prof_presence_4'>"
"<show>dnd</show>"
"<status>dnd</status>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"</presence>"
));
assert_true(prof_output_exact("Status set to dnd (priority 0)"));
}
void
@@ -136,17 +133,17 @@ presence_dnd_with_message(void **state)
{
prof_connect();
prof_input("/status set dnd \"Working\"");
assert_true(prof_output_exact("Status set to dnd (priority 0), \"Working\"."));
prof_input("/dnd \"Working\"");
assert_true(stbbr_received(
"<presence id='*'>"
"<presence id='prof_presence_4'>"
"<show>dnd</show>"
"<status>Working</status>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"</presence>"
));
assert_true(prof_output_exact("Status set to dnd (priority 0), \"Working\"."));
}
void
@@ -154,17 +151,16 @@ presence_chat(void **state)
{
prof_connect();
prof_input("/status set chat");
assert_true(prof_output_exact("Status set to chat (priority 0), \"chat\"."));
prof_input("/chat");
assert_true(stbbr_received(
"<presence id='*'>"
"<presence id='prof_presence_4'>"
"<show>chat</show>"
"<status>chat</status>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"</presence>"
));
assert_true(prof_output_exact("Status set to chat (priority 0)"));
}
void
@@ -172,17 +168,17 @@ presence_chat_with_message(void **state)
{
prof_connect();
prof_input("/status set chat \"Free to talk\"");
assert_true(prof_output_exact("Status set to chat (priority 0), \"Free to talk\"."));
prof_input("/chat \"Free to talk\"");
assert_true(stbbr_received(
"<presence id='*'>"
"<presence id='prof_presence_4'>"
"<show>chat</show>"
"<status>Free to talk</status>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"</presence>"
));
assert_true(prof_output_exact("Status set to chat (priority 0), \"Free to talk\"."));
}
void
@@ -192,14 +188,14 @@ presence_set_priority(void **state)
prof_input("/priority 25");
assert_true(prof_output_exact("Priority set to 25."));
assert_true(stbbr_received(
"<presence id='*'>"
"<presence id='prof_presence_4'>"
"<priority>25</priority>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"</presence>"
));
assert_true(prof_output_exact("Priority set to 25."));
}
void
@@ -208,24 +204,24 @@ presence_includes_priority(void **state)
prof_connect();
prof_input("/priority 25");
assert_true(prof_output_exact("Priority set to 25."));
assert_true(stbbr_received(
"<presence id='*'>"
"<presence id='prof_presence_4'>"
"<priority>25</priority>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"</presence>"
));
assert_true(prof_output_exact("Priority set to 25."));
prof_input("/status set chat \"Free to talk\"");
assert_true(prof_output_exact("Status set to chat (priority 25), \"Free to talk\"."));
prof_input("/chat \"Free to talk\"");
assert_true(stbbr_received(
"<presence id='*'>"
"<presence id='prof_presence_5'>"
"<priority>25</priority>"
"<show>chat</show>"
"<status>Free to talk</status>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"</presence>"
));
assert_true(prof_output_exact("Status set to chat (priority 25), \"Free to talk\"."));
}
void
@@ -233,26 +229,26 @@ presence_keeps_status(void **state)
{
prof_connect();
prof_input("/status set chat \"Free to talk\"");
assert_true(prof_output_exact("Status set to chat (priority 0), \"Free to talk\"."));
prof_input("/chat \"Free to talk\"");
assert_true(stbbr_received(
"<presence id='*'>"
"<presence id='prof_presence_4'>"
"<show>chat</show>"
"<status>Free to talk</status>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"</presence>"
));
assert_true(prof_output_exact("Status set to chat (priority 0), \"Free to talk\"."));
prof_input("/priority 25");
assert_true(prof_output_exact("Priority set to 25."));
assert_true(stbbr_received(
"<presence id='*'>"
"<presence id='prof_presence_5'>"
"<show>chat</show>"
"<status>Free to talk</status>"
"<priority>25</priority>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"</presence>"
));
assert_true(prof_output_exact("Priority set to 25."));
}
void

View File

@@ -4,6 +4,7 @@
#include <string.h>
#include <stabber.h>
#include <expect.h>
#include "proftest.h"
@@ -30,9 +31,8 @@ send_receipt_request(void **state)
prof_connect();
// Register disco#info response for capabilities query (receipts support)
stbbr_for_query("http://jabber.org/protocol/disco#info",
"<iq from='buddy1@localhost/laptop' to='stabber@localhost' id='*' type='result'>"
stbbr_for_id("prof_caps_4",
"<iq from='buddy1@localhost/laptop' to='stabber@localhost' id='prof_caps_4' type='result'>"
"<query xmlns='http://jabber.org/protocol/disco#info' node='http://profanity-im.github.io#hAkb1xZdJV9BQpgGNw8zG5Xsals='>"
"<identity category='client' name='Profanity 0.5.0' type='console'/>"
"<feature var='urn:xmpp:receipts'/>"

View File

@@ -4,14 +4,15 @@
#include <string.h>
#include <stabber.h>
#include <expect.h>
#include "proftest.h"
void
rooms_query(void **state)
{
stbbr_for_query("http://jabber.org/protocol/disco#items",
"<iq id='*' type='result' to='stabber@localhost/profanity' from='conference.localhost'>"
stbbr_for_id("prof_confreq_4",
"<iq id='prof_confreq_4' type='result' to='stabber@localhost/profanity' from='conference.localhost'>"
"<query xmlns='http://jabber.org/protocol/disco#items'>"
"<item jid='chatroom@conference.localhost' name='A chat room'/>"
"<item jid='hangout@conference.localhost' name='Another chat room'/>"
@@ -26,8 +27,8 @@ rooms_query(void **state)
assert_true(prof_output_exact("chatroom@conference.localhost (A chat room)"));
assert_true(prof_output_exact("hangout@conference.localhost (Another chat room)"));
assert_true(stbbr_received(
"<iq id='*' to='conference.localhost' type='get'>"
assert_true(stbbr_last_received(
"<iq id='prof_confreq_4' to='conference.localhost' type='get'>"
"<query xmlns='http://jabber.org/protocol/disco#items'/>"
"</iq>"
));

View File

@@ -4,6 +4,7 @@
#include <string.h>
#include <stabber.h>
#include <expect.h>
#include "proftest.h"

View File

@@ -4,6 +4,7 @@
#include <string.h>
#include <stabber.h>
#include <expect.h>
#include "proftest.h"

View File

@@ -25,8 +25,6 @@
#include "database.h"
db_backend_t* active_db_backend = NULL;
gboolean
log_database_init(ProfAccount* account)
{
@@ -52,27 +50,3 @@ void
log_database_close(void)
{
}
GSList*
log_database_verify_integrity(const gchar* const contact_barejid)
{
return NULL;
}
void
integrity_issue_free(integrity_issue_t* issue)
{
if (issue) {
g_free(issue->file);
g_free(issue->message);
g_free(issue);
}
}
db_backend_t*
db_backend_sqlite(void)
{
return NULL;
}
db_backend_t*
db_backend_flatfile(void)
{
return NULL;
}

View File

@@ -26,7 +26,7 @@
#include "log.h"
void
log_init(log_level_t filter, const char* const log_file)
log_init(log_level_t filter, char* log_file)
{
}
log_level_t

View File

@@ -156,11 +156,6 @@ connection_get_profanity_identifier(void)
return "profident";
}
void
connection_debug_print_features()
{
}
jabber_conn_status_t
connection_register(const char* const altdomain, int port, const char* const tls_policy,
const char* const username, const char* const password)