LCOV - code coverage report
Current view: top level - src/infrastructure - cache_store.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 89.6 % 77 69
Test Date: 2026-04-20 19:54:22 Functions: 100.0 % 7 7

            Line data    Source code
       1              : /* SPDX-License-Identifier: GPL-3.0-or-later */
       2              : /* Copyright 2026 Peter Csaszar */
       3              : 
       4              : #include "cache_store.h"
       5              : #include "fs_util.h"
       6              : #include "platform/path.h"
       7              : #include "raii.h"
       8              : #include "logger.h"
       9              : #include <stdio.h>
      10              : #include <stdlib.h>
      11              : #include <string.h>
      12              : #include <dirent.h>
      13              : 
      14              : /** @brief Returns a heap-allocated path for a cache entry. Caller must free. */
      15           26 : char *cache_path(const char *category, const char *key) {
      16           26 :     const char *cache_base = platform_cache_dir();
      17           26 :     if (!cache_base) return NULL;
      18           26 :     char *path = NULL;
      19           26 :     if (asprintf(&path, "%s/tg-cli/%s/%s",
      20              :                  cache_base, category, key) == -1)
      21            0 :         return NULL;
      22           26 :     return path;
      23              : }
      24              : 
      25           12 : int cache_exists(const char *category, const char *key) {
      26           24 :     RAII_STRING char *path = cache_path(category, key);
      27           12 :     if (!path) return 0;
      28           12 :     RAII_FILE FILE *fp = fopen(path, "r");
      29           12 :     return fp != NULL;
      30              : }
      31              : 
      32            8 : int cache_save(const char *category, const char *key,
      33              :                const char *content, size_t len) {
      34            8 :     const char *cache_base = platform_cache_dir();
      35            8 :     if (!cache_base) return -1;
      36              : 
      37              :     /* Build directory path: ~/.cache/tg-cli/<category>/
      38              :      * key may contain slashes (e.g. "chat_id/msg_id") — create parent dirs. */
      39           16 :     RAII_STRING char *full = cache_path(category, key);
      40            8 :     if (!full) return -1;
      41              : 
      42              :     /* Find last '/' to isolate directory portion */
      43            8 :     RAII_STRING char *dir = strdup(full);
      44            8 :     if (!dir) return -1;
      45            8 :     char *slash = strrchr(dir, '/');
      46            8 :     if (slash) *slash = '\0';
      47              : 
      48            8 :     if (fs_mkdir_p(dir, 0700) != 0) {
      49            1 :         logger_log(LOG_ERROR, "Failed to create cache directory %s", dir);
      50            1 :         return -1;
      51              :     }
      52              : 
      53           14 :     RAII_FILE FILE *fp = fopen(full, "w");
      54            7 :     if (!fp) {
      55            0 :         logger_log(LOG_ERROR, "Failed to open cache file for writing: %s", full);
      56            0 :         return -1;
      57              :     }
      58              : 
      59            7 :     if (fwrite(content, 1, len, fp) != len) {
      60            0 :         logger_log(LOG_ERROR, "Failed to write cache file: %s", full);
      61            0 :         return -1;
      62              :     }
      63              : 
      64            7 :     logger_log(LOG_DEBUG, "Cached %s/%s (%zu bytes)", category, key, len);
      65            7 :     return 0;
      66              : }
      67              : 
      68              : /* ── Shared file I/O helper ──────────────────────────────────────────── */
      69              : 
      70            4 : static char *load_file(const char *path) {
      71            8 :     RAII_FILE FILE *fp = fopen(path, "r");
      72            4 :     if (!fp) return NULL;
      73            2 :     if (fseek(fp, 0, SEEK_END) != 0) return NULL;
      74            2 :     long size = ftell(fp);
      75            2 :     if (size <= 0) return NULL;
      76            2 :     rewind(fp);
      77            2 :     char *buf = malloc((size_t)size + 1);
      78            2 :     if (!buf) return NULL;
      79            2 :     if ((long)fread(buf, 1, (size_t)size, fp) != size) { free(buf); return NULL; }
      80            2 :     buf[size] = '\0';
      81            2 :     return buf;
      82              : }
      83              : 
      84            4 : char *cache_load(const char *category, const char *key) {
      85            8 :     RAII_STRING char *path = cache_path(category, key);
      86            4 :     if (!path) {
      87            0 :         logger_log(LOG_ERROR, "cache_load: failed to determine cache path for %s/%s",
      88              :                    category ? category : "(null)", key ? key : "(null)");
      89            0 :         return NULL;
      90              :     }
      91            4 :     char *data = load_file(path);
      92            4 :     if (!data) {
      93            2 :         logger_log(LOG_DEBUG, "cache_load: no data at %s", path);
      94              :     }
      95            4 :     return data;
      96              : }
      97              : 
      98              : /* ── Stale entry eviction ────────────────────────────────────────────── */
      99              : 
     100            5 : static int cmp_str_evict(const void *a, const void *b) {
     101            5 :     return strcmp(*(const char **)a, *(const char **)b);
     102              : }
     103              : 
     104            3 : void cache_evict_stale(const char *category,
     105              :                        const char **keep_keys, int keep_count) {
     106            3 :     const char *cache_base = platform_cache_dir();
     107            4 :     if (!cache_base) return;
     108              : 
     109            3 :     RAII_STRING char *dir = NULL;
     110            3 :     if (asprintf(&dir, "%s/tg-cli/%s", cache_base, category) == -1)
     111            0 :         return;
     112              : 
     113              :     /* Sort a local copy of keys for bsearch */
     114            3 :     const char **sorted = malloc((size_t)keep_count * sizeof(char *));
     115            3 :     if (!sorted) return;
     116            3 :     memcpy(sorted, keep_keys, (size_t)keep_count * sizeof(char *));
     117            3 :     qsort(sorted, (size_t)keep_count, sizeof(char *), cmp_str_evict);
     118              : 
     119            6 :     RAII_DIR DIR *d = opendir(dir);
     120            3 :     if (!d) { free(sorted); return; }
     121              : 
     122              :     struct dirent *ent;
     123           11 :     while ((ent = readdir(d)) != NULL) {
     124            9 :         const char *name = ent->d_name;
     125            9 :         if (name[0] == '.') continue;  /* skip . and .. */
     126            5 :         if (!bsearch(&name, sorted, (size_t)keep_count,
     127              :                      sizeof(char *), cmp_str_evict)) {
     128            4 :             RAII_STRING char *path = NULL;
     129            4 :             if (asprintf(&path, "%s/%s", dir, name) != -1) {
     130            4 :                 remove(path);
     131            4 :                 logger_log(LOG_DEBUG, "Evicted stale cache entry: %s/%s",
     132              :                            category, name);
     133              :             }
     134              :         }
     135              :     }
     136            2 :     free(sorted);
     137              : }
        

Generated by: LCOV version 2.0-1