LCOV - code coverage report
Current view: top level - src/infrastructure - updates_state_store.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 81.2 % 64 52
Test Date: 2026-04-20 19:54:22 Functions: 100.0 % 3 3

            Line data    Source code
       1              : /* SPDX-License-Identifier: GPL-3.0-or-later */
       2              : /* Copyright 2026 Peter Csaszar */
       3              : 
       4              : /**
       5              :  * @file infrastructure/updates_state_store.c
       6              :  * @brief Persist UpdatesState to ~/.cache/tg-cli/updates.state (INI format).
       7              :  */
       8              : 
       9              : #include "updates_state_store.h"
      10              : #include "fs_util.h"
      11              : #include "logger.h"
      12              : #include "platform/path.h"
      13              : 
      14              : #include <stdio.h>
      15              : #include <string.h>
      16              : #include <sys/stat.h>
      17              : #include <errno.h>
      18              : 
      19              : /** Maximum path length used internally. */
      20              : #define STATE_PATH_MAX 2048
      21              : 
      22              : /** Build the full path to the state file into @p buf (size STATE_PATH_MAX). */
      23            7 : static int build_state_path(char *buf) {
      24            7 :     const char *cache_base = platform_cache_dir();
      25            7 :     if (!cache_base) return -1;
      26            7 :     int n = snprintf(buf, STATE_PATH_MAX, "%s/tg-cli/updates.state", cache_base);
      27            7 :     if (n <= 0 || n >= STATE_PATH_MAX) return -1;
      28            7 :     return 0;
      29              : }
      30              : 
      31            4 : int updates_state_load(UpdatesState *out) {
      32            4 :     if (!out) return -1;
      33              : 
      34              :     char path[STATE_PATH_MAX];
      35            4 :     if (build_state_path(path) != 0) return -1;
      36              : 
      37            4 :     FILE *fp = fopen(path, "r");
      38            4 :     if (!fp) {
      39              :         /* Missing file is not an error — caller falls back to getState. */
      40            1 :         return -1;
      41              :     }
      42              : 
      43            3 :     UpdatesState tmp = {0};
      44              :     char line[256];
      45            3 :     int fields = 0;
      46              : 
      47           15 :     while (fgets(line, sizeof(line), fp)) {
      48              :         /* Strip trailing newline */
      49           13 :         char *nl = strchr(line, '\n');
      50           13 :         if (nl) *nl = '\0';
      51              : 
      52              :         /* Skip comments and blank lines */
      53           13 :         if (line[0] == '#' || line[0] == '\0') continue;
      54              : 
      55              :         char key[64];
      56              :         long long val;
      57           11 :         if (sscanf(line, "%63[^=]=%lld", key, &val) != 2) {
      58            1 :             fclose(fp);
      59            1 :             logger_log(LOG_WARN, "updates_state_load: malformed line: %s", line);
      60            1 :             return -2;
      61              :         }
      62              : 
      63           10 :         if (strcmp(key, "pts") == 0)  { tmp.pts  = (int32_t)val; fields++; }
      64            8 :         else if (strcmp(key, "qts") == 0)  { tmp.qts  = (int32_t)val; fields++; }
      65            6 :         else if (strcmp(key, "date") == 0) { tmp.date = (int64_t)val; fields++; }
      66            4 :         else if (strcmp(key, "seq") == 0)  { tmp.seq  = (int32_t)val; fields++; }
      67              :         /* unread_count is informational only — not required. */
      68            2 :         else if (strcmp(key, "unread_count") == 0) {
      69            2 :             tmp.unread_count = (int32_t)val;
      70              :         }
      71              :     }
      72              : 
      73            2 :     fclose(fp);
      74              : 
      75              :     /* Require at least pts/qts/date/seq to be present. */
      76            2 :     if (fields < 4) {
      77            0 :         logger_log(LOG_WARN, "updates_state_load: incomplete state file (%d/4 fields)", fields);
      78            0 :         return -2;
      79              :     }
      80              : 
      81            2 :     *out = tmp;
      82            2 :     return 0;
      83              : }
      84              : 
      85            3 : int updates_state_save(const UpdatesState *st) {
      86            3 :     if (!st) return -1;
      87              : 
      88              :     char path[STATE_PATH_MAX];
      89            3 :     if (build_state_path(path) != 0) return -1;
      90              : 
      91              :     /* Ensure the directory exists with mode 0700. */
      92              :     char dir[STATE_PATH_MAX];
      93            3 :     snprintf(dir, sizeof(dir), "%s", path);
      94            3 :     char *slash = strrchr(dir, '/');
      95            3 :     if (slash) *slash = '\0';
      96            3 :     if (fs_mkdir_p(dir, 0700) != 0) {
      97            0 :         logger_log(LOG_ERROR, "updates_state_save: cannot create dir %s", dir);
      98            0 :         return -1;
      99              :     }
     100              : 
     101              :     /* Write to a temp file then rename for atomicity.
     102              :      * ".tmp" suffix is 4 chars; allocate extra room to silence -Wformat-truncation. */
     103              :     char tmp_path[STATE_PATH_MAX + 8];
     104            3 :     snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", path);
     105              : 
     106            3 :     FILE *fp = fopen(tmp_path, "w");
     107            3 :     if (!fp) {
     108            0 :         logger_log(LOG_ERROR, "updates_state_save: fopen %s: %s",
     109            0 :                    tmp_path, strerror(errno));
     110            0 :         return -1;
     111              :     }
     112              : 
     113              :     /* Set permissions to 0600 before writing any data. */
     114            3 :     if (fchmod(fileno(fp), 0600) != 0) {
     115            0 :         logger_log(LOG_WARN, "updates_state_save: fchmod failed: %s", strerror(errno));
     116              :         /* Non-fatal — continue. */
     117              :     }
     118              : 
     119            3 :     fprintf(fp, "# tg-cli updates state — do not edit by hand\n");
     120            3 :     fprintf(fp, "pts=%d\n",          (int)st->pts);
     121            3 :     fprintf(fp, "qts=%d\n",          (int)st->qts);
     122            3 :     fprintf(fp, "date=%lld\n",        (long long)st->date);
     123            3 :     fprintf(fp, "seq=%d\n",          (int)st->seq);
     124            3 :     fprintf(fp, "unread_count=%d\n", (int)st->unread_count);
     125              : 
     126            3 :     if (fclose(fp) != 0) {
     127            0 :         logger_log(LOG_ERROR, "updates_state_save: fclose failed: %s", strerror(errno));
     128            0 :         return -1;
     129              :     }
     130              : 
     131            3 :     if (rename(tmp_path, path) != 0) {
     132            0 :         logger_log(LOG_ERROR, "updates_state_save: rename failed: %s", strerror(errno));
     133            0 :         return -1;
     134              :     }
     135              : 
     136            3 :     return 0;
     137              : }
        

Generated by: LCOV version 2.0-1