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 : }
|