LCOV - code coverage report
Current view: top level - src/app - session_store.c (source / functions) Coverage Total Hit
Test: coverage-functional.info Lines: 93.8 % 211 198
Test Date: 2026-05-06 13:17:08 Functions: 100.0 % 16 16

            Line data    Source code
       1              : /* SPDX-License-Identifier: GPL-3.0-or-later */
       2              : /* Copyright 2026 Peter Csaszar */
       3              : 
       4              : /**
       5              :  * @file app/session_store.c
       6              :  * @brief Multi-DC session persistence (v2).
       7              :  *
       8              :  * Write safety:
       9              :  *   - An exclusive advisory lock (flock LOCK_EX | LOCK_NB) is acquired on
      10              :  *     the session file before every read-modify-write cycle.  A non-blocking
      11              :  *     attempt is used; if the lock is busy we return -1 with a log message
      12              :  *     so the caller can surface "another tg-cli process is using this session".
      13              :  *   - The new content is written to `session.bin.tmp`, fsync'd, then renamed
      14              :  *     atomically over `session.bin`.  This prevents a truncated file on crash
      15              :  *     or disk-full.
      16              :  *   - Reads also take a shared lock (LOCK_SH | LOCK_NB) so they never observe
      17              :  *     a partially-written file.
      18              :  *   - On Windows the lock calls are compiled out (advisory locks are not
      19              :  *     available via flock on MinGW); the atomic-rename pattern still applies.
      20              :  */
      21              : 
      22              : #include "app/session_store.h"
      23              : 
      24              : #include "fs_util.h"
      25              : #include "logger.h"
      26              : #include "platform/path.h"
      27              : #include "raii.h"
      28              : 
      29              : #include <errno.h>
      30              : #include <fcntl.h>
      31              : #include <stdio.h>
      32              : #include <stdlib.h>
      33              : #include <string.h>
      34              : #include <unistd.h>
      35              : 
      36              : #if !defined(_WIN32)
      37              : #  include <sys/file.h>   /* flock(2) */
      38              : #else
      39              : #  include <windows.h>    /* LockFileEx / UnlockFileEx */
      40              : #  include <io.h>         /* _open, _close, _get_osfhandle */
      41              : #  include <share.h>      /* _SH_DENYNO */
      42              : #  include <sys/stat.h>   /* _S_IREAD, _S_IWRITE */
      43              : #endif
      44              : 
      45              : #define STORE_MAGIC      "TGCS"
      46              : /* Current on-disk schema version. Increment (and teach the loader the previous
      47              :  * layout) whenever the header or entry format changes. Older files are
      48              :  * silently upgraded on the next save; newer ("future") files are refused
      49              :  * with a distinct diagnostic so an out-of-date client does not clobber
      50              :  * session state it cannot safely parse. */
      51              : #define STORE_VERSION        2
      52              : /* Historical single-DC layout predating multi-DC support (US-16 landing).
      53              :  *   4 bytes magic "TGCS"
      54              :  *   4 bytes version = 1
      55              :  *   4 bytes dc_id         (int32 LE)
      56              :  *   8 bytes server_salt   (uint64 LE)
      57              :  *   8 bytes session_id    (uint64 LE)
      58              :  * 256 bytes auth_key
      59              :  *
      60              :  * The whole payload is exactly 284 bytes. Retained as a read-only
      61              :  * compatibility path — on load the entry is lifted into the v2 multi-DC
      62              :  * struct, marked as home_dc, and the next successful save() atomically
      63              :  * rewrites the file in v2 format. */
      64              : #define STORE_VERSION_V1     1
      65              : #define STORE_V1_TOTAL_SIZE  284
      66              : #define STORE_HEADER     16                  /* magic+ver+home_dc+count */
      67              : #define STORE_ENTRY_SIZE 276                 /* 4 + 8 + 8 + 256 */
      68              : #define STORE_MAX_SIZE   (STORE_HEADER + SESSION_STORE_MAX_DCS * STORE_ENTRY_SIZE)
      69              : 
      70              : typedef struct {
      71              :     int32_t  dc_id;
      72              :     uint64_t server_salt;
      73              :     uint64_t session_id;
      74              :     uint8_t  auth_key[MTPROTO_AUTH_KEY_SIZE];
      75              : } StoreEntry;
      76              : 
      77              : typedef struct {
      78              :     int32_t    home_dc_id;
      79              :     uint32_t   count;
      80              :     StoreEntry entries[SESSION_STORE_MAX_DCS];
      81              : } StoreFile;
      82              : 
      83              : /* -------------------------------------------------------------------------
      84              :  * Path helpers
      85              :  * ---------------------------------------------------------------------- */
      86              : 
      87         1349 : static char *store_path(void) {
      88         1349 :     const char *cfg = platform_config_dir();
      89         1349 :     if (!cfg) return NULL;
      90         1349 :     char *p = NULL;
      91         1349 :     if (asprintf(&p, "%s/tg-cli/session.bin", cfg) == -1) return NULL;
      92         1349 :     return p;
      93              : }
      94              : 
      95          259 : static char *store_tmp_path(void) {
      96          259 :     const char *cfg = platform_config_dir();
      97          259 :     if (!cfg) return NULL;
      98          259 :     char *p = NULL;
      99          259 :     if (asprintf(&p, "%s/tg-cli/session.bin.tmp", cfg) == -1) return NULL;
     100          259 :     return p;
     101              : }
     102              : 
     103          262 : static int ensure_dir(void) {
     104          262 :     const char *cfg_dir = platform_config_dir();
     105          262 :     if (!cfg_dir) return -1;
     106              :     char dir_path[1024];
     107          262 :     snprintf(dir_path, sizeof(dir_path), "%s/tg-cli", cfg_dir);
     108          262 :     if (fs_mkdir_p(dir_path, 0700) != 0) {
     109            1 :         logger_log(LOG_ERROR, "session_store: cannot create %s", dir_path);
     110            1 :         return -1;
     111              :     }
     112          261 :     return 0;
     113              : }
     114              : 
     115              : /* -------------------------------------------------------------------------
     116              :  * Advisory locking
     117              :  *
     118              :  * Returns an open fd that holds the lock, or -1 on error / busy.
     119              :  * The caller must pass the fd to unlock_file() to release and close it.
     120              :  * POSIX: uses flock(2).
     121              :  * Windows: uses LockFileEx via _get_osfhandle; _open provides the fd.
     122              :  * ---------------------------------------------------------------------- */
     123              : 
     124              : #if !defined(_WIN32)
     125              : 
     126              : /**
     127              :  * @brief Open @p path and acquire an advisory flock.
     128              :  *
     129              :  * @param path   Path to lock (created if absent).
     130              :  * @param how    LOCK_EX for exclusive, LOCK_SH for shared.
     131              :  * @return open fd with lock held, or -1 on failure.
     132              :  */
     133          544 : static int lock_file(const char *path, int how) {
     134              :     /* O_CREAT so the lock file can exist even before first write. */
     135          544 :     int fd = open(path, O_CREAT | O_RDWR, 0600);
     136          544 :     if (fd == -1) {
     137            1 :         logger_log(LOG_ERROR, "session_store: open(%s) failed: %s",
     138            1 :                    path, strerror(errno));
     139            1 :         return -1;
     140              :     }
     141          543 :     if (flock(fd, how | LOCK_NB) == -1) {
     142            1 :         if (errno == EWOULDBLOCK || errno == EAGAIN) {
     143            1 :             logger_log(LOG_ERROR,
     144              :                        "session_store: another tg-cli process is using "
     145              :                        "this session; please close it first");
     146              :         } else {
     147            0 :             logger_log(LOG_ERROR, "session_store: flock failed: %s",
     148            0 :                        strerror(errno));
     149              :         }
     150            1 :         close(fd);
     151            1 :         return -1;
     152              :     }
     153          542 :     return fd;
     154              : }
     155              : 
     156          542 : static void unlock_file(int fd) {
     157          542 :     if (fd >= 0) close(fd);
     158          542 : }
     159              : 
     160              : #else /* _WIN32 — LockFileEx advisory locking */
     161              : 
     162              : static int lock_file(const char *path, int how) {
     163              :     int fd = _open(path,
     164              :                    _O_CREAT | _O_RDWR | _O_BINARY,
     165              :                    _S_IREAD | _S_IWRITE);
     166              :     if (fd == -1) {
     167              :         logger_log(LOG_ERROR, "session_store: _open(%s) failed", path);
     168              :         return -1;
     169              :     }
     170              :     HANDLE h = (HANDLE)_get_osfhandle(fd);
     171              :     OVERLAPPED ov;
     172              :     memset(&ov, 0, sizeof(ov));
     173              :     DWORD flags = LOCKFILE_FAIL_IMMEDIATELY;
     174              :     if (how == LOCK_EX) flags |= LOCKFILE_EXCLUSIVE_LOCK;
     175              :     if (!LockFileEx(h, flags, 0, 1, 0, &ov)) {
     176              :         logger_log(LOG_ERROR,
     177              :                    "session_store: LockFileEx failed (another process may "
     178              :                    "hold the session lock)");
     179              :         _close(fd);
     180              :         return -1;
     181              :     }
     182              :     return fd;
     183              : }
     184              : 
     185              : static void unlock_file(int fd) {
     186              :     if (fd >= 0) {
     187              :         HANDLE h = (HANDLE)_get_osfhandle(fd);
     188              :         OVERLAPPED ov;
     189              :         memset(&ov, 0, sizeof(ov));
     190              :         UnlockFileEx(h, 0, 1, 0, &ov);
     191              :         _close(fd);
     192              :     }
     193              : }
     194              : 
     195              : #endif /* _WIN32 */
     196              : 
     197              : /* -------------------------------------------------------------------------
     198              :  * Serialise / deserialise
     199              :  * ---------------------------------------------------------------------- */
     200              : 
     201              : /* Read the file into `out` if present.  The caller is responsible for holding
     202              :  * a shared lock before calling this function.
     203              :  *
     204              :  * Returns:
     205              :  *   0  on success (file existed and parsed cleanly)
     206              :  *  +1  on "file absent" (caller treats as empty store)
     207              :  *  -1  on corrupt / unsupported
     208              :  */
     209          542 : static int read_file_locked(StoreFile *out) {
     210          542 :     memset(out, 0, sizeof(*out));
     211              : 
     212         1084 :     RAII_STRING char *path = store_path();
     213          542 :     if (!path) return -1;
     214              : 
     215         1084 :     RAII_FILE FILE *f = fopen(path, "rb");
     216          542 :     if (!f) return +1;
     217              : 
     218              :     uint8_t buf[STORE_MAX_SIZE];
     219          542 :     size_t n = fread(buf, 1, sizeof(buf), f);
     220          542 :     if (n < STORE_HEADER) {
     221          142 :         logger_log(LOG_WARN, "session_store: truncated header");
     222          142 :         return -1;
     223              :     }
     224          400 :     if (memcmp(buf, STORE_MAGIC, 4) != 0) {
     225            1 :         logger_log(LOG_WARN, "session_store: bad magic");
     226            1 :         return -1;
     227              :     }
     228              :     int32_t version;
     229          399 :     memcpy(&version, buf + 4, 4);
     230              :     /* Legacy v1 single-DC payload — lift into a v2-shaped in-memory store so
     231              :      * the rest of the code (and the next save) is version-agnostic. The file
     232              :      * on disk is left untouched until an explicit save rewrites it atomically
     233              :      * in v2 format, which preserves the migration's crash-safety: if the
     234              :      * client exits between load and save, the v1 bytes remain usable. */
     235          399 :     if (version == STORE_VERSION_V1) {
     236            8 :         if (n < STORE_V1_TOTAL_SIZE) {
     237            1 :             logger_log(LOG_WARN, "session_store: truncated v1 payload");
     238            1 :             return -1;
     239              :         }
     240              :         int32_t  dc_id;
     241              :         uint64_t server_salt;
     242              :         uint64_t session_id;
     243            7 :         memcpy(&dc_id,       buf + 8,  4);
     244            7 :         memcpy(&server_salt, buf + 12, 8);
     245            7 :         memcpy(&session_id,  buf + 20, 8);
     246            7 :         out->home_dc_id = dc_id;
     247            7 :         out->count      = 1;
     248            7 :         out->entries[0].dc_id       = dc_id;
     249            7 :         out->entries[0].server_salt = server_salt;
     250            7 :         out->entries[0].session_id  = session_id;
     251            7 :         memcpy(out->entries[0].auth_key, buf + 28, MTPROTO_AUTH_KEY_SIZE);
     252            7 :         logger_log(LOG_INFO,
     253              :                    "session_store: migrated v1 file for DC%d "
     254              :                    "(will rewrite as v2 on next save)", dc_id);
     255            7 :         return 0;
     256              :     }
     257          391 :     if (version != STORE_VERSION) {
     258              :         /* Either an out-of-bounds garbage number (classic corruption) or a
     259              :          * *future* version that a newer client wrote.  In the latter case
     260              :          * the safe reaction is to refuse the load and never overwrite — we
     261              :          * ask the operator to upgrade the client instead of silently
     262              :          * clobbering their real session with a freshly-re-authenticated
     263              :          * v2 one. Both paths share the "unsupported version" prefix so
     264              :          * existing corruption-recovery assertions keep matching. */
     265            3 :         if (version > STORE_VERSION) {
     266            2 :             logger_log(LOG_WARN,
     267              :                        "session_store: unsupported version %d "
     268              :                        "— unknown session version, upgrade client",
     269              :                        version);
     270              :         } else {
     271            1 :             logger_log(LOG_WARN,
     272              :                        "session_store: unsupported version %d", version);
     273              :         }
     274            3 :         return -1;
     275              :     }
     276          388 :     memcpy(&out->home_dc_id, buf + 8,  4);
     277          388 :     memcpy(&out->count,      buf + 12, 4);
     278          388 :     if (out->count > SESSION_STORE_MAX_DCS) {
     279            1 :         logger_log(LOG_WARN, "session_store: count %u too large", out->count);
     280            1 :         return -1;
     281              :     }
     282          387 :     size_t need = STORE_HEADER + (size_t)out->count * STORE_ENTRY_SIZE;
     283          387 :     if (n < need) {
     284            1 :         logger_log(LOG_WARN, "session_store: truncated body");
     285            1 :         return -1;
     286              :     }
     287          902 :     for (uint32_t i = 0; i < out->count; i++) {
     288          516 :         size_t off = STORE_HEADER + (size_t)i * STORE_ENTRY_SIZE;
     289          516 :         memcpy(&out->entries[i].dc_id,       buf + off + 0,   4);
     290          516 :         memcpy(&out->entries[i].server_salt, buf + off + 4,   8);
     291          516 :         memcpy(&out->entries[i].session_id,  buf + off + 12,  8);
     292          516 :         memcpy( out->entries[i].auth_key,    buf + off + 20,  256);
     293              :     }
     294          386 :     return 0;
     295              : }
     296              : 
     297              : /**
     298              :  * @brief Atomically write @p st to the session file.
     299              :  *
     300              :  * Writes to a sibling .tmp file, fsync's it, then renames it over the real
     301              :  * path.  The rename is atomic on POSIX.  The caller must hold an exclusive
     302              :  * lock before calling this function.
     303              :  */
     304          259 : static int write_file_atomic(const StoreFile *st) {
     305          518 :     RAII_STRING char *path     = store_path();
     306          518 :     RAII_STRING char *tmp_path = store_tmp_path();
     307          259 :     if (!path || !tmp_path) return -1;
     308              : 
     309              :     /* Build the serialised buffer. */
     310              :     uint8_t buf[STORE_MAX_SIZE];
     311          259 :     memset(buf, 0, sizeof(buf));
     312          259 :     memcpy(buf, STORE_MAGIC, 4);
     313          259 :     int32_t version = STORE_VERSION;
     314          259 :     memcpy(buf + 4,  &version,        4);
     315          259 :     memcpy(buf + 8,  &st->home_dc_id, 4);
     316          259 :     memcpy(buf + 12, &st->count,      4);
     317          579 :     for (uint32_t i = 0; i < st->count; i++) {
     318          320 :         size_t off = STORE_HEADER + (size_t)i * STORE_ENTRY_SIZE;
     319          320 :         memcpy(buf + off + 0,   &st->entries[i].dc_id,       4);
     320          320 :         memcpy(buf + off + 4,   &st->entries[i].server_salt, 8);
     321          320 :         memcpy(buf + off + 12,  &st->entries[i].session_id,  8);
     322          320 :         memcpy(buf + off + 20,   st->entries[i].auth_key,    256);
     323              :     }
     324          259 :     size_t total = STORE_HEADER + (size_t)st->count * STORE_ENTRY_SIZE;
     325              : 
     326              :     /* Write to tmp. */
     327          259 :     int tfd = open(tmp_path, O_WRONLY | O_CREAT | O_TRUNC, 0600);
     328          259 :     if (tfd == -1) {
     329            2 :         logger_log(LOG_ERROR, "session_store: cannot open tmp %s: %s",
     330            2 :                    tmp_path, strerror(errno));
     331            2 :         return -1;
     332              :     }
     333              : 
     334          257 :     ssize_t n = write(tfd, buf, total);
     335          257 :     if (n < 0 || (size_t)n != total) {
     336            0 :         logger_log(LOG_ERROR, "session_store: short write to %s", tmp_path);
     337            0 :         close(tfd);
     338            0 :         unlink(tmp_path);
     339            0 :         return -1;
     340              :     }
     341              : 
     342              : #if !defined(_WIN32)
     343          257 :     if (fsync(tfd) != 0) {
     344            0 :         logger_log(LOG_WARN, "session_store: fsync(%s) failed: %s",
     345            0 :                    tmp_path, strerror(errno));
     346              :         /* Non-fatal — proceed with rename. */
     347              :     }
     348              : #endif
     349          257 :     close(tfd);
     350              : 
     351              :     /* Set permissions on the tmp file before rename. */
     352          257 :     if (fs_ensure_permissions(tmp_path, 0600) != 0) {
     353            0 :         logger_log(LOG_WARN, "session_store: cannot set 0600 on %s", tmp_path);
     354              :     }
     355              : 
     356              :     /* Atomic rename. */
     357          257 :     if (rename(tmp_path, path) != 0) {
     358            0 :         logger_log(LOG_ERROR, "session_store: rename(%s, %s) failed: %s",
     359            0 :                    tmp_path, path, strerror(errno));
     360            0 :         unlink(tmp_path);
     361            0 :         return -1;
     362              :     }
     363          257 :     return 0;
     364              : }
     365              : 
     366              : /* -------------------------------------------------------------------------
     367              :  * Internal entry helpers
     368              :  * ---------------------------------------------------------------------- */
     369              : 
     370              : /* Find the index of @p dc_id in the store, or -1 if absent. */
     371          532 : static int find_entry(const StoreFile *st, int dc_id) {
     372          612 :     for (uint32_t i = 0; i < st->count; i++) {
     373          452 :         if (st->entries[i].dc_id == dc_id) return (int)i;
     374              :     }
     375          160 :     return -1;
     376              : }
     377              : 
     378          259 : static void populate_entry(StoreEntry *e, int dc_id, const MtProtoSession *s) {
     379          259 :     e->dc_id       = dc_id;
     380          259 :     e->server_salt = s->server_salt;
     381          259 :     e->session_id  = s->session_id;
     382          259 :     memcpy(e->auth_key, s->auth_key, MTPROTO_AUTH_KEY_SIZE);
     383          259 : }
     384              : 
     385          270 : static void apply_entry(MtProtoSession *s, const StoreEntry *e) {
     386          270 :     s->server_salt  = e->server_salt;
     387          270 :     s->session_id   = e->session_id;
     388          270 :     memcpy(s->auth_key, e->auth_key, MTPROTO_AUTH_KEY_SIZE);
     389          270 :     s->has_auth_key = 1;
     390          270 :     s->seq_no       = 0;
     391          270 :     s->last_msg_id  = 0;
     392          270 : }
     393              : 
     394              : /* -------------------------------------------------------------------------
     395              :  * Upsert (read-modify-write under exclusive lock)
     396              :  * ---------------------------------------------------------------------- */
     397              : 
     398          262 : static int upsert(int dc_id, const MtProtoSession *s, int set_home) {
     399          262 :     if (!s || !s->has_auth_key) return -1;
     400              : 
     401          262 :     if (ensure_dir() != 0) return -1;
     402              : 
     403          522 :     RAII_STRING char *path = store_path();
     404          261 :     if (!path) return -1;
     405              : 
     406              :     /* Acquire exclusive lock. */
     407          261 :     int lock_fd = lock_file(path, LOCK_EX);
     408          261 :     if (lock_fd == -1) return -1;
     409              : 
     410              :     StoreFile st;
     411          260 :     int rc = read_file_locked(&st);
     412          260 :     if (rc < 0) {
     413              :         /* Corrupt: start fresh. The user is re-authenticating anyway. */
     414          139 :         memset(&st, 0, sizeof(st));
     415              :     }
     416              : 
     417          260 :     int idx = find_entry(&st, dc_id);
     418          260 :     if (idx < 0) {
     419          158 :         if (st.count >= SESSION_STORE_MAX_DCS) {
     420            1 :             logger_log(LOG_ERROR,
     421              :                        "session_store: no slot left for DC%d", dc_id);
     422            1 :             unlock_file(lock_fd);
     423            1 :             return -1;
     424              :         }
     425          157 :         idx = (int)st.count++;
     426              :     }
     427          259 :     populate_entry(&st.entries[idx], dc_id, s);
     428              : 
     429          259 :     if (set_home || st.home_dc_id == 0) {
     430          237 :         st.home_dc_id = dc_id;
     431              :     }
     432              : 
     433          259 :     int write_rc = write_file_atomic(&st);
     434              : 
     435          259 :     unlock_file(lock_fd);
     436              : 
     437          259 :     if (write_rc != 0) return -1;
     438              : 
     439          257 :     logger_log(LOG_INFO,
     440              :                "session_store: persisted DC%d (home=%d, count=%u)",
     441              :                dc_id, st.home_dc_id, st.count);
     442          257 :     return 0;
     443              : }
     444              : 
     445              : /* -------------------------------------------------------------------------
     446              :  * Public API
     447              :  * ---------------------------------------------------------------------- */
     448              : 
     449          238 : int session_store_save(const MtProtoSession *s, int dc_id) {
     450          238 :     return upsert(dc_id, s, /*set_home=*/1);
     451              : }
     452              : 
     453           24 : int session_store_save_dc(int dc_id, const MtProtoSession *s) {
     454           24 :     return upsert(dc_id, s, /*set_home=*/0);
     455              : }
     456              : 
     457          257 : int session_store_load(MtProtoSession *s, int *dc_id) {
     458          257 :     if (!s || !dc_id) return -1;
     459              : 
     460          514 :     RAII_STRING char *path = store_path();
     461          257 :     if (!path) return -1;
     462              : 
     463              :     /* Shared lock — wait for any in-progress write to finish. */
     464          257 :     int lock_fd = lock_file(path, LOCK_SH);
     465          257 :     if (lock_fd == -1) return -1;
     466              : 
     467              :     StoreFile st;
     468          256 :     int rc = read_file_locked(&st);
     469              : 
     470          256 :     unlock_file(lock_fd);
     471              : 
     472          256 :     if (rc != 0) return -1;
     473          246 :     if (st.count == 0 || st.home_dc_id == 0) return -1;
     474              : 
     475          246 :     int idx = find_entry(&st, st.home_dc_id);
     476          246 :     if (idx < 0) {
     477            1 :         logger_log(LOG_WARN,
     478              :                    "session_store: home DC%d has no entry", st.home_dc_id);
     479            1 :         return -1;
     480              :     }
     481          245 :     apply_entry(s, &st.entries[idx]);
     482          245 :     *dc_id = st.home_dc_id;
     483          245 :     logger_log(LOG_INFO, "session_store: loaded home DC%d", *dc_id);
     484          245 :     return 0;
     485              : }
     486              : 
     487           26 : int session_store_load_dc(int dc_id, MtProtoSession *s) {
     488           26 :     if (!s) return -1;
     489              : 
     490           52 :     RAII_STRING char *path = store_path();
     491           26 :     if (!path) return -1;
     492              : 
     493           26 :     int lock_fd = lock_file(path, LOCK_SH);
     494           26 :     if (lock_fd == -1) return -1;
     495              : 
     496              :     StoreFile st;
     497           26 :     int rc = read_file_locked(&st);
     498              : 
     499           26 :     unlock_file(lock_fd);
     500              : 
     501           26 :     if (rc != 0) return -1;
     502              : 
     503           26 :     int idx = find_entry(&st, dc_id);
     504           26 :     if (idx < 0) return -1;
     505              : 
     506           25 :     apply_entry(s, &st.entries[idx]);
     507           25 :     logger_log(LOG_INFO, "session_store: loaded DC%d", dc_id);
     508           25 :     return 0;
     509              : }
     510              : 
     511            4 : void session_store_clear(void) {
     512            8 :     RAII_STRING char *path = store_path();
     513            4 :     if (!path) return;
     514            4 :     if (remove(path) == 0)
     515            4 :         logger_log(LOG_INFO, "session_store: cleared");
     516              : }
        

Generated by: LCOV version 2.0-1